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