Skip to content

Commit 105e05b

Browse files
authored
Merge pull request #27 from JackCme/master
Fix/Relation model class and Self relation class missing imports
2 parents b87a3bc + fcde709 commit 105e05b

File tree

4 files changed

+325
-5
lines changed

4 files changed

+325
-5
lines changed

src/generate-class.ts

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -215,15 +215,31 @@ 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

224-
generateRelationImportsImport(sourceFile, [
225-
...relationImports,
226-
] as Array<string>);
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+
238+
if (relationImports.size > 0) {
239+
generateRelationImportsImport(sourceFile, [
240+
...relationImports,
241+
] as Array<string>);
242+
}
227243

228244
sourceFile.addClass({
229245
name: `${model.name}Relations`,
@@ -264,6 +280,31 @@ function generateCombinedClass(
264280
namedImports: [`${model.name}Base`],
265281
});
266282

283+
// Add class validator imports for relation fields
284+
const validatorImports = [
285+
...new Set(
286+
relationFields
287+
.map((field) => getDecoratorsImportsByType(field))
288+
.flatMap((item) => item),
289+
),
290+
];
291+
292+
if (validatorImports.length > 0) {
293+
generateClassValidatorImport(sourceFile, validatorImports as Array<string>);
294+
}
295+
296+
// Add Swagger imports if enabled
297+
if (
298+
config.swagger &&
299+
relationFields.length > 0 &&
300+
shouldImportSwagger(relationFields as PrismaDMMF.Field[])
301+
) {
302+
const swaggerImports = getSwaggerImportsByType(
303+
relationFields as PrismaDMMF.Field[],
304+
);
305+
generateSwaggerImport(sourceFile, swaggerImports);
306+
}
307+
267308
// Import relation types for the combined class
268309
const relationImports = new Set();
269310
relationFields.forEach((field) => {
@@ -293,6 +334,7 @@ function generateCombinedClass(
293334
hasExclamationToken: field.isRequired,
294335
hasQuestionToken: !field.isRequired,
295336
trailingTrivia: '\r\n',
337+
decorators: getDecoratorsByFieldType(field, config.swagger),
296338
};
297339
},
298340
),

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+
});

tests/swagger-generation.test.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,124 @@ describe('Swagger Generation', () => {
7070
expect(userModel).toContain('@ApiProperty({');
7171
});
7272
});
73+
74+
describe('Combined Class with separateRelationFields', () => {
75+
beforeAll(async () => {
76+
// Build the generator first
77+
await execAsync('npm run build');
78+
79+
// Generate models for full-features schema (has both swagger and separateRelationFields)
80+
const schemaPath = path.resolve(__dirname, 'schemas/full-features.prisma');
81+
await execAsync(`npx prisma generate --schema="${schemaPath}"`);
82+
}, 60000);
83+
84+
it('should generate combined class with class-validator and Swagger decorators for relations', () => {
85+
const outputPath = path.resolve(__dirname, 'generated/full-features');
86+
87+
// Check User combined class
88+
const userModelPath = path.join(outputPath, 'models', 'User.model.ts');
89+
const userModel = readFileSync(userModelPath, 'utf-8');
90+
91+
// Check that it imports from UserBase
92+
expect(userModel).toContain('import { UserBase } from "./UserBase.model"');
93+
94+
// Check for class-validator imports
95+
expect(userModel).toContain('import { IsDefined } from "class-validator"');
96+
97+
// Check for Swagger imports
98+
expect(userModel).toContain(
99+
'import { ApiProperty } from "@nestjs/swagger"',
100+
);
101+
102+
// Check for relation type imports (from index file)
103+
expect(userModel).toContain('import { Post } from "./"');
104+
105+
// Check that class extends UserBase
106+
expect(userModel).toContain('export class User extends UserBase');
107+
108+
// Check for relation field with decorators
109+
expect(userModel).toContain('posts!: Post[]');
110+
expect(userModel).toContain('@IsDefined()');
111+
expect(userModel).toContain(
112+
'@ApiProperty({ isArray: true, type: () => Post })',
113+
);
114+
});
115+
116+
it('should generate Post combined class with decorators for author relation', () => {
117+
const outputPath = path.resolve(__dirname, 'generated/full-features');
118+
119+
// Check Post combined class
120+
const postModelPath = path.join(outputPath, 'models', 'Post.model.ts');
121+
const postModel = readFileSync(postModelPath, 'utf-8');
122+
123+
// Check that it imports from PostBase
124+
expect(postModel).toContain('import { PostBase } from "./PostBase.model"');
125+
126+
// Check for class-validator imports for optional relation
127+
expect(postModel).toContain('import { IsOptional } from "class-validator"');
128+
129+
// Check for Swagger imports
130+
expect(postModel).toContain(
131+
'import { ApiProperty } from "@nestjs/swagger"',
132+
);
133+
134+
// Check for relation type imports (from index file)
135+
expect(postModel).toContain('import { User } from "./"');
136+
137+
// Check that class extends PostBase
138+
expect(postModel).toContain('export class Post extends PostBase');
139+
140+
// Check for optional relation field with decorators
141+
expect(postModel).toContain('author?: User');
142+
expect(postModel).toContain('@IsOptional()');
143+
expect(postModel).toContain(
144+
'@ApiProperty({ required: false, type: () => User })',
145+
);
146+
});
147+
148+
it('should generate base classes without relation fields', () => {
149+
const outputPath = path.resolve(__dirname, 'generated/full-features');
150+
151+
// Check UserBase class
152+
const userBasePath = path.join(outputPath, 'models', 'UserBase.model.ts');
153+
const userBase = readFileSync(userBasePath, 'utf-8');
154+
155+
// Should have scalar fields with decorators
156+
expect(userBase).toContain('@IsInt()');
157+
expect(userBase).toContain('@IsString()');
158+
expect(userBase).toContain('@ApiProperty({');
159+
expect(userBase).toContain('id!: number');
160+
expect(userBase).toContain('email!: string');
161+
expect(userBase).toContain('name?: string');
162+
163+
// Should NOT have relation fields
164+
expect(userBase).not.toContain('posts');
165+
expect(userBase).not.toContain('Post[]');
166+
});
167+
168+
it('should generate relation classes with only relation fields', () => {
169+
const outputPath = path.resolve(__dirname, 'generated/full-features');
170+
171+
// Check UserRelations class
172+
const userRelationsPath = path.join(
173+
outputPath,
174+
'models',
175+
'UserRelations.model.ts',
176+
);
177+
178+
// Check if file exists
179+
expect(existsSync(userRelationsPath)).toBe(true);
180+
181+
const userRelations = readFileSync(userRelationsPath, 'utf-8');
182+
183+
// Should have relation field with decorators
184+
expect(userRelations).toContain('export class UserRelations');
185+
expect(userRelations).toContain('posts!: Post[]');
186+
expect(userRelations).toContain('@IsDefined()');
187+
expect(userRelations).toContain('@ApiProperty({');
188+
189+
// Should NOT have scalar fields
190+
expect(userRelations).not.toContain('id!: number');
191+
expect(userRelations).not.toContain('email!: string');
192+
});
193+
});

0 commit comments

Comments
 (0)