4.6 KiB
4.6 KiB
adapters/surrealql — SurrealDB migration adapter
(no submodules)
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 idempotentDEFINE TABLE / DEFINE FIELD / DEFINE INDEXstatements gated onIF NOT EXISTS, so concurrent boots converge rather than racing.selectApplied(table)returns canonicalMigrationRecordshapes — the adapter normalises the Surrealid(Record ID object or string) to a plain string andappliedAt(ISO string) to aDate.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 formCREATE table:$name SET ...was silently dropped by SurrealDB v3's parser (the$paramwas 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 matchingtable:`id`form so theselectApplied→deleteAppliedround-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 ascause. - Disabled rollback raises
MigrationError(MIGRATION.ROLLBACK_NOT_SUPPORTED)fromdeleteApplied.
Usage: Basic Example
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
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 officialsurrealdbdriver.interface SurrealqlAdapterOptions { rollback?: boolean }— defaults totrue. Whenfalse,deleteAppliedrejects withMIGRATION.ROLLBACK_NOT_SUPPORTED.createSurrealqlAdapter(client, options?): MigrationAdapter— returns an adapter withdialect: "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.
appliedAtis sent as an ISO string ininsertApplied; SurrealQL accepts ISO 8601 fordatetimefields and the round-trip preserves wall-clock ordering even when the client is in a different time zone.selectApplieddoes not enforce ordering — the runner sorts bynamebefore comparing against pending files, so order is a runner concern.selectAppliednormalises the returnedidto the bare migration name: strips the${table}:prefix and any wrapping backticks, and for object- formRecordId { tb, id }returns the inner string id when present. This guarantees the value that round-trips intodeleteApplied(table, id)produces the sametable:`id`reference thatinsertAppliedwrote.- The adapter never injects an
AbortSignalintoclient.querybecause the upstreamsurrealdbdriver does not yet honor one; if a future driver version gains signal support, the adapter will pick it up by forwarding the optionalsignalargument through.