first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
# `adapters/postgres` — PostgreSQL migration adapter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Implements the `MigrationAdapter` contract against any Postgres-wire-protocol
server (Postgres ≥ 12, CockroachDB, Yugabyte, etc.). The factory
`createPostgresAdapter(client)` accepts any client exposing a single
`unsafe(text, params?)` method that returns rows — the official `postgres`
(porsager/postgres) driver's `Sql` instance and a thin wrapper around
`pg.Client.query(text, values)` both satisfy this surface structurally.
The DDL is plain ANSI SQL (`CREATE TABLE IF NOT EXISTS ... text primary key
... timestamptz`), so the same adapter ports unchanged to Postgres-compatible
engines and is the basis for the spec scenario "postgres adapter emits
standard SQL DDL on ensureTable."
## Features
- Idempotent `CREATE TABLE IF NOT EXISTS` for the tracking table — concurrent
boots converge.
- Parameterised inserts and deletes — no string interpolation of caller data.
- Stores `applied_at` as `timestamptz`, normalises to a JavaScript `Date` on
read.
- Stores `duration` as `duration_ms int` and rehydrates as a plain number.
- Wraps `ensureTable` failures as
`MigrationError(MIGRATION.TABLE_BOOTSTRAP_FAILED)` so consumers can branch
on the namespaced code rather than the underlying driver error.
## Usage: Basic Example
```ts
import postgres from "postgres";
import { createPostgresAdapter } from "@suaveplan/api-migrations/adapters/postgres/postgres";
import { MigrationRunner } from "@suaveplan/api-migrations/runner/runner";
const sql = postgres(process.env.DATABASE_URL!);
const runner = new MigrationRunner(createPostgresAdapter(sql), {
fileExtension: ".sql",
});
await runner.runFromDirectory("./db/migrations");
```
## Usage: Advanced Example
```ts
import { createPostgresAdapter, type PostgresClient } from "@suaveplan/api-migrations/adapters/postgres/postgres";
// Adapting node-postgres' `pg.Client`:
import pg from "pg";
const pgClient = new pg.Client();
await pgClient.connect();
const adapted: PostgresClient = {
async unsafe<T>(text: string, params?: unknown[]): Promise<T[]> {
const r = await pgClient.query<T>({ text, values: params });
return r.rows;
},
};
const adapter = createPostgresAdapter(adapted);
```
## API Reference
- `interface PostgresClient { unsafe<T>(text: string, params?: unknown[]):
Promise<T[]> }` — structural client surface.
- `createPostgresAdapter(client): MigrationAdapter` — returns an adapter
with `dialect: "postgres"`.
## Implementation Notes
- The choice of a single `unsafe(text, params?)` surface is deliberate: it is
the smallest common denominator between `porsager/postgres` (tagged
templates) and `node-postgres` (positional placeholders), so callers can
bring whichever driver fits their stack.
- The runner generates the `id` field; the adapter does not auto-assign one.
- The DDL avoids Postgres-specific extensions (no `JSONB`, no `GENERATED`,
no `SERIAL`), so it is compatible with the broader Postgres ecosystem.
- `selectApplied` returns rows in unspecified order. The runner sorts by
`name` before comparing against pending files, so order is a runner
concern — not a SQL concern.
- The adapter does not currently forward `AbortSignal` into `client.unsafe`
because neither porsager/postgres nor node-postgres support it uniformly;
the per-migration cancellation gate in the runner still applies between
apply attempts.

View File

