@prisma-next/sql-contract 0.13.0 → 0.14.0-dev.10

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 (48) hide show
  1. package/dist/{types-DqhaAjCH.mjs → entity-kinds-Cl36zL5j.mjs} +121 -205
  2. package/dist/entity-kinds-Cl36zL5j.mjs.map +1 -0
  3. package/dist/entity-kinds.d.mts +18 -0
  4. package/dist/entity-kinds.d.mts.map +1 -0
  5. package/dist/entity-kinds.mjs +2 -0
  6. package/dist/factories.d.mts +2 -2
  7. package/dist/factories.mjs +2 -1
  8. package/dist/factories.mjs.map +1 -1
  9. package/dist/index-type-validation.d.mts +1 -1
  10. package/dist/index-type-validation.mjs +9 -12
  11. package/dist/index-type-validation.mjs.map +1 -1
  12. package/dist/resolve-storage-table.d.mts +2 -1
  13. package/dist/resolve-storage-table.d.mts.map +1 -1
  14. package/dist/resolve-storage-table.mjs +11 -8
  15. package/dist/resolve-storage-table.mjs.map +1 -1
  16. package/dist/sql-storage-Dga0jwP2.d.mts +128 -0
  17. package/dist/sql-storage-Dga0jwP2.d.mts.map +1 -0
  18. package/dist/{sql-storage-CXf9xjAL.d.mts → storage-value-set-WnYsIFM8.d.mts} +8 -120
  19. package/dist/storage-value-set-WnYsIFM8.d.mts.map +1 -0
  20. package/dist/types-B-eiQXff.mjs +191 -0
  21. package/dist/types-B-eiQXff.mjs.map +1 -0
  22. package/dist/{types-DEnWD3xB.d.mts → types-B1N8w0I2.d.mts} +11 -62
  23. package/dist/types-B1N8w0I2.d.mts.map +1 -0
  24. package/dist/types.d.mts +4 -3
  25. package/dist/types.mjs +3 -2
  26. package/dist/validators.d.mts +75 -40
  27. package/dist/validators.d.mts.map +1 -1
  28. package/dist/validators.mjs +156 -184
  29. package/dist/validators.mjs.map +1 -1
  30. package/package.json +8 -7
  31. package/src/entity-kinds.ts +45 -0
  32. package/src/exports/entity-kinds.ts +5 -0
  33. package/src/exports/types.ts +2 -4
  34. package/src/index-type-validation.ts +2 -3
  35. package/src/ir/build-sql-namespace.ts +39 -32
  36. package/src/ir/sql-node.ts +2 -2
  37. package/src/ir/sql-storage.ts +22 -24
  38. package/src/ir/sql-unbound-namespace.ts +15 -3
  39. package/src/ir/storage-entry-schemas.ts +128 -0
  40. package/src/ir/storage-type-instance.ts +3 -3
  41. package/src/ir/storage-value-set.ts +6 -5
  42. package/src/resolve-storage-table.ts +12 -17
  43. package/src/types.ts +10 -10
  44. package/src/validators.ts +288 -225
  45. package/dist/sql-storage-CXf9xjAL.d.mts.map +0 -1
  46. package/dist/types-DEnWD3xB.d.mts.map +0 -1
  47. package/dist/types-DqhaAjCH.mjs.map +0 -1
  48. package/src/ir/postgres-enum-storage-entry.ts +0 -57
