@prisma-next/sql-contract-psl 0.12.0-dev.59 → 0.12.0-dev.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,25 +1,25 @@
1
1
  {
2
2
  "name": "@prisma-next/sql-contract-psl",
3
- "version": "0.12.0-dev.59",
3
+ "version": "0.12.0-dev.60",
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.59",
10
- "@prisma-next/contract": "0.12.0-dev.59",
11
- "@prisma-next/framework-components": "0.12.0-dev.59",
12
- "@prisma-next/psl-parser": "0.12.0-dev.59",
13
- "@prisma-next/sql-contract": "0.12.0-dev.59",
14
- "@prisma-next/sql-contract-ts": "0.12.0-dev.59",
15
- "@prisma-next/utils": "0.12.0-dev.59",
9
+ "@prisma-next/config": "0.12.0-dev.60",
10
+ "@prisma-next/contract": "0.12.0-dev.60",
11
+ "@prisma-next/framework-components": "0.12.0-dev.60",
12
+ "@prisma-next/psl-parser": "0.12.0-dev.60",
13
+ "@prisma-next/sql-contract": "0.12.0-dev.60",
14
+ "@prisma-next/sql-contract-ts": "0.12.0-dev.60",
15
+ "@prisma-next/utils": "0.12.0-dev.60",
16
16
  "pathe": "^2.0.3"
17
17
  },
