@sequenceholdings/orm 0.1.3 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +108 -38
  2. package/dist/authoring.cjs +128 -16
  3. package/dist/authoring.cjs.map +1 -1
  4. package/dist/authoring.d.cts +21 -4
  5. package/dist/authoring.d.ts +21 -4
  6. package/dist/authoring.js +128 -16
  7. package/dist/authoring.js.map +1 -1
  8. package/dist/canonical.cjs +83 -9
  9. package/dist/canonical.cjs.map +1 -1
  10. package/dist/canonical.d.cts +47 -1
  11. package/dist/canonical.d.ts +47 -1
  12. package/dist/canonical.js +83 -10
  13. package/dist/canonical.js.map +1 -1
  14. package/dist/cli.cjs +816 -134
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +818 -136
  17. package/dist/cli.js.map +1 -1
  18. package/dist/client.cjs.map +1 -1
  19. package/dist/client.js.map +1 -1
  20. package/dist/compiler.cjs +488 -96
  21. package/dist/compiler.cjs.map +1 -1
  22. package/dist/compiler.d.cts +34 -2
  23. package/dist/compiler.d.ts +34 -2
  24. package/dist/compiler.js +489 -97
  25. package/dist/compiler.js.map +1 -1
  26. package/dist/index.cjs +420 -71
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +266 -9
  29. package/dist/index.d.ts +266 -9
  30. package/dist/index.js +410 -73
  31. package/dist/index.js.map +1 -1
  32. package/dist/ir.cjs +146 -12
  33. package/dist/ir.cjs.map +1 -1
  34. package/dist/ir.d.cts +161 -5
  35. package/dist/ir.d.ts +161 -5
  36. package/dist/ir.js +144 -13
  37. package/dist/ir.js.map +1 -1
  38. package/dist/{naming-CTvWSRTf.d.ts → naming-DCn94LJ4.d.ts} +15 -1
  39. package/dist/{naming-DRyinU4u.d.cts → naming-DPglwzkB.d.cts} +15 -1
  40. package/dist/predicate-v2.cjs +136 -11
  41. package/dist/predicate-v2.cjs.map +1 -1
  42. package/dist/predicate-v2.d.cts +3 -0
  43. package/dist/predicate-v2.d.ts +3 -0
  44. package/dist/predicate-v2.js +136 -11
  45. package/dist/predicate-v2.js.map +1 -1
  46. package/dist/sdl.cjs +460 -13
  47. package/dist/sdl.cjs.map +1 -1
  48. package/dist/sdl.d.cts +1 -1
  49. package/dist/sdl.d.ts +1 -1
  50. package/dist/sdl.js +459 -14
  51. package/dist/sdl.js.map +1 -1
  52. package/package.json +6 -4
  53. package/src/codegen-gql/preset.cjs +64 -0
  54. package/src/codegen-gql/server-fns.cjs +11 -6
  55. package/templates/orm-namespace/codegen.preset.cjs +17 -0
  56. package/templates/orm-namespace/codegen.ts +1 -1
  57. package/templates/orm-namespace/package.json +3 -0
  58. package/templates/orm-namespace/schema/tables.ts +1 -1
  59. package/templates/orm-namespace/sequence.config.ts +1 -1
