@prisma-next/sql-contract-ts 0.3.0-dev.1 → 0.3.0-dev.11

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.
@@ -0,0 +1,561 @@
1
+ import type { ExtensionPackRef, TargetPackRef } from '@prisma-next/contract/framework-components';
2
+ import type {
3
+ ColumnBuilderState,
4
+ ModelBuilderState,
5
+ RelationDefinition,
6
+ TableBuilderState,
7
+ } from '@prisma-next/contract-authoring';
8
+ import {
9
+ type BuildModels,
10
+ type BuildRelations,
11
+ type BuildStorageColumn,
12
+ ContractBuilder,
13
+ type ExtractColumns,
14
+ type ExtractPrimaryKey,
15
+ ModelBuilder,
16
+ type Mutable,
17
+ TableBuilder,
18
+ } from '@prisma-next/contract-authoring';
19
+ import type {
20
+ ModelDefinition,
21
+ ModelField,
22
+ SqlContract,
23
+ SqlMappings,
24
+ SqlStorage,
25
+ } from '@prisma-next/sql-contract/types';
26
+ import { computeMappings } from './contract';
27
+
28
+ /**
29
+ * Type-level mappings structure for contracts built via `defineContract()`.
30
+ *
31
+ * Compile-time type helper (not a runtime object) that ensures mappings match what the builder
32
+ * produces. `codecTypes` uses the generic `CodecTypes` parameter; `operationTypes` is always
33
+ * empty since operations are added via extensions at runtime.
34
+ *
35
+ * **Difference from RuntimeContext**: This is a compile-time type for contract construction.
36
+ * `RuntimeContext` is a runtime object with populated registries for query execution.
37
+ *
38
+ * @template C - The `CodecTypes` generic parameter passed to `defineContract<CodecTypes>()`
39
+ */
40
+ type ContractBuilderMappings<C extends Record<string, { output: unknown }>> = Omit<
41
+ SqlMappings,
42
+ 'codecTypes' | 'operationTypes'
43
+ > & {
44
+ readonly codecTypes: C;
45
+ readonly operationTypes: Record<string, never>;
46
+ };
47
+
48
+ type BuildStorageTable<
49
+ _TableName extends string,
50
+ Columns extends Record<string, ColumnBuilderState<string, boolean, string>>,
51
+ PK extends readonly string[] | undefined,
52
+ > = {
53
+ readonly columns: {
54
+ readonly [K in keyof Columns]: Columns[K] extends ColumnBuilderState<
55
+ string,
56
+ infer Null,
57
+ infer TType
58
+ >
59
+ ? BuildStorageColumn<Null & boolean, TType>
60
+ : never;
61
+ };
62
+ readonly uniques: ReadonlyArray<never>;
63
+ readonly indexes: ReadonlyArray<never>;
64
+ readonly foreignKeys: ReadonlyArray<never>;
65
+ } & (PK extends readonly string[]
66
+ ? { readonly primaryKey: { readonly columns: PK } }
67
+ : Record<string, never>);
68
+
69
+ type BuildStorage<
70
+ Tables extends Record<
71
+ string,
72
+ TableBuilderState<
73
+ string,
74
+ Record<string, ColumnBuilderState<string, boolean, string>>,
75
+ readonly string[] | undefined
76
+ >
77
+ >,
78
+ > = {
79
+ readonly tables: {
80
+ readonly [K in keyof Tables]: BuildStorageTable<
81
+ K & string,
82
+ ExtractColumns<Tables[K]>,
83
+ ExtractPrimaryKey<Tables[K]>
84
+ >;
85
+ };
86
+ };
87
+
88
+ type BuildStorageTables<
89
+ Tables extends Record<
90
+ string,
91
+ TableBuilderState<
92
+ string,
93
+ Record<string, ColumnBuilderState<string, boolean, string>>,
94
+ readonly string[] | undefined
95
+ >
96
+ >,
97
+ > = {
98
+ readonly [K in keyof Tables]: BuildStorageTable<
99
+ K & string,
100
+ ExtractColumns<Tables[K]>,
101
+ ExtractPrimaryKey<Tables[K]>
102
+ >;
103
+ };
104
+
105
+ export interface ColumnBuilder<Name extends string, Nullable extends boolean, Type extends string> {
106
+ nullable<Value extends boolean>(value?: Value): ColumnBuilder<Name, Value, Type>;
107
+ type<Id extends string>(id: Id): ColumnBuilder<Name, Nullable, Id>;
108
+ build(): ColumnBuilderState<Name, Nullable, Type>;
109
+ }
110
+
111
+ class SqlContractBuilder<
112
+ CodecTypes extends Record<string, { output: unknown }> = Record<string, never>,
113
+ Target extends string | undefined = undefined,
114
+ Tables extends Record<
115
+ string,
116
+ TableBuilderState<
117
+ string,
118
+ Record<string, ColumnBuilderState<string, boolean, string>>,
119
+ readonly string[] | undefined
120
+ >
121
+ > = Record<never, never>,
122
+ Models extends Record<
123
+ string,
124
+ ModelBuilderState<string, string, Record<string, string>, Record<string, RelationDefinition>>
125
+ > = Record<never, never>,
126
+ CoreHash extends string | undefined = undefined,
127
+ ExtensionPacks extends Record<string, unknown> | undefined = undefined,
128
+ Capabilities extends Record<string, Record<string, boolean>> | undefined = undefined,
129
+ > extends ContractBuilder<Target, Tables, Models, CoreHash, ExtensionPacks, Capabilities> {
130
+ /**
131
+ * This method is responsible for normalizing the contract IR by setting default values
132
+ * for all required fields:
133
+ * - `nullable`: defaults to `false` if not provided
134
+ * - `uniques`: defaults to `[]` (empty array)
135
+ * - `indexes`: defaults to `[]` (empty array)
136
+ * - `foreignKeys`: defaults to `[]` (empty array)
137
+ * - `relations`: defaults to `{}` (empty object) for both model-level and contract-level
138
+ * - `nativeType`: required field set from column type descriptor when columns are defined
139
+ *
140
+ * The contract builder is the **only** place where normalization should occur.
141
+ * Validators, parsers, and emitters should assume the contract is already normalized.
142
+ *
143
+ * **Required**: Use column type descriptors (e.g., `int4Column`, `textColumn`) when defining columns.
144
+ * This ensures `nativeType` is set correctly at build time.
145
+ *
146
+ * @returns A normalized SqlContract with all required fields present
147
+ */
148
+ build(): Target extends string
149
+ ? SqlContract<
150
+ BuildStorage<Tables>,
151
+ BuildModels<Models>,
152
+ BuildRelations<Models>,
153
+ ContractBuilderMappings<CodecTypes>
154
+ > & {
155
+ readonly schemaVersion: '1';
156
+ readonly target: Target;
157
+ readonly targetFamily: 'sql';
158
+ readonly coreHash: CoreHash extends string ? CoreHash : string;
159
+ } & (ExtensionPacks extends Record<string, unknown>
160
+ ? { readonly extensionPacks: ExtensionPacks }
161
+ : Record<string, never>) &
162
+ (Capabilities extends Record<string, Record<string, boolean>>
163
+ ? { readonly capabilities: Capabilities }
164
+ : Record<string, never>)
165
+ : never {
166
+ // Type helper to ensure literal types are preserved in return type
167
+ type BuiltContract = Target extends string
168
+ ? SqlContract<
169
+ BuildStorage<Tables>,
170
+ BuildModels<Models>,
171
+ BuildRelations<Models>,
172
+ ContractBuilderMappings<CodecTypes>
173
+ > & {
174
+ readonly schemaVersion: '1';
175
+ readonly target: Target;
176
+ readonly targetFamily: 'sql';
177
+ readonly coreHash: CoreHash extends string ? CoreHash : string;
178
+ } & (ExtensionPacks extends Record<string, unknown>
179
+ ? { readonly extensionPacks: ExtensionPacks }
180
+ : Record<string, never>) &
181
+ (Capabilities extends Record<string, Record<string, boolean>>
182
+ ? { readonly capabilities: Capabilities }
183
+ : Record<string, never>)
184
+ : never;
185
+ if (!this.state.target) {
186
+ throw new Error('target is required. Call .target() before .build()');
187
+ }
188
+
189
+ const target = this.state.target as Target & string;
190
+
191
+ const storageTables = {} as Partial<Mutable<BuildStorageTables<Tables>>>;
192
+
193
+ for (const tableName of Object.keys(this.state.tables) as Array<keyof Tables & string>) {
194
+ const tableState = this.state.tables[tableName];
195
+ if (!tableState) continue;
196
+
197
+ type TableKey = typeof tableName;
198
+ type ColumnDefs = ExtractColumns<Tables[TableKey]>;
199
+ type PrimaryKey = ExtractPrimaryKey<Tables[TableKey]>;
200
+
201
+ const columns = {} as Partial<{
202
+ [K in keyof ColumnDefs]: BuildStorageColumn<
203
+ ColumnDefs[K]['nullable'] & boolean,
204
+ ColumnDefs[K]['type']
205
+ >;
206
+ }>;
207
+
208
+ for (const columnName in tableState.columns) {
209
+ const columnState = tableState.columns[columnName];
210
+ if (!columnState) continue;
211
+ const codecId = columnState.type;
212
+ const nativeType = columnState.nativeType;
213
+
214
+ columns[columnName as keyof ColumnDefs] = {
215
+ nativeType,
216
+ codecId,
217
+ nullable: (columnState.nullable ?? false) as ColumnDefs[keyof ColumnDefs]['nullable'] &
218
+ boolean,
219
+ } as BuildStorageColumn<
220
+ ColumnDefs[keyof ColumnDefs]['nullable'] & boolean,
221
+ ColumnDefs[keyof ColumnDefs]['type']
222
+ >;
223
+ }
224
+
225
+ const table = {
226
+ columns: columns as {
227
+ [K in keyof ColumnDefs]: BuildStorageColumn<
228
+ ColumnDefs[K]['nullable'] & boolean,
229
+ ColumnDefs[K]['type']
230
+ >;
231
+ },
232
+ uniques: [],
233
+ indexes: [],
234
+ foreignKeys: [],
235
+ ...(tableState.primaryKey
236
+ ? {
237
+ primaryKey: {
238
+ columns: tableState.primaryKey,
239
+ },
240
+ }
241
+ : {}),
242
+ } as unknown as BuildStorageTable<TableKey & string, ColumnDefs, PrimaryKey>;
243
+
244
+ (storageTables as Mutable<BuildStorageTables<Tables>>)[tableName] = table;
245
+ }
246
+
247
+ const storage = { tables: storageTables as BuildStorageTables<Tables> } as BuildStorage<Tables>;
248
+
249
+ // Build models - construct as partial first, then assert full type
250
+ const modelsPartial: Partial<BuildModels<Models>> = {};
251
+
252
+ // Iterate over models - TypeScript will see keys as string, but type assertion preserves literals
253
+ for (const modelName in this.state.models) {
254
+ const modelState = this.state.models[modelName];
255
+ if (!modelState) continue;
256
+
257
+ const modelStateTyped = modelState as unknown as {
258
+ name: string;
259
+ table: string;
260
+ fields: Record<string, string>;
261
+ };
262
+
263
+ // Build fields object
264
+ const fields: Partial<Record<string, ModelField>> = {};
265
+
266
+ // Iterate over fields
267
+ for (const fieldName in modelStateTyped.fields) {
268
+ const columnName = modelStateTyped.fields[fieldName];
269
+ if (columnName) {
270
+ fields[fieldName] = {
271
+ column: columnName,
272
+ };
273
+ }
274
+ }
275
+
276
+ // Assign to models - type assertion preserves literal keys
277
+ (modelsPartial as unknown as Record<string, ModelDefinition>)[modelName] = {
278
+ storage: {
279
+ table: modelStateTyped.table,
280
+ },
281
+ fields: fields as Record<string, ModelField>,
282
+ relations: {},
283
+ };
284
+ }
285
+
286
+ // Build relations object - organized by table name
287
+ const relationsPartial: Partial<Record<string, Record<string, RelationDefinition>>> = {};
288
+
289
+ // Iterate over models to collect relations
290
+ for (const modelName in this.state.models) {
291
+ const modelState = this.state.models[modelName];
292
+ if (!modelState) continue;
293
+
294
+ const modelStateTyped = modelState as unknown as {
295
+ name: string;
296
+ table: string;
297
+ fields: Record<string, string>;
298
+ relations: Record<string, RelationDefinition>;
299
+ };
300
+
301
+ const tableName = modelStateTyped.table;
302
+ if (!tableName) continue;
303
+
304
+ // Only initialize relations object for this table if it has relations
305
+ if (modelStateTyped.relations && Object.keys(modelStateTyped.relations).length > 0) {
306
+ if (!relationsPartial[tableName]) {
307
+ relationsPartial[tableName] = {};
308
+ }
309
+
310
+ // Add relations from this model to the table's relations
311
+ const tableRelations = relationsPartial[tableName];
312
+ if (tableRelations) {
313
+ for (const relationName in modelStateTyped.relations) {
314
+ const relation = modelStateTyped.relations[relationName];
315
+ if (relation) {
316
+ tableRelations[relationName] = relation;
317
+ }
318
+ }
319
+ }
320
+ }
321
+ }
322
+
323
+ const models = modelsPartial as unknown as BuildModels<Models>;
324
+
325
+ const baseMappings = computeMappings(
326
+ models as unknown as Record<string, ModelDefinition>,
327
+ storage as SqlStorage,
328
+ );
329
+
330
+ const mappings = {
331
+ ...baseMappings,
332
+ codecTypes: {} as CodecTypes,
333
+ operationTypes: {} as Record<string, never>,
334
+ } as ContractBuilderMappings<CodecTypes>;
335
+
336
+ const extensionNamespaces = this.state.extensionNamespaces ?? [];
337
+ const extensionPacks: Record<string, unknown> = { ...(this.state.extensionPacks || {}) };
338
+ for (const namespace of extensionNamespaces) {
339
+ if (!Object.hasOwn(extensionPacks, namespace)) {
340
+ extensionPacks[namespace] = {};
341
+ }
342
+ }
343
+
344
+ // Construct contract with explicit type that matches the generic parameters
345
+ // This ensures TypeScript infers literal types from the generics, not runtime values
346
+ // Always include relations, even if empty (normalized to empty object)
347
+ const contract = {
348
+ schemaVersion: '1' as const,
349
+ target,
350
+ targetFamily: 'sql' as const,
351
+ coreHash: this.state.coreHash || 'sha256:ts-builder-placeholder',
352
+ models,
353
+ relations: relationsPartial,
354
+ storage,
355
+ mappings,
356
+ extensionPacks,
357
+ capabilities: this.state.capabilities || {},
358
+ meta: {},
359
+ sources: {},
360
+ } as unknown as BuiltContract;
361
+
362
+ return contract as unknown as ReturnType<
363
+ SqlContractBuilder<
364
+ CodecTypes,
365
+ Target,
366
+ Tables,
367
+ Models,
368
+ CoreHash,
369
+ ExtensionPacks,
370
+ Capabilities
371
+ >['build']
372
+ >;
373
+ }
374
+
375
+ override target<T extends string>(
376
+ packRef: TargetPackRef<'sql', T>,
377
+ ): SqlContractBuilder<CodecTypes, T, Tables, Models, CoreHash, ExtensionPacks, Capabilities> {
378
+ return new SqlContractBuilder<
379
+ CodecTypes,
380
+ T,
381
+ Tables,
382
+ Models,
383
+ CoreHash,
384
+ ExtensionPacks,
385
+ Capabilities
386
+ >({
387
+ ...this.state,
388
+ target: packRef.targetId,
389
+ });
390
+ }
391
+
392
+ extensionPacks(
393
+ packs: Record<string, ExtensionPackRef<'sql', string>>,
394
+ ): SqlContractBuilder<
395
+ CodecTypes,
396
+ Target,
397
+ Tables,
398
+ Models,
399
+ CoreHash,
400
+ ExtensionPacks,
401
+ Capabilities
402
+ > {
403
+ if (!this.state.target) {
404
+ throw new Error('extensionPacks() requires target() to be called first');
405
+ }
406
+
407
+ const namespaces = new Set(this.state.extensionNamespaces ?? []);
408
+
409
+ for (const packRef of Object.values(packs)) {
410
+ if (!packRef) continue;
411
+
412
+ if (packRef.kind !== 'extension') {
413
+ throw new Error(
414
+ `extensionPacks() only accepts extension pack refs. Received kind \"${packRef.kind}\".`,
415
+ );
416
+ }
417
+
418
+ if (packRef.familyId !== 'sql') {
419
+ throw new Error(
420
+ `extension pack \"${packRef.id}\" targets family \"${packRef.familyId}\" but this builder targets \"sql\".`,
421
+ );
422
+ }
423
+
424
+ if (packRef.targetId && packRef.targetId !== this.state.target) {
425
+ throw new Error(
426
+ `extension pack \"${packRef.id}\" targets \"${packRef.targetId}\" but builder target is \"${this.state.target}\".`,
427
+ );
428
+ }
429
+
430
+ namespaces.add(packRef.id);
431
+ }
432
+
433
+ return new SqlContractBuilder<
434
+ CodecTypes,
435
+ Target,
436
+ Tables,
437
+ Models,
438
+ CoreHash,
439
+ ExtensionPacks,
440
+ Capabilities
441
+ >({
442
+ ...this.state,
443
+ extensionNamespaces: [...namespaces],
444
+ });
445
+ }
446
+
447
+ override capabilities<C extends Record<string, Record<string, boolean>>>(
448
+ capabilities: C,
449
+ ): SqlContractBuilder<CodecTypes, Target, Tables, Models, CoreHash, ExtensionPacks, C> {
450
+ return new SqlContractBuilder<CodecTypes, Target, Tables, Models, CoreHash, ExtensionPacks, C>({
451
+ ...this.state,
452
+ capabilities,
453
+ });
454
+ }
455
+
456
+ override coreHash<H extends string>(
457
+ hash: H,
458
+ ): SqlContractBuilder<CodecTypes, Target, Tables, Models, H, ExtensionPacks, Capabilities> {
459
+ return new SqlContractBuilder<
460
+ CodecTypes,
461
+ Target,
462
+ Tables,
463
+ Models,
464
+ H,
465
+ ExtensionPacks,
466
+ Capabilities
467
+ >({
468
+ ...this.state,
469
+ coreHash: hash,
470
+ });
471
+ }
472
+
473
+ override table<
474
+ TableName extends string,
475
+ T extends TableBuilder<
476
+ TableName,
477
+ Record<string, ColumnBuilderState<string, boolean, string>>,
478
+ readonly string[] | undefined
479
+ >,
480
+ >(
481
+ name: TableName,
482
+ callback: (t: TableBuilder<TableName>) => T | undefined,
483
+ ): SqlContractBuilder<
484
+ CodecTypes,
485
+ Target,
486
+ Tables & Record<TableName, ReturnType<T['build']>>,
487
+ Models,
488
+ CoreHash,
489
+ ExtensionPacks,
490
+ Capabilities
491
+ > {
492
+ const tableBuilder = new TableBuilder<TableName>(name);
493
+ const result = callback(tableBuilder);
494
+ const finalBuilder = result instanceof TableBuilder ? result : tableBuilder;
495
+ const tableState = finalBuilder.build();
496
+
497
+ return new SqlContractBuilder<
498
+ CodecTypes,
499
+ Target,
500
+ Tables & Record<TableName, ReturnType<T['build']>>,
501
+ Models,
502
+ CoreHash,
503
+ ExtensionPacks,
504
+ Capabilities
505
+ >({
506
+ ...this.state,
507
+ tables: { ...this.state.tables, [name]: tableState } as Tables &
508
+ Record<TableName, ReturnType<T['build']>>,
509
+ });
510
+ }
511
+
512
+ override model<
513
+ ModelName extends string,
514
+ TableName extends string,
515
+ M extends ModelBuilder<
516
+ ModelName,
517
+ TableName,
518
+ Record<string, string>,
519
+ Record<string, RelationDefinition>
520
+ >,
521
+ >(
522
+ name: ModelName,
523
+ table: TableName,
524
+ callback: (
525
+ m: ModelBuilder<ModelName, TableName, Record<string, string>, Record<never, never>>,
526
+ ) => M | undefined,
527
+ ): SqlContractBuilder<
528
+ CodecTypes,
529
+ Target,
530
+ Tables,
531
+ Models & Record<ModelName, ReturnType<M['build']>>,
532
+ CoreHash,
533
+ ExtensionPacks,
534
+ Capabilities
535
+ > {
536
+ const modelBuilder = new ModelBuilder<ModelName, TableName>(name, table);
537
+ const result = callback(modelBuilder);
538
+ const finalBuilder = result instanceof ModelBuilder ? result : modelBuilder;
539
+ const modelState = finalBuilder.build();
540
+
541
+ return new SqlContractBuilder<
542
+ CodecTypes,
543
+ Target,
544
+ Tables,
545
+ Models & Record<ModelName, ReturnType<M['build']>>,
546
+ CoreHash,
547
+ ExtensionPacks,
548
+ Capabilities
549
+ >({
550
+ ...this.state,
551
+ models: { ...this.state.models, [name]: modelState } as Models &
552
+ Record<ModelName, ReturnType<M['build']>>,
553
+ });
554
+ }
555
+ }
556
+
557
+ export function defineContract<
558
+ CodecTypes extends Record<string, { output: unknown }> = Record<string, never>,
559
+ >(): SqlContractBuilder<CodecTypes> {
560
+ return new SqlContractBuilder<CodecTypes>();
561
+ }