@prisma-next/sql-contract-ts 0.16.0-dev.3 → 0.16.0-dev.30
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/README.md +1 -1
- package/dist/{build-contract-CAX6inbk.mjs → build-contract-DbUeVkiu.mjs} +94 -30
- package/dist/build-contract-DbUeVkiu.mjs.map +1 -0
- package/dist/config-types.d.mts.map +1 -1
- package/dist/config-types.mjs +7 -6
- package/dist/config-types.mjs.map +1 -1
- package/dist/contract-builder.d.mts +30 -27
- package/dist/contract-builder.d.mts.map +1 -1
- package/dist/contract-builder.mjs +243 -86
- package/dist/contract-builder.mjs.map +1 -1
- package/package.json +10 -10
- package/schemas/data-contract-sql-v1.json +4 -4
- package/src/authoring-helper-runtime.ts +16 -3
- package/src/build-contract.ts +102 -33
- package/src/composed-authoring-helpers.ts +26 -21
- package/src/config-types.ts +9 -5
- package/src/contract-builder.ts +92 -41
- package/src/contract-definition.ts +4 -1
- package/src/contract-dsl.ts +59 -15
- package/src/contract-errors.ts +39 -0
- package/src/contract-lowering.ts +116 -44
- package/src/contract-types.ts +7 -7
- package/dist/build-contract-CAX6inbk.mjs.map +0 -1
package/src/contract-lowering.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from '@prisma-next/sql-contract/entity-handle-lowering-hook';
|
|
12
12
|
import type { StorageTypeInstance } from '@prisma-next/sql-contract/types';
|
|
13
13
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
14
|
+
import { InternalError } from '@prisma-next/utils/internal-error';
|
|
14
15
|
import type {
|
|
15
16
|
AttachedEntities,
|
|
16
17
|
ContractDefinition,
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
type SqlStageSpec,
|
|
40
41
|
type UniqueConstraint,
|
|
41
42
|
} from './contract-dsl';
|
|
43
|
+
import { contractError } from './contract-errors';
|
|
42
44
|
import {
|
|
43
45
|
emitTypedCrossModelFallbackWarnings,
|
|
44
46
|
emitTypedNamedTypeFallbackWarnings,
|
|
@@ -106,15 +108,19 @@ function resolveFieldDescriptor(
|
|
|
106
108
|
: storageTypeReverseLookup.get(fieldState.typeRef as StorageTypeInstance);
|
|
107
109
|
|
|
108
110
|
if (!typeRef) {
|
|
109
|
-
throw
|
|
111
|
+
throw contractError(
|
|
112
|
+
'CONTRACT.TYPE_UNKNOWN',
|
|
110
113
|
`Field "${modelName}.${fieldName}" references a storage type instance that is not present in definition.types`,
|
|
114
|
+
{ meta: { modelName, fieldName, reason: 'instance-not-in-definition-types' } },
|
|
111
115
|
);
|
|
112
116
|
}
|
|
113
117
|
|
|
114
118
|
const referencedType = storageTypes[typeRef];
|
|
115
119
|
if (!referencedType) {
|
|
116
|
-
throw
|
|
120
|
+
throw contractError(
|
|
121
|
+
'CONTRACT.TYPE_UNKNOWN',
|
|
117
122
|
`Field "${modelName}.${fieldName}" references unknown storage type "${typeRef}"`,
|
|
123
|
+
{ meta: { modelName, fieldName, typeRef } },
|
|
118
124
|
);
|
|
119
125
|
}
|
|
120
126
|
|
|
@@ -125,7 +131,11 @@ function resolveFieldDescriptor(
|
|
|
125
131
|
};
|
|
126
132
|
}
|
|
127
133
|
|
|
128
|
-
throw
|
|
134
|
+
throw contractError(
|
|
135
|
+
'CONTRACT.TYPE_UNKNOWN',
|
|
136
|
+
`Field "${modelName}.${fieldName}" does not resolve to a storage descriptor`,
|
|
137
|
+
{ meta: { modelName, fieldName, reason: 'unresolved-storage-descriptor' } },
|
|
138
|
+
);
|
|
129
139
|
}
|
|
130
140
|
|
|
131
141
|
function mapFieldNamesToColumnNames(
|
|
@@ -136,7 +146,11 @@ function mapFieldNamesToColumnNames(
|
|
|
136
146
|
return fieldNames.map((fieldName) => {
|
|
137
147
|
const columnName = fieldToColumn[fieldName];
|
|
138
148
|
if (!columnName) {
|
|
139
|
-
throw
|
|
149
|
+
throw contractError(
|
|
150
|
+
'CONTRACT.FIELD_UNKNOWN',
|
|
151
|
+
`Unknown field "${modelName}.${fieldName}" in contract definition`,
|
|
152
|
+
{ meta: { modelName, fieldName } },
|
|
153
|
+
);
|
|
140
154
|
}
|
|
141
155
|
return columnName;
|
|
142
156
|
});
|
|
@@ -154,8 +168,16 @@ function assertRelationFieldArity(params: {
|
|
|
154
168
|
return;
|
|
155
169
|
}
|
|
156
170
|
|
|
157
|
-
throw
|
|
171
|
+
throw contractError(
|
|
172
|
+
'CONTRACT.RELATION_INVALID',
|
|
158
173
|
`Relation "${params.modelName}.${params.relationName}" maps ${params.leftFields.length} ${params.leftLabel} field(s) to ${params.rightFields.length} ${params.rightLabel} field(s).`,
|
|
174
|
+
{
|
|
175
|
+
meta: {
|
|
176
|
+
modelName: params.modelName,
|
|
177
|
+
relationName: params.relationName,
|
|
178
|
+
reason: 'field-count-mismatch',
|
|
179
|
+
},
|
|
180
|
+
},
|
|
159
181
|
);
|
|
160
182
|
}
|
|
161
183
|
|
|
@@ -182,8 +204,12 @@ function resolveInlineIdConstraint(
|
|
|
182
204
|
}
|
|
183
205
|
|
|
184
206
|
if (inlineIdFields.length > 1) {
|
|
185
|
-
throw
|
|
207
|
+
throw contractError(
|
|
208
|
+
'CONTRACT.IDENTITY_INVALID',
|
|
186
209
|
`Model "${spec.modelName}" marks multiple fields with .id(). Use .attributes(...) for compound identities.`,
|
|
210
|
+
{
|
|
211
|
+
meta: { modelName: spec.modelName, reason: 'multiple-inline-ids', fields: inlineIdFields },
|
|
212
|
+
},
|
|
187
213
|
);
|
|
188
214
|
}
|
|
189
215
|
|
|
@@ -225,14 +251,20 @@ function resolveModelIdConstraint(
|
|
|
225
251
|
const attributeId = spec.attributesSpec?.id;
|
|
226
252
|
|
|
227
253
|
if (inlineId && attributeId) {
|
|
228
|
-
throw
|
|
254
|
+
throw contractError(
|
|
255
|
+
'CONTRACT.IDENTITY_INVALID',
|
|
229
256
|
`Model "${spec.modelName}" defines identity both inline and in .attributes(...). Pick one identity style.`,
|
|
257
|
+
{ meta: { modelName: spec.modelName, reason: 'inline-and-attributes' } },
|
|
230
258
|
);
|
|
231
259
|
}
|
|
232
260
|
|
|
233
261
|
const resolvedId = attributeId ?? inlineId;
|
|
234
262
|
if (resolvedId && resolvedId.fields.length === 0) {
|
|
235
|
-
throw
|
|
263
|
+
throw contractError(
|
|
264
|
+
'CONTRACT.IDENTITY_INVALID',
|
|
265
|
+
`Model "${spec.modelName}" defines an empty identity. Add at least one field.`,
|
|
266
|
+
{ meta: { modelName: spec.modelName, reason: 'empty-identity' } },
|
|
267
|
+
);
|
|
236
268
|
}
|
|
237
269
|
|
|
238
270
|
return resolvedId;
|
|
@@ -242,8 +274,10 @@ function resolveModelUniqueConstraints(spec: RuntimeModelSpec): readonly UniqueC
|
|
|
242
274
|
const attributeUniques = spec.attributesSpec?.uniques ?? [];
|
|
243
275
|
for (const unique of attributeUniques) {
|
|
244
276
|
if (unique.fields.length === 0) {
|
|
245
|
-
throw
|
|
277
|
+
throw contractError(
|
|
278
|
+
'CONTRACT.CONSTRAINT_INVALID',
|
|
246
279
|
`Model "${spec.modelName}" defines an empty unique constraint. Add at least one field.`,
|
|
280
|
+
{ meta: { modelName: spec.modelName } },
|
|
247
281
|
);
|
|
248
282
|
}
|
|
249
283
|
}
|
|
@@ -300,8 +334,10 @@ function resolveRelationForeignKeys(
|
|
|
300
334
|
}
|
|
301
335
|
|
|
302
336
|
if (!allSpecs.has(targetModelName)) {
|
|
303
|
-
throw
|
|
337
|
+
throw contractError(
|
|
338
|
+
'CONTRACT.MODEL_UNKNOWN',
|
|
304
339
|
`Relation "${spec.modelName}.${relationName}" references unknown model "${targetModelName}"`,
|
|
340
|
+
{ meta: { sourceModel: spec.modelName, relationName, targetModel: targetModelName } },
|
|
305
341
|
);
|
|
306
342
|
}
|
|
307
343
|
|
|
@@ -344,8 +380,10 @@ function resolveRelationAnchorFields(spec: RuntimeModelSpec): readonly string[]
|
|
|
344
380
|
return ['id'];
|
|
345
381
|
}
|
|
346
382
|
|
|
347
|
-
throw
|
|
383
|
+
throw contractError(
|
|
384
|
+
'CONTRACT.IDENTITY_INVALID',
|
|
348
385
|
`Model "${spec.modelName}" needs an explicit id or an "id" field to anchor non-owning relations`,
|
|
386
|
+
{ meta: { modelName: spec.modelName, reason: 'missing-anchor-id' } },
|
|
349
387
|
);
|
|
350
388
|
}
|
|
351
389
|
|
|
@@ -354,7 +392,7 @@ function lowerBelongsToRelation(
|
|
|
354
392
|
relation: Extract<RelationState, { kind: 'belongsTo' }>,
|
|
355
393
|
currentSpec: RuntimeModelSpec,
|
|
356
394
|
allSpecs: ReadonlyMap<string, RuntimeModelSpec>,
|
|
357
|
-
|
|
395
|
+
extensions?: Record<string, ExtensionPackRef<'sql', string>>,
|
|
358
396
|
): RelationNode {
|
|
359
397
|
const targetModelName = resolveRelationModelName(relation.toModel);
|
|
360
398
|
const fromFields = normalizeRelationFieldNames(relation.from);
|
|
@@ -374,7 +412,7 @@ function lowerBelongsToRelation(
|
|
|
374
412
|
// requiring a local model spec — matching how the FK lowering works.
|
|
375
413
|
if (relation.spaceId !== undefined) {
|
|
376
414
|
assertKnownExtensionPack(
|
|
377
|
-
|
|
415
|
+
extensions,
|
|
378
416
|
relation.spaceId,
|
|
379
417
|
`Relation "${currentSpec.modelName}.${relationName}"`,
|
|
380
418
|
);
|
|
@@ -406,8 +444,10 @@ function lowerBelongsToRelation(
|
|
|
406
444
|
|
|
407
445
|
const targetSpec = allSpecs.get(targetModelName);
|
|
408
446
|
if (!targetSpec) {
|
|
409
|
-
throw
|
|
447
|
+
throw contractError(
|
|
448
|
+
'CONTRACT.MODEL_UNKNOWN',
|
|
410
449
|
`Relation "${currentSpec.modelName}.${relationName}" references unknown model "${targetModelName}"`,
|
|
450
|
+
{ meta: { sourceModel: currentSpec.modelName, relationName, targetModel: targetModelName } },
|
|
411
451
|
);
|
|
412
452
|
}
|
|
413
453
|
|
|
@@ -442,8 +482,10 @@ function lowerHasOwnershipRelation(
|
|
|
442
482
|
const targetModelName = resolveRelationModelName(relation.toModel);
|
|
443
483
|
const targetSpec = allSpecs.get(targetModelName);
|
|
444
484
|
if (!targetSpec) {
|
|
445
|
-
throw
|
|
485
|
+
throw contractError(
|
|
486
|
+
'CONTRACT.MODEL_UNKNOWN',
|
|
446
487
|
`Relation "${currentSpec.modelName}.${relationName}" references unknown model "${targetModelName}"`,
|
|
488
|
+
{ meta: { sourceModel: currentSpec.modelName, relationName, targetModel: targetModelName } },
|
|
447
489
|
);
|
|
448
490
|
}
|
|
449
491
|
|
|
@@ -489,16 +531,20 @@ function lowerManyToManyRelation(
|
|
|
489
531
|
const targetModelName = resolveRelationModelName(relation.toModel);
|
|
490
532
|
const targetSpec = allSpecs.get(targetModelName);
|
|
491
533
|
if (!targetSpec) {
|
|
492
|
-
throw
|
|
534
|
+
throw contractError(
|
|
535
|
+
'CONTRACT.MODEL_UNKNOWN',
|
|
493
536
|
`Relation "${currentSpec.modelName}.${relationName}" references unknown model "${targetModelName}"`,
|
|
537
|
+
{ meta: { sourceModel: currentSpec.modelName, relationName, targetModel: targetModelName } },
|
|
494
538
|
);
|
|
495
539
|
}
|
|
496
540
|
|
|
497
541
|
const throughModelName = resolveRelationModelName(relation.through);
|
|
498
542
|
const throughSpec = allSpecs.get(throughModelName);
|
|
499
543
|
if (!throughSpec) {
|
|
500
|
-
throw
|
|
544
|
+
throw contractError(
|
|
545
|
+
'CONTRACT.MODEL_UNKNOWN',
|
|
501
546
|
`Relation "${currentSpec.modelName}.${relationName}" references unknown through model "${throughModelName}"`,
|
|
547
|
+
{ meta: { sourceModel: currentSpec.modelName, relationName, targetModel: throughModelName } },
|
|
502
548
|
);
|
|
503
549
|
}
|
|
504
550
|
|
|
@@ -510,8 +556,16 @@ function lowerManyToManyRelation(
|
|
|
510
556
|
currentAnchorFields.length !== throughFromFields.length ||
|
|
511
557
|
targetAnchorFields.length !== throughToFields.length
|
|
512
558
|
) {
|
|
513
|
-
throw
|
|
559
|
+
throw contractError(
|
|
560
|
+
'CONTRACT.RELATION_INVALID',
|
|
514
561
|
`Relation "${currentSpec.modelName}.${relationName}" has mismatched many-to-many field counts.`,
|
|
562
|
+
{
|
|
563
|
+
meta: {
|
|
564
|
+
modelName: currentSpec.modelName,
|
|
565
|
+
relationName,
|
|
566
|
+
reason: 'many-to-many-field-count-mismatch',
|
|
567
|
+
},
|
|
568
|
+
},
|
|
515
569
|
);
|
|
516
570
|
}
|
|
517
571
|
|
|
@@ -556,10 +610,10 @@ function resolveRelationNode(
|
|
|
556
610
|
relation: RelationState,
|
|
557
611
|
currentSpec: RuntimeModelSpec,
|
|
558
612
|
allSpecs: ReadonlyMap<string, RuntimeModelSpec>,
|
|
559
|
-
|
|
613
|
+
extensions?: Record<string, ExtensionPackRef<'sql', string>>,
|
|
560
614
|
): RelationNode {
|
|
561
615
|
if (relation.kind === 'belongsTo') {
|
|
562
|
-
return lowerBelongsToRelation(relationName, relation, currentSpec, allSpecs,
|
|
616
|
+
return lowerBelongsToRelation(relationName, relation, currentSpec, allSpecs, extensions);
|
|
563
617
|
}
|
|
564
618
|
|
|
565
619
|
if (relation.kind === 'hasMany' || relation.kind === 'hasOne') {
|
|
@@ -637,29 +691,31 @@ function lowerCrossSpaceForeignKeyNode(
|
|
|
637
691
|
}
|
|
638
692
|
|
|
639
693
|
function assertKnownExtensionPack(
|
|
640
|
-
|
|
694
|
+
extensions: Record<string, ExtensionPackRef<'sql', string>> | undefined,
|
|
641
695
|
spaceId: string,
|
|
642
696
|
context: string,
|
|
643
697
|
): void {
|
|
644
|
-
if (
|
|
698
|
+
if (extensions !== undefined && Object.hasOwn(extensions, spaceId)) {
|
|
645
699
|
return;
|
|
646
700
|
}
|
|
647
|
-
throw
|
|
648
|
-
|
|
701
|
+
throw contractError(
|
|
702
|
+
'CONTRACT.PACK_MISSING',
|
|
703
|
+
`${context} references contract space "${spaceId}" but "${spaceId}" is not declared in extensions. Add the pack to extensions.`,
|
|
704
|
+
{ meta: { spaceId, context } },
|
|
649
705
|
);
|
|
650
706
|
}
|
|
651
707
|
|
|
652
708
|
function resolveForeignKeyNodes(
|
|
653
709
|
spec: RuntimeModelSpec,
|
|
654
710
|
allSpecs: ReadonlyMap<string, RuntimeModelSpec>,
|
|
655
|
-
|
|
711
|
+
extensions?: Record<string, ExtensionPackRef<'sql', string>>,
|
|
656
712
|
): readonly ForeignKeyNode[] {
|
|
657
713
|
const relationForeignKeys = resolveRelationForeignKeys(spec, allSpecs).map((foreignKey) => {
|
|
658
714
|
// F-relfk: relation-derived FKs for cross-space targets carry targetSpaceId;
|
|
659
715
|
// route them through the cross-space path, just like explicit sql() FKs.
|
|
660
716
|
if (foreignKey.targetSpaceId !== undefined) {
|
|
661
717
|
assertKnownExtensionPack(
|
|
662
|
-
|
|
718
|
+
extensions,
|
|
663
719
|
foreignKey.targetSpaceId,
|
|
664
720
|
`Relation-derived foreign key on "${spec.modelName}"`,
|
|
665
721
|
);
|
|
@@ -671,8 +727,10 @@ function resolveForeignKeyNodes(
|
|
|
671
727
|
|
|
672
728
|
const targetSpec = allSpecs.get(foreignKey.targetModel);
|
|
673
729
|
if (!targetSpec) {
|
|
674
|
-
throw
|
|
730
|
+
throw contractError(
|
|
731
|
+
'CONTRACT.MODEL_UNKNOWN',
|
|
675
732
|
`Foreign key on "${spec.modelName}" references unknown model "${foreignKey.targetModel}"`,
|
|
733
|
+
{ meta: { sourceModel: spec.modelName, targetModel: foreignKey.targetModel } },
|
|
676
734
|
);
|
|
677
735
|
}
|
|
678
736
|
|
|
@@ -682,7 +740,7 @@ function resolveForeignKeyNodes(
|
|
|
682
740
|
const sqlForeignKeys = (spec.sqlSpec?.foreignKeys ?? []).map((foreignKey) => {
|
|
683
741
|
if (foreignKey.targetSpaceId !== undefined) {
|
|
684
742
|
assertKnownExtensionPack(
|
|
685
|
-
|
|
743
|
+
extensions,
|
|
686
744
|
foreignKey.targetSpaceId,
|
|
687
745
|
`Foreign key on "${spec.modelName}"`,
|
|
688
746
|
);
|
|
@@ -694,8 +752,10 @@ function resolveForeignKeyNodes(
|
|
|
694
752
|
|
|
695
753
|
const targetSpec = allSpecs.get(foreignKey.targetModel);
|
|
696
754
|
if (!targetSpec) {
|
|
697
|
-
throw
|
|
755
|
+
throw contractError(
|
|
756
|
+
'CONTRACT.MODEL_UNKNOWN',
|
|
698
757
|
`Foreign key on "${spec.modelName}" references unknown model "${foreignKey.targetModel}"`,
|
|
758
|
+
{ meta: { sourceModel: spec.modelName, targetModel: foreignKey.targetModel } },
|
|
699
759
|
);
|
|
700
760
|
}
|
|
701
761
|
|
|
@@ -710,7 +770,7 @@ function resolveModelNode(
|
|
|
710
770
|
allSpecs: ReadonlyMap<string, RuntimeModelSpec>,
|
|
711
771
|
storageTypes: Record<string, StorageTypeInstance>,
|
|
712
772
|
storageTypeReverseLookup: ReadonlyMap<StorageTypeInstance, string>,
|
|
713
|
-
|
|
773
|
+
extensions?: Record<string, ExtensionPackRef<'sql', string>>,
|
|
714
774
|
): ModelNode {
|
|
715
775
|
const fields: FieldNode[] = [];
|
|
716
776
|
|
|
@@ -725,7 +785,7 @@ function resolveModelNode(
|
|
|
725
785
|
);
|
|
726
786
|
const columnName = spec.fieldToColumn[fieldName];
|
|
727
787
|
if (!columnName) {
|
|
728
|
-
throw new
|
|
788
|
+
throw new InternalError(`Column name resolution failed for "${spec.modelName}.${fieldName}"`);
|
|
729
789
|
}
|
|
730
790
|
|
|
731
791
|
const enumHandle =
|
|
@@ -756,9 +816,9 @@ function resolveModelNode(
|
|
|
756
816
|
...ifDefined('type', index.type),
|
|
757
817
|
...ifDefined('options', index.options),
|
|
758
818
|
})) satisfies readonly IndexNode[];
|
|
759
|
-
const foreignKeys = resolveForeignKeyNodes(spec, allSpecs,
|
|
819
|
+
const foreignKeys = resolveForeignKeyNodes(spec, allSpecs, extensions);
|
|
760
820
|
const relations = Object.entries(spec.relations).map(([relationName, relationBuilder]) =>
|
|
761
|
-
resolveRelationNode(relationName, relationBuilder.build(), spec, allSpecs,
|
|
821
|
+
resolveRelationNode(relationName, relationBuilder.build(), spec, allSpecs, extensions),
|
|
762
822
|
);
|
|
763
823
|
|
|
764
824
|
return {
|
|
@@ -798,8 +858,10 @@ function collectRuntimeModelSpecs(definition: ContractInput): RuntimeCollection
|
|
|
798
858
|
for (const [modelName, modelDefinition] of Object.entries(models)) {
|
|
799
859
|
const tokenModelName = modelDefinition.stageOne.modelName;
|
|
800
860
|
if (tokenModelName && tokenModelName !== modelName) {
|
|
801
|
-
throw
|
|
861
|
+
throw contractError(
|
|
862
|
+
'CONTRACT.MODEL_TOKEN_INVALID',
|
|
802
863
|
`Model token "${tokenModelName}" must be assigned to models.${tokenModelName}. Received models.${modelName}.`,
|
|
864
|
+
{ meta: { tokenModelName, assignedKey: modelName } },
|
|
803
865
|
);
|
|
804
866
|
}
|
|
805
867
|
|
|
@@ -812,8 +874,10 @@ function collectRuntimeModelSpecs(definition: ContractInput): RuntimeCollection
|
|
|
812
874
|
const tableKey = JSON.stringify([namespaceId, tableName]);
|
|
813
875
|
const existingModel = tableOwners.get(tableKey);
|
|
814
876
|
if (existingModel) {
|
|
815
|
-
throw
|
|
877
|
+
throw contractError(
|
|
878
|
+
'CONTRACT.NAME_DUPLICATE',
|
|
816
879
|
`Models "${existingModel}" and "${modelName}" both map to table "${tableName}".`,
|
|
880
|
+
{ meta: { kind: 'table', name: tableName, first: existingModel, second: modelName } },
|
|
817
881
|
);
|
|
818
882
|
}
|
|
819
883
|
tableOwners.set(tableKey, modelName);
|
|
@@ -827,8 +891,10 @@ function collectRuntimeModelSpecs(definition: ContractInput): RuntimeCollection
|
|
|
827
891
|
fieldState.columnName ?? applyNaming(fieldName, definition.naming?.columns);
|
|
828
892
|
const existingField = columnOwners.get(columnName);
|
|
829
893
|
if (existingField) {
|
|
830
|
-
throw
|
|
894
|
+
throw contractError(
|
|
895
|
+
'CONTRACT.NAME_DUPLICATE',
|
|
831
896
|
`Model "${modelName}" maps both "${existingField}" and "${fieldName}" to column "${columnName}".`,
|
|
897
|
+
{ meta: { kind: 'column', name: columnName, first: existingField, second: fieldName } },
|
|
832
898
|
);
|
|
833
899
|
}
|
|
834
900
|
columnOwners.set(columnName, fieldName);
|
|
@@ -859,7 +925,7 @@ function collectRuntimeModelSpecs(definition: ContractInput): RuntimeCollection
|
|
|
859
925
|
|
|
860
926
|
function lowerModels(
|
|
861
927
|
collection: RuntimeCollection,
|
|
862
|
-
|
|
928
|
+
extensions?: Record<string, ExtensionPackRef<'sql', string>>,
|
|
863
929
|
): readonly ModelNode[] {
|
|
864
930
|
emitTypedCrossModelFallbackWarnings(collection);
|
|
865
931
|
|
|
@@ -870,7 +936,7 @@ function lowerModels(
|
|
|
870
936
|
collection.modelSpecs,
|
|
871
937
|
collection.storageTypes,
|
|
872
938
|
storageTypeReverseLookup,
|
|
873
|
-
|
|
939
|
+
extensions,
|
|
874
940
|
),
|
|
875
941
|
);
|
|
876
942
|
}
|
|
@@ -906,7 +972,7 @@ function lowerPackEntityHandles(
|
|
|
906
972
|
readonly authoring?: import('@prisma-next/framework-components/authoring').AuthoringContributions;
|
|
907
973
|
}[] = [
|
|
908
974
|
definition.target,
|
|
909
|
-
...Object.values<ExtensionPackRef<'sql', string>>(definition.
|
|
975
|
+
...Object.values<ExtensionPackRef<'sql', string>>(definition.extensions ?? {}),
|
|
910
976
|
];
|
|
911
977
|
const owningComponent = new Map<string, (typeof components)[number]>();
|
|
912
978
|
const walkEntityTypes = (
|
|
@@ -976,8 +1042,10 @@ function lowerPackEntityHandles(
|
|
|
976
1042
|
for (const handle of entities) {
|
|
977
1043
|
const component = owningComponent.get(handle.entityKind);
|
|
978
1044
|
if (component === undefined) {
|
|
979
|
-
throw
|
|
1045
|
+
throw contractError(
|
|
1046
|
+
'CONTRACT.ENTITY_KIND_UNKNOWN',
|
|
980
1047
|
`defineContract: entities contains a handle with entityKind "${handle.entityKind}", which no composed pack registers. Compose a pack whose entityTypes contribution claims "${handle.entityKind}", or remove the handle.`,
|
|
1048
|
+
{ meta: { entityKind: handle.entityKind } },
|
|
981
1049
|
);
|
|
982
1050
|
}
|
|
983
1051
|
const refs: Record<string, ResolvedEntityHandleRef> = {};
|
|
@@ -994,8 +1062,10 @@ function lowerPackEntityHandles(
|
|
|
994
1062
|
const authoring = component.authoring;
|
|
995
1063
|
if (!providesEntityHandleLowering(authoring)) {
|
|
996
1064
|
const kinds = [...new Set(handles.map((entry) => entry.handle.entityKind))].sort();
|
|
997
|
-
throw
|
|
1065
|
+
throw contractError(
|
|
1066
|
+
'CONTRACT.PACK_CONTRIBUTION_INVALID',
|
|
998
1067
|
`defineContract: entityKind(s) ${kinds.map((kind) => `"${kind}"`).join(', ')} are registered by a pack that does not implement entity-handle lowering (no lowerEntityHandles on its authoring contributions).`,
|
|
1068
|
+
{ meta: { entityKinds: kinds, reason: 'missing-lowerEntityHandles' } },
|
|
999
1069
|
);
|
|
1000
1070
|
}
|
|
1001
1071
|
for (const row of authoring.lowerEntityHandles({ handles, defaultNamespaceId })) {
|
|
@@ -1005,8 +1075,10 @@ function lowerPackEntityHandles(
|
|
|
1005
1075
|
forNamespace[row.entityKind] = forKind;
|
|
1006
1076
|
const existing = forKind[row.key];
|
|
1007
1077
|
if (existing !== undefined && existing !== row.entity) {
|
|
1008
|
-
throw
|
|
1078
|
+
throw contractError(
|
|
1079
|
+
'CONTRACT.NAME_DUPLICATE',
|
|
1009
1080
|
`defineContract: two different "${row.entityKind}" entities named "${row.key}" in namespace "${row.namespaceId}" — pack-entity names must be unique per namespace.`,
|
|
1081
|
+
{ meta: { kind: row.entityKind, name: row.key, namespaceId: row.namespaceId } },
|
|
1010
1082
|
);
|
|
1011
1083
|
}
|
|
1012
1084
|
forKind[row.key] = row.entity;
|
|
@@ -1017,13 +1089,13 @@ function lowerPackEntityHandles(
|
|
|
1017
1089
|
|
|
1018
1090
|
export function buildContractDefinition(definition: ContractInput): ContractDefinition {
|
|
1019
1091
|
const collection = collectRuntimeModelSpecs(definition);
|
|
1020
|
-
const models = lowerModels(collection, definition.
|
|
1092
|
+
const models = lowerModels(collection, definition.extensions);
|
|
1021
1093
|
const attachedEntities = lowerPackEntityHandles(definition, collection.modelSpecs);
|
|
1022
1094
|
|
|
1023
1095
|
return {
|
|
1024
1096
|
target: definition.target,
|
|
1025
1097
|
...ifDefined('defaultControlPolicy', definition.defaultControlPolicy),
|
|
1026
|
-
...(definition.
|
|
1098
|
+
...(definition.extensions ? { extensions: definition.extensions } : {}),
|
|
1027
1099
|
...(definition.storageHash ? { storageHash: definition.storageHash } : {}),
|
|
1028
1100
|
...(definition.foreignKeyDefaults ? { foreignKeyDefaults: definition.foreignKeyDefaults } : {}),
|
|
1029
1101
|
...(Object.keys(collection.storageTypes).length > 0
|
package/src/contract-types.ts
CHANGED
|
@@ -67,8 +67,8 @@ export type MergeExtensionPackRefs<
|
|
|
67
67
|
Added extends Record<string, ExtensionPackRef<'sql', string>>,
|
|
68
68
|
> = Existing extends Record<string, unknown> ? Existing & Added : Added;
|
|
69
69
|
|
|
70
|
-
type
|
|
71
|
-
readonly
|
|
70
|
+
type DefinitionExtensions<Definition> = Definition extends {
|
|
71
|
+
readonly extensions?: infer Packs extends Record<string, ExtensionPackRef<'sql', string>>;
|
|
72
72
|
}
|
|
73
73
|
? Packs
|
|
74
74
|
: Record<never, never>;
|
|
@@ -101,7 +101,7 @@ type DerivedCapabilities<Definition> = Defaulted<
|
|
|
101
101
|
ExtractPackCapabilities<DefinitionTarget<Definition>>,
|
|
102
102
|
Record<string, never>
|
|
103
103
|
> &
|
|
104
|
-
MergeExtensionPackCapabilities<
|
|
104
|
+
MergeExtensionPackCapabilities<DefinitionExtensions<Definition>>;
|
|
105
105
|
|
|
106
106
|
type DefinitionTargetId<Definition> = Definition extends {
|
|
107
107
|
readonly target: TargetPackRef<'sql', infer Target>;
|
|
@@ -114,13 +114,13 @@ type Present<T> = Exclude<T, undefined>;
|
|
|
114
114
|
type CodecTypesFromDefinition<Definition> = ExtractCodecTypesFromPack<
|
|
115
115
|
Definition extends { readonly target: infer Target } ? Target : never
|
|
116
116
|
> &
|
|
117
|
-
MergeExtensionCodecTypesSafe<
|
|
117
|
+
MergeExtensionCodecTypesSafe<DefinitionExtensions<Definition>>;
|
|
118
118
|
|
|
119
119
|
type DefinitionTarget<Definition> = Definition extends { readonly target: infer Target }
|
|
120
120
|
? Target
|
|
121
121
|
: never;
|
|
122
122
|
|
|
123
|
-
type AllPacks<Definition> =
|
|
123
|
+
type AllPacks<Definition> = DefinitionExtensions<Definition> & {
|
|
124
124
|
readonly __target: DefinitionTarget<Definition>;
|
|
125
125
|
};
|
|
126
126
|
|
|
@@ -799,9 +799,9 @@ export type SqlContractResult<Definition> = ContractWithTypeMaps<
|
|
|
799
799
|
readonly namespaces: Readonly<Record<string, BuiltDomainNamespace<Definition>>>;
|
|
800
800
|
} & BuiltDomain<Definition>;
|
|
801
801
|
} & {
|
|
802
|
-
readonly
|
|
802
|
+
readonly extensions: keyof DefinitionExtensions<Definition> extends never
|
|
803
803
|
? Record<string, never>
|
|
804
|
-
:
|
|
804
|
+
: DefinitionExtensions<Definition>;
|
|
805
805
|
readonly capabilities: DerivedCapabilities<Definition>;
|
|
806
806
|
readonly enumAccessors: BuiltEnumAccessors<Definition>;
|
|
807
807
|
},
|