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