From 9f9d27704c2eecbbbd69e841ece6b1d4d22040f6 Mon Sep 17 00:00:00 2001 From: Yiming Date: Fri, 8 Mar 2024 21:11:26 -0800 Subject: [PATCH] fix(polymorphism): relation name disambiguation (#1107) --- .../validator/datamodel-validator.ts | 2 +- .../src/plugins/enhancer/enhance/index.ts | 2 +- .../src/plugins/prisma/schema-generator.ts | 55 +++++++++++++++ packages/schema/src/utils/ast-utils.ts | 11 +-- .../with-delegate/issue-1100.test.ts | 69 +++++++++++++++++++ 5 files changed, 133 insertions(+), 6 deletions(-) create mode 100644 tests/integration/tests/enhancements/with-delegate/issue-1100.test.ts diff --git a/packages/schema/src/language-server/validator/datamodel-validator.ts b/packages/schema/src/language-server/validator/datamodel-validator.ts index 1d442f12b..4f9cd0039 100644 --- a/packages/schema/src/language-server/validator/datamodel-validator.ts +++ b/packages/schema/src/language-server/validator/datamodel-validator.ts @@ -241,7 +241,7 @@ export default class DataModelValidator implements AstValidator { const oppositeModel = field.type.reference!.ref! as DataModel; // Use name because the current document might be updated - let oppositeFields = getModelFieldsWithBases(oppositeModel).filter( + let oppositeFields = getModelFieldsWithBases(oppositeModel, false).filter( (f) => f.type.reference?.ref?.name === contextModel.name ); oppositeFields = oppositeFields.filter((f) => { diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index df14e0826..a379e5ad0 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -133,7 +133,7 @@ async function generateLogicalPrisma(model: Model, options: PluginOptions, outDi } catch { // noop } - throw new PluginError(name, `Failed to run "prisma generate"`); + throw new PluginError(name, `Failed to run "prisma generate" on logical schema: ${logicalPrismaFile}`); } // make a bunch of typing fixes to the generated prisma client diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index bc63d535a..bfcecc9ef 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -34,6 +34,7 @@ import { getIdFields } from '../../utils/ast-utils'; import { DELEGATE_AUX_RELATION_PREFIX, PRISMA_MINIMUM_VERSION } from '@zenstackhq/runtime'; import { getAttribute, + getAttributeArg, getForeignKeyFields, getLiteral, getPrismaVersion, @@ -299,6 +300,9 @@ export class PrismaSchemaGenerator { // expand relations on other models that reference delegated models to concrete models this.expandPolymorphicRelations(model, decl); + + // name relations inherited from delegate base models for disambiguation + this.nameRelationsInheritedFromDelegate(model, decl); } private generateDelegateRelationForBase(model: PrismaDataModel, decl: DataModel) { @@ -422,6 +426,8 @@ export class PrismaSchemaGenerator { ); const addedRel = new PrismaFieldAttribute('@relation', [ + // use field name as relation name for disambiguation + new PrismaAttributeArg(undefined, new AttributeArgValue('String', relationField.name)), new PrismaAttributeArg('fields', args), new PrismaAttributeArg('references', args), ]); @@ -440,11 +446,60 @@ export class PrismaSchemaGenerator { } else { relationField.attributes.push(this.makeFieldAttribute(relAttr as DataModelFieldAttribute)); } + } else { + relationField.attributes.push( + new PrismaFieldAttribute('@relation', [ + // use field name as relation name for disambiguation + new PrismaAttributeArg(undefined, new AttributeArgValue('String', relationField.name)), + ]) + ); } }); }); } + private nameRelationsInheritedFromDelegate(model: PrismaDataModel, decl: DataModel) { + if (this.mode !== 'logical') { + return; + } + + // the logical schema needs to name relations inherited from delegate base models for disambiguation + + decl.fields.forEach((f) => { + if (!f.$inheritedFrom || !isDelegateModel(f.$inheritedFrom) || !isDataModel(f.type.reference?.ref)) { + return; + } + + const prismaField = model.fields.find((field) => field.name === f.name); + if (!prismaField) { + return; + } + + const relAttr = getAttribute(f, '@relation'); + const relName = `${DELEGATE_AUX_RELATION_PREFIX}_${lowerCaseFirst(decl.name)}`; + + if (relAttr) { + const nameArg = getAttributeArg(relAttr, 'name'); + if (!nameArg) { + const prismaRelAttr = prismaField.attributes.find( + (attr) => (attr as PrismaFieldAttribute).name === '@relation' + ) as PrismaFieldAttribute; + if (prismaRelAttr) { + prismaRelAttr.args.unshift( + new PrismaAttributeArg(undefined, new AttributeArgValue('String', relName)) + ); + } + } + } else { + prismaField.attributes.push( + new PrismaFieldAttribute('@relation', [ + new PrismaAttributeArg(undefined, new AttributeArgValue('String', relName)), + ]) + ); + } + }); + } + private get supportNamedConstraints() { const ds = this.zmodel.declarations.find(isDataSource); if (!ds) { diff --git a/packages/schema/src/utils/ast-utils.ts b/packages/schema/src/utils/ast-utils.ts index 2688987a2..8dfe75b4b 100644 --- a/packages/schema/src/utils/ast-utils.ts +++ b/packages/schema/src/utils/ast-utils.ts @@ -16,7 +16,7 @@ import { ModelImport, ReferenceExpr, } from '@zenstackhq/language/ast'; -import { isFromStdlib } from '@zenstackhq/sdk'; +import { isDelegateModel, isFromStdlib } from '@zenstackhq/sdk'; import { AstNode, copyAstNode, @@ -207,19 +207,22 @@ export function getContainingDataModel(node: Expression): DataModel | undefined return undefined; } -export function getModelFieldsWithBases(model: DataModel) { +export function getModelFieldsWithBases(model: DataModel, includeDelegate = true) { if (model.$baseMerged) { return model.fields; } else { - return [...model.fields, ...getRecursiveBases(model).flatMap((base) => base.fields)]; + return [...model.fields, ...getRecursiveBases(model, includeDelegate).flatMap((base) => base.fields)]; } } -export function getRecursiveBases(dataModel: DataModel): DataModel[] { +export function getRecursiveBases(dataModel: DataModel, includeDelegate = true): DataModel[] { const result: DataModel[] = []; dataModel.superTypes.forEach((superType) => { const baseDecl = superType.ref; if (baseDecl) { + if (!includeDelegate && isDelegateModel(baseDecl)) { + return; + } result.push(baseDecl); result.push(...getRecursiveBases(baseDecl)); } diff --git a/tests/integration/tests/enhancements/with-delegate/issue-1100.test.ts b/tests/integration/tests/enhancements/with-delegate/issue-1100.test.ts new file mode 100644 index 000000000..8b1945b8d --- /dev/null +++ b/tests/integration/tests/enhancements/with-delegate/issue-1100.test.ts @@ -0,0 +1,69 @@ +import { loadModelWithError, loadSchema } from '@zenstackhq/testtools'; + +describe('Regression for issue 1100', () => { + it('missing opposite relation', async () => { + const schema = ` + model User { + id String @id @default(cuid()) + name String? + content Content[] + post Post[] + } + + model Content { + id String @id @default(cuid()) + published Boolean @default(false) + contentType String + @@delegate(contentType) + + user User @relation(fields: [userId], references: [id]) + userId String + } + + model Post extends Content { + title String + } + + model Image extends Content { + url String + } + `; + + await expect(loadModelWithError(schema)).resolves.toContain( + 'The relation field "post" on model "User" is missing an opposite relation field on model "Post"' + ); + }); + + it('success', async () => { + const schema = ` + model User { + id String @id @default(cuid()) + name String? + content Content[] + post Post[] + } + + model Content { + id String @id @default(cuid()) + published Boolean @default(false) + contentType String + @@delegate(contentType) + + user User @relation(fields: [userId], references: [id]) + userId String + } + + model Post extends Content { + title String + author User @relation(fields: [authorId], references: [id]) + authorId String + } + + model Image extends Content { + url String + } + `; + + await expect(loadSchema(schema)).toResolveTruthy(); + }); +});