@prisma-next/sql-contract-ts 0.3.0-pr.99.6 → 0.4.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +206 -73
  3. package/dist/config-types.d.mts +8 -0
  4. package/dist/config-types.d.mts.map +1 -0
  5. package/dist/config-types.mjs +14 -0
  6. package/dist/config-types.mjs.map +1 -0
  7. package/dist/contract-builder.d.mts +769 -0
  8. package/dist/contract-builder.d.mts.map +1 -0
  9. package/dist/contract-builder.mjs +1288 -0
  10. package/dist/contract-builder.mjs.map +1 -0
  11. package/package.json +19 -16
  12. package/schemas/data-contract-sql-v1.json +189 -23
  13. package/src/authoring-helper-runtime.ts +139 -0
  14. package/src/authoring-type-utils.ts +168 -0
  15. package/src/build-contract.ts +463 -0
  16. package/src/composed-authoring-helpers.ts +256 -0
  17. package/src/config-types.ts +11 -0
  18. package/src/contract-builder.ts +232 -551
  19. package/src/contract-definition.ts +103 -0
  20. package/src/contract-dsl.ts +1492 -0
  21. package/src/contract-lowering.ts +703 -0
  22. package/src/contract-types.ts +534 -0
  23. package/src/contract-warnings.ts +242 -0
  24. package/src/exports/config-types.ts +2 -0
  25. package/src/exports/contract-builder.ts +23 -2
  26. package/dist/chunk-HTNUNGA2.js +0 -346
  27. package/dist/chunk-HTNUNGA2.js.map +0 -1
  28. package/dist/contract-builder.d.ts +0 -101
  29. package/dist/contract-builder.d.ts.map +0 -1
  30. package/dist/contract.d.ts +0 -50
  31. package/dist/contract.d.ts.map +0 -1
  32. package/dist/exports/contract-builder.d.ts +0 -3
  33. package/dist/exports/contract-builder.d.ts.map +0 -1
  34. package/dist/exports/contract-builder.js +0 -231
  35. package/dist/exports/contract-builder.js.map +0 -1
  36. package/dist/exports/contract.d.ts +0 -2
  37. package/dist/exports/contract.d.ts.map +0 -1
  38. package/dist/exports/contract.js +0 -9
  39. package/dist/exports/contract.js.map +0 -1
  40. package/src/contract.ts +0 -582
  41. package/src/exports/contract.ts +0 -1
