@prisma-next/sql-contract-ts 0.3.0-dev.135 → 0.3.0-dev.146

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.
package/src/contract.ts DELETED
@@ -1,475 +0,0 @@
1
- import type {
2
- PrimaryKey,
3
- SqlContract,
4
- SqlStorage,
5
- StorageTypeInstance,
6
- UniqueConstraint,
7
- } from '@prisma-next/sql-contract/types';
8
- import { decodeContractDefaults } from '@prisma-next/sql-contract/validate';
9
- import {
10
- ColumnDefaultSchema,
11
- ForeignKeySchema,
12
- IndexSchema,
13
- validateStorageSemantics,
14
- } from '@prisma-next/sql-contract/validators';
15
- import { type } from 'arktype';
16
- import type { O } from 'ts-toolbelt';
17
-
18
- /**
19
- * Structural validation schema for SqlContract using Arktype.
20
- * This validates the shape and types of the contract structure.
21
- */
22
-
23
- const StorageColumnSchema = type({
24
- nativeType: 'string',
25
- codecId: 'string',
26
- nullable: 'boolean',
27
- 'typeParams?': 'Record<string, unknown>',
28
- 'typeRef?': 'string',
29
- 'default?': ColumnDefaultSchema,
30
- });
31
-
32
- const StorageTypeInstanceSchema = type.declare<StorageTypeInstance>().type({
33
- codecId: 'string',
34
- nativeType: 'string',
35
- typeParams: 'Record<string, unknown>',
36
- });
37
-
38
- const PrimaryKeySchema = type.declare<PrimaryKey>().type({
39
- columns: type.string.array().readonly(),
40
- 'name?': 'string',
41
- });
42
-
43
- const UniqueConstraintSchema = type.declare<UniqueConstraint>().type({
44
- columns: type.string.array().readonly(),
45
- 'name?': 'string',
46
- });
47
-
48
- const StorageTableSchema = type({
49
- columns: type({ '[string]': StorageColumnSchema }),
50
- 'primaryKey?': PrimaryKeySchema,
51
- uniques: UniqueConstraintSchema.array().readonly(),
52
- indexes: IndexSchema.array().readonly(),
53
- foreignKeys: ForeignKeySchema.array().readonly(),
54
- });
55
-
56
- const StorageSchema = type({
57
- tables: type({ '[string]': StorageTableSchema }),
58
- 'types?': type({ '[string]': StorageTypeInstanceSchema }),
59
- });
60
-
61
- const ModelFieldSchema = type({
62
- 'nullable?': 'boolean',
63
- 'codecId?': 'string',
64
- });
65
-
66
- const ModelStorageFieldSchema = type({
67
- column: 'string',
68
- });
69
-
70
- const ModelStorageSchema = type({
71
- table: 'string',
72
- 'fields?': type({ '[string]': ModelStorageFieldSchema }),
73
- });
74
-
75
- const ModelSchema = type({
76
- storage: ModelStorageSchema,
77
- fields: type({ '[string]': ModelFieldSchema }),
78
- relations: type({ '[string]': 'unknown' }),
79
- 'owner?': 'string',
80
- });
81
-
82
- const GeneratorIdSchema = type('string').narrow((value, ctx) => {
83
- return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(value) ? true : ctx.mustBe('a flat generator id');
84
- });
85
-
86
- const ExecutionMutationDefaultValueSchema = type({
87
- kind: "'generator'",
88
- id: GeneratorIdSchema,
89
- 'params?': 'Record<string, unknown>',
90
- });
91
-
92
- const ExecutionMutationDefaultSchema = type({
93
- ref: {
94
- table: 'string',
95
- column: 'string',
96
- },
97
- 'onCreate?': ExecutionMutationDefaultValueSchema,
98
- 'onUpdate?': ExecutionMutationDefaultValueSchema,
99
- });
100
-
101
- const ExecutionSchema = type({
102
- mutations: {
103
- defaults: ExecutionMutationDefaultSchema.array().readonly(),
104
- },
105
- });
106
-
107
- /**
108
- * Complete SqlContract schema for structural validation.
109
- * This validates the entire contract structure at once.
110
- */
111
- const SqlContractSchema = type({
112
- 'schemaVersion?': "'1'",
113
- target: 'string',
114
- targetFamily: "'sql'",
115
- storageHash: 'string',
116
- 'executionHash?': 'string',
117
- 'profileHash?': 'string',
118
- 'capabilities?': 'Record<string, Record<string, boolean>>',
119
- 'extensionPacks?': 'Record<string, unknown>',
120
- 'meta?': 'Record<string, unknown>',
121
- 'sources?': 'Record<string, unknown>',
122
- 'roots?': 'Record<string, string>',
123
- models: type({ '[string]': ModelSchema }),
124
- storage: StorageSchema,
125
- 'execution?': ExecutionSchema,
126
- });
127
-
128
- /**
129
- * Validates the structural shape of a SqlContract using Arktype.
130
- *
131
- * **Responsibility: Validation Only**
132
- * This function validates that the contract has the correct structure and types.
133
- * It does NOT normalize the contract - normalization must happen in the contract builder.
134
- *
135
- * The contract passed to this function must already be normalized (all required fields present).
136
- * If normalization is needed, it should be done by the contract builder before calling this function.
137
- *
138
- * This ensures all required fields are present and have the correct types.
139
- *
140
- * @param value - The contract value to validate (typically from a JSON import)
141
- * @returns The validated contract if structure is valid
142
- * @throws Error if the contract structure is invalid
143
- */
144
- function validateContractStructure<T extends SqlContract<SqlStorage>>(
145
- value: unknown,
146
- ): O.Overwrite<T, { targetFamily: 'sql' }> {
147
- // Check targetFamily first to provide a clear error message for unsupported target families
148
- const rawValue = value as { targetFamily?: string };
149
- if (rawValue.targetFamily !== undefined && rawValue.targetFamily !== 'sql') {
150
- /* c8 ignore next */
151
- throw new Error(`Unsupported target family: ${rawValue.targetFamily}`);
152
- }
153
-
154
- const contractResult = SqlContractSchema(value);
155
-
156
- if (contractResult instanceof type.errors) {
157
- const messages = contractResult.map((p: { message: string }) => p.message).join('; ');
158
- throw new Error(`Contract structural validation failed: ${messages}`);
159
- }
160
-
161
- // After validation, contractResult matches the schema and preserves the input structure
162
- // TypeScript needs an assertion here due to exactOptionalPropertyTypes differences
163
- // between Arktype's inferred type and the generic T, but runtime-wise they're compatible
164
- return contractResult as O.Overwrite<T, { targetFamily: 'sql' }>;
165
- }
166
-
167
- /**
168
- * Validates logical consistency of a **structurally validated** SqlContract.
169
- * This checks that references (e.g., foreign keys, primary keys, uniques) point to storage objects that already exist.
170
- * Structural validation is expected to have already completed before this helper runs.
171
- *
172
- * Rule: keep this focused on structural consistency only; capability/feature
173
- * gating (e.g., defaults.*) belongs in migration/runtime verification, not here.
174
- *
175
- * @param structurallyValidatedContract - The contract whose structure has already been validated
176
- * @throws Error if logical validation fails
177
- */
178
- function validateContractLogic(structurallyValidatedContract: SqlContract<SqlStorage>): void {
179
- const { storage, models } = structurallyValidatedContract;
180
- const tableNames = new Set(Object.keys(storage.tables));
181
-
182
- // Validate storage.types if present
183
- if (storage.types) {
184
- for (const [typeName, typeInstance] of Object.entries(storage.types)) {
185
- // Validate typeParams is not an array (arrays are objects in JS but not valid here)
186
- if (Array.isArray(typeInstance.typeParams)) {
187
- throw new Error(
188
- `Type instance "${typeName}" has invalid typeParams: must be a plain object, not an array`,
189
- );
190
- }
191
- }
192
- }
193
-
194
- // Validate columns in all tables
195
- for (const [tableName, table] of Object.entries(storage.tables)) {
196
- for (const [columnName, column] of Object.entries(table.columns)) {
197
- // Validate typeParams and typeRef are mutually exclusive
198
- if (column.typeParams !== undefined && column.typeRef !== undefined) {
199
- throw new Error(
200
- `Column "${columnName}" in table "${tableName}" has both typeParams and typeRef; these are mutually exclusive`,
201
- );
202
- }
203
-
204
- // Validate typeParams is not an array (arrays are objects in JS but not valid here)
205
- if (column.typeParams !== undefined && Array.isArray(column.typeParams)) {
206
- throw new Error(
207
- `Column "${columnName}" in table "${tableName}" has invalid typeParams: must be a plain object, not an array`,
208
- );
209
- }
210
-
211
- // Validate NOT NULL columns do not have literal null defaults
212
- if (!column.nullable && column.default?.kind === 'literal' && column.default.value === null) {
213
- throw new Error(
214
- `Table "${tableName}" column "${columnName}" is NOT NULL but has a literal null default`,
215
- );
216
- }
217
-
218
- // Validate typeRef points to an existing storage.types key and matches codecId/nativeType
219
- if (column.typeRef !== undefined) {
220
- const referencedType = storage.types?.[column.typeRef];
221
- if (!referencedType) {
222
- throw new Error(
223
- `Column "${columnName}" in table "${tableName}" references non-existent type instance "${column.typeRef}" (not found in storage.types)`,
224
- );
225
- }
226
-
227
- if (column.codecId !== referencedType.codecId) {
228
- throw new Error(
229
- `Column "${columnName}" in table "${tableName}" has codecId "${column.codecId}" but references type instance "${column.typeRef}" with codecId "${referencedType.codecId}"`,
230
- );
231
- }
232
-
233
- if (column.nativeType !== referencedType.nativeType) {
234
- throw new Error(
235
- `Column "${columnName}" in table "${tableName}" has nativeType "${column.nativeType}" but references type instance "${column.typeRef}" with nativeType "${referencedType.nativeType}"`,
236
- );
237
- }
238
- }
239
- }
240
- }
241
-
242
- for (const [modelName, modelUnknown] of Object.entries(models)) {
243
- const model = modelUnknown as {
244
- storage?: { table?: string; fields?: Record<string, { column?: string }> };
245
- fields?: Record<string, unknown>;
246
- relations?: Record<string, unknown>;
247
- };
248
- if (!model.storage?.table) {
249
- /* c8 ignore next */
250
- throw new Error(`Model "${modelName}" is missing storage.table`);
251
- }
252
-
253
- const tableName = model.storage.table;
254
-
255
- if (!tableNames.has(tableName)) {
256
- /* c8 ignore next */
257
- throw new Error(`Model "${modelName}" references non-existent table "${tableName}"`);
258
- }
259
-
260
- const table = storage.tables[tableName];
261
- if (!table) {
262
- /* c8 ignore next */
263
- throw new Error(`Model "${modelName}" references non-existent table "${tableName}"`);
264
- }
265
-
266
- if (!table.primaryKey) {
267
- /* c8 ignore next */
268
- throw new Error(`Model "${modelName}" table "${tableName}" is missing a primary key`);
269
- }
270
-
271
- const columnNames = new Set(Object.keys(table.columns));
272
-
273
- const storageFields = model.storage.fields;
274
- if (storageFields) {
275
- for (const [fieldName, storageField] of Object.entries(storageFields)) {
276
- if (!storageField.column) {
277
- /* c8 ignore next */
278
- throw new Error(`Model "${modelName}" field "${fieldName}" is missing column property`);
279
- }
280
-
281
- if (!columnNames.has(storageField.column)) {
282
- /* c8 ignore next */
283
- throw new Error(
284
- `Model "${modelName}" field "${fieldName}" references non-existent column "${storageField.column}" in table "${tableName}"`,
285
- );
286
- }
287
- }
288
- }
289
-
290
- if (model.relations) {
291
- for (const [relationName, relation] of Object.entries(model.relations)) {
292
- if (
293
- typeof relation === 'object' &&
294
- relation !== null &&
295
- 'on' in relation &&
296
- 'to' in relation
297
- ) {
298
- const on = relation.on as { localFields?: string[]; targetFields?: string[] };
299
- const cardinality = (relation as { cardinality?: string }).cardinality;
300
- const localFields = on.localFields;
301
- const targetFields = on.targetFields;
302
- if (!localFields || !targetFields) {
303
- throw new Error(
304
- `Model "${modelName}" relation "${relationName}" uses unsupported relation format (expected localFields/targetFields)`,
305
- );
306
- }
307
- if (cardinality === '1:N') {
308
- continue;
309
- }
310
-
311
- const hasMatchingFk = table.foreignKeys?.some((fk) => {
312
- return (
313
- fk.columns.length === localFields.length &&
314
- fk.columns.every((col, i) => col === localFields[i]) &&
315
- fk.references.table &&
316
- fk.references.columns.length === targetFields.length &&
317
- fk.references.columns.every((col, i) => col === targetFields[i])
318
- );
319
- });
320
-
321
- if (!hasMatchingFk) {
322
- /* c8 ignore next */
323
- throw new Error(
324
- `Model "${modelName}" relation "${relationName}" does not have a corresponding foreign key in table "${tableName}"`,
325
- );
326
- }
327
- }
328
- }
329
- }
330
- }
331
-
332
- for (const [tableName, table] of Object.entries(storage.tables)) {
333
- const columnNames = new Set(Object.keys(table.columns));
334
-
335
- // Validate primaryKey references existing columns
336
- if (table.primaryKey) {
337
- for (const colName of table.primaryKey.columns) {
338
- if (!columnNames.has(colName)) {
339
- /* c8 ignore next */
340
- throw new Error(
341
- `Table "${tableName}" primaryKey references non-existent column "${colName}"`,
342
- );
343
- }
344
- }
345
- }
346
-
347
- // Validate unique constraints reference existing columns
348
- for (const unique of table.uniques) {
349
- for (const colName of unique.columns) {
350
- if (!columnNames.has(colName)) {
351
- /* c8 ignore next */
352
- throw new Error(
353
- `Table "${tableName}" unique constraint references non-existent column "${colName}"`,
354
- );
355
- }
356
- }
357
- }
358
-
359
- // Validate indexes reference existing columns
360
- for (const index of table.indexes) {
361
- for (const colName of index.columns) {
362
- if (!columnNames.has(colName)) {
363
- /* c8 ignore next */
364
- throw new Error(`Table "${tableName}" index references non-existent column "${colName}"`);
365
- }
366
- }
367
- }
368
-
369
- // Validate foreignKeys reference existing tables and columns
370
- for (const fk of table.foreignKeys) {
371
- // Validate FK columns exist in the referencing table
372
- for (const colName of fk.columns) {
373
- if (!columnNames.has(colName)) {
374
- /* c8 ignore next */
375
- throw new Error(
376
- `Table "${tableName}" foreignKey references non-existent column "${colName}"`,
377
- );
378
- }
379
- }
380
-
381
- // Validate referenced table exists
382
- if (!tableNames.has(fk.references.table)) {
383
- /* c8 ignore next */
384
- throw new Error(
385
- `Table "${tableName}" foreignKey references non-existent table "${fk.references.table}"`,
386
- );
387
- }
388
-
389
- // Validate referenced columns exist in the referenced table
390
- const referencedTable = storage.tables[fk.references.table];
391
- if (!referencedTable) {
392
- /* c8 ignore next */
393
- throw new Error(
394
- `Table "${tableName}" foreignKey references non-existent table "${fk.references.table}"`,
395
- );
396
- }
397
- const referencedColumnNames = new Set(Object.keys(referencedTable.columns));
398
-
399
- for (const colName of fk.references.columns) {
400
- if (!referencedColumnNames.has(colName)) {
401
- /* c8 ignore next */
402
- throw new Error(
403
- `Table "${tableName}" foreignKey references non-existent column "${colName}" in table "${fk.references.table}"`,
404
- );
405
- }
406
- }
407
-
408
- if (fk.columns.length !== fk.references.columns.length) {
409
- /* c8 ignore next */
410
- throw new Error(
411
- `Table "${tableName}" foreignKey column count (${fk.columns.length}) does not match referenced column count (${fk.references.columns.length})`,
412
- );
413
- }
414
- }
415
- }
416
- }
417
-
418
- import { normalizeContract } from '@prisma-next/sql-contract/validate';
419
- export { normalizeContract };
420
-
421
- /**
422
- * Validates that a JSON import conforms to the SqlContract structure
423
- * and returns a fully typed SqlContract.
424
- *
425
- * This function is specifically for validating JSON imports (e.g., from contract.json).
426
- * Contracts created via the builder API (defineContract) are already valid and should
427
- * not be passed to this function - use them directly without validation.
428
- *
429
- * Performs both structural validation (using Arktype) and logical validation
430
- * (ensuring all references are valid).
431
- *
432
- *
433
- * The type parameter `TContract` must be a fully-typed contract type (e.g., from `contract.d.ts`),
434
- * NOT a generic `SqlContract<SqlStorage>`.
435
- *
436
- * **Correct:**
437
- * ```typescript
438
- * import type { Contract } from './contract.d';
439
- * const contract = validateContract<Contract>(contractJson);
440
- * ```
441
- *
442
- * **Incorrect:**
443
- * ```typescript
444
- * import type { SqlContract, SqlStorage } from '@prisma-next/sql-contract/types';
445
- * const contract = validateContract<SqlContract<SqlStorage>>(contractJson);
446
- * // ❌ Types will be inferred as 'unknown' - this won't work!
447
- * ```
448
- *
449
- * The type parameter provides the specific table structure, column types, and model definitions.
450
- * This function validates the runtime structure matches the type, but does not infer types
451
- * from JSON (as JSON imports lose literal type information).
452
- *
453
- * @param value - The contract value to validate (must be from a JSON import, not a builder)
454
- * @returns A validated contract matching the TContract type
455
- * @throws Error if the contract structure or logic is invalid
456
- */
457
- export function validateContract<TContract extends SqlContract<SqlStorage>>(
458
- value: unknown,
459
- ): TContract {
460
- // Normalize contract first (add defaults for missing fields)
461
- const normalized = normalizeContract(value);
462
-
463
- const structurallyValid = validateContractStructure<SqlContract<SqlStorage>>(normalized);
464
-
465
- const contractForValidation = structurallyValid as SqlContract<SqlStorage>;
466
-
467
- validateContractLogic(contractForValidation);
468
-
469
- const semanticErrors = validateStorageSemantics(contractForValidation.storage);
470
- if (semanticErrors.length > 0) {
471
- throw new Error(`Contract semantic validation failed: ${semanticErrors.join('; ')}`);
472
- }
473
-
474
- return decodeContractDefaults(contractForValidation) as TContract;
475
- }