@flusys/nestjs-shared 5.4.0 → 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.
@@ -69,7 +69,7 @@ let ApiService = class ApiService {
69
69
  await this.beforeInsertOperation(dto, user, qr);
70
70
  const entities = await this.convertRequestDtoToEntity(dto, user);
71
71
  const saved = await qr.manager.save(this.repository.target, entities);
72
- await this.afterInsertOperation(this.ensureArray(saved), user, qr);
72
+ await this.afterInsertOperation(this.ensureArray(saved), dto, user, qr);
73
73
  return {
74
74
  saved,
75
75
  returnFirst: true
@@ -81,7 +81,7 @@ let ApiService = class ApiService {
81
81
  await this.beforeInsertOperation(dtos, user, qr);
82
82
  const entities = await this.convertRequestDtoToEntity(dtos, user);
83
83
  const saved = await qr.manager.save(this.repository.target, entities);
84
- await this.afterInsertOperation(this.ensureArray(saved), user, qr);
84
+ await this.afterInsertOperation(this.ensureArray(saved), dtos, user, qr);
85
85
  return {
86
86
  saved,
87
87
  returnFirst: false
@@ -93,7 +93,7 @@ let ApiService = class ApiService {
93
93
  await this.beforeUpdateOperation(dto, user, qr);
94
94
  const entities = await this.convertRequestDtoToEntity(dto, user);
95
95
  const saved = await qr.manager.save(this.repository.target, entities);
96
- await this.afterUpdateOperation(this.ensureArray(saved), user, qr);
96
+ await this.afterUpdateOperation(this.ensureArray(saved), dto, user, qr);
97
97
  return {
98
98
  saved,
99
99
  returnFirst: true
@@ -105,7 +105,7 @@ let ApiService = class ApiService {
105
105
  await this.beforeUpdateOperation(dtos, user, qr);
106
106
  const entities = await this.convertRequestDtoToEntity(dtos, user);
107
107
  const saved = await qr.manager.save(this.repository.target, entities);
108
- await this.afterUpdateOperation(this.ensureArray(saved), user, qr);
108
+ await this.afterUpdateOperation(this.ensureArray(saved), dtos, user, qr);
109
109
  return {
110
110
  saved,
111
111
  returnFirst: false
@@ -119,14 +119,14 @@ let ApiService = class ApiService {
119
119
  await this.beforeInsertOperation(toInsert, user, qr);
120
120
  const insertedEntities = await this.convertRequestDtoToEntity(toInsert, user);
121
121
  const insertedSaved = await qr.manager.save(this.repository.target, insertedEntities);
122
- await this.afterInsertOperation(this.ensureArray(insertedSaved), user, qr);
122
+ await this.afterInsertOperation(this.ensureArray(insertedSaved), toInsert, user, qr);
123
123
  allResults.push(...this.ensureArray(insertedSaved));
124
124
  }
125
125
  if (toUpdate.length > 0) {
126
126
  await this.beforeUpdateOperation(toUpdate, user, qr);
127
127
  const updatedEntities = await this.convertRequestDtoToEntity(toUpdate, user);
128
128
  const updatedSaved = await qr.manager.save(this.repository.target, updatedEntities);
129
- await this.afterUpdateOperation(this.ensureArray(updatedSaved), user, qr);
129
+ await this.afterUpdateOperation(this.ensureArray(updatedSaved), toUpdate, user, qr);
130
130
  allResults.push(...this.ensureArray(updatedSaved));
131
131
  }
132
132
  return {
@@ -329,7 +329,7 @@ let ApiService = class ApiService {
329
329
  id
330
330
  };
331
331
  });
332
- await this.afterDeleteOperation(ids, user, queryRunner);
332
+ await this.afterDeleteOperation(ids, option, user, queryRunner);
333
333
  await queryRunner.commitTransaction();
334
334
  await Promise.all([
335
335
  this.clearCacheForAll(),
@@ -390,11 +390,11 @@ let ApiService = class ApiService {
390
390
  }
391
391
  }
392
392
  async beforeInsertOperation(_dto, _user, _queryRunner) {}
393
- async afterInsertOperation(_entity, _user, _queryRunner) {}
393
+ async afterInsertOperation(_entity, _dto, _user, _queryRunner) {}
394
394
  async beforeUpdateOperation(_dto, _user, _queryRunner) {}
395
- async afterUpdateOperation(_entity, _user, _queryRunner) {}
395
+ async afterUpdateOperation(_entity, _dto, _user, _queryRunner) {}
396
396
  async beforeDeleteOperation(_dto, _user, _queryRunner) {}
397
- async afterDeleteOperation(_entity, _user, _queryRunner) {}
397
+ async afterDeleteOperation(_entity, _dto, _user, _queryRunner) {}
398
398
  async getFilterQuery(query, filter, _user) {
399
399
  Object.entries(filter).forEach(([key, value])=>{
400
400
  query.andWhere(`${this.entityName}.${key} = :value`, {
@@ -9,6 +9,9 @@ function _export(target, all) {
9
9
  });
10
10
  }
11
11
  _export(exports, {
12
+ get buildQuery () {
13
+ return buildQuery;
14
+ },
12
15
  get getCastIntType () {
13
16
  return getCastIntType;
14
17
  },
@@ -27,3 +30,8 @@ function getCastIntType() {
27
30
  const driver = _nestjscore.envConfig.getTypeOrmConfig().type;
28
31
  return driver === 'postgres' ? 'INTEGER' : 'UNSIGNED';
29
32
  }
33
+ function buildQuery(sql, dbType) {
34
+ if (dbType !== 'postgres') return sql;
35
+ let index = 0;
36
+ return sql.replace(/\?/g, ()=>`$${++index}`);
37
+ }
@@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  });
5
5
  _export_star(require("./api-controller.class"), exports);
6
6
  _export_star(require("./api-service.class"), exports);
7
- _export_star(require("./handle-custome-db-column-type"), exports);
7
+ _export_star(require("./handle-custome-db"), exports);
8
8
  _export_star(require("./hybrid-cache.class"), exports);
9
9
  _export_star(require("./winston.logger.class"), exports);
10
10
  _export_star(require("../constants/permissions"), exports);
@@ -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) {
@@ -41,13 +41,13 @@ export declare abstract class ApiService<CreateDtoT extends object, UpdateDtoT e
41
41
  private ensureArray;
42
42
  private executeInTransaction;
43
43
  protected beforeInsertOperation(_dto: CreateDtoT | Array<CreateDtoT>, _user: ILoggedUserInfo | null, _queryRunner: QueryRunner): Promise<void>;
44
- protected afterInsertOperation(_entity: EntityT[], _user: ILoggedUserInfo | null, _queryRunner: QueryRunner): Promise<void>;
44
+ protected afterInsertOperation(_entity: EntityT[], _dto: CreateDtoT | Array<CreateDtoT>, _user: ILoggedUserInfo | null, _queryRunner: QueryRunner): Promise<void>;
45
45
  protected beforeUpdateOperation(_dto: UpdateDtoT | Array<UpdateDtoT>, _user: ILoggedUserInfo | null, _queryRunner: QueryRunner): Promise<void>;
46
- protected afterUpdateOperation(_entity: EntityT[], _user: ILoggedUserInfo | null, _queryRunner: QueryRunner): Promise<void>;
46
+ protected afterUpdateOperation(_entity: EntityT[], _dto: UpdateDtoT | Array<UpdateDtoT>, _user: ILoggedUserInfo | null, _queryRunner: QueryRunner): Promise<void>;
47
47
  protected beforeDeleteOperation(_dto: DeleteDto, _user: ILoggedUserInfo | null, _queryRunner: QueryRunner): Promise<void>;
48
48
  protected afterDeleteOperation(_entity: Array<{
49
49
  id: string;
50
- }>, _user: ILoggedUserInfo | null, _queryRunner: QueryRunner): Promise<void>;
50
+ }>, _dto: DeleteDto, _user: ILoggedUserInfo | null, _queryRunner: QueryRunner): Promise<void>;
51
51
  protected getFilterQuery(query: SelectQueryBuilder<EntityT>, filter: Record<string, unknown>, _user: ILoggedUserInfo | null): Promise<{
52
52
  query: SelectQueryBuilder<EntityT>;
53
53
  isRaw: boolean;
@@ -1,2 +1,3 @@
1
1
  export declare function getJsonColumnType(): "simple-json" | "json" | "jsonb";
2
2
  export declare function getCastIntType(): string;
3
+ export declare function buildQuery(sql: string, dbType: string): string;
@@ -1,6 +1,6 @@
1
1
  export * from './api-controller.class';
2
2
  export * from './api-service.class';
3
- export * from './handle-custome-db-column-type';
3
+ export * from './handle-custome-db';
4
4
  export * from './hybrid-cache.class';
5
5
  export * from './winston.logger.class';
6
6
  export * from '../constants/permissions';
@@ -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";
@@ -59,7 +59,7 @@ export class ApiService {
59
59
  await this.beforeInsertOperation(dto, user, qr);
60
60
  const entities = await this.convertRequestDtoToEntity(dto, user);
61
61
  const saved = await qr.manager.save(this.repository.target, entities);
62
- await this.afterInsertOperation(this.ensureArray(saved), user, qr);
62
+ await this.afterInsertOperation(this.ensureArray(saved), dto, user, qr);
63
63
  return {
64
64
  saved,
65
65
  returnFirst: true
@@ -71,7 +71,7 @@ export class ApiService {
71
71
  await this.beforeInsertOperation(dtos, user, qr);
72
72
  const entities = await this.convertRequestDtoToEntity(dtos, user);
73
73
  const saved = await qr.manager.save(this.repository.target, entities);
74
- await this.afterInsertOperation(this.ensureArray(saved), user, qr);
74
+ await this.afterInsertOperation(this.ensureArray(saved), dtos, user, qr);
75
75
  return {
76
76
  saved,
77
77
  returnFirst: false
@@ -83,7 +83,7 @@ export class ApiService {
83
83
  await this.beforeUpdateOperation(dto, user, qr);
84
84
  const entities = await this.convertRequestDtoToEntity(dto, user);
85
85
  const saved = await qr.manager.save(this.repository.target, entities);
86
- await this.afterUpdateOperation(this.ensureArray(saved), user, qr);
86
+ await this.afterUpdateOperation(this.ensureArray(saved), dto, user, qr);
87
87
  return {
88
88
  saved,
89
89
  returnFirst: true
@@ -95,7 +95,7 @@ export class ApiService {
95
95
  await this.beforeUpdateOperation(dtos, user, qr);
96
96
  const entities = await this.convertRequestDtoToEntity(dtos, user);
97
97
  const saved = await qr.manager.save(this.repository.target, entities);
98
- await this.afterUpdateOperation(this.ensureArray(saved), user, qr);
98
+ await this.afterUpdateOperation(this.ensureArray(saved), dtos, user, qr);
99
99
  return {
100
100
  saved,
101
101
  returnFirst: false
@@ -109,14 +109,14 @@ export class ApiService {
109
109
  await this.beforeInsertOperation(toInsert, user, qr);
110
110
  const insertedEntities = await this.convertRequestDtoToEntity(toInsert, user);
111
111
  const insertedSaved = await qr.manager.save(this.repository.target, insertedEntities);
112
- await this.afterInsertOperation(this.ensureArray(insertedSaved), user, qr);
112
+ await this.afterInsertOperation(this.ensureArray(insertedSaved), toInsert, user, qr);
113
113
  allResults.push(...this.ensureArray(insertedSaved));
114
114
  }
115
115
  if (toUpdate.length > 0) {
116
116
  await this.beforeUpdateOperation(toUpdate, user, qr);
117
117
  const updatedEntities = await this.convertRequestDtoToEntity(toUpdate, user);
118
118
  const updatedSaved = await qr.manager.save(this.repository.target, updatedEntities);
119
- await this.afterUpdateOperation(this.ensureArray(updatedSaved), user, qr);
119
+ await this.afterUpdateOperation(this.ensureArray(updatedSaved), toUpdate, user, qr);
120
120
  allResults.push(...this.ensureArray(updatedSaved));
121
121
  }
122
122
  return {
@@ -319,7 +319,7 @@ export class ApiService {
319
319
  id
320
320
  };
321
321
  });
322
- await this.afterDeleteOperation(ids, user, queryRunner);
322
+ await this.afterDeleteOperation(ids, option, user, queryRunner);
323
323
  await queryRunner.commitTransaction();
324
324
  await Promise.all([
325
325
  this.clearCacheForAll(),
@@ -380,11 +380,11 @@ export class ApiService {
380
380
  }
381
381
  }
382
382
  async beforeInsertOperation(_dto, _user, _queryRunner) {}
383
- async afterInsertOperation(_entity, _user, _queryRunner) {}
383
+ async afterInsertOperation(_entity, _dto, _user, _queryRunner) {}
384
384
  async beforeUpdateOperation(_dto, _user, _queryRunner) {}
385
- async afterUpdateOperation(_entity, _user, _queryRunner) {}
385
+ async afterUpdateOperation(_entity, _dto, _user, _queryRunner) {}
386
386
  async beforeDeleteOperation(_dto, _user, _queryRunner) {}
387
- async afterDeleteOperation(_entity, _user, _queryRunner) {}
387
+ async afterDeleteOperation(_entity, _dto, _user, _queryRunner) {}
388
388
  async getFilterQuery(query, filter, _user) {
389
389
  Object.entries(filter).forEach(([key, value])=>{
390
390
  query.andWhere(`${this.entityName}.${key} = :value`, {
@@ -9,3 +9,11 @@ export function getCastIntType() {
9
9
  const driver = envConfig.getTypeOrmConfig().type;
10
10
  return driver === 'postgres' ? 'INTEGER' : 'UNSIGNED';
11
11
  }
12
+ /**
13
+ * Converts MySQL-style `?` placeholders to PostgreSQL `$1, $2, ...` placeholders.
14
+ * No-op for MySQL/MariaDB.
15
+ */ export function buildQuery(sql, dbType) {
16
+ if (dbType !== 'postgres') return sql;
17
+ let index = 0;
18
+ return sql.replace(/\?/g, ()=>`$${++index}`);
19
+ }
@@ -1,6 +1,6 @@
1
1
  export * from './api-controller.class';
2
2
  export * from './api-service.class';
3
- export * from './handle-custome-db-column-type';
3
+ export * from './handle-custome-db';
4
4
  export * from './hybrid-cache.class';
5
5
  export * from './winston.logger.class';
6
6
  // Re-export permission constants for convenience
@@ -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.0",
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.0"
115
+ "@flusys/nestjs-core": "5.5.0"
116
116
  }
117
117
  }