@schemic/surrealdb 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,112 @@
1
+ // SurrealDB connection runtime — split out of cli/config.ts so that module stays dialect-neutral
2
+ // (config types + loadConfig only). This is the Surreal driver's `connect` implementation; it imports
3
+ // the surrealdb SDK and belongs to @schemic/surrealdb at the physical split.
4
+
5
+ import type { ConnectionOverrides, ResolvedConfig } from "@schemic/core";
6
+ import { escapeIdent, Surreal } from "surrealdb";
7
+ import type { AuthLevel, SurrealParams } from "../config";
8
+
9
+ const errMsg = (e: unknown) => (e instanceof Error ? e.message : String(e));
10
+
11
+ const CONNECT_TIMEOUT_MS = 5_000;
12
+
13
+ /** Reject if `promise` doesn't settle within `ms` — guards against a hung connect. */
14
+ function withTimeout<T>(
15
+ promise: Promise<T>,
16
+ ms: number,
17
+ message: string,
18
+ ): Promise<T> {
19
+ return new Promise<T>((resolve, reject) => {
20
+ const timer = setTimeout(() => reject(new Error(message)), ms);
21
+ promise.then(
22
+ (v) => {
23
+ clearTimeout(timer);
24
+ resolve(v);
25
+ },
26
+ (e) => {
27
+ clearTimeout(timer);
28
+ reject(e);
29
+ },
30
+ );
31
+ });
32
+ }
33
+
34
+ /** Connect + authenticate + select the namespace/database. Caller closes the handle. */
35
+ export async function connect(
36
+ config: ResolvedConfig,
37
+ over: ConnectionOverrides = {},
38
+ ): Promise<Surreal> {
39
+ const params = config.params as unknown as SurrealParams;
40
+ const url = over.url ?? params.url;
41
+ const namespace = over.namespace ?? params.namespace;
42
+ const database = over.database ?? params.database;
43
+ const username = over.username ?? params.username;
44
+ const password = over.password ?? params.password;
45
+ const level: AuthLevel = (over.authLevel ??
46
+ params.authLevel ??
47
+ "root") as AuthLevel;
48
+
49
+ const db = new Surreal();
50
+ // On ANY failure, close the handle before throwing — otherwise the SDK's reconnect timer
51
+ // keeps the event loop alive and the command hangs instead of exiting.
52
+ try {
53
+ try {
54
+ // `reconnect: false` so a dead server rejects immediately instead of entering a retry
55
+ // loop; `withTimeout` is a fallback for an unreachable host that never rejects.
56
+ await withTimeout(
57
+ db.connect(url, { reconnect: false }),
58
+ CONNECT_TIMEOUT_MS,
59
+ "connection timed out",
60
+ );
61
+ } catch (e) {
62
+ throw new Error(
63
+ `Can't reach SurrealDB at ${url} — is the server running? (${errMsg(e)})`,
64
+ );
65
+ }
66
+ if (username && password) {
67
+ // Scope the signin to the requested level (mirrors `surreal sql --auth-level`).
68
+ const auth =
69
+ level === "root"
70
+ ? { username, password }
71
+ : level === "namespace"
72
+ ? { namespace, username, password }
73
+ : { namespace, database, username, password };
74
+ try {
75
+ await db.signin(auth);
76
+ } catch (e) {
77
+ throw new Error(
78
+ `Authentication failed (auth level "${level}") — check SURREAL_USER / SURREAL_PASS. (${errMsg(e)})`,
79
+ );
80
+ }
81
+ }
82
+ // Best-effort: create the namespace/database when we likely have the rights. A `database`
83
+ // user can't define either; a `namespace` user can define databases; `root` can do both.
84
+ try {
85
+ if (level === "root") {
86
+ await db.query(
87
+ `DEFINE NAMESPACE IF NOT EXISTS ${escapeIdent(namespace)};`,
88
+ );
89
+ }
90
+ if (level !== "database") {
91
+ await db.use({ namespace });
92
+ await db.query(
93
+ `DEFINE DATABASE IF NOT EXISTS ${escapeIdent(database)};`,
94
+ );
95
+ }
96
+ } catch {
97
+ // insufficient privileges — assume the namespace/database already exist
98
+ }
99
+ try {
100
+ await db.use({ namespace, database });
101
+ } catch (e) {
102
+ throw new Error(
103
+ `Couldn't select ${namespace}/${database} — does it exist and do you have access? (${errMsg(e)})`,
104
+ );
105
+ }
106
+ return db;
107
+ } catch (e) {
108
+ // Fire-and-forget: closing a half-open socket can be slow; don't block the error path.
109
+ void db.close().catch(() => {});
110
+ throw e;
111
+ }
112
+ }
@@ -0,0 +1,321 @@
1
+ // The SurrealDB STATEMENT-diff engine: builds canonical `DEFINE` snapshots from authored schemas
2
+ // and diffs two snapshots into SurrealQL up/down (clause-level ALTER where possible). This is the
3
+ // Surreal driver's internal diff strategy — invoked via `surrealDriver.diff` and the migrations
4
+ // capability's `render`. The dialect-free diff DISPLAY + types live in `./diff`. In the package
5
+ // split this module moves to `@schemic/surrealdb` (see docs/AUTHORING-SPLIT.md / MULTI-DB-SPIKE.md).
6
+
7
+ import { relative } from "node:path";
8
+ import type { AnyTable, AuthoredDef, Diff, DiffItem } from "@schemic/core";
9
+ import type { DefineStatement } from "../ddl";
10
+ import {
11
+ alterField,
12
+ alterTable,
13
+ emitDefStatement,
14
+ emitStatements,
15
+ overwriteStatement,
16
+ removeStatement,
17
+ } from "../ddl";
18
+ import { schemaStruct } from "./lower";
19
+ import { deepEqual } from "./struct";
20
+ import type { DbStructured, Snapshot, SnapshotStatement } from "./structure";
21
+
22
+ const keyOf = (s: Pick<DefineStatement, "kind" | "name" | "table">) =>
23
+ `${s.kind}:${s.table ?? ""}:${s.name}`;
24
+
25
+ /** Index a normalized DbStructured by statement key (matching `keyOf`) for structural equality. */
26
+ function structIndex(db: DbStructured): Map<string, unknown> {
27
+ const idx = new Map<string, unknown>();
28
+ for (const t of db.tables) {
29
+ // The table STATEMENT is just the head — fields/indexes/events are their own statements.
30
+ idx.set(`table::${t.name}`, {
31
+ kind: t.kind,
32
+ schemafull: t.schemafull,
33
+ drop: t.drop,
34
+ comment: t.comment,
35
+ changefeed: t.changefeed,
36
+ permissions: t.permissions,
37
+ });
38
+ for (const f of t.fields) idx.set(`field:${t.name}:${f.name}`, f);
39
+ for (const i of t.indexes) idx.set(`index:${t.name}:${i.name}`, i);
40
+ for (const e of t.events) idx.set(`event:${t.name}:${e.name}`, e);
41
+ }
42
+ for (const fn of db.functions) idx.set(`function::${fn.name}`, fn);
43
+ for (const a of db.accesses) idx.set(`access::${a.name}`, a);
44
+ return idx;
45
+ }
46
+
47
+ /**
48
+ * Build the canonical snapshot (keyed `DEFINE` statements) for the current schemas: every table's
49
+ * statements, plus any standalone defs (`defineEvent`/`defineFunction`), keyed by kind+table+name.
50
+ */
51
+ export function buildSnapshot(
52
+ tables: AnyTable[],
53
+ defs: AuthoredDef[] = [],
54
+ opts: {
55
+ fileOf?: Map<unknown, string>;
56
+ root?: string;
57
+ /** Also compute + store the normalized Struct-IR (for `diff --ts`). Off by default. */
58
+ withStruct?: boolean;
59
+ } = {},
60
+ ): Snapshot {
61
+ const statements: Record<string, SnapshotStatement> = {};
62
+ // The source file each object came from, project-root-relative for portable, readable snapshots.
63
+ const fileFor = (obj: unknown): string | undefined => {
64
+ const abs = opts.fileOf?.get(obj);
65
+ return abs ? (opts.root ? relative(opts.root, abs) : abs) : undefined;
66
+ };
67
+ for (const t of tables) {
68
+ const file = fileFor(t);
69
+ try {
70
+ for (const s of emitStatements(
71
+ t as unknown as Parameters<typeof emitStatements>[0],
72
+ ))
73
+ statements[keyOf(s)] = file ? { ...s, file } : s;
74
+ } catch (e) {
75
+ // Pin an emit failure (e.g. a non-Surreal field type) to the source file.
76
+ const msg = e instanceof Error ? e.message : String(e);
77
+ throw new Error(file ? `${msg}\n in ${file}` : msg);
78
+ }
79
+ }
80
+ for (const d of defs) {
81
+ const s = emitDefStatement(
82
+ d as unknown as Parameters<typeof emitDefStatement>[0],
83
+ );
84
+ const file = fileFor(d);
85
+ statements[keyOf(s)] = file ? { ...s, file } : s;
86
+ }
87
+ const snap: Snapshot = { version: 1, statements };
88
+ if (opts.withStruct)
89
+ // Bridge the lib/src type duality (AnyTable is the built-lib TableDef; schemaStruct uses src).
90
+ snap.struct = schemaStruct(
91
+ tables as unknown as Parameters<typeof schemaStruct>[0],
92
+ defs as unknown as Parameters<typeof schemaStruct>[1],
93
+ );
94
+ return snap;
95
+ }
96
+
97
+ // Within a table, create order is table -> field -> index (each depends on the prior); drop
98
+ // order is the reverse. Statements are grouped by table (see `diffSnapshots`).
99
+ const RANK: Record<DefineStatement["kind"], number> = {
100
+ analyzer: 0, // db-level; defined first (a FULLTEXT index references its analyzer)
101
+ function: 0, // db-level; defined first (tables/events may reference fn::…)
102
+ table: 1,
103
+ field: 2,
104
+ index: 3,
105
+ event: 4,
106
+ access: 5, // db-level; defined last (SIGNUP/SIGNIN reference tables)
107
+ };
108
+ const tableOf = (s: DefineStatement) => s.table ?? s.name;
109
+
110
+ /**
111
+ * The up-migration statement(s) for a CHANGED object:
112
+ * - `table` -> a clause-level `ALTER TABLE` (falls back to `DEFINE … OVERWRITE` when the delta
113
+ * can't be expressed via ALTER, e.g. a `TYPE` NORMAL/RELATION change or an older snapshot),
114
+ * - `field` -> a clause-level `ALTER FIELD` (falls back to `DEFINE … OVERWRITE` when the delta
115
+ * can't be expressed via ALTER, e.g. a `COMPUTED` change or an older snapshot w/o clauses),
116
+ * - `index` -> `REMOVE` + `DEFINE` (ALTER INDEX can't change fields/kind),
117
+ * - everything else (event/function/access) -> `DEFINE … OVERWRITE`.
118
+ */
119
+ function changeUp(old: DefineStatement, next: DefineStatement): string[] {
120
+ if (next.kind === "table") {
121
+ const alt = alterTable(next.name, old.clauses, next.clauses);
122
+ return [alt ?? overwriteStatement(next.ddl)];
123
+ }
124
+ if (next.kind === "field") {
125
+ const alt = alterField(tableOf(next), next.name, old.clauses, next.clauses);
126
+ return [alt ?? overwriteStatement(next.ddl)];
127
+ }
128
+ if (next.kind === "index") return [removeStatement(old), next.ddl];
129
+ return [overwriteStatement(next.ddl)];
130
+ }
131
+ /** The inverse (next -> old) statement(s) for the down migration. */
132
+ function changeDown(old: DefineStatement, next: DefineStatement): string[] {
133
+ if (next.kind === "table") {
134
+ const alt = alterTable(old.name, next.clauses, old.clauses);
135
+ return [alt ?? overwriteStatement(old.ddl)];
136
+ }
137
+ if (next.kind === "field") {
138
+ const alt = alterField(tableOf(old), old.name, next.clauses, old.clauses);
139
+ return [alt ?? overwriteStatement(old.ddl)];
140
+ }
141
+ if (next.kind === "index") return [removeStatement(next), old.ddl];
142
+ return [overwriteStatement(old.ddl)];
143
+ }
144
+
145
+ /**
146
+ * Diff two snapshots into `up`/`down` SurrealQL. Added/changed objects → `DEFINE` (with
147
+ * `OVERWRITE` when changed); dropped objects → `REMOVE`. `down` is the exact inverse, so a
148
+ * migration can be rolled back. Fields of a dropped/added table are skipped (the `TABLE`
149
+ * statement covers them).
150
+ */
151
+ export function diffSnapshots(prev: Snapshot, next: Snapshot): Diff {
152
+ const prevS = prev.statements;
153
+ const nextS = next.statements;
154
+
155
+ // Group output by table: tables in snapshot order, each followed by its own fields/indexes.
156
+ const tableOrder = new Map<string, number>();
157
+ for (const s of [...Object.values(nextS), ...Object.values(prevS)]) {
158
+ const t = tableOf(s);
159
+ if (!tableOrder.has(t)) tableOrder.set(t, tableOrder.size);
160
+ }
161
+ const ord = (s: DefineStatement) => tableOrder.get(tableOf(s)) ?? 0;
162
+ const byCreate = (a: DefineStatement, b: DefineStatement) =>
163
+ ord(a) - ord(b) || RANK[a.kind] - RANK[b.kind];
164
+ const byDrop = (a: DefineStatement, b: DefineStatement) =>
165
+ ord(b) - ord(a) || RANK[b.kind] - RANK[a.kind];
166
+
167
+ const removed = Object.keys(prevS)
168
+ .filter((k) => !(k in nextS))
169
+ .map((k) => prevS[k]);
170
+ const removedTables = new Set(
171
+ removed.filter((s) => s.kind === "table").map((s) => s.name),
172
+ );
173
+
174
+ // Structural change-detection: a DDL difference that normalizes away (e.g. an enum/union/record
175
+ // reorder) is NOT a real change. Only applies when BOTH snapshots carry a Struct; older snapshots
176
+ // (or keys absent from the struct, like folded array elements) fall back to the DDL comparison.
177
+ const prevIdx = prev.struct ? structIndex(prev.struct) : null;
178
+ const nextIdx = next.struct ? structIndex(next.struct) : null;
179
+ const added: SnapshotStatement[] = [];
180
+ const changed: { old: SnapshotStatement; next: SnapshotStatement }[] = [];
181
+ for (const k of Object.keys(nextS)) {
182
+ const s = nextS[k];
183
+ if (!(k in prevS)) {
184
+ added.push(s);
185
+ continue;
186
+ }
187
+ if (prevS[k].ddl === s.ddl) continue;
188
+ if (prevIdx && nextIdx) {
189
+ const a = prevIdx.get(k);
190
+ const b = nextIdx.get(k);
191
+ if (a !== undefined && b !== undefined && deepEqual(a, b)) continue; // cosmetic-only
192
+ }
193
+ changed.push({ old: prevS[k], next: s });
194
+ }
195
+ const addedTables = new Set(
196
+ added.filter((s) => s.kind === "table").map((s) => s.name),
197
+ );
198
+
199
+ // A field/index whose owning table is dropped (or added) is covered by the TABLE statement.
200
+ const isOrphan = (s: DefineStatement, droppedTables: Set<string>) =>
201
+ s.kind !== "table" && droppedTables.has(s.table ?? "");
202
+
203
+ const up: string[] = [];
204
+ for (const s of removed
205
+ .filter((s) => !isOrphan(s, removedTables))
206
+ .sort(byDrop)) {
207
+ up.push(removeStatement(s));
208
+ }
209
+ for (const s of [...added].sort(byCreate)) up.push(s.ddl);
210
+ for (const c of [...changed].sort((a, b) => byCreate(a.next, b.next)))
211
+ up.push(...changeUp(c.old, c.next));
212
+
213
+ const down: string[] = [];
214
+ for (const s of added.filter((s) => !isOrphan(s, addedTables)).sort(byDrop)) {
215
+ down.push(removeStatement(s));
216
+ }
217
+ for (const s of [...removed].sort(byCreate)) down.push(s.ddl);
218
+ for (const c of [...changed].sort((a, b) => byCreate(a.next, b.next)))
219
+ down.push(...changeDown(c.old, c.next));
220
+
221
+ // Structured display items, grouped by table (table → its fields → its indexes).
222
+ const tagged: { sort: [number, number]; item: DiffItem }[] = [];
223
+ const at = (s: DefineStatement): [number, number] => [ord(s), RANK[s.kind]];
224
+ for (const s of removed.filter((s) => !isOrphan(s, removedTables))) {
225
+ tagged.push({
226
+ sort: at(s),
227
+ item: {
228
+ op: "remove",
229
+ kind: s.kind,
230
+ key: keyOf(s),
231
+ table: tableOf(s),
232
+ file: s.file,
233
+ ddl: removeStatement(s),
234
+ old: s.ddl,
235
+ },
236
+ });
237
+ }
238
+ for (const s of added) {
239
+ tagged.push({
240
+ sort: at(s),
241
+ item: {
242
+ op: "add",
243
+ kind: s.kind,
244
+ key: keyOf(s),
245
+ table: tableOf(s),
246
+ file: s.file,
247
+ ddl: s.ddl,
248
+ },
249
+ });
250
+ }
251
+ for (const c of changed) {
252
+ tagged.push({
253
+ sort: at(c.next),
254
+ item: {
255
+ op: "change",
256
+ kind: c.next.kind,
257
+ key: keyOf(c.next),
258
+ table: tableOf(c.next),
259
+ file: c.next.file ?? c.old.file,
260
+ before: c.old.ddl,
261
+ after: c.next.ddl,
262
+ },
263
+ });
264
+ }
265
+ tagged.sort((a, b) => a.sort[0] - b.sort[0] || a.sort[1] - b.sort[1]);
266
+ const items = tagged.map((t) => t.item);
267
+
268
+ const full = Object.values(nextS)
269
+ .sort(byCreate)
270
+ .map((s) => ({ key: keyOf(s), table: tableOf(s), ddl: s.ddl }));
271
+
272
+ return { up, down, items, full };
273
+ }
274
+
275
+ /** The table a statement targets (for grouping the migration body). */
276
+ function stmtTable(s: string): string {
277
+ const on = /\bON (?:TABLE )?`?([^\s`;]+)`?/.exec(s);
278
+ if (on) return on[1];
279
+ const t = /\bTABLE (?:OVERWRITE |IF (?:NOT )?EXISTS )?`?([^\s`;]+)`?/.exec(s);
280
+ return t ? t[1] : "";
281
+ }
282
+
283
+ /** Indent statements, with a blank line between consecutive table groups. */
284
+ const indent = (stmts: string[]): string => {
285
+ const out: string[] = [];
286
+ let prev: string | undefined;
287
+ for (const s of stmts) {
288
+ const t = stmtTable(s);
289
+ if (prev !== undefined && t !== prev) out.push("");
290
+ out.push(` ${s}`);
291
+ prev = t;
292
+ }
293
+ return out.join("\n");
294
+ };
295
+
296
+ /**
297
+ * Render a migration as a single self-contained SurrealQL program. Applied with the
298
+ * `$direction` parameter bound to `"up"` or `"down"` — verified to support `DEFINE`/`REMOVE`
299
+ * inside `IF` blocks on SurrealDB 3.x.
300
+ */
301
+ export function renderMigration(tag: string, diff: Diff): string {
302
+ // A migration is REPLAYED from a fixed file regardless of the database's current state (unlike
303
+ // `diff`/`push`, which recompute a delta each run), so its DEFINEs must be idempotent: re-marking
304
+ // them DEFINE … OVERWRITE re-applies cleanly whether or not the object already exists — and
305
+ // OVERWRITE preserves row data (REMOVEs already use IF EXISTS). This is what lets `schemic migrate`
306
+ // run against a database whose objects were applied out-of-band (e.g. `schemic push`/`schemic pull`). The
307
+ // changed-object statements are already OVERWRITE/ALTER, and `overwriteStatement` is a no-op on
308
+ // those and on REMOVE/ALTER, so only plain DEFINEs (added objects) gain the keyword.
309
+ const idempotent = (stmts: string[]) => stmts.map(overwriteStatement);
310
+ return [
311
+ `-- ${tag}`,
312
+ "-- Generated by @schemic/core. Review before applying.",
313
+ "",
314
+ 'IF $direction = "up" {',
315
+ indent(idempotent(diff.up)),
316
+ "} ELSE {",
317
+ indent(idempotent(diff.down)),
318
+ "};",
319
+ "",
320
+ ].join("\n");
321
+ }
@@ -0,0 +1,67 @@
1
+ // The SurrealDB STATEMENT/STRUCT filters — the dialect halves of the per-kind object filter. They
2
+ // operate on `DefineStatement`/`Snapshot` (the statement engine) and `DbStructured` (introspection).
3
+ // The dialect-free `Filter` definition + the portable-IR filters live in `./filter`. In the package
4
+ // split this module moves to `@schemic/surrealdb` (see docs/MULTI-DB-SPIKE.md).
5
+
6
+ import { type Filter, inCat } from "@schemic/core";
7
+ import type { DefineStatement } from "../ddl";
8
+ import type { DbStructured, Snapshot } from "./structure";
9
+
10
+ /** Whether a `DefineStatement` passes the filter (table-scoped objects also need their table in). */
11
+ export function included(f: Filter, s: DefineStatement): boolean {
12
+ const table = s.table ?? s.name;
13
+ switch (s.kind) {
14
+ case "table":
15
+ case "field":
16
+ case "index":
17
+ return inCat(f.tables, table);
18
+ case "event":
19
+ return inCat(f.tables, table) && inCat(f.events, s.name);
20
+ case "function":
21
+ return inCat(f.functions, s.name);
22
+ case "access":
23
+ return inCat(f.access, s.name);
24
+ case "analyzer":
25
+ // Analyzers are shared infra a FULLTEXT index depends on — always kept (no filter category).
26
+ return true;
27
+ }
28
+ }
29
+
30
+ /** Keep only the snapshot statements that pass the filter (for `diff`/`sync`/`generate`). */
31
+ export function filterSnapshot(snap: Snapshot, f: Filter): Snapshot {
32
+ const statements: Record<string, DefineStatement> = {};
33
+ for (const [k, s] of Object.entries(snap.statements))
34
+ if (included(f, s)) statements[k] = s;
35
+ return { ...snap, statements };
36
+ }
37
+
38
+ /**
39
+ * The snapshot to persist after a filtered `generate`: included kinds take their new state from
40
+ * `next`, excluded kinds keep their prior state from `prev`. So generating without `--access`
41
+ * leaves the snapshot's access untouched (no phantom add/remove) rather than dropping it.
42
+ */
43
+ export function mergeSnapshot(
44
+ prev: Snapshot,
45
+ next: Snapshot,
46
+ f: Filter,
47
+ ): Snapshot {
48
+ const statements: Record<string, DefineStatement> = {};
49
+ for (const [k, s] of Object.entries(prev.statements))
50
+ if (!included(f, s)) statements[k] = s;
51
+ for (const [k, s] of Object.entries(next.statements))
52
+ if (included(f, s)) statements[k] = s;
53
+ return { ...next, statements };
54
+ }
55
+
56
+ /** Keep only the introspected objects that pass the filter (for `pull`). */
57
+ export function filterStructured(db: DbStructured, f: Filter): DbStructured {
58
+ const tables = db.tables
59
+ .filter((t) => inCat(f.tables, t.name))
60
+ .map((t) => ({
61
+ ...t,
62
+ events: t.events.filter((e) => inCat(f.events, e.name)),
63
+ }));
64
+ const functions = db.functions.filter((fn) => inCat(f.functions, fn.name));
65
+ const accesses = db.accesses.filter((a) => inCat(f.access, a.name));
66
+ return { tables, functions, accesses, analyzers: db.analyzers };
67
+ }
package/src/config.ts ADDED
@@ -0,0 +1,94 @@
1
+ // SurrealDB-specific config types — relocated out of @schemic/core/config, which is now connections-only
2
+ // and dialect-free. A `surrealConnection({ … })` carries these; the resolution engine strips the neutral
3
+ // base (schema/key/migrations) and the rest lands in `ResolvedConfig.params` (read it as {@link SurrealParams}).
4
+
5
+ /** Which level to authenticate at — mirrors `surreal sql --auth-level`. */
6
+ export type AuthLevel = "root" | "namespace" | "database";
7
+
8
+ /** A SurrealDB connection's params (the surreal-specific half of a `surrealConnection` config). */
9
+ export interface SurrealZodConnection {
10
+ /** Endpoint, e.g. `ws://localhost:8000` or `http://localhost:8000`. */
11
+ url: string;
12
+ /** Target namespace. */
13
+ namespace: string;
14
+ /** Target database. */
15
+ database: string;
16
+ /** Auth username. */
17
+ username?: string;
18
+ /** Auth password. */
19
+ password?: string;
20
+ /**
21
+ * Level to sign in at: `root` (default), `namespace`, or `database`. Determines the
22
+ * signin payload — `namespace`/`database` scope the credentials to that ns/db.
23
+ */
24
+ authLevel?: AuthLevel;
25
+ }
26
+
27
+ /** An allow/deny list for a single capability — mirrors `@surrealdb/node`. */
28
+ export interface CapabilityList {
29
+ allow?: boolean | string[];
30
+ deny?: boolean | string[];
31
+ }
32
+
33
+ /** Capabilities for the embedded check engine — mirrors `@surrealdb/node`'s `capabilities` option. */
34
+ export interface EmbeddedCapabilities {
35
+ scripting?: boolean;
36
+ guest_access?: boolean;
37
+ live_query_notifications?: boolean;
38
+ functions?: boolean | string[] | CapabilityList;
39
+ network_targets?: boolean | string[] | CapabilityList;
40
+ experimental?: boolean | string[] | CapabilityList;
41
+ }
42
+
43
+ /**
44
+ * Run `schemic check`'s replay on an EMBEDDED in-process SurrealDB via the optional `@surrealdb/node`
45
+ * package (install it yourself — `npm i -D @surrealdb/node`). Options pass through to
46
+ * `createNodeEngines`; `backend`/`path` choose the storage. No external server, your data untouched.
47
+ */
48
+ export interface SurrealZodCheckEmbedded {
49
+ /** Storage backend. `memory` (default) is throwaway in-RAM; the others persist to `path`. */
50
+ backend?: "memory" | "surrealkv" | "surrealkv+versioned" | "rocksdb";
51
+ /** Filesystem path for the persistent backends (ignored for `memory`). */
52
+ path?: string;
53
+ /** Capabilities for the instance. Default: all allowed, so asserts/defaults/functions work. */
54
+ capabilities?: boolean | EmbeddedCapabilities;
55
+ /** SurrealDB strict mode. */
56
+ strict?: boolean;
57
+ /** Query timeout. */
58
+ query_timeout?: number;
59
+ /** Transaction timeout. */
60
+ transaction_timeout?: number;
61
+ }
62
+
63
+ /** `schemic check` options (lives on a SurrealDB connection's config; rides into `params.check`). */
64
+ export interface SurrealZodCheck {
65
+ /**
66
+ * Engine for the migration replay:
67
+ * - `"auto"` (default) — if the `surreal` CLI is on PATH, spin up an ephemeral in-memory instance
68
+ * (your EXACT SurrealDB version, no external server, your data untouched); otherwise fall back to
69
+ * the `check.db` server.
70
+ * - `"binary"` — require the local `surreal` CLI (error if it's missing).
71
+ * - `"remote"` — always use the `check.db` server (throwaway scratch databases on it).
72
+ * - an embedded object (`{ backend, capabilities, … }`) — run in-process via the optional
73
+ * `@surrealdb/node` package. See {@link SurrealZodCheckEmbedded}.
74
+ */
75
+ engine?: "auto" | "binary" | "remote" | SurrealZodCheckEmbedded;
76
+ /** Path to the `surreal` CLI for the `auto`/`binary` engines. Default: `surreal` on PATH. */
77
+ binary?: string;
78
+ /**
79
+ * Connection used for the `remote` engine, merged field-by-field over the connection's own params.
80
+ * The replay spins up throwaway scratch databases and drops them — it NEVER reads or writes your real
81
+ * database — but it DOES reach the server. Point this at a local/scratch SurrealDB so `schemic check`
82
+ * never touches production. Falls back to the connection params for any field you omit.
83
+ */
84
+ db?: Partial<SurrealZodConnection>;
85
+ }
86
+
87
+ /**
88
+ * The SurrealDB-specific bag carried in `ResolvedConfig.params`: the connection params plus the optional
89
+ * `check` replay config. `connect`/`introspect`/`checkReplay` read `config.params as SurrealParams`.
90
+ */
91
+ export interface SurrealParams extends SurrealZodConnection {
92
+ /** `schemic check` overrides — e.g. a dedicated connection for its migration replay. */
93
+ check?: SurrealZodCheck;
94
+ }
@@ -0,0 +1,51 @@
1
+ // The SurrealDB connection factory — binds the neutral `connectionEntry` (from @schemic/core) to the
2
+ // SurrealDB connection shape, so `defineConfig({ connections: { … } })` gets a typed `surrealConnection`
3
+ // with no hand-authored `driver: "…"` string. Design: @schemic/core docs/MULTI-CONNECTION.md.
4
+
5
+ import {
6
+ type ConnectionConfigBase,
7
+ type ConnectionEntry,
8
+ type ConnectionInput,
9
+ connectionEntry,
10
+ type ResolveContext,
11
+ } from "@schemic/core/driver";
12
+ import type { SurrealZodCheck, SurrealZodConnection } from "./config";
13
+
14
+ /**
15
+ * A SurrealDB connection's config: the dialect-neutral base (`schema`, optional `key`/`migrations`)
16
+ * plus the SurrealDB-specific connection params and the optional `check` replay config. Read env
17
+ * yourself in a resolver if you need it — there is no implicit `SURREAL_*` magic. The resolution engine
18
+ * strips the neutral base; the surreal half (url/namespace/…/check) lands in `ResolvedConfig.params`.
19
+ */
20
+ export interface SurrealConnectionConfig
21
+ extends ConnectionConfigBase,
22
+ SurrealZodConnection {
23
+ /** `schemic check` overrides — e.g. a dedicated scratch connection for the migration replay. */
24
+ check?: SurrealZodCheck;
25
+ }
26
+
27
+ /**
28
+ * Build a SurrealDB {@link ConnectionEntry} for a config's `connections` map. Three forms:
29
+ * a single static config, a resolver returning one config, or a resolver returning a keyed
30
+ * COLLECTION (one connection per entry — `key` is then required and addressable as `<name>:<key>`).
31
+ */
32
+ export function surrealConnection(
33
+ config: SurrealConnectionConfig,
34
+ ): ConnectionEntry;
35
+ export function surrealConnection(
36
+ resolve: (
37
+ ctx: ResolveContext,
38
+ ) => SurrealConnectionConfig | Promise<SurrealConnectionConfig>,
39
+ ): ConnectionEntry;
40
+ export function surrealConnection(
41
+ resolve: (
42
+ ctx: ResolveContext,
43
+ ) =>
44
+ | (SurrealConnectionConfig & { key: string })[]
45
+ | Promise<(SurrealConnectionConfig & { key: string })[]>,
46
+ ): ConnectionEntry;
47
+ export function surrealConnection(
48
+ input: ConnectionInput<SurrealConnectionConfig>,
49
+ ): ConnectionEntry {
50
+ return connectionEntry<SurrealConnectionConfig>("surrealdb", input);
51
+ }