@flusys/nestjs-shared 1.0.0-beta → 1.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 (113) hide show
  1. package/README.md +501 -720
  2. package/cjs/classes/api-controller.class.js +9 -24
  3. package/cjs/classes/api-service.class.js +59 -92
  4. package/cjs/classes/index.js +1 -0
  5. package/cjs/classes/winston-logger-adapter.class.js +23 -40
  6. package/cjs/constants/index.js +14 -0
  7. package/cjs/constants/permissions.js +184 -0
  8. package/cjs/decorators/api-response.decorator.js +1 -1
  9. package/cjs/decorators/index.js +1 -0
  10. package/cjs/decorators/sanitize-html.decorator.js +36 -0
  11. package/cjs/dtos/delete.dto.js +10 -0
  12. package/cjs/dtos/filter-and-pagination.dto.js +24 -34
  13. package/cjs/dtos/pagination.dto.js +4 -8
  14. package/cjs/dtos/response-payload.dto.js +0 -116
  15. package/cjs/entities/identity.js +4 -4
  16. package/cjs/entities/user-root.js +13 -14
  17. package/cjs/guards/permission.guard.js +51 -105
  18. package/cjs/interceptors/index.js +1 -3
  19. package/cjs/interceptors/set-user-field-on-body.interceptor.js +60 -0
  20. package/cjs/interceptors/slug.interceptor.js +30 -9
  21. package/cjs/interfaces/datasource.interface.js +4 -0
  22. package/cjs/interfaces/index.js +2 -1
  23. package/cjs/interfaces/module-config.interface.js +4 -0
  24. package/cjs/middlewares/logger.middleware.js +50 -89
  25. package/cjs/modules/cache/cache.module.js +3 -3
  26. package/cjs/modules/datasource/datasource.module.js +11 -14
  27. package/cjs/modules/datasource/multi-tenant-datasource.service.js +29 -113
  28. package/cjs/modules/utils/utils.service.js +40 -203
  29. package/cjs/utils/error-handler.util.js +35 -12
  30. package/cjs/utils/html-sanitizer.util.js +64 -0
  31. package/cjs/utils/index.js +4 -0
  32. package/cjs/utils/query-helpers.util.js +53 -0
  33. package/cjs/utils/request.util.js +70 -0
  34. package/cjs/utils/string.util.js +63 -0
  35. package/classes/api-controller.class.d.ts +5 -5
  36. package/classes/api-service.class.d.ts +7 -5
  37. package/classes/index.d.ts +1 -0
  38. package/classes/request-scoped-api.service.d.ts +3 -2
  39. package/classes/winston-logger-adapter.class.d.ts +2 -0
  40. package/constants/index.d.ts +1 -0
  41. package/constants/permissions.d.ts +179 -0
  42. package/decorators/index.d.ts +1 -0
  43. package/decorators/sanitize-html.decorator.d.ts +2 -0
  44. package/dtos/delete.dto.d.ts +1 -0
  45. package/dtos/filter-and-pagination.dto.d.ts +0 -2
  46. package/dtos/response-payload.dto.d.ts +0 -20
  47. package/fesm/classes/api-controller.class.js +9 -24
  48. package/fesm/classes/api-service.class.js +59 -92
  49. package/fesm/classes/index.js +2 -0
  50. package/fesm/classes/winston-logger-adapter.class.js +23 -40
  51. package/fesm/constants/index.js +2 -0
  52. package/fesm/constants/permissions.js +128 -0
  53. package/fesm/decorators/api-response.decorator.js +1 -1
  54. package/fesm/decorators/index.js +1 -0
  55. package/fesm/decorators/sanitize-html.decorator.js +45 -0
  56. package/fesm/dtos/delete.dto.js +12 -2
  57. package/fesm/dtos/filter-and-pagination.dto.js +26 -47
  58. package/fesm/dtos/pagination.dto.js +4 -8
  59. package/fesm/dtos/response-payload.dto.js +0 -107
  60. package/fesm/entities/identity.js +4 -4
  61. package/fesm/entities/user-root.js +13 -14
  62. package/fesm/guards/permission.guard.js +51 -105
  63. package/fesm/interceptors/index.js +1 -3
  64. package/fesm/interceptors/set-user-field-on-body.interceptor.js +39 -0
  65. package/fesm/interceptors/slug.interceptor.js +31 -10
  66. package/fesm/interfaces/datasource.interface.js +20 -0
  67. package/fesm/interfaces/index.js +2 -1
  68. package/fesm/interfaces/module-config.interface.js +5 -0
  69. package/fesm/middlewares/logger.middleware.js +50 -83
  70. package/fesm/modules/cache/cache.module.js +2 -2
  71. package/fesm/modules/datasource/datasource.module.js +11 -14
  72. package/fesm/modules/datasource/multi-tenant-datasource.service.js +29 -113
  73. package/fesm/modules/utils/utils.service.js +41 -204
  74. package/fesm/utils/error-handler.util.js +36 -13
  75. package/fesm/utils/html-sanitizer.util.js +69 -0
  76. package/fesm/utils/index.js +4 -0
  77. package/fesm/utils/query-helpers.util.js +78 -0
  78. package/fesm/utils/request.util.js +58 -0
  79. package/fesm/utils/string.util.js +71 -0
  80. package/guards/permission.guard.d.ts +2 -0
  81. package/interceptors/index.d.ts +1 -3
  82. package/interceptors/set-user-field-on-body.interceptor.d.ts +5 -0
  83. package/interceptors/slug.interceptor.d.ts +2 -1
  84. package/interfaces/api.interface.d.ts +2 -2
  85. package/interfaces/datasource.interface.d.ts +5 -0
  86. package/interfaces/identity.interface.d.ts +4 -4
  87. package/interfaces/index.d.ts +2 -1
  88. package/interfaces/logged-user-info.interface.d.ts +0 -2
  89. package/interfaces/module-config.interface.d.ts +6 -0
  90. package/interfaces/permission.interface.d.ts +0 -1
  91. package/middlewares/logger.middleware.d.ts +2 -2
  92. package/modules/datasource/datasource.module.d.ts +1 -0
  93. package/modules/datasource/multi-tenant-datasource.service.d.ts +0 -1
  94. package/modules/utils/utils.service.d.ts +4 -14
  95. package/package.json +4 -4
  96. package/utils/error-handler.util.d.ts +14 -19
  97. package/utils/html-sanitizer.util.d.ts +2 -0
  98. package/utils/index.d.ts +4 -0
  99. package/utils/query-helpers.util.d.ts +16 -0
  100. package/utils/request.util.d.ts +4 -0
  101. package/utils/string.util.d.ts +2 -0
  102. package/cjs/interceptors/set-create-by-on-body.interceptor.js +0 -40
  103. package/cjs/interceptors/set-delete-by-on-body.interceptor.js +0 -40
  104. package/cjs/interceptors/set-update-by-on-body.interceptor.js +0 -40
  105. package/cjs/interfaces/base-query.interface.js +0 -6
  106. package/fesm/interceptors/set-create-by-on-body.interceptor.js +0 -30
  107. package/fesm/interceptors/set-delete-by-on-body.interceptor.js +0 -30
  108. package/fesm/interceptors/set-update-by-on-body.interceptor.js +0 -30
  109. package/fesm/interfaces/base-query.interface.js +0 -3
  110. package/interceptors/set-create-by-on-body.interceptor.d.ts +0 -5
  111. package/interceptors/set-delete-by-on-body.interceptor.d.ts +0 -5
  112. package/interceptors/set-update-by-on-body.interceptor.d.ts +0 -5
  113. package/interfaces/base-query.interface.d.ts +0 -7
