@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
package/src/cli/lower.ts
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
// The Struct-IR lowering for the OFFLINE side: turn an in-memory @schemic/core `TableDef`/`RelationDef`
|
|
2
|
+
// (and standalone `defineFunction`/`defineAccess`/`defineEvent`) into the `Struct` IR, so it can be
|
|
3
|
+
// `normalize`d and structurally diffed against the live DB's `fromInfo` (introspectStructured). This
|
|
4
|
+
// is the inverse of `pull` and the keystone of the Struct-IR effort. See docs/STRUCT-IR.md.
|
|
5
|
+
//
|
|
6
|
+
// It reads the `TableDef`/`SField` directly (no DDL round-trip) and reuses the emitter's
|
|
7
|
+
// `inferField()` so the type strings and dotted field paths are identical by construction; clauses
|
|
8
|
+
// come straight off `SField.surreal` and `TableConfig`. The output is RAW (unsorted, defaults not
|
|
9
|
+
// stripped, `option<>` not folded, `x.*` elements present) — `normalizeTable` closes those gaps.
|
|
10
|
+
|
|
11
|
+
import { BoundQuery, escapeIdent } from "surrealdb";
|
|
12
|
+
import {
|
|
13
|
+
assertExpr,
|
|
14
|
+
braceBody,
|
|
15
|
+
eventClause,
|
|
16
|
+
type FieldInfo,
|
|
17
|
+
fieldType,
|
|
18
|
+
inferField,
|
|
19
|
+
inline,
|
|
20
|
+
} from "../ddl";
|
|
21
|
+
import type {
|
|
22
|
+
AccessDef,
|
|
23
|
+
AnalyzerDef,
|
|
24
|
+
FieldPermissions,
|
|
25
|
+
FunctionDef,
|
|
26
|
+
PermOp,
|
|
27
|
+
SField,
|
|
28
|
+
Shape,
|
|
29
|
+
StandaloneDef,
|
|
30
|
+
SurrealMeta,
|
|
31
|
+
TableDef,
|
|
32
|
+
TableEvent,
|
|
33
|
+
TablePermissions,
|
|
34
|
+
} from "../pure";
|
|
35
|
+
import { normalizeDb } from "./struct";
|
|
36
|
+
import type {
|
|
37
|
+
DbStructured,
|
|
38
|
+
StructAccess,
|
|
39
|
+
StructAnalyzer,
|
|
40
|
+
StructEvent,
|
|
41
|
+
StructField,
|
|
42
|
+
StructFunction,
|
|
43
|
+
StructIndex,
|
|
44
|
+
StructPerm,
|
|
45
|
+
StructPermissions,
|
|
46
|
+
StructTable,
|
|
47
|
+
StructTableKind,
|
|
48
|
+
} from "./structure";
|
|
49
|
+
|
|
50
|
+
const FIELD_PERM_OPS = ["select", "create", "update"] as const;
|
|
51
|
+
const TABLE_PERM_OPS = ["select", "create", "update", "delete"] as const;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Lower a permissions spec to `StructPermissions` (per-op `true`/`false`/WHERE-expr string). A
|
|
55
|
+
* blanket `true`/`false`/`BoundQuery` materializes every op; an object resolves each present op,
|
|
56
|
+
* following `same as X` references (with cycle detection, mirroring `renderPermissions`) and
|
|
57
|
+
* inlining a `BoundQuery` to its WHERE expression. Omitted ops are left unset — `normalize` reads
|
|
58
|
+
* them as the kind default, so an unspecified op compares equal to the materialized default.
|
|
59
|
+
*/
|
|
60
|
+
function lowerPermissions(
|
|
61
|
+
spec: TablePermissions | FieldPermissions,
|
|
62
|
+
ops: readonly PermOp[],
|
|
63
|
+
): StructPermissions {
|
|
64
|
+
const out: StructPermissions = {};
|
|
65
|
+
if (spec === true) {
|
|
66
|
+
for (const op of ops) out[op] = true;
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
if (spec === false) {
|
|
70
|
+
for (const op of ops) out[op] = false;
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
if (spec instanceof BoundQuery) {
|
|
74
|
+
const where = inline(spec);
|
|
75
|
+
for (const op of ops) out[op] = where;
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const rules = spec as Partial<Record<PermOp, boolean | BoundQuery | string>>;
|
|
80
|
+
const resolved = new Map<PermOp, StructPerm>();
|
|
81
|
+
const resolve = (op: PermOp, chain: PermOp[]): StructPerm => {
|
|
82
|
+
const cached = resolved.get(op);
|
|
83
|
+
if (cached !== undefined) return cached;
|
|
84
|
+
const rule = rules[op];
|
|
85
|
+
if (rule === undefined) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`PERMISSIONS: "same as ${op}" references op "${op}", which is not in the spec`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (chain.includes(op)) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`PERMISSIONS: "same as" reference cycle: ${[...chain, op].join(" -> ")}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
let value: StructPerm;
|
|
96
|
+
if (typeof rule === "string") {
|
|
97
|
+
value = resolve(rule.slice("same as ".length).trim() as PermOp, [
|
|
98
|
+
...chain,
|
|
99
|
+
op,
|
|
100
|
+
]);
|
|
101
|
+
} else if (rule instanceof BoundQuery) {
|
|
102
|
+
value = inline(rule);
|
|
103
|
+
} else {
|
|
104
|
+
value = rule;
|
|
105
|
+
}
|
|
106
|
+
resolved.set(op, value);
|
|
107
|
+
return value;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
for (const op of ops) {
|
|
111
|
+
if (rules[op] === undefined) continue;
|
|
112
|
+
out[op] = resolve(op, []);
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Lower a field `REFERENCE` to the IR's structured form: `{}` (bare) or `{ on_delete: <action> }`. */
|
|
118
|
+
function lowerReference(ref: NonNullable<SurrealMeta["reference"]>): {
|
|
119
|
+
on_delete?: string;
|
|
120
|
+
} {
|
|
121
|
+
if (ref === true || ref.onDelete === undefined) return {};
|
|
122
|
+
const onDelete = ref.onDelete;
|
|
123
|
+
return {
|
|
124
|
+
on_delete:
|
|
125
|
+
onDelete instanceof BoundQuery
|
|
126
|
+
? inline(onDelete)
|
|
127
|
+
: onDelete.toUpperCase(),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Walk an `inferField` node into flattened dotted `StructField`s — exactly the paths the emitter
|
|
133
|
+
* produces (`address`, `address.city`, `tags.*`). Unlike the emitter this keeps `option<>` (it does
|
|
134
|
+
* NOT strip it for defaulted/valued/computed fields), and it EMITS the `x.*` element node (rather
|
|
135
|
+
* than folding it) — `normalizeTable` does both folds. Single-field indexes (`.index()`/`.unique()`)
|
|
136
|
+
* are collected into `indexes`.
|
|
137
|
+
*/
|
|
138
|
+
function lowerField(
|
|
139
|
+
path: string,
|
|
140
|
+
table: string,
|
|
141
|
+
info: FieldInfo,
|
|
142
|
+
surreal: SurrealMeta | undefined,
|
|
143
|
+
fields: StructField[],
|
|
144
|
+
indexes: StructIndex[],
|
|
145
|
+
): void {
|
|
146
|
+
const sf: StructField = { name: path, kind: info.type, table };
|
|
147
|
+
if (info.flexible) sf.flexible = true;
|
|
148
|
+
if (surreal) {
|
|
149
|
+
if (surreal.reference) sf.reference = lowerReference(surreal.reference);
|
|
150
|
+
if (surreal.default) {
|
|
151
|
+
sf.default = inline(surreal.default);
|
|
152
|
+
if (surreal.defaultAlways) sf.default_always = true;
|
|
153
|
+
}
|
|
154
|
+
if (surreal.value) sf.value = inline(surreal.value);
|
|
155
|
+
if (surreal.computed) sf.computed = inline(surreal.computed);
|
|
156
|
+
const assert = assertExpr(surreal.asserts);
|
|
157
|
+
if (assert) sf.assert = assert;
|
|
158
|
+
if (surreal.readonly) sf.readonly = true;
|
|
159
|
+
if (surreal.comment !== undefined) sf.comment = surreal.comment;
|
|
160
|
+
// An internal field grants no record-user access (PERMISSIONS NONE) — it wins over `$permissions`.
|
|
161
|
+
if (surreal.internal) {
|
|
162
|
+
sf.permissions = { select: false, create: false, update: false };
|
|
163
|
+
} else if (surreal.permissions !== undefined) {
|
|
164
|
+
sf.permissions = lowerPermissions(surreal.permissions, FIELD_PERM_OPS);
|
|
165
|
+
}
|
|
166
|
+
if (surreal.index) {
|
|
167
|
+
// The single-field index name the emitter derives: `<table>_<sanitized-path>_idx`.
|
|
168
|
+
const idxName = `${table}_${path
|
|
169
|
+
.replace(/[`]/g, "")
|
|
170
|
+
.replace(/[^a-zA-Z0-9]+/g, "_")}_idx`;
|
|
171
|
+
indexes.push({
|
|
172
|
+
name: idxName,
|
|
173
|
+
cols: [path],
|
|
174
|
+
index: surreal.index.unique ? "UNIQUE" : "",
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
fields.push(sf);
|
|
179
|
+
|
|
180
|
+
for (const child of info.children) {
|
|
181
|
+
lowerField(
|
|
182
|
+
`${path}${child.suffix}`,
|
|
183
|
+
table,
|
|
184
|
+
child.info,
|
|
185
|
+
child.surreal,
|
|
186
|
+
fields,
|
|
187
|
+
indexes,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Lower a table/relation event to a `StructEvent` (one or several `THEN` exprs, bare). */
|
|
193
|
+
function lowerEvent(table: string, ev: TableEvent): StructEvent {
|
|
194
|
+
const thens = (Array.isArray(ev.then) ? ev.then : [ev.then]).map(eventClause);
|
|
195
|
+
// biome-ignore lint/suspicious/noThenProperty: `then` mirrors SurrealQL's event THEN clause.
|
|
196
|
+
const out: StructEvent = { name: ev.name, what: table, then: thens };
|
|
197
|
+
if (ev.when !== undefined) out.when = eventClause(ev.when);
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Lower an in-memory `TableDef`/`RelationDef` to the `Struct` IR. The result is raw — feed it
|
|
203
|
+
* through `normalizeTable` before comparing. Skips the implicit `id` field (and `in`/`out` on a
|
|
204
|
+
* relation); they are managed by SurrealDB and never emitted.
|
|
205
|
+
*/
|
|
206
|
+
export function fromTableDef(t: TableDef<string, Shape>): StructTable {
|
|
207
|
+
const cfg = t.config;
|
|
208
|
+
const rel = cfg.relation;
|
|
209
|
+
const implicit = rel ? new Set(["id", "in", "out"]) : new Set(["id"]);
|
|
210
|
+
|
|
211
|
+
const fields: StructField[] = [];
|
|
212
|
+
const indexes: StructIndex[] = [];
|
|
213
|
+
for (const [name, field] of Object.entries(t.fields)) {
|
|
214
|
+
if (implicit.has(name)) continue;
|
|
215
|
+
const f = field as SField;
|
|
216
|
+
lowerField(
|
|
217
|
+
escapeIdent(name),
|
|
218
|
+
t.name,
|
|
219
|
+
inferField(f.schema),
|
|
220
|
+
f.surreal,
|
|
221
|
+
fields,
|
|
222
|
+
indexes,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
// Composite (multi-field) indexes — and the row-count index (no FIELDS).
|
|
226
|
+
for (const idx of cfg.indexes ?? []) {
|
|
227
|
+
indexes.push({
|
|
228
|
+
name: idx.name,
|
|
229
|
+
cols: idx.count ? [] : idx.fields.map(escapeIdent),
|
|
230
|
+
index: idx.count ? "COUNT" : (idx.spec ?? (idx.unique ? "UNIQUE" : "")), // HNSW/DISKANN/FULLTEXT or UNIQUE/plain
|
|
231
|
+
...(idx.comment !== undefined ? { comment: idx.comment } : {}),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let kind: StructTableKind;
|
|
236
|
+
if (rel) {
|
|
237
|
+
kind = { kind: "RELATION" };
|
|
238
|
+
if (rel.from.length) kind.in = [...rel.from];
|
|
239
|
+
if (rel.to.length) kind.out = [...rel.to];
|
|
240
|
+
if (rel.enforced) kind.enforced = true;
|
|
241
|
+
} else if (cfg.type === "any") {
|
|
242
|
+
kind = { kind: "ANY" };
|
|
243
|
+
} else {
|
|
244
|
+
kind = { kind: "NORMAL" };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const out: StructTable = {
|
|
248
|
+
name: t.name,
|
|
249
|
+
kind,
|
|
250
|
+
schemafull: cfg.schemafull,
|
|
251
|
+
fields,
|
|
252
|
+
indexes,
|
|
253
|
+
events: (cfg.events ?? []).map((ev) => lowerEvent(t.name, ev)),
|
|
254
|
+
};
|
|
255
|
+
if (cfg.drop) out.drop = true;
|
|
256
|
+
if (cfg.comment !== undefined) out.comment = cfg.comment;
|
|
257
|
+
if (cfg.changefeed) {
|
|
258
|
+
out.changefeed = {
|
|
259
|
+
expiry: cfg.changefeed.expiry,
|
|
260
|
+
original: !!cfg.changefeed.includeOriginal,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
if (cfg.permissions !== undefined) {
|
|
264
|
+
out.permissions = lowerPermissions(cfg.permissions, TABLE_PERM_OPS);
|
|
265
|
+
}
|
|
266
|
+
// A pre-computed VIEW — the inlined `SELECT …` (without the `AS ` keyword; canonical adds it).
|
|
267
|
+
if (cfg.view !== undefined) out.view = eventClause(cfg.view);
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Lower a `defineFunction` to a `StructFunction` (block wrapped as `{ … }` to match INFO). */
|
|
272
|
+
function lowerFunction(fn: FunctionDef): StructFunction {
|
|
273
|
+
const args: [string, string][] = Object.entries(fn.args).map(([n, f]) => [
|
|
274
|
+
n,
|
|
275
|
+
fieldType(f),
|
|
276
|
+
]);
|
|
277
|
+
const out: StructFunction = {
|
|
278
|
+
name: fn.name,
|
|
279
|
+
args,
|
|
280
|
+
block: fn.config.body !== undefined ? braceBody(fn.config.body) : "{}",
|
|
281
|
+
};
|
|
282
|
+
if (fn.config.returns) out.returns = fieldType(fn.config.returns);
|
|
283
|
+
const p = fn.config.permissions;
|
|
284
|
+
if (p !== undefined)
|
|
285
|
+
out.permissions = typeof p === "boolean" ? p : eventClause(p);
|
|
286
|
+
if (fn.config.comment !== undefined) out.comment = fn.config.comment;
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Lower a `defineAccess` to a `StructAccess`. Signing keys are NOT carried (SurrealDB redacts them). */
|
|
291
|
+
function lowerAccess(a: AccessDef): StructAccess {
|
|
292
|
+
const cfg = a.config;
|
|
293
|
+
const k = cfg.kind;
|
|
294
|
+
let kind: StructAccess["kind"];
|
|
295
|
+
if (k.type === "bearer") {
|
|
296
|
+
kind = {
|
|
297
|
+
kind: "BEARER",
|
|
298
|
+
subject: k.subject === "user" ? "USER" : "RECORD",
|
|
299
|
+
};
|
|
300
|
+
} else if (k.type === "jwt") {
|
|
301
|
+
kind = {
|
|
302
|
+
kind: "JWT",
|
|
303
|
+
jwt: { verify: k.url ? { url: k.url } : { alg: k.alg ?? "HS512" } },
|
|
304
|
+
};
|
|
305
|
+
} else {
|
|
306
|
+
kind = { kind: "RECORD" };
|
|
307
|
+
if (cfg.signup) kind.signup = braceBody(cfg.signup);
|
|
308
|
+
if (cfg.signin) kind.signin = braceBody(cfg.signin);
|
|
309
|
+
if (cfg.authenticate) kind.authenticate = braceBody(cfg.authenticate);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const out: StructAccess = { name: a.name, kind };
|
|
313
|
+
const d = cfg.duration;
|
|
314
|
+
if (d && (d.grant || d.token || d.session)) {
|
|
315
|
+
out.duration = {};
|
|
316
|
+
if (d.grant) out.duration.grant = d.grant;
|
|
317
|
+
if (d.token) out.duration.token = d.token;
|
|
318
|
+
if (d.session) out.duration.session = d.session;
|
|
319
|
+
}
|
|
320
|
+
return out;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Lower a `defineAnalyzer` to a `StructAnalyzer` (tokenizers/filters uppercased to match INFO). */
|
|
324
|
+
function lowerAnalyzer(a: AnalyzerDef): StructAnalyzer {
|
|
325
|
+
const out: StructAnalyzer = {
|
|
326
|
+
name: a.name,
|
|
327
|
+
tokenizers: a.config.tokenizers.map((t) => t.toUpperCase()),
|
|
328
|
+
};
|
|
329
|
+
if (a.config.filters?.length)
|
|
330
|
+
out.filters = a.config.filters.map((f) => f.toUpperCase());
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Lower a standalone def (`defineFunction`/`defineAccess`/`defineEvent`/`defineAnalyzer`) to its `Struct` IR. */
|
|
335
|
+
export function fromStandalone(
|
|
336
|
+
def: StandaloneDef,
|
|
337
|
+
): StructFunction | StructAccess | StructEvent | StructAnalyzer {
|
|
338
|
+
// An `EventDef` already carries `name`/`when`/`then` — the `TableEvent` shape `lowerEvent` reads.
|
|
339
|
+
if (def.kind === "event") return lowerEvent(def.table, def);
|
|
340
|
+
if (def.kind === "function") return lowerFunction(def);
|
|
341
|
+
if (def.kind === "analyzer") return lowerAnalyzer(def);
|
|
342
|
+
return lowerAccess(def);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* The NORMALIZED Struct-IR for a whole loaded schema — the offline counterpart of `fromInfo`
|
|
347
|
+
* (introspectStructured). Standalone events are attached to their owning table. Used to render the
|
|
348
|
+
* schema as TypeScript (`diff --ts`) and stored in the snapshot.
|
|
349
|
+
*/
|
|
350
|
+
export function schemaStruct(
|
|
351
|
+
tables: TableDef<string, Shape>[],
|
|
352
|
+
defs: StandaloneDef[],
|
|
353
|
+
): DbStructured {
|
|
354
|
+
const structTables = tables.map(fromTableDef);
|
|
355
|
+
const byName = new Map(structTables.map((t) => [t.name, t]));
|
|
356
|
+
const functions: StructFunction[] = [];
|
|
357
|
+
const accesses: StructAccess[] = [];
|
|
358
|
+
const analyzers: StructAnalyzer[] = [];
|
|
359
|
+
for (const d of defs) {
|
|
360
|
+
if (d.kind === "function")
|
|
361
|
+
functions.push(fromStandalone(d) as StructFunction);
|
|
362
|
+
else if (d.kind === "access")
|
|
363
|
+
accesses.push(fromStandalone(d) as StructAccess);
|
|
364
|
+
else if (d.kind === "analyzer")
|
|
365
|
+
analyzers.push(fromStandalone(d) as StructAnalyzer);
|
|
366
|
+
else if (d.kind === "event")
|
|
367
|
+
byName.get(d.table)?.events.push(fromStandalone(d) as StructEvent);
|
|
368
|
+
}
|
|
369
|
+
return normalizeDb({ tables: structTables, functions, accesses, analyzers });
|
|
370
|
+
}
|