@@ -0,0 +1,108 @@
# `adapters/sqlite` — SQLite migration adapter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Implements the `MigrationAdapter` contract for SQLite-family engines. The
factory `createSqliteAdapter(client)` accepts any object exposing three
methods (`runScript`, `selectAll`, `runStatement`); a small structural
wrapper makes `@sqlite.org/sqlite-wasm`, `bun:sqlite`, and `better-sqlite3`
all conform. This decouples the adapter from the WASM build cost when a
consumer wants to use a native driver, while still letting universal
callers bring `sqlite-wasm` if they need browser-tier portability.
The DDL is plain ANSI SQLite (`TEXT PRIMARY KEY`, `INTEGER`, `TEXT NOT NULL`),
so the same adapter works against the upstream `sqlite3` CLI and against
any wrapper that supports parameterised statements.
## Features
- Idempotent `CREATE TABLE IF NOT EXISTS` for the tracking table.
- Parameterised inserts and deletes use `?` placeholders — no caller data is
interpolated into SQL.
- Stores `applied_at` as ISO 8601 text (SQLite has no native datetime); the
adapter rehydrates to a JavaScript `Date` on read.
- Stores `duration_ms` as `INTEGER`.
- Wraps `ensureTable` failures as `MigrationError(MIGRATION.TABLE_BOOTSTRAP_FAILED)`.
- Sync clients (`better-sqlite3`, `bun:sqlite`) work without modification —
the adapter awaits methods that may return `void` synchronously.
## Usage: Basic Example
```ts
import { Database } from "bun:sqlite";
import { createSqliteAdapter } from "@suaveplan/api-migrations/adapters/sqlite/sqlite";
import { MigrationRunner } from "@suaveplan/api-migrations/runner/runner";
const db = new Database(":memory:");
const client = {
runScript(sql: string) { db.run(sql); },
selectAll<T>(sql: string, params: unknown[] = []) {
return db.query<T, unknown[]>(sql).all(...params) as T[];
},
runStatement(sql: string, params: unknown[] = []) {
db.query(sql).run(...params);
},
};
const runner = new MigrationRunner(createSqliteAdapter(client));
await runner.runFromDirectory("./db/migrations");
```
## Usage: Advanced Example
```ts
import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
import { createSqliteAdapter } from "@suaveplan/api-migrations/adapters/sqlite/sqlite";
const sqlite3 = await sqlite3InitModule();
const db = new sqlite3.oo1.DB(":memory:");
const client = {
runScript(sql: string) {
db.executeBatch(sql);
},
selectAll<T>(sql: string, params: unknown[] = []) {
const out: T[] = [];
db.executeBatch({
sql,
bind: params,
rowMode: "object",
callback(row: T) { out.push(row); return undefined; },
});
return out;
},
runStatement(sql: string, params: unknown[] = []) {
db.executeBatch({ sql, bind: params });
},
};
const adapter = createSqliteAdapter(client);
```
## API Reference
- `interface SqliteClient { runScript(sql); selectAll<T>(sql, params?);
runStatement(sql, params?) }` — structural client surface.
- `createSqliteAdapter(client): MigrationAdapter` — returns an adapter with
`dialect: "sqlite"`.
## Implementation Notes
- The three-method surface is the smallest common denominator across the
three SQLite drivers the suaveplan tree is likely to encounter. Every
shipped adapter normalises to async by awaiting the result, so sync
drivers work without modification.
- `applied_at` is stored as ISO 8601 text because SQLite has no native
datetime type. Round-trip preserves wall-clock ordering — sorting on the
raw `applied_at` string yields the same order as sorting on parsed
`Date`.
- The adapter does not currently forward `AbortSignal` into `runScript`
because none of the supported SQLite drivers honor mid-query
cancellation. The runner's per-migration abort gate still applies between
apply attempts.
- The DDL is intentionally minimal — no indices beyond the `name`
uniqueness implicit in `UNIQUE NOT NULL`, no foreign keys. The runner
reads applied rows in unspecified order and sorts by `name` itself.

View File

