@flusys/nestjs-shared 1.1.0 → 1.2.0-rc.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.
Files changed (158) hide show
  1. package/README.md +139 -1103
  2. package/cjs/classes/api-controller.class.js +320 -29
  3. package/cjs/classes/api-service.class.js +219 -64
  4. package/cjs/classes/handle-custome-db.js +37 -0
  5. package/cjs/classes/index.js +1 -2
  6. package/cjs/classes/winston.logger.class.js +103 -70
  7. package/cjs/constants/index.js +1 -4
  8. package/cjs/constants/message-keys.js +68 -0
  9. package/cjs/constants/permissions.js +114 -11
  10. package/cjs/decorators/api-response.decorator.js +1 -2
  11. package/cjs/decorators/index.js +1 -1
  12. package/cjs/decorators/log-action.decorator.js +149 -0
  13. package/cjs/decorators/require-permission.decorator.js +5 -5
  14. package/cjs/dtos/get-by-ids.dto.js +65 -0
  15. package/cjs/dtos/identity-response.dto.js +4 -8
  16. package/cjs/dtos/index.js +1 -0
  17. package/cjs/dtos/response-payload.dto.js +72 -0
  18. package/cjs/entities/identity.js +8 -2
  19. package/cjs/entities/index.js +1 -0
  20. package/cjs/entities/raw-type.js +4 -0
  21. package/cjs/entities/user-root.js +2 -1
  22. package/cjs/enums/index.js +20 -0
  23. package/cjs/enums/notification-type.enum.js +17 -0
  24. package/cjs/enums/participant-status.enum.js +17 -0
  25. package/cjs/enums/recurrence-type.enum.js +18 -0
  26. package/cjs/exceptions/base-app.exception.js +151 -0
  27. package/cjs/exceptions/index.js +1 -0
  28. package/cjs/exceptions/permission.exception.js +13 -9
  29. package/cjs/filters/global-exception.filter.js +279 -0
  30. package/cjs/filters/index.js +18 -0
  31. package/cjs/guards/jwt-auth.guard.js +4 -1
  32. package/cjs/guards/permission.guard.js +71 -77
  33. package/cjs/index.js +2 -0
  34. package/cjs/interceptors/idempotency.interceptor.js +1 -1
  35. package/cjs/interceptors/index.js +0 -1
  36. package/cjs/interfaces/event-manager-adapter.interface.js +11 -0
  37. package/cjs/interfaces/index.js +2 -1
  38. package/cjs/interfaces/notification-adapter.interface.js +11 -0
  39. package/cjs/interfaces/permission.interface.js +1 -1
  40. package/cjs/middlewares/logger.middleware.js +83 -26
  41. package/cjs/modules/datasource/index.js +0 -1
  42. package/cjs/modules/datasource/multi-tenant-datasource.service.js +46 -85
  43. package/cjs/modules/utils/utils.service.js +4 -20
  44. package/cjs/utils/date-time.util.js +94 -0
  45. package/cjs/utils/index.js +1 -1
  46. package/cjs/utils/query-helpers.util.js +8 -1
  47. package/cjs/utils/request.util.js +25 -0
  48. package/classes/api-controller.class.d.ts +14 -6
  49. package/classes/api-service.class.d.ts +25 -22
  50. package/classes/handle-custome-db.d.ts +3 -0
  51. package/classes/index.d.ts +1 -2
  52. package/classes/winston.logger.class.d.ts +1 -0
  53. package/constants/index.d.ts +1 -1
  54. package/constants/message-keys.d.ts +45 -0
  55. package/constants/permissions.d.ts +127 -0
  56. package/decorators/api-response.decorator.d.ts +1 -1
  57. package/decorators/current-user.decorator.d.ts +1 -1
  58. package/decorators/index.d.ts +1 -1
  59. package/decorators/log-action.decorator.d.ts +8 -0
  60. package/decorators/require-permission.decorator.d.ts +2 -2
  61. package/dtos/get-by-ids.dto.d.ts +4 -0
  62. package/dtos/index.d.ts +1 -0
  63. package/dtos/response-payload.dto.d.ts +8 -0
  64. package/entities/index.d.ts +1 -0
  65. package/entities/raw-type.d.ts +1 -0
  66. package/enums/index.d.ts +3 -0
  67. package/enums/notification-type.enum.d.ts +6 -0
  68. package/enums/participant-status.enum.d.ts +6 -0
  69. package/enums/recurrence-type.enum.d.ts +7 -0
  70. package/exceptions/base-app.exception.d.ts +41 -0
  71. package/exceptions/index.d.ts +1 -0
  72. package/exceptions/permission.exception.d.ts +2 -2
  73. package/fesm/classes/api-controller.class.js +321 -30
  74. package/fesm/classes/api-service.class.js +222 -67
  75. package/fesm/classes/handle-custome-db.js +19 -0
  76. package/fesm/classes/index.js +1 -2
  77. package/fesm/classes/winston.logger.class.js +100 -68
  78. package/fesm/constants/index.js +2 -1
  79. package/fesm/constants/message-keys.js +49 -0
  80. package/fesm/constants/permissions.js +86 -14
  81. package/fesm/decorators/api-response.decorator.js +2 -20
  82. package/fesm/decorators/index.js +1 -1
  83. package/fesm/decorators/log-action.decorator.js +139 -0
  84. package/fesm/decorators/require-permission.decorator.js +17 -65
  85. package/fesm/dtos/get-by-ids.dto.js +55 -0
  86. package/fesm/dtos/identity-response.dto.js +4 -8
  87. package/fesm/dtos/index.js +1 -0
  88. package/fesm/dtos/response-payload.dto.js +72 -0
  89. package/fesm/entities/identity.js +8 -2
  90. package/fesm/entities/index.js +1 -0
  91. package/fesm/entities/raw-type.js +1 -0
  92. package/fesm/entities/user-root.js +2 -1
  93. package/fesm/enums/index.js +3 -0
  94. package/fesm/enums/notification-type.enum.js +7 -0
  95. package/fesm/enums/participant-status.enum.js +7 -0
  96. package/fesm/enums/recurrence-type.enum.js +8 -0
  97. package/fesm/exceptions/base-app.exception.js +115 -0
  98. package/fesm/exceptions/index.js +1 -0
  99. package/fesm/exceptions/permission.exception.js +16 -18
  100. package/fesm/filters/global-exception.filter.js +269 -0
  101. package/fesm/filters/index.js +1 -0
  102. package/fesm/guards/jwt-auth.guard.js +5 -2
  103. package/fesm/guards/permission.guard.js +73 -79
  104. package/fesm/index.js +2 -0
  105. package/fesm/interceptors/idempotency.interceptor.js +2 -2
  106. package/fesm/interceptors/index.js +0 -1
  107. package/fesm/interfaces/datasource.interface.js +1 -20
  108. package/fesm/interfaces/event-manager-adapter.interface.js +1 -0
  109. package/fesm/interfaces/index.js +2 -1
  110. package/fesm/interfaces/notification-adapter.interface.js +1 -0
  111. package/fesm/interfaces/permission.interface.js +1 -3
  112. package/fesm/middlewares/logger.middleware.js +83 -26
  113. package/fesm/modules/datasource/index.js +0 -1
  114. package/fesm/modules/datasource/multi-tenant-datasource.service.js +47 -86
  115. package/fesm/modules/utils/utils.service.js +5 -21
  116. package/fesm/utils/date-time.util.js +70 -0
  117. package/fesm/utils/index.js +1 -1
  118. package/fesm/utils/query-helpers.util.js +8 -1
  119. package/fesm/utils/request.util.js +22 -0
  120. package/filters/global-exception.filter.d.ts +9 -0
  121. package/filters/index.d.ts +1 -0
  122. package/guards/permission.guard.d.ts +3 -7
  123. package/index.d.ts +2 -0
  124. package/interceptors/idempotency.interceptor.d.ts +1 -1
  125. package/interceptors/index.d.ts +0 -1
  126. package/interfaces/api.interface.d.ts +2 -0
  127. package/interfaces/event-manager-adapter.interface.d.ts +43 -0
  128. package/interfaces/index.d.ts +2 -1
  129. package/interfaces/notification-adapter.interface.d.ts +21 -0
  130. package/interfaces/permission.interface.d.ts +13 -8
  131. package/modules/datasource/index.d.ts +0 -1
  132. package/modules/datasource/multi-tenant-datasource.service.d.ts +5 -15
  133. package/modules/utils/utils.service.d.ts +0 -1
  134. package/package.json +21 -14
  135. package/utils/date-time.util.d.ts +8 -0
  136. package/utils/index.d.ts +1 -1
  137. package/utils/request.util.d.ts +2 -0
  138. package/cjs/classes/request-scoped-api.service.js +0 -61
  139. package/cjs/classes/winston-logger-adapter.class.js +0 -104
  140. package/cjs/decorators/sanitize-html.decorator.js +0 -36
  141. package/cjs/interceptors/query-performance.interceptor.js +0 -66
  142. package/cjs/interfaces/logger.interface.js +0 -7
  143. package/cjs/modules/datasource/datasource.module.js +0 -117
  144. package/cjs/utils/error-handler.util.js +0 -90
  145. package/classes/request-scoped-api.service.d.ts +0 -14
  146. package/classes/winston-logger-adapter.class.d.ts +0 -22
  147. package/decorators/sanitize-html.decorator.d.ts +0 -2
  148. package/fesm/classes/request-scoped-api.service.js +0 -51
  149. package/fesm/classes/winston-logger-adapter.class.js +0 -107
  150. package/fesm/decorators/sanitize-html.decorator.js +0 -45
  151. package/fesm/interceptors/query-performance.interceptor.js +0 -56
  152. package/fesm/interfaces/logger.interface.js +0 -4
  153. package/fesm/modules/datasource/datasource.module.js +0 -107
  154. package/fesm/utils/error-handler.util.js +0 -82
  155. package/interceptors/query-performance.interceptor.d.ts +0 -8
  156. package/interfaces/logger.interface.d.ts +0 -7
  157. package/modules/datasource/datasource.module.d.ts +0 -19
  158. package/utils/error-handler.util.d.ts +0 -19
