Strata · guide

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.

Install it
pnpm add @niscorp/strata @niscorp/prism zod
Step 1 of 5

A 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.

A format, and its schema
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));
Step 2 of 5

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.

Buttons that were really stored
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' } },
];
Step 3 of 5

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.

Change the schema, and nothing else
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));
Step 4 of 5

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.

Ask the stored buttons
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);
Step 5 of 5

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.

Append the migration
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);
Then

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.

Run it
npx tsx check.mts
It prints
on 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