@sequenceholdings/orm 2.1.0 → 2.2.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/README.md +40 -1
- package/dist/authoring.cjs +1619 -121
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +252 -8
- package/dist/authoring.d.ts +252 -8
- package/dist/authoring.js +1607 -123
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +713 -41
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +1520 -51
- package/dist/canonical.d.ts +1520 -51
- package/dist/canonical.js +693 -42
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +7103 -3901
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +7104 -3902
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs +122 -40
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +85 -25
- package/dist/client.d.ts +85 -25
- package/dist/client.js +119 -41
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +2464 -450
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +79 -4
- package/dist/compiler.d.ts +79 -4
- package/dist/compiler.js +2457 -451
- package/dist/compiler.js.map +1 -1
- package/dist/constants-C6EG6Wq7.d.cts +99 -0
- package/dist/constants-C6EG6Wq7.d.ts +99 -0
- package/dist/definition-DjfC7Lll.d.cts +91 -0
- package/dist/definition-DjfC7Lll.d.ts +91 -0
- package/dist/dynamic-input.cjs +2 -1
- package/dist/dynamic-input.cjs.map +1 -1
- package/dist/dynamic-input.d.cts +9 -1
- package/dist/dynamic-input.d.ts +9 -1
- package/dist/dynamic-input.js +2 -1
- package/dist/dynamic-input.js.map +1 -1
- package/dist/index.cjs +2656 -418
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +693 -116
- package/dist/index.d.ts +693 -116
- package/dist/index.js +2622 -420
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +811 -48
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +670 -27
- package/dist/ir.d.ts +670 -27
- package/dist/ir.js +802 -49
- package/dist/ir.js.map +1 -1
- package/dist/{naming-DCn94LJ4.d.ts → naming-CPfx7EfF.d.ts} +14 -1
- package/dist/{naming-DPglwzkB.d.cts → naming-CU9ThP_E.d.cts} +14 -1
- package/dist/policy-1iZbGnZZ.d.cts +24 -0
- package/dist/policy-1iZbGnZZ.d.ts +24 -0
- package/dist/predicate-v2.cjs +804 -51
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.d.cts +2 -1
- package/dist/predicate-v2.d.ts +2 -1
- package/dist/predicate-v2.js +804 -51
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +1014 -75
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.d.cts +14 -3
- package/dist/sdl.d.ts +14 -3
- package/dist/sdl.js +1012 -76
- package/dist/sdl.js.map +1 -1
- package/package.json +13 -7
- package/src/codegen-gql/server-fns.cjs +2 -2
- package/templates/orm-namespace/AGENTS.md +10 -0
- package/templates/orm-namespace/CLAUDE.md +1 -82
- package/templates/orm-namespace/codegen.preset.d.cts +7 -0
- package/templates/orm-namespace/codegen.ts +2 -1
- package/templates/orm-namespace/graphql/example/queries.ts +4 -3
- package/templates/orm-namespace/package.json +2 -1
- package/templates/orm-namespace/schema/tables.ts +3 -2
- package/templates/orm-namespace/sequence.config.ts +7 -11
package/dist/ir.cjs
CHANGED
|
@@ -3,14 +3,480 @@
|
|
|
3
3
|
var zod = require('zod');
|
|
4
4
|
|
|
5
5
|
// src/ir/index.ts
|
|
6
|
+
var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
7
|
+
var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
8
|
+
|
|
9
|
+
// src/checks/compile.ts
|
|
10
|
+
function tokenize(source) {
|
|
11
|
+
const tokens = [];
|
|
12
|
+
for (let position = 0; position < source.length; ) {
|
|
13
|
+
const char = source[position];
|
|
14
|
+
if (/\s/.test(char)) {
|
|
15
|
+
position++;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (char === "(" || char === ")" || char === ",") {
|
|
19
|
+
tokens.push({ kind: char === "(" ? "lparen" : char === ")" ? "rparen" : "comma", value: char, position });
|
|
20
|
+
position++;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (char === "'") {
|
|
24
|
+
const start = position++;
|
|
25
|
+
let value = "";
|
|
26
|
+
let closed = false;
|
|
27
|
+
while (position < source.length) {
|
|
28
|
+
if (source[position] !== "'") {
|
|
29
|
+
value += source[position++];
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (source[position + 1] === "'") {
|
|
33
|
+
value += "'";
|
|
34
|
+
position += 2;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
position++;
|
|
38
|
+
closed = true;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
if (!closed) throw new Error(`unterminated string at position ${start}`);
|
|
42
|
+
if (/[\x00-\x1f\x7f]/.test(value)) throw new Error(`control character in string at position ${start}`);
|
|
43
|
+
tokens.push({ kind: "string", value, position: start });
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const operator = /^(<=|>=|<>|!=|!~|=|<|>|~)/.exec(source.slice(position));
|
|
47
|
+
if (operator) {
|
|
48
|
+
tokens.push({ kind: "operator", value: operator[0] === "<>" ? "!=" : operator[0], position });
|
|
49
|
+
position += operator[0].length;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const number = /^-?\d+(?:\.\d+)?/.exec(source.slice(position));
|
|
53
|
+
if (number) {
|
|
54
|
+
tokens.push({ kind: "number", value: number[0], position });
|
|
55
|
+
position += number[0].length;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const identifier3 = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(source.slice(position));
|
|
59
|
+
if (identifier3) {
|
|
60
|
+
tokens.push({ kind: "identifier", value: identifier3[0], position });
|
|
61
|
+
position += identifier3[0].length;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`unexpected character '${char}' at position ${position}`);
|
|
65
|
+
}
|
|
66
|
+
tokens.push({ kind: "end", value: "", position: source.length });
|
|
67
|
+
if (tokens.length > 256) throw new Error("check expression exceeds 256 tokens");
|
|
68
|
+
return tokens;
|
|
69
|
+
}
|
|
70
|
+
var Parser = class {
|
|
71
|
+
constructor(tokens, columns) {
|
|
72
|
+
this.tokens = tokens;
|
|
73
|
+
this.columns = /* @__PURE__ */ new Map();
|
|
74
|
+
for (const column of columns) {
|
|
75
|
+
for (const alias of [column.name, column.columnName]) {
|
|
76
|
+
const previous = this.columns.get(alias);
|
|
77
|
+
if (previous && previous.columnName !== column.columnName) {
|
|
78
|
+
throw new Error(`ambiguous check column '${alias}'`);
|
|
79
|
+
}
|
|
80
|
+
this.columns.set(alias, column);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
index = 0;
|
|
85
|
+
depth = 0;
|
|
86
|
+
columns;
|
|
87
|
+
parse() {
|
|
88
|
+
const expression = this.parseOr();
|
|
89
|
+
this.expect("end", "end of expression");
|
|
90
|
+
return expression;
|
|
91
|
+
}
|
|
92
|
+
parseOr() {
|
|
93
|
+
let left = this.parseAnd();
|
|
94
|
+
while (this.matchKeyword("or")) left = `(${left} OR ${this.parseAnd()})`;
|
|
95
|
+
return left;
|
|
96
|
+
}
|
|
97
|
+
parseAnd() {
|
|
98
|
+
let left = this.parseNot();
|
|
99
|
+
while (this.matchKeyword("and")) left = `(${left} AND ${this.parseNot()})`;
|
|
100
|
+
return left;
|
|
101
|
+
}
|
|
102
|
+
parseNot() {
|
|
103
|
+
if (this.matchKeyword("not")) return `(NOT ${this.parseNot()})`;
|
|
104
|
+
return this.parsePrimary();
|
|
105
|
+
}
|
|
106
|
+
parsePrimary() {
|
|
107
|
+
if (this.peek().kind === "lparen") {
|
|
108
|
+
if (++this.depth > 32) throw new Error("check expression nesting exceeds 32 levels");
|
|
109
|
+
this.advance();
|
|
110
|
+
const expression = this.parseOr();
|
|
111
|
+
this.expect("rparen", "')'");
|
|
112
|
+
this.depth--;
|
|
113
|
+
return `(${expression})`;
|
|
114
|
+
}
|
|
115
|
+
return this.parseComparison();
|
|
116
|
+
}
|
|
117
|
+
parseComparison() {
|
|
118
|
+
const left = this.parseValue();
|
|
119
|
+
if (this.matchKeyword("is")) {
|
|
120
|
+
const negate = this.matchKeyword("not");
|
|
121
|
+
this.expectKeyword("null");
|
|
122
|
+
return `${left.sql} IS ${negate ? "NOT " : ""}NULL`;
|
|
123
|
+
}
|
|
124
|
+
if (this.matchKeyword("between")) {
|
|
125
|
+
const lower = this.parseValue();
|
|
126
|
+
this.expectKeyword("and");
|
|
127
|
+
const upper = this.parseValue();
|
|
128
|
+
return `${left.sql} BETWEEN ${lower.sql} AND ${upper.sql}`;
|
|
129
|
+
}
|
|
130
|
+
if (this.matchKeyword("in")) {
|
|
131
|
+
this.expect("lparen", "'(' after IN");
|
|
132
|
+
const values = [this.parseValue()];
|
|
133
|
+
while (this.peek().kind === "comma") {
|
|
134
|
+
this.advance();
|
|
135
|
+
values.push(this.parseValue());
|
|
136
|
+
}
|
|
137
|
+
this.expect("rparen", "')' after IN values");
|
|
138
|
+
if (values.some((value) => !value.literal)) throw new Error("IN values must be literals");
|
|
139
|
+
return `${left.sql} IN (${values.map((value) => value.sql).join(", ")})`;
|
|
140
|
+
}
|
|
141
|
+
const operator = this.expect("operator", "a comparison operator");
|
|
142
|
+
const right = this.parseValue();
|
|
143
|
+
if (operator.value === "~" || operator.value === "!~") {
|
|
144
|
+
if (left.column && left.column.type !== "text") throw new Error("regex checks require a text column");
|
|
145
|
+
if (!right.literal) throw new Error("regex pattern must be a literal");
|
|
146
|
+
}
|
|
147
|
+
return `${left.sql} ${operator.value} ${right.sql}`;
|
|
148
|
+
}
|
|
149
|
+
parseValue() {
|
|
150
|
+
const token = this.advance();
|
|
151
|
+
if (token.kind === "string") return { sql: quoteLiteral(token.value), literal: true };
|
|
152
|
+
if (token.kind === "number") {
|
|
153
|
+
const number = Number(token.value);
|
|
154
|
+
if (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number)) {
|
|
155
|
+
throw new Error(`unsafe numeric literal at position ${token.position}`);
|
|
156
|
+
}
|
|
157
|
+
return { sql: token.value, literal: true };
|
|
158
|
+
}
|
|
159
|
+
if (token.kind !== "identifier") throw new Error(`expected a column or literal at position ${token.position}`);
|
|
160
|
+
const keyword = token.value.toLowerCase();
|
|
161
|
+
if (keyword === "true" || keyword === "false" || keyword === "null") return { sql: keyword.toUpperCase(), literal: true };
|
|
162
|
+
if (keyword === "btrim") {
|
|
163
|
+
this.expect("lparen", "'(' after btrim");
|
|
164
|
+
const column2 = this.parseColumn();
|
|
165
|
+
this.expect("rparen", "')' after btrim column");
|
|
166
|
+
if (column2.type !== "text") throw new Error("btrim requires a text column");
|
|
167
|
+
return { sql: `btrim(${quoteIdentifier(column2.columnName)})`, column: column2 };
|
|
168
|
+
}
|
|
169
|
+
const column = this.columns.get(token.value);
|
|
170
|
+
if (!column) throw new Error(`unknown check column '${token.value}'`);
|
|
171
|
+
return { sql: quoteIdentifier(column.columnName), column };
|
|
172
|
+
}
|
|
173
|
+
parseColumn() {
|
|
174
|
+
const token = this.expect("identifier", "a column name");
|
|
175
|
+
const column = this.columns.get(token.value);
|
|
176
|
+
if (!column) throw new Error(`unknown check column '${token.value}'`);
|
|
177
|
+
return column;
|
|
178
|
+
}
|
|
179
|
+
peek() {
|
|
180
|
+
return this.tokens[this.index];
|
|
181
|
+
}
|
|
182
|
+
advance() {
|
|
183
|
+
const token = this.peek();
|
|
184
|
+
if (token.kind !== "end") this.index++;
|
|
185
|
+
return token;
|
|
186
|
+
}
|
|
187
|
+
matchKeyword(keyword) {
|
|
188
|
+
if (this.peek().kind !== "identifier" || this.peek().value.toLowerCase() !== keyword) return false;
|
|
189
|
+
this.advance();
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
expectKeyword(keyword) {
|
|
193
|
+
if (!this.matchKeyword(keyword)) throw new Error(`expected '${keyword}' at position ${this.peek().position}`);
|
|
194
|
+
}
|
|
195
|
+
expect(kind, description) {
|
|
196
|
+
const token = this.peek();
|
|
197
|
+
if (token.kind !== kind) throw new Error(`expected ${description} at position ${token.position}`);
|
|
198
|
+
return this.advance();
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
function compileCheckExpression(source, columns) {
|
|
202
|
+
return new Parser(tokenize(source), columns).parse();
|
|
203
|
+
}
|
|
6
204
|
|
|
7
205
|
// src/constants.ts
|
|
8
206
|
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
207
|
+
var MAX_IDENTIFIER_LENGTH = 63;
|
|
208
|
+
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]{0,62}\.[a-z][a-z0-9_]{0,62}$/;
|
|
209
|
+
var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
|
|
210
|
+
var CELL_PART_SUFFIX = {
|
|
211
|
+
at: "__at",
|
|
212
|
+
by: "__by",
|
|
213
|
+
rev: "__rev",
|
|
214
|
+
source: "__src",
|
|
215
|
+
conflicts: "__lost"
|
|
216
|
+
};
|
|
217
|
+
var MAX_TRACKED_COLUMN_NAME = MAX_IDENTIFIER_LENGTH - 6;
|
|
218
|
+
var CELLS_FIELD = "cells";
|
|
219
|
+
function cellColumnName(column, part) {
|
|
220
|
+
return `${column}${CELL_PART_SUFFIX[part]}`;
|
|
221
|
+
}
|
|
222
|
+
var MATERIALIZE_ACTOR = "orm:materialize";
|
|
223
|
+
JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
|
|
224
|
+
var policyVariantSchema = zod.z.object({
|
|
225
|
+
/** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
|
|
226
|
+
when: zod.z.string().min(1).max(1e3),
|
|
227
|
+
/** Exactly one of: allow-all bypass, or a row predicate. */
|
|
228
|
+
allow: zod.z.literal(true).optional(),
|
|
229
|
+
policy: zod.z.string().min(1).max(2e3).optional()
|
|
230
|
+
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
231
|
+
message: "A variant declares exactly one of allow: true or policy"
|
|
232
|
+
});
|
|
233
|
+
var policyExpressionSchema = zod.z.union([
|
|
234
|
+
zod.z.string().min(1).max(2e3),
|
|
235
|
+
zod.z.object({
|
|
236
|
+
default: zod.z.string().min(1).max(2e3),
|
|
237
|
+
variants: zod.z.array(policyVariantSchema).max(10).default([])
|
|
238
|
+
})
|
|
239
|
+
]);
|
|
240
|
+
var identifier = zod.z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
|
|
241
|
+
var durationMs = zod.z.number().int().positive().max(24 * 60 * 60 * 1e3);
|
|
242
|
+
var RESOLVED_VIEW_SYSTEM_COLUMNS = {
|
|
243
|
+
writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
|
|
244
|
+
writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
|
|
245
|
+
};
|
|
246
|
+
var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
|
|
247
|
+
tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
|
|
248
|
+
fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
|
|
249
|
+
writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
|
|
250
|
+
writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
|
|
251
|
+
writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
|
|
252
|
+
};
|
|
253
|
+
var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
|
|
254
|
+
/* @__PURE__ */ new Set([
|
|
255
|
+
...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
|
|
256
|
+
...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
|
|
257
|
+
]);
|
|
258
|
+
var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
|
|
259
|
+
var canonicalFunctionRefSchema = zod.z.object({
|
|
260
|
+
/** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
|
|
261
|
+
kind: zod.z.enum(["managed", "native"]),
|
|
262
|
+
id: zod.z.string().min(1).max(200)
|
|
263
|
+
}).strict();
|
|
264
|
+
var canonicalFetchShapeSchema = zod.z.union([
|
|
265
|
+
zod.z.object({ shape: zod.z.literal("single") }).strict(),
|
|
266
|
+
zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().min(2).max(500) }).strict()
|
|
267
|
+
]);
|
|
268
|
+
var canonicalResolvedSourceSchema = zod.z.object({
|
|
269
|
+
name: identifier,
|
|
270
|
+
kind: zod.z.enum(["manual", "etl", "prefetch"]),
|
|
271
|
+
/** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
|
|
272
|
+
table: identifier,
|
|
273
|
+
/** Resolved columns (logical names) this source supplies — the primary key is implicit. */
|
|
274
|
+
columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
|
|
275
|
+
/** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
|
|
276
|
+
updatedAt: zod.z.string().min(1).max(63).optional(),
|
|
277
|
+
/** prefetch: resolved column holding the source system's key (may be the primary key). */
|
|
278
|
+
key: zod.z.string().min(1).max(63).optional(),
|
|
279
|
+
fetch: canonicalFetchShapeSchema.optional(),
|
|
280
|
+
function: canonicalFunctionRefSchema.optional(),
|
|
281
|
+
prefetchDefaults: zod.z.object({
|
|
282
|
+
maxStalenessMs: durationMs,
|
|
283
|
+
timeoutMs: durationMs,
|
|
284
|
+
onError: zod.z.enum(["fallback", "fail"])
|
|
285
|
+
}).strict().optional(),
|
|
286
|
+
writeback: zod.z.object({
|
|
287
|
+
columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
|
|
288
|
+
defaultMode: zod.z.enum(["sync", "async"]),
|
|
289
|
+
timeoutMs: durationMs,
|
|
290
|
+
/** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
|
|
291
|
+
clearManualOnConfirm: zod.z.boolean()
|
|
292
|
+
}).strict().optional()
|
|
293
|
+
}).strict();
|
|
294
|
+
var canonicalResolveRuleSchema = zod.z.object({
|
|
295
|
+
column: zod.z.string().min(1).max(63),
|
|
296
|
+
comparator: zod.z.enum(COMPARATORS),
|
|
297
|
+
/** Source names, in tie-break order. */
|
|
298
|
+
candidates: zod.z.array(identifier).min(1).max(16),
|
|
299
|
+
/** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
|
|
300
|
+
orderBy: zod.z.string().min(1).max(2e3).optional()
|
|
301
|
+
}).strict();
|
|
302
|
+
var canonicalResolvedSchema = zod.z.object({
|
|
303
|
+
sources: zod.z.array(canonicalResolvedSourceSchema).min(1).max(16),
|
|
304
|
+
/** One rule per resolved (non-pk) column; documents the generated view for tooling. */
|
|
305
|
+
resolve: zod.z.array(canonicalResolveRuleSchema).max(500),
|
|
306
|
+
/** Sources whose tombstone hides the resolved row (v1: the manual source only). */
|
|
307
|
+
deletedBy: zod.z.array(identifier).max(1).default([]),
|
|
308
|
+
/**
|
|
309
|
+
* Materialized: the view reads an indexed internal table (`table`) that the
|
|
310
|
+
* data plane keeps in step with `compute`, the internal view holding the
|
|
311
|
+
* resolution query. Omitted when the view computes on read.
|
|
312
|
+
*/
|
|
313
|
+
materialized: zod.z.object({ table: identifier, compute: identifier }).strict().optional(),
|
|
314
|
+
/** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
|
|
315
|
+
policies: zod.z.object({
|
|
316
|
+
write: policyExpressionSchema.optional(),
|
|
317
|
+
insert: policyExpressionSchema.optional(),
|
|
318
|
+
update: policyExpressionSchema.optional()
|
|
319
|
+
}).strict().default({})
|
|
320
|
+
}).strict();
|
|
321
|
+
function validateResolvedView({ view, tables, views = [] }) {
|
|
322
|
+
const resolved = view.resolved;
|
|
323
|
+
if (!resolved) return [];
|
|
324
|
+
const errors = [];
|
|
325
|
+
const fail = (message) => errors.push(`resolved table '${view.view}': ${message}`);
|
|
326
|
+
const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
|
|
327
|
+
if (!pkColumn) {
|
|
328
|
+
fail("requires exactly one primary-key column");
|
|
329
|
+
return errors;
|
|
330
|
+
}
|
|
331
|
+
const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
|
|
332
|
+
const scope = {
|
|
333
|
+
view,
|
|
334
|
+
resolved,
|
|
335
|
+
pkColumn,
|
|
336
|
+
dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
|
|
337
|
+
sourceByName: /* @__PURE__ */ new Map(),
|
|
338
|
+
fail
|
|
339
|
+
};
|
|
340
|
+
if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail("requires exactly one manual source");
|
|
341
|
+
const tableByName = new Map(tables.map((table) => [table.table, table]));
|
|
342
|
+
for (const source of resolved.sources) {
|
|
343
|
+
if (scope.sourceByName.has(source.name)) fail(`duplicate source '${source.name}'`);
|
|
344
|
+
scope.sourceByName.set(source.name, source);
|
|
345
|
+
const table = tableByName.get(source.table);
|
|
346
|
+
if (!table) fail(`source '${source.name}' references unknown table '${source.table}'`);
|
|
347
|
+
else validateSource({ scope, source, table });
|
|
348
|
+
}
|
|
349
|
+
validateRules(scope);
|
|
350
|
+
validateViewCells(scope);
|
|
351
|
+
if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
|
|
352
|
+
for (const name of resolved.deletedBy) {
|
|
353
|
+
if (scope.sourceByName.get(name)?.kind !== "manual") fail(`deletedBy '${name}' must be the manual source`);
|
|
354
|
+
}
|
|
355
|
+
return errors;
|
|
356
|
+
}
|
|
357
|
+
function validateSource({ scope, source, table }) {
|
|
358
|
+
const { fail } = scope;
|
|
359
|
+
if (source.kind === "etl" && table.source === void 0) fail(`etl source '${source.name}' must reference a synced table`);
|
|
360
|
+
if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
|
|
361
|
+
fail(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
|
|
362
|
+
}
|
|
363
|
+
const hasRev = table.columns.some((column) => column.cell?.part === "rev");
|
|
364
|
+
if (source.kind === "manual" && !hasRev) fail(`manual source '${source.name}' must carry '__rev' cell columns`);
|
|
365
|
+
if (source.kind === "prefetch" && hasRev) fail(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
|
|
366
|
+
const byName = new Map(table.columns.map((c) => [c.name, c]));
|
|
367
|
+
validateSourceColumns({ scope, source, byName });
|
|
368
|
+
if (source.kind === "etl") {
|
|
369
|
+
const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
|
|
370
|
+
if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
|
|
371
|
+
fail(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
validatePrefetchFields({ scope, source });
|
|
375
|
+
}
|
|
376
|
+
function validatePrefetchFields({ scope, source }) {
|
|
377
|
+
const { fail, pkColumn } = scope;
|
|
378
|
+
if (source.kind !== "prefetch") {
|
|
379
|
+
if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
|
|
380
|
+
fail(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
|
|
381
|
+
}
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
|
|
385
|
+
fail(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
|
|
386
|
+
} else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
|
|
387
|
+
fail(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
|
|
388
|
+
}
|
|
389
|
+
for (const column of source.writeback?.columns ?? []) {
|
|
390
|
+
if (!source.columns.includes(column)) fail(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
function validateSourceColumns({ scope, source, byName }) {
|
|
394
|
+
const { fail, pkColumn, dataColumns } = scope;
|
|
395
|
+
for (const column of [pkColumn.name, ...source.columns]) {
|
|
396
|
+
const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
|
|
397
|
+
const theirs = byName.get(column);
|
|
398
|
+
if (!own) fail(`source '${source.name}' supplies unknown column '${column}'`);
|
|
399
|
+
else if (!theirs) fail(`source '${source.name}' table '${source.table}' has no column '${column}'`);
|
|
400
|
+
else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail(`column '${column}' has a different type on source '${source.name}'`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail }) {
|
|
404
|
+
const ruled = /* @__PURE__ */ new Set();
|
|
405
|
+
for (const rule of resolved.resolve) {
|
|
406
|
+
if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
|
|
407
|
+
fail(`resolve rule targets unknown column '${rule.column}'`);
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (ruled.has(rule.column)) fail(`column '${rule.column}' has more than one resolve rule`);
|
|
411
|
+
ruled.add(rule.column);
|
|
412
|
+
if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
|
|
413
|
+
fail(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
|
|
414
|
+
}
|
|
415
|
+
if (new Set(rule.candidates).size !== rule.candidates.length) fail(`column '${rule.column}' repeats a candidate`);
|
|
416
|
+
for (const candidate of rule.candidates) {
|
|
417
|
+
const source = sourceByName.get(candidate);
|
|
418
|
+
if (!source) fail(`column '${rule.column}' names unknown candidate source '${candidate}'`);
|
|
419
|
+
else if (!source.columns.includes(rule.column)) fail(`column '${rule.column}' candidate '${candidate}' does not supply it`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
for (const column of dataColumns.keys()) {
|
|
423
|
+
if (column !== pkColumn.name && !ruled.has(column)) fail(`column '${column}' has no resolve rule`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
var VIEW_CELL_TYPES = {
|
|
427
|
+
source: "text",
|
|
428
|
+
at: "timestamptz",
|
|
429
|
+
by: "text",
|
|
430
|
+
conflicts: "jsonb"
|
|
431
|
+
};
|
|
432
|
+
function validateViewCells({ view, dataColumns, pkColumn, fail }) {
|
|
433
|
+
const expected = /* @__PURE__ */ new Map();
|
|
434
|
+
for (const column of dataColumns.values()) {
|
|
435
|
+
if (column.name === pkColumn.name) continue;
|
|
436
|
+
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
437
|
+
expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
for (const column of view.columns) {
|
|
441
|
+
if (!column.cell) continue;
|
|
442
|
+
const want = expected.get(column.name);
|
|
443
|
+
if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
|
|
444
|
+
fail(`unexpected cell column '${column.name}'`);
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
|
|
448
|
+
expected.delete(column.name);
|
|
449
|
+
}
|
|
450
|
+
for (const name of expected.keys()) fail(`missing cell column '${name}'`);
|
|
451
|
+
}
|
|
452
|
+
var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
|
|
453
|
+
function validateMaterialized({ scope, tableByName, views }) {
|
|
454
|
+
const { view, resolved, fail } = scope;
|
|
455
|
+
const { table: tableName, compute: computeName } = resolved.materialized;
|
|
456
|
+
const table = tableByName.get(tableName);
|
|
457
|
+
if (!table || table.internal !== true || table.materializes !== view.view) {
|
|
458
|
+
fail(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
|
|
459
|
+
} else {
|
|
460
|
+
if (columnSignature(table.columns) !== columnSignature(view.columns)) fail(`materialized table '${tableName}' must have exactly the view's columns`);
|
|
461
|
+
if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail(`materialized table '${tableName}' must share the view's primary key`);
|
|
462
|
+
}
|
|
463
|
+
const compute = views.find((candidate) => candidate.view === computeName);
|
|
464
|
+
if (!compute || compute.internal !== true) fail(`compute view '${computeName}' must be an internal view`);
|
|
465
|
+
else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail(`compute view '${computeName}' must have exactly the view's columns`);
|
|
466
|
+
for (const source of resolved.sources) {
|
|
467
|
+
if (source.kind !== "etl") continue;
|
|
468
|
+
const read = tableByName.get(source.table)?.policies?.read;
|
|
469
|
+
const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
|
|
470
|
+
if (!unconditional) {
|
|
471
|
+
fail(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
9
475
|
|
|
10
476
|
// src/canonical/v2.ts
|
|
11
477
|
var CANONICAL_FORMAT = 2;
|
|
12
478
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
13
|
-
var
|
|
479
|
+
var identifier2 = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
14
480
|
var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
|
|
15
481
|
var COLUMN_TYPES = [
|
|
16
482
|
"uuid",
|
|
@@ -26,14 +492,14 @@ var COLUMN_TYPES = [
|
|
|
26
492
|
];
|
|
27
493
|
var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
|
|
28
494
|
var canonicalEnumSchema = zod.z.object({
|
|
29
|
-
name:
|
|
495
|
+
name: identifier2,
|
|
30
496
|
values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
|
|
31
497
|
});
|
|
32
498
|
var canonicalColumnSchema = zod.z.object({
|
|
33
499
|
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
34
500
|
name: zod.z.string().min(1).max(63),
|
|
35
501
|
columnName: columnIdentifier,
|
|
36
|
-
type: zod.z.union([columnTypeSchema, zod.z.object({ enum:
|
|
502
|
+
type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier2 })]),
|
|
37
503
|
nullable: zod.z.boolean().default(true),
|
|
38
504
|
unique: zod.z.boolean().default(false),
|
|
39
505
|
/** Literal default, or one of the allowlisted SQL functions. */
|
|
@@ -49,7 +515,14 @@ var canonicalColumnSchema = zod.z.object({
|
|
|
49
515
|
/** Migration hints — same semantics as v1. */
|
|
50
516
|
renamedFrom: columnIdentifier.optional(),
|
|
51
517
|
cast: zod.z.string().max(200).optional(),
|
|
52
|
-
deprecated: zod.z.boolean().default(false)
|
|
518
|
+
deprecated: zod.z.boolean().default(false),
|
|
519
|
+
/**
|
|
520
|
+
* Marks a system cell-metadata column (see CELL_PARTS in constants): part
|
|
521
|
+
* `part` of the cell of logical column `of`. Synthesized by authoring,
|
|
522
|
+
* never client-writable, absent from GraphQL scalar fields. Omitted on
|
|
523
|
+
* ordinary columns so their hashes are unchanged.
|
|
524
|
+
*/
|
|
525
|
+
cell: zod.z.object({ of: zod.z.string().min(1).max(63), part: zod.z.enum(CELL_PARTS) }).strict().optional()
|
|
53
526
|
}).superRefine((column, ctx) => {
|
|
54
527
|
if (column.onUpdateFn === void 0) return;
|
|
55
528
|
if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
|
|
@@ -64,36 +537,28 @@ var canonicalLinkSchema = zod.z.object({
|
|
|
64
537
|
name: zod.z.string().min(1).max(63),
|
|
65
538
|
kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
|
|
66
539
|
/** Target table name (namespace-local). Absent for toPlatformUser. */
|
|
67
|
-
target:
|
|
540
|
+
target: identifier2.optional(),
|
|
541
|
+
/** Owning namespace of an imported target; omitted for local links. */
|
|
542
|
+
namespace: identifier2.max(41).optional(),
|
|
68
543
|
/** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
|
|
69
544
|
on: columnIdentifier,
|
|
70
545
|
/** Referenced column on the target (default: target's PK / platform user `sub`). */
|
|
71
546
|
references: columnIdentifier.optional(),
|
|
72
547
|
/** enforce: true emits a real FK constraint (same-namespace toOne only). */
|
|
73
548
|
enforce: zod.z.boolean().default(false),
|
|
549
|
+
onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
550
|
+
onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
74
551
|
description: zod.z.string().max(500).optional()
|
|
75
552
|
});
|
|
76
553
|
var canonicalIndexSchema = zod.z.object({
|
|
77
554
|
columns: zod.z.array(columnIdentifier).min(1),
|
|
78
555
|
unique: zod.z.boolean().default(false),
|
|
79
|
-
name:
|
|
80
|
-
});
|
|
81
|
-
var policyVariantSchema = zod.z.object({
|
|
82
|
-
/** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
|
|
83
|
-
when: zod.z.string().min(1).max(1e3),
|
|
84
|
-
/** Exactly one of: allow-all bypass, or a row predicate. */
|
|
85
|
-
allow: zod.z.literal(true).optional(),
|
|
86
|
-
policy: zod.z.string().min(1).max(2e3).optional()
|
|
87
|
-
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
88
|
-
message: "A variant declares exactly one of allow: true or policy"
|
|
556
|
+
name: identifier2.optional()
|
|
89
557
|
});
|
|
90
|
-
var
|
|
91
|
-
|
|
92
|
-
zod.z.
|
|
93
|
-
|
|
94
|
-
variants: zod.z.array(policyVariantSchema).max(10).default([])
|
|
95
|
-
})
|
|
96
|
-
]);
|
|
558
|
+
var canonicalCheckSchema = zod.z.object({
|
|
559
|
+
name: identifier2,
|
|
560
|
+
expression: zod.z.string().min(1).max(2e3)
|
|
561
|
+
}).strict();
|
|
97
562
|
var canonicalPoliciesSchema = zod.z.object({
|
|
98
563
|
read: policyExpressionSchema.optional(),
|
|
99
564
|
write: policyExpressionSchema.optional(),
|
|
@@ -124,7 +589,7 @@ function isSingleViewQuerySql(value) {
|
|
|
124
589
|
return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
|
|
125
590
|
}
|
|
126
591
|
var canonicalActionSchema = zod.z.object({
|
|
127
|
-
name:
|
|
592
|
+
name: identifier2,
|
|
128
593
|
requires: zod.z.string().max(1e3).optional(),
|
|
129
594
|
params: zod.z.record(zod.z.string(), zod.z.object({
|
|
130
595
|
type: columnTypeSchema,
|
|
@@ -136,9 +601,9 @@ var canonicalActionSchema = zod.z.object({
|
|
|
136
601
|
});
|
|
137
602
|
var canonicalCowMutationSchema = zod.z.object({
|
|
138
603
|
kind: zod.z.enum(["override", "revert"]),
|
|
139
|
-
name:
|
|
604
|
+
name: identifier2,
|
|
140
605
|
/** Synced table (same namespace) the overlay copies from / falls back to. */
|
|
141
|
-
synced:
|
|
606
|
+
synced: identifier2,
|
|
142
607
|
/** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
|
|
143
608
|
pk: zod.z.string().min(1).max(63),
|
|
144
609
|
/** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
|
|
@@ -166,23 +631,28 @@ var canonicalCowMutationSchema = zod.z.object({
|
|
|
166
631
|
}
|
|
167
632
|
});
|
|
168
633
|
var canonicalForeignKeySchema = zod.z.object({
|
|
169
|
-
name:
|
|
634
|
+
name: identifier2,
|
|
170
635
|
columns: zod.z.array(columnIdentifier).min(1).max(8),
|
|
171
|
-
target:
|
|
636
|
+
target: identifier2,
|
|
172
637
|
references: zod.z.array(columnIdentifier).min(1).max(8),
|
|
173
|
-
deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional()
|
|
638
|
+
deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional(),
|
|
639
|
+
onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
640
|
+
onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
|
|
174
641
|
}).strict();
|
|
175
642
|
var canonicalTableSchema = zod.z.object({
|
|
176
|
-
table:
|
|
643
|
+
table: identifier2,
|
|
644
|
+
displayName: zod.z.string().trim().min(1).max(120).optional(),
|
|
177
645
|
description: zod.z.string().max(2e3).optional(),
|
|
178
646
|
/** Table-level rename hint (new in v2). */
|
|
179
|
-
renamedFrom:
|
|
180
|
-
/** Synced/reflected read-only tables (`source:
|
|
181
|
-
source: zod.z.string().regex(
|
|
647
|
+
renamedFrom: identifier2.optional(),
|
|
648
|
+
/** Synced/reflected read-only tables (`source: schema.table`) — v1 semantics. */
|
|
649
|
+
source: zod.z.string().regex(QUALIFIED_TABLE_PATTERN).optional(),
|
|
182
650
|
columns: zod.z.array(canonicalColumnSchema).default([]),
|
|
183
651
|
primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
|
|
184
652
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
185
653
|
indexes: zod.z.array(canonicalIndexSchema).default([]),
|
|
654
|
+
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
655
|
+
checks: zod.z.array(canonicalCheckSchema).max(100).optional(),
|
|
186
656
|
policies: canonicalPoliciesSchema.default({}),
|
|
187
657
|
/**
|
|
188
658
|
* v1-conversion carry-over only. v2 has no action surface — the runtime
|
|
@@ -193,22 +663,55 @@ var canonicalTableSchema = zod.z.object({
|
|
|
193
663
|
/** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
|
|
194
664
|
cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
|
|
195
665
|
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
196
|
-
foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional()
|
|
666
|
+
foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional(),
|
|
667
|
+
/**
|
|
668
|
+
* Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
|
|
669
|
+
* column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
|
|
670
|
+
* on a resolved table's internal sources), listed explicitly in `columns`
|
|
671
|
+
* with a `cell` marker so DDL and migrations treat them as ordinary columns.
|
|
672
|
+
* Omitted when off so existing definitions hash unchanged.
|
|
673
|
+
*/
|
|
674
|
+
cellMetadata: zod.z.literal(true).optional(),
|
|
675
|
+
/**
|
|
676
|
+
* System-owned backing table of a resolved table (its manual or prefetch
|
|
677
|
+
* source). Internal tables get DDL and IR metadata but no GraphQL surface;
|
|
678
|
+
* only the data plane writes them. Omitted when false.
|
|
679
|
+
*/
|
|
680
|
+
internal: zod.z.literal(true).optional(),
|
|
681
|
+
/**
|
|
682
|
+
* The materialized copy of resolved table `materializes` (internal): the
|
|
683
|
+
* same columns as that view, including its cell parts, maintained by the
|
|
684
|
+
* data plane. Omitted otherwise.
|
|
685
|
+
*/
|
|
686
|
+
materializes: identifier2.optional()
|
|
197
687
|
});
|
|
688
|
+
var MAX_VIEW_QUERY = 1e5;
|
|
689
|
+
var MAX_RESOLVED_VIEW_QUERY = 5e6;
|
|
198
690
|
var canonicalViewSchema = zod.z.object({
|
|
199
|
-
view:
|
|
691
|
+
view: identifier2,
|
|
200
692
|
description: zod.z.string().max(2e3).optional(),
|
|
201
693
|
columns: zod.z.array(canonicalColumnSchema).min(1),
|
|
202
694
|
/** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
|
|
203
695
|
primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
|
|
204
696
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
205
697
|
policies: zod.z.object({ read: policyExpressionSchema }).strict(),
|
|
206
|
-
query: zod.z.string().min(1).max(
|
|
698
|
+
query: zod.z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
|
|
207
699
|
message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
|
|
208
700
|
}),
|
|
209
701
|
/** Namespace-local logical relation names, sorted for deterministic hashing. */
|
|
210
|
-
dependencies: zod.z.array(
|
|
702
|
+
dependencies: zod.z.array(identifier2).max(500),
|
|
703
|
+
/**
|
|
704
|
+
* Present when this view is a resolved table (defineResolvedTable): the
|
|
705
|
+
* multi-source metadata behind its generated query. Omitted otherwise so
|
|
706
|
+
* existing views hash unchanged.
|
|
707
|
+
*/
|
|
708
|
+
resolved: canonicalResolvedSchema.optional(),
|
|
709
|
+
/** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
|
|
710
|
+
internal: zod.z.literal(true).optional()
|
|
211
711
|
}).superRefine((view, ctx) => {
|
|
712
|
+
if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
|
|
713
|
+
ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
|
|
714
|
+
}
|
|
212
715
|
for (const [index, column] of view.columns.entries()) {
|
|
213
716
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
214
717
|
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
|
|
@@ -223,20 +726,91 @@ var canonicalViewSchema = zod.z.object({
|
|
|
223
726
|
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare migration hints` });
|
|
224
727
|
}
|
|
225
728
|
}
|
|
729
|
+
if (view.resolved === void 0 && view.internal !== true) {
|
|
730
|
+
for (const [index, column] of view.columns.entries()) {
|
|
731
|
+
if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index], message: `only resolved tables carry cell columns ('${column.name}')` });
|
|
732
|
+
}
|
|
733
|
+
}
|
|
226
734
|
for (const [index, link] of view.links.entries()) {
|
|
227
735
|
if (link.enforce) {
|
|
228
736
|
ctx.addIssue({ code: "custom", path: ["links", index, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
|
|
229
737
|
}
|
|
738
|
+
if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
|
|
739
|
+
ctx.addIssue({ code: "custom", path: ["links", index], message: `view link '${link.name}' cannot declare referential actions` });
|
|
740
|
+
}
|
|
230
741
|
}
|
|
231
742
|
});
|
|
232
|
-
|
|
743
|
+
var CELL_PART_TYPES = {
|
|
744
|
+
at: "timestamptz",
|
|
745
|
+
by: "text",
|
|
746
|
+
rev: "uuid",
|
|
747
|
+
source: "text",
|
|
748
|
+
conflicts: "jsonb"
|
|
749
|
+
};
|
|
750
|
+
function trackedCellColumns(table) {
|
|
751
|
+
const pk = new Set(table.primaryKey ?? []);
|
|
752
|
+
return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
|
|
753
|
+
}
|
|
754
|
+
var SUFFIXES = Object.values(CELL_PART_SUFFIX);
|
|
755
|
+
function validateCellMetadata(table, index, ctx) {
|
|
756
|
+
const fail = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
|
|
757
|
+
const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
|
|
758
|
+
if (table.materializes !== void 0) return;
|
|
759
|
+
if (table.cellMetadata !== true) {
|
|
760
|
+
for (const { position } of cells) fail(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index, "columns", position]);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (table.source !== void 0) {
|
|
764
|
+
fail(`synced table '${table.table}' cannot track cell metadata`);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const tracked = trackedCellColumns(table);
|
|
768
|
+
const byName = new Map(tracked.map((column) => [column.name, column]));
|
|
769
|
+
const withRev = cells.some(({ cell }) => cell.part === "rev");
|
|
770
|
+
if (withRev && table.internal !== true) fail(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
|
|
771
|
+
const seen = /* @__PURE__ */ new Set();
|
|
772
|
+
for (const { column, position, cell } of cells) {
|
|
773
|
+
const path = ["tables", index, "columns", position];
|
|
774
|
+
const owner = byName.get(cell.of);
|
|
775
|
+
if (!owner) {
|
|
776
|
+
fail(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
if (cell.part !== "at" && cell.part !== "by" && cell.part !== "rev") fail(`a table cell column must be part at/by/rev, not '${cell.part}'`, path);
|
|
780
|
+
if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
|
|
781
|
+
fail(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
|
|
782
|
+
}
|
|
783
|
+
if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
|
|
784
|
+
fail(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
|
|
785
|
+
}
|
|
786
|
+
const key = `${cell.of}:${cell.part}`;
|
|
787
|
+
if (seen.has(key)) fail(`duplicate cell column '${column.name}'`, path);
|
|
788
|
+
seen.add(key);
|
|
789
|
+
}
|
|
790
|
+
for (const column of tracked) {
|
|
791
|
+
if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
|
|
792
|
+
fail(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
|
|
793
|
+
}
|
|
794
|
+
if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
|
|
795
|
+
fail(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
|
|
796
|
+
}
|
|
797
|
+
for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
|
|
798
|
+
if (!seen.has(`${column.name}:${part}`)) fail(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
var exportGateSchema = zod.z.union([zod.z.literal(true), zod.z.object({ capability: zod.z.string().min(1) }).strict(), zod.z.object({ role: zod.z.string().min(1) }).strict()]);
|
|
803
|
+
var relationExportSchema = zod.z.object({ read: exportGateSchema, write: exportGateSchema.optional() }).strict();
|
|
804
|
+
var localDefinitionSchema = zod.z.object({
|
|
233
805
|
definition_format: zod.z.literal(CANONICAL_FORMAT),
|
|
234
|
-
namespace:
|
|
806
|
+
namespace: identifier2.max(41),
|
|
235
807
|
description: zod.z.string().max(2e3).optional(),
|
|
236
808
|
enums: zod.z.array(canonicalEnumSchema).default([]),
|
|
237
809
|
tables: zod.z.array(canonicalTableSchema).min(0).max(500),
|
|
238
810
|
/** Optional to preserve byte-identical hashes for pre-view canonical v2 documents. */
|
|
239
811
|
views: zod.z.array(canonicalViewSchema).max(500).optional(),
|
|
812
|
+
/** Target-owned cross-namespace access contract, using existing namespace grants. */
|
|
813
|
+
exports: zod.z.record(identifier2, relationExportSchema).optional(),
|
|
240
814
|
/** Verbatim custom-roles grammar (@sequenceholdings/custom-roles-manifest). */
|
|
241
815
|
capabilities: zod.z.object({
|
|
242
816
|
roles: zod.z.unknown().optional(),
|
|
@@ -249,6 +823,7 @@ zod.z.object({
|
|
|
249
823
|
ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
|
|
250
824
|
}
|
|
251
825
|
seen.add(table.table);
|
|
826
|
+
validateCellMetadata(table, index, ctx);
|
|
252
827
|
if (table.source !== void 0) {
|
|
253
828
|
for (const [columnIndex, column] of table.columns.entries()) {
|
|
254
829
|
if (column.onUpdateFn !== void 0) {
|
|
@@ -262,12 +837,41 @@ zod.z.object({
|
|
|
262
837
|
}
|
|
263
838
|
}
|
|
264
839
|
const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
840
|
+
const relations = /* @__PURE__ */ new Set([...fkTables.keys(), ...(definition.views ?? []).map((view) => view.view)]);
|
|
841
|
+
for (const name of Object.keys(definition.exports ?? {})) {
|
|
842
|
+
if (!relations.has(name)) ctx.addIssue({ code: "custom", path: ["exports", name], message: `export '${name}' is not a local object` });
|
|
843
|
+
}
|
|
265
844
|
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
845
|
+
for (const [linkIndex, link] of table.links.entries()) {
|
|
846
|
+
if (link.enforce && link.namespace !== void 0 && link.namespace !== definition.namespace) {
|
|
847
|
+
ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", linkIndex], message: "physical foreign keys require a local target; use an ORM link without enforce" });
|
|
848
|
+
}
|
|
849
|
+
}
|
|
266
850
|
const names = /* @__PURE__ */ new Set([
|
|
267
851
|
...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
|
|
268
852
|
...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
|
|
269
853
|
...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
|
|
270
854
|
]);
|
|
855
|
+
for (const [index, link] of table.links.entries()) {
|
|
856
|
+
if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
|
|
857
|
+
ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "referential actions require an enforced toOne link" });
|
|
858
|
+
}
|
|
859
|
+
if (link.onDelete === "set null" || link.onUpdate === "set null") {
|
|
860
|
+
const source = table.columns.find((column) => column.columnName === link.on);
|
|
861
|
+
if (source?.nullable === false || table.primaryKey?.includes(link.on)) ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "SET NULL requires a nullable source column" });
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
for (const [index, check] of (table.checks ?? []).entries()) {
|
|
865
|
+
const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index], message });
|
|
866
|
+
if (table.source) fail("synced tables cannot declare checks");
|
|
867
|
+
if (names.has(check.name)) fail(`duplicate constraint '${check.name}'`);
|
|
868
|
+
names.add(check.name);
|
|
869
|
+
try {
|
|
870
|
+
compileCheckExpression(check.expression, table.columns);
|
|
871
|
+
} catch (error) {
|
|
872
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
873
|
+
}
|
|
874
|
+
}
|
|
271
875
|
for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
|
|
272
876
|
const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
|
|
273
877
|
if (names.has(fk.name)) fail(`duplicate foreign key '${fk.name}'`);
|
|
@@ -285,10 +889,18 @@ zod.z.object({
|
|
|
285
889
|
if (!sourceColumn || !targetColumn) fail("foreign key references an unknown column");
|
|
286
890
|
else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail("foreign key column types must match");
|
|
287
891
|
}
|
|
892
|
+
if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
|
|
893
|
+
if (fk.columns.some((name) => table.columns.find((column) => column.columnName === name)?.nullable === false || table.primaryKey?.includes(name))) fail("SET NULL requires nullable source columns");
|
|
894
|
+
}
|
|
288
895
|
const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index2) => index2.unique).map((index2) => index2.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
|
|
289
896
|
if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail("foreign key target columns must form a unique key");
|
|
290
897
|
}
|
|
291
898
|
}
|
|
899
|
+
for (const [viewIndex, view] of (definition.views ?? []).entries()) {
|
|
900
|
+
for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
|
|
901
|
+
ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
|
|
902
|
+
}
|
|
903
|
+
}
|
|
292
904
|
const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
293
905
|
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
294
906
|
const cowMutations = table.cowMutations ?? [];
|
|
@@ -404,11 +1016,19 @@ zod.z.object({
|
|
|
404
1016
|
};
|
|
405
1017
|
for (const view of views) visit(view.view, []);
|
|
406
1018
|
});
|
|
1019
|
+
localDefinitionSchema.safeExtend({
|
|
1020
|
+
/** Flattened, pinned snapshots. Imported relations never participate in this namespace's DDL. */
|
|
1021
|
+
imports: zod.z.array(localDefinitionSchema).max(32).optional()
|
|
1022
|
+
});
|
|
407
1023
|
|
|
408
1024
|
// src/ir/index.ts
|
|
409
|
-
var IR_VERSION =
|
|
1025
|
+
var IR_VERSION = 5;
|
|
410
1026
|
var LEGACY_IR_VERSION = 1;
|
|
411
|
-
var
|
|
1027
|
+
var GUARDED_IR_VERSION = 2;
|
|
1028
|
+
var COMPOSED_IR_VERSION = 3;
|
|
1029
|
+
var SQL_IR_VERSION = 4;
|
|
1030
|
+
var RUNTIME_SQL_IR_VERSION = 5;
|
|
1031
|
+
var irVersionSchema = zod.z.union([zod.z.literal(LEGACY_IR_VERSION), zod.z.literal(GUARDED_IR_VERSION), zod.z.literal(COMPOSED_IR_VERSION), zod.z.literal(SQL_IR_VERSION), zod.z.literal(RUNTIME_SQL_IR_VERSION)]);
|
|
412
1032
|
var operationVersionSchema = zod.z.object({ irVersion: irVersionSchema });
|
|
413
1033
|
function requiredArtifactIrVersion(operations) {
|
|
414
1034
|
let required = LEGACY_IR_VERSION;
|
|
@@ -460,8 +1080,8 @@ var predIrSchema = zod.z.lazy(
|
|
|
460
1080
|
value: zod.z.number(),
|
|
461
1081
|
where: predIrSchema.optional()
|
|
462
1082
|
}),
|
|
463
|
-
zod.z.object({ kind: zod.z.literal("capability"), key: zod.z.string() }),
|
|
464
|
-
zod.z.object({ kind: zod.z.literal("role"), key: zod.z.string() }),
|
|
1083
|
+
zod.z.object({ kind: zod.z.literal("capability"), key: zod.z.string(), namespace: zod.z.string().optional() }),
|
|
1084
|
+
zod.z.object({ kind: zod.z.literal("role"), key: zod.z.string(), namespace: zod.z.string().optional() }),
|
|
465
1085
|
zod.z.object({ kind: zod.z.literal("dynamicFilter"), slot: zod.z.string() }),
|
|
466
1086
|
zod.z.object({ kind: zod.z.literal("const"), value: zod.z.boolean() })
|
|
467
1087
|
])
|
|
@@ -489,6 +1109,21 @@ var selectionIrSchema = zod.z.lazy(
|
|
|
489
1109
|
() => zod.z.object({
|
|
490
1110
|
typename: zod.z.string(),
|
|
491
1111
|
columns: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })),
|
|
1112
|
+
cells: zod.z.object({
|
|
1113
|
+
alias: zod.z.string(),
|
|
1114
|
+
typename: zod.z.string(),
|
|
1115
|
+
fields: zod.z.array(zod.z.object({
|
|
1116
|
+
alias: zod.z.string(),
|
|
1117
|
+
column: zod.z.string(),
|
|
1118
|
+
typename: zod.z.string(),
|
|
1119
|
+
parts: zod.z.array(zod.z.object({
|
|
1120
|
+
alias: zod.z.string(),
|
|
1121
|
+
part: zod.z.enum(["source", "at", "by", "conflicts"]),
|
|
1122
|
+
fields: zod.z.array(zod.z.object({ alias: zod.z.string(), field: zod.z.enum(["source", "at", "value"]) })).optional(),
|
|
1123
|
+
typename: zod.z.string().optional()
|
|
1124
|
+
}))
|
|
1125
|
+
}))
|
|
1126
|
+
}).optional(),
|
|
492
1127
|
relations: zod.z.array(zod.z.object({
|
|
493
1128
|
alias: zod.z.string(),
|
|
494
1129
|
link: zod.z.string(),
|
|
@@ -510,6 +1145,11 @@ var selectionIrSchema = zod.z.lazy(
|
|
|
510
1145
|
})
|
|
511
1146
|
);
|
|
512
1147
|
var includeIfSchema = zod.z.object({ variable: zod.z.string(), negated: zod.z.boolean() });
|
|
1148
|
+
var prefetchPlanIrSchema = zod.z.object({
|
|
1149
|
+
source: zod.z.string(),
|
|
1150
|
+
maxStalenessMs: zod.z.number().int().positive().optional(),
|
|
1151
|
+
key: zod.z.union([zod.z.object({ one: valueIrSchema }), zod.z.object({ many: valueIrSchema })])
|
|
1152
|
+
});
|
|
513
1153
|
var selectRootSchema = zod.z.object({
|
|
514
1154
|
kind: zod.z.literal("select"),
|
|
515
1155
|
alias: zod.z.string(),
|
|
@@ -521,7 +1161,8 @@ var selectRootSchema = zod.z.object({
|
|
|
521
1161
|
distinctOn: zod.z.union([zod.z.array(zod.z.string()), zod.z.object({ slot: zod.z.string() })]).optional(),
|
|
522
1162
|
limit: limitIrSchema,
|
|
523
1163
|
offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
|
|
524
|
-
includeIf: includeIfSchema.optional()
|
|
1164
|
+
includeIf: includeIfSchema.optional(),
|
|
1165
|
+
prefetch: zod.z.array(prefetchPlanIrSchema).optional()
|
|
525
1166
|
});
|
|
526
1167
|
var byPkRootSchema = zod.z.object({
|
|
527
1168
|
kind: zod.z.literal("byPk"),
|
|
@@ -531,7 +1172,8 @@ var byPkRootSchema = zod.z.object({
|
|
|
531
1172
|
pk: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).min(1),
|
|
532
1173
|
/** Policy predicate — ANDed with the PK match. */
|
|
533
1174
|
where: predIrSchema,
|
|
534
|
-
includeIf: includeIfSchema.optional()
|
|
1175
|
+
includeIf: includeIfSchema.optional(),
|
|
1176
|
+
prefetch: zod.z.array(prefetchPlanIrSchema).optional()
|
|
535
1177
|
});
|
|
536
1178
|
var aggregateRootSchema = zod.z.object({
|
|
537
1179
|
kind: zod.z.literal("aggregate"),
|
|
@@ -589,7 +1231,20 @@ var mutationRootSchema = zod.z.object({
|
|
|
589
1231
|
* coalesce view falls back to synced. Both are gated on a matching synced
|
|
590
1232
|
* row and carry the write policy in `where` like update/delete.
|
|
591
1233
|
*/
|
|
592
|
-
op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
|
|
1234
|
+
op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert", "resolvedWrite"]),
|
|
1235
|
+
/**
|
|
1236
|
+
* resolvedWrite: a write to a resolved table (`table` is its view). The
|
|
1237
|
+
* executor gates on the resolved row (write policy in `where`), writes only
|
|
1238
|
+
* the touched cells into the manual source, then re-checks `insertCheck`
|
|
1239
|
+
* against the resolved row. `revert` clears `revertColumns` from the manual
|
|
1240
|
+
* source; `delete` tombstones the row there.
|
|
1241
|
+
*/
|
|
1242
|
+
resolvedAction: zod.z.enum(["insert", "update", "delete", "revert"]).optional(),
|
|
1243
|
+
revertColumns: zod.z.array(zod.z.string()).optional(),
|
|
1244
|
+
/** `@writeback(source:, mode:)` directives on this root. */
|
|
1245
|
+
writeback: zod.z.array(zod.z.object({ source: zod.z.string(), mode: zod.z.enum(["sync", "async"]) })).optional(),
|
|
1246
|
+
/** `@localOnly(source:)` — writeback columns deliberately kept local for these sources. */
|
|
1247
|
+
localOnly: zod.z.array(zod.z.string()).optional(),
|
|
593
1248
|
alias: zod.z.string(),
|
|
594
1249
|
table: zod.z.string(),
|
|
595
1250
|
/** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
|
|
@@ -653,7 +1308,31 @@ var variableIrSchema = zod.z.object({
|
|
|
653
1308
|
/** Absent = no default declared; see {@link variableDefaultIrSchema}. */
|
|
654
1309
|
defaultValue: variableDefaultIrSchema.optional()
|
|
655
1310
|
});
|
|
1311
|
+
var sqlColumnSchema = zod.z.object({ name: zod.z.string(), type: zod.z.enum(["text", "boolean", "integer", "bigint", "numeric", "double", "uuid", "date", "timestamptz"]), nullable: zod.z.boolean() });
|
|
656
1312
|
var compiledOperationSchema = zod.z.object({
|
|
1313
|
+
language: zod.z.literal("sql").optional(),
|
|
1314
|
+
/** Server-compiled object catalog and permission ceiling, never caller SQL. */
|
|
1315
|
+
sqlQuery: zod.z.object({
|
|
1316
|
+
catalog: zod.z.object({
|
|
1317
|
+
identity: zod.z.string(),
|
|
1318
|
+
objects: zod.z.array(zod.z.object({
|
|
1319
|
+
ref: zod.z.string(),
|
|
1320
|
+
table: zod.z.string(),
|
|
1321
|
+
columns: zod.z.array(sqlColumnSchema.extend({ type: zod.z.string() })).min(1),
|
|
1322
|
+
scan: zod.z.string().min(1),
|
|
1323
|
+
read: zod.z.array(zod.z.string()).min(1)
|
|
1324
|
+
}).strict()).min(1).max(256)
|
|
1325
|
+
}).strict(),
|
|
1326
|
+
read: zod.z.array(zod.z.string()).min(1).max(256),
|
|
1327
|
+
maxRows: zod.z.number().int().min(1).max(1e4)
|
|
1328
|
+
}).strict().optional(),
|
|
1329
|
+
sql: zod.z.object({
|
|
1330
|
+
text: zod.z.string().min(1).max(1048576),
|
|
1331
|
+
columns: zod.z.array(sqlColumnSchema).min(1).max(256),
|
|
1332
|
+
parameters: zod.z.array(zod.z.string()).max(128),
|
|
1333
|
+
read: zod.z.array(zod.z.string()).min(1).max(256),
|
|
1334
|
+
maxRows: zod.z.number().int().min(1).max(1e4)
|
|
1335
|
+
}).strict().optional(),
|
|
657
1336
|
irVersion: irVersionSchema,
|
|
658
1337
|
name: zod.z.string(),
|
|
659
1338
|
opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
@@ -665,8 +1344,16 @@ var compiledOperationSchema = zod.z.object({
|
|
|
665
1344
|
variables: zod.z.array(variableIrSchema),
|
|
666
1345
|
/** Every table the op touches — data-token scoping + audit. */
|
|
667
1346
|
tableSet: zod.z.array(zod.z.string()).min(1),
|
|
668
|
-
roots: zod.z.array(rootIrSchema)
|
|
1347
|
+
roots: zod.z.array(rootIrSchema)
|
|
669
1348
|
}).superRefine((operation, ctx) => {
|
|
1349
|
+
if (operation.language === "sql") {
|
|
1350
|
+
const validPlan = operation.sqlQuery ? !operation.sql && operation.irVersion === RUNTIME_SQL_IR_VERSION : !!operation.sql && operation.irVersion === SQL_IR_VERSION;
|
|
1351
|
+
if (!validPlan || operation.kind !== "query" || operation.roots.length !== 0 || !operation.requires?.capability) {
|
|
1352
|
+
ctx.addIssue({ code: "custom", message: "SQL operations require a query-only v4 plan or v5 runtime contract, capability, and no GraphQL roots" });
|
|
1353
|
+
}
|
|
1354
|
+
} else if (operation.sql || operation.sqlQuery || operation.roots.length === 0) {
|
|
1355
|
+
ctx.addIssue({ code: "custom", message: "GraphQL operations require roots and cannot carry SQL" });
|
|
1356
|
+
}
|
|
670
1357
|
for (const [index, root] of operation.roots.entries()) {
|
|
671
1358
|
if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
|
|
672
1359
|
if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
|
|
@@ -674,13 +1361,55 @@ var compiledOperationSchema = zod.z.object({
|
|
|
674
1361
|
}
|
|
675
1362
|
}
|
|
676
1363
|
});
|
|
1364
|
+
var resolvedSourceIrSchema = zod.z.object({
|
|
1365
|
+
name: zod.z.string(),
|
|
1366
|
+
kind: zod.z.enum(["manual", "etl", "prefetch"]),
|
|
1367
|
+
/** Logical table key of the backing relation. */
|
|
1368
|
+
table: zod.z.string(),
|
|
1369
|
+
columns: zod.z.array(zod.z.string()),
|
|
1370
|
+
key: zod.z.string().optional(),
|
|
1371
|
+
fetch: zod.z.union([
|
|
1372
|
+
zod.z.object({ shape: zod.z.literal("single") }),
|
|
1373
|
+
zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().positive() })
|
|
1374
|
+
]).optional(),
|
|
1375
|
+
function: zod.z.object({ kind: zod.z.enum(["managed", "native"]), id: zod.z.string() }).optional(),
|
|
1376
|
+
prefetchDefaults: zod.z.object({
|
|
1377
|
+
maxStalenessMs: zod.z.number().int().positive(),
|
|
1378
|
+
timeoutMs: zod.z.number().int().positive(),
|
|
1379
|
+
onError: zod.z.enum(["fallback", "fail"])
|
|
1380
|
+
}).optional(),
|
|
1381
|
+
writeback: zod.z.object({
|
|
1382
|
+
columns: zod.z.array(zod.z.string()),
|
|
1383
|
+
defaultMode: zod.z.enum(["sync", "async"]),
|
|
1384
|
+
timeoutMs: zod.z.number().int().positive(),
|
|
1385
|
+
clearManualOnConfirm: zod.z.boolean()
|
|
1386
|
+
}).optional()
|
|
1387
|
+
});
|
|
1388
|
+
var resolvedMetaIrSchema = zod.z.object({
|
|
1389
|
+
/** Logical pk column of the resolved view (single-column). */
|
|
1390
|
+
pk: zod.z.string(),
|
|
1391
|
+
/** Source name of the manual source. */
|
|
1392
|
+
manual: zod.z.string(),
|
|
1393
|
+
sources: zod.z.array(resolvedSourceIrSchema),
|
|
1394
|
+
deletedBy: zod.z.array(zod.z.string()),
|
|
1395
|
+
/**
|
|
1396
|
+
* Materialized: the view reads `table`, which the data plane refreshes from
|
|
1397
|
+
* the `compute` view (claims-gated) whenever it writes a source row.
|
|
1398
|
+
*/
|
|
1399
|
+
materialized: zod.z.object({ table: zod.z.string(), compute: zod.z.string() }).optional()
|
|
1400
|
+
});
|
|
677
1401
|
var tableMetaIrSchema = zod.z.object({
|
|
678
1402
|
table: zod.z.string(),
|
|
1403
|
+
namespace: zod.z.string().optional(),
|
|
1404
|
+
dependencies: zod.z.array(zod.z.string()).optional(),
|
|
1405
|
+
foreignReferences: zod.z.array(zod.z.object({ column: zod.z.string(), target: zod.z.string(), targetColumn: zod.z.string() })).optional(),
|
|
1406
|
+
exportRead: zod.z.union([zod.z.literal(true), zod.z.object({ capability: zod.z.string() }), zod.z.object({ role: zod.z.string() })]).optional(),
|
|
1407
|
+
exportWrite: zod.z.union([zod.z.literal(true), zod.z.object({ capability: zod.z.string() }), zod.z.object({ role: zod.z.string() })]).optional(),
|
|
679
1408
|
schema: zod.z.string(),
|
|
680
1409
|
typename: zod.z.string(),
|
|
681
1410
|
/**
|
|
682
1411
|
* True when statements over this relation evaluate a claims-gated synced
|
|
683
|
-
* view: synced twins (`source:
|
|
1412
|
+
* view: synced twins (`source: schema.table`) materialize as views whose baked
|
|
684
1413
|
* policy reads `current_setting('request.jwt.claims')`, and namespace views
|
|
685
1414
|
* inherit the gate transitively through their declared dependencies. The
|
|
686
1415
|
* executor must stamp that GUC (transaction-scoped) before any statement
|
|
@@ -714,6 +1443,22 @@ var tableMetaIrSchema = zod.z.object({
|
|
|
714
1443
|
* `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
|
|
715
1444
|
*/
|
|
716
1445
|
touchOnUpdate: zod.z.array(zod.z.string()).optional(),
|
|
1446
|
+
/**
|
|
1447
|
+
* Per-cell metadata: logical column → the logical names of its cell-part
|
|
1448
|
+
* columns (which are listed in `columns`, but never filterable, orderable or
|
|
1449
|
+
* writable). Tables stamp `at`/`by` (and a manual source `rev`) on every
|
|
1450
|
+
* write; resolved views expose `source`/`at`/`by`/`conflicts`. Omitted when
|
|
1451
|
+
* the relation tracks no cells.
|
|
1452
|
+
*/
|
|
1453
|
+
cells: zod.z.record(zod.z.string(), zod.z.object({
|
|
1454
|
+
at: zod.z.string().optional(),
|
|
1455
|
+
by: zod.z.string().optional(),
|
|
1456
|
+
rev: zod.z.string().optional(),
|
|
1457
|
+
source: zod.z.string().optional(),
|
|
1458
|
+
conflicts: zod.z.string().optional()
|
|
1459
|
+
}).strict()).optional(),
|
|
1460
|
+
/** Resolved-table view metadata (sources, prefetch/writeback config) for the data plane. */
|
|
1461
|
+
resolved: resolvedMetaIrSchema.optional(),
|
|
717
1462
|
constraints: zod.z.array(zod.z.object({ name: zod.z.string(), columns: zod.z.array(zod.z.string()) })),
|
|
718
1463
|
/** Depth/node caps for dynamic filter trees on this table. */
|
|
719
1464
|
dynamicFilterMaxDepth: zod.z.number().int().positive().default(6),
|
|
@@ -723,10 +1468,18 @@ var compiledArtifactSchema = zod.z.object({
|
|
|
723
1468
|
irVersion: irVersionSchema,
|
|
724
1469
|
namespace: zod.z.string(),
|
|
725
1470
|
schemaHash: zod.z.string(),
|
|
1471
|
+
ownSchemaHash: zod.z.string().optional(),
|
|
1472
|
+
dependencies: zod.z.record(zod.z.string(), zod.z.string()).optional(),
|
|
1473
|
+
readPolicies: zod.z.record(zod.z.string(), predIrSchema).optional(),
|
|
726
1474
|
opsHash: zod.z.string(),
|
|
1475
|
+
/** Apply-observed synced objects without a serving backing; not part of the authored schema hash. */
|
|
1476
|
+
unbackedTables: zod.z.array(zod.z.string()).optional(),
|
|
727
1477
|
tables: zod.z.record(zod.z.string(), tableMetaIrSchema),
|
|
728
1478
|
operations: zod.z.record(zod.z.string(), compiledOperationSchema)
|
|
729
1479
|
}).superRefine((artifact, ctx) => {
|
|
1480
|
+
if (Object.keys(artifact.dependencies ?? {}).length > 0 && artifact.irVersion < COMPOSED_IR_VERSION) {
|
|
1481
|
+
ctx.addIssue({ code: "custom", path: ["irVersion"], message: "cross-namespace dependencies require IR v3" });
|
|
1482
|
+
}
|
|
730
1483
|
for (const [name, operation] of Object.entries(artifact.operations)) {
|
|
731
1484
|
if (operation.irVersion > artifact.irVersion) {
|
|
732
1485
|
ctx.addIssue({ code: "custom", path: ["operations", name, "irVersion"], message: "operation requires a newer IR version than its artifact" });
|
|
@@ -742,7 +1495,7 @@ var IrVersionError = class extends Error {
|
|
|
742
1495
|
function normalizeIr(raw) {
|
|
743
1496
|
if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
|
|
744
1497
|
const version = raw.irVersion;
|
|
745
|
-
if (version
|
|
1498
|
+
if (!irVersionSchema.safeParse(version).success) throw new IrVersionError(version);
|
|
746
1499
|
return compiledArtifactSchema.parse(raw);
|
|
747
1500
|
}
|
|
748
1501
|
function indexByHash(artifact) {
|
|
@@ -751,13 +1504,20 @@ function indexByHash(artifact) {
|
|
|
751
1504
|
return map;
|
|
752
1505
|
}
|
|
753
1506
|
|
|
1507
|
+
exports.CELLS_FIELD = CELLS_FIELD;
|
|
1508
|
+
exports.CELL_PARTS = CELL_PARTS;
|
|
754
1509
|
exports.CLAIMS_CONTRACT = CLAIMS_CONTRACT;
|
|
755
1510
|
exports.CMP_OPS = CMP_OPS;
|
|
1511
|
+
exports.COMPOSED_IR_VERSION = COMPOSED_IR_VERSION;
|
|
756
1512
|
exports.DATE_BUCKETS = DATE_BUCKETS;
|
|
1513
|
+
exports.GUARDED_IR_VERSION = GUARDED_IR_VERSION;
|
|
757
1514
|
exports.INPUT_REF_KINDS = INPUT_REF_KINDS;
|
|
758
1515
|
exports.IR_VERSION = IR_VERSION;
|
|
759
1516
|
exports.IrVersionError = IrVersionError;
|
|
760
1517
|
exports.LEGACY_IR_VERSION = LEGACY_IR_VERSION;
|
|
1518
|
+
exports.RUNTIME_SQL_IR_VERSION = RUNTIME_SQL_IR_VERSION;
|
|
1519
|
+
exports.SQL_IR_VERSION = SQL_IR_VERSION;
|
|
1520
|
+
exports.cellColumnName = cellColumnName;
|
|
761
1521
|
exports.compiledArtifactSchema = compiledArtifactSchema;
|
|
762
1522
|
exports.compiledOperationSchema = compiledOperationSchema;
|
|
763
1523
|
exports.indexByHash = indexByHash;
|
|
@@ -765,7 +1525,10 @@ exports.limitIrSchema = limitIrSchema;
|
|
|
765
1525
|
exports.normalizeIr = normalizeIr;
|
|
766
1526
|
exports.orderIrSchema = orderIrSchema;
|
|
767
1527
|
exports.predIrSchema = predIrSchema;
|
|
1528
|
+
exports.prefetchPlanIrSchema = prefetchPlanIrSchema;
|
|
768
1529
|
exports.requiredArtifactIrVersion = requiredArtifactIrVersion;
|
|
1530
|
+
exports.resolvedMetaIrSchema = resolvedMetaIrSchema;
|
|
1531
|
+
exports.resolvedSourceIrSchema = resolvedSourceIrSchema;
|
|
769
1532
|
exports.rootIrSchema = rootIrSchema;
|
|
770
1533
|
exports.selectionIrSchema = selectionIrSchema;
|
|
771
1534
|
exports.tableMetaIrSchema = tableMetaIrSchema;
|