create-forgeon 0.1.21 → 0.1.23
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/README.md +11 -5
- package/package.json +1 -1
- package/src/modules/executor.test.mjs +4 -2
- package/src/modules/i18n.mjs +0 -24
- package/src/presets/i18n.mjs +9 -82
- package/templates/base/apps/api/src/app.module.ts +7 -7
- package/templates/base/apps/api/src/main.ts +8 -9
- package/templates/base/packages/core/README.md +1 -0
- package/templates/base/packages/core/src/errors/core-errors.module.ts +8 -0
- package/templates/base/packages/core/src/errors/core-exception.filter.ts +137 -0
- package/templates/base/packages/core/src/errors/error.types.ts +19 -0
- package/templates/base/packages/core/src/errors/index.ts +3 -0
- package/templates/base/packages/core/src/index.ts +1 -0
- package/templates/base/apps/api/src/common/filters/app-exception.filter.ts +0 -130
package/README.md
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
# create-forgeon
|
|
2
|
-
|
|
3
|
-
CLI package for generating Forgeon fullstack monorepo projects.
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
# create-forgeon
|
|
2
|
+
|
|
3
|
+
CLI package for generating Forgeon fullstack monorepo projects.
|
|
4
|
+
|
|
5
|
+
> [!WARNING]
|
|
6
|
+
> **Pre-release package. Do not use in production before `1.0.0`.**
|
|
7
|
+
> The project is under active development: each patch can add changes and may introduce breaking regressions.
|
|
8
|
+
>
|
|
9
|
+
> 
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
6
12
|
|
|
7
13
|
```bash
|
|
8
14
|
npx create-forgeon@latest my-app --i18n true --proxy caddy
|
package/package.json
CHANGED
|
@@ -115,6 +115,10 @@ describe('addModule', () => {
|
|
|
115
115
|
assert.match(appModule, /i18nConfig/);
|
|
116
116
|
assert.match(appModule, /i18nEnvSchema/);
|
|
117
117
|
assert.match(appModule, /CoreConfigModule/);
|
|
118
|
+
assert.match(appModule, /CoreErrorsModule/);
|
|
119
|
+
|
|
120
|
+
const mainTs = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'main.ts'), 'utf8');
|
|
121
|
+
assert.match(mainTs, /CoreExceptionFilter/);
|
|
118
122
|
|
|
119
123
|
const forgeonI18nModule = fs.readFileSync(
|
|
120
124
|
path.join(projectRoot, 'packages', 'i18n', 'src', 'forgeon-i18n.module.ts'),
|
|
@@ -198,8 +202,6 @@ describe('addModule', () => {
|
|
|
198
202
|
assert.match(rootPackage, /"i18n:sync"/);
|
|
199
203
|
assert.match(rootPackage, /"i18n:check"/);
|
|
200
204
|
assert.match(rootPackage, /"i18n:types"/);
|
|
201
|
-
assert.match(rootPackage, /postinstall/);
|
|
202
|
-
assert.match(rootPackage, /pnpm i18n:sync/);
|
|
203
205
|
|
|
204
206
|
const caddyDockerfile = fs.readFileSync(
|
|
205
207
|
path.join(projectRoot, 'infra', 'docker', 'caddy.Dockerfile'),
|
package/src/modules/i18n.mjs
CHANGED
|
@@ -34,24 +34,6 @@ function ensureScript(packageJson, name, command) {
|
|
|
34
34
|
packageJson.scripts[name] = command;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
function ensurePostinstallStep(packageJson, command) {
|
|
38
|
-
if (!packageJson.scripts) {
|
|
39
|
-
packageJson.scripts = {};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const current = packageJson.scripts.postinstall;
|
|
43
|
-
if (!current) {
|
|
44
|
-
packageJson.scripts.postinstall = command;
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (current.includes(command)) {
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
packageJson.scripts.postinstall = `${current} && ${command}`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
37
|
function upsertEnvLines(filePath, lines) {
|
|
56
38
|
let content = '';
|
|
57
39
|
if (fs.existsSync(filePath)) {
|
|
@@ -312,7 +294,6 @@ function patchRootPackage(targetRoot) {
|
|
|
312
294
|
'i18n:types',
|
|
313
295
|
'pnpm --filter @forgeon/i18n-contracts i18n:types',
|
|
314
296
|
);
|
|
315
|
-
ensurePostinstallStep(packageJson, 'pnpm i18n:sync');
|
|
316
297
|
writeJson(packagePath, packageJson);
|
|
317
298
|
}
|
|
318
299
|
|
|
@@ -332,11 +313,6 @@ export function applyI18nModule({ packageRoot, targetRoot }) {
|
|
|
332
313
|
targetRoot,
|
|
333
314
|
path.join('apps', 'api', 'src', 'health', 'health.controller.ts'),
|
|
334
315
|
);
|
|
335
|
-
copyFromBase(
|
|
336
|
-
packageRoot,
|
|
337
|
-
targetRoot,
|
|
338
|
-
path.join('apps', 'api', 'src', 'common', 'filters', 'app-exception.filter.ts'),
|
|
339
|
-
);
|
|
340
316
|
|
|
341
317
|
patchI18nPackage(targetRoot);
|
|
342
318
|
patchApiPackage(targetRoot);
|
package/src/presets/i18n.mjs
CHANGED
|
@@ -110,10 +110,9 @@ export function applyI18nDisabled(targetRoot) {
|
|
|
110
110
|
appModulePath,
|
|
111
111
|
`import { Module } from '@nestjs/common';
|
|
112
112
|
import { ConfigModule } from '@nestjs/config';
|
|
113
|
-
import { CoreConfigModule, coreConfig, validateCoreEnv } from '@forgeon/core';
|
|
113
|
+
import { CoreConfigModule, CoreErrorsModule, coreConfig, validateCoreEnv } from '@forgeon/core';
|
|
114
114
|
import { HealthController } from './health/health.controller';
|
|
115
115
|
import { PrismaModule } from './prisma/prisma.module';
|
|
116
|
-
import { AppExceptionFilter } from './common/filters/app-exception.filter';
|
|
117
116
|
|
|
118
117
|
@Module({
|
|
119
118
|
imports: [
|
|
@@ -124,15 +123,15 @@ import { AppExceptionFilter } from './common/filters/app-exception.filter';
|
|
|
124
123
|
envFilePath: '.env',
|
|
125
124
|
}),
|
|
126
125
|
CoreConfigModule,
|
|
126
|
+
CoreErrorsModule,
|
|
127
127
|
PrismaModule,
|
|
128
128
|
],
|
|
129
129
|
controllers: [HealthController],
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
);
|
|
130
|
+
})
|
|
131
|
+
export class AppModule {}
|
|
132
|
+
`,
|
|
133
|
+
'utf8',
|
|
134
|
+
);
|
|
136
135
|
|
|
137
136
|
const healthControllerPath = path.join(
|
|
138
137
|
targetRoot,
|
|
@@ -166,80 +165,8 @@ export class HealthController {
|
|
|
166
165
|
'utf8',
|
|
167
166
|
);
|
|
168
167
|
|
|
169
|
-
|
|
170
|
-
targetRoot,
|
|
171
|
-
'apps',
|
|
172
|
-
'api',
|
|
173
|
-
'src',
|
|
174
|
-
'common',
|
|
175
|
-
'filters',
|
|
176
|
-
'app-exception.filter.ts',
|
|
177
|
-
);
|
|
178
|
-
fs.writeFileSync(
|
|
179
|
-
filterPath,
|
|
180
|
-
`import {
|
|
181
|
-
ArgumentsHost,
|
|
182
|
-
Catch,
|
|
183
|
-
ExceptionFilter,
|
|
184
|
-
HttpException,
|
|
185
|
-
HttpStatus,
|
|
186
|
-
Injectable,
|
|
187
|
-
} from '@nestjs/common';
|
|
188
|
-
import { Response } from 'express';
|
|
189
|
-
|
|
190
|
-
@Injectable()
|
|
191
|
-
@Catch()
|
|
192
|
-
export class AppExceptionFilter implements ExceptionFilter {
|
|
193
|
-
catch(exception: unknown, host: ArgumentsHost): void {
|
|
194
|
-
const context = host.switchToHttp();
|
|
195
|
-
const response = context.getResponse<Response>();
|
|
196
|
-
|
|
197
|
-
const status =
|
|
198
|
-
exception instanceof HttpException
|
|
199
|
-
? exception.getStatus()
|
|
200
|
-
: HttpStatus.INTERNAL_SERVER_ERROR;
|
|
201
|
-
|
|
202
|
-
const payload =
|
|
203
|
-
exception instanceof HttpException
|
|
204
|
-
? exception.getResponse()
|
|
205
|
-
: { message: 'Internal server error' };
|
|
206
|
-
|
|
207
|
-
const message =
|
|
208
|
-
typeof payload === 'object' && payload !== null && 'message' in payload
|
|
209
|
-
? Array.isArray((payload as { message?: unknown }).message)
|
|
210
|
-
? String((payload as { message: unknown[] }).message[0] ?? 'Internal server error')
|
|
211
|
-
: String((payload as { message?: unknown }).message ?? 'Internal server error')
|
|
212
|
-
: typeof payload === 'string'
|
|
213
|
-
? payload
|
|
214
|
-
: 'Internal server error';
|
|
215
|
-
|
|
216
|
-
response.status(status).json({
|
|
217
|
-
error: {
|
|
218
|
-
code: this.resolveCode(status),
|
|
219
|
-
message,
|
|
220
|
-
},
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
private resolveCode(status: number): string {
|
|
225
|
-
switch (status) {
|
|
226
|
-
case HttpStatus.BAD_REQUEST:
|
|
227
|
-
return 'validation_error';
|
|
228
|
-
case HttpStatus.UNAUTHORIZED:
|
|
229
|
-
return 'unauthorized';
|
|
230
|
-
case HttpStatus.FORBIDDEN:
|
|
231
|
-
return 'forbidden';
|
|
232
|
-
case HttpStatus.NOT_FOUND:
|
|
233
|
-
return 'not_found';
|
|
234
|
-
case HttpStatus.CONFLICT:
|
|
235
|
-
return 'conflict';
|
|
236
|
-
default:
|
|
237
|
-
return 'internal_error';
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
`,
|
|
242
|
-
'utf8',
|
|
168
|
+
removeIfExists(
|
|
169
|
+
path.join(targetRoot, 'apps', 'api', 'src', 'common', 'filters', 'app-exception.filter.ts'),
|
|
243
170
|
);
|
|
244
171
|
}
|
|
245
172
|
|
|
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
|
|
2
2
|
import { ConfigModule } from '@nestjs/config';
|
|
3
3
|
import {
|
|
4
4
|
CoreConfigModule,
|
|
5
|
+
CoreErrorsModule,
|
|
5
6
|
coreConfig,
|
|
6
7
|
coreEnvSchema,
|
|
7
8
|
createEnvValidator,
|
|
@@ -10,7 +11,6 @@ import { ForgeonI18nModule, i18nConfig, i18nEnvSchema } from '@forgeon/i18n';
|
|
|
10
11
|
import { join } from 'path';
|
|
11
12
|
import { HealthController } from './health/health.controller';
|
|
12
13
|
import { PrismaModule } from './prisma/prisma.module';
|
|
13
|
-
import { AppExceptionFilter } from './common/filters/app-exception.filter';
|
|
14
14
|
|
|
15
15
|
const i18nPath = join(__dirname, '..', '..', '..', 'resources', 'i18n');
|
|
16
16
|
|
|
@@ -23,13 +23,13 @@ const i18nPath = join(__dirname, '..', '..', '..', 'resources', 'i18n');
|
|
|
23
23
|
envFilePath: '.env',
|
|
24
24
|
}),
|
|
25
25
|
CoreConfigModule,
|
|
26
|
+
CoreErrorsModule,
|
|
26
27
|
ForgeonI18nModule.register({
|
|
27
28
|
path: i18nPath,
|
|
28
29
|
}),
|
|
29
|
-
PrismaModule,
|
|
30
|
-
],
|
|
31
|
-
controllers: [HealthController],
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
export class AppModule {}
|
|
30
|
+
PrismaModule,
|
|
31
|
+
],
|
|
32
|
+
controllers: [HealthController],
|
|
33
|
+
})
|
|
34
|
+
export class AppModule {}
|
|
35
35
|
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
2
|
import { ValidationPipe } from '@nestjs/common';
|
|
3
|
-
import { CoreConfigService } from '@forgeon/core';
|
|
3
|
+
import { CoreConfigService, CoreExceptionFilter } from '@forgeon/core';
|
|
4
4
|
import { NestFactory } from '@nestjs/core';
|
|
5
5
|
import { AppModule } from './app.module';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const app = await NestFactory.create(AppModule);
|
|
6
|
+
|
|
7
|
+
async function bootstrap() {
|
|
8
|
+
const app = await NestFactory.create(AppModule);
|
|
10
9
|
|
|
11
10
|
const coreConfigService = app.get(CoreConfigService);
|
|
12
11
|
|
|
@@ -14,10 +13,10 @@ async function bootstrap() {
|
|
|
14
13
|
app.useGlobalPipes(
|
|
15
14
|
new ValidationPipe({
|
|
16
15
|
whitelist: true,
|
|
17
|
-
transform: true,
|
|
18
|
-
}),
|
|
19
|
-
);
|
|
20
|
-
app.useGlobalFilters(app.get(
|
|
16
|
+
transform: true,
|
|
17
|
+
}),
|
|
18
|
+
);
|
|
19
|
+
app.useGlobalFilters(app.get(CoreExceptionFilter));
|
|
21
20
|
|
|
22
21
|
await app.listen(coreConfigService.port);
|
|
23
22
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArgumentsHost,
|
|
3
|
+
Catch,
|
|
4
|
+
ExceptionFilter,
|
|
5
|
+
HttpException,
|
|
6
|
+
HttpStatus,
|
|
7
|
+
Injectable,
|
|
8
|
+
} from '@nestjs/common';
|
|
9
|
+
import { AppErrorDetails, ValidationErrorDetail } from './error.types';
|
|
10
|
+
|
|
11
|
+
type HttpLikeResponse = {
|
|
12
|
+
status: (statusCode: number) => {
|
|
13
|
+
json: (body: unknown) => void;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
@Injectable()
|
|
18
|
+
@Catch()
|
|
19
|
+
export class CoreExceptionFilter implements ExceptionFilter {
|
|
20
|
+
catch(exception: unknown, host: ArgumentsHost): void {
|
|
21
|
+
const context = host.switchToHttp();
|
|
22
|
+
const response = context.getResponse<HttpLikeResponse>();
|
|
23
|
+
const request = context.getRequest<{ headers?: Record<string, unknown> }>();
|
|
24
|
+
|
|
25
|
+
const status =
|
|
26
|
+
exception instanceof HttpException
|
|
27
|
+
? exception.getStatus()
|
|
28
|
+
: HttpStatus.INTERNAL_SERVER_ERROR;
|
|
29
|
+
|
|
30
|
+
const payload =
|
|
31
|
+
exception instanceof HttpException
|
|
32
|
+
? exception.getResponse()
|
|
33
|
+
: { message: 'Internal server error' };
|
|
34
|
+
|
|
35
|
+
const requestId = this.resolveRequestId(request?.headers);
|
|
36
|
+
const details = this.resolveDetails(payload, status);
|
|
37
|
+
const timestamp = new Date().toISOString();
|
|
38
|
+
|
|
39
|
+
response.status(status).json({
|
|
40
|
+
error: {
|
|
41
|
+
code: this.resolveCode(status),
|
|
42
|
+
message: this.resolveMessage(payload, status),
|
|
43
|
+
status,
|
|
44
|
+
...(details !== undefined ? { details } : {}),
|
|
45
|
+
...(requestId !== undefined ? { requestId } : {}),
|
|
46
|
+
timestamp,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private resolveCode(status: number): string {
|
|
52
|
+
switch (status) {
|
|
53
|
+
case HttpStatus.BAD_REQUEST:
|
|
54
|
+
return 'validation_error';
|
|
55
|
+
case HttpStatus.UNAUTHORIZED:
|
|
56
|
+
return 'unauthorized';
|
|
57
|
+
case HttpStatus.FORBIDDEN:
|
|
58
|
+
return 'forbidden';
|
|
59
|
+
case HttpStatus.NOT_FOUND:
|
|
60
|
+
return 'not_found';
|
|
61
|
+
case HttpStatus.CONFLICT:
|
|
62
|
+
return 'conflict';
|
|
63
|
+
default:
|
|
64
|
+
return 'internal_error';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private resolveMessage(payload: unknown, status: number): string {
|
|
69
|
+
if (status === HttpStatus.NOT_FOUND) {
|
|
70
|
+
return 'Resource not found';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (typeof payload === 'string') {
|
|
74
|
+
return payload;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (typeof payload === 'object' && payload !== null) {
|
|
78
|
+
const obj = payload as { message?: string | string[] };
|
|
79
|
+
if (Array.isArray(obj.message) && obj.message.length > 0) {
|
|
80
|
+
return String(obj.message[0]);
|
|
81
|
+
}
|
|
82
|
+
if (typeof obj.message === 'string' && obj.message.length > 0) {
|
|
83
|
+
return obj.message;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return status >= 500 ? 'Internal server error' : 'Request failed';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private resolveDetails(payload: unknown, status: number): AppErrorDetails | undefined {
|
|
91
|
+
if (status !== HttpStatus.BAD_REQUEST) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (typeof payload !== 'object' || payload === null) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const obj = payload as { message?: unknown };
|
|
100
|
+
const messages = Array.isArray(obj.message)
|
|
101
|
+
? obj.message.filter((item): item is string => typeof item === 'string')
|
|
102
|
+
: typeof obj.message === 'string'
|
|
103
|
+
? [obj.message]
|
|
104
|
+
: [];
|
|
105
|
+
|
|
106
|
+
if (messages.length === 0) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const details: ValidationErrorDetail[] = messages.map((message) => {
|
|
111
|
+
const field = this.extractField(message);
|
|
112
|
+
return field ? { field, message } : { message };
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return details;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private resolveRequestId(headers: Record<string, unknown> | undefined): string | undefined {
|
|
119
|
+
if (!headers) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const value = headers['x-request-id'];
|
|
124
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
if (Array.isArray(value) && typeof value[0] === 'string' && value[0].trim().length > 0) {
|
|
128
|
+
return value[0];
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private extractField(message: string): string | undefined {
|
|
134
|
+
const match = message.match(/^([A-Za-z0-9_.[\]-]+)\s+/);
|
|
135
|
+
return match?.[1];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ValidationErrorDetail {
|
|
2
|
+
field?: string;
|
|
3
|
+
message: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type AppErrorDetails = ValidationErrorDetail[] | Record<string, unknown>;
|
|
7
|
+
|
|
8
|
+
export interface AppErrorPayload {
|
|
9
|
+
code: string;
|
|
10
|
+
message: string;
|
|
11
|
+
status: number;
|
|
12
|
+
details?: AppErrorDetails;
|
|
13
|
+
requestId?: string;
|
|
14
|
+
timestamp: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AppErrorEnvelope {
|
|
18
|
+
error: AppErrorPayload;
|
|
19
|
+
}
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ArgumentsHost,
|
|
3
|
-
Catch,
|
|
4
|
-
ExceptionFilter,
|
|
5
|
-
HttpException,
|
|
6
|
-
HttpStatus,
|
|
7
|
-
Injectable,
|
|
8
|
-
Optional,
|
|
9
|
-
} from '@nestjs/common';
|
|
10
|
-
import { I18nService } from 'nestjs-i18n';
|
|
11
|
-
import { Request, Response } from 'express';
|
|
12
|
-
|
|
13
|
-
@Injectable()
|
|
14
|
-
@Catch()
|
|
15
|
-
export class AppExceptionFilter implements ExceptionFilter {
|
|
16
|
-
constructor(@Optional() private readonly i18n?: I18nService) {}
|
|
17
|
-
|
|
18
|
-
catch(exception: unknown, host: ArgumentsHost): void {
|
|
19
|
-
const context = host.switchToHttp();
|
|
20
|
-
const response = context.getResponse<Response>();
|
|
21
|
-
const request = context.getRequest<Request>();
|
|
22
|
-
|
|
23
|
-
const status =
|
|
24
|
-
exception instanceof HttpException
|
|
25
|
-
? exception.getStatus()
|
|
26
|
-
: HttpStatus.INTERNAL_SERVER_ERROR;
|
|
27
|
-
|
|
28
|
-
const payload =
|
|
29
|
-
exception instanceof HttpException
|
|
30
|
-
? exception.getResponse()
|
|
31
|
-
: { message: 'Internal server error' };
|
|
32
|
-
|
|
33
|
-
const code = this.resolveCode(status);
|
|
34
|
-
const message = this.resolveMessage(payload, status, request);
|
|
35
|
-
const details = this.resolveDetails(payload);
|
|
36
|
-
|
|
37
|
-
response.status(status).json({
|
|
38
|
-
error: {
|
|
39
|
-
code,
|
|
40
|
-
message,
|
|
41
|
-
...(details !== undefined ? { details } : {}),
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
private resolveCode(status: number): string {
|
|
47
|
-
switch (status) {
|
|
48
|
-
case HttpStatus.BAD_REQUEST:
|
|
49
|
-
return 'validation_error';
|
|
50
|
-
case HttpStatus.UNAUTHORIZED:
|
|
51
|
-
return 'unauthorized';
|
|
52
|
-
case HttpStatus.FORBIDDEN:
|
|
53
|
-
return 'forbidden';
|
|
54
|
-
case HttpStatus.NOT_FOUND:
|
|
55
|
-
return 'not_found';
|
|
56
|
-
case HttpStatus.CONFLICT:
|
|
57
|
-
return 'conflict';
|
|
58
|
-
default:
|
|
59
|
-
return 'internal_error';
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
private resolveMessage(
|
|
64
|
-
payload: unknown,
|
|
65
|
-
status: number,
|
|
66
|
-
request: Request,
|
|
67
|
-
): string {
|
|
68
|
-
const lang = this.resolveLang(request);
|
|
69
|
-
|
|
70
|
-
if (status === HttpStatus.NOT_FOUND) {
|
|
71
|
-
return this.translate('errors.notFound', lang);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (typeof payload === 'string') {
|
|
75
|
-
return this.translate(payload, lang);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (typeof payload === 'object' && payload !== null) {
|
|
79
|
-
const obj = payload as { message?: string | string[] };
|
|
80
|
-
if (Array.isArray(obj.message) && obj.message.length > 0) {
|
|
81
|
-
return this.translate(obj.message[0], lang);
|
|
82
|
-
}
|
|
83
|
-
if (typeof obj.message === 'string') {
|
|
84
|
-
return this.translate(obj.message, lang);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return 'Internal server error';
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
private resolveDetails(payload: unknown): unknown {
|
|
92
|
-
if (typeof payload !== 'object' || payload === null) {
|
|
93
|
-
return undefined;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const obj = payload as { message?: string | string[]; error?: string };
|
|
97
|
-
if (Array.isArray(obj.message) && obj.message.length > 1) {
|
|
98
|
-
return obj.message;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return undefined;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
private resolveLang(request: Request): string | undefined {
|
|
105
|
-
const queryLang = request.query?.lang;
|
|
106
|
-
if (typeof queryLang === 'string' && queryLang.length > 0) {
|
|
107
|
-
return queryLang;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const header = request.headers['accept-language'];
|
|
111
|
-
if (typeof header === 'string' && header.length > 0) {
|
|
112
|
-
return header.split(',')[0].trim();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return undefined;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
private translate(keyOrMessage: string, lang?: string): string {
|
|
119
|
-
if (!this.i18n) {
|
|
120
|
-
return keyOrMessage;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const translated = this.i18n.t(keyOrMessage, {
|
|
124
|
-
lang,
|
|
125
|
-
defaultValue: keyOrMessage,
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
return typeof translated === 'string' ? translated : keyOrMessage;
|
|
129
|
-
}
|
|
130
|
-
}
|