@@ -1,32 +1,21 @@
1
- import { C as SqlUnboundNamespace, f as buildSqlNamespaceMap, i as SqlStorage } from "./types-DqhaAjCH.mjs";
2
- import { UNBOUND_NAMESPACE_ID } from "@prisma-next/framework-components/ir";
3
- import { blindCast } from "@prisma-next/utils/casts";
1
+ import { C as StorageTableSchema, S as ReferentialActionSchema, _ as ColumnDefaultSchema, b as ForeignKeySourceSchema, g as ColumnDefaultLiteralSchema, h as ColumnDefaultFunctionSchema, m as CheckConstraintSchema, t as composeSqlEntityKinds, v as ForeignKeyReferenceSchema, w as StorageValueSetSchema, x as IndexSchema, y as ForeignKeySchema } from "./entity-kinds-Cl36zL5j.mjs";
2
+ import { i as SqlStorage, l as buildSqlNamespaceMap, u as SqlUnboundNamespace } from "./types-B-eiQXff.mjs";
3
+ import { type } from "arktype";
4
+ import { UNBOUND_NAMESPACE_ID, isPlainRecord } from "@prisma-next/framework-components/ir";
4
5
  import { CrossReferenceSchema } from "@prisma-next/contract/types";
6
+ import { blindCast } from "@prisma-next/utils/casts";
5
7
  import { ContractValidationError } from "@prisma-next/contract/contract-validation-error";
6
8
  import { validateContractDomain } from "@prisma-next/contract/validate-domain";
7
9
  import { ifDefined } from "@prisma-next/utils/defined";
8
- import { type } from "arktype";
9
10
  //#region src/validators.ts
10
- const literalKindSchema = type("'literal'");
11
- const functionKindSchema = type("'function'");
12
11
  const generatorKindSchema = type("'generator'");
13
12
  const ControlPolicySchema = type("'managed' | 'tolerated' | 'external' | 'observed'");
14
- const generatorIdSchema = type("string").narrow((value, ctx) => {
15
- return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(value) ? true : ctx.mustBe("a flat generator id");
16
- });
17
- const ColumnDefaultLiteralSchema = type.declare().type({
18
- kind: literalKindSchema,
19
- value: "string | number | boolean | null | unknown[] | Record<string, unknown>"
20
- });
21
- const ColumnDefaultFunctionSchema = type.declare().type({
22
- kind: functionKindSchema,
23
- expression: "string"
24
- });
25
- const ColumnDefaultSchema = ColumnDefaultLiteralSchema.or(ColumnDefaultFunctionSchema);
26
13
  const ExecutionMutationDefaultValueSchema = type({
27
14
  "+": "reject",
28
15
  kind: generatorKindSchema,
29
- id: generatorIdSchema,
16
+ id: type("string").narrow((value, ctx) => {
17
+ return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(value) ? true : ctx.mustBe("a flat generator id");
18
+ }),
30
19
  "params?": "Record<string, unknown>"
31
20
  });
