Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(polymorphism): relation name disambiguation #1107

Merged
merged 1 commit into from
Mar 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ export default class DataModelValidator implements AstValidator<DataModel> {
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) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/schema/src/plugins/enhancer/enhance/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 55 additions & 0 deletions packages/schema/src/plugins/prisma/schema-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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),
]);
Expand All @@ -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) {
Expand Down
11 changes: 7 additions & 4 deletions packages/schema/src/utils/ast-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});
});
Loading