Skip to content

Commit 6205d0a

Browse files
authored
Merge pull request #469 from lenneTech/DEV-43
11.9.0: Add ErrorCodeModule for centralized error handling with i18n translations and REST API
2 parents 629d497 + 5160d74 commit 6205d0a

34 files changed

+4153
-1227
lines changed

docs/error-codes.md

Lines changed: 430 additions & 0 deletions
Large diffs are not rendered by default.

package-lock.json

Lines changed: 1133 additions & 1176 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@lenne.tech/nest-server",
3-
"version": "11.8.0",
3+
"version": "11.9.0",
44
"description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
55
"keywords": [
66
"node",
@@ -21,7 +21,7 @@
2121
"build:pack": "npm pack && echo 'use file:/ROOT_PATH_TO_TGZ_FILE to integrate the package'",
2222
"build:dev": "npm run build && yalc push --private",
2323
"docs": "npm run docs:ci && open http://127.0.0.1:8080/ && open ./public/index.html && compodoc -p tsconfig.json -s ",
24-
"docs:bootstrap": "node extras/update-spectaql-version.mjs && npx -y spectaql ./spectaql.yml",
24+
"docs:bootstrap": "node extras/update-spectaql-version.mjs && node scripts/run-spectaql.mjs",
2525
"docs:ci": "ts-node ./scripts/init-server.ts && npm run docs:bootstrap && compodoc -p tsconfig.json",
2626
"format": "prettier --write 'src/**/*.ts'",
2727
"format:staged": "pretty-quick --staged",

scripts/run-spectaql.mjs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Wrapper script for spectaql that filters Sass deprecation warnings
4+
*
5+
* Sass deprecation warnings come from spectaql's internal dependencies
6+
* and cannot be silenced through configuration. This script filters them
7+
* from the output while preserving the exit code.
8+
*/
9+
10+
import { spawn } from 'child_process';
11+
12+
const spectaql = spawn('npx', ['-y', 'spectaql', './spectaql.yml'], {
13+
stdio: ['inherit', 'pipe', 'pipe'],
14+
});
15+
16+
// Patterns to filter from output (Sass deprecation warnings)
17+
const filterPatterns = [
18+
'DEPRECATION WARNING',
19+
'More info and automated migrator:',
20+
'sass-lang.com',
21+
'───', // Box drawing characters
22+
'│', // Vertical line in Sass output
23+
'╵', // Bottom corner
24+
'╷', // Top corner
25+
'@import', // Import statements in warnings
26+
'root stylesheet',
27+
];
28+
29+
function shouldFilter(line) {
30+
return filterPatterns.some((pattern) => line.includes(pattern));
31+
}
32+
33+
function processOutput(data, stream) {
34+
const lines = data.toString().split('\n');
35+
const filtered = lines.filter((line) => !shouldFilter(line));
36+
const output = filtered.join('\n');
37+
if (output.trim()) {
38+
stream.write(output + (output.endsWith('\n') ? '' : '\n'));
39+
}
40+
}
41+
42+
spectaql.stdout.on('data', (data) => {
43+
processOutput(data, process.stdout);
44+
});
45+
46+
spectaql.stderr.on('data', (data) => {
47+
processOutput(data, process.stderr);
48+
});
49+
50+
spectaql.on('close', (code) => {
51+
process.exit(code);
52+
});

spectaql.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ servers:
1111
info:
1212
title: lT Nest Server
1313
description: Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).
14-
version: 11.8.0
14+
version: 11.9.0
1515
contact:
1616
name: lenne.Tech GmbH
1717
url: https://lenne.tech

