@prisma-next/sql-contract-ts 0.12.0-dev.7 → 0.12.0-dev.71

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/package.json CHANGED
@@ -1,30 +1,30 @@
1
1
  {
2
2
  "name": "@prisma-next/sql-contract-ts",
3
- "version": "0.12.0-dev.7",
3
+ "version": "0.12.0-dev.71",
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.7",
10
- "@prisma-next/contract": "0.12.0-dev.7",
11
- "@prisma-next/contract-authoring": "0.12.0-dev.7",
12
- "@prisma-next/framework-components": "0.12.0-dev.7",
13
- "@prisma-next/sql-contract": "0.12.0-dev.7",
14
- "@prisma-next/utils": "0.12.0-dev.7",
9
+ "@prisma-next/config": "0.12.0-dev.71",
10
+ "@prisma-next/contract": "0.12.0-dev.71",
11
+ "@prisma-next/contract-authoring": "0.12.0-dev.71",
12
+ "@prisma-next/framework-components": "0.12.0-dev.71",
13
+ "@prisma-next/sql-contract": "0.12.0-dev.71",
14
+ "@prisma-next/utils": "0.12.0-dev.71",
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.7",
21
- "@prisma-next/tsconfig": "0.12.0-dev.7",
20
+ "@prisma-next/test-utils": "0.12.0-dev.71",
21
+ "@prisma-next/tsconfig": "0.12.0-dev.71",
22
22
  "@types/pg": "8.20.0",
23
- "pg": "8.20.0",
24
- "@prisma-next/tsdown": "0.12.0-dev.7",
25
- "tsdown": "0.22.0",
23
+ "pg": "8.21.0",
24
+ "@prisma-next/tsdown": "0.12.0-dev.71",
25
+ "tsdown": "0.22.1",
26
26
  "typescript": "5.9.3",
27
- "vitest": "4.1.6"
27
+ "vitest": "4.1.8"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "typescript": ">=5.9"
@@ -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
- targetId: string,
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) ?? defaultModelNamespaceId(targetId);
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(defaultModelNamespaceId(definition.target.targetId));
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
- const POSTGRES_ENUM_NAMESPACE_ID = 'public';
249
- const POSTGRES_DEFAULT_NAMESPACE_ID = 'public';
250
-
251
- function defaultModelNamespaceId(targetId: string): string {
252
- return targetId === 'postgres' ? POSTGRES_DEFAULT_NAMESPACE_ID : UNBOUND_NAMESPACE_ID;
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 | PostgresEnumStorageEntry>;
284
+ readonly documentTypes: Record<string, StorageTypeInstance>;
261
285
  readonly namespaceEnumTypesById: Record<string, Record<string, PostgresEnumStorageEntry>>;
262
286
  } {
263
- const documentTypes: Record<string, StorageTypeInstance | PostgresEnumStorageEntry> = {};
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
- : defaultModelNamespaceId(target);
347
+ : defaultNamespaceId;
323
348
  modelNameToNamespaceId.set(semanticModel.modelName, namespaceId);
324
- roots[tableName] = crossRef(semanticModel.modelName, namespaceId);
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 column = buildStorageColumn(field, codecLookup);
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
- : defaultModelNamespaceId(target));
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
- const existingNs = tableNameToNamespaceId.get(tableName);
415
- if (existingNs !== undefined && existingNs !== namespaceId) {
416
- throw new Error(
417
- `buildSqlContractFromDefinition: table "${tableName}" is mapped in namespace "${namespaceId}" but already exists in namespace "${existingNs}".`,
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
- let nsTables = tablesByNamespace[namespaceId];
446
- if (nsTables === undefined) {
447
- nsTables = {};
448
- tablesByNamespace[namespaceId] = nsTables;
449
- }
450
- if (nsTables[tableName] !== undefined) {
451
- throw new Error(
452
- `buildSqlContractFromDefinition: duplicate table "${tableName}" in namespace "${namespaceId}".`,
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, target),
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
- tables: tablesByNamespace[id] ?? {},
571
- ...ifDefined('enum', enumTypes),
716
+ entries: {
717
+ table: tablesByNamespace[id] ?? {},
718
+ ...(valueSetEntries !== undefined && Object.keys(valueSetEntries).length > 0
719
+ ? { valueSet: valueSetEntries }
720
+ : {}),
721
+ },
572
722
  };
573
- return [id, createNamespace ? createNamespace(nsInput) : buildSqlNamespace(nsInput)];
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 namespaceSlice =
687
- namespaceId === defaultNamespaceId && valueObjects !== undefined
688
- ? { models: modelsInNs, valueObjects }
689
- : { models: modelsInNs };
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,
@@ -1,6 +1,7 @@
1
1
  import { pathToFileURL } from 'node:url';
2
2
  import type { ContractConfig } from '@prisma-next/config/config-types';
3
- import type { Contract } from '@prisma-next/contract/types';
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 () => ok(buildSqlContractFromDefinition({ target: options.target, models: [] })),
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(contract: Contract, output?: string): ContractConfig {
43
+ export function typescriptContract(
44
+ contract: Contract,
45
+ output?: string,
46
+ options?: TypeScriptContractSpecifierOptions,
47
+ ): ContractConfig {
35
48
  return {
36
49
  source: {
37
- load: async () => ok(contract),
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(contractPath: string, output?: string): ContractConfig {
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),