nestcraftx 0.4.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 +59 -57
- package/README.fr.md +60 -69
- package/bin/nestcraft.js +8 -0
- package/commands/demo.js +10 -0
- 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 +26 -9
- 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/configs/setupCleanArchitecture.js +1 -1
- package/utils/configs/setupLightArchitecture.js +98 -26
- package/utils/envGenerator.js +3 -1
- package/utils/file-utils/saveProjectConfig.js +3 -0
- package/utils/fullModeInput.js +4 -0
- package/utils/generators/application/dtoGenerator.js +20 -3
- package/utils/generators/cleanModuleGenerator.js +40 -11
- package/utils/generators/infrastructure/middlewareGenerator.js +112 -315
- package/utils/generators/infrastructure/mongooseSchemaGenerator.js +78 -1
- 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/interactive/askRelationCommand.js +98 -0
- package/utils/interactive/entityBuilder.js +11 -0
- package/utils/lightModeInput.js +14 -0
- package/utils/setups/orms/typeOrmSetup.js +11 -4
- package/utils/setups/projectSetup.js +10 -2
- package/utils/setups/setupAuth.js +334 -18
- package/utils/setups/setupDatabase.js +4 -1
- package/utils/setups/setupHealth.js +74 -0
- package/utils/setups/setupMongoose.js +4 -1
- package/utils/setups/setupPrisma.js +110 -1
- package/utils/setups/setupSwagger.js +4 -1
- package/utils/setups/setupThrottler.js +92 -0
- package/utils/shell.js +21 -4
|
@@ -226,4 +226,81 @@ ${allFields}
|
|
|
226
226
|
export const ${entityName}Schema = SchemaFactory.createForClass(${entityName});`.trim();
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Ajoute une relation entre deux schémas Mongoose EXISTANTS.
|
|
231
|
+
* Patche les deux fichiers .schema.ts pour ajouter les champs de référence.
|
|
232
|
+
*
|
|
233
|
+
* @param {string} source - Nom du module source (ex: "post")
|
|
234
|
+
* @param {string} target - Nom du module cible (ex: "user")
|
|
235
|
+
* @param {string} relationType - "n-1" | "1-n" | "1-1" | "n-n"
|
|
236
|
+
* @param {string} mode - "full" | "light"
|
|
237
|
+
*/
|
|
238
|
+
async function addMongooseRelation(source, target, relationType, mode = "full") {
|
|
239
|
+
const fs = require("fs");
|
|
240
|
+
const { updateFile } = require("../../userInput");
|
|
241
|
+
|
|
242
|
+
const sourceLow = source.toLowerCase();
|
|
243
|
+
const targetLow = target.toLowerCase();
|
|
244
|
+
const sourceCap = capitalize(source);
|
|
245
|
+
const targetCap = capitalize(target);
|
|
246
|
+
|
|
247
|
+
const isFull = mode === "full";
|
|
248
|
+
|
|
249
|
+
const sourceSchemaPath = isFull
|
|
250
|
+
? `src/${sourceLow}/infrastructure/persistence/mongoose/${sourceLow}.schema.ts`
|
|
251
|
+
: `src/${sourceLow}/entities/${sourceLow}.schema.ts`;
|
|
252
|
+
|
|
253
|
+
const targetSchemaPath = isFull
|
|
254
|
+
? `src/${targetLow}/infrastructure/persistence/mongoose/${targetLow}.schema.ts`
|
|
255
|
+
: `src/${targetLow}/entities/${targetLow}.schema.ts`;
|
|
256
|
+
|
|
257
|
+
// --- Champs à injecter dans SOURCE ---
|
|
258
|
+
let sourceField = "";
|
|
259
|
+
switch (relationType) {
|
|
260
|
+
case "n-1":
|
|
261
|
+
case "1-1":
|
|
262
|
+
// FK : référence vers un document cible
|
|
263
|
+
sourceField = `\n @Prop({ type: 'ObjectId', ref: '${targetCap}' })\n ${targetLow}Id: string;\n`;
|
|
264
|
+
break;
|
|
265
|
+
case "1-n":
|
|
266
|
+
case "n-n":
|
|
267
|
+
// Tableau de références vers la cible
|
|
268
|
+
sourceField = `\n @Prop([{ type: 'ObjectId', ref: '${targetCap}' }])\n ${targetLow}Ids: string[];\n`;
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// --- Champs inverses à injecter dans TARGET ---
|
|
273
|
+
let targetField = "";
|
|
274
|
+
switch (relationType) {
|
|
275
|
+
case "n-1":
|
|
276
|
+
case "n-n":
|
|
277
|
+
// Le target a un tableau de sources
|
|
278
|
+
targetField = `\n @Prop([{ type: 'ObjectId', ref: '${sourceCap}' }])\n ${sourceLow}Ids: string[];\n`;
|
|
279
|
+
break;
|
|
280
|
+
case "1-n":
|
|
281
|
+
case "1-1":
|
|
282
|
+
// Le target a une référence unique vers source
|
|
283
|
+
targetField = `\n @Prop({ type: 'ObjectId', ref: '${sourceCap}' })\n ${sourceLow}Id: string;\n`;
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Patch source schema
|
|
288
|
+
if (fs.existsSync(sourceSchemaPath) && sourceField) {
|
|
289
|
+
await updateFile({
|
|
290
|
+
path: sourceSchemaPath,
|
|
291
|
+
pattern: new RegExp(`(export class ${sourceCap} \\{)`),
|
|
292
|
+
replacement: `$1${sourceField}`,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Patch target schema
|
|
297
|
+
if (fs.existsSync(targetSchemaPath) && targetField) {
|
|
298
|
+
await updateFile({
|
|
299
|
+
path: targetSchemaPath,
|
|
300
|
+
pattern: new RegExp(`(export class ${targetCap} \\{)`),
|
|
301
|
+
replacement: `$1${targetField}`,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
module.exports = { generateMongooseSchemaFileContent, addMongooseRelation };
|
|
@@ -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) {
|
|
@@ -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 };
|
|
@@ -50,6 +50,7 @@ async function buildEntityInteractive(initialName) {
|
|
|
50
50
|
if (f.unique) constraints.push("unique");
|
|
51
51
|
if (f.nullable) constraints.push("nullable");
|
|
52
52
|
if (f.default !== null && f.default !== "") constraints.push(`default: ${f.default}`);
|
|
53
|
+
if (f.description) constraints.push(`desc: "${f.description}"`);
|
|
53
54
|
const constraintStr = constraints.length > 0 ? ` (${constraints.join(", ")})` : "";
|
|
54
55
|
console.log(` ${idx + 1}. ${f.name} : ${f.type}${constraintStr}`);
|
|
55
56
|
});
|
|
@@ -256,12 +257,22 @@ async function promptFieldDetails(fieldName, existingField = null) {
|
|
|
256
257
|
defaultValue = null;
|
|
257
258
|
}
|
|
258
259
|
|
|
260
|
+
let descriptionPrompt = " Description (press Enter for auto): ";
|
|
261
|
+
if (existingField && existingField.description) {
|
|
262
|
+
descriptionPrompt = ` Description [${existingField.description}]: `;
|
|
263
|
+
}
|
|
264
|
+
let descriptionValue = readline.question(descriptionPrompt);
|
|
265
|
+
if (descriptionValue === "") {
|
|
266
|
+
descriptionValue = existingField ? existingField.description : null;
|
|
267
|
+
}
|
|
268
|
+
|
|
259
269
|
return {
|
|
260
270
|
name: fieldName,
|
|
261
271
|
type: ftype,
|
|
262
272
|
unique: isUnique,
|
|
263
273
|
nullable: isNullable,
|
|
264
274
|
default: defaultValue,
|
|
275
|
+
description: descriptionValue || null,
|
|
265
276
|
};
|
|
266
277
|
}
|
|
267
278
|
|
package/utils/lightModeInput.js
CHANGED
|
@@ -94,12 +94,16 @@ async function getLightModeInputs(projectName, flags) {
|
|
|
94
94
|
// JWT Auth config
|
|
95
95
|
const useAuth = getAuthChoice(flags);
|
|
96
96
|
|
|
97
|
+
// Throttler config
|
|
98
|
+
const useThrottler = getThrottlerChoice(flags);
|
|
99
|
+
|
|
97
100
|
const packageManager = await getPackageManager(flags);
|
|
98
101
|
inputs.packageManager = packageManager;
|
|
99
102
|
|
|
100
103
|
inputs.useAuth = useAuth;
|
|
101
104
|
inputs.useSwagger = useSwagger;
|
|
102
105
|
inputs.useDocker = useDocker;
|
|
106
|
+
inputs.useThrottler = useThrottler;
|
|
103
107
|
|
|
104
108
|
if (useAuth) {
|
|
105
109
|
console.log(
|
|
@@ -224,4 +228,14 @@ function getDockerChoice(flags) {
|
|
|
224
228
|
return readline.keyInYNStrict(`${info("[?]")} Generate Docker files ?`);
|
|
225
229
|
}
|
|
226
230
|
|
|
231
|
+
function getThrottlerChoice(flags) {
|
|
232
|
+
if (flags.throttler !== undefined) {
|
|
233
|
+
console.log(
|
|
234
|
+
`${info("[INFO]")} Throttler: ${flags.throttler ? "Yes" : "No"} (via flag)`
|
|
235
|
+
);
|
|
236
|
+
return flags.throttler === true || flags.throttler === "true";
|
|
237
|
+
}
|
|
238
|
+
return readline.keyInYNStrict(`${info("[?]")} Enable Rate Limiting (Throttler) ?`);
|
|
239
|
+
}
|
|
240
|
+
|
|
227
241
|
module.exports = { getLightModeInputs };
|
|
@@ -21,8 +21,11 @@ async function setupTypeORM(inputs) {
|
|
|
21
21
|
logInfo("📦 Installing TypeORM and PostgreSQL dependencies...");
|
|
22
22
|
|
|
23
23
|
const mode = inputs.mode;
|
|
24
|
+
const pkgManager = inputs.packageManager || "npm";
|
|
25
|
+
const installCmd = pkgManager === "yarn" || pkgManager === "pnpm" ? `${pkgManager} add` : "npm install";
|
|
26
|
+
|
|
24
27
|
await runCommand(
|
|
25
|
-
|
|
28
|
+
`${installCmd} @nestjs/typeorm@^10.0.2 typeorm@^0.3.20 pg@^8.11.3 reflect-metadata@^0.2.1`,
|
|
26
29
|
"TypeORM and PostgreSQL dependencies installed successfully"
|
|
27
30
|
); // Updating app.module.ts with TypeORM
|
|
28
31
|
|
|
@@ -446,13 +449,17 @@ async function setupTypeOrmSeeding(inputs) {
|
|
|
446
449
|
"typeorm-extension",
|
|
447
450
|
];
|
|
448
451
|
|
|
452
|
+
const pkgManager = inputs.packageManager || "npm";
|
|
453
|
+
const installDevCmd = pkgManager === "yarn" || pkgManager === "pnpm" ? `${pkgManager} add -D` : "npm install -D";
|
|
454
|
+
const installCmd = pkgManager === "yarn" || pkgManager === "pnpm" ? `${pkgManager} add` : "npm install";
|
|
455
|
+
|
|
449
456
|
await runCommand(
|
|
450
|
-
`${
|
|
457
|
+
`${installDevCmd} ${typeOrmDevDeps.join(" ")}`,
|
|
451
458
|
"❌ Failed to install TypeORM seeding dependencies"
|
|
452
459
|
);
|
|
453
460
|
|
|
454
461
|
await runCommand(
|
|
455
|
-
`${
|
|
462
|
+
`${installCmd} bcrypt`,
|
|
456
463
|
"❌ Failed to install bcrypt"
|
|
457
464
|
);
|
|
458
465
|
|
|
@@ -462,7 +469,7 @@ async function setupTypeOrmSeeding(inputs) {
|
|
|
462
469
|
"typeorm-ts-node-commonjs migration:run -d ./src/database/typeorm.config.ts",
|
|
463
470
|
"typeorm:seed":
|
|
464
471
|
"typeorm-extension seed:run -d src/database/typeorm.config.ts",
|
|
465
|
-
seed:
|
|
472
|
+
seed: `${pkgManager} run typeorm:seed`,
|
|
466
473
|
};
|
|
467
474
|
|
|
468
475
|
await updatePackageJson(inputs, typeOrmScripts);
|