@flusys/nestjs-shared 5.4.1 → 5.5.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.
@@ -35,6 +35,10 @@ const ERROR_MESSAGES = {
35
35
  CONFLICT: 'error.conflict',
36
36
  DB_FOREIGN_KEY: 'error.db.foreign.key',
37
37
  DB_NOT_NULL: 'error.db.not.null',
38
+ DB_INVALID_INPUT: 'error.db.invalid.input',
39
+ DB_VALUE_TOO_LONG: 'error.db.value.too.long',
40
+ DB_CHECK_CONSTRAINT: 'error.db.check.constraint',
41
+ DB_DEADLOCK: 'error.db.deadlock',
38
42
  INTERNAL: 'error.internal',
39
43
  SERVICE_UNAVAILABLE: 'error.service.unavailable',
40
44
  UNKNOWN: 'error.unknown',
@@ -163,15 +163,27 @@ let GlobalExceptionFilter = class GlobalExceptionFilter {
163
163
  const dbError = exception;
164
164
  const code = dbError.driverError?.code ?? dbError.code;
165
165
  const number = dbError.driverError?.number ?? dbError.number;
166
+ const errno = dbError.driverError?.errno ?? dbError.errno;
167
+ const sqlMessage = dbError.driverError?.sqlMessage ?? '';
166
168
  const detail = dbError.driverError?.detail ?? dbError.detail ?? '';
167
- const table = dbError.driverError?.table ?? dbError.table ?? 'record';
169
+ const table = dbError.driverError?.table ?? dbError.table ?? '';
168
170
  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);
171
+ const pgField = detail.match(/Key \(([^)]+)\)/)?.[1] ?? column;
172
+ // DUPLICATE / UNIQUE VIOLATION
173
+ // PG: 23505 | MSSQL: 2627 (constraint), 2601 (index) | MySQL: ER_DUP_ENTRY / errno 1062
174
+ if (code === '23505' || number === 2627 || number === 2601 || code === 'ER_DUP_ENTRY' || errno === 1062) {
175
+ const mysqlTable = sqlMessage.match(/for key '([^'.]+)\./)?.[1];
176
+ const mysqlIndex = sqlMessage.match(/for key '(?:[^.]+\.)?([^']+)'/)?.[1];
177
+ const resolvedTable = table || mysqlTable || 'record';
178
+ const resolvedField = pgField ? this.toHumanReadable(pgField) : mysqlIndex ? this.toHumanReadable(mysqlIndex) : undefined;
179
+ return new _baseappexception.ConflictException(this.toHumanReadable(resolvedTable), resolvedField);
172
180
  }
