@prisma-next/sql-contract-ts 0.16.0-dev.3 → 0.16.0-dev.31
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 +244 -87
- 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 +94 -42
- package/src/contract-definition.ts +4 -1
- package/src/contract-dsl.ts +59 -15
- package/src/contract-errors.ts +39 -0
- package/src/contract-lowering.ts +116 -44
- package/src/contract-types.ts +7 -7
- package/dist/build-contract-CAX6inbk.mjs.map +0 -1
package/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.31",
|
|
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.31",
|
|
10
|
+
"@prisma-next/contract": "0.16.0-dev.31",
|
|
11
|
+
"@prisma-next/contract-authoring": "0.16.0-dev.31",
|
|
12
|
+
"@prisma-next/framework-components": "0.16.0-dev.31",
|
|
13
|
+
"@prisma-next/sql-contract": "0.16.0-dev.31",
|
|
14
|
+
"@prisma-next/utils": "0.16.0-dev.31",
|
|
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.31",
|
|
21
|
+
"@prisma-next/tsconfig": "0.16.0-dev.31",
|
|
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.31",
|
|
25
25
|
"tsdown": "0.22.8",
|
|
26
26
|
"typescript": "5.9.3",
|
|
27
27
|
"vitest": "4.1.10"
|
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
},
|
|
27
27
|
"storageHash": {
|
|
28
28
|
"type": "string",
|
|
29
|
-
"pattern": "^
|
|
29
|
+
"pattern": "^[a-f0-9]{64}$",
|
|
30
30
|
"description": "SHA-256 hash of the storage section (DB-satisfied expectations)"
|
|
31
31
|
},
|
|
32
32
|
"profileHash": {
|
|
33
33
|
"type": "string",
|
|
34
|
-
"pattern": "^
|
|
34
|
+
"pattern": "^[a-f0-9]{64}$",
|
|
35
35
|
"description": "SHA-256 hash of the pinned capability profile (capability keys/variants and adapter pins)"
|
|
36
36
|
},
|
|
37
37
|
"capabilities": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
|
-
"
|
|
47
|
+
"extensions": {
|
|
48
48
|
"type": "object",
|
|
49
49
|
"description": "Extension packs and their configuration",
|
|
50
50
|
"additionalProperties": true
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
"properties": {
|
|
105
105
|
"executionHash": {
|
|
106
106
|
"type": "string",
|
|
107
|
-
"pattern": "^
|
|
107
|
+
"pattern": "^[a-f0-9]{64}$",
|
|
108
108
|
"description": "SHA-256 hash of the execution section (client-side behavior)"
|
|
109
109
|
},
|
|
110
110
|
"mutations": {
|
|
@@ -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
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
type ForeignKeyAuthoringInput,
|
|
37
37
|
materializeForeignKeysAndIndexes,
|
|
38
38
|
} from '@prisma-next/sql-contract/foreign-key-materialization';
|
|
39
|
+
import { lowerAuthoredIndex } from '@prisma-next/sql-contract/index-naming';
|
|
39
40
|
import { validateIndexTypes } from '@prisma-next/sql-contract/index-type-validation';
|
|
40
41
|
import {
|
|
41
42
|
createIndexTypeRegistry,
|
|
@@ -58,6 +59,7 @@ import { validateStorageSemantics } from '@prisma-next/sql-contract/validators';
|
|
|
58
59
|
import { deriveValueSetFromEntity } from '@prisma-next/sql-contract/value-set-derivation-hook';
|
|
59
60
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
60
61
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
62
|
+
import { InternalError } from '@prisma-next/utils/internal-error';
|
|
61
63
|
import type {
|
|
62
64
|
ContractDefinition,
|
|
63
65
|
FieldNode,
|
|
@@ -65,6 +67,7 @@ import type {
|
|
|
65
67
|
RelationNode,
|
|
66
68
|
ValueObjectFieldNode,
|
|
67
69
|
} from './contract-definition';
|
|
70
|
+
import { contractError } from './contract-errors';
|
|
68
71
|
|
|
69
72
|
type DomainFieldRef =
|
|
70
73
|
| { readonly kind: 'scalar'; readonly many?: boolean }
|
|
@@ -92,7 +95,7 @@ function encodeColumnDefault(
|
|
|
92
95
|
}
|
|
93
96
|
if (many) {
|
|
94
97
|
if (!Array.isArray(defaultInput.value)) {
|
|
95
|
-
throw new
|
|
98
|
+
throw new InternalError(
|
|
96
99
|
`Literal default on a list column must be an array; received ${typeof defaultInput.value}. ` +
|
|
97
100
|
'A scalar default on a list field must be rejected at the authoring surface.',
|
|
98
101
|
);
|
|
@@ -114,13 +117,17 @@ function assertStorageSemantics(
|
|
|
114
117
|
): void {
|
|
115
118
|
const semanticErrors = validateStorageSemantics(contract.storage);
|
|
116
119
|
if (semanticErrors.length > 0) {
|
|
117
|
-
throw
|
|
120
|
+
throw contractError(
|
|
121
|
+
'CONTRACT.VALIDATION_FAILED',
|
|
122
|
+
`Contract semantic validation failed: ${semanticErrors.join('; ')}`,
|
|
123
|
+
{ meta: { errors: semanticErrors } },
|
|
124
|
+
);
|
|
118
125
|
}
|
|
119
126
|
|
|
120
127
|
const indexTypeRegistry = createIndexTypeRegistry();
|
|
121
128
|
const packsToRegister: ReadonlyArray<{ readonly id?: string; readonly indexTypes?: unknown }> = [
|
|
122
129
|
definition.target,
|
|
123
|
-
...Object.values(definition.
|
|
130
|
+
...Object.values(definition.extensions ?? {}),
|
|
124
131
|
];
|
|
125
132
|
for (const pack of packsToRegister) {
|
|
126
133
|
const registration = pack.indexTypes;
|
|
@@ -130,8 +137,10 @@ function assertStorageSemantics(
|
|
|
130
137
|
registration === null ||
|
|
131
138
|
!Array.isArray((registration as { entries?: unknown }).entries)
|
|
132
139
|
) {
|
|
133
|
-
throw
|
|
140
|
+
throw contractError(
|
|
141
|
+
'CONTRACT.PACK_CONTRIBUTION_INVALID',
|
|
134
142
|
`Pack "${pack.id ?? '<unknown>'}" declares "indexTypes" but its value is not an IndexTypeRegistration (expected an object with an "entries" array; got ${typeof registration}).`,
|
|
143
|
+
{ meta: { packId: pack.id, contribution: 'indexTypes', reason: 'invalid-shape' } },
|
|
135
144
|
);
|
|
136
145
|
}
|
|
137
146
|
for (const entry of (registration as IndexTypeRegistration<IndexTypeMap>).entries) {
|
|
@@ -158,8 +167,10 @@ function assertKnownTargetModel(
|
|
|
158
167
|
targetNamespaceId !== undefined && targetNamespaceId.length > 0
|
|
159
168
|
? `${targetNamespaceId}.${targetModelName}`
|
|
160
169
|
: targetModelName;
|
|
161
|
-
throw
|
|
170
|
+
throw contractError(
|
|
171
|
+
'CONTRACT.MODEL_UNKNOWN',
|
|
162
172
|
`${context} on model "${sourceModelName}" references unknown model "${qualified}"`,
|
|
173
|
+
{ meta: { sourceModel: sourceModelName, targetModel: qualified, context } },
|
|
163
174
|
);
|
|
164
175
|
}
|
|
165
176
|
return targetModel;
|
|
@@ -172,8 +183,17 @@ function assertTargetTableMatches(
|
|
|
172
183
|
context: string,
|
|
173
184
|
): void {
|
|
174
185
|
if (targetModel.tableName !== referencedTableName) {
|
|
175
|
-
throw
|
|
186
|
+
throw contractError(
|
|
187
|
+
'CONTRACT.TABLE_MISMATCH',
|
|
176
188
|
`${context} on model "${sourceModelName}" references table "${referencedTableName}" but model "${targetModel.modelName}" maps to "${targetModel.tableName}"`,
|
|
189
|
+
{
|
|
190
|
+
meta: {
|
|
191
|
+
sourceModel: sourceModelName,
|
|
192
|
+
referencedTable: referencedTableName,
|
|
193
|
+
mappedTable: targetModel.tableName,
|
|
194
|
+
context,
|
|
195
|
+
},
|
|
196
|
+
},
|
|
177
197
|
);
|
|
178
198
|
}
|
|
179
199
|
}
|
|
@@ -310,8 +330,10 @@ function collectEntityFromColumn(
|
|
|
310
330
|
const forKind = forNs[entityRef.entityKind] ?? {};
|
|
311
331
|
const existing = forKind[entityRef.entityName];
|
|
312
332
|
if (existing !== undefined && existing !== entityRef.entity) {
|
|
313
|
-
throw
|
|
333
|
+
throw contractError(
|
|
334
|
+
'CONTRACT.NAME_DUPLICATE',
|
|
314
335
|
`buildSqlContractFromDefinition: two different "${entityRef.entityKind}" entities named "${entityRef.entityName}" in namespace "${namespaceId}" — pack-entity names must be unique per namespace.`,
|
|
336
|
+
{ meta: { kind: entityRef.entityKind, name: entityRef.entityName, namespaceId } },
|
|
315
337
|
);
|
|
316
338
|
}
|
|
317
339
|
forKind[entityRef.entityName] = entityRef.entity;
|
|
@@ -344,8 +366,10 @@ function mergeColumnAndAttachedEntities(
|
|
|
344
366
|
for (const [name, entity] of Object.entries(columnForKind ?? {})) {
|
|
345
367
|
const existing = attachedForKind?.[name];
|
|
346
368
|
if (existing !== undefined && existing !== entity) {
|
|
347
|
-
throw
|
|
369
|
+
throw contractError(
|
|
370
|
+
'CONTRACT.NAME_DUPLICATE',
|
|
348
371
|
`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.`,
|
|
372
|
+
{ meta: { kind, name, namespaceId } },
|
|
349
373
|
);
|
|
350
374
|
}
|
|
351
375
|
}
|
|
@@ -377,8 +401,10 @@ function buildThroughDescriptor(
|
|
|
377
401
|
defaultNamespaceId: string,
|
|
378
402
|
): ContractRelationThrough {
|
|
379
403
|
if (!tableNamespaceByName.has(through.table)) {
|
|
380
|
-
throw
|
|
404
|
+
throw contractError(
|
|
405
|
+
'CONTRACT.MODEL_UNKNOWN',
|
|
381
406
|
`buildSqlContractFromDefinition: junction table "${through.table}" for relation "${modelName}.${fieldName}" is not a declared model.`,
|
|
407
|
+
{ meta: { sourceModel: modelName, relationName: fieldName, junctionTable: through.table } },
|
|
382
408
|
);
|
|
383
409
|
}
|
|
384
410
|
// Junction table names are unique per namespace, not globally. Prefer the
|
|
@@ -405,8 +431,10 @@ function targetColumnsForJunction(targetModel: ModelNode, fieldName: string): re
|
|
|
405
431
|
if (firstUnique) {
|
|
406
432
|
return firstUnique.columns;
|
|
407
433
|
}
|
|
408
|
-
throw
|
|
434
|
+
throw contractError(
|
|
435
|
+
'CONTRACT.IDENTITY_INVALID',
|
|
409
436
|
`M:N target model "${targetModel.modelName}" (relation field "${fieldName}") has no primary id or unique key to derive junction targetColumns.`,
|
|
437
|
+
{ meta: { modelName: targetModel.modelName, reason: 'no-key-for-junction-target-columns' } },
|
|
410
438
|
);
|
|
411
439
|
}
|
|
412
440
|
|
|
@@ -515,8 +543,10 @@ function assertNoManagedEntityKinds(
|
|
|
515
543
|
if (entitiesForNs === undefined) return;
|
|
516
544
|
for (const kind of Object.keys(entitiesForNs)) {
|
|
517
545
|
if (MANAGED_ENTRY_KINDS.has(kind)) {
|
|
518
|
-
throw
|
|
546
|
+
throw contractError(
|
|
547
|
+
'CONTRACT.ENTITY_KIND_INVALID',
|
|
519
548
|
`buildSqlContractFromDefinition: attached entity in namespace "${namespaceId}" declares entry kind "${kind}", which is managed by the framework (table/valueSet) and cannot be attached.`,
|
|
549
|
+
{ meta: { entityKind: kind, namespaceId } },
|
|
520
550
|
);
|
|
521
551
|
}
|
|
522
552
|
}
|
|
@@ -528,7 +558,7 @@ function assertNoManagedEntityKinds(
|
|
|
528
558
|
* the same string a pack entity's entries-map key (`entries.<kind>`) uses.
|
|
529
559
|
* Mirrors `contract-psl`'s `buildEntityTypesByDiscriminator`, recomposed here
|
|
530
560
|
* from the packs `ContractDefinition` already carries (`target` +
|
|
531
|
-
* `
|
|
561
|
+
* `extensions`) since the TS assembler has no single pre-merged
|
|
532
562
|
* `AuthoringContributions` input to read the way the PSL interpreter does.
|
|
533
563
|
*/
|
|
534
564
|
function collectEntityTypeDescriptorsByDiscriminator(
|
|
@@ -544,7 +574,7 @@ function collectEntityTypeDescriptorsByDiscriminator(
|
|
|
544
574
|
}
|
|
545
575
|
}
|
|
546
576
|
};
|
|
547
|
-
const components = [definition.target, ...Object.values(definition.
|
|
577
|
+
const components = [definition.target, ...Object.values(definition.extensions ?? {})];
|
|
548
578
|
for (const component of components) {
|
|
549
579
|
const entityTypes = component.authoring?.entityTypes;
|
|
550
580
|
if (entityTypes !== undefined) {
|
|
@@ -599,8 +629,10 @@ function mergeNamespaceValueSets(
|
|
|
599
629
|
if (enumValueSets !== undefined && packValueSets !== undefined) {
|
|
600
630
|
for (const name of Object.keys(packValueSets)) {
|
|
601
631
|
if (Object.hasOwn(enumValueSets, name)) {
|
|
602
|
-
throw
|
|
632
|
+
throw contractError(
|
|
633
|
+
'CONTRACT.NAME_DUPLICATE',
|
|
603
634
|
`buildSqlContractFromDefinition: value-set "${name}" in namespace "${namespaceId}" is derived from both an enum and a pack entity — names must be unique per namespace.`,
|
|
635
|
+
{ meta: { kind: 'valueSet', name, namespaceId } },
|
|
604
636
|
);
|
|
605
637
|
}
|
|
606
638
|
}
|
|
@@ -690,13 +722,29 @@ export function buildSqlContractFromDefinition(
|
|
|
690
722
|
: undefined;
|
|
691
723
|
if (executionDefaultPhases) {
|
|
692
724
|
if (field.default !== undefined) {
|
|
693
|
-
throw
|
|
725
|
+
throw contractError(
|
|
726
|
+
'CONTRACT.DEFAULT_INVALID',
|
|
694
727
|
`Field "${semanticModel.modelName}.${field.fieldName}" cannot define both default and executionDefaults.`,
|
|
728
|
+
{
|
|
729
|
+
meta: {
|
|
730
|
+
modelName: semanticModel.modelName,
|
|
731
|
+
fieldName: field.fieldName,
|
|
732
|
+
reason: 'default-and-executionDefaults',
|
|
733
|
+
},
|
|
734
|
+
},
|
|
695
735
|
);
|
|
696
736
|
}
|
|
697
737
|
if (field.nullable) {
|
|
698
|
-
throw
|
|
738
|
+
throw contractError(
|
|
739
|
+
'CONTRACT.DEFAULT_INVALID',
|
|
699
740
|
`Field "${semanticModel.modelName}.${field.fieldName}" cannot be nullable when executionDefaults are present.`,
|
|
741
|
+
{
|
|
742
|
+
meta: {
|
|
743
|
+
modelName: semanticModel.modelName,
|
|
744
|
+
fieldName: field.fieldName,
|
|
745
|
+
reason: 'nullable-with-executionDefaults',
|
|
746
|
+
},
|
|
747
|
+
},
|
|
700
748
|
);
|
|
701
749
|
}
|
|
702
750
|
}
|
|
@@ -872,12 +920,15 @@ export function buildSqlContractFromDefinition(
|
|
|
872
920
|
columns: u.columns,
|
|
873
921
|
...ifDefined('name', u.name),
|
|
874
922
|
}));
|
|
875
|
-
const declaredIndexes = (semanticModel.indexes ?? []).map((i) =>
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
923
|
+
const declaredIndexes = (semanticModel.indexes ?? []).map((i) =>
|
|
924
|
+
lowerAuthoredIndex(tableName, {
|
|
925
|
+
columns: i.columns,
|
|
926
|
+
map: i.map,
|
|
927
|
+
name: i.name,
|
|
928
|
+
type: i.type,
|
|
929
|
+
options: i.options,
|
|
930
|
+
}),
|
|
931
|
+
);
|
|
881
932
|
const primaryKey = semanticModel.id
|
|
882
933
|
? { columns: semanticModel.id.columns, ...ifDefined('name', semanticModel.id.name) }
|
|
883
934
|
: undefined;
|
|
@@ -914,8 +965,10 @@ export function buildSqlContractFromDefinition(
|
|
|
914
965
|
tablesByNamespace[namespaceId] = nsTables;
|
|
915
966
|
}
|
|
916
967
|
if (nsTables[tableName] !== undefined) {
|
|
917
|
-
throw
|
|
968
|
+
throw contractError(
|
|
969
|
+
'CONTRACT.NAME_DUPLICATE',
|
|
918
970
|
`buildSqlContractFromDefinition: duplicate table "${tableName}" in namespace "${namespaceId}".`,
|
|
971
|
+
{ meta: { kind: 'table', name: tableName, namespaceId } },
|
|
919
972
|
);
|
|
920
973
|
}
|
|
921
974
|
nsTables[tableName] = tableInput;
|
|
@@ -978,8 +1031,16 @@ export function buildSqlContractFromDefinition(
|
|
|
978
1031
|
|
|
979
1032
|
if (relation.cardinality === 'N:M') {
|
|
980
1033
|
if (!relation.through) {
|
|
981
|
-
throw
|
|
1034
|
+
throw contractError(
|
|
1035
|
+
'CONTRACT.RELATION_INVALID',
|
|
982
1036
|
`Relation "${semanticModel.modelName}.${relation.fieldName}" with cardinality "N:M" requires through metadata`,
|
|
1037
|
+
{
|
|
1038
|
+
meta: {
|
|
1039
|
+
modelName: semanticModel.modelName,
|
|
1040
|
+
relationName: relation.fieldName,
|
|
1041
|
+
reason: 'many-to-many-missing-through',
|
|
1042
|
+
},
|
|
1043
|
+
},
|
|
983
1044
|
);
|
|
984
1045
|
}
|
|
985
1046
|
modelRelations[relation.fieldName] = {
|
|
@@ -1062,8 +1123,16 @@ export function buildSqlContractFromDefinition(
|
|
|
1062
1123
|
const storageValueSetsByNs: Record<string, Record<string, StorageValueSetInput>> = {};
|
|
1063
1124
|
for (const [enumName, handle] of Object.entries(definition.enums ?? {})) {
|
|
1064
1125
|
if (enumName !== handle.enumName) {
|
|
1065
|
-
throw
|
|
1126
|
+
throw contractError(
|
|
1127
|
+
'CONTRACT.ENUM_INVALID',
|
|
1066
1128
|
`enum declaration key "${enumName}" must match enumType name "${handle.enumName}". Aliases are not supported.`,
|
|
1129
|
+
{
|
|
1130
|
+
meta: {
|
|
1131
|
+
enumName: handle.enumName,
|
|
1132
|
+
declarationKey: enumName,
|
|
1133
|
+
reason: 'key-name-mismatch',
|
|
1134
|
+
},
|
|
1135
|
+
},
|
|
1067
1136
|
);
|
|
1068
1137
|
}
|
|
1069
1138
|
const nsId = defaultNamespaceId;
|
|
@@ -1154,21 +1223,21 @@ export function buildSqlContractFromDefinition(
|
|
|
1154
1223
|
}
|
|
1155
1224
|
: undefined;
|
|
1156
1225
|
|
|
1157
|
-
const extensionNamespaces = definition.
|
|
1158
|
-
? Object.values(definition.
|
|
1226
|
+
const extensionNamespaces = definition.extensions
|
|
1227
|
+
? Object.values(definition.extensions).map((pack) => pack.id)
|
|
1159
1228
|
: undefined;
|
|
1160
1229
|
|
|
1161
|
-
const
|
|
1230
|
+
const extensions: Record<string, unknown> = { ...(definition.extensions || {}) };
|
|
1162
1231
|
if (extensionNamespaces) {
|
|
1163
1232
|
for (const namespace of extensionNamespaces) {
|
|
1164
|
-
if (!Object.hasOwn(
|
|
1165
|
-
|
|
1233
|
+
if (!Object.hasOwn(extensions, namespace)) {
|
|
1234
|
+
extensions[namespace] = {};
|
|
1166
1235
|
}
|
|
1167
1236
|
}
|
|
1168
1237
|
}
|
|
1169
1238
|
|
|
1170
|
-
const extensionPackCapabilitySources = definition.
|
|
1171
|
-
? Object.values(definition.
|
|
1239
|
+
const extensionPackCapabilitySources = definition.extensions
|
|
1240
|
+
? Object.values(definition.extensions).map(
|
|
1172
1241
|
(pack) => pack.capabilities as CapabilityMatrix | undefined,
|
|
1173
1242
|
)
|
|
1174
1243
|
: [];
|
|
@@ -1258,7 +1327,7 @@ export function buildSqlContractFromDefinition(
|
|
|
1258
1327
|
roots,
|
|
1259
1328
|
storage,
|
|
1260
1329
|
...(executionWithHash ? { execution: executionWithHash } : {}),
|
|
1261
|
-
|
|
1330
|
+
extensions,
|
|
1262
1331
|
capabilities,
|
|
1263
1332
|
profileHash,
|
|
1264
1333
|
meta: {},
|
|
@@ -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<
|
|
@@ -55,16 +56,16 @@ type ExtractEntitiesNamespaceFromPack<Pack> = ExtractAuthoringNamespaceFromPack<
|
|
|
55
56
|
Record<never, never>
|
|
56
57
|
>;
|
|
57
58
|
|
|
58
|
-
type MergeExtensionTypeNamespaces<
|
|
59
|
-
|
|
59
|
+
type MergeExtensionTypeNamespaces<Extensions> = MergeExtensionAuthoringNamespaces<
|
|
60
|
+
Extensions,
|
|
60
61
|
'type'
|
|
61
62
|
>;
|
|
62
|
-
type MergeExtensionFieldNamespaces<
|
|
63
|
-
|
|
63
|
+
type MergeExtensionFieldNamespaces<Extensions> = MergeExtensionAuthoringNamespaces<
|
|
64
|
+
Extensions,
|
|
64
65
|
'field'
|
|
65
66
|
>;
|
|
66
|
-
type MergeExtensionEntityNamespaces<
|
|
67
|
-
|
|
67
|
+
type MergeExtensionEntityNamespaces<Extensions> = MergeExtensionAuthoringNamespaces<
|
|
68
|
+
Extensions,
|
|
68
69
|
'entityTypes'
|
|
69
70
|
>;
|
|
70
71
|
|
|
@@ -100,12 +101,12 @@ type TypeHelpersFromNamespace<Namespace> = {
|
|
|
100
101
|
|
|
101
102
|
type CoreFieldHelpers = Pick<typeof field, 'column' | 'generated' | 'namedType'>;
|
|
102
103
|
|
|
103
|
-
type MergeAllPackIndexTypes<Family, Target,
|
|
104
|
-
{ readonly __family: Family; readonly __target: Target } & (
|
|
104
|
+
type MergeAllPackIndexTypes<Family, Target, Extensions> = MergeExtensionIndexTypes<
|
|
105
|
+
{ readonly __family: Family; readonly __target: Target } & (Extensions extends Record<
|
|
105
106
|
string,
|
|
106
107
|
unknown
|
|
107
108
|
>
|
|
108
|
-
?
|
|
109
|
+
? Extensions
|
|
109
110
|
: Record<never, never>)
|
|
110
111
|
>;
|
|
111
112
|
|
|
@@ -131,24 +132,24 @@ type PackAwareModel<IndexTypes extends IndexTypeMap> = {
|
|
|
131
132
|
export type ComposedAuthoringHelpers<
|
|
132
133
|
Family extends FamilyPackRef<string>,
|
|
133
134
|
Target extends TargetPackRef<'sql', string>,
|
|
134
|
-
|
|
135
|
+
Extensions extends Record<string, ExtensionPackRef<'sql', string>> | undefined,
|
|
135
136
|
> = EntityHelpersFromNamespace<
|
|
136
137
|
ExtractEntitiesNamespaceFromPack<Family> &
|
|
137
138
|
ExtractEntitiesNamespaceFromPack<Target> &
|
|
138
|
-
MergeExtensionEntityNamespaces<
|
|
139
|
+
MergeExtensionEntityNamespaces<Extensions>
|
|
139
140
|
> & {
|
|
140
141
|
readonly field: CoreFieldHelpers &
|
|
141
142
|
FieldHelpersFromNamespace<
|
|
142
143
|
ExtractFieldNamespaceFromPack<Family> &
|
|
143
144
|
ExtractFieldNamespaceFromPack<Target> &
|
|
144
|
-
MergeExtensionFieldNamespaces<
|
|
145
|
+
MergeExtensionFieldNamespaces<Extensions>
|
|
145
146
|
>;
|
|
146
|
-
readonly model: PackAwareModel<MergeAllPackIndexTypes<Family, Target,
|
|
147
|
+
readonly model: PackAwareModel<MergeAllPackIndexTypes<Family, Target, Extensions>>;
|
|
147
148
|
readonly rel: typeof rel;
|
|
148
149
|
readonly type: TypeHelpersFromNamespace<
|
|
149
150
|
ExtractTypeNamespaceFromPack<Family> &
|
|
150
151
|
ExtractTypeNamespaceFromPack<Target> &
|
|
151
|
-
MergeExtensionTypeNamespaces<
|
|
152
|
+
MergeExtensionTypeNamespaces<Extensions>
|
|
152
153
|
>;
|
|
153
154
|
};
|
|
154
155
|
|
|
@@ -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
|
}
|
|
@@ -267,14 +272,14 @@ function createComposedFieldHelpers(
|
|
|
267
272
|
export function createComposedAuthoringHelpers<
|
|
268
273
|
Family extends FamilyPackRef<string>,
|
|
269
274
|
Target extends TargetPackRef<'sql', string>,
|
|
270
|
-
|
|
275
|
+
Extensions extends Record<string, ExtensionPackRef<'sql', string>> | undefined,
|
|
271
276
|
>(options: {
|
|
272
277
|
readonly family: Family;
|
|
273
278
|
readonly target: Target;
|
|
274
|
-
readonly
|
|
275
|
-
}): ComposedAuthoringHelpers<Family, Target,
|
|
279
|
+
readonly extensions?: Extensions;
|
|
280
|
+
}): ComposedAuthoringHelpers<Family, Target, Extensions> {
|
|
276
281
|
const extensionValues: readonly ExtensionPackRef<'sql', string>[] = Object.values(
|
|
277
|
-
(options.
|
|
282
|
+
(options.extensions ?? {}) as Record<string, ExtensionPackRef<'sql', string>>,
|
|
278
283
|
);
|
|
279
284
|
const components: readonly AuthoringComponent[] = [
|
|
280
285
|
options.family,
|
|
@@ -298,5 +303,5 @@ export function createComposedAuthoringHelpers<
|
|
|
298
303
|
model,
|
|
299
304
|
rel,
|
|
300
305
|
type: createTypeHelpersFromNamespace(typeNamespace),
|
|
301
|
-
} as ComposedAuthoringHelpers<Family, Target,
|
|
306
|
+
} as ComposedAuthoringHelpers<Family, Target, Extensions>;
|
|
302
307
|
}
|
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
|
|
@@ -33,7 +35,7 @@ export function emptyContract(options: {
|
|
|
33
35
|
}): ContractConfig {
|
|
34
36
|
return {
|
|
35
37
|
source: {
|
|
36
|
-
|
|
38
|
+
format: 'typescript',
|
|
37
39
|
load: async () => {
|
|
38
40
|
const built = buildSqlContractFromDefinition({
|
|
39
41
|
target: options.target,
|
|
@@ -54,7 +56,7 @@ export function typescriptContract(
|
|
|
54
56
|
): ContractConfig {
|
|
55
57
|
return {
|
|
56
58
|
source: {
|
|
57
|
-
|
|
59
|
+
format: 'typescript',
|
|
58
60
|
load: async () =>
|
|
59
61
|
ok(applySpecifierDefaultControlPolicy(contract, options?.defaultControlPolicy)),
|
|
60
62
|
},
|
|
@@ -71,20 +73,22 @@ export function typescriptContractFromPath(
|
|
|
71
73
|
): ContractConfig {
|
|
72
74
|
return {
|
|
73
75
|
source: {
|
|
74
|
-
|
|
76
|
+
format: 'typescript',
|
|
75
77
|
inputs: [contractPath],
|
|
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));
|