@@ -0,0 +1,269 @@
1
+ function _define_property(obj, key, value) {
2
+ if (key in obj) {
3
+ Object.defineProperty(obj, key, {
4
+ value: value,
5
+ enumerable: true,
6
+ configurable: true,
7
+ writable: true
8
+ });
9
+ } else {
10
+ obj[key] = value;
11
+ }
12
+ return obj;
13
+ }
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
+ import { Catch, HttpException, HttpStatus, Logger } from '@nestjs/common';
21
+ import { QueryFailedError } from 'typeorm';
22
+ import { ERROR_MESSAGES } from '../constants/message-keys';
23
+ import { BaseAppException, ConflictException } from '../exceptions/base-app.exception';
24
+ import { getCompanyId, getRequestId, getTenantId, getUserId } from '../middlewares/logger.middleware';
25
+ export class GlobalExceptionFilter {
26
+ catch(exception, host) {
27
+ const ctx = host.switchToHttp();
28
+ const request = ctx.getRequest();
29
+ const response = ctx.getResponse();
30
+ const responseTime = Date.now() - (request._startTime || Date.now());
31
+ const requestId = getRequestId() || request.headers['x-request-id'] || 'no-request-id';
32
+ const { statusCode, errorResponse } = this.resolveException(exception, requestId, responseTime);
33
+ const logContext = {
34
+ requestId,
35
+ userId: getUserId(),
36
+ tenantId: getTenantId(),
37
+ companyId: getCompanyId(),
38
+ method: request.method,
39
+ url: request.originalUrl,
40
+ path: request.path,
41
+ statusCode,
42
+ duration: `${responseTime}ms`,
43
+ messageKey: errorResponse.messageKey
44
+ };
45
+ const logMessage = `Exception caught "HTTP/1.1" "${request.method}" "${request.originalUrl}" - ${statusCode} "${errorResponse.messageKey}" ${responseTime}ms | ${errorResponse.message}`;
46
+ if (statusCode >= 500) {
47
+ this.logger.error(logMessage, exception instanceof Error ? exception.stack : undefined, logContext);
48
+ } else if (statusCode >= 400) {
49
+ this.logger.warn(logMessage, logContext);
50
+ }
51
+ response.status(statusCode).json(errorResponse);
52
+ }
53
+ resolveException(exception, requestId, responseTime) {
54
+ const meta = {
55
+ requestId,
56
+ timestamp: new Date().toISOString(),
57
+ responseTime
58
+ };
59
+ if (exception instanceof BaseAppException) {
60
+ return {
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
69
+ }
70
+ };
71
+ }
72
+ if (exception instanceof HttpException) {
73
+ const resolved = this.resolveHttpException(exception);
74
+ return {
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
83
+ }
84
+ };
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
+ }
101
+ return {
102
+ statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
103
+ errorResponse: {
104
+ success: false,
105
+ message: 'Internal server error',
106
+ messageKey: ERROR_MESSAGES.INTERNAL,
107
+ _meta: meta
108
+ }
109
+ };
110
+ }
111
+ resolveHttpException(exception) {
112
+ const statusCode = exception.getStatus();
113
+ const response = exception.getResponse();
114
+ if (typeof response === 'string') {
115
+ return {
116
+ statusCode,
117
+ message: response,
118
+ messageKey: ERROR_MESSAGES.HTTP
119
+ };
120
+ }
121
+ if (typeof response === 'object' && response !== null) {
122
+ const obj = response;
123
+ if (Array.isArray(obj.message)) {
124
+ const errors = obj.message.map((msg)=>{
125
+ const [field, ...rest] = msg.split(' ');
126
+ return {
127
+ field: field || 'unknown',
128
+ message: rest.join(' ') || msg
129
+ };
130
+ });
131
+ return {
132
+ statusCode,
133
+ message: 'Validation failed',
134
+ messageKey: ERROR_MESSAGES.VALIDATION,
135
+ errors
136
+ };
137
+ }
138
+ return {
139
+ statusCode,
140
+ message: String(obj.message || 'Unknown error'),
141
+ messageKey: String(obj.messageKey || ERROR_MESSAGES.HTTP),
142
+ messageVariables: obj.messageVariables,
143
+ errors: obj.errors
144
+ };
145
+ }
146
+ return {
147
+ statusCode,
148
+ message: 'Unknown error',
149
+ messageKey: ERROR_MESSAGES.UNKNOWN
150
+ };
151
+ }
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 errno = dbError.driverError?.errno ?? dbError.errno;
157
+ const sqlMessage = dbError.driverError?.sqlMessage ?? '';
158
+ const detail = dbError.driverError?.detail ?? dbError.detail ?? '';
159
+ const table = dbError.driverError?.table ?? dbError.table ?? '';
160
+ const column = dbError.driverError?.column ?? dbError.column;
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);
170
+ }
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;
177
+ return new BaseAppException({
178
+ message: humanField ? `Invalid reference for '${humanField}'` : 'Invalid reference',
179
+ messageKey: ERROR_MESSAGES.DB_FOREIGN_KEY,
180
+ messageVariables: humanField ? {
181
+ field: humanField
182
+ } : undefined,
183
+ status: HttpStatus.BAD_REQUEST
184
+ });
185
+ }
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');
191
+ return new BaseAppException({
192
+ message: `Required field '${humanField}' cannot be null`,
193
+ messageKey: ERROR_MESSAGES.DB_NOT_NULL,
194
+ messageVariables: {
195
+ field: humanField
196
+ },
197
+ status: HttpStatus.BAD_REQUEST
198
+ });
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
+ }
258
+ return null;
259
+ }
260
+ toHumanReadable(dbName) {
261
+ return dbName.split(', ').map((part)=>part.split('_').map((word)=>word.toLowerCase() === 'id' ? 'ID' : word.charAt(0).toUpperCase() + word.slice(1)).join(' ')).join(', ');
262
+ }
263
+ constructor(){
264
+ _define_property(this, "logger", new Logger('ExceptionFilter'));
265
+ }
266
+ }
267
+ GlobalExceptionFilter = _ts_decorate([
268
+ Catch()
269
+ ], GlobalExceptionFilter);
@@ -0,0 +1 @@
1
+ export * from './global-exception.filter';
@@ -25,7 +25,7 @@ function _ts_param(paramIndex, decorator) {
25
25
  decorator(target, key, paramIndex);
26
26
  };
