Files
suaveplan/packages/api-stack/api/api-migrations/README.md
2026-05-27 23:29:53 +01:00

6.8 KiB

@suaveplan/api-migrations - Database-agnostic migration runner for the Suaveplan API — narrow MigrationAdapter surface with per-dialect DDL bootstrap, real SHA-256 checksum validation against on-disk files, AbortSignal cancellation between migrations, OTel counter + histogram per apply, MigrationError with six MIGRATION.* codes. Ships three adapters (surrealql, postgres, sqlite); MySQL slot reserved in the dialect union. Server-tier — Bun + Node.

Database-agnostic migration runner for the Suaveplan API. Ships a narrow MigrationAdapter surface (six methods), three shipped adapters (surrealql, postgres, sqlite), a MigrationRunner class that ACTUALLY validates SHA-256 checksums against on-disk migration files before each apply phase, AbortSignal propagation between migrations, OTel counter + histogram per apply attempt, and MigrationError with six namespaced MIGRATION.* codes.

Splits and rewrites the upstream @suaveplan/api's database/migration-runner.ts (331 LOC). fixes baked in:

  1. Narrow MigrationAdapter surface — the runner depends on six methods, not the whole DatabaseLike. Each adapter ships its own ensureTable DDL.
  2. Checksums are ACTUALLY validated — drift halts apply with MigrationError(MIGRATION.CHECKSUM_DRIFT) and {name, expected, actual} context. Upstream's validateChecksums: true was a no-op; this one isn't.
  3. AbortSignal propagation — runFromDirectory(dir, { signal }) honors cancellation between migrations.
  4. OTel counter migrations.applied_total{dialect, outcome} and histogram migrations.apply_duration_ms{name} emit on every attempt regardless of success.
  5. MigrationError extends SuaveplanError<MigrationErrorCode> via defineCodes("MIGRATION", [...]) — six codes, every one raised from a production call site.

Documentation tree

  • postgres — Implements the MigrationAdapter contract against any Postgres-wire-protocol server (Postgres ≥ 12, CockroachDB, Yugab…
    • postgres — Implements the MigrationAdapter contract against any Postgres-wire-protocol server (Postgres ≥ 12, CockroachDB, Yugab…
  • sqlite — Implements the MigrationAdapter contract for SQLite-family engines. The factory createSqliteAdapter(client) accepts…
    • sqlite — Implements the MigrationAdapter contract for SQLite-family engines. The factory createSqliteAdapter(client) accepts…
  • surrealql — Implements the MigrationAdapter contract for SurrealDB. The factory createSurrealqlAdapter(client) wraps any Surrea…
    • surrealql — Implements the MigrationAdapter contract for SurrealDB. The factory createSurrealqlAdapter(client) wraps any Surrea…
  • error-codes — Defines the stable, namespaced error registry for @suaveplan/api-migrations. Every public failure path in the package…
    • error-codes — Defines the stable, namespaced error registry for @suaveplan/api-migrations. Every public failure path in the package…
  • otel — Lazily builds and caches the two OpenTelemetry instruments that @suaveplan/api-migrations emits on every migration ap…
    • instruments — Lazily builds and caches the two OpenTelemetry instruments that @suaveplan/api-migrations emits on every migration ap…
  • runner — The MigrationRunner is the dialect-agnostic core of the package. It reads migration files from a directory, sorts the…
    • runner — The MigrationRunner is the dialect-agnostic core of the package. It reads migration files from a directory, sorts the…
  • types — Declares every shared type that the runner, adapters, and consumers exchange across the @suaveplan/api-migrations pac…
    • migration — Declares every shared type that the runner, adapters, and consumers exchange across the @suaveplan/api-migrations pac…

Install

bun add @suaveplan/api-migrations

Quick start

import { MigrationRunner } from "@suaveplan/api-migrations/runner/runner";
import { createSqliteAdapter } from "@suaveplan/api-migrations/adapters/sqlite/sqlite";

const runner = new MigrationRunner(createSqliteAdapter(client), {
    fileExtension: ".sql",
});

const controller = new AbortController();
const results = await runner.runFromDirectory("./db/migrations", {
    signal: controller.signal,
});
for (const r of results) {
    if (!r.success) throw new Error(`Apply halted at ${r.name}: ${r.error}`);
}

Checksum-drift recovery

When validateChecksums: true (default) and a previously applied migration's on-disk content has changed, the runner halts before applying any pending migration:

try {
    await runner.runFromDirectory("./db/migrations");
} catch (err) {
    if (
        err instanceof MigrationError &&
        err.code === MIGRATION_CODES.CHECKSUM_DRIFT
    ) {
        // err.context = { name, expected, actual }
        // Resolution:
        //   1. Restore the migration file to its applied form (`git checkout`
        //      the file at the commit it was applied from).
        //   2. If the change is intentional, ship a NEW migration file with
        //      the additive change. Never rewrite history.
    }
    throw err;
}

Modules

Subpath exports

Every feature module is independently importable for maximum tree-shakeability:

import { MigrationRunner } from "@suaveplan/api-migrations/runner/runner";
import { createPostgresAdapter } from "@suaveplan/api-migrations/adapters/postgres/postgres";
import { MIGRATION_CODES } from "@suaveplan/api-migrations/error-codes/error-codes";

Tier

Server (Bun + Node). Uses node:crypto for SHA-256 and node:fs/promises for directory scanning and file reads.