@prisma-next/sql-contract 0.8.0 → 0.9.0-dev.1

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 (52) hide show
  1. package/README.md +13 -11
  2. package/dist/factories.d.mts +2 -2
  3. package/dist/factories.d.mts.map +1 -1
  4. package/dist/factories.mjs +16 -14
  5. package/dist/factories.mjs.map +1 -1
  6. package/dist/index-type-validation.d.mts +2 -2
  7. package/dist/index-type-validation.mjs +1 -1
  8. package/dist/index-type-validation.mjs.map +1 -1
  9. package/dist/{index-types-DqVqGHwg.d.mts → index-types-B1cf5N0F.d.mts} +1 -1
  10. package/dist/{index-types-DqVqGHwg.d.mts.map → index-types-B1cf5N0F.d.mts.map} +1 -1
  11. package/dist/index-types.d.mts +1 -1
  12. package/dist/types-B0lbr9cb.d.mts +498 -0
  13. package/dist/types-B0lbr9cb.d.mts.map +1 -0
  14. package/dist/types-iqFGDcJp.mjs +389 -0
  15. package/dist/types-iqFGDcJp.mjs.map +1 -0
  16. package/dist/types.d.mts +2 -2
  17. package/dist/types.mjs +2 -2
  18. package/dist/validators.d.mts +27 -15
  19. package/dist/validators.d.mts.map +1 -1
  20. package/dist/validators.mjs +409 -2
  21. package/dist/validators.mjs.map +1 -0
  22. package/package.json +6 -7
  23. package/src/exports/types.ts +30 -9
  24. package/src/factories.ts +19 -32
  25. package/src/index-type-validation.ts +1 -1
  26. package/src/index.ts +0 -1
  27. package/src/ir/foreign-key-references.ts +26 -0
  28. package/src/ir/foreign-key.ts +50 -0
  29. package/src/ir/postgres-enum-storage-entry.ts +55 -0
  30. package/src/ir/primary-key.ts +22 -0
  31. package/src/ir/sql-index.ts +33 -0
  32. package/src/ir/sql-node.ts +52 -0
  33. package/src/ir/sql-storage.ts +154 -0
  34. package/src/ir/sql-unspecified-namespace.ts +51 -0
  35. package/src/ir/storage-column.ts +55 -0
  36. package/src/ir/storage-table.ts +63 -0
  37. package/src/ir/storage-type-instance.ts +60 -0
  38. package/src/ir/unique-constraint.ts +22 -0
  39. package/src/types.ts +38 -99
  40. package/src/validators.ts +268 -28
  41. package/dist/types-hgzy8ME1.mjs +0 -13
  42. package/dist/types-hgzy8ME1.mjs.map +0 -1
  43. package/dist/types-njsiV-Ck.d.mts +0 -186
  44. package/dist/types-njsiV-Ck.d.mts.map +0 -1
  45. package/dist/validate.d.mts +0 -9
  46. package/dist/validate.d.mts.map +0 -1
  47. package/dist/validate.mjs +0 -106
  48. package/dist/validate.mjs.map +0 -1
  49. package/dist/validators-Dm5X-Hvg.mjs +0 -294
  50. package/dist/validators-Dm5X-Hvg.mjs.map +0 -1
  51. package/src/exports/validate.ts +0 -1
  52. package/src/validate.ts +0 -227
