@flusys/nestjs-shared 5.3.1 → 5.4.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.
@@ -33,6 +33,8 @@ const ERROR_MESSAGES = {
33
33
  UNAUTHORIZED: 'error.unauthorized',
34
34
  FORBIDDEN: 'error.forbidden',
35
35
  CONFLICT: 'error.conflict',
36
+ DB_FOREIGN_KEY: 'error.db.foreign.key',
37
+ DB_NOT_NULL: 'error.db.not.null',
36
38
  INTERNAL: 'error.internal',
37
39
  SERVICE_UNAVAILABLE: 'error.service.unavailable',
38
40
  UNKNOWN: 'error.unknown',
@@ -111,6 +111,12 @@ let ConflictException = class ConflictException extends BaseAppException {
111
111
  super({
112
112
  message: field ? `${entity} with this ${field} already exists` : `${entity} already exists`,
113
113
  messageKey: _messagekeys.ERROR_MESSAGES.CONFLICT,
114
+ messageVariables: {
115
+ entity,
116
+ ...field !== undefined && {
117
+ field
118
+ }
119
+ },
114
120
  status: _common.HttpStatus.CONFLICT,
115
121
  metadata: {
116
122
  entity,
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "GlobalExceptionFilter", {
9
9
  }
10
10
  });
11
11
  const _common = require("@nestjs/common");
12
+ const _typeorm = require("typeorm");
12
13
  const _messagekeys = require("../constants/message-keys");
13
14
  const _baseappexception = require("../exceptions/base-app.exception");
14
15
  const _loggermiddleware = require("../middlewares/logger.middleware");
@@ -36,19 +37,14 @@ let GlobalExceptionFilter = class GlobalExceptionFilter {
36
37
  const ctx = host.switchToHttp();
37
38
  const request = ctx.getRequest();
38
39
  const response = ctx.getResponse();
39
- const startTime = request._startTime || Date.now();
40
- const responseTime = Date.now() - startTime;
40
+ const responseTime = Date.now() - (request._startTime || Date.now());
41
41
  const requestId = (0, _loggermiddleware.getRequestId)() || request.headers['x-request-id'] || 'no-request-id';
42
- const userId = (0, _loggermiddleware.getUserId)();
43
- const tenantId = (0, _loggermiddleware.getTenantId)();
44
- const companyId = (0, _loggermiddleware.getCompanyId)();
45
- const errorResponse = this.buildErrorResponse(exception, requestId, responseTime);
46
- const statusCode = this.getStatusCode(exception);
42
+ const { statusCode, errorResponse } = this.resolveException(exception, requestId, responseTime);
47
43
  const logContext = {
48
44
  requestId,
49
- userId,
50
- tenantId,
51
- companyId,
45
+ userId: (0, _loggermiddleware.getUserId)(),
46
+ tenantId: (0, _loggermiddleware.getTenantId)(),
47
+ companyId: (0, _loggermiddleware.getCompanyId)(),
52
48
  method: request.method,
53
49
  url: request.originalUrl,
54
50
  path: request.path,
@@ -56,67 +52,84 @@ let GlobalExceptionFilter = class GlobalExceptionFilter {
56
52
  duration: `${responseTime}ms`,
57
53
  messageKey: errorResponse.messageKey
58
54
  };
59
- const logMessage = this.buildLogMessage(request, statusCode, responseTime, errorResponse);
55
+ const logMessage = `Exception caught "HTTP/1.1" "${request.method}" "${request.originalUrl}" - ${statusCode} "${errorResponse.messageKey}" ${responseTime}ms | ${errorResponse.message}`;
60
56
  if (statusCode >= 500) {
61
- this.logger.error(logMessage, this.getStack(exception), logContext);
57
+ this.logger.error(logMessage, exception instanceof Error ? exception.stack : undefined, logContext);
62
58
  } else if (statusCode >= 400) {
63
59
  this.logger.warn(logMessage, logContext);
64
60
  }
65
61
  response.status(statusCode).json(errorResponse);
66
62
  }
67
- buildErrorResponse(exception, requestId, responseTime) {
68
- const timestamp = new Date().toISOString();
63
+ resolveException(exception, requestId, responseTime) {
64
+ const meta = {
65
+ requestId,
66
+ timestamp: new Date().toISOString(),
67
+ responseTime
68
+ };
69
69
  if (exception instanceof _baseappexception.BaseAppException) {
70
70
  return {
71
- success: false,
72
- message: exception.message,
73
- messageKey: exception.messageKey,
74
- messageVariables: exception.messageVariables,
75
- errors: exception.errors,
76
- _meta: {
77
- requestId,
78
- timestamp,
79
- responseTime
71
+ statusCode: exception.getStatus(),
72
+ errorResponse: {
73
+ success: false,
74
+ message: exception.message,
75
+ messageKey: exception.messageKey,
76
+ messageVariables: exception.messageVariables,
77
+ errors: exception.errors,
78
+ _meta: meta
80
79
  }
81
80
  };
82
81
  }
83
82
  if (exception instanceof _common.HttpException) {
84
- const response = exception.getResponse();
85
- const { message, messageKey, messageVariables, errors } = this.extractHttpExceptionDetails(response);
83
+ const resolved = this.resolveHttpException(exception);
86
84
  return {
87
- success: false,
88
- message,
89
- messageKey,
90
- messageVariables,
91
- errors,
92
- _meta: {
93
- requestId,
94
- timestamp,
95
- responseTime
85
+ statusCode: resolved.statusCode,
86
+ errorResponse: {
87
+ success: false,
88
+ message: resolved.message,
89
+ messageKey: resolved.messageKey,
90
+ messageVariables: resolved.messageVariables,
91
+ errors: resolved.errors,
92
+ _meta: meta
96
93
  }
97
94
  };
98
95
  }
96
+ if (exception instanceof _typeorm.QueryFailedError) {
97
+ const resolved = this.resolveQueryFailedError(exception);
98
+ if (resolved) {
99
+ return {
100
+ statusCode: resolved.getStatus(),
101
+ errorResponse: {
102
+ success: false,
103
+ message: resolved.message,
104
+ messageKey: resolved.messageKey,
105
+ messageVariables: resolved.messageVariables,
106
+ _meta: meta
107
+ }
108
+ };
109
+ }
110
+ }
99
111
  return {
100
- success: false,
101
- message: 'Internal server error',
102
- messageKey: _messagekeys.ERROR_MESSAGES.INTERNAL,
103
- _meta: {
104
- requestId,
105
- timestamp,
106
- responseTime
112
+ statusCode: _common.HttpStatus.INTERNAL_SERVER_ERROR,
113
+ errorResponse: {
114
+ success: false,
115
+ message: 'Internal server error',
116
+ messageKey: _messagekeys.ERROR_MESSAGES.INTERNAL,
117
+ _meta: meta
107
118
  }
108
119
  };
109
120
  }
110
- extractHttpExceptionDetails(response) {
121
+ resolveHttpException(exception) {
122
+ const statusCode = exception.getStatus();
123
+ const response = exception.getResponse();
111
124
  if (typeof response === 'string') {
112
125
  return {
126
+ statusCode,
113
127
  message: response,
114
128
  messageKey: _messagekeys.ERROR_MESSAGES.HTTP
115
129
  };
116
130
  }
117
131
  if (typeof response === 'object' && response !== null) {
118
132
  const obj = response;
119
- // Handle class-validator validation errors
120
133
  if (Array.isArray(obj.message)) {
121
134
  const errors = obj.message.map((msg)=>{
122
135
  const [field, ...rest] = msg.split(' ');
@@ -126,12 +139,14 @@ let GlobalExceptionFilter = class GlobalExceptionFilter {
126
139
  };
127
140
  });
128
141
  return {
142
+ statusCode,
129
143
  message: 'Validation failed',
130
144
  messageKey: _messagekeys.ERROR_MESSAGES.VALIDATION,
131
145
  errors
132
146
  };
133
147
  }
134
148
  return {
149
+ statusCode,
135
150
  message: String(obj.message || 'Unknown error'),
136
151
  messageKey: String(obj.messageKey || _messagekeys.ERROR_MESSAGES.HTTP),
137
152
  messageVariables: obj.messageVariables,
@@ -139,24 +154,48 @@ let GlobalExceptionFilter = class GlobalExceptionFilter {
139
154
  };
140
155
  }
141
156
  return {
157
+ statusCode,
142
158
  message: 'Unknown error',
143
159
  messageKey: _messagekeys.ERROR_MESSAGES.UNKNOWN
144
160
  };
145
161
  }
146
- getStatusCode(exception) {
147
- if (exception instanceof _common.HttpException) {
148
- return exception.getStatus();
162
+ resolveQueryFailedError(exception) {
163
+ const dbError = exception;
164
+ const code = dbError.driverError?.code ?? dbError.code;
165
+ const number = dbError.driverError?.number ?? dbError.number;
166
+ const detail = dbError.driverError?.detail ?? dbError.detail ?? '';
167
+ const table = dbError.driverError?.table ?? dbError.table ?? 'record';
168
+ const column = dbError.driverError?.column ?? dbError.column;
169
+ const field = detail.match(/Key \(([^)]+)\)/)?.[1] ?? column;
170
+ if (code === '23505' || number === 2627) {
171
+ return new _baseappexception.ConflictException(this.toHumanReadable(table), field ? this.toHumanReadable(field) : undefined);
149
172
  }
150
- return _common.HttpStatus.INTERNAL_SERVER_ERROR;
151
- }
152
- getStack(exception) {
153
- if (exception instanceof Error) {
154
- return exception.stack;
173
+ if (code === '23503' || number === 547) {
174
+ const humanField = field ? this.toHumanReadable(field) : undefined;
175
+ return new _baseappexception.BaseAppException({
176
+ message: humanField ? `Invalid reference for '${humanField}'` : 'Invalid reference',
177
+ messageKey: _messagekeys.ERROR_MESSAGES.DB_FOREIGN_KEY,
178
+ messageVariables: humanField ? {
179
+ field: humanField
180
+ } : undefined,
181
+ status: _common.HttpStatus.BAD_REQUEST
182
+ });
183
+ }
184
+ if (code === '23502' || number === 515) {
185
+ const humanField = this.toHumanReadable(column ?? 'field');
186
+ return new _baseappexception.BaseAppException({
187
+ message: `Required field '${humanField}' cannot be null`,
188
+ messageKey: _messagekeys.ERROR_MESSAGES.DB_NOT_NULL,
189
+ messageVariables: {
190
+ field: humanField
191
+ },
192
+ status: _common.HttpStatus.BAD_REQUEST
193
+ });
155
194
  }
156
- return undefined;
195
+ return null;
157
196
  }
158
- buildLogMessage(request, statusCode, responseTime, errorResponse) {
159
- return `Exception caught "HTTP/1.1" "${request.method}" "${request.originalUrl}" - ${statusCode} "${errorResponse.messageKey}" ${responseTime}ms | ${errorResponse.message}`;
197
+ toHumanReadable(dbName) {
198
+ return dbName.split(', ').map((part)=>part.split('_').map((word)=>word.toLowerCase() === 'id' ? 'ID' : word.charAt(0).toUpperCase() + word.slice(1)).join(' ')).join(', ');
160
199
  }
161
200
  constructor(){
162
201
  _define_property(this, "logger", new _common.Logger('ExceptionFilter'));
@@ -10,6 +10,8 @@ export declare const ERROR_MESSAGES: {
10
10
  readonly UNAUTHORIZED: "error.unauthorized";
11
11
  readonly FORBIDDEN: "error.forbidden";
12
12
  readonly CONFLICT: "error.conflict";
13
+ readonly DB_FOREIGN_KEY: "error.db.foreign.key";
14
+ readonly DB_NOT_NULL: "error.db.not.null";
13
15
  readonly INTERNAL: "error.internal";
14
16
  readonly SERVICE_UNAVAILABLE: "error.service.unavailable";
15
17
  readonly UNKNOWN: "error.unknown";
@@ -13,6 +13,8 @@ export const ERROR_MESSAGES = {
13
13
  UNAUTHORIZED: 'error.unauthorized',
14
14
  FORBIDDEN: 'error.forbidden',
15
15
  CONFLICT: 'error.conflict',
16
+ DB_FOREIGN_KEY: 'error.db.foreign.key',
17
+ DB_NOT_NULL: 'error.db.not.null',
16
18
  INTERNAL: 'error.internal',
17
19
  SERVICE_UNAVAILABLE: 'error.service.unavailable',
18
20
  UNKNOWN: 'error.unknown',
@@ -75,6 +75,12 @@ export class ConflictException extends BaseAppException {
75
75
  super({
76
76
  message: field ? `${entity} with this ${field} already exists` : `${entity} already exists`,
77
77
  messageKey: ERROR_MESSAGES.CONFLICT,
78
+ messageVariables: {
79
+ entity,
80
+ ...field !== undefined && {
81
+ field
82
+ }
83
+ },
78
84
  status: HttpStatus.CONFLICT,
79
85
  metadata: {
80
86
  entity,
@@ -18,27 +18,23 @@ function _ts_decorate(decorators, target, key, desc) {
18
18
  return c > 3 && r && Object.defineProperty(target, key, r), r;
19
19
  }
20
20
  import { Catch, HttpException, HttpStatus, Logger } from '@nestjs/common';
21
+ import { QueryFailedError } from 'typeorm';
21
22
  import { ERROR_MESSAGES } from '../constants/message-keys';
22
- import { BaseAppException } from '../exceptions/base-app.exception';
23
- import { getRequestId, getUserId, getTenantId, getCompanyId } from '../middlewares/logger.middleware';
23
+ import { BaseAppException, ConflictException } from '../exceptions/base-app.exception';
24
+ import { getCompanyId, getRequestId, getTenantId, getUserId } from '../middlewares/logger.middleware';
24
25
  export class GlobalExceptionFilter {
25
26
  catch(exception, host) {
26
27
  const ctx = host.switchToHttp();
27
28
  const request = ctx.getRequest();
28
29
  const response = ctx.getResponse();
29
- const startTime = request._startTime || Date.now();
30
- const responseTime = Date.now() - startTime;
30
+ const responseTime = Date.now() - (request._startTime || Date.now());
31
31
  const requestId = getRequestId() || request.headers['x-request-id'] || 'no-request-id';
32
- const userId = getUserId();
33
- const tenantId = getTenantId();
34
- const companyId = getCompanyId();
35
- const errorResponse = this.buildErrorResponse(exception, requestId, responseTime);
36
- const statusCode = this.getStatusCode(exception);
32
+ const { statusCode, errorResponse } = this.resolveException(exception, requestId, responseTime);
37
33
  const logContext = {
38
34
  requestId,
39
- userId,
40
- tenantId,
41
- companyId,
35
+ userId: getUserId(),
36
+ tenantId: getTenantId(),
37
+ companyId: getCompanyId(),
42
38
  method: request.method,
43
39
  url: request.originalUrl,
44
40
  path: request.path,
@@ -46,67 +42,84 @@ export class GlobalExceptionFilter {
46
42
  duration: `${responseTime}ms`,
47
43
  messageKey: errorResponse.messageKey
48
44
  };
49
- const logMessage = this.buildLogMessage(request, statusCode, responseTime, errorResponse);
45
+ const logMessage = `Exception caught "HTTP/1.1" "${request.method}" "${request.originalUrl}" - ${statusCode} "${errorResponse.messageKey}" ${responseTime}ms | ${errorResponse.message}`;
50
46
  if (statusCode >= 500) {
51
- this.logger.error(logMessage, this.getStack(exception), logContext);
47
+ this.logger.error(logMessage, exception instanceof Error ? exception.stack : undefined, logContext);
52
48
  } else if (statusCode >= 400) {
53
49
  this.logger.warn(logMessage, logContext);
54
50
  }
55
51
  response.status(statusCode).json(errorResponse);
56
52
  }
57
- buildErrorResponse(exception, requestId, responseTime) {
58
- const timestamp = new Date().toISOString();
53
+ resolveException(exception, requestId, responseTime) {
54
+ const meta = {
55
+ requestId,
56
+ timestamp: new Date().toISOString(),
57
+ responseTime
58
+ };
59
59
  if (exception instanceof BaseAppException) {
60
60
  return {
61
- success: false,
62
- message: exception.message,
63
- messageKey: exception.messageKey,
64
- messageVariables: exception.messageVariables,
65
- errors: exception.errors,
66
- _meta: {
67
- requestId,
68
- timestamp,
69
- responseTime
61
+ statusCode: exception.getStatus(),
62
+ errorResponse: {
63
+ success: false,
64
+ message: exception.message,
65
+ messageKey: exception.messageKey,
66
+ messageVariables: exception.messageVariables,
67
+ errors: exception.errors,
68
+ _meta: meta
70
69
  }
71
70
  };
72
71
  }
73
72
  if (exception instanceof HttpException) {
74
- const response = exception.getResponse();
75
- const { message, messageKey, messageVariables, errors } = this.extractHttpExceptionDetails(response);
73
+ const resolved = this.resolveHttpException(exception);
76
74
  return {
77
- success: false,
78
- message,
79
- messageKey,
80
- messageVariables,
81
- errors,
82
- _meta: {
83
- requestId,
84
- timestamp,
85
- responseTime
75
+ statusCode: resolved.statusCode,
76
+ errorResponse: {
77
+ success: false,
78
+ message: resolved.message,
79
+ messageKey: resolved.messageKey,
80
+ messageVariables: resolved.messageVariables,
81
+ errors: resolved.errors,
82
+ _meta: meta
86
83
  }
87
84
  };
88
85
  }
86
+ if (exception instanceof QueryFailedError) {
87
+ const resolved = this.resolveQueryFailedError(exception);
88
+ if (resolved) {
89
+ return {
90
+ statusCode: resolved.getStatus(),
91
+ errorResponse: {
92
+ success: false,
93
+ message: resolved.message,
94
+ messageKey: resolved.messageKey,
95
+ messageVariables: resolved.messageVariables,
96
+ _meta: meta
97
+ }
98
+ };
99
+ }
100
+ }
89
101
  return {
90
- success: false,
91
- message: 'Internal server error',
92
- messageKey: ERROR_MESSAGES.INTERNAL,
93
- _meta: {
94
- requestId,
95
- timestamp,
96
- responseTime
102
+ statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
103
+ errorResponse: {
104
+ success: false,
105
+ message: 'Internal server error',
106
+ messageKey: ERROR_MESSAGES.INTERNAL,
107
+ _meta: meta
97
108
  }
98
109
  };
99
110
  }
100
- extractHttpExceptionDetails(response) {
111
+ resolveHttpException(exception) {
112
+ const statusCode = exception.getStatus();
113
+ const response = exception.getResponse();
101
114
  if (typeof response === 'string') {
102
115
  return {
116
+ statusCode,
103
117
  message: response,
104
118
  messageKey: ERROR_MESSAGES.HTTP
105
119
  };
106
120
  }
107
121
  if (typeof response === 'object' && response !== null) {
108
122
  const obj = response;
109
- // Handle class-validator validation errors
110
123
  if (Array.isArray(obj.message)) {
111
124
  const errors = obj.message.map((msg)=>{
112
125
  const [field, ...rest] = msg.split(' ');
@@ -116,12 +129,14 @@ export class GlobalExceptionFilter {
116
129
  };
117
130
  });
118
131
  return {
132
+ statusCode,
119
133
  message: 'Validation failed',
120
134
  messageKey: ERROR_MESSAGES.VALIDATION,
121
135
  errors
122
136
  };
123
137
  }
124
138
  return {
139
+ statusCode,
125
140
  message: String(obj.message || 'Unknown error'),
126
141
  messageKey: String(obj.messageKey || ERROR_MESSAGES.HTTP),
127
142
  messageVariables: obj.messageVariables,
@@ -129,24 +144,48 @@ export class GlobalExceptionFilter {
129
144
  };
130
145
  }
131
146
  return {
147
+ statusCode,
132
148
  message: 'Unknown error',
133
149
  messageKey: ERROR_MESSAGES.UNKNOWN
134
150
  };
135
151
  }
136
- getStatusCode(exception) {
137
- if (exception instanceof HttpException) {
138
- return exception.getStatus();
152
+ resolveQueryFailedError(exception) {
153
+ const dbError = exception;
154
+ const code = dbError.driverError?.code ?? dbError.code;
155
+ const number = dbError.driverError?.number ?? dbError.number;
156
+ const detail = dbError.driverError?.detail ?? dbError.detail ?? '';
157
+ const table = dbError.driverError?.table ?? dbError.table ?? 'record';
158
+ const column = dbError.driverError?.column ?? dbError.column;
159
+ const field = detail.match(/Key \(([^)]+)\)/)?.[1] ?? column;
160
+ if (code === '23505' || number === 2627) {
161
+ return new ConflictException(this.toHumanReadable(table), field ? this.toHumanReadable(field) : undefined);
139
162
  }
140
- return HttpStatus.INTERNAL_SERVER_ERROR;
141
- }
142
- getStack(exception) {
143
- if (exception instanceof Error) {
144
- return exception.stack;
163
+ if (code === '23503' || number === 547) {
164
+ const humanField = field ? this.toHumanReadable(field) : undefined;
165
+ return new BaseAppException({
166
+ message: humanField ? `Invalid reference for '${humanField}'` : 'Invalid reference',
167
+ messageKey: ERROR_MESSAGES.DB_FOREIGN_KEY,
168
+ messageVariables: humanField ? {
169
+ field: humanField
170
+ } : undefined,
171
+ status: HttpStatus.BAD_REQUEST
172
+ });
173
+ }
174
+ if (code === '23502' || number === 515) {
175
+ const humanField = this.toHumanReadable(column ?? 'field');
176
+ return new BaseAppException({
177
+ message: `Required field '${humanField}' cannot be null`,
178
+ messageKey: ERROR_MESSAGES.DB_NOT_NULL,
179
+ messageVariables: {
180
+ field: humanField
181
+ },
182
+ status: HttpStatus.BAD_REQUEST
183
+ });
145
184
  }
146
- return undefined;
185
+ return null;
147
186
  }
148
- buildLogMessage(request, statusCode, responseTime, errorResponse) {
149
- return `Exception caught "HTTP/1.1" "${request.method}" "${request.originalUrl}" - ${statusCode} "${errorResponse.messageKey}" ${responseTime}ms | ${errorResponse.message}`;
187
+ toHumanReadable(dbName) {
188
+ return dbName.split(', ').map((part)=>part.split('_').map((word)=>word.toLowerCase() === 'id' ? 'ID' : word.charAt(0).toUpperCase() + word.slice(1)).join(' ')).join(', ');
150
189
  }
151
190
  constructor(){
152
191
  _define_property(this, "logger", new Logger('ExceptionFilter'));
@@ -2,9 +2,8 @@ import { ArgumentsHost, ExceptionFilter } from '@nestjs/common';
2
2
  export declare class GlobalExceptionFilter implements ExceptionFilter {
3
3
  private readonly logger;
4
4
  catch(exception: unknown, host: ArgumentsHost): void;
5
- private buildErrorResponse;
6
- private extractHttpExceptionDetails;
7
- private getStatusCode;
8
- private getStack;
9
- private buildLogMessage;
5
+ private resolveException;
6
+ private resolveHttpException;
7
+ private resolveQueryFailedError;
8
+ private toHumanReadable;
10
9
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flusys/nestjs-shared",
3
- "version": "5.3.1",
3
+ "version": "5.4.0",
4
4
  "description": "Common shared utilities for Flusys NestJS applications",
5
5
  "main": "cjs/index.js",
6
6
  "module": "fesm/index.js",
@@ -112,6 +112,6 @@
112
112
  "rxjs": "^7.8.0"
113
113
  },
114
114
  "dependencies": {
115
- "@flusys/nestjs-core": "5.3.1"
115
+ "@flusys/nestjs-core": "5.4.0"
116
116
  }
117
117
  }