From 810070a3a4010e5d94418bf6bc49f39bcb2949eb Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 7 Jun 2024 21:58:08 +0800 Subject: [PATCH] fix(delegate): generated logical prisma schema has errors when abstract model is involved --- .../validator/datamodel-validator.ts | 13 +++++- .../src/plugins/prisma/schema-generator.ts | 44 ++++++++++++++++--- packages/schema/src/utils/ast-utils.ts | 16 +++++++ tests/regression/tests/issue-1474.test.ts | 27 ++++++++++++ 4 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 tests/regression/tests/issue-1474.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 9185443f3..eb6d06400 100644 --- a/packages/schema/src/language-server/validator/datamodel-validator.ts +++ b/packages/schema/src/language-server/validator/datamodel-validator.ts @@ -15,6 +15,7 @@ import { isDelegateModel, } from '@zenstackhq/sdk'; import { AstNode, DiagnosticInfo, ValidationAcceptor, getDocument } from 'langium'; +import { findUpInheritance } from '../../utils/ast-utils'; import { IssueCodes, SCALAR_TYPES } from '../constants'; import { AstValidator } from '../types'; import { getUniqueFields } from '../utils'; @@ -238,7 +239,7 @@ export default class DataModelValidator implements AstValidator { return; } - if (field.$container !== contextModel && isDelegateModel(field.$container as DataModel)) { + if (this.isFieldInheritedFromDelegateModel(field, contextModel)) { // relation fields inherited from delegate model don't need opposite relation return; } @@ -390,6 +391,16 @@ export default class DataModelValidator implements AstValidator { } } + // checks if the given field is inherited directly or indirectly from a delegate model + private isFieldInheritedFromDelegateModel(field: DataModelField, contextModel: DataModel) { + const basePath = findUpInheritance(contextModel, field.$container as DataModel); + if (basePath && basePath.some(isDelegateModel)) { + return true; + } else { + return false; + } + } + private validateBaseAbstractModel(model: DataModel, accept: ValidationAcceptor) { model.superTypes.forEach((superType, index) => { if ( diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 0f5095a4f..e73d8203f 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -579,18 +579,30 @@ export class PrismaSchemaGenerator { // 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)) { + if (!isDataModel(f.type.reference?.ref)) { + // only process relation fields return; } - const prismaField = model.fields.find((field) => field.name === f.name); - if (!prismaField) { + if (!f.$inheritedFrom) { + // only process inherited fields return; } - // find the base field that this field is inherited from - const baseField = f.$inheritedFrom.fields.find((field) => field.name === f.name); + // Walk up the inheritance chain to find a field with matching name + // which is where this field is inherited from. + // + // Note that we can't walk all the way up to the $inheritedFrom model + // because it may have been eliminated because of being abstract. + + const baseField = this.findUpMatchingFieldFromDelegate(decl, f); if (!baseField) { + // only process fields inherited from delegate models + return; + } + + const prismaField = model.fields.find((field) => field.name === f.name); + if (!prismaField) { return; } @@ -629,6 +641,28 @@ export class PrismaSchemaGenerator { }); } + private findUpMatchingFieldFromDelegate(start: DataModel, target: DataModelField): DataModelField | undefined { + for (const base of start.superTypes) { + if (isDataModel(base.ref)) { + if (isDelegateModel(base.ref)) { + const field = base.ref.fields.find((f) => f.name === target.name); + if (field) { + if (!field.$inheritedFrom || !isDelegateModel(field.$inheritedFrom)) { + // if this field is not inherited from an upper delegate, we're done + return field; + } + } + } + + const upper = this.findUpMatchingFieldFromDelegate(base.ref, target); + if (upper) { + return upper; + } + } + } + return undefined; + } + private getOppositeRelationField(oppositeModel: DataModel, relationField: DataModelField) { const relName = this.getRelationName(relationField); return oppositeModel.fields.find( diff --git a/packages/schema/src/utils/ast-utils.ts b/packages/schema/src/utils/ast-utils.ts index e891056c2..24af8862d 100644 --- a/packages/schema/src/utils/ast-utils.ts +++ b/packages/schema/src/utils/ast-utils.ts @@ -296,3 +296,19 @@ export function getAllLoadedAndReachableDataModels(langiumDocuments: LangiumDocu return allDataModels; } + +/** + * Walk up the inheritance chain to find the path from the start model to the target model + */ +export function findUpInheritance(start: DataModel, target: DataModel): DataModel[] | undefined { + for (const base of start.superTypes) { + if (base.ref === target) { + return [base.ref]; + } + const path = findUpInheritance(base.ref as DataModel, target); + if (path) { + return [base.ref as DataModel, ...path]; + } + } + return undefined; +} diff --git a/tests/regression/tests/issue-1474.test.ts b/tests/regression/tests/issue-1474.test.ts new file mode 100644 index 000000000..9e157d40d --- /dev/null +++ b/tests/regression/tests/issue-1474.test.ts @@ -0,0 +1,27 @@ +import { loadSchema } from '@zenstackhq/testtools'; +describe('issue 1474', () => { + it('regression', async () => { + await loadSchema( + ` + model A { + id Int @id + cs C[] + } + + abstract model B { + a A @relation(fields: [aId], references: [id]) + aId Int + } + + model C extends B { + id Int @id + type String + @@delegate(type) + } + + model D extends C { + } + ` + ); + }); +});