Demo

Migrations

Ship schema changes through IPlugin.getMigrations() instead of relying on auto-sync. Each migration implements IDatabaseMigration with up/down on a QueryRunner.

When to use migrations

Entity registration still declares tables for the runtime, but prefer migrations for real schema evolution. Do not depend on synchronize for production upgrades.

  • Additive or destructive schema changes (columns, indexes, constraints, renames)
  • Data backfills that must run once as part of a plugin upgrade
  • Anything that must be reversible in staging / production

Folder layout

Keep migration classes under backend/src/migrations/ (or backend/migrations/ depending on the plugin). Export them from the plugin entry so getMigrations() can return them.

TSlayout
1backend/ 2 src/ 3 index.ts # IPlugin — getMigrations() 4 migrations/ 5 001-initial.migration.ts 6 002-add-status.migration.ts 7 feature/ 8 my.module.ts

IDatabaseMigration

Implement getName(), up(), down(), and getSource(). getSource() tells the core which datasource (usually plugin: "default") owns the change.

TSXmigration
1import type { IDatabaseMigration } from "@quan-erp/shared-types"; 2import type { QueryRunner } from "typeorm"; 3import metadata from "../../module.metadata.json" with { type: "json" }; 4 5export class AddStatusMigration implements IDatabaseMigration { 6 getName() { 7 return `${metadata.name}_002_add_status`; 8 } 9 10 getSource() { 11 return { plugin: "default", name: "default" }; 12 } 13 14 async up(queryRunner: QueryRunner): Promise<void> { 15 await queryRunner.query( 16 `ALTER TABLE ${metadata.name}_order ADD COLUMN IF NOT EXISTS status varchar(32)` 17 ); 18 } 19 20 async down(queryRunner: QueryRunner): Promise<void> { 21 await queryRunner.query( 22 `ALTER TABLE ${metadata.name}_order DROP COLUMN IF EXISTS status` 23 ); 24 } 25}

Register on IPlugin

Return the migration classes from getMigrations() on the default-exported plugin class in backend/src/index.ts. Use onMigrate() for post-migration hooks when needed.

TSXbackend/src/index.ts
1import type { GetMigrationsType, IPlugin } from "@quan-erp/shared-types"; 2import { InitialMigration } from "./migrations/001-initial.migration.js"; 3import { AddStatusMigration } from "./migrations/002-add-status.migration.js"; 4 5export default class MyPlugin implements IPlugin { 6 getMigrations(): GetMigrationsType { 7 return [InitialMigration, AddStatusMigration]; 8 } 9 10 async onMigrate(): Promise<void> { 11 // optional: seed / repair after migrations apply 12 } 13 14 // … getRootModule, getName, getVersion, getMetadata, … 15}

Run order

  1. 01

    Install / upgrade plugin

    Core loads module.js and reads getMigrations().

  2. 02

    Apply pending migrations

    Each unused getName() runs up(queryRunner) against getSource().

  3. 03

    onMigrate()

    Optional plugin hook after migration batch completes.

  4. 04

    Module init

    Entities, DI, and @OnInit continue as usual.

Practices

  • Prefix migration names with metadata.name so names stay unique across plugins
  • Keep up/down paired; down should undo the same change
  • Prefer IF EXISTS / IF NOT EXISTS (or equivalent checks) for safer re-runs in local/dev
  • Do not edit a migration that already shipped — add a new numbered migration instead
  • Align entity definitions with the final schema the migrations produce