@@ -0,0 +1,703 @@
1
+ import type { ColumnTypeDescriptor } from '@prisma-next/contract-authoring';
2
+ import type { StorageTypeInstance } from '@prisma-next/sql-contract/types';
3
+ import type {
4
+ ContractDefinition,
5
+ FieldNode,
6
+ ForeignKeyNode,
7
+ IndexNode,
8
+ ModelNode,
9
+ PrimaryKeyNode,
10
+ RelationNode,
11
+ UniqueConstraintNode,
12
+ } from './contract-definition';
13
+ import {
14
+ applyNaming,
15
+ type ContractInput,
16
+ type ContractModelBuilder,
17
+ type FieldStateOf,
18
+ type ForeignKeyConstraint,
19
+ type IdConstraint,
20
+ type ModelAttributesSpec,
21
+ normalizeRelationFieldNames,
22
+ type RelationBuilder,
23
+ type RelationState,
24
+ resolveRelationModelName,
25
+ type ScalarFieldBuilder,
26
+ type SqlStageSpec,
27
+ type UniqueConstraint,
28
+ } from './contract-dsl';
29
+ import {
30
+ emitTypedCrossModelFallbackWarnings,
31
+ emitTypedNamedTypeFallbackWarnings,
32
+ } from './contract-warnings';
33
+
34
+ type RuntimeModel = ContractModelBuilder<
35
+ string | undefined,
36
+ Record<string, ScalarFieldBuilder>,
37
+ Record<string, RelationBuilder<RelationState>>,
38
+ ModelAttributesSpec | undefined,
39
+ SqlStageSpec | undefined
40
+ >;
41
+
42
+ type RuntimeModelSpec = {
43
+ readonly modelName: string;
44
+ readonly tableName: string;
45
+ readonly fieldBuilders: Record<string, ScalarFieldBuilder>;
46
+ readonly fieldToColumn: Record<string, string>;
47
+ readonly relations: Record<string, RelationBuilder<RelationState>>;
48
+ readonly attributesSpec: ModelAttributesSpec | undefined;
49
+ readonly sqlSpec: SqlStageSpec | undefined;
50
+ readonly idConstraint: IdConstraint | undefined;
51
+ };
52
+
53
+ type RuntimeCollection = {
54
+ readonly storageTypes: Record<string, StorageTypeInstance>;
55
+ readonly models: Record<string, RuntimeModel>;
56
+ readonly modelSpecs: ReadonlyMap<string, RuntimeModelSpec>;
57
+ };
58
+
59
+ function buildStorageTypeReverseLookup(
60
+ storageTypes: Record<string, StorageTypeInstance>,
61
+ ): ReadonlyMap<StorageTypeInstance, string> {
62
+ const lookup = new Map<StorageTypeInstance, string>();
63
+ for (const [key, instance] of Object.entries(storageTypes)) {
64
+ lookup.set(instance, key);
65
+ }
66
+ return lookup;
67
+ }
68
+
69
+ function resolveFieldDescriptor(
70
+ modelName: string,
71
+ fieldName: string,
72
+ fieldState: FieldStateOf<ScalarFieldBuilder>,
73
+ storageTypes: Record<string, StorageTypeInstance>,
74
+ storageTypeReverseLookup: ReadonlyMap<StorageTypeInstance, string>,
75
+ ): ColumnTypeDescriptor {
76
+ if ('descriptor' in fieldState && fieldState.descriptor) {
77
+ return fieldState.descriptor;
78
+ }
79
+
80
+ if ('typeRef' in fieldState && fieldState.typeRef) {
81
+ const typeRef =
82
+ typeof fieldState.typeRef === 'string'
83
+ ? fieldState.typeRef
84
+ : storageTypeReverseLookup.get(fieldState.typeRef as StorageTypeInstance);
85
+
86
+ if (!typeRef) {
87
+ throw new Error(
88
+ `Field "${modelName}.${fieldName}" references a storage type instance that is not present in definition.types`,
89
+ );
90
+ }
91
+
92
+ const referencedType = storageTypes[typeRef];
93
+ if (!referencedType) {
94
+ throw new Error(
95
+ `Field "${modelName}.${fieldName}" references unknown storage type "${typeRef}"`,
96
+ );
97
+ }
98
+
99
+ return {
100
+ codecId: referencedType.codecId,
101
+ nativeType: referencedType.nativeType,
102
+ typeRef,
103
+ };
104
+ }
105
+
106
+ throw new Error(`Field "${modelName}.${fieldName}" does not resolve to a storage descriptor`);
107
+ }
108
+
109
+ function mapFieldNamesToColumnNames(
110
+ modelName: string,
111
+ fieldNames: readonly string[],
112
+ fieldToColumn: Record<string, string>,
113
+ ): readonly string[] {
114
+ return fieldNames.map((fieldName) => {
115
+ const columnName = fieldToColumn[fieldName];
116
+ if (!columnName) {
117
+ throw new Error(`Unknown field "${modelName}.${fieldName}" in contract definition`);
118
+ }
119
+ return columnName;
120
+ });
121
+ }
122
+
123
+ function assertRelationFieldArity(params: {
124
+ readonly modelName: string;
125
+ readonly relationName: string;
126
+ readonly leftLabel: string;
127
+ readonly leftFields: readonly string[];
128
+ readonly rightLabel: string;
129
+ readonly rightFields: readonly string[];
130
+ }): void {
131
+ if (params.leftFields.length === params.rightFields.length) {
132
+ return;
133
+ }
134
+
135
+ throw new Error(
136
+ `Relation "${params.modelName}.${params.relationName}" maps ${params.leftFields.length} ${params.leftLabel} field(s) to ${params.rightFields.length} ${params.rightLabel} field(s).`,
137
+ );
138
+ }
139
+
140
+ function resolveInlineIdConstraint(
141
+ spec: Pick<RuntimeModelSpec, 'modelName' | 'fieldBuilders'>,
142
+ ): IdConstraint | undefined {
143
+ const inlineIdFields: string[] = [];
144
+ let idName: string | undefined;
145
+
146
+ for (const [fieldName, fieldBuilder] of Object.entries(spec.fieldBuilders)) {
147
+ const fieldState = fieldBuilder.build();
148
+ if (!fieldState.id) {
149
+ continue;
150
+ }
151
+
152
+ inlineIdFields.push(fieldName);
153
+ if (fieldState.id.name) {
154
+ idName = fieldState.id.name;
155
+ }
156
+ }
157
+
158
+ if (inlineIdFields.length === 0) {
159
+ return undefined;
160
+ }
161
+
162
+ if (inlineIdFields.length > 1) {
163
+ throw new Error(
164
+ `Model "${spec.modelName}" marks multiple fields with .id(). Use .attributes(...) for compound identities.`,
165
+ );
166
+ }
167
+
168
+ const [inlineIdField] = inlineIdFields;
169
+ if (!inlineIdField) {
170
+ return undefined;
171
+ }
172
+
173
+ return {
174
+ kind: 'id',
175
+ fields: [inlineIdField],
176
+ ...(idName ? { name: idName } : {}),
177
+ };
178
+ }
179
+
180
+ function collectInlineUniqueConstraints(spec: RuntimeModelSpec): readonly UniqueConstraint[] {
181
+ const constraints: UniqueConstraint[] = [];
182
+
183
+ for (const [fieldName, fieldBuilder] of Object.entries(spec.fieldBuilders)) {
184
+ const fieldState = fieldBuilder.build();
185
+ if (!fieldState.unique) {
186
+ continue;
187
+ }
188
+
189
+ constraints.push({
190
+ kind: 'unique',
191
+ fields: [fieldName],
192
+ ...(fieldState.unique.name ? { name: fieldState.unique.name } : {}),
193
+ });
194
+ }
195
+
196
+ return constraints;
197
+ }
198
+
199
+ function resolveModelIdConstraint(
200
+ spec: Pick<RuntimeModelSpec, 'modelName' | 'fieldBuilders' | 'attributesSpec'>,
201
+ ): IdConstraint | undefined {
202
+ const inlineId = resolveInlineIdConstraint(spec);
203
+ const attributeId = spec.attributesSpec?.id;
204
+
205
+ if (inlineId && attributeId) {
206
+ throw new Error(
207
+ `Model "${spec.modelName}" defines identity both inline and in .attributes(...). Pick one identity style.`,
208
+ );
209
+ }
210
+
211
+ const resolvedId = attributeId ?? inlineId;
212
+ if (resolvedId && resolvedId.fields.length === 0) {
213
+ throw new Error(`Model "${spec.modelName}" defines an empty identity. Add at least one field.`);
214
+ }
215
+
216
+ return resolvedId;
217
+ }
218
+
219
+ function resolveModelUniqueConstraints(spec: RuntimeModelSpec): readonly UniqueConstraint[] {
220
+ const attributeUniques = spec.attributesSpec?.uniques ?? [];
221
+ for (const unique of attributeUniques) {
222
+ if (unique.fields.length === 0) {
223
+ throw new Error(
224
+ `Model "${spec.modelName}" defines an empty unique constraint. Add at least one field.`,
225
+ );
226
+ }
227
+ }
228
+
229
+ return [...collectInlineUniqueConstraints(spec), ...attributeUniques];
230
+ }
231
+
232
+ function resolveRelationForeignKeys(
233
+ spec: RuntimeModelSpec,
234
+ allSpecs: ReadonlyMap<string, RuntimeModelSpec>,
235
+ ): readonly ForeignKeyConstraint[] {
236
+ const foreignKeys: ForeignKeyConstraint[] = [];
237
+
238
+ for (const [relationName, relationBuilder] of Object.entries(spec.relations)) {
239
+ const relation = relationBuilder.build();
240
+ if (relation.kind !== 'belongsTo' || !relation.sql?.fk) {
241
+ continue;
242
+ }
243
+
244
+ const targetModelName = resolveRelationModelName(relation.toModel);
245
+ if (!allSpecs.has(targetModelName)) {
246
+ throw new Error(
247
+ `Relation "${spec.modelName}.${relationName}" references unknown model "${targetModelName}"`,
248
+ );
249
+ }
250
+
251
+ const fields = normalizeRelationFieldNames(relation.from);
252
+ const targetFields = normalizeRelationFieldNames(relation.to);
253
+ assertRelationFieldArity({
254
+ modelName: spec.modelName,
255
+ relationName,
256
+ leftLabel: 'source',
257
+ leftFields: fields,
258
+ rightLabel: 'target',
259
+ rightFields: targetFields,
260
+ });
261
+
262
+ foreignKeys.push({
263
+ kind: 'fk',
264
+ fields,
265
+ targetModel: targetModelName,
266
+ targetFields,
267
+ ...(relation.sql.fk.name ? { name: relation.sql.fk.name } : {}),
268
+ ...(relation.sql.fk.onDelete ? { onDelete: relation.sql.fk.onDelete } : {}),
269
+ ...(relation.sql.fk.onUpdate ? { onUpdate: relation.sql.fk.onUpdate } : {}),
270
+ ...(relation.sql.fk.constraint !== undefined
271
+ ? { constraint: relation.sql.fk.constraint }
272
+ : {}),
273
+ ...(relation.sql.fk.index !== undefined ? { index: relation.sql.fk.index } : {}),
274
+ });
275
+ }
276
+
277
+ return foreignKeys;
278
+ }
279
+
280
+ function resolveRelationAnchorFields(spec: RuntimeModelSpec): readonly string[] {
281
+ const idFields = spec.idConstraint?.fields;
282
+ if (idFields && idFields.length > 0) {
283
+ return idFields;
284
+ }
285
+
286
+ if ('id' in spec.fieldToColumn) {
287
+ return ['id'];
288
+ }
289
+
290
+ throw new Error(
291
+ `Model "${spec.modelName}" needs an explicit id or an "id" field to anchor non-owning relations`,
292
+ );
293
+ }
294
+
295
+ function lowerBelongsToRelation(
296
+ relationName: string,
297
+ relation: Extract<RelationState, { kind: 'belongsTo' }>,
298
+ currentSpec: RuntimeModelSpec,
299
+ allSpecs: ReadonlyMap<string, RuntimeModelSpec>,
300
+ ): RelationNode {
301
+ const targetModelName = resolveRelationModelName(relation.toModel);
302
+ const targetSpec = allSpecs.get(targetModelName);
303
+ if (!targetSpec) {
304
+ throw new Error(
305
+ `Relation "${currentSpec.modelName}.${relationName}" references unknown model "${targetModelName}"`,
306
+ );
307
+ }
308
+
309
+ const fromFields = normalizeRelationFieldNames(relation.from);
310
+ const toFields = normalizeRelationFieldNames(relation.to);
311
+ assertRelationFieldArity({
312
+ modelName: currentSpec.modelName,
313
+ relationName,
314
+ leftLabel: 'source',
315
+ leftFields: fromFields,
316
+ rightLabel: 'target',
317
+ rightFields: toFields,
318
+ });
319
+
320
+ return {
321
+ fieldName: relationName,
322
+ toModel: targetModelName,
323
+ toTable: targetSpec.tableName,
324
+ cardinality: 'N:1',
325
+ on: {
326
+ parentTable: currentSpec.tableName,
327
+ parentColumns: mapFieldNamesToColumnNames(
328
+ currentSpec.modelName,
329
+ fromFields,
330
+ currentSpec.fieldToColumn,
331
+ ),
332
+ childTable: targetSpec.tableName,
333
+ childColumns: mapFieldNamesToColumnNames(
334
+ targetSpec.modelName,
335
+ toFields,
336
+ targetSpec.fieldToColumn,
337
+ ),
338
+ },
339
+ };
340
+ }
341
+
342
+ function lowerHasOwnershipRelation(
343
+ relationName: string,
344
+ relation: Extract<RelationState, { kind: 'hasMany' | 'hasOne' }>,
345
+ currentSpec: RuntimeModelSpec,
346
+ allSpecs: ReadonlyMap<string, RuntimeModelSpec>,
347
+ ): RelationNode {
348
+ const targetModelName = resolveRelationModelName(relation.toModel);
349
+ const targetSpec = allSpecs.get(targetModelName);
350
+ if (!targetSpec) {
351
+ throw new Error(
352
+ `Relation "${currentSpec.modelName}.${relationName}" references unknown model "${targetModelName}"`,
353
+ );
354
+ }
355
+
356
+ const parentFields = resolveRelationAnchorFields(currentSpec);
357
+ const childFields = normalizeRelationFieldNames(relation.by);
358
+ assertRelationFieldArity({
359
+ modelName: currentSpec.modelName,
360
+ relationName,
361
+ leftLabel: 'anchor',
362
+ leftFields: parentFields,
363
+ rightLabel: 'child',
364
+ rightFields: childFields,
365
+ });
366
+
367
+ return {
368
+ fieldName: relationName,
369
+ toModel: targetModelName,
370
+ toTable: targetSpec.tableName,
371
+ cardinality: relation.kind === 'hasMany' ? '1:N' : '1:1',
372
+ on: {
373
+ parentTable: currentSpec.tableName,
374
+ parentColumns: mapFieldNamesToColumnNames(
375
+ currentSpec.modelName,
376
+ parentFields,
377
+ currentSpec.fieldToColumn,
378
+ ),
379
+ childTable: targetSpec.tableName,
380
+ childColumns: mapFieldNamesToColumnNames(
381
+ targetSpec.modelName,
382
+ childFields,
383
+ targetSpec.fieldToColumn,
384
+ ),
385
+ },
386
+ };
387
+ }
388
+
389
+ function lowerManyToManyRelation(
390
+ relationName: string,
391
+ relation: Extract<RelationState, { kind: 'manyToMany' }>,
392
+ currentSpec: RuntimeModelSpec,
393
+ allSpecs: ReadonlyMap<string, RuntimeModelSpec>,
394
+ ): RelationNode {
395
+ const targetModelName = resolveRelationModelName(relation.toModel);
396
+ const targetSpec = allSpecs.get(targetModelName);
397
+ if (!targetSpec) {
398
+ throw new Error(
399
+ `Relation "${currentSpec.modelName}.${relationName}" references unknown model "${targetModelName}"`,
400
+ );
401
+ }
402
+
403
+ const throughModelName = resolveRelationModelName(relation.through);
404
+ const throughSpec = allSpecs.get(throughModelName);
405
+ if (!throughSpec) {
406
+ throw new Error(
407
+ `Relation "${currentSpec.modelName}.${relationName}" references unknown through model "${throughModelName}"`,
408
+ );
409
+ }
410
+
411
+ const currentAnchorFields = resolveRelationAnchorFields(currentSpec);
412
+ const targetAnchorFields = resolveRelationAnchorFields(targetSpec);
413
+ const throughFromFields = normalizeRelationFieldNames(relation.from);
414
+ const throughToFields = normalizeRelationFieldNames(relation.to);
415
+ if (
416
+ currentAnchorFields.length !== throughFromFields.length ||
417
+ targetAnchorFields.length !== throughToFields.length
418
+ ) {
419
+ throw new Error(
420
+ `Relation "${currentSpec.modelName}.${relationName}" has mismatched many-to-many field counts.`,
421
+ );
422
+ }
423
+
424
+ return {
425
+ fieldName: relationName,
426
+ toModel: targetModelName,
427
+ toTable: targetSpec.tableName,
428
+ cardinality: 'N:M',
429
+ through: {
430
+ table: throughSpec.tableName,
431
+ parentColumns: mapFieldNamesToColumnNames(
432
+ throughSpec.modelName,
433
+ throughFromFields,
434
+ throughSpec.fieldToColumn,
435
+ ),
436
+ childColumns: mapFieldNamesToColumnNames(
437
+ throughSpec.modelName,
438
+ throughToFields,
439
+ throughSpec.fieldToColumn,
440
+ ),
441
+ },
442
+ on: {
443
+ parentTable: currentSpec.tableName,
444
+ parentColumns: mapFieldNamesToColumnNames(
445
+ currentSpec.modelName,
446
+ currentAnchorFields,
447
+ currentSpec.fieldToColumn,
448
+ ),
449
+ childTable: throughSpec.tableName,
450
+ childColumns: mapFieldNamesToColumnNames(
451
+ throughSpec.modelName,
452
+ throughFromFields,
453
+ throughSpec.fieldToColumn,
454
+ ),
455
+ },
456
+ };
457
+ }
458
+
459
+ function resolveRelationNode(
460
+ relationName: string,
461
+ relation: RelationState,
462
+ currentSpec: RuntimeModelSpec,
463
+ allSpecs: ReadonlyMap<string, RuntimeModelSpec>,
464
+ ): RelationNode {
465
+ if (relation.kind === 'belongsTo') {
466
+ return lowerBelongsToRelation(relationName, relation, currentSpec, allSpecs);
467
+ }
468
+
469
+ if (relation.kind === 'hasMany' || relation.kind === 'hasOne') {
470
+ return lowerHasOwnershipRelation(relationName, relation, currentSpec, allSpecs);
471
+ }
472
+
473
+ return lowerManyToManyRelation(relationName, relation, currentSpec, allSpecs);
474
+ }
475
+
476
+ function lowerForeignKeyNode(
477
+ spec: RuntimeModelSpec,
478
+ targetSpec: RuntimeModelSpec,
479
+ foreignKey: {
480
+ readonly fields: readonly string[];
481
+ readonly targetFields: readonly string[];
482
+ readonly name?: string | undefined;
483
+ readonly onDelete?: ForeignKeyConstraint['onDelete'] | undefined;
484
+ readonly onUpdate?: ForeignKeyConstraint['onUpdate'] | undefined;
485
+ readonly constraint?: boolean | undefined;
486
+ readonly index?: boolean | undefined;
487
+ },
488
+ ): ForeignKeyNode {
489
+ return {
490
+ columns: mapFieldNamesToColumnNames(spec.modelName, foreignKey.fields, spec.fieldToColumn),
491
+ references: {
492
+ model: targetSpec.modelName,
493
+ table: targetSpec.tableName,
494
+ columns: mapFieldNamesToColumnNames(
495
+ targetSpec.modelName,
496
+ foreignKey.targetFields,
497
+ targetSpec.fieldToColumn,
498
+ ),
499
+ },
500
+ ...(foreignKey.name ? { name: foreignKey.name } : {}),
501
+ ...(foreignKey.onDelete ? { onDelete: foreignKey.onDelete } : {}),
502
+ ...(foreignKey.onUpdate ? { onUpdate: foreignKey.onUpdate } : {}),
503
+ ...(foreignKey.constraint !== undefined ? { constraint: foreignKey.constraint } : {}),
504
+ ...(foreignKey.index !== undefined ? { index: foreignKey.index } : {}),
505
+ };
506
+ }
507
+
508
+ function resolveForeignKeyNodes(
509
+ spec: RuntimeModelSpec,
510
+ allSpecs: ReadonlyMap<string, RuntimeModelSpec>,
511
+ ): readonly ForeignKeyNode[] {
512
+ const relationForeignKeys = resolveRelationForeignKeys(spec, allSpecs).map((foreignKey) => {
513
+ const targetSpec = allSpecs.get(foreignKey.targetModel);
514
+ if (!targetSpec) {
515
+ throw new Error(
516
+ `Foreign key on "${spec.modelName}" references unknown model "${foreignKey.targetModel}"`,
517
+ );
518
+ }
519
+
520
+ return lowerForeignKeyNode(spec, targetSpec, foreignKey);
521
+ });
522
+
523
+ const sqlForeignKeys = (spec.sqlSpec?.foreignKeys ?? []).map((foreignKey) => {
524
+ const targetSpec = allSpecs.get(foreignKey.targetModel);
525
+ if (!targetSpec) {
526
+ throw new Error(
527
+ `Foreign key on "${spec.modelName}" references unknown model "${foreignKey.targetModel}"`,
528
+ );
529
+ }
530
+
531
+ return lowerForeignKeyNode(spec, targetSpec, foreignKey);
532
+ });
533
+
534
+ return [...relationForeignKeys, ...sqlForeignKeys];
535
+ }
536
+
537
+ function resolveModelNode(
538
+ spec: RuntimeModelSpec,
539
+ allSpecs: ReadonlyMap<string, RuntimeModelSpec>,
540
+ storageTypes: Record<string, StorageTypeInstance>,
541
+ storageTypeReverseLookup: ReadonlyMap<StorageTypeInstance, string>,
542
+ ): ModelNode {
543
+ const fields: FieldNode[] = [];
544
+
545
+ for (const [fieldName, fieldBuilder] of Object.entries(spec.fieldBuilders)) {
546
+ const fieldState = fieldBuilder.build();
547
+ const descriptor = resolveFieldDescriptor(
548
+ spec.modelName,
549
+ fieldName,
550
+ fieldState,
551
+ storageTypes,
552
+ storageTypeReverseLookup,
553
+ );
554
+ const columnName = spec.fieldToColumn[fieldName];
555
+ if (!columnName) {
556
+ throw new Error(`Column name resolution failed for "${spec.modelName}.${fieldName}"`);
557
+ }
558
+
559
+ fields.push({
560
+ fieldName,
561
+ columnName,
562
+ descriptor,
563
+ nullable: fieldState.nullable,
564
+ ...(fieldState.default ? { default: fieldState.default } : {}),
565
+ ...(fieldState.executionDefault ? { executionDefault: fieldState.executionDefault } : {}),
566
+ });
567
+ }
568
+
569
+ const { idConstraint } = spec;
570
+ const uniques = resolveModelUniqueConstraints(spec).map((unique) => ({
571
+ columns: mapFieldNamesToColumnNames(spec.modelName, unique.fields, spec.fieldToColumn),
572
+ ...(unique.name ? { name: unique.name } : {}),
573
+ })) satisfies readonly UniqueConstraintNode[];
574
+ const indexes = (spec.sqlSpec?.indexes ?? []).map((index) => ({
575
+ columns: mapFieldNamesToColumnNames(spec.modelName, index.fields, spec.fieldToColumn),
576
+ ...(index.name ? { name: index.name } : {}),
577
+ ...(index.using ? { using: index.using } : {}),
578
+ ...(index.config ? { config: index.config } : {}),
579
+ })) satisfies readonly IndexNode[];
580
+ const foreignKeys = resolveForeignKeyNodes(spec, allSpecs);
581
+ const relations = Object.entries(spec.relations).map(([relationName, relationBuilder]) =>
582
+ resolveRelationNode(relationName, relationBuilder.build(), spec, allSpecs),
583
+ );
584
+
585
+ return {
586
+ modelName: spec.modelName,
587
+ tableName: spec.tableName,
588
+ fields,
589
+ ...(idConstraint
590
+ ? {
591
+ id: {
592
+ columns: mapFieldNamesToColumnNames(
593
+ spec.modelName,
594
+ idConstraint.fields,
595
+ spec.fieldToColumn,
596
+ ),
597
+ ...(idConstraint.name ? { name: idConstraint.name } : {}),
598
+ } satisfies PrimaryKeyNode,
599
+ }
600
+ : {}),
601
+ ...(uniques.length > 0 ? { uniques } : {}),
602
+ ...(indexes.length > 0 ? { indexes } : {}),
603
+ ...(foreignKeys.length > 0 ? { foreignKeys } : {}),
604
+ ...(relations.length > 0 ? { relations } : {}),
605
+ };
606
+ }
607
+
608
+ function collectRuntimeModelSpecs(definition: ContractInput): RuntimeCollection {
609
+ const storageTypes = { ...(definition.types ?? {}) } as Record<string, StorageTypeInstance>;
610
+ const models = { ...(definition.models ?? {}) } as Record<string, RuntimeModel>;
611
+
612
+ emitTypedNamedTypeFallbackWarnings(models, storageTypes);
613
+
614
+ const modelSpecs = new Map<string, RuntimeModelSpec>();
615
+ const tableOwners = new Map<string, string>();
616
+
617
+ for (const [modelName, modelDefinition] of Object.entries(models)) {
618
+ const tokenModelName = modelDefinition.stageOne.modelName;
619
+ if (tokenModelName && tokenModelName !== modelName) {
620
+ throw new Error(
621
+ `Model token "${tokenModelName}" must be assigned to models.${tokenModelName}. Received models.${modelName}.`,
622
+ );
623
+ }
624
+
625
+ const attributesSpec = modelDefinition.buildAttributesSpec();
626
+ const sqlSpec = modelDefinition.buildSqlSpec();
627
+ const tableName = sqlSpec?.table ?? applyNaming(modelName, definition.naming?.tables);
628
+ const existingModel = tableOwners.get(tableName);
629
+ if (existingModel) {
630
+ throw new Error(
631
+ `Models "${existingModel}" and "${modelName}" both map to table "${tableName}".`,
632
+ );
633
+ }
634
+ tableOwners.set(tableName, modelName);
635
+
636
+ const fieldToColumn: Record<string, string> = {};
637
+ const columnOwners = new Map<string, string>();
638
+
639
+ for (const [fieldName, fieldBuilder] of Object.entries(modelDefinition.stageOne.fields)) {
640
+ const fieldState = fieldBuilder.build();
641
+ const columnName =
642
+ fieldState.columnName ?? applyNaming(fieldName, definition.naming?.columns);
643
+ const existingField = columnOwners.get(columnName);
644
+ if (existingField) {
645
+ throw new Error(
646
+ `Model "${modelName}" maps both "${existingField}" and "${fieldName}" to column "${columnName}".`,
647
+ );
648
+ }
649
+ columnOwners.set(columnName, fieldName);
650
+ fieldToColumn[fieldName] = columnName;
651
+ }
652
+
653
+ const fieldBuilders = modelDefinition.stageOne.fields;
654
+ const idConstraint = resolveModelIdConstraint({ modelName, fieldBuilders, attributesSpec });
655
+ modelSpecs.set(modelName, {
656
+ modelName,
657
+ tableName,
658
+ fieldBuilders,
659
+ fieldToColumn,
660
+ relations: modelDefinition.stageOne.relations,
661
+ attributesSpec,
662
+ sqlSpec,
663
+ idConstraint,
664
+ });
665
+ }
666
+
667
+ return {
668
+ storageTypes,
669
+ models,
670
+ modelSpecs,
671
+ };
672
+ }
673
+
674
+ function lowerModels(collection: RuntimeCollection): readonly ModelNode[] {
675
+ emitTypedCrossModelFallbackWarnings(collection);
676
+
677
+ const storageTypeReverseLookup = buildStorageTypeReverseLookup(collection.storageTypes);
678
+ return Array.from(collection.modelSpecs.values()).map((spec) =>
679
+ resolveModelNode(
680
+ spec,
681
+ collection.modelSpecs,
682
+ collection.storageTypes,
683
+ storageTypeReverseLookup,
684
+ ),
685
+ );
686
+ }
687
+
688
+ export function buildContractDefinition(definition: ContractInput): ContractDefinition {
689
+ const collection = collectRuntimeModelSpecs(definition);
690
+ const models = lowerModels(collection);
691
+
692
+ return {
693
+ target: definition.target,
694
+ ...(definition.extensionPacks ? { extensionPacks: definition.extensionPacks } : {}),
695
+ ...(definition.capabilities ? { capabilities: definition.capabilities } : {}),
696
+ ...(definition.storageHash ? { storageHash: definition.storageHash } : {}),
697
+ ...(definition.foreignKeyDefaults ? { foreignKeyDefaults: definition.foreignKeyDefaults } : {}),
698
+ ...(Object.keys(collection.storageTypes).length > 0
699
+ ? { storageTypes: collection.storageTypes }
700
+ : {}),
701
+ models,
702
+ };
703
+ }