@sequenceholdings/orm 0.1.2 → 2.0.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 +49 -24
- package/dist/authoring.cjs +1072 -60
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +291 -26
- package/dist/authoring.d.ts +291 -26
- package/dist/authoring.js +1062 -61
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +415 -9
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +235 -1
- package/dist/canonical.d.ts +235 -1
- package/dist/canonical.js +411 -10
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +1860 -228
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1861 -229
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +512 -54
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +39 -231
- package/dist/compiler.d.ts +39 -231
- package/dist/compiler.js +510 -55
- package/dist/compiler.js.map +1 -1
- package/dist/drizzle.cjs +14 -0
- package/dist/drizzle.cjs.map +1 -0
- package/dist/drizzle.d.cts +1 -0
- package/dist/drizzle.d.ts +1 -0
- package/dist/drizzle.js +3 -0
- package/dist/drizzle.js.map +1 -0
- package/dist/dynamic-input.cjs +391 -0
- package/dist/dynamic-input.cjs.map +1 -0
- package/dist/dynamic-input.d.cts +111 -0
- package/dist/dynamic-input.d.ts +111 -0
- package/dist/dynamic-input.js +380 -0
- package/dist/dynamic-input.js.map +1 -0
- package/dist/index.cjs +2229 -220
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +125 -9
- package/dist/index.d.ts +125 -9
- package/dist/index.js +2205 -221
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +249 -14
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +20 -80
- package/dist/ir.d.ts +20 -80
- package/dist/ir.js +249 -14
- package/dist/ir.js.map +1 -1
- package/dist/predicate-v2.cjs +1099 -0
- package/dist/predicate-v2.cjs.map +1 -0
- package/dist/predicate-v2.d.cts +142 -0
- package/dist/predicate-v2.d.ts +142 -0
- package/dist/predicate-v2.js +1089 -0
- package/dist/predicate-v2.js.map +1 -0
- package/dist/sdl.cjs +344 -55
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.d.cts +1 -3
- package/dist/sdl.d.ts +1 -3
- package/dist/sdl.js +344 -55
- package/dist/sdl.js.map +1 -1
- package/package.json +20 -3
- package/src/codegen-gql/preset.cjs +64 -0
- package/src/codegen-gql/server-fns.cjs +11 -6
- package/templates/orm-namespace/CLAUDE.md +6 -5
- package/templates/orm-namespace/codegen.preset.cjs +17 -0
- package/templates/orm-namespace/codegen.ts +1 -1
- package/templates/orm-namespace/package.json +3 -0
- package/templates/orm-namespace/schema/tables.ts +1 -1
- package/templates/orm-namespace/sequence.config.ts +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var drizzleOrm = require('drizzle-orm');
|
|
4
|
+
var pgCore = require('drizzle-orm/pg-core');
|
|
3
5
|
var crypto = require('crypto');
|
|
4
6
|
var zod = require('zod');
|
|
5
7
|
var jiti = require('jiti');
|
|
@@ -7,8 +9,6 @@ var promises = require('fs/promises');
|
|
|
7
9
|
var module$1 = require('module');
|
|
8
10
|
var path = require('path');
|
|
9
11
|
var api = require('drizzle-kit/api');
|
|
10
|
-
var pgCore = require('drizzle-orm/pg-core');
|
|
11
|
-
var drizzleOrm = require('drizzle-orm');
|
|
12
12
|
var jsYaml = require('js-yaml');
|
|
13
13
|
var customRolesManifest = require('@sequenceholdings/custom-roles-manifest');
|
|
14
14
|
|
|
@@ -71,8 +71,8 @@ var ColumnBuilder = class {
|
|
|
71
71
|
this.state.defaultFn = void 0;
|
|
72
72
|
return this;
|
|
73
73
|
}
|
|
74
|
-
description(
|
|
75
|
-
this.state.description =
|
|
74
|
+
description(text4) {
|
|
75
|
+
this.state.description = text4;
|
|
76
76
|
return this;
|
|
77
77
|
}
|
|
78
78
|
/** Migration rename hint: the previous Postgres column name. */
|
|
@@ -188,7 +188,7 @@ function contextStack() {
|
|
|
188
188
|
return created;
|
|
189
189
|
}
|
|
190
190
|
function beginAuthoringContext() {
|
|
191
|
-
const context = { currentFile: void 0, tables: [], enums: [], configs: [] };
|
|
191
|
+
const context = { currentFile: void 0, tables: [], views: [], enums: [], configs: [] };
|
|
192
192
|
contextStack().push(context);
|
|
193
193
|
return context;
|
|
194
194
|
}
|
|
@@ -212,6 +212,9 @@ function register(bucket, value) {
|
|
|
212
212
|
function registerTable(value) {
|
|
213
213
|
register("tables", value);
|
|
214
214
|
}
|
|
215
|
+
function registerView(value) {
|
|
216
|
+
register("views", value);
|
|
217
|
+
}
|
|
215
218
|
function registerEnum(value) {
|
|
216
219
|
register("enums", value);
|
|
217
220
|
}
|
|
@@ -299,20 +302,69 @@ function defineTable(name, columns, config = {}) {
|
|
|
299
302
|
registerTable(table);
|
|
300
303
|
return table;
|
|
301
304
|
}
|
|
302
|
-
function defineSyncedTable(name,
|
|
305
|
+
function defineSyncedTable(name, columnsOrConfig, maybeConfig) {
|
|
303
306
|
const file = activeAuthoringContext()?.currentFile;
|
|
304
307
|
assertIdentifier({ value: name, what: "table name", file });
|
|
308
|
+
const isTyped = maybeConfig !== void 0;
|
|
309
|
+
const columns = isTyped ? columnsOrConfig : {};
|
|
310
|
+
const config = isTyped ? maybeConfig : columnsOrConfig;
|
|
305
311
|
if (!SYNCED_SOURCE_PATTERN.test(config.source)) {
|
|
306
312
|
throw new OrmDefinitionError(
|
|
307
313
|
`synced table "${name}": source "${config.source}" must match ${SYNCED_SOURCE_PATTERN} (gold layer only)`,
|
|
308
314
|
file
|
|
309
315
|
);
|
|
310
316
|
}
|
|
317
|
+
const entries = Object.entries(columns);
|
|
318
|
+
const seenColumnNames = /* @__PURE__ */ new Map();
|
|
319
|
+
const columnLevelPk = [];
|
|
320
|
+
for (const [propertyName, builder] of entries) {
|
|
321
|
+
if (!isColumnBuilder(builder)) {
|
|
322
|
+
throw new OrmDefinitionError(
|
|
323
|
+
`synced table "${name}": column "${propertyName}" is not a column builder \u2014 use the t.* factories (t.uuid(), t.text(), \u2026)`,
|
|
324
|
+
file
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
const columnName = builder.columnNameFor(propertyName);
|
|
328
|
+
assertIdentifier({
|
|
329
|
+
value: columnName,
|
|
330
|
+
what: `synced table "${name}": column "${propertyName}" column name`,
|
|
331
|
+
file
|
|
332
|
+
});
|
|
333
|
+
const existing = seenColumnNames.get(columnName);
|
|
334
|
+
if (existing !== void 0) {
|
|
335
|
+
throw new OrmDefinitionError(
|
|
336
|
+
`synced table "${name}": columns "${existing}" and "${propertyName}" both map to Postgres column "${columnName}"`,
|
|
337
|
+
file
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
seenColumnNames.set(columnName, propertyName);
|
|
341
|
+
if (builder.isPrimaryKey) columnLevelPk.push(propertyName);
|
|
342
|
+
}
|
|
343
|
+
if (entries.length > 0) {
|
|
344
|
+
const hasConfigPk = config.primaryKey !== void 0 && config.primaryKey.length > 0;
|
|
345
|
+
if (columnLevelPk.length === 0 && !hasConfigPk) {
|
|
346
|
+
throw new OrmDefinitionError(
|
|
347
|
+
`synced table "${name}" declares columns but no primary key \u2014 use .primaryKey() or config.primaryKey`,
|
|
348
|
+
file
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
if (columnLevelPk.length > 0 && hasConfigPk) {
|
|
352
|
+
throw new OrmDefinitionError(
|
|
353
|
+
`synced table "${name}": declare the primary key either with .primaryKey() on columns or with config.primaryKey \u2014 not both`,
|
|
354
|
+
file
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const tableConfig = {};
|
|
359
|
+
if (config.description !== void 0) tableConfig.description = config.description;
|
|
360
|
+
if (config.primaryKey !== void 0) tableConfig.primaryKey = config.primaryKey;
|
|
361
|
+
if (config.links !== void 0) tableConfig.links = config.links;
|
|
362
|
+
if (config.policies !== void 0) tableConfig.policies = { read: config.policies.read };
|
|
311
363
|
const table = {
|
|
312
364
|
kind: TABLE_BRAND,
|
|
313
365
|
tableName: name,
|
|
314
|
-
columns
|
|
315
|
-
config:
|
|
366
|
+
columns,
|
|
367
|
+
config: tableConfig,
|
|
316
368
|
source: config.source,
|
|
317
369
|
file
|
|
318
370
|
};
|
|
@@ -342,6 +394,475 @@ function defineEnum(name, values) {
|
|
|
342
394
|
registerEnum(ref);
|
|
343
395
|
return ref;
|
|
344
396
|
}
|
|
397
|
+
var VIEW_BRAND = "seq-view";
|
|
398
|
+
function isSeqView(value) {
|
|
399
|
+
return typeof value === "object" && value !== null && value.kind === VIEW_BRAND;
|
|
400
|
+
}
|
|
401
|
+
function defineView(name, columns, config) {
|
|
402
|
+
const file = activeAuthoringContext()?.currentFile;
|
|
403
|
+
if (!IDENTIFIER_PATTERN.test(name) || name.length > MAX_IDENTIFIER_LENGTH) {
|
|
404
|
+
throw new OrmDefinitionError(
|
|
405
|
+
`view name "${name}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`,
|
|
406
|
+
file
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
const entries = Object.entries(columns);
|
|
410
|
+
if (entries.length === 0) throw new OrmDefinitionError(`view "${name}" declares no columns`, file);
|
|
411
|
+
const seenColumnNames = /* @__PURE__ */ new Map();
|
|
412
|
+
for (const [propertyName, builder] of entries) {
|
|
413
|
+
if (!isColumnBuilder(builder)) {
|
|
414
|
+
throw new OrmDefinitionError(
|
|
415
|
+
`view "${name}": column "${propertyName}" is not a column builder \u2014 use the t.* factories`,
|
|
416
|
+
file
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
const columnName = builder.columnNameFor(propertyName);
|
|
420
|
+
if (!IDENTIFIER_PATTERN.test(columnName) || columnName.length > MAX_IDENTIFIER_LENGTH) {
|
|
421
|
+
throw new OrmDefinitionError(`view "${name}": column "${propertyName}" has invalid column name "${columnName}"`, file);
|
|
422
|
+
}
|
|
423
|
+
const existing = seenColumnNames.get(columnName);
|
|
424
|
+
if (existing !== void 0) {
|
|
425
|
+
throw new OrmDefinitionError(
|
|
426
|
+
`view "${name}": columns "${existing}" and "${propertyName}" both map to Postgres column "${columnName}"`,
|
|
427
|
+
file
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
seenColumnNames.set(columnName, propertyName);
|
|
431
|
+
}
|
|
432
|
+
const view = { kind: VIEW_BRAND, viewName: name, columns, config, file };
|
|
433
|
+
registerView(view);
|
|
434
|
+
return view;
|
|
435
|
+
}
|
|
436
|
+
var QUERY_COLUMN_FACTORIES = {
|
|
437
|
+
uuid: (name) => pgCore.uuid(name),
|
|
438
|
+
text: (name) => pgCore.text(name),
|
|
439
|
+
integer: (name) => pgCore.integer(name),
|
|
440
|
+
bigint: (name) => pgCore.bigint(name, { mode: "bigint" }),
|
|
441
|
+
numeric: (name) => pgCore.numeric(name),
|
|
442
|
+
double: (name) => pgCore.doublePrecision(name),
|
|
443
|
+
boolean: (name) => pgCore.boolean(name),
|
|
444
|
+
timestamptz: (name) => pgCore.timestamp(name, { withTimezone: true }),
|
|
445
|
+
date: (name) => pgCore.date(name),
|
|
446
|
+
jsonb: (name) => pgCore.jsonb(name)
|
|
447
|
+
};
|
|
448
|
+
function compileViewQueries(input) {
|
|
449
|
+
const schemaName = `orm_${input.namespace}`;
|
|
450
|
+
const schema = pgCore.pgSchema(schemaName);
|
|
451
|
+
const enumFactories = /* @__PURE__ */ new Map();
|
|
452
|
+
for (const entry of input.enums) {
|
|
453
|
+
const values = entry.values;
|
|
454
|
+
enumFactories.set(entry.name, schema.enum(entry.name, values));
|
|
455
|
+
}
|
|
456
|
+
const handles = /* @__PURE__ */ new Map();
|
|
457
|
+
const physicalToLogical = /* @__PURE__ */ new Map();
|
|
458
|
+
const tableByName = new Map(input.tables.map((table) => [table.table, table]));
|
|
459
|
+
for (const authored of input.authoredTables) {
|
|
460
|
+
const canonical = tableByName.get(authored.tableName);
|
|
461
|
+
if (!canonical) continue;
|
|
462
|
+
const physicalSchema = schemaName;
|
|
463
|
+
const physicalName = canonical.table;
|
|
464
|
+
const relationSchema = pgCore.pgSchema(physicalSchema);
|
|
465
|
+
const columns = buildQueryColumns(canonical.columns, enumFactories);
|
|
466
|
+
const handle = relationSchema.table(physicalName, columns);
|
|
467
|
+
handles.set(authored, handle);
|
|
468
|
+
physicalToLogical.set(`${physicalSchema}.${physicalName}`, canonical.table);
|
|
469
|
+
}
|
|
470
|
+
const viewByName = new Map(input.views.map((view) => [view.view, view]));
|
|
471
|
+
for (const authored of input.authoredViews) {
|
|
472
|
+
const canonical = viewByName.get(authored.viewName);
|
|
473
|
+
if (!canonical) continue;
|
|
474
|
+
const columns = buildQueryColumns(canonical.columns, enumFactories);
|
|
475
|
+
const handle = schema.view(canonical.view, columns).existing();
|
|
476
|
+
handles.set(authored, handle);
|
|
477
|
+
physicalToLogical.set(`${schemaName}.${canonical.view}`, canonical.view);
|
|
478
|
+
}
|
|
479
|
+
const dialect = new pgCore.PgDialect();
|
|
480
|
+
const compiled = /* @__PURE__ */ new Map();
|
|
481
|
+
for (const authored of input.authoredViews) {
|
|
482
|
+
const expectedFields = Object.keys(authored.columns);
|
|
483
|
+
const declaredDependencies = /* @__PURE__ */ new Set();
|
|
484
|
+
const relation = (ref) => {
|
|
485
|
+
let resolved;
|
|
486
|
+
try {
|
|
487
|
+
resolved = typeof ref === "function" ? ref() : ref;
|
|
488
|
+
} catch (error) {
|
|
489
|
+
throw new OrmDefinitionError(
|
|
490
|
+
`view "${authored.viewName}" relation thunk threw: ${error instanceof Error ? error.message : String(error)}`,
|
|
491
|
+
authored.file
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
if (!isSeqTable(resolved) && !isSeqView(resolved)) {
|
|
495
|
+
throw new OrmDefinitionError(
|
|
496
|
+
`view "${authored.viewName}" relation() expects a table or view defined by this ORM`,
|
|
497
|
+
authored.file
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
const handle = handles.get(resolved);
|
|
501
|
+
if (!handle) {
|
|
502
|
+
const name = isSeqTable(resolved) ? resolved.tableName : resolved.viewName;
|
|
503
|
+
throw new OrmDefinitionError(
|
|
504
|
+
`view "${authored.viewName}" references relation "${name}" which is not part of this namespace`,
|
|
505
|
+
authored.file
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
declaredDependencies.add(isSeqTable(resolved) ? resolved.tableName : resolved.viewName);
|
|
509
|
+
return handle;
|
|
510
|
+
};
|
|
511
|
+
let queryBuilder;
|
|
512
|
+
try {
|
|
513
|
+
queryBuilder = authored.config.query({ qb: new pgCore.QueryBuilder(), relation });
|
|
514
|
+
} catch (error) {
|
|
515
|
+
if (error instanceof OrmDefinitionError) throw error;
|
|
516
|
+
throw new OrmDefinitionError(
|
|
517
|
+
`view "${authored.viewName}" query callback failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
518
|
+
authored.file
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
let config;
|
|
522
|
+
try {
|
|
523
|
+
config = pgCore.getViewConfig(schema.view(authored.viewName).as(queryBuilder));
|
|
524
|
+
} catch (error) {
|
|
525
|
+
throw new OrmDefinitionError(
|
|
526
|
+
`view "${authored.viewName}" query must return a Drizzle select builder: ${error instanceof Error ? error.message : String(error)}`,
|
|
527
|
+
authored.file
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
const selectedFields = Object.keys(config.selectedFields);
|
|
531
|
+
if (selectedFields.length !== expectedFields.length || selectedFields.some((field, index3) => field !== expectedFields[index3])) {
|
|
532
|
+
throw new OrmDefinitionError(
|
|
533
|
+
`view "${authored.viewName}" query selects [${selectedFields.join(", ")}], but declared columns are [${expectedFields.join(", ")}]`,
|
|
534
|
+
authored.file
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
if (!config.query) throw new OrmDefinitionError(`view "${authored.viewName}" produced no query`, authored.file);
|
|
538
|
+
const tableDependencies = pgCore.extractUsedTable(config.query).map((physical) => {
|
|
539
|
+
const logical = physicalToLogical.get(physical);
|
|
540
|
+
if (!logical) {
|
|
541
|
+
throw new OrmDefinitionError(
|
|
542
|
+
`view "${authored.viewName}" query references unmanaged relation "${physical}" \u2014 use relation(...) with a namespace table or view`,
|
|
543
|
+
authored.file
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
return logical;
|
|
547
|
+
});
|
|
548
|
+
const renderedQuery = dialect.sqlToQuery(config.query.inlineParams());
|
|
549
|
+
if (renderedQuery.params.length > 0) {
|
|
550
|
+
throw new OrmDefinitionError(
|
|
551
|
+
`view "${authored.viewName}" query produced bind parameters which cannot be persisted in view DDL`,
|
|
552
|
+
authored.file
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
compiled.set(authored.viewName, {
|
|
556
|
+
query: renderedQuery.sql,
|
|
557
|
+
dependencies: [.../* @__PURE__ */ new Set([...declaredDependencies, ...tableDependencies])].sort()
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
return compiled;
|
|
561
|
+
}
|
|
562
|
+
function buildQueryColumns(columns, enumFactories) {
|
|
563
|
+
return Object.fromEntries(columns.map((column) => [column.name, buildQueryColumn(column, enumFactories)]));
|
|
564
|
+
}
|
|
565
|
+
function buildQueryColumn(column, enumFactories) {
|
|
566
|
+
const factory = typeof column.type === "string" ? QUERY_COLUMN_FACTORIES[column.type] : enumFactories.get(column.type.enum);
|
|
567
|
+
if (!factory) {
|
|
568
|
+
const type = typeof column.type === "string" ? column.type : column.type.enum;
|
|
569
|
+
throw new OrmDefinitionError(`view query column '${column.columnName}' references unknown type '${type}'`);
|
|
570
|
+
}
|
|
571
|
+
let builder = factory(column.columnName);
|
|
572
|
+
if (!column.nullable) builder = builder.notNull();
|
|
573
|
+
return builder;
|
|
574
|
+
}
|
|
575
|
+
function relationName(relation) {
|
|
576
|
+
return isSeqTable(relation) ? relation.tableName : relation.viewName;
|
|
577
|
+
}
|
|
578
|
+
function relationKind(relation) {
|
|
579
|
+
return isSeqTable(relation) ? "table" : "view";
|
|
580
|
+
}
|
|
581
|
+
function declaredPrimaryKeyProperties(table) {
|
|
582
|
+
const fromColumns = Object.entries(table.columns).filter(([, builder]) => builder.isPrimaryKey).map(([propertyName]) => propertyName);
|
|
583
|
+
if (fromColumns.length > 0) return fromColumns;
|
|
584
|
+
const configPk = table.config.primaryKey ?? [];
|
|
585
|
+
const resolved = [];
|
|
586
|
+
for (const ref of configPk) {
|
|
587
|
+
if (ref in table.columns) {
|
|
588
|
+
resolved.push(ref);
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
const byColumnName = Object.entries(table.columns).find(
|
|
592
|
+
([propertyName, builder]) => builder.columnNameFor(propertyName) === ref
|
|
593
|
+
);
|
|
594
|
+
if (byColumnName) resolved.push(byColumnName[0]);
|
|
595
|
+
}
|
|
596
|
+
return resolved;
|
|
597
|
+
}
|
|
598
|
+
function assertOverlayWinsPrimaryKey({
|
|
599
|
+
table,
|
|
600
|
+
pk,
|
|
601
|
+
role
|
|
602
|
+
}) {
|
|
603
|
+
if (!(pk in table.columns)) {
|
|
604
|
+
throw new OrmDefinitionError(
|
|
605
|
+
`overlayWinsUnion: pk "${pk}" must exist as a property on both overlay and synced tables`
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
const declared = declaredPrimaryKeyProperties(table);
|
|
609
|
+
if (declared.length !== 1 || declared[0] !== pk) {
|
|
610
|
+
throw new OrmDefinitionError(
|
|
611
|
+
`overlayWinsUnion: ${role} primary key must be exactly "${pk}" (declared: ${declared.length > 0 ? declared.join(", ") : "none"})`
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
function formatCanonicalColumnType(type) {
|
|
616
|
+
return typeof type === "string" ? type : `enum(${type.enum})`;
|
|
617
|
+
}
|
|
618
|
+
function assertOverlayWinsMatchingColumnTypes({
|
|
619
|
+
overlay,
|
|
620
|
+
synced,
|
|
621
|
+
columns
|
|
622
|
+
}) {
|
|
623
|
+
for (const column of columns) {
|
|
624
|
+
const overlayType = overlay.columns[column].toCanonicalColumn(column).type;
|
|
625
|
+
const syncedType = synced.columns[column].toCanonicalColumn(column).type;
|
|
626
|
+
if (JSON.stringify(overlayType) !== JSON.stringify(syncedType)) {
|
|
627
|
+
throw new OrmDefinitionError(
|
|
628
|
+
`overlayWinsUnion: column "${column}" must have the same type on overlay and synced (overlay: ${formatCanonicalColumnType(overlayType)}, synced: ${formatCanonicalColumnType(syncedType)})`
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
function overlayWinsUnion(input) {
|
|
634
|
+
const { overlay, synced, pk, tombstone } = input;
|
|
635
|
+
return ({ qb, relation }) => {
|
|
636
|
+
assertOverlayWinsPrimaryKey({ table: overlay, pk, role: "overlay" });
|
|
637
|
+
assertOverlayWinsPrimaryKey({ table: synced, pk, role: "synced" });
|
|
638
|
+
const overlayHandle = relation(overlay);
|
|
639
|
+
const syncedHandle = relation(synced);
|
|
640
|
+
const overlayCols = overlayHandle;
|
|
641
|
+
const syncedCols = syncedHandle;
|
|
642
|
+
const overlayPk = overlayCols[pk];
|
|
643
|
+
const syncedPk = syncedCols[pk];
|
|
644
|
+
if (tombstone !== void 0) {
|
|
645
|
+
const tombstoneBuilder = overlay.columns[tombstone];
|
|
646
|
+
if (!tombstoneBuilder) {
|
|
647
|
+
throw new OrmDefinitionError(
|
|
648
|
+
`overlayWinsUnion: tombstone "${tombstone}" must exist as a property on the overlay table`
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
const tombstoneCanonical = tombstoneBuilder.toCanonicalColumn(tombstone);
|
|
652
|
+
if (tombstoneCanonical.type !== "boolean") {
|
|
653
|
+
throw new OrmDefinitionError(
|
|
654
|
+
`overlayWinsUnion: tombstone "${tombstone}" must be a boolean column`
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
if (tombstoneCanonical.nullable) {
|
|
658
|
+
throw new OrmDefinitionError(
|
|
659
|
+
`overlayWinsUnion: tombstone "${tombstone}" must be notNull \u2014 nullable tombstones silently drop rows`
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
const overlayColumns = Object.keys(overlay.columns);
|
|
664
|
+
const syncedColumns = Object.keys(synced.columns);
|
|
665
|
+
const sharedColumns = overlayColumns.filter((col) => syncedColumns.includes(col) && col !== pk);
|
|
666
|
+
assertOverlayWinsMatchingColumnTypes({
|
|
667
|
+
overlay,
|
|
668
|
+
synced,
|
|
669
|
+
columns: [pk, ...sharedColumns]
|
|
670
|
+
});
|
|
671
|
+
const overlaySelect = {
|
|
672
|
+
[pk]: overlayPk,
|
|
673
|
+
...Object.fromEntries(sharedColumns.map((col) => [col, overlayCols[col]])),
|
|
674
|
+
provenance: drizzleOrm.sql`'overlay'`.as("provenance")
|
|
675
|
+
};
|
|
676
|
+
const syncedSelect = {
|
|
677
|
+
[pk]: syncedPk,
|
|
678
|
+
...Object.fromEntries(sharedColumns.map((col) => [col, syncedCols[col]])),
|
|
679
|
+
provenance: drizzleOrm.sql`'synced'`.as("provenance")
|
|
680
|
+
};
|
|
681
|
+
const overlayFrom = qb.select(overlaySelect).from(overlayHandle);
|
|
682
|
+
const overlayQuery = tombstone !== void 0 ? overlayFrom.where(drizzleOrm.eq(overlayCols[tombstone], false)) : overlayFrom;
|
|
683
|
+
const syncedQuery = qb.select(syncedSelect).from(syncedHandle).where(
|
|
684
|
+
drizzleOrm.notExists(
|
|
685
|
+
qb.select({ one: drizzleOrm.sql`1` }).from(overlayHandle).where(drizzleOrm.eq(overlayPk, syncedPk))
|
|
686
|
+
)
|
|
687
|
+
);
|
|
688
|
+
return overlayQuery.unionAll(syncedQuery);
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// src/authoring/cow-mutations.ts
|
|
693
|
+
function overrideRowMutation(input) {
|
|
694
|
+
const { overlay, synced, pk } = input;
|
|
695
|
+
const name = input.name ?? `override_${overlay.tableName}`;
|
|
696
|
+
assertMutationName(name, "overrideRowMutation");
|
|
697
|
+
assertOverlayPairPrimaryKey({ overlay, synced, pk, helper: "overrideRowMutation" });
|
|
698
|
+
const sharedProperties = sharedNonPkProperties({ overlay, synced, pk });
|
|
699
|
+
const setProperties = resolveSetProperties({
|
|
700
|
+
overlay,
|
|
701
|
+
synced,
|
|
702
|
+
pk,
|
|
703
|
+
set: input.set,
|
|
704
|
+
sharedProperties,
|
|
705
|
+
helper: "overrideRowMutation"
|
|
706
|
+
});
|
|
707
|
+
assertMatchingColumns({
|
|
708
|
+
overlay,
|
|
709
|
+
synced,
|
|
710
|
+
properties: [pk, ...sharedProperties],
|
|
711
|
+
helper: "overrideRowMutation"
|
|
712
|
+
});
|
|
713
|
+
return {
|
|
714
|
+
kind: "override",
|
|
715
|
+
name,
|
|
716
|
+
synced: synced.tableName,
|
|
717
|
+
pk,
|
|
718
|
+
copy: [pk, ...sharedProperties],
|
|
719
|
+
set: setProperties
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function revertOverlayMutation(input) {
|
|
723
|
+
const { overlay, synced, pk } = input;
|
|
724
|
+
const name = input.name ?? `revert_${overlay.tableName}`;
|
|
725
|
+
assertMutationName(name, "revertOverlayMutation");
|
|
726
|
+
assertOverlayPairPrimaryKey({ overlay, synced, pk, helper: "revertOverlayMutation" });
|
|
727
|
+
assertMatchingColumns({ overlay, synced, properties: [pk], helper: "revertOverlayMutation" });
|
|
728
|
+
return {
|
|
729
|
+
kind: "revert",
|
|
730
|
+
name,
|
|
731
|
+
synced: synced.tableName,
|
|
732
|
+
pk,
|
|
733
|
+
copy: [],
|
|
734
|
+
set: []
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function assertMutationName(name, helper) {
|
|
738
|
+
if (!IDENTIFIER_PATTERN.test(name) || name.length > MAX_IDENTIFIER_LENGTH) {
|
|
739
|
+
throw new OrmDefinitionError(
|
|
740
|
+
`${helper}: mutation name "${name}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
function assertOverlayPairPrimaryKey({
|
|
745
|
+
overlay,
|
|
746
|
+
synced,
|
|
747
|
+
pk,
|
|
748
|
+
helper
|
|
749
|
+
}) {
|
|
750
|
+
assertSinglePrimaryKey({ table: overlay, pk, role: "overlay", helper });
|
|
751
|
+
assertSinglePrimaryKey({ table: synced, pk, role: "synced", helper });
|
|
752
|
+
}
|
|
753
|
+
function assertSinglePrimaryKey({
|
|
754
|
+
table,
|
|
755
|
+
pk,
|
|
756
|
+
role,
|
|
757
|
+
helper
|
|
758
|
+
}) {
|
|
759
|
+
if (!(pk in table.columns)) {
|
|
760
|
+
throw new OrmDefinitionError(`${helper}: pk "${pk}" must exist as a property on the ${role} table`);
|
|
761
|
+
}
|
|
762
|
+
const declared = declaredPrimaryKeyProperties2(table);
|
|
763
|
+
if (declared.length !== 1 || declared[0] !== pk) {
|
|
764
|
+
throw new OrmDefinitionError(
|
|
765
|
+
`${helper}: ${role} primary key must be exactly "${pk}" (declared: ${declared.length > 0 ? declared.join(", ") : "none"})`
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
function declaredPrimaryKeyProperties2(table) {
|
|
770
|
+
const fromColumns = Object.entries(table.columns).filter(([, builder]) => builder.isPrimaryKey).map(([propertyName]) => propertyName);
|
|
771
|
+
if (fromColumns.length > 0) return fromColumns;
|
|
772
|
+
const configPk = table.config?.primaryKey ?? [];
|
|
773
|
+
const resolved = [];
|
|
774
|
+
for (const ref of configPk) {
|
|
775
|
+
if (ref in table.columns) {
|
|
776
|
+
resolved.push(ref);
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
const byColumnName = Object.entries(table.columns).find(
|
|
780
|
+
([propertyName, builder]) => builder.columnNameFor(propertyName) === ref
|
|
781
|
+
);
|
|
782
|
+
if (byColumnName) resolved.push(byColumnName[0]);
|
|
783
|
+
}
|
|
784
|
+
return resolved;
|
|
785
|
+
}
|
|
786
|
+
function sharedNonPkProperties({
|
|
787
|
+
overlay,
|
|
788
|
+
synced,
|
|
789
|
+
pk
|
|
790
|
+
}) {
|
|
791
|
+
return Object.keys(overlay.columns).filter((property) => property !== pk && property in synced.columns);
|
|
792
|
+
}
|
|
793
|
+
function resolveSetProperties({
|
|
794
|
+
overlay,
|
|
795
|
+
synced,
|
|
796
|
+
pk,
|
|
797
|
+
set,
|
|
798
|
+
sharedProperties,
|
|
799
|
+
helper
|
|
800
|
+
}) {
|
|
801
|
+
const properties = set !== void 0 ? [...set] : [...sharedProperties];
|
|
802
|
+
if (properties.length === 0) {
|
|
803
|
+
throw new OrmDefinitionError(
|
|
804
|
+
`${helper}: set must declare at least one non-PK column to update after the copy`
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
const seen = /* @__PURE__ */ new Set();
|
|
808
|
+
for (const property of properties) {
|
|
809
|
+
if (property === pk) {
|
|
810
|
+
throw new OrmDefinitionError(`${helper}: set must not include pk "${pk}" \u2014 identity columns are pinned`);
|
|
811
|
+
}
|
|
812
|
+
if (!(property in overlay.columns)) {
|
|
813
|
+
throw new OrmDefinitionError(`${helper}: set column "${property}" must exist on the overlay table`);
|
|
814
|
+
}
|
|
815
|
+
if (!(property in synced.columns)) {
|
|
816
|
+
throw new OrmDefinitionError(
|
|
817
|
+
`${helper}: set column "${property}" must exist on the synced table (contract columns only)`
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
if (seen.has(property)) {
|
|
821
|
+
throw new OrmDefinitionError(`${helper}: set column "${property}" is duplicated`);
|
|
822
|
+
}
|
|
823
|
+
seen.add(property);
|
|
824
|
+
}
|
|
825
|
+
return properties;
|
|
826
|
+
}
|
|
827
|
+
function assertMatchingColumns({
|
|
828
|
+
overlay,
|
|
829
|
+
synced,
|
|
830
|
+
properties,
|
|
831
|
+
helper
|
|
832
|
+
}) {
|
|
833
|
+
for (const property of properties) {
|
|
834
|
+
const overlayCanonical = overlay.columns[property].toCanonicalColumn(property);
|
|
835
|
+
const syncedCanonical = synced.columns[property].toCanonicalColumn(property);
|
|
836
|
+
const overlayType = scalarColumnType(overlay, property, helper);
|
|
837
|
+
const syncedType = scalarColumnType(synced, property, helper);
|
|
838
|
+
if (overlayType !== syncedType) {
|
|
839
|
+
throw new OrmDefinitionError(
|
|
840
|
+
`${helper}: column "${property}" must have the same scalar type on overlay and synced (overlay: ${overlayType}, synced: ${syncedType})`
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
const overlayColumn = overlay.columns[property].columnNameFor(property);
|
|
844
|
+
const syncedColumn = synced.columns[property].columnNameFor(property);
|
|
845
|
+
if (overlayColumn !== syncedColumn) {
|
|
846
|
+
throw new OrmDefinitionError(
|
|
847
|
+
`${helper}: column "${property}" maps to different Postgres names on overlay (${overlayColumn}) and synced (${syncedColumn})`
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
if (syncedCanonical.nullable && !overlayCanonical.nullable) {
|
|
851
|
+
throw new OrmDefinitionError(
|
|
852
|
+
`${helper}: column "${property}" is nullable on synced but NOT NULL on overlay \u2014 a NULL synced value would fail the copy-from-synced insert; make overlay nullable or synced notNull`
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
function scalarColumnType(table, property, helper) {
|
|
858
|
+
const type = table.columns[property].toCanonicalColumn(property).type;
|
|
859
|
+
if (typeof type !== "string") {
|
|
860
|
+
throw new OrmDefinitionError(
|
|
861
|
+
`${helper}: column "${property}" on "${table.tableName}" must be a scalar type (enums are not supported in cow mutation arguments)`
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
return type;
|
|
865
|
+
}
|
|
345
866
|
var CANONICAL_FORMAT = 2;
|
|
346
867
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
347
868
|
var identifier = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
@@ -414,8 +935,190 @@ var policyExpressionSchema = zod.z.union([
|
|
|
414
935
|
]);
|
|
415
936
|
var canonicalPoliciesSchema = zod.z.object({
|
|
416
937
|
read: policyExpressionSchema.optional(),
|
|
417
|
-
write: policyExpressionSchema.optional()
|
|
938
|
+
write: policyExpressionSchema.optional(),
|
|
939
|
+
insert: policyExpressionSchema.optional(),
|
|
940
|
+
update: policyExpressionSchema.optional()
|
|
418
941
|
});
|
|
942
|
+
function isSingleViewQuerySql(value) {
|
|
943
|
+
const query = value.trim();
|
|
944
|
+
if (!/^\(*(?:select|with)\b/i.test(query) || query.includes("\0")) return false;
|
|
945
|
+
let state = "normal";
|
|
946
|
+
for (let index3 = 0; index3 < query.length; index3 += 1) {
|
|
947
|
+
const char = query[index3];
|
|
948
|
+
const next = query[index3 + 1];
|
|
949
|
+
if (state === "single") {
|
|
950
|
+
if (char === "'" && next === "'") index3 += 1;
|
|
951
|
+
else if (char === "'") state = "normal";
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
if (state === "double") {
|
|
955
|
+
if (char === '"' && next === '"') index3 += 1;
|
|
956
|
+
else if (char === '"') state = "normal";
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
if (char === "'") state = "single";
|
|
960
|
+
else if (char === '"') state = "double";
|
|
961
|
+
else if (char === ";" || char === "-" && next === "-" || char === "/" && next === "*") return false;
|
|
962
|
+
}
|
|
963
|
+
return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
|
|
964
|
+
}
|
|
965
|
+
var VIEW_QUERY_FUNCTION_ALLOWLIST = [
|
|
966
|
+
"abs",
|
|
967
|
+
"age",
|
|
968
|
+
"array_agg",
|
|
969
|
+
"array_append",
|
|
970
|
+
"array_cat",
|
|
971
|
+
"array_dims",
|
|
972
|
+
"array_length",
|
|
973
|
+
"array_lower",
|
|
974
|
+
"array_ndims",
|
|
975
|
+
"array_position",
|
|
976
|
+
"array_positions",
|
|
977
|
+
"array_prepend",
|
|
978
|
+
"array_remove",
|
|
979
|
+
"array_replace",
|
|
980
|
+
"array_to_string",
|
|
981
|
+
"array_trim",
|
|
982
|
+
"array_upper",
|
|
983
|
+
"ascii",
|
|
984
|
+
"avg",
|
|
985
|
+
"bit_and",
|
|
986
|
+
"bit_or",
|
|
987
|
+
"bit_xor",
|
|
988
|
+
"bool_and",
|
|
989
|
+
"bool_or",
|
|
990
|
+
"btrim",
|
|
991
|
+
"cardinality",
|
|
992
|
+
"cbrt",
|
|
993
|
+
"ceil",
|
|
994
|
+
"ceiling",
|
|
995
|
+
"char_length",
|
|
996
|
+
"character_length",
|
|
997
|
+
"chr",
|
|
998
|
+
"concat",
|
|
999
|
+
"concat_ws",
|
|
1000
|
+
"count",
|
|
1001
|
+
"cume_dist",
|
|
1002
|
+
"date_bin",
|
|
1003
|
+
"date_part",
|
|
1004
|
+
"date_trunc",
|
|
1005
|
+
"decode",
|
|
1006
|
+
"degrees",
|
|
1007
|
+
"dense_rank",
|
|
1008
|
+
"encode",
|
|
1009
|
+
"enum_first",
|
|
1010
|
+
"enum_last",
|
|
1011
|
+
"enum_range",
|
|
1012
|
+
"every",
|
|
1013
|
+
"exp",
|
|
1014
|
+
"first_value",
|
|
1015
|
+
"floor",
|
|
1016
|
+
"format",
|
|
1017
|
+
"grouping",
|
|
1018
|
+
"initcap",
|
|
1019
|
+
"json_agg",
|
|
1020
|
+
"json_array_length",
|
|
1021
|
+
"json_build_array",
|
|
1022
|
+
"json_build_object",
|
|
1023
|
+
"json_extract_path",
|
|
1024
|
+
"json_extract_path_text",
|
|
1025
|
+
"json_object_agg",
|
|
1026
|
+
"json_object_keys",
|
|
1027
|
+
"json_strip_nulls",
|
|
1028
|
+
"json_typeof",
|
|
1029
|
+
"jsonb_agg",
|
|
1030
|
+
"jsonb_array_length",
|
|
1031
|
+
"jsonb_build_array",
|
|
1032
|
+
"jsonb_build_object",
|
|
1033
|
+
"jsonb_extract_path",
|
|
1034
|
+
"jsonb_extract_path_text",
|
|
1035
|
+
"jsonb_insert",
|
|
1036
|
+
"jsonb_object_agg",
|
|
1037
|
+
"jsonb_object_keys",
|
|
1038
|
+
"jsonb_path_exists",
|
|
1039
|
+
"jsonb_path_match",
|
|
1040
|
+
"jsonb_path_query",
|
|
1041
|
+
"jsonb_path_query_array",
|
|
1042
|
+
"jsonb_path_query_first",
|
|
1043
|
+
"jsonb_pretty",
|
|
1044
|
+
"jsonb_set",
|
|
1045
|
+
"jsonb_set_lax",
|
|
1046
|
+
"jsonb_strip_nulls",
|
|
1047
|
+
"jsonb_typeof",
|
|
1048
|
+
"justify_days",
|
|
1049
|
+
"justify_hours",
|
|
1050
|
+
"justify_interval",
|
|
1051
|
+
"lag",
|
|
1052
|
+
"last_value",
|
|
1053
|
+
"lead",
|
|
1054
|
+
"left",
|
|
1055
|
+
"length",
|
|
1056
|
+
"ln",
|
|
1057
|
+
"log",
|
|
1058
|
+
"lower",
|
|
1059
|
+
"lpad",
|
|
1060
|
+
"ltrim",
|
|
1061
|
+
"make_date",
|
|
1062
|
+
"make_interval",
|
|
1063
|
+
"make_time",
|
|
1064
|
+
"make_timestamp",
|
|
1065
|
+
"make_timestamptz",
|
|
1066
|
+
"max",
|
|
1067
|
+
"md5",
|
|
1068
|
+
"min",
|
|
1069
|
+
"mod",
|
|
1070
|
+
"nth_value",
|
|
1071
|
+
"ntile",
|
|
1072
|
+
"octet_length",
|
|
1073
|
+
"percent_rank",
|
|
1074
|
+
"pi",
|
|
1075
|
+
"power",
|
|
1076
|
+
"quote_ident",
|
|
1077
|
+
"quote_literal",
|
|
1078
|
+
"quote_nullable",
|
|
1079
|
+
"radians",
|
|
1080
|
+
"rank",
|
|
1081
|
+
"regexp_count",
|
|
1082
|
+
"regexp_instr",
|
|
1083
|
+
"regexp_like",
|
|
1084
|
+
"regexp_match",
|
|
1085
|
+
"regexp_matches",
|
|
1086
|
+
"regexp_replace",
|
|
1087
|
+
"regexp_split_to_array",
|
|
1088
|
+
"regexp_split_to_table",
|
|
1089
|
+
"repeat",
|
|
1090
|
+
"replace",
|
|
1091
|
+
"reverse",
|
|
1092
|
+
"right",
|
|
1093
|
+
"round",
|
|
1094
|
+
"row_number",
|
|
1095
|
+
"rpad",
|
|
1096
|
+
"rtrim",
|
|
1097
|
+
"sign",
|
|
1098
|
+
"split_part",
|
|
1099
|
+
"sqrt",
|
|
1100
|
+
"starts_with",
|
|
1101
|
+
"string_agg",
|
|
1102
|
+
"string_to_array",
|
|
1103
|
+
"strpos",
|
|
1104
|
+
"substr",
|
|
1105
|
+
"substring",
|
|
1106
|
+
"sum",
|
|
1107
|
+
"timezone",
|
|
1108
|
+
"to_ascii",
|
|
1109
|
+
"to_char",
|
|
1110
|
+
"to_date",
|
|
1111
|
+
"to_hex",
|
|
1112
|
+
"to_json",
|
|
1113
|
+
"to_jsonb",
|
|
1114
|
+
"to_number",
|
|
1115
|
+
"to_timestamp",
|
|
1116
|
+
"translate",
|
|
1117
|
+
"trunc",
|
|
1118
|
+
"unnest",
|
|
1119
|
+
"upper",
|
|
1120
|
+
"width_bucket"
|
|
1121
|
+
];
|
|
419
1122
|
var canonicalActionSchema = zod.z.object({
|
|
420
1123
|
name: identifier,
|
|
421
1124
|
requires: zod.z.string().max(1e3).optional(),
|
|
@@ -427,6 +1130,37 @@ var canonicalActionSchema = zod.z.object({
|
|
|
427
1130
|
returns: zod.z.enum(["rows", "rowsAffected", "scalar"]).default("rowsAffected"),
|
|
428
1131
|
audit: zod.z.object({ action: zod.z.string(), resource: zod.z.string() }).optional()
|
|
429
1132
|
});
|
|
1133
|
+
var canonicalCowMutationSchema = zod.z.object({
|
|
1134
|
+
kind: zod.z.enum(["override", "revert"]),
|
|
1135
|
+
name: identifier,
|
|
1136
|
+
/** Synced table (same namespace) the overlay copies from / falls back to. */
|
|
1137
|
+
synced: identifier,
|
|
1138
|
+
/** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
|
|
1139
|
+
pk: zod.z.string().min(1).max(63),
|
|
1140
|
+
/** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
|
|
1141
|
+
copy: zod.z.array(zod.z.string().min(1).max(63)).default([]),
|
|
1142
|
+
/** Override: overlay properties written by the UPDATE (never the pk). Empty for revert. */
|
|
1143
|
+
set: zod.z.array(zod.z.string().min(1).max(63)).default([])
|
|
1144
|
+
}).superRefine((mutation, ctx) => {
|
|
1145
|
+
if (mutation.kind === "override") {
|
|
1146
|
+
if (mutation.set.length === 0) {
|
|
1147
|
+
ctx.addIssue({ code: "custom", path: ["set"], message: "override mutations must set at least one column" });
|
|
1148
|
+
}
|
|
1149
|
+
if (!mutation.copy.includes(mutation.pk)) {
|
|
1150
|
+
ctx.addIssue({ code: "custom", path: ["copy"], message: "override copy list must include the pk" });
|
|
1151
|
+
}
|
|
1152
|
+
} else if (mutation.set.length > 0 || mutation.copy.length > 0) {
|
|
1153
|
+
ctx.addIssue({ code: "custom", path: ["set"], message: "revert mutations take no copy/set columns" });
|
|
1154
|
+
}
|
|
1155
|
+
if (mutation.set.includes(mutation.pk)) {
|
|
1156
|
+
ctx.addIssue({ code: "custom", path: ["set"], message: "set must not include the pk \u2014 identity columns are pinned" });
|
|
1157
|
+
}
|
|
1158
|
+
for (const [field, values] of [["copy", mutation.copy], ["set", mutation.set]]) {
|
|
1159
|
+
if (new Set(values).size !== values.length) {
|
|
1160
|
+
ctx.addIssue({ code: "custom", path: [field], message: `${field} must not contain duplicate columns` });
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
});
|
|
430
1164
|
var canonicalTableSchema = zod.z.object({
|
|
431
1165
|
table: identifier,
|
|
432
1166
|
description: zod.z.string().max(2e3).optional(),
|
|
@@ -439,7 +1173,45 @@ var canonicalTableSchema = zod.z.object({
|
|
|
439
1173
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
440
1174
|
indexes: zod.z.array(canonicalIndexSchema).default([]),
|
|
441
1175
|
policies: canonicalPoliciesSchema.default({}),
|
|
442
|
-
|
|
1176
|
+
/**
|
|
1177
|
+
* v1-conversion carry-over only. v2 has no action surface — the runtime
|
|
1178
|
+
* neither compiles nor executes these; writes are mutations. Kept parseable
|
|
1179
|
+
* so stored definitions and their content hashes stay stable.
|
|
1180
|
+
*/
|
|
1181
|
+
actions: zod.z.array(canonicalActionSchema).default([]),
|
|
1182
|
+
/** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
|
|
1183
|
+
cowMutations: zod.z.array(canonicalCowMutationSchema).optional()
|
|
1184
|
+
});
|
|
1185
|
+
var canonicalViewSchema = zod.z.object({
|
|
1186
|
+
view: identifier,
|
|
1187
|
+
description: zod.z.string().max(2e3).optional(),
|
|
1188
|
+
columns: zod.z.array(canonicalColumnSchema).min(1),
|
|
1189
|
+
/** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
|
|
1190
|
+
primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
|
|
1191
|
+
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
1192
|
+
policies: zod.z.object({ read: policyExpressionSchema }).strict(),
|
|
1193
|
+
query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
|
|
1194
|
+
message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
|
|
1195
|
+
}),
|
|
1196
|
+
/** Namespace-local logical relation names, sorted for deterministic hashing. */
|
|
1197
|
+
dependencies: zod.z.array(identifier).max(500)
|
|
1198
|
+
}).superRefine((view, ctx) => {
|
|
1199
|
+
for (const [index3, column] of view.columns.entries()) {
|
|
1200
|
+
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1201
|
+
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare a default` });
|
|
1202
|
+
}
|
|
1203
|
+
if (column.unique) {
|
|
1204
|
+
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare unique` });
|
|
1205
|
+
}
|
|
1206
|
+
if (column.renamedFrom !== void 0 || column.cast !== void 0) {
|
|
1207
|
+
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare migration hints` });
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
for (const [index3, link] of view.links.entries()) {
|
|
1211
|
+
if (link.enforce) {
|
|
1212
|
+
ctx.addIssue({ code: "custom", path: ["links", index3, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
443
1215
|
});
|
|
444
1216
|
var canonicalDefinitionSchema = zod.z.object({
|
|
445
1217
|
definition_format: zod.z.literal(CANONICAL_FORMAT),
|
|
@@ -447,24 +1219,174 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
447
1219
|
description: zod.z.string().max(2e3).optional(),
|
|
448
1220
|
enums: zod.z.array(canonicalEnumSchema).default([]),
|
|
449
1221
|
tables: zod.z.array(canonicalTableSchema).min(0).max(500),
|
|
1222
|
+
/** Optional to preserve byte-identical hashes for pre-view canonical v2 documents. */
|
|
1223
|
+
views: zod.z.array(canonicalViewSchema).max(500).optional(),
|
|
450
1224
|
/** Verbatim custom-roles grammar (@sequenceholdings/custom-roles-manifest). */
|
|
451
1225
|
capabilities: zod.z.object({
|
|
452
1226
|
roles: zod.z.unknown().optional(),
|
|
453
1227
|
uses: zod.z.array(zod.z.string()).default([])
|
|
454
1228
|
}).default({ uses: [] })
|
|
1229
|
+
}).superRefine((definition, ctx) => {
|
|
1230
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1231
|
+
for (const [index3, table] of definition.tables.entries()) {
|
|
1232
|
+
if (seen.has(table.table)) {
|
|
1233
|
+
ctx.addIssue({ code: "custom", path: ["tables", index3, "table"], message: `duplicate relation '${table.table}'` });
|
|
1234
|
+
}
|
|
1235
|
+
seen.add(table.table);
|
|
1236
|
+
}
|
|
1237
|
+
const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
1238
|
+
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
1239
|
+
const cowMutations = table.cowMutations ?? [];
|
|
1240
|
+
if (cowMutations.length === 0) continue;
|
|
1241
|
+
const path = (index3, ...rest) => ["tables", tableIndex, "cowMutations", index3, ...rest];
|
|
1242
|
+
if (table.source !== void 0) {
|
|
1243
|
+
ctx.addIssue({ code: "custom", path: path(0), message: `synced table '${table.table}' cannot declare cow mutations \u2014 it is read-only` });
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
for (const [index3, mutation] of cowMutations.entries()) {
|
|
1247
|
+
const synced = tableByName.get(mutation.synced);
|
|
1248
|
+
if (!synced || synced.source === void 0) {
|
|
1249
|
+
ctx.addIssue({
|
|
1250
|
+
code: "custom",
|
|
1251
|
+
path: path(index3, "synced"),
|
|
1252
|
+
message: `cow mutation '${mutation.name}' references '${mutation.synced}', which is not a synced table in this namespace`
|
|
1253
|
+
});
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
const overlayColumnsByName = new Map(table.columns.map((column) => [column.name, column]));
|
|
1257
|
+
const syncedColumnsByName = new Map(synced.columns.map((column) => [column.name, column]));
|
|
1258
|
+
let columnsExist = true;
|
|
1259
|
+
for (const property of [mutation.pk, ...mutation.copy, ...mutation.set]) {
|
|
1260
|
+
if (!overlayColumnsByName.has(property)) {
|
|
1261
|
+
columnsExist = false;
|
|
1262
|
+
ctx.addIssue({ code: "custom", path: path(index3), message: `cow mutation '${mutation.name}': column '${property}' does not exist on overlay '${table.table}'` });
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
for (const property of [mutation.pk, ...mutation.copy]) {
|
|
1266
|
+
if (!syncedColumnsByName.has(property)) {
|
|
1267
|
+
columnsExist = false;
|
|
1268
|
+
ctx.addIssue({ code: "custom", path: path(index3), message: `cow mutation '${mutation.name}': column '${property}' does not exist on synced '${mutation.synced}'` });
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
if (!columnsExist) continue;
|
|
1272
|
+
const overlayPk = table.primaryKey ?? [];
|
|
1273
|
+
const syncedPk = synced.primaryKey ?? [];
|
|
1274
|
+
if (overlayPk.length !== 1 || overlayPk[0] !== mutation.pk || syncedPk.length !== 1 || syncedPk[0] !== mutation.pk) {
|
|
1275
|
+
ctx.addIssue({
|
|
1276
|
+
code: "custom",
|
|
1277
|
+
path: path(index3, "pk"),
|
|
1278
|
+
message: `cow mutation '${mutation.name}': pk '${mutation.pk}' must be the single-column primary key on both '${table.table}' and '${mutation.synced}'`
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
for (const property of [mutation.pk, ...mutation.copy]) {
|
|
1282
|
+
const overlayColumn = overlayColumnsByName.get(property);
|
|
1283
|
+
const syncedColumn = syncedColumnsByName.get(property);
|
|
1284
|
+
if (!overlayColumn || !syncedColumn) continue;
|
|
1285
|
+
if (JSON.stringify(overlayColumn.type) !== JSON.stringify(syncedColumn.type)) {
|
|
1286
|
+
ctx.addIssue({
|
|
1287
|
+
code: "custom",
|
|
1288
|
+
path: path(index3),
|
|
1289
|
+
message: `cow mutation '${mutation.name}': column '${property}' must have the same type on overlay and synced`
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
if (overlayColumn.columnName !== syncedColumn.columnName) {
|
|
1293
|
+
ctx.addIssue({
|
|
1294
|
+
code: "custom",
|
|
1295
|
+
path: path(index3),
|
|
1296
|
+
message: `cow mutation '${mutation.name}': column '${property}' maps to different Postgres names on overlay ('${overlayColumn.columnName}') and synced ('${syncedColumn.columnName}')`
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
if (syncedColumn.nullable !== false && overlayColumn.nullable === false) {
|
|
1300
|
+
ctx.addIssue({
|
|
1301
|
+
code: "custom",
|
|
1302
|
+
path: path(index3),
|
|
1303
|
+
message: `cow mutation '${mutation.name}': column '${property}' is nullable on synced but NOT NULL on overlay \u2014 a NULL synced value would fail the copy`
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
for (const [index3, view] of (definition.views ?? []).entries()) {
|
|
1310
|
+
if (seen.has(view.view)) {
|
|
1311
|
+
ctx.addIssue({ code: "custom", path: ["views", index3, "view"], message: `duplicate relation '${view.view}'` });
|
|
1312
|
+
}
|
|
1313
|
+
seen.add(view.view);
|
|
1314
|
+
}
|
|
1315
|
+
const views = definition.views ?? [];
|
|
1316
|
+
const viewByName = new Map(views.map((view) => [view.view, view]));
|
|
1317
|
+
for (const [viewIndex, view] of views.entries()) {
|
|
1318
|
+
for (const [dependencyIndex, dependency] of view.dependencies.entries()) {
|
|
1319
|
+
if (!seen.has(dependency)) {
|
|
1320
|
+
ctx.addIssue({
|
|
1321
|
+
code: "custom",
|
|
1322
|
+
path: ["views", viewIndex, "dependencies", dependencyIndex],
|
|
1323
|
+
message: `view '${view.view}' depends on unknown relation '${dependency}'`
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
1329
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1330
|
+
let cycleReported = false;
|
|
1331
|
+
const visit = (name, path) => {
|
|
1332
|
+
if (visited.has(name) || cycleReported) return;
|
|
1333
|
+
if (visiting.has(name)) {
|
|
1334
|
+
cycleReported = true;
|
|
1335
|
+
ctx.addIssue({
|
|
1336
|
+
code: "custom",
|
|
1337
|
+
path: ["views"],
|
|
1338
|
+
message: `view dependencies form a cycle: ${[...path, name].join(" -> ")}`
|
|
1339
|
+
});
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
visiting.add(name);
|
|
1343
|
+
const view = viewByName.get(name);
|
|
1344
|
+
for (const dependency of view?.dependencies ?? []) {
|
|
1345
|
+
if (viewByName.has(dependency)) visit(dependency, [...path, name]);
|
|
1346
|
+
}
|
|
1347
|
+
visiting.delete(name);
|
|
1348
|
+
visited.add(name);
|
|
1349
|
+
};
|
|
1350
|
+
for (const view of views) visit(view.view, []);
|
|
455
1351
|
});
|
|
1352
|
+
function canonicalRelations(definition) {
|
|
1353
|
+
const views = (definition.views ?? []).map((view) => ({
|
|
1354
|
+
table: view.view,
|
|
1355
|
+
...view.description !== void 0 ? { description: view.description } : {},
|
|
1356
|
+
columns: view.columns,
|
|
1357
|
+
...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
|
|
1358
|
+
links: view.links,
|
|
1359
|
+
indexes: [],
|
|
1360
|
+
policies: { read: view.policies.read },
|
|
1361
|
+
actions: []
|
|
1362
|
+
}));
|
|
1363
|
+
return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
|
|
1364
|
+
}
|
|
456
1365
|
function normalizeCanonical(definition) {
|
|
457
1366
|
const sortByName = (items) => [...items].sort((a, b) => a.name.localeCompare(b.name));
|
|
458
|
-
|
|
1367
|
+
const normalized = {
|
|
459
1368
|
...definition,
|
|
460
1369
|
enums: sortByName(definition.enums),
|
|
461
|
-
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) =>
|
|
462
|
-
...t2
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
1370
|
+
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
|
|
1371
|
+
const { cowMutations, ...rest } = t2;
|
|
1372
|
+
return {
|
|
1373
|
+
...rest,
|
|
1374
|
+
links: sortByName(t2.links),
|
|
1375
|
+
indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
|
|
1376
|
+
actions: sortByName(t2.actions),
|
|
1377
|
+
// Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
|
|
1378
|
+
...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
|
|
1379
|
+
};
|
|
1380
|
+
})
|
|
467
1381
|
};
|
|
1382
|
+
if (definition.views !== void 0) {
|
|
1383
|
+
normalized.views = [...definition.views].sort((a, b) => a.view.localeCompare(b.view)).map((view) => ({
|
|
1384
|
+
...view,
|
|
1385
|
+
links: sortByName(view.links),
|
|
1386
|
+
dependencies: [...view.dependencies].sort()
|
|
1387
|
+
}));
|
|
1388
|
+
}
|
|
1389
|
+
return normalized;
|
|
468
1390
|
}
|
|
469
1391
|
function stableJson(value) {
|
|
470
1392
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
@@ -486,9 +1408,13 @@ function canonicalTable(table) {
|
|
|
486
1408
|
source: table.source ?? null,
|
|
487
1409
|
links: emptyToNull(table.links),
|
|
488
1410
|
indexes: emptyToNull(table.indexes),
|
|
1411
|
+
// insert/update are omitted rather than nulled when absent, so definitions
|
|
1412
|
+
// predating split policies keep the hash they were registered under.
|
|
489
1413
|
policies: {
|
|
490
1414
|
read: table.policies?.read?.source ?? null,
|
|
491
|
-
write: table.policies?.write?.source ?? null
|
|
1415
|
+
write: table.policies?.write?.source ?? null,
|
|
1416
|
+
...table.policies?.insert !== void 0 ? { insert: table.policies.insert.source } : {},
|
|
1417
|
+
...table.policies?.update !== void 0 ? { update: table.policies.update.source } : {}
|
|
492
1418
|
},
|
|
493
1419
|
actions: emptyToNull(
|
|
494
1420
|
Object.fromEntries(
|
|
@@ -569,6 +1495,7 @@ function defineNamespace(input) {
|
|
|
569
1495
|
}
|
|
570
1496
|
function serializeNamespace(input) {
|
|
571
1497
|
const { config, tables } = input;
|
|
1498
|
+
const views = input.views ?? [];
|
|
572
1499
|
const enums = input.enums ?? [];
|
|
573
1500
|
const tablesByName = /* @__PURE__ */ new Map();
|
|
574
1501
|
for (const table of tables) {
|
|
@@ -581,6 +1508,29 @@ function serializeNamespace(input) {
|
|
|
581
1508
|
}
|
|
582
1509
|
tablesByName.set(table.tableName, table);
|
|
583
1510
|
}
|
|
1511
|
+
const viewsByName = /* @__PURE__ */ new Map();
|
|
1512
|
+
for (const view of views) {
|
|
1513
|
+
const existing = viewsByName.get(view.viewName);
|
|
1514
|
+
if (existing) {
|
|
1515
|
+
throw new OrmDefinitionError(
|
|
1516
|
+
`duplicate view "${view.viewName}" (defined in ${describeFile(existing)} and ${describeFile(view)})`,
|
|
1517
|
+
view.file
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
viewsByName.set(view.viewName, view);
|
|
1521
|
+
}
|
|
1522
|
+
const relationsByName = /* @__PURE__ */ new Map();
|
|
1523
|
+
for (const relation of [...tables, ...views]) {
|
|
1524
|
+
const name = relationName(relation);
|
|
1525
|
+
const existing = relationsByName.get(name);
|
|
1526
|
+
if (existing) {
|
|
1527
|
+
throw new OrmDefinitionError(
|
|
1528
|
+
`duplicate relation "${name}" (defined as ${relationKind(existing)} in ${describeFile(existing)} and ${relationKind(relation)} in ${describeFile(relation)})`,
|
|
1529
|
+
relation.file
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
relationsByName.set(name, relation);
|
|
1533
|
+
}
|
|
584
1534
|
const enumsByName = /* @__PURE__ */ new Map();
|
|
585
1535
|
for (const ref of enums) {
|
|
586
1536
|
if (enumsByName.has(ref.name)) {
|
|
@@ -588,11 +1538,26 @@ function serializeNamespace(input) {
|
|
|
588
1538
|
}
|
|
589
1539
|
enumsByName.set(ref.name, ref);
|
|
590
1540
|
}
|
|
591
|
-
const canonicalTables = tables.map((table) => buildCanonicalTable({ table,
|
|
1541
|
+
const canonicalTables = tables.map((table) => buildCanonicalTable({ table, relationsByName, enumsByName }));
|
|
592
1542
|
const canonicalEnums = [...enumsByName.values()].map((ref) => ({
|
|
593
1543
|
name: ref.name,
|
|
594
1544
|
values: [...ref.values]
|
|
595
1545
|
}));
|
|
1546
|
+
const viewDrafts = views.map((view) => buildCanonicalView({ view, relationsByName, enumsByName }));
|
|
1547
|
+
const compiledViews = compileViewQueries({
|
|
1548
|
+
namespace: config.namespace,
|
|
1549
|
+
tables: canonicalTables,
|
|
1550
|
+
views: viewDrafts,
|
|
1551
|
+
enums: canonicalEnums,
|
|
1552
|
+
authoredTables: tables,
|
|
1553
|
+
authoredViews: views
|
|
1554
|
+
});
|
|
1555
|
+
const canonicalViews = viewDrafts.map((draft) => {
|
|
1556
|
+
const compiled = compiledViews.get(draft.view);
|
|
1557
|
+
if (!compiled) throw new OrmDefinitionError(`view "${draft.view}" did not compile`, viewsByName.get(draft.view)?.file);
|
|
1558
|
+
return canonicalViewSchema.parse({ ...draft, ...compiled });
|
|
1559
|
+
});
|
|
1560
|
+
assertAcyclicViewDependencies(canonicalViews);
|
|
596
1561
|
const candidate = {
|
|
597
1562
|
definition_format: CANONICAL_FORMAT,
|
|
598
1563
|
namespace: config.namespace,
|
|
@@ -604,6 +1569,7 @@ function serializeNamespace(input) {
|
|
|
604
1569
|
}
|
|
605
1570
|
};
|
|
606
1571
|
if (config.description !== void 0) candidate.description = config.description;
|
|
1572
|
+
if (canonicalViews.length > 0) candidate.views = canonicalViews;
|
|
607
1573
|
let parsed;
|
|
608
1574
|
try {
|
|
609
1575
|
parsed = canonicalDefinitionSchema.parse(candidate);
|
|
@@ -618,7 +1584,7 @@ function serializeNamespace(input) {
|
|
|
618
1584
|
function computeCanonicalHash(definition) {
|
|
619
1585
|
return crypto.createHash("sha256").update(HASH_DOMAIN).update(stableJson(definition)).digest("hex");
|
|
620
1586
|
}
|
|
621
|
-
function buildCanonicalTable({ table,
|
|
1587
|
+
function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
622
1588
|
const candidate = {
|
|
623
1589
|
table: table.tableName,
|
|
624
1590
|
columns: [],
|
|
@@ -630,6 +1596,31 @@ function buildCanonicalTable({ table, tablesByName, enumsByName }) {
|
|
|
630
1596
|
if (table.config.description !== void 0) candidate.description = table.config.description;
|
|
631
1597
|
if (table.config.renamedFrom !== void 0) candidate.renamedFrom = table.config.renamedFrom;
|
|
632
1598
|
if (table.source !== void 0) candidate.source = table.source;
|
|
1599
|
+
if (table.source !== void 0) {
|
|
1600
|
+
if (table.config.indexes !== void 0 && table.config.indexes.length > 0) {
|
|
1601
|
+
throw new OrmDefinitionError(
|
|
1602
|
+
`synced table "${table.tableName}" is system-owned; indexes are not allowed`,
|
|
1603
|
+
table.file
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
if (table.config.cowMutations !== void 0 && table.config.cowMutations.length > 0) {
|
|
1607
|
+
throw new OrmDefinitionError(
|
|
1608
|
+
`synced table "${table.tableName}" is read-only; cow mutations belong on the overlay table`,
|
|
1609
|
+
table.file
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
if (table.config.policies?.write !== void 0 || table.config.policies?.insert !== void 0 || table.config.policies?.update !== void 0) {
|
|
1613
|
+
throw new OrmDefinitionError(`synced table "${table.tableName}" is read-only`, table.file);
|
|
1614
|
+
}
|
|
1615
|
+
for (const [linkName, link] of Object.entries(table.config.links ?? {})) {
|
|
1616
|
+
if (link.enforce) {
|
|
1617
|
+
throw new OrmDefinitionError(
|
|
1618
|
+
`synced table "${table.tableName}": link "${linkName}" cannot enforce foreign keys \u2014 its DDL is owned by the sync`,
|
|
1619
|
+
table.file
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
633
1624
|
const columnLevelPk = [];
|
|
634
1625
|
for (const [propertyName, builder] of Object.entries(table.columns)) {
|
|
635
1626
|
const column = builder.toCanonicalColumn(propertyName);
|
|
@@ -651,40 +1642,38 @@ function buildCanonicalTable({ table, tablesByName, enumsByName }) {
|
|
|
651
1642
|
);
|
|
652
1643
|
}
|
|
653
1644
|
candidate.primaryKey = table.config.primaryKey.map(
|
|
654
|
-
(ref) => resolveColumnRef({ table, ref, role: "primaryKey" })
|
|
1645
|
+
(ref) => resolveColumnRef({ relation: table, ref, role: "primaryKey" })
|
|
655
1646
|
);
|
|
656
1647
|
} else if (columnLevelPk.length > 0) {
|
|
657
1648
|
candidate.primaryKey = columnLevelPk;
|
|
658
1649
|
}
|
|
659
1650
|
for (const index3 of table.config.indexes ?? []) {
|
|
660
1651
|
const entry = {
|
|
661
|
-
columns: index3.columns.map((ref) => resolveColumnRef({ table, ref, role: "index" }))
|
|
1652
|
+
columns: index3.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
|
|
662
1653
|
};
|
|
663
1654
|
if (index3.unique !== void 0) entry.unique = index3.unique;
|
|
664
1655
|
if (index3.name !== void 0) entry.name = index3.name;
|
|
665
1656
|
candidate.indexes?.push(entry);
|
|
666
1657
|
}
|
|
667
1658
|
for (const [linkName, link] of Object.entries(table.config.links ?? {})) {
|
|
668
|
-
candidate.links?.push(buildCanonicalLink({ table, linkName, link,
|
|
1659
|
+
candidate.links?.push(buildCanonicalLink({ relation: table, linkName, link, relationsByName }));
|
|
669
1660
|
}
|
|
670
1661
|
const policies = {};
|
|
671
1662
|
if (table.config.policies?.read !== void 0) policies.read = toCanonicalPolicy(table.config.policies.read);
|
|
672
1663
|
if (table.config.policies?.write !== void 0) policies.write = toCanonicalPolicy(table.config.policies.write);
|
|
1664
|
+
if (table.config.policies?.insert !== void 0) policies.insert = toCanonicalPolicy(table.config.policies.insert);
|
|
1665
|
+
if (table.config.policies?.update !== void 0) policies.update = toCanonicalPolicy(table.config.policies.update);
|
|
673
1666
|
candidate.policies = policies;
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
executes: action.executes,
|
|
685
|
-
returns: action.returns ?? "rowsAffected",
|
|
686
|
-
...action.audit !== void 0 ? { audit: action.audit } : {}
|
|
687
|
-
});
|
|
1667
|
+
const cowMutations = table.config.cowMutations ?? [];
|
|
1668
|
+
if (cowMutations.length > 0) {
|
|
1669
|
+
candidate.cowMutations = cowMutations.map((mutation) => ({
|
|
1670
|
+
kind: mutation.kind,
|
|
1671
|
+
name: mutation.name,
|
|
1672
|
+
synced: mutation.synced,
|
|
1673
|
+
pk: mutation.pk,
|
|
1674
|
+
copy: [...mutation.copy],
|
|
1675
|
+
set: [...mutation.set]
|
|
1676
|
+
}));
|
|
688
1677
|
}
|
|
689
1678
|
try {
|
|
690
1679
|
return canonicalTableSchema.parse(candidate);
|
|
@@ -695,14 +1684,71 @@ function buildCanonicalTable({ table, tablesByName, enumsByName }) {
|
|
|
695
1684
|
throw error;
|
|
696
1685
|
}
|
|
697
1686
|
}
|
|
1687
|
+
function buildCanonicalView({
|
|
1688
|
+
view,
|
|
1689
|
+
relationsByName,
|
|
1690
|
+
enumsByName
|
|
1691
|
+
}) {
|
|
1692
|
+
if (view.config.policies?.read === void 0) {
|
|
1693
|
+
throw new OrmDefinitionError(`view "${view.viewName}" must declare policies.read`, view.file);
|
|
1694
|
+
}
|
|
1695
|
+
const columns = [];
|
|
1696
|
+
const columnLevelPk = [];
|
|
1697
|
+
for (const [propertyName, builder] of Object.entries(view.columns)) {
|
|
1698
|
+
const column = builder.toCanonicalColumn(propertyName);
|
|
1699
|
+
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1700
|
+
throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare a default`, view.file);
|
|
1701
|
+
}
|
|
1702
|
+
if (column.unique) {
|
|
1703
|
+
throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare unique`, view.file);
|
|
1704
|
+
}
|
|
1705
|
+
if (column.renamedFrom !== void 0 || column.cast !== void 0) {
|
|
1706
|
+
throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare migration hints`, view.file);
|
|
1707
|
+
}
|
|
1708
|
+
const enumName = builder.enumName;
|
|
1709
|
+
if (enumName !== void 0 && !enumsByName.has(enumName)) {
|
|
1710
|
+
throw new OrmDefinitionError(
|
|
1711
|
+
`view "${view.viewName}": column "${propertyName}" references enum "${enumName}" which is not part of this namespace`,
|
|
1712
|
+
view.file
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
|
|
1716
|
+
columns.push(column);
|
|
1717
|
+
}
|
|
1718
|
+
let primaryKey2;
|
|
1719
|
+
if (view.config.primaryKey !== void 0) {
|
|
1720
|
+
if (columnLevelPk.length > 0) {
|
|
1721
|
+
throw new OrmDefinitionError(
|
|
1722
|
+
`view "${view.viewName}": declare the logical primary key either on columns or with config.primaryKey \u2014 not both`,
|
|
1723
|
+
view.file
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
primaryKey2 = view.config.primaryKey.map((ref) => resolveColumnRef({ relation: view, ref, role: "primaryKey" }));
|
|
1727
|
+
} else if (columnLevelPk.length > 0) {
|
|
1728
|
+
primaryKey2 = columnLevelPk;
|
|
1729
|
+
}
|
|
1730
|
+
const links = Object.entries(view.config.links ?? {}).map(
|
|
1731
|
+
([linkName, link]) => buildCanonicalLink({ relation: view, linkName, link, relationsByName })
|
|
1732
|
+
);
|
|
1733
|
+
return {
|
|
1734
|
+
view: view.viewName,
|
|
1735
|
+
...view.config.description !== void 0 ? { description: view.config.description } : {},
|
|
1736
|
+
columns,
|
|
1737
|
+
...primaryKey2 !== void 0 ? { primaryKey: primaryKey2 } : {},
|
|
1738
|
+
links,
|
|
1739
|
+
policies: { read: toCanonicalPolicy(view.config.policies.read) }
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
698
1742
|
function buildCanonicalLink({
|
|
699
|
-
|
|
1743
|
+
relation,
|
|
700
1744
|
linkName,
|
|
701
1745
|
link,
|
|
702
|
-
|
|
1746
|
+
relationsByName
|
|
703
1747
|
}) {
|
|
1748
|
+
const ownerKind = relationKind(relation);
|
|
1749
|
+
const ownerName = relationName(relation);
|
|
704
1750
|
const fail = (message) => {
|
|
705
|
-
throw new OrmDefinitionError(
|
|
1751
|
+
throw new OrmDefinitionError(`${ownerKind} "${ownerName}": link "${linkName}" ${message}`, relation.file);
|
|
706
1752
|
};
|
|
707
1753
|
if (link.enforce && link.linkKind !== "toOne") {
|
|
708
1754
|
return fail("sets enforce: true, which is only supported on toOne links");
|
|
@@ -712,29 +1758,34 @@ function buildCanonicalLink({
|
|
|
712
1758
|
return {
|
|
713
1759
|
name: linkName,
|
|
714
1760
|
kind: "toPlatformUser",
|
|
715
|
-
on: resolveColumnRef({
|
|
1761
|
+
on: resolveColumnRef({ relation, ref: link.on, role: `link "${linkName}" on` }),
|
|
716
1762
|
...link.references !== void 0 ? { references: deriveColumnName(link.references) } : {},
|
|
717
1763
|
enforce: false,
|
|
718
1764
|
...link.description !== void 0 ? { description: link.description } : {}
|
|
719
1765
|
};
|
|
720
1766
|
}
|
|
721
|
-
const target =
|
|
722
|
-
|
|
723
|
-
|
|
1767
|
+
const target = resolveRelationRef({ ref: link.target, fail });
|
|
1768
|
+
const targetName = relationName(target);
|
|
1769
|
+
if (!relationsByName.has(targetName)) {
|
|
1770
|
+
return fail(`targets ${relationKind(target)} "${targetName}" which is not part of this namespace`);
|
|
1771
|
+
}
|
|
1772
|
+
if (link.enforce && (!isSeqTable(relation) || !isSeqTable(target))) {
|
|
1773
|
+
return fail("cannot enforce a PostgreSQL foreign key involving a view");
|
|
724
1774
|
}
|
|
725
|
-
const owningSide = link.linkKind === "toOne" ?
|
|
1775
|
+
const owningSide = link.linkKind === "toOne" ? relation : target;
|
|
1776
|
+
const referencedSide = link.linkKind === "toOne" ? target : relation;
|
|
726
1777
|
return {
|
|
727
1778
|
name: linkName,
|
|
728
1779
|
kind: link.linkKind,
|
|
729
|
-
target:
|
|
730
|
-
on: resolveColumnRef({
|
|
731
|
-
...link.references !== void 0 ? { references: resolveColumnRef({
|
|
1780
|
+
target: targetName,
|
|
1781
|
+
on: resolveColumnRef({ relation: owningSide, ref: link.on, role: `link "${linkName}" on` }),
|
|
1782
|
+
...link.references !== void 0 ? { references: resolveColumnRef({ relation: referencedSide, ref: link.references, role: `link "${linkName}" references` }) } : {},
|
|
732
1783
|
enforce: link.enforce,
|
|
733
1784
|
...link.description !== void 0 ? { description: link.description } : {}
|
|
734
1785
|
};
|
|
735
1786
|
}
|
|
736
|
-
function
|
|
737
|
-
if (ref === void 0) return fail("declares no target
|
|
1787
|
+
function resolveRelationRef({ ref, fail }) {
|
|
1788
|
+
if (ref === void 0) return fail("declares no target relation");
|
|
738
1789
|
if (typeof ref !== "function") return ref;
|
|
739
1790
|
let resolved;
|
|
740
1791
|
try {
|
|
@@ -744,21 +1795,36 @@ function resolveTableRef({ ref, fail }) {
|
|
|
744
1795
|
`target thunk threw (${error instanceof Error ? error.message : String(error)}) \u2014 lazy refs resolve at serialize time; make sure the target module ran`
|
|
745
1796
|
);
|
|
746
1797
|
}
|
|
747
|
-
if (!isSeqTable(resolved)
|
|
1798
|
+
if (!isSeqTable(resolved) && !isSeqView(resolved)) {
|
|
1799
|
+
return fail("target thunk did not return a relation defined with defineTable/defineView");
|
|
1800
|
+
}
|
|
748
1801
|
return resolved;
|
|
749
1802
|
}
|
|
750
|
-
function resolveColumnRef({
|
|
751
|
-
const direct =
|
|
1803
|
+
function resolveColumnRef({ relation, ref, role }) {
|
|
1804
|
+
const direct = relation.columns[ref];
|
|
752
1805
|
if (direct) return direct.columnNameFor(ref);
|
|
753
|
-
for (const [propertyName, builder] of Object.entries(
|
|
1806
|
+
for (const [propertyName, builder] of Object.entries(relation.columns)) {
|
|
754
1807
|
if (builder.columnNameFor(propertyName) === ref) return ref;
|
|
755
1808
|
}
|
|
756
|
-
const known = Object.keys(
|
|
1809
|
+
const known = Object.keys(relation.columns).join(", ");
|
|
757
1810
|
throw new OrmDefinitionError(
|
|
758
|
-
|
|
759
|
-
|
|
1811
|
+
`${relationKind(relation)} "${relationName(relation)}": ${role} references unknown column "${ref}" (known columns: ${known || "none"})`,
|
|
1812
|
+
relation.file
|
|
760
1813
|
);
|
|
761
1814
|
}
|
|
1815
|
+
function assertAcyclicViewDependencies(views) {
|
|
1816
|
+
const viewNames = new Set(views.map((view) => view.view));
|
|
1817
|
+
const remaining = new Map(
|
|
1818
|
+
views.map((view) => [view.view, new Set(view.dependencies.filter((dependency) => viewNames.has(dependency)))])
|
|
1819
|
+
);
|
|
1820
|
+
while (remaining.size > 0) {
|
|
1821
|
+
const ready = [...remaining.entries()].filter(([, dependencies]) => [...dependencies].every((dependency) => !remaining.has(dependency))).map(([name]) => name);
|
|
1822
|
+
if (ready.length === 0) {
|
|
1823
|
+
throw new OrmDefinitionError(`view dependencies form a cycle among: ${[...remaining.keys()].sort().join(", ")}`);
|
|
1824
|
+
}
|
|
1825
|
+
for (const name of ready) remaining.delete(name);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
762
1828
|
function toCanonicalPolicy(policy) {
|
|
763
1829
|
if (typeof policy === "string") return policy;
|
|
764
1830
|
return {
|
|
@@ -770,12 +1836,110 @@ function toCanonicalPolicy(policy) {
|
|
|
770
1836
|
}))
|
|
771
1837
|
};
|
|
772
1838
|
}
|
|
773
|
-
function describeFile(
|
|
774
|
-
return
|
|
1839
|
+
function describeFile(relation) {
|
|
1840
|
+
return relation.file ?? "<unknown file>";
|
|
775
1841
|
}
|
|
776
1842
|
function formatZodIssues(error) {
|
|
777
1843
|
return error.issues.map((issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message).join("; ");
|
|
778
1844
|
}
|
|
1845
|
+
var SYNCED_CONTRACT_HASH_DOMAIN = "sequence-orm-synced-contract\0";
|
|
1846
|
+
var ORM_TO_PIPELINE_TYPE = {
|
|
1847
|
+
uuid: "uuid",
|
|
1848
|
+
text: "string",
|
|
1849
|
+
integer: "int",
|
|
1850
|
+
bigint: "long",
|
|
1851
|
+
numeric: "numeric",
|
|
1852
|
+
double: "double",
|
|
1853
|
+
boolean: "boolean",
|
|
1854
|
+
timestamptz: "timestamp",
|
|
1855
|
+
date: "date",
|
|
1856
|
+
jsonb: "json"
|
|
1857
|
+
};
|
|
1858
|
+
function isColumnType(value) {
|
|
1859
|
+
return typeof value === "string" && value in ORM_TO_PIPELINE_TYPE;
|
|
1860
|
+
}
|
|
1861
|
+
function syncedContractSurface({
|
|
1862
|
+
namespace,
|
|
1863
|
+
table,
|
|
1864
|
+
columns,
|
|
1865
|
+
primary_key
|
|
1866
|
+
}) {
|
|
1867
|
+
return {
|
|
1868
|
+
namespace,
|
|
1869
|
+
table,
|
|
1870
|
+
columns: Object.fromEntries(
|
|
1871
|
+
Object.entries(columns).map(([name, column]) => [
|
|
1872
|
+
name,
|
|
1873
|
+
{ type: column.type, nullable: column.nullable, key: column.key }
|
|
1874
|
+
])
|
|
1875
|
+
),
|
|
1876
|
+
primary_key: [...primary_key]
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
function computeSyncedContractHash(surface) {
|
|
1880
|
+
return crypto.createHash("sha256").update(SYNCED_CONTRACT_HASH_DOMAIN).update(stableJson(surface)).digest("hex");
|
|
1881
|
+
}
|
|
1882
|
+
function extractOrmSyncedTableContract({
|
|
1883
|
+
namespace,
|
|
1884
|
+
table
|
|
1885
|
+
}) {
|
|
1886
|
+
if (!NAMESPACE_PATTERN.test(namespace)) {
|
|
1887
|
+
throw new OrmDefinitionError(
|
|
1888
|
+
`namespace "${namespace}" must match ${NAMESPACE_PATTERN}`
|
|
1889
|
+
);
|
|
1890
|
+
}
|
|
1891
|
+
if (table.source === void 0) {
|
|
1892
|
+
throw new OrmDefinitionError(
|
|
1893
|
+
`table "${table.table}" is not a synced table \u2014 extractOrmSyncedTableContract requires source: gold.*`
|
|
1894
|
+
);
|
|
1895
|
+
}
|
|
1896
|
+
if (table.columns.length === 0) {
|
|
1897
|
+
throw new OrmDefinitionError(
|
|
1898
|
+
`synced table "${table.table}" has no typed columns \u2014 declare a typed defineSyncedTable contract before exporting`
|
|
1899
|
+
);
|
|
1900
|
+
}
|
|
1901
|
+
const primaryKey2 = table.primaryKey;
|
|
1902
|
+
if (primaryKey2 === void 0 || primaryKey2.length === 0) {
|
|
1903
|
+
throw new OrmDefinitionError(
|
|
1904
|
+
`synced table "${table.table}" has no primary key \u2014 typed synced contracts require a primary key`
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
const pkSet = new Set(primaryKey2);
|
|
1908
|
+
const columns = {};
|
|
1909
|
+
for (const column of table.columns) {
|
|
1910
|
+
if (!isColumnType(column.type)) {
|
|
1911
|
+
const enumName = typeof column.type === "object" && column.type !== null && "enum" in column.type ? column.type.enum : String(column.type);
|
|
1912
|
+
throw new OrmDefinitionError(
|
|
1913
|
+
`synced table "${table.table}": column "${column.columnName}" has enum/type '${enumName}', which has no pipeline-type mapping \u2014 use an ORM-mappable scalar`
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
columns[column.columnName] = {
|
|
1917
|
+
type: ORM_TO_PIPELINE_TYPE[column.type],
|
|
1918
|
+
nullable: column.nullable,
|
|
1919
|
+
key: pkSet.has(column.columnName)
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
for (const key of primaryKey2) {
|
|
1923
|
+
if (!(key in columns)) {
|
|
1924
|
+
throw new OrmDefinitionError(
|
|
1925
|
+
`synced table "${table.table}": primary key column "${key}" is not declared in columns`
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
const surface = syncedContractSurface({
|
|
1930
|
+
namespace,
|
|
1931
|
+
table: table.table,
|
|
1932
|
+
columns,
|
|
1933
|
+
primary_key: primaryKey2
|
|
1934
|
+
});
|
|
1935
|
+
return {
|
|
1936
|
+
namespace,
|
|
1937
|
+
table: table.table,
|
|
1938
|
+
columns,
|
|
1939
|
+
primary_key: [...primaryKey2],
|
|
1940
|
+
content_hash: computeSyncedContractHash(surface)
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
779
1943
|
var NAMESPACE_CONFIG_FILE = "sequence.config.ts";
|
|
780
1944
|
var SCHEMA_DIR = "schema";
|
|
781
1945
|
async function loadNamespaceFromDir(dir, options = {}) {
|
|
@@ -793,7 +1957,12 @@ async function loadNamespaceFromDir(dir, options = {}) {
|
|
|
793
1957
|
const jiti$1 = jiti.createJiti((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)), {
|
|
794
1958
|
interopDefault: true,
|
|
795
1959
|
fsCache: false,
|
|
796
|
-
...options.ormModulePath !== void 0 ? {
|
|
1960
|
+
...options.ormModulePath !== void 0 ? {
|
|
1961
|
+
alias: {
|
|
1962
|
+
"@sequenceholdings/orm": options.ormModulePath,
|
|
1963
|
+
"@sequenceholdings/orm/authoring": options.ormModulePath
|
|
1964
|
+
}
|
|
1965
|
+
} : {}
|
|
797
1966
|
});
|
|
798
1967
|
const context = beginAuthoringContext();
|
|
799
1968
|
try {
|
|
@@ -834,16 +2003,20 @@ async function loadNamespaceFromDir(dir, options = {}) {
|
|
|
834
2003
|
for (const entry of context.tables) {
|
|
835
2004
|
if (isSeqTable(entry.value)) tables.push(entry.value);
|
|
836
2005
|
}
|
|
837
|
-
|
|
2006
|
+
const views = [];
|
|
2007
|
+
for (const entry of context.views) {
|
|
2008
|
+
if (isSeqView(entry.value)) views.push(entry.value);
|
|
2009
|
+
}
|
|
2010
|
+
if (tables.length === 0 && views.length === 0) {
|
|
838
2011
|
throw new OrmDefinitionError(
|
|
839
|
-
`no
|
|
2012
|
+
`no relations registered \u2014 ${SCHEMA_DIR}/*.ts modules must call defineTable(...) or defineView(...) at import time`
|
|
840
2013
|
);
|
|
841
2014
|
}
|
|
842
2015
|
const enums = [];
|
|
843
2016
|
for (const entry of context.enums) {
|
|
844
2017
|
if (isEnumRef(entry.value)) enums.push(entry.value);
|
|
845
2018
|
}
|
|
846
|
-
const canonical = serializeNamespace({ config: configEntry.value, tables, enums });
|
|
2019
|
+
const canonical = serializeNamespace({ config: configEntry.value, tables, views, enums });
|
|
847
2020
|
return { canonical, hash: computeCanonicalHash(canonical), files };
|
|
848
2021
|
}
|
|
849
2022
|
async function listSchemaFiles(root) {
|
|
@@ -881,13 +2054,17 @@ function generateNamespaceTypes(definition) {
|
|
|
881
2054
|
`export type ${typesInterfaceName(definition.namespace)} = {`
|
|
882
2055
|
];
|
|
883
2056
|
for (const table of definition.tables) {
|
|
884
|
-
|
|
2057
|
+
const columns = Object.entries(table.columns ?? {});
|
|
2058
|
+
if (table.source && columns.length === 0) {
|
|
885
2059
|
lines.push(` /** Synced from ${table.source} \u2014 columns reflect the gold source. */`);
|
|
886
2060
|
lines.push(` ${table.table}: Record<string, unknown>`);
|
|
887
2061
|
continue;
|
|
888
2062
|
}
|
|
2063
|
+
if (table.source) {
|
|
2064
|
+
lines.push(` /** Synced from ${table.source} \u2014 read-only typed contract. */`);
|
|
2065
|
+
}
|
|
889
2066
|
lines.push(` ${table.table}: {`);
|
|
890
|
-
for (const [name, column] of
|
|
2067
|
+
for (const [name, column] of columns) {
|
|
891
2068
|
if (column.deprecated) continue;
|
|
892
2069
|
lines.push(` ${name}: ${columnWireType(column)}`);
|
|
893
2070
|
}
|
|
@@ -1344,8 +2521,8 @@ var actionSchema = zod.z.strictObject({
|
|
|
1344
2521
|
}
|
|
1345
2522
|
}
|
|
1346
2523
|
});
|
|
1347
|
-
function extractNamedParams(
|
|
1348
|
-
const withoutStrings =
|
|
2524
|
+
function extractNamedParams(sql4) {
|
|
2525
|
+
const withoutStrings = sql4.replace(QUOTED_STRING_PATTERN, "''");
|
|
1349
2526
|
const names = [...withoutStrings.matchAll(NAMED_PARAM_PATTERN)].map((match) => match[1]);
|
|
1350
2527
|
return [...new Set(names)];
|
|
1351
2528
|
}
|
|
@@ -1415,11 +2592,13 @@ var sqlTypeName = (type) => SQL_TYPE_NAMES[type] ?? type;
|
|
|
1415
2592
|
// src/compiler/sql.ts
|
|
1416
2593
|
var CLAIMS_SETTING = "request.jwt.claims";
|
|
1417
2594
|
var CAPABILITIES_CLAIM_KEY = "capabilities";
|
|
2595
|
+
var ROLES_CLAIM_KEY = "roles";
|
|
1418
2596
|
var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
1419
2597
|
var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
1420
2598
|
var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
|
|
1421
2599
|
var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
|
|
1422
2600
|
var capabilityExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
2601
|
+
var roleExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${ROLES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
1423
2602
|
|
|
1424
2603
|
// src/compiler/predicates.ts
|
|
1425
2604
|
var SQL_COMPARE_OPS = {
|
|
@@ -1500,8 +2679,8 @@ var actionParamArg = (name) => `p_${name}`;
|
|
|
1500
2679
|
function compileActionStatements(action, renderParam = actionParamArg) {
|
|
1501
2680
|
return actionStatementSources(action.executes).map((statement) => compileStatement(statement, renderParam));
|
|
1502
2681
|
}
|
|
1503
|
-
function compileStatement(
|
|
1504
|
-
const compiled =
|
|
2682
|
+
function compileStatement(sql4, renderParam) {
|
|
2683
|
+
const compiled = sql4.replace(TOKEN_PATTERN, (token, claimKey, capabilityKey, paramName) => {
|
|
1505
2684
|
if (claimKey !== void 0) {
|
|
1506
2685
|
if (!CLAIM_KEY_PATTERN.test(claimKey)) throw new OrmDefinitionError(`invalid claim key in executes: ${token}`);
|
|
1507
2686
|
return claimExpression(claimKey);
|
|
@@ -1533,6 +2712,118 @@ function referencedExecuteCapabilities(action) {
|
|
|
1533
2712
|
return keys;
|
|
1534
2713
|
}
|
|
1535
2714
|
|
|
2715
|
+
// src/compiler/pred-ir-sql.ts
|
|
2716
|
+
function compilePredIrToSql(pred, options = {}) {
|
|
2717
|
+
switch (pred.kind) {
|
|
2718
|
+
case "const":
|
|
2719
|
+
return pred.value ? "TRUE" : "FALSE";
|
|
2720
|
+
case "and":
|
|
2721
|
+
return `(${pred.operands.map((operand) => compilePredIrToSql(operand, options)).join(" AND ")})`;
|
|
2722
|
+
case "or":
|
|
2723
|
+
return `(${pred.operands.map((operand) => compilePredIrToSql(operand, options)).join(" OR ")})`;
|
|
2724
|
+
case "not":
|
|
2725
|
+
return `(NOT ${compilePredIrToSql(pred.operand, options)})`;
|
|
2726
|
+
case "cmp":
|
|
2727
|
+
return compileCmp(pred.op, pred.column, pred.value, options);
|
|
2728
|
+
case "capability":
|
|
2729
|
+
return `(${capabilityExpression(pred.key)})`;
|
|
2730
|
+
case "role":
|
|
2731
|
+
return `(${roleExpression(pred.key)})`;
|
|
2732
|
+
case "exists":
|
|
2733
|
+
case "aggPredicate":
|
|
2734
|
+
case "dynamicFilter":
|
|
2735
|
+
throw new OrmDefinitionError(
|
|
2736
|
+
`synced view read policy cannot use '${pred.kind}' \u2014 use row filters and claim/capability/role checks only`
|
|
2737
|
+
);
|
|
2738
|
+
default: {
|
|
2739
|
+
const exhausted = pred;
|
|
2740
|
+
throw new OrmDefinitionError(`unsupported synced view predicate kind '${String(exhausted)}'`);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
function physicalColumn(logical, options) {
|
|
2745
|
+
return options.columnNames?.[logical] ?? logical;
|
|
2746
|
+
}
|
|
2747
|
+
function compileCmp(op, column, value, options) {
|
|
2748
|
+
const physical = physicalColumn(column, options);
|
|
2749
|
+
const declared = options.columnTypes?.[physical] ?? options.columnTypes?.[column];
|
|
2750
|
+
const isEnum = typeof declared === "object" && declared !== null;
|
|
2751
|
+
const col = isEnum ? `${quoteIdentifier(physical)}::text` : quoteIdentifier(physical);
|
|
2752
|
+
const columnType = isEnum ? "text" : declared;
|
|
2753
|
+
switch (op) {
|
|
2754
|
+
case "eq":
|
|
2755
|
+
return compileEquality(col, value, columnType, false);
|
|
2756
|
+
case "neq":
|
|
2757
|
+
return compileEquality(col, value, columnType, true);
|
|
2758
|
+
case "gt":
|
|
2759
|
+
return `${col} > ${compileValue(value, columnType)}`;
|
|
2760
|
+
case "gte":
|
|
2761
|
+
return `${col} >= ${compileValue(value, columnType)}`;
|
|
2762
|
+
case "lt":
|
|
2763
|
+
return `${col} < ${compileValue(value, columnType)}`;
|
|
2764
|
+
case "lte":
|
|
2765
|
+
return `${col} <= ${compileValue(value, columnType)}`;
|
|
2766
|
+
case "in":
|
|
2767
|
+
return `${col} = ANY(${compileArrayValue(value, columnType)})`;
|
|
2768
|
+
case "nin":
|
|
2769
|
+
return `${col} <> ALL(${compileArrayValue(value, columnType)})`;
|
|
2770
|
+
case "like":
|
|
2771
|
+
return `${col} LIKE ${compileValue(value, columnType)}`;
|
|
2772
|
+
case "ilike":
|
|
2773
|
+
return `${col} ILIKE ${compileValue(value, columnType)}`;
|
|
2774
|
+
case "isNull":
|
|
2775
|
+
return value.kind === "literal" && value.value === true ? `${col} IS NULL` : `${col} IS NOT NULL`;
|
|
2776
|
+
case "contains":
|
|
2777
|
+
case "hasKey":
|
|
2778
|
+
throw new OrmDefinitionError(`synced view read policy cannot use json operator '${op}'`);
|
|
2779
|
+
default: {
|
|
2780
|
+
const exhausted = op;
|
|
2781
|
+
throw new OrmDefinitionError(`unsupported synced view comparison '${String(exhausted)}'`);
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
function compileEquality(col, value, columnType, negated, options) {
|
|
2786
|
+
if (value.kind === "literal" && value.value === null) {
|
|
2787
|
+
return negated ? `${col} IS NOT NULL` : `${col} IS NULL`;
|
|
2788
|
+
}
|
|
2789
|
+
const rhs = compileValue(value, columnType);
|
|
2790
|
+
return negated ? `${col} <> ${rhs}` : `${col} = ${rhs}`;
|
|
2791
|
+
}
|
|
2792
|
+
function compileValue(value, columnType, _options) {
|
|
2793
|
+
switch (value.kind) {
|
|
2794
|
+
case "literal": {
|
|
2795
|
+
const literal = value.value;
|
|
2796
|
+
if (literal === null) return "NULL";
|
|
2797
|
+
if (typeof literal === "string") return quoteLiteral(literal);
|
|
2798
|
+
if (typeof literal === "boolean") return literal ? "TRUE" : "FALSE";
|
|
2799
|
+
return String(literal);
|
|
2800
|
+
}
|
|
2801
|
+
case "claim": {
|
|
2802
|
+
const expression = claimExpression(value.key);
|
|
2803
|
+
return columnType && columnType !== "text" ? `(${expression})::${sqlTypeName(columnType)}` : expression;
|
|
2804
|
+
}
|
|
2805
|
+
case "variable":
|
|
2806
|
+
throw new OrmDefinitionError("synced view read policy cannot reference operation variables");
|
|
2807
|
+
default: {
|
|
2808
|
+
const exhausted = value;
|
|
2809
|
+
throw new OrmDefinitionError(`unsupported synced view value kind '${String(exhausted)}'`);
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
function compileArrayValue(value, columnType, _options) {
|
|
2814
|
+
if (value.kind !== "literal" || !Array.isArray(value.value)) {
|
|
2815
|
+
throw new OrmDefinitionError("synced view IN/NIN comparisons require literal arrays");
|
|
2816
|
+
}
|
|
2817
|
+
const entries = value.value.map((entry) => {
|
|
2818
|
+
if (entry === null) return "NULL";
|
|
2819
|
+
if (typeof entry === "string") return quoteLiteral(entry);
|
|
2820
|
+
if (typeof entry === "boolean") return entry ? "TRUE" : "FALSE";
|
|
2821
|
+
return String(entry);
|
|
2822
|
+
});
|
|
2823
|
+
const array = `ARRAY[${entries.join(", ")}]`;
|
|
2824
|
+
return columnType ? `${array}::${sqlTypeName(columnType)}[]` : array;
|
|
2825
|
+
}
|
|
2826
|
+
|
|
1536
2827
|
// src/engine/actions.ts
|
|
1537
2828
|
var ACTION_REQUIRES_DENIED_SQLSTATE = "ORMRQ";
|
|
1538
2829
|
var ACTION_VALIDATES_FAILED_SQLSTATE = "ORMVL";
|
|
@@ -1554,8 +2845,8 @@ function actionPrepareChecks(definition) {
|
|
|
1554
2845
|
const order = actionParamOrder(action);
|
|
1555
2846
|
const signature = order.length > 0 ? ` (${order.map((param) => sqlTypeName(action.params[param].type)).join(", ")})` : "";
|
|
1556
2847
|
const compiled = compileActionStatements(action, (param) => `$${order.indexOf(param) + 1}`);
|
|
1557
|
-
compiled.forEach((
|
|
1558
|
-
const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns,
|
|
2848
|
+
compiled.forEach((sql4, index3) => {
|
|
2849
|
+
const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns, sql4) : sql4;
|
|
1559
2850
|
statements.push(`PREPARE ${quoteIdentifier(`orm_check_${name}_${index3}`)}${signature} AS ${checked}`);
|
|
1560
2851
|
});
|
|
1561
2852
|
}
|
|
@@ -1674,7 +2965,9 @@ var indexSchema = zod.z.union([
|
|
|
1674
2965
|
]).transform((index3) => Array.isArray(index3) ? { columns: index3 } : index3);
|
|
1675
2966
|
var policiesSchema = zod.z.strictObject({
|
|
1676
2967
|
read: parsedPredicateSchema.optional(),
|
|
1677
|
-
write: parsedPredicateSchema.optional()
|
|
2968
|
+
write: parsedPredicateSchema.optional(),
|
|
2969
|
+
insert: parsedPredicateSchema.optional(),
|
|
2970
|
+
update: parsedPredicateSchema.optional()
|
|
1678
2971
|
});
|
|
1679
2972
|
var tableFileSchema = zod.z.strictObject({
|
|
1680
2973
|
namespace: namespaceSchema,
|
|
@@ -1710,7 +3003,6 @@ var policyName = (table, suffix) => `${table}_${suffix}`;
|
|
|
1710
3003
|
var foreignKeyName = (table, link) => `fk_${table}_${link}`;
|
|
1711
3004
|
function validateSyncedTable(table, ctx) {
|
|
1712
3005
|
const forbidden = [
|
|
1713
|
-
["columns", "synced tables reflect their source; columns are not declared"],
|
|
1714
3006
|
["actions", "synced tables are read-only; actions are not allowed"],
|
|
1715
3007
|
["indexes", "synced tables are system-owned; indexes are not allowed"]
|
|
1716
3008
|
];
|
|
@@ -1720,6 +3012,39 @@ function validateSyncedTable(table, ctx) {
|
|
|
1720
3012
|
if (table.policies?.write) {
|
|
1721
3013
|
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "write"] });
|
|
1722
3014
|
}
|
|
3015
|
+
if (table.policies?.insert) {
|
|
3016
|
+
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "insert"] });
|
|
3017
|
+
}
|
|
3018
|
+
if (table.policies?.update) {
|
|
3019
|
+
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "update"] });
|
|
3020
|
+
}
|
|
3021
|
+
const columns = table.columns ?? {};
|
|
3022
|
+
const names = Object.keys(columns);
|
|
3023
|
+
if (names.length === 0) return;
|
|
3024
|
+
const primaryKeys = names.filter((name) => columns[name]?.primary_key);
|
|
3025
|
+
if (primaryKeys.length !== 1) {
|
|
3026
|
+
ctx.addIssue({ code: "custom", message: "exactly one column must be primary_key", path: ["columns"] });
|
|
3027
|
+
}
|
|
3028
|
+
validateRenames(columns, names, ctx);
|
|
3029
|
+
for (const name of names) {
|
|
3030
|
+
const column = columns[name];
|
|
3031
|
+
if (!column) continue;
|
|
3032
|
+
const cast = column.cast ? castParts(column.cast) : null;
|
|
3033
|
+
if (column.cast && cast?.column !== name) {
|
|
3034
|
+
ctx.addIssue({
|
|
3035
|
+
code: "custom",
|
|
3036
|
+
message: "cast must reference the column it is declared on",
|
|
3037
|
+
path: ["columns", name, "cast"]
|
|
3038
|
+
});
|
|
3039
|
+
}
|
|
3040
|
+
if (cast && cast.type !== column.type) {
|
|
3041
|
+
ctx.addIssue({
|
|
3042
|
+
code: "custom",
|
|
3043
|
+
message: `cast target '${cast.type}' must match the declared type '${column.type}'`,
|
|
3044
|
+
path: ["columns", name, "cast"]
|
|
3045
|
+
});
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
1723
3048
|
}
|
|
1724
3049
|
function validateRenames(columns, names, ctx) {
|
|
1725
3050
|
const renameSources = /* @__PURE__ */ new Map();
|
|
@@ -1797,7 +3122,7 @@ function validateColumnTable(table, ctx) {
|
|
|
1797
3122
|
function validateLinks(table, ctx) {
|
|
1798
3123
|
const columnNames = Object.keys(table.columns ?? {});
|
|
1799
3124
|
for (const [name, link] of Object.entries(table.links ?? {})) {
|
|
1800
|
-
if (
|
|
3125
|
+
if (columnNames.length > 0 && !columnNames.includes(link.on)) {
|
|
1801
3126
|
ctx.addIssue({ code: "custom", message: `link references unknown column '${link.on}'`, path: ["links", name, "on"] });
|
|
1802
3127
|
}
|
|
1803
3128
|
if (link.enforce && link.to.includes(".")) {
|
|
@@ -1870,10 +3195,10 @@ var TYPE_GROUPS = {
|
|
|
1870
3195
|
};
|
|
1871
3196
|
var columnTypeGroup = (type) => TYPE_GROUPS[type];
|
|
1872
3197
|
function validatePolicyColumns(table, ctx) {
|
|
1873
|
-
if (table.source) return;
|
|
1874
3198
|
const columns = table.columns ?? {};
|
|
1875
3199
|
const columnNames = Object.keys(columns);
|
|
1876
|
-
|
|
3200
|
+
if (columnNames.length === 0) return;
|
|
3201
|
+
for (const kind of ["read", "write", "insert", "update"]) {
|
|
1877
3202
|
const predicate = table.policies?.[kind];
|
|
1878
3203
|
if (!predicate) continue;
|
|
1879
3204
|
for (const column of referencedColumns(predicate.ast)) {
|
|
@@ -1950,10 +3275,15 @@ function policyStatements(schema, table) {
|
|
|
1950
3275
|
if (read) {
|
|
1951
3276
|
statements.push(`CREATE POLICY ${policyName2("read")} ON ${qualified} FOR SELECT USING (${compilePredicate(read.ast, columnTypes)})`);
|
|
1952
3277
|
}
|
|
1953
|
-
const
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
statements.push(
|
|
3278
|
+
const insertPolicy = table.policies?.insert ?? table.policies?.write;
|
|
3279
|
+
const updatePolicy = table.policies?.update ?? table.policies?.write;
|
|
3280
|
+
if (insertPolicy) {
|
|
3281
|
+
statements.push(
|
|
3282
|
+
`CREATE POLICY ${policyName2("insert")} ON ${qualified} FOR INSERT WITH CHECK (${compilePredicate(insertPolicy.ast, columnTypes)})`
|
|
3283
|
+
);
|
|
3284
|
+
}
|
|
3285
|
+
if (updatePolicy) {
|
|
3286
|
+
const predicate = compilePredicate(updatePolicy.ast, columnTypes);
|
|
1957
3287
|
statements.push(
|
|
1958
3288
|
`CREATE POLICY ${policyName2("update")} ON ${qualified} FOR UPDATE USING (${predicate}) WITH CHECK (${predicate})`
|
|
1959
3289
|
);
|
|
@@ -1962,6 +3292,118 @@ function policyStatements(schema, table) {
|
|
|
1962
3292
|
return statements;
|
|
1963
3293
|
}
|
|
1964
3294
|
var columnTables = (definition) => definition.tables.filter((table) => !table.source);
|
|
3295
|
+
var syncedTables = (definition) => definition.tables.filter((table) => !!table.source);
|
|
3296
|
+
var SYNCED_VIEW_EXECUTION_OWNER_ROLE = "orm_exec";
|
|
3297
|
+
function syncedViewStatements({
|
|
3298
|
+
definition,
|
|
3299
|
+
backings,
|
|
3300
|
+
readPolicyIrByTable,
|
|
3301
|
+
columnNamesByTable,
|
|
3302
|
+
columnTypesByTable,
|
|
3303
|
+
executionOwnerRole = SYNCED_VIEW_EXECUTION_OWNER_ROLE
|
|
3304
|
+
}) {
|
|
3305
|
+
const schema = quoteIdentifier(ormSchemaName(definition.namespace));
|
|
3306
|
+
const role = quoteIdentifier(consumerRoleName(definition.namespace));
|
|
3307
|
+
const owner = quoteIdentifier(executionOwnerRole);
|
|
3308
|
+
const backingMap = /* @__PURE__ */ new Map();
|
|
3309
|
+
for (const backing of backings) {
|
|
3310
|
+
const existing = backingMap.get(backing.table);
|
|
3311
|
+
if (existing) {
|
|
3312
|
+
throw new OrmDefinitionError(
|
|
3313
|
+
`duplicate synced backing for table "${backing.table}" ('${existing.physicalRelation}' and '${backing.physicalRelation}') \u2014 refusing to materialize an ambiguous authorization boundary`
|
|
3314
|
+
);
|
|
3315
|
+
}
|
|
3316
|
+
backingMap.set(backing.table, backing);
|
|
3317
|
+
}
|
|
3318
|
+
const statements = [];
|
|
3319
|
+
for (const table of syncedTables(definition)) {
|
|
3320
|
+
const backing = backingMap.get(table.table);
|
|
3321
|
+
if (!backing) continue;
|
|
3322
|
+
const viewName = `${schema}.${quoteIdentifier(table.table)}`;
|
|
3323
|
+
const physical = splitPhysicalRelation(backing.physicalRelation);
|
|
3324
|
+
const physicalTarget = `${quoteIdentifier(physical.schema)}.${quoteIdentifier(physical.table)}`;
|
|
3325
|
+
const columnTypes = mergeSyncedViewColumnTypes({
|
|
3326
|
+
table,
|
|
3327
|
+
canonicalColumnTypes: columnTypesByTable?.get(table.table),
|
|
3328
|
+
backingColumnTypes: backing.columnTypes,
|
|
3329
|
+
columnNames: columnNamesByTable?.get(table.table)
|
|
3330
|
+
});
|
|
3331
|
+
const readPolicyIr = readPolicyIrByTable?.get(table.table);
|
|
3332
|
+
const readPolicy = table.policies?.read;
|
|
3333
|
+
const whereClause = readPolicyIr ? ` WHERE ${compilePredIrToSql(readPolicyIr, {
|
|
3334
|
+
columnTypes,
|
|
3335
|
+
columnNames: columnNamesByTable?.get(table.table)
|
|
3336
|
+
})}` : readPolicy ? ` WHERE ${compilePredicate(readPolicy.ast, narrowToV1ColumnTypes(columnTypes))}` : " WHERE false";
|
|
3337
|
+
statements.push(`GRANT USAGE ON SCHEMA ${quoteIdentifier(physical.schema)} TO ${owner}`);
|
|
3338
|
+
statements.push(`GRANT SELECT ON ${physicalTarget} TO ${owner}`);
|
|
3339
|
+
statements.push(
|
|
3340
|
+
`CREATE OR REPLACE VIEW ${viewName} AS SELECT * FROM ${physicalTarget}${whereClause}`
|
|
3341
|
+
);
|
|
3342
|
+
statements.push(`GRANT SELECT ON ${viewName} TO ${role}`);
|
|
3343
|
+
}
|
|
3344
|
+
return statements;
|
|
3345
|
+
}
|
|
3346
|
+
function mergeSyncedViewColumnTypes({
|
|
3347
|
+
table,
|
|
3348
|
+
canonicalColumnTypes,
|
|
3349
|
+
backingColumnTypes,
|
|
3350
|
+
columnNames
|
|
3351
|
+
}) {
|
|
3352
|
+
const fromTable = {};
|
|
3353
|
+
for (const [name, def] of Object.entries(table.columns ?? {})) {
|
|
3354
|
+
if (def?.type) fromTable[name] = def.type;
|
|
3355
|
+
}
|
|
3356
|
+
if (columnNames) {
|
|
3357
|
+
for (const [logical, physical] of Object.entries(columnNames)) {
|
|
3358
|
+
const declared = fromTable[physical] ?? fromTable[logical];
|
|
3359
|
+
if (!declared) continue;
|
|
3360
|
+
fromTable[physical] = declared;
|
|
3361
|
+
fromTable[logical] = declared;
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
const canonicalEnums = Object.fromEntries(
|
|
3365
|
+
Object.entries(canonicalColumnTypes ?? {}).filter(([, type]) => typeof type === "object")
|
|
3366
|
+
);
|
|
3367
|
+
return { ...fromTable, ...canonicalColumnTypes ?? {}, ...backingColumnTypes ?? {}, ...canonicalEnums };
|
|
3368
|
+
}
|
|
3369
|
+
function narrowToV1ColumnTypes(columnTypes) {
|
|
3370
|
+
return Object.fromEntries(
|
|
3371
|
+
Object.entries(columnTypes).map(([column, type]) => [column, typeof type === "object" ? "text" : type])
|
|
3372
|
+
);
|
|
3373
|
+
}
|
|
3374
|
+
function splitPhysicalRelation(physicalRelation) {
|
|
3375
|
+
const parts = physicalRelation.split(".");
|
|
3376
|
+
if (parts.length === 1 && parts[0]) return { schema: "public", table: parts[0] };
|
|
3377
|
+
if (parts.length === 2 && parts[0] && parts[1]) return { schema: parts[0], table: parts[1] };
|
|
3378
|
+
throw new OrmDefinitionError(
|
|
3379
|
+
`synced view physical relation must be "table" or "schema.table" (got "${physicalRelation}")`
|
|
3380
|
+
);
|
|
3381
|
+
}
|
|
3382
|
+
function syncedViewDropStatements({
|
|
3383
|
+
schemaName,
|
|
3384
|
+
tables
|
|
3385
|
+
}) {
|
|
3386
|
+
const schema = quoteIdentifier(schemaName);
|
|
3387
|
+
return tables.map(
|
|
3388
|
+
(table) => `DROP VIEW IF EXISTS ${schema}.${quoteIdentifier(table)} CASCADE`
|
|
3389
|
+
);
|
|
3390
|
+
}
|
|
3391
|
+
function syncedBackingRevokeStatements({
|
|
3392
|
+
backings,
|
|
3393
|
+
executionOwnerRole = SYNCED_VIEW_EXECUTION_OWNER_ROLE
|
|
3394
|
+
}) {
|
|
3395
|
+
const owner = quoteIdentifier(executionOwnerRole);
|
|
3396
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3397
|
+
const statements = [];
|
|
3398
|
+
for (const backing of backings) {
|
|
3399
|
+
const physical = splitPhysicalRelation(backing.physicalRelation);
|
|
3400
|
+
const physicalTarget = `${quoteIdentifier(physical.schema)}.${quoteIdentifier(physical.table)}`;
|
|
3401
|
+
if (seen.has(physicalTarget)) continue;
|
|
3402
|
+
seen.add(physicalTarget);
|
|
3403
|
+
statements.push(`REVOKE SELECT ON ${physicalTarget} FROM ${owner}`);
|
|
3404
|
+
}
|
|
3405
|
+
return statements;
|
|
3406
|
+
}
|
|
1965
3407
|
var FACTORIES = {
|
|
1966
3408
|
uuid: (name) => pgCore.uuid(name),
|
|
1967
3409
|
text: (name) => pgCore.text(name),
|
|
@@ -2155,99 +3597,16 @@ function applyMigrationHints(previous, definition) {
|
|
|
2155
3597
|
}
|
|
2156
3598
|
}
|
|
2157
3599
|
}
|
|
2158
|
-
return { statements, adjusted };
|
|
2159
|
-
}
|
|
2160
|
-
function findTable(snapshot, schema, table) {
|
|
2161
|
-
const tables = snapshot.tables ?? {};
|
|
2162
|
-
return Object.values(tables).find((entry) => entry.schema === schema && entry.name === table);
|
|
2163
|
-
}
|
|
2164
|
-
function snapshotTableNames(snapshot, schema) {
|
|
2165
|
-
const tables = snapshot.tables ?? {};
|
|
2166
|
-
return Object.values(tables).filter((entry) => entry.schema === schema).map((entry) => entry.name);
|
|
2167
|
-
}
|
|
2168
|
-
var OWNED_STATEMENT = new RegExp(
|
|
2169
|
-
[
|
|
2170
|
-
String.raw`^\s*(CREATE|DROP)\s+POLICY\b`,
|
|
2171
|
-
String.raw`^\s*(GRANT|REVOKE)\b`,
|
|
2172
|
-
String.raw`^\s*(CREATE|DROP|ALTER)\s+ROLE\b`,
|
|
2173
|
-
String.raw`^\s*(CREATE|DROP)\s+(OR\s+REPLACE\s+)?VIEW\b`,
|
|
2174
|
-
// Only ever appears in an RLS toggle (ALTER TABLE … [ENABLE|FORCE] ROW LEVEL
|
|
2175
|
-
// SECURITY) — a three-word phrase, never an identifier.
|
|
2176
|
-
String.raw`\bROW LEVEL SECURITY\b`
|
|
2177
|
-
].join("|"),
|
|
2178
|
-
"i"
|
|
2179
|
-
);
|
|
2180
|
-
function isOwnedStatement(statement) {
|
|
2181
|
-
return OWNED_STATEMENT.test(statement);
|
|
2182
|
-
}
|
|
2183
|
-
async function pendingKitStatements({
|
|
2184
|
-
definition,
|
|
2185
|
-
db
|
|
2186
|
-
}) {
|
|
2187
|
-
const { imports, schemaName } = buildDrizzleEntities(definition);
|
|
2188
|
-
const { statementsToExecute } = await withExitGuard(
|
|
2189
|
-
() => api.pushSchema(imports, shimExecute(db), [schemaName])
|
|
2190
|
-
);
|
|
2191
|
-
return statementsToExecute.filter((statement) => !OWNED_STATEMENT.test(statement));
|
|
2192
|
-
}
|
|
2193
|
-
function shimExecute(db) {
|
|
2194
|
-
const execute = db.execute.bind(db);
|
|
2195
|
-
return {
|
|
2196
|
-
execute: async (query) => {
|
|
2197
|
-
const result = await execute(query);
|
|
2198
|
-
if (result && typeof result === "object" && "rows" in result) return result;
|
|
2199
|
-
return { rows: result };
|
|
2200
|
-
}
|
|
2201
|
-
};
|
|
2202
|
-
}
|
|
2203
|
-
var exitGuardChain = Promise.resolve();
|
|
2204
|
-
function withExitGuard(run) {
|
|
2205
|
-
const next = exitGuardChain.then(() => guardedRun(run));
|
|
2206
|
-
exitGuardChain = next.then(
|
|
2207
|
-
() => void 0,
|
|
2208
|
-
() => void 0
|
|
2209
|
-
);
|
|
2210
|
-
return next;
|
|
2211
|
-
}
|
|
2212
|
-
async function guardedRun(run) {
|
|
2213
|
-
const originalExit = process.exit;
|
|
2214
|
-
process.exit = ((code) => {
|
|
2215
|
-
throw new Error(`drizzle-kit attempted process.exit(${code ?? 0}) during pushSchema introspection`);
|
|
2216
|
-
});
|
|
2217
|
-
try {
|
|
2218
|
-
return await run();
|
|
2219
|
-
} finally {
|
|
2220
|
-
process.exit = originalExit;
|
|
2221
|
-
}
|
|
3600
|
+
return { statements, adjusted };
|
|
2222
3601
|
}
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
const schema = ormSchemaName(definition.namespace);
|
|
2231
|
-
const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(definition.namespace);
|
|
2232
|
-
const hints = applyMigrationHints(previous, definition);
|
|
2233
|
-
const snapshot = generateSnapshot(definition, previousId(previous));
|
|
2234
|
-
const ddl = await diffStatements(hints.adjusted, snapshot);
|
|
2235
|
-
const policyDrops = policyDropStatements(schema, snapshotTableNames(previous, schema));
|
|
2236
|
-
const statements = [
|
|
2237
|
-
...policyDrops.map((sql3) => ({ kind: "security", sql: sql3 })),
|
|
2238
|
-
...hints.statements.map((sql3) => ({ kind: "hint", sql: sql3 })),
|
|
2239
|
-
...ddl.map((sql3) => ({ kind: "ddl", sql: sql3.replace(/;\s*$/, "") })),
|
|
2240
|
-
...securityStatements(definition).map((sql3) => ({ kind: "security", sql: sql3 })),
|
|
2241
|
-
// PREPARE each action's executes first (plpgsql bodies don't check columns
|
|
2242
|
-
// at CREATE), then create the functions. Both reference the tables the DDL
|
|
2243
|
-
// above creates, so they close the plan.
|
|
2244
|
-
...actionPrepareChecks(definition).map((sql3) => ({ kind: "function", sql: sql3 })),
|
|
2245
|
-
...actionFunctionStatements(definition).map((sql3) => ({ kind: "function", sql: sql3 }))
|
|
2246
|
-
];
|
|
2247
|
-
return { namespace: definition.namespace, schema, statements, snapshot };
|
|
3602
|
+
function findTable(snapshot, schema, table) {
|
|
3603
|
+
const tables = snapshot.tables ?? {};
|
|
3604
|
+
return Object.values(tables).find((entry) => entry.schema === schema && entry.name === table);
|
|
3605
|
+
}
|
|
3606
|
+
function snapshotTableNames(snapshot, schema) {
|
|
3607
|
+
const tables = snapshot.tables ?? {};
|
|
3608
|
+
return Object.values(tables).filter((entry) => entry.schema === schema).map((entry) => entry.name);
|
|
2248
3609
|
}
|
|
2249
|
-
var compileNamespace = (definition) => planMigration({ definition });
|
|
2250
|
-
var previousId = (snapshot) => snapshot.id;
|
|
2251
3610
|
var FACTORIES2 = {
|
|
2252
3611
|
uuid: (name) => pgCore.uuid(name),
|
|
2253
3612
|
text: (name) => pgCore.text(name),
|
|
@@ -2285,11 +3644,268 @@ function buildDrizzleEntitiesV2(definition) {
|
|
|
2285
3644
|
imports[table.table] = drizzleTable;
|
|
2286
3645
|
tables[table.table] = drizzleTable;
|
|
2287
3646
|
}
|
|
3647
|
+
for (const view of definition.views ?? []) {
|
|
3648
|
+
const columns = Object.fromEntries(
|
|
3649
|
+
view.columns.map((column) => [column.columnName, buildViewColumnV2({ view: view.view, column, enums })])
|
|
3650
|
+
);
|
|
3651
|
+
imports[view.view] = schema.view(view.view, columns).as(
|
|
3652
|
+
drizzleOrm.sql.raw(
|
|
3653
|
+
rewriteSyncedSourceRelationsInViewQuery({
|
|
3654
|
+
query: view.query,
|
|
3655
|
+
canonical: definition,
|
|
3656
|
+
schemaName,
|
|
3657
|
+
dependencies: view.dependencies
|
|
3658
|
+
})
|
|
3659
|
+
)
|
|
3660
|
+
);
|
|
3661
|
+
}
|
|
2288
3662
|
return { imports, tables, schemaName };
|
|
2289
3663
|
}
|
|
3664
|
+
function rewriteSyncedSourceRelationsInViewQuery({
|
|
3665
|
+
query,
|
|
3666
|
+
canonical,
|
|
3667
|
+
schemaName,
|
|
3668
|
+
dependencies
|
|
3669
|
+
}) {
|
|
3670
|
+
const tablesBySource = /* @__PURE__ */ new Map();
|
|
3671
|
+
for (const table of canonical.tables) {
|
|
3672
|
+
if (!table.source) continue;
|
|
3673
|
+
tablesBySource.set(table.source, [...tablesBySource.get(table.source) ?? [], table.table]);
|
|
3674
|
+
}
|
|
3675
|
+
const declared = new Set(dependencies ?? []);
|
|
3676
|
+
const boundTableBySource = /* @__PURE__ */ new Map();
|
|
3677
|
+
for (const [source, tables] of tablesBySource) {
|
|
3678
|
+
if (tables.length === 1) {
|
|
3679
|
+
boundTableBySource.set(source, tables[0]);
|
|
3680
|
+
continue;
|
|
3681
|
+
}
|
|
3682
|
+
const [sourceSchema, sourceName] = source.split(".");
|
|
3683
|
+
if (!sourceSchema || !sourceName) continue;
|
|
3684
|
+
const referenced = sqlReferencesQualifiedRelation(query, sourceSchema, sourceName);
|
|
3685
|
+
if (!referenced) continue;
|
|
3686
|
+
const declaredMatches = tables.filter((table) => declared.has(table));
|
|
3687
|
+
if (declaredMatches.length === 1) {
|
|
3688
|
+
boundTableBySource.set(source, declaredMatches[0]);
|
|
3689
|
+
continue;
|
|
3690
|
+
}
|
|
3691
|
+
throw new OrmDefinitionError(
|
|
3692
|
+
`synced source '${source}' is declared by multiple synced tables (${[...tables].sort().join(", ")}) \u2014 this view references it but its declared dependencies do not name exactly one of them, so the rewrite is ambiguous`
|
|
3693
|
+
);
|
|
3694
|
+
}
|
|
3695
|
+
return transformSqlOutsideLiterals(query, (segment) => {
|
|
3696
|
+
let rewritten = segment;
|
|
3697
|
+
for (const table of canonical.tables) {
|
|
3698
|
+
if (!table.source) continue;
|
|
3699
|
+
const boundTable = boundTableBySource.get(table.source);
|
|
3700
|
+
if (boundTable === void 0 || boundTable !== table.table) continue;
|
|
3701
|
+
const [sourceSchema, sourceName] = table.source.split(".");
|
|
3702
|
+
if (!sourceSchema || !sourceName) continue;
|
|
3703
|
+
rewritten = replaceQualifiedRelation(
|
|
3704
|
+
rewritten,
|
|
3705
|
+
sourceSchema,
|
|
3706
|
+
sourceName,
|
|
3707
|
+
schemaName,
|
|
3708
|
+
table.table
|
|
3709
|
+
);
|
|
3710
|
+
}
|
|
3711
|
+
return rewritten;
|
|
3712
|
+
});
|
|
3713
|
+
}
|
|
3714
|
+
function transformSqlOutsideLiterals(sql4, transform) {
|
|
3715
|
+
const out = [];
|
|
3716
|
+
let state = { kind: "normal" };
|
|
3717
|
+
let i = 0;
|
|
3718
|
+
let codeStart = 0;
|
|
3719
|
+
const emitCode = (end) => {
|
|
3720
|
+
if (end > codeStart) out.push(transform(sql4.slice(codeStart, end)));
|
|
3721
|
+
codeStart = end;
|
|
3722
|
+
};
|
|
3723
|
+
while (i < sql4.length) {
|
|
3724
|
+
const c = sql4[i];
|
|
3725
|
+
const next = i + 1 < sql4.length ? sql4[i + 1] : "";
|
|
3726
|
+
switch (state.kind) {
|
|
3727
|
+
case "normal": {
|
|
3728
|
+
if (c === "-" && next === "-") {
|
|
3729
|
+
emitCode(i);
|
|
3730
|
+
state = { kind: "line-comment" };
|
|
3731
|
+
out.push("--");
|
|
3732
|
+
i += 2;
|
|
3733
|
+
codeStart = i;
|
|
3734
|
+
break;
|
|
3735
|
+
}
|
|
3736
|
+
if (c === "/" && next === "*") {
|
|
3737
|
+
emitCode(i);
|
|
3738
|
+
state = { kind: "block-comment", depth: 1 };
|
|
3739
|
+
out.push("/*");
|
|
3740
|
+
i += 2;
|
|
3741
|
+
codeStart = i;
|
|
3742
|
+
break;
|
|
3743
|
+
}
|
|
3744
|
+
if ((c === "E" || c === "e") && next === "'") {
|
|
3745
|
+
const prev = i > 0 ? sql4[i - 1] : "";
|
|
3746
|
+
if (!/[A-Za-z0-9_]/.test(prev)) {
|
|
3747
|
+
emitCode(i);
|
|
3748
|
+
state = { kind: "e-single" };
|
|
3749
|
+
out.push("E'");
|
|
3750
|
+
i += 2;
|
|
3751
|
+
codeStart = i;
|
|
3752
|
+
break;
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
if (c === "'") {
|
|
3756
|
+
emitCode(i);
|
|
3757
|
+
state = { kind: "single" };
|
|
3758
|
+
out.push("'");
|
|
3759
|
+
i += 1;
|
|
3760
|
+
codeStart = i;
|
|
3761
|
+
break;
|
|
3762
|
+
}
|
|
3763
|
+
if (c === "$") {
|
|
3764
|
+
const prev = i > 0 ? sql4[i - 1] : "";
|
|
3765
|
+
if (!/[A-Za-z0-9_$]/.test(prev)) {
|
|
3766
|
+
const tagMatch = /^\$([A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sql4.slice(i));
|
|
3767
|
+
if (tagMatch) {
|
|
3768
|
+
emitCode(i);
|
|
3769
|
+
state = { kind: "dollar", tag: tagMatch[1] ?? "" };
|
|
3770
|
+
out.push(tagMatch[0]);
|
|
3771
|
+
i += tagMatch[0].length;
|
|
3772
|
+
codeStart = i;
|
|
3773
|
+
break;
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
i += 1;
|
|
3778
|
+
break;
|
|
3779
|
+
}
|
|
3780
|
+
case "single": {
|
|
3781
|
+
if (c === "'" && next === "'") {
|
|
3782
|
+
out.push("''");
|
|
3783
|
+
i += 2;
|
|
3784
|
+
break;
|
|
3785
|
+
}
|
|
3786
|
+
if (c === "'") {
|
|
3787
|
+
out.push("'");
|
|
3788
|
+
state = { kind: "normal" };
|
|
3789
|
+
i += 1;
|
|
3790
|
+
codeStart = i;
|
|
3791
|
+
break;
|
|
3792
|
+
}
|
|
3793
|
+
out.push(c);
|
|
3794
|
+
i += 1;
|
|
3795
|
+
break;
|
|
3796
|
+
}
|
|
3797
|
+
case "e-single": {
|
|
3798
|
+
if (c === "\\" && i + 1 < sql4.length) {
|
|
3799
|
+
out.push(c, sql4[i + 1]);
|
|
3800
|
+
i += 2;
|
|
3801
|
+
break;
|
|
3802
|
+
}
|
|
3803
|
+
if (c === "'" && next === "'") {
|
|
3804
|
+
out.push("''");
|
|
3805
|
+
i += 2;
|
|
3806
|
+
break;
|
|
3807
|
+
}
|
|
3808
|
+
if (c === "'") {
|
|
3809
|
+
out.push("'");
|
|
3810
|
+
state = { kind: "normal" };
|
|
3811
|
+
i += 1;
|
|
3812
|
+
codeStart = i;
|
|
3813
|
+
break;
|
|
3814
|
+
}
|
|
3815
|
+
out.push(c);
|
|
3816
|
+
i += 1;
|
|
3817
|
+
break;
|
|
3818
|
+
}
|
|
3819
|
+
case "dollar": {
|
|
3820
|
+
const closer = `$${state.tag}$`;
|
|
3821
|
+
if (sql4.startsWith(closer, i)) {
|
|
3822
|
+
out.push(closer);
|
|
3823
|
+
state = { kind: "normal" };
|
|
3824
|
+
i += closer.length;
|
|
3825
|
+
codeStart = i;
|
|
3826
|
+
break;
|
|
3827
|
+
}
|
|
3828
|
+
out.push(c);
|
|
3829
|
+
i += 1;
|
|
3830
|
+
break;
|
|
3831
|
+
}
|
|
3832
|
+
case "line-comment": {
|
|
3833
|
+
if (c === "\n") {
|
|
3834
|
+
out.push("\n");
|
|
3835
|
+
state = { kind: "normal" };
|
|
3836
|
+
i += 1;
|
|
3837
|
+
codeStart = i;
|
|
3838
|
+
break;
|
|
3839
|
+
}
|
|
3840
|
+
out.push(c);
|
|
3841
|
+
i += 1;
|
|
3842
|
+
break;
|
|
3843
|
+
}
|
|
3844
|
+
case "block-comment": {
|
|
3845
|
+
if (c === "/" && next === "*") {
|
|
3846
|
+
out.push("/*");
|
|
3847
|
+
state = { kind: "block-comment", depth: state.depth + 1 };
|
|
3848
|
+
i += 2;
|
|
3849
|
+
break;
|
|
3850
|
+
}
|
|
3851
|
+
if (c === "*" && next === "/") {
|
|
3852
|
+
out.push("*/");
|
|
3853
|
+
const newDepth = state.depth - 1;
|
|
3854
|
+
state = newDepth === 0 ? { kind: "normal" } : { kind: "block-comment", depth: newDepth };
|
|
3855
|
+
i += 2;
|
|
3856
|
+
if (state.kind === "normal") codeStart = i;
|
|
3857
|
+
break;
|
|
3858
|
+
}
|
|
3859
|
+
out.push(c);
|
|
3860
|
+
i += 1;
|
|
3861
|
+
break;
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
emitCode(sql4.length);
|
|
3866
|
+
return out.join("");
|
|
3867
|
+
}
|
|
3868
|
+
function escapeRegExp(value) {
|
|
3869
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3870
|
+
}
|
|
3871
|
+
function sqlReferencesQualifiedRelation(sql4, sourceSchema, sourceName) {
|
|
3872
|
+
let referenced = false;
|
|
3873
|
+
transformSqlOutsideLiterals(sql4, (segment) => {
|
|
3874
|
+
if (qualifiedRelationPattern(sourceSchema, sourceName).test(segment)) referenced = true;
|
|
3875
|
+
return segment;
|
|
3876
|
+
});
|
|
3877
|
+
return referenced;
|
|
3878
|
+
}
|
|
3879
|
+
function qualifiedRelationPattern(sourceSchema, sourceName) {
|
|
3880
|
+
const schemaPattern = `(?:"${escapeRegExp(sourceSchema)}"|${escapeRegExp(sourceSchema)})`;
|
|
3881
|
+
const namePattern = `(?:"${escapeRegExp(sourceName)}"|${escapeRegExp(sourceName)})`;
|
|
3882
|
+
return new RegExp(`(?<![\\w."])${schemaPattern}\\.${namePattern}(?![\\w"])`, "g");
|
|
3883
|
+
}
|
|
3884
|
+
function replaceQualifiedRelation(sql4, sourceSchema, sourceName, boundSchema, boundTable) {
|
|
3885
|
+
return sql4.replace(qualifiedRelationPattern(sourceSchema, sourceName), `"${boundSchema}"."${boundTable}"`);
|
|
3886
|
+
}
|
|
2290
3887
|
function generateSnapshotV2(definition, previousId3) {
|
|
2291
3888
|
const { imports, schemaName } = buildDrizzleEntitiesV2(definition);
|
|
2292
|
-
|
|
3889
|
+
const snapshot = api.generateDrizzleJson(imports, previousId3, [schemaName]);
|
|
3890
|
+
if ((definition.views?.length ?? 0) > 0) {
|
|
3891
|
+
const extended = snapshot;
|
|
3892
|
+
extended._sequenceOrm = {
|
|
3893
|
+
viewDependencies: Object.fromEntries(
|
|
3894
|
+
(definition.views ?? []).map((view) => [view.view, [...view.dependencies].sort()])
|
|
3895
|
+
)
|
|
3896
|
+
};
|
|
3897
|
+
}
|
|
3898
|
+
return snapshot;
|
|
3899
|
+
}
|
|
3900
|
+
function buildViewColumnV2({
|
|
3901
|
+
view,
|
|
3902
|
+
column,
|
|
3903
|
+
enums
|
|
3904
|
+
}) {
|
|
3905
|
+
const factory = resolveColumnFactory(view, column, enums);
|
|
3906
|
+
let builder = factory(column.columnName);
|
|
3907
|
+
if (!column.nullable) builder = builder.notNull();
|
|
3908
|
+
return builder;
|
|
2293
3909
|
}
|
|
2294
3910
|
function buildTable2(schema, table, built, enums, byName) {
|
|
2295
3911
|
const primaryKeyColumns = table.primaryKey ?? [];
|
|
@@ -2533,15 +4149,379 @@ async function planMigrationV2({
|
|
|
2533
4149
|
}) {
|
|
2534
4150
|
const schema = ormSchemaName(canonical.namespace);
|
|
2535
4151
|
const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
|
|
4152
|
+
assertValidViewDependencies(canonical);
|
|
2536
4153
|
const hints = applyMigrationHintsV2(previous, canonical);
|
|
2537
|
-
const snapshot = generateSnapshotV2(canonical,
|
|
2538
|
-
const
|
|
4154
|
+
const snapshot = generateSnapshotV2(canonical, previousId(previous));
|
|
4155
|
+
const baseDdl = await diffStatements(withoutViews(hints.adjusted), withoutViews(snapshot));
|
|
4156
|
+
const previousViews = snapshotViews(hints.adjusted);
|
|
4157
|
+
const currentViews = snapshotViews(snapshot);
|
|
4158
|
+
const previousDependencies = snapshotViewDependencies(hints.adjusted);
|
|
4159
|
+
const currentDependencies = snapshotViewDependencies(snapshot);
|
|
4160
|
+
const affected = affectedViewNames({
|
|
4161
|
+
previousViews,
|
|
4162
|
+
currentViews,
|
|
4163
|
+
previousDependencies,
|
|
4164
|
+
currentDependencies,
|
|
4165
|
+
baseChanged: hints.statements.length > 0 || baseDdl.length > 0
|
|
4166
|
+
});
|
|
4167
|
+
const dropOrder = topologicalViewNames(
|
|
4168
|
+
new Set([...affected].filter((name) => previousViews.has(name))),
|
|
4169
|
+
previousDependencies
|
|
4170
|
+
).reverse();
|
|
4171
|
+
const createOrder = topologicalViewNames(
|
|
4172
|
+
new Set([...affected].filter((name) => currentViews.has(name))),
|
|
4173
|
+
currentDependencies
|
|
4174
|
+
);
|
|
4175
|
+
const dropDdl = await generatedViewStatements({
|
|
4176
|
+
snapshot: hints.adjusted,
|
|
4177
|
+
names: new Set(dropOrder),
|
|
4178
|
+
mode: "drop",
|
|
4179
|
+
schema
|
|
4180
|
+
});
|
|
4181
|
+
const createDdl = await generatedViewStatements({
|
|
4182
|
+
snapshot,
|
|
4183
|
+
names: new Set(createOrder),
|
|
4184
|
+
mode: "create",
|
|
4185
|
+
schema
|
|
4186
|
+
});
|
|
4187
|
+
const currentViewDefinitions = new Map((canonical.views ?? []).map((view) => [view.view, view]));
|
|
2539
4188
|
const statements = [
|
|
4189
|
+
...dropOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(dropDdl, name, schema, "DROP") })),
|
|
2540
4190
|
...hints.statements.map((statement) => ({ kind: "hint", sql: statement })),
|
|
2541
|
-
...
|
|
4191
|
+
...baseDdl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") })),
|
|
4192
|
+
...createOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(createDdl, name, schema, "CREATE") })),
|
|
4193
|
+
...createOrder.map((name) => {
|
|
4194
|
+
const view = currentViewDefinitions.get(name);
|
|
4195
|
+
if (!view) throw new OrmDefinitionError(`missing canonical definition for view '${name}'`);
|
|
4196
|
+
return { kind: "ddl", sql: validateViewCatalogStatement({ canonical, view, schema }) };
|
|
4197
|
+
})
|
|
2542
4198
|
];
|
|
2543
4199
|
return { namespace: canonical.namespace, schema, statements, snapshot };
|
|
2544
4200
|
}
|
|
4201
|
+
function validateViewCatalogStatement({
|
|
4202
|
+
canonical,
|
|
4203
|
+
view,
|
|
4204
|
+
schema
|
|
4205
|
+
}) {
|
|
4206
|
+
const qualified = `${schema}.${view.view}`;
|
|
4207
|
+
const expectedColumns = sqlTextArray(view.columns.map((column) => column.columnName));
|
|
4208
|
+
const expectedTypes = `ARRAY[${view.columns.map((column) => `${viewTypeRegtype(column, schema)}::oid`).join(", ")}]::oid[]`;
|
|
4209
|
+
const relationIdentity = /* @__PURE__ */ new Map();
|
|
4210
|
+
for (const table of canonical.tables) {
|
|
4211
|
+
relationIdentity.set(table.table, `${schema}.${table.table}`);
|
|
4212
|
+
}
|
|
4213
|
+
for (const candidate of canonical.views ?? []) {
|
|
4214
|
+
relationIdentity.set(candidate.view, `${schema}.${candidate.view}`);
|
|
4215
|
+
}
|
|
4216
|
+
const expectedDependencies = sqlTextArray(
|
|
4217
|
+
view.dependencies.map((dependency) => {
|
|
4218
|
+
const physical = relationIdentity.get(dependency);
|
|
4219
|
+
if (!physical) throw new OrmDefinitionError(`view '${view.view}' depends on unknown relation '${dependency}'`);
|
|
4220
|
+
return physical;
|
|
4221
|
+
}).sort()
|
|
4222
|
+
);
|
|
4223
|
+
const allowedFunctions = sqlTextArray(VIEW_QUERY_FUNCTION_ALLOWLIST);
|
|
4224
|
+
return `DO $sequence_orm_validate_view$
|
|
4225
|
+
DECLARE
|
|
4226
|
+
actual_columns text[];
|
|
4227
|
+
actual_types oid[];
|
|
4228
|
+
actual_dependencies text[];
|
|
4229
|
+
BEGIN
|
|
4230
|
+
SELECT
|
|
4231
|
+
array_agg(attribute.attname ORDER BY attribute.attnum),
|
|
4232
|
+
array_agg(attribute.atttypid ORDER BY attribute.attnum)
|
|
4233
|
+
INTO actual_columns, actual_types
|
|
4234
|
+
FROM pg_catalog.pg_attribute AS attribute
|
|
4235
|
+
WHERE attribute.attrelid = pg_catalog.to_regclass(${quoteLiteral(qualified)})
|
|
4236
|
+
AND attribute.attnum > 0
|
|
4237
|
+
AND NOT attribute.attisdropped;
|
|
4238
|
+
|
|
4239
|
+
IF actual_columns IS DISTINCT FROM ${expectedColumns} THEN
|
|
4240
|
+
RAISE EXCEPTION 'managed view ${qualified} columns do not match its declaration: expected %, got %', ${expectedColumns}, actual_columns
|
|
4241
|
+
USING ERRCODE = '42804';
|
|
4242
|
+
END IF;
|
|
4243
|
+
IF actual_types IS DISTINCT FROM ${expectedTypes} THEN
|
|
4244
|
+
RAISE EXCEPTION 'managed view ${qualified} types do not match its declaration: expected %, got %', ${expectedTypes}, actual_types
|
|
4245
|
+
USING ERRCODE = '42804';
|
|
4246
|
+
END IF;
|
|
4247
|
+
|
|
4248
|
+
SELECT coalesce(array_agg(found.name ORDER BY found.name), ARRAY[]::text[])
|
|
4249
|
+
INTO actual_dependencies
|
|
4250
|
+
FROM (
|
|
4251
|
+
SELECT DISTINCT dependency_namespace.nspname || '.' || dependency_relation.relname AS name
|
|
4252
|
+
FROM pg_catalog.pg_rewrite AS rewrite
|
|
4253
|
+
JOIN pg_catalog.pg_depend AS dependency
|
|
4254
|
+
ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
|
|
4255
|
+
AND dependency.objid = rewrite.oid
|
|
4256
|
+
AND dependency.refclassid = 'pg_catalog.pg_class'::regclass
|
|
4257
|
+
JOIN pg_catalog.pg_class AS dependency_relation ON dependency_relation.oid = dependency.refobjid
|
|
4258
|
+
JOIN pg_catalog.pg_namespace AS dependency_namespace ON dependency_namespace.oid = dependency_relation.relnamespace
|
|
4259
|
+
WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
|
|
4260
|
+
AND dependency.refobjid <> rewrite.ev_class
|
|
4261
|
+
AND dependency_relation.relkind IN ('r', 'p', 'v', 'm', 'f')
|
|
4262
|
+
) AS found;
|
|
4263
|
+
|
|
4264
|
+
IF actual_dependencies IS DISTINCT FROM ${expectedDependencies} THEN
|
|
4265
|
+
RAISE EXCEPTION 'managed view ${qualified} dependencies do not match its declaration: expected %, got %', ${expectedDependencies}, actual_dependencies
|
|
4266
|
+
USING ERRCODE = '2BP01';
|
|
4267
|
+
END IF;
|
|
4268
|
+
|
|
4269
|
+
IF EXISTS (
|
|
4270
|
+
SELECT 1
|
|
4271
|
+
FROM pg_catalog.pg_rewrite AS rewrite
|
|
4272
|
+
JOIN pg_catalog.pg_depend AS dependency
|
|
4273
|
+
ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
|
|
4274
|
+
AND dependency.objid = rewrite.oid
|
|
4275
|
+
AND dependency.refclassid = 'pg_catalog.pg_proc'::regclass
|
|
4276
|
+
JOIN pg_catalog.pg_proc AS dependency_function ON dependency_function.oid = dependency.refobjid
|
|
4277
|
+
JOIN pg_catalog.pg_namespace AS function_namespace ON function_namespace.oid = dependency_function.pronamespace
|
|
4278
|
+
WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
|
|
4279
|
+
AND (
|
|
4280
|
+
function_namespace.nspname <> 'pg_catalog'
|
|
4281
|
+
OR dependency_function.proname <> ALL (${allowedFunctions})
|
|
4282
|
+
)
|
|
4283
|
+
) THEN
|
|
4284
|
+
RAISE EXCEPTION 'managed view ${qualified} may only call explicitly allowlisted side-effect-free pg_catalog functions'
|
|
4285
|
+
USING ERRCODE = '42501';
|
|
4286
|
+
END IF;
|
|
4287
|
+
|
|
4288
|
+
IF EXISTS (
|
|
4289
|
+
SELECT 1
|
|
4290
|
+
FROM pg_catalog.pg_rewrite AS rewrite
|
|
4291
|
+
JOIN pg_catalog.pg_depend AS dependency
|
|
4292
|
+
ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
|
|
4293
|
+
AND dependency.objid = rewrite.oid
|
|
4294
|
+
AND dependency.refclassid = 'pg_catalog.pg_operator'::regclass
|
|
4295
|
+
JOIN pg_catalog.pg_operator AS dependency_operator ON dependency_operator.oid = dependency.refobjid
|
|
4296
|
+
JOIN pg_catalog.pg_namespace AS operator_namespace ON operator_namespace.oid = dependency_operator.oprnamespace
|
|
4297
|
+
WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
|
|
4298
|
+
AND operator_namespace.nspname <> 'pg_catalog'
|
|
4299
|
+
) THEN
|
|
4300
|
+
RAISE EXCEPTION 'managed view ${qualified} may only use pg_catalog operators'
|
|
4301
|
+
USING ERRCODE = '42501';
|
|
4302
|
+
END IF;
|
|
4303
|
+
END
|
|
4304
|
+
$sequence_orm_validate_view$`;
|
|
4305
|
+
}
|
|
4306
|
+
function sqlTextArray(values) {
|
|
4307
|
+
return values.length === 0 ? "ARRAY[]::text[]" : `ARRAY[${values.map((value) => quoteLiteral(value)).join(", ")}]::text[]`;
|
|
4308
|
+
}
|
|
4309
|
+
function viewTypeRegtype(column, schema) {
|
|
4310
|
+
if (typeof column.type === "string") return `${quoteLiteral(sqlTypeName(column.type))}::regtype`;
|
|
4311
|
+
return `${quoteLiteral(`${quoteIdentifier(schema)}.${quoteIdentifier(column.type.enum)}`)}::regtype`;
|
|
4312
|
+
}
|
|
4313
|
+
var previousId = (snapshot) => snapshot.id;
|
|
4314
|
+
function snapshotViews(snapshot) {
|
|
4315
|
+
const views = snapshot.views ?? {};
|
|
4316
|
+
return new Map(Object.values(views).map((view) => [view.name, view]));
|
|
4317
|
+
}
|
|
4318
|
+
function snapshotViewDependencies(snapshot) {
|
|
4319
|
+
const metadata = snapshot._sequenceOrm;
|
|
4320
|
+
return metadata?.viewDependencies ?? {};
|
|
4321
|
+
}
|
|
4322
|
+
function withoutViews(snapshot) {
|
|
4323
|
+
const clone = structuredClone(snapshot);
|
|
4324
|
+
clone.views = {};
|
|
4325
|
+
delete clone._sequenceOrm;
|
|
4326
|
+
return clone;
|
|
4327
|
+
}
|
|
4328
|
+
function affectedViewNames({
|
|
4329
|
+
previousViews,
|
|
4330
|
+
currentViews,
|
|
4331
|
+
previousDependencies,
|
|
4332
|
+
currentDependencies,
|
|
4333
|
+
baseChanged
|
|
4334
|
+
}) {
|
|
4335
|
+
const allNames = /* @__PURE__ */ new Set([...previousViews.keys(), ...currentViews.keys()]);
|
|
4336
|
+
if (baseChanged) return allNames;
|
|
4337
|
+
const affected = new Set(
|
|
4338
|
+
[...allNames].filter((name) => {
|
|
4339
|
+
const before = previousViews.get(name);
|
|
4340
|
+
const after = currentViews.get(name);
|
|
4341
|
+
return before === void 0 || after === void 0 || stableJson(before) !== stableJson(after);
|
|
4342
|
+
})
|
|
4343
|
+
);
|
|
4344
|
+
let grew = true;
|
|
4345
|
+
while (grew) {
|
|
4346
|
+
grew = false;
|
|
4347
|
+
for (const name of allNames) {
|
|
4348
|
+
if (affected.has(name)) continue;
|
|
4349
|
+
const dependencies = /* @__PURE__ */ new Set([...previousDependencies[name] ?? [], ...currentDependencies[name] ?? []]);
|
|
4350
|
+
if ([...dependencies].some((dependency) => affected.has(dependency))) {
|
|
4351
|
+
affected.add(name);
|
|
4352
|
+
grew = true;
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4356
|
+
return affected;
|
|
4357
|
+
}
|
|
4358
|
+
function topologicalViewNames(names, dependencies) {
|
|
4359
|
+
const remaining = new Set(names);
|
|
4360
|
+
const ordered = [];
|
|
4361
|
+
while (remaining.size > 0) {
|
|
4362
|
+
const ready = [...remaining].filter((name) => (dependencies[name] ?? []).every((dependency) => !remaining.has(dependency))).sort();
|
|
4363
|
+
if (ready.length === 0) {
|
|
4364
|
+
throw new OrmDefinitionError(`view dependencies form a cycle among: ${[...remaining].sort().join(", ")}`);
|
|
4365
|
+
}
|
|
4366
|
+
for (const name of ready) {
|
|
4367
|
+
ordered.push(name);
|
|
4368
|
+
remaining.delete(name);
|
|
4369
|
+
}
|
|
4370
|
+
}
|
|
4371
|
+
return ordered;
|
|
4372
|
+
}
|
|
4373
|
+
async function generatedViewStatements({
|
|
4374
|
+
snapshot,
|
|
4375
|
+
names,
|
|
4376
|
+
mode,
|
|
4377
|
+
schema
|
|
4378
|
+
}) {
|
|
4379
|
+
if (names.size === 0) return /* @__PURE__ */ new Map();
|
|
4380
|
+
const without = structuredClone(snapshot);
|
|
4381
|
+
const holder = without;
|
|
4382
|
+
const views = holder.views ?? {};
|
|
4383
|
+
holder.views = Object.fromEntries(Object.entries(views).filter(([, view]) => !names.has(view.name)));
|
|
4384
|
+
const generated = mode === "drop" ? await diffStatements(snapshot, without) : await diffStatements(without, snapshot);
|
|
4385
|
+
const statements = /* @__PURE__ */ new Map();
|
|
4386
|
+
for (const name of names) {
|
|
4387
|
+
const qualified = `${quoteIdentifier(schema)}.${quoteIdentifier(name)}`;
|
|
4388
|
+
const prefix = mode === "drop" ? "DROP VIEW" : "CREATE VIEW";
|
|
4389
|
+
const statement = generated.find((candidate) => candidate.trimStart().startsWith(`${prefix} ${qualified}`));
|
|
4390
|
+
if (statement) statements.set(name, statement.replace(/;\s*$/, ""));
|
|
4391
|
+
}
|
|
4392
|
+
return statements;
|
|
4393
|
+
}
|
|
4394
|
+
function requiredViewStatement(statements, name, schema, operation) {
|
|
4395
|
+
const statement = statements.get(name);
|
|
4396
|
+
if (!statement) {
|
|
4397
|
+
throw new OrmDefinitionError(`drizzle-kit did not generate ${operation} VIEW for '${schema}.${name}'`);
|
|
4398
|
+
}
|
|
4399
|
+
return statement;
|
|
4400
|
+
}
|
|
4401
|
+
function assertValidViewDependencies(canonical) {
|
|
4402
|
+
const views = canonical.views ?? [];
|
|
4403
|
+
const known = /* @__PURE__ */ new Set([...canonical.tables.map((table) => table.table), ...views.map((view) => view.view)]);
|
|
4404
|
+
for (const view of views) {
|
|
4405
|
+
for (const dependency of view.dependencies) {
|
|
4406
|
+
if (!known.has(dependency)) {
|
|
4407
|
+
throw new OrmDefinitionError(`view '${view.view}' depends on unknown relation '${dependency}'`);
|
|
4408
|
+
}
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
topologicalViewNames(
|
|
4412
|
+
new Set(views.map((view) => view.view)),
|
|
4413
|
+
Object.fromEntries(views.map((view) => [view.view, view.dependencies]))
|
|
4414
|
+
);
|
|
4415
|
+
}
|
|
4416
|
+
|
|
4417
|
+
// src/engine/converge.ts
|
|
4418
|
+
var OWNED_STATEMENT = new RegExp(
|
|
4419
|
+
[
|
|
4420
|
+
String.raw`^\s*(CREATE|DROP)\s+POLICY\b`,
|
|
4421
|
+
String.raw`^\s*(GRANT|REVOKE)\b`,
|
|
4422
|
+
String.raw`^\s*(CREATE|DROP|ALTER)\s+ROLE\b`,
|
|
4423
|
+
String.raw`^\s*(CREATE|DROP)\s+(OR\s+REPLACE\s+)?VIEW\b`,
|
|
4424
|
+
// Only ever appears in an RLS toggle (ALTER TABLE … [ENABLE|FORCE] ROW LEVEL
|
|
4425
|
+
// SECURITY) — a three-word phrase, never an identifier.
|
|
4426
|
+
String.raw`\bROW LEVEL SECURITY\b`
|
|
4427
|
+
].join("|"),
|
|
4428
|
+
"i"
|
|
4429
|
+
);
|
|
4430
|
+
var V2_OWNED_STATEMENT = new RegExp(
|
|
4431
|
+
[
|
|
4432
|
+
String.raw`^\s*(CREATE|DROP)\s+POLICY\b`,
|
|
4433
|
+
String.raw`^\s*(GRANT|REVOKE)\b`,
|
|
4434
|
+
String.raw`^\s*(CREATE|DROP|ALTER)\s+ROLE\b`,
|
|
4435
|
+
String.raw`\bROW LEVEL SECURITY\b`
|
|
4436
|
+
].join("|"),
|
|
4437
|
+
"i"
|
|
4438
|
+
);
|
|
4439
|
+
function isOwnedStatement(statement) {
|
|
4440
|
+
return OWNED_STATEMENT.test(statement);
|
|
4441
|
+
}
|
|
4442
|
+
function isOwnedStatementV2(statement) {
|
|
4443
|
+
return V2_OWNED_STATEMENT.test(statement);
|
|
4444
|
+
}
|
|
4445
|
+
async function pendingKitStatements({
|
|
4446
|
+
definition,
|
|
4447
|
+
db
|
|
4448
|
+
}) {
|
|
4449
|
+
return pendingStatements({ entities: buildDrizzleEntities(definition), db, owned: OWNED_STATEMENT });
|
|
4450
|
+
}
|
|
4451
|
+
async function pendingKitStatementsV2({
|
|
4452
|
+
canonical,
|
|
4453
|
+
db
|
|
4454
|
+
}) {
|
|
4455
|
+
return pendingStatements({ entities: buildDrizzleEntitiesV2(canonical), db, owned: V2_OWNED_STATEMENT });
|
|
4456
|
+
}
|
|
4457
|
+
async function pendingStatements({
|
|
4458
|
+
entities,
|
|
4459
|
+
db,
|
|
4460
|
+
owned
|
|
4461
|
+
}) {
|
|
4462
|
+
const { imports, schemaName } = entities;
|
|
4463
|
+
const { statementsToExecute } = await withExitGuard(
|
|
4464
|
+
() => api.pushSchema(imports, shimExecute(db), [schemaName])
|
|
4465
|
+
);
|
|
4466
|
+
return statementsToExecute.filter((statement) => !owned.test(statement));
|
|
4467
|
+
}
|
|
4468
|
+
function shimExecute(db) {
|
|
4469
|
+
const execute = db.execute.bind(db);
|
|
4470
|
+
return {
|
|
4471
|
+
execute: async (query) => {
|
|
4472
|
+
const result = await execute(query);
|
|
4473
|
+
if (result && typeof result === "object" && "rows" in result) return result;
|
|
4474
|
+
return { rows: result };
|
|
4475
|
+
}
|
|
4476
|
+
};
|
|
4477
|
+
}
|
|
4478
|
+
var exitGuardChain = Promise.resolve();
|
|
4479
|
+
function withExitGuard(run) {
|
|
4480
|
+
const next = exitGuardChain.then(() => guardedRun(run));
|
|
4481
|
+
exitGuardChain = next.then(
|
|
4482
|
+
() => void 0,
|
|
4483
|
+
() => void 0
|
|
4484
|
+
);
|
|
4485
|
+
return next;
|
|
4486
|
+
}
|
|
4487
|
+
async function guardedRun(run) {
|
|
4488
|
+
const originalExit = process.exit;
|
|
4489
|
+
process.exit = ((code) => {
|
|
4490
|
+
throw new Error(`drizzle-kit attempted process.exit(${code ?? 0}) during pushSchema introspection`);
|
|
4491
|
+
});
|
|
4492
|
+
try {
|
|
4493
|
+
return await run();
|
|
4494
|
+
} finally {
|
|
4495
|
+
process.exit = originalExit;
|
|
4496
|
+
}
|
|
4497
|
+
}
|
|
4498
|
+
|
|
4499
|
+
// src/engine/index.ts
|
|
4500
|
+
var STATEMENT_KINDS = ["hint", "ddl", "security", "function", "synced_view"];
|
|
4501
|
+
async function planMigration({
|
|
4502
|
+
definition,
|
|
4503
|
+
previousSnapshot
|
|
4504
|
+
}) {
|
|
4505
|
+
const schema = ormSchemaName(definition.namespace);
|
|
4506
|
+
const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(definition.namespace);
|
|
4507
|
+
const hints = applyMigrationHints(previous, definition);
|
|
4508
|
+
const snapshot = generateSnapshot(definition, previousId2(previous));
|
|
4509
|
+
const ddl = await diffStatements(hints.adjusted, snapshot);
|
|
4510
|
+
const policyDrops = policyDropStatements(schema, snapshotTableNames(previous, schema));
|
|
4511
|
+
const statements = [
|
|
4512
|
+
...policyDrops.map((sql4) => ({ kind: "security", sql: sql4 })),
|
|
4513
|
+
...hints.statements.map((sql4) => ({ kind: "hint", sql: sql4 })),
|
|
4514
|
+
...ddl.map((sql4) => ({ kind: "ddl", sql: sql4.replace(/;\s*$/, "") })),
|
|
4515
|
+
...securityStatements(definition).map((sql4) => ({ kind: "security", sql: sql4 })),
|
|
4516
|
+
// PREPARE each action's executes first (plpgsql bodies don't check columns
|
|
4517
|
+
// at CREATE), then create the functions. Both reference the tables the DDL
|
|
4518
|
+
// above creates, so they close the plan.
|
|
4519
|
+
...actionPrepareChecks(definition).map((sql4) => ({ kind: "function", sql: sql4 })),
|
|
4520
|
+
...actionFunctionStatements(definition).map((sql4) => ({ kind: "function", sql: sql4 }))
|
|
4521
|
+
];
|
|
4522
|
+
return { namespace: definition.namespace, schema, statements, snapshot };
|
|
4523
|
+
}
|
|
4524
|
+
var compileNamespace = (definition) => planMigration({ definition });
|
|
2545
4525
|
var previousId2 = (snapshot) => snapshot.id;
|
|
2546
4526
|
var IR_VERSION = 1;
|
|
2547
4527
|
var CLAIMS_CONTRACT = {
|
|
@@ -2680,9 +4660,23 @@ var onConflictIrSchema = zod.z.union([
|
|
|
2680
4660
|
]);
|
|
2681
4661
|
var mutationRootSchema = zod.z.object({
|
|
2682
4662
|
kind: zod.z.literal("mutation"),
|
|
2683
|
-
|
|
4663
|
+
/**
|
|
4664
|
+
* CRUD verbs plus the structured copy-on-write ops. `cowOverride` copies the
|
|
4665
|
+
* synced row into the overlay (INSERT…SELECT, ON CONFLICT DO NOTHING) then
|
|
4666
|
+
* applies `set` — both statements generated by the executor from this
|
|
4667
|
+
* metadata, never authored SQL. `cowRevert` deletes the override so the
|
|
4668
|
+
* coalesce view falls back to synced. Both are gated on a matching synced
|
|
4669
|
+
* row and carry the write policy in `where` like update/delete.
|
|
4670
|
+
*/
|
|
4671
|
+
op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
|
|
2684
4672
|
alias: zod.z.string(),
|
|
2685
4673
|
table: zod.z.string(),
|
|
4674
|
+
/** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
|
|
4675
|
+
cowSynced: zod.z.string().optional(),
|
|
4676
|
+
/** cowOverride: logical properties copied INSERT…SELECT from synced (pk included). */
|
|
4677
|
+
cowCopyColumns: zod.z.array(zod.z.string()).optional(),
|
|
4678
|
+
/** cowOverride: insert-policy WITH CHECK evaluated over the copied rows. */
|
|
4679
|
+
cowCopyCheck: predIrSchema.optional(),
|
|
2686
4680
|
/** insert/insertOne: rows come from a slot ($object/$objects) or literals. */
|
|
2687
4681
|
objects: zod.z.union([
|
|
2688
4682
|
zod.z.object({ slot: zod.z.string() }),
|
|
@@ -2703,19 +4697,11 @@ var mutationRootSchema = zod.z.object({
|
|
|
2703
4697
|
affectedRows: zod.z.boolean().default(false),
|
|
2704
4698
|
includeIf: includeIfSchema.optional()
|
|
2705
4699
|
});
|
|
2706
|
-
var actionRootSchema = zod.z.object({
|
|
2707
|
-
kind: zod.z.literal("action"),
|
|
2708
|
-
alias: zod.z.string(),
|
|
2709
|
-
name: zod.z.string(),
|
|
2710
|
-
params: zod.z.array(zod.z.object({ name: zod.z.string(), value: valueIrSchema })),
|
|
2711
|
-
returns: zod.z.enum(["rows", "rowsAffected", "scalar"])
|
|
2712
|
-
});
|
|
2713
4700
|
var rootIrSchema = zod.z.discriminatedUnion("kind", [
|
|
2714
4701
|
selectRootSchema,
|
|
2715
4702
|
byPkRootSchema,
|
|
2716
4703
|
aggregateRootSchema,
|
|
2717
|
-
mutationRootSchema
|
|
2718
|
-
actionRootSchema
|
|
4704
|
+
mutationRootSchema
|
|
2719
4705
|
]);
|
|
2720
4706
|
var INPUT_REF_KINDS = [
|
|
2721
4707
|
"boolExp",
|
|
@@ -2747,7 +4733,7 @@ var compiledOperationSchema = zod.z.object({
|
|
|
2747
4733
|
irVersion: zod.z.literal(IR_VERSION),
|
|
2748
4734
|
name: zod.z.string(),
|
|
2749
4735
|
opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
2750
|
-
kind: zod.z.enum(["query", "mutation"
|
|
4736
|
+
kind: zod.z.enum(["query", "mutation"]),
|
|
2751
4737
|
requires: zod.z.object({
|
|
2752
4738
|
capability: zod.z.string().optional(),
|
|
2753
4739
|
role: zod.z.string().optional()
|
|
@@ -2761,13 +4747,24 @@ var tableMetaIrSchema = zod.z.object({
|
|
|
2761
4747
|
table: zod.z.string(),
|
|
2762
4748
|
schema: zod.z.string(),
|
|
2763
4749
|
typename: zod.z.string(),
|
|
4750
|
+
/**
|
|
4751
|
+
* True when statements over this relation evaluate a claims-gated synced
|
|
4752
|
+
* view: synced twins (`source: gold.*`) materialize as views whose baked
|
|
4753
|
+
* policy reads `current_setting('request.jwt.claims')`, and namespace views
|
|
4754
|
+
* inherit the gate transitively through their declared dependencies. The
|
|
4755
|
+
* executor must stamp that GUC (transaction-scoped) before any statement
|
|
4756
|
+
* touching such a relation, or the synced arm yields zero rows regardless
|
|
4757
|
+
* of the compiled predicates.
|
|
4758
|
+
*/
|
|
4759
|
+
claimsGated: zod.z.boolean().default(false),
|
|
2764
4760
|
columns: zod.z.array(zod.z.object({
|
|
2765
4761
|
name: zod.z.string(),
|
|
2766
4762
|
columnName: zod.z.string(),
|
|
2767
4763
|
type: zod.z.union([zod.z.enum(COLUMN_TYPES), zod.z.object({ enum: zod.z.string(), values: zod.z.array(zod.z.string()) })]),
|
|
2768
4764
|
nullable: zod.z.boolean()
|
|
2769
4765
|
})),
|
|
2770
|
-
|
|
4766
|
+
/** Empty for keyless read-only views; keyed tables and views list logical columns. */
|
|
4767
|
+
primaryKey: zod.z.array(zod.z.string()),
|
|
2771
4768
|
links: zod.z.array(zod.z.object({
|
|
2772
4769
|
name: zod.z.string(),
|
|
2773
4770
|
kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
|
|
@@ -2934,6 +4931,8 @@ function validateCapabilityRefs(files, declared) {
|
|
|
2934
4931
|
const referenced = [
|
|
2935
4932
|
...table.policies?.read ? referencedCapabilities(table.policies.read.ast) : [],
|
|
2936
4933
|
...table.policies?.write ? referencedCapabilities(table.policies.write.ast) : [],
|
|
4934
|
+
...table.policies?.insert ? referencedCapabilities(table.policies.insert.ast) : [],
|
|
4935
|
+
...table.policies?.update ? referencedCapabilities(table.policies.update.ast) : [],
|
|
2937
4936
|
...Object.values(table.actions ?? {}).flatMap((action) => [
|
|
2938
4937
|
...action.requires ? referencedCapabilities(action.requires.ast) : [],
|
|
2939
4938
|
...referencedExecuteCapabilities(action)
|
|
@@ -3091,7 +5090,7 @@ async function readMigrationSql(dir, entry) {
|
|
|
3091
5090
|
throw new OrmDefinitionError("missing migration file", path$1);
|
|
3092
5091
|
});
|
|
3093
5092
|
}
|
|
3094
|
-
var checksumOf = (
|
|
5093
|
+
var checksumOf = (text4) => crypto.createHash("sha256").update(text4).digest("hex");
|
|
3095
5094
|
var migrationName = (ordinal, contentHash) => `${pad(ordinal)}_${contentHash.slice(0, 12)}`;
|
|
3096
5095
|
function renderMigrationSql({
|
|
3097
5096
|
name,
|
|
@@ -3117,12 +5116,12 @@ async function writeMigration({
|
|
|
3117
5116
|
const tip = workspace.journal.entries.at(-1);
|
|
3118
5117
|
const ordinal = (tip?.ordinal ?? 0) + 1;
|
|
3119
5118
|
const name = migrationName(ordinal, contentHash);
|
|
3120
|
-
const
|
|
5119
|
+
const sql4 = renderMigrationSql({ name, namespace: workspace.journal.namespace, contentHash, plan });
|
|
3121
5120
|
const metaDir = path.join(dir, MIGRATIONS_DIRNAME, META_DIRNAME);
|
|
3122
5121
|
await promises.mkdir(metaDir, { recursive: true });
|
|
3123
5122
|
const snapshotText = `${JSON.stringify(plan.snapshot, null, 2)}
|
|
3124
5123
|
`;
|
|
3125
|
-
await promises.writeFile(path.join(dir, MIGRATIONS_DIRNAME, `${name}.sql`),
|
|
5124
|
+
await promises.writeFile(path.join(dir, MIGRATIONS_DIRNAME, `${name}.sql`), sql4);
|
|
3126
5125
|
await promises.writeFile(path.join(metaDir, `${pad(ordinal)}.snapshot.json`), snapshotText);
|
|
3127
5126
|
const journal = {
|
|
3128
5127
|
...workspace.journal,
|
|
@@ -3133,7 +5132,7 @@ async function writeMigration({
|
|
|
3133
5132
|
name,
|
|
3134
5133
|
content_hash: contentHash,
|
|
3135
5134
|
previous_content_hash: tip?.content_hash ?? null,
|
|
3136
|
-
checksum: checksumOf(
|
|
5135
|
+
checksum: checksumOf(sql4),
|
|
3137
5136
|
snapshot_checksum: checksumOf(snapshotText),
|
|
3138
5137
|
destructive
|
|
3139
5138
|
}
|
|
@@ -3248,8 +5247,8 @@ async function checkWorkspace({
|
|
|
3248
5247
|
let previousId3;
|
|
3249
5248
|
let previousHash = null;
|
|
3250
5249
|
for (const entry of workspace.journal.entries) {
|
|
3251
|
-
const
|
|
3252
|
-
if (checksumOf(
|
|
5250
|
+
const sql4 = await readMigrationSql(dir, entry);
|
|
5251
|
+
if (checksumOf(sql4) !== entry.checksum) {
|
|
3253
5252
|
log(`${MIGRATIONS_DIRNAME}/${entry.name}.sql does not match its journal checksum \u2014 committed migrations must not be edited`);
|
|
3254
5253
|
return 1;
|
|
3255
5254
|
}
|
|
@@ -3289,7 +5288,7 @@ async function checkWorkspace({
|
|
|
3289
5288
|
log(`migrations in sync \u2014 ${workspace.journal.entries.length} committed, tip ${tip.name}`);
|
|
3290
5289
|
return 0;
|
|
3291
5290
|
}
|
|
3292
|
-
var firstLine = (
|
|
5291
|
+
var firstLine = (sql4) => sql4.split("\n")[0] ?? sql4;
|
|
3293
5292
|
|
|
3294
5293
|
// src/migrations/from-yaml.ts
|
|
3295
5294
|
function convertV1DefinitionToCanonical(definition) {
|
|
@@ -3321,7 +5320,7 @@ function convertTable(table) {
|
|
|
3321
5320
|
description: table.description,
|
|
3322
5321
|
source: table.source,
|
|
3323
5322
|
columns: columns.map(([name, column]) => convertColumn(name, column)),
|
|
3324
|
-
// Synced tables
|
|
5323
|
+
// Synced tables may declare a typed column/PK contract; untyped remain empty.
|
|
3325
5324
|
primaryKey: primaryKey2.length > 0 ? primaryKey2 : void 0,
|
|
3326
5325
|
links: Object.entries(table.links ?? {}).map(([name, link]) => convertLink(table.table, name, link)),
|
|
3327
5326
|
indexes: (table.indexes ?? []).map((index3) => ({
|
|
@@ -3334,7 +5333,10 @@ function convertTable(table) {
|
|
|
3334
5333
|
read: table.policies?.read?.source,
|
|
3335
5334
|
write: table.policies?.write?.source
|
|
3336
5335
|
},
|
|
3337
|
-
|
|
5336
|
+
// v2 has no action surface (writes are mutations); the transition sweep
|
|
5337
|
+
// drops the plpgsql action_* functions, so carrying the metadata would be
|
|
5338
|
+
// dead weight. droppedV1Actions() surfaces them for the emitter's TODO.
|
|
5339
|
+
actions: []
|
|
3338
5340
|
};
|
|
3339
5341
|
}
|
|
3340
5342
|
function convertColumn(name, column) {
|
|
@@ -3370,25 +5372,6 @@ function convertLink(table, name, link) {
|
|
|
3370
5372
|
description: link.description
|
|
3371
5373
|
};
|
|
3372
5374
|
}
|
|
3373
|
-
function convertAction(table, name, action) {
|
|
3374
|
-
return {
|
|
3375
|
-
name,
|
|
3376
|
-
requires: action.requires?.source,
|
|
3377
|
-
params: Object.fromEntries(
|
|
3378
|
-
Object.entries(action.params ?? {}).map(([param, definition]) => [
|
|
3379
|
-
param,
|
|
3380
|
-
{ type: definition.type, nullable: definition.optional === true }
|
|
3381
|
-
])
|
|
3382
|
-
),
|
|
3383
|
-
// The v2 contract carries one SQL string; statement boundaries survive as
|
|
3384
|
-
// `;\n` (v1 validation guarantees no bare `;` inside a single statement).
|
|
3385
|
-
executes: actionStatementSources(action.executes).join(";\n"),
|
|
3386
|
-
// v1 declared the RETURNING columns inline; v2 types them at compile time,
|
|
3387
|
-
// so the shape collapses to rows-vs-rowsAffected.
|
|
3388
|
-
returns: action.returns ? "rows" : "rowsAffected",
|
|
3389
|
-
audit: action.audit ? { action: action.audit.action, resource: action.audit.resource ?? table } : void 0
|
|
3390
|
-
};
|
|
3391
|
-
}
|
|
3392
5375
|
|
|
3393
5376
|
// src/migrations/v2.ts
|
|
3394
5377
|
var SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
@@ -3411,7 +5394,7 @@ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, sn
|
|
|
3411
5394
|
throw new OrmDefinitionError(`rebase ordinal must be an integer \u2265 2 (a rebase sits on an existing journal tip); got ${ordinal}`);
|
|
3412
5395
|
}
|
|
3413
5396
|
const name = migrationName(ordinal, newHash);
|
|
3414
|
-
const
|
|
5397
|
+
const sql4 = renderRebaseMigrationSql({ name, namespace, previousHash, newHash });
|
|
3415
5398
|
const snapshotText = `${JSON.stringify(snapshot, null, 2)}
|
|
3416
5399
|
`;
|
|
3417
5400
|
const entry = {
|
|
@@ -3419,11 +5402,11 @@ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, sn
|
|
|
3419
5402
|
name,
|
|
3420
5403
|
content_hash: newHash,
|
|
3421
5404
|
previous_content_hash: previousHash,
|
|
3422
|
-
checksum: checksumOf(
|
|
5405
|
+
checksum: checksumOf(sql4),
|
|
3423
5406
|
snapshot_checksum: checksumOf(snapshotText),
|
|
3424
5407
|
destructive: false
|
|
3425
5408
|
};
|
|
3426
|
-
return { name, entry, sql:
|
|
5409
|
+
return { name, entry, sql: sql4, snapshotText };
|
|
3427
5410
|
}
|
|
3428
5411
|
function renderRebaseMigrationSql({
|
|
3429
5412
|
name,
|
|
@@ -3496,8 +5479,9 @@ function canonicalPolicies(value) {
|
|
|
3496
5479
|
if (value === null || typeof value !== "object") return void 0;
|
|
3497
5480
|
const record = value;
|
|
3498
5481
|
const policies = {};
|
|
3499
|
-
|
|
3500
|
-
|
|
5482
|
+
for (const kind of ["read", "write", "insert", "update"]) {
|
|
5483
|
+
if (typeof record[kind] === "string") policies[kind] = record[kind];
|
|
5484
|
+
}
|
|
3501
5485
|
return Object.keys(policies).length > 0 ? policies : void 0;
|
|
3502
5486
|
}
|
|
3503
5487
|
function canonicalActions(value) {
|
|
@@ -3575,15 +5559,20 @@ exports.NAMESPACE_PATTERN = NAMESPACE_PATTERN;
|
|
|
3575
5559
|
exports.ORM_DATA_TOKEN_HEADER = ORM_DATA_TOKEN_HEADER;
|
|
3576
5560
|
exports.ORM_SCHEMA_PREFIX = ORM_SCHEMA_PREFIX;
|
|
3577
5561
|
exports.ORM_SERVICE_DATA_TOKEN_HEADER = ORM_SERVICE_DATA_TOKEN_HEADER;
|
|
5562
|
+
exports.ORM_TO_PIPELINE_TYPE = ORM_TO_PIPELINE_TYPE;
|
|
3578
5563
|
exports.OrmDefinitionError = OrmDefinitionError;
|
|
3579
5564
|
exports.PredicateParseError = PredicateParseError;
|
|
3580
5565
|
exports.QUALIFIED_TABLE_PATTERN = QUALIFIED_TABLE_PATTERN;
|
|
3581
5566
|
exports.RESERVED_COLUMN_NAMES = RESERVED_COLUMN_NAMES;
|
|
3582
5567
|
exports.RESERVED_TABLE_NAMES = RESERVED_TABLE_NAMES;
|
|
5568
|
+
exports.ROLES_CLAIM_KEY = ROLES_CLAIM_KEY;
|
|
3583
5569
|
exports.SCHEMA_DIR = SCHEMA_DIR;
|
|
3584
5570
|
exports.STATEMENT_KINDS = STATEMENT_KINDS;
|
|
5571
|
+
exports.SYNCED_CONTRACT_HASH_DOMAIN = SYNCED_CONTRACT_HASH_DOMAIN;
|
|
3585
5572
|
exports.SYNCED_SOURCE_PATTERN = SYNCED_SOURCE_PATTERN;
|
|
5573
|
+
exports.SYNCED_VIEW_EXECUTION_OWNER_ROLE = SYNCED_VIEW_EXECUTION_OWNER_ROLE;
|
|
3586
5574
|
exports.TABLE_BRAND = TABLE_BRAND;
|
|
5575
|
+
exports.VIEW_BRAND = VIEW_BRAND;
|
|
3587
5576
|
exports.actionFunctionName = actionFunctionName;
|
|
3588
5577
|
exports.actionFunctionStatements = actionFunctionStatements;
|
|
3589
5578
|
exports.actionParamArg = actionParamArg;
|
|
@@ -3609,7 +5598,9 @@ exports.canonicalEnumSchema = canonicalEnumSchema;
|
|
|
3609
5598
|
exports.canonicalIndexSchema = canonicalIndexSchema;
|
|
3610
5599
|
exports.canonicalLinkSchema = canonicalLinkSchema;
|
|
3611
5600
|
exports.canonicalPoliciesSchema = canonicalPoliciesSchema;
|
|
5601
|
+
exports.canonicalRelations = canonicalRelations;
|
|
3612
5602
|
exports.canonicalTableSchema = canonicalTableSchema;
|
|
5603
|
+
exports.canonicalViewSchema = canonicalViewSchema;
|
|
3613
5604
|
exports.canonicalizeDefinition = canonicalizeDefinition;
|
|
3614
5605
|
exports.capabilityExpression = capabilityExpression;
|
|
3615
5606
|
exports.castParts = castParts;
|
|
@@ -3621,11 +5612,13 @@ exports.columnTypeSchema = columnTypeSchema2;
|
|
|
3621
5612
|
exports.compileActionRequires = compileActionRequires;
|
|
3622
5613
|
exports.compileActionStatements = compileActionStatements;
|
|
3623
5614
|
exports.compileNamespace = compileNamespace;
|
|
5615
|
+
exports.compilePredIrToSql = compilePredIrToSql;
|
|
3624
5616
|
exports.compilePredicate = compilePredicate;
|
|
3625
5617
|
exports.compiledArtifactSchema = compiledArtifactSchema;
|
|
3626
5618
|
exports.compiledOperationSchema = compiledOperationSchema;
|
|
3627
5619
|
exports.computeCanonicalHash = computeCanonicalHash;
|
|
3628
5620
|
exports.computeDefinitionHash = computeDefinitionHash;
|
|
5621
|
+
exports.computeSyncedContractHash = computeSyncedContractHash;
|
|
3629
5622
|
exports.consumerRoleName = consumerRoleName;
|
|
3630
5623
|
exports.convertV1DefinitionToCanonical = convertV1DefinitionToCanonical;
|
|
3631
5624
|
exports.declaredCapabilityKeys = declaredCapabilityKeys;
|
|
@@ -3633,6 +5626,7 @@ exports.defineEnum = defineEnum;
|
|
|
3633
5626
|
exports.defineNamespace = defineNamespace;
|
|
3634
5627
|
exports.defineSyncedTable = defineSyncedTable;
|
|
3635
5628
|
exports.defineTable = defineTable;
|
|
5629
|
+
exports.defineView = defineView;
|
|
3636
5630
|
exports.deriveColumnName = deriveColumnName;
|
|
3637
5631
|
exports.diffCommand = diffCommand;
|
|
3638
5632
|
exports.diffStatements = diffStatements;
|
|
@@ -3640,6 +5634,7 @@ exports.emptyJournal = emptyJournal;
|
|
|
3640
5634
|
exports.emptySnapshot = emptySnapshot;
|
|
3641
5635
|
exports.endAuthoringContext = endAuthoringContext;
|
|
3642
5636
|
exports.extractNamedParams = extractNamedParams;
|
|
5637
|
+
exports.extractOrmSyncedTableContract = extractOrmSyncedTableContract;
|
|
3643
5638
|
exports.generateNamespaceTypes = generateNamespaceTypes;
|
|
3644
5639
|
exports.generateSnapshot = generateSnapshot;
|
|
3645
5640
|
exports.generateSnapshotV2 = generateSnapshotV2;
|
|
@@ -3649,10 +5644,12 @@ exports.isColumnBuilder = isColumnBuilder;
|
|
|
3649
5644
|
exports.isEnumRef = isEnumRef;
|
|
3650
5645
|
exports.isFunctionDefault = isFunctionDefault;
|
|
3651
5646
|
exports.isOwnedStatement = isOwnedStatement;
|
|
5647
|
+
exports.isOwnedStatementV2 = isOwnedStatementV2;
|
|
3652
5648
|
exports.isQualifiedReference = isQualifiedReference;
|
|
3653
5649
|
exports.isSeqLink = isSeqLink;
|
|
3654
5650
|
exports.isSeqNamespaceConfig = isSeqNamespaceConfig;
|
|
3655
5651
|
exports.isSeqTable = isSeqTable;
|
|
5652
|
+
exports.isSeqView = isSeqView;
|
|
3656
5653
|
exports.limitIrSchema = limitIrSchema;
|
|
3657
5654
|
exports.loadCommittedMigrations = loadCommittedMigrations;
|
|
3658
5655
|
exports.loadCommittedTipMigration = loadCommittedTipMigration;
|
|
@@ -3666,12 +5663,15 @@ exports.normalizeIr = normalizeIr;
|
|
|
3666
5663
|
exports.normalizeSnapshot = normalizeSnapshot;
|
|
3667
5664
|
exports.orderIrSchema = orderIrSchema;
|
|
3668
5665
|
exports.ormSchemaName = ormSchemaName;
|
|
5666
|
+
exports.overlayWinsUnion = overlayWinsUnion;
|
|
5667
|
+
exports.overrideRowMutation = overrideRowMutation;
|
|
3669
5668
|
exports.parseCanonicalDefinition = parseCanonicalDefinition;
|
|
3670
5669
|
exports.parseDefinitionSources = parseDefinitionSources;
|
|
3671
5670
|
exports.parsePredicate = parsePredicate;
|
|
3672
5671
|
exports.parseTableFile = parseTableFile;
|
|
3673
5672
|
exports.parsedPredicateSchema = parsedPredicateSchema;
|
|
3674
5673
|
exports.pendingKitStatements = pendingKitStatements;
|
|
5674
|
+
exports.pendingKitStatementsV2 = pendingKitStatementsV2;
|
|
3675
5675
|
exports.planMigration = planMigration;
|
|
3676
5676
|
exports.planMigrationV2 = planMigrationV2;
|
|
3677
5677
|
exports.policyExpressionSchema = policyExpressionSchema;
|
|
@@ -3687,12 +5687,20 @@ exports.rebaseViolations = rebaseViolations;
|
|
|
3687
5687
|
exports.referencedCapabilities = referencedCapabilities;
|
|
3688
5688
|
exports.referencedColumns = referencedColumns;
|
|
3689
5689
|
exports.renderMigrationSql = renderMigrationSql;
|
|
5690
|
+
exports.revertOverlayMutation = revertOverlayMutation;
|
|
5691
|
+
exports.rewriteSyncedSourceRelationsInViewQuery = rewriteSyncedSourceRelationsInViewQuery;
|
|
5692
|
+
exports.roleExpression = roleExpression;
|
|
3690
5693
|
exports.rootIrSchema = rootIrSchema;
|
|
3691
5694
|
exports.securityStatements = securityStatements;
|
|
3692
5695
|
exports.selectionIrSchema = selectionIrSchema;
|
|
3693
5696
|
exports.serializeNamespace = serializeNamespace;
|
|
3694
5697
|
exports.sqlTypeName = sqlTypeName;
|
|
3695
5698
|
exports.stableJson = stableJson;
|
|
5699
|
+
exports.syncedBackingRevokeStatements = syncedBackingRevokeStatements;
|
|
5700
|
+
exports.syncedContractSurface = syncedContractSurface;
|
|
5701
|
+
exports.syncedTables = syncedTables;
|
|
5702
|
+
exports.syncedViewDropStatements = syncedViewDropStatements;
|
|
5703
|
+
exports.syncedViewStatements = syncedViewStatements;
|
|
3696
5704
|
exports.t = t;
|
|
3697
5705
|
exports.tableFileSchema = tableFileSchema;
|
|
3698
5706
|
exports.tableMetaIrSchema = tableMetaIrSchema;
|
|
@@ -3701,6 +5709,7 @@ exports.toMany = toMany;
|
|
|
3701
5709
|
exports.toOne = toOne;
|
|
3702
5710
|
exports.toPlatformUser = toPlatformUser;
|
|
3703
5711
|
exports.typesInterfaceName = typesInterfaceName;
|
|
5712
|
+
exports.validateViewCatalogStatement = validateViewCatalogStatement;
|
|
3704
5713
|
exports.valueIrSchema = valueIrSchema;
|
|
3705
5714
|
exports.variableDefaultIrSchema = variableDefaultIrSchema;
|
|
3706
5715
|
exports.variableIrSchema = variableIrSchema;
|