src/config.env.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,10 @@ const config: { [env: string]: IServerOptions } = {
262262
verificationLink: 'http://localhost:4200/user/verification',
263263
},
264264
env: 'local',
265+
// Disable auto-registration to allow Server ErrorCodeModule with SRV_* codes
266+
errorCode: {
267+
autoRegister: false,
268+
},
265269
execAfterInit: 'npm run docs:bootstrap',
266270
filter: {
267271
maxLimit: null,

src/core.module.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { TemplateService } from './core/common/services/template.service';
2222
import { BetterAuthUserMapper } from './core/modules/better-auth/better-auth-user.mapper';
2323
import { BetterAuthModule } from './core/modules/better-auth/better-auth.module';
2424
import { BetterAuthService } from './core/modules/better-auth/better-auth.service';
25+
import { ErrorCodeModule } from './core/modules/error-code/error-code.module';
2526
import { CoreHealthCheckModule } from './core/modules/health-check/core-health-check.module';
2627

2728
/**
@@ -226,6 +227,21 @@ export class CoreModule implements NestModule {
226227
Object.assign({ driver: ApolloDriver }, config.graphQl.driver, config.graphQl.options),
227228
),
228229
];
230+
231+
// Add ErrorCodeModule based on configuration
232+
// autoRegister defaults to true (backward compatible)
233+
const errorCodeConfig = config.errorCode;
234+
const isErrorCodeAutoRegister = errorCodeConfig?.autoRegister !== false;
235+
236+
if (isErrorCodeAutoRegister) {
237+
// Always use forRoot() - it registers the controller and handles configuration
238+
imports.push(
239+
ErrorCodeModule.forRoot({
240+
additionalErrorRegistry: errorCodeConfig?.additionalErrorRegistry,
241+
}),
242+
);
243+
}
244+
229245
if (config.healthCheck) {
230246
imports.push(CoreHealthCheckModule);
231247
}

src/core/common/interfaces/server-options.interface.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,70 @@ export interface IBetterAuthUserField {
589589
type: BetterAuthFieldType;
590590
}
591591

592+
/**
593+
* Interface for Error Code module configuration
594+
*
595+
* Controls how the ErrorCodeModule is registered and configured.
596+
*
597+
* @since 11.9.0
598+
*/
599+
export interface IErrorCode {
600+
/**
601+
* Additional error registry to merge with core LTNS_* errors
602+
*
603+
* Use this to add project-specific error codes with a custom prefix.
604+
*
605+
* @example
606+
* ```typescript
607+
* const ProjectErrors = {
608+
* ORDER_NOT_FOUND: {
609+
* code: 'PROJ_0001',
610+
* message: 'Order not found',
611+
* translations: { de: 'Bestellung nicht gefunden.', en: 'Order not found.' }
612+
* }
613+
* } as const satisfies IErrorRegistry;
614+
*
615+
* errorCode: {
616+
* additionalErrorRegistry: ProjectErrors,
617+
* }
618+
* ```
619+
*/
620+
additionalErrorRegistry?: Record<
621+
string,
622+
{
623+
code: string;
624+
message: string;
625+
translations: { [locale: string]: string; de: string; en: string };
626+
}
627+
>;
628+
629+
/**
630+
* Automatically register the ErrorCodeModule in CoreModule
631+
*
632+
* Set to `false` to disable auto-registration and provide your own
633+
* ErrorCodeModule with custom controller and/or service.
634+
*
635+
* @default true
636+
*
637+
* @example
638+
* ```typescript
639+
* // In config.env.ts - disable auto-registration
640+
* errorCode: {
641+
* autoRegister: false,
642+
* }
643+
*
644+
* // In server.module.ts - import your custom module
645+
* @Module({
646+
* imports: [
647+
* CoreModule.forRoot(...),
648+
* ErrorCodeModule.forRoot(), // Your custom module
649+
* ],
650+
* })
651+
* ```
652+
*/
653+
autoRegister?: boolean;
654+
}
655+
592656
/**
593657
* Interface for JWT configuration (main and refresh)
594658
*/
@@ -766,6 +830,31 @@ export interface IServerOptions {
766830
*/
767831
env?: string;
768832

833+
/**
834+
* Configuration for the error code module
835+
*
836+
* Controls how error codes and translations are handled.
837+
*
838+
* @since 11.9.0
839+
*
840+
* @example
841+
* ```typescript
842+
* // Default: auto-register with core errors only
843+
* errorCode: undefined
844+
*
845+
* // Add project-specific error codes
846+
* errorCode: {
847+
* additionalErrorRegistry: ProjectErrors,
848+
* }
849+
*
850+
* // Disable auto-registration to provide your own module
851+
* errorCode: {
852+
* autoRegister: false,
853+
* }
854+
* ```
855+
*/
856+
errorCode?: IErrorCode;
857+
769858
/**
770859
* Exec a command after server is initialized
771860
* e.g. 'npm run docs:bootstrap'

src/core/modules/auth/guards/roles.guard.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ExecutionContext, Injectable, Logger, Optional, UnauthorizedException } from '@nestjs/common';
1+
import { ExecutionContext, ForbiddenException, Injectable, Logger, Optional, UnauthorizedException } from '@nestjs/common';
22
import { ModuleRef, Reflector } from '@nestjs/core';
33
import { GqlExecutionContext } from '@nestjs/graphql';
44
import { getConnectionToken } from '@nestjs/mongoose';
@@ -7,6 +7,7 @@ import { firstValueFrom, isObservable } from 'rxjs';
77

88
import { RoleEnum } from '../../../common/enums/role.enum';
99
import { BetterAuthService } from '../../better-auth/better-auth.service';
10+
import { ErrorCode } from '../../error-code';
1011
import { AuthGuardStrategy } from '../auth-guard-strategy.enum';
1112
import { ExpiredTokenException } from '../exceptions/expired-token.exception';
1213
import { InvalidTokenException } from '../exceptions/invalid-token.exception';
@@ -331,7 +332,7 @@ export class RolesGuard extends AuthGuard(AuthGuardStrategy.JWT) {
331332

332333
// Check if locked
333334
if (roles && roles.includes(RoleEnum.S_NO_ONE)) {
334-
throw new UnauthorizedException('No access');
335+
throw new UnauthorizedException(ErrorCode.UNAUTHORIZED);
335336
}
336337

337338
// Check roles
@@ -354,11 +355,11 @@ export class RolesGuard extends AuthGuard(AuthGuardStrategy.JWT) {
354355
if (info?.name === 'TokenExpiredError') {
355356
throw new ExpiredTokenException();
356357
}
357-
throw new UnauthorizedException('Unauthorized');
358+
throw new UnauthorizedException(ErrorCode.UNAUTHORIZED);
358359
}
359360

360361
// Requester is not authorized
361-
throw new UnauthorizedException('Missing role');
362+
throw new ForbiddenException(ErrorCode.ACCESS_DENIED);
362363
}
363364

364365
// Everything is ok

src/core/modules/auth/services/core-auth.service.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { ServiceOptions } from '../../../common/interfaces/service-options.inter
1717
import { ConfigService } from '../../../common/services/config.service';
1818
import { BetterAuthUserMapper } from '../../better-auth/better-auth-user.mapper';
1919
import { BetterAuthService } from '../../better-auth/better-auth.service';
20+
import { ErrorCode } from '../../error-code';
2021
import { CoreAuthModel } from '../core-auth.model';
2122
import { CoreAuthSignInInput } from '../inputs/core-auth-sign-in.input';
2223
import { CoreAuthSignUpInput } from '../inputs/core-auth-sign-up.input';
@@ -83,13 +84,13 @@ export class CoreAuthService {
8384
// Check authentication
8485
const user = serviceOptions.currentUser;
8586
if (!user || !tokenOrRefreshToken) {
86-
throw new UnauthorizedException('Invalid token');
87+
throw new UnauthorizedException(ErrorCode.INVALID_TOKEN);
8788
}
8889

8990
// Check authorization
9091
const deviceId = this.decodeJwt(tokenOrRefreshToken)?.deviceId;
9192
if (!deviceId || !user.refreshTokens[deviceId]) {
92-
throw new UnauthorizedException('Invalid token');
93+
throw new UnauthorizedException(ErrorCode.INVALID_TOKEN);
9394
}
9495

9596
// Logout from all devices
@@ -374,7 +375,7 @@ export class CoreAuthService {
374375
if (currentRefreshToken) {
375376
deviceId = this.decodeJwt(currentRefreshToken)?.deviceId;
376377
if (!deviceId || !user.refreshTokens?.[deviceId]) {
377-
throw new UnauthorizedException('Invalid token');
378+
throw new UnauthorizedException(ErrorCode.INVALID_TOKEN);
378379
}
379380
if (!this.configService.getFastButReadOnly('jwt.refresh.renewal')) {
380381
// Return currentToken
@@ -398,7 +399,7 @@ export class CoreAuthService {
398399
// Set new token
399400
const payload = this.decodeJwt(newRefreshToken);
400401
if (!payload) {
401-
throw new UnauthorizedException('Invalid token');
402+
throw new UnauthorizedException(ErrorCode.INVALID_TOKEN);
402403
}
403404
if (!deviceId) {
404405
deviceId = payload.deviceId;

0 commit comments

Comments
 (0)