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