@prisma-next/sql-contract 0.7.0-dev.7 → 0.8.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 (49) hide show
  1. package/README.md +11 -9
  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-types-DqVqGHwg.d.mts → index-types-B1cf5N0F.d.mts} +1 -1
  8. package/dist/{index-types-DqVqGHwg.d.mts.map → index-types-B1cf5N0F.d.mts.map} +1 -1
  9. package/dist/index-types.d.mts +1 -1
  10. package/dist/types-B_diLlWt.d.mts +498 -0
  11. package/dist/types-B_diLlWt.d.mts.map +1 -0
  12. package/dist/types-BtB74x-Z.mjs +370 -0
  13. package/dist/types-BtB74x-Z.mjs.map +1 -0
  14. package/dist/types.d.mts +2 -2
  15. package/dist/types.mjs +2 -2
  16. package/dist/validators.d.mts +27 -4
  17. package/dist/validators.d.mts.map +1 -1
  18. package/dist/validators.mjs +413 -2
  19. package/dist/validators.mjs.map +1 -0
  20. package/package.json +6 -7
  21. package/src/exports/types.ts +30 -9
  22. package/src/factories.ts +19 -32
  23. package/src/index.ts +0 -1
  24. package/src/ir/foreign-key-references.ts +26 -0
  25. package/src/ir/foreign-key.ts +50 -0
  26. package/src/ir/postgres-enum-storage-entry.ts +55 -0
  27. package/src/ir/primary-key.ts +22 -0
  28. package/src/ir/sql-index.ts +33 -0
  29. package/src/ir/sql-node.ts +52 -0
  30. package/src/ir/sql-storage.ts +130 -0
  31. package/src/ir/sql-unspecified-namespace.ts +51 -0
  32. package/src/ir/storage-column.ts +55 -0
  33. package/src/ir/storage-table.ts +63 -0
  34. package/src/ir/storage-type-instance.ts +60 -0
  35. package/src/ir/unique-constraint.ts +22 -0
  36. package/src/types.ts +38 -99
  37. package/src/validators.ts +262 -18
  38. package/dist/types-hgzy8ME1.mjs +0 -13
  39. package/dist/types-hgzy8ME1.mjs.map +0 -1
  40. package/dist/types-njsiV-Ck.d.mts +0 -186
  41. package/dist/types-njsiV-Ck.d.mts.map +0 -1
  42. package/dist/validate.d.mts +0 -9
  43. package/dist/validate.d.mts.map +0 -1
  44. package/dist/validate.mjs +0 -106
  45. package/dist/validate.mjs.map +0 -1
  46. package/dist/validators-Dm5X-Hvg.mjs +0 -294
  47. package/dist/validators-Dm5X-Hvg.mjs.map +0 -1
  48. package/src/exports/validate.ts +0 -1
  49. package/src/validate.ts +0 -227
