@flusys/nestjs-shared 3.0.1 → 4.0.0-lts

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.
Files changed (82) hide show
  1. package/README.md +159 -79
  2. package/cjs/classes/api-controller.class.js +26 -8
  3. package/cjs/classes/api-service.class.js +100 -17
  4. package/cjs/classes/winston-logger-adapter.class.js +15 -20
  5. package/cjs/classes/winston.logger.class.js +103 -70
  6. package/cjs/constants/index.js +1 -0
  7. package/cjs/constants/message-keys.js +80 -0
  8. package/cjs/constants/permissions.js +32 -1
  9. package/cjs/decorators/index.js +1 -0
  10. package/cjs/decorators/log-action.decorator.js +149 -0
  11. package/cjs/dtos/response-payload.dto.js +72 -0
  12. package/cjs/exceptions/base-app.exception.js +145 -0
  13. package/cjs/exceptions/index.js +1 -0
  14. package/cjs/exceptions/permission.exception.js +12 -8
  15. package/cjs/filters/global-exception.filter.js +167 -0
  16. package/cjs/filters/index.js +18 -0
  17. package/cjs/guards/jwt-auth.guard.js +4 -1
  18. package/cjs/guards/permission.guard.js +6 -13
  19. package/cjs/index.js +1 -0
  20. package/cjs/interceptors/idempotency.interceptor.js +1 -1
  21. package/cjs/interceptors/index.js +0 -1
  22. package/cjs/interfaces/logger.interface.js +1 -4
  23. package/cjs/middlewares/logger.middleware.js +83 -26
  24. package/cjs/modules/datasource/multi-tenant-datasource.service.js +33 -11
  25. package/cjs/modules/utils/utils.service.js +4 -20
  26. package/cjs/utils/index.js +0 -1
  27. package/cjs/utils/query-helpers.util.js +8 -1
  28. package/classes/api-controller.class.d.ts +1 -0
  29. package/classes/api-service.class.d.ts +5 -10
  30. package/classes/winston-logger-adapter.class.d.ts +12 -11
  31. package/classes/winston.logger.class.d.ts +1 -0
  32. package/constants/index.d.ts +1 -0
  33. package/constants/message-keys.d.ts +81 -0
  34. package/constants/permissions.d.ts +36 -0
  35. package/decorators/index.d.ts +1 -0
  36. package/decorators/log-action.decorator.d.ts +8 -0
  37. package/dtos/response-payload.dto.d.ts +8 -0
  38. package/exceptions/base-app.exception.d.ts +41 -0
  39. package/exceptions/index.d.ts +1 -0
  40. package/exceptions/permission.exception.d.ts +1 -1
  41. package/fesm/classes/api-controller.class.js +26 -8
  42. package/fesm/classes/api-service.class.js +101 -18
  43. package/fesm/classes/winston-logger-adapter.class.js +18 -44
  44. package/fesm/classes/winston.logger.class.js +100 -68
  45. package/fesm/constants/index.js +2 -0
  46. package/fesm/constants/message-keys.js +59 -0
  47. package/fesm/constants/permissions.js +24 -1
  48. package/fesm/decorators/index.js +1 -0
  49. package/fesm/decorators/log-action.decorator.js +139 -0
  50. package/fesm/dtos/response-payload.dto.js +72 -0
  51. package/fesm/exceptions/base-app.exception.js +109 -0
  52. package/fesm/exceptions/index.js +1 -0
  53. package/fesm/exceptions/permission.exception.js +15 -17
  54. package/fesm/filters/global-exception.filter.js +157 -0
  55. package/fesm/filters/index.js +1 -0
  56. package/fesm/guards/jwt-auth.guard.js +5 -2
  57. package/fesm/guards/permission.guard.js +8 -15
  58. package/fesm/index.js +1 -0
  59. package/fesm/interceptors/idempotency.interceptor.js +2 -2
  60. package/fesm/interceptors/index.js +0 -1
  61. package/fesm/interfaces/logger.interface.js +1 -4
  62. package/fesm/middlewares/logger.middleware.js +83 -26
  63. package/fesm/modules/datasource/multi-tenant-datasource.service.js +34 -12
  64. package/fesm/modules/utils/utils.service.js +5 -21
  65. package/fesm/utils/index.js +0 -1
  66. package/fesm/utils/query-helpers.util.js +8 -1
  67. package/filters/global-exception.filter.d.ts +10 -0
  68. package/filters/index.d.ts +1 -0
  69. package/guards/permission.guard.d.ts +1 -3
  70. package/index.d.ts +1 -0
  71. package/interceptors/index.d.ts +0 -1
  72. package/interfaces/logger.interface.d.ts +5 -5
  73. package/modules/datasource/multi-tenant-datasource.service.d.ts +1 -2
  74. package/modules/utils/utils.service.d.ts +0 -1
  75. package/package.json +5 -3
  76. package/utils/index.d.ts +0 -1
  77. package/cjs/interceptors/query-performance.interceptor.js +0 -66
  78. package/cjs/utils/error-handler.util.js +0 -90
  79. package/fesm/interceptors/query-performance.interceptor.js +0 -56
  80. package/fesm/utils/error-handler.util.js +0 -82
  81. package/interceptors/query-performance.interceptor.d.ts +0 -8
  82. package/utils/error-handler.util.d.ts +0 -19