18
18
  "devDependencies": {
19
- "@prisma-next/contract-authoring": "0.12.0-dev.59",
20
- "@prisma-next/test-utils": "0.12.0-dev.59",
21
- "@prisma-next/tsconfig": "0.12.0-dev.59",
22
- "@prisma-next/tsdown": "0.12.0-dev.59",
19
+ "@prisma-next/contract-authoring": "0.12.0-dev.60",
20
+ "@prisma-next/test-utils": "0.12.0-dev.60",
21
+ "@prisma-next/tsconfig": "0.12.0-dev.60",
22
+ "@prisma-next/tsdown": "0.12.0-dev.60",
23
23
  "arktype": "^2.2.0",
24
24
  "tsdown": "0.22.1",
25
25
  "typescript": "5.9.3",
@@ -48,6 +48,7 @@ import {
48
48
  type IndexNode,
49
49
  type ModelNode,
50
50
  type PrimaryKeyNode,
51
+ type RelationNode,
51
52
  type UniqueConstraintNode,
52
53
  } from '@prisma-next/sql-contract-ts/contract-builder';
53
54
  import { blindCast } from '@prisma-next/utils/casts';
@@ -605,6 +606,8 @@ interface BuildModelNodeResult {
605
606
  readonly fkRelationMetadata: FkRelationMetadata[];
606
607
  readonly backrelationCandidates: ModelBackrelationCandidate[];
607
608
  readonly resolvedFields: readonly ResolvedField[];
609
+ /** Cross-contract-space relation nodes that bypass the local back-relation matching. */
610
+ readonly crossSpaceRelations: RelationNode[];
608
611
  }
609
612
 
610
613
  function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult {
@@ -960,13 +963,162 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
960
963
  }
961
964
 
962
965
  const resultFkRelationMetadata: FkRelationMetadata[] = [];
966
+ const resultCrossSpaceRelations: RelationNode[] = [];
963
967
  for (const relationAttribute of relationAttributes) {
968
+ const {
969
+ typeName: fieldTypeName,
970
+ typeNamespaceId: fieldTypeNamespaceId,
971
+ typeContractSpaceId: fieldTypeContractSpaceId,
972
+ } = relationAttribute.field;
973
+
964
974
  if (relationAttribute.field.list) {
975
+ // F-list: cross-space list relations are explicitly unsupported (Option B does not
976
+ // navigate, so a list target makes no sense to carry). Emit a diagnostic instead of
977
+ // silently dropping the field — the author needs to know the field was ignored.
978
+ if (fieldTypeContractSpaceId !== undefined) {
979
+ diagnostics.push({
980
+ code: 'PSL_UNSUPPORTED_CROSS_SPACE_LIST',
981
+ message: `Relation field "${model.name}.${relationAttribute.field.name}" is a cross-space list relation (type "${fieldTypeContractSpaceId}:${fieldTypeNamespaceId !== undefined ? `${fieldTypeNamespaceId}.` : ''}${fieldTypeName}[]"). Cross-space relations must be singular in v0.1 — list cross-space relations are not supported.`,
982
+ sourceId,
983
+ span: relationAttribute.field.span,
984
+ });
985
+ }
986
+ continue;
987
+ }
988
+
989
+ // Cross-contract-space relation: the target model lives in a different contract space
990
+ // identified by `typeContractSpaceId` (e.g. `supabase:auth.User`).
991
+ if (fieldTypeContractSpaceId !== undefined) {
992
+ // Fail fast if the space is not in the composed extension packs (AC5 PSL half).
993
+ if (!input.composedExtensions.has(fieldTypeContractSpaceId)) {
994
+ diagnostics.push({
995
+ code: 'PSL_UNKNOWN_CONTRACT_SPACE',
996
+ 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.`,
997
+ sourceId,
998
+ span: relationAttribute.field.span,
999
+ data: { space: fieldTypeContractSpaceId, suggestedPack: fieldTypeContractSpaceId },
1000
+ });
1001
+ continue;
1002
+ }
1003
+
1004
+ const parsedRelation = parseRelationAttribute({
1005
+ attribute: relationAttribute.relation,
1006
+ modelName: model.name,
1007
+ fieldName: relationAttribute.field.name,
1008
+ sourceId,
1009
+ diagnostics,
1010
+ });
1011
+ if (!parsedRelation) {
1012
+ continue;
1013
+ }
1014
+ if (!parsedRelation.fields || !parsedRelation.references) {
1015
+ diagnostics.push({
1016
+ code: 'PSL_INVALID_RELATION_ATTRIBUTE',
1017
+ message: `Relation field "${model.name}.${relationAttribute.field.name}" requires fields and references arguments`,
1018
+ sourceId,
1019
+ span: relationAttribute.relation.span,
1020
+ });
1021
+ continue;
1022
+ }
1023
+
1024
+ const localColumns = mapFieldNamesToColumns({
1025
+ modelName: model.name,
1026
+ fieldNames: parsedRelation.fields,
1027
+ mapping,
1028
+ sourceId,
1029
+ diagnostics,
1030
+ span: relationAttribute.relation.span,
1031
+ entityLabel: `Relation field "${model.name}.${relationAttribute.field.name}"`,
1032
+ });
1033
+ if (!localColumns) {
1034
+ continue;
1035
+ }
1036
+
1037
+ // For cross-space references the `references` list provides field names from the remote
1038
+ // model. Since the interpreter has no access to the extension contract, these field names
1039
+ // are treated as column names directly (matching the TS builder's cross-space path).
1040
+ const referencedColumns = parsedRelation.references;
1041
+
1042
+ if (localColumns.length !== referencedColumns.length) {
1043
+ diagnostics.push({
1044
+ code: 'PSL_INVALID_RELATION_ATTRIBUTE',
1045
+ message: `Relation field "${model.name}.${relationAttribute.field.name}" must provide the same number of fields and references`,
1046
+ sourceId,
1047
+ span: relationAttribute.relation.span,
1048
+ });
1049
+ continue;
1050
+ }
1051
+
1052
+ const onDelete = parsedRelation.onDelete
1053
+ ? normalizeReferentialAction({
1054
+ modelName: model.name,
1055
+ fieldName: relationAttribute.field.name,
1056
+ actionName: 'onDelete',
1057
+ actionToken: parsedRelation.onDelete,
1058
+ sourceId,
1059
+ span: relationAttribute.field.span,
1060
+ diagnostics,
1061
+ })
1062
+ : undefined;
1063
+ const onUpdate = parsedRelation.onUpdate
1064
+ ? normalizeReferentialAction({
1065
+ modelName: model.name,
1066
+ fieldName: relationAttribute.field.name,
1067
+ actionName: 'onUpdate',
1068
+ actionToken: parsedRelation.onUpdate,
1069
+ sourceId,
1070
+ span: relationAttribute.field.span,
1071
+ diagnostics,
1072
+ })
1073
+ : undefined;
1074
+
1075
+ // Target namespace: use the colon-prefix namespace qualifier, or `__unbound__` when the
1076
+ // no-namespace form is used (e.g. `supabase:User` → AC3).
1077
+ const crossTargetNamespaceId = fieldTypeNamespaceId ?? '__unbound__';
1078
+
1079
+ // Target table name: the interpreter cannot resolve `User → users` because it has no
1080
+ // access to the extension contract (only a Set<string> of space names is available).
1081
+ // Use `fieldTypeName.toLowerCase()` as a symbolic fallback — the same convention the TS
1082
+ // builder uses (`targetModelName.toLowerCase()`) when `targetTableName` is absent.
1083
+ // Physical table resolution against the extension contract is deferred to the aggregate
1084
+ // stage (M3), which has access to the full extension contract.
1085
+ const crossTargetTableName = fieldTypeName.toLowerCase();
1086
+
1087
+ foreignKeyNodes.push({
1088
+ columns: localColumns,
1089
+ references: {
1090
+ model: fieldTypeName,
1091
+ table: crossTargetTableName,
1092
+ columns: referencedColumns,
1093
+ namespaceId: crossTargetNamespaceId,
1094
+ spaceId: fieldTypeContractSpaceId,
1095
+ },
1096
+ ...ifDefined('name', parsedRelation.constraintName),
1097
+ ...ifDefined('onDelete', onDelete),
1098
+ ...ifDefined('onUpdate', onUpdate),
1099
+ });
1100
+
1101
+ // Build the cross-space RelationNode directly (no local back-relation candidate).
1102
+ // `buildSqlContractFromDefinition` recognises `spaceId` on a RelationNode and routes it
1103
+ // through the cross-space domain-relation path (produces a non-navigable CrossReference).
1104
+ resultCrossSpaceRelations.push({
1105
+ fieldName: relationAttribute.field.name,
1106
+ toModel: fieldTypeName,
1107
+ toTable: crossTargetTableName,
1108
+ cardinality: 'N:1',
1109
+ spaceId: fieldTypeContractSpaceId,
1110
+ namespaceId: crossTargetNamespaceId,
1111
+ on: {
1112
+ parentTable: tableName,
1113
+ parentColumns: localColumns,
1114
+ childTable: crossTargetTableName,
1115
+ childColumns: referencedColumns,
1116
+ },
1117
+ });
1118
+
965
1119
  continue;
966
1120
  }
967
1121
 
968
- const { typeName: fieldTypeName, typeNamespaceId: fieldTypeNamespaceId } =
969
- relationAttribute.field;
970
1122
  const qualifiedTypeName = fieldTypeNamespaceId
971
1123
  ? `${fieldTypeNamespaceId}.${fieldTypeName}`
972
1124
  : fieldTypeName;
@@ -1129,6 +1281,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
1129
1281
  ...ifDefined('control', controlPolicy),
1130
1282
  },
1131
1283
  fkRelationMetadata: resultFkRelationMetadata,
1284
+ crossSpaceRelations: resultCrossSpaceRelations,
1132
1285
  backrelationCandidates: resultBackrelationCandidates,
1133
1286
  resolvedFields,
1134
1287
  };
@@ -1809,6 +1962,9 @@ export function interpretPslDocumentToSqlContract(
1809
1962
  const fkRelationMetadata: FkRelationMetadata[] = [];
1810
1963
  const backrelationCandidates: ModelBackrelationCandidate[] = [];
1811
1964
  const modelResolvedFields = new Map<string, readonly ResolvedField[]>();
1965
+ // Cross-space relation nodes keyed by declaring model name — merged into
1966
+ // modelRelations after local back-relation matching so they bypass that step.
1967
+ const crossSpaceRelationsByModel = new Map<string, RelationNode[]>();
1812
1968
 
1813
1969
  for (const model of models) {
1814
1970
  const mapping = modelMappings.get(model.name);
@@ -1843,6 +1999,10 @@ export function interpretPslDocumentToSqlContract(
1843
1999
  fkRelationMetadata.push(...result.fkRelationMetadata);
1844
2000
  backrelationCandidates.push(...result.backrelationCandidates);
1845
2001
  modelResolvedFields.set(model.name, result.resolvedFields);
2002
+ if (result.crossSpaceRelations.length > 0) {
2003
+ const existing = crossSpaceRelationsByModel.get(model.name) ?? [];
2004
+ crossSpaceRelationsByModel.set(model.name, [...existing, ...result.crossSpaceRelations]);
2005
+ }
1846
2006
  }
1847
2007
 
1848
2008
  const { modelRelations, fkRelationsByPair } = indexFkRelations({ fkRelationMetadata });
@@ -1854,6 +2014,17 @@ export function interpretPslDocumentToSqlContract(
1854
2014
  sourceId,
1855
2015
  });
1856
2016
 
2017
+ // Merge cross-space relations into modelRelations after local back-relation matching.
2018
+ // Cross-space targets have no local back-relation candidates, so they bypass that step.
2019
+ for (const [modelName, relations] of crossSpaceRelationsByModel) {
2020
+ const existing = modelRelations.get(modelName);
2021
+ if (existing) {
2022
+ existing.push(...relations);
2023
+ } else {
2024
+ modelRelations.set(modelName, [...relations]);
2025
+ }
2026
+ }
2027
+
1857
2028
  const { discriminatorDeclarations, baseDeclarations } = collectPolymorphismDeclarations(
1858
2029
  models,
1859
2030
  sourceId,
@@ -232,6 +232,12 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
232
232
  if (isModelField && relationAttribute) {
233
233
  continue;
234
234
  }
235
+ // Cross-contract-space relation fields (e.g. `supabase:auth.User @relation(...)`) are not
236
+ // local model fields, but they carry a @relation attribute and should be skipped here.
237
+ // Their FK and RelationNode lowering is handled separately in the interpreter.
238
+ if (field.typeContractSpaceId !== undefined && relationAttribute) {
239
+ continue;
240
+ }
235
241
 
236
242
  const isValueObjectField = compositeTypeNames.has(field.typeName);
237
243
  const isListField = field.list;