@@ -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';
1
+ import type { Contract, ContractField, ContractModel } from '@prisma-next/contract/types';
2
2
  import { ContractValidationError } from '@prisma-next/contract/validate-contract';
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 {
@@ -449,3 +517,179 @@ export function validateStorageSemantics(storage: SqlStorage): string[] {
449
517
 
450
518
  return errors;
451
519
  }
520
+
521
+ /**
522
+ * SQL storage logical-consistency checks: every model.storage.table
523
+ * resolves to a real table, every model.storage.fields[*].column
524
+ * resolves to a real column, and value-object fields land on JSON-native
525
+ * columns. Throws `ContractValidationError` on the first mismatch.
526
+ */
527
+ export function validateModelStorageReferences(contract: Contract<SqlStorage>): void {
528
+ const models = contract.models as Record<string, ContractModel<SqlModelStorage>>;
529
+ for (const [modelName, model] of Object.entries(models)) {
530
+ const storageTable = model.storage.table;
531
+
532
+ const table = contract.storage.tables[storageTable] as
533
+ | (typeof contract.storage.tables)[string]
534
+ | undefined;
535
+ if (!table) {
536
+ throw new ContractValidationError(
537
+ `Model "${modelName}" references non-existent table "${storageTable}"`,
538
+ 'storage',
539
+ );
540
+ }
541
+
542
+ const columnNames = new Set(Object.keys(table.columns));
543
+ for (const [fieldName, field] of Object.entries(model.storage.fields)) {
544
+ if (!columnNames.has(field.column)) {
545
+ throw new ContractValidationError(
546
+ `Model "${modelName}" field "${fieldName}" references non-existent column "${field.column}" in table "${storageTable}"`,
547
+ 'storage',
548
+ );
549
+ }
550
+ }
551
+
552
+ const JSON_NATIVE_TYPES = new Set(['json', 'jsonb']);
553
+ for (const [fieldName, domainField] of Object.entries(model.fields ?? {})) {
554
+ const f = domainField as ContractField;
555
+ if (f.type?.kind !== 'valueObject') continue;
556
+ const storageField = model.storage.fields[fieldName];
557
+ if (!storageField) continue;
558
+ const column = table.columns[storageField.column];
559
+ if (!column) continue;
560
+ if (!JSON_NATIVE_TYPES.has(column.nativeType)) {
561
+ throw new ContractValidationError(
562
+ `Model "${modelName}" field "${fieldName}" is a value object but storage column "${storageField.column}" has nativeType "${column.nativeType}" (expected json or jsonb)`,
563
+ 'storage',
564
+ );
565
+ }
566
+ }
567
+ }
568
+ }
569
+
570
+ /**
571
+ * Cross-table consistency checks for SQL storage: primary key, unique,
572
+ * index, and foreign key column references resolve to real columns;
573
+ * NOT NULL columns don't carry a literal `null` default; FK column
574
+ * counts match their referenced columns. Throws on the first mismatch.
575
+ */
576
+ export function validateSqlStorageConsistency(contract: Contract<SqlStorage>): void {
577
+ const tableNames = new Set(Object.keys(contract.storage.tables));
578
+
579
+ for (const [tableName, table] of Object.entries(contract.storage.tables)) {
580
+ const columnNames = new Set(Object.keys(table.columns));
581
+
582
+ if (table.primaryKey) {
583
+ for (const colName of table.primaryKey.columns) {
584
+ if (!columnNames.has(colName)) {
585
+ throw new ContractValidationError(
586
+ `Table "${tableName}" primaryKey references non-existent column "${colName}"`,
587
+ 'storage',
588
+ );
589
+ }
590
+ }
591
+ }
592
+
593
+ for (const unique of table.uniques) {
594
+ for (const colName of unique.columns) {
595
+ if (!columnNames.has(colName)) {
596
+ throw new ContractValidationError(
597
+ `Table "${tableName}" unique constraint references non-existent column "${colName}"`,
598
+ 'storage',
599
+ );
600
+ }
601
+ }
602
+ }
603
+
604
+ for (const index of table.indexes) {
605
+ for (const colName of index.columns) {
606
+ if (!columnNames.has(colName)) {
607
+ throw new ContractValidationError(
608
+ `Table "${tableName}" index references non-existent column "${colName}"`,
609
+ 'storage',
610
+ );
611
+ }
612
+ }
613
+ }
614
+
615
+ for (const [colName, column] of Object.entries(table.columns)) {
616
+ if (!column.nullable && column.default?.kind === 'literal' && column.default.value === null) {
617
+ throw new ContractValidationError(
618
+ `Table "${tableName}" column "${colName}" is NOT NULL but has a literal null default`,
619
+ 'storage',
620
+ );
621
+ }
622
+ }
623
+
624
+ for (const fk of table.foreignKeys) {
625
+ for (const colName of fk.columns) {
626
+ if (!columnNames.has(colName)) {
627
+ throw new ContractValidationError(
628
+ `Table "${tableName}" foreignKey references non-existent column "${colName}"`,
629
+ 'storage',
630
+ );
631
+ }
632
+ }
633
+
634
+ if (!tableNames.has(fk.references.table)) {
635
+ throw new ContractValidationError(
636
+ `Table "${tableName}" foreignKey references non-existent table "${fk.references.table}"`,
637
+ 'storage',
638
+ );
639
+ }
640
+
641
+ const referencedTable = contract.storage.tables[fk.references.table];
642
+ if (!referencedTable) continue;
643
+ const referencedColumnNames = new Set(Object.keys(referencedTable.columns));
644
+ for (const colName of fk.references.columns) {
645
+ if (!referencedColumnNames.has(colName)) {
646
+ throw new ContractValidationError(
647
+ `Table "${tableName}" foreignKey references non-existent column "${colName}" in table "${fk.references.table}"`,
648
+ 'storage',
649
+ );
650
+ }
651
+ }
652
+
653
+ if (fk.columns.length !== fk.references.columns.length) {
654
+ throw new ContractValidationError(
655
+ `Table "${tableName}" foreignKey column count (${fk.columns.length}) does not match referenced column count (${fk.references.columns.length})`,
656
+ 'storage',
657
+ );
658
+ }
659
+ }
660
+ }
661
+ }
662
+
663
+ /**
664
+ * Full SQL contract validation: structural (arktype) +
665
+ * framework-shared domain + SQL storage logical-consistency + SQL
666
+ * storage semantic + model ↔ storage reference checks. Throws
667
+ * `ContractValidationError` on the first failure. Returns the
668
+ * validated flat-data shape; IR class hydration happens in the SPI
669
+ * base on top of this helper.
670
+ */
671
+ export function validateSqlContractFully<T extends Contract<SqlStorage>>(value: unknown): T {
672
+ const stripped =
673
+ typeof value === 'object' && value !== null
674
+ ? (() => {
675
+ const { schemaVersion: _, _generated: _g, ...rest } = value as Record<string, unknown>;
676
+ return rest;
677
+ })()
678
+ : value;
679
+ const validated = validateSqlContract<T>(stripped);
680
+ validateContractDomain({
681
+ roots: validated.roots,
682
+ models: validated.models,
683
+ ...(validated.valueObjects ? { valueObjects: validated.valueObjects } : {}),
684
+ });
685
+ validateSqlStorageConsistency(validated);
686
+ const semanticErrors = validateStorageSemantics(validated.storage);
687
+ if (semanticErrors.length > 0) {
688
+ throw new ContractValidationError(
689
+ `Contract semantic validation failed: ${semanticErrors.join('; ')}`,
690
+ 'storage',
691
+ );
692
+ }
693
+ validateModelStorageReferences(validated);
694
+ return validated;
695
+ }
@@ -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
@@ -1 +0,0 @@
1
- {"version":3,"file":"types-hgzy8ME1.mjs","names":[],"sources":["../src/types.ts"],"sourcesContent":["import type { ColumnDefault, StorageBase } from '@prisma-next/contract/types';\nimport type { CodecTrait } from '@prisma-next/framework-components/codec';\n\n/**\n * A column definition in storage.\n *\n * `typeParams` is optional because most columns use non-parameterized types.\n * Columns with parameterized types can either inline `typeParams` or reference\n * a named {@link StorageTypeInstance} via `typeRef`.\n */\nexport type StorageColumn = {\n readonly nativeType: string;\n readonly codecId: string;\n readonly nullable: boolean;\n /**\n * Opaque, codec-owned JS/type parameters.\n * The codec that owns `codecId` defines the shape and semantics.\n * Mutually exclusive with `typeRef`.\n */\n readonly typeParams?: Record<string, unknown>;\n /**\n * Reference to a named type instance in `storage.types`.\n * Mutually exclusive with `typeParams`.\n */\n readonly typeRef?: string;\n /**\n * Default value for the column.\n * Can be a literal value or database function.\n */\n readonly default?: ColumnDefault;\n};\n\nexport type PrimaryKey = {\n readonly columns: readonly string[];\n readonly name?: string;\n};\n\nexport type UniqueConstraint = {\n readonly columns: readonly string[];\n readonly name?: string;\n};\n\nexport type Index = {\n readonly columns: readonly string[];\n readonly name?: string;\n readonly type?: string;\n readonly options?: Record<string, unknown>;\n};\n\nexport type ForeignKeyReferences = {\n readonly table: string;\n readonly columns: readonly string[];\n};\n\nexport type ReferentialAction = 'noAction' | 'restrict' | 'cascade' | 'setNull' | 'setDefault';\n\nexport type ForeignKeyOptions = {\n readonly name?: string;\n readonly onDelete?: ReferentialAction;\n readonly onUpdate?: ReferentialAction;\n};\n\nexport type ForeignKey = {\n readonly columns: readonly string[];\n readonly references: ForeignKeyReferences;\n readonly name?: string;\n readonly onDelete?: ReferentialAction;\n readonly onUpdate?: ReferentialAction;\n /** Whether to emit FK constraint DDL (ALTER TABLE … ADD CONSTRAINT … FOREIGN KEY). */\n readonly constraint: boolean;\n /** Whether to emit a backing index for the FK columns. */\n readonly index: boolean;\n};\n\nexport type StorageTable = {\n readonly columns: Record<string, StorageColumn>;\n readonly primaryKey?: PrimaryKey;\n readonly uniques: ReadonlyArray<UniqueConstraint>;\n readonly indexes: ReadonlyArray<Index>;\n readonly foreignKeys: ReadonlyArray<ForeignKey>;\n};\n\n/**\n * A named, parameterized type instance.\n * These are registered in `storage.types` for reuse across columns\n * and to enable ergonomic schema surfaces like `schema.types.MyType`.\n *\n * Unlike {@link StorageColumn}, `typeParams` is required here because\n * `StorageTypeInstance` exists specifically to define reusable parameterized types.\n * A type instance without parameters would be redundant—columns can reference\n * the codec directly via `codecId`.\n */\nexport type StorageTypeInstance = {\n readonly codecId: string;\n readonly nativeType: string;\n readonly typeParams: Record<string, unknown>;\n};\n\nexport type SqlStorage<THash extends string = string> = StorageBase<THash> & {\n readonly tables: Record<string, StorageTable>;\n /**\n * Named type instances for parameterized/custom types.\n * Columns can reference these via `typeRef`.\n */\n readonly types?: Record<string, StorageTypeInstance>;\n};\n\nexport type SqlModelFieldStorage = {\n readonly column: string;\n readonly codecId?: string;\n readonly nullable?: boolean;\n};\n\nexport type SqlModelStorage = {\n readonly table: string;\n readonly fields: Record<string, SqlModelFieldStorage>;\n};\n\nexport const DEFAULT_FK_CONSTRAINT = true;\nexport const DEFAULT_FK_INDEX = true;\n\nexport function applyFkDefaults(\n fk: { constraint?: boolean | undefined; index?: boolean | undefined },\n overrideDefaults?: { constraint?: boolean | undefined; index?: boolean | undefined },\n): { constraint: boolean; index: boolean } {\n return {\n constraint: fk.constraint ?? overrideDefaults?.constraint ?? DEFAULT_FK_CONSTRAINT,\n index: fk.index ?? overrideDefaults?.index ?? DEFAULT_FK_INDEX,\n };\n}\n\nexport type TypeMaps<\n TCodecTypes extends Record<string, { output: unknown }> = Record<string, never>,\n TQueryOperationTypes extends Record<string, unknown> = Record<string, never>,\n TFieldOutputTypes extends Record<string, Record<string, unknown>> = Record<string, never>,\n TFieldInputTypes extends Record<string, Record<string, unknown>> = Record<string, never>,\n> = {\n readonly codecTypes: TCodecTypes;\n readonly queryOperationTypes: TQueryOperationTypes;\n readonly fieldOutputTypes: TFieldOutputTypes;\n readonly fieldInputTypes: TFieldInputTypes;\n};\n\nexport type CodecTypesOf<T> = [T] extends [never]\n ? Record<string, never>\n : T extends { readonly codecTypes: infer C }\n ? C extends Record<string, { output: unknown }>\n ? C\n : Record<string, never>\n : Record<string, never>;\n\n/**\n * Dispatch hint identifying the first-argument target of an operation.\n *\n * Used by ORM column helpers to decide whether an operation is reachable on a\n * field. Either names a concrete codec identity or a set of capability traits\n * that the field's codec must carry.\n */\nexport type QueryOperationSelfSpec =\n | { readonly codecId: string; readonly traits?: never }\n | { readonly traits: readonly CodecTrait[]; readonly codecId?: never };\n\n/**\n * Structural shape an operation's impl must return: any value carrying a\n * codec-exact `returnType` descriptor. `Expression<T>` (from\n * `@prisma-next/sql-relational-core/expression`, with `T extends ScopeField`)\n * extends this. Trait-targeted returns are deliberately excluded — predicate\n * detection and result decoding both depend on knowing the concrete return\n * codec.\n */\nexport type QueryOperationReturn = {\n readonly returnType: { readonly codecId: string; readonly nullable: boolean };\n};\n\nexport type QueryOperationTypeEntry = {\n readonly self?: QueryOperationSelfSpec;\n readonly impl: (...args: never[]) => QueryOperationReturn;\n};\n\nexport type SqlQueryOperationTypes<\n _CT extends Record<string, { readonly input: unknown; readonly output: unknown }>,\n T extends Record<string, QueryOperationTypeEntry>,\n> = T;\n\nexport type QueryOperationTypesBase = Record<string, QueryOperationTypeEntry>;\n\nexport type QueryOperationTypesOf<T> = [T] extends [never]\n ? Record<string, never>\n : T extends { readonly queryOperationTypes: infer Q }\n ? Q extends Record<string, unknown>\n ? Q\n : Record<string, never>\n : Record<string, never>;\n\nexport type TypeMapsPhantomKey = '__@prisma-next/sql-contract/typeMaps@__';\n\nexport type ContractWithTypeMaps<TContract, TTypeMaps> = TContract & {\n readonly [K in TypeMapsPhantomKey]?: TTypeMaps;\n};\n\nexport type ExtractTypeMapsFromContract<T> = TypeMapsPhantomKey extends keyof T\n ? NonNullable<T[TypeMapsPhantomKey & keyof T]>\n : never;\n\nexport type FieldOutputTypesOf<T> = [T] extends [never]\n ? Record<string, never>\n : T extends { readonly fieldOutputTypes: infer F }\n ? F extends Record<string, Record<string, unknown>>\n ? F\n : Record<string, never>\n : Record<string, never>;\n\nexport type FieldInputTypesOf<T> = [T] extends [never]\n ? Record<string, never>\n : T extends { readonly fieldInputTypes: infer F }\n ? F extends Record<string, Record<string, unknown>>\n ? F\n : Record<string, never>\n : Record<string, never>;\n\nexport type ExtractCodecTypes<T> = CodecTypesOf<ExtractTypeMapsFromContract<T>>;\nexport type ExtractQueryOperationTypes<T> = QueryOperationTypesOf<ExtractTypeMapsFromContract<T>>;\nexport type ExtractFieldOutputTypes<T> = FieldOutputTypesOf<ExtractTypeMapsFromContract<T>>;\nexport type ExtractFieldInputTypes<T> = FieldInputTypesOf<ExtractTypeMapsFromContract<T>>;\n\nexport type ResolveCodecTypes<TContract, TTypeMaps> = [TTypeMaps] extends [never]\n ? ExtractCodecTypes<TContract>\n : CodecTypesOf<TTypeMaps>;\n"],"mappings":";AAsHA,MAAa,wBAAwB;AACrC,MAAa,mBAAmB;AAEhC,SAAgB,gBACd,IACA,kBACyC;CACzC,OAAO;EACL,YAAY,GAAG,cAAc,kBAAkB,cAAA;EAC/C,OAAO,GAAG,SAAS,kBAAkB,SAAA;EACtC"}