@@ -0,0 +1,41 @@
1
+ import { HttpException, HttpStatus } from '@nestjs/common';
2
+ export interface IValidationError {
3
+ field: string;
4
+ message: string;
5
+ }
6
+ export interface IBaseAppExceptionOptions {
7
+ message: string;
8
+ messageKey?: string;
9
+ messageParams?: Record<string, unknown>;
10
+ status?: HttpStatus;
11
+ errors?: IValidationError[];
12
+ metadata?: Record<string, unknown>;
13
+ }
14
+ export declare class BaseAppException extends HttpException {
15
+ readonly messageKey: string;
16
+ readonly messageParams?: Record<string, unknown>;
17
+ readonly errors?: IValidationError[];
18
+ readonly metadata?: Record<string, unknown>;
19
+ constructor(options: IBaseAppExceptionOptions);
20
+ }
21
+ export declare class NotFoundException extends BaseAppException {
22
+ constructor(entity: string, id?: string);
23
+ }
24
+ export declare class ValidationException extends BaseAppException {
25
+ constructor(errors: IValidationError[]);
26
+ }
27
+ export declare class UnauthorizedException extends BaseAppException {
28
+ constructor(message?: string);
29
+ }
30
+ export declare class ForbiddenException extends BaseAppException {
31
+ constructor(message?: string);
32
+ }
33
+ export declare class ConflictException extends BaseAppException {
34
+ constructor(entity: string, field?: string);
35
+ }
36
+ export declare class InternalServerException extends BaseAppException {
37
+ constructor(message?: string);
38
+ }
39
+ export declare class ServiceUnavailableException extends BaseAppException {
40
+ constructor(service: string);
41
+ }
@@ -1 +1,2 @@
1
+ export * from './base-app.exception';
1
2
  export * from './permission.exception';
@@ -1,6 +1,6 @@
1
1
  import { ForbiddenException, InternalServerErrorException } from '@nestjs/common';
2
2
  export declare class PermissionSystemUnavailableException extends InternalServerErrorException {
3
- constructor(message?: string);
3
+ constructor();
4
4
  }
5
5
  export declare class InsufficientPermissionsException extends ForbiddenException {
6
6
  constructor(missingPermissions: string[], operator?: 'AND' | 'OR');
@@ -78,6 +78,8 @@ import { ApiResponseDto } from '../decorators/api-response.decorator';
78
78
  return applyDecorators(...decorators);
79
79
  }
