@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/pure.ts
ADDED
|
@@ -0,0 +1,2671 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Bound,
|
|
3
|
+
BoundExcluded,
|
|
4
|
+
BoundIncluded,
|
|
5
|
+
BoundQuery,
|
|
6
|
+
DateTime,
|
|
7
|
+
Decimal,
|
|
8
|
+
Duration,
|
|
9
|
+
FileRef,
|
|
10
|
+
Geometry,
|
|
11
|
+
RecordId,
|
|
12
|
+
RecordIdRange,
|
|
13
|
+
type RecordIdValue,
|
|
14
|
+
surql,
|
|
15
|
+
Table,
|
|
16
|
+
Uuid,
|
|
17
|
+
} from "surrealdb";
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The "pure" approach: a field is a stock Zod schema + SurrealQL DDL metadata.
|
|
22
|
+
* The JS<->DB mapping rides on Zod's two native channels via codecs:
|
|
23
|
+
* - encoded side (`z.input`) = DB wire type
|
|
24
|
+
* - decoded side (`z.output`) = app type
|
|
25
|
+
* `z.decode` reads from the DB, `z.encode` writes to it.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Maps a Surreal-native schema (datetime codec, recordId) to its SurrealQL type.
|
|
30
|
+
* Kept on the schema — not the field — so it composes through array()/optional()/nesting.
|
|
31
|
+
*/
|
|
32
|
+
export const surrealTypeRegistry = new WeakMap<z.ZodType, string>();
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Maps an object schema built via `s.object` to its original SField shape, so
|
|
36
|
+
* nested fields keep their DDL metadata ($default/$assert/...) during generation.
|
|
37
|
+
*/
|
|
38
|
+
export const objectFieldsRegistry = new WeakMap<
|
|
39
|
+
z.ZodType,
|
|
40
|
+
Record<string, AnyField>
|
|
41
|
+
>();
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Per-table/field row-level permissions. A `PermOp` is one access operation; `Perm` is
|
|
45
|
+
* the rule for one op — `true` (FULL) / `false` (NONE) / a `BoundQuery` (a `WHERE` expr) /
|
|
46
|
+
* `` `same as X` `` to reuse another op's resolved rule. A `TablePermissions` is a blanket
|
|
47
|
+
* rule, a shared `WHERE`, or per-op rules. Fields have NO `delete` op (verified against
|
|
48
|
+
* the DB), so they use `FieldPerm` / `FieldPermissions`.
|
|
49
|
+
*/
|
|
50
|
+
export type PermOp = "select" | "create" | "update" | "delete";
|
|
51
|
+
export type Perm = boolean | BoundQuery | `same as ${PermOp}`;
|
|
52
|
+
export type TablePermissions =
|
|
53
|
+
| boolean
|
|
54
|
+
| BoundQuery
|
|
55
|
+
| Partial<Record<PermOp, Perm>>;
|
|
56
|
+
export type FieldPerm =
|
|
57
|
+
| boolean
|
|
58
|
+
| BoundQuery
|
|
59
|
+
| "same as select"
|
|
60
|
+
| "same as create"
|
|
61
|
+
| "same as update";
|
|
62
|
+
export type FieldPermissions =
|
|
63
|
+
| boolean
|
|
64
|
+
| BoundQuery
|
|
65
|
+
| Partial<Record<"select" | "create" | "update", FieldPerm>>;
|
|
66
|
+
|
|
67
|
+
/** SurrealQL DDL metadata — the `$`-prefixed field options. */
|
|
68
|
+
export interface SurrealMeta {
|
|
69
|
+
default?: BoundQuery;
|
|
70
|
+
defaultAlways?: boolean;
|
|
71
|
+
value?: BoundQuery;
|
|
72
|
+
/** `COMPUTED <expr>` — a derived, read-only column (computed on read; never written). */
|
|
73
|
+
computed?: BoundQuery;
|
|
74
|
+
/**
|
|
75
|
+
* `ASSERT` fragments that AND-combine into one clause. Computed checks (format
|
|
76
|
+
* builders, `$`-constraints, `.$assert()`-derived) are plain strings; a custom
|
|
77
|
+
* `.$assert(surql\`…\`)` is a `BoundQuery` (inlined during DDL generation).
|
|
78
|
+
*/
|
|
79
|
+
asserts?: (string | BoundQuery)[];
|
|
80
|
+
readonly?: boolean;
|
|
81
|
+
comment?: string;
|
|
82
|
+
/** Field-level `PERMISSIONS` (no `delete` op). Omitted ops default to FULL in
|
|
83
|
+
* SurrealDB — the table is the gate; set an op `false` to lock it. See `.$permissions()`. */
|
|
84
|
+
permissions?: FieldPermissions;
|
|
85
|
+
/** DB-managed, client-hidden: still emits DEFINE FIELD (+ PERMISSIONS NONE) but is
|
|
86
|
+
* excluded from the public app/create/update surface. See `.$internal()` / `.system`. */
|
|
87
|
+
internal?: boolean;
|
|
88
|
+
/** Single-field index: `.index()` (normal) / `.unique()` (uniqueness). Emits a `DEFINE
|
|
89
|
+
* INDEX <table>_<field>_idx ON TABLE <table> FIELDS <field> [UNIQUE]`. */
|
|
90
|
+
index?: { unique?: boolean };
|
|
91
|
+
/** `REFERENCE [ON DELETE …]` on a record-link field. See `.reference()`. */
|
|
92
|
+
reference?:
|
|
93
|
+
| true
|
|
94
|
+
| { onDelete?: "reject" | "cascade" | "ignore" | "unset" | BoundQuery };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Render a primitive as a clean SurrealQL literal; non-primitives return "". */
|
|
98
|
+
function primitiveLiteral(value: unknown): string {
|
|
99
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
100
|
+
if (
|
|
101
|
+
typeof value === "number" ||
|
|
102
|
+
typeof value === "boolean" ||
|
|
103
|
+
typeof value === "bigint"
|
|
104
|
+
) {
|
|
105
|
+
return String(value);
|
|
106
|
+
}
|
|
107
|
+
return "";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Coerce a `$default`/`$defaultAlways` argument (a value or a `surql` expr) to a BoundQuery. */
|
|
111
|
+
function toExpr(value: unknown): BoundQuery {
|
|
112
|
+
if (value instanceof BoundQuery) return value;
|
|
113
|
+
const literal = primitiveLiteral(value);
|
|
114
|
+
return literal ? new BoundQuery(literal) : surql`${value}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Zod string formats whose `string::is_<fmt>` validator exists on SurrealDB v3.x
|
|
119
|
+
* (probed live on 3.1.3: `RETURN string::is_<fmt>("x")`). A matching format builder
|
|
120
|
+
* bakes `string::is_<fmt>($value)` by default; formats absent here (nanoid/cuid/cuid2/
|
|
121
|
+
* xid/ksuid/cidrv4/cidrv6/guid/base64/base64url/e164/jwt/emoji) stay assert-free — no
|
|
122
|
+
* fabricated regex. `uuid` is the native `uuid` type, not a string format (no assert).
|
|
123
|
+
*/
|
|
124
|
+
const STRING_IS_FORMATS = new Set([
|
|
125
|
+
"email",
|
|
126
|
+
"url",
|
|
127
|
+
"ulid",
|
|
128
|
+
"ipv4",
|
|
129
|
+
"ipv6",
|
|
130
|
+
// batch 2: 3.1.3 `string::is_*` validators with no Zod format builder (plain string
|
|
131
|
+
// app-side; the ASSERT enforces the format in SurrealDB).
|
|
132
|
+
"alpha",
|
|
133
|
+
"alphanum",
|
|
134
|
+
"ascii",
|
|
135
|
+
"numeric",
|
|
136
|
+
"semver",
|
|
137
|
+
"hexadecimal",
|
|
138
|
+
"latitude",
|
|
139
|
+
"longitude",
|
|
140
|
+
"ip",
|
|
141
|
+
"domain",
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
/** Map a Zod string format to its SurrealDB `string::is_*` assert, when one exists. */
|
|
145
|
+
function formatAssert(format: string): string | undefined {
|
|
146
|
+
return STRING_IS_FORMATS.has(format)
|
|
147
|
+
? `string::is_${format}($value)`
|
|
148
|
+
: undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Reverse of {@link formatAssert}: recover a format name from a baked `string::is_<fmt>($value)`
|
|
153
|
+
* assert. Used by `pull` to restore `s.<format>()` instead of `s.string().$assert(...)`. Returns
|
|
154
|
+
* undefined for any other assert — including one that combines a format with extra text — so only an
|
|
155
|
+
* exact, single-format assert reverses (a user's own assert is never swallowed).
|
|
156
|
+
*/
|
|
157
|
+
export function formatForAssert(assert: string): string | undefined {
|
|
158
|
+
const m = /^string::is_([a-z0-9]+)\(\s*\$value\s*\)$/.exec(assert.trim());
|
|
159
|
+
return m && STRING_IS_FORMATS.has(m[1]) ? m[1] : undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Build an SField for a Zod string-format schema, baking `string::is_<fmt>($value)`
|
|
163
|
+
* when SurrealDB has that validator (else no assert). */
|
|
164
|
+
function formatField<S extends z.ZodType>(
|
|
165
|
+
schema: S,
|
|
166
|
+
format: string,
|
|
167
|
+
): SField<S> {
|
|
168
|
+
const frag = formatAssert(format);
|
|
169
|
+
return new SField(schema, frag ? { asserts: [frag] } : {});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The check methods that live on concrete Zod subtypes (ZodString/ZodNumber) but not
|
|
173
|
+
* the base `z.ZodType` — `$`-constraints call these (cast through this shape). */
|
|
174
|
+
type CheckableSchema = {
|
|
175
|
+
min(n: number): z.ZodType;
|
|
176
|
+
max(n: number): z.ZodType;
|
|
177
|
+
length(n: number): z.ZodType;
|
|
178
|
+
regex(re: RegExp): z.ZodType;
|
|
179
|
+
gt(n: number): z.ZodType;
|
|
180
|
+
gte(n: number): z.ZodType;
|
|
181
|
+
lt(n: number): z.ZodType;
|
|
182
|
+
lte(n: number): z.ZodType;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/** One entry in a Zod schema's `_zod.def.checks`. */
|
|
186
|
+
type ZodCheck = {
|
|
187
|
+
_zod: {
|
|
188
|
+
def: {
|
|
189
|
+
check?: string;
|
|
190
|
+
minimum?: number;
|
|
191
|
+
maximum?: number;
|
|
192
|
+
length?: number;
|
|
193
|
+
value?: number;
|
|
194
|
+
inclusive?: boolean;
|
|
195
|
+
format?: string;
|
|
196
|
+
pattern?: RegExp;
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Best-effort: derive DB `ASSERT` fragments from a Zod schema's checks. Reads the Zod 4
|
|
203
|
+
* check shape (`schema._zod.def.checks[]._zod.def`): string `min_length`/`max_length`/
|
|
204
|
+
* `length_equals`, `string_format` (regex -> `$value = /…/`; email/url/… -> `string::is_*`),
|
|
205
|
+
* and number `greater_than`/`less_than` (with `inclusive`). The schema may itself be a
|
|
206
|
+
* `string_format` (e.g. `z.email()`), so its top-level `def.format` is mapped too. Unknown
|
|
207
|
+
* checks are skipped silently.
|
|
208
|
+
*/
|
|
209
|
+
function deriveAsserts(schema: z.ZodType): string[] {
|
|
210
|
+
const def = schema._zod.def as {
|
|
211
|
+
check?: string;
|
|
212
|
+
format?: string;
|
|
213
|
+
checks?: ZodCheck[];
|
|
214
|
+
};
|
|
215
|
+
const out: string[] = [];
|
|
216
|
+
|
|
217
|
+
// The schema itself may be a string-format (z.email()/z.url()/…).
|
|
218
|
+
if (def.check === "string_format" && typeof def.format === "string") {
|
|
219
|
+
const frag = formatAssert(def.format);
|
|
220
|
+
if (frag) out.push(frag);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
for (const c of def.checks ?? []) {
|
|
224
|
+
const d = c._zod.def;
|
|
225
|
+
switch (d.check) {
|
|
226
|
+
case "min_length":
|
|
227
|
+
out.push(`string::len($value) >= ${d.minimum}`);
|
|
228
|
+
break;
|
|
229
|
+
case "max_length":
|
|
230
|
+
out.push(`string::len($value) <= ${d.maximum}`);
|
|
231
|
+
break;
|
|
232
|
+
case "length_equals":
|
|
233
|
+
out.push(`string::len($value) == ${d.length}`);
|
|
234
|
+
break;
|
|
235
|
+
case "string_format":
|
|
236
|
+
if (d.format === "regex" && d.pattern) {
|
|
237
|
+
out.push(`$value = /${d.pattern.source}/`);
|
|
238
|
+
} else if (typeof d.format === "string") {
|
|
239
|
+
const frag = formatAssert(d.format);
|
|
240
|
+
if (frag) out.push(frag);
|
|
241
|
+
}
|
|
242
|
+
break;
|
|
243
|
+
case "greater_than":
|
|
244
|
+
out.push(`$value >${d.inclusive ? "=" : ""} ${d.value}`);
|
|
245
|
+
break;
|
|
246
|
+
case "less_than":
|
|
247
|
+
out.push(`$value <${d.inclusive ? "=" : ""} ${d.value}`);
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** The schema one wrapper down — what `unwrap()` returns. */
|
|
255
|
+
type InnerOf<S extends z.ZodType> =
|
|
256
|
+
S extends z.ZodOptional<infer I extends z.ZodType>
|
|
257
|
+
? I
|
|
258
|
+
: S extends z.ZodNullable<infer I extends z.ZodType>
|
|
259
|
+
? I
|
|
260
|
+
: S extends z.ZodDefault<infer I extends z.ZodType>
|
|
261
|
+
? I
|
|
262
|
+
: S extends z.ZodPrefault<infer I extends z.ZodType>
|
|
263
|
+
? I
|
|
264
|
+
: S extends z.ZodCatch<infer I extends z.ZodType>
|
|
265
|
+
? I
|
|
266
|
+
: S extends z.ZodReadonly<infer I extends z.ZodType>
|
|
267
|
+
? I
|
|
268
|
+
: S extends z.ZodArray<infer I extends z.ZodType>
|
|
269
|
+
? I
|
|
270
|
+
: S;
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* A Zod schema paired with SurrealQL DDL metadata. `Flags` tracks input traits
|
|
274
|
+
* used by `Create<>`/`Update<>`: `"create"` (DB-filled -> optional on create) and
|
|
275
|
+
* `"readonly"` (excluded from updates). It only appears in method return types, so
|
|
276
|
+
* `SField` is covariant in it.
|
|
277
|
+
*/
|
|
278
|
+
/**
|
|
279
|
+
* The PORTABLE, dialect-agnostic field base (extraction phase B — see docs/AUTHORING-SPLIT.md).
|
|
280
|
+
* Holds the Zod schema, an opaque per-dialect `native` metadata slot, the field-level codecs, and
|
|
281
|
+
* the App-land Zod wrappers (which carry `native` forward via the `rebuild` hook so a chain keeps
|
|
282
|
+
* its concrete dialect type). It references NOTHING SurrealDB-specific. Each dialect subclasses it
|
|
283
|
+
* (see {@link SField} for SurrealDB) to add native authoring (`$`-methods) and re-type the wrappers.
|
|
284
|
+
*/
|
|
285
|
+
export abstract class SFieldBase<
|
|
286
|
+
S extends z.ZodType = z.ZodType,
|
|
287
|
+
Flags extends string = never,
|
|
288
|
+
N = unknown,
|
|
289
|
+
> {
|
|
290
|
+
constructor(
|
|
291
|
+
readonly schema: S,
|
|
292
|
+
readonly native: N,
|
|
293
|
+
) {}
|
|
294
|
+
|
|
295
|
+
/** Rebuild a sibling field of the SAME dialect with a new schema/flags. Each dialect overrides it. */
|
|
296
|
+
protected abstract rebuild<S2 extends z.ZodType, F2 extends string>(
|
|
297
|
+
schema: S2,
|
|
298
|
+
native: N,
|
|
299
|
+
): SFieldBase<S2, F2, N>;
|
|
300
|
+
/** A fresh, empty native-metadata bag (for wrappers like `or`/`and` that reset it). */
|
|
301
|
+
protected abstract blank(): N;
|
|
302
|
+
|
|
303
|
+
// --- Field-level codec (raw, on `this.schema`): `decode` reads (wire -> app), `encode`
|
|
304
|
+
// writes (app -> wire). Create-shaping is a table concept, so these are NOT create-shaped —
|
|
305
|
+
// e.g. `s.datetime().decode(dbDateTime) -> Date`, `s.uuid().encode("…") -> Uuid`. ---
|
|
306
|
+
/** Decode a DB value to its app type (wire -> app). */
|
|
307
|
+
decode(value: unknown): z.output<S> {
|
|
308
|
+
return z.decode(this.schema, value as never);
|
|
309
|
+
}
|
|
310
|
+
/** Encode an app value to its DB wire type (app -> wire). */
|
|
311
|
+
encode(value: z.output<S>): z.input<S> {
|
|
312
|
+
return z.encode(this.schema, value);
|
|
313
|
+
}
|
|
314
|
+
decodeAsync(value: unknown): Promise<z.output<S>> {
|
|
315
|
+
return z.decodeAsync(this.schema, value as never);
|
|
316
|
+
}
|
|
317
|
+
encodeAsync(value: z.output<S>): Promise<z.input<S>> {
|
|
318
|
+
return z.encodeAsync(this.schema, value);
|
|
319
|
+
}
|
|
320
|
+
safeDecode(value: unknown) {
|
|
321
|
+
return z.safeDecode(this.schema, value as never);
|
|
322
|
+
}
|
|
323
|
+
safeEncode(value: z.output<S>) {
|
|
324
|
+
return z.safeEncode(this.schema, value);
|
|
325
|
+
}
|
|
326
|
+
safeDecodeAsync(value: unknown) {
|
|
327
|
+
return z.safeDecodeAsync(this.schema, value as never);
|
|
328
|
+
}
|
|
329
|
+
safeEncodeAsync(value: z.output<S>) {
|
|
330
|
+
return z.safeEncodeAsync(this.schema, value);
|
|
331
|
+
}
|
|
332
|
+
// Deprecated Zod-style aliases — `parse` runs the DECODE direction (wire -> app), so it's
|
|
333
|
+
// just `decode` under a misleading name. Kept for `z`-API familiarity (struck through).
|
|
334
|
+
/** @deprecated `parse` decodes a value (wire -> app). Use {@link SField.decode | decode}. */
|
|
335
|
+
parse(value: unknown): z.output<S> {
|
|
336
|
+
return this.decode(value);
|
|
337
|
+
}
|
|
338
|
+
/** @deprecated Use {@link SField.safeDecode | safeDecode}. */
|
|
339
|
+
safeParse(value: unknown) {
|
|
340
|
+
return this.safeDecode(value);
|
|
341
|
+
}
|
|
342
|
+
/** @deprecated Use {@link SField.decodeAsync | decodeAsync}. */
|
|
343
|
+
parseAsync(value: unknown): Promise<z.output<S>> {
|
|
344
|
+
return this.decodeAsync(value);
|
|
345
|
+
}
|
|
346
|
+
/** @deprecated Use {@link SField.safeDecodeAsync | safeDecodeAsync}. */
|
|
347
|
+
safeParseAsync(value: unknown) {
|
|
348
|
+
return this.safeDecodeAsync(value);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Zod wrappers — delegate to the inner schema, carry native metadata + flags forward.
|
|
352
|
+
optional(): SFieldBase<z.ZodOptional<S>, Flags, N> {
|
|
353
|
+
return this.rebuild(this.schema.optional(), this.native);
|
|
354
|
+
}
|
|
355
|
+
nullable(): SFieldBase<z.ZodNullable<S>, Flags, N> {
|
|
356
|
+
return this.rebuild(this.schema.nullable(), this.native);
|
|
357
|
+
}
|
|
358
|
+
default(value: z.input<S>): SFieldBase<z.ZodDefault<S>, Flags, N> {
|
|
359
|
+
return this.rebuild(this.schema.default(value as never), this.native);
|
|
360
|
+
}
|
|
361
|
+
/** Zod prefault: fill an absent value with `value`, then validate it (unlike `.default`). */
|
|
362
|
+
prefault(value: z.input<S>): SFieldBase<z.ZodPrefault<S>, Flags, N> {
|
|
363
|
+
return this.rebuild(z.prefault(this.schema, value as never), this.native);
|
|
364
|
+
}
|
|
365
|
+
/** Zod catch: fall back to `value` when parsing fails. */
|
|
366
|
+
catch(value: z.output<S>): SFieldBase<z.ZodCatch<S>, Flags, N> {
|
|
367
|
+
return this.rebuild(this.schema.catch(value as never), this.native);
|
|
368
|
+
}
|
|
369
|
+
array(): SFieldBase<z.ZodArray<S>, Flags, N> {
|
|
370
|
+
return this.rebuild(z.array(this.schema), this.native);
|
|
371
|
+
}
|
|
372
|
+
nullish(): SFieldBase<z.ZodOptional<z.ZodNullable<S>>, Flags, N> {
|
|
373
|
+
return this.rebuild(this.schema.nullish(), this.native);
|
|
374
|
+
}
|
|
375
|
+
/** Zod union — `a.or(b)` accepts either; SurrealQL `<a> | <b>`. Mirrors Zod's `.or()`. */
|
|
376
|
+
or<F extends AnyField | z.ZodType>(
|
|
377
|
+
other: F,
|
|
378
|
+
): SFieldBase<z.ZodUnion<[S, SchemaOf<F>]>, never, N> {
|
|
379
|
+
return this.rebuild<z.ZodUnion<[S, SchemaOf<F>]>, never>(
|
|
380
|
+
z.union([this.schema, toZod(other)]) as z.ZodUnion<[S, SchemaOf<F>]>,
|
|
381
|
+
this.blank(),
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
/** Zod intersection — `a.and(b)`; merges object fields in DDL. Mirrors Zod's `.and()`. */
|
|
385
|
+
and<F extends AnyField | z.ZodType>(
|
|
386
|
+
other: F,
|
|
387
|
+
): SFieldBase<z.ZodIntersection<S, SchemaOf<F>>, never, N> {
|
|
388
|
+
return this.rebuild<z.ZodIntersection<S, SchemaOf<F>>, never>(
|
|
389
|
+
z.intersection(this.schema, toZod(other) as SchemaOf<F>),
|
|
390
|
+
this.blank(),
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// --- Native Zod passthrough (drop-in for `z.*`): app-side validation / transform / metadata,
|
|
395
|
+
// delegated to the inner schema. These mirror Zod exactly; the SurrealDB-DDL side stays under the
|
|
396
|
+
// `$`-prefixed methods (`$readonly`/`$comment`/…). A field's SurrealQL type is its WIRE/input
|
|
397
|
+
// type, so refine/check/brand/readonly/describe leave the DDL type untouched; transform/pipe keep
|
|
398
|
+
// the storable input type and change only the decoded `App<>`. A resulting type SurrealDB can't
|
|
399
|
+
// represent is rejected at emit — use `$surreal(type, codec)`. (Explicit method signatures, not
|
|
400
|
+
// `Parameters<…>`, so these stay method-bivariant and don't break table-name covariance.)
|
|
401
|
+
refine(
|
|
402
|
+
check: (arg: z.output<S>) => unknown,
|
|
403
|
+
params?: string | z.core.$ZodCustomParams,
|
|
404
|
+
): this {
|
|
405
|
+
return this.rebuild(
|
|
406
|
+
this.schema.refine(check, params) as S,
|
|
407
|
+
this.native,
|
|
408
|
+
) as unknown as this;
|
|
409
|
+
}
|
|
410
|
+
superRefine(
|
|
411
|
+
refinement: (
|
|
412
|
+
arg: z.output<S>,
|
|
413
|
+
ctx: z.core.$RefinementCtx<z.output<S>>,
|
|
414
|
+
) => void,
|
|
415
|
+
): this {
|
|
416
|
+
return this.rebuild(
|
|
417
|
+
this.schema.superRefine(refinement) as S,
|
|
418
|
+
this.native,
|
|
419
|
+
) as unknown as this;
|
|
420
|
+
}
|
|
421
|
+
check(
|
|
422
|
+
...checks: (z.core.CheckFn<z.output<S>> | z.core.$ZodCheck<z.output<S>>)[]
|
|
423
|
+
): this {
|
|
424
|
+
return this.rebuild(
|
|
425
|
+
this.schema.check(...checks) as S,
|
|
426
|
+
this.native,
|
|
427
|
+
) as unknown as this;
|
|
428
|
+
}
|
|
429
|
+
overwrite(fn: (x: z.output<S>) => z.output<S>): this {
|
|
430
|
+
return this.rebuild(
|
|
431
|
+
this.schema.overwrite(fn) as S,
|
|
432
|
+
this.native,
|
|
433
|
+
) as unknown as this;
|
|
434
|
+
}
|
|
435
|
+
brand<B extends PropertyKey = PropertyKey>(value?: B): this {
|
|
436
|
+
return this.rebuild(
|
|
437
|
+
this.schema.brand(value) as unknown as S,
|
|
438
|
+
this.native,
|
|
439
|
+
) as unknown as this;
|
|
440
|
+
}
|
|
441
|
+
/** Zod's app-side metadata (JSON-schema/docs) — distinct from `$comment()` (SurrealDB COMMENT). */
|
|
442
|
+
describe(description: string): this {
|
|
443
|
+
return this.rebuild(
|
|
444
|
+
this.schema.describe(description) as S,
|
|
445
|
+
this.native,
|
|
446
|
+
) as unknown as this;
|
|
447
|
+
}
|
|
448
|
+
meta(data: z.core.GlobalMeta): this {
|
|
449
|
+
return this.rebuild(
|
|
450
|
+
this.schema.meta(data) as S,
|
|
451
|
+
this.native,
|
|
452
|
+
) as unknown as this;
|
|
453
|
+
}
|
|
454
|
+
/** Zod's app-side readonly (TS-immutable output) — distinct from `$readonly()` (SurrealDB READONLY). */
|
|
455
|
+
readonly(): SFieldBase<z.ZodReadonly<S>, Flags, N> {
|
|
456
|
+
return this.rebuild(this.schema.readonly(), this.native);
|
|
457
|
+
}
|
|
458
|
+
/** Zod transform — changes the decoded `App<>` value; the stored (wire) type is unchanged. */
|
|
459
|
+
transform<NewOut>(
|
|
460
|
+
fn: (arg: z.output<S>, ctx: z.core.$RefinementCtx<z.output<S>>) => NewOut,
|
|
461
|
+
): SFieldBase<
|
|
462
|
+
z.ZodPipe<S, z.ZodTransform<Awaited<NewOut>, z.output<S>>>,
|
|
463
|
+
Flags,
|
|
464
|
+
N
|
|
465
|
+
> {
|
|
466
|
+
return this.rebuild(this.schema.transform(fn), this.native);
|
|
467
|
+
}
|
|
468
|
+
/** Zod pipe — feed this field's output into `target`; the stored (wire) type stays `this`. */
|
|
469
|
+
pipe<T extends z.core.$ZodType<unknown, z.output<S>>>(
|
|
470
|
+
target: T,
|
|
471
|
+
): SFieldBase<z.ZodPipe<S, T>, Flags, N> {
|
|
472
|
+
return this.rebuild(
|
|
473
|
+
this.schema.pipe(target) as z.ZodPipe<S, T>,
|
|
474
|
+
this.native,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
/** Peel one wrapper (optional/nullable/default/prefault/catch/readonly/array) off the field. */
|
|
478
|
+
unwrap(): SFieldBase<InnerOf<S>, Flags, N> {
|
|
479
|
+
const def = this.schema._zod.def as {
|
|
480
|
+
innerType?: z.ZodType;
|
|
481
|
+
element?: z.ZodType;
|
|
482
|
+
};
|
|
483
|
+
const inner = def.innerType ?? def.element ?? this.schema;
|
|
484
|
+
return this.rebuild(inner, this.native) as unknown as SFieldBase<
|
|
485
|
+
InnerOf<S>,
|
|
486
|
+
Flags,
|
|
487
|
+
N
|
|
488
|
+
>;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Object-only: allow arbitrary extra keys — `FLEXIBLE` in DDL. Mirrors Zod's `.loose()`. */
|
|
492
|
+
loose(): this {
|
|
493
|
+
return this.objectMode("loose");
|
|
494
|
+
}
|
|
495
|
+
/** Object-only: reject unknown keys — non-`FLEXIBLE` (the default). Mirrors Zod's `.strict()`. */
|
|
496
|
+
strict(): this {
|
|
497
|
+
return this.objectMode("strict");
|
|
498
|
+
}
|
|
499
|
+
/** Alias for {@link loose} — a `FLEXIBLE` object accepting arbitrary keys. */
|
|
500
|
+
flexible(): this {
|
|
501
|
+
return this.loose();
|
|
502
|
+
}
|
|
503
|
+
private objectMode(mode: "loose" | "strict"): this {
|
|
504
|
+
const obj = this.schema as unknown as {
|
|
505
|
+
loose?: () => z.ZodType;
|
|
506
|
+
strict?: () => z.ZodType;
|
|
507
|
+
};
|
|
508
|
+
if (typeof obj.loose !== "function" || typeof obj.strict !== "function") {
|
|
509
|
+
return this; // not an object schema — no-op
|
|
510
|
+
}
|
|
511
|
+
const next = (mode === "loose"
|
|
512
|
+
? obj.loose()
|
|
513
|
+
: obj.strict()) as unknown as S;
|
|
514
|
+
// Carry the nested-field registry forward so DDL/create-shaping still see the subfields.
|
|
515
|
+
const fields = objectFieldsRegistry.get(this.schema);
|
|
516
|
+
if (fields) objectFieldsRegistry.set(next, fields);
|
|
517
|
+
return this.rebuild(next, this.native) as unknown as this;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* The SurrealDB field — the dialect extension of {@link SFieldBase}. Adds SurrealDB-native authoring
|
|
523
|
+
* (the `$`-methods over `SurrealMeta`: DEFAULT/VALUE/ASSERT/PERMISSIONS/REFERENCE/…) and re-types the
|
|
524
|
+
* inherited portable Zod wrappers so a chain stays a `SField`. `s.*` produces these. In the package
|
|
525
|
+
* split this class moves to `@schemic/surrealdb` (see docs/AUTHORING-SPLIT.md).
|
|
526
|
+
*/
|
|
527
|
+
export class SField<
|
|
528
|
+
S extends z.ZodType = z.ZodType,
|
|
529
|
+
Flags extends string = never,
|
|
530
|
+
> extends SFieldBase<S, Flags, SurrealMeta> {
|
|
531
|
+
constructor(schema: S, surreal: SurrealMeta = {}) {
|
|
532
|
+
super(schema, surreal);
|
|
533
|
+
}
|
|
534
|
+
/** The SurrealDB-native field metadata (DEFAULT/VALUE/ASSERT/PERMISSIONS/…). */
|
|
535
|
+
get surreal(): SurrealMeta {
|
|
536
|
+
return this.native;
|
|
537
|
+
}
|
|
538
|
+
protected rebuild<S2 extends z.ZodType, F2 extends string>(
|
|
539
|
+
schema: S2,
|
|
540
|
+
native: SurrealMeta,
|
|
541
|
+
): SField<S2, F2> {
|
|
542
|
+
return new SField<S2, F2>(schema, native);
|
|
543
|
+
}
|
|
544
|
+
protected blank(): SurrealMeta {
|
|
545
|
+
return {};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Re-type the inherited schema-changing portable wrappers so a chain keeps its `SField` type. These
|
|
549
|
+
// are real METHOD overrides (not `declare` properties) so they stay method-bivariant — a property
|
|
550
|
+
// would be param-contravariant and break table-name covariance / record-id `default` (see below).
|
|
551
|
+
// Each just delegates to the SFieldBase body (which rebuilds a SField via the `rebuild` hook).
|
|
552
|
+
override optional(): SField<z.ZodOptional<S>, Flags> {
|
|
553
|
+
return super.optional() as SField<z.ZodOptional<S>, Flags>;
|
|
554
|
+
}
|
|
555
|
+
override nullable(): SField<z.ZodNullable<S>, Flags> {
|
|
556
|
+
return super.nullable() as SField<z.ZodNullable<S>, Flags>;
|
|
557
|
+
}
|
|
558
|
+
override default(value: z.input<S>): SField<z.ZodDefault<S>, Flags> {
|
|
559
|
+
return super.default(value) as SField<z.ZodDefault<S>, Flags>;
|
|
560
|
+
}
|
|
561
|
+
override prefault(value: z.input<S>): SField<z.ZodPrefault<S>, Flags> {
|
|
562
|
+
return super.prefault(value) as SField<z.ZodPrefault<S>, Flags>;
|
|
563
|
+
}
|
|
564
|
+
override catch(value: z.output<S>): SField<z.ZodCatch<S>, Flags> {
|
|
565
|
+
return super.catch(value) as SField<z.ZodCatch<S>, Flags>;
|
|
566
|
+
}
|
|
567
|
+
override array(): SField<z.ZodArray<S>, Flags> {
|
|
568
|
+
return super.array() as SField<z.ZodArray<S>, Flags>;
|
|
569
|
+
}
|
|
570
|
+
override nullish(): SField<z.ZodOptional<z.ZodNullable<S>>, Flags> {
|
|
571
|
+
return super.nullish() as SField<z.ZodOptional<z.ZodNullable<S>>, Flags>;
|
|
572
|
+
}
|
|
573
|
+
override or<F extends AnyField | z.ZodType>(
|
|
574
|
+
other: F,
|
|
575
|
+
): SField<z.ZodUnion<[S, SchemaOf<F>]>> {
|
|
576
|
+
return super.or(other) as SField<z.ZodUnion<[S, SchemaOf<F>]>>;
|
|
577
|
+
}
|
|
578
|
+
override and<F extends AnyField | z.ZodType>(
|
|
579
|
+
other: F,
|
|
580
|
+
): SField<z.ZodIntersection<S, SchemaOf<F>>> {
|
|
581
|
+
return super.and(other) as SField<z.ZodIntersection<S, SchemaOf<F>>>;
|
|
582
|
+
}
|
|
583
|
+
override readonly(): SField<z.ZodReadonly<S>, Flags> {
|
|
584
|
+
return super.readonly() as SField<z.ZodReadonly<S>, Flags>;
|
|
585
|
+
}
|
|
586
|
+
override transform<NewOut>(
|
|
587
|
+
fn: (arg: z.output<S>, ctx: z.core.$RefinementCtx<z.output<S>>) => NewOut,
|
|
588
|
+
): SField<z.ZodPipe<S, z.ZodTransform<Awaited<NewOut>, z.output<S>>>, Flags> {
|
|
589
|
+
return super.transform(fn) as SField<
|
|
590
|
+
z.ZodPipe<S, z.ZodTransform<Awaited<NewOut>, z.output<S>>>,
|
|
591
|
+
Flags
|
|
592
|
+
>;
|
|
593
|
+
}
|
|
594
|
+
override pipe<T extends z.core.$ZodType<unknown, z.output<S>>>(
|
|
595
|
+
target: T,
|
|
596
|
+
): SField<z.ZodPipe<S, T>, Flags> {
|
|
597
|
+
return super.pipe(target) as SField<z.ZodPipe<S, T>, Flags>;
|
|
598
|
+
}
|
|
599
|
+
override unwrap(): SField<InnerOf<S>, Flags> {
|
|
600
|
+
return super.unwrap() as unknown as SField<InnerOf<S>, Flags>;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// SurrealQL DDL metadata. $default/$defaultAlways mark the field create-optional;
|
|
604
|
+
// $readonly marks it non-updatable (see Create<>/Update<>). The default accepts a
|
|
605
|
+
// plain value (rendered as a literal) or a `surql` expression.
|
|
606
|
+
$default(value: z.output<S> | BoundQuery): SField<S, Flags | "create"> {
|
|
607
|
+
return new SField(this.schema, {
|
|
608
|
+
...this.surreal,
|
|
609
|
+
default: toExpr(value),
|
|
610
|
+
defaultAlways: false,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
$defaultAlways(value: z.output<S> | BoundQuery): SField<S, Flags | "create"> {
|
|
614
|
+
return new SField(this.schema, {
|
|
615
|
+
...this.surreal,
|
|
616
|
+
default: toExpr(value),
|
|
617
|
+
defaultAlways: true,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Set a DB-side `VALUE` clause. Whether the field is create-OPTIONAL depends on
|
|
622
|
+
* whether the expression consumes the client input (`$value`), which can't be
|
|
623
|
+
* inferred — so it's explicit via the `optional` option:
|
|
624
|
+
* - `time::now()` ignores input -> `{ optional: true }` (create-optional)
|
|
625
|
+
* - `string::lowercase($value)` requires input -> default (create-required)
|
|
626
|
+
* Optionality is purely type-level (the option drives the `"create"` flag that
|
|
627
|
+
* `Create<>`/`encode()` read); it does not touch the app type or DB nullability.
|
|
628
|
+
* There is no separate update option — every field is already optional in `Update<>`.
|
|
629
|
+
*/
|
|
630
|
+
$value<O extends boolean = false>(
|
|
631
|
+
expr: BoundQuery,
|
|
632
|
+
// biome-ignore lint/correctness/noUnusedFunctionParameters: drives the O generic (type-level only)
|
|
633
|
+
opts?: { optional?: O },
|
|
634
|
+
): SField<S, O extends true ? Flags | "create" : Flags> {
|
|
635
|
+
return new SField(this.schema, { ...this.surreal, value: expr });
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* `COMPUTED <expr>` — a derived, read-only column computed from other fields. Never written, so
|
|
639
|
+
* it's create-OPTIONAL: `s.string().$computed(surql\`string::concat(first, " ", last)\`)`.
|
|
640
|
+
*/
|
|
641
|
+
$computed(expr: BoundQuery): SField<S, Flags | "create"> {
|
|
642
|
+
return new SField(this.schema, { ...this.surreal, computed: expr });
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Add an `ASSERT` fragment (fragments AND-combine into one clause):
|
|
646
|
+
* - `.$assert(surql\`…\`)` pushes a custom expression (inlined during DDL generation).
|
|
647
|
+
* - `.$assert()` (no args) derives fragments from the field's existing Zod checks
|
|
648
|
+
* (formats, string length/regex, number bounds) — best-effort; unknowns are skipped.
|
|
649
|
+
*/
|
|
650
|
+
$assert(expr?: BoundQuery): SField<S, Flags> {
|
|
651
|
+
const frags = expr ? [expr] : deriveAsserts(this.schema);
|
|
652
|
+
return this.pushAsserts(frags);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// --- $-constraints: apply the app-side Zod check AND push a type-aware DB ASSERT. ---
|
|
656
|
+
// String-vs-number is read from the schema's own `def.type`; unsupported type/method
|
|
657
|
+
// combos no-op (return the field unchanged).
|
|
658
|
+
|
|
659
|
+
/** Min length (string) / minimum value (number). */
|
|
660
|
+
$min(n: number): SField<S, Flags> {
|
|
661
|
+
if (this.schemaType === "string")
|
|
662
|
+
return this.constrain("min", n, `string::len($value) >= ${n}`);
|
|
663
|
+
if (this.schemaType === "number")
|
|
664
|
+
return this.constrain("min", n, `$value >= ${n}`);
|
|
665
|
+
return this;
|
|
666
|
+
}
|
|
667
|
+
/** Max length (string) / maximum value (number). */
|
|
668
|
+
$max(n: number): SField<S, Flags> {
|
|
669
|
+
if (this.schemaType === "string")
|
|
670
|
+
return this.constrain("max", n, `string::len($value) <= ${n}`);
|
|
671
|
+
if (this.schemaType === "number")
|
|
672
|
+
return this.constrain("max", n, `$value <= ${n}`);
|
|
673
|
+
return this;
|
|
674
|
+
}
|
|
675
|
+
/** Exact length (string). */
|
|
676
|
+
$length(n: number): SField<S, Flags> {
|
|
677
|
+
if (this.schemaType === "string") {
|
|
678
|
+
return this.constrain("length", n, `string::len($value) == ${n}`);
|
|
679
|
+
}
|
|
680
|
+
return this;
|
|
681
|
+
}
|
|
682
|
+
/** Pattern match (string). */
|
|
683
|
+
$regex(re: RegExp): SField<S, Flags> {
|
|
684
|
+
if (this.schemaType === "string")
|
|
685
|
+
return this.constrain("regex", re, `$value = /${re.source}/`);
|
|
686
|
+
return this;
|
|
687
|
+
}
|
|
688
|
+
/** Greater than (number). */
|
|
689
|
+
$gt(n: number): SField<S, Flags> {
|
|
690
|
+
if (this.schemaType === "number")
|
|
691
|
+
return this.constrain("gt", n, `$value > ${n}`);
|
|
692
|
+
return this;
|
|
693
|
+
}
|
|
694
|
+
/** Greater than or equal (number). */
|
|
695
|
+
$gte(n: number): SField<S, Flags> {
|
|
696
|
+
if (this.schemaType === "number")
|
|
697
|
+
return this.constrain("gte", n, `$value >= ${n}`);
|
|
698
|
+
return this;
|
|
699
|
+
}
|
|
700
|
+
/** Less than (number). */
|
|
701
|
+
$lt(n: number): SField<S, Flags> {
|
|
702
|
+
if (this.schemaType === "number")
|
|
703
|
+
return this.constrain("lt", n, `$value < ${n}`);
|
|
704
|
+
return this;
|
|
705
|
+
}
|
|
706
|
+
/** Less than or equal (number). */
|
|
707
|
+
$lte(n: number): SField<S, Flags> {
|
|
708
|
+
if (this.schemaType === "number")
|
|
709
|
+
return this.constrain("lte", n, `$value <= ${n}`);
|
|
710
|
+
return this;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/** The underlying Zod schema's `def.type` ("string" / "number" / …). */
|
|
714
|
+
private get schemaType(): string {
|
|
715
|
+
return (this.schema._zod.def as { type: string }).type;
|
|
716
|
+
}
|
|
717
|
+
/** Append ASSERT fragments, returning a new field (same type param + flags). */
|
|
718
|
+
private pushAsserts(frags: (string | BoundQuery)[]): SField<S, Flags> {
|
|
719
|
+
if (frags.length === 0) return this;
|
|
720
|
+
return new SField(this.schema, {
|
|
721
|
+
...this.surreal,
|
|
722
|
+
asserts: [...(this.surreal.asserts ?? []), ...frags],
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
/** Apply a concrete-subtype Zod check (`min`/`max`/`length`/`regex`/`gt`/…) and push its
|
|
726
|
+
* matching DB fragment, returning a new field carrying the refined schema. */
|
|
727
|
+
private constrain(
|
|
728
|
+
method: keyof CheckableSchema,
|
|
729
|
+
arg: number | RegExp,
|
|
730
|
+
frag: string,
|
|
731
|
+
): SField<S, Flags> {
|
|
732
|
+
const apply = (
|
|
733
|
+
this.schema as unknown as Record<
|
|
734
|
+
string,
|
|
735
|
+
(a: number | RegExp) => z.ZodType
|
|
736
|
+
>
|
|
737
|
+
)[method];
|
|
738
|
+
return new SField(apply(arg) as S, {
|
|
739
|
+
...this.surreal,
|
|
740
|
+
asserts: [...(this.surreal.asserts ?? []), frag],
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
/** Set field-level `PERMISSIONS` (no `delete` op). Omitted ops default to FULL. */
|
|
744
|
+
$permissions(spec: FieldPermissions): SField<S, Flags> {
|
|
745
|
+
return new SField(this.schema, { ...this.surreal, permissions: spec });
|
|
746
|
+
}
|
|
747
|
+
$readonly(readonly = true): SField<S, Flags | "readonly"> {
|
|
748
|
+
return new SField(this.schema, { ...this.surreal, readonly });
|
|
749
|
+
}
|
|
750
|
+
$comment(comment: string): SField<S, Flags> {
|
|
751
|
+
return new SField(this.schema, { ...this.surreal, comment });
|
|
752
|
+
}
|
|
753
|
+
/** Index this field — `DEFINE INDEX <table>_<field>_idx ON TABLE <table> FIELDS <field>`. */
|
|
754
|
+
index(): SField<S, Flags> {
|
|
755
|
+
return new SField(this.schema, {
|
|
756
|
+
...this.surreal,
|
|
757
|
+
index: { ...this.surreal.index },
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
/** Index this field with a uniqueness constraint (`… FIELDS <field> UNIQUE`). */
|
|
761
|
+
unique(): SField<S, Flags> {
|
|
762
|
+
return new SField(this.schema, {
|
|
763
|
+
...this.surreal,
|
|
764
|
+
index: { unique: true },
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Mark a record-link field as a `REFERENCE` so the DB tracks back-links. `onDelete` sets the
|
|
769
|
+
* `ON DELETE` action — `"reject" | "cascade" | "ignore" | "unset"`, or a `surql` expression
|
|
770
|
+
* (emitted as `ON DELETE THEN …`). Omit it for a bare `REFERENCE`.
|
|
771
|
+
*/
|
|
772
|
+
reference(opts?: {
|
|
773
|
+
onDelete?: "reject" | "cascade" | "ignore" | "unset" | BoundQuery;
|
|
774
|
+
}): SField<S, Flags> {
|
|
775
|
+
return new SField(this.schema, {
|
|
776
|
+
...this.surreal,
|
|
777
|
+
reference:
|
|
778
|
+
opts?.onDelete === undefined ? true : { onDelete: opts.onDelete },
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Teach @schemic/core how to store this value in SurrealDB: give the **wire type** as an `s.*`
|
|
783
|
+
* field (its SurrealQL DDL type and Zod schema are derived from it) plus a codec
|
|
784
|
+
* (`encode`: app -> wire, `decode`: wire -> app). This turns an otherwise-unmappable field
|
|
785
|
+
* (e.g. `s.custom`/`s.instanceof`) into a real table field and clears the no-mapping brand;
|
|
786
|
+
* `s.input<>` then reports the wire type. Omit the codec for an identity mapping (the app
|
|
787
|
+
* value is stored as-is). `$`-prefixed to avoid clashing with Zod.
|
|
788
|
+
*/
|
|
789
|
+
$surreal<WF extends AnyField | z.ZodType, A = z.output<S>>(
|
|
790
|
+
wire: WF,
|
|
791
|
+
codec?: {
|
|
792
|
+
encode: (app: A) => z.output<SchemaOf<WF>>;
|
|
793
|
+
decode: (wire: z.output<SchemaOf<WF>>) => A;
|
|
794
|
+
},
|
|
795
|
+
): SField<z.ZodCodec<SchemaOf<WF>, S>, Exclude<Flags, NoDdl>> {
|
|
796
|
+
const wireSchema = toZod(wire) as SchemaOf<WF>;
|
|
797
|
+
const c = z.codec(wireSchema, this.schema, {
|
|
798
|
+
decode: (w) => (codec ? codec.decode(w as never) : w) as never,
|
|
799
|
+
encode: (a) => (codec ? codec.encode(a as A) : a) as never,
|
|
800
|
+
});
|
|
801
|
+
return new SField(c, this.surreal) as SField<
|
|
802
|
+
z.ZodCodec<SchemaOf<WF>, S>,
|
|
803
|
+
Exclude<Flags, NoDdl>
|
|
804
|
+
>;
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Mark the field DB-managed and client-hidden. It still emits its `DEFINE FIELD`
|
|
808
|
+
* (so SCHEMAFULL writes from a record-access SIGNUP block succeed) plus
|
|
809
|
+
* `PERMISSIONS NONE`, but is excluded from the public app/create/update surface.
|
|
810
|
+
* Reach internal fields via the `.system` view (server/system code).
|
|
811
|
+
*/
|
|
812
|
+
$internal(): SField<S, Flags | "internal"> {
|
|
813
|
+
return new SField(this.schema, { ...this.surreal, internal: true });
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/** A flag-agnostic SField, for internal storage where flags don't matter. */
|
|
818
|
+
type AnyField = SField<z.ZodType, string>;
|
|
819
|
+
|
|
820
|
+
// --- Surreal-native field schemas ---
|
|
821
|
+
|
|
822
|
+
/** Surreal `datetime` <-> JS `Date` (a real codec — the types differ). */
|
|
823
|
+
function datetimeCodec() {
|
|
824
|
+
const codec = z.codec(z.instanceof(DateTime), z.date(), {
|
|
825
|
+
decode: (dt) => new Date(dt.toString()),
|
|
826
|
+
encode: (d) => new DateTime(d),
|
|
827
|
+
});
|
|
828
|
+
surrealTypeRegistry.set(codec, "datetime");
|
|
829
|
+
return codec;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/** Like `datetimeCodec`, but the app side coerces to `Date` (`s.coerce.date`). Same `datetime` DDL. */
|
|
833
|
+
function coercedDatetimeCodec() {
|
|
834
|
+
const codec = z.codec(z.instanceof(DateTime), z.coerce.date(), {
|
|
835
|
+
decode: (dt): Date => new Date(dt.toString()),
|
|
836
|
+
// the schema coerces the value to a `Date` before `encode` runs (typed `unknown` by Zod).
|
|
837
|
+
encode: (d) => new DateTime(d as Date),
|
|
838
|
+
});
|
|
839
|
+
surrealTypeRegistry.set(codec, "datetime");
|
|
840
|
+
return codec;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/** Register a schema's SurrealQL type and return it (for instanceof-backed native types). */
|
|
844
|
+
function native<T>(schema: z.ZodType<T>, surrealType: string): z.ZodType<T, T> {
|
|
845
|
+
surrealTypeRegistry.set(schema, surrealType);
|
|
846
|
+
return schema as z.ZodType<T, T>;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/** Surreal `uuid` <-> JS `string` (a codec: app `string`, DB `Uuid`). */
|
|
850
|
+
function uuidCodec() {
|
|
851
|
+
const codec = z.codec(z.instanceof(Uuid), z.uuid(), {
|
|
852
|
+
decode: (u) => u.toString(),
|
|
853
|
+
encode: (s) => new Uuid(s),
|
|
854
|
+
});
|
|
855
|
+
surrealTypeRegistry.set(codec, "uuid");
|
|
856
|
+
return codec;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/** Surreal `bytes` <-> JS `Uint8Array` (the DB may return an ArrayBuffer; normalize it). */
|
|
860
|
+
function bytesCodec() {
|
|
861
|
+
const codec = z.codec(
|
|
862
|
+
z.union([z.instanceof(Uint8Array), z.instanceof(ArrayBuffer)]),
|
|
863
|
+
z.instanceof(Uint8Array),
|
|
864
|
+
{
|
|
865
|
+
decode: (b) => (b instanceof Uint8Array ? b : new Uint8Array(b)),
|
|
866
|
+
encode: (u) => u,
|
|
867
|
+
},
|
|
868
|
+
);
|
|
869
|
+
surrealTypeRegistry.set(codec, "bytes");
|
|
870
|
+
return codec;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
type GeometryKind =
|
|
874
|
+
| "point"
|
|
875
|
+
| "line"
|
|
876
|
+
| "polygon"
|
|
877
|
+
| "multipoint"
|
|
878
|
+
| "multiline"
|
|
879
|
+
| "multipolygon"
|
|
880
|
+
| "collection";
|
|
881
|
+
|
|
882
|
+
/** A `RecordId` restricted to `tables` (+ optional id-value type). Identity, so no codec. */
|
|
883
|
+
function recordIdSchema<
|
|
884
|
+
T extends string,
|
|
885
|
+
V extends RecordIdValue = RecordIdValue,
|
|
886
|
+
>(
|
|
887
|
+
tables: T[],
|
|
888
|
+
valueType?: z.ZodType<V>,
|
|
889
|
+
): z.ZodType<RecordId<T, V>, RecordId<T, V>> {
|
|
890
|
+
// Empty `tables` = an unrestricted `record` (any table) — used for endpoint-less relations.
|
|
891
|
+
const anyTable = tables.length === 0;
|
|
892
|
+
const schema = z.instanceof(RecordId).refine(
|
|
893
|
+
// RecordId.table is a Table object; .name is the unescaped name.
|
|
894
|
+
(r) =>
|
|
895
|
+
(anyTable || (tables as readonly string[]).includes(r.table.name)) &&
|
|
896
|
+
(valueType ? valueType.safeParse(r.id).success : true),
|
|
897
|
+
{
|
|
898
|
+
error: anyTable
|
|
899
|
+
? "Expected a record"
|
|
900
|
+
: `Expected record<${tables.join(" | ")}>`,
|
|
901
|
+
},
|
|
902
|
+
);
|
|
903
|
+
surrealTypeRegistry.set(
|
|
904
|
+
schema,
|
|
905
|
+
anyTable ? "record" : `record<${tables.join(" | ")}>`,
|
|
906
|
+
);
|
|
907
|
+
return schema as unknown as z.ZodType<RecordId<T, V>, RecordId<T, V>>;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/** A `record<…>` field: table restriction (+ optional id-value type) and construction helpers. */
|
|
911
|
+
export class RecordIdField<
|
|
912
|
+
T extends string,
|
|
913
|
+
V extends RecordIdValue = RecordIdValue,
|
|
914
|
+
> extends SField<z.ZodType<RecordId<T, V>, RecordId<T, V>>> {
|
|
915
|
+
constructor(
|
|
916
|
+
readonly tables: T[],
|
|
917
|
+
readonly valueType?: z.ZodType<V>,
|
|
918
|
+
surreal: SurrealMeta = {},
|
|
919
|
+
// The `this`-returning Zod passthroughs (refine/check/…) wrap the schema and rebuild via the hook
|
|
920
|
+
// below; this lets such a rebuild carry the wrapped schema instead of rebuilding the bare record
|
|
921
|
+
// schema. Defaults to the record schema for the normal `s.recordId(...)` construction.
|
|
922
|
+
schemaOverride?: z.ZodType<RecordId<T, V>, RecordId<T, V>>,
|
|
923
|
+
) {
|
|
924
|
+
super(schemaOverride ?? recordIdSchema<T, V>(tables, valueType), surreal);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// The base `this`-returning wrappers (refine/superRefine/check/…) construct via `rebuild`. SField's
|
|
928
|
+
// rebuild makes a plain SField, which would make `this` (typed RecordIdField) a LIE at runtime —
|
|
929
|
+
// `s.recordId("x").refine(p).for(id)` would crash. Override so those chains stay a RecordIdField
|
|
930
|
+
// (the schema-CHANGING wrappers like `.optional()` still narrow to SField via SField's overrides).
|
|
931
|
+
protected override rebuild<S2 extends z.ZodType, F2 extends string>(
|
|
932
|
+
schema: S2,
|
|
933
|
+
native: SurrealMeta,
|
|
934
|
+
): SField<S2, F2> {
|
|
935
|
+
return new RecordIdField<T, V>(
|
|
936
|
+
this.tables,
|
|
937
|
+
this.valueType,
|
|
938
|
+
native,
|
|
939
|
+
schema as unknown as z.ZodType<RecordId<T, V>, RecordId<T, V>>,
|
|
940
|
+
) as unknown as SField<S2, F2>;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/** Restrict the id value's type — reflected as `RecordId<T, V>` and validated at runtime. */
|
|
944
|
+
type<V2 extends RecordIdValue>(schema: z.ZodType<V2>): RecordIdField<T, V2> {
|
|
945
|
+
return new RecordIdField<T, V2>(this.tables, schema, this.surreal);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/** Build a RecordId. Single-table: `for(id)`; multi-table: `for(table, id)`. */
|
|
949
|
+
for(idOrTable: V | T, id?: V): RecordId<T, V> {
|
|
950
|
+
return (
|
|
951
|
+
id === undefined
|
|
952
|
+
? new RecordId(this.tables[0]!, idOrTable as V)
|
|
953
|
+
: new RecordId(idOrTable as T, id)
|
|
954
|
+
) as RecordId<T, V>;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/** A record-id range for queries (default: inclusive start .. exclusive end). */
|
|
958
|
+
range(from?: V | Bound<V>, to?: V | Bound<V>): RecordIdRange<T, V> {
|
|
959
|
+
// `undefined` -> an open bound (`user:..x` / `user:x..`); otherwise wrap the value
|
|
960
|
+
// (default inclusive start, exclusive end). Pass a Bound to override inclusivity.
|
|
961
|
+
const bound = (b: V | Bound<V> | undefined, exclusive: boolean) =>
|
|
962
|
+
b === undefined
|
|
963
|
+
? undefined
|
|
964
|
+
: b instanceof BoundIncluded || b instanceof BoundExcluded
|
|
965
|
+
? b
|
|
966
|
+
: exclusive
|
|
967
|
+
? new BoundExcluded(b)
|
|
968
|
+
: new BoundIncluded(b);
|
|
969
|
+
return new RecordIdRange(
|
|
970
|
+
this.tables[0]!,
|
|
971
|
+
bound(from, false) as Bound<RecordIdValue>,
|
|
972
|
+
bound(to, true) as Bound<RecordIdValue>,
|
|
973
|
+
) as RecordIdRange<T, V>;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/** Unwrap an SField to its Zod schema (raw Zod schemas pass through). */
|
|
978
|
+
const toZod = (v: AnyField | z.ZodType): z.ZodType =>
|
|
979
|
+
v instanceof SField ? v.schema : v;
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Internal `Flags` brand for a field with no SurrealQL mapping. It rides the `Flags` channel,
|
|
983
|
+
* so it survives every wrapper (`.optional()`/`.array()`/…); `defineTable`/`defineRelation`
|
|
984
|
+
* reject a branded field at compile time, and `.$surreal(...)` clears it. (Runtime `inferField`
|
|
985
|
+
* is the backstop for nested/dynamic shapes.)
|
|
986
|
+
*/
|
|
987
|
+
type NoDdl = "~no-ddl";
|
|
988
|
+
const noDdl = <S extends z.ZodType>(f: SField<S>): SField<S, NoDdl> =>
|
|
989
|
+
f as SField<S, NoDdl>;
|
|
990
|
+
type ZodsOf<T extends readonly (AnyField | z.ZodType)[]> = {
|
|
991
|
+
-readonly [K in keyof T]: SchemaOf<T[K]>;
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
/** Field constructors — the authoring surface. */
|
|
995
|
+
export const s = {
|
|
996
|
+
string: () => new SField(z.string()),
|
|
997
|
+
number: () => new SField(z.number()),
|
|
998
|
+
boolean: () => new SField(z.boolean()),
|
|
999
|
+
// String formats — all map to DDL `string` (their Zod def.type is "string"). Builders
|
|
1000
|
+
// whose `string::is_<fmt>` validator exists on SurrealDB bake that ASSERT by default
|
|
1001
|
+
// (see STRING_IS_FORMATS); the rest stay assert-free (no fabricated regex).
|
|
1002
|
+
email: () => formatField(z.email(), "email"),
|
|
1003
|
+
url: (params?: Parameters<typeof z.url>[0]) =>
|
|
1004
|
+
formatField(z.url(params), "url"),
|
|
1005
|
+
/** Surreal native `uuid`: a `string` app-side, stored as a `Uuid` (no ASSERT — native type). */
|
|
1006
|
+
uuid: () => new SField(uuidCodec()),
|
|
1007
|
+
guid: (params?: Parameters<typeof z.guid>[0]) =>
|
|
1008
|
+
formatField(z.guid(params), "guid"),
|
|
1009
|
+
nanoid: (params?: Parameters<typeof z.nanoid>[0]) =>
|
|
1010
|
+
formatField(z.nanoid(params), "nanoid"),
|
|
1011
|
+
cuid: (params?: Parameters<typeof z.cuid>[0]) =>
|
|
1012
|
+
formatField(z.cuid(params), "cuid"),
|
|
1013
|
+
cuid2: (params?: Parameters<typeof z.cuid2>[0]) =>
|
|
1014
|
+
formatField(z.cuid2(params), "cuid2"),
|
|
1015
|
+
ulid: (params?: Parameters<typeof z.ulid>[0]) =>
|
|
1016
|
+
formatField(z.ulid(params), "ulid"),
|
|
1017
|
+
xid: (params?: Parameters<typeof z.xid>[0]) =>
|
|
1018
|
+
formatField(z.xid(params), "xid"),
|
|
1019
|
+
ksuid: (params?: Parameters<typeof z.ksuid>[0]) =>
|
|
1020
|
+
formatField(z.ksuid(params), "ksuid"),
|
|
1021
|
+
ipv4: (params?: Parameters<typeof z.ipv4>[0]) =>
|
|
1022
|
+
formatField(z.ipv4(params), "ipv4"),
|
|
1023
|
+
ipv6: (params?: Parameters<typeof z.ipv6>[0]) =>
|
|
1024
|
+
formatField(z.ipv6(params), "ipv6"),
|
|
1025
|
+
cidrv4: (params?: Parameters<typeof z.cidrv4>[0]) =>
|
|
1026
|
+
formatField(z.cidrv4(params), "cidrv4"),
|
|
1027
|
+
cidrv6: (params?: Parameters<typeof z.cidrv6>[0]) =>
|
|
1028
|
+
formatField(z.cidrv6(params), "cidrv6"),
|
|
1029
|
+
base64: (params?: Parameters<typeof z.base64>[0]) =>
|
|
1030
|
+
formatField(z.base64(params), "base64"),
|
|
1031
|
+
base64url: (params?: Parameters<typeof z.base64url>[0]) =>
|
|
1032
|
+
formatField(z.base64url(params), "base64url"),
|
|
1033
|
+
e164: (params?: Parameters<typeof z.e164>[0]) =>
|
|
1034
|
+
formatField(z.e164(params), "e164"),
|
|
1035
|
+
jwt: (params?: Parameters<typeof z.jwt>[0]) =>
|
|
1036
|
+
formatField(z.jwt(params), "jwt"),
|
|
1037
|
+
emoji: (params?: Parameters<typeof z.emoji>[0]) =>
|
|
1038
|
+
formatField(z.emoji(params), "emoji"),
|
|
1039
|
+
|
|
1040
|
+
// String fields validated by SurrealDB's `string::is_*` (no Zod format — plain string
|
|
1041
|
+
// app-side, the baked ASSERT enforces the format in the DB).
|
|
1042
|
+
alpha: () => formatField(z.string(), "alpha"),
|
|
1043
|
+
alphanum: () => formatField(z.string(), "alphanum"),
|
|
1044
|
+
ascii: () => formatField(z.string(), "ascii"),
|
|
1045
|
+
numeric: () => formatField(z.string(), "numeric"),
|
|
1046
|
+
semver: () => formatField(z.string(), "semver"),
|
|
1047
|
+
hexadecimal: () => formatField(z.string(), "hexadecimal"),
|
|
1048
|
+
latitude: () => formatField(z.string(), "latitude"),
|
|
1049
|
+
longitude: () => formatField(z.string(), "longitude"),
|
|
1050
|
+
ip: () => formatField(z.string(), "ip"),
|
|
1051
|
+
domain: () => formatField(z.string(), "domain"),
|
|
1052
|
+
|
|
1053
|
+
// Numbers. int/int32/uint32 -> DDL `int`; float -> DDL `float` (def.format-driven).
|
|
1054
|
+
int: (params?: Parameters<typeof z.int>[0]) => new SField(z.int(params)),
|
|
1055
|
+
float: (params?: Parameters<typeof z.float64>[0]) =>
|
|
1056
|
+
new SField(z.float64(params)),
|
|
1057
|
+
int32: (params?: Parameters<typeof z.int32>[0]) =>
|
|
1058
|
+
new SField(z.int32(params)),
|
|
1059
|
+
uint32: (params?: Parameters<typeof z.uint32>[0]) =>
|
|
1060
|
+
new SField(z.uint32(params)),
|
|
1061
|
+
bigint: (params?: Parameters<typeof z.bigint>[0]) =>
|
|
1062
|
+
new SField(z.bigint(params)),
|
|
1063
|
+
|
|
1064
|
+
datetime: () => new SField(datetimeCodec()),
|
|
1065
|
+
/** Alias of `datetime` (Surreal stores a `datetime`; there is no plain date). */
|
|
1066
|
+
date: () => new SField(datetimeCodec()),
|
|
1067
|
+
/** Surreal `duration` (a `Duration` instance). */
|
|
1068
|
+
duration: () => new SField(native(z.instanceof(Duration), "duration")),
|
|
1069
|
+
/** Surreal `decimal` (a `Decimal` instance — arbitrary precision). */
|
|
1070
|
+
decimal: () => new SField(native(z.instanceof(Decimal), "decimal")),
|
|
1071
|
+
/** Surreal `bytes` (a `Uint8Array`). */
|
|
1072
|
+
bytes: () => new SField(bytesCodec()),
|
|
1073
|
+
/** Surreal `file` (a `FileRef`). */
|
|
1074
|
+
file: () => new SField(native(z.instanceof(FileRef), "file")),
|
|
1075
|
+
/** Surreal `geometry` (a `Geometry`), optionally narrowed to a kind. */
|
|
1076
|
+
geometry: (kind?: GeometryKind) =>
|
|
1077
|
+
new SField(
|
|
1078
|
+
native(z.instanceof(Geometry), kind ? `geometry<${kind}>` : "geometry"),
|
|
1079
|
+
),
|
|
1080
|
+
/**
|
|
1081
|
+
* A `record<…>` link. Pass a table name, the imported `TableDef`/`RelationDef`, or an array for a
|
|
1082
|
+
* multi-table union — `s.recordId(User)`, `s.recordId([User, Service])` — so a table's name is
|
|
1083
|
+
* only ever written in its own definition. (For a single-table link `User.record()` is preferred:
|
|
1084
|
+
* it also carries the id value type; `User.record().or(Post.record())` composes a union.)
|
|
1085
|
+
*
|
|
1086
|
+
* Called with NO argument — `s.recordId()` — it emits a bare `record` (a link to ANY table), since a
|
|
1087
|
+
* record id's table is optional in SurrealDB.
|
|
1088
|
+
*/
|
|
1089
|
+
recordId: <T extends string | AnyTable = string>(
|
|
1090
|
+
table?: T | readonly T[],
|
|
1091
|
+
): RecordIdField<T extends string ? T : NamesOf<T>> =>
|
|
1092
|
+
new RecordIdField(
|
|
1093
|
+
(table === undefined ? [] : Array.isArray(table) ? table : [table]).map(
|
|
1094
|
+
(t) => (typeof t === "string" ? t : t.name),
|
|
1095
|
+
) as (T extends string ? T : NamesOf<T>)[],
|
|
1096
|
+
),
|
|
1097
|
+
/**
|
|
1098
|
+
* A nested object whose fields keep their surreal metadata + native types. The returned
|
|
1099
|
+
* schema TYPE carries the original shape `S` via the `~szShape` brand (type-only — runtime
|
|
1100
|
+
* is unchanged) so `CreateValue`/`ShapeOf` can recover the nested fields' create-flags
|
|
1101
|
+
* (e.g. a nested `$default`) and make them create-optional. The brand survives every
|
|
1102
|
+
* `$`-method and Zod wrapper (`.optional()`/`.array()`/`.$default()`), which all reuse
|
|
1103
|
+
* `this.schema`.
|
|
1104
|
+
*/
|
|
1105
|
+
object: <S extends Shape>(shape: S): SField<SZObject<S>> => {
|
|
1106
|
+
const fields: Record<string, AnyField> = {};
|
|
1107
|
+
const zshape: Record<string, z.ZodType> = {};
|
|
1108
|
+
for (const [k, v] of Object.entries(shape)) {
|
|
1109
|
+
const f = v instanceof SField ? v : new SField(v);
|
|
1110
|
+
fields[k] = f;
|
|
1111
|
+
zshape[k] = f.schema;
|
|
1112
|
+
}
|
|
1113
|
+
const schema = z.object(zshape) as SZObject<S>;
|
|
1114
|
+
objectFieldsRegistry.set(schema, fields);
|
|
1115
|
+
return new SField(schema);
|
|
1116
|
+
},
|
|
1117
|
+
/** An array of `element`. `opts.max` -> sized `array<T, N>` (N is the MAX length). */
|
|
1118
|
+
array: <F extends AnyField | z.ZodType>(
|
|
1119
|
+
element: F,
|
|
1120
|
+
opts?: { max?: number },
|
|
1121
|
+
): SField<z.ZodArray<SchemaOf<F>>> => {
|
|
1122
|
+
const base = (
|
|
1123
|
+
element instanceof SField ? element : new SField(element)
|
|
1124
|
+
).array() as SField<z.ZodArray<SchemaOf<F>>>;
|
|
1125
|
+
return opts?.max === undefined
|
|
1126
|
+
? base
|
|
1127
|
+
: new SField(base.schema.max(opts.max), base.surreal);
|
|
1128
|
+
},
|
|
1129
|
+
/** A literal value type. */
|
|
1130
|
+
literal: <const T extends string | number | boolean | bigint>(value: T) =>
|
|
1131
|
+
new SField(z.literal(value)),
|
|
1132
|
+
/** A string enum. */
|
|
1133
|
+
enum: <const T extends readonly [string, ...string[]]>(values: T) =>
|
|
1134
|
+
new SField(z.enum(values)),
|
|
1135
|
+
/** A union of fields/schemas. */
|
|
1136
|
+
union: <
|
|
1137
|
+
const T extends readonly [
|
|
1138
|
+
AnyField | z.ZodType,
|
|
1139
|
+
...(AnyField | z.ZodType)[],
|
|
1140
|
+
],
|
|
1141
|
+
>(
|
|
1142
|
+
options: T,
|
|
1143
|
+
): SField<z.ZodUnion<ZodsOf<T>>> =>
|
|
1144
|
+
new SField(z.union(options.map(toZod) as ZodsOf<T>)),
|
|
1145
|
+
/** A fixed-length tuple of fields/schemas. */
|
|
1146
|
+
tuple: <
|
|
1147
|
+
const T extends readonly [
|
|
1148
|
+
AnyField | z.ZodType,
|
|
1149
|
+
...(AnyField | z.ZodType)[],
|
|
1150
|
+
],
|
|
1151
|
+
>(
|
|
1152
|
+
items: T,
|
|
1153
|
+
): SField<z.ZodTuple<ZodsOf<T>>> =>
|
|
1154
|
+
new SField(z.tuple(items.map(toZod) as ZodsOf<T>)),
|
|
1155
|
+
|
|
1156
|
+
/** An open-keyed record `record<key, value>` -> SurrealQL `object` with a `.*` value field. */
|
|
1157
|
+
record: <K extends z.core.$ZodRecordKey, V extends AnyField | z.ZodType>(
|
|
1158
|
+
key: K,
|
|
1159
|
+
value: V,
|
|
1160
|
+
): SField<z.ZodRecord<K, SchemaOf<V>>> =>
|
|
1161
|
+
new SField(z.record(key, toZod(value) as SchemaOf<V>)),
|
|
1162
|
+
/** A `Map<key, value>` -> SurrealQL `object` with a `.*` value field. */
|
|
1163
|
+
map: <K extends AnyField | z.ZodType, V extends AnyField | z.ZodType>(
|
|
1164
|
+
key: K,
|
|
1165
|
+
value: V,
|
|
1166
|
+
): SField<z.ZodMap<SchemaOf<K>, SchemaOf<V>>> =>
|
|
1167
|
+
new SField(z.map(toZod(key) as SchemaOf<K>, toZod(value) as SchemaOf<V>)),
|
|
1168
|
+
/** A `Set<element>` -> SurrealQL `set<element>`. `opts.max` -> sized `set<T, N>` (MAX). */
|
|
1169
|
+
set: <V extends AnyField | z.ZodType>(
|
|
1170
|
+
element: V,
|
|
1171
|
+
opts?: { max?: number },
|
|
1172
|
+
): SField<z.ZodSet<SchemaOf<V>>> => {
|
|
1173
|
+
const base = z.set(toZod(element) as SchemaOf<V>);
|
|
1174
|
+
return new SField(
|
|
1175
|
+
opts?.max === undefined ? base : base.max(opts.max),
|
|
1176
|
+
) as SField<z.ZodSet<SchemaOf<V>>>;
|
|
1177
|
+
},
|
|
1178
|
+
/** The intersection of two schemas (object fields are merged in DDL). */
|
|
1179
|
+
intersection: <
|
|
1180
|
+
A extends AnyField | z.ZodType,
|
|
1181
|
+
B extends AnyField | z.ZodType,
|
|
1182
|
+
>(
|
|
1183
|
+
a: A,
|
|
1184
|
+
b: B,
|
|
1185
|
+
): SField<z.ZodIntersection<SchemaOf<A>, SchemaOf<B>>> =>
|
|
1186
|
+
new SField(
|
|
1187
|
+
z.intersection(toZod(a) as SchemaOf<A>, toZod(b) as SchemaOf<B>),
|
|
1188
|
+
),
|
|
1189
|
+
/** A lazily-resolved schema/field (for recursive types). */
|
|
1190
|
+
lazy: <V extends AnyField | z.ZodType>(
|
|
1191
|
+
getter: () => V,
|
|
1192
|
+
): SField<z.ZodLazy<SchemaOf<V>>> =>
|
|
1193
|
+
new SField(z.lazy(() => toZod(getter()) as SchemaOf<V>)),
|
|
1194
|
+
|
|
1195
|
+
/** A native TS enum — string or numeric (numeric reverse-mappings are filtered out). */
|
|
1196
|
+
nativeEnum: <const T extends Record<string, string | number>>(entries: T) =>
|
|
1197
|
+
new SField(z.nativeEnum(entries)),
|
|
1198
|
+
/** A discriminated union of object schemas/fields -> DDL `object`. */
|
|
1199
|
+
discriminatedUnion: <
|
|
1200
|
+
Disc extends string,
|
|
1201
|
+
const T extends readonly [
|
|
1202
|
+
AnyField | z.ZodType,
|
|
1203
|
+
...(AnyField | z.ZodType)[],
|
|
1204
|
+
],
|
|
1205
|
+
>(
|
|
1206
|
+
discriminator: Disc,
|
|
1207
|
+
options: T,
|
|
1208
|
+
): SField<z.ZodDiscriminatedUnion<ZodsOf<T>, Disc>> =>
|
|
1209
|
+
new SField(
|
|
1210
|
+
z.discriminatedUnion(
|
|
1211
|
+
discriminator,
|
|
1212
|
+
options.map(toZod) as never,
|
|
1213
|
+
) as unknown as z.ZodDiscriminatedUnion<ZodsOf<T>, Disc>,
|
|
1214
|
+
),
|
|
1215
|
+
|
|
1216
|
+
/** Wrap a field/schema as optional (constructor form of `.optional()`). */
|
|
1217
|
+
optional: <F extends AnyField | z.ZodType>(
|
|
1218
|
+
field: F,
|
|
1219
|
+
): SField<z.ZodOptional<SchemaOf<F>>, FlagsOf<F>> =>
|
|
1220
|
+
(field instanceof SField ? field : new SField(field)).optional() as SField<
|
|
1221
|
+
z.ZodOptional<SchemaOf<F>>,
|
|
1222
|
+
FlagsOf<F>
|
|
1223
|
+
>,
|
|
1224
|
+
/** Wrap a field/schema as nullable (constructor form of `.nullable()`). */
|
|
1225
|
+
nullable: <F extends AnyField | z.ZodType>(
|
|
1226
|
+
field: F,
|
|
1227
|
+
): SField<z.ZodNullable<SchemaOf<F>>, FlagsOf<F>> =>
|
|
1228
|
+
(field instanceof SField ? field : new SField(field)).nullable() as SField<
|
|
1229
|
+
z.ZodNullable<SchemaOf<F>>,
|
|
1230
|
+
FlagsOf<F>
|
|
1231
|
+
>,
|
|
1232
|
+
|
|
1233
|
+
/** Optional **and** nullable — Zod's `nullish`. */
|
|
1234
|
+
nullish: <F extends AnyField | z.ZodType>(
|
|
1235
|
+
field: F,
|
|
1236
|
+
): SField<z.ZodNullable<z.ZodOptional<SchemaOf<F>>>, FlagsOf<F>> => {
|
|
1237
|
+
const f = field instanceof SField ? field : new SField(field);
|
|
1238
|
+
return f.optional().nullable() as SField<
|
|
1239
|
+
z.ZodNullable<z.ZodOptional<SchemaOf<F>>>,
|
|
1240
|
+
FlagsOf<F>
|
|
1241
|
+
>;
|
|
1242
|
+
},
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* Zod-style coercion. Each maps to the **same** SurrealQL type as its non-coerced builder —
|
|
1246
|
+
* coercion only loosens the app/input side; the DB/wire type is unchanged.
|
|
1247
|
+
*/
|
|
1248
|
+
coerce: {
|
|
1249
|
+
string: () => new SField(z.coerce.string()),
|
|
1250
|
+
number: () => new SField(z.coerce.number()),
|
|
1251
|
+
boolean: () => new SField(z.coerce.boolean()),
|
|
1252
|
+
bigint: () => new SField(z.coerce.bigint()),
|
|
1253
|
+
date: () => new SField(coercedDatetimeCodec()),
|
|
1254
|
+
},
|
|
1255
|
+
|
|
1256
|
+
// Catch-alls.
|
|
1257
|
+
any: () => new SField(z.any()),
|
|
1258
|
+
unknown: () => new SField(z.unknown()),
|
|
1259
|
+
null: () => new SField(z.null()),
|
|
1260
|
+
|
|
1261
|
+
// --- Non-Surreal types ---
|
|
1262
|
+
// Present so a global `z.*` -> `s.*` swap never collides. They carry NO SurrealQL mapping,
|
|
1263
|
+
// so they're rejected as a table field at compile time (and by `inferField` at runtime) —
|
|
1264
|
+
// unless you teach them to serialize via `.$surreal(type, codec)`.
|
|
1265
|
+
symbol: () => noDdl(new SField(z.symbol())),
|
|
1266
|
+
undefined: () => noDdl(new SField(z.undefined())),
|
|
1267
|
+
void: () => noDdl(new SField(z.void())),
|
|
1268
|
+
never: () => noDdl(new SField(z.never())),
|
|
1269
|
+
nan: () => noDdl(new SField(z.nan())),
|
|
1270
|
+
custom: <T>(check?: (val: unknown) => boolean) =>
|
|
1271
|
+
noDdl(new SField(z.custom<T>(check))),
|
|
1272
|
+
instanceof: <T extends Parameters<typeof z.instanceof>[0]>(cls: T) =>
|
|
1273
|
+
noDdl(new SField(z.instanceof(cls))),
|
|
1274
|
+
promise: <F extends AnyField | z.ZodType>(schema: F) =>
|
|
1275
|
+
noDdl(new SField(z.promise(toZod(schema)))),
|
|
1276
|
+
/** Zod's function factory (not a schema/field — present for drop-in `z.*` parity). */
|
|
1277
|
+
function: (...args: Parameters<typeof z.function>) => z.function(...args),
|
|
1278
|
+
};
|
|
1279
|
+
|
|
1280
|
+
// --- Tables & relations ---
|
|
1281
|
+
|
|
1282
|
+
export type Shape = Record<string, AnyField | z.ZodType>;
|
|
1283
|
+
type SchemaOf<F> =
|
|
1284
|
+
F extends SField<infer S, infer _> ? S : F extends z.ZodType ? F : never;
|
|
1285
|
+
type FlagsOf<F> = F extends SField<z.ZodType, infer Fl> ? Fl : never;
|
|
1286
|
+
/**
|
|
1287
|
+
* Whether a field carries the `"internal"` flag (set by `.$internal()`). The
|
|
1288
|
+
* `string extends FlagsOf<F>` guard short-circuits the broad `Shape` case (where
|
|
1289
|
+
* flags widen to `string`, and `"internal" extends string` would wrongly be true),
|
|
1290
|
+
* so `ZShape<Shape>` keeps every key for shape-agnostic refs like `TableDef<string, Shape>`.
|
|
1291
|
+
*/
|
|
1292
|
+
type IsInternal<F> =
|
|
1293
|
+
string extends FlagsOf<F>
|
|
1294
|
+
? false
|
|
1295
|
+
: "internal" extends FlagsOf<F>
|
|
1296
|
+
? true
|
|
1297
|
+
: false;
|
|
1298
|
+
/** The public zshape — internal fields are excluded (see `ZShapeAll` for the system view). */
|
|
1299
|
+
type ZShape<S extends Shape> = {
|
|
1300
|
+
[K in keyof S as IsInternal<S[K]> extends true ? never : K]: SchemaOf<S[K]>;
|
|
1301
|
+
};
|
|
1302
|
+
/** Every field's zshape, including internal ones — backs the `.system` view. */
|
|
1303
|
+
type ZShapeAll<S extends Shape> = { [K in keyof S]: SchemaOf<S[K]> };
|
|
1304
|
+
/**
|
|
1305
|
+
* The schema type returned by `s.object`: a plain `z.ZodObject` carrying its original
|
|
1306
|
+
* `Shape` via a type-only `~szShape` brand. The brand is optional, so the runtime cast
|
|
1307
|
+
* (`z.object(...) as SZObject<S>`) is sound and the brand is invisible to `z.input`/
|
|
1308
|
+
* `z.output`/`App`/`Wire` — nested fields stay REQUIRED on the decoded side. It exists
|
|
1309
|
+
* solely so `ShapeOf`/`CreateValue` can recover the nested shape for the create surface.
|
|
1310
|
+
*/
|
|
1311
|
+
type SZObject<S extends Shape> = z.ZodObject<ZShape<S>> & {
|
|
1312
|
+
readonly "~szShape"?: S;
|
|
1313
|
+
};
|
|
1314
|
+
type ToField<F> =
|
|
1315
|
+
F extends SField<infer Sc, infer Fl> ? SField<Sc, Fl> : SField<SchemaOf<F>>;
|
|
1316
|
+
type Fields<S extends Shape> = { [K in keyof S]: ToField<S[K]> };
|
|
1317
|
+
type Unwrap<F> =
|
|
1318
|
+
F extends SField<z.ZodOptional<infer Inner extends z.ZodType>, infer Fl>
|
|
1319
|
+
? SField<Inner, Fl>
|
|
1320
|
+
: F;
|
|
1321
|
+
type PartialShape<S extends Shape> = {
|
|
1322
|
+
[K in keyof S]: SField<z.ZodOptional<SchemaOf<S[K]>>, FlagsOf<S[K]>>;
|
|
1323
|
+
};
|
|
1324
|
+
type RequiredShape<S extends Shape> = { [K in keyof S]: Unwrap<Fields<S>[K]> };
|
|
1325
|
+
|
|
1326
|
+
export interface TableConfig {
|
|
1327
|
+
schemafull: boolean;
|
|
1328
|
+
/** Table `TYPE`: `normal` (default) or `any` (holds both records and graph edges). */
|
|
1329
|
+
type?: "normal" | "any";
|
|
1330
|
+
drop?: boolean;
|
|
1331
|
+
comment?: string;
|
|
1332
|
+
/** Table-level `PERMISSIONS`. Omitted ops default to NONE in SurrealDB. See `.permissions()`. */
|
|
1333
|
+
permissions?: TablePermissions;
|
|
1334
|
+
relation?: { from: string[]; to: string[]; enforced?: boolean };
|
|
1335
|
+
/** Composite (multi-field) indexes. See `.index(name, fields, opts)`. */
|
|
1336
|
+
indexes?: TableIndex[];
|
|
1337
|
+
/** Row-change events. See `.event(name, { when?, then })`. */
|
|
1338
|
+
events?: TableEvent[];
|
|
1339
|
+
/** `CHANGEFEED <dur> [INCLUDE ORIGINAL]`. See `.changefeed(dur, opts?)`. */
|
|
1340
|
+
changefeed?: { expiry: string; includeOriginal?: boolean };
|
|
1341
|
+
/**
|
|
1342
|
+
* A pre-computed (materialized) VIEW — `AS <SELECT …>`. When set, the table is computed from the
|
|
1343
|
+
* query (forced `TYPE ANY SCHEMALESS`, no authored fields). See {@link defineView}.
|
|
1344
|
+
*/
|
|
1345
|
+
view?: Expr;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/** A table index definition (single- or multi-field, or a row-count index). */
|
|
1349
|
+
export interface TableIndex {
|
|
1350
|
+
name: string;
|
|
1351
|
+
fields: string[];
|
|
1352
|
+
unique?: boolean;
|
|
1353
|
+
/** A materialized row-count index (`DEFINE INDEX … COUNT`, no fields). */
|
|
1354
|
+
count?: boolean;
|
|
1355
|
+
/** `COMMENT <string>` on the index. */
|
|
1356
|
+
comment?: string;
|
|
1357
|
+
/**
|
|
1358
|
+
* A special index spec appended after `FIELDS` — a vector (`HNSW …`/`DISKANN …`) or full-text
|
|
1359
|
+
* (`FULLTEXT ANALYZER …`) index. Built from the `.index()` opts; minimal form (SurrealDB
|
|
1360
|
+
* materializes the rest), so it round-trips against the introspected, canonicalized spec.
|
|
1361
|
+
*/
|
|
1362
|
+
spec?: string;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
/** Options for a HNSW vector index (`.index(name, [field], { hnsw: {…} })`). */
|
|
1366
|
+
export interface HnswOptions {
|
|
1367
|
+
dimension: number;
|
|
1368
|
+
dist?: "euclidean" | "cosine" | "manhattan" | "minkowski" | "hamming";
|
|
1369
|
+
type?: "f64" | "f32" | "i64" | "i32" | "i16";
|
|
1370
|
+
efc?: number;
|
|
1371
|
+
m?: number;
|
|
1372
|
+
}
|
|
1373
|
+
/** Options for a DISKANN vector index (`.index(name, [field], { diskann: {…} })`). */
|
|
1374
|
+
export interface DiskannOptions {
|
|
1375
|
+
dimension: number;
|
|
1376
|
+
dist?: "euclidean" | "cosine" | "manhattan";
|
|
1377
|
+
type?: "f64" | "f32" | "i64" | "i32" | "i16";
|
|
1378
|
+
degree?: number;
|
|
1379
|
+
l_build?: number;
|
|
1380
|
+
alpha?: number;
|
|
1381
|
+
}
|
|
1382
|
+
/** Options for a FULL-TEXT search index (`.index(name, [field], { fulltext: {…} })`). Needs a
|
|
1383
|
+
* `defineAnalyzer` of the same name. `bm25: [k1, b]` tunes scoring; `true` uses the defaults. */
|
|
1384
|
+
export interface FulltextOptions {
|
|
1385
|
+
analyzer: string;
|
|
1386
|
+
bm25?: boolean | [number, number];
|
|
1387
|
+
highlights?: boolean;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
/** Build the special index spec string (minimal — SurrealDB fills in the rest). */
|
|
1391
|
+
function buildIndexSpec(opts: {
|
|
1392
|
+
hnsw?: HnswOptions;
|
|
1393
|
+
diskann?: DiskannOptions;
|
|
1394
|
+
fulltext?: FulltextOptions;
|
|
1395
|
+
}): string | undefined {
|
|
1396
|
+
if (opts.hnsw) {
|
|
1397
|
+
const h = opts.hnsw;
|
|
1398
|
+
let s = `HNSW DIMENSION ${h.dimension}`;
|
|
1399
|
+
if (h.dist) s += ` DIST ${h.dist.toUpperCase()}`;
|
|
1400
|
+
if (h.type) s += ` TYPE ${h.type.toUpperCase()}`;
|
|
1401
|
+
if (h.efc !== undefined) s += ` EFC ${h.efc}`;
|
|
1402
|
+
if (h.m !== undefined) s += ` M ${h.m}`;
|
|
1403
|
+
return s;
|
|
1404
|
+
}
|
|
1405
|
+
if (opts.diskann) {
|
|
1406
|
+
const d = opts.diskann;
|
|
1407
|
+
let s = `DISKANN DIMENSION ${d.dimension}`;
|
|
1408
|
+
if (d.dist) s += ` DIST ${d.dist.toUpperCase()}`;
|
|
1409
|
+
if (d.type) s += ` TYPE ${d.type.toUpperCase()}`;
|
|
1410
|
+
if (d.degree !== undefined) s += ` DEGREE ${d.degree}`;
|
|
1411
|
+
if (d.l_build !== undefined) s += ` L_BUILD ${d.l_build}`;
|
|
1412
|
+
if (d.alpha !== undefined) s += ` ALPHA ${d.alpha}`;
|
|
1413
|
+
return s;
|
|
1414
|
+
}
|
|
1415
|
+
if (opts.fulltext) {
|
|
1416
|
+
const f = opts.fulltext;
|
|
1417
|
+
let s = `FULLTEXT ANALYZER ${f.analyzer}`;
|
|
1418
|
+
if (Array.isArray(f.bm25)) s += ` BM25(${f.bm25[0]},${f.bm25[1]})`;
|
|
1419
|
+
if (f.highlights) s += " HIGHLIGHTS";
|
|
1420
|
+
return s;
|
|
1421
|
+
}
|
|
1422
|
+
return undefined;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/** A SurrealQL expression: a `surql\`…\`` bound query (bindings inlined) or a raw string. */
|
|
1426
|
+
export type Expr = BoundQuery | string;
|
|
1427
|
+
|
|
1428
|
+
/**
|
|
1429
|
+
* A table event: `DEFINE EVENT <name> ON TABLE <table> [WHEN <when>] THEN <then>`. The event
|
|
1430
|
+
* body sees `$before`/`$after`/`$event`/`$value`. `then` may be one expression or several
|
|
1431
|
+
* (run in order). Author expressions with `surql\`…\`` (bindings inline) or a raw string.
|
|
1432
|
+
*/
|
|
1433
|
+
export interface TableEvent {
|
|
1434
|
+
name: string;
|
|
1435
|
+
when?: Expr;
|
|
1436
|
+
then: Expr | Expr[];
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function normalizeFields<S extends Shape>(shape: S): Fields<S> {
|
|
1440
|
+
const out: Record<string, AnyField> = {};
|
|
1441
|
+
for (const [k, v] of Object.entries(shape)) {
|
|
1442
|
+
out[k] = v instanceof SField ? v : new SField(v);
|
|
1443
|
+
}
|
|
1444
|
+
return out as unknown as Fields<S>;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/** The wrappers `safeEncodeValue` peels to reach a schema registered in `objectFieldsRegistry`
|
|
1448
|
+
* (and the array element) — the same identity-preserving set `ShapeOf` strips at the type
|
|
1449
|
+
* level. `array` is intentionally NOT peeled (it's handled separately). */
|
|
1450
|
+
const ENCODE_PEEL = new Set([
|
|
1451
|
+
"optional",
|
|
1452
|
+
"nullable",
|
|
1453
|
+
"default",
|
|
1454
|
+
"prefault",
|
|
1455
|
+
"catch",
|
|
1456
|
+
"readonly",
|
|
1457
|
+
]);
|
|
1458
|
+
|
|
1459
|
+
/** Peel identity-preserving wrappers off a schema to reach its core (registered) schema. */
|
|
1460
|
+
function unwrapCore(schema: z.ZodType): z.ZodType {
|
|
1461
|
+
let s = schema;
|
|
1462
|
+
while (ENCODE_PEEL.has((s._zod.def as { type: string }).type)) {
|
|
1463
|
+
const inner = (s._zod.def as { innerType?: z.ZodType }).innerType;
|
|
1464
|
+
if (!inner) break;
|
|
1465
|
+
s = inner;
|
|
1466
|
+
}
|
|
1467
|
+
return s;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/** If `core` is a `ZodArray` whose (unwrapped) element is a registered `s.object`, return
|
|
1471
|
+
* that element's fields; otherwise undefined. */
|
|
1472
|
+
function arrayElementFields(
|
|
1473
|
+
core: z.ZodType,
|
|
1474
|
+
): Record<string, AnyField> | undefined {
|
|
1475
|
+
if ((core._zod.def as { type: string }).type !== "array") return undefined;
|
|
1476
|
+
const element = (core._zod.def as { element?: z.ZodType }).element;
|
|
1477
|
+
if (!element) return undefined;
|
|
1478
|
+
return objectFieldsRegistry.get(unwrapCore(element));
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/**
|
|
1482
|
+
* Validate + encode one provided field value to its wire form (non-throwing — the shared core
|
|
1483
|
+
* of both `encode` and `safeEncode`). A nested `s.object` (or an array of one) recurses via
|
|
1484
|
+
* `safeEncodeInput`, so absent nested keys are OMITTED — on CREATE the DB fills their defaults;
|
|
1485
|
+
* on UPDATE `encodePartial` is deep-partial and pairs with `MERGE` (which deep-merges), so
|
|
1486
|
+
* omitted siblings are preserved. Leaf fields go through `z.safeEncode` (which validates);
|
|
1487
|
+
* issues are pushed into `issues` with their path prefixed by `path`, so the aggregate
|
|
1488
|
+
* `ZodError` carries fully-qualified paths. Object-LEVEL refinements on a nested `s.object`
|
|
1489
|
+
* are skipped (rare; leaf validation still runs).
|
|
1490
|
+
*/
|
|
1491
|
+
function safeEncodeValue(
|
|
1492
|
+
field: AnyField,
|
|
1493
|
+
v: unknown,
|
|
1494
|
+
path: PropertyKey[],
|
|
1495
|
+
issues: z.core.$ZodIssue[],
|
|
1496
|
+
): unknown {
|
|
1497
|
+
const core = unwrapCore(field.schema);
|
|
1498
|
+
const nested = objectFieldsRegistry.get(core);
|
|
1499
|
+
if (nested)
|
|
1500
|
+
return safeEncodeInput(nested, v as Record<string, unknown>, path, issues);
|
|
1501
|
+
const elem = arrayElementFields(core);
|
|
1502
|
+
if (elem) {
|
|
1503
|
+
return (v as unknown[]).map((el, i) =>
|
|
1504
|
+
safeEncodeInput(
|
|
1505
|
+
elem,
|
|
1506
|
+
el as Record<string, unknown>,
|
|
1507
|
+
[...path, i],
|
|
1508
|
+
issues,
|
|
1509
|
+
),
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
const res = z.safeEncode(field.schema, v as never);
|
|
1513
|
+
if (res.success) return res.data;
|
|
1514
|
+
for (const issue of res.error.issues)
|
|
1515
|
+
issues.push({ ...issue, path: [...path, ...issue.path] });
|
|
1516
|
+
return undefined;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/** Recurse over the provided keys (see `safeEncodeValue`), omitting absent (`undefined`) ones,
|
|
1520
|
+
* building the wire object and collecting issues. */
|
|
1521
|
+
function safeEncodeInput(
|
|
1522
|
+
fields: Record<string, AnyField>,
|
|
1523
|
+
input: Record<string, unknown>,
|
|
1524
|
+
path: PropertyKey[],
|
|
1525
|
+
issues: z.core.$ZodIssue[],
|
|
1526
|
+
): Record<string, unknown> {
|
|
1527
|
+
const out: Record<string, unknown> = {};
|
|
1528
|
+
for (const [k, v] of Object.entries(input)) {
|
|
1529
|
+
if (v === undefined) continue;
|
|
1530
|
+
const field = fields[k];
|
|
1531
|
+
out[k] = field ? safeEncodeValue(field, v, [...path, k], issues) : v;
|
|
1532
|
+
}
|
|
1533
|
+
return out;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* The core of `safeEncode`/`safeEncodePartial` AND `encode`/`encodePartial`: validate+encode
|
|
1538
|
+
* the PROVIDED keys, aggregating every leaf issue (with correct paths) into one `z.ZodError`.
|
|
1539
|
+
* `safeEncode` returns the result; `encode` throws `error` (so `encode` and `safeEncode` are
|
|
1540
|
+
* the same operation — `encode` = `safeEncode` + throw — including for a PARTIAL nested
|
|
1541
|
+
* `s.object`).
|
|
1542
|
+
*/
|
|
1543
|
+
function safeEncodeFields(
|
|
1544
|
+
fields: Record<string, AnyField>,
|
|
1545
|
+
input: Record<string, unknown>,
|
|
1546
|
+
): z.ZodSafeParseResult<unknown> {
|
|
1547
|
+
const issues: z.core.$ZodIssue[] = [];
|
|
1548
|
+
const data = safeEncodeInput(fields, input, [], issues);
|
|
1549
|
+
return issues.length > 0
|
|
1550
|
+
? { success: false, error: new z.ZodError(issues) }
|
|
1551
|
+
: { success: true, data };
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
/** Async mirror of `safeEncodeValue` — awaits `z.safeEncodeAsync` per leaf and recurses into a
|
|
1555
|
+
* nested `s.object` (or array of one) via `safeEncodeInputAsync`. Backs the `*Async` writes. */
|
|
1556
|
+
async function safeEncodeValueAsync(
|
|
1557
|
+
field: AnyField,
|
|
1558
|
+
v: unknown,
|
|
1559
|
+
path: PropertyKey[],
|
|
1560
|
+
issues: z.core.$ZodIssue[],
|
|
1561
|
+
): Promise<unknown> {
|
|
1562
|
+
const core = unwrapCore(field.schema);
|
|
1563
|
+
const nested = objectFieldsRegistry.get(core);
|
|
1564
|
+
if (nested)
|
|
1565
|
+
return safeEncodeInputAsync(
|
|
1566
|
+
nested,
|
|
1567
|
+
v as Record<string, unknown>,
|
|
1568
|
+
path,
|
|
1569
|
+
issues,
|
|
1570
|
+
);
|
|
1571
|
+
const elem = arrayElementFields(core);
|
|
1572
|
+
if (elem) {
|
|
1573
|
+
return Promise.all(
|
|
1574
|
+
(v as unknown[]).map((el, i) =>
|
|
1575
|
+
safeEncodeInputAsync(
|
|
1576
|
+
elem,
|
|
1577
|
+
el as Record<string, unknown>,
|
|
1578
|
+
[...path, i],
|
|
1579
|
+
issues,
|
|
1580
|
+
),
|
|
1581
|
+
),
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
const res = await z.safeEncodeAsync(field.schema, v as never);
|
|
1585
|
+
if (res.success) return res.data;
|
|
1586
|
+
for (const issue of res.error.issues)
|
|
1587
|
+
issues.push({ ...issue, path: [...path, ...issue.path] });
|
|
1588
|
+
return undefined;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
/** Async mirror of `safeEncodeInput` — recurse over the provided keys, omitting absent ones. */
|
|
1592
|
+
async function safeEncodeInputAsync(
|
|
1593
|
+
fields: Record<string, AnyField>,
|
|
1594
|
+
input: Record<string, unknown>,
|
|
1595
|
+
path: PropertyKey[],
|
|
1596
|
+
issues: z.core.$ZodIssue[],
|
|
1597
|
+
): Promise<Record<string, unknown>> {
|
|
1598
|
+
const out: Record<string, unknown> = {};
|
|
1599
|
+
for (const [k, v] of Object.entries(input)) {
|
|
1600
|
+
if (v === undefined) continue;
|
|
1601
|
+
const field = fields[k];
|
|
1602
|
+
out[k] = field
|
|
1603
|
+
? await safeEncodeValueAsync(field, v, [...path, k], issues)
|
|
1604
|
+
: v;
|
|
1605
|
+
}
|
|
1606
|
+
return out;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
/** Async mirror of `safeEncodeFields` — backs `safeEncodeAsync`/`encodeAsync` (run + throw). */
|
|
1610
|
+
async function safeEncodeFieldsAsync(
|
|
1611
|
+
fields: Record<string, AnyField>,
|
|
1612
|
+
input: Record<string, unknown>,
|
|
1613
|
+
): Promise<z.ZodSafeParseResult<unknown>> {
|
|
1614
|
+
const issues: z.core.$ZodIssue[] = [];
|
|
1615
|
+
const data = await safeEncodeInputAsync(fields, input, [], issues);
|
|
1616
|
+
return issues.length > 0
|
|
1617
|
+
? { success: false, error: new z.ZodError(issues) }
|
|
1618
|
+
: { success: true, data };
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// --- Create / Update input shapes ---
|
|
1622
|
+
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
1623
|
+
type AppOf<F> = z.output<SchemaOf<F>>;
|
|
1624
|
+
type InputOptional<F> = undefined extends z.input<SchemaOf<F>> ? true : false;
|
|
1625
|
+
|
|
1626
|
+
/**
|
|
1627
|
+
* Recover the nested `Shape` of an `s.object` schema (`never` if the schema isn't one).
|
|
1628
|
+
* Identity-preserving wrappers (optional/default/readonly/nullable) are peeled first, then
|
|
1629
|
+
* the `~szShape` brand is read. The inner `NS extends Shape ? NS : never` drops the
|
|
1630
|
+
* `| undefined` that inferring from an optional property can introduce, so the result is the
|
|
1631
|
+
* clean shape — or `never` for any non-object schema.
|
|
1632
|
+
*/
|
|
1633
|
+
type ShapeOf<Sc> =
|
|
1634
|
+
Sc extends z.ZodOptional<infer I>
|
|
1635
|
+
? ShapeOf<I>
|
|
1636
|
+
: Sc extends z.ZodDefault<infer I>
|
|
1637
|
+
? ShapeOf<I>
|
|
1638
|
+
: Sc extends z.ZodReadonly<infer I>
|
|
1639
|
+
? ShapeOf<I>
|
|
1640
|
+
: Sc extends z.ZodNullable<infer I>
|
|
1641
|
+
? ShapeOf<I>
|
|
1642
|
+
: Sc extends { "~szShape"?: infer NS }
|
|
1643
|
+
? NS extends Shape
|
|
1644
|
+
? NS
|
|
1645
|
+
: never
|
|
1646
|
+
: never;
|
|
1647
|
+
|
|
1648
|
+
/**
|
|
1649
|
+
* The element `Shape` of an `s.object(...).array()` field (`never` otherwise). Peels the
|
|
1650
|
+
* same identity-preserving wrappers off the array, then reads the element's `~szShape`.
|
|
1651
|
+
*/
|
|
1652
|
+
type ArrayShapeOf<Sc> =
|
|
1653
|
+
Sc extends z.ZodOptional<infer I>
|
|
1654
|
+
? ArrayShapeOf<I>
|
|
1655
|
+
: Sc extends z.ZodDefault<infer I>
|
|
1656
|
+
? ArrayShapeOf<I>
|
|
1657
|
+
: Sc extends z.ZodReadonly<infer I>
|
|
1658
|
+
? ArrayShapeOf<I>
|
|
1659
|
+
: Sc extends z.ZodNullable<infer I>
|
|
1660
|
+
? ArrayShapeOf<I>
|
|
1661
|
+
: Sc extends z.ZodArray<infer E>
|
|
1662
|
+
? ShapeOf<E>
|
|
1663
|
+
: never;
|
|
1664
|
+
|
|
1665
|
+
/**
|
|
1666
|
+
* The create-input VALUE type for a field. A nested `s.object` recurses into its own
|
|
1667
|
+
* `CreateShape` (so nested `$default`/`"create"` fields become optional too); an array of
|
|
1668
|
+
* `s.object` becomes that nested create-shape's array; everything else is the plain app
|
|
1669
|
+
* type (`AppOf`). `[X] extends [never]` guards each branch because `never extends Shape` is
|
|
1670
|
+
* vacuously true and would otherwise wrongly match the object branch for scalar fields.
|
|
1671
|
+
*/
|
|
1672
|
+
type CreateValue<F, Sc = SchemaOf<F>> = [ShapeOf<Sc>] extends [never]
|
|
1673
|
+
? [ArrayShapeOf<Sc>] extends [never]
|
|
1674
|
+
? AppOf<F>
|
|
1675
|
+
: ArrayShapeOf<Sc> extends infer ENS extends Shape
|
|
1676
|
+
? CreateShape<ENS>[]
|
|
1677
|
+
: AppOf<F>
|
|
1678
|
+
: ShapeOf<Sc> extends infer NS extends Shape
|
|
1679
|
+
? CreateShape<NS>
|
|
1680
|
+
: AppOf<F>;
|
|
1681
|
+
|
|
1682
|
+
type CreateOptional<S extends Shape, K extends keyof S> = K extends "id"
|
|
1683
|
+
? true
|
|
1684
|
+
: "create" extends FlagsOf<S[K]>
|
|
1685
|
+
? true
|
|
1686
|
+
: InputOptional<S[K]>;
|
|
1687
|
+
// Public create input: internal fields are never settable by clients. Field VALUES use
|
|
1688
|
+
// `CreateValue` so a nested `s.object`'s own create-optional fields (a nested `$default`)
|
|
1689
|
+
// are optional too — while `CreateOptional` (the `?` modifier) is unchanged.
|
|
1690
|
+
type CreateShape<S extends Shape> = Prettify<
|
|
1691
|
+
{
|
|
1692
|
+
[K in keyof S as IsInternal<S[K]> extends true
|
|
1693
|
+
? never
|
|
1694
|
+
: CreateOptional<S, K> extends true
|
|
1695
|
+
? never
|
|
1696
|
+
: K]: CreateValue<S[K]>;
|
|
1697
|
+
} & {
|
|
1698
|
+
[K in keyof S as IsInternal<S[K]> extends true
|
|
1699
|
+
? never
|
|
1700
|
+
: CreateOptional<S, K> extends true
|
|
1701
|
+
? K
|
|
1702
|
+
: never]?: CreateValue<S[K]>;
|
|
1703
|
+
}
|
|
1704
|
+
>;
|
|
1705
|
+
// System create input: includes internal fields (the old, all-fields behavior).
|
|
1706
|
+
type CreateShapeAll<S extends Shape> = Prettify<
|
|
1707
|
+
{
|
|
1708
|
+
[K in keyof S as CreateOptional<S, K> extends true
|
|
1709
|
+
? never
|
|
1710
|
+
: K]: CreateValue<S[K]>;
|
|
1711
|
+
} & {
|
|
1712
|
+
[K in keyof S as CreateOptional<S, K> extends true
|
|
1713
|
+
? K
|
|
1714
|
+
: never]?: CreateValue<S[K]>;
|
|
1715
|
+
}
|
|
1716
|
+
>;
|
|
1717
|
+
|
|
1718
|
+
type UpdateExcluded<S extends Shape, K extends keyof S> = K extends "id"
|
|
1719
|
+
? true
|
|
1720
|
+
: "readonly" extends FlagsOf<S[K]>
|
|
1721
|
+
? true
|
|
1722
|
+
: false;
|
|
1723
|
+
/**
|
|
1724
|
+
* The update-input VALUE type for a field — a DEEP partial, since `MERGE` recursively
|
|
1725
|
+
* deep-merges nested objects (so any subset of nested keys is a valid patch). A nested
|
|
1726
|
+
* `s.object` recurses into its own `UpdateShape` (every nested field optional); an array
|
|
1727
|
+
* of `s.object` becomes that update-shape's array; everything else is the plain app type
|
|
1728
|
+
* (`AppOf`). The `[X] extends [never]` guards mirror `CreateValue` (so scalar fields don't
|
|
1729
|
+
* wrongly match the object branch via `never extends Shape`).
|
|
1730
|
+
*/
|
|
1731
|
+
type UpdateValue<F, Sc = SchemaOf<F>> = [ShapeOf<Sc>] extends [never]
|
|
1732
|
+
? [ArrayShapeOf<Sc>] extends [never]
|
|
1733
|
+
? AppOf<F>
|
|
1734
|
+
: ArrayShapeOf<Sc> extends infer ENS extends Shape
|
|
1735
|
+
? UpdateShape<ENS>[]
|
|
1736
|
+
: AppOf<F>
|
|
1737
|
+
: ShapeOf<Sc> extends infer NS extends Shape
|
|
1738
|
+
? UpdateShape<NS>
|
|
1739
|
+
: AppOf<F>;
|
|
1740
|
+
// Public update input: internal fields are excluded. Field VALUES use `UpdateValue` so a
|
|
1741
|
+
// nested `s.object` is itself a deep partial (every nested key optional), matching MERGE.
|
|
1742
|
+
type UpdateShape<S extends Shape> = Prettify<{
|
|
1743
|
+
[K in keyof S as IsInternal<S[K]> extends true
|
|
1744
|
+
? never
|
|
1745
|
+
: UpdateExcluded<S, K> extends true
|
|
1746
|
+
? never
|
|
1747
|
+
: K]?: UpdateValue<S[K]>;
|
|
1748
|
+
}>;
|
|
1749
|
+
// System update input: includes internal fields (the old, all-fields behavior).
|
|
1750
|
+
type UpdateShapeAll<S extends Shape> = Prettify<{
|
|
1751
|
+
[K in keyof S as UpdateExcluded<S, K> extends true ? never : K]?: UpdateValue<
|
|
1752
|
+
S[K]
|
|
1753
|
+
>;
|
|
1754
|
+
}>;
|
|
1755
|
+
|
|
1756
|
+
/** A Zod-style non-throwing result: `{ success: true; data }` | `{ success: false; error }`
|
|
1757
|
+
* (mirrors `z.safeEncode`/`z.safeDecode`). */
|
|
1758
|
+
type SafeResult<T> = z.ZodSafeParseResult<T>;
|
|
1759
|
+
/** The wire payload `encode`/`safeEncode` build: the provided keys' wire (`z.input`) types. Only
|
|
1760
|
+
* the supplied keys are present at runtime, hence `Partial`. */
|
|
1761
|
+
type MakeWire<S extends Shape> = Partial<z.input<z.ZodObject<ZShape<S>>>>;
|
|
1762
|
+
/** Same, over ALL fields — the `.system` view includes `$internal()` ones. */
|
|
1763
|
+
type MakeWireAll<S extends Shape> = Partial<z.input<z.ZodObject<ZShapeAll<S>>>>;
|
|
1764
|
+
|
|
1765
|
+
/** A table (or relation) definition: shape + DDL config, with chainable builders. */
|
|
1766
|
+
export class TableDef<Name extends string, S extends Shape> {
|
|
1767
|
+
/** Zod object over the inner schemas — drives validation, encode/decode, types. */
|
|
1768
|
+
readonly object: z.ZodObject<ZShape<S>>;
|
|
1769
|
+
|
|
1770
|
+
constructor(
|
|
1771
|
+
readonly name: Name,
|
|
1772
|
+
readonly fields: Fields<S>,
|
|
1773
|
+
readonly config: TableConfig = { schemafull: true },
|
|
1774
|
+
) {
|
|
1775
|
+
// Public object skips internal fields (zod also strips unknown keys — double-safe);
|
|
1776
|
+
// `emitTable` still iterates ALL `this.fields`, so internal fields stay in the DDL.
|
|
1777
|
+
const zshape: Record<string, z.ZodType> = {};
|
|
1778
|
+
for (const [k, f] of Object.entries(fields)) {
|
|
1779
|
+
if ((f as AnyField).surreal.internal) continue;
|
|
1780
|
+
zshape[k] = (f as AnyField).schema;
|
|
1781
|
+
}
|
|
1782
|
+
this.object = z.object(zshape) as z.ZodObject<ZShape<S>>;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
get kind(): "table" | "relation" {
|
|
1786
|
+
return this.config.relation ? "relation" : "table";
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
/**
|
|
1790
|
+
* A SurrealDB `Table` instance for this table — for direct SDK calls that take a table reference,
|
|
1791
|
+
* e.g. `db.select(User.table)`. (For a record id, chain `User.record().for(id)`.)
|
|
1792
|
+
*/
|
|
1793
|
+
get table(): Table<Name> {
|
|
1794
|
+
return new Table(this.name);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
/** DB wire row -> app object. */
|
|
1798
|
+
decode(row: unknown): z.output<z.ZodObject<ZShape<S>>> {
|
|
1799
|
+
return z.decode(this.object, row as never);
|
|
1800
|
+
}
|
|
1801
|
+
/** DB wire row -> app object (async — for async refinements). */
|
|
1802
|
+
decodeAsync(row: unknown): Promise<z.output<z.ZodObject<ZShape<S>>>> {
|
|
1803
|
+
return z.decodeAsync(this.object, row as never);
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// No-throw read variants — return { success, data } | { success, error }.
|
|
1807
|
+
safeDecode(row: unknown) {
|
|
1808
|
+
return z.safeDecode(this.object, row as never);
|
|
1809
|
+
}
|
|
1810
|
+
safeDecodeAsync(row: unknown) {
|
|
1811
|
+
return z.safeDecodeAsync(this.object, row as never);
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
// Deprecated Zod-style aliases. For codecs `parse` runs the DECODE direction (wire -> app),
|
|
1815
|
+
// so it's just `decode` under a misleading name — prefer `decode` (and `encode` for create
|
|
1816
|
+
// payloads). Kept for `z`-API familiarity; editors will strike them through.
|
|
1817
|
+
/** @deprecated `parse` decodes a DB row (wire -> app). Use {@link TableDef.decode | decode}. */
|
|
1818
|
+
parse(row: unknown): z.output<z.ZodObject<ZShape<S>>> {
|
|
1819
|
+
return this.decode(row);
|
|
1820
|
+
}
|
|
1821
|
+
/** @deprecated Use {@link TableDef.safeDecode | safeDecode} (or {@link TableDef.safeEncode | safeEncode} to validate an app object). */
|
|
1822
|
+
safeParse(row: unknown) {
|
|
1823
|
+
return this.safeDecode(row);
|
|
1824
|
+
}
|
|
1825
|
+
/** @deprecated Use {@link TableDef.decodeAsync | decodeAsync}. */
|
|
1826
|
+
parseAsync(row: unknown): Promise<z.output<z.ZodObject<ZShape<S>>>> {
|
|
1827
|
+
return this.decodeAsync(row);
|
|
1828
|
+
}
|
|
1829
|
+
/** @deprecated Use {@link TableDef.safeDecodeAsync | safeDecodeAsync}. */
|
|
1830
|
+
safeParseAsync(row: unknown) {
|
|
1831
|
+
return this.safeDecodeAsync(row);
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
// --- Write side (app -> wire). `encode`/`encodePartial` are create/patch-shaped: DB-filled
|
|
1835
|
+
// (`$default`/`id`) fields are optional (the DB fills them), absent keys are OMITTED, and each
|
|
1836
|
+
// provided leaf is validated via the recursive encoder. The raw full-object codec (no create-
|
|
1837
|
+
// shaping) is `z.encode(table.object, app)` if ever needed. ---
|
|
1838
|
+
|
|
1839
|
+
/**
|
|
1840
|
+
* Build a wire payload for `CREATE` (DB-filled fields optional). Validates+encodes each
|
|
1841
|
+
* provided field — so this VALIDATES and THROWS the aggregated `z.ZodError` on invalid
|
|
1842
|
+
* input. Use `safeEncode` for the non-throwing form.
|
|
1843
|
+
*/
|
|
1844
|
+
encode(input: CreateShape<S>): MakeWire<S> {
|
|
1845
|
+
const r = this.safeEncode(input);
|
|
1846
|
+
if (!r.success) throw r.error;
|
|
1847
|
+
return r.data;
|
|
1848
|
+
}
|
|
1849
|
+
/**
|
|
1850
|
+
* Build a wire payload for `UPDATE`/`MERGE` (a partial patch; excludes id/readonly).
|
|
1851
|
+
* VALIDATES and THROWS on invalid input; use `safeEncodePartial` for the non-throwing form.
|
|
1852
|
+
*/
|
|
1853
|
+
encodePartial(input: UpdateShape<S>): MakeWire<S> {
|
|
1854
|
+
const r = this.safeEncodePartial(input);
|
|
1855
|
+
if (!r.success) throw r.error;
|
|
1856
|
+
return r.data;
|
|
1857
|
+
}
|
|
1858
|
+
/**
|
|
1859
|
+
* Non-throwing `encode`: validates+encodes the provided keys and returns a Zod-style
|
|
1860
|
+
* `{ success: true; data }` | `{ success: false; error }`. All field errors are
|
|
1861
|
+
* aggregated (with correct paths) into a single `z.ZodError`.
|
|
1862
|
+
*/
|
|
1863
|
+
safeEncode(input: CreateShape<S>): SafeResult<MakeWire<S>> {
|
|
1864
|
+
return safeEncodeFields(
|
|
1865
|
+
this.fields as unknown as Record<string, AnyField>,
|
|
1866
|
+
input as Record<string, unknown>,
|
|
1867
|
+
) as SafeResult<MakeWire<S>>;
|
|
1868
|
+
}
|
|
1869
|
+
/** Non-throwing `encodePartial` (see `safeEncode`). */
|
|
1870
|
+
safeEncodePartial(input: UpdateShape<S>): SafeResult<MakeWire<S>> {
|
|
1871
|
+
return safeEncodeFields(
|
|
1872
|
+
this.fields as unknown as Record<string, AnyField>,
|
|
1873
|
+
input as Record<string, unknown>,
|
|
1874
|
+
) as SafeResult<MakeWire<S>>;
|
|
1875
|
+
}
|
|
1876
|
+
/** Async `encode` (awaits async refinements per leaf); throws the aggregated error. */
|
|
1877
|
+
async encodeAsync(input: CreateShape<S>): Promise<MakeWire<S>> {
|
|
1878
|
+
const r = await this.safeEncodeAsync(input);
|
|
1879
|
+
if (!r.success) throw r.error;
|
|
1880
|
+
return r.data;
|
|
1881
|
+
}
|
|
1882
|
+
/** Async `encodePartial`; throws the aggregated error. */
|
|
1883
|
+
async encodePartialAsync(input: UpdateShape<S>): Promise<MakeWire<S>> {
|
|
1884
|
+
const r = await this.safeEncodePartialAsync(input);
|
|
1885
|
+
if (!r.success) throw r.error;
|
|
1886
|
+
return r.data;
|
|
1887
|
+
}
|
|
1888
|
+
/** Non-throwing async `encode` (see `safeEncode`). */
|
|
1889
|
+
safeEncodeAsync(input: CreateShape<S>): Promise<SafeResult<MakeWire<S>>> {
|
|
1890
|
+
return safeEncodeFieldsAsync(
|
|
1891
|
+
this.fields as unknown as Record<string, AnyField>,
|
|
1892
|
+
input as Record<string, unknown>,
|
|
1893
|
+
) as Promise<SafeResult<MakeWire<S>>>;
|
|
1894
|
+
}
|
|
1895
|
+
/** Non-throwing async `encodePartial`. */
|
|
1896
|
+
safeEncodePartialAsync(
|
|
1897
|
+
input: UpdateShape<S>,
|
|
1898
|
+
): Promise<SafeResult<MakeWire<S>>> {
|
|
1899
|
+
return safeEncodeFieldsAsync(
|
|
1900
|
+
this.fields as unknown as Record<string, AnyField>,
|
|
1901
|
+
input as Record<string, unknown>,
|
|
1902
|
+
) as Promise<SafeResult<MakeWire<S>>>;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
/**
|
|
1906
|
+
* The server/system view: the same table over ALL fields, including `$internal()`
|
|
1907
|
+
* ones the public surface hides. Use it in trusted server code that must read or
|
|
1908
|
+
* write internal fields (e.g. a `passhash`).
|
|
1909
|
+
*/
|
|
1910
|
+
get system(): SystemView<Name, S> {
|
|
1911
|
+
return new SystemView<Name, S>(
|
|
1912
|
+
this.fields as unknown as Record<string, AnyField>,
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// --- DDL config (chainable, immutable) ---
|
|
1917
|
+
private withConfig(config: Partial<TableConfig>): TableDef<Name, S> {
|
|
1918
|
+
return new TableDef(this.name, this.fields, { ...this.config, ...config });
|
|
1919
|
+
}
|
|
1920
|
+
schemafull() {
|
|
1921
|
+
return this.withConfig({ schemafull: true });
|
|
1922
|
+
}
|
|
1923
|
+
schemaless() {
|
|
1924
|
+
return this.withConfig({ schemafull: false });
|
|
1925
|
+
}
|
|
1926
|
+
/** `TYPE ANY` — the table may hold both normal records and graph edges. */
|
|
1927
|
+
typeAny() {
|
|
1928
|
+
return this.withConfig({ type: "any" });
|
|
1929
|
+
}
|
|
1930
|
+
drop(drop = true) {
|
|
1931
|
+
return this.withConfig({ drop });
|
|
1932
|
+
}
|
|
1933
|
+
comment(comment: string) {
|
|
1934
|
+
return this.withConfig({ comment });
|
|
1935
|
+
}
|
|
1936
|
+
/** Set table-level `PERMISSIONS` (folded into the single `DEFINE TABLE` head). */
|
|
1937
|
+
permissions(spec: TablePermissions) {
|
|
1938
|
+
return this.withConfig({ permissions: spec });
|
|
1939
|
+
}
|
|
1940
|
+
/** `CHANGEFEED <dur> [INCLUDE ORIGINAL]` — track row changes for `SHOW CHANGES`. */
|
|
1941
|
+
changefeed(expiry: string, opts: { includeOriginal?: boolean } = {}) {
|
|
1942
|
+
return this.withConfig({
|
|
1943
|
+
changefeed: { expiry, includeOriginal: opts.includeOriginal },
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
/**
|
|
1947
|
+
* Add a composite index: `DEFINE INDEX <name> ON TABLE <table> FIELDS <fields> [UNIQUE]`, or a
|
|
1948
|
+
* materialized row-count index with `{ count: true }` (no fields → `DEFINE INDEX <name> … COUNT`).
|
|
1949
|
+
*/
|
|
1950
|
+
index(
|
|
1951
|
+
name: string,
|
|
1952
|
+
fields: (keyof S & string)[],
|
|
1953
|
+
opts: {
|
|
1954
|
+
unique?: boolean;
|
|
1955
|
+
count?: boolean;
|
|
1956
|
+
comment?: string;
|
|
1957
|
+
/** A HNSW vector index over the field. */
|
|
1958
|
+
hnsw?: HnswOptions;
|
|
1959
|
+
/** A DISKANN vector index over the field. */
|
|
1960
|
+
diskann?: DiskannOptions;
|
|
1961
|
+
/** A full-text search index — needs a `defineAnalyzer` of `analyzer`'s name. */
|
|
1962
|
+
fulltext?: FulltextOptions;
|
|
1963
|
+
} = {},
|
|
1964
|
+
) {
|
|
1965
|
+
const index: TableIndex = {
|
|
1966
|
+
name,
|
|
1967
|
+
fields,
|
|
1968
|
+
unique: opts.unique,
|
|
1969
|
+
count: opts.count,
|
|
1970
|
+
comment: opts.comment,
|
|
1971
|
+
spec: buildIndexSpec(opts),
|
|
1972
|
+
};
|
|
1973
|
+
return this.withConfig({
|
|
1974
|
+
indexes: [...(this.config.indexes ?? []), index],
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
/**
|
|
1978
|
+
* Add a row-change event: `DEFINE EVENT <name> ON TABLE <table> [WHEN <when>] THEN <then>`.
|
|
1979
|
+
* The body sees `$before`/`$after`/`$event`/`$value`; author with `surql\`…\`` or a raw string.
|
|
1980
|
+
*/
|
|
1981
|
+
event(name: string, spec: { when?: Expr; then: Expr | Expr[] }) {
|
|
1982
|
+
// biome-ignore lint/suspicious/noThenProperty: `then` is the SurrealQL THEN clause (a string/BoundQuery), not a PromiseLike.
|
|
1983
|
+
const event: TableEvent = { name, when: spec.when, then: spec.then };
|
|
1984
|
+
return this.withConfig({
|
|
1985
|
+
events: [...(this.config.events ?? []), event],
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
// --- Shape ops (mirror Zod's object methods; carry DDL metadata + config) ---
|
|
1990
|
+
extend<E extends Shape>(ext: E): TableDef<Name, Omit<S, keyof E> & E> {
|
|
1991
|
+
const f: Record<string, AnyField> = {
|
|
1992
|
+
...(this.fields as unknown as Record<string, AnyField>),
|
|
1993
|
+
...normalizeFields(ext),
|
|
1994
|
+
};
|
|
1995
|
+
return new TableDef(
|
|
1996
|
+
this.name,
|
|
1997
|
+
f as unknown as Fields<Omit<S, keyof E> & E>,
|
|
1998
|
+
this.config,
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
pick<K extends keyof S>(...keys: K[]): TableDef<Name, Pick<S, K>> {
|
|
2002
|
+
const src = this.fields as unknown as Record<string, AnyField>;
|
|
2003
|
+
const f: Record<string, AnyField> = {};
|
|
2004
|
+
for (const k of keys) f[k as string] = src[k as string]!;
|
|
2005
|
+
return new TableDef(
|
|
2006
|
+
this.name,
|
|
2007
|
+
f as unknown as Fields<Pick<S, K>>,
|
|
2008
|
+
this.config,
|
|
2009
|
+
);
|
|
2010
|
+
}
|
|
2011
|
+
omit<K extends keyof S>(...keys: K[]): TableDef<Name, Omit<S, K>> {
|
|
2012
|
+
const f: Record<string, AnyField> = {
|
|
2013
|
+
...(this.fields as unknown as Record<string, AnyField>),
|
|
2014
|
+
};
|
|
2015
|
+
for (const k of keys) delete f[k as string];
|
|
2016
|
+
return new TableDef(
|
|
2017
|
+
this.name,
|
|
2018
|
+
f as unknown as Fields<Omit<S, K>>,
|
|
2019
|
+
this.config,
|
|
2020
|
+
);
|
|
2021
|
+
}
|
|
2022
|
+
partial(): TableDef<Name, PartialShape<S>> {
|
|
2023
|
+
const f: Record<string, AnyField> = {};
|
|
2024
|
+
for (const [k, field] of Object.entries(this.fields))
|
|
2025
|
+
f[k] = (field as AnyField).optional();
|
|
2026
|
+
return new TableDef(
|
|
2027
|
+
this.name,
|
|
2028
|
+
f as unknown as Fields<PartialShape<S>>,
|
|
2029
|
+
this.config,
|
|
2030
|
+
);
|
|
2031
|
+
}
|
|
2032
|
+
required(): TableDef<Name, RequiredShape<S>> {
|
|
2033
|
+
const f: Record<string, AnyField> = {};
|
|
2034
|
+
for (const [k, field] of Object.entries(this.fields)) {
|
|
2035
|
+
const sf = field as AnyField;
|
|
2036
|
+
const def = sf.schema._zod.def as unknown as {
|
|
2037
|
+
type: string;
|
|
2038
|
+
innerType?: z.ZodType;
|
|
2039
|
+
};
|
|
2040
|
+
f[k] =
|
|
2041
|
+
def.type === "optional" && def.innerType
|
|
2042
|
+
? new SField(def.innerType, sf.surreal)
|
|
2043
|
+
: sf;
|
|
2044
|
+
}
|
|
2045
|
+
return new TableDef(
|
|
2046
|
+
this.name,
|
|
2047
|
+
f as unknown as Fields<RequiredShape<S>>,
|
|
2048
|
+
this.config,
|
|
2049
|
+
);
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
/** Derive a `record<name>` link to this table (carrying its id value type). */
|
|
2053
|
+
record(): S extends { id: RecordIdField<Name, infer V> }
|
|
2054
|
+
? RecordIdField<Name, V>
|
|
2055
|
+
: RecordIdField<Name> {
|
|
2056
|
+
const idField = (this.fields as unknown as Record<string, AnyField>).id as
|
|
2057
|
+
| RecordIdField<Name>
|
|
2058
|
+
| undefined;
|
|
2059
|
+
return new RecordIdField([this.name], idField?.valueType) as never;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
/**
|
|
2064
|
+
* The server/system view of a table (`TableDef.system`): the same data methods typed
|
|
2065
|
+
* over ALL fields, including `$internal()` ones the public `TableDef` hides. Its
|
|
2066
|
+
* `.object` validates/encodes/decodes the full shape, and `encode`/`encodePartial` accept
|
|
2067
|
+
* internal fields. Exposed for trusted server code; never hand it to a browser client.
|
|
2068
|
+
*/
|
|
2069
|
+
// biome-ignore lint/correctness/noUnusedVariables: Name mirrors TableDef<Name, S> for symmetry (type-only)
|
|
2070
|
+
export class SystemView<Name extends string, S extends Shape> {
|
|
2071
|
+
/** Zod object over ALL fields (internal included). */
|
|
2072
|
+
readonly object: z.ZodObject<ZShapeAll<S>>;
|
|
2073
|
+
|
|
2074
|
+
constructor(readonly fields: Record<string, AnyField>) {
|
|
2075
|
+
const zshape: Record<string, z.ZodType> = {};
|
|
2076
|
+
for (const [k, f] of Object.entries(fields)) zshape[k] = f.schema;
|
|
2077
|
+
this.object = z.object(zshape) as z.ZodObject<ZShapeAll<S>>;
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
/** DB wire row -> app object (internal fields kept). */
|
|
2081
|
+
decode(row: unknown): z.output<z.ZodObject<ZShapeAll<S>>> {
|
|
2082
|
+
return z.decode(this.object, row as never);
|
|
2083
|
+
}
|
|
2084
|
+
/** DB wire row -> app object (async; internal fields kept). */
|
|
2085
|
+
decodeAsync(row: unknown): Promise<z.output<z.ZodObject<ZShapeAll<S>>>> {
|
|
2086
|
+
return z.decodeAsync(this.object, row as never);
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
// No-throw read variants.
|
|
2090
|
+
safeDecode(row: unknown) {
|
|
2091
|
+
return z.safeDecode(this.object, row as never);
|
|
2092
|
+
}
|
|
2093
|
+
safeDecodeAsync(row: unknown) {
|
|
2094
|
+
return z.safeDecodeAsync(this.object, row as never);
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
// Deprecated Zod-style aliases (parse runs the decode direction; use `decode`).
|
|
2098
|
+
/** @deprecated `parse` decodes a DB row (wire -> app). Use {@link SystemView.decode | decode}. */
|
|
2099
|
+
parse(row: unknown): z.output<z.ZodObject<ZShapeAll<S>>> {
|
|
2100
|
+
return this.decode(row);
|
|
2101
|
+
}
|
|
2102
|
+
/** @deprecated Use {@link SystemView.safeDecode | safeDecode}. */
|
|
2103
|
+
safeParse(row: unknown) {
|
|
2104
|
+
return this.safeDecode(row);
|
|
2105
|
+
}
|
|
2106
|
+
/** @deprecated Use {@link SystemView.decodeAsync | decodeAsync}. */
|
|
2107
|
+
parseAsync(row: unknown): Promise<z.output<z.ZodObject<ZShapeAll<S>>>> {
|
|
2108
|
+
return this.decodeAsync(row);
|
|
2109
|
+
}
|
|
2110
|
+
/** @deprecated Use {@link SystemView.safeDecodeAsync | safeDecodeAsync}. */
|
|
2111
|
+
safeParseAsync(row: unknown) {
|
|
2112
|
+
return this.safeDecodeAsync(row);
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
// --- Write side over ALL fields (internal included). Mirrors `TableDef`'s create/patch-shaped
|
|
2116
|
+
// `encode`/`encodePartial`; the raw full-object codec is `z.encode(view.object, app)`. ---
|
|
2117
|
+
|
|
2118
|
+
/**
|
|
2119
|
+
* Build a `CREATE` payload allowed to set internal fields. VALIDATES and THROWS the
|
|
2120
|
+
* aggregated `z.ZodError` on invalid input; use `safeEncode` for the non-throwing form.
|
|
2121
|
+
*/
|
|
2122
|
+
encode(input: CreateShapeAll<S>): MakeWireAll<S> {
|
|
2123
|
+
const r = this.safeEncode(input);
|
|
2124
|
+
if (!r.success) throw r.error;
|
|
2125
|
+
return r.data;
|
|
2126
|
+
}
|
|
2127
|
+
/**
|
|
2128
|
+
* Build an `UPDATE`/`MERGE` payload allowed to set internal fields. VALIDATES and THROWS
|
|
2129
|
+
* on invalid input; use `safeEncodePartial` for the non-throwing form.
|
|
2130
|
+
*/
|
|
2131
|
+
encodePartial(input: UpdateShapeAll<S>): MakeWireAll<S> {
|
|
2132
|
+
const r = this.safeEncodePartial(input);
|
|
2133
|
+
if (!r.success) throw r.error;
|
|
2134
|
+
return r.data;
|
|
2135
|
+
}
|
|
2136
|
+
/** Non-throwing `encode` over ALL fields (see `TableDef.safeEncode`). */
|
|
2137
|
+
safeEncode(input: CreateShapeAll<S>): SafeResult<MakeWireAll<S>> {
|
|
2138
|
+
return safeEncodeFields(
|
|
2139
|
+
this.fields,
|
|
2140
|
+
input as Record<string, unknown>,
|
|
2141
|
+
) as SafeResult<MakeWireAll<S>>;
|
|
2142
|
+
}
|
|
2143
|
+
/** Non-throwing `encodePartial` over ALL fields. */
|
|
2144
|
+
safeEncodePartial(input: UpdateShapeAll<S>): SafeResult<MakeWireAll<S>> {
|
|
2145
|
+
return safeEncodeFields(
|
|
2146
|
+
this.fields,
|
|
2147
|
+
input as Record<string, unknown>,
|
|
2148
|
+
) as SafeResult<MakeWireAll<S>>;
|
|
2149
|
+
}
|
|
2150
|
+
/** Async `encode` over ALL fields; throws the aggregated error. */
|
|
2151
|
+
async encodeAsync(input: CreateShapeAll<S>): Promise<MakeWireAll<S>> {
|
|
2152
|
+
const r = await this.safeEncodeAsync(input);
|
|
2153
|
+
if (!r.success) throw r.error;
|
|
2154
|
+
return r.data;
|
|
2155
|
+
}
|
|
2156
|
+
/** Async `encodePartial` over ALL fields; throws the aggregated error. */
|
|
2157
|
+
async encodePartialAsync(input: UpdateShapeAll<S>): Promise<MakeWireAll<S>> {
|
|
2158
|
+
const r = await this.safeEncodePartialAsync(input);
|
|
2159
|
+
if (!r.success) throw r.error;
|
|
2160
|
+
return r.data;
|
|
2161
|
+
}
|
|
2162
|
+
/** Non-throwing async `encode` over ALL fields. */
|
|
2163
|
+
safeEncodeAsync(
|
|
2164
|
+
input: CreateShapeAll<S>,
|
|
2165
|
+
): Promise<SafeResult<MakeWireAll<S>>> {
|
|
2166
|
+
return safeEncodeFieldsAsync(
|
|
2167
|
+
this.fields,
|
|
2168
|
+
input as Record<string, unknown>,
|
|
2169
|
+
) as Promise<SafeResult<MakeWireAll<S>>>;
|
|
2170
|
+
}
|
|
2171
|
+
/** Non-throwing async `encodePartial` over ALL fields. */
|
|
2172
|
+
safeEncodePartialAsync(
|
|
2173
|
+
input: UpdateShapeAll<S>,
|
|
2174
|
+
): Promise<SafeResult<MakeWireAll<S>>> {
|
|
2175
|
+
return safeEncodeFieldsAsync(
|
|
2176
|
+
this.fields,
|
|
2177
|
+
input as Record<string, unknown>,
|
|
2178
|
+
) as Promise<SafeResult<MakeWireAll<S>>>;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
// --- Smart id: the `id` field describes the id value type; wrapped as record<thisTable, V>. ---
|
|
2183
|
+
type IdValue<Id> =
|
|
2184
|
+
Id extends RecordIdField<string, infer V>
|
|
2185
|
+
? V
|
|
2186
|
+
: Id extends SField<infer Sc, infer _>
|
|
2187
|
+
? z.output<Sc> extends RecordIdValue
|
|
2188
|
+
? z.output<Sc>
|
|
2189
|
+
: RecordIdValue
|
|
2190
|
+
: Id extends z.ZodType
|
|
2191
|
+
? z.output<Id> extends RecordIdValue
|
|
2192
|
+
? z.output<Id>
|
|
2193
|
+
: RecordIdValue
|
|
2194
|
+
: RecordIdValue;
|
|
2195
|
+
type WithSmartId<Name extends string, S extends Shape> = Omit<S, "id"> & {
|
|
2196
|
+
id: RecordIdField<
|
|
2197
|
+
Name,
|
|
2198
|
+
"id" extends keyof S ? IdValue<S["id"]> : RecordIdValue
|
|
2199
|
+
>;
|
|
2200
|
+
};
|
|
2201
|
+
|
|
2202
|
+
/** Build a table's `id` field: a `record<name>` whose value type comes from `given`. */
|
|
2203
|
+
function buildIdField(
|
|
2204
|
+
name: string,
|
|
2205
|
+
given: AnyField | z.ZodType | undefined,
|
|
2206
|
+
): RecordIdField<string> {
|
|
2207
|
+
if (given === undefined) return new RecordIdField([name]);
|
|
2208
|
+
if (given instanceof RecordIdField)
|
|
2209
|
+
return new RecordIdField([name], given.valueType);
|
|
2210
|
+
const valueSchema = given instanceof SField ? given.schema : given;
|
|
2211
|
+
return new RecordIdField([name], valueSchema as z.ZodType<RecordIdValue>);
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
/** Normalize a shape, replacing/adding the special `id` field via buildIdField. */
|
|
2215
|
+
function applySmartId(name: string, shape: Shape): Record<string, AnyField> {
|
|
2216
|
+
const out: Record<string, AnyField> = {};
|
|
2217
|
+
for (const [k, v] of Object.entries(shape)) {
|
|
2218
|
+
if (k === "id") continue;
|
|
2219
|
+
out[k] = v instanceof SField ? v : new SField(v);
|
|
2220
|
+
}
|
|
2221
|
+
out.id = buildIdField(
|
|
2222
|
+
name,
|
|
2223
|
+
(shape as Record<string, AnyField | z.ZodType>).id,
|
|
2224
|
+
);
|
|
2225
|
+
return out;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
/**
|
|
2229
|
+
* Define a normal table (schemafull by default). The shape may be a plain object, or a callback
|
|
2230
|
+
* `(self) => ({...})` that receives a `record<thisTable>` field — use it for self-referential
|
|
2231
|
+
* links: `manager: self.optional()`. Type-safe with no repeated table name: `self`'s type comes
|
|
2232
|
+
* from the `name` arg, not from `typeof <theConst>`, so it sidesteps the self-in-its-own-
|
|
2233
|
+
* initializer cycle (TS 7022) that would otherwise widen the whole table to `any`.
|
|
2234
|
+
*/
|
|
2235
|
+
/**
|
|
2236
|
+
* Reject a shape field carrying the `NoDdl` brand (no SurrealQL mapping) at compile time: the
|
|
2237
|
+
* offending key resolves to an error string, so the shape literal won't type-check. Give such a
|
|
2238
|
+
* field `.$surreal(type, codec)` to make it storable, or drop it.
|
|
2239
|
+
*/
|
|
2240
|
+
type RejectNoDdl<S extends Shape> = {
|
|
2241
|
+
// `string extends FlagsOf` -> flags are unresolved (e.g. the callback form leaves `S` broad);
|
|
2242
|
+
// only reject when the brand is a concrete member, never on the generic fallback.
|
|
2243
|
+
[K in keyof S]: string extends FlagsOf<S[K]>
|
|
2244
|
+
? S[K]
|
|
2245
|
+
: NoDdl extends FlagsOf<S[K]>
|
|
2246
|
+
? "no SurrealQL mapping for this field — give it `.$surreal(type, codec)` or remove it"
|
|
2247
|
+
: S[K];
|
|
2248
|
+
};
|
|
2249
|
+
|
|
2250
|
+
// The output (id value) type of an authored `id` field — WITHOUT the widen-to-RecordIdValue fallback
|
|
2251
|
+
// `IdValue` does, so `RejectBadId` can see whether it's actually a valid record-id value type.
|
|
2252
|
+
type IdOutput<Id> =
|
|
2253
|
+
Id extends RecordIdField<string, infer V>
|
|
2254
|
+
? V
|
|
2255
|
+
: Id extends SField<infer Sc, infer _>
|
|
2256
|
+
? z.output<Sc>
|
|
2257
|
+
: Id extends z.ZodType
|
|
2258
|
+
? z.output<Id>
|
|
2259
|
+
: never;
|
|
2260
|
+
|
|
2261
|
+
/** Compile-time guard: an explicit `id` field must have a valid `RecordIdValue` value type — a
|
|
2262
|
+
* `s.symbol()`/`s.boolean()` id (not a valid id value) is rejected rather than silently widened. */
|
|
2263
|
+
type RejectBadId<S extends Shape> = "id" extends keyof S
|
|
2264
|
+
? [IdOutput<S["id"]>] extends [RecordIdValue]
|
|
2265
|
+
? unknown
|
|
2266
|
+
: {
|
|
2267
|
+
id: "the `id` field's value must be a valid RecordId value type (string | number | bigint | uuid | array | object) — e.g. s.string(), s.int(), s.uuid()";
|
|
2268
|
+
}
|
|
2269
|
+
: unknown;
|
|
2270
|
+
|
|
2271
|
+
export function defineTable<Name extends string, S extends Shape>(
|
|
2272
|
+
name: Name,
|
|
2273
|
+
// The object form is rejected at compile time (`RejectNoDdl` + `RejectBadId`); the callback form
|
|
2274
|
+
// keeps its precise `S` inference (a `& RejectNoDdl<S>` in a function-return position collapses it),
|
|
2275
|
+
// so a no-DDL field there is caught by the runtime `inferField` backstop instead.
|
|
2276
|
+
shape:
|
|
2277
|
+
| (S & RejectNoDdl<S> & RejectBadId<S>)
|
|
2278
|
+
| ((self: RecordIdField<Name>) => S),
|
|
2279
|
+
): TableDef<Name, WithSmartId<Name, S>> {
|
|
2280
|
+
const resolved =
|
|
2281
|
+
typeof shape === "function" ? shape(new RecordIdField([name])) : shape;
|
|
2282
|
+
return new TableDef(
|
|
2283
|
+
name,
|
|
2284
|
+
applySmartId(name, resolved) as unknown as Fields<WithSmartId<Name, S>>,
|
|
2285
|
+
{
|
|
2286
|
+
schemafull: true,
|
|
2287
|
+
},
|
|
2288
|
+
);
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
// biome-ignore lint/suspicious/noExplicitAny: shape-agnostic table reference for relation endpoints
|
|
2292
|
+
type AnyTable = TableDef<string, any>;
|
|
2293
|
+
type TableRef = AnyTable | readonly AnyTable[];
|
|
2294
|
+
type NamesOf<T> =
|
|
2295
|
+
T extends TableDef<infer N extends string, infer _>
|
|
2296
|
+
? N
|
|
2297
|
+
: T extends readonly (infer E)[]
|
|
2298
|
+
? E extends TableDef<infer N extends string, infer _>
|
|
2299
|
+
? N
|
|
2300
|
+
: never
|
|
2301
|
+
: never;
|
|
2302
|
+
|
|
2303
|
+
/** A relation's full shape: the edge fields plus the `in`/`out` record endpoints. */
|
|
2304
|
+
type RelationShape<
|
|
2305
|
+
Name extends string,
|
|
2306
|
+
S extends Shape,
|
|
2307
|
+
In extends string,
|
|
2308
|
+
Out extends string,
|
|
2309
|
+
> = Omit<WithSmartId<Name, S>, "in" | "out"> & {
|
|
2310
|
+
in: RecordIdField<In>;
|
|
2311
|
+
out: RecordIdField<Out>;
|
|
2312
|
+
};
|
|
2313
|
+
|
|
2314
|
+
function tableNames(ref: TableRef): string[] {
|
|
2315
|
+
return (Array.isArray(ref) ? ref : [ref as AnyTable]).map((t) => t.name);
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
/** Build a relation's runtime fields: the edge fields + `in`/`out` (empty endpoints = any record). */
|
|
2319
|
+
function relationFields(
|
|
2320
|
+
name: string,
|
|
2321
|
+
edge: Shape,
|
|
2322
|
+
fromNames: string[],
|
|
2323
|
+
toNames: string[],
|
|
2324
|
+
): Record<string, AnyField> {
|
|
2325
|
+
return {
|
|
2326
|
+
...applySmartId(name, edge),
|
|
2327
|
+
in: new RecordIdField(fromNames),
|
|
2328
|
+
out: new RecordIdField(toNames),
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
/**
|
|
2333
|
+
* A graph relation (edge table). It's a usable `TableDef` immediately — endpoints are OPTIONAL
|
|
2334
|
+
* (`TYPE RELATION` with no `FROM`/`TO` restricts nothing) — and `.from(X)` / `.to(Y)` narrow the
|
|
2335
|
+
* `in` / `out` record types. Both return a new `RelationDef` (immutable), chainable in any order.
|
|
2336
|
+
*/
|
|
2337
|
+
export class RelationDef<
|
|
2338
|
+
Name extends string,
|
|
2339
|
+
S extends Shape,
|
|
2340
|
+
In extends string = string,
|
|
2341
|
+
Out extends string = string,
|
|
2342
|
+
> extends TableDef<Name, RelationShape<Name, S, In, Out>> {
|
|
2343
|
+
constructor(
|
|
2344
|
+
name: Name,
|
|
2345
|
+
private readonly edge: S,
|
|
2346
|
+
private readonly fromNames: string[] = [],
|
|
2347
|
+
private readonly toNames: string[] = [],
|
|
2348
|
+
private readonly isEnforced: boolean = false,
|
|
2349
|
+
) {
|
|
2350
|
+
super(
|
|
2351
|
+
name,
|
|
2352
|
+
relationFields(name, edge, fromNames, toNames) as unknown as Fields<
|
|
2353
|
+
RelationShape<Name, S, In, Out>
|
|
2354
|
+
>,
|
|
2355
|
+
{
|
|
2356
|
+
schemafull: true,
|
|
2357
|
+
relation: {
|
|
2358
|
+
from: fromNames,
|
|
2359
|
+
to: toNames,
|
|
2360
|
+
...(isEnforced ? { enforced: true } : {}),
|
|
2361
|
+
},
|
|
2362
|
+
},
|
|
2363
|
+
);
|
|
2364
|
+
}
|
|
2365
|
+
/** Restrict the source endpoint(s) (`in`). */
|
|
2366
|
+
from<F extends TableRef>(ref: F): RelationDef<Name, S, NamesOf<F>, Out> {
|
|
2367
|
+
return new RelationDef(
|
|
2368
|
+
this.name,
|
|
2369
|
+
this.edge,
|
|
2370
|
+
tableNames(ref),
|
|
2371
|
+
this.toNames,
|
|
2372
|
+
this.isEnforced,
|
|
2373
|
+
) as unknown as RelationDef<Name, S, NamesOf<F>, Out>;
|
|
2374
|
+
}
|
|
2375
|
+
/** Restrict the target endpoint(s) (`out`). */
|
|
2376
|
+
to<T extends TableRef>(ref: T): RelationDef<Name, S, In, NamesOf<T>> {
|
|
2377
|
+
return new RelationDef(
|
|
2378
|
+
this.name,
|
|
2379
|
+
this.edge,
|
|
2380
|
+
this.fromNames,
|
|
2381
|
+
tableNames(ref),
|
|
2382
|
+
this.isEnforced,
|
|
2383
|
+
) as unknown as RelationDef<Name, S, In, NamesOf<T>>;
|
|
2384
|
+
}
|
|
2385
|
+
/** Require both endpoints to exist on RELATE (`TYPE RELATION … ENFORCED`). */
|
|
2386
|
+
enforced(): RelationDef<Name, S, In, Out> {
|
|
2387
|
+
return new RelationDef(
|
|
2388
|
+
this.name,
|
|
2389
|
+
this.edge,
|
|
2390
|
+
this.fromNames,
|
|
2391
|
+
this.toNames,
|
|
2392
|
+
true,
|
|
2393
|
+
) as unknown as RelationDef<Name, S, In, Out>;
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
/**
|
|
2398
|
+
* Define a graph relation (edge table). Endpoints are optional — the result is a usable table
|
|
2399
|
+
* right away; chain `.from(X).to(Y)` to restrict the `in`/`out` records.
|
|
2400
|
+
*/
|
|
2401
|
+
export function defineRelation<Name extends string, S extends Shape = {}>(
|
|
2402
|
+
name: Name,
|
|
2403
|
+
fields?: S & RejectNoDdl<S>,
|
|
2404
|
+
): RelationDef<Name, S> {
|
|
2405
|
+
return new RelationDef(name, (fields ?? {}) as S);
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
/**
|
|
2409
|
+
* Define a pre-computed (materialized) VIEW table — `DEFINE TABLE <name> TYPE ANY SCHEMALESS AS
|
|
2410
|
+
* <query>`. Its rows are computed from the SELECT (SurrealDB keeps them in sync as the source tables
|
|
2411
|
+
* change), so a view has NO authored fields/id. Chain `.permissions()` / `.comment()` / `.changefeed()`
|
|
2412
|
+
* as on any table:
|
|
2413
|
+
*
|
|
2414
|
+
* ```ts
|
|
2415
|
+
* export const Adults = defineView("adults", surql`SELECT name, age FROM person WHERE age >= 18`);
|
|
2416
|
+
* ```
|
|
2417
|
+
*/
|
|
2418
|
+
export function defineView<Name extends string>(
|
|
2419
|
+
name: Name,
|
|
2420
|
+
// biome-ignore lint/complexity/noBannedTypes: an empty shape — a view has no authored fields.
|
|
2421
|
+
query: Expr,
|
|
2422
|
+
): TableDef<Name, {}> {
|
|
2423
|
+
// biome-ignore lint/complexity/noBannedTypes: see above.
|
|
2424
|
+
return new TableDef<Name, {}>(name, {} as Fields<{}>, {
|
|
2425
|
+
schemafull: false,
|
|
2426
|
+
type: "any",
|
|
2427
|
+
view: query,
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
/**
|
|
2432
|
+
* A standalone `DEFINE EVENT`, declared apart from its table (vs the inline `TableDef.event(…)`).
|
|
2433
|
+
* Export one per event when you want each event as its own named symbol. It compiles to the same
|
|
2434
|
+
* statement as the inline form — `pull` regenerates events inline, so the two are interchangeable.
|
|
2435
|
+
*/
|
|
2436
|
+
export class EventDef {
|
|
2437
|
+
readonly kind = "event" as const;
|
|
2438
|
+
constructor(
|
|
2439
|
+
/** Owning table name. */
|
|
2440
|
+
readonly table: string,
|
|
2441
|
+
readonly name: string,
|
|
2442
|
+
readonly when: Expr | undefined,
|
|
2443
|
+
readonly then: Expr | Expr[],
|
|
2444
|
+
) {}
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
/**
|
|
2448
|
+
* Declare a row-change event on `table` as a standalone, exportable object:
|
|
2449
|
+
* `export const reverify = defineEvent(User, "reverify", { when, then })`. Pass the `TableDef`
|
|
2450
|
+
* (preferred — no name repetition) or a table name string. See {@link TableDef.event} for the
|
|
2451
|
+
* inline, chainable form.
|
|
2452
|
+
*/
|
|
2453
|
+
export function defineEvent(
|
|
2454
|
+
table: TableDef<string, Shape> | string,
|
|
2455
|
+
name: string,
|
|
2456
|
+
spec: { when?: Expr; then: Expr | Expr[] },
|
|
2457
|
+
): EventDef {
|
|
2458
|
+
const tableName = typeof table === "string" ? table : table.name;
|
|
2459
|
+
return new EventDef(tableName, name, spec.when, spec.then);
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
interface FunctionConfig {
|
|
2463
|
+
/** Return type (an s schema, inferred to a SurrealQL type — like a field). */
|
|
2464
|
+
returns?: AnyField;
|
|
2465
|
+
/** Function body: a `surql\`…\`` block (or raw string). Required to emit. */
|
|
2466
|
+
body?: Expr;
|
|
2467
|
+
/** `PERMISSIONS FULL` (true) / `NONE` (false) / a `surql` condition. */
|
|
2468
|
+
permissions?: boolean | Expr;
|
|
2469
|
+
comment?: string;
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
/**
|
|
2473
|
+
* A custom function — `DEFINE FUNCTION fn::<name>(<args>) [-> <returns>] { <body> }`. Built with a
|
|
2474
|
+
* chainable, immutable API (like {@link TableDef}): `defineFunction(name, args).returns(…).body(…)`.
|
|
2475
|
+
* Args and the return type are s schemas (inferred to SurrealQL types, same as table fields).
|
|
2476
|
+
*/
|
|
2477
|
+
export class FunctionDef {
|
|
2478
|
+
readonly kind = "function" as const;
|
|
2479
|
+
constructor(
|
|
2480
|
+
readonly name: string,
|
|
2481
|
+
/** Ordered named args, each an s schema. */
|
|
2482
|
+
readonly args: Record<string, AnyField>,
|
|
2483
|
+
readonly config: FunctionConfig = {},
|
|
2484
|
+
) {}
|
|
2485
|
+
private withConfig(c: Partial<FunctionConfig>): FunctionDef {
|
|
2486
|
+
return new FunctionDef(this.name, this.args, { ...this.config, ...c });
|
|
2487
|
+
}
|
|
2488
|
+
/** Declare the return type (an s schema). */
|
|
2489
|
+
returns(type: AnyField): FunctionDef {
|
|
2490
|
+
return this.withConfig({ returns: type });
|
|
2491
|
+
}
|
|
2492
|
+
/** The function body — a `surql\`…\`` block (braces optional) or a raw string. */
|
|
2493
|
+
body(body: Expr): FunctionDef {
|
|
2494
|
+
return this.withConfig({ body });
|
|
2495
|
+
}
|
|
2496
|
+
/** `PERMISSIONS`: `FULL` (true, the default), `NONE` (false), or a `surql` condition. */
|
|
2497
|
+
permissions(p: boolean | Expr): FunctionDef {
|
|
2498
|
+
return this.withConfig({ permissions: p });
|
|
2499
|
+
}
|
|
2500
|
+
comment(comment: string): FunctionDef {
|
|
2501
|
+
return this.withConfig({ comment });
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
/**
|
|
2506
|
+
* Declare a custom function as a standalone, exportable object:
|
|
2507
|
+
* `export const greet = defineFunction("greet", { name: s.string() }).returns(s.string()).body(surql\`…\`)`.
|
|
2508
|
+
* Emitted as `DEFINE FUNCTION fn::greet(...)`. Args are s schemas (inferred to SurrealQL types).
|
|
2509
|
+
*/
|
|
2510
|
+
export function defineFunction(name: string, args: Shape = {}): FunctionDef {
|
|
2511
|
+
return new FunctionDef(
|
|
2512
|
+
name,
|
|
2513
|
+
normalizeFields(args) as unknown as Record<string, AnyField>,
|
|
2514
|
+
);
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
/** The access type + its type-specific config. `RECORD` (default) / `JWT` / `BEARER`. */
|
|
2518
|
+
export type AccessKind =
|
|
2519
|
+
| { type: "record" }
|
|
2520
|
+
| { type: "jwt"; alg?: string; key?: string; url?: string }
|
|
2521
|
+
| { type: "bearer"; subject: "record" | "user" };
|
|
2522
|
+
|
|
2523
|
+
/** Token/session/grant lifetimes, e.g. `{ token: "1h", session: "12h", grant: "30d" }`. */
|
|
2524
|
+
export interface AccessDuration {
|
|
2525
|
+
grant?: string;
|
|
2526
|
+
token?: string;
|
|
2527
|
+
session?: string;
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
interface AccessConfig {
|
|
2531
|
+
/** `ON DATABASE` (default) or `ON NAMESPACE`. */
|
|
2532
|
+
on: "database" | "namespace";
|
|
2533
|
+
kind: AccessKind;
|
|
2534
|
+
/** RECORD-only: SIGNUP/SIGNIN/AUTHENTICATE blocks. */
|
|
2535
|
+
signup?: Expr;
|
|
2536
|
+
signin?: Expr;
|
|
2537
|
+
authenticate?: Expr;
|
|
2538
|
+
duration?: AccessDuration;
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
/**
|
|
2542
|
+
* An access definition — `DEFINE ACCESS <name> ON DATABASE TYPE …`. Chainable like {@link TableDef}.
|
|
2543
|
+
* Pick a type with `.record()` (default; SIGNUP/SIGNIN), `.jwt({ alg, key } | { url })` (validate
|
|
2544
|
+
* external tokens), or `.bearer({ for })` (API-key grants). The RECORD bodies are `surql\`…\`` blocks
|
|
2545
|
+
* (braces optional). NOTE: SurrealDB redacts signing keys in introspection, so `pull` can't recover
|
|
2546
|
+
* them — see the CLI (`--access` is opt-in for that reason).
|
|
2547
|
+
*/
|
|
2548
|
+
export class AccessDef {
|
|
2549
|
+
readonly kind = "access" as const;
|
|
2550
|
+
constructor(
|
|
2551
|
+
readonly name: string,
|
|
2552
|
+
readonly config: AccessConfig = {
|
|
2553
|
+
on: "database",
|
|
2554
|
+
kind: { type: "record" },
|
|
2555
|
+
},
|
|
2556
|
+
) {}
|
|
2557
|
+
private withConfig(c: Partial<AccessConfig>): AccessDef {
|
|
2558
|
+
return new AccessDef(this.name, { ...this.config, ...c });
|
|
2559
|
+
}
|
|
2560
|
+
/** `TYPE RECORD` (the default) — end users sign up / sign in directly. */
|
|
2561
|
+
record(): AccessDef {
|
|
2562
|
+
return this.withConfig({ kind: { type: "record" } });
|
|
2563
|
+
}
|
|
2564
|
+
/** `TYPE JWT` — validate tokens from an external issuer: `{ alg, key }` (symmetric/PEM) or `{ url }` (JWKS). */
|
|
2565
|
+
jwt(opts: { alg?: string; key?: string; url?: string }): AccessDef {
|
|
2566
|
+
return this.withConfig({ kind: { type: "jwt", ...opts } });
|
|
2567
|
+
}
|
|
2568
|
+
/** `TYPE BEARER FOR USER|RECORD` — bearer-token / API-key grants. */
|
|
2569
|
+
bearer(opts: { for: "record" | "user" }): AccessDef {
|
|
2570
|
+
return this.withConfig({ kind: { type: "bearer", subject: opts.for } });
|
|
2571
|
+
}
|
|
2572
|
+
onNamespace(): AccessDef {
|
|
2573
|
+
return this.withConfig({ on: "namespace" });
|
|
2574
|
+
}
|
|
2575
|
+
onDatabase(): AccessDef {
|
|
2576
|
+
return this.withConfig({ on: "database" });
|
|
2577
|
+
}
|
|
2578
|
+
/** `SIGNUP { … }` (RECORD) — a `surql\`…\`` block (braces optional) run on sign-up. */
|
|
2579
|
+
signup(body: Expr): AccessDef {
|
|
2580
|
+
return this.withConfig({ signup: body });
|
|
2581
|
+
}
|
|
2582
|
+
/** `SIGNIN { … }` (RECORD) — a `surql\`…\`` block run on sign-in. */
|
|
2583
|
+
signin(body: Expr): AccessDef {
|
|
2584
|
+
return this.withConfig({ signin: body });
|
|
2585
|
+
}
|
|
2586
|
+
/** `AUTHENTICATE { … }` — a `surql\`…\`` block run on each authenticated request. */
|
|
2587
|
+
authenticate(body: Expr): AccessDef {
|
|
2588
|
+
return this.withConfig({ authenticate: body });
|
|
2589
|
+
}
|
|
2590
|
+
/** Token/session/grant lifetimes (`DURATION FOR TOKEN …, FOR SESSION …, FOR GRANT …`). */
|
|
2591
|
+
duration(d: AccessDuration): AccessDef {
|
|
2592
|
+
return this.withConfig({ duration: d });
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
/**
|
|
2597
|
+
* Declare an access definition: `export const account = defineAccess("account").record()
|
|
2598
|
+
* .signup(surql\`…\`).signin(surql\`…\`).duration({ token: "1h", session: "12h" })`. See {@link AccessDef}
|
|
2599
|
+
* for `.jwt(…)` / `.bearer(…)`.
|
|
2600
|
+
*/
|
|
2601
|
+
export function defineAccess(name: string): AccessDef {
|
|
2602
|
+
return new AccessDef(name);
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
/** A text-search `DEFINE ANALYZER`'s config: an ordered tokenizer + filter pipeline. */
|
|
2606
|
+
export interface AnalyzerConfig {
|
|
2607
|
+
/** `TOKENIZERS …` — e.g. `["blank", "class", "camel", "punct"]` (at least one). */
|
|
2608
|
+
tokenizers: string[];
|
|
2609
|
+
/** `FILTERS …` — e.g. `["lowercase", "ascii", "snowball(english)", "ngram(1,3)"]`. */
|
|
2610
|
+
filters?: string[];
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
/**
|
|
2614
|
+
* A text-search analyzer (`DEFINE ANALYZER`), referenced by a `FULLTEXT` index. Declared standalone:
|
|
2615
|
+
* `export const english = defineAnalyzer("english", { tokenizers: ["blank"], filters: ["lowercase", "snowball(english)"] })`.
|
|
2616
|
+
*/
|
|
2617
|
+
export class AnalyzerDef {
|
|
2618
|
+
readonly kind = "analyzer" as const;
|
|
2619
|
+
constructor(
|
|
2620
|
+
readonly name: string,
|
|
2621
|
+
readonly config: AnalyzerConfig,
|
|
2622
|
+
) {}
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
/** Declare a text-search analyzer. See {@link AnalyzerConfig}; reference it from a `fulltext` index. */
|
|
2626
|
+
export function defineAnalyzer(
|
|
2627
|
+
name: string,
|
|
2628
|
+
config: AnalyzerConfig,
|
|
2629
|
+
): AnalyzerDef {
|
|
2630
|
+
return new AnalyzerDef(name, config);
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
/** A schema object declared apart from a table (collected by the CLI loader and emitted on its own). */
|
|
2634
|
+
export type StandaloneDef = EventDef | FunctionDef | AccessDef | AnalyzerDef;
|
|
2635
|
+
|
|
2636
|
+
/**
|
|
2637
|
+
* The underlying Zod schema of any s value: a field (`SField`), a table/relation def
|
|
2638
|
+
* (anything carrying an `.object`), or a raw Zod type.
|
|
2639
|
+
*/
|
|
2640
|
+
type ZodOf<T> = T extends { object: infer O }
|
|
2641
|
+
? O extends z.ZodType
|
|
2642
|
+
? O
|
|
2643
|
+
: never
|
|
2644
|
+
: SchemaOf<T>;
|
|
2645
|
+
|
|
2646
|
+
/** The app-facing type (what your code reads). Same as `s.output` / Zod's `infer`. */
|
|
2647
|
+
export type App<T> = z.output<ZodOf<T>>;
|
|
2648
|
+
/** The DB wire type (what crosses the wire). Same as `s.input`. */
|
|
2649
|
+
export type Wire<T> = z.input<ZodOf<T>>;
|
|
2650
|
+
|
|
2651
|
+
/**
|
|
2652
|
+
* Zod-style inference helpers, exposed on `s` (a type-only namespace merged with the `s`
|
|
2653
|
+
* value — the same trick Zod uses for `z.infer`). They accept fields, table/relation defs,
|
|
2654
|
+
* and raw schemas alike:
|
|
2655
|
+
* - `s.infer<T>` / `s.output<T>` / `s.TypeOf<T>` -> the decoded **app** type (== `App<T>`)
|
|
2656
|
+
* - `s.input<T>` -> the **wire/DB** type (== `Wire<T>`)
|
|
2657
|
+
*/
|
|
2658
|
+
export namespace s {
|
|
2659
|
+
export type infer<T> = z.output<ZodOf<T>>;
|
|
2660
|
+
export type output<T> = z.output<ZodOf<T>>;
|
|
2661
|
+
export type input<T> = z.input<ZodOf<T>>;
|
|
2662
|
+
export type TypeOf<T> = z.output<ZodOf<T>>;
|
|
2663
|
+
/** Any s field — the `z.ZodTypeAny` analogue, for typing generic schemas. */
|
|
2664
|
+
export type Field = AnyField;
|
|
2665
|
+
}
|
|
2666
|
+
/** The typed input for creating a record (DB-filled fields optional). */
|
|
2667
|
+
export type Create<T> =
|
|
2668
|
+
T extends TableDef<string, infer S> ? CreateShape<S> : never;
|
|
2669
|
+
/** The typed input for updating a record (partial; excludes id and readonly fields). */
|
|
2670
|
+
export type Update<T> =
|
|
2671
|
+
T extends TableDef<string, infer S> ? UpdateShape<S> : never;
|