@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
package/README.md CHANGED
@@ -1,47 +1,117 @@
1
1
  # @sequenceholdings/orm
2
2
 
3
- Sequence ORM — the OpCo data layer. V2 namespaces define managed tables and
4
- read-only views in TypeScript, compile named GraphQL operations to persisted
5
- IR, and execute them through the governed Atlas Data API. Legacy v1 YAML
6
- namespaces remain supported during migration.
7
-
8
- Design source of truth: the "Sequence ORM: OpCo Data Layer" Notion page.
9
-
10
- This package is the pure core — no Atlas imports, no database access:
11
-
12
- - `authoring/` — TypeScript `defineTable` / `defineView` DSL. View queries use
13
- Drizzle through `@sequenceholdings/orm/drizzle`; only normalized SQL and
14
- dependencies enter canonical JSON.
15
- - `canonical/` — validated v2 table/view wire contract shared by CLI and Atlas.
16
- - `compiler-gql/`, `sdl/`, and `ir/` — read/write table operations plus
17
- read-only keyed or keyless view queries compiled to persisted operation IR.
18
-
19
- - `schema/` — zod schemas for the YAML definition surface (tables, columns, links, indexes, policies, actions)
20
- - `migrations/` — the committed migration workspace (`orm diff`, offline `--check` verification, destructive-statement classification)
21
- - `predicate/` — the policy expression grammar (`claim('…')`, `capability('…')`, comparisons, `and`/`or`/`not`)
22
- - `compiler/` — predicate AST and identifier/literal helpers → SQL fragments for policies and claims
23
- - `engine/` — namespace definitions → ordered migration plans (`hint` / `ddl` / `security` / `function`) plus drizzle-kit snapshots for the registry. Each declared action compiles to a `SECURITY INVOKER` Postgres function (`orm_<ns>.action_<name>`) created at apply, so the invoke path is a single `SELECT`; writes never run as client-rewritten statements. Apply also `PREPARE`s each executes statement so Postgres validates its columns/types (plpgsql function bodies defer that to first invoke), failing the apply on a bad reference. `executes` may be one statement or an ordered list (run atomically); `returns:` makes the function `RETURNS TABLE(...)` and hands back the RETURNING rows.
24
- - `loader.ts` — a namespace directory of `*.yaml` files → validated `NamespaceDefinition`
25
- - `hash.ts` — canonical content hash (key-order-insensitive) used for content-addressed registry versions
26
- - `client/` (`@sequenceholdings/orm/client`) — typed Data API client; fetch-only, no node builtins. `createOrmClient` acts as the caller (session bearer, or a managed function's delegated user token — the default); `createServiceOrmClient` is the loudly-named escalation that acts as the platform service account attached to the function (its manifest's top-level `service_account`). See the `sequence-orm` skill → "two consumer modes".
27
- - `codegen/` — derives the client's wire types from the definitions; `apply` refreshes `types.gen.ts` on every successful run (there is no separate `generate` command — one verb owns the loop)
28
-
29
- Deploy with `seq-studio orm …` (see `@sequenceholdings/studio-cli`):
3
+ Sequence ORM v2 is a Preview SDK for TypeScript-defined governed data
4
+ namespaces. You define tables, policies, and named GraphQL operations in source
5
+ control; the platform compiles them into persisted operations and enforces the
6
+ caller's access on every execution.
30
7
 
8
+ Sequence ORM is a workflow-agnostic platform primitive. The compiler and
9
+ runtime own generic schema, policy, migration, and execution contracts;
10
+ deployed namespaces own domain tables and operations. Primitive code must not
11
+ branch on a namespace or product identity. See
12
+ [Platform primitive boundaries](../../../docs/platform/platform-primitives.md).
13
+
14
+ ## Install
15
+
16
+ Install the CLI and SDK together:
17
+
18
+ ```bash
19
+ pnpm add -g @sequenceholdings/studio-cli@0.1.35 @sequenceholdings/orm@2.1.0
31
20
  ```
