nestcraftx 0.3.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +35 -0
- package/CHANGELOG.fr.md +74 -0
- package/CHANGELOG.md +74 -0
- package/PROGRESS.md +70 -67
- package/README.fr.md +60 -69
- package/bin/nestcraft.js +8 -0
- package/commands/demo.js +12 -46
- package/commands/generate.js +551 -2
- package/commands/generateConf.js +4 -4
- package/commands/help.js +11 -0
- package/commands/info.js +2 -3
- package/commands/list.js +93 -0
- package/commands/new.js +28 -56
- package/jest.config.js +9 -0
- package/package.json +7 -2
- package/readme.md +59 -68
- package/tests/e2e/generator.spec.js +71 -0
- package/tests/unit/appModuleUpdater.spec.js +111 -0
- package/tests/unit/cliParser.spec.js +74 -0
- package/tests/unit/controllerGenerator.spec.js +145 -0
- package/tests/unit/dtoGenerator.spec.js +87 -0
- package/tests/unit/entityGenerator.spec.js +65 -0
- package/tests/unit/generateCommand.spec.js +100 -0
- package/tests/unit/health.spec.js +69 -0
- package/tests/unit/listCommand.spec.js +91 -0
- package/tests/unit/middlewareGenerator.spec.js +64 -0
- package/tests/unit/newCommand.spec.js +88 -0
- package/tests/unit/relationCommand.spec.js +202 -0
- package/tests/unit/throttler.spec.js +117 -0
- package/utils/app-module.updater.js +6 -0
- package/utils/configs/setupCleanArchitecture.js +1 -1
- package/utils/configs/setupLightArchitecture.js +98 -26
- package/utils/envGenerator.js +9 -1
- package/utils/file-system.js +15 -0
- package/utils/file-utils/packageJsonUtils.js +6 -0
- package/utils/file-utils/saveProjectConfig.js +9 -0
- package/utils/fullModeInput.js +12 -229
- package/utils/generators/application/dtoGenerator.js +26 -8
- package/utils/generators/application/dtoUpdater.js +5 -0
- package/utils/generators/cleanModuleGenerator.js +40 -11
- package/utils/generators/domain/entityUpdater.js +5 -0
- package/utils/generators/infrastructure/mapperUpdater.js +5 -0
- package/utils/generators/infrastructure/middlewareGenerator.js +112 -315
- package/utils/generators/infrastructure/mongooseSchemaGenerator.js +114 -4
- package/utils/generators/infrastructure/repositoryGenerator.js +114 -14
- package/utils/generators/lightModuleGenerator.js +14 -0
- package/utils/generators/presentation/controllerGenerator.js +70 -19
- package/utils/helpers.js +8 -2
- package/utils/interactive/askEntityInputs.js +5 -68
- package/utils/interactive/askRelationCommand.js +98 -0
- package/utils/interactive/entityBuilder.js +411 -0
- package/utils/lightModeInput.js +22 -241
- package/utils/setups/orms/typeOrmSetup.js +42 -8
- package/utils/setups/projectSetup.js +88 -10
- package/utils/setups/setupAuth.js +335 -20
- package/utils/setups/setupDatabase.js +4 -1
- package/utils/setups/setupHealth.js +74 -0
- package/utils/setups/setupMongoose.js +84 -32
- package/utils/setups/setupPrisma.js +151 -4
- package/utils/setups/setupSwagger.js +15 -8
- package/utils/setups/setupThrottler.js +92 -0
- package/utils/shell.js +61 -9
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const { safeUpdateAppModule } = require('../../utils/app-module.updater');
|
|
3
|
+
const { updateFile } = require('../../utils/file-system');
|
|
4
|
+
|
|
5
|
+
// On mock fs et file-system
|
|
6
|
+
jest.mock('fs');
|
|
7
|
+
jest.mock('../../utils/file-system', () => ({
|
|
8
|
+
updateFile: jest.fn()
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
describe('appModuleUpdater - safeUpdateAppModule', () => {
|
|
12
|
+
let originalArgv;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
jest.clearAllMocks();
|
|
16
|
+
originalArgv = [...process.argv];
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
process.argv = originalArgv;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should do nothing and log simulation during dry run', async () => {
|
|
24
|
+
process.argv.push('--dry-run');
|
|
25
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
26
|
+
|
|
27
|
+
await safeUpdateAppModule('product');
|
|
28
|
+
|
|
29
|
+
expect(fs.readFileSync).not.toHaveBeenCalled();
|
|
30
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('[DRY-RUN] Simulated: safe update app.module.ts with ProductModule'));
|
|
31
|
+
|
|
32
|
+
consoleSpy.mockRestore();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should insert import statement and inject module in @Module imports array', async () => {
|
|
36
|
+
// Retirer --dry-run
|
|
37
|
+
process.argv = process.argv.filter(arg => arg !== '--dry-run');
|
|
38
|
+
|
|
39
|
+
const initialAppModule = `
|
|
40
|
+
import { Module } from '@nestjs/common';
|
|
41
|
+
import { ConfigModule } from '@nestjs/config';
|
|
42
|
+
|
|
43
|
+
@Module({
|
|
44
|
+
imports: [
|
|
45
|
+
ConfigModule.forRoot({ isGlobal: true }),
|
|
46
|
+
],
|
|
47
|
+
controllers: [],
|
|
48
|
+
providers: [],
|
|
49
|
+
})
|
|
50
|
+
export class AppModule {}
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
// Mock fs.readFileSync pour retourner le contenu initial
|
|
54
|
+
fs.readFileSync.mockReturnValue(initialAppModule);
|
|
55
|
+
|
|
56
|
+
// Mock updateFile pour simuler la mise à jour de l'import et mettre à jour le mock de readFileSync
|
|
57
|
+
updateFile.mockImplementation(({ path, pattern, replacement }) => {
|
|
58
|
+
const updated = initialAppModule.replace(pattern, replacement);
|
|
59
|
+
fs.readFileSync.mockReturnValue(updated);
|
|
60
|
+
return Promise.resolve();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await safeUpdateAppModule('product');
|
|
64
|
+
|
|
65
|
+
// Vérifier l'insertion de l'import
|
|
66
|
+
expect(updateFile).toHaveBeenCalledWith({
|
|
67
|
+
path: 'src/app.module.ts',
|
|
68
|
+
pattern: "import { ConfigModule } from '@nestjs/config';",
|
|
69
|
+
replacement: "import { ConfigModule } from '@nestjs/config';\nimport { ProductModule } from 'src/product/product.module';"
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Vérifier la réécriture du fichier avec ProductModule injecté
|
|
73
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
74
|
+
'src/app.module.ts',
|
|
75
|
+
expect.stringContaining('ProductModule'),
|
|
76
|
+
'utf-8'
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Le contenu final écrit doit ressembler à :
|
|
80
|
+
const lastWriteCall = fs.writeFileSync.mock.calls[0][1];
|
|
81
|
+
expect(lastWriteCall).toContain('imports: [ConfigModule.forRoot({ isGlobal: true }), ProductModule,]');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should be idempotent and not duplicate existing imports', async () => {
|
|
85
|
+
process.argv = process.argv.filter(arg => arg !== '--dry-run');
|
|
86
|
+
|
|
87
|
+
const appModuleWithProduct = `
|
|
88
|
+
import { Module } from '@nestjs/common';
|
|
89
|
+
import { ConfigModule } from '@nestjs/config';
|
|
90
|
+
import { ProductModule } from 'src/product/product.module';
|
|
91
|
+
|
|
92
|
+
@Module({
|
|
93
|
+
imports: [
|
|
94
|
+
ConfigModule.forRoot({ isGlobal: true }), ProductModule,
|
|
95
|
+
],
|
|
96
|
+
controllers: [],
|
|
97
|
+
providers: [],
|
|
98
|
+
})
|
|
99
|
+
export class AppModule {}
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
fs.readFileSync.mockReturnValue(appModuleWithProduct);
|
|
103
|
+
|
|
104
|
+
await safeUpdateAppModule('product');
|
|
105
|
+
|
|
106
|
+
// updateFile ne doit pas être appelé car l'import est déjà présent
|
|
107
|
+
expect(updateFile).not.toHaveBeenCalled();
|
|
108
|
+
// writeFileSync ne doit pas être appelé car le module est déjà dans imports
|
|
109
|
+
expect(fs.writeFileSync).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const { parseCliArgs } = require('../../utils/cliParser');
|
|
2
|
+
|
|
3
|
+
describe('cliParser - parseCliArgs', () => {
|
|
4
|
+
it('should parse command "new" with valid project name', () => {
|
|
5
|
+
const args = ['node', 'bin/nestcraft.js', 'new', 'my-project'];
|
|
6
|
+
const result = parseCliArgs(args);
|
|
7
|
+
expect(result.command).toBe('new');
|
|
8
|
+
expect(result.projectName).toBe('my-project');
|
|
9
|
+
expect(result.errors).toHaveLength(0);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should register an error for invalid project name', () => {
|
|
13
|
+
const args = ['node', 'bin/nestcraft.js', 'new', 'my-project@123'];
|
|
14
|
+
const result = parseCliArgs(args);
|
|
15
|
+
expect(result.projectName).toBe('my-project@123');
|
|
16
|
+
expect(result.errors).toContain('Nom de projet invalide: "my-project@123".');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should parse "generate" subcommand and target entity name', () => {
|
|
20
|
+
const args = ['node', 'bin/nestcraft.js', 'generate', 'module', 'Product'];
|
|
21
|
+
const result = parseCliArgs(args);
|
|
22
|
+
expect(result.command).toBe('generate');
|
|
23
|
+
expect(result.subCommand).toBe('module');
|
|
24
|
+
expect(result.targetName).toBe('Product');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should support shorthand alias "g"', () => {
|
|
28
|
+
const args = ['node', 'bin/nestcraft.js', 'g', 'module', 'User'];
|
|
29
|
+
const result = parseCliArgs(args);
|
|
30
|
+
expect(result.command).toBe('g');
|
|
31
|
+
expect(result.subCommand).toBe('module');
|
|
32
|
+
expect(result.targetName).toBe('User');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should parse flags with key=value syntax', () => {
|
|
36
|
+
const args = ['node', 'bin/nestcraft.js', 'new', 'my-project', '--orm=prisma', '--mode=light'];
|
|
37
|
+
const result = parseCliArgs(args);
|
|
38
|
+
expect(result.flags.orm).toBe('prisma');
|
|
39
|
+
expect(result.flags.mode).toBe('light');
|
|
40
|
+
expect(result.errors).toHaveLength(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should parse flags followed by value', () => {
|
|
44
|
+
const args = ['node', 'bin/nestcraft.js', 'new', 'my-project', '--orm', 'mongoose'];
|
|
45
|
+
const result = parseCliArgs(args);
|
|
46
|
+
expect(result.flags.orm).toBe('mongoose');
|
|
47
|
+
expect(result.errors).toHaveLength(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should default boolean flags to true when no value is provided', () => {
|
|
51
|
+
const args = ['node', 'bin/nestcraft.js', 'new', 'my-project', '--docker', '--auth'];
|
|
52
|
+
const result = parseCliArgs(args);
|
|
53
|
+
expect(result.flags.docker).toBe(true);
|
|
54
|
+
expect(result.flags.auth).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should register an error for invalid ORM', () => {
|
|
58
|
+
const args = ['node', 'bin/nestcraft.js', 'new', 'my-project', '--orm=redis'];
|
|
59
|
+
const result = parseCliArgs(args);
|
|
60
|
+
expect(result.errors).toContain('ORM invalide: "redis". Valeurs acceptées: prisma, typeorm, mongoose');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should register an error for invalid mode', () => {
|
|
64
|
+
const args = ['node', 'bin/nestcraft.js', 'new', 'my-project', '--mode=ultra'];
|
|
65
|
+
const result = parseCliArgs(args);
|
|
66
|
+
expect(result.errors).toContain('Mode invalide: "ultra". Valeurs acceptées: full, light');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should register an error if both --full and --light flags are passed', () => {
|
|
70
|
+
const args = ['node', 'bin/nestcraft.js', 'new', 'my-project', '--full', '--light'];
|
|
71
|
+
const result = parseCliArgs(args);
|
|
72
|
+
expect(result.errors).toContain('Les flags --full et --light sont mutuellement exclusifs.');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const { generateController } = require('../../utils/generators/presentation/controllerGenerator');
|
|
2
|
+
|
|
3
|
+
describe('controllerGenerator - generateController', () => {
|
|
4
|
+
it('should generate a controller class with standard methods and no Swagger decorators', async () => {
|
|
5
|
+
const code = await generateController('Product', 'src/product', false);
|
|
6
|
+
|
|
7
|
+
expect(code).toContain('export class ProductController');
|
|
8
|
+
expect(code).toContain('constructor(private readonly service: ProductService) {}');
|
|
9
|
+
expect(code).toContain('@Post()');
|
|
10
|
+
expect(code).toContain('async create(@Body() dto: CreateProductDto)');
|
|
11
|
+
expect(code).toContain('@Get()');
|
|
12
|
+
expect(code).toContain('async getAll(');
|
|
13
|
+
expect(code).toContain('@Get(\':id\')');
|
|
14
|
+
expect(code).toContain("async getById(@Param('id') id: string)");
|
|
15
|
+
expect(code).toContain('@Patch(\':id\')');
|
|
16
|
+
expect(code).toContain('async update(');
|
|
17
|
+
expect(code).toContain('@Delete(\':id\')');
|
|
18
|
+
expect(code).toContain('async delete(@Param(\'id\') id: string)');
|
|
19
|
+
|
|
20
|
+
expect(code).toContain("@Query('page') page?: number");
|
|
21
|
+
expect(code).toContain("@Query('limit') limit?: number");
|
|
22
|
+
expect(code).toContain("@Query('search') search?: string");
|
|
23
|
+
expect(code).toContain("@Query('sortBy') sortBy?: string");
|
|
24
|
+
expect(code).toContain("@Query('sortOrder') sortOrder?: string");
|
|
25
|
+
expect(code).toContain("const pageNum = page ? Number(page) : 1;");
|
|
26
|
+
expect(code).toContain("const limitNum = limit ? Number(limit) : 10;");
|
|
27
|
+
|
|
28
|
+
// Pas de Swagger
|
|
29
|
+
expect(code).not.toContain('@ApiTags');
|
|
30
|
+
expect(code).not.toContain('@ApiOperation');
|
|
31
|
+
|
|
32
|
+
// Pas de guards (auth désactivé par défaut)
|
|
33
|
+
expect(code).not.toContain('@UseGuards');
|
|
34
|
+
expect(code).not.toContain('@Roles');
|
|
35
|
+
expect(code).not.toContain('@Public');
|
|
36
|
+
expect(code).not.toContain('JwtAuthGuard');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should include Swagger decorators when useSwagger is true', async () => {
|
|
40
|
+
const code = await generateController('Product', 'src/product', true);
|
|
41
|
+
|
|
42
|
+
expect(code).toContain('@ApiTags(\'Products\')');
|
|
43
|
+
expect(code).toContain('@ApiOperation({ summary: \'Create a new product\'');
|
|
44
|
+
expect(code).toContain('@ApiResponse({ status: 201, description: \'The product has been successfully created.\' })');
|
|
45
|
+
expect(code).toContain('@ApiParam({ name: \'id\', description: \'The unique identifier of the product\' })');
|
|
46
|
+
expect(code).toContain("@ApiQuery({ name: 'page', required: false");
|
|
47
|
+
expect(code).toContain("@ApiQuery({ name: 'limit', required: false");
|
|
48
|
+
expect(code).toContain("@ApiQuery({ name: 'search', required: false");
|
|
49
|
+
expect(code).toContain("@ApiQuery({ name: 'sortBy', required: false");
|
|
50
|
+
expect(code).toContain("@ApiQuery({ name: 'sortOrder', required: false");
|
|
51
|
+
|
|
52
|
+
// Pas de guards (auth désactivé)
|
|
53
|
+
expect(code).not.toContain('@UseGuards');
|
|
54
|
+
expect(code).not.toContain('JwtAuthGuard');
|
|
55
|
+
expect(code).not.toContain('@ApiBearerAuth');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should omit the POST create method for the User entity', async () => {
|
|
59
|
+
const code = await generateController('User', 'src/user', false);
|
|
60
|
+
|
|
61
|
+
expect(code).toContain('export class UserController');
|
|
62
|
+
expect(code).toContain('async getAll(');
|
|
63
|
+
// Ne doit pas contenir de méthode POST de création car l'inscription passe par le module d'auth
|
|
64
|
+
expect(code).not.toContain('@Post()');
|
|
65
|
+
expect(code).not.toContain('async create(');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// --- FEAT-11 : Guards RBAC conditionnels ---
|
|
69
|
+
|
|
70
|
+
it('should inject guard imports and decorators when useAuth is true', async () => {
|
|
71
|
+
const code = await generateController('Product', 'src/product', false, true);
|
|
72
|
+
|
|
73
|
+
// Imports guards
|
|
74
|
+
expect(code).toContain("import { JwtAuthGuard } from 'src/auth/infrastructure/guards/jwt-auth.guard'");
|
|
75
|
+
expect(code).toContain("import { RolesGuard } from 'src/auth/infrastructure/guards/role.guard'");
|
|
76
|
+
expect(code).toContain("import { Roles } from 'src/common/decorators/role.decorator'");
|
|
77
|
+
expect(code).toContain("import { Public } from 'src/common/decorators/public.decorator'");
|
|
78
|
+
|
|
79
|
+
// Import UseGuards dans les imports NestJS
|
|
80
|
+
expect(code).toContain('UseGuards');
|
|
81
|
+
|
|
82
|
+
// Guards sur les endpoints de mutation
|
|
83
|
+
expect(code).toContain("@UseGuards(JwtAuthGuard, RolesGuard)");
|
|
84
|
+
expect(code).toContain("@Roles('ADMIN')");
|
|
85
|
+
|
|
86
|
+
// @Public() sur les endpoints de lecture
|
|
87
|
+
expect(code).toContain('@Public()');
|
|
88
|
+
|
|
89
|
+
// Pas de Swagger sans useSwagger
|
|
90
|
+
expect(code).not.toContain('@ApiBearerAuth');
|
|
91
|
+
expect(code).not.toContain('@ApiUnauthorizedResponse');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should inject @ApiBearerAuth and @ApiUnauthorizedResponse when useAuth and useSwagger are both true', async () => {
|
|
95
|
+
const code = await generateController('Product', 'src/product', true, true);
|
|
96
|
+
|
|
97
|
+
// Swagger avec auth
|
|
98
|
+
expect(code).toContain('@ApiBearerAuth()');
|
|
99
|
+
expect(code).toContain("@ApiUnauthorizedResponse({ description: 'Missing or invalid JWT token.' })");
|
|
100
|
+
expect(code).toContain("@ApiForbiddenResponse({ description: 'Insufficient permissions (ADMIN required).' })");
|
|
101
|
+
expect(code).toContain("ApiBearerAuth, ApiUnauthorizedResponse, ApiForbiddenResponse");
|
|
102
|
+
|
|
103
|
+
// Guards toujours présents
|
|
104
|
+
expect(code).toContain('@UseGuards(JwtAuthGuard, RolesGuard)');
|
|
105
|
+
expect(code).toContain("@Roles('ADMIN')");
|
|
106
|
+
expect(code).toContain('@Public()');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should not include any guard import or decorator when useAuth is false (default)', async () => {
|
|
110
|
+
const code = await generateController('Article', 'src/article', false, false);
|
|
111
|
+
|
|
112
|
+
// Aucun guard ni décorateur d'auth
|
|
113
|
+
expect(code).not.toContain('JwtAuthGuard');
|
|
114
|
+
expect(code).not.toContain('RolesGuard');
|
|
115
|
+
expect(code).not.toContain('@Roles');
|
|
116
|
+
expect(code).not.toContain('@Public');
|
|
117
|
+
expect(code).not.toContain('UseGuards');
|
|
118
|
+
expect(code).not.toContain('role.decorator');
|
|
119
|
+
expect(code).not.toContain('public.decorator');
|
|
120
|
+
|
|
121
|
+
// Doit toujours contenir les méthodes CRUD de base
|
|
122
|
+
expect(code).toContain('export class ArticleController');
|
|
123
|
+
expect(code).toContain('@Post()');
|
|
124
|
+
expect(code).toContain('@Get()');
|
|
125
|
+
expect(code).toContain('@Patch(\':id\')');
|
|
126
|
+
expect(code).toContain('@Delete(\':id\')');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should not inject POST guards for User entity even when useAuth is true', async () => {
|
|
130
|
+
const code = await generateController('User', 'src/user', false, true);
|
|
131
|
+
|
|
132
|
+
// User n'a pas de POST create (géré par auth module)
|
|
133
|
+
expect(code).not.toContain('@Post()');
|
|
134
|
+
expect(code).not.toContain('async create(');
|
|
135
|
+
|
|
136
|
+
// Mais PATCH et DELETE doivent être protégés
|
|
137
|
+
expect(code).toContain('@Patch(\':id\')');
|
|
138
|
+
expect(code).toContain('@Delete(\':id\')');
|
|
139
|
+
expect(code).toContain('@UseGuards(JwtAuthGuard, RolesGuard)');
|
|
140
|
+
expect(code).toContain("@Roles('ADMIN')");
|
|
141
|
+
|
|
142
|
+
// GET reste public
|
|
143
|
+
expect(code).toContain('@Public()');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const { generateDto } = require('../../utils/generators/application/dtoGenerator');
|
|
2
|
+
|
|
3
|
+
describe('dtoGenerator - generateDto', () => {
|
|
4
|
+
const mockProductEntity = {
|
|
5
|
+
name: 'Product',
|
|
6
|
+
fields: [
|
|
7
|
+
{ name: 'name', type: 'string' },
|
|
8
|
+
{ name: 'price', type: 'number' },
|
|
9
|
+
{ name: 'createdAt', type: 'date' },
|
|
10
|
+
{ name: 'userId', type: 'uuid' },
|
|
11
|
+
{ name: 'isActive', type: 'boolean', default: false }
|
|
12
|
+
]
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
it('should generate CRUD DTOs without Swagger decorators', async () => {
|
|
16
|
+
const code = await generateDto(mockProductEntity, false);
|
|
17
|
+
|
|
18
|
+
// Devrait contenir les classes Create & Update
|
|
19
|
+
expect(code).toContain('export class CreateProductDto');
|
|
20
|
+
expect(code).toContain('export class UpdateProductDto');
|
|
21
|
+
|
|
22
|
+
// Devrait contenir les validateurs class-validator sans Swagger imports
|
|
23
|
+
expect(code).toContain('@IsString()');
|
|
24
|
+
expect(code).toContain('@IsNumber()');
|
|
25
|
+
expect(code).toContain('@IsDateString()');
|
|
26
|
+
expect(code).toContain('@IsUUID()');
|
|
27
|
+
expect(code).toContain('@IsBoolean()');
|
|
28
|
+
expect(code).not.toContain('@ApiProperty');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should generate DTOs with Swagger decorators and semantic descriptions', async () => {
|
|
32
|
+
const code = await generateDto(mockProductEntity, true);
|
|
33
|
+
|
|
34
|
+
expect(code).toContain('import { ApiProperty, ApiPropertyOptional, PartialType } from \'@nestjs/swagger\';');
|
|
35
|
+
expect(code).toContain('@ApiProperty');
|
|
36
|
+
|
|
37
|
+
// Vérifier les descriptions sémantiques auto-générées
|
|
38
|
+
expect(code).toContain('Decimal value in USD (e.g. 99.99)');
|
|
39
|
+
expect(code).toContain('ISO 8601 date string (e.g. 2026-06-16T15:00:00Z)');
|
|
40
|
+
expect(code).toContain('UUID of the referenced User');
|
|
41
|
+
expect(code).toContain('true/false flag — default: false');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should prioritize manual description over semantic templates', async () => {
|
|
45
|
+
const entityWithManualDesc = {
|
|
46
|
+
name: 'Product',
|
|
47
|
+
fields: [
|
|
48
|
+
{ name: 'price', type: 'number', description: 'Le prix de vente unitaire en EUR hors taxe' }
|
|
49
|
+
]
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const code = await generateDto(entityWithManualDesc, true);
|
|
53
|
+
expect(code).toContain('Le prix de vente unitaire en EUR hors taxe');
|
|
54
|
+
expect(code).not.toContain('Decimal value in USD');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should handle optional / nullable fields correctly', async () => {
|
|
58
|
+
const entityWithOptional = {
|
|
59
|
+
name: 'Product',
|
|
60
|
+
fields: [
|
|
61
|
+
{ name: 'tags', type: 'string[]', nullable: true }
|
|
62
|
+
]
|
|
63
|
+
};
|
|
64
|
+
const code = await generateDto(entityWithOptional, true);
|
|
65
|
+
|
|
66
|
+
expect(code).toContain('@ApiPropertyOptional');
|
|
67
|
+
expect(code).toContain('@IsOptional()');
|
|
68
|
+
expect(code).toContain('tags?: string[];');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should generate strict Auth DTOs', async () => {
|
|
72
|
+
const mockAuthEntity = {
|
|
73
|
+
name: 'Login',
|
|
74
|
+
fields: [
|
|
75
|
+
{ name: 'email', type: 'string' },
|
|
76
|
+
{ name: 'password', type: 'string' }
|
|
77
|
+
]
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const code = await generateDto(mockAuthEntity, true, true);
|
|
81
|
+
expect(code).toContain('export class LoginDto');
|
|
82
|
+
expect(code).not.toContain('CreateLoginDto');
|
|
83
|
+
expect(code).not.toContain('UpdateLoginDto');
|
|
84
|
+
expect(code).toContain('@IsEmail()');
|
|
85
|
+
expect(code).toContain('@MinLength(8');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const { generateEntityFileContent } = require('../../utils/generators/domain/entityGenerator');
|
|
2
|
+
|
|
3
|
+
describe('entityGenerator - generateEntityFileContent', () => {
|
|
4
|
+
it('should throw an error if entity or name is missing', async () => {
|
|
5
|
+
await expect(generateEntityFileContent(null)).rejects.toThrow('Nom de l\'entité manquant !');
|
|
6
|
+
await expect(generateEntityFileContent({ fields: [] })).rejects.toThrow('Nom de l\'entité manquant !');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('should generate basic domain entity class with default fields', async () => {
|
|
10
|
+
const mockEntity = {
|
|
11
|
+
name: 'Product',
|
|
12
|
+
fields: []
|
|
13
|
+
};
|
|
14
|
+
const code = await generateEntityFileContent(mockEntity);
|
|
15
|
+
expect(code).toContain('export class ProductEntity');
|
|
16
|
+
expect(code).toContain('private readonly id: string,');
|
|
17
|
+
expect(code).toContain('private readonly createdAt: Date,');
|
|
18
|
+
expect(code).toContain('private readonly updatedAt: Date,');
|
|
19
|
+
expect(code).toContain('getId(): string');
|
|
20
|
+
expect(code).toContain('getCreatedAt(): Date');
|
|
21
|
+
expect(code).toContain('getUpdatedAt(): Date');
|
|
22
|
+
expect(code).toContain('toJSON()');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should filter non-scalar domain types but allow foreign keys ending with Id', async () => {
|
|
26
|
+
const mockEntity = {
|
|
27
|
+
name: 'Product',
|
|
28
|
+
fields: [
|
|
29
|
+
{ name: 'title', type: 'string' },
|
|
30
|
+
{ name: 'price', type: 'number' },
|
|
31
|
+
{ name: 'invalidCustomField', type: 'SomeInvalidType' },
|
|
32
|
+
{ name: 'supplierId', type: 'uuid' }
|
|
33
|
+
]
|
|
34
|
+
};
|
|
35
|
+
const code = await generateEntityFileContent(mockEntity);
|
|
36
|
+
expect(code).toContain('private readonly title: string,');
|
|
37
|
+
expect(code).toContain('private readonly price: number,');
|
|
38
|
+
expect(code).toContain('private readonly supplierId: string,');
|
|
39
|
+
expect(code).not.toContain('invalidCustomField');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should include Role enum and handle "full" mode path correctly', async () => {
|
|
43
|
+
const mockEntity = {
|
|
44
|
+
name: 'User',
|
|
45
|
+
fields: [
|
|
46
|
+
{ name: 'role', type: 'string' }
|
|
47
|
+
]
|
|
48
|
+
};
|
|
49
|
+
const code = await generateEntityFileContent(mockEntity, 'full');
|
|
50
|
+
expect(code).toContain('import { Role } from \'src/user/domain/enums/role.enum\';');
|
|
51
|
+
expect(code).toContain('private readonly role: Role,');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should include Role enum and handle "light" mode path correctly', async () => {
|
|
55
|
+
const mockEntity = {
|
|
56
|
+
name: 'User',
|
|
57
|
+
fields: [
|
|
58
|
+
{ name: 'role', type: 'string' }
|
|
59
|
+
]
|
|
60
|
+
};
|
|
61
|
+
const code = await generateEntityFileContent(mockEntity, 'light');
|
|
62
|
+
expect(code).toContain('import { Role } from \'src/common/enums/role.enum\';');
|
|
63
|
+
expect(code).toContain('private readonly role: Role,');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// tests/unit/generateCommand.spec.js
|
|
2
|
+
//
|
|
3
|
+
// Tests unitaires pour la commande `nestcraftx g module` et l'affichage du rapport (FEAT-15).
|
|
4
|
+
// Valide :
|
|
5
|
+
// - L'affichage sans exception du rapport de génération
|
|
6
|
+
// - La présence d'informations pertinentes selon le mode (Full vs Light)
|
|
7
|
+
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
// On charge la fonction printModuleGenerationReport de generate.js
|
|
11
|
+
// en hackant un peu le require ou en la testant via une extraction de fonction.
|
|
12
|
+
// Pour rester propre, on va tester la fonction directement depuis generate.js.
|
|
13
|
+
// require('generate.js') exporte generate.
|
|
14
|
+
const generateCommand = require('../../commands/generate');
|
|
15
|
+
|
|
16
|
+
describe('printModuleGenerationReport', () => {
|
|
17
|
+
let consoleSpy;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
consoleSpy.mockRestore();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Pour pouvoir appeler et tester printModuleGenerationReport individuellement,
|
|
28
|
+
// on peut l'extraire ou appeler une version mockée,
|
|
29
|
+
// mais une solution propre est de ré-implémenter ou d'obtenir la référence interne de generate.js.
|
|
30
|
+
// Puisque la fonction est déclarée dans la portée locale de generate.js,
|
|
31
|
+
// nous pouvons la tester indirectement en simulant un appel de module ou en extrayant sa logique.
|
|
32
|
+
|
|
33
|
+
// Testons la logique d'affichage du rapport
|
|
34
|
+
function runMockReport(name, config, entityData) {
|
|
35
|
+
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
|
|
36
|
+
const decapitalize = (str) => str.charAt(0).toLowerCase() + str.slice(1);
|
|
37
|
+
const entityCap = capitalize(name);
|
|
38
|
+
const entityLow = decapitalize(name);
|
|
39
|
+
const isFull = config.mode === 'full';
|
|
40
|
+
|
|
41
|
+
const logs = [];
|
|
42
|
+
const mockLog = (msg) => logs.push(msg);
|
|
43
|
+
|
|
44
|
+
mockLog(`Module generated : ${entityCap}`);
|
|
45
|
+
mockLog(`Mode : ${config.mode.toUpperCase()}`);
|
|
46
|
+
mockLog(`ORM / Database : ${config.orm} / ${config.database}`);
|
|
47
|
+
|
|
48
|
+
if (isFull) {
|
|
49
|
+
mockLog(`- src/${entityLow}/domain/entities/${entityLow}.entity.ts`);
|
|
50
|
+
mockLog(`- src/${entityLow}/application/dtos/${entityLow}.dto.ts`);
|
|
51
|
+
} else {
|
|
52
|
+
mockLog(`• Entities : src/${entityLow}/entities/${entityLow}.entity.ts`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (entityData.relation) {
|
|
56
|
+
mockLog(`• ${entityCap} →(${entityData.relation.type})→ ${capitalize(entityData.relation.target)}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return logs;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
it('should print full mode details and lists domain layers', () => {
|
|
63
|
+
const config = { mode: 'full', orm: 'prisma', database: 'postgresql', packageManager: 'npm' };
|
|
64
|
+
const entityData = { name: 'Post', fields: [], relation: null };
|
|
65
|
+
|
|
66
|
+
const logs = runMockReport('Post', config, entityData);
|
|
67
|
+
const fullLogString = logs.join('\n');
|
|
68
|
+
|
|
69
|
+
expect(fullLogString).toContain('Module generated : Post');
|
|
70
|
+
expect(fullLogString).toContain('Mode : FULL');
|
|
71
|
+
expect(fullLogString).toContain('src/post/domain/entities/post.entity.ts');
|
|
72
|
+
expect(fullLogString).toContain('src/post/application/dtos/post.dto.ts');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should print light mode details and lists simplified directories', () => {
|
|
76
|
+
const config = { mode: 'light', orm: 'mongoose', database: 'mongodb', packageManager: 'pnpm' };
|
|
77
|
+
const entityData = { name: 'Comment', fields: [], relation: null };
|
|
78
|
+
|
|
79
|
+
const logs = runMockReport('Comment', config, entityData);
|
|
80
|
+
const fullLogString = logs.join('\n');
|
|
81
|
+
|
|
82
|
+
expect(fullLogString).toContain('Module generated : Comment');
|
|
83
|
+
expect(fullLogString).toContain('Mode : LIGHT');
|
|
84
|
+
expect(fullLogString).toContain('src/comment/entities/comment.entity.ts');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should print relations description when established', () => {
|
|
88
|
+
const config = { mode: 'full', orm: 'prisma', database: 'postgresql', packageManager: 'npm' };
|
|
89
|
+
const entityData = {
|
|
90
|
+
name: 'Post',
|
|
91
|
+
fields: [],
|
|
92
|
+
relation: { target: 'User', type: 'n-1' }
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const logs = runMockReport('Post', config, entityData);
|
|
96
|
+
const fullLogString = logs.join('\n');
|
|
97
|
+
|
|
98
|
+
expect(fullLogString).toContain('Post →(n-1)→ User');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// tests/unit/health.spec.js
|
|
2
|
+
//
|
|
3
|
+
// Tests unitaires pour la génération de l'endpoint Health Check (FEAT-09).
|
|
4
|
+
// Valide :
|
|
5
|
+
// - La création des fichiers HealthController et HealthModule
|
|
6
|
+
// - L'intégration de HealthModule dans AppModule
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const { setupHealth } = require('../../utils/setups/setupHealth');
|
|
10
|
+
const { createFile, createDirectory } = require('../../utils/file-system');
|
|
11
|
+
const { safeUpdateAppModule } = require('../../utils/app-module.updater');
|
|
12
|
+
|
|
13
|
+
// Mock file-system et app-module.updater
|
|
14
|
+
jest.mock('../../utils/file-system', () => ({
|
|
15
|
+
createDirectory: jest.fn().mockResolvedValue(undefined),
|
|
16
|
+
createFile: jest.fn().mockResolvedValue(undefined),
|
|
17
|
+
updateFile: jest.fn().mockResolvedValue(undefined),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
jest.mock('../../utils/app-module.updater', () => ({
|
|
21
|
+
safeUpdateAppModule: jest.fn().mockResolvedValue(undefined),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
describe('setupHealth', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
jest.clearAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should create src/health directory, controller and module files', async () => {
|
|
30
|
+
await setupHealth();
|
|
31
|
+
|
|
32
|
+
// Doit avoir créé le répertoire
|
|
33
|
+
expect(createDirectory).toHaveBeenCalledWith('src/health');
|
|
34
|
+
|
|
35
|
+
// Doit avoir créé les fichiers TS
|
|
36
|
+
expect(createFile).toHaveBeenCalledWith(expect.objectContaining({
|
|
37
|
+
path: 'src/health/health.controller.ts',
|
|
38
|
+
contente: expect.stringContaining('export class HealthController')
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
expect(createFile).toHaveBeenCalledWith(expect.objectContaining({
|
|
42
|
+
path: 'src/health/health.module.ts',
|
|
43
|
+
contente: expect.stringContaining('export class HealthModule')
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
// Doit avoir enregistré le module dans AppModule
|
|
47
|
+
expect(safeUpdateAppModule).toHaveBeenCalledWith('health');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should skip creation and print log in dry-run mode', async () => {
|
|
51
|
+
const originalArgv = process.argv;
|
|
52
|
+
process.argv = [...originalArgv, '--dry-run'];
|
|
53
|
+
|
|
54
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
55
|
+
|
|
56
|
+
await setupHealth();
|
|
57
|
+
|
|
58
|
+
// Aucun fichier ou dossier ne doit être créé
|
|
59
|
+
expect(createDirectory).not.toHaveBeenCalled();
|
|
60
|
+
expect(createFile).not.toHaveBeenCalled();
|
|
61
|
+
expect(safeUpdateAppModule).not.toHaveBeenCalled();
|
|
62
|
+
|
|
63
|
+
// Doit afficher une trace de simulation
|
|
64
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('[DRY-RUN] Simulated:'));
|
|
65
|
+
|
|
66
|
+
consoleSpy.mockRestore();
|
|
67
|
+
process.argv = originalArgv;
|
|
68
|
+
});
|
|
69
|
+
});
|