@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,201 @@
|
|
|
1
|
+
// The driver-side EXPLODE (docs/kind-registry-contract.md §7, sanctioned by core-dev): SurrealDB
|
|
2
|
+
// authors index/event/field-index INLINE on the table, so a single `TableDef` fans out into one TABLE
|
|
3
|
+
// object + N INDEX + M EVENT objects (and the db-level FUNCTION/ACCESS objects ride alongside). We
|
|
4
|
+
// flatten the whole schema into per-kind portable objects BEFORE lowering, so `KindEngine.lower` stays
|
|
5
|
+
// a clean 1:1 and the contract needs no "explode hook". This is what `Driver.lower` will wrap around
|
|
6
|
+
// `lowerSchema` at the eventual wholesale flip.
|
|
7
|
+
//
|
|
8
|
+
// It reuses the EXISTING canonical pipeline — `schemaStruct` (TableDef -> normalized Struct IR, with
|
|
9
|
+
// standalone events attached + functions/accesses resolved) then `structuredSnapshot` (Struct ->
|
|
10
|
+
// canonical `DEFINE` statements keyed kind:table:name) — so every emitted DDL string is byte-identical
|
|
11
|
+
// to the fixed-slot `surrealDriver.diff` path.
|
|
12
|
+
//
|
|
13
|
+
// CROSS-KIND DEPS: an object that calls `fn::name` must emit AFTER that function (the function-before-
|
|
14
|
+
// table case the ordinal alone gets wrong). We scan each object's rendered DDL for `fn::` references and
|
|
15
|
+
// attach `deps -> {kind:"function", name}` — on a table (field VALUE/ASSERT/DEFAULT/PERMISSIONS + table
|
|
16
|
+
// PERMISSIONS), an event (WHEN/THEN), an access (SIGNUP/SIGNIN/AUTHENTICATE), and a function (its body
|
|
17
|
+
// calling another fn::). Edges to functions outside the diff are ignored by the spine, so over-reporting
|
|
18
|
+
// is harmless. (SEARCH index -> analyzer edges land when the analyzer kind is registered.)
|
|
19
|
+
|
|
20
|
+
import type { AnyTable, AuthoredDef, Ref } from "@schemic/core";
|
|
21
|
+
import type { Surreal } from "surrealdb";
|
|
22
|
+
import { schemaStruct } from "../cli/lower";
|
|
23
|
+
import { normalizeDb } from "../cli/struct";
|
|
24
|
+
import type {
|
|
25
|
+
DbStructured,
|
|
26
|
+
StructAccess,
|
|
27
|
+
StructAnalyzer,
|
|
28
|
+
StructFunction,
|
|
29
|
+
StructTable,
|
|
30
|
+
} from "../cli/structure";
|
|
31
|
+
import { introspectStructured, structuredSnapshot } from "../cli/structure";
|
|
32
|
+
import type { DefineStatement } from "../ddl";
|
|
33
|
+
import type { Shape, StandaloneDef, TableDef } from "../pure";
|
|
34
|
+
import type { SurrealPortable } from "./portable";
|
|
35
|
+
|
|
36
|
+
/** Match a `fn::name` reference (incl. namespaced `fn::a::b`), capturing the bare function name. */
|
|
37
|
+
const FN_REF = /fn::([A-Za-z0-9_]+(?:::[A-Za-z0-9_]+)*)/g;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The cross-kind dependency edges for one object: a `base` set (its table, for index/event) plus the
|
|
41
|
+
* `fn::` functions referenced in its DDL — deduped, and excluding `self` (a function's body referencing
|
|
42
|
+
* itself is recursion, not an ordering edge).
|
|
43
|
+
*/
|
|
44
|
+
function depsOf(ddls: string[], base: Ref[] = [], self?: string): Ref[] {
|
|
45
|
+
const seen = new Set(base.map((r) => `${r.kind}:${r.name}`));
|
|
46
|
+
const out: Ref[] = [...base];
|
|
47
|
+
for (const ddl of ddls) {
|
|
48
|
+
for (const m of ddl.matchAll(FN_REF)) {
|
|
49
|
+
const name = m[1];
|
|
50
|
+
if (name === self) continue;
|
|
51
|
+
const key = `function:${name}`;
|
|
52
|
+
if (seen.has(key)) continue;
|
|
53
|
+
seen.add(key);
|
|
54
|
+
out.push({ kind: "function", name });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The shared core: a NORMALIZED Struct IR -> per-kind portable objects. Renders each object's canonical
|
|
62
|
+
* `DEFINE` statement(s) via `structuredSnapshot`, then partitions: a {@link PTable} per table (head +
|
|
63
|
+
* nested fields), a {@link PIndex} per index, a {@link PEvent} per event, and the db-level
|
|
64
|
+
* {@link PFunction}/{@link PAccess} objects — each carrying its `fn::`/endpoint dependency edges. A
|
|
65
|
+
* table also carries its normalized {@link StructTable} and the opaque kinds their `native` struct, so
|
|
66
|
+
* `renderSchema` reconstructs `DbStructured` without re-parsing DDL. Both entry points
|
|
67
|
+
* ({@link explodeSchema} from authoring, {@link introspectAll} from a live DB) feed their normalized
|
|
68
|
+
* Struct here, so they produce byte-identical portable objects.
|
|
69
|
+
*/
|
|
70
|
+
export function fromStructured(db: DbStructured): SurrealPortable[] {
|
|
71
|
+
const stmts = Object.values(structuredSnapshot(db).statements);
|
|
72
|
+
const of = (kind: DefineStatement["kind"]) =>
|
|
73
|
+
stmts.filter((s) => s.kind === kind);
|
|
74
|
+
|
|
75
|
+
const out: SurrealPortable[] = [];
|
|
76
|
+
|
|
77
|
+
// Tables: head + nested fields. deps = RELATION in/out endpoints + fn:: from the head/fields.
|
|
78
|
+
const headByTable = new Map(of("table").map((s) => [s.name, s]));
|
|
79
|
+
const fieldsByTable = new Map<string, DefineStatement[]>();
|
|
80
|
+
for (const s of of("field")) {
|
|
81
|
+
const arr = fieldsByTable.get(s.table ?? "") ?? [];
|
|
82
|
+
arr.push(s);
|
|
83
|
+
fieldsByTable.set(s.table ?? "", arr);
|
|
84
|
+
}
|
|
85
|
+
for (const st of db.tables) {
|
|
86
|
+
const head = headByTable.get(st.name);
|
|
87
|
+
if (!head) continue; // a table always has a head; defensive only
|
|
88
|
+
const fields = fieldsByTable.get(st.name) ?? [];
|
|
89
|
+
const endpoints: Ref[] = [];
|
|
90
|
+
for (const t of st.kind.in ?? [])
|
|
91
|
+
endpoints.push({ kind: "table", name: t });
|
|
92
|
+
for (const t of st.kind.out ?? [])
|
|
93
|
+
endpoints.push({ kind: "table", name: t });
|
|
94
|
+
const deps = depsOf([head.ddl, ...fields.map((f) => f.ddl)], endpoints);
|
|
95
|
+
out.push({ kind: "table", name: st.name, head, fields, deps, struct: st });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const s of of("index")) {
|
|
99
|
+
// A FULLTEXT index depends on its analyzer (so the analyzer emits first); all indexes -> their table.
|
|
100
|
+
const deps: Ref[] = [{ kind: "table", name: s.table ?? "" }];
|
|
101
|
+
const ft = /FULLTEXT ANALYZER (\S+)/.exec(s.ddl);
|
|
102
|
+
if (ft) deps.push({ kind: "analyzer", name: ft[1] });
|
|
103
|
+
out.push({
|
|
104
|
+
kind: "index",
|
|
105
|
+
name: s.name,
|
|
106
|
+
table: s.table ?? "",
|
|
107
|
+
stmt: s,
|
|
108
|
+
deps,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
for (const s of of("event"))
|
|
112
|
+
out.push({
|
|
113
|
+
kind: "event",
|
|
114
|
+
name: s.name,
|
|
115
|
+
table: s.table ?? "",
|
|
116
|
+
stmt: s,
|
|
117
|
+
deps: depsOf([s.ddl], [{ kind: "table", name: s.table ?? "" }]),
|
|
118
|
+
});
|
|
119
|
+
// The opaque kinds carry their structured `native` (matched by name) for renderSchema.
|
|
120
|
+
const fnByName = new Map(db.functions.map((f) => [f.name, f]));
|
|
121
|
+
const accByName = new Map(db.accesses.map((a) => [a.name, a]));
|
|
122
|
+
for (const s of of("function")) {
|
|
123
|
+
const native = fnByName.get(s.name);
|
|
124
|
+
if (native)
|
|
125
|
+
out.push({
|
|
126
|
+
kind: "function",
|
|
127
|
+
name: s.name,
|
|
128
|
+
stmt: s,
|
|
129
|
+
deps: depsOf([s.ddl], [], s.name),
|
|
130
|
+
native,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
for (const s of of("access")) {
|
|
134
|
+
const native = accByName.get(s.name);
|
|
135
|
+
if (native)
|
|
136
|
+
out.push({
|
|
137
|
+
kind: "access",
|
|
138
|
+
name: s.name,
|
|
139
|
+
stmt: s,
|
|
140
|
+
deps: depsOf([s.ddl]),
|
|
141
|
+
native,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const anByName = new Map(db.analyzers.map((a) => [a.name, a]));
|
|
145
|
+
for (const s of of("analyzer")) {
|
|
146
|
+
const native = anByName.get(s.name);
|
|
147
|
+
if (native)
|
|
148
|
+
out.push({ kind: "analyzer", name: s.name, stmt: s, deps: [], native });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Reassemble `DbStructured` from per-kind portable objects — the inverse of {@link fromStructured},
|
|
155
|
+
* for `renderSchema` TS codegen. Tables carry their full normalized struct; the opaque kinds their
|
|
156
|
+
* `native`. (No DDL re-parsing: the structured data rides on the objects per flip-plan §6b.) */
|
|
157
|
+
export function toStructured(objects: SurrealPortable[]): DbStructured {
|
|
158
|
+
const tables: StructTable[] = [];
|
|
159
|
+
const functions: StructFunction[] = [];
|
|
160
|
+
const accesses: StructAccess[] = [];
|
|
161
|
+
const analyzers: StructAnalyzer[] = [];
|
|
162
|
+
for (const o of objects) {
|
|
163
|
+
if (o.kind === "table") tables.push(o.struct);
|
|
164
|
+
else if (o.kind === "function") functions.push(o.native);
|
|
165
|
+
else if (o.kind === "access") accesses.push(o.native);
|
|
166
|
+
else if (o.kind === "analyzer") analyzers.push(o.native);
|
|
167
|
+
}
|
|
168
|
+
return { tables, functions, accesses, analyzers };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Authoring -> per-kind portable objects (what `Driver.lower` wraps around `lowerSchema`). Params mirror
|
|
173
|
+
* `buildSnapshot`'s public bound (`AnyTable`/`AuthoredDef`) and cast to the src `TableDef` the canonical
|
|
174
|
+
* pipeline reads. `schemaStruct` already normalizes (events attached, functions/accesses resolved).
|
|
175
|
+
*/
|
|
176
|
+
export function explodeSchema(
|
|
177
|
+
tables: AnyTable[],
|
|
178
|
+
defs: AuthoredDef[] = [],
|
|
179
|
+
): SurrealPortable[] {
|
|
180
|
+
return fromStructured(
|
|
181
|
+
schemaStruct(
|
|
182
|
+
tables as unknown as TableDef<string, Shape>[],
|
|
183
|
+
defs as unknown as StandaloneDef[],
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The reverse path (flip-plan §4): a live connection -> per-kind portable objects, via ONE
|
|
190
|
+
* `INFO … STRUCTURE` read (`introspectStructured`) fanned across every kind. Normalized the same way as
|
|
191
|
+
* {@link decompose}/{@link explodeSchema} (`normalizeDb` → canonical `structuredSnapshot`), so an
|
|
192
|
+
* introspected schema canonicalizes IDENTICALLY to a lowered one — no introspect phantom-diffs. This is
|
|
193
|
+
* the COMPLETE introspection the flip needs: it returns objects for every registered kind that round-
|
|
194
|
+
* trips (table/index/event/function/access). `exclude` drops tables by name (e.g. the migrations table).
|
|
195
|
+
*/
|
|
196
|
+
export async function introspectAll(
|
|
197
|
+
conn: Surreal,
|
|
198
|
+
exclude?: Set<string>,
|
|
199
|
+
): Promise<SurrealPortable[]> {
|
|
200
|
+
return fromStructured(normalizeDb(await introspectStructured(conn, exclude)));
|
|
201
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// The PORTABLE objects for the SurrealDB kind registry (core-v2, docs/kind-registry-contract.md
|
|
2
|
+
// slice 2). Each is a {@link PortableObject} (tagged `kind`/`name` for the generic spine) wrapping
|
|
3
|
+
// the canonical {@link DefineStatement}(s) the legacy diff engine already produces — so a kind's
|
|
4
|
+
// `emit`/`overwrite` reuse the SAME renderer + clause maps the fixed-slot `surrealDriver.diff` uses,
|
|
5
|
+
// and parity is byte-exact by construction (see ./registry.ts).
|
|
6
|
+
//
|
|
7
|
+
// SurrealDB authors index/event INLINE on the table, but the contract models them as their OWN kinds
|
|
8
|
+
// (deps/owner -> table). The driver-side EXPLODE (./explode.ts) fans a `TableDef` into one table object
|
|
9
|
+
// + N index + M event objects BEFORE lowering, so `KindEngine.lower` stays a clean 1:1. Fields stay
|
|
10
|
+
// NESTED in the table object (a table HAS fields — they are the shared substrate, not a kind), and the
|
|
11
|
+
// table kind owns field-level diff inside its `overwrite`.
|
|
12
|
+
|
|
13
|
+
import type { PortableObject, Ref } from "@schemic/core";
|
|
14
|
+
import type {
|
|
15
|
+
StructAccess,
|
|
16
|
+
StructAnalyzer,
|
|
17
|
+
StructFunction,
|
|
18
|
+
StructTable,
|
|
19
|
+
} from "../cli/structure";
|
|
20
|
+
import type { DefineStatement } from "../ddl";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A table's portable form: its `DEFINE TABLE` head + its `DEFINE FIELD` statements (nested). Indexes
|
|
24
|
+
* and events are EXCLUDED — they are their own kinds. `deps` carries the table's cross-kind edges
|
|
25
|
+
* (a RELATION's in/out endpoint tables), computed at explode time.
|
|
26
|
+
*/
|
|
27
|
+
export interface PTable extends PortableObject {
|
|
28
|
+
readonly kind: "table";
|
|
29
|
+
readonly name: string;
|
|
30
|
+
/** The `DEFINE TABLE` head statement (carries the table-level clause map for `ALTER TABLE`). */
|
|
31
|
+
readonly head: DefineStatement;
|
|
32
|
+
/** The nested `DEFINE FIELD` statements (each carries its clause map for `ALTER FIELD`). */
|
|
33
|
+
readonly fields: DefineStatement[];
|
|
34
|
+
/** Objects this table must be emitted AFTER — a RELATION's in/out tables. */
|
|
35
|
+
readonly deps: Ref[];
|
|
36
|
+
/**
|
|
37
|
+
* The NORMALIZED structured table (fields/indexes/events nested) — carried so `renderSchema` can
|
|
38
|
+
* reconstruct `DbStructured` for TS codegen WITHOUT re-parsing rendered DDL (the robust read of
|
|
39
|
+
* flip-plan §6b's "derive structured from your portable objects"; emit/diff still use head/fields).
|
|
40
|
+
*/
|
|
41
|
+
readonly struct: StructTable;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** An index's portable form: the `DEFINE INDEX` statement + its owning table. `deps` carries the table
|
|
45
|
+
* and, for a FULLTEXT index, its analyzer (so the analyzer emits BEFORE the index). */
|
|
46
|
+
export interface PIndex extends PortableObject {
|
|
47
|
+
readonly kind: "index";
|
|
48
|
+
readonly name: string;
|
|
49
|
+
readonly table: string;
|
|
50
|
+
readonly stmt: DefineStatement;
|
|
51
|
+
readonly deps: Ref[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** An event's portable form: the `DEFINE EVENT` statement + its owning table. `deps` carries any
|
|
55
|
+
* `fn::` functions the WHEN/THEN call (so a called function emits BEFORE the event). */
|
|
56
|
+
export interface PEvent extends PortableObject {
|
|
57
|
+
readonly kind: "event";
|
|
58
|
+
readonly name: string;
|
|
59
|
+
readonly table: string;
|
|
60
|
+
readonly stmt: DefineStatement;
|
|
61
|
+
readonly deps: Ref[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A db-level function's portable form — OPAQUE (a neutral identity + its canonical `DEFINE FUNCTION`
|
|
66
|
+
* statement, round-tripped verbatim). `deps` carries any OTHER `fn::` functions its body calls.
|
|
67
|
+
*/
|
|
68
|
+
export interface PFunction extends PortableObject {
|
|
69
|
+
readonly kind: "function";
|
|
70
|
+
readonly name: string;
|
|
71
|
+
readonly stmt: DefineStatement;
|
|
72
|
+
readonly deps: Ref[];
|
|
73
|
+
/** The structured function (the opaque kind's `native` payload) — for `renderSchema` TS codegen. */
|
|
74
|
+
readonly native: StructFunction;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A db-level access/auth definition's portable form — OPAQUE. `deps` carries any `fn::` functions its
|
|
79
|
+
* SIGNUP/SIGNIN/AUTHENTICATE call (so those functions emit BEFORE the access).
|
|
80
|
+
*/
|
|
81
|
+
export interface PAccess extends PortableObject {
|
|
82
|
+
readonly kind: "access";
|
|
83
|
+
readonly name: string;
|
|
84
|
+
readonly stmt: DefineStatement;
|
|
85
|
+
readonly deps: Ref[];
|
|
86
|
+
/** The structured access (the opaque kind's `native` payload) — for `renderSchema` TS codegen. */
|
|
87
|
+
readonly native: StructAccess;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A db-level text-search analyzer's portable form — OPAQUE. A FULLTEXT index `deps` on it.
|
|
92
|
+
*/
|
|
93
|
+
export interface PAnalyzer extends PortableObject {
|
|
94
|
+
readonly kind: "analyzer";
|
|
95
|
+
readonly name: string;
|
|
96
|
+
readonly stmt: DefineStatement;
|
|
97
|
+
readonly deps: Ref[];
|
|
98
|
+
/** The structured analyzer (the opaque kind's `native` payload) — for `renderSchema` TS codegen. */
|
|
99
|
+
readonly native: StructAnalyzer;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Every portable object a SurrealDB schema lowers to. */
|
|
103
|
+
export type SurrealPortable =
|
|
104
|
+
| PTable
|
|
105
|
+
| PIndex
|
|
106
|
+
| PEvent
|
|
107
|
+
| PFunction
|
|
108
|
+
| PAccess
|
|
109
|
+
| PAnalyzer;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The authoring-side definables the explode produces. They ALREADY carry the normalized, canonical
|
|
113
|
+
* `DefineStatement`(s) (the explode runs `schemaStruct` + `structuredSnapshot` first), so each kind's
|
|
114
|
+
* `lower` is the identity — exactly the pattern core's `kind-registry.test.ts` uses.
|
|
115
|
+
*/
|
|
116
|
+
export type SurrealDefinable = SurrealPortable;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// The SurrealDB KIND REGISTRY (core-v2, docs/kind-registry-contract.md): the per-driver registry +
|
|
2
|
+
// the `table`/`index`/`event`/`function`/`access` engines. Core orchestrates generically over this registry
|
|
3
|
+
// (lower -> diff -> order -> emit); each engine delegates to the EXISTING fixed-slot primitives, so the
|
|
4
|
+
// generic path is byte-exact with `surrealDriver.diff`:
|
|
5
|
+
// - table.overwrite -> `diffSnapshots` (the whole field + table-head ALTER engine, scoped to one table)
|
|
6
|
+
// - index -> recreate (the spine's remove+emit default == legacy `changeUp` for an index)
|
|
7
|
+
// - event.overwrite -> `overwriteStatement` (DEFINE EVENT OVERWRITE == legacy `changeUp` for an event)
|
|
8
|
+
//
|
|
9
|
+
// Registration ORDER is each kind's ordinal (the tie-break among independent objects): table (0) before
|
|
10
|
+
// index (1) before event (2). index/event point `deps`+`owner` at their table, so the graph emits them
|
|
11
|
+
// AFTER and clusters them next to it. Fields are NESTED in the table object (substrate, not a kind).
|
|
12
|
+
//
|
|
13
|
+
// NOTE (slice 2): the legacy `Driver.lower/emit/diff` path is UNTOUCHED and still production. This
|
|
14
|
+
// registry is built + parity-tested ALONGSIDE it; the wholesale flip (routing `Driver` + the `s.*`
|
|
15
|
+
// authoring `build` through here) lands once access/function/natives are migrated too.
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
type AnyTable,
|
|
19
|
+
type AuthoredDef,
|
|
20
|
+
type DiffItem,
|
|
21
|
+
type KindEngine,
|
|
22
|
+
KindRegistry,
|
|
23
|
+
lowerSchema,
|
|
24
|
+
type PortableObject,
|
|
25
|
+
type Ref,
|
|
26
|
+
} from "@schemic/core";
|
|
27
|
+
import { EMPTY_SNAPSHOT, type Snapshot } from "../cli/structure";
|
|
28
|
+
import { diffSnapshots } from "../cli/surreal-diff";
|
|
29
|
+
import type { DefineStatement } from "../ddl";
|
|
30
|
+
import { overwriteStatement, removeStatement } from "../ddl";
|
|
31
|
+
import { explodeSchema } from "./explode";
|
|
32
|
+
import type {
|
|
33
|
+
PAccess,
|
|
34
|
+
PAnalyzer,
|
|
35
|
+
PEvent,
|
|
36
|
+
PFunction,
|
|
37
|
+
PIndex,
|
|
38
|
+
PTable,
|
|
39
|
+
} from "./portable";
|
|
40
|
+
|
|
41
|
+
/** Statement key matching the legacy engine's `keyOf` (`kind:table:name`). */
|
|
42
|
+
const keyOf = (s: Pick<DefineStatement, "kind" | "name" | "table">) =>
|
|
43
|
+
`${s.kind}:${s.table ?? ""}:${s.name}`;
|
|
44
|
+
|
|
45
|
+
/** A one-table {@link Snapshot} (head + its fields) for the legacy `diffSnapshots` to diff. */
|
|
46
|
+
function snapOf(head: DefineStatement, fields: DefineStatement[]): Snapshot {
|
|
47
|
+
const statements: Snapshot["statements"] = {};
|
|
48
|
+
for (const s of [head, ...fields]) statements[keyOf(s)] = s;
|
|
49
|
+
return { version: 1, statements };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// --- table: structured, field-level diff inside `overwrite` -------------------------------------
|
|
53
|
+
|
|
54
|
+
const tableEngine: KindEngine<PTable, PTable> = {
|
|
55
|
+
lower: (t) => t,
|
|
56
|
+
emit: (t) => [t.head.ddl, ...t.fields.map((f) => f.ddl)],
|
|
57
|
+
// REMOVE TABLE covers its fields (no orphan REMOVE FIELDs) — matches the legacy drop.
|
|
58
|
+
remove: (t) => [removeStatement(t.head)],
|
|
59
|
+
// The field + table-head delta IS the legacy engine: build both sides as one-table snapshots and
|
|
60
|
+
// take the up. The spine calls `overwrite(next, prev)` for the down, so this stays symmetric.
|
|
61
|
+
overwrite: (prev, next) =>
|
|
62
|
+
diffSnapshots(
|
|
63
|
+
snapOf(prev.head, prev.fields),
|
|
64
|
+
snapOf(next.head, next.fields),
|
|
65
|
+
).up,
|
|
66
|
+
deps: (t) => t.deps,
|
|
67
|
+
// PER-FIELD display (Manuel's call): the table's diff items are its head + per-field statements
|
|
68
|
+
// (each carrying its `table` so the display groups under it) — exactly `diffSnapshots().items`.
|
|
69
|
+
// `full` uses displayItems(undefined, next) -> the per-field add items.
|
|
70
|
+
displayItems: (prev, next): DiffItem[] =>
|
|
71
|
+
diffSnapshots(
|
|
72
|
+
prev ? snapOf(prev.head, prev.fields) : EMPTY_SNAPSHOT,
|
|
73
|
+
next ? snapOf(next.head, next.fields) : EMPTY_SNAPSHOT,
|
|
74
|
+
).items ?? [],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// --- index: own kind, owned by its table, recreated on change -----------------------------------
|
|
78
|
+
|
|
79
|
+
const indexEngine: KindEngine<PIndex, PIndex> = {
|
|
80
|
+
lower: (i) => i,
|
|
81
|
+
emit: (i) => [i.stmt.ddl],
|
|
82
|
+
remove: (i) => [removeStatement(i.stmt)],
|
|
83
|
+
// No `overwrite` -> the spine recreates (remove + emit): `REMOVE INDEX … ; DEFINE INDEX …`,
|
|
84
|
+
// exactly the legacy `changeUp` for an index (ALTER INDEX can't change fields/kind).
|
|
85
|
+
// -> its table (owner) + a FULLTEXT index's analyzer (so the analyzer emits first).
|
|
86
|
+
deps: (i) => i.deps,
|
|
87
|
+
owner: (i) => ({ kind: "table", name: i.table }),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// --- event: own kind, owned by its table, OVERWRITE on change -----------------------------------
|
|
91
|
+
|
|
92
|
+
const eventEngine: KindEngine<PEvent, PEvent> = {
|
|
93
|
+
lower: (e) => e,
|
|
94
|
+
emit: (e) => [e.stmt.ddl],
|
|
95
|
+
remove: (e) => [removeStatement(e.stmt)],
|
|
96
|
+
// DEFINE EVENT OVERWRITE in place — matches the legacy `changeUp`/`changeDown` for an event.
|
|
97
|
+
overwrite: (_prev, next) => [overwriteStatement(next.stmt.ddl)],
|
|
98
|
+
// -> its table (owner) + any `fn::` the WHEN/THEN call (so the function emits first).
|
|
99
|
+
deps: (e) => e.deps,
|
|
100
|
+
owner: (e) => ({ kind: "table", name: e.table }),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// --- function / access: db-level OPAQUE kinds, OVERWRITE on change -------------------------------
|
|
104
|
+
|
|
105
|
+
const functionEngine: KindEngine<PFunction, PFunction> = {
|
|
106
|
+
lower: (f) => f,
|
|
107
|
+
emit: (f) => [f.stmt.ddl],
|
|
108
|
+
remove: (f) => [removeStatement(f.stmt)],
|
|
109
|
+
// DEFINE FUNCTION OVERWRITE in place — matches the legacy `changeUp`/`changeDown` for a function.
|
|
110
|
+
overwrite: (_prev, next) => [overwriteStatement(next.stmt.ddl)],
|
|
111
|
+
// Other `fn::` its body calls (db-level, no owner cluster).
|
|
112
|
+
deps: (f) => f.deps,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const accessEngine: KindEngine<PAccess, PAccess> = {
|
|
116
|
+
lower: (a) => a,
|
|
117
|
+
emit: (a) => [a.stmt.ddl],
|
|
118
|
+
remove: (a) => [removeStatement(a.stmt)],
|
|
119
|
+
// DEFINE ACCESS OVERWRITE in place — matches the legacy `changeUp`/`changeDown` for an access.
|
|
120
|
+
overwrite: (_prev, next) => [overwriteStatement(next.stmt.ddl)],
|
|
121
|
+
// Any `fn::` the SIGNUP/SIGNIN/AUTHENTICATE call (db-level, no owner cluster).
|
|
122
|
+
deps: (a) => a.deps,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// --- analyzer: db-level OPAQUE kind (a FULLTEXT index depends on it) -----------------------------
|
|
126
|
+
|
|
127
|
+
const analyzerEngine: KindEngine<PAnalyzer, PAnalyzer> = {
|
|
128
|
+
lower: (a) => a,
|
|
129
|
+
emit: (a) => [a.stmt.ddl],
|
|
130
|
+
remove: (a) => [removeStatement(a.stmt)],
|
|
131
|
+
// DEFINE ANALYZER OVERWRITE in place.
|
|
132
|
+
overwrite: (_prev, next) => [overwriteStatement(next.stmt.ddl)],
|
|
133
|
+
deps: (a) => a.deps,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The SurrealDB driver's kind registry. Registration order == kind ordinal (the tie-break among
|
|
138
|
+
* objects with no dependency relation): table < index < event < function < access. Correctness is the
|
|
139
|
+
* dependency GRAPH (deps), not the ordinal — a `fn::`-called function still emits before its caller via
|
|
140
|
+
* the function `deps` regardless of its ordinal.
|
|
141
|
+
*/
|
|
142
|
+
export const surrealKinds = new KindRegistry();
|
|
143
|
+
|
|
144
|
+
// `build` is the kind's authoring entry. At the wholesale flip these route the `s.*` authoring through
|
|
145
|
+
// the registry; for the facade-test phase the explode produces portable objects directly, so the builds
|
|
146
|
+
// are the identity (the registry only needs the engine behavior registered here).
|
|
147
|
+
surrealKinds.define({ name: "table", build: (t: PTable) => t, ...tableEngine });
|
|
148
|
+
surrealKinds.define({ name: "index", build: (i: PIndex) => i, ...indexEngine });
|
|
149
|
+
surrealKinds.define({ name: "event", build: (e: PEvent) => e, ...eventEngine });
|
|
150
|
+
surrealKinds.define({
|
|
151
|
+
name: "function",
|
|
152
|
+
build: (f: PFunction) => f,
|
|
153
|
+
...functionEngine,
|
|
154
|
+
});
|
|
155
|
+
surrealKinds.define({
|
|
156
|
+
name: "access",
|
|
157
|
+
build: (a: PAccess) => a,
|
|
158
|
+
// The generic pluralizer would make "access" -> "Accesses"/"accesses"; SurrealQL's noun is
|
|
159
|
+
// invariant ("Access") and the dir is `access/`, so pin both.
|
|
160
|
+
display: { plural: "Access", folder: "access" },
|
|
161
|
+
...accessEngine,
|
|
162
|
+
});
|
|
163
|
+
surrealKinds.define({
|
|
164
|
+
name: "analyzer",
|
|
165
|
+
build: (a: PAnalyzer) => a,
|
|
166
|
+
...analyzerEngine,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
/** Author -> portable via the registry: explode tables/defs into per-kind objects, then lower each. */
|
|
170
|
+
export function lowerAll(
|
|
171
|
+
tables: AnyTable[],
|
|
172
|
+
defs: AuthoredDef[] = [],
|
|
173
|
+
): PortableObject[] {
|
|
174
|
+
return lowerSchema(surrealKinds, explodeSchema(tables, defs));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export type { Ref };
|