@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
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { t as buildSqlContractFromDefinition } from "./build-contract-
|
|
1
|
+
import { n as contractError, t as buildSqlContractFromDefinition } from "./build-contract-DbUeVkiu.mjs";
|
|
2
2
|
import { blindCast } from "@prisma-next/utils/casts";
|
|
3
3
|
import { ifDefined } from "@prisma-next/utils/defined";
|
|
4
4
|
import { isColumnDefault } from "@prisma-next/contract/types";
|
|
5
5
|
import { bindEnumType, createEntityHelpersFromNamespace, enumType, isEnumTypeHandle, member } from "@prisma-next/contract-authoring";
|
|
6
6
|
import { assertNoCrossRegistryCollisions, instantiateAuthoringFieldPreset, instantiateAuthoringTypeConstructor, isAuthoringEntityTypeDescriptor, isAuthoringFieldPresetDescriptor, isAuthoringTypeConstructorDescriptor, mergeAuthoringNamespaces, validateAuthoringHelperArguments } from "@prisma-next/framework-components/authoring";
|
|
7
7
|
import { toStorageTypeInstance } from "@prisma-next/sql-contract/types";
|
|
8
|
+
import { InternalError } from "@prisma-next/utils/internal-error";
|
|
8
9
|
import { providesEntityHandleLowering } from "@prisma-next/sql-contract/entity-handle-lowering-hook";
|
|
9
10
|
//#region src/authoring-helper-runtime.ts
|
|
10
11
|
function isNamedConstraintOptionsLike(value) {
|
|
@@ -19,7 +20,10 @@ const blockedSegments = /* @__PURE__ */ new Set([
|
|
|
19
20
|
"prototype"
|
|
20
21
|
]);
|
|
21
22
|
function assertSafeHelperKey(key, path) {
|
|
22
|
-
if (blockedSegments.has(key)) throw
|
|
23
|
+
if (blockedSegments.has(key)) throw contractError("CONTRACT.PACK_CONTRIBUTION_INVALID", `Invalid authoring helper "${[...path, key].join(".")}". Helper path segments must not use "${key}".`, { meta: {
|
|
24
|
+
helperPath: [...path, key].join("."),
|
|
25
|
+
segment: key
|
|
26
|
+
} });
|
|
23
27
|
}
|
|
24
28
|
function createTypeHelpersFromNamespace(namespace, path = []) {
|
|
25
29
|
const helpers = {};
|
|
@@ -47,12 +51,16 @@ function createFieldPresetHelper(options) {
|
|
|
47
51
|
return (...rawArgs) => {
|
|
48
52
|
const acceptsNamedConstraintOptions = options.descriptor.output.id === true || options.descriptor.output.unique === true;
|
|
49
53
|
const declaredArguments = options.descriptor.args ?? [];
|
|
50
|
-
if (acceptsNamedConstraintOptions && rawArgs.length > declaredArguments.length + 1) throw
|
|
54
|
+
if (acceptsNamedConstraintOptions && rawArgs.length > declaredArguments.length + 1) throw contractError("CONTRACT.ARGUMENT_INVALID", `${options.helperPath} expects at most ${declaredArguments.length + 1} argument(s), received ${rawArgs.length}`, { meta: {
|
|
55
|
+
helperPath: options.helperPath,
|
|
56
|
+
expected: declaredArguments.length + 1,
|
|
57
|
+
received: rawArgs.length
|
|
58
|
+
} });
|
|
51
59
|
let args = rawArgs;
|
|
52
60
|
let namedConstraintOptions;
|
|
53
61
|
if (acceptsNamedConstraintOptions && rawArgs.length === declaredArguments.length + 1) {
|
|
54
62
|
const maybeNamedConstraintOptions = rawArgs.at(-1);
|
|
55
|
-
if (!isNamedConstraintOptionsLike(maybeNamedConstraintOptions)) throw
|
|
63
|
+
if (!isNamedConstraintOptionsLike(maybeNamedConstraintOptions)) throw contractError("CONTRACT.ARGUMENT_INVALID", `${options.helperPath} accepts an optional trailing { name?: string } constraint options object`, { meta: { helperPath: options.helperPath } });
|
|
56
64
|
namedConstraintOptions = maybeNamedConstraintOptions;
|
|
57
65
|
args = rawArgs.slice(0, -1);
|
|
58
66
|
}
|
|
@@ -150,8 +158,8 @@ var ScalarFieldBuilder = class ScalarFieldBuilder {
|
|
|
150
158
|
sql(spec) {
|
|
151
159
|
const idSpec = "id" in spec ? spec.id : void 0;
|
|
152
160
|
const uniqueSpec = "unique" in spec ? spec.unique : void 0;
|
|
153
|
-
if (idSpec && !this.state.id) throw
|
|
154
|
-
if (uniqueSpec && !this.state.unique) throw
|
|
161
|
+
if (idSpec && !this.state.id) throw contractError("CONTRACT.ARGUMENT_INVALID", "field.sql({ id }) requires an existing inline .id(...) declaration.");
|
|
162
|
+
if (uniqueSpec && !this.state.unique) throw contractError("CONTRACT.ARGUMENT_INVALID", "field.sql({ unique }) requires an existing inline .unique(...) declaration.");
|
|
155
163
|
return new ScalarFieldBuilder(blindCast({
|
|
156
164
|
...this.state,
|
|
157
165
|
...spec.column ? { columnName: spec.column } : {},
|
|
@@ -179,7 +187,7 @@ var EnumScalarFieldBuilder = class EnumScalarFieldBuilder extends ScalarFieldBui
|
|
|
179
187
|
}, this.#handle));
|
|
180
188
|
}
|
|
181
189
|
defaultSql(_expression) {
|
|
182
|
-
throw
|
|
190
|
+
throw contractError("CONTRACT.DEFAULT_INVALID", "defaultSql is not available on an enum field; use .default(members.X) instead", { meta: { reason: "defaultSql-on-enum-field" } });
|
|
183
191
|
}
|
|
184
192
|
};
|
|
185
193
|
function columnField(descriptor) {
|
|
@@ -230,7 +238,7 @@ var RelationBuilder = class RelationBuilder {
|
|
|
230
238
|
this.state = state;
|
|
231
239
|
}
|
|
232
240
|
sql(spec) {
|
|
233
|
-
if (this.state.kind !== "belongsTo") throw
|
|
241
|
+
if (this.state.kind !== "belongsTo") throw contractError("CONTRACT.RELATION_INVALID", "relation.sql(...) is only supported for belongsTo relations.", { meta: { relationKind: this.state.kind } });
|
|
234
242
|
return new RelationBuilder({
|
|
235
243
|
...this.state,
|
|
236
244
|
sql: spec
|
|
@@ -246,11 +254,18 @@ function normalizeFieldRefInput(input) {
|
|
|
246
254
|
function normalizeTargetFieldRefInput(input) {
|
|
247
255
|
const refs = Array.isArray(input) ? input : [input];
|
|
248
256
|
const [first] = refs;
|
|
249
|
-
if (!first) throw
|
|
250
|
-
if (refs.some((ref) => ref.modelName !== first.modelName)) throw
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
257
|
+
if (!first) throw contractError("CONTRACT.FOREIGN_KEY_INVALID", "Expected at least one target ref", { meta: { reason: "empty-target-refs" } });
|
|
258
|
+
if (refs.some((ref) => ref.modelName !== first.modelName)) throw contractError("CONTRACT.FOREIGN_KEY_INVALID", "All target refs in a foreign key must point to the same model", { meta: {
|
|
259
|
+
mismatch: "modelName",
|
|
260
|
+
models: refs.map((ref) => ref.modelName)
|
|
261
|
+
} });
|
|
262
|
+
if (refs.some((ref) => ref.spaceId !== first.spaceId)) throw contractError("CONTRACT.FOREIGN_KEY_INVALID", `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>"}")`, { meta: {
|
|
263
|
+
mismatch: "spaceId",
|
|
264
|
+
first: first.spaceId,
|
|
265
|
+
second: refs.find((r) => r.spaceId !== first.spaceId)?.spaceId
|
|
266
|
+
} });
|
|
267
|
+
if (refs.some((ref) => ref.namespaceId !== first.namespaceId)) throw contractError("CONTRACT.FOREIGN_KEY_INVALID", "All target refs in a compound foreign key must share the same namespaceId (found mismatch)", { meta: { mismatch: "namespaceId" } });
|
|
268
|
+
if (refs.some((ref) => ref.tableName !== first.tableName)) throw contractError("CONTRACT.FOREIGN_KEY_INVALID", "All target refs in a compound foreign key must share the same tableName (found mismatch)", { meta: { mismatch: "tableName" } });
|
|
254
269
|
return {
|
|
255
270
|
modelName: first.modelName,
|
|
256
271
|
fieldNames: refs.map((ref) => ref.columnName ?? ref.fieldName),
|
|
@@ -392,7 +407,7 @@ var ContractModelBuilder = class ContractModelBuilder {
|
|
|
392
407
|
}
|
|
393
408
|
ref(fieldName) {
|
|
394
409
|
const modelName = this.stageOne.modelName;
|
|
395
|
-
if (!modelName) throw
|
|
410
|
+
if (!modelName) throw contractError("CONTRACT.MODEL_TOKEN_INVALID", "Model tokens require model(\"ModelName\", ...) before calling .ref(...)");
|
|
396
411
|
return {
|
|
397
412
|
kind: "targetFieldRef",
|
|
398
413
|
source: "token",
|
|
@@ -402,7 +417,11 @@ var ContractModelBuilder = class ContractModelBuilder {
|
|
|
402
417
|
}
|
|
403
418
|
relations(relations) {
|
|
404
419
|
const duplicateRelationName = findDuplicateRelationName(this.stageOne.relations, relations);
|
|
405
|
-
if (duplicateRelationName) throw
|
|
420
|
+
if (duplicateRelationName) throw contractError("CONTRACT.NAME_DUPLICATE", `Model "${this.stageOne.modelName ?? "<anonymous>"}" already defines relation "${duplicateRelationName}".`, { meta: {
|
|
421
|
+
kind: "relation",
|
|
422
|
+
name: duplicateRelationName,
|
|
423
|
+
modelName: this.stageOne.modelName
|
|
424
|
+
} });
|
|
406
425
|
return new ContractModelBuilder({
|
|
407
426
|
...this.stageOne,
|
|
408
427
|
relations: {
|
|
@@ -438,7 +457,7 @@ function isLazyRelationModelName(value) {
|
|
|
438
457
|
}
|
|
439
458
|
function resolveNamedModelTokenName(token) {
|
|
440
459
|
const modelName = token.stageOne.modelName;
|
|
441
|
-
if (!modelName) throw
|
|
460
|
+
if (!modelName) throw contractError("CONTRACT.MODEL_TOKEN_INVALID", "Relation targets require named model tokens. Use model(\"ModelName\", ...) before passing a token to rel.*(...).");
|
|
442
461
|
return modelName;
|
|
443
462
|
}
|
|
444
463
|
function normalizeRelationModelSource(target) {
|
|
@@ -460,7 +479,7 @@ function normalizeRelationModelSource(target) {
|
|
|
460
479
|
}
|
|
461
480
|
function model(modelNameOrInput, maybeInput) {
|
|
462
481
|
const input = typeof modelNameOrInput === "string" ? maybeInput : modelNameOrInput;
|
|
463
|
-
if (!input) throw
|
|
482
|
+
if (!input) throw contractError("CONTRACT.ARGUMENT_INVALID", "model(\"ModelName\", ...) requires a model definition.");
|
|
464
483
|
return new ContractModelBuilder({
|
|
465
484
|
...typeof modelNameOrInput === "string" ? { modelName: modelNameOrInput } : {},
|
|
466
485
|
...input.namespace !== void 0 ? { namespace: input.namespace } : {},
|
|
@@ -632,7 +651,10 @@ const RESERVED_HELPER_KEYS = [
|
|
|
632
651
|
];
|
|
633
652
|
function assertNoBuiltInEntityCollisions(namespace) {
|
|
634
653
|
const collisions = Object.keys(namespace).filter((name) => RESERVED_HELPER_KEYS.includes(name));
|
|
635
|
-
if (collisions.length > 0) throw
|
|
654
|
+
if (collisions.length > 0) throw contractError("CONTRACT.PACK_CONTRIBUTION_INVALID", `Pack-contributed entity type(s) ${collisions.map((c) => `"${c}"`).join(", ")} collide with the reserved built-in helper key(s) on the composed helpers surface. Reserved keys: ${RESERVED_HELPER_KEYS.map((k) => `"${k}"`).join(", ")}.`, { meta: {
|
|
655
|
+
collisions,
|
|
656
|
+
reservedKeys: RESERVED_HELPER_KEYS
|
|
657
|
+
} });
|
|
636
658
|
}
|
|
637
659
|
function createComposedFieldHelpers(fieldNamespace) {
|
|
638
660
|
const helperNamespace = createFieldHelpersFromNamespace(fieldNamespace, ({ helperPath, descriptor }) => createFieldPresetHelper({
|
|
@@ -646,14 +668,14 @@ function createComposedFieldHelpers(fieldNamespace) {
|
|
|
646
668
|
namedType: field.namedType
|
|
647
669
|
};
|
|
648
670
|
const coreHelperNames = new Set(Object.keys(coreFieldHelpers));
|
|
649
|
-
for (const helperName of Object.keys(helperNamespace)) if (coreHelperNames.has(helperName)) throw
|
|
671
|
+
for (const helperName of Object.keys(helperNamespace)) if (coreHelperNames.has(helperName)) throw contractError("CONTRACT.PACK_CONTRIBUTION_INVALID", `Duplicate authoring field helper "${helperName}". Core field helpers reserve that name.`, { meta: { helperName } });
|
|
650
672
|
return {
|
|
651
673
|
...coreFieldHelpers,
|
|
652
674
|
...helperNamespace
|
|
653
675
|
};
|
|
654
676
|
}
|
|
655
677
|
function createComposedAuthoringHelpers(options) {
|
|
656
|
-
const extensionValues = Object.values(options.
|
|
678
|
+
const extensionValues = Object.values(options.extensions ?? {});
|
|
657
679
|
const components = [
|
|
658
680
|
options.family,
|
|
659
681
|
options.target,
|
|
@@ -785,27 +807,46 @@ function resolveFieldDescriptor(modelName, fieldName, fieldState, storageTypes,
|
|
|
785
807
|
nativeType: fieldState.typeRef.nativeType
|
|
786
808
|
};
|
|
787
809
|
const typeRef = typeof fieldState.typeRef === "string" ? fieldState.typeRef : storageTypeReverseLookup.get(fieldState.typeRef);
|
|
788
|
-
if (!typeRef) throw
|
|
810
|
+
if (!typeRef) throw contractError("CONTRACT.TYPE_UNKNOWN", `Field "${modelName}.${fieldName}" references a storage type instance that is not present in definition.types`, { meta: {
|
|
811
|
+
modelName,
|
|
812
|
+
fieldName,
|
|
813
|
+
reason: "instance-not-in-definition-types"
|
|
814
|
+
} });
|
|
789
815
|
const referencedType = storageTypes[typeRef];
|
|
790
|
-
if (!referencedType) throw
|
|
816
|
+
if (!referencedType) throw contractError("CONTRACT.TYPE_UNKNOWN", `Field "${modelName}.${fieldName}" references unknown storage type "${typeRef}"`, { meta: {
|
|
817
|
+
modelName,
|
|
818
|
+
fieldName,
|
|
819
|
+
typeRef
|
|
820
|
+
} });
|
|
791
821
|
return {
|
|
792
822
|
codecId: referencedType.codecId,
|
|
793
823
|
nativeType: referencedType.nativeType,
|
|
794
824
|
typeRef
|
|
795
825
|
};
|
|
796
826
|
}
|
|
797
|
-
throw
|
|
827
|
+
throw contractError("CONTRACT.TYPE_UNKNOWN", `Field "${modelName}.${fieldName}" does not resolve to a storage descriptor`, { meta: {
|
|
828
|
+
modelName,
|
|
829
|
+
fieldName,
|
|
830
|
+
reason: "unresolved-storage-descriptor"
|
|
831
|
+
} });
|
|
798
832
|
}
|
|
799
833
|
function mapFieldNamesToColumnNames(modelName, fieldNames, fieldToColumn) {
|
|
800
834
|
return fieldNames.map((fieldName) => {
|
|
801
835
|
const columnName = fieldToColumn[fieldName];
|
|
802
|
-
if (!columnName) throw
|
|
836
|
+
if (!columnName) throw contractError("CONTRACT.FIELD_UNKNOWN", `Unknown field "${modelName}.${fieldName}" in contract definition`, { meta: {
|
|
837
|
+
modelName,
|
|
838
|
+
fieldName
|
|
839
|
+
} });
|
|
803
840
|
return columnName;
|
|
804
841
|
});
|
|
805
842
|
}
|
|
806
843
|
function assertRelationFieldArity(params) {
|
|
807
844
|
if (params.leftFields.length === params.rightFields.length) return;
|
|
808
|
-
throw
|
|
845
|
+
throw contractError("CONTRACT.RELATION_INVALID", `Relation "${params.modelName}.${params.relationName}" maps ${params.leftFields.length} ${params.leftLabel} field(s) to ${params.rightFields.length} ${params.rightLabel} field(s).`, { meta: {
|
|
846
|
+
modelName: params.modelName,
|
|
847
|
+
relationName: params.relationName,
|
|
848
|
+
reason: "field-count-mismatch"
|
|
849
|
+
} });
|
|
809
850
|
}
|
|
810
851
|
function resolveInlineIdConstraint(spec) {
|
|
811
852
|
const inlineIdFields = [];
|
|
@@ -817,7 +858,11 @@ function resolveInlineIdConstraint(spec) {
|
|
|
817
858
|
if (fieldState.id.name) idName = fieldState.id.name;
|
|
818
859
|
}
|
|
819
860
|
if (inlineIdFields.length === 0) return;
|
|
820
|
-
if (inlineIdFields.length > 1) throw
|
|
861
|
+
if (inlineIdFields.length > 1) throw contractError("CONTRACT.IDENTITY_INVALID", `Model "${spec.modelName}" marks multiple fields with .id(). Use .attributes(...) for compound identities.`, { meta: {
|
|
862
|
+
modelName: spec.modelName,
|
|
863
|
+
reason: "multiple-inline-ids",
|
|
864
|
+
fields: inlineIdFields
|
|
865
|
+
} });
|
|
821
866
|
const [inlineIdField] = inlineIdFields;
|
|
822
867
|
if (!inlineIdField) return;
|
|
823
868
|
return {
|
|
@@ -842,14 +887,20 @@ function collectInlineUniqueConstraints(spec) {
|
|
|
842
887
|
function resolveModelIdConstraint(spec) {
|
|
843
888
|
const inlineId = resolveInlineIdConstraint(spec);
|
|
844
889
|
const attributeId = spec.attributesSpec?.id;
|
|
845
|
-
if (inlineId && attributeId) throw
|
|
890
|
+
if (inlineId && attributeId) throw contractError("CONTRACT.IDENTITY_INVALID", `Model "${spec.modelName}" defines identity both inline and in .attributes(...). Pick one identity style.`, { meta: {
|
|
891
|
+
modelName: spec.modelName,
|
|
892
|
+
reason: "inline-and-attributes"
|
|
893
|
+
} });
|
|
846
894
|
const resolvedId = attributeId ?? inlineId;
|
|
847
|
-
if (resolvedId && resolvedId.fields.length === 0) throw
|
|
895
|
+
if (resolvedId && resolvedId.fields.length === 0) throw contractError("CONTRACT.IDENTITY_INVALID", `Model "${spec.modelName}" defines an empty identity. Add at least one field.`, { meta: {
|
|
896
|
+
modelName: spec.modelName,
|
|
897
|
+
reason: "empty-identity"
|
|
898
|
+
} });
|
|
848
899
|
return resolvedId;
|
|
849
900
|
}
|
|
850
901
|
function resolveModelUniqueConstraints(spec) {
|
|
851
902
|
const attributeUniques = spec.attributesSpec?.uniques ?? [];
|
|
852
|
-
for (const unique of attributeUniques) if (unique.fields.length === 0) throw
|
|
903
|
+
for (const unique of attributeUniques) if (unique.fields.length === 0) throw contractError("CONTRACT.CONSTRAINT_INVALID", `Model "${spec.modelName}" defines an empty unique constraint. Add at least one field.`, { meta: { modelName: spec.modelName } });
|
|
853
904
|
return [...collectInlineUniqueConstraints(spec), ...attributeUniques];
|
|
854
905
|
}
|
|
855
906
|
function resolveRelationForeignKeys(spec, allSpecs) {
|
|
@@ -885,7 +936,11 @@ function resolveRelationForeignKeys(spec, allSpecs) {
|
|
|
885
936
|
});
|
|
886
937
|
continue;
|
|
887
938
|
}
|
|
888
|
-
if (!allSpecs.has(targetModelName)) throw
|
|
939
|
+
if (!allSpecs.has(targetModelName)) throw contractError("CONTRACT.MODEL_UNKNOWN", `Relation "${spec.modelName}.${relationName}" references unknown model "${targetModelName}"`, { meta: {
|
|
940
|
+
sourceModel: spec.modelName,
|
|
941
|
+
relationName,
|
|
942
|
+
targetModel: targetModelName
|
|
943
|
+
} });
|
|
889
944
|
const fields = normalizeRelationFieldNames(relation.from);
|
|
890
945
|
const targetFields = normalizeRelationFieldNames(relation.to);
|
|
891
946
|
assertRelationFieldArity({
|
|
@@ -914,9 +969,12 @@ function resolveRelationAnchorFields(spec) {
|
|
|
914
969
|
const idFields = spec.idConstraint?.fields;
|
|
915
970
|
if (idFields && idFields.length > 0) return idFields;
|
|
916
971
|
if ("id" in spec.fieldToColumn) return ["id"];
|
|
917
|
-
throw
|
|
972
|
+
throw contractError("CONTRACT.IDENTITY_INVALID", `Model "${spec.modelName}" needs an explicit id or an "id" field to anchor non-owning relations`, { meta: {
|
|
973
|
+
modelName: spec.modelName,
|
|
974
|
+
reason: "missing-anchor-id"
|
|
975
|
+
} });
|
|
918
976
|
}
|
|
919
|
-
function lowerBelongsToRelation(relationName, relation, currentSpec, allSpecs,
|
|
977
|
+
function lowerBelongsToRelation(relationName, relation, currentSpec, allSpecs, extensions) {
|
|
920
978
|
const targetModelName = resolveRelationModelName(relation.toModel);
|
|
921
979
|
const fromFields = normalizeRelationFieldNames(relation.from);
|
|
922
980
|
const toFields = normalizeRelationFieldNames(relation.to);
|
|
@@ -929,7 +987,7 @@ function lowerBelongsToRelation(relationName, relation, currentSpec, allSpecs, e
|
|
|
929
987
|
rightFields: toFields
|
|
930
988
|
});
|
|
931
989
|
if (relation.spaceId !== void 0) {
|
|
932
|
-
assertKnownExtensionPack(
|
|
990
|
+
assertKnownExtensionPack(extensions, relation.spaceId, `Relation "${currentSpec.modelName}.${relationName}"`);
|
|
933
991
|
const targetTable = relation.tableName ?? targetModelName.toLowerCase();
|
|
934
992
|
const parentColumns = mapFieldNamesToColumnNames(currentSpec.modelName, fromFields, currentSpec.fieldToColumn);
|
|
935
993
|
return {
|
|
@@ -948,7 +1006,11 @@ function lowerBelongsToRelation(relationName, relation, currentSpec, allSpecs, e
|
|
|
948
1006
|
};
|
|
949
1007
|
}
|
|
950
1008
|
const targetSpec = allSpecs.get(targetModelName);
|
|
951
|
-
if (!targetSpec) throw
|
|
1009
|
+
if (!targetSpec) throw contractError("CONTRACT.MODEL_UNKNOWN", `Relation "${currentSpec.modelName}.${relationName}" references unknown model "${targetModelName}"`, { meta: {
|
|
1010
|
+
sourceModel: currentSpec.modelName,
|
|
1011
|
+
relationName,
|
|
1012
|
+
targetModel: targetModelName
|
|
1013
|
+
} });
|
|
952
1014
|
return {
|
|
953
1015
|
fieldName: relationName,
|
|
954
1016
|
toModel: targetModelName,
|
|
@@ -965,7 +1027,11 @@ function lowerBelongsToRelation(relationName, relation, currentSpec, allSpecs, e
|
|
|
965
1027
|
function lowerHasOwnershipRelation(relationName, relation, currentSpec, allSpecs) {
|
|
966
1028
|
const targetModelName = resolveRelationModelName(relation.toModel);
|
|
967
1029
|
const targetSpec = allSpecs.get(targetModelName);
|
|
968
|
-
if (!targetSpec) throw
|
|
1030
|
+
if (!targetSpec) throw contractError("CONTRACT.MODEL_UNKNOWN", `Relation "${currentSpec.modelName}.${relationName}" references unknown model "${targetModelName}"`, { meta: {
|
|
1031
|
+
sourceModel: currentSpec.modelName,
|
|
1032
|
+
relationName,
|
|
1033
|
+
targetModel: targetModelName
|
|
1034
|
+
} });
|
|
969
1035
|
const parentFields = resolveRelationAnchorFields(currentSpec);
|
|
970
1036
|
const childFields = normalizeRelationFieldNames(relation.by);
|
|
971
1037
|
assertRelationFieldArity({
|
|
@@ -992,15 +1058,27 @@ function lowerHasOwnershipRelation(relationName, relation, currentSpec, allSpecs
|
|
|
992
1058
|
function lowerManyToManyRelation(relationName, relation, currentSpec, allSpecs) {
|
|
993
1059
|
const targetModelName = resolveRelationModelName(relation.toModel);
|
|
994
1060
|
const targetSpec = allSpecs.get(targetModelName);
|
|
995
|
-
if (!targetSpec) throw
|
|
1061
|
+
if (!targetSpec) throw contractError("CONTRACT.MODEL_UNKNOWN", `Relation "${currentSpec.modelName}.${relationName}" references unknown model "${targetModelName}"`, { meta: {
|
|
1062
|
+
sourceModel: currentSpec.modelName,
|
|
1063
|
+
relationName,
|
|
1064
|
+
targetModel: targetModelName
|
|
1065
|
+
} });
|
|
996
1066
|
const throughModelName = resolveRelationModelName(relation.through);
|
|
997
1067
|
const throughSpec = allSpecs.get(throughModelName);
|
|
998
|
-
if (!throughSpec) throw
|
|
1068
|
+
if (!throughSpec) throw contractError("CONTRACT.MODEL_UNKNOWN", `Relation "${currentSpec.modelName}.${relationName}" references unknown through model "${throughModelName}"`, { meta: {
|
|
1069
|
+
sourceModel: currentSpec.modelName,
|
|
1070
|
+
relationName,
|
|
1071
|
+
targetModel: throughModelName
|
|
1072
|
+
} });
|
|
999
1073
|
const currentAnchorFields = resolveRelationAnchorFields(currentSpec);
|
|
1000
1074
|
const targetAnchorFields = resolveRelationAnchorFields(targetSpec);
|
|
1001
1075
|
const throughFromFields = normalizeRelationFieldNames(relation.from);
|
|
1002
1076
|
const throughToFields = normalizeRelationFieldNames(relation.to);
|
|
1003
|
-
if (currentAnchorFields.length !== throughFromFields.length || targetAnchorFields.length !== throughToFields.length) throw
|
|
1077
|
+
if (currentAnchorFields.length !== throughFromFields.length || targetAnchorFields.length !== throughToFields.length) throw contractError("CONTRACT.RELATION_INVALID", `Relation "${currentSpec.modelName}.${relationName}" has mismatched many-to-many field counts.`, { meta: {
|
|
1078
|
+
modelName: currentSpec.modelName,
|
|
1079
|
+
relationName,
|
|
1080
|
+
reason: "many-to-many-field-count-mismatch"
|
|
1081
|
+
} });
|
|
1004
1082
|
return {
|
|
1005
1083
|
fieldName: relationName,
|
|
1006
1084
|
toModel: targetModelName,
|
|
@@ -1020,8 +1098,8 @@ function lowerManyToManyRelation(relationName, relation, currentSpec, allSpecs)
|
|
|
1020
1098
|
}
|
|
1021
1099
|
};
|
|
1022
1100
|
}
|
|
1023
|
-
function resolveRelationNode(relationName, relation, currentSpec, allSpecs,
|
|
1024
|
-
if (relation.kind === "belongsTo") return lowerBelongsToRelation(relationName, relation, currentSpec, allSpecs,
|
|
1101
|
+
function resolveRelationNode(relationName, relation, currentSpec, allSpecs, extensions) {
|
|
1102
|
+
if (relation.kind === "belongsTo") return lowerBelongsToRelation(relationName, relation, currentSpec, allSpecs, extensions);
|
|
1025
1103
|
if (relation.kind === "hasMany" || relation.kind === "hasOne") return lowerHasOwnershipRelation(relationName, relation, currentSpec, allSpecs);
|
|
1026
1104
|
return lowerManyToManyRelation(relationName, relation, currentSpec, allSpecs);
|
|
1027
1105
|
}
|
|
@@ -1057,44 +1135,53 @@ function lowerCrossSpaceForeignKeyNode(spec, foreignKey) {
|
|
|
1057
1135
|
...foreignKey.index !== void 0 ? { index: foreignKey.index } : {}
|
|
1058
1136
|
};
|
|
1059
1137
|
}
|
|
1060
|
-
function assertKnownExtensionPack(
|
|
1061
|
-
if (
|
|
1062
|
-
throw
|
|
1138
|
+
function assertKnownExtensionPack(extensions, spaceId, context) {
|
|
1139
|
+
if (extensions !== void 0 && Object.hasOwn(extensions, spaceId)) return;
|
|
1140
|
+
throw contractError("CONTRACT.PACK_MISSING", `${context} references contract space "${spaceId}" but "${spaceId}" is not declared in extensions. Add the pack to extensions.`, { meta: {
|
|
1141
|
+
spaceId,
|
|
1142
|
+
context
|
|
1143
|
+
} });
|
|
1063
1144
|
}
|
|
1064
|
-
function resolveForeignKeyNodes(spec, allSpecs,
|
|
1145
|
+
function resolveForeignKeyNodes(spec, allSpecs, extensions) {
|
|
1065
1146
|
const relationForeignKeys = resolveRelationForeignKeys(spec, allSpecs).map((foreignKey) => {
|
|
1066
1147
|
if (foreignKey.targetSpaceId !== void 0) {
|
|
1067
|
-
assertKnownExtensionPack(
|
|
1148
|
+
assertKnownExtensionPack(extensions, foreignKey.targetSpaceId, `Relation-derived foreign key on "${spec.modelName}"`);
|
|
1068
1149
|
return lowerCrossSpaceForeignKeyNode(spec, {
|
|
1069
1150
|
...foreignKey,
|
|
1070
1151
|
targetSpaceId: foreignKey.targetSpaceId
|
|
1071
1152
|
});
|
|
1072
1153
|
}
|
|
1073
1154
|
const targetSpec = allSpecs.get(foreignKey.targetModel);
|
|
1074
|
-
if (!targetSpec) throw
|
|
1155
|
+
if (!targetSpec) throw contractError("CONTRACT.MODEL_UNKNOWN", `Foreign key on "${spec.modelName}" references unknown model "${foreignKey.targetModel}"`, { meta: {
|
|
1156
|
+
sourceModel: spec.modelName,
|
|
1157
|
+
targetModel: foreignKey.targetModel
|
|
1158
|
+
} });
|
|
1075
1159
|
return lowerLocalForeignKeyNode(spec, targetSpec, foreignKey);
|
|
1076
1160
|
});
|
|
1077
1161
|
const sqlForeignKeys = (spec.sqlSpec?.foreignKeys ?? []).map((foreignKey) => {
|
|
1078
1162
|
if (foreignKey.targetSpaceId !== void 0) {
|
|
1079
|
-
assertKnownExtensionPack(
|
|
1163
|
+
assertKnownExtensionPack(extensions, foreignKey.targetSpaceId, `Foreign key on "${spec.modelName}"`);
|
|
1080
1164
|
return lowerCrossSpaceForeignKeyNode(spec, {
|
|
1081
1165
|
...foreignKey,
|
|
1082
1166
|
targetSpaceId: foreignKey.targetSpaceId
|
|
1083
1167
|
});
|
|
1084
1168
|
}
|
|
1085
1169
|
const targetSpec = allSpecs.get(foreignKey.targetModel);
|
|
1086
|
-
if (!targetSpec) throw
|
|
1170
|
+
if (!targetSpec) throw contractError("CONTRACT.MODEL_UNKNOWN", `Foreign key on "${spec.modelName}" references unknown model "${foreignKey.targetModel}"`, { meta: {
|
|
1171
|
+
sourceModel: spec.modelName,
|
|
1172
|
+
targetModel: foreignKey.targetModel
|
|
1173
|
+
} });
|
|
1087
1174
|
return lowerLocalForeignKeyNode(spec, targetSpec, foreignKey);
|
|
1088
1175
|
});
|
|
1089
1176
|
return [...relationForeignKeys, ...sqlForeignKeys];
|
|
1090
1177
|
}
|
|
1091
|
-
function resolveModelNode(spec, allSpecs, storageTypes, storageTypeReverseLookup,
|
|
1178
|
+
function resolveModelNode(spec, allSpecs, storageTypes, storageTypeReverseLookup, extensions) {
|
|
1092
1179
|
const fields = [];
|
|
1093
1180
|
for (const [fieldName, fieldBuilder] of Object.entries(spec.fieldBuilders)) {
|
|
1094
1181
|
const fieldState = fieldBuilder.build();
|
|
1095
1182
|
const descriptor = resolveFieldDescriptor(spec.modelName, fieldName, fieldState, storageTypes, storageTypeReverseLookup);
|
|
1096
1183
|
const columnName = spec.fieldToColumn[fieldName];
|
|
1097
|
-
if (!columnName) throw new
|
|
1184
|
+
if (!columnName) throw new InternalError(`Column name resolution failed for "${spec.modelName}.${fieldName}"`);
|
|
1098
1185
|
const enumHandle = "typeRef" in fieldState && isEnumTypeHandle(fieldState.typeRef) ? fieldState.typeRef : void 0;
|
|
1099
1186
|
fields.push({
|
|
1100
1187
|
fieldName,
|
|
@@ -1118,8 +1205,8 @@ function resolveModelNode(spec, allSpecs, storageTypes, storageTypeReverseLookup
|
|
|
1118
1205
|
...ifDefined("type", index.type),
|
|
1119
1206
|
...ifDefined("options", index.options)
|
|
1120
1207
|
}));
|
|
1121
|
-
const foreignKeys = resolveForeignKeyNodes(spec, allSpecs,
|
|
1122
|
-
const relations = Object.entries(spec.relations).map(([relationName, relationBuilder]) => resolveRelationNode(relationName, relationBuilder.build(), spec, allSpecs,
|
|
1208
|
+
const foreignKeys = resolveForeignKeyNodes(spec, allSpecs, extensions);
|
|
1209
|
+
const relations = Object.entries(spec.relations).map(([relationName, relationBuilder]) => resolveRelationNode(relationName, relationBuilder.build(), spec, allSpecs, extensions));
|
|
1123
1210
|
return {
|
|
1124
1211
|
modelName: spec.modelName,
|
|
1125
1212
|
tableName: spec.tableName,
|
|
@@ -1144,21 +1231,34 @@ function collectRuntimeModelSpecs(definition) {
|
|
|
1144
1231
|
const tableOwners = /* @__PURE__ */ new Map();
|
|
1145
1232
|
for (const [modelName, modelDefinition] of Object.entries(models)) {
|
|
1146
1233
|
const tokenModelName = modelDefinition.stageOne.modelName;
|
|
1147
|
-
if (tokenModelName && tokenModelName !== modelName) throw
|
|
1234
|
+
if (tokenModelName && tokenModelName !== modelName) throw contractError("CONTRACT.MODEL_TOKEN_INVALID", `Model token "${tokenModelName}" must be assigned to models.${tokenModelName}. Received models.${modelName}.`, { meta: {
|
|
1235
|
+
tokenModelName,
|
|
1236
|
+
assignedKey: modelName
|
|
1237
|
+
} });
|
|
1148
1238
|
const attributesSpec = modelDefinition.buildAttributesSpec();
|
|
1149
1239
|
const sqlSpec = modelDefinition.buildSqlSpec();
|
|
1150
1240
|
const tableName = sqlSpec?.table ?? applyNaming(modelName, definition.naming?.tables);
|
|
1151
1241
|
const namespaceId = modelDefinition.stageOne.namespace ?? definition.target.defaultNamespaceId;
|
|
1152
1242
|
const tableKey = JSON.stringify([namespaceId, tableName]);
|
|
1153
1243
|
const existingModel = tableOwners.get(tableKey);
|
|
1154
|
-
if (existingModel) throw
|
|
1244
|
+
if (existingModel) throw contractError("CONTRACT.NAME_DUPLICATE", `Models "${existingModel}" and "${modelName}" both map to table "${tableName}".`, { meta: {
|
|
1245
|
+
kind: "table",
|
|
1246
|
+
name: tableName,
|
|
1247
|
+
first: existingModel,
|
|
1248
|
+
second: modelName
|
|
1249
|
+
} });
|
|
1155
1250
|
tableOwners.set(tableKey, modelName);
|
|
1156
1251
|
const fieldToColumn = {};
|
|
1157
1252
|
const columnOwners = /* @__PURE__ */ new Map();
|
|
1158
1253
|
for (const [fieldName, fieldBuilder] of Object.entries(modelDefinition.stageOne.fields)) {
|
|
1159
1254
|
const columnName = fieldBuilder.build().columnName ?? applyNaming(fieldName, definition.naming?.columns);
|
|
1160
1255
|
const existingField = columnOwners.get(columnName);
|
|
1161
|
-
if (existingField) throw
|
|
1256
|
+
if (existingField) throw contractError("CONTRACT.NAME_DUPLICATE", `Model "${modelName}" maps both "${existingField}" and "${fieldName}" to column "${columnName}".`, { meta: {
|
|
1257
|
+
kind: "column",
|
|
1258
|
+
name: columnName,
|
|
1259
|
+
first: existingField,
|
|
1260
|
+
second: fieldName
|
|
1261
|
+
} });
|
|
1162
1262
|
columnOwners.set(columnName, fieldName);
|
|
1163
1263
|
fieldToColumn[fieldName] = columnName;
|
|
1164
1264
|
}
|
|
@@ -1186,10 +1286,10 @@ function collectRuntimeModelSpecs(definition) {
|
|
|
1186
1286
|
modelSpecs
|
|
1187
1287
|
};
|
|
1188
1288
|
}
|
|
1189
|
-
function lowerModels(collection,
|
|
1289
|
+
function lowerModels(collection, extensions) {
|
|
1190
1290
|
emitTypedCrossModelFallbackWarnings(collection);
|
|
1191
1291
|
const storageTypeReverseLookup = buildStorageTypeReverseLookup(collection.storageTypes);
|
|
1192
|
-
return Array.from(collection.modelSpecs.values()).map((spec) => resolveModelNode(spec, collection.modelSpecs, collection.storageTypes, storageTypeReverseLookup,
|
|
1292
|
+
return Array.from(collection.modelSpecs.values()).map((spec) => resolveModelNode(spec, collection.modelSpecs, collection.storageTypes, storageTypeReverseLookup, extensions));
|
|
1193
1293
|
}
|
|
1194
1294
|
/**
|
|
1195
1295
|
* Kind-agnostic walk over the author-declared `entities` handle list:
|
|
@@ -1214,7 +1314,7 @@ function lowerModels(collection, extensionPacks) {
|
|
|
1214
1314
|
function lowerPackEntityHandles(definition, modelSpecs) {
|
|
1215
1315
|
const entities = definition.entities;
|
|
1216
1316
|
if (entities === void 0 || entities.length === 0) return void 0;
|
|
1217
|
-
const components = [definition.target, ...Object.values(definition.
|
|
1317
|
+
const components = [definition.target, ...Object.values(definition.extensions ?? {})];
|
|
1218
1318
|
const owningComponent = /* @__PURE__ */ new Map();
|
|
1219
1319
|
const walkEntityTypes = (namespace, component) => {
|
|
1220
1320
|
for (const value of Object.values(namespace)) if (isAuthoringEntityTypeDescriptor(value)) owningComponent.set(value.discriminator, component);
|
|
@@ -1269,7 +1369,7 @@ function lowerPackEntityHandles(definition, modelSpecs) {
|
|
|
1269
1369
|
const claimed = /* @__PURE__ */ new Map();
|
|
1270
1370
|
for (const handle of entities) {
|
|
1271
1371
|
const component = owningComponent.get(handle.entityKind);
|
|
1272
|
-
if (component === void 0) throw
|
|
1372
|
+
if (component === void 0) throw contractError("CONTRACT.ENTITY_KIND_UNKNOWN", `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.`, { meta: { entityKind: handle.entityKind } });
|
|
1273
1373
|
const refs = {};
|
|
1274
1374
|
for (const [refName, refValue] of Object.entries(handle.refs ?? {})) refs[refName] = resolveRef(refValue);
|
|
1275
1375
|
const forComponent = claimed.get(component) ?? [];
|
|
@@ -1284,7 +1384,10 @@ function lowerPackEntityHandles(definition, modelSpecs) {
|
|
|
1284
1384
|
const authoring = component.authoring;
|
|
1285
1385
|
if (!providesEntityHandleLowering(authoring)) {
|
|
1286
1386
|
const kinds = [...new Set(handles.map((entry) => entry.handle.entityKind))].sort();
|
|
1287
|
-
throw
|
|
1387
|
+
throw contractError("CONTRACT.PACK_CONTRIBUTION_INVALID", `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).`, { meta: {
|
|
1388
|
+
entityKinds: kinds,
|
|
1389
|
+
reason: "missing-lowerEntityHandles"
|
|
1390
|
+
} });
|
|
1288
1391
|
}
|
|
1289
1392
|
for (const row of authoring.lowerEntityHandles({
|
|
1290
1393
|
handles,
|
|
@@ -1295,7 +1398,11 @@ function lowerPackEntityHandles(definition, modelSpecs) {
|
|
|
1295
1398
|
const forKind = forNamespace[row.entityKind] ?? {};
|
|
1296
1399
|
forNamespace[row.entityKind] = forKind;
|
|
1297
1400
|
const existing = forKind[row.key];
|
|
1298
|
-
if (existing !== void 0 && existing !== row.entity) throw
|
|
1401
|
+
if (existing !== void 0 && existing !== row.entity) throw contractError("CONTRACT.NAME_DUPLICATE", `defineContract: two different "${row.entityKind}" entities named "${row.key}" in namespace "${row.namespaceId}" — pack-entity names must be unique per namespace.`, { meta: {
|
|
1402
|
+
kind: row.entityKind,
|
|
1403
|
+
name: row.key,
|
|
1404
|
+
namespaceId: row.namespaceId
|
|
1405
|
+
} });
|
|
1299
1406
|
forKind[row.key] = row.entity;
|
|
1300
1407
|
}
|
|
1301
1408
|
}
|
|
@@ -1303,12 +1410,12 @@ function lowerPackEntityHandles(definition, modelSpecs) {
|
|
|
1303
1410
|
}
|
|
1304
1411
|
function buildContractDefinition(definition) {
|
|
1305
1412
|
const collection = collectRuntimeModelSpecs(definition);
|
|
1306
|
-
const models = lowerModels(collection, definition.
|
|
1413
|
+
const models = lowerModels(collection, definition.extensions);
|
|
1307
1414
|
const attachedEntities = lowerPackEntityHandles(definition, collection.modelSpecs);
|
|
1308
1415
|
return {
|
|
1309
1416
|
target: definition.target,
|
|
1310
1417
|
...ifDefined("defaultControlPolicy", definition.defaultControlPolicy),
|
|
1311
|
-
...definition.
|
|
1418
|
+
...definition.extensions ? { extensions: definition.extensions } : {},
|
|
1312
1419
|
...definition.storageHash ? { storageHash: definition.storageHash } : {},
|
|
1313
1420
|
...definition.foreignKeyDefaults ? { foreignKeyDefaults: definition.foreignKeyDefaults } : {},
|
|
1314
1421
|
...Object.keys(collection.storageTypes).length > 0 ? { storageTypes: collection.storageTypes } : {},
|
|
@@ -1322,8 +1429,16 @@ function buildContractDefinition(definition) {
|
|
|
1322
1429
|
//#endregion
|
|
1323
1430
|
//#region src/contract-builder.ts
|
|
1324
1431
|
function validateTargetPackRef(family, target) {
|
|
1325
|
-
if (family.familyId !== "sql") throw
|
|
1326
|
-
|
|
1432
|
+
if (family.familyId !== "sql") throw contractError("CONTRACT.PACK_FAMILY_MISMATCH", `defineContract only accepts SQL family packs. Received family "${family.familyId}".`, { meta: {
|
|
1433
|
+
packId: family.id,
|
|
1434
|
+
packFamilyId: family.familyId,
|
|
1435
|
+
contractFamilyId: "sql"
|
|
1436
|
+
} });
|
|
1437
|
+
if (target.familyId !== family.familyId) throw contractError("CONTRACT.PACK_FAMILY_MISMATCH", `target pack "${target.id}" targets family "${target.familyId}" but contract family is "${family.familyId}".`, { meta: {
|
|
1438
|
+
packId: target.id,
|
|
1439
|
+
packFamilyId: target.familyId,
|
|
1440
|
+
contractFamilyId: family.familyId
|
|
1441
|
+
} });
|
|
1327
1442
|
}
|
|
1328
1443
|
/**
|
|
1329
1444
|
* Per-target reserved namespace names enforced by `defineContract` for
|
|
@@ -1342,14 +1457,32 @@ function validateTargetPackRef(family, target) {
|
|
|
1342
1457
|
*/
|
|
1343
1458
|
function validateNamespaceDeclarations(target, namespaces) {
|
|
1344
1459
|
if (!namespaces) return;
|
|
1345
|
-
if (target.targetId === "sqlite" && namespaces.length > 0) throw
|
|
1460
|
+
if (target.targetId === "sqlite" && namespaces.length > 0) throw contractError("CONTRACT.NAMESPACE_UNSUPPORTED", `defineContract: SQLite contracts cannot declare namespaces (SQLite has no schema concept; emitted DDL is always unqualified). Received namespaces: [${namespaces.map((name) => `"${name}"`).join(", ")}].`, { meta: {
|
|
1461
|
+
namespaces,
|
|
1462
|
+
targetId: target.targetId
|
|
1463
|
+
} });
|
|
1346
1464
|
const seen = /* @__PURE__ */ new Set();
|
|
1347
1465
|
for (const namespace of namespaces) {
|
|
1348
|
-
if (namespace.length === 0) throw
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
if (
|
|
1466
|
+
if (namespace.length === 0) throw contractError("CONTRACT.NAMESPACE_INVALID", "defineContract: namespace names cannot be empty.", { meta: {
|
|
1467
|
+
namespace,
|
|
1468
|
+
reason: "empty"
|
|
1469
|
+
} });
|
|
1470
|
+
if (namespace.trim().length === 0) throw contractError("CONTRACT.NAMESPACE_INVALID", `defineContract: namespace name "${namespace}" cannot be whitespace-only.`, { meta: {
|
|
1471
|
+
namespace,
|
|
1472
|
+
reason: "whitespace-only"
|
|
1473
|
+
} });
|
|
1474
|
+
if (namespace === "__unbound__" || namespace === "__unspecified__") throw contractError("CONTRACT.NAMESPACE_INVALID", `defineContract: namespace name "${namespace}" is a reserved IR sentinel and cannot appear in the declared namespaces list.`, { meta: {
|
|
1475
|
+
namespace,
|
|
1476
|
+
reason: "reserved-ir-sentinel"
|
|
1477
|
+
} });
|
|
1478
|
+
if (target.targetId === "postgres" && namespace === "unbound") throw contractError("CONTRACT.NAMESPACE_INVALID", `defineContract: namespace name "unbound" is reserved by Postgres for the late-binding opt-in (use \`namespace unbound { … }\` in PSL instead of declaring it as a regular schema).`, { meta: {
|
|
1479
|
+
namespace,
|
|
1480
|
+
reason: "reserved-by-postgres"
|
|
1481
|
+
} });
|
|
1482
|
+
if (seen.has(namespace)) throw contractError("CONTRACT.NAME_DUPLICATE", `defineContract: namespaces list contains duplicate entry "${namespace}".`, { meta: {
|
|
1483
|
+
kind: "namespace",
|
|
1484
|
+
name: namespace
|
|
1485
|
+
} });
|
|
1353
1486
|
seen.add(namespace);
|
|
1354
1487
|
}
|
|
1355
1488
|
}
|
|
@@ -1379,26 +1512,50 @@ function validatePerModelNamespaces(target, namespaces, models) {
|
|
|
1379
1512
|
for (const [modelKey, modelBuilder] of Object.entries(models)) {
|
|
1380
1513
|
const perModelNamespace = modelBuilder.stageOne.namespace;
|
|
1381
1514
|
if (perModelNamespace === void 0) continue;
|
|
1382
|
-
if (target.targetId === "sqlite") throw
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1515
|
+
if (target.targetId === "sqlite") throw contractError("CONTRACT.NAMESPACE_UNSUPPORTED", `defineContract: model "${modelKey}" sets \`namespace: "${perModelNamespace}"\` but the target is SQLite (SQLite has no schema concept; remove the per-model \`namespace\` field).`, { meta: {
|
|
1516
|
+
modelKey,
|
|
1517
|
+
namespace: perModelNamespace,
|
|
1518
|
+
targetId: target.targetId
|
|
1519
|
+
} });
|
|
1520
|
+
if (perModelNamespace === "__unbound__" || perModelNamespace === "__unspecified__") throw contractError("CONTRACT.NAMESPACE_INVALID", `defineContract: model "${modelKey}" sets \`namespace: "${perModelNamespace}"\` but that name is a reserved IR sentinel and cannot appear in user code.`, { meta: {
|
|
1521
|
+
modelKey,
|
|
1522
|
+
namespace: perModelNamespace,
|
|
1523
|
+
reason: "reserved-ir-sentinel"
|
|
1524
|
+
} });
|
|
1525
|
+
if (target.targetId === "postgres" && perModelNamespace === "unbound") throw contractError("CONTRACT.NAMESPACE_INVALID", `defineContract: model "${modelKey}" sets \`namespace: "unbound"\` but that name is reserved by Postgres for the late-binding opt-in (use \`namespace unbound { … }\` in PSL instead — there is no equivalent surface in the TS builder today).`, { meta: {
|
|
1526
|
+
modelKey,
|
|
1527
|
+
namespace: perModelNamespace,
|
|
1528
|
+
reason: "reserved-by-postgres"
|
|
1529
|
+
} });
|
|
1530
|
+
if (!declaredNamespaces.has(perModelNamespace)) throw contractError("CONTRACT.NAMESPACE_UNKNOWN", `defineContract: model "${modelKey}" references namespace "${perModelNamespace}" but that name does not appear in the contract's declared \`namespaces\` list.${declaredNamespaces.size > 0 ? ` Declared namespaces: [${[...declaredNamespaces].map((name) => `"${name}"`).join(", ")}].` : " The contract does not declare any namespaces; add `namespaces: [\"…\"]` to `defineContract` first."}`, { meta: {
|
|
1531
|
+
modelKey,
|
|
1532
|
+
namespace: perModelNamespace,
|
|
1533
|
+
declared: [...declaredNamespaces]
|
|
1534
|
+
} });
|
|
1389
1535
|
}
|
|
1390
1536
|
}
|
|
1391
|
-
function validateExtensionPackRefs(target,
|
|
1392
|
-
if (!
|
|
1393
|
-
for (const packRef of Object.values(
|
|
1394
|
-
if (packRef.kind !== "extension") throw
|
|
1395
|
-
|
|
1396
|
-
|
|
1537
|
+
function validateExtensionPackRefs(target, extensions) {
|
|
1538
|
+
if (!extensions) return;
|
|
1539
|
+
for (const packRef of Object.values(extensions)) {
|
|
1540
|
+
if (packRef.kind !== "extension") throw contractError("CONTRACT.PACK_REF_INVALID", `defineContract only accepts extension pack refs in extensions. Received kind "${packRef.kind}".`, { meta: {
|
|
1541
|
+
packId: packRef.id,
|
|
1542
|
+
kind: packRef.kind
|
|
1543
|
+
} });
|
|
1544
|
+
if (packRef.familyId !== target.familyId) throw contractError("CONTRACT.PACK_FAMILY_MISMATCH", `extension pack "${packRef.id}" targets family "${packRef.familyId}" but contract target family is "${target.familyId}".`, { meta: {
|
|
1545
|
+
packId: packRef.id,
|
|
1546
|
+
packFamilyId: packRef.familyId,
|
|
1547
|
+
contractFamilyId: target.familyId
|
|
1548
|
+
} });
|
|
1549
|
+
if (packRef.targetId && packRef.targetId !== target.targetId) throw contractError("CONTRACT.PACK_TARGET_MISMATCH", `extension pack "${packRef.id}" targets "${packRef.targetId}" but contract target is "${target.targetId}".`, { meta: {
|
|
1550
|
+
packId: packRef.id,
|
|
1551
|
+
packTargetId: packRef.targetId,
|
|
1552
|
+
contractTargetId: target.targetId
|
|
1553
|
+
} });
|
|
1397
1554
|
}
|
|
1398
1555
|
}
|
|
1399
1556
|
function buildContractFromDsl(definition) {
|
|
1400
1557
|
validateTargetPackRef(definition.family, definition.target);
|
|
1401
|
-
validateExtensionPackRefs(definition.target, definition.
|
|
1558
|
+
validateExtensionPackRefs(definition.target, definition.extensions);
|
|
1402
1559
|
validateNamespaceDeclarations(definition.target, definition.namespaces);
|
|
1403
1560
|
validatePerModelNamespaces(definition.target, definition.namespaces, definition.models ?? {});
|
|
1404
1561
|
return blindCast(buildSqlContractFromDefinition(buildContractDefinition(definition), definition.codecLookup));
|
|
@@ -1414,7 +1571,7 @@ function buildBoundContract(family, target, definition, factory) {
|
|
|
1414
1571
|
const built = factory(createComposedAuthoringHelpers({
|
|
1415
1572
|
family,
|
|
1416
1573
|
target,
|
|
1417
|
-
|
|
1574
|
+
extensions: definition.extensions
|
|
1418
1575
|
}));
|
|
1419
1576
|
const mergedEnums = {
|
|
1420
1577
|
...definition.enums ?? {},
|