@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.
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/lib/index.d.ts +1231 -0
- package/lib/index.js +5019 -0
- package/lib/index.js.map +1 -0
- package/package.json +68 -0
- package/src/cli/engine.ts +189 -0
- package/src/cli/introspect.ts +275 -0
- package/src/cli/lower.ts +370 -0
- package/src/cli/pull.ts +1049 -0
- package/src/cli/scaffold.ts +167 -0
- package/src/cli/struct.ts +0 -0
- package/src/cli/structure.ts +696 -0
- package/src/cli/surreal-connect.ts +112 -0
- package/src/cli/surreal-diff.ts +321 -0
- package/src/cli/surreal-filter.ts +67 -0
- package/src/config.ts +94 -0
- package/src/connection.ts +51 -0
- package/src/ddl.ts +931 -0
- package/src/driver/surql-type.ts +191 -0
- package/src/driver/surreal.ts +364 -0
- package/src/index.ts +99 -0
- package/src/kinds/explode.ts +201 -0
- package/src/kinds/portable.ts +116 -0
- package/src/kinds/registry.ts +177 -0
- package/src/pure.ts +2671 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// The SurrealQL type-expression <-> PortableType bridge (Milestone 2). This is the Surreal driver's
|
|
2
|
+
// `parseType`/`emitType`: it lifts the Struct-IR's `kind` STRING (e.g. `option<int>`,
|
|
3
|
+
// `array<record<user>, 3>`, `'a' | 'b'`) into the dialect-independent PortableType, and renders it
|
|
4
|
+
// back to the canonical SurrealQL spelling (the form `normalizeType` produces). Round-tripping
|
|
5
|
+
// proves the portable model is LOSSLESS for the Surreal dialect — the precondition for flipping diff
|
|
6
|
+
// equality to a structured deep-compare (see docs/MULTI-DB-SPIKE.md, Part 6).
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
array,
|
|
10
|
+
literal,
|
|
11
|
+
nullable,
|
|
12
|
+
option,
|
|
13
|
+
type PortableType,
|
|
14
|
+
record,
|
|
15
|
+
type ScalarName,
|
|
16
|
+
scalar,
|
|
17
|
+
union,
|
|
18
|
+
} from "@schemic/core";
|
|
19
|
+
|
|
20
|
+
const SCALARS = new Set<string>([
|
|
21
|
+
"any",
|
|
22
|
+
"bool",
|
|
23
|
+
"string",
|
|
24
|
+
"int",
|
|
25
|
+
"float",
|
|
26
|
+
"decimal",
|
|
27
|
+
"number",
|
|
28
|
+
"datetime",
|
|
29
|
+
"duration",
|
|
30
|
+
"uuid",
|
|
31
|
+
"bytes",
|
|
32
|
+
"null",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const GEOMETRY_KINDS = new Set([
|
|
36
|
+
"feature",
|
|
37
|
+
"point",
|
|
38
|
+
"line",
|
|
39
|
+
"polygon",
|
|
40
|
+
"multipoint",
|
|
41
|
+
"multiline",
|
|
42
|
+
"multipolygon",
|
|
43
|
+
"collection",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
/** Split a type expression on its top-level `|` (ignoring `|` inside `<…>`). */
|
|
47
|
+
function splitTopUnion(expr: string): string[] {
|
|
48
|
+
const parts: string[] = [];
|
|
49
|
+
let depth = 0;
|
|
50
|
+
let cur = "";
|
|
51
|
+
for (const c of expr) {
|
|
52
|
+
if (c === "<") depth++;
|
|
53
|
+
else if (c === ">") depth--;
|
|
54
|
+
if (c === "|" && depth === 0) {
|
|
55
|
+
parts.push(cur.trim());
|
|
56
|
+
cur = "";
|
|
57
|
+
} else cur += c;
|
|
58
|
+
}
|
|
59
|
+
parts.push(cur.trim());
|
|
60
|
+
return parts;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Split `s` once on the first top-level `sep` (outside `<…>`), or null if absent. */
|
|
64
|
+
function topLevelSplitOnce(s: string, sep: string): [string, string] | null {
|
|
65
|
+
let depth = 0;
|
|
66
|
+
for (let i = 0; i < s.length; i++) {
|
|
67
|
+
const c = s[i];
|
|
68
|
+
if (c === "<") depth++;
|
|
69
|
+
else if (c === ">") depth--;
|
|
70
|
+
else if (c === sep && depth === 0) return [s.slice(0, i), s.slice(i + 1)];
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Parse a (single/double) quoted string literal token, or null if it isn't one. */
|
|
76
|
+
function parseStringLiteral(t: string): string | null {
|
|
77
|
+
if (t.length >= 2 && (t[0] === "'" || t[0] === '"') && t.at(-1) === t[0]) {
|
|
78
|
+
const body = t.slice(1, -1);
|
|
79
|
+
return body.replace(/\\(['"\\])/g, "$1");
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parse a SurrealQL type expression into a {@link PortableType}. Mirrors the grammar `normalizeType`
|
|
86
|
+
* canonicalizes: `option<…>`, top-level unions (a `none` member ⇒ `option`, a `null` member ⇒
|
|
87
|
+
* `nullable`), `array`/`set`/`record`/`references` constructors, literals, and scalars.
|
|
88
|
+
*/
|
|
89
|
+
export function parseSurqlType(kind: string): PortableType {
|
|
90
|
+
const t = kind.trim();
|
|
91
|
+
|
|
92
|
+
// option<X>
|
|
93
|
+
const opt = /^option<([\s\S]+)>$/.exec(t);
|
|
94
|
+
if (opt) return option(parseSurqlType(opt[1]));
|
|
95
|
+
|
|
96
|
+
// Top-level union: peel `none` (absence) and `null` (null) markers, recurse on the rest.
|
|
97
|
+
const parts = splitTopUnion(t);
|
|
98
|
+
if (parts.length > 1) {
|
|
99
|
+
const hasNone = parts.includes("none");
|
|
100
|
+
const hasNull = parts.includes("null");
|
|
101
|
+
const rest = parts.filter((p) => p !== "none" && p !== "null");
|
|
102
|
+
let inner: PortableType =
|
|
103
|
+
rest.length === 0 ? { t: "never" } : union(rest.map(parseSurqlType));
|
|
104
|
+
if (hasNull) inner = nullable(inner);
|
|
105
|
+
if (hasNone) inner = option(inner);
|
|
106
|
+
return inner;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Constructor term `ctor<inner>`. `references<…>` (rare) is kept as a Surreal-native escape hatch
|
|
110
|
+
// so it round-trips losslessly rather than collapsing into `record<…>`.
|
|
111
|
+
const ctor = /^(array|set|record)<([\s\S]+)>$/.exec(t);
|
|
112
|
+
if (ctor) {
|
|
113
|
+
const [, name, innerRaw] = ctor;
|
|
114
|
+
if (name === "array" || name === "set") {
|
|
115
|
+
const comma = topLevelSplitOnce(innerRaw, ",");
|
|
116
|
+
const elem = parseSurqlType(comma ? comma[0] : innerRaw);
|
|
117
|
+
const size = comma ? Number(comma[1].trim()) : undefined;
|
|
118
|
+
return name === "array"
|
|
119
|
+
? array(elem, Number.isFinite(size) ? size : undefined)
|
|
120
|
+
: { t: "set", elem, ...(Number.isFinite(size) ? { size } : {}) };
|
|
121
|
+
}
|
|
122
|
+
// record<…>: a `|`-list of target tables.
|
|
123
|
+
const tables = innerRaw
|
|
124
|
+
.split("|")
|
|
125
|
+
.map((s) => s.trim())
|
|
126
|
+
.filter(Boolean);
|
|
127
|
+
return record(tables);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// geometry<point> etc.
|
|
131
|
+
const geo = /^geometry<([a-z]+)>$/.exec(t);
|
|
132
|
+
if (geo && GEOMETRY_KINDS.has(geo[1])) {
|
|
133
|
+
return { t: "geometry", kind: geo[1] as never };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Literals: quoted string, number, boolean.
|
|
137
|
+
const str = parseStringLiteral(t);
|
|
138
|
+
if (str !== null) return literal(str);
|
|
139
|
+
if (/^-?\d+(\.\d+)?$/.test(t)) return literal(Number(t));
|
|
140
|
+
if (t === "true" || t === "false") return literal(t === "true");
|
|
141
|
+
|
|
142
|
+
// Scalars (incl. `null` as a unit type). `none` standalone ⇒ option<never>. `object` ⇒ empty object.
|
|
143
|
+
if (t === "none") return option({ t: "never" });
|
|
144
|
+
if (t === "object") return { t: "object", fields: {} };
|
|
145
|
+
if (SCALARS.has(t)) return scalar(t as ScalarName);
|
|
146
|
+
|
|
147
|
+
// Unknown: keep it as a Surreal-native escape hatch rather than losing information.
|
|
148
|
+
return { t: "native", db: "surreal", name: t };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Render a {@link PortableType} back to its canonical SurrealQL spelling (matches `normalizeType`). */
|
|
152
|
+
export function emitSurqlType(p: PortableType): string {
|
|
153
|
+
switch (p.t) {
|
|
154
|
+
case "scalar":
|
|
155
|
+
return p.name;
|
|
156
|
+
case "literal":
|
|
157
|
+
return typeof p.value === "string"
|
|
158
|
+
? `'${p.value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`
|
|
159
|
+
: String(p.value);
|
|
160
|
+
case "option":
|
|
161
|
+
return `option<${emitSurqlType(p.inner)}>`;
|
|
162
|
+
case "nullable": {
|
|
163
|
+
// Canonical: a sorted top-level union with `null` (matches normalizeType, which sorts members
|
|
164
|
+
// and does not special-case null). `null` sorts before any scalar/ctor name.
|
|
165
|
+
const inner = emitSurqlType(p.inner);
|
|
166
|
+
return [inner, "null"].sort().join(" | ");
|
|
167
|
+
}
|
|
168
|
+
case "array":
|
|
169
|
+
return p.size !== undefined
|
|
170
|
+
? `array<${emitSurqlType(p.elem)}, ${p.size}>`
|
|
171
|
+
: `array<${emitSurqlType(p.elem)}>`;
|
|
172
|
+
case "set":
|
|
173
|
+
return p.size !== undefined
|
|
174
|
+
? `set<${emitSurqlType(p.elem)}, ${p.size}>`
|
|
175
|
+
: `set<${emitSurqlType(p.elem)}>`;
|
|
176
|
+
case "union":
|
|
177
|
+
return [...p.members.map(emitSurqlType)].sort().join(" | ");
|
|
178
|
+
case "object":
|
|
179
|
+
return "object";
|
|
180
|
+
case "record":
|
|
181
|
+
return `record<${[...p.tables].sort().join(" | ")}>`;
|
|
182
|
+
case "geometry":
|
|
183
|
+
return `geometry<${p.kind}>`;
|
|
184
|
+
case "never":
|
|
185
|
+
return "none";
|
|
186
|
+
case "native":
|
|
187
|
+
return p.name;
|
|
188
|
+
}
|
|
189
|
+
// Unreachable: the switch is exhaustive over PortableType["t"].
|
|
190
|
+
throw new Error(`unhandled portable type: ${(p as { t: string }).t}`);
|
|
191
|
+
}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
// The SURREAL driver — a SET OF KINDS on the core-v2 registry (see docs/kind-registry-flip-plan.md).
|
|
2
|
+
//
|
|
3
|
+
// Everything dialect-specific lives here: the kind `registry` (table/index/event/function/access), the
|
|
4
|
+
// authoring -> kinded `explode`, the single-read `introspectAll`, the connection lifecycle, and the
|
|
5
|
+
// SurrealDB command capabilities. Core orchestrates schema ops (lower/diff/emit/order) GENERICALLY over
|
|
6
|
+
// `registry` — it never names a kind. The Struct-IR (`DbStructured`) + `diffSnapshots` stay the driver's
|
|
7
|
+
// INTERNAL clause-level engine (the kinds delegate to them); the field/type substrate stays core.
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
ApplyOptions,
|
|
11
|
+
ConnectionOverrides as CfgOverrides,
|
|
12
|
+
ConnectionOverrides,
|
|
13
|
+
Definable,
|
|
14
|
+
Diff,
|
|
15
|
+
Driver,
|
|
16
|
+
Filter,
|
|
17
|
+
MigrationRecord,
|
|
18
|
+
MigrationStore,
|
|
19
|
+
PortableObject,
|
|
20
|
+
PullPlan,
|
|
21
|
+
RenderedUnit,
|
|
22
|
+
ResolvedConfig,
|
|
23
|
+
ShadowCapability,
|
|
24
|
+
} from "@schemic/core";
|
|
25
|
+
import { registerDriver } from "@schemic/core";
|
|
26
|
+
import { escapeIdent, type Surreal } from "surrealdb";
|
|
27
|
+
import {
|
|
28
|
+
connectEmbedded,
|
|
29
|
+
spawnEphemeralServer,
|
|
30
|
+
surrealBinaryAvailable,
|
|
31
|
+
} from "../cli/engine";
|
|
32
|
+
import {
|
|
33
|
+
applyStatements,
|
|
34
|
+
diffAgainstDb,
|
|
35
|
+
shadowStructured,
|
|
36
|
+
syncPlan,
|
|
37
|
+
tsStructsAgainstDb,
|
|
38
|
+
verifyMigrations,
|
|
39
|
+
} from "../cli/introspect";
|
|
40
|
+
import { planPull, renderPerFile, renderSchemaToTS } from "../cli/pull";
|
|
41
|
+
import { initScaffold, scaffoldEntity } from "../cli/scaffold";
|
|
42
|
+
import { normalizeDb } from "../cli/struct";
|
|
43
|
+
import type { DbStructured } from "../cli/structure";
|
|
44
|
+
import { connect as surrealConnect } from "../cli/surreal-connect";
|
|
45
|
+
import { renderMigration } from "../cli/surreal-diff";
|
|
46
|
+
import { filterStructured } from "../cli/surreal-filter";
|
|
47
|
+
import type { SurrealParams } from "../config";
|
|
48
|
+
import {
|
|
49
|
+
explodeSchema,
|
|
50
|
+
fromStructured,
|
|
51
|
+
introspectAll as introspectAllKinds,
|
|
52
|
+
toStructured,
|
|
53
|
+
} from "../kinds/explode";
|
|
54
|
+
import type { SurrealPortable } from "../kinds/portable";
|
|
55
|
+
import { surrealKinds } from "../kinds/registry";
|
|
56
|
+
import type { Shape, StandaloneDef, TableDef } from "../pure";
|
|
57
|
+
|
|
58
|
+
const shadow: ShadowCapability<Surreal> = {
|
|
59
|
+
// Apply DDL to a throwaway database, read it back via INFO STRUCTURE, drop it — the live-side
|
|
60
|
+
// canonicalizer. Delegates to `shadowStructured`, then to per-kind portable objects (== lowering).
|
|
61
|
+
roundTrip: async (conn, config, ddl) =>
|
|
62
|
+
fromStructured(normalizeDb(await shadowStructured(conn, config, ddl))),
|
|
63
|
+
// `ephemeral` (full isolated instance for `sz check` replay) is intentionally not wired here —
|
|
64
|
+
// `check` still uses its existing path. A later milestone routes it through this capability.
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// --- migration bookkeeping (the apply-time SurrealQL, moved behind the driver) -------------------
|
|
68
|
+
|
|
69
|
+
/** Record one applied migration: a `_migrations` row keyed by tag, with file + checksum + time. */
|
|
70
|
+
const RECORD_SQL =
|
|
71
|
+
"CREATE type::record($tbl, $tag) CONTENT { tag: $tag, file: $file, checksum: $sum, applied_at: time::now() }";
|
|
72
|
+
const recordVars = (table: string, r: MigrationRecord) => ({
|
|
73
|
+
tbl: table,
|
|
74
|
+
tag: r.tag,
|
|
75
|
+
file: r.file,
|
|
76
|
+
sum: r.checksum,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
/** `DEFINE TABLE … SCHEMALESS` for an internal tracking table (migrations or its lock). */
|
|
80
|
+
async function ensureTrackTable(conn: Surreal, table: string): Promise<void> {
|
|
81
|
+
await conn.query(
|
|
82
|
+
`DEFINE TABLE IF NOT EXISTS ${escapeIdent(table)} TYPE NORMAL SCHEMALESS PERMISSIONS NONE;`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const lockTableOf = (table: string) => `${table}_lock`;
|
|
87
|
+
|
|
88
|
+
const migrations: MigrationStore<Surreal> = {
|
|
89
|
+
// SurrealDB migrations are SurrealQL scripts.
|
|
90
|
+
extension: ".surql",
|
|
91
|
+
render: (tag, diff) => renderMigration(tag, diff),
|
|
92
|
+
ensure: ensureTrackTable,
|
|
93
|
+
|
|
94
|
+
async applied(conn, table) {
|
|
95
|
+
const [rows] = await conn.query<[{ tag: string; checksum: string }[]]>(
|
|
96
|
+
"SELECT tag, checksum FROM type::table($tbl)",
|
|
97
|
+
{ tbl: table },
|
|
98
|
+
);
|
|
99
|
+
return new Map((rows ?? []).map((r) => [r.tag, r.checksum]));
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
// SurrealDB is natively transactional — run the migration program + its bookkeeping write in one
|
|
103
|
+
// BEGIN/COMMIT so the record is written iff the DDL applied. `$direction` drives the up/down branch.
|
|
104
|
+
async apply(conn, table, { content, direction, record }) {
|
|
105
|
+
const bookkeep =
|
|
106
|
+
direction === "up"
|
|
107
|
+
? { sql: RECORD_SQL, vars: recordVars(table, record) }
|
|
108
|
+
: {
|
|
109
|
+
sql: "DELETE type::record($tbl, $tag)",
|
|
110
|
+
vars: { tbl: table, tag: record.tag },
|
|
111
|
+
};
|
|
112
|
+
await conn.query(`BEGIN;\n${content}\n${bookkeep.sql};\nCOMMIT;`, {
|
|
113
|
+
direction,
|
|
114
|
+
...bookkeep.vars,
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
async record(conn, table, record) {
|
|
119
|
+
await ensureTrackTable(conn, table);
|
|
120
|
+
await conn.query(RECORD_SQL, recordVars(table, record));
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
async clear(conn, table) {
|
|
124
|
+
await conn.query("DELETE type::table($tbl)", { tbl: table });
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
async lock(conn, table) {
|
|
128
|
+
const tbl = lockTableOf(table);
|
|
129
|
+
await ensureTrackTable(conn, tbl);
|
|
130
|
+
try {
|
|
131
|
+
await conn.query(
|
|
132
|
+
"CREATE type::record($tbl, 'lock') CONTENT { at: time::now() }",
|
|
133
|
+
{ tbl },
|
|
134
|
+
);
|
|
135
|
+
} catch {
|
|
136
|
+
throw new Error(
|
|
137
|
+
"Migrations are locked — another run is in progress. If it's stale, run `schemic unlock`.",
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
async unlock(conn, table) {
|
|
143
|
+
await conn.query("DELETE type::record($tbl, 'lock')", {
|
|
144
|
+
tbl: lockTableOf(table),
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Render a structured schema to per-file source: one combined module under `single`, else one file
|
|
151
|
+
* per object via `fileFor`. Shared by `renderSchema` (offline `diff --ts`) and `diffTsLive`.
|
|
152
|
+
*/
|
|
153
|
+
function renderFiles(
|
|
154
|
+
struct: DbStructured,
|
|
155
|
+
fileFor: (kind: string, name: string) => string,
|
|
156
|
+
single?: string,
|
|
157
|
+
): Map<string, string> {
|
|
158
|
+
return single
|
|
159
|
+
? new Map([[single, renderSchemaToTS(struct)]])
|
|
160
|
+
: renderPerFile(
|
|
161
|
+
struct,
|
|
162
|
+
fileFor as (kind: RenderedUnit["kind"], name: string) => string,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export const surrealDriver: Driver<
|
|
167
|
+
Surreal,
|
|
168
|
+
TableDef<string, Shape>,
|
|
169
|
+
StandaloneDef
|
|
170
|
+
> = {
|
|
171
|
+
name: "surrealdb",
|
|
172
|
+
registry: surrealKinds,
|
|
173
|
+
|
|
174
|
+
// --- kind registry (the schema engine) -----------------------------------------------------
|
|
175
|
+
// Authoring -> kinded definables (the driver-side explode: one inline-authored table fans out into
|
|
176
|
+
// [table, ...index, ...event], db-level functions/accesses alongside). Core lowers via
|
|
177
|
+
// `lowerSchema(registry, explode(...))`, then diffs/emits/orders GENERICALLY — it never names a kind.
|
|
178
|
+
explode: (tables, defs) => explodeSchema(tables, defs),
|
|
179
|
+
// Live DB -> all portable objects: ONE `INFO … STRUCTURE` read fanned per kind, canonicalized
|
|
180
|
+
// IDENTICALLY to lowering (a clean apply round-trips to a zero diff) and complete (every kind).
|
|
181
|
+
introspectAll: (conn, exclude) => introspectAllKinds(conn, exclude),
|
|
182
|
+
|
|
183
|
+
// --- execution -----------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
connect(
|
|
186
|
+
config: ResolvedConfig,
|
|
187
|
+
over?: ConnectionOverrides,
|
|
188
|
+
): Promise<Surreal> {
|
|
189
|
+
// The driver's portable `ConnectionOverrides` is a structural superset of the SDK's; the only
|
|
190
|
+
// soft field is `authLevel` (a string here vs. the SDK's `AuthLevel` union) — pass it through.
|
|
191
|
+
return surrealConnect(config, (over ?? {}) as CfgOverrides);
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
async apply(
|
|
195
|
+
conn: Surreal,
|
|
196
|
+
statements: string[],
|
|
197
|
+
opts?: ApplyOptions,
|
|
198
|
+
): Promise<void> {
|
|
199
|
+
if (!statements.length) return;
|
|
200
|
+
if (opts?.transactional === false) {
|
|
201
|
+
for (const s of statements) await conn.query(s);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// SurrealDB is natively transactional — one BEGIN/COMMIT around the batch (matches applyStatements).
|
|
205
|
+
await applyStatements(conn, statements);
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
async close(conn: Surreal): Promise<void> {
|
|
209
|
+
await conn.close();
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Raw READ query -> rows, for connection resolvers (`ctx.connections.<name>.query(...)`) and seed.
|
|
214
|
+
* SurrealQL returns one result per statement; we hand back the LAST statement's rows (the payload of
|
|
215
|
+
* a `… ; SELECT …` resolver), wrapping a scalar/`RETURN` result and treating none as an empty set.
|
|
216
|
+
*/
|
|
217
|
+
async query<T = unknown>(
|
|
218
|
+
conn: Surreal,
|
|
219
|
+
sql: string,
|
|
220
|
+
vars?: Record<string, unknown>,
|
|
221
|
+
): Promise<T[]> {
|
|
222
|
+
const results = (await conn.query(sql, vars)) as unknown[];
|
|
223
|
+
if (!results.length) return [];
|
|
224
|
+
const last = results[results.length - 1];
|
|
225
|
+
return (Array.isArray(last) ? last : last == null ? [] : [last]) as T[];
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
shadow,
|
|
229
|
+
migrations,
|
|
230
|
+
|
|
231
|
+
// --- command capabilities (thin adapters over the existing surreal cli functions) ----------
|
|
232
|
+
|
|
233
|
+
diffLive: (conn, config, filter) => diffAgainstDb(conn, config, filter),
|
|
234
|
+
syncPlan: (diff, prune) => syncPlan(diff, prune),
|
|
235
|
+
|
|
236
|
+
// Offline `diff --ts` / `pull`: reconstruct the structured form from the portable objects (no DDL
|
|
237
|
+
// re-parse — the normalized struct rides on them), filter, render to per-file `s.*` source.
|
|
238
|
+
renderSchema(objects, filter, fileFor, single) {
|
|
239
|
+
return renderFiles(
|
|
240
|
+
filterStructured(toStructured(objects as SurrealPortable[]), filter),
|
|
241
|
+
fileFor,
|
|
242
|
+
single,
|
|
243
|
+
);
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
// Live `diff --ts`: both sides normalized through SurrealDB, then rendered per file.
|
|
247
|
+
async diffTsLive(conn, config, filter, fileFor, single) {
|
|
248
|
+
const { current, desired } = await tsStructsAgainstDb(conn, config, filter);
|
|
249
|
+
return {
|
|
250
|
+
current: renderFiles(current, fileFor, single),
|
|
251
|
+
desired: renderFiles(desired, fileFor, single),
|
|
252
|
+
};
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
planPull: (conn, config, opts) => planPull(conn, config, opts),
|
|
256
|
+
|
|
257
|
+
async serverInfo(conn) {
|
|
258
|
+
let v = "unknown";
|
|
259
|
+
try {
|
|
260
|
+
v = (await conn.version()).version;
|
|
261
|
+
} catch {
|
|
262
|
+
// server version unavailable
|
|
263
|
+
}
|
|
264
|
+
return `SurrealDB ${v}`;
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
// The files `schemic init` scaffolds for a fresh SurrealDB project (connections-only config + sample
|
|
268
|
+
// s.* schema + seed + .env.example); the CLI writes them and adds the neutral migration snapshot.
|
|
269
|
+
initScaffold,
|
|
270
|
+
|
|
271
|
+
// `schemic new <kind> <name>`: the starter `s.*`/`define*` module for one entity (table/relation/
|
|
272
|
+
// view/function/access/event/analyzer); throws for inline-only (index/field) or unknown kinds.
|
|
273
|
+
scaffoldEntity,
|
|
274
|
+
|
|
275
|
+
// `check`: replay every migration into a throwaway engine and diff the result against the schema.
|
|
276
|
+
// Owns ephemeral-engine selection — an embedded @surrealdb/node instance, an ephemeral server from
|
|
277
|
+
// the local `surreal` binary, or a configured scratch server — and the replay never touches the
|
|
278
|
+
// real database. Progress lines go to `log`; the empty/non-empty diff is reported by the caller.
|
|
279
|
+
async checkReplay(config, over, filter, log) {
|
|
280
|
+
const params = config.params as unknown as SurrealParams;
|
|
281
|
+
const check = params.check;
|
|
282
|
+
const engine = check?.engine ?? "auto";
|
|
283
|
+
const useBinary =
|
|
284
|
+
engine === "binary" ||
|
|
285
|
+
(engine === "auto" && surrealBinaryAvailable(check?.binary));
|
|
286
|
+
if (engine === "binary" && !useBinary) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
'check.engine "binary" needs the `surreal` CLI on PATH (or set `check.binary`). Run `schemic check --schema` to skip the replay.',
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let db: Surreal;
|
|
293
|
+
let checkCfg: ResolvedConfig;
|
|
294
|
+
let cleanup: () => Promise<void>;
|
|
295
|
+
if (typeof engine === "object") {
|
|
296
|
+
const embedded = await connectEmbedded(engine, "check", "check");
|
|
297
|
+
db = embedded.db;
|
|
298
|
+
checkCfg = {
|
|
299
|
+
...config,
|
|
300
|
+
params: {
|
|
301
|
+
url: embedded.url,
|
|
302
|
+
namespace: "check",
|
|
303
|
+
database: "check",
|
|
304
|
+
authLevel: "root",
|
|
305
|
+
} satisfies SurrealParams,
|
|
306
|
+
};
|
|
307
|
+
cleanup = embedded.stop;
|
|
308
|
+
log(
|
|
309
|
+
` replaying on an ${embedded.url} SurrealDB (@surrealdb/node) — no server, your data untouched`,
|
|
310
|
+
);
|
|
311
|
+
} else if (useBinary) {
|
|
312
|
+
const server = await spawnEphemeralServer(check?.binary);
|
|
313
|
+
checkCfg = {
|
|
314
|
+
...config,
|
|
315
|
+
params: {
|
|
316
|
+
url: server.url,
|
|
317
|
+
namespace: "check",
|
|
318
|
+
database: "check",
|
|
319
|
+
username: server.username,
|
|
320
|
+
password: server.password,
|
|
321
|
+
authLevel: "root",
|
|
322
|
+
} satisfies SurrealParams,
|
|
323
|
+
};
|
|
324
|
+
db = await surrealConnect(checkCfg, {});
|
|
325
|
+
cleanup = async () => {
|
|
326
|
+
await db.close().catch(() => {});
|
|
327
|
+
await server.stop();
|
|
328
|
+
};
|
|
329
|
+
log(
|
|
330
|
+
" replaying on an ephemeral in-memory SurrealDB (local `surreal` binary) — your server is untouched",
|
|
331
|
+
);
|
|
332
|
+
} else {
|
|
333
|
+
// Scratch connection for the `remote` engine: check.db merged over the connection's own params.
|
|
334
|
+
const remote: SurrealParams = { ...params, ...(check?.db ?? {}) };
|
|
335
|
+
checkCfg = {
|
|
336
|
+
...config,
|
|
337
|
+
params: remote as unknown as Record<string, unknown>,
|
|
338
|
+
};
|
|
339
|
+
try {
|
|
340
|
+
db = await surrealConnect(checkCfg, over as CfgOverrides);
|
|
341
|
+
} catch (e) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
`${e instanceof Error ? e.message : String(e)}\n (run \`schemic check --schema\` to skip the replay, install the \`surreal\` CLI for an in-memory engine, or set \`check.db\` to point the replay at a scratch server)`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
cleanup = async () => {
|
|
347
|
+
await db.close().catch(() => {});
|
|
348
|
+
};
|
|
349
|
+
log(
|
|
350
|
+
` replaying on ${remote.url} (${remote.namespace}) — isolated scratch databases; your data is untouched`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
return await verifyMigrations(db, checkCfg, filter, (tag) =>
|
|
356
|
+
log(` ${tag}`),
|
|
357
|
+
);
|
|
358
|
+
} finally {
|
|
359
|
+
await cleanup();
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
registerDriver(surrealDriver as Driver<unknown>);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @schemic/surrealdb — author SurrealDB schemas with Zod, and the SurrealDB driver.
|
|
3
|
+
*
|
|
4
|
+
* Define tables/relations with `s.*` (a drop-in for `z.*`), generate SurrealQL DDL, and map JS <-> DB
|
|
5
|
+
* across Zod's two channels via codecs (`decode`/`encode`). Importing this package registers the
|
|
6
|
+
* SurrealDB driver with `@schemic/core` (so the CLI's `getDriver("surrealdb")` resolves).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Side-effect: register `surrealDriver` with the core registry on import.
|
|
10
|
+
import "./driver/surreal";
|
|
11
|
+
|
|
12
|
+
import { type BoundQuery, surql as sdkSurql } from "surrealdb";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Author SurrealQL expressions — the `s.*` authoring API takes these `BoundQuery` values everywhere a
|
|
16
|
+
* dynamic expression is allowed (`$default`/`$value`/`$computed`/`$assert`, `reference({ onDelete })`,
|
|
17
|
+
* event `when`/`then`, function bodies, permissions). A thin GENERIC wrapper over the SDK's tag so a
|
|
18
|
+
* direct SDK query can also carry its RESULT type — one tuple entry per statement:
|
|
19
|
+
* `db.query(surql<[string[]]>\`RETURN ['a', 'b', 'c']\`)`. Plain `surql\`…\`` (no type arg) is unchanged.
|
|
20
|
+
* Provided here (typed) so you stay on a single import, decoupled from the SDK version.
|
|
21
|
+
*/
|
|
22
|
+
export function surql<R extends unknown[] = unknown[]>(
|
|
23
|
+
strings: TemplateStringsArray,
|
|
24
|
+
...values: unknown[]
|
|
25
|
+
): BoundQuery<R> {
|
|
26
|
+
return sdkSurql(strings, ...values) as BoundQuery<R>;
|
|
27
|
+
}
|
|
28
|
+
export type { BoundQuery } from "surrealdb";
|
|
29
|
+
/** SurrealDB config types (relocated from @schemic/core/config, now connections-only + dialect-free). */
|
|
30
|
+
export type {
|
|
31
|
+
AuthLevel,
|
|
32
|
+
CapabilityList,
|
|
33
|
+
EmbeddedCapabilities,
|
|
34
|
+
SurrealParams,
|
|
35
|
+
SurrealZodCheck,
|
|
36
|
+
SurrealZodCheckEmbedded,
|
|
37
|
+
SurrealZodConnection,
|
|
38
|
+
} from "./config";
|
|
39
|
+
export type { SurrealConnectionConfig } from "./connection";
|
|
40
|
+
/** Multi-connection factory: `defineConfig({ connections: { db: surrealConnection({ … }) } })`. */
|
|
41
|
+
export { surrealConnection } from "./connection";
|
|
42
|
+
export type { DefineOptions, DefineStatement, FieldInfo } from "./ddl";
|
|
43
|
+
export {
|
|
44
|
+
alterField,
|
|
45
|
+
alterTable,
|
|
46
|
+
assertExpr,
|
|
47
|
+
braceBody,
|
|
48
|
+
emitDefStatement,
|
|
49
|
+
emitField,
|
|
50
|
+
emitFieldStatements,
|
|
51
|
+
emitStatements,
|
|
52
|
+
emitTable,
|
|
53
|
+
eventClause,
|
|
54
|
+
fieldType,
|
|
55
|
+
inferField,
|
|
56
|
+
inline,
|
|
57
|
+
overwriteStatement,
|
|
58
|
+
removeStatement,
|
|
59
|
+
} from "./ddl";
|
|
60
|
+
export { surrealDriver } from "./driver/surreal";
|
|
61
|
+
export type {
|
|
62
|
+
AnalyzerConfig,
|
|
63
|
+
App,
|
|
64
|
+
Create,
|
|
65
|
+
DiskannOptions,
|
|
66
|
+
Expr,
|
|
67
|
+
FulltextOptions,
|
|
68
|
+
HnswOptions,
|
|
69
|
+
Shape,
|
|
70
|
+
StandaloneDef,
|
|
71
|
+
SurrealMeta,
|
|
72
|
+
TableConfig,
|
|
73
|
+
TableEvent,
|
|
74
|
+
TableIndex,
|
|
75
|
+
Update,
|
|
76
|
+
Wire,
|
|
77
|
+
} from "./pure";
|
|
78
|
+
export {
|
|
79
|
+
AccessDef,
|
|
80
|
+
AnalyzerDef,
|
|
81
|
+
defineAccess,
|
|
82
|
+
defineAnalyzer,
|
|
83
|
+
defineEvent,
|
|
84
|
+
defineFunction,
|
|
85
|
+
defineRelation,
|
|
86
|
+
defineTable,
|
|
87
|
+
defineView,
|
|
88
|
+
EventDef,
|
|
89
|
+
FunctionDef,
|
|
90
|
+
formatForAssert,
|
|
91
|
+
objectFieldsRegistry,
|
|
92
|
+
RecordIdField,
|
|
93
|
+
RelationDef,
|
|
94
|
+
SField,
|
|
95
|
+
SystemView,
|
|
96
|
+
s,
|
|
97
|
+
surrealTypeRegistry,
|
|
98
|
+
TableDef,
|
|
99
|
+
} from "./pure";
|