@prisma-next/sql-contract 0.3.0-pr.99.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +80 -11
  3. package/dist/factories.d.mts +29 -0
  4. package/dist/factories.d.mts.map +1 -0
  5. package/dist/factories.mjs +64 -0
  6. package/dist/factories.mjs.map +1 -0
  7. package/dist/pack-types.d.mts +13 -0
  8. package/dist/pack-types.d.mts.map +1 -0
  9. package/dist/pack-types.mjs +1 -0
  10. package/dist/types-BYQMEXGG.d.mts +176 -0
  11. package/dist/types-BYQMEXGG.d.mts.map +1 -0
  12. package/dist/types-DRR5stkj.mjs +13 -0
  13. package/dist/types-DRR5stkj.mjs.map +1 -0
  14. package/dist/types.d.mts +2 -0
  15. package/dist/types.mjs +3 -0
  16. package/dist/validate.d.mts +9 -0
  17. package/dist/validate.d.mts.map +1 -0
  18. package/dist/validate.mjs +107 -0
  19. package/dist/validate.mjs.map +1 -0
  20. package/dist/validators-BjZ6lOS1.mjs +281 -0
  21. package/dist/validators-BjZ6lOS1.mjs.map +1 -0
  22. package/dist/validators.d.mts +61 -0
  23. package/dist/validators.d.mts.map +1 -0
  24. package/dist/validators.mjs +3 -0
  25. package/package.json +18 -21
  26. package/src/exports/factories.ts +1 -11
  27. package/src/exports/types.ts +23 -6
  28. package/src/exports/validate.ts +1 -0
  29. package/src/exports/validators.ts +1 -1
  30. package/src/factories.ts +29 -57
  31. package/src/index.ts +1 -0
  32. package/src/types.ts +126 -31
  33. package/src/validate.ts +227 -0
  34. package/src/validators.ts +296 -64
  35. package/dist/exports/factories.d.ts +0 -2
  36. package/dist/exports/factories.d.ts.map +0 -1
  37. package/dist/exports/factories.js +0 -83
  38. package/dist/exports/factories.js.map +0 -1
  39. package/dist/exports/pack-types.d.ts +0 -2
  40. package/dist/exports/pack-types.d.ts.map +0 -1
  41. package/dist/exports/pack-types.js +0 -1
  42. package/dist/exports/pack-types.js.map +0 -1
  43. package/dist/exports/types.d.ts +0 -2
  44. package/dist/exports/types.d.ts.map +0 -1
  45. package/dist/exports/types.js +0 -1
  46. package/dist/exports/types.js.map +0 -1
  47. package/dist/exports/validators.d.ts +0 -2
  48. package/dist/exports/validators.d.ts.map +0 -1
  49. package/dist/exports/validators.js +0 -109
  50. package/dist/exports/validators.js.map +0 -1
  51. package/dist/factories.d.ts +0 -38
  52. package/dist/factories.d.ts.map +0 -1
  53. package/dist/index.d.ts +0 -4
  54. package/dist/index.d.ts.map +0 -1
  55. package/dist/pack-types.d.ts +0 -10
  56. package/dist/pack-types.d.ts.map +0 -1
  57. package/dist/types.d.ts +0 -106
  58. package/dist/types.d.ts.map +0 -1
  59. package/dist/validators.d.ts +0 -35
  60. package/dist/validators.d.ts.map +0 -1