173
- if (code === '23503' || number === 547) {
174
- const humanField = field ? this.toHumanReadable(field) : undefined;
181
+ // FOREIGN KEY VIOLATION
182
+ // PG: 23503 | MSSQL: 547 | MySQL: ER_NO_REFERENCED_ROW_2/1452 (insert), ER_ROW_IS_REFERENCED_2/1451 (delete)
183
+ if (code === '23503' || number === 547 || code === 'ER_NO_REFERENCED_ROW_2' || code === 'ER_ROW_IS_REFERENCED_2' || errno === 1452 || errno === 1451) {
184
+ const mysqlColumn = sqlMessage.match(/FOREIGN KEY \(`([^`]+)`\)/)?.[1];
185
+ const raw = pgField ?? mysqlColumn;
186
+ const humanField = raw ? this.toHumanReadable(raw) : undefined;
175
187
  return new _baseappexception.BaseAppException({
176
188
  message: humanField ? `Invalid reference for '${humanField}'` : 'Invalid reference',
177
189
  messageKey: _messagekeys.ERROR_MESSAGES.DB_FOREIGN_KEY,
@@ -181,8 +193,11 @@ let GlobalExceptionFilter = class GlobalExceptionFilter {
181
193
  status: _common.HttpStatus.BAD_REQUEST
182
194
  });
183
195
  }
184
- if (code === '23502' || number === 515) {
185
- const humanField = this.toHumanReadable(column ?? 'field');
196
+ // NOT NULL VIOLATION
197
+ // PG: 23502 | MSSQL: 515 | MySQL: ER_BAD_NULL_ERROR / errno 1048
198
+ if (code === '23502' || number === 515 || code === 'ER_BAD_NULL_ERROR' || errno === 1048) {
199
+ const mysqlColumn = sqlMessage.match(/Column '([^']+)' cannot be null/)?.[1];
200
+ const humanField = this.toHumanReadable(column ?? mysqlColumn ?? 'field');
186
201
  return new _baseappexception.BaseAppException({
187
202
  message: `Required field '${humanField}' cannot be null`,
188
203
  messageKey: _messagekeys.ERROR_MESSAGES.DB_NOT_NULL,
@@ -192,6 +207,64 @@ let GlobalExceptionFilter = class GlobalExceptionFilter {
192
207
  status: _common.HttpStatus.BAD_REQUEST
193
208
  });
194
209
  }
210
+ // INVALID INPUT SYNTAX — PostgreSQL only (MySQL silently coerces types)
211
+ if (code === '22P02') {
212
+ const typeMatch = exception.message.match(/invalid input syntax for(?:\s+type)?\s+(\w+)/i);
213
+ const type = typeMatch?.[1] ?? 'value';
214
+ const humanField = column ? this.toHumanReadable(column) : undefined;
215
+ return new _baseappexception.BaseAppException({
216
+ message: humanField ? `Invalid ${type} value for '${humanField}'` : `Invalid ${type} value`,
217
+ messageKey: _messagekeys.ERROR_MESSAGES.DB_INVALID_INPUT,
218
+ messageVariables: {
219
+ type,
220
+ ...humanField !== undefined && {
221
+ field: humanField
222
+ }
223
+ },
224
+ status: _common.HttpStatus.BAD_REQUEST
225
+ });
226
+ }
227
+ // VALUE TOO LONG
228
+ // PG: 22001 | MySQL: ER_DATA_TOO_LONG / errno 1406
229
+ if (code === '22001' || code === 'ER_DATA_TOO_LONG' || errno === 1406) {
230
+ const mysqlColumn = sqlMessage.match(/Data too long for column '([^']+)'/)?.[1];
231
+ const resolvedColumn = column ?? mysqlColumn;
232
+ const humanField = resolvedColumn ? this.toHumanReadable(resolvedColumn) : undefined;
233
+ return new _baseappexception.BaseAppException({
234
+ message: humanField ? `Value too long for '${humanField}'` : 'Value exceeds maximum length',
235
+ messageKey: _messagekeys.ERROR_MESSAGES.DB_VALUE_TOO_LONG,
236
+ messageVariables: humanField !== undefined ? {
237
+ field: humanField
238
+ } : undefined,
239
+ status: _common.HttpStatus.BAD_REQUEST
240
+ });
241
+ }
242
+ // CHECK CONSTRAINT VIOLATION
243
+ // PG: 23514 | MySQL 8.0+: ER_CHECK_CONSTRAINT_VIOLATED / errno 3819
244
+ if (code === '23514' || code === 'ER_CHECK_CONSTRAINT_VIOLATED' || errno === 3819) {
245
+ const constraint = dbError.driverError?.constraint ?? dbError.constraint;
246
+ const humanTable = this.toHumanReadable(table || 'record');
247
+ return new _baseappexception.BaseAppException({
248
+ message: `${humanTable} violates a validation rule`,
249
+ messageKey: _messagekeys.ERROR_MESSAGES.DB_CHECK_CONSTRAINT,
250
+ messageVariables: {
251
+ entity: humanTable,
252
+ ...constraint !== undefined && {
253
+ constraint
254
+ }
255
+ },
256
+ status: _common.HttpStatus.BAD_REQUEST
257
+ });
258
+ }
259
+ // DEADLOCK
260
+ // PG: 40P01 | MSSQL: 1205 | MySQL: ER_LOCK_DEADLOCK / errno 1213
261
+ if (code === '40P01' || number === 1205 || code === 'ER_LOCK_DEADLOCK' || errno === 1213) {
262
+ return new _baseappexception.BaseAppException({
263
+ message: 'The request could not be completed due to a conflict, please try again',
264
+ messageKey: _messagekeys.ERROR_MESSAGES.DB_DEADLOCK,
265
+ status: _common.HttpStatus.SERVICE_UNAVAILABLE
266
+ });
267
+ }
195
268
  return null;
196
269
  }
197
270
  toHumanReadable(dbName) {
@@ -12,6 +12,10 @@ export declare const ERROR_MESSAGES: {
12
12
  readonly CONFLICT: "error.conflict";
13
13
  readonly DB_FOREIGN_KEY: "error.db.foreign.key";
14
14
  readonly DB_NOT_NULL: "error.db.not.null";
15
+ readonly DB_INVALID_INPUT: "error.db.invalid.input";
16
+ readonly DB_VALUE_TOO_LONG: "error.db.value.too.long";
17
+ readonly DB_CHECK_CONSTRAINT: "error.db.check.constraint";
18
+ readonly DB_DEADLOCK: "error.db.deadlock";
15
19
  readonly INTERNAL: "error.internal";
16
20
  readonly SERVICE_UNAVAILABLE: "error.service.unavailable";
17
21
  readonly UNKNOWN: "error.unknown";
@@ -15,6 +15,10 @@ export const ERROR_MESSAGES = {
15
15
  CONFLICT: 'error.conflict',
16
16
  DB_FOREIGN_KEY: 'error.db.foreign.key',
17
17
  DB_NOT_NULL: 'error.db.not.null',
18
+ DB_INVALID_INPUT: 'error.db.invalid.input',
19
+ DB_VALUE_TOO_LONG: 'error.db.value.too.long',
20
+ DB_CHECK_CONSTRAINT: 'error.db.check.constraint',
21
+ DB_DEADLOCK: 'error.db.deadlock',
18
22
  INTERNAL: 'error.internal',
19
23
  SERVICE_UNAVAILABLE: 'error.service.unavailable',
20
24
  UNKNOWN: 'error.unknown',
@@ -153,15 +153,27 @@ export class GlobalExceptionFilter {
153
153
  const dbError = exception;
154
154
  const code = dbError.driverError?.code ?? dbError.code;
155
155
  const number = dbError.driverError?.number ?? dbError.number;
156
+ const errno = dbError.driverError?.errno ?? dbError.errno;
157
+ const sqlMessage = dbError.driverError?.sqlMessage ?? '';
156
158
  const detail = dbError.driverError?.detail ?? dbError.detail ?? '';
157
- const table = dbError.driverError?.table ?? dbError.table ?? 'record';
159
+ const table = dbError.driverError?.table ?? dbError.table ?? '';
158
160
  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);
161
+ const pgField = detail.match(/Key \(([^)]+)\)/)?.[1] ?? column;
162
+ // DUPLICATE / UNIQUE VIOLATION
163
+ // PG: 23505 | MSSQL: 2627 (constraint), 2601 (index) | MySQL: ER_DUP_ENTRY / errno 1062
164
+ if (code === '23505' || number === 2627 || number === 2601 || code === 'ER_DUP_ENTRY' || errno === 1062) {
165
+ const mysqlTable = sqlMessage.match(/for key '([^'.]+)\./)?.[1];
166
+ const mysqlIndex = sqlMessage.match(/for key '(?:[^.]+\.)?([^']+)'/)?.[1];
167
+ const resolvedTable = table || mysqlTable || 'record';
168
+ const resolvedField = pgField ? this.toHumanReadable(pgField) : mysqlIndex ? this.toHumanReadable(mysqlIndex) : undefined;
169
+ return new ConflictException(this.toHumanReadable(resolvedTable), resolvedField);
162
170
  }
163
- if (code === '23503' || number === 547) {
164
- const humanField = field ? this.toHumanReadable(field) : undefined;
171
+ // FOREIGN KEY VIOLATION
172
+ // PG: 23503 | MSSQL: 547 | MySQL: ER_NO_REFERENCED_ROW_2/1452 (insert), ER_ROW_IS_REFERENCED_2/1451 (delete)
173
+ if (code === '23503' || number === 547 || code === 'ER_NO_REFERENCED_ROW_2' || code === 'ER_ROW_IS_REFERENCED_2' || errno === 1452 || errno === 1451) {
174
+ const mysqlColumn = sqlMessage.match(/FOREIGN KEY \(`([^`]+)`\)/)?.[1];
175
+ const raw = pgField ?? mysqlColumn;
176
+ const humanField = raw ? this.toHumanReadable(raw) : undefined;
165
177
  return new BaseAppException({
166
178
  message: humanField ? `Invalid reference for '${humanField}'` : 'Invalid reference',
167
179
  messageKey: ERROR_MESSAGES.DB_FOREIGN_KEY,
@@ -171,8 +183,11 @@ export class GlobalExceptionFilter {
171
183
  status: HttpStatus.BAD_REQUEST
172
184
  });
173
185
  }
174
- if (code === '23502' || number === 515) {
175
- const humanField = this.toHumanReadable(column ?? 'field');
186
+ // NOT NULL VIOLATION
187
+ // PG: 23502 | MSSQL: 515 | MySQL: ER_BAD_NULL_ERROR / errno 1048
188
+ if (code === '23502' || number === 515 || code === 'ER_BAD_NULL_ERROR' || errno === 1048) {
189
+ const mysqlColumn = sqlMessage.match(/Column '([^']+)' cannot be null/)?.[1];
190
+ const humanField = this.toHumanReadable(column ?? mysqlColumn ?? 'field');
176
191
  return new BaseAppException({
177
192
  message: `Required field '${humanField}' cannot be null`,
178
193
  messageKey: ERROR_MESSAGES.DB_NOT_NULL,
@@ -182,6 +197,64 @@ export class GlobalExceptionFilter {
182
197
  status: HttpStatus.BAD_REQUEST
183
198
  });
184
199
  }
200
+ // INVALID INPUT SYNTAX — PostgreSQL only (MySQL silently coerces types)
201
+ if (code === '22P02') {
202
+ const typeMatch = exception.message.match(/invalid input syntax for(?:\s+type)?\s+(\w+)/i);
203
+ const type = typeMatch?.[1] ?? 'value';
204
+ const humanField = column ? this.toHumanReadable(column) : undefined;
205
+ return new BaseAppException({
206
+ message: humanField ? `Invalid ${type} value for '${humanField}'` : `Invalid ${type} value`,
207
+ messageKey: ERROR_MESSAGES.DB_INVALID_INPUT,
208
+ messageVariables: {
209
+ type,
210
+ ...humanField !== undefined && {
211
+ field: humanField
212
+ }
213
+ },
214
+ status: HttpStatus.BAD_REQUEST
215
+ });
216
+ }
217
+ // VALUE TOO LONG
218
+ // PG: 22001 | MySQL: ER_DATA_TOO_LONG / errno 1406
219
+ if (code === '22001' || code === 'ER_DATA_TOO_LONG' || errno === 1406) {
220
+ const mysqlColumn = sqlMessage.match(/Data too long for column '([^']+)'/)?.[1];
221
+ const resolvedColumn = column ?? mysqlColumn;
222
+ const humanField = resolvedColumn ? this.toHumanReadable(resolvedColumn) : undefined;
223
+ return new BaseAppException({
224
+ message: humanField ? `Value too long for '${humanField}'` : 'Value exceeds maximum length',
225
+ messageKey: ERROR_MESSAGES.DB_VALUE_TOO_LONG,
226
+ messageVariables: humanField !== undefined ? {
227
+ field: humanField
228
+ } : undefined,
229
+ status: HttpStatus.BAD_REQUEST
230
+ });
231
+ }
232
+ // CHECK CONSTRAINT VIOLATION
233
+ // PG: 23514 | MySQL 8.0+: ER_CHECK_CONSTRAINT_VIOLATED / errno 3819
234
+ if (code === '23514' || code === 'ER_CHECK_CONSTRAINT_VIOLATED' || errno === 3819) {
235
+ const constraint = dbError.driverError?.constraint ?? dbError.constraint;
236
+ const humanTable = this.toHumanReadable(table || 'record');
237
+ return new BaseAppException({
238
+ message: `${humanTable} violates a validation rule`,
239
+ messageKey: ERROR_MESSAGES.DB_CHECK_CONSTRAINT,
240
+ messageVariables: {
241
+ entity: humanTable,
242
+ ...constraint !== undefined && {
243
+ constraint
244
+ }
245
+ },
246
+ status: HttpStatus.BAD_REQUEST
247
+ });
248
+ }
249
+ // DEADLOCK
250
+ // PG: 40P01 | MSSQL: 1205 | MySQL: ER_LOCK_DEADLOCK / errno 1213
251
+ if (code === '40P01' || number === 1205 || code === 'ER_LOCK_DEADLOCK' || errno === 1213) {
252
+ return new BaseAppException({
253
+ message: 'The request could not be completed due to a conflict, please try again',
254
+ messageKey: ERROR_MESSAGES.DB_DEADLOCK,
255
+ status: HttpStatus.SERVICE_UNAVAILABLE
256
+ });
257
+ }
185
258
  return null;
186
259
  }
187
260
  toHumanReadable(dbName) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flusys/nestjs-shared",
3
- "version": "5.4.1",
3
+ "version": "5.5.0",
4
4
  "description": "Common shared utilities for Flusys NestJS applications",
5
5
  "main": "cjs/index.js",
6
6
  "module": "fesm/index.js",
@@ -103,15 +103,15 @@
103
103
  "@nestjs/typeorm": "^11.0.0",
104
104
  "cacheable": "^2.1.0",
105
105
  "class-transformer": "^0.5.1",
106
- "class-validator": "^0.14.0 || ^0.15.0",
106
+ "class-validator": "^0.15.0",
107
107
  "keyv": "^5.0.0",
108
108
  "typeorm": "^0.3.0",
109
109
  "winston": "^3.0.0",
110
110
  "winston-daily-rotate-file": "^5.0.0",
111
- "express": "^4.18.0 || ^5.0.0",
111
+ "express": "^5.0.0",
112
112
  "rxjs": "^7.8.0"
113
113
  },
114
114
  "dependencies": {
115
- "@flusys/nestjs-core": "5.4.1"
115
+ "@flusys/nestjs-core": "5.5.0"
116
116
  }
117
117
  }