Skip to content

Commit 9f9d277

Browse files
authored
fix(polymorphism): relation name disambiguation (#1107)
1 parent d11d4ba commit 9f9d277

File tree

5 files changed

+133
-6
lines changed

5 files changed

+133
-6
lines changed

packages/schema/src/language-server/validator/datamodel-validator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ export default class DataModelValidator implements AstValidator<DataModel> {
241241
const oppositeModel = field.type.reference!.ref! as DataModel;
242242

243243
// Use name because the current document might be updated
244-
let oppositeFields = getModelFieldsWithBases(oppositeModel).filter(
244+
let oppositeFields = getModelFieldsWithBases(oppositeModel, false).filter(
245245
(f) => f.type.reference?.ref?.name === contextModel.name
246246
);
247247
oppositeFields = oppositeFields.filter((f) => {

packages/schema/src/plugins/enhancer/enhance/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ async function generateLogicalPrisma(model: Model, options: PluginOptions, outDi
133133
} catch {
134134
// noop
135135
}
136-
throw new PluginError(name, `Failed to run "prisma generate"`);
136+
throw new PluginError(name, `Failed to run "prisma generate" on logical schema: ${logicalPrismaFile}`);
137137
}
138138

139139
// make a bunch of typing fixes to the generated prisma client

packages/schema/src/plugins/prisma/schema-generator.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { getIdFields } from '../../utils/ast-utils';
3434
import { DELEGATE_AUX_RELATION_PREFIX, PRISMA_MINIMUM_VERSION } from '@zenstackhq/runtime';
3535
import {
3636
getAttribute,
37+
getAttributeArg,
3738
getForeignKeyFields,
3839
getLiteral,
3940
getPrismaVersion,
@@ -299,6 +300,9 @@ export class PrismaSchemaGenerator {
299300

300301
// expand relations on other models that reference delegated models to concrete models
301302
this.expandPolymorphicRelations(model, decl);
303+
304+
// name relations inherited from delegate base models for disambiguation
305+
this.nameRelationsInheritedFromDelegate(model, decl);
302306
}
303307

304308
private generateDelegateRelationForBase(model: PrismaDataModel, decl: DataModel) {
@@ -422,6 +426,8 @@ export class PrismaSchemaGenerator {
422426
);
423427

424428
const addedRel = new PrismaFieldAttribute('@relation', [
429+
// use field name as relation name for disambiguation
430+
new PrismaAttributeArg(undefined, new AttributeArgValue('String', relationField.name)),
425431
new PrismaAttributeArg('fields', args),
426432
new PrismaAttributeArg('references', args),
427433
]);
@@ -440,11 +446,60 @@ export class PrismaSchemaGenerator {
440446
} else {
441447
relationField.attributes.push(this.makeFieldAttribute(relAttr as DataModelFieldAttribute));
442448
}
449+
} else {
450+
relationField.attributes.push(
451+
new PrismaFieldAttribute('@relation', [
452+
// use field name as relation name for disambiguation
453+
new PrismaAttributeArg(undefined, new AttributeArgValue('String', relationField.name)),
454+
])
455+
);
443456
}
444457
});
445458
});
446459
}
447460