@@ -0,0 +1,63 @@
1
+ import { freezeNode } from '@prisma-next/framework-components/ir';
2
+ import { ForeignKey, type ForeignKeyInput } from './foreign-key';
3
+ import { PrimaryKey, type PrimaryKeyInput } from './primary-key';
4
+ import { Index, type IndexInput } from './sql-index';
5
+ import { SqlNode } from './sql-node';
6
+ import { StorageColumn, type StorageColumnInput } from './storage-column';
7
+ import { UniqueConstraint, type UniqueConstraintInput } from './unique-constraint';
8
+
9
+ export interface StorageTableInput {
10
+ readonly columns: Record<string, StorageColumn | StorageColumnInput>;
11
+ readonly primaryKey?: PrimaryKey | PrimaryKeyInput;
12
+ readonly uniques: ReadonlyArray<UniqueConstraint | UniqueConstraintInput>;
13
+ readonly indexes: ReadonlyArray<Index | IndexInput>;
14
+ readonly foreignKeys: ReadonlyArray<ForeignKey | ForeignKeyInput>;
15
+ }
16
+
17
+ /**
18
+ * SQL Contract IR node for a single table entry in `SqlStorage.tables`.
19
+ *
20
+ * The constructor normalises nested IR-class fields (columns, primary
21
+ * key, uniques, indexes, foreign keys) into the appropriate class
22
+ * instances so downstream walks see a uniform AST regardless of whether
23
+ * the input was a JSON literal or an already-constructed class.
24
+ *
25
+ * The table's `name` is not on the class — tables are keyed by name in
26
+ * the parent `SqlStorage.tables: Record<string, StorageTable>` map.
27
+ * A future namespace-aware milestone will add a `namespaceId` field
28
+ * when namespace-keyed storage lands; today's single-namespace shape
29
+ * needs neither field.
30
+ */
31
+ export class StorageTable extends SqlNode {
32
+ readonly columns: Readonly<Record<string, StorageColumn>>;
33
+ readonly uniques: ReadonlyArray<UniqueConstraint>;
34
+ readonly indexes: ReadonlyArray<Index>;
35
+ readonly foreignKeys: ReadonlyArray<ForeignKey>;
36
+ declare readonly primaryKey?: PrimaryKey;
37
+
38
+ constructor(input: StorageTableInput) {
39
+ super();
40
+ this.columns = Object.freeze(
41
+ Object.fromEntries(
42
+ Object.entries(input.columns).map(([name, col]) => [
43
+ name,
44
+ col instanceof StorageColumn ? col : new StorageColumn(col),
45
+ ]),
46
+ ),
47
+ );
48
+ if (input.primaryKey !== undefined) {
49
+ this.primaryKey =
50
+ input.primaryKey instanceof PrimaryKey
51
+ ? input.primaryKey
52
+ : new PrimaryKey(input.primaryKey);
53
+ }
54
+ this.uniques = Object.freeze(
55
+ input.uniques.map((u) => (u instanceof UniqueConstraint ? u : new UniqueConstraint(u))),
56
+ );
57
+ this.indexes = Object.freeze(input.indexes.map((i) => (i instanceof Index ? i : new Index(i))));
58
+ this.foreignKeys = Object.freeze(
59
+ input.foreignKeys.map((fk) => (fk instanceof ForeignKey ? fk : new ForeignKey(fk))),
60
+ );
61
+ freezeNode(this);
62
+ }
63
+ }
@@ -0,0 +1,60 @@
1
+ import type { StorageType } from '@prisma-next/framework-components/ir';
2
+
3
+ /**
4
+ * Sentinel kind for the legacy codec-triple shape persisted under
5
+ * `SqlStorage.types`. Plain JSON-clean object literals carry this
6
+ * discriminator so the polymorphic slot dispatch can route them down
7
+ * the codec path while target-specific IR class instances (e.g. the
8
+ * Postgres enum class) keep their own narrower `kind` literal.
9
+ */
10
+ export const CODEC_INSTANCE_KIND = 'codec-instance' as const;
11
+
12
+ /**
13
+ * Structural sub-interface of {@link StorageType} for codec-typed entries
14
+ * in `SqlStorage.types`. These are plain object literals — there is no
15
+ * runtime IR class, the JSON envelope round-trips through the slot
16
+ * unchanged. The `kind: 'codec-instance'` discriminator is the dispatch
17
+ * key that distinguishes codec-typed entries from class-instance entries
18
+ * (e.g. `PostgresEnumType`) sharing the polymorphic slot.
19
+ */
20
+ export interface StorageTypeInstance extends StorageType {
21
+ readonly kind: typeof CODEC_INSTANCE_KIND;
22
+ readonly codecId: string;
23
+ readonly nativeType: string;
24
+ readonly typeParams: Record<string, unknown>;
25
+ }
26
+
27
+ /**
28
+ * Construction-time input for a codec-triple entry. Symmetric with the
29
+ * structural runtime shape minus the `kind` discriminator — callers may
30
+ * omit `kind`; the helper {@link toStorageTypeInstance} stamps it on.
31
+ */
32
+ export interface StorageTypeInstanceInput {
33
+ readonly codecId: string;
34
+ readonly nativeType: string;
35
+ readonly typeParams: Record<string, unknown>;
36
+ }
37
+
38
+ /**
39
+ * Stamp the codec-instance `kind` discriminator on a caller-supplied
40
+ * codec triple. Idempotent: input that already carries the discriminator
41
+ * passes through unchanged.
42
+ */
43
+ export function toStorageTypeInstance(input: StorageTypeInstanceInput): StorageTypeInstance {
44
+ return {
45
+ kind: CODEC_INSTANCE_KIND,
46
+ codecId: input.codecId,
47
+ nativeType: input.nativeType,
48
+ typeParams: input.typeParams,
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Type-guard for codec-typed entries on the polymorphic
54
+ * `SqlStorage.types` slot. Distinguishes `StorageTypeInstance` from
55
+ * class-instance kinds (e.g. `PostgresEnumType`).
56
+ */
57
+ export function isStorageTypeInstance(value: unknown): value is StorageTypeInstance {
58
+ if (typeof value !== 'object' || value === null) return false;
59
+ return (value as { kind?: unknown }).kind === CODEC_INSTANCE_KIND;
60
+ }
@@ -0,0 +1,22 @@
1
+ import { freezeNode } from '@prisma-next/framework-components/ir';
2
+ import { SqlNode } from './sql-node';
3
+
4
+ export interface UniqueConstraintInput {
5
+ readonly columns: readonly string[];
6
+ readonly name?: string;
7
+ }
8
+
9
+ /**
10
+ * SQL Contract IR node for a table-level unique constraint.
11
+ */
12
+ export class UniqueConstraint extends SqlNode {
13
+ readonly columns: readonly string[];
14
+ declare readonly name?: string;
15
+
16
+ constructor(input: UniqueConstraintInput) {
17
+ super();
18
+ this.columns = input.columns;
19
+ if (input.name !== undefined) this.name = input.name;
20
+ freezeNode(this);
21
+ }
22
+ }
package/src/types.ts CHANGED
@@ -1,58 +1,42 @@
1
- import type { ColumnDefault, StorageBase } from '@prisma-next/contract/types';
2
1
  import type { CodecTrait } from '@prisma-next/framework-components/codec';
3
-
4
- /**
5
- * A column definition in storage.
6
- *
7
- * `typeParams` is optional because most columns use non-parameterized types.
8
- * Columns with parameterized types can either inline `typeParams` or reference
9
- * a named {@link StorageTypeInstance} via `typeRef`.
10
- */
11
- export type StorageColumn = {
12
- readonly nativeType: string;
13
- readonly codecId: string;
14
- readonly nullable: boolean;
15
- /**
16
- * Opaque, codec-owned JS/type parameters.
17
- * The codec that owns `codecId` defines the shape and semantics.
18
- * Mutually exclusive with `typeRef`.
19
- */
20
- readonly typeParams?: Record<string, unknown>;
21
- /**
22
- * Reference to a named type instance in `storage.types`.
23
- * Mutually exclusive with `typeParams`.
24
- */
25
- readonly typeRef?: string;
26
- /**
27
- * Default value for the column.
28
- * Can be a literal value or database function.
29
- */
30
- readonly default?: ColumnDefault;
31
- };
32
-
33
- export type PrimaryKey = {
34
- readonly columns: readonly string[];
35
- readonly name?: string;
36
- };
37
-
38
- export type UniqueConstraint = {
39
- readonly columns: readonly string[];
40
- readonly name?: string;
41
- };
42
-
43
- export type Index = {
44
- readonly columns: readonly string[];
45
- readonly name?: string;
46
- readonly type?: string;
47
- readonly options?: Record<string, unknown>;
48
- };
49
-
50
- export type ForeignKeyReferences = {
51
- readonly table: string;
52
- readonly columns: readonly string[];
53
- };
54
-
55
- export type ReferentialAction = 'noAction' | 'restrict' | 'cascade' | 'setNull' | 'setDefault';
2
+ import type { ReferentialAction } from './ir/foreign-key';
3
+
4
+ export {
5
+ ForeignKey,
6
+ type ForeignKeyInput,
7
+ type ReferentialAction,
8
+ } from './ir/foreign-key';
9
+ export {
10
+ ForeignKeyReferences,
11
+ type ForeignKeyReferencesInput,
12
+ } from './ir/foreign-key-references';
13
+ export {
14
+ isPostgresEnumStorageEntry,
15
+ POSTGRES_ENUM_KIND,
16
+ type PostgresEnumStorageEntry,
17
+ } from './ir/postgres-enum-storage-entry';
18
+ export { PrimaryKey, type PrimaryKeyInput } from './ir/primary-key';
19
+ export { Index, type IndexInput } from './ir/sql-index';
20
+ export { SqlNode } from './ir/sql-node';
21
+ export {
22
+ SqlStorage,
23
+ type SqlStorageInput,
24
+ type SqlStorageTypeEntry,
25
+ } from './ir/sql-storage';
26
+ export { SqlUnspecifiedNamespace } from './ir/sql-unspecified-namespace';
27
+ export { StorageColumn, type StorageColumnInput } from './ir/storage-column';
28
+ export { StorageTable, type StorageTableInput } from './ir/storage-table';
29
+ export {
30
+ CODEC_INSTANCE_KIND,
31
+ isStorageTypeInstance,
32
+ type StorageTypeInstance,
33
+ type StorageTypeInstanceInput,
34
+ toStorageTypeInstance,
35
+ } from './ir/storage-type-instance';
36
+ export {
37
+ UniqueConstraint,
38
+ type UniqueConstraintInput,
39
+ } from './ir/unique-constraint';
56
40
 
57
41
  export type ForeignKeyOptions = {
58
42
  readonly name?: string;
@@ -60,51 +44,6 @@ export type ForeignKeyOptions = {
60
44
  readonly onUpdate?: ReferentialAction;
61
45
  };
62
46
 
63
- export type ForeignKey = {
64
- readonly columns: readonly string[];
65
- readonly references: ForeignKeyReferences;
66
- readonly name?: string;
67
- readonly onDelete?: ReferentialAction;
68
- readonly onUpdate?: ReferentialAction;
69
- /** Whether to emit FK constraint DDL (ALTER TABLE … ADD CONSTRAINT … FOREIGN KEY). */
70
- readonly constraint: boolean;
71
- /** Whether to emit a backing index for the FK columns. */
72
- readonly index: boolean;
73
- };
74
-
75
- export type StorageTable = {
76
- readonly columns: Record<string, StorageColumn>;
77
- readonly primaryKey?: PrimaryKey;
78
- readonly uniques: ReadonlyArray<UniqueConstraint>;
79
- readonly indexes: ReadonlyArray<Index>;
80
- readonly foreignKeys: ReadonlyArray<ForeignKey>;
81
- };
82
-
83
- /**
84
- * A named, parameterized type instance.
85
- * These are registered in `storage.types` for reuse across columns
86
- * and to enable ergonomic schema surfaces like `schema.types.MyType`.
87
- *
88
- * Unlike {@link StorageColumn}, `typeParams` is required here because
89
- * `StorageTypeInstance` exists specifically to define reusable parameterized types.
90
- * A type instance without parameters would be redundant—columns can reference
91
- * the codec directly via `codecId`.
92
- */
93
- export type StorageTypeInstance = {
94
- readonly codecId: string;
95
- readonly nativeType: string;
96
- readonly typeParams: Record<string, unknown>;
97
- };
98
-
99
- export type SqlStorage<THash extends string = string> = StorageBase<THash> & {
100
- readonly tables: Record<string, StorageTable>;
101
- /**
102
- * Named type instances for parameterized/custom types.
103
- * Columns can reference these via `typeRef`.
104
- */
105
- readonly types?: Record<string, StorageTypeInstance>;
106
- };
107
-
108
47
  export type SqlModelFieldStorage = {
109
48
  readonly column: string;
110
49
  readonly codecId?: string;
package/src/validators.ts CHANGED
@@ -1,14 +1,17 @@
1
- import type { Contract } from '@prisma-next/contract/types';
2
- import { ContractValidationError } from '@prisma-next/contract/validate-contract';
1
+ import { ContractValidationError } from '@prisma-next/contract/contract-validation-error';
2
+ import type { Contract, ContractField, ContractModel } from '@prisma-next/contract/types';
3
+ import { validateContractDomain } from '@prisma-next/contract/validate-domain';
3
4
  import { type } from 'arktype';
4
- import type {
5
- ForeignKey,
6
- ForeignKeyReferences,
7
- PrimaryKey,
8
- ReferentialAction,
5
+ import {
6
+ type ForeignKeyInput,
7
+ type ForeignKeyReferencesInput,
8
+ type PrimaryKeyInput,
9
+ type ReferentialAction,
10
+ type SqlModelStorage,
9
11
  SqlStorage,
10
- StorageTypeInstance,
11
- UniqueConstraint,
12
+ type SqlStorageInput,
13
+ type StorageTypeInstanceInput,
14
+ type UniqueConstraintInput,
12
15
  } from './types';
13
16
 
14
17
  type ColumnDefaultLiteral = {
@@ -77,18 +80,66 @@ const StorageColumnSchema = type({
77
80
  return true;
78
81
  });
79
82
 
80
- const StorageTypeInstanceSchema = type.declare<StorageTypeInstance>().type({
81
- codecId: 'string',
83
+ /**
84
+ * Codec-triple entry persisted under `storage.types[name]`. Carries an
85
+ * enumerable literal `kind: 'codec-instance'` discriminator so the
86
+ * polymorphic slot dispatch can distinguish codec triples from
87
+ * class-instance kinds (e.g. `'postgres-enum'`) sharing the slot.
88
+ */
89
+ const StorageTypeInstanceSchema = type
90
+ .declare<StorageTypeInstanceInput & { kind: 'codec-instance' }>()
91
+ .type({
92
+ kind: "'codec-instance'",
93
+ codecId: 'string',
94
+ nativeType: 'string',
95
+ typeParams: 'Record<string, unknown>',
96
+ });
97
+
98
+ /**
99
+ * Polymorphic enum-type entry under `storage.types[name]`. Carries an
100
+ * enumerable literal `kind: 'postgres-enum'` discriminator so the
101
+ * per-target hydration walker can dispatch cleanly back to a typed
102
+ * IR-class instance during `deserializeContract`. The discriminator
103
+ * reflects target-level behaviour (Postgres-native enums versus
104
+ * family-layer codec triples) — not the family abstract altitude alone.
105
+ *
106
+ * The schema literal lives at the family layer today because
107
+ * registry-driven validation for arbitrary slot shapes is not wired
108
+ * yet; once a second polymorphic kind ships through the slot, this
109
+ * structural enumeration can move to the registry-dispatch site and
110
+ * per-target schemas can live in their target packages.
111
+ */
112
+ const PostgresEnumTypeSchema = type({
113
+ kind: "'postgres-enum'",
114
+ name: 'string',
82
115
  nativeType: 'string',
83
- typeParams: 'Record<string, unknown>',
116
+ values: type.string.array().readonly(),
84
117
  });
85
118
 
86
- const PrimaryKeySchema = type.declare<PrimaryKey>().type({
119
+ /**
120
+ * Family-layer arktype validation enumerates the polymorphic shapes the
121
+ * SQL family ships today (codec-instance + Postgres-enum). Pack-contributed
122
+ * entity types ship a parallel arktype schema entry here when they
123
+ * introduce a new persisted shape; the registry-driven hydration seam at
124
+ * `SqlContractSerializerBase.hydrateStorageTypeEntry` is open, but the
125
+ * family-layer structural validator is closed by design — extension
126
+ * packs cannot inject arbitrary persisted shapes through the slot
127
+ * without their structural shape being known at the family layer.
128
+ *
129
+ * A future refinement is to lift `StorageTypeEntrySchema` toward an
130
+ * `unknown` fallback and move structural diagnostics to the
131
+ * registry-dispatch site at hydration time, earned once a non-enum
132
+ * storage shape needs to flow through the slot without growing another
133
+ * closed union arm here first.
134
+ */
135
+ const StorageTypeEntrySchema = PostgresEnumTypeSchema.or(StorageTypeInstanceSchema);
136
+
137
+ const PrimaryKeySchema = type.declare<PrimaryKeyInput>().type({
87
138
  columns: type.string.array().readonly(),
88
139
  'name?': 'string',
89
140
  });
90
141
 
91
- const UniqueConstraintSchema = type.declare<UniqueConstraint>().type({
142
+ const UniqueConstraintSchema = type.declare<UniqueConstraintInput>().type({
92
143
  columns: type.string.array().readonly(),
93
144
  'name?': 'string',
94
145
  });
@@ -100,7 +151,7 @@ export const IndexSchema = type({
100
151
  'options?': 'Record<string, unknown>',
101
152
  });
102
153
 
103
- export const ForeignKeyReferencesSchema = type.declare<ForeignKeyReferences>().type({
154
+ export const ForeignKeyReferencesSchema = type.declare<ForeignKeyReferencesInput>().type({
104
155
  table: 'string',
105
156
  columns: type.string.array().readonly(),
106
157
  });
@@ -109,7 +160,7 @@ export const ReferentialActionSchema = type
109
160
  .declare<ReferentialAction>()
110
161
  .type("'noAction' | 'restrict' | 'cascade' | 'setNull' | 'setDefault'");
111
162
 
112
- export const ForeignKeySchema = type.declare<ForeignKey>().type({
163
+ export const ForeignKeySchema = type.declare<ForeignKeyInput>().type({
113
164
  columns: type.string.array().readonly(),
114
165
  references: ForeignKeyReferencesSchema,
115
166
  'name?': 'string',
@@ -128,11 +179,25 @@ const StorageTableSchema = type({
128
179
  foreignKeys: ForeignKeySchema.array().readonly(),
129
180
  });
130
181
 
182
+ /**
183
+ * Namespace entry under `storage.namespaces[id]`. SQL contracts honour
184
+ * the framework `Storage.namespaces` invariant from PR1; today every
185
+ * contract binds to the singleton placeholder
186
+ * (`SqlUnspecifiedNamespace.instance`) and the persisted shape carries
187
+ * just the namespace id. Per-target namespace concretions
188
+ * (`PostgresSchema`, `SqliteUnspecifiedDatabase`) can additively grow
189
+ * the persisted shape when they earn their slots.
190
+ */
191
+ const NamespaceEntrySchema = type({
192
+ id: 'string',
193
+ });
194
+
131
195
  const StorageSchema = type({
132
196
  '+': 'reject',
133
197
  storageHash: 'string',
134
198
  tables: type({ '[string]': StorageTableSchema }),
135
- 'types?': type({ '[string]': StorageTypeInstanceSchema }),
199
+ 'types?': type({ '[string]': StorageTypeEntrySchema }),
200
+ 'namespaces?': type({ '[string]': NamespaceEntrySchema }),
136
201
  });
137
202
 
138
203
  function isPlainRecord(value: unknown): value is Record<string, unknown> {
@@ -228,7 +293,6 @@ const SqlContractSchema = type({
228
293
  // includes bigint | Date (runtime-only types after decoding) which cannot be expressed
229
294
  // in Arktype's JSON validation DSL. The `as SqlStorage` cast in validateStorage() bridges
230
295
  // the gap between the JSON-safe Arktype output and the runtime TypeScript type.
231
- // See decodeContractDefaults() in validate.ts for the decoding step.
232
296
 
233
297
  /**
234
298
  * Validates the structural shape of SqlStorage using Arktype.
@@ -243,7 +307,11 @@ export function validateStorage(value: unknown): SqlStorage {
243
307
  const messages = result.map((p: { message: string }) => p.message).join('; ');
244
308
  throw new Error(`Storage validation failed: ${messages}`);
245
309
  }
246
- return result as SqlStorage;
310
+ // The arktype-validated shape matches `SqlStorageInput`
311
+ // structurally. Funnel through the constructor so nested IR fields
312
+ // (`tables`, `types`) are normalised into class instances and the
313
+ // branded `storageHash` is preserved on the returned `SqlStorage`.
314
+ return new SqlStorage(result as SqlStorageInput);
247
315
  }
248
316
 
249
317
  export function validateModel(value: unknown): unknown {
@@ -256,16 +324,12 @@ export function validateModel(value: unknown): unknown {
256
324
  }
257
325
 
258
326
  /**
259
- * Validates the structural shape of an SQL contract using Arktype.
260
- *
261
- * Ensures all required fields are present and have the correct types,
262
- * including SQL-specific storage structure (tables, columns, constraints).
263
- *
264
- * @param value - The contract value to validate (typically from a JSON import)
265
- * @returns The validated contract if structure is valid
266
- * @throws ContractValidationError if the contract structure is invalid
327
+ * Structural arktype validation of an SQL contract envelope. Internal
328
+ * helper for {@link validateSqlContractFully} — exposed only inside
329
+ * this module, since the family seam-of-record is the
330
+ * `SqlContractSerializerBase.deserializeContract` SPI.
267
331
  */
268
- export function validateSqlContract<T extends Contract<SqlStorage>>(value: unknown): T {
332
+ function validateSqlContractStructure<T extends Contract<SqlStorage>>(value: unknown): T {
269
333
  if (typeof value !== 'object' || value === null) {
270
334
  throw new ContractValidationError(
271
335
  'Contract structural validation failed: value must be an object',
@@ -449,3 +513,179 @@ export function validateStorageSemantics(storage: SqlStorage): string[] {
449
513
 
450
514
  return errors;
451
515
  }
516
+
517
+ /**
518
+ * SQL storage logical-consistency checks: every model.storage.table
519
+ * resolves to a real table, every model.storage.fields[*].column
520
+ * resolves to a real column, and value-object fields land on JSON-native
521
+ * columns. Throws `ContractValidationError` on the first mismatch.
522
+ */
523
+ export function validateModelStorageReferences(contract: Contract<SqlStorage>): void {
524
+ const models = contract.models as Record<string, ContractModel<SqlModelStorage>>;
525
+ for (const [modelName, model] of Object.entries(models)) {
526
+ const storageTable = model.storage.table;
527
+
528
+ const table = contract.storage.tables[storageTable] as
529
+ | (typeof contract.storage.tables)[string]
530
+ | undefined;
531
+ if (!table) {
532
+ throw new ContractValidationError(
533
+ `Model "${modelName}" references non-existent table "${storageTable}"`,
534
+ 'storage',
535
+ );
536
+ }
537
+
538
+ const columnNames = new Set(Object.keys(table.columns));
539
+ for (const [fieldName, field] of Object.entries(model.storage.fields)) {
540
+ if (!columnNames.has(field.column)) {
541
+ throw new ContractValidationError(
542
+ `Model "${modelName}" field "${fieldName}" references non-existent column "${field.column}" in table "${storageTable}"`,
543
+ 'storage',
544
+ );
545
+ }
546
+ }
547
+
548
+ const JSON_NATIVE_TYPES = new Set(['json', 'jsonb']);
549
+ for (const [fieldName, domainField] of Object.entries(model.fields ?? {})) {
550
+ const f = domainField as ContractField;
551
+ if (f.type?.kind !== 'valueObject') continue;
552
+ const storageField = model.storage.fields[fieldName];
553
+ if (!storageField) continue;
554
+ const column = table.columns[storageField.column];
555
+ if (!column) continue;
556
+ if (!JSON_NATIVE_TYPES.has(column.nativeType)) {
557
+ throw new ContractValidationError(
558
+ `Model "${modelName}" field "${fieldName}" is a value object but storage column "${storageField.column}" has nativeType "${column.nativeType}" (expected json or jsonb)`,
559
+ 'storage',
560
+ );
561
+ }
562
+ }
563
+ }
564
+ }
565
+
566
+ /**
567
+ * Cross-table consistency checks for SQL storage: primary key, unique,
568
+ * index, and foreign key column references resolve to real columns;
569
+ * NOT NULL columns don't carry a literal `null` default; FK column
570
+ * counts match their referenced columns. Throws on the first mismatch.
571
+ */
572
+ export function validateSqlStorageConsistency(contract: Contract<SqlStorage>): void {
573
+ const tableNames = new Set(Object.keys(contract.storage.tables));
574
+
575
+ for (const [tableName, table] of Object.entries(contract.storage.tables)) {
576
+ const columnNames = new Set(Object.keys(table.columns));
577
+
578
+ if (table.primaryKey) {
579
+ for (const colName of table.primaryKey.columns) {
580
+ if (!columnNames.has(colName)) {
581
+ throw new ContractValidationError(
582
+ `Table "${tableName}" primaryKey references non-existent column "${colName}"`,
583
+ 'storage',
584
+ );
585
+ }
586
+ }
587
+ }
588
+
589
+ for (const unique of table.uniques) {
590
+ for (const colName of unique.columns) {
591
+ if (!columnNames.has(colName)) {
592
+ throw new ContractValidationError(
593
+ `Table "${tableName}" unique constraint references non-existent column "${colName}"`,
594
+ 'storage',
595
+ );
596
+ }
597
+ }
598
+ }
599
+
600
+ for (const index of table.indexes) {
601
+ for (const colName of index.columns) {
602
+ if (!columnNames.has(colName)) {
603
+ throw new ContractValidationError(
604
+ `Table "${tableName}" index references non-existent column "${colName}"`,
605
+ 'storage',
606
+ );
607
+ }
608
+ }
609
+ }
610
+
611
+ for (const [colName, column] of Object.entries(table.columns)) {
612
+ if (!column.nullable && column.default?.kind === 'literal' && column.default.value === null) {
613
+ throw new ContractValidationError(
614
+ `Table "${tableName}" column "${colName}" is NOT NULL but has a literal null default`,
615
+ 'storage',
616
+ );
617
+ }
618
+ }
619
+
620
+ for (const fk of table.foreignKeys) {
621
+ for (const colName of fk.columns) {
622
+ if (!columnNames.has(colName)) {
623
+ throw new ContractValidationError(
624
+ `Table "${tableName}" foreignKey references non-existent column "${colName}"`,
625
+ 'storage',
626
+ );
627
+ }
628
+ }
629
+
630
+ if (!tableNames.has(fk.references.table)) {
631
+ throw new ContractValidationError(
632
+ `Table "${tableName}" foreignKey references non-existent table "${fk.references.table}"`,
633
+ 'storage',
634
+ );
635
+ }
636
+
637
+ const referencedTable = contract.storage.tables[fk.references.table];
638
+ if (!referencedTable) continue;
639
+ const referencedColumnNames = new Set(Object.keys(referencedTable.columns));
640
+ for (const colName of fk.references.columns) {
641
+ if (!referencedColumnNames.has(colName)) {
642
+ throw new ContractValidationError(
643
+ `Table "${tableName}" foreignKey references non-existent column "${colName}" in table "${fk.references.table}"`,
644
+ 'storage',
645
+ );
646
+ }
647
+ }
648
+
649
+ if (fk.columns.length !== fk.references.columns.length) {
650
+ throw new ContractValidationError(
651
+ `Table "${tableName}" foreignKey column count (${fk.columns.length}) does not match referenced column count (${fk.references.columns.length})`,
652
+ 'storage',
653
+ );
654
+ }
655
+ }
656
+ }
657
+ }
658
+
659
+ /**
660
+ * Full SQL contract validation: structural (arktype) +
661
+ * framework-shared domain + SQL storage logical-consistency + SQL
662
+ * storage semantic + model ↔ storage reference checks. Throws
663
+ * `ContractValidationError` on the first failure. Returns the
664
+ * validated flat-data shape; IR class hydration happens in the SPI
665
+ * base on top of this helper.
666
+ */
667
+ export function validateSqlContractFully<T extends Contract<SqlStorage>>(value: unknown): T {
668
+ const stripped =
669
+ typeof value === 'object' && value !== null
670
+ ? (() => {
671
+ const { schemaVersion: _, _generated: _g, ...rest } = value as Record<string, unknown>;
672
+ return rest;
673
+ })()
674
+ : value;
675
+ const validated = validateSqlContractStructure<T>(stripped);
676
+ validateContractDomain({
677
+ roots: validated.roots,
678
+ models: validated.models,
679
+ ...(validated.valueObjects ? { valueObjects: validated.valueObjects } : {}),
680
+ });
681
+ validateSqlStorageConsistency(validated);
682
+ const semanticErrors = validateStorageSemantics(validated.storage);
683
+ if (semanticErrors.length > 0) {
684
+ throw new ContractValidationError(
685
+ `Contract semantic validation failed: ${semanticErrors.join('; ')}`,
686
+ 'storage',
687
+ );
688
+ }
689
+ validateModelStorageReferences(validated);
690
+ return validated;
691
+ }
@@ -1,13 +0,0 @@
1
- //#region src/types.ts
2
- const DEFAULT_FK_CONSTRAINT = true;
3
- const DEFAULT_FK_INDEX = true;
4
- function applyFkDefaults(fk, overrideDefaults) {
5
- return {
6
- constraint: fk.constraint ?? overrideDefaults?.constraint ?? true,
7
- index: fk.index ?? overrideDefaults?.index ?? true
8
- };
9
- }
10
- //#endregion
11
- export { DEFAULT_FK_INDEX as n, applyFkDefaults as r, DEFAULT_FK_CONSTRAINT as t };
12
-
13
- //# sourceMappingURL=types-hgzy8ME1.mjs.map