@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.
- package/README.md +146 -1330
- package/cjs/classes/api-controller.class.js +325 -49
- package/cjs/classes/api-service.class.js +271 -149
- package/cjs/classes/handle-custome-db.js +37 -0
- package/cjs/classes/index.js +2 -2
- package/cjs/classes/winston.logger.class.js +103 -70
- package/cjs/constants/index.js +15 -4
- package/cjs/constants/message-keys.js +68 -0
- package/cjs/constants/permissions.js +287 -0
- package/cjs/decorators/api-response.decorator.js +2 -3
- package/cjs/decorators/index.js +1 -0
- package/cjs/decorators/log-action.decorator.js +149 -0
- package/cjs/decorators/require-permission.decorator.js +5 -5
- package/cjs/dtos/delete.dto.js +10 -0
- package/cjs/dtos/filter-and-pagination.dto.js +24 -34
- package/cjs/dtos/get-by-ids.dto.js +65 -0
- package/cjs/dtos/identity-response.dto.js +4 -8
- package/cjs/dtos/index.js +1 -0
- package/cjs/dtos/pagination.dto.js +4 -8
- package/cjs/dtos/response-payload.dto.js +68 -112
- package/cjs/entities/identity.js +12 -6
- package/cjs/entities/index.js +1 -0
- package/cjs/entities/raw-type.js +4 -0
- package/cjs/entities/user-root.js +15 -15
- package/cjs/enums/index.js +20 -0
- package/cjs/enums/notification-type.enum.js +17 -0
- package/cjs/enums/participant-status.enum.js +17 -0
- package/cjs/enums/recurrence-type.enum.js +18 -0
- package/cjs/exceptions/base-app.exception.js +151 -0
- package/cjs/exceptions/index.js +1 -0
- package/cjs/exceptions/permission.exception.js +13 -9
- package/cjs/filters/global-exception.filter.js +279 -0
- package/cjs/filters/index.js +18 -0
- package/cjs/guards/jwt-auth.guard.js +4 -1
- package/cjs/guards/permission.guard.js +88 -148
- package/cjs/index.js +2 -0
- package/cjs/interceptors/idempotency.interceptor.js +1 -1
- package/cjs/interceptors/index.js +1 -4
- 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/event-manager-adapter.interface.js +11 -0
- package/cjs/interfaces/index.js +4 -2
- package/cjs/interfaces/module-config.interface.js +4 -0
- package/cjs/interfaces/notification-adapter.interface.js +11 -0
- package/cjs/interfaces/permission.interface.js +1 -1
- package/cjs/middlewares/logger.middleware.js +133 -115
- package/cjs/modules/cache/cache.module.js +3 -3
- package/cjs/modules/datasource/index.js +0 -1
- package/cjs/modules/datasource/multi-tenant-datasource.service.js +60 -183
- package/cjs/modules/utils/utils.service.js +27 -206
- package/cjs/utils/date-time.util.js +94 -0
- package/cjs/utils/html-sanitizer.util.js +64 -0
- package/cjs/utils/index.js +5 -1
- package/cjs/utils/query-helpers.util.js +60 -0
- package/cjs/utils/request.util.js +96 -0
- package/cjs/utils/string.util.js +63 -0
- package/classes/api-controller.class.d.ts +16 -8
- package/classes/api-service.class.d.ts +30 -25
- package/classes/handle-custome-db.d.ts +3 -0
- package/classes/index.d.ts +2 -2
- package/classes/winston.logger.class.d.ts +1 -0
- package/constants/index.d.ts +2 -1
- package/constants/message-keys.d.ts +45 -0
- package/constants/permissions.d.ts +306 -0
- package/decorators/api-response.decorator.d.ts +1 -1
- package/decorators/current-user.decorator.d.ts +1 -1
- package/decorators/index.d.ts +1 -0
- package/decorators/log-action.decorator.d.ts +8 -0
- package/decorators/require-permission.decorator.d.ts +2 -2
- package/dtos/delete.dto.d.ts +1 -0
- package/dtos/filter-and-pagination.dto.d.ts +0 -2
- package/dtos/get-by-ids.dto.d.ts +4 -0
- package/dtos/index.d.ts +1 -0
- package/dtos/response-payload.dto.d.ts +8 -20
- package/entities/index.d.ts +1 -0
- package/entities/raw-type.d.ts +1 -0
- package/enums/index.d.ts +3 -0
- package/enums/notification-type.enum.d.ts +6 -0
- package/enums/participant-status.enum.d.ts +6 -0
- package/enums/recurrence-type.enum.d.ts +7 -0
- package/exceptions/base-app.exception.d.ts +41 -0
- package/exceptions/index.d.ts +1 -0
- package/exceptions/permission.exception.d.ts +2 -2
- package/fesm/classes/api-controller.class.js +326 -50
- package/fesm/classes/api-service.class.js +274 -152
- package/fesm/classes/handle-custome-db.js +19 -0
- package/fesm/classes/index.js +3 -2
- package/fesm/classes/winston.logger.class.js +100 -68
- package/fesm/constants/index.js +4 -1
- package/fesm/constants/message-keys.js +49 -0
- package/fesm/constants/permissions.js +200 -0
- package/fesm/decorators/api-response.decorator.js +3 -21
- package/fesm/decorators/index.js +1 -0
- package/fesm/decorators/log-action.decorator.js +139 -0
- package/fesm/decorators/require-permission.decorator.js +17 -65
- package/fesm/dtos/delete.dto.js +12 -2
- package/fesm/dtos/filter-and-pagination.dto.js +26 -47
- package/fesm/dtos/get-by-ids.dto.js +55 -0
- package/fesm/dtos/identity-response.dto.js +4 -8
- package/fesm/dtos/index.js +1 -0
- package/fesm/dtos/pagination.dto.js +4 -8
- package/fesm/dtos/response-payload.dto.js +68 -103
- package/fesm/entities/identity.js +12 -6
- package/fesm/entities/index.js +1 -0
- package/fesm/entities/raw-type.js +1 -0
- package/fesm/entities/user-root.js +15 -15
- package/fesm/enums/index.js +3 -0
- package/fesm/enums/notification-type.enum.js +7 -0
- package/fesm/enums/participant-status.enum.js +7 -0
- package/fesm/enums/recurrence-type.enum.js +8 -0
- package/fesm/exceptions/base-app.exception.js +115 -0
- package/fesm/exceptions/index.js +1 -0
- package/fesm/exceptions/permission.exception.js +16 -18
- package/fesm/filters/global-exception.filter.js +269 -0
- package/fesm/filters/index.js +1 -0
- package/fesm/guards/jwt-auth.guard.js +5 -2
- package/fesm/guards/permission.guard.js +90 -150
- package/fesm/index.js +2 -0
- package/fesm/interceptors/idempotency.interceptor.js +2 -2
- package/fesm/interceptors/index.js +1 -4
- 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 +1 -0
- package/fesm/interfaces/event-manager-adapter.interface.js +1 -0
- package/fesm/interfaces/index.js +4 -2
- package/fesm/interfaces/module-config.interface.js +5 -0
- package/fesm/interfaces/notification-adapter.interface.js +1 -0
- package/fesm/interfaces/permission.interface.js +1 -3
- package/fesm/middlewares/logger.middleware.js +133 -109
- package/fesm/modules/cache/cache.module.js +2 -2
- package/fesm/modules/datasource/index.js +0 -1
- package/fesm/modules/datasource/multi-tenant-datasource.service.js +61 -184
- package/fesm/modules/utils/utils.service.js +27 -206
- package/fesm/utils/date-time.util.js +70 -0
- package/fesm/utils/html-sanitizer.util.js +69 -0
- package/fesm/utils/index.js +5 -1
- package/fesm/utils/query-helpers.util.js +85 -0
- package/fesm/utils/request.util.js +81 -0
- package/fesm/utils/string.util.js +71 -0
- package/filters/global-exception.filter.d.ts +9 -0
- package/filters/index.d.ts +1 -0
- package/guards/permission.guard.d.ts +4 -6
- package/index.d.ts +2 -0
- package/interceptors/idempotency.interceptor.d.ts +1 -1
- package/interceptors/index.d.ts +1 -4
- 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 +4 -2
- package/interfaces/datasource.interface.d.ts +5 -0
- package/interfaces/event-manager-adapter.interface.d.ts +43 -0
- package/interfaces/identity.interface.d.ts +4 -4
- package/interfaces/index.d.ts +4 -2
- package/interfaces/logged-user-info.interface.d.ts +0 -2
- package/interfaces/module-config.interface.d.ts +6 -0
- package/interfaces/notification-adapter.interface.d.ts +21 -0
- package/interfaces/permission.interface.d.ts +13 -9
- package/middlewares/logger.middleware.d.ts +2 -2
- package/modules/datasource/index.d.ts +0 -1
- package/modules/datasource/multi-tenant-datasource.service.d.ts +5 -16
- package/modules/utils/utils.service.d.ts +3 -14
- package/package.json +21 -14
- package/utils/date-time.util.d.ts +8 -0
- package/utils/html-sanitizer.util.d.ts +2 -0
- package/utils/index.d.ts +5 -1
- package/utils/query-helpers.util.d.ts +16 -0
- package/utils/request.util.d.ts +6 -0
- package/utils/string.util.d.ts +2 -0
- package/cjs/classes/request-scoped-api.service.js +0 -61
- package/cjs/classes/winston-logger-adapter.class.js +0 -121
- package/cjs/interceptors/query-performance.interceptor.js +0 -66
- 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/cjs/interfaces/logger.interface.js +0 -7
- package/cjs/modules/datasource/datasource.module.js +0 -120
- package/cjs/utils/error-handler.util.js +0 -67
- package/classes/request-scoped-api.service.d.ts +0 -13
- package/classes/winston-logger-adapter.class.d.ts +0 -20
- package/fesm/classes/request-scoped-api.service.js +0 -51
- package/fesm/classes/winston-logger-adapter.class.js +0 -124
- package/fesm/interceptors/query-performance.interceptor.js +0 -56
- 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/fesm/interfaces/logger.interface.js +0 -4
- package/fesm/modules/datasource/datasource.module.js +0 -110
- package/fesm/utils/error-handler.util.js +0 -59
- package/interceptors/query-performance.interceptor.d.ts +0 -8
- 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
- package/interfaces/logger.interface.d.ts +0 -7
- package/modules/datasource/datasource.module.d.ts +0 -18
- 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,
|
|
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
|
-
|
|
36
|
+
get self() {
|
|
37
|
+
return this.constructor;
|
|
38
|
+
}
|
|
36
39
|
initializeFromOptions() {
|
|
37
40
|
if (!this.options) return;
|
|
38
|
-
if (!
|
|
41
|
+
if (!this.self.initialized) {
|
|
39
42
|
this.registerTenants();
|
|
40
|
-
|
|
43
|
+
this.self.initialized = true;
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
registerTenants() {
|
|
44
47
|
this.options?.tenants?.forEach((tenant)=>{
|
|
45
|
-
|
|
48
|
+
this.self.tenantsRegistry.set(tenant.id, tenant);
|
|
46
49
|
});
|
|
47
50
|
}
|
|
48
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
173
|
-
this.logger.log(`Closed connection for tenant: ${tenantId}`);
|
|
87
|
+
this.self.tenantConnections.delete(tenantId);
|
|
174
88
|
}
|
|
175
89
|
}
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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
|
-
|
|
202
|
-
|
|
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
|
|
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
|
-
|
|
110
|
+
this.self.singleConnectionLock = connectionPromise;
|
|
212
111
|
try {
|
|
213
112
|
const dataSource = await connectionPromise;
|
|
214
|
-
|
|
113
|
+
this.self.singleDataSource = dataSource;
|
|
215
114
|
return dataSource;
|
|
216
115
|
} finally{
|
|
217
|
-
|
|
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
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
139
|
+
this.self.connectionLocks.set(tenant.id, connectionPromise);
|
|
246
140
|
try {
|
|
247
141
|
const dataSource = await connectionPromise;
|
|
248
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
22
|
-
if (entityId)
|
|
23
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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);
|