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