@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/package.json
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/sql-contract-ts",
|
|
3
|
-
"version": "0.16.0-dev.
|
|
3
|
+
"version": "0.16.0-dev.7",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"description": "SQL-specific TypeScript contract authoring surface for Prisma Next",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@prisma-next/config": "0.16.0-dev.
|
|
10
|
-
"@prisma-next/contract": "0.16.0-dev.
|
|
11
|
-
"@prisma-next/contract-authoring": "0.16.0-dev.
|
|
12
|
-
"@prisma-next/framework-components": "0.16.0-dev.
|
|
13
|
-
"@prisma-next/sql-contract": "0.16.0-dev.
|
|
14
|
-
"@prisma-next/utils": "0.16.0-dev.
|
|
9
|
+
"@prisma-next/config": "0.16.0-dev.7",
|
|
10
|
+
"@prisma-next/contract": "0.16.0-dev.7",
|
|
11
|
+
"@prisma-next/contract-authoring": "0.16.0-dev.7",
|
|
12
|
+
"@prisma-next/framework-components": "0.16.0-dev.7",
|
|
13
|
+
"@prisma-next/sql-contract": "0.16.0-dev.7",
|
|
14
|
+
"@prisma-next/utils": "0.16.0-dev.7",
|
|
15
15
|
"arktype": "^2.2.2",
|
|
16
16
|
"pathe": "^2.0.3",
|
|
17
17
|
"ts-toolbelt": "^9.6.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@prisma-next/test-utils": "0.16.0-dev.
|
|
21
|
-
"@prisma-next/tsconfig": "0.16.0-dev.
|
|
20
|
+
"@prisma-next/test-utils": "0.16.0-dev.7",
|
|
21
|
+
"@prisma-next/tsconfig": "0.16.0-dev.7",
|
|
22
22
|
"@types/pg": "8.20.0",
|
|
23
23
|
"pg": "8.22.0",
|
|
24
|
-
"@prisma-next/tsdown": "0.16.0-dev.
|
|
24
|
+
"@prisma-next/tsdown": "0.16.0-dev.7",
|
|
25
25
|
"tsdown": "0.22.8",
|
|
26
26
|
"typescript": "5.9.3",
|
|
27
27
|
"vitest": "4.1.10"
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
validateAuthoringHelperArguments,
|
|
11
11
|
} from '@prisma-next/framework-components/authoring';
|
|
12
12
|
import { type StorageTypeInstance, toStorageTypeInstance } from '@prisma-next/sql-contract/types';
|
|
13
|
+
import { contractError } from './contract-errors';
|
|
13
14
|
|
|
14
15
|
export type RuntimeNamedConstraintSpec = {
|
|
15
16
|
readonly name?: string;
|
|
@@ -33,8 +34,10 @@ const blockedSegments = new Set(['__proto__', 'constructor', 'prototype']);
|
|
|
33
34
|
|
|
34
35
|
function assertSafeHelperKey(key: string, path: readonly string[]): void {
|
|
35
36
|
if (blockedSegments.has(key)) {
|
|
36
|
-
throw
|
|
37
|
+
throw contractError(
|
|
38
|
+
'CONTRACT.PACK_CONTRIBUTION_INVALID',
|
|
37
39
|
`Invalid authoring helper "${[...path, key].join('.')}". Helper path segments must not use "${key}".`,
|
|
40
|
+
{ meta: { helperPath: [...path, key].join('.'), segment: key } },
|
|
38
41
|
);
|
|
39
42
|
}
|
|
40
43
|
}
|
|
@@ -83,8 +86,16 @@ export function createFieldPresetHelper<Result>(options: {
|
|
|
83
86
|
const declaredArguments = options.descriptor.args ?? [];
|
|
84
87
|
|
|
85
88
|
if (acceptsNamedConstraintOptions && rawArgs.length > declaredArguments.length + 1) {
|
|
86
|
-
throw
|
|
89
|
+
throw contractError(
|
|
90
|
+
'CONTRACT.ARGUMENT_INVALID',
|
|
87
91
|
`${options.helperPath} expects at most ${declaredArguments.length + 1} argument(s), received ${rawArgs.length}`,
|
|
92
|
+
{
|
|
93
|
+
meta: {
|
|
94
|
+
helperPath: options.helperPath,
|
|
95
|
+
expected: declaredArguments.length + 1,
|
|
96
|
+
received: rawArgs.length,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
88
99
|
);
|
|
89
100
|
}
|
|
90
101
|
|
|
@@ -94,8 +105,10 @@ export function createFieldPresetHelper<Result>(options: {
|
|
|
94
105
|
if (acceptsNamedConstraintOptions && rawArgs.length === declaredArguments.length + 1) {
|
|
95
106
|
const maybeNamedConstraintOptions = rawArgs.at(-1);
|
|
96
107
|
if (!isNamedConstraintOptionsLike(maybeNamedConstraintOptions)) {
|
|
97
|
-
throw
|
|
108
|
+
throw contractError(
|
|
109
|
+
'CONTRACT.ARGUMENT_INVALID',
|
|
98
110
|
`${options.helperPath} accepts an optional trailing { name?: string } constraint options object`,
|
|
111
|
+
{ meta: { helperPath: options.helperPath } },
|
|
99
112
|
);
|
|
100
113
|
}
|
|
101
114
|
namedConstraintOptions = maybeNamedConstraintOptions;
|
package/src/build-contract.ts
CHANGED
|
@@ -58,6 +58,7 @@ import { validateStorageSemantics } from '@prisma-next/sql-contract/validators';
|
|
|
58
58
|
import { deriveValueSetFromEntity } from '@prisma-next/sql-contract/value-set-derivation-hook';
|
|
59
59
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
60
60
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
61
|
+
import { InternalError } from '@prisma-next/utils/internal-error';
|
|
61
62
|
import type {
|
|
62
63
|
ContractDefinition,
|
|
63
64
|
FieldNode,
|
|
@@ -65,6 +66,7 @@ import type {
|
|
|
65
66
|
RelationNode,
|
|
66
67
|
ValueObjectFieldNode,
|
|
67
68
|
} from './contract-definition';
|
|
69
|
+
import { contractError } from './contract-errors';
|
|
68
70
|
|
|
69
71
|
type DomainFieldRef =
|
|
70
72
|
| { readonly kind: 'scalar'; readonly many?: boolean }
|
|
@@ -92,7 +94,7 @@ function encodeColumnDefault(
|
|
|
92
94
|
}
|
|
93
95
|
if (many) {
|
|
94
96
|
if (!Array.isArray(defaultInput.value)) {
|
|
95
|
-
throw new
|
|
97
|
+
throw new InternalError(
|
|
96
98
|
`Literal default on a list column must be an array; received ${typeof defaultInput.value}. ` +
|
|
97
99
|
'A scalar default on a list field must be rejected at the authoring surface.',
|
|
98
100
|
);
|
|
@@ -114,7 +116,11 @@ function assertStorageSemantics(
|
|
|
114
116
|
): void {
|
|
115
117
|
const semanticErrors = validateStorageSemantics(contract.storage);
|
|
116
118
|
if (semanticErrors.length > 0) {
|
|
117
|
-
throw
|
|
119
|
+
throw contractError(
|
|
120
|
+
'CONTRACT.VALIDATION_FAILED',
|
|
121
|
+
`Contract semantic validation failed: ${semanticErrors.join('; ')}`,
|
|
122
|
+
{ meta: { errors: semanticErrors } },
|
|
123
|
+
);
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
const indexTypeRegistry = createIndexTypeRegistry();
|
|
@@ -130,8 +136,10 @@ function assertStorageSemantics(
|
|
|
130
136
|
registration === null ||
|
|
131
137
|
!Array.isArray((registration as { entries?: unknown }).entries)
|
|
132
138
|
) {
|
|
133
|
-
throw
|
|
139
|
+
throw contractError(
|
|
140
|
+
'CONTRACT.PACK_CONTRIBUTION_INVALID',
|
|
134
141
|
`Pack "${pack.id ?? '<unknown>'}" declares "indexTypes" but its value is not an IndexTypeRegistration (expected an object with an "entries" array; got ${typeof registration}).`,
|
|
142
|
+
{ meta: { packId: pack.id, contribution: 'indexTypes', reason: 'invalid-shape' } },
|
|
135
143
|
);
|
|
136
144
|
}
|
|
137
145
|
for (const entry of (registration as IndexTypeRegistration<IndexTypeMap>).entries) {
|
|
@@ -158,8 +166,10 @@ function assertKnownTargetModel(
|
|
|
158
166
|
targetNamespaceId !== undefined && targetNamespaceId.length > 0
|
|
159
167
|
? `${targetNamespaceId}.${targetModelName}`
|
|
160
168
|
: targetModelName;
|
|
161
|
-
throw
|
|
169
|
+
throw contractError(
|
|
170
|
+
'CONTRACT.MODEL_UNKNOWN',
|
|
162
171
|
`${context} on model "${sourceModelName}" references unknown model "${qualified}"`,
|
|
172
|
+
{ meta: { sourceModel: sourceModelName, targetModel: qualified, context } },
|
|
163
173
|
);
|
|
164
174
|
}
|
|
165
175
|
return targetModel;
|
|
@@ -172,8 +182,17 @@ function assertTargetTableMatches(
|
|
|
172
182
|
context: string,
|
|
173
183
|
): void {
|
|
174
184
|
if (targetModel.tableName !== referencedTableName) {
|
|
175
|
-
throw
|
|
185
|
+
throw contractError(
|
|
186
|
+
'CONTRACT.TABLE_MISMATCH',
|
|
176
187
|
`${context} on model "${sourceModelName}" references table "${referencedTableName}" but model "${targetModel.modelName}" maps to "${targetModel.tableName}"`,
|
|
188
|
+
{
|
|
189
|
+
meta: {
|
|
190
|
+
sourceModel: sourceModelName,
|
|
191
|
+
referencedTable: referencedTableName,
|
|
192
|
+
mappedTable: targetModel.tableName,
|
|
193
|
+
context,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
177
196
|
);
|
|
178
197
|
}
|
|
179
198
|
}
|
|
@@ -310,8 +329,10 @@ function collectEntityFromColumn(
|
|
|
310
329
|
const forKind = forNs[entityRef.entityKind] ?? {};
|
|
311
330
|
const existing = forKind[entityRef.entityName];
|
|
312
331
|
if (existing !== undefined && existing !== entityRef.entity) {
|
|
313
|
-
throw
|
|
332
|
+
throw contractError(
|
|
333
|
+
'CONTRACT.NAME_DUPLICATE',
|
|
314
334
|
`buildSqlContractFromDefinition: two different "${entityRef.entityKind}" entities named "${entityRef.entityName}" in namespace "${namespaceId}" — pack-entity names must be unique per namespace.`,
|
|
335
|
+
{ meta: { kind: entityRef.entityKind, name: entityRef.entityName, namespaceId } },
|
|
315
336
|
);
|
|
316
337
|
}
|
|
317
338
|
forKind[entityRef.entityName] = entityRef.entity;
|
|
@@ -344,8 +365,10 @@ function mergeColumnAndAttachedEntities(
|
|
|
344
365
|
for (const [name, entity] of Object.entries(columnForKind ?? {})) {
|
|
345
366
|
const existing = attachedForKind?.[name];
|
|
346
367
|
if (existing !== undefined && existing !== entity) {
|
|
347
|
-
throw
|
|
368
|
+
throw contractError(
|
|
369
|
+
'CONTRACT.NAME_DUPLICATE',
|
|
348
370
|
`buildSqlContractFromDefinition: two different "${kind}" entities named "${name}" in namespace "${namespaceId}" — a column-referenced entity conflicts with an attached one; pack-entity names must be unique per namespace.`,
|
|
371
|
+
{ meta: { kind, name, namespaceId } },
|
|
349
372
|
);
|
|
350
373
|
}
|
|
351
374
|
}
|
|
@@ -377,8 +400,10 @@ function buildThroughDescriptor(
|
|
|
377
400
|
defaultNamespaceId: string,
|
|
378
401
|
): ContractRelationThrough {
|
|
379
402
|
if (!tableNamespaceByName.has(through.table)) {
|
|
380
|
-
throw
|
|
403
|
+
throw contractError(
|
|
404
|
+
'CONTRACT.MODEL_UNKNOWN',
|
|
381
405
|
`buildSqlContractFromDefinition: junction table "${through.table}" for relation "${modelName}.${fieldName}" is not a declared model.`,
|
|
406
|
+
{ meta: { sourceModel: modelName, relationName: fieldName, junctionTable: through.table } },
|
|
382
407
|
);
|
|
383
408
|
}
|
|
384
409
|
// Junction table names are unique per namespace, not globally. Prefer the
|
|
@@ -405,8 +430,10 @@ function targetColumnsForJunction(targetModel: ModelNode, fieldName: string): re
|
|
|
405
430
|
if (firstUnique) {
|
|
406
431
|
return firstUnique.columns;
|
|
407
432
|
}
|
|
408
|
-
throw
|
|
433
|
+
throw contractError(
|
|
434
|
+
'CONTRACT.IDENTITY_INVALID',
|
|
409
435
|
`M:N target model "${targetModel.modelName}" (relation field "${fieldName}") has no primary id or unique key to derive junction targetColumns.`,
|
|
436
|
+
{ meta: { modelName: targetModel.modelName, reason: 'no-key-for-junction-target-columns' } },
|
|
410
437
|
);
|
|
411
438
|
}
|
|
412
439
|
|
|
@@ -515,8 +542,10 @@ function assertNoManagedEntityKinds(
|
|
|
515
542
|
if (entitiesForNs === undefined) return;
|
|
516
543
|
for (const kind of Object.keys(entitiesForNs)) {
|
|
517
544
|
if (MANAGED_ENTRY_KINDS.has(kind)) {
|
|
518
|
-
throw
|
|
545
|
+
throw contractError(
|
|
546
|
+
'CONTRACT.ENTITY_KIND_INVALID',
|
|
519
547
|
`buildSqlContractFromDefinition: attached entity in namespace "${namespaceId}" declares entry kind "${kind}", which is managed by the framework (table/valueSet) and cannot be attached.`,
|
|
548
|
+
{ meta: { entityKind: kind, namespaceId } },
|
|
520
549
|
);
|
|
521
550
|
}
|
|
522
551
|
}
|
|
@@ -599,8 +628,10 @@ function mergeNamespaceValueSets(
|
|
|
599
628
|
if (enumValueSets !== undefined && packValueSets !== undefined) {
|
|
600
629
|
for (const name of Object.keys(packValueSets)) {
|
|
601
630
|
if (Object.hasOwn(enumValueSets, name)) {
|
|
602
|
-
throw
|
|
631
|
+
throw contractError(
|
|
632
|
+
'CONTRACT.NAME_DUPLICATE',
|
|
603
633
|
`buildSqlContractFromDefinition: value-set "${name}" in namespace "${namespaceId}" is derived from both an enum and a pack entity — names must be unique per namespace.`,
|
|
634
|
+
{ meta: { kind: 'valueSet', name, namespaceId } },
|
|
604
635
|
);
|
|
605
636
|
}
|
|
606
637
|
}
|
|
@@ -690,13 +721,29 @@ export function buildSqlContractFromDefinition(
|
|
|
690
721
|
: undefined;
|
|
691
722
|
if (executionDefaultPhases) {
|
|
692
723
|
if (field.default !== undefined) {
|
|
693
|
-
throw
|
|
724
|
+
throw contractError(
|
|
725
|
+
'CONTRACT.DEFAULT_INVALID',
|
|
694
726
|
`Field "${semanticModel.modelName}.${field.fieldName}" cannot define both default and executionDefaults.`,
|
|
727
|
+
{
|
|
728
|
+
meta: {
|
|
729
|
+
modelName: semanticModel.modelName,
|
|
730
|
+
fieldName: field.fieldName,
|
|
731
|
+
reason: 'default-and-executionDefaults',
|
|
732
|
+
},
|
|
733
|
+
},
|
|
695
734
|
);
|
|
696
735
|
}
|
|
697
736
|
if (field.nullable) {
|
|
698
|
-
throw
|
|
737
|
+
throw contractError(
|
|
738
|
+
'CONTRACT.DEFAULT_INVALID',
|
|
699
739
|
`Field "${semanticModel.modelName}.${field.fieldName}" cannot be nullable when executionDefaults are present.`,
|
|
740
|
+
{
|
|
741
|
+
meta: {
|
|
742
|
+
modelName: semanticModel.modelName,
|
|
743
|
+
fieldName: field.fieldName,
|
|
744
|
+
reason: 'nullable-with-executionDefaults',
|
|
745
|
+
},
|
|
746
|
+
},
|
|
700
747
|
);
|
|
701
748
|
}
|
|
702
749
|
}
|
|
@@ -914,8 +961,10 @@ export function buildSqlContractFromDefinition(
|
|
|
914
961
|
tablesByNamespace[namespaceId] = nsTables;
|
|
915
962
|
}
|
|
916
963
|
if (nsTables[tableName] !== undefined) {
|
|
917
|
-
throw
|
|
964
|
+
throw contractError(
|
|
965
|
+
'CONTRACT.NAME_DUPLICATE',
|
|
918
966
|
`buildSqlContractFromDefinition: duplicate table "${tableName}" in namespace "${namespaceId}".`,
|
|
967
|
+
{ meta: { kind: 'table', name: tableName, namespaceId } },
|
|
919
968
|
);
|
|
920
969
|
}
|
|
921
970
|
nsTables[tableName] = tableInput;
|
|
@@ -978,8 +1027,16 @@ export function buildSqlContractFromDefinition(
|
|
|
978
1027
|
|
|
979
1028
|
if (relation.cardinality === 'N:M') {
|
|
980
1029
|
if (!relation.through) {
|
|
981
|
-
throw
|
|
1030
|
+
throw contractError(
|
|
1031
|
+
'CONTRACT.RELATION_INVALID',
|
|
982
1032
|
`Relation "${semanticModel.modelName}.${relation.fieldName}" with cardinality "N:M" requires through metadata`,
|
|
1033
|
+
{
|
|
1034
|
+
meta: {
|
|
1035
|
+
modelName: semanticModel.modelName,
|
|
1036
|
+
relationName: relation.fieldName,
|
|
1037
|
+
reason: 'many-to-many-missing-through',
|
|
1038
|
+
},
|
|
1039
|
+
},
|
|
983
1040
|
);
|
|
984
1041
|
}
|
|
985
1042
|
modelRelations[relation.fieldName] = {
|
|
@@ -1062,8 +1119,16 @@ export function buildSqlContractFromDefinition(
|
|
|
1062
1119
|
const storageValueSetsByNs: Record<string, Record<string, StorageValueSetInput>> = {};
|
|
1063
1120
|
for (const [enumName, handle] of Object.entries(definition.enums ?? {})) {
|
|
1064
1121
|
if (enumName !== handle.enumName) {
|
|
1065
|
-
throw
|
|
1122
|
+
throw contractError(
|
|
1123
|
+
'CONTRACT.ENUM_INVALID',
|
|
1066
1124
|
`enum declaration key "${enumName}" must match enumType name "${handle.enumName}". Aliases are not supported.`,
|
|
1125
|
+
{
|
|
1126
|
+
meta: {
|
|
1127
|
+
enumName: handle.enumName,
|
|
1128
|
+
declarationKey: enumName,
|
|
1129
|
+
reason: 'key-name-mismatch',
|
|
1130
|
+
},
|
|
1131
|
+
},
|
|
1067
1132
|
);
|
|
1068
1133
|
}
|
|
1069
1134
|
const nsId = defaultNamespaceId;
|
|
@@ -37,6 +37,7 @@ import type {
|
|
|
37
37
|
ScalarFieldBuilder,
|
|
38
38
|
} from './contract-dsl';
|
|
39
39
|
import { buildFieldPreset, field, model, rel } from './contract-dsl';
|
|
40
|
+
import { contractError } from './contract-errors';
|
|
40
41
|
import type { MergeExtensionIndexTypes } from './contract-types';
|
|
41
42
|
|
|
42
43
|
type ExtractTypeNamespaceFromPack<Pack> = ExtractAuthoringNamespaceFromPack<
|
|
@@ -224,8 +225,10 @@ const RESERVED_HELPER_KEYS: readonly string[] = ['field', 'model', 'rel', 'type'
|
|
|
224
225
|
function assertNoBuiltInEntityCollisions(namespace: AuthoringEntityTypeNamespace): void {
|
|
225
226
|
const collisions = Object.keys(namespace).filter((name) => RESERVED_HELPER_KEYS.includes(name));
|
|
226
227
|
if (collisions.length > 0) {
|
|
227
|
-
throw
|
|
228
|
+
throw contractError(
|
|
229
|
+
'CONTRACT.PACK_CONTRIBUTION_INVALID',
|
|
228
230
|
`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(', ')}.`,
|
|
231
|
+
{ meta: { collisions, reservedKeys: RESERVED_HELPER_KEYS } },
|
|
229
232
|
);
|
|
230
233
|
}
|
|
231
234
|
}
|
|
@@ -252,8 +255,10 @@ function createComposedFieldHelpers(
|
|
|
252
255
|
const coreHelperNames = new Set(Object.keys(coreFieldHelpers));
|
|
253
256
|
for (const helperName of Object.keys(helperNamespace)) {
|
|
254
257
|
if (coreHelperNames.has(helperName)) {
|
|
255
|
-
throw
|
|
258
|
+
throw contractError(
|
|
259
|
+
'CONTRACT.PACK_CONTRIBUTION_INVALID',
|
|
256
260
|
`Duplicate authoring field helper "${helperName}". Core field helpers reserve that name.`,
|
|
261
|
+
{ meta: { helperName } },
|
|
257
262
|
);
|
|
258
263
|
}
|
|
259
264
|
}
|
package/src/config-types.ts
CHANGED
|
@@ -5,9 +5,11 @@ import type { Contract, ControlPolicy } from '@prisma-next/contract/types';
|
|
|
5
5
|
import type { TargetPackRef } from '@prisma-next/framework-components/components';
|
|
6
6
|
import type { SqlNamespaceBase, SqlNamespaceInput } from '@prisma-next/sql-contract/types';
|
|
7
7
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
8
|
+
import { InternalError } from '@prisma-next/utils/internal-error';
|
|
8
9
|
import { ok } from '@prisma-next/utils/result';
|
|
9
10
|
import { extname } from 'pathe';
|
|
10
11
|
import { buildSqlContractFromDefinition } from './build-contract';
|
|
12
|
+
import { contractError } from './contract-errors';
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* Derives the emit output path from the TS contract input so artefacts land
|
|
@@ -76,15 +78,17 @@ export function typescriptContractFromPath(
|
|
|
76
78
|
load: async (context) => {
|
|
77
79
|
const [absolutePath] = context.resolvedInputs;
|
|
78
80
|
if (absolutePath === undefined) {
|
|
79
|
-
throw new
|
|
81
|
+
throw new InternalError(
|
|
80
82
|
'typescriptContractFromPath: context.resolvedInputs is empty. The CLI config loader should populate it positional-matched with source.inputs.',
|
|
81
83
|
);
|
|
82
84
|
}
|
|
83
85
|
const mod = await import(pathToFileURL(absolutePath).href);
|
|
84
86
|
const contract: Contract | undefined = mod.default ?? mod.contract;
|
|
85
87
|
if (contract === undefined) {
|
|
86
|
-
throw
|
|
88
|
+
throw contractError(
|
|
89
|
+
'CONTRACT.MODULE_EXPORT_MISSING',
|
|
87
90
|
`typescriptContractFromPath: module at "${absolutePath}" has no "default" or "contract" export.`,
|
|
91
|
+
{ meta: { path: absolutePath } },
|
|
88
92
|
);
|
|
89
93
|
}
|
|
90
94
|
return ok(applySpecifierDefaultControlPolicy(contract, options?.defaultControlPolicy));
|
package/src/contract-builder.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
type ScalarFieldBuilder,
|
|
34
34
|
type SqlStageSpec,
|
|
35
35
|
} from './contract-dsl';
|
|
36
|
+
import { contractError } from './contract-errors';
|
|
36
37
|
import { buildContractDefinition } from './contract-lowering';
|
|
37
38
|
import type { SqlContractResult } from './contract-types';
|
|
38
39
|
import type { EnumTypeHandle } from './enum-type';
|
|
@@ -125,14 +126,24 @@ function validateTargetPackRef(
|
|
|
125
126
|
target: TargetPackRef<'sql', string>,
|
|
126
127
|
): void {
|
|
127
128
|
if (family.familyId !== 'sql') {
|
|
128
|
-
throw
|
|
129
|
+
throw contractError(
|
|
130
|
+
'CONTRACT.PACK_FAMILY_MISMATCH',
|
|
129
131
|
`defineContract only accepts SQL family packs. Received family "${family.familyId}".`,
|
|
132
|
+
{ meta: { packId: family.id, packFamilyId: family.familyId, contractFamilyId: 'sql' } },
|
|
130
133
|
);
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
if (target.familyId !== family.familyId) {
|
|
134
|
-
throw
|
|
137
|
+
throw contractError(
|
|
138
|
+
'CONTRACT.PACK_FAMILY_MISMATCH',
|
|
135
139
|
`target pack "${target.id}" targets family "${target.familyId}" but contract family is "${family.familyId}".`,
|
|
140
|
+
{
|
|
141
|
+
meta: {
|
|
142
|
+
packId: target.id,
|
|
143
|
+
packFamilyId: target.familyId,
|
|
144
|
+
contractFamilyId: family.familyId,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
136
147
|
);
|
|
137
148
|
}
|
|
138
149
|
}
|
|
@@ -161,33 +172,51 @@ function validateNamespaceDeclarations(
|
|
|
161
172
|
}
|
|
162
173
|
|
|
163
174
|
if (target.targetId === 'sqlite' && namespaces.length > 0) {
|
|
164
|
-
throw
|
|
175
|
+
throw contractError(
|
|
176
|
+
'CONTRACT.NAMESPACE_UNSUPPORTED',
|
|
165
177
|
`defineContract: SQLite contracts cannot declare namespaces (SQLite has no schema concept; emitted DDL is always unqualified). Received namespaces: [${namespaces
|
|
166
178
|
.map((name) => `"${name}"`)
|
|
167
179
|
.join(', ')}].`,
|
|
180
|
+
{ meta: { namespaces, targetId: target.targetId } },
|
|
168
181
|
);
|
|
169
182
|
}
|
|
170
183
|
|
|
171
184
|
const seen = new Set<string>();
|
|
172
185
|
for (const namespace of namespaces) {
|
|
173
186
|
if (namespace.length === 0) {
|
|
174
|
-
throw
|
|
187
|
+
throw contractError(
|
|
188
|
+
'CONTRACT.NAMESPACE_INVALID',
|
|
189
|
+
'defineContract: namespace names cannot be empty.',
|
|
190
|
+
{ meta: { namespace, reason: 'empty' } },
|
|
191
|
+
);
|
|
175
192
|
}
|
|
176
193
|
if (namespace.trim().length === 0) {
|
|
177
|
-
throw
|
|
194
|
+
throw contractError(
|
|
195
|
+
'CONTRACT.NAMESPACE_INVALID',
|
|
196
|
+
`defineContract: namespace name "${namespace}" cannot be whitespace-only.`,
|
|
197
|
+
{ meta: { namespace, reason: 'whitespace-only' } },
|
|
198
|
+
);
|
|
178
199
|
}
|
|
179
200
|
if (namespace === '__unbound__' || namespace === '__unspecified__') {
|
|
180
|
-
throw
|
|
201
|
+
throw contractError(
|
|
202
|
+
'CONTRACT.NAMESPACE_INVALID',
|
|
181
203
|
`defineContract: namespace name "${namespace}" is a reserved IR sentinel and cannot appear in the declared namespaces list.`,
|
|
204
|
+
{ meta: { namespace, reason: 'reserved-ir-sentinel' } },
|
|
182
205
|
);
|
|
183
206
|
}
|
|
184
207
|
if (target.targetId === 'postgres' && namespace === 'unbound') {
|
|
185
|
-
throw
|
|
208
|
+
throw contractError(
|
|
209
|
+
'CONTRACT.NAMESPACE_INVALID',
|
|
186
210
|
`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).`,
|
|
211
|
+
{ meta: { namespace, reason: 'reserved-by-postgres' } },
|
|
187
212
|
);
|
|
188
213
|
}
|
|
189
214
|
if (seen.has(namespace)) {
|
|
190
|
-
throw
|
|
215
|
+
throw contractError(
|
|
216
|
+
'CONTRACT.NAME_DUPLICATE',
|
|
217
|
+
`defineContract: namespaces list contains duplicate entry "${namespace}".`,
|
|
218
|
+
{ meta: { kind: 'namespace', name: namespace } },
|
|
219
|
+
);
|
|
191
220
|
}
|
|
192
221
|
seen.add(namespace);
|
|
193
222
|
}
|
|
@@ -228,20 +257,26 @@ function validatePerModelNamespaces(
|
|
|
228
257
|
}
|
|
229
258
|
|
|
230
259
|
if (target.targetId === 'sqlite') {
|
|
231
|
-
throw
|
|
260
|
+
throw contractError(
|
|
261
|
+
'CONTRACT.NAMESPACE_UNSUPPORTED',
|
|
232
262
|
`defineContract: model "${modelKey}" sets \`namespace: "${perModelNamespace}"\` but the target is SQLite (SQLite has no schema concept; remove the per-model \`namespace\` field).`,
|
|
263
|
+
{ meta: { modelKey, namespace: perModelNamespace, targetId: target.targetId } },
|
|
233
264
|
);
|
|
234
265
|
}
|
|
235
266
|
|
|
236
267
|
if (perModelNamespace === '__unbound__' || perModelNamespace === '__unspecified__') {
|
|
237
|
-
throw
|
|
268
|
+
throw contractError(
|
|
269
|
+
'CONTRACT.NAMESPACE_INVALID',
|
|
238
270
|
`defineContract: model "${modelKey}" sets \`namespace: "${perModelNamespace}"\` but that name is a reserved IR sentinel and cannot appear in user code.`,
|
|
271
|
+
{ meta: { modelKey, namespace: perModelNamespace, reason: 'reserved-ir-sentinel' } },
|
|
239
272
|
);
|
|
240
273
|
}
|
|
241
274
|
|
|
242
275
|
if (target.targetId === 'postgres' && perModelNamespace === 'unbound') {
|
|
243
|
-
throw
|
|
276
|
+
throw contractError(
|
|
277
|
+
'CONTRACT.NAMESPACE_INVALID',
|
|
244
278
|
`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).`,
|
|
279
|
+
{ meta: { modelKey, namespace: perModelNamespace, reason: 'reserved-by-postgres' } },
|
|
245
280
|
);
|
|
246
281
|
}
|
|
247
282
|
|
|
@@ -250,8 +285,10 @@ function validatePerModelNamespaces(
|
|
|
250
285
|
declaredNamespaces.size > 0
|
|
251
286
|
? ` Declared namespaces: [${[...declaredNamespaces].map((name) => `"${name}"`).join(', ')}].`
|
|
252
287
|
: ' The contract does not declare any namespaces; add `namespaces: ["…"]` to `defineContract` first.';
|
|
253
|
-
throw
|
|
288
|
+
throw contractError(
|
|
289
|
+
'CONTRACT.NAMESPACE_UNKNOWN',
|
|
254
290
|
`defineContract: model "${modelKey}" references namespace "${perModelNamespace}" but that name does not appear in the contract's declared \`namespaces\` list.${hint}`,
|
|
291
|
+
{ meta: { modelKey, namespace: perModelNamespace, declared: [...declaredNamespaces] } },
|
|
255
292
|
);
|
|
256
293
|
}
|
|
257
294
|
}
|
|
@@ -267,20 +304,38 @@ function validateExtensionPackRefs(
|
|
|
267
304
|
|
|
268
305
|
for (const packRef of Object.values(extensionPacks)) {
|
|
269
306
|
if (packRef.kind !== 'extension') {
|
|
270
|
-
throw
|
|
307
|
+
throw contractError(
|
|
308
|
+
'CONTRACT.PACK_REF_INVALID',
|
|
271
309
|
`defineContract only accepts extension pack refs in extensionPacks. Received kind "${packRef.kind}".`,
|
|
310
|
+
{ meta: { packId: packRef.id, kind: packRef.kind } },
|
|
272
311
|
);
|
|
273
312
|
}
|
|
274
313
|
|
|
275
314
|
if (packRef.familyId !== target.familyId) {
|
|
276
|
-
throw
|
|
315
|
+
throw contractError(
|
|
316
|
+
'CONTRACT.PACK_FAMILY_MISMATCH',
|
|
277
317
|
`extension pack "${packRef.id}" targets family "${packRef.familyId}" but contract target family is "${target.familyId}".`,
|
|
318
|
+
{
|
|
319
|
+
meta: {
|
|
320
|
+
packId: packRef.id,
|
|
321
|
+
packFamilyId: packRef.familyId,
|
|
322
|
+
contractFamilyId: target.familyId,
|
|
323
|
+
},
|
|
324
|
+
},
|
|
278
325
|
);
|
|
279
326
|
}
|
|
280
327
|
|
|
281
328
|
if (packRef.targetId && packRef.targetId !== target.targetId) {
|
|
282
|
-
throw
|
|
329
|
+
throw contractError(
|
|
330
|
+
'CONTRACT.PACK_TARGET_MISMATCH',
|
|
283
331
|
`extension pack "${packRef.id}" targets "${packRef.targetId}" but contract target is "${target.targetId}".`,
|
|
332
|
+
{
|
|
333
|
+
meta: {
|
|
334
|
+
packId: packRef.id,
|
|
335
|
+
packTargetId: packRef.targetId,
|
|
336
|
+
contractTargetId: target.targetId,
|
|
337
|
+
},
|
|
338
|
+
},
|
|
284
339
|
);
|
|
285
340
|
}
|
|
286
341
|
}
|