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
|
@@ -26,11 +26,54 @@ const { createFile } = require("../../file-system");
|
|
|
26
26
|
* @param {"prisma"|"typeorm"|"mongoose"|"sequelize"} orm - L'ORM cible
|
|
27
27
|
* @returns {Promise<void>}
|
|
28
28
|
*/
|
|
29
|
-
async function generateRepository(entityName, orm) {
|
|
29
|
+
async function generateRepository(entityName, orm, entityData = null) {
|
|
30
30
|
const entityNameCapitalized = capitalize(entityName);
|
|
31
31
|
const entityNameLower = entityName.toLowerCase();
|
|
32
32
|
const entityPath = `src/${entityNameLower}`;
|
|
33
33
|
|
|
34
|
+
const path = require("path");
|
|
35
|
+
const fs = require("fs");
|
|
36
|
+
|
|
37
|
+
const configPath = path.join(process.cwd(), ".nestcraftx", ".nestcraftxrc");
|
|
38
|
+
let stringFields = [];
|
|
39
|
+
if (entityData && entityData.fields) {
|
|
40
|
+
stringFields = entityData.fields
|
|
41
|
+
.filter(f => f.type && (f.type.toLowerCase() === "string" || f.type.toLowerCase() === "text"))
|
|
42
|
+
.map(f => f.name);
|
|
43
|
+
} else if (fs.existsSync(configPath)) {
|
|
44
|
+
try {
|
|
45
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
46
|
+
const entity = config.entities?.find(e => e.name.toLowerCase() === entityNameLower);
|
|
47
|
+
if (entity && entity.fields) {
|
|
48
|
+
stringFields = entity.fields
|
|
49
|
+
.filter(f => f.type && (f.type.toLowerCase() === "string" || f.type.toLowerCase() === "text"))
|
|
50
|
+
.map(f => f.name);
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
// fallback
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (stringFields.length === 0) {
|
|
58
|
+
if (entityNameLower === "user") {
|
|
59
|
+
stringFields = ["email", "username"];
|
|
60
|
+
} else {
|
|
61
|
+
stringFields = ["name", "title"];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const prismaWhereClause = stringFields.length > 0
|
|
66
|
+
? `where.OR = [\n ` + stringFields.map(f => '{ ' + f + ': { contains: search, mode: \'insensitive\' } }').join(',\n ') + `\n ];`
|
|
67
|
+
: `// No searchable string fields defined`;
|
|
68
|
+
|
|
69
|
+
const typeormWhereClause = stringFields.length > 0
|
|
70
|
+
? `where.push(\n ` + stringFields.map(f => '{ ' + f + ': Like(`%' + '${search}' + '%`) }').join(',\n ') + `\n );`
|
|
71
|
+
: `// No searchable string fields defined`;
|
|
72
|
+
|
|
73
|
+
const mongooseWhereClause = stringFields.length > 0
|
|
74
|
+
? `query.$or = [\n ` + stringFields.map(f => '{ ' + f + ': { $regex: search, $options: \'i\' } }').join(',\n ') + `\n ];`
|
|
75
|
+
: `// No searchable string fields defined`;
|
|
76
|
+
|
|
34
77
|
// Méthode supplémentaire findByEmail pour l'entité User
|
|
35
78
|
const isUser = entityNameLower === "user";
|
|
36
79
|
const getExtraMethods = (ormType) => {
|
|
@@ -81,7 +124,7 @@ async findByEmail(email: string): Promise<${entityNameCapitalized}Entity | null>
|
|
|
81
124
|
*/
|
|
82
125
|
|
|
83
126
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
84
|
-
import { Repository } from 'typeorm';
|
|
127
|
+
import { Repository, Like } from 'typeorm';
|
|
85
128
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
86
129
|
import { ${entityNameCapitalized}Entity } from '${entityPath}/domain/entities/${entityNameLower}.entity';
|
|
87
130
|
import { ${entityNameCapitalized} } from 'src/entities/${entityNameCapitalized}.entity';
|
|
@@ -125,9 +168,28 @@ export class ${entityNameCapitalized}Repository implements I${entityNameCapitali
|
|
|
125
168
|
}
|
|
126
169
|
|
|
127
170
|
// find all
|
|
128
|
-
async findAll(): Promise
|
|
129
|
-
const
|
|
130
|
-
|
|
171
|
+
async findAll(page: number = 1, limit: number = 10, search?: string, sortBy?: string, sortOrder?: string): Promise<{ data: ${entityNameCapitalized}Entity[], total: number }> {
|
|
172
|
+
const skip = (page - 1) * limit;
|
|
173
|
+
const where: any = [];
|
|
174
|
+
if (search) {
|
|
175
|
+
${typeormWhereClause}
|
|
176
|
+
}
|
|
177
|
+
const order: any = {};
|
|
178
|
+
if (sortBy) {
|
|
179
|
+
order[sortBy] = sortOrder?.toUpperCase() === 'ASC' ? 'ASC' : 'DESC';
|
|
180
|
+
} else {
|
|
181
|
+
order.createdAt = 'DESC';
|
|
182
|
+
}
|
|
183
|
+
const [records, total] = await this.repository.findAndCount({
|
|
184
|
+
skip,
|
|
185
|
+
take: limit,
|
|
186
|
+
where: where.length > 0 ? where : undefined,
|
|
187
|
+
order,
|
|
188
|
+
});
|
|
189
|
+
return {
|
|
190
|
+
data: records.map(record => this.mapper.toDomain(record)),
|
|
191
|
+
total,
|
|
192
|
+
};
|
|
131
193
|
}
|
|
132
194
|
|
|
133
195
|
// delete
|
|
@@ -188,9 +250,31 @@ export class ${entityNameCapitalized}Repository implements I${entityNameCapitali
|
|
|
188
250
|
}
|
|
189
251
|
|
|
190
252
|
// find all
|
|
191
|
-
async findAll(): Promise
|
|
192
|
-
const
|
|
193
|
-
|
|
253
|
+
async findAll(page: number = 1, limit: number = 10, search?: string, sortBy?: string, sortOrder?: string): Promise<{ data: ${entityNameCapitalized}Entity[], total: number }> {
|
|
254
|
+
const skip = (page - 1) * limit;
|
|
255
|
+
const where: any = {};
|
|
256
|
+
if (search) {
|
|
257
|
+
${prismaWhereClause}
|
|
258
|
+
}
|
|
259
|
+
const orderBy: any = {};
|
|
260
|
+
if (sortBy) {
|
|
261
|
+
orderBy[sortBy] = sortOrder === 'asc' ? 'asc' : 'desc';
|
|
262
|
+
} else {
|
|
263
|
+
orderBy.createdAt = 'desc';
|
|
264
|
+
}
|
|
265
|
+
const [records, total] = await this.prisma.$transaction([
|
|
266
|
+
this.prisma.${entityNameLower}.findMany({
|
|
267
|
+
skip,
|
|
268
|
+
take: limit,
|
|
269
|
+
where,
|
|
270
|
+
orderBy,
|
|
271
|
+
}),
|
|
272
|
+
this.prisma.${entityNameLower}.count({ where }),
|
|
273
|
+
]);
|
|
274
|
+
return {
|
|
275
|
+
data: records.map(record => this.mapper.toDomain(record)),
|
|
276
|
+
total,
|
|
277
|
+
};
|
|
194
278
|
}
|
|
195
279
|
|
|
196
280
|
// delete
|
|
@@ -250,9 +334,26 @@ export class ${entityNameCapitalized}Repository implements I${entityNameCapitali
|
|
|
250
334
|
}
|
|
251
335
|
|
|
252
336
|
// find all
|
|
253
|
-
async findAll(): Promise
|
|
254
|
-
const
|
|
255
|
-
|
|
337
|
+
async findAll(page: number = 1, limit: number = 10, search?: string, sortBy?: string, sortOrder?: string): Promise<{ data: ${entityNameCapitalized}Entity[], total: number }> {
|
|
338
|
+
const skip = (page - 1) * limit;
|
|
339
|
+
const query: any = {};
|
|
340
|
+
if (search) {
|
|
341
|
+
${mongooseWhereClause}
|
|
342
|
+
}
|
|
343
|
+
const sort: any = {};
|
|
344
|
+
if (sortBy) {
|
|
345
|
+
sort[sortBy] = sortOrder === 'asc' ? 1 : -1;
|
|
346
|
+
} else {
|
|
347
|
+
sort.createdAt = -1;
|
|
348
|
+
}
|
|
349
|
+
const [records, total] = await Promise.all([
|
|
350
|
+
this.model.find(query).sort(sort).skip(skip).limit(limit).exec(),
|
|
351
|
+
this.model.countDocuments(query).exec(),
|
|
352
|
+
]);
|
|
353
|
+
return {
|
|
354
|
+
data: records.map(record => this.mapper.toDomain(record)),
|
|
355
|
+
total,
|
|
356
|
+
};
|
|
256
357
|
}
|
|
257
358
|
|
|
258
359
|
// delete
|
|
@@ -311,9 +412,8 @@ export class ${entityNameCapitalized}Repository implements I${entityNameCapitali
|
|
|
311
412
|
}
|
|
312
413
|
|
|
313
414
|
// find all
|
|
314
|
-
async findAll(): Promise
|
|
315
|
-
|
|
316
|
-
return records.map(record => this.mapper.toDomain(record));
|
|
415
|
+
async findAll(page: number = 1, limit: number = 10, search?: string, sortBy?: string, sortOrder?: string): Promise<{ data: ${entityNameCapitalized}Entity[], total: number }> {
|
|
416
|
+
throw new Error('Repository not implemented');
|
|
317
417
|
}
|
|
318
418
|
|
|
319
419
|
// delete
|
|
@@ -54,6 +54,19 @@ async function lightModuleGenerator(entity, config) {
|
|
|
54
54
|
contente: dtoContent,
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
if (entityNameLow === "user") {
|
|
58
|
+
await createDirectory(`src/common/enums`);
|
|
59
|
+
await createFile({
|
|
60
|
+
path: `src/common/enums/role.enum.ts`,
|
|
61
|
+
contente: `export enum Role {
|
|
62
|
+
USER = 'USER',
|
|
63
|
+
ADMIN = 'ADMIN',
|
|
64
|
+
SUPER_ADMIN = 'SUPER_ADMIN',
|
|
65
|
+
}
|
|
66
|
+
`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
57
70
|
// 4️⃣ Générer le repository
|
|
58
71
|
const repositoryContent = generateLightRepository(
|
|
59
72
|
entityNameCap,
|
|
@@ -78,6 +91,7 @@ async function lightModuleGenerator(entity, config) {
|
|
|
78
91
|
entityNameCap,
|
|
79
92
|
entityNameLow,
|
|
80
93
|
config.swagger,
|
|
94
|
+
config.auth || false,
|
|
81
95
|
);
|
|
82
96
|
await createFile({
|
|
83
97
|
path: `${entityPath}/controllers/${entityNameLow}.controller.ts`,
|
|
@@ -21,27 +21,59 @@ const { capitalize, decapitalize, pluralize } = require("../../helpers");
|
|
|
21
21
|
* @param {string} entityName - Le nom de l'entité (ex: "Post")
|
|
22
22
|
* @param {string} entityPath - Le chemin de base (ex: "src/post")
|
|
23
23
|
* @param {boolean} useSwagger - Inclure les décorateurs Swagger
|
|
24
|
+
* @param {boolean} useAuth - Inclure les guards RBAC (JwtAuthGuard + RolesGuard)
|
|
24
25
|
* @returns {Promise<string>} Le code TypeScript du controller
|
|
25
26
|
*/
|
|
26
|
-
async function generateController(entityName, entityPath, useSwagger) {
|
|
27
|
+
async function generateController(entityName, entityPath, useSwagger, useAuth = false) {
|
|
27
28
|
const entityNameLower = decapitalize(entityName);
|
|
28
29
|
const entityNameCapitalized = capitalize(entityName);
|
|
29
30
|
const pluralName = pluralize(entityNameLower);
|
|
30
31
|
|
|
32
|
+
// --- Imports NestJS de base ---
|
|
33
|
+
const nestImports = useAuth
|
|
34
|
+
? `import { Controller, Get, Post, Body, Param, Patch, Delete, HttpCode, HttpStatus, Query, UseGuards } from '@nestjs/common';`
|
|
35
|
+
: `import { Controller, Get, Post, Body, Param, Patch, Delete, HttpCode, HttpStatus, Query } from '@nestjs/common';`;
|
|
36
|
+
|
|
37
|
+
// --- Imports Swagger ---
|
|
31
38
|
const swaggerImports = useSwagger
|
|
32
|
-
? `import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';`
|
|
39
|
+
? `import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery${useAuth ? ', ApiBearerAuth, ApiUnauthorizedResponse, ApiForbiddenResponse' : ''} } from '@nestjs/swagger';`
|
|
40
|
+
: "";
|
|
41
|
+
|
|
42
|
+
// --- Imports Guards & Décorateurs (uniquement si auth activé) ---
|
|
43
|
+
const guardImports = useAuth
|
|
44
|
+
? `import { JwtAuthGuard } from 'src/auth/infrastructure/guards/jwt-auth.guard';
|
|
45
|
+
import { RolesGuard } from 'src/auth/infrastructure/guards/role.guard';
|
|
46
|
+
import { Roles } from 'src/common/decorators/role.decorator';
|
|
47
|
+
import { Public } from 'src/common/decorators/public.decorator';`
|
|
33
48
|
: "";
|
|
34
49
|
|
|
50
|
+
// --- Décorateur de classe Swagger ---
|
|
35
51
|
const swaggerClassDecorator = useSwagger
|
|
36
52
|
? `@ApiTags('${capitalize(pluralName)}')`
|
|
37
53
|
: "";
|
|
38
54
|
|
|
55
|
+
// --- Snippet pour les routes protégées (ADMIN) ---
|
|
56
|
+
const adminGuards = useAuth
|
|
57
|
+
? ` @UseGuards(JwtAuthGuard, RolesGuard)
|
|
58
|
+
@Roles('ADMIN')`
|
|
59
|
+
: "";
|
|
60
|
+
|
|
61
|
+
// --- Snippet @ApiBearerAuth pour les routes protégées ---
|
|
62
|
+
const apiBearerAuth = useAuth && useSwagger ? ` @ApiBearerAuth()` : "";
|
|
63
|
+
const apiUnauthorized = useAuth && useSwagger
|
|
64
|
+
? ` @ApiUnauthorizedResponse({ description: 'Missing or invalid JWT token.' })
|
|
65
|
+
@ApiForbiddenResponse({ description: 'Insufficient permissions (ADMIN required).' })`
|
|
66
|
+
: "";
|
|
67
|
+
|
|
68
|
+
// --- Snippet @Public() pour les routes de lecture ---
|
|
69
|
+
const publicDecorator = useAuth ? ` @Public()` : "";
|
|
70
|
+
|
|
39
71
|
return `
|
|
40
|
-
|
|
72
|
+
${nestImports}
|
|
41
73
|
${swaggerImports}
|
|
42
74
|
import { ${entityNameCapitalized}Service } from '${entityPath}/application/services/${entityNameLower}.service';
|
|
43
75
|
import { Create${entityNameCapitalized}Dto, Update${entityNameCapitalized}Dto } from 'src/${entityNameLower}/application/dtos/${entityNameLower}.dto';
|
|
44
|
-
|
|
76
|
+
${guardImports}
|
|
45
77
|
|
|
46
78
|
/**
|
|
47
79
|
* Controller for ${entityNameCapitalized} management.
|
|
@@ -55,14 +87,16 @@ export class ${entityNameCapitalized}Controller {
|
|
|
55
87
|
${
|
|
56
88
|
entityNameLower !== "user"
|
|
57
89
|
? `
|
|
90
|
+
${apiBearerAuth}
|
|
91
|
+
${adminGuards}
|
|
58
92
|
@Post()
|
|
59
93
|
@HttpCode(HttpStatus.CREATED)
|
|
60
94
|
${
|
|
61
95
|
useSwagger
|
|
62
|
-
?
|
|
63
|
-
@ApiOperation({ summary: 'Create a new ${entityNameLower}', description: 'Creates a new record for ${entityNameLower} in the database.' })
|
|
96
|
+
? `@ApiOperation({ summary: 'Create a new ${entityNameLower}', description: 'Creates a new record for ${entityNameLower} in the database.' })
|
|
64
97
|
@ApiResponse({ status: 201, description: 'The ${entityNameLower} has been successfully created.' })
|
|
65
|
-
@ApiResponse({ status: 400, description: 'Invalid input data.' })
|
|
98
|
+
@ApiResponse({ status: 400, description: 'Invalid input data.' })
|
|
99
|
+
${apiUnauthorized}`
|
|
66
100
|
: ""
|
|
67
101
|
}
|
|
68
102
|
async create(@Body() dto: Create${entityNameCapitalized}Dto) {
|
|
@@ -76,23 +110,36 @@ export class ${entityNameCapitalized}Controller {
|
|
|
76
110
|
: ""
|
|
77
111
|
}
|
|
78
112
|
|
|
113
|
+
${publicDecorator}
|
|
79
114
|
@Get()
|
|
80
115
|
${
|
|
81
116
|
useSwagger
|
|
82
|
-
?
|
|
83
|
-
@
|
|
117
|
+
? `@ApiOperation({ summary: 'Get all ${pluralName}', description: 'Retrieves a list of all ${pluralName} available.' })
|
|
118
|
+
@ApiQuery({ name: 'page', required: false, type: Number, schema: { default: 1 } })
|
|
119
|
+
@ApiQuery({ name: 'limit', required: false, type: Number, schema: { default: 10 } })
|
|
120
|
+
@ApiQuery({ name: 'search', required: false, type: String, description: 'Search term' })
|
|
121
|
+
@ApiQuery({ name: 'sortBy', required: false, type: String, description: 'Field to sort by' })
|
|
122
|
+
@ApiQuery({ name: 'sortOrder', required: false, enum: ['asc', 'desc'], description: 'Sort order' })
|
|
84
123
|
@ApiResponse({ status: 200, description: 'Return all ${pluralName}.' })`
|
|
85
124
|
: ""
|
|
86
125
|
}
|
|
87
|
-
async getAll(
|
|
88
|
-
|
|
126
|
+
async getAll(
|
|
127
|
+
@Query('page') page?: number,
|
|
128
|
+
@Query('limit') limit?: number,
|
|
129
|
+
@Query('search') search?: string,
|
|
130
|
+
@Query('sortBy') sortBy?: string,
|
|
131
|
+
@Query('sortOrder') sortOrder?: string,
|
|
132
|
+
) {
|
|
133
|
+
const pageNum = page ? Number(page) : 1;
|
|
134
|
+
const limitNum = limit ? Number(limit) : 10;
|
|
135
|
+
return await this.service.getAll(pageNum, limitNum, search, sortBy, sortOrder);
|
|
89
136
|
}
|
|
90
137
|
|
|
138
|
+
${publicDecorator}
|
|
91
139
|
@Get(':id')
|
|
92
140
|
${
|
|
93
141
|
useSwagger
|
|
94
|
-
?
|
|
95
|
-
@ApiOperation({ summary: 'Get ${entityNameLower} by ID' })
|
|
142
|
+
? `@ApiOperation({ summary: 'Get ${entityNameLower} by ID' })
|
|
96
143
|
@ApiParam({ name: 'id', description: 'The unique identifier of the ${entityNameLower}' })
|
|
97
144
|
@ApiResponse({ status: 200, description: 'The ${entityNameLower} has been found.' })
|
|
98
145
|
@ApiResponse({ status: 404, description: '${entityNameCapitalized} not found.' })`
|
|
@@ -102,14 +149,16 @@ export class ${entityNameCapitalized}Controller {
|
|
|
102
149
|
return await this.service.getById(id);
|
|
103
150
|
}
|
|
104
151
|
|
|
152
|
+
${apiBearerAuth}
|
|
153
|
+
${adminGuards}
|
|
105
154
|
@Patch(':id')
|
|
106
155
|
${
|
|
107
156
|
useSwagger
|
|
108
|
-
?
|
|
109
|
-
@ApiOperation({ summary: 'Update an existing ${entityNameLower}' })
|
|
157
|
+
? `@ApiOperation({ summary: 'Update an existing ${entityNameLower}' })
|
|
110
158
|
@ApiParam({ name: 'id', description: 'The unique identifier of the ${entityNameLower} to update' })
|
|
111
159
|
@ApiResponse({ status: 200, description: 'The ${entityNameLower} has been successfully updated.' })
|
|
112
|
-
@ApiResponse({ status: 404, description: '${entityNameCapitalized} not found.' })
|
|
160
|
+
@ApiResponse({ status: 404, description: '${entityNameCapitalized} not found.' })
|
|
161
|
+
${apiUnauthorized}`
|
|
113
162
|
: ""
|
|
114
163
|
}
|
|
115
164
|
async update(
|
|
@@ -120,15 +169,17 @@ export class ${entityNameCapitalized}Controller {
|
|
|
120
169
|
return { message: '${entityNameCapitalized} updated successfully' };
|
|
121
170
|
}
|
|
122
171
|
|
|
172
|
+
${apiBearerAuth}
|
|
173
|
+
${adminGuards}
|
|
123
174
|
@Delete(':id')
|
|
124
175
|
@HttpCode(HttpStatus.NO_CONTENT)
|
|
125
176
|
${
|
|
126
177
|
useSwagger
|
|
127
|
-
?
|
|
128
|
-
@ApiOperation({ summary: 'Delete a ${entityNameLower}' })
|
|
178
|
+
? `@ApiOperation({ summary: 'Delete a ${entityNameLower}' })
|
|
129
179
|
@ApiParam({ name: 'id', description: 'The unique identifier of the ${entityNameLower} to delete' })
|
|
130
180
|
@ApiResponse({ status: 204, description: 'The ${entityNameLower} has been successfully deleted.' })
|
|
131
|
-
@ApiResponse({ status: 404, description: '${entityNameCapitalized} not found.' })
|
|
181
|
+
@ApiResponse({ status: 404, description: '${entityNameCapitalized} not found.' })
|
|
182
|
+
${apiUnauthorized}`
|
|
132
183
|
: ""
|
|
133
184
|
}
|
|
134
185
|
async delete(@Param('id') id: string) {
|
package/utils/helpers.js
CHANGED
|
@@ -94,10 +94,16 @@ function formatType(type) {
|
|
|
94
94
|
* @returns {string} Le type final formaté
|
|
95
95
|
*/
|
|
96
96
|
function getFormattedType(field) {
|
|
97
|
+
let typeStr;
|
|
97
98
|
if (field.name === "role" && field.type.toLowerCase().startsWith("string")) {
|
|
98
|
-
|
|
99
|
+
typeStr = "Role";
|
|
100
|
+
} else {
|
|
101
|
+
typeStr = formatType(field.type);
|
|
99
102
|
}
|
|
100
|
-
|
|
103
|
+
if (field.nullable) {
|
|
104
|
+
typeStr += " | null";
|
|
105
|
+
}
|
|
106
|
+
return typeStr;
|
|
101
107
|
}
|
|
102
108
|
|
|
103
109
|
module.exports = {
|
|
@@ -6,78 +6,15 @@ const inquirer = require("inquirer");
|
|
|
6
6
|
const { info, success } = require("../colors");
|
|
7
7
|
const { logWarning } = require("../loggers/logWarning");
|
|
8
8
|
const { capitalize } = require("../userInput");
|
|
9
|
+
const { buildEntityInteractive } = require("./entityBuilder");
|
|
9
10
|
const actualInquirer = inquirer.default || inquirer;
|
|
10
11
|
|
|
11
12
|
async function askEntityInputs(targetName) {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
`\n${info("[ENTITY DESIGN]")} Define fields for "${targetName}" :`,
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
while (true) {
|
|
19
|
-
let fname = readline.question(" Field name (leave empty to finish) : ");
|
|
20
|
-
if (!fname) break;
|
|
21
|
-
|
|
22
|
-
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(fname)) {
|
|
23
|
-
logWarning("Invalid field name.");
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const baseTypeChoices = [
|
|
28
|
-
"string",
|
|
29
|
-
"text",
|
|
30
|
-
"number",
|
|
31
|
-
"boolean",
|
|
32
|
-
"Date",
|
|
33
|
-
"uuid",
|
|
34
|
-
"json",
|
|
35
|
-
"enum",
|
|
36
|
-
"array",
|
|
37
|
-
"object",
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
const typeAnswer = await actualInquirer.prompt([
|
|
41
|
-
{
|
|
42
|
-
type: "list",
|
|
43
|
-
name: "ftype",
|
|
44
|
-
message: `Type for "${fname}"`,
|
|
45
|
-
choices: baseTypeChoices,
|
|
46
|
-
},
|
|
47
|
-
]);
|
|
48
|
-
|
|
49
|
-
let ftype = typeAnswer.ftype;
|
|
50
|
-
|
|
51
|
-
// Logique de raffinement des types (identique à ton script original)
|
|
52
|
-
if (ftype === "array") {
|
|
53
|
-
const inner = await actualInquirer.prompt([
|
|
54
|
-
{
|
|
55
|
-
type: "list",
|
|
56
|
-
name: "innerType",
|
|
57
|
-
message: `Type of elements for "${fname}[]"`,
|
|
58
|
-
choices: baseTypeChoices.filter(
|
|
59
|
-
(c) => c !== "array" && c !== "object",
|
|
60
|
-
),
|
|
61
|
-
},
|
|
62
|
-
]);
|
|
63
|
-
ftype = `${inner.innerType}[]`;
|
|
64
|
-
} else if (ftype === "enum") {
|
|
65
|
-
ftype = capitalize(fname) + "Enum";
|
|
66
|
-
} else if (ftype === "object") {
|
|
67
|
-
const obj = await actualInquirer.prompt([
|
|
68
|
-
{
|
|
69
|
-
type: "input",
|
|
70
|
-
name: "val",
|
|
71
|
-
message: "Complex type name :",
|
|
72
|
-
default: "json",
|
|
73
|
-
},
|
|
74
|
-
]);
|
|
75
|
-
ftype = capitalize(obj.val);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
entity.fields.push({ name: fname, type: ftype });
|
|
79
|
-
console.log(` Type for "${fname}" : ${ftype} ${success("[✓]")}`);
|
|
13
|
+
const result = await buildEntityInteractive(targetName);
|
|
14
|
+
if (!result) {
|
|
15
|
+
throw new Error("Module generation cancelled by user.");
|
|
80
16
|
}
|
|
17
|
+
const entity = { name: result.name, fields: result.fields, relation: null };
|
|
81
18
|
|
|
82
19
|
const relationData = await askRelationInputs(entity.name);
|
|
83
20
|
entity.relation = relationData;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// utils/interactive/askRelationCommand.js
|
|
2
|
+
//
|
|
3
|
+
// Prompt interactif autonome pour `nestcraftx g relation`.
|
|
4
|
+
// Contrairement à askRelationInputs (qui s'intègre à la création d'un module),
|
|
5
|
+
// cette fonction demande AUSSI la source, car les deux modules existent déjà.
|
|
6
|
+
|
|
7
|
+
const inquirer = require("inquirer");
|
|
8
|
+
const { capitalize } = require("../userInput");
|
|
9
|
+
const { logWarning } = require("../loggers/logWarning");
|
|
10
|
+
const { logError } = require("../loggers/logError");
|
|
11
|
+
|
|
12
|
+
const actualInquirer = inquirer.default || inquirer;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Demande interactivement la source, la cible et le type de relation.
|
|
16
|
+
* Valide que les deux modules existent dans config.entities[].
|
|
17
|
+
* Vérifie l'idempotence (relation déjà enregistrée dans config.relations[]).
|
|
18
|
+
*
|
|
19
|
+
* @param {object} config - La config lue depuis .nestcraftxrc
|
|
20
|
+
* @returns {Promise<{source: string, target: string, type: string} | null>}
|
|
21
|
+
*/
|
|
22
|
+
async function askRelationCommand(config) {
|
|
23
|
+
const entities = (config.entities || []).map((e) => capitalize(e.name));
|
|
24
|
+
|
|
25
|
+
if (entities.length < 2) {
|
|
26
|
+
logError(
|
|
27
|
+
"❌ Vous devez avoir au moins 2 modules générés pour créer une relation.\n" +
|
|
28
|
+
" Lancez d'abord : nestcraftx g module <NomEntité>",
|
|
29
|
+
);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 1. Demander la source
|
|
34
|
+
const { source } = await actualInquirer.prompt([
|
|
35
|
+
{
|
|
36
|
+
type: "list",
|
|
37
|
+
name: "source",
|
|
38
|
+
message: "Module SOURCE (le module qui porte la relation) :",
|
|
39
|
+
choices: entities,
|
|
40
|
+
},
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
// 2. Demander la cible (exclure la source)
|
|
44
|
+
const targetChoices = entities.filter((e) => e !== source);
|
|
45
|
+
|
|
46
|
+
if (targetChoices.length === 0) {
|
|
47
|
+
logError("❌ Aucun autre module disponible comme cible.");
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const { target } = await actualInquirer.prompt([
|
|
52
|
+
{
|
|
53
|
+
type: "list",
|
|
54
|
+
name: "target",
|
|
55
|
+
message: `Module CIBLE (lié à ${source}) :`,
|
|
56
|
+
choices: targetChoices,
|
|
57
|
+
},
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
// 3. Demander le type de relation
|
|
61
|
+
const { type } = await actualInquirer.prompt([
|
|
62
|
+
{
|
|
63
|
+
type: "list",
|
|
64
|
+
name: "type",
|
|
65
|
+
message: `Type de relation (${source} → ${target}) :`,
|
|
66
|
+
choices: [
|
|
67
|
+
{ name: `n-1 (Many-to-One : chaque ${source} appartient à un ${target})`, value: "n-1" },
|
|
68
|
+
{ name: `1-n (One-to-Many : un ${source} possède plusieurs ${target}s)`, value: "1-n" },
|
|
69
|
+
{ name: `1-1 (One-to-One : un ${source} est lié à exactement un ${target})`, value: "1-1" },
|
|
70
|
+
{ name: `n-n (Many-to-Many : plusieurs ${source}s liés à plusieurs ${target}s)`, value: "n-n" },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
// 4. Vérification d'idempotence — relation déjà enregistrée ?
|
|
76
|
+
const existing = (config.relations || []).find(
|
|
77
|
+
(r) =>
|
|
78
|
+
r.from.toLowerCase() === source.toLowerCase() &&
|
|
79
|
+
r.to.toLowerCase() === target.toLowerCase() &&
|
|
80
|
+
r.type === type,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (existing) {
|
|
84
|
+
logWarning(
|
|
85
|
+
`⚠️ La relation ${source} →(${type})→ ${target} existe déjà dans .nestcraftxrc.\n` +
|
|
86
|
+
" Aucune modification effectuée.",
|
|
87
|
+
);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
source: source.toLowerCase(),
|
|
93
|
+
target: target.toLowerCase(),
|
|
94
|
+
type,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { askRelationCommand };
|