Skip to content

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

database-migration

CI

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 @ferrow/database-migration

Quickstart

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 by id on 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. Omit toId to 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

About

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.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages