Skip to content

Commit

Permalink
fix(polymorphism): relation name disambiguation (#1107)
Browse files Browse the repository at this point in the history
  • Loading branch information
ymc9 authored Mar 9, 2024
1 parent d11d4ba commit 9f9d277
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 6 deletions.
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();
});
});

0 comments on commit 9f9d277

Please sign in to comment.