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.
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.tsIDatabaseMigration
Implement getName(), up(), down(), and getSource(). getSource() tells the core which datasource (usually plugin: "default") owns the change.
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.
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
- 01
Install / upgrade plugin
Core loads module.js and reads getMigrations().
- 02
Apply pending migrations
Each unused getName() runs up(queryRunner) against getSource().
- 03
onMigrate()
Optional plugin hook after migration batch completes.
- 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