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,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.