@prisma-next/sql-contract-psl 0.12.0 → 0.13.0-dev.2

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.
@@ -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
  }
@@ -1258,6 +1308,10 @@ function resolveColumnDescriptor(field, enumTypeDescriptors, namedTypeDescriptor
1258
1308
  }
1259
1309
  //#endregion
1260
1310
  //#region src/psl-field-resolution.ts
1311
+ const MODEL_COORDINATE_SEPARATOR = "\0";
1312
+ function modelCoordinateKey(namespaceId, modelName) {
1313
+ return `${namespaceId}${MODEL_COORDINATE_SEPARATOR}${modelName}`;
1314
+ }
1261
1315
  const BUILTIN_FIELD_ATTRIBUTE_NAMES = new Set([
1262
1316
  "id",
1263
1317
  "unique",
@@ -1344,6 +1398,7 @@ function collectResolvedFields(input) {
1344
1398
  });
1345
1399
  const relationAttribute = getAttribute(field.attributes, "relation");
1346
1400
  if (isModelField && relationAttribute) continue;
1401
+ if (field.typeContractSpaceId !== void 0 && relationAttribute) continue;
1347
1402
  const isValueObjectField = compositeTypeNames.has(field.typeName);
1348
1403
  const isListField = field.list;
1349
1404
  let descriptor;
@@ -1489,9 +1544,9 @@ function collectResolvedFields(input) {
1489
1544
  }
1490
1545
  return resolvedFields;
1491
1546
  }
