@sequenceholdings/orm 2.1.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -1
- package/dist/authoring.cjs +1358 -45
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +223 -2
- package/dist/authoring.d.ts +223 -2
- package/dist/authoring.js +1349 -47
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +662 -38
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +696 -53
- package/dist/canonical.d.ts +696 -53
- package/dist/canonical.js +647 -39
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +1808 -247
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1809 -248
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs +90 -25
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +39 -22
- package/dist/client.d.ts +39 -22
- package/dist/client.js +87 -26
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +1325 -285
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +23 -2
- package/dist/compiler.d.ts +23 -2
- package/dist/compiler.js +1325 -285
- package/dist/compiler.js.map +1 -1
- package/dist/constants-YoFGjIBM.d.cts +99 -0
- package/dist/constants-YoFGjIBM.d.ts +99 -0
- package/dist/dynamic-input.d.cts +1 -0
- package/dist/dynamic-input.d.ts +1 -0
- package/dist/index.cjs +1605 -140
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +230 -113
- package/dist/index.d.ts +230 -113
- package/dist/index.js +1583 -142
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +727 -38
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +539 -21
- package/dist/ir.d.ts +539 -21
- package/dist/ir.js +722 -39
- package/dist/ir.js.map +1 -1
- package/dist/{naming-DCn94LJ4.d.ts → naming-CPfx7EfF.d.ts} +14 -1
- package/dist/{naming-DPglwzkB.d.cts → naming-CU9ThP_E.d.cts} +14 -1
- package/dist/policy-1iZbGnZZ.d.cts +24 -0
- package/dist/policy-1iZbGnZZ.d.ts +24 -0
- package/dist/predicate-v2.cjs +726 -42
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.d.cts +2 -1
- package/dist/predicate-v2.d.ts +2 -1
- package/dist/predicate-v2.js +726 -42
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +858 -54
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.d.cts +7 -3
- package/dist/sdl.d.ts +7 -3
- package/dist/sdl.js +856 -55
- package/dist/sdl.js.map +1 -1
- package/package.json +9 -4
- package/templates/orm-namespace/CLAUDE.md +6 -4
- package/templates/orm-namespace/codegen.preset.d.cts +7 -0
- package/templates/orm-namespace/codegen.ts +2 -1
- package/templates/orm-namespace/graphql/example/queries.ts +4 -3
- package/templates/orm-namespace/package.json +2 -1
- package/templates/orm-namespace/schema/tables.ts +3 -2
- package/templates/orm-namespace/sequence.config.ts +7 -11
package/dist/cli.cjs
CHANGED
|
@@ -40,12 +40,217 @@ var PredicateParseError = class extends Error {
|
|
|
40
40
|
name = "PredicateParseError";
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
+
// src/compiler/sql.ts
|
|
44
|
+
var CLAIMS_SETTING = "request.jwt.claims";
|
|
45
|
+
var CAPABILITIES_CLAIM_KEY = "capabilities";
|
|
46
|
+
var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
47
|
+
var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
48
|
+
var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
|
|
49
|
+
var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
|
|
50
|
+
var capabilityExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
51
|
+
|
|
52
|
+
// src/checks/compile.ts
|
|
53
|
+
function tokenize(source) {
|
|
54
|
+
const tokens = [];
|
|
55
|
+
for (let position = 0; position < source.length; ) {
|
|
56
|
+
const char = source[position];
|
|
57
|
+
if (/\s/.test(char)) {
|
|
58
|
+
position++;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (char === "(" || char === ")" || char === ",") {
|
|
62
|
+
tokens.push({ kind: char === "(" ? "lparen" : char === ")" ? "rparen" : "comma", value: char, position });
|
|
63
|
+
position++;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (char === "'") {
|
|
67
|
+
const start = position++;
|
|
68
|
+
let value = "";
|
|
69
|
+
let closed = false;
|
|
70
|
+
while (position < source.length) {
|
|
71
|
+
if (source[position] !== "'") {
|
|
72
|
+
value += source[position++];
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (source[position + 1] === "'") {
|
|
76
|
+
value += "'";
|
|
77
|
+
position += 2;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
position++;
|
|
81
|
+
closed = true;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
if (!closed) throw new Error(`unterminated string at position ${start}`);
|
|
85
|
+
if (/[\x00-\x1f\x7f]/.test(value)) throw new Error(`control character in string at position ${start}`);
|
|
86
|
+
tokens.push({ kind: "string", value, position: start });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const operator = /^(<=|>=|<>|!=|!~|=|<|>|~)/.exec(source.slice(position));
|
|
90
|
+
if (operator) {
|
|
91
|
+
tokens.push({ kind: "operator", value: operator[0] === "<>" ? "!=" : operator[0], position });
|
|
92
|
+
position += operator[0].length;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const number = /^-?\d+(?:\.\d+)?/.exec(source.slice(position));
|
|
96
|
+
if (number) {
|
|
97
|
+
tokens.push({ kind: "number", value: number[0], position });
|
|
98
|
+
position += number[0].length;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const identifier3 = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(source.slice(position));
|
|
102
|
+
if (identifier3) {
|
|
103
|
+
tokens.push({ kind: "identifier", value: identifier3[0], position });
|
|
104
|
+
position += identifier3[0].length;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
throw new Error(`unexpected character '${char}' at position ${position}`);
|
|
108
|
+
}
|
|
109
|
+
tokens.push({ kind: "end", value: "", position: source.length });
|
|
110
|
+
if (tokens.length > 256) throw new Error("check expression exceeds 256 tokens");
|
|
111
|
+
return tokens;
|
|
112
|
+
}
|
|
113
|
+
var Parser = class {
|
|
114
|
+
constructor(tokens, columns) {
|
|
115
|
+
this.tokens = tokens;
|
|
116
|
+
this.columns = /* @__PURE__ */ new Map();
|
|
117
|
+
for (const column of columns) {
|
|
118
|
+
for (const alias of [column.name, column.columnName]) {
|
|
119
|
+
const previous = this.columns.get(alias);
|
|
120
|
+
if (previous && previous.columnName !== column.columnName) {
|
|
121
|
+
throw new Error(`ambiguous check column '${alias}'`);
|
|
122
|
+
}
|
|
123
|
+
this.columns.set(alias, column);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
index = 0;
|
|
128
|
+
depth = 0;
|
|
129
|
+
columns;
|
|
130
|
+
parse() {
|
|
131
|
+
const expression = this.parseOr();
|
|
132
|
+
this.expect("end", "end of expression");
|
|
133
|
+
return expression;
|
|
134
|
+
}
|
|
135
|
+
parseOr() {
|
|
136
|
+
let left = this.parseAnd();
|
|
137
|
+
while (this.matchKeyword("or")) left = `(${left} OR ${this.parseAnd()})`;
|
|
138
|
+
return left;
|
|
139
|
+
}
|
|
140
|
+
parseAnd() {
|
|
141
|
+
let left = this.parseNot();
|
|
142
|
+
while (this.matchKeyword("and")) left = `(${left} AND ${this.parseNot()})`;
|
|
143
|
+
return left;
|
|
144
|
+
}
|
|
145
|
+
parseNot() {
|
|
146
|
+
if (this.matchKeyword("not")) return `(NOT ${this.parseNot()})`;
|
|
147
|
+
return this.parsePrimary();
|
|
148
|
+
}
|
|
149
|
+
parsePrimary() {
|
|
150
|
+
if (this.peek().kind === "lparen") {
|
|
151
|
+
if (++this.depth > 32) throw new Error("check expression nesting exceeds 32 levels");
|
|
152
|
+
this.advance();
|
|
153
|
+
const expression = this.parseOr();
|
|
154
|
+
this.expect("rparen", "')'");
|
|
155
|
+
this.depth--;
|
|
156
|
+
return `(${expression})`;
|
|
157
|
+
}
|
|
158
|
+
return this.parseComparison();
|
|
159
|
+
}
|
|
160
|
+
parseComparison() {
|
|
161
|
+
const left = this.parseValue();
|
|
162
|
+
if (this.matchKeyword("is")) {
|
|
163
|
+
const negate = this.matchKeyword("not");
|
|
164
|
+
this.expectKeyword("null");
|
|
165
|
+
return `${left.sql} IS ${negate ? "NOT " : ""}NULL`;
|
|
166
|
+
}
|
|
167
|
+
if (this.matchKeyword("between")) {
|
|
168
|
+
const lower = this.parseValue();
|
|
169
|
+
this.expectKeyword("and");
|
|
170
|
+
const upper = this.parseValue();
|
|
171
|
+
return `${left.sql} BETWEEN ${lower.sql} AND ${upper.sql}`;
|
|
172
|
+
}
|
|
173
|
+
if (this.matchKeyword("in")) {
|
|
174
|
+
this.expect("lparen", "'(' after IN");
|
|
175
|
+
const values = [this.parseValue()];
|
|
176
|
+
while (this.peek().kind === "comma") {
|
|
177
|
+
this.advance();
|
|
178
|
+
values.push(this.parseValue());
|
|
179
|
+
}
|
|
180
|
+
this.expect("rparen", "')' after IN values");
|
|
181
|
+
if (values.some((value) => !value.literal)) throw new Error("IN values must be literals");
|
|
182
|
+
return `${left.sql} IN (${values.map((value) => value.sql).join(", ")})`;
|
|
183
|
+
}
|
|
184
|
+
const operator = this.expect("operator", "a comparison operator");
|
|
185
|
+
const right = this.parseValue();
|
|
186
|
+
if (operator.value === "~" || operator.value === "!~") {
|
|
187
|
+
if (left.column && left.column.type !== "text") throw new Error("regex checks require a text column");
|
|
188
|
+
if (!right.literal) throw new Error("regex pattern must be a literal");
|
|
189
|
+
}
|
|
190
|
+
return `${left.sql} ${operator.value} ${right.sql}`;
|
|
191
|
+
}
|
|
192
|
+
parseValue() {
|
|
193
|
+
const token = this.advance();
|
|
194
|
+
if (token.kind === "string") return { sql: quoteLiteral(token.value), literal: true };
|
|
195
|
+
if (token.kind === "number") {
|
|
196
|
+
const number = Number(token.value);
|
|
197
|
+
if (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number)) {
|
|
198
|
+
throw new Error(`unsafe numeric literal at position ${token.position}`);
|
|
199
|
+
}
|
|
200
|
+
return { sql: token.value, literal: true };
|
|
201
|
+
}
|
|
202
|
+
if (token.kind !== "identifier") throw new Error(`expected a column or literal at position ${token.position}`);
|
|
203
|
+
const keyword = token.value.toLowerCase();
|
|
204
|
+
if (keyword === "true" || keyword === "false" || keyword === "null") return { sql: keyword.toUpperCase(), literal: true };
|
|
205
|
+
if (keyword === "btrim") {
|
|
206
|
+
this.expect("lparen", "'(' after btrim");
|
|
207
|
+
const column2 = this.parseColumn();
|
|
208
|
+
this.expect("rparen", "')' after btrim column");
|
|
209
|
+
if (column2.type !== "text") throw new Error("btrim requires a text column");
|
|
210
|
+
return { sql: `btrim(${quoteIdentifier(column2.columnName)})`, column: column2 };
|
|
211
|
+
}
|
|
212
|
+
const column = this.columns.get(token.value);
|
|
213
|
+
if (!column) throw new Error(`unknown check column '${token.value}'`);
|
|
214
|
+
return { sql: quoteIdentifier(column.columnName), column };
|
|
215
|
+
}
|
|
216
|
+
parseColumn() {
|
|
217
|
+
const token = this.expect("identifier", "a column name");
|
|
218
|
+
const column = this.columns.get(token.value);
|
|
219
|
+
if (!column) throw new Error(`unknown check column '${token.value}'`);
|
|
220
|
+
return column;
|
|
221
|
+
}
|
|
222
|
+
peek() {
|
|
223
|
+
return this.tokens[this.index];
|
|
224
|
+
}
|
|
225
|
+
advance() {
|
|
226
|
+
const token = this.peek();
|
|
227
|
+
if (token.kind !== "end") this.index++;
|
|
228
|
+
return token;
|
|
229
|
+
}
|
|
230
|
+
matchKeyword(keyword) {
|
|
231
|
+
if (this.peek().kind !== "identifier" || this.peek().value.toLowerCase() !== keyword) return false;
|
|
232
|
+
this.advance();
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
expectKeyword(keyword) {
|
|
236
|
+
if (!this.matchKeyword(keyword)) throw new Error(`expected '${keyword}' at position ${this.peek().position}`);
|
|
237
|
+
}
|
|
238
|
+
expect(kind, description) {
|
|
239
|
+
const token = this.peek();
|
|
240
|
+
if (token.kind !== kind) throw new Error(`expected ${description} at position ${token.position}`);
|
|
241
|
+
return this.advance();
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
function compileCheckExpression(source, columns) {
|
|
245
|
+
return new Parser(tokenize(source), columns).parse();
|
|
246
|
+
}
|
|
247
|
+
|
|
43
248
|
// src/constants.ts
|
|
44
249
|
var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
|
45
250
|
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
46
251
|
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
47
252
|
var MAX_IDENTIFIER_LENGTH = 63;
|
|
48
|
-
var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
|
|
253
|
+
var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
|
|
49
254
|
var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
50
255
|
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
|
|
51
256
|
var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
|
|
@@ -61,11 +266,291 @@ var MAX_GENERATED_SDL_BYTES = 16 * 1024 * 1024;
|
|
|
61
266
|
var ORM_REGISTRATION_MAX_BYTES = 64 * 1024 * 1024;
|
|
62
267
|
var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
|
|
63
268
|
var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
|
|
269
|
+
var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
|
|
270
|
+
var CELL_PART_SUFFIX = {
|
|
271
|
+
at: "__at",
|
|
272
|
+
by: "__by",
|
|
273
|
+
rev: "__rev",
|
|
274
|
+
source: "__src",
|
|
275
|
+
conflicts: "__lost"
|
|
276
|
+
};
|
|
277
|
+
var MAX_TRACKED_COLUMN_NAME = MAX_IDENTIFIER_LENGTH - 6;
|
|
278
|
+
var CELLS_FIELD = "cells";
|
|
279
|
+
function cellColumnName(column, part) {
|
|
280
|
+
return `${column}${CELL_PART_SUFFIX[part]}`;
|
|
281
|
+
}
|
|
282
|
+
var MATERIALIZE_ACTOR = "orm:materialize";
|
|
283
|
+
var MATERIALIZE_CLAIMS_JSON = JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
|
|
284
|
+
function materializedRefreshFunctionName(table) {
|
|
285
|
+
return `${table}__refresh`;
|
|
286
|
+
}
|
|
287
|
+
function materializeLockKey(schema, table) {
|
|
288
|
+
return `orm:materialize:${schema}.${table}`;
|
|
289
|
+
}
|
|
290
|
+
var policyVariantSchema = zod.z.object({
|
|
291
|
+
/** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
|
|
292
|
+
when: zod.z.string().min(1).max(1e3),
|
|
293
|
+
/** Exactly one of: allow-all bypass, or a row predicate. */
|
|
294
|
+
allow: zod.z.literal(true).optional(),
|
|
295
|
+
policy: zod.z.string().min(1).max(2e3).optional()
|
|
296
|
+
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
297
|
+
message: "A variant declares exactly one of allow: true or policy"
|
|
298
|
+
});
|
|
299
|
+
var policyExpressionSchema = zod.z.union([
|
|
300
|
+
zod.z.string().min(1).max(2e3),
|
|
301
|
+
zod.z.object({
|
|
302
|
+
default: zod.z.string().min(1).max(2e3),
|
|
303
|
+
variants: zod.z.array(policyVariantSchema).max(10).default([])
|
|
304
|
+
})
|
|
305
|
+
]);
|
|
306
|
+
function parseDurationMs(input) {
|
|
307
|
+
if (typeof input === "number") return Number.isInteger(input) && input > 0 ? input : void 0;
|
|
308
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(input);
|
|
309
|
+
if (!match) return void 0;
|
|
310
|
+
const factor = match[2] === "ms" ? 1 : match[2] === "s" ? 1e3 : 6e4;
|
|
311
|
+
const value = Math.round(Number(match[1]) * factor);
|
|
312
|
+
return value > 0 ? value : void 0;
|
|
313
|
+
}
|
|
314
|
+
var identifier = zod.z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
|
|
315
|
+
var durationMs = zod.z.number().int().positive().max(24 * 60 * 60 * 1e3);
|
|
316
|
+
var RESOLVED_VIEW_SYSTEM_COLUMNS = {
|
|
317
|
+
writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
|
|
318
|
+
writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
|
|
319
|
+
};
|
|
320
|
+
var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
|
|
321
|
+
tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
|
|
322
|
+
fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
|
|
323
|
+
writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
|
|
324
|
+
writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
|
|
325
|
+
writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
|
|
326
|
+
};
|
|
327
|
+
var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
|
|
328
|
+
/* @__PURE__ */ new Set([
|
|
329
|
+
...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
|
|
330
|
+
...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
|
|
331
|
+
]);
|
|
332
|
+
var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
|
|
333
|
+
var canonicalFunctionRefSchema = zod.z.object({
|
|
334
|
+
/** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
|
|
335
|
+
kind: zod.z.enum(["managed", "native"]),
|
|
336
|
+
id: zod.z.string().min(1).max(200)
|
|
337
|
+
}).strict();
|
|
338
|
+
var canonicalFetchShapeSchema = zod.z.union([
|
|
339
|
+
zod.z.object({ shape: zod.z.literal("single") }).strict(),
|
|
340
|
+
zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().min(2).max(500) }).strict()
|
|
341
|
+
]);
|
|
342
|
+
var canonicalResolvedSourceSchema = zod.z.object({
|
|
343
|
+
name: identifier,
|
|
344
|
+
kind: zod.z.enum(["manual", "etl", "prefetch"]),
|
|
345
|
+
/** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
|
|
346
|
+
table: identifier,
|
|
347
|
+
/** Resolved columns (logical names) this source supplies — the primary key is implicit. */
|
|
348
|
+
columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
|
|
349
|
+
/** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
|
|
350
|
+
updatedAt: zod.z.string().min(1).max(63).optional(),
|
|
351
|
+
/** prefetch: resolved column holding the source system's key (may be the primary key). */
|
|
352
|
+
key: zod.z.string().min(1).max(63).optional(),
|
|
353
|
+
fetch: canonicalFetchShapeSchema.optional(),
|
|
354
|
+
function: canonicalFunctionRefSchema.optional(),
|
|
355
|
+
prefetchDefaults: zod.z.object({
|
|
356
|
+
maxStalenessMs: durationMs,
|
|
357
|
+
timeoutMs: durationMs,
|
|
358
|
+
onError: zod.z.enum(["fallback", "fail"])
|
|
359
|
+
}).strict().optional(),
|
|
360
|
+
writeback: zod.z.object({
|
|
361
|
+
columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
|
|
362
|
+
defaultMode: zod.z.enum(["sync", "async"]),
|
|
363
|
+
timeoutMs: durationMs,
|
|
364
|
+
/** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
|
|
365
|
+
clearManualOnConfirm: zod.z.boolean()
|
|
366
|
+
}).strict().optional()
|
|
367
|
+
}).strict();
|
|
368
|
+
var canonicalResolveRuleSchema = zod.z.object({
|
|
369
|
+
column: zod.z.string().min(1).max(63),
|
|
370
|
+
comparator: zod.z.enum(COMPARATORS),
|
|
371
|
+
/** Source names, in tie-break order. */
|
|
372
|
+
candidates: zod.z.array(identifier).min(1).max(16),
|
|
373
|
+
/** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
|
|
374
|
+
orderBy: zod.z.string().min(1).max(2e3).optional()
|
|
375
|
+
}).strict();
|
|
376
|
+
var canonicalResolvedSchema = zod.z.object({
|
|
377
|
+
sources: zod.z.array(canonicalResolvedSourceSchema).min(1).max(16),
|
|
378
|
+
/** One rule per resolved (non-pk) column; documents the generated view for tooling. */
|
|
379
|
+
resolve: zod.z.array(canonicalResolveRuleSchema).max(500),
|
|
380
|
+
/** Sources whose tombstone hides the resolved row (v1: the manual source only). */
|
|
381
|
+
deletedBy: zod.z.array(identifier).max(1).default([]),
|
|
382
|
+
/**
|
|
383
|
+
* Materialized: the view reads an indexed internal table (`table`) that the
|
|
384
|
+
* data plane keeps in step with `compute`, the internal view holding the
|
|
385
|
+
* resolution query. Omitted when the view computes on read.
|
|
386
|
+
*/
|
|
387
|
+
materialized: zod.z.object({ table: identifier, compute: identifier }).strict().optional(),
|
|
388
|
+
/** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
|
|
389
|
+
policies: zod.z.object({
|
|
390
|
+
write: policyExpressionSchema.optional(),
|
|
391
|
+
insert: policyExpressionSchema.optional(),
|
|
392
|
+
update: policyExpressionSchema.optional()
|
|
393
|
+
}).strict().default({})
|
|
394
|
+
}).strict();
|
|
395
|
+
function validateResolvedView({ view, tables, views = [] }) {
|
|
396
|
+
const resolved = view.resolved;
|
|
397
|
+
if (!resolved) return [];
|
|
398
|
+
const errors = [];
|
|
399
|
+
const fail3 = (message) => errors.push(`resolved table '${view.view}': ${message}`);
|
|
400
|
+
const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
|
|
401
|
+
if (!pkColumn) {
|
|
402
|
+
fail3("requires exactly one primary-key column");
|
|
403
|
+
return errors;
|
|
404
|
+
}
|
|
405
|
+
const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
|
|
406
|
+
const scope = {
|
|
407
|
+
view,
|
|
408
|
+
resolved,
|
|
409
|
+
pkColumn,
|
|
410
|
+
dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
|
|
411
|
+
sourceByName: /* @__PURE__ */ new Map(),
|
|
412
|
+
fail: fail3
|
|
413
|
+
};
|
|
414
|
+
if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail3("requires exactly one manual source");
|
|
415
|
+
const tableByName = new Map(tables.map((table) => [table.table, table]));
|
|
416
|
+
for (const source of resolved.sources) {
|
|
417
|
+
if (scope.sourceByName.has(source.name)) fail3(`duplicate source '${source.name}'`);
|
|
418
|
+
scope.sourceByName.set(source.name, source);
|
|
419
|
+
const table = tableByName.get(source.table);
|
|
420
|
+
if (!table) fail3(`source '${source.name}' references unknown table '${source.table}'`);
|
|
421
|
+
else validateSource({ scope, source, table });
|
|
422
|
+
}
|
|
423
|
+
validateRules(scope);
|
|
424
|
+
validateViewCells(scope);
|
|
425
|
+
if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
|
|
426
|
+
for (const name of resolved.deletedBy) {
|
|
427
|
+
if (scope.sourceByName.get(name)?.kind !== "manual") fail3(`deletedBy '${name}' must be the manual source`);
|
|
428
|
+
}
|
|
429
|
+
return errors;
|
|
430
|
+
}
|
|
431
|
+
function validateSource({ scope, source, table }) {
|
|
432
|
+
const { fail: fail3 } = scope;
|
|
433
|
+
if (source.kind === "etl" && table.source === void 0) fail3(`etl source '${source.name}' must reference a synced table`);
|
|
434
|
+
if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
|
|
435
|
+
fail3(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
|
|
436
|
+
}
|
|
437
|
+
const hasRev = table.columns.some((column) => column.cell?.part === "rev");
|
|
438
|
+
if (source.kind === "manual" && !hasRev) fail3(`manual source '${source.name}' must carry '__rev' cell columns`);
|
|
439
|
+
if (source.kind === "prefetch" && hasRev) fail3(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
|
|
440
|
+
const byName = new Map(table.columns.map((c) => [c.name, c]));
|
|
441
|
+
validateSourceColumns({ scope, source, byName });
|
|
442
|
+
if (source.kind === "etl") {
|
|
443
|
+
const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
|
|
444
|
+
if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
|
|
445
|
+
fail3(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
validatePrefetchFields({ scope, source });
|
|
449
|
+
}
|
|
450
|
+
function validatePrefetchFields({ scope, source }) {
|
|
451
|
+
const { fail: fail3, pkColumn } = scope;
|
|
452
|
+
if (source.kind !== "prefetch") {
|
|
453
|
+
if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
|
|
454
|
+
fail3(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
|
|
455
|
+
}
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
|
|
459
|
+
fail3(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
|
|
460
|
+
} else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
|
|
461
|
+
fail3(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
|
|
462
|
+
}
|
|
463
|
+
for (const column of source.writeback?.columns ?? []) {
|
|
464
|
+
if (!source.columns.includes(column)) fail3(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function validateSourceColumns({ scope, source, byName }) {
|
|
468
|
+
const { fail: fail3, pkColumn, dataColumns } = scope;
|
|
469
|
+
for (const column of [pkColumn.name, ...source.columns]) {
|
|
470
|
+
const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
|
|
471
|
+
const theirs = byName.get(column);
|
|
472
|
+
if (!own) fail3(`source '${source.name}' supplies unknown column '${column}'`);
|
|
473
|
+
else if (!theirs) fail3(`source '${source.name}' table '${source.table}' has no column '${column}'`);
|
|
474
|
+
else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail3(`column '${column}' has a different type on source '${source.name}'`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail: fail3 }) {
|
|
478
|
+
const ruled = /* @__PURE__ */ new Set();
|
|
479
|
+
for (const rule of resolved.resolve) {
|
|
480
|
+
if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
|
|
481
|
+
fail3(`resolve rule targets unknown column '${rule.column}'`);
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (ruled.has(rule.column)) fail3(`column '${rule.column}' has more than one resolve rule`);
|
|
485
|
+
ruled.add(rule.column);
|
|
486
|
+
if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
|
|
487
|
+
fail3(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
|
|
488
|
+
}
|
|
489
|
+
if (new Set(rule.candidates).size !== rule.candidates.length) fail3(`column '${rule.column}' repeats a candidate`);
|
|
490
|
+
for (const candidate of rule.candidates) {
|
|
491
|
+
const source = sourceByName.get(candidate);
|
|
492
|
+
if (!source) fail3(`column '${rule.column}' names unknown candidate source '${candidate}'`);
|
|
493
|
+
else if (!source.columns.includes(rule.column)) fail3(`column '${rule.column}' candidate '${candidate}' does not supply it`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
for (const column of dataColumns.keys()) {
|
|
497
|
+
if (column !== pkColumn.name && !ruled.has(column)) fail3(`column '${column}' has no resolve rule`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
var VIEW_CELL_TYPES = {
|
|
501
|
+
source: "text",
|
|
502
|
+
at: "timestamptz",
|
|
503
|
+
by: "text",
|
|
504
|
+
conflicts: "jsonb"
|
|
505
|
+
};
|
|
506
|
+
function validateViewCells({ view, dataColumns, pkColumn, fail: fail3 }) {
|
|
507
|
+
const expected = /* @__PURE__ */ new Map();
|
|
508
|
+
for (const column of dataColumns.values()) {
|
|
509
|
+
if (column.name === pkColumn.name) continue;
|
|
510
|
+
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
511
|
+
expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
for (const column of view.columns) {
|
|
515
|
+
if (!column.cell) continue;
|
|
516
|
+
const want = expected.get(column.name);
|
|
517
|
+
if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
|
|
518
|
+
fail3(`unexpected cell column '${column.name}'`);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail3(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
|
|
522
|
+
expected.delete(column.name);
|
|
523
|
+
}
|
|
524
|
+
for (const name of expected.keys()) fail3(`missing cell column '${name}'`);
|
|
525
|
+
}
|
|
526
|
+
var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
|
|
527
|
+
function validateMaterialized({ scope, tableByName, views }) {
|
|
528
|
+
const { view, resolved, fail: fail3 } = scope;
|
|
529
|
+
const { table: tableName, compute: computeName } = resolved.materialized;
|
|
530
|
+
const table = tableByName.get(tableName);
|
|
531
|
+
if (!table || table.internal !== true || table.materializes !== view.view) {
|
|
532
|
+
fail3(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
|
|
533
|
+
} else {
|
|
534
|
+
if (columnSignature(table.columns) !== columnSignature(view.columns)) fail3(`materialized table '${tableName}' must have exactly the view's columns`);
|
|
535
|
+
if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail3(`materialized table '${tableName}' must share the view's primary key`);
|
|
536
|
+
}
|
|
537
|
+
const compute = views.find((candidate) => candidate.view === computeName);
|
|
538
|
+
if (!compute || compute.internal !== true) fail3(`compute view '${computeName}' must be an internal view`);
|
|
539
|
+
else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail3(`compute view '${computeName}' must have exactly the view's columns`);
|
|
540
|
+
for (const source of resolved.sources) {
|
|
541
|
+
if (source.kind !== "etl") continue;
|
|
542
|
+
const read = tableByName.get(source.table)?.policies?.read;
|
|
543
|
+
const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
|
|
544
|
+
if (!unconditional) {
|
|
545
|
+
fail3(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
64
549
|
|
|
65
550
|
// src/canonical/v2.ts
|
|
66
551
|
var CANONICAL_FORMAT = 2;
|
|
67
552
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
68
|
-
var
|
|
553
|
+
var identifier2 = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
69
554
|
var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
|
|
70
555
|
var COLUMN_TYPES = [
|
|
71
556
|
"uuid",
|
|
@@ -81,14 +566,14 @@ var COLUMN_TYPES = [
|
|
|
81
566
|
];
|
|
82
567
|
var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
|
|
83
568
|
var canonicalEnumSchema = zod.z.object({
|
|
84
|
-
name:
|
|
569
|
+
name: identifier2,
|
|
85
570
|
values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
|
|
86
571
|
});
|
|
87
572
|
var canonicalColumnSchema = zod.z.object({
|
|
88
573
|
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
89
574
|
name: zod.z.string().min(1).max(63),
|
|
90
575
|
columnName: columnIdentifier,
|
|
91
|
-
type: zod.z.union([columnTypeSchema, zod.z.object({ enum:
|
|
576
|
+
type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier2 })]),
|
|
92
577
|
nullable: zod.z.boolean().default(true),
|
|
93
578
|
unique: zod.z.boolean().default(false),
|
|
94
579
|
/** Literal default, or one of the allowlisted SQL functions. */
|
|
@@ -104,7 +589,14 @@ var canonicalColumnSchema = zod.z.object({
|
|
|
104
589
|
/** Migration hints — same semantics as v1. */
|
|
105
590
|
renamedFrom: columnIdentifier.optional(),
|
|
106
591
|
cast: zod.z.string().max(200).optional(),
|
|
107
|
-
deprecated: zod.z.boolean().default(false)
|
|
592
|
+
deprecated: zod.z.boolean().default(false),
|
|
593
|
+
/**
|
|
594
|
+
* Marks a system cell-metadata column (see CELL_PARTS in constants): part
|
|
595
|
+
* `part` of the cell of logical column `of`. Synthesized by authoring,
|
|
596
|
+
* never client-writable, absent from GraphQL scalar fields. Omitted on
|
|
597
|
+
* ordinary columns so their hashes are unchanged.
|
|
598
|
+
*/
|
|
599
|
+
cell: zod.z.object({ of: zod.z.string().min(1).max(63), part: zod.z.enum(CELL_PARTS) }).strict().optional()
|
|
108
600
|
}).superRefine((column, ctx) => {
|
|
109
601
|
if (column.onUpdateFn === void 0) return;
|
|
110
602
|
if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
|
|
@@ -119,36 +611,26 @@ var canonicalLinkSchema = zod.z.object({
|
|
|
119
611
|
name: zod.z.string().min(1).max(63),
|
|
120
612
|
kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
|
|
121
613
|
/** Target table name (namespace-local). Absent for toPlatformUser. */
|
|
122
|
-
target:
|
|
614
|
+
target: identifier2.optional(),
|
|
123
615
|
/** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
|
|
124
616
|
on: columnIdentifier,
|
|
125
617
|
/** Referenced column on the target (default: target's PK / platform user `sub`). */
|
|
126
618
|
references: columnIdentifier.optional(),
|
|
127
619
|
/** enforce: true emits a real FK constraint (same-namespace toOne only). */
|
|
128
620
|
enforce: zod.z.boolean().default(false),
|
|
621
|
+
onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
622
|
+
onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
129
623
|
description: zod.z.string().max(500).optional()
|
|
130
624
|
});
|
|
131
625
|
var canonicalIndexSchema = zod.z.object({
|
|
132
626
|
columns: zod.z.array(columnIdentifier).min(1),
|
|
133
627
|
unique: zod.z.boolean().default(false),
|
|
134
|
-
name:
|
|
628
|
+
name: identifier2.optional()
|
|
135
629
|
});
|
|
136
|
-
var
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
allow: zod.z.literal(true).optional(),
|
|
141
|
-
policy: zod.z.string().min(1).max(2e3).optional()
|
|
142
|
-
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
143
|
-
message: "A variant declares exactly one of allow: true or policy"
|
|
144
|
-
});
|
|
145
|
-
var policyExpressionSchema = zod.z.union([
|
|
146
|
-
zod.z.string().min(1).max(2e3),
|
|
147
|
-
zod.z.object({
|
|
148
|
-
default: zod.z.string().min(1).max(2e3),
|
|
149
|
-
variants: zod.z.array(policyVariantSchema).max(10).default([])
|
|
150
|
-
})
|
|
151
|
-
]);
|
|
630
|
+
var canonicalCheckSchema = zod.z.object({
|
|
631
|
+
name: identifier2,
|
|
632
|
+
expression: zod.z.string().min(1).max(2e3)
|
|
633
|
+
}).strict();
|
|
152
634
|
var canonicalPoliciesSchema = zod.z.object({
|
|
153
635
|
read: policyExpressionSchema.optional(),
|
|
154
636
|
write: policyExpressionSchema.optional(),
|
|
@@ -336,7 +818,7 @@ var VIEW_QUERY_FUNCTION_ALLOWLIST = [
|
|
|
336
818
|
"width_bucket"
|
|
337
819
|
];
|
|
338
820
|
var canonicalActionSchema = zod.z.object({
|
|
339
|
-
name:
|
|
821
|
+
name: identifier2,
|
|
340
822
|
requires: zod.z.string().max(1e3).optional(),
|
|
341
823
|
params: zod.z.record(zod.z.string(), zod.z.object({
|
|
342
824
|
type: columnTypeSchema,
|
|
@@ -348,9 +830,9 @@ var canonicalActionSchema = zod.z.object({
|
|
|
348
830
|
});
|
|
349
831
|
var canonicalCowMutationSchema = zod.z.object({
|
|
350
832
|
kind: zod.z.enum(["override", "revert"]),
|
|
351
|
-
name:
|
|
833
|
+
name: identifier2,
|
|
352
834
|
/** Synced table (same namespace) the overlay copies from / falls back to. */
|
|
353
|
-
synced:
|
|
835
|
+
synced: identifier2,
|
|
354
836
|
/** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
|
|
355
837
|
pk: zod.z.string().min(1).max(63),
|
|
356
838
|
/** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
|
|
@@ -378,23 +860,27 @@ var canonicalCowMutationSchema = zod.z.object({
|
|
|
378
860
|
}
|
|
379
861
|
});
|
|
380
862
|
var canonicalForeignKeySchema = zod.z.object({
|
|
381
|
-
name:
|
|
863
|
+
name: identifier2,
|
|
382
864
|
columns: zod.z.array(columnIdentifier).min(1).max(8),
|
|
383
|
-
target:
|
|
865
|
+
target: identifier2,
|
|
384
866
|
references: zod.z.array(columnIdentifier).min(1).max(8),
|
|
385
|
-
deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional()
|
|
867
|
+
deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional(),
|
|
868
|
+
onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
869
|
+
onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
|
|
386
870
|
}).strict();
|
|
387
871
|
var canonicalTableSchema = zod.z.object({
|
|
388
|
-
table:
|
|
872
|
+
table: identifier2,
|
|
389
873
|
description: zod.z.string().max(2e3).optional(),
|
|
390
874
|
/** Table-level rename hint (new in v2). */
|
|
391
|
-
renamedFrom:
|
|
875
|
+
renamedFrom: identifier2.optional(),
|
|
392
876
|
/** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
|
|
393
877
|
source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
|
|
394
878
|
columns: zod.z.array(canonicalColumnSchema).default([]),
|
|
395
879
|
primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
|
|
396
880
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
397
881
|
indexes: zod.z.array(canonicalIndexSchema).default([]),
|
|
882
|
+
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
883
|
+
checks: zod.z.array(canonicalCheckSchema).max(100).optional(),
|
|
398
884
|
policies: canonicalPoliciesSchema.default({}),
|
|
399
885
|
/**
|
|
400
886
|
* v1-conversion carry-over only. v2 has no action surface — the runtime
|
|
@@ -405,22 +891,55 @@ var canonicalTableSchema = zod.z.object({
|
|
|
405
891
|
/** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
|
|
406
892
|
cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
|
|
407
893
|
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
408
|
-
foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional()
|
|
894
|
+
foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional(),
|
|
895
|
+
/**
|
|
896
|
+
* Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
|
|
897
|
+
* column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
|
|
898
|
+
* on a resolved table's internal sources), listed explicitly in `columns`
|
|
899
|
+
* with a `cell` marker so DDL and migrations treat them as ordinary columns.
|
|
900
|
+
* Omitted when off so existing definitions hash unchanged.
|
|
901
|
+
*/
|
|
902
|
+
cellMetadata: zod.z.literal(true).optional(),
|
|
903
|
+
/**
|
|
904
|
+
* System-owned backing table of a resolved table (its manual or prefetch
|
|
905
|
+
* source). Internal tables get DDL and IR metadata but no GraphQL surface;
|
|
906
|
+
* only the data plane writes them. Omitted when false.
|
|
907
|
+
*/
|
|
908
|
+
internal: zod.z.literal(true).optional(),
|
|
909
|
+
/**
|
|
910
|
+
* The materialized copy of resolved table `materializes` (internal): the
|
|
911
|
+
* same columns as that view, including its cell parts, maintained by the
|
|
912
|
+
* data plane. Omitted otherwise.
|
|
913
|
+
*/
|
|
914
|
+
materializes: identifier2.optional()
|
|
409
915
|
});
|
|
916
|
+
var MAX_VIEW_QUERY = 1e5;
|
|
917
|
+
var MAX_RESOLVED_VIEW_QUERY = 5e6;
|
|
410
918
|
var canonicalViewSchema = zod.z.object({
|
|
411
|
-
view:
|
|
919
|
+
view: identifier2,
|
|
412
920
|
description: zod.z.string().max(2e3).optional(),
|
|
413
921
|
columns: zod.z.array(canonicalColumnSchema).min(1),
|
|
414
922
|
/** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
|
|
415
923
|
primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
|
|
416
924
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
417
925
|
policies: zod.z.object({ read: policyExpressionSchema }).strict(),
|
|
418
|
-
query: zod.z.string().min(1).max(
|
|
926
|
+
query: zod.z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
|
|
419
927
|
message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
|
|
420
928
|
}),
|
|
421
929
|
/** Namespace-local logical relation names, sorted for deterministic hashing. */
|
|
422
|
-
dependencies: zod.z.array(
|
|
930
|
+
dependencies: zod.z.array(identifier2).max(500),
|
|
931
|
+
/**
|
|
932
|
+
* Present when this view is a resolved table (defineResolvedTable): the
|
|
933
|
+
* multi-source metadata behind its generated query. Omitted otherwise so
|
|
934
|
+
* existing views hash unchanged.
|
|
935
|
+
*/
|
|
936
|
+
resolved: canonicalResolvedSchema.optional(),
|
|
937
|
+
/** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
|
|
938
|
+
internal: zod.z.literal(true).optional()
|
|
423
939
|
}).superRefine((view, ctx) => {
|
|
940
|
+
if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
|
|
941
|
+
ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
|
|
942
|
+
}
|
|
424
943
|
for (const [index3, column] of view.columns.entries()) {
|
|
425
944
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
426
945
|
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare a default` });
|
|
@@ -435,15 +954,82 @@ var canonicalViewSchema = zod.z.object({
|
|
|
435
954
|
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare migration hints` });
|
|
436
955
|
}
|
|
437
956
|
}
|
|
957
|
+
if (view.resolved === void 0 && view.internal !== true) {
|
|
958
|
+
for (const [index3, column] of view.columns.entries()) {
|
|
959
|
+
if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index3], message: `only resolved tables carry cell columns ('${column.name}')` });
|
|
960
|
+
}
|
|
961
|
+
}
|
|
438
962
|
for (const [index3, link] of view.links.entries()) {
|
|
439
963
|
if (link.enforce) {
|
|
440
964
|
ctx.addIssue({ code: "custom", path: ["links", index3, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
|
|
441
965
|
}
|
|
966
|
+
if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
|
|
967
|
+
ctx.addIssue({ code: "custom", path: ["links", index3], message: `view link '${link.name}' cannot declare referential actions` });
|
|
968
|
+
}
|
|
442
969
|
}
|
|
443
970
|
});
|
|
971
|
+
var CELL_PART_TYPES = {
|
|
972
|
+
at: "timestamptz",
|
|
973
|
+
by: "text",
|
|
974
|
+
rev: "uuid",
|
|
975
|
+
source: "text",
|
|
976
|
+
conflicts: "jsonb"
|
|
977
|
+
};
|
|
978
|
+
function trackedCellColumns(table) {
|
|
979
|
+
const pk = new Set(table.primaryKey ?? []);
|
|
980
|
+
return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
|
|
981
|
+
}
|
|
982
|
+
var SUFFIXES = Object.values(CELL_PART_SUFFIX);
|
|
983
|
+
function validateCellMetadata(table, index3, ctx) {
|
|
984
|
+
const fail3 = (message, path = ["tables", index3]) => ctx.addIssue({ code: "custom", path, message });
|
|
985
|
+
const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
|
|
986
|
+
if (table.materializes !== void 0) return;
|
|
987
|
+
if (table.cellMetadata !== true) {
|
|
988
|
+
for (const { position } of cells) fail3(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index3, "columns", position]);
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
if (table.source !== void 0) {
|
|
992
|
+
fail3(`synced table '${table.table}' cannot track cell metadata`);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
const tracked = trackedCellColumns(table);
|
|
996
|
+
const byName = new Map(tracked.map((column) => [column.name, column]));
|
|
997
|
+
const withRev = cells.some(({ cell }) => cell.part === "rev");
|
|
998
|
+
if (withRev && table.internal !== true) fail3(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
|
|
999
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1000
|
+
for (const { column, position, cell } of cells) {
|
|
1001
|
+
const path = ["tables", index3, "columns", position];
|
|
1002
|
+
const owner = byName.get(cell.of);
|
|
1003
|
+
if (!owner) {
|
|
1004
|
+
fail3(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
if (cell.part !== "at" && cell.part !== "by" && cell.part !== "rev") fail3(`a table cell column must be part at/by/rev, not '${cell.part}'`, path);
|
|
1008
|
+
if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
|
|
1009
|
+
fail3(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
|
|
1010
|
+
}
|
|
1011
|
+
if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1012
|
+
fail3(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
|
|
1013
|
+
}
|
|
1014
|
+
const key = `${cell.of}:${cell.part}`;
|
|
1015
|
+
if (seen.has(key)) fail3(`duplicate cell column '${column.name}'`, path);
|
|
1016
|
+
seen.add(key);
|
|
1017
|
+
}
|
|
1018
|
+
for (const column of tracked) {
|
|
1019
|
+
if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
|
|
1020
|
+
fail3(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
|
|
1021
|
+
}
|
|
1022
|
+
if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
|
|
1023
|
+
fail3(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
|
|
1024
|
+
}
|
|
1025
|
+
for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
|
|
1026
|
+
if (!seen.has(`${column.name}:${part}`)) fail3(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
444
1030
|
var canonicalDefinitionSchema = zod.z.object({
|
|
445
1031
|
definition_format: zod.z.literal(CANONICAL_FORMAT),
|
|
446
|
-
namespace:
|
|
1032
|
+
namespace: identifier2.max(41),
|
|
447
1033
|
description: zod.z.string().max(2e3).optional(),
|
|
448
1034
|
enums: zod.z.array(canonicalEnumSchema).default([]),
|
|
449
1035
|
tables: zod.z.array(canonicalTableSchema).min(0).max(500),
|
|
@@ -461,6 +1047,7 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
461
1047
|
ctx.addIssue({ code: "custom", path: ["tables", index3, "table"], message: `duplicate relation '${table.table}'` });
|
|
462
1048
|
}
|
|
463
1049
|
seen.add(table.table);
|
|
1050
|
+
validateCellMetadata(table, index3, ctx);
|
|
464
1051
|
if (table.source !== void 0) {
|
|
465
1052
|
for (const [columnIndex, column] of table.columns.entries()) {
|
|
466
1053
|
if (column.onUpdateFn !== void 0) {
|
|
@@ -480,25 +1067,53 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
480
1067
|
...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
|
|
481
1068
|
...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
|
|
482
1069
|
]);
|
|
1070
|
+
for (const [index3, link] of table.links.entries()) {
|
|
1071
|
+
if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
|
|
1072
|
+
ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index3], message: "referential actions require an enforced toOne link" });
|
|
1073
|
+
}
|
|
1074
|
+
if (link.onDelete === "set null" || link.onUpdate === "set null") {
|
|
1075
|
+
const source = table.columns.find((column) => column.columnName === link.on);
|
|
1076
|
+
if (source?.nullable === false || table.primaryKey?.includes(link.on)) ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index3], message: "SET NULL requires a nullable source column" });
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
for (const [index3, check] of (table.checks ?? []).entries()) {
|
|
1080
|
+
const fail3 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index3], message });
|
|
1081
|
+
if (table.source) fail3("synced tables cannot declare checks");
|
|
1082
|
+
if (names.has(check.name)) fail3(`duplicate constraint '${check.name}'`);
|
|
1083
|
+
names.add(check.name);
|
|
1084
|
+
try {
|
|
1085
|
+
compileCheckExpression(check.expression, table.columns);
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
fail3(error instanceof Error ? error.message : String(error));
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
483
1090
|
for (const [index3, fk] of (table.foreignKeys ?? []).entries()) {
|
|
484
|
-
const
|
|
485
|
-
if (names.has(fk.name))
|
|
1091
|
+
const fail3 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index3], message });
|
|
1092
|
+
if (names.has(fk.name)) fail3(`duplicate foreign key '${fk.name}'`);
|
|
486
1093
|
names.add(fk.name);
|
|
487
1094
|
const target = fkTables.get(fk.target);
|
|
488
1095
|
if (table.source || !target || target.source) {
|
|
489
|
-
|
|
1096
|
+
fail3("foreign keys must connect owned tables in the same namespace");
|
|
490
1097
|
continue;
|
|
491
1098
|
}
|
|
492
|
-
if (fk.columns.length !== fk.references.length)
|
|
493
|
-
if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length)
|
|
1099
|
+
if (fk.columns.length !== fk.references.length) fail3("foreign key columns and references must have the same number of columns");
|
|
1100
|
+
if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail3("foreign key columns must not repeat");
|
|
494
1101
|
for (const [position, name] of fk.columns.entries()) {
|
|
495
1102
|
const sourceColumn = table.columns.find((column) => column.columnName === name);
|
|
496
1103
|
const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
|
|
497
|
-
if (!sourceColumn || !targetColumn)
|
|
498
|
-
else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type))
|
|
1104
|
+
if (!sourceColumn || !targetColumn) fail3("foreign key references an unknown column");
|
|
1105
|
+
else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail3("foreign key column types must match");
|
|
1106
|
+
}
|
|
1107
|
+
if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
|
|
1108
|
+
if (fk.columns.some((name) => table.columns.find((column) => column.columnName === name)?.nullable === false || table.primaryKey?.includes(name))) fail3("SET NULL requires nullable source columns");
|
|
499
1109
|
}
|
|
500
1110
|
const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index4) => index4.unique).map((index4) => index4.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
|
|
501
|
-
if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column))))
|
|
1111
|
+
if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail3("foreign key target columns must form a unique key");
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
for (const [viewIndex, view] of (definition.views ?? []).entries()) {
|
|
1115
|
+
for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
|
|
1116
|
+
ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
|
|
502
1117
|
}
|
|
503
1118
|
}
|
|
504
1119
|
const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
@@ -624,8 +1239,12 @@ function canonicalRelations(definition) {
|
|
|
624
1239
|
...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
|
|
625
1240
|
links: view.links,
|
|
626
1241
|
indexes: [],
|
|
627
|
-
|
|
628
|
-
|
|
1242
|
+
// A resolved table's write policies (evaluated against the resolved row)
|
|
1243
|
+
// ride alongside the view's read policy.
|
|
1244
|
+
policies: { read: view.policies.read, ...view.resolved?.policies ?? {} },
|
|
1245
|
+
actions: [],
|
|
1246
|
+
...view.resolved !== void 0 ? { resolved: view.resolved } : {},
|
|
1247
|
+
...view.internal ? { internal: true } : {}
|
|
629
1248
|
}));
|
|
630
1249
|
return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
|
|
631
1250
|
}
|
|
@@ -635,13 +1254,14 @@ function normalizeCanonical(definition) {
|
|
|
635
1254
|
...definition,
|
|
636
1255
|
enums: sortByName(definition.enums),
|
|
637
1256
|
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
|
|
638
|
-
const { cowMutations, foreignKeys, ...rest } = t2;
|
|
1257
|
+
const { cowMutations, foreignKeys, checks, ...rest } = t2;
|
|
639
1258
|
return {
|
|
640
1259
|
...rest,
|
|
641
1260
|
links: sortByName(t2.links),
|
|
642
1261
|
indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
|
|
643
1262
|
actions: sortByName(t2.actions),
|
|
644
1263
|
...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
|
|
1264
|
+
...checks?.length ? { checks: sortByName(checks) } : {},
|
|
645
1265
|
// Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
|
|
646
1266
|
...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
|
|
647
1267
|
};
|
|
@@ -928,8 +1548,19 @@ function isSeqNamespaceConfig(value) {
|
|
|
928
1548
|
return typeof value === "object" && value !== null && value.kind === NAMESPACE_CONFIG_BRAND;
|
|
929
1549
|
}
|
|
930
1550
|
function serializeNamespace(input) {
|
|
931
|
-
const { config
|
|
932
|
-
const views = input.views ?? [];
|
|
1551
|
+
const { config } = input;
|
|
1552
|
+
const views = [...input.views ?? []];
|
|
1553
|
+
const tables = [...input.tables];
|
|
1554
|
+
for (const view of views) {
|
|
1555
|
+
for (const internal of view.config.resolved?.internalTables ?? []) {
|
|
1556
|
+
if (!tables.includes(internal)) tables.push(internal);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
for (const view of [...views]) {
|
|
1560
|
+
for (const internal of view.config.resolved?.internalViews ?? []) {
|
|
1561
|
+
if (!views.includes(internal)) views.push(internal);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
933
1564
|
const enums = input.enums ?? [];
|
|
934
1565
|
const tablesByName = /* @__PURE__ */ new Map();
|
|
935
1566
|
for (const table of tables) {
|
|
@@ -1031,6 +1662,9 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1031
1662
|
if (table.config.renamedFrom !== void 0) candidate.renamedFrom = table.config.renamedFrom;
|
|
1032
1663
|
if (table.source !== void 0) candidate.source = table.source;
|
|
1033
1664
|
if (table.source !== void 0) {
|
|
1665
|
+
if (table.config.checks?.length) {
|
|
1666
|
+
throw new OrmDefinitionError(`synced table "${table.tableName}" is system-owned; checks are not allowed`, table.file);
|
|
1667
|
+
}
|
|
1034
1668
|
if (table.config.indexes !== void 0 && table.config.indexes.length > 0) {
|
|
1035
1669
|
throw new OrmDefinitionError(
|
|
1036
1670
|
`synced table "${table.tableName}" is system-owned; indexes are not allowed`,
|
|
@@ -1068,6 +1702,11 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1068
1702
|
if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
|
|
1069
1703
|
candidate.columns?.push(column);
|
|
1070
1704
|
}
|
|
1705
|
+
if (table.config.internal === true) candidate.internal = true;
|
|
1706
|
+
if (table.config.materializes !== void 0) candidate.materializes = table.config.materializes;
|
|
1707
|
+
if (table.config.cellMetadata === true && table.source !== void 0) {
|
|
1708
|
+
throw new OrmDefinitionError(`synced table "${table.tableName}" cannot track cell metadata \u2014 its DDL is owned by the sync`, table.file);
|
|
1709
|
+
}
|
|
1071
1710
|
if (table.config.primaryKey !== void 0) {
|
|
1072
1711
|
if (columnLevelPk.length > 0) {
|
|
1073
1712
|
throw new OrmDefinitionError(
|
|
@@ -1081,6 +1720,24 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1081
1720
|
} else if (columnLevelPk.length > 0) {
|
|
1082
1721
|
candidate.primaryKey = columnLevelPk;
|
|
1083
1722
|
}
|
|
1723
|
+
if (table.config.cellMetadata === true) {
|
|
1724
|
+
candidate.cellMetadata = true;
|
|
1725
|
+
const parts = table.config.cellRevisions === true ? ["at", "by", "rev"] : ["at", "by"];
|
|
1726
|
+
const tracked = trackedCellColumns({ columns: candidate.columns ?? [], primaryKey: candidate.primaryKey });
|
|
1727
|
+
for (const column of tracked) {
|
|
1728
|
+
for (const part of parts) {
|
|
1729
|
+
candidate.columns?.push({
|
|
1730
|
+
name: cellColumnName(column.name, part),
|
|
1731
|
+
columnName: cellColumnName(column.columnName, part),
|
|
1732
|
+
type: CELL_PART_TYPES[part],
|
|
1733
|
+
nullable: true,
|
|
1734
|
+
unique: false,
|
|
1735
|
+
deprecated: false,
|
|
1736
|
+
cell: { of: column.name, part }
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1084
1741
|
if (table.config.foreignKeys?.length) {
|
|
1085
1742
|
if (table.source) throw new OrmDefinitionError(`synced table "${table.tableName}" cannot enforce foreign keys`, table.file);
|
|
1086
1743
|
candidate.foreignKeys = table.config.foreignKeys.map((fk) => {
|
|
@@ -1093,10 +1750,15 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1093
1750
|
columns: fk.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: `foreign key "${fk.name}" column` })),
|
|
1094
1751
|
target: target.tableName,
|
|
1095
1752
|
references: fk.references.map((ref) => resolveColumnRef({ relation: target, ref, role: `foreign key "${fk.name}" reference` })),
|
|
1096
|
-
...fk.deferrable ? { deferrable: fk.deferrable } : {}
|
|
1753
|
+
...fk.deferrable ? { deferrable: fk.deferrable } : {},
|
|
1754
|
+
...fk.onDelete ? { onDelete: fk.onDelete } : {},
|
|
1755
|
+
...fk.onUpdate ? { onUpdate: fk.onUpdate } : {}
|
|
1097
1756
|
};
|
|
1098
1757
|
});
|
|
1099
1758
|
}
|
|
1759
|
+
if (table.config.checks?.length) {
|
|
1760
|
+
candidate.checks = table.config.checks.map((check) => ({ name: check.name, expression: check.expression }));
|
|
1761
|
+
}
|
|
1100
1762
|
for (const index3 of table.config.indexes ?? []) {
|
|
1101
1763
|
const entry = {
|
|
1102
1764
|
columns: index3.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
|
|
@@ -1192,7 +1854,33 @@ function buildCanonicalView({
|
|
|
1192
1854
|
columns,
|
|
1193
1855
|
...primaryKey2 !== void 0 ? { primaryKey: primaryKey2 } : {},
|
|
1194
1856
|
links,
|
|
1195
|
-
policies: { read: toCanonicalPolicy(view.config.policies.read) }
|
|
1857
|
+
policies: { read: toCanonicalPolicy(view.config.policies.read) },
|
|
1858
|
+
...view.config.resolved !== void 0 ? { resolved: buildCanonicalResolved(view.config.resolved) } : {},
|
|
1859
|
+
...view.config.internal === true ? { internal: true } : {}
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
function buildCanonicalResolved(resolved) {
|
|
1863
|
+
const policies = {};
|
|
1864
|
+
if (resolved.policies.write !== void 0) policies.write = toCanonicalPolicy(resolved.policies.write);
|
|
1865
|
+
if (resolved.policies.insert !== void 0) policies.insert = toCanonicalPolicy(resolved.policies.insert);
|
|
1866
|
+
if (resolved.policies.update !== void 0) policies.update = toCanonicalPolicy(resolved.policies.update);
|
|
1867
|
+
return {
|
|
1868
|
+
sources: resolved.sources.map((source) => ({
|
|
1869
|
+
name: source.name,
|
|
1870
|
+
kind: source.kind,
|
|
1871
|
+
table: source.table.tableName,
|
|
1872
|
+
columns: [...source.columns],
|
|
1873
|
+
...source.updatedAt !== void 0 ? { updatedAt: source.updatedAt } : {},
|
|
1874
|
+
...source.key !== void 0 ? { key: source.key } : {},
|
|
1875
|
+
...source.fetch !== void 0 ? { fetch: source.fetch } : {},
|
|
1876
|
+
...source.function !== void 0 ? { function: { ...source.function } } : {},
|
|
1877
|
+
...source.prefetchDefaults !== void 0 ? { prefetchDefaults: { ...source.prefetchDefaults } } : {},
|
|
1878
|
+
...source.writeback !== void 0 ? { writeback: { ...source.writeback, columns: [...source.writeback.columns] } } : {}
|
|
1879
|
+
})),
|
|
1880
|
+
resolve: resolved.rules.map((rule) => ({ ...rule, candidates: [...rule.candidates] })),
|
|
1881
|
+
deletedBy: [...resolved.deletedBy],
|
|
1882
|
+
...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
|
|
1883
|
+
policies
|
|
1196
1884
|
};
|
|
1197
1885
|
}
|
|
1198
1886
|
function buildCanonicalLink({
|
|
@@ -1203,14 +1891,17 @@ function buildCanonicalLink({
|
|
|
1203
1891
|
}) {
|
|
1204
1892
|
const ownerKind = relationKind(relation);
|
|
1205
1893
|
const ownerName = relationName(relation);
|
|
1206
|
-
const
|
|
1894
|
+
const fail3 = (message) => {
|
|
1207
1895
|
throw new OrmDefinitionError(`${ownerKind} "${ownerName}": link "${linkName}" ${message}`, relation.file);
|
|
1208
1896
|
};
|
|
1209
1897
|
if (link.enforce && link.linkKind !== "toOne") {
|
|
1210
|
-
return
|
|
1898
|
+
return fail3("sets enforce: true, which is only supported on toOne links");
|
|
1899
|
+
}
|
|
1900
|
+
if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && !link.enforce) {
|
|
1901
|
+
return fail3("sets a referential action without enforce: true");
|
|
1211
1902
|
}
|
|
1212
1903
|
if (link.linkKind === "toPlatformUser") {
|
|
1213
|
-
if (link.target !== void 0) return
|
|
1904
|
+
if (link.target !== void 0) return fail3("is toPlatformUser and must not declare a target table");
|
|
1214
1905
|
return {
|
|
1215
1906
|
name: linkName,
|
|
1216
1907
|
kind: "toPlatformUser",
|
|
@@ -1220,13 +1911,13 @@ function buildCanonicalLink({
|
|
|
1220
1911
|
...link.description !== void 0 ? { description: link.description } : {}
|
|
1221
1912
|
};
|
|
1222
1913
|
}
|
|
1223
|
-
const target = resolveRelationRef({ ref: link.target, fail:
|
|
1914
|
+
const target = resolveRelationRef({ ref: link.target, fail: fail3 });
|
|
1224
1915
|
const targetName = relationName(target);
|
|
1225
1916
|
if (!relationsByName.has(targetName)) {
|
|
1226
|
-
return
|
|
1917
|
+
return fail3(`targets ${relationKind(target)} "${targetName}" which is not part of this namespace`);
|
|
1227
1918
|
}
|
|
1228
1919
|
if (link.enforce && (!isSeqTable(relation) || !isSeqTable(target))) {
|
|
1229
|
-
return
|
|
1920
|
+
return fail3("cannot enforce a PostgreSQL foreign key involving a view");
|
|
1230
1921
|
}
|
|
1231
1922
|
const owningSide = link.linkKind === "toOne" ? relation : target;
|
|
1232
1923
|
const referencedSide = link.linkKind === "toOne" ? target : relation;
|
|
@@ -1237,22 +1928,24 @@ function buildCanonicalLink({
|
|
|
1237
1928
|
on: resolveColumnRef({ relation: owningSide, ref: link.on, role: `link "${linkName}" on` }),
|
|
1238
1929
|
...link.references !== void 0 ? { references: resolveColumnRef({ relation: referencedSide, ref: link.references, role: `link "${linkName}" references` }) } : {},
|
|
1239
1930
|
enforce: link.enforce,
|
|
1931
|
+
...link.onDelete !== void 0 ? { onDelete: link.onDelete } : {},
|
|
1932
|
+
...link.onUpdate !== void 0 ? { onUpdate: link.onUpdate } : {},
|
|
1240
1933
|
...link.description !== void 0 ? { description: link.description } : {}
|
|
1241
1934
|
};
|
|
1242
1935
|
}
|
|
1243
|
-
function resolveRelationRef({ ref, fail:
|
|
1244
|
-
if (ref === void 0) return
|
|
1936
|
+
function resolveRelationRef({ ref, fail: fail3 }) {
|
|
1937
|
+
if (ref === void 0) return fail3("declares no target relation");
|
|
1245
1938
|
if (typeof ref !== "function") return ref;
|
|
1246
1939
|
let resolved;
|
|
1247
1940
|
try {
|
|
1248
1941
|
resolved = ref();
|
|
1249
1942
|
} catch (error) {
|
|
1250
|
-
return
|
|
1943
|
+
return fail3(
|
|
1251
1944
|
`target thunk threw (${error instanceof Error ? error.message : String(error)}) \u2014 lazy refs resolve at serialize time; make sure the target module ran`
|
|
1252
1945
|
);
|
|
1253
1946
|
}
|
|
1254
1947
|
if (!isSeqTable(resolved) && !isSeqView(resolved)) {
|
|
1255
|
-
return
|
|
1948
|
+
return fail3("target thunk did not return a relation defined with defineTable/defineView");
|
|
1256
1949
|
}
|
|
1257
1950
|
return resolved;
|
|
1258
1951
|
}
|
|
@@ -1552,6 +2245,7 @@ function tableNames(table) {
|
|
|
1552
2245
|
updateByPkRoot: `update${rootPascal}ByPk`,
|
|
1553
2246
|
deleteRoot: `delete${rootPascal}`,
|
|
1554
2247
|
deleteByPkRoot: `delete${rootPascal}ByPk`,
|
|
2248
|
+
revertByPkRoot: `revert${rootPascal}ByPk`,
|
|
1555
2249
|
boolExp: `${type}BoolExp`,
|
|
1556
2250
|
orderBy: `${type}OrderBy`,
|
|
1557
2251
|
selectColumn: `${type}SelectColumn`,
|
|
@@ -1577,6 +2271,17 @@ function tableNames(table) {
|
|
|
1577
2271
|
aggregateGroup: `${type}AggregateGroup`
|
|
1578
2272
|
};
|
|
1579
2273
|
}
|
|
2274
|
+
var CELL_TYPENAMES = {
|
|
2275
|
+
/** A plain table's cell: when and by whom it was last written. */
|
|
2276
|
+
stamp: "CellStamp",
|
|
2277
|
+
/** A resolved table's cell: which source won, when, by whom, and newer losers. */
|
|
2278
|
+
resolved: "ResolvedCell",
|
|
2279
|
+
/** A newer candidate that lost to the column's rule. */
|
|
2280
|
+
conflict: "CellConflict"
|
|
2281
|
+
};
|
|
2282
|
+
function cellsTypename(typename) {
|
|
2283
|
+
return `${typename}Cells`;
|
|
2284
|
+
}
|
|
1580
2285
|
|
|
1581
2286
|
// src/codegen-gql/type-policies.ts
|
|
1582
2287
|
function generateTypePolicies(canonical) {
|
|
@@ -1584,13 +2289,13 @@ function generateTypePolicies(canonical) {
|
|
|
1584
2289
|
const entries = [];
|
|
1585
2290
|
let hasPlatformUser = false;
|
|
1586
2291
|
for (const table of canonicalRelations(def)) {
|
|
1587
|
-
if (table.source !== void 0) continue;
|
|
2292
|
+
if (table.source !== void 0 || table.internal === true) continue;
|
|
1588
2293
|
if (table.links.some((link) => link.kind === "toPlatformUser")) hasPlatformUser = true;
|
|
1589
2294
|
if (!table.primaryKey || table.primaryKey.length === 0) continue;
|
|
1590
|
-
const keyFields = table.primaryKey.map((
|
|
1591
|
-
const column = table.columns.find((c) => c.columnName ===
|
|
2295
|
+
const keyFields = table.primaryKey.map((identifier3) => {
|
|
2296
|
+
const column = table.columns.find((c) => c.columnName === identifier3 || c.name === identifier3);
|
|
1592
2297
|
if (!column) {
|
|
1593
|
-
throw new CodegenGqlError(`Primary key column "${
|
|
2298
|
+
throw new CodegenGqlError(`Primary key column "${identifier3}" not found on relation "${table.table}".`);
|
|
1594
2299
|
}
|
|
1595
2300
|
return fieldName(column.name);
|
|
1596
2301
|
});
|
|
@@ -1607,6 +2312,88 @@ ${body}
|
|
|
1607
2312
|
}
|
|
1608
2313
|
`;
|
|
1609
2314
|
}
|
|
2315
|
+
|
|
2316
|
+
// src/codegen-gql/source-contracts.ts
|
|
2317
|
+
var TS_TYPE = {
|
|
2318
|
+
uuid: "string",
|
|
2319
|
+
text: "string",
|
|
2320
|
+
integer: "number",
|
|
2321
|
+
bigint: "number | string",
|
|
2322
|
+
numeric: "number | string",
|
|
2323
|
+
double: "number | string",
|
|
2324
|
+
boolean: "boolean",
|
|
2325
|
+
timestamptz: "string",
|
|
2326
|
+
date: "string",
|
|
2327
|
+
jsonb: "unknown"
|
|
2328
|
+
};
|
|
2329
|
+
function tsType(column, enums) {
|
|
2330
|
+
if (typeof column.type === "string") return TS_TYPE[column.type] ?? "unknown";
|
|
2331
|
+
const values = enums.get(column.type.enum) ?? [];
|
|
2332
|
+
return values.length > 0 ? values.map((value) => JSON.stringify(value)).join(" | ") : "string";
|
|
2333
|
+
}
|
|
2334
|
+
function generateSourceContracts(canonical) {
|
|
2335
|
+
const def = normalizeCanonical(canonicalDefinitionSchema.parse(canonical));
|
|
2336
|
+
const enums = new Map(def.enums.map((entry) => [entry.name, entry.values]));
|
|
2337
|
+
const blocks = [];
|
|
2338
|
+
for (const view of def.views ?? []) {
|
|
2339
|
+
for (const source of view.resolved?.sources ?? []) {
|
|
2340
|
+
if (source.kind !== "prefetch") continue;
|
|
2341
|
+
const name = `${pascalCase(view.view)}${pascalCase(source.name)}`;
|
|
2342
|
+
const byName = new Map(view.columns.map((column) => [column.name, column]));
|
|
2343
|
+
const key = byName.get(source.key ?? "");
|
|
2344
|
+
const valueLines = source.columns.map((column) => {
|
|
2345
|
+
const def2 = byName.get(column);
|
|
2346
|
+
return ` ${column}?: ${def2 ? tsType(def2, enums) : "unknown"} | null`;
|
|
2347
|
+
});
|
|
2348
|
+
const writable = source.writeback?.columns ?? [];
|
|
2349
|
+
blocks.push([
|
|
2350
|
+
`/** ${view.view} \u2190 ${source.name} (${source.function?.kind ?? "managed"} function '${source.function?.id ?? ""}'). */`,
|
|
2351
|
+
`export type ${name}Key = ${key ? tsType(key, enums) : "string"}`,
|
|
2352
|
+
`export interface ${name}Values {
|
|
2353
|
+
${valueLines.join("\n")}
|
|
2354
|
+
}`,
|
|
2355
|
+
`export interface ${name}PrefetchRequest {
|
|
2356
|
+
action: 'prefetch'
|
|
2357
|
+
namespace: string
|
|
2358
|
+
table: '${view.view}'
|
|
2359
|
+
source: '${source.name}'
|
|
2360
|
+
keys: ${name}Key[]
|
|
2361
|
+
columns: string[]
|
|
2362
|
+
actor: { sub: string }
|
|
2363
|
+
}`,
|
|
2364
|
+
`export interface ${name}PrefetchResponse {
|
|
2365
|
+
rows: Array<{ key: ${name}Key; values: ${name}Values; cells?: Partial<Record<keyof ${name}Values, string>>; sourceUpdatedAt?: string }>
|
|
2366
|
+
missing?: ${name}Key[]
|
|
2367
|
+
}`,
|
|
2368
|
+
...writable.length > 0 ? [
|
|
2369
|
+
`export type ${name}WritebackColumn = ${writable.map((column) => JSON.stringify(column)).join(" | ")}`,
|
|
2370
|
+
`export interface ${name}WritebackRequest {
|
|
2371
|
+
action: 'writeback'
|
|
2372
|
+
namespace: string
|
|
2373
|
+
table: '${view.view}'
|
|
2374
|
+
source: '${source.name}'
|
|
2375
|
+
key: ${name}Key
|
|
2376
|
+
changes: Partial<Pick<${name}Values, ${name}WritebackColumn>>
|
|
2377
|
+
before: Partial<Pick<${name}Values, ${name}WritebackColumn>>
|
|
2378
|
+
idempotencyKey: string
|
|
2379
|
+
actor: { sub: string }
|
|
2380
|
+
}`,
|
|
2381
|
+
`/** Respond 200 with this; respond 400/409/422 with { detail } when the source system refuses the change. */
|
|
2382
|
+
export interface ${name}WritebackResponse {
|
|
2383
|
+
values?: ${name}Values
|
|
2384
|
+
cells?: Partial<Record<keyof ${name}Values, string>>
|
|
2385
|
+
sourceUpdatedAt?: string
|
|
2386
|
+
}`
|
|
2387
|
+
] : []
|
|
2388
|
+
].join("\n\n"));
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
if (blocks.length === 0) return null;
|
|
2392
|
+
return `/* Generated by @sequenceholdings/orm \u2014 resolved-table source function contracts. Do not edit. */
|
|
2393
|
+
|
|
2394
|
+
${blocks.join("\n\n")}
|
|
2395
|
+
`;
|
|
2396
|
+
}
|
|
1610
2397
|
var IR_VERSION = 2;
|
|
1611
2398
|
var LEGACY_IR_VERSION = 1;
|
|
1612
2399
|
var irVersionSchema = zod.z.union([zod.z.literal(LEGACY_IR_VERSION), zod.z.literal(IR_VERSION)]);
|
|
@@ -1690,6 +2477,21 @@ var selectionIrSchema = zod.z.lazy(
|
|
|
1690
2477
|
() => zod.z.object({
|
|
1691
2478
|
typename: zod.z.string(),
|
|
1692
2479
|
columns: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })),
|
|
2480
|
+
cells: zod.z.object({
|
|
2481
|
+
alias: zod.z.string(),
|
|
2482
|
+
typename: zod.z.string(),
|
|
2483
|
+
fields: zod.z.array(zod.z.object({
|
|
2484
|
+
alias: zod.z.string(),
|
|
2485
|
+
column: zod.z.string(),
|
|
2486
|
+
typename: zod.z.string(),
|
|
2487
|
+
parts: zod.z.array(zod.z.object({
|
|
2488
|
+
alias: zod.z.string(),
|
|
2489
|
+
part: zod.z.enum(["source", "at", "by", "conflicts"]),
|
|
2490
|
+
fields: zod.z.array(zod.z.object({ alias: zod.z.string(), field: zod.z.enum(["source", "at", "value"]) })).optional(),
|
|
2491
|
+
typename: zod.z.string().optional()
|
|
2492
|
+
}))
|
|
2493
|
+
}))
|
|
2494
|
+
}).optional(),
|
|
1693
2495
|
relations: zod.z.array(zod.z.object({
|
|
1694
2496
|
alias: zod.z.string(),
|
|
1695
2497
|
link: zod.z.string(),
|
|
@@ -1711,6 +2513,11 @@ var selectionIrSchema = zod.z.lazy(
|
|
|
1711
2513
|
})
|
|
1712
2514
|
);
|
|
1713
2515
|
var includeIfSchema = zod.z.object({ variable: zod.z.string(), negated: zod.z.boolean() });
|
|
2516
|
+
var prefetchPlanIrSchema = zod.z.object({
|
|
2517
|
+
source: zod.z.string(),
|
|
2518
|
+
maxStalenessMs: zod.z.number().int().positive().optional(),
|
|
2519
|
+
key: zod.z.union([zod.z.object({ one: valueIrSchema }), zod.z.object({ many: valueIrSchema })])
|
|
2520
|
+
});
|
|
1714
2521
|
var selectRootSchema = zod.z.object({
|
|
1715
2522
|
kind: zod.z.literal("select"),
|
|
1716
2523
|
alias: zod.z.string(),
|
|
@@ -1722,7 +2529,8 @@ var selectRootSchema = zod.z.object({
|
|
|
1722
2529
|
distinctOn: zod.z.union([zod.z.array(zod.z.string()), zod.z.object({ slot: zod.z.string() })]).optional(),
|
|
1723
2530
|
limit: limitIrSchema,
|
|
1724
2531
|
offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
|
|
1725
|
-
includeIf: includeIfSchema.optional()
|
|
2532
|
+
includeIf: includeIfSchema.optional(),
|
|
2533
|
+
prefetch: zod.z.array(prefetchPlanIrSchema).optional()
|
|
1726
2534
|
});
|
|
1727
2535
|
var byPkRootSchema = zod.z.object({
|
|
1728
2536
|
kind: zod.z.literal("byPk"),
|
|
@@ -1732,7 +2540,8 @@ var byPkRootSchema = zod.z.object({
|
|
|
1732
2540
|
pk: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).min(1),
|
|
1733
2541
|
/** Policy predicate — ANDed with the PK match. */
|
|
1734
2542
|
where: predIrSchema,
|
|
1735
|
-
includeIf: includeIfSchema.optional()
|
|
2543
|
+
includeIf: includeIfSchema.optional(),
|
|
2544
|
+
prefetch: zod.z.array(prefetchPlanIrSchema).optional()
|
|
1736
2545
|
});
|
|
1737
2546
|
var aggregateRootSchema = zod.z.object({
|
|
1738
2547
|
kind: zod.z.literal("aggregate"),
|
|
@@ -1790,7 +2599,20 @@ var mutationRootSchema = zod.z.object({
|
|
|
1790
2599
|
* coalesce view falls back to synced. Both are gated on a matching synced
|
|
1791
2600
|
* row and carry the write policy in `where` like update/delete.
|
|
1792
2601
|
*/
|
|
1793
|
-
op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
|
|
2602
|
+
op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert", "resolvedWrite"]),
|
|
2603
|
+
/**
|
|
2604
|
+
* resolvedWrite: a write to a resolved table (`table` is its view). The
|
|
2605
|
+
* executor gates on the resolved row (write policy in `where`), writes only
|
|
2606
|
+
* the touched cells into the manual source, then re-checks `insertCheck`
|
|
2607
|
+
* against the resolved row. `revert` clears `revertColumns` from the manual
|
|
2608
|
+
* source; `delete` tombstones the row there.
|
|
2609
|
+
*/
|
|
2610
|
+
resolvedAction: zod.z.enum(["insert", "update", "delete", "revert"]).optional(),
|
|
2611
|
+
revertColumns: zod.z.array(zod.z.string()).optional(),
|
|
2612
|
+
/** `@writeback(source:, mode:)` directives on this root. */
|
|
2613
|
+
writeback: zod.z.array(zod.z.object({ source: zod.z.string(), mode: zod.z.enum(["sync", "async"]) })).optional(),
|
|
2614
|
+
/** `@localOnly(source:)` — writeback columns deliberately kept local for these sources. */
|
|
2615
|
+
localOnly: zod.z.array(zod.z.string()).optional(),
|
|
1794
2616
|
alias: zod.z.string(),
|
|
1795
2617
|
table: zod.z.string(),
|
|
1796
2618
|
/** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
|
|
@@ -1875,6 +2697,43 @@ var compiledOperationSchema = zod.z.object({
|
|
|
1875
2697
|
}
|
|
1876
2698
|
}
|
|
1877
2699
|
});
|
|
2700
|
+
var resolvedSourceIrSchema = zod.z.object({
|
|
2701
|
+
name: zod.z.string(),
|
|
2702
|
+
kind: zod.z.enum(["manual", "etl", "prefetch"]),
|
|
2703
|
+
/** Logical table key of the backing relation. */
|
|
2704
|
+
table: zod.z.string(),
|
|
2705
|
+
columns: zod.z.array(zod.z.string()),
|
|
2706
|
+
key: zod.z.string().optional(),
|
|
2707
|
+
fetch: zod.z.union([
|
|
2708
|
+
zod.z.object({ shape: zod.z.literal("single") }),
|
|
2709
|
+
zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().positive() })
|
|
2710
|
+
]).optional(),
|
|
2711
|
+
function: zod.z.object({ kind: zod.z.enum(["managed", "native"]), id: zod.z.string() }).optional(),
|
|
2712
|
+
prefetchDefaults: zod.z.object({
|
|
2713
|
+
maxStalenessMs: zod.z.number().int().positive(),
|
|
2714
|
+
timeoutMs: zod.z.number().int().positive(),
|
|
2715
|
+
onError: zod.z.enum(["fallback", "fail"])
|
|
2716
|
+
}).optional(),
|
|
2717
|
+
writeback: zod.z.object({
|
|
2718
|
+
columns: zod.z.array(zod.z.string()),
|
|
2719
|
+
defaultMode: zod.z.enum(["sync", "async"]),
|
|
2720
|
+
timeoutMs: zod.z.number().int().positive(),
|
|
2721
|
+
clearManualOnConfirm: zod.z.boolean()
|
|
2722
|
+
}).optional()
|
|
2723
|
+
});
|
|
2724
|
+
var resolvedMetaIrSchema = zod.z.object({
|
|
2725
|
+
/** Logical pk column of the resolved view (single-column). */
|
|
2726
|
+
pk: zod.z.string(),
|
|
2727
|
+
/** Source name of the manual source. */
|
|
2728
|
+
manual: zod.z.string(),
|
|
2729
|
+
sources: zod.z.array(resolvedSourceIrSchema),
|
|
2730
|
+
deletedBy: zod.z.array(zod.z.string()),
|
|
2731
|
+
/**
|
|
2732
|
+
* Materialized: the view reads `table`, which the data plane refreshes from
|
|
2733
|
+
* the `compute` view (claims-gated) whenever it writes a source row.
|
|
2734
|
+
*/
|
|
2735
|
+
materialized: zod.z.object({ table: zod.z.string(), compute: zod.z.string() }).optional()
|
|
2736
|
+
});
|
|
1878
2737
|
var tableMetaIrSchema = zod.z.object({
|
|
1879
2738
|
table: zod.z.string(),
|
|
1880
2739
|
schema: zod.z.string(),
|
|
@@ -1915,6 +2774,22 @@ var tableMetaIrSchema = zod.z.object({
|
|
|
1915
2774
|
* `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
|
|
1916
2775
|
*/
|
|
1917
2776
|
touchOnUpdate: zod.z.array(zod.z.string()).optional(),
|
|
2777
|
+
/**
|
|
2778
|
+
* Per-cell metadata: logical column → the logical names of its cell-part
|
|
2779
|
+
* columns (which are listed in `columns`, but never filterable, orderable or
|
|
2780
|
+
* writable). Tables stamp `at`/`by` (and a manual source `rev`) on every
|
|
2781
|
+
* write; resolved views expose `source`/`at`/`by`/`conflicts`. Omitted when
|
|
2782
|
+
* the relation tracks no cells.
|
|
2783
|
+
*/
|
|
2784
|
+
cells: zod.z.record(zod.z.string(), zod.z.object({
|
|
2785
|
+
at: zod.z.string().optional(),
|
|
2786
|
+
by: zod.z.string().optional(),
|
|
2787
|
+
rev: zod.z.string().optional(),
|
|
2788
|
+
source: zod.z.string().optional(),
|
|
2789
|
+
conflicts: zod.z.string().optional()
|
|
2790
|
+
}).strict()).optional(),
|
|
2791
|
+
/** Resolved-table view metadata (sources, prefetch/writeback config) for the data plane. */
|
|
2792
|
+
resolved: resolvedMetaIrSchema.optional(),
|
|
1918
2793
|
constraints: zod.z.array(zod.z.object({ name: zod.z.string(), columns: zod.z.array(zod.z.string()) })),
|
|
1919
2794
|
/** Depth/node caps for dynamic filter trees on this table. */
|
|
1920
2795
|
dynamicFilterMaxDepth: zod.z.number().int().positive().default(6),
|
|
@@ -1983,7 +2858,7 @@ var IDENTIFIER_CHAR = /[a-zA-Z0-9_]/;
|
|
|
1983
2858
|
var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
|
|
1984
2859
|
var SCALAR_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => !type.endsWith("[]")).map(([key]) => key);
|
|
1985
2860
|
var ARRAY_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => type.endsWith("[]")).map(([key]) => key);
|
|
1986
|
-
function
|
|
2861
|
+
function tokenize2(source) {
|
|
1987
2862
|
const tokens = [];
|
|
1988
2863
|
let position = 0;
|
|
1989
2864
|
while (position < source.length) {
|
|
@@ -2062,7 +2937,7 @@ function readString(source, start) {
|
|
|
2062
2937
|
}
|
|
2063
2938
|
throw new PolicyParseError("unterminated string literal", start);
|
|
2064
2939
|
}
|
|
2065
|
-
var
|
|
2940
|
+
var Parser2 = class {
|
|
2066
2941
|
constructor(tokens) {
|
|
2067
2942
|
this.tokens = tokens;
|
|
2068
2943
|
}
|
|
@@ -2236,7 +3111,7 @@ var Parser = class {
|
|
|
2236
3111
|
function parsePolicy(source) {
|
|
2237
3112
|
const trimmed = source.trim();
|
|
2238
3113
|
if (trimmed.length === 0) throw new PolicyParseError("empty policy expression", 0);
|
|
2239
|
-
return { source: trimmed, ast: new
|
|
3114
|
+
return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
|
|
2240
3115
|
}
|
|
2241
3116
|
|
|
2242
3117
|
// src/predicate-v2/compile.ts
|
|
@@ -2628,6 +3503,7 @@ var PolicyInjector = class {
|
|
|
2628
3503
|
injectMutation(root) {
|
|
2629
3504
|
const table = root.table;
|
|
2630
3505
|
const returning = root.returning ? this.injectSelection(root.returning, table) : root.returning;
|
|
3506
|
+
if (root.op === "resolvedWrite") return this.injectResolvedWrite(root, returning);
|
|
2631
3507
|
if (root.op === "insert" || root.op === "insertOne") {
|
|
2632
3508
|
const insertPolicy = this.insert(table);
|
|
2633
3509
|
if (predContainsExists(insertPolicy)) {
|
|
@@ -2669,6 +3545,24 @@ var PolicyInjector = class {
|
|
|
2669
3545
|
}
|
|
2670
3546
|
return { ...root, where, returning };
|
|
2671
3547
|
}
|
|
3548
|
+
/**
|
|
3549
|
+
* Resolved writes are policed against the RESOLVED row, not the sparse
|
|
3550
|
+
* manual row: `where` gates the existing row (update/revert/delete) and
|
|
3551
|
+
* `insertCheck` is re-evaluated as a SELECT over the view after the write,
|
|
3552
|
+
* so link-traversing (exists) policies are representable here.
|
|
3553
|
+
*/
|
|
3554
|
+
injectResolvedWrite(root, returning) {
|
|
3555
|
+
const table = root.table;
|
|
3556
|
+
if (root.resolvedAction === "insert") {
|
|
3557
|
+
const pins = (this.ctx.pins.get(table) ?? []).map(
|
|
3558
|
+
(pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
|
|
3559
|
+
);
|
|
3560
|
+
return { ...root, pinnedColumns: pins, insertCheck: this.insert(table), returning };
|
|
3561
|
+
}
|
|
3562
|
+
const updatePolicy = this.update(table);
|
|
3563
|
+
const where = andPred(updatePolicy, root.where ? this.injectDocPred(root.where, table) : void 0);
|
|
3564
|
+
return root.resolvedAction === "delete" ? { ...root, where, returning } : { ...root, where, insertCheck: updatePolicy, returning };
|
|
3565
|
+
}
|
|
2672
3566
|
};
|
|
2673
3567
|
function collectTableSet(roots, ctx) {
|
|
2674
3568
|
const tables = /* @__PURE__ */ new Set();
|
|
@@ -2728,6 +3622,7 @@ function collectTableSet(roots, ctx) {
|
|
|
2728
3622
|
break;
|
|
2729
3623
|
case "mutation":
|
|
2730
3624
|
if (root.cowSynced !== void 0) tables.add(root.cowSynced);
|
|
3625
|
+
for (const source of ctx.tables[root.table]?.resolved?.sources ?? []) tables.add(source.table);
|
|
2731
3626
|
if (root.where) visitPred(root.where, root.table);
|
|
2732
3627
|
if (root.insertCheck) visitPred(root.insertCheck, root.table);
|
|
2733
3628
|
if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
|
|
@@ -2849,7 +3744,8 @@ function resolveColumnEntries(table, enums, diagnostics) {
|
|
|
2849
3744
|
type,
|
|
2850
3745
|
nullable: column.nullable,
|
|
2851
3746
|
deprecated: column.deprecated,
|
|
2852
|
-
onUpdateFn: column.onUpdateFn !== void 0
|
|
3747
|
+
onUpdateFn: column.onUpdateFn !== void 0,
|
|
3748
|
+
...column.cell ? { cell: column.cell } : {}
|
|
2853
3749
|
};
|
|
2854
3750
|
});
|
|
2855
3751
|
}
|
|
@@ -2886,8 +3782,8 @@ function buildCompileContext(rawCanonical) {
|
|
|
2886
3782
|
const pkRefs = table.primaryKey ?? (isView ? [] : ["id"]);
|
|
2887
3783
|
const primaryKey2 = [];
|
|
2888
3784
|
for (const ref of pkRefs) {
|
|
2889
|
-
const
|
|
2890
|
-
if (!
|
|
3785
|
+
const resolved2 = logicalColumn(entries, ref);
|
|
3786
|
+
if (!resolved2) {
|
|
2891
3787
|
diagnostics.push({
|
|
2892
3788
|
severity: "error",
|
|
2893
3789
|
code: "unknown-pk-column",
|
|
@@ -2896,9 +3792,12 @@ function buildCompileContext(rawCanonical) {
|
|
|
2896
3792
|
});
|
|
2897
3793
|
continue;
|
|
2898
3794
|
}
|
|
2899
|
-
primaryKey2.push(
|
|
3795
|
+
primaryKey2.push(resolved2);
|
|
2900
3796
|
}
|
|
2901
|
-
const
|
|
3797
|
+
const resolved = isView ? table.resolved : void 0;
|
|
3798
|
+
const data = entries.filter((c) => c.cell === void 0);
|
|
3799
|
+
const cells = cellMap(entries);
|
|
3800
|
+
const active = data.filter((c) => !c.deprecated);
|
|
2902
3801
|
const touchOnUpdate = isView ? [] : active.filter((c) => c.onUpdateFn).map((c) => c.name);
|
|
2903
3802
|
const constraints = [];
|
|
2904
3803
|
if (primaryKey2.length > 0) constraints.push({ name: `${table.table}_pkey`, columns: [...primaryKey2] });
|
|
@@ -2923,12 +3822,14 @@ function buildCompileContext(rawCanonical) {
|
|
|
2923
3822
|
columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
|
|
2924
3823
|
primaryKey: isView ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
|
|
2925
3824
|
links: [],
|
|
2926
|
-
filterable:
|
|
2927
|
-
orderable:
|
|
2928
|
-
insertable: isView ? [] : active.map((c) => c.name),
|
|
2929
|
-
settable: isView ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
|
|
3825
|
+
filterable: data.map((c) => c.name),
|
|
3826
|
+
orderable: data.filter((c) => c.type !== "jsonb").map((c) => c.name),
|
|
3827
|
+
insertable: resolved ? resolvedWritable(data) : isView ? [] : active.map((c) => c.name),
|
|
3828
|
+
settable: resolved ? resolvedWritable(data).filter((name) => !primaryKey2.includes(name)) : isView ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
|
|
2930
3829
|
appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
|
|
2931
3830
|
...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
|
|
3831
|
+
...cells ? { cells } : {},
|
|
3832
|
+
...resolved ? { resolved: resolvedMeta({ resolved, pk: primaryKey2[0] }) } : {},
|
|
2932
3833
|
constraints: isView ? [] : constraints,
|
|
2933
3834
|
dynamicFilterMaxDepth: 6,
|
|
2934
3835
|
dynamicFilterMaxNodes: 64
|
|
@@ -3001,7 +3902,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
3001
3902
|
compilePolicyFor("write", writePolicies);
|
|
3002
3903
|
compilePolicyFor("insert", insertPolicies);
|
|
3003
3904
|
compilePolicyFor("update", updatePolicies);
|
|
3004
|
-
if (isView) {
|
|
3905
|
+
if (isView && table.resolved === void 0) {
|
|
3005
3906
|
writePolicies.set(table.table, CONST_FALSE);
|
|
3006
3907
|
insertPolicies.set(table.table, CONST_FALSE);
|
|
3007
3908
|
updatePolicies.set(table.table, CONST_FALSE);
|
|
@@ -3040,6 +3941,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
3040
3941
|
const queryRoots = /* @__PURE__ */ new Map();
|
|
3041
3942
|
const mutationRoots = /* @__PURE__ */ new Map();
|
|
3042
3943
|
for (const table of relations) {
|
|
3944
|
+
if (table.internal === true) continue;
|
|
3043
3945
|
const roots = queryRootNames(table.table);
|
|
3044
3946
|
queryRoots.set(roots.select, { kind: "select", table: table.table });
|
|
3045
3947
|
if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
|
|
@@ -3055,6 +3957,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
3055
3957
|
mutationRoots.set(field, { op, table: table.table });
|
|
3056
3958
|
}
|
|
3057
3959
|
}
|
|
3960
|
+
if (table.resolved !== void 0) registerResolvedRoots({ table, mutationRoots });
|
|
3058
3961
|
}
|
|
3059
3962
|
for (const table of relations) {
|
|
3060
3963
|
for (const mutation of table.cowMutations ?? []) {
|
|
@@ -3125,6 +4028,49 @@ function buildCompileContext(rawCanonical) {
|
|
|
3125
4028
|
diagnostics
|
|
3126
4029
|
};
|
|
3127
4030
|
}
|
|
4031
|
+
function registerResolvedRoots({ table, mutationRoots }) {
|
|
4032
|
+
const names = tableNames(table.table);
|
|
4033
|
+
const canInsert = table.policies.write !== void 0 || table.policies.insert !== void 0;
|
|
4034
|
+
const canUpdate = table.policies.write !== void 0 || table.policies.update !== void 0;
|
|
4035
|
+
const add = (field, action) => mutationRoots.set(field, { op: "resolvedWrite", action, table: table.table });
|
|
4036
|
+
if (canInsert) add(names.insertOneRoot, "insert");
|
|
4037
|
+
if (canUpdate) {
|
|
4038
|
+
add(names.updateByPkRoot, "update");
|
|
4039
|
+
add(names.revertByPkRoot, "revert");
|
|
4040
|
+
if ((table.resolved?.deletedBy.length ?? 0) > 0) add(names.deleteByPkRoot, "delete");
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
function cellMap(entries) {
|
|
4044
|
+
const cells = {};
|
|
4045
|
+
for (const entry of entries) {
|
|
4046
|
+
if (entry.cell) cells[entry.cell.of] = { ...cells[entry.cell.of], [entry.cell.part]: entry.name };
|
|
4047
|
+
}
|
|
4048
|
+
return Object.keys(cells).length > 0 ? cells : void 0;
|
|
4049
|
+
}
|
|
4050
|
+
function resolvedWritable(entries) {
|
|
4051
|
+
const system = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((column) => column.name));
|
|
4052
|
+
return entries.filter((entry) => !entry.deprecated && !system.has(entry.name)).map((entry) => entry.name);
|
|
4053
|
+
}
|
|
4054
|
+
function resolvedMeta({ resolved, pk }) {
|
|
4055
|
+
const manual = resolved.sources.find((source) => source.kind === "manual");
|
|
4056
|
+
return {
|
|
4057
|
+
pk,
|
|
4058
|
+
manual: manual?.name ?? "",
|
|
4059
|
+
deletedBy: [...resolved.deletedBy],
|
|
4060
|
+
...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
|
|
4061
|
+
sources: resolved.sources.map((source) => ({
|
|
4062
|
+
name: source.name,
|
|
4063
|
+
kind: source.kind,
|
|
4064
|
+
table: source.table,
|
|
4065
|
+
columns: [...source.columns],
|
|
4066
|
+
...source.key !== void 0 ? { key: source.key } : {},
|
|
4067
|
+
...source.fetch !== void 0 ? { fetch: source.fetch } : {},
|
|
4068
|
+
...source.function !== void 0 ? { function: source.function } : {},
|
|
4069
|
+
...source.prefetchDefaults !== void 0 ? { prefetchDefaults: source.prefetchDefaults } : {},
|
|
4070
|
+
...source.writeback !== void 0 ? { writeback: source.writeback } : {}
|
|
4071
|
+
}))
|
|
4072
|
+
};
|
|
4073
|
+
}
|
|
3128
4074
|
function flattenSelectionSet(selectionSet, fragments, stack) {
|
|
3129
4075
|
const selections = [];
|
|
3130
4076
|
for (const selection of selectionSet.selections) {
|
|
@@ -3182,6 +4128,136 @@ function computeOpsHash(opHashes) {
|
|
|
3182
4128
|
const digest = crypto.createHash("sha256").update(OPS_HASH_DOMAIN).update([...opHashes].sort().join("\n")).digest("hex");
|
|
3183
4129
|
return `sha256:${digest}`;
|
|
3184
4130
|
}
|
|
4131
|
+
var SOURCE_DIRECTIVES = /* @__PURE__ */ new Set(["prefetch", "writeback", "localOnly"]);
|
|
4132
|
+
var fail = (message, code) => {
|
|
4133
|
+
throw new DocumentCompileError(message, code);
|
|
4134
|
+
};
|
|
4135
|
+
function readArgs(node, name) {
|
|
4136
|
+
return (node.directives ?? []).filter((directive) => directive.name.value === name).map((directive) => {
|
|
4137
|
+
const args = {};
|
|
4138
|
+
for (const arg of directive.arguments ?? []) {
|
|
4139
|
+
const key = arg.name.value;
|
|
4140
|
+
if (key === "source" || key === "maxStaleness") {
|
|
4141
|
+
if (arg.value.kind !== graphql.Kind.STRING) fail(`@${name}(${key}:) must be a string literal`, "invalid-source-directive");
|
|
4142
|
+
args[key] = arg.value.value;
|
|
4143
|
+
} else if (key === "mode" && name === "writeback") {
|
|
4144
|
+
if (arg.value.kind !== graphql.Kind.ENUM || !["SYNC", "ASYNC"].includes(arg.value.value)) {
|
|
4145
|
+
fail("@writeback(mode:) must be SYNC or ASYNC", "invalid-source-directive");
|
|
4146
|
+
}
|
|
4147
|
+
args.mode = arg.value.value === "SYNC" ? "sync" : "async";
|
|
4148
|
+
} else {
|
|
4149
|
+
fail(`@${name} does not take argument '${key}'`, "invalid-source-directive");
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
if (!args.source) fail(`@${name} needs a source: argument`, "invalid-source-directive");
|
|
4153
|
+
return args;
|
|
4154
|
+
});
|
|
4155
|
+
}
|
|
4156
|
+
function prefetchSourceFor({ resolved, name, directive, table }) {
|
|
4157
|
+
if (!resolved) return fail(`@${directive} is only valid on resolved tables \u2014 '${table}' is not one`, "invalid-source-directive");
|
|
4158
|
+
const source = resolved.sources.find((entry) => entry.name === name);
|
|
4159
|
+
if (!source || source.kind !== "prefetch") {
|
|
4160
|
+
const options = resolved.sources.filter((entry) => entry.kind === "prefetch").map((entry) => entry.name);
|
|
4161
|
+
return fail(`@${directive}(source: "${name}") on '${table}' must name a prefetch source (${options.join(", ") || "none declared"})`, "invalid-source-directive");
|
|
4162
|
+
}
|
|
4163
|
+
return source;
|
|
4164
|
+
}
|
|
4165
|
+
function applyPrefetchDirectives({ node, root, meta }) {
|
|
4166
|
+
const directives = readArgs(node, "prefetch");
|
|
4167
|
+
for (const name of ["writeback", "localOnly"]) {
|
|
4168
|
+
if (readArgs(node, name).length > 0) fail(`@${name} is only valid on resolved-table mutation roots`, "invalid-source-directive");
|
|
4169
|
+
}
|
|
4170
|
+
if (directives.length === 0) return root;
|
|
4171
|
+
if (root.kind !== "select" && root.kind !== "byPk") {
|
|
4172
|
+
return fail(`@prefetch is only valid on select and byPk roots, not '${root.alias}'`, "invalid-source-directive");
|
|
4173
|
+
}
|
|
4174
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4175
|
+
const plans = directives.map((directive) => {
|
|
4176
|
+
const source = prefetchSourceFor({ resolved: meta.resolved, name: directive.source, directive: "prefetch", table: root.table });
|
|
4177
|
+
if (seen.has(source.name)) fail(`@prefetch(source: "${source.name}") appears twice on '${root.alias}'`, "invalid-source-directive");
|
|
4178
|
+
seen.add(source.name);
|
|
4179
|
+
const maxStalenessMs = directive.maxStaleness === void 0 ? void 0 : parseDurationMs(directive.maxStaleness);
|
|
4180
|
+
if (directive.maxStaleness !== void 0 && maxStalenessMs === void 0) {
|
|
4181
|
+
fail(`@prefetch(maxStaleness: "${directive.maxStaleness}") must look like '500ms', '30s' or '5m'`, "invalid-source-directive");
|
|
4182
|
+
}
|
|
4183
|
+
const key = root.kind === "byPk" ? { one: root.pk[0].value } : selectKey({ where: root.where, pk: meta.resolved.pk, source, alias: root.alias });
|
|
4184
|
+
return { source: source.name, ...maxStalenessMs !== void 0 ? { maxStalenessMs } : {}, key };
|
|
4185
|
+
});
|
|
4186
|
+
return { ...root, prefetch: plans };
|
|
4187
|
+
}
|
|
4188
|
+
function selectKey({ where, pk, source, alias }) {
|
|
4189
|
+
const leaves = where.kind === "and" ? where.operands : [where];
|
|
4190
|
+
const pinned = leaves.find((leaf) => leaf.kind === "cmp" && leaf.column === pk && (leaf.op === "eq" || leaf.op === "in"));
|
|
4191
|
+
const shape = source.fetch?.shape ?? "single";
|
|
4192
|
+
if (!pinned || pinned.kind !== "cmp") {
|
|
4193
|
+
return fail(
|
|
4194
|
+
`@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)`,
|
|
4195
|
+
"prefetch-shape-mismatch"
|
|
4196
|
+
);
|
|
4197
|
+
}
|
|
4198
|
+
if (pinned.op === "in" && shape !== "batch") {
|
|
4199
|
+
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");
|
|
4200
|
+
}
|
|
4201
|
+
return pinned.op === "eq" ? { one: pinned.value } : { many: pinned.value };
|
|
4202
|
+
}
|
|
4203
|
+
function applyWritebackDirectives({ node, root, meta }) {
|
|
4204
|
+
if (readArgs(node, "prefetch").length > 0) fail("@prefetch is only valid on query roots", "invalid-source-directive");
|
|
4205
|
+
const writebacks = readArgs(node, "writeback");
|
|
4206
|
+
const localOnly = readArgs(node, "localOnly");
|
|
4207
|
+
if (root.kind !== "mutation" || root.op !== "resolvedWrite" || !meta.resolved) {
|
|
4208
|
+
if (writebacks.length + localOnly.length > 0) fail("@writeback/@localOnly are only valid on resolved-table mutation roots", "invalid-source-directive");
|
|
4209
|
+
return root;
|
|
4210
|
+
}
|
|
4211
|
+
const resolved = meta.resolved;
|
|
4212
|
+
const declared = /* @__PURE__ */ new Map();
|
|
4213
|
+
const bindings = [];
|
|
4214
|
+
for (const directive of writebacks) {
|
|
4215
|
+
const source = prefetchSourceFor({ resolved, name: directive.source, directive: "writeback", table: root.table });
|
|
4216
|
+
if (!source.writeback) fail(`@writeback(source: "${source.name}"): source '${source.name}' declares no writeback columns`, "invalid-source-directive");
|
|
4217
|
+
if (root.resolvedAction !== "update") {
|
|
4218
|
+
fail(`@writeback on '${root.alias}': only update roots write back in v1 (use @localOnly for ${root.resolvedAction ?? "this"} roots)`, "writeback-shape-mismatch");
|
|
4219
|
+
}
|
|
4220
|
+
if (declared.has(source.name)) fail(`source '${source.name}' is declared twice on '${root.alias}'`, "invalid-source-directive");
|
|
4221
|
+
declared.set(source.name, "writeback");
|
|
4222
|
+
bindings.push({ source: source.name, mode: directive.mode ?? source.writeback.defaultMode });
|
|
4223
|
+
}
|
|
4224
|
+
for (const directive of localOnly) {
|
|
4225
|
+
const source = prefetchSourceFor({ resolved, name: directive.source, directive: "localOnly", table: root.table });
|
|
4226
|
+
if (declared.has(source.name)) fail(`source '${source.name}' is declared twice on '${root.alias}'`, "invalid-source-directive");
|
|
4227
|
+
declared.set(source.name, "localOnly");
|
|
4228
|
+
}
|
|
4229
|
+
assertWritebacksDeclared({ root, meta, resolved, declared });
|
|
4230
|
+
return {
|
|
4231
|
+
...root,
|
|
4232
|
+
...bindings.length > 0 ? { writeback: bindings } : {},
|
|
4233
|
+
...localOnly.length > 0 ? { localOnly: localOnly.map((directive) => directive.source) } : {}
|
|
4234
|
+
};
|
|
4235
|
+
}
|
|
4236
|
+
function assertWritebacksDeclared({ root, meta, resolved, declared }) {
|
|
4237
|
+
const written = writtenColumns({ root, meta });
|
|
4238
|
+
for (const source of resolved.sources) {
|
|
4239
|
+
const columns = source.writeback?.columns.filter((column) => written.has(column)) ?? [];
|
|
4240
|
+
if (columns.length > 0 && !declared.has(source.name)) {
|
|
4241
|
+
fail(
|
|
4242
|
+
`'${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`,
|
|
4243
|
+
"writeback-undeclared"
|
|
4244
|
+
);
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
function writtenColumns({ root, meta }) {
|
|
4249
|
+
if (root.resolvedAction === "update") {
|
|
4250
|
+
if (!root.set || "slot" in root.set) return new Set(meta.settable);
|
|
4251
|
+
return new Set(root.set.entries.map((entry) => entry.column));
|
|
4252
|
+
}
|
|
4253
|
+
if (root.resolvedAction === "insert") {
|
|
4254
|
+
if (!root.objects || "slot" in root.objects) return new Set(meta.insertable);
|
|
4255
|
+
return new Set(root.objects.rows.flatMap((row) => row.map((entry) => entry.column)));
|
|
4256
|
+
}
|
|
4257
|
+
return /* @__PURE__ */ new Set();
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4260
|
+
// src/compiler-gql/lower.ts
|
|
3185
4261
|
var DEFAULT_LIMIT = 100;
|
|
3186
4262
|
var MAX_LIMIT = 1e3;
|
|
3187
4263
|
var MAX_GROUPS = 1e3;
|
|
@@ -3219,7 +4295,7 @@ var ORDER_DIRECTIONS = {
|
|
|
3219
4295
|
DESC_NULLS_LAST: { direction: "desc", nulls: "last" }
|
|
3220
4296
|
};
|
|
3221
4297
|
var NUMERIC_TYPES = /* @__PURE__ */ new Set(["integer", "bigint", "numeric", "double"]);
|
|
3222
|
-
function
|
|
4298
|
+
function fail2(message, code, candidates = []) {
|
|
3223
4299
|
throw new DocumentCompileError(message, code, candidates);
|
|
3224
4300
|
}
|
|
3225
4301
|
function argsByName(field) {
|
|
@@ -3227,16 +4303,16 @@ function argsByName(field) {
|
|
|
3227
4303
|
}
|
|
3228
4304
|
function* aggregateBlockFields(selectionSet) {
|
|
3229
4305
|
for (const node of selectionSet.selections) {
|
|
3230
|
-
if (node.kind !== graphql.Kind.FIELD)
|
|
4306
|
+
if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
3231
4307
|
if (node.name.value === "__typename") continue;
|
|
3232
|
-
if (node.alias)
|
|
4308
|
+
if (node.alias) fail2(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
|
|
3233
4309
|
yield node;
|
|
3234
4310
|
}
|
|
3235
4311
|
}
|
|
3236
4312
|
function assertKnownArgs(field, allowed) {
|
|
3237
4313
|
for (const arg of field.arguments ?? []) {
|
|
3238
4314
|
if (!allowed.includes(arg.name.value)) {
|
|
3239
|
-
|
|
4315
|
+
fail2(`argument '${arg.name.value}' on '${field.name.value}' is not supported in v2.0`, "unsupported-argument", allowed);
|
|
3240
4316
|
}
|
|
3241
4317
|
}
|
|
3242
4318
|
}
|
|
@@ -3249,8 +4325,11 @@ var OperationLowerer = class {
|
|
|
3249
4325
|
meta(table) {
|
|
3250
4326
|
return this.ctx.tables[table];
|
|
3251
4327
|
}
|
|
4328
|
+
/** A data column — cell-metadata columns are reachable only through `cells { … }`. */
|
|
3252
4329
|
column(table, name) {
|
|
3253
|
-
|
|
4330
|
+
const meta = this.meta(table);
|
|
4331
|
+
const column = meta.columns.find((c) => c.name === name);
|
|
4332
|
+
return column && !isCellColumn(meta, name) ? column : void 0;
|
|
3254
4333
|
}
|
|
3255
4334
|
link(table, name) {
|
|
3256
4335
|
return this.meta(table).links.find((l) => l.name === name);
|
|
@@ -3258,7 +4337,8 @@ var OperationLowerer = class {
|
|
|
3258
4337
|
fieldCandidates(table) {
|
|
3259
4338
|
const meta = this.meta(table);
|
|
3260
4339
|
return [
|
|
3261
|
-
...meta.columns.map((c) => c.name),
|
|
4340
|
+
...meta.columns.filter((c) => !isCellColumn(meta, c.name)).map((c) => c.name),
|
|
4341
|
+
...meta.cells ? [CELLS_FIELD] : [],
|
|
3262
4342
|
...meta.links.map((l) => l.name),
|
|
3263
4343
|
...meta.links.filter((l) => l.kind === "toMany").map((l) => `${l.name}Aggregate`)
|
|
3264
4344
|
];
|
|
@@ -3267,7 +4347,7 @@ var OperationLowerer = class {
|
|
|
3267
4347
|
slot(name, input, table) {
|
|
3268
4348
|
const variable = this.variables.get(name);
|
|
3269
4349
|
if (!variable || variable.type.kind !== "inputRef" || variable.type.input !== input || variable.type.table !== table) {
|
|
3270
|
-
|
|
4350
|
+
fail2(`variable $${name} must be declared as the ${input} input type for table '${table}'`, "slot-mismatch");
|
|
3271
4351
|
}
|
|
3272
4352
|
return name;
|
|
3273
4353
|
}
|
|
@@ -3292,14 +4372,14 @@ var OperationLowerer = class {
|
|
|
3292
4372
|
const items = node.values.map((item) => {
|
|
3293
4373
|
const lowered = this.lowerValue(item);
|
|
3294
4374
|
if (lowered.kind !== "literal" || Array.isArray(lowered.value)) {
|
|
3295
|
-
|
|
4375
|
+
fail2("list literals must contain scalar literals only \u2014 pass the whole list as a variable instead", "unsupported-literal");
|
|
3296
4376
|
}
|
|
3297
4377
|
return lowered.value;
|
|
3298
4378
|
});
|
|
3299
4379
|
return { kind: "literal", value: items };
|
|
3300
4380
|
}
|
|
3301
4381
|
case graphql.Kind.OBJECT:
|
|
3302
|
-
|
|
4382
|
+
fail2("literal JSON objects are not supported in documents \u2014 pass the value via a variable", "unsupported-literal");
|
|
3303
4383
|
}
|
|
3304
4384
|
}
|
|
3305
4385
|
// -------------------------------------------------------------------------
|
|
@@ -3309,7 +4389,7 @@ var OperationLowerer = class {
|
|
|
3309
4389
|
if (node.kind === graphql.Kind.VARIABLE) {
|
|
3310
4390
|
return { kind: "dynamicFilter", slot: this.slot(node.name.value, "boolExp", table) };
|
|
3311
4391
|
}
|
|
3312
|
-
if (node.kind !== graphql.Kind.OBJECT)
|
|
4392
|
+
if (node.kind !== graphql.Kind.OBJECT) fail2(`a boolean expression on '${table}' must be an object or a variable`, "invalid-boolexp");
|
|
3313
4393
|
const operands = [];
|
|
3314
4394
|
for (const field of node.fields) {
|
|
3315
4395
|
const key = field.name.value;
|
|
@@ -3340,45 +4420,45 @@ var OperationLowerer = class {
|
|
|
3340
4420
|
continue;
|
|
3341
4421
|
}
|
|
3342
4422
|
}
|
|
3343
|
-
|
|
4423
|
+
fail2(`unknown field '${key}' in a boolean expression on '${table}'`, "unknown-column", this.fieldCandidates(table));
|
|
3344
4424
|
}
|
|
3345
4425
|
if (operands.length === 0) return CONST_TRUE;
|
|
3346
4426
|
return operands.length === 1 ? operands[0] : { kind: "and", operands };
|
|
3347
4427
|
}
|
|
3348
4428
|
lowerComparison(node, column, table) {
|
|
3349
4429
|
if (node.kind !== graphql.Kind.OBJECT) {
|
|
3350
|
-
|
|
4430
|
+
fail2(`comparison on '${table}.${column}' must be an object like { _eq: \u2026 }`, "invalid-comparison", Object.keys(BOOLEXP_OPS));
|
|
3351
4431
|
}
|
|
3352
4432
|
return node.fields.map((field) => {
|
|
3353
4433
|
const op = BOOLEXP_OPS[field.name.value];
|
|
3354
4434
|
if (!op) {
|
|
3355
|
-
|
|
4435
|
+
fail2(`unknown comparison operator '${field.name.value}' on '${table}.${column}'`, "unknown-operator", Object.keys(BOOLEXP_OPS));
|
|
3356
4436
|
}
|
|
3357
4437
|
return { kind: "cmp", op, column, value: this.lowerValue(field.value) };
|
|
3358
4438
|
});
|
|
3359
4439
|
}
|
|
3360
4440
|
/** Hasura aggregation predicate: `{ count: { predicate: { _gt: 2 }, filter: {…} } }`. */
|
|
3361
4441
|
lowerAggPredicate(node, link, target) {
|
|
3362
|
-
if (node.kind !== graphql.Kind.OBJECT)
|
|
4442
|
+
if (node.kind !== graphql.Kind.OBJECT) fail2(`aggregate predicate through '${link}' must be an object`, "invalid-agg-predicate");
|
|
3363
4443
|
const countField = node.fields.find((f) => f.name.value === "count");
|
|
3364
4444
|
if (!countField || node.fields.length !== 1) {
|
|
3365
|
-
|
|
4445
|
+
fail2(`aggregate predicates support { count: \u2026 } only in v2.0 (through '${link}')`, "invalid-agg-predicate", ["count"]);
|
|
3366
4446
|
}
|
|
3367
|
-
if (countField.value.kind !== graphql.Kind.OBJECT)
|
|
4447
|
+
if (countField.value.kind !== graphql.Kind.OBJECT) fail2(`count predicate through '${link}' must be an object`, "invalid-agg-predicate");
|
|
3368
4448
|
let predicate;
|
|
3369
4449
|
let where;
|
|
3370
4450
|
for (const field of countField.value.fields) {
|
|
3371
4451
|
if (field.name.value === "predicate") {
|
|
3372
4452
|
if (field.value.kind !== graphql.Kind.OBJECT || field.value.fields.length !== 1) {
|
|
3373
|
-
|
|
4453
|
+
fail2(`count predicate through '${link}' needs exactly one operator`, "invalid-agg-predicate", [...AGG_PREDICATE_OPS]);
|
|
3374
4454
|
}
|
|
3375
4455
|
const opField = field.value.fields[0];
|
|
3376
4456
|
const opName = opField.name.value;
|
|
3377
4457
|
if (!AGG_PREDICATE_OPS.includes(opName)) {
|
|
3378
|
-
|
|
4458
|
+
fail2(`aggregate count predicates support ${AGG_PREDICATE_OPS.join("/")} only, got '${opName}'`, "invalid-agg-predicate");
|
|
3379
4459
|
}
|
|
3380
4460
|
if (opField.value.kind !== graphql.Kind.INT) {
|
|
3381
|
-
|
|
4461
|
+
fail2(`aggregate count predicate value must be an integer literal`, "invalid-agg-predicate");
|
|
3382
4462
|
}
|
|
3383
4463
|
predicate = { op: opName.slice(1), value: Number(opField.value.value) };
|
|
3384
4464
|
continue;
|
|
@@ -3387,9 +4467,9 @@ var OperationLowerer = class {
|
|
|
3387
4467
|
where = this.lowerBoolExp(field.value, target);
|
|
3388
4468
|
continue;
|
|
3389
4469
|
}
|
|
3390
|
-
|
|
4470
|
+
fail2(`unknown key '${field.name.value}' in a count predicate`, "invalid-agg-predicate", ["predicate", "filter"]);
|
|
3391
4471
|
}
|
|
3392
|
-
if (!predicate)
|
|
4472
|
+
if (!predicate) fail2(`count predicate through '${link}' needs a { predicate: { _gt: n } }`, "invalid-agg-predicate");
|
|
3393
4473
|
return { kind: "aggPredicate", link, fn: "count", op: predicate.op, value: predicate.value, ...where ? { where } : {} };
|
|
3394
4474
|
}
|
|
3395
4475
|
lowerOrderBy(node, table) {
|
|
@@ -3399,7 +4479,7 @@ var OperationLowerer = class {
|
|
|
3399
4479
|
const items = node.kind === graphql.Kind.LIST ? node.values : [node];
|
|
3400
4480
|
const lowered = [];
|
|
3401
4481
|
for (const item of items) {
|
|
3402
|
-
if (item.kind !== graphql.Kind.OBJECT)
|
|
4482
|
+
if (item.kind !== graphql.Kind.OBJECT) fail2(`orderBy entries on '${table}' must be objects`, "invalid-orderby");
|
|
3403
4483
|
for (const field of item.fields) {
|
|
3404
4484
|
const key = field.name.value;
|
|
3405
4485
|
const column = this.column(table, key);
|
|
@@ -3410,25 +4490,25 @@ var OperationLowerer = class {
|
|
|
3410
4490
|
const link = this.link(table, key);
|
|
3411
4491
|
if (link && link.kind !== "toMany") {
|
|
3412
4492
|
if (field.value.kind !== graphql.Kind.OBJECT || field.value.fields.length !== 1) {
|
|
3413
|
-
|
|
4493
|
+
fail2(`orderBy through '${key}' needs exactly one target column`, "invalid-orderby");
|
|
3414
4494
|
}
|
|
3415
4495
|
const inner = field.value.fields[0];
|
|
3416
4496
|
const targetColumn = this.column(link.target, inner.name.value);
|
|
3417
4497
|
if (!targetColumn) {
|
|
3418
|
-
|
|
4498
|
+
fail2(`unknown column '${inner.name.value}' on '${link.target}' in orderBy`, "unknown-column", this.meta(link.target).columns.map((c) => c.name));
|
|
3419
4499
|
}
|
|
3420
4500
|
lowered.push({ kind: "column", column: inner.name.value, link: key, ...this.lowerDirection(inner.value, link.target, inner.name.value) });
|
|
3421
4501
|
continue;
|
|
3422
4502
|
}
|
|
3423
|
-
|
|
4503
|
+
fail2(`unknown orderBy key '${key}' on '${table}' (to-one links only)`, "invalid-orderby", this.fieldCandidates(table));
|
|
3424
4504
|
}
|
|
3425
4505
|
}
|
|
3426
4506
|
return lowered;
|
|
3427
4507
|
}
|
|
3428
4508
|
lowerDirection(node, table, column) {
|
|
3429
|
-
if (node.kind !== graphql.Kind.ENUM)
|
|
4509
|
+
if (node.kind !== graphql.Kind.ENUM) fail2(`orderBy direction on '${table}.${column}' must be a direction enum`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
|
|
3430
4510
|
const direction = ORDER_DIRECTIONS[node.value];
|
|
3431
|
-
if (!direction)
|
|
4511
|
+
if (!direction) fail2(`unknown orderBy direction '${node.value}'`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
|
|
3432
4512
|
return direction.nulls ? { direction: direction.direction, nulls: direction.nulls } : { direction: direction.direction };
|
|
3433
4513
|
}
|
|
3434
4514
|
lowerDistinctOn(node, table) {
|
|
@@ -3437,9 +4517,9 @@ var OperationLowerer = class {
|
|
|
3437
4517
|
}
|
|
3438
4518
|
const items = node.kind === graphql.Kind.LIST ? node.values : [node];
|
|
3439
4519
|
return items.map((item) => {
|
|
3440
|
-
if (item.kind !== graphql.Kind.ENUM)
|
|
4520
|
+
if (item.kind !== graphql.Kind.ENUM) fail2(`distinctOn entries on '${table}' must be select-column enum values`, "invalid-distinct-on");
|
|
3441
4521
|
if (!this.column(table, item.value)) {
|
|
3442
|
-
|
|
4522
|
+
fail2(`unknown distinctOn column '${item.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
|
|
3443
4523
|
}
|
|
3444
4524
|
return item.value;
|
|
3445
4525
|
});
|
|
@@ -3450,16 +4530,16 @@ var OperationLowerer = class {
|
|
|
3450
4530
|
if (node.kind === graphql.Kind.VARIABLE) return { ...base, variable: node.name.value };
|
|
3451
4531
|
if (node.kind === graphql.Kind.INT) {
|
|
3452
4532
|
const value = Number(node.value);
|
|
3453
|
-
if (value < 1)
|
|
4533
|
+
if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
|
|
3454
4534
|
return { ...base, value: Math.min(value, MAX_LIMIT) };
|
|
3455
4535
|
}
|
|
3456
|
-
|
|
4536
|
+
fail2("limit must be an integer literal or a variable", "invalid-limit");
|
|
3457
4537
|
}
|
|
3458
4538
|
lowerOffset(node) {
|
|
3459
4539
|
if (!node) return void 0;
|
|
3460
4540
|
if (node.kind === graphql.Kind.VARIABLE) return { variable: node.name.value };
|
|
3461
4541
|
if (node.kind === graphql.Kind.INT) return { value: Number(node.value) };
|
|
3462
|
-
|
|
4542
|
+
fail2("offset must be an integer literal or a variable", "invalid-offset");
|
|
3463
4543
|
}
|
|
3464
4544
|
// -------------------------------------------------------------------------
|
|
3465
4545
|
// Selections
|
|
@@ -3468,9 +4548,9 @@ var OperationLowerer = class {
|
|
|
3468
4548
|
const meta = this.meta(table);
|
|
3469
4549
|
const selection = { typename: meta.typename, columns: [], relations: [], aggregates: [] };
|
|
3470
4550
|
for (const node of selectionSet.selections) {
|
|
3471
|
-
if (node.kind !== graphql.Kind.FIELD)
|
|
4551
|
+
if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
3472
4552
|
if (node.directives && node.directives.length > 0) {
|
|
3473
|
-
|
|
4553
|
+
fail2(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
|
|
3474
4554
|
}
|
|
3475
4555
|
const name = node.name.value;
|
|
3476
4556
|
if (name === "__typename") continue;
|
|
@@ -3478,14 +4558,19 @@ var OperationLowerer = class {
|
|
|
3478
4558
|
const column = this.column(table, name);
|
|
3479
4559
|
if (column) {
|
|
3480
4560
|
if (node.arguments !== void 0 && node.arguments.length > 0 || node.selectionSet !== void 0) {
|
|
3481
|
-
|
|
4561
|
+
fail2(`column '${table}.${name}' takes no arguments or sub-selection`, "invalid-selection");
|
|
3482
4562
|
}
|
|
3483
4563
|
selection.columns.push({ alias, column: name });
|
|
3484
4564
|
continue;
|
|
3485
4565
|
}
|
|
4566
|
+
if (name === CELLS_FIELD && meta.cells) {
|
|
4567
|
+
if (selection.cells) fail2(`'${CELLS_FIELD}' may be selected once per object`, "invalid-selection");
|
|
4568
|
+
selection.cells = this.lowerCells(node, table, alias);
|
|
4569
|
+
continue;
|
|
4570
|
+
}
|
|
3486
4571
|
const link = this.link(table, name);
|
|
3487
4572
|
if (link) {
|
|
3488
|
-
if (!node.selectionSet)
|
|
4573
|
+
if (!node.selectionSet) fail2(`link '${table}.${name}' needs a selection set`, "invalid-selection");
|
|
3489
4574
|
const relation = {
|
|
3490
4575
|
alias,
|
|
3491
4576
|
link: name,
|
|
@@ -3504,7 +4589,7 @@ var OperationLowerer = class {
|
|
|
3504
4589
|
if (offset) relation.offset = offset;
|
|
3505
4590
|
}
|
|
3506
4591
|
} else if (node.arguments && node.arguments.length > 0) {
|
|
3507
|
-
|
|
4592
|
+
fail2(`to-one link '${table}.${name}' takes no arguments`, "invalid-selection");
|
|
3508
4593
|
}
|
|
3509
4594
|
selection.relations.push(relation);
|
|
3510
4595
|
continue;
|
|
@@ -3512,7 +4597,7 @@ var OperationLowerer = class {
|
|
|
3512
4597
|
if (name.endsWith("Aggregate")) {
|
|
3513
4598
|
const aggLink = this.link(table, name.slice(0, -"Aggregate".length));
|
|
3514
4599
|
if (aggLink && aggLink.kind === "toMany") {
|
|
3515
|
-
if (!node.selectionSet)
|
|
4600
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
3516
4601
|
assertKnownArgs(node, ["where"]);
|
|
3517
4602
|
const whereArg = argsByName(node).get("where");
|
|
3518
4603
|
const aggregate = this.lowerAggregateSelection(node.selectionSet, aggLink.target);
|
|
@@ -3525,10 +4610,57 @@ var OperationLowerer = class {
|
|
|
3525
4610
|
continue;
|
|
3526
4611
|
}
|
|
3527
4612
|
}
|
|
3528
|
-
|
|
4613
|
+
fail2(`unknown field '${name}' on '${table}'`, "unknown-field", this.fieldCandidates(table));
|
|
3529
4614
|
}
|
|
3530
4615
|
return selection;
|
|
3531
4616
|
}
|
|
4617
|
+
/**
|
|
4618
|
+
* `cells { <column> { at by … } }` — typed per-cell metadata. Plain tables
|
|
4619
|
+
* offer `at`/`by`; resolved tables add the winning `source` and newer
|
|
4620
|
+
* `conflicts { source at value }`.
|
|
4621
|
+
*/
|
|
4622
|
+
lowerCells(node, table, alias) {
|
|
4623
|
+
const meta = this.meta(table);
|
|
4624
|
+
const cells = meta.cells ?? {};
|
|
4625
|
+
if (node.arguments && node.arguments.length > 0) fail2(`'${CELLS_FIELD}' takes no arguments`, "invalid-selection");
|
|
4626
|
+
if (!node.selectionSet) fail2(`'${CELLS_FIELD}' needs a selection set`, "invalid-selection");
|
|
4627
|
+
const fields = [];
|
|
4628
|
+
for (const cellNode of fieldNodes(node.selectionSet)) {
|
|
4629
|
+
const column = cellNode.name.value;
|
|
4630
|
+
const parts = cells[column];
|
|
4631
|
+
if (!parts || parts.at === void 0 && parts.source === void 0) {
|
|
4632
|
+
fail2(`unknown cell '${column}' on '${table}'`, "unknown-field", Object.keys(cells));
|
|
4633
|
+
}
|
|
4634
|
+
if (!cellNode.selectionSet) fail2(`cell '${column}' needs a selection set`, "invalid-selection");
|
|
4635
|
+
const resolved = parts.source !== void 0;
|
|
4636
|
+
const allowed = resolved ? ["source", "at", "by", "conflicts"] : ["at", "by"];
|
|
4637
|
+
const selected = [];
|
|
4638
|
+
for (const partNode of fieldNodes(cellNode.selectionSet)) {
|
|
4639
|
+
const part = partNode.name.value;
|
|
4640
|
+
if (!allowed.includes(part)) fail2(`unknown field '${part}' on cell '${column}'`, "unknown-field", allowed);
|
|
4641
|
+
const partAlias = partNode.alias?.value ?? part;
|
|
4642
|
+
if (part !== "conflicts") {
|
|
4643
|
+
if (partNode.selectionSet) fail2(`cell field '${part}' takes no sub-selection`, "invalid-selection");
|
|
4644
|
+
selected.push({ alias: partAlias, part });
|
|
4645
|
+
continue;
|
|
4646
|
+
}
|
|
4647
|
+
if (!partNode.selectionSet) fail2(`'conflicts' needs a selection set`, "invalid-selection");
|
|
4648
|
+
const conflictFields = fieldNodes(partNode.selectionSet).map((fieldNode) => {
|
|
4649
|
+
const field = fieldNode.name.value;
|
|
4650
|
+
if (field !== "source" && field !== "at" && field !== "value") fail2(`unknown field '${field}' on a cell conflict`, "unknown-field", ["source", "at", "value"]);
|
|
4651
|
+
return { alias: fieldNode.alias?.value ?? field, field };
|
|
4652
|
+
});
|
|
4653
|
+
selected.push({ alias: partAlias, part: "conflicts", fields: conflictFields, typename: CELL_TYPENAMES.conflict });
|
|
4654
|
+
}
|
|
4655
|
+
fields.push({
|
|
4656
|
+
alias: cellNode.alias?.value ?? column,
|
|
4657
|
+
column,
|
|
4658
|
+
typename: resolved ? CELL_TYPENAMES.resolved : CELL_TYPENAMES.stamp,
|
|
4659
|
+
parts: selected
|
|
4660
|
+
});
|
|
4661
|
+
}
|
|
4662
|
+
return { alias, typename: cellsTypename(meta.typename), fields };
|
|
4663
|
+
}
|
|
3532
4664
|
/** The `{ aggregate { count sum {…} avg {…} } nodes {…} }` block shared by roots and relations. */
|
|
3533
4665
|
lowerAggregateSelection(selectionSet, table) {
|
|
3534
4666
|
const result = {};
|
|
@@ -3536,17 +4668,17 @@ var OperationLowerer = class {
|
|
|
3536
4668
|
for (const node of aggregateBlockFields(selectionSet)) {
|
|
3537
4669
|
const name = node.name.value;
|
|
3538
4670
|
if (name === "nodes") {
|
|
3539
|
-
if (!node.selectionSet)
|
|
4671
|
+
if (!node.selectionSet) fail2("'nodes' needs a selection set", "invalid-aggregate");
|
|
3540
4672
|
result.nodes = this.lowerSelection(node.selectionSet, table);
|
|
3541
4673
|
continue;
|
|
3542
4674
|
}
|
|
3543
|
-
if (name !== "aggregate")
|
|
3544
|
-
if (sawAggregate)
|
|
4675
|
+
if (name !== "aggregate") fail2(`unknown aggregate field '${name}'`, "invalid-aggregate", ["aggregate", "nodes"]);
|
|
4676
|
+
if (sawAggregate) fail2("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
|
|
3545
4677
|
sawAggregate = true;
|
|
3546
|
-
if (!node.selectionSet)
|
|
4678
|
+
if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
|
|
3547
4679
|
Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
|
|
3548
4680
|
}
|
|
3549
|
-
if (!sawAggregate && !result.nodes)
|
|
4681
|
+
if (!sawAggregate && !result.nodes) fail2("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
|
|
3550
4682
|
return result;
|
|
3551
4683
|
}
|
|
3552
4684
|
/**
|
|
@@ -3563,24 +4695,24 @@ var OperationLowerer = class {
|
|
|
3563
4695
|
for (const node of aggregateBlockFields(selectionSet)) {
|
|
3564
4696
|
const name = node.name.value;
|
|
3565
4697
|
if (name === "key") {
|
|
3566
|
-
if (sawKey)
|
|
4698
|
+
if (sawKey) fail2("one key block per grouped aggregate", "invalid-aggregate");
|
|
3567
4699
|
sawKey = true;
|
|
3568
|
-
if (!node.selectionSet)
|
|
4700
|
+
if (!node.selectionSet) fail2("'key' needs a selection set", "invalid-aggregate");
|
|
3569
4701
|
result.key = this.lowerGroupKeySelection(node.selectionSet, table, groupColumns);
|
|
3570
4702
|
continue;
|
|
3571
4703
|
}
|
|
3572
4704
|
if (name === "nodes") {
|
|
3573
|
-
|
|
4705
|
+
fail2("'nodes' is not available on a grouped aggregate \u2014 query the rows separately", "invalid-aggregate", ["key", "aggregate"]);
|
|
3574
4706
|
}
|
|
3575
|
-
if (name !== "aggregate")
|
|
3576
|
-
if (sawAggregate)
|
|
4707
|
+
if (name !== "aggregate") fail2(`unknown grouped-aggregate field '${name}'`, "invalid-aggregate", ["key", "aggregate"]);
|
|
4708
|
+
if (sawAggregate) fail2("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
|
|
3577
4709
|
sawAggregate = true;
|
|
3578
|
-
if (!node.selectionSet)
|
|
4710
|
+
if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
|
|
3579
4711
|
Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
|
|
3580
4712
|
}
|
|
3581
4713
|
const rendersAggregate = result.count !== void 0 || (result.sum?.length ?? 0) > 0 || (result.avg?.length ?? 0) > 0;
|
|
3582
4714
|
if (result.key.length === 0 && !rendersAggregate) {
|
|
3583
|
-
|
|
4715
|
+
fail2(
|
|
3584
4716
|
"grouped aggregate needs at least one key column or an aggregate \u2014 a block selecting only __typename produces nothing",
|
|
3585
4717
|
"invalid-aggregate",
|
|
3586
4718
|
["key", "aggregate"]
|
|
@@ -3597,14 +4729,14 @@ var OperationLowerer = class {
|
|
|
3597
4729
|
lowerGroupKeySelection(selectionSet, table, groupColumns) {
|
|
3598
4730
|
const key = [];
|
|
3599
4731
|
for (const node of selectionSet.selections) {
|
|
3600
|
-
if (node.kind !== graphql.Kind.FIELD)
|
|
4732
|
+
if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
3601
4733
|
const name = node.name.value;
|
|
3602
4734
|
if (name === "__typename") continue;
|
|
3603
4735
|
if (!this.column(table, name)) {
|
|
3604
|
-
|
|
4736
|
+
fail2(`unknown key column '${name}' on '${table}'`, "unknown-column", [...groupColumns]);
|
|
3605
4737
|
}
|
|
3606
4738
|
if (!groupColumns.has(name)) {
|
|
3607
|
-
|
|
4739
|
+
fail2(
|
|
3608
4740
|
`key column '${name}' is not one of this query's groupBy keys \u2014 it has no single value within a group`,
|
|
3609
4741
|
"invalid-group-key",
|
|
3610
4742
|
[...groupColumns]
|
|
@@ -3627,21 +4759,21 @@ var OperationLowerer = class {
|
|
|
3627
4759
|
if (columns) count.columns = this.lowerDistinctOn(columns.value, table);
|
|
3628
4760
|
const distinct = args.get("distinct");
|
|
3629
4761
|
if (distinct) {
|
|
3630
|
-
if (distinct.value.kind !== graphql.Kind.BOOLEAN)
|
|
4762
|
+
if (distinct.value.kind !== graphql.Kind.BOOLEAN) fail2("count(distinct:) must be a boolean literal", "invalid-aggregate");
|
|
3631
4763
|
count.distinct = distinct.value.value;
|
|
3632
4764
|
}
|
|
3633
4765
|
result.count = count;
|
|
3634
4766
|
continue;
|
|
3635
4767
|
}
|
|
3636
4768
|
if (aggName === "sum" || aggName === "avg") {
|
|
3637
|
-
if (!aggField.selectionSet)
|
|
4769
|
+
if (!aggField.selectionSet) fail2(`'${aggName}' needs a column selection`, "invalid-aggregate");
|
|
3638
4770
|
const columns = [];
|
|
3639
4771
|
for (const columnField of aggField.selectionSet.selections) {
|
|
3640
|
-
if (columnField.kind !== graphql.Kind.FIELD)
|
|
4772
|
+
if (columnField.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
3641
4773
|
if (columnField.name.value === "__typename") continue;
|
|
3642
4774
|
const column = this.column(table, columnField.name.value);
|
|
3643
4775
|
if (!column || typeof column.type !== "string" || !NUMERIC_TYPES.has(column.type)) {
|
|
3644
|
-
|
|
4776
|
+
fail2(
|
|
3645
4777
|
`'${aggName}' supports numeric columns only, got '${columnField.name.value}' on '${table}'`,
|
|
3646
4778
|
"invalid-aggregate",
|
|
3647
4779
|
this.meta(table).columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type)).map((c) => c.name)
|
|
@@ -3652,7 +4784,7 @@ var OperationLowerer = class {
|
|
|
3652
4784
|
result[aggName] = columns;
|
|
3653
4785
|
continue;
|
|
3654
4786
|
}
|
|
3655
|
-
|
|
4787
|
+
fail2(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
|
|
3656
4788
|
}
|
|
3657
4789
|
return result;
|
|
3658
4790
|
}
|
|
@@ -3662,41 +4794,43 @@ var OperationLowerer = class {
|
|
|
3662
4794
|
lowerRoots(operation, kind) {
|
|
3663
4795
|
const roots = [];
|
|
3664
4796
|
for (const node of operation.selectionSet.selections) {
|
|
3665
|
-
if (node.kind !== graphql.Kind.FIELD)
|
|
4797
|
+
if (node.kind !== graphql.Kind.FIELD) fail2("fragments are not supported at the operation root", "invalid-root");
|
|
3666
4798
|
if (node.selectionSet) graphql.visit(node.selectionSet, {
|
|
3667
4799
|
Directive(directive) {
|
|
3668
4800
|
if (directive.name.value === "assertAffectedRows") {
|
|
3669
|
-
|
|
4801
|
+
fail2("@assertAffectedRows is supported on mutation roots only", "invalid-affected-row-assertion");
|
|
3670
4802
|
}
|
|
3671
4803
|
}
|
|
3672
4804
|
});
|
|
3673
4805
|
const assertion = this.parseAffectedRowAssertion(node, kind);
|
|
3674
4806
|
const gate = this.parseRootDirectives(node);
|
|
3675
4807
|
if (gate.omit) continue;
|
|
3676
|
-
const
|
|
4808
|
+
const lowered = kind === "query" ? this.lowerQueryRoot(node) : this.lowerMutationRoot(node);
|
|
4809
|
+
const meta = this.meta(lowered.table);
|
|
4810
|
+
const root = kind === "query" ? applyPrefetchDirectives({ node, root: lowered, meta }) : applyWritebackDirectives({ node, root: lowered, meta });
|
|
3677
4811
|
const guarded = root.kind === "mutation" && assertion !== void 0 ? { ...root, assertAffectedRows: assertion } : root;
|
|
3678
4812
|
roots.push(gate.includeIf ? { ...guarded, includeIf: gate.includeIf } : guarded);
|
|
3679
4813
|
}
|
|
3680
|
-
if (roots.length === 0)
|
|
4814
|
+
if (roots.length === 0) fail2("operation has no roots (all statically skipped?)", "invalid-root");
|
|
3681
4815
|
return roots;
|
|
3682
4816
|
}
|
|
3683
4817
|
parseAffectedRowAssertion(node, kind) {
|
|
3684
4818
|
const assertions = node.directives?.filter((directive) => directive.name.value === "assertAffectedRows") ?? [];
|
|
3685
4819
|
if (!assertions.length) return void 0;
|
|
3686
4820
|
if (kind !== "mutation" || assertions.length !== 1) {
|
|
3687
|
-
|
|
4821
|
+
fail2("@assertAffectedRows is supported once per mutation root", "invalid-affected-row-assertion");
|
|
3688
4822
|
}
|
|
3689
4823
|
if (node.directives?.some((directive) => ["include", "skip"].includes(directive.name.value))) {
|
|
3690
|
-
|
|
4824
|
+
fail2("an @assertAffectedRows root cannot use @include or @skip", "invalid-affected-row-assertion");
|
|
3691
4825
|
}
|
|
3692
4826
|
const args = assertions[0].arguments;
|
|
3693
4827
|
const arg = args?.[0];
|
|
3694
4828
|
if (args?.length !== 1 || arg?.name.value !== "exactly" || arg.value.kind !== graphql.Kind.INT) {
|
|
3695
|
-
|
|
4829
|
+
fail2("@assertAffectedRows(exactly:) requires a document-authored nonnegative Int literal", "invalid-affected-row-assertion");
|
|
3696
4830
|
}
|
|
3697
4831
|
const count = Number(arg.value.value);
|
|
3698
4832
|
if (!Number.isInteger(count) || count < 0 || count > 2147483647) {
|
|
3699
|
-
|
|
4833
|
+
fail2("@assertAffectedRows(exactly:) must be between 0 and 2147483647", "invalid-affected-row-assertion");
|
|
3700
4834
|
}
|
|
3701
4835
|
return count;
|
|
3702
4836
|
}
|
|
@@ -3706,18 +4840,19 @@ var OperationLowerer = class {
|
|
|
3706
4840
|
for (const directive of node.directives ?? []) {
|
|
3707
4841
|
const name = directive.name.value;
|
|
3708
4842
|
if (name === "assertAffectedRows") continue;
|
|
4843
|
+
if (SOURCE_DIRECTIVES.has(name)) continue;
|
|
3709
4844
|
if (name !== "include" && name !== "skip") {
|
|
3710
|
-
|
|
4845
|
+
fail2(`directive @${name} is not supported on fields`, "unknown-directive", ["include", "skip"]);
|
|
3711
4846
|
}
|
|
3712
4847
|
const ifArg = directive.arguments?.find((a) => a.name.value === "if");
|
|
3713
|
-
if (!ifArg)
|
|
4848
|
+
if (!ifArg) fail2(`@${name} needs an if: argument`, "invalid-directive");
|
|
3714
4849
|
if (ifArg.value.kind === graphql.Kind.BOOLEAN) {
|
|
3715
4850
|
const keep = name === "include" ? ifArg.value.value : !ifArg.value.value;
|
|
3716
4851
|
if (!keep) omit = true;
|
|
3717
4852
|
continue;
|
|
3718
4853
|
}
|
|
3719
|
-
if (ifArg.value.kind !== graphql.Kind.VARIABLE)
|
|
3720
|
-
if (includeIf)
|
|
4854
|
+
if (ifArg.value.kind !== graphql.Kind.VARIABLE) fail2(`@${name}(if:) must be a boolean literal or variable`, "invalid-directive");
|
|
4855
|
+
if (includeIf) fail2("at most one of @include/@skip per root field", "invalid-directive");
|
|
3721
4856
|
includeIf = { variable: ifArg.value.name.value, negated: name === "skip" };
|
|
3722
4857
|
}
|
|
3723
4858
|
return omit ? { omit: true } : { includeIf, omit: false };
|
|
@@ -3726,12 +4861,12 @@ var OperationLowerer = class {
|
|
|
3726
4861
|
const name = node.name.value;
|
|
3727
4862
|
const alias = node.alias?.value ?? name;
|
|
3728
4863
|
const rootInfo = this.ctx.queryRoots.get(name);
|
|
3729
|
-
if (!rootInfo)
|
|
4864
|
+
if (!rootInfo) fail2(`unknown query root '${name}'`, "unknown-root", [...this.ctx.queryRoots.keys()]);
|
|
3730
4865
|
const { table } = rootInfo;
|
|
3731
4866
|
const args = argsByName(node);
|
|
3732
4867
|
if (rootInfo.kind === "select") {
|
|
3733
4868
|
assertKnownArgs(node, ["where", "orderBy", "distinctOn", "limit", "offset"]);
|
|
3734
|
-
if (!node.selectionSet)
|
|
4869
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
3735
4870
|
const where = args.get("where");
|
|
3736
4871
|
const orderBy = args.get("orderBy");
|
|
3737
4872
|
const distinctOn = args.get("distinctOn");
|
|
@@ -3750,7 +4885,7 @@ var OperationLowerer = class {
|
|
|
3750
4885
|
};
|
|
3751
4886
|
}
|
|
3752
4887
|
if (rootInfo.kind === "byPk") {
|
|
3753
|
-
if (!node.selectionSet)
|
|
4888
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
3754
4889
|
const pk = this.lowerPkArgs(node, table);
|
|
3755
4890
|
return { kind: "byPk", alias, table, selection: this.lowerSelection(node.selectionSet, table), pk, where: CONST_TRUE };
|
|
3756
4891
|
}
|
|
@@ -3758,7 +4893,7 @@ var OperationLowerer = class {
|
|
|
3758
4893
|
return this.lowerAggregateGroupsRoot(node, { alias, table });
|
|
3759
4894
|
}
|
|
3760
4895
|
assertKnownArgs(node, ["where"]);
|
|
3761
|
-
if (!node.selectionSet)
|
|
4896
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
3762
4897
|
const whereArg = args.get("where");
|
|
3763
4898
|
const aggregate = this.lowerAggregateSelection(node.selectionSet, table);
|
|
3764
4899
|
return {
|
|
@@ -3772,10 +4907,10 @@ var OperationLowerer = class {
|
|
|
3772
4907
|
/** `<t>AggregateGroups(where:, groupBy:, limit:, offset:)` → an aggregateGroups root. */
|
|
3773
4908
|
lowerAggregateGroupsRoot(node, { alias, table }) {
|
|
3774
4909
|
assertKnownArgs(node, ["where", "groupBy", "limit", "offset"]);
|
|
3775
|
-
if (!node.selectionSet)
|
|
4910
|
+
if (!node.selectionSet) fail2(`'${node.name.value}' needs a selection set`, "invalid-selection");
|
|
3776
4911
|
const args = argsByName(node);
|
|
3777
4912
|
const groupByArg = args.get("groupBy");
|
|
3778
|
-
if (!groupByArg)
|
|
4913
|
+
if (!groupByArg) fail2(`'${node.name.value}' requires a 'groupBy' argument`, "missing-group-by");
|
|
3779
4914
|
const whereArg = args.get("where");
|
|
3780
4915
|
const groupBy = this.lowerGroupBy(groupByArg.value, table);
|
|
3781
4916
|
const aggregate = this.lowerGroupedAggregateSelection(node.selectionSet, table, groupBy);
|
|
@@ -3805,55 +4940,55 @@ var OperationLowerer = class {
|
|
|
3805
4940
|
*/
|
|
3806
4941
|
lowerGroupBy(node, table) {
|
|
3807
4942
|
if (node.kind === graphql.Kind.VARIABLE) {
|
|
3808
|
-
|
|
4943
|
+
fail2(
|
|
3809
4944
|
`groupBy on '${table}' must be written in the document \u2014 it cannot come from a variable`,
|
|
3810
4945
|
"dynamic-group-by"
|
|
3811
4946
|
);
|
|
3812
4947
|
}
|
|
3813
4948
|
const items = node.kind === graphql.Kind.LIST ? node.values : [node];
|
|
3814
|
-
if (items.length === 0)
|
|
4949
|
+
if (items.length === 0) fail2(`groupBy on '${table}' needs at least one key`, "invalid-group-by");
|
|
3815
4950
|
const seen = /* @__PURE__ */ new Set();
|
|
3816
4951
|
return items.map((item) => {
|
|
3817
4952
|
if (item.kind !== graphql.Kind.OBJECT) {
|
|
3818
|
-
|
|
4953
|
+
fail2(`groupBy entries on '${table}' must be objects like { column: foo }`, "invalid-group-by");
|
|
3819
4954
|
}
|
|
3820
4955
|
const fields = new Map(item.fields.map((field) => [field.name.value, field.value]));
|
|
3821
4956
|
for (const key of fields.keys()) {
|
|
3822
4957
|
if (key !== "column" && key !== "bucket") {
|
|
3823
|
-
|
|
4958
|
+
fail2(`unknown groupBy field '${key}' on '${table}'`, "invalid-group-by", ["column", "bucket"]);
|
|
3824
4959
|
}
|
|
3825
4960
|
}
|
|
3826
4961
|
const columnNode = fields.get("column");
|
|
3827
|
-
if (!columnNode)
|
|
4962
|
+
if (!columnNode) fail2(`groupBy entries on '${table}' need a 'column'`, "invalid-group-by");
|
|
3828
4963
|
if (columnNode.kind !== graphql.Kind.ENUM) {
|
|
3829
|
-
|
|
4964
|
+
fail2(`groupBy column on '${table}' must be a group-by column enum value`, "invalid-group-by");
|
|
3830
4965
|
}
|
|
3831
4966
|
const column = this.column(table, columnNode.value);
|
|
3832
4967
|
if (!column) {
|
|
3833
|
-
|
|
4968
|
+
fail2(`unknown groupBy column '${columnNode.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
|
|
3834
4969
|
}
|
|
3835
4970
|
if (column.type === "jsonb") {
|
|
3836
|
-
|
|
4971
|
+
fail2(`groupBy column '${table}.${columnNode.value}' is jsonb and cannot be a group key`, "invalid-group-by");
|
|
3837
4972
|
}
|
|
3838
4973
|
const bucketNode = fields.get("bucket");
|
|
3839
4974
|
let bucket;
|
|
3840
4975
|
if (bucketNode) {
|
|
3841
4976
|
if (bucketNode.kind !== graphql.Kind.ENUM) {
|
|
3842
|
-
|
|
4977
|
+
fail2(`groupBy bucket on '${table}.${columnNode.value}' must be a DateBucket enum value`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
|
|
3843
4978
|
}
|
|
3844
4979
|
bucket = DATE_BUCKET_BY_ENUM[bucketNode.value];
|
|
3845
4980
|
if (!bucket) {
|
|
3846
|
-
|
|
4981
|
+
fail2(`unknown DateBucket '${bucketNode.value}'`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
|
|
3847
4982
|
}
|
|
3848
4983
|
if (typeof column.type !== "string" || !BUCKETABLE_TYPES.has(column.type)) {
|
|
3849
|
-
|
|
4984
|
+
fail2(
|
|
3850
4985
|
`groupBy bucket is only valid on date/timestamptz columns \u2014 '${table}.${columnNode.value}' is ${typeof column.type === "string" ? column.type : "an enum"}`,
|
|
3851
4986
|
"invalid-group-by"
|
|
3852
4987
|
);
|
|
3853
4988
|
}
|
|
3854
4989
|
}
|
|
3855
4990
|
if (seen.has(columnNode.value)) {
|
|
3856
|
-
|
|
4991
|
+
fail2(`duplicate groupBy column '${columnNode.value}' on '${table}'`, "invalid-group-by");
|
|
3857
4992
|
}
|
|
3858
4993
|
seen.add(columnNode.value);
|
|
3859
4994
|
return bucket ? { column: columnNode.value, bucket } : { column: columnNode.value };
|
|
@@ -3866,10 +5001,10 @@ var OperationLowerer = class {
|
|
|
3866
5001
|
if (node.kind === graphql.Kind.VARIABLE) return { ...base, variable: node.name.value };
|
|
3867
5002
|
if (node.kind === graphql.Kind.INT) {
|
|
3868
5003
|
const value = Number(node.value);
|
|
3869
|
-
if (value < 1)
|
|
5004
|
+
if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
|
|
3870
5005
|
return { ...base, value: Math.min(value, MAX_GROUPS) };
|
|
3871
5006
|
}
|
|
3872
|
-
|
|
5007
|
+
fail2("limit must be an integer literal or a variable", "invalid-limit");
|
|
3873
5008
|
}
|
|
3874
5009
|
/** byPk-style pk arguments: one argument per primary-key column, exactly. */
|
|
3875
5010
|
lowerPkArgs(node, table) {
|
|
@@ -3878,7 +5013,7 @@ var OperationLowerer = class {
|
|
|
3878
5013
|
assertKnownArgs(node, meta.primaryKey);
|
|
3879
5014
|
return meta.primaryKey.map((column) => {
|
|
3880
5015
|
const arg = args.get(column);
|
|
3881
|
-
if (!arg)
|
|
5016
|
+
if (!arg) fail2(`'${node.name.value}' is missing primary-key argument '${column}'`, "missing-pk-argument", meta.primaryKey);
|
|
3882
5017
|
return { column, value: this.lowerValue(arg.value) };
|
|
3883
5018
|
});
|
|
3884
5019
|
}
|
|
@@ -3889,19 +5024,22 @@ var OperationLowerer = class {
|
|
|
3889
5024
|
const name = node.name.value;
|
|
3890
5025
|
const alias = node.alias?.value ?? name;
|
|
3891
5026
|
const rootInfo = this.ctx.mutationRoots.get(name);
|
|
3892
|
-
if (!rootInfo)
|
|
5027
|
+
if (!rootInfo) fail2(`unknown mutation root '${name}'`, "unknown-root", [...this.ctx.mutationRoots.keys()]);
|
|
3893
5028
|
if (rootInfo.op === "cowOverride" || rootInfo.op === "cowRevert") {
|
|
3894
5029
|
return this.lowerCowRoot(node, rootInfo);
|
|
3895
5030
|
}
|
|
5031
|
+
if (rootInfo.op === "resolvedWrite") {
|
|
5032
|
+
return this.lowerResolvedRoot(node, rootInfo);
|
|
5033
|
+
}
|
|
3896
5034
|
const { op, table } = rootInfo;
|
|
3897
5035
|
const args = argsByName(node);
|
|
3898
|
-
if (!node.selectionSet)
|
|
5036
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
3899
5037
|
const base = { kind: "mutation", op, alias, table, pinnedColumns: [], affectedRows: false };
|
|
3900
5038
|
switch (op) {
|
|
3901
5039
|
case "insert": {
|
|
3902
5040
|
assertKnownArgs(node, ["objects", "onConflict"]);
|
|
3903
5041
|
const objects = args.get("objects");
|
|
3904
|
-
if (!objects)
|
|
5042
|
+
if (!objects) fail2(`'${name}' needs objects:`, "missing-argument");
|
|
3905
5043
|
const response = this.lowerMutationResponse(node.selectionSet, table);
|
|
3906
5044
|
return {
|
|
3907
5045
|
...base,
|
|
@@ -3913,7 +5051,7 @@ var OperationLowerer = class {
|
|
|
3913
5051
|
case "insertOne": {
|
|
3914
5052
|
assertKnownArgs(node, ["object", "onConflict"]);
|
|
3915
5053
|
const object = args.get("object");
|
|
3916
|
-
if (!object)
|
|
5054
|
+
if (!object) fail2(`'${name}' needs object:`, "missing-argument");
|
|
3917
5055
|
return {
|
|
3918
5056
|
...base,
|
|
3919
5057
|
objects: this.lowerInsertObjects(object.value, table, { list: false }),
|
|
@@ -3924,7 +5062,7 @@ var OperationLowerer = class {
|
|
|
3924
5062
|
case "update": {
|
|
3925
5063
|
assertKnownArgs(node, ["where", "_set", "_append", "_inc"]);
|
|
3926
5064
|
const where = args.get("where");
|
|
3927
|
-
if (!where)
|
|
5065
|
+
if (!where) fail2(`'${name}' needs where: (bulk updates without a filter are not allowed)`, "missing-argument");
|
|
3928
5066
|
return {
|
|
3929
5067
|
...base,
|
|
3930
5068
|
...this.lowerMutationResponse(node.selectionSet, table),
|
|
@@ -3935,7 +5073,7 @@ var OperationLowerer = class {
|
|
|
3935
5073
|
case "updateByPk": {
|
|
3936
5074
|
assertKnownArgs(node, ["pkColumns", "_set", "_append", "_inc"]);
|
|
3937
5075
|
const pkColumns = args.get("pkColumns");
|
|
3938
|
-
if (!pkColumns)
|
|
5076
|
+
if (!pkColumns) fail2(`'${name}' needs pkColumns:`, "missing-argument");
|
|
3939
5077
|
return {
|
|
3940
5078
|
...base,
|
|
3941
5079
|
pk: this.lowerPkColumnsArg(pkColumns.value, table),
|
|
@@ -3946,7 +5084,7 @@ var OperationLowerer = class {
|
|
|
3946
5084
|
case "delete": {
|
|
3947
5085
|
assertKnownArgs(node, ["where"]);
|
|
3948
5086
|
const where = args.get("where");
|
|
3949
|
-
if (!where)
|
|
5087
|
+
if (!where) fail2(`'${name}' needs where: (bulk deletes without a filter are not allowed)`, "missing-argument");
|
|
3950
5088
|
return {
|
|
3951
5089
|
...base,
|
|
3952
5090
|
...this.lowerMutationResponse(node.selectionSet, table),
|
|
@@ -3973,11 +5111,11 @@ var OperationLowerer = class {
|
|
|
3973
5111
|
const name = node.name.value;
|
|
3974
5112
|
const alias = node.alias?.value ?? name;
|
|
3975
5113
|
const { table } = rootInfo;
|
|
3976
|
-
if (!node.selectionSet)
|
|
5114
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
3977
5115
|
const meta = this.meta(table);
|
|
3978
5116
|
const args = argsByName(node);
|
|
3979
5117
|
const pkArg = args.get(rootInfo.pk);
|
|
3980
|
-
if (!pkArg)
|
|
5118
|
+
if (!pkArg) fail2(`'${name}' is missing primary-key argument '${rootInfo.pk}'`, "missing-pk-argument", [rootInfo.pk]);
|
|
3981
5119
|
const pk = [{ column: rootInfo.pk, value: this.lowerValue(pkArg.value) }];
|
|
3982
5120
|
const base = {
|
|
3983
5121
|
kind: "mutation",
|
|
@@ -3998,24 +5136,78 @@ var OperationLowerer = class {
|
|
|
3998
5136
|
const entries = rootInfo.set.map((column) => {
|
|
3999
5137
|
const arg = args.get(column);
|
|
4000
5138
|
if (!arg) {
|
|
4001
|
-
|
|
5139
|
+
fail2(
|
|
4002
5140
|
`'${name}' is missing set argument '${column}' \u2014 a cow override writes every declared set column (pass null to clear a nullable one)`,
|
|
4003
5141
|
"missing-argument",
|
|
4004
5142
|
[...rootInfo.set]
|
|
4005
5143
|
);
|
|
4006
5144
|
}
|
|
4007
5145
|
if (!meta.settable.includes(column)) {
|
|
4008
|
-
|
|
5146
|
+
fail2(`column '${table}.${column}' is not updatable (pinned by the write policy or deprecated)`, "column-not-settable", meta.settable);
|
|
4009
5147
|
}
|
|
4010
5148
|
return { column, value: this.lowerValue(arg.value) };
|
|
4011
5149
|
});
|
|
4012
5150
|
return { ...base, set: { entries }, cowCopyColumns: [...rootInfo.copyColumns] };
|
|
4013
5151
|
}
|
|
5152
|
+
/**
|
|
5153
|
+
* Resolved-table roots: insertOne(object), updateByPk(pkColumns, _set),
|
|
5154
|
+
* deleteByPk(pk…), revertByPk(pkColumns, columns). The executor writes only
|
|
5155
|
+
* the touched cells into the manual source; `returning` reads the view.
|
|
5156
|
+
*/
|
|
5157
|
+
lowerResolvedRoot(node, rootInfo) {
|
|
5158
|
+
const name = node.name.value;
|
|
5159
|
+
const alias = node.alias?.value ?? name;
|
|
5160
|
+
const { table, action } = rootInfo;
|
|
5161
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
5162
|
+
const args = argsByName(node);
|
|
5163
|
+
const base = {
|
|
5164
|
+
kind: "mutation",
|
|
5165
|
+
op: "resolvedWrite",
|
|
5166
|
+
resolvedAction: action,
|
|
5167
|
+
alias,
|
|
5168
|
+
table,
|
|
5169
|
+
pinnedColumns: [],
|
|
5170
|
+
affectedRows: false,
|
|
5171
|
+
returning: this.lowerSelection(node.selectionSet, table)
|
|
5172
|
+
};
|
|
5173
|
+
switch (action) {
|
|
5174
|
+
case "insert": {
|
|
5175
|
+
assertKnownArgs(node, ["object"]);
|
|
5176
|
+
const object = args.get("object");
|
|
5177
|
+
if (!object) fail2(`'${name}' needs object:`, "missing-argument");
|
|
5178
|
+
return { ...base, objects: this.lowerInsertObjects(object.value, table, { list: false }) };
|
|
5179
|
+
}
|
|
5180
|
+
case "update": {
|
|
5181
|
+
assertKnownArgs(node, ["pkColumns", "_set"]);
|
|
5182
|
+
const pkColumns = args.get("pkColumns");
|
|
5183
|
+
if (!pkColumns || !args.get("_set")) fail2(`'${name}' needs pkColumns: and _set:`, "missing-argument");
|
|
5184
|
+
return { ...base, pk: this.lowerPkColumnsArg(pkColumns.value, table), ...this.lowerWriteValues(args, table) };
|
|
5185
|
+
}
|
|
5186
|
+
case "delete":
|
|
5187
|
+
return { ...base, pk: this.lowerPkArgs(node, table) };
|
|
5188
|
+
case "revert": {
|
|
5189
|
+
assertKnownArgs(node, ["pkColumns", "columns"]);
|
|
5190
|
+
const pkColumns = args.get("pkColumns");
|
|
5191
|
+
const columns = args.get("columns");
|
|
5192
|
+
if (!pkColumns || !columns) fail2(`'${name}' needs pkColumns: and columns:`, "missing-argument");
|
|
5193
|
+
const items = columns.value.kind === graphql.Kind.LIST ? columns.value.values : [columns.value];
|
|
5194
|
+
const meta = this.meta(table);
|
|
5195
|
+
const revertColumns = items.map((item) => {
|
|
5196
|
+
if (item.kind !== graphql.Kind.ENUM || !meta.settable.includes(item.value)) {
|
|
5197
|
+
fail2(`'${name}' columns: must list update-column enum values written in the document`, "invalid-revert-columns", meta.settable);
|
|
5198
|
+
}
|
|
5199
|
+
return item.value;
|
|
5200
|
+
});
|
|
5201
|
+
if (revertColumns.length === 0) fail2(`'${name}' columns: must name at least one column`, "invalid-revert-columns");
|
|
5202
|
+
return { ...base, pk: this.lowerPkColumnsArg(pkColumns.value, table), revertColumns: [...new Set(revertColumns)] };
|
|
5203
|
+
}
|
|
5204
|
+
}
|
|
5205
|
+
}
|
|
4014
5206
|
lowerMutationResponse(selectionSet, table) {
|
|
4015
5207
|
let affectedRows = false;
|
|
4016
5208
|
let returning = null;
|
|
4017
5209
|
for (const node of selectionSet.selections) {
|
|
4018
|
-
if (node.kind !== graphql.Kind.FIELD)
|
|
5210
|
+
if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
4019
5211
|
const name = node.name.value;
|
|
4020
5212
|
if (name === "__typename") continue;
|
|
4021
5213
|
if (name === "affectedRows") {
|
|
@@ -4023,34 +5215,34 @@ var OperationLowerer = class {
|
|
|
4023
5215
|
continue;
|
|
4024
5216
|
}
|
|
4025
5217
|
if (name === "returning") {
|
|
4026
|
-
if (!node.selectionSet)
|
|
5218
|
+
if (!node.selectionSet) fail2("'returning' needs a selection set", "invalid-selection");
|
|
4027
5219
|
returning = this.lowerSelection(node.selectionSet, table);
|
|
4028
5220
|
continue;
|
|
4029
5221
|
}
|
|
4030
|
-
|
|
5222
|
+
fail2(`unknown mutation-response field '${name}'`, "invalid-selection", ["affectedRows", "returning"]);
|
|
4031
5223
|
}
|
|
4032
5224
|
return { affectedRows, returning };
|
|
4033
5225
|
}
|
|
4034
5226
|
lowerInsertObjects(node, table, { list }) {
|
|
4035
5227
|
if (node.kind === graphql.Kind.VARIABLE) return { slot: this.slot(node.name.value, "insertInput", table) };
|
|
4036
5228
|
if (list) {
|
|
4037
|
-
if (node.kind !== graphql.Kind.LIST)
|
|
5229
|
+
if (node.kind !== graphql.Kind.LIST) fail2(`objects: must be a list literal or a variable`, "invalid-insert");
|
|
4038
5230
|
return { rows: node.values.map((item) => this.lowerInsertRow(item, table)) };
|
|
4039
5231
|
}
|
|
4040
5232
|
return { rows: [this.lowerInsertRow(node, table)] };
|
|
4041
5233
|
}
|
|
4042
5234
|
lowerInsertRow(node, table) {
|
|
4043
|
-
if (node.kind !== graphql.Kind.OBJECT)
|
|
5235
|
+
if (node.kind !== graphql.Kind.OBJECT) fail2(`insert rows on '${table}' must be objects`, "invalid-insert");
|
|
4044
5236
|
const meta = this.meta(table);
|
|
4045
5237
|
return node.fields.map((field) => {
|
|
4046
5238
|
const key = field.name.value;
|
|
4047
5239
|
if (this.link(table, key)) {
|
|
4048
|
-
|
|
5240
|
+
fail2(`nested inserts are not supported in v2.0 \u2014 '${key}' is a link on '${table}'`, "nested-insert-unsupported");
|
|
4049
5241
|
}
|
|
4050
5242
|
if (!meta.insertable.includes(key)) {
|
|
4051
5243
|
const column = this.column(table, key);
|
|
4052
|
-
if (column)
|
|
4053
|
-
|
|
5244
|
+
if (column) fail2(`column '${table}.${key}' is not insertable (pinned by the write policy or deprecated)`, "column-not-insertable", meta.insertable);
|
|
5245
|
+
fail2(`unknown column '${key}' in an insert on '${table}'`, "unknown-column", meta.insertable);
|
|
4054
5246
|
}
|
|
4055
5247
|
return { column: key, value: this.lowerValue(field.value) };
|
|
4056
5248
|
});
|
|
@@ -4059,32 +5251,32 @@ var OperationLowerer = class {
|
|
|
4059
5251
|
if (!arg) return {};
|
|
4060
5252
|
const node = arg.value;
|
|
4061
5253
|
if (node.kind === graphql.Kind.VARIABLE) return { onConflict: { slot: this.slot(node.name.value, "onConflict", table) } };
|
|
4062
|
-
if (node.kind !== graphql.Kind.OBJECT)
|
|
5254
|
+
if (node.kind !== graphql.Kind.OBJECT) fail2("onConflict: must be an object literal or a variable", "invalid-on-conflict");
|
|
4063
5255
|
const meta = this.meta(table);
|
|
4064
5256
|
let constraint;
|
|
4065
5257
|
const updateColumns = [];
|
|
4066
5258
|
for (const field of node.fields) {
|
|
4067
5259
|
if (field.name.value === "constraint") {
|
|
4068
|
-
if (field.value.kind !== graphql.Kind.ENUM)
|
|
5260
|
+
if (field.value.kind !== graphql.Kind.ENUM) fail2("onConflict.constraint must be a constraint enum value", "invalid-on-conflict");
|
|
4069
5261
|
constraint = field.value.value;
|
|
4070
5262
|
continue;
|
|
4071
5263
|
}
|
|
4072
5264
|
if (field.name.value === "updateColumns") {
|
|
4073
5265
|
const items = field.value.kind === graphql.Kind.LIST ? field.value.values : [field.value];
|
|
4074
5266
|
for (const item of items) {
|
|
4075
|
-
if (item.kind !== graphql.Kind.ENUM)
|
|
5267
|
+
if (item.kind !== graphql.Kind.ENUM) fail2("onConflict.updateColumns must be update-column enum values", "invalid-on-conflict");
|
|
4076
5268
|
if (!meta.settable.includes(item.value)) {
|
|
4077
|
-
|
|
5269
|
+
fail2(`onConflict.updateColumns: '${item.value}' is not updatable on '${table}'`, "invalid-on-conflict", meta.settable);
|
|
4078
5270
|
}
|
|
4079
5271
|
updateColumns.push(item.value);
|
|
4080
5272
|
}
|
|
4081
5273
|
continue;
|
|
4082
5274
|
}
|
|
4083
|
-
|
|
5275
|
+
fail2(`unknown onConflict key '${field.name.value}'`, "invalid-on-conflict", ["constraint", "updateColumns"]);
|
|
4084
5276
|
}
|
|
4085
|
-
if (!constraint)
|
|
5277
|
+
if (!constraint) fail2("onConflict needs a constraint", "invalid-on-conflict", meta.constraints.map((c) => c.name));
|
|
4086
5278
|
if (!meta.constraints.some((c) => c.name === constraint)) {
|
|
4087
|
-
|
|
5279
|
+
fail2(`unknown constraint '${constraint}' on '${table}'`, "invalid-on-conflict", meta.constraints.map((c) => c.name));
|
|
4088
5280
|
}
|
|
4089
5281
|
return { onConflict: { constraint, updateColumns } };
|
|
4090
5282
|
}
|
|
@@ -4102,36 +5294,36 @@ var OperationLowerer = class {
|
|
|
4102
5294
|
const inc = args.get("_inc");
|
|
4103
5295
|
if (inc) {
|
|
4104
5296
|
if (inc.value.kind === graphql.Kind.VARIABLE) {
|
|
4105
|
-
|
|
5297
|
+
fail2("_inc has no dynamic input type \u2014 inline the columns with variable leaf values", "invalid-inc");
|
|
4106
5298
|
}
|
|
4107
5299
|
const numeric4 = meta.columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type) && meta.settable.includes(c.name)).map((c) => c.name);
|
|
4108
5300
|
result.inc = { entries: this.lowerColumnEntries(inc.value, table, numeric4, "_inc") };
|
|
4109
5301
|
}
|
|
4110
5302
|
if (!result.set && !result.append && !result.inc) {
|
|
4111
|
-
|
|
5303
|
+
fail2(`an update on '${table}' needs at least one of _set/_append/_inc`, "missing-argument");
|
|
4112
5304
|
}
|
|
4113
5305
|
return result;
|
|
4114
5306
|
}
|
|
4115
5307
|
lowerColumnEntries(node, table, whitelist, argName) {
|
|
4116
|
-
if (node.kind !== graphql.Kind.OBJECT)
|
|
5308
|
+
if (node.kind !== graphql.Kind.OBJECT) fail2(`${argName} on '${table}' must be an object literal or a variable`, "invalid-write-values");
|
|
4117
5309
|
return node.fields.map((field) => {
|
|
4118
5310
|
const key = field.name.value;
|
|
4119
5311
|
if (!whitelist.includes(key)) {
|
|
4120
|
-
|
|
5312
|
+
fail2(`${argName}: column '${key}' is not allowed on '${table}'`, "column-not-settable", [...whitelist]);
|
|
4121
5313
|
}
|
|
4122
5314
|
return { column: key, value: this.lowerValue(field.value) };
|
|
4123
5315
|
});
|
|
4124
5316
|
}
|
|
4125
5317
|
lowerPkColumnsArg(node, table) {
|
|
4126
|
-
if (node.kind !== graphql.Kind.OBJECT)
|
|
5318
|
+
if (node.kind !== graphql.Kind.OBJECT) fail2(`pkColumns on '${table}' must be an object literal`, "invalid-pk-columns");
|
|
4127
5319
|
const meta = this.meta(table);
|
|
4128
5320
|
const provided = new Map(node.fields.map((field) => [field.name.value, field.value]));
|
|
4129
5321
|
for (const key of provided.keys()) {
|
|
4130
|
-
if (!meta.primaryKey.includes(key))
|
|
5322
|
+
if (!meta.primaryKey.includes(key)) fail2(`pkColumns: '${key}' is not a primary-key column of '${table}'`, "invalid-pk-columns", meta.primaryKey);
|
|
4131
5323
|
}
|
|
4132
5324
|
return meta.primaryKey.map((column) => {
|
|
4133
5325
|
const value = provided.get(column);
|
|
4134
|
-
if (!value)
|
|
5326
|
+
if (!value) fail2(`pkColumns is missing '${column}'`, "invalid-pk-columns", meta.primaryKey);
|
|
4135
5327
|
return { column, value: this.lowerValue(value) };
|
|
4136
5328
|
});
|
|
4137
5329
|
}
|
|
@@ -4140,6 +5332,19 @@ function lowerOperation({ restricted, ctx }) {
|
|
|
4140
5332
|
const lowerer = new OperationLowerer(ctx, restricted);
|
|
4141
5333
|
return lowerer.lowerRoots(restricted.definition, restricted.kind);
|
|
4142
5334
|
}
|
|
5335
|
+
function isCellColumn(meta, name) {
|
|
5336
|
+
if (!meta.cells) return false;
|
|
5337
|
+
return Object.values(meta.cells).some((parts) => Object.values(parts).includes(name));
|
|
5338
|
+
}
|
|
5339
|
+
function fieldNodes(selectionSet) {
|
|
5340
|
+
const out = [];
|
|
5341
|
+
for (const node of selectionSet.selections) {
|
|
5342
|
+
if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
5343
|
+
if (node.directives && node.directives.length > 0) fail2(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
|
|
5344
|
+
if (node.name.value !== "__typename") out.push(node);
|
|
5345
|
+
}
|
|
5346
|
+
return out;
|
|
5347
|
+
}
|
|
4143
5348
|
function parseSdl(sdl) {
|
|
4144
5349
|
return graphql.buildSchema(sdl);
|
|
4145
5350
|
}
|
|
@@ -4516,15 +5721,6 @@ function columnWireType(column) {
|
|
|
4516
5721
|
const notNull = column.primary_key === true || column.nullable === false;
|
|
4517
5722
|
return notNull || base === "unknown" ? base : `${base} | null`;
|
|
4518
5723
|
}
|
|
4519
|
-
|
|
4520
|
-
// src/compiler/sql.ts
|
|
4521
|
-
var CLAIMS_SETTING = "request.jwt.claims";
|
|
4522
|
-
var CAPABILITIES_CLAIM_KEY = "capabilities";
|
|
4523
|
-
var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
4524
|
-
var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
4525
|
-
var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
|
|
4526
|
-
var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
|
|
4527
|
-
var capabilityExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
4528
5724
|
var identifierSchema = zod.z.string().max(MAX_IDENTIFIER_LENGTH).regex(IDENTIFIER_PATTERN, "must be a lowercase identifier (a-z, 0-9, _)");
|
|
4529
5725
|
var namespaceSchema = zod.z.string().regex(NAMESPACE_PATTERN, "must be lowercase, start with a letter, and be at most 41 characters");
|
|
4530
5726
|
var tableReferenceSchema = zod.z.string().refine(
|
|
@@ -4641,7 +5837,7 @@ var OPERATOR_PATTERN2 = /^(!=|<=|>=|<>|=|<|>)/;
|
|
|
4641
5837
|
var IDENTIFIER_START2 = /[a-z_]/;
|
|
4642
5838
|
var IDENTIFIER_CHAR2 = /[a-z0-9_.-]/;
|
|
4643
5839
|
var NUMBER_PATTERN2 = /^-?\d+(\.\d+)?/;
|
|
4644
|
-
function
|
|
5840
|
+
function tokenize3(source) {
|
|
4645
5841
|
const tokens = [];
|
|
4646
5842
|
let position = 0;
|
|
4647
5843
|
while (position < source.length) {
|
|
@@ -4715,7 +5911,7 @@ function readString2(source, start) {
|
|
|
4715
5911
|
}
|
|
4716
5912
|
throw new PredicateParseError("unterminated string literal", start);
|
|
4717
5913
|
}
|
|
4718
|
-
var
|
|
5914
|
+
var Parser3 = class {
|
|
4719
5915
|
constructor(tokens) {
|
|
4720
5916
|
this.tokens = tokens;
|
|
4721
5917
|
}
|
|
@@ -4847,7 +6043,7 @@ var Parser2 = class {
|
|
|
4847
6043
|
function parsePredicate(source) {
|
|
4848
6044
|
const trimmed = source.trim();
|
|
4849
6045
|
if (trimmed.length === 0) throw new PredicateParseError("empty predicate", 0);
|
|
4850
|
-
return { source: trimmed, ast: new
|
|
6046
|
+
return { source: trimmed, ast: new Parser3(tokenize3(trimmed)).parse() };
|
|
4851
6047
|
}
|
|
4852
6048
|
|
|
4853
6049
|
// src/schema/predicates.ts
|
|
@@ -5569,6 +6765,88 @@ function snapshotTableNames(snapshot, schema) {
|
|
|
5569
6765
|
return Object.values(tables).filter((entry) => entry.schema === schema).map((entry) => entry.name);
|
|
5570
6766
|
}
|
|
5571
6767
|
|
|
6768
|
+
// src/engine/materialized-refresh.ts
|
|
6769
|
+
function materializedRefreshFunctions(canonical) {
|
|
6770
|
+
const schema = ormSchemaName(canonical.namespace);
|
|
6771
|
+
const functions = /* @__PURE__ */ new Map();
|
|
6772
|
+
for (const view of canonical.views ?? []) {
|
|
6773
|
+
const materialized = view.resolved?.materialized;
|
|
6774
|
+
if (!materialized) continue;
|
|
6775
|
+
const table = canonical.tables.find((candidate) => candidate.table === materialized.table);
|
|
6776
|
+
const pk = view.primaryKey ?? [];
|
|
6777
|
+
if (!table || pk.length === 0) {
|
|
6778
|
+
throw new OrmDefinitionError(`materialized resolved table '${view.view}' needs its table and primary key`);
|
|
6779
|
+
}
|
|
6780
|
+
functions.set(view.view, refreshFunctionSql({
|
|
6781
|
+
schema,
|
|
6782
|
+
name: view.view,
|
|
6783
|
+
table: materialized.table,
|
|
6784
|
+
compute: materialized.compute,
|
|
6785
|
+
columns: table.columns.map((column) => column.columnName),
|
|
6786
|
+
pk
|
|
6787
|
+
}));
|
|
6788
|
+
}
|
|
6789
|
+
return functions;
|
|
6790
|
+
}
|
|
6791
|
+
function refreshFunctionSql({ schema, name, table, compute, columns, pk }) {
|
|
6792
|
+
const fn = `${quoteIdentifier(schema)}.${quoteIdentifier(materializedRefreshFunctionName(name))}`;
|
|
6793
|
+
const target = `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;
|
|
6794
|
+
const source = `${quoteIdentifier(schema)}.${quoteIdentifier(compute)}`;
|
|
6795
|
+
const list = columns.map(quoteIdentifier).join(", ");
|
|
6796
|
+
const others = columns.filter((column) => !pk.includes(column));
|
|
6797
|
+
const keys = pk.map(quoteIdentifier).join(", ");
|
|
6798
|
+
const matches = pk.map((column) => `c.${quoteIdentifier(column)} = m.${quoteIdentifier(column)}`).join(" AND ");
|
|
6799
|
+
const onConflict = others.length === 0 ? "DO NOTHING" : `DO UPDATE SET ${others.map((column) => `${quoteIdentifier(column)} = EXCLUDED.${quoteIdentifier(column)}`).join(", ")}
|
|
6800
|
+
WHERE (${others.map((column) => `m.${quoteIdentifier(column)}`).join(", ")}) IS DISTINCT FROM (${others.map((column) => `EXCLUDED.${quoteIdentifier(column)}`).join(", ")})`;
|
|
6801
|
+
const lockKey = quoteLiteral(materializeLockKey(schema, name));
|
|
6802
|
+
return `CREATE OR REPLACE FUNCTION ${fn}(wait boolean DEFAULT false)
|
|
6803
|
+
RETURNS TABLE(upserted bigint, deleted bigint, locked boolean)
|
|
6804
|
+
LANGUAGE plpgsql
|
|
6805
|
+
SECURITY INVOKER
|
|
6806
|
+
SET search_path = pg_catalog, pg_temp
|
|
6807
|
+
AS $orm_refresh$
|
|
6808
|
+
#variable_conflict use_column
|
|
6809
|
+
DECLARE
|
|
6810
|
+
caller_claims text := current_setting('request.jwt.claims', true);
|
|
6811
|
+
BEGIN
|
|
6812
|
+
IF wait THEN
|
|
6813
|
+
PERFORM pg_advisory_xact_lock(hashtext(${lockKey}));
|
|
6814
|
+
ELSIF NOT pg_try_advisory_xact_lock(hashtext(${lockKey})) THEN
|
|
6815
|
+
RETURN QUERY SELECT 0::bigint, 0::bigint, false;
|
|
6816
|
+
RETURN;
|
|
6817
|
+
END IF;
|
|
6818
|
+
PERFORM set_config('request.jwt.claims', ${quoteLiteral(MATERIALIZE_CLAIMS_JSON)}, true);
|
|
6819
|
+
RETURN QUERY
|
|
6820
|
+
WITH up AS (
|
|
6821
|
+
INSERT INTO ${target} AS m (${list})
|
|
6822
|
+
SELECT ${list} FROM ${source}
|
|
6823
|
+
ON CONFLICT (${keys}) ${onConflict}
|
|
6824
|
+
RETURNING 1
|
|
6825
|
+
), gone AS (
|
|
6826
|
+
DELETE FROM ${target} AS m
|
|
6827
|
+
WHERE NOT EXISTS (SELECT 1 FROM ${source} AS c WHERE ${matches})
|
|
6828
|
+
RETURNING 1
|
|
6829
|
+
)
|
|
6830
|
+
SELECT (SELECT count(*) FROM up), (SELECT count(*) FROM gone), true;
|
|
6831
|
+
-- NULL resets to the session value when the caller had none.
|
|
6832
|
+
PERFORM set_config('request.jwt.claims', caller_claims, true);
|
|
6833
|
+
END
|
|
6834
|
+
$orm_refresh$`;
|
|
6835
|
+
}
|
|
6836
|
+
function refreshFunctionStatements({ namespace, previous, current }) {
|
|
6837
|
+
const schema = quoteIdentifier(ormSchemaName(namespace));
|
|
6838
|
+
const statements = [];
|
|
6839
|
+
for (const name of Object.keys(previous).sort()) {
|
|
6840
|
+
if (!current.has(name)) statements.push(`DROP FUNCTION IF EXISTS ${schema}.${quoteIdentifier(materializedRefreshFunctionName(name))}(boolean)`);
|
|
6841
|
+
}
|
|
6842
|
+
for (const [name, create] of [...current].sort(([a], [b]) => a.localeCompare(b))) {
|
|
6843
|
+
if (previous[name] === create) continue;
|
|
6844
|
+
const fn = `${schema}.${quoteIdentifier(materializedRefreshFunctionName(name))}(boolean)`;
|
|
6845
|
+
statements.push(create, `REVOKE ALL ON FUNCTION ${fn} FROM PUBLIC`);
|
|
6846
|
+
}
|
|
6847
|
+
return statements;
|
|
6848
|
+
}
|
|
6849
|
+
|
|
5572
6850
|
// src/engine/canonical2.ts
|
|
5573
6851
|
var FACTORIES2 = {
|
|
5574
6852
|
uuid: (name) => pgCore.uuid(name),
|
|
@@ -5860,8 +7138,10 @@ function generateSnapshotV2(definition, previousId3) {
|
|
|
5860
7138
|
const snapshot = api.generateDrizzleJson(imports, previousId3, [schemaName]);
|
|
5861
7139
|
if ((definition.views?.length ?? 0) > 0 || definition.tables.some((table) => table.foreignKeys?.length)) {
|
|
5862
7140
|
const extended = snapshot;
|
|
7141
|
+
const refreshFunctions = materializedRefreshFunctions(definition);
|
|
5863
7142
|
extended._sequenceOrm = {
|
|
5864
7143
|
...definition.tables.some((table) => table.foreignKeys?.length) ? { foreignKeys: Object.fromEntries(definition.tables.flatMap((table) => (table.foreignKeys ?? []).map((fk) => [`${table.table}.${fk.name}`, structuredClone(fk)]))) } : {},
|
|
7144
|
+
...refreshFunctions.size > 0 ? { refreshFunctions: Object.fromEntries(refreshFunctions) } : {},
|
|
5865
7145
|
viewDependencies: Object.fromEntries(
|
|
5866
7146
|
(definition.views ?? []).map((view) => [view.view, [...view.dependencies].sort()])
|
|
5867
7147
|
)
|
|
@@ -5901,15 +7181,19 @@ function buildTable2(schema, table, built, enums, byName, introspection) {
|
|
|
5901
7181
|
const drizzleTable = schema.table(table.table, columns, (self) => [
|
|
5902
7182
|
...buildCompositePrimaryKey(table, primaryKeyColumns2, self),
|
|
5903
7183
|
...buildIndexes2(table, self),
|
|
7184
|
+
...(table.checks ?? []).map((check) => pgCore.check(check.name, drizzleOrm.sql.raw(compileCheckExpression(check.expression, table.columns)))),
|
|
5904
7185
|
...buildForeignKeys2(table, self, built, byName),
|
|
5905
7186
|
...(table.foreignKeys ?? []).map((fk) => {
|
|
5906
7187
|
const target = fk.target === table.table ? self : built.get(fk.target);
|
|
5907
7188
|
if (!target) throw new OrmDefinitionError(`foreign key '${fk.name}' has no target '${fk.target}'`);
|
|
5908
|
-
|
|
7189
|
+
let constraint = pgCore.foreignKey({
|
|
5909
7190
|
name: fk.name,
|
|
5910
7191
|
columns: fk.columns.map((column) => self[column]),
|
|
5911
7192
|
foreignColumns: fk.references.map((column) => target[column])
|
|
5912
7193
|
});
|
|
7194
|
+
if (fk.onDelete !== void 0) constraint = constraint.onDelete(fk.onDelete);
|
|
7195
|
+
if (fk.onUpdate !== void 0) constraint = constraint.onUpdate(fk.onUpdate);
|
|
7196
|
+
return constraint;
|
|
5913
7197
|
})
|
|
5914
7198
|
]);
|
|
5915
7199
|
return drizzleTable.enableRLS();
|
|
@@ -6000,11 +7284,14 @@ function buildForeignKeys2(table, self, built, byName) {
|
|
|
6000
7284
|
`enforced link '${link.name}' on '${table.table}' references unknown column '${targetColumn ? link.on : referencedColumn}'`
|
|
6001
7285
|
);
|
|
6002
7286
|
}
|
|
6003
|
-
|
|
7287
|
+
let constraint = pgCore.foreignKey({
|
|
6004
7288
|
name: foreignKeyName(table.table, link.name),
|
|
6005
7289
|
columns: [sourceColumn],
|
|
6006
7290
|
foreignColumns: [targetColumn]
|
|
6007
7291
|
});
|
|
7292
|
+
if (link.onDelete !== void 0) constraint = constraint.onDelete(link.onDelete);
|
|
7293
|
+
if (link.onUpdate !== void 0) constraint = constraint.onUpdate(link.onUpdate);
|
|
7294
|
+
return constraint;
|
|
6008
7295
|
});
|
|
6009
7296
|
}
|
|
6010
7297
|
var singlePrimaryKeyOf = (table) => table.primaryKey?.length === 1 ? table.primaryKey[0] : void 0;
|
|
@@ -6125,9 +7412,23 @@ function findTable2(snapshot, schema, table) {
|
|
|
6125
7412
|
const tables = snapshot.tables ?? {};
|
|
6126
7413
|
return Object.values(tables).find((entry) => entry.schema === schema && entry.name === table);
|
|
6127
7414
|
}
|
|
6128
|
-
|
|
7415
|
+
function planMigrationV2(input) {
|
|
7416
|
+
return planWithViewRecreation({ ...input, viewRecreation: "affected" });
|
|
7417
|
+
}
|
|
7418
|
+
async function planCommittedMigrationV2({
|
|
6129
7419
|
canonical,
|
|
6130
|
-
previousSnapshot
|
|
7420
|
+
previousSnapshot,
|
|
7421
|
+
reproduces
|
|
7422
|
+
}) {
|
|
7423
|
+
const current = await planMigrationV2({ canonical, previousSnapshot });
|
|
7424
|
+
if (reproduces(current)) return { plan: current, reproduced: true };
|
|
7425
|
+
const legacy = await planWithViewRecreation({ canonical, previousSnapshot, viewRecreation: "all-on-base-change" });
|
|
7426
|
+
return reproduces(legacy) ? { plan: legacy, reproduced: true } : { plan: current, reproduced: false };
|
|
7427
|
+
}
|
|
7428
|
+
async function planWithViewRecreation({
|
|
7429
|
+
canonical,
|
|
7430
|
+
previousSnapshot,
|
|
7431
|
+
viewRecreation
|
|
6131
7432
|
}) {
|
|
6132
7433
|
const schema = ormSchemaName(canonical.namespace);
|
|
6133
7434
|
const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
|
|
@@ -6145,7 +7446,14 @@ async function planMigrationV2({
|
|
|
6145
7446
|
currentViews,
|
|
6146
7447
|
previousDependencies,
|
|
6147
7448
|
currentDependencies,
|
|
6148
|
-
|
|
7449
|
+
reach: viewReach({
|
|
7450
|
+
viewRecreation,
|
|
7451
|
+
hintStatements: hints.statements,
|
|
7452
|
+
baseDdl,
|
|
7453
|
+
previous,
|
|
7454
|
+
adjusted: hints.adjusted,
|
|
7455
|
+
current: snapshot
|
|
7456
|
+
})
|
|
6149
7457
|
});
|
|
6150
7458
|
const dropOrder = topologicalViewNames(
|
|
6151
7459
|
new Set([...affected].filter((name) => previousViews.has(name))),
|
|
@@ -6178,7 +7486,12 @@ async function planMigrationV2({
|
|
|
6178
7486
|
const view = currentViewDefinitions.get(name);
|
|
6179
7487
|
if (!view) throw new OrmDefinitionError(`missing canonical definition for view '${name}'`);
|
|
6180
7488
|
return { kind: "ddl", sql: validateViewCatalogStatement({ canonical, view, schema }) };
|
|
6181
|
-
})
|
|
7489
|
+
}),
|
|
7490
|
+
...refreshFunctionStatements({
|
|
7491
|
+
namespace: canonical.namespace,
|
|
7492
|
+
previous: previous._sequenceOrm?.refreshFunctions ?? {},
|
|
7493
|
+
current: materializedRefreshFunctions(canonical)
|
|
7494
|
+
}).map((sql4) => ({ kind: "ddl", sql: sql4 }))
|
|
6182
7495
|
];
|
|
6183
7496
|
return { namespace: canonical.namespace, schema, statements, snapshot };
|
|
6184
7497
|
}
|
|
@@ -6332,20 +7645,102 @@ function withoutViews(snapshot) {
|
|
|
6332
7645
|
delete clone._sequenceOrm;
|
|
6333
7646
|
return clone;
|
|
6334
7647
|
}
|
|
7648
|
+
var DROPS_TYPE = /^\s*DROP\s+TYPE\b/i;
|
|
7649
|
+
function viewReach({
|
|
7650
|
+
viewRecreation,
|
|
7651
|
+
hintStatements,
|
|
7652
|
+
baseDdl,
|
|
7653
|
+
previous,
|
|
7654
|
+
adjusted,
|
|
7655
|
+
current
|
|
7656
|
+
}) {
|
|
7657
|
+
if (hintStatements.length === 0 && baseDdl.length === 0) return /* @__PURE__ */ new Set();
|
|
7658
|
+
if (viewRecreation === "all-on-base-change") return "all";
|
|
7659
|
+
if (baseDdl.some((statement) => DROPS_TYPE.test(statement))) return "all";
|
|
7660
|
+
const dependencies = snapshotViewDependencies(adjusted);
|
|
7661
|
+
if ([...snapshotViews(adjusted).keys()].some((name) => dependencies[name] === void 0)) return "all";
|
|
7662
|
+
return baseChangesReachingViews({ previous, adjusted, current });
|
|
7663
|
+
}
|
|
7664
|
+
var SNAPSHOT_BOOKKEEPING = /* @__PURE__ */ new Set(["id", "prevId", "version", "dialect", "_meta", "views", "_sequenceOrm", "enums"]);
|
|
7665
|
+
var VIEW_SAFE_TABLE_FIELDS = /* @__PURE__ */ new Set([
|
|
7666
|
+
"indexes",
|
|
7667
|
+
"foreignKeys",
|
|
7668
|
+
"uniqueConstraints",
|
|
7669
|
+
"checkConstraints",
|
|
7670
|
+
"policies",
|
|
7671
|
+
"isRLSEnabled"
|
|
7672
|
+
]);
|
|
7673
|
+
var VIEW_SAFE_COLUMN_FIELDS = /* @__PURE__ */ new Set(["notNull", "default", "isUnique", "uniqueName", "nullsNotDistinct"]);
|
|
7674
|
+
function baseChangesReachingViews({
|
|
7675
|
+
previous,
|
|
7676
|
+
adjusted,
|
|
7677
|
+
current
|
|
7678
|
+
}) {
|
|
7679
|
+
const before = adjusted;
|
|
7680
|
+
const after = current;
|
|
7681
|
+
for (const section of /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])) {
|
|
7682
|
+
if (SNAPSHOT_BOOKKEEPING.has(section) || section === "tables") continue;
|
|
7683
|
+
if (stableJson(before[section]) !== stableJson(after[section])) return "all";
|
|
7684
|
+
}
|
|
7685
|
+
const relations = /* @__PURE__ */ new Set();
|
|
7686
|
+
const previousTables = snapshotTables(previous);
|
|
7687
|
+
const adjustedTables = snapshotTables(adjusted);
|
|
7688
|
+
for (const key of changedEntryKeys(previousTables, adjustedTables)) {
|
|
7689
|
+
const table = previousTables[key] ?? adjustedTables[key];
|
|
7690
|
+
if (table) relations.add(table.name);
|
|
7691
|
+
}
|
|
7692
|
+
const currentTables = snapshotTables(current);
|
|
7693
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(adjustedTables), ...Object.keys(currentTables)])) {
|
|
7694
|
+
const was = adjustedTables[key];
|
|
7695
|
+
const is = currentTables[key];
|
|
7696
|
+
if (!was || !is) {
|
|
7697
|
+
const table = was ?? is;
|
|
7698
|
+
if (table) relations.add(table.name);
|
|
7699
|
+
} else if (tableChangeReachesViews({ was, is })) {
|
|
7700
|
+
relations.add(is.name);
|
|
7701
|
+
}
|
|
7702
|
+
}
|
|
7703
|
+
return relations;
|
|
7704
|
+
}
|
|
7705
|
+
function tableChangeReachesViews({ was, is }) {
|
|
7706
|
+
for (const field of /* @__PURE__ */ new Set([...Object.keys(was), ...Object.keys(is)])) {
|
|
7707
|
+
if (field === "columns" || VIEW_SAFE_TABLE_FIELDS.has(field)) continue;
|
|
7708
|
+
if (stableJson(was[field]) !== stableJson(is[field])) return true;
|
|
7709
|
+
}
|
|
7710
|
+
return Object.entries(was.columns).some(([name, column]) => {
|
|
7711
|
+
const next = is.columns[name];
|
|
7712
|
+
if (!next) return true;
|
|
7713
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(column), ...Object.keys(next)])].some(
|
|
7714
|
+
(field) => !VIEW_SAFE_COLUMN_FIELDS.has(field) && stableJson(column[field]) !== stableJson(next[field])
|
|
7715
|
+
);
|
|
7716
|
+
});
|
|
7717
|
+
}
|
|
7718
|
+
function changedEntryKeys(before, after) {
|
|
7719
|
+
return new Set(
|
|
7720
|
+
[.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].filter(
|
|
7721
|
+
(key) => stableJson(before[key]) !== stableJson(after[key])
|
|
7722
|
+
)
|
|
7723
|
+
);
|
|
7724
|
+
}
|
|
7725
|
+
function snapshotTables(snapshot) {
|
|
7726
|
+
return snapshot.tables ?? {};
|
|
7727
|
+
}
|
|
6335
7728
|
function affectedViewNames({
|
|
6336
7729
|
previousViews,
|
|
6337
7730
|
currentViews,
|
|
6338
7731
|
previousDependencies,
|
|
6339
7732
|
currentDependencies,
|
|
6340
|
-
|
|
7733
|
+
reach
|
|
6341
7734
|
}) {
|
|
6342
7735
|
const allNames = /* @__PURE__ */ new Set([...previousViews.keys(), ...currentViews.keys()]);
|
|
6343
|
-
if (
|
|
7736
|
+
if (reach === "all") return allNames;
|
|
7737
|
+
const dependenciesOf = (name) => /* @__PURE__ */ new Set([...previousDependencies[name] ?? [], ...currentDependencies[name] ?? []]);
|
|
6344
7738
|
const affected = new Set(
|
|
6345
7739
|
[...allNames].filter((name) => {
|
|
6346
7740
|
const before = previousViews.get(name);
|
|
6347
7741
|
const after = currentViews.get(name);
|
|
6348
|
-
|
|
7742
|
+
if (before === void 0 || after === void 0 || stableJson(before) !== stableJson(after)) return true;
|
|
7743
|
+
return [...dependenciesOf(name)].some((dependency) => reach.has(dependency));
|
|
6349
7744
|
})
|
|
6350
7745
|
);
|
|
6351
7746
|
let grew = true;
|
|
@@ -6353,8 +7748,7 @@ function affectedViewNames({
|
|
|
6353
7748
|
grew = false;
|
|
6354
7749
|
for (const name of allNames) {
|
|
6355
7750
|
if (affected.has(name)) continue;
|
|
6356
|
-
|
|
6357
|
-
if ([...dependencies].some((dependency) => affected.has(dependency))) {
|
|
7751
|
+
if ([...dependenciesOf(name)].some((dependency) => affected.has(dependency))) {
|
|
6358
7752
|
affected.add(name);
|
|
6359
7753
|
grew = true;
|
|
6360
7754
|
}
|
|
@@ -7269,7 +8663,9 @@ var PLATFORM_USER_FIELDS = [
|
|
|
7269
8663
|
];
|
|
7270
8664
|
function generateSdl(canonical, options = {}) {
|
|
7271
8665
|
const def = normalizeCanonical(canonicalDefinitionSchema.parse(canonical));
|
|
7272
|
-
const
|
|
8666
|
+
const exposed = canonicalRelations(def).filter((table) => table.source === void 0 && table.internal !== true);
|
|
8667
|
+
const cellsByTable = new Map(exposed.map((table) => [table.table, trackedCells(table)]));
|
|
8668
|
+
const tables = exposed.map((table) => ({ ...table, columns: table.columns.filter((column) => column.cell === void 0) }));
|
|
7273
8669
|
if (tables.length === 0) {
|
|
7274
8670
|
throw new SdlGenerationError(
|
|
7275
8671
|
`Namespace "${def.namespace}" exposes no relations (synced tables are excluded from SDL in v2.0) \u2014 nothing to generate.`
|
|
@@ -7328,7 +8724,9 @@ function generateSdl(canonical, options = {}) {
|
|
|
7328
8724
|
blocks.push(
|
|
7329
8725
|
'"""Abort the entire mutation unless this root affects exactly the document-authored number of rows. Mutation roots only; cannot be conditional."""\ndirective @assertAffectedRows(exactly: Int!) on FIELD'
|
|
7330
8726
|
);
|
|
8727
|
+
if (tables.some((table) => table.resolved !== void 0)) blocks.push(...RESOLVED_DIRECTIVE_BLOCKS);
|
|
7331
8728
|
for (const scalar2 of CUSTOM_SCALARS) blocks.push(`scalar ${scalar2}`);
|
|
8729
|
+
blocks.push(...renderSharedCellTypes([...cellsByTable.values()]));
|
|
7332
8730
|
blocks.push(renderEnum(ORDER_BY_ENUM, [...ORDER_BY_ENUM_VALUES]));
|
|
7333
8731
|
blocks.push(renderEnum(DATE_BUCKET_ENUM, DATE_BUCKETS.map((bucket) => bucket.toUpperCase())));
|
|
7334
8732
|
for (const enumName of [...usedEnums].sort()) {
|
|
@@ -7353,7 +8751,8 @@ function generateSdl(canonical, options = {}) {
|
|
|
7353
8751
|
names,
|
|
7354
8752
|
namesByTable,
|
|
7355
8753
|
emitAggregateBoolExp: aggregateBoolExpTargets.has(table.table),
|
|
7356
|
-
pinned: options.pinnedColumns?.[table.table] ?? []
|
|
8754
|
+
pinned: options.pinnedColumns?.[table.table] ?? [],
|
|
8755
|
+
cells: cellsByTable.get(table.table) ?? null
|
|
7357
8756
|
})
|
|
7358
8757
|
);
|
|
7359
8758
|
}
|
|
@@ -7443,6 +8842,52 @@ ${orderFields}
|
|
|
7443
8842
|
}`
|
|
7444
8843
|
];
|
|
7445
8844
|
}
|
|
8845
|
+
var RESOLVED_DIRECTIVE_BLOCKS = [
|
|
8846
|
+
'"""Refresh this resolved-table root from a prefetch source before reading. Key-addressed roots only; the source must support the root shape."""\ndirective @prefetch(source: String!, maxStaleness: String) repeatable on FIELD',
|
|
8847
|
+
renderEnum("WritebackMode", ["SYNC", "ASYNC"]),
|
|
8848
|
+
'"""Write this resolved-table mutation back to a prefetch source (SYNC rolls back on failure; ASYNC queues)."""\ndirective @writeback(source: String!, mode: WritebackMode) repeatable on FIELD',
|
|
8849
|
+
'"""Declare that this mutation deliberately keeps writeback columns local to the named source."""\ndirective @localOnly(source: String!) repeatable on FIELD'
|
|
8850
|
+
];
|
|
8851
|
+
function trackedCells(table) {
|
|
8852
|
+
const columns = [...new Set(table.columns.flatMap((column) => column.cell ? [column.cell.of] : []))];
|
|
8853
|
+
if (columns.length === 0) return null;
|
|
8854
|
+
return { resolved: table.columns.some((column) => column.cell?.part === "source"), columns };
|
|
8855
|
+
}
|
|
8856
|
+
function renderSharedCellTypes(all) {
|
|
8857
|
+
const present = all.filter((cells) => cells !== null);
|
|
8858
|
+
const blocks = [];
|
|
8859
|
+
if (present.some((cells) => !cells.resolved)) {
|
|
8860
|
+
blocks.push(`"""When, and by whom, a cell was last written."""
|
|
8861
|
+
type ${CELL_TYPENAMES.stamp} {
|
|
8862
|
+
at: timestamptz!
|
|
8863
|
+
by: String
|
|
8864
|
+
}`);
|
|
8865
|
+
}
|
|
8866
|
+
if (present.some((cells) => cells.resolved)) {
|
|
8867
|
+
blocks.push(`"""A newer candidate value that lost to the column's resolve rule."""
|
|
8868
|
+
type ${CELL_TYPENAMES.conflict} {
|
|
8869
|
+
source: String!
|
|
8870
|
+
at: timestamptz
|
|
8871
|
+
value: jsonb
|
|
8872
|
+
}`);
|
|
8873
|
+
blocks.push(`"""How a resolved cell was decided: the winning source, its time and actor, and newer losers."""
|
|
8874
|
+
type ${CELL_TYPENAMES.resolved} {
|
|
8875
|
+
source: String!
|
|
8876
|
+
at: timestamptz
|
|
8877
|
+
by: String
|
|
8878
|
+
conflicts: [${CELL_TYPENAMES.conflict}!]!
|
|
8879
|
+
}`);
|
|
8880
|
+
}
|
|
8881
|
+
return blocks;
|
|
8882
|
+
}
|
|
8883
|
+
function renderCellsType(ctx) {
|
|
8884
|
+
if (!ctx.cells) return [];
|
|
8885
|
+
const cellType = ctx.cells.resolved ? CELL_TYPENAMES.resolved : CELL_TYPENAMES.stamp;
|
|
8886
|
+
const lines = ctx.cells.columns.map((column) => ` ${fieldName(column)}: ${cellType}`);
|
|
8887
|
+
return [`type ${cellsTypename(ctx.names.typename)} {
|
|
8888
|
+
${lines.join("\n")}
|
|
8889
|
+
}`];
|
|
8890
|
+
}
|
|
7446
8891
|
function relationArgs(target) {
|
|
7447
8892
|
return `where: ${target.boolExp}, orderBy: [${target.orderBy}!], limit: Int, offset: Int`;
|
|
7448
8893
|
}
|
|
@@ -7451,10 +8896,10 @@ function rootArgs(names) {
|
|
|
7451
8896
|
}
|
|
7452
8897
|
function primaryKeyColumns(table) {
|
|
7453
8898
|
if (!table.primaryKey || table.primaryKey.length === 0) return [];
|
|
7454
|
-
return table.primaryKey.map((
|
|
7455
|
-
const column = table.columns.find((c) => c.columnName ===
|
|
8899
|
+
return table.primaryKey.map((identifier3) => {
|
|
8900
|
+
const column = table.columns.find((c) => c.columnName === identifier3 || c.name === identifier3);
|
|
7456
8901
|
if (!column) {
|
|
7457
|
-
throw new SdlGenerationError(`Primary key column "${
|
|
8902
|
+
throw new SdlGenerationError(`Primary key column "${identifier3}" not found on table "${table.table}".`);
|
|
7458
8903
|
}
|
|
7459
8904
|
return column;
|
|
7460
8905
|
});
|
|
@@ -7463,6 +8908,7 @@ function renderTableBlocks(ctx) {
|
|
|
7463
8908
|
const { table, names } = ctx;
|
|
7464
8909
|
const blocks = [
|
|
7465
8910
|
renderObjectType(ctx),
|
|
8911
|
+
...renderCellsType(ctx),
|
|
7466
8912
|
renderSelectColumnEnum(ctx),
|
|
7467
8913
|
renderOrderByInput(ctx),
|
|
7468
8914
|
renderBoolExp(ctx),
|
|
@@ -7477,11 +8923,49 @@ function renderTableBlocks(ctx) {
|
|
|
7477
8923
|
${fields}
|
|
7478
8924
|
}`);
|
|
7479
8925
|
}
|
|
7480
|
-
if (
|
|
8926
|
+
if (ctx.table.resolved !== void 0) {
|
|
8927
|
+
blocks.push(...renderResolvedWriteBlocks(ctx));
|
|
8928
|
+
} else if (allowsInsert(table) || allowsUpdate(table)) {
|
|
7481
8929
|
blocks.push(...renderWriteBlocks(ctx, { insert: allowsInsert(table), update: allowsUpdate(table) }));
|
|
7482
8930
|
}
|
|
7483
8931
|
return blocks;
|
|
7484
8932
|
}
|
|
8933
|
+
function resolvedWritableColumns(table) {
|
|
8934
|
+
const system = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((column) => column.name));
|
|
8935
|
+
return table.columns.filter((column) => !system.has(column.name));
|
|
8936
|
+
}
|
|
8937
|
+
function renderResolvedWriteBlocks(ctx) {
|
|
8938
|
+
const { table, names } = ctx;
|
|
8939
|
+
const writable = resolvedWritableColumns(table);
|
|
8940
|
+
const pk = new Set(table.primaryKey ?? []);
|
|
8941
|
+
const settable = writable.filter((column) => !pk.has(column.columnName));
|
|
8942
|
+
const line = (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`;
|
|
8943
|
+
const blocks = [];
|
|
8944
|
+
if (allowsInsert(table)) blocks.push(`input ${names.insertInput} {
|
|
8945
|
+
${writable.map(line).join("\n")}
|
|
8946
|
+
}`);
|
|
8947
|
+
if (allowsUpdate(table)) {
|
|
8948
|
+
blocks.push(`input ${names.setInput} {
|
|
8949
|
+
${settable.map(line).join("\n")}
|
|
8950
|
+
}`);
|
|
8951
|
+
blocks.push(renderEnum(names.updateColumnEnum, settable.map((column) => fieldName(column.name))));
|
|
8952
|
+
}
|
|
8953
|
+
return blocks;
|
|
8954
|
+
}
|
|
8955
|
+
function renderResolvedMutationFields(table, names) {
|
|
8956
|
+
const lines = [];
|
|
8957
|
+
const pk = primaryKeyColumns(table);
|
|
8958
|
+
if (allowsInsert(table)) lines.push(` ${names.insertOneRoot}(object: ${names.insertInput}!): ${names.typename}`);
|
|
8959
|
+
if (allowsUpdate(table) && pk.length > 0) {
|
|
8960
|
+
lines.push(` ${names.updateByPkRoot}(pkColumns: ${names.pkColumnsInput}!, _set: ${names.setInput}!): ${names.typename}`);
|
|
8961
|
+
lines.push(` ${names.revertByPkRoot}(pkColumns: ${names.pkColumnsInput}!, columns: [${names.updateColumnEnum}!]!): ${names.typename}`);
|
|
8962
|
+
if ((table.resolved?.deletedBy.length ?? 0) > 0) {
|
|
8963
|
+
const args = pk.map((column) => `${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}!`).join(", ");
|
|
8964
|
+
lines.push(` ${names.deleteByPkRoot}(${args}): ${names.typename}`);
|
|
8965
|
+
}
|
|
8966
|
+
}
|
|
8967
|
+
return lines;
|
|
8968
|
+
}
|
|
7485
8969
|
function allowsInsert(table) {
|
|
7486
8970
|
return table.policies.write !== void 0 || table.policies.insert !== void 0;
|
|
7487
8971
|
}
|
|
@@ -7504,6 +8988,10 @@ function renderObjectType(ctx) {
|
|
|
7504
8988
|
const deprecated = column.deprecated ? " @deprecated" : "";
|
|
7505
8989
|
lines.push(` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}${bang}${deprecated}`);
|
|
7506
8990
|
}
|
|
8991
|
+
if (ctx.cells) {
|
|
8992
|
+
lines.push(' """Per-cell metadata: when each field was last written, and by whom."""');
|
|
8993
|
+
lines.push(` ${CELLS_FIELD}: ${cellsTypename(names.typename)}!`);
|
|
8994
|
+
}
|
|
7507
8995
|
for (const link of table.links) {
|
|
7508
8996
|
if (link.description !== void 0) lines.push(...descriptionLines(link.description, " "));
|
|
7509
8997
|
if (link.kind === "toPlatformUser") {
|
|
@@ -7572,7 +9060,10 @@ function numericishColumns(table) {
|
|
|
7572
9060
|
return table.columns.filter((column) => typeof column.type !== "object" && NUMERICISH.has(column.type));
|
|
7573
9061
|
}
|
|
7574
9062
|
function jsonbColumns(table) {
|
|
7575
|
-
return table.
|
|
9063
|
+
return clientWritableColumns(table).filter((column) => column.type === "jsonb");
|
|
9064
|
+
}
|
|
9065
|
+
function clientWritableColumns(table) {
|
|
9066
|
+
return table.columns;
|
|
7576
9067
|
}
|
|
7577
9068
|
function renderAggregateTypes(ctx) {
|
|
7578
9069
|
const { table, names } = ctx;
|
|
@@ -7656,14 +9147,14 @@ function constraintValues(table) {
|
|
|
7656
9147
|
return [...values].sort();
|
|
7657
9148
|
}
|
|
7658
9149
|
function clientSettableColumns(table) {
|
|
7659
|
-
return table.
|
|
9150
|
+
return clientWritableColumns(table).filter((column) => column.onUpdateFn === void 0);
|
|
7660
9151
|
}
|
|
7661
9152
|
function renderWriteBlocks(ctx, access) {
|
|
7662
9153
|
const { table, names, pinned } = ctx;
|
|
7663
9154
|
const blocks = [];
|
|
7664
9155
|
if (access.insert) {
|
|
7665
9156
|
const pinnedSet = new Set(pinned);
|
|
7666
|
-
const insertable = table.
|
|
9157
|
+
const insertable = clientWritableColumns(table).filter(
|
|
7667
9158
|
(column) => !pinnedSet.has(column.name) && !pinnedSet.has(column.columnName)
|
|
7668
9159
|
);
|
|
7669
9160
|
if (insertable.length === 0) {
|
|
@@ -7756,6 +9247,10 @@ function renderMutationRoot(tables, namesByTable) {
|
|
|
7756
9247
|
if (!canInsert && !canUpdate) continue;
|
|
7757
9248
|
const names = namesByTable.get(table.table);
|
|
7758
9249
|
if (!names) continue;
|
|
9250
|
+
if (table.resolved !== void 0) {
|
|
9251
|
+
lines.push(...renderResolvedMutationFields(table, names));
|
|
9252
|
+
continue;
|
|
9253
|
+
}
|
|
7759
9254
|
const constraints = constraintValues(table);
|
|
7760
9255
|
const onConflictArg = canInsert && allowsOnConflict(table) && constraints.length > 0 ? `, onConflict: ${names.onConflict}` : "";
|
|
7761
9256
|
const updateArgs = [`_set: ${names.setInput}`];
|
|
@@ -7844,13 +9339,65 @@ function renderOrmMigrationExport(exported) {
|
|
|
7844
9339
|
return text4;
|
|
7845
9340
|
}
|
|
7846
9341
|
|
|
9342
|
+
// src/compiler-gql/role-warnings.ts
|
|
9343
|
+
var ROLE_CHECK = /\brole\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
9344
|
+
function isRecord(value) {
|
|
9345
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9346
|
+
}
|
|
9347
|
+
function roleKeysIn(value) {
|
|
9348
|
+
const keys = /* @__PURE__ */ new Set();
|
|
9349
|
+
for (const match of JSON.stringify(value ?? null).matchAll(ROLE_CHECK)) {
|
|
9350
|
+
if (match[1]) keys.add(match[1]);
|
|
9351
|
+
}
|
|
9352
|
+
return [...keys].sort();
|
|
9353
|
+
}
|
|
9354
|
+
function roleWarnings({
|
|
9355
|
+
canonical,
|
|
9356
|
+
manifest
|
|
9357
|
+
}) {
|
|
9358
|
+
const warnings = [];
|
|
9359
|
+
const roles = canonical.capabilities.roles;
|
|
9360
|
+
const defined = isRecord(roles) && isRecord(roles.definitions) ? Object.keys(roles.definitions).sort() : [];
|
|
9361
|
+
if (defined.length > 0) {
|
|
9362
|
+
warnings.push({
|
|
9363
|
+
severity: "warning",
|
|
9364
|
+
code: "deprecated-role-definitions",
|
|
9365
|
+
message: `The namespace defines roles (${defined.join(", ")}). Role definitions here are a deprecated one-time seed: apply creates any that don't exist yet and never updates them after. Create and edit roles in Atlas; keep declaring the capabilities your policies check.`
|
|
9366
|
+
});
|
|
9367
|
+
}
|
|
9368
|
+
const relations = [
|
|
9369
|
+
...canonical.tables,
|
|
9370
|
+
...(canonical.views ?? []).map((view) => ({ table: view.view, policies: view.policies, resolved: view.resolved }))
|
|
9371
|
+
];
|
|
9372
|
+
for (const relation of relations) {
|
|
9373
|
+
const keys = roleKeysIn([relation.policies, relation.resolved]);
|
|
9374
|
+
if (keys.length === 0) continue;
|
|
9375
|
+
warnings.push({
|
|
9376
|
+
severity: "warning",
|
|
9377
|
+
code: "role-check",
|
|
9378
|
+
table: relation.table,
|
|
9379
|
+
message: `Policy checks ${keys.map((key) => `role('${key}')`).join(", ")}. Roles are defined in Atlas and can be renamed or removed there; check the capability the role grants with capability('\u2026') instead.`
|
|
9380
|
+
});
|
|
9381
|
+
}
|
|
9382
|
+
for (const [operation, entry] of Object.entries(manifest.operations).sort(([a], [b]) => a.localeCompare(b))) {
|
|
9383
|
+
if (!entry.requires?.role) continue;
|
|
9384
|
+
warnings.push({
|
|
9385
|
+
severity: "warning",
|
|
9386
|
+
code: "role-requires",
|
|
9387
|
+
operation,
|
|
9388
|
+
message: `@requires(role: "${entry.requires.role}") gates on a role Atlas can rename or remove; use @requires(capability: "\u2026") with the capability the role grants.`
|
|
9389
|
+
});
|
|
9390
|
+
}
|
|
9391
|
+
return warnings;
|
|
9392
|
+
}
|
|
9393
|
+
|
|
7847
9394
|
// src/cli.ts
|
|
7848
9395
|
var BASE_URL_ENV = "SEQUENCE_ORM_BASE_URL";
|
|
7849
9396
|
var TOKEN_ENV = "SEQUENCE_ORM_TOKEN";
|
|
7850
9397
|
var ORM_MODULE_PATH_ENV = "SEQUENCE_ORM_MODULE_PATH";
|
|
7851
9398
|
var USAGE = `usage:
|
|
7852
9399
|
seq-studio orm init <dir> scaffold a namespace package (v2: TypeScript schema + GraphQL documents)
|
|
7853
|
-
seq-studio orm generate [dir] write schema.graphql + operations.manifest.json + typePolicies.gen.ts (+ consumer graphql-codegen when codegen.ts is present)
|
|
9400
|
+
seq-studio orm generate [dir] write schema.graphql + operations.manifest.json + typePolicies.gen.ts (+ sources.gen.ts for resolved-table sources, + consumer graphql-codegen when codegen.ts is present)
|
|
7854
9401
|
seq-studio orm generate [dir] --migration-export [--check]
|
|
7855
9402
|
export reviewed ORM inputs for a coordinated pipeline migration; --check verifies without writing
|
|
7856
9403
|
seq-studio orm validate [dir] parse + validate definitions and verify committed migrations are in sync
|
|
@@ -8021,6 +9568,7 @@ async function walkFiles(root, prefix = "") {
|
|
|
8021
9568
|
var SCHEMA_GRAPHQL_FILE = "schema.graphql";
|
|
8022
9569
|
var OPERATIONS_MANIFEST_FILE = "operations.manifest.json";
|
|
8023
9570
|
var TYPE_POLICIES_FILE = "typePolicies.gen.ts";
|
|
9571
|
+
var SOURCE_CONTRACTS_FILE = "sources.gen.ts";
|
|
8024
9572
|
var GRAPHQL_DIRNAME = "graphql";
|
|
8025
9573
|
var CODEGEN_CONFIG_FILE = "codegen.ts";
|
|
8026
9574
|
function ormLoadOptions() {
|
|
@@ -8040,9 +9588,10 @@ async function compileV2Project(dir) {
|
|
|
8040
9588
|
const documents = await collectGraphqlDocuments(dir);
|
|
8041
9589
|
const result = compileNamespace({ canonical: loaded.canonical, sdl, documents });
|
|
8042
9590
|
if (!result.ok) return { ok: false, diagnostics: result.diagnostics };
|
|
9591
|
+
const diagnostics = [...result.diagnostics, ...roleWarnings({ canonical: loaded.canonical, manifest: result.manifest })];
|
|
8043
9592
|
return {
|
|
8044
9593
|
ok: true,
|
|
8045
|
-
project: { loaded, sdl, documents, manifest: result.manifest, diagnostics
|
|
9594
|
+
project: { loaded, sdl, documents, manifest: result.manifest, diagnostics }
|
|
8046
9595
|
};
|
|
8047
9596
|
}
|
|
8048
9597
|
var GQL_TEMPLATE_RE = /(?:\bgql|\/\*\s*GraphQL\s*\*\/)\s*`([^`]*)`/g;
|
|
@@ -8146,6 +9695,8 @@ async function writeGenerateOutputs(dir, project) {
|
|
|
8146
9695
|
await promises.writeFile(path.join(dir, OPERATIONS_MANIFEST_FILE), `${JSON.stringify(manifestFile, null, 2)}
|
|
8147
9696
|
`);
|
|
8148
9697
|
await promises.writeFile(path.join(dir, TYPE_POLICIES_FILE), generateTypePolicies(loaded.canonical));
|
|
9698
|
+
const sourceContracts = generateSourceContracts(loaded.canonical);
|
|
9699
|
+
if (sourceContracts !== null) await promises.writeFile(path.join(dir, SOURCE_CONTRACTS_FILE), sourceContracts);
|
|
8149
9700
|
}
|
|
8150
9701
|
async function runConsumerCodegen(dir) {
|
|
8151
9702
|
const codegenPath = path.join(dir, CODEGEN_CONFIG_FILE);
|
|
@@ -8413,16 +9964,28 @@ async function checkWorkspaceV2({
|
|
|
8413
9964
|
return 1;
|
|
8414
9965
|
}
|
|
8415
9966
|
const previousSnapshot = tip.ordinal > 1 ? await readSnapshot(dir, tip.ordinal - 1) : void 0;
|
|
8416
|
-
const plan = await planMigrationV2({ canonical: loaded.canonical, previousSnapshot });
|
|
8417
|
-
const regenerated = renderMigrationSql({ name: tip.name, namespace, contentHash: loaded.hash, plan });
|
|
8418
9967
|
const committed = await readMigrationSql(dir, tip);
|
|
8419
|
-
|
|
9968
|
+
const { reproduced } = await planCommittedTipV2({ loaded, tip, previousSnapshot, committed });
|
|
9969
|
+
if (!reproduced) {
|
|
8420
9970
|
log(`${MIGRATIONS_DIRNAME}/${tip.name}.sql does not regenerate from the definitions \u2014 re-run \`seq-studio orm diff\``);
|
|
8421
9971
|
return 1;
|
|
8422
9972
|
}
|
|
8423
9973
|
log(`migrations in sync \u2014 ${workspace.journal.entries.length} committed, tip ${tip.name}`);
|
|
8424
9974
|
return 0;
|
|
8425
9975
|
}
|
|
9976
|
+
function planCommittedTipV2({
|
|
9977
|
+
loaded,
|
|
9978
|
+
tip,
|
|
9979
|
+
previousSnapshot,
|
|
9980
|
+
committed
|
|
9981
|
+
}) {
|
|
9982
|
+
const namespace = loaded.canonical.namespace;
|
|
9983
|
+
return planCommittedMigrationV2({
|
|
9984
|
+
canonical: loaded.canonical,
|
|
9985
|
+
previousSnapshot,
|
|
9986
|
+
reproduces: (plan) => renderMigrationSql({ name: tip.name, namespace, contentHash: loaded.hash, plan }) === committed || matchesRebaseTip({ committed, tip, namespace, plan })
|
|
9987
|
+
});
|
|
9988
|
+
}
|
|
8426
9989
|
function matchesRebaseTip({
|
|
8427
9990
|
committed,
|
|
8428
9991
|
tip,
|
|
@@ -8525,7 +10088,6 @@ async function loadCommittedTipMigrationV2({
|
|
|
8525
10088
|
loaded,
|
|
8526
10089
|
workspace
|
|
8527
10090
|
}) {
|
|
8528
|
-
const namespace = loaded.canonical.namespace;
|
|
8529
10091
|
const tip = workspace.journal.entries.at(-1);
|
|
8530
10092
|
if (!tip) return void 0;
|
|
8531
10093
|
if (tip.content_hash !== loaded.hash) {
|
|
@@ -8535,10 +10097,9 @@ async function loadCommittedTipMigrationV2({
|
|
|
8535
10097
|
);
|
|
8536
10098
|
}
|
|
8537
10099
|
const previousSnapshot = tip.ordinal > 1 ? await readSnapshot(dir, tip.ordinal - 1) : void 0;
|
|
8538
|
-
const plan = await planMigrationV2({ canonical: loaded.canonical, previousSnapshot });
|
|
8539
10100
|
const committed = await readMigrationSql(dir, tip);
|
|
8540
|
-
const
|
|
8541
|
-
if (
|
|
10101
|
+
const { plan, reproduced } = await planCommittedTipV2({ loaded, tip, previousSnapshot, committed });
|
|
10102
|
+
if (!reproduced) {
|
|
8542
10103
|
throw new OrmDefinitionError(
|
|
8543
10104
|
`${tip.name}.sql does not regenerate from the definitions \u2014 run \`seq-studio orm diff --check\``,
|
|
8544
10105
|
MIGRATIONS_DIRNAME
|