@sequenceholdings/orm 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -2
- package/dist/authoring.cjs +1474 -54
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +242 -4
- package/dist/authoring.d.ts +242 -4
- package/dist/authoring.js +1465 -56
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +740 -42
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +742 -53
- package/dist/canonical.d.ts +742 -53
- package/dist/canonical.js +724 -43
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +2478 -328
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2480 -330
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs +90 -25
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +39 -22
- package/dist/client.d.ts +39 -22
- package/dist/client.js +87 -26
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +1755 -329
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +56 -3
- package/dist/compiler.d.ts +56 -3
- package/dist/compiler.js +1756 -330
- package/dist/compiler.js.map +1 -1
- package/dist/constants-YoFGjIBM.d.cts +99 -0
- package/dist/constants-YoFGjIBM.d.ts +99 -0
- package/dist/dynamic-input.d.cts +1 -0
- package/dist/dynamic-input.d.ts +1 -0
- package/dist/index.cjs +1973 -164
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +474 -100
- package/dist/index.d.ts +474 -100
- package/dist/index.js +1940 -167
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +869 -46
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +699 -25
- package/dist/ir.d.ts +699 -25
- package/dist/ir.js +861 -47
- package/dist/ir.js.map +1 -1
- package/dist/{naming-CTvWSRTf.d.ts → naming-CPfx7EfF.d.ts} +28 -1
- package/dist/{naming-DRyinU4u.d.cts → naming-CU9ThP_E.d.cts} +28 -1
- package/dist/policy-1iZbGnZZ.d.cts +24 -0
- package/dist/policy-1iZbGnZZ.d.ts +24 -0
- package/dist/predicate-v2.cjs +852 -49
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.d.cts +2 -1
- package/dist/predicate-v2.d.ts +2 -1
- package/dist/predicate-v2.js +852 -49
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +1309 -58
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.d.cts +7 -3
- package/dist/sdl.d.ts +7 -3
- package/dist/sdl.js +1305 -59
- package/dist/sdl.js.map +1 -1
- package/package.json +11 -6
- package/templates/orm-namespace/CLAUDE.md +6 -4
- package/templates/orm-namespace/codegen.preset.d.cts +7 -0
- package/templates/orm-namespace/codegen.ts +2 -1
- package/templates/orm-namespace/graphql/example/queries.ts +4 -3
- package/templates/orm-namespace/package.json +2 -1
- package/templates/orm-namespace/schema/tables.ts +3 -2
- package/templates/orm-namespace/sequence.config.ts +7 -11
package/dist/cli.js
CHANGED
|
@@ -6,8 +6,8 @@ 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,
|
|
10
|
-
import { buildSchema, parse, validate, Kind, valueFromASTUntyped, print } from 'graphql';
|
|
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
|
+
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';
|
|
13
13
|
import { capabilityPinsSchema, customRolesBlockSchema, validateCapabilityPins } from '@sequenceholdings/custom-roles-manifest';
|
|
@@ -36,9 +36,519 @@ var PredicateParseError = class extends Error {
|
|
|
36
36
|
}
|
|
37
37
|
name = "PredicateParseError";
|
|
38
38
|
};
|
|
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
|
+
|
|
245
|
+
// src/constants.ts
|
|
246
|
+
var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
|
247
|
+
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
248
|
+
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
249
|
+
var MAX_IDENTIFIER_LENGTH = 63;
|
|
250
|
+
var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
|
|
251
|
+
var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
252
|
+
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
|
|
253
|
+
var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
|
|
254
|
+
var ORM_SCHEMA_PREFIX = "orm_";
|
|
255
|
+
var CONSUMER_ROLE_SUFFIX = "_consumer";
|
|
256
|
+
var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
|
|
257
|
+
var consumerRoleName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}${CONSUMER_ROLE_SUFFIX}`;
|
|
258
|
+
var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
|
|
259
|
+
var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
|
|
260
|
+
var HASH_DOMAIN = "sequence-orm-definitions\0";
|
|
261
|
+
var MAX_DEFINITION_FILE_BYTES = 1024 * 1024;
|
|
262
|
+
var MAX_GENERATED_SDL_BYTES = 16 * 1024 * 1024;
|
|
263
|
+
var ORM_REGISTRATION_MAX_BYTES = 64 * 1024 * 1024;
|
|
264
|
+
var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
|
|
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
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/canonical/v2.ts
|
|
39
548
|
var CANONICAL_FORMAT = 2;
|
|
40
549
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
41
|
-
var
|
|
550
|
+
var identifier2 = z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
551
|
+
var columnIdentifier = z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
|
|
42
552
|
var COLUMN_TYPES = [
|
|
43
553
|
"uuid",
|
|
44
554
|
"text",
|
|
@@ -53,59 +563,71 @@ var COLUMN_TYPES = [
|
|
|
53
563
|
];
|
|
54
564
|
var columnTypeSchema = z.enum(COLUMN_TYPES);
|
|
55
565
|
var canonicalEnumSchema = z.object({
|
|
56
|
-
name:
|
|
566
|
+
name: identifier2,
|
|
57
567
|
values: z.array(z.string().min(1).max(63)).min(1).max(100)
|
|
58
568
|
});
|
|
59
569
|
var canonicalColumnSchema = z.object({
|
|
60
570
|
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
61
571
|
name: z.string().min(1).max(63),
|
|
62
|
-
columnName:
|
|
63
|
-
type: z.union([columnTypeSchema, z.object({ enum:
|
|
572
|
+
columnName: columnIdentifier,
|
|
573
|
+
type: z.union([columnTypeSchema, z.object({ enum: identifier2 })]),
|
|
64
574
|
nullable: z.boolean().default(true),
|
|
65
575
|
unique: z.boolean().default(false),
|
|
66
576
|
/** Literal default, or one of the allowlisted SQL functions. */
|
|
67
577
|
default: z.union([z.string(), z.number(), z.boolean(), z.null()]).optional(),
|
|
68
578
|
defaultFn: z.enum(["gen_random_uuid()", "now()"]).optional(),
|
|
579
|
+
/**
|
|
580
|
+
* Stamp this column from the database clock on UPDATE (`now()`). The
|
|
581
|
+
* column stays insertable (defaults fill gaps) but is omitted from
|
|
582
|
+
* SetInput / settable so callers cannot backdate it.
|
|
583
|
+
*/
|
|
584
|
+
onUpdateFn: z.enum(["now()"]).optional(),
|
|
69
585
|
description: z.string().max(500).optional(),
|
|
70
586
|
/** Migration hints — same semantics as v1. */
|
|
71
|
-
renamedFrom:
|
|
587
|
+
renamedFrom: columnIdentifier.optional(),
|
|
72
588
|
cast: z.string().max(200).optional(),
|
|
73
|
-
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()
|
|
597
|
+
}).superRefine((column, ctx) => {
|
|
598
|
+
if (column.onUpdateFn === void 0) return;
|
|
599
|
+
if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
|
|
600
|
+
ctx.addIssue({
|
|
601
|
+
code: "custom",
|
|
602
|
+
path: ["onUpdateFn"],
|
|
603
|
+
message: `onUpdateFn '${column.onUpdateFn}' is only valid on timestamptz/date columns`
|
|
604
|
+
});
|
|
605
|
+
}
|
|
74
606
|
});
|
|
75
607
|
var canonicalLinkSchema = z.object({
|
|
76
608
|
name: z.string().min(1).max(63),
|
|
77
609
|
kind: z.enum(["toOne", "toMany", "toPlatformUser"]),
|
|
78
610
|
/** Target table name (namespace-local). Absent for toPlatformUser. */
|
|
79
|
-
target:
|
|
611
|
+
target: identifier2.optional(),
|
|
80
612
|
/** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
|
|
81
|
-
on:
|
|
613
|
+
on: columnIdentifier,
|
|
82
614
|
/** Referenced column on the target (default: target's PK / platform user `sub`). */
|
|
83
|
-
references:
|
|
615
|
+
references: columnIdentifier.optional(),
|
|
84
616
|
/** enforce: true emits a real FK constraint (same-namespace toOne only). */
|
|
85
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(),
|
|
86
620
|
description: z.string().max(500).optional()
|
|
87
621
|
});
|
|
88
622
|
var canonicalIndexSchema = z.object({
|
|
89
|
-
columns: z.array(
|
|
623
|
+
columns: z.array(columnIdentifier).min(1),
|
|
90
624
|
unique: z.boolean().default(false),
|
|
91
|
-
name:
|
|
625
|
+
name: identifier2.optional()
|
|
92
626
|
});
|
|
93
|
-
var
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
allow: z.literal(true).optional(),
|
|
98
|
-
policy: z.string().min(1).max(2e3).optional()
|
|
99
|
-
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
100
|
-
message: "A variant declares exactly one of allow: true or policy"
|
|
101
|
-
});
|
|
102
|
-
var policyExpressionSchema = z.union([
|
|
103
|
-
z.string().min(1).max(2e3),
|
|
104
|
-
z.object({
|
|
105
|
-
default: z.string().min(1).max(2e3),
|
|
106
|
-
variants: z.array(policyVariantSchema).max(10).default([])
|
|
107
|
-
})
|
|
108
|
-
]);
|
|
627
|
+
var canonicalCheckSchema = z.object({
|
|
628
|
+
name: identifier2,
|
|
629
|
+
expression: z.string().min(1).max(2e3)
|
|
630
|
+
}).strict();
|
|
109
631
|
var canonicalPoliciesSchema = z.object({
|
|
110
632
|
read: policyExpressionSchema.optional(),
|
|
111
633
|
write: policyExpressionSchema.optional(),
|
|
@@ -293,7 +815,7 @@ var VIEW_QUERY_FUNCTION_ALLOWLIST = [
|
|
|
293
815
|
"width_bucket"
|
|
294
816
|
];
|
|
295
817
|
var canonicalActionSchema = z.object({
|
|
296
|
-
name:
|
|
818
|
+
name: identifier2,
|
|
297
819
|
requires: z.string().max(1e3).optional(),
|
|
298
820
|
params: z.record(z.string(), z.object({
|
|
299
821
|
type: columnTypeSchema,
|
|
@@ -305,9 +827,9 @@ var canonicalActionSchema = z.object({
|
|
|
305
827
|
});
|
|
306
828
|
var canonicalCowMutationSchema = z.object({
|
|
307
829
|
kind: z.enum(["override", "revert"]),
|
|
308
|
-
name:
|
|
830
|
+
name: identifier2,
|
|
309
831
|
/** Synced table (same namespace) the overlay copies from / falls back to. */
|
|
310
|
-
synced:
|
|
832
|
+
synced: identifier2,
|
|
311
833
|
/** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
|
|
312
834
|
pk: z.string().min(1).max(63),
|
|
313
835
|
/** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
|
|
@@ -334,17 +856,28 @@ var canonicalCowMutationSchema = z.object({
|
|
|
334
856
|
}
|
|
335
857
|
}
|
|
336
858
|
});
|
|
859
|
+
var canonicalForeignKeySchema = z.object({
|
|
860
|
+
name: identifier2,
|
|
861
|
+
columns: z.array(columnIdentifier).min(1).max(8),
|
|
862
|
+
target: identifier2,
|
|
863
|
+
references: z.array(columnIdentifier).min(1).max(8),
|
|
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()
|
|
867
|
+
}).strict();
|
|
337
868
|
var canonicalTableSchema = z.object({
|
|
338
|
-
table:
|
|
869
|
+
table: identifier2,
|
|
339
870
|
description: z.string().max(2e3).optional(),
|
|
340
871
|
/** Table-level rename hint (new in v2). */
|
|
341
|
-
renamedFrom:
|
|
872
|
+
renamedFrom: identifier2.optional(),
|
|
342
873
|
/** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
|
|
343
874
|
source: z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
|
|
344
875
|
columns: z.array(canonicalColumnSchema).default([]),
|
|
345
|
-
primaryKey: z.array(
|
|
876
|
+
primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
|
|
346
877
|
links: z.array(canonicalLinkSchema).default([]),
|
|
347
878
|
indexes: z.array(canonicalIndexSchema).default([]),
|
|
879
|
+
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
880
|
+
checks: z.array(canonicalCheckSchema).max(100).optional(),
|
|
348
881
|
policies: canonicalPoliciesSchema.default({}),
|
|
349
882
|
/**
|
|
350
883
|
* v1-conversion carry-over only. v2 has no action surface — the runtime
|
|
@@ -353,26 +886,64 @@ var canonicalTableSchema = z.object({
|
|
|
353
886
|
*/
|
|
354
887
|
actions: z.array(canonicalActionSchema).default([]),
|
|
355
888
|
/** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
|
|
356
|
-
cowMutations: z.array(canonicalCowMutationSchema).optional()
|
|
889
|
+
cowMutations: z.array(canonicalCowMutationSchema).optional(),
|
|
890
|
+
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
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()
|
|
357
912
|
});
|
|
913
|
+
var MAX_VIEW_QUERY = 1e5;
|
|
914
|
+
var MAX_RESOLVED_VIEW_QUERY = 5e6;
|
|
358
915
|
var canonicalViewSchema = z.object({
|
|
359
|
-
view:
|
|
916
|
+
view: identifier2,
|
|
360
917
|
description: z.string().max(2e3).optional(),
|
|
361
918
|
columns: z.array(canonicalColumnSchema).min(1),
|
|
362
919
|
/** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
|
|
363
|
-
primaryKey: z.array(
|
|
920
|
+
primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
|
|
364
921
|
links: z.array(canonicalLinkSchema).default([]),
|
|
365
922
|
policies: z.object({ read: policyExpressionSchema }).strict(),
|
|
366
|
-
query: z.string().min(1).max(
|
|
923
|
+
query: z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
|
|
367
924
|
message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
|
|
368
925
|
}),
|
|
369
926
|
/** Namespace-local logical relation names, sorted for deterministic hashing. */
|
|
370
|
-
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()
|
|
371
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
|
+
}
|
|
372
940
|
for (const [index3, column] of view.columns.entries()) {
|
|
373
941
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
374
942
|
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare a default` });
|
|
375
943
|
}
|
|
944
|
+
if (column.onUpdateFn !== void 0) {
|
|
945
|
+
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare onUpdateFn` });
|
|
946
|
+
}
|
|
376
947
|
if (column.unique) {
|
|
377
948
|
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare unique` });
|
|
378
949
|
}
|
|
@@ -380,15 +951,82 @@ var canonicalViewSchema = z.object({
|
|
|
380
951
|
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare migration hints` });
|
|
381
952
|
}
|
|
382
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
|
+
}
|
|
383
959
|
for (const [index3, link] of view.links.entries()) {
|
|
384
960
|
if (link.enforce) {
|
|
385
961
|
ctx.addIssue({ code: "custom", path: ["links", index3, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
|
|
386
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
|
+
}
|
|
387
966
|
}
|
|
388
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
|
+
}
|
|
389
1027
|
var canonicalDefinitionSchema = z.object({
|
|
390
1028
|
definition_format: z.literal(CANONICAL_FORMAT),
|
|
391
|
-
namespace:
|
|
1029
|
+
namespace: identifier2.max(41),
|
|
392
1030
|
description: z.string().max(2e3).optional(),
|
|
393
1031
|
enums: z.array(canonicalEnumSchema).default([]),
|
|
394
1032
|
tables: z.array(canonicalTableSchema).min(0).max(500),
|
|
@@ -406,6 +1044,74 @@ var canonicalDefinitionSchema = z.object({
|
|
|
406
1044
|
ctx.addIssue({ code: "custom", path: ["tables", index3, "table"], message: `duplicate relation '${table.table}'` });
|
|
407
1045
|
}
|
|
408
1046
|
seen.add(table.table);
|
|
1047
|
+
validateCellMetadata(table, index3, ctx);
|
|
1048
|
+
if (table.source !== void 0) {
|
|
1049
|
+
for (const [columnIndex, column] of table.columns.entries()) {
|
|
1050
|
+
if (column.onUpdateFn !== void 0) {
|
|
1051
|
+
ctx.addIssue({
|
|
1052
|
+
code: "custom",
|
|
1053
|
+
path: ["tables", index3, "columns", columnIndex],
|
|
1054
|
+
message: `synced table '${table.table}' column '${column.name}' cannot declare onUpdateFn`
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
1061
|
+
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
1062
|
+
const names = /* @__PURE__ */ new Set([
|
|
1063
|
+
...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
|
|
1064
|
+
...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
|
|
1065
|
+
...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
|
|
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
|
+
}
|
|
1087
|
+
for (const [index3, fk] of (table.foreignKeys ?? []).entries()) {
|
|
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}'`);
|
|
1090
|
+
names.add(fk.name);
|
|
1091
|
+
const target = fkTables.get(fk.target);
|
|
1092
|
+
if (table.source || !target || target.source) {
|
|
1093
|
+
fail3("foreign keys must connect owned tables in the same namespace");
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
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");
|
|
1098
|
+
for (const [position, name] of fk.columns.entries()) {
|
|
1099
|
+
const sourceColumn = table.columns.find((column) => column.columnName === name);
|
|
1100
|
+
const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
|
|
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");
|
|
1106
|
+
}
|
|
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])];
|
|
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 });
|
|
1114
|
+
}
|
|
409
1115
|
}
|
|
410
1116
|
const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
411
1117
|
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
@@ -501,7 +1207,7 @@ var canonicalDefinitionSchema = z.object({
|
|
|
501
1207
|
const visiting = /* @__PURE__ */ new Set();
|
|
502
1208
|
const visited = /* @__PURE__ */ new Set();
|
|
503
1209
|
let cycleReported = false;
|
|
504
|
-
const
|
|
1210
|
+
const visit2 = (name, path) => {
|
|
505
1211
|
if (visited.has(name) || cycleReported) return;
|
|
506
1212
|
if (visiting.has(name)) {
|
|
507
1213
|
cycleReported = true;
|
|
@@ -515,12 +1221,12 @@ var canonicalDefinitionSchema = z.object({
|
|
|
515
1221
|
visiting.add(name);
|
|
516
1222
|
const view = viewByName.get(name);
|
|
517
1223
|
for (const dependency of view?.dependencies ?? []) {
|
|
518
|
-
if (viewByName.has(dependency))
|
|
1224
|
+
if (viewByName.has(dependency)) visit2(dependency, [...path, name]);
|
|
519
1225
|
}
|
|
520
1226
|
visiting.delete(name);
|
|
521
1227
|
visited.add(name);
|
|
522
1228
|
};
|
|
523
|
-
for (const view of views)
|
|
1229
|
+
for (const view of views) visit2(view.view, []);
|
|
524
1230
|
});
|
|
525
1231
|
function canonicalRelations(definition) {
|
|
526
1232
|
const views = (definition.views ?? []).map((view) => ({
|
|
@@ -530,8 +1236,12 @@ function canonicalRelations(definition) {
|
|
|
530
1236
|
...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
|
|
531
1237
|
links: view.links,
|
|
532
1238
|
indexes: [],
|
|
533
|
-
|
|
534
|
-
|
|
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 } : {}
|
|
535
1245
|
}));
|
|
536
1246
|
return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
|
|
537
1247
|
}
|
|
@@ -541,12 +1251,14 @@ function normalizeCanonical(definition) {
|
|
|
541
1251
|
...definition,
|
|
542
1252
|
enums: sortByName(definition.enums),
|
|
543
1253
|
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
|
|
544
|
-
const { cowMutations, ...rest } = t2;
|
|
1254
|
+
const { cowMutations, foreignKeys, checks, ...rest } = t2;
|
|
545
1255
|
return {
|
|
546
1256
|
...rest,
|
|
547
1257
|
links: sortByName(t2.links),
|
|
548
1258
|
indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
|
|
549
1259
|
actions: sortByName(t2.actions),
|
|
1260
|
+
...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
|
|
1261
|
+
...checks?.length ? { checks: sortByName(checks) } : {},
|
|
550
1262
|
// Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
|
|
551
1263
|
...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
|
|
552
1264
|
};
|
|
@@ -561,25 +1273,6 @@ function normalizeCanonical(definition) {
|
|
|
561
1273
|
}
|
|
562
1274
|
return normalized;
|
|
563
1275
|
}
|
|
564
|
-
|
|
565
|
-
// src/constants.ts
|
|
566
|
-
var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
|
567
|
-
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
568
|
-
var MAX_IDENTIFIER_LENGTH = 63;
|
|
569
|
-
var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
|
|
570
|
-
var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
571
|
-
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
|
|
572
|
-
var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
|
|
573
|
-
var ORM_SCHEMA_PREFIX = "orm_";
|
|
574
|
-
var CONSUMER_ROLE_SUFFIX = "_consumer";
|
|
575
|
-
var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
|
|
576
|
-
var consumerRoleName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}${CONSUMER_ROLE_SUFFIX}`;
|
|
577
|
-
var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
|
|
578
|
-
var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
|
|
579
|
-
var HASH_DOMAIN = "sequence-orm-definitions\0";
|
|
580
|
-
var MAX_DEFINITION_FILE_BYTES = 1024 * 1024;
|
|
581
|
-
var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
|
|
582
|
-
var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
|
|
583
1276
|
function stableJson(value) {
|
|
584
1277
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
585
1278
|
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
@@ -852,8 +1545,19 @@ function isSeqNamespaceConfig(value) {
|
|
|
852
1545
|
return typeof value === "object" && value !== null && value.kind === NAMESPACE_CONFIG_BRAND;
|
|
853
1546
|
}
|
|
854
1547
|
function serializeNamespace(input) {
|
|
855
|
-
const { config
|
|
856
|
-
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
|
+
}
|
|
857
1561
|
const enums = input.enums ?? [];
|
|
858
1562
|
const tablesByName = /* @__PURE__ */ new Map();
|
|
859
1563
|
for (const table of tables) {
|
|
@@ -955,6 +1659,9 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
955
1659
|
if (table.config.renamedFrom !== void 0) candidate.renamedFrom = table.config.renamedFrom;
|
|
956
1660
|
if (table.source !== void 0) candidate.source = table.source;
|
|
957
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
|
+
}
|
|
958
1665
|
if (table.config.indexes !== void 0 && table.config.indexes.length > 0) {
|
|
959
1666
|
throw new OrmDefinitionError(
|
|
960
1667
|
`synced table "${table.tableName}" is system-owned; indexes are not allowed`,
|
|
@@ -992,6 +1699,11 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
992
1699
|
if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
|
|
993
1700
|
candidate.columns?.push(column);
|
|
994
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
|
+
}
|
|
995
1707
|
if (table.config.primaryKey !== void 0) {
|
|
996
1708
|
if (columnLevelPk.length > 0) {
|
|
997
1709
|
throw new OrmDefinitionError(
|
|
@@ -1005,6 +1717,45 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1005
1717
|
} else if (columnLevelPk.length > 0) {
|
|
1006
1718
|
candidate.primaryKey = columnLevelPk;
|
|
1007
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
|
+
}
|
|
1738
|
+
if (table.config.foreignKeys?.length) {
|
|
1739
|
+
if (table.source) throw new OrmDefinitionError(`synced table "${table.tableName}" cannot enforce foreign keys`, table.file);
|
|
1740
|
+
candidate.foreignKeys = table.config.foreignKeys.map((fk) => {
|
|
1741
|
+
const target = resolveRelationRef({ ref: fk.target, fail: (message) => {
|
|
1742
|
+
throw new OrmDefinitionError(message, table.file);
|
|
1743
|
+
} });
|
|
1744
|
+
if (!isSeqTable(target) || !relationsByName.has(target.tableName)) throw new OrmDefinitionError("foreign key target must be a table in this namespace", table.file);
|
|
1745
|
+
return {
|
|
1746
|
+
name: fk.name,
|
|
1747
|
+
columns: fk.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: `foreign key "${fk.name}" column` })),
|
|
1748
|
+
target: target.tableName,
|
|
1749
|
+
references: fk.references.map((ref) => resolveColumnRef({ relation: target, ref, role: `foreign key "${fk.name}" reference` })),
|
|
1750
|
+
...fk.deferrable ? { deferrable: fk.deferrable } : {},
|
|
1751
|
+
...fk.onDelete ? { onDelete: fk.onDelete } : {},
|
|
1752
|
+
...fk.onUpdate ? { onUpdate: fk.onUpdate } : {}
|
|
1753
|
+
};
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
if (table.config.checks?.length) {
|
|
1757
|
+
candidate.checks = table.config.checks.map((check) => ({ name: check.name, expression: check.expression }));
|
|
1758
|
+
}
|
|
1008
1759
|
for (const index3 of table.config.indexes ?? []) {
|
|
1009
1760
|
const entry = {
|
|
1010
1761
|
columns: index3.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
|
|
@@ -1057,6 +1808,12 @@ function buildCanonicalView({
|
|
|
1057
1808
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1058
1809
|
throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare a default`, view.file);
|
|
1059
1810
|
}
|
|
1811
|
+
if (column.onUpdateFn !== void 0) {
|
|
1812
|
+
throw new OrmDefinitionError(
|
|
1813
|
+
`view "${view.viewName}": column "${propertyName}" cannot declare onUpdateFn`,
|
|
1814
|
+
view.file
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1060
1817
|
if (column.unique) {
|
|
1061
1818
|
throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare unique`, view.file);
|
|
1062
1819
|
}
|
|
@@ -1094,7 +1851,33 @@ function buildCanonicalView({
|
|
|
1094
1851
|
columns,
|
|
1095
1852
|
...primaryKey2 !== void 0 ? { primaryKey: primaryKey2 } : {},
|
|
1096
1853
|
links,
|
|
1097
|
-
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
|
|
1098
1881
|
};
|
|
1099
1882
|
}
|
|
1100
1883
|
function buildCanonicalLink({
|
|
@@ -1105,14 +1888,17 @@ function buildCanonicalLink({
|
|
|
1105
1888
|
}) {
|
|
1106
1889
|
const ownerKind = relationKind(relation);
|
|
1107
1890
|
const ownerName = relationName(relation);
|
|
1108
|
-
const
|
|
1891
|
+
const fail3 = (message) => {
|
|
1109
1892
|
throw new OrmDefinitionError(`${ownerKind} "${ownerName}": link "${linkName}" ${message}`, relation.file);
|
|
1110
1893
|
};
|
|
1111
1894
|
if (link.enforce && link.linkKind !== "toOne") {
|
|
1112
|
-
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");
|
|
1113
1899
|
}
|
|
1114
1900
|
if (link.linkKind === "toPlatformUser") {
|
|
1115
|
-
if (link.target !== void 0) return
|
|
1901
|
+
if (link.target !== void 0) return fail3("is toPlatformUser and must not declare a target table");
|
|
1116
1902
|
return {
|
|
1117
1903
|
name: linkName,
|
|
1118
1904
|
kind: "toPlatformUser",
|
|
@@ -1122,13 +1908,13 @@ function buildCanonicalLink({
|
|
|
1122
1908
|
...link.description !== void 0 ? { description: link.description } : {}
|
|
1123
1909
|
};
|
|
1124
1910
|
}
|
|
1125
|
-
const target = resolveRelationRef({ ref: link.target, fail:
|
|
1911
|
+
const target = resolveRelationRef({ ref: link.target, fail: fail3 });
|
|
1126
1912
|
const targetName = relationName(target);
|
|
1127
1913
|
if (!relationsByName.has(targetName)) {
|
|
1128
|
-
return
|
|
1914
|
+
return fail3(`targets ${relationKind(target)} "${targetName}" which is not part of this namespace`);
|
|
1129
1915
|
}
|
|
1130
1916
|
if (link.enforce && (!isSeqTable(relation) || !isSeqTable(target))) {
|
|
1131
|
-
return
|
|
1917
|
+
return fail3("cannot enforce a PostgreSQL foreign key involving a view");
|
|
1132
1918
|
}
|
|
1133
1919
|
const owningSide = link.linkKind === "toOne" ? relation : target;
|
|
1134
1920
|
const referencedSide = link.linkKind === "toOne" ? target : relation;
|
|
@@ -1139,22 +1925,24 @@ function buildCanonicalLink({
|
|
|
1139
1925
|
on: resolveColumnRef({ relation: owningSide, ref: link.on, role: `link "${linkName}" on` }),
|
|
1140
1926
|
...link.references !== void 0 ? { references: resolveColumnRef({ relation: referencedSide, ref: link.references, role: `link "${linkName}" references` }) } : {},
|
|
1141
1927
|
enforce: link.enforce,
|
|
1928
|
+
...link.onDelete !== void 0 ? { onDelete: link.onDelete } : {},
|
|
1929
|
+
...link.onUpdate !== void 0 ? { onUpdate: link.onUpdate } : {},
|
|
1142
1930
|
...link.description !== void 0 ? { description: link.description } : {}
|
|
1143
1931
|
};
|
|
1144
1932
|
}
|
|
1145
|
-
function resolveRelationRef({ ref, fail:
|
|
1146
|
-
if (ref === void 0) return
|
|
1933
|
+
function resolveRelationRef({ ref, fail: fail3 }) {
|
|
1934
|
+
if (ref === void 0) return fail3("declares no target relation");
|
|
1147
1935
|
if (typeof ref !== "function") return ref;
|
|
1148
1936
|
let resolved;
|
|
1149
1937
|
try {
|
|
1150
1938
|
resolved = ref();
|
|
1151
1939
|
} catch (error) {
|
|
1152
|
-
return
|
|
1940
|
+
return fail3(
|
|
1153
1941
|
`target thunk threw (${error instanceof Error ? error.message : String(error)}) \u2014 lazy refs resolve at serialize time; make sure the target module ran`
|
|
1154
1942
|
);
|
|
1155
1943
|
}
|
|
1156
1944
|
if (!isSeqTable(resolved) && !isSeqView(resolved)) {
|
|
1157
|
-
return
|
|
1945
|
+
return fail3("target thunk did not return a relation defined with defineTable/defineView");
|
|
1158
1946
|
}
|
|
1159
1947
|
return resolved;
|
|
1160
1948
|
}
|
|
@@ -1221,6 +2009,10 @@ async function loadNamespaceFromDir(dir, options = {}) {
|
|
|
1221
2009
|
fsCache: false,
|
|
1222
2010
|
...options.ormModulePath !== void 0 ? {
|
|
1223
2011
|
alias: {
|
|
2012
|
+
// The public /drizzle barrel re-exports this pinned dependency.
|
|
2013
|
+
// Explicitly map it before the root alias: otherwise jiti appends
|
|
2014
|
+
// "/drizzle" to the overridden module's filename.
|
|
2015
|
+
"@sequenceholdings/orm/drizzle": createRequire(import.meta.url).resolve("drizzle-orm"),
|
|
1224
2016
|
"@sequenceholdings/orm": options.ormModulePath,
|
|
1225
2017
|
"@sequenceholdings/orm/authoring": options.ormModulePath
|
|
1226
2018
|
}
|
|
@@ -1434,6 +2226,7 @@ var ORDER_BY_ENUM_VALUES = [
|
|
|
1434
2226
|
"DESC_NULLS_LAST"
|
|
1435
2227
|
];
|
|
1436
2228
|
var PLATFORM_USER_TYPENAME = "PlatformUser";
|
|
2229
|
+
var DATE_BUCKET_ENUM = "DateBucket";
|
|
1437
2230
|
function tableNames(table) {
|
|
1438
2231
|
const root = camelCase(table);
|
|
1439
2232
|
const rootPascal = pascalCase(table);
|
|
@@ -1449,6 +2242,7 @@ function tableNames(table) {
|
|
|
1449
2242
|
updateByPkRoot: `update${rootPascal}ByPk`,
|
|
1450
2243
|
deleteRoot: `delete${rootPascal}`,
|
|
1451
2244
|
deleteByPkRoot: `delete${rootPascal}ByPk`,
|
|
2245
|
+
revertByPkRoot: `revert${rootPascal}ByPk`,
|
|
1452
2246
|
boolExp: `${type}BoolExp`,
|
|
1453
2247
|
orderBy: `${type}OrderBy`,
|
|
1454
2248
|
selectColumn: `${type}SelectColumn`,
|
|
@@ -1466,9 +2260,25 @@ function tableNames(table) {
|
|
|
1466
2260
|
sumFields: `${type}SumFields`,
|
|
1467
2261
|
avgFields: `${type}AvgFields`,
|
|
1468
2262
|
aggregateBoolExp: `${type}AggregateBoolExp`,
|
|
1469
|
-
aggregateBoolExpCount: `${type}AggregateBoolExpCount
|
|
2263
|
+
aggregateBoolExpCount: `${type}AggregateBoolExpCount`,
|
|
2264
|
+
aggregateGroupsRoot: `${root}AggregateGroups`,
|
|
2265
|
+
groupByColumnEnum: `${type}GroupByColumn`,
|
|
2266
|
+
groupByInput: `${type}GroupBy`,
|
|
2267
|
+
groupKey: `${type}GroupKey`,
|
|
2268
|
+
aggregateGroup: `${type}AggregateGroup`
|
|
1470
2269
|
};
|
|
1471
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
|
+
}
|
|
1472
2282
|
|
|
1473
2283
|
// src/codegen-gql/type-policies.ts
|
|
1474
2284
|
function generateTypePolicies(canonical) {
|
|
@@ -1476,13 +2286,13 @@ function generateTypePolicies(canonical) {
|
|
|
1476
2286
|
const entries = [];
|
|
1477
2287
|
let hasPlatformUser = false;
|
|
1478
2288
|
for (const table of canonicalRelations(def)) {
|
|
1479
|
-
if (table.source !== void 0) continue;
|
|
2289
|
+
if (table.source !== void 0 || table.internal === true) continue;
|
|
1480
2290
|
if (table.links.some((link) => link.kind === "toPlatformUser")) hasPlatformUser = true;
|
|
1481
2291
|
if (!table.primaryKey || table.primaryKey.length === 0) continue;
|
|
1482
|
-
const keyFields = table.primaryKey.map((
|
|
1483
|
-
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);
|
|
1484
2294
|
if (!column) {
|
|
1485
|
-
throw new CodegenGqlError(`Primary key column "${
|
|
2295
|
+
throw new CodegenGqlError(`Primary key column "${identifier3}" not found on relation "${table.table}".`);
|
|
1486
2296
|
}
|
|
1487
2297
|
return fieldName(column.name);
|
|
1488
2298
|
});
|
|
@@ -1499,7 +2309,100 @@ ${body}
|
|
|
1499
2309
|
}
|
|
1500
2310
|
`;
|
|
1501
2311
|
}
|
|
1502
|
-
|
|
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
|
+
}
|
|
2394
|
+
var IR_VERSION = 2;
|
|
2395
|
+
var LEGACY_IR_VERSION = 1;
|
|
2396
|
+
var irVersionSchema = z.union([z.literal(LEGACY_IR_VERSION), z.literal(IR_VERSION)]);
|
|
2397
|
+
var operationVersionSchema = z.object({ irVersion: irVersionSchema });
|
|
2398
|
+
function requiredArtifactIrVersion(operations) {
|
|
2399
|
+
let required = LEGACY_IR_VERSION;
|
|
2400
|
+
for (const operation of operations) {
|
|
2401
|
+
const { irVersion } = operationVersionSchema.parse(operation);
|
|
2402
|
+
if (irVersion > required) required = irVersion;
|
|
2403
|
+
}
|
|
2404
|
+
return required;
|
|
2405
|
+
}
|
|
1503
2406
|
var CLAIMS_CONTRACT = {
|
|
1504
2407
|
sub: "string",
|
|
1505
2408
|
org: "string?",
|
|
@@ -1571,6 +2474,21 @@ var selectionIrSchema = z.lazy(
|
|
|
1571
2474
|
() => z.object({
|
|
1572
2475
|
typename: z.string(),
|
|
1573
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(),
|
|
1574
2492
|
relations: z.array(z.object({
|
|
1575
2493
|
alias: z.string(),
|
|
1576
2494
|
link: z.string(),
|
|
@@ -1592,6 +2510,11 @@ var selectionIrSchema = z.lazy(
|
|
|
1592
2510
|
})
|
|
1593
2511
|
);
|
|
1594
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
|
+
});
|
|
1595
2518
|
var selectRootSchema = z.object({
|
|
1596
2519
|
kind: z.literal("select"),
|
|
1597
2520
|
alias: z.string(),
|
|
@@ -1603,7 +2526,8 @@ var selectRootSchema = z.object({
|
|
|
1603
2526
|
distinctOn: z.union([z.array(z.string()), z.object({ slot: z.string() })]).optional(),
|
|
1604
2527
|
limit: limitIrSchema,
|
|
1605
2528
|
offset: z.object({ variable: z.string().optional(), value: z.number().int().optional() }).optional(),
|
|
1606
|
-
includeIf: includeIfSchema.optional()
|
|
2529
|
+
includeIf: includeIfSchema.optional(),
|
|
2530
|
+
prefetch: z.array(prefetchPlanIrSchema).optional()
|
|
1607
2531
|
});
|
|
1608
2532
|
var byPkRootSchema = z.object({
|
|
1609
2533
|
kind: z.literal("byPk"),
|
|
@@ -1613,7 +2537,8 @@ var byPkRootSchema = z.object({
|
|
|
1613
2537
|
pk: z.array(z.object({ column: z.string(), value: valueIrSchema })).min(1),
|
|
1614
2538
|
/** Policy predicate — ANDed with the PK match. */
|
|
1615
2539
|
where: predIrSchema,
|
|
1616
|
-
includeIf: includeIfSchema.optional()
|
|
2540
|
+
includeIf: includeIfSchema.optional(),
|
|
2541
|
+
prefetch: z.array(prefetchPlanIrSchema).optional()
|
|
1617
2542
|
});
|
|
1618
2543
|
var aggregateRootSchema = z.object({
|
|
1619
2544
|
kind: z.literal("aggregate"),
|
|
@@ -1626,6 +2551,33 @@ var aggregateRootSchema = z.object({
|
|
|
1626
2551
|
nodes: selectionIrSchema.optional(),
|
|
1627
2552
|
includeIf: includeIfSchema.optional()
|
|
1628
2553
|
});
|
|
2554
|
+
var DATE_BUCKETS = ["year", "quarter", "month", "week", "day", "hour"];
|
|
2555
|
+
var groupByKeyIrSchema = z.object({
|
|
2556
|
+
column: z.string(),
|
|
2557
|
+
bucket: z.enum(DATE_BUCKETS).optional()
|
|
2558
|
+
});
|
|
2559
|
+
var aggregateGroupsRootSchema = z.object({
|
|
2560
|
+
kind: z.literal("aggregateGroups"),
|
|
2561
|
+
alias: z.string(),
|
|
2562
|
+
table: z.string(),
|
|
2563
|
+
/** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
|
|
2564
|
+
where: predIrSchema,
|
|
2565
|
+
groupBy: z.array(groupByKeyIrSchema).min(1),
|
|
2566
|
+
/**
|
|
2567
|
+
* The `key { … }` projection: which group columns appear in each entry's key
|
|
2568
|
+
* object, in selection order. A subset of `groupBy` (the compiler rejects a
|
|
2569
|
+
* key field that is not a group key — it would be indeterminate within the
|
|
2570
|
+
* group). Empty when the document selects only `aggregate`.
|
|
2571
|
+
*/
|
|
2572
|
+
key: z.array(z.object({ alias: z.string(), column: z.string() })).default([]),
|
|
2573
|
+
count: z.object({ columns: z.array(z.string()).optional(), distinct: z.boolean().optional() }).optional(),
|
|
2574
|
+
sum: z.array(z.string()).optional(),
|
|
2575
|
+
avg: z.array(z.string()).optional(),
|
|
2576
|
+
/** Bounds the GROUP count, not the scanned rows — see MAX_GROUPS. */
|
|
2577
|
+
limit: limitIrSchema,
|
|
2578
|
+
offset: z.object({ variable: z.string().optional(), value: z.number().int().optional() }).optional(),
|
|
2579
|
+
includeIf: includeIfSchema.optional()
|
|
2580
|
+
});
|
|
1629
2581
|
var writeValuesSchema = z.union([
|
|
1630
2582
|
z.object({ slot: z.string() }),
|
|
1631
2583
|
z.object({ entries: z.array(z.object({ column: z.string(), value: valueIrSchema })) })
|
|
@@ -1644,7 +2596,20 @@ var mutationRootSchema = z.object({
|
|
|
1644
2596
|
* coalesce view falls back to synced. Both are gated on a matching synced
|
|
1645
2597
|
* row and carry the write policy in `where` like update/delete.
|
|
1646
2598
|
*/
|
|
1647
|
-
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(),
|
|
1648
2613
|
alias: z.string(),
|
|
1649
2614
|
table: z.string(),
|
|
1650
2615
|
/** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
|
|
@@ -1671,12 +2636,15 @@ var mutationRootSchema = z.object({
|
|
|
1671
2636
|
insertCheck: predIrSchema.optional(),
|
|
1672
2637
|
returning: selectionIrSchema.nullable(),
|
|
1673
2638
|
affectedRows: z.boolean().default(false),
|
|
2639
|
+
/** v2: abort the transaction when the actual write count differs, regardless of selection. */
|
|
2640
|
+
assertAffectedRows: z.number().int().min(0).max(2147483647).optional(),
|
|
1674
2641
|
includeIf: includeIfSchema.optional()
|
|
1675
2642
|
});
|
|
1676
2643
|
var rootIrSchema = z.discriminatedUnion("kind", [
|
|
1677
2644
|
selectRootSchema,
|
|
1678
2645
|
byPkRootSchema,
|
|
1679
2646
|
aggregateRootSchema,
|
|
2647
|
+
aggregateGroupsRootSchema,
|
|
1680
2648
|
mutationRootSchema
|
|
1681
2649
|
]);
|
|
1682
2650
|
var INPUT_REF_KINDS = [
|
|
@@ -1706,7 +2674,7 @@ var variableIrSchema = z.object({
|
|
|
1706
2674
|
defaultValue: variableDefaultIrSchema.optional()
|
|
1707
2675
|
});
|
|
1708
2676
|
var compiledOperationSchema = z.object({
|
|
1709
|
-
irVersion:
|
|
2677
|
+
irVersion: irVersionSchema,
|
|
1710
2678
|
name: z.string(),
|
|
1711
2679
|
opHash: z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
1712
2680
|
kind: z.enum(["query", "mutation"]),
|
|
@@ -1718,6 +2686,50 @@ var compiledOperationSchema = z.object({
|
|
|
1718
2686
|
/** Every table the op touches — data-token scoping + audit. */
|
|
1719
2687
|
tableSet: z.array(z.string()).min(1),
|
|
1720
2688
|
roots: z.array(rootIrSchema).min(1)
|
|
2689
|
+
}).superRefine((operation, ctx) => {
|
|
2690
|
+
for (const [index3, root] of operation.roots.entries()) {
|
|
2691
|
+
if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
|
|
2692
|
+
if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
|
|
2693
|
+
ctx.addIssue({ code: "custom", path: ["roots", index3], message: "affected-row assertions require unconditional mutation roots and IR v2" });
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
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()
|
|
1721
2733
|
});
|
|
1722
2734
|
var tableMetaIrSchema = z.object({
|
|
1723
2735
|
table: z.string(),
|
|
@@ -1754,18 +2766,45 @@ var tableMetaIrSchema = z.object({
|
|
|
1754
2766
|
insertable: z.array(z.string()),
|
|
1755
2767
|
settable: z.array(z.string()),
|
|
1756
2768
|
appendable: z.array(z.string()),
|
|
2769
|
+
/**
|
|
2770
|
+
* Logical columns stamped with Postgres `now()` on UPDATE (canonical
|
|
2771
|
+
* `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
|
|
2772
|
+
*/
|
|
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(),
|
|
1757
2790
|
constraints: z.array(z.object({ name: z.string(), columns: z.array(z.string()) })),
|
|
1758
2791
|
/** Depth/node caps for dynamic filter trees on this table. */
|
|
1759
2792
|
dynamicFilterMaxDepth: z.number().int().positive().default(6),
|
|
1760
2793
|
dynamicFilterMaxNodes: z.number().int().positive().default(64)
|
|
1761
2794
|
});
|
|
1762
2795
|
var compiledArtifactSchema = z.object({
|
|
1763
|
-
irVersion:
|
|
2796
|
+
irVersion: irVersionSchema,
|
|
1764
2797
|
namespace: z.string(),
|
|
1765
2798
|
schemaHash: z.string(),
|
|
1766
2799
|
opsHash: z.string(),
|
|
1767
2800
|
tables: z.record(z.string(), tableMetaIrSchema),
|
|
1768
2801
|
operations: z.record(z.string(), compiledOperationSchema)
|
|
2802
|
+
}).superRefine((artifact, ctx) => {
|
|
2803
|
+
for (const [name, operation] of Object.entries(artifact.operations)) {
|
|
2804
|
+
if (operation.irVersion > artifact.irVersion) {
|
|
2805
|
+
ctx.addIssue({ code: "custom", path: ["operations", name, "irVersion"], message: "operation requires a newer IR version than its artifact" });
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
1769
2808
|
});
|
|
1770
2809
|
var IrVersionError = class extends Error {
|
|
1771
2810
|
constructor(found) {
|
|
@@ -1776,19 +2815,19 @@ var IrVersionError = class extends Error {
|
|
|
1776
2815
|
function normalizeIr(raw) {
|
|
1777
2816
|
if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
|
|
1778
2817
|
const version = raw.irVersion;
|
|
1779
|
-
if (version !== IR_VERSION) throw new IrVersionError(version);
|
|
2818
|
+
if (version !== LEGACY_IR_VERSION && version !== IR_VERSION) throw new IrVersionError(version);
|
|
1780
2819
|
return compiledArtifactSchema.parse(raw);
|
|
1781
2820
|
}
|
|
1782
2821
|
|
|
1783
2822
|
// src/predicate-v2/ast.ts
|
|
1784
2823
|
var POLICY_COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
|
|
1785
|
-
function walkPolicy(node,
|
|
1786
|
-
|
|
2824
|
+
function walkPolicy(node, visit2) {
|
|
2825
|
+
visit2(node);
|
|
1787
2826
|
if (node.kind === "and" || node.kind === "or") {
|
|
1788
|
-
for (const operand of node.operands) walkPolicy(operand,
|
|
2827
|
+
for (const operand of node.operands) walkPolicy(operand, visit2);
|
|
1789
2828
|
}
|
|
1790
|
-
if (node.kind === "not") walkPolicy(node.operand,
|
|
1791
|
-
if (node.kind === "exists") walkPolicy(node.where,
|
|
2829
|
+
if (node.kind === "not") walkPolicy(node.operand, visit2);
|
|
2830
|
+
if (node.kind === "exists") walkPolicy(node.where, visit2);
|
|
1792
2831
|
}
|
|
1793
2832
|
|
|
1794
2833
|
// src/predicate-v2/errors.ts
|
|
@@ -1816,7 +2855,7 @@ var IDENTIFIER_CHAR = /[a-zA-Z0-9_]/;
|
|
|
1816
2855
|
var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
|
|
1817
2856
|
var SCALAR_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => !type.endsWith("[]")).map(([key]) => key);
|
|
1818
2857
|
var ARRAY_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => type.endsWith("[]")).map(([key]) => key);
|
|
1819
|
-
function
|
|
2858
|
+
function tokenize2(source) {
|
|
1820
2859
|
const tokens = [];
|
|
1821
2860
|
let position = 0;
|
|
1822
2861
|
while (position < source.length) {
|
|
@@ -1895,7 +2934,7 @@ function readString(source, start) {
|
|
|
1895
2934
|
}
|
|
1896
2935
|
throw new PolicyParseError("unterminated string literal", start);
|
|
1897
2936
|
}
|
|
1898
|
-
var
|
|
2937
|
+
var Parser2 = class {
|
|
1899
2938
|
constructor(tokens) {
|
|
1900
2939
|
this.tokens = tokens;
|
|
1901
2940
|
}
|
|
@@ -2069,7 +3108,7 @@ var Parser = class {
|
|
|
2069
3108
|
function parsePolicy(source) {
|
|
2070
3109
|
const trimmed = source.trim();
|
|
2071
3110
|
if (trimmed.length === 0) throw new PolicyParseError("empty policy expression", 0);
|
|
2072
|
-
return { source: trimmed, ast: new
|
|
3111
|
+
return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
|
|
2073
3112
|
}
|
|
2074
3113
|
|
|
2075
3114
|
// src/predicate-v2/compile.ts
|
|
@@ -2294,21 +3333,21 @@ function andPred(...preds) {
|
|
|
2294
3333
|
if (operands.length === 1) return operands[0];
|
|
2295
3334
|
return { kind: "and", operands };
|
|
2296
3335
|
}
|
|
2297
|
-
function walkPred(pred,
|
|
2298
|
-
|
|
3336
|
+
function walkPred(pred, visit2) {
|
|
3337
|
+
visit2(pred);
|
|
2299
3338
|
switch (pred.kind) {
|
|
2300
3339
|
case "and":
|
|
2301
3340
|
case "or":
|
|
2302
|
-
for (const operand of pred.operands) walkPred(operand,
|
|
3341
|
+
for (const operand of pred.operands) walkPred(operand, visit2);
|
|
2303
3342
|
return;
|
|
2304
3343
|
case "not":
|
|
2305
|
-
walkPred(pred.operand,
|
|
3344
|
+
walkPred(pred.operand, visit2);
|
|
2306
3345
|
return;
|
|
2307
3346
|
case "exists":
|
|
2308
|
-
walkPred(pred.where,
|
|
3347
|
+
walkPred(pred.where, visit2);
|
|
2309
3348
|
return;
|
|
2310
3349
|
case "aggPredicate":
|
|
2311
|
-
if (pred.where) walkPred(pred.where,
|
|
3350
|
+
if (pred.where) walkPred(pred.where, visit2);
|
|
2312
3351
|
return;
|
|
2313
3352
|
default:
|
|
2314
3353
|
return;
|
|
@@ -2325,12 +3364,12 @@ function predContainsExists(pred) {
|
|
|
2325
3364
|
// src/compiler-gql/inject.ts
|
|
2326
3365
|
function collectPinnedColumns(writePolicy) {
|
|
2327
3366
|
const key = (pin) => `${pin.column}\0${pin.claim}`;
|
|
2328
|
-
const
|
|
3367
|
+
const visit2 = (node) => {
|
|
2329
3368
|
if (node.kind === "and") {
|
|
2330
3369
|
const merged = [];
|
|
2331
3370
|
const seen2 = /* @__PURE__ */ new Set();
|
|
2332
3371
|
for (const operand of node.operands) {
|
|
2333
|
-
for (const pin of
|
|
3372
|
+
for (const pin of visit2(operand)) {
|
|
2334
3373
|
if (seen2.has(key(pin))) continue;
|
|
2335
3374
|
seen2.add(key(pin));
|
|
2336
3375
|
merged.push(pin);
|
|
@@ -2341,9 +3380,9 @@ function collectPinnedColumns(writePolicy) {
|
|
|
2341
3380
|
if (node.kind === "or") {
|
|
2342
3381
|
const [first, ...rest] = node.operands;
|
|
2343
3382
|
if (first === void 0) return [];
|
|
2344
|
-
let common =
|
|
3383
|
+
let common = visit2(first);
|
|
2345
3384
|
for (const operand of rest) {
|
|
2346
|
-
const branch = new Set(
|
|
3385
|
+
const branch = new Set(visit2(operand).map(key));
|
|
2347
3386
|
common = common.filter((pin) => branch.has(key(pin)));
|
|
2348
3387
|
if (common.length === 0) break;
|
|
2349
3388
|
}
|
|
@@ -2355,7 +3394,7 @@ function collectPinnedColumns(writePolicy) {
|
|
|
2355
3394
|
return [];
|
|
2356
3395
|
};
|
|
2357
3396
|
const seen = /* @__PURE__ */ new Set();
|
|
2358
|
-
return
|
|
3397
|
+
return visit2(writePolicy).filter((pin) => {
|
|
2359
3398
|
if (seen.has(pin.column)) return false;
|
|
2360
3399
|
seen.add(pin.column);
|
|
2361
3400
|
return true;
|
|
@@ -2449,6 +3488,11 @@ var PolicyInjector = class {
|
|
|
2449
3488
|
where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table)),
|
|
2450
3489
|
...root.nodes ? { nodes: this.injectSelection(root.nodes, root.table) } : {}
|
|
2451
3490
|
};
|
|
3491
|
+
case "aggregateGroups":
|
|
3492
|
+
return {
|
|
3493
|
+
...root,
|
|
3494
|
+
where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table))
|
|
3495
|
+
};
|
|
2452
3496
|
case "mutation":
|
|
2453
3497
|
return this.injectMutation(root);
|
|
2454
3498
|
}
|
|
@@ -2456,6 +3500,7 @@ var PolicyInjector = class {
|
|
|
2456
3500
|
injectMutation(root) {
|
|
2457
3501
|
const table = root.table;
|
|
2458
3502
|
const returning = root.returning ? this.injectSelection(root.returning, table) : root.returning;
|
|
3503
|
+
if (root.op === "resolvedWrite") return this.injectResolvedWrite(root, returning);
|
|
2459
3504
|
if (root.op === "insert" || root.op === "insertOne") {
|
|
2460
3505
|
const insertPolicy = this.insert(table);
|
|
2461
3506
|
if (predContainsExists(insertPolicy)) {
|
|
@@ -2497,6 +3542,24 @@ var PolicyInjector = class {
|
|
|
2497
3542
|
}
|
|
2498
3543
|
return { ...root, where, returning };
|
|
2499
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
|
+
}
|
|
2500
3563
|
};
|
|
2501
3564
|
function collectTableSet(roots, ctx) {
|
|
2502
3565
|
const tables = /* @__PURE__ */ new Set();
|
|
@@ -2541,20 +3604,34 @@ function collectTableSet(roots, ctx) {
|
|
|
2541
3604
|
};
|
|
2542
3605
|
for (const root of roots) {
|
|
2543
3606
|
tables.add(root.table);
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
3607
|
+
switch (root.kind) {
|
|
3608
|
+
case "select":
|
|
3609
|
+
case "byPk":
|
|
3610
|
+
visitPred(root.where, root.table);
|
|
3611
|
+
visitSelection(root.selection, root.table);
|
|
3612
|
+
break;
|
|
3613
|
+
case "aggregate":
|
|
3614
|
+
visitPred(root.where, root.table);
|
|
3615
|
+
if (root.nodes) visitSelection(root.nodes, root.table);
|
|
3616
|
+
break;
|
|
3617
|
+
case "aggregateGroups":
|
|
3618
|
+
visitPred(root.where, root.table);
|
|
3619
|
+
break;
|
|
3620
|
+
case "mutation":
|
|
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);
|
|
3623
|
+
if (root.where) visitPred(root.where, root.table);
|
|
3624
|
+
if (root.insertCheck) visitPred(root.insertCheck, root.table);
|
|
3625
|
+
if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
|
|
3626
|
+
if (root.returning) visitSelection(root.returning, root.table);
|
|
3627
|
+
break;
|
|
3628
|
+
default: {
|
|
3629
|
+
const exhausted = root;
|
|
3630
|
+
throw new DocumentCompileError(
|
|
3631
|
+
`internal: unhandled root kind in tableSet collection ${JSON.stringify(exhausted)}`,
|
|
3632
|
+
"internal"
|
|
3633
|
+
);
|
|
3634
|
+
}
|
|
2558
3635
|
}
|
|
2559
3636
|
}
|
|
2560
3637
|
return [...tables].sort();
|
|
@@ -2572,7 +3649,12 @@ function typeNameForTable(table) {
|
|
|
2572
3649
|
}
|
|
2573
3650
|
function queryRootNames(table) {
|
|
2574
3651
|
const names = tableNames(table);
|
|
2575
|
-
return {
|
|
3652
|
+
return {
|
|
3653
|
+
select: names.selectRoot,
|
|
3654
|
+
byPk: names.byPkRoot,
|
|
3655
|
+
aggregate: names.aggregateRoot,
|
|
3656
|
+
aggregateGroups: names.aggregateGroupsRoot
|
|
3657
|
+
};
|
|
2576
3658
|
}
|
|
2577
3659
|
function mutationRootNames(table) {
|
|
2578
3660
|
const names = tableNames(table);
|
|
@@ -2653,7 +3735,15 @@ function resolveColumnEntries(table, enums, diagnostics) {
|
|
|
2653
3735
|
type = { enum: enumDef.name, values: [...enumDef.values] };
|
|
2654
3736
|
}
|
|
2655
3737
|
}
|
|
2656
|
-
return {
|
|
3738
|
+
return {
|
|
3739
|
+
name: column.name,
|
|
3740
|
+
columnName: column.columnName,
|
|
3741
|
+
type,
|
|
3742
|
+
nullable: column.nullable,
|
|
3743
|
+
deprecated: column.deprecated,
|
|
3744
|
+
onUpdateFn: column.onUpdateFn !== void 0,
|
|
3745
|
+
...column.cell ? { cell: column.cell } : {}
|
|
3746
|
+
};
|
|
2657
3747
|
});
|
|
2658
3748
|
}
|
|
2659
3749
|
function logicalColumn(entries, ref) {
|
|
@@ -2689,8 +3779,8 @@ function buildCompileContext(rawCanonical) {
|
|
|
2689
3779
|
const pkRefs = table.primaryKey ?? (isView ? [] : ["id"]);
|
|
2690
3780
|
const primaryKey2 = [];
|
|
2691
3781
|
for (const ref of pkRefs) {
|
|
2692
|
-
const
|
|
2693
|
-
if (!
|
|
3782
|
+
const resolved2 = logicalColumn(entries, ref);
|
|
3783
|
+
if (!resolved2) {
|
|
2694
3784
|
diagnostics.push({
|
|
2695
3785
|
severity: "error",
|
|
2696
3786
|
code: "unknown-pk-column",
|
|
@@ -2699,9 +3789,13 @@ function buildCompileContext(rawCanonical) {
|
|
|
2699
3789
|
});
|
|
2700
3790
|
continue;
|
|
2701
3791
|
}
|
|
2702
|
-
primaryKey2.push(
|
|
3792
|
+
primaryKey2.push(resolved2);
|
|
2703
3793
|
}
|
|
2704
|
-
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);
|
|
3798
|
+
const touchOnUpdate = isView ? [] : active.filter((c) => c.onUpdateFn).map((c) => c.name);
|
|
2705
3799
|
const constraints = [];
|
|
2706
3800
|
if (primaryKey2.length > 0) constraints.push({ name: `${table.table}_pkey`, columns: [...primaryKey2] });
|
|
2707
3801
|
for (const column of table.columns) {
|
|
@@ -2722,14 +3816,17 @@ function buildCompileContext(rawCanonical) {
|
|
|
2722
3816
|
// Synced twins (and views reading them, resolved transitively below)
|
|
2723
3817
|
// are claims-gated — see tableMetaIrSchema.claimsGated.
|
|
2724
3818
|
claimsGated: relationIsClaimsGated(table.table),
|
|
2725
|
-
columns: entries.map(({ deprecated: _deprecated, ...column }) => column),
|
|
3819
|
+
columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
|
|
2726
3820
|
primaryKey: isView ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
|
|
2727
3821
|
links: [],
|
|
2728
|
-
filterable:
|
|
2729
|
-
orderable:
|
|
2730
|
-
insertable: isView ? [] : active.map((c) => c.name),
|
|
2731
|
-
settable: isView ? [] : active.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),
|
|
2732
3826
|
appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
|
|
3827
|
+
...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
|
|
3828
|
+
...cells ? { cells } : {},
|
|
3829
|
+
...resolved ? { resolved: resolvedMeta({ resolved, pk: primaryKey2[0] }) } : {},
|
|
2733
3830
|
constraints: isView ? [] : constraints,
|
|
2734
3831
|
dynamicFilterMaxDepth: 6,
|
|
2735
3832
|
dynamicFilterMaxNodes: 64
|
|
@@ -2750,7 +3847,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
2750
3847
|
});
|
|
2751
3848
|
continue;
|
|
2752
3849
|
}
|
|
2753
|
-
const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false })) : columnEntries.get(targetName);
|
|
3850
|
+
const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false, onUpdateFn: false })) : columnEntries.get(targetName);
|
|
2754
3851
|
const onEntries = link.kind === "toMany" ? targetEntries : entries;
|
|
2755
3852
|
const referencesEntries = link.kind === "toMany" ? entries : targetEntries;
|
|
2756
3853
|
const on = logicalColumn(onEntries, link.on);
|
|
@@ -2802,7 +3899,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
2802
3899
|
compilePolicyFor("write", writePolicies);
|
|
2803
3900
|
compilePolicyFor("insert", insertPolicies);
|
|
2804
3901
|
compilePolicyFor("update", updatePolicies);
|
|
2805
|
-
if (isView) {
|
|
3902
|
+
if (isView && table.resolved === void 0) {
|
|
2806
3903
|
writePolicies.set(table.table, CONST_FALSE);
|
|
2807
3904
|
insertPolicies.set(table.table, CONST_FALSE);
|
|
2808
3905
|
updatePolicies.set(table.table, CONST_FALSE);
|
|
@@ -2841,18 +3938,23 @@ function buildCompileContext(rawCanonical) {
|
|
|
2841
3938
|
const queryRoots = /* @__PURE__ */ new Map();
|
|
2842
3939
|
const mutationRoots = /* @__PURE__ */ new Map();
|
|
2843
3940
|
for (const table of relations) {
|
|
3941
|
+
if (table.internal === true) continue;
|
|
2844
3942
|
const roots = queryRootNames(table.table);
|
|
2845
3943
|
queryRoots.set(roots.select, { kind: "select", table: table.table });
|
|
2846
3944
|
if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
|
|
2847
3945
|
queryRoots.set(roots.byPk, { kind: "byPk", table: table.table });
|
|
2848
3946
|
}
|
|
2849
3947
|
queryRoots.set(roots.aggregate, { kind: "aggregate", table: table.table });
|
|
3948
|
+
if (table.columns.some((column) => column.type !== "jsonb")) {
|
|
3949
|
+
queryRoots.set(roots.aggregateGroups, { kind: "aggregateGroups", table: table.table });
|
|
3950
|
+
}
|
|
2850
3951
|
const hasWritePolicy = table.policies.write !== void 0 || table.policies.insert !== void 0 || table.policies.update !== void 0;
|
|
2851
3952
|
if (!viewNames.has(table.table) && table.source === void 0 && hasWritePolicy) {
|
|
2852
3953
|
for (const [op, field] of Object.entries(mutationRootNames(table.table))) {
|
|
2853
3954
|
mutationRoots.set(field, { op, table: table.table });
|
|
2854
3955
|
}
|
|
2855
3956
|
}
|
|
3957
|
+
if (table.resolved !== void 0) registerResolvedRoots({ table, mutationRoots });
|
|
2856
3958
|
}
|
|
2857
3959
|
for (const table of relations) {
|
|
2858
3960
|
for (const mutation of table.cowMutations ?? []) {
|
|
@@ -2923,6 +4025,49 @@ function buildCompileContext(rawCanonical) {
|
|
|
2923
4025
|
diagnostics
|
|
2924
4026
|
};
|
|
2925
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
|
+
}
|
|
2926
4071
|
function flattenSelectionSet(selectionSet, fragments, stack) {
|
|
2927
4072
|
const selections = [];
|
|
2928
4073
|
for (const selection of selectionSet.selections) {
|
|
@@ -2980,8 +4125,148 @@ function computeOpsHash(opHashes) {
|
|
|
2980
4125
|
const digest = createHash("sha256").update(OPS_HASH_DOMAIN).update([...opHashes].sort().join("\n")).digest("hex");
|
|
2981
4126
|
return `sha256:${digest}`;
|
|
2982
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
|
|
2983
4258
|
var DEFAULT_LIMIT = 100;
|
|
2984
4259
|
var MAX_LIMIT = 1e3;
|
|
4260
|
+
var MAX_GROUPS = 1e3;
|
|
4261
|
+
var DATE_BUCKET_BY_ENUM = {
|
|
4262
|
+
YEAR: "year",
|
|
4263
|
+
QUARTER: "quarter",
|
|
4264
|
+
MONTH: "month",
|
|
4265
|
+
WEEK: "week",
|
|
4266
|
+
DAY: "day",
|
|
4267
|
+
HOUR: "hour"
|
|
4268
|
+
};
|
|
4269
|
+
var BUCKETABLE_TYPES = /* @__PURE__ */ new Set(["timestamptz", "date"]);
|
|
2985
4270
|
var BOOLEXP_OPS = {
|
|
2986
4271
|
_eq: "eq",
|
|
2987
4272
|
_neq: "neq",
|
|
@@ -3007,16 +4292,24 @@ var ORDER_DIRECTIONS = {
|
|
|
3007
4292
|
DESC_NULLS_LAST: { direction: "desc", nulls: "last" }
|
|
3008
4293
|
};
|
|
3009
4294
|
var NUMERIC_TYPES = /* @__PURE__ */ new Set(["integer", "bigint", "numeric", "double"]);
|
|
3010
|
-
function
|
|
4295
|
+
function fail2(message, code, candidates = []) {
|
|
3011
4296
|
throw new DocumentCompileError(message, code, candidates);
|
|
3012
4297
|
}
|
|
3013
4298
|
function argsByName(field) {
|
|
3014
4299
|
return new Map((field.arguments ?? []).map((arg) => [arg.name.value, arg]));
|
|
3015
4300
|
}
|
|
4301
|
+
function* aggregateBlockFields(selectionSet) {
|
|
4302
|
+
for (const node of selectionSet.selections) {
|
|
4303
|
+
if (node.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
4304
|
+
if (node.name.value === "__typename") continue;
|
|
4305
|
+
if (node.alias) fail2(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
|
|
4306
|
+
yield node;
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
3016
4309
|
function assertKnownArgs(field, allowed) {
|
|
3017
4310
|
for (const arg of field.arguments ?? []) {
|
|
3018
4311
|
if (!allowed.includes(arg.name.value)) {
|
|
3019
|
-
|
|
4312
|
+
fail2(`argument '${arg.name.value}' on '${field.name.value}' is not supported in v2.0`, "unsupported-argument", allowed);
|
|
3020
4313
|
}
|
|
3021
4314
|
}
|
|
3022
4315
|
}
|
|
@@ -3029,8 +4322,11 @@ var OperationLowerer = class {
|
|
|
3029
4322
|
meta(table) {
|
|
3030
4323
|
return this.ctx.tables[table];
|
|
3031
4324
|
}
|
|
4325
|
+
/** A data column — cell-metadata columns are reachable only through `cells { … }`. */
|
|
3032
4326
|
column(table, name) {
|
|
3033
|
-
|
|
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;
|
|
3034
4330
|
}
|
|
3035
4331
|
link(table, name) {
|
|
3036
4332
|
return this.meta(table).links.find((l) => l.name === name);
|
|
@@ -3038,7 +4334,8 @@ var OperationLowerer = class {
|
|
|
3038
4334
|
fieldCandidates(table) {
|
|
3039
4335
|
const meta = this.meta(table);
|
|
3040
4336
|
return [
|
|
3041
|
-
...meta.columns.map((c) => c.name),
|
|
4337
|
+
...meta.columns.filter((c) => !isCellColumn(meta, c.name)).map((c) => c.name),
|
|
4338
|
+
...meta.cells ? [CELLS_FIELD] : [],
|
|
3042
4339
|
...meta.links.map((l) => l.name),
|
|
3043
4340
|
...meta.links.filter((l) => l.kind === "toMany").map((l) => `${l.name}Aggregate`)
|
|
3044
4341
|
];
|
|
@@ -3047,7 +4344,7 @@ var OperationLowerer = class {
|
|
|
3047
4344
|
slot(name, input, table) {
|
|
3048
4345
|
const variable = this.variables.get(name);
|
|
3049
4346
|
if (!variable || variable.type.kind !== "inputRef" || variable.type.input !== input || variable.type.table !== table) {
|
|
3050
|
-
|
|
4347
|
+
fail2(`variable $${name} must be declared as the ${input} input type for table '${table}'`, "slot-mismatch");
|
|
3051
4348
|
}
|
|
3052
4349
|
return name;
|
|
3053
4350
|
}
|
|
@@ -3072,14 +4369,14 @@ var OperationLowerer = class {
|
|
|
3072
4369
|
const items = node.values.map((item) => {
|
|
3073
4370
|
const lowered = this.lowerValue(item);
|
|
3074
4371
|
if (lowered.kind !== "literal" || Array.isArray(lowered.value)) {
|
|
3075
|
-
|
|
4372
|
+
fail2("list literals must contain scalar literals only \u2014 pass the whole list as a variable instead", "unsupported-literal");
|
|
3076
4373
|
}
|
|
3077
4374
|
return lowered.value;
|
|
3078
4375
|
});
|
|
3079
4376
|
return { kind: "literal", value: items };
|
|
3080
4377
|
}
|
|
3081
4378
|
case Kind.OBJECT:
|
|
3082
|
-
|
|
4379
|
+
fail2("literal JSON objects are not supported in documents \u2014 pass the value via a variable", "unsupported-literal");
|
|
3083
4380
|
}
|
|
3084
4381
|
}
|
|
3085
4382
|
// -------------------------------------------------------------------------
|
|
@@ -3089,7 +4386,7 @@ var OperationLowerer = class {
|
|
|
3089
4386
|
if (node.kind === Kind.VARIABLE) {
|
|
3090
4387
|
return { kind: "dynamicFilter", slot: this.slot(node.name.value, "boolExp", table) };
|
|
3091
4388
|
}
|
|
3092
|
-
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");
|
|
3093
4390
|
const operands = [];
|
|
3094
4391
|
for (const field of node.fields) {
|
|
3095
4392
|
const key = field.name.value;
|
|
@@ -3120,45 +4417,45 @@ var OperationLowerer = class {
|
|
|
3120
4417
|
continue;
|
|
3121
4418
|
}
|
|
3122
4419
|
}
|
|
3123
|
-
|
|
4420
|
+
fail2(`unknown field '${key}' in a boolean expression on '${table}'`, "unknown-column", this.fieldCandidates(table));
|
|
3124
4421
|
}
|
|
3125
4422
|
if (operands.length === 0) return CONST_TRUE;
|
|
3126
4423
|
return operands.length === 1 ? operands[0] : { kind: "and", operands };
|
|
3127
4424
|
}
|
|
3128
4425
|
lowerComparison(node, column, table) {
|
|
3129
4426
|
if (node.kind !== Kind.OBJECT) {
|
|
3130
|
-
|
|
4427
|
+
fail2(`comparison on '${table}.${column}' must be an object like { _eq: \u2026 }`, "invalid-comparison", Object.keys(BOOLEXP_OPS));
|
|
3131
4428
|
}
|
|
3132
4429
|
return node.fields.map((field) => {
|
|
3133
4430
|
const op = BOOLEXP_OPS[field.name.value];
|
|
3134
4431
|
if (!op) {
|
|
3135
|
-
|
|
4432
|
+
fail2(`unknown comparison operator '${field.name.value}' on '${table}.${column}'`, "unknown-operator", Object.keys(BOOLEXP_OPS));
|
|
3136
4433
|
}
|
|
3137
4434
|
return { kind: "cmp", op, column, value: this.lowerValue(field.value) };
|
|
3138
4435
|
});
|
|
3139
4436
|
}
|
|
3140
4437
|
/** Hasura aggregation predicate: `{ count: { predicate: { _gt: 2 }, filter: {…} } }`. */
|
|
3141
4438
|
lowerAggPredicate(node, link, target) {
|
|
3142
|
-
if (node.kind !== Kind.OBJECT)
|
|
4439
|
+
if (node.kind !== Kind.OBJECT) fail2(`aggregate predicate through '${link}' must be an object`, "invalid-agg-predicate");
|
|
3143
4440
|
const countField = node.fields.find((f) => f.name.value === "count");
|
|
3144
4441
|
if (!countField || node.fields.length !== 1) {
|
|
3145
|
-
|
|
4442
|
+
fail2(`aggregate predicates support { count: \u2026 } only in v2.0 (through '${link}')`, "invalid-agg-predicate", ["count"]);
|
|
3146
4443
|
}
|
|
3147
|
-
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");
|
|
3148
4445
|
let predicate;
|
|
3149
4446
|
let where;
|
|
3150
4447
|
for (const field of countField.value.fields) {
|
|
3151
4448
|
if (field.name.value === "predicate") {
|
|
3152
4449
|
if (field.value.kind !== Kind.OBJECT || field.value.fields.length !== 1) {
|
|
3153
|
-
|
|
4450
|
+
fail2(`count predicate through '${link}' needs exactly one operator`, "invalid-agg-predicate", [...AGG_PREDICATE_OPS]);
|
|
3154
4451
|
}
|
|
3155
4452
|
const opField = field.value.fields[0];
|
|
3156
4453
|
const opName = opField.name.value;
|
|
3157
4454
|
if (!AGG_PREDICATE_OPS.includes(opName)) {
|
|
3158
|
-
|
|
4455
|
+
fail2(`aggregate count predicates support ${AGG_PREDICATE_OPS.join("/")} only, got '${opName}'`, "invalid-agg-predicate");
|
|
3159
4456
|
}
|
|
3160
4457
|
if (opField.value.kind !== Kind.INT) {
|
|
3161
|
-
|
|
4458
|
+
fail2(`aggregate count predicate value must be an integer literal`, "invalid-agg-predicate");
|
|
3162
4459
|
}
|
|
3163
4460
|
predicate = { op: opName.slice(1), value: Number(opField.value.value) };
|
|
3164
4461
|
continue;
|
|
@@ -3167,9 +4464,9 @@ var OperationLowerer = class {
|
|
|
3167
4464
|
where = this.lowerBoolExp(field.value, target);
|
|
3168
4465
|
continue;
|
|
3169
4466
|
}
|
|
3170
|
-
|
|
4467
|
+
fail2(`unknown key '${field.name.value}' in a count predicate`, "invalid-agg-predicate", ["predicate", "filter"]);
|
|
3171
4468
|
}
|
|
3172
|
-
if (!predicate)
|
|
4469
|
+
if (!predicate) fail2(`count predicate through '${link}' needs a { predicate: { _gt: n } }`, "invalid-agg-predicate");
|
|
3173
4470
|
return { kind: "aggPredicate", link, fn: "count", op: predicate.op, value: predicate.value, ...where ? { where } : {} };
|
|
3174
4471
|
}
|
|
3175
4472
|
lowerOrderBy(node, table) {
|
|
@@ -3179,7 +4476,7 @@ var OperationLowerer = class {
|
|
|
3179
4476
|
const items = node.kind === Kind.LIST ? node.values : [node];
|
|
3180
4477
|
const lowered = [];
|
|
3181
4478
|
for (const item of items) {
|
|
3182
|
-
if (item.kind !== Kind.OBJECT)
|
|
4479
|
+
if (item.kind !== Kind.OBJECT) fail2(`orderBy entries on '${table}' must be objects`, "invalid-orderby");
|
|
3183
4480
|
for (const field of item.fields) {
|
|
3184
4481
|
const key = field.name.value;
|
|
3185
4482
|
const column = this.column(table, key);
|
|
@@ -3190,25 +4487,25 @@ var OperationLowerer = class {
|
|
|
3190
4487
|
const link = this.link(table, key);
|
|
3191
4488
|
if (link && link.kind !== "toMany") {
|
|
3192
4489
|
if (field.value.kind !== Kind.OBJECT || field.value.fields.length !== 1) {
|
|
3193
|
-
|
|
4490
|
+
fail2(`orderBy through '${key}' needs exactly one target column`, "invalid-orderby");
|
|
3194
4491
|
}
|
|
3195
4492
|
const inner = field.value.fields[0];
|
|
3196
4493
|
const targetColumn = this.column(link.target, inner.name.value);
|
|
3197
4494
|
if (!targetColumn) {
|
|
3198
|
-
|
|
4495
|
+
fail2(`unknown column '${inner.name.value}' on '${link.target}' in orderBy`, "unknown-column", this.meta(link.target).columns.map((c) => c.name));
|
|
3199
4496
|
}
|
|
3200
4497
|
lowered.push({ kind: "column", column: inner.name.value, link: key, ...this.lowerDirection(inner.value, link.target, inner.name.value) });
|
|
3201
4498
|
continue;
|
|
3202
4499
|
}
|
|
3203
|
-
|
|
4500
|
+
fail2(`unknown orderBy key '${key}' on '${table}' (to-one links only)`, "invalid-orderby", this.fieldCandidates(table));
|
|
3204
4501
|
}
|
|
3205
4502
|
}
|
|
3206
4503
|
return lowered;
|
|
3207
4504
|
}
|
|
3208
4505
|
lowerDirection(node, table, column) {
|
|
3209
|
-
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));
|
|
3210
4507
|
const direction = ORDER_DIRECTIONS[node.value];
|
|
3211
|
-
if (!direction)
|
|
4508
|
+
if (!direction) fail2(`unknown orderBy direction '${node.value}'`, "invalid-orderby", Object.keys(ORDER_DIRECTIONS));
|
|
3212
4509
|
return direction.nulls ? { direction: direction.direction, nulls: direction.nulls } : { direction: direction.direction };
|
|
3213
4510
|
}
|
|
3214
4511
|
lowerDistinctOn(node, table) {
|
|
@@ -3217,9 +4514,9 @@ var OperationLowerer = class {
|
|
|
3217
4514
|
}
|
|
3218
4515
|
const items = node.kind === Kind.LIST ? node.values : [node];
|
|
3219
4516
|
return items.map((item) => {
|
|
3220
|
-
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");
|
|
3221
4518
|
if (!this.column(table, item.value)) {
|
|
3222
|
-
|
|
4519
|
+
fail2(`unknown distinctOn column '${item.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
|
|
3223
4520
|
}
|
|
3224
4521
|
return item.value;
|
|
3225
4522
|
});
|
|
@@ -3230,16 +4527,16 @@ var OperationLowerer = class {
|
|
|
3230
4527
|
if (node.kind === Kind.VARIABLE) return { ...base, variable: node.name.value };
|
|
3231
4528
|
if (node.kind === Kind.INT) {
|
|
3232
4529
|
const value = Number(node.value);
|
|
3233
|
-
if (value < 1)
|
|
4530
|
+
if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
|
|
3234
4531
|
return { ...base, value: Math.min(value, MAX_LIMIT) };
|
|
3235
4532
|
}
|
|
3236
|
-
|
|
4533
|
+
fail2("limit must be an integer literal or a variable", "invalid-limit");
|
|
3237
4534
|
}
|
|
3238
4535
|
lowerOffset(node) {
|
|
3239
4536
|
if (!node) return void 0;
|
|
3240
4537
|
if (node.kind === Kind.VARIABLE) return { variable: node.name.value };
|
|
3241
4538
|
if (node.kind === Kind.INT) return { value: Number(node.value) };
|
|
3242
|
-
|
|
4539
|
+
fail2("offset must be an integer literal or a variable", "invalid-offset");
|
|
3243
4540
|
}
|
|
3244
4541
|
// -------------------------------------------------------------------------
|
|
3245
4542
|
// Selections
|
|
@@ -3248,9 +4545,9 @@ var OperationLowerer = class {
|
|
|
3248
4545
|
const meta = this.meta(table);
|
|
3249
4546
|
const selection = { typename: meta.typename, columns: [], relations: [], aggregates: [] };
|
|
3250
4547
|
for (const node of selectionSet.selections) {
|
|
3251
|
-
if (node.kind !== Kind.FIELD)
|
|
4548
|
+
if (node.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
3252
4549
|
if (node.directives && node.directives.length > 0) {
|
|
3253
|
-
|
|
4550
|
+
fail2(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
|
|
3254
4551
|
}
|
|
3255
4552
|
const name = node.name.value;
|
|
3256
4553
|
if (name === "__typename") continue;
|
|
@@ -3258,14 +4555,19 @@ var OperationLowerer = class {
|
|
|
3258
4555
|
const column = this.column(table, name);
|
|
3259
4556
|
if (column) {
|
|
3260
4557
|
if (node.arguments !== void 0 && node.arguments.length > 0 || node.selectionSet !== void 0) {
|
|
3261
|
-
|
|
4558
|
+
fail2(`column '${table}.${name}' takes no arguments or sub-selection`, "invalid-selection");
|
|
3262
4559
|
}
|
|
3263
4560
|
selection.columns.push({ alias, column: name });
|
|
3264
4561
|
continue;
|
|
3265
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
|
+
}
|
|
3266
4568
|
const link = this.link(table, name);
|
|
3267
4569
|
if (link) {
|
|
3268
|
-
if (!node.selectionSet)
|
|
4570
|
+
if (!node.selectionSet) fail2(`link '${table}.${name}' needs a selection set`, "invalid-selection");
|
|
3269
4571
|
const relation = {
|
|
3270
4572
|
alias,
|
|
3271
4573
|
link: name,
|
|
@@ -3284,7 +4586,7 @@ var OperationLowerer = class {
|
|
|
3284
4586
|
if (offset) relation.offset = offset;
|
|
3285
4587
|
}
|
|
3286
4588
|
} else if (node.arguments && node.arguments.length > 0) {
|
|
3287
|
-
|
|
4589
|
+
fail2(`to-one link '${table}.${name}' takes no arguments`, "invalid-selection");
|
|
3288
4590
|
}
|
|
3289
4591
|
selection.relations.push(relation);
|
|
3290
4592
|
continue;
|
|
@@ -3292,7 +4594,7 @@ var OperationLowerer = class {
|
|
|
3292
4594
|
if (name.endsWith("Aggregate")) {
|
|
3293
4595
|
const aggLink = this.link(table, name.slice(0, -"Aggregate".length));
|
|
3294
4596
|
if (aggLink && aggLink.kind === "toMany") {
|
|
3295
|
-
if (!node.selectionSet)
|
|
4597
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
3296
4598
|
assertKnownArgs(node, ["where"]);
|
|
3297
4599
|
const whereArg = argsByName(node).get("where");
|
|
3298
4600
|
const aggregate = this.lowerAggregateSelection(node.selectionSet, aggLink.target);
|
|
@@ -3305,70 +4607,182 @@ var OperationLowerer = class {
|
|
|
3305
4607
|
continue;
|
|
3306
4608
|
}
|
|
3307
4609
|
}
|
|
3308
|
-
|
|
4610
|
+
fail2(`unknown field '${name}' on '${table}'`, "unknown-field", this.fieldCandidates(table));
|
|
4611
|
+
}
|
|
4612
|
+
return selection;
|
|
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
|
+
}
|
|
4661
|
+
/** The `{ aggregate { count sum {…} avg {…} } nodes {…} }` block shared by roots and relations. */
|
|
4662
|
+
lowerAggregateSelection(selectionSet, table) {
|
|
4663
|
+
const result = {};
|
|
4664
|
+
let sawAggregate = false;
|
|
4665
|
+
for (const node of aggregateBlockFields(selectionSet)) {
|
|
4666
|
+
const name = node.name.value;
|
|
4667
|
+
if (name === "nodes") {
|
|
4668
|
+
if (!node.selectionSet) fail2("'nodes' needs a selection set", "invalid-aggregate");
|
|
4669
|
+
result.nodes = this.lowerSelection(node.selectionSet, table);
|
|
4670
|
+
continue;
|
|
4671
|
+
}
|
|
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");
|
|
4674
|
+
sawAggregate = true;
|
|
4675
|
+
if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
|
|
4676
|
+
Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
|
|
4677
|
+
}
|
|
4678
|
+
if (!sawAggregate && !result.nodes) fail2("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
|
|
4679
|
+
return result;
|
|
4680
|
+
}
|
|
4681
|
+
/**
|
|
4682
|
+
* The `{ key {…} aggregate {…} }` block of a grouped aggregate root. Same
|
|
4683
|
+
* `aggregate` sub-block as the ungrouped shape (shared via
|
|
4684
|
+
* `lowerAggregateFields`), but `key` replaces `nodes` — see the IR comment on
|
|
4685
|
+
* `aggregateGroupsRootSchema` for why grouped roots have no `nodes`.
|
|
4686
|
+
*/
|
|
4687
|
+
lowerGroupedAggregateSelection(selectionSet, table, groupBy) {
|
|
4688
|
+
const groupColumns = new Set(groupBy.map((entry) => entry.column));
|
|
4689
|
+
const result = { key: [] };
|
|
4690
|
+
let sawAggregate = false;
|
|
4691
|
+
let sawKey = false;
|
|
4692
|
+
for (const node of aggregateBlockFields(selectionSet)) {
|
|
4693
|
+
const name = node.name.value;
|
|
4694
|
+
if (name === "key") {
|
|
4695
|
+
if (sawKey) fail2("one key block per grouped aggregate", "invalid-aggregate");
|
|
4696
|
+
sawKey = true;
|
|
4697
|
+
if (!node.selectionSet) fail2("'key' needs a selection set", "invalid-aggregate");
|
|
4698
|
+
result.key = this.lowerGroupKeySelection(node.selectionSet, table, groupColumns);
|
|
4699
|
+
continue;
|
|
4700
|
+
}
|
|
4701
|
+
if (name === "nodes") {
|
|
4702
|
+
fail2("'nodes' is not available on a grouped aggregate \u2014 query the rows separately", "invalid-aggregate", ["key", "aggregate"]);
|
|
4703
|
+
}
|
|
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");
|
|
4706
|
+
sawAggregate = true;
|
|
4707
|
+
if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
|
|
4708
|
+
Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
|
|
4709
|
+
}
|
|
4710
|
+
const rendersAggregate = result.count !== void 0 || (result.sum?.length ?? 0) > 0 || (result.avg?.length ?? 0) > 0;
|
|
4711
|
+
if (result.key.length === 0 && !rendersAggregate) {
|
|
4712
|
+
fail2(
|
|
4713
|
+
"grouped aggregate needs at least one key column or an aggregate \u2014 a block selecting only __typename produces nothing",
|
|
4714
|
+
"invalid-aggregate",
|
|
4715
|
+
["key", "aggregate"]
|
|
4716
|
+
);
|
|
3309
4717
|
}
|
|
3310
|
-
return
|
|
4718
|
+
return result;
|
|
3311
4719
|
}
|
|
3312
|
-
/**
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
4720
|
+
/**
|
|
4721
|
+
* `key { month officer }` → the projected group columns. Every field must be
|
|
4722
|
+
* one of the query's own group keys: any other column is indeterminate
|
|
4723
|
+
* within a group, so selecting it is a document error rather than an
|
|
4724
|
+
* arbitrary pick.
|
|
4725
|
+
*/
|
|
4726
|
+
lowerGroupKeySelection(selectionSet, table, groupColumns) {
|
|
4727
|
+
const key = [];
|
|
3316
4728
|
for (const node of selectionSet.selections) {
|
|
3317
|
-
if (node.kind !== Kind.FIELD)
|
|
4729
|
+
if (node.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
3318
4730
|
const name = node.name.value;
|
|
3319
4731
|
if (name === "__typename") continue;
|
|
3320
|
-
if (
|
|
3321
|
-
|
|
3322
|
-
if (!node.selectionSet) fail("'nodes' needs a selection set", "invalid-aggregate");
|
|
3323
|
-
result.nodes = this.lowerSelection(node.selectionSet, table);
|
|
3324
|
-
continue;
|
|
4732
|
+
if (!this.column(table, name)) {
|
|
4733
|
+
fail2(`unknown key column '${name}' on '${table}'`, "unknown-column", [...groupColumns]);
|
|
3325
4734
|
}
|
|
3326
|
-
if (name
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
4735
|
+
if (!groupColumns.has(name)) {
|
|
4736
|
+
fail2(
|
|
4737
|
+
`key column '${name}' is not one of this query's groupBy keys \u2014 it has no single value within a group`,
|
|
4738
|
+
"invalid-group-key",
|
|
4739
|
+
[...groupColumns]
|
|
4740
|
+
);
|
|
4741
|
+
}
|
|
4742
|
+
key.push({ alias: node.alias?.value ?? name, column: name });
|
|
4743
|
+
}
|
|
4744
|
+
return key;
|
|
4745
|
+
}
|
|
4746
|
+
/** The `count` / `sum {…}` / `avg {…}` fields inside an `aggregate { … }` block. */
|
|
4747
|
+
lowerAggregateFields(selectionSet, table) {
|
|
4748
|
+
const result = {};
|
|
4749
|
+
for (const aggField of aggregateBlockFields(selectionSet)) {
|
|
4750
|
+
const aggName = aggField.name.value;
|
|
4751
|
+
if (aggName === "count") {
|
|
4752
|
+
assertKnownArgs(aggField, ["columns", "distinct"]);
|
|
4753
|
+
const args = argsByName(aggField);
|
|
4754
|
+
const count = {};
|
|
4755
|
+
const columns = args.get("columns");
|
|
4756
|
+
if (columns) count.columns = this.lowerDistinctOn(columns.value, table);
|
|
4757
|
+
const distinct = args.get("distinct");
|
|
4758
|
+
if (distinct) {
|
|
4759
|
+
if (distinct.value.kind !== Kind.BOOLEAN) fail2("count(distinct:) must be a boolean literal", "invalid-aggregate");
|
|
4760
|
+
count.distinct = distinct.value.value;
|
|
3348
4761
|
}
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
4762
|
+
result.count = count;
|
|
4763
|
+
continue;
|
|
4764
|
+
}
|
|
4765
|
+
if (aggName === "sum" || aggName === "avg") {
|
|
4766
|
+
if (!aggField.selectionSet) fail2(`'${aggName}' needs a column selection`, "invalid-aggregate");
|
|
4767
|
+
const columns = [];
|
|
4768
|
+
for (const columnField of aggField.selectionSet.selections) {
|
|
4769
|
+
if (columnField.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
4770
|
+
if (columnField.name.value === "__typename") continue;
|
|
4771
|
+
const column = this.column(table, columnField.name.value);
|
|
4772
|
+
if (!column || typeof column.type !== "string" || !NUMERIC_TYPES.has(column.type)) {
|
|
4773
|
+
fail2(
|
|
4774
|
+
`'${aggName}' supports numeric columns only, got '${columnField.name.value}' on '${table}'`,
|
|
4775
|
+
"invalid-aggregate",
|
|
4776
|
+
this.meta(table).columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type)).map((c) => c.name)
|
|
4777
|
+
);
|
|
3364
4778
|
}
|
|
3365
|
-
|
|
3366
|
-
continue;
|
|
4779
|
+
columns.push(columnField.name.value);
|
|
3367
4780
|
}
|
|
3368
|
-
|
|
4781
|
+
result[aggName] = columns;
|
|
4782
|
+
continue;
|
|
3369
4783
|
}
|
|
4784
|
+
fail2(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
|
|
3370
4785
|
}
|
|
3371
|
-
if (!sawAggregate && !result.nodes) fail("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
|
|
3372
4786
|
return result;
|
|
3373
4787
|
}
|
|
3374
4788
|
// -------------------------------------------------------------------------
|
|
@@ -3377,32 +4791,65 @@ var OperationLowerer = class {
|
|
|
3377
4791
|
lowerRoots(operation, kind) {
|
|
3378
4792
|
const roots = [];
|
|
3379
4793
|
for (const node of operation.selectionSet.selections) {
|
|
3380
|
-
if (node.kind !== Kind.FIELD)
|
|
4794
|
+
if (node.kind !== Kind.FIELD) fail2("fragments are not supported at the operation root", "invalid-root");
|
|
4795
|
+
if (node.selectionSet) visit(node.selectionSet, {
|
|
4796
|
+
Directive(directive) {
|
|
4797
|
+
if (directive.name.value === "assertAffectedRows") {
|
|
4798
|
+
fail2("@assertAffectedRows is supported on mutation roots only", "invalid-affected-row-assertion");
|
|
4799
|
+
}
|
|
4800
|
+
}
|
|
4801
|
+
});
|
|
4802
|
+
const assertion = this.parseAffectedRowAssertion(node, kind);
|
|
3381
4803
|
const gate = this.parseRootDirectives(node);
|
|
3382
4804
|
if (gate.omit) continue;
|
|
3383
|
-
const
|
|
3384
|
-
|
|
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 });
|
|
4808
|
+
const guarded = root.kind === "mutation" && assertion !== void 0 ? { ...root, assertAffectedRows: assertion } : root;
|
|
4809
|
+
roots.push(gate.includeIf ? { ...guarded, includeIf: gate.includeIf } : guarded);
|
|
3385
4810
|
}
|
|
3386
|
-
if (roots.length === 0)
|
|
4811
|
+
if (roots.length === 0) fail2("operation has no roots (all statically skipped?)", "invalid-root");
|
|
3387
4812
|
return roots;
|
|
3388
4813
|
}
|
|
4814
|
+
parseAffectedRowAssertion(node, kind) {
|
|
4815
|
+
const assertions = node.directives?.filter((directive) => directive.name.value === "assertAffectedRows") ?? [];
|
|
4816
|
+
if (!assertions.length) return void 0;
|
|
4817
|
+
if (kind !== "mutation" || assertions.length !== 1) {
|
|
4818
|
+
fail2("@assertAffectedRows is supported once per mutation root", "invalid-affected-row-assertion");
|
|
4819
|
+
}
|
|
4820
|
+
if (node.directives?.some((directive) => ["include", "skip"].includes(directive.name.value))) {
|
|
4821
|
+
fail2("an @assertAffectedRows root cannot use @include or @skip", "invalid-affected-row-assertion");
|
|
4822
|
+
}
|
|
4823
|
+
const args = assertions[0].arguments;
|
|
4824
|
+
const arg = args?.[0];
|
|
4825
|
+
if (args?.length !== 1 || arg?.name.value !== "exactly" || arg.value.kind !== Kind.INT) {
|
|
4826
|
+
fail2("@assertAffectedRows(exactly:) requires a document-authored nonnegative Int literal", "invalid-affected-row-assertion");
|
|
4827
|
+
}
|
|
4828
|
+
const count = Number(arg.value.value);
|
|
4829
|
+
if (!Number.isInteger(count) || count < 0 || count > 2147483647) {
|
|
4830
|
+
fail2("@assertAffectedRows(exactly:) must be between 0 and 2147483647", "invalid-affected-row-assertion");
|
|
4831
|
+
}
|
|
4832
|
+
return count;
|
|
4833
|
+
}
|
|
3389
4834
|
parseRootDirectives(node) {
|
|
3390
4835
|
let includeIf;
|
|
3391
4836
|
let omit = false;
|
|
3392
4837
|
for (const directive of node.directives ?? []) {
|
|
3393
4838
|
const name = directive.name.value;
|
|
4839
|
+
if (name === "assertAffectedRows") continue;
|
|
4840
|
+
if (SOURCE_DIRECTIVES.has(name)) continue;
|
|
3394
4841
|
if (name !== "include" && name !== "skip") {
|
|
3395
|
-
|
|
4842
|
+
fail2(`directive @${name} is not supported on fields`, "unknown-directive", ["include", "skip"]);
|
|
3396
4843
|
}
|
|
3397
4844
|
const ifArg = directive.arguments?.find((a) => a.name.value === "if");
|
|
3398
|
-
if (!ifArg)
|
|
4845
|
+
if (!ifArg) fail2(`@${name} needs an if: argument`, "invalid-directive");
|
|
3399
4846
|
if (ifArg.value.kind === Kind.BOOLEAN) {
|
|
3400
4847
|
const keep = name === "include" ? ifArg.value.value : !ifArg.value.value;
|
|
3401
4848
|
if (!keep) omit = true;
|
|
3402
4849
|
continue;
|
|
3403
4850
|
}
|
|
3404
|
-
if (ifArg.value.kind !== Kind.VARIABLE)
|
|
3405
|
-
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");
|
|
3406
4853
|
includeIf = { variable: ifArg.value.name.value, negated: name === "skip" };
|
|
3407
4854
|
}
|
|
3408
4855
|
return omit ? { omit: true } : { includeIf, omit: false };
|
|
@@ -3411,12 +4858,12 @@ var OperationLowerer = class {
|
|
|
3411
4858
|
const name = node.name.value;
|
|
3412
4859
|
const alias = node.alias?.value ?? name;
|
|
3413
4860
|
const rootInfo = this.ctx.queryRoots.get(name);
|
|
3414
|
-
if (!rootInfo)
|
|
4861
|
+
if (!rootInfo) fail2(`unknown query root '${name}'`, "unknown-root", [...this.ctx.queryRoots.keys()]);
|
|
3415
4862
|
const { table } = rootInfo;
|
|
3416
4863
|
const args = argsByName(node);
|
|
3417
4864
|
if (rootInfo.kind === "select") {
|
|
3418
4865
|
assertKnownArgs(node, ["where", "orderBy", "distinctOn", "limit", "offset"]);
|
|
3419
|
-
if (!node.selectionSet)
|
|
4866
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
3420
4867
|
const where = args.get("where");
|
|
3421
4868
|
const orderBy = args.get("orderBy");
|
|
3422
4869
|
const distinctOn = args.get("distinctOn");
|
|
@@ -3435,12 +4882,15 @@ var OperationLowerer = class {
|
|
|
3435
4882
|
};
|
|
3436
4883
|
}
|
|
3437
4884
|
if (rootInfo.kind === "byPk") {
|
|
3438
|
-
if (!node.selectionSet)
|
|
4885
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
3439
4886
|
const pk = this.lowerPkArgs(node, table);
|
|
3440
4887
|
return { kind: "byPk", alias, table, selection: this.lowerSelection(node.selectionSet, table), pk, where: CONST_TRUE };
|
|
3441
4888
|
}
|
|
4889
|
+
if (rootInfo.kind === "aggregateGroups") {
|
|
4890
|
+
return this.lowerAggregateGroupsRoot(node, { alias, table });
|
|
4891
|
+
}
|
|
3442
4892
|
assertKnownArgs(node, ["where"]);
|
|
3443
|
-
if (!node.selectionSet)
|
|
4893
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
3444
4894
|
const whereArg = args.get("where");
|
|
3445
4895
|
const aggregate = this.lowerAggregateSelection(node.selectionSet, table);
|
|
3446
4896
|
return {
|
|
@@ -3451,6 +4901,108 @@ var OperationLowerer = class {
|
|
|
3451
4901
|
...aggregate
|
|
3452
4902
|
};
|
|
3453
4903
|
}
|
|
4904
|
+
/** `<t>AggregateGroups(where:, groupBy:, limit:, offset:)` → an aggregateGroups root. */
|
|
4905
|
+
lowerAggregateGroupsRoot(node, { alias, table }) {
|
|
4906
|
+
assertKnownArgs(node, ["where", "groupBy", "limit", "offset"]);
|
|
4907
|
+
if (!node.selectionSet) fail2(`'${node.name.value}' needs a selection set`, "invalid-selection");
|
|
4908
|
+
const args = argsByName(node);
|
|
4909
|
+
const groupByArg = args.get("groupBy");
|
|
4910
|
+
if (!groupByArg) fail2(`'${node.name.value}' requires a 'groupBy' argument`, "missing-group-by");
|
|
4911
|
+
const whereArg = args.get("where");
|
|
4912
|
+
const groupBy = this.lowerGroupBy(groupByArg.value, table);
|
|
4913
|
+
const aggregate = this.lowerGroupedAggregateSelection(node.selectionSet, table, groupBy);
|
|
4914
|
+
const offset = this.lowerOffset(args.get("offset")?.value);
|
|
4915
|
+
return {
|
|
4916
|
+
kind: "aggregateGroups",
|
|
4917
|
+
alias,
|
|
4918
|
+
table,
|
|
4919
|
+
where: whereArg ? this.lowerBoolExp(whereArg.value, table) : CONST_TRUE,
|
|
4920
|
+
groupBy,
|
|
4921
|
+
key: aggregate.key,
|
|
4922
|
+
...aggregate.count ? { count: aggregate.count } : {},
|
|
4923
|
+
...aggregate.sum ? { sum: aggregate.sum } : {},
|
|
4924
|
+
...aggregate.avg ? { avg: aggregate.avg } : {},
|
|
4925
|
+
limit: this.lowerGroupLimit(args.get("limit")?.value),
|
|
4926
|
+
...offset ? { offset } : {}
|
|
4927
|
+
};
|
|
4928
|
+
}
|
|
4929
|
+
/**
|
|
4930
|
+
* `groupBy: [{ column: startedAt, bucket: MONTH }, …]` → GroupByKeyIr[].
|
|
4931
|
+
*
|
|
4932
|
+
* Document-authored only: a whole-argument variable is rejected rather than
|
|
4933
|
+
* becoming a §4f slot. Group keys choose which expressions reach GROUP BY,
|
|
4934
|
+
* and dynamic-input.ts refuses aggregation shapes from client input on the
|
|
4935
|
+
* same reasoning — `where` stays dynamic, which is what runtime filtering
|
|
4936
|
+
* actually needs.
|
|
4937
|
+
*/
|
|
4938
|
+
lowerGroupBy(node, table) {
|
|
4939
|
+
if (node.kind === Kind.VARIABLE) {
|
|
4940
|
+
fail2(
|
|
4941
|
+
`groupBy on '${table}' must be written in the document \u2014 it cannot come from a variable`,
|
|
4942
|
+
"dynamic-group-by"
|
|
4943
|
+
);
|
|
4944
|
+
}
|
|
4945
|
+
const items = node.kind === Kind.LIST ? node.values : [node];
|
|
4946
|
+
if (items.length === 0) fail2(`groupBy on '${table}' needs at least one key`, "invalid-group-by");
|
|
4947
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4948
|
+
return items.map((item) => {
|
|
4949
|
+
if (item.kind !== Kind.OBJECT) {
|
|
4950
|
+
fail2(`groupBy entries on '${table}' must be objects like { column: foo }`, "invalid-group-by");
|
|
4951
|
+
}
|
|
4952
|
+
const fields = new Map(item.fields.map((field) => [field.name.value, field.value]));
|
|
4953
|
+
for (const key of fields.keys()) {
|
|
4954
|
+
if (key !== "column" && key !== "bucket") {
|
|
4955
|
+
fail2(`unknown groupBy field '${key}' on '${table}'`, "invalid-group-by", ["column", "bucket"]);
|
|
4956
|
+
}
|
|
4957
|
+
}
|
|
4958
|
+
const columnNode = fields.get("column");
|
|
4959
|
+
if (!columnNode) fail2(`groupBy entries on '${table}' need a 'column'`, "invalid-group-by");
|
|
4960
|
+
if (columnNode.kind !== Kind.ENUM) {
|
|
4961
|
+
fail2(`groupBy column on '${table}' must be a group-by column enum value`, "invalid-group-by");
|
|
4962
|
+
}
|
|
4963
|
+
const column = this.column(table, columnNode.value);
|
|
4964
|
+
if (!column) {
|
|
4965
|
+
fail2(`unknown groupBy column '${columnNode.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
|
|
4966
|
+
}
|
|
4967
|
+
if (column.type === "jsonb") {
|
|
4968
|
+
fail2(`groupBy column '${table}.${columnNode.value}' is jsonb and cannot be a group key`, "invalid-group-by");
|
|
4969
|
+
}
|
|
4970
|
+
const bucketNode = fields.get("bucket");
|
|
4971
|
+
let bucket;
|
|
4972
|
+
if (bucketNode) {
|
|
4973
|
+
if (bucketNode.kind !== Kind.ENUM) {
|
|
4974
|
+
fail2(`groupBy bucket on '${table}.${columnNode.value}' must be a DateBucket enum value`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
|
|
4975
|
+
}
|
|
4976
|
+
bucket = DATE_BUCKET_BY_ENUM[bucketNode.value];
|
|
4977
|
+
if (!bucket) {
|
|
4978
|
+
fail2(`unknown DateBucket '${bucketNode.value}'`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
|
|
4979
|
+
}
|
|
4980
|
+
if (typeof column.type !== "string" || !BUCKETABLE_TYPES.has(column.type)) {
|
|
4981
|
+
fail2(
|
|
4982
|
+
`groupBy bucket is only valid on date/timestamptz columns \u2014 '${table}.${columnNode.value}' is ${typeof column.type === "string" ? column.type : "an enum"}`,
|
|
4983
|
+
"invalid-group-by"
|
|
4984
|
+
);
|
|
4985
|
+
}
|
|
4986
|
+
}
|
|
4987
|
+
if (seen.has(columnNode.value)) {
|
|
4988
|
+
fail2(`duplicate groupBy column '${columnNode.value}' on '${table}'`, "invalid-group-by");
|
|
4989
|
+
}
|
|
4990
|
+
seen.add(columnNode.value);
|
|
4991
|
+
return bucket ? { column: columnNode.value, bucket } : { column: columnNode.value };
|
|
4992
|
+
});
|
|
4993
|
+
}
|
|
4994
|
+
/** Like lowerLimit, but bounded by MAX_GROUPS and defaulting to it. */
|
|
4995
|
+
lowerGroupLimit(node) {
|
|
4996
|
+
const base = { default: MAX_GROUPS, max: MAX_GROUPS };
|
|
4997
|
+
if (!node) return base;
|
|
4998
|
+
if (node.kind === Kind.VARIABLE) return { ...base, variable: node.name.value };
|
|
4999
|
+
if (node.kind === Kind.INT) {
|
|
5000
|
+
const value = Number(node.value);
|
|
5001
|
+
if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
|
|
5002
|
+
return { ...base, value: Math.min(value, MAX_GROUPS) };
|
|
5003
|
+
}
|
|
5004
|
+
fail2("limit must be an integer literal or a variable", "invalid-limit");
|
|
5005
|
+
}
|
|
3454
5006
|
/** byPk-style pk arguments: one argument per primary-key column, exactly. */
|
|
3455
5007
|
lowerPkArgs(node, table) {
|
|
3456
5008
|
const meta = this.meta(table);
|
|
@@ -3458,7 +5010,7 @@ var OperationLowerer = class {
|
|
|
3458
5010
|
assertKnownArgs(node, meta.primaryKey);
|
|
3459
5011
|
return meta.primaryKey.map((column) => {
|
|
3460
5012
|
const arg = args.get(column);
|
|
3461
|
-
if (!arg)
|
|
5013
|
+
if (!arg) fail2(`'${node.name.value}' is missing primary-key argument '${column}'`, "missing-pk-argument", meta.primaryKey);
|
|
3462
5014
|
return { column, value: this.lowerValue(arg.value) };
|
|
3463
5015
|
});
|
|
3464
5016
|
}
|
|
@@ -3469,19 +5021,22 @@ var OperationLowerer = class {
|
|
|
3469
5021
|
const name = node.name.value;
|
|
3470
5022
|
const alias = node.alias?.value ?? name;
|
|
3471
5023
|
const rootInfo = this.ctx.mutationRoots.get(name);
|
|
3472
|
-
if (!rootInfo)
|
|
5024
|
+
if (!rootInfo) fail2(`unknown mutation root '${name}'`, "unknown-root", [...this.ctx.mutationRoots.keys()]);
|
|
3473
5025
|
if (rootInfo.op === "cowOverride" || rootInfo.op === "cowRevert") {
|
|
3474
5026
|
return this.lowerCowRoot(node, rootInfo);
|
|
3475
5027
|
}
|
|
5028
|
+
if (rootInfo.op === "resolvedWrite") {
|
|
5029
|
+
return this.lowerResolvedRoot(node, rootInfo);
|
|
5030
|
+
}
|
|
3476
5031
|
const { op, table } = rootInfo;
|
|
3477
5032
|
const args = argsByName(node);
|
|
3478
|
-
if (!node.selectionSet)
|
|
5033
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
3479
5034
|
const base = { kind: "mutation", op, alias, table, pinnedColumns: [], affectedRows: false };
|
|
3480
5035
|
switch (op) {
|
|
3481
5036
|
case "insert": {
|
|
3482
5037
|
assertKnownArgs(node, ["objects", "onConflict"]);
|
|
3483
5038
|
const objects = args.get("objects");
|
|
3484
|
-
if (!objects)
|
|
5039
|
+
if (!objects) fail2(`'${name}' needs objects:`, "missing-argument");
|
|
3485
5040
|
const response = this.lowerMutationResponse(node.selectionSet, table);
|
|
3486
5041
|
return {
|
|
3487
5042
|
...base,
|
|
@@ -3493,7 +5048,7 @@ var OperationLowerer = class {
|
|
|
3493
5048
|
case "insertOne": {
|
|
3494
5049
|
assertKnownArgs(node, ["object", "onConflict"]);
|
|
3495
5050
|
const object = args.get("object");
|
|
3496
|
-
if (!object)
|
|
5051
|
+
if (!object) fail2(`'${name}' needs object:`, "missing-argument");
|
|
3497
5052
|
return {
|
|
3498
5053
|
...base,
|
|
3499
5054
|
objects: this.lowerInsertObjects(object.value, table, { list: false }),
|
|
@@ -3504,7 +5059,7 @@ var OperationLowerer = class {
|
|
|
3504
5059
|
case "update": {
|
|
3505
5060
|
assertKnownArgs(node, ["where", "_set", "_append", "_inc"]);
|
|
3506
5061
|
const where = args.get("where");
|
|
3507
|
-
if (!where)
|
|
5062
|
+
if (!where) fail2(`'${name}' needs where: (bulk updates without a filter are not allowed)`, "missing-argument");
|
|
3508
5063
|
return {
|
|
3509
5064
|
...base,
|
|
3510
5065
|
...this.lowerMutationResponse(node.selectionSet, table),
|
|
@@ -3515,7 +5070,7 @@ var OperationLowerer = class {
|
|
|
3515
5070
|
case "updateByPk": {
|
|
3516
5071
|
assertKnownArgs(node, ["pkColumns", "_set", "_append", "_inc"]);
|
|
3517
5072
|
const pkColumns = args.get("pkColumns");
|
|
3518
|
-
if (!pkColumns)
|
|
5073
|
+
if (!pkColumns) fail2(`'${name}' needs pkColumns:`, "missing-argument");
|
|
3519
5074
|
return {
|
|
3520
5075
|
...base,
|
|
3521
5076
|
pk: this.lowerPkColumnsArg(pkColumns.value, table),
|
|
@@ -3526,7 +5081,7 @@ var OperationLowerer = class {
|
|
|
3526
5081
|
case "delete": {
|
|
3527
5082
|
assertKnownArgs(node, ["where"]);
|
|
3528
5083
|
const where = args.get("where");
|
|
3529
|
-
if (!where)
|
|
5084
|
+
if (!where) fail2(`'${name}' needs where: (bulk deletes without a filter are not allowed)`, "missing-argument");
|
|
3530
5085
|
return {
|
|
3531
5086
|
...base,
|
|
3532
5087
|
...this.lowerMutationResponse(node.selectionSet, table),
|
|
@@ -3553,11 +5108,11 @@ var OperationLowerer = class {
|
|
|
3553
5108
|
const name = node.name.value;
|
|
3554
5109
|
const alias = node.alias?.value ?? name;
|
|
3555
5110
|
const { table } = rootInfo;
|
|
3556
|
-
if (!node.selectionSet)
|
|
5111
|
+
if (!node.selectionSet) fail2(`'${name}' needs a selection set`, "invalid-selection");
|
|
3557
5112
|
const meta = this.meta(table);
|
|
3558
5113
|
const args = argsByName(node);
|
|
3559
5114
|
const pkArg = args.get(rootInfo.pk);
|
|
3560
|
-
if (!pkArg)
|
|
5115
|
+
if (!pkArg) fail2(`'${name}' is missing primary-key argument '${rootInfo.pk}'`, "missing-pk-argument", [rootInfo.pk]);
|
|
3561
5116
|
const pk = [{ column: rootInfo.pk, value: this.lowerValue(pkArg.value) }];
|
|
3562
5117
|
const base = {
|
|
3563
5118
|
kind: "mutation",
|
|
@@ -3578,24 +5133,78 @@ var OperationLowerer = class {
|
|
|
3578
5133
|
const entries = rootInfo.set.map((column) => {
|
|
3579
5134
|
const arg = args.get(column);
|
|
3580
5135
|
if (!arg) {
|
|
3581
|
-
|
|
5136
|
+
fail2(
|
|
3582
5137
|
`'${name}' is missing set argument '${column}' \u2014 a cow override writes every declared set column (pass null to clear a nullable one)`,
|
|
3583
5138
|
"missing-argument",
|
|
3584
5139
|
[...rootInfo.set]
|
|
3585
5140
|
);
|
|
3586
5141
|
}
|
|
3587
5142
|
if (!meta.settable.includes(column)) {
|
|
3588
|
-
|
|
5143
|
+
fail2(`column '${table}.${column}' is not updatable (pinned by the write policy or deprecated)`, "column-not-settable", meta.settable);
|
|
3589
5144
|
}
|
|
3590
5145
|
return { column, value: this.lowerValue(arg.value) };
|
|
3591
5146
|
});
|
|
3592
5147
|
return { ...base, set: { entries }, cowCopyColumns: [...rootInfo.copyColumns] };
|
|
3593
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
|
+
}
|
|
3594
5203
|
lowerMutationResponse(selectionSet, table) {
|
|
3595
5204
|
let affectedRows = false;
|
|
3596
5205
|
let returning = null;
|
|
3597
5206
|
for (const node of selectionSet.selections) {
|
|
3598
|
-
if (node.kind !== Kind.FIELD)
|
|
5207
|
+
if (node.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
3599
5208
|
const name = node.name.value;
|
|
3600
5209
|
if (name === "__typename") continue;
|
|
3601
5210
|
if (name === "affectedRows") {
|
|
@@ -3603,34 +5212,34 @@ var OperationLowerer = class {
|
|
|
3603
5212
|
continue;
|
|
3604
5213
|
}
|
|
3605
5214
|
if (name === "returning") {
|
|
3606
|
-
if (!node.selectionSet)
|
|
5215
|
+
if (!node.selectionSet) fail2("'returning' needs a selection set", "invalid-selection");
|
|
3607
5216
|
returning = this.lowerSelection(node.selectionSet, table);
|
|
3608
5217
|
continue;
|
|
3609
5218
|
}
|
|
3610
|
-
|
|
5219
|
+
fail2(`unknown mutation-response field '${name}'`, "invalid-selection", ["affectedRows", "returning"]);
|
|
3611
5220
|
}
|
|
3612
5221
|
return { affectedRows, returning };
|
|
3613
5222
|
}
|
|
3614
5223
|
lowerInsertObjects(node, table, { list }) {
|
|
3615
5224
|
if (node.kind === Kind.VARIABLE) return { slot: this.slot(node.name.value, "insertInput", table) };
|
|
3616
5225
|
if (list) {
|
|
3617
|
-
if (node.kind !== Kind.LIST)
|
|
5226
|
+
if (node.kind !== Kind.LIST) fail2(`objects: must be a list literal or a variable`, "invalid-insert");
|
|
3618
5227
|
return { rows: node.values.map((item) => this.lowerInsertRow(item, table)) };
|
|
3619
5228
|
}
|
|
3620
5229
|
return { rows: [this.lowerInsertRow(node, table)] };
|
|
3621
5230
|
}
|
|
3622
5231
|
lowerInsertRow(node, table) {
|
|
3623
|
-
if (node.kind !== Kind.OBJECT)
|
|
5232
|
+
if (node.kind !== Kind.OBJECT) fail2(`insert rows on '${table}' must be objects`, "invalid-insert");
|
|
3624
5233
|
const meta = this.meta(table);
|
|
3625
5234
|
return node.fields.map((field) => {
|
|
3626
5235
|
const key = field.name.value;
|
|
3627
5236
|
if (this.link(table, key)) {
|
|
3628
|
-
|
|
5237
|
+
fail2(`nested inserts are not supported in v2.0 \u2014 '${key}' is a link on '${table}'`, "nested-insert-unsupported");
|
|
3629
5238
|
}
|
|
3630
5239
|
if (!meta.insertable.includes(key)) {
|
|
3631
5240
|
const column = this.column(table, key);
|
|
3632
|
-
if (column)
|
|
3633
|
-
|
|
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);
|
|
3634
5243
|
}
|
|
3635
5244
|
return { column: key, value: this.lowerValue(field.value) };
|
|
3636
5245
|
});
|
|
@@ -3639,32 +5248,32 @@ var OperationLowerer = class {
|
|
|
3639
5248
|
if (!arg) return {};
|
|
3640
5249
|
const node = arg.value;
|
|
3641
5250
|
if (node.kind === Kind.VARIABLE) return { onConflict: { slot: this.slot(node.name.value, "onConflict", table) } };
|
|
3642
|
-
if (node.kind !== Kind.OBJECT)
|
|
5251
|
+
if (node.kind !== Kind.OBJECT) fail2("onConflict: must be an object literal or a variable", "invalid-on-conflict");
|
|
3643
5252
|
const meta = this.meta(table);
|
|
3644
5253
|
let constraint;
|
|
3645
5254
|
const updateColumns = [];
|
|
3646
5255
|
for (const field of node.fields) {
|
|
3647
5256
|
if (field.name.value === "constraint") {
|
|
3648
|
-
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");
|
|
3649
5258
|
constraint = field.value.value;
|
|
3650
5259
|
continue;
|
|
3651
5260
|
}
|
|
3652
5261
|
if (field.name.value === "updateColumns") {
|
|
3653
5262
|
const items = field.value.kind === Kind.LIST ? field.value.values : [field.value];
|
|
3654
5263
|
for (const item of items) {
|
|
3655
|
-
if (item.kind !== Kind.ENUM)
|
|
5264
|
+
if (item.kind !== Kind.ENUM) fail2("onConflict.updateColumns must be update-column enum values", "invalid-on-conflict");
|
|
3656
5265
|
if (!meta.settable.includes(item.value)) {
|
|
3657
|
-
|
|
5266
|
+
fail2(`onConflict.updateColumns: '${item.value}' is not updatable on '${table}'`, "invalid-on-conflict", meta.settable);
|
|
3658
5267
|
}
|
|
3659
5268
|
updateColumns.push(item.value);
|
|
3660
5269
|
}
|
|
3661
5270
|
continue;
|
|
3662
5271
|
}
|
|
3663
|
-
|
|
5272
|
+
fail2(`unknown onConflict key '${field.name.value}'`, "invalid-on-conflict", ["constraint", "updateColumns"]);
|
|
3664
5273
|
}
|
|
3665
|
-
if (!constraint)
|
|
5274
|
+
if (!constraint) fail2("onConflict needs a constraint", "invalid-on-conflict", meta.constraints.map((c) => c.name));
|
|
3666
5275
|
if (!meta.constraints.some((c) => c.name === constraint)) {
|
|
3667
|
-
|
|
5276
|
+
fail2(`unknown constraint '${constraint}' on '${table}'`, "invalid-on-conflict", meta.constraints.map((c) => c.name));
|
|
3668
5277
|
}
|
|
3669
5278
|
return { onConflict: { constraint, updateColumns } };
|
|
3670
5279
|
}
|
|
@@ -3682,36 +5291,36 @@ var OperationLowerer = class {
|
|
|
3682
5291
|
const inc = args.get("_inc");
|
|
3683
5292
|
if (inc) {
|
|
3684
5293
|
if (inc.value.kind === Kind.VARIABLE) {
|
|
3685
|
-
|
|
5294
|
+
fail2("_inc has no dynamic input type \u2014 inline the columns with variable leaf values", "invalid-inc");
|
|
3686
5295
|
}
|
|
3687
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);
|
|
3688
5297
|
result.inc = { entries: this.lowerColumnEntries(inc.value, table, numeric4, "_inc") };
|
|
3689
5298
|
}
|
|
3690
5299
|
if (!result.set && !result.append && !result.inc) {
|
|
3691
|
-
|
|
5300
|
+
fail2(`an update on '${table}' needs at least one of _set/_append/_inc`, "missing-argument");
|
|
3692
5301
|
}
|
|
3693
5302
|
return result;
|
|
3694
5303
|
}
|
|
3695
5304
|
lowerColumnEntries(node, table, whitelist, argName) {
|
|
3696
|
-
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");
|
|
3697
5306
|
return node.fields.map((field) => {
|
|
3698
5307
|
const key = field.name.value;
|
|
3699
5308
|
if (!whitelist.includes(key)) {
|
|
3700
|
-
|
|
5309
|
+
fail2(`${argName}: column '${key}' is not allowed on '${table}'`, "column-not-settable", [...whitelist]);
|
|
3701
5310
|
}
|
|
3702
5311
|
return { column: key, value: this.lowerValue(field.value) };
|
|
3703
5312
|
});
|
|
3704
5313
|
}
|
|
3705
5314
|
lowerPkColumnsArg(node, table) {
|
|
3706
|
-
if (node.kind !== Kind.OBJECT)
|
|
5315
|
+
if (node.kind !== Kind.OBJECT) fail2(`pkColumns on '${table}' must be an object literal`, "invalid-pk-columns");
|
|
3707
5316
|
const meta = this.meta(table);
|
|
3708
5317
|
const provided = new Map(node.fields.map((field) => [field.name.value, field.value]));
|
|
3709
5318
|
for (const key of provided.keys()) {
|
|
3710
|
-
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);
|
|
3711
5320
|
}
|
|
3712
5321
|
return meta.primaryKey.map((column) => {
|
|
3713
5322
|
const value = provided.get(column);
|
|
3714
|
-
if (!value)
|
|
5323
|
+
if (!value) fail2(`pkColumns is missing '${column}'`, "invalid-pk-columns", meta.primaryKey);
|
|
3715
5324
|
return { column, value: this.lowerValue(value) };
|
|
3716
5325
|
});
|
|
3717
5326
|
}
|
|
@@ -3720,6 +5329,19 @@ function lowerOperation({ restricted, ctx }) {
|
|
|
3720
5329
|
const lowerer = new OperationLowerer(ctx, restricted);
|
|
3721
5330
|
return lowerer.lowerRoots(restricted.definition, restricted.kind);
|
|
3722
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
|
+
}
|
|
3723
5345
|
function parseSdl(sdl) {
|
|
3724
5346
|
return buildSchema(sdl);
|
|
3725
5347
|
}
|
|
@@ -3960,7 +5582,7 @@ function compileNamespace({ canonical, sdl, documents }) {
|
|
|
3960
5582
|
const loweredRoots = lowerOperation({ restricted: { ...restricted, definition: flattened }, ctx });
|
|
3961
5583
|
const { roots, tableSet } = injectPolicies({ roots: loweredRoots, ctx });
|
|
3962
5584
|
operations[restricted.name] = {
|
|
3963
|
-
irVersion: IR_VERSION,
|
|
5585
|
+
irVersion: roots.some((root) => root.kind === "mutation" && root.assertAffectedRows !== void 0) ? IR_VERSION : LEGACY_IR_VERSION,
|
|
3964
5586
|
name: restricted.name,
|
|
3965
5587
|
opHash: computeOpHash({ definition: flattened, schemaHash }),
|
|
3966
5588
|
kind: restricted.kind,
|
|
@@ -3987,7 +5609,7 @@ function compileNamespace({ canonical, sdl, documents }) {
|
|
|
3987
5609
|
}
|
|
3988
5610
|
if (diagnostics.some((d) => d.severity === "error")) return { ok: false, diagnostics };
|
|
3989
5611
|
const artifact = {
|
|
3990
|
-
irVersion:
|
|
5612
|
+
irVersion: requiredArtifactIrVersion(Object.values(operations)),
|
|
3991
5613
|
namespace: ctx.namespace,
|
|
3992
5614
|
schemaHash,
|
|
3993
5615
|
opsHash: computeOpsHash(Object.values(operations).map((op) => op.opHash)),
|
|
@@ -4096,15 +5718,6 @@ function columnWireType(column) {
|
|
|
4096
5718
|
const notNull = column.primary_key === true || column.nullable === false;
|
|
4097
5719
|
return notNull || base === "unknown" ? base : `${base} | null`;
|
|
4098
5720
|
}
|
|
4099
|
-
|
|
4100
|
-
// src/compiler/sql.ts
|
|
4101
|
-
var CLAIMS_SETTING = "request.jwt.claims";
|
|
4102
|
-
var CAPABILITIES_CLAIM_KEY = "capabilities";
|
|
4103
|
-
var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
4104
|
-
var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
4105
|
-
var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
|
|
4106
|
-
var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
|
|
4107
|
-
var capabilityExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
4108
5721
|
var identifierSchema = z.string().max(MAX_IDENTIFIER_LENGTH).regex(IDENTIFIER_PATTERN, "must be a lowercase identifier (a-z, 0-9, _)");
|
|
4109
5722
|
var namespaceSchema = z.string().regex(NAMESPACE_PATTERN, "must be lowercase, start with a letter, and be at most 41 characters");
|
|
4110
5723
|
var tableReferenceSchema = z.string().refine(
|
|
@@ -4187,12 +5800,12 @@ function castParts(cast) {
|
|
|
4187
5800
|
|
|
4188
5801
|
// src/predicate/ast.ts
|
|
4189
5802
|
var COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
|
|
4190
|
-
function walkPredicate(predicate,
|
|
4191
|
-
|
|
5803
|
+
function walkPredicate(predicate, visit2) {
|
|
5804
|
+
visit2(predicate);
|
|
4192
5805
|
if (predicate.kind === "and" || predicate.kind === "or") {
|
|
4193
|
-
for (const operand of predicate.operands) walkPredicate(operand,
|
|
5806
|
+
for (const operand of predicate.operands) walkPredicate(operand, visit2);
|
|
4194
5807
|
}
|
|
4195
|
-
if (predicate.kind === "not") walkPredicate(predicate.operand,
|
|
5808
|
+
if (predicate.kind === "not") walkPredicate(predicate.operand, visit2);
|
|
4196
5809
|
}
|
|
4197
5810
|
function predicateOperands(predicate) {
|
|
4198
5811
|
const operands = [];
|
|
@@ -4221,7 +5834,7 @@ var OPERATOR_PATTERN2 = /^(!=|<=|>=|<>|=|<|>)/;
|
|
|
4221
5834
|
var IDENTIFIER_START2 = /[a-z_]/;
|
|
4222
5835
|
var IDENTIFIER_CHAR2 = /[a-z0-9_.-]/;
|
|
4223
5836
|
var NUMBER_PATTERN2 = /^-?\d+(\.\d+)?/;
|
|
4224
|
-
function
|
|
5837
|
+
function tokenize3(source) {
|
|
4225
5838
|
const tokens = [];
|
|
4226
5839
|
let position = 0;
|
|
4227
5840
|
while (position < source.length) {
|
|
@@ -4295,7 +5908,7 @@ function readString2(source, start) {
|
|
|
4295
5908
|
}
|
|
4296
5909
|
throw new PredicateParseError("unterminated string literal", start);
|
|
4297
5910
|
}
|
|
4298
|
-
var
|
|
5911
|
+
var Parser3 = class {
|
|
4299
5912
|
constructor(tokens) {
|
|
4300
5913
|
this.tokens = tokens;
|
|
4301
5914
|
}
|
|
@@ -4427,7 +6040,7 @@ var Parser2 = class {
|
|
|
4427
6040
|
function parsePredicate(source) {
|
|
4428
6041
|
const trimmed = source.trim();
|
|
4429
6042
|
if (trimmed.length === 0) throw new PredicateParseError("empty predicate", 0);
|
|
4430
|
-
return { source: trimmed, ast: new
|
|
6043
|
+
return { source: trimmed, ast: new Parser3(tokenize3(trimmed)).parse() };
|
|
4431
6044
|
}
|
|
4432
6045
|
|
|
4433
6046
|
// src/schema/predicates.ts
|
|
@@ -5149,6 +6762,88 @@ function snapshotTableNames(snapshot, schema) {
|
|
|
5149
6762
|
return Object.values(tables).filter((entry) => entry.schema === schema).map((entry) => entry.name);
|
|
5150
6763
|
}
|
|
5151
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
|
+
|
|
5152
6847
|
// src/engine/canonical2.ts
|
|
5153
6848
|
var FACTORIES2 = {
|
|
5154
6849
|
uuid: (name) => uuid(name),
|
|
@@ -5167,7 +6862,7 @@ var FACTORIES2 = {
|
|
|
5167
6862
|
var SCHEMA_IMPORT_KEY2 = "$schema";
|
|
5168
6863
|
var enumImportKey = (name) => `$enum_${name}`;
|
|
5169
6864
|
var columnTablesV2 = (definition) => definition.tables.filter((table) => !table.source);
|
|
5170
|
-
function buildDrizzleEntitiesV2(definition) {
|
|
6865
|
+
function buildDrizzleEntitiesV2(definition, options = {}) {
|
|
5171
6866
|
const schemaName = ormSchemaName(definition.namespace);
|
|
5172
6867
|
const schema = pgSchema(schemaName);
|
|
5173
6868
|
const imports = { [SCHEMA_IMPORT_KEY2]: schema };
|
|
@@ -5182,11 +6877,19 @@ function buildDrizzleEntitiesV2(definition) {
|
|
|
5182
6877
|
}
|
|
5183
6878
|
const byName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
5184
6879
|
for (const table of sortByEnforcedLinks2(columnTablesV2(definition))) {
|
|
5185
|
-
const drizzleTable = buildTable2(schema, table, built, enums, byName);
|
|
6880
|
+
const drizzleTable = buildTable2(schema, table, built, enums, byName, options.introspection ?? false);
|
|
5186
6881
|
built.set(table.table, drizzleTable);
|
|
5187
6882
|
imports[table.table] = drizzleTable;
|
|
5188
6883
|
tables[table.table] = drizzleTable;
|
|
5189
6884
|
}
|
|
6885
|
+
if (options.introspection) {
|
|
6886
|
+
for (const table of definition.tables.filter((table2) => table2.source)) {
|
|
6887
|
+
const columns = Object.fromEntries(
|
|
6888
|
+
table.columns.map((column) => [column.columnName, buildViewColumnV2({ view: table.table, column, enums })])
|
|
6889
|
+
);
|
|
6890
|
+
imports[table.table] = schema.view(table.table, columns).existing();
|
|
6891
|
+
}
|
|
6892
|
+
}
|
|
5190
6893
|
for (const view of definition.views ?? []) {
|
|
5191
6894
|
const columns = Object.fromEntries(
|
|
5192
6895
|
view.columns.map((column) => [column.columnName, buildViewColumnV2({ view: view.view, column, enums })])
|
|
@@ -5430,9 +7133,12 @@ function replaceQualifiedRelation(sql4, sourceSchema, sourceName, boundSchema, b
|
|
|
5430
7133
|
function generateSnapshotV2(definition, previousId3) {
|
|
5431
7134
|
const { imports, schemaName } = buildDrizzleEntitiesV2(definition);
|
|
5432
7135
|
const snapshot = generateDrizzleJson(imports, previousId3, [schemaName]);
|
|
5433
|
-
if ((definition.views?.length ?? 0) > 0) {
|
|
7136
|
+
if ((definition.views?.length ?? 0) > 0 || definition.tables.some((table) => table.foreignKeys?.length)) {
|
|
5434
7137
|
const extended = snapshot;
|
|
7138
|
+
const refreshFunctions = materializedRefreshFunctions(definition);
|
|
5435
7139
|
extended._sequenceOrm = {
|
|
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) } : {},
|
|
5436
7142
|
viewDependencies: Object.fromEntries(
|
|
5437
7143
|
(definition.views ?? []).map((view) => [view.view, [...view.dependencies].sort()])
|
|
5438
7144
|
)
|
|
@@ -5450,7 +7156,7 @@ function buildViewColumnV2({
|
|
|
5450
7156
|
if (!column.nullable) builder = builder.notNull();
|
|
5451
7157
|
return builder;
|
|
5452
7158
|
}
|
|
5453
|
-
function buildTable2(schema, table, built, enums, byName) {
|
|
7159
|
+
function buildTable2(schema, table, built, enums, byName, introspection) {
|
|
5454
7160
|
const primaryKeyColumns2 = table.primaryKey ?? [];
|
|
5455
7161
|
if (primaryKeyColumns2.length === 0) {
|
|
5456
7162
|
throw new OrmDefinitionError(`table '${table.table}' declares no primaryKey`);
|
|
@@ -5466,13 +7172,26 @@ function buildTable2(schema, table, built, enums, byName) {
|
|
|
5466
7172
|
const columns = Object.fromEntries(
|
|
5467
7173
|
table.columns.map((column) => [
|
|
5468
7174
|
column.columnName,
|
|
5469
|
-
buildColumnV2({ table: table.table, column, singlePrimaryKey, compositeMembers, enums })
|
|
7175
|
+
buildColumnV2({ table: table.table, column, singlePrimaryKey, compositeMembers, enums, introspection })
|
|
5470
7176
|
])
|
|
5471
7177
|
);
|
|
5472
7178
|
const drizzleTable = schema.table(table.table, columns, (self) => [
|
|
5473
7179
|
...buildCompositePrimaryKey(table, primaryKeyColumns2, self),
|
|
5474
7180
|
...buildIndexes2(table, self),
|
|
5475
|
-
...
|
|
7181
|
+
...(table.checks ?? []).map((check$1) => check(check$1.name, sql.raw(compileCheckExpression(check$1.expression, table.columns)))),
|
|
7182
|
+
...buildForeignKeys2(table, self, built, byName),
|
|
7183
|
+
...(table.foreignKeys ?? []).map((fk) => {
|
|
7184
|
+
const target = fk.target === table.table ? self : built.get(fk.target);
|
|
7185
|
+
if (!target) throw new OrmDefinitionError(`foreign key '${fk.name}' has no target '${fk.target}'`);
|
|
7186
|
+
let constraint = foreignKey({
|
|
7187
|
+
name: fk.name,
|
|
7188
|
+
columns: fk.columns.map((column) => self[column]),
|
|
7189
|
+
foreignColumns: fk.references.map((column) => target[column])
|
|
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;
|
|
7194
|
+
})
|
|
5476
7195
|
]);
|
|
5477
7196
|
return drizzleTable.enableRLS();
|
|
5478
7197
|
}
|
|
@@ -5489,7 +7208,8 @@ function buildColumnV2({
|
|
|
5489
7208
|
column,
|
|
5490
7209
|
singlePrimaryKey,
|
|
5491
7210
|
compositeMembers,
|
|
5492
|
-
enums
|
|
7211
|
+
enums,
|
|
7212
|
+
introspection
|
|
5493
7213
|
}) {
|
|
5494
7214
|
const factory = resolveColumnFactory(table, column, enums);
|
|
5495
7215
|
let builder = factory(column.columnName);
|
|
@@ -5510,7 +7230,7 @@ function buildColumnV2({
|
|
|
5510
7230
|
}
|
|
5511
7231
|
builder = builder.default(sql.raw(column.defaultFn));
|
|
5512
7232
|
} else if (column.default !== void 0 && column.default !== null) {
|
|
5513
|
-
builder = builder.default(column.default);
|
|
7233
|
+
builder = introspection && column.type === "jsonb" && typeof column.default === "string" ? builder.default(sql.raw(`${quoteLiteral(column.default)}::jsonb`)) : builder.default(column.default);
|
|
5514
7234
|
}
|
|
5515
7235
|
return builder;
|
|
5516
7236
|
}
|
|
@@ -5561,11 +7281,14 @@ function buildForeignKeys2(table, self, built, byName) {
|
|
|
5561
7281
|
`enforced link '${link.name}' on '${table.table}' references unknown column '${targetColumn ? link.on : referencedColumn}'`
|
|
5562
7282
|
);
|
|
5563
7283
|
}
|
|
5564
|
-
|
|
7284
|
+
let constraint = foreignKey({
|
|
5565
7285
|
name: foreignKeyName(table.table, link.name),
|
|
5566
7286
|
columns: [sourceColumn],
|
|
5567
7287
|
foreignColumns: [targetColumn]
|
|
5568
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;
|
|
5569
7292
|
});
|
|
5570
7293
|
}
|
|
5571
7294
|
var singlePrimaryKeyOf = (table) => table.primaryKey?.length === 1 ? table.primaryKey[0] : void 0;
|
|
@@ -5686,16 +7409,31 @@ function findTable2(snapshot, schema, table) {
|
|
|
5686
7409
|
const tables = snapshot.tables ?? {};
|
|
5687
7410
|
return Object.values(tables).find((entry) => entry.schema === schema && entry.name === table);
|
|
5688
7411
|
}
|
|
5689
|
-
|
|
7412
|
+
function planMigrationV2(input) {
|
|
7413
|
+
return planWithViewRecreation({ ...input, viewRecreation: "affected" });
|
|
7414
|
+
}
|
|
7415
|
+
async function planCommittedMigrationV2({
|
|
5690
7416
|
canonical,
|
|
5691
|
-
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
|
|
5692
7429
|
}) {
|
|
5693
7430
|
const schema = ormSchemaName(canonical.namespace);
|
|
5694
7431
|
const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
|
|
5695
7432
|
assertValidViewDependencies(canonical);
|
|
5696
7433
|
const hints = applyMigrationHintsV2(previous, canonical);
|
|
5697
7434
|
const snapshot = generateSnapshotV2(canonical, previousId(previous));
|
|
5698
|
-
const
|
|
7435
|
+
const generatedBaseDdl = await diffStatements(withoutViews(hints.adjusted), withoutViews(snapshot));
|
|
7436
|
+
const baseDdl = orderForeignKeyAdditions(canonical, generatedBaseDdl);
|
|
5699
7437
|
const previousViews = snapshotViews(hints.adjusted);
|
|
5700
7438
|
const currentViews = snapshotViews(snapshot);
|
|
5701
7439
|
const previousDependencies = snapshotViewDependencies(hints.adjusted);
|
|
@@ -5705,7 +7443,14 @@ async function planMigrationV2({
|
|
|
5705
7443
|
currentViews,
|
|
5706
7444
|
previousDependencies,
|
|
5707
7445
|
currentDependencies,
|
|
5708
|
-
|
|
7446
|
+
reach: viewReach({
|
|
7447
|
+
viewRecreation,
|
|
7448
|
+
hintStatements: hints.statements,
|
|
7449
|
+
baseDdl,
|
|
7450
|
+
previous,
|
|
7451
|
+
adjusted: hints.adjusted,
|
|
7452
|
+
current: snapshot
|
|
7453
|
+
})
|
|
5709
7454
|
});
|
|
5710
7455
|
const dropOrder = topologicalViewNames(
|
|
5711
7456
|
new Set([...affected].filter((name) => previousViews.has(name))),
|
|
@@ -5732,15 +7477,44 @@ async function planMigrationV2({
|
|
|
5732
7477
|
...dropOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(dropDdl, name, schema, "DROP") })),
|
|
5733
7478
|
...hints.statements.map((statement) => ({ kind: "hint", sql: statement })),
|
|
5734
7479
|
...baseDdl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") })),
|
|
7480
|
+
...foreignKeyTimingStatements(canonical, previous, baseDdl).map((sql4) => ({ kind: "ddl", sql: sql4 })),
|
|
5735
7481
|
...createOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(createDdl, name, schema, "CREATE") })),
|
|
5736
7482
|
...createOrder.map((name) => {
|
|
5737
7483
|
const view = currentViewDefinitions.get(name);
|
|
5738
7484
|
if (!view) throw new OrmDefinitionError(`missing canonical definition for view '${name}'`);
|
|
5739
7485
|
return { kind: "ddl", sql: validateViewCatalogStatement({ canonical, view, schema }) };
|
|
5740
|
-
})
|
|
7486
|
+
}),
|
|
7487
|
+
...refreshFunctionStatements({
|
|
7488
|
+
namespace: canonical.namespace,
|
|
7489
|
+
previous: previous._sequenceOrm?.refreshFunctions ?? {},
|
|
7490
|
+
current: materializedRefreshFunctions(canonical)
|
|
7491
|
+
}).map((sql4) => ({ kind: "ddl", sql: sql4 }))
|
|
5741
7492
|
];
|
|
5742
7493
|
return { namespace: canonical.namespace, schema, statements, snapshot };
|
|
5743
7494
|
}
|
|
7495
|
+
function orderForeignKeyAdditions(canonical, statements) {
|
|
7496
|
+
if (!canonical.tables.some((table) => table.foreignKeys?.length)) return [...statements];
|
|
7497
|
+
const addsForeignKey = (statement) => /^ALTER TABLE[\s\S]* ADD CONSTRAINT[\s\S]* FOREIGN KEY\b/.test(statement);
|
|
7498
|
+
return [...statements.filter((statement) => !addsForeignKey(statement)), ...statements.filter(addsForeignKey)];
|
|
7499
|
+
}
|
|
7500
|
+
function foreignKeyTimingStatements(canonical, previous, baseDdl) {
|
|
7501
|
+
const prior = previous._sequenceOrm?.foreignKeys ?? {};
|
|
7502
|
+
const result = [];
|
|
7503
|
+
for (const table of canonical.tables) {
|
|
7504
|
+
for (const fk of table.foreignKeys ?? []) {
|
|
7505
|
+
const before = prior[`${table.table}.${fk.name}`];
|
|
7506
|
+
const recreated = baseDdl.some((statement) => statement.includes(`ALTER TABLE ${quoteIdentifier(ormSchemaName(canonical.namespace))}.${quoteIdentifier(table.table)} ADD CONSTRAINT ${quoteIdentifier(fk.name)}`));
|
|
7507
|
+
if (fk.deferrable ? !before || before.deferrable !== fk.deferrable || recreated : before?.deferrable && !recreated) {
|
|
7508
|
+
result.push(foreignKeyTimingSql(ormSchemaName(canonical.namespace), table.table, fk.name, fk.deferrable));
|
|
7509
|
+
}
|
|
7510
|
+
}
|
|
7511
|
+
}
|
|
7512
|
+
return result;
|
|
7513
|
+
}
|
|
7514
|
+
function foreignKeyTimingSql(schema, table, name, timing) {
|
|
7515
|
+
const clause = timing === void 0 ? "NOT DEFERRABLE" : timing === "initially-deferred" ? "DEFERRABLE INITIALLY DEFERRED" : "DEFERRABLE INITIALLY IMMEDIATE";
|
|
7516
|
+
return `ALTER TABLE ${quoteIdentifier(schema)}.${quoteIdentifier(table)} ALTER CONSTRAINT ${quoteIdentifier(name)} ${clause}`;
|
|
7517
|
+
}
|
|
5744
7518
|
function validateViewCatalogStatement({
|
|
5745
7519
|
canonical,
|
|
5746
7520
|
view,
|
|
@@ -5868,20 +7642,102 @@ function withoutViews(snapshot) {
|
|
|
5868
7642
|
delete clone._sequenceOrm;
|
|
5869
7643
|
return clone;
|
|
5870
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
|
+
}
|
|
5871
7725
|
function affectedViewNames({
|
|
5872
7726
|
previousViews,
|
|
5873
7727
|
currentViews,
|
|
5874
7728
|
previousDependencies,
|
|
5875
7729
|
currentDependencies,
|
|
5876
|
-
|
|
7730
|
+
reach
|
|
5877
7731
|
}) {
|
|
5878
7732
|
const allNames = /* @__PURE__ */ new Set([...previousViews.keys(), ...currentViews.keys()]);
|
|
5879
|
-
if (
|
|
7733
|
+
if (reach === "all") return allNames;
|
|
7734
|
+
const dependenciesOf = (name) => /* @__PURE__ */ new Set([...previousDependencies[name] ?? [], ...currentDependencies[name] ?? []]);
|
|
5880
7735
|
const affected = new Set(
|
|
5881
7736
|
[...allNames].filter((name) => {
|
|
5882
7737
|
const before = previousViews.get(name);
|
|
5883
7738
|
const after = currentViews.get(name);
|
|
5884
|
-
|
|
7739
|
+
if (before === void 0 || after === void 0 || stableJson(before) !== stableJson(after)) return true;
|
|
7740
|
+
return [...dependenciesOf(name)].some((dependency) => reach.has(dependency));
|
|
5885
7741
|
})
|
|
5886
7742
|
);
|
|
5887
7743
|
let grew = true;
|
|
@@ -5889,8 +7745,7 @@ function affectedViewNames({
|
|
|
5889
7745
|
grew = false;
|
|
5890
7746
|
for (const name of allNames) {
|
|
5891
7747
|
if (affected.has(name)) continue;
|
|
5892
|
-
|
|
5893
|
-
if ([...dependencies].some((dependency) => affected.has(dependency))) {
|
|
7748
|
+
if ([...dependenciesOf(name)].some((dependency) => affected.has(dependency))) {
|
|
5894
7749
|
affected.add(name);
|
|
5895
7750
|
grew = true;
|
|
5896
7751
|
}
|
|
@@ -6805,7 +8660,9 @@ var PLATFORM_USER_FIELDS = [
|
|
|
6805
8660
|
];
|
|
6806
8661
|
function generateSdl(canonical, options = {}) {
|
|
6807
8662
|
const def = normalizeCanonical(canonicalDefinitionSchema.parse(canonical));
|
|
6808
|
-
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) }));
|
|
6809
8666
|
if (tables.length === 0) {
|
|
6810
8667
|
throw new SdlGenerationError(
|
|
6811
8668
|
`Namespace "${def.namespace}" exposes no relations (synced tables are excluded from SDL in v2.0) \u2014 nothing to generate.`
|
|
@@ -6861,8 +8718,14 @@ function generateSdl(canonical, options = {}) {
|
|
|
6861
8718
|
'"""Gates an operation behind a platform capability or custom role \u2014 checked by the data plane before execution."""\ndirective @requires(capability: String, role: String) on QUERY | MUTATION'
|
|
6862
8719
|
);
|
|
6863
8720
|
blocks.push("directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT");
|
|
8721
|
+
blocks.push(
|
|
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'
|
|
8723
|
+
);
|
|
8724
|
+
if (tables.some((table) => table.resolved !== void 0)) blocks.push(...RESOLVED_DIRECTIVE_BLOCKS);
|
|
6864
8725
|
for (const scalar2 of CUSTOM_SCALARS) blocks.push(`scalar ${scalar2}`);
|
|
8726
|
+
blocks.push(...renderSharedCellTypes([...cellsByTable.values()]));
|
|
6865
8727
|
blocks.push(renderEnum(ORDER_BY_ENUM, [...ORDER_BY_ENUM_VALUES]));
|
|
8728
|
+
blocks.push(renderEnum(DATE_BUCKET_ENUM, DATE_BUCKETS.map((bucket) => bucket.toUpperCase())));
|
|
6866
8729
|
for (const enumName of [...usedEnums].sort()) {
|
|
6867
8730
|
const entry = enumByName.get(enumName);
|
|
6868
8731
|
if (!entry) continue;
|
|
@@ -6885,16 +8748,32 @@ function generateSdl(canonical, options = {}) {
|
|
|
6885
8748
|
names,
|
|
6886
8749
|
namesByTable,
|
|
6887
8750
|
emitAggregateBoolExp: aggregateBoolExpTargets.has(table.table),
|
|
6888
|
-
pinned: options.pinnedColumns?.[table.table] ?? []
|
|
8751
|
+
pinned: options.pinnedColumns?.[table.table] ?? [],
|
|
8752
|
+
cells: cellsByTable.get(table.table) ?? null
|
|
6889
8753
|
})
|
|
6890
8754
|
);
|
|
6891
8755
|
}
|
|
6892
8756
|
blocks.push(renderQueryRoot(tables, namesByTable));
|
|
6893
8757
|
const mutationRoot = renderMutationRoot(tables, namesByTable);
|
|
6894
8758
|
if (mutationRoot !== null) blocks.push(mutationRoot);
|
|
8759
|
+
assertNoDuplicateTypeNames(blocks);
|
|
6895
8760
|
return `${blocks.join("\n\n")}
|
|
6896
8761
|
`;
|
|
6897
8762
|
}
|
|
8763
|
+
var TYPE_DECLARATION_RE = /^(?:type|input|enum|scalar|interface|union)\s+([_A-Za-z][_0-9A-Za-z]*)/;
|
|
8764
|
+
function assertNoDuplicateTypeNames(blocks) {
|
|
8765
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8766
|
+
for (const block of blocks) {
|
|
8767
|
+
const declared = TYPE_DECLARATION_RE.exec(block)?.[1];
|
|
8768
|
+
if (declared === void 0) continue;
|
|
8769
|
+
if (seen.has(declared)) {
|
|
8770
|
+
throw new SdlGenerationError(
|
|
8771
|
+
`Generated schema declares "${declared}" twice. A canonical enum or table name is PascalCased into this GraphQL type name and collides with another declaration (for example an enum named "date_bucket" renders as "DateBucket", the shared bucket enum). Rename the offending enum or table.`
|
|
8772
|
+
);
|
|
8773
|
+
}
|
|
8774
|
+
seen.add(declared);
|
|
8775
|
+
}
|
|
8776
|
+
}
|
|
6898
8777
|
function assertEnumValue(value, context) {
|
|
6899
8778
|
if (!GRAPHQL_NAME_RE2.test(value) || RESERVED_ENUM_VALUES.has(value)) {
|
|
6900
8779
|
throw new SdlGenerationError(
|
|
@@ -6960,6 +8839,52 @@ ${orderFields}
|
|
|
6960
8839
|
}`
|
|
6961
8840
|
];
|
|
6962
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
|
+
}
|
|
6963
8888
|
function relationArgs(target) {
|
|
6964
8889
|
return `where: ${target.boolExp}, orderBy: [${target.orderBy}!], limit: Int, offset: Int`;
|
|
6965
8890
|
}
|
|
@@ -6968,10 +8893,10 @@ function rootArgs(names) {
|
|
|
6968
8893
|
}
|
|
6969
8894
|
function primaryKeyColumns(table) {
|
|
6970
8895
|
if (!table.primaryKey || table.primaryKey.length === 0) return [];
|
|
6971
|
-
return table.primaryKey.map((
|
|
6972
|
-
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);
|
|
6973
8898
|
if (!column) {
|
|
6974
|
-
throw new SdlGenerationError(`Primary key column "${
|
|
8899
|
+
throw new SdlGenerationError(`Primary key column "${identifier3}" not found on table "${table.table}".`);
|
|
6975
8900
|
}
|
|
6976
8901
|
return column;
|
|
6977
8902
|
});
|
|
@@ -6980,10 +8905,12 @@ function renderTableBlocks(ctx) {
|
|
|
6980
8905
|
const { table, names } = ctx;
|
|
6981
8906
|
const blocks = [
|
|
6982
8907
|
renderObjectType(ctx),
|
|
8908
|
+
...renderCellsType(ctx),
|
|
6983
8909
|
renderSelectColumnEnum(ctx),
|
|
6984
8910
|
renderOrderByInput(ctx),
|
|
6985
8911
|
renderBoolExp(ctx),
|
|
6986
|
-
...renderAggregateTypes(ctx)
|
|
8912
|
+
...renderAggregateTypes(ctx),
|
|
8913
|
+
...renderGroupByTypes(ctx)
|
|
6987
8914
|
];
|
|
6988
8915
|
if (ctx.emitAggregateBoolExp) blocks.push(...renderAggregateBoolExp(ctx));
|
|
6989
8916
|
const pk = primaryKeyColumns(table);
|
|
@@ -6993,11 +8920,49 @@ function renderTableBlocks(ctx) {
|
|
|
6993
8920
|
${fields}
|
|
6994
8921
|
}`);
|
|
6995
8922
|
}
|
|
6996
|
-
if (
|
|
8923
|
+
if (ctx.table.resolved !== void 0) {
|
|
8924
|
+
blocks.push(...renderResolvedWriteBlocks(ctx));
|
|
8925
|
+
} else if (allowsInsert(table) || allowsUpdate(table)) {
|
|
6997
8926
|
blocks.push(...renderWriteBlocks(ctx, { insert: allowsInsert(table), update: allowsUpdate(table) }));
|
|
6998
8927
|
}
|
|
6999
8928
|
return blocks;
|
|
7000
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
|
+
}
|
|
7001
8966
|
function allowsInsert(table) {
|
|
7002
8967
|
return table.policies.write !== void 0 || table.policies.insert !== void 0;
|
|
7003
8968
|
}
|
|
@@ -7020,6 +8985,10 @@ function renderObjectType(ctx) {
|
|
|
7020
8985
|
const deprecated = column.deprecated ? " @deprecated" : "";
|
|
7021
8986
|
lines.push(` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}${bang}${deprecated}`);
|
|
7022
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
|
+
}
|
|
7023
8992
|
for (const link of table.links) {
|
|
7024
8993
|
if (link.description !== void 0) lines.push(...descriptionLines(link.description, " "));
|
|
7025
8994
|
if (link.kind === "toPlatformUser") {
|
|
@@ -7088,7 +9057,10 @@ function numericishColumns(table) {
|
|
|
7088
9057
|
return table.columns.filter((column) => typeof column.type !== "object" && NUMERICISH.has(column.type));
|
|
7089
9058
|
}
|
|
7090
9059
|
function jsonbColumns(table) {
|
|
7091
|
-
return table.
|
|
9060
|
+
return clientWritableColumns(table).filter((column) => column.type === "jsonb");
|
|
9061
|
+
}
|
|
9062
|
+
function clientWritableColumns(table) {
|
|
9063
|
+
return table.columns;
|
|
7092
9064
|
}
|
|
7093
9065
|
function renderAggregateTypes(ctx) {
|
|
7094
9066
|
const { table, names } = ctx;
|
|
@@ -7120,6 +9092,32 @@ ${sumLines.join("\n")}
|
|
|
7120
9092
|
}
|
|
7121
9093
|
return blocks;
|
|
7122
9094
|
}
|
|
9095
|
+
function groupableColumns(table) {
|
|
9096
|
+
return table.columns.filter((column) => column.type !== "jsonb");
|
|
9097
|
+
}
|
|
9098
|
+
function renderGroupByTypes(ctx) {
|
|
9099
|
+
const { table, names } = ctx;
|
|
9100
|
+
const groupable = groupableColumns(table);
|
|
9101
|
+
if (groupable.length === 0) return [];
|
|
9102
|
+
const keyLines = groupable.map((column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`);
|
|
9103
|
+
return [
|
|
9104
|
+
renderEnum(names.groupByColumnEnum, groupable.map((column) => fieldName(column.name))),
|
|
9105
|
+
`input ${names.groupByInput} {
|
|
9106
|
+
column: ${names.groupByColumnEnum}!
|
|
9107
|
+
bucket: ${DATE_BUCKET_ENUM}
|
|
9108
|
+
}`,
|
|
9109
|
+
`type ${names.groupKey} {
|
|
9110
|
+
${keyLines.join("\n")}
|
|
9111
|
+
}`,
|
|
9112
|
+
`type ${names.aggregateGroup} {
|
|
9113
|
+
key: ${names.groupKey}!
|
|
9114
|
+
aggregate: ${names.aggregateFields}
|
|
9115
|
+
}`
|
|
9116
|
+
];
|
|
9117
|
+
}
|
|
9118
|
+
function groupRootArgs(names) {
|
|
9119
|
+
return `where: ${names.boolExp}, groupBy: [${names.groupByInput}!]!, limit: Int, offset: Int`;
|
|
9120
|
+
}
|
|
7123
9121
|
function renderAggregateBoolExp(ctx) {
|
|
7124
9122
|
const { names } = ctx;
|
|
7125
9123
|
return [
|
|
@@ -7145,12 +9143,15 @@ function constraintValues(table) {
|
|
|
7145
9143
|
}
|
|
7146
9144
|
return [...values].sort();
|
|
7147
9145
|
}
|
|
9146
|
+
function clientSettableColumns(table) {
|
|
9147
|
+
return clientWritableColumns(table).filter((column) => column.onUpdateFn === void 0);
|
|
9148
|
+
}
|
|
7148
9149
|
function renderWriteBlocks(ctx, access) {
|
|
7149
9150
|
const { table, names, pinned } = ctx;
|
|
7150
9151
|
const blocks = [];
|
|
7151
9152
|
if (access.insert) {
|
|
7152
9153
|
const pinnedSet = new Set(pinned);
|
|
7153
|
-
const insertable = table.
|
|
9154
|
+
const insertable = clientWritableColumns(table).filter(
|
|
7154
9155
|
(column) => !pinnedSet.has(column.name) && !pinnedSet.has(column.columnName)
|
|
7155
9156
|
);
|
|
7156
9157
|
if (insertable.length === 0) {
|
|
@@ -7166,7 +9167,7 @@ ${insertLines.join("\n")}
|
|
|
7166
9167
|
}`);
|
|
7167
9168
|
}
|
|
7168
9169
|
if (access.update) {
|
|
7169
|
-
const setLines = table.
|
|
9170
|
+
const setLines = clientSettableColumns(table).map(
|
|
7170
9171
|
(column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
|
|
7171
9172
|
);
|
|
7172
9173
|
blocks.push(`input ${names.setInput} {
|
|
@@ -7193,7 +9194,7 @@ ${incLines.join("\n")}
|
|
|
7193
9194
|
if (access.insert && allowsOnConflict(table) && constraints.length > 0) {
|
|
7194
9195
|
for (const value of constraints) assertEnumValue(value, `constraint enum for table "${table.table}"`);
|
|
7195
9196
|
blocks.push(renderEnum(names.constraintEnum, constraints));
|
|
7196
|
-
const updateColumns = table.
|
|
9197
|
+
const updateColumns = clientSettableColumns(table).map((column) => fieldName(column.name));
|
|
7197
9198
|
blocks.push(renderEnum(names.updateColumnEnum, updateColumns));
|
|
7198
9199
|
blocks.push(
|
|
7199
9200
|
`input ${names.onConflict} {
|
|
@@ -7225,6 +9226,9 @@ function renderQueryRoot(tables, namesByTable) {
|
|
|
7225
9226
|
lines.push(` ${names.byPkRoot}(${args}): ${names.typename}`);
|
|
7226
9227
|
}
|
|
7227
9228
|
lines.push(` ${names.aggregateRoot}(${rootArgs(names)}): ${names.aggregate}!`);
|
|
9229
|
+
if (groupableColumns(table).length > 0) {
|
|
9230
|
+
lines.push(` ${names.aggregateGroupsRoot}(${groupRootArgs(names)}): [${names.aggregateGroup}!]!`);
|
|
9231
|
+
}
|
|
7228
9232
|
}
|
|
7229
9233
|
lines.push("}");
|
|
7230
9234
|
return lines.join("\n");
|
|
@@ -7240,6 +9244,10 @@ function renderMutationRoot(tables, namesByTable) {
|
|
|
7240
9244
|
if (!canInsert && !canUpdate) continue;
|
|
7241
9245
|
const names = namesByTable.get(table.table);
|
|
7242
9246
|
if (!names) continue;
|
|
9247
|
+
if (table.resolved !== void 0) {
|
|
9248
|
+
lines.push(...renderResolvedMutationFields(table, names));
|
|
9249
|
+
continue;
|
|
9250
|
+
}
|
|
7243
9251
|
const constraints = constraintValues(table);
|
|
7244
9252
|
const onConflictArg = canInsert && allowsOnConflict(table) && constraints.length > 0 ? `, onConflict: ${names.onConflict}` : "";
|
|
7245
9253
|
const updateArgs = [`_set: ${names.setInput}`];
|
|
@@ -7293,6 +9301,92 @@ function renderCowMutationFields(table, names) {
|
|
|
7293
9301
|
}
|
|
7294
9302
|
return lines;
|
|
7295
9303
|
}
|
|
9304
|
+
var ORM_MIGRATION_EXPORT_FILE = "orm.migration.json";
|
|
9305
|
+
var ORM_MIGRATION_EXPORT_MAX_BYTES = ORM_REGISTRATION_MAX_BYTES;
|
|
9306
|
+
var hash = z.string().regex(/^sha256:[0-9a-f]{64}$/);
|
|
9307
|
+
var ormMigrationPayloadSchema = z.object({
|
|
9308
|
+
canonical_definition: canonicalDefinitionSchema,
|
|
9309
|
+
documents: z.array(z.object({ path: z.string().min(1).max(2048), contents: z.string().max(1e6) }).strict()).max(5e3),
|
|
9310
|
+
client_manifest: z.object({ ops_hash: hash, operations: z.record(z.string(), hash) }).strict(),
|
|
9311
|
+
sdl: z.string().refine((value) => Buffer.byteLength(value, "utf8") <= MAX_GENERATED_SDL_BYTES, {
|
|
9312
|
+
message: `sdl exceeds ${MAX_GENERATED_SDL_BYTES} bytes`
|
|
9313
|
+
}),
|
|
9314
|
+
schema_snapshot: z.json(),
|
|
9315
|
+
// The existing ORM committed-migration validator and replay engine remain
|
|
9316
|
+
// authoritative. This envelope hashes the provenance, never authorizes SQL.
|
|
9317
|
+
migration: z.json().optional()
|
|
9318
|
+
}).strict();
|
|
9319
|
+
z.object({
|
|
9320
|
+
schema_version: z.literal(1),
|
|
9321
|
+
schema_hash: hash,
|
|
9322
|
+
payload: ormMigrationPayloadSchema,
|
|
9323
|
+
checksum: z.string().regex(/^[0-9a-f]{64}$/)
|
|
9324
|
+
}).strict();
|
|
9325
|
+
function checksum(value) {
|
|
9326
|
+
return createHash("sha256").update(stableJson(value)).digest("hex");
|
|
9327
|
+
}
|
|
9328
|
+
function createOrmMigrationExport({ schemaHash, payload }) {
|
|
9329
|
+
const contents = { schema_version: 1, schema_hash: hash.parse(schemaHash), payload: ormMigrationPayloadSchema.parse(payload) };
|
|
9330
|
+
return { ...contents, checksum: checksum(contents) };
|
|
9331
|
+
}
|
|
9332
|
+
function renderOrmMigrationExport(exported) {
|
|
9333
|
+
const text4 = `${JSON.stringify(exported, null, 2)}
|
|
9334
|
+
`;
|
|
9335
|
+
if (Buffer.byteLength(text4, "utf8") > ORM_MIGRATION_EXPORT_MAX_BYTES) throw new Error("ORM migration export exceeds the size limit");
|
|
9336
|
+
return text4;
|
|
9337
|
+
}
|
|
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
|
+
}
|
|
7296
9390
|
|
|
7297
9391
|
// src/cli.ts
|
|
7298
9392
|
var BASE_URL_ENV = "SEQUENCE_ORM_BASE_URL";
|
|
@@ -7300,7 +9394,9 @@ var TOKEN_ENV = "SEQUENCE_ORM_TOKEN";
|
|
|
7300
9394
|
var ORM_MODULE_PATH_ENV = "SEQUENCE_ORM_MODULE_PATH";
|
|
7301
9395
|
var USAGE = `usage:
|
|
7302
9396
|
seq-studio orm init <dir> scaffold a namespace package (v2: TypeScript schema + GraphQL documents)
|
|
7303
|
-
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)
|
|
9398
|
+
seq-studio orm generate [dir] --migration-export [--check]
|
|
9399
|
+
export reviewed ORM inputs for a coordinated pipeline migration; --check verifies without writing
|
|
7304
9400
|
seq-studio orm validate [dir] parse + validate definitions and verify committed migrations are in sync
|
|
7305
9401
|
seq-studio orm plan [dir] -e <env> offline dry run: compile definitions and diff against the registry (no database, no writes)
|
|
7306
9402
|
seq-studio orm apply [dir] -e <env> author the migration, register, apply, and refresh the generated outputs (the everyday command)
|
|
@@ -7315,6 +9411,8 @@ var USAGE = `usage:
|
|
|
7315
9411
|
v2 = sequence.config.ts + schema/*.ts + graphql/**; v1 (legacy) = one <table>.yaml per table.
|
|
7316
9412
|
Environments: see \`seq-studio envs list\` (discovered after you authenticate).
|
|
7317
9413
|
Authenticate with: seq-studio login
|
|
9414
|
+
Execute endpoint rollout: SEQUENCE_ORM_ATLAS_EXECUTE_V2=1 opts into /api/data/v2;
|
|
9415
|
+
otherwise execute uses /_data/v2. Server rollout and authorization still apply.
|
|
7318
9416
|
`;
|
|
7319
9417
|
var tokenProvider = null;
|
|
7320
9418
|
function setTokenProvider(provider) {
|
|
@@ -7467,6 +9565,7 @@ async function walkFiles(root, prefix = "") {
|
|
|
7467
9565
|
var SCHEMA_GRAPHQL_FILE = "schema.graphql";
|
|
7468
9566
|
var OPERATIONS_MANIFEST_FILE = "operations.manifest.json";
|
|
7469
9567
|
var TYPE_POLICIES_FILE = "typePolicies.gen.ts";
|
|
9568
|
+
var SOURCE_CONTRACTS_FILE = "sources.gen.ts";
|
|
7470
9569
|
var GRAPHQL_DIRNAME = "graphql";
|
|
7471
9570
|
var CODEGEN_CONFIG_FILE = "codegen.ts";
|
|
7472
9571
|
function ormLoadOptions() {
|
|
@@ -7486,9 +9585,10 @@ async function compileV2Project(dir) {
|
|
|
7486
9585
|
const documents = await collectGraphqlDocuments(dir);
|
|
7487
9586
|
const result = compileNamespace({ canonical: loaded.canonical, sdl, documents });
|
|
7488
9587
|
if (!result.ok) return { ok: false, diagnostics: result.diagnostics };
|
|
9588
|
+
const diagnostics = [...result.diagnostics, ...roleWarnings({ canonical: loaded.canonical, manifest: result.manifest })];
|
|
7489
9589
|
return {
|
|
7490
9590
|
ok: true,
|
|
7491
|
-
project: { loaded, sdl, documents, manifest: result.manifest, diagnostics
|
|
9591
|
+
project: { loaded, sdl, documents, manifest: result.manifest, diagnostics }
|
|
7492
9592
|
};
|
|
7493
9593
|
}
|
|
7494
9594
|
var GQL_TEMPLATE_RE = /(?:\bgql|\/\*\s*GraphQL\s*\*\/)\s*`([^`]*)`/g;
|
|
@@ -7529,6 +9629,12 @@ function printDiagnostics(diagnostics) {
|
|
|
7529
9629
|
}
|
|
7530
9630
|
async function generateCommand(rest) {
|
|
7531
9631
|
const dir = positional(rest) ?? ".";
|
|
9632
|
+
const migrationExport = rest.includes("--migration-export");
|
|
9633
|
+
const check = rest.includes("--check");
|
|
9634
|
+
if (check && !migrationExport) {
|
|
9635
|
+
console.error("generate --check requires --migration-export");
|
|
9636
|
+
return 1;
|
|
9637
|
+
}
|
|
7532
9638
|
if (await detectProjectFormat(dir) === "v1") {
|
|
7533
9639
|
console.error(`generate needs a v2 project (${NAMESPACE_CONFIG_FILE}) \u2014 convert with: seq-studio orm migrate-from-yaml ${dir}`);
|
|
7534
9640
|
return 1;
|
|
@@ -7536,6 +9642,36 @@ async function generateCommand(rest) {
|
|
|
7536
9642
|
const compiled = await compileV2Project(dir);
|
|
7537
9643
|
printDiagnostics(compiled.ok ? compiled.project.diagnostics : compiled.diagnostics);
|
|
7538
9644
|
if (!compiled.ok) return 1;
|
|
9645
|
+
if (migrationExport) {
|
|
9646
|
+
if (await validateV2Command(dir) !== 0) return 1;
|
|
9647
|
+
const { loaded: loaded2, sdl, documents, manifest: manifest2 } = compiled.project;
|
|
9648
|
+
const workspace = await readMigrationWorkspace(dir, loaded2.canonical.namespace);
|
|
9649
|
+
const migration = await loadCommittedTipMigrationV2({ dir, loaded: loaded2, workspace });
|
|
9650
|
+
const exported = createOrmMigrationExport({ schemaHash: manifest2.schemaHash, payload: {
|
|
9651
|
+
canonical_definition: loaded2.canonical,
|
|
9652
|
+
documents,
|
|
9653
|
+
sdl,
|
|
9654
|
+
client_manifest: { ops_hash: manifest2.opsHash, operations: Object.fromEntries(Object.entries(manifest2.operations).map(([name, operation]) => [name, operation.opHash])) },
|
|
9655
|
+
schema_snapshot: workspace.tipSnapshot,
|
|
9656
|
+
...migration ? { migration } : {}
|
|
9657
|
+
} });
|
|
9658
|
+
const text4 = renderOrmMigrationExport(exported);
|
|
9659
|
+
const path = join(dir, ORM_MIGRATION_EXPORT_FILE);
|
|
9660
|
+
if (check) {
|
|
9661
|
+
const committed = await readFile(path, "utf8").catch((error) => {
|
|
9662
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return void 0;
|
|
9663
|
+
throw error;
|
|
9664
|
+
});
|
|
9665
|
+
if (committed !== text4) {
|
|
9666
|
+
console.error(`${ORM_MIGRATION_EXPORT_FILE} is missing or stale; run seq-studio orm generate ${dir} --migration-export and commit it`);
|
|
9667
|
+
return 1;
|
|
9668
|
+
}
|
|
9669
|
+
console.log(`${ORM_MIGRATION_EXPORT_FILE} matches the current definitions, operations, and committed migration`);
|
|
9670
|
+
return 0;
|
|
9671
|
+
}
|
|
9672
|
+
await writeFile(path, text4);
|
|
9673
|
+
console.log(`wrote ${ORM_MIGRATION_EXPORT_FILE} (${exported.checksum.slice(0, 12)})`);
|
|
9674
|
+
}
|
|
7539
9675
|
await writeGenerateOutputs(dir, compiled.project);
|
|
7540
9676
|
const { loaded, manifest } = compiled.project;
|
|
7541
9677
|
console.log(
|
|
@@ -7556,6 +9692,8 @@ async function writeGenerateOutputs(dir, project) {
|
|
|
7556
9692
|
await writeFile(join(dir, OPERATIONS_MANIFEST_FILE), `${JSON.stringify(manifestFile, null, 2)}
|
|
7557
9693
|
`);
|
|
7558
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);
|
|
7559
9697
|
}
|
|
7560
9698
|
async function runConsumerCodegen(dir) {
|
|
7561
9699
|
const codegenPath = join(dir, CODEGEN_CONFIG_FILE);
|
|
@@ -7651,9 +9789,10 @@ async function executeV2Command(rest) {
|
|
|
7651
9789
|
);
|
|
7652
9790
|
}
|
|
7653
9791
|
const variables = await readExecuteVariables(variablesFile);
|
|
9792
|
+
const executePrefix = process.env["SEQUENCE_ORM_ATLAS_EXECUTE_V2"] === "1" ? "/api/data/v2" : "/_data/v2";
|
|
7654
9793
|
const response = await apiRequestStrict(
|
|
7655
9794
|
"POST",
|
|
7656
|
-
|
|
9795
|
+
`${executePrefix}/${loaded.canonical.namespace}`,
|
|
7657
9796
|
{ op: selected.hash, variables }
|
|
7658
9797
|
);
|
|
7659
9798
|
console.log(JSON.stringify(response, null, 2));
|
|
@@ -7822,16 +9961,28 @@ async function checkWorkspaceV2({
|
|
|
7822
9961
|
return 1;
|
|
7823
9962
|
}
|
|
7824
9963
|
const previousSnapshot = tip.ordinal > 1 ? await readSnapshot(dir, tip.ordinal - 1) : void 0;
|
|
7825
|
-
const plan = await planMigrationV2({ canonical: loaded.canonical, previousSnapshot });
|
|
7826
|
-
const regenerated = renderMigrationSql({ name: tip.name, namespace, contentHash: loaded.hash, plan });
|
|
7827
9964
|
const committed = await readMigrationSql(dir, tip);
|
|
7828
|
-
|
|
9965
|
+
const { reproduced } = await planCommittedTipV2({ loaded, tip, previousSnapshot, committed });
|
|
9966
|
+
if (!reproduced) {
|
|
7829
9967
|
log(`${MIGRATIONS_DIRNAME}/${tip.name}.sql does not regenerate from the definitions \u2014 re-run \`seq-studio orm diff\``);
|
|
7830
9968
|
return 1;
|
|
7831
9969
|
}
|
|
7832
9970
|
log(`migrations in sync \u2014 ${workspace.journal.entries.length} committed, tip ${tip.name}`);
|
|
7833
9971
|
return 0;
|
|
7834
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
|
+
}
|
|
7835
9986
|
function matchesRebaseTip({
|
|
7836
9987
|
committed,
|
|
7837
9988
|
tip,
|
|
@@ -7934,7 +10085,6 @@ async function loadCommittedTipMigrationV2({
|
|
|
7934
10085
|
loaded,
|
|
7935
10086
|
workspace
|
|
7936
10087
|
}) {
|
|
7937
|
-
const namespace = loaded.canonical.namespace;
|
|
7938
10088
|
const tip = workspace.journal.entries.at(-1);
|
|
7939
10089
|
if (!tip) return void 0;
|
|
7940
10090
|
if (tip.content_hash !== loaded.hash) {
|
|
@@ -7944,10 +10094,9 @@ async function loadCommittedTipMigrationV2({
|
|
|
7944
10094
|
);
|
|
7945
10095
|
}
|
|
7946
10096
|
const previousSnapshot = tip.ordinal > 1 ? await readSnapshot(dir, tip.ordinal - 1) : void 0;
|
|
7947
|
-
const plan = await planMigrationV2({ canonical: loaded.canonical, previousSnapshot });
|
|
7948
10097
|
const committed = await readMigrationSql(dir, tip);
|
|
7949
|
-
const
|
|
7950
|
-
if (
|
|
10098
|
+
const { plan, reproduced } = await planCommittedTipV2({ loaded, tip, previousSnapshot, committed });
|
|
10099
|
+
if (!reproduced) {
|
|
7951
10100
|
throw new OrmDefinitionError(
|
|
7952
10101
|
`${tip.name}.sql does not regenerate from the definitions \u2014 run \`seq-studio orm diff --check\``,
|
|
7953
10102
|
MIGRATIONS_DIRNAME
|
|
@@ -8168,6 +10317,7 @@ function columnProperty(table, column, singlePrimaryKey) {
|
|
|
8168
10317
|
}
|
|
8169
10318
|
chain += `.default(${JSON.stringify(column.default)})`;
|
|
8170
10319
|
}
|
|
10320
|
+
if (column.onUpdateFn !== void 0) chain += `.onUpdate(${quote(column.onUpdateFn)})`;
|
|
8171
10321
|
if (column.description !== void 0) chain += `.description(${quote(column.description)})`;
|
|
8172
10322
|
if (column.renamedFrom !== void 0) chain += `.renamedFrom(${quote(column.renamedFrom)})`;
|
|
8173
10323
|
if (column.cast !== void 0) chain += `.cast(${quote(column.cast)})`;
|