@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/compiler.cjs
CHANGED
|
@@ -7,9 +7,26 @@ var graphql = require('graphql');
|
|
|
7
7
|
// src/compiler-gql/artifact.ts
|
|
8
8
|
|
|
9
9
|
// src/constants.ts
|
|
10
|
-
var
|
|
10
|
+
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
11
|
+
var MAX_IDENTIFIER_LENGTH = 63;
|
|
12
|
+
var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
|
|
11
13
|
var ORM_SCHEMA_PREFIX = "orm_";
|
|
12
14
|
var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
|
|
15
|
+
var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
|
|
16
|
+
var CELL_PART_SUFFIX = {
|
|
17
|
+
at: "__at",
|
|
18
|
+
by: "__by",
|
|
19
|
+
rev: "__rev",
|
|
20
|
+
source: "__src",
|
|
21
|
+
conflicts: "__lost"
|
|
22
|
+
};
|
|
23
|
+
var MAX_TRACKED_COLUMN_NAME = MAX_IDENTIFIER_LENGTH - 6;
|
|
24
|
+
var CELLS_FIELD = "cells";
|
|
25
|
+
function cellColumnName(column, part) {
|
|
26
|
+
return `${column}${CELL_PART_SUFFIX[part]}`;
|
|
27
|
+
}
|
|
28
|
+
var MATERIALIZE_ACTOR = "orm:materialize";
|
|
29
|
+
JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
|
|
13
30
|
|
|
14
31
|
// src/hash.ts
|
|
15
32
|
function stableJson(value) {
|
|
@@ -18,9 +35,469 @@ function stableJson(value) {
|
|
|
18
35
|
const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== void 0).sort(([a], [b]) => a < b ? -1 : 1).map(([key, entryValue]) => `${JSON.stringify(key)}:${stableJson(entryValue)}`);
|
|
19
36
|
return `{${entries.join(",")}}`;
|
|
20
37
|
}
|
|
38
|
+
var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
39
|
+
var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
40
|
+
|
|
41
|
+
// src/checks/compile.ts
|
|
42
|
+
function tokenize(source) {
|
|
43
|
+
const tokens = [];
|
|
44
|
+
for (let position = 0; position < source.length; ) {
|
|
45
|
+
const char = source[position];
|
|
46
|
+
if (/\s/.test(char)) {
|
|
47
|
+
position++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (char === "(" || char === ")" || char === ",") {
|
|
51
|
+
tokens.push({ kind: char === "(" ? "lparen" : char === ")" ? "rparen" : "comma", value: char, position });
|
|
52
|
+
position++;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (char === "'") {
|
|
56
|
+
const start = position++;
|
|
57
|
+
let value = "";
|
|
58
|
+
let closed = false;
|
|
59
|
+
while (position < source.length) {
|
|
60
|
+
if (source[position] !== "'") {
|
|
61
|
+
value += source[position++];
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (source[position + 1] === "'") {
|
|
65
|
+
value += "'";
|
|
66
|
+
position += 2;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
position++;
|
|
70
|
+
closed = true;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
if (!closed) throw new Error(`unterminated string at position ${start}`);
|
|
74
|
+
if (/[\x00-\x1f\x7f]/.test(value)) throw new Error(`control character in string at position ${start}`);
|
|
75
|
+
tokens.push({ kind: "string", value, position: start });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const operator = /^(<=|>=|<>|!=|!~|=|<|>|~)/.exec(source.slice(position));
|
|
79
|
+
if (operator) {
|
|
80
|
+
tokens.push({ kind: "operator", value: operator[0] === "<>" ? "!=" : operator[0], position });
|
|
81
|
+
position += operator[0].length;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const number = /^-?\d+(?:\.\d+)?/.exec(source.slice(position));
|
|
85
|
+
if (number) {
|
|
86
|
+
tokens.push({ kind: "number", value: number[0], position });
|
|
87
|
+
position += number[0].length;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const identifier3 = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(source.slice(position));
|
|
91
|
+
if (identifier3) {
|
|
92
|
+
tokens.push({ kind: "identifier", value: identifier3[0], position });
|
|
93
|
+
position += identifier3[0].length;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`unexpected character '${char}' at position ${position}`);
|
|
97
|
+
}
|
|
98
|
+
tokens.push({ kind: "end", value: "", position: source.length });
|
|
99
|
+
if (tokens.length > 256) throw new Error("check expression exceeds 256 tokens");
|
|
100
|
+
return tokens;
|
|
101
|
+
}
|
|
102
|
+
var Parser = class {
|
|
103
|
+
constructor(tokens, columns) {
|
|
104
|
+
this.tokens = tokens;
|
|
105
|
+
this.columns = /* @__PURE__ */ new Map();
|
|
106
|
+
for (const column of columns) {
|
|
107
|
+
for (const alias of [column.name, column.columnName]) {
|
|
108
|
+
const previous = this.columns.get(alias);
|
|
109
|
+
if (previous && previous.columnName !== column.columnName) {
|
|
110
|
+
throw new Error(`ambiguous check column '${alias}'`);
|
|
111
|
+
}
|
|
112
|
+
this.columns.set(alias, column);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
index = 0;
|
|
117
|
+
depth = 0;
|
|
118
|
+
columns;
|
|
119
|
+
parse() {
|
|
120
|
+
const expression = this.parseOr();
|
|
121
|
+
this.expect("end", "end of expression");
|
|
122
|
+
return expression;
|
|
123
|
+
}
|
|
124
|
+
parseOr() {
|
|
125
|
+
let left = this.parseAnd();
|
|
126
|
+
while (this.matchKeyword("or")) left = `(${left} OR ${this.parseAnd()})`;
|
|
127
|
+
return left;
|
|
128
|
+
}
|
|
129
|
+
parseAnd() {
|
|
130
|
+
let left = this.parseNot();
|
|
131
|
+
while (this.matchKeyword("and")) left = `(${left} AND ${this.parseNot()})`;
|
|
132
|
+
return left;
|
|
133
|
+
}
|
|
134
|
+
parseNot() {
|
|
135
|
+
if (this.matchKeyword("not")) return `(NOT ${this.parseNot()})`;
|
|
136
|
+
return this.parsePrimary();
|
|
137
|
+
}
|
|
138
|
+
parsePrimary() {
|
|
139
|
+
if (this.peek().kind === "lparen") {
|
|
140
|
+
if (++this.depth > 32) throw new Error("check expression nesting exceeds 32 levels");
|
|
141
|
+
this.advance();
|
|
142
|
+
const expression = this.parseOr();
|
|
143
|
+
this.expect("rparen", "')'");
|
|
144
|
+
this.depth--;
|
|
145
|
+
return `(${expression})`;
|
|
146
|
+
}
|
|
147
|
+
return this.parseComparison();
|
|
148
|
+
}
|
|
149
|
+
parseComparison() {
|
|
150
|
+
const left = this.parseValue();
|
|
151
|
+
if (this.matchKeyword("is")) {
|
|
152
|
+
const negate = this.matchKeyword("not");
|
|
153
|
+
this.expectKeyword("null");
|
|
154
|
+
return `${left.sql} IS ${negate ? "NOT " : ""}NULL`;
|
|
155
|
+
}
|
|
156
|
+
if (this.matchKeyword("between")) {
|
|
157
|
+
const lower = this.parseValue();
|
|
158
|
+
this.expectKeyword("and");
|
|
159
|
+
const upper = this.parseValue();
|
|
160
|
+
return `${left.sql} BETWEEN ${lower.sql} AND ${upper.sql}`;
|
|
161
|
+
}
|
|
162
|
+
if (this.matchKeyword("in")) {
|
|
163
|
+
this.expect("lparen", "'(' after IN");
|
|
164
|
+
const values = [this.parseValue()];
|
|
165
|
+
while (this.peek().kind === "comma") {
|
|
166
|
+
this.advance();
|
|
167
|
+
values.push(this.parseValue());
|
|
168
|
+
}
|
|
169
|
+
this.expect("rparen", "')' after IN values");
|
|
170
|
+
if (values.some((value) => !value.literal)) throw new Error("IN values must be literals");
|
|
171
|
+
return `${left.sql} IN (${values.map((value) => value.sql).join(", ")})`;
|
|
172
|
+
}
|
|
173
|
+
const operator = this.expect("operator", "a comparison operator");
|
|
174
|
+
const right = this.parseValue();
|
|
175
|
+
if (operator.value === "~" || operator.value === "!~") {
|
|
176
|
+
if (left.column && left.column.type !== "text") throw new Error("regex checks require a text column");
|
|
177
|
+
if (!right.literal) throw new Error("regex pattern must be a literal");
|
|
178
|
+
}
|
|
179
|
+
return `${left.sql} ${operator.value} ${right.sql}`;
|
|
180
|
+
}
|
|
181
|
+
parseValue() {
|
|
182
|
+
const token = this.advance();
|
|
183
|
+
if (token.kind === "string") return { sql: quoteLiteral(token.value), literal: true };
|
|
184
|
+
if (token.kind === "number") {
|
|
185
|
+
const number = Number(token.value);
|
|
186
|
+
if (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number)) {
|
|
187
|
+
throw new Error(`unsafe numeric literal at position ${token.position}`);
|
|
188
|
+
}
|
|
189
|
+
return { sql: token.value, literal: true };
|
|
190
|
+
}
|
|
191
|
+
if (token.kind !== "identifier") throw new Error(`expected a column or literal at position ${token.position}`);
|
|
192
|
+
const keyword = token.value.toLowerCase();
|
|
193
|
+
if (keyword === "true" || keyword === "false" || keyword === "null") return { sql: keyword.toUpperCase(), literal: true };
|
|
194
|
+
if (keyword === "btrim") {
|
|
195
|
+
this.expect("lparen", "'(' after btrim");
|
|
196
|
+
const column2 = this.parseColumn();
|
|
197
|
+
this.expect("rparen", "')' after btrim column");
|
|
198
|
+
if (column2.type !== "text") throw new Error("btrim requires a text column");
|
|
199
|
+
return { sql: `btrim(${quoteIdentifier(column2.columnName)})`, column: column2 };
|
|
200
|
+
}
|
|
201
|
+
const column = this.columns.get(token.value);
|
|
202
|
+
if (!column) throw new Error(`unknown check column '${token.value}'`);
|
|
203
|
+
return { sql: quoteIdentifier(column.columnName), column };
|
|
204
|
+
}
|
|
205
|
+
parseColumn() {
|
|
206
|
+
const token = this.expect("identifier", "a column name");
|
|
207
|
+
const column = this.columns.get(token.value);
|
|
208
|
+
if (!column) throw new Error(`unknown check column '${token.value}'`);
|
|
209
|
+
return column;
|
|
210
|
+
}
|
|
211
|
+
peek() {
|
|
212
|
+
return this.tokens[this.index];
|
|
213
|
+
}
|
|
214
|
+
advance() {
|
|
215
|
+
const token = this.peek();
|
|
216
|
+
if (token.kind !== "end") this.index++;
|
|
217
|
+
return token;
|
|
218
|
+
}
|
|
219
|
+
matchKeyword(keyword) {
|
|
220
|
+
if (this.peek().kind !== "identifier" || this.peek().value.toLowerCase() !== keyword) return false;
|
|
221
|
+
this.advance();
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
expectKeyword(keyword) {
|
|
225
|
+
if (!this.matchKeyword(keyword)) throw new Error(`expected '${keyword}' at position ${this.peek().position}`);
|
|
226
|
+
}
|
|
227
|
+
expect(kind, description) {
|
|
228
|
+
const token = this.peek();
|
|
229
|
+
if (token.kind !== kind) throw new Error(`expected ${description} at position ${token.position}`);
|
|
230
|
+
return this.advance();
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
function compileCheckExpression(source, columns) {
|
|
234
|
+
return new Parser(tokenize(source), columns).parse();
|
|
235
|
+
}
|
|
236
|
+
var policyVariantSchema = zod.z.object({
|
|
237
|
+
/** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
|
|
238
|
+
when: zod.z.string().min(1).max(1e3),
|
|
239
|
+
/** Exactly one of: allow-all bypass, or a row predicate. */
|
|
240
|
+
allow: zod.z.literal(true).optional(),
|
|
241
|
+
policy: zod.z.string().min(1).max(2e3).optional()
|
|
242
|
+
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
243
|
+
message: "A variant declares exactly one of allow: true or policy"
|
|
244
|
+
});
|
|
245
|
+
var policyExpressionSchema = zod.z.union([
|
|
246
|
+
zod.z.string().min(1).max(2e3),
|
|
247
|
+
zod.z.object({
|
|
248
|
+
default: zod.z.string().min(1).max(2e3),
|
|
249
|
+
variants: zod.z.array(policyVariantSchema).max(10).default([])
|
|
250
|
+
})
|
|
251
|
+
]);
|
|
252
|
+
function parseDurationMs(input) {
|
|
253
|
+
if (typeof input === "number") return Number.isInteger(input) && input > 0 ? input : void 0;
|
|
254
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(input);
|
|
255
|
+
if (!match) return void 0;
|
|
256
|
+
const factor = match[2] === "ms" ? 1 : match[2] === "s" ? 1e3 : 6e4;
|
|
257
|
+
const value = Math.round(Number(match[1]) * factor);
|
|
258
|
+
return value > 0 ? value : void 0;
|
|
259
|
+
}
|
|
260
|
+
var identifier = zod.z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
|
|
261
|
+
var durationMs = zod.z.number().int().positive().max(24 * 60 * 60 * 1e3);
|
|
262
|
+
var RESOLVED_VIEW_SYSTEM_COLUMNS = {
|
|
263
|
+
writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
|
|
264
|
+
writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
|
|
265
|
+
};
|
|
266
|
+
var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
|
|
267
|
+
tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
|
|
268
|
+
fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
|
|
269
|
+
writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
|
|
270
|
+
writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
|
|
271
|
+
writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
|
|
272
|
+
};
|
|
273
|
+
var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
|
|
274
|
+
/* @__PURE__ */ new Set([
|
|
275
|
+
...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
|
|
276
|
+
...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
|
|
277
|
+
]);
|
|
278
|
+
var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
|
|
279
|
+
var canonicalFunctionRefSchema = zod.z.object({
|
|
280
|
+
/** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
|
|
281
|
+
kind: zod.z.enum(["managed", "native"]),
|
|
282
|
+
id: zod.z.string().min(1).max(200)
|
|
283
|
+
}).strict();
|
|
284
|
+
var canonicalFetchShapeSchema = zod.z.union([
|
|
285
|
+
zod.z.object({ shape: zod.z.literal("single") }).strict(),
|
|
286
|
+
zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().min(2).max(500) }).strict()
|
|
287
|
+
]);
|
|
288
|
+
var canonicalResolvedSourceSchema = zod.z.object({
|
|
289
|
+
name: identifier,
|
|
290
|
+
kind: zod.z.enum(["manual", "etl", "prefetch"]),
|
|
291
|
+
/** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
|
|
292
|
+
table: identifier,
|
|
293
|
+
/** Resolved columns (logical names) this source supplies — the primary key is implicit. */
|
|
294
|
+
columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
|
|
295
|
+
/** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
|
|
296
|
+
updatedAt: zod.z.string().min(1).max(63).optional(),
|
|
297
|
+
/** prefetch: resolved column holding the source system's key (may be the primary key). */
|
|
298
|
+
key: zod.z.string().min(1).max(63).optional(),
|
|
299
|
+
fetch: canonicalFetchShapeSchema.optional(),
|
|
300
|
+
function: canonicalFunctionRefSchema.optional(),
|
|
301
|
+
prefetchDefaults: zod.z.object({
|
|
302
|
+
maxStalenessMs: durationMs,
|
|
303
|
+
timeoutMs: durationMs,
|
|
304
|
+
onError: zod.z.enum(["fallback", "fail"])
|
|
305
|
+
}).strict().optional(),
|
|
306
|
+
writeback: zod.z.object({
|
|
307
|
+
columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
|
|
308
|
+
defaultMode: zod.z.enum(["sync", "async"]),
|
|
309
|
+
timeoutMs: durationMs,
|
|
310
|
+
/** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
|
|
311
|
+
clearManualOnConfirm: zod.z.boolean()
|
|
312
|
+
}).strict().optional()
|
|
313
|
+
}).strict();
|
|
314
|
+
var canonicalResolveRuleSchema = zod.z.object({
|
|
315
|
+
column: zod.z.string().min(1).max(63),
|
|
316
|
+
comparator: zod.z.enum(COMPARATORS),
|
|
317
|
+
/** Source names, in tie-break order. */
|
|
318
|
+
candidates: zod.z.array(identifier).min(1).max(16),
|
|
319
|
+
/** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
|
|
320
|
+
orderBy: zod.z.string().min(1).max(2e3).optional()
|
|
321
|
+
}).strict();
|
|
322
|
+
var canonicalResolvedSchema = zod.z.object({
|
|
323
|
+
sources: zod.z.array(canonicalResolvedSourceSchema).min(1).max(16),
|
|
324
|
+
/** One rule per resolved (non-pk) column; documents the generated view for tooling. */
|
|
325
|
+
resolve: zod.z.array(canonicalResolveRuleSchema).max(500),
|
|
326
|
+
/** Sources whose tombstone hides the resolved row (v1: the manual source only). */
|
|
327
|
+
deletedBy: zod.z.array(identifier).max(1).default([]),
|
|
328
|
+
/**
|
|
329
|
+
* Materialized: the view reads an indexed internal table (`table`) that the
|
|
330
|
+
* data plane keeps in step with `compute`, the internal view holding the
|
|
331
|
+
* resolution query. Omitted when the view computes on read.
|
|
332
|
+
*/
|
|
333
|
+
materialized: zod.z.object({ table: identifier, compute: identifier }).strict().optional(),
|
|
334
|
+
/** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
|
|
335
|
+
policies: zod.z.object({
|
|
336
|
+
write: policyExpressionSchema.optional(),
|
|
337
|
+
insert: policyExpressionSchema.optional(),
|
|
338
|
+
update: policyExpressionSchema.optional()
|
|
339
|
+
}).strict().default({})
|
|
340
|
+
}).strict();
|
|
341
|
+
function validateResolvedView({ view, tables, views = [] }) {
|
|
342
|
+
const resolved = view.resolved;
|
|
343
|
+
if (!resolved) return [];
|
|
344
|
+
const errors = [];
|
|
345
|
+
const fail3 = (message) => errors.push(`resolved table '${view.view}': ${message}`);
|
|
346
|
+
const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
|
|
347
|
+
if (!pkColumn) {
|
|
348
|
+
fail3("requires exactly one primary-key column");
|
|
349
|
+
return errors;
|
|
350
|
+
}
|
|
351
|
+
const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
|
|
352
|
+
const scope = {
|
|
353
|
+
view,
|
|
354
|
+
resolved,
|
|
355
|
+
pkColumn,
|
|
356
|
+
dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
|
|
357
|
+
sourceByName: /* @__PURE__ */ new Map(),
|
|
358
|
+
fail: fail3
|
|
359
|
+
};
|
|
360
|
+
if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail3("requires exactly one manual source");
|
|
361
|
+
const tableByName = new Map(tables.map((table) => [table.table, table]));
|
|
362
|
+
for (const source of resolved.sources) {
|
|
363
|
+
if (scope.sourceByName.has(source.name)) fail3(`duplicate source '${source.name}'`);
|
|
364
|
+
scope.sourceByName.set(source.name, source);
|
|
365
|
+
const table = tableByName.get(source.table);
|
|
366
|
+
if (!table) fail3(`source '${source.name}' references unknown table '${source.table}'`);
|
|
367
|
+
else validateSource({ scope, source, table });
|
|
368
|
+
}
|
|
369
|
+
validateRules(scope);
|
|
370
|
+
validateViewCells(scope);
|
|
371
|
+
if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
|
|
372
|
+
for (const name of resolved.deletedBy) {
|
|
373
|
+
if (scope.sourceByName.get(name)?.kind !== "manual") fail3(`deletedBy '${name}' must be the manual source`);
|
|
374
|
+
}
|
|
375
|
+
return errors;
|
|
376
|
+
}
|
|
377
|
+
function validateSource({ scope, source, table }) {
|
|
378
|
+
const { fail: fail3 } = scope;
|
|
379
|
+
if (source.kind === "etl" && table.source === void 0) fail3(`etl source '${source.name}' must reference a synced table`);
|
|
380
|
+
if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
|
|
381
|
+
fail3(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
|
|
382
|
+
}
|
|
383
|
+
const hasRev = table.columns.some((column) => column.cell?.part === "rev");
|
|
384
|
+
if (source.kind === "manual" && !hasRev) fail3(`manual source '${source.name}' must carry '__rev' cell columns`);
|
|
385
|
+
if (source.kind === "prefetch" && hasRev) fail3(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
|
|
386
|
+
const byName = new Map(table.columns.map((c) => [c.name, c]));
|
|
387
|
+
validateSourceColumns({ scope, source, byName });
|
|
388
|
+
if (source.kind === "etl") {
|
|
389
|
+
const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
|
|
390
|
+
if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
|
|
391
|
+
fail3(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
validatePrefetchFields({ scope, source });
|
|
395
|
+
}
|
|
396
|
+
function validatePrefetchFields({ scope, source }) {
|
|
397
|
+
const { fail: fail3, pkColumn } = scope;
|
|
398
|
+
if (source.kind !== "prefetch") {
|
|
399
|
+
if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
|
|
400
|
+
fail3(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
|
|
401
|
+
}
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
|
|
405
|
+
fail3(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
|
|
406
|
+
} else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
|
|
407
|
+
fail3(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
|
|
408
|
+
}
|
|
409
|
+
for (const column of source.writeback?.columns ?? []) {
|
|
410
|
+
if (!source.columns.includes(column)) fail3(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function validateSourceColumns({ scope, source, byName }) {
|
|
414
|
+
const { fail: fail3, pkColumn, dataColumns } = scope;
|
|
415
|
+
for (const column of [pkColumn.name, ...source.columns]) {
|
|
416
|
+
const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
|
|
417
|
+
const theirs = byName.get(column);
|
|
418
|
+
if (!own) fail3(`source '${source.name}' supplies unknown column '${column}'`);
|
|
419
|
+
else if (!theirs) fail3(`source '${source.name}' table '${source.table}' has no column '${column}'`);
|
|
420
|
+
else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail3(`column '${column}' has a different type on source '${source.name}'`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail: fail3 }) {
|
|
424
|
+
const ruled = /* @__PURE__ */ new Set();
|
|
425
|
+
for (const rule of resolved.resolve) {
|
|
426
|
+
if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
|
|
427
|
+
fail3(`resolve rule targets unknown column '${rule.column}'`);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (ruled.has(rule.column)) fail3(`column '${rule.column}' has more than one resolve rule`);
|
|
431
|
+
ruled.add(rule.column);
|
|
432
|
+
if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
|
|
433
|
+
fail3(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
|
|
434
|
+
}
|
|
435
|
+
if (new Set(rule.candidates).size !== rule.candidates.length) fail3(`column '${rule.column}' repeats a candidate`);
|
|
436
|
+
for (const candidate of rule.candidates) {
|
|
437
|
+
const source = sourceByName.get(candidate);
|
|
438
|
+
if (!source) fail3(`column '${rule.column}' names unknown candidate source '${candidate}'`);
|
|
439
|
+
else if (!source.columns.includes(rule.column)) fail3(`column '${rule.column}' candidate '${candidate}' does not supply it`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
for (const column of dataColumns.keys()) {
|
|
443
|
+
if (column !== pkColumn.name && !ruled.has(column)) fail3(`column '${column}' has no resolve rule`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
var VIEW_CELL_TYPES = {
|
|
447
|
+
source: "text",
|
|
448
|
+
at: "timestamptz",
|
|
449
|
+
by: "text",
|
|
450
|
+
conflicts: "jsonb"
|
|
451
|
+
};
|
|
452
|
+
function validateViewCells({ view, dataColumns, pkColumn, fail: fail3 }) {
|
|
453
|
+
const expected = /* @__PURE__ */ new Map();
|
|
454
|
+
for (const column of dataColumns.values()) {
|
|
455
|
+
if (column.name === pkColumn.name) continue;
|
|
456
|
+
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
457
|
+
expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
for (const column of view.columns) {
|
|
461
|
+
if (!column.cell) continue;
|
|
462
|
+
const want = expected.get(column.name);
|
|
463
|
+
if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
|
|
464
|
+
fail3(`unexpected cell column '${column.name}'`);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail3(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
|
|
468
|
+
expected.delete(column.name);
|
|
469
|
+
}
|
|
470
|
+
for (const name of expected.keys()) fail3(`missing cell column '${name}'`);
|
|
471
|
+
}
|
|
472
|
+
var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
|
|
473
|
+
function validateMaterialized({ scope, tableByName, views }) {
|
|
474
|
+
const { view, resolved, fail: fail3 } = scope;
|
|
475
|
+
const { table: tableName, compute: computeName } = resolved.materialized;
|
|
476
|
+
const table = tableByName.get(tableName);
|
|
477
|
+
if (!table || table.internal !== true || table.materializes !== view.view) {
|
|
478
|
+
fail3(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
|
|
479
|
+
} else {
|
|
480
|
+
if (columnSignature(table.columns) !== columnSignature(view.columns)) fail3(`materialized table '${tableName}' must have exactly the view's columns`);
|
|
481
|
+
if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail3(`materialized table '${tableName}' must share the view's primary key`);
|
|
482
|
+
}
|
|
483
|
+
const compute = views.find((candidate) => candidate.view === computeName);
|
|
484
|
+
if (!compute || compute.internal !== true) fail3(`compute view '${computeName}' must be an internal view`);
|
|
485
|
+
else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail3(`compute view '${computeName}' must have exactly the view's columns`);
|
|
486
|
+
for (const source of resolved.sources) {
|
|
487
|
+
if (source.kind !== "etl") continue;
|
|
488
|
+
const read = tableByName.get(source.table)?.policies?.read;
|
|
489
|
+
const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
|
|
490
|
+
if (!unconditional) {
|
|
491
|
+
fail3(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// src/canonical/v2.ts
|
|
21
497
|
var CANONICAL_FORMAT = 2;
|
|
22
498
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
23
|
-
var
|
|
499
|
+
var identifier2 = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
500
|
+
var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
|
|
24
501
|
var COLUMN_TYPES = [
|
|
25
502
|
"uuid",
|
|
26
503
|
"text",
|
|
@@ -35,59 +512,71 @@ var COLUMN_TYPES = [
|
|
|
35
512
|
];
|
|
36
513
|
var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
|
|
37
514
|
var canonicalEnumSchema = zod.z.object({
|
|
38
|
-
name:
|
|
515
|
+
name: identifier2,
|
|
39
516
|
values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
|
|
40
517
|
});
|
|
41
518
|
var canonicalColumnSchema = zod.z.object({
|
|
42
519
|
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
43
520
|
name: zod.z.string().min(1).max(63),
|
|
44
|
-
columnName:
|
|
45
|
-
type: zod.z.union([columnTypeSchema, zod.z.object({ enum:
|
|
521
|
+
columnName: columnIdentifier,
|
|
522
|
+
type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier2 })]),
|
|
46
523
|
nullable: zod.z.boolean().default(true),
|
|
47
524
|
unique: zod.z.boolean().default(false),
|
|
48
525
|
/** Literal default, or one of the allowlisted SQL functions. */
|
|
49
526
|
default: zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()]).optional(),
|
|
50
527
|
defaultFn: zod.z.enum(["gen_random_uuid()", "now()"]).optional(),
|
|
528
|
+
/**
|
|
529
|
+
* Stamp this column from the database clock on UPDATE (`now()`). The
|
|
530
|
+
* column stays insertable (defaults fill gaps) but is omitted from
|
|
531
|
+
* SetInput / settable so callers cannot backdate it.
|
|
532
|
+
*/
|
|
533
|
+
onUpdateFn: zod.z.enum(["now()"]).optional(),
|
|
51
534
|
description: zod.z.string().max(500).optional(),
|
|
52
535
|
/** Migration hints — same semantics as v1. */
|
|
53
|
-
renamedFrom:
|
|
536
|
+
renamedFrom: columnIdentifier.optional(),
|
|
54
537
|
cast: zod.z.string().max(200).optional(),
|
|
55
|
-
deprecated: zod.z.boolean().default(false)
|
|
538
|
+
deprecated: zod.z.boolean().default(false),
|
|
539
|
+
/**
|
|
540
|
+
* Marks a system cell-metadata column (see CELL_PARTS in constants): part
|
|
541
|
+
* `part` of the cell of logical column `of`. Synthesized by authoring,
|
|
542
|
+
* never client-writable, absent from GraphQL scalar fields. Omitted on
|
|
543
|
+
* ordinary columns so their hashes are unchanged.
|
|
544
|
+
*/
|
|
545
|
+
cell: zod.z.object({ of: zod.z.string().min(1).max(63), part: zod.z.enum(CELL_PARTS) }).strict().optional()
|
|
546
|
+
}).superRefine((column, ctx) => {
|
|
547
|
+
if (column.onUpdateFn === void 0) return;
|
|
548
|
+
if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
|
|
549
|
+
ctx.addIssue({
|
|
550
|
+
code: "custom",
|
|
551
|
+
path: ["onUpdateFn"],
|
|
552
|
+
message: `onUpdateFn '${column.onUpdateFn}' is only valid on timestamptz/date columns`
|
|
553
|
+
});
|
|
554
|
+
}
|
|
56
555
|
});
|
|
57
556
|
var canonicalLinkSchema = zod.z.object({
|
|
58
557
|
name: zod.z.string().min(1).max(63),
|
|
59
558
|
kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
|
|
60
559
|
/** Target table name (namespace-local). Absent for toPlatformUser. */
|
|
61
|
-
target:
|
|
560
|
+
target: identifier2.optional(),
|
|
62
561
|
/** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
|
|
63
|
-
on:
|
|
562
|
+
on: columnIdentifier,
|
|
64
563
|
/** Referenced column on the target (default: target's PK / platform user `sub`). */
|
|
65
|
-
references:
|
|
564
|
+
references: columnIdentifier.optional(),
|
|
66
565
|
/** enforce: true emits a real FK constraint (same-namespace toOne only). */
|
|
67
566
|
enforce: zod.z.boolean().default(false),
|
|
567
|
+
onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
568
|
+
onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
68
569
|
description: zod.z.string().max(500).optional()
|
|
69
570
|
});
|
|
70
571
|
var canonicalIndexSchema = zod.z.object({
|
|
71
|
-
columns: zod.z.array(
|
|
572
|
+
columns: zod.z.array(columnIdentifier).min(1),
|
|
72
573
|
unique: zod.z.boolean().default(false),
|
|
73
|
-
name:
|
|
574
|
+
name: identifier2.optional()
|
|
74
575
|
});
|
|
75
|
-
var
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
allow: zod.z.literal(true).optional(),
|
|
80
|
-
policy: zod.z.string().min(1).max(2e3).optional()
|
|
81
|
-
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
82
|
-
message: "A variant declares exactly one of allow: true or policy"
|
|
83
|
-
});
|
|
84
|
-
var policyExpressionSchema = zod.z.union([
|
|
85
|
-
zod.z.string().min(1).max(2e3),
|
|
86
|
-
zod.z.object({
|
|
87
|
-
default: zod.z.string().min(1).max(2e3),
|
|
88
|
-
variants: zod.z.array(policyVariantSchema).max(10).default([])
|
|
89
|
-
})
|
|
90
|
-
]);
|
|
576
|
+
var canonicalCheckSchema = zod.z.object({
|
|
577
|
+
name: identifier2,
|
|
578
|
+
expression: zod.z.string().min(1).max(2e3)
|
|
579
|
+
}).strict();
|
|
91
580
|
var canonicalPoliciesSchema = zod.z.object({
|
|
92
581
|
read: policyExpressionSchema.optional(),
|
|
93
582
|
write: policyExpressionSchema.optional(),
|
|
@@ -118,7 +607,7 @@ function isSingleViewQuerySql(value) {
|
|
|
118
607
|
return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
|
|
119
608
|
}
|
|
120
609
|
var canonicalActionSchema = zod.z.object({
|
|
121
|
-
name:
|
|
610
|
+
name: identifier2,
|
|
122
611
|
requires: zod.z.string().max(1e3).optional(),
|
|
123
612
|
params: zod.z.record(zod.z.string(), zod.z.object({
|
|
124
613
|
type: columnTypeSchema,
|
|
@@ -130,9 +619,9 @@ var canonicalActionSchema = zod.z.object({
|
|
|
130
619
|
});
|
|
131
620
|
var canonicalCowMutationSchema = zod.z.object({
|
|
132
621
|
kind: zod.z.enum(["override", "revert"]),
|
|
133
|
-
name:
|
|
622
|
+
name: identifier2,
|
|
134
623
|
/** Synced table (same namespace) the overlay copies from / falls back to. */
|
|
135
|
-
synced:
|
|
624
|
+
synced: identifier2,
|
|
136
625
|
/** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
|
|
137
626
|
pk: zod.z.string().min(1).max(63),
|
|
138
627
|
/** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
|
|
@@ -159,17 +648,28 @@ var canonicalCowMutationSchema = zod.z.object({
|
|
|
159
648
|
}
|
|
160
649
|
}
|
|
161
650
|
});
|
|
651
|
+
var canonicalForeignKeySchema = zod.z.object({
|
|
652
|
+
name: identifier2,
|
|
653
|
+
columns: zod.z.array(columnIdentifier).min(1).max(8),
|
|
654
|
+
target: identifier2,
|
|
655
|
+
references: zod.z.array(columnIdentifier).min(1).max(8),
|
|
656
|
+
deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional(),
|
|
657
|
+
onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
658
|
+
onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
|
|
659
|
+
}).strict();
|
|
162
660
|
var canonicalTableSchema = zod.z.object({
|
|
163
|
-
table:
|
|
661
|
+
table: identifier2,
|
|
164
662
|
description: zod.z.string().max(2e3).optional(),
|
|
165
663
|
/** Table-level rename hint (new in v2). */
|
|
166
|
-
renamedFrom:
|
|
664
|
+
renamedFrom: identifier2.optional(),
|
|
167
665
|
/** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
|
|
168
666
|
source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
|
|
169
667
|
columns: zod.z.array(canonicalColumnSchema).default([]),
|
|
170
|
-
primaryKey: zod.z.array(
|
|
668
|
+
primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
|
|
171
669
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
172
670
|
indexes: zod.z.array(canonicalIndexSchema).default([]),
|
|
671
|
+
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
672
|
+
checks: zod.z.array(canonicalCheckSchema).max(100).optional(),
|
|
173
673
|
policies: canonicalPoliciesSchema.default({}),
|
|
174
674
|
/**
|
|
175
675
|
* v1-conversion carry-over only. v2 has no action surface — the runtime
|
|
@@ -178,26 +678,64 @@ var canonicalTableSchema = zod.z.object({
|
|
|
178
678
|
*/
|
|
179
679
|
actions: zod.z.array(canonicalActionSchema).default([]),
|
|
180
680
|
/** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
|
|
181
|
-
cowMutations: zod.z.array(canonicalCowMutationSchema).optional()
|
|
681
|
+
cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
|
|
682
|
+
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
683
|
+
foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional(),
|
|
684
|
+
/**
|
|
685
|
+
* Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
|
|
686
|
+
* column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
|
|
687
|
+
* on a resolved table's internal sources), listed explicitly in `columns`
|
|
688
|
+
* with a `cell` marker so DDL and migrations treat them as ordinary columns.
|
|
689
|
+
* Omitted when off so existing definitions hash unchanged.
|
|
690
|
+
*/
|
|
691
|
+
cellMetadata: zod.z.literal(true).optional(),
|
|
692
|
+
/**
|
|
693
|
+
* System-owned backing table of a resolved table (its manual or prefetch
|
|
694
|
+
* source). Internal tables get DDL and IR metadata but no GraphQL surface;
|
|
695
|
+
* only the data plane writes them. Omitted when false.
|
|
696
|
+
*/
|
|
697
|
+
internal: zod.z.literal(true).optional(),
|
|
698
|
+
/**
|
|
699
|
+
* The materialized copy of resolved table `materializes` (internal): the
|
|
700
|
+
* same columns as that view, including its cell parts, maintained by the
|
|
701
|
+
* data plane. Omitted otherwise.
|
|
702
|
+
*/
|
|
703
|
+
materializes: identifier2.optional()
|
|
182
704
|
});
|
|
705
|
+
var MAX_VIEW_QUERY = 1e5;
|
|
706
|
+
var MAX_RESOLVED_VIEW_QUERY = 5e6;
|
|
183
707
|
var canonicalViewSchema = zod.z.object({
|
|
184
|
-
view:
|
|
708
|
+
view: identifier2,
|
|
185
709
|
description: zod.z.string().max(2e3).optional(),
|
|
186
710
|
columns: zod.z.array(canonicalColumnSchema).min(1),
|
|
187
711
|
/** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
|
|
188
|
-
primaryKey: zod.z.array(
|
|
712
|
+
primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
|
|
189
713
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
190
714
|
policies: zod.z.object({ read: policyExpressionSchema }).strict(),
|
|
191
|
-
query: zod.z.string().min(1).max(
|
|
715
|
+
query: zod.z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
|
|
192
716
|
message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
|
|
193
717
|
}),
|
|
194
718
|
/** Namespace-local logical relation names, sorted for deterministic hashing. */
|
|
195
|
-
dependencies: zod.z.array(
|
|
719
|
+
dependencies: zod.z.array(identifier2).max(500),
|
|
720
|
+
/**
|
|
721
|
+
* Present when this view is a resolved table (defineResolvedTable): the
|
|
722
|
+
* multi-source metadata behind its generated query. Omitted otherwise so
|
|
723
|
+
* existing views hash unchanged.
|
|
724
|
+
*/
|
|
725
|
+
resolved: canonicalResolvedSchema.optional(),
|
|
726
|
+
/** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
|
|
727
|
+
internal: zod.z.literal(true).optional()
|
|
196
728
|
}).superRefine((view, ctx) => {
|
|
729
|
+
if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
|
|
730
|
+
ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
|
|
731
|
+
}
|
|
197
732
|
for (const [index, column] of view.columns.entries()) {
|
|
198
733
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
199
734
|
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
|
|
200
735
|
}
|
|
736
|
+
if (column.onUpdateFn !== void 0) {
|
|
737
|
+
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare onUpdateFn` });
|
|
738
|
+
}
|
|
201
739
|
if (column.unique) {
|
|
202
740
|
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare unique` });
|
|
203
741
|
}
|
|
@@ -205,15 +743,82 @@ var canonicalViewSchema = zod.z.object({
|
|
|
205
743
|
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare migration hints` });
|
|
206
744
|
}
|
|
207
745
|
}
|
|
746
|
+
if (view.resolved === void 0 && view.internal !== true) {
|
|
747
|
+
for (const [index, column] of view.columns.entries()) {
|
|
748
|
+
if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index], message: `only resolved tables carry cell columns ('${column.name}')` });
|
|
749
|
+
}
|
|
750
|
+
}
|
|
208
751
|
for (const [index, link] of view.links.entries()) {
|
|
209
752
|
if (link.enforce) {
|
|
210
753
|
ctx.addIssue({ code: "custom", path: ["links", index, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
|
|
211
754
|
}
|
|
755
|
+
if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
|
|
756
|
+
ctx.addIssue({ code: "custom", path: ["links", index], message: `view link '${link.name}' cannot declare referential actions` });
|
|
757
|
+
}
|
|
212
758
|
}
|
|
213
759
|
});
|
|
760
|
+
var CELL_PART_TYPES = {
|
|
761
|
+
at: "timestamptz",
|
|
762
|
+
by: "text",
|
|
763
|
+
rev: "uuid",
|
|
764
|
+
source: "text",
|
|
765
|
+
conflicts: "jsonb"
|
|
766
|
+
};
|
|
767
|
+
function trackedCellColumns(table) {
|
|
768
|
+
const pk = new Set(table.primaryKey ?? []);
|
|
769
|
+
return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
|
|
770
|
+
}
|
|
771
|
+
var SUFFIXES = Object.values(CELL_PART_SUFFIX);
|
|
772
|
+
function validateCellMetadata(table, index, ctx) {
|
|
773
|
+
const fail3 = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
|
|
774
|
+
const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
|
|
775
|
+
if (table.materializes !== void 0) return;
|
|
776
|
+
if (table.cellMetadata !== true) {
|
|
777
|
+
for (const { position } of cells) fail3(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index, "columns", position]);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
if (table.source !== void 0) {
|
|
781
|
+
fail3(`synced table '${table.table}' cannot track cell metadata`);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const tracked = trackedCellColumns(table);
|
|
785
|
+
const byName = new Map(tracked.map((column) => [column.name, column]));
|
|
786
|
+
const withRev = cells.some(({ cell }) => cell.part === "rev");
|
|
787
|
+
if (withRev && table.internal !== true) fail3(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
|
|
788
|
+
const seen = /* @__PURE__ */ new Set();
|
|
789
|
+
for (const { column, position, cell } of cells) {
|
|
790
|
+
const path = ["tables", index, "columns", position];
|
|
791
|
+
const owner = byName.get(cell.of);
|
|
792
|
+
if (!owner) {
|
|
793
|
+
fail3(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
if (cell.part !== "at" && cell.part !== "by" && cell.part !== "rev") fail3(`a table cell column must be part at/by/rev, not '${cell.part}'`, path);
|
|
797
|
+
if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
|
|
798
|
+
fail3(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
|
|
799
|
+
}
|
|
800
|
+
if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
|
|
801
|
+
fail3(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
|
|
802
|
+
}
|
|
803
|
+
const key = `${cell.of}:${cell.part}`;
|
|
804
|
+
if (seen.has(key)) fail3(`duplicate cell column '${column.name}'`, path);
|
|
805
|
+
seen.add(key);
|
|
806
|
+
}
|
|
807
|
+
for (const column of tracked) {
|
|
808
|
+
if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
|
|
809
|
+
fail3(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
|
|
810
|
+
}
|
|
811
|
+
if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
|
|
812
|
+
fail3(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
|
|
813
|
+
}
|
|
814
|
+
for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
|
|
815
|
+
if (!seen.has(`${column.name}:${part}`)) fail3(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
214
819
|
var canonicalDefinitionSchema = zod.z.object({
|
|
215
820
|
definition_format: zod.z.literal(CANONICAL_FORMAT),
|
|
216
|
-
namespace:
|
|
821
|
+
namespace: identifier2.max(41),
|
|
217
822
|
description: zod.z.string().max(2e3).optional(),
|
|
218
823
|
enums: zod.z.array(canonicalEnumSchema).default([]),
|
|
219
824
|
tables: zod.z.array(canonicalTableSchema).min(0).max(500),
|
|
@@ -231,6 +836,74 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
231
836
|
ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
|
|
232
837
|
}
|
|
233
838
|
seen.add(table.table);
|
|
839
|
+
validateCellMetadata(table, index, ctx);
|
|
840
|
+
if (table.source !== void 0) {
|
|
841
|
+
for (const [columnIndex, column] of table.columns.entries()) {
|
|
842
|
+
if (column.onUpdateFn !== void 0) {
|
|
843
|
+
ctx.addIssue({
|
|
844
|
+
code: "custom",
|
|
845
|
+
path: ["tables", index, "columns", columnIndex],
|
|
846
|
+
message: `synced table '${table.table}' column '${column.name}' cannot declare onUpdateFn`
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
853
|
+
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
854
|
+
const names = /* @__PURE__ */ new Set([
|
|
855
|
+
...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
|
|
856
|
+
...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
|
|
857
|
+
...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
|
|
858
|
+
]);
|
|
859
|
+
for (const [index, link] of table.links.entries()) {
|
|
860
|
+
if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
|
|
861
|
+
ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "referential actions require an enforced toOne link" });
|
|
862
|
+
}
|
|
863
|
+
if (link.onDelete === "set null" || link.onUpdate === "set null") {
|
|
864
|
+
const source = table.columns.find((column) => column.columnName === link.on);
|
|
865
|
+
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" });
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
for (const [index, check] of (table.checks ?? []).entries()) {
|
|
869
|
+
const fail3 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index], message });
|
|
870
|
+
if (table.source) fail3("synced tables cannot declare checks");
|
|
871
|
+
if (names.has(check.name)) fail3(`duplicate constraint '${check.name}'`);
|
|
872
|
+
names.add(check.name);
|
|
873
|
+
try {
|
|
874
|
+
compileCheckExpression(check.expression, table.columns);
|
|
875
|
+
} catch (error) {
|
|
876
|
+
fail3(error instanceof Error ? error.message : String(error));
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
|
|
880
|
+
const fail3 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
|
|
881
|
+
if (names.has(fk.name)) fail3(`duplicate foreign key '${fk.name}'`);
|
|
882
|
+
names.add(fk.name);
|
|
883
|
+
const target = fkTables.get(fk.target);
|
|
884
|
+
if (table.source || !target || target.source) {
|
|
885
|
+
fail3("foreign keys must connect owned tables in the same namespace");
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
if (fk.columns.length !== fk.references.length) fail3("foreign key columns and references must have the same number of columns");
|
|
889
|
+
if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail3("foreign key columns must not repeat");
|
|
890
|
+
for (const [position, name] of fk.columns.entries()) {
|
|
891
|
+
const sourceColumn = table.columns.find((column) => column.columnName === name);
|
|
892
|
+
const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
|
|
893
|
+
if (!sourceColumn || !targetColumn) fail3("foreign key references an unknown column");
|
|
894
|
+
else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail3("foreign key column types must match");
|
|
895
|
+
}
|
|
896
|
+
if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
|
|
897
|
+
if (fk.columns.some((name) => table.columns.find((column) => column.columnName === name)?.nullable === false || table.primaryKey?.includes(name))) fail3("SET NULL requires nullable source columns");
|
|
898
|
+
}
|
|
899
|
+
const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index2) => index2.unique).map((index2) => index2.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
|
|
900
|
+
if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail3("foreign key target columns must form a unique key");
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
for (const [viewIndex, view] of (definition.views ?? []).entries()) {
|
|
904
|
+
for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
|
|
905
|
+
ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
|
|
906
|
+
}
|
|
234
907
|
}
|
|
235
908
|
const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
236
909
|
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
@@ -326,7 +999,7 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
326
999
|
const visiting = /* @__PURE__ */ new Set();
|
|
327
1000
|
const visited = /* @__PURE__ */ new Set();
|
|
328
1001
|
let cycleReported = false;
|
|
329
|
-
const
|
|
1002
|
+
const visit2 = (name, path) => {
|
|
330
1003
|
if (visited.has(name) || cycleReported) return;
|
|
331
1004
|
if (visiting.has(name)) {
|
|
332
1005
|
cycleReported = true;
|
|
@@ -340,12 +1013,12 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
340
1013
|
visiting.add(name);
|
|
341
1014
|
const view = viewByName.get(name);
|
|
342
1015
|
for (const dependency of view?.dependencies ?? []) {
|
|
343
|
-
if (viewByName.has(dependency))
|
|
1016
|
+
if (viewByName.has(dependency)) visit2(dependency, [...path, name]);
|
|
344
1017
|
}
|
|
345
1018
|
visiting.delete(name);
|
|
346
1019
|
visited.add(name);
|
|
347
1020
|
};
|
|
348
|
-
for (const view of views)
|
|
1021
|
+
for (const view of views) visit2(view.view, []);
|
|
349
1022
|
});
|
|
350
1023
|
function canonicalRelations(definition) {
|
|
351
1024
|
const views = (definition.views ?? []).map((view) => ({
|
|
@@ -355,8 +1028,12 @@ function canonicalRelations(definition) {
|
|
|
355
1028
|
...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
|
|
356
1029
|
links: view.links,
|
|
357
1030
|
indexes: [],
|
|
358
|
-
|
|
359
|
-
|
|
1031
|
+
// A resolved table's write policies (evaluated against the resolved row)
|
|
1032
|
+
// ride alongside the view's read policy.
|
|
1033
|
+
policies: { read: view.policies.read, ...view.resolved?.policies ?? {} },
|
|
1034
|
+
actions: [],
|
|
1035
|
+
...view.resolved !== void 0 ? { resolved: view.resolved } : {},
|
|
1036
|
+
...view.internal ? { internal: true } : {}
|
|
360
1037
|
}));
|
|
361
1038
|
return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
|
|
362
1039
|
}
|
|
@@ -366,12 +1043,14 @@ function normalizeCanonical(definition) {
|
|
|
366
1043
|
...definition,
|
|
367
1044
|
enums: sortByName(definition.enums),
|
|
368
1045
|
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t) => {
|
|
369
|
-
const { cowMutations, ...rest } = t;
|
|
1046
|
+
const { cowMutations, foreignKeys, checks, ...rest } = t;
|
|
370
1047
|
return {
|
|
371
1048
|
...rest,
|
|
372
1049
|
links: sortByName(t.links),
|
|
373
1050
|
indexes: [...t.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
|
|
374
1051
|
actions: sortByName(t.actions),
|
|
1052
|
+
...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
|
|
1053
|
+
...checks?.length ? { checks: sortByName(checks) } : {},
|
|
375
1054
|
// Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
|
|
376
1055
|
...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
|
|
377
1056
|
};
|
|
@@ -388,7 +1067,18 @@ function normalizeCanonical(definition) {
|
|
|
388
1067
|
}
|
|
389
1068
|
|
|
390
1069
|
// src/ir/index.ts
|
|
391
|
-
var IR_VERSION =
|
|
1070
|
+
var IR_VERSION = 2;
|
|
1071
|
+
var LEGACY_IR_VERSION = 1;
|
|
1072
|
+
var irVersionSchema = zod.z.union([zod.z.literal(LEGACY_IR_VERSION), zod.z.literal(IR_VERSION)]);
|
|
1073
|
+
var operationVersionSchema = zod.z.object({ irVersion: irVersionSchema });
|
|
1074
|
+
function requiredArtifactIrVersion(operations) {
|
|
1075
|
+
let required = LEGACY_IR_VERSION;
|
|
1076
|
+
for (const operation of operations) {
|
|
1077
|
+
const { irVersion } = operationVersionSchema.parse(operation);
|
|
1078
|
+
if (irVersion > required) required = irVersion;
|
|
1079
|
+
}
|
|
1080
|
+
return required;
|
|
1081
|
+
}
|
|
392
1082
|
var CLAIMS_CONTRACT = {
|
|
393
1083
|
sub: "string",
|
|
394
1084
|
org: "string?",
|
|
@@ -460,6 +1150,21 @@ var selectionIrSchema = zod.z.lazy(
|
|
|
460
1150
|
() => zod.z.object({
|
|
461
1151
|
typename: zod.z.string(),
|
|
462
1152
|
columns: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })),
|
|
1153
|
+
cells: zod.z.object({
|
|
1154
|
+
alias: zod.z.string(),
|
|
1155
|
+
typename: zod.z.string(),
|
|
1156
|
+
fields: zod.z.array(zod.z.object({
|
|
1157
|
+
alias: zod.z.string(),
|
|
1158
|
+
column: zod.z.string(),
|
|
1159
|
+
typename: zod.z.string(),
|
|
1160
|
+
parts: zod.z.array(zod.z.object({
|
|
1161
|
+
alias: zod.z.string(),
|
|
1162
|
+
part: zod.z.enum(["source", "at", "by", "conflicts"]),
|
|
1163
|
+
fields: zod.z.array(zod.z.object({ alias: zod.z.string(), field: zod.z.enum(["source", "at", "value"]) })).optional(),
|
|
1164
|
+
typename: zod.z.string().optional()
|
|
1165
|
+
}))
|
|
1166
|
+
}))
|
|
1167
|
+
}).optional(),
|
|
463
1168
|
relations: zod.z.array(zod.z.object({
|
|
464
1169
|
alias: zod.z.string(),
|
|
465
1170
|
link: zod.z.string(),
|
|
@@ -481,6 +1186,11 @@ var selectionIrSchema = zod.z.lazy(
|
|
|
481
1186
|
})
|
|
482
1187
|
);
|
|
483
1188
|
var includeIfSchema = zod.z.object({ variable: zod.z.string(), negated: zod.z.boolean() });
|
|
1189
|
+
var prefetchPlanIrSchema = zod.z.object({
|
|
1190
|
+
source: zod.z.string(),
|
|
1191
|
+
maxStalenessMs: zod.z.number().int().positive().optional(),
|
|
1192
|
+
key: zod.z.union([zod.z.object({ one: valueIrSchema }), zod.z.object({ many: valueIrSchema })])
|
|
1193
|
+
});
|
|
484
1194
|
var selectRootSchema = zod.z.object({
|
|
485
1195
|
kind: zod.z.literal("select"),
|
|
486
1196
|
alias: zod.z.string(),
|
|
@@ -492,7 +1202,8 @@ var selectRootSchema = zod.z.object({
|
|
|
492
1202
|
distinctOn: zod.z.union([zod.z.array(zod.z.string()), zod.z.object({ slot: zod.z.string() })]).optional(),
|
|
493
1203
|
limit: limitIrSchema,
|
|
494
1204
|
offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
|
|
495
|
-
includeIf: includeIfSchema.optional()
|
|
1205
|
+
includeIf: includeIfSchema.optional(),
|
|
1206
|
+
prefetch: zod.z.array(prefetchPlanIrSchema).optional()
|
|
496
1207
|
});
|
|
497
1208
|
var byPkRootSchema = zod.z.object({
|
|
498
1209
|
kind: zod.z.literal("byPk"),
|
|
@@ -502,7 +1213,8 @@ var byPkRootSchema = zod.z.object({
|
|
|
502
1213
|
pk: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).min(1),
|
|
503
1214
|
/** Policy predicate — ANDed with the PK match. */
|
|
504
1215
|
where: predIrSchema,
|
|
505
|
-
includeIf: includeIfSchema.optional()
|
|
1216
|
+
includeIf: includeIfSchema.optional(),
|
|
1217
|
+
prefetch: zod.z.array(prefetchPlanIrSchema).optional()
|
|
506
1218
|
});
|
|
507
1219
|
var aggregateRootSchema = zod.z.object({
|
|
508
1220
|
kind: zod.z.literal("aggregate"),
|
|
@@ -515,6 +1227,33 @@ var aggregateRootSchema = zod.z.object({
|
|
|
515
1227
|
nodes: selectionIrSchema.optional(),
|
|
516
1228
|
includeIf: includeIfSchema.optional()
|
|
517
1229
|
});
|
|
1230
|
+
var DATE_BUCKETS = ["year", "quarter", "month", "week", "day", "hour"];
|
|
1231
|
+
var groupByKeyIrSchema = zod.z.object({
|
|
1232
|
+
column: zod.z.string(),
|
|
1233
|
+
bucket: zod.z.enum(DATE_BUCKETS).optional()
|
|
1234
|
+
});
|
|
1235
|
+
var aggregateGroupsRootSchema = zod.z.object({
|
|
1236
|
+
kind: zod.z.literal("aggregateGroups"),
|
|
1237
|
+
alias: zod.z.string(),
|
|
1238
|
+
table: zod.z.string(),
|
|
1239
|
+
/** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
|
|
1240
|
+
where: predIrSchema,
|
|
1241
|
+
groupBy: zod.z.array(groupByKeyIrSchema).min(1),
|
|
1242
|
+
/**
|
|
1243
|
+
* The `key { … }` projection: which group columns appear in each entry's key
|
|
1244
|
+
* object, in selection order. A subset of `groupBy` (the compiler rejects a
|
|
1245
|
+
* key field that is not a group key — it would be indeterminate within the
|
|
1246
|
+
* group). Empty when the document selects only `aggregate`.
|
|
1247
|
+
*/
|
|
1248
|
+
key: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })).default([]),
|
|
1249
|
+
count: zod.z.object({ columns: zod.z.array(zod.z.string()).optional(), distinct: zod.z.boolean().optional() }).optional(),
|
|
1250
|
+
sum: zod.z.array(zod.z.string()).optional(),
|
|
1251
|
+
avg: zod.z.array(zod.z.string()).optional(),
|
|
1252
|
+
/** Bounds the GROUP count, not the scanned rows — see MAX_GROUPS. */
|
|
1253
|
+
limit: limitIrSchema,
|
|
1254
|
+
offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
|
|
1255
|
+
includeIf: includeIfSchema.optional()
|
|
1256
|
+
});
|
|
518
1257
|
var writeValuesSchema = zod.z.union([
|
|
519
1258
|
zod.z.object({ slot: zod.z.string() }),
|
|
520
1259
|
zod.z.object({ entries: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })) })
|
|
@@ -533,7 +1272,20 @@ var mutationRootSchema = zod.z.object({
|
|
|
533
1272
|
* coalesce view falls back to synced. Both are gated on a matching synced
|
|
534
1273
|
* row and carry the write policy in `where` like update/delete.
|
|
535
1274
|
*/
|
|
536
|
-
op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
|
|
1275
|
+
op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert", "resolvedWrite"]),
|
|
1276
|
+
/**
|
|
1277
|
+
* resolvedWrite: a write to a resolved table (`table` is its view). The
|
|
1278
|
+
* executor gates on the resolved row (write policy in `where`), writes only
|
|
1279
|
+
* the touched cells into the manual source, then re-checks `insertCheck`
|
|
1280
|
+
* against the resolved row. `revert` clears `revertColumns` from the manual
|
|
1281
|
+
* source; `delete` tombstones the row there.
|
|
1282
|
+
*/
|
|
1283
|
+
resolvedAction: zod.z.enum(["insert", "update", "delete", "revert"]).optional(),
|
|
1284
|
+
revertColumns: zod.z.array(zod.z.string()).optional(),
|
|
1285
|
+
/** `@writeback(source:, mode:)` directives on this root. */
|
|
1286
|
+
writeback: zod.z.array(zod.z.object({ source: zod.z.string(), mode: zod.z.enum(["sync", "async"]) })).optional(),
|
|
1287
|
+
/** `@localOnly(source:)` — writeback columns deliberately kept local for these sources. */
|
|
1288
|
+
localOnly: zod.z.array(zod.z.string()).optional(),
|
|
537
1289
|
alias: zod.z.string(),
|
|
538
1290
|
table: zod.z.string(),
|
|
539
1291
|
/** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
|
|
@@ -560,12 +1312,15 @@ var mutationRootSchema = zod.z.object({
|
|
|
560
1312
|
insertCheck: predIrSchema.optional(),
|
|
561
1313
|
returning: selectionIrSchema.nullable(),
|
|
562
1314
|
affectedRows: zod.z.boolean().default(false),
|
|
1315
|
+
/** v2: abort the transaction when the actual write count differs, regardless of selection. */
|
|
1316
|
+
assertAffectedRows: zod.z.number().int().min(0).max(2147483647).optional(),
|
|
563
1317
|
includeIf: includeIfSchema.optional()
|
|
564
1318
|
});
|
|
565
1319
|
var rootIrSchema = zod.z.discriminatedUnion("kind", [
|
|
566
1320
|
selectRootSchema,
|
|
567
1321
|
byPkRootSchema,
|
|
568
1322
|
aggregateRootSchema,
|
|
1323
|
+
aggregateGroupsRootSchema,
|
|
569
1324
|
mutationRootSchema
|
|
570
1325
|
]);
|
|
571
1326
|
var INPUT_REF_KINDS = [
|
|
@@ -595,7 +1350,7 @@ var variableIrSchema = zod.z.object({
|
|
|
595
1350
|
defaultValue: variableDefaultIrSchema.optional()
|
|
596
1351
|
});
|
|
597
1352
|
var compiledOperationSchema = zod.z.object({
|
|
598
|
-
irVersion:
|
|
1353
|
+
irVersion: irVersionSchema,
|
|
599
1354
|
name: zod.z.string(),
|
|
600
1355
|
opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
601
1356
|
kind: zod.z.enum(["query", "mutation"]),
|
|
@@ -607,6 +1362,50 @@ var compiledOperationSchema = zod.z.object({
|
|
|
607
1362
|
/** Every table the op touches — data-token scoping + audit. */
|
|
608
1363
|
tableSet: zod.z.array(zod.z.string()).min(1),
|
|
609
1364
|
roots: zod.z.array(rootIrSchema).min(1)
|
|
1365
|
+
}).superRefine((operation, ctx) => {
|
|
1366
|
+
for (const [index, root] of operation.roots.entries()) {
|
|
1367
|
+
if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
|
|
1368
|
+
if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
|
|
1369
|
+
ctx.addIssue({ code: "custom", path: ["roots", index], message: "affected-row assertions require unconditional mutation roots and IR v2" });
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1373
|
+
var resolvedSourceIrSchema = zod.z.object({
|
|
1374
|
+
name: zod.z.string(),
|
|
1375
|
+
kind: zod.z.enum(["manual", "etl", "prefetch"]),
|
|
1376
|
+
/** Logical table key of the backing relation. */
|
|
1377
|
+
table: zod.z.string(),
|
|
1378
|
+
columns: zod.z.array(zod.z.string()),
|
|
1379
|
+
key: zod.z.string().optional(),
|
|
1380
|
+
fetch: zod.z.union([
|
|
1381
|
+
zod.z.object({ shape: zod.z.literal("single") }),
|
|
1382
|
+
zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().positive() })
|
|
1383
|
+
]).optional(),
|
|
1384
|
+
function: zod.z.object({ kind: zod.z.enum(["managed", "native"]), id: zod.z.string() }).optional(),
|
|
1385
|
+
prefetchDefaults: zod.z.object({
|
|
1386
|
+
maxStalenessMs: zod.z.number().int().positive(),
|
|
1387
|
+
timeoutMs: zod.z.number().int().positive(),
|
|
1388
|
+
onError: zod.z.enum(["fallback", "fail"])
|
|
1389
|
+
}).optional(),
|
|
1390
|
+
writeback: zod.z.object({
|
|
1391
|
+
columns: zod.z.array(zod.z.string()),
|
|
1392
|
+
defaultMode: zod.z.enum(["sync", "async"]),
|
|
1393
|
+
timeoutMs: zod.z.number().int().positive(),
|
|
1394
|
+
clearManualOnConfirm: zod.z.boolean()
|
|
1395
|
+
}).optional()
|
|
1396
|
+
});
|
|
1397
|
+
var resolvedMetaIrSchema = zod.z.object({
|
|
1398
|
+
/** Logical pk column of the resolved view (single-column). */
|
|
1399
|
+
pk: zod.z.string(),
|
|
1400
|
+
/** Source name of the manual source. */
|
|
1401
|
+
manual: zod.z.string(),
|
|
1402
|
+
sources: zod.z.array(resolvedSourceIrSchema),
|
|
1403
|
+
deletedBy: zod.z.array(zod.z.string()),
|
|
1404
|
+
/**
|
|
1405
|
+
* Materialized: the view reads `table`, which the data plane refreshes from
|
|
1406
|
+
* the `compute` view (claims-gated) whenever it writes a source row.
|
|
1407
|
+
*/
|
|
1408
|
+
materialized: zod.z.object({ table: zod.z.string(), compute: zod.z.string() }).optional()
|
|
610
1409
|
});
|
|
611
1410
|
var tableMetaIrSchema = zod.z.object({
|
|
612
1411
|
table: zod.z.string(),
|
|
@@ -643,18 +1442,45 @@ var tableMetaIrSchema = zod.z.object({
|
|
|
643
1442
|
insertable: zod.z.array(zod.z.string()),
|
|
644
1443
|
settable: zod.z.array(zod.z.string()),
|
|
645
1444
|
appendable: zod.z.array(zod.z.string()),
|
|
1445
|
+
/**
|
|
1446
|
+
* Logical columns stamped with Postgres `now()` on UPDATE (canonical
|
|
1447
|
+
* `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
|
|
1448
|
+
*/
|
|
1449
|
+
touchOnUpdate: zod.z.array(zod.z.string()).optional(),
|
|
1450
|
+
/**
|
|
1451
|
+
* Per-cell metadata: logical column → the logical names of its cell-part
|
|
1452
|
+
* columns (which are listed in `columns`, but never filterable, orderable or
|
|
1453
|
+
* writable). Tables stamp `at`/`by` (and a manual source `rev`) on every
|
|
1454
|
+
* write; resolved views expose `source`/`at`/`by`/`conflicts`. Omitted when
|
|
1455
|
+
* the relation tracks no cells.
|
|
1456
|
+
*/
|
|
1457
|
+
cells: zod.z.record(zod.z.string(), zod.z.object({
|
|
1458
|
+
at: zod.z.string().optional(),
|
|
1459
|
+
by: zod.z.string().optional(),
|
|
1460
|
+
rev: zod.z.string().optional(),
|
|
1461
|
+
source: zod.z.string().optional(),
|
|
1462
|
+
conflicts: zod.z.string().optional()
|
|
1463
|
+
}).strict()).optional(),
|
|
1464
|
+
/** Resolved-table view metadata (sources, prefetch/writeback config) for the data plane. */
|
|
1465
|
+
resolved: resolvedMetaIrSchema.optional(),
|
|
646
1466
|
constraints: zod.z.array(zod.z.object({ name: zod.z.string(), columns: zod.z.array(zod.z.string()) })),
|
|
647
1467
|
/** Depth/node caps for dynamic filter trees on this table. */
|
|
648
1468
|
dynamicFilterMaxDepth: zod.z.number().int().positive().default(6),
|
|
649
1469
|
dynamicFilterMaxNodes: zod.z.number().int().positive().default(64)
|
|
650
1470
|
});
|
|
651
1471
|
var compiledArtifactSchema = zod.z.object({
|
|
652
|
-
irVersion:
|
|
1472
|
+
irVersion: irVersionSchema,
|
|
653
1473
|
namespace: zod.z.string(),
|
|
654
1474
|
schemaHash: zod.z.string(),
|
|
655
1475
|
opsHash: zod.z.string(),
|
|
656
1476
|
tables: zod.z.record(zod.z.string(), tableMetaIrSchema),
|
|
657
1477
|
operations: zod.z.record(zod.z.string(), compiledOperationSchema)
|
|
1478
|
+
}).superRefine((artifact, ctx) => {
|
|
1479
|
+
for (const [name, operation] of Object.entries(artifact.operations)) {
|
|
1480
|
+
if (operation.irVersion > artifact.irVersion) {
|
|
1481
|
+
ctx.addIssue({ code: "custom", path: ["operations", name, "irVersion"], message: "operation requires a newer IR version than its artifact" });
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
658
1484
|
});
|
|
659
1485
|
var IrVersionError = class extends Error {
|
|
660
1486
|
constructor(found) {
|
|
@@ -665,19 +1491,128 @@ var IrVersionError = class extends Error {
|
|
|
665
1491
|
function normalizeIr(raw) {
|
|
666
1492
|
if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
|
|
667
1493
|
const version = raw.irVersion;
|
|
668
|
-
if (version !== IR_VERSION) throw new IrVersionError(version);
|
|
1494
|
+
if (version !== LEGACY_IR_VERSION && version !== IR_VERSION) throw new IrVersionError(version);
|
|
669
1495
|
return compiledArtifactSchema.parse(raw);
|
|
670
1496
|
}
|
|
671
1497
|
|
|
1498
|
+
// src/sdl/naming.ts
|
|
1499
|
+
function camelCase(name) {
|
|
1500
|
+
const segments = name.split("_").filter((segment) => segment.length > 0);
|
|
1501
|
+
const first = segments[0];
|
|
1502
|
+
if (first === void 0) return name;
|
|
1503
|
+
const head = first.charAt(0).toLowerCase() + first.slice(1);
|
|
1504
|
+
return head + segments.slice(1).map(capitalizeFirst).join("");
|
|
1505
|
+
}
|
|
1506
|
+
function pascalCase(name) {
|
|
1507
|
+
return capitalizeFirst(camelCase(name));
|
|
1508
|
+
}
|
|
1509
|
+
function capitalizeFirst(segment) {
|
|
1510
|
+
return segment.charAt(0).toUpperCase() + segment.slice(1);
|
|
1511
|
+
}
|
|
1512
|
+
function singularizeWord(word) {
|
|
1513
|
+
if (word.endsWith("ies") && word.length > 3) return `${word.slice(0, -3)}y`;
|
|
1514
|
+
for (const suffix of ["sses", "xes", "ches", "shes"]) {
|
|
1515
|
+
if (word.endsWith(suffix)) return word.slice(0, -2);
|
|
1516
|
+
}
|
|
1517
|
+
if (word.endsWith("ss")) return word;
|
|
1518
|
+
if (word.endsWith("s") && word.length > 1) return word.slice(0, -1);
|
|
1519
|
+
return word;
|
|
1520
|
+
}
|
|
1521
|
+
function typeName(table) {
|
|
1522
|
+
const segments = table.split("_").filter((segment) => segment.length > 0);
|
|
1523
|
+
const last = segments[segments.length - 1];
|
|
1524
|
+
if (last === void 0) return pascalCase(table);
|
|
1525
|
+
return [...segments.slice(0, -1), singularizeWord(last)].map(capitalizeFirst).join("");
|
|
1526
|
+
}
|
|
1527
|
+
function enumTypeName(name) {
|
|
1528
|
+
return pascalCase(name);
|
|
1529
|
+
}
|
|
1530
|
+
var SCALAR_FOR_COLUMN_TYPE = {
|
|
1531
|
+
uuid: "uuid",
|
|
1532
|
+
text: "String",
|
|
1533
|
+
integer: "Int",
|
|
1534
|
+
bigint: "bigint",
|
|
1535
|
+
numeric: "numeric",
|
|
1536
|
+
double: "Float",
|
|
1537
|
+
boolean: "Boolean",
|
|
1538
|
+
timestamptz: "timestamptz",
|
|
1539
|
+
date: "date",
|
|
1540
|
+
jsonb: "jsonb"
|
|
1541
|
+
};
|
|
1542
|
+
function graphqlScalarFor(type) {
|
|
1543
|
+
return SCALAR_FOR_COLUMN_TYPE[type];
|
|
1544
|
+
}
|
|
1545
|
+
var COLUMN_TYPE_BY_SCALAR = new Map([
|
|
1546
|
+
["ID", "text"],
|
|
1547
|
+
...COLUMN_TYPES.map((type) => [graphqlScalarFor(type), type])
|
|
1548
|
+
]);
|
|
1549
|
+
function columnTypeForScalar(scalarName) {
|
|
1550
|
+
return COLUMN_TYPE_BY_SCALAR.get(scalarName);
|
|
1551
|
+
}
|
|
1552
|
+
var VARIABLE_SCALAR_NAMES = [...COLUMN_TYPE_BY_SCALAR.keys()];
|
|
1553
|
+
var PLATFORM_USER_TYPENAME = "PlatformUser";
|
|
1554
|
+
function tableNames(table) {
|
|
1555
|
+
const root = camelCase(table);
|
|
1556
|
+
const rootPascal = pascalCase(table);
|
|
1557
|
+
const type = typeName(table);
|
|
1558
|
+
return {
|
|
1559
|
+
typename: type,
|
|
1560
|
+
selectRoot: root,
|
|
1561
|
+
byPkRoot: `${root}ByPk`,
|
|
1562
|
+
aggregateRoot: `${root}Aggregate`,
|
|
1563
|
+
insertRoot: `insert${rootPascal}`,
|
|
1564
|
+
insertOneRoot: `insert${rootPascal}One`,
|
|
1565
|
+
updateRoot: `update${rootPascal}`,
|
|
1566
|
+
updateByPkRoot: `update${rootPascal}ByPk`,
|
|
1567
|
+
deleteRoot: `delete${rootPascal}`,
|
|
1568
|
+
deleteByPkRoot: `delete${rootPascal}ByPk`,
|
|
1569
|
+
revertByPkRoot: `revert${rootPascal}ByPk`,
|
|
1570
|
+
boolExp: `${type}BoolExp`,
|
|
1571
|
+
orderBy: `${type}OrderBy`,
|
|
1572
|
+
selectColumn: `${type}SelectColumn`,
|
|
1573
|
+
insertInput: `${type}InsertInput`,
|
|
1574
|
+
setInput: `${type}SetInput`,
|
|
1575
|
+
appendInput: `${type}AppendInput`,
|
|
1576
|
+
incInput: `${type}IncInput`,
|
|
1577
|
+
onConflict: `${type}OnConflict`,
|
|
1578
|
+
pkColumnsInput: `${type}PkColumnsInput`,
|
|
1579
|
+
constraintEnum: `${type}Constraint`,
|
|
1580
|
+
updateColumnEnum: `${type}UpdateColumn`,
|
|
1581
|
+
mutationResponse: `${type}MutationResponse`,
|
|
1582
|
+
aggregate: `${type}Aggregate`,
|
|
1583
|
+
aggregateFields: `${type}AggregateFields`,
|
|
1584
|
+
sumFields: `${type}SumFields`,
|
|
1585
|
+
avgFields: `${type}AvgFields`,
|
|
1586
|
+
aggregateBoolExp: `${type}AggregateBoolExp`,
|
|
1587
|
+
aggregateBoolExpCount: `${type}AggregateBoolExpCount`,
|
|
1588
|
+
aggregateGroupsRoot: `${root}AggregateGroups`,
|
|
1589
|
+
groupByColumnEnum: `${type}GroupByColumn`,
|
|
1590
|
+
groupByInput: `${type}GroupBy`,
|
|
1591
|
+
groupKey: `${type}GroupKey`,
|
|
1592
|
+
aggregateGroup: `${type}AggregateGroup`
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
var CELL_TYPENAMES = {
|
|
1596
|
+
/** A plain table's cell: when and by whom it was last written. */
|
|
1597
|
+
stamp: "CellStamp",
|
|
1598
|
+
/** A resolved table's cell: which source won, when, by whom, and newer losers. */
|
|
1599
|
+
resolved: "ResolvedCell",
|
|
1600
|
+
/** A newer candidate that lost to the column's rule. */
|
|
1601
|
+
conflict: "CellConflict"
|
|
1602
|
+
};
|
|
1603
|
+
function cellsTypename(typename) {
|
|
1604
|
+
return `${typename}Cells`;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
672
1607
|
// src/predicate-v2/ast.ts
|
|
673
1608
|
var POLICY_COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
|
|
674
|
-
function walkPolicy(node,
|
|
675
|
-
|
|
1609
|
+
function walkPolicy(node, visit2) {
|
|
1610
|
+
visit2(node);
|
|
676
1611
|
if (node.kind === "and" || node.kind === "or") {
|
|
677
|
-
for (const operand of node.operands) walkPolicy(operand,
|
|
1612
|
+
for (const operand of node.operands) walkPolicy(operand, visit2);
|
|
678
1613
|
}
|
|
679
|
-
if (node.kind === "not") walkPolicy(node.operand,
|
|
680
|
-
if (node.kind === "exists") walkPolicy(node.where,
|
|
1614
|
+
if (node.kind === "not") walkPolicy(node.operand, visit2);
|
|
1615
|
+
if (node.kind === "exists") walkPolicy(node.where, visit2);
|
|
681
1616
|
}
|
|
682
1617
|
|
|
683
1618
|
// src/predicate-v2/errors.ts
|
|
@@ -705,7 +1640,7 @@ var IDENTIFIER_CHAR = /[a-zA-Z0-9_]/;
|
|
|
705
1640
|
var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
|
|
706
1641
|
var SCALAR_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => !type.endsWith("[]")).map(([key]) => key);
|
|
707
1642
|
var ARRAY_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => type.endsWith("[]")).map(([key]) => key);
|
|
708
|
-
function
|
|
1643
|
+
function tokenize2(source) {
|
|
709
1644
|
const tokens = [];
|
|
710
1645
|
let position = 0;
|
|
711
1646
|
while (position < source.length) {
|
|
@@ -784,7 +1719,7 @@ function readString(source, start) {
|
|
|
784
1719
|
}
|
|
785
1720
|
throw new PolicyParseError("unterminated string literal", start);
|
|
786
1721
|
}
|
|
787
|
-
var
|
|
1722
|
+
var Parser2 = class {
|
|
788
1723
|
constructor(tokens) {
|
|
789
1724
|
this.tokens = tokens;
|
|
790
1725
|
}
|
|
@@ -958,7 +1893,7 @@ var Parser = class {
|
|
|
958
1893
|
function parsePolicy(source) {
|
|
959
1894
|
const trimmed = source.trim();
|
|
960
1895
|
if (trimmed.length === 0) throw new PolicyParseError("empty policy expression", 0);
|
|
961
|
-
return { source: trimmed, ast: new
|
|
1896
|
+
return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
|
|
962
1897
|
}
|
|
963
1898
|
|
|
964
1899
|
// src/predicate-v2/compile.ts
|
|
@@ -1183,21 +2118,21 @@ function andPred(...preds) {
|
|
|
1183
2118
|
if (operands.length === 1) return operands[0];
|
|
1184
2119
|
return { kind: "and", operands };
|
|
1185
2120
|
}
|
|
1186
|
-
function walkPred(pred,
|
|
1187
|
-
|
|
2121
|
+
function walkPred(pred, visit2) {
|
|
2122
|
+
visit2(pred);
|
|
1188
2123
|
switch (pred.kind) {
|
|
1189
2124
|
case "and":
|
|
1190
2125
|
case "or":
|
|
1191
|
-
for (const operand of pred.operands) walkPred(operand,
|
|
2126
|
+
for (const operand of pred.operands) walkPred(operand, visit2);
|
|
1192
2127
|
return;
|
|
1193
2128
|
case "not":
|
|
1194
|
-
walkPred(pred.operand,
|
|
2129
|
+
walkPred(pred.operand, visit2);
|
|
1195
2130
|
return;
|
|
1196
2131
|
case "exists":
|
|
1197
|
-
walkPred(pred.where,
|
|
2132
|
+
walkPred(pred.where, visit2);
|
|
1198
2133
|
return;
|
|
1199
2134
|
case "aggPredicate":
|
|
1200
|
-
if (pred.where) walkPred(pred.where,
|
|
2135
|
+
if (pred.where) walkPred(pred.where, visit2);
|
|
1201
2136
|
return;
|
|
1202
2137
|
default:
|
|
1203
2138
|
return;
|
|
@@ -1222,12 +2157,12 @@ function predContains(haystack, needle) {
|
|
|
1222
2157
|
// src/compiler-gql/inject.ts
|
|
1223
2158
|
function collectPinnedColumns(writePolicy) {
|
|
1224
2159
|
const key = (pin) => `${pin.column}\0${pin.claim}`;
|
|
1225
|
-
const
|
|
2160
|
+
const visit2 = (node) => {
|
|
1226
2161
|
if (node.kind === "and") {
|
|
1227
2162
|
const merged = [];
|
|
1228
2163
|
const seen2 = /* @__PURE__ */ new Set();
|
|
1229
2164
|
for (const operand of node.operands) {
|
|
1230
|
-
for (const pin of
|
|
2165
|
+
for (const pin of visit2(operand)) {
|
|
1231
2166
|
if (seen2.has(key(pin))) continue;
|
|
1232
2167
|
seen2.add(key(pin));
|
|
1233
2168
|
merged.push(pin);
|
|
@@ -1238,9 +2173,9 @@ function collectPinnedColumns(writePolicy) {
|
|
|
1238
2173
|
if (node.kind === "or") {
|
|
1239
2174
|
const [first, ...rest] = node.operands;
|
|
1240
2175
|
if (first === void 0) return [];
|
|
1241
|
-
let common =
|
|
2176
|
+
let common = visit2(first);
|
|
1242
2177
|
for (const operand of rest) {
|
|
1243
|
-
const branch = new Set(
|
|
2178
|
+
const branch = new Set(visit2(operand).map(key));
|
|
1244
2179
|
common = common.filter((pin) => branch.has(key(pin)));
|
|
1245
2180
|
if (common.length === 0) break;
|
|
1246
2181
|
}
|
|
@@ -1252,7 +2187,7 @@ function collectPinnedColumns(writePolicy) {
|
|
|
1252
2187
|
return [];
|
|
1253
2188
|
};
|
|
1254
2189
|
const seen = /* @__PURE__ */ new Set();
|
|
1255
|
-
return
|
|
2190
|
+
return visit2(writePolicy).filter((pin) => {
|
|
1256
2191
|
if (seen.has(pin.column)) return false;
|
|
1257
2192
|
seen.add(pin.column);
|
|
1258
2193
|
return true;
|
|
@@ -1346,6 +2281,11 @@ var PolicyInjector = class {
|
|
|
1346
2281
|
where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table)),
|
|
1347
2282
|
...root.nodes ? { nodes: this.injectSelection(root.nodes, root.table) } : {}
|
|
1348
2283
|
};
|
|
2284
|
+
case "aggregateGroups":
|
|
2285
|
+
return {
|
|
2286
|
+
...root,
|
|
2287
|
+
where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table))
|
|
2288
|
+
};
|
|
1349
2289
|
case "mutation":
|
|
1350
2290
|
return this.injectMutation(root);
|
|
1351
2291
|
}
|
|
@@ -1353,6 +2293,7 @@ var PolicyInjector = class {
|
|
|
1353
2293
|
injectMutation(root) {
|
|
1354
2294
|
const table = root.table;
|
|
1355
2295
|
const returning = root.returning ? this.injectSelection(root.returning, table) : root.returning;
|
|
2296
|
+
if (root.op === "resolvedWrite") return this.injectResolvedWrite(root, returning);
|
|
1356
2297
|
if (root.op === "insert" || root.op === "insertOne") {
|
|
1357
2298
|
const insertPolicy = this.insert(table);
|
|
1358
2299
|
if (predContainsExists(insertPolicy)) {
|
|
@@ -1394,6 +2335,24 @@ var PolicyInjector = class {
|
|
|
1394
2335
|
}
|
|
1395
2336
|
return { ...root, where, returning };
|
|
1396
2337
|
}
|
|
2338
|
+
/**
|
|
2339
|
+
* Resolved writes are policed against the RESOLVED row, not the sparse
|
|
2340
|
+
* manual row: `where` gates the existing row (update/revert/delete) and
|
|
2341
|
+
* `insertCheck` is re-evaluated as a SELECT over the view after the write,
|
|
2342
|
+
* so link-traversing (exists) policies are representable here.
|
|
2343
|
+
*/
|
|
2344
|
+
injectResolvedWrite(root, returning) {
|
|
2345
|
+
const table = root.table;
|
|
2346
|
+
if (root.resolvedAction === "insert") {
|
|
2347
|
+
const pins = (this.ctx.pins.get(table) ?? []).map(
|
|
2348
|
+
(pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
|
|
2349
|
+
);
|
|
2350
|
+
return { ...root, pinnedColumns: pins, insertCheck: this.insert(table), returning };
|
|
2351
|
+
}
|
|
2352
|
+
const updatePolicy = this.update(table);
|
|
2353
|
+
const where = andPred(updatePolicy, root.where ? this.injectDocPred(root.where, table) : void 0);
|
|
2354
|
+
return root.resolvedAction === "delete" ? { ...root, where, returning } : { ...root, where, insertCheck: updatePolicy, returning };
|
|
2355
|
+
}
|
|
1397
2356
|
};
|
|
1398
2357
|
function collectTableSet(roots, ctx) {
|
|
1399
2358
|
const tables = /* @__PURE__ */ new Set();
|
|
@@ -1436,122 +2395,44 @@ function collectTableSet(roots, ctx) {
|
|
|
1436
2395
|
if (aggregate.nodes) visitSelection(aggregate.nodes, target);
|
|
1437
2396
|
}
|
|
1438
2397
|
};
|
|
1439
|
-
for (const root of roots) {
|
|
1440
|
-
tables.add(root.table);
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
const head = first.charAt(0).toLowerCase() + first.slice(1);
|
|
1471
|
-
return head + segments.slice(1).map(capitalizeFirst).join("");
|
|
1472
|
-
}
|
|
1473
|
-
function pascalCase(name) {
|
|
1474
|
-
return capitalizeFirst(camelCase(name));
|
|
1475
|
-
}
|
|
1476
|
-
function capitalizeFirst(segment) {
|
|
1477
|
-
return segment.charAt(0).toUpperCase() + segment.slice(1);
|
|
1478
|
-
}
|
|
1479
|
-
function singularizeWord(word) {
|
|
1480
|
-
if (word.endsWith("ies") && word.length > 3) return `${word.slice(0, -3)}y`;
|
|
1481
|
-
for (const suffix of ["sses", "xes", "ches", "shes"]) {
|
|
1482
|
-
if (word.endsWith(suffix)) return word.slice(0, -2);
|
|
2398
|
+
for (const root of roots) {
|
|
2399
|
+
tables.add(root.table);
|
|
2400
|
+
switch (root.kind) {
|
|
2401
|
+
case "select":
|
|
2402
|
+
case "byPk":
|
|
2403
|
+
visitPred(root.where, root.table);
|
|
2404
|
+
visitSelection(root.selection, root.table);
|
|
2405
|
+
break;
|
|
2406
|
+
case "aggregate":
|
|
2407
|
+
visitPred(root.where, root.table);
|
|
2408
|
+
if (root.nodes) visitSelection(root.nodes, root.table);
|
|
2409
|
+
break;
|
|
2410
|
+
case "aggregateGroups":
|
|
2411
|
+
visitPred(root.where, root.table);
|
|
2412
|
+
break;
|
|
2413
|
+
case "mutation":
|
|
2414
|
+
if (root.cowSynced !== void 0) tables.add(root.cowSynced);
|
|
2415
|
+
for (const source of ctx.tables[root.table]?.resolved?.sources ?? []) tables.add(source.table);
|
|
2416
|
+
if (root.where) visitPred(root.where, root.table);
|
|
2417
|
+
if (root.insertCheck) visitPred(root.insertCheck, root.table);
|
|
2418
|
+
if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
|
|
2419
|
+
if (root.returning) visitSelection(root.returning, root.table);
|
|
2420
|
+
break;
|
|
2421
|
+
default: {
|
|
2422
|
+
const exhausted = root;
|
|
2423
|
+
throw new DocumentCompileError(
|
|
2424
|
+
`internal: unhandled root kind in tableSet collection ${JSON.stringify(exhausted)}`,
|
|
2425
|
+
"internal"
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
1483
2429
|
}
|
|
1484
|
-
|
|
1485
|
-
if (word.endsWith("s") && word.length > 1) return word.slice(0, -1);
|
|
1486
|
-
return word;
|
|
1487
|
-
}
|
|
1488
|
-
function typeName(table) {
|
|
1489
|
-
const segments = table.split("_").filter((segment) => segment.length > 0);
|
|
1490
|
-
const last = segments[segments.length - 1];
|
|
1491
|
-
if (last === void 0) return pascalCase(table);
|
|
1492
|
-
return [...segments.slice(0, -1), singularizeWord(last)].map(capitalizeFirst).join("");
|
|
1493
|
-
}
|
|
1494
|
-
function enumTypeName(name) {
|
|
1495
|
-
return pascalCase(name);
|
|
1496
|
-
}
|
|
1497
|
-
var SCALAR_FOR_COLUMN_TYPE = {
|
|
1498
|
-
uuid: "uuid",
|
|
1499
|
-
text: "String",
|
|
1500
|
-
integer: "Int",
|
|
1501
|
-
bigint: "bigint",
|
|
1502
|
-
numeric: "numeric",
|
|
1503
|
-
double: "Float",
|
|
1504
|
-
boolean: "Boolean",
|
|
1505
|
-
timestamptz: "timestamptz",
|
|
1506
|
-
date: "date",
|
|
1507
|
-
jsonb: "jsonb"
|
|
1508
|
-
};
|
|
1509
|
-
function graphqlScalarFor(type) {
|
|
1510
|
-
return SCALAR_FOR_COLUMN_TYPE[type];
|
|
1511
|
-
}
|
|
1512
|
-
var COLUMN_TYPE_BY_SCALAR = new Map([
|
|
1513
|
-
["ID", "text"],
|
|
1514
|
-
...COLUMN_TYPES.map((type) => [graphqlScalarFor(type), type])
|
|
1515
|
-
]);
|
|
1516
|
-
function columnTypeForScalar(scalarName) {
|
|
1517
|
-
return COLUMN_TYPE_BY_SCALAR.get(scalarName);
|
|
2430
|
+
return [...tables].sort();
|
|
1518
2431
|
}
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
const rootPascal = pascalCase(table);
|
|
1524
|
-
const type = typeName(table);
|
|
1525
|
-
return {
|
|
1526
|
-
typename: type,
|
|
1527
|
-
selectRoot: root,
|
|
1528
|
-
byPkRoot: `${root}ByPk`,
|
|
1529
|
-
aggregateRoot: `${root}Aggregate`,
|
|
1530
|
-
insertRoot: `insert${rootPascal}`,
|
|
1531
|
-
insertOneRoot: `insert${rootPascal}One`,
|
|
1532
|
-
updateRoot: `update${rootPascal}`,
|
|
1533
|
-
updateByPkRoot: `update${rootPascal}ByPk`,
|
|
1534
|
-
deleteRoot: `delete${rootPascal}`,
|
|
1535
|
-
deleteByPkRoot: `delete${rootPascal}ByPk`,
|
|
1536
|
-
boolExp: `${type}BoolExp`,
|
|
1537
|
-
orderBy: `${type}OrderBy`,
|
|
1538
|
-
selectColumn: `${type}SelectColumn`,
|
|
1539
|
-
insertInput: `${type}InsertInput`,
|
|
1540
|
-
setInput: `${type}SetInput`,
|
|
1541
|
-
appendInput: `${type}AppendInput`,
|
|
1542
|
-
incInput: `${type}IncInput`,
|
|
1543
|
-
onConflict: `${type}OnConflict`,
|
|
1544
|
-
pkColumnsInput: `${type}PkColumnsInput`,
|
|
1545
|
-
constraintEnum: `${type}Constraint`,
|
|
1546
|
-
updateColumnEnum: `${type}UpdateColumn`,
|
|
1547
|
-
mutationResponse: `${type}MutationResponse`,
|
|
1548
|
-
aggregate: `${type}Aggregate`,
|
|
1549
|
-
aggregateFields: `${type}AggregateFields`,
|
|
1550
|
-
sumFields: `${type}SumFields`,
|
|
1551
|
-
avgFields: `${type}AvgFields`,
|
|
1552
|
-
aggregateBoolExp: `${type}AggregateBoolExp`,
|
|
1553
|
-
aggregateBoolExpCount: `${type}AggregateBoolExpCount`
|
|
1554
|
-
};
|
|
2432
|
+
function injectPolicies({ roots, ctx }) {
|
|
2433
|
+
const injector = new PolicyInjector(ctx);
|
|
2434
|
+
const injected = roots.map((root) => injector.injectRoot(root));
|
|
2435
|
+
return { roots: injected, tableSet: collectTableSet(injected, ctx) };
|
|
1555
2436
|
}
|
|
1556
2437
|
|
|
1557
2438
|
// src/compiler-gql/naming.ts
|
|
@@ -1561,7 +2442,12 @@ function typeNameForTable(table) {
|
|
|
1561
2442
|
}
|
|
1562
2443
|
function queryRootNames(table) {
|
|
1563
2444
|
const names = tableNames(table);
|
|
1564
|
-
return {
|
|
2445
|
+
return {
|
|
2446
|
+
select: names.selectRoot,
|
|
2447
|
+
byPk: names.byPkRoot,
|
|
2448
|
+
aggregate: names.aggregateRoot,
|
|
2449
|
+
aggregateGroups: names.aggregateGroupsRoot
|
|
2450
|
+
};
|
|
1565
2451
|
}
|
|
1566
2452
|
function mutationRootNames(table) {
|
|
1567
2453
|
const names = tableNames(table);
|
|
@@ -1654,7 +2540,15 @@ function resolveColumnEntries(table, enums, diagnostics) {
|
|
|
1654
2540
|
type = { enum: enumDef.name, values: [...enumDef.values] };
|
|
1655
2541
|
}
|
|
1656
2542
|
}
|
|
1657
|
-
return {
|
|
2543
|
+
return {
|
|
2544
|
+
name: column.name,
|
|
2545
|
+
columnName: column.columnName,
|
|
2546
|
+
type,
|
|
2547
|
+
nullable: column.nullable,
|
|
2548
|
+
deprecated: column.deprecated,
|
|
2549
|
+
onUpdateFn: column.onUpdateFn !== void 0,
|
|
2550
|
+
...column.cell ? { cell: column.cell } : {}
|
|
2551
|
+
};
|
|
1658
2552
|
});
|
|
1659
2553
|
}
|
|
1660
2554
|
function logicalColumn(entries, ref) {
|
|
@@ -1690,8 +2584,8 @@ function buildCompileContext(rawCanonical) {
|
|
|
1690
2584
|
const pkRefs = table.primaryKey ?? (isView ? [] : ["id"]);
|
|
1691
2585
|
const primaryKey = [];
|
|
1692
2586
|
for (const ref of pkRefs) {
|
|
1693
|
-
const
|
|
1694
|
-
if (!
|
|
2587
|
+
const resolved2 = logicalColumn(entries, ref);
|
|
2588
|
+
if (!resolved2) {
|
|
1695
2589
|
diagnostics.push({
|
|
1696
2590
|
severity: "error",
|
|
1697
2591
|
code: "unknown-pk-column",
|
|
@@ -1700,9 +2594,13 @@ function buildCompileContext(rawCanonical) {
|
|
|
1700
2594
|
});
|
|
1701
2595
|
continue;
|
|
1702
2596
|
}
|
|
1703
|
-
primaryKey.push(
|
|
2597
|
+
primaryKey.push(resolved2);
|
|
1704
2598
|
}
|
|
1705
|
-
const
|
|
2599
|
+
const resolved = isView ? table.resolved : void 0;
|
|
2600
|
+
const data = entries.filter((c) => c.cell === void 0);
|
|
2601
|
+
const cells = cellMap(entries);
|
|
2602
|
+
const active = data.filter((c) => !c.deprecated);
|
|
2603
|
+
const touchOnUpdate = isView ? [] : active.filter((c) => c.onUpdateFn).map((c) => c.name);
|
|
1706
2604
|
const constraints = [];
|
|
1707
2605
|
if (primaryKey.length > 0) constraints.push({ name: `${table.table}_pkey`, columns: [...primaryKey] });
|
|
1708
2606
|
for (const column of table.columns) {
|
|
@@ -1723,14 +2621,17 @@ function buildCompileContext(rawCanonical) {
|
|
|
1723
2621
|
// Synced twins (and views reading them, resolved transitively below)
|
|
1724
2622
|
// are claims-gated — see tableMetaIrSchema.claimsGated.
|
|
1725
2623
|
claimsGated: relationIsClaimsGated(table.table),
|
|
1726
|
-
columns: entries.map(({ deprecated: _deprecated, ...column }) => column),
|
|
2624
|
+
columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
|
|
1727
2625
|
primaryKey: isView ? primaryKey : primaryKey.length > 0 ? primaryKey : ["id"],
|
|
1728
2626
|
links: [],
|
|
1729
|
-
filterable:
|
|
1730
|
-
orderable:
|
|
1731
|
-
insertable: isView ? [] : active.map((c) => c.name),
|
|
1732
|
-
settable: isView ? [] : active.map((c) => c.name),
|
|
2627
|
+
filterable: data.map((c) => c.name),
|
|
2628
|
+
orderable: data.filter((c) => c.type !== "jsonb").map((c) => c.name),
|
|
2629
|
+
insertable: resolved ? resolvedWritable(data) : isView ? [] : active.map((c) => c.name),
|
|
2630
|
+
settable: resolved ? resolvedWritable(data).filter((name) => !primaryKey.includes(name)) : isView ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
|
|
1733
2631
|
appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
|
|
2632
|
+
...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
|
|
2633
|
+
...cells ? { cells } : {},
|
|
2634
|
+
...resolved ? { resolved: resolvedMeta({ resolved, pk: primaryKey[0] }) } : {},
|
|
1734
2635
|
constraints: isView ? [] : constraints,
|
|
1735
2636
|
dynamicFilterMaxDepth: 6,
|
|
1736
2637
|
dynamicFilterMaxNodes: 64
|
|
@@ -1751,7 +2652,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
1751
2652
|
});
|
|
1752
2653
|
continue;
|
|
1753
2654
|
}
|
|
1754
|
-
const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false })) : columnEntries.get(targetName);
|
|
2655
|
+
const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false, onUpdateFn: false })) : columnEntries.get(targetName);
|
|
1755
2656
|
const onEntries = link.kind === "toMany" ? targetEntries : entries;
|
|
1756
2657
|
const referencesEntries = link.kind === "toMany" ? entries : targetEntries;
|
|
1757
2658
|
const on = logicalColumn(onEntries, link.on);
|
|
@@ -1803,7 +2704,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
1803
2704
|
compilePolicyFor("write", writePolicies);
|
|
1804
2705
|
compilePolicyFor("insert", insertPolicies);
|
|
1805
2706
|
compilePolicyFor("update", updatePolicies);
|
|
1806
|
-
if (isView) {
|
|
2707
|
+
if (isView && table.resolved === void 0) {
|
|
1807
2708
|
writePolicies.set(table.table, CONST_FALSE);
|
|
1808
2709
|
insertPolicies.set(table.table, CONST_FALSE);
|
|
1809
2710
|
updatePolicies.set(table.table, CONST_FALSE);
|
|
@@ -1842,18 +2743,23 @@ function buildCompileContext(rawCanonical) {
|
|
|
1842
2743
|
const queryRoots = /* @__PURE__ */ new Map();
|
|
1843
2744
|
const mutationRoots = /* @__PURE__ */ new Map();
|
|
1844
2745
|
for (const table of relations) {
|
|
2746
|
+
if (table.internal === true) continue;
|
|
1845
2747
|
const roots = queryRootNames(table.table);
|
|
1846
2748
|
queryRoots.set(roots.select, { kind: "select", table: table.table });
|
|
1847
2749
|
if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
|
|
1848
2750
|
queryRoots.set(roots.byPk, { kind: "byPk", table: table.table });
|
|
1849
2751
|
}
|
|
1850
2752
|
queryRoots.set(roots.aggregate, { kind: "aggregate", table: table.table });
|
|
2753
|
+
if (table.columns.some((column) => column.type !== "jsonb")) {
|
|
2754
|
+
queryRoots.set(roots.aggregateGroups, { kind: "aggregateGroups", table: table.table });
|
|
2755
|
+
}
|
|
1851
2756
|
const hasWritePolicy = table.policies.write !== void 0 || table.policies.insert !== void 0 || table.policies.update !== void 0;
|
|
1852
2757
|
if (!viewNames.has(table.table) && table.source === void 0 && hasWritePolicy) {
|
|
1853
2758
|
for (const [op, field] of Object.entries(mutationRootNames(table.table))) {
|
|
1854
2759
|
mutationRoots.set(field, { op, table: table.table });
|
|
1855
2760
|
}
|
|
1856
2761
|
}
|
|
2762
|
+
if (table.resolved !== void 0) registerResolvedRoots({ table, mutationRoots });
|
|
1857
2763
|
}
|
|
1858
2764
|
for (const table of relations) {
|
|
1859
2765
|
for (const mutation of table.cowMutations ?? []) {
|
|
@@ -1924,6 +2830,49 @@ function buildCompileContext(rawCanonical) {
|
|
|
1924
2830
|
diagnostics
|
|
1925
2831
|
};
|
|
1926
2832
|
}
|
|
2833
|
+
function registerResolvedRoots({ table, mutationRoots }) {
|
|
2834
|
+
const names = tableNames(table.table);
|
|
2835
|
+
const canInsert = table.policies.write !== void 0 || table.policies.insert !== void 0;
|
|
2836
|
+
const canUpdate = table.policies.write !== void 0 || table.policies.update !== void 0;
|
|
2837
|
+
const add = (field, action) => mutationRoots.set(field, { op: "resolvedWrite", action, table: table.table });
|
|
2838
|
+
if (canInsert) add(names.insertOneRoot, "insert");
|
|
2839
|
+
if (canUpdate) {
|
|
2840
|
+
add(names.updateByPkRoot, "update");
|
|
2841
|
+
add(names.revertByPkRoot, "revert");
|
|
2842
|
+
if ((table.resolved?.deletedBy.length ?? 0) > 0) add(names.deleteByPkRoot, "delete");
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
function cellMap(entries) {
|
|
2846
|
+
const cells = {};
|
|
2847
|
+
for (const entry of entries) {
|
|
2848
|
+
if (entry.cell) cells[entry.cell.of] = { ...cells[entry.cell.of], [entry.cell.part]: entry.name };
|
|
2849
|
+
}
|
|
2850
|
+
return Object.keys(cells).length > 0 ? cells : void 0;
|
|
2851
|
+
}
|
|
2852
|
+
function resolvedWritable(entries) {
|
|
2853
|
+
const system = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((column) => column.name));
|
|
2854
|
+
return entries.filter((entry) => !entry.deprecated && !system.has(entry.name)).map((entry) => entry.name);
|
|
2855
|
+
}
|
|
2856
|
+
function resolvedMeta({ resolved, pk }) {
|
|
2857
|
+
const manual = resolved.sources.find((source) => source.kind === "manual");
|
|
2858
|
+
return {
|
|
2859
|
+
pk,
|
|
2860
|
+
manual: manual?.name ?? "",
|
|
2861
|
+
deletedBy: [...resolved.deletedBy],
|
|
2862
|
+
...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
|
|
2863
|
+
sources: resolved.sources.map((source) => ({
|
|
2864
|
+
name: source.name,
|
|
2865
|
+
kind: source.kind,
|
|
2866
|
+
table: source.table,
|
|
2867
|
+
columns: [...source.columns],
|
|
2868
|
+
...source.key !== void 0 ? { key: source.key } : {},
|
|
2869
|
+
...source.fetch !== void 0 ? { fetch: source.fetch } : {},
|
|
2870
|
+
...source.function !== void 0 ? { function: source.function } : {},
|
|
2871
|
+
...source.prefetchDefaults !== void 0 ? { prefetchDefaults: source.prefetchDefaults } : {},
|
|
2872
|
+
...source.writeback !== void 0 ? { writeback: source.writeback } : {}
|
|
2873
|
+
}))
|
|
2874
|
+
};
|
|
2875
|
+
}
|
|
1927
2876
|
function flattenSelectionSet(selectionSet, fragments, stack) {
|
|
1928
2877
|
const selections = [];
|
|
1929
2878
|
for (const selection of selectionSet.selections) {
|
|
@@ -1981,8 +2930,148 @@ function computeOpsHash(opHashes) {
|
|
|
1981
2930
|
const digest = crypto.createHash("sha256").update(OPS_HASH_DOMAIN).update([...opHashes].sort().join("\n")).digest("hex");
|
|
1982
2931
|
return `sha256:${digest}`;
|
|
1983
2932
|
}
|
|
2933
|
+
var SOURCE_DIRECTIVES = /* @__PURE__ */ new Set(["prefetch", "writeback", "localOnly"]);
|
|
2934
|
+
var fail = (message, code) => {
|
|
2935
|
+
throw new DocumentCompileError(message, code);
|
|
2936
|
+
};
|
|
2937
|
+
function readArgs(node, name) {
|
|
2938
|
+
return (node.directives ?? []).filter((directive) => directive.name.value === name).map((directive) => {
|
|
2939
|
+
const args = {};
|
|
2940
|
+
for (const arg of directive.arguments ?? []) {
|
|
2941
|
+
const key = arg.name.value;
|
|
2942
|
+
if (key === "source" || key === "maxStaleness") {
|
|
2943
|
+
if (arg.value.kind !== graphql.Kind.STRING) fail(`@${name}(${key}:) must be a string literal`, "invalid-source-directive");
|
|
2944
|
+
args[key] = arg.value.value;
|
|
2945
|
+
} else if (key === "mode" && name === "writeback") {
|
|
2946
|
+
if (arg.value.kind !== graphql.Kind.ENUM || !["SYNC", "ASYNC"].includes(arg.value.value)) {
|
|
2947
|
+
fail("@writeback(mode:) must be SYNC or ASYNC", "invalid-source-directive");
|
|
2948
|
+
}
|
|
2949
|
+
args.mode = arg.value.value === "SYNC" ? "sync" : "async";
|
|
2950
|
+
} else {
|
|
2951
|
+
fail(`@${name} does not take argument '${key}'`, "invalid-source-directive");
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
if (!args.source) fail(`@${name} needs a source: argument`, "invalid-source-directive");
|
|
2955
|
+
return args;
|
|
2956
|
+
});
|
|
2957
|
+
}
|
|
2958
|
+
function prefetchSourceFor({ resolved, name, directive, table }) {
|
|
2959
|
+
if (!resolved) return fail(`@${directive} is only valid on resolved tables \u2014 '${table}' is not one`, "invalid-source-directive");
|
|
2960
|
+
const source = resolved.sources.find((entry) => entry.name === name);
|
|
2961
|
+
if (!source || source.kind !== "prefetch") {
|
|
2962
|
+
const options = resolved.sources.filter((entry) => entry.kind === "prefetch").map((entry) => entry.name);
|
|
2963
|
+
return fail(`@${directive}(source: "${name}") on '${table}' must name a prefetch source (${options.join(", ") || "none declared"})`, "invalid-source-directive");
|
|
2964
|
+
}
|
|
2965
|
+
return source;
|
|
2966
|
+
}
|
|
2967
|
+
function applyPrefetchDirectives({ node, root, meta }) {
|
|
2968
|
+
const directives = readArgs(node, "prefetch");
|
|
2969
|
+
for (const name of ["writeback", "localOnly"]) {
|
|
2970
|
+
if (readArgs(node, name).length > 0) fail(`@${name} is only valid on resolved-table mutation roots`, "invalid-source-directive");
|
|
2971
|
+
}
|
|
2972
|
+
if (directives.length === 0) return root;
|
|
2973
|
+
if (root.kind !== "select" && root.kind !== "byPk") {
|
|
2974
|
+
return fail(`@prefetch is only valid on select and byPk roots, not '${root.alias}'`, "invalid-source-directive");
|
|
2975
|
+
}
|
|
2976
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2977
|
+
const plans = directives.map((directive) => {
|
|
2978
|
+
const source = prefetchSourceFor({ resolved: meta.resolved, name: directive.source, directive: "prefetch", table: root.table });
|
|
2979
|
+
if (seen.has(source.name)) fail(`@prefetch(source: "${source.name}") appears twice on '${root.alias}'`, "invalid-source-directive");
|
|
2980
|
+
seen.add(source.name);
|
|
2981
|
+
const maxStalenessMs = directive.maxStaleness === void 0 ? void 0 : parseDurationMs(directive.maxStaleness);
|
|
2982
|
+
if (directive.maxStaleness !== void 0 && maxStalenessMs === void 0) {
|
|
2983
|
+
fail(`@prefetch(maxStaleness: "${directive.maxStaleness}") must look like '500ms', '30s' or '5m'`, "invalid-source-directive");
|
|
2984
|
+
}
|
|
2985
|
+
const key = root.kind === "byPk" ? { one: root.pk[0].value } : selectKey({ where: root.where, pk: meta.resolved.pk, source, alias: root.alias });
|
|
2986
|
+
return { source: source.name, ...maxStalenessMs !== void 0 ? { maxStalenessMs } : {}, key };
|
|
2987
|
+
});
|
|
2988
|
+
return { ...root, prefetch: plans };
|
|
2989
|
+
}
|
|
2990
|
+
function selectKey({ where, pk, source, alias }) {
|
|
2991
|
+
const leaves = where.kind === "and" ? where.operands : [where];
|
|
2992
|
+
const pinned = leaves.find((leaf) => leaf.kind === "cmp" && leaf.column === pk && (leaf.op === "eq" || leaf.op === "in"));
|
|
2993
|
+
const shape = source.fetch?.shape ?? "single";
|
|
2994
|
+
if (!pinned || pinned.kind !== "cmp") {
|
|
2995
|
+
return fail(
|
|
2996
|
+
`@prefetch(source: "${source.name}") on '${alias}': the root is not key-addressed \u2014 its where must pin '${pk}' with _eq${shape === "batch" ? " or _in" : ""} (${source.name} supports ${shape === "batch" ? "batch" : "single-key"} fetch)`,
|
|
2997
|
+
"prefetch-shape-mismatch"
|
|
2998
|
+
);
|
|
2999
|
+
}
|
|
3000
|
+
if (pinned.op === "in" && shape !== "batch") {
|
|
3001
|
+
return fail(`@prefetch(source: "${source.name}") on '${alias}': ${source.name} only supports single-key fetch, but the root selects '${pk}' with _in`, "prefetch-shape-mismatch");
|
|
3002
|
+
}
|
|
3003
|
+
return pinned.op === "eq" ? { one: pinned.value } : { many: pinned.value };
|
|
3004
|
+
}
|
|
3005
|
+
function applyWritebackDirectives({ node, root, meta }) {
|
|
3006
|
+
if (readArgs(node, "prefetch").length > 0) fail("@prefetch is only valid on query roots", "invalid-source-directive");
|
|
3007
|
+
const writebacks = readArgs(node, "writeback");
|
|
3008
|
+
const localOnly = readArgs(node, "localOnly");
|
|
3009
|
+
if (root.kind !== "mutation" || root.op !== "resolvedWrite" || !meta.resolved) {
|
|
3010
|
+
if (writebacks.length + localOnly.length > 0) fail("@writeback/@localOnly are only valid on resolved-table mutation roots", "invalid-source-directive");
|
|
3011
|
+
return root;
|
|
3012
|
+
}
|
|
3013
|
+
const resolved = meta.resolved;
|
|
3014
|
+
const declared = /* @__PURE__ */ new Map();
|
|
3015
|
+
const bindings = [];
|
|
3016
|
+
for (const directive of writebacks) {
|
|
3017
|
+
const source = prefetchSourceFor({ resolved, name: directive.source, directive: "writeback", table: root.table });
|
|
3018
|
+
if (!source.writeback) fail(`@writeback(source: "${source.name}"): source '${source.name}' declares no writeback columns`, "invalid-source-directive");
|
|
3019
|
+
if (root.resolvedAction !== "update") {
|
|
3020
|
+
fail(`@writeback on '${root.alias}': only update roots write back in v1 (use @localOnly for ${root.resolvedAction ?? "this"} roots)`, "writeback-shape-mismatch");
|
|
3021
|
+
}
|
|
3022
|
+
if (declared.has(source.name)) fail(`source '${source.name}' is declared twice on '${root.alias}'`, "invalid-source-directive");
|
|
3023
|
+
declared.set(source.name, "writeback");
|
|
3024
|
+
bindings.push({ source: source.name, mode: directive.mode ?? source.writeback.defaultMode });
|
|
3025
|
+
}
|
|
3026
|
+
for (const directive of localOnly) {
|
|
3027
|
+
const source = prefetchSourceFor({ resolved, name: directive.source, directive: "localOnly", table: root.table });
|
|
3028
|
+
if (declared.has(source.name)) fail(`source '${source.name}' is declared twice on '${root.alias}'`, "invalid-source-directive");
|
|
3029
|
+
declared.set(source.name, "localOnly");
|
|
3030
|
+
}
|
|
3031
|
+
assertWritebacksDeclared({ root, meta, resolved, declared });
|
|
3032
|
+
return {
|
|
3033
|
+
...root,
|
|
3034
|
+
...bindings.length > 0 ? { writeback: bindings } : {},
|
|
3035
|
+
...localOnly.length > 0 ? { localOnly: localOnly.map((directive) => directive.source) } : {}
|
|
3036
|
+
};
|
|
3037
|
+
}
|
|
3038
|
+
function assertWritebacksDeclared({ root, meta, resolved, declared }) {
|
|
3039
|
+
const written = writtenColumns({ root, meta });
|
|
3040
|
+
for (const source of resolved.sources) {
|
|
3041
|
+
const columns = source.writeback?.columns.filter((column) => written.has(column)) ?? [];
|
|
3042
|
+
if (columns.length > 0 && !declared.has(source.name)) {
|
|
3043
|
+
fail(
|
|
3044
|
+
`'${root.alias}' sets ${columns.map((column) => `'${column}'`).join(", ")}, which source '${source.name}' writes back \u2014 add @writeback(source: "${source.name}") to push the change or @localOnly(source: "${source.name}") to keep it local`,
|
|
3045
|
+
"writeback-undeclared"
|
|
3046
|
+
);
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
function writtenColumns({ root, meta }) {
|
|
3051
|
+
if (root.resolvedAction === "update") {
|
|
3052
|
+
if (!root.set || "slot" in root.set) return new Set(meta.settable);
|
|
3053
|
+
return new Set(root.set.entries.map((entry) => entry.column));
|
|
3054
|
+
}
|
|
3055
|
+
if (root.resolvedAction === "insert") {
|
|
3056
|
+
if (!root.objects || "slot" in root.objects) return new Set(meta.insertable);
|
|
3057
|
+
return new Set(root.objects.rows.flatMap((row) => row.map((entry) => entry.column)));
|
|
3058
|
+
}
|
|
3059
|
+
return /* @__PURE__ */ new Set();
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
// src/compiler-gql/lower.ts
|
|
1984
3063
|
var DEFAULT_LIMIT = 100;
|
|
1985
3064
|
var MAX_LIMIT = 1e3;
|
|
3065
|
+
var MAX_GROUPS = 1e3;
|
|
3066
|
+
var DATE_BUCKET_BY_ENUM = {
|
|
3067
|
+
YEAR: "year",
|
|
3068
|
+
QUARTER: "quarter",
|
|
3069
|
+
MONTH: "month",
|
|
3070
|
+
WEEK: "week",
|
|
3071
|
+
DAY: "day",
|
|
3072
|
+
HOUR: "hour"
|
|
3073
|
+
};
|
|
3074
|
+
var BUCKETABLE_TYPES = /* @__PURE__ */ new Set(["timestamptz", "date"]);
|
|
1986
3075
|
var BOOLEXP_OPS = {
|
|
1987
3076
|
_eq: "eq",
|
|
1988
3077
|
_neq: "neq",
|
|
@@ -2008,16 +3097,24 @@ var ORDER_DIRECTIONS = {
|
|
|
2008
3097
|
DESC_NULLS_LAST: { direction: "desc", nulls: "last" }
|
|
2009
3098
|
};
|
|
2010
3099
|
var NUMERIC_TYPES = /* @__PURE__ */ new Set(["integer", "bigint", "numeric", "double"]);
|
|
2011
|
-
function
|
|
3100
|
+
function fail2(message, code, candidates = []) {
|
|
2012
3101
|
throw new DocumentCompileError(message, code, candidates);
|
|
2013
3102
|
}
|
|
2014
3103
|
function argsByName(field) {
|
|
2015
3104
|
return new Map((field.arguments ?? []).map((arg) => [arg.name.value, arg]));
|
|
2016
3105
|
}
|
|
3106
|
+
function* aggregateBlockFields(selectionSet) {
|
|
3107
|
+
for (const node of selectionSet.selections) {
|
|
3108
|
+
if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
3109
|
+
if (node.name.value === "__typename") continue;
|
|
3110
|
+
if (node.alias) fail2(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
|
|
3111
|
+
yield node;
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
2017
3114
|
function assertKnownArgs(field, allowed) {
|
|
2018
3115
|
for (const arg of field.arguments ?? []) {
|
|
2019
3116
|
if (!allowed.includes(arg.name.value)) {
|
|
2020
|
-
|
|
3117
|
+
fail2(`argument '${arg.name.value}' on '${field.name.value}' is not supported in v2.0`, "unsupported-argument", allowed);
|
|
2021
3118
|
}
|
|
2022
3119
|
}
|
|
2023
3120
|
}
|
|
@@ -2030,8 +3127,11 @@ var OperationLowerer = class {
|
|
|
2030
3127
|
meta(table) {
|
|
2031
3128
|
return this.ctx.tables[table];
|
|
2032
3129
|
}
|
|
3130
|
+
/** A data column — cell-metadata columns are reachable only through `cells { … }`. */
|
|
2033
3131
|
column(table, name) {
|
|
2034
|
-
|
|
3132
|
+
const meta = this.meta(table);
|
|
3133
|
+
const column = meta.columns.find((c) => c.name === name);
|
|
3134
|
+
return column && !isCellColumn(meta, name) ? column : void 0;
|
|
2035
3135
|
}
|
|
2036
3136
|
link(table, name) {
|
|
2037
3137
|
return this.meta(table).links.find((l) => l.name === name);
|
|
@@ -2039,7 +3139,8 @@ var OperationLowerer = class {
|
|
|
2039
3139
|
fieldCandidates(table) {
|
|
2040
3140
|
const meta = this.meta(table);
|
|
2041
3141
|
return [
|
|
2042
|
-
...meta.columns.map((c) => c.name),
|
|
3142
|
+
...meta.columns.filter((c) => !isCellColumn(meta, c.name)).map((c) => c.name),
|
|
3143
|
+
...meta.cells ? [CELLS_FIELD] : [],
|
|
2043
3144
|
...meta.links.map((l) => l.name),
|
|
2044
3145
|
...meta.links.filter((l) => l.kind === "toMany").map((l) => `${l.name}Aggregate`)
|
|
2045
3146
|
];
|
|
@@ -2048,7 +3149,7 @@ var OperationLowerer = class {
|
|
|
2048
3149
|
slot(name, input, table) {
|
|
2049
3150
|
const variable = this.variables.get(name);
|
|
2050
3151
|
if (!variable || variable.type.kind !== "inputRef" || variable.type.input !== input || variable.type.table !== table) {
|
|
2051
|
-
|
|
3152
|
+
fail2(`variable $${name} must be declared as the ${input} input type for table '${table}'`, "slot-mismatch");
|
|
2052
3153
|
}
|
|
2053
3154
|
return name;
|
|
2054
3155
|
}
|
|
@@ -2073,14 +3174,14 @@ var OperationLowerer = class {
|
|
|
2073
3174
|
const items = node.values.map((item) => {
|
|
2074
3175
|
const lowered = this.lowerValue(item);
|
|
2075
3176
|
if (lowered.kind !== "literal" || Array.isArray(lowered.value)) {
|
|
2076
|
-
|
|
3177
|
+
fail2("list literals must contain scalar literals only \u2014 pass the whole list as a variable instead", "unsupported-literal");
|
|
2077
3178
|
}
|
|
2078
3179
|
return lowered.value;
|
|
2079
3180
|
});
|
|
2080
3181
|
return { kind: "literal", value: items };
|
|
2081
3182
|
}
|
|
2082
3183
|
case graphql.Kind.OBJECT:
|
|
2083
|
-
|
|
3184
|
+
fail2("literal JSON objects are not supported in documents \u2014 pass the value via a variable", "unsupported-literal");
|
|
2084
3185
|
}
|
|
2085
3186
|
}
|
|
2086
3187
|
// -------------------------------------------------------------------------
|
|
@@ -2090,7 +3191,7 @@ var OperationLowerer = class {
|
|
|
2090
3191
|
if (node.kind === graphql.Kind.VARIABLE) {
|
|
2091
3192
|
return { kind: "dynamicFilter", slot: this.slot(node.name.value, "boolExp", table) };
|
|
2092
3193
|
}
|
|
2093
|
-
if (node.kind !== graphql.Kind.OBJECT)
|
|
3194
|
+
if (node.kind !== graphql.Kind.OBJECT) fail2(`a boolean expression on '${table}' must be an object or a variable`, "invalid-boolexp");
|
|
2094
3195
|
const operands = [];
|
|
2095
3196
|
for (const field of node.fields) {
|
|
2096
3197
|
const key = field.name.value;
|
|
@@ -2121,45 +3222,45 @@ var OperationLowerer = class {
|
|
|
2121
3222
|
continue;
|
|
2122
3223
|
}
|
|
2123
3224
|
}
|
|
2124
|
-
|
|
3225
|
+
fail2(`unknown field '${key}' in a boolean expression on '${table}'`, "unknown-column", this.fieldCandidates(table));
|
|
2125
3226
|
}
|
|
2126
3227
|
if (operands.length === 0) return CONST_TRUE;
|
|
2127
3228
|
return operands.length === 1 ? operands[0] : { kind: "and", operands };
|
|
2128
3229
|
}
|
|
2129
3230
|
lowerComparison(node, column, table) {
|
|
2130
3231
|
if (node.kind !== graphql.Kind.OBJECT) {
|
|
2131
|
-
|
|
3232
|
+
fail2(`comparison on '${table}.${column}' must be an object like { _eq: \u2026 }`, "invalid-comparison", Object.keys(BOOLEXP_OPS));
|
|
2132
3233
|
}
|
|
2133
3234
|
return node.fields.map((field) => {
|
|
2134
3235
|
const op = BOOLEXP_OPS[field.name.value];
|
|
2135
3236
|
if (!op) {
|
|
2136
|
-
|
|
3237
|
+
fail2(`unknown comparison operator '${field.name.value}' on '${table}.${column}'`, "unknown-operator", Object.keys(BOOLEXP_OPS));
|
|
2137
3238
|
}
|
|
2138
3239
|
return { kind: "cmp", op, column, value: this.lowerValue(field.value) };
|
|
2139
3240
|
});
|
|
2140
3241
|
}
|
|
2141
3242
|
/** Hasura aggregation predicate: `{ count: { predicate: { _gt: 2 }, filter: {…} } }`. */
|
|
2142
3243
|
lowerAggPredicate(node, link, target) {
|
|
2143
|
-
if (node.kind !== graphql.Kind.OBJECT)
|
|
3244
|
+
if (node.kind !== graphql.Kind.OBJECT) fail2(`aggregate predicate through '${link}' must be an object`, "invalid-agg-predicate");
|
|
2144
3245
|
const countField = node.fields.find((f) => f.name.value === "count");
|
|
2145
3246
|
if (!countField || node.fields.length !== 1) {
|
|
2146
|
-
|
|
3247
|
+
fail2(`aggregate predicates support { count: \u2026 } only in v2.0 (through '${link}')`, "invalid-agg-predicate", ["count"]);
|
|
2147
3248
|
}
|
|
2148
|
-
if (countField.value.kind !== graphql.Kind.OBJECT)
|
|
3249
|
+
if (countField.value.kind !== graphql.Kind.OBJECT) fail2(`count predicate through '${link}' must be an object`, "invalid-agg-predicate");
|
|
2149
3250
|
let predicate;
|
|
2150
3251
|
let where;
|
|
2151
3252
|
for (const field of countField.value.fields) {
|
|
2152
3253
|
if (field.name.value === "predicate") {
|
|
2153
3254
|
if (field.value.kind !== graphql.Kind.OBJECT || field.value.fields.length !== 1) {
|
|
2154
|
-
|
|
3255
|
+
fail2(`count predicate through '${link}' needs exactly one operator`, "invalid-agg-predicate", [...AGG_PREDICATE_OPS]);
|
|
2155
3256
|
}
|
|
2156
3257
|
const opField = field.value.fields[0];
|
|
2157
3258
|
const opName = opField.name.value;
|
|
2158
3259
|
if (!AGG_PREDICATE_OPS.includes(opName)) {
|
|
2159
|
-
|
|
3260
|
+
fail2(`aggregate count predicates support ${AGG_PREDICATE_OPS.join("/")} only, got '${opName}'`, "invalid-agg-predicate");
|
|
2160
3261
|
}
|
|
2161
3262
|
if (opField.value.kind !== graphql.Kind.INT) {
|
|
2162
|
-
|
|
3263
|
+
fail2(`aggregate count predicate value must be an integer literal`, "invalid-agg-predicate");
|
|
2163
3264
|
}
|
|
2164
3265
|
predicate = { op: opName.slice(1), value: Number(opField.value.value) };
|
|
2165
3266
|
continue;
|
|
@@ -2168,9 +3269,9 @@ var OperationLowerer = class {
|
|
|
2168
3269
|
where = this.lowerBoolExp(field.value, target);
|
|
2169
3270
|
continue;
|
|
2170
3271
|
}
|
|
2171
|
-
|
|
3272
|
+
fail2(`unknown key '${field.name.value}' in a count predicate`, "invalid-agg-predicate", ["predicate", "filter"]);
|
|
2172
3273
|
}
|
|
2173
|
-
if (!predicate)
|
|
3274
|
+
if (!predicate) fail2(`count predicate through '${link}' needs a { predicate: { _gt: n } }`, "invalid-agg-predicate");
|
|
2174
3275
|
return { kind: "aggPredicate", link, fn: "count", op: predicate.op, value: predicate.value, ...where ? { where } : {} };
|
|
2175
3276
|
}
|
|
2176
3277
|
lowerOrderBy(node, table) {
|
|
@@ -2180,7 +3281,7 @@ var OperationLowerer = class {
|
|
|
2180
3281
|
const items = node.kind === graphql.Kind.LIST ? node.values : [node];
|
|
2181
3282
|
const lowered = [];
|
|
2182
3283
|
for (const item of items) {
|
|
2183
|
-
if (item.kind !== graphql.Kind.OBJECT)
|
|
3284
|
+
if (item.kind !== graphql.Kind.OBJECT) fail2(`orderBy entries on '${table}' must be objects`, "invalid-orderby");
|
|
2184
3285
|
for (const field of item.fields) {
|
|
2185
3286
|
const key = field.name.value;
|
|
2186
3287
|
const column = this.column(table, key);
|
|
@@ -2191,25 +3292,25 @@ var OperationLowerer = class {
|
|
|
2191
3292
|
const link = this.link(table, key);
|
|
2192
3293
|
if (link && link.kind !== "toMany") {
|
|
2193
3294
|
if (field.value.kind !== graphql.Kind.OBJECT || field.value.fields.length !== 1) {
|
|
2194
|
-
|
|
3295
|
+
fail2(`orderBy through '${key}' needs exactly one target column`, "invalid-orderby");
|
|
2195
3296
|
}
|
|
2196
3297
|
const inner = field.value.fields[0];
|
|
2197
3298
|
const targetColumn = this.column(link.target, inner.name.value);
|
|
2198
3299
|
if (!targetColumn) {
|
|
2199
|
-
|
|
3300
|
+
fail2(`unknown column '${inner.name.value}' on '${link.target}' in orderBy`, "unknown-column", this.meta(link.target).columns.map((c) => c.name));
|
|
2200
3301
|
}
|
|
2201
3302
|
lowered.push({ kind: "column", column: inner.name.value, link: key, ...this.lowerDirection(inner.value, link.target, inner.name.value) });
|
|
2202
3303
|
continue;
|
|
2203
3304
|
}
|
|
2204
|
-
|
|
3305
|
+
fail2(`unknown orderBy key '${key}' on '${table}' (to-one links only)`, "invalid-orderby", this.fieldCandidates(table));
|
|
2205
3306
|
}
|
|
2206
3307
|
}
|
|
2207
3308
|
return lowered;
|
|
2208
3309
|
}
|
|
2209
3310
|
lowerDirection(node, table, column) {
|
|
2210
|
-
if (node.kind !== graphql.Kind.ENUM)
|
|
3311
|
+
if (node.kind !== graphql.Kind.ENUM) fail2(`orderBy direction on '${table}.${column}' must be a direction enum`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
|
|
2211
3312
|
const direction = ORDER_DIRECTIONS[node.value];
|
|
2212
|
-
if (!direction)
|
|
3313
|
+
if (!direction) fail2(`unknown orderBy direction '${node.value}'`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
|
|
2213
3314
|
return direction.nulls ? { direction: direction.direction, nulls: direction.nulls } : { direction: direction.direction };
|
|
2214
3315
|
}
|
|
2215
3316
|
lowerDistinctOn(node, table) {
|
|
@@ -2218,9 +3319,9 @@ var OperationLowerer = class {
|
|
|
2218
3319
|
}
|
|
2219
3320
|
const items = node.kind === graphql.Kind.LIST ? node.values : [node];
|
|
2220
3321
|
return items.map((item) => {
|
|
2221
|
-
if (item.kind !== graphql.Kind.ENUM)
|
|
3322
|
+
if (item.kind !== graphql.Kind.ENUM) fail2(`distinctOn entries on '${table}' must be select-column enum values`, "invalid-distinct-on");
|
|
2222
3323
|
if (!this.column(table, item.value)) {
|
|
2223
|
-
|
|
3324
|
+
fail2(`unknown distinctOn column '${item.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
|
|
2224
3325
|
}
|
|
2225
3326
|
return item.value;
|
|
2226
3327
|
});
|
|
@@ -2231,16 +3332,16 @@ var OperationLowerer = class {
|
|
|
2231
3332
|
if (node.kind === graphql.Kind.VARIABLE) return { ...base, variable: node.name.value };
|
|
2232
3333
|
if (node.kind === graphql.Kind.INT) {
|
|
2233
3334
|
const value = Number(node.value);
|
|
2234
|
-
if (value < 1)
|
|
3335
|
+
if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
|
|
2235
3336
|
return { ...base, value: Math.min(value, MAX_LIMIT) };
|
|
2236
3337
|
}
|
|
2237
|
-
|
|
3338
|
+
fail2("limit must be an integer literal or a variable", "invalid-limit");
|
|
2238
3339
|
}
|
|
2239
3340
|
lowerOffset(node) {
|
|
2240
3341
|
if (!node) return void 0;
|
|
2241
3342
|
if (node.kind === graphql.Kind.VARIABLE) return { variable: node.name.value };
|
|
2242
3343
|
if (node.kind === graphql.Kind.INT) return { value: Number(node.value) };
|
|
2243
|
-
|
|
3344
|
+
fail2("offset must be an integer literal or a variable", "invalid-offset");
|
|
2244
3345
|
}
|
|
2245
3346
|
// -------------------------------------------------------------------------
|
|
2246
3347
|
// Selections
|
|
@@ -2249,9 +3350,9 @@ var OperationLowerer = class {
|
|
|
2249
3350
|
const meta = this.meta(table);
|
|
2250
3351
|
const selection = { typename: meta.typename, columns: [], relations: [], aggregates: [] };
|
|
2251
3352
|
for (const node of selectionSet.selections) {
|
|
2252
|
-
if (node.kind !== graphql.Kind.FIELD)
|
|
3353
|
+
if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
2253
3354
|
if (node.directives && node.directives.length > 0) {
|
|
2254
|
-
|
|
3355
|
+
fail2(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
|
|
2255
3356
|
}
|
|
2256
3357
|
const name = node.name.value;
|
|
2257
3358
|
if (name === "__typename") continue;
|
|
@@ -2259,14 +3360,19 @@ var OperationLowerer = class {
|
|
|
2259
3360
|
const column = this.column(table, name);
|
|
2260
3361
|
if (column) {
|
|
2261
3362
|
if (node.arguments !== void 0 && node.arguments.length > 0 || node.selectionSet !== void 0) {
|
|
2262
|
-
|
|
3363
|
+
fail2(`column '${table}.${name}' takes no arguments or sub-selection`, "invalid-selection");
|
|
2263
3364
|
}
|
|
2264
3365
|
selection.columns.push({ alias, column: name });
|
|
2265
3366
|
continue;
|
|
2266
3367
|
}
|
|
3368
|
+
if (name === CELLS_FIELD && meta.cells) {
|
|
3369
|
+
if (selection.cells) fail2(`'${CELLS_FIELD}' may be selected once per object`, "invalid-selection");
|
|
3370
|
+
selection.cells = this.lowerCells(node, table, alias);
|
|
3371
|
+
continue;
|
|
3372
|
+
}
|
|
2267
3373
|
const link = this.link(table, name);
|
|
2268
3374
|
if (link) {
|
|
2269
|
-
if (!node.selectionSet)
|
|
3375
|
+
if (!node.selectionSet) fail2(`link '${table}.${name}' needs a selection set`, "invalid-selection");
|
|
2270
3376
|
const relation = {
|
|
2271
3377
|
alias,
|
|
2272
3378
|
link: name,
|
|
@@ -2285,7 +3391,7 @@ var OperationLowerer = class {
|
|
|
2285
3391
|
if (offset) relation.offset = offset;
|
|
2286
3392
|
}
|
|
2287
3393
|
} else if (node.arguments && node.arguments.length > 0) {
|
|
2288
|
-
|
|
3394
|
+
fail2(`to-one link '${table}.${name}' takes no arguments`, "invalid-selection");
|
|
2289
3395
|
}
|
|
2290
3396
|
selection.relations.push(relation);
|
|
2291
3397
|
continue;
|
|
@@ -2293,7 +3399,7 @@ var OperationLowerer = class {
|
|
|
2293
3399
|
if (name.endsWith("Aggregate")) {
|
|
2294
3400
|
const aggLink = this.link(table, name.slice(0, -"Aggregate".length));
|
|
2295
3401
|
if (aggLink && aggLink.kind === "toMany") {
|
|
2296
|
-
if (!node.selectionSet)
|
|
3402
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
2297
3403
|
assertKnownArgs(node, ["where"]);
|
|
2298
3404
|
const whereArg = argsByName(node).get("where");
|
|
2299
3405
|
const aggregate = this.lowerAggregateSelection(node.selectionSet, aggLink.target);
|
|
@@ -2306,70 +3412,182 @@ var OperationLowerer = class {
|
|
|
2306
3412
|
continue;
|
|
2307
3413
|
}
|
|
2308
3414
|
}
|
|
2309
|
-
|
|
3415
|
+
fail2(`unknown field '${name}' on '${table}'`, "unknown-field", this.fieldCandidates(table));
|
|
2310
3416
|
}
|
|
2311
3417
|
return selection;
|
|
2312
3418
|
}
|
|
3419
|
+
/**
|
|
3420
|
+
* `cells { <column> { at by … } }` — typed per-cell metadata. Plain tables
|
|
3421
|
+
* offer `at`/`by`; resolved tables add the winning `source` and newer
|
|
3422
|
+
* `conflicts { source at value }`.
|
|
3423
|
+
*/
|
|
3424
|
+
lowerCells(node, table, alias) {
|
|
3425
|
+
const meta = this.meta(table);
|
|
3426
|
+
const cells = meta.cells ?? {};
|
|
3427
|
+
if (node.arguments && node.arguments.length > 0) fail2(`'${CELLS_FIELD}' takes no arguments`, "invalid-selection");
|
|
3428
|
+
if (!node.selectionSet) fail2(`'${CELLS_FIELD}' needs a selection set`, "invalid-selection");
|
|
3429
|
+
const fields = [];
|
|
3430
|
+
for (const cellNode of fieldNodes(node.selectionSet)) {
|
|
3431
|
+
const column = cellNode.name.value;
|
|
3432
|
+
const parts = cells[column];
|
|
3433
|
+
if (!parts || parts.at === void 0 && parts.source === void 0) {
|
|
3434
|
+
fail2(`unknown cell '${column}' on '${table}'`, "unknown-field", Object.keys(cells));
|
|
3435
|
+
}
|
|
3436
|
+
if (!cellNode.selectionSet) fail2(`cell '${column}' needs a selection set`, "invalid-selection");
|
|
3437
|
+
const resolved = parts.source !== void 0;
|
|
3438
|
+
const allowed = resolved ? ["source", "at", "by", "conflicts"] : ["at", "by"];
|
|
3439
|
+
const selected = [];
|
|
3440
|
+
for (const partNode of fieldNodes(cellNode.selectionSet)) {
|
|
3441
|
+
const part = partNode.name.value;
|
|
3442
|
+
if (!allowed.includes(part)) fail2(`unknown field '${part}' on cell '${column}'`, "unknown-field", allowed);
|
|
3443
|
+
const partAlias = partNode.alias?.value ?? part;
|
|
3444
|
+
if (part !== "conflicts") {
|
|
3445
|
+
if (partNode.selectionSet) fail2(`cell field '${part}' takes no sub-selection`, "invalid-selection");
|
|
3446
|
+
selected.push({ alias: partAlias, part });
|
|
3447
|
+
continue;
|
|
3448
|
+
}
|
|
3449
|
+
if (!partNode.selectionSet) fail2(`'conflicts' needs a selection set`, "invalid-selection");
|
|
3450
|
+
const conflictFields = fieldNodes(partNode.selectionSet).map((fieldNode) => {
|
|
3451
|
+
const field = fieldNode.name.value;
|
|
3452
|
+
if (field !== "source" && field !== "at" && field !== "value") fail2(`unknown field '${field}' on a cell conflict`, "unknown-field", ["source", "at", "value"]);
|
|
3453
|
+
return { alias: fieldNode.alias?.value ?? field, field };
|
|
3454
|
+
});
|
|
3455
|
+
selected.push({ alias: partAlias, part: "conflicts", fields: conflictFields, typename: CELL_TYPENAMES.conflict });
|
|
3456
|
+
}
|
|
3457
|
+
fields.push({
|
|
3458
|
+
alias: cellNode.alias?.value ?? column,
|
|
3459
|
+
column,
|
|
3460
|
+
typename: resolved ? CELL_TYPENAMES.resolved : CELL_TYPENAMES.stamp,
|
|
3461
|
+
parts: selected
|
|
3462
|
+
});
|
|
3463
|
+
}
|
|
3464
|
+
return { alias, typename: cellsTypename(meta.typename), fields };
|
|
3465
|
+
}
|
|
2313
3466
|
/** The `{ aggregate { count sum {…} avg {…} } nodes {…} }` block shared by roots and relations. */
|
|
2314
3467
|
lowerAggregateSelection(selectionSet, table) {
|
|
2315
3468
|
const result = {};
|
|
2316
3469
|
let sawAggregate = false;
|
|
2317
|
-
for (const node of selectionSet
|
|
2318
|
-
if (node.kind !== graphql.Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
|
|
3470
|
+
for (const node of aggregateBlockFields(selectionSet)) {
|
|
2319
3471
|
const name = node.name.value;
|
|
2320
|
-
if (name === "__typename") continue;
|
|
2321
|
-
if (node.alias) fail(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
|
|
2322
3472
|
if (name === "nodes") {
|
|
2323
|
-
if (!node.selectionSet)
|
|
3473
|
+
if (!node.selectionSet) fail2("'nodes' needs a selection set", "invalid-aggregate");
|
|
2324
3474
|
result.nodes = this.lowerSelection(node.selectionSet, table);
|
|
2325
3475
|
continue;
|
|
2326
3476
|
}
|
|
2327
|
-
if (name !== "aggregate")
|
|
2328
|
-
if (sawAggregate)
|
|
3477
|
+
if (name !== "aggregate") fail2(`unknown aggregate field '${name}'`, "invalid-aggregate", ["aggregate", "nodes"]);
|
|
3478
|
+
if (sawAggregate) fail2("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
|
|
2329
3479
|
sawAggregate = true;
|
|
2330
|
-
if (!node.selectionSet)
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
3480
|
+
if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
|
|
3481
|
+
Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
|
|
3482
|
+
}
|
|
3483
|
+
if (!sawAggregate && !result.nodes) fail2("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
|
|
3484
|
+
return result;
|
|
3485
|
+
}
|
|
3486
|
+
/**
|
|
3487
|
+
* The `{ key {…} aggregate {…} }` block of a grouped aggregate root. Same
|
|
3488
|
+
* `aggregate` sub-block as the ungrouped shape (shared via
|
|
3489
|
+
* `lowerAggregateFields`), but `key` replaces `nodes` — see the IR comment on
|
|
3490
|
+
* `aggregateGroupsRootSchema` for why grouped roots have no `nodes`.
|
|
3491
|
+
*/
|
|
3492
|
+
lowerGroupedAggregateSelection(selectionSet, table, groupBy) {
|
|
3493
|
+
const groupColumns = new Set(groupBy.map((entry) => entry.column));
|
|
3494
|
+
const result = { key: [] };
|
|
3495
|
+
let sawAggregate = false;
|
|
3496
|
+
let sawKey = false;
|
|
3497
|
+
for (const node of aggregateBlockFields(selectionSet)) {
|
|
3498
|
+
const name = node.name.value;
|
|
3499
|
+
if (name === "key") {
|
|
3500
|
+
if (sawKey) fail2("one key block per grouped aggregate", "invalid-aggregate");
|
|
3501
|
+
sawKey = true;
|
|
3502
|
+
if (!node.selectionSet) fail2("'key' needs a selection set", "invalid-aggregate");
|
|
3503
|
+
result.key = this.lowerGroupKeySelection(node.selectionSet, table, groupColumns);
|
|
3504
|
+
continue;
|
|
3505
|
+
}
|
|
3506
|
+
if (name === "nodes") {
|
|
3507
|
+
fail2("'nodes' is not available on a grouped aggregate \u2014 query the rows separately", "invalid-aggregate", ["key", "aggregate"]);
|
|
3508
|
+
}
|
|
3509
|
+
if (name !== "aggregate") fail2(`unknown grouped-aggregate field '${name}'`, "invalid-aggregate", ["key", "aggregate"]);
|
|
3510
|
+
if (sawAggregate) fail2("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
|
|
3511
|
+
sawAggregate = true;
|
|
3512
|
+
if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
|
|
3513
|
+
Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
|
|
3514
|
+
}
|
|
3515
|
+
const rendersAggregate = result.count !== void 0 || (result.sum?.length ?? 0) > 0 || (result.avg?.length ?? 0) > 0;
|
|
3516
|
+
if (result.key.length === 0 && !rendersAggregate) {
|
|
3517
|
+
fail2(
|
|
3518
|
+
"grouped aggregate needs at least one key column or an aggregate \u2014 a block selecting only __typename produces nothing",
|
|
3519
|
+
"invalid-aggregate",
|
|
3520
|
+
["key", "aggregate"]
|
|
3521
|
+
);
|
|
3522
|
+
}
|
|
3523
|
+
return result;
|
|
3524
|
+
}
|
|
3525
|
+
/**
|
|
3526
|
+
* `key { month officer }` → the projected group columns. Every field must be
|
|
3527
|
+
* one of the query's own group keys: any other column is indeterminate
|
|
3528
|
+
* within a group, so selecting it is a document error rather than an
|
|
3529
|
+
* arbitrary pick.
|
|
3530
|
+
*/
|
|
3531
|
+
lowerGroupKeySelection(selectionSet, table, groupColumns) {
|
|
3532
|
+
const key = [];
|
|
3533
|
+
for (const node of selectionSet.selections) {
|
|
3534
|
+
if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
3535
|
+
const name = node.name.value;
|
|
3536
|
+
if (name === "__typename") continue;
|
|
3537
|
+
if (!this.column(table, name)) {
|
|
3538
|
+
fail2(`unknown key column '${name}' on '${table}'`, "unknown-column", [...groupColumns]);
|
|
3539
|
+
}
|
|
3540
|
+
if (!groupColumns.has(name)) {
|
|
3541
|
+
fail2(
|
|
3542
|
+
`key column '${name}' is not one of this query's groupBy keys \u2014 it has no single value within a group`,
|
|
3543
|
+
"invalid-group-key",
|
|
3544
|
+
[...groupColumns]
|
|
3545
|
+
);
|
|
3546
|
+
}
|
|
3547
|
+
key.push({ alias: node.alias?.value ?? name, column: name });
|
|
3548
|
+
}
|
|
3549
|
+
return key;
|
|
3550
|
+
}
|
|
3551
|
+
/** The `count` / `sum {…}` / `avg {…}` fields inside an `aggregate { … }` block. */
|
|
3552
|
+
lowerAggregateFields(selectionSet, table) {
|
|
3553
|
+
const result = {};
|
|
3554
|
+
for (const aggField of aggregateBlockFields(selectionSet)) {
|
|
3555
|
+
const aggName = aggField.name.value;
|
|
3556
|
+
if (aggName === "count") {
|
|
3557
|
+
assertKnownArgs(aggField, ["columns", "distinct"]);
|
|
3558
|
+
const args = argsByName(aggField);
|
|
3559
|
+
const count = {};
|
|
3560
|
+
const columns = args.get("columns");
|
|
3561
|
+
if (columns) count.columns = this.lowerDistinctOn(columns.value, table);
|
|
3562
|
+
const distinct = args.get("distinct");
|
|
3563
|
+
if (distinct) {
|
|
3564
|
+
if (distinct.value.kind !== graphql.Kind.BOOLEAN) fail2("count(distinct:) must be a boolean literal", "invalid-aggregate");
|
|
3565
|
+
count.distinct = distinct.value.value;
|
|
2349
3566
|
}
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
3567
|
+
result.count = count;
|
|
3568
|
+
continue;
|
|
3569
|
+
}
|
|
3570
|
+
if (aggName === "sum" || aggName === "avg") {
|
|
3571
|
+
if (!aggField.selectionSet) fail2(`'${aggName}' needs a column selection`, "invalid-aggregate");
|
|
3572
|
+
const columns = [];
|
|
3573
|
+
for (const columnField of aggField.selectionSet.selections) {
|
|
3574
|
+
if (columnField.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
3575
|
+
if (columnField.name.value === "__typename") continue;
|
|
3576
|
+
const column = this.column(table, columnField.name.value);
|
|
3577
|
+
if (!column || typeof column.type !== "string" || !NUMERIC_TYPES.has(column.type)) {
|
|
3578
|
+
fail2(
|
|
3579
|
+
`'${aggName}' supports numeric columns only, got '${columnField.name.value}' on '${table}'`,
|
|
3580
|
+
"invalid-aggregate",
|
|
3581
|
+
this.meta(table).columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type)).map((c) => c.name)
|
|
3582
|
+
);
|
|
2365
3583
|
}
|
|
2366
|
-
|
|
2367
|
-
continue;
|
|
3584
|
+
columns.push(columnField.name.value);
|
|
2368
3585
|
}
|
|
2369
|
-
|
|
3586
|
+
result[aggName] = columns;
|
|
3587
|
+
continue;
|
|
2370
3588
|
}
|
|
3589
|
+
fail2(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
|
|
2371
3590
|
}
|
|
2372
|
-
if (!sawAggregate && !result.nodes) fail("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
|
|
2373
3591
|
return result;
|
|
2374
3592
|
}
|
|
2375
3593
|
// -------------------------------------------------------------------------
|
|
@@ -2378,32 +3596,65 @@ var OperationLowerer = class {
|
|
|
2378
3596
|
lowerRoots(operation, kind) {
|
|
2379
3597
|
const roots = [];
|
|
2380
3598
|
for (const node of operation.selectionSet.selections) {
|
|
2381
|
-
if (node.kind !== graphql.Kind.FIELD)
|
|
3599
|
+
if (node.kind !== graphql.Kind.FIELD) fail2("fragments are not supported at the operation root", "invalid-root");
|
|
3600
|
+
if (node.selectionSet) graphql.visit(node.selectionSet, {
|
|
3601
|
+
Directive(directive) {
|
|
3602
|
+
if (directive.name.value === "assertAffectedRows") {
|
|
3603
|
+
fail2("@assertAffectedRows is supported on mutation roots only", "invalid-affected-row-assertion");
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
});
|
|
3607
|
+
const assertion = this.parseAffectedRowAssertion(node, kind);
|
|
2382
3608
|
const gate = this.parseRootDirectives(node);
|
|
2383
3609
|
if (gate.omit) continue;
|
|
2384
|
-
const
|
|
2385
|
-
|
|
3610
|
+
const lowered = kind === "query" ? this.lowerQueryRoot(node) : this.lowerMutationRoot(node);
|
|
3611
|
+
const meta = this.meta(lowered.table);
|
|
3612
|
+
const root = kind === "query" ? applyPrefetchDirectives({ node, root: lowered, meta }) : applyWritebackDirectives({ node, root: lowered, meta });
|
|
3613
|
+
const guarded = root.kind === "mutation" && assertion !== void 0 ? { ...root, assertAffectedRows: assertion } : root;
|
|
3614
|
+
roots.push(gate.includeIf ? { ...guarded, includeIf: gate.includeIf } : guarded);
|
|
2386
3615
|
}
|
|
2387
|
-
if (roots.length === 0)
|
|
3616
|
+
if (roots.length === 0) fail2("operation has no roots (all statically skipped?)", "invalid-root");
|
|
2388
3617
|
return roots;
|
|
2389
3618
|
}
|
|
3619
|
+
parseAffectedRowAssertion(node, kind) {
|
|
3620
|
+
const assertions = node.directives?.filter((directive) => directive.name.value === "assertAffectedRows") ?? [];
|
|
3621
|
+
if (!assertions.length) return void 0;
|
|
3622
|
+
if (kind !== "mutation" || assertions.length !== 1) {
|
|
3623
|
+
fail2("@assertAffectedRows is supported once per mutation root", "invalid-affected-row-assertion");
|
|
3624
|
+
}
|
|
3625
|
+
if (node.directives?.some((directive) => ["include", "skip"].includes(directive.name.value))) {
|
|
3626
|
+
fail2("an @assertAffectedRows root cannot use @include or @skip", "invalid-affected-row-assertion");
|
|
3627
|
+
}
|
|
3628
|
+
const args = assertions[0].arguments;
|
|
3629
|
+
const arg = args?.[0];
|
|
3630
|
+
if (args?.length !== 1 || arg?.name.value !== "exactly" || arg.value.kind !== graphql.Kind.INT) {
|
|
3631
|
+
fail2("@assertAffectedRows(exactly:) requires a document-authored nonnegative Int literal", "invalid-affected-row-assertion");
|
|
3632
|
+
}
|
|
3633
|
+
const count = Number(arg.value.value);
|
|
3634
|
+
if (!Number.isInteger(count) || count < 0 || count > 2147483647) {
|
|
3635
|
+
fail2("@assertAffectedRows(exactly:) must be between 0 and 2147483647", "invalid-affected-row-assertion");
|
|
3636
|
+
}
|
|
3637
|
+
return count;
|
|
3638
|
+
}
|
|
2390
3639
|
parseRootDirectives(node) {
|
|
2391
3640
|
let includeIf;
|
|
2392
3641
|
let omit = false;
|
|
2393
3642
|
for (const directive of node.directives ?? []) {
|
|
2394
3643
|
const name = directive.name.value;
|
|
3644
|
+
if (name === "assertAffectedRows") continue;
|
|
3645
|
+
if (SOURCE_DIRECTIVES.has(name)) continue;
|
|
2395
3646
|
if (name !== "include" && name !== "skip") {
|
|
2396
|
-
|
|
3647
|
+
fail2(`directive @${name} is not supported on fields`, "unknown-directive", ["include", "skip"]);
|
|
2397
3648
|
}
|
|
2398
3649
|
const ifArg = directive.arguments?.find((a) => a.name.value === "if");
|
|
2399
|
-
if (!ifArg)
|
|
3650
|
+
if (!ifArg) fail2(`@${name} needs an if: argument`, "invalid-directive");
|
|
2400
3651
|
if (ifArg.value.kind === graphql.Kind.BOOLEAN) {
|
|
2401
3652
|
const keep = name === "include" ? ifArg.value.value : !ifArg.value.value;
|
|
2402
3653
|
if (!keep) omit = true;
|
|
2403
3654
|
continue;
|
|
2404
3655
|
}
|
|
2405
|
-
if (ifArg.value.kind !== graphql.Kind.VARIABLE)
|
|
2406
|
-
if (includeIf)
|
|
3656
|
+
if (ifArg.value.kind !== graphql.Kind.VARIABLE) fail2(`@${name}(if:) must be a boolean literal or variable`, "invalid-directive");
|
|
3657
|
+
if (includeIf) fail2("at most one of @include/@skip per root field", "invalid-directive");
|
|
2407
3658
|
includeIf = { variable: ifArg.value.name.value, negated: name === "skip" };
|
|
2408
3659
|
}
|
|
2409
3660
|
return omit ? { omit: true } : { includeIf, omit: false };
|
|
@@ -2412,12 +3663,12 @@ var OperationLowerer = class {
|
|
|
2412
3663
|
const name = node.name.value;
|
|
2413
3664
|
const alias = node.alias?.value ?? name;
|
|
2414
3665
|
const rootInfo = this.ctx.queryRoots.get(name);
|
|
2415
|
-
if (!rootInfo)
|
|
3666
|
+
if (!rootInfo) fail2(`unknown query root '${name}'`, "unknown-root", [...this.ctx.queryRoots.keys()]);
|
|
2416
3667
|
const { table } = rootInfo;
|
|
2417
3668
|
const args = argsByName(node);
|
|
2418
3669
|
if (rootInfo.kind === "select") {
|
|
2419
3670
|
assertKnownArgs(node, ["where", "orderBy", "distinctOn", "limit", "offset"]);
|
|
2420
|
-
if (!node.selectionSet)
|
|
3671
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
2421
3672
|
const where = args.get("where");
|
|
2422
3673
|
const orderBy = args.get("orderBy");
|
|
2423
3674
|
const distinctOn = args.get("distinctOn");
|
|
@@ -2436,12 +3687,15 @@ var OperationLowerer = class {
|
|
|
2436
3687
|
};
|
|
2437
3688
|
}
|
|
2438
3689
|
if (rootInfo.kind === "byPk") {
|
|
2439
|
-
if (!node.selectionSet)
|
|
3690
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
2440
3691
|
const pk = this.lowerPkArgs(node, table);
|
|
2441
3692
|
return { kind: "byPk", alias, table, selection: this.lowerSelection(node.selectionSet, table), pk, where: CONST_TRUE };
|
|
2442
3693
|
}
|
|
3694
|
+
if (rootInfo.kind === "aggregateGroups") {
|
|
3695
|
+
return this.lowerAggregateGroupsRoot(node, { alias, table });
|
|
3696
|
+
}
|
|
2443
3697
|
assertKnownArgs(node, ["where"]);
|
|
2444
|
-
if (!node.selectionSet)
|
|
3698
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
2445
3699
|
const whereArg = args.get("where");
|
|
2446
3700
|
const aggregate = this.lowerAggregateSelection(node.selectionSet, table);
|
|
2447
3701
|
return {
|
|
@@ -2452,6 +3706,108 @@ var OperationLowerer = class {
|
|
|
2452
3706
|
...aggregate
|
|
2453
3707
|
};
|
|
2454
3708
|
}
|
|
3709
|
+
/** `<t>AggregateGroups(where:, groupBy:, limit:, offset:)` → an aggregateGroups root. */
|
|
3710
|
+
lowerAggregateGroupsRoot(node, { alias, table }) {
|
|
3711
|
+
assertKnownArgs(node, ["where", "groupBy", "limit", "offset"]);
|
|
3712
|
+
if (!node.selectionSet) fail2(`'${node.name.value}' needs a selection set`, "invalid-selection");
|
|
3713
|
+
const args = argsByName(node);
|
|
3714
|
+
const groupByArg = args.get("groupBy");
|
|
3715
|
+
if (!groupByArg) fail2(`'${node.name.value}' requires a 'groupBy' argument`, "missing-group-by");
|
|
3716
|
+
const whereArg = args.get("where");
|
|
3717
|
+
const groupBy = this.lowerGroupBy(groupByArg.value, table);
|
|
3718
|
+
const aggregate = this.lowerGroupedAggregateSelection(node.selectionSet, table, groupBy);
|
|
3719
|
+
const offset = this.lowerOffset(args.get("offset")?.value);
|
|
3720
|
+
return {
|
|
3721
|
+
kind: "aggregateGroups",
|
|
3722
|
+
alias,
|
|
3723
|
+
table,
|
|
3724
|
+
where: whereArg ? this.lowerBoolExp(whereArg.value, table) : CONST_TRUE,
|
|
3725
|
+
groupBy,
|
|
3726
|
+
key: aggregate.key,
|
|
3727
|
+
...aggregate.count ? { count: aggregate.count } : {},
|
|
3728
|
+
...aggregate.sum ? { sum: aggregate.sum } : {},
|
|
3729
|
+
...aggregate.avg ? { avg: aggregate.avg } : {},
|
|
3730
|
+
limit: this.lowerGroupLimit(args.get("limit")?.value),
|
|
3731
|
+
...offset ? { offset } : {}
|
|
3732
|
+
};
|
|
3733
|
+
}
|
|
3734
|
+
/**
|
|
3735
|
+
* `groupBy: [{ column: startedAt, bucket: MONTH }, …]` → GroupByKeyIr[].
|
|
3736
|
+
*
|
|
3737
|
+
* Document-authored only: a whole-argument variable is rejected rather than
|
|
3738
|
+
* becoming a §4f slot. Group keys choose which expressions reach GROUP BY,
|
|
3739
|
+
* and dynamic-input.ts refuses aggregation shapes from client input on the
|
|
3740
|
+
* same reasoning — `where` stays dynamic, which is what runtime filtering
|
|
3741
|
+
* actually needs.
|
|
3742
|
+
*/
|
|
3743
|
+
lowerGroupBy(node, table) {
|
|
3744
|
+
if (node.kind === graphql.Kind.VARIABLE) {
|
|
3745
|
+
fail2(
|
|
3746
|
+
`groupBy on '${table}' must be written in the document \u2014 it cannot come from a variable`,
|
|
3747
|
+
"dynamic-group-by"
|
|
3748
|
+
);
|
|
3749
|
+
}
|
|
3750
|
+
const items = node.kind === graphql.Kind.LIST ? node.values : [node];
|
|
3751
|
+
if (items.length === 0) fail2(`groupBy on '${table}' needs at least one key`, "invalid-group-by");
|
|
3752
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3753
|
+
return items.map((item) => {
|
|
3754
|
+
if (item.kind !== graphql.Kind.OBJECT) {
|
|
3755
|
+
fail2(`groupBy entries on '${table}' must be objects like { column: foo }`, "invalid-group-by");
|
|
3756
|
+
}
|
|
3757
|
+
const fields = new Map(item.fields.map((field) => [field.name.value, field.value]));
|
|
3758
|
+
for (const key of fields.keys()) {
|
|
3759
|
+
if (key !== "column" && key !== "bucket") {
|
|
3760
|
+
fail2(`unknown groupBy field '${key}' on '${table}'`, "invalid-group-by", ["column", "bucket"]);
|
|
3761
|
+
}
|
|
3762
|
+
}
|
|
3763
|
+
const columnNode = fields.get("column");
|
|
3764
|
+
if (!columnNode) fail2(`groupBy entries on '${table}' need a 'column'`, "invalid-group-by");
|
|
3765
|
+
if (columnNode.kind !== graphql.Kind.ENUM) {
|
|
3766
|
+
fail2(`groupBy column on '${table}' must be a group-by column enum value`, "invalid-group-by");
|
|
3767
|
+
}
|
|
3768
|
+
const column = this.column(table, columnNode.value);
|
|
3769
|
+
if (!column) {
|
|
3770
|
+
fail2(`unknown groupBy column '${columnNode.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
|
|
3771
|
+
}
|
|
3772
|
+
if (column.type === "jsonb") {
|
|
3773
|
+
fail2(`groupBy column '${table}.${columnNode.value}' is jsonb and cannot be a group key`, "invalid-group-by");
|
|
3774
|
+
}
|
|
3775
|
+
const bucketNode = fields.get("bucket");
|
|
3776
|
+
let bucket;
|
|
3777
|
+
if (bucketNode) {
|
|
3778
|
+
if (bucketNode.kind !== graphql.Kind.ENUM) {
|
|
3779
|
+
fail2(`groupBy bucket on '${table}.${columnNode.value}' must be a DateBucket enum value`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
|
|
3780
|
+
}
|
|
3781
|
+
bucket = DATE_BUCKET_BY_ENUM[bucketNode.value];
|
|
3782
|
+
if (!bucket) {
|
|
3783
|
+
fail2(`unknown DateBucket '${bucketNode.value}'`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
|
|
3784
|
+
}
|
|
3785
|
+
if (typeof column.type !== "string" || !BUCKETABLE_TYPES.has(column.type)) {
|
|
3786
|
+
fail2(
|
|
3787
|
+
`groupBy bucket is only valid on date/timestamptz columns \u2014 '${table}.${columnNode.value}' is ${typeof column.type === "string" ? column.type : "an enum"}`,
|
|
3788
|
+
"invalid-group-by"
|
|
3789
|
+
);
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
if (seen.has(columnNode.value)) {
|
|
3793
|
+
fail2(`duplicate groupBy column '${columnNode.value}' on '${table}'`, "invalid-group-by");
|
|
3794
|
+
}
|
|
3795
|
+
seen.add(columnNode.value);
|
|
3796
|
+
return bucket ? { column: columnNode.value, bucket } : { column: columnNode.value };
|
|
3797
|
+
});
|
|
3798
|
+
}
|
|
3799
|
+
/** Like lowerLimit, but bounded by MAX_GROUPS and defaulting to it. */
|
|
3800
|
+
lowerGroupLimit(node) {
|
|
3801
|
+
const base = { default: MAX_GROUPS, max: MAX_GROUPS };
|
|
3802
|
+
if (!node) return base;
|
|
3803
|
+
if (node.kind === graphql.Kind.VARIABLE) return { ...base, variable: node.name.value };
|
|
3804
|
+
if (node.kind === graphql.Kind.INT) {
|
|
3805
|
+
const value = Number(node.value);
|
|
3806
|
+
if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
|
|
3807
|
+
return { ...base, value: Math.min(value, MAX_GROUPS) };
|
|
3808
|
+
}
|
|
3809
|
+
fail2("limit must be an integer literal or a variable", "invalid-limit");
|
|
3810
|
+
}
|
|
2455
3811
|
/** byPk-style pk arguments: one argument per primary-key column, exactly. */
|
|
2456
3812
|
lowerPkArgs(node, table) {
|
|
2457
3813
|
const meta = this.meta(table);
|
|
@@ -2459,7 +3815,7 @@ var OperationLowerer = class {
|
|
|
2459
3815
|
assertKnownArgs(node, meta.primaryKey);
|
|
2460
3816
|
return meta.primaryKey.map((column) => {
|
|
2461
3817
|
const arg = args.get(column);
|
|
2462
|
-
if (!arg)
|
|
3818
|
+
if (!arg) fail2(`'${node.name.value}' is missing primary-key argument '${column}'`, "missing-pk-argument", meta.primaryKey);
|
|
2463
3819
|
return { column, value: this.lowerValue(arg.value) };
|
|
2464
3820
|
});
|
|
2465
3821
|
}
|
|
@@ -2470,19 +3826,22 @@ var OperationLowerer = class {
|
|
|
2470
3826
|
const name = node.name.value;
|
|
2471
3827
|
const alias = node.alias?.value ?? name;
|
|
2472
3828
|
const rootInfo = this.ctx.mutationRoots.get(name);
|
|
2473
|
-
if (!rootInfo)
|
|
3829
|
+
if (!rootInfo) fail2(`unknown mutation root '${name}'`, "unknown-root", [...this.ctx.mutationRoots.keys()]);
|
|
2474
3830
|
if (rootInfo.op === "cowOverride" || rootInfo.op === "cowRevert") {
|
|
2475
3831
|
return this.lowerCowRoot(node, rootInfo);
|
|
2476
3832
|
}
|
|
3833
|
+
if (rootInfo.op === "resolvedWrite") {
|
|
3834
|
+
return this.lowerResolvedRoot(node, rootInfo);
|
|
3835
|
+
}
|
|
2477
3836
|
const { op, table } = rootInfo;
|
|
2478
3837
|
const args = argsByName(node);
|
|
2479
|
-
if (!node.selectionSet)
|
|
3838
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
2480
3839
|
const base = { kind: "mutation", op, alias, table, pinnedColumns: [], affectedRows: false };
|
|
2481
3840
|
switch (op) {
|
|
2482
3841
|
case "insert": {
|
|
2483
3842
|
assertKnownArgs(node, ["objects", "onConflict"]);
|
|
2484
3843
|
const objects = args.get("objects");
|
|
2485
|
-
if (!objects)
|
|
3844
|
+
if (!objects) fail2(`'${name}' needs objects:`, "missing-argument");
|
|
2486
3845
|
const response = this.lowerMutationResponse(node.selectionSet, table);
|
|
2487
3846
|
return {
|
|
2488
3847
|
...base,
|
|
@@ -2494,7 +3853,7 @@ var OperationLowerer = class {
|
|
|
2494
3853
|
case "insertOne": {
|
|
2495
3854
|
assertKnownArgs(node, ["object", "onConflict"]);
|
|
2496
3855
|
const object = args.get("object");
|
|
2497
|
-
if (!object)
|
|
3856
|
+
if (!object) fail2(`'${name}' needs object:`, "missing-argument");
|
|
2498
3857
|
return {
|
|
2499
3858
|
...base,
|
|
2500
3859
|
objects: this.lowerInsertObjects(object.value, table, { list: false }),
|
|
@@ -2505,7 +3864,7 @@ var OperationLowerer = class {
|
|
|
2505
3864
|
case "update": {
|
|
2506
3865
|
assertKnownArgs(node, ["where", "_set", "_append", "_inc"]);
|
|
2507
3866
|
const where = args.get("where");
|
|
2508
|
-
if (!where)
|
|
3867
|
+
if (!where) fail2(`'${name}' needs where: (bulk updates without a filter are not allowed)`, "missing-argument");
|
|
2509
3868
|
return {
|
|
2510
3869
|
...base,
|
|
2511
3870
|
...this.lowerMutationResponse(node.selectionSet, table),
|
|
@@ -2516,7 +3875,7 @@ var OperationLowerer = class {
|
|
|
2516
3875
|
case "updateByPk": {
|
|
2517
3876
|
assertKnownArgs(node, ["pkColumns", "_set", "_append", "_inc"]);
|
|
2518
3877
|
const pkColumns = args.get("pkColumns");
|
|
2519
|
-
if (!pkColumns)
|
|
3878
|
+
if (!pkColumns) fail2(`'${name}' needs pkColumns:`, "missing-argument");
|
|
2520
3879
|
return {
|
|
2521
3880
|
...base,
|
|
2522
3881
|
pk: this.lowerPkColumnsArg(pkColumns.value, table),
|
|
@@ -2527,7 +3886,7 @@ var OperationLowerer = class {
|
|
|
2527
3886
|
case "delete": {
|
|
2528
3887
|
assertKnownArgs(node, ["where"]);
|
|
2529
3888
|
const where = args.get("where");
|
|
2530
|
-
if (!where)
|
|
3889
|
+
if (!where) fail2(`'${name}' needs where: (bulk deletes without a filter are not allowed)`, "missing-argument");
|
|
2531
3890
|
return {
|
|
2532
3891
|
...base,
|
|
2533
3892
|
...this.lowerMutationResponse(node.selectionSet, table),
|
|
@@ -2554,11 +3913,11 @@ var OperationLowerer = class {
|
|
|
2554
3913
|
const name = node.name.value;
|
|
2555
3914
|
const alias = node.alias?.value ?? name;
|
|
2556
3915
|
const { table } = rootInfo;
|
|
2557
|
-
if (!node.selectionSet)
|
|
3916
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
2558
3917
|
const meta = this.meta(table);
|
|
2559
3918
|
const args = argsByName(node);
|
|
2560
3919
|
const pkArg = args.get(rootInfo.pk);
|
|
2561
|
-
if (!pkArg)
|
|
3920
|
+
if (!pkArg) fail2(`'${name}' is missing primary-key argument '${rootInfo.pk}'`, "missing-pk-argument", [rootInfo.pk]);
|
|
2562
3921
|
const pk = [{ column: rootInfo.pk, value: this.lowerValue(pkArg.value) }];
|
|
2563
3922
|
const base = {
|
|
2564
3923
|
kind: "mutation",
|
|
@@ -2579,24 +3938,78 @@ var OperationLowerer = class {
|
|
|
2579
3938
|
const entries = rootInfo.set.map((column) => {
|
|
2580
3939
|
const arg = args.get(column);
|
|
2581
3940
|
if (!arg) {
|
|
2582
|
-
|
|
3941
|
+
fail2(
|
|
2583
3942
|
`'${name}' is missing set argument '${column}' \u2014 a cow override writes every declared set column (pass null to clear a nullable one)`,
|
|
2584
3943
|
"missing-argument",
|
|
2585
3944
|
[...rootInfo.set]
|
|
2586
3945
|
);
|
|
2587
3946
|
}
|
|
2588
3947
|
if (!meta.settable.includes(column)) {
|
|
2589
|
-
|
|
3948
|
+
fail2(`column '${table}.${column}' is not updatable (pinned by the write policy or deprecated)`, "column-not-settable", meta.settable);
|
|
2590
3949
|
}
|
|
2591
3950
|
return { column, value: this.lowerValue(arg.value) };
|
|
2592
3951
|
});
|
|
2593
3952
|
return { ...base, set: { entries }, cowCopyColumns: [...rootInfo.copyColumns] };
|
|
2594
3953
|
}
|
|
3954
|
+
/**
|
|
3955
|
+
* Resolved-table roots: insertOne(object), updateByPk(pkColumns, _set),
|
|
3956
|
+
* deleteByPk(pk…), revertByPk(pkColumns, columns). The executor writes only
|
|
3957
|
+
* the touched cells into the manual source; `returning` reads the view.
|
|
3958
|
+
*/
|
|
3959
|
+
lowerResolvedRoot(node, rootInfo) {
|
|
3960
|
+
const name = node.name.value;
|
|
3961
|
+
const alias = node.alias?.value ?? name;
|
|
3962
|
+
const { table, action } = rootInfo;
|
|
3963
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
3964
|
+
const args = argsByName(node);
|
|
3965
|
+
const base = {
|
|
3966
|
+
kind: "mutation",
|
|
3967
|
+
op: "resolvedWrite",
|
|
3968
|
+
resolvedAction: action,
|
|
3969
|
+
alias,
|
|
3970
|
+
table,
|
|
3971
|
+
pinnedColumns: [],
|
|
3972
|
+
affectedRows: false,
|
|
3973
|
+
returning: this.lowerSelection(node.selectionSet, table)
|
|
3974
|
+
};
|
|
3975
|
+
switch (action) {
|
|
3976
|
+
case "insert": {
|
|
3977
|
+
assertKnownArgs(node, ["object"]);
|
|
3978
|
+
const object = args.get("object");
|
|
3979
|
+
if (!object) fail2(`'${name}' needs object:`, "missing-argument");
|
|
3980
|
+
return { ...base, objects: this.lowerInsertObjects(object.value, table, { list: false }) };
|
|
3981
|
+
}
|
|
3982
|
+
case "update": {
|
|
3983
|
+
assertKnownArgs(node, ["pkColumns", "_set"]);
|
|
3984
|
+
const pkColumns = args.get("pkColumns");
|
|
3985
|
+
if (!pkColumns || !args.get("_set")) fail2(`'${name}' needs pkColumns: and _set:`, "missing-argument");
|
|
3986
|
+
return { ...base, pk: this.lowerPkColumnsArg(pkColumns.value, table), ...this.lowerWriteValues(args, table) };
|
|
3987
|
+
}
|
|
3988
|
+
case "delete":
|
|
3989
|
+
return { ...base, pk: this.lowerPkArgs(node, table) };
|
|
3990
|
+
case "revert": {
|
|
3991
|
+
assertKnownArgs(node, ["pkColumns", "columns"]);
|
|
3992
|
+
const pkColumns = args.get("pkColumns");
|
|
3993
|
+
const columns = args.get("columns");
|
|
3994
|
+
if (!pkColumns || !columns) fail2(`'${name}' needs pkColumns: and columns:`, "missing-argument");
|
|
3995
|
+
const items = columns.value.kind === graphql.Kind.LIST ? columns.value.values : [columns.value];
|
|
3996
|
+
const meta = this.meta(table);
|
|
3997
|
+
const revertColumns = items.map((item) => {
|
|
3998
|
+
if (item.kind !== graphql.Kind.ENUM || !meta.settable.includes(item.value)) {
|
|
3999
|
+
fail2(`'${name}' columns: must list update-column enum values written in the document`, "invalid-revert-columns", meta.settable);
|
|
4000
|
+
}
|
|
4001
|
+
return item.value;
|
|
4002
|
+
});
|
|
4003
|
+
if (revertColumns.length === 0) fail2(`'${name}' columns: must name at least one column`, "invalid-revert-columns");
|
|
4004
|
+
return { ...base, pk: this.lowerPkColumnsArg(pkColumns.value, table), revertColumns: [...new Set(revertColumns)] };
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
2595
4008
|
lowerMutationResponse(selectionSet, table) {
|
|
2596
4009
|
let affectedRows = false;
|
|
2597
4010
|
let returning = null;
|
|
2598
4011
|
for (const node of selectionSet.selections) {
|
|
2599
|
-
if (node.kind !== graphql.Kind.FIELD)
|
|
4012
|
+
if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
2600
4013
|
const name = node.name.value;
|
|
2601
4014
|
if (name === "__typename") continue;
|
|
2602
4015
|
if (name === "affectedRows") {
|
|
@@ -2604,34 +4017,34 @@ var OperationLowerer = class {
|
|
|
2604
4017
|
continue;
|
|
2605
4018
|
}
|
|
2606
4019
|
if (name === "returning") {
|
|
2607
|
-
if (!node.selectionSet)
|
|
4020
|
+
if (!node.selectionSet) fail2("'returning' needs a selection set", "invalid-selection");
|
|
2608
4021
|
returning = this.lowerSelection(node.selectionSet, table);
|
|
2609
4022
|
continue;
|
|
2610
4023
|
}
|
|
2611
|
-
|
|
4024
|
+
fail2(`unknown mutation-response field '${name}'`, "invalid-selection", ["affectedRows", "returning"]);
|
|
2612
4025
|
}
|
|
2613
4026
|
return { affectedRows, returning };
|
|
2614
4027
|
}
|
|
2615
4028
|
lowerInsertObjects(node, table, { list }) {
|
|
2616
4029
|
if (node.kind === graphql.Kind.VARIABLE) return { slot: this.slot(node.name.value, "insertInput", table) };
|
|
2617
4030
|
if (list) {
|
|
2618
|
-
if (node.kind !== graphql.Kind.LIST)
|
|
4031
|
+
if (node.kind !== graphql.Kind.LIST) fail2(`objects: must be a list literal or a variable`, "invalid-insert");
|
|
2619
4032
|
return { rows: node.values.map((item) => this.lowerInsertRow(item, table)) };
|
|
2620
4033
|
}
|
|
2621
4034
|
return { rows: [this.lowerInsertRow(node, table)] };
|
|
2622
4035
|
}
|
|
2623
4036
|
lowerInsertRow(node, table) {
|
|
2624
|
-
if (node.kind !== graphql.Kind.OBJECT)
|
|
4037
|
+
if (node.kind !== graphql.Kind.OBJECT) fail2(`insert rows on '${table}' must be objects`, "invalid-insert");
|
|
2625
4038
|
const meta = this.meta(table);
|
|
2626
4039
|
return node.fields.map((field) => {
|
|
2627
4040
|
const key = field.name.value;
|
|
2628
4041
|
if (this.link(table, key)) {
|
|
2629
|
-
|
|
4042
|
+
fail2(`nested inserts are not supported in v2.0 \u2014 '${key}' is a link on '${table}'`, "nested-insert-unsupported");
|
|
2630
4043
|
}
|
|
2631
4044
|
if (!meta.insertable.includes(key)) {
|
|
2632
4045
|
const column = this.column(table, key);
|
|
2633
|
-
if (column)
|
|
2634
|
-
|
|
4046
|
+
if (column) fail2(`column '${table}.${key}' is not insertable (pinned by the write policy or deprecated)`, "column-not-insertable", meta.insertable);
|
|
4047
|
+
fail2(`unknown column '${key}' in an insert on '${table}'`, "unknown-column", meta.insertable);
|
|
2635
4048
|
}
|
|
2636
4049
|
return { column: key, value: this.lowerValue(field.value) };
|
|
2637
4050
|
});
|
|
@@ -2640,32 +4053,32 @@ var OperationLowerer = class {
|
|
|
2640
4053
|
if (!arg) return {};
|
|
2641
4054
|
const node = arg.value;
|
|
2642
4055
|
if (node.kind === graphql.Kind.VARIABLE) return { onConflict: { slot: this.slot(node.name.value, "onConflict", table) } };
|
|
2643
|
-
if (node.kind !== graphql.Kind.OBJECT)
|
|
4056
|
+
if (node.kind !== graphql.Kind.OBJECT) fail2("onConflict: must be an object literal or a variable", "invalid-on-conflict");
|
|
2644
4057
|
const meta = this.meta(table);
|
|
2645
4058
|
let constraint;
|
|
2646
4059
|
const updateColumns = [];
|
|
2647
4060
|
for (const field of node.fields) {
|
|
2648
4061
|
if (field.name.value === "constraint") {
|
|
2649
|
-
if (field.value.kind !== graphql.Kind.ENUM)
|
|
4062
|
+
if (field.value.kind !== graphql.Kind.ENUM) fail2("onConflict.constraint must be a constraint enum value", "invalid-on-conflict");
|
|
2650
4063
|
constraint = field.value.value;
|
|
2651
4064
|
continue;
|
|
2652
4065
|
}
|
|
2653
4066
|
if (field.name.value === "updateColumns") {
|
|
2654
4067
|
const items = field.value.kind === graphql.Kind.LIST ? field.value.values : [field.value];
|
|
2655
4068
|
for (const item of items) {
|
|
2656
|
-
if (item.kind !== graphql.Kind.ENUM)
|
|
4069
|
+
if (item.kind !== graphql.Kind.ENUM) fail2("onConflict.updateColumns must be update-column enum values", "invalid-on-conflict");
|
|
2657
4070
|
if (!meta.settable.includes(item.value)) {
|
|
2658
|
-
|
|
4071
|
+
fail2(`onConflict.updateColumns: '${item.value}' is not updatable on '${table}'`, "invalid-on-conflict", meta.settable);
|
|
2659
4072
|
}
|
|
2660
4073
|
updateColumns.push(item.value);
|
|
2661
4074
|
}
|
|
2662
4075
|
continue;
|
|
2663
4076
|
}
|
|
2664
|
-
|
|
4077
|
+
fail2(`unknown onConflict key '${field.name.value}'`, "invalid-on-conflict", ["constraint", "updateColumns"]);
|
|
2665
4078
|
}
|
|
2666
|
-
if (!constraint)
|
|
4079
|
+
if (!constraint) fail2("onConflict needs a constraint", "invalid-on-conflict", meta.constraints.map((c) => c.name));
|
|
2667
4080
|
if (!meta.constraints.some((c) => c.name === constraint)) {
|
|
2668
|
-
|
|
4081
|
+
fail2(`unknown constraint '${constraint}' on '${table}'`, "invalid-on-conflict", meta.constraints.map((c) => c.name));
|
|
2669
4082
|
}
|
|
2670
4083
|
return { onConflict: { constraint, updateColumns } };
|
|
2671
4084
|
}
|
|
@@ -2683,36 +4096,36 @@ var OperationLowerer = class {
|
|
|
2683
4096
|
const inc = args.get("_inc");
|
|
2684
4097
|
if (inc) {
|
|
2685
4098
|
if (inc.value.kind === graphql.Kind.VARIABLE) {
|
|
2686
|
-
|
|
4099
|
+
fail2("_inc has no dynamic input type \u2014 inline the columns with variable leaf values", "invalid-inc");
|
|
2687
4100
|
}
|
|
2688
4101
|
const numeric = meta.columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type) && meta.settable.includes(c.name)).map((c) => c.name);
|
|
2689
4102
|
result.inc = { entries: this.lowerColumnEntries(inc.value, table, numeric, "_inc") };
|
|
2690
4103
|
}
|
|
2691
4104
|
if (!result.set && !result.append && !result.inc) {
|
|
2692
|
-
|
|
4105
|
+
fail2(`an update on '${table}' needs at least one of _set/_append/_inc`, "missing-argument");
|
|
2693
4106
|
}
|
|
2694
4107
|
return result;
|
|
2695
4108
|
}
|
|
2696
4109
|
lowerColumnEntries(node, table, whitelist, argName) {
|
|
2697
|
-
if (node.kind !== graphql.Kind.OBJECT)
|
|
4110
|
+
if (node.kind !== graphql.Kind.OBJECT) fail2(`${argName} on '${table}' must be an object literal or a variable`, "invalid-write-values");
|
|
2698
4111
|
return node.fields.map((field) => {
|
|
2699
4112
|
const key = field.name.value;
|
|
2700
4113
|
if (!whitelist.includes(key)) {
|
|
2701
|
-
|
|
4114
|
+
fail2(`${argName}: column '${key}' is not allowed on '${table}'`, "column-not-settable", [...whitelist]);
|
|
2702
4115
|
}
|
|
2703
4116
|
return { column: key, value: this.lowerValue(field.value) };
|
|
2704
4117
|
});
|
|
2705
4118
|
}
|
|
2706
4119
|
lowerPkColumnsArg(node, table) {
|
|
2707
|
-
if (node.kind !== graphql.Kind.OBJECT)
|
|
4120
|
+
if (node.kind !== graphql.Kind.OBJECT) fail2(`pkColumns on '${table}' must be an object literal`, "invalid-pk-columns");
|
|
2708
4121
|
const meta = this.meta(table);
|
|
2709
4122
|
const provided = new Map(node.fields.map((field) => [field.name.value, field.value]));
|
|
2710
4123
|
for (const key of provided.keys()) {
|
|
2711
|
-
if (!meta.primaryKey.includes(key))
|
|
4124
|
+
if (!meta.primaryKey.includes(key)) fail2(`pkColumns: '${key}' is not a primary-key column of '${table}'`, "invalid-pk-columns", meta.primaryKey);
|
|
2712
4125
|
}
|
|
2713
4126
|
return meta.primaryKey.map((column) => {
|
|
2714
4127
|
const value = provided.get(column);
|
|
2715
|
-
if (!value)
|
|
4128
|
+
if (!value) fail2(`pkColumns is missing '${column}'`, "invalid-pk-columns", meta.primaryKey);
|
|
2716
4129
|
return { column, value: this.lowerValue(value) };
|
|
2717
4130
|
});
|
|
2718
4131
|
}
|
|
@@ -2721,6 +4134,19 @@ function lowerOperation({ restricted, ctx }) {
|
|
|
2721
4134
|
const lowerer = new OperationLowerer(ctx, restricted);
|
|
2722
4135
|
return lowerer.lowerRoots(restricted.definition, restricted.kind);
|
|
2723
4136
|
}
|
|
4137
|
+
function isCellColumn(meta, name) {
|
|
4138
|
+
if (!meta.cells) return false;
|
|
4139
|
+
return Object.values(meta.cells).some((parts) => Object.values(parts).includes(name));
|
|
4140
|
+
}
|
|
4141
|
+
function fieldNodes(selectionSet) {
|
|
4142
|
+
const out = [];
|
|
4143
|
+
for (const node of selectionSet.selections) {
|
|
4144
|
+
if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
4145
|
+
if (node.directives && node.directives.length > 0) fail2(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
|
|
4146
|
+
if (node.name.value !== "__typename") out.push(node);
|
|
4147
|
+
}
|
|
4148
|
+
return out;
|
|
4149
|
+
}
|
|
2724
4150
|
function parseSdl(sdl) {
|
|
2725
4151
|
return graphql.buildSchema(sdl);
|
|
2726
4152
|
}
|
|
@@ -2961,7 +4387,7 @@ function compileNamespace({ canonical, sdl, documents }) {
|
|
|
2961
4387
|
const loweredRoots = lowerOperation({ restricted: { ...restricted, definition: flattened }, ctx });
|
|
2962
4388
|
const { roots, tableSet } = injectPolicies({ roots: loweredRoots, ctx });
|
|
2963
4389
|
operations[restricted.name] = {
|
|
2964
|
-
irVersion: IR_VERSION,
|
|
4390
|
+
irVersion: roots.some((root) => root.kind === "mutation" && root.assertAffectedRows !== void 0) ? IR_VERSION : LEGACY_IR_VERSION,
|
|
2965
4391
|
name: restricted.name,
|
|
2966
4392
|
opHash: computeOpHash({ definition: flattened, schemaHash }),
|
|
2967
4393
|
kind: restricted.kind,
|
|
@@ -2988,7 +4414,7 @@ function compileNamespace({ canonical, sdl, documents }) {
|
|
|
2988
4414
|
}
|
|
2989
4415
|
if (diagnostics.some((d) => d.severity === "error")) return { ok: false, diagnostics };
|
|
2990
4416
|
const artifact = {
|
|
2991
|
-
irVersion:
|
|
4417
|
+
irVersion: requiredArtifactIrVersion(Object.values(operations)),
|
|
2992
4418
|
namespace: ctx.namespace,
|
|
2993
4419
|
schemaHash,
|
|
2994
4420
|
opsHash: computeOpsHash(Object.values(operations).map((op) => op.opHash)),
|