@@ -32,7 +32,6 @@ import { Request } from 'express';
32
32
  import { DataSource } from 'typeorm';
33
33
  import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
34
34
  export class MultiTenantDataSourceService {
35
- // Initialization
36
35
  initializeFromOptions() {
37
36
  if (!this.options) return;
38
37
  if (!MultiTenantDataSourceService.initialized) {
@@ -45,100 +44,57 @@ export class MultiTenantDataSourceService {
45
44
  MultiTenantDataSourceService.tenantsRegistry.set(tenant.id, tenant);
46
45
  });
47
46
  }
48
- // Public API
49
- /**
50
- * Set custom tenant header name
51
- */ setTenantHeader(header) {
47
+ setTenantHeader(header) {
52
48
  this.tenantHeader = header;
53
49
  }
54
- /**
55
- * Get current database mode
56
- */ getDatabaseMode() {
50
+ getDatabaseMode() {
57
51
  return this.options?.bootstrapAppConfig?.databaseMode ?? 'single';
58
52
  }
59
- /**
60
- * Check if running in multi-tenant mode
61
- */ isMultiTenant() {
53
+ isMultiTenant() {
62
54
  return this.getDatabaseMode() === 'multi-tenant';
63
55
  }
64
- // Tenant Resolution
65
- /**
66
- * Get current tenant ID from request header
67
- * Validates tenant ID format for security (alphanumeric, hyphens, underscores only)
68
- */ getCurrentTenantId() {
56
+ getCurrentTenantId() {
69
57
  if (!this.request) return null;
70
58
  const tenantId = this.request.headers[this.tenantHeader];
71
59
  if (!tenantId) return null;
72
- // Validate tenant ID format to prevent injection attacks
73
60
  if (!/^[a-zA-Z0-9_-]+$/.test(tenantId)) {
74
- throw new BadRequestException('Invalid tenant ID format. Only alphanumeric characters, hyphens, and underscores are allowed.');
61
+ throw new BadRequestException('Invalid tenant ID format');
75
62
  }
76
63
  return tenantId;
77
64
  }
78
- /**
79
- * Get current tenant config from request header
80
- */ getCurrentTenant() {
65
+ getCurrentTenant() {
81
66
  const tenantId = this.getCurrentTenantId();
82
67
  return tenantId ? this.getTenant(tenantId) : null;
83
68
  }
84
- /**
85
- * Get tenant config by ID
86
- */ getTenant(tenantId) {
69
+ getTenant(tenantId) {
87
70
  return MultiTenantDataSourceService.tenantsRegistry.get(tenantId) ?? null;
88
71
  }
89
- /**
90
- * Get all registered tenants
91
- */ getAllTenants() {
72
+ getAllTenants() {
92
73
  return Array.from(MultiTenantDataSourceService.tenantsRegistry.values());
93
74
  }
94
- /**
95
- * Get only active tenants
96
- */ getActiveTenants() {
75
+ getActiveTenants() {
97
76
  return this.getAllTenants();
98
77
  }
99
- // DataSource Access
100
- /**
101
- * Get DataSource for current context (tenant or single)
102
- */ async getDataSource() {
78
+ async getDataSource() {
103
79
  return this.isMultiTenant() ? this.getTenantDataSource() : this.getSingleDataSource();
104
80
  }
105
- /**
106
- * Get DataSource for specific tenant
107
- */ async getDataSourceForTenant(tenantId) {
81
+ async getDataSourceForTenant(tenantId) {
108
82
  const tenant = this.getTenant(tenantId);
109
- if (!tenant) {
110
- throw new Error(`Tenant '${tenantId}' not found`);
111
- }
83
+ if (!tenant) throw new Error(`Tenant '${tenantId}' not found`);
112
84
  return this.getOrCreateTenantConnection(tenant);
113
85
  }
114
- /**
115
- * Set external DataSource (for single-tenant mode)
116
- */ setDataSource(dataSource) {
86
+ setDataSource(dataSource) {
117
87
  MultiTenantDataSourceService.singleDataSource = dataSource;
118
88
  }
119
- // Repository Access
120
- /**
121
- * Get repository for entity in current context
122
- */ async getRepository(entity) {
89
+ async getRepository(entity) {
123
90
  const dataSource = await this.getDataSource();
124
91
  return dataSource.getRepository(entity);
125
92
  }
126
- /**
127
- * Get repository for entity in specific tenant
128
- */ async getRepositoryForTenant(entity, tenantId) {
129
- const dataSource = await this.getDataSourceForTenant(tenantId);
130
- return dataSource.getRepository(entity);
131
- }
132
- // Multi-Tenant Operations
133
- /**
134
- * Execute callback with specific tenant's DataSource
135
- */ async withTenant(tenantId, callback) {
93
+ async withTenant(tenantId, callback) {
136
94
  const dataSource = await this.getDataSourceForTenant(tenantId);
137
95
  return callback(dataSource);
138
96
  }
139
- /**
140
- * Execute callback for all active tenants
141
- */ async forAllTenants(callback) {
97
+ async forAllTenants(callback) {
142
98
  const results = new Map();
143
99
  for (const tenant of this.getActiveTenants()){
144
100
  try {
@@ -150,22 +106,14 @@ export class MultiTenantDataSourceService {
150
106
  }
151
107
  return results;
152
108
  }
153
- // Tenant Management
154
- /**
155
- * Register a new tenant at runtime
156
- */ registerTenant(tenant) {
109
+ registerTenant(tenant) {
157
110
  MultiTenantDataSourceService.tenantsRegistry.set(tenant.id, tenant);
158
111
  }
159
- /**
160
- * Remove tenant and close its connection
161
- */ async removeTenant(tenantId) {
112
+ async removeTenant(tenantId) {
162
113
  await this.closeTenantConnection(tenantId);
163
114
  MultiTenantDataSourceService.tenantsRegistry.delete(tenantId);
164
115
  }
165
- // Connection Lifecycle
166
- /**
167
- * Close specific tenant connection
168
- */ async closeTenantConnection(tenantId) {
116
+ async closeTenantConnection(tenantId) {
169
117
  const connection = MultiTenantDataSourceService.tenantConnections.get(tenantId);
170
118
  if (connection?.isInitialized) {
171
119
  await connection.destroy();
@@ -173,16 +121,12 @@ export class MultiTenantDataSourceService {
173
121
  this.logger.log(`Closed connection for tenant: ${tenantId}`);
174
122
  }
175
123
  }
176
- /**
177
- * Lifecycle hook - cleanup on module destroy
178
- */ async onModuleDestroy() {
124
+ async onModuleDestroy() {
179
125
  for (const [tenantId] of MultiTenantDataSourceService.tenantConnections){
180
126
  await this.closeTenantConnection(tenantId);
181
127
  }
182
128
  }
183
- /**
184
- * Reset all static state (useful for testing)
185
- */ static reset() {
129
+ static reset() {
186
130
  MultiTenantDataSourceService.initialized = false;
187
131
  MultiTenantDataSourceService.singleDataSource = null;
188
132
  MultiTenantDataSourceService.singleConnectionLock = null;
@@ -226,20 +170,11 @@ export class MultiTenantDataSourceService {
226
170
  }
227
171
  return this.getOrCreateTenantConnection(tenant);
228
172
  }
229
- /**
230
- * Get or create connection for tenant with locking to prevent race conditions
231
- */ async getOrCreateTenantConnection(tenant) {
232
- // Return existing initialized connection
173
+ async getOrCreateTenantConnection(tenant) {
233
174
  const existing = MultiTenantDataSourceService.tenantConnections.get(tenant.id);
234
- if (existing?.isInitialized) {
235
- return existing;
236
- }
237
- // If another request is creating this tenant's connection, wait for it
175
+ if (existing?.isInitialized) return existing;
238
176
  const pendingConnection = MultiTenantDataSourceService.connectionLocks.get(tenant.id);
239
- if (pendingConnection) {
240
- return pendingConnection;
241
- }
242
- // Create connection with lock to prevent race conditions
177
+ if (pendingConnection) return pendingConnection;
243
178
  const config = this.buildTenantDatabaseConfig(tenant);
244
179
  const connectionPromise = this.createDataSourceFromConfig(config);
245
180
  MultiTenantDataSourceService.connectionLocks.set(tenant.id, connectionPromise);
@@ -252,18 +187,12 @@ export class MultiTenantDataSourceService {
252
187
  MultiTenantDataSourceService.connectionLocks.delete(tenant.id);
253
188
  }
254
189
  }
255
- /**
256
- * Get default database config (supports both naming conventions)
257
- */ getDefaultDatabaseConfig() {
190
+ getDefaultDatabaseConfig() {
258
191
  return this.options?.defaultDatabaseConfig ?? this.options?.tenantDefaultDatabaseConfig;
259
192
  }
260
- /**
261
- * Build database config for tenant (merges with default)
262
- */ buildTenantDatabaseConfig(tenant) {
193
+ buildTenantDatabaseConfig(tenant) {
263
194
  const defaultConfig = this.getDefaultDatabaseConfig();
264
- if (!defaultConfig) {
265
- throw new Error('No default database config for multi-tenant mode.');
266
- }
195
+ if (!defaultConfig) throw new Error('No default database config for multi-tenant mode.');
267
196
  return {
268
197
  type: defaultConfig.type,
269
198
  host: tenant.host ?? defaultConfig.host,
@@ -273,15 +202,7 @@ export class MultiTenantDataSourceService {
273
202
  database: tenant.database
274
203
  };
275
204
  }
276
- /**
277
- * Create DataSource from config - override in subclasses for custom logic
278
- *
279
- * Note: In multi-tenant mode, subclasses should pass entities array.
280
- * For single-tenant mode, entities are registered via TypeOrmModule at app level.
281
- *
282
- * @param config - Database configuration
283
- * @param entities - Optional entities array (for multi-tenant mode)
284
- */ async createDataSourceFromConfig(config, entities = []) {
205
+ async createDataSourceFromConfig(config, entities = []) {
285
206
  const dataSource = new DataSource({
286
207
  type: config.type,
287
208
  host: config.host,
@@ -293,16 +214,13 @@ export class MultiTenantDataSourceService {
293
214
  synchronize: false,
294
215
  namingStrategy: new SnakeNamingStrategy()
295
216
  });
296
- if (!dataSource.isInitialized) {
297
- await dataSource.initialize();
298
- }
217
+ if (!dataSource.isInitialized) await dataSource.initialize();
299
218
  return dataSource;
300
219
  }
301
220
  constructor(options, request){
302
221
  _define_property(this, "options", void 0);
303
222
  _define_property(this, "request", void 0);
304
223
  _define_property(this, "logger", void 0);
305
- // Instance state
306
224
  _define_property(this, "tenantHeader", void 0);
307
225
  this.options = options;
308
226
  this.request = request;
@@ -311,12 +229,10 @@ export class MultiTenantDataSourceService {
311
229
  this.initializeFromOptions();
312
230
  }
313
231
  }
314
- // Static state shared across all instances
315
232
  _define_property(MultiTenantDataSourceService, "tenantConnections", new Map());
316
233
  _define_property(MultiTenantDataSourceService, "singleDataSource", null);
317
234
  _define_property(MultiTenantDataSourceService, "tenantsRegistry", new Map());
318
235
  _define_property(MultiTenantDataSourceService, "initialized", false);
319
- // Connection locks to prevent race conditions during concurrent connection creation
320
236
  _define_property(MultiTenantDataSourceService, "connectionLocks", new Map());
321
237
  _define_property(MultiTenantDataSourceService, "singleConnectionLock", null);
322
238
  MultiTenantDataSourceService = _ts_decorate([
@@ -1,245 +1,82 @@
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
+ }
1
14
  function _ts_decorate(decorators, target, key, desc) {
2
15
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
16
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
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;
5
18
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
19
  }
7
- function _ts_metadata(k, v) {
8
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
- }
10
- import { Injectable } from '@nestjs/common';
11
- import { isEmail } from 'class-validator';
20
+ import { Injectable, Logger } from '@nestjs/common';
12
21
  export class UtilsService {
13
22
  // ---------------- CACHE HELPERS ----------------
14
23
  /**
15
24
  * Generate cache key with optional tenant prefix for multi-tenant isolation
16
- * @param entityName - Name of the entity being cached
17
- * @param params - Query parameters for cache key uniqueness
18
- * @param entityId - Optional entity ID for single entity caching
19
- * @param tenantId - Optional tenant ID for multi-tenant isolation
20
25
  */ getCacheKey(entityName, params, entityId, tenantId) {
21
- const tenantPrefix = tenantId ? `tenant_${tenantId}_` : '';
22
- if (entityId) return `${tenantPrefix}entity_${entityName}_id_${entityId}${params ? '_select_' + JSON.stringify(params) : ''}`;
23
- 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)}`;
24
31
  }
25
32
  /**
26
33
  * Track cache key for later invalidation with optional tenant prefix
27
34
  */ async trackCacheKey(cacheKey, entityName, cacheManager, entityId, tenantId) {
28
- const tenantPrefix = tenantId ? `tenant_${tenantId}_` : '';
29
35
  try {
30
- if (entityId) {
31
- const trackingKey = `${tenantPrefix}entity_${entityName}_id_${entityId}_keys`;
32
- const idKeys = await cacheManager.get(trackingKey) || [];
33
- if (!idKeys.includes(cacheKey)) idKeys.push(cacheKey);
34
- await cacheManager.set(trackingKey, idKeys);
35
- } else {
36
- const trackingKey = `${tenantPrefix}entity_${entityName}_keys`;
37
- const allKeys = await cacheManager.get(trackingKey) || [];
38
- if (!allKeys.includes(cacheKey)) allKeys.push(cacheKey);
39
- await cacheManager.set(trackingKey, allKeys);
40
- }
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);
41
40
  } catch (error) {
42
- // Log but don't throw - cache tracking failure shouldn't break the request
43
- console.error(`Cache tracking failed for ${entityName}:`, error);
41
+ this.logger.error(`Cache tracking failed for ${entityName}`, error instanceof Error ? error.stack : String(error));
44
42
  }
45
43
  }
46
44
  /**
47
45
  * Clear cache for entity with optional tenant prefix
48
- * Uses Promise.allSettled to ensure all deletions are attempted even if some fail
49
46
  */ async clearCache(entityName, cacheManager, entityId, tenantId) {
50
- const tenantPrefix = tenantId ? `tenant_${tenantId}_` : '';
51
47
  try {
52
- if (entityId) {
53
- const trackingKey = `${tenantPrefix}entity_${entityName}_id_${entityId}_keys`;
54
- const idKeys = await cacheManager.get(trackingKey) || [];
55
- // Use Promise.allSettled to ensure all deletions are attempted
56
- await Promise.allSettled(idKeys.map((key)=>cacheManager.del(key)));
57
- await cacheManager.del(trackingKey);
58
- } else {
59
- const trackingKey = `${tenantPrefix}entity_${entityName}_keys`;
60
- const keySet = await cacheManager.get(trackingKey) || [];
61
- // Use Promise.allSettled to ensure all deletions are attempted
62
- await Promise.allSettled(keySet.map((key)=>cacheManager.del(key)));
63
- await cacheManager.del(trackingKey);
64
- }
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);
65
52
  } catch (error) {
66
- // Log but don't throw - cache invalidation failure shouldn't break the request
67
- console.error(`Cache invalidation failed for ${entityName}:`, error);
68
- }
69
- }
70
- /**
71
- * Check Phone or Email
72
- */ checkPhoneOrEmail(value) {
73
- if (isEmail(value)) {
74
- return {
75
- value: value,
76
- type: 'email'
77
- };
78
- }
79
- const phoneMatch = value.match(/^((\+880)|0)?(13|15|16|17|18|19)\d{8}$/);
80
- if (phoneMatch) {
81
- let phone = phoneMatch[0];
82
- if (!phone.startsWith('+88')) {
83
- phone = '+88' + phone;
84
- }
85
- return {
86
- value: phone,
87
- type: 'phone'
88
- };
53
+ this.logger.error(`Cache invalidation failed for ${entityName}`, error instanceof Error ? error.stack : String(error));
89
54
  }
90
- return {
91
- value: null,
92
- type: null
93
- };
94
55
  }
56
+ // ---------------- STRING HELPERS ----------------
95
57
  /**
96
- * STRING FUNCTIONS
97
- * transformToSlug
58
+ * Transform string to URL-friendly slug
98
59
  */ transformToSlug(value, salt) {
99
60
  const slug = value?.trim().replace(/[^A-Z0-9]+/gi, '-').toLowerCase();
100
61
  return salt ? `${slug}-${this.getRandomInt(1, 100)}` : slug;
101
62
  }
102
63
  /**
103
- * RANDOM FUNCTIONS
104
- * getRandomInt
105
- * generateRandomId
106
- * getRandomOtpCode
64
+ * Generate random integer between min and max (inclusive)
107
65
  */ getRandomInt(min, max) {
108
66
  return Math.floor(Math.random() * (max - min + 1)) + min;
109
67
  }
110
- generateRandomId(length) {
111
- const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
112
- let result = '';
113
- for(let i = 0; i < length; i++){
114
- result += characters.charAt(Math.floor(Math.random() * characters.length));
115
- }
116
- return result;
117
- }
118
- getRandomOtpCode() {
119
- return Math.floor(Math.random() * (9999 - 1000 + 1)) + 1000;
68
+ // ---------------- PRIVATE HELPERS ----------------
69
+ buildTenantPrefix(tenantId) {
70
+ return tenantId ? `tenant_${tenantId}_` : '';
120
71
  }
121
- extractColumnNameFromError(detail) {
122
- const match = detail.match(/\((.*?)\)=\((.*?)\)/);
123
- return {
124
- columnName: match ? match[1] : 'unknown',
125
- value: match ? match[2] : 'unknown'
126
- };
127
- }
128
- getOtpEmailFormat(otp, userName) {
129
- return `
130
- <!DOCTYPE html>
131
- <html lang="en">
132
- <head>
133
- <meta charset="UTF-8" />
134
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
135
- <title>Your OTP Code</title>
136
- <style>
137
- body {
138
- font-family: Arial, sans-serif;
139
- background-color: #f4f4f7;
140
- margin: 0;
141
- padding: 0;
142
- }
143
- .email-container {
144
- max-width: 500px;
145
- margin: 40px auto;
146
- background-color: #ffffff;
147
- padding: 30px;
148
- border-radius: 8px;
149
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
150
- text-align: center;
151
- }
152
- h1 {
153
- color: #333;
154
- }
155
- p {
156
- font-size: 16px;
157
- color: #555;
158
- }
159
- .otp-box {
160
- display: inline-block;
161
- margin: 20px 0;
162
- padding: 14px 28px;
163
- font-size: 24px;
164
- letter-spacing: 6px;
165
- background-color: #007BFF;
166
- color: white;
167
- border-radius: 8px;
168
- font-weight: bold;
169
- }
170
- </style>
171
- </head>
172
- <body>
173
- <div class="email-container">
174
- <p>Hi ${userName || 'Sir/Madam'},</p>
175
- <p>Use the code below to verify your identity:</p>
176
- <div class="otp-box">${otp}</div>
177
- <p>This OTP is valid for a limited time. Do not share it with anyone.</p>
178
- <p>If you didn’t request this, please ignore this email.</p>
179
- </div>
180
- </body>
181
- </html>
182
- `;
72
+ buildTrackingKey(entityName, entityId, tenantId) {
73
+ const prefix = this.buildTenantPrefix(tenantId);
74
+ return entityId ? `${prefix}entity_${entityName}_id_${entityId}_keys` : `${prefix}entity_${entityName}_keys`;
183
75
  }
184
- getResetPasswordEmailFormat(resetLink, userName) {
185
- return `
186
- <!DOCTYPE html>
187
- <html lang="en">
188
- <head>
189
- <meta charset="UTF-8" />
190
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
191
- <title>Reset Your Password</title>
192
- <style>
193
- body {
194
- font-family: Arial, sans-serif;
195
- background-color: #f4f4f7;
196
- margin: 0;
197
- padding: 0;
198
- }
199
- .email-container {
200
- max-width: 500px;
201
- margin: 40px auto;
202
- background-color: #ffffff;
203
- padding: 30px;
204
- border-radius: 8px;
205
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
206
- text-align: center;
207
- }
208
- h1 {
209
- color: #333;
210
- }
211
- p {
212
- font-size: 16px;
213
- color: #555;
214
- }
215
- .reset-button {
216
- display: inline-block;
217
- margin: 20px 0;
218
- padding: 14px 28px;
219
- font-size: 16px;
220
- background-color: #007BFF;
221
- color: #ffffff !important;
222
- text-decoration: none;
223
- border-radius: 6px;
224
- font-weight: bold;
225
- }
226
- </style>
227
- </head>
228
- <body>
229
- <div class="email-container">
230
- <p>Hi ${userName || 'Sir/Madam'},</p>
231
- <p>We received a request to reset your password. Click the button below to reset it:</p>
232
- <a href="${resetLink}" class="reset-button" target="_blank">Reset Password</a>
233
- <p>If you didn’t request a password reset, you can safely ignore this email.</p>
234
- </div>
235
- </body>
236
- </html>
237
- `;
76
+ constructor(){
77
+ _define_property(this, "logger", new Logger(UtilsService.name));
238
78
  }
239
- constructor(){}
240
79
  }
241
80
  UtilsService = _ts_decorate([
242
- Injectable(),
243
- _ts_metadata("design:type", Function),
244
- _ts_metadata("design:paramtypes", [])
81
+ Injectable()
245
82
  ], UtilsService);
@@ -1,8 +1,16 @@
1
+ /** Sensitive keys that should be redacted from logs */ const SENSITIVE_KEYS = [
2
+ 'password',
3
+ 'secret',
4
+ 'token',
5
+ 'apiKey',
6
+ 'credential',
7
+ 'authorization'
8
+ ];
1
9
  /**
2
- * Error handling utility for consistent error logging and handling
10
+ * Error handling utility for consistent error logging and handling.
3
11
  */ export class ErrorHandler {
4
12
  /**
5
- * Safely extract error message from unknown error
13
+ * Safely extract error message from unknown error.
6
14
  */ static getErrorMessage(error) {
7
15
  if (error instanceof Error) {
8
16
  return error.message;
@@ -13,15 +21,25 @@
13
21
  return 'Unknown error occurred';
14
22
  }
15
23
  /**
16
- * Safely extract error stack from unknown error
17
- */ static getErrorStack(error) {
18
- if (error instanceof Error) {
19
- return error.stack;
24
+ * Sanitize context data to redact sensitive fields from logs.
25
+ */ static sanitizeContextForLogging(context) {
26
+ const sanitized = {};
27
+ for (const [key, value] of Object.entries(context)){
28
+ const isSensitive = SENSITIVE_KEYS.some((sk)=>key.toLowerCase().includes(sk.toLowerCase()));
29
+ if (isSensitive) {
30
+ sanitized[key] = '[REDACTED]';
31
+ } else if (Array.isArray(value)) {
32
+ sanitized[key] = value.map((item)=>typeof item === 'object' && item !== null ? this.sanitizeContextForLogging(item) : item);
33
+ } else if (typeof value === 'object' && value !== null) {
34
+ sanitized[key] = this.sanitizeContextForLogging(value);
35
+ } else {
36
+ sanitized[key] = value;
37
+ }
20
38
  }
21
- return undefined;
39
+ return sanitized;
22
40
  }
23
41
  /**
24
- * Create error context object for logging
42
+ * Create error context object for internal logging.
25
43
  */ static createErrorContext(error, context) {
26
44
  const errorContext = {
27
45
  error: {
@@ -33,27 +51,32 @@
33
51
  errorContext.error.name = error.name;
34
52
  }
35
53
  if (context && Object.keys(context).length > 0) {
36
- errorContext.context = context;
54
+ errorContext.context = this.sanitizeContextForLogging(context);
37
55
  }
38
56
  return errorContext;
39
57
  }
40
58
  /**
41
- * Log error with consistent format
59
+ * Log error with consistent format.
42
60
  */ static logError(logger, error, operation, context) {
43
61
  const errorContext = this.createErrorContext(error, {
44
62
  operation,
45
63
  ...context
46
64
  });
47
65
  const errorMessage = `Failed to ${operation}: ${errorContext.error.message}`;
48
- const loggerContext = logger.context || 'ErrorHandler';
49
- logger.error(errorMessage, errorContext.error.stack, loggerContext, errorContext);
66
+ logger.error(errorMessage, errorContext.error.stack, errorContext);
50
67
  }
51
68
  /**
52
- * Re-throw error with proper type checking
69
+ * Re-throw error with proper type checking.
53
70
  */ static rethrowError(error) {
54
71
  if (error instanceof Error) {
55
72
  throw error;
56
73
  }
57
74
  throw new Error(`Unexpected error: ${String(error)}`);
58
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
+ }
59
82
  }