@prisma-next/sql-contract-ts 0.12.0-dev.5 → 0.12.0-dev.51

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,27 +1,27 @@
1
1
  {
2
2
  "name": "@prisma-next/sql-contract-ts",
3
- "version": "0.12.0-dev.5",
3
+ "version": "0.12.0-dev.51",
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.5",
10
- "@prisma-next/contract": "0.12.0-dev.5",
11
- "@prisma-next/contract-authoring": "0.12.0-dev.5",
12
- "@prisma-next/framework-components": "0.12.0-dev.5",
13
- "@prisma-next/sql-contract": "0.12.0-dev.5",
14
- "@prisma-next/utils": "0.12.0-dev.5",
9
+ "@prisma-next/config": "0.12.0-dev.51",
10
+ "@prisma-next/contract": "0.12.0-dev.51",
11
+ "@prisma-next/contract-authoring": "0.12.0-dev.51",
12
+ "@prisma-next/framework-components": "0.12.0-dev.51",
13
+ "@prisma-next/sql-contract": "0.12.0-dev.51",
14
+ "@prisma-next/utils": "0.12.0-dev.51",
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.5",
21
- "@prisma-next/tsconfig": "0.12.0-dev.5",
20
+ "@prisma-next/test-utils": "0.12.0-dev.51",
21
+ "@prisma-next/tsconfig": "0.12.0-dev.51",
22
22
  "@types/pg": "8.20.0",
23
23
  "pg": "8.20.0",
24
- "@prisma-next/tsdown": "0.12.0-dev.5",
24
+ "@prisma-next/tsdown": "0.12.0-dev.51",
25
25
  "tsdown": "0.22.0",
26
26
  "typescript": "5.9.3",
27
27
  "vitest": "4.1.6"
@@ -156,12 +156,12 @@ const JSONB_NATIVE_TYPE = 'jsonb';
156
156
  function resolveModelNamespaceId(
157
157
  model: ModelNode,
158
158
  modelNameToNamespaceId: ReadonlyMap<string, string>,
159
- targetId: string,
159
+ defaultNamespaceId: string,
160
160
  ): string {
161
161
  if (model.namespaceId !== undefined && model.namespaceId.length > 0) {
162
162
  return model.namespaceId;
163
163
  }
164
- return modelNameToNamespaceId.get(model.modelName) ?? defaultModelNamespaceId(targetId);
164
+ return modelNameToNamespaceId.get(model.modelName) ?? defaultNamespaceId;
165
165
  }
166
166
 
167
167
  function buildStorageColumn(
@@ -231,7 +231,7 @@ function buildDomainField(
231
231
 
232
232
  function collectStorageNamespaceCoordinateIds(definition: ContractDefinition): Set<string> {
233
233
  const ids = new Set<string>();
234
- ids.add(defaultModelNamespaceId(definition.target.targetId));
234
+ ids.add(definition.target.defaultNamespaceId);
235
235
  for (const id of definition.namespaces ?? []) {
236
236
  if (id.length > 0) {
237
237
  ids.add(id);
@@ -245,22 +245,38 @@ function collectStorageNamespaceCoordinateIds(definition: ContractDefinition): S
245
245
  return ids;
246
246
  }
247
247
 
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;
248
+ function ensureUnboundNamespaceSlot(
249
+ namespaces: SqlStorageInput['namespaces'],
250
+ createNamespace: ContractDefinition['createNamespace'],
251
+ ): SqlStorageInput['namespaces'] {
252
+ if (Object.hasOwn(namespaces, UNBOUND_NAMESPACE_ID)) {
253
+ return namespaces;
254
+ }
255
+ const unboundInput: SqlNamespaceTablesInput = {
256
+ id: UNBOUND_NAMESPACE_ID,
257
+ entries: { table: {} },
258
+ };
259
+ const unbound = createNamespace ? createNamespace(unboundInput) : buildSqlNamespace(unboundInput);
260
+ return blindCast<
261
+ SqlStorageInput['namespaces'],
262
+ 'createNamespace may return a target namespace concretion; the unbound slot matches SqlNamespace at runtime'
263
+ >({
264
+ [UNBOUND_NAMESPACE_ID]: unbound,
265
+ ...namespaces,
266
+ });
253
267
  }
254
268
 
269
+ const POSTGRES_ENUM_NAMESPACE_ID = 'public';
270
+
255
271
  function partitionStorageTypesForTarget(
256
272
  targetId: string,
257
273
  types: Record<string, StorageTypeInstance | PostgresEnumStorageEntry>,
258
274
  namespaceTypes?: Readonly<Record<string, Readonly<Record<string, PostgresEnumStorageEntry>>>>,
259
275
  ): {
260
- readonly documentTypes: Record<string, StorageTypeInstance | PostgresEnumStorageEntry>;
276
+ readonly documentTypes: Record<string, StorageTypeInstance>;
261
277
  readonly namespaceEnumTypesById: Record<string, Record<string, PostgresEnumStorageEntry>>;
262
278
  } {
263
- const documentTypes: Record<string, StorageTypeInstance | PostgresEnumStorageEntry> = {};
279
+ const documentTypes: Record<string, StorageTypeInstance> = {};
264
280
  const namespaceEnumTypesById: Record<string, Record<string, PostgresEnumStorageEntry>> = {};
265
281
  for (const [name, entry] of Object.entries(types)) {
266
282
  if (isPostgresEnumStorageEntry(entry)) {
@@ -304,6 +320,7 @@ export function buildSqlContractFromDefinition(
304
320
  codecLookup?: CodecLookup,
305
321
  ): Contract<SqlStorage> {
306
322
  const target = definition.target.targetId;
323
+ const defaultNamespaceId = definition.target.defaultNamespaceId;
307
324
  const targetFamily = 'sql';
308
325
  const modelsByName = new Map(definition.models.map((m) => [m.modelName, m]));
309
326
 
@@ -319,9 +336,13 @@ export function buildSqlContractFromDefinition(
319
336
  const namespaceId =
320
337
  semanticModel.namespaceId !== undefined && semanticModel.namespaceId.length > 0
321
338
  ? semanticModel.namespaceId
322
- : defaultModelNamespaceId(target);
339
+ : defaultNamespaceId;
323
340
  modelNameToNamespaceId.set(semanticModel.modelName, namespaceId);
324
- roots[tableName] = crossRef(semanticModel.modelName, namespaceId);
341
+ // STI variants share the base table; the base model already owns this
342
+ // table name and its root, so the variant contributes neither.
343
+ if (!semanticModel.sharesBaseTable) {
344
+ roots[tableName] = crossRef(semanticModel.modelName, namespaceId);
345
+ }
325
346
 
326
347
  // --- Build storage table ---
327
348
 
@@ -390,7 +411,7 @@ export function buildSqlContractFromDefinition(
390
411
  fk.references.namespaceId ??
391
412
  (targetModel.namespaceId !== undefined && targetModel.namespaceId.length > 0
392
413
  ? targetModel.namespaceId
393
- : defaultModelNamespaceId(target));
414
+ : defaultNamespaceId);
394
415
  return {
395
416
  source: { namespaceId: asNamespaceId(namespaceId), tableName, columns: fk.columns },
396
417
  target: {
@@ -411,48 +432,54 @@ export function buildSqlContractFromDefinition(
411
432
  };
412
433
  });
413
434
 
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
- };
435
+ // STI variants share the base table: their columns are already
436
+ // materialised onto the base `ModelNode`, so the variant builds a domain
437
+ // model (below) but no storage table of its own.
438
+ if (!semanticModel.sharesBaseTable) {
439
+ const existingNs = tableNameToNamespaceId.get(tableName);
440
+ if (existingNs !== undefined && existingNs !== namespaceId) {
441
+ throw new Error(
442
+ `buildSqlContractFromDefinition: table "${tableName}" is mapped in namespace "${namespaceId}" but already exists in namespace "${existingNs}".`,
443
+ );
444
+ }
445
+ tableNameToNamespaceId.set(tableName, namespaceId);
446
+
447
+ const tableInput: StorageTableInput = {
448
+ columns,
449
+ ...ifDefined('control', semanticModel.control),
450
+ uniques: (semanticModel.uniques ?? []).map((u) => ({
451
+ columns: u.columns,
452
+ ...ifDefined('name', u.name),
453
+ })),
454
+ indexes: (semanticModel.indexes ?? []).map((i) => ({
455
+ columns: i.columns,
456
+ ...ifDefined('name', i.name),
457
+ ...ifDefined('type', i.type),
458
+ ...ifDefined('options', i.options),
459
+ })),
460
+ foreignKeys,
461
+ ...(semanticModel.id
462
+ ? {
463
+ primaryKey: {
464
+ columns: semanticModel.id.columns,
465
+ ...ifDefined('name', semanticModel.id.name),
466
+ },
467
+ }
468
+ : {}),
469
+ };
444
470
 
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
- );
471
+ let nsTables = tablesByNamespace[namespaceId];
472
+ if (nsTables === undefined) {
473
+ nsTables = {};
474
+ tablesByNamespace[namespaceId] = nsTables;
475
+ }
476
+ if (nsTables[tableName] !== undefined) {
477
+ throw new Error(
478
+ `buildSqlContractFromDefinition: duplicate table "${tableName}" in namespace "${namespaceId}".`,
479
+ );
480
+ }
481
+ nsTables[tableName] = new StorageTable(tableInput);
454
482
  }
455
- nsTables[tableName] = new StorageTable(tableInput);
456
483
 
457
484
  // --- Build contract model ---
458
485
 
@@ -487,7 +514,7 @@ export function buildSqlContractFromDefinition(
487
514
  modelRelations[relation.fieldName] = {
488
515
  to: crossRef(
489
516
  relation.toModel,
490
- resolveModelNamespaceId(targetModel, modelNameToNamespaceId, target),
517
+ resolveModelNamespaceId(targetModel, modelNameToNamespaceId, defaultNamespaceId),
491
518
  ),
492
519
  // RelationDefinition.cardinality includes 'N:M' which isn't in
493
520
  // ContractReferenceRelation yet — cast is needed until the contract
@@ -517,6 +544,7 @@ export function buildSqlContractFromDefinition(
517
544
  namespaceModels[semanticModel.modelName] = {
518
545
  storage: {
519
546
  table: tableName,
547
+ namespaceId,
520
548
  fields: storageFields,
521
549
  },
522
550
  fields: domainFields,
@@ -567,16 +595,20 @@ export function buildSqlContractFromDefinition(
567
595
  const enumTypes = namespaceEnumTypesById[id];
568
596
  const nsInput: SqlNamespaceTablesInput = {
569
597
  id,
570
- tables: tablesByNamespace[id] ?? {},
571
- ...ifDefined('enum', enumTypes),
598
+ entries: {
599
+ table: tablesByNamespace[id] ?? {},
600
+ },
572
601
  };
573
- return [id, createNamespace ? createNamespace(nsInput) : buildSqlNamespace(nsInput)];
602
+ return [
603
+ id,
604
+ createNamespace ? createNamespace(nsInput, enumTypes) : buildSqlNamespace(nsInput),
605
+ ];
574
606
  }),
575
607
  ),
576
608
  );
577
609
  const storageWithoutHash = {
578
610
  ...(Object.keys(documentTypes).length > 0 ? { types: documentTypes } : {}),
579
- namespaces,
611
+ namespaces: ensureUnboundNamespaceSlot(namespaces, createNamespace),
580
612
  };
581
613
  const storageHash: StorageHashBase<string> = definition.storageHash
582
614
  ? coreHash(definition.storageHash)
@@ -672,7 +704,6 @@ export function buildSqlContractFromDefinition(
672
704
  )
673
705
  : undefined;
674
706
 
675
- const defaultNamespaceId = defaultModelNamespaceId(target);
676
707
  const domainNamespaceIds = new Set(Object.keys(modelsByNamespace));
677
708
  if (domainNamespaceIds.size === 0) {
678
709
  domainNamespaceIds.add(defaultNamespaceId);
@@ -694,6 +725,7 @@ export function buildSqlContractFromDefinition(
694
725
  const contract: Contract<SqlStorage> = {
695
726
  target,
696
727
  targetFamily,
728
+ ...ifDefined('defaultControlPolicy', definition.defaultControlPolicy),
697
729
  domain: { namespaces: domainNamespaces },
698
730
  roots,
699
731
  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),
@@ -1,3 +1,4 @@
1
+ import type { ControlPolicy } from '@prisma-next/contract/types';
1
2
  import type { ForeignKeyDefaultsState } from '@prisma-next/contract-authoring';
2
3
  import type { CodecLookup } from '@prisma-next/framework-components/codec';
3
4
  import type {
@@ -12,6 +13,7 @@ import type {
12
13
  StorageTypeInstance,
13
14
  } from '@prisma-next/sql-contract/types';
14
15
  import { blindCast } from '@prisma-next/utils/casts';
16
+ import { ifDefined } from '@prisma-next/utils/defined';
15
17
  import { buildSqlContractFromDefinition } from './build-contract';
16
18
  import {
17
19
  type ComposedAuthoringHelpers,
@@ -65,6 +67,7 @@ type ContractDefinition<
65
67
  readonly naming?: Naming;
66
68
  readonly storageHash?: StorageHash;
67
69
  readonly foreignKeyDefaults?: ForeignKeyDefaults;
70
+ readonly defaultControlPolicy?: ControlPolicy;
68
71
  readonly namespaces?: Namespaces;
69
72
  readonly createNamespace?: (input: SqlNamespaceTablesInput) => Namespace;
70
73
  readonly types?: Types;
@@ -87,6 +90,7 @@ type ContractScaffold<
87
90
  readonly naming?: Naming;
88
91
  readonly storageHash?: StorageHash;
89
92
  readonly foreignKeyDefaults?: ForeignKeyDefaults;
93
+ readonly defaultControlPolicy?: ControlPolicy;
90
94
  readonly namespaces?: Namespaces;
91
95
  readonly createNamespace?: (input: SqlNamespaceTablesInput) => Namespace;
92
96
  readonly types?: never;
@@ -289,6 +293,130 @@ function buildContractFromDsl<Definition extends ContractInput>(
289
293
  >(buildSqlContractFromDefinition(buildContractDefinition(definition), definition.codecLookup));
290
294
  }
291
295
 
296
+ // Input for buildBoundContract — all fields from ContractInput except family/target
297
+ // (those are injected by the builder, pre-bound at the call site).
298
+ type BoundDefinitionInput<
299
+ Types extends Record<string, StorageTypeInstance | PostgresEnumStorageEntry> = Record<
300
+ never,
301
+ never
302
+ >,
303
+ Models extends Record<string, ModelLike> = Record<never, never>,
304
+ ExtensionPacks extends Record<string, ExtensionPackRef<'sql', string>> | undefined = undefined,
305
+ Naming extends ContractInput['naming'] | undefined = undefined,
306
+ StorageHash extends string | undefined = undefined,
307
+ ForeignKeyDefaults extends ForeignKeyDefaultsState | undefined = undefined,
308
+ Namespaces extends readonly string[] | undefined = undefined,
309
+ > = {
310
+ readonly extensionPacks?: ExtensionPacks;
311
+ readonly naming?: Naming;
312
+ readonly storageHash?: StorageHash;
313
+ readonly foreignKeyDefaults?: ForeignKeyDefaults;
314
+ readonly defaultControlPolicy?: ControlPolicy;
315
+ readonly namespaces?: Namespaces;
316
+ readonly createNamespace?: (input: SqlNamespaceTablesInput) => Namespace;
317
+ readonly types?: Types;
318
+ readonly models?: Models;
319
+ readonly codecLookup?: CodecLookup;
320
+ };
321
+
322
+ // Merges a bound input with the pre-bound family/target to produce a full ContractDefinition.
323
+ type WithFamilyTarget<
324
+ Input,
325
+ F extends FamilyPackRef<string>,
326
+ T extends TargetPackRef<'sql', string>,
327
+ > = Input & { readonly family: F; readonly target: T };
328
+
329
+ /**
330
+ * Shared builder that assembles a SqlContract with pre-bound family and target.
331
+ * Extension wrappers keep their own public overloads and delegate their impl body here;
332
+ * this is a plain overloaded function (not a factory returning an overloaded function)
333
+ * so no overloaded-function-return cast is needed.
334
+ *
335
+ * Overload 1: definition form (no factory).
336
+ */
337
+ export function buildBoundContract<
338
+ const F extends FamilyPackRef<string>,
339
+ const T extends TargetPackRef<'sql', string>,
340
+ const Definition extends BoundDefinitionInput<
341
+ Record<string, StorageTypeInstance | PostgresEnumStorageEntry>,
342
+ Record<string, ModelLike>,
343
+ Record<string, ExtensionPackRef<'sql', string>> | undefined,
344
+ ContractInput['naming'] | undefined,
345
+ string | undefined,
346
+ ForeignKeyDefaultsState | undefined,
347
+ readonly string[] | undefined
348
+ >,
349
+ >(
350
+ family: F,
351
+ target: T,
352
+ definition: Definition,
353
+ factory?: undefined,
354
+ ): SqlContractResult<WithFamilyTarget<Definition, F, T>>;
355
+ /**
356
+ * Overload 2: factory form.
357
+ */
358
+ export function buildBoundContract<
359
+ const F extends FamilyPackRef<string>,
360
+ const T extends TargetPackRef<'sql', string>,
361
+ const Definition extends BoundDefinitionInput<
362
+ Record<string, StorageTypeInstance | PostgresEnumStorageEntry>,
363
+ Record<string, ModelLike>,
364
+ Record<string, ExtensionPackRef<'sql', string>> | undefined,
365
+ ContractInput['naming'] | undefined,
366
+ string | undefined,
367
+ ForeignKeyDefaultsState | undefined,
368
+ readonly string[] | undefined
369
+ >,
370
+ const Built extends {
371
+ readonly types?: Record<string, StorageTypeInstance | PostgresEnumStorageEntry>;
372
+ readonly models?: Record<string, ModelLike>;
373
+ },
374
+ >(
375
+ family: F,
376
+ target: T,
377
+ definition: Definition,
378
+ factory: (
379
+ helpers: ComposedAuthoringHelpers<F, T, NonNullable<Definition['extensionPacks']>>,
380
+ ) => Built,
381
+ ): SqlContractResult<WithFamilyTarget<Definition & Built, F, T>>;
382
+ /** Implementation. */
383
+ export function buildBoundContract(
384
+ family: FamilyPackRef<string>,
385
+ target: TargetPackRef<'sql', string>,
386
+ definition: Omit<ContractInput, 'family' | 'target'>,
387
+ factory?:
388
+ | ((
389
+ helpers: ComposedAuthoringHelpers<
390
+ FamilyPackRef<string>,
391
+ TargetPackRef<'sql', string>,
392
+ Record<string, ExtensionPackRef<'sql', string>> | undefined
393
+ >,
394
+ ) => {
395
+ readonly types?: Record<string, StorageTypeInstance | PostgresEnumStorageEntry>;
396
+ readonly models?: Record<string, ModelLike>;
397
+ })
398
+ | undefined,
399
+ ) {
400
+ const full = { ...definition, family, target };
401
+
402
+ if (factory !== undefined) {
403
+ const built = factory(
404
+ createComposedAuthoringHelpers({
405
+ family,
406
+ target,
407
+ extensionPacks: definition.extensionPacks,
408
+ }),
409
+ );
410
+ return buildContractFromDsl({
411
+ ...full,
412
+ ...ifDefined('types', built.types),
413
+ ...ifDefined('models', built.models),
414
+ });
415
+ }
416
+
417
+ return buildContractFromDsl(full);
418
+ }
419
+
292
420
  export function defineContract<
293
421
  const Family extends FamilyPackRef<string>,
294
422
  const Target extends TargetPackRef<'sql', string>,
@@ -384,22 +512,10 @@ export function defineContract(
384
512
  );
385
513
  }
386
514
 
387
- if (!factory) {
388
- return buildContractFromDsl(definition);
515
+ if (factory !== undefined) {
516
+ return buildBoundContract(definition.family, definition.target, definition, factory);
389
517
  }
390
-
391
- const builtDefinition = {
392
- ...definition,
393
- ...factory(
394
- createComposedAuthoringHelpers({
395
- family: definition.family,
396
- target: definition.target,
397
- extensionPacks: definition.extensionPacks,
398
- }),
399
- ),
400
- };
401
-
402
- return buildContractFromDsl(builtDefinition);
518
+ return buildBoundContract(definition.family, definition.target, definition);
403
519
  }
404
520
 
405
521
  export type {
@@ -1,4 +1,8 @@
1
- import type { ColumnDefault, ExecutionMutationDefaultPhases } from '@prisma-next/contract/types';
1
+ import type {
2
+ ColumnDefault,
3
+ ControlPolicy,
4
+ ExecutionMutationDefaultPhases,
5
+ } from '@prisma-next/contract/types';
2
6
  import type { ForeignKeyDefaultsState } from '@prisma-next/contract-authoring';
3
7
  import type { ColumnTypeDescriptor } from '@prisma-next/framework-components/codec';
4
8
  import type { ExtensionPackRef, TargetPackRef } from '@prisma-next/framework-components/components';
@@ -113,10 +117,20 @@ export interface ModelNode {
113
117
  readonly indexes?: readonly IndexNode[];
114
118
  readonly foreignKeys?: readonly ForeignKeyNode[];
115
119
  readonly relations?: readonly RelationNode[];
120
+ readonly control?: ControlPolicy;
121
+ /**
122
+ * Single-table-inheritance variants share their base model's storage table:
123
+ * the variant's columns are materialised onto the base `ModelNode`, and this
124
+ * model contributes a domain model but no storage table of its own. When set,
125
+ * the assembler builds the domain model but skips creating a (shadow) storage
126
+ * table and a root for this model — the base owns both.
127
+ */
128
+ readonly sharesBaseTable?: boolean;
116
129
  }
117
130
 
118
131
  export interface ContractDefinition {
119
132
  readonly target: TargetPackRef<'sql', string>;
133
+ readonly defaultControlPolicy?: ControlPolicy;
120
134
  readonly extensionPacks?: Record<string, ExtensionPackRef<'sql', string>>;
121
135
  readonly storageHash?: string;
122
136
  readonly foreignKeyDefaults?: ForeignKeyDefaultsState;
@@ -124,7 +138,7 @@ export interface ContractDefinition {
124
138
  /**
125
139
  * Enum types declared inside a named `namespace { enum … }` block,
126
140
  * keyed first by namespace id then by type name. These are routed to
127
- * `storage.namespaces[nsId].enum` rather than the implicit fallback
141
+ * `storage.namespaces[nsId].entries.type` rather than the implicit fallback
128
142
  * namespace used for top-level `storageTypes` enums.
129
143
  */
130
144
  readonly namespaceTypes?: Readonly<
@@ -139,8 +153,14 @@ export interface ContractDefinition {
139
153
  * Target-supplied factory that materialises a `Namespace` concretion
140
154
  * for a declared namespace coordinate. Mirrors
141
155
  * `ContractInput.createNamespace`.
156
+ *
157
+ * The optional second argument carries target-specific enum types for the
158
+ * namespace (e.g. postgres enum registrations keyed by type name).
142
159
  */
143
- readonly createNamespace?: (input: SqlNamespaceTablesInput) => Namespace;
160
+ readonly createNamespace?: (
161
+ input: SqlNamespaceTablesInput,
162
+ enumTypes?: Readonly<Record<string, PostgresEnumStorageEntry>>,
163
+ ) => Namespace;
144
164
  readonly models: readonly ModelNode[];
145
165
  readonly valueObjects?: readonly ValueObjectNode[];
146
166
  }
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  ColumnDefault,
3
3
  ColumnDefaultLiteralInputValue,
4
+ ControlPolicy,
4
5
  ExecutionMutationDefaultPhases,
5
6
  ExecutionMutationDefaultValue,
6
7
  } from '@prisma-next/contract/types';
@@ -785,6 +786,7 @@ export type ModelAttributesSpec = {
785
786
 
786
787
  export type SqlStageSpec = {
787
788
  readonly table?: string;
789
+ readonly control?: ControlPolicy;
788
790
  readonly indexes?: readonly IndexConstraint[];
789
791
  readonly foreignKeys?: readonly ForeignKeyConstraint[];
790
792
  };
@@ -1216,6 +1218,7 @@ export type ContractInput<
1216
1218
  readonly naming?: NamingConfig;
1217
1219
  readonly storageHash?: string;
1218
1220
  readonly foreignKeyDefaults?: ForeignKeyDefaultsState;
1221
+ readonly defaultControlPolicy?: ControlPolicy;
1219
1222
  /**
1220
1223
  * Declared namespace coordinates the contract recognises. Per-model
1221
1224
  * `namespace` references must reference an entry in this list (or the
@@ -614,6 +614,7 @@ function resolveModelNode(
614
614
  ...(indexes.length > 0 ? { indexes } : {}),
615
615
  ...(foreignKeys.length > 0 ? { foreignKeys } : {}),
616
616
  ...(relations.length > 0 ? { relations } : {}),
617
+ ...ifDefined('control', spec.sqlSpec?.control),
617
618
  };
618
619
  }
619
620
 
@@ -707,6 +708,7 @@ export function buildContractDefinition(definition: ContractInput): ContractDefi
707
708
 
708
709
  return {
709
710
  target: definition.target,
711
+ ...ifDefined('defaultControlPolicy', definition.defaultControlPolicy),
710
712
  ...(definition.extensionPacks ? { extensionPacks: definition.extensionPacks } : {}),
711
713
  ...(definition.storageHash ? { storageHash: definition.storageHash } : {}),
712
714
  ...(definition.foreignKeyDefaults ? { foreignKeyDefaults: definition.foreignKeyDefaults } : {}),