27
27
  }
28
- import { IS_PUBLIC_KEY } from '../constants';
28
+ import { AUTH_MESSAGES, IS_PUBLIC_KEY } from '../constants';
29
29
  import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
30
30
  import { Reflector } from '@nestjs/core';
31
31
  import { AuthGuard } from '@nestjs/passport';
@@ -43,7 +43,10 @@ export class JwtAuthGuard extends AuthGuard('jwt') {
43
43
  }
44
44
  handleRequest(err, user) {
45
45
  if (err || !user) {
46
- throw err || new UnauthorizedException('Invalid or expired token');
46
+ throw err || new UnauthorizedException({
47
+ message: 'Invalid or expired token',
48
+ messageKey: AUTH_MESSAGES.TOKEN_INVALID
49
+ });
47
50
  }
48
51
  return user;
49
52
  }
@@ -25,13 +25,11 @@ function _ts_param(paramIndex, decorator) {
25
25
  decorator(target, key, paramIndex);
26
26
  };
27
27
  }
28
- import { Inject, Injectable, Logger, Optional, UnauthorizedException } from '@nestjs/common';
28
+ import { Inject, Injectable, Optional, UnauthorizedException } from '@nestjs/common';
29
29
  import { Reflector } from '@nestjs/core';
30
30
  import { HybridCache } from '../classes/hybrid-cache.class';