32
21
  const ExecutionSchema = type({
@@ -38,6 +27,7 @@ const ExecutionSchema = type({
38
27
  "+": "reject",
39
28
  ref: {
40
29
  "+": "reject",
30
+ namespace: "string",
41
31
  table: "string",
42
32
  column: "string"
43
33
  },
@@ -46,63 +36,21 @@ const ExecutionSchema = type({
46
36
  }).array().readonly()
47
37
  }
48
38
  });
49
- const ValueSetRefSchema = type({
50
- plane: "'domain' | 'storage'",
39
+ const DomainEnumRefSchema = type({
40
+ plane: "'domain'",
51
41
  namespaceId: "string",
52
- entityKind: "'enum' | 'value-set'",
53
- name: "string",
42
+ entityKind: "'enum'",
43
+ entityName: "string",
54
44
  "spaceId?": "string"
55
45
  });
56
- const StorageColumnSchema = type({
57
- "+": "reject",
58
- nativeType: "string",
59
- codecId: "string",
60
- nullable: "boolean",
61
- "typeParams?": "Record<string, unknown>",
62
- "typeRef?": "string",
63
- "default?": ColumnDefaultSchema,
64
- "control?": ControlPolicySchema,
65
- "valueSet?": ValueSetRefSchema
66
- }).narrow((col, ctx) => {
67
- if (col.typeParams !== void 0 && col.typeRef !== void 0) return ctx.mustBe("a column with either typeParams or typeRef, not both");
68
- return true;
69
- });
70
- /**
71
- * Codec-triple entry persisted under `storage.types[name]`. Carries an
72
- * enumerable literal `kind: 'codec-instance'` discriminator so the
73
- * polymorphic slot dispatch can distinguish codec triples from
74
- * class-instance kinds (e.g. `'postgres-enum'`) sharing the slot.
75
- */
76
- const StorageTypeInstanceSchema = type.declare().type({
46
+ /** Document-scoped `storage.types`: codec triples only. */
47
+ const DocumentScopedStorageTypeSchema = type.declare().type({
77
48
  kind: "'codec-instance'",
78
49
  codecId: "string",
79
50
  nativeType: "string",
80
51
  "typeParams?": "Record<string, unknown>"
81
52
  });
82
53
  /**
83
- * Postgres native enum entry under `storage.namespaces[namespaceId].entries.type[name]`.
84
- * Document-scoped `storage.types` carries codec aliases only
85
- * (`DocumentScopedStorageTypeSchema`).
86
- */
87
- const PostgresEnumTypeSchema = type({
88
- kind: "'postgres-enum'",
89
- "name?": "string",
90
- "nativeType?": "string",
91
- values: type.string.array().readonly(),
92
- "control?": ControlPolicySchema
93
- });
94
- /** Document-scoped `storage.types`: codec triples only. */
95
- const DocumentScopedStorageTypeSchema = StorageTypeInstanceSchema;
96
- /**
97
- * Storage value-set entry under `storage.namespaces[id].entries.valueSet[name]`.
98
- * Carries a `kind: 'value-set'` discriminator (enumerable, survives JSON) and an
99
- * ordered `values` array of codec-encoded permitted values.
100
- */
101
- const StorageValueSetSchema = type({
102
- kind: "'value-set'",
103
- values: type.string.array().readonly()
104
- });
105
- /**
106
54
  * Domain enum entry under `domain.namespaces[id].enum[name]`.
107
55
  * Carries the codec id and an ordered `members` array of `{name, value}` pairs.
108
56
  */
@@ -111,130 +59,55 @@ const ContractEnumSchema = type({
111
59
  codecId: "string",
112
60
  members: type({
113
61
  name: "string",
114
- value: "string"
62
+ value: "string | number | boolean | null | unknown[] | Record<string, unknown>"
115
63
  }).array().readonly()
116
64
  });
117
- const PrimaryKeySchema = type.declare().type({
118
- columns: type.string.array().readonly(),
119
- "name?": "string"
120
- });
121
- const UniqueConstraintSchema = type.declare().type({
122
- columns: type.string.array().readonly(),
123
- "name?": "string"
124
- });
125
- const IndexSchema = type({
126
- columns: type.string.array().readonly(),
127
- "name?": "string",
128
- "type?": "string",
129
- "options?": "Record<string, unknown>"
130
- });
131
- const ForeignKeyReferenceSchema = type({
132
- "+": "reject",
133
- namespaceId: "string",
134
- tableName: "string",
135
- columns: type.string.array().readonly(),
136
- "spaceId?": "string"
137
- });
138
- const ForeignKeySourceSchema = type({
139
- "+": "reject",
140
- namespaceId: "string",
141
- tableName: "string",
142
- columns: type.string.array().readonly()
143
- });
144
- const ReferentialActionSchema = type.declare().type("'noAction' | 'restrict' | 'cascade' | 'setNull' | 'setDefault'");
145
- const ForeignKeySchema = type.declare().type({
146
- source: ForeignKeySourceSchema,
147
- target: ForeignKeyReferenceSchema,
148
- "name?": "string",
149
- "onDelete?": ReferentialActionSchema,
150
- "onUpdate?": ReferentialActionSchema,
151
- constraint: "boolean",
152
- index: "boolean"
153
- });
154
- const CheckConstraintSchema = type({
155
- "+": "reject",
156
- name: "string",
157
- column: "string",
158
- valueSet: ValueSetRefSchema
159
- });
160
- const StorageTableSchema = type({
161
- "+": "reject",
162
- columns: type({ "[string]": StorageColumnSchema }),
163
- "primaryKey?": PrimaryKeySchema,
164
- uniques: UniqueConstraintSchema.array().readonly(),
165
- indexes: IndexSchema.array().readonly(),
166
- foreignKeys: ForeignKeySchema.array().readonly(),
167
- "control?": ControlPolicySchema,
168
- "checks?": CheckConstraintSchema.array().readonly()
169
- });
170
65
  /**
171
- * Composes a hardcoded family `fallback` schema with optional
172
- * pack-contributed `fragments` keyed by the entry's `kind`
173
- * discriminator. The composition is **additive**, not substitutive:
174
- *
175
- * - No fragments registered → entries are validated by `fallback`
176
- * alone (the unchanged baseline).
177
- * - An entry's `kind` matches `fallbackKind` AND a fragment for that
178
- * kind is registered → the entry must pass **both** `fallback` and
179
- * the fragment. This preserves family-owned invariants (e.g. the
180
- * built-in `PostgresEnumType` shape) even when a pack contributes
181
- * its own schema for the same kind.
182
- * - An entry's `kind` matches a registered fragment for some
183
- * non-fallback kind → the fragment alone validates the entry.
184
- * `fallback` is family-specific (validates a single hardcoded kind)
185
- * and would reject any other kind, so it does not apply here.
186
- * - An entry's `kind` matches no fragment → fall through to
187
- * `fallback`.
66
+ * Derives a schema map from a descriptor map: maps each kind's key to its
67
+ * `schema` field. Used by validation functions to validate entries.
188
68
  */
189
- function namespaceSlotEntrySchema(fallback, fallbackKind, fragments) {
190
- if (fragments === void 0 || fragments.size === 0) return fallback;
191
- return type("unknown").narrow((entry, ctx) => {
192
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return ctx.mustBe("an object");
193
- const kind = entry.kind;
194
- if (typeof kind === "string") {
195
- const fragment = fragments.get(kind);
196
- if (fragment !== void 0) {
197
- if (kind === fallbackKind) {
198
- const baseParsed = fallback(entry);
199
- if (baseParsed instanceof type.errors) return ctx.reject({ expected: baseParsed.summary });
200
- }
201
- const parsed = fragment(entry);
202
- if (parsed instanceof type.errors) return ctx.reject({ expected: parsed.summary });
203
- return true;
204
- }
205
- }
206
- const parsed = fallback(entry);
207
- if (parsed instanceof type.errors) return ctx.reject({ expected: parsed.summary });
208
- return true;
209
- });
69
+ function schemaViewOf(kinds) {
70
+ return new Map([...kinds].map(([k, d]) => [k, d.schema]));
210
71
  }
72
+ const DEFAULT_SQL_KINDS = composeSqlEntityKinds();
211
73
  /**
212
74
  * Builds the per-namespace entry schema for `storage.namespaces[id]`.
213
- * Pack-contributed `validatorSchema` fragments — keyed by the
214
- * descriptor's `discriminator` validate each entry by matching the
215
- * entry's `kind` field on the `'entries.type'` slot.
75
+ *
76
+ * Validation is descriptor-driven: the `kinds` map carries both the schema
77
+ * (used here for structural validation) and the construct function (used at
78
+ * hydration time). An unregistered key fails validation naming the kind and
79
+ * the namespace id, so validation fails closed.
216
80
  */
217
- function createNamespaceEntrySchema(fragments) {
81
+ function createNamespaceEntrySchema(kinds) {
82
+ const schemas = schemaViewOf(kinds);
83
+ const knownKinds = new Set(kinds.keys());
218
84
  return type({
219
85
  "+": "reject",
220
86
  id: "string",
221
87
  "kind?": "string",
222
- entries: type({
223
- "+": "reject",
224
- "table?": type({ "[string]": StorageTableSchema }),
225
- "type?": type({ "[string]": namespaceSlotEntrySchema(PostgresEnumTypeSchema, "postgres-enum", fragments) }),
226
- "valueSet?": type({ "[string]": StorageValueSetSchema })
227
- })
88
+ entries: "object"
89
+ }).narrow((ns, ctx) => {
90
+ if (!isPlainRecord(ns.entries)) return ctx.mustBe("an entries object");
91
+ for (const [key, innerMap] of Object.entries(ns.entries)) {
92
+ if (!knownKinds.has(key)) return ctx.reject({ expected: `entries key "${key}" in namespace "${ns.id}" is not a registered entity kind` });
93
+ if (!isPlainRecord(innerMap)) return ctx.reject({ expected: `entries["${key}"] in namespace "${ns.id}" must be an object` });
94
+ const entrySchema = blindCast(schemas.get(key));
95
+ for (const [, value] of Object.entries(innerMap)) {
96
+ const parsed = entrySchema(value);
97
+ if (parsed instanceof type.errors) return ctx.reject({ expected: parsed.summary });
98
+ }
99
+ }
100
+ return true;
228
101
  });
229
102
  }
230
103
  /**
231
104
  * Builds the storage schema. Pack contributions reach the per-namespace
232
105
  * entry shape through {@link createNamespaceEntrySchema}; the
233
- * document-scoped `storage.types` slot (codec triples only) and the
106
+ * document-scoped `storage.types` field (codec triples only) and the
234
107
  * storage hash stay family-shared.
235
108
  */
236
- function createSqlStorageSchema(fragments) {
237
- const namespaceEntry = createNamespaceEntrySchema(fragments);
109
+ function createSqlStorageSchema(kinds) {
110
+ const namespaceEntry = createNamespaceEntrySchema(kinds);
238
111
  return type({
239
112
  "+": "reject",
240
113
  storageHash: "string",
@@ -242,17 +115,14 @@ function createSqlStorageSchema(fragments) {
242
115
  "namespaces?": type({ "[string]": namespaceEntry })
243
116
  });
244
117
  }
245
- const StorageSchema = createSqlStorageSchema();
118
+ const StorageSchema = createSqlStorageSchema(DEFAULT_SQL_KINDS);
246
119
  function eachStorageTable(storage) {
247
- return Object.entries(storage.namespaces).flatMap(([namespaceId, ns]) => Object.entries(ns.entries.table).map(([tableName, table]) => ({
120
+ return Object.entries(storage.namespaces).flatMap(([namespaceId, ns]) => Object.entries(ns.entries["table"] ?? {}).map(([tableName, table]) => ({
248
121
  namespaceId,
249
122
  tableName,
250
123
  table
251
124
  })));
252
125
  }
253
- function isPlainRecord(value) {
254
- return typeof value === "object" && value !== null && !Array.isArray(value);
255
- }
256
126
  function findDuplicateValue(values) {
257
127
  const seen = /* @__PURE__ */ new Set();
258
128
  for (const value of values) {
@@ -283,7 +153,7 @@ const ModelFieldSchema = type({
283
153
  type: type("unknown").narrow((value, ctx) => isContractFieldType(value) ? true : ctx.mustBe("scalar, valueObject, or union field type")),
284
154
  "many?": "true",
285
155
  "dict?": "true",
286
- "valueSet?": ValueSetRefSchema
156
+ "valueSet?": DomainEnumRefSchema
287
157
  });
288
158
  const ModelStorageSchema = type({
289
159
  table: "string",
@@ -342,8 +212,8 @@ const ContractMetaSchema = type({ "[string]": "unknown" });
342
212
  * pack contributions through {@link createSqlStorageSchema}; the rest
343
213
  * of the contract envelope is family-shared.
344
214
  */
345
- function createSqlContractSchema(fragments) {
346
- const storage = createSqlStorageSchema(fragments);
215
+ function createSqlContractSchema(kinds) {
216
+ const storage = createSqlStorageSchema(kinds);
347
217
  return type({
348
218
  "+": "reject",
349
219
  target: "string",
@@ -364,7 +234,7 @@ function createSqlContractSchema(fragments) {
364
234
  "execution?": ExecutionSchema
365
235
  });
366
236
  }
367
- const SqlContractSchema = createSqlContractSchema();
237
+ const SqlContractSchema = createSqlContractSchema(DEFAULT_SQL_KINDS);
368
238
  /**
369
239
  * Validates the structural shape of SqlStorage using Arktype.
370
240
  *
@@ -525,7 +395,7 @@ function validateModelStorageReferences(contract) {
525
395
  const storageNamespaceId = model.storage.namespaceId;
526
396
  if (storageNamespaceId !== namespaceId) throw new ContractValidationError(`Model "${qualifiedName}" storage.namespaceId "${storageNamespaceId}" does not match domain namespace "${namespaceId}"`, "storage");
527
397
  const storageTable = model.storage.table;
528
- const rawTable = contract.storage.namespaces[storageNamespaceId]?.entries.table[storageTable];
398
+ const rawTable = contract.storage.namespaces[storageNamespaceId]?.entries.table?.[storageTable];
529
399
  if (rawTable === void 0) throw new ContractValidationError(`Model "${qualifiedName}" references non-existent table "${storageNamespaceId}.${storageTable}"`, "storage");
530
400
  const table = rawTable;
531
401
  const columnNames = new Set(Object.keys(table.columns));
@@ -563,7 +433,7 @@ function validateSqlStorageConsistency(contract) {
563
433
  if (fk.source.namespaceId !== namespaceId || fk.source.tableName !== tableName) throw new ContractValidationError(`Namespace "${namespaceId}" table "${tableName}" contains foreignKey with mismatched source coordinates (${fk.source.namespaceId}.${fk.source.tableName})`, "storage");
564
434
  for (const colName of fk.source.columns) if (!columnNames.has(colName)) throw new ContractValidationError(`Namespace "${namespaceId}" table "${tableName}" foreignKey references non-existent column "${colName}"`, "storage");
565
435
  if (fk.target.spaceId === void 0) {
566
- const referencedRaw = contract.storage.namespaces[fk.target.namespaceId]?.entries.table[fk.target.tableName];
436
+ const referencedRaw = contract.storage.namespaces[fk.target.namespaceId]?.entries.table?.[fk.target.tableName];
567
437
  if (referencedRaw === void 0) throw new ContractValidationError(`Namespace "${namespaceId}" table "${tableName}" foreignKey references non-existent table "${fk.target.namespaceId}.${fk.target.tableName}"`, "storage");
568
438
  const referencedColumnNames = new Set(Object.keys(referencedRaw.columns));
569
439
  for (const colName of fk.target.columns) if (!referencedColumnNames.has(colName)) throw new ContractValidationError(`Namespace "${namespaceId}" table "${tableName}" foreignKey references non-existent column "${colName}" in table "${fk.target.tableName}"`, "storage");
@@ -593,9 +463,111 @@ function validateSqlContractFully(value, options) {
593
463
  const semanticErrors = validateStorageSemantics(validated.storage);
594
464
  if (semanticErrors.length > 0) throw new ContractValidationError(`Contract semantic validation failed: ${semanticErrors.join("; ")}`, "storage");
595
465
  validateModelStorageReferences(validated);
466
+ validateRelationThroughConsistency(validated);
596
467
  return validated;
597
468
  }
469
+ /** Storage column lookup for through-consistency validation. */
470
+ function lookupStorageColumn(contract, namespaceId, tableName, columnName) {
471
+ const rawTable = contract.storage.namespaces[namespaceId]?.entries.table?.[tableName];
472
+ if (rawTable === void 0) return;
473
+ return blindCast(rawTable).columns[columnName];
474
+ }
475
+ /**
476
+ * Two storage columns share a type when their `nativeType` and `typeParams`
477
+ * match. The contract is canonicalized, so `typeParams` key order is stable and
478
+ * a JSON comparison is exact. `codecId` and `nullable` are intentionally not
479
+ * compared: they do not change the database-level type that governs a join.
480
+ */
481
+ function sameStorageType(a, b) {
482
+ return a.nativeType === b.nativeType && JSON.stringify(a.typeParams ?? null) === JSON.stringify(b.typeParams ?? null);
483
+ }
484
+ function describeColumnType(column) {
485
+ return column.typeParams === void 0 ? column.nativeType : `${column.nativeType} ${JSON.stringify(column.typeParams)}`;
486
+ }
487
+ /**
488
+ * Validates one side of an N:M join: the junction columns and the model
489
+ * columns they pair against positionally must be equal in number, exist in
490
+ * their tables, and share the same storage type (`nativeType` + `typeParams`).
491
+ * The junction's storage foreign keys already guarantee this for user-declared
492
+ * FK constraints, but `through` is a logical descriptor never tied to them by
493
+ * the rest of validation — and the TS builder accepts explicit join columns
494
+ * without requiring a junction FK at all — so this checks the columns directly
495
+ * against storage, one path regardless of how the junction was authored.
496
+ *
497
+ * Joined columns must be the *same* storage type, not merely compatible:
498
+ * relying on implicit conversion (e.g. `text`↔`character`) is unsafe on writes
499
+ * — `character(n)` space-padding makes such coercions non-associative — and no
500
+ * ADR sanctions heterogeneous junction columns. Equality is the conservative
501
+ * default; it can be relaxed deliberately if a real use case ever appears.
502
+ */
503
+ function validateThroughJoinSide(input) {
504
+ const fail = (detail) => new ContractValidationError(`Many-to-many relation "${input.qualifiedName}" ${detail}`, "storage");
505
+ if (input.junctionColumns.length !== input.modelColumns.length) throw fail(`pairs ${input.junctionColumnsLabel} (${input.junctionColumns.length}) with ${input.modelColumnsLabel} (${input.modelColumns.length}) of differing length; they join positionally and must match.`);
506
+ for (const [index, junctionColumnName] of input.junctionColumns.entries()) {
507
+ const modelColumnRef = input.modelColumns[index];
508
+ if (modelColumnRef === void 0) continue;
509
+ const junctionColumn = lookupStorageColumn(input.contract, input.junctionNamespaceId, input.junctionTable, junctionColumnName);
510
+ if (junctionColumn === void 0) throw fail(`${input.junctionColumnsLabel} references column "${junctionColumnName}" absent from junction table "${input.junctionNamespaceId}.${input.junctionTable}".`);
511
+ const model = input.model;
512
+ if (model === void 0) continue;
513
+ let modelColumnName = modelColumnRef;
514
+ if (model.fieldToColumn !== void 0) {
515
+ const mapped = model.fieldToColumn[modelColumnRef];
516
+ if (mapped === void 0) throw fail(`${input.modelColumnsLabel} references field "${modelColumnRef}" absent from model on table "${model.namespaceId}.${model.table}".`);
517
+ modelColumnName = mapped.column;
518
+ }
519
+ const modelColumn = lookupStorageColumn(input.contract, model.namespaceId, model.table, modelColumnName);
520
+ if (modelColumn === void 0) throw fail(`${input.modelColumnsLabel} references column "${modelColumnName}" absent from table "${model.namespaceId}.${model.table}".`);
521
+ if (!sameStorageType(junctionColumn, modelColumn)) throw fail(`joins "${input.junctionTable}.${junctionColumnName}" (${describeColumnType(junctionColumn)}) with "${model.table}.${modelColumnName}" (${describeColumnType(modelColumn)}) of differing storage type; junction columns must match the type of the column they reference.`);
522
+ }
523
+ }
524
+ /**
525
+ * Validates that every N:M relation's `through` descriptor is consistent with
526
+ * the storage columns it joins: both join sides match in column count,
527
+ * reference columns that exist in their tables, and pair columns of the same
528
+ * storage type. Without this, a `through` that disagrees with storage surfaces
529
+ * as a silently wrong JOIN at query time rather than a validation error here.
530
+ */
531
+ function validateRelationThroughConsistency(contract) {
532
+ for (const [namespaceId, namespace] of Object.entries(contract.domain.namespaces)) for (const [modelName, model] of Object.entries(namespace.models)) for (const [relationName, relation] of Object.entries(model.relations)) {
533
+ if (relation.cardinality !== "N:M") continue;
534
+ const qualifiedName = `${namespaceId}.${modelName}.${relationName}`;
535
+ const { on, through } = relation;
536
+ const modelStorage = blindCast(model.storage);
537
+ validateThroughJoinSide({
538
+ contract,
539
+ qualifiedName,
540
+ modelColumns: on.localFields,
541
+ modelColumnsLabel: "on.localFields",
542
+ model: {
543
+ namespaceId,
544
+ table: modelStorage.table,
545
+ fieldToColumn: modelStorage.fields
546
+ },
547
+ junctionColumns: through.parentColumns,
548
+ junctionColumnsLabel: "through.parentColumns",
549
+ junctionNamespaceId: through.namespaceId,
550
+ junctionTable: through.table
551
+ });
552
+ const targetModel = relation.to.space === void 0 ? contract.domain.namespaces[relation.to.namespace]?.models[relation.to.model] : void 0;
553
+ const targetModelSide = targetModel === void 0 ? void 0 : {
554
+ namespaceId: relation.to.namespace,
555
+ table: blindCast(targetModel.storage).table
556
+ };
557
+ validateThroughJoinSide({
558
+ contract,
559
+ qualifiedName,
560
+ modelColumns: through.targetColumns,
561
+ modelColumnsLabel: "through.targetColumns",
562
+ ...ifDefined("model", targetModelSide),
563
+ junctionColumns: through.childColumns,
564
+ junctionColumnsLabel: "through.childColumns",
565
+ junctionNamespaceId: through.namespaceId,
566
+ junctionTable: through.table
567
+ });
568
+ }
569
+ }
598
570
  //#endregion
599
- export { CheckConstraintSchema, ColumnDefaultFunctionSchema, ColumnDefaultLiteralSchema, ColumnDefaultSchema, ContractEnumSchema, ForeignKeyReferenceSchema, ForeignKeySchema, ForeignKeySourceSchema, IndexSchema, PostgresEnumTypeSchema, ReferentialActionSchema, StorageValueSetSchema, createNamespaceEntrySchema, createSqlContractSchema, createSqlStorageSchema, validateModel, validateModelStorageReferences, validateSqlContractFully, validateSqlStorageConsistency, validateStorage, validateStorageSemantics };
571
+ export { CheckConstraintSchema, ColumnDefaultFunctionSchema, ColumnDefaultLiteralSchema, ColumnDefaultSchema, ContractEnumSchema, ForeignKeyReferenceSchema, ForeignKeySchema, ForeignKeySourceSchema, IndexSchema, ReferentialActionSchema, StorageTableSchema, StorageValueSetSchema, createNamespaceEntrySchema, createSqlContractSchema, createSqlStorageSchema, validateModel, validateModelStorageReferences, validateSqlContractFully, validateSqlStorageConsistency, validateStorage, validateStorageSemantics };
600
572
 
601
573
  //# sourceMappingURL=validators.mjs.map