@flusys/nestjs-shared 5.3.1 → 5.4.1

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);
@@ -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'));
@@ -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';
@@ -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";
@@ -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
@@ -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.1",
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.1"
116
116
  }
117
117
  }