Skip to content

Migrations

Migrations are what makes your Stately Schema Elastic! They allow you to evolve your schema over time without breaking existing clients. A powerful feature of Stately’s Elastic Schema is that every version of your schema is active all the time. This means that you can read or write items from any version of your schema, and Stately will automatically apply the necessary migrations.

To get started with migrations, use the migrate function in either the same schema file or a separate TypeScript file. The migration function accepts the version you’re migrating from, a human-readable description, and a Migrator function. A Migrator function lets you define any number of actions to take the current schema that Stately knows about to an updated version. You may wish to compose all your actions in one migrate block, or split them up for clarity/documentation. However, the order of actions and the migration blocks are important.

migrate Example

The following examples represent the same migration from version 1 while demonstrating several ways to structure your migrations. These are by no means the only way to structure your migrations. You can mix and match these actions in any order that makes sense for your schema evolution.

One Block

schema.ts
import { migrate } from "@stately-cloud/schema";
// ... your whole schema in the same file ...
export const ExampleMigration = migrate(
1,
"An example migration",
(migrator) => {
migrator.addType("NewType");
migrator.removeType("Url");
migrator.renameType("Account", "Profile");
migrator.changeType("Profile", (i) => {
// was previously named "Account"
i.addField("description");
i.removeField("note");
i.renameField("joinDate", "accountAge");
});
},
);

Separate Blocks

schema.ts
import { migrate } from "@stately-cloud/schema";
// ... your whole schema in the same file ...
export const AddNewType = migrate(1, "Add a new type", (migrator) => {
migrator.addType("NewType");
});
export const RemoveExistingType = migrate(
1,
"Remove a deprecated type",
(migrator) => {
migrator.removeType("Url");
},
);
export const UpdateProfile = migrate(
1,
"Migrate Accounts to Profiles",
(migrator) => {
migrator.renameType("Account", "Profile");
migrator.changeType("Profile", (i) => {
// was previously named "Account"
i.addField("description");
i.removeField("note");
i.renameField("joinDate", "accountAge");
});
},
);

Separate Files

You may also find it useful to move migrations into separate file(s) and order them in an index file.

migrations/1.ts
import { migrate } from "@stately-cloud/schema";
export const AddNewType = migrate(1, "Add a new type", (migrator) => {
migrator.addType("NewType");
});
export const RemoveExistingType = migrate(
1,
"Remove a deprecated type",
(migrator) => {
migrator.removeType("Url");
},
);
export const UpdateProfile = migrate(
1,
"Update Account to be a Profile",
(migrator) => {
migrator.renameType("Account", "Profile");
migrator.changeType("Profile", (i) => {
// was previously named "Account"
i.addField("description");
i.removeField("note");
i.renameField("joinDate", "accountAge");
});
},
);

Now that you have your migrations defined in a separate file, you can order them in an index.ts file.

index.ts
export { * } from "./schema"; // Order is important, this should be first
export { * } from "./migrations/1";

Actions

Actions are instructions that tells Stately how to change the shape of your schema. They are persisted when you call stately schema put and instruct Stately how to migrate items between versions when reading or writing. You may find references to all the available actions below.

AddType

Adding a new objectType, itemType, or enumType is as simple as 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.

Newer versions can read/write to this new type, while older versions won’t. Stately will handle filtering this new type out of list/sync calls from older versions.

export const AddType = migrate(1, "An add type example", (m) => {
m.addType("NewType");
});

RemoveType

Similar to AddType, removing an objectType, itemType or enumType is just as easy. Pass the name of the type you want to remove to the removeType migration.

When you remove a type from your schema, newer schema versions will no longer see this type. In other words, 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.

export const RemoveType = migrate(1, "A remove type example", (m) => {
m.removeType("ExistingType");
});

RenameType

This action will rename an existing type in the schema. If you reference this type in subsequent migration actions, you must use the NewType name.

When you rename a type, the wire format for the type will not change, only the name of the type. This means that older and newer versions of the schema can still read/write to the type.

export const RenameType = migrate(1, "A rename type example", (m) => {
m.renameType("OldType", "NewType");
});

AddField

Adds a new field to a type. Only required: false fields can be added for now. Support for adding required fields will be added in a future release.

Newer versions can read/write to this new field, while older versions won’t see this field. If a client uses an older version to update an item that has new fields in other schema versions, those fields will be untouched. In other words, there is no way to replace the whole object, Stately only replaces the fields known to the caller’s schema version. We will be adding more flexibility to the merge semantics in the future.

export const AddField = migrate(1, "An add field example", (m) => {
m.changeType("ExistingType", (i) => {
i.addField("name");
});
});

RemoveField

Removes an existing field from a type. Only required: false fields can be removed for now. Support for removing required fields will be added in a future release.

Newer versions can read/write to this type without the field, while older versions will still see the field untouched. In other words, there’s currently no way for a newer client to zero out this removed field for older clients. We will be adding more flexibility to the merge semantics in the future.

export const RemoveField = migrate(1, "A remove field example", (m) => {
m.changeType("ExistingType", (i) => {
i.removeField("name");
});
});

RenameField

Renames an existing field from a type. If you reference this field in subsequent migration actions you must use the newName.

When you rename a field, the wire format for the field will not change, only the name of the field. This means that older and newer versions of the schema can still read/write to the field.

export const RenameField = migrate(1, "A rename field example", (m) => {
m.changeType("ExistingType", (i) => {
i.renameField("oldName", "newName");
});
});