@flusys/nestjs-shared 1.1.0-beta → 1.2.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/README.md +146 -1330
  2. package/cjs/classes/api-controller.class.js +325 -49
  3. package/cjs/classes/api-service.class.js +271 -149
  4. package/cjs/classes/handle-custome-db.js +37 -0
  5. package/cjs/classes/index.js +2 -2
  6. package/cjs/classes/winston.logger.class.js +103 -70
  7. package/cjs/constants/index.js +15 -4
  8. package/cjs/constants/message-keys.js +68 -0
  9. package/cjs/constants/permissions.js +287 -0
  10. package/cjs/decorators/api-response.decorator.js +2 -3
  11. package/cjs/decorators/index.js +1 -0
  12. package/cjs/decorators/log-action.decorator.js +149 -0
  13. package/cjs/decorators/require-permission.decorator.js +5 -5
  14. package/cjs/dtos/delete.dto.js +10 -0
  15. package/cjs/dtos/filter-and-pagination.dto.js +24 -34
  16. package/cjs/dtos/get-by-ids.dto.js +65 -0
  17. package/cjs/dtos/identity-response.dto.js +4 -8
  18. package/cjs/dtos/index.js +1 -0
  19. package/cjs/dtos/pagination.dto.js +4 -8
  20. package/cjs/dtos/response-payload.dto.js +68 -112
  21. package/cjs/entities/identity.js +12 -6
  22. package/cjs/entities/index.js +1 -0
  23. package/cjs/entities/raw-type.js +4 -0
  24. package/cjs/entities/user-root.js +15 -15
  25. package/cjs/enums/index.js +20 -0
  26. package/cjs/enums/notification-type.enum.js +17 -0
  27. package/cjs/enums/participant-status.enum.js +17 -0
  28. package/cjs/enums/recurrence-type.enum.js +18 -0
  29. package/cjs/exceptions/base-app.exception.js +151 -0
  30. package/cjs/exceptions/index.js +1 -0
  31. package/cjs/exceptions/permission.exception.js +13 -9
  32. package/cjs/filters/global-exception.filter.js +279 -0
  33. package/cjs/filters/index.js +18 -0
  34. package/cjs/guards/jwt-auth.guard.js +4 -1
  35. package/cjs/guards/permission.guard.js +88 -148
  36. package/cjs/index.js +2 -0
  37. package/cjs/interceptors/idempotency.interceptor.js +1 -1
  38. package/cjs/interceptors/index.js +1 -4
  39. package/cjs/interceptors/set-user-field-on-body.interceptor.js +60 -0
  40. package/cjs/interceptors/slug.interceptor.js +30 -9
  41. package/cjs/interfaces/datasource.interface.js +4 -0
  42. package/cjs/interfaces/event-manager-adapter.interface.js +11 -0
  43. package/cjs/interfaces/index.js +4 -2
  44. package/cjs/interfaces/module-config.interface.js +4 -0
  45. package/cjs/interfaces/notification-adapter.interface.js +11 -0
  46. package/cjs/interfaces/permission.interface.js +1 -1
  47. package/cjs/middlewares/logger.middleware.js +133 -115
  48. package/cjs/modules/cache/cache.module.js +3 -3
  49. package/cjs/modules/datasource/index.js +0 -1
  50. package/cjs/modules/datasource/multi-tenant-datasource.service.js +60 -183
  51. package/cjs/modules/utils/utils.service.js +27 -206
  52. package/cjs/utils/date-time.util.js +94 -0
  53. package/cjs/utils/html-sanitizer.util.js +64 -0
  54. package/cjs/utils/index.js +5 -1
  55. package/cjs/utils/query-helpers.util.js +60 -0
  56. package/cjs/utils/request.util.js +96 -0
  57. package/cjs/utils/string.util.js +63 -0
  58. package/classes/api-controller.class.d.ts +16 -8
  59. package/classes/api-service.class.d.ts +30 -25
  60. package/classes/handle-custome-db.d.ts +3 -0
  61. package/classes/index.d.ts +2 -2
  62. package/classes/winston.logger.class.d.ts +1 -0
  63. package/constants/index.d.ts +2 -1
  64. package/constants/message-keys.d.ts +45 -0
  65. package/constants/permissions.d.ts +306 -0
  66. package/decorators/api-response.decorator.d.ts +1 -1
  67. package/decorators/current-user.decorator.d.ts +1 -1
  68. package/decorators/index.d.ts +1 -0
  69. package/decorators/log-action.decorator.d.ts +8 -0
  70. package/decorators/require-permission.decorator.d.ts +2 -2
  71. package/dtos/delete.dto.d.ts +1 -0
  72. package/dtos/filter-and-pagination.dto.d.ts +0 -2
  73. package/dtos/get-by-ids.dto.d.ts +4 -0
  74. package/dtos/index.d.ts +1 -0
  75. package/dtos/response-payload.dto.d.ts +8 -20
  76. package/entities/index.d.ts +1 -0
  77. package/entities/raw-type.d.ts +1 -0
  78. package/enums/index.d.ts +3 -0
  79. package/enums/notification-type.enum.d.ts +6 -0
  80. package/enums/participant-status.enum.d.ts +6 -0
  81. package/enums/recurrence-type.enum.d.ts +7 -0
  82. package/exceptions/base-app.exception.d.ts +41 -0
  83. package/exceptions/index.d.ts +1 -0
  84. package/exceptions/permission.exception.d.ts +2 -2
  85. package/fesm/classes/api-controller.class.js +326 -50
  86. package/fesm/classes/api-service.class.js +274 -152
  87. package/fesm/classes/handle-custome-db.js +19 -0
  88. package/fesm/classes/index.js +3 -2
  89. package/fesm/classes/winston.logger.class.js +100 -68
  90. package/fesm/constants/index.js +4 -1
  91. package/fesm/constants/message-keys.js +49 -0
  92. package/fesm/constants/permissions.js +200 -0
  93. package/fesm/decorators/api-response.decorator.js +3 -21
  94. package/fesm/decorators/index.js +1 -0
  95. package/fesm/decorators/log-action.decorator.js +139 -0
  96. package/fesm/decorators/require-permission.decorator.js +17 -65
  97. package/fesm/dtos/delete.dto.js +12 -2
  98. package/fesm/dtos/filter-and-pagination.dto.js +26 -47
  99. package/fesm/dtos/get-by-ids.dto.js +55 -0
  100. package/fesm/dtos/identity-response.dto.js +4 -8
  101. package/fesm/dtos/index.js +1 -0
  102. package/fesm/dtos/pagination.dto.js +4 -8
  103. package/fesm/dtos/response-payload.dto.js +68 -103
  104. package/fesm/entities/identity.js +12 -6
  105. package/fesm/entities/index.js +1 -0
  106. package/fesm/entities/raw-type.js +1 -0
  107. package/fesm/entities/user-root.js +15 -15
  108. package/fesm/enums/index.js +3 -0
  109. package/fesm/enums/notification-type.enum.js +7 -0
  110. package/fesm/enums/participant-status.enum.js +7 -0
  111. package/fesm/enums/recurrence-type.enum.js +8 -0
  112. package/fesm/exceptions/base-app.exception.js +115 -0
  113. package/fesm/exceptions/index.js +1 -0
  114. package/fesm/exceptions/permission.exception.js +16 -18
  115. package/fesm/filters/global-exception.filter.js +269 -0
  116. package/fesm/filters/index.js +1 -0
  117. package/fesm/guards/jwt-auth.guard.js +5 -2
  118. package/fesm/guards/permission.guard.js +90 -150
  119. package/fesm/index.js +2 -0
  120. package/fesm/interceptors/idempotency.interceptor.js +2 -2
  121. package/fesm/interceptors/index.js +1 -4
  122. package/fesm/interceptors/set-user-field-on-body.interceptor.js +39 -0
  123. package/fesm/interceptors/slug.interceptor.js +31 -10
  124. package/fesm/interfaces/datasource.interface.js +1 -0
  125. package/fesm/interfaces/event-manager-adapter.interface.js +1 -0
  126. package/fesm/interfaces/index.js +4 -2
  127. package/fesm/interfaces/module-config.interface.js +5 -0
  128. package/fesm/interfaces/notification-adapter.interface.js +1 -0
  129. package/fesm/interfaces/permission.interface.js +1 -3
  130. package/fesm/middlewares/logger.middleware.js +133 -109
  131. package/fesm/modules/cache/cache.module.js +2 -2
  132. package/fesm/modules/datasource/index.js +0 -1
  133. package/fesm/modules/datasource/multi-tenant-datasource.service.js +61 -184
  134. package/fesm/modules/utils/utils.service.js +27 -206
  135. package/fesm/utils/date-time.util.js +70 -0
  136. package/fesm/utils/html-sanitizer.util.js +69 -0
  137. package/fesm/utils/index.js +5 -1
  138. package/fesm/utils/query-helpers.util.js +85 -0
  139. package/fesm/utils/request.util.js +81 -0
  140. package/fesm/utils/string.util.js +71 -0
  141. package/filters/global-exception.filter.d.ts +9 -0
  142. package/filters/index.d.ts +1 -0
  143. package/guards/permission.guard.d.ts +4 -6
  144. package/index.d.ts +2 -0
  145. package/interceptors/idempotency.interceptor.d.ts +1 -1
  146. package/interceptors/index.d.ts +1 -4
  147. package/interceptors/set-user-field-on-body.interceptor.d.ts +5 -0
  148. package/interceptors/slug.interceptor.d.ts +2 -1
  149. package/interfaces/api.interface.d.ts +4 -2
  150. package/interfaces/datasource.interface.d.ts +5 -0
  151. package/interfaces/event-manager-adapter.interface.d.ts +43 -0
  152. package/interfaces/identity.interface.d.ts +4 -4
  153. package/interfaces/index.d.ts +4 -2
  154. package/interfaces/logged-user-info.interface.d.ts +0 -2
  155. package/interfaces/module-config.interface.d.ts +6 -0
  156. package/interfaces/notification-adapter.interface.d.ts +21 -0
  157. package/interfaces/permission.interface.d.ts +13 -9
  158. package/middlewares/logger.middleware.d.ts +2 -2
  159. package/modules/datasource/index.d.ts +0 -1
  160. package/modules/datasource/multi-tenant-datasource.service.d.ts +5 -16
  161. package/modules/utils/utils.service.d.ts +3 -14
  162. package/package.json +21 -14
  163. package/utils/date-time.util.d.ts +8 -0
  164. package/utils/html-sanitizer.util.d.ts +2 -0
  165. package/utils/index.d.ts +5 -1
  166. package/utils/query-helpers.util.d.ts +16 -0
  167. package/utils/request.util.d.ts +6 -0
  168. package/utils/string.util.d.ts +2 -0
  169. package/cjs/classes/request-scoped-api.service.js +0 -61
  170. package/cjs/classes/winston-logger-adapter.class.js +0 -121
  171. package/cjs/interceptors/query-performance.interceptor.js +0 -66
  172. package/cjs/interceptors/set-create-by-on-body.interceptor.js +0 -40
  173. package/cjs/interceptors/set-delete-by-on-body.interceptor.js +0 -40
  174. package/cjs/interceptors/set-update-by-on-body.interceptor.js +0 -40
  175. package/cjs/interfaces/base-query.interface.js +0 -6
  176. package/cjs/interfaces/logger.interface.js +0 -7
  177. package/cjs/modules/datasource/datasource.module.js +0 -120
  178. package/cjs/utils/error-handler.util.js +0 -67
  179. package/classes/request-scoped-api.service.d.ts +0 -13
  180. package/classes/winston-logger-adapter.class.d.ts +0 -20
  181. package/fesm/classes/request-scoped-api.service.js +0 -51
  182. package/fesm/classes/winston-logger-adapter.class.js +0 -124
  183. package/fesm/interceptors/query-performance.interceptor.js +0 -56
  184. package/fesm/interceptors/set-create-by-on-body.interceptor.js +0 -30
  185. package/fesm/interceptors/set-delete-by-on-body.interceptor.js +0 -30
  186. package/fesm/interceptors/set-update-by-on-body.interceptor.js +0 -30
  187. package/fesm/interfaces/base-query.interface.js +0 -3
  188. package/fesm/interfaces/logger.interface.js +0 -4
  189. package/fesm/modules/datasource/datasource.module.js +0 -110
  190. package/fesm/utils/error-handler.util.js +0 -59
  191. package/interceptors/query-performance.interceptor.d.ts +0 -8
  192. package/interceptors/set-create-by-on-body.interceptor.d.ts +0 -5
  193. package/interceptors/set-delete-by-on-body.interceptor.d.ts +0 -5
  194. package/interceptors/set-update-by-on-body.interceptor.d.ts +0 -5
  195. package/interfaces/base-query.interface.d.ts +0 -7
  196. package/interfaces/logger.interface.d.ts +0 -7
  197. package/modules/datasource/datasource.module.d.ts +0 -18
  198. package/utils/error-handler.util.d.ts +0 -24