32
- seq-studio orm apply [dir] -e <env> # the everyday loop: author the migration, register, apply, refresh types
33
- seq-studio orm apply [dir] --dry-run -e <env> # rehearse the migration on a copy of the env's real data, then discard it (no real apply)
34
- seq-studio orm plan [dir] -e <env> # offline dry run — compiled SQL + registry status (no database, no writes)
35
- seq-studio orm diff [dir] [--check] # write / verify the committed migration (--check is the offline CI gate)
36
- seq-studio orm validate [dir] # parse + content hash + the offline migration gate
21
+
22
+ Create a namespace project:
23
+
24
+ ```bash
25
+ seq-studio orm init lending
26
+ cd lending
27
+ pnpm install
37
28
  ```
38
29
 
39
- `--dry-run` registers the version and rehearses its plan on a throwaway
40
- copy-on-write fork of the environment's live data — proving the migration
41
- survives real rows (a `NOT NULL` onto populated data, a constraint existing
42
- rows violate) — then discards the fork without applying for real.
30
+ ## Everyday workflow
43
31
 
44
32
  ```bash
45
- pnpm --filter @sequenceholdings/orm build
46
- pnpm --filter @sequenceholdings/orm test
33
+ # Edit sequence.config.ts, schema/*.ts, and graphql/**/*.ts.
34
+ seq-studio orm generate .
35
+ seq-studio orm validate .
36
+ seq-studio orm plan . -e <environment-name>
37
+ seq-studio orm apply . -e <environment-name>
47
38
  ```
