@sequenceholdings/orm 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -2
- package/dist/authoring.cjs +1474 -54
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +242 -4
- package/dist/authoring.d.ts +242 -4
- package/dist/authoring.js +1465 -56
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +740 -42
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +742 -53
- package/dist/canonical.d.ts +742 -53
- package/dist/canonical.js +724 -43
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +2478 -328
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2480 -330
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs +90 -25
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +39 -22
- package/dist/client.d.ts +39 -22
- package/dist/client.js +87 -26
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +1755 -329
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +56 -3
- package/dist/compiler.d.ts +56 -3
- package/dist/compiler.js +1756 -330
- package/dist/compiler.js.map +1 -1
- package/dist/constants-YoFGjIBM.d.cts +99 -0
- package/dist/constants-YoFGjIBM.d.ts +99 -0
- package/dist/dynamic-input.d.cts +1 -0
- package/dist/dynamic-input.d.ts +1 -0
- package/dist/index.cjs +1973 -164
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +474 -100
- package/dist/index.d.ts +474 -100
- package/dist/index.js +1940 -167
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +869 -46
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +699 -25
- package/dist/ir.d.ts +699 -25
- package/dist/ir.js +861 -47
- package/dist/ir.js.map +1 -1
- package/dist/{naming-CTvWSRTf.d.ts → naming-CPfx7EfF.d.ts} +28 -1
- package/dist/{naming-DRyinU4u.d.cts → naming-CU9ThP_E.d.cts} +28 -1
- package/dist/policy-1iZbGnZZ.d.cts +24 -0
- package/dist/policy-1iZbGnZZ.d.ts +24 -0
- package/dist/predicate-v2.cjs +852 -49
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.d.cts +2 -1
- package/dist/predicate-v2.d.ts +2 -1
- package/dist/predicate-v2.js +852 -49
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +1309 -58
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.d.cts +7 -3
- package/dist/sdl.d.ts +7 -3
- package/dist/sdl.js +1305 -59
- package/dist/sdl.js.map +1 -1
- package/package.json +11 -6
- package/templates/orm-namespace/CLAUDE.md +6 -4
- package/templates/orm-namespace/codegen.preset.d.cts +7 -0
- package/templates/orm-namespace/codegen.ts +2 -1
- package/templates/orm-namespace/graphql/example/queries.ts +4 -3
- package/templates/orm-namespace/package.json +2 -1
- package/templates/orm-namespace/schema/tables.ts +3 -2
- package/templates/orm-namespace/sequence.config.ts +7 -11
package/dist/compiler.js
CHANGED
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { Kind, print, buildSchema, parse, validate, valueFromASTUntyped } from 'graphql';
|
|
3
|
+
import { Kind, print, visit, buildSchema, parse, validate, valueFromASTUntyped } from 'graphql';
|
|
4
4
|
|
|
5
5
|
// src/compiler-gql/artifact.ts
|
|
6
6
|
|
|
7
7
|
// src/constants.ts
|
|
8
|
-
var
|
|
8
|
+
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
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}$/;
|
|
9
11
|
var ORM_SCHEMA_PREFIX = "orm_";
|
|
10
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: [] });
|
|
11
28
|
|
|
12
29
|
// src/hash.ts
|
|
13
30
|
function stableJson(value) {
|
|
@@ -16,9 +33,469 @@ function stableJson(value) {
|
|
|
16
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)}`);
|
|
17
34
|
return `{${entries.join(",")}}`;
|
|
18
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
|
|
19
495
|
var CANONICAL_FORMAT = 2;
|
|
20
496
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
21
|
-
var
|
|
497
|
+
var identifier2 = z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
498
|
+
var columnIdentifier = z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
|
|
22
499
|
var COLUMN_TYPES = [
|
|
23
500
|
"uuid",
|
|
24
501
|
"text",
|
|
@@ -33,59 +510,71 @@ var COLUMN_TYPES = [
|
|
|
33
510
|
];
|
|
34
511
|
var columnTypeSchema = z.enum(COLUMN_TYPES);
|
|
35
512
|
var canonicalEnumSchema = z.object({
|
|
36
|
-
name:
|
|
513
|
+
name: identifier2,
|
|
37
514
|
values: z.array(z.string().min(1).max(63)).min(1).max(100)
|
|
38
515
|
});
|
|
39
516
|
var canonicalColumnSchema = z.object({
|
|
40
517
|
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
41
518
|
name: z.string().min(1).max(63),
|
|
42
|
-
columnName:
|
|
43
|
-
type: z.union([columnTypeSchema, z.object({ enum:
|
|
519
|
+
columnName: columnIdentifier,
|
|
520
|
+
type: z.union([columnTypeSchema, z.object({ enum: identifier2 })]),
|
|
44
521
|
nullable: z.boolean().default(true),
|
|
45
522
|
unique: z.boolean().default(false),
|
|
46
523
|
/** Literal default, or one of the allowlisted SQL functions. */
|
|
47
524
|
default: z.union([z.string(), z.number(), z.boolean(), z.null()]).optional(),
|
|
48
525
|
defaultFn: z.enum(["gen_random_uuid()", "now()"]).optional(),
|
|
526
|
+
/**
|
|
527
|
+
* Stamp this column from the database clock on UPDATE (`now()`). The
|
|
528
|
+
* column stays insertable (defaults fill gaps) but is omitted from
|
|
529
|
+
* SetInput / settable so callers cannot backdate it.
|
|
530
|
+
*/
|
|
531
|
+
onUpdateFn: z.enum(["now()"]).optional(),
|
|
49
532
|
description: z.string().max(500).optional(),
|
|
50
533
|
/** Migration hints — same semantics as v1. */
|
|
51
|
-
renamedFrom:
|
|
534
|
+
renamedFrom: columnIdentifier.optional(),
|
|
52
535
|
cast: z.string().max(200).optional(),
|
|
53
|
-
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()
|
|
544
|
+
}).superRefine((column, ctx) => {
|
|
545
|
+
if (column.onUpdateFn === void 0) return;
|
|
546
|
+
if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
|
|
547
|
+
ctx.addIssue({
|
|
548
|
+
code: "custom",
|
|
549
|
+
path: ["onUpdateFn"],
|
|
550
|
+
message: `onUpdateFn '${column.onUpdateFn}' is only valid on timestamptz/date columns`
|
|
551
|
+
});
|
|
552
|
+
}
|
|
54
553
|
});
|
|
55
554
|
var canonicalLinkSchema = z.object({
|
|
56
555
|
name: z.string().min(1).max(63),
|
|
57
556
|
kind: z.enum(["toOne", "toMany", "toPlatformUser"]),
|
|
58
557
|
/** Target table name (namespace-local). Absent for toPlatformUser. */
|
|
59
|
-
target:
|
|
558
|
+
target: identifier2.optional(),
|
|
60
559
|
/** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
|
|
61
|
-
on:
|
|
560
|
+
on: columnIdentifier,
|
|
62
561
|
/** Referenced column on the target (default: target's PK / platform user `sub`). */
|
|
63
|
-
references:
|
|
562
|
+
references: columnIdentifier.optional(),
|
|
64
563
|
/** enforce: true emits a real FK constraint (same-namespace toOne only). */
|
|
65
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(),
|
|
66
567
|
description: z.string().max(500).optional()
|
|
67
568
|
});
|
|
68
569
|
var canonicalIndexSchema = z.object({
|
|
69
|
-
columns: z.array(
|
|
570
|
+
columns: z.array(columnIdentifier).min(1),
|
|
70
571
|
unique: z.boolean().default(false),
|
|
71
|
-
name:
|
|
572
|
+
name: identifier2.optional()
|
|
72
573
|
});
|
|
73
|
-
var
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
allow: z.literal(true).optional(),
|
|
78
|
-
policy: z.string().min(1).max(2e3).optional()
|
|
79
|
-
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
80
|
-
message: "A variant declares exactly one of allow: true or policy"
|
|
81
|
-
});
|
|
82
|
-
var policyExpressionSchema = z.union([
|
|
83
|
-
z.string().min(1).max(2e3),
|
|
84
|
-
z.object({
|
|
85
|
-
default: z.string().min(1).max(2e3),
|
|
86
|
-
variants: z.array(policyVariantSchema).max(10).default([])
|
|
87
|
-
})
|
|
88
|
-
]);
|
|
574
|
+
var canonicalCheckSchema = z.object({
|
|
575
|
+
name: identifier2,
|
|
576
|
+
expression: z.string().min(1).max(2e3)
|
|
577
|
+
}).strict();
|
|
89
578
|
var canonicalPoliciesSchema = z.object({
|
|
90
579
|
read: policyExpressionSchema.optional(),
|
|
91
580
|
write: policyExpressionSchema.optional(),
|
|
@@ -116,7 +605,7 @@ function isSingleViewQuerySql(value) {
|
|
|
116
605
|
return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
|
|
117
606
|
}
|
|
118
607
|
var canonicalActionSchema = z.object({
|
|
119
|
-
name:
|
|
608
|
+
name: identifier2,
|
|
120
609
|
requires: z.string().max(1e3).optional(),
|
|
121
610
|
params: z.record(z.string(), z.object({
|
|
122
611
|
type: columnTypeSchema,
|
|
@@ -128,9 +617,9 @@ var canonicalActionSchema = z.object({
|
|
|
128
617
|
});
|
|
129
618
|
var canonicalCowMutationSchema = z.object({
|
|
130
619
|
kind: z.enum(["override", "revert"]),
|
|
131
|
-
name:
|
|
620
|
+
name: identifier2,
|
|
132
621
|
/** Synced table (same namespace) the overlay copies from / falls back to. */
|
|
133
|
-
synced:
|
|
622
|
+
synced: identifier2,
|
|
134
623
|
/** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
|
|
135
624
|
pk: z.string().min(1).max(63),
|
|
136
625
|
/** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
|
|
@@ -157,17 +646,28 @@ var canonicalCowMutationSchema = z.object({
|
|
|
157
646
|
}
|
|
158
647
|
}
|
|
159
648
|
});
|
|
649
|
+
var canonicalForeignKeySchema = z.object({
|
|
650
|
+
name: identifier2,
|
|
651
|
+
columns: z.array(columnIdentifier).min(1).max(8),
|
|
652
|
+
target: identifier2,
|
|
653
|
+
references: z.array(columnIdentifier).min(1).max(8),
|
|
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()
|
|
657
|
+
}).strict();
|
|
160
658
|
var canonicalTableSchema = z.object({
|
|
161
|
-
table:
|
|
659
|
+
table: identifier2,
|
|
162
660
|
description: z.string().max(2e3).optional(),
|
|
163
661
|
/** Table-level rename hint (new in v2). */
|
|
164
|
-
renamedFrom:
|
|
662
|
+
renamedFrom: identifier2.optional(),
|
|
165
663
|
/** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
|
|
166
664
|
source: z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
|
|
167
665
|
columns: z.array(canonicalColumnSchema).default([]),
|
|
168
|
-
primaryKey: z.array(
|
|
666
|
+
primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
|
|
169
667
|
links: z.array(canonicalLinkSchema).default([]),
|
|
170
668
|
indexes: z.array(canonicalIndexSchema).default([]),
|
|
669
|
+
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
670
|
+
checks: z.array(canonicalCheckSchema).max(100).optional(),
|
|
171
671
|
policies: canonicalPoliciesSchema.default({}),
|
|
172
672
|
/**
|
|
173
673
|
* v1-conversion carry-over only. v2 has no action surface — the runtime
|
|
@@ -176,26 +676,64 @@ var canonicalTableSchema = z.object({
|
|
|
176
676
|
*/
|
|
177
677
|
actions: z.array(canonicalActionSchema).default([]),
|
|
178
678
|
/** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
|
|
179
|
-
cowMutations: z.array(canonicalCowMutationSchema).optional()
|
|
679
|
+
cowMutations: z.array(canonicalCowMutationSchema).optional(),
|
|
680
|
+
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
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()
|
|
180
702
|
});
|
|
703
|
+
var MAX_VIEW_QUERY = 1e5;
|
|
704
|
+
var MAX_RESOLVED_VIEW_QUERY = 5e6;
|
|
181
705
|
var canonicalViewSchema = z.object({
|
|
182
|
-
view:
|
|
706
|
+
view: identifier2,
|
|
183
707
|
description: z.string().max(2e3).optional(),
|
|
184
708
|
columns: z.array(canonicalColumnSchema).min(1),
|
|
185
709
|
/** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
|
|
186
|
-
primaryKey: z.array(
|
|
710
|
+
primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
|
|
187
711
|
links: z.array(canonicalLinkSchema).default([]),
|
|
188
712
|
policies: z.object({ read: policyExpressionSchema }).strict(),
|
|
189
|
-
query: z.string().min(1).max(
|
|
713
|
+
query: z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
|
|
190
714
|
message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
|
|
191
715
|
}),
|
|
192
716
|
/** Namespace-local logical relation names, sorted for deterministic hashing. */
|
|
193
|
-
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()
|
|
194
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
|
+
}
|
|
195
730
|
for (const [index, column] of view.columns.entries()) {
|
|
196
731
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
197
732
|
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
|
|
198
733
|
}
|
|
734
|
+
if (column.onUpdateFn !== void 0) {
|
|
735
|
+
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare onUpdateFn` });
|
|
736
|
+
}
|
|
199
737
|
if (column.unique) {
|
|
200
738
|
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare unique` });
|
|
201
739
|
}
|
|
@@ -203,15 +741,82 @@ var canonicalViewSchema = z.object({
|
|
|
203
741
|
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare migration hints` });
|
|
204
742
|
}
|
|
205
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
|
+
}
|
|
206
749
|
for (const [index, link] of view.links.entries()) {
|
|
207
750
|
if (link.enforce) {
|
|
208
751
|
ctx.addIssue({ code: "custom", path: ["links", index, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
|
|
209
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
|
+
}
|
|
210
756
|
}
|
|
211
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
|
+
}
|
|
212
817
|
var canonicalDefinitionSchema = z.object({
|
|
213
818
|
definition_format: z.literal(CANONICAL_FORMAT),
|
|
214
|
-
namespace:
|
|
819
|
+
namespace: identifier2.max(41),
|
|
215
820
|
description: z.string().max(2e3).optional(),
|
|
216
821
|
enums: z.array(canonicalEnumSchema).default([]),
|
|
217
822
|
tables: z.array(canonicalTableSchema).min(0).max(500),
|
|
@@ -229,6 +834,74 @@ var canonicalDefinitionSchema = z.object({
|
|
|
229
834
|
ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
|
|
230
835
|
}
|
|
231
836
|
seen.add(table.table);
|
|
837
|
+
validateCellMetadata(table, index, ctx);
|
|
838
|
+
if (table.source !== void 0) {
|
|
839
|
+
for (const [columnIndex, column] of table.columns.entries()) {
|
|
840
|
+
if (column.onUpdateFn !== void 0) {
|
|
841
|
+
ctx.addIssue({
|
|
842
|
+
code: "custom",
|
|
843
|
+
path: ["tables", index, "columns", columnIndex],
|
|
844
|
+
message: `synced table '${table.table}' column '${column.name}' cannot declare onUpdateFn`
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
851
|
+
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
852
|
+
const names = /* @__PURE__ */ new Set([
|
|
853
|
+
...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
|
|
854
|
+
...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
|
|
855
|
+
...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
|
|
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
|
+
}
|
|
877
|
+
for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
|
|
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}'`);
|
|
880
|
+
names.add(fk.name);
|
|
881
|
+
const target = fkTables.get(fk.target);
|
|
882
|
+
if (table.source || !target || target.source) {
|
|
883
|
+
fail3("foreign keys must connect owned tables in the same namespace");
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
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");
|
|
888
|
+
for (const [position, name] of fk.columns.entries()) {
|
|
889
|
+
const sourceColumn = table.columns.find((column) => column.columnName === name);
|
|
890
|
+
const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
|
|
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");
|
|
896
|
+
}
|
|
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])];
|
|
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 });
|
|
904
|
+
}
|
|
232
905
|
}
|
|
233
906
|
const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
234
907
|
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
@@ -324,7 +997,7 @@ var canonicalDefinitionSchema = z.object({
|
|
|
324
997
|
const visiting = /* @__PURE__ */ new Set();
|
|
325
998
|
const visited = /* @__PURE__ */ new Set();
|
|
326
999
|
let cycleReported = false;
|
|
327
|
-
const
|
|
1000
|
+
const visit2 = (name, path) => {
|
|
328
1001
|
if (visited.has(name) || cycleReported) return;
|
|
329
1002
|
if (visiting.has(name)) {
|
|
330
1003
|
cycleReported = true;
|
|
@@ -338,12 +1011,12 @@ var canonicalDefinitionSchema = z.object({
|
|
|
338
1011
|
visiting.add(name);
|
|
339
1012
|
const view = viewByName.get(name);
|
|
340
1013
|
for (const dependency of view?.dependencies ?? []) {
|
|
341
|
-
if (viewByName.has(dependency))
|
|
1014
|
+
if (viewByName.has(dependency)) visit2(dependency, [...path, name]);
|
|
342
1015
|
}
|
|
343
1016
|
visiting.delete(name);
|
|
344
1017
|
visited.add(name);
|
|
345
1018
|
};
|
|
346
|
-
for (const view of views)
|
|
1019
|
+
for (const view of views) visit2(view.view, []);
|
|
347
1020
|
});
|
|
348
1021
|
function canonicalRelations(definition) {
|
|
349
1022
|
const views = (definition.views ?? []).map((view) => ({
|
|
@@ -353,8 +1026,12 @@ function canonicalRelations(definition) {
|
|
|
353
1026
|
...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
|
|
354
1027
|
links: view.links,
|
|
355
1028
|
indexes: [],
|
|
356
|
-
|
|
357
|
-
|
|
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 } : {}
|
|
358
1035
|
}));
|
|
359
1036
|
return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
|
|
360
1037
|
}
|
|
@@ -364,12 +1041,14 @@ function normalizeCanonical(definition) {
|
|
|
364
1041
|
...definition,
|
|
365
1042
|
enums: sortByName(definition.enums),
|
|
366
1043
|
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t) => {
|
|
367
|
-
const { cowMutations, ...rest } = t;
|
|
1044
|
+
const { cowMutations, foreignKeys, checks, ...rest } = t;
|
|
368
1045
|
return {
|
|
369
1046
|
...rest,
|
|
370
1047
|
links: sortByName(t.links),
|
|
371
1048
|
indexes: [...t.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
|
|
372
1049
|
actions: sortByName(t.actions),
|
|
1050
|
+
...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
|
|
1051
|
+
...checks?.length ? { checks: sortByName(checks) } : {},
|
|
373
1052
|
// Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
|
|
374
1053
|
...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
|
|
375
1054
|
};
|
|
@@ -386,7 +1065,18 @@ function normalizeCanonical(definition) {
|
|
|
386
1065
|
}
|
|
387
1066
|
|
|
388
1067
|
// src/ir/index.ts
|
|
389
|
-
var IR_VERSION =
|
|
1068
|
+
var IR_VERSION = 2;
|
|
1069
|
+
var LEGACY_IR_VERSION = 1;
|
|
1070
|
+
var irVersionSchema = z.union([z.literal(LEGACY_IR_VERSION), z.literal(IR_VERSION)]);
|
|
1071
|
+
var operationVersionSchema = z.object({ irVersion: irVersionSchema });
|
|
1072
|
+
function requiredArtifactIrVersion(operations) {
|
|
1073
|
+
let required = LEGACY_IR_VERSION;
|
|
1074
|
+
for (const operation of operations) {
|
|
1075
|
+
const { irVersion } = operationVersionSchema.parse(operation);
|
|
1076
|
+
if (irVersion > required) required = irVersion;
|
|
1077
|
+
}
|
|
1078
|
+
return required;
|
|
1079
|
+
}
|
|
390
1080
|
var CLAIMS_CONTRACT = {
|
|
391
1081
|
sub: "string",
|
|
392
1082
|
org: "string?",
|
|
@@ -458,6 +1148,21 @@ var selectionIrSchema = z.lazy(
|
|
|
458
1148
|
() => z.object({
|
|
459
1149
|
typename: z.string(),
|
|
460
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(),
|
|
461
1166
|
relations: z.array(z.object({
|
|
462
1167
|
alias: z.string(),
|
|
463
1168
|
link: z.string(),
|
|
@@ -479,6 +1184,11 @@ var selectionIrSchema = z.lazy(
|
|
|
479
1184
|
})
|
|
480
1185
|
);
|
|
481
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
|
+
});
|
|
482
1192
|
var selectRootSchema = z.object({
|
|
483
1193
|
kind: z.literal("select"),
|
|
484
1194
|
alias: z.string(),
|
|
@@ -490,7 +1200,8 @@ var selectRootSchema = z.object({
|
|
|
490
1200
|
distinctOn: z.union([z.array(z.string()), z.object({ slot: z.string() })]).optional(),
|
|
491
1201
|
limit: limitIrSchema,
|
|
492
1202
|
offset: z.object({ variable: z.string().optional(), value: z.number().int().optional() }).optional(),
|
|
493
|
-
includeIf: includeIfSchema.optional()
|
|
1203
|
+
includeIf: includeIfSchema.optional(),
|
|
1204
|
+
prefetch: z.array(prefetchPlanIrSchema).optional()
|
|
494
1205
|
});
|
|
495
1206
|
var byPkRootSchema = z.object({
|
|
496
1207
|
kind: z.literal("byPk"),
|
|
@@ -500,7 +1211,8 @@ var byPkRootSchema = z.object({
|
|
|
500
1211
|
pk: z.array(z.object({ column: z.string(), value: valueIrSchema })).min(1),
|
|
501
1212
|
/** Policy predicate — ANDed with the PK match. */
|
|
502
1213
|
where: predIrSchema,
|
|
503
|
-
includeIf: includeIfSchema.optional()
|
|
1214
|
+
includeIf: includeIfSchema.optional(),
|
|
1215
|
+
prefetch: z.array(prefetchPlanIrSchema).optional()
|
|
504
1216
|
});
|
|
505
1217
|
var aggregateRootSchema = z.object({
|
|
506
1218
|
kind: z.literal("aggregate"),
|
|
@@ -513,6 +1225,33 @@ var aggregateRootSchema = z.object({
|
|
|
513
1225
|
nodes: selectionIrSchema.optional(),
|
|
514
1226
|
includeIf: includeIfSchema.optional()
|
|
515
1227
|
});
|
|
1228
|
+
var DATE_BUCKETS = ["year", "quarter", "month", "week", "day", "hour"];
|
|
1229
|
+
var groupByKeyIrSchema = z.object({
|
|
1230
|
+
column: z.string(),
|
|
1231
|
+
bucket: z.enum(DATE_BUCKETS).optional()
|
|
1232
|
+
});
|
|
1233
|
+
var aggregateGroupsRootSchema = z.object({
|
|
1234
|
+
kind: z.literal("aggregateGroups"),
|
|
1235
|
+
alias: z.string(),
|
|
1236
|
+
table: z.string(),
|
|
1237
|
+
/** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
|
|
1238
|
+
where: predIrSchema,
|
|
1239
|
+
groupBy: z.array(groupByKeyIrSchema).min(1),
|
|
1240
|
+
/**
|
|
1241
|
+
* The `key { … }` projection: which group columns appear in each entry's key
|
|
1242
|
+
* object, in selection order. A subset of `groupBy` (the compiler rejects a
|
|
1243
|
+
* key field that is not a group key — it would be indeterminate within the
|
|
1244
|
+
* group). Empty when the document selects only `aggregate`.
|
|
1245
|
+
*/
|
|
1246
|
+
key: z.array(z.object({ alias: z.string(), column: z.string() })).default([]),
|
|
1247
|
+
count: z.object({ columns: z.array(z.string()).optional(), distinct: z.boolean().optional() }).optional(),
|
|
1248
|
+
sum: z.array(z.string()).optional(),
|
|
1249
|
+
avg: z.array(z.string()).optional(),
|
|
1250
|
+
/** Bounds the GROUP count, not the scanned rows — see MAX_GROUPS. */
|
|
1251
|
+
limit: limitIrSchema,
|
|
1252
|
+
offset: z.object({ variable: z.string().optional(), value: z.number().int().optional() }).optional(),
|
|
1253
|
+
includeIf: includeIfSchema.optional()
|
|
1254
|
+
});
|
|
516
1255
|
var writeValuesSchema = z.union([
|
|
517
1256
|
z.object({ slot: z.string() }),
|
|
518
1257
|
z.object({ entries: z.array(z.object({ column: z.string(), value: valueIrSchema })) })
|
|
@@ -531,7 +1270,20 @@ var mutationRootSchema = z.object({
|
|
|
531
1270
|
* coalesce view falls back to synced. Both are gated on a matching synced
|
|
532
1271
|
* row and carry the write policy in `where` like update/delete.
|
|
533
1272
|
*/
|
|
534
|
-
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(),
|
|
535
1287
|
alias: z.string(),
|
|
536
1288
|
table: z.string(),
|
|
537
1289
|
/** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
|
|
@@ -558,12 +1310,15 @@ var mutationRootSchema = z.object({
|
|
|
558
1310
|
insertCheck: predIrSchema.optional(),
|
|
559
1311
|
returning: selectionIrSchema.nullable(),
|
|
560
1312
|
affectedRows: z.boolean().default(false),
|
|
1313
|
+
/** v2: abort the transaction when the actual write count differs, regardless of selection. */
|
|
1314
|
+
assertAffectedRows: z.number().int().min(0).max(2147483647).optional(),
|
|
561
1315
|
includeIf: includeIfSchema.optional()
|
|
562
1316
|
});
|
|
563
1317
|
var rootIrSchema = z.discriminatedUnion("kind", [
|
|
564
1318
|
selectRootSchema,
|
|
565
1319
|
byPkRootSchema,
|
|
566
1320
|
aggregateRootSchema,
|
|
1321
|
+
aggregateGroupsRootSchema,
|
|
567
1322
|
mutationRootSchema
|
|
568
1323
|
]);
|
|
569
1324
|
var INPUT_REF_KINDS = [
|
|
@@ -593,7 +1348,7 @@ var variableIrSchema = z.object({
|
|
|
593
1348
|
defaultValue: variableDefaultIrSchema.optional()
|
|
594
1349
|
});
|
|
595
1350
|
var compiledOperationSchema = z.object({
|
|
596
|
-
irVersion:
|
|
1351
|
+
irVersion: irVersionSchema,
|
|
597
1352
|
name: z.string(),
|
|
598
1353
|
opHash: z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
599
1354
|
kind: z.enum(["query", "mutation"]),
|
|
@@ -605,6 +1360,50 @@ var compiledOperationSchema = z.object({
|
|
|
605
1360
|
/** Every table the op touches — data-token scoping + audit. */
|
|
606
1361
|
tableSet: z.array(z.string()).min(1),
|
|
607
1362
|
roots: z.array(rootIrSchema).min(1)
|
|
1363
|
+
}).superRefine((operation, ctx) => {
|
|
1364
|
+
for (const [index, root] of operation.roots.entries()) {
|
|
1365
|
+
if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
|
|
1366
|
+
if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
|
|
1367
|
+
ctx.addIssue({ code: "custom", path: ["roots", index], message: "affected-row assertions require unconditional mutation roots and IR v2" });
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
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()
|
|
608
1407
|
});
|
|
609
1408
|
var tableMetaIrSchema = z.object({
|
|
610
1409
|
table: z.string(),
|
|
@@ -641,18 +1440,45 @@ var tableMetaIrSchema = z.object({
|
|
|
641
1440
|
insertable: z.array(z.string()),
|
|
642
1441
|
settable: z.array(z.string()),
|
|
643
1442
|
appendable: z.array(z.string()),
|
|
1443
|
+
/**
|
|
1444
|
+
* Logical columns stamped with Postgres `now()` on UPDATE (canonical
|
|
1445
|
+
* `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
|
|
1446
|
+
*/
|
|
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(),
|
|
644
1464
|
constraints: z.array(z.object({ name: z.string(), columns: z.array(z.string()) })),
|
|
645
1465
|
/** Depth/node caps for dynamic filter trees on this table. */
|
|
646
1466
|
dynamicFilterMaxDepth: z.number().int().positive().default(6),
|
|
647
1467
|
dynamicFilterMaxNodes: z.number().int().positive().default(64)
|
|
648
1468
|
});
|
|
649
1469
|
var compiledArtifactSchema = z.object({
|
|
650
|
-
irVersion:
|
|
1470
|
+
irVersion: irVersionSchema,
|
|
651
1471
|
namespace: z.string(),
|
|
652
1472
|
schemaHash: z.string(),
|
|
653
1473
|
opsHash: z.string(),
|
|
654
1474
|
tables: z.record(z.string(), tableMetaIrSchema),
|
|
655
1475
|
operations: z.record(z.string(), compiledOperationSchema)
|
|
1476
|
+
}).superRefine((artifact, ctx) => {
|
|
1477
|
+
for (const [name, operation] of Object.entries(artifact.operations)) {
|
|
1478
|
+
if (operation.irVersion > artifact.irVersion) {
|
|
1479
|
+
ctx.addIssue({ code: "custom", path: ["operations", name, "irVersion"], message: "operation requires a newer IR version than its artifact" });
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
656
1482
|
});
|
|
657
1483
|
var IrVersionError = class extends Error {
|
|
658
1484
|
constructor(found) {
|
|
@@ -663,19 +1489,128 @@ var IrVersionError = class extends Error {
|
|
|
663
1489
|
function normalizeIr(raw) {
|
|
664
1490
|
if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
|
|
665
1491
|
const version = raw.irVersion;
|
|
666
|
-
if (version !== IR_VERSION) throw new IrVersionError(version);
|
|
1492
|
+
if (version !== LEGACY_IR_VERSION && version !== IR_VERSION) throw new IrVersionError(version);
|
|
667
1493
|
return compiledArtifactSchema.parse(raw);
|
|
668
1494
|
}
|
|
669
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
|
+
|
|
670
1605
|
// src/predicate-v2/ast.ts
|
|
671
1606
|
var POLICY_COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
|
|
672
|
-
function walkPolicy(node,
|
|
673
|
-
|
|
1607
|
+
function walkPolicy(node, visit2) {
|
|
1608
|
+
visit2(node);
|
|
674
1609
|
if (node.kind === "and" || node.kind === "or") {
|
|
675
|
-
for (const operand of node.operands) walkPolicy(operand,
|
|
1610
|
+
for (const operand of node.operands) walkPolicy(operand, visit2);
|
|
676
1611
|
}
|
|
677
|
-
if (node.kind === "not") walkPolicy(node.operand,
|
|
678
|
-
if (node.kind === "exists") walkPolicy(node.where,
|
|
1612
|
+
if (node.kind === "not") walkPolicy(node.operand, visit2);
|
|
1613
|
+
if (node.kind === "exists") walkPolicy(node.where, visit2);
|
|
679
1614
|
}
|
|
680
1615
|
|
|
681
1616
|
// src/predicate-v2/errors.ts
|
|
@@ -703,7 +1638,7 @@ var IDENTIFIER_CHAR = /[a-zA-Z0-9_]/;
|
|
|
703
1638
|
var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
|
|
704
1639
|
var SCALAR_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => !type.endsWith("[]")).map(([key]) => key);
|
|
705
1640
|
var ARRAY_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => type.endsWith("[]")).map(([key]) => key);
|
|
706
|
-
function
|
|
1641
|
+
function tokenize2(source) {
|
|
707
1642
|
const tokens = [];
|
|
708
1643
|
let position = 0;
|
|
709
1644
|
while (position < source.length) {
|
|
@@ -782,7 +1717,7 @@ function readString(source, start) {
|
|
|
782
1717
|
}
|
|
783
1718
|
throw new PolicyParseError("unterminated string literal", start);
|
|
784
1719
|
}
|
|
785
|
-
var
|
|
1720
|
+
var Parser2 = class {
|
|
786
1721
|
constructor(tokens) {
|
|
787
1722
|
this.tokens = tokens;
|
|
788
1723
|
}
|
|
@@ -956,7 +1891,7 @@ var Parser = class {
|
|
|
956
1891
|
function parsePolicy(source) {
|
|
957
1892
|
const trimmed = source.trim();
|
|
958
1893
|
if (trimmed.length === 0) throw new PolicyParseError("empty policy expression", 0);
|
|
959
|
-
return { source: trimmed, ast: new
|
|
1894
|
+
return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
|
|
960
1895
|
}
|
|
961
1896
|
|
|
962
1897
|
// src/predicate-v2/compile.ts
|
|
@@ -1181,21 +2116,21 @@ function andPred(...preds) {
|
|
|
1181
2116
|
if (operands.length === 1) return operands[0];
|
|
1182
2117
|
return { kind: "and", operands };
|
|
1183
2118
|
}
|
|
1184
|
-
function walkPred(pred,
|
|
1185
|
-
|
|
2119
|
+
function walkPred(pred, visit2) {
|
|
2120
|
+
visit2(pred);
|
|
1186
2121
|
switch (pred.kind) {
|
|
1187
2122
|
case "and":
|
|
1188
2123
|
case "or":
|
|
1189
|
-
for (const operand of pred.operands) walkPred(operand,
|
|
2124
|
+
for (const operand of pred.operands) walkPred(operand, visit2);
|
|
1190
2125
|
return;
|
|
1191
2126
|
case "not":
|
|
1192
|
-
walkPred(pred.operand,
|
|
2127
|
+
walkPred(pred.operand, visit2);
|
|
1193
2128
|
return;
|
|
1194
2129
|
case "exists":
|
|
1195
|
-
walkPred(pred.where,
|
|
2130
|
+
walkPred(pred.where, visit2);
|
|
1196
2131
|
return;
|
|
1197
2132
|
case "aggPredicate":
|
|
1198
|
-
if (pred.where) walkPred(pred.where,
|
|
2133
|
+
if (pred.where) walkPred(pred.where, visit2);
|
|
1199
2134
|
return;
|
|
1200
2135
|
default:
|
|
1201
2136
|
return;
|
|
@@ -1220,12 +2155,12 @@ function predContains(haystack, needle) {
|
|
|
1220
2155
|
// src/compiler-gql/inject.ts
|
|
1221
2156
|
function collectPinnedColumns(writePolicy) {
|
|
1222
2157
|
const key = (pin) => `${pin.column}\0${pin.claim}`;
|
|
1223
|
-
const
|
|
2158
|
+
const visit2 = (node) => {
|
|
1224
2159
|
if (node.kind === "and") {
|
|
1225
2160
|
const merged = [];
|
|
1226
2161
|
const seen2 = /* @__PURE__ */ new Set();
|
|
1227
2162
|
for (const operand of node.operands) {
|
|
1228
|
-
for (const pin of
|
|
2163
|
+
for (const pin of visit2(operand)) {
|
|
1229
2164
|
if (seen2.has(key(pin))) continue;
|
|
1230
2165
|
seen2.add(key(pin));
|
|
1231
2166
|
merged.push(pin);
|
|
@@ -1236,9 +2171,9 @@ function collectPinnedColumns(writePolicy) {
|
|
|
1236
2171
|
if (node.kind === "or") {
|
|
1237
2172
|
const [first, ...rest] = node.operands;
|
|
1238
2173
|
if (first === void 0) return [];
|
|
1239
|
-
let common =
|
|
2174
|
+
let common = visit2(first);
|
|
1240
2175
|
for (const operand of rest) {
|
|
1241
|
-
const branch = new Set(
|
|
2176
|
+
const branch = new Set(visit2(operand).map(key));
|
|
1242
2177
|
common = common.filter((pin) => branch.has(key(pin)));
|
|
1243
2178
|
if (common.length === 0) break;
|
|
1244
2179
|
}
|
|
@@ -1250,7 +2185,7 @@ function collectPinnedColumns(writePolicy) {
|
|
|
1250
2185
|
return [];
|
|
1251
2186
|
};
|
|
1252
2187
|
const seen = /* @__PURE__ */ new Set();
|
|
1253
|
-
return
|
|
2188
|
+
return visit2(writePolicy).filter((pin) => {
|
|
1254
2189
|
if (seen.has(pin.column)) return false;
|
|
1255
2190
|
seen.add(pin.column);
|
|
1256
2191
|
return true;
|
|
@@ -1344,6 +2279,11 @@ var PolicyInjector = class {
|
|
|
1344
2279
|
where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table)),
|
|
1345
2280
|
...root.nodes ? { nodes: this.injectSelection(root.nodes, root.table) } : {}
|
|
1346
2281
|
};
|
|
2282
|
+
case "aggregateGroups":
|
|
2283
|
+
return {
|
|
2284
|
+
...root,
|
|
2285
|
+
where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table))
|
|
2286
|
+
};
|
|
1347
2287
|
case "mutation":
|
|
1348
2288
|
return this.injectMutation(root);
|
|
1349
2289
|
}
|
|
@@ -1351,6 +2291,7 @@ var PolicyInjector = class {
|
|
|
1351
2291
|
injectMutation(root) {
|
|
1352
2292
|
const table = root.table;
|
|
1353
2293
|
const returning = root.returning ? this.injectSelection(root.returning, table) : root.returning;
|
|
2294
|
+
if (root.op === "resolvedWrite") return this.injectResolvedWrite(root, returning);
|
|
1354
2295
|
if (root.op === "insert" || root.op === "insertOne") {
|
|
1355
2296
|
const insertPolicy = this.insert(table);
|
|
1356
2297
|
if (predContainsExists(insertPolicy)) {
|
|
@@ -1392,6 +2333,24 @@ var PolicyInjector = class {
|
|
|
1392
2333
|
}
|
|
1393
2334
|
return { ...root, where, returning };
|
|
1394
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
|
+
}
|
|
1395
2354
|
};
|
|
1396
2355
|
function collectTableSet(roots, ctx) {
|
|
1397
2356
|
const tables = /* @__PURE__ */ new Set();
|
|
@@ -1434,122 +2393,44 @@ function collectTableSet(roots, ctx) {
|
|
|
1434
2393
|
if (aggregate.nodes) visitSelection(aggregate.nodes, target);
|
|
1435
2394
|
}
|
|
1436
2395
|
};
|
|
1437
|
-
for (const root of roots) {
|
|
1438
|
-
tables.add(root.table);
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
const head = first.charAt(0).toLowerCase() + first.slice(1);
|
|
1469
|
-
return head + segments.slice(1).map(capitalizeFirst).join("");
|
|
1470
|
-
}
|
|
1471
|
-
function pascalCase(name) {
|
|
1472
|
-
return capitalizeFirst(camelCase(name));
|
|
1473
|
-
}
|
|
1474
|
-
function capitalizeFirst(segment) {
|
|
1475
|
-
return segment.charAt(0).toUpperCase() + segment.slice(1);
|
|
1476
|
-
}
|
|
1477
|
-
function singularizeWord(word) {
|
|
1478
|
-
if (word.endsWith("ies") && word.length > 3) return `${word.slice(0, -3)}y`;
|
|
1479
|
-
for (const suffix of ["sses", "xes", "ches", "shes"]) {
|
|
1480
|
-
if (word.endsWith(suffix)) return word.slice(0, -2);
|
|
2396
|
+
for (const root of roots) {
|
|
2397
|
+
tables.add(root.table);
|
|
2398
|
+
switch (root.kind) {
|
|
2399
|
+
case "select":
|
|
2400
|
+
case "byPk":
|
|
2401
|
+
visitPred(root.where, root.table);
|
|
2402
|
+
visitSelection(root.selection, root.table);
|
|
2403
|
+
break;
|
|
2404
|
+
case "aggregate":
|
|
2405
|
+
visitPred(root.where, root.table);
|
|
2406
|
+
if (root.nodes) visitSelection(root.nodes, root.table);
|
|
2407
|
+
break;
|
|
2408
|
+
case "aggregateGroups":
|
|
2409
|
+
visitPred(root.where, root.table);
|
|
2410
|
+
break;
|
|
2411
|
+
case "mutation":
|
|
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);
|
|
2414
|
+
if (root.where) visitPred(root.where, root.table);
|
|
2415
|
+
if (root.insertCheck) visitPred(root.insertCheck, root.table);
|
|
2416
|
+
if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
|
|
2417
|
+
if (root.returning) visitSelection(root.returning, root.table);
|
|
2418
|
+
break;
|
|
2419
|
+
default: {
|
|
2420
|
+
const exhausted = root;
|
|
2421
|
+
throw new DocumentCompileError(
|
|
2422
|
+
`internal: unhandled root kind in tableSet collection ${JSON.stringify(exhausted)}`,
|
|
2423
|
+
"internal"
|
|
2424
|
+
);
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
1481
2427
|
}
|
|
1482
|
-
|
|
1483
|
-
if (word.endsWith("s") && word.length > 1) return word.slice(0, -1);
|
|
1484
|
-
return word;
|
|
1485
|
-
}
|
|
1486
|
-
function typeName(table) {
|
|
1487
|
-
const segments = table.split("_").filter((segment) => segment.length > 0);
|
|
1488
|
-
const last = segments[segments.length - 1];
|
|
1489
|
-
if (last === void 0) return pascalCase(table);
|
|
1490
|
-
return [...segments.slice(0, -1), singularizeWord(last)].map(capitalizeFirst).join("");
|
|
1491
|
-
}
|
|
1492
|
-
function enumTypeName(name) {
|
|
1493
|
-
return pascalCase(name);
|
|
1494
|
-
}
|
|
1495
|
-
var SCALAR_FOR_COLUMN_TYPE = {
|
|
1496
|
-
uuid: "uuid",
|
|
1497
|
-
text: "String",
|
|
1498
|
-
integer: "Int",
|
|
1499
|
-
bigint: "bigint",
|
|
1500
|
-
numeric: "numeric",
|
|
1501
|
-
double: "Float",
|
|
1502
|
-
boolean: "Boolean",
|
|
1503
|
-
timestamptz: "timestamptz",
|
|
1504
|
-
date: "date",
|
|
1505
|
-
jsonb: "jsonb"
|
|
1506
|
-
};
|
|
1507
|
-
function graphqlScalarFor(type) {
|
|
1508
|
-
return SCALAR_FOR_COLUMN_TYPE[type];
|
|
1509
|
-
}
|
|
1510
|
-
var COLUMN_TYPE_BY_SCALAR = new Map([
|
|
1511
|
-
["ID", "text"],
|
|
1512
|
-
...COLUMN_TYPES.map((type) => [graphqlScalarFor(type), type])
|
|
1513
|
-
]);
|
|
1514
|
-
function columnTypeForScalar(scalarName) {
|
|
1515
|
-
return COLUMN_TYPE_BY_SCALAR.get(scalarName);
|
|
2428
|
+
return [...tables].sort();
|
|
1516
2429
|
}
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
const rootPascal = pascalCase(table);
|
|
1522
|
-
const type = typeName(table);
|
|
1523
|
-
return {
|
|
1524
|
-
typename: type,
|
|
1525
|
-
selectRoot: root,
|
|
1526
|
-
byPkRoot: `${root}ByPk`,
|
|
1527
|
-
aggregateRoot: `${root}Aggregate`,
|
|
1528
|
-
insertRoot: `insert${rootPascal}`,
|
|
1529
|
-
insertOneRoot: `insert${rootPascal}One`,
|
|
1530
|
-
updateRoot: `update${rootPascal}`,
|
|
1531
|
-
updateByPkRoot: `update${rootPascal}ByPk`,
|
|
1532
|
-
deleteRoot: `delete${rootPascal}`,
|
|
1533
|
-
deleteByPkRoot: `delete${rootPascal}ByPk`,
|
|
1534
|
-
boolExp: `${type}BoolExp`,
|
|
1535
|
-
orderBy: `${type}OrderBy`,
|
|
1536
|
-
selectColumn: `${type}SelectColumn`,
|
|
1537
|
-
insertInput: `${type}InsertInput`,
|
|
1538
|
-
setInput: `${type}SetInput`,
|
|
1539
|
-
appendInput: `${type}AppendInput`,
|
|
1540
|
-
incInput: `${type}IncInput`,
|
|
1541
|
-
onConflict: `${type}OnConflict`,
|
|
1542
|
-
pkColumnsInput: `${type}PkColumnsInput`,
|
|
1543
|
-
constraintEnum: `${type}Constraint`,
|
|
1544
|
-
updateColumnEnum: `${type}UpdateColumn`,
|
|
1545
|
-
mutationResponse: `${type}MutationResponse`,
|
|
1546
|
-
aggregate: `${type}Aggregate`,
|
|
1547
|
-
aggregateFields: `${type}AggregateFields`,
|
|
1548
|
-
sumFields: `${type}SumFields`,
|
|
1549
|
-
avgFields: `${type}AvgFields`,
|
|
1550
|
-
aggregateBoolExp: `${type}AggregateBoolExp`,
|
|
1551
|
-
aggregateBoolExpCount: `${type}AggregateBoolExpCount`
|
|
1552
|
-
};
|
|
2430
|
+
function injectPolicies({ roots, ctx }) {
|
|
2431
|
+
const injector = new PolicyInjector(ctx);
|
|
2432
|
+
const injected = roots.map((root) => injector.injectRoot(root));
|
|
2433
|
+
return { roots: injected, tableSet: collectTableSet(injected, ctx) };
|
|
1553
2434
|
}
|
|
1554
2435
|
|
|
1555
2436
|
// src/compiler-gql/naming.ts
|
|
@@ -1559,7 +2440,12 @@ function typeNameForTable(table) {
|
|
|
1559
2440
|
}
|
|
1560
2441
|
function queryRootNames(table) {
|
|
1561
2442
|
const names = tableNames(table);
|
|
1562
|
-
return {
|
|
2443
|
+
return {
|
|
2444
|
+
select: names.selectRoot,
|
|
2445
|
+
byPk: names.byPkRoot,
|
|
2446
|
+
aggregate: names.aggregateRoot,
|
|
2447
|
+
aggregateGroups: names.aggregateGroupsRoot
|
|
2448
|
+
};
|
|
1563
2449
|
}
|
|
1564
2450
|
function mutationRootNames(table) {
|
|
1565
2451
|
const names = tableNames(table);
|
|
@@ -1652,7 +2538,15 @@ function resolveColumnEntries(table, enums, diagnostics) {
|
|
|
1652
2538
|
type = { enum: enumDef.name, values: [...enumDef.values] };
|
|
1653
2539
|
}
|
|
1654
2540
|
}
|
|
1655
|
-
return {
|
|
2541
|
+
return {
|
|
2542
|
+
name: column.name,
|
|
2543
|
+
columnName: column.columnName,
|
|
2544
|
+
type,
|
|
2545
|
+
nullable: column.nullable,
|
|
2546
|
+
deprecated: column.deprecated,
|
|
2547
|
+
onUpdateFn: column.onUpdateFn !== void 0,
|
|
2548
|
+
...column.cell ? { cell: column.cell } : {}
|
|
2549
|
+
};
|
|
1656
2550
|
});
|
|
1657
2551
|
}
|
|
1658
2552
|
function logicalColumn(entries, ref) {
|
|
@@ -1688,8 +2582,8 @@ function buildCompileContext(rawCanonical) {
|
|
|
1688
2582
|
const pkRefs = table.primaryKey ?? (isView ? [] : ["id"]);
|
|
1689
2583
|
const primaryKey = [];
|
|
1690
2584
|
for (const ref of pkRefs) {
|
|
1691
|
-
const
|
|
1692
|
-
if (!
|
|
2585
|
+
const resolved2 = logicalColumn(entries, ref);
|
|
2586
|
+
if (!resolved2) {
|
|
1693
2587
|
diagnostics.push({
|
|
1694
2588
|
severity: "error",
|
|
1695
2589
|
code: "unknown-pk-column",
|
|
@@ -1698,9 +2592,13 @@ function buildCompileContext(rawCanonical) {
|
|
|
1698
2592
|
});
|
|
1699
2593
|
continue;
|
|
1700
2594
|
}
|
|
1701
|
-
primaryKey.push(
|
|
2595
|
+
primaryKey.push(resolved2);
|
|
1702
2596
|
}
|
|
1703
|
-
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);
|
|
2601
|
+
const touchOnUpdate = isView ? [] : active.filter((c) => c.onUpdateFn).map((c) => c.name);
|
|
1704
2602
|
const constraints = [];
|
|
1705
2603
|
if (primaryKey.length > 0) constraints.push({ name: `${table.table}_pkey`, columns: [...primaryKey] });
|
|
1706
2604
|
for (const column of table.columns) {
|
|
@@ -1721,14 +2619,17 @@ function buildCompileContext(rawCanonical) {
|
|
|
1721
2619
|
// Synced twins (and views reading them, resolved transitively below)
|
|
1722
2620
|
// are claims-gated — see tableMetaIrSchema.claimsGated.
|
|
1723
2621
|
claimsGated: relationIsClaimsGated(table.table),
|
|
1724
|
-
columns: entries.map(({ deprecated: _deprecated, ...column }) => column),
|
|
2622
|
+
columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
|
|
1725
2623
|
primaryKey: isView ? primaryKey : primaryKey.length > 0 ? primaryKey : ["id"],
|
|
1726
2624
|
links: [],
|
|
1727
|
-
filterable:
|
|
1728
|
-
orderable:
|
|
1729
|
-
insertable: isView ? [] : active.map((c) => c.name),
|
|
1730
|
-
settable: isView ? [] : active.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),
|
|
1731
2629
|
appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
|
|
2630
|
+
...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
|
|
2631
|
+
...cells ? { cells } : {},
|
|
2632
|
+
...resolved ? { resolved: resolvedMeta({ resolved, pk: primaryKey[0] }) } : {},
|
|
1732
2633
|
constraints: isView ? [] : constraints,
|
|
1733
2634
|
dynamicFilterMaxDepth: 6,
|
|
1734
2635
|
dynamicFilterMaxNodes: 64
|
|
@@ -1749,7 +2650,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
1749
2650
|
});
|
|
1750
2651
|
continue;
|
|
1751
2652
|
}
|
|
1752
|
-
const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false })) : columnEntries.get(targetName);
|
|
2653
|
+
const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false, onUpdateFn: false })) : columnEntries.get(targetName);
|
|
1753
2654
|
const onEntries = link.kind === "toMany" ? targetEntries : entries;
|
|
1754
2655
|
const referencesEntries = link.kind === "toMany" ? entries : targetEntries;
|
|
1755
2656
|
const on = logicalColumn(onEntries, link.on);
|
|
@@ -1801,7 +2702,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
1801
2702
|
compilePolicyFor("write", writePolicies);
|
|
1802
2703
|
compilePolicyFor("insert", insertPolicies);
|
|
1803
2704
|
compilePolicyFor("update", updatePolicies);
|
|
1804
|
-
if (isView) {
|
|
2705
|
+
if (isView && table.resolved === void 0) {
|
|
1805
2706
|
writePolicies.set(table.table, CONST_FALSE);
|
|
1806
2707
|
insertPolicies.set(table.table, CONST_FALSE);
|
|
1807
2708
|
updatePolicies.set(table.table, CONST_FALSE);
|
|
@@ -1840,18 +2741,23 @@ function buildCompileContext(rawCanonical) {
|
|
|
1840
2741
|
const queryRoots = /* @__PURE__ */ new Map();
|
|
1841
2742
|
const mutationRoots = /* @__PURE__ */ new Map();
|
|
1842
2743
|
for (const table of relations) {
|
|
2744
|
+
if (table.internal === true) continue;
|
|
1843
2745
|
const roots = queryRootNames(table.table);
|
|
1844
2746
|
queryRoots.set(roots.select, { kind: "select", table: table.table });
|
|
1845
2747
|
if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
|
|
1846
2748
|
queryRoots.set(roots.byPk, { kind: "byPk", table: table.table });
|
|
1847
2749
|
}
|
|
1848
2750
|
queryRoots.set(roots.aggregate, { kind: "aggregate", table: table.table });
|
|
2751
|
+
if (table.columns.some((column) => column.type !== "jsonb")) {
|
|
2752
|
+
queryRoots.set(roots.aggregateGroups, { kind: "aggregateGroups", table: table.table });
|
|
2753
|
+
}
|
|
1849
2754
|
const hasWritePolicy = table.policies.write !== void 0 || table.policies.insert !== void 0 || table.policies.update !== void 0;
|
|
1850
2755
|
if (!viewNames.has(table.table) && table.source === void 0 && hasWritePolicy) {
|
|
1851
2756
|
for (const [op, field] of Object.entries(mutationRootNames(table.table))) {
|
|
1852
2757
|
mutationRoots.set(field, { op, table: table.table });
|
|
1853
2758
|
}
|
|
1854
2759
|
}
|
|
2760
|
+
if (table.resolved !== void 0) registerResolvedRoots({ table, mutationRoots });
|
|
1855
2761
|
}
|
|
1856
2762
|
for (const table of relations) {
|
|
1857
2763
|
for (const mutation of table.cowMutations ?? []) {
|
|
@@ -1922,6 +2828,49 @@ function buildCompileContext(rawCanonical) {
|
|
|
1922
2828
|
diagnostics
|
|
1923
2829
|
};
|
|
1924
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
|
+
}
|
|
1925
2874
|
function flattenSelectionSet(selectionSet, fragments, stack) {
|
|
1926
2875
|
const selections = [];
|
|
1927
2876
|
for (const selection of selectionSet.selections) {
|
|
@@ -1979,8 +2928,148 @@ function computeOpsHash(opHashes) {
|
|
|
1979
2928
|
const digest = createHash("sha256").update(OPS_HASH_DOMAIN).update([...opHashes].sort().join("\n")).digest("hex");
|
|
1980
2929
|
return `sha256:${digest}`;
|
|
1981
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
|
|
1982
3061
|
var DEFAULT_LIMIT = 100;
|
|
1983
3062
|
var MAX_LIMIT = 1e3;
|
|
3063
|
+
var MAX_GROUPS = 1e3;
|
|
3064
|
+
var DATE_BUCKET_BY_ENUM = {
|
|
3065
|
+
YEAR: "year",
|
|
3066
|
+
QUARTER: "quarter",
|
|
3067
|
+
MONTH: "month",
|
|
3068
|
+
WEEK: "week",
|
|
3069
|
+
DAY: "day",
|
|
3070
|
+
HOUR: "hour"
|
|
3071
|
+
};
|
|
3072
|
+
var BUCKETABLE_TYPES = /* @__PURE__ */ new Set(["timestamptz", "date"]);
|
|
1984
3073
|
var BOOLEXP_OPS = {
|
|
1985
3074
|
_eq: "eq",
|
|
1986
3075
|
_neq: "neq",
|
|
@@ -2006,16 +3095,24 @@ var ORDER_DIRECTIONS = {
|
|
|
2006
3095
|
DESC_NULLS_LAST: { direction: "desc", nulls: "last" }
|
|
2007
3096
|
};
|
|
2008
3097
|
var NUMERIC_TYPES = /* @__PURE__ */ new Set(["integer", "bigint", "numeric", "double"]);
|
|
2009
|
-
function
|
|
3098
|
+
function fail2(message, code, candidates = []) {
|
|
2010
3099
|
throw new DocumentCompileError(message, code, candidates);
|
|
2011
3100
|
}
|
|
2012
3101
|
function argsByName(field) {
|
|
2013
3102
|
return new Map((field.arguments ?? []).map((arg) => [arg.name.value, arg]));
|
|
2014
3103
|
}
|
|
3104
|
+
function* aggregateBlockFields(selectionSet) {
|
|
3105
|
+
for (const node of selectionSet.selections) {
|
|
3106
|
+
if (node.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
3107
|
+
if (node.name.value === "__typename") continue;
|
|
3108
|
+
if (node.alias) fail2(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
|
|
3109
|
+
yield node;
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
2015
3112
|
function assertKnownArgs(field, allowed) {
|
|
2016
3113
|
for (const arg of field.arguments ?? []) {
|
|
2017
3114
|
if (!allowed.includes(arg.name.value)) {
|
|
2018
|
-
|
|
3115
|
+
fail2(`argument '${arg.name.value}' on '${field.name.value}' is not supported in v2.0`, "unsupported-argument", allowed);
|
|
2019
3116
|
}
|
|
2020
3117
|
}
|
|
2021
3118
|
}
|
|
@@ -2028,8 +3125,11 @@ var OperationLowerer = class {
|
|
|
2028
3125
|
meta(table) {
|
|
2029
3126
|
return this.ctx.tables[table];
|
|
2030
3127
|
}
|
|
3128
|
+
/** A data column — cell-metadata columns are reachable only through `cells { … }`. */
|
|
2031
3129
|
column(table, name) {
|
|
2032
|
-
|
|
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;
|
|
2033
3133
|
}
|
|
2034
3134
|
link(table, name) {
|
|
2035
3135
|
return this.meta(table).links.find((l) => l.name === name);
|
|
@@ -2037,7 +3137,8 @@ var OperationLowerer = class {
|
|
|
2037
3137
|
fieldCandidates(table) {
|
|
2038
3138
|
const meta = this.meta(table);
|
|
2039
3139
|
return [
|
|
2040
|
-
...meta.columns.map((c) => c.name),
|
|
3140
|
+
...meta.columns.filter((c) => !isCellColumn(meta, c.name)).map((c) => c.name),
|
|
3141
|
+
...meta.cells ? [CELLS_FIELD] : [],
|
|
2041
3142
|
...meta.links.map((l) => l.name),
|
|
2042
3143
|
...meta.links.filter((l) => l.kind === "toMany").map((l) => `${l.name}Aggregate`)
|
|
2043
3144
|
];
|
|
@@ -2046,7 +3147,7 @@ var OperationLowerer = class {
|
|
|
2046
3147
|
slot(name, input, table) {
|
|
2047
3148
|
const variable = this.variables.get(name);
|
|
2048
3149
|
if (!variable || variable.type.kind !== "inputRef" || variable.type.input !== input || variable.type.table !== table) {
|
|
2049
|
-
|
|
3150
|
+
fail2(`variable $${name} must be declared as the ${input} input type for table '${table}'`, "slot-mismatch");
|
|
2050
3151
|
}
|
|
2051
3152
|
return name;
|
|
2052
3153
|
}
|
|
@@ -2071,14 +3172,14 @@ var OperationLowerer = class {
|
|
|
2071
3172
|
const items = node.values.map((item) => {
|
|
2072
3173
|
const lowered = this.lowerValue(item);
|
|
2073
3174
|
if (lowered.kind !== "literal" || Array.isArray(lowered.value)) {
|
|
2074
|
-
|
|
3175
|
+
fail2("list literals must contain scalar literals only \u2014 pass the whole list as a variable instead", "unsupported-literal");
|
|
2075
3176
|
}
|
|
2076
3177
|
return lowered.value;
|
|
2077
3178
|
});
|
|
2078
3179
|
return { kind: "literal", value: items };
|
|
2079
3180
|
}
|
|
2080
3181
|
case Kind.OBJECT:
|
|
2081
|
-
|
|
3182
|
+
fail2("literal JSON objects are not supported in documents \u2014 pass the value via a variable", "unsupported-literal");
|
|
2082
3183
|
}
|
|
2083
3184
|
}
|
|
2084
3185
|
// -------------------------------------------------------------------------
|
|
@@ -2088,7 +3189,7 @@ var OperationLowerer = class {
|
|
|
2088
3189
|
if (node.kind === Kind.VARIABLE) {
|
|
2089
3190
|
return { kind: "dynamicFilter", slot: this.slot(node.name.value, "boolExp", table) };
|
|
2090
3191
|
}
|
|
2091
|
-
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");
|
|
2092
3193
|
const operands = [];
|
|
2093
3194
|
for (const field of node.fields) {
|
|
2094
3195
|
const key = field.name.value;
|
|
@@ -2119,45 +3220,45 @@ var OperationLowerer = class {
|
|
|
2119
3220
|
continue;
|
|
2120
3221
|
}
|
|
2121
3222
|
}
|
|
2122
|
-
|
|
3223
|
+
fail2(`unknown field '${key}' in a boolean expression on '${table}'`, "unknown-column", this.fieldCandidates(table));
|
|
2123
3224
|
}
|
|
2124
3225
|
if (operands.length === 0) return CONST_TRUE;
|
|
2125
3226
|
return operands.length === 1 ? operands[0] : { kind: "and", operands };
|
|
2126
3227
|
}
|
|
2127
3228
|
lowerComparison(node, column, table) {
|
|
2128
3229
|
if (node.kind !== Kind.OBJECT) {
|
|
2129
|
-
|
|
3230
|
+
fail2(`comparison on '${table}.${column}' must be an object like { _eq: \u2026 }`, "invalid-comparison", Object.keys(BOOLEXP_OPS));
|
|
2130
3231
|
}
|
|
2131
3232
|
return node.fields.map((field) => {
|
|
2132
3233
|
const op = BOOLEXP_OPS[field.name.value];
|
|
2133
3234
|
if (!op) {
|
|
2134
|
-
|
|
3235
|
+
fail2(`unknown comparison operator '${field.name.value}' on '${table}.${column}'`, "unknown-operator", Object.keys(BOOLEXP_OPS));
|
|
2135
3236
|
}
|
|
2136
3237
|
return { kind: "cmp", op, column, value: this.lowerValue(field.value) };
|
|
2137
3238
|
});
|
|
2138
3239
|
}
|
|
2139
3240
|
/** Hasura aggregation predicate: `{ count: { predicate: { _gt: 2 }, filter: {…} } }`. */
|
|
2140
3241
|
lowerAggPredicate(node, link, target) {
|
|
2141
|
-
if (node.kind !== Kind.OBJECT)
|
|
3242
|
+
if (node.kind !== Kind.OBJECT) fail2(`aggregate predicate through '${link}' must be an object`, "invalid-agg-predicate");
|
|
2142
3243
|
const countField = node.fields.find((f) => f.name.value === "count");
|
|
2143
3244
|
if (!countField || node.fields.length !== 1) {
|
|
2144
|
-
|
|
3245
|
+
fail2(`aggregate predicates support { count: \u2026 } only in v2.0 (through '${link}')`, "invalid-agg-predicate", ["count"]);
|
|
2145
3246
|
}
|
|
2146
|
-
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");
|
|
2147
3248
|
let predicate;
|
|
2148
3249
|
let where;
|
|
2149
3250
|
for (const field of countField.value.fields) {
|
|
2150
3251
|
if (field.name.value === "predicate") {
|
|
2151
3252
|
if (field.value.kind !== Kind.OBJECT || field.value.fields.length !== 1) {
|
|
2152
|
-
|
|
3253
|
+
fail2(`count predicate through '${link}' needs exactly one operator`, "invalid-agg-predicate", [...AGG_PREDICATE_OPS]);
|
|
2153
3254
|
}
|
|
2154
3255
|
const opField = field.value.fields[0];
|
|
2155
3256
|
const opName = opField.name.value;
|
|
2156
3257
|
if (!AGG_PREDICATE_OPS.includes(opName)) {
|
|
2157
|
-
|
|
3258
|
+
fail2(`aggregate count predicates support ${AGG_PREDICATE_OPS.join("/")} only, got '${opName}'`, "invalid-agg-predicate");
|
|
2158
3259
|
}
|
|
2159
3260
|
if (opField.value.kind !== Kind.INT) {
|
|
2160
|
-
|
|
3261
|
+
fail2(`aggregate count predicate value must be an integer literal`, "invalid-agg-predicate");
|
|
2161
3262
|
}
|
|
2162
3263
|
predicate = { op: opName.slice(1), value: Number(opField.value.value) };
|
|
2163
3264
|
continue;
|
|
@@ -2166,9 +3267,9 @@ var OperationLowerer = class {
|
|
|
2166
3267
|
where = this.lowerBoolExp(field.value, target);
|
|
2167
3268
|
continue;
|
|
2168
3269
|
}
|
|
2169
|
-
|
|
3270
|
+
fail2(`unknown key '${field.name.value}' in a count predicate`, "invalid-agg-predicate", ["predicate", "filter"]);
|
|
2170
3271
|
}
|
|
2171
|
-
if (!predicate)
|
|
3272
|
+
if (!predicate) fail2(`count predicate through '${link}' needs a { predicate: { _gt: n } }`, "invalid-agg-predicate");
|
|
2172
3273
|
return { kind: "aggPredicate", link, fn: "count", op: predicate.op, value: predicate.value, ...where ? { where } : {} };
|
|
2173
3274
|
}
|
|
2174
3275
|
lowerOrderBy(node, table) {
|
|
@@ -2178,7 +3279,7 @@ var OperationLowerer = class {
|
|
|
2178
3279
|
const items = node.kind === Kind.LIST ? node.values : [node];
|
|
2179
3280
|
const lowered = [];
|
|
2180
3281
|
for (const item of items) {
|
|
2181
|
-
if (item.kind !== Kind.OBJECT)
|
|
3282
|
+
if (item.kind !== Kind.OBJECT) fail2(`orderBy entries on '${table}' must be objects`, "invalid-orderby");
|
|
2182
3283
|
for (const field of item.fields) {
|
|
2183
3284
|
const key = field.name.value;
|
|
2184
3285
|
const column = this.column(table, key);
|
|
@@ -2189,25 +3290,25 @@ var OperationLowerer = class {
|
|
|
2189
3290
|
const link = this.link(table, key);
|
|
2190
3291
|
if (link && link.kind !== "toMany") {
|
|
2191
3292
|
if (field.value.kind !== Kind.OBJECT || field.value.fields.length !== 1) {
|
|
2192
|
-
|
|
3293
|
+
fail2(`orderBy through '${key}' needs exactly one target column`, "invalid-orderby");
|
|
2193
3294
|
}
|
|
2194
3295
|
const inner = field.value.fields[0];
|
|
2195
3296
|
const targetColumn = this.column(link.target, inner.name.value);
|
|
2196
3297
|
if (!targetColumn) {
|
|
2197
|
-
|
|
3298
|
+
fail2(`unknown column '${inner.name.value}' on '${link.target}' in orderBy`, "unknown-column", this.meta(link.target).columns.map((c) => c.name));
|
|
2198
3299
|
}
|
|
2199
3300
|
lowered.push({ kind: "column", column: inner.name.value, link: key, ...this.lowerDirection(inner.value, link.target, inner.name.value) });
|
|
2200
3301
|
continue;
|
|
2201
3302
|
}
|
|
2202
|
-
|
|
3303
|
+
fail2(`unknown orderBy key '${key}' on '${table}' (to-one links only)`, "invalid-orderby", this.fieldCandidates(table));
|
|
2203
3304
|
}
|
|
2204
3305
|
}
|
|
2205
3306
|
return lowered;
|
|
2206
3307
|
}
|
|
2207
3308
|
lowerDirection(node, table, column) {
|
|
2208
|
-
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));
|
|
2209
3310
|
const direction = ORDER_DIRECTIONS[node.value];
|
|
2210
|
-
if (!direction)
|
|
3311
|
+
if (!direction) fail2(`unknown orderBy direction '${node.value}'`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
|
|
2211
3312
|
return direction.nulls ? { direction: direction.direction, nulls: direction.nulls } : { direction: direction.direction };
|
|
2212
3313
|
}
|
|
2213
3314
|
lowerDistinctOn(node, table) {
|
|
@@ -2216,9 +3317,9 @@ var OperationLowerer = class {
|
|
|
2216
3317
|
}
|
|
2217
3318
|
const items = node.kind === Kind.LIST ? node.values : [node];
|
|
2218
3319
|
return items.map((item) => {
|
|
2219
|
-
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");
|
|
2220
3321
|
if (!this.column(table, item.value)) {
|
|
2221
|
-
|
|
3322
|
+
fail2(`unknown distinctOn column '${item.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
|
|
2222
3323
|
}
|
|
2223
3324
|
return item.value;
|
|
2224
3325
|
});
|
|
@@ -2229,16 +3330,16 @@ var OperationLowerer = class {
|
|
|
2229
3330
|
if (node.kind === Kind.VARIABLE) return { ...base, variable: node.name.value };
|
|
2230
3331
|
if (node.kind === Kind.INT) {
|
|
2231
3332
|
const value = Number(node.value);
|
|
2232
|
-
if (value < 1)
|
|
3333
|
+
if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
|
|
2233
3334
|
return { ...base, value: Math.min(value, MAX_LIMIT) };
|
|
2234
3335
|
}
|
|
2235
|
-
|
|
3336
|
+
fail2("limit must be an integer literal or a variable", "invalid-limit");
|
|
2236
3337
|
}
|
|
2237
3338
|
lowerOffset(node) {
|
|
2238
3339
|
if (!node) return void 0;
|
|
2239
3340
|
if (node.kind === Kind.VARIABLE) return { variable: node.name.value };
|
|
2240
3341
|
if (node.kind === Kind.INT) return { value: Number(node.value) };
|
|
2241
|
-
|
|
3342
|
+
fail2("offset must be an integer literal or a variable", "invalid-offset");
|
|
2242
3343
|
}
|
|
2243
3344
|
// -------------------------------------------------------------------------
|
|
2244
3345
|
// Selections
|
|
@@ -2247,9 +3348,9 @@ var OperationLowerer = class {
|
|
|
2247
3348
|
const meta = this.meta(table);
|
|
2248
3349
|
const selection = { typename: meta.typename, columns: [], relations: [], aggregates: [] };
|
|
2249
3350
|
for (const node of selectionSet.selections) {
|
|
2250
|
-
if (node.kind !== Kind.FIELD)
|
|
3351
|
+
if (node.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
2251
3352
|
if (node.directives && node.directives.length > 0) {
|
|
2252
|
-
|
|
3353
|
+
fail2(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
|
|
2253
3354
|
}
|
|
2254
3355
|
const name = node.name.value;
|
|
2255
3356
|
if (name === "__typename") continue;
|
|
@@ -2257,14 +3358,19 @@ var OperationLowerer = class {
|
|
|
2257
3358
|
const column = this.column(table, name);
|
|
2258
3359
|
if (column) {
|
|
2259
3360
|
if (node.arguments !== void 0 && node.arguments.length > 0 || node.selectionSet !== void 0) {
|
|
2260
|
-
|
|
3361
|
+
fail2(`column '${table}.${name}' takes no arguments or sub-selection`, "invalid-selection");
|
|
2261
3362
|
}
|
|
2262
3363
|
selection.columns.push({ alias, column: name });
|
|
2263
3364
|
continue;
|
|
2264
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
|
+
}
|
|
2265
3371
|
const link = this.link(table, name);
|
|
2266
3372
|
if (link) {
|
|
2267
|
-
if (!node.selectionSet)
|
|
3373
|
+
if (!node.selectionSet) fail2(`link '${table}.${name}' needs a selection set`, "invalid-selection");
|
|
2268
3374
|
const relation = {
|
|
2269
3375
|
alias,
|
|
2270
3376
|
link: name,
|
|
@@ -2283,7 +3389,7 @@ var OperationLowerer = class {
|
|
|
2283
3389
|
if (offset) relation.offset = offset;
|
|
2284
3390
|
}
|
|
2285
3391
|
} else if (node.arguments && node.arguments.length > 0) {
|
|
2286
|
-
|
|
3392
|
+
fail2(`to-one link '${table}.${name}' takes no arguments`, "invalid-selection");
|
|
2287
3393
|
}
|
|
2288
3394
|
selection.relations.push(relation);
|
|
2289
3395
|
continue;
|
|
@@ -2291,7 +3397,7 @@ var OperationLowerer = class {
|
|
|
2291
3397
|
if (name.endsWith("Aggregate")) {
|
|
2292
3398
|
const aggLink = this.link(table, name.slice(0, -"Aggregate".length));
|
|
2293
3399
|
if (aggLink && aggLink.kind === "toMany") {
|
|
2294
|
-
if (!node.selectionSet)
|
|
3400
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
2295
3401
|
assertKnownArgs(node, ["where"]);
|
|
2296
3402
|
const whereArg = argsByName(node).get("where");
|
|
2297
3403
|
const aggregate = this.lowerAggregateSelection(node.selectionSet, aggLink.target);
|
|
@@ -2304,70 +3410,182 @@ var OperationLowerer = class {
|
|
|
2304
3410
|
continue;
|
|
2305
3411
|
}
|
|
2306
3412
|
}
|
|
2307
|
-
|
|
3413
|
+
fail2(`unknown field '${name}' on '${table}'`, "unknown-field", this.fieldCandidates(table));
|
|
2308
3414
|
}
|
|
2309
3415
|
return selection;
|
|
2310
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
|
+
}
|
|
2311
3464
|
/** The `{ aggregate { count sum {…} avg {…} } nodes {…} }` block shared by roots and relations. */
|
|
2312
3465
|
lowerAggregateSelection(selectionSet, table) {
|
|
2313
3466
|
const result = {};
|
|
2314
3467
|
let sawAggregate = false;
|
|
2315
|
-
for (const node of selectionSet
|
|
2316
|
-
if (node.kind !== Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
|
|
3468
|
+
for (const node of aggregateBlockFields(selectionSet)) {
|
|
2317
3469
|
const name = node.name.value;
|
|
2318
|
-
if (name === "__typename") continue;
|
|
2319
|
-
if (node.alias) fail(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
|
|
2320
3470
|
if (name === "nodes") {
|
|
2321
|
-
if (!node.selectionSet)
|
|
3471
|
+
if (!node.selectionSet) fail2("'nodes' needs a selection set", "invalid-aggregate");
|
|
2322
3472
|
result.nodes = this.lowerSelection(node.selectionSet, table);
|
|
2323
3473
|
continue;
|
|
2324
3474
|
}
|
|
2325
|
-
if (name !== "aggregate")
|
|
2326
|
-
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");
|
|
2327
3477
|
sawAggregate = true;
|
|
2328
|
-
if (!node.selectionSet)
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
3478
|
+
if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
|
|
3479
|
+
Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
|
|
3480
|
+
}
|
|
3481
|
+
if (!sawAggregate && !result.nodes) fail2("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
|
|
3482
|
+
return result;
|
|
3483
|
+
}
|
|
3484
|
+
/**
|
|
3485
|
+
* The `{ key {…} aggregate {…} }` block of a grouped aggregate root. Same
|
|
3486
|
+
* `aggregate` sub-block as the ungrouped shape (shared via
|
|
3487
|
+
* `lowerAggregateFields`), but `key` replaces `nodes` — see the IR comment on
|
|
3488
|
+
* `aggregateGroupsRootSchema` for why grouped roots have no `nodes`.
|
|
3489
|
+
*/
|
|
3490
|
+
lowerGroupedAggregateSelection(selectionSet, table, groupBy) {
|
|
3491
|
+
const groupColumns = new Set(groupBy.map((entry) => entry.column));
|
|
3492
|
+
const result = { key: [] };
|
|
3493
|
+
let sawAggregate = false;
|
|
3494
|
+
let sawKey = false;
|
|
3495
|
+
for (const node of aggregateBlockFields(selectionSet)) {
|
|
3496
|
+
const name = node.name.value;
|
|
3497
|
+
if (name === "key") {
|
|
3498
|
+
if (sawKey) fail2("one key block per grouped aggregate", "invalid-aggregate");
|
|
3499
|
+
sawKey = true;
|
|
3500
|
+
if (!node.selectionSet) fail2("'key' needs a selection set", "invalid-aggregate");
|
|
3501
|
+
result.key = this.lowerGroupKeySelection(node.selectionSet, table, groupColumns);
|
|
3502
|
+
continue;
|
|
3503
|
+
}
|
|
3504
|
+
if (name === "nodes") {
|
|
3505
|
+
fail2("'nodes' is not available on a grouped aggregate \u2014 query the rows separately", "invalid-aggregate", ["key", "aggregate"]);
|
|
3506
|
+
}
|
|
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");
|
|
3509
|
+
sawAggregate = true;
|
|
3510
|
+
if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
|
|
3511
|
+
Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
|
|
3512
|
+
}
|
|
3513
|
+
const rendersAggregate = result.count !== void 0 || (result.sum?.length ?? 0) > 0 || (result.avg?.length ?? 0) > 0;
|
|
3514
|
+
if (result.key.length === 0 && !rendersAggregate) {
|
|
3515
|
+
fail2(
|
|
3516
|
+
"grouped aggregate needs at least one key column or an aggregate \u2014 a block selecting only __typename produces nothing",
|
|
3517
|
+
"invalid-aggregate",
|
|
3518
|
+
["key", "aggregate"]
|
|
3519
|
+
);
|
|
3520
|
+
}
|
|
3521
|
+
return result;
|
|
3522
|
+
}
|
|
3523
|
+
/**
|
|
3524
|
+
* `key { month officer }` → the projected group columns. Every field must be
|
|
3525
|
+
* one of the query's own group keys: any other column is indeterminate
|
|
3526
|
+
* within a group, so selecting it is a document error rather than an
|
|
3527
|
+
* arbitrary pick.
|
|
3528
|
+
*/
|
|
3529
|
+
lowerGroupKeySelection(selectionSet, table, groupColumns) {
|
|
3530
|
+
const key = [];
|
|
3531
|
+
for (const node of selectionSet.selections) {
|
|
3532
|
+
if (node.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
3533
|
+
const name = node.name.value;
|
|
3534
|
+
if (name === "__typename") continue;
|
|
3535
|
+
if (!this.column(table, name)) {
|
|
3536
|
+
fail2(`unknown key column '${name}' on '${table}'`, "unknown-column", [...groupColumns]);
|
|
3537
|
+
}
|
|
3538
|
+
if (!groupColumns.has(name)) {
|
|
3539
|
+
fail2(
|
|
3540
|
+
`key column '${name}' is not one of this query's groupBy keys \u2014 it has no single value within a group`,
|
|
3541
|
+
"invalid-group-key",
|
|
3542
|
+
[...groupColumns]
|
|
3543
|
+
);
|
|
3544
|
+
}
|
|
3545
|
+
key.push({ alias: node.alias?.value ?? name, column: name });
|
|
3546
|
+
}
|
|
3547
|
+
return key;
|
|
3548
|
+
}
|
|
3549
|
+
/** The `count` / `sum {…}` / `avg {…}` fields inside an `aggregate { … }` block. */
|
|
3550
|
+
lowerAggregateFields(selectionSet, table) {
|
|
3551
|
+
const result = {};
|
|
3552
|
+
for (const aggField of aggregateBlockFields(selectionSet)) {
|
|
3553
|
+
const aggName = aggField.name.value;
|
|
3554
|
+
if (aggName === "count") {
|
|
3555
|
+
assertKnownArgs(aggField, ["columns", "distinct"]);
|
|
3556
|
+
const args = argsByName(aggField);
|
|
3557
|
+
const count = {};
|
|
3558
|
+
const columns = args.get("columns");
|
|
3559
|
+
if (columns) count.columns = this.lowerDistinctOn(columns.value, table);
|
|
3560
|
+
const distinct = args.get("distinct");
|
|
3561
|
+
if (distinct) {
|
|
3562
|
+
if (distinct.value.kind !== Kind.BOOLEAN) fail2("count(distinct:) must be a boolean literal", "invalid-aggregate");
|
|
3563
|
+
count.distinct = distinct.value.value;
|
|
2347
3564
|
}
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
3565
|
+
result.count = count;
|
|
3566
|
+
continue;
|
|
3567
|
+
}
|
|
3568
|
+
if (aggName === "sum" || aggName === "avg") {
|
|
3569
|
+
if (!aggField.selectionSet) fail2(`'${aggName}' needs a column selection`, "invalid-aggregate");
|
|
3570
|
+
const columns = [];
|
|
3571
|
+
for (const columnField of aggField.selectionSet.selections) {
|
|
3572
|
+
if (columnField.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
3573
|
+
if (columnField.name.value === "__typename") continue;
|
|
3574
|
+
const column = this.column(table, columnField.name.value);
|
|
3575
|
+
if (!column || typeof column.type !== "string" || !NUMERIC_TYPES.has(column.type)) {
|
|
3576
|
+
fail2(
|
|
3577
|
+
`'${aggName}' supports numeric columns only, got '${columnField.name.value}' on '${table}'`,
|
|
3578
|
+
"invalid-aggregate",
|
|
3579
|
+
this.meta(table).columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type)).map((c) => c.name)
|
|
3580
|
+
);
|
|
2363
3581
|
}
|
|
2364
|
-
|
|
2365
|
-
continue;
|
|
3582
|
+
columns.push(columnField.name.value);
|
|
2366
3583
|
}
|
|
2367
|
-
|
|
3584
|
+
result[aggName] = columns;
|
|
3585
|
+
continue;
|
|
2368
3586
|
}
|
|
3587
|
+
fail2(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
|
|
2369
3588
|
}
|
|
2370
|
-
if (!sawAggregate && !result.nodes) fail("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
|
|
2371
3589
|
return result;
|
|
2372
3590
|
}
|
|
2373
3591
|
// -------------------------------------------------------------------------
|
|
@@ -2376,32 +3594,65 @@ var OperationLowerer = class {
|
|
|
2376
3594
|
lowerRoots(operation, kind) {
|
|
2377
3595
|
const roots = [];
|
|
2378
3596
|
for (const node of operation.selectionSet.selections) {
|
|
2379
|
-
if (node.kind !== Kind.FIELD)
|
|
3597
|
+
if (node.kind !== Kind.FIELD) fail2("fragments are not supported at the operation root", "invalid-root");
|
|
3598
|
+
if (node.selectionSet) visit(node.selectionSet, {
|
|
3599
|
+
Directive(directive) {
|
|
3600
|
+
if (directive.name.value === "assertAffectedRows") {
|
|
3601
|
+
fail2("@assertAffectedRows is supported on mutation roots only", "invalid-affected-row-assertion");
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
});
|
|
3605
|
+
const assertion = this.parseAffectedRowAssertion(node, kind);
|
|
2380
3606
|
const gate = this.parseRootDirectives(node);
|
|
2381
3607
|
if (gate.omit) continue;
|
|
2382
|
-
const
|
|
2383
|
-
|
|
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 });
|
|
3611
|
+
const guarded = root.kind === "mutation" && assertion !== void 0 ? { ...root, assertAffectedRows: assertion } : root;
|
|
3612
|
+
roots.push(gate.includeIf ? { ...guarded, includeIf: gate.includeIf } : guarded);
|
|
2384
3613
|
}
|
|
2385
|
-
if (roots.length === 0)
|
|
3614
|
+
if (roots.length === 0) fail2("operation has no roots (all statically skipped?)", "invalid-root");
|
|
2386
3615
|
return roots;
|
|
2387
3616
|
}
|
|
3617
|
+
parseAffectedRowAssertion(node, kind) {
|
|
3618
|
+
const assertions = node.directives?.filter((directive) => directive.name.value === "assertAffectedRows") ?? [];
|
|
3619
|
+
if (!assertions.length) return void 0;
|
|
3620
|
+
if (kind !== "mutation" || assertions.length !== 1) {
|
|
3621
|
+
fail2("@assertAffectedRows is supported once per mutation root", "invalid-affected-row-assertion");
|
|
3622
|
+
}
|
|
3623
|
+
if (node.directives?.some((directive) => ["include", "skip"].includes(directive.name.value))) {
|
|
3624
|
+
fail2("an @assertAffectedRows root cannot use @include or @skip", "invalid-affected-row-assertion");
|
|
3625
|
+
}
|
|
3626
|
+
const args = assertions[0].arguments;
|
|
3627
|
+
const arg = args?.[0];
|
|
3628
|
+
if (args?.length !== 1 || arg?.name.value !== "exactly" || arg.value.kind !== Kind.INT) {
|
|
3629
|
+
fail2("@assertAffectedRows(exactly:) requires a document-authored nonnegative Int literal", "invalid-affected-row-assertion");
|
|
3630
|
+
}
|
|
3631
|
+
const count = Number(arg.value.value);
|
|
3632
|
+
if (!Number.isInteger(count) || count < 0 || count > 2147483647) {
|
|
3633
|
+
fail2("@assertAffectedRows(exactly:) must be between 0 and 2147483647", "invalid-affected-row-assertion");
|
|
3634
|
+
}
|
|
3635
|
+
return count;
|
|
3636
|
+
}
|
|
2388
3637
|
parseRootDirectives(node) {
|
|
2389
3638
|
let includeIf;
|
|
2390
3639
|
let omit = false;
|
|
2391
3640
|
for (const directive of node.directives ?? []) {
|
|
2392
3641
|
const name = directive.name.value;
|
|
3642
|
+
if (name === "assertAffectedRows") continue;
|
|
3643
|
+
if (SOURCE_DIRECTIVES.has(name)) continue;
|
|
2393
3644
|
if (name !== "include" && name !== "skip") {
|
|
2394
|
-
|
|
3645
|
+
fail2(`directive @${name} is not supported on fields`, "unknown-directive", ["include", "skip"]);
|
|
2395
3646
|
}
|
|
2396
3647
|
const ifArg = directive.arguments?.find((a) => a.name.value === "if");
|
|
2397
|
-
if (!ifArg)
|
|
3648
|
+
if (!ifArg) fail2(`@${name} needs an if: argument`, "invalid-directive");
|
|
2398
3649
|
if (ifArg.value.kind === Kind.BOOLEAN) {
|
|
2399
3650
|
const keep = name === "include" ? ifArg.value.value : !ifArg.value.value;
|
|
2400
3651
|
if (!keep) omit = true;
|
|
2401
3652
|
continue;
|
|
2402
3653
|
}
|
|
2403
|
-
if (ifArg.value.kind !== Kind.VARIABLE)
|
|
2404
|
-
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");
|
|
2405
3656
|
includeIf = { variable: ifArg.value.name.value, negated: name === "skip" };
|
|
2406
3657
|
}
|
|
2407
3658
|
return omit ? { omit: true } : { includeIf, omit: false };
|
|
@@ -2410,12 +3661,12 @@ var OperationLowerer = class {
|
|
|
2410
3661
|
const name = node.name.value;
|
|
2411
3662
|
const alias = node.alias?.value ?? name;
|
|
2412
3663
|
const rootInfo = this.ctx.queryRoots.get(name);
|
|
2413
|
-
if (!rootInfo)
|
|
3664
|
+
if (!rootInfo) fail2(`unknown query root '${name}'`, "unknown-root", [...this.ctx.queryRoots.keys()]);
|
|
2414
3665
|
const { table } = rootInfo;
|
|
2415
3666
|
const args = argsByName(node);
|
|
2416
3667
|
if (rootInfo.kind === "select") {
|
|
2417
3668
|
assertKnownArgs(node, ["where", "orderBy", "distinctOn", "limit", "offset"]);
|
|
2418
|
-
if (!node.selectionSet)
|
|
3669
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
2419
3670
|
const where = args.get("where");
|
|
2420
3671
|
const orderBy = args.get("orderBy");
|
|
2421
3672
|
const distinctOn = args.get("distinctOn");
|
|
@@ -2434,12 +3685,15 @@ var OperationLowerer = class {
|
|
|
2434
3685
|
};
|
|
2435
3686
|
}
|
|
2436
3687
|
if (rootInfo.kind === "byPk") {
|
|
2437
|
-
if (!node.selectionSet)
|
|
3688
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
2438
3689
|
const pk = this.lowerPkArgs(node, table);
|
|
2439
3690
|
return { kind: "byPk", alias, table, selection: this.lowerSelection(node.selectionSet, table), pk, where: CONST_TRUE };
|
|
2440
3691
|
}
|
|
3692
|
+
if (rootInfo.kind === "aggregateGroups") {
|
|
3693
|
+
return this.lowerAggregateGroupsRoot(node, { alias, table });
|
|
3694
|
+
}
|
|
2441
3695
|
assertKnownArgs(node, ["where"]);
|
|
2442
|
-
if (!node.selectionSet)
|
|
3696
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
2443
3697
|
const whereArg = args.get("where");
|
|
2444
3698
|
const aggregate = this.lowerAggregateSelection(node.selectionSet, table);
|
|
2445
3699
|
return {
|
|
@@ -2450,6 +3704,108 @@ var OperationLowerer = class {
|
|
|
2450
3704
|
...aggregate
|
|
2451
3705
|
};
|
|
2452
3706
|
}
|
|
3707
|
+
/** `<t>AggregateGroups(where:, groupBy:, limit:, offset:)` → an aggregateGroups root. */
|
|
3708
|
+
lowerAggregateGroupsRoot(node, { alias, table }) {
|
|
3709
|
+
assertKnownArgs(node, ["where", "groupBy", "limit", "offset"]);
|
|
3710
|
+
if (!node.selectionSet) fail2(`'${node.name.value}' needs a selection set`, "invalid-selection");
|
|
3711
|
+
const args = argsByName(node);
|
|
3712
|
+
const groupByArg = args.get("groupBy");
|
|
3713
|
+
if (!groupByArg) fail2(`'${node.name.value}' requires a 'groupBy' argument`, "missing-group-by");
|
|
3714
|
+
const whereArg = args.get("where");
|
|
3715
|
+
const groupBy = this.lowerGroupBy(groupByArg.value, table);
|
|
3716
|
+
const aggregate = this.lowerGroupedAggregateSelection(node.selectionSet, table, groupBy);
|
|
3717
|
+
const offset = this.lowerOffset(args.get("offset")?.value);
|
|
3718
|
+
return {
|
|
3719
|
+
kind: "aggregateGroups",
|
|
3720
|
+
alias,
|
|
3721
|
+
table,
|
|
3722
|
+
where: whereArg ? this.lowerBoolExp(whereArg.value, table) : CONST_TRUE,
|
|
3723
|
+
groupBy,
|
|
3724
|
+
key: aggregate.key,
|
|
3725
|
+
...aggregate.count ? { count: aggregate.count } : {},
|
|
3726
|
+
...aggregate.sum ? { sum: aggregate.sum } : {},
|
|
3727
|
+
...aggregate.avg ? { avg: aggregate.avg } : {},
|
|
3728
|
+
limit: this.lowerGroupLimit(args.get("limit")?.value),
|
|
3729
|
+
...offset ? { offset } : {}
|
|
3730
|
+
};
|
|
3731
|
+
}
|
|
3732
|
+
/**
|
|
3733
|
+
* `groupBy: [{ column: startedAt, bucket: MONTH }, …]` → GroupByKeyIr[].
|
|
3734
|
+
*
|
|
3735
|
+
* Document-authored only: a whole-argument variable is rejected rather than
|
|
3736
|
+
* becoming a §4f slot. Group keys choose which expressions reach GROUP BY,
|
|
3737
|
+
* and dynamic-input.ts refuses aggregation shapes from client input on the
|
|
3738
|
+
* same reasoning — `where` stays dynamic, which is what runtime filtering
|
|
3739
|
+
* actually needs.
|
|
3740
|
+
*/
|
|
3741
|
+
lowerGroupBy(node, table) {
|
|
3742
|
+
if (node.kind === Kind.VARIABLE) {
|
|
3743
|
+
fail2(
|
|
3744
|
+
`groupBy on '${table}' must be written in the document \u2014 it cannot come from a variable`,
|
|
3745
|
+
"dynamic-group-by"
|
|
3746
|
+
);
|
|
3747
|
+
}
|
|
3748
|
+
const items = node.kind === Kind.LIST ? node.values : [node];
|
|
3749
|
+
if (items.length === 0) fail2(`groupBy on '${table}' needs at least one key`, "invalid-group-by");
|
|
3750
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3751
|
+
return items.map((item) => {
|
|
3752
|
+
if (item.kind !== Kind.OBJECT) {
|
|
3753
|
+
fail2(`groupBy entries on '${table}' must be objects like { column: foo }`, "invalid-group-by");
|
|
3754
|
+
}
|
|
3755
|
+
const fields = new Map(item.fields.map((field) => [field.name.value, field.value]));
|
|
3756
|
+
for (const key of fields.keys()) {
|
|
3757
|
+
if (key !== "column" && key !== "bucket") {
|
|
3758
|
+
fail2(`unknown groupBy field '${key}' on '${table}'`, "invalid-group-by", ["column", "bucket"]);
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
const columnNode = fields.get("column");
|
|
3762
|
+
if (!columnNode) fail2(`groupBy entries on '${table}' need a 'column'`, "invalid-group-by");
|
|
3763
|
+
if (columnNode.kind !== Kind.ENUM) {
|
|
3764
|
+
fail2(`groupBy column on '${table}' must be a group-by column enum value`, "invalid-group-by");
|
|
3765
|
+
}
|
|
3766
|
+
const column = this.column(table, columnNode.value);
|
|
3767
|
+
if (!column) {
|
|
3768
|
+
fail2(`unknown groupBy column '${columnNode.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
|
|
3769
|
+
}
|
|
3770
|
+
if (column.type === "jsonb") {
|
|
3771
|
+
fail2(`groupBy column '${table}.${columnNode.value}' is jsonb and cannot be a group key`, "invalid-group-by");
|
|
3772
|
+
}
|
|
3773
|
+
const bucketNode = fields.get("bucket");
|
|
3774
|
+
let bucket;
|
|
3775
|
+
if (bucketNode) {
|
|
3776
|
+
if (bucketNode.kind !== Kind.ENUM) {
|
|
3777
|
+
fail2(`groupBy bucket on '${table}.${columnNode.value}' must be a DateBucket enum value`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
|
|
3778
|
+
}
|
|
3779
|
+
bucket = DATE_BUCKET_BY_ENUM[bucketNode.value];
|
|
3780
|
+
if (!bucket) {
|
|
3781
|
+
fail2(`unknown DateBucket '${bucketNode.value}'`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
|
|
3782
|
+
}
|
|
3783
|
+
if (typeof column.type !== "string" || !BUCKETABLE_TYPES.has(column.type)) {
|
|
3784
|
+
fail2(
|
|
3785
|
+
`groupBy bucket is only valid on date/timestamptz columns \u2014 '${table}.${columnNode.value}' is ${typeof column.type === "string" ? column.type : "an enum"}`,
|
|
3786
|
+
"invalid-group-by"
|
|
3787
|
+
);
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
if (seen.has(columnNode.value)) {
|
|
3791
|
+
fail2(`duplicate groupBy column '${columnNode.value}' on '${table}'`, "invalid-group-by");
|
|
3792
|
+
}
|
|
3793
|
+
seen.add(columnNode.value);
|
|
3794
|
+
return bucket ? { column: columnNode.value, bucket } : { column: columnNode.value };
|
|
3795
|
+
});
|
|
3796
|
+
}
|
|
3797
|
+
/** Like lowerLimit, but bounded by MAX_GROUPS and defaulting to it. */
|
|
3798
|
+
lowerGroupLimit(node) {
|
|
3799
|
+
const base = { default: MAX_GROUPS, max: MAX_GROUPS };
|
|
3800
|
+
if (!node) return base;
|
|
3801
|
+
if (node.kind === Kind.VARIABLE) return { ...base, variable: node.name.value };
|
|
3802
|
+
if (node.kind === Kind.INT) {
|
|
3803
|
+
const value = Number(node.value);
|
|
3804
|
+
if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
|
|
3805
|
+
return { ...base, value: Math.min(value, MAX_GROUPS) };
|
|
3806
|
+
}
|
|
3807
|
+
fail2("limit must be an integer literal or a variable", "invalid-limit");
|
|
3808
|
+
}
|
|
2453
3809
|
/** byPk-style pk arguments: one argument per primary-key column, exactly. */
|
|
2454
3810
|
lowerPkArgs(node, table) {
|
|
2455
3811
|
const meta = this.meta(table);
|
|
@@ -2457,7 +3813,7 @@ var OperationLowerer = class {
|
|
|
2457
3813
|
assertKnownArgs(node, meta.primaryKey);
|
|
2458
3814
|
return meta.primaryKey.map((column) => {
|
|
2459
3815
|
const arg = args.get(column);
|
|
2460
|
-
if (!arg)
|
|
3816
|
+
if (!arg) fail2(`'${node.name.value}' is missing primary-key argument '${column}'`, "missing-pk-argument", meta.primaryKey);
|
|
2461
3817
|
return { column, value: this.lowerValue(arg.value) };
|
|
2462
3818
|
});
|
|
2463
3819
|
}
|
|
@@ -2468,19 +3824,22 @@ var OperationLowerer = class {
|
|
|
2468
3824
|
const name = node.name.value;
|
|
2469
3825
|
const alias = node.alias?.value ?? name;
|
|
2470
3826
|
const rootInfo = this.ctx.mutationRoots.get(name);
|
|
2471
|
-
if (!rootInfo)
|
|
3827
|
+
if (!rootInfo) fail2(`unknown mutation root '${name}'`, "unknown-root", [...this.ctx.mutationRoots.keys()]);
|
|
2472
3828
|
if (rootInfo.op === "cowOverride" || rootInfo.op === "cowRevert") {
|
|
2473
3829
|
return this.lowerCowRoot(node, rootInfo);
|
|
2474
3830
|
}
|
|
3831
|
+
if (rootInfo.op === "resolvedWrite") {
|
|
3832
|
+
return this.lowerResolvedRoot(node, rootInfo);
|
|
3833
|
+
}
|
|
2475
3834
|
const { op, table } = rootInfo;
|
|
2476
3835
|
const args = argsByName(node);
|
|
2477
|
-
if (!node.selectionSet)
|
|
3836
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
2478
3837
|
const base = { kind: "mutation", op, alias, table, pinnedColumns: [], affectedRows: false };
|
|
2479
3838
|
switch (op) {
|
|
2480
3839
|
case "insert": {
|
|
2481
3840
|
assertKnownArgs(node, ["objects", "onConflict"]);
|
|
2482
3841
|
const objects = args.get("objects");
|
|
2483
|
-
if (!objects)
|
|
3842
|
+
if (!objects) fail2(`'${name}' needs objects:`, "missing-argument");
|
|
2484
3843
|
const response = this.lowerMutationResponse(node.selectionSet, table);
|
|
2485
3844
|
return {
|
|
2486
3845
|
...base,
|
|
@@ -2492,7 +3851,7 @@ var OperationLowerer = class {
|
|
|
2492
3851
|
case "insertOne": {
|
|
2493
3852
|
assertKnownArgs(node, ["object", "onConflict"]);
|
|
2494
3853
|
const object = args.get("object");
|
|
2495
|
-
if (!object)
|
|
3854
|
+
if (!object) fail2(`'${name}' needs object:`, "missing-argument");
|
|
2496
3855
|
return {
|
|
2497
3856
|
...base,
|
|
2498
3857
|
objects: this.lowerInsertObjects(object.value, table, { list: false }),
|
|
@@ -2503,7 +3862,7 @@ var OperationLowerer = class {
|
|
|
2503
3862
|
case "update": {
|
|
2504
3863
|
assertKnownArgs(node, ["where", "_set", "_append", "_inc"]);
|
|
2505
3864
|
const where = args.get("where");
|
|
2506
|
-
if (!where)
|
|
3865
|
+
if (!where) fail2(`'${name}' needs where: (bulk updates without a filter are not allowed)`, "missing-argument");
|
|
2507
3866
|
return {
|
|
2508
3867
|
...base,
|
|
2509
3868
|
...this.lowerMutationResponse(node.selectionSet, table),
|
|
@@ -2514,7 +3873,7 @@ var OperationLowerer = class {
|
|
|
2514
3873
|
case "updateByPk": {
|
|
2515
3874
|
assertKnownArgs(node, ["pkColumns", "_set", "_append", "_inc"]);
|
|
2516
3875
|
const pkColumns = args.get("pkColumns");
|
|
2517
|
-
if (!pkColumns)
|
|
3876
|
+
if (!pkColumns) fail2(`'${name}' needs pkColumns:`, "missing-argument");
|
|
2518
3877
|
return {
|
|
2519
3878
|
...base,
|
|
2520
3879
|
pk: this.lowerPkColumnsArg(pkColumns.value, table),
|
|
@@ -2525,7 +3884,7 @@ var OperationLowerer = class {
|
|
|
2525
3884
|
case "delete": {
|
|
2526
3885
|
assertKnownArgs(node, ["where"]);
|
|
2527
3886
|
const where = args.get("where");
|
|
2528
|
-
if (!where)
|
|
3887
|
+
if (!where) fail2(`'${name}' needs where: (bulk deletes without a filter are not allowed)`, "missing-argument");
|
|
2529
3888
|
return {
|
|
2530
3889
|
...base,
|
|
2531
3890
|
...this.lowerMutationResponse(node.selectionSet, table),
|
|
@@ -2552,11 +3911,11 @@ var OperationLowerer = class {
|
|
|
2552
3911
|
const name = node.name.value;
|
|
2553
3912
|
const alias = node.alias?.value ?? name;
|
|
2554
3913
|
const { table } = rootInfo;
|
|
2555
|
-
if (!node.selectionSet)
|
|
3914
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
2556
3915
|
const meta = this.meta(table);
|
|
2557
3916
|
const args = argsByName(node);
|
|
2558
3917
|
const pkArg = args.get(rootInfo.pk);
|
|
2559
|
-
if (!pkArg)
|
|
3918
|
+
if (!pkArg) fail2(`'${name}' is missing primary-key argument '${rootInfo.pk}'`, "missing-pk-argument", [rootInfo.pk]);
|
|
2560
3919
|
const pk = [{ column: rootInfo.pk, value: this.lowerValue(pkArg.value) }];
|
|
2561
3920
|
const base = {
|
|
2562
3921
|
kind: "mutation",
|
|
@@ -2577,24 +3936,78 @@ var OperationLowerer = class {
|
|
|
2577
3936
|
const entries = rootInfo.set.map((column) => {
|
|
2578
3937
|
const arg = args.get(column);
|
|
2579
3938
|
if (!arg) {
|
|
2580
|
-
|
|
3939
|
+
fail2(
|
|
2581
3940
|
`'${name}' is missing set argument '${column}' \u2014 a cow override writes every declared set column (pass null to clear a nullable one)`,
|
|
2582
3941
|
"missing-argument",
|
|
2583
3942
|
[...rootInfo.set]
|
|
2584
3943
|
);
|
|
2585
3944
|
}
|
|
2586
3945
|
if (!meta.settable.includes(column)) {
|
|
2587
|
-
|
|
3946
|
+
fail2(`column '${table}.${column}' is not updatable (pinned by the write policy or deprecated)`, "column-not-settable", meta.settable);
|
|
2588
3947
|
}
|
|
2589
3948
|
return { column, value: this.lowerValue(arg.value) };
|
|
2590
3949
|
});
|
|
2591
3950
|
return { ...base, set: { entries }, cowCopyColumns: [...rootInfo.copyColumns] };
|
|
2592
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
|
+
}
|
|
2593
4006
|
lowerMutationResponse(selectionSet, table) {
|
|
2594
4007
|
let affectedRows = false;
|
|
2595
4008
|
let returning = null;
|
|
2596
4009
|
for (const node of selectionSet.selections) {
|
|
2597
|
-
if (node.kind !== Kind.FIELD)
|
|
4010
|
+
if (node.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
2598
4011
|
const name = node.name.value;
|
|
2599
4012
|
if (name === "__typename") continue;
|
|
2600
4013
|
if (name === "affectedRows") {
|
|
@@ -2602,34 +4015,34 @@ var OperationLowerer = class {
|
|
|
2602
4015
|
continue;
|
|
2603
4016
|
}
|
|
2604
4017
|
if (name === "returning") {
|
|
2605
|
-
if (!node.selectionSet)
|
|
4018
|
+
if (!node.selectionSet) fail2("'returning' needs a selection set", "invalid-selection");
|
|
2606
4019
|
returning = this.lowerSelection(node.selectionSet, table);
|
|
2607
4020
|
continue;
|
|
2608
4021
|
}
|
|
2609
|
-
|
|
4022
|
+
fail2(`unknown mutation-response field '${name}'`, "invalid-selection", ["affectedRows", "returning"]);
|
|
2610
4023
|
}
|
|
2611
4024
|
return { affectedRows, returning };
|
|
2612
4025
|
}
|
|
2613
4026
|
lowerInsertObjects(node, table, { list }) {
|
|
2614
4027
|
if (node.kind === Kind.VARIABLE) return { slot: this.slot(node.name.value, "insertInput", table) };
|
|
2615
4028
|
if (list) {
|
|
2616
|
-
if (node.kind !== Kind.LIST)
|
|
4029
|
+
if (node.kind !== Kind.LIST) fail2(`objects: must be a list literal or a variable`, "invalid-insert");
|
|
2617
4030
|
return { rows: node.values.map((item) => this.lowerInsertRow(item, table)) };
|
|
2618
4031
|
}
|
|
2619
4032
|
return { rows: [this.lowerInsertRow(node, table)] };
|
|
2620
4033
|
}
|
|
2621
4034
|
lowerInsertRow(node, table) {
|
|
2622
|
-
if (node.kind !== Kind.OBJECT)
|
|
4035
|
+
if (node.kind !== Kind.OBJECT) fail2(`insert rows on '${table}' must be objects`, "invalid-insert");
|
|
2623
4036
|
const meta = this.meta(table);
|
|
2624
4037
|
return node.fields.map((field) => {
|
|
2625
4038
|
const key = field.name.value;
|
|
2626
4039
|
if (this.link(table, key)) {
|
|
2627
|
-
|
|
4040
|
+
fail2(`nested inserts are not supported in v2.0 \u2014 '${key}' is a link on '${table}'`, "nested-insert-unsupported");
|
|
2628
4041
|
}
|
|
2629
4042
|
if (!meta.insertable.includes(key)) {
|
|
2630
4043
|
const column = this.column(table, key);
|
|
2631
|
-
if (column)
|
|
2632
|
-
|
|
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);
|
|
2633
4046
|
}
|
|
2634
4047
|
return { column: key, value: this.lowerValue(field.value) };
|
|
2635
4048
|
});
|
|
@@ -2638,32 +4051,32 @@ var OperationLowerer = class {
|
|
|
2638
4051
|
if (!arg) return {};
|
|
2639
4052
|
const node = arg.value;
|
|
2640
4053
|
if (node.kind === Kind.VARIABLE) return { onConflict: { slot: this.slot(node.name.value, "onConflict", table) } };
|
|
2641
|
-
if (node.kind !== Kind.OBJECT)
|
|
4054
|
+
if (node.kind !== Kind.OBJECT) fail2("onConflict: must be an object literal or a variable", "invalid-on-conflict");
|
|
2642
4055
|
const meta = this.meta(table);
|
|
2643
4056
|
let constraint;
|
|
2644
4057
|
const updateColumns = [];
|
|
2645
4058
|
for (const field of node.fields) {
|
|
2646
4059
|
if (field.name.value === "constraint") {
|
|
2647
|
-
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");
|
|
2648
4061
|
constraint = field.value.value;
|
|
2649
4062
|
continue;
|
|
2650
4063
|
}
|
|
2651
4064
|
if (field.name.value === "updateColumns") {
|
|
2652
4065
|
const items = field.value.kind === Kind.LIST ? field.value.values : [field.value];
|
|
2653
4066
|
for (const item of items) {
|
|
2654
|
-
if (item.kind !== Kind.ENUM)
|
|
4067
|
+
if (item.kind !== Kind.ENUM) fail2("onConflict.updateColumns must be update-column enum values", "invalid-on-conflict");
|
|
2655
4068
|
if (!meta.settable.includes(item.value)) {
|
|
2656
|
-
|
|
4069
|
+
fail2(`onConflict.updateColumns: '${item.value}' is not updatable on '${table}'`, "invalid-on-conflict", meta.settable);
|
|
2657
4070
|
}
|
|
2658
4071
|
updateColumns.push(item.value);
|
|
2659
4072
|
}
|
|
2660
4073
|
continue;
|
|
2661
4074
|
}
|
|
2662
|
-
|
|
4075
|
+
fail2(`unknown onConflict key '${field.name.value}'`, "invalid-on-conflict", ["constraint", "updateColumns"]);
|
|
2663
4076
|
}
|
|
2664
|
-
if (!constraint)
|
|
4077
|
+
if (!constraint) fail2("onConflict needs a constraint", "invalid-on-conflict", meta.constraints.map((c) => c.name));
|
|
2665
4078
|
if (!meta.constraints.some((c) => c.name === constraint)) {
|
|
2666
|
-
|
|
4079
|
+
fail2(`unknown constraint '${constraint}' on '${table}'`, "invalid-on-conflict", meta.constraints.map((c) => c.name));
|
|
2667
4080
|
}
|
|
2668
4081
|
return { onConflict: { constraint, updateColumns } };
|
|
2669
4082
|
}
|
|
@@ -2681,36 +4094,36 @@ var OperationLowerer = class {
|
|
|
2681
4094
|
const inc = args.get("_inc");
|
|
2682
4095
|
if (inc) {
|
|
2683
4096
|
if (inc.value.kind === Kind.VARIABLE) {
|
|
2684
|
-
|
|
4097
|
+
fail2("_inc has no dynamic input type \u2014 inline the columns with variable leaf values", "invalid-inc");
|
|
2685
4098
|
}
|
|
2686
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);
|
|
2687
4100
|
result.inc = { entries: this.lowerColumnEntries(inc.value, table, numeric, "_inc") };
|
|
2688
4101
|
}
|
|
2689
4102
|
if (!result.set && !result.append && !result.inc) {
|
|
2690
|
-
|
|
4103
|
+
fail2(`an update on '${table}' needs at least one of _set/_append/_inc`, "missing-argument");
|
|
2691
4104
|
}
|
|
2692
4105
|
return result;
|
|
2693
4106
|
}
|
|
2694
4107
|
lowerColumnEntries(node, table, whitelist, argName) {
|
|
2695
|
-
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");
|
|
2696
4109
|
return node.fields.map((field) => {
|
|
2697
4110
|
const key = field.name.value;
|
|
2698
4111
|
if (!whitelist.includes(key)) {
|
|
2699
|
-
|
|
4112
|
+
fail2(`${argName}: column '${key}' is not allowed on '${table}'`, "column-not-settable", [...whitelist]);
|
|
2700
4113
|
}
|
|
2701
4114
|
return { column: key, value: this.lowerValue(field.value) };
|
|
2702
4115
|
});
|
|
2703
4116
|
}
|
|
2704
4117
|
lowerPkColumnsArg(node, table) {
|
|
2705
|
-
if (node.kind !== Kind.OBJECT)
|
|
4118
|
+
if (node.kind !== Kind.OBJECT) fail2(`pkColumns on '${table}' must be an object literal`, "invalid-pk-columns");
|
|
2706
4119
|
const meta = this.meta(table);
|
|
2707
4120
|
const provided = new Map(node.fields.map((field) => [field.name.value, field.value]));
|
|
2708
4121
|
for (const key of provided.keys()) {
|
|
2709
|
-
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);
|
|
2710
4123
|
}
|
|
2711
4124
|
return meta.primaryKey.map((column) => {
|
|
2712
4125
|
const value = provided.get(column);
|
|
2713
|
-
if (!value)
|
|
4126
|
+
if (!value) fail2(`pkColumns is missing '${column}'`, "invalid-pk-columns", meta.primaryKey);
|
|
2714
4127
|
return { column, value: this.lowerValue(value) };
|
|
2715
4128
|
});
|
|
2716
4129
|
}
|
|
@@ -2719,6 +4132,19 @@ function lowerOperation({ restricted, ctx }) {
|
|
|
2719
4132
|
const lowerer = new OperationLowerer(ctx, restricted);
|
|
2720
4133
|
return lowerer.lowerRoots(restricted.definition, restricted.kind);
|
|
2721
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
|
+
}
|
|
2722
4148
|
function parseSdl(sdl) {
|
|
2723
4149
|
return buildSchema(sdl);
|
|
2724
4150
|
}
|
|
@@ -2959,7 +4385,7 @@ function compileNamespace({ canonical, sdl, documents }) {
|
|
|
2959
4385
|
const loweredRoots = lowerOperation({ restricted: { ...restricted, definition: flattened }, ctx });
|
|
2960
4386
|
const { roots, tableSet } = injectPolicies({ roots: loweredRoots, ctx });
|
|
2961
4387
|
operations[restricted.name] = {
|
|
2962
|
-
irVersion: IR_VERSION,
|
|
4388
|
+
irVersion: roots.some((root) => root.kind === "mutation" && root.assertAffectedRows !== void 0) ? IR_VERSION : LEGACY_IR_VERSION,
|
|
2963
4389
|
name: restricted.name,
|
|
2964
4390
|
opHash: computeOpHash({ definition: flattened, schemaHash }),
|
|
2965
4391
|
kind: restricted.kind,
|
|
@@ -2986,7 +4412,7 @@ function compileNamespace({ canonical, sdl, documents }) {
|
|
|
2986
4412
|
}
|
|
2987
4413
|
if (diagnostics.some((d) => d.severity === "error")) return { ok: false, diagnostics };
|
|
2988
4414
|
const artifact = {
|
|
2989
|
-
irVersion:
|
|
4415
|
+
irVersion: requiredArtifactIrVersion(Object.values(operations)),
|
|
2990
4416
|
namespace: ctx.namespace,
|
|
2991
4417
|
schemaHash,
|
|
2992
4418
|
opsHash: computeOpsHash(Object.values(operations).map((op) => op.opHash)),
|