@prisma-next/sql-contract-ts 0.12.0-dev.6 → 0.12.0-dev.61

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.6",
3
+ "version": "0.12.0-dev.61",
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.6",
10
- "@prisma-next/contract": "0.12.0-dev.6",
11
- "@prisma-next/contract-authoring": "0.12.0-dev.6",
12
- "@prisma-next/framework-components": "0.12.0-dev.6",
13
- "@prisma-next/sql-contract": "0.12.0-dev.6",
14
- "@prisma-next/utils": "0.12.0-dev.6",
9
+ "@prisma-next/config": "0.12.0-dev.61",
10
+ "@prisma-next/contract": "0.12.0-dev.61",
11
+ "@prisma-next/contract-authoring": "0.12.0-dev.61",
12
+ "@prisma-next/framework-components": "0.12.0-dev.61",
13
+ "@prisma-next/sql-contract": "0.12.0-dev.61",
14
+ "@prisma-next/utils": "0.12.0-dev.61",
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.6",
21
- "@prisma-next/tsconfig": "0.12.0-dev.6",
20
+ "@prisma-next/test-utils": "0.12.0-dev.61",
21
+ "@prisma-next/tsconfig": "0.12.0-dev.61",
22
22
  "@types/pg": "8.20.0",
23
- "pg": "8.20.0",
24
- "@prisma-next/tsdown": "0.12.0-dev.6",
25
- "tsdown": "0.22.0",
23
+ "pg": "8.21.0",
24
+ "@prisma-next/tsdown": "0.12.0-dev.61",
25
+ "tsdown": "0.22.1",
26
26
  "typescript": "5.9.3",
27
- "vitest": "4.1.6"
27
+ "vitest": "4.1.7"
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';
@@ -41,6 +43,7 @@ import {
41
43
  StorageTable,
42
44
  type StorageTableInput,
43
45
  type StorageTypeInstance,
46
+ type StorageValueSetInput,
44
47
  toStorageTypeInstance,
45
48
  } from '@prisma-next/sql-contract/types';
46
49
  import { validateStorageSemantics } from '@prisma-next/sql-contract/validators';
@@ -156,16 +159,17 @@ const JSONB_NATIVE_TYPE = 'jsonb';
156
159
  function resolveModelNamespaceId(
157
160
  model: ModelNode,
158
161
  modelNameToNamespaceId: ReadonlyMap<string, string>,
159
- targetId: string,
162
+ defaultNamespaceId: string,
160
163
  ): string {
161
164
  if (model.namespaceId !== undefined && model.namespaceId.length > 0) {
162
165
  return model.namespaceId;
163
166
  }
164
- return modelNameToNamespaceId.get(model.modelName) ?? defaultModelNamespaceId(targetId);
167
+ return modelNameToNamespaceId.get(model.modelName) ?? defaultNamespaceId;
165
168
  }
166
169
 
