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
package/utils/fullModeInput.js
CHANGED
|
@@ -211,6 +211,8 @@ async function getFullModeInputs(projectName, flags) {
|
|
|
211
211
|
{ name: "auth", default: true, prompt: "Add JWT authentication?" },
|
|
212
212
|
|
|
213
213
|
{ name: "swagger", default: true, prompt: "Install Swagger?" },
|
|
214
|
+
|
|
215
|
+
{ name: "throttler", default: true, prompt: "Enable Rate Limiting (Throttler)?" },
|
|
214
216
|
];
|
|
215
217
|
|
|
216
218
|
const booleanResults = {};
|
|
@@ -239,6 +241,7 @@ async function getFullModeInputs(projectName, flags) {
|
|
|
239
241
|
const useDocker = booleanResults.docker;
|
|
240
242
|
const useAuth = booleanResults.auth;
|
|
241
243
|
const useSwagger = booleanResults.swagger;
|
|
244
|
+
const useThrottler = booleanResults.throttler;
|
|
242
245
|
|
|
243
246
|
// --- 5. Swagger Configuration (Prioritize flags) ---
|
|
244
247
|
let swaggerInputs;
|
|
@@ -357,6 +360,7 @@ async function getFullModeInputs(projectName, flags) {
|
|
|
357
360
|
useDocker,
|
|
358
361
|
useAuth,
|
|
359
362
|
useSwagger,
|
|
363
|
+
useThrottler,
|
|
360
364
|
swaggerInputs,
|
|
361
365
|
packageManager,
|
|
362
366
|
entitiesData,
|
|
@@ -47,13 +47,30 @@ async function generateDto(
|
|
|
47
47
|
SWAGGER HELPERS
|
|
48
48
|
=============================== */
|
|
49
49
|
const getFieldDescription = (f) => {
|
|
50
|
+
if (f.description) return f.description;
|
|
51
|
+
|
|
50
52
|
const name = f.name.toLowerCase();
|
|
51
53
|
if (name.includes("email")) return "The official email address of the user";
|
|
52
54
|
if (name.includes("password"))
|
|
53
55
|
return "Must contain at least 8 characters, one letter and one number";
|
|
54
56
|
if (name.includes("token")) return "Authentication token";
|
|
55
|
-
if (name.
|
|
56
|
-
return `
|
|
57
|
+
if (name.endsWith("id") && name !== "id")
|
|
58
|
+
return `UUID of the referenced ${capitalize(f.name.slice(0, -2))}`;
|
|
59
|
+
if (name.endsWith("ids"))
|
|
60
|
+
return `Array of UUIDs of the referenced ${capitalize(f.name.slice(0, -3))}`;
|
|
61
|
+
|
|
62
|
+
const cleanType = f.type.toLowerCase().replace("[]", "");
|
|
63
|
+
if (cleanType === "date" || name.endsWith("date") || name.endsWith("at")) {
|
|
64
|
+
return "ISO 8601 date string (e.g. 2026-06-16T15:00:00Z)";
|
|
65
|
+
}
|
|
66
|
+
if (name.includes("price") || name.includes("amount")) {
|
|
67
|
+
return "Decimal value in USD (e.g. 99.99)";
|
|
68
|
+
}
|
|
69
|
+
if (cleanType === "boolean") {
|
|
70
|
+
const defSuffix = f.default !== undefined && f.default !== null ? ` — default: ${f.default}` : "";
|
|
71
|
+
return `true/false flag${defSuffix}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
57
74
|
return `The ${f.name} of the ${entityNameLower}`;
|
|
58
75
|
};
|
|
59
76
|
|
|
@@ -229,7 +246,7 @@ export class Create${entityName}Dto {
|
|
|
229
246
|
}
|
|
230
247
|
|
|
231
248
|
/**
|
|
232
|
-
* DTO for updating a ${entityName}
|
|
249
|
+
* DTO for updating a ${entityName} (All fields are optional)
|
|
233
250
|
*/
|
|
234
251
|
${
|
|
235
252
|
useSwagger
|
|
@@ -64,7 +64,7 @@ async function generateCleanModule(name, config, entityData) {
|
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
// 4. Génération du Repository (Implementation)
|
|
67
|
-
await generateRepository(name, config.orm);
|
|
67
|
+
await generateRepository(name, config.orm, entityData);
|
|
68
68
|
|
|
69
69
|
// 5. Génération des Use Cases (Create, Get, Update, Delete)
|
|
70
70
|
await generateUseCases(entityPath, entityNameCapitalized, entityNameLower);
|
|
@@ -83,10 +83,25 @@ async function generateCleanModule(name, config, entityData) {
|
|
|
83
83
|
contente: mapperContent,
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
+
if (entityNameLower === "user") {
|
|
87
|
+
await createDirectory(`${entityPath}/domain/enums`);
|
|
88
|
+
await createFile({
|
|
89
|
+
path: `${entityPath}/domain/enums/role.enum.ts`,
|
|
90
|
+
contente: `// Enumération des rôles utilisateurs
|
|
91
|
+
export enum Role {
|
|
92
|
+
USER = 'USER',
|
|
93
|
+
ADMIN = 'ADMIN',
|
|
94
|
+
SUPER_ADMIN = 'SUPER_ADMIN',
|
|
95
|
+
}
|
|
96
|
+
`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
86
100
|
// ÉTAPE RELATIONS : Patching des fichiers si une relation existe
|
|
87
101
|
if (
|
|
88
102
|
entityData.relation &&
|
|
89
|
-
|
|
103
|
+
entityData.relation.type !== "1-n" &&
|
|
104
|
+
entityData.relation.type !== "n-n"
|
|
90
105
|
) {
|
|
91
106
|
const { target, type } = entityData.relation;
|
|
92
107
|
logInfo(
|
|
@@ -114,6 +129,7 @@ async function generateCleanModule(name, config, entityData) {
|
|
|
114
129
|
name,
|
|
115
130
|
entityPath,
|
|
116
131
|
config.swagger,
|
|
132
|
+
config.auth || false,
|
|
117
133
|
);
|
|
118
134
|
await createFile({
|
|
119
135
|
path: `${entityPath}/presentation/controllers/${entityNameLower}.controller.ts`,
|
|
@@ -146,6 +162,10 @@ async function generateCleanModule(name, config, entityData) {
|
|
|
146
162
|
// --- HELPERS DE TEMPLATES (Extraits de ton setup original) ---
|
|
147
163
|
|
|
148
164
|
function getRepositoryInterfaceTemplate(cap, low) {
|
|
165
|
+
let findByEmailMethod = "";
|
|
166
|
+
if (low === "user") {
|
|
167
|
+
findByEmailMethod = `\n findByEmail(email: string): Promise<${cap}Entity | null>;`;
|
|
168
|
+
}
|
|
149
169
|
return `import { Create${cap}Dto, Update${cap}Dto } from 'src/${low}/application/dtos/${low}.dto';
|
|
150
170
|
import { ${cap}Entity } from 'src/${low}/domain/entities/${low}.entity';
|
|
151
171
|
|
|
@@ -154,9 +174,9 @@ export const I${cap}RepositoryName = 'I${cap}Repository';
|
|
|
154
174
|
export interface I${cap}Repository {
|
|
155
175
|
create(data: Create${cap}Dto): Promise<${cap}Entity>;
|
|
156
176
|
findById(id: string): Promise<${cap}Entity | null>;
|
|
157
|
-
findAll(): Promise
|
|
177
|
+
findAll(page?: number, limit?: number, search?: string, sortBy?: string, sortOrder?: string): Promise<{ data: ${cap}Entity[], total: number }>;
|
|
158
178
|
update(id: string, data: Update${cap}Dto): Promise<${cap}Entity | null>;
|
|
159
|
-
delete(id: string): Promise<void
|
|
179
|
+
delete(id: string): Promise<void>;${findByEmailMethod}
|
|
160
180
|
}`;
|
|
161
181
|
}
|
|
162
182
|
|
|
@@ -237,11 +257,20 @@ export class GetAll${cap}UseCase {
|
|
|
237
257
|
private readonly repository: I${cap}Repository,
|
|
238
258
|
) {}
|
|
239
259
|
|
|
240
|
-
async execute(): Promise
|
|
241
|
-
this.logger.log(
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
260
|
+
async execute(page: number = 1, limit: number = 10, search?: string, sortBy?: string, sortOrder?: string): Promise<{ data: ${cap}Entity[], meta: { total: number, page: number, limit: number, totalPages: number } }> {
|
|
261
|
+
this.logger.log(\`Requesting page \${page} of ${cap} records (limit: \${limit}, search: \${search}, sortBy: \${sortBy}, sortOrder: \${sortOrder})\`);
|
|
262
|
+
const { data, total } = await this.repository.findAll(page, limit, search, sortBy, sortOrder);
|
|
263
|
+
const totalPages = Math.ceil(total / limit);
|
|
264
|
+
this.logger.log(\`Retrieved \${data.length} of \${total} ${cap}(s)\`);
|
|
265
|
+
return {
|
|
266
|
+
data,
|
|
267
|
+
meta: {
|
|
268
|
+
total,
|
|
269
|
+
page,
|
|
270
|
+
limit,
|
|
271
|
+
totalPages,
|
|
272
|
+
},
|
|
273
|
+
};
|
|
245
274
|
}
|
|
246
275
|
}`;
|
|
247
276
|
break;
|
|
@@ -430,8 +459,8 @@ export class ${entityNameCapitalized}Service {
|
|
|
430
459
|
async getById(id: string): Promise<${entityNameCapitalized}Entity | null> {
|
|
431
460
|
return await this.getByIdUseCase.execute(id);
|
|
432
461
|
}
|
|
433
|
-
async getAll(): Promise
|
|
434
|
-
return await this.getAllUseCase.execute();
|
|
462
|
+
async getAll(page: number = 1, limit: number = 10, search?: string, sortBy?: string, sortOrder?: string): Promise<{ data: ${entityNameCapitalized}Entity[], meta: { total: number, page: number, limit: number, totalPages: number } }> {
|
|
463
|
+
return await this.getAllUseCase.execute(page, limit, search, sortBy, sortOrder);
|
|
435
464
|
}
|
|
436
465
|
async delete(id: string): Promise<void> {
|
|
437
466
|
return await this.deleteUseCase.execute(id);
|
|
@@ -33,42 +33,10 @@ const { createDirectory, createFile, updateFile } = require("../../file-system")
|
|
|
33
33
|
* @returns {string}
|
|
34
34
|
*/
|
|
35
35
|
function getExceptionFilterContent(orm) {
|
|
36
|
+
let dbSpecificChecks = "";
|
|
36
37
|
if (orm === "prisma") {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
ExceptionFilter,
|
|
40
|
-
Catch,
|
|
41
|
-
ArgumentsHost,
|
|
42
|
-
HttpException,
|
|
43
|
-
HttpStatus,
|
|
44
|
-
Logger,
|
|
45
|
-
} from '@nestjs/common';
|
|
46
|
-
import { Request, Response } from 'express';
|
|
47
|
-
|
|
48
|
-
@Catch()
|
|
49
|
-
export class AllExceptionsFilter implements ExceptionFilter {
|
|
50
|
-
private readonly logger = new Logger(AllExceptionsFilter.name);
|
|
51
|
-
|
|
52
|
-
catch(exception: unknown, host: ArgumentsHost) {
|
|
53
|
-
const ctx = host.switchToHttp();
|
|
54
|
-
const response = ctx.getResponse<Response>();
|
|
55
|
-
const request = ctx.getRequest<Request>();
|
|
56
|
-
|
|
57
|
-
let status = HttpStatus.INTERNAL_SERVER_ERROR;
|
|
58
|
-
let message: string | string[] = 'Internal server error';
|
|
59
|
-
let errorDetails: any = null;
|
|
60
|
-
|
|
61
|
-
if (exception instanceof HttpException) {
|
|
62
|
-
status = exception.getStatus();
|
|
63
|
-
const res = exception.getResponse();
|
|
64
|
-
if (typeof res === 'string') {
|
|
65
|
-
message = res;
|
|
66
|
-
} else if (typeof res === 'object' && res !== null) {
|
|
67
|
-
const resObj = res as any;
|
|
68
|
-
message = resObj.message || resObj.error || 'HttpException';
|
|
69
|
-
errorDetails = resObj;
|
|
70
|
-
}
|
|
71
|
-
} else if (
|
|
38
|
+
dbSpecificChecks = `
|
|
39
|
+
else if (
|
|
72
40
|
typeof exception === 'object' &&
|
|
73
41
|
exception &&
|
|
74
42
|
exception.constructor &&
|
|
@@ -78,75 +46,13 @@ export class AllExceptionsFilter implements ExceptionFilter {
|
|
|
78
46
|
)
|
|
79
47
|
) {
|
|
80
48
|
status = HttpStatus.BAD_REQUEST;
|
|
81
|
-
message = (exception as any).message || 'Prisma error';
|
|
49
|
+
message = (exception as any).message || 'Prisma database error';
|
|
50
|
+
errorResponseName = exception.constructor.name;
|
|
82
51
|
errorDetails = exception;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
stack: exception.stack,
|
|
88
|
-
};
|
|
89
|
-
} else {
|
|
90
|
-
message = 'Une erreur inattendue est survenue';
|
|
91
|
-
errorDetails = exception;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
95
|
-
this.logger.error(
|
|
96
|
-
\`Exception on \${request.method} \${request.url}\`,
|
|
97
|
-
JSON.stringify({ message, status, errorDetails, exception }),
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
response.status(status).json({
|
|
102
|
-
statusCode: status,
|
|
103
|
-
timestamp: new Date().toISOString(),
|
|
104
|
-
path: request.url,
|
|
105
|
-
method: request.method,
|
|
106
|
-
message,
|
|
107
|
-
error: errorDetails,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
`.trim();
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (orm === "mongoose") {
|
|
115
|
-
return `
|
|
116
|
-
import {
|
|
117
|
-
ExceptionFilter,
|
|
118
|
-
Catch,
|
|
119
|
-
ArgumentsHost,
|
|
120
|
-
HttpException,
|
|
121
|
-
HttpStatus,
|
|
122
|
-
Logger,
|
|
123
|
-
} from '@nestjs/common';
|
|
124
|
-
import { Request, Response } from 'express';
|
|
125
|
-
|
|
126
|
-
@Catch()
|
|
127
|
-
export class AllExceptionsFilter implements ExceptionFilter {
|
|
128
|
-
private readonly logger = new Logger(AllExceptionsFilter.name);
|
|
129
|
-
|
|
130
|
-
catch(exception: unknown, host: ArgumentsHost) {
|
|
131
|
-
const ctx = host.switchToHttp();
|
|
132
|
-
const response = ctx.getResponse<Response>();
|
|
133
|
-
const request = ctx.getRequest<Request>();
|
|
134
|
-
|
|
135
|
-
let status = HttpStatus.INTERNAL_SERVER_ERROR;
|
|
136
|
-
let message: string | string[] = 'Internal server error';
|
|
137
|
-
let errorDetails: any = null;
|
|
138
|
-
|
|
139
|
-
if (exception instanceof HttpException) {
|
|
140
|
-
status = exception.getStatus();
|
|
141
|
-
const res = exception.getResponse();
|
|
142
|
-
if (typeof res === 'string') {
|
|
143
|
-
message = res;
|
|
144
|
-
} else if (typeof res === 'object' && res !== null) {
|
|
145
|
-
const resObj = res as any;
|
|
146
|
-
message = resObj.message || resObj.error || 'HttpException';
|
|
147
|
-
errorDetails = resObj;
|
|
148
|
-
}
|
|
149
|
-
} else if (
|
|
52
|
+
}`;
|
|
53
|
+
} else if (orm === "mongoose") {
|
|
54
|
+
dbSpecificChecks = `
|
|
55
|
+
else if (
|
|
150
56
|
typeof exception === 'object' &&
|
|
151
57
|
exception &&
|
|
152
58
|
'name' in exception &&
|
|
@@ -156,75 +62,78 @@ export class AllExceptionsFilter implements ExceptionFilter {
|
|
|
156
62
|
)
|
|
157
63
|
) {
|
|
158
64
|
status = HttpStatus.BAD_REQUEST;
|
|
159
|
-
message = (exception as any).message || '
|
|
65
|
+
message = (exception as any).message || 'Mongoose database error';
|
|
66
|
+
errorResponseName = (exception as any).name;
|
|
160
67
|
errorDetails = exception;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
68
|
+
}`;
|
|
69
|
+
} else if (orm === "typeorm") {
|
|
70
|
+
dbSpecificChecks = `
|
|
71
|
+
else if (
|
|
72
|
+
typeof exception === 'object' &&
|
|
73
|
+
exception &&
|
|
74
|
+
'name' in exception &&
|
|
75
|
+
(
|
|
76
|
+
(exception as any).name === 'QueryFailedError' ||
|
|
77
|
+
(exception as any).name === 'EntityNotFoundError' ||
|
|
78
|
+
(exception as any).name === 'CannotCreateEntityIdMapError'
|
|
79
|
+
)
|
|
80
|
+
) {
|
|
81
|
+
status = HttpStatus.BAD_REQUEST;
|
|
82
|
+
message = (exception as any).message || 'TypeORM database error';
|
|
83
|
+
errorResponseName = (exception as any).name;
|
|
84
|
+
errorDetails = exception;
|
|
85
|
+
}`;
|
|
86
|
+
} else if (orm === "sequelize") {
|
|
87
|
+
dbSpecificChecks = `
|
|
88
|
+
else if (
|
|
89
|
+
typeof exception === 'object' &&
|
|
90
|
+
exception &&
|
|
91
|
+
exception.constructor &&
|
|
92
|
+
(
|
|
93
|
+
exception.constructor.name === 'SequelizeDatabaseError' ||
|
|
94
|
+
exception.constructor.name === 'SequelizeValidationError'
|
|
95
|
+
)
|
|
96
|
+
) {
|
|
97
|
+
status = HttpStatus.BAD_REQUEST;
|
|
98
|
+
message = (exception as any).message || 'Sequelize database error';
|
|
99
|
+
errorResponseName = exception.constructor.name;
|
|
100
|
+
errorDetails = exception;
|
|
101
|
+
}`;
|
|
102
|
+
} else {
|
|
103
|
+
// fallback global / universal
|
|
104
|
+
dbSpecificChecks = `
|
|
105
|
+
// Prisma
|
|
106
|
+
else if (
|
|
107
|
+
typeof exception === 'object' &&
|
|
108
|
+
exception &&
|
|
109
|
+
exception.constructor &&
|
|
110
|
+
(
|
|
111
|
+
exception.constructor.name === 'PrismaClientKnownRequestError' ||
|
|
112
|
+
exception.constructor.name === 'PrismaClientValidationError'
|
|
113
|
+
)
|
|
114
|
+
) {
|
|
115
|
+
status = HttpStatus.BAD_REQUEST;
|
|
116
|
+
message = (exception as any).message || 'Prisma database error';
|
|
117
|
+
errorResponseName = exception.constructor.name;
|
|
169
118
|
errorDetails = exception;
|
|
170
119
|
}
|
|
171
|
-
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
120
|
+
// Mongoose/Mongo
|
|
121
|
+
else if (
|
|
122
|
+
typeof exception === 'object' &&
|
|
123
|
+
exception &&
|
|
124
|
+
'name' in exception &&
|
|
125
|
+
(
|
|
126
|
+
(exception as any).name === 'MongoError' ||
|
|
127
|
+
(exception as any).name === 'MongooseError'
|
|
128
|
+
)
|
|
129
|
+
) {
|
|
130
|
+
status = HttpStatus.BAD_REQUEST;
|
|
131
|
+
message = (exception as any).message || 'Mongoose database error';
|
|
132
|
+
errorResponseName = (exception as any).name;
|
|
133
|
+
errorDetails = exception;
|
|
177
134
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
statusCode: status,
|
|
181
|
-
timestamp: new Date().toISOString(),
|
|
182
|
-
path: request.url,
|
|
183
|
-
method: request.method,
|
|
184
|
-
message,
|
|
185
|
-
error: errorDetails,
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
`.trim();
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (orm === "typeorm") {
|
|
193
|
-
return `
|
|
194
|
-
import {
|
|
195
|
-
ExceptionFilter,
|
|
196
|
-
Catch,
|
|
197
|
-
ArgumentsHost,
|
|
198
|
-
HttpException,
|
|
199
|
-
HttpStatus,
|
|
200
|
-
Logger,
|
|
201
|
-
} from '@nestjs/common';
|
|
202
|
-
import { Request, Response } from 'express';
|
|
203
|
-
|
|
204
|
-
@Catch()
|
|
205
|
-
export class AllExceptionsFilter implements ExceptionFilter {
|
|
206
|
-
private readonly logger = new Logger(AllExceptionsFilter.name);
|
|
207
|
-
|
|
208
|
-
catch(exception: unknown, host: ArgumentsHost) {
|
|
209
|
-
const ctx = host.switchToHttp();
|
|
210
|
-
const response = ctx.getResponse<Response>();
|
|
211
|
-
const request = ctx.getRequest<Request>();
|
|
212
|
-
|
|
213
|
-
let status = HttpStatus.INTERNAL_SERVER_ERROR;
|
|
214
|
-
let message: string | string[] = 'Internal server error';
|
|
215
|
-
let errorDetails: any = null;
|
|
216
|
-
|
|
217
|
-
if (exception instanceof HttpException) {
|
|
218
|
-
status = exception.getStatus();
|
|
219
|
-
const res = exception.getResponse();
|
|
220
|
-
if (typeof res === 'string') {
|
|
221
|
-
message = res;
|
|
222
|
-
} else if (typeof res === 'object' && res !== null) {
|
|
223
|
-
const resObj = res as any;
|
|
224
|
-
message = resObj.message || resObj.error || 'HttpException';
|
|
225
|
-
errorDetails = resObj;
|
|
226
|
-
}
|
|
227
|
-
} else if (
|
|
135
|
+
// TypeORM
|
|
136
|
+
else if (
|
|
228
137
|
typeof exception === 'object' &&
|
|
229
138
|
exception &&
|
|
230
139
|
'name' in exception &&
|
|
@@ -235,75 +144,12 @@ export class AllExceptionsFilter implements ExceptionFilter {
|
|
|
235
144
|
)
|
|
236
145
|
) {
|
|
237
146
|
status = HttpStatus.BAD_REQUEST;
|
|
238
|
-
message = (exception as any).message || 'TypeORM error';
|
|
239
|
-
|
|
240
|
-
} else if (exception instanceof Error) {
|
|
241
|
-
message = exception.message;
|
|
242
|
-
errorDetails = {
|
|
243
|
-
name: exception.name,
|
|
244
|
-
stack: exception.stack,
|
|
245
|
-
};
|
|
246
|
-
} else {
|
|
247
|
-
message = 'Une erreur inattendue est survenue';
|
|
147
|
+
message = (exception as any).message || 'TypeORM database error';
|
|
148
|
+
errorResponseName = (exception as any).name;
|
|
248
149
|
errorDetails = exception;
|
|
249
150
|
}
|
|
250
|
-
|
|
251
|
-
if (
|
|
252
|
-
this.logger.error(
|
|
253
|
-
\`Exception on \${request.method} \${request.url}\`,
|
|
254
|
-
JSON.stringify({ message, status, errorDetails, exception }),
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
response.status(status).json({
|
|
259
|
-
statusCode: status,
|
|
260
|
-
timestamp: new Date().toISOString(),
|
|
261
|
-
path: request.url,
|
|
262
|
-
method: request.method,
|
|
263
|
-
message,
|
|
264
|
-
error: errorDetails,
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
`.trim();
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (orm === "sequelize") {
|
|
272
|
-
return `
|
|
273
|
-
import {
|
|
274
|
-
ExceptionFilter,
|
|
275
|
-
Catch,
|
|
276
|
-
ArgumentsHost,
|
|
277
|
-
HttpException,
|
|
278
|
-
HttpStatus,
|
|
279
|
-
Logger,
|
|
280
|
-
} from '@nestjs/common';
|
|
281
|
-
import { Request, Response } from 'express';
|
|
282
|
-
|
|
283
|
-
@Catch()
|
|
284
|
-
export class AllExceptionsFilter implements ExceptionFilter {
|
|
285
|
-
private readonly logger = new Logger(AllExceptionsFilter.name);
|
|
286
|
-
|
|
287
|
-
catch(exception: unknown, host: ArgumentsHost) {
|
|
288
|
-
const ctx = host.switchToHttp();
|
|
289
|
-
const response = ctx.getResponse<Response>();
|
|
290
|
-
const request = ctx.getRequest<Request>();
|
|
291
|
-
|
|
292
|
-
let status = HttpStatus.INTERNAL_SERVER_ERROR;
|
|
293
|
-
let message: string | string[] = 'Internal server error';
|
|
294
|
-
let errorDetails: any = null;
|
|
295
|
-
|
|
296
|
-
if (exception instanceof HttpException) {
|
|
297
|
-
status = exception.getStatus();
|
|
298
|
-
const res = exception.getResponse();
|
|
299
|
-
if (typeof res === 'string') {
|
|
300
|
-
message = res;
|
|
301
|
-
} else if (typeof res === 'object' && res !== null) {
|
|
302
|
-
const resObj = res as any;
|
|
303
|
-
message = resObj.message || resObj.error || 'HttpException';
|
|
304
|
-
errorDetails = resObj;
|
|
305
|
-
}
|
|
306
|
-
} else if (
|
|
151
|
+
// Sequelize
|
|
152
|
+
else if (
|
|
307
153
|
typeof exception === 'object' &&
|
|
308
154
|
exception &&
|
|
309
155
|
exception.constructor &&
|
|
@@ -313,40 +159,12 @@ export class AllExceptionsFilter implements ExceptionFilter {
|
|
|
313
159
|
)
|
|
314
160
|
) {
|
|
315
161
|
status = HttpStatus.BAD_REQUEST;
|
|
316
|
-
message = (exception as any).message || 'Sequelize error';
|
|
317
|
-
|
|
318
|
-
} else if (exception instanceof Error) {
|
|
319
|
-
message = exception.message;
|
|
320
|
-
errorDetails = {
|
|
321
|
-
name: exception.name,
|
|
322
|
-
stack: exception.stack,
|
|
323
|
-
};
|
|
324
|
-
} else {
|
|
325
|
-
message = 'Une erreur inattendue est survenue';
|
|
162
|
+
message = (exception as any).message || 'Sequelize database error';
|
|
163
|
+
errorResponseName = exception.constructor.name;
|
|
326
164
|
errorDetails = exception;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
330
|
-
this.logger.error(
|
|
331
|
-
\`Exception on \${request.method} \${request.url}\`,
|
|
332
|
-
JSON.stringify({ message, status, errorDetails, exception }),
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
response.status(status).json({
|
|
337
|
-
statusCode: status,
|
|
338
|
-
timestamp: new Date().toISOString(),
|
|
339
|
-
path: request.url,
|
|
340
|
-
method: request.method,
|
|
341
|
-
message,
|
|
342
|
-
error: errorDetails,
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
`.trim();
|
|
165
|
+
}`;
|
|
347
166
|
}
|
|
348
167
|
|
|
349
|
-
// Version universelle (multi-ORM / fallback)
|
|
350
168
|
return `
|
|
351
169
|
import {
|
|
352
170
|
ExceptionFilter,
|
|
@@ -358,6 +176,15 @@ import {
|
|
|
358
176
|
} from '@nestjs/common';
|
|
359
177
|
import { Request, Response } from 'express';
|
|
360
178
|
|
|
179
|
+
export interface ErrorResponse {
|
|
180
|
+
statusCode: number;
|
|
181
|
+
timestamp: string;
|
|
182
|
+
path: string;
|
|
183
|
+
method: string;
|
|
184
|
+
message: string | string[];
|
|
185
|
+
error: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
361
188
|
@Catch()
|
|
362
189
|
export class AllExceptionsFilter implements ExceptionFilter {
|
|
363
190
|
private readonly logger = new Logger(AllExceptionsFilter.name);
|
|
@@ -369,11 +196,13 @@ export class AllExceptionsFilter implements ExceptionFilter {
|
|
|
369
196
|
|
|
370
197
|
let status = HttpStatus.INTERNAL_SERVER_ERROR;
|
|
371
198
|
let message: string | string[] = 'Internal server error';
|
|
199
|
+
let errorResponseName = 'InternalServerError';
|
|
372
200
|
let errorDetails: any = null;
|
|
373
201
|
|
|
374
202
|
if (exception instanceof HttpException) {
|
|
375
203
|
status = exception.getStatus();
|
|
376
204
|
const res = exception.getResponse();
|
|
205
|
+
errorResponseName = exception.name || 'HttpException';
|
|
377
206
|
if (typeof res === 'string') {
|
|
378
207
|
message = res;
|
|
379
208
|
} else if (typeof res === 'object' && res !== null) {
|
|
@@ -381,51 +210,10 @@ export class AllExceptionsFilter implements ExceptionFilter {
|
|
|
381
210
|
message = resObj.message || resObj.error || 'HttpException';
|
|
382
211
|
errorDetails = resObj;
|
|
383
212
|
}
|
|
384
|
-
}
|
|
385
|
-
// Prisma
|
|
386
|
-
else if (
|
|
387
|
-
typeof exception === 'object' &&
|
|
388
|
-
exception &&
|
|
389
|
-
exception.constructor &&
|
|
390
|
-
(
|
|
391
|
-
exception.constructor.name === 'PrismaClientKnownRequestError' ||
|
|
392
|
-
exception.constructor.name === 'PrismaClientValidationError'
|
|
393
|
-
)
|
|
394
|
-
) {
|
|
395
|
-
status = HttpStatus.BAD_REQUEST;
|
|
396
|
-
message = (exception as any).message || 'Prisma error';
|
|
397
|
-
errorDetails = exception;
|
|
398
|
-
}
|
|
399
|
-
// Mongoose/Mongo
|
|
400
|
-
else if (
|
|
401
|
-
typeof exception === 'object' &&
|
|
402
|
-
exception &&
|
|
403
|
-
'name' in exception &&
|
|
404
|
-
(
|
|
405
|
-
(exception as any).name === 'MongoError' ||
|
|
406
|
-
(exception as any).name === 'MongooseError'
|
|
407
|
-
)
|
|
408
|
-
) {
|
|
409
|
-
status = HttpStatus.BAD_REQUEST;
|
|
410
|
-
message = (exception as any).message || 'MongoDB error';
|
|
411
|
-
errorDetails = exception;
|
|
412
|
-
}
|
|
413
|
-
// Sequelize
|
|
414
|
-
else if (
|
|
415
|
-
typeof exception === 'object' &&
|
|
416
|
-
exception &&
|
|
417
|
-
exception.constructor &&
|
|
418
|
-
(
|
|
419
|
-
exception.constructor.name === 'SequelizeDatabaseError' ||
|
|
420
|
-
exception.constructor.name === 'SequelizeValidationError'
|
|
421
|
-
)
|
|
422
|
-
) {
|
|
423
|
-
status = HttpStatus.BAD_REQUEST;
|
|
424
|
-
message = (exception as any).message || 'Sequelize error';
|
|
425
|
-
errorDetails = exception;
|
|
426
|
-
}
|
|
213
|
+
}${dbSpecificChecks}
|
|
427
214
|
else if (exception instanceof Error) {
|
|
428
215
|
message = exception.message;
|
|
216
|
+
errorResponseName = exception.name;
|
|
429
217
|
errorDetails = {
|
|
430
218
|
name: exception.name,
|
|
431
219
|
stack: exception.stack,
|
|
@@ -436,18 +224,27 @@ export class AllExceptionsFilter implements ExceptionFilter {
|
|
|
436
224
|
}
|
|
437
225
|
|
|
438
226
|
this.logger.error(
|
|
439
|
-
\`
|
|
440
|
-
|
|
227
|
+
\`[\${request.method}] \${request.url} - Error status: \${status}\`,
|
|
228
|
+
exception instanceof Error ? exception.stack : JSON.stringify(exception),
|
|
441
229
|
);
|
|
442
230
|
|
|
443
|
-
|
|
231
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
232
|
+
const payload: ErrorResponse = {
|
|
444
233
|
statusCode: status,
|
|
445
234
|
timestamp: new Date().toISOString(),
|
|
446
235
|
path: request.url,
|
|
447
236
|
method: request.method,
|
|
448
|
-
message
|
|
449
|
-
|
|
450
|
-
|
|
237
|
+
message: (status === HttpStatus.INTERNAL_SERVER_ERROR && isProduction)
|
|
238
|
+
? 'Internal server error'
|
|
239
|
+
: message,
|
|
240
|
+
error: isProduction ? errorResponseName : (errorDetails?.error || errorResponseName),
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
if (!isProduction && errorDetails) {
|
|
244
|
+
(payload as any).details = errorDetails;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
response.status(status).json(payload);
|
|
451
248
|
}
|
|
452
249
|
}
|
|
453
250
|
`.trim();
|