The check before a release
A schema is recorded at a release. Later it is changed, and two questions are asked of the change before it ships.
pnpm add @niscorp/strata @niscorp/prism zodA format, and its schema
A button’s props are a kind of document, and a Zod schema says what one may hold. The sequence has no migrations yet: this is the first release.
snapshotOf takes the schema of every kind as JSON Schema, with the version the sequence is at. That is the record of what was released: snapshotText writes it as the text of a file, and you commit the file.
import { z } from 'zod';
import { evaluate } from '@niscorp/prism';
import { createUpgrader, defineSequence } from '@niscorp/strata';
import { checkCorpus, compareSnapshot, snapshotOf } from '@niscorp/strata/check';
const kind = 'theatre.buttons/button';
const released = defineSequence({ id: 'theatre.buttons', documents: { button: {} }, migrations: [] });
const Button = z.object({ text: z.string(), tone: z.enum(['primary', 'quiet']).optional() }).strict();
const recorded = snapshotOf(released, { [kind]: Button });
console.log('on record: version', recorded.version, 'of', Object.keys(recorded.kinds));Buttons that were really stored
The second thing to keep is a set of documents taken from real use, each with the stamp it was stored at. A schema says what is allowed. These say what is out there.
const stamp = { 'theatre.buttons': 0 };
const stored = [
{ id: 'tonight/book', kind, stamp, document: { text: 'Book seats', tone: 'primary' } },
{ id: 'tonight/all', kind, stamp, document: { text: 'All shows', tone: 'quiet' } },
{ id: 'ticket/show', kind, stamp, document: { text: 'Show my ticket' } },
];Change the schema, and nothing else
Somebody renames tone to variant in the schema and writes no migration.
compareSnapshot says the schema changed and what moved. The sequence is still at the version on record, which is what makes it a fault: a schema may only change together with a migration.
const Changed = z.object({ text: z.string(), variant: z.enum(['filled', 'plain']).optional() }).strict();
const bare = compareSnapshot(recorded, snapshotOf(released, { [kind]: Changed }));
if (bare.status === 'changed') console.log('moved:', bare.changes.flatMap((change) => change.lines));Ask the stored buttons
checkCorpus brings every stored document forward from its stamp and holds it against the schema as it is now. With no migration, the two buttons that say tone fail.
const asReleased = await createUpgrader([released], { transform: evaluate });
const before = await checkCorpus(asReleased, { [kind]: Changed }, stored);
console.log('without a migration:', before.passed, 'passes, and these fail:');
for (const failure of before.failures) console.log(' ', failure.id, failure.reason);Append the migration
The same change with its migration: a button that has a tone gets a variant in its place. The sequence is one version on, and every stored button passes once it is brought forward.
const button = { $ref: '$.document' };
const variant = {
$case: {
branches: [{ when: { $eq: [{ $var: 'tone' }, 'primary'] }, then: 'filled' }],
else: 'plain',
},
};
const toneToVariant = {
$case: {
branches: [
{
when: { $has: { from: button, path: ['tone'] } },
then: {
$renameKeys: {
from: { $update: { from: button, path: ['tone'], as: 'tone', value: variant } },
map: { tone: 'variant' },
},
},
},
],
else: button,
},
};
const next = defineSequence({
id: 'theatre.buttons',
documents: { button: {} },
migrations: [
{ description: 'tone is now variant', steps: [{ kind: 'document', at: kind, transform: toneToVariant }] },
],
});
const asNext = await createUpgrader([next], { transform: evaluate });
const after = await checkCorpus(asNext, { [kind]: Changed }, stored);
console.log('with it:', after.passed, 'pass and', after.failures.length, 'fail, at version', next.migrations.length);Run it.
What is on record, what moved in the schema, the two stored buttons that fail without a migration and why, and all three passing with it.
npx tsx check.mtson record: version 0 of [ 'theatre.buttons/button' ] moved: [ '- properties.tone', '+ properties.variant' ] without a migration: 1 passes, and these fail: tonight/book Unrecognized key: "tone" tonight/all Unrecognized key: "tone" with it: 3 pass and 0 fail, at version 1