@@ -0,0 +1,101 @@
# `adapters/surrealql` — SurrealDB migration adapter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Implements the `MigrationAdapter` contract for SurrealDB. The factory
`createSurrealqlAdapter(client)` wraps any SurrealDB client that exposes a
`query(sql, vars?)` method (the official `surrealdb` JavaScript driver and
the in-process `SurrealAdapter` from `@suaveplan/surrealdb` both satisfy this
structurally) and returns the six-method surface the runner consumes.
The SurrealQL DDL for the tracking table lives in this adapter — not in the
runner — so the runner stays dialect-agnostic and a sibling adapter package
may ship a different DDL for the same logical schema.
## Features
- `ensureTable(table)` issues idempotent `DEFINE TABLE / DEFINE FIELD / DEFINE
INDEX` statements gated on `IF NOT EXISTS`, so concurrent boots converge
rather than racing.
- `selectApplied(table)` returns canonical `MigrationRecord` shapes — the
adapter normalises the Surreal `id` (Record ID object or string) to a plain
string and `appliedAt` (ISO string) to a `Date`.
- `insertApplied(table, record)` writes the ledger row using a
backtick-quoted record-id literal (`` CREATE table:`id` SET ... ``) so the
CREATE is parsed as a record-id reference under SurrealDB v3. Field values
use named bindings, never string interpolation. The pre-fix form
`CREATE table:$name SET ...` was silently dropped by SurrealDB v3's
parser (the `$param` was consumed as a literal ident in the record-id
position, not substituted, so the statement returned no row).
- `deleteApplied(table, id)` removes the tracking row via the matching
`` table:`id` `` form so the `selectApplied` → `deleteApplied` round-trip
targets the same row regardless of whether the migration name is a bare
SurrealQL ident. Optionally locked behind `{ rollback: false }` to enforce
forward-only deploys.
- `execute(content)` runs the migration body as-is.
- Bootstrap failures raise `MigrationError(MIGRATION.TABLE_BOOTSTRAP_FAILED)`
with the underlying client error as `cause`.
- Disabled rollback raises `MigrationError(MIGRATION.ROLLBACK_NOT_SUPPORTED)`
from `deleteApplied`.
## Usage: Basic Example
```ts
import { Surreal } from "surrealdb";
import { createSurrealqlAdapter } from "@suaveplan/api-migrations/adapters/surrealql/surrealql";
import { MigrationRunner } from "@suaveplan/api-migrations/runner/runner";
const client = new Surreal();
await client.connect("ws://localhost:8000/rpc");
await client.use({ namespace: "test", database: "test" });
await client.signin({ username: "root", password: "root" });
const runner = new MigrationRunner(createSurrealqlAdapter(client));
const results = await runner.runFromDirectory("./db/migrations");
```
## Usage: Advanced Example
```ts
import { createSurrealqlAdapter } from "@suaveplan/api-migrations/adapters/surrealql/surrealql";
const forwardOnly = createSurrealqlAdapter(client, { rollback: false });
try {
await forwardOnly.deleteApplied("_migrations", "001_initial");
} catch (err) {
// err.code === "MIGRATION.ROLLBACK_NOT_SUPPORTED"
}
```
## API Reference
- `interface SurrealqlClient { query<T>(sql, vars?): Promise<T[]> }` —
structural client surface; satisfied by the official `surrealdb` driver.
- `interface SurrealqlAdapterOptions { rollback?: boolean }` — defaults to
`true`. When `false`, `deleteApplied` rejects with
`MIGRATION.ROLLBACK_NOT_SUPPORTED`.
- `createSurrealqlAdapter(client, options?): MigrationAdapter` — returns an
adapter with `dialect: "surrealql"`.
## Implementation Notes
- The DDL is emitted as a single multi-statement string so a single round
trip bootstraps the tracking table on first run.
- `appliedAt` is sent as an ISO string in `insertApplied`; SurrealQL accepts
ISO 8601 for `datetime` fields and the round-trip preserves wall-clock
ordering even when the client is in a different time zone.
- `selectApplied` does not enforce ordering — the runner sorts by `name`
before comparing against pending files, so order is a runner concern.
- `selectApplied` normalises the returned `id` to the bare migration name:
strips the `${table}:` prefix and any wrapping backticks, and for object-
form `RecordId { tb, id }` returns the inner string id when present. This
guarantees the value that round-trips into `deleteApplied(table, id)`
produces the same `` table:`id` `` reference that `insertApplied` wrote.
- The adapter never injects an `AbortSignal` into `client.query` because the
upstream `surrealdb` driver does not yet honor one; if a future driver
version gains signal support, the adapter will pick it up by forwarding
the optional `signal` argument through.

View File

@@ -0,0 +1,98 @@
# `error-codes` — `MIGRATION_*` registry and `MigrationError`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the stable, namespaced error registry for `@suaveplan/api-migrations`.
Every public failure path in the package raises a `MigrationError` carrying
exactly one of the six `MIGRATION.*` codes defined here. The codes are
authored once at draft time and may not be renumbered or renamed without an
OpenSpec change, so consumers may branch on the literal string values across
versions.
The pairing of `defineCodes("MIGRATION", [...])` (from `@suaveplan/error`)
with a typed `SuaveplanError` subclass keeps the surface narrow: the runtime
sees a frozen registry object, TypeScript sees a literal union, and consumers
get an `instanceof MigrationError` narrowing the `code` field to
`MigrationErrorCode`.
## Features
- Frozen registry of exactly six codes.
- `MigrationError` is a public-constructor subclass of `SuaveplanError`.
- `instanceof MigrationError` narrows `error.code` to `MigrationErrorCode`.
- The `migrationCode` field is a synonym for `code` typed as the literal
union, useful inside generic helpers that have already widened `code`.
- Every code is exercised by the package's spec-conformance test.
## Usage: Basic Example
```ts
import {
MIGRATION_CODES,
MigrationError,
} from "@suaveplan/api-migrations/error-codes/error-codes";
throw new MigrationError(
MIGRATION_CODES.DIRECTORY_NOT_FOUND,
"Migration directory does not exist: ./db/migrations",
{ context: { dir: "./db/migrations" } }
);
```
## Usage: Advanced Example
```ts
import {
MIGRATION_CODES,
MigrationError,
type MigrationErrorCode,
} from "@suaveplan/api-migrations/error-codes/error-codes";
function isRecoverable(code: MigrationErrorCode): boolean {
return (
code === MIGRATION_CODES.CHECKSUM_DRIFT ||
code === MIGRATION_CODES.INVALID_FILE_NAME
);
}
try {
await runner.runFromDirectory("./db/migrations");
} catch (err) {
if (err instanceof MigrationError && isRecoverable(err.code)) {
// Operator-fixable conditions: stale file or rename.
console.error("Recoverable migration error:", err.code, err.context);
} else {
throw err;
}
}
```
## API Reference
- `MIGRATION_CODES` — frozen registry `{ TABLE_BOOTSTRAP_FAILED, APPLY_FAILED,
CHECKSUM_DRIFT, DIRECTORY_NOT_FOUND, INVALID_FILE_NAME, ROLLBACK_NOT_SUPPORTED }`,
each value the literal string of the form `"MIGRATION.<NAME>"`.
- `type MigrationErrorCode` — union of the six literal strings.
- `class MigrationError extends SuaveplanError<MigrationErrorCode>` —
constructor `(code, message, options?)`. `options.context` and
`options.cause` are forwarded to the base class. `migrationCode` is a
typed alias of `code`.
## Implementation Notes
- The registry is authored as `as const` and passed to `defineCodes`, which
freezes the resulting object and returns a strongly typed map. Renaming or
renumbering codes is a breaking change that must be made through OpenSpec.
- `MigrationError`'s constructor is `public` so call sites may instantiate it
directly; `SuaveplanError`'s constructor is `protected` and would otherwise
refuse construction outside of subclasses.
- `migrationCode` exists in addition to `code` because some generic helpers
(logging, OTel attribute attachment) widen the type to `string` for
ergonomics; tests and downstream consumers that want the narrow union read
`migrationCode` instead.
- Every code is thrown from at least one production call site — see
`src/api-migrations.spec-conformance.test.ts` for the inventory.

View File

@@ -0,0 +1,78 @@
# `otel/instruments` — counter + histogram per migration apply
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Lazily builds and caches the two OpenTelemetry instruments that
`@suaveplan/api-migrations` emits on every migration apply attempt:
- counter `migrations.applied_total{dialect, outcome}`
- histogram `migrations.apply_duration_ms{name}`
Lazy construction defers `metrics.getMeter(...)` until the first apply, so
the meter resolves against whatever `MeterProvider` is globally installed at
that moment — including the test in-memory provider that
`tests/setup/polyfills.ts` registers before any source module loads.
## Features
- Single `recordApply(dialect, name, durationMs, outcome)` entry point — keeps
the call-site code in `runner.ts` short and uniform regardless of success
or failure path.
- Emits both instruments on every call, so observability does not depend on
whether a migration succeeded.
- `resetMigrationInstruments()` lets the test suite discard the cached
instance between `MeterProvider` swaps.
- Counter attributes are deliberately low-cardinality (`dialect` × two
outcomes) so the time series is stable across releases.
## Usage: Basic Example
```ts
import { getMigrationInstruments } from "@suaveplan/api-migrations/otel/instruments";
const instruments = getMigrationInstruments();
instruments.recordApply("sqlite", "001_initial", 17, "success");
```
## Usage: Advanced Example
```ts
import {
getMigrationInstruments,
resetMigrationInstruments,
} from "@suaveplan/api-migrations/otel/instruments";
import { metrics } from "@opentelemetry/api";
import { MeterProvider } from "@opentelemetry/sdk-metrics";
// Swap providers between test phases — call reset to rebuild the cached
// instruments against the new meter.
metrics.setGlobalMeterProvider(new MeterProvider());
resetMigrationInstruments();
getMigrationInstruments().recordApply("postgres", "002_users", 42, "error");
```
## API Reference
- `interface MigrationInstruments { recordApply(dialect, name, durationMs,
outcome): void }`.
- `getMigrationInstruments(): MigrationInstruments` — returns the cached
instance, building it on first call against the currently registered
`MeterProvider`.
- `resetMigrationInstruments(): void` — clears the cache (test-only).
## Implementation Notes
- The module-level `instruments` cache is the only mutable state and is
scoped to the module — re-importing the module under a fresh ESM realm
yields a fresh cache, which is the desired behaviour for isolated tests.
- The counter is namespaced `migrations.applied_total`; the histogram is
namespaced `migrations.apply_duration_ms` — both names match the spec
delta literally so downstream dashboards do not need translation tables.
- Attribute cardinality: `dialect` is bounded by `MigrationDialect`
(currently four members), `outcome` is exactly two, and `name` is the
user-supplied migration basename. The histogram attribute is intentionally
`name`-only — adding `outcome` here would explode the bucket count.

View File

@@ -0,0 +1,119 @@
# `runner` — `MigrationRunner` class + `createMigrationRunner` factory
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `MigrationRunner` is the dialect-agnostic core of the package. It reads
migration files from a directory, sorts them by leading-digit order, applies
any not-yet-applied ones via the injected `MigrationAdapter`, and records
each application with a real SHA-256 checksum. Unlike the upstream runner —
which declared `validateChecksums: true` but never actually compared
checksums — this implementation reads every already-applied migration's file
content from disk, recomputes the hash, and halts with
`MigrationError(MIGRATION.CHECKSUM_DRIFT)` on mismatch BEFORE any pending
migration is touched.
The runner is server-tier: it uses `node:fs/promises` for directory and file
access and `node:crypto` for SHA-256. Both Bun and Node ship these natively.
## Features
- Per-dialect default file extension (`.surql` for `surrealql`, `.sql`
otherwise).
- Numerical ordering by leading-digit prefix (`001_initial.sql` → order 1).
- Real SHA-256 checksum validation against on-disk files — drift detected
pre-apply and surfaced as `MIGRATION.CHECKSUM_DRIFT` with `{name,
expected, actual}` context.
- `AbortSignal` propagation — the runner checks `signal.aborted` between
pending migrations; aborts return the partial result array.
- Dry-run mode (`{ dryRun: true }`) logs but does not invoke
`adapter.execute` / `adapter.insertApplied`.
- Halt on first failure — the runner does NOT continue past a failed apply,
so partial-success deployments stay consistent.
- OTel counter + histogram per apply attempt, regardless of outcome.
- `rollback()` removes the tracking record of the most recent migration
(sorted by `appliedAt` desc); does not revert schema.
- `MIGRATION.DIRECTORY_NOT_FOUND` when `runFromDirectory` is given a missing
path.
- `MIGRATION.INVALID_FILE_NAME` when a `.sql`/`.surql` file does not start
with one or more digits.
## Usage: Basic Example
```ts
import { MigrationRunner } from "@suaveplan/api-migrations/runner/runner";
import { createSqliteAdapter } from "@suaveplan/api-migrations/adapters/sqlite/sqlite";
const runner = new MigrationRunner(createSqliteAdapter(client));
const results = await runner.runFromDirectory("./db/migrations");
for (const r of results) {
console.log(r.success ? `OK ${r.name} (${r.duration}ms)` : `ERR ${r.name}: ${r.error}`);
}
```
## Usage: Advanced Example
```ts
import { MigrationRunner } from "@suaveplan/api-migrations/runner/runner";
const controller = new AbortController();
setTimeout(() => controller.abort(), 5_000);
const runner = new MigrationRunner(adapter, {
tableName: "_app_migrations",
fileExtension: ".sql",
validateChecksums: true,
logger: {
debug() {},
info: (m) => console.log(`[mig] ${m}`),
warn: (m) => console.warn(`[mig] ${m}`),
error: (m) => console.error(`[mig] ${m}`),
},
});
const results = await runner.runFromDirectory("./db/migrations", {
signal: controller.signal,
});
const failed = results.find((r) => !r.success);
if (failed) {
throw new Error(`Apply halted at ${failed.name}: ${failed.error}`);
}
```
## API Reference
- `class MigrationRunner` — constructor `(adapter, options?)`.
- `runFromDirectory(dirPath, { signal? }?): Promise<MigrationResult[]>`.
- `run(files, { signal? }?): Promise<MigrationResult[]>`.
- `runMigration(file, signal?): Promise<MigrationResult>`.
- `getAppliedMigrations(): Promise<MigrationRecord[]>`.
- `getPendingMigrations(dir): Promise<MigrationFile[]>`.
- `rollback(): Promise<MigrationResult | null>`.
- `scanDirectory(dir): Promise<MigrationFile[]>`.
- `createMigrationRunner(adapter, options?)` — factory mirroring upstream.
- `computeChecksum(content)` — `node:crypto` SHA-256, lowercase hex.
- Options: `tableName` (default `"_migrations"`), `fileExtension`
(per-dialect default), `validateChecksums` (default `true`), `dryRun`
(default `false`), `logger` (default noop), `clock` (default
`Date.now`).
## Implementation Notes
- Checksum validation happens BEFORE any pending migration is applied. If a
drift is detected the runner halts — the operator must restore the file
or rerun with `validateChecksums: false` (audited and discouraged).
- Pre-aborted signals are honored before the first apply attempt — the
result array is empty in that case.
- Failures inside `adapter.execute` or `adapter.insertApplied` are wrapped
in `MigrationError(MIGRATION.APPLY_FAILED)`, with the underlying client
error as `cause`. The runner returns `{success: false, error}` and breaks
the loop, so subsequent migrations are not attempted.
- Rollback only removes the most recent tracking row — sorting is
performed in memory by `appliedAt` desc so adapters do not need to
guarantee returned-row order.
- `MIGRATION.ROLLBACK_NOT_SUPPORTED` is raised by adapters configured to
refuse `deleteApplied` (e.g. the surrealql adapter with `{rollback:
false}`). The runner surfaces this code untouched.

View File

