@sequenceholdings/orm 2.0.0 → 2.1.1
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 +94 -2
- package/dist/authoring.cjs +1474 -54
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +242 -4
- package/dist/authoring.d.ts +242 -4
- package/dist/authoring.js +1465 -56
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +740 -42
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +742 -53
- package/dist/canonical.d.ts +742 -53
- package/dist/canonical.js +724 -43
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +2478 -328
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2480 -330
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs +90 -25
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +39 -22
- package/dist/client.d.ts +39 -22
- package/dist/client.js +87 -26
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +1755 -329
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +56 -3
- package/dist/compiler.d.ts +56 -3
- package/dist/compiler.js +1756 -330
- package/dist/compiler.js.map +1 -1
- package/dist/constants-YoFGjIBM.d.cts +99 -0
- package/dist/constants-YoFGjIBM.d.ts +99 -0
- package/dist/dynamic-input.d.cts +1 -0
- package/dist/dynamic-input.d.ts +1 -0
- package/dist/index.cjs +1973 -164
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +474 -100
- package/dist/index.d.ts +474 -100
- package/dist/index.js +1940 -167
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +869 -46
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +699 -25
- package/dist/ir.d.ts +699 -25
- package/dist/ir.js +861 -47
- package/dist/ir.js.map +1 -1
- package/dist/{naming-CTvWSRTf.d.ts → naming-CPfx7EfF.d.ts} +28 -1
- package/dist/{naming-DRyinU4u.d.cts → naming-CU9ThP_E.d.cts} +28 -1
- package/dist/policy-1iZbGnZZ.d.cts +24 -0
- package/dist/policy-1iZbGnZZ.d.ts +24 -0
- package/dist/predicate-v2.cjs +852 -49
- 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 +852 -49
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +1309 -58
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.d.cts +7 -3
- package/dist/sdl.d.ts +7 -3
- package/dist/sdl.js +1305 -59
- package/dist/sdl.js.map +1 -1
- package/package.json +11 -6
- package/templates/orm-namespace/CLAUDE.md +6 -4
- 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/predicate-v2.cjs
CHANGED
|
@@ -31,10 +31,478 @@ var PolicyCompileError = class extends Error {
|
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
// src/constants.ts
|
|
34
|
-
var
|
|
34
|
+
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
35
|
+
var MAX_IDENTIFIER_LENGTH = 63;
|
|
36
|
+
var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
|
|
37
|
+
var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
|
|
38
|
+
var CELL_PART_SUFFIX = {
|
|
39
|
+
at: "__at",
|
|
40
|
+
by: "__by",
|
|
41
|
+
rev: "__rev",
|
|
42
|
+
source: "__src",
|
|
43
|
+
conflicts: "__lost"
|
|
44
|
+
};
|
|
45
|
+
var MAX_TRACKED_COLUMN_NAME = MAX_IDENTIFIER_LENGTH - 6;
|
|
46
|
+
function cellColumnName(column, part) {
|
|
47
|
+
return `${column}${CELL_PART_SUFFIX[part]}`;
|
|
48
|
+
}
|
|
49
|
+
var MATERIALIZE_ACTOR = "orm:materialize";
|
|
50
|
+
JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
|
|
51
|
+
var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
52
|
+
var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
53
|
+
|
|
54
|
+
// src/checks/compile.ts
|
|
55
|
+
function tokenize(source) {
|
|
56
|
+
const tokens = [];
|
|
57
|
+
for (let position = 0; position < source.length; ) {
|
|
58
|
+
const char = source[position];
|
|
59
|
+
if (/\s/.test(char)) {
|
|
60
|
+
position++;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (char === "(" || char === ")" || char === ",") {
|
|
64
|
+
tokens.push({ kind: char === "(" ? "lparen" : char === ")" ? "rparen" : "comma", value: char, position });
|
|
65
|
+
position++;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (char === "'") {
|
|
69
|
+
const start = position++;
|
|
70
|
+
let value = "";
|
|
71
|
+
let closed = false;
|
|
72
|
+
while (position < source.length) {
|
|
73
|
+
if (source[position] !== "'") {
|
|
74
|
+
value += source[position++];
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (source[position + 1] === "'") {
|
|
78
|
+
value += "'";
|
|
79
|
+
position += 2;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
position++;
|
|
83
|
+
closed = true;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
if (!closed) throw new Error(`unterminated string at position ${start}`);
|
|
87
|
+
if (/[\x00-\x1f\x7f]/.test(value)) throw new Error(`control character in string at position ${start}`);
|
|
88
|
+
tokens.push({ kind: "string", value, position: start });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const operator = /^(<=|>=|<>|!=|!~|=|<|>|~)/.exec(source.slice(position));
|
|
92
|
+
if (operator) {
|
|
93
|
+
tokens.push({ kind: "operator", value: operator[0] === "<>" ? "!=" : operator[0], position });
|
|
94
|
+
position += operator[0].length;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const number = /^-?\d+(?:\.\d+)?/.exec(source.slice(position));
|
|
98
|
+
if (number) {
|
|
99
|
+
tokens.push({ kind: "number", value: number[0], position });
|
|
100
|
+
position += number[0].length;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const identifier3 = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(source.slice(position));
|
|
104
|
+
if (identifier3) {
|
|
105
|
+
tokens.push({ kind: "identifier", value: identifier3[0], position });
|
|
106
|
+
position += identifier3[0].length;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
throw new Error(`unexpected character '${char}' at position ${position}`);
|
|
110
|
+
}
|
|
111
|
+
tokens.push({ kind: "end", value: "", position: source.length });
|
|
112
|
+
if (tokens.length > 256) throw new Error("check expression exceeds 256 tokens");
|
|
113
|
+
return tokens;
|
|
114
|
+
}
|
|
115
|
+
var Parser = class {
|
|
116
|
+
constructor(tokens, columns) {
|
|
117
|
+
this.tokens = tokens;
|
|
118
|
+
this.columns = /* @__PURE__ */ new Map();
|
|
119
|
+
for (const column of columns) {
|
|
120
|
+
for (const alias of [column.name, column.columnName]) {
|
|
121
|
+
const previous = this.columns.get(alias);
|
|
122
|
+
if (previous && previous.columnName !== column.columnName) {
|
|
123
|
+
throw new Error(`ambiguous check column '${alias}'`);
|
|
124
|
+
}
|
|
125
|
+
this.columns.set(alias, column);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
index = 0;
|
|
130
|
+
depth = 0;
|
|
131
|
+
columns;
|
|
132
|
+
parse() {
|
|
133
|
+
const expression = this.parseOr();
|
|
134
|
+
this.expect("end", "end of expression");
|
|
135
|
+
return expression;
|
|
136
|
+
}
|
|
137
|
+
parseOr() {
|
|
138
|
+
let left = this.parseAnd();
|
|
139
|
+
while (this.matchKeyword("or")) left = `(${left} OR ${this.parseAnd()})`;
|
|
140
|
+
return left;
|
|
141
|
+
}
|
|
142
|
+
parseAnd() {
|
|
143
|
+
let left = this.parseNot();
|
|
144
|
+
while (this.matchKeyword("and")) left = `(${left} AND ${this.parseNot()})`;
|
|
145
|
+
return left;
|
|
146
|
+
}
|
|
147
|
+
parseNot() {
|
|
148
|
+
if (this.matchKeyword("not")) return `(NOT ${this.parseNot()})`;
|
|
149
|
+
return this.parsePrimary();
|
|
150
|
+
}
|
|
151
|
+
parsePrimary() {
|
|
152
|
+
if (this.peek().kind === "lparen") {
|
|
153
|
+
if (++this.depth > 32) throw new Error("check expression nesting exceeds 32 levels");
|
|
154
|
+
this.advance();
|
|
155
|
+
const expression = this.parseOr();
|
|
156
|
+
this.expect("rparen", "')'");
|
|
157
|
+
this.depth--;
|
|
158
|
+
return `(${expression})`;
|
|
159
|
+
}
|
|
160
|
+
return this.parseComparison();
|
|
161
|
+
}
|
|
162
|
+
parseComparison() {
|
|
163
|
+
const left = this.parseValue();
|
|
164
|
+
if (this.matchKeyword("is")) {
|
|
165
|
+
const negate = this.matchKeyword("not");
|
|
166
|
+
this.expectKeyword("null");
|
|
167
|
+
return `${left.sql} IS ${negate ? "NOT " : ""}NULL`;
|
|
168
|
+
}
|
|
169
|
+
if (this.matchKeyword("between")) {
|
|
170
|
+
const lower = this.parseValue();
|
|
171
|
+
this.expectKeyword("and");
|
|
172
|
+
const upper = this.parseValue();
|
|
173
|
+
return `${left.sql} BETWEEN ${lower.sql} AND ${upper.sql}`;
|
|
174
|
+
}
|
|
175
|
+
if (this.matchKeyword("in")) {
|
|
176
|
+
this.expect("lparen", "'(' after IN");
|
|
177
|
+
const values = [this.parseValue()];
|
|
178
|
+
while (this.peek().kind === "comma") {
|
|
179
|
+
this.advance();
|
|
180
|
+
values.push(this.parseValue());
|
|
181
|
+
}
|
|
182
|
+
this.expect("rparen", "')' after IN values");
|
|
183
|
+
if (values.some((value) => !value.literal)) throw new Error("IN values must be literals");
|
|
184
|
+
return `${left.sql} IN (${values.map((value) => value.sql).join(", ")})`;
|
|
185
|
+
}
|
|
186
|
+
const operator = this.expect("operator", "a comparison operator");
|
|
187
|
+
const right = this.parseValue();
|
|
188
|
+
if (operator.value === "~" || operator.value === "!~") {
|
|
189
|
+
if (left.column && left.column.type !== "text") throw new Error("regex checks require a text column");
|
|
190
|
+
if (!right.literal) throw new Error("regex pattern must be a literal");
|
|
191
|
+
}
|
|
192
|
+
return `${left.sql} ${operator.value} ${right.sql}`;
|
|
193
|
+
}
|
|
194
|
+
parseValue() {
|
|
195
|
+
const token = this.advance();
|
|
196
|
+
if (token.kind === "string") return { sql: quoteLiteral(token.value), literal: true };
|
|
197
|
+
if (token.kind === "number") {
|
|
198
|
+
const number = Number(token.value);
|
|
199
|
+
if (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number)) {
|
|
200
|
+
throw new Error(`unsafe numeric literal at position ${token.position}`);
|
|
201
|
+
}
|
|
202
|
+
return { sql: token.value, literal: true };
|
|
203
|
+
}
|
|
204
|
+
if (token.kind !== "identifier") throw new Error(`expected a column or literal at position ${token.position}`);
|
|
205
|
+
const keyword = token.value.toLowerCase();
|
|
206
|
+
if (keyword === "true" || keyword === "false" || keyword === "null") return { sql: keyword.toUpperCase(), literal: true };
|
|
207
|
+
if (keyword === "btrim") {
|
|
208
|
+
this.expect("lparen", "'(' after btrim");
|
|
209
|
+
const column2 = this.parseColumn();
|
|
210
|
+
this.expect("rparen", "')' after btrim column");
|
|
211
|
+
if (column2.type !== "text") throw new Error("btrim requires a text column");
|
|
212
|
+
return { sql: `btrim(${quoteIdentifier(column2.columnName)})`, column: column2 };
|
|
213
|
+
}
|
|
214
|
+
const column = this.columns.get(token.value);
|
|
215
|
+
if (!column) throw new Error(`unknown check column '${token.value}'`);
|
|
216
|
+
return { sql: quoteIdentifier(column.columnName), column };
|
|
217
|
+
}
|
|
218
|
+
parseColumn() {
|
|
219
|
+
const token = this.expect("identifier", "a column name");
|
|
220
|
+
const column = this.columns.get(token.value);
|
|
221
|
+
if (!column) throw new Error(`unknown check column '${token.value}'`);
|
|
222
|
+
return column;
|
|
223
|
+
}
|
|
224
|
+
peek() {
|
|
225
|
+
return this.tokens[this.index];
|
|
226
|
+
}
|
|
227
|
+
advance() {
|
|
228
|
+
const token = this.peek();
|
|
229
|
+
if (token.kind !== "end") this.index++;
|
|
230
|
+
return token;
|
|
231
|
+
}
|
|
232
|
+
matchKeyword(keyword) {
|
|
233
|
+
if (this.peek().kind !== "identifier" || this.peek().value.toLowerCase() !== keyword) return false;
|
|
234
|
+
this.advance();
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
expectKeyword(keyword) {
|
|
238
|
+
if (!this.matchKeyword(keyword)) throw new Error(`expected '${keyword}' at position ${this.peek().position}`);
|
|
239
|
+
}
|
|
240
|
+
expect(kind, description) {
|
|
241
|
+
const token = this.peek();
|
|
242
|
+
if (token.kind !== kind) throw new Error(`expected ${description} at position ${token.position}`);
|
|
243
|
+
return this.advance();
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
function compileCheckExpression(source, columns) {
|
|
247
|
+
return new Parser(tokenize(source), columns).parse();
|
|
248
|
+
}
|
|
249
|
+
var policyVariantSchema = zod.z.object({
|
|
250
|
+
/** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
|
|
251
|
+
when: zod.z.string().min(1).max(1e3),
|
|
252
|
+
/** Exactly one of: allow-all bypass, or a row predicate. */
|
|
253
|
+
allow: zod.z.literal(true).optional(),
|
|
254
|
+
policy: zod.z.string().min(1).max(2e3).optional()
|
|
255
|
+
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
256
|
+
message: "A variant declares exactly one of allow: true or policy"
|
|
257
|
+
});
|
|
258
|
+
var policyExpressionSchema = zod.z.union([
|
|
259
|
+
zod.z.string().min(1).max(2e3),
|
|
260
|
+
zod.z.object({
|
|
261
|
+
default: zod.z.string().min(1).max(2e3),
|
|
262
|
+
variants: zod.z.array(policyVariantSchema).max(10).default([])
|
|
263
|
+
})
|
|
264
|
+
]);
|
|
265
|
+
var identifier = zod.z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
|
|
266
|
+
var durationMs = zod.z.number().int().positive().max(24 * 60 * 60 * 1e3);
|
|
267
|
+
var RESOLVED_VIEW_SYSTEM_COLUMNS = {
|
|
268
|
+
writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
|
|
269
|
+
writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
|
|
270
|
+
};
|
|
271
|
+
var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
|
|
272
|
+
tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
|
|
273
|
+
fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
|
|
274
|
+
writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
|
|
275
|
+
writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
|
|
276
|
+
writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
|
|
277
|
+
};
|
|
278
|
+
var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
|
|
279
|
+
/* @__PURE__ */ new Set([
|
|
280
|
+
...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
|
|
281
|
+
...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
|
|
282
|
+
]);
|
|
283
|
+
var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
|
|
284
|
+
var canonicalFunctionRefSchema = zod.z.object({
|
|
285
|
+
/** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
|
|
286
|
+
kind: zod.z.enum(["managed", "native"]),
|
|
287
|
+
id: zod.z.string().min(1).max(200)
|
|
288
|
+
}).strict();
|
|
289
|
+
var canonicalFetchShapeSchema = zod.z.union([
|
|
290
|
+
zod.z.object({ shape: zod.z.literal("single") }).strict(),
|
|
291
|
+
zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().min(2).max(500) }).strict()
|
|
292
|
+
]);
|
|
293
|
+
var canonicalResolvedSourceSchema = zod.z.object({
|
|
294
|
+
name: identifier,
|
|
295
|
+
kind: zod.z.enum(["manual", "etl", "prefetch"]),
|
|
296
|
+
/** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
|
|
297
|
+
table: identifier,
|
|
298
|
+
/** Resolved columns (logical names) this source supplies — the primary key is implicit. */
|
|
299
|
+
columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
|
|
300
|
+
/** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
|
|
301
|
+
updatedAt: zod.z.string().min(1).max(63).optional(),
|
|
302
|
+
/** prefetch: resolved column holding the source system's key (may be the primary key). */
|
|
303
|
+
key: zod.z.string().min(1).max(63).optional(),
|
|
304
|
+
fetch: canonicalFetchShapeSchema.optional(),
|
|
305
|
+
function: canonicalFunctionRefSchema.optional(),
|
|
306
|
+
prefetchDefaults: zod.z.object({
|
|
307
|
+
maxStalenessMs: durationMs,
|
|
308
|
+
timeoutMs: durationMs,
|
|
309
|
+
onError: zod.z.enum(["fallback", "fail"])
|
|
310
|
+
}).strict().optional(),
|
|
311
|
+
writeback: zod.z.object({
|
|
312
|
+
columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
|
|
313
|
+
defaultMode: zod.z.enum(["sync", "async"]),
|
|
314
|
+
timeoutMs: durationMs,
|
|
315
|
+
/** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
|
|
316
|
+
clearManualOnConfirm: zod.z.boolean()
|
|
317
|
+
}).strict().optional()
|
|
318
|
+
}).strict();
|
|
319
|
+
var canonicalResolveRuleSchema = zod.z.object({
|
|
320
|
+
column: zod.z.string().min(1).max(63),
|
|
321
|
+
comparator: zod.z.enum(COMPARATORS),
|
|
322
|
+
/** Source names, in tie-break order. */
|
|
323
|
+
candidates: zod.z.array(identifier).min(1).max(16),
|
|
324
|
+
/** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
|
|
325
|
+
orderBy: zod.z.string().min(1).max(2e3).optional()
|
|
326
|
+
}).strict();
|
|
327
|
+
var canonicalResolvedSchema = zod.z.object({
|
|
328
|
+
sources: zod.z.array(canonicalResolvedSourceSchema).min(1).max(16),
|
|
329
|
+
/** One rule per resolved (non-pk) column; documents the generated view for tooling. */
|
|
330
|
+
resolve: zod.z.array(canonicalResolveRuleSchema).max(500),
|
|
331
|
+
/** Sources whose tombstone hides the resolved row (v1: the manual source only). */
|
|
332
|
+
deletedBy: zod.z.array(identifier).max(1).default([]),
|
|
333
|
+
/**
|
|
334
|
+
* Materialized: the view reads an indexed internal table (`table`) that the
|
|
335
|
+
* data plane keeps in step with `compute`, the internal view holding the
|
|
336
|
+
* resolution query. Omitted when the view computes on read.
|
|
337
|
+
*/
|
|
338
|
+
materialized: zod.z.object({ table: identifier, compute: identifier }).strict().optional(),
|
|
339
|
+
/** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
|
|
340
|
+
policies: zod.z.object({
|
|
341
|
+
write: policyExpressionSchema.optional(),
|
|
342
|
+
insert: policyExpressionSchema.optional(),
|
|
343
|
+
update: policyExpressionSchema.optional()
|
|
344
|
+
}).strict().default({})
|
|
345
|
+
}).strict();
|
|
346
|
+
function validateResolvedView({ view, tables, views = [] }) {
|
|
347
|
+
const resolved = view.resolved;
|
|
348
|
+
if (!resolved) return [];
|
|
349
|
+
const errors = [];
|
|
350
|
+
const fail = (message) => errors.push(`resolved table '${view.view}': ${message}`);
|
|
351
|
+
const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
|
|
352
|
+
if (!pkColumn) {
|
|
353
|
+
fail("requires exactly one primary-key column");
|
|
354
|
+
return errors;
|
|
355
|
+
}
|
|
356
|
+
const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
|
|
357
|
+
const scope = {
|
|
358
|
+
view,
|
|
359
|
+
resolved,
|
|
360
|
+
pkColumn,
|
|
361
|
+
dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
|
|
362
|
+
sourceByName: /* @__PURE__ */ new Map(),
|
|
363
|
+
fail
|
|
364
|
+
};
|
|
365
|
+
if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail("requires exactly one manual source");
|
|
366
|
+
const tableByName = new Map(tables.map((table) => [table.table, table]));
|
|
367
|
+
for (const source of resolved.sources) {
|
|
368
|
+
if (scope.sourceByName.has(source.name)) fail(`duplicate source '${source.name}'`);
|
|
369
|
+
scope.sourceByName.set(source.name, source);
|
|
370
|
+
const table = tableByName.get(source.table);
|
|
371
|
+
if (!table) fail(`source '${source.name}' references unknown table '${source.table}'`);
|
|
372
|
+
else validateSource({ scope, source, table });
|
|
373
|
+
}
|
|
374
|
+
validateRules(scope);
|
|
375
|
+
validateViewCells(scope);
|
|
376
|
+
if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
|
|
377
|
+
for (const name of resolved.deletedBy) {
|
|
378
|
+
if (scope.sourceByName.get(name)?.kind !== "manual") fail(`deletedBy '${name}' must be the manual source`);
|
|
379
|
+
}
|
|
380
|
+
return errors;
|
|
381
|
+
}
|
|
382
|
+
function validateSource({ scope, source, table }) {
|
|
383
|
+
const { fail } = scope;
|
|
384
|
+
if (source.kind === "etl" && table.source === void 0) fail(`etl source '${source.name}' must reference a synced table`);
|
|
385
|
+
if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
|
|
386
|
+
fail(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
|
|
387
|
+
}
|
|
388
|
+
const hasRev = table.columns.some((column) => column.cell?.part === "rev");
|
|
389
|
+
if (source.kind === "manual" && !hasRev) fail(`manual source '${source.name}' must carry '__rev' cell columns`);
|
|
390
|
+
if (source.kind === "prefetch" && hasRev) fail(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
|
|
391
|
+
const byName = new Map(table.columns.map((c) => [c.name, c]));
|
|
392
|
+
validateSourceColumns({ scope, source, byName });
|
|
393
|
+
if (source.kind === "etl") {
|
|
394
|
+
const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
|
|
395
|
+
if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
|
|
396
|
+
fail(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
validatePrefetchFields({ scope, source });
|
|
400
|
+
}
|
|
401
|
+
function validatePrefetchFields({ scope, source }) {
|
|
402
|
+
const { fail, pkColumn } = scope;
|
|
403
|
+
if (source.kind !== "prefetch") {
|
|
404
|
+
if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
|
|
405
|
+
fail(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
|
|
406
|
+
}
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
|
|
410
|
+
fail(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
|
|
411
|
+
} else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
|
|
412
|
+
fail(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
|
|
413
|
+
}
|
|
414
|
+
for (const column of source.writeback?.columns ?? []) {
|
|
415
|
+
if (!source.columns.includes(column)) fail(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function validateSourceColumns({ scope, source, byName }) {
|
|
419
|
+
const { fail, pkColumn, dataColumns } = scope;
|
|
420
|
+
for (const column of [pkColumn.name, ...source.columns]) {
|
|
421
|
+
const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
|
|
422
|
+
const theirs = byName.get(column);
|
|
423
|
+
if (!own) fail(`source '${source.name}' supplies unknown column '${column}'`);
|
|
424
|
+
else if (!theirs) fail(`source '${source.name}' table '${source.table}' has no column '${column}'`);
|
|
425
|
+
else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail(`column '${column}' has a different type on source '${source.name}'`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail }) {
|
|
429
|
+
const ruled = /* @__PURE__ */ new Set();
|
|
430
|
+
for (const rule of resolved.resolve) {
|
|
431
|
+
if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
|
|
432
|
+
fail(`resolve rule targets unknown column '${rule.column}'`);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (ruled.has(rule.column)) fail(`column '${rule.column}' has more than one resolve rule`);
|
|
436
|
+
ruled.add(rule.column);
|
|
437
|
+
if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
|
|
438
|
+
fail(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
|
|
439
|
+
}
|
|
440
|
+
if (new Set(rule.candidates).size !== rule.candidates.length) fail(`column '${rule.column}' repeats a candidate`);
|
|
441
|
+
for (const candidate of rule.candidates) {
|
|
442
|
+
const source = sourceByName.get(candidate);
|
|
443
|
+
if (!source) fail(`column '${rule.column}' names unknown candidate source '${candidate}'`);
|
|
444
|
+
else if (!source.columns.includes(rule.column)) fail(`column '${rule.column}' candidate '${candidate}' does not supply it`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
for (const column of dataColumns.keys()) {
|
|
448
|
+
if (column !== pkColumn.name && !ruled.has(column)) fail(`column '${column}' has no resolve rule`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
var VIEW_CELL_TYPES = {
|
|
452
|
+
source: "text",
|
|
453
|
+
at: "timestamptz",
|
|
454
|
+
by: "text",
|
|
455
|
+
conflicts: "jsonb"
|
|
456
|
+
};
|
|
457
|
+
function validateViewCells({ view, dataColumns, pkColumn, fail }) {
|
|
458
|
+
const expected = /* @__PURE__ */ new Map();
|
|
459
|
+
for (const column of dataColumns.values()) {
|
|
460
|
+
if (column.name === pkColumn.name) continue;
|
|
461
|
+
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
462
|
+
expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
for (const column of view.columns) {
|
|
466
|
+
if (!column.cell) continue;
|
|
467
|
+
const want = expected.get(column.name);
|
|
468
|
+
if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
|
|
469
|
+
fail(`unexpected cell column '${column.name}'`);
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
|
|
473
|
+
expected.delete(column.name);
|
|
474
|
+
}
|
|
475
|
+
for (const name of expected.keys()) fail(`missing cell column '${name}'`);
|
|
476
|
+
}
|
|
477
|
+
var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
|
|
478
|
+
function validateMaterialized({ scope, tableByName, views }) {
|
|
479
|
+
const { view, resolved, fail } = scope;
|
|
480
|
+
const { table: tableName, compute: computeName } = resolved.materialized;
|
|
481
|
+
const table = tableByName.get(tableName);
|
|
482
|
+
if (!table || table.internal !== true || table.materializes !== view.view) {
|
|
483
|
+
fail(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
|
|
484
|
+
} else {
|
|
485
|
+
if (columnSignature(table.columns) !== columnSignature(view.columns)) fail(`materialized table '${tableName}' must have exactly the view's columns`);
|
|
486
|
+
if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail(`materialized table '${tableName}' must share the view's primary key`);
|
|
487
|
+
}
|
|
488
|
+
const compute = views.find((candidate) => candidate.view === computeName);
|
|
489
|
+
if (!compute || compute.internal !== true) fail(`compute view '${computeName}' must be an internal view`);
|
|
490
|
+
else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail(`compute view '${computeName}' must have exactly the view's columns`);
|
|
491
|
+
for (const source of resolved.sources) {
|
|
492
|
+
if (source.kind !== "etl") continue;
|
|
493
|
+
const read = tableByName.get(source.table)?.policies?.read;
|
|
494
|
+
const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
|
|
495
|
+
if (!unconditional) {
|
|
496
|
+
fail(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// src/canonical/v2.ts
|
|
35
502
|
var CANONICAL_FORMAT = 2;
|
|
36
503
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
37
|
-
var
|
|
504
|
+
var identifier2 = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
505
|
+
var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
|
|
38
506
|
var COLUMN_TYPES = [
|
|
39
507
|
"uuid",
|
|
40
508
|
"text",
|
|
@@ -49,59 +517,71 @@ var COLUMN_TYPES = [
|
|
|
49
517
|
];
|
|
50
518
|
var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
|
|
51
519
|
var canonicalEnumSchema = zod.z.object({
|
|
52
|
-
name:
|
|
520
|
+
name: identifier2,
|
|
53
521
|
values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
|
|
54
522
|
});
|
|
55
523
|
var canonicalColumnSchema = zod.z.object({
|
|
56
524
|
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
57
525
|
name: zod.z.string().min(1).max(63),
|
|
58
|
-
columnName:
|
|
59
|
-
type: zod.z.union([columnTypeSchema, zod.z.object({ enum:
|
|
526
|
+
columnName: columnIdentifier,
|
|
527
|
+
type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier2 })]),
|
|
60
528
|
nullable: zod.z.boolean().default(true),
|
|
61
529
|
unique: zod.z.boolean().default(false),
|
|
62
530
|
/** Literal default, or one of the allowlisted SQL functions. */
|
|
63
531
|
default: zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()]).optional(),
|
|
64
532
|
defaultFn: zod.z.enum(["gen_random_uuid()", "now()"]).optional(),
|
|
533
|
+
/**
|
|
534
|
+
* Stamp this column from the database clock on UPDATE (`now()`). The
|
|
535
|
+
* column stays insertable (defaults fill gaps) but is omitted from
|
|
536
|
+
* SetInput / settable so callers cannot backdate it.
|
|
537
|
+
*/
|
|
538
|
+
onUpdateFn: zod.z.enum(["now()"]).optional(),
|
|
65
539
|
description: zod.z.string().max(500).optional(),
|
|
66
540
|
/** Migration hints — same semantics as v1. */
|
|
67
|
-
renamedFrom:
|
|
541
|
+
renamedFrom: columnIdentifier.optional(),
|
|
68
542
|
cast: zod.z.string().max(200).optional(),
|
|
69
|
-
deprecated: zod.z.boolean().default(false)
|
|
543
|
+
deprecated: zod.z.boolean().default(false),
|
|
544
|
+
/**
|
|
545
|
+
* Marks a system cell-metadata column (see CELL_PARTS in constants): part
|
|
546
|
+
* `part` of the cell of logical column `of`. Synthesized by authoring,
|
|
547
|
+
* never client-writable, absent from GraphQL scalar fields. Omitted on
|
|
548
|
+
* ordinary columns so their hashes are unchanged.
|
|
549
|
+
*/
|
|
550
|
+
cell: zod.z.object({ of: zod.z.string().min(1).max(63), part: zod.z.enum(CELL_PARTS) }).strict().optional()
|
|
551
|
+
}).superRefine((column, ctx) => {
|
|
552
|
+
if (column.onUpdateFn === void 0) return;
|
|
553
|
+
if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
|
|
554
|
+
ctx.addIssue({
|
|
555
|
+
code: "custom",
|
|
556
|
+
path: ["onUpdateFn"],
|
|
557
|
+
message: `onUpdateFn '${column.onUpdateFn}' is only valid on timestamptz/date columns`
|
|
558
|
+
});
|
|
559
|
+
}
|
|
70
560
|
});
|
|
71
561
|
var canonicalLinkSchema = zod.z.object({
|
|
72
562
|
name: zod.z.string().min(1).max(63),
|
|
73
563
|
kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
|
|
74
564
|
/** Target table name (namespace-local). Absent for toPlatformUser. */
|
|
75
|
-
target:
|
|
565
|
+
target: identifier2.optional(),
|
|
76
566
|
/** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
|
|
77
|
-
on:
|
|
567
|
+
on: columnIdentifier,
|
|
78
568
|
/** Referenced column on the target (default: target's PK / platform user `sub`). */
|
|
79
|
-
references:
|
|
569
|
+
references: columnIdentifier.optional(),
|
|
80
570
|
/** enforce: true emits a real FK constraint (same-namespace toOne only). */
|
|
81
571
|
enforce: zod.z.boolean().default(false),
|
|
572
|
+
onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
573
|
+
onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
82
574
|
description: zod.z.string().max(500).optional()
|
|
83
575
|
});
|
|
84
576
|
var canonicalIndexSchema = zod.z.object({
|
|
85
|
-
columns: zod.z.array(
|
|
577
|
+
columns: zod.z.array(columnIdentifier).min(1),
|
|
86
578
|
unique: zod.z.boolean().default(false),
|
|
87
|
-
name:
|
|
88
|
-
});
|
|
89
|
-
var policyVariantSchema = zod.z.object({
|
|
90
|
-
/** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
|
|
91
|
-
when: zod.z.string().min(1).max(1e3),
|
|
92
|
-
/** Exactly one of: allow-all bypass, or a row predicate. */
|
|
93
|
-
allow: zod.z.literal(true).optional(),
|
|
94
|
-
policy: zod.z.string().min(1).max(2e3).optional()
|
|
95
|
-
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
96
|
-
message: "A variant declares exactly one of allow: true or policy"
|
|
579
|
+
name: identifier2.optional()
|
|
97
580
|
});
|
|
98
|
-
var
|
|
99
|
-
|
|
100
|
-
zod.z.
|
|
101
|
-
|
|
102
|
-
variants: zod.z.array(policyVariantSchema).max(10).default([])
|
|
103
|
-
})
|
|
104
|
-
]);
|
|
581
|
+
var canonicalCheckSchema = zod.z.object({
|
|
582
|
+
name: identifier2,
|
|
583
|
+
expression: zod.z.string().min(1).max(2e3)
|
|
584
|
+
}).strict();
|
|
105
585
|
var canonicalPoliciesSchema = zod.z.object({
|
|
106
586
|
read: policyExpressionSchema.optional(),
|
|
107
587
|
write: policyExpressionSchema.optional(),
|
|
@@ -132,7 +612,7 @@ function isSingleViewQuerySql(value) {
|
|
|
132
612
|
return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
|
|
133
613
|
}
|
|
134
614
|
var canonicalActionSchema = zod.z.object({
|
|
135
|
-
name:
|
|
615
|
+
name: identifier2,
|
|
136
616
|
requires: zod.z.string().max(1e3).optional(),
|
|
137
617
|
params: zod.z.record(zod.z.string(), zod.z.object({
|
|
138
618
|
type: columnTypeSchema,
|
|
@@ -144,9 +624,9 @@ var canonicalActionSchema = zod.z.object({
|
|
|
144
624
|
});
|
|
145
625
|
var canonicalCowMutationSchema = zod.z.object({
|
|
146
626
|
kind: zod.z.enum(["override", "revert"]),
|
|
147
|
-
name:
|
|
627
|
+
name: identifier2,
|
|
148
628
|
/** Synced table (same namespace) the overlay copies from / falls back to. */
|
|
149
|
-
synced:
|
|
629
|
+
synced: identifier2,
|
|
150
630
|
/** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
|
|
151
631
|
pk: zod.z.string().min(1).max(63),
|
|
152
632
|
/** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
|
|
@@ -173,17 +653,28 @@ var canonicalCowMutationSchema = zod.z.object({
|
|
|
173
653
|
}
|
|
174
654
|
}
|
|
175
655
|
});
|
|
656
|
+
var canonicalForeignKeySchema = zod.z.object({
|
|
657
|
+
name: identifier2,
|
|
658
|
+
columns: zod.z.array(columnIdentifier).min(1).max(8),
|
|
659
|
+
target: identifier2,
|
|
660
|
+
references: zod.z.array(columnIdentifier).min(1).max(8),
|
|
661
|
+
deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional(),
|
|
662
|
+
onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
663
|
+
onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
|
|
664
|
+
}).strict();
|
|
176
665
|
var canonicalTableSchema = zod.z.object({
|
|
177
|
-
table:
|
|
666
|
+
table: identifier2,
|
|
178
667
|
description: zod.z.string().max(2e3).optional(),
|
|
179
668
|
/** Table-level rename hint (new in v2). */
|
|
180
|
-
renamedFrom:
|
|
669
|
+
renamedFrom: identifier2.optional(),
|
|
181
670
|
/** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
|
|
182
671
|
source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
|
|
183
672
|
columns: zod.z.array(canonicalColumnSchema).default([]),
|
|
184
|
-
primaryKey: zod.z.array(
|
|
673
|
+
primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
|
|
185
674
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
186
675
|
indexes: zod.z.array(canonicalIndexSchema).default([]),
|
|
676
|
+
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
677
|
+
checks: zod.z.array(canonicalCheckSchema).max(100).optional(),
|
|
187
678
|
policies: canonicalPoliciesSchema.default({}),
|
|
188
679
|
/**
|
|
189
680
|
* v1-conversion carry-over only. v2 has no action surface — the runtime
|
|
@@ -192,26 +683,64 @@ var canonicalTableSchema = zod.z.object({
|
|
|
192
683
|
*/
|
|
193
684
|
actions: zod.z.array(canonicalActionSchema).default([]),
|
|
194
685
|
/** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
|
|
195
|
-
cowMutations: zod.z.array(canonicalCowMutationSchema).optional()
|
|
686
|
+
cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
|
|
687
|
+
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
688
|
+
foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional(),
|
|
689
|
+
/**
|
|
690
|
+
* Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
|
|
691
|
+
* column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
|
|
692
|
+
* on a resolved table's internal sources), listed explicitly in `columns`
|
|
693
|
+
* with a `cell` marker so DDL and migrations treat them as ordinary columns.
|
|
694
|
+
* Omitted when off so existing definitions hash unchanged.
|
|
695
|
+
*/
|
|
696
|
+
cellMetadata: zod.z.literal(true).optional(),
|
|
697
|
+
/**
|
|
698
|
+
* System-owned backing table of a resolved table (its manual or prefetch
|
|
699
|
+
* source). Internal tables get DDL and IR metadata but no GraphQL surface;
|
|
700
|
+
* only the data plane writes them. Omitted when false.
|
|
701
|
+
*/
|
|
702
|
+
internal: zod.z.literal(true).optional(),
|
|
703
|
+
/**
|
|
704
|
+
* The materialized copy of resolved table `materializes` (internal): the
|
|
705
|
+
* same columns as that view, including its cell parts, maintained by the
|
|
706
|
+
* data plane. Omitted otherwise.
|
|
707
|
+
*/
|
|
708
|
+
materializes: identifier2.optional()
|
|
196
709
|
});
|
|
710
|
+
var MAX_VIEW_QUERY = 1e5;
|
|
711
|
+
var MAX_RESOLVED_VIEW_QUERY = 5e6;
|
|
197
712
|
var canonicalViewSchema = zod.z.object({
|
|
198
|
-
view:
|
|
713
|
+
view: identifier2,
|
|
199
714
|
description: zod.z.string().max(2e3).optional(),
|
|
200
715
|
columns: zod.z.array(canonicalColumnSchema).min(1),
|
|
201
716
|
/** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
|
|
202
|
-
primaryKey: zod.z.array(
|
|
717
|
+
primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
|
|
203
718
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
204
719
|
policies: zod.z.object({ read: policyExpressionSchema }).strict(),
|
|
205
|
-
query: zod.z.string().min(1).max(
|
|
720
|
+
query: zod.z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
|
|
206
721
|
message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
|
|
207
722
|
}),
|
|
208
723
|
/** Namespace-local logical relation names, sorted for deterministic hashing. */
|
|
209
|
-
dependencies: zod.z.array(
|
|
724
|
+
dependencies: zod.z.array(identifier2).max(500),
|
|
725
|
+
/**
|
|
726
|
+
* Present when this view is a resolved table (defineResolvedTable): the
|
|
727
|
+
* multi-source metadata behind its generated query. Omitted otherwise so
|
|
728
|
+
* existing views hash unchanged.
|
|
729
|
+
*/
|
|
730
|
+
resolved: canonicalResolvedSchema.optional(),
|
|
731
|
+
/** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
|
|
732
|
+
internal: zod.z.literal(true).optional()
|
|
210
733
|
}).superRefine((view, ctx) => {
|
|
734
|
+
if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
|
|
735
|
+
ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
|
|
736
|
+
}
|
|
211
737
|
for (const [index, column] of view.columns.entries()) {
|
|
212
738
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
213
739
|
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
|
|
214
740
|
}
|
|
741
|
+
if (column.onUpdateFn !== void 0) {
|
|
742
|
+
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare onUpdateFn` });
|
|
743
|
+
}
|
|
215
744
|
if (column.unique) {
|
|
216
745
|
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare unique` });
|
|
217
746
|
}
|
|
@@ -219,15 +748,82 @@ var canonicalViewSchema = zod.z.object({
|
|
|
219
748
|
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare migration hints` });
|
|
220
749
|
}
|
|
221
750
|
}
|
|
751
|
+
if (view.resolved === void 0 && view.internal !== true) {
|
|
752
|
+
for (const [index, column] of view.columns.entries()) {
|
|
753
|
+
if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index], message: `only resolved tables carry cell columns ('${column.name}')` });
|
|
754
|
+
}
|
|
755
|
+
}
|
|
222
756
|
for (const [index, link] of view.links.entries()) {
|
|
223
757
|
if (link.enforce) {
|
|
224
758
|
ctx.addIssue({ code: "custom", path: ["links", index, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
|
|
225
759
|
}
|
|
760
|
+
if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
|
|
761
|
+
ctx.addIssue({ code: "custom", path: ["links", index], message: `view link '${link.name}' cannot declare referential actions` });
|
|
762
|
+
}
|
|
226
763
|
}
|
|
227
764
|
});
|
|
765
|
+
var CELL_PART_TYPES = {
|
|
766
|
+
at: "timestamptz",
|
|
767
|
+
by: "text",
|
|
768
|
+
rev: "uuid",
|
|
769
|
+
source: "text",
|
|
770
|
+
conflicts: "jsonb"
|
|
771
|
+
};
|
|
772
|
+
function trackedCellColumns(table) {
|
|
773
|
+
const pk = new Set(table.primaryKey ?? []);
|
|
774
|
+
return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
|
|
775
|
+
}
|
|
776
|
+
var SUFFIXES = Object.values(CELL_PART_SUFFIX);
|
|
777
|
+
function validateCellMetadata(table, index, ctx) {
|
|
778
|
+
const fail = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
|
|
779
|
+
const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
|
|
780
|
+
if (table.materializes !== void 0) return;
|
|
781
|
+
if (table.cellMetadata !== true) {
|
|
782
|
+
for (const { position } of cells) fail(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index, "columns", position]);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (table.source !== void 0) {
|
|
786
|
+
fail(`synced table '${table.table}' cannot track cell metadata`);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
const tracked = trackedCellColumns(table);
|
|
790
|
+
const byName = new Map(tracked.map((column) => [column.name, column]));
|
|
791
|
+
const withRev = cells.some(({ cell }) => cell.part === "rev");
|
|
792
|
+
if (withRev && table.internal !== true) fail(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
|
|
793
|
+
const seen = /* @__PURE__ */ new Set();
|
|
794
|
+
for (const { column, position, cell } of cells) {
|
|
795
|
+
const path = ["tables", index, "columns", position];
|
|
796
|
+
const owner = byName.get(cell.of);
|
|
797
|
+
if (!owner) {
|
|
798
|
+
fail(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
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);
|
|
802
|
+
if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
|
|
803
|
+
fail(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
|
|
804
|
+
}
|
|
805
|
+
if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
|
|
806
|
+
fail(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
|
|
807
|
+
}
|
|
808
|
+
const key = `${cell.of}:${cell.part}`;
|
|
809
|
+
if (seen.has(key)) fail(`duplicate cell column '${column.name}'`, path);
|
|
810
|
+
seen.add(key);
|
|
811
|
+
}
|
|
812
|
+
for (const column of tracked) {
|
|
813
|
+
if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
|
|
814
|
+
fail(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
|
|
815
|
+
}
|
|
816
|
+
if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
|
|
817
|
+
fail(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
|
|
818
|
+
}
|
|
819
|
+
for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
|
|
820
|
+
if (!seen.has(`${column.name}:${part}`)) fail(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
228
824
|
zod.z.object({
|
|
229
825
|
definition_format: zod.z.literal(CANONICAL_FORMAT),
|
|
230
|
-
namespace:
|
|
826
|
+
namespace: identifier2.max(41),
|
|
231
827
|
description: zod.z.string().max(2e3).optional(),
|
|
232
828
|
enums: zod.z.array(canonicalEnumSchema).default([]),
|
|
233
829
|
tables: zod.z.array(canonicalTableSchema).min(0).max(500),
|
|
@@ -245,6 +841,74 @@ zod.z.object({
|
|
|
245
841
|
ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
|
|
246
842
|
}
|
|
247
843
|
seen.add(table.table);
|
|
844
|
+
validateCellMetadata(table, index, ctx);
|
|
845
|
+
if (table.source !== void 0) {
|
|
846
|
+
for (const [columnIndex, column] of table.columns.entries()) {
|
|
847
|
+
if (column.onUpdateFn !== void 0) {
|
|
848
|
+
ctx.addIssue({
|
|
849
|
+
code: "custom",
|
|
850
|
+
path: ["tables", index, "columns", columnIndex],
|
|
851
|
+
message: `synced table '${table.table}' column '${column.name}' cannot declare onUpdateFn`
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
858
|
+
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
859
|
+
const names = /* @__PURE__ */ new Set([
|
|
860
|
+
...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
|
|
861
|
+
...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
|
|
862
|
+
...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
|
|
863
|
+
]);
|
|
864
|
+
for (const [index, link] of table.links.entries()) {
|
|
865
|
+
if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
|
|
866
|
+
ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "referential actions require an enforced toOne link" });
|
|
867
|
+
}
|
|
868
|
+
if (link.onDelete === "set null" || link.onUpdate === "set null") {
|
|
869
|
+
const source = table.columns.find((column) => column.columnName === link.on);
|
|
870
|
+
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" });
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
for (const [index, check] of (table.checks ?? []).entries()) {
|
|
874
|
+
const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index], message });
|
|
875
|
+
if (table.source) fail("synced tables cannot declare checks");
|
|
876
|
+
if (names.has(check.name)) fail(`duplicate constraint '${check.name}'`);
|
|
877
|
+
names.add(check.name);
|
|
878
|
+
try {
|
|
879
|
+
compileCheckExpression(check.expression, table.columns);
|
|
880
|
+
} catch (error) {
|
|
881
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
|
|
885
|
+
const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
|
|
886
|
+
if (names.has(fk.name)) fail(`duplicate foreign key '${fk.name}'`);
|
|
887
|
+
names.add(fk.name);
|
|
888
|
+
const target = fkTables.get(fk.target);
|
|
889
|
+
if (table.source || !target || target.source) {
|
|
890
|
+
fail("foreign keys must connect owned tables in the same namespace");
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
if (fk.columns.length !== fk.references.length) fail("foreign key columns and references must have the same number of columns");
|
|
894
|
+
if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail("foreign key columns must not repeat");
|
|
895
|
+
for (const [position, name] of fk.columns.entries()) {
|
|
896
|
+
const sourceColumn = table.columns.find((column) => column.columnName === name);
|
|
897
|
+
const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
|
|
898
|
+
if (!sourceColumn || !targetColumn) fail("foreign key references an unknown column");
|
|
899
|
+
else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail("foreign key column types must match");
|
|
900
|
+
}
|
|
901
|
+
if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
|
|
902
|
+
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");
|
|
903
|
+
}
|
|
904
|
+
const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index2) => index2.unique).map((index2) => index2.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
|
|
905
|
+
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");
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
for (const [viewIndex, view] of (definition.views ?? []).entries()) {
|
|
909
|
+
for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
|
|
910
|
+
ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
|
|
911
|
+
}
|
|
248
912
|
}
|
|
249
913
|
const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
250
914
|
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
@@ -363,7 +1027,10 @@ zod.z.object({
|
|
|
363
1027
|
});
|
|
364
1028
|
|
|
365
1029
|
// src/ir/index.ts
|
|
366
|
-
var IR_VERSION =
|
|
1030
|
+
var IR_VERSION = 2;
|
|
1031
|
+
var LEGACY_IR_VERSION = 1;
|
|
1032
|
+
var irVersionSchema = zod.z.union([zod.z.literal(LEGACY_IR_VERSION), zod.z.literal(IR_VERSION)]);
|
|
1033
|
+
zod.z.object({ irVersion: irVersionSchema });
|
|
367
1034
|
var CLAIMS_CONTRACT = {
|
|
368
1035
|
sub: "string",
|
|
369
1036
|
org: "string?",
|
|
@@ -435,6 +1102,21 @@ var selectionIrSchema = zod.z.lazy(
|
|
|
435
1102
|
() => zod.z.object({
|
|
436
1103
|
typename: zod.z.string(),
|
|
437
1104
|
columns: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })),
|
|
1105
|
+
cells: zod.z.object({
|
|
1106
|
+
alias: zod.z.string(),
|
|
1107
|
+
typename: zod.z.string(),
|
|
1108
|
+
fields: zod.z.array(zod.z.object({
|
|
1109
|
+
alias: zod.z.string(),
|
|
1110
|
+
column: zod.z.string(),
|
|
1111
|
+
typename: zod.z.string(),
|
|
1112
|
+
parts: zod.z.array(zod.z.object({
|
|
1113
|
+
alias: zod.z.string(),
|
|
1114
|
+
part: zod.z.enum(["source", "at", "by", "conflicts"]),
|
|
1115
|
+
fields: zod.z.array(zod.z.object({ alias: zod.z.string(), field: zod.z.enum(["source", "at", "value"]) })).optional(),
|
|
1116
|
+
typename: zod.z.string().optional()
|
|
1117
|
+
}))
|
|
1118
|
+
}))
|
|
1119
|
+
}).optional(),
|
|
438
1120
|
relations: zod.z.array(zod.z.object({
|
|
439
1121
|
alias: zod.z.string(),
|
|
440
1122
|
link: zod.z.string(),
|
|
@@ -456,6 +1138,11 @@ var selectionIrSchema = zod.z.lazy(
|
|
|
456
1138
|
})
|
|
457
1139
|
);
|
|
458
1140
|
var includeIfSchema = zod.z.object({ variable: zod.z.string(), negated: zod.z.boolean() });
|
|
1141
|
+
var prefetchPlanIrSchema = zod.z.object({
|
|
1142
|
+
source: zod.z.string(),
|
|
1143
|
+
maxStalenessMs: zod.z.number().int().positive().optional(),
|
|
1144
|
+
key: zod.z.union([zod.z.object({ one: valueIrSchema }), zod.z.object({ many: valueIrSchema })])
|
|
1145
|
+
});
|
|
459
1146
|
var selectRootSchema = zod.z.object({
|
|
460
1147
|
kind: zod.z.literal("select"),
|
|
461
1148
|
alias: zod.z.string(),
|
|
@@ -467,7 +1154,8 @@ var selectRootSchema = zod.z.object({
|
|
|
467
1154
|
distinctOn: zod.z.union([zod.z.array(zod.z.string()), zod.z.object({ slot: zod.z.string() })]).optional(),
|
|
468
1155
|
limit: limitIrSchema,
|
|
469
1156
|
offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
|
|
470
|
-
includeIf: includeIfSchema.optional()
|
|
1157
|
+
includeIf: includeIfSchema.optional(),
|
|
1158
|
+
prefetch: zod.z.array(prefetchPlanIrSchema).optional()
|
|
471
1159
|
});
|
|
472
1160
|
var byPkRootSchema = zod.z.object({
|
|
473
1161
|
kind: zod.z.literal("byPk"),
|
|
@@ -477,7 +1165,8 @@ var byPkRootSchema = zod.z.object({
|
|
|
477
1165
|
pk: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).min(1),
|
|
478
1166
|
/** Policy predicate — ANDed with the PK match. */
|
|
479
1167
|
where: predIrSchema,
|
|
480
|
-
includeIf: includeIfSchema.optional()
|
|
1168
|
+
includeIf: includeIfSchema.optional(),
|
|
1169
|
+
prefetch: zod.z.array(prefetchPlanIrSchema).optional()
|
|
481
1170
|
});
|
|
482
1171
|
var aggregateRootSchema = zod.z.object({
|
|
483
1172
|
kind: zod.z.literal("aggregate"),
|
|
@@ -490,6 +1179,33 @@ var aggregateRootSchema = zod.z.object({
|
|
|
490
1179
|
nodes: selectionIrSchema.optional(),
|
|
491
1180
|
includeIf: includeIfSchema.optional()
|
|
492
1181
|
});
|
|
1182
|
+
var DATE_BUCKETS = ["year", "quarter", "month", "week", "day", "hour"];
|
|
1183
|
+
var groupByKeyIrSchema = zod.z.object({
|
|
1184
|
+
column: zod.z.string(),
|
|
1185
|
+
bucket: zod.z.enum(DATE_BUCKETS).optional()
|
|
1186
|
+
});
|
|
1187
|
+
var aggregateGroupsRootSchema = zod.z.object({
|
|
1188
|
+
kind: zod.z.literal("aggregateGroups"),
|
|
1189
|
+
alias: zod.z.string(),
|
|
1190
|
+
table: zod.z.string(),
|
|
1191
|
+
/** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
|
|
1192
|
+
where: predIrSchema,
|
|
1193
|
+
groupBy: zod.z.array(groupByKeyIrSchema).min(1),
|
|
1194
|
+
/**
|
|
1195
|
+
* The `key { … }` projection: which group columns appear in each entry's key
|
|
1196
|
+
* object, in selection order. A subset of `groupBy` (the compiler rejects a
|
|
1197
|
+
* key field that is not a group key — it would be indeterminate within the
|
|
1198
|
+
* group). Empty when the document selects only `aggregate`.
|
|
1199
|
+
*/
|
|
1200
|
+
key: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })).default([]),
|
|
1201
|
+
count: zod.z.object({ columns: zod.z.array(zod.z.string()).optional(), distinct: zod.z.boolean().optional() }).optional(),
|
|
1202
|
+
sum: zod.z.array(zod.z.string()).optional(),
|
|
1203
|
+
avg: zod.z.array(zod.z.string()).optional(),
|
|
1204
|
+
/** Bounds the GROUP count, not the scanned rows — see MAX_GROUPS. */
|
|
1205
|
+
limit: limitIrSchema,
|
|
1206
|
+
offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
|
|
1207
|
+
includeIf: includeIfSchema.optional()
|
|
1208
|
+
});
|
|
493
1209
|
var writeValuesSchema = zod.z.union([
|
|
494
1210
|
zod.z.object({ slot: zod.z.string() }),
|
|
495
1211
|
zod.z.object({ entries: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })) })
|
|
@@ -508,7 +1224,20 @@ var mutationRootSchema = zod.z.object({
|
|
|
508
1224
|
* coalesce view falls back to synced. Both are gated on a matching synced
|
|
509
1225
|
* row and carry the write policy in `where` like update/delete.
|
|
510
1226
|
*/
|
|
511
|
-
op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
|
|
1227
|
+
op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert", "resolvedWrite"]),
|
|
1228
|
+
/**
|
|
1229
|
+
* resolvedWrite: a write to a resolved table (`table` is its view). The
|
|
1230
|
+
* executor gates on the resolved row (write policy in `where`), writes only
|
|
1231
|
+
* the touched cells into the manual source, then re-checks `insertCheck`
|
|
1232
|
+
* against the resolved row. `revert` clears `revertColumns` from the manual
|
|
1233
|
+
* source; `delete` tombstones the row there.
|
|
1234
|
+
*/
|
|
1235
|
+
resolvedAction: zod.z.enum(["insert", "update", "delete", "revert"]).optional(),
|
|
1236
|
+
revertColumns: zod.z.array(zod.z.string()).optional(),
|
|
1237
|
+
/** `@writeback(source:, mode:)` directives on this root. */
|
|
1238
|
+
writeback: zod.z.array(zod.z.object({ source: zod.z.string(), mode: zod.z.enum(["sync", "async"]) })).optional(),
|
|
1239
|
+
/** `@localOnly(source:)` — writeback columns deliberately kept local for these sources. */
|
|
1240
|
+
localOnly: zod.z.array(zod.z.string()).optional(),
|
|
512
1241
|
alias: zod.z.string(),
|
|
513
1242
|
table: zod.z.string(),
|
|
514
1243
|
/** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
|
|
@@ -535,12 +1264,15 @@ var mutationRootSchema = zod.z.object({
|
|
|
535
1264
|
insertCheck: predIrSchema.optional(),
|
|
536
1265
|
returning: selectionIrSchema.nullable(),
|
|
537
1266
|
affectedRows: zod.z.boolean().default(false),
|
|
1267
|
+
/** v2: abort the transaction when the actual write count differs, regardless of selection. */
|
|
1268
|
+
assertAffectedRows: zod.z.number().int().min(0).max(2147483647).optional(),
|
|
538
1269
|
includeIf: includeIfSchema.optional()
|
|
539
1270
|
});
|
|
540
1271
|
var rootIrSchema = zod.z.discriminatedUnion("kind", [
|
|
541
1272
|
selectRootSchema,
|
|
542
1273
|
byPkRootSchema,
|
|
543
1274
|
aggregateRootSchema,
|
|
1275
|
+
aggregateGroupsRootSchema,
|
|
544
1276
|
mutationRootSchema
|
|
545
1277
|
]);
|
|
546
1278
|
var INPUT_REF_KINDS = [
|
|
@@ -570,7 +1302,7 @@ var variableIrSchema = zod.z.object({
|
|
|
570
1302
|
defaultValue: variableDefaultIrSchema.optional()
|
|
571
1303
|
});
|
|
572
1304
|
var compiledOperationSchema = zod.z.object({
|
|
573
|
-
irVersion:
|
|
1305
|
+
irVersion: irVersionSchema,
|
|
574
1306
|
name: zod.z.string(),
|
|
575
1307
|
opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
576
1308
|
kind: zod.z.enum(["query", "mutation"]),
|
|
@@ -582,6 +1314,50 @@ var compiledOperationSchema = zod.z.object({
|
|
|
582
1314
|
/** Every table the op touches — data-token scoping + audit. */
|
|
583
1315
|
tableSet: zod.z.array(zod.z.string()).min(1),
|
|
584
1316
|
roots: zod.z.array(rootIrSchema).min(1)
|
|
1317
|
+
}).superRefine((operation, ctx) => {
|
|
1318
|
+
for (const [index, root] of operation.roots.entries()) {
|
|
1319
|
+
if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
|
|
1320
|
+
if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
|
|
1321
|
+
ctx.addIssue({ code: "custom", path: ["roots", index], message: "affected-row assertions require unconditional mutation roots and IR v2" });
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
var resolvedSourceIrSchema = zod.z.object({
|
|
1326
|
+
name: zod.z.string(),
|
|
1327
|
+
kind: zod.z.enum(["manual", "etl", "prefetch"]),
|
|
1328
|
+
/** Logical table key of the backing relation. */
|
|
1329
|
+
table: zod.z.string(),
|
|
1330
|
+
columns: zod.z.array(zod.z.string()),
|
|
1331
|
+
key: zod.z.string().optional(),
|
|
1332
|
+
fetch: zod.z.union([
|
|
1333
|
+
zod.z.object({ shape: zod.z.literal("single") }),
|
|
1334
|
+
zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().positive() })
|
|
1335
|
+
]).optional(),
|
|
1336
|
+
function: zod.z.object({ kind: zod.z.enum(["managed", "native"]), id: zod.z.string() }).optional(),
|
|
1337
|
+
prefetchDefaults: zod.z.object({
|
|
1338
|
+
maxStalenessMs: zod.z.number().int().positive(),
|
|
1339
|
+
timeoutMs: zod.z.number().int().positive(),
|
|
1340
|
+
onError: zod.z.enum(["fallback", "fail"])
|
|
1341
|
+
}).optional(),
|
|
1342
|
+
writeback: zod.z.object({
|
|
1343
|
+
columns: zod.z.array(zod.z.string()),
|
|
1344
|
+
defaultMode: zod.z.enum(["sync", "async"]),
|
|
1345
|
+
timeoutMs: zod.z.number().int().positive(),
|
|
1346
|
+
clearManualOnConfirm: zod.z.boolean()
|
|
1347
|
+
}).optional()
|
|
1348
|
+
});
|
|
1349
|
+
var resolvedMetaIrSchema = zod.z.object({
|
|
1350
|
+
/** Logical pk column of the resolved view (single-column). */
|
|
1351
|
+
pk: zod.z.string(),
|
|
1352
|
+
/** Source name of the manual source. */
|
|
1353
|
+
manual: zod.z.string(),
|
|
1354
|
+
sources: zod.z.array(resolvedSourceIrSchema),
|
|
1355
|
+
deletedBy: zod.z.array(zod.z.string()),
|
|
1356
|
+
/**
|
|
1357
|
+
* Materialized: the view reads `table`, which the data plane refreshes from
|
|
1358
|
+
* the `compute` view (claims-gated) whenever it writes a source row.
|
|
1359
|
+
*/
|
|
1360
|
+
materialized: zod.z.object({ table: zod.z.string(), compute: zod.z.string() }).optional()
|
|
585
1361
|
});
|
|
586
1362
|
var tableMetaIrSchema = zod.z.object({
|
|
587
1363
|
table: zod.z.string(),
|
|
@@ -618,18 +1394,45 @@ var tableMetaIrSchema = zod.z.object({
|
|
|
618
1394
|
insertable: zod.z.array(zod.z.string()),
|
|
619
1395
|
settable: zod.z.array(zod.z.string()),
|
|
620
1396
|
appendable: zod.z.array(zod.z.string()),
|
|
1397
|
+
/**
|
|
1398
|
+
* Logical columns stamped with Postgres `now()` on UPDATE (canonical
|
|
1399
|
+
* `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
|
|
1400
|
+
*/
|
|
1401
|
+
touchOnUpdate: zod.z.array(zod.z.string()).optional(),
|
|
1402
|
+
/**
|
|
1403
|
+
* Per-cell metadata: logical column → the logical names of its cell-part
|
|
1404
|
+
* columns (which are listed in `columns`, but never filterable, orderable or
|
|
1405
|
+
* writable). Tables stamp `at`/`by` (and a manual source `rev`) on every
|
|
1406
|
+
* write; resolved views expose `source`/`at`/`by`/`conflicts`. Omitted when
|
|
1407
|
+
* the relation tracks no cells.
|
|
1408
|
+
*/
|
|
1409
|
+
cells: zod.z.record(zod.z.string(), zod.z.object({
|
|
1410
|
+
at: zod.z.string().optional(),
|
|
1411
|
+
by: zod.z.string().optional(),
|
|
1412
|
+
rev: zod.z.string().optional(),
|
|
1413
|
+
source: zod.z.string().optional(),
|
|
1414
|
+
conflicts: zod.z.string().optional()
|
|
1415
|
+
}).strict()).optional(),
|
|
1416
|
+
/** Resolved-table view metadata (sources, prefetch/writeback config) for the data plane. */
|
|
1417
|
+
resolved: resolvedMetaIrSchema.optional(),
|
|
621
1418
|
constraints: zod.z.array(zod.z.object({ name: zod.z.string(), columns: zod.z.array(zod.z.string()) })),
|
|
622
1419
|
/** Depth/node caps for dynamic filter trees on this table. */
|
|
623
1420
|
dynamicFilterMaxDepth: zod.z.number().int().positive().default(6),
|
|
624
1421
|
dynamicFilterMaxNodes: zod.z.number().int().positive().default(64)
|
|
625
1422
|
});
|
|
626
1423
|
zod.z.object({
|
|
627
|
-
irVersion:
|
|
1424
|
+
irVersion: irVersionSchema,
|
|
628
1425
|
namespace: zod.z.string(),
|
|
629
1426
|
schemaHash: zod.z.string(),
|
|
630
1427
|
opsHash: zod.z.string(),
|
|
631
1428
|
tables: zod.z.record(zod.z.string(), tableMetaIrSchema),
|
|
632
1429
|
operations: zod.z.record(zod.z.string(), compiledOperationSchema)
|
|
1430
|
+
}).superRefine((artifact, ctx) => {
|
|
1431
|
+
for (const [name, operation] of Object.entries(artifact.operations)) {
|
|
1432
|
+
if (operation.irVersion > artifact.irVersion) {
|
|
1433
|
+
ctx.addIssue({ code: "custom", path: ["operations", name, "irVersion"], message: "operation requires a newer IR version than its artifact" });
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
633
1436
|
});
|
|
634
1437
|
|
|
635
1438
|
// src/predicate-v2/parse.ts
|
|
@@ -640,7 +1443,7 @@ var IDENTIFIER_CHAR = /[a-zA-Z0-9_]/;
|
|
|
640
1443
|
var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
|
|
641
1444
|
var SCALAR_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => !type.endsWith("[]")).map(([key]) => key);
|
|
642
1445
|
var ARRAY_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => type.endsWith("[]")).map(([key]) => key);
|
|
643
|
-
function
|
|
1446
|
+
function tokenize2(source) {
|
|
644
1447
|
const tokens = [];
|
|
645
1448
|
let position = 0;
|
|
646
1449
|
while (position < source.length) {
|
|
@@ -719,7 +1522,7 @@ function readString(source, start) {
|
|
|
719
1522
|
}
|
|
720
1523
|
throw new PolicyParseError("unterminated string literal", start);
|
|
721
1524
|
}
|
|
722
|
-
var
|
|
1525
|
+
var Parser2 = class {
|
|
723
1526
|
constructor(tokens) {
|
|
724
1527
|
this.tokens = tokens;
|
|
725
1528
|
}
|
|
@@ -893,7 +1696,7 @@ var Parser = class {
|
|
|
893
1696
|
function parsePolicy(source) {
|
|
894
1697
|
const trimmed = source.trim();
|
|
895
1698
|
if (trimmed.length === 0) throw new PolicyParseError("empty policy expression", 0);
|
|
896
|
-
return { source: trimmed, ast: new
|
|
1699
|
+
return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
|
|
897
1700
|
}
|
|
898
1701
|
|
|
899
1702
|
// src/predicate-v2/compile.ts
|