@flusys/nestjs-shared 1.0.0-rc → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +493 -658
- package/cjs/classes/api-service.class.js +59 -92
- package/cjs/classes/winston-logger-adapter.class.js +23 -40
- package/cjs/constants/permissions.js +11 -1
- package/cjs/dtos/delete.dto.js +10 -0
- package/cjs/dtos/response-payload.dto.js +0 -75
- package/cjs/guards/permission.guard.js +19 -18
- package/cjs/interceptors/index.js +0 -3
- package/cjs/interceptors/set-user-field-on-body.interceptor.js +20 -3
- package/cjs/middlewares/logger.middleware.js +50 -89
- package/cjs/modules/datasource/datasource.module.js +11 -14
- package/cjs/modules/datasource/multi-tenant-datasource.service.js +0 -4
- package/cjs/modules/utils/utils.service.js +22 -103
- package/cjs/utils/error-handler.util.js +12 -67
- package/cjs/utils/html-sanitizer.util.js +1 -11
- package/cjs/utils/index.js +2 -0
- package/cjs/utils/request.util.js +70 -0
- package/cjs/utils/string.util.js +63 -0
- package/classes/api-service.class.d.ts +2 -0
- package/classes/winston-logger-adapter.class.d.ts +2 -0
- package/constants/permissions.d.ts +12 -0
- package/dtos/delete.dto.d.ts +1 -0
- package/dtos/response-payload.dto.d.ts +0 -13
- package/fesm/classes/api-service.class.js +59 -92
- package/fesm/classes/winston-logger-adapter.class.js +23 -40
- package/fesm/constants/permissions.js +8 -1
- package/fesm/dtos/delete.dto.js +12 -2
- package/fesm/dtos/response-payload.dto.js +0 -69
- package/fesm/guards/permission.guard.js +19 -18
- package/fesm/interceptors/index.js +0 -3
- package/fesm/interceptors/set-user-field-on-body.interceptor.js +3 -0
- package/fesm/middlewares/logger.middleware.js +50 -83
- package/fesm/modules/datasource/datasource.module.js +11 -14
- package/fesm/modules/datasource/multi-tenant-datasource.service.js +0 -4
- package/fesm/modules/utils/utils.service.js +19 -89
- package/fesm/utils/error-handler.util.js +12 -68
- package/fesm/utils/html-sanitizer.util.js +1 -14
- package/fesm/utils/index.js +2 -0
- package/fesm/utils/request.util.js +58 -0
- package/fesm/utils/string.util.js +71 -0
- package/guards/permission.guard.d.ts +2 -0
- package/interceptors/index.d.ts +0 -3
- package/interceptors/set-user-field-on-body.interceptor.d.ts +3 -0
- package/interfaces/logged-user-info.interface.d.ts +0 -2
- package/middlewares/logger.middleware.d.ts +2 -2
- package/modules/datasource/datasource.module.d.ts +1 -0
- package/modules/datasource/multi-tenant-datasource.service.d.ts +0 -1
- package/modules/utils/utils.service.d.ts +2 -18
- package/package.json +2 -2
- package/utils/error-handler.util.d.ts +3 -18
- package/utils/html-sanitizer.util.d.ts +0 -1
- package/utils/index.d.ts +2 -0
- package/utils/request.util.d.ts +4 -0
- package/utils/string.util.d.ts +2 -0
- package/cjs/interceptors/set-create-by-on-body.interceptor.js +0 -12
- package/cjs/interceptors/set-delete-by-on-body.interceptor.js +0 -12
- package/cjs/interceptors/set-update-by-on-body.interceptor.js +0 -12
- package/fesm/interceptors/set-create-by-on-body.interceptor.js +0 -4
- package/fesm/interceptors/set-delete-by-on-body.interceptor.js +0 -4
- package/fesm/interceptors/set-update-by-on-body.interceptor.js +0 -4
- package/interceptors/set-create-by-on-body.interceptor.d.ts +0 -1
- package/interceptors/set-delete-by-on-body.interceptor.d.ts +0 -1
- package/interceptors/set-update-by-on-body.interceptor.d.ts +0 -1
|
@@ -26,12 +26,6 @@ _export(exports, {
|
|
|
26
26
|
},
|
|
27
27
|
get requestContext () {
|
|
28
28
|
return requestContext;
|
|
29
|
-
},
|
|
30
|
-
get setCompanyId () {
|
|
31
|
-
return setCompanyId;
|
|
32
|
-
},
|
|
33
|
-
get setUserId () {
|
|
34
|
-
return setUserId;
|
|
35
29
|
}
|
|
36
30
|
});
|
|
37
31
|
const _config = require("@flusys/nestjs-core/config");
|
|
@@ -78,14 +72,6 @@ const getRequestId = ()=>requestContext.getStore()?.requestId;
|
|
|
78
72
|
const getTenantId = ()=>requestContext.getStore()?.tenantId;
|
|
79
73
|
const getUserId = ()=>requestContext.getStore()?.userId;
|
|
80
74
|
const getCompanyId = ()=>requestContext.getStore()?.companyId;
|
|
81
|
-
const setUserId = (userId)=>{
|
|
82
|
-
const store = requestContext.getStore();
|
|
83
|
-
if (store) store.userId = userId;
|
|
84
|
-
};
|
|
85
|
-
const setCompanyId = (companyId)=>{
|
|
86
|
-
const store = requestContext.getStore();
|
|
87
|
-
if (store) store.companyId = companyId;
|
|
88
|
-
};
|
|
89
75
|
// Helper Functions
|
|
90
76
|
function sanitizeHeaders(headers) {
|
|
91
77
|
const sanitized = {};
|
|
@@ -107,84 +93,62 @@ let LoggerMiddleware = class LoggerMiddleware {
|
|
|
107
93
|
const requestId = req.headers[_constants.REQUEST_ID_HEADER] || (0, _uuid.v4)();
|
|
108
94
|
const tenantId = req.headers[TENANT_ID_HEADER];
|
|
109
95
|
const startTime = Date.now();
|
|
110
|
-
// Set response header
|
|
111
96
|
res.setHeader(_constants.REQUEST_ID_HEADER, requestId);
|
|
112
|
-
// Create context
|
|
113
97
|
const context = {
|
|
114
98
|
requestId,
|
|
115
99
|
tenantId,
|
|
116
100
|
startTime
|
|
117
101
|
};
|
|
118
|
-
// Run in AsyncLocalStorage context
|
|
119
102
|
requestContext.run(context, ()=>{
|
|
120
|
-
// Check if we should skip logging
|
|
121
103
|
const shouldSkipLogging = EXCLUDED_PATHS.some((path)=>req.originalUrl.startsWith(path));
|
|
122
104
|
if (!shouldSkipLogging) {
|
|
123
105
|
this.logRequest(req, requestId, tenantId);
|
|
124
|
-
|
|
125
|
-
let responseLogged = false;
|
|
126
|
-
// Capture response using multiple hooks to ensure we catch it
|
|
127
|
-
const originalSend = res.send;
|
|
128
|
-
const originalJson = res.json;
|
|
129
|
-
const originalEnd = res.end;
|
|
130
|
-
// Store reference to this for use in callbacks
|
|
131
|
-
const self = this;
|
|
132
|
-
// Override res.send
|
|
133
|
-
res.send = function(body) {
|
|
134
|
-
if (!responseLogged) {
|
|
135
|
-
responseLogged = true;
|
|
136
|
-
self.logResponse(req, res, startTime, body, requestId, tenantId);
|
|
137
|
-
}
|
|
138
|
-
return originalSend.call(this, body);
|
|
139
|
-
};
|
|
140
|
-
// Override res.json
|
|
141
|
-
res.json = function(body) {
|
|
142
|
-
if (!responseLogged) {
|
|
143
|
-
responseLogged = true;
|
|
144
|
-
self.logResponse(req, res, startTime, body, requestId, tenantId);
|
|
145
|
-
}
|
|
146
|
-
return originalJson.call(this, body);
|
|
147
|
-
};
|
|
148
|
-
// Override res.end as fallback
|
|
149
|
-
res.end = function(...args) {
|
|
150
|
-
if (!responseLogged) {
|
|
151
|
-
responseLogged = true;
|
|
152
|
-
self.logResponse(req, res, startTime, args[0], requestId, tenantId);
|
|
153
|
-
}
|
|
154
|
-
return originalEnd.apply(this, args);
|
|
155
|
-
};
|
|
156
|
-
// Handle errors
|
|
157
|
-
res.on('error', (error)=>{
|
|
158
|
-
this.logger.error('Response error', {
|
|
159
|
-
context: 'HTTP',
|
|
160
|
-
requestId,
|
|
161
|
-
tenantId,
|
|
162
|
-
method: req.method,
|
|
163
|
-
url: req.originalUrl,
|
|
164
|
-
path: req.path,
|
|
165
|
-
error: error.message,
|
|
166
|
-
stack: error.stack
|
|
167
|
-
});
|
|
168
|
-
});
|
|
106
|
+
this.setupResponseLogging(req, res, startTime, requestId, tenantId);
|
|
169
107
|
}
|
|
170
108
|
next();
|
|
171
109
|
});
|
|
172
110
|
}
|
|
111
|
+
setupResponseLogging(req, res, startTime, requestId, tenantId) {
|
|
112
|
+
let responseLogged = false;
|
|
113
|
+
const originalSend = res.send;
|
|
114
|
+
const originalJson = res.json;
|
|
115
|
+
const originalEnd = res.end;
|
|
116
|
+
const self = this;
|
|
117
|
+
const logOnce = (body)=>{
|
|
118
|
+
if (!responseLogged) {
|
|
119
|
+
responseLogged = true;
|
|
120
|
+
self.logResponse(req, res, startTime, body, requestId, tenantId);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
res.send = function(body) {
|
|
124
|
+
logOnce(body);
|
|
125
|
+
return originalSend.call(this, body);
|
|
126
|
+
};
|
|
127
|
+
res.json = function(body) {
|
|
128
|
+
logOnce(body);
|
|
129
|
+
return originalJson.call(this, body);
|
|
130
|
+
};
|
|
131
|
+
res.end = function(...args) {
|
|
132
|
+
logOnce(args[0]);
|
|
133
|
+
return originalEnd.apply(this, args);
|
|
134
|
+
};
|
|
135
|
+
res.on('error', (error)=>{
|
|
136
|
+
this.logger.error('Response error', {
|
|
137
|
+
...this.buildBaseLogData(req, requestId, tenantId),
|
|
138
|
+
error: error.message,
|
|
139
|
+
stack: error.stack
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
173
143
|
logRequest(req, requestId, tenantId) {
|
|
174
144
|
const logData = {
|
|
175
|
-
|
|
176
|
-
requestId,
|
|
177
|
-
tenantId,
|
|
178
|
-
method: req.method,
|
|
179
|
-
url: req.originalUrl,
|
|
180
|
-
path: req.path,
|
|
145
|
+
...this.buildBaseLogData(req, requestId, tenantId),
|
|
181
146
|
query: Object.keys(req.query).length > 0 ? req.query : undefined,
|
|
182
147
|
ip: getClientIp(req),
|
|
183
148
|
userAgent: req.headers['user-agent'],
|
|
184
149
|
contentType: req.headers['content-type'],
|
|
185
150
|
contentLength: req.headers['content-length']
|
|
186
151
|
};
|
|
187
|
-
// Add debug details if enabled
|
|
188
152
|
if (IS_DEBUG) {
|
|
189
153
|
logData.headers = sanitizeHeaders(req.headers);
|
|
190
154
|
logData.body = truncateBody(req.body);
|
|
@@ -196,13 +160,12 @@ let LoggerMiddleware = class LoggerMiddleware {
|
|
|
196
160
|
const duration = Date.now() - startTime;
|
|
197
161
|
const statusCode = res.statusCode;
|
|
198
162
|
const level = statusCode >= 500 ? 'error' : statusCode >= 400 ? 'warn' : 'info';
|
|
163
|
+
const userId = getUserId();
|
|
164
|
+
const companyId = getCompanyId();
|
|
199
165
|
const logData = {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
method: req.method,
|
|
204
|
-
url: req.originalUrl,
|
|
205
|
-
path: req.path,
|
|
166
|
+
...this.buildBaseLogData(req, requestId, tenantId),
|
|
167
|
+
userId,
|
|
168
|
+
companyId,
|
|
206
169
|
statusCode,
|
|
207
170
|
statusMessage: res.statusMessage,
|
|
208
171
|
duration: `${duration}ms`,
|
|
@@ -210,33 +173,31 @@ let LoggerMiddleware = class LoggerMiddleware {
|
|
|
210
173
|
contentType: res.getHeader('content-type'),
|
|
211
174
|
contentLength: res.getHeader('content-length')
|
|
212
175
|
};
|
|
213
|
-
// Add user context if available
|
|
214
|
-
const userId = getUserId();
|
|
215
|
-
const companyId = getCompanyId();
|
|
216
|
-
if (userId) logData.userId = userId;
|
|
217
|
-
if (companyId) logData.companyId = companyId;
|
|
218
|
-
// Add response body for errors or debug mode
|
|
219
176
|
if (statusCode >= 400 || IS_DEBUG) {
|
|
220
177
|
logData.responseBody = truncateBody(body);
|
|
221
178
|
}
|
|
222
179
|
this.logger.log(level, `Response [${statusCode}]`, logData);
|
|
223
|
-
// Log slow requests separately
|
|
224
180
|
if (duration > 3000 && statusCode < 400) {
|
|
225
181
|
this.logger.warn('Slow request detected', {
|
|
226
|
-
|
|
227
|
-
requestId,
|
|
228
|
-
tenantId,
|
|
182
|
+
...this.buildBaseLogData(req, requestId, tenantId),
|
|
229
183
|
userId,
|
|
230
184
|
companyId,
|
|
231
|
-
method: req.method,
|
|
232
|
-
url: req.originalUrl,
|
|
233
|
-
path: req.path,
|
|
234
185
|
duration: `${duration}ms`,
|
|
235
186
|
durationMs: duration,
|
|
236
187
|
threshold: '3000ms'
|
|
237
188
|
});
|
|
238
189
|
}
|
|
239
190
|
}
|
|
191
|
+
buildBaseLogData(req, requestId, tenantId) {
|
|
192
|
+
return {
|
|
193
|
+
context: 'HTTP',
|
|
194
|
+
requestId,
|
|
195
|
+
tenantId,
|
|
196
|
+
method: req.method,
|
|
197
|
+
url: req.originalUrl,
|
|
198
|
+
path: req.path
|
|
199
|
+
};
|
|
200
|
+
}
|
|
240
201
|
constructor(){
|
|
241
202
|
_define_property(this, "logger", _winstonloggerclass.instance);
|
|
242
203
|
}
|
|
@@ -87,24 +87,12 @@ let DataSourceModule = class DataSourceModule {
|
|
|
87
87
|
provide: options.useClass,
|
|
88
88
|
useClass: options.useClass
|
|
89
89
|
},
|
|
90
|
-
|
|
91
|
-
provide: _nestjscore.MODULE_OPTIONS,
|
|
92
|
-
useFactory: async (factory)=>factory.createOptions(),
|
|
93
|
-
inject: [
|
|
94
|
-
options.useClass
|
|
95
|
-
]
|
|
96
|
-
}
|
|
90
|
+
this.createFactoryProvider(options.useClass)
|
|
97
91
|
];
|
|
98
92
|
}
|
|
99
93
|
if (options.useExisting) {
|
|
100
94
|
return [
|
|
101
|
-
|
|
102
|
-
provide: _nestjscore.MODULE_OPTIONS,
|
|
103
|
-
useFactory: async (factory)=>factory.createOptions(),
|
|
104
|
-
inject: [
|
|
105
|
-
options.useExisting
|
|
106
|
-
]
|
|
107
|
-
}
|
|
95
|
+
this.createFactoryProvider(options.useExisting)
|
|
108
96
|
];
|
|
109
97
|
}
|
|
110
98
|
return [
|
|
@@ -114,6 +102,15 @@ let DataSourceModule = class DataSourceModule {
|
|
|
114
102
|
}
|
|
115
103
|
];
|
|
116
104
|
}
|
|
105
|
+
static createFactoryProvider(factoryClass) {
|
|
106
|
+
return {
|
|
107
|
+
provide: _nestjscore.MODULE_OPTIONS,
|
|
108
|
+
useFactory: async (factory)=>factory.createOptions(),
|
|
109
|
+
inject: [
|
|
110
|
+
factoryClass
|
|
111
|
+
]
|
|
112
|
+
};
|
|
113
|
+
}
|
|
117
114
|
};
|
|
118
115
|
DataSourceModule = _ts_decorate([
|
|
119
116
|
(0, _common.Module)({})
|
|
@@ -100,10 +100,6 @@ let MultiTenantDataSourceService = class MultiTenantDataSourceService {
|
|
|
100
100
|
const dataSource = await this.getDataSource();
|
|
101
101
|
return dataSource.getRepository(entity);
|
|
102
102
|
}
|
|
103
|
-
async getRepositoryForTenant(entity, tenantId) {
|
|
104
|
-
const dataSource = await this.getDataSourceForTenant(tenantId);
|
|
105
|
-
return dataSource.getRepository(entity);
|
|
106
|
-
}
|
|
107
103
|
async withTenant(tenantId, callback) {
|
|
108
104
|
const dataSource = await this.getDataSourceForTenant(tenantId);
|
|
109
105
|
return callback(dataSource);
|
|
@@ -2,25 +2,13 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", {
|
|
3
3
|
value: true
|
|
4
4
|
});
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
get: Object.getOwnPropertyDescriptor(all, name).get
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
|
-
_export(exports, {
|
|
12
|
-
get DEFAULT_PHONE_COUNTRY_CODE () {
|
|
13
|
-
return DEFAULT_PHONE_COUNTRY_CODE;
|
|
14
|
-
},
|
|
15
|
-
get DEFAULT_PHONE_REGEX () {
|
|
16
|
-
return DEFAULT_PHONE_REGEX;
|
|
17
|
-
},
|
|
18
|
-
get UtilsService () {
|
|
5
|
+
Object.defineProperty(exports, "UtilsService", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
19
8
|
return UtilsService;
|
|
20
9
|
}
|
|
21
10
|
});
|
|
22
11
|
const _common = require("@nestjs/common");
|
|
23
|
-
const _classvalidator = require("class-validator");
|
|
24
12
|
function _define_property(obj, key, value) {
|
|
25
13
|
if (key in obj) {
|
|
26
14
|
Object.defineProperty(obj, key, {
|
|
@@ -40,38 +28,25 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
40
28
|
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;
|
|
41
29
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
42
30
|
}
|
|
43
|
-
const DEFAULT_PHONE_REGEX = /^((\+880)|0)?(13|15|16|17|18|19)\d{8}$/;
|
|
44
|
-
const DEFAULT_PHONE_COUNTRY_CODE = '+88';
|
|
45
31
|
let UtilsService = class UtilsService {
|
|
46
|
-
/**
|
|
47
|
-
* Configure phone validation regex and country code
|
|
48
|
-
*/ setPhoneValidationConfig(config) {
|
|
49
|
-
this.phoneConfig = config;
|
|
50
|
-
}
|
|
51
32
|
// ---------------- CACHE HELPERS ----------------
|
|
52
33
|
/**
|
|
53
34
|
* Generate cache key with optional tenant prefix for multi-tenant isolation
|
|
54
35
|
*/ getCacheKey(entityName, params, entityId, tenantId) {
|
|
55
|
-
const
|
|
56
|
-
if (entityId)
|
|
57
|
-
|
|
36
|
+
const prefix = this.buildTenantPrefix(tenantId);
|
|
37
|
+
if (entityId) {
|
|
38
|
+
return `${prefix}entity_${entityName}_id_${entityId}${params ? '_select_' + JSON.stringify(params) : ''}`;
|
|
39
|
+
}
|
|
40
|
+
return `${prefix}entity_${entityName}_all_${JSON.stringify(params)}`;
|
|
58
41
|
}
|
|
59
42
|
/**
|
|
60
43
|
* Track cache key for later invalidation with optional tenant prefix
|
|
61
44
|
*/ async trackCacheKey(cacheKey, entityName, cacheManager, entityId, tenantId) {
|
|
62
|
-
const tenantPrefix = tenantId ? `tenant_${tenantId}_` : '';
|
|
63
45
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
await cacheManager.set(trackingKey, idKeys);
|
|
69
|
-
} else {
|
|
70
|
-
const trackingKey = `${tenantPrefix}entity_${entityName}_keys`;
|
|
71
|
-
const allKeys = await cacheManager.get(trackingKey) || [];
|
|
72
|
-
if (!allKeys.includes(cacheKey)) allKeys.push(cacheKey);
|
|
73
|
-
await cacheManager.set(trackingKey, allKeys);
|
|
74
|
-
}
|
|
46
|
+
const trackingKey = this.buildTrackingKey(entityName, entityId, tenantId);
|
|
47
|
+
const keys = await cacheManager.get(trackingKey) || [];
|
|
48
|
+
if (!keys.includes(cacheKey)) keys.push(cacheKey);
|
|
49
|
+
await cacheManager.set(trackingKey, keys);
|
|
75
50
|
} catch (error) {
|
|
76
51
|
this.logger.error(`Cache tracking failed for ${entityName}`, error instanceof Error ? error.stack : String(error));
|
|
77
52
|
}
|
|
@@ -79,49 +54,15 @@ let UtilsService = class UtilsService {
|
|
|
79
54
|
/**
|
|
80
55
|
* Clear cache for entity with optional tenant prefix
|
|
81
56
|
*/ async clearCache(entityName, cacheManager, entityId, tenantId) {
|
|
82
|
-
const tenantPrefix = tenantId ? `tenant_${tenantId}_` : '';
|
|
83
57
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
await cacheManager.del(trackingKey);
|
|
89
|
-
} else {
|
|
90
|
-
const trackingKey = `${tenantPrefix}entity_${entityName}_keys`;
|
|
91
|
-
const keySet = await cacheManager.get(trackingKey) || [];
|
|
92
|
-
await Promise.allSettled(keySet.map((key)=>cacheManager.del(key)));
|
|
93
|
-
await cacheManager.del(trackingKey);
|
|
94
|
-
}
|
|
58
|
+
const trackingKey = this.buildTrackingKey(entityName, entityId, tenantId);
|
|
59
|
+
const keys = await cacheManager.get(trackingKey) || [];
|
|
60
|
+
await Promise.allSettled(keys.map((key)=>cacheManager.del(key)));
|
|
61
|
+
await cacheManager.del(trackingKey);
|
|
95
62
|
} catch (error) {
|
|
96
63
|
this.logger.error(`Cache invalidation failed for ${entityName}`, error instanceof Error ? error.stack : String(error));
|
|
97
64
|
}
|
|
98
65
|
}
|
|
99
|
-
// ---------------- VALIDATION HELPERS ----------------
|
|
100
|
-
/**
|
|
101
|
-
* Check if value is phone or email and normalize phone number
|
|
102
|
-
*/ checkPhoneOrEmail(value) {
|
|
103
|
-
if ((0, _classvalidator.isEmail)(value)) {
|
|
104
|
-
return {
|
|
105
|
-
value,
|
|
106
|
-
type: 'email'
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
const phoneMatch = value.match(this.phoneConfig.regex);
|
|
110
|
-
if (phoneMatch) {
|
|
111
|
-
let phone = phoneMatch[0];
|
|
112
|
-
if (!phone.startsWith(this.phoneConfig.countryCode)) {
|
|
113
|
-
phone = this.phoneConfig.countryCode + phone;
|
|
114
|
-
}
|
|
115
|
-
return {
|
|
116
|
-
value: phone,
|
|
117
|
-
type: 'phone'
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
return {
|
|
121
|
-
value: null,
|
|
122
|
-
type: null
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
66
|
// ---------------- STRING HELPERS ----------------
|
|
126
67
|
/**
|
|
127
68
|
* Transform string to URL-friendly slug
|
|
@@ -129,43 +70,21 @@ let UtilsService = class UtilsService {
|
|
|
129
70
|
const slug = value?.trim().replace(/[^A-Z0-9]+/gi, '-').toLowerCase();
|
|
130
71
|
return salt ? `${slug}-${this.getRandomInt(1, 100)}` : slug;
|
|
131
72
|
}
|
|
132
|
-
// ---------------- RANDOM HELPERS ----------------
|
|
133
73
|
/**
|
|
134
74
|
* Generate random integer between min and max (inclusive)
|
|
135
75
|
*/ getRandomInt(min, max) {
|
|
136
76
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
137
77
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
142
|
-
let result = '';
|
|
143
|
-
for(let i = 0; i < length; i++){
|
|
144
|
-
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
|
145
|
-
}
|
|
146
|
-
return result;
|
|
78
|
+
// ---------------- PRIVATE HELPERS ----------------
|
|
79
|
+
buildTenantPrefix(tenantId) {
|
|
80
|
+
return tenantId ? `tenant_${tenantId}_` : '';
|
|
147
81
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return Math.floor(Math.random() * (9999 - 1000 + 1)) + 1000;
|
|
152
|
-
}
|
|
153
|
-
// ---------------- ERROR HELPERS ----------------
|
|
154
|
-
/**
|
|
155
|
-
* Extract column name and value from PostgreSQL error detail
|
|
156
|
-
*/ extractColumnNameFromError(detail) {
|
|
157
|
-
const match = detail.match(/\((.*?)\)=\((.*?)\)/);
|
|
158
|
-
return {
|
|
159
|
-
columnName: match ? match[1] : 'unknown',
|
|
160
|
-
value: match ? match[2] : 'unknown'
|
|
161
|
-
};
|
|
82
|
+
buildTrackingKey(entityName, entityId, tenantId) {
|
|
83
|
+
const prefix = this.buildTenantPrefix(tenantId);
|
|
84
|
+
return entityId ? `${prefix}entity_${entityName}_id_${entityId}_keys` : `${prefix}entity_${entityName}_keys`;
|
|
162
85
|
}
|
|
163
86
|
constructor(){
|
|
164
87
|
_define_property(this, "logger", new _common.Logger(UtilsService.name));
|
|
165
|
-
_define_property(this, "phoneConfig", {
|
|
166
|
-
regex: DEFAULT_PHONE_REGEX,
|
|
167
|
-
countryCode: DEFAULT_PHONE_COUNTRY_CODE
|
|
168
|
-
});
|
|
169
88
|
}
|
|
170
89
|
};
|
|
171
90
|
UtilsService = _ts_decorate([
|
|
@@ -8,8 +8,6 @@ Object.defineProperty(exports, "ErrorHandler", {
|
|
|
8
8
|
return ErrorHandler;
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
|
-
const _common = require("@nestjs/common");
|
|
12
|
-
/** Check if running in production environment */ const IS_PRODUCTION = process.env.NODE_ENV === 'production';
|
|
13
11
|
/** Sensitive keys that should be redacted from logs */ const SENSITIVE_KEYS = [
|
|
14
12
|
'password',
|
|
15
13
|
'secret',
|
|
@@ -18,78 +16,23 @@ const _common = require("@nestjs/common");
|
|
|
18
16
|
'credential',
|
|
19
17
|
'authorization'
|
|
20
18
|
];
|
|
21
|
-
/** Patterns that indicate sensitive data in error messages */ const SENSITIVE_PATTERNS = [
|
|
22
|
-
/password/i,
|
|
23
|
-
/secret/i,
|
|
24
|
-
/token/i,
|
|
25
|
-
/key/i,
|
|
26
|
-
/credential/i,
|
|
27
|
-
/authorization/i,
|
|
28
|
-
/bearer/i
|
|
29
|
-
];
|
|
30
19
|
let ErrorHandler = class ErrorHandler {
|
|
31
20
|
/**
|
|
32
21
|
* Safely extract error message from unknown error.
|
|
33
|
-
|
|
34
|
-
* @param sanitizeForClient - If true, redacts potentially sensitive info in production
|
|
35
|
-
*/ static getErrorMessage(error, sanitizeForClient = false) {
|
|
36
|
-
let message = 'Unknown error occurred';
|
|
37
|
-
if (error instanceof Error) {
|
|
38
|
-
message = error.message;
|
|
39
|
-
} else if (typeof error === 'string') {
|
|
40
|
-
message = error;
|
|
41
|
-
}
|
|
42
|
-
// Sanitize for client responses in production
|
|
43
|
-
if (sanitizeForClient && IS_PRODUCTION) {
|
|
44
|
-
if (this.containsSensitiveData(message)) {
|
|
45
|
-
return 'An unexpected error occurred. Please try again later.';
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return message;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Check if a string contains potentially sensitive data.
|
|
52
|
-
*/ static containsSensitiveData(text) {
|
|
53
|
-
return SENSITIVE_PATTERNS.some((pattern)=>pattern.test(text));
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Safely extract error stack from unknown error.
|
|
57
|
-
* Returns undefined in production for client responses.
|
|
58
|
-
* @param error - The error to extract stack from
|
|
59
|
-
* @param forClient - If true, never returns stack in production
|
|
60
|
-
*/ static getErrorStack(error, forClient = false) {
|
|
61
|
-
// Never expose stack traces to clients in production
|
|
62
|
-
if (forClient && IS_PRODUCTION) {
|
|
63
|
-
return undefined;
|
|
64
|
-
}
|
|
22
|
+
*/ static getErrorMessage(error) {
|
|
65
23
|
if (error instanceof Error) {
|
|
66
|
-
return error.
|
|
67
|
-
}
|
|
68
|
-
return undefined;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Create a sanitized error response for clients.
|
|
72
|
-
* In production, sensitive data is redacted and stack traces removed.
|
|
73
|
-
*/ static createClientError(error, statusCode = _common.HttpStatus.INTERNAL_SERVER_ERROR, code) {
|
|
74
|
-
const sanitizedError = {
|
|
75
|
-
message: this.getErrorMessage(error, true),
|
|
76
|
-
statusCode
|
|
77
|
-
};
|
|
78
|
-
if (code) {
|
|
79
|
-
sanitizedError.code = code;
|
|
24
|
+
return error.message;
|
|
80
25
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
sanitizedError.stack = this.getErrorStack(error);
|
|
26
|
+
if (typeof error === 'string') {
|
|
27
|
+
return error;
|
|
84
28
|
}
|
|
85
|
-
return
|
|
29
|
+
return 'Unknown error occurred';
|
|
86
30
|
}
|
|
87
31
|
/**
|
|
88
32
|
* Sanitize context data to redact sensitive fields from logs.
|
|
89
33
|
*/ static sanitizeContextForLogging(context) {
|
|
90
34
|
const sanitized = {};
|
|
91
35
|
for (const [key, value] of Object.entries(context)){
|
|
92
|
-
// Check if key contains sensitive words
|
|
93
36
|
const isSensitive = SENSITIVE_KEYS.some((sk)=>key.toLowerCase().includes(sk.toLowerCase()));
|
|
94
37
|
if (isSensitive) {
|
|
95
38
|
sanitized[key] = '[REDACTED]';
|
|
@@ -105,7 +48,6 @@ let ErrorHandler = class ErrorHandler {
|
|
|
105
48
|
}
|
|
106
49
|
/**
|
|
107
50
|
* Create error context object for internal logging.
|
|
108
|
-
* Context data is sanitized to redact sensitive fields.
|
|
109
51
|
*/ static createErrorContext(error, context) {
|
|
110
52
|
const errorContext = {
|
|
111
53
|
error: {
|
|
@@ -117,29 +59,32 @@ let ErrorHandler = class ErrorHandler {
|
|
|
117
59
|
errorContext.error.name = error.name;
|
|
118
60
|
}
|
|
119
61
|
if (context && Object.keys(context).length > 0) {
|
|
120
|
-
// Sanitize context to redact sensitive fields
|
|
121
62
|
errorContext.context = this.sanitizeContextForLogging(context);
|
|
122
63
|
}
|
|
123
64
|
return errorContext;
|
|
124
65
|
}
|
|
125
66
|
/**
|
|
126
67
|
* Log error with consistent format.
|
|
127
|
-
* Sensitive data in context is automatically redacted.
|
|
128
68
|
*/ static logError(logger, error, operation, context) {
|
|
129
69
|
const errorContext = this.createErrorContext(error, {
|
|
130
70
|
operation,
|
|
131
71
|
...context
|
|
132
72
|
});
|
|
133
73
|
const errorMessage = `Failed to ${operation}: ${errorContext.error.message}`;
|
|
134
|
-
// Log full details internally (stack traces are fine for internal logs)
|
|
135
74
|
logger.error(errorMessage, errorContext.error.stack, errorContext);
|
|
136
75
|
}
|
|
137
76
|
/**
|
|
138
|
-
* Re-throw error with proper type checking
|
|
77
|
+
* Re-throw error with proper type checking.
|
|
139
78
|
*/ static rethrowError(error) {
|
|
140
79
|
if (error instanceof Error) {
|
|
141
80
|
throw error;
|
|
142
81
|
}
|
|
143
82
|
throw new Error(`Unexpected error: ${String(error)}`);
|
|
144
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Log error and re-throw (common pattern).
|
|
86
|
+
*/ static logAndRethrow(logger, error, operation, context) {
|
|
87
|
+
this.logError(logger, error, operation, context);
|
|
88
|
+
this.rethrowError(error);
|
|
89
|
+
}
|
|
145
90
|
};
|
|
@@ -16,9 +16,6 @@ function _export(target, all) {
|
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
18
|
_export(exports, {
|
|
19
|
-
get containsHtmlContent () {
|
|
20
|
-
return containsHtmlContent;
|
|
21
|
-
},
|
|
22
19
|
get escapeHtml () {
|
|
23
20
|
return escapeHtml;
|
|
24
21
|
},
|
|
@@ -43,7 +40,7 @@ function escapeHtml(str) {
|
|
|
43
40
|
if (!str || typeof str !== 'string') {
|
|
44
41
|
return str ?? '';
|
|
45
42
|
}
|
|
46
|
-
return str.replace(HTML_ESCAPE_REGEX, (char)=>HTML_ESCAPE_MAP[char]
|
|
43
|
+
return str.replace(HTML_ESCAPE_REGEX, (char)=>HTML_ESCAPE_MAP[char]);
|
|
47
44
|
}
|
|
48
45
|
function escapeHtmlVariables(variables) {
|
|
49
46
|
if (!variables || typeof variables !== 'object') {
|
|
@@ -65,10 +62,3 @@ function escapeHtmlVariables(variables) {
|
|
|
65
62
|
}
|
|
66
63
|
return escaped;
|
|
67
64
|
}
|
|
68
|
-
function containsHtmlContent(str) {
|
|
69
|
-
if (!str || typeof str !== 'string') {
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
// Check for common HTML patterns
|
|
73
|
-
return /<[a-z][\s\S]*>/i.test(str) || /javascript:/i.test(str) || /on\w+=/i.test(str);
|
|
74
|
-
}
|
package/cjs/utils/index.js
CHANGED
|
@@ -5,6 +5,8 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
_export_star(require("./error-handler.util"), exports);
|
|
6
6
|
_export_star(require("./html-sanitizer.util"), exports);
|
|
7
7
|
_export_star(require("./query-helpers.util"), exports);
|
|
8
|
+
_export_star(require("./request.util"), exports);
|
|
9
|
+
_export_star(require("./string.util"), exports);
|
|
8
10
|
function _export_star(from, to) {
|
|
9
11
|
Object.keys(from).forEach(function(k) {
|
|
10
12
|
if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
|