@flusys/nestjs-shared 1.1.0-beta → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +501 -720
- package/cjs/classes/api-controller.class.js +9 -24
- package/cjs/classes/api-service.class.js +59 -92
- package/cjs/classes/index.js +1 -0
- package/cjs/classes/winston-logger-adapter.class.js +23 -40
- package/cjs/constants/index.js +14 -0
- package/cjs/constants/permissions.js +184 -0
- package/cjs/decorators/api-response.decorator.js +1 -1
- package/cjs/decorators/index.js +1 -0
- package/cjs/decorators/sanitize-html.decorator.js +36 -0
- package/cjs/dtos/delete.dto.js +10 -0
- package/cjs/dtos/filter-and-pagination.dto.js +24 -34
- package/cjs/dtos/pagination.dto.js +4 -8
- package/cjs/dtos/response-payload.dto.js +0 -116
- package/cjs/entities/identity.js +4 -4
- package/cjs/entities/user-root.js +13 -14
- package/cjs/guards/permission.guard.js +51 -105
- package/cjs/interceptors/index.js +1 -3
- package/cjs/interceptors/set-user-field-on-body.interceptor.js +60 -0
- package/cjs/interceptors/slug.interceptor.js +30 -9
- package/cjs/interfaces/datasource.interface.js +4 -0
- package/cjs/interfaces/index.js +2 -1
- package/cjs/interfaces/module-config.interface.js +4 -0
- package/cjs/middlewares/logger.middleware.js +50 -89
- package/cjs/modules/cache/cache.module.js +3 -3
- package/cjs/modules/datasource/datasource.module.js +11 -14
- package/cjs/modules/datasource/multi-tenant-datasource.service.js +29 -113
- package/cjs/modules/utils/utils.service.js +40 -203
- package/cjs/utils/error-handler.util.js +35 -12
- package/cjs/utils/html-sanitizer.util.js +64 -0
- package/cjs/utils/index.js +4 -0
- package/cjs/utils/query-helpers.util.js +53 -0
- package/cjs/utils/request.util.js +70 -0
- package/cjs/utils/string.util.js +63 -0
- package/classes/api-controller.class.d.ts +5 -5
- package/classes/api-service.class.d.ts +7 -5
- package/classes/index.d.ts +1 -0
- package/classes/request-scoped-api.service.d.ts +3 -2
- package/classes/winston-logger-adapter.class.d.ts +2 -0
- package/constants/index.d.ts +1 -0
- package/constants/permissions.d.ts +179 -0
- package/decorators/index.d.ts +1 -0
- package/decorators/sanitize-html.decorator.d.ts +2 -0
- package/dtos/delete.dto.d.ts +1 -0
- package/dtos/filter-and-pagination.dto.d.ts +0 -2
- package/dtos/response-payload.dto.d.ts +0 -20
- package/fesm/classes/api-controller.class.js +9 -24
- package/fesm/classes/api-service.class.js +59 -92
- package/fesm/classes/index.js +2 -0
- package/fesm/classes/winston-logger-adapter.class.js +23 -40
- package/fesm/constants/index.js +2 -0
- package/fesm/constants/permissions.js +128 -0
- package/fesm/decorators/api-response.decorator.js +1 -1
- package/fesm/decorators/index.js +1 -0
- package/fesm/decorators/sanitize-html.decorator.js +45 -0
- package/fesm/dtos/delete.dto.js +12 -2
- package/fesm/dtos/filter-and-pagination.dto.js +26 -47
- package/fesm/dtos/pagination.dto.js +4 -8
- package/fesm/dtos/response-payload.dto.js +0 -107
- package/fesm/entities/identity.js +4 -4
- package/fesm/entities/user-root.js +13 -14
- package/fesm/guards/permission.guard.js +51 -105
- package/fesm/interceptors/index.js +1 -3
- package/fesm/interceptors/set-user-field-on-body.interceptor.js +39 -0
- package/fesm/interceptors/slug.interceptor.js +31 -10
- package/fesm/interfaces/datasource.interface.js +20 -0
- package/fesm/interfaces/index.js +2 -1
- package/fesm/interfaces/module-config.interface.js +5 -0
- package/fesm/middlewares/logger.middleware.js +50 -83
- package/fesm/modules/cache/cache.module.js +2 -2
- package/fesm/modules/datasource/datasource.module.js +11 -14
- package/fesm/modules/datasource/multi-tenant-datasource.service.js +29 -113
- package/fesm/modules/utils/utils.service.js +41 -204
- package/fesm/utils/error-handler.util.js +36 -13
- package/fesm/utils/html-sanitizer.util.js +69 -0
- package/fesm/utils/index.js +4 -0
- package/fesm/utils/query-helpers.util.js +78 -0
- package/fesm/utils/request.util.js +58 -0
- package/fesm/utils/string.util.js +71 -0
- package/guards/permission.guard.d.ts +2 -0
- package/interceptors/index.d.ts +1 -3
- package/interceptors/set-user-field-on-body.interceptor.d.ts +5 -0
- package/interceptors/slug.interceptor.d.ts +2 -1
- package/interfaces/api.interface.d.ts +2 -2
- package/interfaces/datasource.interface.d.ts +5 -0
- package/interfaces/identity.interface.d.ts +4 -4
- package/interfaces/index.d.ts +2 -1
- package/interfaces/logged-user-info.interface.d.ts +0 -2
- package/interfaces/module-config.interface.d.ts +6 -0
- package/interfaces/permission.interface.d.ts +0 -1
- package/middlewares/logger.middleware.d.ts +2 -2
- package/modules/datasource/datasource.module.d.ts +1 -0
- package/modules/datasource/multi-tenant-datasource.service.d.ts +0 -1
- package/modules/utils/utils.service.d.ts +4 -14
- package/package.json +4 -4
- package/utils/error-handler.util.d.ts +14 -19
- package/utils/html-sanitizer.util.d.ts +2 -0
- package/utils/index.d.ts +4 -0
- package/utils/query-helpers.util.d.ts +16 -0
- package/utils/request.util.d.ts +4 -0
- package/utils/string.util.d.ts +2 -0
- package/cjs/interceptors/set-create-by-on-body.interceptor.js +0 -40
- package/cjs/interceptors/set-delete-by-on-body.interceptor.js +0 -40
- package/cjs/interceptors/set-update-by-on-body.interceptor.js +0 -40
- package/cjs/interfaces/base-query.interface.js +0 -6
- package/fesm/interceptors/set-create-by-on-body.interceptor.js +0 -30
- package/fesm/interceptors/set-delete-by-on-body.interceptor.js +0 -30
- package/fesm/interceptors/set-update-by-on-body.interceptor.js +0 -30
- package/fesm/interfaces/base-query.interface.js +0 -3
- package/interceptors/set-create-by-on-body.interceptor.d.ts +0 -5
- package/interceptors/set-delete-by-on-body.interceptor.d.ts +0 -5
- package/interceptors/set-update-by-on-body.interceptor.d.ts +0 -5
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
22
|
-
if (entityId)
|
|
23
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
122
|
-
const
|
|
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
|
-
|
|
185
|
-
|
|
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
|
-
*
|
|
17
|
-
*/ static
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|