Migrations
When you update your schema, you need to change your schema definition in your TypeScript files, but you also need to specify migrations that describe what changes you’ve made. This helps to double-check that you’ve made the changes you intend to, and also resolves some ambiguities - for example, did you mean to rename a field, or did you add a new field and delete an old field? This is part of what allows StatelyDB’s elastic schema to automatically convert between different schema versions and allow different versions of your clients to coexist.
Declaring Migrations
Whenever you update your schema, you need to declare a migration with
the migrate
function. Just like your schema types, the migration
needs to be exported from your schema’s top level JavaScript module,
and you can use modules to organize them into different
files
however you like.
The migrate
function requires the schema version you’re migrating
from (they start with 1), a human-readable description, and a
function that applies one or more migration commands. You can look up
the current version number of your schema on the
console. Because these migration
declarations have a version associated with them, you don’t need to
delete them after they’ve been applied—you can keep them around as a
record of changes to your schema, or you can clean them out of your
schema files after they’re no longer needed.
import { migrate } from "@stately-cloud/schema";// ... the rest of your schema, other migrations, etc.
// Declare a migration from version 1 to version 2 that describes a// bunch of changes to the schema.export const MyMigration = migrate(1, "An example migration", (m) => { m.addType("NewType"); m.removeType("Url"); m.renameType("Account", "Profile"); m.changeType("Profile", (t) => { // was previously named "Account" t.addField("description"); t.removeField("note"); t.renameField("joinDate", "accountAge"); });});
Migration Commands
Migration commands are the individual instructions in each migration
declaration that tell StatelyDB how your schema’s shape has changed.
When you call stately schema put
, these commands help StatelyDB
understand how to migrate items between versions when reading or
writing them. The available commands are:
AddType
You can declare that you’ve added a new objectType
, itemType
, or enumType
by passing the name of your new type to the addType
migration. This
type must exist in your new schema version and not the old version.
Clients using this new version can read and write using this new type, while older versions won’t see it. StatelyDB will filter this new type out of List/SyncList results for clients on older versions.
export const NewType = itemType("NewType", { keyPath: "/newType-:name", fields: { name: { type: string }, },});
export const AddType = migrate(2, "An add type example", (m) => { m.addType("NewType");});
RemoveType
When you remove a objectType
, itemType
or enumType
, use the
removeType
command and pass the name of the type you have removed.
When you remove a type from your schema, newer schema versions will no longer see this type. The items or fields that reference the removed type are still accessible for older versions of the schema but newer versions of the schema cannot interact with them.
export const ExistingType = itemType("ExistingType", { keyPath: "/et-:id", fields: { id: { type: string }, },});
export const RemoveType = migrate(3, "A remove type example", (m) => { m.removeType("ExistingType");});
RenameType
When you rename a type in your schema, use the renameType
command
to clarify that it’s the same type, and not a new type plus a removed
type. If you reference this type in subsequent migration actions, you
must use the new name.
When you rename a type, the data stored in that type doesn’t change. Clients using older schema versions will use the old name, while clients on newer version will use the new name, but they’re all interacting with the same data.
export const OldType = itemType("OldType", {export const NewType = itemType("NewType", { keyPath: "/t-:name", fields: { name: { type: string }, },});
export const RenameType = migrate(4, "A rename type example", (m) => { m.renameType("OldType", "NewType");});
AddField
When you add a new field to a type, use the addField
command within
a changeType
command to declare the new field.
Clients using newer schema versions can read and write to this added field, while older versions won’t see this field at all. If a client using an older version updates an item that has added fields, those fields will be untouched. In other words, there is no way to replace the whole object—StatelyDB only updates the fields known to the caller’s schema version. If you want to completely replace an item, make sure to delete it first.
Adding required
fields to an existing type must be accompanied
by a readDefault
to ensure newer clients can read items created by
older clients that didn’t know about this field. The provided
readDefault
will be subject to any valid
expression on the
new field.
export const ExistingType = itemType("ExistingType", { keyPath: "/et-:id", fields: { id: { type: string }, name: { type: string, readDefault: "John Doe" }, },});
export const AddField = migrate(5, "An add field example", (m) => { m.changeType("ExistingType", (i) => { i.addField("name"); });});
RemoveField
When you’ve removed a field from a type, use the removeField
command within a changeType
command.
Clients using older schema versions will still see the field, while newer clients cannot interact with the field but will not disturb any data already saved there. If you want to zero out this removed field for older clients, you will need to delete the item first.
Removing required
fields from an existing type must be accompanied
by a readDefault
in the migration command to ensure older clients can
read items created by newer clients that no longer know about this field.
The provided readDefault
will be subject to any valid
expression on
the removed field.
export const ExistingType = itemType("ExistingType", { keyPath: "/et-:id", fields: { id: { type: string }, name: { type: string }, },});
export const RemoveField = migrate(6, "A remove field example", (m) => { m.changeType("ExistingType", (i) => { i.removeField("name", "John Doe"); });});
RenameField
If you’ve renamed a field in a type, use the renameField
command in
a changeType
command, to distinguish a rename from an add and a
remove. Renamed fields keep the same stored data and only change the
name. This means that clients on different schema versions of the
schema will read and write the same data, just with different field
names.
export const ExistingType = itemType("ExistingType", { keyPath: "/et-:id", fields: { id: { type: string }, oldName: { type: string }, newName: { type: string }, },});
export const RenameField = migrate(7, "A rename field example", (m) => { m.changeType("ExistingType", (i) => { i.renameField("oldName", "newName"); });});
ModifyFieldReadDefault
If you’ve changed the default value of a field, by either adding, removing
or modifying the readDefault
value, use the modifyFieldReadDefault
command
within a changeType
command. The provided readDefault
will be subject
to any valid
expression on the field.
Note: You can never add or remove a required
field’s readDefault value,
only modify the existing value. This is because required fields must always
have a value.
export const ExistingType = itemType("ExistingType", {keyPath: "/et-:id",fields: { id: { type: string }, name: { type: string, readDefault: "John Doe" }, name: { type: string, readDefault: "Jane Doe" },},});
export const ModifyFieldReadDefault = migrate( 8, "A modify field readDefault example", (m) => { m.changeType("ExistingType", (i) => { i.modifyFieldReadDefault("name"); }); },);
MarkFieldAsRequired
If you’ve changed a field to be required
use the
markFieldAsRequired
command within a changeType
command.
Additionally, you must provide a readDefault
on the field.
Newer clients will see this readDefault
when reading items created by older
clients that may have set the zero value for this field.
The provided readDefault
will be subject to any valid
expression on the field.
export const ExistingType = itemType("ExistingType", {keyPath: "/et-:id",fields: { id: { type: string }, name: { type: string, required: false }, // required: true is implicit in schema name: { type: string, readDefault: "John Doe" },},});
export const MarkFieldAsRequired = migrate( 9, "A required example", (m) => { m.changeType("ExistingType", (i) => { i.markFieldAsRequired("name"); }); },);
MarkFieldAsNotRequired
If you’ve changed a field to not be required
use the
markFieldAsNotRequired
command with a readDefault
within a changeType
command.
Older clients will see the readDefault
when reading items modified by newer
clients that set the zero value.
This readDefault
will be subject to any valid
expression on the field.
export const ExistingType = itemType("ExistingType", { keyPath: "/et-:id", fields: { id: { type: string }, // required: true is implicit in schema name: { type: string }, name: { type: string, required: false }, },});
export const MarkFieldAsNotRequired = migrate( 10, "A not required example", (m) => { m.changeType("ExistingType", (i) => { i.markFieldAsNotRequired("name", "John Doe"); }); },);