@prisma-next/sql-contract-ts 0.12.0-dev.7 → 0.12.0-dev.70
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 +8 -0
- package/dist/{build-contract-BCYW3_wE.mjs → build-contract-0mmycHVI.mjs} +161 -58
- package/dist/build-contract-0mmycHVI.mjs.map +1 -0
- package/dist/config-types.d.mts +7 -3
- package/dist/config-types.d.mts.map +1 -1
- package/dist/config-types.mjs +12 -9
- package/dist/config-types.mjs.map +1 -1
- package/dist/contract-builder.d.mts +310 -23
- package/dist/contract-builder.d.mts.map +1 -1
- package/dist/contract-builder.mjs +280 -44
- package/dist/contract-builder.mjs.map +1 -1
- package/package.json +13 -13
- package/src/build-contract.ts +226 -64
- package/src/config-types.ts +24 -6
- package/src/contract-builder.ts +137 -16
- package/src/contract-definition.ts +49 -3
- package/src/contract-dsl.ts +346 -18
- package/src/contract-lowering.ts +188 -15
- package/src/contract-types.ts +17 -21
- package/src/enum-type.ts +236 -0
- package/src/exports/contract-builder.ts +5 -0
- package/dist/build-contract-BCYW3_wE.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/sql-contract-ts",
|
|
3
|
-
"version": "0.12.0-dev.
|
|
3
|
+
"version": "0.12.0-dev.70",
|
|
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.12.0-dev.
|
|
10
|
-
"@prisma-next/contract": "0.12.0-dev.
|
|
11
|
-
"@prisma-next/contract-authoring": "0.12.0-dev.
|
|
12
|
-
"@prisma-next/framework-components": "0.12.0-dev.
|
|
13
|
-
"@prisma-next/sql-contract": "0.12.0-dev.
|
|
14
|
-
"@prisma-next/utils": "0.12.0-dev.
|
|
9
|
+
"@prisma-next/config": "0.12.0-dev.70",
|
|
10
|
+
"@prisma-next/contract": "0.12.0-dev.70",
|
|
11
|
+
"@prisma-next/contract-authoring": "0.12.0-dev.70",
|
|
12
|
+
"@prisma-next/framework-components": "0.12.0-dev.70",
|
|
13
|
+
"@prisma-next/sql-contract": "0.12.0-dev.70",
|
|
14
|
+
"@prisma-next/utils": "0.12.0-dev.70",
|
|
15
15
|
"arktype": "^2.2.0",
|
|
16
16
|
"pathe": "^2.0.3",
|
|
17
17
|
"ts-toolbelt": "^9.6.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@prisma-next/test-utils": "0.12.0-dev.
|
|
21
|
-
"@prisma-next/tsconfig": "0.12.0-dev.
|
|
20
|
+
"@prisma-next/test-utils": "0.12.0-dev.70",
|
|
21
|
+
"@prisma-next/tsconfig": "0.12.0-dev.70",
|
|
22
22
|
"@types/pg": "8.20.0",
|
|
23
|
-
"pg": "8.
|
|
24
|
-
"@prisma-next/tsdown": "0.12.0-dev.
|
|
25
|
-
"tsdown": "0.22.
|
|
23
|
+
"pg": "8.21.0",
|
|
24
|
+
"@prisma-next/tsdown": "0.12.0-dev.70",
|
|
25
|
+
"tsdown": "0.22.1",
|
|
26
26
|
"typescript": "5.9.3",
|
|
27
|
-
"vitest": "4.1.
|
|
27
|
+
"vitest": "4.1.8"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"typescript": ">=5.9"
|
package/src/build-contract.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type ColumnDefault,
|
|
9
9
|
type ColumnDefaultLiteralInputValue,
|
|
10
10
|
type Contract,
|
|
11
|
+
type ContractEnum,
|
|
11
12
|
type ContractField,
|
|
12
13
|
type ContractModel,
|
|
13
14
|
type ContractRelation,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
type ExecutionMutationDefault,
|
|
19
20
|
type JsonValue,
|
|
20
21
|
type StorageHashBase,
|
|
22
|
+
type ValueSetRef,
|
|
21
23
|
} from '@prisma-next/contract/types';
|
|
22
24
|
import { type CapabilityMatrix, mergeCapabilityMatrices } from '@prisma-next/contract-authoring';
|
|
23
25
|
import type { CodecLookup } from '@prisma-next/framework-components/codec';
|
|
@@ -32,6 +34,7 @@ import {
|
|
|
32
34
|
import {
|
|
33
35
|
applyFkDefaults,
|
|
34
36
|
buildSqlNamespace,
|
|
37
|
+
type CheckConstraintInput,
|
|
35
38
|
isPostgresEnumStorageEntry,
|
|
36
39
|
type PostgresEnumStorageEntry,
|
|
37
40
|
type SqlNamespaceTablesInput,
|
|
@@ -41,6 +44,7 @@ import {
|
|
|
41
44
|
StorageTable,
|
|
42
45
|
type StorageTableInput,
|
|
43
46
|
type StorageTypeInstance,
|
|
47
|
+
type StorageValueSetInput,
|
|
44
48
|
toStorageTypeInstance,
|
|
45
49
|
} from '@prisma-next/sql-contract/types';
|
|
46
50
|
import { validateStorageSemantics } from '@prisma-next/sql-contract/validators';
|
|
@@ -156,16 +160,17 @@ const JSONB_NATIVE_TYPE = 'jsonb';
|
|
|
156
160
|
function resolveModelNamespaceId(
|
|
157
161
|
model: ModelNode,
|
|
158
162
|
modelNameToNamespaceId: ReadonlyMap<string, string>,
|
|
159
|
-
|
|
163
|
+
defaultNamespaceId: string,
|
|
160
164
|
): string {
|
|
161
165
|
if (model.namespaceId !== undefined && model.namespaceId.length > 0) {
|
|
162
166
|
return model.namespaceId;
|
|
163
167
|
}
|
|
164
|
-
return modelNameToNamespaceId.get(model.modelName) ??
|
|
168
|
+
return modelNameToNamespaceId.get(model.modelName) ?? defaultNamespaceId;
|
|
165
169
|
}
|
|
166
170
|
|
|
167
171
|
function buildStorageColumn(
|
|
168
172
|
field: FieldNode | ValueObjectFieldNode,
|
|
173
|
+
storageValueSetRef: ValueSetRef | undefined,
|
|
169
174
|
codecLookup?: CodecLookup,
|
|
170
175
|
): StorageColumn {
|
|
171
176
|
if (isValueObjectField(field)) {
|
|
@@ -203,12 +208,14 @@ function buildStorageColumn(
|
|
|
203
208
|
...ifDefined('typeParams', field.descriptor.typeParams),
|
|
204
209
|
...ifDefined('default', encodedDefault),
|
|
205
210
|
...ifDefined('typeRef', field.descriptor.typeRef),
|
|
211
|
+
...ifDefined('valueSet', storageValueSetRef),
|
|
206
212
|
};
|
|
207
213
|
}
|
|
208
214
|
|
|
209
215
|
function buildDomainField(
|
|
210
216
|
field: FieldNode | ValueObjectFieldNode,
|
|
211
217
|
column: StorageColumn,
|
|
218
|
+
domainValueSetRef: ValueSetRef | undefined,
|
|
212
219
|
): ContractField {
|
|
213
220
|
if (isValueObjectField(field)) {
|
|
214
221
|
return {
|
|
@@ -226,12 +233,13 @@ function buildDomainField(
|
|
|
226
233
|
},
|
|
227
234
|
nullable: column.nullable,
|
|
228
235
|
...(field.many ? { many: true } : {}),
|
|
236
|
+
...ifDefined('valueSet', domainValueSetRef),
|
|
229
237
|
};
|
|
230
238
|
}
|
|
231
239
|
|
|
232
240
|
function collectStorageNamespaceCoordinateIds(definition: ContractDefinition): Set<string> {
|
|
233
241
|
const ids = new Set<string>();
|
|
234
|
-
ids.add(
|
|
242
|
+
ids.add(definition.target.defaultNamespaceId);
|
|
235
243
|
for (const id of definition.namespaces ?? []) {
|
|
236
244
|
if (id.length > 0) {
|
|
237
245
|
ids.add(id);
|
|
@@ -245,22 +253,38 @@ function collectStorageNamespaceCoordinateIds(definition: ContractDefinition): S
|
|
|
245
253
|
return ids;
|
|
246
254
|
}
|
|
247
255
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
256
|
+
function ensureUnboundNamespaceSlot(
|
|
257
|
+
namespaces: SqlStorageInput['namespaces'],
|
|
258
|
+
createNamespace: ContractDefinition['createNamespace'],
|
|
259
|
+
): SqlStorageInput['namespaces'] {
|
|
260
|
+
if (Object.hasOwn(namespaces, UNBOUND_NAMESPACE_ID)) {
|
|
261
|
+
return namespaces;
|
|
262
|
+
}
|
|
263
|
+
const unboundInput: SqlNamespaceTablesInput = {
|
|
264
|
+
id: UNBOUND_NAMESPACE_ID,
|
|
265
|
+
entries: { table: {} },
|
|
266
|
+
};
|
|
267
|
+
const unbound = createNamespace ? createNamespace(unboundInput) : buildSqlNamespace(unboundInput);
|
|
268
|
+
return blindCast<
|
|
269
|
+
SqlStorageInput['namespaces'],
|
|
270
|
+
'createNamespace may return a target namespace concretion; the unbound slot matches SqlNamespace at runtime'
|
|
271
|
+
>({
|
|
272
|
+
[UNBOUND_NAMESPACE_ID]: unbound,
|
|
273
|
+
...namespaces,
|
|
274
|
+
});
|
|
253
275
|
}
|
|
254
276
|
|
|
277
|
+
const POSTGRES_ENUM_NAMESPACE_ID = 'public';
|
|
278
|
+
|
|
255
279
|
function partitionStorageTypesForTarget(
|
|
256
280
|
targetId: string,
|
|
257
281
|
types: Record<string, StorageTypeInstance | PostgresEnumStorageEntry>,
|
|
258
282
|
namespaceTypes?: Readonly<Record<string, Readonly<Record<string, PostgresEnumStorageEntry>>>>,
|
|
259
283
|
): {
|
|
260
|
-
readonly documentTypes: Record<string, StorageTypeInstance
|
|
284
|
+
readonly documentTypes: Record<string, StorageTypeInstance>;
|
|
261
285
|
readonly namespaceEnumTypesById: Record<string, Record<string, PostgresEnumStorageEntry>>;
|
|
262
286
|
} {
|
|
263
|
-
const documentTypes: Record<string, StorageTypeInstance
|
|
287
|
+
const documentTypes: Record<string, StorageTypeInstance> = {};
|
|
264
288
|
const namespaceEnumTypesById: Record<string, Record<string, PostgresEnumStorageEntry>> = {};
|
|
265
289
|
for (const [name, entry] of Object.entries(types)) {
|
|
266
290
|
if (isPostgresEnumStorageEntry(entry)) {
|
|
@@ -304,6 +328,7 @@ export function buildSqlContractFromDefinition(
|
|
|
304
328
|
codecLookup?: CodecLookup,
|
|
305
329
|
): Contract<SqlStorage> {
|
|
306
330
|
const target = definition.target.targetId;
|
|
331
|
+
const defaultNamespaceId = definition.target.defaultNamespaceId;
|
|
307
332
|
const targetFamily = 'sql';
|
|
308
333
|
const modelsByName = new Map(definition.models.map((m) => [m.modelName, m]));
|
|
309
334
|
|
|
@@ -319,9 +344,13 @@ export function buildSqlContractFromDefinition(
|
|
|
319
344
|
const namespaceId =
|
|
320
345
|
semanticModel.namespaceId !== undefined && semanticModel.namespaceId.length > 0
|
|
321
346
|
? semanticModel.namespaceId
|
|
322
|
-
:
|
|
347
|
+
: defaultNamespaceId;
|
|
323
348
|
modelNameToNamespaceId.set(semanticModel.modelName, namespaceId);
|
|
324
|
-
|
|
349
|
+
// STI variants share the base table; the base model already owns this
|
|
350
|
+
// table name and its root, so the variant contributes neither.
|
|
351
|
+
if (!semanticModel.sharesBaseTable) {
|
|
352
|
+
roots[tableName] = crossRef(semanticModel.modelName, namespaceId);
|
|
353
|
+
}
|
|
325
354
|
|
|
326
355
|
// --- Build storage table ---
|
|
327
356
|
|
|
@@ -348,11 +377,34 @@ export function buildSqlContractFromDefinition(
|
|
|
348
377
|
}
|
|
349
378
|
}
|
|
350
379
|
|
|
351
|
-
const
|
|
380
|
+
const enumHandle = !isValueObjectField(field) ? field.enumTypeHandle : undefined;
|
|
381
|
+
// Authored enums are always registered under the contract's defaultNamespaceId
|
|
382
|
+
// (see the enum registration loop below), so refs must point there regardless
|
|
383
|
+
// of which namespace the consuming model lives in.
|
|
384
|
+
const storageValueSetRef: ValueSetRef | undefined =
|
|
385
|
+
enumHandle !== undefined
|
|
386
|
+
? {
|
|
387
|
+
plane: 'storage',
|
|
388
|
+
entityKind: 'value-set',
|
|
389
|
+
namespaceId: defaultNamespaceId,
|
|
390
|
+
name: enumHandle.enumName,
|
|
391
|
+
}
|
|
392
|
+
: undefined;
|
|
393
|
+
const domainValueSetRef: ValueSetRef | undefined =
|
|
394
|
+
enumHandle !== undefined
|
|
395
|
+
? {
|
|
396
|
+
plane: 'domain',
|
|
397
|
+
entityKind: 'enum',
|
|
398
|
+
namespaceId: defaultNamespaceId,
|
|
399
|
+
name: enumHandle.enumName,
|
|
400
|
+
}
|
|
401
|
+
: undefined;
|
|
402
|
+
|
|
403
|
+
const column = buildStorageColumn(field, storageValueSetRef, codecLookup);
|
|
352
404
|
columns[field.columnName] = column;
|
|
353
405
|
fieldToColumn[field.fieldName] = field.columnName;
|
|
354
406
|
|
|
355
|
-
domainFields[field.fieldName] = buildDomainField(field, column);
|
|
407
|
+
domainFields[field.fieldName] = buildDomainField(field, column, domainValueSetRef);
|
|
356
408
|
|
|
357
409
|
if (isValueObjectField(field)) {
|
|
358
410
|
domainFieldRefs[field.fieldName] = {
|
|
@@ -374,6 +426,31 @@ export function buildSqlContractFromDefinition(
|
|
|
374
426
|
}
|
|
375
427
|
|
|
376
428
|
const foreignKeys = (semanticModel.foreignKeys ?? []).map((fk) => {
|
|
429
|
+
if (fk.references.spaceId !== undefined) {
|
|
430
|
+
// Cross-space FK: the target lives in a different contract space.
|
|
431
|
+
// Skip local model lookup and carry the spaceId coordinate through.
|
|
432
|
+
const targetNamespaceId = fk.references.namespaceId ?? defaultNamespaceId;
|
|
433
|
+
return {
|
|
434
|
+
source: { namespaceId: asNamespaceId(namespaceId), tableName, columns: fk.columns },
|
|
435
|
+
target: {
|
|
436
|
+
namespaceId: asNamespaceId(targetNamespaceId),
|
|
437
|
+
tableName: fk.references.table,
|
|
438
|
+
columns: fk.references.columns,
|
|
439
|
+
spaceId: fk.references.spaceId,
|
|
440
|
+
},
|
|
441
|
+
...applyFkDefaults(
|
|
442
|
+
{
|
|
443
|
+
...ifDefined('constraint', fk.constraint),
|
|
444
|
+
...ifDefined('index', fk.index),
|
|
445
|
+
},
|
|
446
|
+
definition.foreignKeyDefaults,
|
|
447
|
+
),
|
|
448
|
+
...ifDefined('name', fk.name),
|
|
449
|
+
...ifDefined('onDelete', fk.onDelete),
|
|
450
|
+
...ifDefined('onUpdate', fk.onUpdate),
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
377
454
|
const targetModel = assertKnownTargetModel(
|
|
378
455
|
modelsByName,
|
|
379
456
|
semanticModel.modelName,
|
|
@@ -390,7 +467,7 @@ export function buildSqlContractFromDefinition(
|
|
|
390
467
|
fk.references.namespaceId ??
|
|
391
468
|
(targetModel.namespaceId !== undefined && targetModel.namespaceId.length > 0
|
|
392
469
|
? targetModel.namespaceId
|
|
393
|
-
:
|
|
470
|
+
: defaultNamespaceId);
|
|
394
471
|
return {
|
|
395
472
|
source: { namespaceId: asNamespaceId(namespaceId), tableName, columns: fk.columns },
|
|
396
473
|
target: {
|
|
@@ -411,48 +488,64 @@ export function buildSqlContractFromDefinition(
|
|
|
411
488
|
};
|
|
412
489
|
});
|
|
413
490
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
491
|
+
// STI variants share the base table: their columns are already
|
|
492
|
+
// materialised onto the base `ModelNode`, so the variant builds a domain
|
|
493
|
+
// model (below) but no storage table of its own.
|
|
494
|
+
if (!semanticModel.sharesBaseTable) {
|
|
495
|
+
const existingNs = tableNameToNamespaceId.get(tableName);
|
|
496
|
+
if (existingNs !== undefined && existingNs !== namespaceId) {
|
|
497
|
+
throw new Error(
|
|
498
|
+
`buildSqlContractFromDefinition: table "${tableName}" is mapped in namespace "${namespaceId}" but already exists in namespace "${existingNs}".`,
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
tableNameToNamespaceId.set(tableName, namespaceId);
|
|
502
|
+
|
|
503
|
+
const checksForTable: CheckConstraintInput[] = Object.entries(columns).flatMap(
|
|
504
|
+
([columnName, col]) => {
|
|
505
|
+
const valueSet = col.valueSet;
|
|
506
|
+
return valueSet === undefined
|
|
507
|
+
? []
|
|
508
|
+
: [{ name: `${tableName}_${columnName}_check`, column: columnName, valueSet }];
|
|
509
|
+
},
|
|
418
510
|
);
|
|
419
|
-
}
|
|
420
|
-
tableNameToNamespaceId.set(tableName, namespaceId);
|
|
421
|
-
|
|
422
|
-
const tableInput: StorageTableInput = {
|
|
423
|
-
columns,
|
|
424
|
-
uniques: (semanticModel.uniques ?? []).map((u) => ({
|
|
425
|
-
columns: u.columns,
|
|
426
|
-
...ifDefined('name', u.name),
|
|
427
|
-
})),
|
|
428
|
-
indexes: (semanticModel.indexes ?? []).map((i) => ({
|
|
429
|
-
columns: i.columns,
|
|
430
|
-
...ifDefined('name', i.name),
|
|
431
|
-
...ifDefined('type', i.type),
|
|
432
|
-
...ifDefined('options', i.options),
|
|
433
|
-
})),
|
|
434
|
-
foreignKeys,
|
|
435
|
-
...(semanticModel.id
|
|
436
|
-
? {
|
|
437
|
-
primaryKey: {
|
|
438
|
-
columns: semanticModel.id.columns,
|
|
439
|
-
...ifDefined('name', semanticModel.id.name),
|
|
440
|
-
},
|
|
441
|
-
}
|
|
442
|
-
: {}),
|
|
443
|
-
};
|
|
444
511
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
512
|
+
const tableInput: StorageTableInput = {
|
|
513
|
+
columns,
|
|
514
|
+
...ifDefined('control', semanticModel.control),
|
|
515
|
+
uniques: (semanticModel.uniques ?? []).map((u) => ({
|
|
516
|
+
columns: u.columns,
|
|
517
|
+
...ifDefined('name', u.name),
|
|
518
|
+
})),
|
|
519
|
+
indexes: (semanticModel.indexes ?? []).map((i) => ({
|
|
520
|
+
columns: i.columns,
|
|
521
|
+
...ifDefined('name', i.name),
|
|
522
|
+
...ifDefined('type', i.type),
|
|
523
|
+
...ifDefined('options', i.options),
|
|
524
|
+
})),
|
|
525
|
+
foreignKeys,
|
|
526
|
+
...(semanticModel.id
|
|
527
|
+
? {
|
|
528
|
+
primaryKey: {
|
|
529
|
+
columns: semanticModel.id.columns,
|
|
530
|
+
...ifDefined('name', semanticModel.id.name),
|
|
531
|
+
},
|
|
532
|
+
}
|
|
533
|
+
: {}),
|
|
534
|
+
...(checksForTable.length > 0 ? { checks: checksForTable } : {}),
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
let nsTables = tablesByNamespace[namespaceId];
|
|
538
|
+
if (nsTables === undefined) {
|
|
539
|
+
nsTables = {};
|
|
540
|
+
tablesByNamespace[namespaceId] = nsTables;
|
|
541
|
+
}
|
|
542
|
+
if (nsTables[tableName] !== undefined) {
|
|
543
|
+
throw new Error(
|
|
544
|
+
`buildSqlContractFromDefinition: duplicate table "${tableName}" in namespace "${namespaceId}".`,
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
nsTables[tableName] = new StorageTable(tableInput);
|
|
454
548
|
}
|
|
455
|
-
nsTables[tableName] = new StorageTable(tableInput);
|
|
456
549
|
|
|
457
550
|
// --- Build contract model ---
|
|
458
551
|
|
|
@@ -466,6 +559,24 @@ export function buildSqlContractFromDefinition(
|
|
|
466
559
|
);
|
|
467
560
|
const modelRelations: Record<string, ContractRelation> = {};
|
|
468
561
|
for (const relation of semanticModel.relations ?? []) {
|
|
562
|
+
// Cross-space relations have `spaceId` set — the target model lives in
|
|
563
|
+
// a different contract space, so skip local model lookup and validation.
|
|
564
|
+
if (relation.spaceId !== undefined) {
|
|
565
|
+
const targetNamespaceId = relation.namespaceId ?? defaultNamespaceId;
|
|
566
|
+
modelRelations[relation.fieldName] = {
|
|
567
|
+
to: crossRef(relation.toModel, targetNamespaceId, relation.spaceId),
|
|
568
|
+
// Cross-space belongsTo relations are always N:1 (the FK-owning side).
|
|
569
|
+
cardinality: 'N:1',
|
|
570
|
+
on: {
|
|
571
|
+
localFields: relation.on.parentColumns.map((col) => columnToField.get(col) ?? col),
|
|
572
|
+
// For cross-space targets the lowering carries field names directly
|
|
573
|
+
// (no fieldToColumn map available for the remote model).
|
|
574
|
+
targetFields: relation.on.childColumns,
|
|
575
|
+
},
|
|
576
|
+
};
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
|
|
469
580
|
const targetModel = assertKnownTargetModel(
|
|
470
581
|
modelsByName,
|
|
471
582
|
semanticModel.modelName,
|
|
@@ -487,7 +598,7 @@ export function buildSqlContractFromDefinition(
|
|
|
487
598
|
modelRelations[relation.fieldName] = {
|
|
488
599
|
to: crossRef(
|
|
489
600
|
relation.toModel,
|
|
490
|
-
resolveModelNamespaceId(targetModel, modelNameToNamespaceId,
|
|
601
|
+
resolveModelNamespaceId(targetModel, modelNameToNamespaceId, defaultNamespaceId),
|
|
491
602
|
),
|
|
492
603
|
// RelationDefinition.cardinality includes 'N:M' which isn't in
|
|
493
604
|
// ContractReferenceRelation yet — cast is needed until the contract
|
|
@@ -517,6 +628,7 @@ export function buildSqlContractFromDefinition(
|
|
|
517
628
|
namespaceModels[semanticModel.modelName] = {
|
|
518
629
|
storage: {
|
|
519
630
|
table: tableName,
|
|
631
|
+
namespaceId,
|
|
520
632
|
fields: storageFields,
|
|
521
633
|
},
|
|
522
634
|
fields: domainFields,
|
|
@@ -557,6 +669,39 @@ export function buildSqlContractFromDefinition(
|
|
|
557
669
|
for (const id of Object.keys(namespaceEnumTypesById)) {
|
|
558
670
|
namespaceCoordinateIds.add(id);
|
|
559
671
|
}
|
|
672
|
+
|
|
673
|
+
// Build per-namespace registries for `enumType()` handles.
|
|
674
|
+
// All authored enums target the contract's default namespace.
|
|
675
|
+
const domainEnumsByNs: Record<string, Record<string, ContractEnum>> = {};
|
|
676
|
+
const storageValueSetsByNs: Record<string, Record<string, StorageValueSetInput>> = {};
|
|
677
|
+
for (const [enumName, handle] of Object.entries(definition.enums ?? {})) {
|
|
678
|
+
if (enumName !== handle.enumName) {
|
|
679
|
+
throw new Error(
|
|
680
|
+
`enum declaration key "${enumName}" must match enumType name "${handle.enumName}". Aliases are not supported.`,
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
const nsId = defaultNamespaceId;
|
|
684
|
+
let domainSlot = domainEnumsByNs[nsId];
|
|
685
|
+
if (domainSlot === undefined) {
|
|
686
|
+
domainSlot = {};
|
|
687
|
+
domainEnumsByNs[nsId] = domainSlot;
|
|
688
|
+
}
|
|
689
|
+
domainSlot[enumName] = {
|
|
690
|
+
codecId: handle.codecId,
|
|
691
|
+
members: handle.enumMembers,
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
let storageSlot = storageValueSetsByNs[nsId];
|
|
695
|
+
if (storageSlot === undefined) {
|
|
696
|
+
storageSlot = {};
|
|
697
|
+
storageValueSetsByNs[nsId] = storageSlot;
|
|
698
|
+
}
|
|
699
|
+
storageSlot[enumName] = {
|
|
700
|
+
kind: 'value-set',
|
|
701
|
+
values: handle.values,
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
560
705
|
const { createNamespace } = definition;
|
|
561
706
|
const namespaces = blindCast<
|
|
562
707
|
SqlStorageInput['namespaces'],
|
|
@@ -565,18 +710,26 @@ export function buildSqlContractFromDefinition(
|
|
|
565
710
|
Object.fromEntries(
|
|
566
711
|
[...namespaceCoordinateIds].sort().map((id) => {
|
|
567
712
|
const enumTypes = namespaceEnumTypesById[id];
|
|
713
|
+
const valueSetEntries = storageValueSetsByNs[id];
|
|
568
714
|
const nsInput: SqlNamespaceTablesInput = {
|
|
569
715
|
id,
|
|
570
|
-
|
|
571
|
-
|
|
716
|
+
entries: {
|
|
717
|
+
table: tablesByNamespace[id] ?? {},
|
|
718
|
+
...(valueSetEntries !== undefined && Object.keys(valueSetEntries).length > 0
|
|
719
|
+
? { valueSet: valueSetEntries }
|
|
720
|
+
: {}),
|
|
721
|
+
},
|
|
572
722
|
};
|
|
573
|
-
return [
|
|
723
|
+
return [
|
|
724
|
+
id,
|
|
725
|
+
createNamespace ? createNamespace(nsInput, enumTypes) : buildSqlNamespace(nsInput),
|
|
726
|
+
];
|
|
574
727
|
}),
|
|
575
728
|
),
|
|
576
729
|
);
|
|
577
730
|
const storageWithoutHash = {
|
|
578
731
|
...(Object.keys(documentTypes).length > 0 ? { types: documentTypes } : {}),
|
|
579
|
-
namespaces,
|
|
732
|
+
namespaces: ensureUnboundNamespaceSlot(namespaces, createNamespace),
|
|
580
733
|
};
|
|
581
734
|
const storageHash: StorageHashBase<string> = definition.storageHash
|
|
582
735
|
? coreHash(definition.storageHash)
|
|
@@ -672,7 +825,6 @@ export function buildSqlContractFromDefinition(
|
|
|
672
825
|
)
|
|
673
826
|
: undefined;
|
|
674
827
|
|
|
675
|
-
const defaultNamespaceId = defaultModelNamespaceId(target);
|
|
676
828
|
const domainNamespaceIds = new Set(Object.keys(modelsByNamespace));
|
|
677
829
|
if (domainNamespaceIds.size === 0) {
|
|
678
830
|
domainNamespaceIds.add(defaultNamespaceId);
|
|
@@ -680,13 +832,22 @@ export function buildSqlContractFromDefinition(
|
|
|
680
832
|
if (valueObjects !== undefined) {
|
|
681
833
|
domainNamespaceIds.add(defaultNamespaceId);
|
|
682
834
|
}
|
|
835
|
+
for (const nsId of Object.keys(domainEnumsByNs)) {
|
|
836
|
+
domainNamespaceIds.add(nsId);
|
|
837
|
+
}
|
|
683
838
|
const domainNamespaces = Object.fromEntries(
|
|
684
839
|
[...domainNamespaceIds].sort().map((namespaceId) => {
|
|
685
840
|
const modelsInNs = modelsByNamespace[namespaceId] ?? {};
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
841
|
+
const enumsInNs = domainEnumsByNs[namespaceId];
|
|
842
|
+
const namespaceSlice = {
|
|
843
|
+
models: modelsInNs,
|
|
844
|
+
...(namespaceId === defaultNamespaceId && valueObjects !== undefined
|
|
845
|
+
? { valueObjects }
|
|
846
|
+
: {}),
|
|
847
|
+
...(enumsInNs !== undefined && Object.keys(enumsInNs).length > 0
|
|
848
|
+
? { enum: enumsInNs }
|
|
849
|
+
: {}),
|
|
850
|
+
};
|
|
690
851
|
return [namespaceId, namespaceSlice];
|
|
691
852
|
}),
|
|
692
853
|
);
|
|
@@ -694,6 +855,7 @@ export function buildSqlContractFromDefinition(
|
|
|
694
855
|
const contract: Contract<SqlStorage> = {
|
|
695
856
|
target,
|
|
696
857
|
targetFamily,
|
|
858
|
+
...ifDefined('defaultControlPolicy', definition.defaultControlPolicy),
|
|
697
859
|
domain: { namespaces: domainNamespaces },
|
|
698
860
|
roots,
|
|
699
861
|
storage,
|
package/src/config-types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { pathToFileURL } from 'node:url';
|
|
2
2
|
import type { ContractConfig } from '@prisma-next/config/config-types';
|
|
3
|
-
import
|
|
3
|
+
import { applySpecifierDefaultControlPolicy } from '@prisma-next/contract/apply-specifier-default-control-policy';
|
|
4
|
+
import type { Contract, ControlPolicy } from '@prisma-next/contract/types';
|
|
4
5
|
import type { TargetPackRef } from '@prisma-next/framework-components/components';
|
|
5
6
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
6
7
|
import { ok } from '@prisma-next/utils/result';
|
|
@@ -19,22 +20,35 @@ function defaultOutputFromContractPath(contractPath: string): string {
|
|
|
19
20
|
return `${contractPath.slice(0, -ext.length)}.json`;
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
export interface TypeScriptContractSpecifierOptions {
|
|
24
|
+
readonly defaultControlPolicy?: ControlPolicy;
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
export function emptyContract(options: {
|
|
23
28
|
readonly output?: string;
|
|
24
29
|
readonly target: TargetPackRef<'sql', string>;
|
|
30
|
+
readonly defaultControlPolicy?: ControlPolicy;
|
|
25
31
|
}): ContractConfig {
|
|
26
32
|
return {
|
|
27
33
|
source: {
|
|
28
|
-
load: async () =>
|
|
34
|
+
load: async () => {
|
|
35
|
+
const built = buildSqlContractFromDefinition({ target: options.target, models: [] });
|
|
36
|
+
return ok(applySpecifierDefaultControlPolicy(built, options.defaultControlPolicy));
|
|
37
|
+
},
|
|
29
38
|
},
|
|
30
39
|
...ifDefined('output', options.output),
|
|
31
40
|
};
|
|
32
41
|
}
|
|
33
42
|
|
|
34
|
-
export function typescriptContract(
|
|
43
|
+
export function typescriptContract(
|
|
44
|
+
contract: Contract,
|
|
45
|
+
output?: string,
|
|
46
|
+
options?: TypeScriptContractSpecifierOptions,
|
|
47
|
+
): ContractConfig {
|
|
35
48
|
return {
|
|
36
49
|
source: {
|
|
37
|
-
load: async () =>
|
|
50
|
+
load: async () =>
|
|
51
|
+
ok(applySpecifierDefaultControlPolicy(contract, options?.defaultControlPolicy)),
|
|
38
52
|
},
|
|
39
53
|
// The in-memory variant has no input path to anchor on; fall through to
|
|
40
54
|
// the global default in `normalizeContractConfig` when caller doesn't pin it.
|
|
@@ -42,7 +56,11 @@ export function typescriptContract(contract: Contract, output?: string): Contrac
|
|
|
42
56
|
};
|
|
43
57
|
}
|
|
44
58
|
|
|
45
|
-
export function typescriptContractFromPath(
|
|
59
|
+
export function typescriptContractFromPath(
|
|
60
|
+
contractPath: string,
|
|
61
|
+
output?: string,
|
|
62
|
+
options?: TypeScriptContractSpecifierOptions,
|
|
63
|
+
): ContractConfig {
|
|
46
64
|
return {
|
|
47
65
|
source: {
|
|
48
66
|
inputs: [contractPath],
|
|
@@ -60,7 +78,7 @@ export function typescriptContractFromPath(contractPath: string, output?: string
|
|
|
60
78
|
`typescriptContractFromPath: module at "${absolutePath}" has no "default" or "contract" export.`,
|
|
61
79
|
);
|
|
62
80
|
}
|
|
63
|
-
return ok(contract);
|
|
81
|
+
return ok(applySpecifierDefaultControlPolicy(contract, options?.defaultControlPolicy));
|
|
64
82
|
},
|
|
65
83
|
},
|
|
66
84
|
output: output ?? defaultOutputFromContractPath(contractPath),
|