@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.
Files changed (65) hide show
  1. package/README.md +11 -1
  2. package/dist/authoring.cjs +1066 -59
  3. package/dist/authoring.cjs.map +1 -1
  4. package/dist/authoring.d.cts +289 -24
  5. package/dist/authoring.d.ts +289 -24
  6. package/dist/authoring.js +1056 -60
  7. package/dist/authoring.js.map +1 -1
  8. package/dist/canonical.cjs +415 -9
  9. package/dist/canonical.cjs.map +1 -1
  10. package/dist/canonical.d.cts +235 -1
  11. package/dist/canonical.d.ts +235 -1
  12. package/dist/canonical.js +411 -10
  13. package/dist/canonical.js.map +1 -1
  14. package/dist/cli.cjs +1764 -225
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +1765 -226
  17. package/dist/cli.js.map +1 -1
  18. package/dist/client.cjs.map +1 -1
  19. package/dist/client.d.cts +1 -1
  20. package/dist/client.d.ts +1 -1
  21. package/dist/client.js.map +1 -1
  22. package/dist/compiler.cjs +506 -54
  23. package/dist/compiler.cjs.map +1 -1
  24. package/dist/compiler.d.cts +39 -231
  25. package/dist/compiler.d.ts +39 -231
  26. package/dist/compiler.js +504 -55
  27. package/dist/compiler.js.map +1 -1
  28. package/dist/drizzle.cjs +14 -0
  29. package/dist/drizzle.cjs.map +1 -0
  30. package/dist/drizzle.d.cts +1 -0
  31. package/dist/drizzle.d.ts +1 -0
  32. package/dist/drizzle.js +3 -0
  33. package/dist/drizzle.js.map +1 -0
  34. package/dist/dynamic-input.cjs +391 -0
  35. package/dist/dynamic-input.cjs.map +1 -0
  36. package/dist/dynamic-input.d.cts +111 -0
  37. package/dist/dynamic-input.d.ts +111 -0
  38. package/dist/dynamic-input.js +380 -0
  39. package/dist/dynamic-input.js.map +1 -0
  40. package/dist/index.cjs +2223 -219
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.d.cts +125 -9
  43. package/dist/index.d.ts +125 -9
  44. package/dist/index.js +2199 -220
  45. package/dist/index.js.map +1 -1
  46. package/dist/ir.cjs +249 -14
  47. package/dist/ir.cjs.map +1 -1
  48. package/dist/ir.d.cts +20 -80
  49. package/dist/ir.d.ts +20 -80
  50. package/dist/ir.js +249 -14
  51. package/dist/ir.js.map +1 -1
  52. package/dist/predicate-v2.cjs +1093 -0
  53. package/dist/predicate-v2.cjs.map +1 -0
  54. package/dist/predicate-v2.d.cts +139 -0
  55. package/dist/predicate-v2.d.ts +139 -0
  56. package/dist/predicate-v2.js +1083 -0
  57. package/dist/predicate-v2.js.map +1 -0
  58. package/dist/sdl.cjs +344 -55
  59. package/dist/sdl.cjs.map +1 -1
  60. package/dist/sdl.d.cts +1 -3
  61. package/dist/sdl.d.ts +1 -3
  62. package/dist/sdl.js +344 -55
  63. package/dist/sdl.js.map +1 -1
  64. package/package.json +16 -1
  65. package/templates/orm-namespace/CLAUDE.md +6 -5
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ var drizzleOrm = require('drizzle-orm');
4
+ var pgCore = require('drizzle-orm/pg-core');
3
5
  var crypto = require('crypto');
4
6
  var zod = require('zod');
5
7
  var jiti = require('jiti');
@@ -66,8 +68,8 @@ var ColumnBuilder = class {
66
68
  this.state.defaultFn = void 0;
67
69
  return this;
68
70
  }