31
- import { NestLoggerAdapter } from '../classes/winston-logger-adapter.class';
32
- import { CACHE_INSTANCE, IS_PUBLIC_KEY, LOGGER_INSTANCE, PERMISSION_GUARD_CONFIG, PERMISSIONS_CACHE_PREFIX, PERMISSIONS_KEY } from '../constants';
31
+ import { AUTH_MESSAGES, CACHE_INSTANCE, IS_PUBLIC_KEY, PERMISSION_GUARD_CONFIG, PERMISSIONS_CACHE_PREFIX, PERMISSIONS_KEY } from '../constants';
33
32
  import { InsufficientPermissionsException, NoPermissionsFoundException, PermissionSystemUnavailableException } from '../exceptions/permission.exception';
34
- import { ILogger } from '../interfaces/logger.interface';
35
33
  import { PermissionGuardConfig } from '../interfaces/permission.interface';
36
34
  export class PermissionGuard {
37
35
  async canActivate(context) {
@@ -45,104 +43,105 @@ export class PermissionGuard {
45
43
  context.getClass()
46
44
  ]);
47
45
  if (!permissionConfig) return true;
48
- const { permissions: requiredPermissions, operator } = this.normalizePermissionConfig(permissionConfig);
49
- if (!requiredPermissions || requiredPermissions.length === 0) return true;
50
46
  const request = context.switchToHttp().getRequest();
51
47
  const user = request.user;
52
- if (!user) throw new UnauthorizedException('Authentication required');
48
+ if (!user) throw new UnauthorizedException({
49
+ message: 'Authentication required',
50
+ messageKey: AUTH_MESSAGES.TOKEN_REQUIRED
51
+ });
53
52
  if (!this.cache) {
54
- this.logger.error(`Cache not available (userId: ${user.id})`, undefined, 'PermissionGuard');
55
53
  throw new PermissionSystemUnavailableException();
56
54
  }
57
55
  const userPermissions = await this.getUserPermissions(user);
58
56
  if (!userPermissions || userPermissions.length === 0) {
59
- this.logger.warn(`No permissions found (userId: ${user.id})`, 'PermissionGuard');
60
57
  throw new NoPermissionsFoundException();
61
58
  }
62
- if (this.isNestedCondition(permissionConfig)) {
63
- const result = this.evaluateCondition(permissionConfig, userPermissions);
64
- if (!result.passed) {
65
- this.logger.warn(`Permission denied (userId: ${user.id})`, 'PermissionGuard');
66
- throw new InsufficientPermissionsException(result.missingPermissions, result.operator);
67
- }
68
- } else {
69
- this.validateSimplePermissions(requiredPermissions, userPermissions, operator);
59
+ const logicNode = this.normalizeToLogicNode(permissionConfig);
60
+ if (!logicNode) return true;
61
+ const result = this.evaluateLogicNode(logicNode, userPermissions);
62
+ if (!result.passed) {
63
+ throw new InsufficientPermissionsException(result.missingPermissions, result.operator);
70
64
  }
71
65
  return true;
72
66
  }
73
- normalizePermissionConfig(config) {
74
- if (Array.isArray(config)) return {
75
- permissions: config,
76
- operator: 'and'
77
- };
78
- return {
79
- permissions: config.permissions || [],
80
- operator: config.operator || 'and'
81
- };
82
- }
83
- validateSimplePermissions(requiredPermissions, userPermissions, operator) {
84
- if (operator === 'or') {
85
- const hasAny = requiredPermissions.some((p)=>this.hasPermission(userPermissions, p));
86
- if (!hasAny) throw new InsufficientPermissionsException(requiredPermissions, 'or');
87
- } else {
88
- const hasAll = requiredPermissions.every((p)=>this.hasPermission(userPermissions, p));
89
- if (!hasAll) {
90
- const missing = requiredPermissions.filter((p)=>!this.hasPermission(userPermissions, p));
91
- throw new InsufficientPermissionsException(missing, 'and');
92
- }
67
+ normalizeToLogicNode(config) {
68
+ // string - single permission
69
+ if (typeof config === 'string') {
70
+ return config ? {
71
+ type: 'action',
72
+ actionId: config
73
+ } : null;
93
74
  }
75
+ // string[] - treat as AND of multiple permissions
76
+ if (Array.isArray(config)) {
77
+ if (config.length === 0) return null;
78
+ if (config.length === 1) return {
79
+ type: 'action',
80
+ actionId: config[0]
81
+ };
82
+ return {
83
+ type: 'group',
84
+ operator: 'AND',
85
+ children: config.map((p)=>({
86
+ type: 'action',
87
+ actionId: p
88
+ }))
89
+ };
90
+ }
91
+ // SimplePermissionConfig - { permissions: string[], operator: 'AND'|'OR' }
92
+ if ('permissions' in config && Array.isArray(config.permissions)) {
93
+ const simple = config;
94
+ if (simple.permissions.length === 0) return null;
95
+ if (simple.permissions.length === 1) return {
96
+ type: 'action',
97
+ actionId: simple.permissions[0]
98
+ };
99
+ return {
100
+ type: 'group',
101
+ operator: simple.operator || 'AND',
102
+ children: simple.permissions.map((p)=>({
103
+ type: 'action',
104
+ actionId: p
105
+ }))
106
+ };
107
+ }
108
+ // ILogicNode
109
+ return config;
94
110
  }
95
- isNestedCondition(config) {
96
- if (Array.isArray(config)) return false;
97
- return 'children' in config && Array.isArray(config.children) && config.children.length > 0;
98
- }
99
- evaluateCondition(condition, userPermissions) {
100
- const { permissions = [], operator, children = [] } = condition;
101
- // SECURITY: Fail-closed - deny access when no permissions configured (empty condition)
102
- if (permissions.length === 0 && children.length === 0) {
111
+ evaluateLogicNode(node, userPermissions) {
112
+ if (node.type === 'action') {
113
+ const passed = this.hasPermission(userPermissions, node.actionId);
114
+ return {
115
+ passed,
116
+ missingPermissions: passed ? [] : [
117
+ node.actionId
118
+ ],
119
+ operator: 'AND'
120
+ };
121
+ }
122
+ // Group node
123
+ const { operator, children } = node;
124
+ // SECURITY: Fail-closed - deny access when no children configured
125
+ if (!children || children.length === 0) {
103
126
  return {
104
127
  passed: false,
105
- message: 'No permissions configured - access denied by default',
106
128
  missingPermissions: [],
107
129
  operator
108
130
  };
109
131
  }
110
132
  const results = [];
111
- const failureDetails = [];
112
133
  const missingPermissions = [];
113
- if (permissions.length > 0) {
114
- if (operator === 'or') {
115
- const hasAny = permissions.some((p)=>this.hasPermission(userPermissions, p));
116
- results.push(hasAny);
117
- if (!hasAny) {
118
- failureDetails.push(`needs one of: [${permissions.join(', ')}]`);
119
- missingPermissions.push(...permissions);
120
- }
121
- } else {
122
- const hasAll = permissions.every((p)=>this.hasPermission(userPermissions, p));
123
- results.push(hasAll);
124
- if (!hasAll) {
125
- const missing = permissions.filter((p)=>!this.hasPermission(userPermissions, p));
126
- failureDetails.push(`missing: [${missing.join(', ')}]`);
127
- missingPermissions.push(...missing);
128
- }
129
- }
130
- }
131
134
  for (const child of children){
132
- const childResult = this.evaluateCondition(child, userPermissions);
135
+ const childResult = this.evaluateLogicNode(child, userPermissions);
133
136
  results.push(childResult.passed);
134
137
  if (!childResult.passed) {
135
- failureDetails.push(`(${childResult.message})`);
136
138
  missingPermissions.push(...childResult.missingPermissions);
137
139
  }
138
140
  }
139
- // Evaluate based on operator - empty results already handled above
140
- const passed = operator === 'or' ? results.some((r)=>r) : results.every((r)=>r);
141
- const message = passed ? 'OK' : `Denied: ${failureDetails.join(` ${operator.toUpperCase()} `)}`;
141
+ const passed = operator === 'OR' ? results.some((r)=>r) : results.every((r)=>r);
142
142
  return {
143
143
  passed,
144
- message,
145
- missingPermissions,
144
+ missingPermissions: passed ? [] : missingPermissions,
146
145
  operator
147
146
  };
148
147
  }
@@ -167,11 +166,10 @@ export class PermissionGuard {
167
166
  }
168
167
  return false;
169
168
  }
170
- constructor(reflector, cache, config, logger){
169
+ constructor(reflector, cache, config){
171
170
  _define_property(this, "reflector", void 0);
172
171
  _define_property(this, "cache", void 0);
173
172
  _define_property(this, "config", void 0);
174
- _define_property(this, "logger", void 0);
175
173
  this.reflector = reflector;
176
174
  this.cache = cache;
177
175
  this.config = {
@@ -180,7 +178,6 @@ export class PermissionGuard {
180
178
  companyPermissionKeyFormat: `${PERMISSIONS_CACHE_PREFIX}:company:{companyId}:branch:{branchId}:user:{userId}`,
181
179
  ...config
182
180
  };
183
- this.logger = logger || new NestLoggerAdapter(new Logger(PermissionGuard.name));
184
181
  }
185
182
  }
186
183
  PermissionGuard = _ts_decorate([
@@ -190,13 +187,10 @@ PermissionGuard = _ts_decorate([
190
187
  _ts_param(1, Inject(CACHE_INSTANCE)),
191
188
  _ts_param(2, Optional()),
192
189
  _ts_param(2, Inject(PERMISSION_GUARD_CONFIG)),
193
- _ts_param(3, Optional()),
194
- _ts_param(3, Inject(LOGGER_INSTANCE)),
195
190
  _ts_metadata("design:type", Function),
196
191
  _ts_metadata("design:paramtypes", [
197
192
  typeof Reflector === "undefined" ? Object : Reflector,
198
193
  typeof HybridCache === "undefined" ? Object : HybridCache,
199
- typeof PermissionGuardConfig === "undefined" ? Object : PermissionGuardConfig,
200
- typeof ILogger === "undefined" ? Object : ILogger
194
+ typeof PermissionGuardConfig === "undefined" ? Object : PermissionGuardConfig
201
195
  ])
202
196
  ], PermissionGuard);
package/fesm/index.js CHANGED
@@ -3,7 +3,9 @@ export * from './constants';
3
3
  export * from './decorators';
4
4
  export * from './dtos';
5
5
  export * from './entities';
6
+ export * from './enums';
6
7
  export * from './exceptions';
8
+ export * from './filters';
7
9
  export * from './guards';
8
10
  export * from './interceptors';
9
11
  export * from './interfaces';
@@ -29,7 +29,7 @@ import { ConflictException, Inject, Injectable, Optional } from '@nestjs/common'
29
29
  import { of } from 'rxjs';
30
30
  import { tap } from 'rxjs/operators';
31
31
  import { HybridCache } from '../classes/hybrid-cache.class';
32
- import { CACHE_INSTANCE, IDEMPOTENCY_CACHE_PREFIX, IDEMPOTENCY_KEY_HEADER } from '../constants';
32
+ import { CACHE_INSTANCE, IDEMPOTENCY_CACHE_PREFIX, IDEMPOTENCY_KEY_HEADER, SYSTEM_MESSAGES } from '../constants';
33
33
  export class IdempotencyInterceptor {
34
34
  async intercept(context, next) {
35
35
  // Skip if no cache available
@@ -51,7 +51,7 @@ export class IdempotencyInterceptor {
51
51
  throw new ConflictException({
52
52
  success: false,
53
53
  message: 'Request is already being processed',
54
- code: 'DUPLICATE_REQUEST_IN_PROGRESS'
54
+ messageKey: SYSTEM_MESSAGES.DUPLICATE_REQUEST
55
55
  });
56
56
  }
57
57
  // Return cached response
@@ -1,6 +1,5 @@
1
1
  export * from './delete-empty-id-from-body.interceptor';
2
2
  export * from './idempotency.interceptor';
3
- export * from './query-performance.interceptor';
4
3
  export * from './response-meta.interceptor';
5
4
  export * from './set-user-field-on-body.interceptor';
6
5
  export * from './slug.interceptor';
@@ -1,20 +1 @@
1
- /**
2
- * Interface for DataSource providers used by RequestScopedApiService.
3
- * Implement this interface in each module's DataSourceProvider to ensure type safety.
4
- *
5
- * @example
6
- * ```typescript
7
- * @Injectable()
8
- * export class IAMDataSourceProvider implements IDataSourceProvider {
9
- * constructor(private readonly dataSource: DataSource) {}
10
- *
11
- * async getRepository<T extends ObjectLiteral>(entity: EntityTarget<T>): Promise<Repository<T>> {
12
- * return this.dataSource.getRepository(entity);
13
- * }
14
- *
15
- * async getDataSource(): Promise<DataSource> {
16
- * return this.dataSource;
17
- * }
18
- * }
19
- * ```
20
- */ export { };
1
+ export { };
@@ -0,0 +1 @@
1
+ export const EVENT_MANAGER_ADAPTER = 'EVENT_MANAGER_ADAPTER';
@@ -1,7 +1,8 @@
1
1
  export * from './api.interface';
2
2
  export * from './datasource.interface';
3
+ export * from './event-manager-adapter.interface';
3
4
  export * from './identity.interface';
4
5
  export * from './logged-user-info.interface';
5
- export * from './logger.interface';
6
6
  export * from './module-config.interface';
7
+ export * from './notification-adapter.interface';
7
8
  export * from './permission.interface';
@@ -0,0 +1 @@
1
+ export const NOTIFICATION_ADAPTER = 'NOTIFICATION_ADAPTER';
@@ -1,3 +1 @@
1
- /**
2
- * Configuration for the permission guard
3
- */ export { };
1
+ /** Action node - checks a single permission/action code */ /** Configuration for the permission guard */ export { };