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