@prisma-next/sql-contract-psl 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/README.md CHANGED
@@ -14,7 +14,7 @@ This keeps core/CLI source-agnostic while giving PSL-first SQL users a one-line
14
14
  ## Responsibilities
15
15
 
16
16
  - Interpret `ParsePslDocumentResult` into SQL `Contract`
17
- - Interpret generic PSL attributes into SQL contract semantics (`@id`, `@unique`, `@default`, `@relation`, `@map`, `@@map`)
17
+ - Interpret generic PSL attributes into SQL contract semantics (`@id`, `@unique`, `@default`, `@relation`, `@map`, `@@map`, `@@control`)
18
18
  - Interpret SQL timestamp semantics: `DateTime @default(now())` (or the equivalent `temporal.createdAt()` field-preset call) as a storage default, and `temporal.updatedAt()` as an execution mutation default
19
19
  - Lower shared constructor expressions in both `types {}` blocks and inline field positions (for example `ShortName = sql.String(length: 35)` and `embedding pgvector.Vector(length: 1536)?`)
20
20
  - Lower supported default functions through composed registry inputs
@@ -71,12 +71,20 @@ Supported timestamp authoring surface:
71
71
  - The Prisma-flavored `@updatedAt` attribute is not supported; references produce `PSL_UNSUPPORTED_FIELD_ATTRIBUTE` with a migration hint pointing at `temporal.updatedAt()`. The hint is suppressed when the field already declares any `temporal.*` preset.
72
72
  - `@createdAt` is not supported as a PSL alias.
73
73
 
74
+ Model-level control policy:
75
+
76
+ - `@@control(<policy>)` lowers to the storage table's `control` field. The argument is one positional lowercase literal: `managed`, `tolerated`, `external`, or `observed`. Omit `@@control` to leave per-table control unset (the framework default applies at runtime).
77
+
78
+ Contract-level default (specifier options bag):
79
+
80
+ - `defaultControlPolicy` on `prismaContract(...)` sets `Contract.defaultControlPolicy` at load time when the interpreted contract does not already define one (source wins when both are present).
81
+
74
82
  ## Public API
75
83
 
76
84
  - `@prisma-next/sql-contract-psl`
77
85
  - `interpretPslDocumentToSqlContract({ document, target, scalarTypeDescriptors, authoringContributions?, controlMutationDefaults?, composedExtensionPacks? })`
78
86
  - `@prisma-next/sql-contract-psl/provider`
79
- - `prismaContract(schemaPath, { output?, target, scalarTypeDescriptors, authoringContributions?, controlMutationDefaults?, composedExtensionPacks? })`
87
+ - `prismaContract(schemaPath, { output?, target, defaultControlPolicy?, scalarTypeDescriptors, authoringContributions?, controlMutationDefaults?, composedExtensionPacks? })`
80
88
  - Provider input is fully preassembled by composition layers (for example `@prisma-next/family-sql/control` helpers).
81
89
 
82
90
  ## Dependencies
package/dist/index.d.mts CHANGED
@@ -1,12 +1,12 @@
1
1
  import { Contract } from "@prisma-next/contract/types";
2
2
  import { AuthoringContributions } from "@prisma-next/framework-components/authoring";
3
- import { Namespace } from "@prisma-next/framework-components/ir";
4
3
  import { SqlNamespaceTablesInput } from "@prisma-next/sql-contract/types";
5
4
  import { Result } from "@prisma-next/utils/result";
6
5
  import { ParsePslDocumentResult } from "@prisma-next/psl-parser";
7
6
  import { ControlMutationDefaults, ControlMutationDefaults as ControlMutationDefaults$1, DefaultFunctionLoweringContext, DefaultFunctionLoweringHandler, DefaultFunctionRegistry, DefaultFunctionRegistryEntry, MutationDefaultGeneratorDescriptor } from "@prisma-next/framework-components/control";
8
7
  import { ContractSourceDiagnostics } from "@prisma-next/config/config-types";
9
8
  import { ExtensionPackRef, TargetPackRef } from "@prisma-next/framework-components/components";
9
+ import { Namespace } from "@prisma-next/framework-components/ir";
10
10
 
11
11
  //#region src/psl-column-resolution.d.ts
