Migrations
Move record data alongside a schema change with imperative, reviewable migrations. Use Schema Sync for declarative schema reshaping; reach for a migration when you also need to backfill or transform existing records.
defineMigration()
Author a migration with an async up (and optional down). The context exposes schema (add / remove / rename entities and attributes by name) and records (backfill, bulk ops). The expand → backfill → contract pattern keeps a rename loss-free.
// migrations/20260601_rename_tier_to_level.ts
import { defineMigration } from "@reopt-ai/brandapp-sdk/eav/migrate";
export default defineMigration({
name: "rename_tier_to_level",
up: async ({ schema, records, log }) => {
// 1. expand — add the new attribute
await schema.addAttribute("contacts", "level", {
dataType: "string",
label: "Level",
});
// 2. backfill — copy data from the old attribute to the new one
await records.backfill("contacts", {
transform: (r) => ({ level: r.values.tier ?? "free" }),
onProgress: ({ processed, total }) => log(`${processed}/${total}`),
});
// 3. contract — drop the old attribute
await schema.removeAttribute("contacts", "tier");
},
// Forward-only is recommended; only declare down for trivially reversible work.
});runMigration()
Execute a migration's up (or down) against a live client. The runner returns a MigrationRunResult ({ name, checksum, durationMs, alreadyApplied }); recording the applied-state audit row is the CLI's job.
import { createReoptEavClient } from "@reopt-ai/brandapp-sdk/eav";
import { runMigration } from "@reopt-ai/brandapp-sdk/eav/migrate";
import migration from "./migrations/20260601_rename_tier_to_level";
const eav = createReoptEavClient({
clientId: process.env.BRANDAPP_CLIENT_ID!,
clientSecret: process.env.BRANDAPP_CLIENT_SECRET!,
brandappId: process.env.BRANDAPP_ID!,
});
const result = await runMigration(eav, migration, {
direction: "up", // "up" (default) | "down"
checksum: "sha256-of-the-migration-file",
onLog: (msg) => console.log(msg),
});
// result: { name, checksum, durationMs, alreadyApplied }
console.log(`${result.name} applied in ${result.durationMs}ms`);
// runMigration does NOT record applied state — the CLI (reopt brandapp eav
// migrate run) records the audit row after this resolves.runMigrationCollecting()
Buffer the schema operations instead of sending them one-by-one, then hand the resulting MigrationOperation[] to the server's batch apply endpoint for a single-transaction apply. Backfills inside up() still run live against the current server state.
import { createReoptEavClient } from "@reopt-ai/brandapp-sdk/eav";
import { runMigrationCollecting } from "@reopt-ai/brandapp-sdk/eav/migrate";
import migration from "./migrations/20260601_rename_tier_to_level";
const eav = createReoptEavClient({
clientId: process.env.BRANDAPP_CLIENT_ID!,
clientSecret: process.env.BRANDAPP_CLIENT_SECRET!,
brandappId: process.env.BRANDAPP_ID!,
});
const { operations, durationMs } = await runMigrationCollecting(eav, migration);
// operations: MigrationOperation[] captured from ctx.schema.* calls, in order:
// [{ kind: "addAttribute", entity: "contacts", name: "level", def: {...} },
// { kind: "removeAttribute", entity: "contacts", name: "tier" }]
// Hand the array to POST /v1/brandapp/{id}/migrations/apply for an atomic,
// single-transaction apply. (Backfills inside up() already ran live.)
console.log(`Buffered ${operations.length} op(s) in ${durationMs}ms`);createMigrationContext()
Build the { schema, records, log } context yourself for ad-hoc or scripted runs that don't need the defineMigration() wrapper. The runners construct this context internally.
import { createReoptEavClient } from "@reopt-ai/brandapp-sdk/eav";
import { createMigrationContext } from "@reopt-ai/brandapp-sdk/eav/migrate";
const eav = createReoptEavClient({
clientId: process.env.BRANDAPP_CLIENT_ID!,
clientSecret: process.env.BRANDAPP_CLIENT_SECRET!,
brandappId: process.env.BRANDAPP_ID!,
});
// ctx exposes the same { schema, records, log } a migration's up() receives —
// useful for one-off scripts that don't need the defineMigration() wrapper.
// (runMigration / runMigrationCollecting build this context internally — reach
// for createMigrationContext only when you want manual control.)
const ctx = createMigrationContext(eav, { log: console.log });
await ctx.schema.addAttribute("contacts", "newsletter", {
dataType: "boolean",
label: "Newsletter opt-in",
});
await ctx.records.backfill("contacts", {
transform: () => ({ newsletter: false }),
});