@sequenceholdings/orm 2.1.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 +38 -1
- package/dist/authoring.cjs +1358 -45
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +223 -2
- package/dist/authoring.d.ts +223 -2
- package/dist/authoring.js +1349 -47
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +662 -38
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +696 -53
- package/dist/canonical.d.ts +696 -53
- package/dist/canonical.js +647 -39
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +1808 -247
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1809 -248
- 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 +1325 -285
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +23 -2
- package/dist/compiler.d.ts +23 -2
- package/dist/compiler.js +1325 -285
- 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 +1605 -140
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +230 -113
- package/dist/index.d.ts +230 -113
- package/dist/index.js +1583 -142
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +727 -38
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +539 -21
- package/dist/ir.d.ts +539 -21
- package/dist/ir.js +722 -39
- package/dist/ir.js.map +1 -1
- package/dist/{naming-DCn94LJ4.d.ts → naming-CPfx7EfF.d.ts} +14 -1
- package/dist/{naming-DPglwzkB.d.cts → naming-CU9ThP_E.d.cts} +14 -1
- package/dist/policy-1iZbGnZZ.d.cts +24 -0
- package/dist/policy-1iZbGnZZ.d.ts +24 -0
- package/dist/predicate-v2.cjs +726 -42
- 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 +726 -42
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +858 -54
- 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 +856 -55
- package/dist/sdl.js.map +1 -1
- package/package.json +9 -4
- 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.js
CHANGED
|
@@ -6,9 +6,25 @@ import { Kind, print, visit, buildSchema, parse, validate, valueFromASTUntyped }
|
|
|
6
6
|
|
|
7
7
|
// src/constants.ts
|
|
8
8
|
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
9
|
-
var
|
|
9
|
+
var MAX_IDENTIFIER_LENGTH = 63;
|
|
10
|
+
var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
|
|
10
11
|
var ORM_SCHEMA_PREFIX = "orm_";
|
|
11
12
|
var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
|
|
13
|
+
var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
|
|
14
|
+
var CELL_PART_SUFFIX = {
|
|
15
|
+
at: "__at",
|
|
16
|
+
by: "__by",
|
|
17
|
+
rev: "__rev",
|
|
18
|
+
source: "__src",
|
|
19
|
+
conflicts: "__lost"
|
|
20
|
+
};
|
|
21
|
+
var MAX_TRACKED_COLUMN_NAME = MAX_IDENTIFIER_LENGTH - 6;
|
|
22
|
+
var CELLS_FIELD = "cells";
|
|
23
|
+
function cellColumnName(column, part) {
|
|
24
|
+
return `${column}${CELL_PART_SUFFIX[part]}`;
|
|
25
|
+
}
|
|
26
|
+
var MATERIALIZE_ACTOR = "orm:materialize";
|
|
27
|
+
JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
|
|
12
28
|
|
|
13
29
|
// src/hash.ts
|
|
14
30
|
function stableJson(value) {
|
|
@@ -17,9 +33,468 @@ function stableJson(value) {
|
|
|
17
33
|
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)}`);
|
|
18
34
|
return `{${entries.join(",")}}`;
|
|
19
35
|
}
|
|
36
|
+
var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
37
|
+
var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
38
|
+
|
|
39
|
+
// src/checks/compile.ts
|
|
40
|
+
function tokenize(source) {
|
|
41
|
+
const tokens = [];
|
|
42
|
+
for (let position = 0; position < source.length; ) {
|
|
43
|
+
const char = source[position];
|
|
44
|
+
if (/\s/.test(char)) {
|
|
45
|
+
position++;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (char === "(" || char === ")" || char === ",") {
|
|
49
|
+
tokens.push({ kind: char === "(" ? "lparen" : char === ")" ? "rparen" : "comma", value: char, position });
|
|
50
|
+
position++;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (char === "'") {
|
|
54
|
+
const start = position++;
|
|
55
|
+
let value = "";
|
|
56
|
+
let closed = false;
|
|
57
|
+
while (position < source.length) {
|
|
58
|
+
if (source[position] !== "'") {
|
|
59
|
+
value += source[position++];
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (source[position + 1] === "'") {
|
|
63
|
+
value += "'";
|
|
64
|
+
position += 2;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
position++;
|
|
68
|
+
closed = true;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
if (!closed) throw new Error(`unterminated string at position ${start}`);
|
|
72
|
+
if (/[\x00-\x1f\x7f]/.test(value)) throw new Error(`control character in string at position ${start}`);
|
|
73
|
+
tokens.push({ kind: "string", value, position: start });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const operator = /^(<=|>=|<>|!=|!~|=|<|>|~)/.exec(source.slice(position));
|
|
77
|
+
if (operator) {
|
|
78
|
+
tokens.push({ kind: "operator", value: operator[0] === "<>" ? "!=" : operator[0], position });
|
|
79
|
+
position += operator[0].length;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const number = /^-?\d+(?:\.\d+)?/.exec(source.slice(position));
|
|
83
|
+
if (number) {
|
|
84
|
+
tokens.push({ kind: "number", value: number[0], position });
|
|
85
|
+
position += number[0].length;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const identifier3 = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(source.slice(position));
|
|
89
|
+
if (identifier3) {
|
|
90
|
+
tokens.push({ kind: "identifier", value: identifier3[0], position });
|
|
91
|
+
position += identifier3[0].length;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
throw new Error(`unexpected character '${char}' at position ${position}`);
|
|
95
|
+
}
|
|
96
|
+
tokens.push({ kind: "end", value: "", position: source.length });
|
|
97
|
+
if (tokens.length > 256) throw new Error("check expression exceeds 256 tokens");
|
|
98
|
+
return tokens;
|
|
99
|
+
}
|
|
100
|
+
var Parser = class {
|
|
101
|
+
constructor(tokens, columns) {
|
|
102
|
+
this.tokens = tokens;
|
|
103
|
+
this.columns = /* @__PURE__ */ new Map();
|
|
104
|
+
for (const column of columns) {
|
|
105
|
+
for (const alias of [column.name, column.columnName]) {
|
|
106
|
+
const previous = this.columns.get(alias);
|
|
107
|
+
if (previous && previous.columnName !== column.columnName) {
|
|
108
|
+
throw new Error(`ambiguous check column '${alias}'`);
|
|
109
|
+
}
|
|
110
|
+
this.columns.set(alias, column);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
index = 0;
|
|
115
|
+
depth = 0;
|
|
116
|
+
columns;
|
|
117
|
+
parse() {
|
|
118
|
+
const expression = this.parseOr();
|
|
119
|
+
this.expect("end", "end of expression");
|
|
120
|
+
return expression;
|
|
121
|
+
}
|
|
122
|
+
parseOr() {
|
|
123
|
+
let left = this.parseAnd();
|
|
124
|
+
while (this.matchKeyword("or")) left = `(${left} OR ${this.parseAnd()})`;
|
|
125
|
+
return left;
|
|
126
|
+
}
|
|
127
|
+
parseAnd() {
|
|
128
|
+
let left = this.parseNot();
|
|
129
|
+
while (this.matchKeyword("and")) left = `(${left} AND ${this.parseNot()})`;
|
|
130
|
+
return left;
|
|
131
|
+
}
|
|
132
|
+
parseNot() {
|
|
133
|
+
if (this.matchKeyword("not")) return `(NOT ${this.parseNot()})`;
|
|
134
|
+
return this.parsePrimary();
|
|
135
|
+
}
|
|
136
|
+
parsePrimary() {
|
|
137
|
+
if (this.peek().kind === "lparen") {
|
|
138
|
+
if (++this.depth > 32) throw new Error("check expression nesting exceeds 32 levels");
|
|
139
|
+
this.advance();
|
|
140
|
+
const expression = this.parseOr();
|
|
141
|
+
this.expect("rparen", "')'");
|
|
142
|
+
this.depth--;
|
|
143
|
+
return `(${expression})`;
|
|
144
|
+
}
|
|
145
|
+
return this.parseComparison();
|
|
146
|
+
}
|
|
147
|
+
parseComparison() {
|
|
148
|
+
const left = this.parseValue();
|
|
149
|
+
if (this.matchKeyword("is")) {
|
|
150
|
+
const negate = this.matchKeyword("not");
|
|
151
|
+
this.expectKeyword("null");
|
|
152
|
+
return `${left.sql} IS ${negate ? "NOT " : ""}NULL`;
|
|
153
|
+
}
|
|
154
|
+
if (this.matchKeyword("between")) {
|
|
155
|
+
const lower = this.parseValue();
|
|
156
|
+
this.expectKeyword("and");
|
|
157
|
+
const upper = this.parseValue();
|
|
158
|
+
return `${left.sql} BETWEEN ${lower.sql} AND ${upper.sql}`;
|
|
159
|
+
}
|
|
160
|
+
if (this.matchKeyword("in")) {
|
|
161
|
+
this.expect("lparen", "'(' after IN");
|
|
162
|
+
const values = [this.parseValue()];
|
|
163
|
+
while (this.peek().kind === "comma") {
|
|
164
|
+
this.advance();
|
|
165
|
+
values.push(this.parseValue());
|
|
166
|
+
}
|
|
167
|
+
this.expect("rparen", "')' after IN values");
|
|
168
|
+
if (values.some((value) => !value.literal)) throw new Error("IN values must be literals");
|
|
169
|
+
return `${left.sql} IN (${values.map((value) => value.sql).join(", ")})`;
|
|
170
|
+
}
|
|
171
|
+
const operator = this.expect("operator", "a comparison operator");
|
|
172
|
+
const right = this.parseValue();
|
|
173
|
+
if (operator.value === "~" || operator.value === "!~") {
|
|
174
|
+
if (left.column && left.column.type !== "text") throw new Error("regex checks require a text column");
|
|
175
|
+
if (!right.literal) throw new Error("regex pattern must be a literal");
|
|
176
|
+
}
|
|
177
|
+
return `${left.sql} ${operator.value} ${right.sql}`;
|
|
178
|
+
}
|
|
179
|
+
parseValue() {
|
|
180
|
+
const token = this.advance();
|
|
181
|
+
if (token.kind === "string") return { sql: quoteLiteral(token.value), literal: true };
|
|
182
|
+
if (token.kind === "number") {
|
|
183
|
+
const number = Number(token.value);
|
|
184
|
+
if (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number)) {
|
|
185
|
+
throw new Error(`unsafe numeric literal at position ${token.position}`);
|
|
186
|
+
}
|
|
187
|
+
return { sql: token.value, literal: true };
|
|
188
|
+
}
|
|
189
|
+
if (token.kind !== "identifier") throw new Error(`expected a column or literal at position ${token.position}`);
|
|
190
|
+
const keyword = token.value.toLowerCase();
|
|
191
|
+
if (keyword === "true" || keyword === "false" || keyword === "null") return { sql: keyword.toUpperCase(), literal: true };
|
|
192
|
+
if (keyword === "btrim") {
|
|
193
|
+
this.expect("lparen", "'(' after btrim");
|
|
194
|
+
const column2 = this.parseColumn();
|
|
195
|
+
this.expect("rparen", "')' after btrim column");
|
|
196
|
+
if (column2.type !== "text") throw new Error("btrim requires a text column");
|
|
197
|
+
return { sql: `btrim(${quoteIdentifier(column2.columnName)})`, column: column2 };
|
|
198
|
+
}
|
|
199
|
+
const column = this.columns.get(token.value);
|
|
200
|
+
if (!column) throw new Error(`unknown check column '${token.value}'`);
|
|
201
|
+
return { sql: quoteIdentifier(column.columnName), column };
|
|
202
|
+
}
|
|
203
|
+
parseColumn() {
|
|
204
|
+
const token = this.expect("identifier", "a column name");
|
|
205
|
+
const column = this.columns.get(token.value);
|
|
206
|
+
if (!column) throw new Error(`unknown check column '${token.value}'`);
|
|
207
|
+
return column;
|
|
208
|
+
}
|
|
209
|
+
peek() {
|
|
210
|
+
return this.tokens[this.index];
|
|
211
|
+
}
|
|
212
|
+
advance() {
|
|
213
|
+
const token = this.peek();
|
|
214
|
+
if (token.kind !== "end") this.index++;
|
|
215
|
+
return token;
|
|
216
|
+
}
|
|
217
|
+
matchKeyword(keyword) {
|
|
218
|
+
if (this.peek().kind !== "identifier" || this.peek().value.toLowerCase() !== keyword) return false;
|
|
219
|
+
this.advance();
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
expectKeyword(keyword) {
|
|
223
|
+
if (!this.matchKeyword(keyword)) throw new Error(`expected '${keyword}' at position ${this.peek().position}`);
|
|
224
|
+
}
|
|
225
|
+
expect(kind, description) {
|
|
226
|
+
const token = this.peek();
|
|
227
|
+
if (token.kind !== kind) throw new Error(`expected ${description} at position ${token.position}`);
|
|
228
|
+
return this.advance();
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
function compileCheckExpression(source, columns) {
|
|
232
|
+
return new Parser(tokenize(source), columns).parse();
|
|
233
|
+
}
|
|
234
|
+
var policyVariantSchema = z.object({
|
|
235
|
+
/** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
|
|
236
|
+
when: z.string().min(1).max(1e3),
|
|
237
|
+
/** Exactly one of: allow-all bypass, or a row predicate. */
|
|
238
|
+
allow: z.literal(true).optional(),
|
|
239
|
+
policy: z.string().min(1).max(2e3).optional()
|
|
240
|
+
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
241
|
+
message: "A variant declares exactly one of allow: true or policy"
|
|
242
|
+
});
|
|
243
|
+
var policyExpressionSchema = z.union([
|
|
244
|
+
z.string().min(1).max(2e3),
|
|
245
|
+
z.object({
|
|
246
|
+
default: z.string().min(1).max(2e3),
|
|
247
|
+
variants: z.array(policyVariantSchema).max(10).default([])
|
|
248
|
+
})
|
|
249
|
+
]);
|
|
250
|
+
function parseDurationMs(input) {
|
|
251
|
+
if (typeof input === "number") return Number.isInteger(input) && input > 0 ? input : void 0;
|
|
252
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(input);
|
|
253
|
+
if (!match) return void 0;
|
|
254
|
+
const factor = match[2] === "ms" ? 1 : match[2] === "s" ? 1e3 : 6e4;
|
|
255
|
+
const value = Math.round(Number(match[1]) * factor);
|
|
256
|
+
return value > 0 ? value : void 0;
|
|
257
|
+
}
|
|
258
|
+
var identifier = z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
|
|
259
|
+
var durationMs = z.number().int().positive().max(24 * 60 * 60 * 1e3);
|
|
260
|
+
var RESOLVED_VIEW_SYSTEM_COLUMNS = {
|
|
261
|
+
writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
|
|
262
|
+
writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
|
|
263
|
+
};
|
|
264
|
+
var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
|
|
265
|
+
tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
|
|
266
|
+
fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
|
|
267
|
+
writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
|
|
268
|
+
writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
|
|
269
|
+
writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
|
|
270
|
+
};
|
|
271
|
+
var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
|
|
272
|
+
/* @__PURE__ */ new Set([
|
|
273
|
+
...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
|
|
274
|
+
...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
|
|
275
|
+
]);
|
|
276
|
+
var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
|
|
277
|
+
var canonicalFunctionRefSchema = z.object({
|
|
278
|
+
/** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
|
|
279
|
+
kind: z.enum(["managed", "native"]),
|
|
280
|
+
id: z.string().min(1).max(200)
|
|
281
|
+
}).strict();
|
|
282
|
+
var canonicalFetchShapeSchema = z.union([
|
|
283
|
+
z.object({ shape: z.literal("single") }).strict(),
|
|
284
|
+
z.object({ shape: z.literal("batch"), maxKeys: z.number().int().min(2).max(500) }).strict()
|
|
285
|
+
]);
|
|
286
|
+
var canonicalResolvedSourceSchema = z.object({
|
|
287
|
+
name: identifier,
|
|
288
|
+
kind: z.enum(["manual", "etl", "prefetch"]),
|
|
289
|
+
/** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
|
|
290
|
+
table: identifier,
|
|
291
|
+
/** Resolved columns (logical names) this source supplies — the primary key is implicit. */
|
|
292
|
+
columns: z.array(z.string().min(1).max(63)).min(1).max(500),
|
|
293
|
+
/** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
|
|
294
|
+
updatedAt: z.string().min(1).max(63).optional(),
|
|
295
|
+
/** prefetch: resolved column holding the source system's key (may be the primary key). */
|
|
296
|
+
key: z.string().min(1).max(63).optional(),
|
|
297
|
+
fetch: canonicalFetchShapeSchema.optional(),
|
|
298
|
+
function: canonicalFunctionRefSchema.optional(),
|
|
299
|
+
prefetchDefaults: z.object({
|
|
300
|
+
maxStalenessMs: durationMs,
|
|
301
|
+
timeoutMs: durationMs,
|
|
302
|
+
onError: z.enum(["fallback", "fail"])
|
|
303
|
+
}).strict().optional(),
|
|
304
|
+
writeback: z.object({
|
|
305
|
+
columns: z.array(z.string().min(1).max(63)).min(1).max(500),
|
|
306
|
+
defaultMode: z.enum(["sync", "async"]),
|
|
307
|
+
timeoutMs: durationMs,
|
|
308
|
+
/** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
|
|
309
|
+
clearManualOnConfirm: z.boolean()
|
|
310
|
+
}).strict().optional()
|
|
311
|
+
}).strict();
|
|
312
|
+
var canonicalResolveRuleSchema = z.object({
|
|
313
|
+
column: z.string().min(1).max(63),
|
|
314
|
+
comparator: z.enum(COMPARATORS),
|
|
315
|
+
/** Source names, in tie-break order. */
|
|
316
|
+
candidates: z.array(identifier).min(1).max(16),
|
|
317
|
+
/** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
|
|
318
|
+
orderBy: z.string().min(1).max(2e3).optional()
|
|
319
|
+
}).strict();
|
|
320
|
+
var canonicalResolvedSchema = z.object({
|
|
321
|
+
sources: z.array(canonicalResolvedSourceSchema).min(1).max(16),
|
|
322
|
+
/** One rule per resolved (non-pk) column; documents the generated view for tooling. */
|
|
323
|
+
resolve: z.array(canonicalResolveRuleSchema).max(500),
|
|
324
|
+
/** Sources whose tombstone hides the resolved row (v1: the manual source only). */
|
|
325
|
+
deletedBy: z.array(identifier).max(1).default([]),
|
|
326
|
+
/**
|
|
327
|
+
* Materialized: the view reads an indexed internal table (`table`) that the
|
|
328
|
+
* data plane keeps in step with `compute`, the internal view holding the
|
|
329
|
+
* resolution query. Omitted when the view computes on read.
|
|
330
|
+
*/
|
|
331
|
+
materialized: z.object({ table: identifier, compute: identifier }).strict().optional(),
|
|
332
|
+
/** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
|
|
333
|
+
policies: z.object({
|
|
334
|
+
write: policyExpressionSchema.optional(),
|
|
335
|
+
insert: policyExpressionSchema.optional(),
|
|
336
|
+
update: policyExpressionSchema.optional()
|
|
337
|
+
}).strict().default({})
|
|
338
|
+
}).strict();
|
|
339
|
+
function validateResolvedView({ view, tables, views = [] }) {
|
|
340
|
+
const resolved = view.resolved;
|
|
341
|
+
if (!resolved) return [];
|
|
342
|
+
const errors = [];
|
|
343
|
+
const fail3 = (message) => errors.push(`resolved table '${view.view}': ${message}`);
|
|
344
|
+
const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
|
|
345
|
+
if (!pkColumn) {
|
|
346
|
+
fail3("requires exactly one primary-key column");
|
|
347
|
+
return errors;
|
|
348
|
+
}
|
|
349
|
+
const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
|
|
350
|
+
const scope = {
|
|
351
|
+
view,
|
|
352
|
+
resolved,
|
|
353
|
+
pkColumn,
|
|
354
|
+
dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
|
|
355
|
+
sourceByName: /* @__PURE__ */ new Map(),
|
|
356
|
+
fail: fail3
|
|
357
|
+
};
|
|
358
|
+
if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail3("requires exactly one manual source");
|
|
359
|
+
const tableByName = new Map(tables.map((table) => [table.table, table]));
|
|
360
|
+
for (const source of resolved.sources) {
|
|
361
|
+
if (scope.sourceByName.has(source.name)) fail3(`duplicate source '${source.name}'`);
|
|
362
|
+
scope.sourceByName.set(source.name, source);
|
|
363
|
+
const table = tableByName.get(source.table);
|
|
364
|
+
if (!table) fail3(`source '${source.name}' references unknown table '${source.table}'`);
|
|
365
|
+
else validateSource({ scope, source, table });
|
|
366
|
+
}
|
|
367
|
+
validateRules(scope);
|
|
368
|
+
validateViewCells(scope);
|
|
369
|
+
if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
|
|
370
|
+
for (const name of resolved.deletedBy) {
|
|
371
|
+
if (scope.sourceByName.get(name)?.kind !== "manual") fail3(`deletedBy '${name}' must be the manual source`);
|
|
372
|
+
}
|
|
373
|
+
return errors;
|
|
374
|
+
}
|
|
375
|
+
function validateSource({ scope, source, table }) {
|
|
376
|
+
const { fail: fail3 } = scope;
|
|
377
|
+
if (source.kind === "etl" && table.source === void 0) fail3(`etl source '${source.name}' must reference a synced table`);
|
|
378
|
+
if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
|
|
379
|
+
fail3(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
|
|
380
|
+
}
|
|
381
|
+
const hasRev = table.columns.some((column) => column.cell?.part === "rev");
|
|
382
|
+
if (source.kind === "manual" && !hasRev) fail3(`manual source '${source.name}' must carry '__rev' cell columns`);
|
|
383
|
+
if (source.kind === "prefetch" && hasRev) fail3(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
|
|
384
|
+
const byName = new Map(table.columns.map((c) => [c.name, c]));
|
|
385
|
+
validateSourceColumns({ scope, source, byName });
|
|
386
|
+
if (source.kind === "etl") {
|
|
387
|
+
const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
|
|
388
|
+
if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
|
|
389
|
+
fail3(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
validatePrefetchFields({ scope, source });
|
|
393
|
+
}
|
|
394
|
+
function validatePrefetchFields({ scope, source }) {
|
|
395
|
+
const { fail: fail3, pkColumn } = scope;
|
|
396
|
+
if (source.kind !== "prefetch") {
|
|
397
|
+
if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
|
|
398
|
+
fail3(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
|
|
399
|
+
}
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
|
|
403
|
+
fail3(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
|
|
404
|
+
} else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
|
|
405
|
+
fail3(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
|
|
406
|
+
}
|
|
407
|
+
for (const column of source.writeback?.columns ?? []) {
|
|
408
|
+
if (!source.columns.includes(column)) fail3(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function validateSourceColumns({ scope, source, byName }) {
|
|
412
|
+
const { fail: fail3, pkColumn, dataColumns } = scope;
|
|
413
|
+
for (const column of [pkColumn.name, ...source.columns]) {
|
|
414
|
+
const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
|
|
415
|
+
const theirs = byName.get(column);
|
|
416
|
+
if (!own) fail3(`source '${source.name}' supplies unknown column '${column}'`);
|
|
417
|
+
else if (!theirs) fail3(`source '${source.name}' table '${source.table}' has no column '${column}'`);
|
|
418
|
+
else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail3(`column '${column}' has a different type on source '${source.name}'`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail: fail3 }) {
|
|
422
|
+
const ruled = /* @__PURE__ */ new Set();
|
|
423
|
+
for (const rule of resolved.resolve) {
|
|
424
|
+
if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
|
|
425
|
+
fail3(`resolve rule targets unknown column '${rule.column}'`);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (ruled.has(rule.column)) fail3(`column '${rule.column}' has more than one resolve rule`);
|
|
429
|
+
ruled.add(rule.column);
|
|
430
|
+
if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
|
|
431
|
+
fail3(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
|
|
432
|
+
}
|
|
433
|
+
if (new Set(rule.candidates).size !== rule.candidates.length) fail3(`column '${rule.column}' repeats a candidate`);
|
|
434
|
+
for (const candidate of rule.candidates) {
|
|
435
|
+
const source = sourceByName.get(candidate);
|
|
436
|
+
if (!source) fail3(`column '${rule.column}' names unknown candidate source '${candidate}'`);
|
|
437
|
+
else if (!source.columns.includes(rule.column)) fail3(`column '${rule.column}' candidate '${candidate}' does not supply it`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
for (const column of dataColumns.keys()) {
|
|
441
|
+
if (column !== pkColumn.name && !ruled.has(column)) fail3(`column '${column}' has no resolve rule`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
var VIEW_CELL_TYPES = {
|
|
445
|
+
source: "text",
|
|
446
|
+
at: "timestamptz",
|
|
447
|
+
by: "text",
|
|
448
|
+
conflicts: "jsonb"
|
|
449
|
+
};
|
|
450
|
+
function validateViewCells({ view, dataColumns, pkColumn, fail: fail3 }) {
|
|
451
|
+
const expected = /* @__PURE__ */ new Map();
|
|
452
|
+
for (const column of dataColumns.values()) {
|
|
453
|
+
if (column.name === pkColumn.name) continue;
|
|
454
|
+
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
455
|
+
expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
for (const column of view.columns) {
|
|
459
|
+
if (!column.cell) continue;
|
|
460
|
+
const want = expected.get(column.name);
|
|
461
|
+
if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
|
|
462
|
+
fail3(`unexpected cell column '${column.name}'`);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail3(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
|
|
466
|
+
expected.delete(column.name);
|
|
467
|
+
}
|
|
468
|
+
for (const name of expected.keys()) fail3(`missing cell column '${name}'`);
|
|
469
|
+
}
|
|
470
|
+
var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
|
|
471
|
+
function validateMaterialized({ scope, tableByName, views }) {
|
|
472
|
+
const { view, resolved, fail: fail3 } = scope;
|
|
473
|
+
const { table: tableName, compute: computeName } = resolved.materialized;
|
|
474
|
+
const table = tableByName.get(tableName);
|
|
475
|
+
if (!table || table.internal !== true || table.materializes !== view.view) {
|
|
476
|
+
fail3(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
|
|
477
|
+
} else {
|
|
478
|
+
if (columnSignature(table.columns) !== columnSignature(view.columns)) fail3(`materialized table '${tableName}' must have exactly the view's columns`);
|
|
479
|
+
if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail3(`materialized table '${tableName}' must share the view's primary key`);
|
|
480
|
+
}
|
|
481
|
+
const compute = views.find((candidate) => candidate.view === computeName);
|
|
482
|
+
if (!compute || compute.internal !== true) fail3(`compute view '${computeName}' must be an internal view`);
|
|
483
|
+
else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail3(`compute view '${computeName}' must have exactly the view's columns`);
|
|
484
|
+
for (const source of resolved.sources) {
|
|
485
|
+
if (source.kind !== "etl") continue;
|
|
486
|
+
const read = tableByName.get(source.table)?.policies?.read;
|
|
487
|
+
const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
|
|
488
|
+
if (!unconditional) {
|
|
489
|
+
fail3(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// src/canonical/v2.ts
|
|
20
495
|
var CANONICAL_FORMAT = 2;
|
|
21
496
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
22
|
-
var
|
|
497
|
+
var identifier2 = z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
23
498
|
var columnIdentifier = z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
|
|
24
499
|
var COLUMN_TYPES = [
|
|
25
500
|
"uuid",
|
|
@@ -35,14 +510,14 @@ var COLUMN_TYPES = [
|
|
|
35
510
|
];
|
|
36
511
|
var columnTypeSchema = z.enum(COLUMN_TYPES);
|
|
37
512
|
var canonicalEnumSchema = z.object({
|
|
38
|
-
name:
|
|
513
|
+
name: identifier2,
|
|
39
514
|
values: z.array(z.string().min(1).max(63)).min(1).max(100)
|
|
40
515
|
});
|
|
41
516
|
var canonicalColumnSchema = z.object({
|
|
42
517
|
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
43
518
|
name: z.string().min(1).max(63),
|
|
44
519
|
columnName: columnIdentifier,
|
|
45
|
-
type: z.union([columnTypeSchema, z.object({ enum:
|
|
520
|
+
type: z.union([columnTypeSchema, z.object({ enum: identifier2 })]),
|
|
46
521
|
nullable: z.boolean().default(true),
|
|
47
522
|
unique: z.boolean().default(false),
|
|
48
523
|
/** Literal default, or one of the allowlisted SQL functions. */
|
|
@@ -58,7 +533,14 @@ var canonicalColumnSchema = z.object({
|
|
|
58
533
|
/** Migration hints — same semantics as v1. */
|
|
59
534
|
renamedFrom: columnIdentifier.optional(),
|
|
60
535
|
cast: z.string().max(200).optional(),
|
|
61
|
-
deprecated: z.boolean().default(false)
|
|
536
|
+
deprecated: z.boolean().default(false),
|
|
537
|
+
/**
|
|
538
|
+
* Marks a system cell-metadata column (see CELL_PARTS in constants): part
|
|
539
|
+
* `part` of the cell of logical column `of`. Synthesized by authoring,
|
|
540
|
+
* never client-writable, absent from GraphQL scalar fields. Omitted on
|
|
541
|
+
* ordinary columns so their hashes are unchanged.
|
|
542
|
+
*/
|
|
543
|
+
cell: z.object({ of: z.string().min(1).max(63), part: z.enum(CELL_PARTS) }).strict().optional()
|
|
62
544
|
}).superRefine((column, ctx) => {
|
|
63
545
|
if (column.onUpdateFn === void 0) return;
|
|
64
546
|
if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
|
|
@@ -73,36 +555,26 @@ var canonicalLinkSchema = z.object({
|
|
|
73
555
|
name: z.string().min(1).max(63),
|
|
74
556
|
kind: z.enum(["toOne", "toMany", "toPlatformUser"]),
|
|
75
557
|
/** Target table name (namespace-local). Absent for toPlatformUser. */
|
|
76
|
-
target:
|
|
558
|
+
target: identifier2.optional(),
|
|
77
559
|
/** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
|
|
78
560
|
on: columnIdentifier,
|
|
79
561
|
/** Referenced column on the target (default: target's PK / platform user `sub`). */
|
|
80
562
|
references: columnIdentifier.optional(),
|
|
81
563
|
/** enforce: true emits a real FK constraint (same-namespace toOne only). */
|
|
82
564
|
enforce: z.boolean().default(false),
|
|
565
|
+
onDelete: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
566
|
+
onUpdate: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
83
567
|
description: z.string().max(500).optional()
|
|
84
568
|
});
|
|
85
569
|
var canonicalIndexSchema = z.object({
|
|
86
570
|
columns: z.array(columnIdentifier).min(1),
|
|
87
571
|
unique: z.boolean().default(false),
|
|
88
|
-
name:
|
|
89
|
-
});
|
|
90
|
-
var policyVariantSchema = z.object({
|
|
91
|
-
/** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
|
|
92
|
-
when: z.string().min(1).max(1e3),
|
|
93
|
-
/** Exactly one of: allow-all bypass, or a row predicate. */
|
|
94
|
-
allow: z.literal(true).optional(),
|
|
95
|
-
policy: z.string().min(1).max(2e3).optional()
|
|
96
|
-
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
97
|
-
message: "A variant declares exactly one of allow: true or policy"
|
|
572
|
+
name: identifier2.optional()
|
|
98
573
|
});
|
|
99
|
-
var
|
|
100
|
-
|
|
101
|
-
z.
|
|
102
|
-
|
|
103
|
-
variants: z.array(policyVariantSchema).max(10).default([])
|
|
104
|
-
})
|
|
105
|
-
]);
|
|
574
|
+
var canonicalCheckSchema = z.object({
|
|
575
|
+
name: identifier2,
|
|
576
|
+
expression: z.string().min(1).max(2e3)
|
|
577
|
+
}).strict();
|
|
106
578
|
var canonicalPoliciesSchema = z.object({
|
|
107
579
|
read: policyExpressionSchema.optional(),
|
|
108
580
|
write: policyExpressionSchema.optional(),
|
|
@@ -133,7 +605,7 @@ function isSingleViewQuerySql(value) {
|
|
|
133
605
|
return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
|
|
134
606
|
}
|
|
135
607
|
var canonicalActionSchema = z.object({
|
|
136
|
-
name:
|
|
608
|
+
name: identifier2,
|
|
137
609
|
requires: z.string().max(1e3).optional(),
|
|
138
610
|
params: z.record(z.string(), z.object({
|
|
139
611
|
type: columnTypeSchema,
|
|
@@ -145,9 +617,9 @@ var canonicalActionSchema = z.object({
|
|
|
145
617
|
});
|
|
146
618
|
var canonicalCowMutationSchema = z.object({
|
|
147
619
|
kind: z.enum(["override", "revert"]),
|
|
148
|
-
name:
|
|
620
|
+
name: identifier2,
|
|
149
621
|
/** Synced table (same namespace) the overlay copies from / falls back to. */
|
|
150
|
-
synced:
|
|
622
|
+
synced: identifier2,
|
|
151
623
|
/** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
|
|
152
624
|
pk: z.string().min(1).max(63),
|
|
153
625
|
/** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
|
|
@@ -175,23 +647,27 @@ var canonicalCowMutationSchema = z.object({
|
|
|
175
647
|
}
|
|
176
648
|
});
|
|
177
649
|
var canonicalForeignKeySchema = z.object({
|
|
178
|
-
name:
|
|
650
|
+
name: identifier2,
|
|
179
651
|
columns: z.array(columnIdentifier).min(1).max(8),
|
|
180
|
-
target:
|
|
652
|
+
target: identifier2,
|
|
181
653
|
references: z.array(columnIdentifier).min(1).max(8),
|
|
182
|
-
deferrable: z.enum(["initially-immediate", "initially-deferred"]).optional()
|
|
654
|
+
deferrable: z.enum(["initially-immediate", "initially-deferred"]).optional(),
|
|
655
|
+
onDelete: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
656
|
+
onUpdate: z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
|
|
183
657
|
}).strict();
|
|
184
658
|
var canonicalTableSchema = z.object({
|
|
185
|
-
table:
|
|
659
|
+
table: identifier2,
|
|
186
660
|
description: z.string().max(2e3).optional(),
|
|
187
661
|
/** Table-level rename hint (new in v2). */
|
|
188
|
-
renamedFrom:
|
|
662
|
+
renamedFrom: identifier2.optional(),
|
|
189
663
|
/** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
|
|
190
664
|
source: z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
|
|
191
665
|
columns: z.array(canonicalColumnSchema).default([]),
|
|
192
666
|
primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
|
|
193
667
|
links: z.array(canonicalLinkSchema).default([]),
|
|
194
668
|
indexes: z.array(canonicalIndexSchema).default([]),
|
|
669
|
+
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
670
|
+
checks: z.array(canonicalCheckSchema).max(100).optional(),
|
|
195
671
|
policies: canonicalPoliciesSchema.default({}),
|
|
196
672
|
/**
|
|
197
673
|
* v1-conversion carry-over only. v2 has no action surface — the runtime
|
|
@@ -202,22 +678,55 @@ var canonicalTableSchema = z.object({
|
|
|
202
678
|
/** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
|
|
203
679
|
cowMutations: z.array(canonicalCowMutationSchema).optional(),
|
|
204
680
|
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
205
|
-
foreignKeys: z.array(canonicalForeignKeySchema).max(100).optional()
|
|
681
|
+
foreignKeys: z.array(canonicalForeignKeySchema).max(100).optional(),
|
|
682
|
+
/**
|
|
683
|
+
* Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
|
|
684
|
+
* column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
|
|
685
|
+
* on a resolved table's internal sources), listed explicitly in `columns`
|
|
686
|
+
* with a `cell` marker so DDL and migrations treat them as ordinary columns.
|
|
687
|
+
* Omitted when off so existing definitions hash unchanged.
|
|
688
|
+
*/
|
|
689
|
+
cellMetadata: z.literal(true).optional(),
|
|
690
|
+
/**
|
|
691
|
+
* System-owned backing table of a resolved table (its manual or prefetch
|
|
692
|
+
* source). Internal tables get DDL and IR metadata but no GraphQL surface;
|
|
693
|
+
* only the data plane writes them. Omitted when false.
|
|
694
|
+
*/
|
|
695
|
+
internal: z.literal(true).optional(),
|
|
696
|
+
/**
|
|
697
|
+
* The materialized copy of resolved table `materializes` (internal): the
|
|
698
|
+
* same columns as that view, including its cell parts, maintained by the
|
|
699
|
+
* data plane. Omitted otherwise.
|
|
700
|
+
*/
|
|
701
|
+
materializes: identifier2.optional()
|
|
206
702
|
});
|
|
703
|
+
var MAX_VIEW_QUERY = 1e5;
|
|
704
|
+
var MAX_RESOLVED_VIEW_QUERY = 5e6;
|
|
207
705
|
var canonicalViewSchema = z.object({
|
|
208
|
-
view:
|
|
706
|
+
view: identifier2,
|
|
209
707
|
description: z.string().max(2e3).optional(),
|
|
210
708
|
columns: z.array(canonicalColumnSchema).min(1),
|
|
211
709
|
/** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
|
|
212
710
|
primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
|
|
213
711
|
links: z.array(canonicalLinkSchema).default([]),
|
|
214
712
|
policies: z.object({ read: policyExpressionSchema }).strict(),
|
|
215
|
-
query: z.string().min(1).max(
|
|
713
|
+
query: z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
|
|
216
714
|
message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
|
|
217
715
|
}),
|
|
218
716
|
/** Namespace-local logical relation names, sorted for deterministic hashing. */
|
|
219
|
-
dependencies: z.array(
|
|
717
|
+
dependencies: z.array(identifier2).max(500),
|
|
718
|
+
/**
|
|
719
|
+
* Present when this view is a resolved table (defineResolvedTable): the
|
|
720
|
+
* multi-source metadata behind its generated query. Omitted otherwise so
|
|
721
|
+
* existing views hash unchanged.
|
|
722
|
+
*/
|
|
723
|
+
resolved: canonicalResolvedSchema.optional(),
|
|
724
|
+
/** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
|
|
725
|
+
internal: z.literal(true).optional()
|
|
220
726
|
}).superRefine((view, ctx) => {
|
|
727
|
+
if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
|
|
728
|
+
ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
|
|
729
|
+
}
|
|
221
730
|
for (const [index, column] of view.columns.entries()) {
|
|
222
731
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
223
732
|
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
|
|
@@ -232,15 +741,82 @@ var canonicalViewSchema = z.object({
|
|
|
232
741
|
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare migration hints` });
|
|
233
742
|
}
|
|
234
743
|
}
|
|
744
|
+
if (view.resolved === void 0 && view.internal !== true) {
|
|
745
|
+
for (const [index, column] of view.columns.entries()) {
|
|
746
|
+
if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index], message: `only resolved tables carry cell columns ('${column.name}')` });
|
|
747
|
+
}
|
|
748
|
+
}
|
|
235
749
|
for (const [index, link] of view.links.entries()) {
|
|
236
750
|
if (link.enforce) {
|
|
237
751
|
ctx.addIssue({ code: "custom", path: ["links", index, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
|
|
238
752
|
}
|
|
753
|
+
if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
|
|
754
|
+
ctx.addIssue({ code: "custom", path: ["links", index], message: `view link '${link.name}' cannot declare referential actions` });
|
|
755
|
+
}
|
|
239
756
|
}
|
|
240
757
|
});
|
|
758
|
+
var CELL_PART_TYPES = {
|
|
759
|
+
at: "timestamptz",
|
|
760
|
+
by: "text",
|
|
761
|
+
rev: "uuid",
|
|
762
|
+
source: "text",
|
|
763
|
+
conflicts: "jsonb"
|
|
764
|
+
};
|
|
765
|
+
function trackedCellColumns(table) {
|
|
766
|
+
const pk = new Set(table.primaryKey ?? []);
|
|
767
|
+
return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
|
|
768
|
+
}
|
|
769
|
+
var SUFFIXES = Object.values(CELL_PART_SUFFIX);
|
|
770
|
+
function validateCellMetadata(table, index, ctx) {
|
|
771
|
+
const fail3 = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
|
|
772
|
+
const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
|
|
773
|
+
if (table.materializes !== void 0) return;
|
|
774
|
+
if (table.cellMetadata !== true) {
|
|
775
|
+
for (const { position } of cells) fail3(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index, "columns", position]);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
if (table.source !== void 0) {
|
|
779
|
+
fail3(`synced table '${table.table}' cannot track cell metadata`);
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const tracked = trackedCellColumns(table);
|
|
783
|
+
const byName = new Map(tracked.map((column) => [column.name, column]));
|
|
784
|
+
const withRev = cells.some(({ cell }) => cell.part === "rev");
|
|
785
|
+
if (withRev && table.internal !== true) fail3(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
|
|
786
|
+
const seen = /* @__PURE__ */ new Set();
|
|
787
|
+
for (const { column, position, cell } of cells) {
|
|
788
|
+
const path = ["tables", index, "columns", position];
|
|
789
|
+
const owner = byName.get(cell.of);
|
|
790
|
+
if (!owner) {
|
|
791
|
+
fail3(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
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);
|
|
795
|
+
if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
|
|
796
|
+
fail3(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
|
|
797
|
+
}
|
|
798
|
+
if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
|
|
799
|
+
fail3(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
|
|
800
|
+
}
|
|
801
|
+
const key = `${cell.of}:${cell.part}`;
|
|
802
|
+
if (seen.has(key)) fail3(`duplicate cell column '${column.name}'`, path);
|
|
803
|
+
seen.add(key);
|
|
804
|
+
}
|
|
805
|
+
for (const column of tracked) {
|
|
806
|
+
if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
|
|
807
|
+
fail3(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
|
|
808
|
+
}
|
|
809
|
+
if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
|
|
810
|
+
fail3(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
|
|
811
|
+
}
|
|
812
|
+
for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
|
|
813
|
+
if (!seen.has(`${column.name}:${part}`)) fail3(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
241
817
|
var canonicalDefinitionSchema = z.object({
|
|
242
818
|
definition_format: z.literal(CANONICAL_FORMAT),
|
|
243
|
-
namespace:
|
|
819
|
+
namespace: identifier2.max(41),
|
|
244
820
|
description: z.string().max(2e3).optional(),
|
|
245
821
|
enums: z.array(canonicalEnumSchema).default([]),
|
|
246
822
|
tables: z.array(canonicalTableSchema).min(0).max(500),
|
|
@@ -258,6 +834,7 @@ var canonicalDefinitionSchema = z.object({
|
|
|
258
834
|
ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
|
|
259
835
|
}
|
|
260
836
|
seen.add(table.table);
|
|
837
|
+
validateCellMetadata(table, index, ctx);
|
|
261
838
|
if (table.source !== void 0) {
|
|
262
839
|
for (const [columnIndex, column] of table.columns.entries()) {
|
|
263
840
|
if (column.onUpdateFn !== void 0) {
|
|
@@ -277,25 +854,53 @@ var canonicalDefinitionSchema = z.object({
|
|
|
277
854
|
...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
|
|
278
855
|
...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
|
|
279
856
|
]);
|
|
857
|
+
for (const [index, link] of table.links.entries()) {
|
|
858
|
+
if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
|
|
859
|
+
ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "referential actions require an enforced toOne link" });
|
|
860
|
+
}
|
|
861
|
+
if (link.onDelete === "set null" || link.onUpdate === "set null") {
|
|
862
|
+
const source = table.columns.find((column) => column.columnName === link.on);
|
|
863
|
+
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" });
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
for (const [index, check] of (table.checks ?? []).entries()) {
|
|
867
|
+
const fail3 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index], message });
|
|
868
|
+
if (table.source) fail3("synced tables cannot declare checks");
|
|
869
|
+
if (names.has(check.name)) fail3(`duplicate constraint '${check.name}'`);
|
|
870
|
+
names.add(check.name);
|
|
871
|
+
try {
|
|
872
|
+
compileCheckExpression(check.expression, table.columns);
|
|
873
|
+
} catch (error) {
|
|
874
|
+
fail3(error instanceof Error ? error.message : String(error));
|
|
875
|
+
}
|
|
876
|
+
}
|
|
280
877
|
for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
|
|
281
|
-
const
|
|
282
|
-
if (names.has(fk.name))
|
|
878
|
+
const fail3 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
|
|
879
|
+
if (names.has(fk.name)) fail3(`duplicate foreign key '${fk.name}'`);
|
|
283
880
|
names.add(fk.name);
|
|
284
881
|
const target = fkTables.get(fk.target);
|
|
285
882
|
if (table.source || !target || target.source) {
|
|
286
|
-
|
|
883
|
+
fail3("foreign keys must connect owned tables in the same namespace");
|
|
287
884
|
continue;
|
|
288
885
|
}
|
|
289
|
-
if (fk.columns.length !== fk.references.length)
|
|
290
|
-
if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length)
|
|
886
|
+
if (fk.columns.length !== fk.references.length) fail3("foreign key columns and references must have the same number of columns");
|
|
887
|
+
if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail3("foreign key columns must not repeat");
|
|
291
888
|
for (const [position, name] of fk.columns.entries()) {
|
|
292
889
|
const sourceColumn = table.columns.find((column) => column.columnName === name);
|
|
293
890
|
const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
|
|
294
|
-
if (!sourceColumn || !targetColumn)
|
|
295
|
-
else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type))
|
|
891
|
+
if (!sourceColumn || !targetColumn) fail3("foreign key references an unknown column");
|
|
892
|
+
else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail3("foreign key column types must match");
|
|
893
|
+
}
|
|
894
|
+
if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
|
|
895
|
+
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");
|
|
296
896
|
}
|
|
297
897
|
const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index2) => index2.unique).map((index2) => index2.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
|
|
298
|
-
if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column))))
|
|
898
|
+
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");
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
for (const [viewIndex, view] of (definition.views ?? []).entries()) {
|
|
902
|
+
for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
|
|
903
|
+
ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
|
|
299
904
|
}
|
|
300
905
|
}
|
|
301
906
|
const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
@@ -421,8 +1026,12 @@ function canonicalRelations(definition) {
|
|
|
421
1026
|
...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
|
|
422
1027
|
links: view.links,
|
|
423
1028
|
indexes: [],
|
|
424
|
-
|
|
425
|
-
|
|
1029
|
+
// A resolved table's write policies (evaluated against the resolved row)
|
|
1030
|
+
// ride alongside the view's read policy.
|
|
1031
|
+
policies: { read: view.policies.read, ...view.resolved?.policies ?? {} },
|
|
1032
|
+
actions: [],
|
|
1033
|
+
...view.resolved !== void 0 ? { resolved: view.resolved } : {},
|
|
1034
|
+
...view.internal ? { internal: true } : {}
|
|
426
1035
|
}));
|
|
427
1036
|
return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
|
|
428
1037
|
}
|
|
@@ -432,13 +1041,14 @@ function normalizeCanonical(definition) {
|
|
|
432
1041
|
...definition,
|
|
433
1042
|
enums: sortByName(definition.enums),
|
|
434
1043
|
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t) => {
|
|
435
|
-
const { cowMutations, foreignKeys, ...rest } = t;
|
|
1044
|
+
const { cowMutations, foreignKeys, checks, ...rest } = t;
|
|
436
1045
|
return {
|
|
437
1046
|
...rest,
|
|
438
1047
|
links: sortByName(t.links),
|
|
439
1048
|
indexes: [...t.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
|
|
440
1049
|
actions: sortByName(t.actions),
|
|
441
1050
|
...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
|
|
1051
|
+
...checks?.length ? { checks: sortByName(checks) } : {},
|
|
442
1052
|
// Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
|
|
443
1053
|
...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
|
|
444
1054
|
};
|
|
@@ -538,6 +1148,21 @@ var selectionIrSchema = z.lazy(
|
|
|
538
1148
|
() => z.object({
|
|
539
1149
|
typename: z.string(),
|
|
540
1150
|
columns: z.array(z.object({ alias: z.string(), column: z.string() })),
|
|
1151
|
+
cells: z.object({
|
|
1152
|
+
alias: z.string(),
|
|
1153
|
+
typename: z.string(),
|
|
1154
|
+
fields: z.array(z.object({
|
|
1155
|
+
alias: z.string(),
|
|
1156
|
+
column: z.string(),
|
|
1157
|
+
typename: z.string(),
|
|
1158
|
+
parts: z.array(z.object({
|
|
1159
|
+
alias: z.string(),
|
|
1160
|
+
part: z.enum(["source", "at", "by", "conflicts"]),
|
|
1161
|
+
fields: z.array(z.object({ alias: z.string(), field: z.enum(["source", "at", "value"]) })).optional(),
|
|
1162
|
+
typename: z.string().optional()
|
|
1163
|
+
}))
|
|
1164
|
+
}))
|
|
1165
|
+
}).optional(),
|
|
541
1166
|
relations: z.array(z.object({
|
|
542
1167
|
alias: z.string(),
|
|
543
1168
|
link: z.string(),
|
|
@@ -559,6 +1184,11 @@ var selectionIrSchema = z.lazy(
|
|
|
559
1184
|
})
|
|
560
1185
|
);
|
|
561
1186
|
var includeIfSchema = z.object({ variable: z.string(), negated: z.boolean() });
|
|
1187
|
+
var prefetchPlanIrSchema = z.object({
|
|
1188
|
+
source: z.string(),
|
|
1189
|
+
maxStalenessMs: z.number().int().positive().optional(),
|
|
1190
|
+
key: z.union([z.object({ one: valueIrSchema }), z.object({ many: valueIrSchema })])
|
|
1191
|
+
});
|
|
562
1192
|
var selectRootSchema = z.object({
|
|
563
1193
|
kind: z.literal("select"),
|
|
564
1194
|
alias: z.string(),
|
|
@@ -570,7 +1200,8 @@ var selectRootSchema = z.object({
|
|
|
570
1200
|
distinctOn: z.union([z.array(z.string()), z.object({ slot: z.string() })]).optional(),
|
|
571
1201
|
limit: limitIrSchema,
|
|
572
1202
|
offset: z.object({ variable: z.string().optional(), value: z.number().int().optional() }).optional(),
|
|
573
|
-
includeIf: includeIfSchema.optional()
|
|
1203
|
+
includeIf: includeIfSchema.optional(),
|
|
1204
|
+
prefetch: z.array(prefetchPlanIrSchema).optional()
|
|
574
1205
|
});
|
|
575
1206
|
var byPkRootSchema = z.object({
|
|
576
1207
|
kind: z.literal("byPk"),
|
|
@@ -580,7 +1211,8 @@ var byPkRootSchema = z.object({
|
|
|
580
1211
|
pk: z.array(z.object({ column: z.string(), value: valueIrSchema })).min(1),
|
|
581
1212
|
/** Policy predicate — ANDed with the PK match. */
|
|
582
1213
|
where: predIrSchema,
|
|
583
|
-
includeIf: includeIfSchema.optional()
|
|
1214
|
+
includeIf: includeIfSchema.optional(),
|
|
1215
|
+
prefetch: z.array(prefetchPlanIrSchema).optional()
|
|
584
1216
|
});
|
|
585
1217
|
var aggregateRootSchema = z.object({
|
|
586
1218
|
kind: z.literal("aggregate"),
|
|
@@ -638,7 +1270,20 @@ var mutationRootSchema = z.object({
|
|
|
638
1270
|
* coalesce view falls back to synced. Both are gated on a matching synced
|
|
639
1271
|
* row and carry the write policy in `where` like update/delete.
|
|
640
1272
|
*/
|
|
641
|
-
op: z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
|
|
1273
|
+
op: z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert", "resolvedWrite"]),
|
|
1274
|
+
/**
|
|
1275
|
+
* resolvedWrite: a write to a resolved table (`table` is its view). The
|
|
1276
|
+
* executor gates on the resolved row (write policy in `where`), writes only
|
|
1277
|
+
* the touched cells into the manual source, then re-checks `insertCheck`
|
|
1278
|
+
* against the resolved row. `revert` clears `revertColumns` from the manual
|
|
1279
|
+
* source; `delete` tombstones the row there.
|
|
1280
|
+
*/
|
|
1281
|
+
resolvedAction: z.enum(["insert", "update", "delete", "revert"]).optional(),
|
|
1282
|
+
revertColumns: z.array(z.string()).optional(),
|
|
1283
|
+
/** `@writeback(source:, mode:)` directives on this root. */
|
|
1284
|
+
writeback: z.array(z.object({ source: z.string(), mode: z.enum(["sync", "async"]) })).optional(),
|
|
1285
|
+
/** `@localOnly(source:)` — writeback columns deliberately kept local for these sources. */
|
|
1286
|
+
localOnly: z.array(z.string()).optional(),
|
|
642
1287
|
alias: z.string(),
|
|
643
1288
|
table: z.string(),
|
|
644
1289
|
/** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
|
|
@@ -723,6 +1368,43 @@ var compiledOperationSchema = z.object({
|
|
|
723
1368
|
}
|
|
724
1369
|
}
|
|
725
1370
|
});
|
|
1371
|
+
var resolvedSourceIrSchema = z.object({
|
|
1372
|
+
name: z.string(),
|
|
1373
|
+
kind: z.enum(["manual", "etl", "prefetch"]),
|
|
1374
|
+
/** Logical table key of the backing relation. */
|
|
1375
|
+
table: z.string(),
|
|
1376
|
+
columns: z.array(z.string()),
|
|
1377
|
+
key: z.string().optional(),
|
|
1378
|
+
fetch: z.union([
|
|
1379
|
+
z.object({ shape: z.literal("single") }),
|
|
1380
|
+
z.object({ shape: z.literal("batch"), maxKeys: z.number().int().positive() })
|
|
1381
|
+
]).optional(),
|
|
1382
|
+
function: z.object({ kind: z.enum(["managed", "native"]), id: z.string() }).optional(),
|
|
1383
|
+
prefetchDefaults: z.object({
|
|
1384
|
+
maxStalenessMs: z.number().int().positive(),
|
|
1385
|
+
timeoutMs: z.number().int().positive(),
|
|
1386
|
+
onError: z.enum(["fallback", "fail"])
|
|
1387
|
+
}).optional(),
|
|
1388
|
+
writeback: z.object({
|
|
1389
|
+
columns: z.array(z.string()),
|
|
1390
|
+
defaultMode: z.enum(["sync", "async"]),
|
|
1391
|
+
timeoutMs: z.number().int().positive(),
|
|
1392
|
+
clearManualOnConfirm: z.boolean()
|
|
1393
|
+
}).optional()
|
|
1394
|
+
});
|
|
1395
|
+
var resolvedMetaIrSchema = z.object({
|
|
1396
|
+
/** Logical pk column of the resolved view (single-column). */
|
|
1397
|
+
pk: z.string(),
|
|
1398
|
+
/** Source name of the manual source. */
|
|
1399
|
+
manual: z.string(),
|
|
1400
|
+
sources: z.array(resolvedSourceIrSchema),
|
|
1401
|
+
deletedBy: z.array(z.string()),
|
|
1402
|
+
/**
|
|
1403
|
+
* Materialized: the view reads `table`, which the data plane refreshes from
|
|
1404
|
+
* the `compute` view (claims-gated) whenever it writes a source row.
|
|
1405
|
+
*/
|
|
1406
|
+
materialized: z.object({ table: z.string(), compute: z.string() }).optional()
|
|
1407
|
+
});
|
|
726
1408
|
var tableMetaIrSchema = z.object({
|
|
727
1409
|
table: z.string(),
|
|
728
1410
|
schema: z.string(),
|
|
@@ -763,6 +1445,22 @@ var tableMetaIrSchema = z.object({
|
|
|
763
1445
|
* `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
|
|
764
1446
|
*/
|
|
765
1447
|
touchOnUpdate: z.array(z.string()).optional(),
|
|
1448
|
+
/**
|
|
1449
|
+
* Per-cell metadata: logical column → the logical names of its cell-part
|
|
1450
|
+
* columns (which are listed in `columns`, but never filterable, orderable or
|
|
1451
|
+
* writable). Tables stamp `at`/`by` (and a manual source `rev`) on every
|
|
1452
|
+
* write; resolved views expose `source`/`at`/`by`/`conflicts`. Omitted when
|
|
1453
|
+
* the relation tracks no cells.
|
|
1454
|
+
*/
|
|
1455
|
+
cells: z.record(z.string(), z.object({
|
|
1456
|
+
at: z.string().optional(),
|
|
1457
|
+
by: z.string().optional(),
|
|
1458
|
+
rev: z.string().optional(),
|
|
1459
|
+
source: z.string().optional(),
|
|
1460
|
+
conflicts: z.string().optional()
|
|
1461
|
+
}).strict()).optional(),
|
|
1462
|
+
/** Resolved-table view metadata (sources, prefetch/writeback config) for the data plane. */
|
|
1463
|
+
resolved: resolvedMetaIrSchema.optional(),
|
|
766
1464
|
constraints: z.array(z.object({ name: z.string(), columns: z.array(z.string()) })),
|
|
767
1465
|
/** Depth/node caps for dynamic filter trees on this table. */
|
|
768
1466
|
dynamicFilterMaxDepth: z.number().int().positive().default(6),
|
|
@@ -795,6 +1493,115 @@ function normalizeIr(raw) {
|
|
|
795
1493
|
return compiledArtifactSchema.parse(raw);
|
|
796
1494
|
}
|
|
797
1495
|
|
|
1496
|
+
// src/sdl/naming.ts
|
|
1497
|
+
function camelCase(name) {
|
|
1498
|
+
const segments = name.split("_").filter((segment) => segment.length > 0);
|
|
1499
|
+
const first = segments[0];
|
|
1500
|
+
if (first === void 0) return name;
|
|
1501
|
+
const head = first.charAt(0).toLowerCase() + first.slice(1);
|
|
1502
|
+
return head + segments.slice(1).map(capitalizeFirst).join("");
|
|
1503
|
+
}
|
|
1504
|
+
function pascalCase(name) {
|
|
1505
|
+
return capitalizeFirst(camelCase(name));
|
|
1506
|
+
}
|
|
1507
|
+
function capitalizeFirst(segment) {
|
|
1508
|
+
return segment.charAt(0).toUpperCase() + segment.slice(1);
|
|
1509
|
+
}
|
|
1510
|
+
function singularizeWord(word) {
|
|
1511
|
+
if (word.endsWith("ies") && word.length > 3) return `${word.slice(0, -3)}y`;
|
|
1512
|
+
for (const suffix of ["sses", "xes", "ches", "shes"]) {
|
|
1513
|
+
if (word.endsWith(suffix)) return word.slice(0, -2);
|
|
1514
|
+
}
|
|
1515
|
+
if (word.endsWith("ss")) return word;
|
|
1516
|
+
if (word.endsWith("s") && word.length > 1) return word.slice(0, -1);
|
|
1517
|
+
return word;
|
|
1518
|
+
}
|
|
1519
|
+
function typeName(table) {
|
|
1520
|
+
const segments = table.split("_").filter((segment) => segment.length > 0);
|
|
1521
|
+
const last = segments[segments.length - 1];
|
|
1522
|
+
if (last === void 0) return pascalCase(table);
|
|
1523
|
+
return [...segments.slice(0, -1), singularizeWord(last)].map(capitalizeFirst).join("");
|
|
1524
|
+
}
|
|
1525
|
+
function enumTypeName(name) {
|
|
1526
|
+
return pascalCase(name);
|
|
1527
|
+
}
|
|
1528
|
+
var SCALAR_FOR_COLUMN_TYPE = {
|
|
1529
|
+
uuid: "uuid",
|
|
1530
|
+
text: "String",
|
|
1531
|
+
integer: "Int",
|
|
1532
|
+
bigint: "bigint",
|
|
1533
|
+
numeric: "numeric",
|
|
1534
|
+
double: "Float",
|
|
1535
|
+
boolean: "Boolean",
|
|
1536
|
+
timestamptz: "timestamptz",
|
|
1537
|
+
date: "date",
|
|
1538
|
+
jsonb: "jsonb"
|
|
1539
|
+
};
|
|
1540
|
+
function graphqlScalarFor(type) {
|
|
1541
|
+
return SCALAR_FOR_COLUMN_TYPE[type];
|
|
1542
|
+
}
|
|
1543
|
+
var COLUMN_TYPE_BY_SCALAR = new Map([
|
|
1544
|
+
["ID", "text"],
|
|
1545
|
+
...COLUMN_TYPES.map((type) => [graphqlScalarFor(type), type])
|
|
1546
|
+
]);
|
|
1547
|
+
function columnTypeForScalar(scalarName) {
|
|
1548
|
+
return COLUMN_TYPE_BY_SCALAR.get(scalarName);
|
|
1549
|
+
}
|
|
1550
|
+
var VARIABLE_SCALAR_NAMES = [...COLUMN_TYPE_BY_SCALAR.keys()];
|
|
1551
|
+
var PLATFORM_USER_TYPENAME = "PlatformUser";
|
|
1552
|
+
function tableNames(table) {
|
|
1553
|
+
const root = camelCase(table);
|
|
1554
|
+
const rootPascal = pascalCase(table);
|
|
1555
|
+
const type = typeName(table);
|
|
1556
|
+
return {
|
|
1557
|
+
typename: type,
|
|
1558
|
+
selectRoot: root,
|
|
1559
|
+
byPkRoot: `${root}ByPk`,
|
|
1560
|
+
aggregateRoot: `${root}Aggregate`,
|
|
1561
|
+
insertRoot: `insert${rootPascal}`,
|
|
1562
|
+
insertOneRoot: `insert${rootPascal}One`,
|
|
1563
|
+
updateRoot: `update${rootPascal}`,
|
|
1564
|
+
updateByPkRoot: `update${rootPascal}ByPk`,
|
|
1565
|
+
deleteRoot: `delete${rootPascal}`,
|
|
1566
|
+
deleteByPkRoot: `delete${rootPascal}ByPk`,
|
|
1567
|
+
revertByPkRoot: `revert${rootPascal}ByPk`,
|
|
1568
|
+
boolExp: `${type}BoolExp`,
|
|
1569
|
+
orderBy: `${type}OrderBy`,
|
|
1570
|
+
selectColumn: `${type}SelectColumn`,
|
|
1571
|
+
insertInput: `${type}InsertInput`,
|
|
1572
|
+
setInput: `${type}SetInput`,
|
|
1573
|
+
appendInput: `${type}AppendInput`,
|
|
1574
|
+
incInput: `${type}IncInput`,
|
|
1575
|
+
onConflict: `${type}OnConflict`,
|
|
1576
|
+
pkColumnsInput: `${type}PkColumnsInput`,
|
|
1577
|
+
constraintEnum: `${type}Constraint`,
|
|
1578
|
+
updateColumnEnum: `${type}UpdateColumn`,
|
|
1579
|
+
mutationResponse: `${type}MutationResponse`,
|
|
1580
|
+
aggregate: `${type}Aggregate`,
|
|
1581
|
+
aggregateFields: `${type}AggregateFields`,
|
|
1582
|
+
sumFields: `${type}SumFields`,
|
|
1583
|
+
avgFields: `${type}AvgFields`,
|
|
1584
|
+
aggregateBoolExp: `${type}AggregateBoolExp`,
|
|
1585
|
+
aggregateBoolExpCount: `${type}AggregateBoolExpCount`,
|
|
1586
|
+
aggregateGroupsRoot: `${root}AggregateGroups`,
|
|
1587
|
+
groupByColumnEnum: `${type}GroupByColumn`,
|
|
1588
|
+
groupByInput: `${type}GroupBy`,
|
|
1589
|
+
groupKey: `${type}GroupKey`,
|
|
1590
|
+
aggregateGroup: `${type}AggregateGroup`
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
var CELL_TYPENAMES = {
|
|
1594
|
+
/** A plain table's cell: when and by whom it was last written. */
|
|
1595
|
+
stamp: "CellStamp",
|
|
1596
|
+
/** A resolved table's cell: which source won, when, by whom, and newer losers. */
|
|
1597
|
+
resolved: "ResolvedCell",
|
|
1598
|
+
/** A newer candidate that lost to the column's rule. */
|
|
1599
|
+
conflict: "CellConflict"
|
|
1600
|
+
};
|
|
1601
|
+
function cellsTypename(typename) {
|
|
1602
|
+
return `${typename}Cells`;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
798
1605
|
// src/predicate-v2/ast.ts
|
|
799
1606
|
var POLICY_COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
|
|
800
1607
|
function walkPolicy(node, visit2) {
|
|
@@ -831,7 +1638,7 @@ var IDENTIFIER_CHAR = /[a-zA-Z0-9_]/;
|
|
|
831
1638
|
var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
|
|
832
1639
|
var SCALAR_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => !type.endsWith("[]")).map(([key]) => key);
|
|
833
1640
|
var ARRAY_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => type.endsWith("[]")).map(([key]) => key);
|
|
834
|
-
function
|
|
1641
|
+
function tokenize2(source) {
|
|
835
1642
|
const tokens = [];
|
|
836
1643
|
let position = 0;
|
|
837
1644
|
while (position < source.length) {
|
|
@@ -910,7 +1717,7 @@ function readString(source, start) {
|
|
|
910
1717
|
}
|
|
911
1718
|
throw new PolicyParseError("unterminated string literal", start);
|
|
912
1719
|
}
|
|
913
|
-
var
|
|
1720
|
+
var Parser2 = class {
|
|
914
1721
|
constructor(tokens) {
|
|
915
1722
|
this.tokens = tokens;
|
|
916
1723
|
}
|
|
@@ -1084,7 +1891,7 @@ var Parser = class {
|
|
|
1084
1891
|
function parsePolicy(source) {
|
|
1085
1892
|
const trimmed = source.trim();
|
|
1086
1893
|
if (trimmed.length === 0) throw new PolicyParseError("empty policy expression", 0);
|
|
1087
|
-
return { source: trimmed, ast: new
|
|
1894
|
+
return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
|
|
1088
1895
|
}
|
|
1089
1896
|
|
|
1090
1897
|
// src/predicate-v2/compile.ts
|
|
@@ -1484,6 +2291,7 @@ var PolicyInjector = class {
|
|
|
1484
2291
|
injectMutation(root) {
|
|
1485
2292
|
const table = root.table;
|
|
1486
2293
|
const returning = root.returning ? this.injectSelection(root.returning, table) : root.returning;
|
|
2294
|
+
if (root.op === "resolvedWrite") return this.injectResolvedWrite(root, returning);
|
|
1487
2295
|
if (root.op === "insert" || root.op === "insertOne") {
|
|
1488
2296
|
const insertPolicy = this.insert(table);
|
|
1489
2297
|
if (predContainsExists(insertPolicy)) {
|
|
@@ -1525,6 +2333,24 @@ var PolicyInjector = class {
|
|
|
1525
2333
|
}
|
|
1526
2334
|
return { ...root, where, returning };
|
|
1527
2335
|
}
|
|
2336
|
+
/**
|
|
2337
|
+
* Resolved writes are policed against the RESOLVED row, not the sparse
|
|
2338
|
+
* manual row: `where` gates the existing row (update/revert/delete) and
|
|
2339
|
+
* `insertCheck` is re-evaluated as a SELECT over the view after the write,
|
|
2340
|
+
* so link-traversing (exists) policies are representable here.
|
|
2341
|
+
*/
|
|
2342
|
+
injectResolvedWrite(root, returning) {
|
|
2343
|
+
const table = root.table;
|
|
2344
|
+
if (root.resolvedAction === "insert") {
|
|
2345
|
+
const pins = (this.ctx.pins.get(table) ?? []).map(
|
|
2346
|
+
(pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
|
|
2347
|
+
);
|
|
2348
|
+
return { ...root, pinnedColumns: pins, insertCheck: this.insert(table), returning };
|
|
2349
|
+
}
|
|
2350
|
+
const updatePolicy = this.update(table);
|
|
2351
|
+
const where = andPred(updatePolicy, root.where ? this.injectDocPred(root.where, table) : void 0);
|
|
2352
|
+
return root.resolvedAction === "delete" ? { ...root, where, returning } : { ...root, where, insertCheck: updatePolicy, returning };
|
|
2353
|
+
}
|
|
1528
2354
|
};
|
|
1529
2355
|
function collectTableSet(roots, ctx) {
|
|
1530
2356
|
const tables = /* @__PURE__ */ new Set();
|
|
@@ -1584,6 +2410,7 @@ function collectTableSet(roots, ctx) {
|
|
|
1584
2410
|
break;
|
|
1585
2411
|
case "mutation":
|
|
1586
2412
|
if (root.cowSynced !== void 0) tables.add(root.cowSynced);
|
|
2413
|
+
for (const source of ctx.tables[root.table]?.resolved?.sources ?? []) tables.add(source.table);
|
|
1587
2414
|
if (root.where) visitPred(root.where, root.table);
|
|
1588
2415
|
if (root.insertCheck) visitPred(root.insertCheck, root.table);
|
|
1589
2416
|
if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
|
|
@@ -1606,103 +2433,6 @@ function injectPolicies({ roots, ctx }) {
|
|
|
1606
2433
|
return { roots: injected, tableSet: collectTableSet(injected, ctx) };
|
|
1607
2434
|
}
|
|
1608
2435
|
|
|
1609
|
-
// src/sdl/naming.ts
|
|
1610
|
-
function camelCase(name) {
|
|
1611
|
-
const segments = name.split("_").filter((segment) => segment.length > 0);
|
|
1612
|
-
const first = segments[0];
|
|
1613
|
-
if (first === void 0) return name;
|
|
1614
|
-
const head = first.charAt(0).toLowerCase() + first.slice(1);
|
|
1615
|
-
return head + segments.slice(1).map(capitalizeFirst).join("");
|
|
1616
|
-
}
|
|
1617
|
-
function pascalCase(name) {
|
|
1618
|
-
return capitalizeFirst(camelCase(name));
|
|
1619
|
-
}
|
|
1620
|
-
function capitalizeFirst(segment) {
|
|
1621
|
-
return segment.charAt(0).toUpperCase() + segment.slice(1);
|
|
1622
|
-
}
|
|
1623
|
-
function singularizeWord(word) {
|
|
1624
|
-
if (word.endsWith("ies") && word.length > 3) return `${word.slice(0, -3)}y`;
|
|
1625
|
-
for (const suffix of ["sses", "xes", "ches", "shes"]) {
|
|
1626
|
-
if (word.endsWith(suffix)) return word.slice(0, -2);
|
|
1627
|
-
}
|
|
1628
|
-
if (word.endsWith("ss")) return word;
|
|
1629
|
-
if (word.endsWith("s") && word.length > 1) return word.slice(0, -1);
|
|
1630
|
-
return word;
|
|
1631
|
-
}
|
|
1632
|
-
function typeName(table) {
|
|
1633
|
-
const segments = table.split("_").filter((segment) => segment.length > 0);
|
|
1634
|
-
const last = segments[segments.length - 1];
|
|
1635
|
-
if (last === void 0) return pascalCase(table);
|
|
1636
|
-
return [...segments.slice(0, -1), singularizeWord(last)].map(capitalizeFirst).join("");
|
|
1637
|
-
}
|
|
1638
|
-
function enumTypeName(name) {
|
|
1639
|
-
return pascalCase(name);
|
|
1640
|
-
}
|
|
1641
|
-
var SCALAR_FOR_COLUMN_TYPE = {
|
|
1642
|
-
uuid: "uuid",
|
|
1643
|
-
text: "String",
|
|
1644
|
-
integer: "Int",
|
|
1645
|
-
bigint: "bigint",
|
|
1646
|
-
numeric: "numeric",
|
|
1647
|
-
double: "Float",
|
|
1648
|
-
boolean: "Boolean",
|
|
1649
|
-
timestamptz: "timestamptz",
|
|
1650
|
-
date: "date",
|
|
1651
|
-
jsonb: "jsonb"
|
|
1652
|
-
};
|
|
1653
|
-
function graphqlScalarFor(type) {
|
|
1654
|
-
return SCALAR_FOR_COLUMN_TYPE[type];
|
|
1655
|
-
}
|
|
1656
|
-
var COLUMN_TYPE_BY_SCALAR = new Map([
|
|
1657
|
-
["ID", "text"],
|
|
1658
|
-
...COLUMN_TYPES.map((type) => [graphqlScalarFor(type), type])
|
|
1659
|
-
]);
|
|
1660
|
-
function columnTypeForScalar(scalarName) {
|
|
1661
|
-
return COLUMN_TYPE_BY_SCALAR.get(scalarName);
|
|
1662
|
-
}
|
|
1663
|
-
var VARIABLE_SCALAR_NAMES = [...COLUMN_TYPE_BY_SCALAR.keys()];
|
|
1664
|
-
var PLATFORM_USER_TYPENAME = "PlatformUser";
|
|
1665
|
-
function tableNames(table) {
|
|
1666
|
-
const root = camelCase(table);
|
|
1667
|
-
const rootPascal = pascalCase(table);
|
|
1668
|
-
const type = typeName(table);
|
|
1669
|
-
return {
|
|
1670
|
-
typename: type,
|
|
1671
|
-
selectRoot: root,
|
|
1672
|
-
byPkRoot: `${root}ByPk`,
|
|
1673
|
-
aggregateRoot: `${root}Aggregate`,
|
|
1674
|
-
insertRoot: `insert${rootPascal}`,
|
|
1675
|
-
insertOneRoot: `insert${rootPascal}One`,
|
|
1676
|
-
updateRoot: `update${rootPascal}`,
|
|
1677
|
-
updateByPkRoot: `update${rootPascal}ByPk`,
|
|
1678
|
-
deleteRoot: `delete${rootPascal}`,
|
|
1679
|
-
deleteByPkRoot: `delete${rootPascal}ByPk`,
|
|
1680
|
-
boolExp: `${type}BoolExp`,
|
|
1681
|
-
orderBy: `${type}OrderBy`,
|
|
1682
|
-
selectColumn: `${type}SelectColumn`,
|
|
1683
|
-
insertInput: `${type}InsertInput`,
|
|
1684
|
-
setInput: `${type}SetInput`,
|
|
1685
|
-
appendInput: `${type}AppendInput`,
|
|
1686
|
-
incInput: `${type}IncInput`,
|
|
1687
|
-
onConflict: `${type}OnConflict`,
|
|
1688
|
-
pkColumnsInput: `${type}PkColumnsInput`,
|
|
1689
|
-
constraintEnum: `${type}Constraint`,
|
|
1690
|
-
updateColumnEnum: `${type}UpdateColumn`,
|
|
1691
|
-
mutationResponse: `${type}MutationResponse`,
|
|
1692
|
-
aggregate: `${type}Aggregate`,
|
|
1693
|
-
aggregateFields: `${type}AggregateFields`,
|
|
1694
|
-
sumFields: `${type}SumFields`,
|
|
1695
|
-
avgFields: `${type}AvgFields`,
|
|
1696
|
-
aggregateBoolExp: `${type}AggregateBoolExp`,
|
|
1697
|
-
aggregateBoolExpCount: `${type}AggregateBoolExpCount`,
|
|
1698
|
-
aggregateGroupsRoot: `${root}AggregateGroups`,
|
|
1699
|
-
groupByColumnEnum: `${type}GroupByColumn`,
|
|
1700
|
-
groupByInput: `${type}GroupBy`,
|
|
1701
|
-
groupKey: `${type}GroupKey`,
|
|
1702
|
-
aggregateGroup: `${type}AggregateGroup`
|
|
1703
|
-
};
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
2436
|
// src/compiler-gql/naming.ts
|
|
1707
2437
|
var PLATFORM_USER_TABLE = "platformUsers";
|
|
1708
2438
|
function typeNameForTable(table) {
|
|
@@ -1814,7 +2544,8 @@ function resolveColumnEntries(table, enums, diagnostics) {
|
|
|
1814
2544
|
type,
|
|
1815
2545
|
nullable: column.nullable,
|
|
1816
2546
|
deprecated: column.deprecated,
|
|
1817
|
-
onUpdateFn: column.onUpdateFn !== void 0
|
|
2547
|
+
onUpdateFn: column.onUpdateFn !== void 0,
|
|
2548
|
+
...column.cell ? { cell: column.cell } : {}
|
|
1818
2549
|
};
|
|
1819
2550
|
});
|
|
1820
2551
|
}
|
|
@@ -1851,8 +2582,8 @@ function buildCompileContext(rawCanonical) {
|
|
|
1851
2582
|
const pkRefs = table.primaryKey ?? (isView ? [] : ["id"]);
|
|
1852
2583
|
const primaryKey = [];
|
|
1853
2584
|
for (const ref of pkRefs) {
|
|
1854
|
-
const
|
|
1855
|
-
if (!
|
|
2585
|
+
const resolved2 = logicalColumn(entries, ref);
|
|
2586
|
+
if (!resolved2) {
|
|
1856
2587
|
diagnostics.push({
|
|
1857
2588
|
severity: "error",
|
|
1858
2589
|
code: "unknown-pk-column",
|
|
@@ -1861,9 +2592,12 @@ function buildCompileContext(rawCanonical) {
|
|
|
1861
2592
|
});
|
|
1862
2593
|
continue;
|
|
1863
2594
|
}
|
|
1864
|
-
primaryKey.push(
|
|
2595
|
+
primaryKey.push(resolved2);
|
|
1865
2596
|
}
|
|
1866
|
-
const
|
|
2597
|
+
const resolved = isView ? table.resolved : void 0;
|
|
2598
|
+
const data = entries.filter((c) => c.cell === void 0);
|
|
2599
|
+
const cells = cellMap(entries);
|
|
2600
|
+
const active = data.filter((c) => !c.deprecated);
|
|
1867
2601
|
const touchOnUpdate = isView ? [] : active.filter((c) => c.onUpdateFn).map((c) => c.name);
|
|
1868
2602
|
const constraints = [];
|
|
1869
2603
|
if (primaryKey.length > 0) constraints.push({ name: `${table.table}_pkey`, columns: [...primaryKey] });
|
|
@@ -1888,12 +2622,14 @@ function buildCompileContext(rawCanonical) {
|
|
|
1888
2622
|
columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
|
|
1889
2623
|
primaryKey: isView ? primaryKey : primaryKey.length > 0 ? primaryKey : ["id"],
|
|
1890
2624
|
links: [],
|
|
1891
|
-
filterable:
|
|
1892
|
-
orderable:
|
|
1893
|
-
insertable: isView ? [] : active.map((c) => c.name),
|
|
1894
|
-
settable: isView ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
|
|
2625
|
+
filterable: data.map((c) => c.name),
|
|
2626
|
+
orderable: data.filter((c) => c.type !== "jsonb").map((c) => c.name),
|
|
2627
|
+
insertable: resolved ? resolvedWritable(data) : isView ? [] : active.map((c) => c.name),
|
|
2628
|
+
settable: resolved ? resolvedWritable(data).filter((name) => !primaryKey.includes(name)) : isView ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
|
|
1895
2629
|
appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
|
|
1896
2630
|
...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
|
|
2631
|
+
...cells ? { cells } : {},
|
|
2632
|
+
...resolved ? { resolved: resolvedMeta({ resolved, pk: primaryKey[0] }) } : {},
|
|
1897
2633
|
constraints: isView ? [] : constraints,
|
|
1898
2634
|
dynamicFilterMaxDepth: 6,
|
|
1899
2635
|
dynamicFilterMaxNodes: 64
|
|
@@ -1966,7 +2702,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
1966
2702
|
compilePolicyFor("write", writePolicies);
|
|
1967
2703
|
compilePolicyFor("insert", insertPolicies);
|
|
1968
2704
|
compilePolicyFor("update", updatePolicies);
|
|
1969
|
-
if (isView) {
|
|
2705
|
+
if (isView && table.resolved === void 0) {
|
|
1970
2706
|
writePolicies.set(table.table, CONST_FALSE);
|
|
1971
2707
|
insertPolicies.set(table.table, CONST_FALSE);
|
|
1972
2708
|
updatePolicies.set(table.table, CONST_FALSE);
|
|
@@ -2005,6 +2741,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
2005
2741
|
const queryRoots = /* @__PURE__ */ new Map();
|
|
2006
2742
|
const mutationRoots = /* @__PURE__ */ new Map();
|
|
2007
2743
|
for (const table of relations) {
|
|
2744
|
+
if (table.internal === true) continue;
|
|
2008
2745
|
const roots = queryRootNames(table.table);
|
|
2009
2746
|
queryRoots.set(roots.select, { kind: "select", table: table.table });
|
|
2010
2747
|
if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
|
|
@@ -2020,6 +2757,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
2020
2757
|
mutationRoots.set(field, { op, table: table.table });
|
|
2021
2758
|
}
|
|
2022
2759
|
}
|
|
2760
|
+
if (table.resolved !== void 0) registerResolvedRoots({ table, mutationRoots });
|
|
2023
2761
|
}
|
|
2024
2762
|
for (const table of relations) {
|
|
2025
2763
|
for (const mutation of table.cowMutations ?? []) {
|
|
@@ -2090,6 +2828,49 @@ function buildCompileContext(rawCanonical) {
|
|
|
2090
2828
|
diagnostics
|
|
2091
2829
|
};
|
|
2092
2830
|
}
|
|
2831
|
+
function registerResolvedRoots({ table, mutationRoots }) {
|
|
2832
|
+
const names = tableNames(table.table);
|
|
2833
|
+
const canInsert = table.policies.write !== void 0 || table.policies.insert !== void 0;
|
|
2834
|
+
const canUpdate = table.policies.write !== void 0 || table.policies.update !== void 0;
|
|
2835
|
+
const add = (field, action) => mutationRoots.set(field, { op: "resolvedWrite", action, table: table.table });
|
|
2836
|
+
if (canInsert) add(names.insertOneRoot, "insert");
|
|
2837
|
+
if (canUpdate) {
|
|
2838
|
+
add(names.updateByPkRoot, "update");
|
|
2839
|
+
add(names.revertByPkRoot, "revert");
|
|
2840
|
+
if ((table.resolved?.deletedBy.length ?? 0) > 0) add(names.deleteByPkRoot, "delete");
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
function cellMap(entries) {
|
|
2844
|
+
const cells = {};
|
|
2845
|
+
for (const entry of entries) {
|
|
2846
|
+
if (entry.cell) cells[entry.cell.of] = { ...cells[entry.cell.of], [entry.cell.part]: entry.name };
|
|
2847
|
+
}
|
|
2848
|
+
return Object.keys(cells).length > 0 ? cells : void 0;
|
|
2849
|
+
}
|
|
2850
|
+
function resolvedWritable(entries) {
|
|
2851
|
+
const system = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((column) => column.name));
|
|
2852
|
+
return entries.filter((entry) => !entry.deprecated && !system.has(entry.name)).map((entry) => entry.name);
|
|
2853
|
+
}
|
|
2854
|
+
function resolvedMeta({ resolved, pk }) {
|
|
2855
|
+
const manual = resolved.sources.find((source) => source.kind === "manual");
|
|
2856
|
+
return {
|
|
2857
|
+
pk,
|
|
2858
|
+
manual: manual?.name ?? "",
|
|
2859
|
+
deletedBy: [...resolved.deletedBy],
|
|
2860
|
+
...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
|
|
2861
|
+
sources: resolved.sources.map((source) => ({
|
|
2862
|
+
name: source.name,
|
|
2863
|
+
kind: source.kind,
|
|
2864
|
+
table: source.table,
|
|
2865
|
+
columns: [...source.columns],
|
|
2866
|
+
...source.key !== void 0 ? { key: source.key } : {},
|
|
2867
|
+
...source.fetch !== void 0 ? { fetch: source.fetch } : {},
|
|
2868
|
+
...source.function !== void 0 ? { function: source.function } : {},
|
|
2869
|
+
...source.prefetchDefaults !== void 0 ? { prefetchDefaults: source.prefetchDefaults } : {},
|
|
2870
|
+
...source.writeback !== void 0 ? { writeback: source.writeback } : {}
|
|
2871
|
+
}))
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2093
2874
|
function flattenSelectionSet(selectionSet, fragments, stack) {
|
|
2094
2875
|
const selections = [];
|
|
2095
2876
|
for (const selection of selectionSet.selections) {
|
|
@@ -2147,6 +2928,136 @@ function computeOpsHash(opHashes) {
|
|
|
2147
2928
|
const digest = createHash("sha256").update(OPS_HASH_DOMAIN).update([...opHashes].sort().join("\n")).digest("hex");
|
|
2148
2929
|
return `sha256:${digest}`;
|
|
2149
2930
|
}
|
|
2931
|
+
var SOURCE_DIRECTIVES = /* @__PURE__ */ new Set(["prefetch", "writeback", "localOnly"]);
|
|
2932
|
+
var fail = (message, code) => {
|
|
2933
|
+
throw new DocumentCompileError(message, code);
|
|
2934
|
+
};
|
|
2935
|
+
function readArgs(node, name) {
|
|
2936
|
+
return (node.directives ?? []).filter((directive) => directive.name.value === name).map((directive) => {
|
|
2937
|
+
const args = {};
|
|
2938
|
+
for (const arg of directive.arguments ?? []) {
|
|
2939
|
+
const key = arg.name.value;
|
|
2940
|
+
if (key === "source" || key === "maxStaleness") {
|
|
2941
|
+
if (arg.value.kind !== Kind.STRING) fail(`@${name}(${key}:) must be a string literal`, "invalid-source-directive");
|
|
2942
|
+
args[key] = arg.value.value;
|
|
2943
|
+
} else if (key === "mode" && name === "writeback") {
|
|
2944
|
+
if (arg.value.kind !== Kind.ENUM || !["SYNC", "ASYNC"].includes(arg.value.value)) {
|
|
2945
|
+
fail("@writeback(mode:) must be SYNC or ASYNC", "invalid-source-directive");
|
|
2946
|
+
}
|
|
2947
|
+
args.mode = arg.value.value === "SYNC" ? "sync" : "async";
|
|
2948
|
+
} else {
|
|
2949
|
+
fail(`@${name} does not take argument '${key}'`, "invalid-source-directive");
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
if (!args.source) fail(`@${name} needs a source: argument`, "invalid-source-directive");
|
|
2953
|
+
return args;
|
|
2954
|
+
});
|
|
2955
|
+
}
|
|
2956
|
+
function prefetchSourceFor({ resolved, name, directive, table }) {
|
|
2957
|
+
if (!resolved) return fail(`@${directive} is only valid on resolved tables \u2014 '${table}' is not one`, "invalid-source-directive");
|
|
2958
|
+
const source = resolved.sources.find((entry) => entry.name === name);
|
|
2959
|
+
if (!source || source.kind !== "prefetch") {
|
|
2960
|
+
const options = resolved.sources.filter((entry) => entry.kind === "prefetch").map((entry) => entry.name);
|
|
2961
|
+
return fail(`@${directive}(source: "${name}") on '${table}' must name a prefetch source (${options.join(", ") || "none declared"})`, "invalid-source-directive");
|
|
2962
|
+
}
|
|
2963
|
+
return source;
|
|
2964
|
+
}
|
|
2965
|
+
function applyPrefetchDirectives({ node, root, meta }) {
|
|
2966
|
+
const directives = readArgs(node, "prefetch");
|
|
2967
|
+
for (const name of ["writeback", "localOnly"]) {
|
|
2968
|
+
if (readArgs(node, name).length > 0) fail(`@${name} is only valid on resolved-table mutation roots`, "invalid-source-directive");
|
|
2969
|
+
}
|
|
2970
|
+
if (directives.length === 0) return root;
|
|
2971
|
+
if (root.kind !== "select" && root.kind !== "byPk") {
|
|
2972
|
+
return fail(`@prefetch is only valid on select and byPk roots, not '${root.alias}'`, "invalid-source-directive");
|
|
2973
|
+
}
|
|
2974
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2975
|
+
const plans = directives.map((directive) => {
|
|
2976
|
+
const source = prefetchSourceFor({ resolved: meta.resolved, name: directive.source, directive: "prefetch", table: root.table });
|
|
2977
|
+
if (seen.has(source.name)) fail(`@prefetch(source: "${source.name}") appears twice on '${root.alias}'`, "invalid-source-directive");
|
|
2978
|
+
seen.add(source.name);
|
|
2979
|
+
const maxStalenessMs = directive.maxStaleness === void 0 ? void 0 : parseDurationMs(directive.maxStaleness);
|
|
2980
|
+
if (directive.maxStaleness !== void 0 && maxStalenessMs === void 0) {
|
|
2981
|
+
fail(`@prefetch(maxStaleness: "${directive.maxStaleness}") must look like '500ms', '30s' or '5m'`, "invalid-source-directive");
|
|
2982
|
+
}
|
|
2983
|
+
const key = root.kind === "byPk" ? { one: root.pk[0].value } : selectKey({ where: root.where, pk: meta.resolved.pk, source, alias: root.alias });
|
|
2984
|
+
return { source: source.name, ...maxStalenessMs !== void 0 ? { maxStalenessMs } : {}, key };
|
|
2985
|
+
});
|
|
2986
|
+
return { ...root, prefetch: plans };
|
|
2987
|
+
}
|
|
2988
|
+
function selectKey({ where, pk, source, alias }) {
|
|
2989
|
+
const leaves = where.kind === "and" ? where.operands : [where];
|
|
2990
|
+
const pinned = leaves.find((leaf) => leaf.kind === "cmp" && leaf.column === pk && (leaf.op === "eq" || leaf.op === "in"));
|
|
2991
|
+
const shape = source.fetch?.shape ?? "single";
|
|
2992
|
+
if (!pinned || pinned.kind !== "cmp") {
|
|
2993
|
+
return fail(
|
|
2994
|
+
`@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)`,
|
|
2995
|
+
"prefetch-shape-mismatch"
|
|
2996
|
+
);
|
|
2997
|
+
}
|
|
2998
|
+
if (pinned.op === "in" && shape !== "batch") {
|
|
2999
|
+
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");
|
|
3000
|
+
}
|
|
3001
|
+
return pinned.op === "eq" ? { one: pinned.value } : { many: pinned.value };
|
|
3002
|
+
}
|
|
3003
|
+
function applyWritebackDirectives({ node, root, meta }) {
|
|
3004
|
+
if (readArgs(node, "prefetch").length > 0) fail("@prefetch is only valid on query roots", "invalid-source-directive");
|
|
3005
|
+
const writebacks = readArgs(node, "writeback");
|
|
3006
|
+
const localOnly = readArgs(node, "localOnly");
|
|
3007
|
+
if (root.kind !== "mutation" || root.op !== "resolvedWrite" || !meta.resolved) {
|
|
3008
|
+
if (writebacks.length + localOnly.length > 0) fail("@writeback/@localOnly are only valid on resolved-table mutation roots", "invalid-source-directive");
|
|
3009
|
+
return root;
|
|
3010
|
+
}
|
|
3011
|
+
const resolved = meta.resolved;
|
|
3012
|
+
const declared = /* @__PURE__ */ new Map();
|
|
3013
|
+
const bindings = [];
|
|
3014
|
+
for (const directive of writebacks) {
|
|
3015
|
+
const source = prefetchSourceFor({ resolved, name: directive.source, directive: "writeback", table: root.table });
|
|
3016
|
+
if (!source.writeback) fail(`@writeback(source: "${source.name}"): source '${source.name}' declares no writeback columns`, "invalid-source-directive");
|
|
3017
|
+
if (root.resolvedAction !== "update") {
|
|
3018
|
+
fail(`@writeback on '${root.alias}': only update roots write back in v1 (use @localOnly for ${root.resolvedAction ?? "this"} roots)`, "writeback-shape-mismatch");
|
|
3019
|
+
}
|
|
3020
|
+
if (declared.has(source.name)) fail(`source '${source.name}' is declared twice on '${root.alias}'`, "invalid-source-directive");
|
|
3021
|
+
declared.set(source.name, "writeback");
|
|
3022
|
+
bindings.push({ source: source.name, mode: directive.mode ?? source.writeback.defaultMode });
|
|
3023
|
+
}
|
|
3024
|
+
for (const directive of localOnly) {
|
|
3025
|
+
const source = prefetchSourceFor({ resolved, name: directive.source, directive: "localOnly", table: root.table });
|
|
3026
|
+
if (declared.has(source.name)) fail(`source '${source.name}' is declared twice on '${root.alias}'`, "invalid-source-directive");
|
|
3027
|
+
declared.set(source.name, "localOnly");
|
|
3028
|
+
}
|
|
3029
|
+
assertWritebacksDeclared({ root, meta, resolved, declared });
|
|
3030
|
+
return {
|
|
3031
|
+
...root,
|
|
3032
|
+
...bindings.length > 0 ? { writeback: bindings } : {},
|
|
3033
|
+
...localOnly.length > 0 ? { localOnly: localOnly.map((directive) => directive.source) } : {}
|
|
3034
|
+
};
|
|
3035
|
+
}
|
|
3036
|
+
function assertWritebacksDeclared({ root, meta, resolved, declared }) {
|
|
3037
|
+
const written = writtenColumns({ root, meta });
|
|
3038
|
+
for (const source of resolved.sources) {
|
|
3039
|
+
const columns = source.writeback?.columns.filter((column) => written.has(column)) ?? [];
|
|
3040
|
+
if (columns.length > 0 && !declared.has(source.name)) {
|
|
3041
|
+
fail(
|
|
3042
|
+
`'${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`,
|
|
3043
|
+
"writeback-undeclared"
|
|
3044
|
+
);
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
function writtenColumns({ root, meta }) {
|
|
3049
|
+
if (root.resolvedAction === "update") {
|
|
3050
|
+
if (!root.set || "slot" in root.set) return new Set(meta.settable);
|
|
3051
|
+
return new Set(root.set.entries.map((entry) => entry.column));
|
|
3052
|
+
}
|
|
3053
|
+
if (root.resolvedAction === "insert") {
|
|
3054
|
+
if (!root.objects || "slot" in root.objects) return new Set(meta.insertable);
|
|
3055
|
+
return new Set(root.objects.rows.flatMap((row) => row.map((entry) => entry.column)));
|
|
3056
|
+
}
|
|
3057
|
+
return /* @__PURE__ */ new Set();
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
// src/compiler-gql/lower.ts
|
|
2150
3061
|
var DEFAULT_LIMIT = 100;
|
|
2151
3062
|
var MAX_LIMIT = 1e3;
|
|
2152
3063
|
var MAX_GROUPS = 1e3;
|
|
@@ -2184,7 +3095,7 @@ var ORDER_DIRECTIONS = {
|
|
|
2184
3095
|
DESC_NULLS_LAST: { direction: "desc", nulls: "last" }
|
|
2185
3096
|
};
|
|
2186
3097
|
var NUMERIC_TYPES = /* @__PURE__ */ new Set(["integer", "bigint", "numeric", "double"]);
|
|
2187
|
-
function
|
|
3098
|
+
function fail2(message, code, candidates = []) {
|
|
2188
3099
|
throw new DocumentCompileError(message, code, candidates);
|
|
2189
3100
|
}
|
|
2190
3101
|
function argsByName(field) {
|
|
@@ -2192,16 +3103,16 @@ function argsByName(field) {
|
|
|
2192
3103
|
}
|
|
2193
3104
|
function* aggregateBlockFields(selectionSet) {
|
|
2194
3105
|
for (const node of selectionSet.selections) {
|
|
2195
|
-
if (node.kind !== Kind.FIELD)
|
|
3106
|
+
if (node.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
2196
3107
|
if (node.name.value === "__typename") continue;
|
|
2197
|
-
if (node.alias)
|
|
3108
|
+
if (node.alias) fail2(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
|
|
2198
3109
|
yield node;
|
|
2199
3110
|
}
|
|
2200
3111
|
}
|
|
2201
3112
|
function assertKnownArgs(field, allowed) {
|
|
2202
3113
|
for (const arg of field.arguments ?? []) {
|
|
2203
3114
|
if (!allowed.includes(arg.name.value)) {
|
|
2204
|
-
|
|
3115
|
+
fail2(`argument '${arg.name.value}' on '${field.name.value}' is not supported in v2.0`, "unsupported-argument", allowed);
|
|
2205
3116
|
}
|
|
2206
3117
|
}
|
|
2207
3118
|
}
|
|
@@ -2214,8 +3125,11 @@ var OperationLowerer = class {
|
|
|
2214
3125
|
meta(table) {
|
|
2215
3126
|
return this.ctx.tables[table];
|
|
2216
3127
|
}
|
|
3128
|
+
/** A data column — cell-metadata columns are reachable only through `cells { … }`. */
|
|
2217
3129
|
column(table, name) {
|
|
2218
|
-
|
|
3130
|
+
const meta = this.meta(table);
|
|
3131
|
+
const column = meta.columns.find((c) => c.name === name);
|
|
3132
|
+
return column && !isCellColumn(meta, name) ? column : void 0;
|
|
2219
3133
|
}
|
|
2220
3134
|
link(table, name) {
|
|
2221
3135
|
return this.meta(table).links.find((l) => l.name === name);
|
|
@@ -2223,7 +3137,8 @@ var OperationLowerer = class {
|
|
|
2223
3137
|
fieldCandidates(table) {
|
|
2224
3138
|
const meta = this.meta(table);
|
|
2225
3139
|
return [
|
|
2226
|
-
...meta.columns.map((c) => c.name),
|
|
3140
|
+
...meta.columns.filter((c) => !isCellColumn(meta, c.name)).map((c) => c.name),
|
|
3141
|
+
...meta.cells ? [CELLS_FIELD] : [],
|
|
2227
3142
|
...meta.links.map((l) => l.name),
|
|
2228
3143
|
...meta.links.filter((l) => l.kind === "toMany").map((l) => `${l.name}Aggregate`)
|
|
2229
3144
|
];
|
|
@@ -2232,7 +3147,7 @@ var OperationLowerer = class {
|
|
|
2232
3147
|
slot(name, input, table) {
|
|
2233
3148
|
const variable = this.variables.get(name);
|
|
2234
3149
|
if (!variable || variable.type.kind !== "inputRef" || variable.type.input !== input || variable.type.table !== table) {
|
|
2235
|
-
|
|
3150
|
+
fail2(`variable $${name} must be declared as the ${input} input type for table '${table}'`, "slot-mismatch");
|
|
2236
3151
|
}
|
|
2237
3152
|
return name;
|
|
2238
3153
|
}
|
|
@@ -2257,14 +3172,14 @@ var OperationLowerer = class {
|
|
|
2257
3172
|
const items = node.values.map((item) => {
|
|
2258
3173
|
const lowered = this.lowerValue(item);
|
|
2259
3174
|
if (lowered.kind !== "literal" || Array.isArray(lowered.value)) {
|
|
2260
|
-
|
|
3175
|
+
fail2("list literals must contain scalar literals only \u2014 pass the whole list as a variable instead", "unsupported-literal");
|
|
2261
3176
|
}
|
|
2262
3177
|
return lowered.value;
|
|
2263
3178
|
});
|
|
2264
3179
|
return { kind: "literal", value: items };
|
|
2265
3180
|
}
|
|
2266
3181
|
case Kind.OBJECT:
|
|
2267
|
-
|
|
3182
|
+
fail2("literal JSON objects are not supported in documents \u2014 pass the value via a variable", "unsupported-literal");
|
|
2268
3183
|
}
|
|
2269
3184
|
}
|
|
2270
3185
|
// -------------------------------------------------------------------------
|
|
@@ -2274,7 +3189,7 @@ var OperationLowerer = class {
|
|
|
2274
3189
|
if (node.kind === Kind.VARIABLE) {
|
|
2275
3190
|
return { kind: "dynamicFilter", slot: this.slot(node.name.value, "boolExp", table) };
|
|
2276
3191
|
}
|
|
2277
|
-
if (node.kind !== Kind.OBJECT)
|
|
3192
|
+
if (node.kind !== Kind.OBJECT) fail2(`a boolean expression on '${table}' must be an object or a variable`, "invalid-boolexp");
|
|
2278
3193
|
const operands = [];
|
|
2279
3194
|
for (const field of node.fields) {
|
|
2280
3195
|
const key = field.name.value;
|
|
@@ -2305,45 +3220,45 @@ var OperationLowerer = class {
|
|
|
2305
3220
|
continue;
|
|
2306
3221
|
}
|
|
2307
3222
|
}
|
|
2308
|
-
|
|
3223
|
+
fail2(`unknown field '${key}' in a boolean expression on '${table}'`, "unknown-column", this.fieldCandidates(table));
|
|
2309
3224
|
}
|
|
2310
3225
|
if (operands.length === 0) return CONST_TRUE;
|
|
2311
3226
|
return operands.length === 1 ? operands[0] : { kind: "and", operands };
|
|
2312
3227
|
}
|
|
2313
3228
|
lowerComparison(node, column, table) {
|
|
2314
3229
|
if (node.kind !== Kind.OBJECT) {
|
|
2315
|
-
|
|
3230
|
+
fail2(`comparison on '${table}.${column}' must be an object like { _eq: \u2026 }`, "invalid-comparison", Object.keys(BOOLEXP_OPS));
|
|
2316
3231
|
}
|
|
2317
3232
|
return node.fields.map((field) => {
|
|
2318
3233
|
const op = BOOLEXP_OPS[field.name.value];
|
|
2319
3234
|
if (!op) {
|
|
2320
|
-
|
|
3235
|
+
fail2(`unknown comparison operator '${field.name.value}' on '${table}.${column}'`, "unknown-operator", Object.keys(BOOLEXP_OPS));
|
|
2321
3236
|
}
|
|
2322
3237
|
return { kind: "cmp", op, column, value: this.lowerValue(field.value) };
|
|
2323
3238
|
});
|
|
2324
3239
|
}
|
|
2325
3240
|
/** Hasura aggregation predicate: `{ count: { predicate: { _gt: 2 }, filter: {…} } }`. */
|
|
2326
3241
|
lowerAggPredicate(node, link, target) {
|
|
2327
|
-
if (node.kind !== Kind.OBJECT)
|
|
3242
|
+
if (node.kind !== Kind.OBJECT) fail2(`aggregate predicate through '${link}' must be an object`, "invalid-agg-predicate");
|
|
2328
3243
|
const countField = node.fields.find((f) => f.name.value === "count");
|
|
2329
3244
|
if (!countField || node.fields.length !== 1) {
|
|
2330
|
-
|
|
3245
|
+
fail2(`aggregate predicates support { count: \u2026 } only in v2.0 (through '${link}')`, "invalid-agg-predicate", ["count"]);
|
|
2331
3246
|
}
|
|
2332
|
-
if (countField.value.kind !== Kind.OBJECT)
|
|
3247
|
+
if (countField.value.kind !== Kind.OBJECT) fail2(`count predicate through '${link}' must be an object`, "invalid-agg-predicate");
|
|
2333
3248
|
let predicate;
|
|
2334
3249
|
let where;
|
|
2335
3250
|
for (const field of countField.value.fields) {
|
|
2336
3251
|
if (field.name.value === "predicate") {
|
|
2337
3252
|
if (field.value.kind !== Kind.OBJECT || field.value.fields.length !== 1) {
|
|
2338
|
-
|
|
3253
|
+
fail2(`count predicate through '${link}' needs exactly one operator`, "invalid-agg-predicate", [...AGG_PREDICATE_OPS]);
|
|
2339
3254
|
}
|
|
2340
3255
|
const opField = field.value.fields[0];
|
|
2341
3256
|
const opName = opField.name.value;
|
|
2342
3257
|
if (!AGG_PREDICATE_OPS.includes(opName)) {
|
|
2343
|
-
|
|
3258
|
+
fail2(`aggregate count predicates support ${AGG_PREDICATE_OPS.join("/")} only, got '${opName}'`, "invalid-agg-predicate");
|
|
2344
3259
|
}
|
|
2345
3260
|
if (opField.value.kind !== Kind.INT) {
|
|
2346
|
-
|
|
3261
|
+
fail2(`aggregate count predicate value must be an integer literal`, "invalid-agg-predicate");
|
|
2347
3262
|
}
|
|
2348
3263
|
predicate = { op: opName.slice(1), value: Number(opField.value.value) };
|
|
2349
3264
|
continue;
|
|
@@ -2352,9 +3267,9 @@ var OperationLowerer = class {
|
|
|
2352
3267
|
where = this.lowerBoolExp(field.value, target);
|
|
2353
3268
|
continue;
|
|
2354
3269
|
}
|
|
2355
|
-
|
|
3270
|
+
fail2(`unknown key '${field.name.value}' in a count predicate`, "invalid-agg-predicate", ["predicate", "filter"]);
|
|
2356
3271
|
}
|
|
2357
|
-
if (!predicate)
|
|
3272
|
+
if (!predicate) fail2(`count predicate through '${link}' needs a { predicate: { _gt: n } }`, "invalid-agg-predicate");
|
|
2358
3273
|
return { kind: "aggPredicate", link, fn: "count", op: predicate.op, value: predicate.value, ...where ? { where } : {} };
|
|
2359
3274
|
}
|
|
2360
3275
|
lowerOrderBy(node, table) {
|
|
@@ -2364,7 +3279,7 @@ var OperationLowerer = class {
|
|
|
2364
3279
|
const items = node.kind === Kind.LIST ? node.values : [node];
|
|
2365
3280
|
const lowered = [];
|
|
2366
3281
|
for (const item of items) {
|
|
2367
|
-
if (item.kind !== Kind.OBJECT)
|
|
3282
|
+
if (item.kind !== Kind.OBJECT) fail2(`orderBy entries on '${table}' must be objects`, "invalid-orderby");
|
|
2368
3283
|
for (const field of item.fields) {
|
|
2369
3284
|
const key = field.name.value;
|
|
2370
3285
|
const column = this.column(table, key);
|
|
@@ -2375,25 +3290,25 @@ var OperationLowerer = class {
|
|
|
2375
3290
|
const link = this.link(table, key);
|
|
2376
3291
|
if (link && link.kind !== "toMany") {
|
|
2377
3292
|
if (field.value.kind !== Kind.OBJECT || field.value.fields.length !== 1) {
|
|
2378
|
-
|
|
3293
|
+
fail2(`orderBy through '${key}' needs exactly one target column`, "invalid-orderby");
|
|
2379
3294
|
}
|
|
2380
3295
|
const inner = field.value.fields[0];
|
|
2381
3296
|
const targetColumn = this.column(link.target, inner.name.value);
|
|
2382
3297
|
if (!targetColumn) {
|
|
2383
|
-
|
|
3298
|
+
fail2(`unknown column '${inner.name.value}' on '${link.target}' in orderBy`, "unknown-column", this.meta(link.target).columns.map((c) => c.name));
|
|
2384
3299
|
}
|
|
2385
3300
|
lowered.push({ kind: "column", column: inner.name.value, link: key, ...this.lowerDirection(inner.value, link.target, inner.name.value) });
|
|
2386
3301
|
continue;
|
|
2387
3302
|
}
|
|
2388
|
-
|
|
3303
|
+
fail2(`unknown orderBy key '${key}' on '${table}' (to-one links only)`, "invalid-orderby", this.fieldCandidates(table));
|
|
2389
3304
|
}
|
|
2390
3305
|
}
|
|
2391
3306
|
return lowered;
|
|
2392
3307
|
}
|
|
2393
3308
|
lowerDirection(node, table, column) {
|
|
2394
|
-
if (node.kind !== Kind.ENUM)
|
|
3309
|
+
if (node.kind !== Kind.ENUM) fail2(`orderBy direction on '${table}.${column}' must be a direction enum`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
|
|
2395
3310
|
const direction = ORDER_DIRECTIONS[node.value];
|
|
2396
|
-
if (!direction)
|
|
3311
|
+
if (!direction) fail2(`unknown orderBy direction '${node.value}'`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
|
|
2397
3312
|
return direction.nulls ? { direction: direction.direction, nulls: direction.nulls } : { direction: direction.direction };
|
|
2398
3313
|
}
|
|
2399
3314
|
lowerDistinctOn(node, table) {
|
|
@@ -2402,9 +3317,9 @@ var OperationLowerer = class {
|
|
|
2402
3317
|
}
|
|
2403
3318
|
const items = node.kind === Kind.LIST ? node.values : [node];
|
|
2404
3319
|
return items.map((item) => {
|
|
2405
|
-
if (item.kind !== Kind.ENUM)
|
|
3320
|
+
if (item.kind !== Kind.ENUM) fail2(`distinctOn entries on '${table}' must be select-column enum values`, "invalid-distinct-on");
|
|
2406
3321
|
if (!this.column(table, item.value)) {
|
|
2407
|
-
|
|
3322
|
+
fail2(`unknown distinctOn column '${item.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
|
|
2408
3323
|
}
|
|
2409
3324
|
return item.value;
|
|
2410
3325
|
});
|
|
@@ -2415,16 +3330,16 @@ var OperationLowerer = class {
|
|
|
2415
3330
|
if (node.kind === Kind.VARIABLE) return { ...base, variable: node.name.value };
|
|
2416
3331
|
if (node.kind === Kind.INT) {
|
|
2417
3332
|
const value = Number(node.value);
|
|
2418
|
-
if (value < 1)
|
|
3333
|
+
if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
|
|
2419
3334
|
return { ...base, value: Math.min(value, MAX_LIMIT) };
|
|
2420
3335
|
}
|
|
2421
|
-
|
|
3336
|
+
fail2("limit must be an integer literal or a variable", "invalid-limit");
|
|
2422
3337
|
}
|
|
2423
3338
|
lowerOffset(node) {
|
|
2424
3339
|
if (!node) return void 0;
|
|
2425
3340
|
if (node.kind === Kind.VARIABLE) return { variable: node.name.value };
|
|
2426
3341
|
if (node.kind === Kind.INT) return { value: Number(node.value) };
|
|
2427
|
-
|
|
3342
|
+
fail2("offset must be an integer literal or a variable", "invalid-offset");
|
|
2428
3343
|
}
|
|
2429
3344
|
// -------------------------------------------------------------------------
|
|
2430
3345
|
// Selections
|
|
@@ -2433,9 +3348,9 @@ var OperationLowerer = class {
|
|
|
2433
3348
|
const meta = this.meta(table);
|
|
2434
3349
|
const selection = { typename: meta.typename, columns: [], relations: [], aggregates: [] };
|
|
2435
3350
|
for (const node of selectionSet.selections) {
|
|
2436
|
-
if (node.kind !== Kind.FIELD)
|
|
3351
|
+
if (node.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
2437
3352
|
if (node.directives && node.directives.length > 0) {
|
|
2438
|
-
|
|
3353
|
+
fail2(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
|
|
2439
3354
|
}
|
|
2440
3355
|
const name = node.name.value;
|
|
2441
3356
|
if (name === "__typename") continue;
|
|
@@ -2443,14 +3358,19 @@ var OperationLowerer = class {
|
|
|
2443
3358
|
const column = this.column(table, name);
|
|
2444
3359
|
if (column) {
|
|
2445
3360
|
if (node.arguments !== void 0 && node.arguments.length > 0 || node.selectionSet !== void 0) {
|
|
2446
|
-
|
|
3361
|
+
fail2(`column '${table}.${name}' takes no arguments or sub-selection`, "invalid-selection");
|
|
2447
3362
|
}
|
|
2448
3363
|
selection.columns.push({ alias, column: name });
|
|
2449
3364
|
continue;
|
|
2450
3365
|
}
|
|
3366
|
+
if (name === CELLS_FIELD && meta.cells) {
|
|
3367
|
+
if (selection.cells) fail2(`'${CELLS_FIELD}' may be selected once per object`, "invalid-selection");
|
|
3368
|
+
selection.cells = this.lowerCells(node, table, alias);
|
|
3369
|
+
continue;
|
|
3370
|
+
}
|
|
2451
3371
|
const link = this.link(table, name);
|
|
2452
3372
|
if (link) {
|
|
2453
|
-
if (!node.selectionSet)
|
|
3373
|
+
if (!node.selectionSet) fail2(`link '${table}.${name}' needs a selection set`, "invalid-selection");
|
|
2454
3374
|
const relation = {
|
|
2455
3375
|
alias,
|
|
2456
3376
|
link: name,
|
|
@@ -2469,7 +3389,7 @@ var OperationLowerer = class {
|
|
|
2469
3389
|
if (offset) relation.offset = offset;
|
|
2470
3390
|
}
|
|
2471
3391
|
} else if (node.arguments && node.arguments.length > 0) {
|
|
2472
|
-
|
|
3392
|
+
fail2(`to-one link '${table}.${name}' takes no arguments`, "invalid-selection");
|
|
2473
3393
|
}
|
|
2474
3394
|
selection.relations.push(relation);
|
|
2475
3395
|
continue;
|
|
@@ -2477,7 +3397,7 @@ var OperationLowerer = class {
|
|
|
2477
3397
|
if (name.endsWith("Aggregate")) {
|
|
2478
3398
|
const aggLink = this.link(table, name.slice(0, -"Aggregate".length));
|
|
2479
3399
|
if (aggLink && aggLink.kind === "toMany") {
|
|
2480
|
-
if (!node.selectionSet)
|
|
3400
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
2481
3401
|
assertKnownArgs(node, ["where"]);
|
|
2482
3402
|
const whereArg = argsByName(node).get("where");
|
|
2483
3403
|
const aggregate = this.lowerAggregateSelection(node.selectionSet, aggLink.target);
|
|
@@ -2490,10 +3410,57 @@ var OperationLowerer = class {
|
|
|
2490
3410
|
continue;
|
|
2491
3411
|
}
|
|
2492
3412
|
}
|
|
2493
|
-
|
|
3413
|
+
fail2(`unknown field '${name}' on '${table}'`, "unknown-field", this.fieldCandidates(table));
|
|
2494
3414
|
}
|
|
2495
3415
|
return selection;
|
|
2496
3416
|
}
|
|
3417
|
+
/**
|
|
3418
|
+
* `cells { <column> { at by … } }` — typed per-cell metadata. Plain tables
|
|
3419
|
+
* offer `at`/`by`; resolved tables add the winning `source` and newer
|
|
3420
|
+
* `conflicts { source at value }`.
|
|
3421
|
+
*/
|
|
3422
|
+
lowerCells(node, table, alias) {
|
|
3423
|
+
const meta = this.meta(table);
|
|
3424
|
+
const cells = meta.cells ?? {};
|
|
3425
|
+
if (node.arguments && node.arguments.length > 0) fail2(`'${CELLS_FIELD}' takes no arguments`, "invalid-selection");
|
|
3426
|
+
if (!node.selectionSet) fail2(`'${CELLS_FIELD}' needs a selection set`, "invalid-selection");
|
|
3427
|
+
const fields = [];
|
|
3428
|
+
for (const cellNode of fieldNodes(node.selectionSet)) {
|
|
3429
|
+
const column = cellNode.name.value;
|
|
3430
|
+
const parts = cells[column];
|
|
3431
|
+
if (!parts || parts.at === void 0 && parts.source === void 0) {
|
|
3432
|
+
fail2(`unknown cell '${column}' on '${table}'`, "unknown-field", Object.keys(cells));
|
|
3433
|
+
}
|
|
3434
|
+
if (!cellNode.selectionSet) fail2(`cell '${column}' needs a selection set`, "invalid-selection");
|
|
3435
|
+
const resolved = parts.source !== void 0;
|
|
3436
|
+
const allowed = resolved ? ["source", "at", "by", "conflicts"] : ["at", "by"];
|
|
3437
|
+
const selected = [];
|
|
3438
|
+
for (const partNode of fieldNodes(cellNode.selectionSet)) {
|
|
3439
|
+
const part = partNode.name.value;
|
|
3440
|
+
if (!allowed.includes(part)) fail2(`unknown field '${part}' on cell '${column}'`, "unknown-field", allowed);
|
|
3441
|
+
const partAlias = partNode.alias?.value ?? part;
|
|
3442
|
+
if (part !== "conflicts") {
|
|
3443
|
+
if (partNode.selectionSet) fail2(`cell field '${part}' takes no sub-selection`, "invalid-selection");
|
|
3444
|
+
selected.push({ alias: partAlias, part });
|
|
3445
|
+
continue;
|
|
3446
|
+
}
|
|
3447
|
+
if (!partNode.selectionSet) fail2(`'conflicts' needs a selection set`, "invalid-selection");
|
|
3448
|
+
const conflictFields = fieldNodes(partNode.selectionSet).map((fieldNode) => {
|
|
3449
|
+
const field = fieldNode.name.value;
|
|
3450
|
+
if (field !== "source" && field !== "at" && field !== "value") fail2(`unknown field '${field}' on a cell conflict`, "unknown-field", ["source", "at", "value"]);
|
|
3451
|
+
return { alias: fieldNode.alias?.value ?? field, field };
|
|
3452
|
+
});
|
|
3453
|
+
selected.push({ alias: partAlias, part: "conflicts", fields: conflictFields, typename: CELL_TYPENAMES.conflict });
|
|
3454
|
+
}
|
|
3455
|
+
fields.push({
|
|
3456
|
+
alias: cellNode.alias?.value ?? column,
|
|
3457
|
+
column,
|
|
3458
|
+
typename: resolved ? CELL_TYPENAMES.resolved : CELL_TYPENAMES.stamp,
|
|
3459
|
+
parts: selected
|
|
3460
|
+
});
|
|
3461
|
+
}
|
|
3462
|
+
return { alias, typename: cellsTypename(meta.typename), fields };
|
|
3463
|
+
}
|
|
2497
3464
|
/** The `{ aggregate { count sum {…} avg {…} } nodes {…} }` block shared by roots and relations. */
|
|
2498
3465
|
lowerAggregateSelection(selectionSet, table) {
|
|
2499
3466
|
const result = {};
|
|
@@ -2501,17 +3468,17 @@ var OperationLowerer = class {
|
|
|
2501
3468
|
for (const node of aggregateBlockFields(selectionSet)) {
|
|
2502
3469
|
const name = node.name.value;
|
|
2503
3470
|
if (name === "nodes") {
|
|
2504
|
-
if (!node.selectionSet)
|
|
3471
|
+
if (!node.selectionSet) fail2("'nodes' needs a selection set", "invalid-aggregate");
|
|
2505
3472
|
result.nodes = this.lowerSelection(node.selectionSet, table);
|
|
2506
3473
|
continue;
|
|
2507
3474
|
}
|
|
2508
|
-
if (name !== "aggregate")
|
|
2509
|
-
if (sawAggregate)
|
|
3475
|
+
if (name !== "aggregate") fail2(`unknown aggregate field '${name}'`, "invalid-aggregate", ["aggregate", "nodes"]);
|
|
3476
|
+
if (sawAggregate) fail2("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
|
|
2510
3477
|
sawAggregate = true;
|
|
2511
|
-
if (!node.selectionSet)
|
|
3478
|
+
if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
|
|
2512
3479
|
Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
|
|
2513
3480
|
}
|
|
2514
|
-
if (!sawAggregate && !result.nodes)
|
|
3481
|
+
if (!sawAggregate && !result.nodes) fail2("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
|
|
2515
3482
|
return result;
|
|
2516
3483
|
}
|
|
2517
3484
|
/**
|
|
@@ -2528,24 +3495,24 @@ var OperationLowerer = class {
|
|
|
2528
3495
|
for (const node of aggregateBlockFields(selectionSet)) {
|
|
2529
3496
|
const name = node.name.value;
|
|
2530
3497
|
if (name === "key") {
|
|
2531
|
-
if (sawKey)
|
|
3498
|
+
if (sawKey) fail2("one key block per grouped aggregate", "invalid-aggregate");
|
|
2532
3499
|
sawKey = true;
|
|
2533
|
-
if (!node.selectionSet)
|
|
3500
|
+
if (!node.selectionSet) fail2("'key' needs a selection set", "invalid-aggregate");
|
|
2534
3501
|
result.key = this.lowerGroupKeySelection(node.selectionSet, table, groupColumns);
|
|
2535
3502
|
continue;
|
|
2536
3503
|
}
|
|
2537
3504
|
if (name === "nodes") {
|
|
2538
|
-
|
|
3505
|
+
fail2("'nodes' is not available on a grouped aggregate \u2014 query the rows separately", "invalid-aggregate", ["key", "aggregate"]);
|
|
2539
3506
|
}
|
|
2540
|
-
if (name !== "aggregate")
|
|
2541
|
-
if (sawAggregate)
|
|
3507
|
+
if (name !== "aggregate") fail2(`unknown grouped-aggregate field '${name}'`, "invalid-aggregate", ["key", "aggregate"]);
|
|
3508
|
+
if (sawAggregate) fail2("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
|
|
2542
3509
|
sawAggregate = true;
|
|
2543
|
-
if (!node.selectionSet)
|
|
3510
|
+
if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
|
|
2544
3511
|
Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
|
|
2545
3512
|
}
|
|
2546
3513
|
const rendersAggregate = result.count !== void 0 || (result.sum?.length ?? 0) > 0 || (result.avg?.length ?? 0) > 0;
|
|
2547
3514
|
if (result.key.length === 0 && !rendersAggregate) {
|
|
2548
|
-
|
|
3515
|
+
fail2(
|
|
2549
3516
|
"grouped aggregate needs at least one key column or an aggregate \u2014 a block selecting only __typename produces nothing",
|
|
2550
3517
|
"invalid-aggregate",
|
|
2551
3518
|
["key", "aggregate"]
|
|
@@ -2562,14 +3529,14 @@ var OperationLowerer = class {
|
|
|
2562
3529
|
lowerGroupKeySelection(selectionSet, table, groupColumns) {
|
|
2563
3530
|
const key = [];
|
|
2564
3531
|
for (const node of selectionSet.selections) {
|
|
2565
|
-
if (node.kind !== Kind.FIELD)
|
|
3532
|
+
if (node.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
2566
3533
|
const name = node.name.value;
|
|
2567
3534
|
if (name === "__typename") continue;
|
|
2568
3535
|
if (!this.column(table, name)) {
|
|
2569
|
-
|
|
3536
|
+
fail2(`unknown key column '${name}' on '${table}'`, "unknown-column", [...groupColumns]);
|
|
2570
3537
|
}
|
|
2571
3538
|
if (!groupColumns.has(name)) {
|
|
2572
|
-
|
|
3539
|
+
fail2(
|
|
2573
3540
|
`key column '${name}' is not one of this query's groupBy keys \u2014 it has no single value within a group`,
|
|
2574
3541
|
"invalid-group-key",
|
|
2575
3542
|
[...groupColumns]
|
|
@@ -2592,21 +3559,21 @@ var OperationLowerer = class {
|
|
|
2592
3559
|
if (columns) count.columns = this.lowerDistinctOn(columns.value, table);
|
|
2593
3560
|
const distinct = args.get("distinct");
|
|
2594
3561
|
if (distinct) {
|
|
2595
|
-
if (distinct.value.kind !== Kind.BOOLEAN)
|
|
3562
|
+
if (distinct.value.kind !== Kind.BOOLEAN) fail2("count(distinct:) must be a boolean literal", "invalid-aggregate");
|
|
2596
3563
|
count.distinct = distinct.value.value;
|
|
2597
3564
|
}
|
|
2598
3565
|
result.count = count;
|
|
2599
3566
|
continue;
|
|
2600
3567
|
}
|
|
2601
3568
|
if (aggName === "sum" || aggName === "avg") {
|
|
2602
|
-
if (!aggField.selectionSet)
|
|
3569
|
+
if (!aggField.selectionSet) fail2(`'${aggName}' needs a column selection`, "invalid-aggregate");
|
|
2603
3570
|
const columns = [];
|
|
2604
3571
|
for (const columnField of aggField.selectionSet.selections) {
|
|
2605
|
-
if (columnField.kind !== Kind.FIELD)
|
|
3572
|
+
if (columnField.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
2606
3573
|
if (columnField.name.value === "__typename") continue;
|
|
2607
3574
|
const column = this.column(table, columnField.name.value);
|
|
2608
3575
|
if (!column || typeof column.type !== "string" || !NUMERIC_TYPES.has(column.type)) {
|
|
2609
|
-
|
|
3576
|
+
fail2(
|
|
2610
3577
|
`'${aggName}' supports numeric columns only, got '${columnField.name.value}' on '${table}'`,
|
|
2611
3578
|
"invalid-aggregate",
|
|
2612
3579
|
this.meta(table).columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type)).map((c) => c.name)
|
|
@@ -2617,7 +3584,7 @@ var OperationLowerer = class {
|
|
|
2617
3584
|
result[aggName] = columns;
|
|
2618
3585
|
continue;
|
|
2619
3586
|
}
|
|
2620
|
-
|
|
3587
|
+
fail2(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
|
|
2621
3588
|
}
|
|
2622
3589
|
return result;
|
|
2623
3590
|
}
|
|
@@ -2627,41 +3594,43 @@ var OperationLowerer = class {
|
|
|
2627
3594
|
lowerRoots(operation, kind) {
|
|
2628
3595
|
const roots = [];
|
|
2629
3596
|
for (const node of operation.selectionSet.selections) {
|
|
2630
|
-
if (node.kind !== Kind.FIELD)
|
|
3597
|
+
if (node.kind !== Kind.FIELD) fail2("fragments are not supported at the operation root", "invalid-root");
|
|
2631
3598
|
if (node.selectionSet) visit(node.selectionSet, {
|
|
2632
3599
|
Directive(directive) {
|
|
2633
3600
|
if (directive.name.value === "assertAffectedRows") {
|
|
2634
|
-
|
|
3601
|
+
fail2("@assertAffectedRows is supported on mutation roots only", "invalid-affected-row-assertion");
|
|
2635
3602
|
}
|
|
2636
3603
|
}
|
|
2637
3604
|
});
|
|
2638
3605
|
const assertion = this.parseAffectedRowAssertion(node, kind);
|
|
2639
3606
|
const gate = this.parseRootDirectives(node);
|
|
2640
3607
|
if (gate.omit) continue;
|
|
2641
|
-
const
|
|
3608
|
+
const lowered = kind === "query" ? this.lowerQueryRoot(node) : this.lowerMutationRoot(node);
|
|
3609
|
+
const meta = this.meta(lowered.table);
|
|
3610
|
+
const root = kind === "query" ? applyPrefetchDirectives({ node, root: lowered, meta }) : applyWritebackDirectives({ node, root: lowered, meta });
|
|
2642
3611
|
const guarded = root.kind === "mutation" && assertion !== void 0 ? { ...root, assertAffectedRows: assertion } : root;
|
|
2643
3612
|
roots.push(gate.includeIf ? { ...guarded, includeIf: gate.includeIf } : guarded);
|
|
2644
3613
|
}
|
|
2645
|
-
if (roots.length === 0)
|
|
3614
|
+
if (roots.length === 0) fail2("operation has no roots (all statically skipped?)", "invalid-root");
|
|
2646
3615
|
return roots;
|
|
2647
3616
|
}
|
|
2648
3617
|
parseAffectedRowAssertion(node, kind) {
|
|
2649
3618
|
const assertions = node.directives?.filter((directive) => directive.name.value === "assertAffectedRows") ?? [];
|
|
2650
3619
|
if (!assertions.length) return void 0;
|
|
2651
3620
|
if (kind !== "mutation" || assertions.length !== 1) {
|
|
2652
|
-
|
|
3621
|
+
fail2("@assertAffectedRows is supported once per mutation root", "invalid-affected-row-assertion");
|
|
2653
3622
|
}
|
|
2654
3623
|
if (node.directives?.some((directive) => ["include", "skip"].includes(directive.name.value))) {
|
|
2655
|
-
|
|
3624
|
+
fail2("an @assertAffectedRows root cannot use @include or @skip", "invalid-affected-row-assertion");
|
|
2656
3625
|
}
|
|
2657
3626
|
const args = assertions[0].arguments;
|
|
2658
3627
|
const arg = args?.[0];
|
|
2659
3628
|
if (args?.length !== 1 || arg?.name.value !== "exactly" || arg.value.kind !== Kind.INT) {
|
|
2660
|
-
|
|
3629
|
+
fail2("@assertAffectedRows(exactly:) requires a document-authored nonnegative Int literal", "invalid-affected-row-assertion");
|
|
2661
3630
|
}
|
|
2662
3631
|
const count = Number(arg.value.value);
|
|
2663
3632
|
if (!Number.isInteger(count) || count < 0 || count > 2147483647) {
|
|
2664
|
-
|
|
3633
|
+
fail2("@assertAffectedRows(exactly:) must be between 0 and 2147483647", "invalid-affected-row-assertion");
|
|
2665
3634
|
}
|
|
2666
3635
|
return count;
|
|
2667
3636
|
}
|
|
@@ -2671,18 +3640,19 @@ var OperationLowerer = class {
|
|
|
2671
3640
|
for (const directive of node.directives ?? []) {
|
|
2672
3641
|
const name = directive.name.value;
|
|
2673
3642
|
if (name === "assertAffectedRows") continue;
|
|
3643
|
+
if (SOURCE_DIRECTIVES.has(name)) continue;
|
|
2674
3644
|
if (name !== "include" && name !== "skip") {
|
|
2675
|
-
|
|
3645
|
+
fail2(`directive @${name} is not supported on fields`, "unknown-directive", ["include", "skip"]);
|
|
2676
3646
|
}
|
|
2677
3647
|
const ifArg = directive.arguments?.find((a) => a.name.value === "if");
|
|
2678
|
-
if (!ifArg)
|
|
3648
|
+
if (!ifArg) fail2(`@${name} needs an if: argument`, "invalid-directive");
|
|
2679
3649
|
if (ifArg.value.kind === Kind.BOOLEAN) {
|
|
2680
3650
|
const keep = name === "include" ? ifArg.value.value : !ifArg.value.value;
|
|
2681
3651
|
if (!keep) omit = true;
|
|
2682
3652
|
continue;
|
|
2683
3653
|
}
|
|
2684
|
-
if (ifArg.value.kind !== Kind.VARIABLE)
|
|
2685
|
-
if (includeIf)
|
|
3654
|
+
if (ifArg.value.kind !== Kind.VARIABLE) fail2(`@${name}(if:) must be a boolean literal or variable`, "invalid-directive");
|
|
3655
|
+
if (includeIf) fail2("at most one of @include/@skip per root field", "invalid-directive");
|
|
2686
3656
|
includeIf = { variable: ifArg.value.name.value, negated: name === "skip" };
|
|
2687
3657
|
}
|
|
2688
3658
|
return omit ? { omit: true } : { includeIf, omit: false };
|
|
@@ -2691,12 +3661,12 @@ var OperationLowerer = class {
|
|
|
2691
3661
|
const name = node.name.value;
|
|
2692
3662
|
const alias = node.alias?.value ?? name;
|
|
2693
3663
|
const rootInfo = this.ctx.queryRoots.get(name);
|
|
2694
|
-
if (!rootInfo)
|
|
3664
|
+
if (!rootInfo) fail2(`unknown query root '${name}'`, "unknown-root", [...this.ctx.queryRoots.keys()]);
|
|
2695
3665
|
const { table } = rootInfo;
|
|
2696
3666
|
const args = argsByName(node);
|
|
2697
3667
|
if (rootInfo.kind === "select") {
|
|
2698
3668
|
assertKnownArgs(node, ["where", "orderBy", "distinctOn", "limit", "offset"]);
|
|
2699
|
-
if (!node.selectionSet)
|
|
3669
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
2700
3670
|
const where = args.get("where");
|
|
2701
3671
|
const orderBy = args.get("orderBy");
|
|
2702
3672
|
const distinctOn = args.get("distinctOn");
|
|
@@ -2715,7 +3685,7 @@ var OperationLowerer = class {
|
|
|
2715
3685
|
};
|
|
2716
3686
|
}
|
|
2717
3687
|
if (rootInfo.kind === "byPk") {
|
|
2718
|
-
if (!node.selectionSet)
|
|
3688
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
2719
3689
|
const pk = this.lowerPkArgs(node, table);
|
|
2720
3690
|
return { kind: "byPk", alias, table, selection: this.lowerSelection(node.selectionSet, table), pk, where: CONST_TRUE };
|
|
2721
3691
|
}
|
|
@@ -2723,7 +3693,7 @@ var OperationLowerer = class {
|
|
|
2723
3693
|
return this.lowerAggregateGroupsRoot(node, { alias, table });
|
|
2724
3694
|
}
|
|
2725
3695
|
assertKnownArgs(node, ["where"]);
|
|
2726
|
-
if (!node.selectionSet)
|
|
3696
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
2727
3697
|
const whereArg = args.get("where");
|
|
2728
3698
|
const aggregate = this.lowerAggregateSelection(node.selectionSet, table);
|
|
2729
3699
|
return {
|
|
@@ -2737,10 +3707,10 @@ var OperationLowerer = class {
|
|
|
2737
3707
|
/** `<t>AggregateGroups(where:, groupBy:, limit:, offset:)` → an aggregateGroups root. */
|
|
2738
3708
|
lowerAggregateGroupsRoot(node, { alias, table }) {
|
|
2739
3709
|
assertKnownArgs(node, ["where", "groupBy", "limit", "offset"]);
|
|
2740
|
-
if (!node.selectionSet)
|
|
3710
|
+
if (!node.selectionSet) fail2(`'${node.name.value}' needs a selection set`, "invalid-selection");
|
|
2741
3711
|
const args = argsByName(node);
|
|
2742
3712
|
const groupByArg = args.get("groupBy");
|
|
2743
|
-
if (!groupByArg)
|
|
3713
|
+
if (!groupByArg) fail2(`'${node.name.value}' requires a 'groupBy' argument`, "missing-group-by");
|
|
2744
3714
|
const whereArg = args.get("where");
|
|
2745
3715
|
const groupBy = this.lowerGroupBy(groupByArg.value, table);
|
|
2746
3716
|
const aggregate = this.lowerGroupedAggregateSelection(node.selectionSet, table, groupBy);
|
|
@@ -2770,55 +3740,55 @@ var OperationLowerer = class {
|
|
|
2770
3740
|
*/
|
|
2771
3741
|
lowerGroupBy(node, table) {
|
|
2772
3742
|
if (node.kind === Kind.VARIABLE) {
|
|
2773
|
-
|
|
3743
|
+
fail2(
|
|
2774
3744
|
`groupBy on '${table}' must be written in the document \u2014 it cannot come from a variable`,
|
|
2775
3745
|
"dynamic-group-by"
|
|
2776
3746
|
);
|
|
2777
3747
|
}
|
|
2778
3748
|
const items = node.kind === Kind.LIST ? node.values : [node];
|
|
2779
|
-
if (items.length === 0)
|
|
3749
|
+
if (items.length === 0) fail2(`groupBy on '${table}' needs at least one key`, "invalid-group-by");
|
|
2780
3750
|
const seen = /* @__PURE__ */ new Set();
|
|
2781
3751
|
return items.map((item) => {
|
|
2782
3752
|
if (item.kind !== Kind.OBJECT) {
|
|
2783
|
-
|
|
3753
|
+
fail2(`groupBy entries on '${table}' must be objects like { column: foo }`, "invalid-group-by");
|
|
2784
3754
|
}
|
|
2785
3755
|
const fields = new Map(item.fields.map((field) => [field.name.value, field.value]));
|
|
2786
3756
|
for (const key of fields.keys()) {
|
|
2787
3757
|
if (key !== "column" && key !== "bucket") {
|
|
2788
|
-
|
|
3758
|
+
fail2(`unknown groupBy field '${key}' on '${table}'`, "invalid-group-by", ["column", "bucket"]);
|
|
2789
3759
|
}
|
|
2790
3760
|
}
|
|
2791
3761
|
const columnNode = fields.get("column");
|
|
2792
|
-
if (!columnNode)
|
|
3762
|
+
if (!columnNode) fail2(`groupBy entries on '${table}' need a 'column'`, "invalid-group-by");
|
|
2793
3763
|
if (columnNode.kind !== Kind.ENUM) {
|
|
2794
|
-
|
|
3764
|
+
fail2(`groupBy column on '${table}' must be a group-by column enum value`, "invalid-group-by");
|
|
2795
3765
|
}
|
|
2796
3766
|
const column = this.column(table, columnNode.value);
|
|
2797
3767
|
if (!column) {
|
|
2798
|
-
|
|
3768
|
+
fail2(`unknown groupBy column '${columnNode.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
|
|
2799
3769
|
}
|
|
2800
3770
|
if (column.type === "jsonb") {
|
|
2801
|
-
|
|
3771
|
+
fail2(`groupBy column '${table}.${columnNode.value}' is jsonb and cannot be a group key`, "invalid-group-by");
|
|
2802
3772
|
}
|
|
2803
3773
|
const bucketNode = fields.get("bucket");
|
|
2804
3774
|
let bucket;
|
|
2805
3775
|
if (bucketNode) {
|
|
2806
3776
|
if (bucketNode.kind !== Kind.ENUM) {
|
|
2807
|
-
|
|
3777
|
+
fail2(`groupBy bucket on '${table}.${columnNode.value}' must be a DateBucket enum value`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
|
|
2808
3778
|
}
|
|
2809
3779
|
bucket = DATE_BUCKET_BY_ENUM[bucketNode.value];
|
|
2810
3780
|
if (!bucket) {
|
|
2811
|
-
|
|
3781
|
+
fail2(`unknown DateBucket '${bucketNode.value}'`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
|
|
2812
3782
|
}
|
|
2813
3783
|
if (typeof column.type !== "string" || !BUCKETABLE_TYPES.has(column.type)) {
|
|
2814
|
-
|
|
3784
|
+
fail2(
|
|
2815
3785
|
`groupBy bucket is only valid on date/timestamptz columns \u2014 '${table}.${columnNode.value}' is ${typeof column.type === "string" ? column.type : "an enum"}`,
|
|
2816
3786
|
"invalid-group-by"
|
|
2817
3787
|
);
|
|
2818
3788
|
}
|
|
2819
3789
|
}
|
|
2820
3790
|
if (seen.has(columnNode.value)) {
|
|
2821
|
-
|
|
3791
|
+
fail2(`duplicate groupBy column '${columnNode.value}' on '${table}'`, "invalid-group-by");
|
|
2822
3792
|
}
|
|
2823
3793
|
seen.add(columnNode.value);
|
|
2824
3794
|
return bucket ? { column: columnNode.value, bucket } : { column: columnNode.value };
|
|
@@ -2831,10 +3801,10 @@ var OperationLowerer = class {
|
|
|
2831
3801
|
if (node.kind === Kind.VARIABLE) return { ...base, variable: node.name.value };
|
|
2832
3802
|
if (node.kind === Kind.INT) {
|
|
2833
3803
|
const value = Number(node.value);
|
|
2834
|
-
if (value < 1)
|
|
3804
|
+
if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
|
|
2835
3805
|
return { ...base, value: Math.min(value, MAX_GROUPS) };
|
|
2836
3806
|
}
|
|
2837
|
-
|
|
3807
|
+
fail2("limit must be an integer literal or a variable", "invalid-limit");
|
|
2838
3808
|
}
|
|
2839
3809
|
/** byPk-style pk arguments: one argument per primary-key column, exactly. */
|
|
2840
3810
|
lowerPkArgs(node, table) {
|
|
@@ -2843,7 +3813,7 @@ var OperationLowerer = class {
|
|
|
2843
3813
|
assertKnownArgs(node, meta.primaryKey);
|
|
2844
3814
|
return meta.primaryKey.map((column) => {
|
|
2845
3815
|
const arg = args.get(column);
|
|
2846
|
-
if (!arg)
|
|
3816
|
+
if (!arg) fail2(`'${node.name.value}' is missing primary-key argument '${column}'`, "missing-pk-argument", meta.primaryKey);
|
|
2847
3817
|
return { column, value: this.lowerValue(arg.value) };
|
|
2848
3818
|
});
|
|
2849
3819
|
}
|
|
@@ -2854,19 +3824,22 @@ var OperationLowerer = class {
|
|
|
2854
3824
|
const name = node.name.value;
|
|
2855
3825
|
const alias = node.alias?.value ?? name;
|
|
2856
3826
|
const rootInfo = this.ctx.mutationRoots.get(name);
|
|
2857
|
-
if (!rootInfo)
|
|
3827
|
+
if (!rootInfo) fail2(`unknown mutation root '${name}'`, "unknown-root", [...this.ctx.mutationRoots.keys()]);
|
|
2858
3828
|
if (rootInfo.op === "cowOverride" || rootInfo.op === "cowRevert") {
|
|
2859
3829
|
return this.lowerCowRoot(node, rootInfo);
|
|
2860
3830
|
}
|
|
3831
|
+
if (rootInfo.op === "resolvedWrite") {
|
|
3832
|
+
return this.lowerResolvedRoot(node, rootInfo);
|
|
3833
|
+
}
|
|
2861
3834
|
const { op, table } = rootInfo;
|
|
2862
3835
|
const args = argsByName(node);
|
|
2863
|
-
if (!node.selectionSet)
|
|
3836
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
2864
3837
|
const base = { kind: "mutation", op, alias, table, pinnedColumns: [], affectedRows: false };
|
|
2865
3838
|
switch (op) {
|
|
2866
3839
|
case "insert": {
|
|
2867
3840
|
assertKnownArgs(node, ["objects", "onConflict"]);
|
|
2868
3841
|
const objects = args.get("objects");
|
|
2869
|
-
if (!objects)
|
|
3842
|
+
if (!objects) fail2(`'${name}' needs objects:`, "missing-argument");
|
|
2870
3843
|
const response = this.lowerMutationResponse(node.selectionSet, table);
|
|
2871
3844
|
return {
|
|
2872
3845
|
...base,
|
|
@@ -2878,7 +3851,7 @@ var OperationLowerer = class {
|
|
|
2878
3851
|
case "insertOne": {
|
|
2879
3852
|
assertKnownArgs(node, ["object", "onConflict"]);
|
|
2880
3853
|
const object = args.get("object");
|
|
2881
|
-
if (!object)
|
|
3854
|
+
if (!object) fail2(`'${name}' needs object:`, "missing-argument");
|
|
2882
3855
|
return {
|
|
2883
3856
|
...base,
|
|
2884
3857
|
objects: this.lowerInsertObjects(object.value, table, { list: false }),
|
|
@@ -2889,7 +3862,7 @@ var OperationLowerer = class {
|
|
|
2889
3862
|
case "update": {
|
|
2890
3863
|
assertKnownArgs(node, ["where", "_set", "_append", "_inc"]);
|
|
2891
3864
|
const where = args.get("where");
|
|
2892
|
-
if (!where)
|
|
3865
|
+
if (!where) fail2(`'${name}' needs where: (bulk updates without a filter are not allowed)`, "missing-argument");
|
|
2893
3866
|
return {
|
|
2894
3867
|
...base,
|
|
2895
3868
|
...this.lowerMutationResponse(node.selectionSet, table),
|
|
@@ -2900,7 +3873,7 @@ var OperationLowerer = class {
|
|
|
2900
3873
|
case "updateByPk": {
|
|
2901
3874
|
assertKnownArgs(node, ["pkColumns", "_set", "_append", "_inc"]);
|
|
2902
3875
|
const pkColumns = args.get("pkColumns");
|
|
2903
|
-
if (!pkColumns)
|
|
3876
|
+
if (!pkColumns) fail2(`'${name}' needs pkColumns:`, "missing-argument");
|
|
2904
3877
|
return {
|
|
2905
3878
|
...base,
|
|
2906
3879
|
pk: this.lowerPkColumnsArg(pkColumns.value, table),
|
|
@@ -2911,7 +3884,7 @@ var OperationLowerer = class {
|
|
|
2911
3884
|
case "delete": {
|
|
2912
3885
|
assertKnownArgs(node, ["where"]);
|
|
2913
3886
|
const where = args.get("where");
|
|
2914
|
-
if (!where)
|
|
3887
|
+
if (!where) fail2(`'${name}' needs where: (bulk deletes without a filter are not allowed)`, "missing-argument");
|
|
2915
3888
|
return {
|
|
2916
3889
|
...base,
|
|
2917
3890
|
...this.lowerMutationResponse(node.selectionSet, table),
|
|
@@ -2938,11 +3911,11 @@ var OperationLowerer = class {
|
|
|
2938
3911
|
const name = node.name.value;
|
|
2939
3912
|
const alias = node.alias?.value ?? name;
|
|
2940
3913
|
const { table } = rootInfo;
|
|
2941
|
-
if (!node.selectionSet)
|
|
3914
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
2942
3915
|
const meta = this.meta(table);
|
|
2943
3916
|
const args = argsByName(node);
|
|
2944
3917
|
const pkArg = args.get(rootInfo.pk);
|
|
2945
|
-
if (!pkArg)
|
|
3918
|
+
if (!pkArg) fail2(`'${name}' is missing primary-key argument '${rootInfo.pk}'`, "missing-pk-argument", [rootInfo.pk]);
|
|
2946
3919
|
const pk = [{ column: rootInfo.pk, value: this.lowerValue(pkArg.value) }];
|
|
2947
3920
|
const base = {
|
|
2948
3921
|
kind: "mutation",
|
|
@@ -2963,24 +3936,78 @@ var OperationLowerer = class {
|
|
|
2963
3936
|
const entries = rootInfo.set.map((column) => {
|
|
2964
3937
|
const arg = args.get(column);
|
|
2965
3938
|
if (!arg) {
|
|
2966
|
-
|
|
3939
|
+
fail2(
|
|
2967
3940
|
`'${name}' is missing set argument '${column}' \u2014 a cow override writes every declared set column (pass null to clear a nullable one)`,
|
|
2968
3941
|
"missing-argument",
|
|
2969
3942
|
[...rootInfo.set]
|
|
2970
3943
|
);
|
|
2971
3944
|
}
|
|
2972
3945
|
if (!meta.settable.includes(column)) {
|
|
2973
|
-
|
|
3946
|
+
fail2(`column '${table}.${column}' is not updatable (pinned by the write policy or deprecated)`, "column-not-settable", meta.settable);
|
|
2974
3947
|
}
|
|
2975
3948
|
return { column, value: this.lowerValue(arg.value) };
|
|
2976
3949
|
});
|
|
2977
3950
|
return { ...base, set: { entries }, cowCopyColumns: [...rootInfo.copyColumns] };
|
|
2978
3951
|
}
|
|
3952
|
+
/**
|
|
3953
|
+
* Resolved-table roots: insertOne(object), updateByPk(pkColumns, _set),
|
|
3954
|
+
* deleteByPk(pk…), revertByPk(pkColumns, columns). The executor writes only
|
|
3955
|
+
* the touched cells into the manual source; `returning` reads the view.
|
|
3956
|
+
*/
|
|
3957
|
+
lowerResolvedRoot(node, rootInfo) {
|
|
3958
|
+
const name = node.name.value;
|
|
3959
|
+
const alias = node.alias?.value ?? name;
|
|
3960
|
+
const { table, action } = rootInfo;
|
|
3961
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
3962
|
+
const args = argsByName(node);
|
|
3963
|
+
const base = {
|
|
3964
|
+
kind: "mutation",
|
|
3965
|
+
op: "resolvedWrite",
|
|
3966
|
+
resolvedAction: action,
|
|
3967
|
+
alias,
|
|
3968
|
+
table,
|
|
3969
|
+
pinnedColumns: [],
|
|
3970
|
+
affectedRows: false,
|
|
3971
|
+
returning: this.lowerSelection(node.selectionSet, table)
|
|
3972
|
+
};
|
|
3973
|
+
switch (action) {
|
|
3974
|
+
case "insert": {
|
|
3975
|
+
assertKnownArgs(node, ["object"]);
|
|
3976
|
+
const object = args.get("object");
|
|
3977
|
+
if (!object) fail2(`'${name}' needs object:`, "missing-argument");
|
|
3978
|
+
return { ...base, objects: this.lowerInsertObjects(object.value, table, { list: false }) };
|
|
3979
|
+
}
|
|
3980
|
+
case "update": {
|
|
3981
|
+
assertKnownArgs(node, ["pkColumns", "_set"]);
|
|
3982
|
+
const pkColumns = args.get("pkColumns");
|
|
3983
|
+
if (!pkColumns || !args.get("_set")) fail2(`'${name}' needs pkColumns: and _set:`, "missing-argument");
|
|
3984
|
+
return { ...base, pk: this.lowerPkColumnsArg(pkColumns.value, table), ...this.lowerWriteValues(args, table) };
|
|
3985
|
+
}
|
|
3986
|
+
case "delete":
|
|
3987
|
+
return { ...base, pk: this.lowerPkArgs(node, table) };
|
|
3988
|
+
case "revert": {
|
|
3989
|
+
assertKnownArgs(node, ["pkColumns", "columns"]);
|
|
3990
|
+
const pkColumns = args.get("pkColumns");
|
|
3991
|
+
const columns = args.get("columns");
|
|
3992
|
+
if (!pkColumns || !columns) fail2(`'${name}' needs pkColumns: and columns:`, "missing-argument");
|
|
3993
|
+
const items = columns.value.kind === Kind.LIST ? columns.value.values : [columns.value];
|
|
3994
|
+
const meta = this.meta(table);
|
|
3995
|
+
const revertColumns = items.map((item) => {
|
|
3996
|
+
if (item.kind !== Kind.ENUM || !meta.settable.includes(item.value)) {
|
|
3997
|
+
fail2(`'${name}' columns: must list update-column enum values written in the document`, "invalid-revert-columns", meta.settable);
|
|
3998
|
+
}
|
|
3999
|
+
return item.value;
|
|
4000
|
+
});
|
|
4001
|
+
if (revertColumns.length === 0) fail2(`'${name}' columns: must name at least one column`, "invalid-revert-columns");
|
|
4002
|
+
return { ...base, pk: this.lowerPkColumnsArg(pkColumns.value, table), revertColumns: [...new Set(revertColumns)] };
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
2979
4006
|
lowerMutationResponse(selectionSet, table) {
|
|
2980
4007
|
let affectedRows = false;
|
|
2981
4008
|
let returning = null;
|
|
2982
4009
|
for (const node of selectionSet.selections) {
|
|
2983
|
-
if (node.kind !== Kind.FIELD)
|
|
4010
|
+
if (node.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
2984
4011
|
const name = node.name.value;
|
|
2985
4012
|
if (name === "__typename") continue;
|
|
2986
4013
|
if (name === "affectedRows") {
|
|
@@ -2988,34 +4015,34 @@ var OperationLowerer = class {
|
|
|
2988
4015
|
continue;
|
|
2989
4016
|
}
|
|
2990
4017
|
if (name === "returning") {
|
|
2991
|
-
if (!node.selectionSet)
|
|
4018
|
+
if (!node.selectionSet) fail2("'returning' needs a selection set", "invalid-selection");
|
|
2992
4019
|
returning = this.lowerSelection(node.selectionSet, table);
|
|
2993
4020
|
continue;
|
|
2994
4021
|
}
|
|
2995
|
-
|
|
4022
|
+
fail2(`unknown mutation-response field '${name}'`, "invalid-selection", ["affectedRows", "returning"]);
|
|
2996
4023
|
}
|
|
2997
4024
|
return { affectedRows, returning };
|
|
2998
4025
|
}
|
|
2999
4026
|
lowerInsertObjects(node, table, { list }) {
|
|
3000
4027
|
if (node.kind === Kind.VARIABLE) return { slot: this.slot(node.name.value, "insertInput", table) };
|
|
3001
4028
|
if (list) {
|
|
3002
|
-
if (node.kind !== Kind.LIST)
|
|
4029
|
+
if (node.kind !== Kind.LIST) fail2(`objects: must be a list literal or a variable`, "invalid-insert");
|
|
3003
4030
|
return { rows: node.values.map((item) => this.lowerInsertRow(item, table)) };
|
|
3004
4031
|
}
|
|
3005
4032
|
return { rows: [this.lowerInsertRow(node, table)] };
|
|
3006
4033
|
}
|
|
3007
4034
|
lowerInsertRow(node, table) {
|
|
3008
|
-
if (node.kind !== Kind.OBJECT)
|
|
4035
|
+
if (node.kind !== Kind.OBJECT) fail2(`insert rows on '${table}' must be objects`, "invalid-insert");
|
|
3009
4036
|
const meta = this.meta(table);
|
|
3010
4037
|
return node.fields.map((field) => {
|
|
3011
4038
|
const key = field.name.value;
|
|
3012
4039
|
if (this.link(table, key)) {
|
|
3013
|
-
|
|
4040
|
+
fail2(`nested inserts are not supported in v2.0 \u2014 '${key}' is a link on '${table}'`, "nested-insert-unsupported");
|
|
3014
4041
|
}
|
|
3015
4042
|
if (!meta.insertable.includes(key)) {
|
|
3016
4043
|
const column = this.column(table, key);
|
|
3017
|
-
if (column)
|
|
3018
|
-
|
|
4044
|
+
if (column) fail2(`column '${table}.${key}' is not insertable (pinned by the write policy or deprecated)`, "column-not-insertable", meta.insertable);
|
|
4045
|
+
fail2(`unknown column '${key}' in an insert on '${table}'`, "unknown-column", meta.insertable);
|
|
3019
4046
|
}
|
|
3020
4047
|
return { column: key, value: this.lowerValue(field.value) };
|
|
3021
4048
|
});
|
|
@@ -3024,32 +4051,32 @@ var OperationLowerer = class {
|
|
|
3024
4051
|
if (!arg) return {};
|
|
3025
4052
|
const node = arg.value;
|
|
3026
4053
|
if (node.kind === Kind.VARIABLE) return { onConflict: { slot: this.slot(node.name.value, "onConflict", table) } };
|
|
3027
|
-
if (node.kind !== Kind.OBJECT)
|
|
4054
|
+
if (node.kind !== Kind.OBJECT) fail2("onConflict: must be an object literal or a variable", "invalid-on-conflict");
|
|
3028
4055
|
const meta = this.meta(table);
|
|
3029
4056
|
let constraint;
|
|
3030
4057
|
const updateColumns = [];
|
|
3031
4058
|
for (const field of node.fields) {
|
|
3032
4059
|
if (field.name.value === "constraint") {
|
|
3033
|
-
if (field.value.kind !== Kind.ENUM)
|
|
4060
|
+
if (field.value.kind !== Kind.ENUM) fail2("onConflict.constraint must be a constraint enum value", "invalid-on-conflict");
|
|
3034
4061
|
constraint = field.value.value;
|
|
3035
4062
|
continue;
|
|
3036
4063
|
}
|
|
3037
4064
|
if (field.name.value === "updateColumns") {
|
|
3038
4065
|
const items = field.value.kind === Kind.LIST ? field.value.values : [field.value];
|
|
3039
4066
|
for (const item of items) {
|
|
3040
|
-
if (item.kind !== Kind.ENUM)
|
|
4067
|
+
if (item.kind !== Kind.ENUM) fail2("onConflict.updateColumns must be update-column enum values", "invalid-on-conflict");
|
|
3041
4068
|
if (!meta.settable.includes(item.value)) {
|
|
3042
|
-
|
|
4069
|
+
fail2(`onConflict.updateColumns: '${item.value}' is not updatable on '${table}'`, "invalid-on-conflict", meta.settable);
|
|
3043
4070
|
}
|
|
3044
4071
|
updateColumns.push(item.value);
|
|
3045
4072
|
}
|
|
3046
4073
|
continue;
|
|
3047
4074
|
}
|
|
3048
|
-
|
|
4075
|
+
fail2(`unknown onConflict key '${field.name.value}'`, "invalid-on-conflict", ["constraint", "updateColumns"]);
|
|
3049
4076
|
}
|
|
3050
|
-
if (!constraint)
|
|
4077
|
+
if (!constraint) fail2("onConflict needs a constraint", "invalid-on-conflict", meta.constraints.map((c) => c.name));
|
|
3051
4078
|
if (!meta.constraints.some((c) => c.name === constraint)) {
|
|
3052
|
-
|
|
4079
|
+
fail2(`unknown constraint '${constraint}' on '${table}'`, "invalid-on-conflict", meta.constraints.map((c) => c.name));
|
|
3053
4080
|
}
|
|
3054
4081
|
return { onConflict: { constraint, updateColumns } };
|
|
3055
4082
|
}
|
|
@@ -3067,36 +4094,36 @@ var OperationLowerer = class {
|
|
|
3067
4094
|
const inc = args.get("_inc");
|
|
3068
4095
|
if (inc) {
|
|
3069
4096
|
if (inc.value.kind === Kind.VARIABLE) {
|
|
3070
|
-
|
|
4097
|
+
fail2("_inc has no dynamic input type \u2014 inline the columns with variable leaf values", "invalid-inc");
|
|
3071
4098
|
}
|
|
3072
4099
|
const numeric = meta.columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type) && meta.settable.includes(c.name)).map((c) => c.name);
|
|
3073
4100
|
result.inc = { entries: this.lowerColumnEntries(inc.value, table, numeric, "_inc") };
|
|
3074
4101
|
}
|
|
3075
4102
|
if (!result.set && !result.append && !result.inc) {
|
|
3076
|
-
|
|
4103
|
+
fail2(`an update on '${table}' needs at least one of _set/_append/_inc`, "missing-argument");
|
|
3077
4104
|
}
|
|
3078
4105
|
return result;
|
|
3079
4106
|
}
|
|
3080
4107
|
lowerColumnEntries(node, table, whitelist, argName) {
|
|
3081
|
-
if (node.kind !== Kind.OBJECT)
|
|
4108
|
+
if (node.kind !== Kind.OBJECT) fail2(`${argName} on '${table}' must be an object literal or a variable`, "invalid-write-values");
|
|
3082
4109
|
return node.fields.map((field) => {
|
|
3083
4110
|
const key = field.name.value;
|
|
3084
4111
|
if (!whitelist.includes(key)) {
|
|
3085
|
-
|
|
4112
|
+
fail2(`${argName}: column '${key}' is not allowed on '${table}'`, "column-not-settable", [...whitelist]);
|
|
3086
4113
|
}
|
|
3087
4114
|
return { column: key, value: this.lowerValue(field.value) };
|
|
3088
4115
|
});
|
|
3089
4116
|
}
|
|
3090
4117
|
lowerPkColumnsArg(node, table) {
|
|
3091
|
-
if (node.kind !== Kind.OBJECT)
|
|
4118
|
+
if (node.kind !== Kind.OBJECT) fail2(`pkColumns on '${table}' must be an object literal`, "invalid-pk-columns");
|
|
3092
4119
|
const meta = this.meta(table);
|
|
3093
4120
|
const provided = new Map(node.fields.map((field) => [field.name.value, field.value]));
|
|
3094
4121
|
for (const key of provided.keys()) {
|
|
3095
|
-
if (!meta.primaryKey.includes(key))
|
|
4122
|
+
if (!meta.primaryKey.includes(key)) fail2(`pkColumns: '${key}' is not a primary-key column of '${table}'`, "invalid-pk-columns", meta.primaryKey);
|
|
3096
4123
|
}
|
|
3097
4124
|
return meta.primaryKey.map((column) => {
|
|
3098
4125
|
const value = provided.get(column);
|
|
3099
|
-
if (!value)
|
|
4126
|
+
if (!value) fail2(`pkColumns is missing '${column}'`, "invalid-pk-columns", meta.primaryKey);
|
|
3100
4127
|
return { column, value: this.lowerValue(value) };
|
|
3101
4128
|
});
|
|
3102
4129
|
}
|
|
@@ -3105,6 +4132,19 @@ function lowerOperation({ restricted, ctx }) {
|
|
|
3105
4132
|
const lowerer = new OperationLowerer(ctx, restricted);
|
|
3106
4133
|
return lowerer.lowerRoots(restricted.definition, restricted.kind);
|
|
3107
4134
|
}
|
|
4135
|
+
function isCellColumn(meta, name) {
|
|
4136
|
+
if (!meta.cells) return false;
|
|
4137
|
+
return Object.values(meta.cells).some((parts) => Object.values(parts).includes(name));
|
|
4138
|
+
}
|
|
4139
|
+
function fieldNodes(selectionSet) {
|
|
4140
|
+
const out = [];
|
|
4141
|
+
for (const node of selectionSet.selections) {
|
|
4142
|
+
if (node.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
4143
|
+
if (node.directives && node.directives.length > 0) fail2(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
|
|
4144
|
+
if (node.name.value !== "__typename") out.push(node);
|
|
4145
|
+
}
|
|
4146
|
+
return out;
|
|
4147
|
+
}
|
|
3108
4148
|
function parseSdl(sdl) {
|
|
3109
4149
|
return buildSchema(sdl);
|
|
3110
4150
|
}
|