@ferrow/database-migration
v2.0.0
Published
DB-agnostic migration runner: ordered up/down migrations, pluggable applied-state store (in-memory or JSON file bundled), dry-run mode, and checksum protection against editing already-applied migrations.
Maintainers
Readme
database-migration
A DB-agnostic migration runner. Register {id, up, down} migrations,
apply them in order, track applied state through a pluggable store, and
detect if an already-applied migration got edited out from under you.
It is database-agnostic by design: the migration context (ctx) is
whatever you pass in — a Postgres client, a Mongo collection, a raw
connection — this library never opens a connection or knows what SQL is.
Install
npm install database-migrationQuickstart
import { MigrationRunner, JsonFileStateStore } from "database-migration";
import { Client } from "pg"; // any db client you like
const db = new Client(/* ... */);
await db.connect();
const runner = new MigrationRunner({
store: new JsonFileStateStore("./migrations-state.json"),
ctx: db,
});
runner.register({
id: "001_create_users",
up: (db) => db.query("CREATE TABLE users (id serial PRIMARY KEY)"),
down: (db) => db.query("DROP TABLE users"),
});
await runner.up(); // apply everything pending
await runner.status(); // [{ id, applied, appliedAt, description }, ...]
await runner.down("000"); // revert everything after "000"API
new MigrationRunner<Ctx>({ store, ctx })
register(migration: Migration<Ctx>): this—{ id, up(ctx), down(ctx), description? }. Sorted byidon registration; throws on duplicate ids.up(options?: { dryRun?: boolean }): Promise<RunResult>— applies all pending migrations in ascending id order.down(toId?: string, options?: { dryRun?: boolean }): Promise<RunResult>— reverts applied migrations, newest-first, down to (not including)toId. OmittoIdto revert everything.status(): Promise<StatusEntry[]>—{ id, description, applied, appliedAt }for every registered migration.
dryRun: true runs the same selection logic without calling up/down
or writing state — use it to preview what would run.
Checksum protection
Every time migrations are applied, the runner records a checksum of the
full registered-id list. On the next up/down, if that checksum no
longer matches (a migration was renamed, reordered, or removed), it
throws instead of silently running against a list that's drifted from
what was actually applied. This is the guard against the classic bug
where a migration gets edited after already shipping to production.
State stores
interface MigrationStateStore {
getApplied(): Promise<AppliedRecord[]>;
markApplied(id: string): Promise<void>;
markReverted(id: string): Promise<void>;
getChecksum(): Promise<string | undefined>;
setChecksum(checksum: string): Promise<void>;
}Bundled: InMemoryStateStore (tests/dev, no persistence) and
JsonFileStateStore (writes atomically via temp-file + rename). Implement
the interface yourself to track state in the same database you're
migrating.
Design notes
Making ctx fully user-supplied is what keeps this DB-agnostic without
resorting to a plugin system — a Postgres migration and a Mongo migration
both just get an opaque ctx and do whatever they want with it. The
checksum check exists because the most common migration-tool footgun
isn't a bad migration, it's someone quietly editing one that already ran
in production; catching that at up()/down() time is cheaper than
debugging the resulting drift.
Sponsored by Ferrow
Part of the ferrow-toolkit collection · Sponsored by Ferrow