@@ -0,0 +1,227 @@
1
+ import type {
2
+ ColumnDefaultLiteralInputValue,
3
+ Contract,
4
+ ContractField,
5
+ ContractModel,
6
+ JsonValue,
7
+ } from '@prisma-next/contract/types';
8
+ import {
9
+ ContractValidationError,
10
+ validateContract as frameworkValidateContract,
11
+ } from '@prisma-next/contract/validate-contract';
12
+ import type { CodecLookup } from '@prisma-next/framework-components/codec';
13
+ import type { SqlModelStorage, SqlStorage, StorageColumn, StorageTable } from './types';
14
+ import { validateSqlContract, validateStorageSemantics } from './validators';
15
+
16
+ type SqlValidationContract = Contract<SqlStorage, Record<string, ContractModel<SqlModelStorage>>>;
17
+
18
+ function validateModelStorageReferences(contract: SqlValidationContract): void {
19
+ for (const [modelName, model] of Object.entries(contract.models)) {
20
+ const storageTable = model.storage.table;
21
+
22
+ const table = contract.storage.tables[storageTable] as
23
+ | (typeof contract.storage.tables)[string]
24
+ | undefined;
25
+ if (!table) {
26
+ throw new ContractValidationError(
27
+ `Model "${modelName}" references non-existent table "${storageTable}"`,
28
+ 'storage',
29
+ );
30
+ }
31
+
32
+ const columnNames = new Set(Object.keys(table.columns));
33
+ for (const [fieldName, field] of Object.entries(model.storage.fields)) {
34
+ if (!columnNames.has(field.column)) {
35
+ throw new ContractValidationError(
36
+ `Model "${modelName}" field "${fieldName}" references non-existent column "${field.column}" in table "${storageTable}"`,
37
+ 'storage',
38
+ );
39
+ }
40
+ }
41
+
42
+ const JSON_NATIVE_TYPES = new Set(['json', 'jsonb']);
43
+ for (const [fieldName, domainField] of Object.entries(model.fields)) {
44
+ const f = domainField as ContractField;
45
+ if (f.type?.kind !== 'valueObject') continue;
46
+ const storageField = model.storage.fields[fieldName];
47
+ if (!storageField) continue;
48
+ const column = table.columns[storageField.column];
49
+ if (!column) continue;
50
+ if (!JSON_NATIVE_TYPES.has(column.nativeType)) {
51
+ throw new ContractValidationError(
52
+ `Model "${modelName}" field "${fieldName}" is a value object but storage column "${storageField.column}" has nativeType "${column.nativeType}" (expected json or jsonb)`,
53
+ 'storage',
54
+ );
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ function validateContractLogic(contract: Contract<SqlStorage>): void {
61
+ const tableNames = new Set(Object.keys(contract.storage.tables));
62
+
63
+ for (const [tableName, table] of Object.entries(contract.storage.tables)) {
64
+ const columnNames = new Set(Object.keys(table.columns));
65
+
66
+ if (table.primaryKey) {
67
+ for (const colName of table.primaryKey.columns) {
68
+ if (!columnNames.has(colName)) {
69
+ throw new ContractValidationError(
70
+ `Table "${tableName}" primaryKey references non-existent column "${colName}"`,
71
+ 'storage',
72
+ );
73
+ }
74
+ }
75
+ }
76
+
77
+ for (const unique of table.uniques) {
78
+ for (const colName of unique.columns) {
79
+ if (!columnNames.has(colName)) {
80
+ throw new ContractValidationError(
81
+ `Table "${tableName}" unique constraint references non-existent column "${colName}"`,
82
+ 'storage',
83
+ );
84
+ }
85
+ }
86
+ }
87
+
88
+ for (const index of table.indexes) {
89
+ for (const colName of index.columns) {
90
+ if (!columnNames.has(colName)) {
91
+ throw new ContractValidationError(
92
+ `Table "${tableName}" index references non-existent column "${colName}"`,
93
+ 'storage',
94
+ );
95
+ }
96
+ }
97
+ }
98
+
99
+ for (const [colName, column] of Object.entries(table.columns)) {
100
+ if (!column.nullable && column.default?.kind === 'literal' && column.default.value === null) {
101
+ throw new ContractValidationError(
102
+ `Table "${tableName}" column "${colName}" is NOT NULL but has a literal null default`,
103
+ 'storage',
104
+ );
105
+ }
106
+ }
107
+
108
+ for (const fk of table.foreignKeys) {
109
+ for (const colName of fk.columns) {
110
+ if (!columnNames.has(colName)) {
111
+ throw new ContractValidationError(
112
+ `Table "${tableName}" foreignKey references non-existent column "${colName}"`,
113
+ 'storage',
114
+ );
115
+ }
116
+ }
117
+
118
+ if (!tableNames.has(fk.references.table)) {
119
+ throw new ContractValidationError(
120
+ `Table "${tableName}" foreignKey references non-existent table "${fk.references.table}"`,
121
+ 'storage',
122
+ );
123
+ }
124
+
125
+ const referencedTable = contract.storage.tables[fk.references.table];
126
+ if (!referencedTable) continue;
127
+ const referencedColumnNames = new Set(Object.keys(referencedTable.columns));
128
+ for (const colName of fk.references.columns) {
129
+ if (!referencedColumnNames.has(colName)) {
130
+ throw new ContractValidationError(
131
+ `Table "${tableName}" foreignKey references non-existent column "${colName}" in table "${fk.references.table}"`,
132
+ 'storage',
133
+ );
134
+ }
135
+ }
136
+
137
+ if (fk.columns.length !== fk.references.columns.length) {
138
+ throw new ContractValidationError(
139
+ `Table "${tableName}" foreignKey column count (${fk.columns.length}) does not match referenced column count (${fk.references.columns.length})`,
140
+ 'storage',
141
+ );
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ function validateSqlStorage(contract: Contract): void {
148
+ const sqlContract = validateSqlContract<SqlValidationContract>(contract);
149
+ validateContractLogic(sqlContract);
150
+ validateModelStorageReferences(sqlContract);
151
+ const semanticErrors = validateStorageSemantics(sqlContract.storage);
152
+ if (semanticErrors.length > 0) {
153
+ throw new ContractValidationError(
154
+ `Contract semantic validation failed: ${semanticErrors.join('; ')}`,
155
+ 'storage',
156
+ );
157
+ }
158
+ }
159
+
160
+ function decodeContractDefaults<T extends Contract<SqlStorage>>(
161
+ contract: T,
162
+ codecLookup: CodecLookup,
163
+ ): T {
164
+ const tables = contract.storage.tables;
165
+ let tablesChanged = false;
166
+ const decodedTables: Record<string, StorageTable> = {};
167
+
168
+ for (const [tableName, table] of Object.entries(tables)) {
169
+ let columnsChanged = false;
170
+ const decodedColumns: Record<string, StorageColumn> = {};
171
+
172
+ for (const [columnName, column] of Object.entries(table.columns)) {
173
+ if (column.default?.kind === 'literal') {
174
+ const codec = codecLookup.get(column.codecId);
175
+ if (codec) {
176
+ const decodedValue = codec.decodeJson(
177
+ column.default.value as JsonValue,
178
+ ) as ColumnDefaultLiteralInputValue;
179
+ if (decodedValue !== column.default.value) {
180
+ columnsChanged = true;
181
+ decodedColumns[columnName] = {
182
+ ...column,
183
+ default: { kind: 'literal', value: decodedValue },
184
+ };
185
+ continue;
186
+ }
187
+ }
188
+ }
189
+ decodedColumns[columnName] = column;
190
+ }
191
+
192
+ if (columnsChanged) {
193
+ tablesChanged = true;
194
+ decodedTables[tableName] = { ...table, columns: decodedColumns };
195
+ } else {
196
+ decodedTables[tableName] = table;
197
+ }
198
+ }
199
+
200
+ if (!tablesChanged) {
201
+ return contract;
202
+ }
203
+
204
+ return {
205
+ ...contract,
206
+ storage: {
207
+ ...contract.storage,
208
+ tables: decodedTables,
209
+ },
210
+ } as T;
211
+ }
212
+
213
+ export function validateContract<TContract extends Contract<SqlStorage>>(
214
+ value: unknown,
215
+ codecLookup: CodecLookup,
216
+ ): TContract {
217
+ const validated = frameworkValidateContract<TContract>(value, validateSqlStorage);
218
+ try {
219
+ return decodeContractDefaults(validated, codecLookup);
220
+ } catch (error) {
221
+ if (error instanceof ContractValidationError) throw error;
222
+ throw new ContractValidationError(
223
+ error instanceof Error ? error.message : String(error),
224
+ 'storage',
225
+ );
226
+ }
227
+ }
package/src/validators.ts CHANGED
@@ -1,35 +1,81 @@
1
+ import type { Contract } from '@prisma-next/contract/types';
2
+ import { ContractValidationError } from '@prisma-next/contract/validate-contract';
1
3
  import { type } from 'arktype';
2
4
  import type {
3
5
  ForeignKey,
4
6
  ForeignKeyReferences,
5
- Index,
6
- ModelDefinition,
7
- ModelField,
8
- ModelStorage,
9
7
  PrimaryKey,
10
- SqlContract,
8
+ ReferentialAction,
11
9
  SqlStorage,
12
- StorageColumn,
13
- StorageTable,
14
10
  StorageTypeInstance,
15
11
  UniqueConstraint,
16
12
  } from './types';
17
13
 
18
- const StorageColumnSchema = type
19
- .declare<StorageColumn>()
20
- .type({
21
- nativeType: 'string',
22
- codecId: 'string',
23
- nullable: 'boolean',
24
- 'typeParams?': 'Record<string, unknown>',
25
- 'typeRef?': 'string',
26
- })
27
- .narrow((col, ctx) => {
28
- if (col.typeParams !== undefined && col.typeRef !== undefined) {
29
- return ctx.mustBe('a column with either typeParams or typeRef, not both');
30
- }
31
- return true;
32
- });
14
+ type ColumnDefaultLiteral = {
15
+ readonly kind: 'literal';
16
+ readonly value: string | number | boolean | Record<string, unknown> | unknown[] | null;
17
+ };
18
+ type ColumnDefaultFunction = { readonly kind: 'function'; readonly expression: string };
19
+ const literalKindSchema = type("'literal'");
20
+ const functionKindSchema = type("'function'");
21
+ const generatorKindSchema = type("'generator'");
22
+ const generatorIdSchema = type('string').narrow((value, ctx) => {
23
+ return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(value) ? true : ctx.mustBe('a flat generator id');
24
+ });
25
+
26
+ export const ColumnDefaultLiteralSchema = type.declare<ColumnDefaultLiteral>().type({
27
+ kind: literalKindSchema,
28
+ value: 'string | number | boolean | null | unknown[] | Record<string, unknown>',
29
+ });
30
+
31
+ export const ColumnDefaultFunctionSchema = type.declare<ColumnDefaultFunction>().type({
32
+ kind: functionKindSchema,
33
+ expression: 'string',
34
+ });
35
+
36
+ export const ColumnDefaultSchema = ColumnDefaultLiteralSchema.or(ColumnDefaultFunctionSchema);
37
+
38
+ const ExecutionMutationDefaultValueSchema = type({
39
+ '+': 'reject',
40
+ kind: generatorKindSchema,
41
+ id: generatorIdSchema,
42
+ 'params?': 'Record<string, unknown>',
43
+ });
44
+
45
+ const ExecutionMutationDefaultSchema = type({
46
+ '+': 'reject',
47
+ ref: {
48
+ '+': 'reject',
49
+ table: 'string',
50
+ column: 'string',
51
+ },
52
+ 'onCreate?': ExecutionMutationDefaultValueSchema,
53
+ 'onUpdate?': ExecutionMutationDefaultValueSchema,
54
+ });
55
+
56
+ const ExecutionSchema = type({
57
+ '+': 'reject',
58
+ executionHash: 'string',
59
+ mutations: {
60
+ '+': 'reject',
61
+ defaults: ExecutionMutationDefaultSchema.array().readonly(),
62
+ },
63
+ });
64
+
65
+ const StorageColumnSchema = type({
66
+ '+': 'reject',
67
+ nativeType: 'string',
68
+ codecId: 'string',
69
+ nullable: 'boolean',
70
+ 'typeParams?': 'Record<string, unknown>',
71
+ 'typeRef?': 'string',
72
+ 'default?': ColumnDefaultSchema,
73
+ }).narrow((col, ctx) => {
74
+ if (col.typeParams !== undefined && col.typeRef !== undefined) {
75
+ return ctx.mustBe('a column with either typeParams or typeRef, not both');
76
+ }
77
+ return true;
78
+ });
33
79
 
34
80
  const StorageTypeInstanceSchema = type.declare<StorageTypeInstance>().type({
35
81
  codecId: 'string',
@@ -47,23 +93,34 @@ const UniqueConstraintSchema = type.declare<UniqueConstraint>().type({
47
93
  'name?': 'string',
48
94
  });
49
95
 
50
- const IndexSchema = type.declare<Index>().type({
96
+ export const IndexSchema = type({
51
97
  columns: type.string.array().readonly(),
52
98
  'name?': 'string',
99
+ 'using?': 'string',
100
+ 'config?': 'Record<string, unknown>',
53
101
  });
54
102
 
55
- const ForeignKeyReferencesSchema = type.declare<ForeignKeyReferences>().type({
103
+ export const ForeignKeyReferencesSchema = type.declare<ForeignKeyReferences>().type({
56
104
  table: 'string',
57
105
  columns: type.string.array().readonly(),
58
106
  });
59
107
 
60
- const ForeignKeySchema = type.declare<ForeignKey>().type({
108
+ export const ReferentialActionSchema = type
109
+ .declare<ReferentialAction>()
110
+ .type("'noAction' | 'restrict' | 'cascade' | 'setNull' | 'setDefault'");
111
+
112
+ export const ForeignKeySchema = type.declare<ForeignKey>().type({
61
113
  columns: type.string.array().readonly(),
62
114
  references: ForeignKeyReferencesSchema,
63
115
  'name?': 'string',
116
+ 'onDelete?': ReferentialActionSchema,
117
+ 'onUpdate?': ReferentialActionSchema,
118
+ constraint: 'boolean',
119
+ index: 'boolean',
64
120
  });
65
121
 
66
- const StorageTableSchema = type.declare<StorageTable>().type({
122
+ const StorageTableSchema = type({
123
+ '+': 'reject',
67
124
  columns: type({ '[string]': StorageColumnSchema }),
68
125
  'primaryKey?': PrimaryKeySchema,
69
126
  uniques: UniqueConstraintSchema.array().readonly(),
@@ -71,39 +128,97 @@ const StorageTableSchema = type.declare<StorageTable>().type({
71
128
  foreignKeys: ForeignKeySchema.array().readonly(),
72
129
  });
73
130
 
74
- const StorageSchema = type.declare<SqlStorage>().type({
131
+ const StorageSchema = type({
132
+ '+': 'reject',
133
+ storageHash: 'string',
75
134
  tables: type({ '[string]': StorageTableSchema }),
76
135
  'types?': type({ '[string]': StorageTypeInstanceSchema }),
77
136
  });
78
137
 
79
- const ModelFieldSchema = type.declare<ModelField>().type({
138
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
139
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
140
+ }
141
+
142
+ function isContractFieldType(value: unknown): boolean {
143
+ if (!isPlainRecord(value)) return false;
144
+ const kind = value['kind'];
145
+ if (kind === 'scalar') {
146
+ if (typeof value['codecId'] !== 'string') return false;
147
+ const typeParams = value['typeParams'];
148
+ if (typeParams !== undefined && !isPlainRecord(typeParams)) return false;
149
+ return true;
150
+ }
151
+ if (kind === 'valueObject') {
152
+ return typeof value['name'] === 'string';
153
+ }
154
+ if (kind === 'union') {
155
+ const members = value['members'];
156
+ if (!Array.isArray(members)) return false;
157
+ return members.every((m) => isContractFieldType(m));
158
+ }
159
+ return false;
160
+ }
161
+
162
+ const ContractFieldTypeSchema = type('unknown').narrow((value, ctx) =>
163
+ isContractFieldType(value) ? true : ctx.mustBe('scalar, valueObject, or union field type'),
164
+ );
165
+
166
+ const ModelFieldSchema = type({
167
+ '+': 'reject',
168
+ nullable: 'boolean',
169
+ type: ContractFieldTypeSchema,
170
+ 'many?': 'true',
171
+ 'dict?': 'true',
172
+ });
173
+
174
+ const ModelStorageFieldSchema = type({
80
175
  column: 'string',
176
+ 'codecId?': 'string',
177
+ 'nullable?': 'boolean',
81
178
  });
82
179
 
83
- const ModelStorageSchema = type.declare<ModelStorage>().type({
180
+ const ModelStorageSchema = type({
84
181
  table: 'string',
182
+ fields: type({ '[string]': ModelStorageFieldSchema }),
85
183
  });
86
184
 
87
- const ModelSchema = type.declare<ModelDefinition>().type({
185
+ const ModelSchema = type({
88
186
  storage: ModelStorageSchema,
89
- fields: type({ '[string]': ModelFieldSchema }),
90
- relations: type({ '[string]': 'unknown' }),
187
+ 'fields?': type({ '[string]': ModelFieldSchema }),
188
+ 'relations?': type({ '[string]': 'unknown' }),
189
+ 'discriminator?': 'unknown',
190
+ 'variants?': 'unknown',
191
+ 'base?': 'string',
192
+ 'owner?': 'string',
193
+ });
194
+
195
+ const ContractMetaSchema = type({
196
+ '[string]': 'unknown',
91
197
  });
92
198
 
93
199
  const SqlContractSchema = type({
94
- 'schemaVersion?': "'1'",
200
+ '+': 'reject',
95
201
  target: 'string',
96
202
  targetFamily: "'sql'",
97
- coreHash: 'string',
98
- 'profileHash?': 'string',
203
+ 'coreHash?': 'string',
204
+ profileHash: 'string',
99
205
  'capabilities?': 'Record<string, Record<string, boolean>>',
100
206
  'extensionPacks?': 'Record<string, unknown>',
101
- 'meta?': 'Record<string, unknown>',
102
- 'sources?': 'Record<string, unknown>',
207
+ 'meta?': ContractMetaSchema,
208
+ 'roots?': 'Record<string, string>',
103
209
  models: type({ '[string]': ModelSchema }),
210
+ 'valueObjects?': 'Record<string, unknown>',
104
211
  storage: StorageSchema,
212
+ 'execution?': ExecutionSchema,
105
213
  });
106
214
 
215
+ // NOTE: StorageColumnSchema, StorageTableSchema, and StorageSchema use bare type()
216
+ // instead of type.declare<T>().type() because the ColumnDefault union's value field
217
+ // includes bigint | Date (runtime-only types after decoding) which cannot be expressed
218
+ // in Arktype's JSON validation DSL. The `as SqlStorage` cast in validateStorage() bridges
219
+ // the gap between the JSON-safe Arktype output and the runtime TypeScript type.
220
+ // See decodeContractDefaults() in validate.ts for the decoding step.
221
+
107
222
  /**
108
223
  * Validates the structural shape of SqlStorage using Arktype.
109
224
  *
@@ -117,17 +232,10 @@ export function validateStorage(value: unknown): SqlStorage {
117
232
  const messages = result.map((p: { message: string }) => p.message).join('; ');
118
233
  throw new Error(`Storage validation failed: ${messages}`);
119
234
  }
120
- return result;
235
+ return result as SqlStorage;
121
236
  }
122
237
 
123
- /**
124
- * Validates the structural shape of ModelDefinition using Arktype.
125
- *
126
- * @param value - The model value to validate
127
- * @returns The validated model if structure is valid
128
- * @throws Error if the model structure is invalid
129
- */
130
- export function validateModel(value: unknown): ModelDefinition {
238
+ export function validateModel(value: unknown): unknown {
131
239
  const result = ModelSchema(value);
132
240
  if (result instanceof type.errors) {
133
241
  const messages = result.map((p: { message: string }) => p.message).join('; ');
@@ -137,37 +245,161 @@ export function validateModel(value: unknown): ModelDefinition {
137
245
  }
138
246
 
139
247
  /**
140
- * Validates the structural shape of a SqlContract using Arktype.
248
+ * Validates the structural shape of an SQL contract using Arktype.
141
249
  *
142
- * **Responsibility: Validation Only**
143
- * This function validates that the contract has the correct structure and types.
144
- * It does NOT normalize the contract - normalization must happen in the contract builder.
145
- *
146
- * The contract passed to this function must already be normalized (all required fields present).
147
- * If normalization is needed, it should be done by the contract builder before calling this function.
148
- *
149
- * This ensures all required fields are present and have the correct types.
250
+ * Ensures all required fields are present and have the correct types,
251
+ * including SQL-specific storage structure (tables, columns, constraints).
150
252
  *
151
253
  * @param value - The contract value to validate (typically from a JSON import)
152
254
  * @returns The validated contract if structure is valid
153
- * @throws Error if the contract structure is invalid
255
+ * @throws ContractValidationError if the contract structure is invalid
154
256
  */
155
- export function validateSqlContract<T extends SqlContract<SqlStorage>>(value: unknown): T {
156
- // Check targetFamily first to provide a clear error message for unsupported target families
257
+ export function validateSqlContract<T extends Contract<SqlStorage>>(value: unknown): T {
258
+ if (typeof value !== 'object' || value === null) {
259
+ throw new ContractValidationError(
260
+ 'Contract structural validation failed: value must be an object',
261
+ 'structural',
262
+ );
263
+ }
264
+
157
265
  const rawValue = value as { targetFamily?: string };
158
266
  if (rawValue.targetFamily !== undefined && rawValue.targetFamily !== 'sql') {
159
- throw new Error(`Unsupported target family: ${rawValue.targetFamily}`);
267
+ throw new ContractValidationError(
268
+ `Unsupported target family: ${rawValue.targetFamily}`,
269
+ 'structural',
270
+ );
160
271
  }
161
272
 
162
273
  const contractResult = SqlContractSchema(value);
163
274
 
164
275
  if (contractResult instanceof type.errors) {
165
276
  const messages = contractResult.map((p: { message: string }) => p.message).join('; ');
166
- throw new Error(`Contract structural validation failed: ${messages}`);
277
+ throw new ContractValidationError(
278
+ `Contract structural validation failed: ${messages}`,
279
+ 'structural',
280
+ );
281
+ }
282
+
283
+ // Arktype's inferred output type differs from T due to exactOptionalPropertyTypes
284
+ // and branded hash types — the runtime value is structurally compatible after validation
285
+ return contractResult as unknown as T;
286
+ }
287
+
288
+ /**
289
+ * Validates semantic constraints on SqlStorage that cannot be expressed in Arktype schemas.
290
+ *
291
+ * Returns an array of human-readable error strings. Empty array = valid.
292
+ *
293
+ * Currently checks:
294
+ * - duplicate named primary key / unique / index / foreign key objects within a table
295
+ * - duplicate unique, index, or foreign key declarations within a table
296
+ * - `setNull` referential action on a non-nullable FK column (would fail at runtime)
297
+ * - `setDefault` referential action on a non-nullable FK column without a DEFAULT (would fail at runtime)
298
+ */
299
+ export function validateStorageSemantics(storage: SqlStorage): string[] {
300
+ const errors: string[] = [];
301
+
302
+ for (const [tableName, table] of Object.entries(storage.tables)) {
303
+ const namedObjects = new Map<string, string[]>();
304
+ const registerNamedObject = (kind: string, name: string | undefined) => {
305
+ if (!name) return;
306
+ namedObjects.set(name, [...(namedObjects.get(name) ?? []), kind]);
307
+ };
308
+
309
+ registerNamedObject('primary key', table.primaryKey?.name);
310
+ for (const unique of table.uniques) {
311
+ registerNamedObject('unique constraint', unique.name);
312
+ }
313
+ for (const index of table.indexes) {
314
+ registerNamedObject('index', index.name);
315
+ }
316
+ for (const fk of table.foreignKeys) {
317
+ registerNamedObject('foreign key', fk.name);
318
+ }
319
+
320
+ for (const [name, kinds] of namedObjects) {
321
+ if (kinds.length > 1) {
322
+ errors.push(
323
+ `Table "${tableName}": named object "${name}" is declared multiple times (${kinds.join(', ')})`,
324
+ );
325
+ }
326
+ }
327
+
328
+ const seenUniqueDefinitions = new Set<string>();
329
+ for (const unique of table.uniques) {
330
+ const signature = JSON.stringify({ columns: unique.columns });
331
+ if (seenUniqueDefinitions.has(signature)) {
332
+ errors.push(
333
+ `Table "${tableName}": duplicate unique constraint definition on columns [${unique.columns.join(', ')}]`,
334
+ );
335
+ continue;
336
+ }
337
+ seenUniqueDefinitions.add(signature);
338
+ }
339
+
340
+ const seenIndexDefinitions = new Set<string>();
341
+ for (const index of table.indexes) {
342
+ const signature = JSON.stringify({
343
+ columns: index.columns,
344
+ using: index.using ?? null,
345
+ config: index.config ?? null,
346
+ });
347
+ if (seenIndexDefinitions.has(signature)) {
348
+ errors.push(
349
+ `Table "${tableName}": duplicate index definition on columns [${index.columns.join(', ')}]`,
350
+ );
351
+ continue;
352
+ }
353
+ seenIndexDefinitions.add(signature);
354
+ }
355
+
356
+ const seenForeignKeyDefinitions = new Set<string>();
357
+ for (const fk of table.foreignKeys) {
358
+ const signature = JSON.stringify({
359
+ columns: fk.columns,
360
+ references: fk.references,
361
+ onDelete: fk.onDelete ?? null,
362
+ onUpdate: fk.onUpdate ?? null,
363
+ constraint: fk.constraint,
364
+ index: fk.index,
365
+ });
366
+ if (seenForeignKeyDefinitions.has(signature)) {
367
+ errors.push(
368
+ `Table "${tableName}": duplicate foreign key definition on columns [${fk.columns.join(', ')}]`,
369
+ );
370
+ continue;
371
+ }
372
+ seenForeignKeyDefinitions.add(signature);
373
+ }
374
+
375
+ for (const fk of table.foreignKeys) {
376
+ for (const colName of fk.columns) {
377
+ const column = table.columns[colName];
378
+ if (!column) continue;
379
+
380
+ if (fk.onDelete === 'setNull' && !column.nullable) {
381
+ errors.push(
382
+ `Table "${tableName}": onDelete setNull on foreign key column "${colName}" which is NOT NULL`,
383
+ );
384
+ }
385
+ if (fk.onUpdate === 'setNull' && !column.nullable) {
386
+ errors.push(
387
+ `Table "${tableName}": onUpdate setNull on foreign key column "${colName}" which is NOT NULL`,
388
+ );
389
+ }
390
+ if (fk.onDelete === 'setDefault' && !column.nullable && column.default === undefined) {
391
+ errors.push(
392
+ `Table "${tableName}": onDelete setDefault on foreign key column "${colName}" which is NOT NULL and has no DEFAULT`,
393
+ );
394
+ }
395
+ if (fk.onUpdate === 'setDefault' && !column.nullable && column.default === undefined) {
396
+ errors.push(
397
+ `Table "${tableName}": onUpdate setDefault on foreign key column "${colName}" which is NOT NULL and has no DEFAULT`,
398
+ );
399
+ }
400
+ }
401
+ }
167
402
  }
168
403
 
169
- // After validation, contractResult matches the schema and preserves the input structure
170
- // TypeScript needs an assertion here due to exactOptionalPropertyTypes differences
171
- // between Arktype's inferred type and the generic T, but runtime-wise they're compatible
172
- return contractResult as T;
404
+ return errors;
173
405
  }
@@ -1,2 +0,0 @@
1
- export { col, contract, fk, index, model, pk, storage, table, unique, } from '../factories';
2
- //# sourceMappingURL=factories.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"factories.d.ts","sourceRoot":"","sources":["../../src/exports/factories.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,GAAG,EACH,QAAQ,EACR,EAAE,EACF,KAAK,EACL,KAAK,EACL,EAAE,EACF,OAAO,EACP,KAAK,EACL,MAAM,GACP,MAAM,cAAc,CAAC"}