@sequenceholdings/orm 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -1
- package/dist/authoring.cjs +1619 -121
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +252 -8
- package/dist/authoring.d.ts +252 -8
- package/dist/authoring.js +1607 -123
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +713 -41
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +1520 -51
- package/dist/canonical.d.ts +1520 -51
- package/dist/canonical.js +693 -42
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +7103 -3901
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +7104 -3902
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs +122 -40
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +85 -25
- package/dist/client.d.ts +85 -25
- package/dist/client.js +119 -41
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +2464 -450
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +79 -4
- package/dist/compiler.d.ts +79 -4
- package/dist/compiler.js +2457 -451
- package/dist/compiler.js.map +1 -1
- package/dist/constants-C6EG6Wq7.d.cts +99 -0
- package/dist/constants-C6EG6Wq7.d.ts +99 -0
- package/dist/definition-DjfC7Lll.d.cts +91 -0
- package/dist/definition-DjfC7Lll.d.ts +91 -0
- package/dist/dynamic-input.cjs +2 -1
- package/dist/dynamic-input.cjs.map +1 -1
- package/dist/dynamic-input.d.cts +9 -1
- package/dist/dynamic-input.d.ts +9 -1
- package/dist/dynamic-input.js +2 -1
- package/dist/dynamic-input.js.map +1 -1
- package/dist/index.cjs +2656 -418
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +693 -116
- package/dist/index.d.ts +693 -116
- package/dist/index.js +2622 -420
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +811 -48
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +670 -27
- package/dist/ir.d.ts +670 -27
- package/dist/ir.js +802 -49
- package/dist/ir.js.map +1 -1
- package/dist/{naming-DCn94LJ4.d.ts → naming-CPfx7EfF.d.ts} +14 -1
- package/dist/{naming-DPglwzkB.d.cts → naming-CU9ThP_E.d.cts} +14 -1
- package/dist/policy-1iZbGnZZ.d.cts +24 -0
- package/dist/policy-1iZbGnZZ.d.ts +24 -0
- package/dist/predicate-v2.cjs +804 -51
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.d.cts +2 -1
- package/dist/predicate-v2.d.ts +2 -1
- package/dist/predicate-v2.js +804 -51
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +1014 -75
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.d.cts +14 -3
- package/dist/sdl.d.ts +14 -3
- package/dist/sdl.js +1012 -76
- package/dist/sdl.js.map +1 -1
- package/package.json +13 -7
- package/src/codegen-gql/server-fns.cjs +2 -2
- package/templates/orm-namespace/AGENTS.md +10 -0
- package/templates/orm-namespace/CLAUDE.md +1 -82
- package/templates/orm-namespace/codegen.preset.d.cts +7 -0
- package/templates/orm-namespace/codegen.ts +2 -1
- package/templates/orm-namespace/graphql/example/queries.ts +4 -3
- package/templates/orm-namespace/package.json +2 -1
- package/templates/orm-namespace/schema/tables.ts +3 -2
- package/templates/orm-namespace/sequence.config.ts +7 -11
package/dist/authoring.cjs
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
var drizzleOrm = require('drizzle-orm');
|
|
4
4
|
var pgCore = require('drizzle-orm/pg-core');
|
|
5
|
-
var crypto = require('crypto');
|
|
6
5
|
var zod = require('zod');
|
|
6
|
+
var crypto = require('crypto');
|
|
7
7
|
var jiti = require('jiti');
|
|
8
8
|
var promises = require('fs/promises');
|
|
9
9
|
var module$1 = require('module');
|
|
@@ -37,7 +37,8 @@ var ColumnBuilder = class {
|
|
|
37
37
|
description: void 0,
|
|
38
38
|
renamedFrom: void 0,
|
|
39
39
|
cast: void 0,
|
|
40
|
-
deprecated: false
|
|
40
|
+
deprecated: false,
|
|
41
|
+
cell: void 0
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
/** Marks this column as (part of) the primary key. Implies NOT NULL. */
|
|
@@ -97,6 +98,11 @@ var ColumnBuilder = class {
|
|
|
97
98
|
this.state.deprecated = true;
|
|
98
99
|
return this;
|
|
99
100
|
}
|
|
101
|
+
/** @internal System cell-metadata part of logical column `of` (generated resolved views only). */
|
|
102
|
+
cellPart(of, part) {
|
|
103
|
+
this.state.cell = { of, part };
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
100
106
|
/** Whether `.primaryKey()` was called — collected into the table-level PK. */
|
|
101
107
|
get isPrimaryKey() {
|
|
102
108
|
return this.state.primaryKey;
|
|
@@ -125,6 +131,7 @@ var ColumnBuilder = class {
|
|
|
125
131
|
if (this.state.description !== void 0) column.description = this.state.description;
|
|
126
132
|
if (this.state.renamedFrom !== void 0) column.renamedFrom = this.state.renamedFrom;
|
|
127
133
|
if (this.state.cast !== void 0) column.cast = this.state.cast;
|
|
134
|
+
if (this.state.cell !== void 0) column.cell = { ...this.state.cell };
|
|
128
135
|
return column;
|
|
129
136
|
}
|
|
130
137
|
};
|
|
@@ -151,8 +158,24 @@ var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
|
|
151
158
|
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
152
159
|
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
153
160
|
var MAX_IDENTIFIER_LENGTH = 63;
|
|
154
|
-
var
|
|
161
|
+
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]{0,62}\.[a-z][a-z0-9_]{0,62}$/;
|
|
162
|
+
var SYNCED_SOURCE_PATTERN = QUALIFIED_TABLE_PATTERN;
|
|
155
163
|
var HASH_DOMAIN = "sequence-orm-definitions\0";
|
|
164
|
+
var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
|
|
165
|
+
var CELL_PART_SUFFIX = {
|
|
166
|
+
at: "__at",
|
|
167
|
+
by: "__by",
|
|
168
|
+
rev: "__rev",
|
|
169
|
+
source: "__src",
|
|
170
|
+
conflicts: "__lost"
|
|
171
|
+
};
|
|
172
|
+
var MAX_TRACKED_COLUMN_NAME = MAX_IDENTIFIER_LENGTH - 6;
|
|
173
|
+
var CELLS_FIELD = "cells";
|
|
174
|
+
function cellColumnName(column, part) {
|
|
175
|
+
return `${column}${CELL_PART_SUFFIX[part]}`;
|
|
176
|
+
}
|
|
177
|
+
var MATERIALIZE_ACTOR = "orm:materialize";
|
|
178
|
+
JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
|
|
156
179
|
|
|
157
180
|
// src/errors.ts
|
|
158
181
|
var OrmDefinitionError = class extends Error {
|
|
@@ -213,6 +236,9 @@ function registerNamespaceConfig(value) {
|
|
|
213
236
|
// src/authoring/table.ts
|
|
214
237
|
var TABLE_BRAND = "seq-table";
|
|
215
238
|
var LINK_BRAND = "seq-link";
|
|
239
|
+
function objectRef(namespace, table) {
|
|
240
|
+
return { kind: "seq-external-object", namespace, table };
|
|
241
|
+
}
|
|
216
242
|
function toOne(target, options) {
|
|
217
243
|
return {
|
|
218
244
|
kind: LINK_BRAND,
|
|
@@ -221,6 +247,8 @@ function toOne(target, options) {
|
|
|
221
247
|
on: options.on,
|
|
222
248
|
references: options.references,
|
|
223
249
|
enforce: options.enforce ?? false,
|
|
250
|
+
onDelete: options.onDelete,
|
|
251
|
+
onUpdate: options.onUpdate,
|
|
224
252
|
description: options.description
|
|
225
253
|
};
|
|
226
254
|
}
|
|
@@ -232,6 +260,8 @@ function toMany(target, options) {
|
|
|
232
260
|
on: options.on,
|
|
233
261
|
references: options.references,
|
|
234
262
|
enforce: false,
|
|
263
|
+
onDelete: void 0,
|
|
264
|
+
onUpdate: void 0,
|
|
235
265
|
description: options.description
|
|
236
266
|
};
|
|
237
267
|
}
|
|
@@ -243,6 +273,8 @@ function toPlatformUser(options) {
|
|
|
243
273
|
on: options.on,
|
|
244
274
|
references: options.references,
|
|
245
275
|
enforce: false,
|
|
276
|
+
onDelete: void 0,
|
|
277
|
+
onUpdate: void 0,
|
|
246
278
|
description: options.description
|
|
247
279
|
};
|
|
248
280
|
}
|
|
@@ -260,45 +292,51 @@ function assertIdentifier({ value, what, file, pattern = IDENTIFIER_PATTERN }) {
|
|
|
260
292
|
);
|
|
261
293
|
}
|
|
262
294
|
}
|
|
263
|
-
function defineTable(
|
|
295
|
+
function defineTable(name2, columns, config = {}) {
|
|
264
296
|
const file = activeAuthoringContext()?.currentFile;
|
|
265
|
-
assertIdentifier({ value:
|
|
297
|
+
assertIdentifier({ value: name2, what: "table name", file });
|
|
266
298
|
const entries = Object.entries(columns);
|
|
267
299
|
if (entries.length === 0) {
|
|
268
|
-
throw new OrmDefinitionError(`table "${
|
|
300
|
+
throw new OrmDefinitionError(`table "${name2}" declares no columns`, file);
|
|
269
301
|
}
|
|
270
302
|
const seenColumnNames = /* @__PURE__ */ new Map();
|
|
271
303
|
for (const [propertyName, builder] of entries) {
|
|
272
304
|
if (!isColumnBuilder(builder)) {
|
|
273
305
|
throw new OrmDefinitionError(
|
|
274
|
-
`table "${
|
|
306
|
+
`table "${name2}": column "${propertyName}" is not a column builder \u2014 use the t.* factories (t.uuid(), t.text(), \u2026)`,
|
|
275
307
|
file
|
|
276
308
|
);
|
|
277
309
|
}
|
|
278
310
|
const columnName = builder.columnNameFor(propertyName);
|
|
279
|
-
assertIdentifier({ value: columnName, what: `table "${
|
|
311
|
+
assertIdentifier({ value: columnName, what: `table "${name2}": column "${propertyName}" column name`, file, pattern: COLUMN_IDENTIFIER_PATTERN });
|
|
312
|
+
if (config.materializes === void 0 && (propertyName === CELLS_FIELD || Object.values(CELL_PART_SUFFIX).some((suffix) => columnName.endsWith(suffix) || propertyName.endsWith(suffix)))) {
|
|
313
|
+
throw new OrmDefinitionError(
|
|
314
|
+
`table "${name2}": "${propertyName}" collides with the reserved cell-metadata names ('${CELLS_FIELD}', or a ${Object.values(CELL_PART_SUFFIX).join("/")} suffix)`,
|
|
315
|
+
file
|
|
316
|
+
);
|
|
317
|
+
}
|
|
280
318
|
const existing = seenColumnNames.get(columnName);
|
|
281
319
|
if (existing !== void 0) {
|
|
282
320
|
throw new OrmDefinitionError(
|
|
283
|
-
`table "${
|
|
321
|
+
`table "${name2}": columns "${existing}" and "${propertyName}" both map to Postgres column "${columnName}"`,
|
|
284
322
|
file
|
|
285
323
|
);
|
|
286
324
|
}
|
|
287
325
|
seenColumnNames.set(columnName, propertyName);
|
|
288
326
|
}
|
|
289
|
-
const table = { kind: TABLE_BRAND, tableName:
|
|
327
|
+
const table = { kind: TABLE_BRAND, tableName: name2, columns, config, source: void 0, file };
|
|
290
328
|
registerTable(table);
|
|
291
329
|
return table;
|
|
292
330
|
}
|
|
293
|
-
function defineSyncedTable(
|
|
331
|
+
function defineSyncedTable(name2, columnsOrConfig, maybeConfig) {
|
|
294
332
|
const file = activeAuthoringContext()?.currentFile;
|
|
295
|
-
assertIdentifier({ value:
|
|
333
|
+
assertIdentifier({ value: name2, what: "table name", file });
|
|
296
334
|
const isTyped = maybeConfig !== void 0;
|
|
297
335
|
const columns = isTyped ? columnsOrConfig : {};
|
|
298
336
|
const config = isTyped ? maybeConfig : columnsOrConfig;
|
|
299
337
|
if (!SYNCED_SOURCE_PATTERN.test(config.source)) {
|
|
300
338
|
throw new OrmDefinitionError(
|
|
301
|
-
`synced table "${
|
|
339
|
+
`synced table "${name2}": source "${config.source}" must be a qualified lowercase schema.table identifier`,
|
|
302
340
|
file
|
|
303
341
|
);
|
|
304
342
|
}
|
|
@@ -308,21 +346,21 @@ function defineSyncedTable(name, columnsOrConfig, maybeConfig) {
|
|
|
308
346
|
for (const [propertyName, builder] of entries) {
|
|
309
347
|
if (!isColumnBuilder(builder)) {
|
|
310
348
|
throw new OrmDefinitionError(
|
|
311
|
-
`synced table "${
|
|
349
|
+
`synced table "${name2}": column "${propertyName}" is not a column builder \u2014 use the t.* factories (t.uuid(), t.text(), \u2026)`,
|
|
312
350
|
file
|
|
313
351
|
);
|
|
314
352
|
}
|
|
315
353
|
const columnName = builder.columnNameFor(propertyName);
|
|
316
354
|
assertIdentifier({
|
|
317
355
|
value: columnName,
|
|
318
|
-
what: `synced table "${
|
|
356
|
+
what: `synced table "${name2}": column "${propertyName}" column name`,
|
|
319
357
|
file,
|
|
320
358
|
pattern: COLUMN_IDENTIFIER_PATTERN
|
|
321
359
|
});
|
|
322
360
|
const existing = seenColumnNames.get(columnName);
|
|
323
361
|
if (existing !== void 0) {
|
|
324
362
|
throw new OrmDefinitionError(
|
|
325
|
-
`synced table "${
|
|
363
|
+
`synced table "${name2}": columns "${existing}" and "${propertyName}" both map to Postgres column "${columnName}"`,
|
|
326
364
|
file
|
|
327
365
|
);
|
|
328
366
|
}
|
|
@@ -333,13 +371,13 @@ function defineSyncedTable(name, columnsOrConfig, maybeConfig) {
|
|
|
333
371
|
const hasConfigPk = config.primaryKey !== void 0 && config.primaryKey.length > 0;
|
|
334
372
|
if (columnLevelPk.length === 0 && !hasConfigPk) {
|
|
335
373
|
throw new OrmDefinitionError(
|
|
336
|
-
`synced table "${
|
|
374
|
+
`synced table "${name2}" declares columns but no primary key \u2014 use .primaryKey() or config.primaryKey`,
|
|
337
375
|
file
|
|
338
376
|
);
|
|
339
377
|
}
|
|
340
378
|
if (columnLevelPk.length > 0 && hasConfigPk) {
|
|
341
379
|
throw new OrmDefinitionError(
|
|
342
|
-
`synced table "${
|
|
380
|
+
`synced table "${name2}": declare the primary key either with .primaryKey() on columns or with config.primaryKey \u2014 not both`,
|
|
343
381
|
file
|
|
344
382
|
);
|
|
345
383
|
}
|
|
@@ -351,7 +389,7 @@ function defineSyncedTable(name, columnsOrConfig, maybeConfig) {
|
|
|
351
389
|
if (config.policies !== void 0) tableConfig.policies = { read: config.policies.read };
|
|
352
390
|
const table = {
|
|
353
391
|
kind: TABLE_BRAND,
|
|
354
|
-
tableName:
|
|
392
|
+
tableName: name2,
|
|
355
393
|
columns,
|
|
356
394
|
config: tableConfig,
|
|
357
395
|
source: config.source,
|
|
@@ -360,26 +398,26 @@ function defineSyncedTable(name, columnsOrConfig, maybeConfig) {
|
|
|
360
398
|
registerTable(table);
|
|
361
399
|
return table;
|
|
362
400
|
}
|
|
363
|
-
function defineEnum(
|
|
401
|
+
function defineEnum(name2, values) {
|
|
364
402
|
const file = activeAuthoringContext()?.currentFile;
|
|
365
|
-
assertIdentifier({ value:
|
|
403
|
+
assertIdentifier({ value: name2, what: "enum name", file });
|
|
366
404
|
if (values.length === 0 || values.length > 100) {
|
|
367
|
-
throw new OrmDefinitionError(`enum "${
|
|
405
|
+
throw new OrmDefinitionError(`enum "${name2}" must declare between 1 and 100 values`, file);
|
|
368
406
|
}
|
|
369
407
|
const seen = /* @__PURE__ */ new Set();
|
|
370
408
|
for (const value of values) {
|
|
371
409
|
if (value.length === 0 || value.length > MAX_IDENTIFIER_LENGTH) {
|
|
372
410
|
throw new OrmDefinitionError(
|
|
373
|
-
`enum "${
|
|
411
|
+
`enum "${name2}": value ${JSON.stringify(value)} must be 1-${MAX_IDENTIFIER_LENGTH} characters`,
|
|
374
412
|
file
|
|
375
413
|
);
|
|
376
414
|
}
|
|
377
415
|
if (seen.has(value)) {
|
|
378
|
-
throw new OrmDefinitionError(`enum "${
|
|
416
|
+
throw new OrmDefinitionError(`enum "${name2}" declares duplicate value "${value}"`, file);
|
|
379
417
|
}
|
|
380
418
|
seen.add(value);
|
|
381
419
|
}
|
|
382
|
-
const ref = { kind: ENUM_BRAND, name, values: [...values] };
|
|
420
|
+
const ref = { kind: ENUM_BRAND, name: name2, values: [...values] };
|
|
383
421
|
registerEnum(ref);
|
|
384
422
|
return ref;
|
|
385
423
|
}
|
|
@@ -387,52 +425,52 @@ var VIEW_BRAND = "seq-view";
|
|
|
387
425
|
function isSeqView(value) {
|
|
388
426
|
return typeof value === "object" && value !== null && value.kind === VIEW_BRAND;
|
|
389
427
|
}
|
|
390
|
-
function defineView(
|
|
428
|
+
function defineView(name2, columns, config) {
|
|
391
429
|
const file = activeAuthoringContext()?.currentFile;
|
|
392
|
-
if (!IDENTIFIER_PATTERN.test(
|
|
430
|
+
if (!IDENTIFIER_PATTERN.test(name2) || name2.length > MAX_IDENTIFIER_LENGTH) {
|
|
393
431
|
throw new OrmDefinitionError(
|
|
394
|
-
`view name "${
|
|
432
|
+
`view name "${name2}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`,
|
|
395
433
|
file
|
|
396
434
|
);
|
|
397
435
|
}
|
|
398
436
|
const entries = Object.entries(columns);
|
|
399
|
-
if (entries.length === 0) throw new OrmDefinitionError(`view "${
|
|
437
|
+
if (entries.length === 0) throw new OrmDefinitionError(`view "${name2}" declares no columns`, file);
|
|
400
438
|
const seenColumnNames = /* @__PURE__ */ new Map();
|
|
401
439
|
for (const [propertyName, builder] of entries) {
|
|
402
440
|
if (!isColumnBuilder(builder)) {
|
|
403
441
|
throw new OrmDefinitionError(
|
|
404
|
-
`view "${
|
|
442
|
+
`view "${name2}": column "${propertyName}" is not a column builder \u2014 use the t.* factories`,
|
|
405
443
|
file
|
|
406
444
|
);
|
|
407
445
|
}
|
|
408
446
|
const columnName = builder.columnNameFor(propertyName);
|
|
409
447
|
if (!COLUMN_IDENTIFIER_PATTERN.test(columnName) || columnName.length > MAX_IDENTIFIER_LENGTH) {
|
|
410
|
-
throw new OrmDefinitionError(`view "${
|
|
448
|
+
throw new OrmDefinitionError(`view "${name2}": column "${propertyName}" has invalid column name "${columnName}"`, file);
|
|
411
449
|
}
|
|
412
450
|
const existing = seenColumnNames.get(columnName);
|
|
413
451
|
if (existing !== void 0) {
|
|
414
452
|
throw new OrmDefinitionError(
|
|
415
|
-
`view "${
|
|
453
|
+
`view "${name2}": columns "${existing}" and "${propertyName}" both map to Postgres column "${columnName}"`,
|
|
416
454
|
file
|
|
417
455
|
);
|
|
418
456
|
}
|
|
419
457
|
seenColumnNames.set(columnName, propertyName);
|
|
420
458
|
}
|
|
421
|
-
const view = { kind: VIEW_BRAND, viewName:
|
|
459
|
+
const view = { kind: VIEW_BRAND, viewName: name2, columns, config, file };
|
|
422
460
|
registerView(view);
|
|
423
461
|
return view;
|
|
424
462
|
}
|
|
425
463
|
var QUERY_COLUMN_FACTORIES = {
|
|
426
|
-
uuid: (
|
|
427
|
-
text: (
|
|
428
|
-
integer: (
|
|
429
|
-
bigint: (
|
|
430
|
-
numeric: (
|
|
431
|
-
double: (
|
|
432
|
-
boolean: (
|
|
433
|
-
timestamptz: (
|
|
434
|
-
date: (
|
|
435
|
-
jsonb: (
|
|
464
|
+
uuid: (name2) => pgCore.uuid(name2),
|
|
465
|
+
text: (name2) => pgCore.text(name2),
|
|
466
|
+
integer: (name2) => pgCore.integer(name2),
|
|
467
|
+
bigint: (name2) => pgCore.bigint(name2, { mode: "bigint" }),
|
|
468
|
+
numeric: (name2) => pgCore.numeric(name2),
|
|
469
|
+
double: (name2) => pgCore.doublePrecision(name2),
|
|
470
|
+
boolean: (name2) => pgCore.boolean(name2),
|
|
471
|
+
timestamptz: (name2) => pgCore.timestamp(name2, { withTimezone: true }),
|
|
472
|
+
date: (name2) => pgCore.date(name2),
|
|
473
|
+
jsonb: (name2) => pgCore.jsonb(name2)
|
|
436
474
|
};
|
|
437
475
|
function compileViewQueries(input) {
|
|
438
476
|
const schemaName = `orm_${input.namespace}`;
|
|
@@ -488,9 +526,9 @@ function compileViewQueries(input) {
|
|
|
488
526
|
}
|
|
489
527
|
const handle = handles.get(resolved);
|
|
490
528
|
if (!handle) {
|
|
491
|
-
const
|
|
529
|
+
const name2 = isSeqTable(resolved) ? resolved.tableName : resolved.viewName;
|
|
492
530
|
throw new OrmDefinitionError(
|
|
493
|
-
`view "${authored.viewName}" references relation "${
|
|
531
|
+
`view "${authored.viewName}" references relation "${name2}" which is not part of this namespace`,
|
|
494
532
|
authored.file
|
|
495
533
|
);
|
|
496
534
|
}
|
|
@@ -524,11 +562,11 @@ function compileViewQueries(input) {
|
|
|
524
562
|
);
|
|
525
563
|
}
|
|
526
564
|
if (!config.query) throw new OrmDefinitionError(`view "${authored.viewName}" produced no query`, authored.file);
|
|
527
|
-
const tableDependencies = pgCore.extractUsedTable(config.query).map((
|
|
528
|
-
const logical = physicalToLogical.get(
|
|
565
|
+
const tableDependencies = pgCore.extractUsedTable(config.query).map((physical2) => {
|
|
566
|
+
const logical = physicalToLogical.get(physical2);
|
|
529
567
|
if (!logical) {
|
|
530
568
|
throw new OrmDefinitionError(
|
|
531
|
-
`view "${authored.viewName}" query references unmanaged relation "${
|
|
569
|
+
`view "${authored.viewName}" query references unmanaged relation "${physical2}" \u2014 use relation(...) with a namespace table or view`,
|
|
532
570
|
authored.file
|
|
533
571
|
);
|
|
534
572
|
}
|
|
@@ -681,8 +719,8 @@ function overlayWinsUnion(input) {
|
|
|
681
719
|
// src/authoring/cow-mutations.ts
|
|
682
720
|
function overrideRowMutation(input) {
|
|
683
721
|
const { overlay, synced, pk } = input;
|
|
684
|
-
const
|
|
685
|
-
assertMutationName(
|
|
722
|
+
const name2 = input.name ?? `override_${overlay.tableName}`;
|
|
723
|
+
assertMutationName(name2, "overrideRowMutation");
|
|
686
724
|
assertOverlayPairPrimaryKey({ overlay, synced, pk, helper: "overrideRowMutation" });
|
|
687
725
|
const sharedProperties = sharedNonPkProperties({ overlay, synced, pk });
|
|
688
726
|
const setProperties = resolveSetProperties({
|
|
@@ -701,7 +739,7 @@ function overrideRowMutation(input) {
|
|
|
701
739
|
});
|
|
702
740
|
return {
|
|
703
741
|
kind: "override",
|
|
704
|
-
name,
|
|
742
|
+
name: name2,
|
|
705
743
|
synced: synced.tableName,
|
|
706
744
|
pk,
|
|
707
745
|
copy: [pk, ...sharedProperties],
|
|
@@ -710,23 +748,23 @@ function overrideRowMutation(input) {
|
|
|
710
748
|
}
|
|
711
749
|
function revertOverlayMutation(input) {
|
|
712
750
|
const { overlay, synced, pk } = input;
|
|
713
|
-
const
|
|
714
|
-
assertMutationName(
|
|
751
|
+
const name2 = input.name ?? `revert_${overlay.tableName}`;
|
|
752
|
+
assertMutationName(name2, "revertOverlayMutation");
|
|
715
753
|
assertOverlayPairPrimaryKey({ overlay, synced, pk, helper: "revertOverlayMutation" });
|
|
716
754
|
assertMatchingColumns({ overlay, synced, properties: [pk], helper: "revertOverlayMutation" });
|
|
717
755
|
return {
|
|
718
756
|
kind: "revert",
|
|
719
|
-
name,
|
|
757
|
+
name: name2,
|
|
720
758
|
synced: synced.tableName,
|
|
721
759
|
pk,
|
|
722
760
|
copy: [],
|
|
723
761
|
set: []
|
|
724
762
|
};
|
|
725
763
|
}
|
|
726
|
-
function assertMutationName(
|
|
727
|
-
if (!IDENTIFIER_PATTERN.test(
|
|
764
|
+
function assertMutationName(name2, helper) {
|
|
765
|
+
if (!IDENTIFIER_PATTERN.test(name2) || name2.length > MAX_IDENTIFIER_LENGTH) {
|
|
728
766
|
throw new OrmDefinitionError(
|
|
729
|
-
`${helper}: mutation name "${
|
|
767
|
+
`${helper}: mutation name "${name2}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`
|
|
730
768
|
);
|
|
731
769
|
}
|
|
732
770
|
}
|
|
@@ -852,9 +890,468 @@ function scalarColumnType(table, property, helper) {
|
|
|
852
890
|
}
|
|
853
891
|
return type;
|
|
854
892
|
}
|
|
893
|
+
var quoteIdentifier = (name2) => `"${name2.replaceAll('"', '""')}"`;
|
|
894
|
+
var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
895
|
+
|
|
896
|
+
// src/checks/compile.ts
|
|
897
|
+
function tokenize(source) {
|
|
898
|
+
const tokens = [];
|
|
899
|
+
for (let position = 0; position < source.length; ) {
|
|
900
|
+
const char = source[position];
|
|
901
|
+
if (/\s/.test(char)) {
|
|
902
|
+
position++;
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
if (char === "(" || char === ")" || char === ",") {
|
|
906
|
+
tokens.push({ kind: char === "(" ? "lparen" : char === ")" ? "rparen" : "comma", value: char, position });
|
|
907
|
+
position++;
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
if (char === "'") {
|
|
911
|
+
const start = position++;
|
|
912
|
+
let value = "";
|
|
913
|
+
let closed = false;
|
|
914
|
+
while (position < source.length) {
|
|
915
|
+
if (source[position] !== "'") {
|
|
916
|
+
value += source[position++];
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
if (source[position + 1] === "'") {
|
|
920
|
+
value += "'";
|
|
921
|
+
position += 2;
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
position++;
|
|
925
|
+
closed = true;
|
|
926
|
+
break;
|
|
927
|
+
}
|
|
928
|
+
if (!closed) throw new Error(`unterminated string at position ${start}`);
|
|
929
|
+
if (/[\x00-\x1f\x7f]/.test(value)) throw new Error(`control character in string at position ${start}`);
|
|
930
|
+
tokens.push({ kind: "string", value, position: start });
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
const operator = /^(<=|>=|<>|!=|!~|=|<|>|~)/.exec(source.slice(position));
|
|
934
|
+
if (operator) {
|
|
935
|
+
tokens.push({ kind: "operator", value: operator[0] === "<>" ? "!=" : operator[0], position });
|
|
936
|
+
position += operator[0].length;
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
const number = /^-?\d+(?:\.\d+)?/.exec(source.slice(position));
|
|
940
|
+
if (number) {
|
|
941
|
+
tokens.push({ kind: "number", value: number[0], position });
|
|
942
|
+
position += number[0].length;
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
const identifier3 = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(source.slice(position));
|
|
946
|
+
if (identifier3) {
|
|
947
|
+
tokens.push({ kind: "identifier", value: identifier3[0], position });
|
|
948
|
+
position += identifier3[0].length;
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
throw new Error(`unexpected character '${char}' at position ${position}`);
|
|
952
|
+
}
|
|
953
|
+
tokens.push({ kind: "end", value: "", position: source.length });
|
|
954
|
+
if (tokens.length > 256) throw new Error("check expression exceeds 256 tokens");
|
|
955
|
+
return tokens;
|
|
956
|
+
}
|
|
957
|
+
var Parser = class {
|
|
958
|
+
constructor(tokens, columns) {
|
|
959
|
+
this.tokens = tokens;
|
|
960
|
+
this.columns = /* @__PURE__ */ new Map();
|
|
961
|
+
for (const column of columns) {
|
|
962
|
+
for (const alias of [column.name, column.columnName]) {
|
|
963
|
+
const previous = this.columns.get(alias);
|
|
964
|
+
if (previous && previous.columnName !== column.columnName) {
|
|
965
|
+
throw new Error(`ambiguous check column '${alias}'`);
|
|
966
|
+
}
|
|
967
|
+
this.columns.set(alias, column);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
index = 0;
|
|
972
|
+
depth = 0;
|
|
973
|
+
columns;
|
|
974
|
+
parse() {
|
|
975
|
+
const expression = this.parseOr();
|
|
976
|
+
this.expect("end", "end of expression");
|
|
977
|
+
return expression;
|
|
978
|
+
}
|
|
979
|
+
parseOr() {
|
|
980
|
+
let left = this.parseAnd();
|
|
981
|
+
while (this.matchKeyword("or")) left = `(${left} OR ${this.parseAnd()})`;
|
|
982
|
+
return left;
|
|
983
|
+
}
|
|
984
|
+
parseAnd() {
|
|
985
|
+
let left = this.parseNot();
|
|
986
|
+
while (this.matchKeyword("and")) left = `(${left} AND ${this.parseNot()})`;
|
|
987
|
+
return left;
|
|
988
|
+
}
|
|
989
|
+
parseNot() {
|
|
990
|
+
if (this.matchKeyword("not")) return `(NOT ${this.parseNot()})`;
|
|
991
|
+
return this.parsePrimary();
|
|
992
|
+
}
|
|
993
|
+
parsePrimary() {
|
|
994
|
+
if (this.peek().kind === "lparen") {
|
|
995
|
+
if (++this.depth > 32) throw new Error("check expression nesting exceeds 32 levels");
|
|
996
|
+
this.advance();
|
|
997
|
+
const expression = this.parseOr();
|
|
998
|
+
this.expect("rparen", "')'");
|
|
999
|
+
this.depth--;
|
|
1000
|
+
return `(${expression})`;
|
|
1001
|
+
}
|
|
1002
|
+
return this.parseComparison();
|
|
1003
|
+
}
|
|
1004
|
+
parseComparison() {
|
|
1005
|
+
const left = this.parseValue();
|
|
1006
|
+
if (this.matchKeyword("is")) {
|
|
1007
|
+
const negate = this.matchKeyword("not");
|
|
1008
|
+
this.expectKeyword("null");
|
|
1009
|
+
return `${left.sql} IS ${negate ? "NOT " : ""}NULL`;
|
|
1010
|
+
}
|
|
1011
|
+
if (this.matchKeyword("between")) {
|
|
1012
|
+
const lower = this.parseValue();
|
|
1013
|
+
this.expectKeyword("and");
|
|
1014
|
+
const upper = this.parseValue();
|
|
1015
|
+
return `${left.sql} BETWEEN ${lower.sql} AND ${upper.sql}`;
|
|
1016
|
+
}
|
|
1017
|
+
if (this.matchKeyword("in")) {
|
|
1018
|
+
this.expect("lparen", "'(' after IN");
|
|
1019
|
+
const values = [this.parseValue()];
|
|
1020
|
+
while (this.peek().kind === "comma") {
|
|
1021
|
+
this.advance();
|
|
1022
|
+
values.push(this.parseValue());
|
|
1023
|
+
}
|
|
1024
|
+
this.expect("rparen", "')' after IN values");
|
|
1025
|
+
if (values.some((value) => !value.literal)) throw new Error("IN values must be literals");
|
|
1026
|
+
return `${left.sql} IN (${values.map((value) => value.sql).join(", ")})`;
|
|
1027
|
+
}
|
|
1028
|
+
const operator = this.expect("operator", "a comparison operator");
|
|
1029
|
+
const right = this.parseValue();
|
|
1030
|
+
if (operator.value === "~" || operator.value === "!~") {
|
|
1031
|
+
if (left.column && left.column.type !== "text") throw new Error("regex checks require a text column");
|
|
1032
|
+
if (!right.literal) throw new Error("regex pattern must be a literal");
|
|
1033
|
+
}
|
|
1034
|
+
return `${left.sql} ${operator.value} ${right.sql}`;
|
|
1035
|
+
}
|
|
1036
|
+
parseValue() {
|
|
1037
|
+
const token = this.advance();
|
|
1038
|
+
if (token.kind === "string") return { sql: quoteLiteral(token.value), literal: true };
|
|
1039
|
+
if (token.kind === "number") {
|
|
1040
|
+
const number = Number(token.value);
|
|
1041
|
+
if (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number)) {
|
|
1042
|
+
throw new Error(`unsafe numeric literal at position ${token.position}`);
|
|
1043
|
+
}
|
|
1044
|
+
return { sql: token.value, literal: true };
|
|
1045
|
+
}
|
|
1046
|
+
if (token.kind !== "identifier") throw new Error(`expected a column or literal at position ${token.position}`);
|
|
1047
|
+
const keyword = token.value.toLowerCase();
|
|
1048
|
+
if (keyword === "true" || keyword === "false" || keyword === "null") return { sql: keyword.toUpperCase(), literal: true };
|
|
1049
|
+
if (keyword === "btrim") {
|
|
1050
|
+
this.expect("lparen", "'(' after btrim");
|
|
1051
|
+
const column2 = this.parseColumn();
|
|
1052
|
+
this.expect("rparen", "')' after btrim column");
|
|
1053
|
+
if (column2.type !== "text") throw new Error("btrim requires a text column");
|
|
1054
|
+
return { sql: `btrim(${quoteIdentifier(column2.columnName)})`, column: column2 };
|
|
1055
|
+
}
|
|
1056
|
+
const column = this.columns.get(token.value);
|
|
1057
|
+
if (!column) throw new Error(`unknown check column '${token.value}'`);
|
|
1058
|
+
return { sql: quoteIdentifier(column.columnName), column };
|
|
1059
|
+
}
|
|
1060
|
+
parseColumn() {
|
|
1061
|
+
const token = this.expect("identifier", "a column name");
|
|
1062
|
+
const column = this.columns.get(token.value);
|
|
1063
|
+
if (!column) throw new Error(`unknown check column '${token.value}'`);
|
|
1064
|
+
return column;
|
|
1065
|
+
}
|
|
1066
|
+
peek() {
|
|
1067
|
+
return this.tokens[this.index];
|
|
1068
|
+
}
|
|
1069
|
+
advance() {
|
|
1070
|
+
const token = this.peek();
|
|
1071
|
+
if (token.kind !== "end") this.index++;
|
|
1072
|
+
return token;
|
|
1073
|
+
}
|
|
1074
|
+
matchKeyword(keyword) {
|
|
1075
|
+
if (this.peek().kind !== "identifier" || this.peek().value.toLowerCase() !== keyword) return false;
|
|
1076
|
+
this.advance();
|
|
1077
|
+
return true;
|
|
1078
|
+
}
|
|
1079
|
+
expectKeyword(keyword) {
|
|
1080
|
+
if (!this.matchKeyword(keyword)) throw new Error(`expected '${keyword}' at position ${this.peek().position}`);
|
|
1081
|
+
}
|
|
1082
|
+
expect(kind, description) {
|
|
1083
|
+
const token = this.peek();
|
|
1084
|
+
if (token.kind !== kind) throw new Error(`expected ${description} at position ${token.position}`);
|
|
1085
|
+
return this.advance();
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
function compileCheckExpression(source, columns) {
|
|
1089
|
+
return new Parser(tokenize(source), columns).parse();
|
|
1090
|
+
}
|
|
1091
|
+
var policyVariantSchema = zod.z.object({
|
|
1092
|
+
/** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
|
|
1093
|
+
when: zod.z.string().min(1).max(1e3),
|
|
1094
|
+
/** Exactly one of: allow-all bypass, or a row predicate. */
|
|
1095
|
+
allow: zod.z.literal(true).optional(),
|
|
1096
|
+
policy: zod.z.string().min(1).max(2e3).optional()
|
|
1097
|
+
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
1098
|
+
message: "A variant declares exactly one of allow: true or policy"
|
|
1099
|
+
});
|
|
1100
|
+
var policyExpressionSchema = zod.z.union([
|
|
1101
|
+
zod.z.string().min(1).max(2e3),
|
|
1102
|
+
zod.z.object({
|
|
1103
|
+
default: zod.z.string().min(1).max(2e3),
|
|
1104
|
+
variants: zod.z.array(policyVariantSchema).max(10).default([])
|
|
1105
|
+
})
|
|
1106
|
+
]);
|
|
1107
|
+
function parseDurationMs(input) {
|
|
1108
|
+
if (typeof input === "number") return Number.isInteger(input) && input > 0 ? input : void 0;
|
|
1109
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(input);
|
|
1110
|
+
if (!match) return void 0;
|
|
1111
|
+
const factor = match[2] === "ms" ? 1 : match[2] === "s" ? 1e3 : 6e4;
|
|
1112
|
+
const value = Math.round(Number(match[1]) * factor);
|
|
1113
|
+
return value > 0 ? value : void 0;
|
|
1114
|
+
}
|
|
1115
|
+
var identifier = zod.z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
|
|
1116
|
+
var durationMs = zod.z.number().int().positive().max(24 * 60 * 60 * 1e3);
|
|
1117
|
+
var RESOLVED_VIEW_SYSTEM_COLUMNS = {
|
|
1118
|
+
writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
|
|
1119
|
+
writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
|
|
1120
|
+
};
|
|
1121
|
+
var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
|
|
1122
|
+
tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
|
|
1123
|
+
fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
|
|
1124
|
+
writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
|
|
1125
|
+
writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
|
|
1126
|
+
writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
|
|
1127
|
+
};
|
|
1128
|
+
var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
|
|
1129
|
+
var RESERVED_RESOLVED_COLUMN_NAMES = /* @__PURE__ */ new Set([
|
|
1130
|
+
...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
|
|
1131
|
+
...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
|
|
1132
|
+
]);
|
|
1133
|
+
var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
|
|
1134
|
+
var canonicalFunctionRefSchema = zod.z.object({
|
|
1135
|
+
/** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
|
|
1136
|
+
kind: zod.z.enum(["managed", "native"]),
|
|
1137
|
+
id: zod.z.string().min(1).max(200)
|
|
1138
|
+
}).strict();
|
|
1139
|
+
var canonicalFetchShapeSchema = zod.z.union([
|
|
1140
|
+
zod.z.object({ shape: zod.z.literal("single") }).strict(),
|
|
1141
|
+
zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().min(2).max(500) }).strict()
|
|
1142
|
+
]);
|
|
1143
|
+
var canonicalResolvedSourceSchema = zod.z.object({
|
|
1144
|
+
name: identifier,
|
|
1145
|
+
kind: zod.z.enum(["manual", "etl", "prefetch"]),
|
|
1146
|
+
/** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
|
|
1147
|
+
table: identifier,
|
|
1148
|
+
/** Resolved columns (logical names) this source supplies — the primary key is implicit. */
|
|
1149
|
+
columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
|
|
1150
|
+
/** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
|
|
1151
|
+
updatedAt: zod.z.string().min(1).max(63).optional(),
|
|
1152
|
+
/** prefetch: resolved column holding the source system's key (may be the primary key). */
|
|
1153
|
+
key: zod.z.string().min(1).max(63).optional(),
|
|
1154
|
+
fetch: canonicalFetchShapeSchema.optional(),
|
|
1155
|
+
function: canonicalFunctionRefSchema.optional(),
|
|
1156
|
+
prefetchDefaults: zod.z.object({
|
|
1157
|
+
maxStalenessMs: durationMs,
|
|
1158
|
+
timeoutMs: durationMs,
|
|
1159
|
+
onError: zod.z.enum(["fallback", "fail"])
|
|
1160
|
+
}).strict().optional(),
|
|
1161
|
+
writeback: zod.z.object({
|
|
1162
|
+
columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
|
|
1163
|
+
defaultMode: zod.z.enum(["sync", "async"]),
|
|
1164
|
+
timeoutMs: durationMs,
|
|
1165
|
+
/** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
|
|
1166
|
+
clearManualOnConfirm: zod.z.boolean()
|
|
1167
|
+
}).strict().optional()
|
|
1168
|
+
}).strict();
|
|
1169
|
+
var canonicalResolveRuleSchema = zod.z.object({
|
|
1170
|
+
column: zod.z.string().min(1).max(63),
|
|
1171
|
+
comparator: zod.z.enum(COMPARATORS),
|
|
1172
|
+
/** Source names, in tie-break order. */
|
|
1173
|
+
candidates: zod.z.array(identifier).min(1).max(16),
|
|
1174
|
+
/** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
|
|
1175
|
+
orderBy: zod.z.string().min(1).max(2e3).optional()
|
|
1176
|
+
}).strict();
|
|
1177
|
+
var canonicalResolvedSchema = zod.z.object({
|
|
1178
|
+
sources: zod.z.array(canonicalResolvedSourceSchema).min(1).max(16),
|
|
1179
|
+
/** One rule per resolved (non-pk) column; documents the generated view for tooling. */
|
|
1180
|
+
resolve: zod.z.array(canonicalResolveRuleSchema).max(500),
|
|
1181
|
+
/** Sources whose tombstone hides the resolved row (v1: the manual source only). */
|
|
1182
|
+
deletedBy: zod.z.array(identifier).max(1).default([]),
|
|
1183
|
+
/**
|
|
1184
|
+
* Materialized: the view reads an indexed internal table (`table`) that the
|
|
1185
|
+
* data plane keeps in step with `compute`, the internal view holding the
|
|
1186
|
+
* resolution query. Omitted when the view computes on read.
|
|
1187
|
+
*/
|
|
1188
|
+
materialized: zod.z.object({ table: identifier, compute: identifier }).strict().optional(),
|
|
1189
|
+
/** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
|
|
1190
|
+
policies: zod.z.object({
|
|
1191
|
+
write: policyExpressionSchema.optional(),
|
|
1192
|
+
insert: policyExpressionSchema.optional(),
|
|
1193
|
+
update: policyExpressionSchema.optional()
|
|
1194
|
+
}).strict().default({})
|
|
1195
|
+
}).strict();
|
|
1196
|
+
function validateResolvedView({ view, tables, views = [] }) {
|
|
1197
|
+
const resolved = view.resolved;
|
|
1198
|
+
if (!resolved) return [];
|
|
1199
|
+
const errors = [];
|
|
1200
|
+
const fail = (message) => errors.push(`resolved table '${view.view}': ${message}`);
|
|
1201
|
+
const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
|
|
1202
|
+
if (!pkColumn) {
|
|
1203
|
+
fail("requires exactly one primary-key column");
|
|
1204
|
+
return errors;
|
|
1205
|
+
}
|
|
1206
|
+
const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
|
|
1207
|
+
const scope = {
|
|
1208
|
+
view,
|
|
1209
|
+
resolved,
|
|
1210
|
+
pkColumn,
|
|
1211
|
+
dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
|
|
1212
|
+
sourceByName: /* @__PURE__ */ new Map(),
|
|
1213
|
+
fail
|
|
1214
|
+
};
|
|
1215
|
+
if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail("requires exactly one manual source");
|
|
1216
|
+
const tableByName = new Map(tables.map((table) => [table.table, table]));
|
|
1217
|
+
for (const source of resolved.sources) {
|
|
1218
|
+
if (scope.sourceByName.has(source.name)) fail(`duplicate source '${source.name}'`);
|
|
1219
|
+
scope.sourceByName.set(source.name, source);
|
|
1220
|
+
const table = tableByName.get(source.table);
|
|
1221
|
+
if (!table) fail(`source '${source.name}' references unknown table '${source.table}'`);
|
|
1222
|
+
else validateSource({ scope, source, table });
|
|
1223
|
+
}
|
|
1224
|
+
validateRules(scope);
|
|
1225
|
+
validateViewCells(scope);
|
|
1226
|
+
if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
|
|
1227
|
+
for (const name2 of resolved.deletedBy) {
|
|
1228
|
+
if (scope.sourceByName.get(name2)?.kind !== "manual") fail(`deletedBy '${name2}' must be the manual source`);
|
|
1229
|
+
}
|
|
1230
|
+
return errors;
|
|
1231
|
+
}
|
|
1232
|
+
function validateSource({ scope, source, table }) {
|
|
1233
|
+
const { fail } = scope;
|
|
1234
|
+
if (source.kind === "etl" && table.source === void 0) fail(`etl source '${source.name}' must reference a synced table`);
|
|
1235
|
+
if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
|
|
1236
|
+
fail(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
|
|
1237
|
+
}
|
|
1238
|
+
const hasRev = table.columns.some((column) => column.cell?.part === "rev");
|
|
1239
|
+
if (source.kind === "manual" && !hasRev) fail(`manual source '${source.name}' must carry '__rev' cell columns`);
|
|
1240
|
+
if (source.kind === "prefetch" && hasRev) fail(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
|
|
1241
|
+
const byName = new Map(table.columns.map((c) => [c.name, c]));
|
|
1242
|
+
validateSourceColumns({ scope, source, byName });
|
|
1243
|
+
if (source.kind === "etl") {
|
|
1244
|
+
const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
|
|
1245
|
+
if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
|
|
1246
|
+
fail(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
validatePrefetchFields({ scope, source });
|
|
1250
|
+
}
|
|
1251
|
+
function validatePrefetchFields({ scope, source }) {
|
|
1252
|
+
const { fail, pkColumn } = scope;
|
|
1253
|
+
if (source.kind !== "prefetch") {
|
|
1254
|
+
if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
|
|
1255
|
+
fail(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
|
|
1256
|
+
}
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
|
|
1260
|
+
fail(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
|
|
1261
|
+
} else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
|
|
1262
|
+
fail(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
|
|
1263
|
+
}
|
|
1264
|
+
for (const column of source.writeback?.columns ?? []) {
|
|
1265
|
+
if (!source.columns.includes(column)) fail(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
function validateSourceColumns({ scope, source, byName }) {
|
|
1269
|
+
const { fail, pkColumn, dataColumns } = scope;
|
|
1270
|
+
for (const column of [pkColumn.name, ...source.columns]) {
|
|
1271
|
+
const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
|
|
1272
|
+
const theirs = byName.get(column);
|
|
1273
|
+
if (!own) fail(`source '${source.name}' supplies unknown column '${column}'`);
|
|
1274
|
+
else if (!theirs) fail(`source '${source.name}' table '${source.table}' has no column '${column}'`);
|
|
1275
|
+
else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail(`column '${column}' has a different type on source '${source.name}'`);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail }) {
|
|
1279
|
+
const ruled = /* @__PURE__ */ new Set();
|
|
1280
|
+
for (const rule of resolved.resolve) {
|
|
1281
|
+
if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
|
|
1282
|
+
fail(`resolve rule targets unknown column '${rule.column}'`);
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
if (ruled.has(rule.column)) fail(`column '${rule.column}' has more than one resolve rule`);
|
|
1286
|
+
ruled.add(rule.column);
|
|
1287
|
+
if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
|
|
1288
|
+
fail(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
|
|
1289
|
+
}
|
|
1290
|
+
if (new Set(rule.candidates).size !== rule.candidates.length) fail(`column '${rule.column}' repeats a candidate`);
|
|
1291
|
+
for (const candidate of rule.candidates) {
|
|
1292
|
+
const source = sourceByName.get(candidate);
|
|
1293
|
+
if (!source) fail(`column '${rule.column}' names unknown candidate source '${candidate}'`);
|
|
1294
|
+
else if (!source.columns.includes(rule.column)) fail(`column '${rule.column}' candidate '${candidate}' does not supply it`);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
for (const column of dataColumns.keys()) {
|
|
1298
|
+
if (column !== pkColumn.name && !ruled.has(column)) fail(`column '${column}' has no resolve rule`);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
var VIEW_CELL_TYPES = {
|
|
1302
|
+
source: "text",
|
|
1303
|
+
at: "timestamptz",
|
|
1304
|
+
by: "text",
|
|
1305
|
+
conflicts: "jsonb"
|
|
1306
|
+
};
|
|
1307
|
+
function validateViewCells({ view, dataColumns, pkColumn, fail }) {
|
|
1308
|
+
const expected = /* @__PURE__ */ new Map();
|
|
1309
|
+
for (const column of dataColumns.values()) {
|
|
1310
|
+
if (column.name === pkColumn.name) continue;
|
|
1311
|
+
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
1312
|
+
expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
for (const column of view.columns) {
|
|
1316
|
+
if (!column.cell) continue;
|
|
1317
|
+
const want = expected.get(column.name);
|
|
1318
|
+
if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
|
|
1319
|
+
fail(`unexpected cell column '${column.name}'`);
|
|
1320
|
+
continue;
|
|
1321
|
+
}
|
|
1322
|
+
if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
|
|
1323
|
+
expected.delete(column.name);
|
|
1324
|
+
}
|
|
1325
|
+
for (const name2 of expected.keys()) fail(`missing cell column '${name2}'`);
|
|
1326
|
+
}
|
|
1327
|
+
var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
|
|
1328
|
+
function validateMaterialized({ scope, tableByName, views }) {
|
|
1329
|
+
const { view, resolved, fail } = scope;
|
|
1330
|
+
const { table: tableName, compute: computeName } = resolved.materialized;
|
|
1331
|
+
const table = tableByName.get(tableName);
|
|
1332
|
+
if (!table || table.internal !== true || table.materializes !== view.view) {
|
|
1333
|
+
fail(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
|
|
1334
|
+
} else {
|
|
1335
|
+
if (columnSignature(table.columns) !== columnSignature(view.columns)) fail(`materialized table '${tableName}' must have exactly the view's columns`);
|
|
1336
|
+
if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail(`materialized table '${tableName}' must share the view's primary key`);
|
|
1337
|
+
}
|
|
1338
|
+
const compute = views.find((candidate) => candidate.view === computeName);
|
|
1339
|
+
if (!compute || compute.internal !== true) fail(`compute view '${computeName}' must be an internal view`);
|
|
1340
|
+
else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail(`compute view '${computeName}' must have exactly the view's columns`);
|
|
1341
|
+
for (const source of resolved.sources) {
|
|
1342
|
+
if (source.kind !== "etl") continue;
|
|
1343
|
+
const read = tableByName.get(source.table)?.policies?.read;
|
|
1344
|
+
const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
|
|
1345
|
+
if (!unconditional) {
|
|
1346
|
+
fail(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// src/canonical/v2.ts
|
|
855
1352
|
var CANONICAL_FORMAT = 2;
|
|
856
1353
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
857
|
-
var
|
|
1354
|
+
var identifier2 = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
858
1355
|
var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
|
|
859
1356
|
var COLUMN_TYPES = [
|
|
860
1357
|
"uuid",
|
|
@@ -870,14 +1367,14 @@ var COLUMN_TYPES = [
|
|
|
870
1367
|
];
|
|
871
1368
|
var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
|
|
872
1369
|
var canonicalEnumSchema = zod.z.object({
|
|
873
|
-
name:
|
|
1370
|
+
name: identifier2,
|
|
874
1371
|
values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
|
|
875
1372
|
});
|
|
876
1373
|
var canonicalColumnSchema = zod.z.object({
|
|
877
1374
|
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
878
1375
|
name: zod.z.string().min(1).max(63),
|
|
879
1376
|
columnName: columnIdentifier,
|
|
880
|
-
type: zod.z.union([columnTypeSchema, zod.z.object({ enum:
|
|
1377
|
+
type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier2 })]),
|
|
881
1378
|
nullable: zod.z.boolean().default(true),
|
|
882
1379
|
unique: zod.z.boolean().default(false),
|
|
883
1380
|
/** Literal default, or one of the allowlisted SQL functions. */
|
|
@@ -893,7 +1390,14 @@ var canonicalColumnSchema = zod.z.object({
|
|
|
893
1390
|
/** Migration hints — same semantics as v1. */
|
|
894
1391
|
renamedFrom: columnIdentifier.optional(),
|
|
895
1392
|
cast: zod.z.string().max(200).optional(),
|
|
896
|
-
deprecated: zod.z.boolean().default(false)
|
|
1393
|
+
deprecated: zod.z.boolean().default(false),
|
|
1394
|
+
/**
|
|
1395
|
+
* Marks a system cell-metadata column (see CELL_PARTS in constants): part
|
|
1396
|
+
* `part` of the cell of logical column `of`. Synthesized by authoring,
|
|
1397
|
+
* never client-writable, absent from GraphQL scalar fields. Omitted on
|
|
1398
|
+
* ordinary columns so their hashes are unchanged.
|
|
1399
|
+
*/
|
|
1400
|
+
cell: zod.z.object({ of: zod.z.string().min(1).max(63), part: zod.z.enum(CELL_PARTS) }).strict().optional()
|
|
897
1401
|
}).superRefine((column, ctx) => {
|
|
898
1402
|
if (column.onUpdateFn === void 0) return;
|
|
899
1403
|
if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
|
|
@@ -908,36 +1412,28 @@ var canonicalLinkSchema = zod.z.object({
|
|
|
908
1412
|
name: zod.z.string().min(1).max(63),
|
|
909
1413
|
kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
|
|
910
1414
|
/** Target table name (namespace-local). Absent for toPlatformUser. */
|
|
911
|
-
target:
|
|
1415
|
+
target: identifier2.optional(),
|
|
1416
|
+
/** Owning namespace of an imported target; omitted for local links. */
|
|
1417
|
+
namespace: identifier2.max(41).optional(),
|
|
912
1418
|
/** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
|
|
913
1419
|
on: columnIdentifier,
|
|
914
1420
|
/** Referenced column on the target (default: target's PK / platform user `sub`). */
|
|
915
1421
|
references: columnIdentifier.optional(),
|
|
916
1422
|
/** enforce: true emits a real FK constraint (same-namespace toOne only). */
|
|
917
1423
|
enforce: zod.z.boolean().default(false),
|
|
1424
|
+
onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
1425
|
+
onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
918
1426
|
description: zod.z.string().max(500).optional()
|
|
919
1427
|
});
|
|
920
1428
|
var canonicalIndexSchema = zod.z.object({
|
|
921
1429
|
columns: zod.z.array(columnIdentifier).min(1),
|
|
922
1430
|
unique: zod.z.boolean().default(false),
|
|
923
|
-
name:
|
|
1431
|
+
name: identifier2.optional()
|
|
924
1432
|
});
|
|
925
|
-
var
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
allow: zod.z.literal(true).optional(),
|
|
930
|
-
policy: zod.z.string().min(1).max(2e3).optional()
|
|
931
|
-
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
932
|
-
message: "A variant declares exactly one of allow: true or policy"
|
|
933
|
-
});
|
|
934
|
-
var policyExpressionSchema = zod.z.union([
|
|
935
|
-
zod.z.string().min(1).max(2e3),
|
|
936
|
-
zod.z.object({
|
|
937
|
-
default: zod.z.string().min(1).max(2e3),
|
|
938
|
-
variants: zod.z.array(policyVariantSchema).max(10).default([])
|
|
939
|
-
})
|
|
940
|
-
]);
|
|
1433
|
+
var canonicalCheckSchema = zod.z.object({
|
|
1434
|
+
name: identifier2,
|
|
1435
|
+
expression: zod.z.string().min(1).max(2e3)
|
|
1436
|
+
}).strict();
|
|
941
1437
|
var canonicalPoliciesSchema = zod.z.object({
|
|
942
1438
|
read: policyExpressionSchema.optional(),
|
|
943
1439
|
write: policyExpressionSchema.optional(),
|
|
@@ -967,8 +1463,165 @@ function isSingleViewQuerySql(value) {
|
|
|
967
1463
|
}
|
|
968
1464
|
return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
|
|
969
1465
|
}
|
|
1466
|
+
var VIEW_QUERY_FUNCTION_ALLOWLIST = [
|
|
1467
|
+
"abs",
|
|
1468
|
+
"age",
|
|
1469
|
+
"array_agg",
|
|
1470
|
+
"array_append",
|
|
1471
|
+
"array_cat",
|
|
1472
|
+
"array_dims",
|
|
1473
|
+
"array_length",
|
|
1474
|
+
"array_lower",
|
|
1475
|
+
"array_ndims",
|
|
1476
|
+
"array_position",
|
|
1477
|
+
"array_positions",
|
|
1478
|
+
"array_prepend",
|
|
1479
|
+
"array_remove",
|
|
1480
|
+
"array_replace",
|
|
1481
|
+
"array_to_string",
|
|
1482
|
+
"array_trim",
|
|
1483
|
+
"array_upper",
|
|
1484
|
+
"ascii",
|
|
1485
|
+
"avg",
|
|
1486
|
+
"bit_and",
|
|
1487
|
+
"bit_or",
|
|
1488
|
+
"bit_xor",
|
|
1489
|
+
"bool_and",
|
|
1490
|
+
"bool_or",
|
|
1491
|
+
"btrim",
|
|
1492
|
+
"cardinality",
|
|
1493
|
+
"cbrt",
|
|
1494
|
+
"ceil",
|
|
1495
|
+
"ceiling",
|
|
1496
|
+
"char_length",
|
|
1497
|
+
"character_length",
|
|
1498
|
+
"chr",
|
|
1499
|
+
"concat",
|
|
1500
|
+
"concat_ws",
|
|
1501
|
+
"count",
|
|
1502
|
+
"cume_dist",
|
|
1503
|
+
"date_bin",
|
|
1504
|
+
"date_part",
|
|
1505
|
+
"date_trunc",
|
|
1506
|
+
"decode",
|
|
1507
|
+
"degrees",
|
|
1508
|
+
"dense_rank",
|
|
1509
|
+
"encode",
|
|
1510
|
+
"enum_first",
|
|
1511
|
+
"enum_last",
|
|
1512
|
+
"enum_range",
|
|
1513
|
+
"every",
|
|
1514
|
+
"exp",
|
|
1515
|
+
"first_value",
|
|
1516
|
+
"floor",
|
|
1517
|
+
"format",
|
|
1518
|
+
"grouping",
|
|
1519
|
+
"initcap",
|
|
1520
|
+
"json_agg",
|
|
1521
|
+
"json_array_length",
|
|
1522
|
+
"json_build_array",
|
|
1523
|
+
"json_build_object",
|
|
1524
|
+
"json_extract_path",
|
|
1525
|
+
"json_extract_path_text",
|
|
1526
|
+
"json_object_agg",
|
|
1527
|
+
"json_object_keys",
|
|
1528
|
+
"json_strip_nulls",
|
|
1529
|
+
"json_typeof",
|
|
1530
|
+
"jsonb_agg",
|
|
1531
|
+
"jsonb_array_length",
|
|
1532
|
+
"jsonb_build_array",
|
|
1533
|
+
"jsonb_build_object",
|
|
1534
|
+
"jsonb_extract_path",
|
|
1535
|
+
"jsonb_extract_path_text",
|
|
1536
|
+
"jsonb_insert",
|
|
1537
|
+
"jsonb_object_agg",
|
|
1538
|
+
"jsonb_object_keys",
|
|
1539
|
+
"jsonb_path_exists",
|
|
1540
|
+
"jsonb_path_match",
|
|
1541
|
+
"jsonb_path_query",
|
|
1542
|
+
"jsonb_path_query_array",
|
|
1543
|
+
"jsonb_path_query_first",
|
|
1544
|
+
"jsonb_pretty",
|
|
1545
|
+
"jsonb_set",
|
|
1546
|
+
"jsonb_set_lax",
|
|
1547
|
+
"jsonb_strip_nulls",
|
|
1548
|
+
"jsonb_typeof",
|
|
1549
|
+
"justify_days",
|
|
1550
|
+
"justify_hours",
|
|
1551
|
+
"justify_interval",
|
|
1552
|
+
"lag",
|
|
1553
|
+
"last_value",
|
|
1554
|
+
"lead",
|
|
1555
|
+
"left",
|
|
1556
|
+
"length",
|
|
1557
|
+
"ln",
|
|
1558
|
+
"log",
|
|
1559
|
+
"lower",
|
|
1560
|
+
"lpad",
|
|
1561
|
+
"ltrim",
|
|
1562
|
+
"make_date",
|
|
1563
|
+
"make_interval",
|
|
1564
|
+
"make_time",
|
|
1565
|
+
"make_timestamp",
|
|
1566
|
+
"make_timestamptz",
|
|
1567
|
+
"max",
|
|
1568
|
+
"md5",
|
|
1569
|
+
"min",
|
|
1570
|
+
"mod",
|
|
1571
|
+
"nth_value",
|
|
1572
|
+
"ntile",
|
|
1573
|
+
"octet_length",
|
|
1574
|
+
"percent_rank",
|
|
1575
|
+
"pi",
|
|
1576
|
+
"power",
|
|
1577
|
+
"quote_ident",
|
|
1578
|
+
"quote_literal",
|
|
1579
|
+
"quote_nullable",
|
|
1580
|
+
"radians",
|
|
1581
|
+
"rank",
|
|
1582
|
+
"regexp_count",
|
|
1583
|
+
"regexp_instr",
|
|
1584
|
+
"regexp_like",
|
|
1585
|
+
"regexp_match",
|
|
1586
|
+
"regexp_matches",
|
|
1587
|
+
"regexp_replace",
|
|
1588
|
+
"regexp_split_to_array",
|
|
1589
|
+
"regexp_split_to_table",
|
|
1590
|
+
"repeat",
|
|
1591
|
+
"replace",
|
|
1592
|
+
"reverse",
|
|
1593
|
+
"right",
|
|
1594
|
+
"round",
|
|
1595
|
+
"row_number",
|
|
1596
|
+
"rpad",
|
|
1597
|
+
"rtrim",
|
|
1598
|
+
"sign",
|
|
1599
|
+
"split_part",
|
|
1600
|
+
"sqrt",
|
|
1601
|
+
"starts_with",
|
|
1602
|
+
"string_agg",
|
|
1603
|
+
"string_to_array",
|
|
1604
|
+
"strpos",
|
|
1605
|
+
"substr",
|
|
1606
|
+
"substring",
|
|
1607
|
+
"sum",
|
|
1608
|
+
"timezone",
|
|
1609
|
+
"to_ascii",
|
|
1610
|
+
"to_char",
|
|
1611
|
+
"to_date",
|
|
1612
|
+
"to_hex",
|
|
1613
|
+
"to_json",
|
|
1614
|
+
"to_jsonb",
|
|
1615
|
+
"to_number",
|
|
1616
|
+
"to_timestamp",
|
|
1617
|
+
"translate",
|
|
1618
|
+
"trunc",
|
|
1619
|
+
"unnest",
|
|
1620
|
+
"upper",
|
|
1621
|
+
"width_bucket"
|
|
1622
|
+
];
|
|
970
1623
|
var canonicalActionSchema = zod.z.object({
|
|
971
|
-
name:
|
|
1624
|
+
name: identifier2,
|
|
972
1625
|
requires: zod.z.string().max(1e3).optional(),
|
|
973
1626
|
params: zod.z.record(zod.z.string(), zod.z.object({
|
|
974
1627
|
type: columnTypeSchema,
|
|
@@ -980,9 +1633,9 @@ var canonicalActionSchema = zod.z.object({
|
|
|
980
1633
|
});
|
|
981
1634
|
var canonicalCowMutationSchema = zod.z.object({
|
|
982
1635
|
kind: zod.z.enum(["override", "revert"]),
|
|
983
|
-
name:
|
|
1636
|
+
name: identifier2,
|
|
984
1637
|
/** Synced table (same namespace) the overlay copies from / falls back to. */
|
|
985
|
-
synced:
|
|
1638
|
+
synced: identifier2,
|
|
986
1639
|
/** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
|
|
987
1640
|
pk: zod.z.string().min(1).max(63),
|
|
988
1641
|
/** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
|
|
@@ -1010,23 +1663,28 @@ var canonicalCowMutationSchema = zod.z.object({
|
|
|
1010
1663
|
}
|
|
1011
1664
|
});
|
|
1012
1665
|
var canonicalForeignKeySchema = zod.z.object({
|
|
1013
|
-
name:
|
|
1666
|
+
name: identifier2,
|
|
1014
1667
|
columns: zod.z.array(columnIdentifier).min(1).max(8),
|
|
1015
|
-
target:
|
|
1668
|
+
target: identifier2,
|
|
1016
1669
|
references: zod.z.array(columnIdentifier).min(1).max(8),
|
|
1017
|
-
deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional()
|
|
1670
|
+
deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional(),
|
|
1671
|
+
onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
1672
|
+
onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
|
|
1018
1673
|
}).strict();
|
|
1019
1674
|
var canonicalTableSchema = zod.z.object({
|
|
1020
|
-
table:
|
|
1675
|
+
table: identifier2,
|
|
1676
|
+
displayName: zod.z.string().trim().min(1).max(120).optional(),
|
|
1021
1677
|
description: zod.z.string().max(2e3).optional(),
|
|
1022
1678
|
/** Table-level rename hint (new in v2). */
|
|
1023
|
-
renamedFrom:
|
|
1024
|
-
/** Synced/reflected read-only tables (`source:
|
|
1025
|
-
source: zod.z.string().regex(
|
|
1679
|
+
renamedFrom: identifier2.optional(),
|
|
1680
|
+
/** Synced/reflected read-only tables (`source: schema.table`) — v1 semantics. */
|
|
1681
|
+
source: zod.z.string().regex(QUALIFIED_TABLE_PATTERN).optional(),
|
|
1026
1682
|
columns: zod.z.array(canonicalColumnSchema).default([]),
|
|
1027
1683
|
primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
|
|
1028
1684
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
1029
1685
|
indexes: zod.z.array(canonicalIndexSchema).default([]),
|
|
1686
|
+
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
1687
|
+
checks: zod.z.array(canonicalCheckSchema).max(100).optional(),
|
|
1030
1688
|
policies: canonicalPoliciesSchema.default({}),
|
|
1031
1689
|
/**
|
|
1032
1690
|
* v1-conversion carry-over only. v2 has no action surface — the runtime
|
|
@@ -1037,22 +1695,55 @@ var canonicalTableSchema = zod.z.object({
|
|
|
1037
1695
|
/** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
|
|
1038
1696
|
cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
|
|
1039
1697
|
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
1040
|
-
foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional()
|
|
1698
|
+
foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional(),
|
|
1699
|
+
/**
|
|
1700
|
+
* Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
|
|
1701
|
+
* column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
|
|
1702
|
+
* on a resolved table's internal sources), listed explicitly in `columns`
|
|
1703
|
+
* with a `cell` marker so DDL and migrations treat them as ordinary columns.
|
|
1704
|
+
* Omitted when off so existing definitions hash unchanged.
|
|
1705
|
+
*/
|
|
1706
|
+
cellMetadata: zod.z.literal(true).optional(),
|
|
1707
|
+
/**
|
|
1708
|
+
* System-owned backing table of a resolved table (its manual or prefetch
|
|
1709
|
+
* source). Internal tables get DDL and IR metadata but no GraphQL surface;
|
|
1710
|
+
* only the data plane writes them. Omitted when false.
|
|
1711
|
+
*/
|
|
1712
|
+
internal: zod.z.literal(true).optional(),
|
|
1713
|
+
/**
|
|
1714
|
+
* The materialized copy of resolved table `materializes` (internal): the
|
|
1715
|
+
* same columns as that view, including its cell parts, maintained by the
|
|
1716
|
+
* data plane. Omitted otherwise.
|
|
1717
|
+
*/
|
|
1718
|
+
materializes: identifier2.optional()
|
|
1041
1719
|
});
|
|
1720
|
+
var MAX_VIEW_QUERY = 1e5;
|
|
1721
|
+
var MAX_RESOLVED_VIEW_QUERY = 5e6;
|
|
1042
1722
|
var canonicalViewSchema = zod.z.object({
|
|
1043
|
-
view:
|
|
1723
|
+
view: identifier2,
|
|
1044
1724
|
description: zod.z.string().max(2e3).optional(),
|
|
1045
1725
|
columns: zod.z.array(canonicalColumnSchema).min(1),
|
|
1046
1726
|
/** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
|
|
1047
1727
|
primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
|
|
1048
1728
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
1049
1729
|
policies: zod.z.object({ read: policyExpressionSchema }).strict(),
|
|
1050
|
-
query: zod.z.string().min(1).max(
|
|
1730
|
+
query: zod.z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
|
|
1051
1731
|
message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
|
|
1052
1732
|
}),
|
|
1053
1733
|
/** Namespace-local logical relation names, sorted for deterministic hashing. */
|
|
1054
|
-
dependencies: zod.z.array(
|
|
1734
|
+
dependencies: zod.z.array(identifier2).max(500),
|
|
1735
|
+
/**
|
|
1736
|
+
* Present when this view is a resolved table (defineResolvedTable): the
|
|
1737
|
+
* multi-source metadata behind its generated query. Omitted otherwise so
|
|
1738
|
+
* existing views hash unchanged.
|
|
1739
|
+
*/
|
|
1740
|
+
resolved: canonicalResolvedSchema.optional(),
|
|
1741
|
+
/** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
|
|
1742
|
+
internal: zod.z.literal(true).optional()
|
|
1055
1743
|
}).superRefine((view, ctx) => {
|
|
1744
|
+
if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
|
|
1745
|
+
ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
|
|
1746
|
+
}
|
|
1056
1747
|
for (const [index, column] of view.columns.entries()) {
|
|
1057
1748
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1058
1749
|
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
|
|
@@ -1067,20 +1758,91 @@ var canonicalViewSchema = zod.z.object({
|
|
|
1067
1758
|
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare migration hints` });
|
|
1068
1759
|
}
|
|
1069
1760
|
}
|
|
1761
|
+
if (view.resolved === void 0 && view.internal !== true) {
|
|
1762
|
+
for (const [index, column] of view.columns.entries()) {
|
|
1763
|
+
if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index], message: `only resolved tables carry cell columns ('${column.name}')` });
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1070
1766
|
for (const [index, link] of view.links.entries()) {
|
|
1071
1767
|
if (link.enforce) {
|
|
1072
1768
|
ctx.addIssue({ code: "custom", path: ["links", index, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
|
|
1073
1769
|
}
|
|
1770
|
+
if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
|
|
1771
|
+
ctx.addIssue({ code: "custom", path: ["links", index], message: `view link '${link.name}' cannot declare referential actions` });
|
|
1772
|
+
}
|
|
1074
1773
|
}
|
|
1075
1774
|
});
|
|
1076
|
-
var
|
|
1775
|
+
var CELL_PART_TYPES = {
|
|
1776
|
+
at: "timestamptz",
|
|
1777
|
+
by: "text",
|
|
1778
|
+
rev: "uuid",
|
|
1779
|
+
source: "text",
|
|
1780
|
+
conflicts: "jsonb"
|
|
1781
|
+
};
|
|
1782
|
+
function trackedCellColumns(table) {
|
|
1783
|
+
const pk = new Set(table.primaryKey ?? []);
|
|
1784
|
+
return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
|
|
1785
|
+
}
|
|
1786
|
+
var SUFFIXES = Object.values(CELL_PART_SUFFIX);
|
|
1787
|
+
function validateCellMetadata(table, index, ctx) {
|
|
1788
|
+
const fail = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
|
|
1789
|
+
const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
|
|
1790
|
+
if (table.materializes !== void 0) return;
|
|
1791
|
+
if (table.cellMetadata !== true) {
|
|
1792
|
+
for (const { position } of cells) fail(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index, "columns", position]);
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
if (table.source !== void 0) {
|
|
1796
|
+
fail(`synced table '${table.table}' cannot track cell metadata`);
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
const tracked = trackedCellColumns(table);
|
|
1800
|
+
const byName = new Map(tracked.map((column) => [column.name, column]));
|
|
1801
|
+
const withRev = cells.some(({ cell }) => cell.part === "rev");
|
|
1802
|
+
if (withRev && table.internal !== true) fail(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
|
|
1803
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1804
|
+
for (const { column, position, cell } of cells) {
|
|
1805
|
+
const path = ["tables", index, "columns", position];
|
|
1806
|
+
const owner = byName.get(cell.of);
|
|
1807
|
+
if (!owner) {
|
|
1808
|
+
fail(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
|
|
1809
|
+
continue;
|
|
1810
|
+
}
|
|
1811
|
+
if (cell.part !== "at" && cell.part !== "by" && cell.part !== "rev") fail(`a table cell column must be part at/by/rev, not '${cell.part}'`, path);
|
|
1812
|
+
if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
|
|
1813
|
+
fail(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
|
|
1814
|
+
}
|
|
1815
|
+
if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1816
|
+
fail(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
|
|
1817
|
+
}
|
|
1818
|
+
const key = `${cell.of}:${cell.part}`;
|
|
1819
|
+
if (seen.has(key)) fail(`duplicate cell column '${column.name}'`, path);
|
|
1820
|
+
seen.add(key);
|
|
1821
|
+
}
|
|
1822
|
+
for (const column of tracked) {
|
|
1823
|
+
if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
|
|
1824
|
+
fail(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
|
|
1825
|
+
}
|
|
1826
|
+
if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
|
|
1827
|
+
fail(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
|
|
1828
|
+
}
|
|
1829
|
+
for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
|
|
1830
|
+
if (!seen.has(`${column.name}:${part}`)) fail(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
var exportGateSchema = zod.z.union([zod.z.literal(true), zod.z.object({ capability: zod.z.string().min(1) }).strict(), zod.z.object({ role: zod.z.string().min(1) }).strict()]);
|
|
1835
|
+
var relationExportSchema = zod.z.object({ read: exportGateSchema, write: exportGateSchema.optional() }).strict();
|
|
1836
|
+
var localDefinitionSchema = zod.z.object({
|
|
1077
1837
|
definition_format: zod.z.literal(CANONICAL_FORMAT),
|
|
1078
|
-
namespace:
|
|
1838
|
+
namespace: identifier2.max(41),
|
|
1079
1839
|
description: zod.z.string().max(2e3).optional(),
|
|
1080
1840
|
enums: zod.z.array(canonicalEnumSchema).default([]),
|
|
1081
1841
|
tables: zod.z.array(canonicalTableSchema).min(0).max(500),
|
|
1082
1842
|
/** Optional to preserve byte-identical hashes for pre-view canonical v2 documents. */
|
|
1083
1843
|
views: zod.z.array(canonicalViewSchema).max(500).optional(),
|
|
1844
|
+
/** Target-owned cross-namespace access contract, using existing namespace grants. */
|
|
1845
|
+
exports: zod.z.record(identifier2, relationExportSchema).optional(),
|
|
1084
1846
|
/** Verbatim custom-roles grammar (@sequenceholdings/custom-roles-manifest). */
|
|
1085
1847
|
capabilities: zod.z.object({
|
|
1086
1848
|
roles: zod.z.unknown().optional(),
|
|
@@ -1093,6 +1855,7 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
1093
1855
|
ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
|
|
1094
1856
|
}
|
|
1095
1857
|
seen.add(table.table);
|
|
1858
|
+
validateCellMetadata(table, index, ctx);
|
|
1096
1859
|
if (table.source !== void 0) {
|
|
1097
1860
|
for (const [columnIndex, column] of table.columns.entries()) {
|
|
1098
1861
|
if (column.onUpdateFn !== void 0) {
|
|
@@ -1106,12 +1869,41 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
1106
1869
|
}
|
|
1107
1870
|
}
|
|
1108
1871
|
const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
1872
|
+
const relations = /* @__PURE__ */ new Set([...fkTables.keys(), ...(definition.views ?? []).map((view) => view.view)]);
|
|
1873
|
+
for (const name2 of Object.keys(definition.exports ?? {})) {
|
|
1874
|
+
if (!relations.has(name2)) ctx.addIssue({ code: "custom", path: ["exports", name2], message: `export '${name2}' is not a local object` });
|
|
1875
|
+
}
|
|
1109
1876
|
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
1877
|
+
for (const [linkIndex, link] of table.links.entries()) {
|
|
1878
|
+
if (link.enforce && link.namespace !== void 0 && link.namespace !== definition.namespace) {
|
|
1879
|
+
ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", linkIndex], message: "physical foreign keys require a local target; use an ORM link without enforce" });
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1110
1882
|
const names = /* @__PURE__ */ new Set([
|
|
1111
1883
|
...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
|
|
1112
1884
|
...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
|
|
1113
1885
|
...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
|
|
1114
1886
|
]);
|
|
1887
|
+
for (const [index, link] of table.links.entries()) {
|
|
1888
|
+
if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
|
|
1889
|
+
ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "referential actions require an enforced toOne link" });
|
|
1890
|
+
}
|
|
1891
|
+
if (link.onDelete === "set null" || link.onUpdate === "set null") {
|
|
1892
|
+
const source = table.columns.find((column) => column.columnName === link.on);
|
|
1893
|
+
if (source?.nullable === false || table.primaryKey?.includes(link.on)) ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "SET NULL requires a nullable source column" });
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
for (const [index, check] of (table.checks ?? []).entries()) {
|
|
1897
|
+
const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index], message });
|
|
1898
|
+
if (table.source) fail("synced tables cannot declare checks");
|
|
1899
|
+
if (names.has(check.name)) fail(`duplicate constraint '${check.name}'`);
|
|
1900
|
+
names.add(check.name);
|
|
1901
|
+
try {
|
|
1902
|
+
compileCheckExpression(check.expression, table.columns);
|
|
1903
|
+
} catch (error) {
|
|
1904
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1115
1907
|
for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
|
|
1116
1908
|
const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
|
|
1117
1909
|
if (names.has(fk.name)) fail(`duplicate foreign key '${fk.name}'`);
|
|
@@ -1123,16 +1915,24 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
1123
1915
|
}
|
|
1124
1916
|
if (fk.columns.length !== fk.references.length) fail("foreign key columns and references must have the same number of columns");
|
|
1125
1917
|
if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail("foreign key columns must not repeat");
|
|
1126
|
-
for (const [position,
|
|
1127
|
-
const sourceColumn = table.columns.find((column) => column.columnName ===
|
|
1918
|
+
for (const [position, name2] of fk.columns.entries()) {
|
|
1919
|
+
const sourceColumn = table.columns.find((column) => column.columnName === name2);
|
|
1128
1920
|
const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
|
|
1129
1921
|
if (!sourceColumn || !targetColumn) fail("foreign key references an unknown column");
|
|
1130
1922
|
else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail("foreign key column types must match");
|
|
1131
1923
|
}
|
|
1924
|
+
if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
|
|
1925
|
+
if (fk.columns.some((name2) => table.columns.find((column) => column.columnName === name2)?.nullable === false || table.primaryKey?.includes(name2))) fail("SET NULL requires nullable source columns");
|
|
1926
|
+
}
|
|
1132
1927
|
const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index2) => index2.unique).map((index2) => index2.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
|
|
1133
1928
|
if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail("foreign key target columns must form a unique key");
|
|
1134
1929
|
}
|
|
1135
1930
|
}
|
|
1931
|
+
for (const [viewIndex, view] of (definition.views ?? []).entries()) {
|
|
1932
|
+
for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
|
|
1933
|
+
ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1136
1936
|
const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
1137
1937
|
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
1138
1938
|
const cowMutations = table.cowMutations ?? [];
|
|
@@ -1227,40 +2027,62 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
1227
2027
|
const visiting = /* @__PURE__ */ new Set();
|
|
1228
2028
|
const visited = /* @__PURE__ */ new Set();
|
|
1229
2029
|
let cycleReported = false;
|
|
1230
|
-
const visit = (
|
|
1231
|
-
if (visited.has(
|
|
1232
|
-
if (visiting.has(
|
|
2030
|
+
const visit = (name2, path) => {
|
|
2031
|
+
if (visited.has(name2) || cycleReported) return;
|
|
2032
|
+
if (visiting.has(name2)) {
|
|
1233
2033
|
cycleReported = true;
|
|
1234
2034
|
ctx.addIssue({
|
|
1235
2035
|
code: "custom",
|
|
1236
2036
|
path: ["views"],
|
|
1237
|
-
message: `view dependencies form a cycle: ${[...path,
|
|
2037
|
+
message: `view dependencies form a cycle: ${[...path, name2].join(" -> ")}`
|
|
1238
2038
|
});
|
|
1239
2039
|
return;
|
|
1240
2040
|
}
|
|
1241
|
-
visiting.add(
|
|
1242
|
-
const view = viewByName.get(
|
|
2041
|
+
visiting.add(name2);
|
|
2042
|
+
const view = viewByName.get(name2);
|
|
1243
2043
|
for (const dependency of view?.dependencies ?? []) {
|
|
1244
|
-
if (viewByName.has(dependency)) visit(dependency, [...path,
|
|
2044
|
+
if (viewByName.has(dependency)) visit(dependency, [...path, name2]);
|
|
1245
2045
|
}
|
|
1246
|
-
visiting.delete(
|
|
1247
|
-
visited.add(
|
|
2046
|
+
visiting.delete(name2);
|
|
2047
|
+
visited.add(name2);
|
|
1248
2048
|
};
|
|
1249
2049
|
for (const view of views) visit(view.view, []);
|
|
1250
2050
|
});
|
|
2051
|
+
var canonicalDefinitionSchema = localDefinitionSchema.safeExtend({
|
|
2052
|
+
/** Flattened, pinned snapshots. Imported relations never participate in this namespace's DDL. */
|
|
2053
|
+
imports: zod.z.array(localDefinitionSchema).max(32).optional()
|
|
2054
|
+
});
|
|
2055
|
+
function canonicalRelations(definition) {
|
|
2056
|
+
const views = (definition.views ?? []).map((view) => ({
|
|
2057
|
+
table: view.view,
|
|
2058
|
+
...view.description !== void 0 ? { description: view.description } : {},
|
|
2059
|
+
columns: view.columns,
|
|
2060
|
+
...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
|
|
2061
|
+
links: view.links,
|
|
2062
|
+
indexes: [],
|
|
2063
|
+
// A resolved table's write policies (evaluated against the resolved row)
|
|
2064
|
+
// ride alongside the view's read policy.
|
|
2065
|
+
policies: { read: view.policies.read, ...view.resolved?.policies ?? {} },
|
|
2066
|
+
actions: [],
|
|
2067
|
+
...view.resolved !== void 0 ? { resolved: view.resolved } : {},
|
|
2068
|
+
...view.internal ? { internal: true } : {}
|
|
2069
|
+
}));
|
|
2070
|
+
return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
|
|
2071
|
+
}
|
|
1251
2072
|
function normalizeCanonical(definition) {
|
|
1252
2073
|
const sortByName = (items) => [...items].sort((a, b) => a.name.localeCompare(b.name));
|
|
1253
2074
|
const normalized = {
|
|
1254
2075
|
...definition,
|
|
1255
2076
|
enums: sortByName(definition.enums),
|
|
1256
2077
|
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
|
|
1257
|
-
const { cowMutations, foreignKeys, ...rest } = t2;
|
|
2078
|
+
const { cowMutations, foreignKeys, checks, ...rest } = t2;
|
|
1258
2079
|
return {
|
|
1259
2080
|
...rest,
|
|
1260
2081
|
links: sortByName(t2.links),
|
|
1261
2082
|
indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
|
|
1262
2083
|
actions: sortByName(t2.actions),
|
|
1263
2084
|
...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
|
|
2085
|
+
...checks?.length ? { checks: sortByName(checks) } : {},
|
|
1264
2086
|
// Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
|
|
1265
2087
|
...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
|
|
1266
2088
|
};
|
|
@@ -1273,8 +2095,461 @@ function normalizeCanonical(definition) {
|
|
|
1273
2095
|
dependencies: [...view.dependencies].sort()
|
|
1274
2096
|
}));
|
|
1275
2097
|
}
|
|
2098
|
+
if (definition.imports?.length) {
|
|
2099
|
+
normalized.imports = definition.imports.map(normalizeCanonical).sort((a, b) => a.namespace.localeCompare(b.namespace));
|
|
2100
|
+
} else {
|
|
2101
|
+
delete normalized.imports;
|
|
2102
|
+
}
|
|
1276
2103
|
return normalized;
|
|
1277
2104
|
}
|
|
2105
|
+
|
|
2106
|
+
// src/authoring/resolved-sources.ts
|
|
2107
|
+
function cloneColumn({ builder, property, notNull, primaryKey }) {
|
|
2108
|
+
const canonical = builder.toCanonicalColumn(property);
|
|
2109
|
+
let clone = new ColumnBuilder(canonical.type, canonical.columnName);
|
|
2110
|
+
if (primaryKey) clone = clone.primaryKey();
|
|
2111
|
+
if (notNull) clone = clone.notNull();
|
|
2112
|
+
if (canonical.description !== void 0) clone = clone.description(canonical.description);
|
|
2113
|
+
return clone;
|
|
2114
|
+
}
|
|
2115
|
+
function planSource(input) {
|
|
2116
|
+
const { sourceName, input: source, fail } = input;
|
|
2117
|
+
if (!IDENTIFIER_PATTERN.test(sourceName)) fail(`source name "${sourceName}" must match ${IDENTIFIER_PATTERN}`);
|
|
2118
|
+
switch (source.kind) {
|
|
2119
|
+
case "manual":
|
|
2120
|
+
return {
|
|
2121
|
+
name: sourceName,
|
|
2122
|
+
kind: "manual",
|
|
2123
|
+
columns: input.dataColumns,
|
|
2124
|
+
table: internalTable(input, input.dataColumns, {
|
|
2125
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.name]: tombstoned(),
|
|
2126
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackStatus.name]: t.text(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackStatus.columnName),
|
|
2127
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackError.name]: t.text(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackError.columnName),
|
|
2128
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackAt.name]: t.timestamptz(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackAt.columnName)
|
|
2129
|
+
}, { revisions: true })
|
|
2130
|
+
};
|
|
2131
|
+
case "etl":
|
|
2132
|
+
return planEtl(input, source);
|
|
2133
|
+
case "prefetch":
|
|
2134
|
+
return planPrefetch(input, source);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
var tombstoned = () => t.boolean(RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName).notNull().default(false);
|
|
2138
|
+
function internalTable(input, supplied, system, options = {}) {
|
|
2139
|
+
const { viewName, sourceName, columns, pk, pkBuilder } = input;
|
|
2140
|
+
const tableColumns = { [pk]: cloneColumn({ builder: pkBuilder, property: pk, notNull: true, primaryKey: true }) };
|
|
2141
|
+
for (const property of supplied) {
|
|
2142
|
+
tableColumns[property] = cloneColumn({ builder: columns[property], property, notNull: false, primaryKey: false });
|
|
2143
|
+
}
|
|
2144
|
+
return defineTable(`${viewName}__${sourceName}`, { ...tableColumns, ...system }, {
|
|
2145
|
+
description: `Internal ${input.input.kind} source "${sourceName}" of resolved table "${viewName}". System-maintained.`,
|
|
2146
|
+
cellMetadata: true,
|
|
2147
|
+
// The manual source identifies each edit (`c__rev`) so an async
|
|
2148
|
+
// confirmation clears only the exact edit it delivered.
|
|
2149
|
+
...options.revisions ? { cellRevisions: true } : {},
|
|
2150
|
+
internal: true
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
function assertColumns(input, list, what) {
|
|
2154
|
+
for (const column of list) if (!input.dataColumns.includes(column)) input.fail(`${what} names unknown column "${column}"`);
|
|
2155
|
+
}
|
|
2156
|
+
function planEtl(input, source) {
|
|
2157
|
+
const { sourceName, pk, dataColumns, fail } = input;
|
|
2158
|
+
const table = typeof source.table === "function" ? source.table() : source.table;
|
|
2159
|
+
if (!isSeqTable(table) || table.source === void 0) fail(`etl source "${sourceName}" must reference a defineSyncedTable`);
|
|
2160
|
+
const synced = table;
|
|
2161
|
+
if (!(pk in synced.columns)) fail(`etl source "${sourceName}" table "${synced.tableName}" has no "${pk}" column`);
|
|
2162
|
+
const supplied = source.columns ? [...source.columns] : dataColumns.filter((column) => column in synced.columns);
|
|
2163
|
+
assertColumns(input, supplied, `etl source "${sourceName}"`);
|
|
2164
|
+
if (source.updatedAt !== void 0 && !(source.updatedAt in synced.columns)) {
|
|
2165
|
+
fail(`etl source "${sourceName}" updatedAt "${source.updatedAt}" is not a column of "${synced.tableName}"`);
|
|
2166
|
+
}
|
|
2167
|
+
return { name: sourceName, kind: "etl", table: synced, columns: supplied, ...source.updatedAt ? { updatedAt: source.updatedAt } : {} };
|
|
2168
|
+
}
|
|
2169
|
+
function planPrefetch(input, source) {
|
|
2170
|
+
const { sourceName, pk, fail } = input;
|
|
2171
|
+
assertColumns(input, source.columns, `prefetch source "${sourceName}"`);
|
|
2172
|
+
if (source.key !== pk && !source.columns.includes(source.key)) {
|
|
2173
|
+
fail(`prefetch source "${sourceName}" key "${source.key}" must be the primary key or one of its columns`);
|
|
2174
|
+
}
|
|
2175
|
+
for (const column of source.writeback?.columns ?? []) {
|
|
2176
|
+
if (!source.columns.includes(column)) fail(`prefetch source "${sourceName}" writes back "${column}", which it does not supply`);
|
|
2177
|
+
}
|
|
2178
|
+
const writeback = source.writeback;
|
|
2179
|
+
return {
|
|
2180
|
+
name: sourceName,
|
|
2181
|
+
kind: "prefetch",
|
|
2182
|
+
columns: [...source.columns],
|
|
2183
|
+
key: source.key,
|
|
2184
|
+
function: source.function,
|
|
2185
|
+
fetch: source.fetch === "single" ? { shape: "single" } : { shape: "batch", maxKeys: source.fetch.batch.maxKeys },
|
|
2186
|
+
prefetchDefaults: {
|
|
2187
|
+
maxStalenessMs: parseDuration(source.prefetchDefaults?.maxStaleness ?? "30s"),
|
|
2188
|
+
timeoutMs: parseDuration(source.prefetchDefaults?.timeout ?? "1500ms"),
|
|
2189
|
+
onError: source.prefetchDefaults?.onError ?? "fallback"
|
|
2190
|
+
},
|
|
2191
|
+
...writeback ? {
|
|
2192
|
+
writeback: {
|
|
2193
|
+
columns: [...writeback.columns],
|
|
2194
|
+
defaultMode: writeback.defaultMode ?? "sync",
|
|
2195
|
+
timeoutMs: parseDuration(writeback.timeout ?? "5s"),
|
|
2196
|
+
clearManualOnConfirm: writeback.clearManualOnConfirm ?? true
|
|
2197
|
+
}
|
|
2198
|
+
} : {},
|
|
2199
|
+
table: internalTable(input, source.columns, {
|
|
2200
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.fetchedAt.name]: t.timestamptz(RESOLVED_SOURCE_SYSTEM_COLUMNS.fetchedAt.columnName).notNull().default("now()"),
|
|
2201
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.name]: tombstoned()
|
|
2202
|
+
})
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2205
|
+
function parseDuration(input) {
|
|
2206
|
+
const value = parseDurationMs(input);
|
|
2207
|
+
if (value === void 0) throw new OrmDefinitionError(`invalid duration "${input}" \u2014 use e.g. '1500ms', '30s', '5m'`);
|
|
2208
|
+
return value;
|
|
2209
|
+
}
|
|
2210
|
+
var PG_TYPE = {
|
|
2211
|
+
uuid: "uuid",
|
|
2212
|
+
text: "text",
|
|
2213
|
+
integer: "integer",
|
|
2214
|
+
bigint: "bigint",
|
|
2215
|
+
numeric: "numeric",
|
|
2216
|
+
double: "double precision",
|
|
2217
|
+
boolean: "boolean",
|
|
2218
|
+
timestamptz: "timestamptz",
|
|
2219
|
+
date: "date",
|
|
2220
|
+
jsonb: "jsonb"
|
|
2221
|
+
};
|
|
2222
|
+
var ident = (value) => `"${value.replace(/"/g, '""')}"`;
|
|
2223
|
+
var literal = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
2224
|
+
var physical = ({ source, property }) => source.table.columns[property].columnNameFor(property);
|
|
2225
|
+
function candidateSql({ source, column, pk, pgType }) {
|
|
2226
|
+
const a = ident(`s_${source.name}`);
|
|
2227
|
+
const name2 = physical({ source, property: column });
|
|
2228
|
+
const value = `CAST(${a}.${ident(name2)} AS ${pgType})`;
|
|
2229
|
+
if (source.kind === "etl") {
|
|
2230
|
+
return {
|
|
2231
|
+
source: source.name,
|
|
2232
|
+
value,
|
|
2233
|
+
has: `(${a}.${ident(physical({ source, property: pk }))} IS NOT NULL)`,
|
|
2234
|
+
at: source.updatedAt ? `CAST(${a}.${ident(physical({ source, property: source.updatedAt }))} AS timestamptz)` : "CAST(NULL AS timestamptz)",
|
|
2235
|
+
by: "CAST(NULL AS text)"
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
const at = `${a}.${ident(cellColumnName(name2, "at"))}`;
|
|
2239
|
+
const tombstoned2 = source.kind === "prefetch" ? ` AND NOT ${a}.${ident(RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName)}` : "";
|
|
2240
|
+
return { source: source.name, value, has: `(${at} IS NOT NULL${tombstoned2})`, at, by: `${a}.${ident(cellColumnName(name2, "by"))}` };
|
|
2241
|
+
}
|
|
2242
|
+
function outranks({ i, j, key, direction, inclusive }) {
|
|
2243
|
+
const ki = i[key];
|
|
2244
|
+
const kj = j[key];
|
|
2245
|
+
const op = inclusive ? `${direction}=` : direction;
|
|
2246
|
+
return inclusive ? `(${kj} IS NULL OR (${ki} IS NOT NULL AND ${ki} ${op} ${kj}))` : `(${ki} IS NOT NULL AND (${kj} IS NULL OR ${ki} ${op} ${kj}))`;
|
|
2247
|
+
}
|
|
2248
|
+
function rankedWinner({ candidates, key, direction }) {
|
|
2249
|
+
const branches = candidates.map((candidate, index) => {
|
|
2250
|
+
const beats = candidates.map((other, otherIndex) => otherIndex === index ? null : `(NOT ${other.has} OR ${outranks({ i: candidate, j: other, key, direction, inclusive: index < otherIndex })})`).filter((clause) => clause !== null);
|
|
2251
|
+
return `WHEN ${[candidate.has, ...beats].join(" AND ")} THEN ${index + 1}`;
|
|
2252
|
+
});
|
|
2253
|
+
return `CASE ${branches.join(" ")} END`;
|
|
2254
|
+
}
|
|
2255
|
+
function winnerIndex({ comparator, candidates }) {
|
|
2256
|
+
const firstWith = (test) => candidates.map((candidate, index) => `WHEN ${test(candidate)} THEN ${index + 1}`).join(" ");
|
|
2257
|
+
if (candidates.length === 1 || comparator === "priority") return `CASE ${firstWith((c) => c.has)} END`;
|
|
2258
|
+
switch (comparator) {
|
|
2259
|
+
case "latest":
|
|
2260
|
+
return rankedWinner({ candidates, key: "at", direction: ">" });
|
|
2261
|
+
case "highest":
|
|
2262
|
+
return rankedWinner({ candidates, key: "value", direction: ">" });
|
|
2263
|
+
case "lowest":
|
|
2264
|
+
return rankedWinner({ candidates, key: "value", direction: "<" });
|
|
2265
|
+
case "nonNullFirst":
|
|
2266
|
+
return `CASE ${firstWith((c) => `${c.has} AND ${c.value} IS NOT NULL`)} ${firstWith((c) => c.has)} END`;
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
function pick(winner, candidates, field) {
|
|
2270
|
+
return `CASE ${winner} ${candidates.map((candidate, index) => `WHEN ${index + 1} THEN ${field(candidate)}`).join(" ")} END`;
|
|
2271
|
+
}
|
|
2272
|
+
function loserSql({ loser, winner }) {
|
|
2273
|
+
return `CASE WHEN ${loser.has} AND ${loser.value} IS DISTINCT FROM ${winner.value} AND ${loser.at} > ${winner.at} THEN jsonb_build_array(jsonb_build_object('source', ${literal(loser.source)}, 'value', to_jsonb(${loser.value}), 'at', ${loser.at})) ELSE '[]'::jsonb END`;
|
|
2274
|
+
}
|
|
2275
|
+
function losersAgainst({ candidates, winner, isWinner }) {
|
|
2276
|
+
const losers = candidates.flatMap((loser, index) => {
|
|
2277
|
+
const excluded = isWinner(loser, index);
|
|
2278
|
+
if (excluded === true) return [];
|
|
2279
|
+
const clause = loserSql({ loser, winner });
|
|
2280
|
+
return [excluded === false ? clause : `CASE WHEN ${excluded} THEN '[]'::jsonb ELSE ${clause} END`];
|
|
2281
|
+
});
|
|
2282
|
+
return losers.length === 0 ? "CAST(NULL AS jsonb)" : `NULLIF(${losers.join(" || ")}, '[]'::jsonb)`;
|
|
2283
|
+
}
|
|
2284
|
+
function builtinCell({ comparator, candidates }) {
|
|
2285
|
+
const winner = winnerIndex({ comparator, candidates });
|
|
2286
|
+
const conflicts = comparator === "latest" || candidates.length < 2 ? "CAST(NULL AS jsonb)" : `CASE ${winner} ${candidates.map((known, index) => `WHEN ${index + 1} THEN ${losersAgainst({ candidates, winner: known, isWinner: (_c, other) => other === index })}`).join(" ")} END`;
|
|
2287
|
+
return {
|
|
2288
|
+
value: pick(winner, candidates, (c) => c.value),
|
|
2289
|
+
parts: {
|
|
2290
|
+
source: pick(winner, candidates, (c) => literal(c.source)),
|
|
2291
|
+
at: pick(winner, candidates, (c) => c.at),
|
|
2292
|
+
by: pick(winner, candidates, (c) => c.by),
|
|
2293
|
+
conflicts
|
|
2294
|
+
}
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
function customCell({ candidates, orderBy, index }) {
|
|
2298
|
+
const w = ident(`w_${index}`);
|
|
2299
|
+
const rows = candidates.map((c, ord) => `(${literal(c.source)}, ${c.has}, ${c.value}, ${c.at}, ${c.by}, ${ord})`);
|
|
2300
|
+
const lateral = ` LEFT JOIN LATERAL (SELECT max(winner.source) AS source, (array_agg(winner.value))[1] AS value, max(winner.updated_at) AS updated_at, max(winner.actor) AS actor FROM (WITH cand(source, has, value, updated_at, actor, ord) AS (VALUES ${rows.join(", ")}) SELECT c.source, c.value, c.updated_at, c.actor FROM cand c WHERE c.has ORDER BY ${orderBy}, c.ord LIMIT 1) AS winner) AS ${w} ON true`;
|
|
2301
|
+
const winner = { value: `${w}.value`, at: `${w}.updated_at` };
|
|
2302
|
+
return {
|
|
2303
|
+
value: winner.value,
|
|
2304
|
+
lateral,
|
|
2305
|
+
parts: {
|
|
2306
|
+
source: `${w}.source`,
|
|
2307
|
+
at: winner.at,
|
|
2308
|
+
by: `${w}.actor`,
|
|
2309
|
+
conflicts: candidates.length < 2 ? "CAST(NULL AS jsonb)" : losersAgainst({ candidates, winner, isWinner: (c) => `${w}.source = ${literal(c.source)}` })
|
|
2310
|
+
}
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2313
|
+
function buildResolvedQuery({ context, plan }) {
|
|
2314
|
+
const { qb, relation } = context;
|
|
2315
|
+
const pkColumn = plan.columns[plan.pk].columnNameFor(plan.pk);
|
|
2316
|
+
const alias = (source) => ident(`s_${source}`);
|
|
2317
|
+
const tombstoned2 = RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName;
|
|
2318
|
+
const sourceByName = new Map(plan.plans.map((source) => [source.name, source]));
|
|
2319
|
+
const schema = pgCore.getTableConfig(relation(plan.plans[0].table)).schema;
|
|
2320
|
+
const keyBranches = plan.plans.map((source) => {
|
|
2321
|
+
const where = source.kind === "prefetch" ? drizzleOrm.sql.raw(` WHERE NOT ${ident(tombstoned2)}`) : drizzleOrm.sql.raw("");
|
|
2322
|
+
const sourcePk = ident(physical({ source, property: plan.pk }));
|
|
2323
|
+
const selectedPk = sourcePk === ident(pkColumn) ? sourcePk : `${sourcePk} AS ${ident(pkColumn)}`;
|
|
2324
|
+
return drizzleOrm.sql`SELECT ${drizzleOrm.sql.raw(selectedPk)} FROM ${relation(source.table)}${where}`;
|
|
2325
|
+
});
|
|
2326
|
+
const joins = plan.plans.map(
|
|
2327
|
+
(source) => drizzleOrm.sql` LEFT JOIN ${relation(source.table)} AS ${drizzleOrm.sql.raw(alias(source.name))} ON ${drizzleOrm.sql.raw(`${alias(source.name)}.${ident(physical({ source, property: plan.pk }))} = "k".${ident(pkColumn)}`)}`
|
|
2328
|
+
);
|
|
2329
|
+
const cells = /* @__PURE__ */ new Map();
|
|
2330
|
+
plan.rules.forEach((rule, index) => {
|
|
2331
|
+
const canonical = plan.columns[rule.column].toCanonicalColumn(rule.column);
|
|
2332
|
+
const pgType = typeof canonical.type === "string" ? PG_TYPE[canonical.type] : `${schema === void 0 ? "" : `${ident(schema)}.`}${ident(canonical.type.enum)}`;
|
|
2333
|
+
const candidates = rule.candidates.map((name2) => candidateSql({ source: sourceByName.get(name2), column: rule.column, pk: plan.pk, pgType }));
|
|
2334
|
+
cells.set(rule.column, rule.comparator === "custom" ? customCell({ candidates, orderBy: rule.orderBy, index }) : builtinCell({ comparator: rule.comparator, candidates }));
|
|
2335
|
+
});
|
|
2336
|
+
const laterals = [...cells.values()].flatMap((cell) => cell.lateral ? [drizzleOrm.sql.raw(cell.lateral)] : []);
|
|
2337
|
+
const from = drizzleOrm.sql.join([drizzleOrm.sql`(${drizzleOrm.sql.join(keyBranches, drizzleOrm.sql.raw(" UNION "))}) AS "k"`, ...joins, ...laterals], drizzleOrm.sql.raw(""));
|
|
2338
|
+
const selected = {
|
|
2339
|
+
[plan.pk]: drizzleOrm.sql.raw(`"k".${ident(pkColumn)}`).as(pkColumn)
|
|
2340
|
+
};
|
|
2341
|
+
for (const [property, builder] of Object.entries(plan.columns)) {
|
|
2342
|
+
if (property === plan.pk) continue;
|
|
2343
|
+
const cell = cells.get(property);
|
|
2344
|
+
const columnName = builder.columnNameFor(property);
|
|
2345
|
+
selected[property] = drizzleOrm.sql.raw(cell.value).as(columnName);
|
|
2346
|
+
for (const [part, expression] of Object.entries(cell.parts)) {
|
|
2347
|
+
selected[cellColumnName(property, part)] = drizzleOrm.sql.raw(expression).as(cellColumnName(columnName, part));
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
const manual = plan.plans.find((source) => source.kind === "manual");
|
|
2351
|
+
const system = RESOLVED_VIEW_SYSTEM_COLUMNS;
|
|
2352
|
+
selected[system.writebackStatus.name] = drizzleOrm.sql.raw(`${alias(manual.name)}.${ident(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackStatus.columnName)}`).as(system.writebackStatus.columnName);
|
|
2353
|
+
selected[system.writebackError.name] = drizzleOrm.sql.raw(`${alias(manual.name)}.${ident(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackError.columnName)}`).as(system.writebackError.columnName);
|
|
2354
|
+
const hidden = plan.deletedBy.map((name2) => `NOT COALESCE(${alias(name2)}.${ident(tombstoned2)}, false)`);
|
|
2355
|
+
const query = qb.select(selected).from(from);
|
|
2356
|
+
return hidden.length > 0 ? query.where(drizzleOrm.sql.raw(hidden.join(" AND "))) : query;
|
|
2357
|
+
}
|
|
2358
|
+
function managedFn(id) {
|
|
2359
|
+
return { kind: "managed", id };
|
|
2360
|
+
}
|
|
2361
|
+
function manualSource() {
|
|
2362
|
+
return { kind: "manual" };
|
|
2363
|
+
}
|
|
2364
|
+
function etlSource(input) {
|
|
2365
|
+
return { kind: "etl", ...input };
|
|
2366
|
+
}
|
|
2367
|
+
function prefetchSource(input) {
|
|
2368
|
+
return { kind: "prefetch", ...input };
|
|
2369
|
+
}
|
|
2370
|
+
var latest = (candidates) => ({ kind: "latest", candidates });
|
|
2371
|
+
var highest = (candidates) => ({ kind: "highest", candidates });
|
|
2372
|
+
var lowest = (candidates) => ({ kind: "lowest", candidates });
|
|
2373
|
+
var priority = (candidates) => ({ kind: "priority", candidates });
|
|
2374
|
+
var nonNullFirst = (candidates) => ({ kind: "nonNullFirst", candidates });
|
|
2375
|
+
var custom = (candidates, orderBy) => ({ kind: "custom", candidates, orderBy });
|
|
2376
|
+
var SYSTEM_VIEW_COLUMNS = {
|
|
2377
|
+
[RESOLVED_VIEW_SYSTEM_COLUMNS.writebackStatus.name]: () => t.text(RESOLVED_VIEW_SYSTEM_COLUMNS.writebackStatus.columnName),
|
|
2378
|
+
[RESOLVED_VIEW_SYSTEM_COLUMNS.writebackError.name]: () => t.text(RESOLVED_VIEW_SYSTEM_COLUMNS.writebackError.columnName)
|
|
2379
|
+
};
|
|
2380
|
+
function defineResolvedTable(name2, columns, config) {
|
|
2381
|
+
const file = activeAuthoringContext()?.currentFile;
|
|
2382
|
+
const fail = (message) => {
|
|
2383
|
+
throw new OrmDefinitionError(`resolved table "${name2}": ${message}`, file);
|
|
2384
|
+
};
|
|
2385
|
+
if (!IDENTIFIER_PATTERN.test(name2)) fail(`name must match ${IDENTIFIER_PATTERN}`);
|
|
2386
|
+
for (const property of Object.keys(columns)) {
|
|
2387
|
+
if (RESERVED_RESOLVED_COLUMN_NAMES.has(property) || RESERVED_RESOLVED_COLUMN_NAMES.has(columns[property].columnNameFor(property))) {
|
|
2388
|
+
fail(`column "${property}" collides with a reserved system column`);
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
for (const property of Object.keys(columns)) {
|
|
2392
|
+
if (columns[property].columnNameFor(property).length > MAX_TRACKED_COLUMN_NAME) {
|
|
2393
|
+
fail(`column "${property}" is too long for a resolved table (max ${MAX_TRACKED_COLUMN_NAME} characters, room for cell parts)`);
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
const pkEntries = Object.entries(columns).filter(([, builder]) => builder.isPrimaryKey);
|
|
2397
|
+
if (pkEntries.length !== 1) fail("declare exactly one .primaryKey() column");
|
|
2398
|
+
const [pk, pkBuilder] = pkEntries[0];
|
|
2399
|
+
const dataColumns = Object.keys(columns).filter((property) => property !== pk);
|
|
2400
|
+
if (dataColumns.length === 0) fail("declare at least one non-key column");
|
|
2401
|
+
const sourceEntries = Object.entries(config.sources);
|
|
2402
|
+
if (sourceEntries.filter(([, source]) => source.kind === "manual").length !== 1) fail("declare exactly one manualSource()");
|
|
2403
|
+
const plans = sourceEntries.map(
|
|
2404
|
+
([sourceName, input]) => planSource({ viewName: name2, sourceName, input, columns, pk, pkBuilder, dataColumns, fail })
|
|
2405
|
+
);
|
|
2406
|
+
const rules = planRules({ config, plans, dataColumns, fail });
|
|
2407
|
+
const deletedBy = [...config.rows?.deletedBy ?? []];
|
|
2408
|
+
for (const source of deletedBy) {
|
|
2409
|
+
if (plans.find((plan) => plan.name === source)?.kind !== "manual") fail(`rows.deletedBy "${source}" must be the manual source`);
|
|
2410
|
+
}
|
|
2411
|
+
const partType = { source: t.text, at: t.timestamptz, by: t.text, conflicts: t.jsonb };
|
|
2412
|
+
const viewColumns = {};
|
|
2413
|
+
for (const [property, builder] of Object.entries(columns)) {
|
|
2414
|
+
viewColumns[property] = cloneColumn({ builder, property, notNull: property === pk, primaryKey: property === pk });
|
|
2415
|
+
if (property === pk) continue;
|
|
2416
|
+
const columnName = builder.columnNameFor(property);
|
|
2417
|
+
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
2418
|
+
viewColumns[cellColumnName(property, part)] = partType[part](cellColumnName(columnName, part)).cellPart(property, part);
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
for (const [property, make] of Object.entries(SYSTEM_VIEW_COLUMNS)) viewColumns[property] = make();
|
|
2422
|
+
const { read, ...writePolicies } = config.policies;
|
|
2423
|
+
const resolved = {
|
|
2424
|
+
internalTables: plans.filter((plan) => plan.kind !== "etl").map((plan) => plan.table),
|
|
2425
|
+
sources: plans,
|
|
2426
|
+
rules,
|
|
2427
|
+
deletedBy,
|
|
2428
|
+
policies: writePolicies
|
|
2429
|
+
};
|
|
2430
|
+
const queryPlan = { columns, pk, plans, rules, deletedBy };
|
|
2431
|
+
const typedColumns = viewColumns;
|
|
2432
|
+
const surface = {
|
|
2433
|
+
...config.description !== void 0 ? { description: config.description } : {},
|
|
2434
|
+
...config.links !== void 0 ? { links: config.links } : {},
|
|
2435
|
+
policies: { read }
|
|
2436
|
+
};
|
|
2437
|
+
if (!config.materialize) {
|
|
2438
|
+
return defineView(name2, typedColumns, { ...surface, query: (context) => buildResolvedQuery({ context, plan: queryPlan }), resolved });
|
|
2439
|
+
}
|
|
2440
|
+
const materialize = config.materialize === true ? {} : config.materialize;
|
|
2441
|
+
const computeName = `${name2}__compute`;
|
|
2442
|
+
const tableName = `${name2}__materialized`;
|
|
2443
|
+
const compute = defineView(computeName, typedColumns, {
|
|
2444
|
+
policies: { read },
|
|
2445
|
+
query: (context) => buildResolvedQuery({ context, plan: queryPlan }),
|
|
2446
|
+
internal: true
|
|
2447
|
+
});
|
|
2448
|
+
const table = defineTable(tableName, viewColumns, {
|
|
2449
|
+
description: `Materialized rows of resolved table "${name2}". System-maintained from "${computeName}".`,
|
|
2450
|
+
internal: true,
|
|
2451
|
+
materializes: name2,
|
|
2452
|
+
...materialize.indexes !== void 0 ? { indexes: materialize.indexes } : {}
|
|
2453
|
+
});
|
|
2454
|
+
return defineView(name2, typedColumns, {
|
|
2455
|
+
...surface,
|
|
2456
|
+
query: (context) => {
|
|
2457
|
+
const handle = context.relation(table);
|
|
2458
|
+
const selected = Object.fromEntries(Object.entries(viewColumns).map(([property, builder]) => [
|
|
2459
|
+
property,
|
|
2460
|
+
drizzleOrm.sql`${handle[property]}`.as(builder.columnNameFor(property))
|
|
2461
|
+
]));
|
|
2462
|
+
return context.qb.select(selected).from(context.relation(table));
|
|
2463
|
+
},
|
|
2464
|
+
resolved: {
|
|
2465
|
+
...resolved,
|
|
2466
|
+
internalTables: [...resolved.internalTables, table],
|
|
2467
|
+
internalViews: [compute],
|
|
2468
|
+
materialized: { table: tableName, compute: computeName }
|
|
2469
|
+
}
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
function planRules({ config, plans, dataColumns, fail }) {
|
|
2473
|
+
const kindOrder = { manual: 0, prefetch: 1, etl: 2 };
|
|
2474
|
+
const fallback = config.default ?? priority([...plans].sort((a, b) => kindOrder[a.kind] - kindOrder[b.kind]).map((plan) => plan.name));
|
|
2475
|
+
for (const column of Object.keys(config.resolve ?? {})) {
|
|
2476
|
+
if (!dataColumns.includes(column)) fail(`resolve names unknown column "${column}"`);
|
|
2477
|
+
}
|
|
2478
|
+
return dataColumns.map((column) => {
|
|
2479
|
+
const explicit = config.resolve?.[column];
|
|
2480
|
+
const comparator = explicit ?? fallback;
|
|
2481
|
+
const candidates = explicit ? [...explicit.candidates] : comparator.candidates.filter((name2) => plans.find((plan) => plan.name === name2)?.columns.includes(column));
|
|
2482
|
+
for (const candidate of candidates) {
|
|
2483
|
+
const plan = plans.find((entry) => entry.name === candidate);
|
|
2484
|
+
if (!plan) fail(`column "${column}" names unknown source "${candidate}"`);
|
|
2485
|
+
else if (!plan.columns.includes(column)) fail(`column "${column}" candidate "${candidate}" does not supply it`);
|
|
2486
|
+
}
|
|
2487
|
+
if (candidates.length === 0) fail(`column "${column}" has no candidate source`);
|
|
2488
|
+
if (comparator.kind === "custom") assertSafeOrderBy({ orderBy: comparator.orderBy ?? "", column, fail });
|
|
2489
|
+
return {
|
|
2490
|
+
column,
|
|
2491
|
+
comparator: comparator.kind,
|
|
2492
|
+
candidates,
|
|
2493
|
+
...comparator.kind === "custom" && comparator.orderBy !== void 0 ? { orderBy: comparator.orderBy } : {}
|
|
2494
|
+
};
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
var ORDER_BY_KEYWORDS = /* @__PURE__ */ new Set([
|
|
2498
|
+
"value",
|
|
2499
|
+
"updated_at",
|
|
2500
|
+
"source",
|
|
2501
|
+
"actor",
|
|
2502
|
+
"ord",
|
|
2503
|
+
"asc",
|
|
2504
|
+
"desc",
|
|
2505
|
+
"nulls",
|
|
2506
|
+
"first",
|
|
2507
|
+
"last",
|
|
2508
|
+
"case",
|
|
2509
|
+
"when",
|
|
2510
|
+
"then",
|
|
2511
|
+
"else",
|
|
2512
|
+
"end",
|
|
2513
|
+
"and",
|
|
2514
|
+
"or",
|
|
2515
|
+
"not",
|
|
2516
|
+
"is",
|
|
2517
|
+
"null",
|
|
2518
|
+
"true",
|
|
2519
|
+
"false",
|
|
2520
|
+
"in",
|
|
2521
|
+
"distinct",
|
|
2522
|
+
"from",
|
|
2523
|
+
"coalesce",
|
|
2524
|
+
"nullif",
|
|
2525
|
+
"greatest",
|
|
2526
|
+
"least",
|
|
2527
|
+
"text",
|
|
2528
|
+
"numeric",
|
|
2529
|
+
"integer",
|
|
2530
|
+
"bigint",
|
|
2531
|
+
"boolean",
|
|
2532
|
+
"timestamptz",
|
|
2533
|
+
"date",
|
|
2534
|
+
"jsonb",
|
|
2535
|
+
"interval"
|
|
2536
|
+
]);
|
|
2537
|
+
var ALLOWED_FUNCTIONS = new Set(VIEW_QUERY_FUNCTION_ALLOWLIST);
|
|
2538
|
+
var ORDER_BY_TOKEN = /\s+|'(?:[^']|'')*'|\d+(?:\.\d+)?|[a-z_][a-z0-9_]*|::|<=|>=|<>|!=|[-+*/%<>=(),|]/giy;
|
|
2539
|
+
function assertSafeOrderBy({ orderBy, column, fail }) {
|
|
2540
|
+
ORDER_BY_TOKEN.lastIndex = 0;
|
|
2541
|
+
let consumed = 0;
|
|
2542
|
+
for (let match = ORDER_BY_TOKEN.exec(orderBy); match; match = ORDER_BY_TOKEN.exec(orderBy)) {
|
|
2543
|
+
consumed = ORDER_BY_TOKEN.lastIndex;
|
|
2544
|
+
const token = match[0].toLowerCase();
|
|
2545
|
+
if (/^[a-z_]/.test(token) && !ORDER_BY_KEYWORDS.has(token) && !ALLOWED_FUNCTIONS.has(token)) {
|
|
2546
|
+
fail(`column "${column}" custom orderBy uses "${match[0]}" \u2014 only value, updated_at, source, actor, ord, SQL keywords and allowlisted functions`);
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
if (consumed !== orderBy.length || orderBy.trim() === "") {
|
|
2550
|
+
fail(`column "${column}" custom orderBy contains an unsupported character near "${orderBy.slice(consumed, consumed + 10)}"`);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
1278
2553
|
function stableJson(value) {
|
|
1279
2554
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
1280
2555
|
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
@@ -1282,6 +2557,72 @@ function stableJson(value) {
|
|
|
1282
2557
|
return `{${entries.join(",")}}`;
|
|
1283
2558
|
}
|
|
1284
2559
|
|
|
2560
|
+
// src/canonical/composition.ts
|
|
2561
|
+
function localContract(definition) {
|
|
2562
|
+
return normalizeCanonical(localDefinitionSchema.parse(definition));
|
|
2563
|
+
}
|
|
2564
|
+
function flattenNamespaceImports(inputs) {
|
|
2565
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
2566
|
+
const visit = (value, depth) => {
|
|
2567
|
+
if (depth > 16 || definitions.size > 32) throw new OrmDefinitionError("namespace import graph is too large");
|
|
2568
|
+
const definition = localContract(localDefinitionSchema.parse(value));
|
|
2569
|
+
const previous = definitions.get(definition.namespace);
|
|
2570
|
+
if (previous) {
|
|
2571
|
+
if (stableJson(previous) !== stableJson(definition)) throw new OrmDefinitionError(`conflicting contracts for namespace '${definition.namespace}'`);
|
|
2572
|
+
}
|
|
2573
|
+
definitions.set(definition.namespace, definition);
|
|
2574
|
+
if (typeof value === "object" && value !== null && "imports" in value && Array.isArray(value.imports)) {
|
|
2575
|
+
for (const dependency of value.imports) visit(dependency, depth + 1);
|
|
2576
|
+
}
|
|
2577
|
+
};
|
|
2578
|
+
for (const input of inputs) visit(input, 0);
|
|
2579
|
+
return [...definitions.values()].sort((a, b) => a.namespace.localeCompare(b.namespace));
|
|
2580
|
+
}
|
|
2581
|
+
function composeNamespace(definition) {
|
|
2582
|
+
const all = [localContract(definition), ...definition.imports ?? []];
|
|
2583
|
+
const owners = /* @__PURE__ */ new Map();
|
|
2584
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
2585
|
+
const enums = /* @__PURE__ */ new Map();
|
|
2586
|
+
for (const owner of all) {
|
|
2587
|
+
if (namespaces.has(owner.namespace)) throw new OrmDefinitionError(`duplicate imported namespace '${owner.namespace}'`);
|
|
2588
|
+
namespaces.add(owner.namespace);
|
|
2589
|
+
for (const value of owner.enums) {
|
|
2590
|
+
const previous = enums.get(value.name);
|
|
2591
|
+
if (previous && stableJson(previous) !== stableJson(value)) {
|
|
2592
|
+
throw new OrmDefinitionError(`conflicting imported enum '${value.name}'`);
|
|
2593
|
+
}
|
|
2594
|
+
enums.set(value.name, value);
|
|
2595
|
+
}
|
|
2596
|
+
for (const relation of canonicalRelations(owner)) {
|
|
2597
|
+
if (owners.has(relation.table)) throw new OrmDefinitionError(`ambiguous imported relation '${relation.table}'`);
|
|
2598
|
+
owners.set(relation.table, owner);
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
for (const owner of definition.imports?.length ? all : []) {
|
|
2602
|
+
for (const relation of canonicalRelations(owner)) {
|
|
2603
|
+
for (const link of relation.links) {
|
|
2604
|
+
if (link.kind === "toPlatformUser") continue;
|
|
2605
|
+
const target = owners.get(link.target ?? "");
|
|
2606
|
+
if (!target || target.namespace !== (link.namespace ?? owner.namespace)) {
|
|
2607
|
+
throw new OrmDefinitionError(`link '${owner.namespace}/${relation.table}.${link.name}' has no matching imported target`);
|
|
2608
|
+
}
|
|
2609
|
+
if (target.namespace !== owner.namespace && !target.exports?.[link.target ?? ""]) {
|
|
2610
|
+
throw new OrmDefinitionError(`target '${target.namespace}/${link.target}' is not exported`);
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
return {
|
|
2616
|
+
owners,
|
|
2617
|
+
definition: {
|
|
2618
|
+
...localContract(definition),
|
|
2619
|
+
tables: all.flatMap((item) => item.tables),
|
|
2620
|
+
views: all.flatMap((item) => item.views ?? []),
|
|
2621
|
+
enums: [...enums.values()]
|
|
2622
|
+
}
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
|
|
1285
2626
|
// src/authoring/namespace.ts
|
|
1286
2627
|
var NAMESPACE_CONFIG_BRAND = "seq-namespace-config";
|
|
1287
2628
|
function isSeqNamespaceConfig(value) {
|
|
@@ -1297,14 +2638,27 @@ function defineNamespace(input) {
|
|
|
1297
2638
|
namespace: input.namespace,
|
|
1298
2639
|
description: input.description,
|
|
1299
2640
|
capabilities: input.capabilities ?? {},
|
|
2641
|
+
imports: input.imports,
|
|
2642
|
+
exports: input.exports,
|
|
1300
2643
|
file
|
|
1301
2644
|
};
|
|
1302
2645
|
registerNamespaceConfig(config);
|
|
1303
2646
|
return config;
|
|
1304
2647
|
}
|
|
1305
2648
|
function serializeNamespace(input) {
|
|
1306
|
-
const { config
|
|
1307
|
-
const views = input.views ?? [];
|
|
2649
|
+
const { config } = input;
|
|
2650
|
+
const views = [...input.views ?? []];
|
|
2651
|
+
const tables = [...input.tables];
|
|
2652
|
+
for (const view of views) {
|
|
2653
|
+
for (const internal of view.config.resolved?.internalTables ?? []) {
|
|
2654
|
+
if (!tables.includes(internal)) tables.push(internal);
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
for (const view of [...views]) {
|
|
2658
|
+
for (const internal of view.config.resolved?.internalViews ?? []) {
|
|
2659
|
+
if (!views.includes(internal)) views.push(internal);
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
1308
2662
|
const enums = input.enums ?? [];
|
|
1309
2663
|
const tablesByName = /* @__PURE__ */ new Map();
|
|
1310
2664
|
for (const table of tables) {
|
|
@@ -1330,15 +2684,15 @@ function serializeNamespace(input) {
|
|
|
1330
2684
|
}
|
|
1331
2685
|
const relationsByName = /* @__PURE__ */ new Map();
|
|
1332
2686
|
for (const relation of [...tables, ...views]) {
|
|
1333
|
-
const
|
|
1334
|
-
const existing = relationsByName.get(
|
|
2687
|
+
const name2 = relationName(relation);
|
|
2688
|
+
const existing = relationsByName.get(name2);
|
|
1335
2689
|
if (existing) {
|
|
1336
2690
|
throw new OrmDefinitionError(
|
|
1337
|
-
`duplicate relation "${
|
|
2691
|
+
`duplicate relation "${name2}" (defined as ${relationKind(existing)} in ${describeFile(existing)} and ${relationKind(relation)} in ${describeFile(relation)})`,
|
|
1338
2692
|
relation.file
|
|
1339
2693
|
);
|
|
1340
2694
|
}
|
|
1341
|
-
relationsByName.set(
|
|
2695
|
+
relationsByName.set(name2, relation);
|
|
1342
2696
|
}
|
|
1343
2697
|
const enumsByName = /* @__PURE__ */ new Map();
|
|
1344
2698
|
for (const ref of enums) {
|
|
@@ -1379,6 +2733,8 @@ function serializeNamespace(input) {
|
|
|
1379
2733
|
};
|
|
1380
2734
|
if (config.description !== void 0) candidate.description = config.description;
|
|
1381
2735
|
if (canonicalViews.length > 0) candidate.views = canonicalViews;
|
|
2736
|
+
if (config.imports?.length) candidate.imports = flattenNamespaceImports(config.imports);
|
|
2737
|
+
if (config.exports) candidate.exports = config.exports;
|
|
1382
2738
|
let parsed;
|
|
1383
2739
|
try {
|
|
1384
2740
|
parsed = canonicalDefinitionSchema.parse(candidate);
|
|
@@ -1388,7 +2744,9 @@ function serializeNamespace(input) {
|
|
|
1388
2744
|
}
|
|
1389
2745
|
throw error;
|
|
1390
2746
|
}
|
|
1391
|
-
|
|
2747
|
+
const normalized = normalizeCanonical(parsed);
|
|
2748
|
+
if (normalized.imports?.length) composeNamespace(normalized);
|
|
2749
|
+
return normalized;
|
|
1392
2750
|
}
|
|
1393
2751
|
function computeCanonicalHash(definition) {
|
|
1394
2752
|
return crypto.createHash("sha256").update(HASH_DOMAIN).update(stableJson(definition)).digest("hex");
|
|
@@ -1402,10 +2760,14 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1402
2760
|
policies: {},
|
|
1403
2761
|
actions: []
|
|
1404
2762
|
};
|
|
2763
|
+
if (table.config.displayName !== void 0) candidate.displayName = table.config.displayName;
|
|
1405
2764
|
if (table.config.description !== void 0) candidate.description = table.config.description;
|
|
1406
2765
|
if (table.config.renamedFrom !== void 0) candidate.renamedFrom = table.config.renamedFrom;
|
|
1407
2766
|
if (table.source !== void 0) candidate.source = table.source;
|
|
1408
2767
|
if (table.source !== void 0) {
|
|
2768
|
+
if (table.config.checks?.length) {
|
|
2769
|
+
throw new OrmDefinitionError(`synced table "${table.tableName}" is system-owned; checks are not allowed`, table.file);
|
|
2770
|
+
}
|
|
1409
2771
|
if (table.config.indexes !== void 0 && table.config.indexes.length > 0) {
|
|
1410
2772
|
throw new OrmDefinitionError(
|
|
1411
2773
|
`synced table "${table.tableName}" is system-owned; indexes are not allowed`,
|
|
@@ -1443,6 +2805,11 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1443
2805
|
if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
|
|
1444
2806
|
candidate.columns?.push(column);
|
|
1445
2807
|
}
|
|
2808
|
+
if (table.config.internal === true) candidate.internal = true;
|
|
2809
|
+
if (table.config.materializes !== void 0) candidate.materializes = table.config.materializes;
|
|
2810
|
+
if (table.config.cellMetadata === true && table.source !== void 0) {
|
|
2811
|
+
throw new OrmDefinitionError(`synced table "${table.tableName}" cannot track cell metadata \u2014 its DDL is owned by the sync`, table.file);
|
|
2812
|
+
}
|
|
1446
2813
|
if (table.config.primaryKey !== void 0) {
|
|
1447
2814
|
if (columnLevelPk.length > 0) {
|
|
1448
2815
|
throw new OrmDefinitionError(
|
|
@@ -1456,9 +2823,30 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1456
2823
|
} else if (columnLevelPk.length > 0) {
|
|
1457
2824
|
candidate.primaryKey = columnLevelPk;
|
|
1458
2825
|
}
|
|
2826
|
+
if (table.config.cellMetadata === true) {
|
|
2827
|
+
candidate.cellMetadata = true;
|
|
2828
|
+
const parts = table.config.cellRevisions === true ? ["at", "by", "rev"] : ["at", "by"];
|
|
2829
|
+
const tracked = trackedCellColumns({ columns: candidate.columns ?? [], primaryKey: candidate.primaryKey });
|
|
2830
|
+
for (const column of tracked) {
|
|
2831
|
+
for (const part of parts) {
|
|
2832
|
+
candidate.columns?.push({
|
|
2833
|
+
name: cellColumnName(column.name, part),
|
|
2834
|
+
columnName: cellColumnName(column.columnName, part),
|
|
2835
|
+
type: CELL_PART_TYPES[part],
|
|
2836
|
+
nullable: true,
|
|
2837
|
+
unique: false,
|
|
2838
|
+
deprecated: false,
|
|
2839
|
+
cell: { of: column.name, part }
|
|
2840
|
+
});
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
1459
2844
|
if (table.config.foreignKeys?.length) {
|
|
1460
2845
|
if (table.source) throw new OrmDefinitionError(`synced table "${table.tableName}" cannot enforce foreign keys`, table.file);
|
|
1461
2846
|
candidate.foreignKeys = table.config.foreignKeys.map((fk) => {
|
|
2847
|
+
if (typeof fk.target === "object" && fk.target.kind === "seq-external-object") {
|
|
2848
|
+
throw new OrmDefinitionError("physical foreign keys require a local target; use a cross-namespace ORM link", table.file);
|
|
2849
|
+
}
|
|
1462
2850
|
const target = resolveRelationRef({ ref: fk.target, fail: (message) => {
|
|
1463
2851
|
throw new OrmDefinitionError(message, table.file);
|
|
1464
2852
|
} });
|
|
@@ -1468,10 +2856,15 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1468
2856
|
columns: fk.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: `foreign key "${fk.name}" column` })),
|
|
1469
2857
|
target: target.tableName,
|
|
1470
2858
|
references: fk.references.map((ref) => resolveColumnRef({ relation: target, ref, role: `foreign key "${fk.name}" reference` })),
|
|
1471
|
-
...fk.deferrable ? { deferrable: fk.deferrable } : {}
|
|
2859
|
+
...fk.deferrable ? { deferrable: fk.deferrable } : {},
|
|
2860
|
+
...fk.onDelete ? { onDelete: fk.onDelete } : {},
|
|
2861
|
+
...fk.onUpdate ? { onUpdate: fk.onUpdate } : {}
|
|
1472
2862
|
};
|
|
1473
2863
|
});
|
|
1474
2864
|
}
|
|
2865
|
+
if (table.config.checks?.length) {
|
|
2866
|
+
candidate.checks = table.config.checks.map((check) => ({ name: check.name, expression: check.expression }));
|
|
2867
|
+
}
|
|
1475
2868
|
for (const index of table.config.indexes ?? []) {
|
|
1476
2869
|
const entry = {
|
|
1477
2870
|
columns: index.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
|
|
@@ -1567,7 +2960,33 @@ function buildCanonicalView({
|
|
|
1567
2960
|
columns,
|
|
1568
2961
|
...primaryKey !== void 0 ? { primaryKey } : {},
|
|
1569
2962
|
links,
|
|
1570
|
-
policies: { read: toCanonicalPolicy(view.config.policies.read) }
|
|
2963
|
+
policies: { read: toCanonicalPolicy(view.config.policies.read) },
|
|
2964
|
+
...view.config.resolved !== void 0 ? { resolved: buildCanonicalResolved(view.config.resolved) } : {},
|
|
2965
|
+
...view.config.internal === true ? { internal: true } : {}
|
|
2966
|
+
};
|
|
2967
|
+
}
|
|
2968
|
+
function buildCanonicalResolved(resolved) {
|
|
2969
|
+
const policies = {};
|
|
2970
|
+
if (resolved.policies.write !== void 0) policies.write = toCanonicalPolicy(resolved.policies.write);
|
|
2971
|
+
if (resolved.policies.insert !== void 0) policies.insert = toCanonicalPolicy(resolved.policies.insert);
|
|
2972
|
+
if (resolved.policies.update !== void 0) policies.update = toCanonicalPolicy(resolved.policies.update);
|
|
2973
|
+
return {
|
|
2974
|
+
sources: resolved.sources.map((source) => ({
|
|
2975
|
+
name: source.name,
|
|
2976
|
+
kind: source.kind,
|
|
2977
|
+
table: source.table.tableName,
|
|
2978
|
+
columns: [...source.columns],
|
|
2979
|
+
...source.updatedAt !== void 0 ? { updatedAt: source.updatedAt } : {},
|
|
2980
|
+
...source.key !== void 0 ? { key: source.key } : {},
|
|
2981
|
+
...source.fetch !== void 0 ? { fetch: source.fetch } : {},
|
|
2982
|
+
...source.function !== void 0 ? { function: { ...source.function } } : {},
|
|
2983
|
+
...source.prefetchDefaults !== void 0 ? { prefetchDefaults: { ...source.prefetchDefaults } } : {},
|
|
2984
|
+
...source.writeback !== void 0 ? { writeback: { ...source.writeback, columns: [...source.writeback.columns] } } : {}
|
|
2985
|
+
})),
|
|
2986
|
+
resolve: resolved.rules.map((rule) => ({ ...rule, candidates: [...rule.candidates] })),
|
|
2987
|
+
deletedBy: [...resolved.deletedBy],
|
|
2988
|
+
...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
|
|
2989
|
+
policies
|
|
1571
2990
|
};
|
|
1572
2991
|
}
|
|
1573
2992
|
function buildCanonicalLink({
|
|
@@ -1584,6 +3003,9 @@ function buildCanonicalLink({
|
|
|
1584
3003
|
if (link.enforce && link.linkKind !== "toOne") {
|
|
1585
3004
|
return fail("sets enforce: true, which is only supported on toOne links");
|
|
1586
3005
|
}
|
|
3006
|
+
if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && !link.enforce) {
|
|
3007
|
+
return fail("sets a referential action without enforce: true");
|
|
3008
|
+
}
|
|
1587
3009
|
if (link.linkKind === "toPlatformUser") {
|
|
1588
3010
|
if (link.target !== void 0) return fail("is toPlatformUser and must not declare a target table");
|
|
1589
3011
|
return {
|
|
@@ -1595,6 +3017,19 @@ function buildCanonicalLink({
|
|
|
1595
3017
|
...link.description !== void 0 ? { description: link.description } : {}
|
|
1596
3018
|
};
|
|
1597
3019
|
}
|
|
3020
|
+
if (typeof link.target === "object" && link.target?.kind === "seq-external-object") {
|
|
3021
|
+
if (link.enforce) return fail("cannot enforce a cross-namespace physical foreign key; use an ORM link without enforce");
|
|
3022
|
+
return {
|
|
3023
|
+
name: linkName,
|
|
3024
|
+
kind: link.linkKind,
|
|
3025
|
+
namespace: link.target.namespace,
|
|
3026
|
+
target: link.target.table,
|
|
3027
|
+
on: link.linkKind === "toOne" ? resolveColumnRef({ relation, ref: link.on, role: "link on" }) : link.on,
|
|
3028
|
+
...link.references !== void 0 ? { references: link.linkKind === "toMany" ? resolveColumnRef({ relation, ref: link.references, role: "link references" }) : link.references } : {},
|
|
3029
|
+
enforce: false,
|
|
3030
|
+
...link.description !== void 0 ? { description: link.description } : {}
|
|
3031
|
+
};
|
|
3032
|
+
}
|
|
1598
3033
|
const target = resolveRelationRef({ ref: link.target, fail });
|
|
1599
3034
|
const targetName = relationName(target);
|
|
1600
3035
|
if (!relationsByName.has(targetName)) {
|
|
@@ -1612,6 +3047,8 @@ function buildCanonicalLink({
|
|
|
1612
3047
|
on: resolveColumnRef({ relation: owningSide, ref: link.on, role: `link "${linkName}" on` }),
|
|
1613
3048
|
...link.references !== void 0 ? { references: resolveColumnRef({ relation: referencedSide, ref: link.references, role: `link "${linkName}" references` }) } : {},
|
|
1614
3049
|
enforce: link.enforce,
|
|
3050
|
+
...link.onDelete !== void 0 ? { onDelete: link.onDelete } : {},
|
|
3051
|
+
...link.onUpdate !== void 0 ? { onUpdate: link.onUpdate } : {},
|
|
1615
3052
|
...link.description !== void 0 ? { description: link.description } : {}
|
|
1616
3053
|
};
|
|
1617
3054
|
}
|
|
@@ -1649,11 +3086,11 @@ function assertAcyclicViewDependencies(views) {
|
|
|
1649
3086
|
views.map((view) => [view.view, new Set(view.dependencies.filter((dependency) => viewNames.has(dependency)))])
|
|
1650
3087
|
);
|
|
1651
3088
|
while (remaining.size > 0) {
|
|
1652
|
-
const ready = [...remaining.entries()].filter(([, dependencies]) => [...dependencies].every((dependency) => !remaining.has(dependency))).map(([
|
|
3089
|
+
const ready = [...remaining.entries()].filter(([, dependencies]) => [...dependencies].every((dependency) => !remaining.has(dependency))).map(([name2]) => name2);
|
|
1653
3090
|
if (ready.length === 0) {
|
|
1654
3091
|
throw new OrmDefinitionError(`view dependencies form a cycle among: ${[...remaining.keys()].sort().join(", ")}`);
|
|
1655
3092
|
}
|
|
1656
|
-
for (const
|
|
3093
|
+
for (const name2 of ready) remaining.delete(name2);
|
|
1657
3094
|
}
|
|
1658
3095
|
}
|
|
1659
3096
|
function toCanonicalPolicy(policy) {
|
|
@@ -1699,8 +3136,8 @@ function syncedContractSurface({
|
|
|
1699
3136
|
namespace,
|
|
1700
3137
|
table,
|
|
1701
3138
|
columns: Object.fromEntries(
|
|
1702
|
-
Object.entries(columns).map(([
|
|
1703
|
-
|
|
3139
|
+
Object.entries(columns).map(([name2, column]) => [
|
|
3140
|
+
name2,
|
|
1704
3141
|
{ type: column.type, nullable: column.nullable, key: column.key }
|
|
1705
3142
|
])
|
|
1706
3143
|
),
|
|
@@ -1721,7 +3158,7 @@ function extractOrmSyncedTableContract({
|
|
|
1721
3158
|
}
|
|
1722
3159
|
if (table.source === void 0) {
|
|
1723
3160
|
throw new OrmDefinitionError(
|
|
1724
|
-
`table "${table.table}" is not a synced table \u2014 extractOrmSyncedTableContract requires source
|
|
3161
|
+
`table "${table.table}" is not a synced table \u2014 extractOrmSyncedTableContract requires a qualified source`
|
|
1725
3162
|
);
|
|
1726
3163
|
}
|
|
1727
3164
|
if (table.columns.length === 0) {
|
|
@@ -1868,6 +3305,53 @@ function purgeModuleCache(root) {
|
|
|
1868
3305
|
if (key.startsWith(prefix)) delete require2.cache[key];
|
|
1869
3306
|
}
|
|
1870
3307
|
}
|
|
3308
|
+
var sqlScalarSchema = zod.z.enum(["text", "boolean", "integer", "bigint", "numeric", "double", "uuid", "date", "timestamptz"]);
|
|
3309
|
+
var name = zod.z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).max(63).refine((value) => !["__proto__", "prototype", "constructor"].includes(value), "reserved name");
|
|
3310
|
+
var base = zod.z.object({
|
|
3311
|
+
name,
|
|
3312
|
+
namespace: name,
|
|
3313
|
+
language: zod.z.literal("sql"),
|
|
3314
|
+
kind: zod.z.literal("query"),
|
|
3315
|
+
requires: zod.z.object({ capability: zod.z.string().min(1).max(128) }).strict(),
|
|
3316
|
+
ontology: zod.z.object({ read: zod.z.array(zod.z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*\/[A-Za-z_][A-Za-z0-9_]*$/)).min(1).max(256) }).strict(),
|
|
3317
|
+
maxRows: zod.z.number().int().min(1).max(1e4).default(1e3)
|
|
3318
|
+
});
|
|
3319
|
+
var fixed = base.extend({
|
|
3320
|
+
parameters: zod.z.record(name, zod.z.object({ type: sqlScalarSchema, required: zod.z.boolean().default(true) }).strict()).default({}),
|
|
3321
|
+
sql: zod.z.string().min(1).max(65536)
|
|
3322
|
+
}).strict();
|
|
3323
|
+
var runtime = base.extend({ mode: zod.z.literal("runtime") }).strict();
|
|
3324
|
+
var sqlOperationDefinitionSchema = zod.z.union([fixed, runtime]);
|
|
3325
|
+
function defineOperation(input) {
|
|
3326
|
+
return sqlOperationDefinitionSchema.parse(input);
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
// src/authoring/catalog.ts
|
|
3330
|
+
var SEGMENT = /^[^./\\\s][^/\\]*[^./\\\s]$|^[^./\\\s]$/;
|
|
3331
|
+
function validateNamespaceCatalog(catalog, file) {
|
|
3332
|
+
const paths = /* @__PURE__ */ new Set([""]);
|
|
3333
|
+
function visit(folders, parent) {
|
|
3334
|
+
for (const [name2, children] of Object.entries(folders)) {
|
|
3335
|
+
if (!SEGMENT.test(name2) || ["__proto__", "constructor", "prototype"].includes(name2)) {
|
|
3336
|
+
throw new OrmDefinitionError(`invalid catalog folder name "${name2}"`, file);
|
|
3337
|
+
}
|
|
3338
|
+
const path = parent ? `${parent}/${name2}` : name2;
|
|
3339
|
+
paths.add(path);
|
|
3340
|
+
if (children !== null) {
|
|
3341
|
+
if (typeof children !== "object" || Array.isArray(children)) {
|
|
3342
|
+
throw new OrmDefinitionError(`catalog folder "${path}" must contain folders or null`, file);
|
|
3343
|
+
}
|
|
3344
|
+
visit(children, path);
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
visit(catalog.folders, "");
|
|
3349
|
+
for (const [object, path] of Object.entries(catalog.objects)) {
|
|
3350
|
+
if (!object || !paths.has(path)) {
|
|
3351
|
+
throw new OrmDefinitionError(`catalog object "${object}" references missing folder "${path}"`, file);
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
1871
3355
|
|
|
1872
3356
|
exports.COLUMN_BRAND = COLUMN_BRAND;
|
|
1873
3357
|
exports.COLUMN_DEFAULT_FNS = COLUMN_DEFAULT_FNS;
|
|
@@ -1885,23 +3369,36 @@ exports.activeAuthoringContext = activeAuthoringContext;
|
|
|
1885
3369
|
exports.beginAuthoringContext = beginAuthoringContext;
|
|
1886
3370
|
exports.computeCanonicalHash = computeCanonicalHash;
|
|
1887
3371
|
exports.computeSyncedContractHash = computeSyncedContractHash;
|
|
3372
|
+
exports.custom = custom;
|
|
1888
3373
|
exports.defineEnum = defineEnum;
|
|
1889
3374
|
exports.defineNamespace = defineNamespace;
|
|
3375
|
+
exports.defineOperation = defineOperation;
|
|
3376
|
+
exports.defineResolvedTable = defineResolvedTable;
|
|
1890
3377
|
exports.defineSyncedTable = defineSyncedTable;
|
|
1891
3378
|
exports.defineTable = defineTable;
|
|
1892
3379
|
exports.defineView = defineView;
|
|
1893
3380
|
exports.deriveColumnName = deriveColumnName;
|
|
1894
3381
|
exports.endAuthoringContext = endAuthoringContext;
|
|
3382
|
+
exports.etlSource = etlSource;
|
|
1895
3383
|
exports.extractOrmSyncedTableContract = extractOrmSyncedTableContract;
|
|
3384
|
+
exports.highest = highest;
|
|
1896
3385
|
exports.isColumnBuilder = isColumnBuilder;
|
|
1897
3386
|
exports.isEnumRef = isEnumRef;
|
|
1898
3387
|
exports.isSeqLink = isSeqLink;
|
|
1899
3388
|
exports.isSeqNamespaceConfig = isSeqNamespaceConfig;
|
|
1900
3389
|
exports.isSeqTable = isSeqTable;
|
|
1901
3390
|
exports.isSeqView = isSeqView;
|
|
3391
|
+
exports.latest = latest;
|
|
1902
3392
|
exports.loadNamespaceFromDir = loadNamespaceFromDir;
|
|
3393
|
+
exports.lowest = lowest;
|
|
3394
|
+
exports.managedFn = managedFn;
|
|
3395
|
+
exports.manualSource = manualSource;
|
|
3396
|
+
exports.nonNullFirst = nonNullFirst;
|
|
3397
|
+
exports.objectRef = objectRef;
|
|
1903
3398
|
exports.overlayWinsUnion = overlayWinsUnion;
|
|
1904
3399
|
exports.overrideRowMutation = overrideRowMutation;
|
|
3400
|
+
exports.prefetchSource = prefetchSource;
|
|
3401
|
+
exports.priority = priority;
|
|
1905
3402
|
exports.revertOverlayMutation = revertOverlayMutation;
|
|
1906
3403
|
exports.serializeNamespace = serializeNamespace;
|
|
1907
3404
|
exports.syncedContractSurface = syncedContractSurface;
|
|
@@ -1909,5 +3406,6 @@ exports.t = t;
|
|
|
1909
3406
|
exports.toMany = toMany;
|
|
1910
3407
|
exports.toOne = toOne;
|
|
1911
3408
|
exports.toPlatformUser = toPlatformUser;
|
|
3409
|
+
exports.validateNamespaceCatalog = validateNamespaceCatalog;
|
|
1912
3410
|
//# sourceMappingURL=authoring.cjs.map
|
|
1913
3411
|
//# sourceMappingURL=authoring.cjs.map
|