@sequenceholdings/orm 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -2
- package/dist/authoring.cjs +1474 -54
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +242 -4
- package/dist/authoring.d.ts +242 -4
- package/dist/authoring.js +1465 -56
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +740 -42
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +742 -53
- package/dist/canonical.d.ts +742 -53
- package/dist/canonical.js +724 -43
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +2478 -328
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2480 -330
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs +90 -25
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +39 -22
- package/dist/client.d.ts +39 -22
- package/dist/client.js +87 -26
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +1755 -329
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +56 -3
- package/dist/compiler.d.ts +56 -3
- package/dist/compiler.js +1756 -330
- package/dist/compiler.js.map +1 -1
- package/dist/constants-YoFGjIBM.d.cts +99 -0
- package/dist/constants-YoFGjIBM.d.ts +99 -0
- package/dist/dynamic-input.d.cts +1 -0
- package/dist/dynamic-input.d.ts +1 -0
- package/dist/index.cjs +1973 -164
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +474 -100
- package/dist/index.d.ts +474 -100
- package/dist/index.js +1940 -167
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +869 -46
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +699 -25
- package/dist/ir.d.ts +699 -25
- package/dist/ir.js +861 -47
- package/dist/ir.js.map +1 -1
- package/dist/{naming-CTvWSRTf.d.ts → naming-CPfx7EfF.d.ts} +28 -1
- package/dist/{naming-DRyinU4u.d.cts → naming-CU9ThP_E.d.cts} +28 -1
- package/dist/policy-1iZbGnZZ.d.cts +24 -0
- package/dist/policy-1iZbGnZZ.d.ts +24 -0
- package/dist/predicate-v2.cjs +852 -49
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.d.cts +2 -1
- package/dist/predicate-v2.d.ts +2 -1
- package/dist/predicate-v2.js +852 -49
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +1309 -58
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.d.cts +7 -3
- package/dist/sdl.d.ts +7 -3
- package/dist/sdl.js +1305 -59
- package/dist/sdl.js.map +1 -1
- package/package.json +11 -6
- package/templates/orm-namespace/CLAUDE.md +6 -4
- package/templates/orm-namespace/codegen.preset.d.cts +7 -0
- package/templates/orm-namespace/codegen.ts +2 -1
- package/templates/orm-namespace/graphql/example/queries.ts +4 -3
- package/templates/orm-namespace/package.json +2 -1
- package/templates/orm-namespace/schema/tables.ts +3 -2
- package/templates/orm-namespace/sequence.config.ts +7 -11
package/dist/cli.cjs
CHANGED
|
@@ -39,9 +39,519 @@ var PredicateParseError = class extends Error {
|
|
|
39
39
|
}
|
|
40
40
|
name = "PredicateParseError";
|
|
41
41
|
};
|
|
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
|
+
|
|
248
|
+
// src/constants.ts
|
|
249
|
+
var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
|
250
|
+
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
251
|
+
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
252
|
+
var MAX_IDENTIFIER_LENGTH = 63;
|
|
253
|
+
var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
|
|
254
|
+
var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
255
|
+
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
|
|
256
|
+
var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
|
|
257
|
+
var ORM_SCHEMA_PREFIX = "orm_";
|
|
258
|
+
var CONSUMER_ROLE_SUFFIX = "_consumer";
|
|
259
|
+
var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
|
|
260
|
+
var consumerRoleName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}${CONSUMER_ROLE_SUFFIX}`;
|
|
261
|
+
var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
|
|
262
|
+
var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
|
|
263
|
+
var HASH_DOMAIN = "sequence-orm-definitions\0";
|
|
264
|
+
var MAX_DEFINITION_FILE_BYTES = 1024 * 1024;
|
|
265
|
+
var MAX_GENERATED_SDL_BYTES = 16 * 1024 * 1024;
|
|
266
|
+
var ORM_REGISTRATION_MAX_BYTES = 64 * 1024 * 1024;
|
|
267
|
+
var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
|
|
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
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/canonical/v2.ts
|
|
42
551
|
var CANONICAL_FORMAT = 2;
|
|
43
552
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
44
|
-
var
|
|
553
|
+
var identifier2 = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
554
|
+
var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
|
|
45
555
|
var COLUMN_TYPES = [
|
|
46
556
|
"uuid",
|
|
47
557
|
"text",
|
|
@@ -56,59 +566,71 @@ var COLUMN_TYPES = [
|
|
|
56
566
|
];
|
|
57
567
|
var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
|
|
58
568
|
var canonicalEnumSchema = zod.z.object({
|
|
59
|
-
name:
|
|
569
|
+
name: identifier2,
|
|
60
570
|
values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
|
|
61
571
|
});
|
|
62
572
|
var canonicalColumnSchema = zod.z.object({
|
|
63
573
|
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
64
574
|
name: zod.z.string().min(1).max(63),
|
|
65
|
-
columnName:
|
|
66
|
-
type: zod.z.union([columnTypeSchema, zod.z.object({ enum:
|
|
575
|
+
columnName: columnIdentifier,
|
|
576
|
+
type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier2 })]),
|
|
67
577
|
nullable: zod.z.boolean().default(true),
|
|
68
578
|
unique: zod.z.boolean().default(false),
|
|
69
579
|
/** Literal default, or one of the allowlisted SQL functions. */
|
|
70
580
|
default: zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()]).optional(),
|
|
71
581
|
defaultFn: zod.z.enum(["gen_random_uuid()", "now()"]).optional(),
|
|
582
|
+
/**
|
|
583
|
+
* Stamp this column from the database clock on UPDATE (`now()`). The
|
|
584
|
+
* column stays insertable (defaults fill gaps) but is omitted from
|
|
585
|
+
* SetInput / settable so callers cannot backdate it.
|
|
586
|
+
*/
|
|
587
|
+
onUpdateFn: zod.z.enum(["now()"]).optional(),
|
|
72
588
|
description: zod.z.string().max(500).optional(),
|
|
73
589
|
/** Migration hints — same semantics as v1. */
|
|
74
|
-
renamedFrom:
|
|
590
|
+
renamedFrom: columnIdentifier.optional(),
|
|
75
591
|
cast: zod.z.string().max(200).optional(),
|
|
76
|
-
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()
|
|
600
|
+
}).superRefine((column, ctx) => {
|
|
601
|
+
if (column.onUpdateFn === void 0) return;
|
|
602
|
+
if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
|
|
603
|
+
ctx.addIssue({
|
|
604
|
+
code: "custom",
|
|
605
|
+
path: ["onUpdateFn"],
|
|
606
|
+
message: `onUpdateFn '${column.onUpdateFn}' is only valid on timestamptz/date columns`
|
|
607
|
+
});
|
|
608
|
+
}
|
|
77
609
|
});
|
|
78
610
|
var canonicalLinkSchema = zod.z.object({
|
|
79
611
|
name: zod.z.string().min(1).max(63),
|
|
80
612
|
kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
|
|
81
613
|
/** Target table name (namespace-local). Absent for toPlatformUser. */
|
|
82
|
-
target:
|
|
614
|
+
target: identifier2.optional(),
|
|
83
615
|
/** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
|
|
84
|
-
on:
|
|
616
|
+
on: columnIdentifier,
|
|
85
617
|
/** Referenced column on the target (default: target's PK / platform user `sub`). */
|
|
86
|
-
references:
|
|
618
|
+
references: columnIdentifier.optional(),
|
|
87
619
|
/** enforce: true emits a real FK constraint (same-namespace toOne only). */
|
|
88
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(),
|
|
89
623
|
description: zod.z.string().max(500).optional()
|
|
90
624
|
});
|
|
91
625
|
var canonicalIndexSchema = zod.z.object({
|
|
92
|
-
columns: zod.z.array(
|
|
626
|
+
columns: zod.z.array(columnIdentifier).min(1),
|
|
93
627
|
unique: zod.z.boolean().default(false),
|
|
94
|
-
name:
|
|
628
|
+
name: identifier2.optional()
|
|
95
629
|
});
|
|
96
|
-
var
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
allow: zod.z.literal(true).optional(),
|
|
101
|
-
policy: zod.z.string().min(1).max(2e3).optional()
|
|
102
|
-
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
103
|
-
message: "A variant declares exactly one of allow: true or policy"
|
|
104
|
-
});
|
|
105
|
-
var policyExpressionSchema = zod.z.union([
|
|
106
|
-
zod.z.string().min(1).max(2e3),
|
|
107
|
-
zod.z.object({
|
|
108
|
-
default: zod.z.string().min(1).max(2e3),
|
|
109
|
-
variants: zod.z.array(policyVariantSchema).max(10).default([])
|
|
110
|
-
})
|
|
111
|
-
]);
|
|
630
|
+
var canonicalCheckSchema = zod.z.object({
|
|
631
|
+
name: identifier2,
|
|
632
|
+
expression: zod.z.string().min(1).max(2e3)
|
|
633
|
+
}).strict();
|
|
112
634
|
var canonicalPoliciesSchema = zod.z.object({
|
|
113
635
|
read: policyExpressionSchema.optional(),
|
|
114
636
|
write: policyExpressionSchema.optional(),
|
|
@@ -296,7 +818,7 @@ var VIEW_QUERY_FUNCTION_ALLOWLIST = [
|
|
|
296
818
|
"width_bucket"
|
|
297
819
|
];
|
|
298
820
|
var canonicalActionSchema = zod.z.object({
|
|
299
|
-
name:
|
|
821
|
+
name: identifier2,
|
|
300
822
|
requires: zod.z.string().max(1e3).optional(),
|
|
301
823
|
params: zod.z.record(zod.z.string(), zod.z.object({
|
|
302
824
|
type: columnTypeSchema,
|
|
@@ -308,9 +830,9 @@ var canonicalActionSchema = zod.z.object({
|
|
|
308
830
|
});
|
|
309
831
|
var canonicalCowMutationSchema = zod.z.object({
|
|
310
832
|
kind: zod.z.enum(["override", "revert"]),
|
|
311
|
-
name:
|
|
833
|
+
name: identifier2,
|
|
312
834
|
/** Synced table (same namespace) the overlay copies from / falls back to. */
|
|
313
|
-
synced:
|
|
835
|
+
synced: identifier2,
|
|
314
836
|
/** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
|
|
315
837
|
pk: zod.z.string().min(1).max(63),
|
|
316
838
|
/** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
|
|
@@ -337,17 +859,28 @@ var canonicalCowMutationSchema = zod.z.object({
|
|
|
337
859
|
}
|
|
338
860
|
}
|
|
339
861
|
});
|
|
862
|
+
var canonicalForeignKeySchema = zod.z.object({
|
|
863
|
+
name: identifier2,
|
|
864
|
+
columns: zod.z.array(columnIdentifier).min(1).max(8),
|
|
865
|
+
target: identifier2,
|
|
866
|
+
references: zod.z.array(columnIdentifier).min(1).max(8),
|
|
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()
|
|
870
|
+
}).strict();
|
|
340
871
|
var canonicalTableSchema = zod.z.object({
|
|
341
|
-
table:
|
|
872
|
+
table: identifier2,
|
|
342
873
|
description: zod.z.string().max(2e3).optional(),
|
|
343
874
|
/** Table-level rename hint (new in v2). */
|
|
344
|
-
renamedFrom:
|
|
875
|
+
renamedFrom: identifier2.optional(),
|
|
345
876
|
/** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
|
|
346
877
|
source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
|
|
347
878
|
columns: zod.z.array(canonicalColumnSchema).default([]),
|
|
348
|
-
primaryKey: zod.z.array(
|
|
879
|
+
primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
|
|
349
880
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
350
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(),
|
|
351
884
|
policies: canonicalPoliciesSchema.default({}),
|
|
352
885
|
/**
|
|
353
886
|
* v1-conversion carry-over only. v2 has no action surface — the runtime
|
|
@@ -356,26 +889,64 @@ var canonicalTableSchema = zod.z.object({
|
|
|
356
889
|
*/
|
|
357
890
|
actions: zod.z.array(canonicalActionSchema).default([]),
|
|
358
891
|
/** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
|
|
359
|
-
cowMutations: zod.z.array(canonicalCowMutationSchema).optional()
|
|
892
|
+
cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
|
|
893
|
+
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
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()
|
|
360
915
|
});
|
|
916
|
+
var MAX_VIEW_QUERY = 1e5;
|
|
917
|
+
var MAX_RESOLVED_VIEW_QUERY = 5e6;
|
|
361
918
|
var canonicalViewSchema = zod.z.object({
|
|
362
|
-
view:
|
|
919
|
+
view: identifier2,
|
|
363
920
|
description: zod.z.string().max(2e3).optional(),
|
|
364
921
|
columns: zod.z.array(canonicalColumnSchema).min(1),
|
|
365
922
|
/** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
|
|
366
|
-
primaryKey: zod.z.array(
|
|
923
|
+
primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
|
|
367
924
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
368
925
|
policies: zod.z.object({ read: policyExpressionSchema }).strict(),
|
|
369
|
-
query: zod.z.string().min(1).max(
|
|
926
|
+
query: zod.z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
|
|
370
927
|
message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
|
|
371
928
|
}),
|
|
372
929
|
/** Namespace-local logical relation names, sorted for deterministic hashing. */
|
|
373
|
-
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()
|
|
374
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
|
+
}
|
|
375
943
|
for (const [index3, column] of view.columns.entries()) {
|
|
376
944
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
377
945
|
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare a default` });
|
|
378
946
|
}
|
|
947
|
+
if (column.onUpdateFn !== void 0) {
|
|
948
|
+
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare onUpdateFn` });
|
|
949
|
+
}
|
|
379
950
|
if (column.unique) {
|
|
380
951
|
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare unique` });
|
|
381
952
|
}
|
|
@@ -383,15 +954,82 @@ var canonicalViewSchema = zod.z.object({
|
|
|
383
954
|
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare migration hints` });
|
|
384
955
|
}
|
|
385
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
|
+
}
|
|
386
962
|
for (const [index3, link] of view.links.entries()) {
|
|
387
963
|
if (link.enforce) {
|
|
388
964
|
ctx.addIssue({ code: "custom", path: ["links", index3, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
|
|
389
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
|
+
}
|
|
390
969
|
}
|
|
391
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
|
+
}
|
|
392
1030
|
var canonicalDefinitionSchema = zod.z.object({
|
|
393
1031
|
definition_format: zod.z.literal(CANONICAL_FORMAT),
|
|
394
|
-
namespace:
|
|
1032
|
+
namespace: identifier2.max(41),
|
|
395
1033
|
description: zod.z.string().max(2e3).optional(),
|
|
396
1034
|
enums: zod.z.array(canonicalEnumSchema).default([]),
|
|
397
1035
|
tables: zod.z.array(canonicalTableSchema).min(0).max(500),
|
|
@@ -409,6 +1047,74 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
409
1047
|
ctx.addIssue({ code: "custom", path: ["tables", index3, "table"], message: `duplicate relation '${table.table}'` });
|
|
410
1048
|
}
|
|
411
1049
|
seen.add(table.table);
|
|
1050
|
+
validateCellMetadata(table, index3, ctx);
|
|
1051
|
+
if (table.source !== void 0) {
|
|
1052
|
+
for (const [columnIndex, column] of table.columns.entries()) {
|
|
1053
|
+
if (column.onUpdateFn !== void 0) {
|
|
1054
|
+
ctx.addIssue({
|
|
1055
|
+
code: "custom",
|
|
1056
|
+
path: ["tables", index3, "columns", columnIndex],
|
|
1057
|
+
message: `synced table '${table.table}' column '${column.name}' cannot declare onUpdateFn`
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
1064
|
+
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
1065
|
+
const names = /* @__PURE__ */ new Set([
|
|
1066
|
+
...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
|
|
1067
|
+
...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
|
|
1068
|
+
...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
|
|
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
|
+
}
|
|
1090
|
+
for (const [index3, fk] of (table.foreignKeys ?? []).entries()) {
|
|
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}'`);
|
|
1093
|
+
names.add(fk.name);
|
|
1094
|
+
const target = fkTables.get(fk.target);
|
|
1095
|
+
if (table.source || !target || target.source) {
|
|
1096
|
+
fail3("foreign keys must connect owned tables in the same namespace");
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
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");
|
|
1101
|
+
for (const [position, name] of fk.columns.entries()) {
|
|
1102
|
+
const sourceColumn = table.columns.find((column) => column.columnName === name);
|
|
1103
|
+
const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
|
|
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");
|
|
1109
|
+
}
|
|
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])];
|
|
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 });
|
|
1117
|
+
}
|
|
412
1118
|
}
|
|
413
1119
|
const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
414
1120
|
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
@@ -504,7 +1210,7 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
504
1210
|
const visiting = /* @__PURE__ */ new Set();
|
|
505
1211
|
const visited = /* @__PURE__ */ new Set();
|
|
506
1212
|
let cycleReported = false;
|
|
507
|
-
const
|
|
1213
|
+
const visit2 = (name, path) => {
|
|
508
1214
|
if (visited.has(name) || cycleReported) return;
|
|
509
1215
|
if (visiting.has(name)) {
|
|
510
1216
|
cycleReported = true;
|
|
@@ -518,12 +1224,12 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
518
1224
|
visiting.add(name);
|
|
519
1225
|
const view = viewByName.get(name);
|
|
520
1226
|
for (const dependency of view?.dependencies ?? []) {
|
|
521
|
-
if (viewByName.has(dependency))
|
|
1227
|
+
if (viewByName.has(dependency)) visit2(dependency, [...path, name]);
|
|
522
1228
|
}
|
|
523
1229
|
visiting.delete(name);
|
|
524
1230
|
visited.add(name);
|
|
525
1231
|
};
|
|
526
|
-
for (const view of views)
|
|
1232
|
+
for (const view of views) visit2(view.view, []);
|
|
527
1233
|
});
|
|
528
1234
|
function canonicalRelations(definition) {
|
|
529
1235
|
const views = (definition.views ?? []).map((view) => ({
|
|
@@ -533,8 +1239,12 @@ function canonicalRelations(definition) {
|
|
|
533
1239
|
...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
|
|
534
1240
|
links: view.links,
|
|
535
1241
|
indexes: [],
|
|
536
|
-
|
|
537
|
-
|
|
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 } : {}
|
|
538
1248
|
}));
|
|
539
1249
|
return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
|
|
540
1250
|
}
|
|
@@ -544,12 +1254,14 @@ function normalizeCanonical(definition) {
|
|
|
544
1254
|
...definition,
|
|
545
1255
|
enums: sortByName(definition.enums),
|
|
546
1256
|
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
|
|
547
|
-
const { cowMutations, ...rest } = t2;
|
|
1257
|
+
const { cowMutations, foreignKeys, checks, ...rest } = t2;
|
|
548
1258
|
return {
|
|
549
1259
|
...rest,
|
|
550
1260
|
links: sortByName(t2.links),
|
|
551
1261
|
indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
|
|
552
1262
|
actions: sortByName(t2.actions),
|
|
1263
|
+
...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
|
|
1264
|
+
...checks?.length ? { checks: sortByName(checks) } : {},
|
|
553
1265
|
// Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
|
|
554
1266
|
...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
|
|
555
1267
|
};
|
|
@@ -564,25 +1276,6 @@ function normalizeCanonical(definition) {
|
|
|
564
1276
|
}
|
|
565
1277
|
return normalized;
|
|
566
1278
|
}
|
|
567
|
-
|
|
568
|
-
// src/constants.ts
|
|
569
|
-
var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
|
570
|
-
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
571
|
-
var MAX_IDENTIFIER_LENGTH = 63;
|
|
572
|
-
var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
|
|
573
|
-
var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
574
|
-
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
|
|
575
|
-
var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
|
|
576
|
-
var ORM_SCHEMA_PREFIX = "orm_";
|
|
577
|
-
var CONSUMER_ROLE_SUFFIX = "_consumer";
|
|
578
|
-
var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
|
|
579
|
-
var consumerRoleName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}${CONSUMER_ROLE_SUFFIX}`;
|
|
580
|
-
var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
|
|
581
|
-
var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
|
|
582
|
-
var HASH_DOMAIN = "sequence-orm-definitions\0";
|
|
583
|
-
var MAX_DEFINITION_FILE_BYTES = 1024 * 1024;
|
|
584
|
-
var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
|
|
585
|
-
var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
|
|
586
1279
|
function stableJson(value) {
|
|
587
1280
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
588
1281
|
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
@@ -855,8 +1548,19 @@ function isSeqNamespaceConfig(value) {
|
|
|
855
1548
|
return typeof value === "object" && value !== null && value.kind === NAMESPACE_CONFIG_BRAND;
|
|
856
1549
|
}
|
|
857
1550
|
function serializeNamespace(input) {
|
|
858
|
-
const { config
|
|
859
|
-
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
|
+
}
|
|
860
1564
|
const enums = input.enums ?? [];
|
|
861
1565
|
const tablesByName = /* @__PURE__ */ new Map();
|
|
862
1566
|
for (const table of tables) {
|
|
@@ -958,6 +1662,9 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
958
1662
|
if (table.config.renamedFrom !== void 0) candidate.renamedFrom = table.config.renamedFrom;
|
|
959
1663
|
if (table.source !== void 0) candidate.source = table.source;
|
|
960
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
|
+
}
|
|
961
1668
|
if (table.config.indexes !== void 0 && table.config.indexes.length > 0) {
|
|
962
1669
|
throw new OrmDefinitionError(
|
|
963
1670
|
`synced table "${table.tableName}" is system-owned; indexes are not allowed`,
|
|
@@ -995,6 +1702,11 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
995
1702
|
if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
|
|
996
1703
|
candidate.columns?.push(column);
|
|
997
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
|
+
}
|
|
998
1710
|
if (table.config.primaryKey !== void 0) {
|
|
999
1711
|
if (columnLevelPk.length > 0) {
|
|
1000
1712
|
throw new OrmDefinitionError(
|
|
@@ -1008,6 +1720,45 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1008
1720
|
} else if (columnLevelPk.length > 0) {
|
|
1009
1721
|
candidate.primaryKey = columnLevelPk;
|
|
1010
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
|
+
}
|
|
1741
|
+
if (table.config.foreignKeys?.length) {
|
|
1742
|
+
if (table.source) throw new OrmDefinitionError(`synced table "${table.tableName}" cannot enforce foreign keys`, table.file);
|
|
1743
|
+
candidate.foreignKeys = table.config.foreignKeys.map((fk) => {
|
|
1744
|
+
const target = resolveRelationRef({ ref: fk.target, fail: (message) => {
|
|
1745
|
+
throw new OrmDefinitionError(message, table.file);
|
|
1746
|
+
} });
|
|
1747
|
+
if (!isSeqTable(target) || !relationsByName.has(target.tableName)) throw new OrmDefinitionError("foreign key target must be a table in this namespace", table.file);
|
|
1748
|
+
return {
|
|
1749
|
+
name: fk.name,
|
|
1750
|
+
columns: fk.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: `foreign key "${fk.name}" column` })),
|
|
1751
|
+
target: target.tableName,
|
|
1752
|
+
references: fk.references.map((ref) => resolveColumnRef({ relation: target, ref, role: `foreign key "${fk.name}" reference` })),
|
|
1753
|
+
...fk.deferrable ? { deferrable: fk.deferrable } : {},
|
|
1754
|
+
...fk.onDelete ? { onDelete: fk.onDelete } : {},
|
|
1755
|
+
...fk.onUpdate ? { onUpdate: fk.onUpdate } : {}
|
|
1756
|
+
};
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
if (table.config.checks?.length) {
|
|
1760
|
+
candidate.checks = table.config.checks.map((check) => ({ name: check.name, expression: check.expression }));
|
|
1761
|
+
}
|
|
1011
1762
|
for (const index3 of table.config.indexes ?? []) {
|
|
1012
1763
|
const entry = {
|
|
1013
1764
|
columns: index3.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
|
|
@@ -1060,6 +1811,12 @@ function buildCanonicalView({
|
|
|
1060
1811
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1061
1812
|
throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare a default`, view.file);
|
|
1062
1813
|
}
|
|
1814
|
+
if (column.onUpdateFn !== void 0) {
|
|
1815
|
+
throw new OrmDefinitionError(
|
|
1816
|
+
`view "${view.viewName}": column "${propertyName}" cannot declare onUpdateFn`,
|
|
1817
|
+
view.file
|
|
1818
|
+
);
|
|
1819
|
+
}
|
|
1063
1820
|
if (column.unique) {
|
|
1064
1821
|
throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare unique`, view.file);
|
|
1065
1822
|
}
|
|
@@ -1097,7 +1854,33 @@ function buildCanonicalView({
|
|
|
1097
1854
|
columns,
|
|
1098
1855
|
...primaryKey2 !== void 0 ? { primaryKey: primaryKey2 } : {},
|
|
1099
1856
|
links,
|
|
1100
|
-
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
|
|
1101
1884
|
};
|
|
1102
1885
|
}
|
|
1103
1886
|
function buildCanonicalLink({
|
|
@@ -1108,14 +1891,17 @@ function buildCanonicalLink({
|
|
|
1108
1891
|
}) {
|
|
1109
1892
|
const ownerKind = relationKind(relation);
|
|
1110
1893
|
const ownerName = relationName(relation);
|
|
1111
|
-
const
|
|
1894
|
+
const fail3 = (message) => {
|
|
1112
1895
|
throw new OrmDefinitionError(`${ownerKind} "${ownerName}": link "${linkName}" ${message}`, relation.file);
|
|
1113
1896
|
};
|
|
1114
1897
|
if (link.enforce && link.linkKind !== "toOne") {
|
|
1115
|
-
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");
|
|
1116
1902
|
}
|
|
1117
1903
|
if (link.linkKind === "toPlatformUser") {
|
|
1118
|
-
if (link.target !== void 0) return
|
|
1904
|
+
if (link.target !== void 0) return fail3("is toPlatformUser and must not declare a target table");
|
|
1119
1905
|
return {
|
|
1120
1906
|
name: linkName,
|
|
1121
1907
|
kind: "toPlatformUser",
|
|
@@ -1125,13 +1911,13 @@ function buildCanonicalLink({
|
|
|
1125
1911
|
...link.description !== void 0 ? { description: link.description } : {}
|
|
1126
1912
|
};
|
|
1127
1913
|
}
|
|
1128
|
-
const target = resolveRelationRef({ ref: link.target, fail:
|
|
1914
|
+
const target = resolveRelationRef({ ref: link.target, fail: fail3 });
|
|
1129
1915
|
const targetName = relationName(target);
|
|
1130
1916
|
if (!relationsByName.has(targetName)) {
|
|
1131
|
-
return
|
|
1917
|
+
return fail3(`targets ${relationKind(target)} "${targetName}" which is not part of this namespace`);
|
|
1132
1918
|
}
|
|
1133
1919
|
if (link.enforce && (!isSeqTable(relation) || !isSeqTable(target))) {
|
|
1134
|
-
return
|
|
1920
|
+
return fail3("cannot enforce a PostgreSQL foreign key involving a view");
|
|
1135
1921
|
}
|
|
1136
1922
|
const owningSide = link.linkKind === "toOne" ? relation : target;
|
|
1137
1923
|
const referencedSide = link.linkKind === "toOne" ? target : relation;
|
|
@@ -1142,22 +1928,24 @@ function buildCanonicalLink({
|
|
|
1142
1928
|
on: resolveColumnRef({ relation: owningSide, ref: link.on, role: `link "${linkName}" on` }),
|
|
1143
1929
|
...link.references !== void 0 ? { references: resolveColumnRef({ relation: referencedSide, ref: link.references, role: `link "${linkName}" references` }) } : {},
|
|
1144
1930
|
enforce: link.enforce,
|
|
1931
|
+
...link.onDelete !== void 0 ? { onDelete: link.onDelete } : {},
|
|
1932
|
+
...link.onUpdate !== void 0 ? { onUpdate: link.onUpdate } : {},
|
|
1145
1933
|
...link.description !== void 0 ? { description: link.description } : {}
|
|
1146
1934
|
};
|
|
1147
1935
|
}
|
|
1148
|
-
function resolveRelationRef({ ref, fail:
|
|
1149
|
-
if (ref === void 0) return
|
|
1936
|
+
function resolveRelationRef({ ref, fail: fail3 }) {
|
|
1937
|
+
if (ref === void 0) return fail3("declares no target relation");
|
|
1150
1938
|
if (typeof ref !== "function") return ref;
|
|
1151
1939
|
let resolved;
|
|
1152
1940
|
try {
|
|
1153
1941
|
resolved = ref();
|
|
1154
1942
|
} catch (error) {
|
|
1155
|
-
return
|
|
1943
|
+
return fail3(
|
|
1156
1944
|
`target thunk threw (${error instanceof Error ? error.message : String(error)}) \u2014 lazy refs resolve at serialize time; make sure the target module ran`
|
|
1157
1945
|
);
|
|
1158
1946
|
}
|
|
1159
1947
|
if (!isSeqTable(resolved) && !isSeqView(resolved)) {
|
|
1160
|
-
return
|
|
1948
|
+
return fail3("target thunk did not return a relation defined with defineTable/defineView");
|
|
1161
1949
|
}
|
|
1162
1950
|
return resolved;
|
|
1163
1951
|
}
|
|
@@ -1224,6 +2012,10 @@ async function loadNamespaceFromDir(dir, options = {}) {
|
|
|
1224
2012
|
fsCache: false,
|
|
1225
2013
|
...options.ormModulePath !== void 0 ? {
|
|
1226
2014
|
alias: {
|
|
2015
|
+
// The public /drizzle barrel re-exports this pinned dependency.
|
|
2016
|
+
// Explicitly map it before the root alias: otherwise jiti appends
|
|
2017
|
+
// "/drizzle" to the overridden module's filename.
|
|
2018
|
+
"@sequenceholdings/orm/drizzle": module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))).resolve("drizzle-orm"),
|
|
1227
2019
|
"@sequenceholdings/orm": options.ormModulePath,
|
|
1228
2020
|
"@sequenceholdings/orm/authoring": options.ormModulePath
|
|
1229
2021
|
}
|
|
@@ -1437,6 +2229,7 @@ var ORDER_BY_ENUM_VALUES = [
|
|
|
1437
2229
|
"DESC_NULLS_LAST"
|
|
1438
2230
|
];
|
|
1439
2231
|
var PLATFORM_USER_TYPENAME = "PlatformUser";
|
|
2232
|
+
var DATE_BUCKET_ENUM = "DateBucket";
|
|
1440
2233
|
function tableNames(table) {
|
|
1441
2234
|
const root = camelCase(table);
|
|
1442
2235
|
const rootPascal = pascalCase(table);
|
|
@@ -1452,6 +2245,7 @@ function tableNames(table) {
|
|
|
1452
2245
|
updateByPkRoot: `update${rootPascal}ByPk`,
|
|
1453
2246
|
deleteRoot: `delete${rootPascal}`,
|
|
1454
2247
|
deleteByPkRoot: `delete${rootPascal}ByPk`,
|
|
2248
|
+
revertByPkRoot: `revert${rootPascal}ByPk`,
|
|
1455
2249
|
boolExp: `${type}BoolExp`,
|
|
1456
2250
|
orderBy: `${type}OrderBy`,
|
|
1457
2251
|
selectColumn: `${type}SelectColumn`,
|
|
@@ -1469,9 +2263,25 @@ function tableNames(table) {
|
|
|
1469
2263
|
sumFields: `${type}SumFields`,
|
|
1470
2264
|
avgFields: `${type}AvgFields`,
|
|
1471
2265
|
aggregateBoolExp: `${type}AggregateBoolExp`,
|
|
1472
|
-
aggregateBoolExpCount: `${type}AggregateBoolExpCount
|
|
2266
|
+
aggregateBoolExpCount: `${type}AggregateBoolExpCount`,
|
|
2267
|
+
aggregateGroupsRoot: `${root}AggregateGroups`,
|
|
2268
|
+
groupByColumnEnum: `${type}GroupByColumn`,
|
|
2269
|
+
groupByInput: `${type}GroupBy`,
|
|
2270
|
+
groupKey: `${type}GroupKey`,
|
|
2271
|
+
aggregateGroup: `${type}AggregateGroup`
|
|
1473
2272
|
};
|
|
1474
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
|
+
}
|
|
1475
2285
|
|
|
1476
2286
|
// src/codegen-gql/type-policies.ts
|
|
1477
2287
|
function generateTypePolicies(canonical) {
|
|
@@ -1479,13 +2289,13 @@ function generateTypePolicies(canonical) {
|
|
|
1479
2289
|
const entries = [];
|
|
1480
2290
|
let hasPlatformUser = false;
|
|
1481
2291
|
for (const table of canonicalRelations(def)) {
|
|
1482
|
-
if (table.source !== void 0) continue;
|
|
2292
|
+
if (table.source !== void 0 || table.internal === true) continue;
|
|
1483
2293
|
if (table.links.some((link) => link.kind === "toPlatformUser")) hasPlatformUser = true;
|
|
1484
2294
|
if (!table.primaryKey || table.primaryKey.length === 0) continue;
|
|
1485
|
-
const keyFields = table.primaryKey.map((
|
|
1486
|
-
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);
|
|
1487
2297
|
if (!column) {
|
|
1488
|
-
throw new CodegenGqlError(`Primary key column "${
|
|
2298
|
+
throw new CodegenGqlError(`Primary key column "${identifier3}" not found on relation "${table.table}".`);
|
|
1489
2299
|
}
|
|
1490
2300
|
return fieldName(column.name);
|
|
1491
2301
|
});
|
|
@@ -1502,7 +2312,100 @@ ${body}
|
|
|
1502
2312
|
}
|
|
1503
2313
|
`;
|
|
1504
2314
|
}
|
|
1505
|
-
|
|
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
|
+
}
|
|
2397
|
+
var IR_VERSION = 2;
|
|
2398
|
+
var LEGACY_IR_VERSION = 1;
|
|
2399
|
+
var irVersionSchema = zod.z.union([zod.z.literal(LEGACY_IR_VERSION), zod.z.literal(IR_VERSION)]);
|
|
2400
|
+
var operationVersionSchema = zod.z.object({ irVersion: irVersionSchema });
|
|
2401
|
+
function requiredArtifactIrVersion(operations) {
|
|
2402
|
+
let required = LEGACY_IR_VERSION;
|
|
2403
|
+
for (const operation of operations) {
|
|
2404
|
+
const { irVersion } = operationVersionSchema.parse(operation);
|
|
2405
|
+
if (irVersion > required) required = irVersion;
|
|
2406
|
+
}
|
|
2407
|
+
return required;
|
|
2408
|
+
}
|
|
1506
2409
|
var CLAIMS_CONTRACT = {
|
|
1507
2410
|
sub: "string",
|
|
1508
2411
|
org: "string?",
|
|
@@ -1574,6 +2477,21 @@ var selectionIrSchema = zod.z.lazy(
|
|
|
1574
2477
|
() => zod.z.object({
|
|
1575
2478
|
typename: zod.z.string(),
|
|
1576
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(),
|
|
1577
2495
|
relations: zod.z.array(zod.z.object({
|
|
1578
2496
|
alias: zod.z.string(),
|
|
1579
2497
|
link: zod.z.string(),
|
|
@@ -1595,6 +2513,11 @@ var selectionIrSchema = zod.z.lazy(
|
|
|
1595
2513
|
})
|
|
1596
2514
|
);
|
|
1597
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
|
+
});
|
|
1598
2521
|
var selectRootSchema = zod.z.object({
|
|
1599
2522
|
kind: zod.z.literal("select"),
|
|
1600
2523
|
alias: zod.z.string(),
|
|
@@ -1606,7 +2529,8 @@ var selectRootSchema = zod.z.object({
|
|
|
1606
2529
|
distinctOn: zod.z.union([zod.z.array(zod.z.string()), zod.z.object({ slot: zod.z.string() })]).optional(),
|
|
1607
2530
|
limit: limitIrSchema,
|
|
1608
2531
|
offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
|
|
1609
|
-
includeIf: includeIfSchema.optional()
|
|
2532
|
+
includeIf: includeIfSchema.optional(),
|
|
2533
|
+
prefetch: zod.z.array(prefetchPlanIrSchema).optional()
|
|
1610
2534
|
});
|
|
1611
2535
|
var byPkRootSchema = zod.z.object({
|
|
1612
2536
|
kind: zod.z.literal("byPk"),
|
|
@@ -1616,7 +2540,8 @@ var byPkRootSchema = zod.z.object({
|
|
|
1616
2540
|
pk: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).min(1),
|
|
1617
2541
|
/** Policy predicate — ANDed with the PK match. */
|
|
1618
2542
|
where: predIrSchema,
|
|
1619
|
-
includeIf: includeIfSchema.optional()
|
|
2543
|
+
includeIf: includeIfSchema.optional(),
|
|
2544
|
+
prefetch: zod.z.array(prefetchPlanIrSchema).optional()
|
|
1620
2545
|
});
|
|
1621
2546
|
var aggregateRootSchema = zod.z.object({
|
|
1622
2547
|
kind: zod.z.literal("aggregate"),
|
|
@@ -1629,6 +2554,33 @@ var aggregateRootSchema = zod.z.object({
|
|
|
1629
2554
|
nodes: selectionIrSchema.optional(),
|
|
1630
2555
|
includeIf: includeIfSchema.optional()
|
|
1631
2556
|
});
|
|
2557
|
+
var DATE_BUCKETS = ["year", "quarter", "month", "week", "day", "hour"];
|
|
2558
|
+
var groupByKeyIrSchema = zod.z.object({
|
|
2559
|
+
column: zod.z.string(),
|
|
2560
|
+
bucket: zod.z.enum(DATE_BUCKETS).optional()
|
|
2561
|
+
});
|
|
2562
|
+
var aggregateGroupsRootSchema = zod.z.object({
|
|
2563
|
+
kind: zod.z.literal("aggregateGroups"),
|
|
2564
|
+
alias: zod.z.string(),
|
|
2565
|
+
table: zod.z.string(),
|
|
2566
|
+
/** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
|
|
2567
|
+
where: predIrSchema,
|
|
2568
|
+
groupBy: zod.z.array(groupByKeyIrSchema).min(1),
|
|
2569
|
+
/**
|
|
2570
|
+
* The `key { … }` projection: which group columns appear in each entry's key
|
|
2571
|
+
* object, in selection order. A subset of `groupBy` (the compiler rejects a
|
|
2572
|
+
* key field that is not a group key — it would be indeterminate within the
|
|
2573
|
+
* group). Empty when the document selects only `aggregate`.
|
|
2574
|
+
*/
|
|
2575
|
+
key: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })).default([]),
|
|
2576
|
+
count: zod.z.object({ columns: zod.z.array(zod.z.string()).optional(), distinct: zod.z.boolean().optional() }).optional(),
|
|
2577
|
+
sum: zod.z.array(zod.z.string()).optional(),
|
|
2578
|
+
avg: zod.z.array(zod.z.string()).optional(),
|
|
2579
|
+
/** Bounds the GROUP count, not the scanned rows — see MAX_GROUPS. */
|
|
2580
|
+
limit: limitIrSchema,
|
|
2581
|
+
offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
|
|
2582
|
+
includeIf: includeIfSchema.optional()
|
|
2583
|
+
});
|
|
1632
2584
|
var writeValuesSchema = zod.z.union([
|
|
1633
2585
|
zod.z.object({ slot: zod.z.string() }),
|
|
1634
2586
|
zod.z.object({ entries: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })) })
|
|
@@ -1647,7 +2599,20 @@ var mutationRootSchema = zod.z.object({
|
|
|
1647
2599
|
* coalesce view falls back to synced. Both are gated on a matching synced
|
|
1648
2600
|
* row and carry the write policy in `where` like update/delete.
|
|
1649
2601
|
*/
|
|
1650
|
-
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(),
|
|
1651
2616
|
alias: zod.z.string(),
|
|
1652
2617
|
table: zod.z.string(),
|
|
1653
2618
|
/** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
|
|
@@ -1674,12 +2639,15 @@ var mutationRootSchema = zod.z.object({
|
|
|
1674
2639
|
insertCheck: predIrSchema.optional(),
|
|
1675
2640
|
returning: selectionIrSchema.nullable(),
|
|
1676
2641
|
affectedRows: zod.z.boolean().default(false),
|
|
2642
|
+
/** v2: abort the transaction when the actual write count differs, regardless of selection. */
|
|
2643
|
+
assertAffectedRows: zod.z.number().int().min(0).max(2147483647).optional(),
|
|
1677
2644
|
includeIf: includeIfSchema.optional()
|
|
1678
2645
|
});
|
|
1679
2646
|
var rootIrSchema = zod.z.discriminatedUnion("kind", [
|
|
1680
2647
|
selectRootSchema,
|
|
1681
2648
|
byPkRootSchema,
|
|
1682
2649
|
aggregateRootSchema,
|
|
2650
|
+
aggregateGroupsRootSchema,
|
|
1683
2651
|
mutationRootSchema
|
|
1684
2652
|
]);
|
|
1685
2653
|
var INPUT_REF_KINDS = [
|
|
@@ -1709,7 +2677,7 @@ var variableIrSchema = zod.z.object({
|
|
|
1709
2677
|
defaultValue: variableDefaultIrSchema.optional()
|
|
1710
2678
|
});
|
|
1711
2679
|
var compiledOperationSchema = zod.z.object({
|
|
1712
|
-
irVersion:
|
|
2680
|
+
irVersion: irVersionSchema,
|
|
1713
2681
|
name: zod.z.string(),
|
|
1714
2682
|
opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
1715
2683
|
kind: zod.z.enum(["query", "mutation"]),
|
|
@@ -1721,6 +2689,50 @@ var compiledOperationSchema = zod.z.object({
|
|
|
1721
2689
|
/** Every table the op touches — data-token scoping + audit. */
|
|
1722
2690
|
tableSet: zod.z.array(zod.z.string()).min(1),
|
|
1723
2691
|
roots: zod.z.array(rootIrSchema).min(1)
|
|
2692
|
+
}).superRefine((operation, ctx) => {
|
|
2693
|
+
for (const [index3, root] of operation.roots.entries()) {
|
|
2694
|
+
if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
|
|
2695
|
+
if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
|
|
2696
|
+
ctx.addIssue({ code: "custom", path: ["roots", index3], message: "affected-row assertions require unconditional mutation roots and IR v2" });
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
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()
|
|
1724
2736
|
});
|
|
1725
2737
|
var tableMetaIrSchema = zod.z.object({
|
|
1726
2738
|
table: zod.z.string(),
|
|
@@ -1757,18 +2769,45 @@ var tableMetaIrSchema = zod.z.object({
|
|
|
1757
2769
|
insertable: zod.z.array(zod.z.string()),
|
|
1758
2770
|
settable: zod.z.array(zod.z.string()),
|
|
1759
2771
|
appendable: zod.z.array(zod.z.string()),
|
|
2772
|
+
/**
|
|
2773
|
+
* Logical columns stamped with Postgres `now()` on UPDATE (canonical
|
|
2774
|
+
* `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
|
|
2775
|
+
*/
|
|
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(),
|
|
1760
2793
|
constraints: zod.z.array(zod.z.object({ name: zod.z.string(), columns: zod.z.array(zod.z.string()) })),
|
|
1761
2794
|
/** Depth/node caps for dynamic filter trees on this table. */
|
|
1762
2795
|
dynamicFilterMaxDepth: zod.z.number().int().positive().default(6),
|
|
1763
2796
|
dynamicFilterMaxNodes: zod.z.number().int().positive().default(64)
|
|
1764
2797
|
});
|
|
1765
2798
|
var compiledArtifactSchema = zod.z.object({
|
|
1766
|
-
irVersion:
|
|
2799
|
+
irVersion: irVersionSchema,
|
|
1767
2800
|
namespace: zod.z.string(),
|
|
1768
2801
|
schemaHash: zod.z.string(),
|
|
1769
2802
|
opsHash: zod.z.string(),
|
|
1770
2803
|
tables: zod.z.record(zod.z.string(), tableMetaIrSchema),
|
|
1771
2804
|
operations: zod.z.record(zod.z.string(), compiledOperationSchema)
|
|
2805
|
+
}).superRefine((artifact, ctx) => {
|
|
2806
|
+
for (const [name, operation] of Object.entries(artifact.operations)) {
|
|
2807
|
+
if (operation.irVersion > artifact.irVersion) {
|
|
2808
|
+
ctx.addIssue({ code: "custom", path: ["operations", name, "irVersion"], message: "operation requires a newer IR version than its artifact" });
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
1772
2811
|
});
|
|
1773
2812
|
var IrVersionError = class extends Error {
|
|
1774
2813
|
constructor(found) {
|
|
@@ -1779,19 +2818,19 @@ var IrVersionError = class extends Error {
|
|
|
1779
2818
|
function normalizeIr(raw) {
|
|
1780
2819
|
if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
|
|
1781
2820
|
const version = raw.irVersion;
|
|
1782
|
-
if (version !== IR_VERSION) throw new IrVersionError(version);
|
|
2821
|
+
if (version !== LEGACY_IR_VERSION && version !== IR_VERSION) throw new IrVersionError(version);
|
|
1783
2822
|
return compiledArtifactSchema.parse(raw);
|
|
1784
2823
|
}
|
|
1785
2824
|
|
|
1786
2825
|
// src/predicate-v2/ast.ts
|
|
1787
2826
|
var POLICY_COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
|
|
1788
|
-
function walkPolicy(node,
|
|
1789
|
-
|
|
2827
|
+
function walkPolicy(node, visit2) {
|
|
2828
|
+
visit2(node);
|
|
1790
2829
|
if (node.kind === "and" || node.kind === "or") {
|
|
1791
|
-
for (const operand of node.operands) walkPolicy(operand,
|
|
2830
|
+
for (const operand of node.operands) walkPolicy(operand, visit2);
|
|
1792
2831
|
}
|
|
1793
|
-
if (node.kind === "not") walkPolicy(node.operand,
|
|
1794
|
-
if (node.kind === "exists") walkPolicy(node.where,
|
|
2832
|
+
if (node.kind === "not") walkPolicy(node.operand, visit2);
|
|
2833
|
+
if (node.kind === "exists") walkPolicy(node.where, visit2);
|
|
1795
2834
|
}
|
|
1796
2835
|
|
|
1797
2836
|
// src/predicate-v2/errors.ts
|
|
@@ -1819,7 +2858,7 @@ var IDENTIFIER_CHAR = /[a-zA-Z0-9_]/;
|
|
|
1819
2858
|
var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
|
|
1820
2859
|
var SCALAR_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => !type.endsWith("[]")).map(([key]) => key);
|
|
1821
2860
|
var ARRAY_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => type.endsWith("[]")).map(([key]) => key);
|
|
1822
|
-
function
|
|
2861
|
+
function tokenize2(source) {
|
|
1823
2862
|
const tokens = [];
|
|
1824
2863
|
let position = 0;
|
|
1825
2864
|
while (position < source.length) {
|
|
@@ -1898,7 +2937,7 @@ function readString(source, start) {
|
|
|
1898
2937
|
}
|
|
1899
2938
|
throw new PolicyParseError("unterminated string literal", start);
|
|
1900
2939
|
}
|
|
1901
|
-
var
|
|
2940
|
+
var Parser2 = class {
|
|
1902
2941
|
constructor(tokens) {
|
|
1903
2942
|
this.tokens = tokens;
|
|
1904
2943
|
}
|
|
@@ -2072,7 +3111,7 @@ var Parser = class {
|
|
|
2072
3111
|
function parsePolicy(source) {
|
|
2073
3112
|
const trimmed = source.trim();
|
|
2074
3113
|
if (trimmed.length === 0) throw new PolicyParseError("empty policy expression", 0);
|
|
2075
|
-
return { source: trimmed, ast: new
|
|
3114
|
+
return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
|
|
2076
3115
|
}
|
|
2077
3116
|
|
|
2078
3117
|
// src/predicate-v2/compile.ts
|
|
@@ -2297,21 +3336,21 @@ function andPred(...preds) {
|
|
|
2297
3336
|
if (operands.length === 1) return operands[0];
|
|
2298
3337
|
return { kind: "and", operands };
|
|
2299
3338
|
}
|
|
2300
|
-
function walkPred(pred,
|
|
2301
|
-
|
|
3339
|
+
function walkPred(pred, visit2) {
|
|
3340
|
+
visit2(pred);
|
|
2302
3341
|
switch (pred.kind) {
|
|
2303
3342
|
case "and":
|
|
2304
3343
|
case "or":
|
|
2305
|
-
for (const operand of pred.operands) walkPred(operand,
|
|
3344
|
+
for (const operand of pred.operands) walkPred(operand, visit2);
|
|
2306
3345
|
return;
|
|
2307
3346
|
case "not":
|
|
2308
|
-
walkPred(pred.operand,
|
|
3347
|
+
walkPred(pred.operand, visit2);
|
|
2309
3348
|
return;
|
|
2310
3349
|
case "exists":
|
|
2311
|
-
walkPred(pred.where,
|
|
3350
|
+
walkPred(pred.where, visit2);
|
|
2312
3351
|
return;
|
|
2313
3352
|
case "aggPredicate":
|
|
2314
|
-
if (pred.where) walkPred(pred.where,
|
|
3353
|
+
if (pred.where) walkPred(pred.where, visit2);
|
|
2315
3354
|
return;
|
|
2316
3355
|
default:
|
|
2317
3356
|
return;
|
|
@@ -2328,12 +3367,12 @@ function predContainsExists(pred) {
|
|
|
2328
3367
|
// src/compiler-gql/inject.ts
|
|
2329
3368
|
function collectPinnedColumns(writePolicy) {
|
|
2330
3369
|
const key = (pin) => `${pin.column}\0${pin.claim}`;
|
|
2331
|
-
const
|
|
3370
|
+
const visit2 = (node) => {
|
|
2332
3371
|
if (node.kind === "and") {
|
|
2333
3372
|
const merged = [];
|
|
2334
3373
|
const seen2 = /* @__PURE__ */ new Set();
|
|
2335
3374
|
for (const operand of node.operands) {
|
|
2336
|
-
for (const pin of
|
|
3375
|
+
for (const pin of visit2(operand)) {
|
|
2337
3376
|
if (seen2.has(key(pin))) continue;
|
|
2338
3377
|
seen2.add(key(pin));
|
|
2339
3378
|
merged.push(pin);
|
|
@@ -2344,9 +3383,9 @@ function collectPinnedColumns(writePolicy) {
|
|
|
2344
3383
|
if (node.kind === "or") {
|
|
2345
3384
|
const [first, ...rest] = node.operands;
|
|
2346
3385
|
if (first === void 0) return [];
|
|
2347
|
-
let common =
|
|
3386
|
+
let common = visit2(first);
|
|
2348
3387
|
for (const operand of rest) {
|
|
2349
|
-
const branch = new Set(
|
|
3388
|
+
const branch = new Set(visit2(operand).map(key));
|
|
2350
3389
|
common = common.filter((pin) => branch.has(key(pin)));
|
|
2351
3390
|
if (common.length === 0) break;
|
|
2352
3391
|
}
|
|
@@ -2358,7 +3397,7 @@ function collectPinnedColumns(writePolicy) {
|
|
|
2358
3397
|
return [];
|
|
2359
3398
|
};
|
|
2360
3399
|
const seen = /* @__PURE__ */ new Set();
|
|
2361
|
-
return
|
|
3400
|
+
return visit2(writePolicy).filter((pin) => {
|
|
2362
3401
|
if (seen.has(pin.column)) return false;
|
|
2363
3402
|
seen.add(pin.column);
|
|
2364
3403
|
return true;
|
|
@@ -2452,6 +3491,11 @@ var PolicyInjector = class {
|
|
|
2452
3491
|
where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table)),
|
|
2453
3492
|
...root.nodes ? { nodes: this.injectSelection(root.nodes, root.table) } : {}
|
|
2454
3493
|
};
|
|
3494
|
+
case "aggregateGroups":
|
|
3495
|
+
return {
|
|
3496
|
+
...root,
|
|
3497
|
+
where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table))
|
|
3498
|
+
};
|
|
2455
3499
|
case "mutation":
|
|
2456
3500
|
return this.injectMutation(root);
|
|
2457
3501
|
}
|
|
@@ -2459,6 +3503,7 @@ var PolicyInjector = class {
|
|
|
2459
3503
|
injectMutation(root) {
|
|
2460
3504
|
const table = root.table;
|
|
2461
3505
|
const returning = root.returning ? this.injectSelection(root.returning, table) : root.returning;
|
|
3506
|
+
if (root.op === "resolvedWrite") return this.injectResolvedWrite(root, returning);
|
|
2462
3507
|
if (root.op === "insert" || root.op === "insertOne") {
|
|
2463
3508
|
const insertPolicy = this.insert(table);
|
|
2464
3509
|
if (predContainsExists(insertPolicy)) {
|
|
@@ -2500,6 +3545,24 @@ var PolicyInjector = class {
|
|
|
2500
3545
|
}
|
|
2501
3546
|
return { ...root, where, returning };
|
|
2502
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
|
+
}
|
|
2503
3566
|
};
|
|
2504
3567
|
function collectTableSet(roots, ctx) {
|
|
2505
3568
|
const tables = /* @__PURE__ */ new Set();
|
|
@@ -2544,20 +3607,34 @@ function collectTableSet(roots, ctx) {
|
|
|
2544
3607
|
};
|
|
2545
3608
|
for (const root of roots) {
|
|
2546
3609
|
tables.add(root.table);
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
3610
|
+
switch (root.kind) {
|
|
3611
|
+
case "select":
|
|
3612
|
+
case "byPk":
|
|
3613
|
+
visitPred(root.where, root.table);
|
|
3614
|
+
visitSelection(root.selection, root.table);
|
|
3615
|
+
break;
|
|
3616
|
+
case "aggregate":
|
|
3617
|
+
visitPred(root.where, root.table);
|
|
3618
|
+
if (root.nodes) visitSelection(root.nodes, root.table);
|
|
3619
|
+
break;
|
|
3620
|
+
case "aggregateGroups":
|
|
3621
|
+
visitPred(root.where, root.table);
|
|
3622
|
+
break;
|
|
3623
|
+
case "mutation":
|
|
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);
|
|
3626
|
+
if (root.where) visitPred(root.where, root.table);
|
|
3627
|
+
if (root.insertCheck) visitPred(root.insertCheck, root.table);
|
|
3628
|
+
if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
|
|
3629
|
+
if (root.returning) visitSelection(root.returning, root.table);
|
|
3630
|
+
break;
|
|
3631
|
+
default: {
|
|
3632
|
+
const exhausted = root;
|
|
3633
|
+
throw new DocumentCompileError(
|
|
3634
|
+
`internal: unhandled root kind in tableSet collection ${JSON.stringify(exhausted)}`,
|
|
3635
|
+
"internal"
|
|
3636
|
+
);
|
|
3637
|
+
}
|
|
2561
3638
|
}
|
|
2562
3639
|
}
|
|
2563
3640
|
return [...tables].sort();
|
|
@@ -2575,7 +3652,12 @@ function typeNameForTable(table) {
|
|
|
2575
3652
|
}
|
|
2576
3653
|
function queryRootNames(table) {
|
|
2577
3654
|
const names = tableNames(table);
|
|
2578
|
-
return {
|
|
3655
|
+
return {
|
|
3656
|
+
select: names.selectRoot,
|
|
3657
|
+
byPk: names.byPkRoot,
|
|
3658
|
+
aggregate: names.aggregateRoot,
|
|
3659
|
+
aggregateGroups: names.aggregateGroupsRoot
|
|
3660
|
+
};
|
|
2579
3661
|
}
|
|
2580
3662
|
function mutationRootNames(table) {
|
|
2581
3663
|
const names = tableNames(table);
|
|
@@ -2656,7 +3738,15 @@ function resolveColumnEntries(table, enums, diagnostics) {
|
|
|
2656
3738
|
type = { enum: enumDef.name, values: [...enumDef.values] };
|
|
2657
3739
|
}
|
|
2658
3740
|
}
|
|
2659
|
-
return {
|
|
3741
|
+
return {
|
|
3742
|
+
name: column.name,
|
|
3743
|
+
columnName: column.columnName,
|
|
3744
|
+
type,
|
|
3745
|
+
nullable: column.nullable,
|
|
3746
|
+
deprecated: column.deprecated,
|
|
3747
|
+
onUpdateFn: column.onUpdateFn !== void 0,
|
|
3748
|
+
...column.cell ? { cell: column.cell } : {}
|
|
3749
|
+
};
|
|
2660
3750
|
});
|
|
2661
3751
|
}
|
|
2662
3752
|
function logicalColumn(entries, ref) {
|
|
@@ -2692,8 +3782,8 @@ function buildCompileContext(rawCanonical) {
|
|
|
2692
3782
|
const pkRefs = table.primaryKey ?? (isView ? [] : ["id"]);
|
|
2693
3783
|
const primaryKey2 = [];
|
|
2694
3784
|
for (const ref of pkRefs) {
|
|
2695
|
-
const
|
|
2696
|
-
if (!
|
|
3785
|
+
const resolved2 = logicalColumn(entries, ref);
|
|
3786
|
+
if (!resolved2) {
|
|
2697
3787
|
diagnostics.push({
|
|
2698
3788
|
severity: "error",
|
|
2699
3789
|
code: "unknown-pk-column",
|
|
@@ -2702,9 +3792,13 @@ function buildCompileContext(rawCanonical) {
|
|
|
2702
3792
|
});
|
|
2703
3793
|
continue;
|
|
2704
3794
|
}
|
|
2705
|
-
primaryKey2.push(
|
|
3795
|
+
primaryKey2.push(resolved2);
|
|
2706
3796
|
}
|
|
2707
|
-
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);
|
|
3801
|
+
const touchOnUpdate = isView ? [] : active.filter((c) => c.onUpdateFn).map((c) => c.name);
|
|
2708
3802
|
const constraints = [];
|
|
2709
3803
|
if (primaryKey2.length > 0) constraints.push({ name: `${table.table}_pkey`, columns: [...primaryKey2] });
|
|
2710
3804
|
for (const column of table.columns) {
|
|
@@ -2725,14 +3819,17 @@ function buildCompileContext(rawCanonical) {
|
|
|
2725
3819
|
// Synced twins (and views reading them, resolved transitively below)
|
|
2726
3820
|
// are claims-gated — see tableMetaIrSchema.claimsGated.
|
|
2727
3821
|
claimsGated: relationIsClaimsGated(table.table),
|
|
2728
|
-
columns: entries.map(({ deprecated: _deprecated, ...column }) => column),
|
|
3822
|
+
columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
|
|
2729
3823
|
primaryKey: isView ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
|
|
2730
3824
|
links: [],
|
|
2731
|
-
filterable:
|
|
2732
|
-
orderable:
|
|
2733
|
-
insertable: isView ? [] : active.map((c) => c.name),
|
|
2734
|
-
settable: isView ? [] : active.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),
|
|
2735
3829
|
appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
|
|
3830
|
+
...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
|
|
3831
|
+
...cells ? { cells } : {},
|
|
3832
|
+
...resolved ? { resolved: resolvedMeta({ resolved, pk: primaryKey2[0] }) } : {},
|
|
2736
3833
|
constraints: isView ? [] : constraints,
|
|
2737
3834
|
dynamicFilterMaxDepth: 6,
|
|
2738
3835
|
dynamicFilterMaxNodes: 64
|
|
@@ -2753,7 +3850,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
2753
3850
|
});
|
|
2754
3851
|
continue;
|
|
2755
3852
|
}
|
|
2756
|
-
const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false })) : columnEntries.get(targetName);
|
|
3853
|
+
const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false, onUpdateFn: false })) : columnEntries.get(targetName);
|
|
2757
3854
|
const onEntries = link.kind === "toMany" ? targetEntries : entries;
|
|
2758
3855
|
const referencesEntries = link.kind === "toMany" ? entries : targetEntries;
|
|
2759
3856
|
const on = logicalColumn(onEntries, link.on);
|
|
@@ -2805,7 +3902,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
2805
3902
|
compilePolicyFor("write", writePolicies);
|
|
2806
3903
|
compilePolicyFor("insert", insertPolicies);
|
|
2807
3904
|
compilePolicyFor("update", updatePolicies);
|
|
2808
|
-
if (isView) {
|
|
3905
|
+
if (isView && table.resolved === void 0) {
|
|
2809
3906
|
writePolicies.set(table.table, CONST_FALSE);
|
|
2810
3907
|
insertPolicies.set(table.table, CONST_FALSE);
|
|
2811
3908
|
updatePolicies.set(table.table, CONST_FALSE);
|
|
@@ -2844,18 +3941,23 @@ function buildCompileContext(rawCanonical) {
|
|
|
2844
3941
|
const queryRoots = /* @__PURE__ */ new Map();
|
|
2845
3942
|
const mutationRoots = /* @__PURE__ */ new Map();
|
|
2846
3943
|
for (const table of relations) {
|
|
3944
|
+
if (table.internal === true) continue;
|
|
2847
3945
|
const roots = queryRootNames(table.table);
|
|
2848
3946
|
queryRoots.set(roots.select, { kind: "select", table: table.table });
|
|
2849
3947
|
if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
|
|
2850
3948
|
queryRoots.set(roots.byPk, { kind: "byPk", table: table.table });
|
|
2851
3949
|
}
|
|
2852
3950
|
queryRoots.set(roots.aggregate, { kind: "aggregate", table: table.table });
|
|
3951
|
+
if (table.columns.some((column) => column.type !== "jsonb")) {
|
|
3952
|
+
queryRoots.set(roots.aggregateGroups, { kind: "aggregateGroups", table: table.table });
|
|
3953
|
+
}
|
|
2853
3954
|
const hasWritePolicy = table.policies.write !== void 0 || table.policies.insert !== void 0 || table.policies.update !== void 0;
|
|
2854
3955
|
if (!viewNames.has(table.table) && table.source === void 0 && hasWritePolicy) {
|
|
2855
3956
|
for (const [op, field] of Object.entries(mutationRootNames(table.table))) {
|
|
2856
3957
|
mutationRoots.set(field, { op, table: table.table });
|
|
2857
3958
|
}
|
|
2858
3959
|
}
|
|
3960
|
+
if (table.resolved !== void 0) registerResolvedRoots({ table, mutationRoots });
|
|
2859
3961
|
}
|
|
2860
3962
|
for (const table of relations) {
|
|
2861
3963
|
for (const mutation of table.cowMutations ?? []) {
|
|
@@ -2926,6 +4028,49 @@ function buildCompileContext(rawCanonical) {
|
|
|
2926
4028
|
diagnostics
|
|
2927
4029
|
};
|
|
2928
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
|
+
}
|
|
2929
4074
|
function flattenSelectionSet(selectionSet, fragments, stack) {
|
|
2930
4075
|
const selections = [];
|
|
2931
4076
|
for (const selection of selectionSet.selections) {
|
|
@@ -2983,8 +4128,148 @@ function computeOpsHash(opHashes) {
|
|
|
2983
4128
|
const digest = crypto.createHash("sha256").update(OPS_HASH_DOMAIN).update([...opHashes].sort().join("\n")).digest("hex");
|
|
2984
4129
|
return `sha256:${digest}`;
|
|
2985
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
|
|
2986
4261
|
var DEFAULT_LIMIT = 100;
|
|
2987
4262
|
var MAX_LIMIT = 1e3;
|
|
4263
|
+
var MAX_GROUPS = 1e3;
|
|
4264
|
+
var DATE_BUCKET_BY_ENUM = {
|
|
4265
|
+
YEAR: "year",
|
|
4266
|
+
QUARTER: "quarter",
|
|
4267
|
+
MONTH: "month",
|
|
4268
|
+
WEEK: "week",
|
|
4269
|
+
DAY: "day",
|
|
4270
|
+
HOUR: "hour"
|
|
4271
|
+
};
|
|
4272
|
+
var BUCKETABLE_TYPES = /* @__PURE__ */ new Set(["timestamptz", "date"]);
|
|
2988
4273
|
var BOOLEXP_OPS = {
|
|
2989
4274
|
_eq: "eq",
|
|
2990
4275
|
_neq: "neq",
|
|
@@ -3010,16 +4295,24 @@ var ORDER_DIRECTIONS = {
|
|
|
3010
4295
|
DESC_NULLS_LAST: { direction: "desc", nulls: "last" }
|
|
3011
4296
|
};
|
|
3012
4297
|
var NUMERIC_TYPES = /* @__PURE__ */ new Set(["integer", "bigint", "numeric", "double"]);
|
|
3013
|
-
function
|
|
4298
|
+
function fail2(message, code, candidates = []) {
|
|
3014
4299
|
throw new DocumentCompileError(message, code, candidates);
|
|
3015
4300
|
}
|
|
3016
4301
|
function argsByName(field) {
|
|
3017
4302
|
return new Map((field.arguments ?? []).map((arg) => [arg.name.value, arg]));
|
|
3018
4303
|
}
|
|
4304
|
+
function* aggregateBlockFields(selectionSet) {
|
|
4305
|
+
for (const node of selectionSet.selections) {
|
|
4306
|
+
if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
4307
|
+
if (node.name.value === "__typename") continue;
|
|
4308
|
+
if (node.alias) fail2(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
|
|
4309
|
+
yield node;
|
|
4310
|
+
}
|
|
4311
|
+
}
|
|
3019
4312
|
function assertKnownArgs(field, allowed) {
|
|
3020
4313
|
for (const arg of field.arguments ?? []) {
|
|
3021
4314
|
if (!allowed.includes(arg.name.value)) {
|
|
3022
|
-
|
|
4315
|
+
fail2(`argument '${arg.name.value}' on '${field.name.value}' is not supported in v2.0`, "unsupported-argument", allowed);
|
|
3023
4316
|
}
|
|
3024
4317
|
}
|
|
3025
4318
|
}
|
|
@@ -3032,8 +4325,11 @@ var OperationLowerer = class {
|
|
|
3032
4325
|
meta(table) {
|
|
3033
4326
|
return this.ctx.tables[table];
|
|
3034
4327
|
}
|
|
4328
|
+
/** A data column — cell-metadata columns are reachable only through `cells { … }`. */
|
|
3035
4329
|
column(table, name) {
|
|
3036
|
-
|
|
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;
|
|
3037
4333
|
}
|
|
3038
4334
|
link(table, name) {
|
|
3039
4335
|
return this.meta(table).links.find((l) => l.name === name);
|
|
@@ -3041,7 +4337,8 @@ var OperationLowerer = class {
|
|
|
3041
4337
|
fieldCandidates(table) {
|
|
3042
4338
|
const meta = this.meta(table);
|
|
3043
4339
|
return [
|
|
3044
|
-
...meta.columns.map((c) => c.name),
|
|
4340
|
+
...meta.columns.filter((c) => !isCellColumn(meta, c.name)).map((c) => c.name),
|
|
4341
|
+
...meta.cells ? [CELLS_FIELD] : [],
|
|
3045
4342
|
...meta.links.map((l) => l.name),
|
|
3046
4343
|
...meta.links.filter((l) => l.kind === "toMany").map((l) => `${l.name}Aggregate`)
|
|
3047
4344
|
];
|
|
@@ -3050,7 +4347,7 @@ var OperationLowerer = class {
|
|
|
3050
4347
|
slot(name, input, table) {
|
|
3051
4348
|
const variable = this.variables.get(name);
|
|
3052
4349
|
if (!variable || variable.type.kind !== "inputRef" || variable.type.input !== input || variable.type.table !== table) {
|
|
3053
|
-
|
|
4350
|
+
fail2(`variable $${name} must be declared as the ${input} input type for table '${table}'`, "slot-mismatch");
|
|
3054
4351
|
}
|
|
3055
4352
|
return name;
|
|
3056
4353
|
}
|
|
@@ -3075,14 +4372,14 @@ var OperationLowerer = class {
|
|
|
3075
4372
|
const items = node.values.map((item) => {
|
|
3076
4373
|
const lowered = this.lowerValue(item);
|
|
3077
4374
|
if (lowered.kind !== "literal" || Array.isArray(lowered.value)) {
|
|
3078
|
-
|
|
4375
|
+
fail2("list literals must contain scalar literals only \u2014 pass the whole list as a variable instead", "unsupported-literal");
|
|
3079
4376
|
}
|
|
3080
4377
|
return lowered.value;
|
|
3081
4378
|
});
|
|
3082
4379
|
return { kind: "literal", value: items };
|
|
3083
4380
|
}
|
|
3084
4381
|
case graphql.Kind.OBJECT:
|
|
3085
|
-
|
|
4382
|
+
fail2("literal JSON objects are not supported in documents \u2014 pass the value via a variable", "unsupported-literal");
|
|
3086
4383
|
}
|
|
3087
4384
|
}
|
|
3088
4385
|
// -------------------------------------------------------------------------
|
|
@@ -3092,7 +4389,7 @@ var OperationLowerer = class {
|
|
|
3092
4389
|
if (node.kind === graphql.Kind.VARIABLE) {
|
|
3093
4390
|
return { kind: "dynamicFilter", slot: this.slot(node.name.value, "boolExp", table) };
|
|
3094
4391
|
}
|
|
3095
|
-
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");
|
|
3096
4393
|
const operands = [];
|
|
3097
4394
|
for (const field of node.fields) {
|
|
3098
4395
|
const key = field.name.value;
|
|
@@ -3123,45 +4420,45 @@ var OperationLowerer = class {
|
|
|
3123
4420
|
continue;
|
|
3124
4421
|
}
|
|
3125
4422
|
}
|
|
3126
|
-
|
|
4423
|
+
fail2(`unknown field '${key}' in a boolean expression on '${table}'`, "unknown-column", this.fieldCandidates(table));
|
|
3127
4424
|
}
|
|
3128
4425
|
if (operands.length === 0) return CONST_TRUE;
|
|
3129
4426
|
return operands.length === 1 ? operands[0] : { kind: "and", operands };
|
|
3130
4427
|
}
|
|
3131
4428
|
lowerComparison(node, column, table) {
|
|
3132
4429
|
if (node.kind !== graphql.Kind.OBJECT) {
|
|
3133
|
-
|
|
4430
|
+
fail2(`comparison on '${table}.${column}' must be an object like { _eq: \u2026 }`, "invalid-comparison", Object.keys(BOOLEXP_OPS));
|
|
3134
4431
|
}
|
|
3135
4432
|
return node.fields.map((field) => {
|
|
3136
4433
|
const op = BOOLEXP_OPS[field.name.value];
|
|
3137
4434
|
if (!op) {
|
|
3138
|
-
|
|
4435
|
+
fail2(`unknown comparison operator '${field.name.value}' on '${table}.${column}'`, "unknown-operator", Object.keys(BOOLEXP_OPS));
|
|
3139
4436
|
}
|
|
3140
4437
|
return { kind: "cmp", op, column, value: this.lowerValue(field.value) };
|
|
3141
4438
|
});
|
|
3142
4439
|
}
|
|
3143
4440
|
/** Hasura aggregation predicate: `{ count: { predicate: { _gt: 2 }, filter: {…} } }`. */
|
|
3144
4441
|
lowerAggPredicate(node, link, target) {
|
|
3145
|
-
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");
|
|
3146
4443
|
const countField = node.fields.find((f) => f.name.value === "count");
|
|
3147
4444
|
if (!countField || node.fields.length !== 1) {
|
|
3148
|
-
|
|
4445
|
+
fail2(`aggregate predicates support { count: \u2026 } only in v2.0 (through '${link}')`, "invalid-agg-predicate", ["count"]);
|
|
3149
4446
|
}
|
|
3150
|
-
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");
|
|
3151
4448
|
let predicate;
|
|
3152
4449
|
let where;
|
|
3153
4450
|
for (const field of countField.value.fields) {
|
|
3154
4451
|
if (field.name.value === "predicate") {
|
|
3155
4452
|
if (field.value.kind !== graphql.Kind.OBJECT || field.value.fields.length !== 1) {
|
|
3156
|
-
|
|
4453
|
+
fail2(`count predicate through '${link}' needs exactly one operator`, "invalid-agg-predicate", [...AGG_PREDICATE_OPS]);
|
|
3157
4454
|
}
|
|
3158
4455
|
const opField = field.value.fields[0];
|
|
3159
4456
|
const opName = opField.name.value;
|
|
3160
4457
|
if (!AGG_PREDICATE_OPS.includes(opName)) {
|
|
3161
|
-
|
|
4458
|
+
fail2(`aggregate count predicates support ${AGG_PREDICATE_OPS.join("/")} only, got '${opName}'`, "invalid-agg-predicate");
|
|
3162
4459
|
}
|
|
3163
4460
|
if (opField.value.kind !== graphql.Kind.INT) {
|
|
3164
|
-
|
|
4461
|
+
fail2(`aggregate count predicate value must be an integer literal`, "invalid-agg-predicate");
|
|
3165
4462
|
}
|
|
3166
4463
|
predicate = { op: opName.slice(1), value: Number(opField.value.value) };
|
|
3167
4464
|
continue;
|
|
@@ -3170,9 +4467,9 @@ var OperationLowerer = class {
|
|
|
3170
4467
|
where = this.lowerBoolExp(field.value, target);
|
|
3171
4468
|
continue;
|
|
3172
4469
|
}
|
|
3173
|
-
|
|
4470
|
+
fail2(`unknown key '${field.name.value}' in a count predicate`, "invalid-agg-predicate", ["predicate", "filter"]);
|
|
3174
4471
|
}
|
|
3175
|
-
if (!predicate)
|
|
4472
|
+
if (!predicate) fail2(`count predicate through '${link}' needs a { predicate: { _gt: n } }`, "invalid-agg-predicate");
|
|
3176
4473
|
return { kind: "aggPredicate", link, fn: "count", op: predicate.op, value: predicate.value, ...where ? { where } : {} };
|
|
3177
4474
|
}
|
|
3178
4475
|
lowerOrderBy(node, table) {
|
|
@@ -3182,7 +4479,7 @@ var OperationLowerer = class {
|
|
|
3182
4479
|
const items = node.kind === graphql.Kind.LIST ? node.values : [node];
|
|
3183
4480
|
const lowered = [];
|
|
3184
4481
|
for (const item of items) {
|
|
3185
|
-
if (item.kind !== graphql.Kind.OBJECT)
|
|
4482
|
+
if (item.kind !== graphql.Kind.OBJECT) fail2(`orderBy entries on '${table}' must be objects`, "invalid-orderby");
|
|
3186
4483
|
for (const field of item.fields) {
|
|
3187
4484
|
const key = field.name.value;
|
|
3188
4485
|
const column = this.column(table, key);
|
|
@@ -3193,25 +4490,25 @@ var OperationLowerer = class {
|
|
|
3193
4490
|
const link = this.link(table, key);
|
|
3194
4491
|
if (link && link.kind !== "toMany") {
|
|
3195
4492
|
if (field.value.kind !== graphql.Kind.OBJECT || field.value.fields.length !== 1) {
|
|
3196
|
-
|
|
4493
|
+
fail2(`orderBy through '${key}' needs exactly one target column`, "invalid-orderby");
|
|
3197
4494
|
}
|
|
3198
4495
|
const inner = field.value.fields[0];
|
|
3199
4496
|
const targetColumn = this.column(link.target, inner.name.value);
|
|
3200
4497
|
if (!targetColumn) {
|
|
3201
|
-
|
|
4498
|
+
fail2(`unknown column '${inner.name.value}' on '${link.target}' in orderBy`, "unknown-column", this.meta(link.target).columns.map((c) => c.name));
|
|
3202
4499
|
}
|
|
3203
4500
|
lowered.push({ kind: "column", column: inner.name.value, link: key, ...this.lowerDirection(inner.value, link.target, inner.name.value) });
|
|
3204
4501
|
continue;
|
|
3205
4502
|
}
|
|
3206
|
-
|
|
4503
|
+
fail2(`unknown orderBy key '${key}' on '${table}' (to-one links only)`, "invalid-orderby", this.fieldCandidates(table));
|
|
3207
4504
|
}
|
|
3208
4505
|
}
|
|
3209
4506
|
return lowered;
|
|
3210
4507
|
}
|
|
3211
4508
|
lowerDirection(node, table, column) {
|
|
3212
|
-
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));
|
|
3213
4510
|
const direction = ORDER_DIRECTIONS[node.value];
|
|
3214
|
-
if (!direction)
|
|
4511
|
+
if (!direction) fail2(`unknown orderBy direction '${node.value}'`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
|
|
3215
4512
|
return direction.nulls ? { direction: direction.direction, nulls: direction.nulls } : { direction: direction.direction };
|
|
3216
4513
|
}
|
|
3217
4514
|
lowerDistinctOn(node, table) {
|
|
@@ -3220,9 +4517,9 @@ var OperationLowerer = class {
|
|
|
3220
4517
|
}
|
|
3221
4518
|
const items = node.kind === graphql.Kind.LIST ? node.values : [node];
|
|
3222
4519
|
return items.map((item) => {
|
|
3223
|
-
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");
|
|
3224
4521
|
if (!this.column(table, item.value)) {
|
|
3225
|
-
|
|
4522
|
+
fail2(`unknown distinctOn column '${item.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
|
|
3226
4523
|
}
|
|
3227
4524
|
return item.value;
|
|
3228
4525
|
});
|
|
@@ -3233,16 +4530,16 @@ var OperationLowerer = class {
|
|
|
3233
4530
|
if (node.kind === graphql.Kind.VARIABLE) return { ...base, variable: node.name.value };
|
|
3234
4531
|
if (node.kind === graphql.Kind.INT) {
|
|
3235
4532
|
const value = Number(node.value);
|
|
3236
|
-
if (value < 1)
|
|
4533
|
+
if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
|
|
3237
4534
|
return { ...base, value: Math.min(value, MAX_LIMIT) };
|
|
3238
4535
|
}
|
|
3239
|
-
|
|
4536
|
+
fail2("limit must be an integer literal or a variable", "invalid-limit");
|
|
3240
4537
|
}
|
|
3241
4538
|
lowerOffset(node) {
|
|
3242
4539
|
if (!node) return void 0;
|
|
3243
4540
|
if (node.kind === graphql.Kind.VARIABLE) return { variable: node.name.value };
|
|
3244
4541
|
if (node.kind === graphql.Kind.INT) return { value: Number(node.value) };
|
|
3245
|
-
|
|
4542
|
+
fail2("offset must be an integer literal or a variable", "invalid-offset");
|
|
3246
4543
|
}
|
|
3247
4544
|
// -------------------------------------------------------------------------
|
|
3248
4545
|
// Selections
|
|
@@ -3251,9 +4548,9 @@ var OperationLowerer = class {
|
|
|
3251
4548
|
const meta = this.meta(table);
|
|
3252
4549
|
const selection = { typename: meta.typename, columns: [], relations: [], aggregates: [] };
|
|
3253
4550
|
for (const node of selectionSet.selections) {
|
|
3254
|
-
if (node.kind !== graphql.Kind.FIELD)
|
|
4551
|
+
if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
3255
4552
|
if (node.directives && node.directives.length > 0) {
|
|
3256
|
-
|
|
4553
|
+
fail2(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
|
|
3257
4554
|
}
|
|
3258
4555
|
const name = node.name.value;
|
|
3259
4556
|
if (name === "__typename") continue;
|
|
@@ -3261,14 +4558,19 @@ var OperationLowerer = class {
|
|
|
3261
4558
|
const column = this.column(table, name);
|
|
3262
4559
|
if (column) {
|
|
3263
4560
|
if (node.arguments !== void 0 && node.arguments.length > 0 || node.selectionSet !== void 0) {
|
|
3264
|
-
|
|
4561
|
+
fail2(`column '${table}.${name}' takes no arguments or sub-selection`, "invalid-selection");
|
|
3265
4562
|
}
|
|
3266
4563
|
selection.columns.push({ alias, column: name });
|
|
3267
4564
|
continue;
|
|
3268
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
|
+
}
|
|
3269
4571
|
const link = this.link(table, name);
|
|
3270
4572
|
if (link) {
|
|
3271
|
-
if (!node.selectionSet)
|
|
4573
|
+
if (!node.selectionSet) fail2(`link '${table}.${name}' needs a selection set`, "invalid-selection");
|
|
3272
4574
|
const relation = {
|
|
3273
4575
|
alias,
|
|
3274
4576
|
link: name,
|
|
@@ -3287,7 +4589,7 @@ var OperationLowerer = class {
|
|
|
3287
4589
|
if (offset) relation.offset = offset;
|
|
3288
4590
|
}
|
|
3289
4591
|
} else if (node.arguments && node.arguments.length > 0) {
|
|
3290
|
-
|
|
4592
|
+
fail2(`to-one link '${table}.${name}' takes no arguments`, "invalid-selection");
|
|
3291
4593
|
}
|
|
3292
4594
|
selection.relations.push(relation);
|
|
3293
4595
|
continue;
|
|
@@ -3295,7 +4597,7 @@ var OperationLowerer = class {
|
|
|
3295
4597
|
if (name.endsWith("Aggregate")) {
|
|
3296
4598
|
const aggLink = this.link(table, name.slice(0, -"Aggregate".length));
|
|
3297
4599
|
if (aggLink && aggLink.kind === "toMany") {
|
|
3298
|
-
if (!node.selectionSet)
|
|
4600
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
3299
4601
|
assertKnownArgs(node, ["where"]);
|
|
3300
4602
|
const whereArg = argsByName(node).get("where");
|
|
3301
4603
|
const aggregate = this.lowerAggregateSelection(node.selectionSet, aggLink.target);
|
|
@@ -3308,70 +4610,182 @@ var OperationLowerer = class {
|
|
|
3308
4610
|
continue;
|
|
3309
4611
|
}
|
|
3310
4612
|
}
|
|
3311
|
-
|
|
4613
|
+
fail2(`unknown field '${name}' on '${table}'`, "unknown-field", this.fieldCandidates(table));
|
|
4614
|
+
}
|
|
4615
|
+
return selection;
|
|
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
|
+
}
|
|
4664
|
+
/** The `{ aggregate { count sum {…} avg {…} } nodes {…} }` block shared by roots and relations. */
|
|
4665
|
+
lowerAggregateSelection(selectionSet, table) {
|
|
4666
|
+
const result = {};
|
|
4667
|
+
let sawAggregate = false;
|
|
4668
|
+
for (const node of aggregateBlockFields(selectionSet)) {
|
|
4669
|
+
const name = node.name.value;
|
|
4670
|
+
if (name === "nodes") {
|
|
4671
|
+
if (!node.selectionSet) fail2("'nodes' needs a selection set", "invalid-aggregate");
|
|
4672
|
+
result.nodes = this.lowerSelection(node.selectionSet, table);
|
|
4673
|
+
continue;
|
|
4674
|
+
}
|
|
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");
|
|
4677
|
+
sawAggregate = true;
|
|
4678
|
+
if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
|
|
4679
|
+
Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
|
|
4680
|
+
}
|
|
4681
|
+
if (!sawAggregate && !result.nodes) fail2("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
|
|
4682
|
+
return result;
|
|
4683
|
+
}
|
|
4684
|
+
/**
|
|
4685
|
+
* The `{ key {…} aggregate {…} }` block of a grouped aggregate root. Same
|
|
4686
|
+
* `aggregate` sub-block as the ungrouped shape (shared via
|
|
4687
|
+
* `lowerAggregateFields`), but `key` replaces `nodes` — see the IR comment on
|
|
4688
|
+
* `aggregateGroupsRootSchema` for why grouped roots have no `nodes`.
|
|
4689
|
+
*/
|
|
4690
|
+
lowerGroupedAggregateSelection(selectionSet, table, groupBy) {
|
|
4691
|
+
const groupColumns = new Set(groupBy.map((entry) => entry.column));
|
|
4692
|
+
const result = { key: [] };
|
|
4693
|
+
let sawAggregate = false;
|
|
4694
|
+
let sawKey = false;
|
|
4695
|
+
for (const node of aggregateBlockFields(selectionSet)) {
|
|
4696
|
+
const name = node.name.value;
|
|
4697
|
+
if (name === "key") {
|
|
4698
|
+
if (sawKey) fail2("one key block per grouped aggregate", "invalid-aggregate");
|
|
4699
|
+
sawKey = true;
|
|
4700
|
+
if (!node.selectionSet) fail2("'key' needs a selection set", "invalid-aggregate");
|
|
4701
|
+
result.key = this.lowerGroupKeySelection(node.selectionSet, table, groupColumns);
|
|
4702
|
+
continue;
|
|
4703
|
+
}
|
|
4704
|
+
if (name === "nodes") {
|
|
4705
|
+
fail2("'nodes' is not available on a grouped aggregate \u2014 query the rows separately", "invalid-aggregate", ["key", "aggregate"]);
|
|
4706
|
+
}
|
|
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");
|
|
4709
|
+
sawAggregate = true;
|
|
4710
|
+
if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
|
|
4711
|
+
Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
|
|
4712
|
+
}
|
|
4713
|
+
const rendersAggregate = result.count !== void 0 || (result.sum?.length ?? 0) > 0 || (result.avg?.length ?? 0) > 0;
|
|
4714
|
+
if (result.key.length === 0 && !rendersAggregate) {
|
|
4715
|
+
fail2(
|
|
4716
|
+
"grouped aggregate needs at least one key column or an aggregate \u2014 a block selecting only __typename produces nothing",
|
|
4717
|
+
"invalid-aggregate",
|
|
4718
|
+
["key", "aggregate"]
|
|
4719
|
+
);
|
|
3312
4720
|
}
|
|
3313
|
-
return
|
|
4721
|
+
return result;
|
|
3314
4722
|
}
|
|
3315
|
-
/**
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
4723
|
+
/**
|
|
4724
|
+
* `key { month officer }` → the projected group columns. Every field must be
|
|
4725
|
+
* one of the query's own group keys: any other column is indeterminate
|
|
4726
|
+
* within a group, so selecting it is a document error rather than an
|
|
4727
|
+
* arbitrary pick.
|
|
4728
|
+
*/
|
|
4729
|
+
lowerGroupKeySelection(selectionSet, table, groupColumns) {
|
|
4730
|
+
const key = [];
|
|
3319
4731
|
for (const node of selectionSet.selections) {
|
|
3320
|
-
if (node.kind !== graphql.Kind.FIELD)
|
|
4732
|
+
if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
3321
4733
|
const name = node.name.value;
|
|
3322
4734
|
if (name === "__typename") continue;
|
|
3323
|
-
if (
|
|
3324
|
-
|
|
3325
|
-
if (!node.selectionSet) fail("'nodes' needs a selection set", "invalid-aggregate");
|
|
3326
|
-
result.nodes = this.lowerSelection(node.selectionSet, table);
|
|
3327
|
-
continue;
|
|
4735
|
+
if (!this.column(table, name)) {
|
|
4736
|
+
fail2(`unknown key column '${name}' on '${table}'`, "unknown-column", [...groupColumns]);
|
|
3328
4737
|
}
|
|
3329
|
-
if (name
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
4738
|
+
if (!groupColumns.has(name)) {
|
|
4739
|
+
fail2(
|
|
4740
|
+
`key column '${name}' is not one of this query's groupBy keys \u2014 it has no single value within a group`,
|
|
4741
|
+
"invalid-group-key",
|
|
4742
|
+
[...groupColumns]
|
|
4743
|
+
);
|
|
4744
|
+
}
|
|
4745
|
+
key.push({ alias: node.alias?.value ?? name, column: name });
|
|
4746
|
+
}
|
|
4747
|
+
return key;
|
|
4748
|
+
}
|
|
4749
|
+
/** The `count` / `sum {…}` / `avg {…}` fields inside an `aggregate { … }` block. */
|
|
4750
|
+
lowerAggregateFields(selectionSet, table) {
|
|
4751
|
+
const result = {};
|
|
4752
|
+
for (const aggField of aggregateBlockFields(selectionSet)) {
|
|
4753
|
+
const aggName = aggField.name.value;
|
|
4754
|
+
if (aggName === "count") {
|
|
4755
|
+
assertKnownArgs(aggField, ["columns", "distinct"]);
|
|
4756
|
+
const args = argsByName(aggField);
|
|
4757
|
+
const count = {};
|
|
4758
|
+
const columns = args.get("columns");
|
|
4759
|
+
if (columns) count.columns = this.lowerDistinctOn(columns.value, table);
|
|
4760
|
+
const distinct = args.get("distinct");
|
|
4761
|
+
if (distinct) {
|
|
4762
|
+
if (distinct.value.kind !== graphql.Kind.BOOLEAN) fail2("count(distinct:) must be a boolean literal", "invalid-aggregate");
|
|
4763
|
+
count.distinct = distinct.value.value;
|
|
3351
4764
|
}
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
4765
|
+
result.count = count;
|
|
4766
|
+
continue;
|
|
4767
|
+
}
|
|
4768
|
+
if (aggName === "sum" || aggName === "avg") {
|
|
4769
|
+
if (!aggField.selectionSet) fail2(`'${aggName}' needs a column selection`, "invalid-aggregate");
|
|
4770
|
+
const columns = [];
|
|
4771
|
+
for (const columnField of aggField.selectionSet.selections) {
|
|
4772
|
+
if (columnField.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
4773
|
+
if (columnField.name.value === "__typename") continue;
|
|
4774
|
+
const column = this.column(table, columnField.name.value);
|
|
4775
|
+
if (!column || typeof column.type !== "string" || !NUMERIC_TYPES.has(column.type)) {
|
|
4776
|
+
fail2(
|
|
4777
|
+
`'${aggName}' supports numeric columns only, got '${columnField.name.value}' on '${table}'`,
|
|
4778
|
+
"invalid-aggregate",
|
|
4779
|
+
this.meta(table).columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type)).map((c) => c.name)
|
|
4780
|
+
);
|
|
3367
4781
|
}
|
|
3368
|
-
|
|
3369
|
-
continue;
|
|
4782
|
+
columns.push(columnField.name.value);
|
|
3370
4783
|
}
|
|
3371
|
-
|
|
4784
|
+
result[aggName] = columns;
|
|
4785
|
+
continue;
|
|
3372
4786
|
}
|
|
4787
|
+
fail2(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
|
|
3373
4788
|
}
|
|
3374
|
-
if (!sawAggregate && !result.nodes) fail("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
|
|
3375
4789
|
return result;
|
|
3376
4790
|
}
|
|
3377
4791
|
// -------------------------------------------------------------------------
|
|
@@ -3380,32 +4794,65 @@ var OperationLowerer = class {
|
|
|
3380
4794
|
lowerRoots(operation, kind) {
|
|
3381
4795
|
const roots = [];
|
|
3382
4796
|
for (const node of operation.selectionSet.selections) {
|
|
3383
|
-
if (node.kind !== graphql.Kind.FIELD)
|
|
4797
|
+
if (node.kind !== graphql.Kind.FIELD) fail2("fragments are not supported at the operation root", "invalid-root");
|
|
4798
|
+
if (node.selectionSet) graphql.visit(node.selectionSet, {
|
|
4799
|
+
Directive(directive) {
|
|
4800
|
+
if (directive.name.value === "assertAffectedRows") {
|
|
4801
|
+
fail2("@assertAffectedRows is supported on mutation roots only", "invalid-affected-row-assertion");
|
|
4802
|
+
}
|
|
4803
|
+
}
|
|
4804
|
+
});
|
|
4805
|
+
const assertion = this.parseAffectedRowAssertion(node, kind);
|
|
3384
4806
|
const gate = this.parseRootDirectives(node);
|
|
3385
4807
|
if (gate.omit) continue;
|
|
3386
|
-
const
|
|
3387
|
-
|
|
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 });
|
|
4811
|
+
const guarded = root.kind === "mutation" && assertion !== void 0 ? { ...root, assertAffectedRows: assertion } : root;
|
|
4812
|
+
roots.push(gate.includeIf ? { ...guarded, includeIf: gate.includeIf } : guarded);
|
|
3388
4813
|
}
|
|
3389
|
-
if (roots.length === 0)
|
|
4814
|
+
if (roots.length === 0) fail2("operation has no roots (all statically skipped?)", "invalid-root");
|
|
3390
4815
|
return roots;
|
|
3391
4816
|
}
|
|
4817
|
+
parseAffectedRowAssertion(node, kind) {
|
|
4818
|
+
const assertions = node.directives?.filter((directive) => directive.name.value === "assertAffectedRows") ?? [];
|
|
4819
|
+
if (!assertions.length) return void 0;
|
|
4820
|
+
if (kind !== "mutation" || assertions.length !== 1) {
|
|
4821
|
+
fail2("@assertAffectedRows is supported once per mutation root", "invalid-affected-row-assertion");
|
|
4822
|
+
}
|
|
4823
|
+
if (node.directives?.some((directive) => ["include", "skip"].includes(directive.name.value))) {
|
|
4824
|
+
fail2("an @assertAffectedRows root cannot use @include or @skip", "invalid-affected-row-assertion");
|
|
4825
|
+
}
|
|
4826
|
+
const args = assertions[0].arguments;
|
|
4827
|
+
const arg = args?.[0];
|
|
4828
|
+
if (args?.length !== 1 || arg?.name.value !== "exactly" || arg.value.kind !== graphql.Kind.INT) {
|
|
4829
|
+
fail2("@assertAffectedRows(exactly:) requires a document-authored nonnegative Int literal", "invalid-affected-row-assertion");
|
|
4830
|
+
}
|
|
4831
|
+
const count = Number(arg.value.value);
|
|
4832
|
+
if (!Number.isInteger(count) || count < 0 || count > 2147483647) {
|
|
4833
|
+
fail2("@assertAffectedRows(exactly:) must be between 0 and 2147483647", "invalid-affected-row-assertion");
|
|
4834
|
+
}
|
|
4835
|
+
return count;
|
|
4836
|
+
}
|
|
3392
4837
|
parseRootDirectives(node) {
|
|
3393
4838
|
let includeIf;
|
|
3394
4839
|
let omit = false;
|
|
3395
4840
|
for (const directive of node.directives ?? []) {
|
|
3396
4841
|
const name = directive.name.value;
|
|
4842
|
+
if (name === "assertAffectedRows") continue;
|
|
4843
|
+
if (SOURCE_DIRECTIVES.has(name)) continue;
|
|
3397
4844
|
if (name !== "include" && name !== "skip") {
|
|
3398
|
-
|
|
4845
|
+
fail2(`directive @${name} is not supported on fields`, "unknown-directive", ["include", "skip"]);
|
|
3399
4846
|
}
|
|
3400
4847
|
const ifArg = directive.arguments?.find((a) => a.name.value === "if");
|
|
3401
|
-
if (!ifArg)
|
|
4848
|
+
if (!ifArg) fail2(`@${name} needs an if: argument`, "invalid-directive");
|
|
3402
4849
|
if (ifArg.value.kind === graphql.Kind.BOOLEAN) {
|
|
3403
4850
|
const keep = name === "include" ? ifArg.value.value : !ifArg.value.value;
|
|
3404
4851
|
if (!keep) omit = true;
|
|
3405
4852
|
continue;
|
|
3406
4853
|
}
|
|
3407
|
-
if (ifArg.value.kind !== graphql.Kind.VARIABLE)
|
|
3408
|
-
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");
|
|
3409
4856
|
includeIf = { variable: ifArg.value.name.value, negated: name === "skip" };
|
|
3410
4857
|
}
|
|
3411
4858
|
return omit ? { omit: true } : { includeIf, omit: false };
|
|
@@ -3414,12 +4861,12 @@ var OperationLowerer = class {
|
|
|
3414
4861
|
const name = node.name.value;
|
|
3415
4862
|
const alias = node.alias?.value ?? name;
|
|
3416
4863
|
const rootInfo = this.ctx.queryRoots.get(name);
|
|
3417
|
-
if (!rootInfo)
|
|
4864
|
+
if (!rootInfo) fail2(`unknown query root '${name}'`, "unknown-root", [...this.ctx.queryRoots.keys()]);
|
|
3418
4865
|
const { table } = rootInfo;
|
|
3419
4866
|
const args = argsByName(node);
|
|
3420
4867
|
if (rootInfo.kind === "select") {
|
|
3421
4868
|
assertKnownArgs(node, ["where", "orderBy", "distinctOn", "limit", "offset"]);
|
|
3422
|
-
if (!node.selectionSet)
|
|
4869
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
3423
4870
|
const where = args.get("where");
|
|
3424
4871
|
const orderBy = args.get("orderBy");
|
|
3425
4872
|
const distinctOn = args.get("distinctOn");
|
|
@@ -3438,12 +4885,15 @@ var OperationLowerer = class {
|
|
|
3438
4885
|
};
|
|
3439
4886
|
}
|
|
3440
4887
|
if (rootInfo.kind === "byPk") {
|
|
3441
|
-
if (!node.selectionSet)
|
|
4888
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
3442
4889
|
const pk = this.lowerPkArgs(node, table);
|
|
3443
4890
|
return { kind: "byPk", alias, table, selection: this.lowerSelection(node.selectionSet, table), pk, where: CONST_TRUE };
|
|
3444
4891
|
}
|
|
4892
|
+
if (rootInfo.kind === "aggregateGroups") {
|
|
4893
|
+
return this.lowerAggregateGroupsRoot(node, { alias, table });
|
|
4894
|
+
}
|
|
3445
4895
|
assertKnownArgs(node, ["where"]);
|
|
3446
|
-
if (!node.selectionSet)
|
|
4896
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
3447
4897
|
const whereArg = args.get("where");
|
|
3448
4898
|
const aggregate = this.lowerAggregateSelection(node.selectionSet, table);
|
|
3449
4899
|
return {
|
|
@@ -3454,6 +4904,108 @@ var OperationLowerer = class {
|
|
|
3454
4904
|
...aggregate
|
|
3455
4905
|
};
|
|
3456
4906
|
}
|
|
4907
|
+
/** `<t>AggregateGroups(where:, groupBy:, limit:, offset:)` → an aggregateGroups root. */
|
|
4908
|
+
lowerAggregateGroupsRoot(node, { alias, table }) {
|
|
4909
|
+
assertKnownArgs(node, ["where", "groupBy", "limit", "offset"]);
|
|
4910
|
+
if (!node.selectionSet) fail2(`'${node.name.value}' needs a selection set`, "invalid-selection");
|
|
4911
|
+
const args = argsByName(node);
|
|
4912
|
+
const groupByArg = args.get("groupBy");
|
|
4913
|
+
if (!groupByArg) fail2(`'${node.name.value}' requires a 'groupBy' argument`, "missing-group-by");
|
|
4914
|
+
const whereArg = args.get("where");
|
|
4915
|
+
const groupBy = this.lowerGroupBy(groupByArg.value, table);
|
|
4916
|
+
const aggregate = this.lowerGroupedAggregateSelection(node.selectionSet, table, groupBy);
|
|
4917
|
+
const offset = this.lowerOffset(args.get("offset")?.value);
|
|
4918
|
+
return {
|
|
4919
|
+
kind: "aggregateGroups",
|
|
4920
|
+
alias,
|
|
4921
|
+
table,
|
|
4922
|
+
where: whereArg ? this.lowerBoolExp(whereArg.value, table) : CONST_TRUE,
|
|
4923
|
+
groupBy,
|
|
4924
|
+
key: aggregate.key,
|
|
4925
|
+
...aggregate.count ? { count: aggregate.count } : {},
|
|
4926
|
+
...aggregate.sum ? { sum: aggregate.sum } : {},
|
|
4927
|
+
...aggregate.avg ? { avg: aggregate.avg } : {},
|
|
4928
|
+
limit: this.lowerGroupLimit(args.get("limit")?.value),
|
|
4929
|
+
...offset ? { offset } : {}
|
|
4930
|
+
};
|
|
4931
|
+
}
|
|
4932
|
+
/**
|
|
4933
|
+
* `groupBy: [{ column: startedAt, bucket: MONTH }, …]` → GroupByKeyIr[].
|
|
4934
|
+
*
|
|
4935
|
+
* Document-authored only: a whole-argument variable is rejected rather than
|
|
4936
|
+
* becoming a §4f slot. Group keys choose which expressions reach GROUP BY,
|
|
4937
|
+
* and dynamic-input.ts refuses aggregation shapes from client input on the
|
|
4938
|
+
* same reasoning — `where` stays dynamic, which is what runtime filtering
|
|
4939
|
+
* actually needs.
|
|
4940
|
+
*/
|
|
4941
|
+
lowerGroupBy(node, table) {
|
|
4942
|
+
if (node.kind === graphql.Kind.VARIABLE) {
|
|
4943
|
+
fail2(
|
|
4944
|
+
`groupBy on '${table}' must be written in the document \u2014 it cannot come from a variable`,
|
|
4945
|
+
"dynamic-group-by"
|
|
4946
|
+
);
|
|
4947
|
+
}
|
|
4948
|
+
const items = node.kind === graphql.Kind.LIST ? node.values : [node];
|
|
4949
|
+
if (items.length === 0) fail2(`groupBy on '${table}' needs at least one key`, "invalid-group-by");
|
|
4950
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4951
|
+
return items.map((item) => {
|
|
4952
|
+
if (item.kind !== graphql.Kind.OBJECT) {
|
|
4953
|
+
fail2(`groupBy entries on '${table}' must be objects like { column: foo }`, "invalid-group-by");
|
|
4954
|
+
}
|
|
4955
|
+
const fields = new Map(item.fields.map((field) => [field.name.value, field.value]));
|
|
4956
|
+
for (const key of fields.keys()) {
|
|
4957
|
+
if (key !== "column" && key !== "bucket") {
|
|
4958
|
+
fail2(`unknown groupBy field '${key}' on '${table}'`, "invalid-group-by", ["column", "bucket"]);
|
|
4959
|
+
}
|
|
4960
|
+
}
|
|
4961
|
+
const columnNode = fields.get("column");
|
|
4962
|
+
if (!columnNode) fail2(`groupBy entries on '${table}' need a 'column'`, "invalid-group-by");
|
|
4963
|
+
if (columnNode.kind !== graphql.Kind.ENUM) {
|
|
4964
|
+
fail2(`groupBy column on '${table}' must be a group-by column enum value`, "invalid-group-by");
|
|
4965
|
+
}
|
|
4966
|
+
const column = this.column(table, columnNode.value);
|
|
4967
|
+
if (!column) {
|
|
4968
|
+
fail2(`unknown groupBy column '${columnNode.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
|
|
4969
|
+
}
|
|
4970
|
+
if (column.type === "jsonb") {
|
|
4971
|
+
fail2(`groupBy column '${table}.${columnNode.value}' is jsonb and cannot be a group key`, "invalid-group-by");
|
|
4972
|
+
}
|
|
4973
|
+
const bucketNode = fields.get("bucket");
|
|
4974
|
+
let bucket;
|
|
4975
|
+
if (bucketNode) {
|
|
4976
|
+
if (bucketNode.kind !== graphql.Kind.ENUM) {
|
|
4977
|
+
fail2(`groupBy bucket on '${table}.${columnNode.value}' must be a DateBucket enum value`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
|
|
4978
|
+
}
|
|
4979
|
+
bucket = DATE_BUCKET_BY_ENUM[bucketNode.value];
|
|
4980
|
+
if (!bucket) {
|
|
4981
|
+
fail2(`unknown DateBucket '${bucketNode.value}'`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
|
|
4982
|
+
}
|
|
4983
|
+
if (typeof column.type !== "string" || !BUCKETABLE_TYPES.has(column.type)) {
|
|
4984
|
+
fail2(
|
|
4985
|
+
`groupBy bucket is only valid on date/timestamptz columns \u2014 '${table}.${columnNode.value}' is ${typeof column.type === "string" ? column.type : "an enum"}`,
|
|
4986
|
+
"invalid-group-by"
|
|
4987
|
+
);
|
|
4988
|
+
}
|
|
4989
|
+
}
|
|
4990
|
+
if (seen.has(columnNode.value)) {
|
|
4991
|
+
fail2(`duplicate groupBy column '${columnNode.value}' on '${table}'`, "invalid-group-by");
|
|
4992
|
+
}
|
|
4993
|
+
seen.add(columnNode.value);
|
|
4994
|
+
return bucket ? { column: columnNode.value, bucket } : { column: columnNode.value };
|
|
4995
|
+
});
|
|
4996
|
+
}
|
|
4997
|
+
/** Like lowerLimit, but bounded by MAX_GROUPS and defaulting to it. */
|
|
4998
|
+
lowerGroupLimit(node) {
|
|
4999
|
+
const base = { default: MAX_GROUPS, max: MAX_GROUPS };
|
|
5000
|
+
if (!node) return base;
|
|
5001
|
+
if (node.kind === graphql.Kind.VARIABLE) return { ...base, variable: node.name.value };
|
|
5002
|
+
if (node.kind === graphql.Kind.INT) {
|
|
5003
|
+
const value = Number(node.value);
|
|
5004
|
+
if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
|
|
5005
|
+
return { ...base, value: Math.min(value, MAX_GROUPS) };
|
|
5006
|
+
}
|
|
5007
|
+
fail2("limit must be an integer literal or a variable", "invalid-limit");
|
|
5008
|
+
}
|
|
3457
5009
|
/** byPk-style pk arguments: one argument per primary-key column, exactly. */
|
|
3458
5010
|
lowerPkArgs(node, table) {
|
|
3459
5011
|
const meta = this.meta(table);
|
|
@@ -3461,7 +5013,7 @@ var OperationLowerer = class {
|
|
|
3461
5013
|
assertKnownArgs(node, meta.primaryKey);
|
|
3462
5014
|
return meta.primaryKey.map((column) => {
|
|
3463
5015
|
const arg = args.get(column);
|
|
3464
|
-
if (!arg)
|
|
5016
|
+
if (!arg) fail2(`'${node.name.value}' is missing primary-key argument '${column}'`, "missing-pk-argument", meta.primaryKey);
|
|
3465
5017
|
return { column, value: this.lowerValue(arg.value) };
|
|
3466
5018
|
});
|
|
3467
5019
|
}
|
|
@@ -3472,19 +5024,22 @@ var OperationLowerer = class {
|
|
|
3472
5024
|
const name = node.name.value;
|
|
3473
5025
|
const alias = node.alias?.value ?? name;
|
|
3474
5026
|
const rootInfo = this.ctx.mutationRoots.get(name);
|
|
3475
|
-
if (!rootInfo)
|
|
5027
|
+
if (!rootInfo) fail2(`unknown mutation root '${name}'`, "unknown-root", [...this.ctx.mutationRoots.keys()]);
|
|
3476
5028
|
if (rootInfo.op === "cowOverride" || rootInfo.op === "cowRevert") {
|
|
3477
5029
|
return this.lowerCowRoot(node, rootInfo);
|
|
3478
5030
|
}
|
|
5031
|
+
if (rootInfo.op === "resolvedWrite") {
|
|
5032
|
+
return this.lowerResolvedRoot(node, rootInfo);
|
|
5033
|
+
}
|
|
3479
5034
|
const { op, table } = rootInfo;
|
|
3480
5035
|
const args = argsByName(node);
|
|
3481
|
-
if (!node.selectionSet)
|
|
5036
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
3482
5037
|
const base = { kind: "mutation", op, alias, table, pinnedColumns: [], affectedRows: false };
|
|
3483
5038
|
switch (op) {
|
|
3484
5039
|
case "insert": {
|
|
3485
5040
|
assertKnownArgs(node, ["objects", "onConflict"]);
|
|
3486
5041
|
const objects = args.get("objects");
|
|
3487
|
-
if (!objects)
|
|
5042
|
+
if (!objects) fail2(`'${name}' needs objects:`, "missing-argument");
|
|
3488
5043
|
const response = this.lowerMutationResponse(node.selectionSet, table);
|
|
3489
5044
|
return {
|
|
3490
5045
|
...base,
|
|
@@ -3496,7 +5051,7 @@ var OperationLowerer = class {
|
|
|
3496
5051
|
case "insertOne": {
|
|
3497
5052
|
assertKnownArgs(node, ["object", "onConflict"]);
|
|
3498
5053
|
const object = args.get("object");
|
|
3499
|
-
if (!object)
|
|
5054
|
+
if (!object) fail2(`'${name}' needs object:`, "missing-argument");
|
|
3500
5055
|
return {
|
|
3501
5056
|
...base,
|
|
3502
5057
|
objects: this.lowerInsertObjects(object.value, table, { list: false }),
|
|
@@ -3507,7 +5062,7 @@ var OperationLowerer = class {
|
|
|
3507
5062
|
case "update": {
|
|
3508
5063
|
assertKnownArgs(node, ["where", "_set", "_append", "_inc"]);
|
|
3509
5064
|
const where = args.get("where");
|
|
3510
|
-
if (!where)
|
|
5065
|
+
if (!where) fail2(`'${name}' needs where: (bulk updates without a filter are not allowed)`, "missing-argument");
|
|
3511
5066
|
return {
|
|
3512
5067
|
...base,
|
|
3513
5068
|
...this.lowerMutationResponse(node.selectionSet, table),
|
|
@@ -3518,7 +5073,7 @@ var OperationLowerer = class {
|
|
|
3518
5073
|
case "updateByPk": {
|
|
3519
5074
|
assertKnownArgs(node, ["pkColumns", "_set", "_append", "_inc"]);
|
|
3520
5075
|
const pkColumns = args.get("pkColumns");
|
|
3521
|
-
if (!pkColumns)
|
|
5076
|
+
if (!pkColumns) fail2(`'${name}' needs pkColumns:`, "missing-argument");
|
|
3522
5077
|
return {
|
|
3523
5078
|
...base,
|
|
3524
5079
|
pk: this.lowerPkColumnsArg(pkColumns.value, table),
|
|
@@ -3529,7 +5084,7 @@ var OperationLowerer = class {
|
|
|
3529
5084
|
case "delete": {
|
|
3530
5085
|
assertKnownArgs(node, ["where"]);
|
|
3531
5086
|
const where = args.get("where");
|
|
3532
|
-
if (!where)
|
|
5087
|
+
if (!where) fail2(`'${name}' needs where: (bulk deletes without a filter are not allowed)`, "missing-argument");
|
|
3533
5088
|
return {
|
|
3534
5089
|
...base,
|
|
3535
5090
|
...this.lowerMutationResponse(node.selectionSet, table),
|
|
@@ -3556,11 +5111,11 @@ var OperationLowerer = class {
|
|
|
3556
5111
|
const name = node.name.value;
|
|
3557
5112
|
const alias = node.alias?.value ?? name;
|
|
3558
5113
|
const { table } = rootInfo;
|
|
3559
|
-
if (!node.selectionSet)
|
|
5114
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
3560
5115
|
const meta = this.meta(table);
|
|
3561
5116
|
const args = argsByName(node);
|
|
3562
5117
|
const pkArg = args.get(rootInfo.pk);
|
|
3563
|
-
if (!pkArg)
|
|
5118
|
+
if (!pkArg) fail2(`'${name}' is missing primary-key argument '${rootInfo.pk}'`, "missing-pk-argument", [rootInfo.pk]);
|
|
3564
5119
|
const pk = [{ column: rootInfo.pk, value: this.lowerValue(pkArg.value) }];
|
|
3565
5120
|
const base = {
|
|
3566
5121
|
kind: "mutation",
|
|
@@ -3581,24 +5136,78 @@ var OperationLowerer = class {
|
|
|
3581
5136
|
const entries = rootInfo.set.map((column) => {
|
|
3582
5137
|
const arg = args.get(column);
|
|
3583
5138
|
if (!arg) {
|
|
3584
|
-
|
|
5139
|
+
fail2(
|
|
3585
5140
|
`'${name}' is missing set argument '${column}' \u2014 a cow override writes every declared set column (pass null to clear a nullable one)`,
|
|
3586
5141
|
"missing-argument",
|
|
3587
5142
|
[...rootInfo.set]
|
|
3588
5143
|
);
|
|
3589
5144
|
}
|
|
3590
5145
|
if (!meta.settable.includes(column)) {
|
|
3591
|
-
|
|
5146
|
+
fail2(`column '${table}.${column}' is not updatable (pinned by the write policy or deprecated)`, "column-not-settable", meta.settable);
|
|
3592
5147
|
}
|
|
3593
5148
|
return { column, value: this.lowerValue(arg.value) };
|
|
3594
5149
|
});
|
|
3595
5150
|
return { ...base, set: { entries }, cowCopyColumns: [...rootInfo.copyColumns] };
|
|
3596
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
|
+
}
|
|
3597
5206
|
lowerMutationResponse(selectionSet, table) {
|
|
3598
5207
|
let affectedRows = false;
|
|
3599
5208
|
let returning = null;
|
|
3600
5209
|
for (const node of selectionSet.selections) {
|
|
3601
|
-
if (node.kind !== graphql.Kind.FIELD)
|
|
5210
|
+
if (node.kind !== graphql.Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
3602
5211
|
const name = node.name.value;
|
|
3603
5212
|
if (name === "__typename") continue;
|
|
3604
5213
|
if (name === "affectedRows") {
|
|
@@ -3606,34 +5215,34 @@ var OperationLowerer = class {
|
|
|
3606
5215
|
continue;
|
|
3607
5216
|
}
|
|
3608
5217
|
if (name === "returning") {
|
|
3609
|
-
if (!node.selectionSet)
|
|
5218
|
+
if (!node.selectionSet) fail2("'returning' needs a selection set", "invalid-selection");
|
|
3610
5219
|
returning = this.lowerSelection(node.selectionSet, table);
|
|
3611
5220
|
continue;
|
|
3612
5221
|
}
|
|
3613
|
-
|
|
5222
|
+
fail2(`unknown mutation-response field '${name}'`, "invalid-selection", ["affectedRows", "returning"]);
|
|
3614
5223
|
}
|
|
3615
5224
|
return { affectedRows, returning };
|
|
3616
5225
|
}
|
|
3617
5226
|
lowerInsertObjects(node, table, { list }) {
|
|
3618
5227
|
if (node.kind === graphql.Kind.VARIABLE) return { slot: this.slot(node.name.value, "insertInput", table) };
|
|
3619
5228
|
if (list) {
|
|
3620
|
-
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");
|
|
3621
5230
|
return { rows: node.values.map((item) => this.lowerInsertRow(item, table)) };
|
|
3622
5231
|
}
|
|
3623
5232
|
return { rows: [this.lowerInsertRow(node, table)] };
|
|
3624
5233
|
}
|
|
3625
5234
|
lowerInsertRow(node, table) {
|
|
3626
|
-
if (node.kind !== graphql.Kind.OBJECT)
|
|
5235
|
+
if (node.kind !== graphql.Kind.OBJECT) fail2(`insert rows on '${table}' must be objects`, "invalid-insert");
|
|
3627
5236
|
const meta = this.meta(table);
|
|
3628
5237
|
return node.fields.map((field) => {
|
|
3629
5238
|
const key = field.name.value;
|
|
3630
5239
|
if (this.link(table, key)) {
|
|
3631
|
-
|
|
5240
|
+
fail2(`nested inserts are not supported in v2.0 \u2014 '${key}' is a link on '${table}'`, "nested-insert-unsupported");
|
|
3632
5241
|
}
|
|
3633
5242
|
if (!meta.insertable.includes(key)) {
|
|
3634
5243
|
const column = this.column(table, key);
|
|
3635
|
-
if (column)
|
|
3636
|
-
|
|
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);
|
|
3637
5246
|
}
|
|
3638
5247
|
return { column: key, value: this.lowerValue(field.value) };
|
|
3639
5248
|
});
|
|
@@ -3642,32 +5251,32 @@ var OperationLowerer = class {
|
|
|
3642
5251
|
if (!arg) return {};
|
|
3643
5252
|
const node = arg.value;
|
|
3644
5253
|
if (node.kind === graphql.Kind.VARIABLE) return { onConflict: { slot: this.slot(node.name.value, "onConflict", table) } };
|
|
3645
|
-
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");
|
|
3646
5255
|
const meta = this.meta(table);
|
|
3647
5256
|
let constraint;
|
|
3648
5257
|
const updateColumns = [];
|
|
3649
5258
|
for (const field of node.fields) {
|
|
3650
5259
|
if (field.name.value === "constraint") {
|
|
3651
|
-
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");
|
|
3652
5261
|
constraint = field.value.value;
|
|
3653
5262
|
continue;
|
|
3654
5263
|
}
|
|
3655
5264
|
if (field.name.value === "updateColumns") {
|
|
3656
5265
|
const items = field.value.kind === graphql.Kind.LIST ? field.value.values : [field.value];
|
|
3657
5266
|
for (const item of items) {
|
|
3658
|
-
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");
|
|
3659
5268
|
if (!meta.settable.includes(item.value)) {
|
|
3660
|
-
|
|
5269
|
+
fail2(`onConflict.updateColumns: '${item.value}' is not updatable on '${table}'`, "invalid-on-conflict", meta.settable);
|
|
3661
5270
|
}
|
|
3662
5271
|
updateColumns.push(item.value);
|
|
3663
5272
|
}
|
|
3664
5273
|
continue;
|
|
3665
5274
|
}
|
|
3666
|
-
|
|
5275
|
+
fail2(`unknown onConflict key '${field.name.value}'`, "invalid-on-conflict", ["constraint", "updateColumns"]);
|
|
3667
5276
|
}
|
|
3668
|
-
if (!constraint)
|
|
5277
|
+
if (!constraint) fail2("onConflict needs a constraint", "invalid-on-conflict", meta.constraints.map((c) => c.name));
|
|
3669
5278
|
if (!meta.constraints.some((c) => c.name === constraint)) {
|
|
3670
|
-
|
|
5279
|
+
fail2(`unknown constraint '${constraint}' on '${table}'`, "invalid-on-conflict", meta.constraints.map((c) => c.name));
|
|
3671
5280
|
}
|
|
3672
5281
|
return { onConflict: { constraint, updateColumns } };
|
|
3673
5282
|
}
|
|
@@ -3685,36 +5294,36 @@ var OperationLowerer = class {
|
|
|
3685
5294
|
const inc = args.get("_inc");
|
|
3686
5295
|
if (inc) {
|
|
3687
5296
|
if (inc.value.kind === graphql.Kind.VARIABLE) {
|
|
3688
|
-
|
|
5297
|
+
fail2("_inc has no dynamic input type \u2014 inline the columns with variable leaf values", "invalid-inc");
|
|
3689
5298
|
}
|
|
3690
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);
|
|
3691
5300
|
result.inc = { entries: this.lowerColumnEntries(inc.value, table, numeric4, "_inc") };
|
|
3692
5301
|
}
|
|
3693
5302
|
if (!result.set && !result.append && !result.inc) {
|
|
3694
|
-
|
|
5303
|
+
fail2(`an update on '${table}' needs at least one of _set/_append/_inc`, "missing-argument");
|
|
3695
5304
|
}
|
|
3696
5305
|
return result;
|
|
3697
5306
|
}
|
|
3698
5307
|
lowerColumnEntries(node, table, whitelist, argName) {
|
|
3699
|
-
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");
|
|
3700
5309
|
return node.fields.map((field) => {
|
|
3701
5310
|
const key = field.name.value;
|
|
3702
5311
|
if (!whitelist.includes(key)) {
|
|
3703
|
-
|
|
5312
|
+
fail2(`${argName}: column '${key}' is not allowed on '${table}'`, "column-not-settable", [...whitelist]);
|
|
3704
5313
|
}
|
|
3705
5314
|
return { column: key, value: this.lowerValue(field.value) };
|
|
3706
5315
|
});
|
|
3707
5316
|
}
|
|
3708
5317
|
lowerPkColumnsArg(node, table) {
|
|
3709
|
-
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");
|
|
3710
5319
|
const meta = this.meta(table);
|
|
3711
5320
|
const provided = new Map(node.fields.map((field) => [field.name.value, field.value]));
|
|
3712
5321
|
for (const key of provided.keys()) {
|
|
3713
|
-
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);
|
|
3714
5323
|
}
|
|
3715
5324
|
return meta.primaryKey.map((column) => {
|
|
3716
5325
|
const value = provided.get(column);
|
|
3717
|
-
if (!value)
|
|
5326
|
+
if (!value) fail2(`pkColumns is missing '${column}'`, "invalid-pk-columns", meta.primaryKey);
|
|
3718
5327
|
return { column, value: this.lowerValue(value) };
|
|
3719
5328
|
});
|
|
3720
5329
|
}
|
|
@@ -3723,6 +5332,19 @@ function lowerOperation({ restricted, ctx }) {
|
|
|
3723
5332
|
const lowerer = new OperationLowerer(ctx, restricted);
|
|
3724
5333
|
return lowerer.lowerRoots(restricted.definition, restricted.kind);
|
|
3725
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
|
+
}
|
|
3726
5348
|
function parseSdl(sdl) {
|
|
3727
5349
|
return graphql.buildSchema(sdl);
|
|
3728
5350
|
}
|
|
@@ -3963,7 +5585,7 @@ function compileNamespace({ canonical, sdl, documents }) {
|
|
|
3963
5585
|
const loweredRoots = lowerOperation({ restricted: { ...restricted, definition: flattened }, ctx });
|
|
3964
5586
|
const { roots, tableSet } = injectPolicies({ roots: loweredRoots, ctx });
|
|
3965
5587
|
operations[restricted.name] = {
|
|
3966
|
-
irVersion: IR_VERSION,
|
|
5588
|
+
irVersion: roots.some((root) => root.kind === "mutation" && root.assertAffectedRows !== void 0) ? IR_VERSION : LEGACY_IR_VERSION,
|
|
3967
5589
|
name: restricted.name,
|
|
3968
5590
|
opHash: computeOpHash({ definition: flattened, schemaHash }),
|
|
3969
5591
|
kind: restricted.kind,
|
|
@@ -3990,7 +5612,7 @@ function compileNamespace({ canonical, sdl, documents }) {
|
|
|
3990
5612
|
}
|
|
3991
5613
|
if (diagnostics.some((d) => d.severity === "error")) return { ok: false, diagnostics };
|
|
3992
5614
|
const artifact = {
|
|
3993
|
-
irVersion:
|
|
5615
|
+
irVersion: requiredArtifactIrVersion(Object.values(operations)),
|
|
3994
5616
|
namespace: ctx.namespace,
|
|
3995
5617
|
schemaHash,
|
|
3996
5618
|
opsHash: computeOpsHash(Object.values(operations).map((op) => op.opHash)),
|
|
@@ -4099,15 +5721,6 @@ function columnWireType(column) {
|
|
|
4099
5721
|
const notNull = column.primary_key === true || column.nullable === false;
|
|
4100
5722
|
return notNull || base === "unknown" ? base : `${base} | null`;
|
|
4101
5723
|
}
|
|
4102
|
-
|
|
4103
|
-
// src/compiler/sql.ts
|
|
4104
|
-
var CLAIMS_SETTING = "request.jwt.claims";
|
|
4105
|
-
var CAPABILITIES_CLAIM_KEY = "capabilities";
|
|
4106
|
-
var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
4107
|
-
var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
4108
|
-
var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
|
|
4109
|
-
var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
|
|
4110
|
-
var capabilityExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
4111
5724
|
var identifierSchema = zod.z.string().max(MAX_IDENTIFIER_LENGTH).regex(IDENTIFIER_PATTERN, "must be a lowercase identifier (a-z, 0-9, _)");
|
|
4112
5725
|
var namespaceSchema = zod.z.string().regex(NAMESPACE_PATTERN, "must be lowercase, start with a letter, and be at most 41 characters");
|
|
4113
5726
|
var tableReferenceSchema = zod.z.string().refine(
|
|
@@ -4190,12 +5803,12 @@ function castParts(cast) {
|
|
|
4190
5803
|
|
|
4191
5804
|
// src/predicate/ast.ts
|
|
4192
5805
|
var COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
|
|
4193
|
-
function walkPredicate(predicate,
|
|
4194
|
-
|
|
5806
|
+
function walkPredicate(predicate, visit2) {
|
|
5807
|
+
visit2(predicate);
|
|
4195
5808
|
if (predicate.kind === "and" || predicate.kind === "or") {
|
|
4196
|
-
for (const operand of predicate.operands) walkPredicate(operand,
|
|
5809
|
+
for (const operand of predicate.operands) walkPredicate(operand, visit2);
|
|
4197
5810
|
}
|
|
4198
|
-
if (predicate.kind === "not") walkPredicate(predicate.operand,
|
|
5811
|
+
if (predicate.kind === "not") walkPredicate(predicate.operand, visit2);
|
|
4199
5812
|
}
|
|
4200
5813
|
function predicateOperands(predicate) {
|
|
4201
5814
|
const operands = [];
|
|
@@ -4224,7 +5837,7 @@ var OPERATOR_PATTERN2 = /^(!=|<=|>=|<>|=|<|>)/;
|
|
|
4224
5837
|
var IDENTIFIER_START2 = /[a-z_]/;
|
|
4225
5838
|
var IDENTIFIER_CHAR2 = /[a-z0-9_.-]/;
|
|
4226
5839
|
var NUMBER_PATTERN2 = /^-?\d+(\.\d+)?/;
|
|
4227
|
-
function
|
|
5840
|
+
function tokenize3(source) {
|
|
4228
5841
|
const tokens = [];
|
|
4229
5842
|
let position = 0;
|
|
4230
5843
|
while (position < source.length) {
|
|
@@ -4298,7 +5911,7 @@ function readString2(source, start) {
|
|
|
4298
5911
|
}
|
|
4299
5912
|
throw new PredicateParseError("unterminated string literal", start);
|
|
4300
5913
|
}
|
|
4301
|
-
var
|
|
5914
|
+
var Parser3 = class {
|
|
4302
5915
|
constructor(tokens) {
|
|
4303
5916
|
this.tokens = tokens;
|
|
4304
5917
|
}
|
|
@@ -4430,7 +6043,7 @@ var Parser2 = class {
|
|
|
4430
6043
|
function parsePredicate(source) {
|
|
4431
6044
|
const trimmed = source.trim();
|
|
4432
6045
|
if (trimmed.length === 0) throw new PredicateParseError("empty predicate", 0);
|
|
4433
|
-
return { source: trimmed, ast: new
|
|
6046
|
+
return { source: trimmed, ast: new Parser3(tokenize3(trimmed)).parse() };
|
|
4434
6047
|
}
|
|
4435
6048
|
|
|
4436
6049
|
// src/schema/predicates.ts
|
|
@@ -5152,6 +6765,88 @@ function snapshotTableNames(snapshot, schema) {
|
|
|
5152
6765
|
return Object.values(tables).filter((entry) => entry.schema === schema).map((entry) => entry.name);
|
|
5153
6766
|
}
|
|
5154
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
|
+
|
|
5155
6850
|
// src/engine/canonical2.ts
|
|
5156
6851
|
var FACTORIES2 = {
|
|
5157
6852
|
uuid: (name) => pgCore.uuid(name),
|
|
@@ -5170,7 +6865,7 @@ var FACTORIES2 = {
|
|
|
5170
6865
|
var SCHEMA_IMPORT_KEY2 = "$schema";
|
|
5171
6866
|
var enumImportKey = (name) => `$enum_${name}`;
|
|
5172
6867
|
var columnTablesV2 = (definition) => definition.tables.filter((table) => !table.source);
|
|
5173
|
-
function buildDrizzleEntitiesV2(definition) {
|
|
6868
|
+
function buildDrizzleEntitiesV2(definition, options = {}) {
|
|
5174
6869
|
const schemaName = ormSchemaName(definition.namespace);
|
|
5175
6870
|
const schema = pgCore.pgSchema(schemaName);
|
|
5176
6871
|
const imports = { [SCHEMA_IMPORT_KEY2]: schema };
|
|
@@ -5185,11 +6880,19 @@ function buildDrizzleEntitiesV2(definition) {
|
|
|
5185
6880
|
}
|
|
5186
6881
|
const byName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
5187
6882
|
for (const table of sortByEnforcedLinks2(columnTablesV2(definition))) {
|
|
5188
|
-
const drizzleTable = buildTable2(schema, table, built, enums, byName);
|
|
6883
|
+
const drizzleTable = buildTable2(schema, table, built, enums, byName, options.introspection ?? false);
|
|
5189
6884
|
built.set(table.table, drizzleTable);
|
|
5190
6885
|
imports[table.table] = drizzleTable;
|
|
5191
6886
|
tables[table.table] = drizzleTable;
|
|
5192
6887
|
}
|
|
6888
|
+
if (options.introspection) {
|
|
6889
|
+
for (const table of definition.tables.filter((table2) => table2.source)) {
|
|
6890
|
+
const columns = Object.fromEntries(
|
|
6891
|
+
table.columns.map((column) => [column.columnName, buildViewColumnV2({ view: table.table, column, enums })])
|
|
6892
|
+
);
|
|
6893
|
+
imports[table.table] = schema.view(table.table, columns).existing();
|
|
6894
|
+
}
|
|
6895
|
+
}
|
|
5193
6896
|
for (const view of definition.views ?? []) {
|
|
5194
6897
|
const columns = Object.fromEntries(
|
|
5195
6898
|
view.columns.map((column) => [column.columnName, buildViewColumnV2({ view: view.view, column, enums })])
|
|
@@ -5433,9 +7136,12 @@ function replaceQualifiedRelation(sql4, sourceSchema, sourceName, boundSchema, b
|
|
|
5433
7136
|
function generateSnapshotV2(definition, previousId3) {
|
|
5434
7137
|
const { imports, schemaName } = buildDrizzleEntitiesV2(definition);
|
|
5435
7138
|
const snapshot = api.generateDrizzleJson(imports, previousId3, [schemaName]);
|
|
5436
|
-
if ((definition.views?.length ?? 0) > 0) {
|
|
7139
|
+
if ((definition.views?.length ?? 0) > 0 || definition.tables.some((table) => table.foreignKeys?.length)) {
|
|
5437
7140
|
const extended = snapshot;
|
|
7141
|
+
const refreshFunctions = materializedRefreshFunctions(definition);
|
|
5438
7142
|
extended._sequenceOrm = {
|
|
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) } : {},
|
|
5439
7145
|
viewDependencies: Object.fromEntries(
|
|
5440
7146
|
(definition.views ?? []).map((view) => [view.view, [...view.dependencies].sort()])
|
|
5441
7147
|
)
|
|
@@ -5453,7 +7159,7 @@ function buildViewColumnV2({
|
|
|
5453
7159
|
if (!column.nullable) builder = builder.notNull();
|
|
5454
7160
|
return builder;
|
|
5455
7161
|
}
|
|
5456
|
-
function buildTable2(schema, table, built, enums, byName) {
|
|
7162
|
+
function buildTable2(schema, table, built, enums, byName, introspection) {
|
|
5457
7163
|
const primaryKeyColumns2 = table.primaryKey ?? [];
|
|
5458
7164
|
if (primaryKeyColumns2.length === 0) {
|
|
5459
7165
|
throw new OrmDefinitionError(`table '${table.table}' declares no primaryKey`);
|
|
@@ -5469,13 +7175,26 @@ function buildTable2(schema, table, built, enums, byName) {
|
|
|
5469
7175
|
const columns = Object.fromEntries(
|
|
5470
7176
|
table.columns.map((column) => [
|
|
5471
7177
|
column.columnName,
|
|
5472
|
-
buildColumnV2({ table: table.table, column, singlePrimaryKey, compositeMembers, enums })
|
|
7178
|
+
buildColumnV2({ table: table.table, column, singlePrimaryKey, compositeMembers, enums, introspection })
|
|
5473
7179
|
])
|
|
5474
7180
|
);
|
|
5475
7181
|
const drizzleTable = schema.table(table.table, columns, (self) => [
|
|
5476
7182
|
...buildCompositePrimaryKey(table, primaryKeyColumns2, self),
|
|
5477
7183
|
...buildIndexes2(table, self),
|
|
5478
|
-
...
|
|
7184
|
+
...(table.checks ?? []).map((check) => pgCore.check(check.name, drizzleOrm.sql.raw(compileCheckExpression(check.expression, table.columns)))),
|
|
7185
|
+
...buildForeignKeys2(table, self, built, byName),
|
|
7186
|
+
...(table.foreignKeys ?? []).map((fk) => {
|
|
7187
|
+
const target = fk.target === table.table ? self : built.get(fk.target);
|
|
7188
|
+
if (!target) throw new OrmDefinitionError(`foreign key '${fk.name}' has no target '${fk.target}'`);
|
|
7189
|
+
let constraint = pgCore.foreignKey({
|
|
7190
|
+
name: fk.name,
|
|
7191
|
+
columns: fk.columns.map((column) => self[column]),
|
|
7192
|
+
foreignColumns: fk.references.map((column) => target[column])
|
|
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;
|
|
7197
|
+
})
|
|
5479
7198
|
]);
|
|
5480
7199
|
return drizzleTable.enableRLS();
|
|
5481
7200
|
}
|
|
@@ -5492,7 +7211,8 @@ function buildColumnV2({
|
|
|
5492
7211
|
column,
|
|
5493
7212
|
singlePrimaryKey,
|
|
5494
7213
|
compositeMembers,
|
|
5495
|
-
enums
|
|
7214
|
+
enums,
|
|
7215
|
+
introspection
|
|
5496
7216
|
}) {
|
|
5497
7217
|
const factory = resolveColumnFactory(table, column, enums);
|
|
5498
7218
|
let builder = factory(column.columnName);
|
|
@@ -5513,7 +7233,7 @@ function buildColumnV2({
|
|
|
5513
7233
|
}
|
|
5514
7234
|
builder = builder.default(drizzleOrm.sql.raw(column.defaultFn));
|
|
5515
7235
|
} else if (column.default !== void 0 && column.default !== null) {
|
|
5516
|
-
builder = builder.default(column.default);
|
|
7236
|
+
builder = introspection && column.type === "jsonb" && typeof column.default === "string" ? builder.default(drizzleOrm.sql.raw(`${quoteLiteral(column.default)}::jsonb`)) : builder.default(column.default);
|
|
5517
7237
|
}
|
|
5518
7238
|
return builder;
|
|
5519
7239
|
}
|
|
@@ -5564,11 +7284,14 @@ function buildForeignKeys2(table, self, built, byName) {
|
|
|
5564
7284
|
`enforced link '${link.name}' on '${table.table}' references unknown column '${targetColumn ? link.on : referencedColumn}'`
|
|
5565
7285
|
);
|
|
5566
7286
|
}
|
|
5567
|
-
|
|
7287
|
+
let constraint = pgCore.foreignKey({
|
|
5568
7288
|
name: foreignKeyName(table.table, link.name),
|
|
5569
7289
|
columns: [sourceColumn],
|
|
5570
7290
|
foreignColumns: [targetColumn]
|
|
5571
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;
|
|
5572
7295
|
});
|
|
5573
7296
|
}
|
|
5574
7297
|
var singlePrimaryKeyOf = (table) => table.primaryKey?.length === 1 ? table.primaryKey[0] : void 0;
|
|
@@ -5689,16 +7412,31 @@ function findTable2(snapshot, schema, table) {
|
|
|
5689
7412
|
const tables = snapshot.tables ?? {};
|
|
5690
7413
|
return Object.values(tables).find((entry) => entry.schema === schema && entry.name === table);
|
|
5691
7414
|
}
|
|
5692
|
-
|
|
7415
|
+
function planMigrationV2(input) {
|
|
7416
|
+
return planWithViewRecreation({ ...input, viewRecreation: "affected" });
|
|
7417
|
+
}
|
|
7418
|
+
async function planCommittedMigrationV2({
|
|
5693
7419
|
canonical,
|
|
5694
|
-
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
|
|
5695
7432
|
}) {
|
|
5696
7433
|
const schema = ormSchemaName(canonical.namespace);
|
|
5697
7434
|
const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
|
|
5698
7435
|
assertValidViewDependencies(canonical);
|
|
5699
7436
|
const hints = applyMigrationHintsV2(previous, canonical);
|
|
5700
7437
|
const snapshot = generateSnapshotV2(canonical, previousId(previous));
|
|
5701
|
-
const
|
|
7438
|
+
const generatedBaseDdl = await diffStatements(withoutViews(hints.adjusted), withoutViews(snapshot));
|
|
7439
|
+
const baseDdl = orderForeignKeyAdditions(canonical, generatedBaseDdl);
|
|
5702
7440
|
const previousViews = snapshotViews(hints.adjusted);
|
|
5703
7441
|
const currentViews = snapshotViews(snapshot);
|
|
5704
7442
|
const previousDependencies = snapshotViewDependencies(hints.adjusted);
|
|
@@ -5708,7 +7446,14 @@ async function planMigrationV2({
|
|
|
5708
7446
|
currentViews,
|
|
5709
7447
|
previousDependencies,
|
|
5710
7448
|
currentDependencies,
|
|
5711
|
-
|
|
7449
|
+
reach: viewReach({
|
|
7450
|
+
viewRecreation,
|
|
7451
|
+
hintStatements: hints.statements,
|
|
7452
|
+
baseDdl,
|
|
7453
|
+
previous,
|
|
7454
|
+
adjusted: hints.adjusted,
|
|
7455
|
+
current: snapshot
|
|
7456
|
+
})
|
|
5712
7457
|
});
|
|
5713
7458
|
const dropOrder = topologicalViewNames(
|
|
5714
7459
|
new Set([...affected].filter((name) => previousViews.has(name))),
|
|
@@ -5735,15 +7480,44 @@ async function planMigrationV2({
|
|
|
5735
7480
|
...dropOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(dropDdl, name, schema, "DROP") })),
|
|
5736
7481
|
...hints.statements.map((statement) => ({ kind: "hint", sql: statement })),
|
|
5737
7482
|
...baseDdl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") })),
|
|
7483
|
+
...foreignKeyTimingStatements(canonical, previous, baseDdl).map((sql4) => ({ kind: "ddl", sql: sql4 })),
|
|
5738
7484
|
...createOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(createDdl, name, schema, "CREATE") })),
|
|
5739
7485
|
...createOrder.map((name) => {
|
|
5740
7486
|
const view = currentViewDefinitions.get(name);
|
|
5741
7487
|
if (!view) throw new OrmDefinitionError(`missing canonical definition for view '${name}'`);
|
|
5742
7488
|
return { kind: "ddl", sql: validateViewCatalogStatement({ canonical, view, schema }) };
|
|
5743
|
-
})
|
|
7489
|
+
}),
|
|
7490
|
+
...refreshFunctionStatements({
|
|
7491
|
+
namespace: canonical.namespace,
|
|
7492
|
+
previous: previous._sequenceOrm?.refreshFunctions ?? {},
|
|
7493
|
+
current: materializedRefreshFunctions(canonical)
|
|
7494
|
+
}).map((sql4) => ({ kind: "ddl", sql: sql4 }))
|
|
5744
7495
|
];
|
|
5745
7496
|
return { namespace: canonical.namespace, schema, statements, snapshot };
|
|
5746
7497
|
}
|
|
7498
|
+
function orderForeignKeyAdditions(canonical, statements) {
|
|
7499
|
+
if (!canonical.tables.some((table) => table.foreignKeys?.length)) return [...statements];
|
|
7500
|
+
const addsForeignKey = (statement) => /^ALTER TABLE[\s\S]* ADD CONSTRAINT[\s\S]* FOREIGN KEY\b/.test(statement);
|
|
7501
|
+
return [...statements.filter((statement) => !addsForeignKey(statement)), ...statements.filter(addsForeignKey)];
|
|
7502
|
+
}
|
|
7503
|
+
function foreignKeyTimingStatements(canonical, previous, baseDdl) {
|
|
7504
|
+
const prior = previous._sequenceOrm?.foreignKeys ?? {};
|
|
7505
|
+
const result = [];
|
|
7506
|
+
for (const table of canonical.tables) {
|
|
7507
|
+
for (const fk of table.foreignKeys ?? []) {
|
|
7508
|
+
const before = prior[`${table.table}.${fk.name}`];
|
|
7509
|
+
const recreated = baseDdl.some((statement) => statement.includes(`ALTER TABLE ${quoteIdentifier(ormSchemaName(canonical.namespace))}.${quoteIdentifier(table.table)} ADD CONSTRAINT ${quoteIdentifier(fk.name)}`));
|
|
7510
|
+
if (fk.deferrable ? !before || before.deferrable !== fk.deferrable || recreated : before?.deferrable && !recreated) {
|
|
7511
|
+
result.push(foreignKeyTimingSql(ormSchemaName(canonical.namespace), table.table, fk.name, fk.deferrable));
|
|
7512
|
+
}
|
|
7513
|
+
}
|
|
7514
|
+
}
|
|
7515
|
+
return result;
|
|
7516
|
+
}
|
|
7517
|
+
function foreignKeyTimingSql(schema, table, name, timing) {
|
|
7518
|
+
const clause = timing === void 0 ? "NOT DEFERRABLE" : timing === "initially-deferred" ? "DEFERRABLE INITIALLY DEFERRED" : "DEFERRABLE INITIALLY IMMEDIATE";
|
|
7519
|
+
return `ALTER TABLE ${quoteIdentifier(schema)}.${quoteIdentifier(table)} ALTER CONSTRAINT ${quoteIdentifier(name)} ${clause}`;
|
|
7520
|
+
}
|
|
5747
7521
|
function validateViewCatalogStatement({
|
|
5748
7522
|
canonical,
|
|
5749
7523
|
view,
|
|
@@ -5871,20 +7645,102 @@ function withoutViews(snapshot) {
|
|
|
5871
7645
|
delete clone._sequenceOrm;
|
|
5872
7646
|
return clone;
|
|
5873
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
|
+
}
|
|
5874
7728
|
function affectedViewNames({
|
|
5875
7729
|
previousViews,
|
|
5876
7730
|
currentViews,
|
|
5877
7731
|
previousDependencies,
|
|
5878
7732
|
currentDependencies,
|
|
5879
|
-
|
|
7733
|
+
reach
|
|
5880
7734
|
}) {
|
|
5881
7735
|
const allNames = /* @__PURE__ */ new Set([...previousViews.keys(), ...currentViews.keys()]);
|
|
5882
|
-
if (
|
|
7736
|
+
if (reach === "all") return allNames;
|
|
7737
|
+
const dependenciesOf = (name) => /* @__PURE__ */ new Set([...previousDependencies[name] ?? [], ...currentDependencies[name] ?? []]);
|
|
5883
7738
|
const affected = new Set(
|
|
5884
7739
|
[...allNames].filter((name) => {
|
|
5885
7740
|
const before = previousViews.get(name);
|
|
5886
7741
|
const after = currentViews.get(name);
|
|
5887
|
-
|
|
7742
|
+
if (before === void 0 || after === void 0 || stableJson(before) !== stableJson(after)) return true;
|
|
7743
|
+
return [...dependenciesOf(name)].some((dependency) => reach.has(dependency));
|
|
5888
7744
|
})
|
|
5889
7745
|
);
|
|
5890
7746
|
let grew = true;
|
|
@@ -5892,8 +7748,7 @@ function affectedViewNames({
|
|
|
5892
7748
|
grew = false;
|
|
5893
7749
|
for (const name of allNames) {
|
|
5894
7750
|
if (affected.has(name)) continue;
|
|
5895
|
-
|
|
5896
|
-
if ([...dependencies].some((dependency) => affected.has(dependency))) {
|
|
7751
|
+
if ([...dependenciesOf(name)].some((dependency) => affected.has(dependency))) {
|
|
5897
7752
|
affected.add(name);
|
|
5898
7753
|
grew = true;
|
|
5899
7754
|
}
|
|
@@ -6808,7 +8663,9 @@ var PLATFORM_USER_FIELDS = [
|
|
|
6808
8663
|
];
|
|
6809
8664
|
function generateSdl(canonical, options = {}) {
|
|
6810
8665
|
const def = normalizeCanonical(canonicalDefinitionSchema.parse(canonical));
|
|
6811
|
-
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) }));
|
|
6812
8669
|
if (tables.length === 0) {
|
|
6813
8670
|
throw new SdlGenerationError(
|
|
6814
8671
|
`Namespace "${def.namespace}" exposes no relations (synced tables are excluded from SDL in v2.0) \u2014 nothing to generate.`
|
|
@@ -6864,8 +8721,14 @@ function generateSdl(canonical, options = {}) {
|
|
|
6864
8721
|
'"""Gates an operation behind a platform capability or custom role \u2014 checked by the data plane before execution."""\ndirective @requires(capability: String, role: String) on QUERY | MUTATION'
|
|
6865
8722
|
);
|
|
6866
8723
|
blocks.push("directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT");
|
|
8724
|
+
blocks.push(
|
|
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'
|
|
8726
|
+
);
|
|
8727
|
+
if (tables.some((table) => table.resolved !== void 0)) blocks.push(...RESOLVED_DIRECTIVE_BLOCKS);
|
|
6867
8728
|
for (const scalar2 of CUSTOM_SCALARS) blocks.push(`scalar ${scalar2}`);
|
|
8729
|
+
blocks.push(...renderSharedCellTypes([...cellsByTable.values()]));
|
|
6868
8730
|
blocks.push(renderEnum(ORDER_BY_ENUM, [...ORDER_BY_ENUM_VALUES]));
|
|
8731
|
+
blocks.push(renderEnum(DATE_BUCKET_ENUM, DATE_BUCKETS.map((bucket) => bucket.toUpperCase())));
|
|
6869
8732
|
for (const enumName of [...usedEnums].sort()) {
|
|
6870
8733
|
const entry = enumByName.get(enumName);
|
|
6871
8734
|
if (!entry) continue;
|
|
@@ -6888,16 +8751,32 @@ function generateSdl(canonical, options = {}) {
|
|
|
6888
8751
|
names,
|
|
6889
8752
|
namesByTable,
|
|
6890
8753
|
emitAggregateBoolExp: aggregateBoolExpTargets.has(table.table),
|
|
6891
|
-
pinned: options.pinnedColumns?.[table.table] ?? []
|
|
8754
|
+
pinned: options.pinnedColumns?.[table.table] ?? [],
|
|
8755
|
+
cells: cellsByTable.get(table.table) ?? null
|
|
6892
8756
|
})
|
|
6893
8757
|
);
|
|
6894
8758
|
}
|
|
6895
8759
|
blocks.push(renderQueryRoot(tables, namesByTable));
|
|
6896
8760
|
const mutationRoot = renderMutationRoot(tables, namesByTable);
|
|
6897
8761
|
if (mutationRoot !== null) blocks.push(mutationRoot);
|
|
8762
|
+
assertNoDuplicateTypeNames(blocks);
|
|
6898
8763
|
return `${blocks.join("\n\n")}
|
|
6899
8764
|
`;
|
|
6900
8765
|
}
|
|
8766
|
+
var TYPE_DECLARATION_RE = /^(?:type|input|enum|scalar|interface|union)\s+([_A-Za-z][_0-9A-Za-z]*)/;
|
|
8767
|
+
function assertNoDuplicateTypeNames(blocks) {
|
|
8768
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8769
|
+
for (const block of blocks) {
|
|
8770
|
+
const declared = TYPE_DECLARATION_RE.exec(block)?.[1];
|
|
8771
|
+
if (declared === void 0) continue;
|
|
8772
|
+
if (seen.has(declared)) {
|
|
8773
|
+
throw new SdlGenerationError(
|
|
8774
|
+
`Generated schema declares "${declared}" twice. A canonical enum or table name is PascalCased into this GraphQL type name and collides with another declaration (for example an enum named "date_bucket" renders as "DateBucket", the shared bucket enum). Rename the offending enum or table.`
|
|
8775
|
+
);
|
|
8776
|
+
}
|
|
8777
|
+
seen.add(declared);
|
|
8778
|
+
}
|
|
8779
|
+
}
|
|
6901
8780
|
function assertEnumValue(value, context) {
|
|
6902
8781
|
if (!GRAPHQL_NAME_RE2.test(value) || RESERVED_ENUM_VALUES.has(value)) {
|
|
6903
8782
|
throw new SdlGenerationError(
|
|
@@ -6963,6 +8842,52 @@ ${orderFields}
|
|
|
6963
8842
|
}`
|
|
6964
8843
|
];
|
|
6965
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
|
+
}
|
|
6966
8891
|
function relationArgs(target) {
|
|
6967
8892
|
return `where: ${target.boolExp}, orderBy: [${target.orderBy}!], limit: Int, offset: Int`;
|
|
6968
8893
|
}
|
|
@@ -6971,10 +8896,10 @@ function rootArgs(names) {
|
|
|
6971
8896
|
}
|
|
6972
8897
|
function primaryKeyColumns(table) {
|
|
6973
8898
|
if (!table.primaryKey || table.primaryKey.length === 0) return [];
|
|
6974
|
-
return table.primaryKey.map((
|
|
6975
|
-
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);
|
|
6976
8901
|
if (!column) {
|
|
6977
|
-
throw new SdlGenerationError(`Primary key column "${
|
|
8902
|
+
throw new SdlGenerationError(`Primary key column "${identifier3}" not found on table "${table.table}".`);
|
|
6978
8903
|
}
|
|
6979
8904
|
return column;
|
|
6980
8905
|
});
|
|
@@ -6983,10 +8908,12 @@ function renderTableBlocks(ctx) {
|
|
|
6983
8908
|
const { table, names } = ctx;
|
|
6984
8909
|
const blocks = [
|
|
6985
8910
|
renderObjectType(ctx),
|
|
8911
|
+
...renderCellsType(ctx),
|
|
6986
8912
|
renderSelectColumnEnum(ctx),
|
|
6987
8913
|
renderOrderByInput(ctx),
|
|
6988
8914
|
renderBoolExp(ctx),
|
|
6989
|
-
...renderAggregateTypes(ctx)
|
|
8915
|
+
...renderAggregateTypes(ctx),
|
|
8916
|
+
...renderGroupByTypes(ctx)
|
|
6990
8917
|
];
|
|
6991
8918
|
if (ctx.emitAggregateBoolExp) blocks.push(...renderAggregateBoolExp(ctx));
|
|
6992
8919
|
const pk = primaryKeyColumns(table);
|
|
@@ -6996,11 +8923,49 @@ function renderTableBlocks(ctx) {
|
|
|
6996
8923
|
${fields}
|
|
6997
8924
|
}`);
|
|
6998
8925
|
}
|
|
6999
|
-
if (
|
|
8926
|
+
if (ctx.table.resolved !== void 0) {
|
|
8927
|
+
blocks.push(...renderResolvedWriteBlocks(ctx));
|
|
8928
|
+
} else if (allowsInsert(table) || allowsUpdate(table)) {
|
|
7000
8929
|
blocks.push(...renderWriteBlocks(ctx, { insert: allowsInsert(table), update: allowsUpdate(table) }));
|
|
7001
8930
|
}
|
|
7002
8931
|
return blocks;
|
|
7003
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
|
+
}
|
|
7004
8969
|
function allowsInsert(table) {
|
|
7005
8970
|
return table.policies.write !== void 0 || table.policies.insert !== void 0;
|
|
7006
8971
|
}
|
|
@@ -7023,6 +8988,10 @@ function renderObjectType(ctx) {
|
|
|
7023
8988
|
const deprecated = column.deprecated ? " @deprecated" : "";
|
|
7024
8989
|
lines.push(` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}${bang}${deprecated}`);
|
|
7025
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
|
+
}
|
|
7026
8995
|
for (const link of table.links) {
|
|
7027
8996
|
if (link.description !== void 0) lines.push(...descriptionLines(link.description, " "));
|
|
7028
8997
|
if (link.kind === "toPlatformUser") {
|
|
@@ -7091,7 +9060,10 @@ function numericishColumns(table) {
|
|
|
7091
9060
|
return table.columns.filter((column) => typeof column.type !== "object" && NUMERICISH.has(column.type));
|
|
7092
9061
|
}
|
|
7093
9062
|
function jsonbColumns(table) {
|
|
7094
|
-
return table.
|
|
9063
|
+
return clientWritableColumns(table).filter((column) => column.type === "jsonb");
|
|
9064
|
+
}
|
|
9065
|
+
function clientWritableColumns(table) {
|
|
9066
|
+
return table.columns;
|
|
7095
9067
|
}
|
|
7096
9068
|
function renderAggregateTypes(ctx) {
|
|
7097
9069
|
const { table, names } = ctx;
|
|
@@ -7123,6 +9095,32 @@ ${sumLines.join("\n")}
|
|
|
7123
9095
|
}
|
|
7124
9096
|
return blocks;
|
|
7125
9097
|
}
|
|
9098
|
+
function groupableColumns(table) {
|
|
9099
|
+
return table.columns.filter((column) => column.type !== "jsonb");
|
|
9100
|
+
}
|
|
9101
|
+
function renderGroupByTypes(ctx) {
|
|
9102
|
+
const { table, names } = ctx;
|
|
9103
|
+
const groupable = groupableColumns(table);
|
|
9104
|
+
if (groupable.length === 0) return [];
|
|
9105
|
+
const keyLines = groupable.map((column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`);
|
|
9106
|
+
return [
|
|
9107
|
+
renderEnum(names.groupByColumnEnum, groupable.map((column) => fieldName(column.name))),
|
|
9108
|
+
`input ${names.groupByInput} {
|
|
9109
|
+
column: ${names.groupByColumnEnum}!
|
|
9110
|
+
bucket: ${DATE_BUCKET_ENUM}
|
|
9111
|
+
}`,
|
|
9112
|
+
`type ${names.groupKey} {
|
|
9113
|
+
${keyLines.join("\n")}
|
|
9114
|
+
}`,
|
|
9115
|
+
`type ${names.aggregateGroup} {
|
|
9116
|
+
key: ${names.groupKey}!
|
|
9117
|
+
aggregate: ${names.aggregateFields}
|
|
9118
|
+
}`
|
|
9119
|
+
];
|
|
9120
|
+
}
|
|
9121
|
+
function groupRootArgs(names) {
|
|
9122
|
+
return `where: ${names.boolExp}, groupBy: [${names.groupByInput}!]!, limit: Int, offset: Int`;
|
|
9123
|
+
}
|
|
7126
9124
|
function renderAggregateBoolExp(ctx) {
|
|
7127
9125
|
const { names } = ctx;
|
|
7128
9126
|
return [
|
|
@@ -7148,12 +9146,15 @@ function constraintValues(table) {
|
|
|
7148
9146
|
}
|
|
7149
9147
|
return [...values].sort();
|
|
7150
9148
|
}
|
|
9149
|
+
function clientSettableColumns(table) {
|
|
9150
|
+
return clientWritableColumns(table).filter((column) => column.onUpdateFn === void 0);
|
|
9151
|
+
}
|
|
7151
9152
|
function renderWriteBlocks(ctx, access) {
|
|
7152
9153
|
const { table, names, pinned } = ctx;
|
|
7153
9154
|
const blocks = [];
|
|
7154
9155
|
if (access.insert) {
|
|
7155
9156
|
const pinnedSet = new Set(pinned);
|
|
7156
|
-
const insertable = table.
|
|
9157
|
+
const insertable = clientWritableColumns(table).filter(
|
|
7157
9158
|
(column) => !pinnedSet.has(column.name) && !pinnedSet.has(column.columnName)
|
|
7158
9159
|
);
|
|
7159
9160
|
if (insertable.length === 0) {
|
|
@@ -7169,7 +9170,7 @@ ${insertLines.join("\n")}
|
|
|
7169
9170
|
}`);
|
|
7170
9171
|
}
|
|
7171
9172
|
if (access.update) {
|
|
7172
|
-
const setLines = table.
|
|
9173
|
+
const setLines = clientSettableColumns(table).map(
|
|
7173
9174
|
(column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
|
|
7174
9175
|
);
|
|
7175
9176
|
blocks.push(`input ${names.setInput} {
|
|
@@ -7196,7 +9197,7 @@ ${incLines.join("\n")}
|
|
|
7196
9197
|
if (access.insert && allowsOnConflict(table) && constraints.length > 0) {
|
|
7197
9198
|
for (const value of constraints) assertEnumValue(value, `constraint enum for table "${table.table}"`);
|
|
7198
9199
|
blocks.push(renderEnum(names.constraintEnum, constraints));
|
|
7199
|
-
const updateColumns = table.
|
|
9200
|
+
const updateColumns = clientSettableColumns(table).map((column) => fieldName(column.name));
|
|
7200
9201
|
blocks.push(renderEnum(names.updateColumnEnum, updateColumns));
|
|
7201
9202
|
blocks.push(
|
|
7202
9203
|
`input ${names.onConflict} {
|
|
@@ -7228,6 +9229,9 @@ function renderQueryRoot(tables, namesByTable) {
|
|
|
7228
9229
|
lines.push(` ${names.byPkRoot}(${args}): ${names.typename}`);
|
|
7229
9230
|
}
|
|
7230
9231
|
lines.push(` ${names.aggregateRoot}(${rootArgs(names)}): ${names.aggregate}!`);
|
|
9232
|
+
if (groupableColumns(table).length > 0) {
|
|
9233
|
+
lines.push(` ${names.aggregateGroupsRoot}(${groupRootArgs(names)}): [${names.aggregateGroup}!]!`);
|
|
9234
|
+
}
|
|
7231
9235
|
}
|
|
7232
9236
|
lines.push("}");
|
|
7233
9237
|
return lines.join("\n");
|
|
@@ -7243,6 +9247,10 @@ function renderMutationRoot(tables, namesByTable) {
|
|
|
7243
9247
|
if (!canInsert && !canUpdate) continue;
|
|
7244
9248
|
const names = namesByTable.get(table.table);
|
|
7245
9249
|
if (!names) continue;
|
|
9250
|
+
if (table.resolved !== void 0) {
|
|
9251
|
+
lines.push(...renderResolvedMutationFields(table, names));
|
|
9252
|
+
continue;
|
|
9253
|
+
}
|
|
7246
9254
|
const constraints = constraintValues(table);
|
|
7247
9255
|
const onConflictArg = canInsert && allowsOnConflict(table) && constraints.length > 0 ? `, onConflict: ${names.onConflict}` : "";
|
|
7248
9256
|
const updateArgs = [`_set: ${names.setInput}`];
|
|
@@ -7296,6 +9304,92 @@ function renderCowMutationFields(table, names) {
|
|
|
7296
9304
|
}
|
|
7297
9305
|
return lines;
|
|
7298
9306
|
}
|
|
9307
|
+
var ORM_MIGRATION_EXPORT_FILE = "orm.migration.json";
|
|
9308
|
+
var ORM_MIGRATION_EXPORT_MAX_BYTES = ORM_REGISTRATION_MAX_BYTES;
|
|
9309
|
+
var hash = zod.z.string().regex(/^sha256:[0-9a-f]{64}$/);
|
|
9310
|
+
var ormMigrationPayloadSchema = zod.z.object({
|
|
9311
|
+
canonical_definition: canonicalDefinitionSchema,
|
|
9312
|
+
documents: zod.z.array(zod.z.object({ path: zod.z.string().min(1).max(2048), contents: zod.z.string().max(1e6) }).strict()).max(5e3),
|
|
9313
|
+
client_manifest: zod.z.object({ ops_hash: hash, operations: zod.z.record(zod.z.string(), hash) }).strict(),
|
|
9314
|
+
sdl: zod.z.string().refine((value) => Buffer.byteLength(value, "utf8") <= MAX_GENERATED_SDL_BYTES, {
|
|
9315
|
+
message: `sdl exceeds ${MAX_GENERATED_SDL_BYTES} bytes`
|
|
9316
|
+
}),
|
|
9317
|
+
schema_snapshot: zod.z.json(),
|
|
9318
|
+
// The existing ORM committed-migration validator and replay engine remain
|
|
9319
|
+
// authoritative. This envelope hashes the provenance, never authorizes SQL.
|
|
9320
|
+
migration: zod.z.json().optional()
|
|
9321
|
+
}).strict();
|
|
9322
|
+
zod.z.object({
|
|
9323
|
+
schema_version: zod.z.literal(1),
|
|
9324
|
+
schema_hash: hash,
|
|
9325
|
+
payload: ormMigrationPayloadSchema,
|
|
9326
|
+
checksum: zod.z.string().regex(/^[0-9a-f]{64}$/)
|
|
9327
|
+
}).strict();
|
|
9328
|
+
function checksum(value) {
|
|
9329
|
+
return crypto.createHash("sha256").update(stableJson(value)).digest("hex");
|
|
9330
|
+
}
|
|
9331
|
+
function createOrmMigrationExport({ schemaHash, payload }) {
|
|
9332
|
+
const contents = { schema_version: 1, schema_hash: hash.parse(schemaHash), payload: ormMigrationPayloadSchema.parse(payload) };
|
|
9333
|
+
return { ...contents, checksum: checksum(contents) };
|
|
9334
|
+
}
|
|
9335
|
+
function renderOrmMigrationExport(exported) {
|
|
9336
|
+
const text4 = `${JSON.stringify(exported, null, 2)}
|
|
9337
|
+
`;
|
|
9338
|
+
if (Buffer.byteLength(text4, "utf8") > ORM_MIGRATION_EXPORT_MAX_BYTES) throw new Error("ORM migration export exceeds the size limit");
|
|
9339
|
+
return text4;
|
|
9340
|
+
}
|
|
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
|
+
}
|
|
7299
9393
|
|
|
7300
9394
|
// src/cli.ts
|
|
7301
9395
|
var BASE_URL_ENV = "SEQUENCE_ORM_BASE_URL";
|
|
@@ -7303,7 +9397,9 @@ var TOKEN_ENV = "SEQUENCE_ORM_TOKEN";
|
|
|
7303
9397
|
var ORM_MODULE_PATH_ENV = "SEQUENCE_ORM_MODULE_PATH";
|
|
7304
9398
|
var USAGE = `usage:
|
|
7305
9399
|
seq-studio orm init <dir> scaffold a namespace package (v2: TypeScript schema + GraphQL documents)
|
|
7306
|
-
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)
|
|
9401
|
+
seq-studio orm generate [dir] --migration-export [--check]
|
|
9402
|
+
export reviewed ORM inputs for a coordinated pipeline migration; --check verifies without writing
|
|
7307
9403
|
seq-studio orm validate [dir] parse + validate definitions and verify committed migrations are in sync
|
|
7308
9404
|
seq-studio orm plan [dir] -e <env> offline dry run: compile definitions and diff against the registry (no database, no writes)
|
|
7309
9405
|
seq-studio orm apply [dir] -e <env> author the migration, register, apply, and refresh the generated outputs (the everyday command)
|
|
@@ -7318,6 +9414,8 @@ var USAGE = `usage:
|
|
|
7318
9414
|
v2 = sequence.config.ts + schema/*.ts + graphql/**; v1 (legacy) = one <table>.yaml per table.
|
|
7319
9415
|
Environments: see \`seq-studio envs list\` (discovered after you authenticate).
|
|
7320
9416
|
Authenticate with: seq-studio login
|
|
9417
|
+
Execute endpoint rollout: SEQUENCE_ORM_ATLAS_EXECUTE_V2=1 opts into /api/data/v2;
|
|
9418
|
+
otherwise execute uses /_data/v2. Server rollout and authorization still apply.
|
|
7321
9419
|
`;
|
|
7322
9420
|
var tokenProvider = null;
|
|
7323
9421
|
function setTokenProvider(provider) {
|
|
@@ -7470,6 +9568,7 @@ async function walkFiles(root, prefix = "") {
|
|
|
7470
9568
|
var SCHEMA_GRAPHQL_FILE = "schema.graphql";
|
|
7471
9569
|
var OPERATIONS_MANIFEST_FILE = "operations.manifest.json";
|
|
7472
9570
|
var TYPE_POLICIES_FILE = "typePolicies.gen.ts";
|
|
9571
|
+
var SOURCE_CONTRACTS_FILE = "sources.gen.ts";
|
|
7473
9572
|
var GRAPHQL_DIRNAME = "graphql";
|
|
7474
9573
|
var CODEGEN_CONFIG_FILE = "codegen.ts";
|
|
7475
9574
|
function ormLoadOptions() {
|
|
@@ -7489,9 +9588,10 @@ async function compileV2Project(dir) {
|
|
|
7489
9588
|
const documents = await collectGraphqlDocuments(dir);
|
|
7490
9589
|
const result = compileNamespace({ canonical: loaded.canonical, sdl, documents });
|
|
7491
9590
|
if (!result.ok) return { ok: false, diagnostics: result.diagnostics };
|
|
9591
|
+
const diagnostics = [...result.diagnostics, ...roleWarnings({ canonical: loaded.canonical, manifest: result.manifest })];
|
|
7492
9592
|
return {
|
|
7493
9593
|
ok: true,
|
|
7494
|
-
project: { loaded, sdl, documents, manifest: result.manifest, diagnostics
|
|
9594
|
+
project: { loaded, sdl, documents, manifest: result.manifest, diagnostics }
|
|
7495
9595
|
};
|
|
7496
9596
|
}
|
|
7497
9597
|
var GQL_TEMPLATE_RE = /(?:\bgql|\/\*\s*GraphQL\s*\*\/)\s*`([^`]*)`/g;
|
|
@@ -7532,6 +9632,12 @@ function printDiagnostics(diagnostics) {
|
|
|
7532
9632
|
}
|
|
7533
9633
|
async function generateCommand(rest) {
|
|
7534
9634
|
const dir = positional(rest) ?? ".";
|
|
9635
|
+
const migrationExport = rest.includes("--migration-export");
|
|
9636
|
+
const check = rest.includes("--check");
|
|
9637
|
+
if (check && !migrationExport) {
|
|
9638
|
+
console.error("generate --check requires --migration-export");
|
|
9639
|
+
return 1;
|
|
9640
|
+
}
|
|
7535
9641
|
if (await detectProjectFormat(dir) === "v1") {
|
|
7536
9642
|
console.error(`generate needs a v2 project (${NAMESPACE_CONFIG_FILE}) \u2014 convert with: seq-studio orm migrate-from-yaml ${dir}`);
|
|
7537
9643
|
return 1;
|
|
@@ -7539,6 +9645,36 @@ async function generateCommand(rest) {
|
|
|
7539
9645
|
const compiled = await compileV2Project(dir);
|
|
7540
9646
|
printDiagnostics(compiled.ok ? compiled.project.diagnostics : compiled.diagnostics);
|
|
7541
9647
|
if (!compiled.ok) return 1;
|
|
9648
|
+
if (migrationExport) {
|
|
9649
|
+
if (await validateV2Command(dir) !== 0) return 1;
|
|
9650
|
+
const { loaded: loaded2, sdl, documents, manifest: manifest2 } = compiled.project;
|
|
9651
|
+
const workspace = await readMigrationWorkspace(dir, loaded2.canonical.namespace);
|
|
9652
|
+
const migration = await loadCommittedTipMigrationV2({ dir, loaded: loaded2, workspace });
|
|
9653
|
+
const exported = createOrmMigrationExport({ schemaHash: manifest2.schemaHash, payload: {
|
|
9654
|
+
canonical_definition: loaded2.canonical,
|
|
9655
|
+
documents,
|
|
9656
|
+
sdl,
|
|
9657
|
+
client_manifest: { ops_hash: manifest2.opsHash, operations: Object.fromEntries(Object.entries(manifest2.operations).map(([name, operation]) => [name, operation.opHash])) },
|
|
9658
|
+
schema_snapshot: workspace.tipSnapshot,
|
|
9659
|
+
...migration ? { migration } : {}
|
|
9660
|
+
} });
|
|
9661
|
+
const text4 = renderOrmMigrationExport(exported);
|
|
9662
|
+
const path$1 = path.join(dir, ORM_MIGRATION_EXPORT_FILE);
|
|
9663
|
+
if (check) {
|
|
9664
|
+
const committed = await promises.readFile(path$1, "utf8").catch((error) => {
|
|
9665
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return void 0;
|
|
9666
|
+
throw error;
|
|
9667
|
+
});
|
|
9668
|
+
if (committed !== text4) {
|
|
9669
|
+
console.error(`${ORM_MIGRATION_EXPORT_FILE} is missing or stale; run seq-studio orm generate ${dir} --migration-export and commit it`);
|
|
9670
|
+
return 1;
|
|
9671
|
+
}
|
|
9672
|
+
console.log(`${ORM_MIGRATION_EXPORT_FILE} matches the current definitions, operations, and committed migration`);
|
|
9673
|
+
return 0;
|
|
9674
|
+
}
|
|
9675
|
+
await promises.writeFile(path$1, text4);
|
|
9676
|
+
console.log(`wrote ${ORM_MIGRATION_EXPORT_FILE} (${exported.checksum.slice(0, 12)})`);
|
|
9677
|
+
}
|
|
7542
9678
|
await writeGenerateOutputs(dir, compiled.project);
|
|
7543
9679
|
const { loaded, manifest } = compiled.project;
|
|
7544
9680
|
console.log(
|
|
@@ -7559,6 +9695,8 @@ async function writeGenerateOutputs(dir, project) {
|
|
|
7559
9695
|
await promises.writeFile(path.join(dir, OPERATIONS_MANIFEST_FILE), `${JSON.stringify(manifestFile, null, 2)}
|
|
7560
9696
|
`);
|
|
7561
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);
|
|
7562
9700
|
}
|
|
7563
9701
|
async function runConsumerCodegen(dir) {
|
|
7564
9702
|
const codegenPath = path.join(dir, CODEGEN_CONFIG_FILE);
|
|
@@ -7654,9 +9792,10 @@ async function executeV2Command(rest) {
|
|
|
7654
9792
|
);
|
|
7655
9793
|
}
|
|
7656
9794
|
const variables = await readExecuteVariables(variablesFile);
|
|
9795
|
+
const executePrefix = process.env["SEQUENCE_ORM_ATLAS_EXECUTE_V2"] === "1" ? "/api/data/v2" : "/_data/v2";
|
|
7657
9796
|
const response = await apiRequestStrict(
|
|
7658
9797
|
"POST",
|
|
7659
|
-
|
|
9798
|
+
`${executePrefix}/${loaded.canonical.namespace}`,
|
|
7660
9799
|
{ op: selected.hash, variables }
|
|
7661
9800
|
);
|
|
7662
9801
|
console.log(JSON.stringify(response, null, 2));
|
|
@@ -7825,16 +9964,28 @@ async function checkWorkspaceV2({
|
|
|
7825
9964
|
return 1;
|
|
7826
9965
|
}
|
|
7827
9966
|
const previousSnapshot = tip.ordinal > 1 ? await readSnapshot(dir, tip.ordinal - 1) : void 0;
|
|
7828
|
-
const plan = await planMigrationV2({ canonical: loaded.canonical, previousSnapshot });
|
|
7829
|
-
const regenerated = renderMigrationSql({ name: tip.name, namespace, contentHash: loaded.hash, plan });
|
|
7830
9967
|
const committed = await readMigrationSql(dir, tip);
|
|
7831
|
-
|
|
9968
|
+
const { reproduced } = await planCommittedTipV2({ loaded, tip, previousSnapshot, committed });
|
|
9969
|
+
if (!reproduced) {
|
|
7832
9970
|
log(`${MIGRATIONS_DIRNAME}/${tip.name}.sql does not regenerate from the definitions \u2014 re-run \`seq-studio orm diff\``);
|
|
7833
9971
|
return 1;
|
|
7834
9972
|
}
|
|
7835
9973
|
log(`migrations in sync \u2014 ${workspace.journal.entries.length} committed, tip ${tip.name}`);
|
|
7836
9974
|
return 0;
|
|
7837
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
|
+
}
|
|
7838
9989
|
function matchesRebaseTip({
|
|
7839
9990
|
committed,
|
|
7840
9991
|
tip,
|
|
@@ -7937,7 +10088,6 @@ async function loadCommittedTipMigrationV2({
|
|
|
7937
10088
|
loaded,
|
|
7938
10089
|
workspace
|
|
7939
10090
|
}) {
|
|
7940
|
-
const namespace = loaded.canonical.namespace;
|
|
7941
10091
|
const tip = workspace.journal.entries.at(-1);
|
|
7942
10092
|
if (!tip) return void 0;
|
|
7943
10093
|
if (tip.content_hash !== loaded.hash) {
|
|
@@ -7947,10 +10097,9 @@ async function loadCommittedTipMigrationV2({
|
|
|
7947
10097
|
);
|
|
7948
10098
|
}
|
|
7949
10099
|
const previousSnapshot = tip.ordinal > 1 ? await readSnapshot(dir, tip.ordinal - 1) : void 0;
|
|
7950
|
-
const plan = await planMigrationV2({ canonical: loaded.canonical, previousSnapshot });
|
|
7951
10100
|
const committed = await readMigrationSql(dir, tip);
|
|
7952
|
-
const
|
|
7953
|
-
if (
|
|
10101
|
+
const { plan, reproduced } = await planCommittedTipV2({ loaded, tip, previousSnapshot, committed });
|
|
10102
|
+
if (!reproduced) {
|
|
7954
10103
|
throw new OrmDefinitionError(
|
|
7955
10104
|
`${tip.name}.sql does not regenerate from the definitions \u2014 run \`seq-studio orm diff --check\``,
|
|
7956
10105
|
MIGRATIONS_DIRNAME
|
|
@@ -8171,6 +10320,7 @@ function columnProperty(table, column, singlePrimaryKey) {
|
|
|
8171
10320
|
}
|
|
8172
10321
|
chain += `.default(${JSON.stringify(column.default)})`;
|
|
8173
10322
|
}
|
|
10323
|
+
if (column.onUpdateFn !== void 0) chain += `.onUpdate(${quote(column.onUpdateFn)})`;
|
|
8174
10324
|
if (column.description !== void 0) chain += `.description(${quote(column.description)})`;
|
|
8175
10325
|
if (column.renamedFrom !== void 0) chain += `.renamedFrom(${quote(column.renamedFrom)})`;
|
|
8176
10326
|
if (column.cast !== void 0) chain += `.cast(${quote(column.cast)})`;
|