Documents inside documents
A season holds shows and a show holds roles. One migration changes a role, and Strata finds every role wherever it is.
pnpm add @niscorp/strata @niscorp/prism zodA season
One document, three levels deep. A season has a list of shows. A show has a cast, keyed by the part, and each entry is a role.
import { evaluate } from '@niscorp/prism';
import { createUpgrader, defineSequence } from '@niscorp/strata';
const season = {
name: 'Autumn 2026',
shows: [
{
title: 'The Tempest',
cast: { prospero: { actor: 'Alan' }, miranda: { actor: 'Ada' } },
},
{
title: 'Twelfth Night',
cast: { viola: { actor: 'Grace' } },
},
],
};Say where one kind sits inside another
A grammar names its kinds, and for each kind where other documents sit inside it. That is its embeds: a path, and the kind found there.
A path is written with three things. A plain key is one document. [] after a key is every item of a list. * is every value of a record, whatever its key.
const documents = {
season: { embeds: { 'shows[]': 'theatre.season/show' } },
show: { embeds: { 'cast.*': 'theatre.season/role' } },
role: {},
};A migration on the innermost kind
A role’s actor becomes player. The config is written for one role and knows nothing about shows or seasons.
const theatre = defineSequence({
id: 'theatre.season',
documents,
migrations: [
{
description: 'actor is now player',
steps: [
{
kind: 'document',
at: 'theatre.season/role',
transform: { $renameKeys: { from: { $ref: '$.document' }, map: { actor: 'player' } } },
},
],
},
],
});
const upgrader = await createUpgrader([theatre], { transform: evaluate });Find every role
locate is the search Strata does before it runs a step. Told what kind the whole document is, it answers with every document inside it, each with its kind and the path to it. It is useful on its own, to see what a migration will touch.
for (const found of upgrader.locate(season, 'theatre.season/season')) {
console.log(found.kind, found.path.join('.'));
}Upgrade the season
The season is handed over as a whole, with its kind. Strata runs the migration on each role it found, deepest first, and puts each answer back where the role stood.
const read = upgrader.upgrade(season, { kind: 'theatre.season/season', stamp: { 'theatre.season': 0 } });
console.log(JSON.stringify(read.document.shows));Run it.
Every document in the season with its kind and its path, the season itself first, and then the shows with player in every cast.
npx tsx nested.mtstheatre.season/season
theatre.season/show shows.0
theatre.season/role shows.0.cast.prospero
theatre.season/role shows.0.cast.miranda
theatre.season/show shows.1
theatre.season/role shows.1.cast.viola
[{"title":"The Tempest","cast":{"prospero":{"player":"Alan"},"miranda":{"player":"Ada"}}},{"title":"Twelfth Night","cast":{"viola":{"player":"Grace"}}}]