@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/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{interpreter-VW03aPWq.mjs → interpreter-1VmrYYbi.mjs} +124 -3
- package/dist/interpreter-1VmrYYbi.mjs.map +1 -0
- package/dist/provider.mjs +1 -1
- package/package.json +12 -12
- package/src/interpreter.ts +173 -2
- package/src/psl-field-resolution.ts +6 -0
- package/dist/interpreter-VW03aPWq.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/sql-contract-psl",
|
|
3
|
-
"version": "0.12.0-dev.
|
|
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.
|
|
10
|
-
"@prisma-next/contract": "0.12.0-dev.
|
|
11
|
-
"@prisma-next/framework-components": "0.12.0-dev.
|
|
12
|
-
"@prisma-next/psl-parser": "0.12.0-dev.
|
|
13
|
-
"@prisma-next/sql-contract": "0.12.0-dev.
|
|
14
|
-
"@prisma-next/sql-contract-ts": "0.12.0-dev.
|
|
15
|
-
"@prisma-next/utils": "0.12.0-dev.
|
|
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.
|
|
20
|
-
"@prisma-next/test-utils": "0.12.0-dev.
|
|
21
|
-
"@prisma-next/tsconfig": "0.12.0-dev.
|
|
22
|
-
"@prisma-next/tsdown": "0.12.0-dev.
|
|
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",
|
package/src/interpreter.ts
CHANGED
|
@@ -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;
|