Repository navigation
Handles carry their lineage; typed migrations are passed at open #407
Copy link
Copy link
Description
Problem
- Migrations are untyped.
registerMigration({ from: BookV1.id, to: Book.id, migrate })takesTypeIds, andmigratereceives and returnsRecord<string, unknown>. The one place two schema versions meet is the one place with no type help: a migration that forgets a new required field is caught only whenmigrateAll()validates its output. - The handle doesn't carry
nameormigratesFrom. EverydefineType()and every manifest entry repeats them beside the spread handle ({ ...Book, name: 'Book', migratesFrom: BookV1.id }).migratesFromis part of what a version is.@haverstack/commonsworks around the same gap with its ownCommonsTypewrapper (a handle plusname). - Registration is a side effect the app must remember on every start. The registry lives in memory on each
Stack, so apps end up with a separate "register migrations" function called after everyStack.open(), and a read before it sees an empty registry.
Decision
A handle carries the same fields defineType() takes: id, name, schema and optional migratesFrom. It stays plain data. A migration is a separate typed value built from two handles and passed to Stack.open(). registerMigration() is deleted.
export const Book = typeHandle({
id: 'com.example.reading/book@2',
name: 'Book',
migratesFrom: BookV1, // a handle or a TypeId; stored as the TypeId
schema: { … },
});
export const bookV1ToV2 = migration(BookV1, Book, (c) => ({
...c,
status: c.status === 'done' ? 'finished' : c.status,
})); // c: ContentOf<V1>, must return ContentOf<V2>
const stack = await Stack.open(adapter, { migrations: [bookV1ToV2] });
await stack.defineType(Book);
const manifest = { …, types: [Shelf, BookV1, Book, Review] };Why the handle doesn't carry the migration function too
- Manifests need data. A manifest is sent over the wire (
POST /installs) and a plan holds a frozen copy of it. A handle holding a function would need stripping first, or would rely onJSON.stringifydropping it silently.apps.mdalready says migration functions are app code, not part of a manifest. - It wouldn't remove the startup step. Registering when a handle is first used makes behaviour depend on call order: an untyped read or
migrateAll(Book.baseId)before any typed use wouldn't know the chain. Fixing that needs an explicit "use this handle" call, which isregisterMigration()under another name. - Handles get shared. A handle published so others can read a family (a commons type, say) shouldn't carry one app's migration code.
- The typing doesn't need it.
migration(from, to, fn)takes both handles, sofnis checked asContentOf<V1> → ContentOf<V2>.
Why at open
That's when the app can name everything it needs, and it replaces a call the app had to remember on every start. The registry is complete before the first read, so call order stops being a source of bugs.
Consequences
defineType(Book)and manifests take the handle as-is;nameandmigratesFromare written once.migration(from, to, fn)refuses atowhosemigratesFromisn'tfrom.id, so a migration can't be paired with the wrong handle.- A handle whose
migratesFromhas no registered migration can't be caught indefineType(): an owner applying another app's manifest defines types whose migrations live in that app's process. The check belongs to Typed access through collections, with records that don't fit reported rather than thrown #409, wherecollection()runs in the app's own process.
Actions
-
typeHandle()takes{ id, name, schema, migratesFrom? }, withmigratesFromas a handle or aTypeId, stored as theTypeId.TypeHandle<S>is assignable toDefineTypeOptions. - Add
migration(from, to, fn), returning a typedMigration. AddmigrationstoStackOptions. DeleteregisterMigration(). Keep the duplicate-fromrefusal, at open. -
@haverstack/commons: dropCommonsType; its exports become plain handles withname. - Type tests: a migration returning content missing a required field, or with a value outside an enum, fails to compile.
- Move every test that calls
registerMigration()to the open option (at leaststack.test.ts,change-events.test.ts,install.test.ts,type-handle.test.ts). - Spec:
data-model.md § Type migrations(migrations passed at open) and§ Type handles(what a handle carries). Inapps.md, the line saying migration functions are "registered at every startup withregisterMigration()" becomes "passed toStack.open()". - Changeset:
minorfor@haverstack/coreand@haverstack/commons.
Order
Early in #405: it rewrites every typeHandle() call and every test that registers a migration, so #409's tests are written against the final handle and open options. Independent of #408; both must land before #409.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity