# `runner` — `MigrationRunner` class + `createMigrationRunner` factory _(no submodules)_ ## 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`. - `run(files, { signal? }?): Promise`. - `runMigration(file, signal?): Promise`. - `getAppliedMigrations(): Promise`. - `getPendingMigrations(dir): Promise`. - `rollback(): Promise`. - `scanDirectory(dir): Promise`. - `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.