@@ -26,243 +26,135 @@ 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, Logger, Optional, Scope } from '@nestjs/common';
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';
33
34
  import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
34
35
  export class MultiTenantDataSourceService {
35
- // Initialization
36
+ get self() {
37
+ return this.constructor;
38
+ }
36
39
  initializeFromOptions() {
37
40
  if (!this.options) return;
38
- if (!MultiTenantDataSourceService.initialized) {
41
+ if (!this.self.initialized) {
39
42
  this.registerTenants();
40
- MultiTenantDataSourceService.initialized = true;
43
+ this.self.initialized = true;
41
44
  }
42
45
  }
43
46
  registerTenants() {
44
47
  this.options?.tenants?.forEach((tenant)=>{
45
- MultiTenantDataSourceService.tenantsRegistry.set(tenant.id, tenant);
48
+ this.self.tenantsRegistry.set(tenant.id, tenant);
46
49
  });
47
50
  }
48
- // Public API
49
- /**
50
- * Set custom tenant header name
51
- */ setTenantHeader(header) {
52
- this.tenantHeader = header;
53
- }
54
- /**
55
- * Get current database mode
56
- */ getDatabaseMode() {
51
+ getDatabaseMode() {
57
52
  return this.options?.bootstrapAppConfig?.databaseMode ?? 'single';
58
53
  }
59
- /**
60
- * Check if running in multi-tenant mode
61
- */ isMultiTenant() {
54
+ isMultiTenant() {
62
55
  return this.getDatabaseMode() === 'multi-tenant';
63
56
  }
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() {
57
+ getCurrentTenantId() {
69
58
  if (!this.request) return null;
70
59
  const tenantId = this.request.headers[this.tenantHeader];
71
60
  if (!tenantId) return null;
72
- // Validate tenant ID format to prevent injection attacks
73
61
  if (!/^[a-zA-Z0-9_-]+$/.test(tenantId)) {
74
- throw new BadRequestException('Invalid tenant ID format. Only alphanumeric characters, hyphens, and underscores are allowed.');
62
+ throw new BadRequestException({
63
+ message: 'Invalid tenant ID format',
64
+ messageKey: SYSTEM_MESSAGES.INVALID_TENANT_ID
65
+ });
75
66
  }
76
67
  return tenantId;
77
68
  }
78
- /**
79
- * Get current tenant config from request header
80
- */ getCurrentTenant() {
69
+ getCurrentTenant() {
81
70
  const tenantId = this.getCurrentTenantId();
82
71
  return tenantId ? this.getTenant(tenantId) : null;
83
72
  }
84
- /**
85
- * Get tenant config by ID
86
- */ getTenant(tenantId) {
87
- return MultiTenantDataSourceService.tenantsRegistry.get(tenantId) ?? null;
88
- }
89
- /**
90
- * Get all registered tenants
91
- */ getAllTenants() {
92
- return Array.from(MultiTenantDataSourceService.tenantsRegistry.values());
73
+ getTenant(tenantId) {
74
+ return this.self.tenantsRegistry.get(tenantId) ?? null;
93
75
  }
94
- /**
95
- * Get only active tenants
96
- */ getActiveTenants() {
97
- return this.getAllTenants();
98
- }
99
- // DataSource Access
100
- /**
101
- * Get DataSource for current context (tenant or single)
102
- */ async getDataSource() {
76
+ async getDataSource() {
103
77
  return this.isMultiTenant() ? this.getTenantDataSource() : this.getSingleDataSource();
104
78
  }
105
- /**
106
- * Get DataSource for specific tenant
107
- */ async getDataSourceForTenant(tenantId) {
108
- const tenant = this.getTenant(tenantId);
109
- if (!tenant) {
110
- throw new Error(`Tenant '${tenantId}' not found`);
111
- }
112
- return this.getOrCreateTenantConnection(tenant);
113
- }
114
- /**
115
- * Set external DataSource (for single-tenant mode)
116
- */ setDataSource(dataSource) {
117
- MultiTenantDataSourceService.singleDataSource = dataSource;
118
- }
119
- // Repository Access
120
- /**
121
- * Get repository for entity in current context
122
- */ async getRepository(entity) {
79
+ async getRepository(entity) {
123
80
  const dataSource = await this.getDataSource();
124
81
  return dataSource.getRepository(entity);
125
82
  }
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) {
136
- const dataSource = await this.getDataSourceForTenant(tenantId);
137
- return callback(dataSource);
138
- }
139
- /**
140
- * Execute callback for all active tenants
141
- */ async forAllTenants(callback) {
142
- const results = new Map();
143
- for (const tenant of this.getActiveTenants()){
144
- try {
145
- const dataSource = await this.getOrCreateTenantConnection(tenant);
146
- results.set(tenant.id, await callback(tenant, dataSource));
147
- } catch (error) {
148
- this.logger.error(`Error processing tenant ${tenant.id}: ${error}`);
149
- }
150
- }
151
- return results;
152
- }
153
- // Tenant Management
154
- /**
155
- * Register a new tenant at runtime
156
- */ registerTenant(tenant) {
157
- MultiTenantDataSourceService.tenantsRegistry.set(tenant.id, tenant);
158
- }
159
- /**
160
- * Remove tenant and close its connection
161
- */ async removeTenant(tenantId) {
162
- await this.closeTenantConnection(tenantId);
163
- MultiTenantDataSourceService.tenantsRegistry.delete(tenantId);
164
- }
165
- // Connection Lifecycle
166
- /**
167
- * Close specific tenant connection
168
- */ async closeTenantConnection(tenantId) {
169
- const connection = MultiTenantDataSourceService.tenantConnections.get(tenantId);
83
+ async closeTenantConnection(tenantId) {
84
+ const connection = this.self.tenantConnections.get(tenantId);
170
85
  if (connection?.isInitialized) {
171
86
  await connection.destroy();
172
- MultiTenantDataSourceService.tenantConnections.delete(tenantId);
173
- this.logger.log(`Closed connection for tenant: ${tenantId}`);
87
+ this.self.tenantConnections.delete(tenantId);
174
88
  }
175
89
  }
176
- /**
177
- * Lifecycle hook - cleanup on module destroy
178
- */ async onModuleDestroy() {
179
- for (const [tenantId] of MultiTenantDataSourceService.tenantConnections){
90
+ async onModuleDestroy() {
91
+ for (const [tenantId] of this.self.tenantConnections){
180
92
  await this.closeTenantConnection(tenantId);
181
93
  }
182
94
  }
183
- /**
184
- * Reset all static state (useful for testing)
185
- */ static reset() {
186
- MultiTenantDataSourceService.initialized = false;
187
- MultiTenantDataSourceService.singleDataSource = null;
188
- MultiTenantDataSourceService.singleConnectionLock = null;
189
- MultiTenantDataSourceService.tenantConnections.clear();
190
- MultiTenantDataSourceService.tenantsRegistry.clear();
191
- MultiTenantDataSourceService.connectionLocks.clear();
192
- }
193
- // Protected Methods (for subclasses)
194
- /**
195
- * Get single database DataSource with connection locking to prevent race conditions
196
- */ async getSingleDataSource() {
197
- // Return existing initialized connection
198
- if (MultiTenantDataSourceService.singleDataSource?.isInitialized) {
199
- return MultiTenantDataSourceService.singleDataSource;
95
+ async getSingleDataSource() {
96
+ if (this.self.singleDataSource?.isInitialized) {
97
+ return this.self.singleDataSource;
200
98
  }
201
- // If another request is creating the connection, wait for it
202
- if (MultiTenantDataSourceService.singleConnectionLock) {
203
- return MultiTenantDataSourceService.singleConnectionLock;
99
+ if (this.self.singleConnectionLock) {
100
+ return this.self.singleConnectionLock;
204
101
  }
205
102
  const config = this.getDefaultDatabaseConfig();
206
103
  if (!config) {
207
- throw new Error('No database config available. Provide defaultDatabaseConfig or tenantDefaultDatabaseConfig.');
104
+ throw new InternalServerErrorException({
105
+ message: 'No database config available. Provide defaultDatabaseConfig or tenantDefaultDatabaseConfig.',
106
+ messageKey: SYSTEM_MESSAGES.DATABASE_CONFIG_NOT_AVAILABLE
107
+ });
208
108
  }
209
- // Create connection with lock to prevent race conditions
210
109
  const connectionPromise = this.createDataSourceFromConfig(config);
211
- MultiTenantDataSourceService.singleConnectionLock = connectionPromise;
110
+ this.self.singleConnectionLock = connectionPromise;
212
111
  try {
213
112
  const dataSource = await connectionPromise;
214
- MultiTenantDataSourceService.singleDataSource = dataSource;
113
+ this.self.singleDataSource = dataSource;
215
114
  return dataSource;
216
115
  } finally{
217
- MultiTenantDataSourceService.singleConnectionLock = null;
116
+ this.self.singleConnectionLock = null;
218
117
  }
219
118
  }
220
- /**
221
- * Get tenant-specific DataSource
222
- */ async getTenantDataSource() {
119
+ async getTenantDataSource() {
223
120
  const tenant = this.getCurrentTenant();
224
121
  if (!tenant) {
225
- throw new Error(`Tenant not found. Ensure '${this.tenantHeader}' header is set.`);
122
+ throw new BadRequestException({
123
+ message: `Tenant not found. Ensure '${this.tenantHeader}' header is set.`,
124
+ messageKey: SYSTEM_MESSAGES.TENANT_HEADER_REQUIRED,
125
+ messageVariables: {
126
+ header: this.tenantHeader
127
+ }
128
+ });
226
129
  }
227
130
  return this.getOrCreateTenantConnection(tenant);
228
131
  }
229
- /**
230
- * Get or create connection for tenant with locking to prevent race conditions
231
- */ async getOrCreateTenantConnection(tenant) {
232
- // Return existing initialized connection
233
- 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
238
- const pendingConnection = MultiTenantDataSourceService.connectionLocks.get(tenant.id);
239
- if (pendingConnection) {
240
- return pendingConnection;
241
- }
242
- // Create connection with lock to prevent race conditions
132
+ async getOrCreateTenantConnection(tenant) {
133
+ const existing = this.self.tenantConnections.get(tenant.id);
134
+ if (existing?.isInitialized) return existing;
135
+ const pendingConnection = this.self.connectionLocks.get(tenant.id);
136
+ if (pendingConnection) return pendingConnection;
243
137
  const config = this.buildTenantDatabaseConfig(tenant);
244
138
  const connectionPromise = this.createDataSourceFromConfig(config);
245
- MultiTenantDataSourceService.connectionLocks.set(tenant.id, connectionPromise);
139
+ this.self.connectionLocks.set(tenant.id, connectionPromise);
246
140
  try {
247
141
  const dataSource = await connectionPromise;
248
- MultiTenantDataSourceService.tenantConnections.set(tenant.id, dataSource);
249
- this.logger.log(`Created connection for tenant: ${tenant.id}`);
142
+ this.self.tenantConnections.set(tenant.id, dataSource);
250
143
  return dataSource;
251
144
  } finally{
252
- MultiTenantDataSourceService.connectionLocks.delete(tenant.id);
145
+ this.self.connectionLocks.delete(tenant.id);
253
146
  }
254
147
  }
255
- /**
256
- * Get default database config (supports both naming conventions)
257
- */ getDefaultDatabaseConfig() {
148
+ getDefaultDatabaseConfig() {
258
149
  return this.options?.defaultDatabaseConfig ?? this.options?.tenantDefaultDatabaseConfig;
259
150
  }
260
- /**
261
- * Build database config for tenant (merges with default)
262
- */ buildTenantDatabaseConfig(tenant) {
151
+ buildTenantDatabaseConfig(tenant) {
263
152
  const defaultConfig = this.getDefaultDatabaseConfig();
264
153
  if (!defaultConfig) {
265
- throw new Error('No default database config for multi-tenant mode.');
154
+ throw new InternalServerErrorException({
155
+ message: 'No default database config for multi-tenant mode.',
156
+ messageKey: SYSTEM_MESSAGES.DATABASE_CONFIG_NOT_AVAILABLE
157
+ });
266
158
  }
267
159
  return {
268
160
  type: defaultConfig.type,
@@ -273,15 +165,7 @@ export class MultiTenantDataSourceService {
273
165
  database: tenant.database
274
166
  };
275
167
  }
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 = []) {
168
+ async createDataSourceFromConfig(config, entities = []) {
285
169
  const dataSource = new DataSource({
286
170
  type: config.type,
287
171
  host: config.host,
@@ -293,30 +177,23 @@ export class MultiTenantDataSourceService {
293
177
  synchronize: false,
294
178
  namingStrategy: new SnakeNamingStrategy()
295
179
  });
296
- if (!dataSource.isInitialized) {
297
- await dataSource.initialize();
298
- }
180
+ if (!dataSource.isInitialized) await dataSource.initialize();
299
181
  return dataSource;
300
182
  }
301
183
  constructor(options, request){
302
184
  _define_property(this, "options", void 0);
303
185
  _define_property(this, "request", void 0);
304
- _define_property(this, "logger", void 0);
305
- // Instance state
306
186
  _define_property(this, "tenantHeader", void 0);
307
187
  this.options = options;
308
188
  this.request = request;
309
- this.logger = new Logger(this.constructor.name);
310
189
  this.tenantHeader = DEFAULT_TENANT_HEADER;
311
190
  this.initializeFromOptions();
312
191
  }
313
192
  }
314
- // Static state shared across all instances
315
193
  _define_property(MultiTenantDataSourceService, "tenantConnections", new Map());
316
194
  _define_property(MultiTenantDataSourceService, "singleDataSource", null);
317
195
  _define_property(MultiTenantDataSourceService, "tenantsRegistry", new Map());
318
196
  _define_property(MultiTenantDataSourceService, "initialized", false);
319
- // Connection locks to prevent race conditions during concurrent connection creation
320
197
  _define_property(MultiTenantDataSourceService, "connectionLocks", new Map());
321
198
  _define_property(MultiTenantDataSourceService, "singleConnectionLock", null);
322
199
  MultiTenantDataSourceService = _ts_decorate([
@@ -4,242 +4,63 @@ function _ts_decorate(decorators, target, key, desc) {
4
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;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  }
7
- function _ts_metadata(k, v) {
8
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
- }
10
7
  import { Injectable } from '@nestjs/common';
11
- import { isEmail } from 'class-validator';
12
8
  export class UtilsService {
13
9
  // ---------------- CACHE HELPERS ----------------
14
10
  /**
15
11
  * 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
12
  */ 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)}`;
13
+ const prefix = this.buildTenantPrefix(tenantId);
14
+ if (entityId) {
15
+ return `${prefix}entity_${entityName}_id_${entityId}${params ? '_select_' + JSON.stringify(params) : ''}`;
16
+ }
17
+ return `${prefix}entity_${entityName}_all_${JSON.stringify(params)}`;
24
18
  }
25
19
  /**
26
20
  * Track cache key for later invalidation with optional tenant prefix
27
21
  */ async trackCacheKey(cacheKey, entityName, cacheManager, entityId, tenantId) {
28
- const tenantPrefix = tenantId ? `tenant_${tenantId}_` : '';
29
22
  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
- }
41
- } 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);
23
+ const trackingKey = this.buildTrackingKey(entityName, entityId, tenantId);
24
+ const keys = await cacheManager.get(trackingKey) || [];
25
+ if (!keys.includes(cacheKey)) keys.push(cacheKey);
26
+ await cacheManager.set(trackingKey, keys);
27
+ } catch {
28
+ // Silent failure - cache tracking is non-critical
44
29
  }
45
30
  }
46
31
  /**
47
32
  * Clear cache for entity with optional tenant prefix
48
- * Uses Promise.allSettled to ensure all deletions are attempted even if some fail
49
33
  */ async clearCache(entityName, cacheManager, entityId, tenantId) {
50
- const tenantPrefix = tenantId ? `tenant_${tenantId}_` : '';
51
34
  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
- }
65
- } 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
- };
35
+ const trackingKey = this.buildTrackingKey(entityName, entityId, tenantId);
36
+ const keys = await cacheManager.get(trackingKey) || [];
37
+ await Promise.allSettled(keys.map((key)=>cacheManager.del(key)));
38
+ await cacheManager.del(trackingKey);
39
+ } catch {
40
+ // Silent failure - cache invalidation is non-critical
89
41
  }
90
- return {
91
- value: null,
92
- type: null
93
- };
94
42
  }
43
+ // ---------------- STRING HELPERS ----------------
95
44
  /**
96
- * STRING FUNCTIONS
97
- * transformToSlug
45
+ * Transform string to URL-friendly slug
98
46
  */ transformToSlug(value, salt) {
99
47
  const slug = value?.trim().replace(/[^A-Z0-9]+/gi, '-').toLowerCase();
100
48
  return salt ? `${slug}-${this.getRandomInt(1, 100)}` : slug;
101
49
  }
102
50
  /**
103
- * RANDOM FUNCTIONS
104
- * getRandomInt
105
- * generateRandomId
106
- * getRandomOtpCode
51
+ * Generate random integer between min and max (inclusive)
107
52
  */ getRandomInt(min, max) {
108
53
  return Math.floor(Math.random() * (max - min + 1)) + min;
109
54
  }
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;
120
- }
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
- `;
55
+ // ---------------- PRIVATE HELPERS ----------------
56
+ buildTenantPrefix(tenantId) {
57
+ return tenantId ? `tenant_${tenantId}_` : '';
183
58
  }
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
- `;
59
+ buildTrackingKey(entityName, entityId, tenantId) {
60
+ const prefix = this.buildTenantPrefix(tenantId);
61
+ return entityId ? `${prefix}entity_${entityName}_id_${entityId}_keys` : `${prefix}entity_${entityName}_keys`;
238
62
  }
239
- constructor(){}
240
63
  }
241
64
  UtilsService = _ts_decorate([
242
- Injectable(),
243
- _ts_metadata("design:type", Function),
244
- _ts_metadata("design:paramtypes", [])
65
+ Injectable()
245
66
  ], UtilsService);