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