@prisma-next/sql-contract-ts 0.16.0-dev.5 → 0.16.0-dev.7
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/dist/{build-contract-CAX6inbk.mjs → build-contract-DWRZ3jRz.mjs} +80 -18
- package/dist/build-contract-DWRZ3jRz.mjs.map +1 -0
- package/dist/config-types.d.mts.map +1 -1
- package/dist/config-types.mjs +4 -3
- package/dist/config-types.mjs.map +1 -1
- package/dist/contract-builder.d.mts.map +1 -1
- package/dist/contract-builder.mjs +220 -63
- package/dist/contract-builder.mjs.map +1 -1
- package/package.json +10 -10
- package/src/authoring-helper-runtime.ts +16 -3
- package/src/build-contract.ts +81 -16
- package/src/composed-authoring-helpers.ts +7 -2
- package/src/config-types.ts +6 -2
- package/src/contract-builder.ts +70 -15
- package/src/contract-dsl.ts +57 -13
- package/src/contract-errors.ts +39 -0
- package/src/contract-lowering.ts +98 -26
- package/dist/build-contract-CAX6inbk.mjs.map +0 -1
package/src/contract-dsl.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
24
24
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
25
25
|
import type { NamedConstraintSpec } from './authoring-type-utils';
|
|
26
|
+
import { contractError } from './contract-errors';
|
|
26
27
|
import type { EnumTypeHandle } from './enum-type';
|
|
27
28
|
import { isEnumTypeHandle } from './enum-type';
|
|
28
29
|
|
|
@@ -402,10 +403,14 @@ export class ScalarFieldBuilder<State extends AnyScalarFieldState = AnyScalarFie
|
|
|
402
403
|
const uniqueSpec = 'unique' in spec ? spec.unique : undefined;
|
|
403
404
|
|
|
404
405
|
if (idSpec && !this.state.id) {
|
|
405
|
-
throw
|
|
406
|
+
throw contractError(
|
|
407
|
+
'CONTRACT.ARGUMENT_INVALID',
|
|
408
|
+
'field.sql({ id }) requires an existing inline .id(...) declaration.',
|
|
409
|
+
);
|
|
406
410
|
}
|
|
407
411
|
if (uniqueSpec && !this.state.unique) {
|
|
408
|
-
throw
|
|
412
|
+
throw contractError(
|
|
413
|
+
'CONTRACT.ARGUMENT_INVALID',
|
|
409
414
|
'field.sql({ unique }) requires an existing inline .unique(...) declaration.',
|
|
410
415
|
);
|
|
411
416
|
}
|
|
@@ -457,8 +462,10 @@ export class EnumScalarFieldBuilder<
|
|
|
457
462
|
}
|
|
458
463
|
|
|
459
464
|
override defaultSql(_expression: never): never {
|
|
460
|
-
throw
|
|
465
|
+
throw contractError(
|
|
466
|
+
'CONTRACT.DEFAULT_INVALID',
|
|
461
467
|
'defaultSql is not available on an enum field; use .default(members.X) instead',
|
|
468
|
+
{ meta: { reason: 'defaultSql-on-enum-field' } },
|
|
462
469
|
);
|
|
463
470
|
}
|
|
464
471
|
}
|
|
@@ -674,7 +681,11 @@ export class RelationBuilder<State extends RelationState = AnyRelationState> {
|
|
|
674
681
|
spec: SqlSpec,
|
|
675
682
|
): RelationBuilder<ApplyBelongsToRelationSqlSpec<State, SqlSpec>> {
|
|
676
683
|
if (this.state.kind !== 'belongsTo') {
|
|
677
|
-
throw
|
|
684
|
+
throw contractError(
|
|
685
|
+
'CONTRACT.RELATION_INVALID',
|
|
686
|
+
'relation.sql(...) is only supported for belongsTo relations.',
|
|
687
|
+
{ meta: { relationKind: this.state.kind } },
|
|
688
|
+
);
|
|
678
689
|
}
|
|
679
690
|
|
|
680
691
|
return new RelationBuilder({
|
|
@@ -860,27 +871,45 @@ function normalizeTargetFieldRefInput(input: TargetFieldRef | readonly TargetFie
|
|
|
860
871
|
const refs = Array.isArray(input) ? input : [input];
|
|
861
872
|
const [first] = refs;
|
|
862
873
|
if (!first) {
|
|
863
|
-
throw
|
|
874
|
+
throw contractError('CONTRACT.FOREIGN_KEY_INVALID', 'Expected at least one target ref', {
|
|
875
|
+
meta: { reason: 'empty-target-refs' },
|
|
876
|
+
});
|
|
864
877
|
}
|
|
865
878
|
if (refs.some((ref) => ref.modelName !== first.modelName)) {
|
|
866
|
-
throw
|
|
879
|
+
throw contractError(
|
|
880
|
+
'CONTRACT.FOREIGN_KEY_INVALID',
|
|
881
|
+
'All target refs in a foreign key must point to the same model',
|
|
882
|
+
{ meta: { mismatch: 'modelName', models: refs.map((ref) => ref.modelName) } },
|
|
883
|
+
);
|
|
867
884
|
}
|
|
868
885
|
// F-compound: all refs in a compound FK must share the same cross-space coordinate.
|
|
869
886
|
// A mismatch in spaceId, namespaceId, or tableName means the refs come from
|
|
870
887
|
// different spaces despite having the same modelName — an impossible FK.
|
|
871
888
|
if (refs.some((ref) => ref.spaceId !== first.spaceId)) {
|
|
872
|
-
throw
|
|
889
|
+
throw contractError(
|
|
890
|
+
'CONTRACT.FOREIGN_KEY_INVALID',
|
|
873
891
|
`All target refs in a compound foreign key must share the same spaceId (found mismatch: "${first.spaceId ?? '<local>'}" vs "${refs.find((r) => r.spaceId !== first.spaceId)?.spaceId ?? '<local>'}")`,
|
|
892
|
+
{
|
|
893
|
+
meta: {
|
|
894
|
+
mismatch: 'spaceId',
|
|
895
|
+
first: first.spaceId,
|
|
896
|
+
second: refs.find((r) => r.spaceId !== first.spaceId)?.spaceId,
|
|
897
|
+
},
|
|
898
|
+
},
|
|
874
899
|
);
|
|
875
900
|
}
|
|
876
901
|
if (refs.some((ref) => ref.namespaceId !== first.namespaceId)) {
|
|
877
|
-
throw
|
|
902
|
+
throw contractError(
|
|
903
|
+
'CONTRACT.FOREIGN_KEY_INVALID',
|
|
878
904
|
'All target refs in a compound foreign key must share the same namespaceId (found mismatch)',
|
|
905
|
+
{ meta: { mismatch: 'namespaceId' } },
|
|
879
906
|
);
|
|
880
907
|
}
|
|
881
908
|
if (refs.some((ref) => ref.tableName !== first.tableName)) {
|
|
882
|
-
throw
|
|
909
|
+
throw contractError(
|
|
910
|
+
'CONTRACT.FOREIGN_KEY_INVALID',
|
|
883
911
|
'All target refs in a compound foreign key must share the same tableName (found mismatch)',
|
|
912
|
+
{ meta: { mismatch: 'tableName' } },
|
|
884
913
|
);
|
|
885
914
|
}
|
|
886
915
|
return {
|
|
@@ -1322,7 +1351,10 @@ export class ContractModelBuilder<
|
|
|
1322
1351
|
): TargetFieldRef<ModelName & string, FieldName> {
|
|
1323
1352
|
const modelName = this.stageOne.modelName;
|
|
1324
1353
|
if (!modelName) {
|
|
1325
|
-
throw
|
|
1354
|
+
throw contractError(
|
|
1355
|
+
'CONTRACT.MODEL_TOKEN_INVALID',
|
|
1356
|
+
'Model tokens require model("ModelName", ...) before calling .ref(...)',
|
|
1357
|
+
);
|
|
1326
1358
|
}
|
|
1327
1359
|
|
|
1328
1360
|
return {
|
|
@@ -1346,8 +1378,16 @@ export class ContractModelBuilder<
|
|
|
1346
1378
|
> {
|
|
1347
1379
|
const duplicateRelationName = findDuplicateRelationName(this.stageOne.relations, relations);
|
|
1348
1380
|
if (duplicateRelationName) {
|
|
1349
|
-
throw
|
|
1381
|
+
throw contractError(
|
|
1382
|
+
'CONTRACT.NAME_DUPLICATE',
|
|
1350
1383
|
`Model "${this.stageOne.modelName ?? '<anonymous>'}" already defines relation "${duplicateRelationName}".`,
|
|
1384
|
+
{
|
|
1385
|
+
meta: {
|
|
1386
|
+
kind: 'relation',
|
|
1387
|
+
name: duplicateRelationName,
|
|
1388
|
+
modelName: this.stageOne.modelName,
|
|
1389
|
+
},
|
|
1390
|
+
},
|
|
1351
1391
|
);
|
|
1352
1392
|
}
|
|
1353
1393
|
|
|
@@ -1501,7 +1541,8 @@ function resolveNamedModelTokenName(token: {
|
|
|
1501
1541
|
}): string {
|
|
1502
1542
|
const modelName = token.stageOne.modelName;
|
|
1503
1543
|
if (!modelName) {
|
|
1504
|
-
throw
|
|
1544
|
+
throw contractError(
|
|
1545
|
+
'CONTRACT.MODEL_TOKEN_INVALID',
|
|
1505
1546
|
'Relation targets require named model tokens. Use model("ModelName", ...) before passing a token to rel.*(...).',
|
|
1506
1547
|
);
|
|
1507
1548
|
}
|
|
@@ -1668,7 +1709,10 @@ export function model<
|
|
|
1668
1709
|
const input = typeof modelNameOrInput === 'string' ? maybeInput : modelNameOrInput;
|
|
1669
1710
|
|
|
1670
1711
|
if (!input) {
|
|
1671
|
-
throw
|
|
1712
|
+
throw contractError(
|
|
1713
|
+
'CONTRACT.ARGUMENT_INVALID',
|
|
1714
|
+
'model("ModelName", ...) requires a model definition.',
|
|
1715
|
+
);
|
|
1672
1716
|
}
|
|
1673
1717
|
|
|
1674
1718
|
return new ContractModelBuilder({
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { StructuredError, StructuredErrorOptions } from '@prisma-next/utils/structured-error';
|
|
2
|
+
import { structuredError } from '@prisma-next/utils/structured-error';
|
|
3
|
+
|
|
4
|
+
export type ContractCode = `CONTRACT.${ContractSubcode}`;
|
|
5
|
+
|
|
6
|
+
type ContractSubcode =
|
|
7
|
+
| 'VALIDATION_FAILED'
|
|
8
|
+
| 'NAME_DUPLICATE'
|
|
9
|
+
| 'MODEL_UNKNOWN'
|
|
10
|
+
| 'PACK_CONTRIBUTION_INVALID'
|
|
11
|
+
| 'PACK_FAMILY_MISMATCH'
|
|
12
|
+
| 'PACK_TARGET_MISMATCH'
|
|
13
|
+
| 'PACK_REF_INVALID'
|
|
14
|
+
| 'PACK_MISSING'
|
|
15
|
+
| 'NAMESPACE_INVALID'
|
|
16
|
+
| 'NAMESPACE_UNSUPPORTED'
|
|
17
|
+
| 'NAMESPACE_UNKNOWN'
|
|
18
|
+
| 'ARGUMENT_INVALID'
|
|
19
|
+
| 'FOREIGN_KEY_INVALID'
|
|
20
|
+
| 'RELATION_INVALID'
|
|
21
|
+
| 'IDENTITY_INVALID'
|
|
22
|
+
| 'CONSTRAINT_INVALID'
|
|
23
|
+
| 'DEFAULT_INVALID'
|
|
24
|
+
| 'ENUM_INVALID'
|
|
25
|
+
| 'TYPE_UNKNOWN'
|
|
26
|
+
| 'FIELD_UNKNOWN'
|
|
27
|
+
| 'MODEL_TOKEN_INVALID'
|
|
28
|
+
| 'MODULE_EXPORT_MISSING'
|
|
29
|
+
| 'ENTITY_KIND_UNKNOWN'
|
|
30
|
+
| 'ENTITY_KIND_INVALID'
|
|
31
|
+
| 'TABLE_MISMATCH';
|
|
32
|
+
|
|
33
|
+
export function contractError(
|
|
34
|
+
code: ContractCode,
|
|
35
|
+
message: string,
|
|
36
|
+
options?: StructuredErrorOptions,
|
|
37
|
+
): StructuredError {
|
|
38
|
+
return structuredError(code, message, options);
|
|
39
|
+
}
|
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
|
|
|
@@ -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
|
|
|
@@ -644,8 +698,10 @@ function assertKnownExtensionPack(
|
|
|
644
698
|
if (extensionPacks !== undefined && Object.hasOwn(extensionPacks, spaceId)) {
|
|
645
699
|
return;
|
|
646
700
|
}
|
|
647
|
-
throw
|
|
701
|
+
throw contractError(
|
|
702
|
+
'CONTRACT.PACK_MISSING',
|
|
648
703
|
`${context} references contract space "${spaceId}" but "${spaceId}" is not declared in extensionPacks. Add the pack to extensionPacks.`,
|
|
704
|
+
{ meta: { spaceId, context } },
|
|
649
705
|
);
|
|
650
706
|
}
|
|
651
707
|
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -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 =
|
|
@@ -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);
|
|
@@ -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;
|