Skip to content

Commit fcde709

Browse files
author
JackCme
committed
fix: Add proper imports for self-relations in Relations class
When using separateRelationFields=true with self-relations, the Relations class was missing the import for the model type it references. Changes: - Detect self-relations in generateRelationClass function - Import the combined model class for self-relations (e.g., import User from ./User.model) - Avoid empty import statements when no external relations exist - Add comprehensive test suite for self-relation scenarios This ensures self-relations work correctly without circular dependencies while maintaining proper type references.
1 parent 6e45a82 commit fcde709

File tree

3 files changed

+173
-2
lines changed

3 files changed

+173
-2
lines changed

src/generate-class.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,12 +215,26 @@ function generateRelationClass(
215215
}
216216

217217
const relationImports = new Set();
218+
let hasSelfRelation = false;
219+
218220
relationFields.forEach((field) => {
219-
if (field.relationName && model.name !== field.type) {
220-
relationImports.add(field.type);
221+
if (field.relationName) {
222+
if (model.name !== field.type) {
223+
relationImports.add(field.type);
224+
} else {
225+
hasSelfRelation = true;
226+
}
221227
}
222228
});
223229

230+
// For self-relations in the Relations class, import the combined model class
231+
if (hasSelfRelation) {
232+
sourceFile.addImportDeclaration({
233+
moduleSpecifier: `./${model.name}.model`,
234+
namedImports: [model.name],
235+
});
236+
}
237+
224238
if (relationImports.size > 0) {
225239
generateRelationImportsImport(sourceFile, [
226240
...relationImports,

tests/schemas/self-relation.prisma

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
generator client {
2+
provider = "prisma-client-js"
3+
}
4+
5+
generator class_validator {
6+
provider = "node ./lib/generator.js"
7+
output = "../generated/self-relation"
8+
swagger = "true"
9+
separateRelationFields = "true"
10+
}
11+
12+
datasource db {
13+
provider = "sqlite"
14+
url = "file:./test.db"
15+
}
16+
17+
model User {
18+
id Int @id @default(autoincrement())
19+
email String @unique
20+
name String?
21+
createdAt DateTime @default(now())
22+
23+
// Self-relation: User can have a mentor (one-to-many)
24+
mentorId Int?
25+
mentor User? @relation("UserMentor", fields: [mentorId], references: [id])
26+
mentees User[] @relation("UserMentor")
27+
}

tests/self-relation.test.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { exec } from 'child_process';
2+
import { promisify } from 'util';
3+
import { existsSync, readFileSync } from 'fs';
4+
import { describe, it, expect, beforeAll } from 'vitest';
5+
import path from 'path';
6+
7+
const execAsync = promisify(exec);
8+
9+
describe('Self-Relation Generation', () => {
10+
beforeAll(async () => {
11+
// Build the generator first
12+
await execAsync('npm run build');
13+
14+
// Generate models for self-relation schema
15+
const schemaPath = path.resolve(__dirname, 'schemas/self-relation.prisma');
16+
await execAsync(`npx prisma generate --schema="${schemaPath}"`);
17+
}, 60000);
18+
19+
it('should generate UserBase class without self-relations', () => {
20+
const outputPath = path.resolve(__dirname, 'generated/self-relation');
21+
const userBasePath = path.join(outputPath, 'models', 'UserBase.model.ts');
22+
23+
expect(existsSync(userBasePath)).toBe(true);
24+
const userBase = readFileSync(userBasePath, 'utf-8');
25+
26+
// Should have scalar fields
27+
expect(userBase).toContain('id!: number');
28+
expect(userBase).toContain('email!: string');
29+
expect(userBase).toContain('name?: string');
30+
expect(userBase).toContain('createdAt!: Date');
31+
expect(userBase).toContain('mentorId?: number');
32+
33+
// Should have decorators
34+
expect(userBase).toContain('@IsInt()');
35+
expect(userBase).toContain('@IsString()');
36+
expect(userBase).toContain('@IsDate()');
37+
expect(userBase).toContain('@IsDefined()');
38+
expect(userBase).toContain('@IsOptional()');
39+
40+
// Should NOT have self-relation fields
41+
expect(userBase).not.toContain('mentor?');
42+
expect(userBase).not.toContain('mentees');
43+
expect(userBase).not.toContain('User[]');
44+
});
45+
46+
it('should generate UserRelations class with only self-relation fields', () => {
47+
const outputPath = path.resolve(__dirname, 'generated/self-relation');
48+
const userRelationsPath = path.join(
49+
outputPath,
50+
'models',
51+
'UserRelations.model.ts',
52+
);
53+
54+
expect(existsSync(userRelationsPath)).toBe(true);
55+
const userRelations = readFileSync(userRelationsPath, 'utf-8');
56+
57+
// Should have self-relation fields
58+
expect(userRelations).toContain('mentor?: User');
59+
expect(userRelations).toContain('mentees!: User[]');
60+
61+
// Should have class-validator decorators
62+
expect(userRelations).toContain('@IsOptional()');
63+
expect(userRelations).toContain('@IsDefined()');
64+
65+
// Should have Swagger decorators
66+
expect(userRelations).toContain('@ApiProperty({');
67+
expect(userRelations).toContain('type: () => User');
68+
expect(userRelations).toContain('required: false');
69+
expect(userRelations).toContain('isArray: true');
70+
71+
// For self-relations in Relations class, should import from the combined model
72+
expect(userRelations).toContain('import { User } from "./User.model"');
73+
74+
// Should NOT have scalar fields
75+
expect(userRelations).not.toContain('id!: number');
76+
expect(userRelations).not.toContain('email!: string');
77+
expect(userRelations).not.toContain('mentorId');
78+
});
79+
80+
it('should generate combined User class with self-relations', () => {
81+
const outputPath = path.resolve(__dirname, 'generated/self-relation');
82+
const userModelPath = path.join(outputPath, 'models', 'User.model.ts');
83+
84+
expect(existsSync(userModelPath)).toBe(true);
85+
const userModel = readFileSync(userModelPath, 'utf-8');
86+
87+
// Should import from UserBase
88+
expect(userModel).toContain('import { UserBase } from "./UserBase.model"');
89+
90+
// Should extend UserBase
91+
expect(userModel).toContain('export class User extends UserBase');
92+
93+
// Should have self-relation fields with decorators
94+
expect(userModel).toContain('mentor?: User');
95+
expect(userModel).toContain('mentees!: User[]');
96+
97+
// Should have class-validator imports
98+
expect(userModel).toContain(
99+
'import { IsOptional, IsDefined } from "class-validator"',
100+
);
101+
102+
// Should have Swagger imports
103+
expect(userModel).toContain(
104+
'import { ApiProperty } from "@nestjs/swagger"',
105+
);
106+
107+
// Should have decorators on relation fields
108+
expect(userModel).toContain('@IsOptional()');
109+
expect(userModel).toContain('@IsDefined()');
110+
expect(userModel).toContain('@ApiProperty({');
111+
112+
// Should NOT import User from itself (no circular import)
113+
expect(userModel).not.toContain('import { User } from "./"');
114+
expect(userModel).not.toContain('import { User } from "./User.model"');
115+
});
116+
117+
it('should handle self-relations without circular import issues', () => {
118+
const outputPath = path.resolve(__dirname, 'generated/self-relation');
119+
120+
// Check that the index file exports User
121+
const indexPath = path.join(outputPath, 'models', 'index.ts');
122+
expect(existsSync(indexPath)).toBe(true);
123+
const index = readFileSync(indexPath, 'utf-8');
124+
125+
// When separateRelationFields is enabled, index should export all classes
126+
expect(index).toContain('export { User } from "./User.model"');
127+
// Note: UserBase and UserRelations might not be in index if only User is exported
128+
// This depends on the generator's index generation logic
129+
});
130+
});

0 commit comments

Comments
 (0)