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

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,915 @@
1
+ import type { ExtensionPackRef, TargetPackRef } from '@prisma-next/contract/framework-components';
2
+ import type {
3
+ ColumnDefault,
4
+ ColumnDefaultLiteralInputValue,
5
+ ColumnDefaultLiteralValue,
6
+ ExecutionMutationDefault,
7
+ ExecutionMutationDefaultValue,
8
+ TaggedRaw,
9
+ } from '@prisma-next/contract/types';
10
+ import type {
11
+ ColumnBuilderState,
12
+ ColumnTypeDescriptor,
13
+ ContractBuilderState,
14
+ ForeignKeyDefaultsState,
15
+ ModelBuilderState,
16
+ RelationDefinition,
17
+ TableBuilderState,
18
+ } from '@prisma-next/contract-authoring';
19
+ import {
20
+ type BuildModels,
21
+ type BuildRelations,
22
+ type BuildStorageColumn,
23
+ ContractBuilder,
24
+ createTable,
25
+ type ExtractColumns,
26
+ type ExtractPrimaryKey,
27
+ ModelBuilder,
28
+ type Mutable,
29
+ TableBuilder,
30
+ } from '@prisma-next/contract-authoring';
31
+ import {
32
+ applyFkDefaults,
33
+ type ContractWithTypeMaps,
34
+ type Index,
35
+ type ModelDefinition,
36
+ type ModelField,
37
+ type ReferentialAction,
38
+ type SqlContract,
39
+ type SqlMappings,
40
+ type SqlStorage,
41
+ type StorageTypeInstance,
42
+ type TypeMaps,
43
+ } from '@prisma-next/sql-contract/types';
44
+ import { ifDefined } from '@prisma-next/utils/defined';
45
+ import { computeMappings } from './contract';
46
+
47
+ type ColumnDefaultForCodec<
48
+ CodecTypes extends Record<string, { output: unknown }>,
49
+ CodecId extends string,
50
+ > =
51
+ | {
52
+ readonly kind: 'literal';
53
+ readonly value: CodecId extends keyof CodecTypes ? CodecTypes[CodecId]['output'] : unknown;
54
+ }
55
+ | { readonly kind: 'function'; readonly expression: string };
56
+
57
+ type SqlNullableColumnOptions<
58
+ Descriptor extends ColumnTypeDescriptor,
59
+ CodecTypes extends Record<string, { output: unknown }>,
60
+ > = {
61
+ readonly type: Descriptor;
62
+ readonly nullable: true;
63
+ readonly typeParams?: Record<string, unknown>;
64
+ readonly default?: ColumnDefaultForCodec<CodecTypes, Descriptor['codecId']>;
65
+ };
66
+
67
+ type SqlNonNullableColumnOptions<
68
+ Descriptor extends ColumnTypeDescriptor,
69
+ CodecTypes extends Record<string, { output: unknown }>,
70
+ > = {
71
+ readonly type: Descriptor;
72
+ readonly nullable?: false;
73
+ readonly typeParams?: Record<string, unknown>;
74
+ readonly default?: ColumnDefaultForCodec<CodecTypes, Descriptor['codecId']>;
75
+ };
76
+
77
+ type SqlGeneratedColumnOptions<
78
+ Descriptor extends ColumnTypeDescriptor,
79
+ CodecTypes extends Record<string, { output: unknown }>,
80
+ > = Omit<SqlNonNullableColumnOptions<Descriptor, CodecTypes>, 'default' | 'nullable'> & {
81
+ readonly nullable?: false;
82
+ readonly generated: ExecutionMutationDefaultValue;
83
+ };
84
+
85
+ type SqlColumnOptions<
86
+ Descriptor extends ColumnTypeDescriptor,
87
+ CodecTypes extends Record<string, { output: unknown }>,
88
+ > =
89
+ | SqlNullableColumnOptions<Descriptor, CodecTypes>
90
+ | SqlNonNullableColumnOptions<Descriptor, CodecTypes>;
91
+
92
+ export interface SqlTableBuilder<
93
+ Name extends string,
94
+ CodecTypes extends Record<string, { output: unknown }>,
95
+ Columns extends Record<string, ColumnBuilderState<string, boolean, string>> = Record<
96
+ never,
97
+ ColumnBuilderState<string, boolean, string>
98
+ >,
99
+ PrimaryKey extends readonly string[] | undefined = undefined,
100
+ > extends Omit<TableBuilder<Name, Columns, PrimaryKey>, 'column' | 'generated'> {
101
+ column<ColName extends string, Descriptor extends ColumnTypeDescriptor>(
102
+ name: ColName,
103
+ options: SqlNullableColumnOptions<Descriptor, CodecTypes>,
104
+ ): TableBuilder<
105
+ Name,
106
+ Columns & Record<ColName, ColumnBuilderState<ColName, true, Descriptor['codecId']>>,
107
+ PrimaryKey
108
+ >;
109
+ column<ColName extends string, Descriptor extends ColumnTypeDescriptor>(
110
+ name: ColName,
111
+ options: SqlNonNullableColumnOptions<Descriptor, CodecTypes>,
112
+ ): TableBuilder<
113
+ Name,
114
+ Columns & Record<ColName, ColumnBuilderState<ColName, false, Descriptor['codecId']>>,
115
+ PrimaryKey
116
+ >;
117
+ column<ColName extends string, Descriptor extends ColumnTypeDescriptor>(
118
+ name: ColName,
119
+ options: SqlColumnOptions<Descriptor, CodecTypes>,
120
+ ): TableBuilder<
121
+ Name,
122
+ Columns & Record<ColName, ColumnBuilderState<ColName, boolean, Descriptor['codecId']>>,
123
+ PrimaryKey
124
+ >;
125
+ generated<ColName extends string, Descriptor extends ColumnTypeDescriptor>(
126
+ name: ColName,
127
+ options: SqlGeneratedColumnOptions<Descriptor, CodecTypes>,
128
+ ): TableBuilder<
129
+ Name,
130
+ Columns & Record<ColName, ColumnBuilderState<ColName, false, Descriptor['codecId']>>,
131
+ PrimaryKey
132
+ >;
133
+ }
134
+
135
+ type ContractBuilderMappings = SqlMappings;
136
+
137
+ type ExtractCodecTypesFromPack<P> = P extends { __codecTypes?: infer C }
138
+ ? C extends Record<string, { output: unknown }>
139
+ ? C
140
+ : Record<string, never>
141
+ : Record<string, never>;
142
+
143
+ type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (
144
+ k: infer I,
145
+ ) => void
146
+ ? I
147
+ : never;
148
+
149
+ type MergeExtensionCodecTypes<Packs extends Record<string, unknown>> = UnionToIntersection<
150
+ {
151
+ [K in keyof Packs]: ExtractCodecTypesFromPack<Packs[K]>;
152
+ }[keyof Packs]
153
+ >;
154
+
155
+ type BuildStorageTable<
156
+ _TableName extends string,
157
+ Columns extends Record<string, ColumnBuilderState<string, boolean, string>>,
158
+ PK extends readonly string[] | undefined,
159
+ > = {
160
+ readonly columns: {
161
+ readonly [K in keyof Columns]: Columns[K] extends ColumnBuilderState<
162
+ string,
163
+ infer Null,
164
+ infer TType
165
+ >
166
+ ? BuildStorageColumn<Null & boolean, TType>
167
+ : never;
168
+ };
169
+ readonly uniques: ReadonlyArray<{ readonly columns: readonly string[]; readonly name?: string }>;
170
+ readonly indexes: ReadonlyArray<Index>;
171
+ readonly foreignKeys: ReadonlyArray<{
172
+ readonly columns: readonly string[];
173
+ readonly references: { readonly table: string; readonly columns: readonly string[] };
174
+ readonly name?: string;
175
+ readonly onDelete?: ReferentialAction;
176
+ readonly onUpdate?: ReferentialAction;
177
+ readonly constraint: boolean;
178
+ readonly index: boolean;
179
+ }>;
180
+ } & (PK extends readonly string[]
181
+ ? { readonly primaryKey: { readonly columns: PK; readonly name?: string } }
182
+ : Record<string, never>);
183
+
184
+ type BuildStorage<
185
+ Tables extends Record<
186
+ string,
187
+ TableBuilderState<
188
+ string,
189
+ Record<string, ColumnBuilderState<string, boolean, string>>,
190
+ readonly string[] | undefined
191
+ >
192
+ >,
193
+ Types extends Record<string, StorageTypeInstance>,
194
+ > = {
195
+ readonly tables: {
196
+ readonly [K in keyof Tables]: BuildStorageTable<
197
+ K & string,
198
+ ExtractColumns<Tables[K]>,
199
+ ExtractPrimaryKey<Tables[K]>
200
+ >;
201
+ };
202
+ readonly types: Types;
203
+ };
204
+
205
+ type BuildStorageTables<
206
+ Tables extends Record<
207
+ string,
208
+ TableBuilderState<
209
+ string,
210
+ Record<string, ColumnBuilderState<string, boolean, string>>,
211
+ readonly string[] | undefined
212
+ >
213
+ >,
214
+ > = {
215
+ readonly [K in keyof Tables]: BuildStorageTable<
216
+ K & string,
217
+ ExtractColumns<Tables[K]>,
218
+ ExtractPrimaryKey<Tables[K]>
219
+ >;
220
+ };
221
+
222
+ export interface ColumnBuilder<Name extends string, Nullable extends boolean, Type extends string> {
223
+ nullable<Value extends boolean>(value?: Value): ColumnBuilder<Name, Value, Type>;
224
+ type<Id extends string>(id: Id): ColumnBuilder<Name, Nullable, Id>;
225
+ build(): ColumnBuilderState<Name, Nullable, Type>;
226
+ }
227
+
228
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
229
+ if (typeof value !== 'object' || value === null) return false;
230
+ const proto = Object.getPrototypeOf(value);
231
+ return proto === Object.prototype || proto === null;
232
+ }
233
+
234
+ function isJsonValue(value: unknown): value is ColumnDefaultLiteralValue {
235
+ if (value === null) return true;
236
+ const valueType = typeof value;
237
+ if (valueType === 'string' || valueType === 'number' || valueType === 'boolean') return true;
238
+ if (Array.isArray(value)) {
239
+ return value.every((item) => isJsonValue(item));
240
+ }
241
+ if (isPlainObject(value)) {
242
+ return Object.values(value).every((item) => isJsonValue(item));
243
+ }
244
+ return false;
245
+ }
246
+
247
+ function encodeDefaultLiteralValue(
248
+ value: ColumnDefaultLiteralInputValue,
249
+ ): ColumnDefaultLiteralValue {
250
+ if (typeof value === 'bigint') {
251
+ return { $type: 'bigint', value: value.toString() };
252
+ }
253
+ if (value instanceof Date) {
254
+ return value.toISOString();
255
+ }
256
+ if (isJsonValue(value)) {
257
+ if (isPlainObject(value) && '$type' in value) {
258
+ return { $type: 'raw', value } satisfies TaggedRaw;
259
+ }
260
+ return value;
261
+ }
262
+ throw new Error(
263
+ 'Unsupported column default literal value: expected JSON-safe value, bigint, or Date.',
264
+ );
265
+ }
266
+
267
+ function encodeColumnDefault(defaultInput: ColumnDefault): ColumnDefault {
268
+ if (defaultInput.kind === 'function') {
269
+ return { kind: 'function', expression: defaultInput.expression };
270
+ }
271
+ return { kind: 'literal', value: encodeDefaultLiteralValue(defaultInput.value) };
272
+ }
273
+
274
+ class SqlContractBuilder<
275
+ CodecTypes extends Record<string, { output: unknown }> = Record<string, never>,
276
+ Target extends string | undefined = undefined,
277
+ Tables extends Record<
278
+ string,
279
+ TableBuilderState<
280
+ string,
281
+ Record<string, ColumnBuilderState<string, boolean, string>>,
282
+ readonly string[] | undefined
283
+ >
284
+ > = Record<never, never>,
285
+ Models extends Record<
286
+ string,
287
+ ModelBuilderState<string, string, Record<string, string>, Record<string, RelationDefinition>>
288
+ > = Record<never, never>,
289
+ Types extends Record<string, StorageTypeInstance> = Record<never, never>,
290
+ StorageHash extends string | undefined = undefined,
291
+ ExtensionPacks extends Record<string, unknown> | undefined = undefined,
292
+ Capabilities extends Record<string, Record<string, boolean>> | undefined = undefined,
293
+ > extends ContractBuilder<Target, Tables, Models, StorageHash, ExtensionPacks, Capabilities> {
294
+ protected declare readonly state: ContractBuilderState<
295
+ Target,
296
+ Tables,
297
+ Models,
298
+ StorageHash,
299
+ ExtensionPacks,
300
+ Capabilities
301
+ > & {
302
+ readonly storageTypes?: Types;
303
+ };
304
+ /**
305
+ * This method is responsible for normalizing the contract IR by setting default values
306
+ * for all required fields:
307
+ * - `nullable`: defaults to `false` if not provided
308
+ * - `uniques`: defaults to `[]` (empty array)
309
+ * - `indexes`: defaults to `[]` (empty array)
310
+ * - `foreignKeys`: defaults to `[]` (empty array)
311
+ * - `relations`: defaults to `{}` (empty object) for both model-level and contract-level
312
+ * - `nativeType`: required field set from column type descriptor when columns are defined
313
+ *
314
+ * The contract builder is the **only** place where normalization should occur.
315
+ * Validators, parsers, and emitters should assume the contract is already normalized.
316
+ *
317
+ * **Required**: Use column type descriptors (e.g., `int4Column`, `textColumn`) when defining columns.
318
+ * This ensures `nativeType` is set correctly at build time.
319
+ *
320
+ * @returns A normalized SqlContract with all required fields present
321
+ */
322
+ build(): Target extends string
323
+ ? ContractWithTypeMaps<
324
+ SqlContract<
325
+ BuildStorage<Tables, Types>,
326
+ BuildModels<Models>,
327
+ BuildRelations<Models>,
328
+ ContractBuilderMappings
329
+ > & {
330
+ readonly schemaVersion: '1';
331
+ readonly target: Target;
332
+ readonly targetFamily: 'sql';
333
+ readonly storageHash: StorageHash extends string ? StorageHash : string;
334
+ } & (ExtensionPacks extends Record<string, unknown>
335
+ ? { readonly extensionPacks: ExtensionPacks }
336
+ : Record<string, never>) &
337
+ (Capabilities extends Record<string, Record<string, boolean>>
338
+ ? { readonly capabilities: Capabilities }
339
+ : Record<string, never>),
340
+ TypeMaps<CodecTypes, Record<string, never>>
341
+ >
342
+ : never {
343
+ type BuiltContract = Target extends string
344
+ ? ContractWithTypeMaps<
345
+ SqlContract<
346
+ BuildStorage<Tables, Types>,
347
+ BuildModels<Models>,
348
+ BuildRelations<Models>,
349
+ ContractBuilderMappings
350
+ > & {
351
+ readonly schemaVersion: '1';
352
+ readonly target: Target;
353
+ readonly targetFamily: 'sql';
354
+ readonly storageHash: StorageHash extends string ? StorageHash : string;
355
+ } & (ExtensionPacks extends Record<string, unknown>
356
+ ? { readonly extensionPacks: ExtensionPacks }
357
+ : Record<string, never>) &
358
+ (Capabilities extends Record<string, Record<string, boolean>>
359
+ ? { readonly capabilities: Capabilities }
360
+ : Record<string, never>),
361
+ TypeMaps<CodecTypes, Record<string, never>>
362
+ >
363
+ : never;
364
+ if (!this.state.target) {
365
+ throw new Error('target is required. Call .target() before .build()');
366
+ }
367
+
368
+ const target = this.state.target as Target & string;
369
+
370
+ const storageTables = {} as Partial<Mutable<BuildStorageTables<Tables>>>;
371
+ const executionDefaults: ExecutionMutationDefault[] = [];
372
+
373
+ for (const tableName of Object.keys(this.state.tables) as Array<keyof Tables & string>) {
374
+ const tableState = this.state.tables[tableName];
375
+ if (!tableState) continue;
376
+
377
+ type TableKey = typeof tableName;
378
+ type ColumnDefs = ExtractColumns<Tables[TableKey]>;
379
+ type PrimaryKey = ExtractPrimaryKey<Tables[TableKey]>;
380
+
381
+ const columns = {} as Partial<{
382
+ [K in keyof ColumnDefs]: BuildStorageColumn<
383
+ ColumnDefs[K]['nullable'] & boolean,
384
+ ColumnDefs[K]['type']
385
+ >;
386
+ }>;
387
+
388
+ for (const columnName in tableState.columns) {
389
+ const columnState = tableState.columns[columnName];
390
+ if (!columnState) continue;
391
+ const codecId = columnState.type;
392
+ const nativeType = columnState.nativeType;
393
+ const typeRef = columnState.typeRef;
394
+
395
+ const encodedDefault =
396
+ columnState.default !== undefined
397
+ ? encodeColumnDefault(columnState.default as ColumnDefault)
398
+ : undefined;
399
+
400
+ columns[columnName as keyof ColumnDefs] = {
401
+ nativeType,
402
+ codecId,
403
+ nullable: (columnState.nullable ?? false) as ColumnDefs[keyof ColumnDefs]['nullable'] &
404
+ boolean,
405
+ ...ifDefined('typeParams', columnState.typeParams),
406
+ ...ifDefined('default', encodedDefault),
407
+ ...ifDefined('typeRef', typeRef),
408
+ } as BuildStorageColumn<
409
+ ColumnDefs[keyof ColumnDefs]['nullable'] & boolean,
410
+ ColumnDefs[keyof ColumnDefs]['type']
411
+ >;
412
+
413
+ if ('executionDefault' in columnState && columnState.executionDefault) {
414
+ executionDefaults.push({
415
+ ref: { table: tableName, column: columnName },
416
+ onCreate: columnState.executionDefault,
417
+ });
418
+ }
419
+ }
420
+
421
+ // Build uniques from table state
422
+ const uniques = (tableState.uniques ?? []).map((u) => ({
423
+ columns: u.columns,
424
+ ...(u.name ? { name: u.name } : {}),
425
+ }));
426
+
427
+ // Build indexes from table state
428
+ const indexes = (tableState.indexes ?? []).map((i) => ({
429
+ columns: i.columns,
430
+ ...(i.name ? { name: i.name } : {}),
431
+ ...(i.using ? { using: i.using } : {}),
432
+ ...(i.config ? { config: i.config } : {}),
433
+ }));
434
+
435
+ // Build foreign keys from table state, materializing defaults
436
+ const foreignKeys = (tableState.foreignKeys ?? []).map((fk) => ({
437
+ columns: fk.columns,
438
+ references: fk.references,
439
+ ...applyFkDefaults(fk, this.state.foreignKeyDefaults),
440
+ ...(fk.name ? { name: fk.name } : {}),
441
+ ...(fk.onDelete !== undefined ? { onDelete: fk.onDelete } : {}),
442
+ ...(fk.onUpdate !== undefined ? { onUpdate: fk.onUpdate } : {}),
443
+ }));
444
+
445
+ const table = {
446
+ columns: columns as {
447
+ [K in keyof ColumnDefs]: BuildStorageColumn<
448
+ ColumnDefs[K]['nullable'] & boolean,
449
+ ColumnDefs[K]['type']
450
+ >;
451
+ },
452
+ uniques,
453
+ indexes,
454
+ foreignKeys,
455
+ ...(tableState.primaryKey
456
+ ? {
457
+ primaryKey: {
458
+ columns: tableState.primaryKey,
459
+ ...(tableState.primaryKeyName ? { name: tableState.primaryKeyName } : {}),
460
+ },
461
+ }
462
+ : {}),
463
+ } as unknown as BuildStorageTable<TableKey & string, ColumnDefs, PrimaryKey>;
464
+
465
+ (storageTables as Mutable<BuildStorageTables<Tables>>)[tableName] = table;
466
+ }
467
+
468
+ const storageTypes = (this.state.storageTypes ?? {}) as Types;
469
+ const storage: BuildStorage<Tables, Types> = {
470
+ tables: storageTables as BuildStorageTables<Tables>,
471
+ types: storageTypes,
472
+ };
473
+
474
+ const execution =
475
+ executionDefaults.length > 0
476
+ ? {
477
+ mutations: {
478
+ defaults: executionDefaults.sort((a, b) => {
479
+ const tableCompare = a.ref.table.localeCompare(b.ref.table);
480
+ if (tableCompare !== 0) {
481
+ return tableCompare;
482
+ }
483
+ return a.ref.column.localeCompare(b.ref.column);
484
+ }),
485
+ },
486
+ }
487
+ : undefined;
488
+
489
+ // Build models - construct as partial first, then assert full type
490
+ const modelsPartial: Partial<BuildModels<Models>> = {};
491
+
492
+ // Iterate over models - TypeScript will see keys as string, but type assertion preserves literals
493
+ for (const modelName in this.state.models) {
494
+ const modelState = this.state.models[modelName];
495
+ if (!modelState) continue;
496
+
497
+ const modelStateTyped = modelState as unknown as {
498
+ name: string;
499
+ table: string;
500
+ fields: Record<string, string>;
501
+ };
502
+
503
+ // Build fields object
504
+ const fields: Partial<Record<string, ModelField>> = {};
505
+
506
+ // Iterate over fields
507
+ for (const fieldName in modelStateTyped.fields) {
508
+ const columnName = modelStateTyped.fields[fieldName];
509
+ if (columnName) {
510
+ fields[fieldName] = {
511
+ column: columnName,
512
+ };
513
+ }
514
+ }
515
+
516
+ // Assign to models - type assertion preserves literal keys
517
+ (modelsPartial as unknown as Record<string, ModelDefinition>)[modelName] = {
518
+ storage: {
519
+ table: modelStateTyped.table,
520
+ },
521
+ fields: fields as Record<string, ModelField>,
522
+ relations: {},
523
+ };
524
+ }
525
+
526
+ // Build relations object - organized by table name
527
+ const relationsPartial: Partial<Record<string, Record<string, RelationDefinition>>> = {};
528
+
529
+ // Iterate over models to collect relations
530
+ for (const modelName in this.state.models) {
531
+ const modelState = this.state.models[modelName];
532
+ if (!modelState) continue;
533
+
534
+ const modelStateTyped = modelState as unknown as {
535
+ name: string;
536
+ table: string;
537
+ fields: Record<string, string>;
538
+ relations: Record<string, RelationDefinition>;
539
+ };
540
+
541
+ const tableName = modelStateTyped.table;
542
+ if (!tableName) continue;
543
+
544
+ // Only initialize relations object for this table if it has relations
545
+ if (modelStateTyped.relations && Object.keys(modelStateTyped.relations).length > 0) {
546
+ if (!relationsPartial[tableName]) {
547
+ relationsPartial[tableName] = {};
548
+ }
549
+
550
+ // Add relations from this model to the table's relations
551
+ const tableRelations = relationsPartial[tableName];
552
+ if (tableRelations) {
553
+ for (const relationName in modelStateTyped.relations) {
554
+ const relation = modelStateTyped.relations[relationName];
555
+ if (relation) {
556
+ tableRelations[relationName] = relation;
557
+ }
558
+ }
559
+ }
560
+ }
561
+ }
562
+
563
+ const models = modelsPartial as unknown as BuildModels<Models>;
564
+
565
+ const baseMappings = computeMappings(
566
+ models as unknown as Record<string, ModelDefinition>,
567
+ storage as SqlStorage,
568
+ );
569
+
570
+ const mappings = baseMappings as ContractBuilderMappings;
571
+
572
+ const extensionNamespaces = this.state.extensionNamespaces ?? [];
573
+ const extensionPacks: Record<string, unknown> = { ...(this.state.extensionPacks || {}) };
574
+ for (const namespace of extensionNamespaces) {
575
+ if (!Object.hasOwn(extensionPacks, namespace)) {
576
+ extensionPacks[namespace] = {};
577
+ }
578
+ }
579
+
580
+ // Construct contract with explicit type that matches the generic parameters
581
+ // This ensures TypeScript infers literal types from the generics, not runtime values
582
+ // Always include relations, even if empty (normalized to empty object)
583
+ const contract = {
584
+ schemaVersion: '1' as const,
585
+ target,
586
+ targetFamily: 'sql' as const,
587
+ storageHash: this.state.storageHash || 'sha256:ts-builder-placeholder',
588
+ models,
589
+ relations: relationsPartial,
590
+ storage,
591
+ mappings,
592
+ ...(execution ? { execution } : {}),
593
+ extensionPacks,
594
+ capabilities: this.state.capabilities || {},
595
+ meta: {},
596
+ sources: {},
597
+ } as unknown as BuiltContract;
598
+
599
+ return contract as unknown as ReturnType<
600
+ SqlContractBuilder<
601
+ CodecTypes,
602
+ Target,
603
+ Tables,
604
+ Models,
605
+ Types,
606
+ StorageHash,
607
+ ExtensionPacks,
608
+ Capabilities
609
+ >['build']
610
+ >;
611
+ }
612
+
613
+ override target<
614
+ T extends string,
615
+ TPack extends TargetPackRef<string, T> = TargetPackRef<string, T>,
616
+ >(
617
+ packRef: TPack & TargetPackRef<string, T>,
618
+ ): SqlContractBuilder<
619
+ ExtractCodecTypesFromPack<TPack> extends Record<string, never>
620
+ ? CodecTypes
621
+ : ExtractCodecTypesFromPack<TPack>,
622
+ T,
623
+ Tables,
624
+ Models,
625
+ Types,
626
+ StorageHash,
627
+ ExtensionPacks,
628
+ Capabilities
629
+ > {
630
+ return new SqlContractBuilder<
631
+ ExtractCodecTypesFromPack<TPack> extends Record<string, never>
632
+ ? CodecTypes
633
+ : ExtractCodecTypesFromPack<TPack>,
634
+ T,
635
+ Tables,
636
+ Models,
637
+ Types,
638
+ StorageHash,
639
+ ExtensionPacks,
640
+ Capabilities
641
+ >({
642
+ ...this.state,
643
+ target: packRef.targetId,
644
+ }) as SqlContractBuilder<
645
+ ExtractCodecTypesFromPack<TPack> extends Record<string, never>
646
+ ? CodecTypes
647
+ : ExtractCodecTypesFromPack<TPack>,
648
+ T,
649
+ Tables,
650
+ Models,
651
+ Types,
652
+ StorageHash,
653
+ ExtensionPacks,
654
+ Capabilities
655
+ >;
656
+ }
657
+
658
+ extensionPacks<const Packs extends Record<string, ExtensionPackRef<'sql', string>>>(
659
+ packs: Packs,
660
+ ): SqlContractBuilder<
661
+ CodecTypes & MergeExtensionCodecTypes<Packs>,
662
+ Target,
663
+ Tables,
664
+ Models,
665
+ Types,
666
+ StorageHash,
667
+ ExtensionPacks,
668
+ Capabilities
669
+ > {
670
+ if (!this.state.target) {
671
+ throw new Error('extensionPacks() requires target() to be called first');
672
+ }
673
+
674
+ const namespaces = new Set(this.state.extensionNamespaces ?? []);
675
+
676
+ for (const packRef of Object.values(packs) as ExtensionPackRef<'sql', string>[]) {
677
+ if (!packRef) continue;
678
+
679
+ if (packRef.kind !== 'extension') {
680
+ throw new Error(
681
+ `extensionPacks() only accepts extension pack refs. Received kind "${packRef.kind}".`,
682
+ );
683
+ }
684
+
685
+ if (packRef.familyId !== 'sql') {
686
+ throw new Error(
687
+ `extension pack "${packRef.id}" targets family "${packRef.familyId}" but this builder targets "sql".`,
688
+ );
689
+ }
690
+
691
+ if (packRef.targetId && packRef.targetId !== this.state.target) {
692
+ throw new Error(
693
+ `extension pack "${packRef.id}" targets "${packRef.targetId}" but builder target is "${this.state.target}".`,
694
+ );
695
+ }
696
+
697
+ namespaces.add(packRef.id);
698
+ }
699
+
700
+ return new SqlContractBuilder<
701
+ CodecTypes & MergeExtensionCodecTypes<Packs>,
702
+ Target,
703
+ Tables,
704
+ Models,
705
+ Types,
706
+ StorageHash,
707
+ ExtensionPacks,
708
+ Capabilities
709
+ >({
710
+ ...this.state,
711
+ extensionNamespaces: [...namespaces],
712
+ });
713
+ }
714
+
715
+ override capabilities<C extends Record<string, Record<string, boolean>>>(
716
+ capabilities: C,
717
+ ): SqlContractBuilder<CodecTypes, Target, Tables, Models, Types, StorageHash, ExtensionPacks, C> {
718
+ return new SqlContractBuilder<
719
+ CodecTypes,
720
+ Target,
721
+ Tables,
722
+ Models,
723
+ Types,
724
+ StorageHash,
725
+ ExtensionPacks,
726
+ C
727
+ >({
728
+ ...this.state,
729
+ capabilities,
730
+ });
731
+ }
732
+
733
+ override storageHash<H extends string>(
734
+ hash: H,
735
+ ): SqlContractBuilder<
736
+ CodecTypes,
737
+ Target,
738
+ Tables,
739
+ Models,
740
+ Types,
741
+ H,
742
+ ExtensionPacks,
743
+ Capabilities
744
+ > {
745
+ return new SqlContractBuilder<
746
+ CodecTypes,
747
+ Target,
748
+ Tables,
749
+ Models,
750
+ Types,
751
+ H,
752
+ ExtensionPacks,
753
+ Capabilities
754
+ >({
755
+ ...this.state,
756
+ storageHash: hash,
757
+ });
758
+ }
759
+
760
+ override table<
761
+ TableName extends string,
762
+ T extends TableBuilder<
763
+ TableName,
764
+ Record<string, ColumnBuilderState<string, boolean, string>>,
765
+ readonly string[] | undefined
766
+ >,
767
+ >(
768
+ name: TableName,
769
+ callback: (t: TableBuilder<TableName>) => T | undefined,
770
+ ): SqlContractBuilder<
771
+ CodecTypes,
772
+ Target,
773
+ Tables & Record<TableName, ReturnType<T['build']>>,
774
+ Models,
775
+ Types,
776
+ StorageHash,
777
+ ExtensionPacks,
778
+ Capabilities
779
+ > {
780
+ const tableBuilder = createTable(name);
781
+ const result = callback(
782
+ tableBuilder as unknown as SqlTableBuilder<
783
+ TableName,
784
+ CodecTypes
785
+ > as unknown as TableBuilder<TableName>,
786
+ );
787
+ const finalBuilder = result instanceof TableBuilder ? result : tableBuilder;
788
+ const tableState = finalBuilder.build();
789
+
790
+ return new SqlContractBuilder<
791
+ CodecTypes,
792
+ Target,
793
+ Tables & Record<TableName, ReturnType<T['build']>>,
794
+ Models,
795
+ Types,
796
+ StorageHash,
797
+ ExtensionPacks,
798
+ Capabilities
799
+ >({
800
+ ...this.state,
801
+ tables: { ...this.state.tables, [name]: tableState } as Tables &
802
+ Record<TableName, ReturnType<T['build']>>,
803
+ });
804
+ }
805
+
806
+ override model<
807
+ ModelName extends string,
808
+ TableName extends string,
809
+ M extends ModelBuilder<
810
+ ModelName,
811
+ TableName,
812
+ Record<string, string>,
813
+ Record<string, RelationDefinition>
814
+ >,
815
+ >(
816
+ name: ModelName,
817
+ table: TableName,
818
+ callback: (
819
+ m: ModelBuilder<ModelName, TableName, Record<never, never>, Record<never, never>>,
820
+ ) => M | undefined,
821
+ ): SqlContractBuilder<
822
+ CodecTypes,
823
+ Target,
824
+ Tables,
825
+ Models & Record<ModelName, ReturnType<M['build']>>,
826
+ Types,
827
+ StorageHash,
828
+ ExtensionPacks,
829
+ Capabilities
830
+ > {
831
+ const modelBuilder = new ModelBuilder<ModelName, TableName>(name, table);
832
+ const result = callback(modelBuilder);
833
+ const finalBuilder = result instanceof ModelBuilder ? result : modelBuilder;
834
+ const modelState = finalBuilder.build();
835
+
836
+ return new SqlContractBuilder<
837
+ CodecTypes,
838
+ Target,
839
+ Tables,
840
+ Models & Record<ModelName, ReturnType<M['build']>>,
841
+ Types,
842
+ StorageHash,
843
+ ExtensionPacks,
844
+ Capabilities
845
+ >({
846
+ ...this.state,
847
+ models: { ...this.state.models, [name]: modelState } as Models &
848
+ Record<ModelName, ReturnType<M['build']>>,
849
+ });
850
+ }
851
+
852
+ override foreignKeyDefaults(
853
+ config: ForeignKeyDefaultsState,
854
+ ): SqlContractBuilder<
855
+ CodecTypes,
856
+ Target,
857
+ Tables,
858
+ Models,
859
+ Types,
860
+ StorageHash,
861
+ ExtensionPacks,
862
+ Capabilities
863
+ > {
864
+ return new SqlContractBuilder<
865
+ CodecTypes,
866
+ Target,
867
+ Tables,
868
+ Models,
869
+ Types,
870
+ StorageHash,
871
+ ExtensionPacks,
872
+ Capabilities
873
+ >({
874
+ ...this.state,
875
+ foreignKeyDefaults: config,
876
+ });
877
+ }
878
+
879
+ storageType<Name extends string, Type extends StorageTypeInstance>(
880
+ name: Name,
881
+ typeInstance: Type,
882
+ ): SqlContractBuilder<
883
+ CodecTypes,
884
+ Target,
885
+ Tables,
886
+ Models,
887
+ Types & Record<Name, Type>,
888
+ StorageHash,
889
+ ExtensionPacks,
890
+ Capabilities
891
+ > {
892
+ return new SqlContractBuilder<
893
+ CodecTypes,
894
+ Target,
895
+ Tables,
896
+ Models,
897
+ Types & Record<Name, Type>,
898
+ StorageHash,
899
+ ExtensionPacks,
900
+ Capabilities
901
+ >({
902
+ ...this.state,
903
+ storageTypes: {
904
+ ...(this.state.storageTypes ?? {}),
905
+ [name]: typeInstance,
906
+ },
907
+ });
908
+ }
909
+ }
910
+
911
+ export function defineContract<
912
+ CodecTypes extends Record<string, { output: unknown }> = Record<string, never>,
913
+ >(): SqlContractBuilder<CodecTypes> {
914
+ return new SqlContractBuilder<CodecTypes>();
915
+ }