167
170
  function buildStorageColumn(
168
171
  field: FieldNode | ValueObjectFieldNode,
172
+ storageValueSetRef: ValueSetRef | undefined,
169
173
  codecLookup?: CodecLookup,
170
174
  ): StorageColumn {
171
175
  if (isValueObjectField(field)) {
@@ -203,12 +207,14 @@ function buildStorageColumn(
203
207
  ...ifDefined('typeParams', field.descriptor.typeParams),
204
208
  ...ifDefined('default', encodedDefault),
205
209
  ...ifDefined('typeRef', field.descriptor.typeRef),
210
+ ...ifDefined('valueSet', storageValueSetRef),
206
211
  };
207
212
  }
208
213
 
209
214
  function buildDomainField(
210
215
  field: FieldNode | ValueObjectFieldNode,
211
216
  column: StorageColumn,
217
+ domainValueSetRef: ValueSetRef | undefined,
212
218
  ): ContractField {
213
219
  if (isValueObjectField(field)) {
214
220
  return {
@@ -226,12 +232,13 @@ function buildDomainField(
226
232
  },
227
233
  nullable: column.nullable,
228
234
  ...(field.many ? { many: true } : {}),
235
+ ...ifDefined('valueSet', domainValueSetRef),
229
236
  };
230
237
  }
231
238
 
232
239
  function collectStorageNamespaceCoordinateIds(definition: ContractDefinition): Set<string> {
233
240
  const ids = new Set<string>();
234
- ids.add(defaultModelNamespaceId(definition.target.targetId));
241
+ ids.add(definition.target.defaultNamespaceId);
235
242
  for (const id of definition.namespaces ?? []) {
236
243
  if (id.length > 0) {
237
244
  ids.add(id);
@@ -245,22 +252,38 @@ function collectStorageNamespaceCoordinateIds(definition: ContractDefinition): S
245
252
  return ids;
246
253
  }
247
254
 
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;
255
+ function ensureUnboundNamespaceSlot(
256
+ namespaces: SqlStorageInput['namespaces'],
257
+ createNamespace: ContractDefinition['createNamespace'],
258
+ ): SqlStorageInput['namespaces'] {
259
+ if (Object.hasOwn(namespaces, UNBOUND_NAMESPACE_ID)) {
260
+ return namespaces;
261
+ }
262
+ const unboundInput: SqlNamespaceTablesInput = {
263
+ id: UNBOUND_NAMESPACE_ID,
264
+ entries: { table: {} },
265
+ };
266
+ const unbound = createNamespace ? createNamespace(unboundInput) : buildSqlNamespace(unboundInput);
267
+ return blindCast<
268
+ SqlStorageInput['namespaces'],
269
+ 'createNamespace may return a target namespace concretion; the unbound slot matches SqlNamespace at runtime'
270
+ >({
271
+ [UNBOUND_NAMESPACE_ID]: unbound,
272
+ ...namespaces,
273
+ });
253
274
  }
254
275
 
276
+ const POSTGRES_ENUM_NAMESPACE_ID = 'public';
277
+
255
278
  function partitionStorageTypesForTarget(
256
279
  targetId: string,
257
280
  types: Record<string, StorageTypeInstance | PostgresEnumStorageEntry>,
258
281
  namespaceTypes?: Readonly<Record<string, Readonly<Record<string, PostgresEnumStorageEntry>>>>,
259
282
  ): {
260
- readonly documentTypes: Record<string, StorageTypeInstance | PostgresEnumStorageEntry>;
283
+ readonly documentTypes: Record<string, StorageTypeInstance>;
261
284
  readonly namespaceEnumTypesById: Record<string, Record<string, PostgresEnumStorageEntry>>;
262
285
  } {
263
- const documentTypes: Record<string, StorageTypeInstance | PostgresEnumStorageEntry> = {};
286
+ const documentTypes: Record<string, StorageTypeInstance> = {};
264
287
  const namespaceEnumTypesById: Record<string, Record<string, PostgresEnumStorageEntry>> = {};
265
288
  for (const [name, entry] of Object.entries(types)) {
266
289
  if (isPostgresEnumStorageEntry(entry)) {
@@ -304,6 +327,7 @@ export function buildSqlContractFromDefinition(
304
327
  codecLookup?: CodecLookup,
305
328
  ): Contract<SqlStorage> {
306
329
  const target = definition.target.targetId;
330
+ const defaultNamespaceId = definition.target.defaultNamespaceId;
307
331
  const targetFamily = 'sql';
308
332
  const modelsByName = new Map(definition.models.map((m) => [m.modelName, m]));
309
333
 
@@ -319,9 +343,13 @@ export function buildSqlContractFromDefinition(
319
343
  const namespaceId =
320
344
  semanticModel.namespaceId !== undefined && semanticModel.namespaceId.length > 0
321
345
  ? semanticModel.namespaceId
322
- : defaultModelNamespaceId(target);
346
+ : defaultNamespaceId;
323
347
  modelNameToNamespaceId.set(semanticModel.modelName, namespaceId);
324
- roots[tableName] = crossRef(semanticModel.modelName, namespaceId);
348
+ // STI variants share the base table; the base model already owns this
349
+ // table name and its root, so the variant contributes neither.
350
+ if (!semanticModel.sharesBaseTable) {
351
+ roots[tableName] = crossRef(semanticModel.modelName, namespaceId);
352
+ }
325
353
 
326
354
  // --- Build storage table ---
327
355
 
@@ -348,11 +376,34 @@ export function buildSqlContractFromDefinition(
348
376
  }
349
377
  }
350
378
 
351
- const column = buildStorageColumn(field, codecLookup);
379
+ const enumHandle = !isValueObjectField(field) ? field.enumTypeHandle : undefined;
380
+ // Authored enums are always registered under the contract's defaultNamespaceId
381
+ // (see the enum registration loop below), so refs must point there regardless
382
+ // of which namespace the consuming model lives in.
383
+ const storageValueSetRef: ValueSetRef | undefined =
384
+ enumHandle !== undefined
385
+ ? {
386
+ plane: 'storage',
387
+ entityKind: 'value-set',
388
+ namespaceId: defaultNamespaceId,
389
+ name: enumHandle.enumName,
390
+ }
391
+ : undefined;
392
+ const domainValueSetRef: ValueSetRef | undefined =
393
+ enumHandle !== undefined
394
+ ? {
395
+ plane: 'domain',
396
+ entityKind: 'enum',
397
+ namespaceId: defaultNamespaceId,
398
+ name: enumHandle.enumName,
399
+ }
400
+ : undefined;
401
+
402
+ const column = buildStorageColumn(field, storageValueSetRef, codecLookup);
352
403
  columns[field.columnName] = column;
353
404
  fieldToColumn[field.fieldName] = field.columnName;
354
405
 
355
- domainFields[field.fieldName] = buildDomainField(field, column);
406
+ domainFields[field.fieldName] = buildDomainField(field, column, domainValueSetRef);
356
407
 
357
408
  if (isValueObjectField(field)) {
358
409
  domainFieldRefs[field.fieldName] = {
@@ -374,6 +425,31 @@ export function buildSqlContractFromDefinition(
374
425
  }
375
426
 
376
427
  const foreignKeys = (semanticModel.foreignKeys ?? []).map((fk) => {
428
+ if (fk.references.spaceId !== undefined) {
429
+ // Cross-space FK: the target lives in a different contract space.
430
+ // Skip local model lookup and carry the spaceId coordinate through.
431
+ const targetNamespaceId = fk.references.namespaceId ?? defaultNamespaceId;
432
+ return {
433
+ source: { namespaceId: asNamespaceId(namespaceId), tableName, columns: fk.columns },
434
+ target: {
435
+ namespaceId: asNamespaceId(targetNamespaceId),
436
+ tableName: fk.references.table,
437
+ columns: fk.references.columns,
438
+ spaceId: fk.references.spaceId,
439
+ },
440
+ ...applyFkDefaults(
441
+ {
442
+ ...ifDefined('constraint', fk.constraint),
443
+ ...ifDefined('index', fk.index),
444
+ },
445
+ definition.foreignKeyDefaults,
446
+ ),
447
+ ...ifDefined('name', fk.name),
448
+ ...ifDefined('onDelete', fk.onDelete),
449
+ ...ifDefined('onUpdate', fk.onUpdate),
450
+ };
451
+ }
452
+
377
453
  const targetModel = assertKnownTargetModel(
378
454
  modelsByName,
379
455
  semanticModel.modelName,
@@ -390,7 +466,7 @@ export function buildSqlContractFromDefinition(
390
466
  fk.references.namespaceId ??
391
467
  (targetModel.namespaceId !== undefined && targetModel.namespaceId.length > 0
392
468
  ? targetModel.namespaceId
393
- : defaultModelNamespaceId(target));
469
+ : defaultNamespaceId);
394
470
  return {
395
471
  source: { namespaceId: asNamespaceId(namespaceId), tableName, columns: fk.columns },
396
472
  target: {
@@ -411,48 +487,54 @@ export function buildSqlContractFromDefinition(
411
487
  };
412
488
  });
413
489
 
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}".`,
418
- );
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
- };
490
+ // STI variants share the base table: their columns are already
491
+ // materialised onto the base `ModelNode`, so the variant builds a domain
492
+ // model (below) but no storage table of its own.
493
+ if (!semanticModel.sharesBaseTable) {
494
+ const existingNs = tableNameToNamespaceId.get(tableName);
495
+ if (existingNs !== undefined && existingNs !== namespaceId) {
496
+ throw new Error(
497
+ `buildSqlContractFromDefinition: table "${tableName}" is mapped in namespace "${namespaceId}" but already exists in namespace "${existingNs}".`,
498
+ );
499
+ }
500
+ tableNameToNamespaceId.set(tableName, namespaceId);
501
+
502
+ const tableInput: StorageTableInput = {
503
+ columns,
504
+ ...ifDefined('control', semanticModel.control),
505
+ uniques: (semanticModel.uniques ?? []).map((u) => ({
506
+ columns: u.columns,
507
+ ...ifDefined('name', u.name),
508
+ })),
509
+ indexes: (semanticModel.indexes ?? []).map((i) => ({
510
+ columns: i.columns,
511
+ ...ifDefined('name', i.name),
512
+ ...ifDefined('type', i.type),
513
+ ...ifDefined('options', i.options),
514
+ })),
515
+ foreignKeys,
516
+ ...(semanticModel.id
517
+ ? {
518
+ primaryKey: {
519
+ columns: semanticModel.id.columns,
520
+ ...ifDefined('name', semanticModel.id.name),
521
+ },
522
+ }
523
+ : {}),
524
+ };
444
525
 
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
- );
526
+ let nsTables = tablesByNamespace[namespaceId];
527
+ if (nsTables === undefined) {
528
+ nsTables = {};
529
+ tablesByNamespace[namespaceId] = nsTables;
530
+ }
531
+ if (nsTables[tableName] !== undefined) {
532
+ throw new Error(
533
+ `buildSqlContractFromDefinition: duplicate table "${tableName}" in namespace "${namespaceId}".`,
534
+ );
535
+ }
536
+ nsTables[tableName] = new StorageTable(tableInput);
454
537
  }
455
- nsTables[tableName] = new StorageTable(tableInput);
456
538
 
457
539
  // --- Build contract model ---
458
540
 
@@ -466,6 +548,24 @@ export function buildSqlContractFromDefinition(
466
548
  );
467
549
  const modelRelations: Record<string, ContractRelation> = {};
468
550
  for (const relation of semanticModel.relations ?? []) {
551
+ // Cross-space relations have `spaceId` set — the target model lives in
552
+ // a different contract space, so skip local model lookup and validation.
553
+ if (relation.spaceId !== undefined) {
554
+ const targetNamespaceId = relation.namespaceId ?? defaultNamespaceId;
555
+ modelRelations[relation.fieldName] = {
556
+ to: crossRef(relation.toModel, targetNamespaceId, relation.spaceId),
557
+ // Cross-space belongsTo relations are always N:1 (the FK-owning side).
558
+ cardinality: 'N:1',
559
+ on: {
560
+ localFields: relation.on.parentColumns.map((col) => columnToField.get(col) ?? col),
561
+ // For cross-space targets the lowering carries field names directly
562
+ // (no fieldToColumn map available for the remote model).
563
+ targetFields: relation.on.childColumns,
564
+ },
565
+ };
566
+ continue;
567
+ }
568
+
469
569
  const targetModel = assertKnownTargetModel(
470
570
  modelsByName,
471
571
  semanticModel.modelName,
@@ -487,7 +587,7 @@ export function buildSqlContractFromDefinition(
487
587
  modelRelations[relation.fieldName] = {
488
588
  to: crossRef(
489
589
  relation.toModel,
490
- resolveModelNamespaceId(targetModel, modelNameToNamespaceId, target),
590
+ resolveModelNamespaceId(targetModel, modelNameToNamespaceId, defaultNamespaceId),
491
591
  ),
492
592
  // RelationDefinition.cardinality includes 'N:M' which isn't in
493
593
  // ContractReferenceRelation yet — cast is needed until the contract
@@ -517,6 +617,7 @@ export function buildSqlContractFromDefinition(
517
617
  namespaceModels[semanticModel.modelName] = {
518
618
  storage: {
519
619
  table: tableName,
620
+ namespaceId,
520
621
  fields: storageFields,
521
622
  },
522
623
  fields: domainFields,
@@ -557,6 +658,39 @@ export function buildSqlContractFromDefinition(
557
658
  for (const id of Object.keys(namespaceEnumTypesById)) {
558
659
  namespaceCoordinateIds.add(id);
559
660
  }
661
+
662
+ // Build per-namespace registries for `enumType()` handles.
663
+ // All authored enums target the contract's default namespace.
664
+ const domainEnumsByNs: Record<string, Record<string, ContractEnum>> = {};
665
+ const storageValueSetsByNs: Record<string, Record<string, StorageValueSetInput>> = {};
666
+ for (const [enumName, handle] of Object.entries(definition.enums ?? {})) {
667
+ if (enumName !== handle.enumName) {
668
+ throw new Error(
669
+ `enum declaration key "${enumName}" must match enumType name "${handle.enumName}". Aliases are not supported.`,
670
+ );
671
+ }
672
+ const nsId = defaultNamespaceId;
673
+ let domainSlot = domainEnumsByNs[nsId];
674
+ if (domainSlot === undefined) {
675
+ domainSlot = {};
676
+ domainEnumsByNs[nsId] = domainSlot;
677
+ }
678
+ domainSlot[enumName] = {
679
+ codecId: handle.codecId,
680
+ members: handle.enumMembers,
681
+ };
682
+
683
+ let storageSlot = storageValueSetsByNs[nsId];
684
+ if (storageSlot === undefined) {
685
+ storageSlot = {};
686
+ storageValueSetsByNs[nsId] = storageSlot;
687
+ }
688
+ storageSlot[enumName] = {
689
+ kind: 'value-set',
690
+ values: handle.values,
691
+ };
692
+ }
693
+
560
694
  const { createNamespace } = definition;
561
695
  const namespaces = blindCast<
562
696
  SqlStorageInput['namespaces'],
@@ -565,18 +699,26 @@ export function buildSqlContractFromDefinition(
565
699
  Object.fromEntries(
566
700
  [...namespaceCoordinateIds].sort().map((id) => {
567
701
  const enumTypes = namespaceEnumTypesById[id];
702
+ const valueSetEntries = storageValueSetsByNs[id];
568
703
  const nsInput: SqlNamespaceTablesInput = {
569
704
  id,
570
- tables: tablesByNamespace[id] ?? {},
571
- ...ifDefined('enum', enumTypes),
705
+ entries: {
706
+ table: tablesByNamespace[id] ?? {},
707
+ ...(valueSetEntries !== undefined && Object.keys(valueSetEntries).length > 0
708
+ ? { valueSet: valueSetEntries }
709
+ : {}),
710
+ },
572
711
  };
573
- return [id, createNamespace ? createNamespace(nsInput) : buildSqlNamespace(nsInput)];
712
+ return [
713
+ id,
714
+ createNamespace ? createNamespace(nsInput, enumTypes) : buildSqlNamespace(nsInput),
715
+ ];
574
716
  }),
575
717
  ),
576
718
  );
577
719
  const storageWithoutHash = {
578
720
  ...(Object.keys(documentTypes).length > 0 ? { types: documentTypes } : {}),
579
- namespaces,
721
+ namespaces: ensureUnboundNamespaceSlot(namespaces, createNamespace),
580
722
  };
581
723
  const storageHash: StorageHashBase<string> = definition.storageHash
582
724
  ? coreHash(definition.storageHash)
@@ -672,7 +814,6 @@ export function buildSqlContractFromDefinition(
672
814
  )
673
815
  : undefined;
674
816
 
675
- const defaultNamespaceId = defaultModelNamespaceId(target);
676
817
  const domainNamespaceIds = new Set(Object.keys(modelsByNamespace));
677
818
  if (domainNamespaceIds.size === 0) {
678
819
  domainNamespaceIds.add(defaultNamespaceId);
@@ -680,13 +821,22 @@ export function buildSqlContractFromDefinition(
680
821
  if (valueObjects !== undefined) {
681
822
  domainNamespaceIds.add(defaultNamespaceId);
682
823
  }
824
+ for (const nsId of Object.keys(domainEnumsByNs)) {
825
+ domainNamespaceIds.add(nsId);
826
+ }
683
827
  const domainNamespaces = Object.fromEntries(
684
828
  [...domainNamespaceIds].sort().map((namespaceId) => {
685
829
  const modelsInNs = modelsByNamespace[namespaceId] ?? {};
686
- const namespaceSlice =
687
- namespaceId === defaultNamespaceId && valueObjects !== undefined
688
- ? { models: modelsInNs, valueObjects }
689
- : { models: modelsInNs };
830
+ const enumsInNs = domainEnumsByNs[namespaceId];
831
+ const namespaceSlice = {
832
+ models: modelsInNs,
833
+ ...(namespaceId === defaultNamespaceId && valueObjects !== undefined
834
+ ? { valueObjects }
835
+ : {}),
836
+ ...(enumsInNs !== undefined && Object.keys(enumsInNs).length > 0
837
+ ? { enum: enumsInNs }
838
+ : {}),
839
+ };
690
840
  return [namespaceId, namespaceSlice];
691
841
  }),
692
842
  );
@@ -694,6 +844,7 @@ export function buildSqlContractFromDefinition(
694
844
  const contract: Contract<SqlStorage> = {
695
845
  target,
696
846
  targetFamily,
847
+ ...ifDefined('defaultControlPolicy', definition.defaultControlPolicy),
697
848
  domain: { namespaces: domainNamespaces },
698
849
  roots,
699
850
  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),