@prisma-next/sql-contract-psl 0.12.0-dev.78 → 0.12.0-dev.79

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/dist/provider.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as interpretPslDocumentToSqlContract } from "./interpreter-B5_yovSP.mjs";
1
+ import { t as interpretPslDocumentToSqlContract } from "./interpreter-B_KtZusL.mjs";
2
2
  import { ifDefined } from "@prisma-next/utils/defined";
3
3
  import { notOk, ok } from "@prisma-next/utils/result";
4
4
  import { parsePslDocument } from "@prisma-next/psl-parser";
package/package.json CHANGED
@@ -1,25 +1,25 @@
1
1
  {
2
2
  "name": "@prisma-next/sql-contract-psl",
3
- "version": "0.12.0-dev.78",
3
+ "version": "0.12.0-dev.79",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "description": "PSL-to-SQL ContractIR interpreter for Prisma Next",
8
8
  "dependencies": {
9
- "@prisma-next/config": "0.12.0-dev.78",
10
- "@prisma-next/contract": "0.12.0-dev.78",
11
- "@prisma-next/framework-components": "0.12.0-dev.78",
12
- "@prisma-next/psl-parser": "0.12.0-dev.78",
13
- "@prisma-next/sql-contract": "0.12.0-dev.78",
14
- "@prisma-next/sql-contract-ts": "0.12.0-dev.78",
15
- "@prisma-next/utils": "0.12.0-dev.78",
9
+ "@prisma-next/config": "0.12.0-dev.79",
10
+ "@prisma-next/contract": "0.12.0-dev.79",
11
+ "@prisma-next/framework-components": "0.12.0-dev.79",
12
+ "@prisma-next/psl-parser": "0.12.0-dev.79",
13
+ "@prisma-next/sql-contract": "0.12.0-dev.79",
14
+ "@prisma-next/sql-contract-ts": "0.12.0-dev.79",
15
+ "@prisma-next/utils": "0.12.0-dev.79",
16
16
  "pathe": "^2.0.3"
17
17
  },
18
18
  "devDependencies": {
19
- "@prisma-next/contract-authoring": "0.12.0-dev.78",
20
- "@prisma-next/test-utils": "0.12.0-dev.78",
21
- "@prisma-next/tsconfig": "0.12.0-dev.78",
22
- "@prisma-next/tsdown": "0.12.0-dev.78",
19
+ "@prisma-next/contract-authoring": "0.12.0-dev.79",
20
+ "@prisma-next/test-utils": "0.12.0-dev.79",
21
+ "@prisma-next/tsconfig": "0.12.0-dev.79",
22
+ "@prisma-next/tsdown": "0.12.0-dev.79",
23
23
  "arktype": "^2.2.0",
24
24
  "tsdown": "0.22.1",
25
25
  "typescript": "5.9.3",
@@ -82,6 +82,8 @@ import {
82
82
  buildModelMappings,
83
83
  collectResolvedFields,
84
84
  type ModelNameMapping,
85
+ type ModelNamespaceEntry,
86
+ modelCoordinateKey,
85
87
  type ResolvedField,
86
88
  } from './psl-field-resolution';
87
89
  import {
@@ -594,6 +596,12 @@ interface BuildModelNodeInput {
594
596
  readonly model: PslModel;
595
597
  readonly mapping: ModelNameMapping;
596
598
  readonly modelMappings: ReadonlyMap<string, ModelNameMapping>;
599
+ /**
600
+ * Model mappings keyed by `(namespaceId, modelName)` coordinate. Used to
601
+ * resolve a namespace-qualified relation target (`auth.User`) to the exact
602
+ * model even when the bare name is shared across namespaces.
603
+ */
604
+ readonly modelMappingsByCoordinate: ReadonlyMap<string, ModelNameMapping>;
597
605
  readonly modelNames: Set<string>;
598
606
  readonly compositeTypeNames: ReadonlySet<string>;
599
607
  readonly enumTypeDescriptors: Map<string, ColumnDescriptor>;
@@ -1166,19 +1174,23 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
1166
1174
  continue;
1167
1175
  }
1168
1176
 
1169
- if (fieldTypeNamespaceId !== undefined) {
1170
- const resolvedTargetNamespaceId = input.modelNamespaceIds.get(fieldTypeName);
1171
- const normalizedQualifier =
1172
- fieldTypeNamespaceId === 'unbound' ? '__unbound__' : fieldTypeNamespaceId;
1173
- if (resolvedTargetNamespaceId !== normalizedQualifier) {
1174
- diagnostics.push({
1175
- code: 'PSL_INVALID_RELATION_TARGET',
1176
- message: `Relation field "${model.name}.${relationAttribute.field.name}" references unknown model "${qualifiedTypeName}"`,
1177
- sourceId,
1178
- span: relationAttribute.field.span,
1179
- });
1180
- continue;
1181
- }
1177
+ const normalizedQualifier =
1178
+ fieldTypeNamespaceId === undefined
1179
+ ? undefined
1180
+ : fieldTypeNamespaceId === 'unbound'
1181
+ ? '__unbound__'
1182
+ : fieldTypeNamespaceId;
1183
+ if (
1184
+ normalizedQualifier !== undefined &&
1185
+ !input.modelMappingsByCoordinate.has(modelCoordinateKey(normalizedQualifier, fieldTypeName))
1186
+ ) {
1187
+ diagnostics.push({
1188
+ code: 'PSL_INVALID_RELATION_TARGET',
1189
+ message: `Relation field "${model.name}.${relationAttribute.field.name}" references unknown model "${qualifiedTypeName}"`,
1190
+ sourceId,
1191
+ span: relationAttribute.field.span,
1192
+ });
1193
+ continue;
1182
1194
  }
1183
1195
 
1184
1196
  const parsedRelation = parseRelationAttribute({
@@ -1201,7 +1213,12 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
1201
1213
  continue;
1202
1214
  }
1203
1215
 
1204
- const targetMapping = input.modelMappings.get(fieldTypeName);
1216
+ const targetMapping =
1217
+ normalizedQualifier !== undefined
1218
+ ? input.modelMappingsByCoordinate.get(
1219
+ modelCoordinateKey(normalizedQualifier, fieldTypeName),
1220
+ )
1221
+ : input.modelMappings.get(fieldTypeName);
1205
1222
  if (!targetMapping) {
1206
1223
  diagnostics.push({
1207
1224
  code: 'PSL_INVALID_RELATION_TARGET',
@@ -1269,7 +1286,10 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
1269
1286
  })
1270
1287
  : undefined;
1271
1288
 
1272
- const targetNamespaceId = input.modelNamespaceIds.get(targetMapping.model.name);
1289
+ const targetNamespaceId =
1290
+ normalizedQualifier !== undefined
1291
+ ? normalizedQualifier
1292
+ : input.modelNamespaceIds.get(targetMapping.model.name);
1273
1293
  foreignKeyNodes.push({
1274
1294
  columns: localColumns,
1275
1295
  references: {
@@ -1289,6 +1309,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
1289
1309
  declaringTableName: tableName,
1290
1310
  targetModelName: targetMapping.model.name,
1291
1311
  targetTableName: targetMapping.tableName,
1312
+ ...ifDefined('targetNamespaceId', targetNamespaceId),
1292
1313
  ...ifDefined('relationName', parsedRelation.relationName),
1293
1314
  localColumns,
1294
1315
  referencedColumns,
@@ -1528,16 +1549,20 @@ function resolvePolymorphism(
1528
1549
  ): Record<string, ContractModel> {
1529
1550
  let patched = models;
1530
1551
 
1552
+ const coordinateFor = (modelName: string): string =>
1553
+ modelCoordinateKey(modelNamespaceIds.get(modelName) ?? defaultNamespaceId, modelName);
1554
+
1531
1555
  // STI variant columns were materialised onto the base storage table so the
1532
1556
  // variants' `storage.fields` resolve. They are storage-only on the base — the
1533
1557
  // domain field belongs to the variant — so strip them from the base model's
1534
1558
  // domain + storage field maps (the table column, built upstream, stays).
1535
1559
  for (const [baseName, fieldNames] of stiBaseFieldsByBase) {
1536
- const baseModel = patched[baseName];
1560
+ const baseKey = coordinateFor(baseName);
1561
+ const baseModel = patched[baseKey];
1537
1562
  if (!baseModel || fieldNames.length === 0) continue;
1538
1563
  patched = {
1539
1564
  ...patched,
1540
- [baseName]: stripStorageOnlyDomainFields(baseModel, fieldNames),
1565
+ [baseKey]: stripStorageOnlyDomainFields(baseModel, fieldNames),
1541
1566
  };
1542
1567
  }
1543
1568
 
@@ -1552,7 +1577,7 @@ function resolvePolymorphism(
1552
1577
  continue;
1553
1578
  }
1554
1579
 
1555
- const model = patched[modelName];
1580
+ const model = patched[coordinateFor(modelName)];
1556
1581
  if (!model) continue;
1557
1582
 
1558
1583
  if (!Object.hasOwn(model.fields, decl.fieldName)) {
@@ -1597,7 +1622,7 @@ function resolvePolymorphism(
1597
1622
 
1598
1623
  patched = {
1599
1624
  ...patched,
1600
- [modelName]: { ...model, discriminator: { field: decl.fieldName }, variants },
1625
+ [coordinateFor(modelName)]: { ...model, discriminator: { field: decl.fieldName }, variants },
1601
1626
  };
1602
1627
  }
1603
1628
 
@@ -1626,7 +1651,7 @@ function resolvePolymorphism(
1626
1651
  continue;
1627
1652
  }
1628
1653
 
1629
- const variantModel = patched[variantName];
1654
+ const variantModel = patched[coordinateFor(variantName)];
1630
1655
  if (!variantModel) continue;
1631
1656
 
1632
1657
  const baseMapping = modelMappings.get(baseDecl.baseName);
@@ -1646,7 +1671,7 @@ function resolvePolymorphism(
1646
1671
 
1647
1672
  patched = {
1648
1673
  ...patched,
1649
- [variantName]: stripStorageOnlyDomainFields(
1674
+ [coordinateFor(variantName)]: stripStorageOnlyDomainFields(
1650
1675
  patchedVariant,
1651
1676
  syntheticPkFieldsByVariant.get(variantName) ?? [],
1652
1677
  ),
@@ -1883,6 +1908,7 @@ export function interpretPslDocumentToSqlContract(
1883
1908
  // remains the input to the rest of the interpreter so non-namespace
1884
1909
  // concerns stay structurally identical to before.
1885
1910
  const models: PslModel[] = [];
1911
+ const modelEntries: ModelNamespaceEntry[] = [];
1886
1912
  const modelNamespaceIds = new Map<string, string>();
1887
1913
  for (const namespace of input.document.ast.namespaces) {
1888
1914
  const resolvedNamespaceId = resolveNamespaceIdForSqlTarget({
@@ -1891,11 +1917,13 @@ export function interpretPslDocumentToSqlContract(
1891
1917
  });
1892
1918
  for (const model of namespace.models) {
1893
1919
  models.push(model);
1920
+ modelEntries.push({ model, namespaceId: resolvedNamespaceId });
1894
1921
  if (resolvedNamespaceId !== undefined) {
1895
1922
  modelNamespaceIds.set(model.name, resolvedNamespaceId);
1896
1923
  }
1897
1924
  }
1898
1925
  }
1926
+ const defaultNamespaceId = input.target.defaultNamespaceId;
1899
1927
  // Top-level enums (the __unspecified__ bucket) route to `storageTypes`;
1900
1928
  // enums inside a named namespace block route to `namespaceTypes[nsId]`.
1901
1929
  const topLevelEnums = input.document.ast.namespaces
@@ -1992,7 +2020,20 @@ export function interpretPslDocumentToSqlContract(
1992
2020
 
1993
2021
  const storageTypes = { ...enumResult.storageTypes, ...namedTypeResult.storageTypes };
1994
2022
 
1995
- const modelMappings = buildModelMappings(models, diagnostics, sourceId);
2023
+ const modelMappingsByCoordinate = buildModelMappings(
2024
+ modelEntries,
2025
+ defaultNamespaceId,
2026
+ diagnostics,
2027
+ sourceId,
2028
+ );
2029
+ // Bare-name view for unqualified relation targets and polymorphism, where
2030
+ // resolution is by bare model name. When a bare name is shared across
2031
+ // namespaces this collapses to the last entry; qualified relation targets
2032
+ // and per-model lowering use the coordinate-keyed map above instead.
2033
+ const modelMappings = new Map<string, ModelNameMapping>();
2034
+ for (const mapping of modelMappingsByCoordinate.values()) {
2035
+ modelMappings.set(mapping.model.name, mapping);
2036
+ }
1996
2037
  const modelNodes: ModelNode[] = [];
1997
2038
  const fkRelationMetadata: FkRelationMetadata[] = [];
1998
2039
  const backrelationCandidates: ModelBackrelationCandidate[] = [];
@@ -2001,8 +2042,9 @@ export function interpretPslDocumentToSqlContract(
2001
2042
  // modelRelations after local back-relation matching so they bypass that step.
2002
2043
  const crossSpaceRelationsByModel = new Map<string, RelationNode[]>();
2003
2044
 
2004
- for (const model of models) {
2005
- const mapping = modelMappings.get(model.name);
2045
+ for (const { model, namespaceId } of modelEntries) {
2046
+ const coordinate = modelCoordinateKey(namespaceId ?? defaultNamespaceId, model.name);
2047
+ const mapping = modelMappingsByCoordinate.get(coordinate);
2006
2048
  if (!mapping) {
2007
2049
  continue;
2008
2050
  }
@@ -2010,6 +2052,7 @@ export function interpretPslDocumentToSqlContract(
2010
2052
  model,
2011
2053
  mapping,
2012
2054
  modelMappings,
2055
+ modelMappingsByCoordinate,
2013
2056
  modelNames,
2014
2057
  compositeTypeNames,
2015
2058
  enumTypeDescriptors: allEnumTypeDescriptors,
@@ -2026,15 +2069,12 @@ export function interpretPslDocumentToSqlContract(
2026
2069
  diagnostics,
2027
2070
  modelNamespaceIds,
2028
2071
  });
2029
- const resolvedNamespaceId = modelNamespaceIds.get(model.name);
2030
2072
  modelNodes.push(
2031
- resolvedNamespaceId !== undefined
2032
- ? { ...result.modelNode, namespaceId: resolvedNamespaceId }
2033
- : result.modelNode,
2073
+ namespaceId !== undefined ? { ...result.modelNode, namespaceId } : result.modelNode,
2034
2074
  );
2035
2075
  fkRelationMetadata.push(...result.fkRelationMetadata);
2036
2076
  backrelationCandidates.push(...result.backrelationCandidates);
2037
- modelResolvedFields.set(model.name, result.resolvedFields);
2077
+ modelResolvedFields.set(coordinate, result.resolvedFields);
2038
2078
  if (result.crossSpaceRelations.length > 0) {
2039
2079
  const existing = crossSpaceRelationsByModel.get(model.name) ?? [];
2040
2080
  crossSpaceRelationsByModel.set(model.name, [...existing, ...result.crossSpaceRelations]);
@@ -2135,15 +2175,17 @@ export function interpretPslDocumentToSqlContract(
2135
2175
  })),
2136
2176
  });
2137
2177
 
2178
+ // Keyed by `(namespaceId, modelName)` coordinate so two models that share a
2179
+ // bare name across namespaces stay distinct through the patch/polymorphism
2180
+ // passes; only a genuine same-namespace duplicate is an error.
2138
2181
  const modelsForPatch: Record<string, ContractModel> = {};
2139
- for (const namespaceSlice of Object.values(contract.domain.namespaces)) {
2182
+ for (const [namespaceId, namespaceSlice] of Object.entries(contract.domain.namespaces)) {
2140
2183
  for (const [modelName, model] of Object.entries(namespaceSlice.models)) {
2141
- if (Object.hasOwn(modelsForPatch, modelName)) {
2142
- throw new Error(
2143
- `duplicate model name "${modelName}" across domain namespaces during PSL interpretation`,
2144
- );
2184
+ const coordinate = modelCoordinateKey(namespaceId, modelName);
2185
+ if (Object.hasOwn(modelsForPatch, coordinate)) {
2186
+ throw new Error(`duplicate model "${namespaceId}.${modelName}" during PSL interpretation`);
2145
2187
  }
2146
- modelsForPatch[modelName] = model;
2188
+ modelsForPatch[coordinate] = model;
2147
2189
  }
2148
2190
  }
2149
2191
  let patchedModels = patchModelDomainFields(modelsForPatch, modelResolvedFields);
@@ -2188,7 +2230,7 @@ export function interpretPslDocumentToSqlContract(
2188
2230
  models: Object.fromEntries(
2189
2231
  Object.entries(namespaceSlice.models).map(([modelName, model]) => [
2190
2232
  modelName,
2191
- patchedModels[modelName] ?? model,
2233
+ patchedModels[modelCoordinateKey(namespaceId, modelName)] ?? model,
2192
2234
  ]),
2193
2235
  ),
2194
2236
  ...(namespaceSlice.valueObjects !== undefined
@@ -42,6 +42,24 @@ export type ModelNameMapping = {
42
42
  readonly fieldColumns: Map<string, string>;
43
43
  };
44
44
 
45
+ /**
46
+ * A PSL model paired with its resolved namespace coordinate (undefined when
47
+ * the target leaves the model late-bound). Two models may share a bare name
48
+ * across namespaces, so structures that must distinguish them are keyed by
49
+ * the `(namespaceId, modelName)` coordinate produced by
50
+ * {@link modelCoordinateKey} rather than the bare model name.
51
+ */
52
+ export type ModelNamespaceEntry = {
53
+ readonly model: PslModel;
54
+ readonly namespaceId: string | undefined;
55
+ };
56
+
57
+ const MODEL_COORDINATE_SEPARATOR = '\u0000';
58
+
59
+ export function modelCoordinateKey(namespaceId: string, modelName: string): string {
60
+ return `${namespaceId}${MODEL_COORDINATE_SEPARATOR}${modelName}`;
61
+ }
62
+
45
63
  export interface CollectResolvedFieldsInput {
46
64
  readonly model: PslModel;
47
65
  readonly mapping: ModelNameMapping;
@@ -424,12 +442,13 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
424
442
  }
425
443
 
426
444
  export function buildModelMappings(
427
- models: readonly PslModel[],
445
+ modelEntries: readonly ModelNamespaceEntry[],
446
+ defaultNamespaceId: string,
428
447
  diagnostics: ContractSourceDiagnostic[],
429
448
  sourceId: string,
430
449
  ): Map<string, ModelNameMapping> {
431
450
  const result = new Map<string, ModelNameMapping>();
432
- for (const model of models) {
451
+ for (const { model, namespaceId } of modelEntries) {
433
452
  const mapAttribute = getAttribute(model.attributes, 'map');
434
453
  const tableName = parseMapName({
435
454
  attribute: mapAttribute,
@@ -452,7 +471,7 @@ export function buildModelMappings(
452
471
  });
453
472
  fieldColumns.set(field.name, columnName);
454
473
  }
455
- result.set(model.name, {
474
+ result.set(modelCoordinateKey(namespaceId ?? defaultNamespaceId, model.name), {
456
475
  model,
457
476
  tableName,
458
477
  fieldColumns,
@@ -42,6 +42,8 @@ export type FkRelationMetadata = {
42
42
  readonly declaringTableName: string;
43
43
  readonly targetModelName: string;
44
44
  readonly targetTableName: string;
45
+ /** Resolved namespace coordinate of the related model, when known. */
46
+ readonly targetNamespaceId?: string;
45
47
  readonly relationName?: string;
46
48
  readonly localColumns: readonly string[];
47
49
  readonly referencedColumns: readonly string[];
@@ -251,6 +253,7 @@ export function indexFkRelations(input: {
251
253
  fieldName: relation.declaringFieldName,
252
254
  toModel: relation.targetModelName,
253
255
  toTable: relation.targetTableName,
256
+ ...ifDefined('toNamespaceId', relation.targetNamespaceId),
254
257
  cardinality: 'N:1',
255
258
  on: {
256
259
  parentTable: relation.declaringTableName,