39
+
40
+ `generate` writes `schema.graphql`, `operations.manifest.json`, cache policies,
41
+ and typed consumer modules. Generated SDL may contain up to 16 MiB of UTF-8
42
+ text for the complete namespace; individual authored files retain a 1 MiB cap.
43
+ Registration and reviewed migration exports are bounded to 64 MiB including
44
+ canonical metadata, snapshots, documents, and migration statements.
45
+
46
+ Commit generated outputs and migrations with the
47
+ source that produced them.
48
+
49
+ Use `apply --dry-run` to rehearse a migration against a disposable copy of the
50
+ target environment's data. `apply --no-create` refuses to provision a missing
51
+ namespace, which is useful for deployment checks.
52
+
53
+ ## Execute persisted operations
54
+
55
+ ### Guard dependent writes
56
+
57
+ Mutation roots run in one transaction. To require a conditional write to
58
+ succeed before committing dependent roots, add a fixed row-count assertion:
59
+
60
+ ```graphql
61
+ mutation SaveDeal($id: uuid!, $revision: Int!, $title: String!) {
62
+ updateDeals(
63
+ where: { id: { _eq: $id }, revision: { _eq: $revision } }
64
+ _set: { title: $title }
65
+ _inc: { revision: 1 }
66
+ ) @assertAffectedRows(exactly: 1) {
67
+ returning { id revision }
68
+ }
69
+ }
70
+ ```
71
+
72
+ This example assumes a `deals` table with `id`, `title`, and integer `revision`
73
+ columns and suitable write policies. The assertion uses the actual SQL write
74
+ count even when `affectedRows` is not selected. A mismatch rolls back **all**
75
+ roots and the operation journal with HTTP 409 `MUTATION_PRECONDITION_FAILED`.
76
+ Missing, policy-hidden, and stale matches receive the same generic error.
77
+ Ordinary mutations without this directive keep zero-match success semantics.
78
+
79
+ `exactly` must be a literal integer from 0 through 2147483647. It cannot be a
80
+ caller variable. Only mutation root fields may carry the directive, and a
81
+ guarded root cannot also use `@include` or `@skip`. Policy and constraint
82
+ failures retain their existing errors. This does not add automatic retries:
83
+ when the domain permits retry, reread authorized state and recompute the write.
84
+ Every writer affecting the invariant must participate in the revision protocol.
85
+
86
+ Guarded operations use **IR v2**; older runtimes reject them. Upgrade the host
87
+ compiler and executor together before registering guarded documents, regenerate
88
+ SDL with the matching SDK, and only then activate consumers. Existing unguarded
89
+ operations keep their IR v1 and operation hashes. This source addition requires
90
+ a matching SDK release and host rollout; it is not a claim that the published
91
+ Preview version already supports it.
92
+
93
+ ### Execute by name
94
+
95
+ Operations execute by their generated manifest name, never by sending GraphQL
96
+ text or SQL:
97
+
98
+ ```bash
99
+ seq-studio orm execute GetOpenDeals . \
100
+ --variables-file variables.json \
101
+ -e <environment-name>
102
+ ```
103
+
104
+ `variables.json` must contain a JSON object. The platform validates variables,
105
+ operation requirements, and row policies as the signed-in caller.
106
+
107
+ Execute uses the `/_data/v2` endpoint by default. Set
108
+ `SEQUENCE_ORM_ATLAS_EXECUTE_V2=1` to opt into Atlas's `/api/data/v2` endpoint.
109
+ This client rollout switch does not bypass server feature flags or authorization.
110
+
111
+ ## Public exports
112
+
113
+ - `@sequenceholdings/orm/authoring` — namespace, table, policy, and view DSL
114
+ - `@sequenceholdings/orm/client` — persisted-operation client utilities
115
+ - `@sequenceholdings/orm/drizzle` — supported Drizzle helpers for views
116
+ - `@sequenceholdings/orm/codegen` — GraphQL Code Generator preset used by a
117
+ scaffolded namespace
@@ -33,6 +33,7 @@ var ColumnBuilder = class {
33
33
  hasDefault: false,
34
34
  defaultValue: void 0,
35
35
  defaultFn: void 0,
36
+ onUpdateFn: void 0,
36
37
  description: void 0,
37
38
  renamedFrom: void 0,
38
39
  cast: void 0,
@@ -68,6 +69,15 @@ var ColumnBuilder = class {
68
69
  this.state.defaultFn = void 0;
69
70
  return this;
70
71
  }
72
+ /**
73
+ * Stamp this column with Postgres `now()` on every UPDATE. Only valid on
74
+ * timestamptz/date; the compiler drops it from SetInput so callers cannot
75
+ * supply a value.
76
+ */
77
+ onUpdate(value) {
78
+ this.state.onUpdateFn = value;
79
+ return this;
80
+ }
71
81
  description(text2) {
72
82
  this.state.description = text2;
73
83
  return this;
@@ -111,6 +121,7 @@ var ColumnBuilder = class {
111
121
  };
112
122
  if (this.state.hasDefault) column.default = this.state.defaultValue ?? null;
113
123
  if (this.state.defaultFn !== void 0) column.defaultFn = this.state.defaultFn;
124
+ if (this.state.onUpdateFn !== void 0) column.onUpdateFn = this.state.onUpdateFn;
114
125
  if (this.state.description !== void 0) column.description = this.state.description;
115
126
  if (this.state.renamedFrom !== void 0) column.renamedFrom = this.state.renamedFrom;
116
127
  if (this.state.cast !== void 0) column.cast = this.state.cast;
@@ -138,6 +149,7 @@ var t = {
138
149
  // src/constants.ts
139
150
  var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
140
151
  var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
152
+ var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
141
153
  var MAX_IDENTIFIER_LENGTH = 63;
142
154
  var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
143
155
  var HASH_DOMAIN = "sequence-orm-definitions\0";
@@ -240,10 +252,10 @@ function isSeqLink(value) {
240
252
  function isSeqTable(value) {
241
253
  return typeof value === "object" && value !== null && value.kind === TABLE_BRAND;
242
254
  }
243
- function assertIdentifier({ value, what, file }) {
244
- if (!IDENTIFIER_PATTERN.test(value) || value.length > MAX_IDENTIFIER_LENGTH) {
255
+ function assertIdentifier({ value, what, file, pattern = IDENTIFIER_PATTERN }) {
256
+ if (!pattern.test(value) || value.length > MAX_IDENTIFIER_LENGTH) {
245
257
  throw new OrmDefinitionError(
246
- `${what} "${value}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`,
258
+ `${what} "${value}" must match ${pattern} and be at most ${MAX_IDENTIFIER_LENGTH} characters`,
247
259
  file
248
260
  );
249
261
  }
@@ -264,7 +276,7 @@ function defineTable(name, columns, config = {}) {
264
276
  );
265
277
  }
266
278
  const columnName = builder.columnNameFor(propertyName);
267
- assertIdentifier({ value: columnName, what: `table "${name}": column "${propertyName}" column name`, file });
279
+ assertIdentifier({ value: columnName, what: `table "${name}": column "${propertyName}" column name`, file, pattern: COLUMN_IDENTIFIER_PATTERN });
268
280
  const existing = seenColumnNames.get(columnName);
269
281
  if (existing !== void 0) {
270
282
  throw new OrmDefinitionError(
@@ -304,7 +316,8 @@ function defineSyncedTable(name, columnsOrConfig, maybeConfig) {
304
316
  assertIdentifier({
305
317
  value: columnName,
306
318
  what: `synced table "${name}": column "${propertyName}" column name`,
307
- file
319
+ file,
320
+ pattern: COLUMN_IDENTIFIER_PATTERN
308
321
  });
309
322
  const existing = seenColumnNames.get(columnName);
310
323
  if (existing !== void 0) {
@@ -393,7 +406,7 @@ function defineView(name, columns, config) {
393
406
  );
394
407
  }
395
408
  const columnName = builder.columnNameFor(propertyName);
396
- if (!IDENTIFIER_PATTERN.test(columnName) || columnName.length > MAX_IDENTIFIER_LENGTH) {
409
+ if (!COLUMN_IDENTIFIER_PATTERN.test(columnName) || columnName.length > MAX_IDENTIFIER_LENGTH) {
397
410
  throw new OrmDefinitionError(`view "${name}": column "${propertyName}" has invalid column name "${columnName}"`, file);
398
411
  }
399
412
  const existing = seenColumnNames.get(columnName);
@@ -842,6 +855,7 @@ function scalarColumnType(table, property, helper) {
842
855
  var CANONICAL_FORMAT = 2;
843
856
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
844
857
  var identifier = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
858
+ var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
845
859
  var COLUMN_TYPES = [
846
860
  "uuid",
847
861
  "text",
@@ -862,18 +876,33 @@ var canonicalEnumSchema = zod.z.object({
862
876
  var canonicalColumnSchema = zod.z.object({
863
877
  /** Property name in TS (camelCase); columnName is the Postgres name. */
864
878
  name: zod.z.string().min(1).max(63),
865
- columnName: identifier,
879
+ columnName: columnIdentifier,
866
880
  type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier })]),
867
881
  nullable: zod.z.boolean().default(true),
868
882
  unique: zod.z.boolean().default(false),
869
883
  /** Literal default, or one of the allowlisted SQL functions. */
870
884
  default: zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()]).optional(),
871
885
  defaultFn: zod.z.enum(["gen_random_uuid()", "now()"]).optional(),
886
+ /**
887
+ * Stamp this column from the database clock on UPDATE (`now()`). The
888
+ * column stays insertable (defaults fill gaps) but is omitted from
889
+ * SetInput / settable so callers cannot backdate it.
890
+ */
891
+ onUpdateFn: zod.z.enum(["now()"]).optional(),
872
892
  description: zod.z.string().max(500).optional(),
873
893
  /** Migration hints — same semantics as v1. */
874
- renamedFrom: identifier.optional(),
894
+ renamedFrom: columnIdentifier.optional(),
875
895
  cast: zod.z.string().max(200).optional(),
876
896
  deprecated: zod.z.boolean().default(false)
897
+ }).superRefine((column, ctx) => {
898
+ if (column.onUpdateFn === void 0) return;
899
+ if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
900
+ ctx.addIssue({
901
+ code: "custom",
902
+ path: ["onUpdateFn"],
903
+ message: `onUpdateFn '${column.onUpdateFn}' is only valid on timestamptz/date columns`
904
+ });
905
+ }
877
906
  });
878
907
  var canonicalLinkSchema = zod.z.object({
879
908
  name: zod.z.string().min(1).max(63),
@@ -881,15 +910,15 @@ var canonicalLinkSchema = zod.z.object({
881
910
  /** Target table name (namespace-local). Absent for toPlatformUser. */
882
911
  target: identifier.optional(),
883
912
  /** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
884
- on: identifier,
913
+ on: columnIdentifier,
885
914
  /** Referenced column on the target (default: target's PK / platform user `sub`). */
886
- references: identifier.optional(),
915
+ references: columnIdentifier.optional(),
887
916
  /** enforce: true emits a real FK constraint (same-namespace toOne only). */
888
917
  enforce: zod.z.boolean().default(false),
889
918
  description: zod.z.string().max(500).optional()
890
919
  });
891
920
  var canonicalIndexSchema = zod.z.object({
892
- columns: zod.z.array(identifier).min(1),
921
+ columns: zod.z.array(columnIdentifier).min(1),
893
922
  unique: zod.z.boolean().default(false),
894
923
  name: identifier.optional()
895
924
  });
@@ -980,6 +1009,13 @@ var canonicalCowMutationSchema = zod.z.object({
980
1009
  }
981
1010
  }
982
1011
  });
1012
+ var canonicalForeignKeySchema = zod.z.object({
1013
+ name: identifier,
1014
+ columns: zod.z.array(columnIdentifier).min(1).max(8),
1015
+ target: identifier,
1016
+ references: zod.z.array(columnIdentifier).min(1).max(8),
1017
+ deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional()
1018
+ }).strict();
983
1019
  var canonicalTableSchema = zod.z.object({
984
1020
  table: identifier,
985
1021
  description: zod.z.string().max(2e3).optional(),
@@ -988,7 +1024,7 @@ var canonicalTableSchema = zod.z.object({
988
1024
  /** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
989
1025
  source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
990
1026
  columns: zod.z.array(canonicalColumnSchema).default([]),
991
- primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
1027
+ primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
992
1028
  links: zod.z.array(canonicalLinkSchema).default([]),
993
1029
  indexes: zod.z.array(canonicalIndexSchema).default([]),
994
1030
  policies: canonicalPoliciesSchema.default({}),
@@ -999,14 +1035,16 @@ var canonicalTableSchema = zod.z.object({
999
1035
  */
1000
1036
  actions: zod.z.array(canonicalActionSchema).default([]),
1001
1037
  /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
1002
- cowMutations: zod.z.array(canonicalCowMutationSchema).optional()
1038
+ cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
1039
+ /** Omitted by old definitions, preserving their existing content hashes. */
1040
+ foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional()
1003
1041
  });
1004
1042
  var canonicalViewSchema = zod.z.object({
1005
1043
  view: identifier,
1006
1044
  description: zod.z.string().max(2e3).optional(),
1007
1045
  columns: zod.z.array(canonicalColumnSchema).min(1),
1008
1046
  /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
1009
- primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
1047
+ primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
1010
1048
  links: zod.z.array(canonicalLinkSchema).default([]),
1011
1049
  policies: zod.z.object({ read: policyExpressionSchema }).strict(),
1012
1050
  query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
@@ -1019,6 +1057,9 @@ var canonicalViewSchema = zod.z.object({
1019
1057
  if (column.default !== void 0 || column.defaultFn !== void 0) {
1020
1058
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
1021
1059
  }
1060
+ if (column.onUpdateFn !== void 0) {
1061
+ ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare onUpdateFn` });
1062
+ }
1022
1063
  if (column.unique) {
1023
1064
  ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare unique` });
1024
1065
  }
@@ -1052,6 +1093,45 @@ var canonicalDefinitionSchema = zod.z.object({
1052
1093
  ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
1053
1094
  }
1054
1095
  seen.add(table.table);
1096
+ if (table.source !== void 0) {
1097
+ for (const [columnIndex, column] of table.columns.entries()) {
1098
+ if (column.onUpdateFn !== void 0) {
1099
+ ctx.addIssue({
1100
+ code: "custom",
1101
+ path: ["tables", index, "columns", columnIndex],
1102
+ message: `synced table '${table.table}' column '${column.name}' cannot declare onUpdateFn`
1103
+ });
1104
+ }
1105
+ }
1106
+ }
1107
+ }
1108
+ const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
1109
+ for (const [tableIndex, table] of definition.tables.entries()) {
1110
+ const names = /* @__PURE__ */ new Set([
1111
+ ...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
1112
+ ...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
1113
+ ...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
1114
+ ]);
1115
+ for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
1116
+ const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
1117
+ if (names.has(fk.name)) fail(`duplicate foreign key '${fk.name}'`);
1118
+ names.add(fk.name);
1119
+ const target = fkTables.get(fk.target);
1120
+ if (table.source || !target || target.source) {
1121
+ fail("foreign keys must connect owned tables in the same namespace");
1122
+ continue;
1123
+ }
1124
+ if (fk.columns.length !== fk.references.length) fail("foreign key columns and references must have the same number of columns");
1125
+ if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail("foreign key columns must not repeat");
1126
+ for (const [position, name] of fk.columns.entries()) {
1127
+ const sourceColumn = table.columns.find((column) => column.columnName === name);
1128
+ const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
1129
+ if (!sourceColumn || !targetColumn) fail("foreign key references an unknown column");
1130
+ else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail("foreign key column types must match");
1131
+ }
1132
+ const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index2) => index2.unique).map((index2) => index2.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
1133
+ 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");
1134
+ }
1055
1135
  }
1056
1136
  const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
1057
1137
  for (const [tableIndex, table] of definition.tables.entries()) {
@@ -1174,12 +1254,13 @@ function normalizeCanonical(definition) {
1174
1254
  ...definition,
1175
1255
  enums: sortByName(definition.enums),
1176
1256
  tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
1177
- const { cowMutations, ...rest } = t2;
1257
+ const { cowMutations, foreignKeys, ...rest } = t2;
1178
1258
  return {
1179
1259
  ...rest,
1180
1260
  links: sortByName(t2.links),
1181
1261
  indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
1182
1262
  actions: sortByName(t2.actions),
1263
+ ...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
1183
1264
  // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
1184
1265
  ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
1185
1266
  };
@@ -1375,6 +1456,22 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
1375
1456
  } else if (columnLevelPk.length > 0) {
1376
1457
  candidate.primaryKey = columnLevelPk;
1377
1458
  }
1459
+ if (table.config.foreignKeys?.length) {
1460
+ if (table.source) throw new OrmDefinitionError(`synced table "${table.tableName}" cannot enforce foreign keys`, table.file);
1461
+ candidate.foreignKeys = table.config.foreignKeys.map((fk) => {
1462
+ const target = resolveRelationRef({ ref: fk.target, fail: (message) => {
1463
+ throw new OrmDefinitionError(message, table.file);
1464
+ } });
1465
+ if (!isSeqTable(target) || !relationsByName.has(target.tableName)) throw new OrmDefinitionError("foreign key target must be a table in this namespace", table.file);
1466
+ return {
1467
+ name: fk.name,
1468
+ columns: fk.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: `foreign key "${fk.name}" column` })),
1469
+ target: target.tableName,
1470
+ references: fk.references.map((ref) => resolveColumnRef({ relation: target, ref, role: `foreign key "${fk.name}" reference` })),
1471
+ ...fk.deferrable ? { deferrable: fk.deferrable } : {}
1472
+ };
1473
+ });
1474
+ }
1378
1475
  for (const index of table.config.indexes ?? []) {
1379
1476
  const entry = {
1380
1477
  columns: index.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
@@ -1427,6 +1524,12 @@ function buildCanonicalView({
1427
1524
  if (column.default !== void 0 || column.defaultFn !== void 0) {
1428
1525
  throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare a default`, view.file);
1429
1526
  }
1527
+ if (column.onUpdateFn !== void 0) {
1528
+ throw new OrmDefinitionError(
1529
+ `view "${view.viewName}": column "${propertyName}" cannot declare onUpdateFn`,
1530
+ view.file
1531
+ );
1532
+ }
1430
1533
  if (column.unique) {
1431
1534
  throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare unique`, view.file);
1432
1535
  }
@@ -1685,7 +1788,16 @@ async function loadNamespaceFromDir(dir, options = {}) {
1685
1788
  const jiti$1 = jiti.createJiti((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('authoring.cjs', document.baseURI).href)), {
1686
1789
  interopDefault: true,
1687
1790
  fsCache: false,
1688
- ...options.ormModulePath !== void 0 ? { alias: { "@sequenceholdings/orm": options.ormModulePath } } : {}
1791
+ ...options.ormModulePath !== void 0 ? {
1792
+ alias: {
1793
+ // The public /drizzle barrel re-exports this pinned dependency.
1794
+ // Explicitly map it before the root alias: otherwise jiti appends
1795
+ // "/drizzle" to the overridden module's filename.
1796
+ "@sequenceholdings/orm/drizzle": module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('authoring.cjs', document.baseURI).href))).resolve("drizzle-orm"),
1797
+ "@sequenceholdings/orm": options.ormModulePath,
1798
+ "@sequenceholdings/orm/authoring": options.ormModulePath
1799
+ }
1800
+ } : {}
1689
1801
  });
1690
1802
  const context = beginAuthoringContext();
1691
1803
  try {