12
12
  type ColumnDescriptor = {
@@ -25,6 +25,16 @@ interface InterpretPslDocumentToSqlContractInput {
25
25
  readonly composedExtensionPackRefs?: readonly ExtensionPackRef<'sql', string>[];
26
26
  readonly controlMutationDefaults?: ControlMutationDefaults$1;
27
27
  readonly authoringContributions?: AuthoringContributions;
28
+ /**
29
+ * Extension contracts keyed by space ID. Required for cross-space FK
30
+ * resolution. A composed space must have an entry here; if the space ID
31
+ * appears in `composedExtensionPacks` but is absent from this map, the
32
+ * interpreter emits `PSL_UNKNOWN_CONTRACT_SPACE` and fails fast — there
33
+ * is no silent fallback. If a space's contract is present but the
34
+ * referenced model or namespace is not found in it, the interpreter
35
+ * emits `PSL_UNKNOWN_CROSS_SPACE_TARGET`.
36
+ */
37
+ readonly composedExtensionContracts: ReadonlyMap<string, Contract>;
28
38
  /**
29
39
  * Target-supplied `Namespace` factory threaded into
30
40
  * `buildSqlContractFromDefinition` for the contract's
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/psl-column-resolution.ts","../src/interpreter.ts"],"mappings":";;;;;;;;;;;KAyCY,gBAAA;EAAA,SACD,OAAA;EAAA,SACA,UAAA;EAAA,SACA,OAAA;EAAA,SACA,UAAA,GAAa,MAAM;AAAA;;;UC8Cb,sCAAA;EAAA,SACN,QAAA,EAAU,sBAAA;EAAA,SACV,MAAA,EAAQ,aAAA;EAAA,SACR,qBAAA,EAAuB,WAAA,SAAoB,gBAAA;EAAA,SAC3C,sBAAA;EAAA,SACA,yBAAA,YAAqC,gBAAA;EAAA,SACrC,uBAAA,GAA0B,yBAAA;EAAA,SAC1B,sBAAA,GAAyB,sBAAA;EDrDzB;;;AAAmB;;;;AC8C9B;;ED9CW,SC+DA,eAAA,IAAmB,KAAA,EAAO,uBAAA,KAA4B,SAAA;AAAA;AAAA,iBAwyCjD,iCAAA,CACd,KAAA,EAAO,sCAAA,GACN,MAAA,CAAO,QAAA,EAAU,yBAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/psl-column-resolution.ts","../src/interpreter.ts"],"mappings":";;;;;;;;;;;KA0CY,gBAAA;EAAA,SACD,OAAA;EAAA,SACA,UAAA;EAAA,SACA,OAAA;EAAA,SACA,UAAA,GAAa,MAAM;AAAA;;;UCkDb,sCAAA;EAAA,SACN,QAAA,EAAU,sBAAA;EAAA,SACV,MAAA,EAAQ,aAAA;EAAA,SACR,qBAAA,EAAuB,WAAA,SAAoB,gBAAA;EAAA,SAC3C,sBAAA;EAAA,SACA,yBAAA,YAAqC,gBAAA;EAAA,SACrC,uBAAA,GAA0B,yBAAA;EAAA,SAC1B,sBAAA,GAAyB,sBAAA;EDzDzB;;;AAAmB;;;;ACkD9B;;EDlDW,SCmEA,0BAAA,EAA4B,WAAA,SAAoB,QAAA;EAhBtC;;;;;;;;;EAAA,SA0BV,eAAA,IAAmB,KAAA,EAAO,uBAAA,KAA4B,SAAA;AAAA;AAAA,iBAurDjD,iCAAA,CACd,KAAA,EAAO,sCAAA,GACN,MAAA,CAAO,QAAA,EAAU,yBAAA"}
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { t as interpretPslDocumentToSqlContract } from "./interpreter-QE6eZZof.mjs";
1
+ import { t as interpretPslDocumentToSqlContract } from "./interpreter-B5_yovSP.mjs";
2
2
  export { interpretPslDocumentToSqlContract };
@@ -1,8 +1,8 @@
1
1
  import { crossRef } from "@prisma-next/contract/types";
2
2
  import { hasRegisteredFieldNamespace, instantiateAuthoringEntityType, instantiateAuthoringFieldPreset, instantiateAuthoringTypeConstructor, isAuthoringEntityTypeDescriptor, isAuthoringFieldPresetDescriptor, isAuthoringTypeConstructorDescriptor, validateAuthoringHelperArguments } from "@prisma-next/framework-components/authoring";
3
- import { UNBOUND_NAMESPACE_ID } from "@prisma-next/framework-components/ir";
4
3
  import { isPostgresEnumStorageEntry } from "@prisma-next/sql-contract/types";
5
4
  import { buildSqlContractFromDefinition } from "@prisma-next/sql-contract-ts/contract-builder";
5
+ import { blindCast } from "@prisma-next/utils/casts";
6
6
  import { ifDefined } from "@prisma-next/utils/defined";
7
7
  import { notOk, ok } from "@prisma-next/utils/result";
8
8
  import { getPositionalArgument, parseQuotedStringLiteral } from "@prisma-next/psl-parser";
@@ -279,6 +279,56 @@ function findDuplicateFieldName(fieldNames) {
279
279
  seen.add(name);
280
280
  }
281
281
  }
282
+ const CONTROL_POLICY_LITERAL_SET = new Set([
283
+ "managed",
284
+ "tolerated",
285
+ "external",
286
+ "observed"
287
+ ]);
288
+ function isControlPolicyLiteral(value) {
289
+ return CONTROL_POLICY_LITERAL_SET.has(value);
290
+ }
291
+ function parseControlPolicyAttribute(input) {
292
+ if (input.attribute.args.filter((arg) => arg.kind === "named").length > 0) {
293
+ input.diagnostics.push({
294
+ code: "PSL_INVALID_ATTRIBUTE_ARGUMENT",
295
+ message: "`@@control` does not accept named arguments; pass the policy positionally as `@@control(external)`.",
296
+ sourceId: input.sourceId,
297
+ span: input.attribute.span
298
+ });
299
+ return;
300
+ }
301
+ const positionalArgs = getPositionalArguments(input.attribute);
302
+ if (positionalArgs.length === 0) {
303
+ input.diagnostics.push({
304
+ code: "PSL_INVALID_ATTRIBUTE_ARGUMENT",
305
+ message: "`@@control` requires exactly one positional argument: `managed`, `tolerated`, `external`, or `observed`.",
306
+ sourceId: input.sourceId,
307
+ span: input.attribute.span
308
+ });
309
+ return;
310
+ }
311
+ if (positionalArgs.length > 1) {
312
+ input.diagnostics.push({
313
+ code: "PSL_INVALID_ATTRIBUTE_ARGUMENT",
314
+ message: `\`@@control\` accepts exactly one positional argument; got ${positionalArgs.length}.`,
315
+ sourceId: input.sourceId,
316
+ span: input.attribute.span
317
+ });
318
+ return;
319
+ }
320
+ const token = unquoteStringLiteral(positionalArgs[0] ?? "").trim();
321
+ if (!isControlPolicyLiteral(token)) {
322
+ input.diagnostics.push({
323
+ code: "PSL_INVALID_ATTRIBUTE_ARGUMENT",
324
+ message: `\`@@control\` argument \`${token}\` is not a known policy. Allowed: \`managed\`, \`tolerated\`, \`external\`, \`observed\`.`,
325
+ sourceId: input.sourceId,
326
+ span: input.attribute.span
327
+ });
328
+ return;
329
+ }
330
+ return token;
331
+ }
282
332
  function mapFieldNamesToColumns(input) {
283
333
  const columns = [];
284
334
  for (const fieldName of input.fieldNames) {
@@ -730,7 +780,7 @@ function getAuthoringTypeConstructor(contributions, path) {
730
780
  let current = contributions?.type;
731
781
  for (const segment of path) {
732
782
  if (typeof current !== "object" || current === null || Array.isArray(current)) return;
733
- current = current[segment];
783
+ current = blindCast(current)[segment];
734
784
  }
735
785
  return isAuthoringTypeConstructorDescriptor(current) ? current : void 0;
736
786
  }
@@ -749,7 +799,7 @@ function getAuthoringEntity(contributions, path) {
749
799
  let current = contributions?.entityTypes;
750
800
  for (const segment of path) {
751
801
  if (typeof current !== "object" || current === null || Array.isArray(current)) return;
752
- current = current[segment];
802
+ current = blindCast(current)[segment];
753
803
  }
754
804
  return isAuthoringEntityTypeDescriptor(current) ? current : void 0;
755
805
  }
@@ -762,7 +812,7 @@ function getAuthoringFieldPreset(contributions, path) {
762
812
  let current = contributions?.field;
763
813
  for (const segment of path) {
764
814
  if (typeof current !== "object" || current === null || Array.isArray(current)) return;
765
- current = current[segment];
815
+ current = blindCast(current)[segment];
766
816
  }
767
817
  return isAuthoringFieldPresetDescriptor(current) ? current : void 0;
768
818
  }
@@ -1344,6 +1394,7 @@ function collectResolvedFields(input) {
1344
1394
  });
1345
1395
  const relationAttribute = getAttribute(field.attributes, "relation");
1346
1396
  if (isModelField && relationAttribute) continue;
1397
+ if (field.typeContractSpaceId !== void 0 && relationAttribute) continue;
1347
1398
  const isValueObjectField = compositeTypeNames.has(field.typeName);
1348
1399
  const isListField = field.list;
1349
1400
  let descriptor;
@@ -1852,9 +1903,6 @@ const UNSPECIFIED_PSL_NAMESPACE_NAME = "__unspecified__";
1852
1903
  * slot empty (which means the late-bound default at the `StorageTable`
1853
1904
  * layer; emitted JSON omits the field).
1854
1905
  */
1855
- function defaultSqlNamespaceIdForTarget(targetId) {
1856
- return targetId === "postgres" ? "public" : UNBOUND_NAMESPACE_ID;
1857
- }
1858
1906
  function resolveNamespaceIdForSqlTarget(input) {
1859
1907
  if (input.targetId !== "postgres") return;
1860
1908
  if (input.bucketName === UNSPECIFIED_PSL_NAMESPACE_NAME) return "public";
@@ -2132,6 +2180,8 @@ function buildModelNodeFromPsl(input) {
2132
2180
  } : void 0;
2133
2181
  const hasInlinePrimaryKey = primaryKey !== void 0;
2134
2182
  let blockPrimaryKeyDeclared = false;
2183
+ let controlPolicyDeclared = false;
2184
+ let controlPolicy;
2135
2185
  const resultBackrelationCandidates = [];
2136
2186
  for (const field of model.fields) {
2137
2187
  if (!field.list || !input.modelNames.has(field.typeName)) continue;
@@ -2198,6 +2248,25 @@ function buildModelNodeFromPsl(input) {
2198
2248
  for (const modelAttribute of model.attributes) {
2199
2249
  if (modelAttribute.name === "map") continue;
2200
2250
  if (modelAttribute.name === "discriminator" || modelAttribute.name === "base") continue;
2251
+ if (modelAttribute.name === "control") {
2252
+ if (controlPolicyDeclared) {
2253
+ diagnostics.push({
2254
+ code: "PSL_DUPLICATE_ATTRIBUTE",
2255
+ message: `\`@@control\` declared more than once on model "${model.name}".`,
2256
+ sourceId,
2257
+ span: modelAttribute.span
2258
+ });
2259
+ continue;
2260
+ }
2261
+ controlPolicyDeclared = true;
2262
+ const parsed = parseControlPolicyAttribute({
2263
+ attribute: modelAttribute,
2264
+ sourceId,
2265
+ diagnostics
2266
+ });
2267
+ if (parsed !== void 0) controlPolicy = parsed;
2268
+ continue;
2269
+ }
2201
2270
  const attributeLabel = `Model "${model.name}" @@${modelAttribute.name}`;
2202
2271
  if (modelAttribute.name === "id") {
2203
2272
  if (blockPrimaryKeyDeclared) {
@@ -2383,9 +2452,136 @@ function buildModelNodeFromPsl(input) {
2383
2452
  });
2384
2453
  }
2385
2454
  const resultFkRelationMetadata = [];
2455
+ const resultCrossSpaceRelations = [];
2386
2456
  for (const relationAttribute of relationAttributes) {
2387
- if (relationAttribute.field.list) continue;
2388
- const { typeName: fieldTypeName, typeNamespaceId: fieldTypeNamespaceId } = relationAttribute.field;
2457
+ const { typeName: fieldTypeName, typeNamespaceId: fieldTypeNamespaceId, typeContractSpaceId: fieldTypeContractSpaceId } = relationAttribute.field;
2458
+ if (relationAttribute.field.list) {
2459
+ if (fieldTypeContractSpaceId !== void 0) diagnostics.push({
2460
+ code: "PSL_UNSUPPORTED_CROSS_SPACE_LIST",
2461
+ message: `Relation field "${model.name}.${relationAttribute.field.name}" is a cross-space list relation (type "${fieldTypeContractSpaceId}:${fieldTypeNamespaceId !== void 0 ? `${fieldTypeNamespaceId}.` : ""}${fieldTypeName}[]"). Cross-space relations must be singular in v0.1 — list cross-space relations are not supported.`,
2462
+ sourceId,
2463
+ span: relationAttribute.field.span
2464
+ });
2465
+ continue;
2466
+ }
2467
+ if (fieldTypeContractSpaceId !== void 0) {
2468
+ const extContractForSpace = input.composedExtensionContracts.get(fieldTypeContractSpaceId);
2469
+ if (extContractForSpace === void 0) {
2470
+ diagnostics.push({
2471
+ code: "PSL_UNKNOWN_CONTRACT_SPACE",
2472
+ message: `Relation field "${model.name}.${relationAttribute.field.name}" references contract space "${fieldTypeContractSpaceId}" which is not declared in extensionPacks. Add "${fieldTypeContractSpaceId}" to extensionPacks in prisma-next.config.ts.`,
2473
+ sourceId,
2474
+ span: relationAttribute.field.span,
2475
+ data: {
2476
+ space: fieldTypeContractSpaceId,
2477
+ suggestedPack: fieldTypeContractSpaceId
2478
+ }
2479
+ });
2480
+ continue;
2481
+ }
2482
+ const parsedRelation = parseRelationAttribute({
2483
+ attribute: relationAttribute.relation,
2484
+ modelName: model.name,
2485
+ fieldName: relationAttribute.field.name,
2486
+ sourceId,
2487
+ diagnostics
2488
+ });
2489
+ if (!parsedRelation) continue;
2490
+ if (!parsedRelation.fields || !parsedRelation.references) {
2491
+ diagnostics.push({
2492
+ code: "PSL_INVALID_RELATION_ATTRIBUTE",
2493
+ message: `Relation field "${model.name}.${relationAttribute.field.name}" requires fields and references arguments`,
2494
+ sourceId,
2495
+ span: relationAttribute.relation.span
2496
+ });
2497
+ continue;
2498
+ }
2499
+ const localColumns = mapFieldNamesToColumns({
2500
+ modelName: model.name,
2501
+ fieldNames: parsedRelation.fields,
2502
+ mapping,
2503
+ sourceId,
2504
+ diagnostics,
2505
+ span: relationAttribute.relation.span,
2506
+ entityLabel: `Relation field "${model.name}.${relationAttribute.field.name}"`
2507
+ });
2508
+ if (!localColumns) continue;
2509
+ const referencedColumns = parsedRelation.references;
2510
+ if (localColumns.length !== referencedColumns.length) {
2511
+ diagnostics.push({
2512
+ code: "PSL_INVALID_RELATION_ATTRIBUTE",
2513
+ message: `Relation field "${model.name}.${relationAttribute.field.name}" must provide the same number of fields and references`,
2514
+ sourceId,
2515
+ span: relationAttribute.relation.span
2516
+ });
2517
+ continue;
2518
+ }
2519
+ const onDelete = parsedRelation.onDelete ? normalizeReferentialAction({
2520
+ modelName: model.name,
2521
+ fieldName: relationAttribute.field.name,
2522
+ actionName: "onDelete",
2523
+ actionToken: parsedRelation.onDelete,
2524
+ sourceId,
2525
+ span: relationAttribute.field.span,
2526
+ diagnostics
2527
+ }) : void 0;
2528
+ const onUpdate = parsedRelation.onUpdate ? normalizeReferentialAction({
2529
+ modelName: model.name,
2530
+ fieldName: relationAttribute.field.name,
2531
+ actionName: "onUpdate",
2532
+ actionToken: parsedRelation.onUpdate,
2533
+ sourceId,
2534
+ span: relationAttribute.field.span,
2535
+ diagnostics
2536
+ }) : void 0;
2537
+ const crossTargetNamespaceId = fieldTypeNamespaceId ?? "__unbound__";
2538
+ const extContract = extContractForSpace;
2539
+ const resolvedTable = extContract.domain.namespaces[crossTargetNamespaceId]?.models[fieldTypeName]?.storage["table"];
2540
+ if (typeof resolvedTable !== "string") {
2541
+ const availableModels = Object.keys(extContract.domain.namespaces[crossTargetNamespaceId]?.models ?? {}).join(", ") || "(none)";
2542
+ diagnostics.push({
2543
+ code: "PSL_UNKNOWN_CROSS_SPACE_TARGET",
2544
+ message: `Relation field "${model.name}.${relationAttribute.field.name}" references model "${fieldTypeName}" in namespace "${crossTargetNamespaceId}" of space "${fieldTypeContractSpaceId}", but that model was not found in the extension contract. Available models: ${availableModels}`,
2545
+ sourceId,
2546
+ span: relationAttribute.field.span,
2547
+ data: {
2548
+ space: fieldTypeContractSpaceId,
2549
+ namespace: crossTargetNamespaceId,
2550
+ model: fieldTypeName
2551
+ }
2552
+ });
2553
+ continue;
2554
+ }
2555
+ const crossTargetTableName = resolvedTable;
2556
+ foreignKeyNodes.push({
2557
+ columns: localColumns,
2558
+ references: {
2559
+ model: fieldTypeName,
2560
+ table: crossTargetTableName,
2561
+ columns: referencedColumns,
2562
+ namespaceId: crossTargetNamespaceId,
2563
+ spaceId: fieldTypeContractSpaceId
2564
+ },
2565
+ ...ifDefined("name", parsedRelation.constraintName),
2566
+ ...ifDefined("onDelete", onDelete),
2567
+ ...ifDefined("onUpdate", onUpdate)
2568
+ });
2569
+ resultCrossSpaceRelations.push({
2570
+ fieldName: relationAttribute.field.name,
2571
+ toModel: fieldTypeName,
2572
+ toTable: crossTargetTableName,
2573
+ cardinality: "N:1",
2574
+ spaceId: fieldTypeContractSpaceId,
2575
+ namespaceId: crossTargetNamespaceId,
2576
+ on: {
2577
+ parentTable: tableName,
2578
+ parentColumns: localColumns,
2579
+ childTable: crossTargetTableName,
2580
+ childColumns: referencedColumns
2581
+ }
2582
+ });
2583
+ continue;
2584
+ }
2389
2585
  const qualifiedTypeName = fieldTypeNamespaceId ? `${fieldTypeNamespaceId}.${fieldTypeName}` : fieldTypeName;
2390
2586
  if (!input.modelNames.has(fieldTypeName)) {
2391
2587
  diagnostics.push({
@@ -2520,9 +2716,11 @@ function buildModelNodeFromPsl(input) {
2520
2716
  ...ifDefined("id", primaryKey),
2521
2717
  ...uniqueConstraints.length > 0 ? { uniques: uniqueConstraints } : {},
2522
2718
  ...indexNodes.length > 0 ? { indexes: indexNodes } : {},
2523
- ...foreignKeyNodes.length > 0 ? { foreignKeys: foreignKeyNodes } : {}
2719
+ ...foreignKeyNodes.length > 0 ? { foreignKeys: foreignKeyNodes } : {},
2720
+ ...ifDefined("control", controlPolicy)
2524
2721
  },
2525
2722
  fkRelationMetadata: resultFkRelationMetadata,
2723
+ crossSpaceRelations: resultCrossSpaceRelations,
2526
2724
  backrelationCandidates: resultBackrelationCandidates,
2527
2725
  resolvedFields
2528
2726
  };
@@ -2688,8 +2886,16 @@ function collectPolymorphismDeclarations(models, sourceId, diagnostics) {
2688
2886
  baseDeclarations
2689
2887
  };
2690
2888
  }
2691
- function resolvePolymorphism(models, discriminatorDeclarations, baseDeclarations, modelNames, modelMappings, modelNamespaceIds, targetId, sourceId, diagnostics) {
2889
+ function resolvePolymorphism(models, discriminatorDeclarations, baseDeclarations, modelNames, modelMappings, modelNamespaceIds, defaultNamespaceId, syntheticPkFieldsByVariant, stiBaseFieldsByBase, sourceId, diagnostics) {
2692
2890
  let patched = models;
2891
+ for (const [baseName, fieldNames] of stiBaseFieldsByBase) {
2892
+ const baseModel = patched[baseName];
2893
+ if (!baseModel || fieldNames.length === 0) continue;
2894
+ patched = {
2895
+ ...patched,
2896
+ [baseName]: stripStorageOnlyDomainFields(baseModel, fieldNames)
2897
+ };
2898
+ }
2693
2899
  for (const [modelName, decl] of discriminatorDeclarations) {
2694
2900
  if (baseDeclarations.has(modelName)) {
2695
2901
  diagnostics.push({
@@ -2771,20 +2977,168 @@ function resolvePolymorphism(models, discriminatorDeclarations, baseDeclarations
2771
2977
  const baseMapping = modelMappings.get(baseDecl.baseName);
2772
2978
  const variantMapping = modelMappings.get(variantName);
2773
2979
  const resolvedTable = variantMapping?.model.attributes.some((attr) => attr.name === "map") ?? false ? variantMapping?.tableName : baseMapping?.tableName;
2980
+ const patchedVariant = {
2981
+ ...variantModel,
2982
+ base: crossRef(baseDecl.baseName, modelNamespaceIds.get(baseDecl.baseName) ?? defaultNamespaceId),
2983
+ ...resolvedTable ? { storage: {
2984
+ ...variantModel.storage,
2985
+ table: resolvedTable
2986
+ } } : {}
2987
+ };
2774
2988
  patched = {
2775
2989
  ...patched,
2776
- [variantName]: {
2777
- ...variantModel,
2778
- base: crossRef(baseDecl.baseName, modelNamespaceIds.get(baseDecl.baseName) ?? defaultSqlNamespaceIdForTarget(targetId)),
2779
- ...resolvedTable ? { storage: {
2780
- ...variantModel.storage,
2781
- table: resolvedTable
2782
- } } : {}
2783
- }
2990
+ [variantName]: stripStorageOnlyDomainFields(patchedVariant, syntheticPkFieldsByVariant.get(variantName) ?? [])
2784
2991
  };
2785
2992
  }
2786
2993
  return patched;
2787
2994
  }
2995
+ /**
2996
+ * Multi-table-inheritance variants (`@@base` + their own `@@map`) live in a
2997
+ * separate table from their base. The ORM joins that table to the base on the
2998
+ * shared primary key (`base.id = variant.id`), so the variant storage table
2999
+ * must carry the base PK column even though the variant domain model declares
3000
+ * only its own fields. This enriches each MTI variant's `ModelNode` with that
3001
+ * link column, a primary key on it, and a FK back to the base table.
3002
+ *
3003
+ * The link column is reported back per variant in `syntheticPkFieldsByVariant`
3004
+ * so the domain-model patch can drop it again — keeping the variant's domain
3005
+ * surface thin (its create/read inputs don't gain a redundant `id`) while the
3006
+ * storage table stays joinable. Single-table-inheritance variants (no own
3007
+ * table) are left untouched.
3008
+ */
3009
+ function materializeMtiVariantStorageLinks(modelNodes, baseDeclarations, stiVariantNames) {
3010
+ const nodeByModel = new Map(modelNodes.map((node) => [node.modelName, node]));
3011
+ const syntheticPkFieldsByVariant = /* @__PURE__ */ new Map();
3012
+ return {
3013
+ modelNodes: modelNodes.map((node) => {
3014
+ const baseDecl = baseDeclarations.get(node.modelName);
3015
+ if (!baseDecl) return node;
3016
+ const baseNode = nodeByModel.get(baseDecl.baseName);
3017
+ if (!baseNode) return node;
3018
+ if (stiVariantNames.has(node.modelName)) return node;
3019
+ const basePrimaryKey = baseNode.id;
3020
+ if (!basePrimaryKey || basePrimaryKey.columns.length === 0) return node;
3021
+ const existingColumns = new Set(node.fields.map((field) => field.columnName));
3022
+ const linkFields = [];
3023
+ for (const pkColumn of basePrimaryKey.columns) {
3024
+ if (existingColumns.has(pkColumn)) continue;
3025
+ const baseField = baseNode.fields.find((field) => "descriptor" in field && field.columnName === pkColumn);
3026
+ if (!baseField) continue;
3027
+ linkFields.push({
3028
+ fieldName: baseField.fieldName,
3029
+ columnName: pkColumn,
3030
+ descriptor: baseField.descriptor,
3031
+ nullable: false
3032
+ });
3033
+ }
3034
+ if (linkFields.length === 0) return node;
3035
+ syntheticPkFieldsByVariant.set(node.modelName, linkFields.map((field) => field.fieldName));
3036
+ const foreignKey = {
3037
+ columns: basePrimaryKey.columns,
3038
+ references: {
3039
+ model: baseNode.modelName,
3040
+ table: baseNode.tableName,
3041
+ columns: basePrimaryKey.columns,
3042
+ ...ifDefined("namespaceId", baseNode.namespaceId)
3043
+ },
3044
+ constraint: true,
3045
+ index: false,
3046
+ onDelete: "cascade"
3047
+ };
3048
+ return {
3049
+ ...node,
3050
+ fields: [...linkFields, ...node.fields],
3051
+ id: { columns: basePrimaryKey.columns },
3052
+ foreignKeys: [...node.foreignKeys ?? [], foreignKey]
3053
+ };
3054
+ }),
3055
+ syntheticPkFieldsByVariant
3056
+ };
3057
+ }
3058
+ /**
3059
+ * Single-table-inheritance variants (`@@base` with no own `@@map`) share the
3060
+ * base table: `resolvePolymorphism` points the variant's `storage.table` at the
3061
+ * base, and the ORM reads variant-declared fields straight off the base table.
3062
+ * For that to validate and round-trip, the base storage table must physically
3063
+ * carry every STI variant's declared columns. This enriches the base
3064
+ * `ModelNode` with those columns.
3065
+ *
3066
+ * The materialised columns are always nullable in storage: the base table hosts
3067
+ * every variant's rows, so a column a variant declares as required is still
3068
+ * NULL on sibling-variant rows. The variant's domain field keeps its declared
3069
+ * nullability — required-in-domain / nullable-in-storage is the intended STI
3070
+ * shape.
3071
+ *
3072
+ * Collisions (two variants declaring the same column, or a variant column name
3073
+ * clashing with a base column) are resolved skip-if-exists here, mirroring the
3074
+ * MTI link guard; surfacing them as diagnostics is tracked separately
3075
+ * (TML-2827).
3076
+ */
3077
+ function materializeStiVariantStorageColumns(modelNodes, baseDeclarations, stiVariantNames) {
3078
+ if (stiVariantNames.size === 0) return {
3079
+ modelNodes: [...modelNodes],
3080
+ stiBaseFieldsByBase: /* @__PURE__ */ new Map()
3081
+ };
3082
+ const nodeByModel = new Map(modelNodes.map((node) => [node.modelName, node]));
3083
+ const stiColumnsByBase = /* @__PURE__ */ new Map();
3084
+ for (const variantName of stiVariantNames) {
3085
+ const variantNode = nodeByModel.get(variantName);
3086
+ const baseDecl = baseDeclarations.get(variantName);
3087
+ if (!variantNode || !baseDecl) continue;
3088
+ const baseNode = nodeByModel.get(baseDecl.baseName);
3089
+ if (!baseNode) continue;
3090
+ const baseColumns = new Set(baseNode.fields.map((field) => field.columnName));
3091
+ const claimed = stiColumnsByBase.get(baseDecl.baseName) ?? [];
3092
+ const claimedColumns = new Set(claimed.map((field) => field.columnName));
3093
+ for (const field of variantNode.fields) {
3094
+ if (baseColumns.has(field.columnName) || claimedColumns.has(field.columnName)) continue;
3095
+ claimedColumns.add(field.columnName);
3096
+ claimed.push({
3097
+ ...field,
3098
+ nullable: true
3099
+ });
3100
+ }
3101
+ stiColumnsByBase.set(baseDecl.baseName, claimed);
3102
+ }
3103
+ const stiBaseFieldsByBase = /* @__PURE__ */ new Map();
3104
+ for (const [baseName, columns] of stiColumnsByBase) stiBaseFieldsByBase.set(baseName, columns.map((field) => field.fieldName));
3105
+ return {
3106
+ modelNodes: modelNodes.map((node) => {
3107
+ if (stiVariantNames.has(node.modelName)) return {
3108
+ ...node,
3109
+ sharesBaseTable: true
3110
+ };
3111
+ const stiColumns = stiColumnsByBase.get(node.modelName);
3112
+ if (!stiColumns || stiColumns.length === 0) return node;
3113
+ return {
3114
+ ...node,
3115
+ fields: [...node.fields, ...stiColumns]
3116
+ };
3117
+ }),
3118
+ stiBaseFieldsByBase
3119
+ };
3120
+ }
3121
+ /**
3122
+ * Drop the storage-only link fields (added by
3123
+ * {@link materializeMtiVariantStorageLinks}) from a variant's domain model, so
3124
+ * the domain surface stays thin while the storage table keeps the link column.
3125
+ */
3126
+ function stripStorageOnlyDomainFields(model, fieldNames) {
3127
+ if (fieldNames.length === 0) return model;
3128
+ const fields = { ...model.fields };
3129
+ for (const name of fieldNames) delete fields[name];
3130
+ const storage = blindCast(model.storage);
3131
+ const storageFields = { ...storage.fields };
3132
+ for (const name of fieldNames) delete storageFields[name];
3133
+ return {
3134
+ ...model,
3135
+ fields,
3136
+ storage: {
3137
+ ...storage,
3138
+ fields: storageFields
3139
+ }
3140
+ };
3141
+ }
2788
3142
  function interpretPslDocumentToSqlContract(input) {
2789
3143
  const sourceId = input.document.ast.sourceId;
2790
3144
  if (!input.target) return notOk({
@@ -2838,6 +3192,7 @@ function interpretPslDocumentToSqlContract(input) {
2838
3192
  const modelNames = new Set(models.map((model) => model.name));
2839
3193
  const compositeTypeNames = new Set(compositeTypes.map((ct) => ct.name));
2840
3194
  const composedExtensions = new Set(input.composedExtensionPacks ?? []);
3195
+ const composedExtensionContracts = input.composedExtensionContracts;
2841
3196
  const defaultFunctionRegistry = input.controlMutationDefaults?.defaultFunctionRegistry ?? /* @__PURE__ */ new Map();
2842
3197
  const generatorDescriptors = input.controlMutationDefaults?.generatorDescriptors ?? [];
2843
3198
  const generatorDescriptorById = /* @__PURE__ */ new Map();
@@ -2889,6 +3244,7 @@ function interpretPslDocumentToSqlContract(input) {
2889
3244
  const fkRelationMetadata = [];
2890
3245
  const backrelationCandidates = [];
2891
3246
  const modelResolvedFields = /* @__PURE__ */ new Map();
3247
+ const crossSpaceRelationsByModel = /* @__PURE__ */ new Map();
2892
3248
  for (const model of models) {
2893
3249
  const mapping = modelMappings.get(model.name);
2894
3250
  if (!mapping) continue;
@@ -2901,6 +3257,7 @@ function interpretPslDocumentToSqlContract(input) {
2901
3257
  enumTypeDescriptors: allEnumTypeDescriptors,
2902
3258
  namedTypeDescriptors: namedTypeResult.namedTypeDescriptors,
2903
3259
  composedExtensions,
3260
+ composedExtensionContracts,
2904
3261
  familyId: input.target.familyId,
2905
3262
  targetId: input.target.targetId,
2906
3263
  authoringContributions: input.authoringContributions,
@@ -2919,6 +3276,10 @@ function interpretPslDocumentToSqlContract(input) {
2919
3276
  fkRelationMetadata.push(...result.fkRelationMetadata);
2920
3277
  backrelationCandidates.push(...result.backrelationCandidates);
2921
3278
  modelResolvedFields.set(model.name, result.resolvedFields);
3279
+ if (result.crossSpaceRelations.length > 0) {
3280
+ const existing = crossSpaceRelationsByModel.get(model.name) ?? [];
3281
+ crossSpaceRelationsByModel.set(model.name, [...existing, ...result.crossSpaceRelations]);
3282
+ }
2922
3283
  }
2923
3284
  const { modelRelations, fkRelationsByPair } = indexFkRelations({ fkRelationMetadata });
2924
3285
  applyBackrelationCandidates({
@@ -2928,7 +3289,16 @@ function interpretPslDocumentToSqlContract(input) {
2928
3289
  diagnostics,
2929
3290
  sourceId
2930
3291
  });
3292
+ for (const [modelName, relations] of crossSpaceRelationsByModel) {
3293
+ const existing = modelRelations.get(modelName);
3294
+ if (existing) existing.push(...relations);
3295
+ else modelRelations.set(modelName, [...relations]);
3296
+ }
2931
3297
  const { discriminatorDeclarations, baseDeclarations } = collectPolymorphismDeclarations(models, sourceId, diagnostics);
3298
+ const stiVariantNames = /* @__PURE__ */ new Set();
3299
+ for (const variantName of baseDeclarations.keys()) if (!(modelMappings.get(variantName)?.model.attributes.some((attr) => attr.name === "map") ?? false)) stiVariantNames.add(variantName);
3300
+ const { modelNodes: mtiLinkedModelNodes, syntheticPkFieldsByVariant } = materializeMtiVariantStorageLinks(modelNodes, baseDeclarations, stiVariantNames);
3301
+ const { modelNodes: stiColumnModelNodes, stiBaseFieldsByBase } = materializeStiVariantStorageColumns(mtiLinkedModelNodes, baseDeclarations, stiVariantNames);
2932
3302
  const valueObjects = buildValueObjects({
2933
3303
  compositeTypes,
2934
3304
  enumTypeDescriptors: allEnumTypeDescriptors,
@@ -2951,7 +3321,7 @@ function interpretPslDocumentToSqlContract(input) {
2951
3321
  ...Object.keys(storageTypes).length > 0 ? { storageTypes } : {},
2952
3322
  ...Object.keys(namespaceEnumStorageTypes).length > 0 ? { namespaceTypes: namespaceEnumStorageTypes } : {},
2953
3323
  ...ifDefined("createNamespace", input.createNamespace),
2954
- models: modelNodes.map((model) => ({
3324
+ models: stiColumnModelNodes.map((model) => ({
2955
3325
  ...model,
2956
3326
  ...modelRelations.has(model.modelName) ? { relations: [...modelRelations.get(model.modelName) ?? []].sort((left, right) => compareStrings(left.fieldName, right.fieldName)) } : {}
2957
3327
  }))
@@ -2963,7 +3333,7 @@ function interpretPslDocumentToSqlContract(input) {
2963
3333
  }
2964
3334
  let patchedModels = patchModelDomainFields(modelsForPatch, modelResolvedFields);
2965
3335
  const polyDiagnostics = [];
2966
- patchedModels = resolvePolymorphism(patchedModels, discriminatorDeclarations, baseDeclarations, modelNames, modelMappings, modelNamespaceIds, input.target.targetId, sourceId, polyDiagnostics);
3336
+ patchedModels = resolvePolymorphism(patchedModels, discriminatorDeclarations, baseDeclarations, modelNames, modelMappings, modelNamespaceIds, input.target.defaultNamespaceId, syntheticPkFieldsByVariant, stiBaseFieldsByBase, sourceId, polyDiagnostics);
2967
3337
  if (polyDiagnostics.length > 0) return notOk({
2968
3338
  summary: "PSL to SQL contract interpretation failed",
2969
3339
  diagnostics: polyDiagnostics
@@ -2976,11 +3346,11 @@ function interpretPslDocumentToSqlContract(input) {
2976
3346
  domain: { namespaces: Object.fromEntries(Object.entries(contract.domain.namespaces).map(([namespaceId, namespaceSlice]) => [namespaceId, {
2977
3347
  models: Object.fromEntries(Object.entries(namespaceSlice.models).map(([modelName, model]) => [modelName, patchedModels[modelName] ?? model])),
2978
3348
  ...namespaceSlice.valueObjects !== void 0 ? { valueObjects: namespaceSlice.valueObjects } : {},
2979
- ...namespaceId === defaultSqlNamespaceIdForTarget(input.target.targetId) && Object.keys(valueObjects).length > 0 ? { valueObjects } : {}
3349
+ ...namespaceId === input.target.defaultNamespaceId && Object.keys(valueObjects).length > 0 ? { valueObjects } : {}
2980
3350
  }])) }
2981
3351
  });
2982
3352
  }
2983
3353
  //#endregion
2984
3354
  export { interpretPslDocumentToSqlContract as t };
2985
3355
 
2986
- //# sourceMappingURL=interpreter-QE6eZZof.mjs.map
3356
+ //# sourceMappingURL=interpreter-B5_yovSP.mjs.map