69
- description(text) {
70
- this.state.description = text;
71
+ description(text2) {
72
+ this.state.description = text2;
71
73
  return this;
72
74
  }
73
75
  /** Migration rename hint: the previous Postgres column name. */
@@ -162,7 +164,7 @@ function contextStack() {
162
164
  return created;
163
165
  }
164
166
  function beginAuthoringContext() {
165
- const context = { currentFile: void 0, tables: [], enums: [], configs: [] };
167
+ const context = { currentFile: void 0, tables: [], views: [], enums: [], configs: [] };
166
168
  contextStack().push(context);
167
169
  return context;
168
170
  }
@@ -186,6 +188,9 @@ function register(bucket, value) {
186
188
  function registerTable(value) {
187
189
  register("tables", value);
188
190
  }
191
+ function registerView(value) {
192
+ register("views", value);
193
+ }
189
194
  function registerEnum(value) {
190
195
  register("enums", value);
191
196
  }
@@ -273,20 +278,69 @@ function defineTable(name, columns, config = {}) {
273
278
  registerTable(table);
274
279
  return table;
275
280
  }
276
- function defineSyncedTable(name, config) {
281
+ function defineSyncedTable(name, columnsOrConfig, maybeConfig) {
277
282
  const file = activeAuthoringContext()?.currentFile;
278
283
  assertIdentifier({ value: name, what: "table name", file });
284
+ const isTyped = maybeConfig !== void 0;
285
+ const columns = isTyped ? columnsOrConfig : {};
286
+ const config = isTyped ? maybeConfig : columnsOrConfig;
279
287
  if (!SYNCED_SOURCE_PATTERN.test(config.source)) {
280
288
  throw new OrmDefinitionError(
281
289
  `synced table "${name}": source "${config.source}" must match ${SYNCED_SOURCE_PATTERN} (gold layer only)`,
282
290
  file
283
291
  );
284
292
  }
293
+ const entries = Object.entries(columns);
294
+ const seenColumnNames = /* @__PURE__ */ new Map();
295
+ const columnLevelPk = [];
296
+ for (const [propertyName, builder] of entries) {
297
+ if (!isColumnBuilder(builder)) {
298
+ throw new OrmDefinitionError(
299
+ `synced table "${name}": column "${propertyName}" is not a column builder \u2014 use the t.* factories (t.uuid(), t.text(), \u2026)`,
300
+ file
301
+ );
302
+ }
303
+ const columnName = builder.columnNameFor(propertyName);
304
+ assertIdentifier({
305
+ value: columnName,
306
+ what: `synced table "${name}": column "${propertyName}" column name`,
307
+ file
308
+ });
309
+ const existing = seenColumnNames.get(columnName);
310
+ if (existing !== void 0) {
311
+ throw new OrmDefinitionError(
312
+ `synced table "${name}": columns "${existing}" and "${propertyName}" both map to Postgres column "${columnName}"`,
313
+ file
314
+ );
315
+ }
316
+ seenColumnNames.set(columnName, propertyName);
317
+ if (builder.isPrimaryKey) columnLevelPk.push(propertyName);
318
+ }
319
+ if (entries.length > 0) {
320
+ const hasConfigPk = config.primaryKey !== void 0 && config.primaryKey.length > 0;
321
+ if (columnLevelPk.length === 0 && !hasConfigPk) {
322
+ throw new OrmDefinitionError(
323
+ `synced table "${name}" declares columns but no primary key \u2014 use .primaryKey() or config.primaryKey`,
324
+ file
325
+ );
326
+ }
327
+ if (columnLevelPk.length > 0 && hasConfigPk) {
328
+ throw new OrmDefinitionError(
329
+ `synced table "${name}": declare the primary key either with .primaryKey() on columns or with config.primaryKey \u2014 not both`,
330
+ file
331
+ );
332
+ }
333
+ }
334
+ const tableConfig = {};
335
+ if (config.description !== void 0) tableConfig.description = config.description;
336
+ if (config.primaryKey !== void 0) tableConfig.primaryKey = config.primaryKey;
337
+ if (config.links !== void 0) tableConfig.links = config.links;
338
+ if (config.policies !== void 0) tableConfig.policies = { read: config.policies.read };
285
339
  const table = {
286
340
  kind: TABLE_BRAND,
287
341
  tableName: name,
288
- columns: {},
289
- config: config.description !== void 0 ? { description: config.description } : {},
342
+ columns,
343
+ config: tableConfig,
290
344
  source: config.source,
291
345
  file
292
346
  };
@@ -316,6 +370,475 @@ function defineEnum(name, values) {
316
370
  registerEnum(ref);
317
371
  return ref;
318
372
  }
373
+ var VIEW_BRAND = "seq-view";
374
+ function isSeqView(value) {
375
+ return typeof value === "object" && value !== null && value.kind === VIEW_BRAND;
376
+ }
377
+ function defineView(name, columns, config) {
378
+ const file = activeAuthoringContext()?.currentFile;
379
+ if (!IDENTIFIER_PATTERN.test(name) || name.length > MAX_IDENTIFIER_LENGTH) {
380
+ throw new OrmDefinitionError(
381
+ `view name "${name}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`,
382
+ file
383
+ );
384
+ }
385
+ const entries = Object.entries(columns);
386
+ if (entries.length === 0) throw new OrmDefinitionError(`view "${name}" declares no columns`, file);
387
+ const seenColumnNames = /* @__PURE__ */ new Map();
388
+ for (const [propertyName, builder] of entries) {
389
+ if (!isColumnBuilder(builder)) {
390
+ throw new OrmDefinitionError(
391
+ `view "${name}": column "${propertyName}" is not a column builder \u2014 use the t.* factories`,
392
+ file
393
+ );
394
+ }
395
+ const columnName = builder.columnNameFor(propertyName);
396
+ if (!IDENTIFIER_PATTERN.test(columnName) || columnName.length > MAX_IDENTIFIER_LENGTH) {
397
+ throw new OrmDefinitionError(`view "${name}": column "${propertyName}" has invalid column name "${columnName}"`, file);
398
+ }
399
+ const existing = seenColumnNames.get(columnName);
400
+ if (existing !== void 0) {
401
+ throw new OrmDefinitionError(
402
+ `view "${name}": columns "${existing}" and "${propertyName}" both map to Postgres column "${columnName}"`,
403
+ file
404
+ );
405
+ }
406
+ seenColumnNames.set(columnName, propertyName);
407
+ }
408
+ const view = { kind: VIEW_BRAND, viewName: name, columns, config, file };
409
+ registerView(view);
410
+ return view;
411
+ }
412
+ var QUERY_COLUMN_FACTORIES = {
413
+ uuid: (name) => pgCore.uuid(name),
414
+ text: (name) => pgCore.text(name),
415
+ integer: (name) => pgCore.integer(name),
416
+ bigint: (name) => pgCore.bigint(name, { mode: "bigint" }),
417
+ numeric: (name) => pgCore.numeric(name),
418
+ double: (name) => pgCore.doublePrecision(name),
419
+ boolean: (name) => pgCore.boolean(name),
420
+ timestamptz: (name) => pgCore.timestamp(name, { withTimezone: true }),
421
+ date: (name) => pgCore.date(name),
422
+ jsonb: (name) => pgCore.jsonb(name)
423
+ };
424
+ function compileViewQueries(input) {
425
+ const schemaName = `orm_${input.namespace}`;
426
+ const schema = pgCore.pgSchema(schemaName);
427
+ const enumFactories = /* @__PURE__ */ new Map();
428
+ for (const entry of input.enums) {
429
+ const values = entry.values;
430
+ enumFactories.set(entry.name, schema.enum(entry.name, values));
431
+ }
432
+ const handles = /* @__PURE__ */ new Map();
433
+ const physicalToLogical = /* @__PURE__ */ new Map();
434
+ const tableByName = new Map(input.tables.map((table) => [table.table, table]));
435
+ for (const authored of input.authoredTables) {
436
+ const canonical = tableByName.get(authored.tableName);
437
+ if (!canonical) continue;
438
+ const physicalSchema = schemaName;
439
+ const physicalName = canonical.table;
440
+ const relationSchema = pgCore.pgSchema(physicalSchema);
441
+ const columns = buildQueryColumns(canonical.columns, enumFactories);
442
+ const handle = relationSchema.table(physicalName, columns);
443
+ handles.set(authored, handle);
444
+ physicalToLogical.set(`${physicalSchema}.${physicalName}`, canonical.table);
445
+ }
446
+ const viewByName = new Map(input.views.map((view) => [view.view, view]));
447
+ for (const authored of input.authoredViews) {
448
+ const canonical = viewByName.get(authored.viewName);
449
+ if (!canonical) continue;
450
+ const columns = buildQueryColumns(canonical.columns, enumFactories);
451
+ const handle = schema.view(canonical.view, columns).existing();
452
+ handles.set(authored, handle);
453
+ physicalToLogical.set(`${schemaName}.${canonical.view}`, canonical.view);
454
+ }
455
+ const dialect = new pgCore.PgDialect();
456
+ const compiled = /* @__PURE__ */ new Map();
457
+ for (const authored of input.authoredViews) {
458
+ const expectedFields = Object.keys(authored.columns);
459
+ const declaredDependencies = /* @__PURE__ */ new Set();
460
+ const relation = (ref) => {
461
+ let resolved;
462
+ try {
463
+ resolved = typeof ref === "function" ? ref() : ref;
464
+ } catch (error) {
465
+ throw new OrmDefinitionError(
466
+ `view "${authored.viewName}" relation thunk threw: ${error instanceof Error ? error.message : String(error)}`,
467
+ authored.file
468
+ );
469
+ }
470
+ if (!isSeqTable(resolved) && !isSeqView(resolved)) {
471
+ throw new OrmDefinitionError(
472
+ `view "${authored.viewName}" relation() expects a table or view defined by this ORM`,
473
+ authored.file
474
+ );
475
+ }
476
+ const handle = handles.get(resolved);
477
+ if (!handle) {
478
+ const name = isSeqTable(resolved) ? resolved.tableName : resolved.viewName;
479
+ throw new OrmDefinitionError(
480
+ `view "${authored.viewName}" references relation "${name}" which is not part of this namespace`,
481
+ authored.file
482
+ );
483
+ }
484
+ declaredDependencies.add(isSeqTable(resolved) ? resolved.tableName : resolved.viewName);
485
+ return handle;
486
+ };
487
+ let queryBuilder;
488
+ try {
489
+ queryBuilder = authored.config.query({ qb: new pgCore.QueryBuilder(), relation });
490
+ } catch (error) {
491
+ if (error instanceof OrmDefinitionError) throw error;
492
+ throw new OrmDefinitionError(
493
+ `view "${authored.viewName}" query callback failed: ${error instanceof Error ? error.message : String(error)}`,
494
+ authored.file
495
+ );
496
+ }
497
+ let config;
498
+ try {
499
+ config = pgCore.getViewConfig(schema.view(authored.viewName).as(queryBuilder));
500
+ } catch (error) {
501
+ throw new OrmDefinitionError(
502
+ `view "${authored.viewName}" query must return a Drizzle select builder: ${error instanceof Error ? error.message : String(error)}`,
503
+ authored.file
504
+ );
505
+ }
506
+ const selectedFields = Object.keys(config.selectedFields);
507
+ if (selectedFields.length !== expectedFields.length || selectedFields.some((field, index) => field !== expectedFields[index])) {
508
+ throw new OrmDefinitionError(
509
+ `view "${authored.viewName}" query selects [${selectedFields.join(", ")}], but declared columns are [${expectedFields.join(", ")}]`,
510
+ authored.file
511
+ );
512
+ }
513
+ if (!config.query) throw new OrmDefinitionError(`view "${authored.viewName}" produced no query`, authored.file);
514
+ const tableDependencies = pgCore.extractUsedTable(config.query).map((physical) => {
515
+ const logical = physicalToLogical.get(physical);
516
+ if (!logical) {
517
+ throw new OrmDefinitionError(
518
+ `view "${authored.viewName}" query references unmanaged relation "${physical}" \u2014 use relation(...) with a namespace table or view`,
519
+ authored.file
520
+ );
521
+ }
522
+ return logical;
523
+ });
524
+ const renderedQuery = dialect.sqlToQuery(config.query.inlineParams());
525
+ if (renderedQuery.params.length > 0) {
526
+ throw new OrmDefinitionError(
527
+ `view "${authored.viewName}" query produced bind parameters which cannot be persisted in view DDL`,
528
+ authored.file
529
+ );
530
+ }
531
+ compiled.set(authored.viewName, {
532
+ query: renderedQuery.sql,
533
+ dependencies: [.../* @__PURE__ */ new Set([...declaredDependencies, ...tableDependencies])].sort()
534
+ });
535
+ }
536
+ return compiled;
537
+ }
538
+ function buildQueryColumns(columns, enumFactories) {
539
+ return Object.fromEntries(columns.map((column) => [column.name, buildQueryColumn(column, enumFactories)]));
540
+ }
541
+ function buildQueryColumn(column, enumFactories) {
542
+ const factory = typeof column.type === "string" ? QUERY_COLUMN_FACTORIES[column.type] : enumFactories.get(column.type.enum);
543
+ if (!factory) {
544
+ const type = typeof column.type === "string" ? column.type : column.type.enum;
545
+ throw new OrmDefinitionError(`view query column '${column.columnName}' references unknown type '${type}'`);
546
+ }
547
+ let builder = factory(column.columnName);
548
+ if (!column.nullable) builder = builder.notNull();
549
+ return builder;
550
+ }
551
+ function relationName(relation) {
552
+ return isSeqTable(relation) ? relation.tableName : relation.viewName;
553
+ }
554
+ function relationKind(relation) {
555
+ return isSeqTable(relation) ? "table" : "view";
556
+ }
557
+ function declaredPrimaryKeyProperties(table) {
558
+ const fromColumns = Object.entries(table.columns).filter(([, builder]) => builder.isPrimaryKey).map(([propertyName]) => propertyName);
559
+ if (fromColumns.length > 0) return fromColumns;
560
+ const configPk = table.config.primaryKey ?? [];
561
+ const resolved = [];
562
+ for (const ref of configPk) {
563
+ if (ref in table.columns) {
564
+ resolved.push(ref);
565
+ continue;
566
+ }
567
+ const byColumnName = Object.entries(table.columns).find(
568
+ ([propertyName, builder]) => builder.columnNameFor(propertyName) === ref
569
+ );
570
+ if (byColumnName) resolved.push(byColumnName[0]);
571
+ }
572
+ return resolved;
573
+ }
574
+ function assertOverlayWinsPrimaryKey({
575
+ table,
576
+ pk,
577
+ role
578
+ }) {
579
+ if (!(pk in table.columns)) {
580
+ throw new OrmDefinitionError(
581
+ `overlayWinsUnion: pk "${pk}" must exist as a property on both overlay and synced tables`
582
+ );
583
+ }
584
+ const declared = declaredPrimaryKeyProperties(table);
585
+ if (declared.length !== 1 || declared[0] !== pk) {
586
+ throw new OrmDefinitionError(
587
+ `overlayWinsUnion: ${role} primary key must be exactly "${pk}" (declared: ${declared.length > 0 ? declared.join(", ") : "none"})`
588
+ );
589
+ }
590
+ }
591
+ function formatCanonicalColumnType(type) {
592
+ return typeof type === "string" ? type : `enum(${type.enum})`;
593
+ }
594
+ function assertOverlayWinsMatchingColumnTypes({
595
+ overlay,
596
+ synced,
597
+ columns
598
+ }) {
599
+ for (const column of columns) {
600
+ const overlayType = overlay.columns[column].toCanonicalColumn(column).type;
601
+ const syncedType = synced.columns[column].toCanonicalColumn(column).type;
602
+ if (JSON.stringify(overlayType) !== JSON.stringify(syncedType)) {
603
+ throw new OrmDefinitionError(
604
+ `overlayWinsUnion: column "${column}" must have the same type on overlay and synced (overlay: ${formatCanonicalColumnType(overlayType)}, synced: ${formatCanonicalColumnType(syncedType)})`
605
+ );
606
+ }
607
+ }
608
+ }
609
+ function overlayWinsUnion(input) {
610
+ const { overlay, synced, pk, tombstone } = input;
611
+ return ({ qb, relation }) => {
612
+ assertOverlayWinsPrimaryKey({ table: overlay, pk, role: "overlay" });
613
+ assertOverlayWinsPrimaryKey({ table: synced, pk, role: "synced" });
614
+ const overlayHandle = relation(overlay);
615
+ const syncedHandle = relation(synced);
616
+ const overlayCols = overlayHandle;
617
+ const syncedCols = syncedHandle;
618
+ const overlayPk = overlayCols[pk];
619
+ const syncedPk = syncedCols[pk];
620
+ if (tombstone !== void 0) {
621
+ const tombstoneBuilder = overlay.columns[tombstone];
622
+ if (!tombstoneBuilder) {
623
+ throw new OrmDefinitionError(
624
+ `overlayWinsUnion: tombstone "${tombstone}" must exist as a property on the overlay table`
625
+ );
626
+ }
627
+ const tombstoneCanonical = tombstoneBuilder.toCanonicalColumn(tombstone);
628
+ if (tombstoneCanonical.type !== "boolean") {
629
+ throw new OrmDefinitionError(
630
+ `overlayWinsUnion: tombstone "${tombstone}" must be a boolean column`
631
+ );
632
+ }
633
+ if (tombstoneCanonical.nullable) {
634
+ throw new OrmDefinitionError(
635
+ `overlayWinsUnion: tombstone "${tombstone}" must be notNull \u2014 nullable tombstones silently drop rows`
636
+ );
637
+ }
638
+ }
639
+ const overlayColumns = Object.keys(overlay.columns);
640
+ const syncedColumns = Object.keys(synced.columns);
641
+ const sharedColumns = overlayColumns.filter((col) => syncedColumns.includes(col) && col !== pk);
642
+ assertOverlayWinsMatchingColumnTypes({
643
+ overlay,
644
+ synced,
645
+ columns: [pk, ...sharedColumns]
646
+ });
647
+ const overlaySelect = {
648
+ [pk]: overlayPk,
649
+ ...Object.fromEntries(sharedColumns.map((col) => [col, overlayCols[col]])),
650
+ provenance: drizzleOrm.sql`'overlay'`.as("provenance")
651
+ };
652
+ const syncedSelect = {
653
+ [pk]: syncedPk,
654
+ ...Object.fromEntries(sharedColumns.map((col) => [col, syncedCols[col]])),
655
+ provenance: drizzleOrm.sql`'synced'`.as("provenance")
656
+ };
657
+ const overlayFrom = qb.select(overlaySelect).from(overlayHandle);
658
+ const overlayQuery = tombstone !== void 0 ? overlayFrom.where(drizzleOrm.eq(overlayCols[tombstone], false)) : overlayFrom;
659
+ const syncedQuery = qb.select(syncedSelect).from(syncedHandle).where(
660
+ drizzleOrm.notExists(
661
+ qb.select({ one: drizzleOrm.sql`1` }).from(overlayHandle).where(drizzleOrm.eq(overlayPk, syncedPk))
662
+ )
663
+ );
664
+ return overlayQuery.unionAll(syncedQuery);
665
+ };
666
+ }
667
+
668
+ // src/authoring/cow-mutations.ts
669
+ function overrideRowMutation(input) {
670
+ const { overlay, synced, pk } = input;
671
+ const name = input.name ?? `override_${overlay.tableName}`;
672
+ assertMutationName(name, "overrideRowMutation");
673
+ assertOverlayPairPrimaryKey({ overlay, synced, pk, helper: "overrideRowMutation" });
674
+ const sharedProperties = sharedNonPkProperties({ overlay, synced, pk });
675
+ const setProperties = resolveSetProperties({
676
+ overlay,
677
+ synced,
678
+ pk,
679
+ set: input.set,
680
+ sharedProperties,
681
+ helper: "overrideRowMutation"
682
+ });
683
+ assertMatchingColumns({
684
+ overlay,
685
+ synced,
686
+ properties: [pk, ...sharedProperties],
687
+ helper: "overrideRowMutation"
688
+ });
689
+ return {
690
+ kind: "override",
691
+ name,
692
+ synced: synced.tableName,
693
+ pk,
694
+ copy: [pk, ...sharedProperties],
695
+ set: setProperties
696
+ };
697
+ }
698
+ function revertOverlayMutation(input) {
699
+ const { overlay, synced, pk } = input;
700
+ const name = input.name ?? `revert_${overlay.tableName}`;
701
+ assertMutationName(name, "revertOverlayMutation");
702
+ assertOverlayPairPrimaryKey({ overlay, synced, pk, helper: "revertOverlayMutation" });
703
+ assertMatchingColumns({ overlay, synced, properties: [pk], helper: "revertOverlayMutation" });
704
+ return {
705
+ kind: "revert",
706
+ name,
707
+ synced: synced.tableName,
708
+ pk,
709
+ copy: [],
710
+ set: []
711
+ };
712
+ }
713
+ function assertMutationName(name, helper) {
714
+ if (!IDENTIFIER_PATTERN.test(name) || name.length > MAX_IDENTIFIER_LENGTH) {
715
+ throw new OrmDefinitionError(
716
+ `${helper}: mutation name "${name}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`
717
+ );
718
+ }
719
+ }
720
+ function assertOverlayPairPrimaryKey({
721
+ overlay,
722
+ synced,
723
+ pk,
724
+ helper
725
+ }) {
726
+ assertSinglePrimaryKey({ table: overlay, pk, role: "overlay", helper });
727
+ assertSinglePrimaryKey({ table: synced, pk, role: "synced", helper });
728
+ }
729
+ function assertSinglePrimaryKey({
730
+ table,
731
+ pk,
732
+ role,
733
+ helper
734
+ }) {
735
+ if (!(pk in table.columns)) {
736
+ throw new OrmDefinitionError(`${helper}: pk "${pk}" must exist as a property on the ${role} table`);
737
+ }
738
+ const declared = declaredPrimaryKeyProperties2(table);
739
+ if (declared.length !== 1 || declared[0] !== pk) {
740
+ throw new OrmDefinitionError(
741
+ `${helper}: ${role} primary key must be exactly "${pk}" (declared: ${declared.length > 0 ? declared.join(", ") : "none"})`
742
+ );
743
+ }
744
+ }
745
+ function declaredPrimaryKeyProperties2(table) {
746
+ const fromColumns = Object.entries(table.columns).filter(([, builder]) => builder.isPrimaryKey).map(([propertyName]) => propertyName);
747
+ if (fromColumns.length > 0) return fromColumns;
748
+ const configPk = table.config?.primaryKey ?? [];
749
+ const resolved = [];
750
+ for (const ref of configPk) {
751
+ if (ref in table.columns) {
752
+ resolved.push(ref);
753
+ continue;
754
+ }
755
+ const byColumnName = Object.entries(table.columns).find(
756
+ ([propertyName, builder]) => builder.columnNameFor(propertyName) === ref
757
+ );
758
+ if (byColumnName) resolved.push(byColumnName[0]);
759
+ }
760
+ return resolved;
761
+ }
762
+ function sharedNonPkProperties({
763
+ overlay,
764
+ synced,
765
+ pk
766
+ }) {
767
+ return Object.keys(overlay.columns).filter((property) => property !== pk && property in synced.columns);
768
+ }
769
+ function resolveSetProperties({
770
+ overlay,
771
+ synced,
772
+ pk,
773
+ set,
774
+ sharedProperties,
775
+ helper
776
+ }) {
777
+ const properties = set !== void 0 ? [...set] : [...sharedProperties];
778
+ if (properties.length === 0) {
779
+ throw new OrmDefinitionError(
780
+ `${helper}: set must declare at least one non-PK column to update after the copy`
781
+ );
782
+ }
783
+ const seen = /* @__PURE__ */ new Set();
784
+ for (const property of properties) {
785
+ if (property === pk) {
786
+ throw new OrmDefinitionError(`${helper}: set must not include pk "${pk}" \u2014 identity columns are pinned`);
787
+ }
788
+ if (!(property in overlay.columns)) {
789
+ throw new OrmDefinitionError(`${helper}: set column "${property}" must exist on the overlay table`);
790
+ }
791
+ if (!(property in synced.columns)) {
792
+ throw new OrmDefinitionError(
793
+ `${helper}: set column "${property}" must exist on the synced table (contract columns only)`
794
+ );
795
+ }
796
+ if (seen.has(property)) {
797
+ throw new OrmDefinitionError(`${helper}: set column "${property}" is duplicated`);
798
+ }
799
+ seen.add(property);
800
+ }
801
+ return properties;
802
+ }
803
+ function assertMatchingColumns({
804
+ overlay,
805
+ synced,
806
+ properties,
807
+ helper
808
+ }) {
809
+ for (const property of properties) {
810
+ const overlayCanonical = overlay.columns[property].toCanonicalColumn(property);
811
+ const syncedCanonical = synced.columns[property].toCanonicalColumn(property);
812
+ const overlayType = scalarColumnType(overlay, property, helper);
813
+ const syncedType = scalarColumnType(synced, property, helper);
814
+ if (overlayType !== syncedType) {
815
+ throw new OrmDefinitionError(
816
+ `${helper}: column "${property}" must have the same scalar type on overlay and synced (overlay: ${overlayType}, synced: ${syncedType})`
817
+ );
818
+ }
819
+ const overlayColumn = overlay.columns[property].columnNameFor(property);
820
+ const syncedColumn = synced.columns[property].columnNameFor(property);
821
+ if (overlayColumn !== syncedColumn) {
822
+ throw new OrmDefinitionError(
823
+ `${helper}: column "${property}" maps to different Postgres names on overlay (${overlayColumn}) and synced (${syncedColumn})`
824
+ );
825
+ }
826
+ if (syncedCanonical.nullable && !overlayCanonical.nullable) {
827
+ throw new OrmDefinitionError(
828
+ `${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`
829
+ );
830
+ }
831
+ }
832
+ }
833
+ function scalarColumnType(table, property, helper) {
834
+ const type = table.columns[property].toCanonicalColumn(property).type;
835
+ if (typeof type !== "string") {
836
+ throw new OrmDefinitionError(
837
+ `${helper}: column "${property}" on "${table.tableName}" must be a scalar type (enums are not supported in cow mutation arguments)`
838
+ );
839
+ }
840
+ return type;
841
+ }
319
842
  var CANONICAL_FORMAT = 2;
320
843
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
321
844
  var identifier = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
@@ -388,8 +911,33 @@ var policyExpressionSchema = zod.z.union([
388
911
  ]);
389
912
  var canonicalPoliciesSchema = zod.z.object({
390
913
  read: policyExpressionSchema.optional(),
391
- write: policyExpressionSchema.optional()
914
+ write: policyExpressionSchema.optional(),
915
+ insert: policyExpressionSchema.optional(),
916
+ update: policyExpressionSchema.optional()
392
917
  });
918
+ function isSingleViewQuerySql(value) {
919
+ const query = value.trim();
920
+ if (!/^\(*(?:select|with)\b/i.test(query) || query.includes("\0")) return false;
921
+ let state = "normal";
922
+ for (let index = 0; index < query.length; index += 1) {
923
+ const char = query[index];
924
+ const next = query[index + 1];
925
+ if (state === "single") {
926
+ if (char === "'" && next === "'") index += 1;
927
+ else if (char === "'") state = "normal";
928
+ continue;
929
+ }
930
+ if (state === "double") {
931
+ if (char === '"' && next === '"') index += 1;
932
+ else if (char === '"') state = "normal";
933
+ continue;
934
+ }
935
+ if (char === "'") state = "single";
936
+ else if (char === '"') state = "double";
937
+ else if (char === ";" || char === "-" && next === "-" || char === "/" && next === "*") return false;
938
+ }
939
+ return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
940
+ }
393
941
  var canonicalActionSchema = zod.z.object({
394
942
  name: identifier,
395
943
  requires: zod.z.string().max(1e3).optional(),
@@ -401,6 +949,37 @@ var canonicalActionSchema = zod.z.object({
401
949
  returns: zod.z.enum(["rows", "rowsAffected", "scalar"]).default("rowsAffected"),
402
950
  audit: zod.z.object({ action: zod.z.string(), resource: zod.z.string() }).optional()
403
951
  });
952
+ var canonicalCowMutationSchema = zod.z.object({
953
+ kind: zod.z.enum(["override", "revert"]),
954
+ name: identifier,
955
+ /** Synced table (same namespace) the overlay copies from / falls back to. */
956
+ synced: identifier,
957
+ /** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
958
+ pk: zod.z.string().min(1).max(63),
959
+ /** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
960
+ copy: zod.z.array(zod.z.string().min(1).max(63)).default([]),
961
+ /** Override: overlay properties written by the UPDATE (never the pk). Empty for revert. */
962
+ set: zod.z.array(zod.z.string().min(1).max(63)).default([])
963
+ }).superRefine((mutation, ctx) => {
964
+ if (mutation.kind === "override") {
965
+ if (mutation.set.length === 0) {
966
+ ctx.addIssue({ code: "custom", path: ["set"], message: "override mutations must set at least one column" });
967
+ }
968
+ if (!mutation.copy.includes(mutation.pk)) {
969
+ ctx.addIssue({ code: "custom", path: ["copy"], message: "override copy list must include the pk" });
970
+ }
971
+ } else if (mutation.set.length > 0 || mutation.copy.length > 0) {
972
+ ctx.addIssue({ code: "custom", path: ["set"], message: "revert mutations take no copy/set columns" });
973
+ }
974
+ if (mutation.set.includes(mutation.pk)) {
975
+ ctx.addIssue({ code: "custom", path: ["set"], message: "set must not include the pk \u2014 identity columns are pinned" });
976
+ }
977
+ for (const [field, values] of [["copy", mutation.copy], ["set", mutation.set]]) {
978
+ if (new Set(values).size !== values.length) {
979
+ ctx.addIssue({ code: "custom", path: [field], message: `${field} must not contain duplicate columns` });
980
+ }
981
+ }
982
+ });
404
983
  var canonicalTableSchema = zod.z.object({
405
984
  table: identifier,
406
985
  description: zod.z.string().max(2e3).optional(),
@@ -413,7 +992,45 @@ var canonicalTableSchema = zod.z.object({
413
992
  links: zod.z.array(canonicalLinkSchema).default([]),
414
993
  indexes: zod.z.array(canonicalIndexSchema).default([]),
415
994
  policies: canonicalPoliciesSchema.default({}),
416
- actions: zod.z.array(canonicalActionSchema).default([])
995
+ /**
996
+ * v1-conversion carry-over only. v2 has no action surface — the runtime
997
+ * neither compiles nor executes these; writes are mutations. Kept parseable
998
+ * so stored definitions and their content hashes stay stable.
999
+ */
1000
+ actions: zod.z.array(canonicalActionSchema).default([]),
1001
+ /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
1002
+ cowMutations: zod.z.array(canonicalCowMutationSchema).optional()
1003
+ });
1004
+ var canonicalViewSchema = zod.z.object({
1005
+ view: identifier,
1006
+ description: zod.z.string().max(2e3).optional(),
1007
+ columns: zod.z.array(canonicalColumnSchema).min(1),
1008
+ /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
1009
+ primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
1010
+ links: zod.z.array(canonicalLinkSchema).default([]),
1011
+ policies: zod.z.object({ read: policyExpressionSchema }).strict(),
1012
+ query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
1013
+ message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
1014
+ }),
1015
+ /** Namespace-local logical relation names, sorted for deterministic hashing. */
1016
+ dependencies: zod.z.array(identifier).max(500)
1017
+ }).superRefine((view, ctx) => {
1018
+ for (const [index, column] of view.columns.entries()) {
1019
+ if (column.default !== void 0 || column.defaultFn !== void 0) {
1020
+ ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
1021
+ }
1022
+ if (column.unique) {
1023
+ ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare unique` });
1024
+ }
1025
+ if (column.renamedFrom !== void 0 || column.cast !== void 0) {
1026
+ ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare migration hints` });
1027
+ }
1028
+ }
1029
+ for (const [index, link] of view.links.entries()) {
1030
+ if (link.enforce) {
1031
+ ctx.addIssue({ code: "custom", path: ["links", index, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
1032
+ }
1033
+ }
417
1034
  });
418
1035
  var canonicalDefinitionSchema = zod.z.object({
419
1036
  definition_format: zod.z.literal(CANONICAL_FORMAT),
@@ -421,24 +1038,161 @@ var canonicalDefinitionSchema = zod.z.object({
421
1038
  description: zod.z.string().max(2e3).optional(),
422
1039
  enums: zod.z.array(canonicalEnumSchema).default([]),
423
1040
  tables: zod.z.array(canonicalTableSchema).min(0).max(500),
1041
+ /** Optional to preserve byte-identical hashes for pre-view canonical v2 documents. */
1042
+ views: zod.z.array(canonicalViewSchema).max(500).optional(),
424
1043
  /** Verbatim custom-roles grammar (@sequenceholdings/custom-roles-manifest). */
425
1044
  capabilities: zod.z.object({
426
1045
  roles: zod.z.unknown().optional(),
427
1046
  uses: zod.z.array(zod.z.string()).default([])
428
1047
  }).default({ uses: [] })
1048
+ }).superRefine((definition, ctx) => {
1049
+ const seen = /* @__PURE__ */ new Set();
1050
+ for (const [index, table] of definition.tables.entries()) {
1051
+ if (seen.has(table.table)) {
1052
+ ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
1053
+ }
1054
+ seen.add(table.table);
1055
+ }
1056
+ const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
1057
+ for (const [tableIndex, table] of definition.tables.entries()) {
1058
+ const cowMutations = table.cowMutations ?? [];
1059
+ if (cowMutations.length === 0) continue;
1060
+ const path = (index, ...rest) => ["tables", tableIndex, "cowMutations", index, ...rest];
1061
+ if (table.source !== void 0) {
1062
+ ctx.addIssue({ code: "custom", path: path(0), message: `synced table '${table.table}' cannot declare cow mutations \u2014 it is read-only` });
1063
+ continue;
1064
+ }
1065
+ for (const [index, mutation] of cowMutations.entries()) {
1066
+ const synced = tableByName.get(mutation.synced);
1067
+ if (!synced || synced.source === void 0) {
1068
+ ctx.addIssue({
1069
+ code: "custom",
1070
+ path: path(index, "synced"),
1071
+ message: `cow mutation '${mutation.name}' references '${mutation.synced}', which is not a synced table in this namespace`
1072
+ });
1073
+ continue;
1074
+ }
1075
+ const overlayColumnsByName = new Map(table.columns.map((column) => [column.name, column]));
1076
+ const syncedColumnsByName = new Map(synced.columns.map((column) => [column.name, column]));
1077
+ let columnsExist = true;
1078
+ for (const property of [mutation.pk, ...mutation.copy, ...mutation.set]) {
1079
+ if (!overlayColumnsByName.has(property)) {
1080
+ columnsExist = false;
1081
+ ctx.addIssue({ code: "custom", path: path(index), message: `cow mutation '${mutation.name}': column '${property}' does not exist on overlay '${table.table}'` });
1082
+ }
1083
+ }
1084
+ for (const property of [mutation.pk, ...mutation.copy]) {
1085
+ if (!syncedColumnsByName.has(property)) {
1086
+ columnsExist = false;
1087
+ ctx.addIssue({ code: "custom", path: path(index), message: `cow mutation '${mutation.name}': column '${property}' does not exist on synced '${mutation.synced}'` });
1088
+ }
1089
+ }
1090
+ if (!columnsExist) continue;
1091
+ const overlayPk = table.primaryKey ?? [];
1092
+ const syncedPk = synced.primaryKey ?? [];
1093
+ if (overlayPk.length !== 1 || overlayPk[0] !== mutation.pk || syncedPk.length !== 1 || syncedPk[0] !== mutation.pk) {
1094
+ ctx.addIssue({
1095
+ code: "custom",
1096
+ path: path(index, "pk"),
1097
+ message: `cow mutation '${mutation.name}': pk '${mutation.pk}' must be the single-column primary key on both '${table.table}' and '${mutation.synced}'`
1098
+ });
1099
+ }
1100
+ for (const property of [mutation.pk, ...mutation.copy]) {
1101
+ const overlayColumn = overlayColumnsByName.get(property);
1102
+ const syncedColumn = syncedColumnsByName.get(property);
1103
+ if (!overlayColumn || !syncedColumn) continue;
1104
+ if (JSON.stringify(overlayColumn.type) !== JSON.stringify(syncedColumn.type)) {
1105
+ ctx.addIssue({
1106
+ code: "custom",
1107
+ path: path(index),
1108
+ message: `cow mutation '${mutation.name}': column '${property}' must have the same type on overlay and synced`
1109
+ });
1110
+ }
1111
+ if (overlayColumn.columnName !== syncedColumn.columnName) {
1112
+ ctx.addIssue({
1113
+ code: "custom",
1114
+ path: path(index),
1115
+ message: `cow mutation '${mutation.name}': column '${property}' maps to different Postgres names on overlay ('${overlayColumn.columnName}') and synced ('${syncedColumn.columnName}')`
1116
+ });
1117
+ }
1118
+ if (syncedColumn.nullable !== false && overlayColumn.nullable === false) {
1119
+ ctx.addIssue({
1120
+ code: "custom",
1121
+ path: path(index),
1122
+ 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`
1123
+ });
1124
+ }
1125
+ }
1126
+ }
1127
+ }
1128
+ for (const [index, view] of (definition.views ?? []).entries()) {
1129
+ if (seen.has(view.view)) {
1130
+ ctx.addIssue({ code: "custom", path: ["views", index, "view"], message: `duplicate relation '${view.view}'` });
1131
+ }
1132
+ seen.add(view.view);
1133
+ }
1134
+ const views = definition.views ?? [];
1135
+ const viewByName = new Map(views.map((view) => [view.view, view]));
1136
+ for (const [viewIndex, view] of views.entries()) {
1137
+ for (const [dependencyIndex, dependency] of view.dependencies.entries()) {
1138
+ if (!seen.has(dependency)) {
1139
+ ctx.addIssue({
1140
+ code: "custom",
1141
+ path: ["views", viewIndex, "dependencies", dependencyIndex],
1142
+ message: `view '${view.view}' depends on unknown relation '${dependency}'`
1143
+ });
1144
+ }
1145
+ }
1146
+ }
1147
+ const visiting = /* @__PURE__ */ new Set();
1148
+ const visited = /* @__PURE__ */ new Set();
1149
+ let cycleReported = false;
1150
+ const visit = (name, path) => {
1151
+ if (visited.has(name) || cycleReported) return;
1152
+ if (visiting.has(name)) {
1153
+ cycleReported = true;
1154
+ ctx.addIssue({
1155
+ code: "custom",
1156
+ path: ["views"],
1157
+ message: `view dependencies form a cycle: ${[...path, name].join(" -> ")}`
1158
+ });
1159
+ return;
1160
+ }
1161
+ visiting.add(name);
1162
+ const view = viewByName.get(name);
1163
+ for (const dependency of view?.dependencies ?? []) {
1164
+ if (viewByName.has(dependency)) visit(dependency, [...path, name]);
1165
+ }
1166
+ visiting.delete(name);
1167
+ visited.add(name);
1168
+ };
1169
+ for (const view of views) visit(view.view, []);
429
1170
  });
430
1171
  function normalizeCanonical(definition) {
431
1172
  const sortByName = (items) => [...items].sort((a, b) => a.name.localeCompare(b.name));
432
- return {
1173
+ const normalized = {
433
1174
  ...definition,
434
1175
  enums: sortByName(definition.enums),
435
- tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => ({
436
- ...t2,
437
- links: sortByName(t2.links),
438
- indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
439
- actions: sortByName(t2.actions)
440
- }))
1176
+ tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
1177
+ const { cowMutations, ...rest } = t2;
1178
+ return {
1179
+ ...rest,
1180
+ links: sortByName(t2.links),
1181
+ indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
1182
+ actions: sortByName(t2.actions),
1183
+ // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
1184
+ ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
1185
+ };
1186
+ })
441
1187
  };
1188
+ if (definition.views !== void 0) {
1189
+ normalized.views = [...definition.views].sort((a, b) => a.view.localeCompare(b.view)).map((view) => ({
1190
+ ...view,
1191
+ links: sortByName(view.links),
1192
+ dependencies: [...view.dependencies].sort()
1193
+ }));
1194
+ }
1195
+ return normalized;
442
1196
  }
443
1197
  function stableJson(value) {
444
1198
  if (value === null || typeof value !== "object") return JSON.stringify(value);
@@ -469,6 +1223,7 @@ function defineNamespace(input) {
469
1223
  }
470
1224
  function serializeNamespace(input) {
471
1225
  const { config, tables } = input;
1226
+ const views = input.views ?? [];
472
1227
  const enums = input.enums ?? [];
473
1228
  const tablesByName = /* @__PURE__ */ new Map();
474
1229
  for (const table of tables) {
@@ -481,6 +1236,29 @@ function serializeNamespace(input) {
481
1236
  }
482
1237
  tablesByName.set(table.tableName, table);
483
1238
  }
1239
+ const viewsByName = /* @__PURE__ */ new Map();
1240
+ for (const view of views) {
1241
+ const existing = viewsByName.get(view.viewName);
1242
+ if (existing) {
1243
+ throw new OrmDefinitionError(
1244
+ `duplicate view "${view.viewName}" (defined in ${describeFile(existing)} and ${describeFile(view)})`,
1245
+ view.file
1246
+ );
1247
+ }
1248
+ viewsByName.set(view.viewName, view);
1249
+ }
1250
+ const relationsByName = /* @__PURE__ */ new Map();
1251
+ for (const relation of [...tables, ...views]) {
1252
+ const name = relationName(relation);
1253
+ const existing = relationsByName.get(name);
1254
+ if (existing) {
1255
+ throw new OrmDefinitionError(
1256
+ `duplicate relation "${name}" (defined as ${relationKind(existing)} in ${describeFile(existing)} and ${relationKind(relation)} in ${describeFile(relation)})`,
1257
+ relation.file
1258
+ );
1259
+ }
1260
+ relationsByName.set(name, relation);
1261
+ }
484
1262
  const enumsByName = /* @__PURE__ */ new Map();
485
1263
  for (const ref of enums) {
486
1264
  if (enumsByName.has(ref.name)) {
@@ -488,11 +1266,26 @@ function serializeNamespace(input) {
488
1266
  }
489
1267
  enumsByName.set(ref.name, ref);
490
1268
  }
491
- const canonicalTables = tables.map((table) => buildCanonicalTable({ table, tablesByName, enumsByName }));
1269
+ const canonicalTables = tables.map((table) => buildCanonicalTable({ table, relationsByName, enumsByName }));
492
1270
  const canonicalEnums = [...enumsByName.values()].map((ref) => ({
493
1271
  name: ref.name,
494
1272
  values: [...ref.values]
495
1273
  }));
1274
+ const viewDrafts = views.map((view) => buildCanonicalView({ view, relationsByName, enumsByName }));
1275
+ const compiledViews = compileViewQueries({
1276
+ namespace: config.namespace,
1277
+ tables: canonicalTables,
1278
+ views: viewDrafts,
1279
+ enums: canonicalEnums,
1280
+ authoredTables: tables,
1281
+ authoredViews: views
1282
+ });
1283
+ const canonicalViews = viewDrafts.map((draft) => {
1284
+ const compiled = compiledViews.get(draft.view);
1285
+ if (!compiled) throw new OrmDefinitionError(`view "${draft.view}" did not compile`, viewsByName.get(draft.view)?.file);
1286
+ return canonicalViewSchema.parse({ ...draft, ...compiled });
1287
+ });
1288
+ assertAcyclicViewDependencies(canonicalViews);
496
1289
  const candidate = {
497
1290
  definition_format: CANONICAL_FORMAT,
498
1291
  namespace: config.namespace,
@@ -504,6 +1297,7 @@ function serializeNamespace(input) {
504
1297
  }
505
1298
  };
506
1299
  if (config.description !== void 0) candidate.description = config.description;
1300
+ if (canonicalViews.length > 0) candidate.views = canonicalViews;
507
1301
  let parsed;
508
1302
  try {
509
1303
  parsed = canonicalDefinitionSchema.parse(candidate);
@@ -518,7 +1312,7 @@ function serializeNamespace(input) {
518
1312
  function computeCanonicalHash(definition) {
519
1313
  return crypto.createHash("sha256").update(HASH_DOMAIN).update(stableJson(definition)).digest("hex");
520
1314
  }
521
- function buildCanonicalTable({ table, tablesByName, enumsByName }) {
1315
+ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
522
1316
  const candidate = {
523
1317
  table: table.tableName,
524
1318
  columns: [],
@@ -530,6 +1324,31 @@ function buildCanonicalTable({ table, tablesByName, enumsByName }) {
530
1324
  if (table.config.description !== void 0) candidate.description = table.config.description;
531
1325
  if (table.config.renamedFrom !== void 0) candidate.renamedFrom = table.config.renamedFrom;
532
1326
  if (table.source !== void 0) candidate.source = table.source;
1327
+ if (table.source !== void 0) {
1328
+ if (table.config.indexes !== void 0 && table.config.indexes.length > 0) {
1329
+ throw new OrmDefinitionError(
1330
+ `synced table "${table.tableName}" is system-owned; indexes are not allowed`,
1331
+ table.file
1332
+ );
1333
+ }
1334
+ if (table.config.cowMutations !== void 0 && table.config.cowMutations.length > 0) {
1335
+ throw new OrmDefinitionError(
1336
+ `synced table "${table.tableName}" is read-only; cow mutations belong on the overlay table`,
1337
+ table.file
1338
+ );
1339
+ }
1340
+ if (table.config.policies?.write !== void 0 || table.config.policies?.insert !== void 0 || table.config.policies?.update !== void 0) {
1341
+ throw new OrmDefinitionError(`synced table "${table.tableName}" is read-only`, table.file);
1342
+ }
1343
+ for (const [linkName, link] of Object.entries(table.config.links ?? {})) {
1344
+ if (link.enforce) {
1345
+ throw new OrmDefinitionError(
1346
+ `synced table "${table.tableName}": link "${linkName}" cannot enforce foreign keys \u2014 its DDL is owned by the sync`,
1347
+ table.file
1348
+ );
1349
+ }
1350
+ }
1351
+ }
533
1352
  const columnLevelPk = [];
534
1353
  for (const [propertyName, builder] of Object.entries(table.columns)) {
535
1354
  const column = builder.toCanonicalColumn(propertyName);
@@ -551,40 +1370,38 @@ function buildCanonicalTable({ table, tablesByName, enumsByName }) {
551
1370
  );
552
1371
  }
553
1372
  candidate.primaryKey = table.config.primaryKey.map(
554
- (ref) => resolveColumnRef({ table, ref, role: "primaryKey" })
1373
+ (ref) => resolveColumnRef({ relation: table, ref, role: "primaryKey" })
555
1374
  );
556
1375
  } else if (columnLevelPk.length > 0) {
557
1376
  candidate.primaryKey = columnLevelPk;
558
1377
  }
559
1378
  for (const index of table.config.indexes ?? []) {
560
1379
  const entry = {
561
- columns: index.columns.map((ref) => resolveColumnRef({ table, ref, role: "index" }))
1380
+ columns: index.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
562
1381
  };
563
1382
  if (index.unique !== void 0) entry.unique = index.unique;
564
1383
  if (index.name !== void 0) entry.name = index.name;
565
1384
  candidate.indexes?.push(entry);
566
1385
  }
567
1386
  for (const [linkName, link] of Object.entries(table.config.links ?? {})) {
568
- candidate.links?.push(buildCanonicalLink({ table, linkName, link, tablesByName }));
1387
+ candidate.links?.push(buildCanonicalLink({ relation: table, linkName, link, relationsByName }));
569
1388
  }
570
1389
  const policies = {};
571
1390
  if (table.config.policies?.read !== void 0) policies.read = toCanonicalPolicy(table.config.policies.read);
572
1391
  if (table.config.policies?.write !== void 0) policies.write = toCanonicalPolicy(table.config.policies.write);
1392
+ if (table.config.policies?.insert !== void 0) policies.insert = toCanonicalPolicy(table.config.policies.insert);
1393
+ if (table.config.policies?.update !== void 0) policies.update = toCanonicalPolicy(table.config.policies.update);
573
1394
  candidate.policies = policies;
574
- for (const action of table.config.actions ?? []) {
575
- candidate.actions?.push({
576
- name: action.name,
577
- ...action.requires !== void 0 ? { requires: action.requires } : {},
578
- params: Object.fromEntries(
579
- Object.entries(action.params ?? {}).map(([name, param]) => [
580
- name,
581
- { type: param.type, nullable: param.nullable ?? false }
582
- ])
583
- ),
584
- executes: action.executes,
585
- returns: action.returns ?? "rowsAffected",
586
- ...action.audit !== void 0 ? { audit: action.audit } : {}
587
- });
1395
+ const cowMutations = table.config.cowMutations ?? [];
1396
+ if (cowMutations.length > 0) {
1397
+ candidate.cowMutations = cowMutations.map((mutation) => ({
1398
+ kind: mutation.kind,
1399
+ name: mutation.name,
1400
+ synced: mutation.synced,
1401
+ pk: mutation.pk,
1402
+ copy: [...mutation.copy],
1403
+ set: [...mutation.set]
1404
+ }));
588
1405
  }
589
1406
  try {
590
1407
  return canonicalTableSchema.parse(candidate);
@@ -595,14 +1412,71 @@ function buildCanonicalTable({ table, tablesByName, enumsByName }) {
595
1412
  throw error;
596
1413
  }
597
1414
  }
1415
+ function buildCanonicalView({
1416
+ view,
1417
+ relationsByName,
1418
+ enumsByName
1419
+ }) {
1420
+ if (view.config.policies?.read === void 0) {
1421
+ throw new OrmDefinitionError(`view "${view.viewName}" must declare policies.read`, view.file);
1422
+ }
1423
+ const columns = [];
1424
+ const columnLevelPk = [];
1425
+ for (const [propertyName, builder] of Object.entries(view.columns)) {
1426
+ const column = builder.toCanonicalColumn(propertyName);
1427
+ if (column.default !== void 0 || column.defaultFn !== void 0) {
1428
+ throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare a default`, view.file);
1429
+ }
1430
+ if (column.unique) {
1431
+ throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare unique`, view.file);
1432
+ }
1433
+ if (column.renamedFrom !== void 0 || column.cast !== void 0) {
1434
+ throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare migration hints`, view.file);
1435
+ }
1436
+ const enumName = builder.enumName;
1437
+ if (enumName !== void 0 && !enumsByName.has(enumName)) {
1438
+ throw new OrmDefinitionError(
1439
+ `view "${view.viewName}": column "${propertyName}" references enum "${enumName}" which is not part of this namespace`,
1440
+ view.file
1441
+ );
1442
+ }
1443
+ if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
1444
+ columns.push(column);
1445
+ }
1446
+ let primaryKey;
1447
+ if (view.config.primaryKey !== void 0) {
1448
+ if (columnLevelPk.length > 0) {
1449
+ throw new OrmDefinitionError(
1450
+ `view "${view.viewName}": declare the logical primary key either on columns or with config.primaryKey \u2014 not both`,
1451
+ view.file
1452
+ );
1453
+ }
1454
+ primaryKey = view.config.primaryKey.map((ref) => resolveColumnRef({ relation: view, ref, role: "primaryKey" }));
1455
+ } else if (columnLevelPk.length > 0) {
1456
+ primaryKey = columnLevelPk;
1457
+ }
1458
+ const links = Object.entries(view.config.links ?? {}).map(
1459
+ ([linkName, link]) => buildCanonicalLink({ relation: view, linkName, link, relationsByName })
1460
+ );
1461
+ return {
1462
+ view: view.viewName,
1463
+ ...view.config.description !== void 0 ? { description: view.config.description } : {},
1464
+ columns,
1465
+ ...primaryKey !== void 0 ? { primaryKey } : {},
1466
+ links,
1467
+ policies: { read: toCanonicalPolicy(view.config.policies.read) }
1468
+ };
1469
+ }
598
1470
  function buildCanonicalLink({
599
- table,
1471
+ relation,
600
1472
  linkName,
601
1473
  link,
602
- tablesByName
1474
+ relationsByName
603
1475
  }) {
1476
+ const ownerKind = relationKind(relation);
1477
+ const ownerName = relationName(relation);
604
1478
  const fail = (message) => {
605
- throw new OrmDefinitionError(`table "${table.tableName}": link "${linkName}" ${message}`, table.file);
1479
+ throw new OrmDefinitionError(`${ownerKind} "${ownerName}": link "${linkName}" ${message}`, relation.file);
606
1480
  };
607
1481
  if (link.enforce && link.linkKind !== "toOne") {
608
1482
  return fail("sets enforce: true, which is only supported on toOne links");
@@ -612,29 +1486,34 @@ function buildCanonicalLink({
612
1486
  return {
613
1487
  name: linkName,
614
1488
  kind: "toPlatformUser",
615
- on: resolveColumnRef({ table, ref: link.on, role: `link "${linkName}" on` }),
1489
+ on: resolveColumnRef({ relation, ref: link.on, role: `link "${linkName}" on` }),
616
1490
  ...link.references !== void 0 ? { references: deriveColumnName(link.references) } : {},
617
1491
  enforce: false,
618
1492
  ...link.description !== void 0 ? { description: link.description } : {}
619
1493
  };
620
1494
  }
621
- const target = resolveTableRef({ ref: link.target, fail });
622
- if (!tablesByName.has(target.tableName)) {
623
- return fail(`targets table "${target.tableName}" which is not part of this namespace`);
1495
+ const target = resolveRelationRef({ ref: link.target, fail });
1496
+ const targetName = relationName(target);
1497
+ if (!relationsByName.has(targetName)) {
1498
+ return fail(`targets ${relationKind(target)} "${targetName}" which is not part of this namespace`);
1499
+ }
1500
+ if (link.enforce && (!isSeqTable(relation) || !isSeqTable(target))) {
1501
+ return fail("cannot enforce a PostgreSQL foreign key involving a view");
624
1502
  }
625
- const owningSide = link.linkKind === "toOne" ? table : target;
1503
+ const owningSide = link.linkKind === "toOne" ? relation : target;
1504
+ const referencedSide = link.linkKind === "toOne" ? target : relation;
626
1505
  return {
627
1506
  name: linkName,
628
1507
  kind: link.linkKind,
629
- target: target.tableName,
630
- on: resolveColumnRef({ table: owningSide, ref: link.on, role: `link "${linkName}" on` }),
631
- ...link.references !== void 0 ? { references: resolveColumnRef({ table: target, ref: link.references, role: `link "${linkName}" references` }) } : {},
1508
+ target: targetName,
1509
+ on: resolveColumnRef({ relation: owningSide, ref: link.on, role: `link "${linkName}" on` }),
1510
+ ...link.references !== void 0 ? { references: resolveColumnRef({ relation: referencedSide, ref: link.references, role: `link "${linkName}" references` }) } : {},
632
1511
  enforce: link.enforce,
633
1512
  ...link.description !== void 0 ? { description: link.description } : {}
634
1513
  };
635
1514
  }
636
- function resolveTableRef({ ref, fail }) {
637
- if (ref === void 0) return fail("declares no target table");
1515
+ function resolveRelationRef({ ref, fail }) {
1516
+ if (ref === void 0) return fail("declares no target relation");
638
1517
  if (typeof ref !== "function") return ref;
639
1518
  let resolved;
640
1519
  try {
@@ -644,20 +1523,35 @@ function resolveTableRef({ ref, fail }) {
644
1523
  `target thunk threw (${error instanceof Error ? error.message : String(error)}) \u2014 lazy refs resolve at serialize time; make sure the target module ran`
645
1524
  );
646
1525
  }
647
- if (!isSeqTable(resolved)) return fail("target thunk did not return a table defined with defineTable");
1526
+ if (!isSeqTable(resolved) && !isSeqView(resolved)) {
1527
+ return fail("target thunk did not return a relation defined with defineTable/defineView");
1528
+ }
648
1529
  return resolved;
649
1530
  }
650
- function resolveColumnRef({ table, ref, role }) {
651
- const direct = table.columns[ref];
1531
+ function resolveColumnRef({ relation, ref, role }) {
1532
+ const direct = relation.columns[ref];
652
1533
  if (direct) return direct.columnNameFor(ref);
653
- for (const [propertyName, builder] of Object.entries(table.columns)) {
1534
+ for (const [propertyName, builder] of Object.entries(relation.columns)) {
654
1535
  if (builder.columnNameFor(propertyName) === ref) return ref;
655
1536
  }
656
- const known = Object.keys(table.columns).join(", ");
1537
+ const known = Object.keys(relation.columns).join(", ");
657
1538
  throw new OrmDefinitionError(
658
- `table "${table.tableName}": ${role} references unknown column "${ref}" (known columns: ${known || "none"})`,
659
- table.file
1539
+ `${relationKind(relation)} "${relationName(relation)}": ${role} references unknown column "${ref}" (known columns: ${known || "none"})`,
1540
+ relation.file
1541
+ );
1542
+ }
1543
+ function assertAcyclicViewDependencies(views) {
1544
+ const viewNames = new Set(views.map((view) => view.view));
1545
+ const remaining = new Map(
1546
+ views.map((view) => [view.view, new Set(view.dependencies.filter((dependency) => viewNames.has(dependency)))])
660
1547
  );
1548
+ while (remaining.size > 0) {
1549
+ const ready = [...remaining.entries()].filter(([, dependencies]) => [...dependencies].every((dependency) => !remaining.has(dependency))).map(([name]) => name);
1550
+ if (ready.length === 0) {
1551
+ throw new OrmDefinitionError(`view dependencies form a cycle among: ${[...remaining.keys()].sort().join(", ")}`);
1552
+ }
1553
+ for (const name of ready) remaining.delete(name);
1554
+ }
661
1555
  }
662
1556
  function toCanonicalPolicy(policy) {
663
1557
  if (typeof policy === "string") return policy;
@@ -670,12 +1564,110 @@ function toCanonicalPolicy(policy) {
670
1564
  }))
671
1565
  };
672
1566
  }
673
- function describeFile(table) {
674
- return table.file ?? "<unknown file>";
1567
+ function describeFile(relation) {
1568
+ return relation.file ?? "<unknown file>";
675
1569
  }
676
1570
  function formatZodIssues(error) {
677
1571
  return error.issues.map((issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message).join("; ");
678
1572
  }
1573
+ var SYNCED_CONTRACT_HASH_DOMAIN = "sequence-orm-synced-contract\0";
1574
+ var ORM_TO_PIPELINE_TYPE = {
1575
+ uuid: "uuid",
1576
+ text: "string",
1577
+ integer: "int",
1578
+ bigint: "long",
1579
+ numeric: "numeric",
1580
+ double: "double",
1581
+ boolean: "boolean",
1582
+ timestamptz: "timestamp",
1583
+ date: "date",
1584
+ jsonb: "json"
1585
+ };
1586
+ function isColumnType(value) {
1587
+ return typeof value === "string" && value in ORM_TO_PIPELINE_TYPE;
1588
+ }
1589
+ function syncedContractSurface({
1590
+ namespace,
1591
+ table,
1592
+ columns,
1593
+ primary_key
1594
+ }) {
1595
+ return {
1596
+ namespace,
1597
+ table,
1598
+ columns: Object.fromEntries(
1599
+ Object.entries(columns).map(([name, column]) => [
1600
+ name,
1601
+ { type: column.type, nullable: column.nullable, key: column.key }
1602
+ ])
1603
+ ),
1604
+ primary_key: [...primary_key]
1605
+ };
1606
+ }
1607
+ function computeSyncedContractHash(surface) {
1608
+ return crypto.createHash("sha256").update(SYNCED_CONTRACT_HASH_DOMAIN).update(stableJson(surface)).digest("hex");
1609
+ }
1610
+ function extractOrmSyncedTableContract({
1611
+ namespace,
1612
+ table
1613
+ }) {
1614
+ if (!NAMESPACE_PATTERN.test(namespace)) {
1615
+ throw new OrmDefinitionError(
1616
+ `namespace "${namespace}" must match ${NAMESPACE_PATTERN}`
1617
+ );
1618
+ }
1619
+ if (table.source === void 0) {
1620
+ throw new OrmDefinitionError(
1621
+ `table "${table.table}" is not a synced table \u2014 extractOrmSyncedTableContract requires source: gold.*`
1622
+ );
1623
+ }
1624
+ if (table.columns.length === 0) {
1625
+ throw new OrmDefinitionError(
1626
+ `synced table "${table.table}" has no typed columns \u2014 declare a typed defineSyncedTable contract before exporting`
1627
+ );
1628
+ }
1629
+ const primaryKey = table.primaryKey;
1630
+ if (primaryKey === void 0 || primaryKey.length === 0) {
1631
+ throw new OrmDefinitionError(
1632
+ `synced table "${table.table}" has no primary key \u2014 typed synced contracts require a primary key`
1633
+ );
1634
+ }
1635
+ const pkSet = new Set(primaryKey);
1636
+ const columns = {};
1637
+ for (const column of table.columns) {
1638
+ if (!isColumnType(column.type)) {
1639
+ const enumName = typeof column.type === "object" && column.type !== null && "enum" in column.type ? column.type.enum : String(column.type);
1640
+ throw new OrmDefinitionError(
1641
+ `synced table "${table.table}": column "${column.columnName}" has enum/type '${enumName}', which has no pipeline-type mapping \u2014 use an ORM-mappable scalar`
1642
+ );
1643
+ }
1644
+ columns[column.columnName] = {
1645
+ type: ORM_TO_PIPELINE_TYPE[column.type],
1646
+ nullable: column.nullable,
1647
+ key: pkSet.has(column.columnName)
1648
+ };
1649
+ }
1650
+ for (const key of primaryKey) {
1651
+ if (!(key in columns)) {
1652
+ throw new OrmDefinitionError(
1653
+ `synced table "${table.table}": primary key column "${key}" is not declared in columns`
1654
+ );
1655
+ }
1656
+ }
1657
+ const surface = syncedContractSurface({
1658
+ namespace,
1659
+ table: table.table,
1660
+ columns,
1661
+ primary_key: primaryKey
1662
+ });
1663
+ return {
1664
+ namespace,
1665
+ table: table.table,
1666
+ columns,
1667
+ primary_key: [...primaryKey],
1668
+ content_hash: computeSyncedContractHash(surface)
1669
+ };
1670
+ }
679
1671
  var NAMESPACE_CONFIG_FILE = "sequence.config.ts";
680
1672
  var SCHEMA_DIR = "schema";
681
1673
  async function loadNamespaceFromDir(dir, options = {}) {
@@ -734,16 +1726,20 @@ async function loadNamespaceFromDir(dir, options = {}) {
734
1726
  for (const entry of context.tables) {
735
1727
  if (isSeqTable(entry.value)) tables.push(entry.value);
736
1728
  }
737
- if (tables.length === 0) {
1729
+ const views = [];
1730
+ for (const entry of context.views) {
1731
+ if (isSeqView(entry.value)) views.push(entry.value);
1732
+ }
1733
+ if (tables.length === 0 && views.length === 0) {
738
1734
  throw new OrmDefinitionError(
739
- `no tables registered \u2014 ${SCHEMA_DIR}/*.ts modules must call defineTable(...) at import time`
1735
+ `no relations registered \u2014 ${SCHEMA_DIR}/*.ts modules must call defineTable(...) or defineView(...) at import time`
740
1736
  );
741
1737
  }
742
1738
  const enums = [];
743
1739
  for (const entry of context.enums) {
744
1740
  if (isEnumRef(entry.value)) enums.push(entry.value);
745
1741
  }
746
- const canonical = serializeNamespace({ config: configEntry.value, tables, enums });
1742
+ const canonical = serializeNamespace({ config: configEntry.value, tables, views, enums });
747
1743
  return { canonical, hash: computeCanonicalHash(canonical), files };
748
1744
  }
749
1745
  async function listSchemaFiles(root) {
@@ -768,24 +1764,35 @@ exports.ENUM_BRAND = ENUM_BRAND;
768
1764
  exports.LINK_BRAND = LINK_BRAND;
769
1765
  exports.NAMESPACE_CONFIG_BRAND = NAMESPACE_CONFIG_BRAND;
770
1766
  exports.NAMESPACE_CONFIG_FILE = NAMESPACE_CONFIG_FILE;
1767
+ exports.ORM_TO_PIPELINE_TYPE = ORM_TO_PIPELINE_TYPE;
771
1768
  exports.SCHEMA_DIR = SCHEMA_DIR;
1769
+ exports.SYNCED_CONTRACT_HASH_DOMAIN = SYNCED_CONTRACT_HASH_DOMAIN;
772
1770
  exports.TABLE_BRAND = TABLE_BRAND;
1771
+ exports.VIEW_BRAND = VIEW_BRAND;
773
1772
  exports.activeAuthoringContext = activeAuthoringContext;
774
1773
  exports.beginAuthoringContext = beginAuthoringContext;
775
1774
  exports.computeCanonicalHash = computeCanonicalHash;
1775
+ exports.computeSyncedContractHash = computeSyncedContractHash;
776
1776
  exports.defineEnum = defineEnum;
777
1777
  exports.defineNamespace = defineNamespace;
778
1778
  exports.defineSyncedTable = defineSyncedTable;
779
1779
  exports.defineTable = defineTable;
1780
+ exports.defineView = defineView;
780
1781
  exports.deriveColumnName = deriveColumnName;
781
1782
  exports.endAuthoringContext = endAuthoringContext;
1783
+ exports.extractOrmSyncedTableContract = extractOrmSyncedTableContract;
782
1784
  exports.isColumnBuilder = isColumnBuilder;
783
1785
  exports.isEnumRef = isEnumRef;
784
1786
  exports.isSeqLink = isSeqLink;
785
1787
  exports.isSeqNamespaceConfig = isSeqNamespaceConfig;
786
1788
  exports.isSeqTable = isSeqTable;
1789
+ exports.isSeqView = isSeqView;
787
1790
  exports.loadNamespaceFromDir = loadNamespaceFromDir;
1791
+ exports.overlayWinsUnion = overlayWinsUnion;
1792
+ exports.overrideRowMutation = overrideRowMutation;
1793
+ exports.revertOverlayMutation = revertOverlayMutation;
788
1794
  exports.serializeNamespace = serializeNamespace;
1795
+ exports.syncedContractSurface = syncedContractSurface;
789
1796
  exports.t = t;
790
1797
  exports.toMany = toMany;
791
1798
  exports.toOne = toOne;