@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.
- package/README.md +159 -79
- package/cjs/classes/api-controller.class.js +26 -8
- package/cjs/classes/api-service.class.js +100 -17
- package/cjs/classes/winston-logger-adapter.class.js +15 -20
- package/cjs/classes/winston.logger.class.js +103 -70
- package/cjs/constants/index.js +1 -0
- package/cjs/constants/message-keys.js +80 -0
- package/cjs/constants/permissions.js +32 -1
- package/cjs/decorators/index.js +1 -0
- package/cjs/decorators/log-action.decorator.js +149 -0
- package/cjs/dtos/response-payload.dto.js +72 -0
- package/cjs/exceptions/base-app.exception.js +145 -0
- package/cjs/exceptions/index.js +1 -0
- package/cjs/exceptions/permission.exception.js +12 -8
- package/cjs/filters/global-exception.filter.js +167 -0
- package/cjs/filters/index.js +18 -0
- package/cjs/guards/jwt-auth.guard.js +4 -1
- package/cjs/guards/permission.guard.js +6 -13
- package/cjs/index.js +1 -0
- package/cjs/interceptors/idempotency.interceptor.js +1 -1
- package/cjs/interceptors/index.js +0 -1
- package/cjs/interfaces/logger.interface.js +1 -4
- package/cjs/middlewares/logger.middleware.js +83 -26
- package/cjs/modules/datasource/multi-tenant-datasource.service.js +33 -11
- package/cjs/modules/utils/utils.service.js +4 -20
- package/cjs/utils/index.js +0 -1
- package/cjs/utils/query-helpers.util.js +8 -1
- package/classes/api-controller.class.d.ts +1 -0
- package/classes/api-service.class.d.ts +5 -10
- package/classes/winston-logger-adapter.class.d.ts +12 -11
- package/classes/winston.logger.class.d.ts +1 -0
- package/constants/index.d.ts +1 -0
- package/constants/message-keys.d.ts +81 -0
- package/constants/permissions.d.ts +36 -0
- package/decorators/index.d.ts +1 -0
- package/decorators/log-action.decorator.d.ts +8 -0
- package/dtos/response-payload.dto.d.ts +8 -0
- package/exceptions/base-app.exception.d.ts +41 -0
- package/exceptions/index.d.ts +1 -0
- package/exceptions/permission.exception.d.ts +1 -1
- package/fesm/classes/api-controller.class.js +26 -8
- package/fesm/classes/api-service.class.js +101 -18
- package/fesm/classes/winston-logger-adapter.class.js +18 -44
- package/fesm/classes/winston.logger.class.js +100 -68
- package/fesm/constants/index.js +2 -0
- package/fesm/constants/message-keys.js +59 -0
- package/fesm/constants/permissions.js +24 -1
- package/fesm/decorators/index.js +1 -0
- package/fesm/decorators/log-action.decorator.js +139 -0
- package/fesm/dtos/response-payload.dto.js +72 -0
- package/fesm/exceptions/base-app.exception.js +109 -0
- package/fesm/exceptions/index.js +1 -0
- package/fesm/exceptions/permission.exception.js +15 -17
- package/fesm/filters/global-exception.filter.js +157 -0
- package/fesm/filters/index.js +1 -0
- package/fesm/guards/jwt-auth.guard.js +5 -2
- package/fesm/guards/permission.guard.js +8 -15
- package/fesm/index.js +1 -0
- package/fesm/interceptors/idempotency.interceptor.js +2 -2
- package/fesm/interceptors/index.js +0 -1
- package/fesm/interfaces/logger.interface.js +1 -4
- package/fesm/middlewares/logger.middleware.js +83 -26
- package/fesm/modules/datasource/multi-tenant-datasource.service.js +34 -12
- package/fesm/modules/utils/utils.service.js +5 -21
- package/fesm/utils/index.js +0 -1
- package/fesm/utils/query-helpers.util.js +8 -1
- package/filters/global-exception.filter.d.ts +10 -0
- package/filters/index.d.ts +1 -0
- package/guards/permission.guard.d.ts +1 -3
- package/index.d.ts +1 -0
- package/interceptors/index.d.ts +0 -1
- package/interfaces/logger.interface.d.ts +5 -5
- package/modules/datasource/multi-tenant-datasource.service.d.ts +1 -2
- package/modules/utils/utils.service.d.ts +0 -1
- package/package.json +5 -3
- package/utils/index.d.ts +0 -1
- package/cjs/interceptors/query-performance.interceptor.js +0 -66
- package/cjs/utils/error-handler.util.js +0 -90
- package/fesm/interceptors/query-performance.interceptor.js +0 -56
- package/fesm/utils/error-handler.util.js +0 -82
- package/interceptors/query-performance.interceptor.d.ts +0 -8
- package/utils/error-handler.util.d.ts +0 -19
|
@@ -0,0 +1,157 @@
|
|
|
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 { ERROR_MESSAGES } from '../constants/message-keys';
|
|
22
|
+
import { BaseAppException } from '../exceptions/base-app.exception';
|
|
23
|
+
import { getRequestId, getUserId, getTenantId, getCompanyId } from '../middlewares/logger.middleware';
|
|
24
|
+
export class GlobalExceptionFilter {
|
|
25
|
+
catch(exception, host) {
|
|
26
|
+
const ctx = host.switchToHttp();
|
|
27
|
+
const request = ctx.getRequest();
|
|
28
|
+
const response = ctx.getResponse();
|
|
29
|
+
const startTime = request._startTime || Date.now();
|
|
30
|
+
const responseTime = Date.now() - startTime;
|
|
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);
|
|
37
|
+
const logContext = {
|
|
38
|
+
requestId,
|
|
39
|
+
userId,
|
|
40
|
+
tenantId,
|
|
41
|
+
companyId,
|
|
42
|
+
method: request.method,
|
|
43
|
+
url: request.originalUrl,
|
|
44
|
+
path: request.path,
|
|
45
|
+
statusCode,
|
|
46
|
+
duration: `${responseTime}ms`,
|
|
47
|
+
messageKey: errorResponse.messageKey
|
|
48
|
+
};
|
|
49
|
+
const logMessage = this.buildLogMessage(request, statusCode, responseTime, errorResponse);
|
|
50
|
+
if (statusCode >= 500) {
|
|
51
|
+
this.logger.error(logMessage, this.getStack(exception), logContext);
|
|
52
|
+
} else if (statusCode >= 400) {
|
|
53
|
+
this.logger.warn(logMessage, logContext);
|
|
54
|
+
}
|
|
55
|
+
response.status(statusCode).json(errorResponse);
|
|
56
|
+
}
|
|
57
|
+
buildErrorResponse(exception, requestId, responseTime) {
|
|
58
|
+
const timestamp = new Date().toISOString();
|
|
59
|
+
if (exception instanceof BaseAppException) {
|
|
60
|
+
return {
|
|
61
|
+
success: false,
|
|
62
|
+
message: exception.message,
|
|
63
|
+
messageKey: exception.messageKey,
|
|
64
|
+
messageParams: exception.messageParams,
|
|
65
|
+
errors: exception.errors,
|
|
66
|
+
_meta: {
|
|
67
|
+
requestId,
|
|
68
|
+
timestamp,
|
|
69
|
+
responseTime
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (exception instanceof HttpException) {
|
|
74
|
+
const response = exception.getResponse();
|
|
75
|
+
const { message, messageKey, messageParams, errors } = this.extractHttpExceptionDetails(response);
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
message,
|
|
79
|
+
messageKey,
|
|
80
|
+
messageParams,
|
|
81
|
+
errors,
|
|
82
|
+
_meta: {
|
|
83
|
+
requestId,
|
|
84
|
+
timestamp,
|
|
85
|
+
responseTime
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
success: false,
|
|
91
|
+
message: 'Internal server error',
|
|
92
|
+
messageKey: ERROR_MESSAGES.INTERNAL,
|
|
93
|
+
_meta: {
|
|
94
|
+
requestId,
|
|
95
|
+
timestamp,
|
|
96
|
+
responseTime
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
extractHttpExceptionDetails(response) {
|
|
101
|
+
if (typeof response === 'string') {
|
|
102
|
+
return {
|
|
103
|
+
message: response,
|
|
104
|
+
messageKey: ERROR_MESSAGES.HTTP
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (typeof response === 'object' && response !== null) {
|
|
108
|
+
const obj = response;
|
|
109
|
+
// Handle class-validator validation errors
|
|
110
|
+
if (Array.isArray(obj.message)) {
|
|
111
|
+
const errors = obj.message.map((msg)=>{
|
|
112
|
+
const [field, ...rest] = msg.split(' ');
|
|
113
|
+
return {
|
|
114
|
+
field: field || 'unknown',
|
|
115
|
+
message: rest.join(' ') || msg
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
message: 'Validation failed',
|
|
120
|
+
messageKey: ERROR_MESSAGES.VALIDATION,
|
|
121
|
+
errors
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
message: String(obj.message || 'Unknown error'),
|
|
126
|
+
messageKey: String(obj.messageKey || ERROR_MESSAGES.HTTP),
|
|
127
|
+
messageParams: obj.messageParams,
|
|
128
|
+
errors: obj.errors
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
message: 'Unknown error',
|
|
133
|
+
messageKey: ERROR_MESSAGES.UNKNOWN
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
getStatusCode(exception) {
|
|
137
|
+
if (exception instanceof HttpException) {
|
|
138
|
+
return exception.getStatus();
|
|
139
|
+
}
|
|
140
|
+
return HttpStatus.INTERNAL_SERVER_ERROR;
|
|
141
|
+
}
|
|
142
|
+
getStack(exception) {
|
|
143
|
+
if (exception instanceof Error) {
|
|
144
|
+
return exception.stack;
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
buildLogMessage(request, statusCode, responseTime, errorResponse) {
|
|
149
|
+
return `Exception caught "HTTP/1.1" "${request.method}" "${request.originalUrl}" - ${statusCode} "${errorResponse.messageKey}" ${responseTime}ms | ${errorResponse.message}`;
|
|
150
|
+
}
|
|
151
|
+
constructor(){
|
|
152
|
+
_define_property(this, "logger", new Logger('ExceptionFilter'));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
GlobalExceptionFilter = _ts_decorate([
|
|
156
|
+
Catch()
|
|
157
|
+
], 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(
|
|
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,
|
|
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 {
|
|
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) {
|
|
@@ -47,21 +45,21 @@ export class PermissionGuard {
|
|
|
47
45
|
if (!permissionConfig) return true;
|
|
48
46
|
const request = context.switchToHttp().getRequest();
|
|
49
47
|
const user = request.user;
|
|
50
|
-
if (!user) throw new UnauthorizedException(
|
|
48
|
+
if (!user) throw new UnauthorizedException({
|
|
49
|
+
message: 'Authentication required',
|
|
50
|
+
messageKey: AUTH_MESSAGES.TOKEN_REQUIRED
|
|
51
|
+
});
|
|
51
52
|
if (!this.cache) {
|
|
52
|
-
this.logger.error(`Cache not available (userId: ${user.id})`, undefined, 'PermissionGuard');
|
|
53
53
|
throw new PermissionSystemUnavailableException();
|
|
54
54
|
}
|
|
55
55
|
const userPermissions = await this.getUserPermissions(user);
|
|
56
56
|
if (!userPermissions || userPermissions.length === 0) {
|
|
57
|
-
this.logger.warn(`No permissions found (userId: ${user.id})`, 'PermissionGuard');
|
|
58
57
|
throw new NoPermissionsFoundException();
|
|
59
58
|
}
|
|
60
59
|
const logicNode = this.normalizeToLogicNode(permissionConfig);
|
|
61
60
|
if (!logicNode) return true;
|
|
62
61
|
const result = this.evaluateLogicNode(logicNode, userPermissions);
|
|
63
62
|
if (!result.passed) {
|
|
64
|
-
this.logger.warn(`Permission denied (userId: ${user.id})`, 'PermissionGuard');
|
|
65
63
|
throw new InsufficientPermissionsException(result.missingPermissions, result.operator);
|
|
66
64
|
}
|
|
67
65
|
return true;
|
|
@@ -168,11 +166,10 @@ export class PermissionGuard {
|
|
|
168
166
|
}
|
|
169
167
|
return false;
|
|
170
168
|
}
|
|
171
|
-
constructor(reflector, cache, config
|
|
169
|
+
constructor(reflector, cache, config){
|
|
172
170
|
_define_property(this, "reflector", void 0);
|
|
173
171
|
_define_property(this, "cache", void 0);
|
|
174
172
|
_define_property(this, "config", void 0);
|
|
175
|
-
_define_property(this, "logger", void 0);
|
|
176
173
|
this.reflector = reflector;
|
|
177
174
|
this.cache = cache;
|
|
178
175
|
this.config = {
|
|
@@ -181,7 +178,6 @@ export class PermissionGuard {
|
|
|
181
178
|
companyPermissionKeyFormat: `${PERMISSIONS_CACHE_PREFIX}:company:{companyId}:branch:{branchId}:user:{userId}`,
|
|
182
179
|
...config
|
|
183
180
|
};
|
|
184
|
-
this.logger = logger || new NestLoggerAdapter(new Logger(PermissionGuard.name));
|
|
185
181
|
}
|
|
186
182
|
}
|
|
187
183
|
PermissionGuard = _ts_decorate([
|
|
@@ -191,13 +187,10 @@ PermissionGuard = _ts_decorate([
|
|
|
191
187
|
_ts_param(1, Inject(CACHE_INSTANCE)),
|
|
192
188
|
_ts_param(2, Optional()),
|
|
193
189
|
_ts_param(2, Inject(PERMISSION_GUARD_CONFIG)),
|
|
194
|
-
_ts_param(3, Optional()),
|
|
195
|
-
_ts_param(3, Inject(LOGGER_INSTANCE)),
|
|
196
190
|
_ts_metadata("design:type", Function),
|
|
197
191
|
_ts_metadata("design:paramtypes", [
|
|
198
192
|
typeof Reflector === "undefined" ? Object : Reflector,
|
|
199
193
|
typeof HybridCache === "undefined" ? Object : HybridCache,
|
|
200
|
-
typeof PermissionGuardConfig === "undefined" ? Object : PermissionGuardConfig
|
|
201
|
-
typeof ILogger === "undefined" ? Object : ILogger
|
|
194
|
+
typeof PermissionGuardConfig === "undefined" ? Object : PermissionGuardConfig
|
|
202
195
|
])
|
|
203
196
|
], PermissionGuard);
|
package/fesm/index.js
CHANGED
|
@@ -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
|
-
|
|
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';
|
|
@@ -24,18 +24,15 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
24
24
|
import { instance as winstonLogger } from '../classes/winston.logger.class';
|
|
25
25
|
import { REQUEST_ID_HEADER } from '../constants';
|
|
26
26
|
// Configuration
|
|
27
|
-
const
|
|
27
|
+
const logConfig = envConfig.getLogConfig();
|
|
28
|
+
const IS_DEBUG = logConfig.level === 'debug';
|
|
29
|
+
const DISABLE_HTTP_LOGGING = logConfig.disableHttpLogging;
|
|
28
30
|
const TENANT_ID_HEADER = 'x-tenant-id';
|
|
29
31
|
const EXCLUDED_PATHS = [
|
|
30
32
|
'/health',
|
|
31
33
|
'/metrics',
|
|
32
34
|
'/favicon.ico'
|
|
33
35
|
];
|
|
34
|
-
const EXCLUDED_HEADERS = [
|
|
35
|
-
'authorization',
|
|
36
|
-
'cookie',
|
|
37
|
-
'x-api-key'
|
|
38
|
-
];
|
|
39
36
|
const MAX_BODY_LOG_SIZE = 1000;
|
|
40
37
|
export const requestContext = new AsyncLocalStorage();
|
|
41
38
|
// Context accessors
|
|
@@ -44,17 +41,71 @@ export const getTenantId = ()=>requestContext.getStore()?.tenantId;
|
|
|
44
41
|
export const getUserId = ()=>requestContext.getStore()?.userId;
|
|
45
42
|
export const getCompanyId = ()=>requestContext.getStore()?.companyId;
|
|
46
43
|
// Helper Functions
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
function summarizeRequestBody(body) {
|
|
45
|
+
if (!body) return undefined;
|
|
46
|
+
const parsed = typeof body === 'string' ? tryParseJson(body) : body;
|
|
47
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
48
|
+
const str = String(body);
|
|
49
|
+
return str.length > MAX_BODY_LOG_SIZE ? str.substring(0, MAX_BODY_LOG_SIZE) + '...' : str;
|
|
50
|
+
}
|
|
51
|
+
// For API request bodies, provide a concise summary
|
|
52
|
+
const summary = {};
|
|
53
|
+
// Pagination info
|
|
54
|
+
if (parsed.pagination) {
|
|
55
|
+
summary.page = parsed.pagination.currentPage;
|
|
56
|
+
summary.pageSize = parsed.pagination.pageSize;
|
|
57
|
+
}
|
|
58
|
+
// Filter keys only (not values - may contain sensitive data)
|
|
59
|
+
if (parsed.filter && typeof parsed.filter === 'object') {
|
|
60
|
+
const filterKeys = Object.keys(parsed.filter);
|
|
61
|
+
if (filterKeys.length > 0) summary.filterKeys = filterKeys;
|
|
62
|
+
}
|
|
63
|
+
// Select fields count
|
|
64
|
+
if (Array.isArray(parsed.select) && parsed.select.length > 0) {
|
|
65
|
+
summary.selectCount = parsed.select.length;
|
|
66
|
+
}
|
|
67
|
+
// Sort keys
|
|
68
|
+
if (parsed.sort && typeof parsed.sort === 'object') {
|
|
69
|
+
const sortKeys = Object.keys(parsed.sort);
|
|
70
|
+
if (sortKeys.length > 0) summary.sortKeys = sortKeys;
|
|
51
71
|
}
|
|
52
|
-
|
|
72
|
+
// ID for single item operations
|
|
73
|
+
if (parsed.id) summary.id = parsed.id;
|
|
74
|
+
// IDs for bulk operations
|
|
75
|
+
if (Array.isArray(parsed.ids)) summary.idsCount = parsed.ids.length;
|
|
76
|
+
return Object.keys(summary).length > 0 ? JSON.stringify(summary) : '{}';
|
|
53
77
|
}
|
|
54
|
-
function
|
|
78
|
+
function summarizeResponseBody(body) {
|
|
55
79
|
if (!body) return undefined;
|
|
56
|
-
const
|
|
57
|
-
|
|
80
|
+
const parsed = typeof body === 'string' ? tryParseJson(body) : body;
|
|
81
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
82
|
+
const str = String(body);
|
|
83
|
+
return str.length > MAX_BODY_LOG_SIZE ? str.substring(0, MAX_BODY_LOG_SIZE) + '...' : str;
|
|
84
|
+
}
|
|
85
|
+
const summary = {};
|
|
86
|
+
if ('success' in parsed) summary.success = parsed.success;
|
|
87
|
+
if ('message' in parsed) summary.message = parsed.message;
|
|
88
|
+
if ('code' in parsed) summary.code = parsed.code;
|
|
89
|
+
if (Array.isArray(parsed.data)) {
|
|
90
|
+
summary.dataCount = parsed.data.length;
|
|
91
|
+
} else if (parsed.data && typeof parsed.data === 'object') {
|
|
92
|
+
summary.hasData = true;
|
|
93
|
+
}
|
|
94
|
+
if (parsed.meta) {
|
|
95
|
+
summary.total = parsed.meta.total;
|
|
96
|
+
summary.page = parsed.meta.page;
|
|
97
|
+
}
|
|
98
|
+
if (parsed.errors) {
|
|
99
|
+
summary.errorsCount = Array.isArray(parsed.errors) ? parsed.errors.length : 1;
|
|
100
|
+
}
|
|
101
|
+
return JSON.stringify(summary);
|
|
102
|
+
}
|
|
103
|
+
function tryParseJson(str) {
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(str);
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
58
109
|
}
|
|
59
110
|
function getClientIp(req) {
|
|
60
111
|
return req.headers['x-forwarded-for']?.split(',')[0] || req.socket?.remoteAddress || 'unknown';
|
|
@@ -71,7 +122,7 @@ export class LoggerMiddleware {
|
|
|
71
122
|
startTime
|
|
72
123
|
};
|
|
73
124
|
requestContext.run(context, ()=>{
|
|
74
|
-
const shouldSkipLogging = EXCLUDED_PATHS.some((path)=>req.originalUrl.startsWith(path));
|
|
125
|
+
const shouldSkipLogging = DISABLE_HTTP_LOGGING || EXCLUDED_PATHS.some((path)=>req.originalUrl.startsWith(path));
|
|
75
126
|
if (!shouldSkipLogging) {
|
|
76
127
|
this.logRequest(req, requestId, tenantId);
|
|
77
128
|
this.setupResponseLogging(req, res, startTime, requestId, tenantId);
|
|
@@ -112,20 +163,22 @@ export class LoggerMiddleware {
|
|
|
112
163
|
});
|
|
113
164
|
}
|
|
114
165
|
logRequest(req, requestId, tenantId) {
|
|
166
|
+
const ip = getClientIp(req);
|
|
167
|
+
const contentType = req.headers['content-type'] || 'none';
|
|
168
|
+
// Build descriptive message like: Started processing request "POST" "/api/users" from "192.168.1.1"
|
|
169
|
+
const message = `Started processing request "${req.method}" "${req.originalUrl}" from "${ip}"`;
|
|
115
170
|
const logData = {
|
|
116
171
|
...this.buildBaseLogData(req, requestId, tenantId),
|
|
117
172
|
query: Object.keys(req.query).length > 0 ? req.query : undefined,
|
|
118
|
-
ip
|
|
173
|
+
ip,
|
|
119
174
|
userAgent: req.headers['user-agent'],
|
|
120
|
-
contentType
|
|
175
|
+
contentType,
|
|
121
176
|
contentLength: req.headers['content-length']
|
|
122
177
|
};
|
|
123
178
|
if (IS_DEBUG) {
|
|
124
|
-
logData.
|
|
125
|
-
logData.body = truncateBody(req.body);
|
|
126
|
-
logData.params = Object.keys(req.params).length > 0 ? req.params : undefined;
|
|
179
|
+
logData.body = summarizeRequestBody(req.body);
|
|
127
180
|
}
|
|
128
|
-
this.logger.info(
|
|
181
|
+
this.logger.info(message, logData);
|
|
129
182
|
}
|
|
130
183
|
logResponse(req, res, startTime, body, requestId, tenantId) {
|
|
131
184
|
const duration = Date.now() - startTime;
|
|
@@ -133,6 +186,10 @@ export class LoggerMiddleware {
|
|
|
133
186
|
const level = statusCode >= 500 ? 'error' : statusCode >= 400 ? 'warn' : 'info';
|
|
134
187
|
const userId = getUserId();
|
|
135
188
|
const companyId = getCompanyId();
|
|
189
|
+
const contentType = res.getHeader('content-type') || 'none';
|
|
190
|
+
const contentLength = res.getHeader('content-length') || 'null';
|
|
191
|
+
// Build descriptive message like: Request finished "HTTP/1.1" "POST" "/api/users" - 200 "application/json" 534.29ms
|
|
192
|
+
const message = `Request finished "HTTP/1.1" "${req.method}" "${req.originalUrl}" - ${statusCode} ${contentLength} "${contentType}" ${duration.toFixed(2)}ms`;
|
|
136
193
|
const logData = {
|
|
137
194
|
...this.buildBaseLogData(req, requestId, tenantId),
|
|
138
195
|
userId,
|
|
@@ -141,15 +198,15 @@ export class LoggerMiddleware {
|
|
|
141
198
|
statusMessage: res.statusMessage,
|
|
142
199
|
duration: `${duration}ms`,
|
|
143
200
|
durationMs: duration,
|
|
144
|
-
contentType
|
|
145
|
-
contentLength
|
|
201
|
+
contentType,
|
|
202
|
+
contentLength
|
|
146
203
|
};
|
|
147
204
|
if (statusCode >= 400 || IS_DEBUG) {
|
|
148
|
-
logData.responseBody =
|
|
205
|
+
logData.responseBody = summarizeResponseBody(body);
|
|
149
206
|
}
|
|
150
|
-
this.logger.log(level,
|
|
207
|
+
this.logger.log(level, message, logData);
|
|
151
208
|
if (duration > 3000 && statusCode < 400) {
|
|
152
|
-
this.logger.warn(
|
|
209
|
+
this.logger.warn(`Slow request detected: "${req.method}" "${req.originalUrl}" took ${duration}ms (threshold: 3000ms)`, {
|
|
153
210
|
...this.buildBaseLogData(req, requestId, tenantId),
|
|
154
211
|
userId,
|
|
155
212
|
companyId,
|
|
@@ -26,7 +26,8 @@ function _ts_param(paramIndex, decorator) {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
import { DEFAULT_TENANT_HEADER, IDataSourceServiceOptions, MODULE_OPTIONS } from '@flusys/nestjs-core';
|
|
29
|
-
import { BadRequestException, Inject, Injectable,
|
|
29
|
+
import { BadRequestException, Inject, Injectable, InternalServerErrorException, Optional, Scope } from '@nestjs/common';
|
|
30
|
+
import { SYSTEM_MESSAGES } from '../../constants';
|
|
30
31
|
import { REQUEST } from '@nestjs/core';
|
|
31
32
|
import { Request } from 'express';
|
|
32
33
|
import { DataSource } from 'typeorm';
|
|
@@ -58,7 +59,10 @@ export class MultiTenantDataSourceService {
|
|
|
58
59
|
const tenantId = this.request.headers[this.tenantHeader];
|
|
59
60
|
if (!tenantId) return null;
|
|
60
61
|
if (!/^[a-zA-Z0-9_-]+$/.test(tenantId)) {
|
|
61
|
-
throw new BadRequestException(
|
|
62
|
+
throw new BadRequestException({
|
|
63
|
+
message: 'Invalid tenant ID format',
|
|
64
|
+
messageKey: SYSTEM_MESSAGES.INVALID_TENANT_ID
|
|
65
|
+
});
|
|
62
66
|
}
|
|
63
67
|
return tenantId;
|
|
64
68
|
}
|
|
@@ -80,7 +84,15 @@ export class MultiTenantDataSourceService {
|
|
|
80
84
|
}
|
|
81
85
|
async getDataSourceForTenant(tenantId) {
|
|
82
86
|
const tenant = this.getTenant(tenantId);
|
|
83
|
-
if (!tenant)
|
|
87
|
+
if (!tenant) {
|
|
88
|
+
throw new BadRequestException({
|
|
89
|
+
message: `Tenant '${tenantId}' not found`,
|
|
90
|
+
messageKey: SYSTEM_MESSAGES.TENANT_NOT_FOUND,
|
|
91
|
+
messageParams: {
|
|
92
|
+
tenantId
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
84
96
|
return this.getOrCreateTenantConnection(tenant);
|
|
85
97
|
}
|
|
86
98
|
setDataSource(dataSource) {
|
|
@@ -100,8 +112,8 @@ export class MultiTenantDataSourceService {
|
|
|
100
112
|
try {
|
|
101
113
|
const dataSource = await this.getOrCreateTenantConnection(tenant);
|
|
102
114
|
results.set(tenant.id, await callback(tenant, dataSource));
|
|
103
|
-
} catch
|
|
104
|
-
|
|
115
|
+
} catch {
|
|
116
|
+
// Silent failure for individual tenant
|
|
105
117
|
}
|
|
106
118
|
}
|
|
107
119
|
return results;
|
|
@@ -118,7 +130,6 @@ export class MultiTenantDataSourceService {
|
|
|
118
130
|
if (connection?.isInitialized) {
|
|
119
131
|
await connection.destroy();
|
|
120
132
|
MultiTenantDataSourceService.tenantConnections.delete(tenantId);
|
|
121
|
-
this.logger.log(`Closed connection for tenant: ${tenantId}`);
|
|
122
133
|
}
|
|
123
134
|
}
|
|
124
135
|
async onModuleDestroy() {
|
|
@@ -148,7 +159,10 @@ export class MultiTenantDataSourceService {
|
|
|
148
159
|
}
|
|
149
160
|
const config = this.getDefaultDatabaseConfig();
|
|
150
161
|
if (!config) {
|
|
151
|
-
throw new
|
|
162
|
+
throw new InternalServerErrorException({
|
|
163
|
+
message: 'No database config available. Provide defaultDatabaseConfig or tenantDefaultDatabaseConfig.',
|
|
164
|
+
messageKey: SYSTEM_MESSAGES.DATABASE_CONFIG_NOT_AVAILABLE
|
|
165
|
+
});
|
|
152
166
|
}
|
|
153
167
|
// Create connection with lock to prevent race conditions
|
|
154
168
|
const connectionPromise = this.createDataSourceFromConfig(config);
|
|
@@ -166,7 +180,13 @@ export class MultiTenantDataSourceService {
|
|
|
166
180
|
*/ async getTenantDataSource() {
|
|
167
181
|
const tenant = this.getCurrentTenant();
|
|
168
182
|
if (!tenant) {
|
|
169
|
-
throw new
|
|
183
|
+
throw new BadRequestException({
|
|
184
|
+
message: `Tenant not found. Ensure '${this.tenantHeader}' header is set.`,
|
|
185
|
+
messageKey: SYSTEM_MESSAGES.TENANT_HEADER_REQUIRED,
|
|
186
|
+
messageParams: {
|
|
187
|
+
header: this.tenantHeader
|
|
188
|
+
}
|
|
189
|
+
});
|
|
170
190
|
}
|
|
171
191
|
return this.getOrCreateTenantConnection(tenant);
|
|
172
192
|
}
|
|
@@ -181,7 +201,6 @@ export class MultiTenantDataSourceService {
|
|
|
181
201
|
try {
|
|
182
202
|
const dataSource = await connectionPromise;
|
|
183
203
|
MultiTenantDataSourceService.tenantConnections.set(tenant.id, dataSource);
|
|
184
|
-
this.logger.log(`Created connection for tenant: ${tenant.id}`);
|
|
185
204
|
return dataSource;
|
|
186
205
|
} finally{
|
|
187
206
|
MultiTenantDataSourceService.connectionLocks.delete(tenant.id);
|
|
@@ -192,7 +211,12 @@ export class MultiTenantDataSourceService {
|
|
|
192
211
|
}
|
|
193
212
|
buildTenantDatabaseConfig(tenant) {
|
|
194
213
|
const defaultConfig = this.getDefaultDatabaseConfig();
|
|
195
|
-
if (!defaultConfig)
|
|
214
|
+
if (!defaultConfig) {
|
|
215
|
+
throw new InternalServerErrorException({
|
|
216
|
+
message: 'No default database config for multi-tenant mode.',
|
|
217
|
+
messageKey: SYSTEM_MESSAGES.DATABASE_CONFIG_NOT_AVAILABLE
|
|
218
|
+
});
|
|
219
|
+
}
|
|
196
220
|
return {
|
|
197
221
|
type: defaultConfig.type,
|
|
198
222
|
host: tenant.host ?? defaultConfig.host,
|
|
@@ -220,11 +244,9 @@ export class MultiTenantDataSourceService {
|
|
|
220
244
|
constructor(options, request){
|
|
221
245
|
_define_property(this, "options", void 0);
|
|
222
246
|
_define_property(this, "request", void 0);
|
|
223
|
-
_define_property(this, "logger", void 0);
|
|
224
247
|
_define_property(this, "tenantHeader", void 0);
|
|
225
248
|
this.options = options;
|
|
226
249
|
this.request = request;
|
|
227
|
-
this.logger = new Logger(this.constructor.name);
|
|
228
250
|
this.tenantHeader = DEFAULT_TENANT_HEADER;
|
|
229
251
|
this.initializeFromOptions();
|
|
230
252
|
}
|
|
@@ -1,23 +1,10 @@
|
|
|
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
1
|
function _ts_decorate(decorators, target, key, desc) {
|
|
15
2
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
16
3
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
17
4
|
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
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
19
6
|
}
|
|
20
|
-
import { Injectable
|
|
7
|
+
import { Injectable } from '@nestjs/common';
|
|
21
8
|
export class UtilsService {
|
|
22
9
|
// ---------------- CACHE HELPERS ----------------
|
|
23
10
|
/**
|
|
@@ -37,8 +24,8 @@ export class UtilsService {
|
|
|
37
24
|
const keys = await cacheManager.get(trackingKey) || [];
|
|
38
25
|
if (!keys.includes(cacheKey)) keys.push(cacheKey);
|
|
39
26
|
await cacheManager.set(trackingKey, keys);
|
|
40
|
-
} catch
|
|
41
|
-
|
|
27
|
+
} catch {
|
|
28
|
+
// Silent failure - cache tracking is non-critical
|
|
42
29
|
}
|
|
43
30
|
}
|
|
44
31
|
/**
|
|
@@ -49,8 +36,8 @@ export class UtilsService {
|
|
|
49
36
|
const keys = await cacheManager.get(trackingKey) || [];
|
|
50
37
|
await Promise.allSettled(keys.map((key)=>cacheManager.del(key)));
|
|
51
38
|
await cacheManager.del(trackingKey);
|
|
52
|
-
} catch
|
|
53
|
-
|
|
39
|
+
} catch {
|
|
40
|
+
// Silent failure - cache invalidation is non-critical
|
|
54
41
|
}
|
|
55
42
|
}
|
|
56
43
|
// ---------------- STRING HELPERS ----------------
|
|
@@ -73,9 +60,6 @@ export class UtilsService {
|
|
|
73
60
|
const prefix = this.buildTenantPrefix(tenantId);
|
|
74
61
|
return entityId ? `${prefix}entity_${entityName}_id_${entityId}_keys` : `${prefix}entity_${entityName}_keys`;
|
|
75
62
|
}
|
|
76
|
-
constructor(){
|
|
77
|
-
_define_property(this, "logger", new Logger(UtilsService.name));
|
|
78
|
-
}
|
|
79
63
|
}
|
|
80
64
|
UtilsService = _ts_decorate([
|
|
81
65
|
Injectable()
|
package/fesm/utils/index.js
CHANGED