diff --git a/packages/schema/src/utils/ast-utils.ts b/packages/schema/src/utils/ast-utils.ts index b040d4cf3..effd472f0 100644 --- a/packages/schema/src/utils/ast-utils.ts +++ b/packages/schema/src/utils/ast-utils.ts @@ -17,7 +17,13 @@ import { ModelImport, ReferenceExpr, } from '@zenstackhq/language/ast'; -import { getModelFieldsWithBases, getRecursiveBases, isDelegateModel, isFromStdlib } from '@zenstackhq/sdk'; +import { + getInheritanceChain, + getModelFieldsWithBases, + getRecursiveBases, + isDelegateModel, + isFromStdlib, +} from '@zenstackhq/sdk'; import { AstNode, copyAstNode, @@ -61,7 +67,7 @@ export function mergeBaseModels(model: Model, linker: Linker) { .concat(dataModel.fields); dataModel.attributes = bases - .flatMap((base) => base.attributes.filter((attr) => filterBaseAttribute(base, attr))) + .flatMap((base) => base.attributes.filter((attr) => filterBaseAttribute(dataModel, base, attr))) .map((attr) => cloneAst(attr, dataModel, buildReference)) .concat(dataModel.attributes); } @@ -85,7 +91,7 @@ export function mergeBaseModels(model: Model, linker: Linker) { linkContentToContainer(model); } -function filterBaseAttribute(base: DataModel, attr: DataModelAttribute) { +function filterBaseAttribute(forModel: DataModel, base: DataModel, attr: DataModelAttribute) { if (attr.$inheritedFrom) { // don't inherit from skip-level base return false; @@ -101,13 +107,26 @@ function filterBaseAttribute(base: DataModel, attr: DataModelAttribute) { return false; } - if (isDelegateModel(base) && uninheritableFromDelegateAttributes.includes(attr.decl.$refText)) { + if ( + // checks if the inheritance is from a delegate model or through one, if so, + // the attribute shouldn't be inherited as the delegate already inherits it + isInheritedFromOrThroughDelegate(forModel, base) && + uninheritableFromDelegateAttributes.includes(attr.decl.$refText) + ) { return false; } return true; } +function isInheritedFromOrThroughDelegate(model: DataModel, base: DataModel) { + if (isDelegateModel(base)) { + return true; + } + const chain = getInheritanceChain(model, base); + return !!chain?.some(isDelegateModel); +} + // deep clone an AST, relink references, and set its container function cloneAst( node: T, diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts index d6c4c0fd5..6fbcfcbf3 100644 --- a/packages/sdk/src/utils.ts +++ b/packages/sdk/src/utils.ts @@ -569,3 +569,24 @@ export function getInheritedFromDelegate(field: DataModelField) { const foundBase = bases.findLast((base) => base.fields.some((f) => f.name === field.name) && isDelegateModel(base)); return foundBase; } + +/** + * Gets the inheritance chain from "from" to "to", excluding them. + */ +export function getInheritanceChain(from: DataModel, to: DataModel): DataModel[] | undefined { + if (from === to) { + return []; + } + + for (const base of from.superTypes) { + if (base.ref === to) { + return []; + } + const path = getInheritanceChain(base.ref!, to); + if (path) { + return [base.ref as DataModel, ...path]; + } + } + + return undefined; +} diff --git a/tests/regression/tests/issue-1786.test.ts b/tests/regression/tests/issue-1786.test.ts new file mode 100644 index 000000000..ae37297de --- /dev/null +++ b/tests/regression/tests/issue-1786.test.ts @@ -0,0 +1,48 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1786', () => { + it('regression', async () => { + await loadSchema( + ` + model User { + id String @id @default(cuid()) + email String @unique @email @length(6, 32) + password String @password @omit + contents Content[] + + // everybody can signup + @@allow('create', true) + + // full access by self + @@allow('all', auth() == this) + } + + abstract model BaseContent { + published Boolean @default(false) + + @@index([published]) + } + + model Content extends BaseContent { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + owner User @relation(fields: [ownerId], references: [id]) + ownerId String + contentType String + + @@delegate(contentType) + } + + model Post extends Content { + title String + } + + model Video extends Content { + name String + duration Int + } + ` + ); + }); +});