80
80
  /** Creates an API Controller with standardized CRUD endpoints (POST-only RPC pattern) */ export function createApiController(createDtoClass, updateDtoClass, responseDtoClass, options = {}) {
81
+ // Entity name for message keys (defaults to 'item')
82
+ const entityName = options.entityName ?? 'item';
81
83
  // Determine if security is global (applies to all) or per-endpoint
82
84
  const securityConfig = options.security;
83
85
  const endpointKeys = [
@@ -114,7 +116,8 @@ import { ApiResponseDto } from '../decorators/api-response.decorator';
114
116
  const data = plainToInstance(responseDtoClass, entity);
115
117
  return {
116
118
  success: true,
117
- message: 'Item created successfully',
119
+ message: `${entityName.charAt(0).toUpperCase() + entityName.slice(1)} created successfully`,
120
+ messageKey: `${entityName}.create.success`,
118
121
  data
119
122
  };
120
123
  }
@@ -123,7 +126,11 @@ import { ApiResponseDto } from '../decorators/api-response.decorator';
123
126
  const data = entities.map((item)=>plainToInstance(responseDtoClass, item));
124
127
  return {
125
128
  success: true,
126
- message: `${data.length} items created successfully`,
129
+ message: `${data.length} ${entityName}s created successfully`,
130
+ messageKey: `${entityName}.create.many.success`,
131
+ messageVariables: {
132
+ count: data.length
133
+ },
127
134
  data,
128
135
  meta: {
129
136
  count: data.length,
@@ -137,7 +144,8 @@ import { ApiResponseDto } from '../decorators/api-response.decorator';
137
144
  const data = plainToInstance(responseDtoClass, entity);
138
145
  return {
139
146
  success: true,
140
- message: 'Item retrieved successfully',
147
+ message: `${entityName.charAt(0).toUpperCase() + entityName.slice(1)} retrieved successfully`,
148
+ messageKey: `${entityName}.get.success`,
141
149
  data
142
150
  };
143
151
  }
@@ -146,7 +154,8 @@ import { ApiResponseDto } from '../decorators/api-response.decorator';
146
154
  const data = plainToInstance(responseDtoClass, entity);
147
155
  return {
148
156
  success: true,
149
- message: 'Item updated successfully',
157
+ message: `${entityName.charAt(0).toUpperCase() + entityName.slice(1)} updated successfully`,
158
+ messageKey: `${entityName}.update.success`,
150
159
  data
151
160
  };
152
161
  }
@@ -155,7 +164,11 @@ import { ApiResponseDto } from '../decorators/api-response.decorator';
155
164
  const data = plainToInstance(responseDtoClass, entities);
156
165
  return {
157
166
  success: true,
158
- message: `${data.length} items updated successfully`,
167
+ message: `${data.length} ${entityName}s updated successfully`,
168
+ messageKey: `${entityName}.update.many.success`,
169
+ messageVariables: {
170
+ count: data.length
171
+ },
159
172
  data,
160
173
  meta: {
161
174
  count: data.length,
@@ -172,7 +185,8 @@ import { ApiResponseDto } from '../decorators/api-response.decorator';
172
185
  const totalPages = pageSize > 0 ? Math.ceil(result.total / pageSize) : 1;
173
186
  return {
174
187
  success: true,
175
- message: 'Data retrieved successfully',
188
+ message: `${entityName.charAt(0).toUpperCase() + entityName.slice(1)}s retrieved successfully`,
189
+ messageKey: `${entityName}.get.all.success`,
176
190
  data,
177
191
  meta: {
178
192
  total: result.total,
@@ -187,10 +201,14 @@ import { ApiResponseDto } from '../decorators/api-response.decorator';
187
201
  async delete(deleteDto, user) {
188
202
  await this.service.delete(deleteDto, user);
189
203
  const count = Array.isArray(deleteDto.id) ? deleteDto.id.length : 1;
190
- const action = deleteDto.type === 'restore' ? 'restored' : 'deleted';
204
+ const action = deleteDto.type === 'restore' ? 'restore' : 'delete';
191
205
  return {
192
206
  success: true,
193
- message: `${count} item${count > 1 ? 's' : ''} ${action} successfully`
207
+ message: `${count} ${entityName}${count > 1 ? 's' : ''} ${action}d successfully`,
208
+ messageKey: `${entityName}.${action}.success`,
209
+ messageVariables: {
210
+ count
211
+ }
194
212
  };
195
213
  }
196
214
  constructor(service){
@@ -11,9 +11,20 @@ function _define_property(obj, key, value) {
11
11
  }
12
12
  return obj;
13
13
  }
14
- import { ErrorHandler } from '../utils/error-handler.util';
15
- import { Logger, NotFoundException } from '@nestjs/common';
14
+ function _ts_decorate(decorators, target, key, desc) {
15
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
16
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
17
+ else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
18
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
19
+ }
20
+ function _ts_metadata(k, v) {
21
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
22
+ }
23
+ import { DeleteDto } from '../dtos';
24
+ import { InternalServerErrorException, NotFoundException } from '@nestjs/common';
25
+ import { SYSTEM_MESSAGES } from '../constants';
16
26
  import { In } from 'typeorm';
27
+ import { LogAction } from '../decorators/log-action.decorator';
17
28
  /** Generic API service with CRUD operations and caching support */ export class ApiService {
18
29
  async insert(dto, user) {
19
30
  return this.executeInTransaction('insert', async (qr)=>{
@@ -78,7 +89,10 @@ import { In } from 'typeorm';
78
89
  output = await finalQuery.getMany();
79
90
  }
80
91
  const result = this.convertEntityListToResponseListDto(output, isRaw);
81
- if (!result || !result.length) throw new NotFoundException('No Data Found');
92
+ if (!result || !result.length) throw new NotFoundException({
93
+ message: 'No Data Found',
94
+ messageKey: SYSTEM_MESSAGES.NOT_FOUND
95
+ });
82
96
  return result;
83
97
  } catch (error) {
84
98
  if (error instanceof NotFoundException) {
@@ -109,7 +123,10 @@ import { In } from 'typeorm';
109
123
  if (convertResult.length) result = convertResult[0];
110
124
  } else {
111
125
  output = await finalQuery.getOne();
112
- if (!output) throw new NotFoundException('No Data Found');
126
+ if (!output) throw new NotFoundException({
127
+ message: 'No Data Found',
128
+ messageKey: SYSTEM_MESSAGES.NOT_FOUND
129
+ });
113
130
  result = this.convertEntityToResponseDto(output, false);
114
131
  }
115
132
  if (this.isCacheable) {
@@ -199,7 +216,15 @@ import { In } from 'typeorm';
199
216
  const parameters = rawSubQuery.getParameters();
200
217
  const orderedValues = [];
201
218
  const convertedQuery = countSql.replace(/:(\w+)/g, (_, key)=>{
202
- if (!(key in parameters)) throw new Error(`Missing parameter value for: ${key}`);
219
+ if (!(key in parameters)) {
220
+ throw new InternalServerErrorException({
221
+ message: `Missing parameter value for: ${key}`,
222
+ messageKey: SYSTEM_MESSAGES.MISSING_PARAMETER,
223
+ messageParams: {
224
+ key
225
+ }
226
+ });
227
+ }
203
228
  orderedValues.push(parameters[key]);
204
229
  return '?';
205
230
  });
@@ -272,13 +297,14 @@ import { In } from 'typeorm';
272
297
  async clearCacheForId(entities) {
273
298
  await Promise.all(entities.map((item)=>this.utilsService.clearCache(this.entityName, this.cacheManager, item.id)));
274
299
  }
275
- /**
276
- * Helper method to handle errors with proper logging and type safety
277
- */ handleError(error, operation) {
278
- ErrorHandler.logError(this.logger, error, operation, {
279
- entity: this.entityName
300
+ handleError(error, _operation) {
301
+ if (error instanceof Error) {
302
+ throw error;
303
+ }
304
+ throw new InternalServerErrorException({
305
+ message: typeof error === 'string' ? error : 'Unknown error',
306
+ messageKey: SYSTEM_MESSAGES.INTERNAL_ERROR
280
307
  });
281
- ErrorHandler.rethrowError(error);
282
308
  }
283
309
  /** Ensures value is always an array */ ensureArray(value) {
284
310
  return Array.isArray(value) ? value : [
@@ -375,7 +401,10 @@ import { In } from 'typeorm';
375
401
  id: d.id
376
402
  }
377
403
  });
378
- if (!existing) throw new NotFoundException('No such entity data found for update! Please, Try Again.');
404
+ if (!existing) throw new NotFoundException({
405
+ message: 'No such entity data found for update! Please, Try Again.',
406
+ messageKey: SYSTEM_MESSAGES.NOT_FOUND
407
+ });
379
408
  return Object.assign(existing, d);
380
409
  }
381
410
  delete d.id;
@@ -393,21 +422,75 @@ import { In } from 'typeorm';
393
422
  async getEntityClass() {
394
423
  return await this.repository.create();
395
424
  }
396
- constructor(entityName, repository, cacheManager, utilsService, loggerName, isCacheable = false){
425
+ constructor(entityName, repository, cacheManager, utilsService, _loggerName, isCacheable = false, moduleName){
397
426
  _define_property(this, "entityName", void 0);
398
427
  _define_property(this, "repository", void 0);
399
428
  _define_property(this, "cacheManager", void 0);
400
429
  _define_property(this, "utilsService", void 0);
401
- _define_property(this, "loggerName", void 0);
430
+ _define_property(this, "_loggerName", void 0);
402
431
  _define_property(this, "isCacheable", void 0);
403
- _define_property(this, "logger", void 0);
432
+ _define_property(this, "moduleName", void 0);
404
433
  this.entityName = entityName;
405
434
  this.repository = repository;
406
435
  this.cacheManager = cacheManager;
407
436
  this.utilsService = utilsService;
408
- this.loggerName = loggerName;
437
+ this._loggerName = _loggerName;
409
438
  this.isCacheable = isCacheable;
410
- this.logger = new Logger('ApiService');
411
- this.logger = new Logger(loggerName);
439
+ this.moduleName = moduleName;
412
440
  }
413
441
  }
442
+ _ts_decorate([
443
+ LogAction({
444
+ action: 'create'
445
+ }),
446
+ _ts_metadata("design:type", Function),
447
+ _ts_metadata("design:paramtypes", [
448
+ typeof CreateDtoT === "undefined" ? Object : CreateDtoT,
449
+ Object
450
+ ]),
451
+ _ts_metadata("design:returntype", Promise)
452
+ ], ApiService.prototype, "insert", null);
453
+ _ts_decorate([
454
+ LogAction({
455
+ action: 'createMany'
456
+ }),
457
+ _ts_metadata("design:type", Function),
458
+ _ts_metadata("design:paramtypes", [
459
+ typeof Array === "undefined" ? Object : Array,
460
+ Object
461
+ ]),
462
+ _ts_metadata("design:returntype", Promise)
463
+ ], ApiService.prototype, "insertMany", null);
464
+ _ts_decorate([
465
+ LogAction({
466
+ action: 'update'
467
+ }),
468
+ _ts_metadata("design:type", Function),
469
+ _ts_metadata("design:paramtypes", [
470
+ typeof UpdateDtoT === "undefined" ? Object : UpdateDtoT,
471
+ Object
472
+ ]),
473
+ _ts_metadata("design:returntype", Promise)
474
+ ], ApiService.prototype, "update", null);
475
+ _ts_decorate([
476
+ LogAction({
477
+ action: 'updateMany'
478
+ }),
479
+ _ts_metadata("design:type", Function),
480
+ _ts_metadata("design:paramtypes", [
481
+ Array,
482
+ Object
483
+ ]),
484
+ _ts_metadata("design:returntype", Promise)
485
+ ], ApiService.prototype, "updateMany", null);
486
+ _ts_decorate([
487
+ LogAction({
488
+ action: 'delete'
489
+ }),
490
+ _ts_metadata("design:type", Function),
491
+ _ts_metadata("design:paramtypes", [
492
+ typeof DeleteDto === "undefined" ? Object : DeleteDto,
493
+ Object
494
+ ]),
495
+ _ts_metadata("design:returntype", Promise)
496
+ ], ApiService.prototype, "delete", null);
@@ -11,39 +11,17 @@ function _define_property(obj, key, value) {
11
11
  }
12
12
  return obj;
13
13
  }
14
- import { instance as winstonLogger } from './winston.logger.class';
14
+ import { instance as winstonLogger, createModuleLogger } from './winston.logger.class';
15
15
  import { getRequestId, getUserId, getTenantId, getCompanyId } from '../middlewares/logger.middleware';
16
- /**
17
- * Get correlation context for logging
18
- * Automatically includes requestId, userId, tenantId, companyId if available
19
- */ function getCorrelationMeta() {
20
- const meta = {};
21
- const requestId = getRequestId();
22
- const userId = getUserId();
23
- const tenantId = getTenantId();
24
- const companyId = getCompanyId();
25
- if (requestId) meta.requestId = requestId;
26
- if (userId) meta.userId = userId;
27
- if (tenantId) meta.tenantId = tenantId;
28
- if (companyId) meta.companyId = companyId;
29
- return meta;
16
+ function getCorrelationMeta() {
17
+ return Object.fromEntries(Object.entries({
18
+ requestId: getRequestId(),
19
+ userId: getUserId(),
20
+ tenantId: getTenantId(),
21
+ companyId: getCompanyId()
22
+ }).filter(([, v])=>v != null));
30
23
  }
31
- /**
32
- * Winston Logger Adapter
33
- * Adapts Winston logger to ILogger interface for dependency injection
34
- *
35
- * Features:
36
- * - Automatic correlation ID injection
37
- * - User ID tracking
38
- * - Structured metadata logging
39
- *
40
- * Usage:
41
- * ```typescript
42
- * const logger = new WinstonLoggerAdapter('MyService');
43
- * logger.log('Operation completed', undefined, { orderId: '123' });
44
- * // Output: { requestId: 'uuid', userId: 'user-id', context: 'MyService', message: 'Operation completed', orderId: '123' }
45
- * ```
46
- */ export class WinstonLoggerAdapter {
24
+ export class WinstonLoggerAdapter {
47
25
  buildLogMeta(context, args, extra) {
48
26
  const meta = args?.length && typeof args[0] === 'object' ? args[0] : {};
49
27
  return {
@@ -54,34 +32,30 @@ import { getRequestId, getUserId, getTenantId, getCompanyId } from '../middlewar
54
32
  };
55
33
  }
56
34
  log(message, context, ...args) {
57
- winstonLogger.info(message, this.buildLogMeta(context, args));
35
+ this.logger.info(message, this.buildLogMeta(context, args));
58
36
  }
59
37
  error(message, trace, context, ...args) {
60
- winstonLogger.error(message, this.buildLogMeta(context, args, {
38
+ this.logger.error(message, this.buildLogMeta(context, args, {
61
39
  stack: trace
62
40
  }));
63
41
  }
64
42
  warn(message, context, ...args) {
65
- winstonLogger.warn(message, this.buildLogMeta(context, args));
43
+ this.logger.warn(message, this.buildLogMeta(context, args));
66
44
  }
67
45
  debug(message, context, ...args) {
68
- winstonLogger.debug(message, this.buildLogMeta(context, args));
46
+ this.logger.debug(message, this.buildLogMeta(context, args));
69
47
  }
70
48
  verbose(message, context, ...args) {
71
- winstonLogger.verbose(message, this.buildLogMeta(context, args));
49
+ this.logger.verbose(message, this.buildLogMeta(context, args));
72
50
  }
73
- constructor(context){
51
+ constructor(context, moduleName){
74
52
  _define_property(this, "context", void 0);
53
+ _define_property(this, "logger", void 0);
75
54
  this.context = context;
55
+ this.logger = moduleName ? createModuleLogger(moduleName) : winstonLogger;
76
56
  }
77
57
  }
78
- /**
79
- * NestJS Logger Adapter
80
- * Adapts NestJS Logger to ILogger interface
81
- *
82
- * Use this when you need to use NestJS's built-in logger
83
- * instead of Winston (e.g., for testing)
84
- */ export class NestLoggerAdapter {
58
+ export class NestLoggerAdapter {
85
59
  formatMessage(message, args) {
86
60
  return args?.length ? `${message} ${JSON.stringify(args)}` : message;
87
61
  }
@@ -16,9 +16,9 @@ import { existsSync, mkdirSync } from 'fs';
16
16
  import { join } from 'path';
17
17
  import { createLogger, format, transports } from 'winston';
18
18
  // Handle both webpack (no default export) and esbuild (default export)
19
+ import * as DailyRotateFileModule from 'winston-daily-rotate-file';
19
20
  import * as TransportModule from 'winston-transport';
20
21
  const Transport = TransportModule.default || TransportModule;
21
- import * as DailyRotateFileModule from 'winston-daily-rotate-file';
22
22
  const DailyRotateFile = DailyRotateFileModule.default || DailyRotateFileModule;
23
23
  // Configuration
24
24
  const logConfig = envConfig.getLogConfig();
@@ -35,33 +35,48 @@ if (!existsSync(LOG_DIR)) {
35
35
  }
36
36
  // Custom Formats
37
37
  /**
38
- * Custom format for structured logging
39
- * Includes: timestamp, level, context, requestId, userId, message, metadata
40
- */ const structuredFormat = format.printf(({ timestamp, level, message, context, requestId, userId, stack, ...metadata })=>{
41
- const logEntry = {
42
- timestamp,
43
- level: level.toUpperCase(),
44
- context: context || 'App',
45
- message
46
- };
47
- if (requestId) logEntry.requestId = requestId;
48
- if (userId) logEntry.userId = userId;
49
- if (Object.keys(metadata).length > 0) logEntry.metadata = metadata;
50
- if (stack) logEntry.stack = stack;
51
- return JSON.stringify(logEntry);
52
- });
53
- /**
54
- * Custom format for development console output
55
- * Human-readable format with colors
56
- */ const devConsoleFormat = format.printf(({ timestamp, level, message, context, requestId, userId, method, url, path, statusCode, duration, stack })=>{
57
- const ctx = context || 'App';
58
- const reqId = requestId ? ` [${requestId}]` : '';
59
- const user = userId ? ` (user:${userId})` : '';
60
- const endpoint = method && (path || url) ? ` [${method} ${path || url}]` : '';
61
- const status = statusCode ? ` [${statusCode}]` : '';
62
- const time = duration ? ` (${duration})` : '';
63
- const stackTrace = stack ? `\n${stack}` : '';
64
- return `${timestamp} [${level.toUpperCase().padEnd(7)}] [${ctx}]${endpoint}${status}${time}${reqId}${user} ${message}${stackTrace}`;
38
+ * Custom format for structured logging (production)
39
+ * Multi-line format:
40
+ * - Line 1: Timestamp
41
+ * - Line 2: Log Level
42
+ * - Line 3: [RequestId]
43
+ * - Line 4: Empty
44
+ * - Line 5+: Message with context
45
+ */ const LEVEL_MAP = {
46
+ error: 'Error',
47
+ warn: 'Warning',
48
+ info: 'Information',
49
+ debug: 'Debug',
50
+ verbose: 'Verbose'
51
+ };
52
+ const LOG_SEPARATOR = '─'.repeat(80);
53
+ const structuredFormat = format.printf(({ timestamp, level, message, context, requestId, userId, tenantId, companyId, method, url, path, statusCode, duration, stack, ...metadata })=>{
54
+ const reqId = requestId || 'no-request-id';
55
+ const levelFormatted = LEVEL_MAP[level] || level.charAt(0).toUpperCase() + level.slice(1);
56
+ const contextParts = [];
57
+ if (context) contextParts.push(`Context: ${context}`);
58
+ if (method && (path || url)) contextParts.push(`Endpoint: ${method} ${path || url}`);
59
+ if (statusCode) contextParts.push(`Status: ${statusCode}`);
60
+ if (duration) contextParts.push(`Duration: ${duration}`);
61
+ if (userId) contextParts.push(`UserId: ${userId}`);
62
+ if (tenantId) contextParts.push(`TenantId: ${tenantId}`);
63
+ if (companyId) contextParts.push(`CompanyId: ${companyId}`);
64
+ let content = message;
65
+ if (contextParts.length > 0) {
66
+ content = `${contextParts.join(' | ')}\n${message}`;
67
+ }
68
+ const metaKeys = Object.keys(metadata).filter((k)=>k !== 'splat');
69
+ if (metaKeys.length > 0) {
70
+ const metaStr = JSON.stringify(metaKeys.reduce((acc, k)=>({
71
+ ...acc,
72
+ [k]: metadata[k]
73
+ }), {}));
74
+ content += `\nMetadata: ${metaStr}`;
75
+ }
76
+ if (stack) {
77
+ content += `\nStack: ${stack}`;
78
+ }
79
+ return `${LOG_SEPARATOR}\n${timestamp} | ${levelFormatted} | [${reqId}]\n${content}`;
65
80
  });
66
81
  // Transports
67
82
  /**
@@ -84,63 +99,44 @@ if (!existsSync(LOG_DIR)) {
84
99
  maxFiles: LOG_MAX_FILES,
85
100
  level: 'error'
86
101
  });
87
- // Tenant-Aware Transport
88
- /**
89
- * Cache for tenant-specific transports
90
- * Avoids creating new transport instances for each log entry
91
- */ const tenantTransportCache = new Map();
92
- /**
93
- * Get or create a tenant-specific transport
94
- * Creates logs in: logs/{tenantId}/combined-YYYY-MM-DD.log
95
- */ function getTenantTransport(tenantId) {
96
- if (tenantTransportCache.has(tenantId)) {
97
- return tenantTransportCache.get(tenantId);
98
- }
99
- const tenantLogDir = join(LOG_DIR, tenantId);
100
- if (!existsSync(tenantLogDir)) {
101
- mkdirSync(tenantLogDir, {
102
+ // Transport Caches
103
+ const moduleTransportCache = new Map();
104
+ const tenantTransportCache = new Map();
105
+ function getCachedTransport(cache, name) {
106
+ const cached = cache.get(name);
107
+ if (cached) return cached;
108
+ const logDir = join(LOG_DIR, name);
109
+ if (!existsSync(logDir)) {
110
+ mkdirSync(logDir, {
102
111
  recursive: true
103
112
  });
104
113
  }
105
114
  const transport = new DailyRotateFile({
106
- filename: `${tenantLogDir}/combined-%DATE%.log`,
115
+ filename: `${logDir}/combined-%DATE%.log`,
107
116
  datePattern: 'YYYY-MM-DD',
108
117
  zippedArchive: true,
109
118
  maxSize: LOG_MAX_SIZE,
110
119
  maxFiles: LOG_MAX_FILES,
111
120
  level: LOG_LEVEL
112
121
  });
113
- tenantTransportCache.set(tenantId, transport);
122
+ cache.set(name, transport);
114
123
  return transport;
115
124
  }
116
- /**
117
- * Tenant-aware transport wrapper
118
- * Routes logs to tenant-specific folders when tenantId is present
119
- */ let TenantAwareTransport = class TenantAwareTransport extends Transport {
125
+ const getModuleTransport = (name)=>getCachedTransport(moduleTransportCache, name);
126
+ const getTenantTransport = (name)=>getCachedTransport(tenantTransportCache, name);
127
+ let TenantAwareTransport = class TenantAwareTransport extends Transport {
120
128
  log(info, callback) {
121
129
  setImmediate(()=>this.emit('logged', info));
122
130
  const tenantId = info.tenantId || info.metadata?.tenantId;
123
- if (USE_TENANT_MODE && tenantId) {
124
- // Write to tenant-specific folder
125
- const tenantTransport = getTenantTransport(tenantId);
126
- tenantTransport.log(info, callback);
127
- } else {
128
- // Write to default log folder
129
- this.defaultTransport.log(info, callback);
130
- }
131
+ const transport = USE_TENANT_MODE && tenantId ? getTenantTransport(tenantId) : this.defaultTransport;
132
+ transport.log(info, callback);
131
133
  }
132
134
  constructor(defaultTransport){
133
- super(), _define_property(this, "defaultTransport", void 0);
134
- this.defaultTransport = defaultTransport;
135
+ super(), _define_property(this, "defaultTransport", void 0), this.defaultTransport = defaultTransport;
135
136
  }
136
137
  };
137
- /**
138
- * Tenant-aware combined transport
139
- * When USE_TENANT_MODE=true, routes to tenant folders (like database mode)
140
- */ const tenantAwareCombinedTransport = new TenantAwareTransport(combinedRotateTransport);
141
- /**
142
- * Console transport for development
143
- */ const consoleTransport = new transports.Console({
138
+ const tenantAwareCombinedTransport = new TenantAwareTransport(combinedRotateTransport);
139
+ const consoleTransport = new transports.Console({
144
140
  level: LOG_LEVEL,
145
141
  format: format.combine(format.colorize({
146
142
  all: true
@@ -148,7 +144,7 @@ if (!existsSync(LOG_DIR)) {
148
144
  format: 'YYYY-MM-DD HH:mm:ss'
149
145
  }), format.errors({
150
146
  stack: true
151
- }), devConsoleFormat)
147
+ }), structuredFormat)
152
148
  });
153
149
  // Logger Instances
154
150
  /**
@@ -190,7 +186,43 @@ if (!existsSync(LOG_DIR)) {
190
186
  });
191
187
  // Exports
192
188
  /**
193
- * Winston logger instance
189
+ * Winston logger instance (global)
194
190
  * - DEV: Console output with colors
195
191
  * - PROD: File-based with daily rotation, tenant-aware routing
196
192
  */ export const instance = envConfig.isProduction() ? prodLogger : devLogger;
193
+ /**
194
+ * Module logger cache to avoid creating multiple loggers for the same module
195
+ */ const moduleLoggerCache = new Map();
196
+ /**
197
+ * Create a module-specific logger instance
198
+ * - DEV: Console output (same as global)
199
+ * - PROD: File-based with module-specific folder + shared error log
200
+ *
201
+ * @param moduleName - The module name (e.g., 'auth', 'iam', 'storage')
202
+ * @returns Logger instance for the module
203
+ */ export function createModuleLogger(moduleName) {
204
+ // In dev mode, use global console logger
205
+ if (!envConfig.isProduction()) {
206
+ return devLogger;
207
+ }
208
+ // Check cache first
209
+ if (moduleLoggerCache.has(moduleName)) {
210
+ return moduleLoggerCache.get(moduleName);
211
+ }
212
+ // Create module-specific logger for production
213
+ const moduleLogger = createLogger({
214
+ level: LOG_LEVEL,
215
+ format: format.combine(format.timestamp({
216
+ format: 'YYYY-MM-DD HH:mm:ss.SSS'
217
+ }), format.errors({
218
+ stack: true
219
+ }), structuredFormat),
220
+ transports: [
221
+ getModuleTransport(moduleName),
222
+ errorRotateTransport
223
+ ],
224
+ exitOnError: false
225
+ });
226
+ moduleLoggerCache.set(moduleName, moduleLogger);
227
+ return moduleLogger;
228
+ }
@@ -14,3 +14,5 @@ export const PERMISSIONS_CACHE_PREFIX = 'permissions';
14
14
  export const IDEMPOTENCY_CACHE_PREFIX = 'idempotency';
15
15
  // Permission codes
16
16
  export * from './permissions';
17
+ // Message keys for localization
18
+ export * from './message-keys';