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