first commit
This commit is contained in:
119
docs/api-stack/api/api-migrations/src/runner/runner.md
Normal file
119
docs/api-stack/api/api-migrations/src/runner/runner.md
Normal 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.
|
||||
Reference in New Issue
Block a user