@sequenceholdings/orm 2.1.0 → 2.2.0
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 +40 -1
- package/dist/authoring.cjs +1619 -121
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +252 -8
- package/dist/authoring.d.ts +252 -8
- package/dist/authoring.js +1607 -123
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +713 -41
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +1520 -51
- package/dist/canonical.d.ts +1520 -51
- package/dist/canonical.js +693 -42
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +7103 -3901
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +7104 -3902
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs +122 -40
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +85 -25
- package/dist/client.d.ts +85 -25
- package/dist/client.js +119 -41
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +2464 -450
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +79 -4
- package/dist/compiler.d.ts +79 -4
- package/dist/compiler.js +2457 -451
- package/dist/compiler.js.map +1 -1
- package/dist/constants-C6EG6Wq7.d.cts +99 -0
- package/dist/constants-C6EG6Wq7.d.ts +99 -0
- package/dist/definition-DjfC7Lll.d.cts +91 -0
- package/dist/definition-DjfC7Lll.d.ts +91 -0
- package/dist/dynamic-input.cjs +2 -1
- package/dist/dynamic-input.cjs.map +1 -1
- package/dist/dynamic-input.d.cts +9 -1
- package/dist/dynamic-input.d.ts +9 -1
- package/dist/dynamic-input.js +2 -1
- package/dist/dynamic-input.js.map +1 -1
- package/dist/index.cjs +2656 -418
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +693 -116
- package/dist/index.d.ts +693 -116
- package/dist/index.js +2622 -420
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +811 -48
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +670 -27
- package/dist/ir.d.ts +670 -27
- package/dist/ir.js +802 -49
- 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 +804 -51
- 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 +804 -51
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +1014 -75
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.d.cts +14 -3
- package/dist/sdl.d.ts +14 -3
- package/dist/sdl.js +1012 -76
- package/dist/sdl.js.map +1 -1
- package/package.json +13 -7
- package/src/codegen-gql/server-fns.cjs +2 -2
- package/templates/orm-namespace/AGENTS.md +10 -0
- package/templates/orm-namespace/CLAUDE.md +1 -82
- 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
|
@@ -3,14 +3,32 @@
|
|
|
3
3
|
var crypto = require('crypto');
|
|
4
4
|
var zod = require('zod');
|
|
5
5
|
var graphql = require('graphql');
|
|
6
|
+
var pgParser = require('@supabase/pg-parser');
|
|
6
7
|
|
|
7
8
|
// src/compiler-gql/artifact.ts
|
|
8
9
|
|
|
9
10
|
// src/constants.ts
|
|
10
11
|
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
11
|
-
var
|
|
12
|
+
var MAX_IDENTIFIER_LENGTH = 63;
|
|
13
|
+
var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
|
|
14
|
+
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]{0,62}\.[a-z][a-z0-9_]{0,62}$/;
|
|
12
15
|
var ORM_SCHEMA_PREFIX = "orm_";
|
|
13
16
|
var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
|
|
17
|
+
var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
|
|
18
|
+
var CELL_PART_SUFFIX = {
|
|
19
|
+
at: "__at",
|
|
20
|
+
by: "__by",
|
|
21
|
+
rev: "__rev",
|
|
22
|
+
source: "__src",
|
|
23
|
+
conflicts: "__lost"
|
|
24
|
+
};
|
|
25
|
+
var MAX_TRACKED_COLUMN_NAME = MAX_IDENTIFIER_LENGTH - 6;
|
|
26
|
+
var CELLS_FIELD = "cells";
|
|
27
|
+
function cellColumnName(column, part) {
|
|
28
|
+
return `${column}${CELL_PART_SUFFIX[part]}`;
|
|
29
|
+
}
|
|
30
|
+
var MATERIALIZE_ACTOR = "orm:materialize";
|
|
31
|
+
JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
|
|
14
32
|
|
|
15
33
|
// src/hash.ts
|
|
16
34
|
function stableJson(value) {
|
|
@@ -19,9 +37,478 @@ function stableJson(value) {
|
|
|
19
37
|
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
38
|
return `{${entries.join(",")}}`;
|
|
21
39
|
}
|
|
40
|
+
|
|
41
|
+
// src/compiler/sql.ts
|
|
42
|
+
var CLAIMS_SETTING = "request.jwt.claims";
|
|
43
|
+
var CAPABILITIES_CLAIM_KEY = "capabilities";
|
|
44
|
+
var ROLES_CLAIM_KEY = "roles";
|
|
45
|
+
var quoteIdentifier = (name2) => `"${name2.replaceAll('"', '""')}"`;
|
|
46
|
+
var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
47
|
+
var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
|
|
48
|
+
var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
|
|
49
|
+
var scopedClaimsSource = (namespace) => namespace ? `(CASE WHEN ${CLAIMS_SOURCE} ? 'namespaces' THEN ${CLAIMS_SOURCE} -> 'namespaces' -> ${quoteLiteral(namespace)} ELSE ${CLAIMS_SOURCE} END)` : CLAIMS_SOURCE;
|
|
50
|
+
var capabilityExpression = (key, namespace) => `coalesce(${scopedClaimsSource(namespace)} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
51
|
+
var roleExpression = (key, namespace) => `coalesce(${scopedClaimsSource(namespace)} -> '${ROLES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
52
|
+
|
|
53
|
+
// src/checks/compile.ts
|
|
54
|
+
function tokenize(source) {
|
|
55
|
+
const tokens = [];
|
|
56
|
+
for (let position = 0; position < source.length; ) {
|
|
57
|
+
const char = source[position];
|
|
58
|
+
if (/\s/.test(char)) {
|
|
59
|
+
position++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (char === "(" || char === ")" || char === ",") {
|
|
63
|
+
tokens.push({ kind: char === "(" ? "lparen" : char === ")" ? "rparen" : "comma", value: char, position });
|
|
64
|
+
position++;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (char === "'") {
|
|
68
|
+
const start = position++;
|
|
69
|
+
let value = "";
|
|
70
|
+
let closed = false;
|
|
71
|
+
while (position < source.length) {
|
|
72
|
+
if (source[position] !== "'") {
|
|
73
|
+
value += source[position++];
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (source[position + 1] === "'") {
|
|
77
|
+
value += "'";
|
|
78
|
+
position += 2;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
position++;
|
|
82
|
+
closed = true;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
if (!closed) throw new Error(`unterminated string at position ${start}`);
|
|
86
|
+
if (/[\x00-\x1f\x7f]/.test(value)) throw new Error(`control character in string at position ${start}`);
|
|
87
|
+
tokens.push({ kind: "string", value, position: start });
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const operator = /^(<=|>=|<>|!=|!~|=|<|>|~)/.exec(source.slice(position));
|
|
91
|
+
if (operator) {
|
|
92
|
+
tokens.push({ kind: "operator", value: operator[0] === "<>" ? "!=" : operator[0], position });
|
|
93
|
+
position += operator[0].length;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const number = /^-?\d+(?:\.\d+)?/.exec(source.slice(position));
|
|
97
|
+
if (number) {
|
|
98
|
+
tokens.push({ kind: "number", value: number[0], position });
|
|
99
|
+
position += number[0].length;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const identifier3 = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(source.slice(position));
|
|
103
|
+
if (identifier3) {
|
|
104
|
+
tokens.push({ kind: "identifier", value: identifier3[0], position });
|
|
105
|
+
position += identifier3[0].length;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
throw new Error(`unexpected character '${char}' at position ${position}`);
|
|
109
|
+
}
|
|
110
|
+
tokens.push({ kind: "end", value: "", position: source.length });
|
|
111
|
+
if (tokens.length > 256) throw new Error("check expression exceeds 256 tokens");
|
|
112
|
+
return tokens;
|
|
113
|
+
}
|
|
114
|
+
var Parser = class {
|
|
115
|
+
constructor(tokens, columns) {
|
|
116
|
+
this.tokens = tokens;
|
|
117
|
+
this.columns = /* @__PURE__ */ new Map();
|
|
118
|
+
for (const column of columns) {
|
|
119
|
+
for (const alias of [column.name, column.columnName]) {
|
|
120
|
+
const previous = this.columns.get(alias);
|
|
121
|
+
if (previous && previous.columnName !== column.columnName) {
|
|
122
|
+
throw new Error(`ambiguous check column '${alias}'`);
|
|
123
|
+
}
|
|
124
|
+
this.columns.set(alias, column);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
index = 0;
|
|
129
|
+
depth = 0;
|
|
130
|
+
columns;
|
|
131
|
+
parse() {
|
|
132
|
+
const expression = this.parseOr();
|
|
133
|
+
this.expect("end", "end of expression");
|
|
134
|
+
return expression;
|
|
135
|
+
}
|
|
136
|
+
parseOr() {
|
|
137
|
+
let left = this.parseAnd();
|
|
138
|
+
while (this.matchKeyword("or")) left = `(${left} OR ${this.parseAnd()})`;
|
|
139
|
+
return left;
|
|
140
|
+
}
|
|
141
|
+
parseAnd() {
|
|
142
|
+
let left = this.parseNot();
|
|
143
|
+
while (this.matchKeyword("and")) left = `(${left} AND ${this.parseNot()})`;
|
|
144
|
+
return left;
|
|
145
|
+
}
|
|
146
|
+
parseNot() {
|
|
147
|
+
if (this.matchKeyword("not")) return `(NOT ${this.parseNot()})`;
|
|
148
|
+
return this.parsePrimary();
|
|
149
|
+
}
|
|
150
|
+
parsePrimary() {
|
|
151
|
+
if (this.peek().kind === "lparen") {
|
|
152
|
+
if (++this.depth > 32) throw new Error("check expression nesting exceeds 32 levels");
|
|
153
|
+
this.advance();
|
|
154
|
+
const expression = this.parseOr();
|
|
155
|
+
this.expect("rparen", "')'");
|
|
156
|
+
this.depth--;
|
|
157
|
+
return `(${expression})`;
|
|
158
|
+
}
|
|
159
|
+
return this.parseComparison();
|
|
160
|
+
}
|
|
161
|
+
parseComparison() {
|
|
162
|
+
const left = this.parseValue();
|
|
163
|
+
if (this.matchKeyword("is")) {
|
|
164
|
+
const negate = this.matchKeyword("not");
|
|
165
|
+
this.expectKeyword("null");
|
|
166
|
+
return `${left.sql} IS ${negate ? "NOT " : ""}NULL`;
|
|
167
|
+
}
|
|
168
|
+
if (this.matchKeyword("between")) {
|
|
169
|
+
const lower = this.parseValue();
|
|
170
|
+
this.expectKeyword("and");
|
|
171
|
+
const upper = this.parseValue();
|
|
172
|
+
return `${left.sql} BETWEEN ${lower.sql} AND ${upper.sql}`;
|
|
173
|
+
}
|
|
174
|
+
if (this.matchKeyword("in")) {
|
|
175
|
+
this.expect("lparen", "'(' after IN");
|
|
176
|
+
const values = [this.parseValue()];
|
|
177
|
+
while (this.peek().kind === "comma") {
|
|
178
|
+
this.advance();
|
|
179
|
+
values.push(this.parseValue());
|
|
180
|
+
}
|
|
181
|
+
this.expect("rparen", "')' after IN values");
|
|
182
|
+
if (values.some((value) => !value.literal)) throw new Error("IN values must be literals");
|
|
183
|
+
return `${left.sql} IN (${values.map((value) => value.sql).join(", ")})`;
|
|
184
|
+
}
|
|
185
|
+
const operator = this.expect("operator", "a comparison operator");
|
|
186
|
+
const right = this.parseValue();
|
|
187
|
+
if (operator.value === "~" || operator.value === "!~") {
|
|
188
|
+
if (left.column && left.column.type !== "text") throw new Error("regex checks require a text column");
|
|
189
|
+
if (!right.literal) throw new Error("regex pattern must be a literal");
|
|
190
|
+
}
|
|
191
|
+
return `${left.sql} ${operator.value} ${right.sql}`;
|
|
192
|
+
}
|
|
193
|
+
parseValue() {
|
|
194
|
+
const token = this.advance();
|
|
195
|
+
if (token.kind === "string") return { sql: quoteLiteral(token.value), literal: true };
|
|
196
|
+
if (token.kind === "number") {
|
|
197
|
+
const number = Number(token.value);
|
|
198
|
+
if (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number)) {
|
|
199
|
+
throw new Error(`unsafe numeric literal at position ${token.position}`);
|
|
200
|
+
}
|
|
201
|
+
return { sql: token.value, literal: true };
|
|
202
|
+
}
|
|
203
|
+
if (token.kind !== "identifier") throw new Error(`expected a column or literal at position ${token.position}`);
|
|
204
|
+
const keyword = token.value.toLowerCase();
|
|
205
|
+
if (keyword === "true" || keyword === "false" || keyword === "null") return { sql: keyword.toUpperCase(), literal: true };
|
|
206
|
+
if (keyword === "btrim") {
|
|
207
|
+
this.expect("lparen", "'(' after btrim");
|
|
208
|
+
const column2 = this.parseColumn();
|
|
209
|
+
this.expect("rparen", "')' after btrim column");
|
|
210
|
+
if (column2.type !== "text") throw new Error("btrim requires a text column");
|
|
211
|
+
return { sql: `btrim(${quoteIdentifier(column2.columnName)})`, column: column2 };
|
|
212
|
+
}
|
|
213
|
+
const column = this.columns.get(token.value);
|
|
214
|
+
if (!column) throw new Error(`unknown check column '${token.value}'`);
|
|
215
|
+
return { sql: quoteIdentifier(column.columnName), column };
|
|
216
|
+
}
|
|
217
|
+
parseColumn() {
|
|
218
|
+
const token = this.expect("identifier", "a column name");
|
|
219
|
+
const column = this.columns.get(token.value);
|
|
220
|
+
if (!column) throw new Error(`unknown check column '${token.value}'`);
|
|
221
|
+
return column;
|
|
222
|
+
}
|
|
223
|
+
peek() {
|
|
224
|
+
return this.tokens[this.index];
|
|
225
|
+
}
|
|
226
|
+
advance() {
|
|
227
|
+
const token = this.peek();
|
|
228
|
+
if (token.kind !== "end") this.index++;
|
|
229
|
+
return token;
|
|
230
|
+
}
|
|
231
|
+
matchKeyword(keyword) {
|
|
232
|
+
if (this.peek().kind !== "identifier" || this.peek().value.toLowerCase() !== keyword) return false;
|
|
233
|
+
this.advance();
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
expectKeyword(keyword) {
|
|
237
|
+
if (!this.matchKeyword(keyword)) throw new Error(`expected '${keyword}' at position ${this.peek().position}`);
|
|
238
|
+
}
|
|
239
|
+
expect(kind, description) {
|
|
240
|
+
const token = this.peek();
|
|
241
|
+
if (token.kind !== kind) throw new Error(`expected ${description} at position ${token.position}`);
|
|
242
|
+
return this.advance();
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
function compileCheckExpression(source, columns) {
|
|
246
|
+
return new Parser(tokenize(source), columns).parse();
|
|
247
|
+
}
|
|
248
|
+
var policyVariantSchema = zod.z.object({
|
|
249
|
+
/** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
|
|
250
|
+
when: zod.z.string().min(1).max(1e3),
|
|
251
|
+
/** Exactly one of: allow-all bypass, or a row predicate. */
|
|
252
|
+
allow: zod.z.literal(true).optional(),
|
|
253
|
+
policy: zod.z.string().min(1).max(2e3).optional()
|
|
254
|
+
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
255
|
+
message: "A variant declares exactly one of allow: true or policy"
|
|
256
|
+
});
|
|
257
|
+
var policyExpressionSchema = zod.z.union([
|
|
258
|
+
zod.z.string().min(1).max(2e3),
|
|
259
|
+
zod.z.object({
|
|
260
|
+
default: zod.z.string().min(1).max(2e3),
|
|
261
|
+
variants: zod.z.array(policyVariantSchema).max(10).default([])
|
|
262
|
+
})
|
|
263
|
+
]);
|
|
264
|
+
function parseDurationMs(input) {
|
|
265
|
+
if (typeof input === "number") return Number.isInteger(input) && input > 0 ? input : void 0;
|
|
266
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(input);
|
|
267
|
+
if (!match) return void 0;
|
|
268
|
+
const factor = match[2] === "ms" ? 1 : match[2] === "s" ? 1e3 : 6e4;
|
|
269
|
+
const value = Math.round(Number(match[1]) * factor);
|
|
270
|
+
return value > 0 ? value : void 0;
|
|
271
|
+
}
|
|
272
|
+
var identifier = zod.z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
|
|
273
|
+
var durationMs = zod.z.number().int().positive().max(24 * 60 * 60 * 1e3);
|
|
274
|
+
var RESOLVED_VIEW_SYSTEM_COLUMNS = {
|
|
275
|
+
writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
|
|
276
|
+
writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
|
|
277
|
+
};
|
|
278
|
+
var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
|
|
279
|
+
tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
|
|
280
|
+
fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
|
|
281
|
+
writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
|
|
282
|
+
writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
|
|
283
|
+
writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
|
|
284
|
+
};
|
|
285
|
+
var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
|
|
286
|
+
/* @__PURE__ */ new Set([
|
|
287
|
+
...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
|
|
288
|
+
...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
|
|
289
|
+
]);
|
|
290
|
+
var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
|
|
291
|
+
var canonicalFunctionRefSchema = zod.z.object({
|
|
292
|
+
/** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
|
|
293
|
+
kind: zod.z.enum(["managed", "native"]),
|
|
294
|
+
id: zod.z.string().min(1).max(200)
|
|
295
|
+
}).strict();
|
|
296
|
+
var canonicalFetchShapeSchema = zod.z.union([
|
|
297
|
+
zod.z.object({ shape: zod.z.literal("single") }).strict(),
|
|
298
|
+
zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().min(2).max(500) }).strict()
|
|
299
|
+
]);
|
|
300
|
+
var canonicalResolvedSourceSchema = zod.z.object({
|
|
301
|
+
name: identifier,
|
|
302
|
+
kind: zod.z.enum(["manual", "etl", "prefetch"]),
|
|
303
|
+
/** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
|
|
304
|
+
table: identifier,
|
|
305
|
+
/** Resolved columns (logical names) this source supplies — the primary key is implicit. */
|
|
306
|
+
columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
|
|
307
|
+
/** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
|
|
308
|
+
updatedAt: zod.z.string().min(1).max(63).optional(),
|
|
309
|
+
/** prefetch: resolved column holding the source system's key (may be the primary key). */
|
|
310
|
+
key: zod.z.string().min(1).max(63).optional(),
|
|
311
|
+
fetch: canonicalFetchShapeSchema.optional(),
|
|
312
|
+
function: canonicalFunctionRefSchema.optional(),
|
|
313
|
+
prefetchDefaults: zod.z.object({
|
|
314
|
+
maxStalenessMs: durationMs,
|
|
315
|
+
timeoutMs: durationMs,
|
|
316
|
+
onError: zod.z.enum(["fallback", "fail"])
|
|
317
|
+
}).strict().optional(),
|
|
318
|
+
writeback: zod.z.object({
|
|
319
|
+
columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
|
|
320
|
+
defaultMode: zod.z.enum(["sync", "async"]),
|
|
321
|
+
timeoutMs: durationMs,
|
|
322
|
+
/** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
|
|
323
|
+
clearManualOnConfirm: zod.z.boolean()
|
|
324
|
+
}).strict().optional()
|
|
325
|
+
}).strict();
|
|
326
|
+
var canonicalResolveRuleSchema = zod.z.object({
|
|
327
|
+
column: zod.z.string().min(1).max(63),
|
|
328
|
+
comparator: zod.z.enum(COMPARATORS),
|
|
329
|
+
/** Source names, in tie-break order. */
|
|
330
|
+
candidates: zod.z.array(identifier).min(1).max(16),
|
|
331
|
+
/** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
|
|
332
|
+
orderBy: zod.z.string().min(1).max(2e3).optional()
|
|
333
|
+
}).strict();
|
|
334
|
+
var canonicalResolvedSchema = zod.z.object({
|
|
335
|
+
sources: zod.z.array(canonicalResolvedSourceSchema).min(1).max(16),
|
|
336
|
+
/** One rule per resolved (non-pk) column; documents the generated view for tooling. */
|
|
337
|
+
resolve: zod.z.array(canonicalResolveRuleSchema).max(500),
|
|
338
|
+
/** Sources whose tombstone hides the resolved row (v1: the manual source only). */
|
|
339
|
+
deletedBy: zod.z.array(identifier).max(1).default([]),
|
|
340
|
+
/**
|
|
341
|
+
* Materialized: the view reads an indexed internal table (`table`) that the
|
|
342
|
+
* data plane keeps in step with `compute`, the internal view holding the
|
|
343
|
+
* resolution query. Omitted when the view computes on read.
|
|
344
|
+
*/
|
|
345
|
+
materialized: zod.z.object({ table: identifier, compute: identifier }).strict().optional(),
|
|
346
|
+
/** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
|
|
347
|
+
policies: zod.z.object({
|
|
348
|
+
write: policyExpressionSchema.optional(),
|
|
349
|
+
insert: policyExpressionSchema.optional(),
|
|
350
|
+
update: policyExpressionSchema.optional()
|
|
351
|
+
}).strict().default({})
|
|
352
|
+
}).strict();
|
|
353
|
+
function validateResolvedView({ view, tables, views = [] }) {
|
|
354
|
+
const resolved = view.resolved;
|
|
355
|
+
if (!resolved) return [];
|
|
356
|
+
const errors = [];
|
|
357
|
+
const fail4 = (message) => errors.push(`resolved table '${view.view}': ${message}`);
|
|
358
|
+
const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
|
|
359
|
+
if (!pkColumn) {
|
|
360
|
+
fail4("requires exactly one primary-key column");
|
|
361
|
+
return errors;
|
|
362
|
+
}
|
|
363
|
+
const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
|
|
364
|
+
const scope = {
|
|
365
|
+
view,
|
|
366
|
+
resolved,
|
|
367
|
+
pkColumn,
|
|
368
|
+
dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
|
|
369
|
+
sourceByName: /* @__PURE__ */ new Map(),
|
|
370
|
+
fail: fail4
|
|
371
|
+
};
|
|
372
|
+
if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail4("requires exactly one manual source");
|
|
373
|
+
const tableByName = new Map(tables.map((table) => [table.table, table]));
|
|
374
|
+
for (const source of resolved.sources) {
|
|
375
|
+
if (scope.sourceByName.has(source.name)) fail4(`duplicate source '${source.name}'`);
|
|
376
|
+
scope.sourceByName.set(source.name, source);
|
|
377
|
+
const table = tableByName.get(source.table);
|
|
378
|
+
if (!table) fail4(`source '${source.name}' references unknown table '${source.table}'`);
|
|
379
|
+
else validateSource({ scope, source, table });
|
|
380
|
+
}
|
|
381
|
+
validateRules(scope);
|
|
382
|
+
validateViewCells(scope);
|
|
383
|
+
if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
|
|
384
|
+
for (const name2 of resolved.deletedBy) {
|
|
385
|
+
if (scope.sourceByName.get(name2)?.kind !== "manual") fail4(`deletedBy '${name2}' must be the manual source`);
|
|
386
|
+
}
|
|
387
|
+
return errors;
|
|
388
|
+
}
|
|
389
|
+
function validateSource({ scope, source, table }) {
|
|
390
|
+
const { fail: fail4 } = scope;
|
|
391
|
+
if (source.kind === "etl" && table.source === void 0) fail4(`etl source '${source.name}' must reference a synced table`);
|
|
392
|
+
if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
|
|
393
|
+
fail4(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
|
|
394
|
+
}
|
|
395
|
+
const hasRev = table.columns.some((column) => column.cell?.part === "rev");
|
|
396
|
+
if (source.kind === "manual" && !hasRev) fail4(`manual source '${source.name}' must carry '__rev' cell columns`);
|
|
397
|
+
if (source.kind === "prefetch" && hasRev) fail4(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
|
|
398
|
+
const byName = new Map(table.columns.map((c) => [c.name, c]));
|
|
399
|
+
validateSourceColumns({ scope, source, byName });
|
|
400
|
+
if (source.kind === "etl") {
|
|
401
|
+
const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
|
|
402
|
+
if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
|
|
403
|
+
fail4(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
validatePrefetchFields({ scope, source });
|
|
407
|
+
}
|
|
408
|
+
function validatePrefetchFields({ scope, source }) {
|
|
409
|
+
const { fail: fail4, pkColumn } = scope;
|
|
410
|
+
if (source.kind !== "prefetch") {
|
|
411
|
+
if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
|
|
412
|
+
fail4(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
|
|
413
|
+
}
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
|
|
417
|
+
fail4(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
|
|
418
|
+
} else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
|
|
419
|
+
fail4(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
|
|
420
|
+
}
|
|
421
|
+
for (const column of source.writeback?.columns ?? []) {
|
|
422
|
+
if (!source.columns.includes(column)) fail4(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function validateSourceColumns({ scope, source, byName }) {
|
|
426
|
+
const { fail: fail4, pkColumn, dataColumns } = scope;
|
|
427
|
+
for (const column of [pkColumn.name, ...source.columns]) {
|
|
428
|
+
const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
|
|
429
|
+
const theirs = byName.get(column);
|
|
430
|
+
if (!own) fail4(`source '${source.name}' supplies unknown column '${column}'`);
|
|
431
|
+
else if (!theirs) fail4(`source '${source.name}' table '${source.table}' has no column '${column}'`);
|
|
432
|
+
else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail4(`column '${column}' has a different type on source '${source.name}'`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail: fail4 }) {
|
|
436
|
+
const ruled = /* @__PURE__ */ new Set();
|
|
437
|
+
for (const rule of resolved.resolve) {
|
|
438
|
+
if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
|
|
439
|
+
fail4(`resolve rule targets unknown column '${rule.column}'`);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (ruled.has(rule.column)) fail4(`column '${rule.column}' has more than one resolve rule`);
|
|
443
|
+
ruled.add(rule.column);
|
|
444
|
+
if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
|
|
445
|
+
fail4(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
|
|
446
|
+
}
|
|
447
|
+
if (new Set(rule.candidates).size !== rule.candidates.length) fail4(`column '${rule.column}' repeats a candidate`);
|
|
448
|
+
for (const candidate of rule.candidates) {
|
|
449
|
+
const source = sourceByName.get(candidate);
|
|
450
|
+
if (!source) fail4(`column '${rule.column}' names unknown candidate source '${candidate}'`);
|
|
451
|
+
else if (!source.columns.includes(rule.column)) fail4(`column '${rule.column}' candidate '${candidate}' does not supply it`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
for (const column of dataColumns.keys()) {
|
|
455
|
+
if (column !== pkColumn.name && !ruled.has(column)) fail4(`column '${column}' has no resolve rule`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
var VIEW_CELL_TYPES = {
|
|
459
|
+
source: "text",
|
|
460
|
+
at: "timestamptz",
|
|
461
|
+
by: "text",
|
|
462
|
+
conflicts: "jsonb"
|
|
463
|
+
};
|
|
464
|
+
function validateViewCells({ view, dataColumns, pkColumn, fail: fail4 }) {
|
|
465
|
+
const expected = /* @__PURE__ */ new Map();
|
|
466
|
+
for (const column of dataColumns.values()) {
|
|
467
|
+
if (column.name === pkColumn.name) continue;
|
|
468
|
+
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
469
|
+
expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
for (const column of view.columns) {
|
|
473
|
+
if (!column.cell) continue;
|
|
474
|
+
const want = expected.get(column.name);
|
|
475
|
+
if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
|
|
476
|
+
fail4(`unexpected cell column '${column.name}'`);
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail4(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
|
|
480
|
+
expected.delete(column.name);
|
|
481
|
+
}
|
|
482
|
+
for (const name2 of expected.keys()) fail4(`missing cell column '${name2}'`);
|
|
483
|
+
}
|
|
484
|
+
var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
|
|
485
|
+
function validateMaterialized({ scope, tableByName, views }) {
|
|
486
|
+
const { view, resolved, fail: fail4 } = scope;
|
|
487
|
+
const { table: tableName, compute: computeName } = resolved.materialized;
|
|
488
|
+
const table = tableByName.get(tableName);
|
|
489
|
+
if (!table || table.internal !== true || table.materializes !== view.view) {
|
|
490
|
+
fail4(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
|
|
491
|
+
} else {
|
|
492
|
+
if (columnSignature(table.columns) !== columnSignature(view.columns)) fail4(`materialized table '${tableName}' must have exactly the view's columns`);
|
|
493
|
+
if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail4(`materialized table '${tableName}' must share the view's primary key`);
|
|
494
|
+
}
|
|
495
|
+
const compute = views.find((candidate) => candidate.view === computeName);
|
|
496
|
+
if (!compute || compute.internal !== true) fail4(`compute view '${computeName}' must be an internal view`);
|
|
497
|
+
else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail4(`compute view '${computeName}' must have exactly the view's columns`);
|
|
498
|
+
for (const source of resolved.sources) {
|
|
499
|
+
if (source.kind !== "etl") continue;
|
|
500
|
+
const read = tableByName.get(source.table)?.policies?.read;
|
|
501
|
+
const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
|
|
502
|
+
if (!unconditional) {
|
|
503
|
+
fail4(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// src/canonical/v2.ts
|
|
22
509
|
var CANONICAL_FORMAT = 2;
|
|
23
510
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
24
|
-
var
|
|
511
|
+
var identifier2 = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
25
512
|
var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
|
|
26
513
|
var COLUMN_TYPES = [
|
|
27
514
|
"uuid",
|
|
@@ -37,14 +524,14 @@ var COLUMN_TYPES = [
|
|
|
37
524
|
];
|
|
38
525
|
var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
|
|
39
526
|
var canonicalEnumSchema = zod.z.object({
|
|
40
|
-
name:
|
|
527
|
+
name: identifier2,
|
|
41
528
|
values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
|
|
42
529
|
});
|
|
43
530
|
var canonicalColumnSchema = zod.z.object({
|
|
44
531
|
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
45
532
|
name: zod.z.string().min(1).max(63),
|
|
46
533
|
columnName: columnIdentifier,
|
|
47
|
-
type: zod.z.union([columnTypeSchema, zod.z.object({ enum:
|
|
534
|
+
type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier2 })]),
|
|
48
535
|
nullable: zod.z.boolean().default(true),
|
|
49
536
|
unique: zod.z.boolean().default(false),
|
|
50
537
|
/** Literal default, or one of the allowlisted SQL functions. */
|
|
@@ -60,7 +547,14 @@ var canonicalColumnSchema = zod.z.object({
|
|
|
60
547
|
/** Migration hints — same semantics as v1. */
|
|
61
548
|
renamedFrom: columnIdentifier.optional(),
|
|
62
549
|
cast: zod.z.string().max(200).optional(),
|
|
63
|
-
deprecated: zod.z.boolean().default(false)
|
|
550
|
+
deprecated: zod.z.boolean().default(false),
|
|
551
|
+
/**
|
|
552
|
+
* Marks a system cell-metadata column (see CELL_PARTS in constants): part
|
|
553
|
+
* `part` of the cell of logical column `of`. Synthesized by authoring,
|
|
554
|
+
* never client-writable, absent from GraphQL scalar fields. Omitted on
|
|
555
|
+
* ordinary columns so their hashes are unchanged.
|
|
556
|
+
*/
|
|
557
|
+
cell: zod.z.object({ of: zod.z.string().min(1).max(63), part: zod.z.enum(CELL_PARTS) }).strict().optional()
|
|
64
558
|
}).superRefine((column, ctx) => {
|
|
65
559
|
if (column.onUpdateFn === void 0) return;
|
|
66
560
|
if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
|
|
@@ -75,36 +569,28 @@ var canonicalLinkSchema = zod.z.object({
|
|
|
75
569
|
name: zod.z.string().min(1).max(63),
|
|
76
570
|
kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
|
|
77
571
|
/** Target table name (namespace-local). Absent for toPlatformUser. */
|
|
78
|
-
target:
|
|
572
|
+
target: identifier2.optional(),
|
|
573
|
+
/** Owning namespace of an imported target; omitted for local links. */
|
|
574
|
+
namespace: identifier2.max(41).optional(),
|
|
79
575
|
/** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
|
|
80
576
|
on: columnIdentifier,
|
|
81
577
|
/** Referenced column on the target (default: target's PK / platform user `sub`). */
|
|
82
578
|
references: columnIdentifier.optional(),
|
|
83
579
|
/** enforce: true emits a real FK constraint (same-namespace toOne only). */
|
|
84
580
|
enforce: zod.z.boolean().default(false),
|
|
581
|
+
onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
582
|
+
onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
85
583
|
description: zod.z.string().max(500).optional()
|
|
86
584
|
});
|
|
87
585
|
var canonicalIndexSchema = zod.z.object({
|
|
88
586
|
columns: zod.z.array(columnIdentifier).min(1),
|
|
89
587
|
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"
|
|
588
|
+
name: identifier2.optional()
|
|
100
589
|
});
|
|
101
|
-
var
|
|
102
|
-
|
|
103
|
-
zod.z.
|
|
104
|
-
|
|
105
|
-
variants: zod.z.array(policyVariantSchema).max(10).default([])
|
|
106
|
-
})
|
|
107
|
-
]);
|
|
590
|
+
var canonicalCheckSchema = zod.z.object({
|
|
591
|
+
name: identifier2,
|
|
592
|
+
expression: zod.z.string().min(1).max(2e3)
|
|
593
|
+
}).strict();
|
|
108
594
|
var canonicalPoliciesSchema = zod.z.object({
|
|
109
595
|
read: policyExpressionSchema.optional(),
|
|
110
596
|
write: policyExpressionSchema.optional(),
|
|
@@ -135,7 +621,7 @@ function isSingleViewQuerySql(value) {
|
|
|
135
621
|
return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
|
|
136
622
|
}
|
|
137
623
|
var canonicalActionSchema = zod.z.object({
|
|
138
|
-
name:
|
|
624
|
+
name: identifier2,
|
|
139
625
|
requires: zod.z.string().max(1e3).optional(),
|
|
140
626
|
params: zod.z.record(zod.z.string(), zod.z.object({
|
|
141
627
|
type: columnTypeSchema,
|
|
@@ -147,9 +633,9 @@ var canonicalActionSchema = zod.z.object({
|
|
|
147
633
|
});
|
|
148
634
|
var canonicalCowMutationSchema = zod.z.object({
|
|
149
635
|
kind: zod.z.enum(["override", "revert"]),
|
|
150
|
-
name:
|
|
636
|
+
name: identifier2,
|
|
151
637
|
/** Synced table (same namespace) the overlay copies from / falls back to. */
|
|
152
|
-
synced:
|
|
638
|
+
synced: identifier2,
|
|
153
639
|
/** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
|
|
154
640
|
pk: zod.z.string().min(1).max(63),
|
|
155
641
|
/** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
|
|
@@ -177,23 +663,28 @@ var canonicalCowMutationSchema = zod.z.object({
|
|
|
177
663
|
}
|
|
178
664
|
});
|
|
179
665
|
var canonicalForeignKeySchema = zod.z.object({
|
|
180
|
-
name:
|
|
666
|
+
name: identifier2,
|
|
181
667
|
columns: zod.z.array(columnIdentifier).min(1).max(8),
|
|
182
|
-
target:
|
|
668
|
+
target: identifier2,
|
|
183
669
|
references: zod.z.array(columnIdentifier).min(1).max(8),
|
|
184
|
-
deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional()
|
|
670
|
+
deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional(),
|
|
671
|
+
onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
672
|
+
onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
|
|
185
673
|
}).strict();
|
|
186
674
|
var canonicalTableSchema = zod.z.object({
|
|
187
|
-
table:
|
|
675
|
+
table: identifier2,
|
|
676
|
+
displayName: zod.z.string().trim().min(1).max(120).optional(),
|
|
188
677
|
description: zod.z.string().max(2e3).optional(),
|
|
189
678
|
/** Table-level rename hint (new in v2). */
|
|
190
|
-
renamedFrom:
|
|
191
|
-
/** Synced/reflected read-only tables (`source:
|
|
192
|
-
source: zod.z.string().regex(
|
|
679
|
+
renamedFrom: identifier2.optional(),
|
|
680
|
+
/** Synced/reflected read-only tables (`source: schema.table`) — v1 semantics. */
|
|
681
|
+
source: zod.z.string().regex(QUALIFIED_TABLE_PATTERN).optional(),
|
|
193
682
|
columns: zod.z.array(canonicalColumnSchema).default([]),
|
|
194
683
|
primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
|
|
195
684
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
196
685
|
indexes: zod.z.array(canonicalIndexSchema).default([]),
|
|
686
|
+
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
687
|
+
checks: zod.z.array(canonicalCheckSchema).max(100).optional(),
|
|
197
688
|
policies: canonicalPoliciesSchema.default({}),
|
|
198
689
|
/**
|
|
199
690
|
* v1-conversion carry-over only. v2 has no action surface — the runtime
|
|
@@ -204,22 +695,55 @@ var canonicalTableSchema = zod.z.object({
|
|
|
204
695
|
/** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
|
|
205
696
|
cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
|
|
206
697
|
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
207
|
-
foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional()
|
|
698
|
+
foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional(),
|
|
699
|
+
/**
|
|
700
|
+
* Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
|
|
701
|
+
* column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
|
|
702
|
+
* on a resolved table's internal sources), listed explicitly in `columns`
|
|
703
|
+
* with a `cell` marker so DDL and migrations treat them as ordinary columns.
|
|
704
|
+
* Omitted when off so existing definitions hash unchanged.
|
|
705
|
+
*/
|
|
706
|
+
cellMetadata: zod.z.literal(true).optional(),
|
|
707
|
+
/**
|
|
708
|
+
* System-owned backing table of a resolved table (its manual or prefetch
|
|
709
|
+
* source). Internal tables get DDL and IR metadata but no GraphQL surface;
|
|
710
|
+
* only the data plane writes them. Omitted when false.
|
|
711
|
+
*/
|
|
712
|
+
internal: zod.z.literal(true).optional(),
|
|
713
|
+
/**
|
|
714
|
+
* The materialized copy of resolved table `materializes` (internal): the
|
|
715
|
+
* same columns as that view, including its cell parts, maintained by the
|
|
716
|
+
* data plane. Omitted otherwise.
|
|
717
|
+
*/
|
|
718
|
+
materializes: identifier2.optional()
|
|
208
719
|
});
|
|
720
|
+
var MAX_VIEW_QUERY = 1e5;
|
|
721
|
+
var MAX_RESOLVED_VIEW_QUERY = 5e6;
|
|
209
722
|
var canonicalViewSchema = zod.z.object({
|
|
210
|
-
view:
|
|
723
|
+
view: identifier2,
|
|
211
724
|
description: zod.z.string().max(2e3).optional(),
|
|
212
725
|
columns: zod.z.array(canonicalColumnSchema).min(1),
|
|
213
726
|
/** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
|
|
214
727
|
primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
|
|
215
728
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
216
729
|
policies: zod.z.object({ read: policyExpressionSchema }).strict(),
|
|
217
|
-
query: zod.z.string().min(1).max(
|
|
730
|
+
query: zod.z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
|
|
218
731
|
message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
|
|
219
732
|
}),
|
|
220
733
|
/** Namespace-local logical relation names, sorted for deterministic hashing. */
|
|
221
|
-
dependencies: zod.z.array(
|
|
734
|
+
dependencies: zod.z.array(identifier2).max(500),
|
|
735
|
+
/**
|
|
736
|
+
* Present when this view is a resolved table (defineResolvedTable): the
|
|
737
|
+
* multi-source metadata behind its generated query. Omitted otherwise so
|
|
738
|
+
* existing views hash unchanged.
|
|
739
|
+
*/
|
|
740
|
+
resolved: canonicalResolvedSchema.optional(),
|
|
741
|
+
/** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
|
|
742
|
+
internal: zod.z.literal(true).optional()
|
|
222
743
|
}).superRefine((view, ctx) => {
|
|
744
|
+
if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
|
|
745
|
+
ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
|
|
746
|
+
}
|
|
223
747
|
for (const [index, column] of view.columns.entries()) {
|
|
224
748
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
225
749
|
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
|
|
@@ -234,20 +758,91 @@ var canonicalViewSchema = zod.z.object({
|
|
|
234
758
|
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare migration hints` });
|
|
235
759
|
}
|
|
236
760
|
}
|
|
761
|
+
if (view.resolved === void 0 && view.internal !== true) {
|
|
762
|
+
for (const [index, column] of view.columns.entries()) {
|
|
763
|
+
if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index], message: `only resolved tables carry cell columns ('${column.name}')` });
|
|
764
|
+
}
|
|
765
|
+
}
|
|
237
766
|
for (const [index, link] of view.links.entries()) {
|
|
238
767
|
if (link.enforce) {
|
|
239
768
|
ctx.addIssue({ code: "custom", path: ["links", index, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
|
|
240
769
|
}
|
|
770
|
+
if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
|
|
771
|
+
ctx.addIssue({ code: "custom", path: ["links", index], message: `view link '${link.name}' cannot declare referential actions` });
|
|
772
|
+
}
|
|
241
773
|
}
|
|
242
774
|
});
|
|
243
|
-
var
|
|
775
|
+
var CELL_PART_TYPES = {
|
|
776
|
+
at: "timestamptz",
|
|
777
|
+
by: "text",
|
|
778
|
+
rev: "uuid",
|
|
779
|
+
source: "text",
|
|
780
|
+
conflicts: "jsonb"
|
|
781
|
+
};
|
|
782
|
+
function trackedCellColumns(table) {
|
|
783
|
+
const pk = new Set(table.primaryKey ?? []);
|
|
784
|
+
return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
|
|
785
|
+
}
|
|
786
|
+
var SUFFIXES = Object.values(CELL_PART_SUFFIX);
|
|
787
|
+
function validateCellMetadata(table, index, ctx) {
|
|
788
|
+
const fail4 = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
|
|
789
|
+
const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
|
|
790
|
+
if (table.materializes !== void 0) return;
|
|
791
|
+
if (table.cellMetadata !== true) {
|
|
792
|
+
for (const { position } of cells) fail4(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index, "columns", position]);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
if (table.source !== void 0) {
|
|
796
|
+
fail4(`synced table '${table.table}' cannot track cell metadata`);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
const tracked = trackedCellColumns(table);
|
|
800
|
+
const byName = new Map(tracked.map((column) => [column.name, column]));
|
|
801
|
+
const withRev = cells.some(({ cell }) => cell.part === "rev");
|
|
802
|
+
if (withRev && table.internal !== true) fail4(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
|
|
803
|
+
const seen = /* @__PURE__ */ new Set();
|
|
804
|
+
for (const { column, position, cell } of cells) {
|
|
805
|
+
const path = ["tables", index, "columns", position];
|
|
806
|
+
const owner = byName.get(cell.of);
|
|
807
|
+
if (!owner) {
|
|
808
|
+
fail4(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if (cell.part !== "at" && cell.part !== "by" && cell.part !== "rev") fail4(`a table cell column must be part at/by/rev, not '${cell.part}'`, path);
|
|
812
|
+
if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
|
|
813
|
+
fail4(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
|
|
814
|
+
}
|
|
815
|
+
if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
|
|
816
|
+
fail4(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
|
|
817
|
+
}
|
|
818
|
+
const key = `${cell.of}:${cell.part}`;
|
|
819
|
+
if (seen.has(key)) fail4(`duplicate cell column '${column.name}'`, path);
|
|
820
|
+
seen.add(key);
|
|
821
|
+
}
|
|
822
|
+
for (const column of tracked) {
|
|
823
|
+
if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
|
|
824
|
+
fail4(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
|
|
825
|
+
}
|
|
826
|
+
if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
|
|
827
|
+
fail4(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
|
|
828
|
+
}
|
|
829
|
+
for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
|
|
830
|
+
if (!seen.has(`${column.name}:${part}`)) fail4(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
var exportGateSchema = zod.z.union([zod.z.literal(true), zod.z.object({ capability: zod.z.string().min(1) }).strict(), zod.z.object({ role: zod.z.string().min(1) }).strict()]);
|
|
835
|
+
var relationExportSchema = zod.z.object({ read: exportGateSchema, write: exportGateSchema.optional() }).strict();
|
|
836
|
+
var localDefinitionSchema = zod.z.object({
|
|
244
837
|
definition_format: zod.z.literal(CANONICAL_FORMAT),
|
|
245
|
-
namespace:
|
|
838
|
+
namespace: identifier2.max(41),
|
|
246
839
|
description: zod.z.string().max(2e3).optional(),
|
|
247
840
|
enums: zod.z.array(canonicalEnumSchema).default([]),
|
|
248
841
|
tables: zod.z.array(canonicalTableSchema).min(0).max(500),
|
|
249
842
|
/** Optional to preserve byte-identical hashes for pre-view canonical v2 documents. */
|
|
250
843
|
views: zod.z.array(canonicalViewSchema).max(500).optional(),
|
|
844
|
+
/** Target-owned cross-namespace access contract, using existing namespace grants. */
|
|
845
|
+
exports: zod.z.record(identifier2, relationExportSchema).optional(),
|
|
251
846
|
/** Verbatim custom-roles grammar (@sequenceholdings/custom-roles-manifest). */
|
|
252
847
|
capabilities: zod.z.object({
|
|
253
848
|
roles: zod.z.unknown().optional(),
|
|
@@ -260,6 +855,7 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
260
855
|
ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
|
|
261
856
|
}
|
|
262
857
|
seen.add(table.table);
|
|
858
|
+
validateCellMetadata(table, index, ctx);
|
|
263
859
|
if (table.source !== void 0) {
|
|
264
860
|
for (const [columnIndex, column] of table.columns.entries()) {
|
|
265
861
|
if (column.onUpdateFn !== void 0) {
|
|
@@ -273,31 +869,68 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
273
869
|
}
|
|
274
870
|
}
|
|
275
871
|
const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
872
|
+
const relations = /* @__PURE__ */ new Set([...fkTables.keys(), ...(definition.views ?? []).map((view) => view.view)]);
|
|
873
|
+
for (const name2 of Object.keys(definition.exports ?? {})) {
|
|
874
|
+
if (!relations.has(name2)) ctx.addIssue({ code: "custom", path: ["exports", name2], message: `export '${name2}' is not a local object` });
|
|
875
|
+
}
|
|
276
876
|
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
877
|
+
for (const [linkIndex, link] of table.links.entries()) {
|
|
878
|
+
if (link.enforce && link.namespace !== void 0 && link.namespace !== definition.namespace) {
|
|
879
|
+
ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", linkIndex], message: "physical foreign keys require a local target; use an ORM link without enforce" });
|
|
880
|
+
}
|
|
881
|
+
}
|
|
277
882
|
const names = /* @__PURE__ */ new Set([
|
|
278
883
|
...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
|
|
279
884
|
...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
|
|
280
885
|
...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
|
|
281
886
|
]);
|
|
887
|
+
for (const [index, link] of table.links.entries()) {
|
|
888
|
+
if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
|
|
889
|
+
ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "referential actions require an enforced toOne link" });
|
|
890
|
+
}
|
|
891
|
+
if (link.onDelete === "set null" || link.onUpdate === "set null") {
|
|
892
|
+
const source = table.columns.find((column) => column.columnName === link.on);
|
|
893
|
+
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" });
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
for (const [index, check] of (table.checks ?? []).entries()) {
|
|
897
|
+
const fail4 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index], message });
|
|
898
|
+
if (table.source) fail4("synced tables cannot declare checks");
|
|
899
|
+
if (names.has(check.name)) fail4(`duplicate constraint '${check.name}'`);
|
|
900
|
+
names.add(check.name);
|
|
901
|
+
try {
|
|
902
|
+
compileCheckExpression(check.expression, table.columns);
|
|
903
|
+
} catch (error) {
|
|
904
|
+
fail4(error instanceof Error ? error.message : String(error));
|
|
905
|
+
}
|
|
906
|
+
}
|
|
282
907
|
for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
|
|
283
|
-
const
|
|
284
|
-
if (names.has(fk.name))
|
|
908
|
+
const fail4 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
|
|
909
|
+
if (names.has(fk.name)) fail4(`duplicate foreign key '${fk.name}'`);
|
|
285
910
|
names.add(fk.name);
|
|
286
911
|
const target = fkTables.get(fk.target);
|
|
287
912
|
if (table.source || !target || target.source) {
|
|
288
|
-
|
|
913
|
+
fail4("foreign keys must connect owned tables in the same namespace");
|
|
289
914
|
continue;
|
|
290
915
|
}
|
|
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)
|
|
293
|
-
for (const [position,
|
|
294
|
-
const sourceColumn = table.columns.find((column) => column.columnName ===
|
|
916
|
+
if (fk.columns.length !== fk.references.length) fail4("foreign key columns and references must have the same number of columns");
|
|
917
|
+
if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail4("foreign key columns must not repeat");
|
|
918
|
+
for (const [position, name2] of fk.columns.entries()) {
|
|
919
|
+
const sourceColumn = table.columns.find((column) => column.columnName === name2);
|
|
295
920
|
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))
|
|
921
|
+
if (!sourceColumn || !targetColumn) fail4("foreign key references an unknown column");
|
|
922
|
+
else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail4("foreign key column types must match");
|
|
923
|
+
}
|
|
924
|
+
if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
|
|
925
|
+
if (fk.columns.some((name2) => table.columns.find((column) => column.columnName === name2)?.nullable === false || table.primaryKey?.includes(name2))) fail4("SET NULL requires nullable source columns");
|
|
298
926
|
}
|
|
299
927
|
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))))
|
|
928
|
+
if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail4("foreign key target columns must form a unique key");
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
for (const [viewIndex, view] of (definition.views ?? []).entries()) {
|
|
932
|
+
for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
|
|
933
|
+
ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
|
|
301
934
|
}
|
|
302
935
|
}
|
|
303
936
|
const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
@@ -394,27 +1027,31 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
394
1027
|
const visiting = /* @__PURE__ */ new Set();
|
|
395
1028
|
const visited = /* @__PURE__ */ new Set();
|
|
396
1029
|
let cycleReported = false;
|
|
397
|
-
const visit2 = (
|
|
398
|
-
if (visited.has(
|
|
399
|
-
if (visiting.has(
|
|
1030
|
+
const visit2 = (name2, path) => {
|
|
1031
|
+
if (visited.has(name2) || cycleReported) return;
|
|
1032
|
+
if (visiting.has(name2)) {
|
|
400
1033
|
cycleReported = true;
|
|
401
1034
|
ctx.addIssue({
|
|
402
1035
|
code: "custom",
|
|
403
1036
|
path: ["views"],
|
|
404
|
-
message: `view dependencies form a cycle: ${[...path,
|
|
1037
|
+
message: `view dependencies form a cycle: ${[...path, name2].join(" -> ")}`
|
|
405
1038
|
});
|
|
406
1039
|
return;
|
|
407
1040
|
}
|
|
408
|
-
visiting.add(
|
|
409
|
-
const view = viewByName.get(
|
|
1041
|
+
visiting.add(name2);
|
|
1042
|
+
const view = viewByName.get(name2);
|
|
410
1043
|
for (const dependency of view?.dependencies ?? []) {
|
|
411
|
-
if (viewByName.has(dependency)) visit2(dependency, [...path,
|
|
1044
|
+
if (viewByName.has(dependency)) visit2(dependency, [...path, name2]);
|
|
412
1045
|
}
|
|
413
|
-
visiting.delete(
|
|
414
|
-
visited.add(
|
|
1046
|
+
visiting.delete(name2);
|
|
1047
|
+
visited.add(name2);
|
|
415
1048
|
};
|
|
416
1049
|
for (const view of views) visit2(view.view, []);
|
|
417
1050
|
});
|
|
1051
|
+
var canonicalDefinitionSchema = localDefinitionSchema.safeExtend({
|
|
1052
|
+
/** Flattened, pinned snapshots. Imported relations never participate in this namespace's DDL. */
|
|
1053
|
+
imports: zod.z.array(localDefinitionSchema).max(32).optional()
|
|
1054
|
+
});
|
|
418
1055
|
function canonicalRelations(definition) {
|
|
419
1056
|
const views = (definition.views ?? []).map((view) => ({
|
|
420
1057
|
table: view.view,
|
|
@@ -423,8 +1060,12 @@ function canonicalRelations(definition) {
|
|
|
423
1060
|
...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
|
|
424
1061
|
links: view.links,
|
|
425
1062
|
indexes: [],
|
|
426
|
-
|
|
427
|
-
|
|
1063
|
+
// A resolved table's write policies (evaluated against the resolved row)
|
|
1064
|
+
// ride alongside the view's read policy.
|
|
1065
|
+
policies: { read: view.policies.read, ...view.resolved?.policies ?? {} },
|
|
1066
|
+
actions: [],
|
|
1067
|
+
...view.resolved !== void 0 ? { resolved: view.resolved } : {},
|
|
1068
|
+
...view.internal ? { internal: true } : {}
|
|
428
1069
|
}));
|
|
429
1070
|
return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
|
|
430
1071
|
}
|
|
@@ -434,13 +1075,14 @@ function normalizeCanonical(definition) {
|
|
|
434
1075
|
...definition,
|
|
435
1076
|
enums: sortByName(definition.enums),
|
|
436
1077
|
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t) => {
|
|
437
|
-
const { cowMutations, foreignKeys, ...rest } = t;
|
|
1078
|
+
const { cowMutations, foreignKeys, checks, ...rest } = t;
|
|
438
1079
|
return {
|
|
439
1080
|
...rest,
|
|
440
1081
|
links: sortByName(t.links),
|
|
441
1082
|
indexes: [...t.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
|
|
442
1083
|
actions: sortByName(t.actions),
|
|
443
1084
|
...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
|
|
1085
|
+
...checks?.length ? { checks: sortByName(checks) } : {},
|
|
444
1086
|
// Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
|
|
445
1087
|
...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
|
|
446
1088
|
};
|
|
@@ -453,13 +1095,22 @@ function normalizeCanonical(definition) {
|
|
|
453
1095
|
dependencies: [...view.dependencies].sort()
|
|
454
1096
|
}));
|
|
455
1097
|
}
|
|
1098
|
+
if (definition.imports?.length) {
|
|
1099
|
+
normalized.imports = definition.imports.map(normalizeCanonical).sort((a, b) => a.namespace.localeCompare(b.namespace));
|
|
1100
|
+
} else {
|
|
1101
|
+
delete normalized.imports;
|
|
1102
|
+
}
|
|
456
1103
|
return normalized;
|
|
457
1104
|
}
|
|
458
1105
|
|
|
459
1106
|
// src/ir/index.ts
|
|
460
|
-
var IR_VERSION =
|
|
1107
|
+
var IR_VERSION = 5;
|
|
461
1108
|
var LEGACY_IR_VERSION = 1;
|
|
462
|
-
var
|
|
1109
|
+
var GUARDED_IR_VERSION = 2;
|
|
1110
|
+
var COMPOSED_IR_VERSION = 3;
|
|
1111
|
+
var SQL_IR_VERSION = 4;
|
|
1112
|
+
var RUNTIME_SQL_IR_VERSION = 5;
|
|
1113
|
+
var irVersionSchema = zod.z.union([zod.z.literal(LEGACY_IR_VERSION), zod.z.literal(GUARDED_IR_VERSION), zod.z.literal(COMPOSED_IR_VERSION), zod.z.literal(SQL_IR_VERSION), zod.z.literal(RUNTIME_SQL_IR_VERSION)]);
|
|
463
1114
|
var operationVersionSchema = zod.z.object({ irVersion: irVersionSchema });
|
|
464
1115
|
function requiredArtifactIrVersion(operations) {
|
|
465
1116
|
let required = LEGACY_IR_VERSION;
|
|
@@ -511,8 +1162,8 @@ var predIrSchema = zod.z.lazy(
|
|
|
511
1162
|
value: zod.z.number(),
|
|
512
1163
|
where: predIrSchema.optional()
|
|
513
1164
|
}),
|
|
514
|
-
zod.z.object({ kind: zod.z.literal("capability"), key: zod.z.string() }),
|
|
515
|
-
zod.z.object({ kind: zod.z.literal("role"), key: zod.z.string() }),
|
|
1165
|
+
zod.z.object({ kind: zod.z.literal("capability"), key: zod.z.string(), namespace: zod.z.string().optional() }),
|
|
1166
|
+
zod.z.object({ kind: zod.z.literal("role"), key: zod.z.string(), namespace: zod.z.string().optional() }),
|
|
516
1167
|
zod.z.object({ kind: zod.z.literal("dynamicFilter"), slot: zod.z.string() }),
|
|
517
1168
|
zod.z.object({ kind: zod.z.literal("const"), value: zod.z.boolean() })
|
|
518
1169
|
])
|
|
@@ -540,6 +1191,21 @@ var selectionIrSchema = zod.z.lazy(
|
|
|
540
1191
|
() => zod.z.object({
|
|
541
1192
|
typename: zod.z.string(),
|
|
542
1193
|
columns: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })),
|
|
1194
|
+
cells: zod.z.object({
|
|
1195
|
+
alias: zod.z.string(),
|
|
1196
|
+
typename: zod.z.string(),
|
|
1197
|
+
fields: zod.z.array(zod.z.object({
|
|
1198
|
+
alias: zod.z.string(),
|
|
1199
|
+
column: zod.z.string(),
|
|
1200
|
+
typename: zod.z.string(),
|
|
1201
|
+
parts: zod.z.array(zod.z.object({
|
|
1202
|
+
alias: zod.z.string(),
|
|
1203
|
+
part: zod.z.enum(["source", "at", "by", "conflicts"]),
|
|
1204
|
+
fields: zod.z.array(zod.z.object({ alias: zod.z.string(), field: zod.z.enum(["source", "at", "value"]) })).optional(),
|
|
1205
|
+
typename: zod.z.string().optional()
|
|
1206
|
+
}))
|
|
1207
|
+
}))
|
|
1208
|
+
}).optional(),
|
|
543
1209
|
relations: zod.z.array(zod.z.object({
|
|
544
1210
|
alias: zod.z.string(),
|
|
545
1211
|
link: zod.z.string(),
|
|
@@ -561,6 +1227,11 @@ var selectionIrSchema = zod.z.lazy(
|
|
|
561
1227
|
})
|
|
562
1228
|
);
|
|
563
1229
|
var includeIfSchema = zod.z.object({ variable: zod.z.string(), negated: zod.z.boolean() });
|
|
1230
|
+
var prefetchPlanIrSchema = zod.z.object({
|
|
1231
|
+
source: zod.z.string(),
|
|
1232
|
+
maxStalenessMs: zod.z.number().int().positive().optional(),
|
|
1233
|
+
key: zod.z.union([zod.z.object({ one: valueIrSchema }), zod.z.object({ many: valueIrSchema })])
|
|
1234
|
+
});
|
|
564
1235
|
var selectRootSchema = zod.z.object({
|
|
565
1236
|
kind: zod.z.literal("select"),
|
|
566
1237
|
alias: zod.z.string(),
|
|
@@ -572,7 +1243,8 @@ var selectRootSchema = zod.z.object({
|
|
|
572
1243
|
distinctOn: zod.z.union([zod.z.array(zod.z.string()), zod.z.object({ slot: zod.z.string() })]).optional(),
|
|
573
1244
|
limit: limitIrSchema,
|
|
574
1245
|
offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
|
|
575
|
-
includeIf: includeIfSchema.optional()
|
|
1246
|
+
includeIf: includeIfSchema.optional(),
|
|
1247
|
+
prefetch: zod.z.array(prefetchPlanIrSchema).optional()
|
|
576
1248
|
});
|
|
577
1249
|
var byPkRootSchema = zod.z.object({
|
|
578
1250
|
kind: zod.z.literal("byPk"),
|
|
@@ -582,7 +1254,8 @@ var byPkRootSchema = zod.z.object({
|
|
|
582
1254
|
pk: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).min(1),
|
|
583
1255
|
/** Policy predicate — ANDed with the PK match. */
|
|
584
1256
|
where: predIrSchema,
|
|
585
|
-
includeIf: includeIfSchema.optional()
|
|
1257
|
+
includeIf: includeIfSchema.optional(),
|
|
1258
|
+
prefetch: zod.z.array(prefetchPlanIrSchema).optional()
|
|
586
1259
|
});
|
|
587
1260
|
var aggregateRootSchema = zod.z.object({
|
|
588
1261
|
kind: zod.z.literal("aggregate"),
|
|
@@ -640,7 +1313,20 @@ var mutationRootSchema = zod.z.object({
|
|
|
640
1313
|
* coalesce view falls back to synced. Both are gated on a matching synced
|
|
641
1314
|
* row and carry the write policy in `where` like update/delete.
|
|
642
1315
|
*/
|
|
643
|
-
op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
|
|
1316
|
+
op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert", "resolvedWrite"]),
|
|
1317
|
+
/**
|
|
1318
|
+
* resolvedWrite: a write to a resolved table (`table` is its view). The
|
|
1319
|
+
* executor gates on the resolved row (write policy in `where`), writes only
|
|
1320
|
+
* the touched cells into the manual source, then re-checks `insertCheck`
|
|
1321
|
+
* against the resolved row. `revert` clears `revertColumns` from the manual
|
|
1322
|
+
* source; `delete` tombstones the row there.
|
|
1323
|
+
*/
|
|
1324
|
+
resolvedAction: zod.z.enum(["insert", "update", "delete", "revert"]).optional(),
|
|
1325
|
+
revertColumns: zod.z.array(zod.z.string()).optional(),
|
|
1326
|
+
/** `@writeback(source:, mode:)` directives on this root. */
|
|
1327
|
+
writeback: zod.z.array(zod.z.object({ source: zod.z.string(), mode: zod.z.enum(["sync", "async"]) })).optional(),
|
|
1328
|
+
/** `@localOnly(source:)` — writeback columns deliberately kept local for these sources. */
|
|
1329
|
+
localOnly: zod.z.array(zod.z.string()).optional(),
|
|
644
1330
|
alias: zod.z.string(),
|
|
645
1331
|
table: zod.z.string(),
|
|
646
1332
|
/** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
|
|
@@ -704,7 +1390,31 @@ var variableIrSchema = zod.z.object({
|
|
|
704
1390
|
/** Absent = no default declared; see {@link variableDefaultIrSchema}. */
|
|
705
1391
|
defaultValue: variableDefaultIrSchema.optional()
|
|
706
1392
|
});
|
|
1393
|
+
var sqlColumnSchema = zod.z.object({ name: zod.z.string(), type: zod.z.enum(["text", "boolean", "integer", "bigint", "numeric", "double", "uuid", "date", "timestamptz"]), nullable: zod.z.boolean() });
|
|
707
1394
|
var compiledOperationSchema = zod.z.object({
|
|
1395
|
+
language: zod.z.literal("sql").optional(),
|
|
1396
|
+
/** Server-compiled object catalog and permission ceiling, never caller SQL. */
|
|
1397
|
+
sqlQuery: zod.z.object({
|
|
1398
|
+
catalog: zod.z.object({
|
|
1399
|
+
identity: zod.z.string(),
|
|
1400
|
+
objects: zod.z.array(zod.z.object({
|
|
1401
|
+
ref: zod.z.string(),
|
|
1402
|
+
table: zod.z.string(),
|
|
1403
|
+
columns: zod.z.array(sqlColumnSchema.extend({ type: zod.z.string() })).min(1),
|
|
1404
|
+
scan: zod.z.string().min(1),
|
|
1405
|
+
read: zod.z.array(zod.z.string()).min(1)
|
|
1406
|
+
}).strict()).min(1).max(256)
|
|
1407
|
+
}).strict(),
|
|
1408
|
+
read: zod.z.array(zod.z.string()).min(1).max(256),
|
|
1409
|
+
maxRows: zod.z.number().int().min(1).max(1e4)
|
|
1410
|
+
}).strict().optional(),
|
|
1411
|
+
sql: zod.z.object({
|
|
1412
|
+
text: zod.z.string().min(1).max(1048576),
|
|
1413
|
+
columns: zod.z.array(sqlColumnSchema).min(1).max(256),
|
|
1414
|
+
parameters: zod.z.array(zod.z.string()).max(128),
|
|
1415
|
+
read: zod.z.array(zod.z.string()).min(1).max(256),
|
|
1416
|
+
maxRows: zod.z.number().int().min(1).max(1e4)
|
|
1417
|
+
}).strict().optional(),
|
|
708
1418
|
irVersion: irVersionSchema,
|
|
709
1419
|
name: zod.z.string(),
|
|
710
1420
|
opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
@@ -716,8 +1426,16 @@ var compiledOperationSchema = zod.z.object({
|
|
|
716
1426
|
variables: zod.z.array(variableIrSchema),
|
|
717
1427
|
/** Every table the op touches — data-token scoping + audit. */
|
|
718
1428
|
tableSet: zod.z.array(zod.z.string()).min(1),
|
|
719
|
-
roots: zod.z.array(rootIrSchema)
|
|
1429
|
+
roots: zod.z.array(rootIrSchema)
|
|
720
1430
|
}).superRefine((operation, ctx) => {
|
|
1431
|
+
if (operation.language === "sql") {
|
|
1432
|
+
const validPlan = operation.sqlQuery ? !operation.sql && operation.irVersion === RUNTIME_SQL_IR_VERSION : !!operation.sql && operation.irVersion === SQL_IR_VERSION;
|
|
1433
|
+
if (!validPlan || operation.kind !== "query" || operation.roots.length !== 0 || !operation.requires?.capability) {
|
|
1434
|
+
ctx.addIssue({ code: "custom", message: "SQL operations require a query-only v4 plan or v5 runtime contract, capability, and no GraphQL roots" });
|
|
1435
|
+
}
|
|
1436
|
+
} else if (operation.sql || operation.sqlQuery || operation.roots.length === 0) {
|
|
1437
|
+
ctx.addIssue({ code: "custom", message: "GraphQL operations require roots and cannot carry SQL" });
|
|
1438
|
+
}
|
|
721
1439
|
for (const [index, root] of operation.roots.entries()) {
|
|
722
1440
|
if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
|
|
723
1441
|
if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
|
|
@@ -725,13 +1443,55 @@ var compiledOperationSchema = zod.z.object({
|
|
|
725
1443
|
}
|
|
726
1444
|
}
|
|
727
1445
|
});
|
|
1446
|
+
var resolvedSourceIrSchema = zod.z.object({
|
|
1447
|
+
name: zod.z.string(),
|
|
1448
|
+
kind: zod.z.enum(["manual", "etl", "prefetch"]),
|
|
1449
|
+
/** Logical table key of the backing relation. */
|
|
1450
|
+
table: zod.z.string(),
|
|
1451
|
+
columns: zod.z.array(zod.z.string()),
|
|
1452
|
+
key: zod.z.string().optional(),
|
|
1453
|
+
fetch: zod.z.union([
|
|
1454
|
+
zod.z.object({ shape: zod.z.literal("single") }),
|
|
1455
|
+
zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().positive() })
|
|
1456
|
+
]).optional(),
|
|
1457
|
+
function: zod.z.object({ kind: zod.z.enum(["managed", "native"]), id: zod.z.string() }).optional(),
|
|
1458
|
+
prefetchDefaults: zod.z.object({
|
|
1459
|
+
maxStalenessMs: zod.z.number().int().positive(),
|
|
1460
|
+
timeoutMs: zod.z.number().int().positive(),
|
|
1461
|
+
onError: zod.z.enum(["fallback", "fail"])
|
|
1462
|
+
}).optional(),
|
|
1463
|
+
writeback: zod.z.object({
|
|
1464
|
+
columns: zod.z.array(zod.z.string()),
|
|
1465
|
+
defaultMode: zod.z.enum(["sync", "async"]),
|
|
1466
|
+
timeoutMs: zod.z.number().int().positive(),
|
|
1467
|
+
clearManualOnConfirm: zod.z.boolean()
|
|
1468
|
+
}).optional()
|
|
1469
|
+
});
|
|
1470
|
+
var resolvedMetaIrSchema = zod.z.object({
|
|
1471
|
+
/** Logical pk column of the resolved view (single-column). */
|
|
1472
|
+
pk: zod.z.string(),
|
|
1473
|
+
/** Source name of the manual source. */
|
|
1474
|
+
manual: zod.z.string(),
|
|
1475
|
+
sources: zod.z.array(resolvedSourceIrSchema),
|
|
1476
|
+
deletedBy: zod.z.array(zod.z.string()),
|
|
1477
|
+
/**
|
|
1478
|
+
* Materialized: the view reads `table`, which the data plane refreshes from
|
|
1479
|
+
* the `compute` view (claims-gated) whenever it writes a source row.
|
|
1480
|
+
*/
|
|
1481
|
+
materialized: zod.z.object({ table: zod.z.string(), compute: zod.z.string() }).optional()
|
|
1482
|
+
});
|
|
728
1483
|
var tableMetaIrSchema = zod.z.object({
|
|
729
1484
|
table: zod.z.string(),
|
|
1485
|
+
namespace: zod.z.string().optional(),
|
|
1486
|
+
dependencies: zod.z.array(zod.z.string()).optional(),
|
|
1487
|
+
foreignReferences: zod.z.array(zod.z.object({ column: zod.z.string(), target: zod.z.string(), targetColumn: zod.z.string() })).optional(),
|
|
1488
|
+
exportRead: zod.z.union([zod.z.literal(true), zod.z.object({ capability: zod.z.string() }), zod.z.object({ role: zod.z.string() })]).optional(),
|
|
1489
|
+
exportWrite: zod.z.union([zod.z.literal(true), zod.z.object({ capability: zod.z.string() }), zod.z.object({ role: zod.z.string() })]).optional(),
|
|
730
1490
|
schema: zod.z.string(),
|
|
731
1491
|
typename: zod.z.string(),
|
|
732
1492
|
/**
|
|
733
1493
|
* True when statements over this relation evaluate a claims-gated synced
|
|
734
|
-
* view: synced twins (`source:
|
|
1494
|
+
* view: synced twins (`source: schema.table`) materialize as views whose baked
|
|
735
1495
|
* policy reads `current_setting('request.jwt.claims')`, and namespace views
|
|
736
1496
|
* inherit the gate transitively through their declared dependencies. The
|
|
737
1497
|
* executor must stamp that GUC (transaction-scoped) before any statement
|
|
@@ -765,6 +1525,22 @@ var tableMetaIrSchema = zod.z.object({
|
|
|
765
1525
|
* `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
|
|
766
1526
|
*/
|
|
767
1527
|
touchOnUpdate: zod.z.array(zod.z.string()).optional(),
|
|
1528
|
+
/**
|
|
1529
|
+
* Per-cell metadata: logical column → the logical names of its cell-part
|
|
1530
|
+
* columns (which are listed in `columns`, but never filterable, orderable or
|
|
1531
|
+
* writable). Tables stamp `at`/`by` (and a manual source `rev`) on every
|
|
1532
|
+
* write; resolved views expose `source`/`at`/`by`/`conflicts`. Omitted when
|
|
1533
|
+
* the relation tracks no cells.
|
|
1534
|
+
*/
|
|
1535
|
+
cells: zod.z.record(zod.z.string(), zod.z.object({
|
|
1536
|
+
at: zod.z.string().optional(),
|
|
1537
|
+
by: zod.z.string().optional(),
|
|
1538
|
+
rev: zod.z.string().optional(),
|
|
1539
|
+
source: zod.z.string().optional(),
|
|
1540
|
+
conflicts: zod.z.string().optional()
|
|
1541
|
+
}).strict()).optional(),
|
|
1542
|
+
/** Resolved-table view metadata (sources, prefetch/writeback config) for the data plane. */
|
|
1543
|
+
resolved: resolvedMetaIrSchema.optional(),
|
|
768
1544
|
constraints: zod.z.array(zod.z.object({ name: zod.z.string(), columns: zod.z.array(zod.z.string()) })),
|
|
769
1545
|
/** Depth/node caps for dynamic filter trees on this table. */
|
|
770
1546
|
dynamicFilterMaxDepth: zod.z.number().int().positive().default(6),
|
|
@@ -774,13 +1550,21 @@ var compiledArtifactSchema = zod.z.object({
|
|
|
774
1550
|
irVersion: irVersionSchema,
|
|
775
1551
|
namespace: zod.z.string(),
|
|
776
1552
|
schemaHash: zod.z.string(),
|
|
1553
|
+
ownSchemaHash: zod.z.string().optional(),
|
|
1554
|
+
dependencies: zod.z.record(zod.z.string(), zod.z.string()).optional(),
|
|
1555
|
+
readPolicies: zod.z.record(zod.z.string(), predIrSchema).optional(),
|
|
777
1556
|
opsHash: zod.z.string(),
|
|
1557
|
+
/** Apply-observed synced objects without a serving backing; not part of the authored schema hash. */
|
|
1558
|
+
unbackedTables: zod.z.array(zod.z.string()).optional(),
|
|
778
1559
|
tables: zod.z.record(zod.z.string(), tableMetaIrSchema),
|
|
779
1560
|
operations: zod.z.record(zod.z.string(), compiledOperationSchema)
|
|
780
1561
|
}).superRefine((artifact, ctx) => {
|
|
781
|
-
|
|
1562
|
+
if (Object.keys(artifact.dependencies ?? {}).length > 0 && artifact.irVersion < COMPOSED_IR_VERSION) {
|
|
1563
|
+
ctx.addIssue({ code: "custom", path: ["irVersion"], message: "cross-namespace dependencies require IR v3" });
|
|
1564
|
+
}
|
|
1565
|
+
for (const [name2, operation] of Object.entries(artifact.operations)) {
|
|
782
1566
|
if (operation.irVersion > artifact.irVersion) {
|
|
783
|
-
ctx.addIssue({ code: "custom", path: ["operations",
|
|
1567
|
+
ctx.addIssue({ code: "custom", path: ["operations", name2, "irVersion"], message: "operation requires a newer IR version than its artifact" });
|
|
784
1568
|
}
|
|
785
1569
|
}
|
|
786
1570
|
});
|
|
@@ -793,10 +1577,119 @@ var IrVersionError = class extends Error {
|
|
|
793
1577
|
function normalizeIr(raw) {
|
|
794
1578
|
if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
|
|
795
1579
|
const version = raw.irVersion;
|
|
796
|
-
if (version
|
|
1580
|
+
if (!irVersionSchema.safeParse(version).success) throw new IrVersionError(version);
|
|
797
1581
|
return compiledArtifactSchema.parse(raw);
|
|
798
1582
|
}
|
|
799
1583
|
|
|
1584
|
+
// src/sdl/naming.ts
|
|
1585
|
+
function camelCase(name2) {
|
|
1586
|
+
const segments = name2.split("_").filter((segment) => segment.length > 0);
|
|
1587
|
+
const first = segments[0];
|
|
1588
|
+
if (first === void 0) return name2;
|
|
1589
|
+
const head = first.charAt(0).toLowerCase() + first.slice(1);
|
|
1590
|
+
return head + segments.slice(1).map(capitalizeFirst).join("");
|
|
1591
|
+
}
|
|
1592
|
+
function pascalCase(name2) {
|
|
1593
|
+
return capitalizeFirst(camelCase(name2));
|
|
1594
|
+
}
|
|
1595
|
+
function capitalizeFirst(segment) {
|
|
1596
|
+
return segment.charAt(0).toUpperCase() + segment.slice(1);
|
|
1597
|
+
}
|
|
1598
|
+
function singularizeWord(word) {
|
|
1599
|
+
if (word.endsWith("ies") && word.length > 3) return `${word.slice(0, -3)}y`;
|
|
1600
|
+
for (const suffix of ["sses", "xes", "ches", "shes"]) {
|
|
1601
|
+
if (word.endsWith(suffix)) return word.slice(0, -2);
|
|
1602
|
+
}
|
|
1603
|
+
if (word.endsWith("ss")) return word;
|
|
1604
|
+
if (word.endsWith("s") && word.length > 1) return word.slice(0, -1);
|
|
1605
|
+
return word;
|
|
1606
|
+
}
|
|
1607
|
+
function typeName(table) {
|
|
1608
|
+
const segments = table.split("_").filter((segment) => segment.length > 0);
|
|
1609
|
+
const last = segments[segments.length - 1];
|
|
1610
|
+
if (last === void 0) return pascalCase(table);
|
|
1611
|
+
return [...segments.slice(0, -1), singularizeWord(last)].map(capitalizeFirst).join("");
|
|
1612
|
+
}
|
|
1613
|
+
function enumTypeName(name2) {
|
|
1614
|
+
return pascalCase(name2);
|
|
1615
|
+
}
|
|
1616
|
+
var SCALAR_FOR_COLUMN_TYPE = {
|
|
1617
|
+
uuid: "uuid",
|
|
1618
|
+
text: "String",
|
|
1619
|
+
integer: "Int",
|
|
1620
|
+
bigint: "bigint",
|
|
1621
|
+
numeric: "numeric",
|
|
1622
|
+
double: "Float",
|
|
1623
|
+
boolean: "Boolean",
|
|
1624
|
+
timestamptz: "timestamptz",
|
|
1625
|
+
date: "date",
|
|
1626
|
+
jsonb: "jsonb"
|
|
1627
|
+
};
|
|
1628
|
+
function graphqlScalarFor(type) {
|
|
1629
|
+
return SCALAR_FOR_COLUMN_TYPE[type];
|
|
1630
|
+
}
|
|
1631
|
+
var COLUMN_TYPE_BY_SCALAR = new Map([
|
|
1632
|
+
["ID", "text"],
|
|
1633
|
+
...COLUMN_TYPES.map((type) => [graphqlScalarFor(type), type])
|
|
1634
|
+
]);
|
|
1635
|
+
function columnTypeForScalar(scalarName) {
|
|
1636
|
+
return COLUMN_TYPE_BY_SCALAR.get(scalarName);
|
|
1637
|
+
}
|
|
1638
|
+
var VARIABLE_SCALAR_NAMES = [...COLUMN_TYPE_BY_SCALAR.keys()];
|
|
1639
|
+
var PLATFORM_USER_TYPENAME = "PlatformUser";
|
|
1640
|
+
function tableNames(table) {
|
|
1641
|
+
const root = camelCase(table);
|
|
1642
|
+
const rootPascal = pascalCase(table);
|
|
1643
|
+
const type = typeName(table);
|
|
1644
|
+
return {
|
|
1645
|
+
typename: type,
|
|
1646
|
+
selectRoot: root,
|
|
1647
|
+
byPkRoot: `${root}ByPk`,
|
|
1648
|
+
aggregateRoot: `${root}Aggregate`,
|
|
1649
|
+
insertRoot: `insert${rootPascal}`,
|
|
1650
|
+
insertOneRoot: `insert${rootPascal}One`,
|
|
1651
|
+
updateRoot: `update${rootPascal}`,
|
|
1652
|
+
updateByPkRoot: `update${rootPascal}ByPk`,
|
|
1653
|
+
deleteRoot: `delete${rootPascal}`,
|
|
1654
|
+
deleteByPkRoot: `delete${rootPascal}ByPk`,
|
|
1655
|
+
revertByPkRoot: `revert${rootPascal}ByPk`,
|
|
1656
|
+
boolExp: `${type}BoolExp`,
|
|
1657
|
+
orderBy: `${type}OrderBy`,
|
|
1658
|
+
selectColumn: `${type}SelectColumn`,
|
|
1659
|
+
insertInput: `${type}InsertInput`,
|
|
1660
|
+
setInput: `${type}SetInput`,
|
|
1661
|
+
appendInput: `${type}AppendInput`,
|
|
1662
|
+
incInput: `${type}IncInput`,
|
|
1663
|
+
onConflict: `${type}OnConflict`,
|
|
1664
|
+
pkColumnsInput: `${type}PkColumnsInput`,
|
|
1665
|
+
constraintEnum: `${type}Constraint`,
|
|
1666
|
+
updateColumnEnum: `${type}UpdateColumn`,
|
|
1667
|
+
mutationResponse: `${type}MutationResponse`,
|
|
1668
|
+
aggregate: `${type}Aggregate`,
|
|
1669
|
+
aggregateFields: `${type}AggregateFields`,
|
|
1670
|
+
sumFields: `${type}SumFields`,
|
|
1671
|
+
avgFields: `${type}AvgFields`,
|
|
1672
|
+
aggregateBoolExp: `${type}AggregateBoolExp`,
|
|
1673
|
+
aggregateBoolExpCount: `${type}AggregateBoolExpCount`,
|
|
1674
|
+
aggregateGroupsRoot: `${root}AggregateGroups`,
|
|
1675
|
+
groupByColumnEnum: `${type}GroupByColumn`,
|
|
1676
|
+
groupByInput: `${type}GroupBy`,
|
|
1677
|
+
groupKey: `${type}GroupKey`,
|
|
1678
|
+
aggregateGroup: `${type}AggregateGroup`
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
var CELL_TYPENAMES = {
|
|
1682
|
+
/** A plain table's cell: when and by whom it was last written. */
|
|
1683
|
+
stamp: "CellStamp",
|
|
1684
|
+
/** A resolved table's cell: which source won, when, by whom, and newer losers. */
|
|
1685
|
+
resolved: "ResolvedCell",
|
|
1686
|
+
/** A newer candidate that lost to the column's rule. */
|
|
1687
|
+
conflict: "CellConflict"
|
|
1688
|
+
};
|
|
1689
|
+
function cellsTypename(typename) {
|
|
1690
|
+
return `${typename}Cells`;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
800
1693
|
// src/predicate-v2/ast.ts
|
|
801
1694
|
var POLICY_COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
|
|
802
1695
|
function walkPolicy(node, visit2) {
|
|
@@ -833,7 +1726,7 @@ var IDENTIFIER_CHAR = /[a-zA-Z0-9_]/;
|
|
|
833
1726
|
var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
|
|
834
1727
|
var SCALAR_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => !type.endsWith("[]")).map(([key]) => key);
|
|
835
1728
|
var ARRAY_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => type.endsWith("[]")).map(([key]) => key);
|
|
836
|
-
function
|
|
1729
|
+
function tokenize2(source) {
|
|
837
1730
|
const tokens = [];
|
|
838
1731
|
let position = 0;
|
|
839
1732
|
while (position < source.length) {
|
|
@@ -912,7 +1805,7 @@ function readString(source, start) {
|
|
|
912
1805
|
}
|
|
913
1806
|
throw new PolicyParseError("unterminated string literal", start);
|
|
914
1807
|
}
|
|
915
|
-
var
|
|
1808
|
+
var Parser2 = class {
|
|
916
1809
|
constructor(tokens) {
|
|
917
1810
|
this.tokens = tokens;
|
|
918
1811
|
}
|
|
@@ -1086,7 +1979,7 @@ var Parser = class {
|
|
|
1086
1979
|
function parsePolicy(source) {
|
|
1087
1980
|
const trimmed = source.trim();
|
|
1088
1981
|
if (trimmed.length === 0) throw new PolicyParseError("empty policy expression", 0);
|
|
1089
|
-
return { source: trimmed, ast: new
|
|
1982
|
+
return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
|
|
1090
1983
|
}
|
|
1091
1984
|
|
|
1092
1985
|
// src/predicate-v2/compile.ts
|
|
@@ -1486,6 +2379,7 @@ var PolicyInjector = class {
|
|
|
1486
2379
|
injectMutation(root) {
|
|
1487
2380
|
const table = root.table;
|
|
1488
2381
|
const returning = root.returning ? this.injectSelection(root.returning, table) : root.returning;
|
|
2382
|
+
if (root.op === "resolvedWrite") return this.injectResolvedWrite(root, returning);
|
|
1489
2383
|
if (root.op === "insert" || root.op === "insertOne") {
|
|
1490
2384
|
const insertPolicy = this.insert(table);
|
|
1491
2385
|
if (predContainsExists(insertPolicy)) {
|
|
@@ -1508,7 +2402,7 @@ var PolicyInjector = class {
|
|
|
1508
2402
|
const updatePolicy = this.update(table);
|
|
1509
2403
|
const where = andPred(updatePolicy, root.where ? this.injectDocPred(root.where, table) : void 0);
|
|
1510
2404
|
if (root.op === "update" || root.op === "updateByPk") {
|
|
1511
|
-
const check = predContainsExists(updatePolicy) ? {} : { insertCheck: updatePolicy };
|
|
2405
|
+
const check = predContainsExists(updatePolicy) && !this.ctx.composed ? {} : { insertCheck: updatePolicy };
|
|
1512
2406
|
return { ...root, where, ...check, returning };
|
|
1513
2407
|
}
|
|
1514
2408
|
if (root.op === "cowOverride") {
|
|
@@ -1522,11 +2416,29 @@ var PolicyInjector = class {
|
|
|
1522
2416
|
const pins = (this.ctx.pins.get(table) ?? []).map(
|
|
1523
2417
|
(pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
|
|
1524
2418
|
);
|
|
1525
|
-
const check = predContainsExists(updatePolicy) ? {} : { insertCheck: updatePolicy };
|
|
2419
|
+
const check = predContainsExists(updatePolicy) && !this.ctx.composed ? {} : { insertCheck: updatePolicy };
|
|
1526
2420
|
return { ...root, pinnedColumns: pins, cowCopyCheck: insertPolicy, where, ...check, returning };
|
|
1527
2421
|
}
|
|
1528
2422
|
return { ...root, where, returning };
|
|
1529
2423
|
}
|
|
2424
|
+
/**
|
|
2425
|
+
* Resolved writes are policed against the RESOLVED row, not the sparse
|
|
2426
|
+
* manual row: `where` gates the existing row (update/revert/delete) and
|
|
2427
|
+
* `insertCheck` is re-evaluated as a SELECT over the view after the write,
|
|
2428
|
+
* so link-traversing (exists) policies are representable here.
|
|
2429
|
+
*/
|
|
2430
|
+
injectResolvedWrite(root, returning) {
|
|
2431
|
+
const table = root.table;
|
|
2432
|
+
if (root.resolvedAction === "insert") {
|
|
2433
|
+
const pins = (this.ctx.pins.get(table) ?? []).map(
|
|
2434
|
+
(pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
|
|
2435
|
+
);
|
|
2436
|
+
return { ...root, pinnedColumns: pins, insertCheck: this.insert(table), returning };
|
|
2437
|
+
}
|
|
2438
|
+
const updatePolicy = this.update(table);
|
|
2439
|
+
const where = andPred(updatePolicy, root.where ? this.injectDocPred(root.where, table) : void 0);
|
|
2440
|
+
return root.resolvedAction === "delete" ? { ...root, where, returning } : { ...root, where, insertCheck: updatePolicy, returning };
|
|
2441
|
+
}
|
|
1530
2442
|
};
|
|
1531
2443
|
function collectTableSet(roots, ctx) {
|
|
1532
2444
|
const tables = /* @__PURE__ */ new Set();
|
|
@@ -1573,136 +2485,40 @@ function collectTableSet(roots, ctx) {
|
|
|
1573
2485
|
tables.add(root.table);
|
|
1574
2486
|
switch (root.kind) {
|
|
1575
2487
|
case "select":
|
|
1576
|
-
case "byPk":
|
|
1577
|
-
visitPred(root.where, root.table);
|
|
1578
|
-
visitSelection(root.selection, root.table);
|
|
1579
|
-
break;
|
|
1580
|
-
case "aggregate":
|
|
1581
|
-
visitPred(root.where, root.table);
|
|
1582
|
-
if (root.nodes) visitSelection(root.nodes, root.table);
|
|
1583
|
-
break;
|
|
1584
|
-
case "aggregateGroups":
|
|
1585
|
-
visitPred(root.where, root.table);
|
|
1586
|
-
break;
|
|
1587
|
-
case "mutation":
|
|
1588
|
-
if (root.cowSynced !== void 0) tables.add(root.cowSynced);
|
|
1589
|
-
|
|
1590
|
-
if (root.
|
|
1591
|
-
if (root.
|
|
1592
|
-
if (root.
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
const
|
|
1608
|
-
|
|
1609
|
-
}
|
|
1610
|
-
|
|
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
|
-
};
|
|
2488
|
+
case "byPk":
|
|
2489
|
+
visitPred(root.where, root.table);
|
|
2490
|
+
visitSelection(root.selection, root.table);
|
|
2491
|
+
break;
|
|
2492
|
+
case "aggregate":
|
|
2493
|
+
visitPred(root.where, root.table);
|
|
2494
|
+
if (root.nodes) visitSelection(root.nodes, root.table);
|
|
2495
|
+
break;
|
|
2496
|
+
case "aggregateGroups":
|
|
2497
|
+
visitPred(root.where, root.table);
|
|
2498
|
+
break;
|
|
2499
|
+
case "mutation":
|
|
2500
|
+
if (root.cowSynced !== void 0) tables.add(root.cowSynced);
|
|
2501
|
+
for (const source of ctx.tables[root.table]?.resolved?.sources ?? []) tables.add(source.table);
|
|
2502
|
+
if (root.where) visitPred(root.where, root.table);
|
|
2503
|
+
if (root.insertCheck) visitPred(root.insertCheck, root.table);
|
|
2504
|
+
if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
|
|
2505
|
+
if (root.returning) visitSelection(root.returning, root.table);
|
|
2506
|
+
break;
|
|
2507
|
+
default: {
|
|
2508
|
+
const exhausted = root;
|
|
2509
|
+
throw new DocumentCompileError(
|
|
2510
|
+
`internal: unhandled root kind in tableSet collection ${JSON.stringify(exhausted)}`,
|
|
2511
|
+
"internal"
|
|
2512
|
+
);
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
return [...tables].sort();
|
|
2517
|
+
}
|
|
2518
|
+
function injectPolicies({ roots, ctx }) {
|
|
2519
|
+
const injector = new PolicyInjector(ctx);
|
|
2520
|
+
const injected = roots.map((root) => injector.injectRoot(root));
|
|
2521
|
+
return { roots: injected, tableSet: collectTableSet(injected, ctx) };
|
|
1706
2522
|
}
|
|
1707
2523
|
|
|
1708
2524
|
// src/compiler-gql/naming.ts
|
|
@@ -1751,16 +2567,92 @@ var NAME_KEY_BY_KIND = {
|
|
|
1751
2567
|
function inputTypeName(table, kind) {
|
|
1752
2568
|
return tableNames(table)[NAME_KEY_BY_KIND[kind]];
|
|
1753
2569
|
}
|
|
1754
|
-
function classifyInputType(
|
|
2570
|
+
function classifyInputType(name2, tableByTypename) {
|
|
1755
2571
|
for (const [suffix, input] of INPUT_TYPE_SUFFIXES) {
|
|
1756
|
-
if (!
|
|
1757
|
-
const table = tableByTypename.get(
|
|
2572
|
+
if (!name2.endsWith(suffix) || name2.length <= suffix.length) continue;
|
|
2573
|
+
const table = tableByTypename.get(name2.slice(0, -suffix.length));
|
|
1758
2574
|
if (table !== void 0) return { input, table };
|
|
1759
2575
|
}
|
|
1760
2576
|
return void 0;
|
|
1761
2577
|
}
|
|
1762
2578
|
|
|
2579
|
+
// src/errors.ts
|
|
2580
|
+
var OrmDefinitionError = class extends Error {
|
|
2581
|
+
constructor(message, file) {
|
|
2582
|
+
super(file ? `${file}: ${message}` : message);
|
|
2583
|
+
this.file = file;
|
|
2584
|
+
}
|
|
2585
|
+
name = "OrmDefinitionError";
|
|
2586
|
+
};
|
|
2587
|
+
|
|
2588
|
+
// src/canonical/composition.ts
|
|
2589
|
+
function localContract(definition) {
|
|
2590
|
+
return normalizeCanonical(localDefinitionSchema.parse(definition));
|
|
2591
|
+
}
|
|
2592
|
+
function composeNamespace(definition) {
|
|
2593
|
+
const all = [localContract(definition), ...definition.imports ?? []];
|
|
2594
|
+
const owners = /* @__PURE__ */ new Map();
|
|
2595
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
2596
|
+
const enums = /* @__PURE__ */ new Map();
|
|
2597
|
+
for (const owner of all) {
|
|
2598
|
+
if (namespaces.has(owner.namespace)) throw new OrmDefinitionError(`duplicate imported namespace '${owner.namespace}'`);
|
|
2599
|
+
namespaces.add(owner.namespace);
|
|
2600
|
+
for (const value of owner.enums) {
|
|
2601
|
+
const previous = enums.get(value.name);
|
|
2602
|
+
if (previous && stableJson(previous) !== stableJson(value)) {
|
|
2603
|
+
throw new OrmDefinitionError(`conflicting imported enum '${value.name}'`);
|
|
2604
|
+
}
|
|
2605
|
+
enums.set(value.name, value);
|
|
2606
|
+
}
|
|
2607
|
+
for (const relation of canonicalRelations(owner)) {
|
|
2608
|
+
if (owners.has(relation.table)) throw new OrmDefinitionError(`ambiguous imported relation '${relation.table}'`);
|
|
2609
|
+
owners.set(relation.table, owner);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
for (const owner of definition.imports?.length ? all : []) {
|
|
2613
|
+
for (const relation of canonicalRelations(owner)) {
|
|
2614
|
+
for (const link of relation.links) {
|
|
2615
|
+
if (link.kind === "toPlatformUser") continue;
|
|
2616
|
+
const target = owners.get(link.target ?? "");
|
|
2617
|
+
if (!target || target.namespace !== (link.namespace ?? owner.namespace)) {
|
|
2618
|
+
throw new OrmDefinitionError(`link '${owner.namespace}/${relation.table}.${link.name}' has no matching imported target`);
|
|
2619
|
+
}
|
|
2620
|
+
if (target.namespace !== owner.namespace && !target.exports?.[link.target ?? ""]) {
|
|
2621
|
+
throw new OrmDefinitionError(`target '${target.namespace}/${link.target}' is not exported`);
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
return {
|
|
2627
|
+
owners,
|
|
2628
|
+
definition: {
|
|
2629
|
+
...localContract(definition),
|
|
2630
|
+
tables: all.flatMap((item) => item.tables),
|
|
2631
|
+
views: all.flatMap((item) => item.views ?? []),
|
|
2632
|
+
enums: [...enums.values()]
|
|
2633
|
+
}
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
2636
|
+
|
|
1763
2637
|
// src/compiler-gql/context.ts
|
|
2638
|
+
function scopePolicy(pred, namespace) {
|
|
2639
|
+
switch (pred.kind) {
|
|
2640
|
+
case "capability":
|
|
2641
|
+
case "role":
|
|
2642
|
+
return { ...pred, namespace };
|
|
2643
|
+
case "and":
|
|
2644
|
+
case "or":
|
|
2645
|
+
return { ...pred, operands: pred.operands.map((p) => scopePolicy(p, namespace)) };
|
|
2646
|
+
case "not":
|
|
2647
|
+
return { ...pred, operand: scopePolicy(pred.operand, namespace) };
|
|
2648
|
+
case "exists":
|
|
2649
|
+
return { ...pred, where: scopePolicy(pred.where, namespace) };
|
|
2650
|
+
case "aggPredicate":
|
|
2651
|
+
return pred.where ? { ...pred, where: scopePolicy(pred.where, namespace) } : pred;
|
|
2652
|
+
default:
|
|
2653
|
+
return pred;
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
1764
2656
|
function platformUserMeta() {
|
|
1765
2657
|
const columns = [
|
|
1766
2658
|
// `sub` is the view's key (auth0_id, filtered NOT NULL in the view).
|
|
@@ -1816,7 +2708,8 @@ function resolveColumnEntries(table, enums, diagnostics) {
|
|
|
1816
2708
|
type,
|
|
1817
2709
|
nullable: column.nullable,
|
|
1818
2710
|
deprecated: column.deprecated,
|
|
1819
|
-
onUpdateFn: column.onUpdateFn !== void 0
|
|
2711
|
+
onUpdateFn: column.onUpdateFn !== void 0,
|
|
2712
|
+
...column.cell ? { cell: column.cell } : {}
|
|
1820
2713
|
};
|
|
1821
2714
|
});
|
|
1822
2715
|
}
|
|
@@ -1824,7 +2717,8 @@ function logicalColumn(entries, ref) {
|
|
|
1824
2717
|
return entries.find((c) => c.name === ref || c.columnName === ref)?.name;
|
|
1825
2718
|
}
|
|
1826
2719
|
function buildCompileContext(rawCanonical) {
|
|
1827
|
-
const
|
|
2720
|
+
const normalized = normalizeCanonical(canonicalDefinitionSchema.parse(rawCanonical));
|
|
2721
|
+
const { definition: canonical, owners } = composeNamespace(normalized);
|
|
1828
2722
|
const diagnostics = [];
|
|
1829
2723
|
const enums = new Map(canonical.enums.map((e) => [e.name, e]));
|
|
1830
2724
|
const relations = canonicalRelations(canonical);
|
|
@@ -1837,24 +2731,25 @@ function buildCompileContext(rawCanonical) {
|
|
|
1837
2731
|
const syncedTableNames = new Set(canonical.tables.filter((t) => t.source !== void 0).map((t) => t.table));
|
|
1838
2732
|
const viewDependencies = new Map((canonical.views ?? []).map((view) => [view.view, view.dependencies]));
|
|
1839
2733
|
const claimsGatedMemo = /* @__PURE__ */ new Map();
|
|
1840
|
-
const relationIsClaimsGated = (
|
|
1841
|
-
if (syncedTableNames.has(
|
|
1842
|
-
const memo = claimsGatedMemo.get(
|
|
2734
|
+
const relationIsClaimsGated = (name2) => {
|
|
2735
|
+
if (syncedTableNames.has(name2)) return true;
|
|
2736
|
+
const memo = claimsGatedMemo.get(name2);
|
|
1843
2737
|
if (memo !== void 0) return memo;
|
|
1844
|
-
claimsGatedMemo.set(
|
|
1845
|
-
const gated = (viewDependencies.get(
|
|
1846
|
-
claimsGatedMemo.set(
|
|
2738
|
+
claimsGatedMemo.set(name2, false);
|
|
2739
|
+
const gated = (viewDependencies.get(name2) ?? []).some(relationIsClaimsGated);
|
|
2740
|
+
claimsGatedMemo.set(name2, gated);
|
|
1847
2741
|
return gated;
|
|
1848
2742
|
};
|
|
1849
2743
|
for (const table of relations) {
|
|
1850
2744
|
const isView = viewNames.has(table.table);
|
|
2745
|
+
const owner = owners.get(table.table);
|
|
1851
2746
|
const entries = resolveColumnEntries(table, enums, diagnostics);
|
|
1852
2747
|
columnEntries.set(table.table, entries);
|
|
1853
2748
|
const pkRefs = table.primaryKey ?? (isView ? [] : ["id"]);
|
|
1854
2749
|
const primaryKey = [];
|
|
1855
2750
|
for (const ref of pkRefs) {
|
|
1856
|
-
const
|
|
1857
|
-
if (!
|
|
2751
|
+
const resolved2 = logicalColumn(entries, ref);
|
|
2752
|
+
if (!resolved2) {
|
|
1858
2753
|
diagnostics.push({
|
|
1859
2754
|
severity: "error",
|
|
1860
2755
|
code: "unknown-pk-column",
|
|
@@ -1863,9 +2758,12 @@ function buildCompileContext(rawCanonical) {
|
|
|
1863
2758
|
});
|
|
1864
2759
|
continue;
|
|
1865
2760
|
}
|
|
1866
|
-
primaryKey.push(
|
|
2761
|
+
primaryKey.push(resolved2);
|
|
1867
2762
|
}
|
|
1868
|
-
const
|
|
2763
|
+
const resolved = isView ? table.resolved : void 0;
|
|
2764
|
+
const data = entries.filter((c) => c.cell === void 0);
|
|
2765
|
+
const cells = cellMap(entries);
|
|
2766
|
+
const active = data.filter((c) => !c.deprecated);
|
|
1869
2767
|
const touchOnUpdate = isView ? [] : active.filter((c) => c.onUpdateFn).map((c) => c.name);
|
|
1870
2768
|
const constraints = [];
|
|
1871
2769
|
if (primaryKey.length > 0) constraints.push({ name: `${table.table}_pkey`, columns: [...primaryKey] });
|
|
@@ -1882,7 +2780,10 @@ function buildCompileContext(rawCanonical) {
|
|
|
1882
2780
|
}
|
|
1883
2781
|
tables[table.table] = {
|
|
1884
2782
|
table: table.table,
|
|
1885
|
-
schema: ormSchemaName(
|
|
2783
|
+
schema: ormSchemaName(owner.namespace),
|
|
2784
|
+
namespace: owner.namespace,
|
|
2785
|
+
...viewDependencies.has(table.table) ? { dependencies: viewDependencies.get(table.table) } : {},
|
|
2786
|
+
...owner.exports?.[table.table] ? { exportRead: owner.exports[table.table].read, exportWrite: owner.exports[table.table].write } : {},
|
|
1886
2787
|
typename: typeNameForTable(table.table),
|
|
1887
2788
|
// Synced twins (and views reading them, resolved transitively below)
|
|
1888
2789
|
// are claims-gated — see tableMetaIrSchema.claimsGated.
|
|
@@ -1890,12 +2791,14 @@ function buildCompileContext(rawCanonical) {
|
|
|
1890
2791
|
columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
|
|
1891
2792
|
primaryKey: isView ? primaryKey : primaryKey.length > 0 ? primaryKey : ["id"],
|
|
1892
2793
|
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),
|
|
2794
|
+
filterable: data.map((c) => c.name),
|
|
2795
|
+
orderable: data.filter((c) => c.type !== "jsonb").map((c) => c.name),
|
|
2796
|
+
insertable: resolved ? resolvedWritable(data) : isView ? [] : active.map((c) => c.name),
|
|
2797
|
+
settable: resolved ? resolvedWritable(data).filter((name2) => !primaryKey.includes(name2)) : isView ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
|
|
1897
2798
|
appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
|
|
1898
2799
|
...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
|
|
2800
|
+
...cells ? { cells } : {},
|
|
2801
|
+
...resolved ? { resolved: resolvedMeta({ resolved, pk: primaryKey[0] }) } : {},
|
|
1899
2802
|
constraints: isView ? [] : constraints,
|
|
1900
2803
|
dynamicFilterMaxDepth: 6,
|
|
1901
2804
|
dynamicFilterMaxNodes: 64
|
|
@@ -1907,7 +2810,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
1907
2810
|
for (const link of table.links) {
|
|
1908
2811
|
const targetName = link.kind === "toPlatformUser" ? PLATFORM_USER_TABLE : link.target;
|
|
1909
2812
|
const target = targetName === void 0 ? void 0 : tables[targetName];
|
|
1910
|
-
if (!targetName || !target) {
|
|
2813
|
+
if (!targetName || !target || link.namespace !== void 0 && link.namespace !== target.namespace) {
|
|
1911
2814
|
diagnostics.push({
|
|
1912
2815
|
severity: "error",
|
|
1913
2816
|
code: "unknown-link-target",
|
|
@@ -1932,6 +2835,11 @@ function buildCompileContext(rawCanonical) {
|
|
|
1932
2835
|
continue;
|
|
1933
2836
|
}
|
|
1934
2837
|
meta.links.push({ name: link.name, kind: link.kind, target: targetName, on, references });
|
|
2838
|
+
if (link.kind !== "toPlatformUser" && meta.namespace !== target.namespace) {
|
|
2839
|
+
const fkOwner = link.kind === "toMany" ? target : meta;
|
|
2840
|
+
const referenced = link.kind === "toMany" ? table.table : targetName;
|
|
2841
|
+
(fkOwner.foreignReferences ??= []).push({ column: on, target: referenced, targetColumn: references });
|
|
2842
|
+
}
|
|
1935
2843
|
}
|
|
1936
2844
|
}
|
|
1937
2845
|
const readPolicies = /* @__PURE__ */ new Map();
|
|
@@ -1949,7 +2857,8 @@ function buildCompileContext(rawCanonical) {
|
|
|
1949
2857
|
return;
|
|
1950
2858
|
}
|
|
1951
2859
|
try {
|
|
1952
|
-
|
|
2860
|
+
const pred = compilePolicy({ expression, table: table.table, resolver });
|
|
2861
|
+
store.set(table.table, normalized.imports?.length ? scopePolicy(pred, owners.get(table.table).namespace) : pred);
|
|
1953
2862
|
} catch (error) {
|
|
1954
2863
|
if (error instanceof PolicyCompileError || error instanceof PolicyParseError) {
|
|
1955
2864
|
diagnostics.push({
|
|
@@ -1968,7 +2877,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
1968
2877
|
compilePolicyFor("write", writePolicies);
|
|
1969
2878
|
compilePolicyFor("insert", insertPolicies);
|
|
1970
2879
|
compilePolicyFor("update", updatePolicies);
|
|
1971
|
-
if (isView) {
|
|
2880
|
+
if (isView && table.resolved === void 0) {
|
|
1972
2881
|
writePolicies.set(table.table, CONST_FALSE);
|
|
1973
2882
|
insertPolicies.set(table.table, CONST_FALSE);
|
|
1974
2883
|
updatePolicies.set(table.table, CONST_FALSE);
|
|
@@ -2003,10 +2912,47 @@ function buildCompileContext(rawCanonical) {
|
|
|
2003
2912
|
updatePolicies.set(PLATFORM_USER_TABLE, { kind: "const", value: false });
|
|
2004
2913
|
pins.set(PLATFORM_USER_TABLE, []);
|
|
2005
2914
|
}
|
|
2915
|
+
if (normalized.imports?.length) {
|
|
2916
|
+
const originalReads = new Map(readPolicies);
|
|
2917
|
+
const protect = (pred, table, path) => {
|
|
2918
|
+
switch (pred.kind) {
|
|
2919
|
+
case "and":
|
|
2920
|
+
case "or":
|
|
2921
|
+
return { ...pred, operands: pred.operands.map((p) => protect(p, table, path)) };
|
|
2922
|
+
case "not":
|
|
2923
|
+
return { ...pred, operand: protect(pred.operand, table, path) };
|
|
2924
|
+
case "exists":
|
|
2925
|
+
case "aggPredicate": {
|
|
2926
|
+
const target = tables[table]?.links.find((link) => link.name === pred.link)?.target;
|
|
2927
|
+
if (!target) return pred;
|
|
2928
|
+
let where = pred.where ? protect(pred.where, target, path) : CONST_TRUE;
|
|
2929
|
+
if (tables[table]?.namespace !== tables[target]?.namespace) {
|
|
2930
|
+
if (path.includes(target)) {
|
|
2931
|
+
diagnostics.push({
|
|
2932
|
+
severity: "error",
|
|
2933
|
+
code: "cyclic-cross-namespace-policy",
|
|
2934
|
+
table,
|
|
2935
|
+
message: `cross-namespace read policies form a cycle through '${target}'`
|
|
2936
|
+
});
|
|
2937
|
+
return CONST_FALSE;
|
|
2938
|
+
}
|
|
2939
|
+
where = andPred(where, protect(originalReads.get(target) ?? CONST_TRUE, target, [...path, target]));
|
|
2940
|
+
}
|
|
2941
|
+
return { ...pred, where };
|
|
2942
|
+
}
|
|
2943
|
+
default:
|
|
2944
|
+
return pred;
|
|
2945
|
+
}
|
|
2946
|
+
};
|
|
2947
|
+
for (const policies of [readPolicies, writePolicies, insertPolicies, updatePolicies]) {
|
|
2948
|
+
for (const [table, policy] of policies) policies.set(table, protect(policy, table, [table]));
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2006
2951
|
const tableByTypename = new Map(Object.entries(tables).map(([key, meta]) => [meta.typename, key]));
|
|
2007
2952
|
const queryRoots = /* @__PURE__ */ new Map();
|
|
2008
2953
|
const mutationRoots = /* @__PURE__ */ new Map();
|
|
2009
2954
|
for (const table of relations) {
|
|
2955
|
+
if (table.internal === true) continue;
|
|
2010
2956
|
const roots = queryRootNames(table.table);
|
|
2011
2957
|
queryRoots.set(roots.select, { kind: "select", table: table.table });
|
|
2012
2958
|
if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
|
|
@@ -2022,6 +2968,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
2022
2968
|
mutationRoots.set(field, { op, table: table.table });
|
|
2023
2969
|
}
|
|
2024
2970
|
}
|
|
2971
|
+
if (table.resolved !== void 0) registerResolvedRoots({ table, mutationRoots });
|
|
2025
2972
|
}
|
|
2026
2973
|
for (const table of relations) {
|
|
2027
2974
|
for (const mutation of table.cowMutations ?? []) {
|
|
@@ -2077,6 +3024,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
2077
3024
|
return {
|
|
2078
3025
|
ctx: {
|
|
2079
3026
|
namespace: canonical.namespace,
|
|
3027
|
+
composed: !!normalized.imports?.length,
|
|
2080
3028
|
tables,
|
|
2081
3029
|
tableByTypename,
|
|
2082
3030
|
queryRoots,
|
|
@@ -2092,6 +3040,49 @@ function buildCompileContext(rawCanonical) {
|
|
|
2092
3040
|
diagnostics
|
|
2093
3041
|
};
|
|
2094
3042
|
}
|
|
3043
|
+
function registerResolvedRoots({ table, mutationRoots }) {
|
|
3044
|
+
const names = tableNames(table.table);
|
|
3045
|
+
const canInsert = table.policies.write !== void 0 || table.policies.insert !== void 0;
|
|
3046
|
+
const canUpdate = table.policies.write !== void 0 || table.policies.update !== void 0;
|
|
3047
|
+
const add = (field, action) => mutationRoots.set(field, { op: "resolvedWrite", action, table: table.table });
|
|
3048
|
+
if (canInsert) add(names.insertOneRoot, "insert");
|
|
3049
|
+
if (canUpdate) {
|
|
3050
|
+
add(names.updateByPkRoot, "update");
|
|
3051
|
+
add(names.revertByPkRoot, "revert");
|
|
3052
|
+
if ((table.resolved?.deletedBy.length ?? 0) > 0) add(names.deleteByPkRoot, "delete");
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
function cellMap(entries) {
|
|
3056
|
+
const cells = {};
|
|
3057
|
+
for (const entry of entries) {
|
|
3058
|
+
if (entry.cell) cells[entry.cell.of] = { ...cells[entry.cell.of], [entry.cell.part]: entry.name };
|
|
3059
|
+
}
|
|
3060
|
+
return Object.keys(cells).length > 0 ? cells : void 0;
|
|
3061
|
+
}
|
|
3062
|
+
function resolvedWritable(entries) {
|
|
3063
|
+
const system = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((column) => column.name));
|
|
3064
|
+
return entries.filter((entry) => !entry.deprecated && !system.has(entry.name)).map((entry) => entry.name);
|
|
3065
|
+
}
|
|
3066
|
+
function resolvedMeta({ resolved, pk }) {
|
|
3067
|
+
const manual = resolved.sources.find((source) => source.kind === "manual");
|
|
3068
|
+
return {
|
|
3069
|
+
pk,
|
|
3070
|
+
manual: manual?.name ?? "",
|
|
3071
|
+
deletedBy: [...resolved.deletedBy],
|
|
3072
|
+
...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
|
|
3073
|
+
sources: resolved.sources.map((source) => ({
|
|
3074
|
+
name: source.name,
|
|
3075
|
+
kind: source.kind,
|
|
3076
|
+
table: source.table,
|
|
3077
|
+
columns: [...source.columns],
|
|
3078
|
+
...source.key !== void 0 ? { key: source.key } : {},
|
|
3079
|
+
...source.fetch !== void 0 ? { fetch: source.fetch } : {},
|
|
3080
|
+
...source.function !== void 0 ? { function: source.function } : {},
|
|
3081
|
+
...source.prefetchDefaults !== void 0 ? { prefetchDefaults: source.prefetchDefaults } : {},
|
|
3082
|
+
...source.writeback !== void 0 ? { writeback: source.writeback } : {}
|
|
3083
|
+
}))
|
|
3084
|
+
};
|
|
3085
|
+
}
|
|
2095
3086
|
function flattenSelectionSet(selectionSet, fragments, stack) {
|
|
2096
3087
|
const selections = [];
|
|
2097
3088
|
for (const selection of selectionSet.selections) {
|
|
@@ -2102,18 +3093,18 @@ function flattenSelectionSet(selectionSet, fragments, stack) {
|
|
|
2102
3093
|
continue;
|
|
2103
3094
|
}
|
|
2104
3095
|
if (selection.kind === graphql.Kind.FRAGMENT_SPREAD) {
|
|
2105
|
-
const
|
|
3096
|
+
const name2 = selection.name.value;
|
|
2106
3097
|
if (selection.directives && selection.directives.length > 0) {
|
|
2107
|
-
throw new DocumentCompileError(`directives on fragment spread '...${
|
|
3098
|
+
throw new DocumentCompileError(`directives on fragment spread '...${name2}' are not supported`, "fragment-directive");
|
|
2108
3099
|
}
|
|
2109
|
-
if (stack.includes(
|
|
2110
|
-
throw new DocumentCompileError(`fragment cycle detected through '${
|
|
3100
|
+
if (stack.includes(name2)) {
|
|
3101
|
+
throw new DocumentCompileError(`fragment cycle detected through '${name2}'`, "fragment-cycle");
|
|
2111
3102
|
}
|
|
2112
|
-
const fragment = fragments.get(
|
|
3103
|
+
const fragment = fragments.get(name2);
|
|
2113
3104
|
if (!fragment) {
|
|
2114
|
-
throw new DocumentCompileError(`unknown fragment '${
|
|
3105
|
+
throw new DocumentCompileError(`unknown fragment '${name2}'`, "unknown-fragment");
|
|
2115
3106
|
}
|
|
2116
|
-
const inlined2 = flattenSelectionSet(fragment.selectionSet, fragments, [...stack,
|
|
3107
|
+
const inlined2 = flattenSelectionSet(fragment.selectionSet, fragments, [...stack, name2]);
|
|
2117
3108
|
selections.push(...inlined2.selections);
|
|
2118
3109
|
continue;
|
|
2119
3110
|
}
|
|
@@ -2149,6 +3140,136 @@ function computeOpsHash(opHashes) {
|
|
|
2149
3140
|
const digest = crypto.createHash("sha256").update(OPS_HASH_DOMAIN).update([...opHashes].sort().join("\n")).digest("hex");
|
|
2150
3141
|
return `sha256:${digest}`;
|
|
2151
3142
|
}
|
|
3143
|
+
var SOURCE_DIRECTIVES = /* @__PURE__ */ new Set(["prefetch", "writeback", "localOnly"]);
|
|
3144
|
+
var fail = (message, code) => {
|
|
3145
|
+
throw new DocumentCompileError(message, code);
|
|
3146
|
+
};
|
|
3147
|
+
function readArgs(node, name2) {
|
|
3148
|
+
return (node.directives ?? []).filter((directive) => directive.name.value === name2).map((directive) => {
|
|
3149
|
+
const args = {};
|
|
3150
|
+
for (const arg of directive.arguments ?? []) {
|
|
3151
|
+
const key = arg.name.value;
|
|
3152
|
+
if (key === "source" || key === "maxStaleness") {
|
|
3153
|
+
if (arg.value.kind !== graphql.Kind.STRING) fail(`@${name2}(${key}:) must be a string literal`, "invalid-source-directive");
|
|
3154
|
+
args[key] = arg.value.value;
|
|
3155
|
+
} else if (key === "mode" && name2 === "writeback") {
|
|
3156
|
+
if (arg.value.kind !== graphql.Kind.ENUM || !["SYNC", "ASYNC"].includes(arg.value.value)) {
|
|
3157
|
+
fail("@writeback(mode:) must be SYNC or ASYNC", "invalid-source-directive");
|
|
3158
|
+
}
|
|
3159
|
+
args.mode = arg.value.value === "SYNC" ? "sync" : "async";
|
|
3160
|
+
} else {
|
|
3161
|
+
fail(`@${name2} does not take argument '${key}'`, "invalid-source-directive");
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
if (!args.source) fail(`@${name2} needs a source: argument`, "invalid-source-directive");
|
|
3165
|
+
return args;
|
|
3166
|
+
});
|
|
3167
|
+
}
|
|
3168
|
+
function prefetchSourceFor({ resolved, name: name2, directive, table }) {
|
|
3169
|
+
if (!resolved) return fail(`@${directive} is only valid on resolved tables \u2014 '${table}' is not one`, "invalid-source-directive");
|
|
3170
|
+
const source = resolved.sources.find((entry) => entry.name === name2);
|
|
3171
|
+
if (!source || source.kind !== "prefetch") {
|
|
3172
|
+
const options = resolved.sources.filter((entry) => entry.kind === "prefetch").map((entry) => entry.name);
|
|
3173
|
+
return fail(`@${directive}(source: "${name2}") on '${table}' must name a prefetch source (${options.join(", ") || "none declared"})`, "invalid-source-directive");
|
|
3174
|
+
}
|
|
3175
|
+
return source;
|
|
3176
|
+
}
|
|
3177
|
+
function applyPrefetchDirectives({ node, root, meta }) {
|
|
3178
|
+
const directives = readArgs(node, "prefetch");
|
|
3179
|
+
for (const name2 of ["writeback", "localOnly"]) {
|
|
3180
|
+
if (readArgs(node, name2).length > 0) fail(`@${name2} is only valid on resolved-table mutation roots`, "invalid-source-directive");
|
|
3181
|
+
}
|
|
3182
|
+
if (directives.length === 0) return root;
|
|
3183
|
+
if (root.kind !== "select" && root.kind !== "byPk") {
|
|
3184
|
+
return fail(`@prefetch is only valid on select and byPk roots, not '${root.alias}'`, "invalid-source-directive");
|
|
3185
|
+
}
|
|
3186
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3187
|
+
const plans = directives.map((directive) => {
|
|
3188
|
+
const source = prefetchSourceFor({ resolved: meta.resolved, name: directive.source, directive: "prefetch", table: root.table });
|
|
3189
|
+
if (seen.has(source.name)) fail(`@prefetch(source: "${source.name}") appears twice on '${root.alias}'`, "invalid-source-directive");
|
|
3190
|
+
seen.add(source.name);
|
|
3191
|
+
const maxStalenessMs = directive.maxStaleness === void 0 ? void 0 : parseDurationMs(directive.maxStaleness);
|
|
3192
|
+
if (directive.maxStaleness !== void 0 && maxStalenessMs === void 0) {
|
|
3193
|
+
fail(`@prefetch(maxStaleness: "${directive.maxStaleness}") must look like '500ms', '30s' or '5m'`, "invalid-source-directive");
|
|
3194
|
+
}
|
|
3195
|
+
const key = root.kind === "byPk" ? { one: root.pk[0].value } : selectKey({ where: root.where, pk: meta.resolved.pk, source, alias: root.alias });
|
|
3196
|
+
return { source: source.name, ...maxStalenessMs !== void 0 ? { maxStalenessMs } : {}, key };
|
|
3197
|
+
});
|
|
3198
|
+
return { ...root, prefetch: plans };
|
|
3199
|
+
}
|
|
3200
|
+
function selectKey({ where, pk, source, alias }) {
|
|
3201
|
+
const leaves = where.kind === "and" ? where.operands : [where];
|
|
3202
|
+
const pinned = leaves.find((leaf) => leaf.kind === "cmp" && leaf.column === pk && (leaf.op === "eq" || leaf.op === "in"));
|
|
3203
|
+
const shape = source.fetch?.shape ?? "single";
|
|
3204
|
+
if (!pinned || pinned.kind !== "cmp") {
|
|
3205
|
+
return fail(
|
|
3206
|
+
`@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)`,
|
|
3207
|
+
"prefetch-shape-mismatch"
|
|
3208
|
+
);
|
|
3209
|
+
}
|
|
3210
|
+
if (pinned.op === "in" && shape !== "batch") {
|
|
3211
|
+
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");
|
|
3212
|
+
}
|
|
3213
|
+
return pinned.op === "eq" ? { one: pinned.value } : { many: pinned.value };
|
|
3214
|
+
}
|
|
3215
|
+
function applyWritebackDirectives({ node, root, meta }) {
|
|
3216
|
+
if (readArgs(node, "prefetch").length > 0) fail("@prefetch is only valid on query roots", "invalid-source-directive");
|
|
3217
|
+
const writebacks = readArgs(node, "writeback");
|
|
3218
|
+
const localOnly = readArgs(node, "localOnly");
|
|
3219
|
+
if (root.kind !== "mutation" || root.op !== "resolvedWrite" || !meta.resolved) {
|
|
3220
|
+
if (writebacks.length + localOnly.length > 0) fail("@writeback/@localOnly are only valid on resolved-table mutation roots", "invalid-source-directive");
|
|
3221
|
+
return root;
|
|
3222
|
+
}
|
|
3223
|
+
const resolved = meta.resolved;
|
|
3224
|
+
const declared = /* @__PURE__ */ new Map();
|
|
3225
|
+
const bindings = [];
|
|
3226
|
+
for (const directive of writebacks) {
|
|
3227
|
+
const source = prefetchSourceFor({ resolved, name: directive.source, directive: "writeback", table: root.table });
|
|
3228
|
+
if (!source.writeback) fail(`@writeback(source: "${source.name}"): source '${source.name}' declares no writeback columns`, "invalid-source-directive");
|
|
3229
|
+
if (root.resolvedAction !== "update") {
|
|
3230
|
+
fail(`@writeback on '${root.alias}': only update roots write back in v1 (use @localOnly for ${root.resolvedAction ?? "this"} roots)`, "writeback-shape-mismatch");
|
|
3231
|
+
}
|
|
3232
|
+
if (declared.has(source.name)) fail(`source '${source.name}' is declared twice on '${root.alias}'`, "invalid-source-directive");
|
|
3233
|
+
declared.set(source.name, "writeback");
|
|
3234
|
+
bindings.push({ source: source.name, mode: directive.mode ?? source.writeback.defaultMode });
|
|
3235
|
+
}
|
|
3236
|
+
for (const directive of localOnly) {
|
|
3237
|
+
const source = prefetchSourceFor({ resolved, name: directive.source, directive: "localOnly", table: root.table });
|
|
3238
|
+
if (declared.has(source.name)) fail(`source '${source.name}' is declared twice on '${root.alias}'`, "invalid-source-directive");
|
|
3239
|
+
declared.set(source.name, "localOnly");
|
|
3240
|
+
}
|
|
3241
|
+
assertWritebacksDeclared({ root, meta, resolved, declared });
|
|
3242
|
+
return {
|
|
3243
|
+
...root,
|
|
3244
|
+
...bindings.length > 0 ? { writeback: bindings } : {},
|
|
3245
|
+
...localOnly.length > 0 ? { localOnly: localOnly.map((directive) => directive.source) } : {}
|
|
3246
|
+
};
|
|
3247
|
+
}
|
|
3248
|
+
function assertWritebacksDeclared({ root, meta, resolved, declared }) {
|
|
3249
|
+
const written = writtenColumns({ root, meta });
|
|
3250
|
+
for (const source of resolved.sources) {
|
|
3251
|
+
const columns = source.writeback?.columns.filter((column) => written.has(column)) ?? [];
|
|
3252
|
+
if (columns.length > 0 && !declared.has(source.name)) {
|
|
3253
|
+
fail(
|
|
3254
|
+
`'${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`,
|
|
3255
|
+
"writeback-undeclared"
|
|
3256
|
+
);
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
function writtenColumns({ root, meta }) {
|
|
3261
|
+
if (root.resolvedAction === "update") {
|
|
3262
|
+
if (!root.set || "slot" in root.set) return new Set(meta.settable);
|
|
3263
|
+
return new Set(root.set.entries.map((entry) => entry.column));
|
|
3264
|
+
}
|
|
3265
|
+
if (root.resolvedAction === "insert") {
|
|
3266
|
+
if (!root.objects || "slot" in root.objects) return new Set(meta.insertable);
|
|
3267
|
+
return new Set(root.objects.rows.flatMap((row) => row.map((entry) => entry.column)));
|
|
3268
|
+
}
|
|
3269
|
+
return /* @__PURE__ */ new Set();
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
// src/compiler-gql/lower.ts
|
|
2152
3273
|
var DEFAULT_LIMIT = 100;
|
|
2153
3274
|
var MAX_LIMIT = 1e3;
|
|
2154
3275
|
var MAX_GROUPS = 1e3;
|
|
@@ -2186,7 +3307,7 @@ var ORDER_DIRECTIONS = {
|
|
|
2186
3307
|
DESC_NULLS_LAST: { direction: "desc", nulls: "last" }
|
|
2187
3308
|
};
|
|
2188
3309
|
var NUMERIC_TYPES = /* @__PURE__ */ new Set(["integer", "bigint", "numeric", "double"]);
|
|
2189
|
-
function
|
|
3310
|
+
function fail2(message, code, candidates = []) {
|
|
2190
3311
|
throw new DocumentCompileError(message, code, candidates);
|
|
2191
3312
|
}
|
|
2192
3313
|
function argsByName(field) {
|
|
@@ -2194,16 +3315,16 @@ function argsByName(field) {
|
|
|
2194
3315
|
}
|
|
2195
3316
|
function* aggregateBlockFields(selectionSet) {
|
|
2196
3317
|
for (const node of selectionSet.selections) {
|
|
2197
|
-
if (node.kind !== graphql.Kind.FIELD)
|
|
3318
|
+
if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
2198
3319
|
if (node.name.value === "__typename") continue;
|
|
2199
|
-
if (node.alias)
|
|
3320
|
+
if (node.alias) fail2(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
|
|
2200
3321
|
yield node;
|
|
2201
3322
|
}
|
|
2202
3323
|
}
|
|
2203
3324
|
function assertKnownArgs(field, allowed) {
|
|
2204
3325
|
for (const arg of field.arguments ?? []) {
|
|
2205
3326
|
if (!allowed.includes(arg.name.value)) {
|
|
2206
|
-
|
|
3327
|
+
fail2(`argument '${arg.name.value}' on '${field.name.value}' is not supported in v2.0`, "unsupported-argument", allowed);
|
|
2207
3328
|
}
|
|
2208
3329
|
}
|
|
2209
3330
|
}
|
|
@@ -2216,27 +3337,31 @@ var OperationLowerer = class {
|
|
|
2216
3337
|
meta(table) {
|
|
2217
3338
|
return this.ctx.tables[table];
|
|
2218
3339
|
}
|
|
2219
|
-
column
|
|
2220
|
-
|
|
3340
|
+
/** A data column — cell-metadata columns are reachable only through `cells { … }`. */
|
|
3341
|
+
column(table, name2) {
|
|
3342
|
+
const meta = this.meta(table);
|
|
3343
|
+
const column = meta.columns.find((c) => c.name === name2);
|
|
3344
|
+
return column && !isCellColumn(meta, name2) ? column : void 0;
|
|
2221
3345
|
}
|
|
2222
|
-
link(table,
|
|
2223
|
-
return this.meta(table).links.find((l) => l.name ===
|
|
3346
|
+
link(table, name2) {
|
|
3347
|
+
return this.meta(table).links.find((l) => l.name === name2);
|
|
2224
3348
|
}
|
|
2225
3349
|
fieldCandidates(table) {
|
|
2226
3350
|
const meta = this.meta(table);
|
|
2227
3351
|
return [
|
|
2228
|
-
...meta.columns.map((c) => c.name),
|
|
3352
|
+
...meta.columns.filter((c) => !isCellColumn(meta, c.name)).map((c) => c.name),
|
|
3353
|
+
...meta.cells ? [CELLS_FIELD] : [],
|
|
2229
3354
|
...meta.links.map((l) => l.name),
|
|
2230
3355
|
...meta.links.filter((l) => l.kind === "toMany").map((l) => `${l.name}Aggregate`)
|
|
2231
3356
|
];
|
|
2232
3357
|
}
|
|
2233
3358
|
/** Validate that a whole-variable argument is the declared §4f slot for this table. */
|
|
2234
|
-
slot(
|
|
2235
|
-
const variable = this.variables.get(
|
|
3359
|
+
slot(name2, input, table) {
|
|
3360
|
+
const variable = this.variables.get(name2);
|
|
2236
3361
|
if (!variable || variable.type.kind !== "inputRef" || variable.type.input !== input || variable.type.table !== table) {
|
|
2237
|
-
|
|
3362
|
+
fail2(`variable $${name2} must be declared as the ${input} input type for table '${table}'`, "slot-mismatch");
|
|
2238
3363
|
}
|
|
2239
|
-
return
|
|
3364
|
+
return name2;
|
|
2240
3365
|
}
|
|
2241
3366
|
// -------------------------------------------------------------------------
|
|
2242
3367
|
// Values
|
|
@@ -2259,14 +3384,14 @@ var OperationLowerer = class {
|
|
|
2259
3384
|
const items = node.values.map((item) => {
|
|
2260
3385
|
const lowered = this.lowerValue(item);
|
|
2261
3386
|
if (lowered.kind !== "literal" || Array.isArray(lowered.value)) {
|
|
2262
|
-
|
|
3387
|
+
fail2("list literals must contain scalar literals only \u2014 pass the whole list as a variable instead", "unsupported-literal");
|
|
2263
3388
|
}
|
|
2264
3389
|
return lowered.value;
|
|
2265
3390
|
});
|
|
2266
3391
|
return { kind: "literal", value: items };
|
|
2267
3392
|
}
|
|
2268
3393
|
case graphql.Kind.OBJECT:
|
|
2269
|
-
|
|
3394
|
+
fail2("literal JSON objects are not supported in documents \u2014 pass the value via a variable", "unsupported-literal");
|
|
2270
3395
|
}
|
|
2271
3396
|
}
|
|
2272
3397
|
// -------------------------------------------------------------------------
|
|
@@ -2276,7 +3401,7 @@ var OperationLowerer = class {
|
|
|
2276
3401
|
if (node.kind === graphql.Kind.VARIABLE) {
|
|
2277
3402
|
return { kind: "dynamicFilter", slot: this.slot(node.name.value, "boolExp", table) };
|
|
2278
3403
|
}
|
|
2279
|
-
if (node.kind !== graphql.Kind.OBJECT)
|
|
3404
|
+
if (node.kind !== graphql.Kind.OBJECT) fail2(`a boolean expression on '${table}' must be an object or a variable`, "invalid-boolexp");
|
|
2280
3405
|
const operands = [];
|
|
2281
3406
|
for (const field of node.fields) {
|
|
2282
3407
|
const key = field.name.value;
|
|
@@ -2307,45 +3432,45 @@ var OperationLowerer = class {
|
|
|
2307
3432
|
continue;
|
|
2308
3433
|
}
|
|
2309
3434
|
}
|
|
2310
|
-
|
|
3435
|
+
fail2(`unknown field '${key}' in a boolean expression on '${table}'`, "unknown-column", this.fieldCandidates(table));
|
|
2311
3436
|
}
|
|
2312
3437
|
if (operands.length === 0) return CONST_TRUE;
|
|
2313
3438
|
return operands.length === 1 ? operands[0] : { kind: "and", operands };
|
|
2314
3439
|
}
|
|
2315
3440
|
lowerComparison(node, column, table) {
|
|
2316
3441
|
if (node.kind !== graphql.Kind.OBJECT) {
|
|
2317
|
-
|
|
3442
|
+
fail2(`comparison on '${table}.${column}' must be an object like { _eq: \u2026 }`, "invalid-comparison", Object.keys(BOOLEXP_OPS));
|
|
2318
3443
|
}
|
|
2319
3444
|
return node.fields.map((field) => {
|
|
2320
3445
|
const op = BOOLEXP_OPS[field.name.value];
|
|
2321
3446
|
if (!op) {
|
|
2322
|
-
|
|
3447
|
+
fail2(`unknown comparison operator '${field.name.value}' on '${table}.${column}'`, "unknown-operator", Object.keys(BOOLEXP_OPS));
|
|
2323
3448
|
}
|
|
2324
3449
|
return { kind: "cmp", op, column, value: this.lowerValue(field.value) };
|
|
2325
3450
|
});
|
|
2326
3451
|
}
|
|
2327
3452
|
/** Hasura aggregation predicate: `{ count: { predicate: { _gt: 2 }, filter: {…} } }`. */
|
|
2328
3453
|
lowerAggPredicate(node, link, target) {
|
|
2329
|
-
if (node.kind !== graphql.Kind.OBJECT)
|
|
3454
|
+
if (node.kind !== graphql.Kind.OBJECT) fail2(`aggregate predicate through '${link}' must be an object`, "invalid-agg-predicate");
|
|
2330
3455
|
const countField = node.fields.find((f) => f.name.value === "count");
|
|
2331
3456
|
if (!countField || node.fields.length !== 1) {
|
|
2332
|
-
|
|
3457
|
+
fail2(`aggregate predicates support { count: \u2026 } only in v2.0 (through '${link}')`, "invalid-agg-predicate", ["count"]);
|
|
2333
3458
|
}
|
|
2334
|
-
if (countField.value.kind !== graphql.Kind.OBJECT)
|
|
3459
|
+
if (countField.value.kind !== graphql.Kind.OBJECT) fail2(`count predicate through '${link}' must be an object`, "invalid-agg-predicate");
|
|
2335
3460
|
let predicate;
|
|
2336
3461
|
let where;
|
|
2337
3462
|
for (const field of countField.value.fields) {
|
|
2338
3463
|
if (field.name.value === "predicate") {
|
|
2339
3464
|
if (field.value.kind !== graphql.Kind.OBJECT || field.value.fields.length !== 1) {
|
|
2340
|
-
|
|
3465
|
+
fail2(`count predicate through '${link}' needs exactly one operator`, "invalid-agg-predicate", [...AGG_PREDICATE_OPS]);
|
|
2341
3466
|
}
|
|
2342
3467
|
const opField = field.value.fields[0];
|
|
2343
3468
|
const opName = opField.name.value;
|
|
2344
3469
|
if (!AGG_PREDICATE_OPS.includes(opName)) {
|
|
2345
|
-
|
|
3470
|
+
fail2(`aggregate count predicates support ${AGG_PREDICATE_OPS.join("/")} only, got '${opName}'`, "invalid-agg-predicate");
|
|
2346
3471
|
}
|
|
2347
3472
|
if (opField.value.kind !== graphql.Kind.INT) {
|
|
2348
|
-
|
|
3473
|
+
fail2(`aggregate count predicate value must be an integer literal`, "invalid-agg-predicate");
|
|
2349
3474
|
}
|
|
2350
3475
|
predicate = { op: opName.slice(1), value: Number(opField.value.value) };
|
|
2351
3476
|
continue;
|
|
@@ -2354,9 +3479,9 @@ var OperationLowerer = class {
|
|
|
2354
3479
|
where = this.lowerBoolExp(field.value, target);
|
|
2355
3480
|
continue;
|
|
2356
3481
|
}
|
|
2357
|
-
|
|
3482
|
+
fail2(`unknown key '${field.name.value}' in a count predicate`, "invalid-agg-predicate", ["predicate", "filter"]);
|
|
2358
3483
|
}
|
|
2359
|
-
if (!predicate)
|
|
3484
|
+
if (!predicate) fail2(`count predicate through '${link}' needs a { predicate: { _gt: n } }`, "invalid-agg-predicate");
|
|
2360
3485
|
return { kind: "aggPredicate", link, fn: "count", op: predicate.op, value: predicate.value, ...where ? { where } : {} };
|
|
2361
3486
|
}
|
|
2362
3487
|
lowerOrderBy(node, table) {
|
|
@@ -2366,7 +3491,7 @@ var OperationLowerer = class {
|
|
|
2366
3491
|
const items = node.kind === graphql.Kind.LIST ? node.values : [node];
|
|
2367
3492
|
const lowered = [];
|
|
2368
3493
|
for (const item of items) {
|
|
2369
|
-
if (item.kind !== graphql.Kind.OBJECT)
|
|
3494
|
+
if (item.kind !== graphql.Kind.OBJECT) fail2(`orderBy entries on '${table}' must be objects`, "invalid-orderby");
|
|
2370
3495
|
for (const field of item.fields) {
|
|
2371
3496
|
const key = field.name.value;
|
|
2372
3497
|
const column = this.column(table, key);
|
|
@@ -2377,25 +3502,25 @@ var OperationLowerer = class {
|
|
|
2377
3502
|
const link = this.link(table, key);
|
|
2378
3503
|
if (link && link.kind !== "toMany") {
|
|
2379
3504
|
if (field.value.kind !== graphql.Kind.OBJECT || field.value.fields.length !== 1) {
|
|
2380
|
-
|
|
3505
|
+
fail2(`orderBy through '${key}' needs exactly one target column`, "invalid-orderby");
|
|
2381
3506
|
}
|
|
2382
3507
|
const inner = field.value.fields[0];
|
|
2383
3508
|
const targetColumn = this.column(link.target, inner.name.value);
|
|
2384
3509
|
if (!targetColumn) {
|
|
2385
|
-
|
|
3510
|
+
fail2(`unknown column '${inner.name.value}' on '${link.target}' in orderBy`, "unknown-column", this.meta(link.target).columns.map((c) => c.name));
|
|
2386
3511
|
}
|
|
2387
3512
|
lowered.push({ kind: "column", column: inner.name.value, link: key, ...this.lowerDirection(inner.value, link.target, inner.name.value) });
|
|
2388
3513
|
continue;
|
|
2389
3514
|
}
|
|
2390
|
-
|
|
3515
|
+
fail2(`unknown orderBy key '${key}' on '${table}' (to-one links only)`, "invalid-orderby", this.fieldCandidates(table));
|
|
2391
3516
|
}
|
|
2392
3517
|
}
|
|
2393
3518
|
return lowered;
|
|
2394
3519
|
}
|
|
2395
3520
|
lowerDirection(node, table, column) {
|
|
2396
|
-
if (node.kind !== graphql.Kind.ENUM)
|
|
3521
|
+
if (node.kind !== graphql.Kind.ENUM) fail2(`orderBy direction on '${table}.${column}' must be a direction enum`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
|
|
2397
3522
|
const direction = ORDER_DIRECTIONS[node.value];
|
|
2398
|
-
if (!direction)
|
|
3523
|
+
if (!direction) fail2(`unknown orderBy direction '${node.value}'`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
|
|
2399
3524
|
return direction.nulls ? { direction: direction.direction, nulls: direction.nulls } : { direction: direction.direction };
|
|
2400
3525
|
}
|
|
2401
3526
|
lowerDistinctOn(node, table) {
|
|
@@ -2404,29 +3529,29 @@ var OperationLowerer = class {
|
|
|
2404
3529
|
}
|
|
2405
3530
|
const items = node.kind === graphql.Kind.LIST ? node.values : [node];
|
|
2406
3531
|
return items.map((item) => {
|
|
2407
|
-
if (item.kind !== graphql.Kind.ENUM)
|
|
3532
|
+
if (item.kind !== graphql.Kind.ENUM) fail2(`distinctOn entries on '${table}' must be select-column enum values`, "invalid-distinct-on");
|
|
2408
3533
|
if (!this.column(table, item.value)) {
|
|
2409
|
-
|
|
3534
|
+
fail2(`unknown distinctOn column '${item.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
|
|
2410
3535
|
}
|
|
2411
3536
|
return item.value;
|
|
2412
3537
|
});
|
|
2413
3538
|
}
|
|
2414
3539
|
lowerLimit(node) {
|
|
2415
|
-
const
|
|
2416
|
-
if (!node) return
|
|
2417
|
-
if (node.kind === graphql.Kind.VARIABLE) return { ...
|
|
3540
|
+
const base2 = { default: DEFAULT_LIMIT, max: MAX_LIMIT };
|
|
3541
|
+
if (!node) return base2;
|
|
3542
|
+
if (node.kind === graphql.Kind.VARIABLE) return { ...base2, variable: node.name.value };
|
|
2418
3543
|
if (node.kind === graphql.Kind.INT) {
|
|
2419
3544
|
const value = Number(node.value);
|
|
2420
|
-
if (value < 1)
|
|
2421
|
-
return { ...
|
|
3545
|
+
if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
|
|
3546
|
+
return { ...base2, value: Math.min(value, MAX_LIMIT) };
|
|
2422
3547
|
}
|
|
2423
|
-
|
|
3548
|
+
fail2("limit must be an integer literal or a variable", "invalid-limit");
|
|
2424
3549
|
}
|
|
2425
3550
|
lowerOffset(node) {
|
|
2426
3551
|
if (!node) return void 0;
|
|
2427
3552
|
if (node.kind === graphql.Kind.VARIABLE) return { variable: node.name.value };
|
|
2428
3553
|
if (node.kind === graphql.Kind.INT) return { value: Number(node.value) };
|
|
2429
|
-
|
|
3554
|
+
fail2("offset must be an integer literal or a variable", "invalid-offset");
|
|
2430
3555
|
}
|
|
2431
3556
|
// -------------------------------------------------------------------------
|
|
2432
3557
|
// Selections
|
|
@@ -2435,27 +3560,32 @@ var OperationLowerer = class {
|
|
|
2435
3560
|
const meta = this.meta(table);
|
|
2436
3561
|
const selection = { typename: meta.typename, columns: [], relations: [], aggregates: [] };
|
|
2437
3562
|
for (const node of selectionSet.selections) {
|
|
2438
|
-
if (node.kind !== graphql.Kind.FIELD)
|
|
3563
|
+
if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
2439
3564
|
if (node.directives && node.directives.length > 0) {
|
|
2440
|
-
|
|
3565
|
+
fail2(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
|
|
2441
3566
|
}
|
|
2442
|
-
const
|
|
2443
|
-
if (
|
|
2444
|
-
const alias = node.alias?.value ??
|
|
2445
|
-
const column = this.column(table,
|
|
3567
|
+
const name2 = node.name.value;
|
|
3568
|
+
if (name2 === "__typename") continue;
|
|
3569
|
+
const alias = node.alias?.value ?? name2;
|
|
3570
|
+
const column = this.column(table, name2);
|
|
2446
3571
|
if (column) {
|
|
2447
3572
|
if (node.arguments !== void 0 && node.arguments.length > 0 || node.selectionSet !== void 0) {
|
|
2448
|
-
|
|
3573
|
+
fail2(`column '${table}.${name2}' takes no arguments or sub-selection`, "invalid-selection");
|
|
2449
3574
|
}
|
|
2450
|
-
selection.columns.push({ alias, column:
|
|
3575
|
+
selection.columns.push({ alias, column: name2 });
|
|
3576
|
+
continue;
|
|
3577
|
+
}
|
|
3578
|
+
if (name2 === CELLS_FIELD && meta.cells) {
|
|
3579
|
+
if (selection.cells) fail2(`'${CELLS_FIELD}' may be selected once per object`, "invalid-selection");
|
|
3580
|
+
selection.cells = this.lowerCells(node, table, alias);
|
|
2451
3581
|
continue;
|
|
2452
3582
|
}
|
|
2453
|
-
const link = this.link(table,
|
|
3583
|
+
const link = this.link(table, name2);
|
|
2454
3584
|
if (link) {
|
|
2455
|
-
if (!node.selectionSet)
|
|
3585
|
+
if (!node.selectionSet) fail2(`link '${table}.${name2}' needs a selection set`, "invalid-selection");
|
|
2456
3586
|
const relation = {
|
|
2457
3587
|
alias,
|
|
2458
|
-
link:
|
|
3588
|
+
link: name2,
|
|
2459
3589
|
selection: this.lowerSelection(node.selectionSet, link.target)
|
|
2460
3590
|
};
|
|
2461
3591
|
if (link.kind === "toMany") {
|
|
@@ -2471,15 +3601,15 @@ var OperationLowerer = class {
|
|
|
2471
3601
|
if (offset) relation.offset = offset;
|
|
2472
3602
|
}
|
|
2473
3603
|
} else if (node.arguments && node.arguments.length > 0) {
|
|
2474
|
-
|
|
3604
|
+
fail2(`to-one link '${table}.${name2}' takes no arguments`, "invalid-selection");
|
|
2475
3605
|
}
|
|
2476
3606
|
selection.relations.push(relation);
|
|
2477
3607
|
continue;
|
|
2478
3608
|
}
|
|
2479
|
-
if (
|
|
2480
|
-
const aggLink = this.link(table,
|
|
3609
|
+
if (name2.endsWith("Aggregate")) {
|
|
3610
|
+
const aggLink = this.link(table, name2.slice(0, -"Aggregate".length));
|
|
2481
3611
|
if (aggLink && aggLink.kind === "toMany") {
|
|
2482
|
-
if (!node.selectionSet)
|
|
3612
|
+
if (!node.selectionSet) fail2(`'${name2}' needs a selection set`, "invalid-selection");
|
|
2483
3613
|
assertKnownArgs(node, ["where"]);
|
|
2484
3614
|
const whereArg = argsByName(node).get("where");
|
|
2485
3615
|
const aggregate = this.lowerAggregateSelection(node.selectionSet, aggLink.target);
|
|
@@ -2492,28 +3622,75 @@ var OperationLowerer = class {
|
|
|
2492
3622
|
continue;
|
|
2493
3623
|
}
|
|
2494
3624
|
}
|
|
2495
|
-
|
|
3625
|
+
fail2(`unknown field '${name2}' on '${table}'`, "unknown-field", this.fieldCandidates(table));
|
|
2496
3626
|
}
|
|
2497
3627
|
return selection;
|
|
2498
3628
|
}
|
|
3629
|
+
/**
|
|
3630
|
+
* `cells { <column> { at by … } }` — typed per-cell metadata. Plain tables
|
|
3631
|
+
* offer `at`/`by`; resolved tables add the winning `source` and newer
|
|
3632
|
+
* `conflicts { source at value }`.
|
|
3633
|
+
*/
|
|
3634
|
+
lowerCells(node, table, alias) {
|
|
3635
|
+
const meta = this.meta(table);
|
|
3636
|
+
const cells = meta.cells ?? {};
|
|
3637
|
+
if (node.arguments && node.arguments.length > 0) fail2(`'${CELLS_FIELD}' takes no arguments`, "invalid-selection");
|
|
3638
|
+
if (!node.selectionSet) fail2(`'${CELLS_FIELD}' needs a selection set`, "invalid-selection");
|
|
3639
|
+
const fields = [];
|
|
3640
|
+
for (const cellNode of fieldNodes(node.selectionSet)) {
|
|
3641
|
+
const column = cellNode.name.value;
|
|
3642
|
+
const parts = cells[column];
|
|
3643
|
+
if (!parts || parts.at === void 0 && parts.source === void 0) {
|
|
3644
|
+
fail2(`unknown cell '${column}' on '${table}'`, "unknown-field", Object.keys(cells));
|
|
3645
|
+
}
|
|
3646
|
+
if (!cellNode.selectionSet) fail2(`cell '${column}' needs a selection set`, "invalid-selection");
|
|
3647
|
+
const resolved = parts.source !== void 0;
|
|
3648
|
+
const allowed = resolved ? ["source", "at", "by", "conflicts"] : ["at", "by"];
|
|
3649
|
+
const selected = [];
|
|
3650
|
+
for (const partNode of fieldNodes(cellNode.selectionSet)) {
|
|
3651
|
+
const part = partNode.name.value;
|
|
3652
|
+
if (!allowed.includes(part)) fail2(`unknown field '${part}' on cell '${column}'`, "unknown-field", allowed);
|
|
3653
|
+
const partAlias = partNode.alias?.value ?? part;
|
|
3654
|
+
if (part !== "conflicts") {
|
|
3655
|
+
if (partNode.selectionSet) fail2(`cell field '${part}' takes no sub-selection`, "invalid-selection");
|
|
3656
|
+
selected.push({ alias: partAlias, part });
|
|
3657
|
+
continue;
|
|
3658
|
+
}
|
|
3659
|
+
if (!partNode.selectionSet) fail2(`'conflicts' needs a selection set`, "invalid-selection");
|
|
3660
|
+
const conflictFields = fieldNodes(partNode.selectionSet).map((fieldNode) => {
|
|
3661
|
+
const field = fieldNode.name.value;
|
|
3662
|
+
if (field !== "source" && field !== "at" && field !== "value") fail2(`unknown field '${field}' on a cell conflict`, "unknown-field", ["source", "at", "value"]);
|
|
3663
|
+
return { alias: fieldNode.alias?.value ?? field, field };
|
|
3664
|
+
});
|
|
3665
|
+
selected.push({ alias: partAlias, part: "conflicts", fields: conflictFields, typename: CELL_TYPENAMES.conflict });
|
|
3666
|
+
}
|
|
3667
|
+
fields.push({
|
|
3668
|
+
alias: cellNode.alias?.value ?? column,
|
|
3669
|
+
column,
|
|
3670
|
+
typename: resolved ? CELL_TYPENAMES.resolved : CELL_TYPENAMES.stamp,
|
|
3671
|
+
parts: selected
|
|
3672
|
+
});
|
|
3673
|
+
}
|
|
3674
|
+
return { alias, typename: cellsTypename(meta.typename), fields };
|
|
3675
|
+
}
|
|
2499
3676
|
/** The `{ aggregate { count sum {…} avg {…} } nodes {…} }` block shared by roots and relations. */
|
|
2500
3677
|
lowerAggregateSelection(selectionSet, table) {
|
|
2501
3678
|
const result = {};
|
|
2502
3679
|
let sawAggregate = false;
|
|
2503
3680
|
for (const node of aggregateBlockFields(selectionSet)) {
|
|
2504
|
-
const
|
|
2505
|
-
if (
|
|
2506
|
-
if (!node.selectionSet)
|
|
3681
|
+
const name2 = node.name.value;
|
|
3682
|
+
if (name2 === "nodes") {
|
|
3683
|
+
if (!node.selectionSet) fail2("'nodes' needs a selection set", "invalid-aggregate");
|
|
2507
3684
|
result.nodes = this.lowerSelection(node.selectionSet, table);
|
|
2508
3685
|
continue;
|
|
2509
3686
|
}
|
|
2510
|
-
if (
|
|
2511
|
-
if (sawAggregate)
|
|
3687
|
+
if (name2 !== "aggregate") fail2(`unknown aggregate field '${name2}'`, "invalid-aggregate", ["aggregate", "nodes"]);
|
|
3688
|
+
if (sawAggregate) fail2("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
|
|
2512
3689
|
sawAggregate = true;
|
|
2513
|
-
if (!node.selectionSet)
|
|
3690
|
+
if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
|
|
2514
3691
|
Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
|
|
2515
3692
|
}
|
|
2516
|
-
if (!sawAggregate && !result.nodes)
|
|
3693
|
+
if (!sawAggregate && !result.nodes) fail2("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
|
|
2517
3694
|
return result;
|
|
2518
3695
|
}
|
|
2519
3696
|
/**
|
|
@@ -2528,26 +3705,26 @@ var OperationLowerer = class {
|
|
|
2528
3705
|
let sawAggregate = false;
|
|
2529
3706
|
let sawKey = false;
|
|
2530
3707
|
for (const node of aggregateBlockFields(selectionSet)) {
|
|
2531
|
-
const
|
|
2532
|
-
if (
|
|
2533
|
-
if (sawKey)
|
|
3708
|
+
const name2 = node.name.value;
|
|
3709
|
+
if (name2 === "key") {
|
|
3710
|
+
if (sawKey) fail2("one key block per grouped aggregate", "invalid-aggregate");
|
|
2534
3711
|
sawKey = true;
|
|
2535
|
-
if (!node.selectionSet)
|
|
3712
|
+
if (!node.selectionSet) fail2("'key' needs a selection set", "invalid-aggregate");
|
|
2536
3713
|
result.key = this.lowerGroupKeySelection(node.selectionSet, table, groupColumns);
|
|
2537
3714
|
continue;
|
|
2538
3715
|
}
|
|
2539
|
-
if (
|
|
2540
|
-
|
|
3716
|
+
if (name2 === "nodes") {
|
|
3717
|
+
fail2("'nodes' is not available on a grouped aggregate \u2014 query the rows separately", "invalid-aggregate", ["key", "aggregate"]);
|
|
2541
3718
|
}
|
|
2542
|
-
if (
|
|
2543
|
-
if (sawAggregate)
|
|
3719
|
+
if (name2 !== "aggregate") fail2(`unknown grouped-aggregate field '${name2}'`, "invalid-aggregate", ["key", "aggregate"]);
|
|
3720
|
+
if (sawAggregate) fail2("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
|
|
2544
3721
|
sawAggregate = true;
|
|
2545
|
-
if (!node.selectionSet)
|
|
3722
|
+
if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
|
|
2546
3723
|
Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
|
|
2547
3724
|
}
|
|
2548
3725
|
const rendersAggregate = result.count !== void 0 || (result.sum?.length ?? 0) > 0 || (result.avg?.length ?? 0) > 0;
|
|
2549
3726
|
if (result.key.length === 0 && !rendersAggregate) {
|
|
2550
|
-
|
|
3727
|
+
fail2(
|
|
2551
3728
|
"grouped aggregate needs at least one key column or an aggregate \u2014 a block selecting only __typename produces nothing",
|
|
2552
3729
|
"invalid-aggregate",
|
|
2553
3730
|
["key", "aggregate"]
|
|
@@ -2564,20 +3741,20 @@ var OperationLowerer = class {
|
|
|
2564
3741
|
lowerGroupKeySelection(selectionSet, table, groupColumns) {
|
|
2565
3742
|
const key = [];
|
|
2566
3743
|
for (const node of selectionSet.selections) {
|
|
2567
|
-
if (node.kind !== graphql.Kind.FIELD)
|
|
2568
|
-
const
|
|
2569
|
-
if (
|
|
2570
|
-
if (!this.column(table,
|
|
2571
|
-
|
|
2572
|
-
}
|
|
2573
|
-
if (!groupColumns.has(
|
|
2574
|
-
|
|
2575
|
-
`key column '${
|
|
3744
|
+
if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
3745
|
+
const name2 = node.name.value;
|
|
3746
|
+
if (name2 === "__typename") continue;
|
|
3747
|
+
if (!this.column(table, name2)) {
|
|
3748
|
+
fail2(`unknown key column '${name2}' on '${table}'`, "unknown-column", [...groupColumns]);
|
|
3749
|
+
}
|
|
3750
|
+
if (!groupColumns.has(name2)) {
|
|
3751
|
+
fail2(
|
|
3752
|
+
`key column '${name2}' is not one of this query's groupBy keys \u2014 it has no single value within a group`,
|
|
2576
3753
|
"invalid-group-key",
|
|
2577
3754
|
[...groupColumns]
|
|
2578
3755
|
);
|
|
2579
3756
|
}
|
|
2580
|
-
key.push({ alias: node.alias?.value ??
|
|
3757
|
+
key.push({ alias: node.alias?.value ?? name2, column: name2 });
|
|
2581
3758
|
}
|
|
2582
3759
|
return key;
|
|
2583
3760
|
}
|
|
@@ -2594,21 +3771,21 @@ var OperationLowerer = class {
|
|
|
2594
3771
|
if (columns) count.columns = this.lowerDistinctOn(columns.value, table);
|
|
2595
3772
|
const distinct = args.get("distinct");
|
|
2596
3773
|
if (distinct) {
|
|
2597
|
-
if (distinct.value.kind !== graphql.Kind.BOOLEAN)
|
|
3774
|
+
if (distinct.value.kind !== graphql.Kind.BOOLEAN) fail2("count(distinct:) must be a boolean literal", "invalid-aggregate");
|
|
2598
3775
|
count.distinct = distinct.value.value;
|
|
2599
3776
|
}
|
|
2600
3777
|
result.count = count;
|
|
2601
3778
|
continue;
|
|
2602
3779
|
}
|
|
2603
3780
|
if (aggName === "sum" || aggName === "avg") {
|
|
2604
|
-
if (!aggField.selectionSet)
|
|
3781
|
+
if (!aggField.selectionSet) fail2(`'${aggName}' needs a column selection`, "invalid-aggregate");
|
|
2605
3782
|
const columns = [];
|
|
2606
3783
|
for (const columnField of aggField.selectionSet.selections) {
|
|
2607
|
-
if (columnField.kind !== graphql.Kind.FIELD)
|
|
3784
|
+
if (columnField.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
2608
3785
|
if (columnField.name.value === "__typename") continue;
|
|
2609
3786
|
const column = this.column(table, columnField.name.value);
|
|
2610
3787
|
if (!column || typeof column.type !== "string" || !NUMERIC_TYPES.has(column.type)) {
|
|
2611
|
-
|
|
3788
|
+
fail2(
|
|
2612
3789
|
`'${aggName}' supports numeric columns only, got '${columnField.name.value}' on '${table}'`,
|
|
2613
3790
|
"invalid-aggregate",
|
|
2614
3791
|
this.meta(table).columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type)).map((c) => c.name)
|
|
@@ -2619,7 +3796,7 @@ var OperationLowerer = class {
|
|
|
2619
3796
|
result[aggName] = columns;
|
|
2620
3797
|
continue;
|
|
2621
3798
|
}
|
|
2622
|
-
|
|
3799
|
+
fail2(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
|
|
2623
3800
|
}
|
|
2624
3801
|
return result;
|
|
2625
3802
|
}
|
|
@@ -2629,41 +3806,43 @@ var OperationLowerer = class {
|
|
|
2629
3806
|
lowerRoots(operation, kind) {
|
|
2630
3807
|
const roots = [];
|
|
2631
3808
|
for (const node of operation.selectionSet.selections) {
|
|
2632
|
-
if (node.kind !== graphql.Kind.FIELD)
|
|
3809
|
+
if (node.kind !== graphql.Kind.FIELD) fail2("fragments are not supported at the operation root", "invalid-root");
|
|
2633
3810
|
if (node.selectionSet) graphql.visit(node.selectionSet, {
|
|
2634
3811
|
Directive(directive) {
|
|
2635
3812
|
if (directive.name.value === "assertAffectedRows") {
|
|
2636
|
-
|
|
3813
|
+
fail2("@assertAffectedRows is supported on mutation roots only", "invalid-affected-row-assertion");
|
|
2637
3814
|
}
|
|
2638
3815
|
}
|
|
2639
3816
|
});
|
|
2640
3817
|
const assertion = this.parseAffectedRowAssertion(node, kind);
|
|
2641
3818
|
const gate = this.parseRootDirectives(node);
|
|
2642
3819
|
if (gate.omit) continue;
|
|
2643
|
-
const
|
|
3820
|
+
const lowered = kind === "query" ? this.lowerQueryRoot(node) : this.lowerMutationRoot(node);
|
|
3821
|
+
const meta = this.meta(lowered.table);
|
|
3822
|
+
const root = kind === "query" ? applyPrefetchDirectives({ node, root: lowered, meta }) : applyWritebackDirectives({ node, root: lowered, meta });
|
|
2644
3823
|
const guarded = root.kind === "mutation" && assertion !== void 0 ? { ...root, assertAffectedRows: assertion } : root;
|
|
2645
3824
|
roots.push(gate.includeIf ? { ...guarded, includeIf: gate.includeIf } : guarded);
|
|
2646
3825
|
}
|
|
2647
|
-
if (roots.length === 0)
|
|
3826
|
+
if (roots.length === 0) fail2("operation has no roots (all statically skipped?)", "invalid-root");
|
|
2648
3827
|
return roots;
|
|
2649
3828
|
}
|
|
2650
3829
|
parseAffectedRowAssertion(node, kind) {
|
|
2651
3830
|
const assertions = node.directives?.filter((directive) => directive.name.value === "assertAffectedRows") ?? [];
|
|
2652
3831
|
if (!assertions.length) return void 0;
|
|
2653
3832
|
if (kind !== "mutation" || assertions.length !== 1) {
|
|
2654
|
-
|
|
3833
|
+
fail2("@assertAffectedRows is supported once per mutation root", "invalid-affected-row-assertion");
|
|
2655
3834
|
}
|
|
2656
3835
|
if (node.directives?.some((directive) => ["include", "skip"].includes(directive.name.value))) {
|
|
2657
|
-
|
|
3836
|
+
fail2("an @assertAffectedRows root cannot use @include or @skip", "invalid-affected-row-assertion");
|
|
2658
3837
|
}
|
|
2659
3838
|
const args = assertions[0].arguments;
|
|
2660
3839
|
const arg = args?.[0];
|
|
2661
3840
|
if (args?.length !== 1 || arg?.name.value !== "exactly" || arg.value.kind !== graphql.Kind.INT) {
|
|
2662
|
-
|
|
3841
|
+
fail2("@assertAffectedRows(exactly:) requires a document-authored nonnegative Int literal", "invalid-affected-row-assertion");
|
|
2663
3842
|
}
|
|
2664
3843
|
const count = Number(arg.value.value);
|
|
2665
3844
|
if (!Number.isInteger(count) || count < 0 || count > 2147483647) {
|
|
2666
|
-
|
|
3845
|
+
fail2("@assertAffectedRows(exactly:) must be between 0 and 2147483647", "invalid-affected-row-assertion");
|
|
2667
3846
|
}
|
|
2668
3847
|
return count;
|
|
2669
3848
|
}
|
|
@@ -2671,34 +3850,35 @@ var OperationLowerer = class {
|
|
|
2671
3850
|
let includeIf;
|
|
2672
3851
|
let omit = false;
|
|
2673
3852
|
for (const directive of node.directives ?? []) {
|
|
2674
|
-
const
|
|
2675
|
-
if (
|
|
2676
|
-
if (
|
|
2677
|
-
|
|
3853
|
+
const name2 = directive.name.value;
|
|
3854
|
+
if (name2 === "assertAffectedRows") continue;
|
|
3855
|
+
if (SOURCE_DIRECTIVES.has(name2)) continue;
|
|
3856
|
+
if (name2 !== "include" && name2 !== "skip") {
|
|
3857
|
+
fail2(`directive @${name2} is not supported on fields`, "unknown-directive", ["include", "skip"]);
|
|
2678
3858
|
}
|
|
2679
3859
|
const ifArg = directive.arguments?.find((a) => a.name.value === "if");
|
|
2680
|
-
if (!ifArg)
|
|
3860
|
+
if (!ifArg) fail2(`@${name2} needs an if: argument`, "invalid-directive");
|
|
2681
3861
|
if (ifArg.value.kind === graphql.Kind.BOOLEAN) {
|
|
2682
|
-
const keep =
|
|
3862
|
+
const keep = name2 === "include" ? ifArg.value.value : !ifArg.value.value;
|
|
2683
3863
|
if (!keep) omit = true;
|
|
2684
3864
|
continue;
|
|
2685
3865
|
}
|
|
2686
|
-
if (ifArg.value.kind !== graphql.Kind.VARIABLE)
|
|
2687
|
-
if (includeIf)
|
|
2688
|
-
includeIf = { variable: ifArg.value.name.value, negated:
|
|
3866
|
+
if (ifArg.value.kind !== graphql.Kind.VARIABLE) fail2(`@${name2}(if:) must be a boolean literal or variable`, "invalid-directive");
|
|
3867
|
+
if (includeIf) fail2("at most one of @include/@skip per root field", "invalid-directive");
|
|
3868
|
+
includeIf = { variable: ifArg.value.name.value, negated: name2 === "skip" };
|
|
2689
3869
|
}
|
|
2690
3870
|
return omit ? { omit: true } : { includeIf, omit: false };
|
|
2691
3871
|
}
|
|
2692
3872
|
lowerQueryRoot(node) {
|
|
2693
|
-
const
|
|
2694
|
-
const alias = node.alias?.value ??
|
|
2695
|
-
const rootInfo = this.ctx.queryRoots.get(
|
|
2696
|
-
if (!rootInfo)
|
|
3873
|
+
const name2 = node.name.value;
|
|
3874
|
+
const alias = node.alias?.value ?? name2;
|
|
3875
|
+
const rootInfo = this.ctx.queryRoots.get(name2);
|
|
3876
|
+
if (!rootInfo) fail2(`unknown query root '${name2}'`, "unknown-root", [...this.ctx.queryRoots.keys()]);
|
|
2697
3877
|
const { table } = rootInfo;
|
|
2698
3878
|
const args = argsByName(node);
|
|
2699
3879
|
if (rootInfo.kind === "select") {
|
|
2700
3880
|
assertKnownArgs(node, ["where", "orderBy", "distinctOn", "limit", "offset"]);
|
|
2701
|
-
if (!node.selectionSet)
|
|
3881
|
+
if (!node.selectionSet) fail2(`'${name2}' needs a selection set`, "invalid-selection");
|
|
2702
3882
|
const where = args.get("where");
|
|
2703
3883
|
const orderBy = args.get("orderBy");
|
|
2704
3884
|
const distinctOn = args.get("distinctOn");
|
|
@@ -2717,7 +3897,7 @@ var OperationLowerer = class {
|
|
|
2717
3897
|
};
|
|
2718
3898
|
}
|
|
2719
3899
|
if (rootInfo.kind === "byPk") {
|
|
2720
|
-
if (!node.selectionSet)
|
|
3900
|
+
if (!node.selectionSet) fail2(`'${name2}' needs a selection set`, "invalid-selection");
|
|
2721
3901
|
const pk = this.lowerPkArgs(node, table);
|
|
2722
3902
|
return { kind: "byPk", alias, table, selection: this.lowerSelection(node.selectionSet, table), pk, where: CONST_TRUE };
|
|
2723
3903
|
}
|
|
@@ -2725,7 +3905,7 @@ var OperationLowerer = class {
|
|
|
2725
3905
|
return this.lowerAggregateGroupsRoot(node, { alias, table });
|
|
2726
3906
|
}
|
|
2727
3907
|
assertKnownArgs(node, ["where"]);
|
|
2728
|
-
if (!node.selectionSet)
|
|
3908
|
+
if (!node.selectionSet) fail2(`'${name2}' needs a selection set`, "invalid-selection");
|
|
2729
3909
|
const whereArg = args.get("where");
|
|
2730
3910
|
const aggregate = this.lowerAggregateSelection(node.selectionSet, table);
|
|
2731
3911
|
return {
|
|
@@ -2739,10 +3919,10 @@ var OperationLowerer = class {
|
|
|
2739
3919
|
/** `<t>AggregateGroups(where:, groupBy:, limit:, offset:)` → an aggregateGroups root. */
|
|
2740
3920
|
lowerAggregateGroupsRoot(node, { alias, table }) {
|
|
2741
3921
|
assertKnownArgs(node, ["where", "groupBy", "limit", "offset"]);
|
|
2742
|
-
if (!node.selectionSet)
|
|
3922
|
+
if (!node.selectionSet) fail2(`'${node.name.value}' needs a selection set`, "invalid-selection");
|
|
2743
3923
|
const args = argsByName(node);
|
|
2744
3924
|
const groupByArg = args.get("groupBy");
|
|
2745
|
-
if (!groupByArg)
|
|
3925
|
+
if (!groupByArg) fail2(`'${node.name.value}' requires a 'groupBy' argument`, "missing-group-by");
|
|
2746
3926
|
const whereArg = args.get("where");
|
|
2747
3927
|
const groupBy = this.lowerGroupBy(groupByArg.value, table);
|
|
2748
3928
|
const aggregate = this.lowerGroupedAggregateSelection(node.selectionSet, table, groupBy);
|
|
@@ -2772,55 +3952,55 @@ var OperationLowerer = class {
|
|
|
2772
3952
|
*/
|
|
2773
3953
|
lowerGroupBy(node, table) {
|
|
2774
3954
|
if (node.kind === graphql.Kind.VARIABLE) {
|
|
2775
|
-
|
|
3955
|
+
fail2(
|
|
2776
3956
|
`groupBy on '${table}' must be written in the document \u2014 it cannot come from a variable`,
|
|
2777
3957
|
"dynamic-group-by"
|
|
2778
3958
|
);
|
|
2779
3959
|
}
|
|
2780
3960
|
const items = node.kind === graphql.Kind.LIST ? node.values : [node];
|
|
2781
|
-
if (items.length === 0)
|
|
3961
|
+
if (items.length === 0) fail2(`groupBy on '${table}' needs at least one key`, "invalid-group-by");
|
|
2782
3962
|
const seen = /* @__PURE__ */ new Set();
|
|
2783
3963
|
return items.map((item) => {
|
|
2784
3964
|
if (item.kind !== graphql.Kind.OBJECT) {
|
|
2785
|
-
|
|
3965
|
+
fail2(`groupBy entries on '${table}' must be objects like { column: foo }`, "invalid-group-by");
|
|
2786
3966
|
}
|
|
2787
3967
|
const fields = new Map(item.fields.map((field) => [field.name.value, field.value]));
|
|
2788
3968
|
for (const key of fields.keys()) {
|
|
2789
3969
|
if (key !== "column" && key !== "bucket") {
|
|
2790
|
-
|
|
3970
|
+
fail2(`unknown groupBy field '${key}' on '${table}'`, "invalid-group-by", ["column", "bucket"]);
|
|
2791
3971
|
}
|
|
2792
3972
|
}
|
|
2793
3973
|
const columnNode = fields.get("column");
|
|
2794
|
-
if (!columnNode)
|
|
3974
|
+
if (!columnNode) fail2(`groupBy entries on '${table}' need a 'column'`, "invalid-group-by");
|
|
2795
3975
|
if (columnNode.kind !== graphql.Kind.ENUM) {
|
|
2796
|
-
|
|
3976
|
+
fail2(`groupBy column on '${table}' must be a group-by column enum value`, "invalid-group-by");
|
|
2797
3977
|
}
|
|
2798
3978
|
const column = this.column(table, columnNode.value);
|
|
2799
3979
|
if (!column) {
|
|
2800
|
-
|
|
3980
|
+
fail2(`unknown groupBy column '${columnNode.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
|
|
2801
3981
|
}
|
|
2802
3982
|
if (column.type === "jsonb") {
|
|
2803
|
-
|
|
3983
|
+
fail2(`groupBy column '${table}.${columnNode.value}' is jsonb and cannot be a group key`, "invalid-group-by");
|
|
2804
3984
|
}
|
|
2805
3985
|
const bucketNode = fields.get("bucket");
|
|
2806
3986
|
let bucket;
|
|
2807
3987
|
if (bucketNode) {
|
|
2808
3988
|
if (bucketNode.kind !== graphql.Kind.ENUM) {
|
|
2809
|
-
|
|
3989
|
+
fail2(`groupBy bucket on '${table}.${columnNode.value}' must be a DateBucket enum value`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
|
|
2810
3990
|
}
|
|
2811
3991
|
bucket = DATE_BUCKET_BY_ENUM[bucketNode.value];
|
|
2812
3992
|
if (!bucket) {
|
|
2813
|
-
|
|
3993
|
+
fail2(`unknown DateBucket '${bucketNode.value}'`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
|
|
2814
3994
|
}
|
|
2815
3995
|
if (typeof column.type !== "string" || !BUCKETABLE_TYPES.has(column.type)) {
|
|
2816
|
-
|
|
3996
|
+
fail2(
|
|
2817
3997
|
`groupBy bucket is only valid on date/timestamptz columns \u2014 '${table}.${columnNode.value}' is ${typeof column.type === "string" ? column.type : "an enum"}`,
|
|
2818
3998
|
"invalid-group-by"
|
|
2819
3999
|
);
|
|
2820
4000
|
}
|
|
2821
4001
|
}
|
|
2822
4002
|
if (seen.has(columnNode.value)) {
|
|
2823
|
-
|
|
4003
|
+
fail2(`duplicate groupBy column '${columnNode.value}' on '${table}'`, "invalid-group-by");
|
|
2824
4004
|
}
|
|
2825
4005
|
seen.add(columnNode.value);
|
|
2826
4006
|
return bucket ? { column: columnNode.value, bucket } : { column: columnNode.value };
|
|
@@ -2828,15 +4008,15 @@ var OperationLowerer = class {
|
|
|
2828
4008
|
}
|
|
2829
4009
|
/** Like lowerLimit, but bounded by MAX_GROUPS and defaulting to it. */
|
|
2830
4010
|
lowerGroupLimit(node) {
|
|
2831
|
-
const
|
|
2832
|
-
if (!node) return
|
|
2833
|
-
if (node.kind === graphql.Kind.VARIABLE) return { ...
|
|
4011
|
+
const base2 = { default: MAX_GROUPS, max: MAX_GROUPS };
|
|
4012
|
+
if (!node) return base2;
|
|
4013
|
+
if (node.kind === graphql.Kind.VARIABLE) return { ...base2, variable: node.name.value };
|
|
2834
4014
|
if (node.kind === graphql.Kind.INT) {
|
|
2835
4015
|
const value = Number(node.value);
|
|
2836
|
-
if (value < 1)
|
|
2837
|
-
return { ...
|
|
4016
|
+
if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
|
|
4017
|
+
return { ...base2, value: Math.min(value, MAX_GROUPS) };
|
|
2838
4018
|
}
|
|
2839
|
-
|
|
4019
|
+
fail2("limit must be an integer literal or a variable", "invalid-limit");
|
|
2840
4020
|
}
|
|
2841
4021
|
/** byPk-style pk arguments: one argument per primary-key column, exactly. */
|
|
2842
4022
|
lowerPkArgs(node, table) {
|
|
@@ -2845,7 +4025,7 @@ var OperationLowerer = class {
|
|
|
2845
4025
|
assertKnownArgs(node, meta.primaryKey);
|
|
2846
4026
|
return meta.primaryKey.map((column) => {
|
|
2847
4027
|
const arg = args.get(column);
|
|
2848
|
-
if (!arg)
|
|
4028
|
+
if (!arg) fail2(`'${node.name.value}' is missing primary-key argument '${column}'`, "missing-pk-argument", meta.primaryKey);
|
|
2849
4029
|
return { column, value: this.lowerValue(arg.value) };
|
|
2850
4030
|
});
|
|
2851
4031
|
}
|
|
@@ -2853,25 +4033,28 @@ var OperationLowerer = class {
|
|
|
2853
4033
|
// Mutations
|
|
2854
4034
|
// -------------------------------------------------------------------------
|
|
2855
4035
|
lowerMutationRoot(node) {
|
|
2856
|
-
const
|
|
2857
|
-
const alias = node.alias?.value ??
|
|
2858
|
-
const rootInfo = this.ctx.mutationRoots.get(
|
|
2859
|
-
if (!rootInfo)
|
|
4036
|
+
const name2 = node.name.value;
|
|
4037
|
+
const alias = node.alias?.value ?? name2;
|
|
4038
|
+
const rootInfo = this.ctx.mutationRoots.get(name2);
|
|
4039
|
+
if (!rootInfo) fail2(`unknown mutation root '${name2}'`, "unknown-root", [...this.ctx.mutationRoots.keys()]);
|
|
2860
4040
|
if (rootInfo.op === "cowOverride" || rootInfo.op === "cowRevert") {
|
|
2861
4041
|
return this.lowerCowRoot(node, rootInfo);
|
|
2862
4042
|
}
|
|
4043
|
+
if (rootInfo.op === "resolvedWrite") {
|
|
4044
|
+
return this.lowerResolvedRoot(node, rootInfo);
|
|
4045
|
+
}
|
|
2863
4046
|
const { op, table } = rootInfo;
|
|
2864
4047
|
const args = argsByName(node);
|
|
2865
|
-
if (!node.selectionSet)
|
|
2866
|
-
const
|
|
4048
|
+
if (!node.selectionSet) fail2(`'${name2}' needs a selection set`, "invalid-selection");
|
|
4049
|
+
const base2 = { kind: "mutation", op, alias, table, pinnedColumns: [], affectedRows: false };
|
|
2867
4050
|
switch (op) {
|
|
2868
4051
|
case "insert": {
|
|
2869
4052
|
assertKnownArgs(node, ["objects", "onConflict"]);
|
|
2870
4053
|
const objects = args.get("objects");
|
|
2871
|
-
if (!objects)
|
|
4054
|
+
if (!objects) fail2(`'${name2}' needs objects:`, "missing-argument");
|
|
2872
4055
|
const response = this.lowerMutationResponse(node.selectionSet, table);
|
|
2873
4056
|
return {
|
|
2874
|
-
...
|
|
4057
|
+
...base2,
|
|
2875
4058
|
...response,
|
|
2876
4059
|
objects: this.lowerInsertObjects(objects.value, table, { list: true }),
|
|
2877
4060
|
...this.lowerOnConflictArg(args.get("onConflict"), table)
|
|
@@ -2880,9 +4063,9 @@ var OperationLowerer = class {
|
|
|
2880
4063
|
case "insertOne": {
|
|
2881
4064
|
assertKnownArgs(node, ["object", "onConflict"]);
|
|
2882
4065
|
const object = args.get("object");
|
|
2883
|
-
if (!object)
|
|
4066
|
+
if (!object) fail2(`'${name2}' needs object:`, "missing-argument");
|
|
2884
4067
|
return {
|
|
2885
|
-
...
|
|
4068
|
+
...base2,
|
|
2886
4069
|
objects: this.lowerInsertObjects(object.value, table, { list: false }),
|
|
2887
4070
|
...this.lowerOnConflictArg(args.get("onConflict"), table),
|
|
2888
4071
|
returning: this.lowerSelection(node.selectionSet, table)
|
|
@@ -2891,9 +4074,9 @@ var OperationLowerer = class {
|
|
|
2891
4074
|
case "update": {
|
|
2892
4075
|
assertKnownArgs(node, ["where", "_set", "_append", "_inc"]);
|
|
2893
4076
|
const where = args.get("where");
|
|
2894
|
-
if (!where)
|
|
4077
|
+
if (!where) fail2(`'${name2}' needs where: (bulk updates without a filter are not allowed)`, "missing-argument");
|
|
2895
4078
|
return {
|
|
2896
|
-
...
|
|
4079
|
+
...base2,
|
|
2897
4080
|
...this.lowerMutationResponse(node.selectionSet, table),
|
|
2898
4081
|
where: this.lowerBoolExp(where.value, table),
|
|
2899
4082
|
...this.lowerWriteValues(args, table)
|
|
@@ -2902,9 +4085,9 @@ var OperationLowerer = class {
|
|
|
2902
4085
|
case "updateByPk": {
|
|
2903
4086
|
assertKnownArgs(node, ["pkColumns", "_set", "_append", "_inc"]);
|
|
2904
4087
|
const pkColumns = args.get("pkColumns");
|
|
2905
|
-
if (!pkColumns)
|
|
4088
|
+
if (!pkColumns) fail2(`'${name2}' needs pkColumns:`, "missing-argument");
|
|
2906
4089
|
return {
|
|
2907
|
-
...
|
|
4090
|
+
...base2,
|
|
2908
4091
|
pk: this.lowerPkColumnsArg(pkColumns.value, table),
|
|
2909
4092
|
...this.lowerWriteValues(args, table),
|
|
2910
4093
|
returning: this.lowerSelection(node.selectionSet, table)
|
|
@@ -2913,16 +4096,16 @@ var OperationLowerer = class {
|
|
|
2913
4096
|
case "delete": {
|
|
2914
4097
|
assertKnownArgs(node, ["where"]);
|
|
2915
4098
|
const where = args.get("where");
|
|
2916
|
-
if (!where)
|
|
4099
|
+
if (!where) fail2(`'${name2}' needs where: (bulk deletes without a filter are not allowed)`, "missing-argument");
|
|
2917
4100
|
return {
|
|
2918
|
-
...
|
|
4101
|
+
...base2,
|
|
2919
4102
|
...this.lowerMutationResponse(node.selectionSet, table),
|
|
2920
4103
|
where: this.lowerBoolExp(where.value, table)
|
|
2921
4104
|
};
|
|
2922
4105
|
}
|
|
2923
4106
|
case "deleteByPk": {
|
|
2924
4107
|
return {
|
|
2925
|
-
...
|
|
4108
|
+
...base2,
|
|
2926
4109
|
pk: this.lowerPkArgs(node, table),
|
|
2927
4110
|
returning: this.lowerSelection(node.selectionSet, table)
|
|
2928
4111
|
};
|
|
@@ -2937,16 +4120,16 @@ var OperationLowerer = class {
|
|
|
2937
4120
|
* the structured fields, never from authored statements.
|
|
2938
4121
|
*/
|
|
2939
4122
|
lowerCowRoot(node, rootInfo) {
|
|
2940
|
-
const
|
|
2941
|
-
const alias = node.alias?.value ??
|
|
4123
|
+
const name2 = node.name.value;
|
|
4124
|
+
const alias = node.alias?.value ?? name2;
|
|
2942
4125
|
const { table } = rootInfo;
|
|
2943
|
-
if (!node.selectionSet)
|
|
4126
|
+
if (!node.selectionSet) fail2(`'${name2}' needs a selection set`, "invalid-selection");
|
|
2944
4127
|
const meta = this.meta(table);
|
|
2945
4128
|
const args = argsByName(node);
|
|
2946
4129
|
const pkArg = args.get(rootInfo.pk);
|
|
2947
|
-
if (!pkArg)
|
|
4130
|
+
if (!pkArg) fail2(`'${name2}' is missing primary-key argument '${rootInfo.pk}'`, "missing-pk-argument", [rootInfo.pk]);
|
|
2948
4131
|
const pk = [{ column: rootInfo.pk, value: this.lowerValue(pkArg.value) }];
|
|
2949
|
-
const
|
|
4132
|
+
const base2 = {
|
|
2950
4133
|
kind: "mutation",
|
|
2951
4134
|
op: rootInfo.op,
|
|
2952
4135
|
alias,
|
|
@@ -2959,65 +4142,119 @@ var OperationLowerer = class {
|
|
|
2959
4142
|
};
|
|
2960
4143
|
if (rootInfo.op === "cowRevert") {
|
|
2961
4144
|
assertKnownArgs(node, [rootInfo.pk]);
|
|
2962
|
-
return
|
|
4145
|
+
return base2;
|
|
2963
4146
|
}
|
|
2964
4147
|
assertKnownArgs(node, [rootInfo.pk, ...rootInfo.set]);
|
|
2965
4148
|
const entries = rootInfo.set.map((column) => {
|
|
2966
4149
|
const arg = args.get(column);
|
|
2967
4150
|
if (!arg) {
|
|
2968
|
-
|
|
2969
|
-
`'${
|
|
4151
|
+
fail2(
|
|
4152
|
+
`'${name2}' is missing set argument '${column}' \u2014 a cow override writes every declared set column (pass null to clear a nullable one)`,
|
|
2970
4153
|
"missing-argument",
|
|
2971
4154
|
[...rootInfo.set]
|
|
2972
4155
|
);
|
|
2973
4156
|
}
|
|
2974
4157
|
if (!meta.settable.includes(column)) {
|
|
2975
|
-
|
|
4158
|
+
fail2(`column '${table}.${column}' is not updatable (pinned by the write policy or deprecated)`, "column-not-settable", meta.settable);
|
|
2976
4159
|
}
|
|
2977
4160
|
return { column, value: this.lowerValue(arg.value) };
|
|
2978
4161
|
});
|
|
2979
|
-
return { ...
|
|
4162
|
+
return { ...base2, set: { entries }, cowCopyColumns: [...rootInfo.copyColumns] };
|
|
4163
|
+
}
|
|
4164
|
+
/**
|
|
4165
|
+
* Resolved-table roots: insertOne(object), updateByPk(pkColumns, _set),
|
|
4166
|
+
* deleteByPk(pk…), revertByPk(pkColumns, columns). The executor writes only
|
|
4167
|
+
* the touched cells into the manual source; `returning` reads the view.
|
|
4168
|
+
*/
|
|
4169
|
+
lowerResolvedRoot(node, rootInfo) {
|
|
4170
|
+
const name2 = node.name.value;
|
|
4171
|
+
const alias = node.alias?.value ?? name2;
|
|
4172
|
+
const { table, action } = rootInfo;
|
|
4173
|
+
if (!node.selectionSet) fail2(`'${name2}' needs a selection set`, "invalid-selection");
|
|
4174
|
+
const args = argsByName(node);
|
|
4175
|
+
const base2 = {
|
|
4176
|
+
kind: "mutation",
|
|
4177
|
+
op: "resolvedWrite",
|
|
4178
|
+
resolvedAction: action,
|
|
4179
|
+
alias,
|
|
4180
|
+
table,
|
|
4181
|
+
pinnedColumns: [],
|
|
4182
|
+
affectedRows: false,
|
|
4183
|
+
returning: this.lowerSelection(node.selectionSet, table)
|
|
4184
|
+
};
|
|
4185
|
+
switch (action) {
|
|
4186
|
+
case "insert": {
|
|
4187
|
+
assertKnownArgs(node, ["object"]);
|
|
4188
|
+
const object = args.get("object");
|
|
4189
|
+
if (!object) fail2(`'${name2}' needs object:`, "missing-argument");
|
|
4190
|
+
return { ...base2, objects: this.lowerInsertObjects(object.value, table, { list: false }) };
|
|
4191
|
+
}
|
|
4192
|
+
case "update": {
|
|
4193
|
+
assertKnownArgs(node, ["pkColumns", "_set"]);
|
|
4194
|
+
const pkColumns = args.get("pkColumns");
|
|
4195
|
+
if (!pkColumns || !args.get("_set")) fail2(`'${name2}' needs pkColumns: and _set:`, "missing-argument");
|
|
4196
|
+
return { ...base2, pk: this.lowerPkColumnsArg(pkColumns.value, table), ...this.lowerWriteValues(args, table) };
|
|
4197
|
+
}
|
|
4198
|
+
case "delete":
|
|
4199
|
+
return { ...base2, pk: this.lowerPkArgs(node, table) };
|
|
4200
|
+
case "revert": {
|
|
4201
|
+
assertKnownArgs(node, ["pkColumns", "columns"]);
|
|
4202
|
+
const pkColumns = args.get("pkColumns");
|
|
4203
|
+
const columns = args.get("columns");
|
|
4204
|
+
if (!pkColumns || !columns) fail2(`'${name2}' needs pkColumns: and columns:`, "missing-argument");
|
|
4205
|
+
const items = columns.value.kind === graphql.Kind.LIST ? columns.value.values : [columns.value];
|
|
4206
|
+
const meta = this.meta(table);
|
|
4207
|
+
const revertColumns = items.map((item) => {
|
|
4208
|
+
if (item.kind !== graphql.Kind.ENUM || !meta.settable.includes(item.value)) {
|
|
4209
|
+
fail2(`'${name2}' columns: must list update-column enum values written in the document`, "invalid-revert-columns", meta.settable);
|
|
4210
|
+
}
|
|
4211
|
+
return item.value;
|
|
4212
|
+
});
|
|
4213
|
+
if (revertColumns.length === 0) fail2(`'${name2}' columns: must name at least one column`, "invalid-revert-columns");
|
|
4214
|
+
return { ...base2, pk: this.lowerPkColumnsArg(pkColumns.value, table), revertColumns: [...new Set(revertColumns)] };
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
2980
4217
|
}
|
|
2981
4218
|
lowerMutationResponse(selectionSet, table) {
|
|
2982
4219
|
let affectedRows = false;
|
|
2983
4220
|
let returning = null;
|
|
2984
4221
|
for (const node of selectionSet.selections) {
|
|
2985
|
-
if (node.kind !== graphql.Kind.FIELD)
|
|
2986
|
-
const
|
|
2987
|
-
if (
|
|
2988
|
-
if (
|
|
4222
|
+
if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
4223
|
+
const name2 = node.name.value;
|
|
4224
|
+
if (name2 === "__typename") continue;
|
|
4225
|
+
if (name2 === "affectedRows") {
|
|
2989
4226
|
affectedRows = true;
|
|
2990
4227
|
continue;
|
|
2991
4228
|
}
|
|
2992
|
-
if (
|
|
2993
|
-
if (!node.selectionSet)
|
|
4229
|
+
if (name2 === "returning") {
|
|
4230
|
+
if (!node.selectionSet) fail2("'returning' needs a selection set", "invalid-selection");
|
|
2994
4231
|
returning = this.lowerSelection(node.selectionSet, table);
|
|
2995
4232
|
continue;
|
|
2996
4233
|
}
|
|
2997
|
-
|
|
4234
|
+
fail2(`unknown mutation-response field '${name2}'`, "invalid-selection", ["affectedRows", "returning"]);
|
|
2998
4235
|
}
|
|
2999
4236
|
return { affectedRows, returning };
|
|
3000
4237
|
}
|
|
3001
|
-
lowerInsertObjects(node, table, { list }) {
|
|
4238
|
+
lowerInsertObjects(node, table, { list: list2 }) {
|
|
3002
4239
|
if (node.kind === graphql.Kind.VARIABLE) return { slot: this.slot(node.name.value, "insertInput", table) };
|
|
3003
|
-
if (
|
|
3004
|
-
if (node.kind !== graphql.Kind.LIST)
|
|
4240
|
+
if (list2) {
|
|
4241
|
+
if (node.kind !== graphql.Kind.LIST) fail2(`objects: must be a list literal or a variable`, "invalid-insert");
|
|
3005
4242
|
return { rows: node.values.map((item) => this.lowerInsertRow(item, table)) };
|
|
3006
4243
|
}
|
|
3007
4244
|
return { rows: [this.lowerInsertRow(node, table)] };
|
|
3008
4245
|
}
|
|
3009
4246
|
lowerInsertRow(node, table) {
|
|
3010
|
-
if (node.kind !== graphql.Kind.OBJECT)
|
|
4247
|
+
if (node.kind !== graphql.Kind.OBJECT) fail2(`insert rows on '${table}' must be objects`, "invalid-insert");
|
|
3011
4248
|
const meta = this.meta(table);
|
|
3012
4249
|
return node.fields.map((field) => {
|
|
3013
4250
|
const key = field.name.value;
|
|
3014
4251
|
if (this.link(table, key)) {
|
|
3015
|
-
|
|
4252
|
+
fail2(`nested inserts are not supported in v2.0 \u2014 '${key}' is a link on '${table}'`, "nested-insert-unsupported");
|
|
3016
4253
|
}
|
|
3017
4254
|
if (!meta.insertable.includes(key)) {
|
|
3018
4255
|
const column = this.column(table, key);
|
|
3019
|
-
if (column)
|
|
3020
|
-
|
|
4256
|
+
if (column) fail2(`column '${table}.${key}' is not insertable (pinned by the write policy or deprecated)`, "column-not-insertable", meta.insertable);
|
|
4257
|
+
fail2(`unknown column '${key}' in an insert on '${table}'`, "unknown-column", meta.insertable);
|
|
3021
4258
|
}
|
|
3022
4259
|
return { column: key, value: this.lowerValue(field.value) };
|
|
3023
4260
|
});
|
|
@@ -3026,32 +4263,32 @@ var OperationLowerer = class {
|
|
|
3026
4263
|
if (!arg) return {};
|
|
3027
4264
|
const node = arg.value;
|
|
3028
4265
|
if (node.kind === graphql.Kind.VARIABLE) return { onConflict: { slot: this.slot(node.name.value, "onConflict", table) } };
|
|
3029
|
-
if (node.kind !== graphql.Kind.OBJECT)
|
|
4266
|
+
if (node.kind !== graphql.Kind.OBJECT) fail2("onConflict: must be an object literal or a variable", "invalid-on-conflict");
|
|
3030
4267
|
const meta = this.meta(table);
|
|
3031
4268
|
let constraint;
|
|
3032
4269
|
const updateColumns = [];
|
|
3033
4270
|
for (const field of node.fields) {
|
|
3034
4271
|
if (field.name.value === "constraint") {
|
|
3035
|
-
if (field.value.kind !== graphql.Kind.ENUM)
|
|
4272
|
+
if (field.value.kind !== graphql.Kind.ENUM) fail2("onConflict.constraint must be a constraint enum value", "invalid-on-conflict");
|
|
3036
4273
|
constraint = field.value.value;
|
|
3037
4274
|
continue;
|
|
3038
4275
|
}
|
|
3039
4276
|
if (field.name.value === "updateColumns") {
|
|
3040
4277
|
const items = field.value.kind === graphql.Kind.LIST ? field.value.values : [field.value];
|
|
3041
4278
|
for (const item of items) {
|
|
3042
|
-
if (item.kind !== graphql.Kind.ENUM)
|
|
4279
|
+
if (item.kind !== graphql.Kind.ENUM) fail2("onConflict.updateColumns must be update-column enum values", "invalid-on-conflict");
|
|
3043
4280
|
if (!meta.settable.includes(item.value)) {
|
|
3044
|
-
|
|
4281
|
+
fail2(`onConflict.updateColumns: '${item.value}' is not updatable on '${table}'`, "invalid-on-conflict", meta.settable);
|
|
3045
4282
|
}
|
|
3046
4283
|
updateColumns.push(item.value);
|
|
3047
4284
|
}
|
|
3048
4285
|
continue;
|
|
3049
4286
|
}
|
|
3050
|
-
|
|
4287
|
+
fail2(`unknown onConflict key '${field.name.value}'`, "invalid-on-conflict", ["constraint", "updateColumns"]);
|
|
3051
4288
|
}
|
|
3052
|
-
if (!constraint)
|
|
4289
|
+
if (!constraint) fail2("onConflict needs a constraint", "invalid-on-conflict", meta.constraints.map((c) => c.name));
|
|
3053
4290
|
if (!meta.constraints.some((c) => c.name === constraint)) {
|
|
3054
|
-
|
|
4291
|
+
fail2(`unknown constraint '${constraint}' on '${table}'`, "invalid-on-conflict", meta.constraints.map((c) => c.name));
|
|
3055
4292
|
}
|
|
3056
4293
|
return { onConflict: { constraint, updateColumns } };
|
|
3057
4294
|
}
|
|
@@ -3069,36 +4306,36 @@ var OperationLowerer = class {
|
|
|
3069
4306
|
const inc = args.get("_inc");
|
|
3070
4307
|
if (inc) {
|
|
3071
4308
|
if (inc.value.kind === graphql.Kind.VARIABLE) {
|
|
3072
|
-
|
|
4309
|
+
fail2("_inc has no dynamic input type \u2014 inline the columns with variable leaf values", "invalid-inc");
|
|
3073
4310
|
}
|
|
3074
4311
|
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
4312
|
result.inc = { entries: this.lowerColumnEntries(inc.value, table, numeric, "_inc") };
|
|
3076
4313
|
}
|
|
3077
4314
|
if (!result.set && !result.append && !result.inc) {
|
|
3078
|
-
|
|
4315
|
+
fail2(`an update on '${table}' needs at least one of _set/_append/_inc`, "missing-argument");
|
|
3079
4316
|
}
|
|
3080
4317
|
return result;
|
|
3081
4318
|
}
|
|
3082
4319
|
lowerColumnEntries(node, table, whitelist, argName) {
|
|
3083
|
-
if (node.kind !== graphql.Kind.OBJECT)
|
|
4320
|
+
if (node.kind !== graphql.Kind.OBJECT) fail2(`${argName} on '${table}' must be an object literal or a variable`, "invalid-write-values");
|
|
3084
4321
|
return node.fields.map((field) => {
|
|
3085
4322
|
const key = field.name.value;
|
|
3086
4323
|
if (!whitelist.includes(key)) {
|
|
3087
|
-
|
|
4324
|
+
fail2(`${argName}: column '${key}' is not allowed on '${table}'`, "column-not-settable", [...whitelist]);
|
|
3088
4325
|
}
|
|
3089
4326
|
return { column: key, value: this.lowerValue(field.value) };
|
|
3090
4327
|
});
|
|
3091
4328
|
}
|
|
3092
4329
|
lowerPkColumnsArg(node, table) {
|
|
3093
|
-
if (node.kind !== graphql.Kind.OBJECT)
|
|
4330
|
+
if (node.kind !== graphql.Kind.OBJECT) fail2(`pkColumns on '${table}' must be an object literal`, "invalid-pk-columns");
|
|
3094
4331
|
const meta = this.meta(table);
|
|
3095
4332
|
const provided = new Map(node.fields.map((field) => [field.name.value, field.value]));
|
|
3096
4333
|
for (const key of provided.keys()) {
|
|
3097
|
-
if (!meta.primaryKey.includes(key))
|
|
4334
|
+
if (!meta.primaryKey.includes(key)) fail2(`pkColumns: '${key}' is not a primary-key column of '${table}'`, "invalid-pk-columns", meta.primaryKey);
|
|
3098
4335
|
}
|
|
3099
4336
|
return meta.primaryKey.map((column) => {
|
|
3100
4337
|
const value = provided.get(column);
|
|
3101
|
-
if (!value)
|
|
4338
|
+
if (!value) fail2(`pkColumns is missing '${column}'`, "invalid-pk-columns", meta.primaryKey);
|
|
3102
4339
|
return { column, value: this.lowerValue(value) };
|
|
3103
4340
|
});
|
|
3104
4341
|
}
|
|
@@ -3107,6 +4344,19 @@ function lowerOperation({ restricted, ctx }) {
|
|
|
3107
4344
|
const lowerer = new OperationLowerer(ctx, restricted);
|
|
3108
4345
|
return lowerer.lowerRoots(restricted.definition, restricted.kind);
|
|
3109
4346
|
}
|
|
4347
|
+
function isCellColumn(meta, name2) {
|
|
4348
|
+
if (!meta.cells) return false;
|
|
4349
|
+
return Object.values(meta.cells).some((parts) => Object.values(parts).includes(name2));
|
|
4350
|
+
}
|
|
4351
|
+
function fieldNodes(selectionSet) {
|
|
4352
|
+
const out = [];
|
|
4353
|
+
for (const node of selectionSet.selections) {
|
|
4354
|
+
if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
4355
|
+
if (node.directives && node.directives.length > 0) fail2(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
|
|
4356
|
+
if (node.name.value !== "__typename") out.push(node);
|
|
4357
|
+
}
|
|
4358
|
+
return out;
|
|
4359
|
+
}
|
|
3110
4360
|
function parseSdl(sdl) {
|
|
3111
4361
|
return graphql.buildSchema(sdl);
|
|
3112
4362
|
}
|
|
@@ -3153,7 +4403,7 @@ function parseDocuments({
|
|
|
3153
4403
|
}
|
|
3154
4404
|
function unwrapType(node, variable) {
|
|
3155
4405
|
let nullable = true;
|
|
3156
|
-
let
|
|
4406
|
+
let list2 = false;
|
|
3157
4407
|
let current = node;
|
|
3158
4408
|
for (; ; ) {
|
|
3159
4409
|
if (current.kind === graphql.Kind.NON_NULL_TYPE) {
|
|
@@ -3162,17 +4412,17 @@ function unwrapType(node, variable) {
|
|
|
3162
4412
|
continue;
|
|
3163
4413
|
}
|
|
3164
4414
|
if (current.kind === graphql.Kind.LIST_TYPE) {
|
|
3165
|
-
if (
|
|
4415
|
+
if (list2) {
|
|
3166
4416
|
throw new DocumentCompileError(`variable $${variable}: nested list types are not supported`, "unsupported-variable-type");
|
|
3167
4417
|
}
|
|
3168
|
-
|
|
4418
|
+
list2 = true;
|
|
3169
4419
|
current = current.type;
|
|
3170
4420
|
continue;
|
|
3171
4421
|
}
|
|
3172
|
-
return { named: current.name.value, list, nullable };
|
|
4422
|
+
return { named: current.name.value, list: list2, nullable };
|
|
3173
4423
|
}
|
|
3174
4424
|
}
|
|
3175
|
-
function classifyVariableType({ named, list }, variable, ctx) {
|
|
4425
|
+
function classifyVariableType({ named, list: list2 }, variable, ctx) {
|
|
3176
4426
|
const inputRef = classifyInputType(named, ctx.tableByTypename);
|
|
3177
4427
|
if (inputRef) {
|
|
3178
4428
|
const listable = {
|
|
@@ -3185,13 +4435,13 @@ function classifyVariableType({ named, list }, variable, ctx) {
|
|
|
3185
4435
|
insertInput: "either"
|
|
3186
4436
|
};
|
|
3187
4437
|
const rule = listable[inputRef.input];
|
|
3188
|
-
if (rule === "must" && !
|
|
4438
|
+
if (rule === "must" && !list2) {
|
|
3189
4439
|
throw new DocumentCompileError(
|
|
3190
4440
|
`variable $${variable}: ${named} must be used as a list (e.g. [${named}!])`,
|
|
3191
4441
|
"unsupported-variable-type"
|
|
3192
4442
|
);
|
|
3193
4443
|
}
|
|
3194
|
-
if (rule === "never" &&
|
|
4444
|
+
if (rule === "never" && list2) {
|
|
3195
4445
|
throw new DocumentCompileError(
|
|
3196
4446
|
`variable $${variable}: ${named} cannot be used as a list`,
|
|
3197
4447
|
"unsupported-variable-type"
|
|
@@ -3228,7 +4478,7 @@ function restrictDefault({
|
|
|
3228
4478
|
node,
|
|
3229
4479
|
variable,
|
|
3230
4480
|
type,
|
|
3231
|
-
list,
|
|
4481
|
+
list: list2,
|
|
3232
4482
|
nullable
|
|
3233
4483
|
}) {
|
|
3234
4484
|
if (type.kind === "inputRef") {
|
|
@@ -3248,7 +4498,7 @@ function restrictDefault({
|
|
|
3248
4498
|
return { value: null };
|
|
3249
4499
|
}
|
|
3250
4500
|
if (Array.isArray(value)) {
|
|
3251
|
-
if (!
|
|
4501
|
+
if (!list2) {
|
|
3252
4502
|
throw new DocumentCompileError(
|
|
3253
4503
|
`variable $${variable}: default for a non-list variable must not be a list`,
|
|
3254
4504
|
"unsupported-variable-default"
|
|
@@ -3256,7 +4506,7 @@ function restrictDefault({
|
|
|
3256
4506
|
}
|
|
3257
4507
|
return { value: value.map((item) => restrictDefaultLiteral(item, variable, type)) };
|
|
3258
4508
|
}
|
|
3259
|
-
if (
|
|
4509
|
+
if (list2) {
|
|
3260
4510
|
throw new DocumentCompileError(
|
|
3261
4511
|
`variable $${variable}: default for a list variable must be a list`,
|
|
3262
4512
|
"unsupported-variable-default"
|
|
@@ -3291,8 +4541,8 @@ function restrictOperation({
|
|
|
3291
4541
|
if (operation.operation === "subscription") {
|
|
3292
4542
|
throw new DocumentCompileError("subscriptions are not supported in v2.0 \u2014 poll a query instead", "subscription-unsupported");
|
|
3293
4543
|
}
|
|
3294
|
-
const
|
|
3295
|
-
if (!
|
|
4544
|
+
const name2 = operation.name?.value;
|
|
4545
|
+
if (!name2) {
|
|
3296
4546
|
throw new DocumentCompileError(
|
|
3297
4547
|
"anonymous operations are not supported \u2014 every operation must be named (the name keys the registry)",
|
|
3298
4548
|
"anonymous-operation"
|
|
@@ -3301,14 +4551,14 @@ function restrictOperation({
|
|
|
3301
4551
|
const variables = (operation.variableDefinitions ?? []).map((definition) => {
|
|
3302
4552
|
const variable = definition.variable.name.value;
|
|
3303
4553
|
const unwrapped = unwrapType(definition.type, variable);
|
|
3304
|
-
const { list, nullable } = unwrapped;
|
|
4554
|
+
const { list: list2, nullable } = unwrapped;
|
|
3305
4555
|
const type = classifyVariableType(unwrapped, variable, ctx);
|
|
3306
|
-
const ir = { name: variable, type, nullable, list };
|
|
4556
|
+
const ir = { name: variable, type, nullable, list: list2 };
|
|
3307
4557
|
if (definition.defaultValue === void 0) return ir;
|
|
3308
|
-
return { ...ir, defaultValue: restrictDefault({ node: definition.defaultValue, variable, type, list, nullable }) };
|
|
4558
|
+
return { ...ir, defaultValue: restrictDefault({ node: definition.defaultValue, variable, type, list: list2, nullable }) };
|
|
3309
4559
|
});
|
|
3310
4560
|
return {
|
|
3311
|
-
name,
|
|
4561
|
+
name: name2,
|
|
3312
4562
|
kind: operation.operation === "mutation" ? "mutation" : "query",
|
|
3313
4563
|
requires: parseRequires(operation),
|
|
3314
4564
|
variables,
|
|
@@ -3346,8 +4596,20 @@ function compileNamespace({ canonical, sdl, documents }) {
|
|
|
3346
4596
|
}
|
|
3347
4597
|
const loweredRoots = lowerOperation({ restricted: { ...restricted, definition: flattened }, ctx });
|
|
3348
4598
|
const { roots, tableSet } = injectPolicies({ roots: loweredRoots, ctx });
|
|
4599
|
+
for (const table of tableSet) {
|
|
4600
|
+
const meta = ctx.tables[table];
|
|
4601
|
+
if (meta.namespace && meta.namespace !== ctx.namespace && !meta.exportRead) {
|
|
4602
|
+
throw new DocumentCompileError(`object '${meta.namespace}/${table}' is not exported for reads`, "unexported-object");
|
|
4603
|
+
}
|
|
4604
|
+
}
|
|
4605
|
+
for (const root of roots) {
|
|
4606
|
+
const meta = ctx.tables[root.table];
|
|
4607
|
+
if (root.kind === "mutation" && meta.namespace && meta.namespace !== ctx.namespace && !meta.exportWrite) {
|
|
4608
|
+
throw new DocumentCompileError(`object '${meta.namespace}/${root.table}' is not exported for writes`, "unexported-object");
|
|
4609
|
+
}
|
|
4610
|
+
}
|
|
3349
4611
|
operations[restricted.name] = {
|
|
3350
|
-
irVersion: roots.some((root) => root.kind === "mutation" && root.assertAffectedRows !== void 0) ?
|
|
4612
|
+
irVersion: canonical.imports?.length ? COMPOSED_IR_VERSION : roots.some((root) => root.kind === "mutation" && root.assertAffectedRows !== void 0) ? GUARDED_IR_VERSION : LEGACY_IR_VERSION,
|
|
3351
4613
|
name: restricted.name,
|
|
3352
4614
|
opHash: computeOpHash({ definition: flattened, schemaHash }),
|
|
3353
4615
|
kind: restricted.kind,
|
|
@@ -3374,9 +4636,14 @@ function compileNamespace({ canonical, sdl, documents }) {
|
|
|
3374
4636
|
}
|
|
3375
4637
|
if (diagnostics.some((d) => d.severity === "error")) return { ok: false, diagnostics };
|
|
3376
4638
|
const artifact = {
|
|
3377
|
-
irVersion: requiredArtifactIrVersion(Object.values(operations)),
|
|
4639
|
+
irVersion: canonical.imports?.length ? COMPOSED_IR_VERSION : requiredArtifactIrVersion(Object.values(operations)),
|
|
3378
4640
|
namespace: ctx.namespace,
|
|
3379
4641
|
schemaHash,
|
|
4642
|
+
ownSchemaHash: computeSchemaHash(localContract(canonical)),
|
|
4643
|
+
...canonical.imports?.length ? {
|
|
4644
|
+
dependencies: Object.fromEntries(canonical.imports.map((owner) => [owner.namespace, computeSchemaHash(localContract(owner))]))
|
|
4645
|
+
} : {},
|
|
4646
|
+
readPolicies: Object.fromEntries(ctx.readPolicies),
|
|
3380
4647
|
opsHash: computeOpsHash(Object.values(operations).map((op) => op.opHash)),
|
|
3381
4648
|
tables: ctx.tables,
|
|
3382
4649
|
operations
|
|
@@ -3439,9 +4706,10 @@ function allowedOps(type) {
|
|
|
3439
4706
|
case "text":
|
|
3440
4707
|
return ["eq", "neq", "gt", "gte", "lt", "lte", "in", "nin", "like", "ilike", "isNull"];
|
|
3441
4708
|
case "uuid":
|
|
3442
|
-
return ["eq", "neq", "in", "nin", "isNull"];
|
|
4709
|
+
return ["eq", "neq", "in", "nin", "like", "ilike", "isNull"];
|
|
3443
4710
|
case "integer":
|
|
3444
4711
|
case "bigint":
|
|
4712
|
+
return ["eq", "neq", "gt", "gte", "lt", "lte", "in", "nin", "like", "ilike", "isNull"];
|
|
3445
4713
|
case "numeric":
|
|
3446
4714
|
case "double":
|
|
3447
4715
|
case "timestamptz":
|
|
@@ -3659,12 +4927,12 @@ function validateSelectColumns({ table, value }) {
|
|
|
3659
4927
|
function validateInsertInput({
|
|
3660
4928
|
table,
|
|
3661
4929
|
value,
|
|
3662
|
-
list,
|
|
4930
|
+
list: list2,
|
|
3663
4931
|
maxRows
|
|
3664
4932
|
}) {
|
|
3665
4933
|
const collector = new IssueCollector();
|
|
3666
4934
|
const rowCap = maxRows ?? 1e3;
|
|
3667
|
-
const rows =
|
|
4935
|
+
const rows = list2 ? value : [value];
|
|
3668
4936
|
if (!Array.isArray(rows)) {
|
|
3669
4937
|
collector.add("invalid-shape", "$", "objects must be an array of rows");
|
|
3670
4938
|
return { ok: false, issues: collector.issues };
|
|
@@ -3675,7 +4943,7 @@ function validateInsertInput({
|
|
|
3675
4943
|
}
|
|
3676
4944
|
const normalized = [];
|
|
3677
4945
|
rows.forEach((row, index) => {
|
|
3678
|
-
const path =
|
|
4946
|
+
const path = list2 ? `$[${index}]` : "$";
|
|
3679
4947
|
if (!isPlainObject(row)) {
|
|
3680
4948
|
collector.add("invalid-shape", path, "insert rows must be objects");
|
|
3681
4949
|
return;
|
|
@@ -3785,8 +5053,747 @@ function validateOnConflictInput({
|
|
|
3785
5053
|
return finish(collector, { constraint, updateColumns });
|
|
3786
5054
|
}
|
|
3787
5055
|
|
|
5056
|
+
// src/engine/types.ts
|
|
5057
|
+
var SQL_TYPE_NAMES = {
|
|
5058
|
+
double: "double precision",
|
|
5059
|
+
timestamptz: "timestamp with time zone"
|
|
5060
|
+
};
|
|
5061
|
+
var sqlTypeName = (type) => SQL_TYPE_NAMES[type] ?? type;
|
|
5062
|
+
|
|
5063
|
+
// src/compiler/pred-ir-sql.ts
|
|
5064
|
+
function compilePredIrToSql(pred, options = {}) {
|
|
5065
|
+
switch (pred.kind) {
|
|
5066
|
+
case "const":
|
|
5067
|
+
return pred.value ? "TRUE" : "FALSE";
|
|
5068
|
+
case "and":
|
|
5069
|
+
return `(${pred.operands.map((operand) => compilePredIrToSql(operand, options)).join(" AND ")})`;
|
|
5070
|
+
case "or":
|
|
5071
|
+
return `(${pred.operands.map((operand) => compilePredIrToSql(operand, options)).join(" OR ")})`;
|
|
5072
|
+
case "not":
|
|
5073
|
+
return `(NOT ${compilePredIrToSql(pred.operand, options)})`;
|
|
5074
|
+
case "cmp":
|
|
5075
|
+
return compileCmp(pred.op, pred.column, pred.value, options);
|
|
5076
|
+
case "capability":
|
|
5077
|
+
return `(${capabilityExpression(pred.key, pred.namespace ?? options.namespace)})`;
|
|
5078
|
+
case "role":
|
|
5079
|
+
return `(${roleExpression(pred.key, pred.namespace ?? options.namespace)})`;
|
|
5080
|
+
case "exists":
|
|
5081
|
+
if (options.exists) return options.exists(pred);
|
|
5082
|
+
throw new OrmDefinitionError("synced view read policy cannot use 'exists' without a relationship resolver");
|
|
5083
|
+
case "aggPredicate":
|
|
5084
|
+
case "dynamicFilter":
|
|
5085
|
+
throw new OrmDefinitionError(
|
|
5086
|
+
`synced view read policy cannot use '${pred.kind}' \u2014 use row filters and claim/capability/role checks only`
|
|
5087
|
+
);
|
|
5088
|
+
default: {
|
|
5089
|
+
const exhausted = pred;
|
|
5090
|
+
throw new OrmDefinitionError(`unsupported synced view predicate kind '${String(exhausted)}'`);
|
|
5091
|
+
}
|
|
5092
|
+
}
|
|
5093
|
+
}
|
|
5094
|
+
function physicalColumn(logical, options) {
|
|
5095
|
+
return options.columnNames?.[logical] ?? logical;
|
|
5096
|
+
}
|
|
5097
|
+
function compileCmp(op, column, value, options) {
|
|
5098
|
+
const physical = physicalColumn(column, options);
|
|
5099
|
+
const declared = options.columnTypes?.[physical] ?? options.columnTypes?.[column];
|
|
5100
|
+
const isEnum = typeof declared === "object" && declared !== null;
|
|
5101
|
+
const qualified = options.alias ? `${quoteIdentifier(options.alias)}.${quoteIdentifier(physical)}` : quoteIdentifier(physical);
|
|
5102
|
+
const col = isEnum ? `${qualified}::text` : qualified;
|
|
5103
|
+
const columnType = isEnum ? "text" : declared;
|
|
5104
|
+
switch (op) {
|
|
5105
|
+
case "eq":
|
|
5106
|
+
return compileEquality(col, value, columnType, false);
|
|
5107
|
+
case "neq":
|
|
5108
|
+
return compileEquality(col, value, columnType, true);
|
|
5109
|
+
case "gt":
|
|
5110
|
+
return `${col} > ${compileValue(value, columnType)}`;
|
|
5111
|
+
case "gte":
|
|
5112
|
+
return `${col} >= ${compileValue(value, columnType)}`;
|
|
5113
|
+
case "lt":
|
|
5114
|
+
return `${col} < ${compileValue(value, columnType)}`;
|
|
5115
|
+
case "lte":
|
|
5116
|
+
return `${col} <= ${compileValue(value, columnType)}`;
|
|
5117
|
+
case "in":
|
|
5118
|
+
return `${col} = ANY(${compileArrayValue(value, columnType)})`;
|
|
5119
|
+
case "nin":
|
|
5120
|
+
return `${col} <> ALL(${compileArrayValue(value, columnType)})`;
|
|
5121
|
+
case "like":
|
|
5122
|
+
return `${col} LIKE ${compileValue(value, columnType)}`;
|
|
5123
|
+
case "ilike":
|
|
5124
|
+
return `${col} ILIKE ${compileValue(value, columnType)}`;
|
|
5125
|
+
case "isNull":
|
|
5126
|
+
return value.kind === "literal" && value.value === true ? `${col} IS NULL` : `${col} IS NOT NULL`;
|
|
5127
|
+
case "contains":
|
|
5128
|
+
case "hasKey":
|
|
5129
|
+
throw new OrmDefinitionError(`synced view read policy cannot use json operator '${op}'`);
|
|
5130
|
+
default: {
|
|
5131
|
+
const exhausted = op;
|
|
5132
|
+
throw new OrmDefinitionError(`unsupported synced view comparison '${String(exhausted)}'`);
|
|
5133
|
+
}
|
|
5134
|
+
}
|
|
5135
|
+
}
|
|
5136
|
+
function compileEquality(col, value, columnType, negated, options) {
|
|
5137
|
+
if (value.kind === "literal" && value.value === null) {
|
|
5138
|
+
return negated ? `${col} IS NOT NULL` : `${col} IS NULL`;
|
|
5139
|
+
}
|
|
5140
|
+
const rhs = compileValue(value, columnType);
|
|
5141
|
+
return negated ? `${col} <> ${rhs}` : `${col} = ${rhs}`;
|
|
5142
|
+
}
|
|
5143
|
+
function compileValue(value, columnType, _options) {
|
|
5144
|
+
switch (value.kind) {
|
|
5145
|
+
case "literal": {
|
|
5146
|
+
const literal = value.value;
|
|
5147
|
+
if (literal === null) return "NULL";
|
|
5148
|
+
if (typeof literal === "string") return quoteLiteral(literal);
|
|
5149
|
+
if (typeof literal === "boolean") return literal ? "TRUE" : "FALSE";
|
|
5150
|
+
return String(literal);
|
|
5151
|
+
}
|
|
5152
|
+
case "claim": {
|
|
5153
|
+
const expression = claimExpression(value.key);
|
|
5154
|
+
return columnType && columnType !== "text" ? `(${expression})::${sqlTypeName(columnType)}` : expression;
|
|
5155
|
+
}
|
|
5156
|
+
case "variable":
|
|
5157
|
+
throw new OrmDefinitionError("synced view read policy cannot reference operation variables");
|
|
5158
|
+
default: {
|
|
5159
|
+
const exhausted = value;
|
|
5160
|
+
throw new OrmDefinitionError(`unsupported synced view value kind '${String(exhausted)}'`);
|
|
5161
|
+
}
|
|
5162
|
+
}
|
|
5163
|
+
}
|
|
5164
|
+
function compileArrayValue(value, columnType, _options) {
|
|
5165
|
+
if (value.kind !== "literal" || !Array.isArray(value.value)) {
|
|
5166
|
+
throw new OrmDefinitionError("synced view IN/NIN comparisons require literal arrays");
|
|
5167
|
+
}
|
|
5168
|
+
const entries = value.value.map((entry) => {
|
|
5169
|
+
if (entry === null) return "NULL";
|
|
5170
|
+
if (typeof entry === "string") return quoteLiteral(entry);
|
|
5171
|
+
if (typeof entry === "boolean") return entry ? "TRUE" : "FALSE";
|
|
5172
|
+
return String(entry);
|
|
5173
|
+
});
|
|
5174
|
+
const array = `ARRAY[${entries.join(", ")}]`;
|
|
5175
|
+
return columnType ? `${array}::${sqlTypeName(columnType)}[]` : array;
|
|
5176
|
+
}
|
|
5177
|
+
|
|
5178
|
+
// src/sql/catalog.ts
|
|
5179
|
+
function sqlHash(value) {
|
|
5180
|
+
return `sha256:${crypto.createHash("sha256").update(value).digest("hex")}`;
|
|
5181
|
+
}
|
|
5182
|
+
function createSqlCatalog({ canonical, publishedObjects, executionTarget }) {
|
|
5183
|
+
if (!publishedObjects.length || new Set(publishedObjects).size !== publishedObjects.length) throw new Error("SQL objects must be explicitly declared and unique");
|
|
5184
|
+
const { ctx, diagnostics } = buildCompileContext(canonical);
|
|
5185
|
+
if (diagnostics.some((d) => d.severity === "error")) throw new Error(diagnostics.map((d) => d.message).join("; "));
|
|
5186
|
+
const identity = sqlHash(`ontology-sql-v1\0${computeSchemaHash(canonical)}\0${executionTarget}`);
|
|
5187
|
+
const refOf = (meta) => `${meta.namespace ?? canonical.namespace}/${meta.table}`;
|
|
5188
|
+
const definitions = [canonical, ...canonical.imports ?? []];
|
|
5189
|
+
const internal = new Set(definitions.flatMap((owner) => [
|
|
5190
|
+
...owner.tables.filter((t) => t.internal).map((t) => `${owner.namespace}/${t.table}`),
|
|
5191
|
+
...(owner.views ?? []).filter((v) => v.internal).map((v) => `${owner.namespace}/${v.view}`)
|
|
5192
|
+
]));
|
|
5193
|
+
const objects = publishedObjects.map((ref) => {
|
|
5194
|
+
const meta = Object.values(ctx.tables).find((table) => refOf(table) === ref);
|
|
5195
|
+
if (!meta || internal.has(ref) || meta.namespace !== canonical.namespace && !meta.exportRead) throw new Error(`SQL object not published: ${ref}`);
|
|
5196
|
+
const reads = /* @__PURE__ */ new Set([ref]);
|
|
5197
|
+
let sequence = 0;
|
|
5198
|
+
const predicate = (table, pred, alias) => compilePredIrToSql(pred, {
|
|
5199
|
+
alias,
|
|
5200
|
+
namespace: table.namespace ?? canonical.namespace,
|
|
5201
|
+
columnNames: Object.fromEntries(table.columns.map((c) => [c.name, c.columnName])),
|
|
5202
|
+
columnTypes: Object.fromEntries(table.columns.map((c) => [c.name, c.type])),
|
|
5203
|
+
exists: (node) => {
|
|
5204
|
+
const link = table.links.find((candidate) => candidate.name === node.link);
|
|
5205
|
+
const target = link?.target ? ctx.tables[link.target] : void 0;
|
|
5206
|
+
if (!link || !target) throw new Error("SQL policy link is unavailable");
|
|
5207
|
+
reads.add(refOf(target));
|
|
5208
|
+
const child = `p${++sequence}`;
|
|
5209
|
+
const col = (t, a, name2) => {
|
|
5210
|
+
const c = t.columns.find((c2) => c2.name === name2 || c2.columnName === name2);
|
|
5211
|
+
if (!c) throw new Error("SQL policy column is unavailable");
|
|
5212
|
+
return `${quoteIdentifier(a)}.${quoteIdentifier(c.columnName)}`;
|
|
5213
|
+
};
|
|
5214
|
+
const join = link.kind === "toMany" ? `${col(target, child, link.on)} = ${col(table, alias, link.references)}` : `${col(table, alias, link.on)} = ${col(target, child, link.references)}`;
|
|
5215
|
+
return `EXISTS (SELECT 1 FROM ${quoteIdentifier(target.schema)}.${quoteIdentifier(target.table)} AS ${quoteIdentifier(child)} WHERE ${join} AND (${predicate(target, node.where, child)}))`;
|
|
5216
|
+
}
|
|
5217
|
+
});
|
|
5218
|
+
const columns = meta.columns.filter((c) => meta.filterable.includes(c.name)).map((c) => ({
|
|
5219
|
+
name: c.name,
|
|
5220
|
+
type: typeof c.type === "string" ? c.type : "text",
|
|
5221
|
+
nullable: c.nullable
|
|
5222
|
+
}));
|
|
5223
|
+
const projection = columns.map((c) => {
|
|
5224
|
+
const physical = meta.columns.find((m) => m.name === c.name);
|
|
5225
|
+
return `${quoteIdentifier("p0")}.${quoteIdentifier(physical.columnName)}${typeof physical.type === "object" ? "::text" : ""} AS ${quoteIdentifier(c.name)}`;
|
|
5226
|
+
}).join(", ");
|
|
5227
|
+
const policy = ctx.readPolicies.get(meta.table) ?? { kind: "const", value: true };
|
|
5228
|
+
const scan = `SELECT ${projection} FROM ${quoteIdentifier(meta.schema)}.${quoteIdentifier(meta.table)} AS ${quoteIdentifier("p0")} WHERE ${predicate(meta, policy, "p0")}`;
|
|
5229
|
+
return { ref, table: meta.table, columns, scan, read: [...reads].sort() };
|
|
5230
|
+
});
|
|
5231
|
+
return { identity, objects };
|
|
5232
|
+
}
|
|
5233
|
+
|
|
5234
|
+
// src/sql/ast.ts
|
|
5235
|
+
function obj(value) {
|
|
5236
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("unsupported SQL node");
|
|
5237
|
+
return value;
|
|
5238
|
+
}
|
|
5239
|
+
function list(value) {
|
|
5240
|
+
return value === void 0 ? [] : Array.isArray(value) ? value : fail3("expected SQL list");
|
|
5241
|
+
}
|
|
5242
|
+
function str(value) {
|
|
5243
|
+
return typeof value === "string" ? value : fail3("expected SQL name");
|
|
5244
|
+
}
|
|
5245
|
+
function fail3(message) {
|
|
5246
|
+
throw new Error(message);
|
|
5247
|
+
}
|
|
5248
|
+
function strings(value) {
|
|
5249
|
+
return list(value).map((v) => str(obj(obj(v).String).sval));
|
|
5250
|
+
}
|
|
5251
|
+
function only(node, keys) {
|
|
5252
|
+
for (const [key, value] of Object.entries(node)) {
|
|
5253
|
+
if (!keys.includes(key) && value !== void 0 && value !== null && value !== false && value !== "" && value !== 0 && !(Array.isArray(value) && value.length === 0)) {
|
|
5254
|
+
fail3(`unsupported SQL clause: ${key}`);
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
}
|
|
5258
|
+
|
|
5259
|
+
// src/sql/compiler.ts
|
|
5260
|
+
var parser;
|
|
5261
|
+
var SQL_TYPES = ["text", "boolean", "integer", "bigint", "numeric", "double", "uuid", "date", "timestamptz"];
|
|
5262
|
+
var pgType = (type) => type === "double" ? "double precision" : SQL_TYPES.some((t) => t === type) ? type : fail3("unsupported SQL type");
|
|
5263
|
+
async function compileOntologySql({ catalog, sql, parameterTypes = [], maxRows = 1e3 }) {
|
|
5264
|
+
if (!Number.isInteger(maxRows) || maxRows < 1 || maxRows > 1e4 || Buffer.byteLength(sql) > 65536) fail3("SQL limit exceeded");
|
|
5265
|
+
parser ??= new pgParser.PgParser({ version: 17 });
|
|
5266
|
+
const parsed = await parser.parse(sql);
|
|
5267
|
+
if (parsed.error || !parsed.tree) fail3("invalid SQL");
|
|
5268
|
+
const statements = list(obj(parsed.tree).stmts);
|
|
5269
|
+
if (statements.length !== 1) fail3("one SQL SELECT is required");
|
|
5270
|
+
const emitter = new SqlEmitter({ catalog, parameterTypes });
|
|
5271
|
+
const result = emitter.select(obj(obj(statements[0]).stmt), { relations: [], ctes: /* @__PURE__ */ new Map() });
|
|
5272
|
+
if (emitter.used.size !== parameterTypes.length) fail3("SQL parameters must be used exactly as declared");
|
|
5273
|
+
if (!emitter.read.size) fail3("SQL operation must read an ontology object");
|
|
5274
|
+
return {
|
|
5275
|
+
...result,
|
|
5276
|
+
read: [...emitter.read].sort(),
|
|
5277
|
+
maxRows,
|
|
5278
|
+
queryHash: sqlHash(JSON.stringify(["ontology-sql-v1", catalog.identity, result.text, parameterTypes, maxRows]))
|
|
5279
|
+
};
|
|
5280
|
+
}
|
|
5281
|
+
var SqlEmitter = class {
|
|
5282
|
+
constructor(input) {
|
|
5283
|
+
this.input = input;
|
|
5284
|
+
}
|
|
5285
|
+
read = /* @__PURE__ */ new Set();
|
|
5286
|
+
used = /* @__PURE__ */ new Set();
|
|
5287
|
+
nodes = 0;
|
|
5288
|
+
depth = 0;
|
|
5289
|
+
budget() {
|
|
5290
|
+
if (++this.nodes > 5e3 || this.depth > 48) fail3("SQL complexity limit exceeded");
|
|
5291
|
+
}
|
|
5292
|
+
select(wrapper, parent, resolveUnknown = true) {
|
|
5293
|
+
this.budget();
|
|
5294
|
+
this.depth++;
|
|
5295
|
+
const node = obj(wrapper.SelectStmt);
|
|
5296
|
+
only(node, ["targetList", "fromClause", "whereClause", "groupClause", "havingClause", "sortClause", "limitCount", "limitOffset", "limitOption", "op", "withClause", "all", "larg", "rarg"]);
|
|
5297
|
+
if (!["SETOP_NONE", "SETOP_UNION"].includes(str(node.op))) fail3("unsupported SQL set operation");
|
|
5298
|
+
if (node.op === "SETOP_NONE" && (node.all || node.larg || node.rarg)) fail3("invalid SQL set operation");
|
|
5299
|
+
if (node.limitOption && !["LIMIT_OPTION_DEFAULT", "LIMIT_OPTION_COUNT"].includes(str(node.limitOption))) fail3("unsupported SQL limit");
|
|
5300
|
+
const scope = { relations: [], parent, ctes: new Map(parent.ctes) };
|
|
5301
|
+
const ctes = [];
|
|
5302
|
+
if (node.withClause) {
|
|
5303
|
+
const withNode = obj(node.withClause);
|
|
5304
|
+
only(withNode, ["ctes", "location"]);
|
|
5305
|
+
for (const entry of list(withNode.ctes)) {
|
|
5306
|
+
const cte = obj(obj(entry).CommonTableExpr);
|
|
5307
|
+
only(cte, ["ctename", "ctequery", "ctematerialized", "location"]);
|
|
5308
|
+
const name2 = str(cte.ctename);
|
|
5309
|
+
if (scope.ctes.has(name2)) fail3("duplicate CTE");
|
|
5310
|
+
const result2 = this.select(obj(cte.ctequery), { ...parent, ctes: scope.ctes });
|
|
5311
|
+
scope.ctes.set(name2, result2);
|
|
5312
|
+
ctes.push(`${quoteIdentifier(name2)} AS (${result2.text})`);
|
|
5313
|
+
}
|
|
5314
|
+
}
|
|
5315
|
+
const result = node.op === "SETOP_UNION" ? this.union(node, scope) : this.projection(node, scope);
|
|
5316
|
+
const { columns } = result;
|
|
5317
|
+
let text = `${ctes.length ? `WITH ${ctes.join(", ")} ` : ""}${result.text}`;
|
|
5318
|
+
if (list(node.sortClause).length) text += ` ORDER BY ${list(node.sortClause).map((n) => {
|
|
5319
|
+
const sort = obj(obj(n).SortBy);
|
|
5320
|
+
only(sort, ["node", "sortby_dir", "sortby_nulls", "location"]);
|
|
5321
|
+
const direction = sort.sortby_dir === "SORTBY_DESC" ? "DESC" : sort.sortby_dir === "SORTBY_ASC" || sort.sortby_dir === "SORTBY_DEFAULT" ? "ASC" : fail3("unsupported SQL sort");
|
|
5322
|
+
const nulls = sort.sortby_nulls === "SORTBY_NULLS_FIRST" ? " NULLS FIRST" : sort.sortby_nulls === "SORTBY_NULLS_LAST" ? " NULLS LAST" : "";
|
|
5323
|
+
const value = obj(sort.node);
|
|
5324
|
+
const reference = value.ColumnRef ? strings(obj(value.ColumnRef).fields) : [];
|
|
5325
|
+
const projected = reference.length === 1 ? columns.filter((c) => c.name === reference[0]) : [];
|
|
5326
|
+
return `${projected.length === 1 ? quoteIdentifier(projected[0].name) : this.expr(value, scope).text} ${direction}${nulls}`;
|
|
5327
|
+
}).join(", ")}`;
|
|
5328
|
+
for (const [key, keyword] of [["limitCount", "LIMIT"], ["limitOffset", "OFFSET"]]) {
|
|
5329
|
+
if (node[key]) text += ` ${keyword} ${this.expr(obj(node[key]), scope).text}`;
|
|
5330
|
+
}
|
|
5331
|
+
this.depth--;
|
|
5332
|
+
return { text, columns: resolveUnknown ? columns.map((c) => c.type === "unknown" ? { ...c, type: "text" } : c) : columns };
|
|
5333
|
+
}
|
|
5334
|
+
union(node, scope) {
|
|
5335
|
+
for (const key of ["targetList", "fromClause", "whereClause", "groupClause", "havingClause"]) {
|
|
5336
|
+
if (node[key]) fail3("unsupported UNION clause");
|
|
5337
|
+
}
|
|
5338
|
+
const left = this.select({ SelectStmt: obj(node.larg) }, scope, false);
|
|
5339
|
+
const right = this.select({ SelectStmt: obj(node.rarg) }, scope, false);
|
|
5340
|
+
if (left.columns.length !== right.columns.length) fail3("UNION branches must have the same column count");
|
|
5341
|
+
const numeric = ["integer", "bigint", "numeric", "double"];
|
|
5342
|
+
const columns = left.columns.map((column, index) => {
|
|
5343
|
+
const other = right.columns[index];
|
|
5344
|
+
const type = column.type === "unknown" && other.type === "unknown" ? "text" : column.type === "unknown" ? other.type : other.type === "unknown" ? column.type : column.type === other.type ? column.type : numeric.includes(column.type) && numeric.includes(other.type) ? numeric[Math.max(numeric.indexOf(column.type), numeric.indexOf(other.type))] : fail3("UNION columns require compatible types; use explicit casts");
|
|
5345
|
+
return { name: column.name, type, nullable: column.nullable || other.nullable };
|
|
5346
|
+
});
|
|
5347
|
+
return { text: `(${left.text}) UNION${node.all ? " ALL" : ""} (${right.text})`, columns };
|
|
5348
|
+
}
|
|
5349
|
+
projection(node, scope) {
|
|
5350
|
+
const from = list(node.fromClause).map((n) => this.from(obj(n), scope)).join(", ");
|
|
5351
|
+
const columns = [];
|
|
5352
|
+
const targets = [];
|
|
5353
|
+
for (const item of list(node.targetList)) {
|
|
5354
|
+
const target = obj(obj(item).ResTarget);
|
|
5355
|
+
only(target, ["name", "val", "location"]);
|
|
5356
|
+
const value = obj(target.val);
|
|
5357
|
+
const fields = value.ColumnRef ? list(obj(value.ColumnRef).fields) : [];
|
|
5358
|
+
if (fields.length && obj(fields[fields.length - 1]).A_Star) {
|
|
5359
|
+
const matches = fields.length === 1 ? scope.relations : scope.relations.filter((r) => r.alias === strings(fields.slice(0, -1))[0]);
|
|
5360
|
+
if (!matches.length || fields.length > 2 || target.name) fail3("invalid wildcard");
|
|
5361
|
+
for (const relation of matches) for (const column of relation.columns) {
|
|
5362
|
+
columns.push(column);
|
|
5363
|
+
targets.push(`${quoteIdentifier(relation.alias)}.${quoteIdentifier(column.name)}`);
|
|
5364
|
+
}
|
|
5365
|
+
} else {
|
|
5366
|
+
const expression = this.expr(value, scope);
|
|
5367
|
+
const name2 = typeof target.name === "string" && target.name ? target.name : expression.name ?? `column${columns.length + 1}`;
|
|
5368
|
+
columns.push({ name: name2, type: expression.type, nullable: true });
|
|
5369
|
+
targets.push(`${expression.text} AS ${quoteIdentifier(name2)}`);
|
|
5370
|
+
}
|
|
5371
|
+
}
|
|
5372
|
+
if (!targets.length || targets.length > 256) fail3("invalid SQL projection");
|
|
5373
|
+
let text = `SELECT ${targets.join(", ")}${from ? ` FROM ${from}` : ""}`;
|
|
5374
|
+
if (node.whereClause) text += ` WHERE ${this.expr(obj(node.whereClause), scope).text}`;
|
|
5375
|
+
if (list(node.groupClause).length) text += ` GROUP BY ${list(node.groupClause).map((n) => this.expr(obj(n), scope).text).join(", ")}`;
|
|
5376
|
+
if (node.havingClause) text += ` HAVING ${this.expr(obj(node.havingClause), scope).text}`;
|
|
5377
|
+
return { text, columns };
|
|
5378
|
+
}
|
|
5379
|
+
from(node, scope) {
|
|
5380
|
+
this.budget();
|
|
5381
|
+
this.depth++;
|
|
5382
|
+
try {
|
|
5383
|
+
return this.relation(node, scope);
|
|
5384
|
+
} finally {
|
|
5385
|
+
this.depth--;
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
relation(node, scope) {
|
|
5389
|
+
if (node.RangeVar) {
|
|
5390
|
+
const range = obj(node.RangeVar);
|
|
5391
|
+
only(range, ["schemaname", "relname", "inh", "relpersistence", "alias", "location"]);
|
|
5392
|
+
if (range.inh !== true) fail3("ONLY is not supported for ontology objects");
|
|
5393
|
+
const name2 = str(range.relname);
|
|
5394
|
+
const alias = range.alias ? str(obj(range.alias).aliasname) : name2;
|
|
5395
|
+
if (range.alias) only(obj(range.alias), ["aliasname"]);
|
|
5396
|
+
if (scope.relations.some((r) => r.alias === alias)) fail3("duplicate SQL alias");
|
|
5397
|
+
const cte = !range.schemaname ? scope.ctes.get(name2) : void 0;
|
|
5398
|
+
if (cte) {
|
|
5399
|
+
scope.relations.push({ alias, columns: cte.columns });
|
|
5400
|
+
return `${quoteIdentifier(name2)} AS ${quoteIdentifier(alias)}`;
|
|
5401
|
+
}
|
|
5402
|
+
const object = this.input.catalog.objects.find((o) => o.ref === `${range.schemaname}/${name2}`);
|
|
5403
|
+
if (!object) fail3("SQL object is not declared or available");
|
|
5404
|
+
for (const ref of object.read) this.read.add(ref);
|
|
5405
|
+
scope.relations.push({ alias, columns: object.columns });
|
|
5406
|
+
const source = `(WITH ${quoteIdentifier("__orm_scan")} AS MATERIALIZED (${object.scan}) SELECT * FROM ${quoteIdentifier("__orm_scan")})`;
|
|
5407
|
+
return `${source} AS ${quoteIdentifier(alias)}`;
|
|
5408
|
+
}
|
|
5409
|
+
if (node.JoinExpr) {
|
|
5410
|
+
const join = obj(node.JoinExpr);
|
|
5411
|
+
only(join, ["jointype", "larg", "rarg", "quals"]);
|
|
5412
|
+
if (!["JOIN_INNER", "JOIN_LEFT", "JOIN_RIGHT", "JOIN_FULL"].includes(str(join.jointype))) fail3("unsupported SQL join");
|
|
5413
|
+
const start = scope.relations.length;
|
|
5414
|
+
const left = this.from(obj(join.larg), scope);
|
|
5415
|
+
const middle = scope.relations.length;
|
|
5416
|
+
const right = this.from(obj(join.rarg), scope);
|
|
5417
|
+
const nullable = (relations) => {
|
|
5418
|
+
for (const relation of relations) relation.columns = relation.columns.map((c) => ({ ...c, nullable: true }));
|
|
5419
|
+
};
|
|
5420
|
+
if (["JOIN_RIGHT", "JOIN_FULL"].includes(str(join.jointype))) nullable(scope.relations.slice(start, middle));
|
|
5421
|
+
if (["JOIN_LEFT", "JOIN_FULL"].includes(str(join.jointype))) nullable(scope.relations.slice(middle));
|
|
5422
|
+
const kind = str(join.jointype).slice("JOIN_".length);
|
|
5423
|
+
return `(${left} ${kind} JOIN ${right} ON ${join.quals ? this.expr(obj(join.quals), scope).text : "TRUE"})`;
|
|
5424
|
+
}
|
|
5425
|
+
if (node.RangeSubselect) {
|
|
5426
|
+
const range = obj(node.RangeSubselect);
|
|
5427
|
+
only(range, ["subquery", "alias"]);
|
|
5428
|
+
const alias = str(obj(range.alias).aliasname);
|
|
5429
|
+
only(obj(range.alias), ["aliasname"]);
|
|
5430
|
+
if (scope.relations.some((r) => r.alias === alias)) fail3("duplicate SQL alias");
|
|
5431
|
+
const result = this.select(obj(range.subquery), { relations: [], ctes: scope.ctes });
|
|
5432
|
+
scope.relations.push({ alias, columns: result.columns });
|
|
5433
|
+
return `(${result.text}) AS ${quoteIdentifier(alias)}`;
|
|
5434
|
+
}
|
|
5435
|
+
return fail3("unsupported SQL relation");
|
|
5436
|
+
}
|
|
5437
|
+
column(fields, scope) {
|
|
5438
|
+
if (fields.length !== 1 && fields.length !== 2) fail3("unsupported column reference");
|
|
5439
|
+
const name2 = fields[fields.length - 1];
|
|
5440
|
+
const matches = scope.relations.flatMap((r) => fields.length === 2 && r.alias !== fields[0] ? [] : r.columns.filter((c) => c.name === name2).map((c) => ({ r, c })));
|
|
5441
|
+
if (matches.length > 1) fail3("ambiguous SQL column");
|
|
5442
|
+
if (matches.length === 1) return { text: `${quoteIdentifier(matches[0].r.alias)}.${quoteIdentifier(name2)}`, type: matches[0].c.type, name: name2 };
|
|
5443
|
+
if (scope.parent && !(fields.length === 2 && scope.relations.some((r) => r.alias === fields[0]))) return this.column(fields, scope.parent);
|
|
5444
|
+
return fail3("unknown ontology property");
|
|
5445
|
+
}
|
|
5446
|
+
expr(node, scope) {
|
|
5447
|
+
this.budget();
|
|
5448
|
+
this.depth++;
|
|
5449
|
+
try {
|
|
5450
|
+
return this.expression(node, scope);
|
|
5451
|
+
} finally {
|
|
5452
|
+
this.depth--;
|
|
5453
|
+
}
|
|
5454
|
+
}
|
|
5455
|
+
expression(node, scope) {
|
|
5456
|
+
if (node.ColumnRef) return this.column(strings(obj(node.ColumnRef).fields), scope);
|
|
5457
|
+
if (node.ParamRef) {
|
|
5458
|
+
const number = obj(node.ParamRef).number;
|
|
5459
|
+
if (typeof number !== "number" || !Number.isInteger(number) || number < 1 || number > this.input.parameterTypes.length) fail3("undeclared SQL parameter");
|
|
5460
|
+
this.used.add(number);
|
|
5461
|
+
const type = this.input.parameterTypes[number - 1];
|
|
5462
|
+
return { text: `$${number}::${pgType(type)}`, type };
|
|
5463
|
+
}
|
|
5464
|
+
if (node.A_Const) {
|
|
5465
|
+
const literal = obj(node.A_Const);
|
|
5466
|
+
if (literal.isnull) return { text: "NULL", type: "unknown" };
|
|
5467
|
+
if (literal.sval) return { text: quoteLiteral(str(obj(literal.sval).sval)), type: "unknown" };
|
|
5468
|
+
if (literal.boolval) return { text: obj(literal.boolval).boolval ? "TRUE" : "FALSE", type: "boolean" };
|
|
5469
|
+
const value = literal.ival ? obj(literal.ival).ival : obj(literal.fval).fval;
|
|
5470
|
+
if (!/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(String(value))) fail3("unsupported SQL constant");
|
|
5471
|
+
return { text: String(value), type: literal.ival ? "integer" : "numeric" };
|
|
5472
|
+
}
|
|
5473
|
+
if (node.A_Expr) {
|
|
5474
|
+
const value = obj(node.A_Expr);
|
|
5475
|
+
only(value, ["kind", "name", "lexpr", "rexpr", "location"]);
|
|
5476
|
+
const names = strings(value.name);
|
|
5477
|
+
if (names.length !== 1 || value.kind !== "AEXPR_OP") fail3("unsupported SQL operator");
|
|
5478
|
+
const operator = names[0];
|
|
5479
|
+
if (!["=", "<>", "!=", "<", ">", "<=", ">=", "+", "-", "*", "/"].includes(operator)) fail3("unsupported SQL operator");
|
|
5480
|
+
const left = this.expr(obj(value.lexpr), scope), right = this.expr(obj(value.rexpr), scope);
|
|
5481
|
+
const numericTypes = ["integer", "bigint", "numeric", "double"];
|
|
5482
|
+
const arithmetic = ["+", "-", "*", "/"].includes(operator);
|
|
5483
|
+
if (arithmetic && (!numericTypes.includes(left.type) || !numericTypes.includes(right.type))) fail3("arithmetic requires numeric operands");
|
|
5484
|
+
const type = arithmetic ? numericTypes[Math.max(numericTypes.indexOf(left.type), numericTypes.indexOf(right.type))] : "boolean";
|
|
5485
|
+
return { text: `(${left.text} OPERATOR(pg_catalog.${operator}) ${right.text})`, type };
|
|
5486
|
+
}
|
|
5487
|
+
if (node.BoolExpr) {
|
|
5488
|
+
const value = obj(node.BoolExpr);
|
|
5489
|
+
const args = list(value.args).map((a) => this.expr(obj(a), scope).text);
|
|
5490
|
+
if (value.boolop === "NOT_EXPR" && args.length === 1) return { text: `(NOT ${args[0]})`, type: "boolean" };
|
|
5491
|
+
if (!["AND_EXPR", "OR_EXPR"].includes(str(value.boolop))) fail3("unsupported boolean expression");
|
|
5492
|
+
return { text: `(${args.join(value.boolop === "AND_EXPR" ? " AND " : " OR ")})`, type: "boolean" };
|
|
5493
|
+
}
|
|
5494
|
+
if (node.NullTest) {
|
|
5495
|
+
const value = obj(node.NullTest);
|
|
5496
|
+
only(value, ["arg", "nulltesttype", "location"]);
|
|
5497
|
+
if (!["IS_NULL", "IS_NOT_NULL"].includes(str(value.nulltesttype))) fail3("invalid null comparison");
|
|
5498
|
+
return { text: `(${this.expr(obj(value.arg), scope).text} IS ${value.nulltesttype === "IS_NOT_NULL" ? "NOT " : ""}NULL)`, type: "boolean" };
|
|
5499
|
+
}
|
|
5500
|
+
if (node.SubLink) {
|
|
5501
|
+
const value = obj(node.SubLink);
|
|
5502
|
+
only(value, ["subLinkType", "subselect", "location"]);
|
|
5503
|
+
if (value.subLinkType !== "EXISTS_SUBLINK") fail3("only EXISTS subqueries are supported");
|
|
5504
|
+
return { text: `EXISTS (${this.select(obj(value.subselect), scope).text})`, type: "boolean" };
|
|
5505
|
+
}
|
|
5506
|
+
if (node.TypeCast) {
|
|
5507
|
+
const value = obj(node.TypeCast), typeName2 = obj(value.typeName);
|
|
5508
|
+
only(typeName2, ["names", "typemod", "location"]);
|
|
5509
|
+
const names = strings(typeName2.names);
|
|
5510
|
+
if (names.length > 2 || names.length === 2 && names[0] !== "pg_catalog") fail3("unsupported SQL cast");
|
|
5511
|
+
const aliases = { int4: "integer", int8: "bigint", float8: "double", bool: "boolean" };
|
|
5512
|
+
const name2 = names[names.length - 1], type = aliases[name2] ?? name2;
|
|
5513
|
+
return { text: `(${this.expr(obj(value.arg), scope).text})::${pgType(type)}`, type };
|
|
5514
|
+
}
|
|
5515
|
+
if (node.FuncCall) {
|
|
5516
|
+
const value = obj(node.FuncCall);
|
|
5517
|
+
only(value, ["funcname", "args", "agg_star", "agg_distinct", "funcformat", "location"]);
|
|
5518
|
+
const names = strings(value.funcname);
|
|
5519
|
+
if (names.length !== 1) fail3("SQL function qualification is not supported");
|
|
5520
|
+
const name2 = names[0];
|
|
5521
|
+
if (!["count", "sum", "avg", "min", "max", "lower", "upper", "length"].includes(name2)) fail3("SQL function is not allowed");
|
|
5522
|
+
const args = list(value.args).map((a) => this.expr(obj(a), scope));
|
|
5523
|
+
if (value.agg_star && (name2 !== "count" || args.length) || !value.agg_star && args.length !== 1) fail3("unsupported SQL function arguments");
|
|
5524
|
+
const argType = args[0]?.type ?? "integer";
|
|
5525
|
+
if (["lower", "upper", "length"].includes(name2) && !["text", "unknown"].includes(argType)) fail3("text function requires text");
|
|
5526
|
+
if (["sum", "avg"].includes(name2) && !["integer", "bigint", "numeric", "double"].includes(argType)) fail3("aggregate requires numeric input");
|
|
5527
|
+
const type = name2 === "count" ? "bigint" : name2 === "length" ? "integer" : ["lower", "upper"].includes(name2) ? "text" : name2 === "avg" ? argType === "double" ? "double" : "numeric" : name2 === "sum" ? argType === "integer" ? "bigint" : argType === "bigint" ? "numeric" : argType : argType;
|
|
5528
|
+
return { text: `pg_catalog.${name2}(${value.agg_distinct ? "DISTINCT " : ""}${value.agg_star ? "*" : args.map((a) => a.text).join(", ")})`, type, name: name2 };
|
|
5529
|
+
}
|
|
5530
|
+
return fail3(`unsupported SQL expression: ${Object.keys(node).join(",")}`);
|
|
5531
|
+
}
|
|
5532
|
+
};
|
|
5533
|
+
var sqlScalarSchema = zod.z.enum(["text", "boolean", "integer", "bigint", "numeric", "double", "uuid", "date", "timestamptz"]);
|
|
5534
|
+
var name = zod.z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).max(63).refine((value) => !["__proto__", "prototype", "constructor"].includes(value), "reserved name");
|
|
5535
|
+
var base = zod.z.object({
|
|
5536
|
+
name,
|
|
5537
|
+
namespace: name,
|
|
5538
|
+
language: zod.z.literal("sql"),
|
|
5539
|
+
kind: zod.z.literal("query"),
|
|
5540
|
+
requires: zod.z.object({ capability: zod.z.string().min(1).max(128) }).strict(),
|
|
5541
|
+
ontology: zod.z.object({ read: zod.z.array(zod.z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*\/[A-Za-z_][A-Za-z0-9_]*$/)).min(1).max(256) }).strict(),
|
|
5542
|
+
maxRows: zod.z.number().int().min(1).max(1e4).default(1e3)
|
|
5543
|
+
});
|
|
5544
|
+
var fixed = base.extend({
|
|
5545
|
+
parameters: zod.z.record(name, zod.z.object({ type: sqlScalarSchema, required: zod.z.boolean().default(true) }).strict()).default({}),
|
|
5546
|
+
sql: zod.z.string().min(1).max(65536)
|
|
5547
|
+
}).strict();
|
|
5548
|
+
var runtime = base.extend({ mode: zod.z.literal("runtime") }).strict();
|
|
5549
|
+
var sqlOperationDefinitionSchema = zod.z.union([fixed, runtime]);
|
|
5550
|
+
function defineOperation(input) {
|
|
5551
|
+
return sqlOperationDefinitionSchema.parse(input);
|
|
5552
|
+
}
|
|
5553
|
+
|
|
5554
|
+
// src/sql/parameters.ts
|
|
5555
|
+
function bindNamedParameters({ sql, names }) {
|
|
5556
|
+
let result = "", i = 0;
|
|
5557
|
+
while (i < sql.length) {
|
|
5558
|
+
const start = i;
|
|
5559
|
+
if (sql.startsWith("--", i)) {
|
|
5560
|
+
i = sql.indexOf("\n", i);
|
|
5561
|
+
if (i < 0) i = sql.length;
|
|
5562
|
+
} else if (sql.startsWith("/*", i)) {
|
|
5563
|
+
i += 2;
|
|
5564
|
+
let depth = 1;
|
|
5565
|
+
while (i < sql.length && depth) {
|
|
5566
|
+
if (sql.startsWith("/*", i)) {
|
|
5567
|
+
depth++;
|
|
5568
|
+
i += 2;
|
|
5569
|
+
} else if (sql.startsWith("*/", i)) {
|
|
5570
|
+
depth--;
|
|
5571
|
+
i += 2;
|
|
5572
|
+
} else i++;
|
|
5573
|
+
}
|
|
5574
|
+
if (depth) throw new Error("unterminated SQL comment");
|
|
5575
|
+
} else if (sql[i] === "'" || sql[i] === '"') {
|
|
5576
|
+
const escapes = sql[i] === "'" && /[eE]/.test(sql[i - 1] ?? "") && !/[A-Za-z0-9_$]/.test(sql[i - 2] ?? "");
|
|
5577
|
+
const quote = sql[i++];
|
|
5578
|
+
while (i < sql.length) {
|
|
5579
|
+
if (sql[i] === quote) {
|
|
5580
|
+
if (sql[i + 1] === quote) i += 2;
|
|
5581
|
+
else {
|
|
5582
|
+
i++;
|
|
5583
|
+
break;
|
|
5584
|
+
}
|
|
5585
|
+
} else if (escapes && sql[i] === "\\") i += 2;
|
|
5586
|
+
else i++;
|
|
5587
|
+
}
|
|
5588
|
+
} else if (sql[i] === "$" && /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/.test(sql.slice(i))) {
|
|
5589
|
+
const marker = /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sql.slice(i))[0];
|
|
5590
|
+
const end = sql.indexOf(marker, i + marker.length);
|
|
5591
|
+
if (end < 0) throw new Error("unterminated SQL string");
|
|
5592
|
+
i = end + marker.length;
|
|
5593
|
+
} else if (sql[i] === ":" && sql[i - 1] !== ":" && sql[i + 1] !== ":") {
|
|
5594
|
+
const name2 = /^[A-Za-z_][A-Za-z0-9_]*/.exec(sql.slice(i + 1))?.[0];
|
|
5595
|
+
if (!name2 || !names.includes(name2)) throw new Error("undeclared named SQL parameter");
|
|
5596
|
+
result += `$${names.indexOf(name2) + 1}`;
|
|
5597
|
+
i += name2.length + 1;
|
|
5598
|
+
continue;
|
|
5599
|
+
} else i++;
|
|
5600
|
+
result += sql.slice(start, i);
|
|
5601
|
+
}
|
|
5602
|
+
return result;
|
|
5603
|
+
}
|
|
5604
|
+
|
|
5605
|
+
// src/sql/operations.ts
|
|
5606
|
+
function isSqlOperationDocument(path, contents) {
|
|
5607
|
+
if (path.endsWith(".operation.json")) return true;
|
|
5608
|
+
if (!contents?.trimStart().startsWith("{")) return false;
|
|
5609
|
+
try {
|
|
5610
|
+
const value = JSON.parse(contents);
|
|
5611
|
+
return !!value && typeof value === "object" && "language" in value && value.language === "sql";
|
|
5612
|
+
} catch {
|
|
5613
|
+
return false;
|
|
5614
|
+
}
|
|
5615
|
+
}
|
|
5616
|
+
async function compileOperationDocuments(input) {
|
|
5617
|
+
const sqlDocuments = input.documents.filter((d) => isSqlOperationDocument(d.path, d.contents));
|
|
5618
|
+
const result = compileNamespace({ ...input, documents: input.documents.filter((d) => !isSqlOperationDocument(d.path, d.contents)) });
|
|
5619
|
+
if (!result.ok || !sqlDocuments.length) return result;
|
|
5620
|
+
for (const document of sqlDocuments) {
|
|
5621
|
+
try {
|
|
5622
|
+
const definition = defineOperation(JSON.parse(document.contents));
|
|
5623
|
+
if (definition.namespace !== input.canonical.namespace) throw new Error("SQL operation namespace does not match its owner");
|
|
5624
|
+
if (result.artifact.operations[definition.name]) throw new Error("duplicate operation name");
|
|
5625
|
+
const roles = input.canonical.capabilities.roles;
|
|
5626
|
+
const localCapabilities = roles && typeof roles === "object" && "capabilities" in roles ? roles.capabilities : void 0;
|
|
5627
|
+
const declaredLocally = localCapabilities && typeof localCapabilities === "object" && Object.hasOwn(localCapabilities, definition.requires.capability);
|
|
5628
|
+
if (!declaredLocally && !input.canonical.capabilities.uses.includes(definition.requires.capability)) throw new Error("SQL operation capability is not declared by its namespace");
|
|
5629
|
+
const read = [...definition.ontology.read].sort();
|
|
5630
|
+
const catalog = createSqlCatalog({ canonical: input.canonical, publishedObjects: read, executionTarget: "registered-operation" });
|
|
5631
|
+
if ("mode" in definition) {
|
|
5632
|
+
if (catalog.objects.some((object) => object.read.some((ref) => !read.includes(ref)))) throw new Error("SQL policy accesses an undeclared ontology object");
|
|
5633
|
+
const opHash2 = sqlHash(JSON.stringify([
|
|
5634
|
+
"sequence-runtime-sql-operation-v1",
|
|
5635
|
+
definition.name,
|
|
5636
|
+
definition.namespace,
|
|
5637
|
+
definition.requires.capability,
|
|
5638
|
+
read,
|
|
5639
|
+
catalog.identity,
|
|
5640
|
+
definition.maxRows
|
|
5641
|
+
]));
|
|
5642
|
+
result.artifact.operations[definition.name] = compiledOperationSchema.parse({
|
|
5643
|
+
irVersion: RUNTIME_SQL_IR_VERSION,
|
|
5644
|
+
language: "sql",
|
|
5645
|
+
name: definition.name,
|
|
5646
|
+
opHash: opHash2,
|
|
5647
|
+
kind: "query",
|
|
5648
|
+
requires: definition.requires,
|
|
5649
|
+
variables: [
|
|
5650
|
+
{ name: "sql", type: { kind: "scalar", scalar: "text" }, nullable: false, list: false },
|
|
5651
|
+
{ name: "parameters", type: { kind: "scalar", scalar: "jsonb" }, nullable: true, list: false }
|
|
5652
|
+
],
|
|
5653
|
+
roots: [],
|
|
5654
|
+
tableSet: read.map((ref) => ref.split("/")[1]).sort(),
|
|
5655
|
+
sqlQuery: { catalog, read, maxRows: definition.maxRows }
|
|
5656
|
+
});
|
|
5657
|
+
result.manifest.operations[definition.name] = { opHash: opHash2, kind: "query", requires: definition.requires };
|
|
5658
|
+
continue;
|
|
5659
|
+
}
|
|
5660
|
+
const names = Object.keys(definition.parameters).sort();
|
|
5661
|
+
if (names.length > 128) throw new Error("too many SQL parameters");
|
|
5662
|
+
const plan = await compileOntologySql({
|
|
5663
|
+
catalog,
|
|
5664
|
+
sql: bindNamedParameters({ sql: definition.sql, names }),
|
|
5665
|
+
parameterTypes: names.map((name2) => definition.parameters[name2].type),
|
|
5666
|
+
maxRows: definition.maxRows
|
|
5667
|
+
});
|
|
5668
|
+
if (plan.read.some((ref) => !read.includes(ref))) throw new Error("SQL policy/query accesses an undeclared ontology object");
|
|
5669
|
+
const variables = names.map((name2) => ({
|
|
5670
|
+
name: name2,
|
|
5671
|
+
type: { kind: "scalar", scalar: definition.parameters[name2].type },
|
|
5672
|
+
nullable: !definition.parameters[name2].required,
|
|
5673
|
+
list: false
|
|
5674
|
+
}));
|
|
5675
|
+
const opHash = sqlHash(JSON.stringify([
|
|
5676
|
+
"sequence-sql-operation-v1",
|
|
5677
|
+
definition.name,
|
|
5678
|
+
definition.namespace,
|
|
5679
|
+
definition.requires.capability,
|
|
5680
|
+
read,
|
|
5681
|
+
variables,
|
|
5682
|
+
plan.queryHash
|
|
5683
|
+
]));
|
|
5684
|
+
const operation = compiledOperationSchema.parse({
|
|
5685
|
+
irVersion: SQL_IR_VERSION,
|
|
5686
|
+
language: "sql",
|
|
5687
|
+
name: definition.name,
|
|
5688
|
+
opHash,
|
|
5689
|
+
kind: "query",
|
|
5690
|
+
requires: definition.requires,
|
|
5691
|
+
variables,
|
|
5692
|
+
roots: [],
|
|
5693
|
+
tableSet: plan.read.map((ref) => ref.split("/")[1]).sort(),
|
|
5694
|
+
sql: { text: plan.text, columns: plan.columns, parameters: names, read, maxRows: plan.maxRows }
|
|
5695
|
+
});
|
|
5696
|
+
result.artifact.operations[definition.name] = operation;
|
|
5697
|
+
result.manifest.operations[definition.name] = { opHash, kind: "query", requires: definition.requires };
|
|
5698
|
+
} catch (error) {
|
|
5699
|
+
return { ok: false, diagnostics: [...result.diagnostics, {
|
|
5700
|
+
severity: "error",
|
|
5701
|
+
code: "invalid-sql-operation",
|
|
5702
|
+
document: document.path,
|
|
5703
|
+
message: error instanceof Error ? error.message : "invalid SQL operation"
|
|
5704
|
+
}] };
|
|
5705
|
+
}
|
|
5706
|
+
}
|
|
5707
|
+
const required = requiredArtifactIrVersion(Object.values(result.artifact.operations));
|
|
5708
|
+
if (required > result.artifact.irVersion) result.artifact.irVersion = required;
|
|
5709
|
+
result.artifact.opsHash = computeOpsHash(Object.values(result.artifact.operations).map((op) => op.opHash));
|
|
5710
|
+
result.manifest.opsHash = result.artifact.opsHash;
|
|
5711
|
+
return { ...result, artifact: normalizeIr(result.artifact) };
|
|
5712
|
+
}
|
|
5713
|
+
var parameterName = zod.z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).max(63).refine((value) => !["__proto__", "prototype", "constructor"].includes(value), "reserved parameter name");
|
|
5714
|
+
var parameter = zod.z.discriminatedUnion("type", [
|
|
5715
|
+
zod.z.object({ type: zod.z.literal("text"), value: zod.z.string().nullable() }).strict(),
|
|
5716
|
+
zod.z.object({ type: zod.z.literal("uuid"), value: zod.z.guid().nullable() }).strict(),
|
|
5717
|
+
zod.z.object({ type: zod.z.literal("date"), value: zod.z.string().nullable() }).strict(),
|
|
5718
|
+
zod.z.object({ type: zod.z.literal("timestamptz"), value: zod.z.string().nullable() }).strict(),
|
|
5719
|
+
zod.z.object({ type: zod.z.literal("numeric"), value: zod.z.string().regex(/^-?\d+(\.\d+)?$/).nullable() }).strict(),
|
|
5720
|
+
zod.z.object({ type: zod.z.literal("bigint"), value: zod.z.string().regex(/^-?\d+$/).nullable() }).strict(),
|
|
5721
|
+
zod.z.object({ type: zod.z.literal("integer"), value: zod.z.number().int().min(-2147483648).max(2147483647).nullable() }).strict(),
|
|
5722
|
+
zod.z.object({ type: zod.z.literal("double"), value: zod.z.number().nullable() }).strict(),
|
|
5723
|
+
zod.z.object({ type: zod.z.literal("boolean"), value: zod.z.boolean().nullable() }).strict()
|
|
5724
|
+
]);
|
|
5725
|
+
var sqlQueryInputSchema = zod.z.object({
|
|
5726
|
+
sql: zod.z.string().min(1).max(65536).refine((value) => Buffer.byteLength(value) <= 65536, "SQL exceeds 64 KiB"),
|
|
5727
|
+
parameters: zod.z.record(parameterName, parameter).refine((value) => Object.keys(value).length <= 128, "too many SQL parameters").default({})
|
|
5728
|
+
}).strict();
|
|
5729
|
+
async function compileRuntimeSql({ operation, input }) {
|
|
5730
|
+
const contract = operation.sqlQuery;
|
|
5731
|
+
if (!contract) throw new Error("missing runtime SQL contract");
|
|
5732
|
+
const names = Object.keys(input.parameters).sort();
|
|
5733
|
+
const plan = await compileOntologySql({
|
|
5734
|
+
catalog: contract.catalog,
|
|
5735
|
+
sql: bindNamedParameters({ sql: input.sql, names }),
|
|
5736
|
+
parameterTypes: names.map((name2) => input.parameters[name2].type),
|
|
5737
|
+
maxRows: contract.maxRows
|
|
5738
|
+
});
|
|
5739
|
+
if (plan.read.some((ref) => !contract.read.includes(ref))) throw new Error("SQL accesses an undeclared ontology object");
|
|
5740
|
+
return compiledOperationSchema.parse({
|
|
5741
|
+
...operation,
|
|
5742
|
+
sqlQuery: void 0,
|
|
5743
|
+
irVersion: SQL_IR_VERSION,
|
|
5744
|
+
variables: names.map((name2) => ({ name: name2, type: { kind: "scalar", scalar: input.parameters[name2].type }, nullable: true, list: false })),
|
|
5745
|
+
tableSet: plan.read.map((ref) => ref.split("/")[1]).sort(),
|
|
5746
|
+
sql: { text: plan.text, columns: plan.columns, parameters: names, read: plan.read, maxRows: plan.maxRows }
|
|
5747
|
+
});
|
|
5748
|
+
}
|
|
5749
|
+
var DATASET_PREVIEW_LIMIT = 100;
|
|
5750
|
+
function compileDatasetPreviews(input) {
|
|
5751
|
+
const canonical = normalizeCanonical(input);
|
|
5752
|
+
const { ctx, diagnostics } = buildCompileContext(canonical);
|
|
5753
|
+
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) throw new NamespaceCompileError(diagnostics);
|
|
5754
|
+
const schemaHash = computeSchemaHash(canonical);
|
|
5755
|
+
const operations = {};
|
|
5756
|
+
for (const relation of canonicalRelations(canonical)) {
|
|
5757
|
+
if (relation.internal || relation.source !== void 0) continue;
|
|
5758
|
+
const columns = relation.columns.filter((column) => column.cell === void 0);
|
|
5759
|
+
const { roots, tableSet } = injectPolicies({ ctx, roots: [{
|
|
5760
|
+
kind: "select",
|
|
5761
|
+
table: relation.table,
|
|
5762
|
+
alias: "rows",
|
|
5763
|
+
where: { kind: "const", value: true },
|
|
5764
|
+
orderBy: [],
|
|
5765
|
+
limit: { value: DATASET_PREVIEW_LIMIT, default: DATASET_PREVIEW_LIMIT, max: DATASET_PREVIEW_LIMIT },
|
|
5766
|
+
selection: {
|
|
5767
|
+
typename: ctx.tables[relation.table].typename,
|
|
5768
|
+
columns: columns.map((column) => ({ column: column.name, alias: column.name })),
|
|
5769
|
+
relations: [],
|
|
5770
|
+
aggregates: []
|
|
5771
|
+
}
|
|
5772
|
+
}] });
|
|
5773
|
+
for (const table of tableSet) {
|
|
5774
|
+
const meta = ctx.tables[table];
|
|
5775
|
+
if (meta.namespace && meta.namespace !== ctx.namespace && !meta.exportRead) {
|
|
5776
|
+
throw new DocumentCompileError(`object '${meta.namespace}/${table}' is not exported for reads`, "unexported-object");
|
|
5777
|
+
}
|
|
5778
|
+
}
|
|
5779
|
+
const hash = crypto.createHash("sha256").update("sequence-orm-dataset-preview-v1\0").update(stableJson({ schemaHash, roots, tableSet })).digest("hex");
|
|
5780
|
+
operations[relation.table] = compiledOperationSchema.parse({
|
|
5781
|
+
irVersion: canonical.imports?.length ? COMPOSED_IR_VERSION : LEGACY_IR_VERSION,
|
|
5782
|
+
name: `DatasetPreview_${relation.table}`,
|
|
5783
|
+
opHash: `sha256:${hash}`,
|
|
5784
|
+
kind: "query",
|
|
5785
|
+
requires: null,
|
|
5786
|
+
variables: [],
|
|
5787
|
+
tableSet,
|
|
5788
|
+
roots
|
|
5789
|
+
});
|
|
5790
|
+
}
|
|
5791
|
+
return operations;
|
|
5792
|
+
}
|
|
5793
|
+
|
|
3788
5794
|
exports.COMPILER_MAJOR = COMPILER_MAJOR;
|
|
3789
5795
|
exports.CONST_TRUE = CONST_TRUE;
|
|
5796
|
+
exports.DATASET_PREVIEW_LIMIT = DATASET_PREVIEW_LIMIT;
|
|
3790
5797
|
exports.DEFAULT_LIMIT = DEFAULT_LIMIT;
|
|
3791
5798
|
exports.DocumentCompileError = DocumentCompileError;
|
|
3792
5799
|
exports.MAX_LIMIT = MAX_LIMIT;
|
|
@@ -3808,18 +5815,23 @@ exports.camelCase = camelCase;
|
|
|
3808
5815
|
exports.canonicalOperationText = canonicalOperationText;
|
|
3809
5816
|
exports.classifyInputType = classifyInputType;
|
|
3810
5817
|
exports.collectPinnedColumns = collectPinnedColumns;
|
|
5818
|
+
exports.compileDatasetPreviews = compileDatasetPreviews;
|
|
3811
5819
|
exports.compileNamespace = compileNamespace;
|
|
3812
5820
|
exports.compileNamespaceOrThrow = compileNamespaceOrThrow;
|
|
5821
|
+
exports.compileOperationDocuments = compileOperationDocuments;
|
|
3813
5822
|
exports.compilePolicy = compilePolicy;
|
|
3814
5823
|
exports.compilePredicateV2 = compilePredicateV2;
|
|
5824
|
+
exports.compileRuntimeSql = compileRuntimeSql;
|
|
3815
5825
|
exports.computeOpHash = computeOpHash;
|
|
3816
5826
|
exports.computeOpsHash = computeOpsHash;
|
|
3817
5827
|
exports.computePinnedColumnsByTable = computePinnedColumnsByTable;
|
|
3818
5828
|
exports.computeSchemaHash = computeSchemaHash;
|
|
5829
|
+
exports.defineOperation = defineOperation;
|
|
3819
5830
|
exports.enumTypeName = enumTypeName;
|
|
3820
5831
|
exports.flattenOperation = flattenOperation;
|
|
3821
5832
|
exports.injectPolicies = injectPolicies;
|
|
3822
5833
|
exports.inputTypeName = inputTypeName;
|
|
5834
|
+
exports.isSqlOperationDocument = isSqlOperationDocument;
|
|
3823
5835
|
exports.lowerOperation = lowerOperation;
|
|
3824
5836
|
exports.mutationRootNames = mutationRootNames;
|
|
3825
5837
|
exports.parseDocuments = parseDocuments;
|
|
@@ -3831,6 +5843,8 @@ exports.predContains = predContains;
|
|
|
3831
5843
|
exports.predContainsExists = predContainsExists;
|
|
3832
5844
|
exports.queryRootNames = queryRootNames;
|
|
3833
5845
|
exports.restrictOperation = restrictOperation;
|
|
5846
|
+
exports.sqlOperationDefinitionSchema = sqlOperationDefinitionSchema;
|
|
5847
|
+
exports.sqlQueryInputSchema = sqlQueryInputSchema;
|
|
3834
5848
|
exports.typeNameForTable = typeNameForTable;
|
|
3835
5849
|
exports.validateAppendInput = validateAppendInput;
|
|
3836
5850
|
exports.validateBoolExpInput = validateBoolExpInput;
|