1492
- function buildModelMappings(models, diagnostics, sourceId) {
1547
+ function buildModelMappings(modelEntries, defaultNamespaceId, diagnostics, sourceId) {
1493
1548
  const result = /* @__PURE__ */ new Map();
1494
- for (const model of models) {
1549
+ for (const { model, namespaceId } of modelEntries) {
1495
1550
  const tableName = parseMapName({
1496
1551
  attribute: getAttribute(model.attributes, "map"),
1497
1552
  defaultValue: lowerFirst(model.name),
@@ -1512,7 +1567,7 @@ function buildModelMappings(models, diagnostics, sourceId) {
1512
1567
  });
1513
1568
  fieldColumns.set(field.name, columnName);
1514
1569
  }
1515
- result.set(model.name, {
1570
+ result.set(modelCoordinateKey(namespaceId ?? defaultNamespaceId, model.name), {
1516
1571
  model,
1517
1572
  tableName,
1518
1573
  fieldColumns
@@ -1666,6 +1721,7 @@ function indexFkRelations(input) {
1666
1721
  fieldName: relation.declaringFieldName,
1667
1722
  toModel: relation.targetModelName,
1668
1723
  toTable: relation.targetTableName,
1724
+ ...ifDefined("toNamespaceId", relation.targetNamespaceId),
1669
1725
  cardinality: "N:1",
1670
1726
  on: {
1671
1727
  parentTable: relation.declaringTableName,
@@ -1852,9 +1908,6 @@ const UNSPECIFIED_PSL_NAMESPACE_NAME = "__unspecified__";
1852
1908
  * slot empty (which means the late-bound default at the `StorageTable`
1853
1909
  * layer; emitted JSON omits the field).
1854
1910
  */
1855
- function defaultSqlNamespaceIdForTarget(targetId) {
1856
- return targetId === "postgres" ? "public" : UNBOUND_NAMESPACE_ID;
1857
- }
1858
1911
  function resolveNamespaceIdForSqlTarget(input) {
1859
1912
  if (input.targetId !== "postgres") return;
1860
1913
  if (input.bucketName === UNSPECIFIED_PSL_NAMESPACE_NAME) return "public";
@@ -2132,6 +2185,8 @@ function buildModelNodeFromPsl(input) {
2132
2185
  } : void 0;
2133
2186
  const hasInlinePrimaryKey = primaryKey !== void 0;
2134
2187
  let blockPrimaryKeyDeclared = false;
2188
+ let controlPolicyDeclared = false;
2189
+ let controlPolicy;
2135
2190
  const resultBackrelationCandidates = [];
2136
2191
  for (const field of model.fields) {
2137
2192
  if (!field.list || !input.modelNames.has(field.typeName)) continue;
@@ -2198,6 +2253,25 @@ function buildModelNodeFromPsl(input) {
2198
2253
  for (const modelAttribute of model.attributes) {
2199
2254
  if (modelAttribute.name === "map") continue;
2200
2255
  if (modelAttribute.name === "discriminator" || modelAttribute.name === "base") continue;
2256
+ if (modelAttribute.name === "control") {
2257
+ if (controlPolicyDeclared) {
2258
+ diagnostics.push({
2259
+ code: "PSL_DUPLICATE_ATTRIBUTE",
2260
+ message: `\`@@control\` declared more than once on model "${model.name}".`,
2261
+ sourceId,
2262
+ span: modelAttribute.span
2263
+ });
2264
+ continue;
2265
+ }
2266
+ controlPolicyDeclared = true;
2267
+ const parsed = parseControlPolicyAttribute({
2268
+ attribute: modelAttribute,
2269
+ sourceId,
2270
+ diagnostics
2271
+ });
2272
+ if (parsed !== void 0) controlPolicy = parsed;
2273
+ continue;
2274
+ }
2201
2275
  const attributeLabel = `Model "${model.name}" @@${modelAttribute.name}`;
2202
2276
  if (modelAttribute.name === "id") {
2203
2277
  if (blockPrimaryKeyDeclared) {
@@ -2383,29 +2457,155 @@ function buildModelNodeFromPsl(input) {
2383
2457
  });
2384
2458
  }
2385
2459
  const resultFkRelationMetadata = [];
2460
+ const resultCrossSpaceRelations = [];
2386
2461
  for (const relationAttribute of relationAttributes) {
2387
- if (relationAttribute.field.list) continue;
2388
- const { typeName: fieldTypeName, typeNamespaceId: fieldTypeNamespaceId } = relationAttribute.field;
2389
- const qualifiedTypeName = fieldTypeNamespaceId ? `${fieldTypeNamespaceId}.${fieldTypeName}` : fieldTypeName;
2390
- if (!input.modelNames.has(fieldTypeName)) {
2391
- diagnostics.push({
2392
- code: "PSL_INVALID_RELATION_TARGET",
2393
- message: `Relation field "${model.name}.${relationAttribute.field.name}" references unknown model "${qualifiedTypeName}"`,
2462
+ const { typeName: fieldTypeName, typeNamespaceId: fieldTypeNamespaceId, typeContractSpaceId: fieldTypeContractSpaceId } = relationAttribute.field;
2463
+ if (relationAttribute.field.list) {
2464
+ if (fieldTypeContractSpaceId !== void 0) diagnostics.push({
2465
+ code: "PSL_UNSUPPORTED_CROSS_SPACE_LIST",
2466
+ 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.`,
2394
2467
  sourceId,
2395
2468
  span: relationAttribute.field.span
2396
2469
  });
2397
2470
  continue;
2398
2471
  }
2399
- if (fieldTypeNamespaceId !== void 0) {
2400
- if (input.modelNamespaceIds.get(fieldTypeName) !== (fieldTypeNamespaceId === "unbound" ? "__unbound__" : fieldTypeNamespaceId)) {
2472
+ if (fieldTypeContractSpaceId !== void 0) {
2473
+ const extContractForSpace = input.composedExtensionContracts.get(fieldTypeContractSpaceId);
2474
+ if (extContractForSpace === void 0) {
2475
+ diagnostics.push({
2476
+ code: "PSL_UNKNOWN_CONTRACT_SPACE",
2477
+ 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.`,
2478
+ sourceId,
2479
+ span: relationAttribute.field.span,
2480
+ data: {
2481
+ space: fieldTypeContractSpaceId,
2482
+ suggestedPack: fieldTypeContractSpaceId
2483
+ }
2484
+ });
2485
+ continue;
2486
+ }
2487
+ const parsedRelation = parseRelationAttribute({
2488
+ attribute: relationAttribute.relation,
2489
+ modelName: model.name,
2490
+ fieldName: relationAttribute.field.name,
2491
+ sourceId,
2492
+ diagnostics
2493
+ });
2494
+ if (!parsedRelation) continue;
2495
+ if (!parsedRelation.fields || !parsedRelation.references) {
2496
+ diagnostics.push({
2497
+ code: "PSL_INVALID_RELATION_ATTRIBUTE",
2498
+ message: `Relation field "${model.name}.${relationAttribute.field.name}" requires fields and references arguments`,
2499
+ sourceId,
2500
+ span: relationAttribute.relation.span
2501
+ });
2502
+ continue;
2503
+ }
2504
+ const localColumns = mapFieldNamesToColumns({
2505
+ modelName: model.name,
2506
+ fieldNames: parsedRelation.fields,
2507
+ mapping,
2508
+ sourceId,
2509
+ diagnostics,
2510
+ span: relationAttribute.relation.span,
2511
+ entityLabel: `Relation field "${model.name}.${relationAttribute.field.name}"`
2512
+ });
2513
+ if (!localColumns) continue;
2514
+ const referencedColumns = parsedRelation.references;
2515
+ if (localColumns.length !== referencedColumns.length) {
2516
+ diagnostics.push({
2517
+ code: "PSL_INVALID_RELATION_ATTRIBUTE",
2518
+ message: `Relation field "${model.name}.${relationAttribute.field.name}" must provide the same number of fields and references`,
2519
+ sourceId,
2520
+ span: relationAttribute.relation.span
2521
+ });
2522
+ continue;
2523
+ }
2524
+ const onDelete = parsedRelation.onDelete ? normalizeReferentialAction({
2525
+ modelName: model.name,
2526
+ fieldName: relationAttribute.field.name,
2527
+ actionName: "onDelete",
2528
+ actionToken: parsedRelation.onDelete,
2529
+ sourceId,
2530
+ span: relationAttribute.field.span,
2531
+ diagnostics
2532
+ }) : void 0;
2533
+ const onUpdate = parsedRelation.onUpdate ? normalizeReferentialAction({
2534
+ modelName: model.name,
2535
+ fieldName: relationAttribute.field.name,
2536
+ actionName: "onUpdate",
2537
+ actionToken: parsedRelation.onUpdate,
2538
+ sourceId,
2539
+ span: relationAttribute.field.span,
2540
+ diagnostics
2541
+ }) : void 0;
2542
+ const crossTargetNamespaceId = fieldTypeNamespaceId ?? "__unbound__";
2543
+ const extContract = extContractForSpace;
2544
+ const resolvedTable = extContract.domain.namespaces[crossTargetNamespaceId]?.models[fieldTypeName]?.storage["table"];
2545
+ if (typeof resolvedTable !== "string") {
2546
+ const availableModels = Object.keys(extContract.domain.namespaces[crossTargetNamespaceId]?.models ?? {}).join(", ") || "(none)";
2401
2547
  diagnostics.push({
2402
- code: "PSL_INVALID_RELATION_TARGET",
2403
- message: `Relation field "${model.name}.${relationAttribute.field.name}" references unknown model "${qualifiedTypeName}"`,
2548
+ code: "PSL_UNKNOWN_CROSS_SPACE_TARGET",
2549
+ 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}`,
2404
2550
  sourceId,
2405
- span: relationAttribute.field.span
2551
+ span: relationAttribute.field.span,
2552
+ data: {
2553
+ space: fieldTypeContractSpaceId,
2554
+ namespace: crossTargetNamespaceId,
2555
+ model: fieldTypeName
2556
+ }
2406
2557
  });
2407
2558
  continue;
2408
2559
  }
2560
+ const crossTargetTableName = resolvedTable;
2561
+ foreignKeyNodes.push({
2562
+ columns: localColumns,
2563
+ references: {
2564
+ model: fieldTypeName,
2565
+ table: crossTargetTableName,
2566
+ columns: referencedColumns,
2567
+ namespaceId: crossTargetNamespaceId,
2568
+ spaceId: fieldTypeContractSpaceId
2569
+ },
2570
+ ...ifDefined("name", parsedRelation.constraintName),
2571
+ ...ifDefined("onDelete", onDelete),
2572
+ ...ifDefined("onUpdate", onUpdate)
2573
+ });
2574
+ resultCrossSpaceRelations.push({
2575
+ fieldName: relationAttribute.field.name,
2576
+ toModel: fieldTypeName,
2577
+ toTable: crossTargetTableName,
2578
+ cardinality: "N:1",
2579
+ spaceId: fieldTypeContractSpaceId,
2580
+ namespaceId: crossTargetNamespaceId,
2581
+ on: {
2582
+ parentTable: tableName,
2583
+ parentColumns: localColumns,
2584
+ childTable: crossTargetTableName,
2585
+ childColumns: referencedColumns
2586
+ }
2587
+ });
2588
+ continue;
2589
+ }
2590
+ const qualifiedTypeName = fieldTypeNamespaceId ? `${fieldTypeNamespaceId}.${fieldTypeName}` : fieldTypeName;
2591
+ if (!input.modelNames.has(fieldTypeName)) {
2592
+ diagnostics.push({
2593
+ code: "PSL_INVALID_RELATION_TARGET",
2594
+ message: `Relation field "${model.name}.${relationAttribute.field.name}" references unknown model "${qualifiedTypeName}"`,
2595
+ sourceId,
2596
+ span: relationAttribute.field.span
2597
+ });
2598
+ continue;
2599
+ }
2600
+ const normalizedQualifier = fieldTypeNamespaceId === void 0 ? void 0 : fieldTypeNamespaceId === "unbound" ? "__unbound__" : fieldTypeNamespaceId;
2601
+ if (normalizedQualifier !== void 0 && !input.modelMappingsByCoordinate.has(modelCoordinateKey(normalizedQualifier, fieldTypeName))) {
2602
+ diagnostics.push({
2603
+ code: "PSL_INVALID_RELATION_TARGET",
2604
+ message: `Relation field "${model.name}.${relationAttribute.field.name}" references unknown model "${qualifiedTypeName}"`,
2605
+ sourceId,
2606
+ span: relationAttribute.field.span
2607
+ });
2608
+ continue;
2409
2609
  }
2410
2610
  const parsedRelation = parseRelationAttribute({
2411
2611
  attribute: relationAttribute.relation,
@@ -2424,7 +2624,7 @@ function buildModelNodeFromPsl(input) {
2424
2624
  });
2425
2625
  continue;
2426
2626
  }
2427
- const targetMapping = input.modelMappings.get(fieldTypeName);
2627
+ const targetMapping = normalizedQualifier !== void 0 ? input.modelMappingsByCoordinate.get(modelCoordinateKey(normalizedQualifier, fieldTypeName)) : input.modelMappings.get(fieldTypeName);
2428
2628
  if (!targetMapping) {
2429
2629
  diagnostics.push({
2430
2630
  code: "PSL_INVALID_RELATION_TARGET",
@@ -2481,7 +2681,7 @@ function buildModelNodeFromPsl(input) {
2481
2681
  span: relationAttribute.field.span,
2482
2682
  diagnostics
2483
2683
  }) : void 0;
2484
- const targetNamespaceId = input.modelNamespaceIds.get(targetMapping.model.name);
2684
+ const targetNamespaceId = normalizedQualifier !== void 0 ? normalizedQualifier : input.modelNamespaceIds.get(targetMapping.model.name);
2485
2685
  foreignKeyNodes.push({
2486
2686
  columns: localColumns,
2487
2687
  references: {
@@ -2500,6 +2700,7 @@ function buildModelNodeFromPsl(input) {
2500
2700
  declaringTableName: tableName,
2501
2701
  targetModelName: targetMapping.model.name,
2502
2702
  targetTableName: targetMapping.tableName,
2703
+ ...ifDefined("targetNamespaceId", targetNamespaceId),
2503
2704
  ...ifDefined("relationName", parsedRelation.relationName),
2504
2705
  localColumns,
2505
2706
  referencedColumns
@@ -2520,9 +2721,11 @@ function buildModelNodeFromPsl(input) {
2520
2721
  ...ifDefined("id", primaryKey),
2521
2722
  ...uniqueConstraints.length > 0 ? { uniques: uniqueConstraints } : {},
2522
2723
  ...indexNodes.length > 0 ? { indexes: indexNodes } : {},
2523
- ...foreignKeyNodes.length > 0 ? { foreignKeys: foreignKeyNodes } : {}
2724
+ ...foreignKeyNodes.length > 0 ? { foreignKeys: foreignKeyNodes } : {},
2725
+ ...ifDefined("control", controlPolicy)
2524
2726
  },
2525
2727
  fkRelationMetadata: resultFkRelationMetadata,
2728
+ crossSpaceRelations: resultCrossSpaceRelations,
2526
2729
  backrelationCandidates: resultBackrelationCandidates,
2527
2730
  resolvedFields
2528
2731
  };
@@ -2688,8 +2891,18 @@ function collectPolymorphismDeclarations(models, sourceId, diagnostics) {
2688
2891
  baseDeclarations
2689
2892
  };
2690
2893
  }
2691
- function resolvePolymorphism(models, discriminatorDeclarations, baseDeclarations, modelNames, modelMappings, modelNamespaceIds, targetId, sourceId, diagnostics) {
2894
+ function resolvePolymorphism(models, discriminatorDeclarations, baseDeclarations, modelNames, modelMappings, modelNamespaceIds, defaultNamespaceId, syntheticPkFieldsByVariant, stiBaseFieldsByBase, sourceId, diagnostics) {
2692
2895
  let patched = models;
2896
+ const coordinateFor = (modelName) => modelCoordinateKey(modelNamespaceIds.get(modelName) ?? defaultNamespaceId, modelName);
2897
+ for (const [baseName, fieldNames] of stiBaseFieldsByBase) {
2898
+ const baseKey = coordinateFor(baseName);
2899
+ const baseModel = patched[baseKey];
2900
+ if (!baseModel || fieldNames.length === 0) continue;
2901
+ patched = {
2902
+ ...patched,
2903
+ [baseKey]: stripStorageOnlyDomainFields(baseModel, fieldNames)
2904
+ };
2905
+ }
2693
2906
  for (const [modelName, decl] of discriminatorDeclarations) {
2694
2907
  if (baseDeclarations.has(modelName)) {
2695
2908
  diagnostics.push({
@@ -2700,7 +2913,7 @@ function resolvePolymorphism(models, discriminatorDeclarations, baseDeclarations
2700
2913
  });
2701
2914
  continue;
2702
2915
  }
2703
- const model = patched[modelName];
2916
+ const model = patched[coordinateFor(modelName)];
2704
2917
  if (!model) continue;
2705
2918
  if (!Object.hasOwn(model.fields, decl.fieldName)) {
2706
2919
  diagnostics.push({
@@ -2739,7 +2952,7 @@ function resolvePolymorphism(models, discriminatorDeclarations, baseDeclarations
2739
2952
  }
2740
2953
  patched = {
2741
2954
  ...patched,
2742
- [modelName]: {
2955
+ [coordinateFor(modelName)]: {
2743
2956
  ...model,
2744
2957
  discriminator: { field: decl.fieldName },
2745
2958
  variants
@@ -2766,25 +2979,173 @@ function resolvePolymorphism(models, discriminatorDeclarations, baseDeclarations
2766
2979
  continue;
2767
2980
  }
2768
2981
  if (discriminatorDeclarations.has(variantName)) continue;
2769
- const variantModel = patched[variantName];
2982
+ const variantModel = patched[coordinateFor(variantName)];
2770
2983
  if (!variantModel) continue;
2771
2984
  const baseMapping = modelMappings.get(baseDecl.baseName);
2772
2985
  const variantMapping = modelMappings.get(variantName);
2773
2986
  const resolvedTable = variantMapping?.model.attributes.some((attr) => attr.name === "map") ?? false ? variantMapping?.tableName : baseMapping?.tableName;
2987
+ const patchedVariant = {
2988
+ ...variantModel,
2989
+ base: crossRef(baseDecl.baseName, modelNamespaceIds.get(baseDecl.baseName) ?? defaultNamespaceId),
2990
+ ...resolvedTable ? { storage: {
2991
+ ...variantModel.storage,
2992
+ table: resolvedTable
2993
+ } } : {}
2994
+ };
2774
2995
  patched = {
2775
2996
  ...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
- }
2997
+ [coordinateFor(variantName)]: stripStorageOnlyDomainFields(patchedVariant, syntheticPkFieldsByVariant.get(variantName) ?? [])
2784
2998
  };
2785
2999
  }
2786
3000
  return patched;
2787
3001
  }
3002
+ /**
3003
+ * Multi-table-inheritance variants (`@@base` + their own `@@map`) live in a
3004
+ * separate table from their base. The ORM joins that table to the base on the
3005
+ * shared primary key (`base.id = variant.id`), so the variant storage table
3006
+ * must carry the base PK column even though the variant domain model declares
3007
+ * only its own fields. This enriches each MTI variant's `ModelNode` with that
3008
+ * link column, a primary key on it, and a FK back to the base table.
3009
+ *
3010
+ * The link column is reported back per variant in `syntheticPkFieldsByVariant`
3011
+ * so the domain-model patch can drop it again — keeping the variant's domain
3012
+ * surface thin (its create/read inputs don't gain a redundant `id`) while the
3013
+ * storage table stays joinable. Single-table-inheritance variants (no own
3014
+ * table) are left untouched.
3015
+ */
3016
+ function materializeMtiVariantStorageLinks(modelNodes, baseDeclarations, stiVariantNames) {
3017
+ const nodeByModel = new Map(modelNodes.map((node) => [node.modelName, node]));
3018
+ const syntheticPkFieldsByVariant = /* @__PURE__ */ new Map();
3019
+ return {
3020
+ modelNodes: modelNodes.map((node) => {
3021
+ const baseDecl = baseDeclarations.get(node.modelName);
3022
+ if (!baseDecl) return node;
3023
+ const baseNode = nodeByModel.get(baseDecl.baseName);
3024
+ if (!baseNode) return node;
3025
+ if (stiVariantNames.has(node.modelName)) return node;
3026
+ const basePrimaryKey = baseNode.id;
3027
+ if (!basePrimaryKey || basePrimaryKey.columns.length === 0) return node;
3028
+ const existingColumns = new Set(node.fields.map((field) => field.columnName));
3029
+ const linkFields = [];
3030
+ for (const pkColumn of basePrimaryKey.columns) {
3031
+ if (existingColumns.has(pkColumn)) continue;
3032
+ const baseField = baseNode.fields.find((field) => "descriptor" in field && field.columnName === pkColumn);
3033
+ if (!baseField) continue;
3034
+ linkFields.push({
3035
+ fieldName: baseField.fieldName,
3036
+ columnName: pkColumn,
3037
+ descriptor: baseField.descriptor,
3038
+ nullable: false
3039
+ });
3040
+ }
3041
+ if (linkFields.length === 0) return node;
3042
+ syntheticPkFieldsByVariant.set(node.modelName, linkFields.map((field) => field.fieldName));
3043
+ const foreignKey = {
3044
+ columns: basePrimaryKey.columns,
3045
+ references: {
3046
+ model: baseNode.modelName,
3047
+ table: baseNode.tableName,
3048
+ columns: basePrimaryKey.columns,
3049
+ ...ifDefined("namespaceId", baseNode.namespaceId)
3050
+ },
3051
+ constraint: true,
3052
+ index: false,
3053
+ onDelete: "cascade"
3054
+ };
3055
+ return {
3056
+ ...node,
3057
+ fields: [...linkFields, ...node.fields],
3058
+ id: { columns: basePrimaryKey.columns },
3059
+ foreignKeys: [...node.foreignKeys ?? [], foreignKey]
3060
+ };
3061
+ }),
3062
+ syntheticPkFieldsByVariant
3063
+ };
3064
+ }
3065
+ /**
3066
+ * Single-table-inheritance variants (`@@base` with no own `@@map`) share the
3067
+ * base table: `resolvePolymorphism` points the variant's `storage.table` at the
3068
+ * base, and the ORM reads variant-declared fields straight off the base table.
3069
+ * For that to validate and round-trip, the base storage table must physically
3070
+ * carry every STI variant's declared columns. This enriches the base
3071
+ * `ModelNode` with those columns.
3072
+ *
3073
+ * The materialised columns are always nullable in storage: the base table hosts
3074
+ * every variant's rows, so a column a variant declares as required is still
3075
+ * NULL on sibling-variant rows. The variant's domain field keeps its declared
3076
+ * nullability — required-in-domain / nullable-in-storage is the intended STI
3077
+ * shape.
3078
+ *
3079
+ * Collisions (two variants declaring the same column, or a variant column name
3080
+ * clashing with a base column) are resolved skip-if-exists here, mirroring the
3081
+ * MTI link guard; surfacing them as diagnostics is tracked separately
3082
+ * (TML-2827).
3083
+ */
3084
+ function materializeStiVariantStorageColumns(modelNodes, baseDeclarations, stiVariantNames) {
3085
+ if (stiVariantNames.size === 0) return {
3086
+ modelNodes: [...modelNodes],
3087
+ stiBaseFieldsByBase: /* @__PURE__ */ new Map()
3088
+ };
3089
+ const nodeByModel = new Map(modelNodes.map((node) => [node.modelName, node]));
3090
+ const stiColumnsByBase = /* @__PURE__ */ new Map();
3091
+ for (const variantName of stiVariantNames) {
3092
+ const variantNode = nodeByModel.get(variantName);
3093
+ const baseDecl = baseDeclarations.get(variantName);
3094
+ if (!variantNode || !baseDecl) continue;
3095
+ const baseNode = nodeByModel.get(baseDecl.baseName);
3096
+ if (!baseNode) continue;
3097
+ const baseColumns = new Set(baseNode.fields.map((field) => field.columnName));
3098
+ const claimed = stiColumnsByBase.get(baseDecl.baseName) ?? [];
3099
+ const claimedColumns = new Set(claimed.map((field) => field.columnName));
3100
+ for (const field of variantNode.fields) {
3101
+ if (baseColumns.has(field.columnName) || claimedColumns.has(field.columnName)) continue;
3102
+ claimedColumns.add(field.columnName);
3103
+ claimed.push({
3104
+ ...field,
3105
+ nullable: true
3106
+ });
3107
+ }
3108
+ stiColumnsByBase.set(baseDecl.baseName, claimed);
3109
+ }
3110
+ const stiBaseFieldsByBase = /* @__PURE__ */ new Map();
3111
+ for (const [baseName, columns] of stiColumnsByBase) stiBaseFieldsByBase.set(baseName, columns.map((field) => field.fieldName));
3112
+ return {
3113
+ modelNodes: modelNodes.map((node) => {
3114
+ if (stiVariantNames.has(node.modelName)) return {
3115
+ ...node,
3116
+ sharesBaseTable: true
3117
+ };
3118
+ const stiColumns = stiColumnsByBase.get(node.modelName);
3119
+ if (!stiColumns || stiColumns.length === 0) return node;
3120
+ return {
3121
+ ...node,
3122
+ fields: [...node.fields, ...stiColumns]
3123
+ };
3124
+ }),
3125
+ stiBaseFieldsByBase
3126
+ };
3127
+ }
3128
+ /**
3129
+ * Drop the storage-only link fields (added by
3130
+ * {@link materializeMtiVariantStorageLinks}) from a variant's domain model, so
3131
+ * the domain surface stays thin while the storage table keeps the link column.
3132
+ */
3133
+ function stripStorageOnlyDomainFields(model, fieldNames) {
3134
+ if (fieldNames.length === 0) return model;
3135
+ const fields = { ...model.fields };
3136
+ for (const name of fieldNames) delete fields[name];
3137
+ const storage = blindCast(model.storage);
3138
+ const storageFields = { ...storage.fields };
3139
+ for (const name of fieldNames) delete storageFields[name];
3140
+ return {
3141
+ ...model,
3142
+ fields,
3143
+ storage: {
3144
+ ...storage,
3145
+ fields: storageFields
3146
+ }
3147
+ };
3148
+ }
2788
3149
  function interpretPslDocumentToSqlContract(input) {
2789
3150
  const sourceId = input.document.ast.sourceId;
2790
3151
  if (!input.target) return notOk({
@@ -2811,6 +3172,7 @@ function interpretPslDocumentToSqlContract(input) {
2811
3172
  diagnostics
2812
3173
  });
2813
3174
  const models = [];
3175
+ const modelEntries = [];
2814
3176
  const modelNamespaceIds = /* @__PURE__ */ new Map();
2815
3177
  for (const namespace of input.document.ast.namespaces) {
2816
3178
  const resolvedNamespaceId = resolveNamespaceIdForSqlTarget({
@@ -2819,9 +3181,14 @@ function interpretPslDocumentToSqlContract(input) {
2819
3181
  });
2820
3182
  for (const model of namespace.models) {
2821
3183
  models.push(model);
3184
+ modelEntries.push({
3185
+ model,
3186
+ namespaceId: resolvedNamespaceId
3187
+ });
2822
3188
  if (resolvedNamespaceId !== void 0) modelNamespaceIds.set(model.name, resolvedNamespaceId);
2823
3189
  }
2824
3190
  }
3191
+ const defaultNamespaceId = input.target.defaultNamespaceId;
2825
3192
  const topLevelEnums = input.document.ast.namespaces.filter((ns) => ns.name === UNSPECIFIED_PSL_NAMESPACE_NAME).flatMap((ns) => ns.enums);
2826
3193
  const namedNamespaceEnumsByNsId = /* @__PURE__ */ new Map();
2827
3194
  for (const ns of input.document.ast.namespaces) {
@@ -2838,6 +3205,7 @@ function interpretPslDocumentToSqlContract(input) {
2838
3205
  const modelNames = new Set(models.map((model) => model.name));
2839
3206
  const compositeTypeNames = new Set(compositeTypes.map((ct) => ct.name));
2840
3207
  const composedExtensions = new Set(input.composedExtensionPacks ?? []);
3208
+ const composedExtensionContracts = input.composedExtensionContracts;
2841
3209
  const defaultFunctionRegistry = input.controlMutationDefaults?.defaultFunctionRegistry ?? /* @__PURE__ */ new Map();
2842
3210
  const generatorDescriptors = input.controlMutationDefaults?.generatorDescriptors ?? [];
2843
3211
  const generatorDescriptorById = /* @__PURE__ */ new Map();
@@ -2884,23 +3252,29 @@ function interpretPslDocumentToSqlContract(input) {
2884
3252
  ...enumResult.storageTypes,
2885
3253
  ...namedTypeResult.storageTypes
2886
3254
  };
2887
- const modelMappings = buildModelMappings(models, diagnostics, sourceId);
3255
+ const modelMappingsByCoordinate = buildModelMappings(modelEntries, defaultNamespaceId, diagnostics, sourceId);
3256
+ const modelMappings = /* @__PURE__ */ new Map();
3257
+ for (const mapping of modelMappingsByCoordinate.values()) modelMappings.set(mapping.model.name, mapping);
2888
3258
  const modelNodes = [];
2889
3259
  const fkRelationMetadata = [];
2890
3260
  const backrelationCandidates = [];
2891
3261
  const modelResolvedFields = /* @__PURE__ */ new Map();
2892
- for (const model of models) {
2893
- const mapping = modelMappings.get(model.name);
3262
+ const crossSpaceRelationsByModel = /* @__PURE__ */ new Map();
3263
+ for (const { model, namespaceId } of modelEntries) {
3264
+ const coordinate = modelCoordinateKey(namespaceId ?? defaultNamespaceId, model.name);
3265
+ const mapping = modelMappingsByCoordinate.get(coordinate);
2894
3266
  if (!mapping) continue;
2895
3267
  const result = buildModelNodeFromPsl({
2896
3268
  model,
2897
3269
  mapping,
2898
3270
  modelMappings,
3271
+ modelMappingsByCoordinate,
2899
3272
  modelNames,
2900
3273
  compositeTypeNames,
2901
3274
  enumTypeDescriptors: allEnumTypeDescriptors,
2902
3275
  namedTypeDescriptors: namedTypeResult.namedTypeDescriptors,
2903
3276
  composedExtensions,
3277
+ composedExtensionContracts,
2904
3278
  familyId: input.target.familyId,
2905
3279
  targetId: input.target.targetId,
2906
3280
  authoringContributions: input.authoringContributions,
@@ -2911,14 +3285,17 @@ function interpretPslDocumentToSqlContract(input) {
2911
3285
  diagnostics,
2912
3286
  modelNamespaceIds
2913
3287
  });
2914
- const resolvedNamespaceId = modelNamespaceIds.get(model.name);
2915
- modelNodes.push(resolvedNamespaceId !== void 0 ? {
3288
+ modelNodes.push(namespaceId !== void 0 ? {
2916
3289
  ...result.modelNode,
2917
- namespaceId: resolvedNamespaceId
3290
+ namespaceId
2918
3291
  } : result.modelNode);
2919
3292
  fkRelationMetadata.push(...result.fkRelationMetadata);
2920
3293
  backrelationCandidates.push(...result.backrelationCandidates);
2921
- modelResolvedFields.set(model.name, result.resolvedFields);
3294
+ modelResolvedFields.set(coordinate, result.resolvedFields);
3295
+ if (result.crossSpaceRelations.length > 0) {
3296
+ const existing = crossSpaceRelationsByModel.get(model.name) ?? [];
3297
+ crossSpaceRelationsByModel.set(model.name, [...existing, ...result.crossSpaceRelations]);
3298
+ }
2922
3299
  }
2923
3300
  const { modelRelations, fkRelationsByPair } = indexFkRelations({ fkRelationMetadata });
2924
3301
  applyBackrelationCandidates({
@@ -2928,7 +3305,16 @@ function interpretPslDocumentToSqlContract(input) {
2928
3305
  diagnostics,
2929
3306
  sourceId
2930
3307
  });
3308
+ for (const [modelName, relations] of crossSpaceRelationsByModel) {
3309
+ const existing = modelRelations.get(modelName);
3310
+ if (existing) existing.push(...relations);
3311
+ else modelRelations.set(modelName, [...relations]);
3312
+ }
2931
3313
  const { discriminatorDeclarations, baseDeclarations } = collectPolymorphismDeclarations(models, sourceId, diagnostics);
3314
+ const stiVariantNames = /* @__PURE__ */ new Set();
3315
+ for (const variantName of baseDeclarations.keys()) if (!(modelMappings.get(variantName)?.model.attributes.some((attr) => attr.name === "map") ?? false)) stiVariantNames.add(variantName);
3316
+ const { modelNodes: mtiLinkedModelNodes, syntheticPkFieldsByVariant } = materializeMtiVariantStorageLinks(modelNodes, baseDeclarations, stiVariantNames);
3317
+ const { modelNodes: stiColumnModelNodes, stiBaseFieldsByBase } = materializeStiVariantStorageColumns(mtiLinkedModelNodes, baseDeclarations, stiVariantNames);
2932
3318
  const valueObjects = buildValueObjects({
2933
3319
  compositeTypes,
2934
3320
  enumTypeDescriptors: allEnumTypeDescriptors,
@@ -2951,19 +3337,20 @@ function interpretPslDocumentToSqlContract(input) {
2951
3337
  ...Object.keys(storageTypes).length > 0 ? { storageTypes } : {},
2952
3338
  ...Object.keys(namespaceEnumStorageTypes).length > 0 ? { namespaceTypes: namespaceEnumStorageTypes } : {},
2953
3339
  ...ifDefined("createNamespace", input.createNamespace),
2954
- models: modelNodes.map((model) => ({
3340
+ models: stiColumnModelNodes.map((model) => ({
2955
3341
  ...model,
2956
3342
  ...modelRelations.has(model.modelName) ? { relations: [...modelRelations.get(model.modelName) ?? []].sort((left, right) => compareStrings(left.fieldName, right.fieldName)) } : {}
2957
3343
  }))
2958
3344
  });
2959
3345
  const modelsForPatch = {};
2960
- for (const namespaceSlice of Object.values(contract.domain.namespaces)) for (const [modelName, model] of Object.entries(namespaceSlice.models)) {
2961
- if (Object.hasOwn(modelsForPatch, modelName)) throw new Error(`duplicate model name "${modelName}" across domain namespaces during PSL interpretation`);
2962
- modelsForPatch[modelName] = model;
3346
+ for (const [namespaceId, namespaceSlice] of Object.entries(contract.domain.namespaces)) for (const [modelName, model] of Object.entries(namespaceSlice.models)) {
3347
+ const coordinate = modelCoordinateKey(namespaceId, modelName);
3348
+ if (Object.hasOwn(modelsForPatch, coordinate)) throw new Error(`duplicate model "${namespaceId}.${modelName}" during PSL interpretation`);
3349
+ modelsForPatch[coordinate] = model;
2963
3350
  }
2964
3351
  let patchedModels = patchModelDomainFields(modelsForPatch, modelResolvedFields);
2965
3352
  const polyDiagnostics = [];
2966
- patchedModels = resolvePolymorphism(patchedModels, discriminatorDeclarations, baseDeclarations, modelNames, modelMappings, modelNamespaceIds, input.target.targetId, sourceId, polyDiagnostics);
3353
+ patchedModels = resolvePolymorphism(patchedModels, discriminatorDeclarations, baseDeclarations, modelNames, modelMappings, modelNamespaceIds, input.target.defaultNamespaceId, syntheticPkFieldsByVariant, stiBaseFieldsByBase, sourceId, polyDiagnostics);
2967
3354
  if (polyDiagnostics.length > 0) return notOk({
2968
3355
  summary: "PSL to SQL contract interpretation failed",
2969
3356
  diagnostics: polyDiagnostics
@@ -2974,13 +3361,13 @@ function interpretPslDocumentToSqlContract(input) {
2974
3361
  ...contract,
2975
3362
  roots: filteredRoots,
2976
3363
  domain: { namespaces: Object.fromEntries(Object.entries(contract.domain.namespaces).map(([namespaceId, namespaceSlice]) => [namespaceId, {
2977
- models: Object.fromEntries(Object.entries(namespaceSlice.models).map(([modelName, model]) => [modelName, patchedModels[modelName] ?? model])),
3364
+ models: Object.fromEntries(Object.entries(namespaceSlice.models).map(([modelName, model]) => [modelName, patchedModels[modelCoordinateKey(namespaceId, modelName)] ?? model])),
2978
3365
  ...namespaceSlice.valueObjects !== void 0 ? { valueObjects: namespaceSlice.valueObjects } : {},
2979
- ...namespaceId === defaultSqlNamespaceIdForTarget(input.target.targetId) && Object.keys(valueObjects).length > 0 ? { valueObjects } : {}
3366
+ ...namespaceId === input.target.defaultNamespaceId && Object.keys(valueObjects).length > 0 ? { valueObjects } : {}
2980
3367
  }])) }
2981
3368
  });
2982
3369
  }
2983
3370
  //#endregion
2984
3371
  export { interpretPslDocumentToSqlContract as t };
2985
3372
 
2986
- //# sourceMappingURL=interpreter-QE6eZZof.mjs.map
3373
+ //# sourceMappingURL=interpreter-B_KtZusL.mjs.map