@@ -0,0 +1,113 @@
# `types/migration` — public type surface
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Declares every shared type that the runner, adapters, and consumers exchange
across the `@suaveplan/api-migrations` package: the dialect literal, the
on-disk `MigrationFile` shape, the persisted `MigrationRecord` row, the
per-call `MigrationResult` outcome, the narrow `MigrationAdapter` contract,
and the option bundles for construction and per-call invocation. Centralising
the contract here keeps the runner's import graph short and lets adapters be
authored in isolation against a single canonical interface.
The module is types-only at runtime — every export is an `interface` or
`type`, so the emitted JavaScript file is empty under `verbatimModuleSyntax`.
## Features
- `MigrationDialect` — open literal union including `mysql` for the future
out-of-tree adapter package.
- `MigrationFile`, `MigrationRecord`, `MigrationResult` — the three on-disk /
in-database / in-memory shapes.
- `MigrationAdapter` — the per-dialect surface the runner depends on.
- `MigrationRunnerOptions` — options bag at construction time.
- `MigrationRunOptions` — options bag per `runFromDirectory` / `run` call
(currently `signal` only — splits cancellation out from the construction
options so a single runner instance can be re-used across cancellable
apply phases).
## Usage: Basic Example
```ts
import type {
MigrationAdapter,
MigrationDialect,
MigrationRecord,
} from "@suaveplan/api-migrations/types/migration";
const dialect: MigrationDialect = "sqlite";
const records: MigrationRecord[] = [
{
id: "001_initial",
name: "001_initial",
appliedAt: new Date(),
checksum: "deadbeef".repeat(8),
duration: 17,
},
];
```
## Usage: Advanced Example
```ts
import type {
MigrationAdapter,
MigrationRecord,
} from "@suaveplan/api-migrations/types/migration";
class CountingAdapter implements MigrationAdapter {
readonly dialect = "sqlite" as const;
public applies = 0;
constructor(private readonly underlying: MigrationAdapter) {}
ensureTable(t: string) { return this.underlying.ensureTable(t); }
selectApplied(t: string) { return this.underlying.selectApplied(t); }
insertApplied(t: string, r: MigrationRecord) {
return this.underlying.insertApplied(t, r);
}
deleteApplied(t: string, id: string) {
return this.underlying.deleteApplied(t, id);
}
async execute(content: string, signal?: AbortSignal) {
this.applies += 1;
return this.underlying.execute(content, signal);
}
}
```
## API Reference
- `type MigrationDialect = "surrealql" | "postgres" | "sqlite" | "mysql"`.
- `interface MigrationFile { name; path; order; content? }` — fields are
read-only; `content` is mutable so the runner can cache the buffer after
the first read.
- `interface MigrationRecord extends Record<string, unknown> { id; name;
appliedAt; checksum; duration }` — extends the index signature so adapter
implementations may attach dialect-specific extras (Postgres `oid`, etc.).
- `interface MigrationResult { name; success; duration; error? }` — the
per-migration outcome aggregated by `run`.
- `interface MigrationAdapter { dialect; ensureTable; selectApplied;
insertApplied; deleteApplied; execute }` — the runner depends on exactly
these six methods.
- `interface MigrationRunnerOptions { tableName?; fileExtension?;
validateChecksums?; dryRun?; logger?; clock? }`.
- `interface MigrationRunOptions { signal? }`.
## Implementation Notes
- `MigrationDialect` is intentionally open (closed unions are banned per
§0a). Adding a new dialect is a non-breaking literal-union extension that
every adapter package can perform via module augmentation.
- `MigrationRecord` extends `Record<string, unknown>` so the surrealql
adapter can decorate rows with the `_migrations:001_initial` Surreal Record
ID without needing a second type.
- The runner consumes `MigrationAdapter` structurally — adapters do not need
to `implements MigrationAdapter` if they return a literal of the right
shape from their factory.
- `MigrationRunOptions` is split from `MigrationRunnerOptions` so a single
runner instance can be reused across many cancellable apply phases without
the construction options needing to carry an `AbortSignal`.