461+
private nameRelationsInheritedFromDelegate(model: PrismaDataModel, decl: DataModel) {
462+
if (this.mode !== 'logical') {
463+
return;
464+
}
465+
466+
// the logical schema needs to name relations inherited from delegate base models for disambiguation
467+
468+
decl.fields.forEach((f) => {
469+
if (!f.$inheritedFrom || !isDelegateModel(f.$inheritedFrom) || !isDataModel(f.type.reference?.ref)) {
470+
return;
471+
}
472+
473+
const prismaField = model.fields.find((field) => field.name === f.name);
474+
if (!prismaField) {
475+
return;
476+
}
477+
478+
const relAttr = getAttribute(f, '@relation');
479+
const relName = `${DELEGATE_AUX_RELATION_PREFIX}_${lowerCaseFirst(decl.name)}`;
480+
481+
if (relAttr) {
482+
const nameArg = getAttributeArg(relAttr, 'name');
483+
if (!nameArg) {
484+
const prismaRelAttr = prismaField.attributes.find(
485+
(attr) => (attr as PrismaFieldAttribute).name === '@relation'
486+
) as PrismaFieldAttribute;
487+
if (prismaRelAttr) {
488+
prismaRelAttr.args.unshift(
489+
new PrismaAttributeArg(undefined, new AttributeArgValue('String', relName))
490+
);
491+
}
492+
}
493+
} else {
494+
prismaField.attributes.push(
495+
new PrismaFieldAttribute('@relation', [
496+
new PrismaAttributeArg(undefined, new AttributeArgValue('String', relName)),
497+
])
498+
);
499+
}
500+
});
501+
}
502+
448503
private get supportNamedConstraints() {
449504
const ds = this.zmodel.declarations.find(isDataSource);
450505
if (!ds) {

packages/schema/src/utils/ast-utils.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
ModelImport,
1717
ReferenceExpr,
1818
} from '@zenstackhq/language/ast';
19-
import { isFromStdlib } from '@zenstackhq/sdk';
19+
import { isDelegateModel, isFromStdlib } from '@zenstackhq/sdk';
2020
import {
2121
AstNode,
2222
copyAstNode,
@@ -207,19 +207,22 @@ export function getContainingDataModel(node: Expression): DataModel | undefined
207207
return undefined;
208208
}
209209

210-
export function getModelFieldsWithBases(model: DataModel) {
210+
export function getModelFieldsWithBases(model: DataModel, includeDelegate = true) {
211211
if (model.$baseMerged) {
212212
return model.fields;
213213
} else {
214-
return [...model.fields, ...getRecursiveBases(model).flatMap((base) => base.fields)];
214+
return [...model.fields, ...getRecursiveBases(model, includeDelegate).flatMap((base) => base.fields)];
215215
}
216216
}
217217

218-
export function getRecursiveBases(dataModel: DataModel): DataModel[] {
218+
export function getRecursiveBases(dataModel: DataModel, includeDelegate = true): DataModel[] {
219219
const result: DataModel[] = [];
220220
dataModel.superTypes.forEach((superType) => {
221221
const baseDecl = superType.ref;
222222
if (baseDecl) {
223+
if (!includeDelegate && isDelegateModel(baseDecl)) {
224+
return;
225+
}
223226
result.push(baseDecl);
224227
result.push(...getRecursiveBases(baseDecl));
225228
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { loadModelWithError, loadSchema } from '@zenstackhq/testtools';
2+
3+
describe('Regression for issue 1100', () => {
4+
it('missing opposite relation', async () => {
5+
const schema = `
6+
model User {
7+
id String @id @default(cuid())
8+
name String?
9+
content Content[]
10+
post Post[]
11+
}
12+
13+
model Content {
14+
id String @id @default(cuid())
15+
published Boolean @default(false)
16+
contentType String
17+
@@delegate(contentType)
18+
19+
user User @relation(fields: [userId], references: [id])
20+
userId String
21+
}
22+
23+
model Post extends Content {
24+
title String
25+
}
26+
27+
model Image extends Content {
28+
url String
29+
}
30+
`;
31+
32+
await expect(loadModelWithError(schema)).resolves.toContain(
33+
'The relation field "post" on model "User" is missing an opposite relation field on model "Post"'
34+
);
35+
});
36+
37+
it('success', async () => {
38+
const schema = `
39+
model User {
40+
id String @id @default(cuid())
41+
name String?
42+
content Content[]
43+
post Post[]
44+
}
45+
46+
model Content {
47+
id String @id @default(cuid())
48+
published Boolean @default(false)
49+
contentType String
50+
@@delegate(contentType)
51+
52+
user User @relation(fields: [userId], references: [id])
53+
userId String
54+
}
55+
56+
model Post extends Content {
57+
title String
58+
author User @relation(fields: [authorId], references: [id])
59+
authorId String
60+
}
61+
62+
model Image extends Content {
63+
url String
64+
}
65+
`;
66+
67+
await expect(loadSchema(schema)).toResolveTruthy();
68+
});
69+
});

0 commit comments

Comments
 (0)