@@ -48,6 +48,12 @@ declare class ColumnBuilder {
48
48
  * (`'gen_random_uuid()'` / `'now()'`), which serialize as `defaultFn`.
49
49
  */
50
50
  default(value: ColumnDefaultLiteral): this;
51
+ /**
52
+ * Stamp this column with Postgres `now()` on every UPDATE. Only valid on
53
+ * timestamptz/date; the compiler drops it from SetInput so callers cannot
54
+ * supply a value.
55
+ */
56
+ onUpdate(value: 'now()'): this;
51
57
  description(text: string): this;
52
58
  /** Migration rename hint: the previous Postgres column name. */
53
59
  renamedFrom(previousColumnName: string): this;
@@ -324,6 +330,15 @@ interface IndexInput {
324
330
  unique?: boolean;
325
331
  name?: string;
326
332
  }
333
+ /** Database integrity constraint; independent of GraphQL relationship traversal. */
334
+ interface ForeignKeyInput {
335
+ name: string;
336
+ columns: readonly string[];
337
+ target: TableRef;
338
+ references: readonly string[];
339
+ /** Omit for immediate, non-deferrable PostgreSQL enforcement. */
340
+ deferrable?: 'initially-immediate' | 'initially-deferred';
341
+ }
327
342
  interface TableConfig {
328
343
  description?: string;
329
344
  /** Table-level migration rename hint (new in v2). */
@@ -331,6 +346,7 @@ interface TableConfig {
331
346
  /** Composite primary key: property names (or column names). Mutually exclusive with column-level `.primaryKey()`. */
332
347
  primaryKey?: readonly string[];
333
348
  indexes?: readonly IndexInput[];
349
+ foreignKeys?: readonly ForeignKeyInput[];
334
350
  links?: Record<string, SeqLink>;
335
351
  policies?: TablePoliciesInput;
336
352
  /**
@@ -491,9 +507,10 @@ declare const NAMESPACE_CONFIG_FILE = "sequence.config.ts";
491
507
  declare const SCHEMA_DIR = "schema";
492
508
  interface LoadNamespaceOptions {
493
509
  /**
494
- * Test/dev hook: absolute path that bare `'@sequenceholdings/orm'` imports
495
- * in consumer files resolve to (jiti alias). Leave unset in production —
496
- * the consumer's own installed package resolves naturally.
510
+ * Test/dev hook: absolute path that package-root and `/authoring` imports
511
+ * in consumer files resolve to (jiti aliases). `/drizzle` still resolves to
512
+ * the SDK-pinned query utilities. Leave unset in production — the consumer's
513
+ * own installed package resolves naturally.
497
514
  */
498
515
  ormModulePath?: string;
499
516
  }
@@ -536,4 +553,4 @@ declare function beginAuthoringContext(): AuthoringContext;
536
553
  declare function endAuthoringContext(context: AuthoringContext): void;
537
554
  declare function activeAuthoringContext(): AuthoringContext | undefined;
538
555
 
539
- export { type AuthoringContext, type AuthoringRegistration, COLUMN_BRAND, COLUMN_DEFAULT_FNS, ColumnBuilder, type ColumnDefaultFn, type ColumnDefaultLiteral, type ColumnTypeSpec, type CowMutationInput, type DrizzleRelation, ENUM_BRAND, type EnumRef, type IndexInput, LINK_BRAND, type LinkKind, type LoadNamespaceOptions, type LoadedNamespaceV2, NAMESPACE_CONFIG_BRAND, NAMESPACE_CONFIG_FILE, type NamespaceCapabilitiesInput, type NamespaceConfigInput, ORM_TO_PIPELINE_TYPE, type OrmContractColumnMap, type OrmContractPipelineColumn, type OrmSyncedTableContract, type OrmToPipelineType, type OverlayTableRef, type OverlayWinsUnionInput, type OverrideRowMutationInput, type PolicyInput, type PolicyVariantInput, type PolicyWithVariantsInput, type RelationRef, type RevertOverlayMutationInput, SCHEMA_DIR, SYNCED_CONTRACT_HASH_DOMAIN, type SeqLink, type SeqNamespaceConfig, type SeqTable, type SeqView, type SerializeNamespaceInput, type SyncedTableConfig, type SyncedTablePoliciesInput, TABLE_BRAND, type TableConfig, type TablePoliciesInput, type TableRef, type ToManyOptions, type ToOneOptions, type ToPlatformUserOptions, type TypedSyncedTableConfig, VIEW_BRAND, type ViewConfig, type ViewPoliciesInput, type ViewQueryContext, type ViewSelectQuery, 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 };
556
+ export { type AuthoringContext, type AuthoringRegistration, COLUMN_BRAND, COLUMN_DEFAULT_FNS, ColumnBuilder, type ColumnDefaultFn, type ColumnDefaultLiteral, type ColumnTypeSpec, type CowMutationInput, type DrizzleRelation, ENUM_BRAND, type EnumRef, type ForeignKeyInput, type IndexInput, LINK_BRAND, type LinkKind, type LoadNamespaceOptions, type LoadedNamespaceV2, NAMESPACE_CONFIG_BRAND, NAMESPACE_CONFIG_FILE, type NamespaceCapabilitiesInput, type NamespaceConfigInput, ORM_TO_PIPELINE_TYPE, type OrmContractColumnMap, type OrmContractPipelineColumn, type OrmSyncedTableContract, type OrmToPipelineType, type OverlayTableRef, type OverlayWinsUnionInput, type OverrideRowMutationInput, type PolicyInput, type PolicyVariantInput, type PolicyWithVariantsInput, type RelationRef, type RevertOverlayMutationInput, SCHEMA_DIR, SYNCED_CONTRACT_HASH_DOMAIN, type SeqLink, type SeqNamespaceConfig, type SeqTable, type SeqView, type SerializeNamespaceInput, type SyncedTableConfig, type SyncedTablePoliciesInput, TABLE_BRAND, type TableConfig, type TablePoliciesInput, type TableRef, type ToManyOptions, type ToOneOptions, type ToPlatformUserOptions, type TypedSyncedTableConfig, VIEW_BRAND, type ViewConfig, type ViewPoliciesInput, type ViewQueryContext, type ViewSelectQuery, 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 };
@@ -48,6 +48,12 @@ declare class ColumnBuilder {
48
48
  * (`'gen_random_uuid()'` / `'now()'`), which serialize as `defaultFn`.
49
49
  */
50
50
  default(value: ColumnDefaultLiteral): this;
51
+ /**
52
+ * Stamp this column with Postgres `now()` on every UPDATE. Only valid on
53
+ * timestamptz/date; the compiler drops it from SetInput so callers cannot
54
+ * supply a value.
55
+ */
56
+ onUpdate(value: 'now()'): this;
51
57
  description(text: string): this;
52
58
  /** Migration rename hint: the previous Postgres column name. */
53
59
  renamedFrom(previousColumnName: string): this;
@@ -324,6 +330,15 @@ interface IndexInput {
324
330
  unique?: boolean;
325
331
  name?: string;
326
332
  }
333
+ /** Database integrity constraint; independent of GraphQL relationship traversal. */
334
+ interface ForeignKeyInput {
335
+ name: string;
336
+ columns: readonly string[];
337
+ target: TableRef;
338
+ references: readonly string[];
339
+ /** Omit for immediate, non-deferrable PostgreSQL enforcement. */
340
+ deferrable?: 'initially-immediate' | 'initially-deferred';
341
+ }
327
342
  interface TableConfig {
328
343
  description?: string;
329
344
  /** Table-level migration rename hint (new in v2). */
@@ -331,6 +346,7 @@ interface TableConfig {
331
346
  /** Composite primary key: property names (or column names). Mutually exclusive with column-level `.primaryKey()`. */
332
347
  primaryKey?: readonly string[];
333
348
  indexes?: readonly IndexInput[];
349
+ foreignKeys?: readonly ForeignKeyInput[];
334
350
  links?: Record<string, SeqLink>;
335
351
  policies?: TablePoliciesInput;
336
352
  /**
@@ -491,9 +507,10 @@ declare const NAMESPACE_CONFIG_FILE = "sequence.config.ts";
491
507
  declare const SCHEMA_DIR = "schema";
492
508
  interface LoadNamespaceOptions {
493
509
  /**
494
- * Test/dev hook: absolute path that bare `'@sequenceholdings/orm'` imports
495
- * in consumer files resolve to (jiti alias). Leave unset in production —
496
- * the consumer's own installed package resolves naturally.
510
+ * Test/dev hook: absolute path that package-root and `/authoring` imports
511
+ * in consumer files resolve to (jiti aliases). `/drizzle` still resolves to
512
+ * the SDK-pinned query utilities. Leave unset in production — the consumer's
513
+ * own installed package resolves naturally.
497
514
  */
498
515
  ormModulePath?: string;
499
516
  }
@@ -536,4 +553,4 @@ declare function beginAuthoringContext(): AuthoringContext;
536
553
  declare function endAuthoringContext(context: AuthoringContext): void;
537
554
  declare function activeAuthoringContext(): AuthoringContext | undefined;
538
555
 
539
- export { type AuthoringContext, type AuthoringRegistration, COLUMN_BRAND, COLUMN_DEFAULT_FNS, ColumnBuilder, type ColumnDefaultFn, type ColumnDefaultLiteral, type ColumnTypeSpec, type CowMutationInput, type DrizzleRelation, ENUM_BRAND, type EnumRef, type IndexInput, LINK_BRAND, type LinkKind, type LoadNamespaceOptions, type LoadedNamespaceV2, NAMESPACE_CONFIG_BRAND, NAMESPACE_CONFIG_FILE, type NamespaceCapabilitiesInput, type NamespaceConfigInput, ORM_TO_PIPELINE_TYPE, type OrmContractColumnMap, type OrmContractPipelineColumn, type OrmSyncedTableContract, type OrmToPipelineType, type OverlayTableRef, type OverlayWinsUnionInput, type OverrideRowMutationInput, type PolicyInput, type PolicyVariantInput, type PolicyWithVariantsInput, type RelationRef, type RevertOverlayMutationInput, SCHEMA_DIR, SYNCED_CONTRACT_HASH_DOMAIN, type SeqLink, type SeqNamespaceConfig, type SeqTable, type SeqView, type SerializeNamespaceInput, type SyncedTableConfig, type SyncedTablePoliciesInput, TABLE_BRAND, type TableConfig, type TablePoliciesInput, type TableRef, type ToManyOptions, type ToOneOptions, type ToPlatformUserOptions, type TypedSyncedTableConfig, VIEW_BRAND, type ViewConfig, type ViewPoliciesInput, type ViewQueryContext, type ViewSelectQuery, 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 };
556
+ export { type AuthoringContext, type AuthoringRegistration, COLUMN_BRAND, COLUMN_DEFAULT_FNS, ColumnBuilder, type ColumnDefaultFn, type ColumnDefaultLiteral, type ColumnTypeSpec, type CowMutationInput, type DrizzleRelation, ENUM_BRAND, type EnumRef, type ForeignKeyInput, type IndexInput, LINK_BRAND, type LinkKind, type LoadNamespaceOptions, type LoadedNamespaceV2, NAMESPACE_CONFIG_BRAND, NAMESPACE_CONFIG_FILE, type NamespaceCapabilitiesInput, type NamespaceConfigInput, ORM_TO_PIPELINE_TYPE, type OrmContractColumnMap, type OrmContractPipelineColumn, type OrmSyncedTableContract, type OrmToPipelineType, type OverlayTableRef, type OverlayWinsUnionInput, type OverrideRowMutationInput, type PolicyInput, type PolicyVariantInput, type PolicyWithVariantsInput, type RelationRef, type RevertOverlayMutationInput, SCHEMA_DIR, SYNCED_CONTRACT_HASH_DOMAIN, type SeqLink, type SeqNamespaceConfig, type SeqTable, type SeqView, type SerializeNamespaceInput, type SyncedTableConfig, type SyncedTablePoliciesInput, TABLE_BRAND, type TableConfig, type TablePoliciesInput, type TableRef, type ToManyOptions, type ToOneOptions, type ToPlatformUserOptions, type TypedSyncedTableConfig, VIEW_BRAND, type ViewConfig, type ViewPoliciesInput, type ViewQueryContext, type ViewSelectQuery, 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 };
package/dist/authoring.js CHANGED
@@ -30,6 +30,7 @@ var ColumnBuilder = class {
30
30
  hasDefault: false,
31
31
  defaultValue: void 0,
32
32
  defaultFn: void 0,
33
+ onUpdateFn: void 0,
33
34
  description: void 0,
34
35
  renamedFrom: void 0,
35
36
  cast: void 0,
@@ -65,6 +66,15 @@ var ColumnBuilder = class {
65
66
  this.state.defaultFn = void 0;
66
67
  return this;
67
68
  }
69
+ /**
70
+ * Stamp this column with Postgres `now()` on every UPDATE. Only valid on
71
+ * timestamptz/date; the compiler drops it from SetInput so callers cannot
72
+ * supply a value.
73
+ */
74
+ onUpdate(value) {
75
+ this.state.onUpdateFn = value;
76
+ return this;
77
+ }
68
78
  description(text2) {
69
79
  this.state.description = text2;
70
80
  return this;
@@ -108,6 +118,7 @@ var ColumnBuilder = class {
108
118
  };
109
119
  if (this.state.hasDefault) column.default = this.state.defaultValue ?? null;
110
120
  if (this.state.defaultFn !== void 0) column.defaultFn = this.state.defaultFn;
121
+ if (this.state.onUpdateFn !== void 0) column.onUpdateFn = this.state.onUpdateFn;
111
122
  if (this.state.description !== void 0) column.description = this.state.description;
112
123
  if (this.state.renamedFrom !== void 0) column.renamedFrom = this.state.renamedFrom;
113
124
  if (this.state.cast !== void 0) column.cast = this.state.cast;
@@ -135,6 +146,7 @@ var t = {
135
146
  // src/constants.ts
136
147
  var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
137
148
  var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
149
+ var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
138
150
  var MAX_IDENTIFIER_LENGTH = 63;
139
151
  var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
140
152
  var HASH_DOMAIN = "sequence-orm-definitions\0";
@@ -237,10 +249,10 @@ function isSeqLink(value) {
237
249
  function isSeqTable(value) {
238
250
  return typeof value === "object" && value !== null && value.kind === TABLE_BRAND;
239
251
  }
240
- function assertIdentifier({ value, what, file }) {
241
- if (!IDENTIFIER_PATTERN.test(value) || value.length > MAX_IDENTIFIER_LENGTH) {
252
+ function assertIdentifier({ value, what, file, pattern = IDENTIFIER_PATTERN }) {
253
+ if (!pattern.test(value) || value.length > MAX_IDENTIFIER_LENGTH) {
242
254
  throw new OrmDefinitionError(
243
- `${what} "${value}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`,
255
+ `${what} "${value}" must match ${pattern} and be at most ${MAX_IDENTIFIER_LENGTH} characters`,
244
256
  file
245
257
  );
246
258
  }
@@ -261,7 +273,7 @@ function defineTable(name, columns, config = {}) {
261
273
  );
262
274
  }
263
275
  const columnName = builder.columnNameFor(propertyName);
264
- assertIdentifier({ value: columnName, what: `table "${name}": column "${propertyName}" column name`, file });
276
+ assertIdentifier({ value: columnName, what: `table "${name}": column "${propertyName}" column name`, file, pattern: COLUMN_IDENTIFIER_PATTERN });
265
277
  const existing = seenColumnNames.get(columnName);
266
278
  if (existing !== void 0) {
267
279
  throw new OrmDefinitionError(
@@ -301,7 +313,8 @@ function defineSyncedTable(name, columnsOrConfig, maybeConfig) {
301
313
  assertIdentifier({
302
314
  value: columnName,
303
315
  what: `synced table "${name}": column "${propertyName}" column name`,
304
- file
316
+ file,
317
+ pattern: COLUMN_IDENTIFIER_PATTERN
305
318
  });
306
319
  const existing = seenColumnNames.get(columnName);
307
320
  if (existing !== void 0) {
@@ -390,7 +403,7 @@ function defineView(name, columns, config) {
390
403
  );
391
404
  }
392
405
  const columnName = builder.columnNameFor(propertyName);
393
- if (!IDENTIFIER_PATTERN.test(columnName) || columnName.length > MAX_IDENTIFIER_LENGTH) {
406
+ if (!COLUMN_IDENTIFIER_PATTERN.test(columnName) || columnName.length > MAX_IDENTIFIER_LENGTH) {
394
407
  throw new OrmDefinitionError(`view "${name}": column "${propertyName}" has invalid column name "${columnName}"`, file);
395
408
  }
396
409
  const existing = seenColumnNames.get(columnName);
@@ -839,6 +852,7 @@ function scalarColumnType(table, property, helper) {
839
852
  var CANONICAL_FORMAT = 2;
840
853
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
841
854
  var identifier = z.string().min(1).max(63).regex(IDENTIFIER_RE);
855
+ var columnIdentifier = z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
842
856
  var COLUMN_TYPES = [
843
857
  "uuid",
844
858
  "text",
@@ -859,18 +873,33 @@ var canonicalEnumSchema = z.object({
859
873
  var canonicalColumnSchema = z.object({
860
874
  /** Property name in TS (camelCase); columnName is the Postgres name. */
861
875
  name: z.string().min(1).max(63),
862
- columnName: identifier,
876
+ columnName: columnIdentifier,
863
877
  type: z.union([columnTypeSchema, z.object({ enum: identifier })]),
864
878
  nullable: z.boolean().default(true),
865
879
  unique: z.boolean().default(false),
866
880
  /** Literal default, or one of the allowlisted SQL functions. */
867
881
  default: z.union([z.string(), z.number(), z.boolean(), z.null()]).optional(),
868
882
  defaultFn: z.enum(["gen_random_uuid()", "now()"]).optional(),
883
+ /**
884
+ * Stamp this column from the database clock on UPDATE (`now()`). The
885
+ * column stays insertable (defaults fill gaps) but is omitted from
886
+ * SetInput / settable so callers cannot backdate it.
887
+ */
888
+ onUpdateFn: z.enum(["now()"]).optional(),
869
889
  description: z.string().max(500).optional(),
870
890
  /** Migration hints — same semantics as v1. */
871
- renamedFrom: identifier.optional(),
891
+ renamedFrom: columnIdentifier.optional(),
872
892
  cast: z.string().max(200).optional(),
873
893
  deprecated: z.boolean().default(false)
894
+ }).superRefine((column, ctx) => {
895
+ if (column.onUpdateFn === void 0) return;
896
+ if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
897
+ ctx.addIssue({
898
+ code: "custom",
899
+ path: ["onUpdateFn"],
900
+ message: `onUpdateFn '${column.onUpdateFn}' is only valid on timestamptz/date columns`
901
+ });
902
+ }
874
903
  });
875
904
  var canonicalLinkSchema = z.object({
876
905
  name: z.string().min(1).max(63),
@@ -878,15 +907,15 @@ var canonicalLinkSchema = z.object({
878
907
  /** Target table name (namespace-local). Absent for toPlatformUser. */
879
908
  target: identifier.optional(),
880
909
  /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
881
- on: identifier,
910
+ on: columnIdentifier,
882
911
  /** Referenced column on the target (default: target's PK / platform user `sub`). */
883
- references: identifier.optional(),
912
+ references: columnIdentifier.optional(),
884
913
  /** enforce: true emits a real FK constraint (same-namespace toOne only). */
885
914
  enforce: z.boolean().default(false),
886
915
  description: z.string().max(500).optional()
887
916
  });
888
917
  var canonicalIndexSchema = z.object({
889
- columns: z.array(identifier).min(1),
918
+ columns: z.array(columnIdentifier).min(1),
890
919
  unique: z.boolean().default(false),
891
920
  name: identifier.optional()
892
921
  });
@@ -977,6 +1006,13 @@ var canonicalCowMutationSchema = z.object({
977
1006
  }
978
1007
  }
979
1008
  });
1009
+ var canonicalForeignKeySchema = z.object({
1010
+ name: identifier,
1011
+ columns: z.array(columnIdentifier).min(1).max(8),
1012
+ target: identifier,
1013
+ references: z.array(columnIdentifier).min(1).max(8),
1014
+ deferrable: z.enum(["initially-immediate", "initially-deferred"]).optional()
1015
+ }).strict();
980
1016
  var canonicalTableSchema = z.object({
981
1017
  table: identifier,
982
1018
  description: z.string().max(2e3).optional(),
@@ -985,7 +1021,7 @@ var canonicalTableSchema = z.object({
985
1021
  /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
986
1022
  source: z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
987
1023
  columns: z.array(canonicalColumnSchema).default([]),
988
- primaryKey: z.array(identifier).min(1).max(8).optional(),
1024
+ primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
989
1025
  links: z.array(canonicalLinkSchema).default([]),
990
1026
  indexes: z.array(canonicalIndexSchema).default([]),
991
1027
  policies: canonicalPoliciesSchema.default({}),
@@ -996,14 +1032,16 @@ var canonicalTableSchema = z.object({
996
1032
  */
997
1033
  actions: z.array(canonicalActionSchema).default([]),
998
1034
  /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
999
- cowMutations: z.array(canonicalCowMutationSchema).optional()
1035
+ cowMutations: z.array(canonicalCowMutationSchema).optional(),
1036
+ /** Omitted by old definitions, preserving their existing content hashes. */
1037
+ foreignKeys: z.array(canonicalForeignKeySchema).max(100).optional()
1000
1038
  });
1001
1039
  var canonicalViewSchema = z.object({
1002
1040
  view: identifier,
1003
1041
  description: z.string().max(2e3).optional(),
1004
1042
  columns: z.array(canonicalColumnSchema).min(1),
1005
1043
  /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
1006
- primaryKey: z.array(identifier).min(1).max(8).optional(),
1044
+ primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
1007
1045
  links: z.array(canonicalLinkSchema).default([]),
1008
1046
  policies: z.object({ read: policyExpressionSchema }).strict(),
1009
1047
  query: z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
@@ -1016,6 +1054,9 @@ var canonicalViewSchema = z.object({
1016
1054
  if (column.default !== void 0 || column.defaultFn !== void 0) {
1017
1055
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
1018
1056
  }
1057
+ if (column.onUpdateFn !== void 0) {
1058
+ ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare onUpdateFn` });
1059
+ }
1019
1060
  if (column.unique) {
1020
1061
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare unique` });
1021
1062
  }
@@ -1049,6 +1090,45 @@ var canonicalDefinitionSchema = z.object({
1049
1090
  ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
1050
1091
  }
1051
1092
  seen.add(table.table);
1093
+ if (table.source !== void 0) {
1094
+ for (const [columnIndex, column] of table.columns.entries()) {
1095
+ if (column.onUpdateFn !== void 0) {
1096
+ ctx.addIssue({
1097
+ code: "custom",
1098
+ path: ["tables", index, "columns", columnIndex],
1099
+ message: `synced table '${table.table}' column '${column.name}' cannot declare onUpdateFn`
1100
+ });
1101
+ }
1102
+ }
1103
+ }
1104
+ }
1105
+ const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
1106
+ for (const [tableIndex, table] of definition.tables.entries()) {
1107
+ const names = /* @__PURE__ */ new Set([
1108
+ ...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
1109
+ ...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
1110
+ ...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
1111
+ ]);
1112
+ for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
1113
+ const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
1114
+ if (names.has(fk.name)) fail(`duplicate foreign key '${fk.name}'`);
1115
+ names.add(fk.name);
1116
+ const target = fkTables.get(fk.target);
1117
+ if (table.source || !target || target.source) {
1118
+ fail("foreign keys must connect owned tables in the same namespace");
1119
+ continue;
1120
+ }
1121
+ if (fk.columns.length !== fk.references.length) fail("foreign key columns and references must have the same number of columns");
1122
+ if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail("foreign key columns must not repeat");
1123
+ for (const [position, name] of fk.columns.entries()) {
1124
+ const sourceColumn = table.columns.find((column) => column.columnName === name);
1125
+ const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
1126
+ if (!sourceColumn || !targetColumn) fail("foreign key references an unknown column");
1127
+ else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail("foreign key column types must match");
1128
+ }
1129
+ const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index2) => index2.unique).map((index2) => index2.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
1130
+ if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail("foreign key target columns must form a unique key");
1131
+ }
1052
1132
  }
1053
1133
  const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
1054
1134
  for (const [tableIndex, table] of definition.tables.entries()) {
@@ -1171,12 +1251,13 @@ function normalizeCanonical(definition) {
1171
1251
  ...definition,
1172
1252
  enums: sortByName(definition.enums),
1173
1253
  tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
1174
- const { cowMutations, ...rest } = t2;
1254
+ const { cowMutations, foreignKeys, ...rest } = t2;
1175
1255
  return {
1176
1256
  ...rest,
1177
1257
  links: sortByName(t2.links),
1178
1258
  indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
1179
1259
  actions: sortByName(t2.actions),
1260
+ ...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
1180
1261
  // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
1181
1262
  ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
1182
1263
  };
@@ -1372,6 +1453,22 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1372
1453
  } else if (columnLevelPk.length > 0) {
1373
1454
  candidate.primaryKey = columnLevelPk;
1374
1455
  }
1456
+ if (table.config.foreignKeys?.length) {
1457
+ if (table.source) throw new OrmDefinitionError(`synced table "${table.tableName}" cannot enforce foreign keys`, table.file);
1458
+ candidate.foreignKeys = table.config.foreignKeys.map((fk) => {
1459
+ const target = resolveRelationRef({ ref: fk.target, fail: (message) => {
1460
+ throw new OrmDefinitionError(message, table.file);
1461
+ } });
1462
+ if (!isSeqTable(target) || !relationsByName.has(target.tableName)) throw new OrmDefinitionError("foreign key target must be a table in this namespace", table.file);
1463
+ return {
1464
+ name: fk.name,
1465
+ columns: fk.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: `foreign key "${fk.name}" column` })),
1466
+ target: target.tableName,
1467
+ references: fk.references.map((ref) => resolveColumnRef({ relation: target, ref, role: `foreign key "${fk.name}" reference` })),
1468
+ ...fk.deferrable ? { deferrable: fk.deferrable } : {}
1469
+ };
1470
+ });
1471
+ }
1375
1472
  for (const index of table.config.indexes ?? []) {
1376
1473
  const entry = {
1377
1474
  columns: index.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
@@ -1424,6 +1521,12 @@ function buildCanonicalView({
1424
1521
  if (column.default !== void 0 || column.defaultFn !== void 0) {
1425
1522
  throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare a default`, view.file);
1426
1523
  }
1524
+ if (column.onUpdateFn !== void 0) {
1525
+ throw new OrmDefinitionError(
1526
+ `view "${view.viewName}": column "${propertyName}" cannot declare onUpdateFn`,
1527
+ view.file
1528
+ );
1529
+ }
1427
1530
  if (column.unique) {
1428
1531
  throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare unique`, view.file);
1429
1532
  }
@@ -1682,7 +1785,16 @@ async function loadNamespaceFromDir(dir, options = {}) {
1682
1785
  const jiti = createJiti(import.meta.url, {
1683
1786
  interopDefault: true,
1684
1787
  fsCache: false,
1685
- ...options.ormModulePath !== void 0 ? { alias: { "@sequenceholdings/orm": options.ormModulePath } } : {}
1788
+ ...options.ormModulePath !== void 0 ? {
1789
+ alias: {
1790
+ // The public /drizzle barrel re-exports this pinned dependency.
1791
+ // Explicitly map it before the root alias: otherwise jiti appends
1792
+ // "/drizzle" to the overridden module's filename.
1793
+ "@sequenceholdings/orm/drizzle": createRequire(import.meta.url).resolve("drizzle-orm"),
1794
+ "@sequenceholdings/orm": options.ormModulePath,
1795
+ "@sequenceholdings/orm/authoring": options.ormModulePath
1796
+ }
1797
+ } : {}
1686
1798
  });
1687
1799
  const context = beginAuthoringContext();
1688
1800
  try {