@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
|
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "MultiTenantDataSourceService", {
|
|
|
10
10
|
});
|
|
11
11
|
const _nestjscore = require("@flusys/nestjs-core");
|
|
12
12
|
const _common = require("@nestjs/common");
|
|
13
|
+
const _constants = require("../../constants");
|
|
13
14
|
const _core = require("@nestjs/core");
|
|
14
15
|
const _express = require("express");
|
|
15
16
|
const _typeorm = require("typeorm");
|
|
@@ -42,237 +43,128 @@ function _ts_param(paramIndex, decorator) {
|
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
45
|
let MultiTenantDataSourceService = class MultiTenantDataSourceService {
|
|
45
|
-
|
|
46
|
+
get self() {
|
|
47
|
+
return this.constructor;
|
|
48
|
+
}
|
|
46
49
|
initializeFromOptions() {
|
|
47
50
|
if (!this.options) return;
|
|
48
|
-
if (!
|
|
51
|
+
if (!this.self.initialized) {
|
|
49
52
|
this.registerTenants();
|
|
50
|
-
|
|
53
|
+
this.self.initialized = true;
|
|
51
54
|
}
|
|
52
55
|
}
|
|
53
56
|
registerTenants() {
|
|
54
57
|
this.options?.tenants?.forEach((tenant)=>{
|
|
55
|
-
|
|
58
|
+
this.self.tenantsRegistry.set(tenant.id, tenant);
|
|
56
59
|
});
|
|
57
60
|
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Set custom tenant header name
|
|
61
|
-
*/ setTenantHeader(header) {
|
|
62
|
-
this.tenantHeader = header;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Get current database mode
|
|
66
|
-
*/ getDatabaseMode() {
|
|
61
|
+
getDatabaseMode() {
|
|
67
62
|
return this.options?.bootstrapAppConfig?.databaseMode ?? 'single';
|
|
68
63
|
}
|
|
69
|
-
|
|
70
|
-
* Check if running in multi-tenant mode
|
|
71
|
-
*/ isMultiTenant() {
|
|
64
|
+
isMultiTenant() {
|
|
72
65
|
return this.getDatabaseMode() === 'multi-tenant';
|
|
73
66
|
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Get current tenant ID from request header
|
|
77
|
-
* Validates tenant ID format for security (alphanumeric, hyphens, underscores only)
|
|
78
|
-
*/ getCurrentTenantId() {
|
|
67
|
+
getCurrentTenantId() {
|
|
79
68
|
if (!this.request) return null;
|
|
80
69
|
const tenantId = this.request.headers[this.tenantHeader];
|
|
81
70
|
if (!tenantId) return null;
|
|
82
|
-
// Validate tenant ID format to prevent injection attacks
|
|
83
71
|
if (!/^[a-zA-Z0-9_-]+$/.test(tenantId)) {
|
|
84
|
-
throw new _common.BadRequestException(
|
|
72
|
+
throw new _common.BadRequestException({
|
|
73
|
+
message: 'Invalid tenant ID format',
|
|
74
|
+
messageKey: _constants.SYSTEM_MESSAGES.INVALID_TENANT_ID
|
|
75
|
+
});
|
|
85
76
|
}
|
|
86
77
|
return tenantId;
|
|
87
78
|
}
|
|
88
|
-
|
|
89
|
-
* Get current tenant config from request header
|
|
90
|
-
*/ getCurrentTenant() {
|
|
79
|
+
getCurrentTenant() {
|
|
91
80
|
const tenantId = this.getCurrentTenantId();
|
|
92
81
|
return tenantId ? this.getTenant(tenantId) : null;
|
|
93
82
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
*/ getTenant(tenantId) {
|
|
97
|
-
return MultiTenantDataSourceService.tenantsRegistry.get(tenantId) ?? null;
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Get all registered tenants
|
|
101
|
-
*/ getAllTenants() {
|
|
102
|
-
return Array.from(MultiTenantDataSourceService.tenantsRegistry.values());
|
|
83
|
+
getTenant(tenantId) {
|
|
84
|
+
return this.self.tenantsRegistry.get(tenantId) ?? null;
|
|
103
85
|
}
|
|
104
|
-
|
|
105
|
-
* Get only active tenants
|
|
106
|
-
*/ getActiveTenants() {
|
|
107
|
-
return this.getAllTenants();
|
|
108
|
-
}
|
|
109
|
-
// DataSource Access
|
|
110
|
-
/**
|
|
111
|
-
* Get DataSource for current context (tenant or single)
|
|
112
|
-
*/ async getDataSource() {
|
|
86
|
+
async getDataSource() {
|
|
113
87
|
return this.isMultiTenant() ? this.getTenantDataSource() : this.getSingleDataSource();
|
|
114
88
|
}
|
|
115
|
-
|
|
116
|
-
* Get DataSource for specific tenant
|
|
117
|
-
*/ async getDataSourceForTenant(tenantId) {
|
|
118
|
-
const tenant = this.getTenant(tenantId);
|
|
119
|
-
if (!tenant) {
|
|
120
|
-
throw new Error(`Tenant '${tenantId}' not found`);
|
|
121
|
-
}
|
|
122
|
-
return this.getOrCreateTenantConnection(tenant);
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Set external DataSource (for single-tenant mode)
|
|
126
|
-
*/ setDataSource(dataSource) {
|
|
127
|
-
MultiTenantDataSourceService.singleDataSource = dataSource;
|
|
128
|
-
}
|
|
129
|
-
// Repository Access
|
|
130
|
-
/**
|
|
131
|
-
* Get repository for entity in current context
|
|
132
|
-
*/ async getRepository(entity) {
|
|
89
|
+
async getRepository(entity) {
|
|
133
90
|
const dataSource = await this.getDataSource();
|
|
134
91
|
return dataSource.getRepository(entity);
|
|
135
92
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
*/ async getRepositoryForTenant(entity, tenantId) {
|
|
139
|
-
const dataSource = await this.getDataSourceForTenant(tenantId);
|
|
140
|
-
return dataSource.getRepository(entity);
|
|
141
|
-
}
|
|
142
|
-
// Multi-Tenant Operations
|
|
143
|
-
/**
|
|
144
|
-
* Execute callback with specific tenant's DataSource
|
|
145
|
-
*/ async withTenant(tenantId, callback) {
|
|
146
|
-
const dataSource = await this.getDataSourceForTenant(tenantId);
|
|
147
|
-
return callback(dataSource);
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Execute callback for all active tenants
|
|
151
|
-
*/ async forAllTenants(callback) {
|
|
152
|
-
const results = new Map();
|
|
153
|
-
for (const tenant of this.getActiveTenants()){
|
|
154
|
-
try {
|
|
155
|
-
const dataSource = await this.getOrCreateTenantConnection(tenant);
|
|
156
|
-
results.set(tenant.id, await callback(tenant, dataSource));
|
|
157
|
-
} catch (error) {
|
|
158
|
-
this.logger.error(`Error processing tenant ${tenant.id}: ${error}`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return results;
|
|
162
|
-
}
|
|
163
|
-
// Tenant Management
|
|
164
|
-
/**
|
|
165
|
-
* Register a new tenant at runtime
|
|
166
|
-
*/ registerTenant(tenant) {
|
|
167
|
-
MultiTenantDataSourceService.tenantsRegistry.set(tenant.id, tenant);
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Remove tenant and close its connection
|
|
171
|
-
*/ async removeTenant(tenantId) {
|
|
172
|
-
await this.closeTenantConnection(tenantId);
|
|
173
|
-
MultiTenantDataSourceService.tenantsRegistry.delete(tenantId);
|
|
174
|
-
}
|
|
175
|
-
// Connection Lifecycle
|
|
176
|
-
/**
|
|
177
|
-
* Close specific tenant connection
|
|
178
|
-
*/ async closeTenantConnection(tenantId) {
|
|
179
|
-
const connection = MultiTenantDataSourceService.tenantConnections.get(tenantId);
|
|
93
|
+
async closeTenantConnection(tenantId) {
|
|
94
|
+
const connection = this.self.tenantConnections.get(tenantId);
|
|
180
95
|
if (connection?.isInitialized) {
|
|
181
96
|
await connection.destroy();
|
|
182
|
-
|
|
183
|
-
this.logger.log(`Closed connection for tenant: ${tenantId}`);
|
|
97
|
+
this.self.tenantConnections.delete(tenantId);
|
|
184
98
|
}
|
|
185
99
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
*/ async onModuleDestroy() {
|
|
189
|
-
for (const [tenantId] of MultiTenantDataSourceService.tenantConnections){
|
|
100
|
+
async onModuleDestroy() {
|
|
101
|
+
for (const [tenantId] of this.self.tenantConnections){
|
|
190
102
|
await this.closeTenantConnection(tenantId);
|
|
191
103
|
}
|
|
192
104
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
MultiTenantDataSourceService.initialized = false;
|
|
197
|
-
MultiTenantDataSourceService.singleDataSource = null;
|
|
198
|
-
MultiTenantDataSourceService.singleConnectionLock = null;
|
|
199
|
-
MultiTenantDataSourceService.tenantConnections.clear();
|
|
200
|
-
MultiTenantDataSourceService.tenantsRegistry.clear();
|
|
201
|
-
MultiTenantDataSourceService.connectionLocks.clear();
|
|
202
|
-
}
|
|
203
|
-
// Protected Methods (for subclasses)
|
|
204
|
-
/**
|
|
205
|
-
* Get single database DataSource with connection locking to prevent race conditions
|
|
206
|
-
*/ async getSingleDataSource() {
|
|
207
|
-
// Return existing initialized connection
|
|
208
|
-
if (MultiTenantDataSourceService.singleDataSource?.isInitialized) {
|
|
209
|
-
return MultiTenantDataSourceService.singleDataSource;
|
|
105
|
+
async getSingleDataSource() {
|
|
106
|
+
if (this.self.singleDataSource?.isInitialized) {
|
|
107
|
+
return this.self.singleDataSource;
|
|
210
108
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return MultiTenantDataSourceService.singleConnectionLock;
|
|
109
|
+
if (this.self.singleConnectionLock) {
|
|
110
|
+
return this.self.singleConnectionLock;
|
|
214
111
|
}
|
|
215
112
|
const config = this.getDefaultDatabaseConfig();
|
|
216
113
|
if (!config) {
|
|
217
|
-
throw new
|
|
114
|
+
throw new _common.InternalServerErrorException({
|
|
115
|
+
message: 'No database config available. Provide defaultDatabaseConfig or tenantDefaultDatabaseConfig.',
|
|
116
|
+
messageKey: _constants.SYSTEM_MESSAGES.DATABASE_CONFIG_NOT_AVAILABLE
|
|
117
|
+
});
|
|
218
118
|
}
|
|
219
|
-
// Create connection with lock to prevent race conditions
|
|
220
119
|
const connectionPromise = this.createDataSourceFromConfig(config);
|
|
221
|
-
|
|
120
|
+
this.self.singleConnectionLock = connectionPromise;
|
|
222
121
|
try {
|
|
223
122
|
const dataSource = await connectionPromise;
|
|
224
|
-
|
|
123
|
+
this.self.singleDataSource = dataSource;
|
|
225
124
|
return dataSource;
|
|
226
125
|
} finally{
|
|
227
|
-
|
|
126
|
+
this.self.singleConnectionLock = null;
|
|
228
127
|
}
|
|
229
128
|
}
|
|
230
|
-
|
|
231
|
-
* Get tenant-specific DataSource
|
|
232
|
-
*/ async getTenantDataSource() {
|
|
129
|
+
async getTenantDataSource() {
|
|
233
130
|
const tenant = this.getCurrentTenant();
|
|
234
131
|
if (!tenant) {
|
|
235
|
-
throw new
|
|
132
|
+
throw new _common.BadRequestException({
|
|
133
|
+
message: `Tenant not found. Ensure '${this.tenantHeader}' header is set.`,
|
|
134
|
+
messageKey: _constants.SYSTEM_MESSAGES.TENANT_HEADER_REQUIRED,
|
|
135
|
+
messageVariables: {
|
|
136
|
+
header: this.tenantHeader
|
|
137
|
+
}
|
|
138
|
+
});
|
|
236
139
|
}
|
|
237
140
|
return this.getOrCreateTenantConnection(tenant);
|
|
238
141
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (existing?.isInitialized) {
|
|
245
|
-
return existing;
|
|
246
|
-
}
|
|
247
|
-
// If another request is creating this tenant's connection, wait for it
|
|
248
|
-
const pendingConnection = MultiTenantDataSourceService.connectionLocks.get(tenant.id);
|
|
249
|
-
if (pendingConnection) {
|
|
250
|
-
return pendingConnection;
|
|
251
|
-
}
|
|
252
|
-
// Create connection with lock to prevent race conditions
|
|
142
|
+
async getOrCreateTenantConnection(tenant) {
|
|
143
|
+
const existing = this.self.tenantConnections.get(tenant.id);
|
|
144
|
+
if (existing?.isInitialized) return existing;
|
|
145
|
+
const pendingConnection = this.self.connectionLocks.get(tenant.id);
|
|
146
|
+
if (pendingConnection) return pendingConnection;
|
|
253
147
|
const config = this.buildTenantDatabaseConfig(tenant);
|
|
254
148
|
const connectionPromise = this.createDataSourceFromConfig(config);
|
|
255
|
-
|
|
149
|
+
this.self.connectionLocks.set(tenant.id, connectionPromise);
|
|
256
150
|
try {
|
|
257
151
|
const dataSource = await connectionPromise;
|
|
258
|
-
|
|
259
|
-
this.logger.log(`Created connection for tenant: ${tenant.id}`);
|
|
152
|
+
this.self.tenantConnections.set(tenant.id, dataSource);
|
|
260
153
|
return dataSource;
|
|
261
154
|
} finally{
|
|
262
|
-
|
|
155
|
+
this.self.connectionLocks.delete(tenant.id);
|
|
263
156
|
}
|
|
264
157
|
}
|
|
265
|
-
|
|
266
|
-
* Get default database config (supports both naming conventions)
|
|
267
|
-
*/ getDefaultDatabaseConfig() {
|
|
158
|
+
getDefaultDatabaseConfig() {
|
|
268
159
|
return this.options?.defaultDatabaseConfig ?? this.options?.tenantDefaultDatabaseConfig;
|
|
269
160
|
}
|
|
270
|
-
|
|
271
|
-
* Build database config for tenant (merges with default)
|
|
272
|
-
*/ buildTenantDatabaseConfig(tenant) {
|
|
161
|
+
buildTenantDatabaseConfig(tenant) {
|
|
273
162
|
const defaultConfig = this.getDefaultDatabaseConfig();
|
|
274
163
|
if (!defaultConfig) {
|
|
275
|
-
throw new
|
|
164
|
+
throw new _common.InternalServerErrorException({
|
|
165
|
+
message: 'No default database config for multi-tenant mode.',
|
|
166
|
+
messageKey: _constants.SYSTEM_MESSAGES.DATABASE_CONFIG_NOT_AVAILABLE
|
|
167
|
+
});
|
|
276
168
|
}
|
|
277
169
|
return {
|
|
278
170
|
type: defaultConfig.type,
|
|
@@ -283,15 +175,7 @@ let MultiTenantDataSourceService = class MultiTenantDataSourceService {
|
|
|
283
175
|
database: tenant.database
|
|
284
176
|
};
|
|
285
177
|
}
|
|
286
|
-
|
|
287
|
-
* Create DataSource from config - override in subclasses for custom logic
|
|
288
|
-
*
|
|
289
|
-
* Note: In multi-tenant mode, subclasses should pass entities array.
|
|
290
|
-
* For single-tenant mode, entities are registered via TypeOrmModule at app level.
|
|
291
|
-
*
|
|
292
|
-
* @param config - Database configuration
|
|
293
|
-
* @param entities - Optional entities array (for multi-tenant mode)
|
|
294
|
-
*/ async createDataSourceFromConfig(config, entities = []) {
|
|
178
|
+
async createDataSourceFromConfig(config, entities = []) {
|
|
295
179
|
const dataSource = new _typeorm.DataSource({
|
|
296
180
|
type: config.type,
|
|
297
181
|
host: config.host,
|
|
@@ -303,30 +187,23 @@ let MultiTenantDataSourceService = class MultiTenantDataSourceService {
|
|
|
303
187
|
synchronize: false,
|
|
304
188
|
namingStrategy: new _typeormnamingstrategies.SnakeNamingStrategy()
|
|
305
189
|
});
|
|
306
|
-
if (!dataSource.isInitialized)
|
|
307
|
-
await dataSource.initialize();
|
|
308
|
-
}
|
|
190
|
+
if (!dataSource.isInitialized) await dataSource.initialize();
|
|
309
191
|
return dataSource;
|
|
310
192
|
}
|
|
311
193
|
constructor(options, request){
|
|
312
194
|
_define_property(this, "options", void 0);
|
|
313
195
|
_define_property(this, "request", void 0);
|
|
314
|
-
_define_property(this, "logger", void 0);
|
|
315
|
-
// Instance state
|
|
316
196
|
_define_property(this, "tenantHeader", void 0);
|
|
317
197
|
this.options = options;
|
|
318
198
|
this.request = request;
|
|
319
|
-
this.logger = new _common.Logger(this.constructor.name);
|
|
320
199
|
this.tenantHeader = _nestjscore.DEFAULT_TENANT_HEADER;
|
|
321
200
|
this.initializeFromOptions();
|
|
322
201
|
}
|
|
323
202
|
};
|
|
324
|
-
// Static state shared across all instances
|
|
325
203
|
_define_property(MultiTenantDataSourceService, "tenantConnections", new Map());
|
|
326
204
|
_define_property(MultiTenantDataSourceService, "singleDataSource", null);
|
|
327
205
|
_define_property(MultiTenantDataSourceService, "tenantsRegistry", new Map());
|
|
328
206
|
_define_property(MultiTenantDataSourceService, "initialized", false);
|
|
329
|
-
// Connection locks to prevent race conditions during concurrent connection creation
|
|
330
207
|
_define_property(MultiTenantDataSourceService, "connectionLocks", new Map());
|
|
331
208
|
_define_property(MultiTenantDataSourceService, "singleConnectionLock", null);
|
|
332
209
|
MultiTenantDataSourceService = _ts_decorate([
|
|
@@ -9,247 +9,68 @@ Object.defineProperty(exports, "UtilsService", {
|
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
11
|
const _common = require("@nestjs/common");
|
|
12
|
-
const _classvalidator = require("class-validator");
|
|
13
12
|
function _ts_decorate(decorators, target, key, desc) {
|
|
14
13
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
15
14
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
16
15
|
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;
|
|
17
16
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
18
17
|
}
|
|
19
|
-
function _ts_metadata(k, v) {
|
|
20
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
21
|
-
}
|
|
22
18
|
let UtilsService = class UtilsService {
|
|
23
19
|
// ---------------- CACHE HELPERS ----------------
|
|
24
20
|
/**
|
|
25
21
|
* Generate cache key with optional tenant prefix for multi-tenant isolation
|
|
26
|
-
* @param entityName - Name of the entity being cached
|
|
27
|
-
* @param params - Query parameters for cache key uniqueness
|
|
28
|
-
* @param entityId - Optional entity ID for single entity caching
|
|
29
|
-
* @param tenantId - Optional tenant ID for multi-tenant isolation
|
|
30
22
|
*/ getCacheKey(entityName, params, entityId, tenantId) {
|
|
31
|
-
const
|
|
32
|
-
if (entityId)
|
|
33
|
-
|
|
23
|
+
const prefix = this.buildTenantPrefix(tenantId);
|
|
24
|
+
if (entityId) {
|
|
25
|
+
return `${prefix}entity_${entityName}_id_${entityId}${params ? '_select_' + JSON.stringify(params) : ''}`;
|
|
26
|
+
}
|
|
27
|
+
return `${prefix}entity_${entityName}_all_${JSON.stringify(params)}`;
|
|
34
28
|
}
|
|
35
29
|
/**
|
|
36
30
|
* Track cache key for later invalidation with optional tenant prefix
|
|
37
31
|
*/ async trackCacheKey(cacheKey, entityName, cacheManager, entityId, tenantId) {
|
|
38
|
-
const tenantPrefix = tenantId ? `tenant_${tenantId}_` : '';
|
|
39
32
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const trackingKey = `${tenantPrefix}entity_${entityName}_keys`;
|
|
47
|
-
const allKeys = await cacheManager.get(trackingKey) || [];
|
|
48
|
-
if (!allKeys.includes(cacheKey)) allKeys.push(cacheKey);
|
|
49
|
-
await cacheManager.set(trackingKey, allKeys);
|
|
50
|
-
}
|
|
51
|
-
} catch (error) {
|
|
52
|
-
// Log but don't throw - cache tracking failure shouldn't break the request
|
|
53
|
-
console.error(`Cache tracking failed for ${entityName}:`, error);
|
|
33
|
+
const trackingKey = this.buildTrackingKey(entityName, entityId, tenantId);
|
|
34
|
+
const keys = await cacheManager.get(trackingKey) || [];
|
|
35
|
+
if (!keys.includes(cacheKey)) keys.push(cacheKey);
|
|
36
|
+
await cacheManager.set(trackingKey, keys);
|
|
37
|
+
} catch {
|
|
38
|
+
// Silent failure - cache tracking is non-critical
|
|
54
39
|
}
|
|
55
40
|
}
|
|
56
41
|
/**
|
|
57
42
|
* Clear cache for entity with optional tenant prefix
|
|
58
|
-
* Uses Promise.allSettled to ensure all deletions are attempted even if some fail
|
|
59
43
|
*/ async clearCache(entityName, cacheManager, entityId, tenantId) {
|
|
60
|
-
const tenantPrefix = tenantId ? `tenant_${tenantId}_` : '';
|
|
61
44
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
} else {
|
|
69
|
-
const trackingKey = `${tenantPrefix}entity_${entityName}_keys`;
|
|
70
|
-
const keySet = await cacheManager.get(trackingKey) || [];
|
|
71
|
-
// Use Promise.allSettled to ensure all deletions are attempted
|
|
72
|
-
await Promise.allSettled(keySet.map((key)=>cacheManager.del(key)));
|
|
73
|
-
await cacheManager.del(trackingKey);
|
|
74
|
-
}
|
|
75
|
-
} catch (error) {
|
|
76
|
-
// Log but don't throw - cache invalidation failure shouldn't break the request
|
|
77
|
-
console.error(`Cache invalidation failed for ${entityName}:`, error);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Check Phone or Email
|
|
82
|
-
*/ checkPhoneOrEmail(value) {
|
|
83
|
-
if ((0, _classvalidator.isEmail)(value)) {
|
|
84
|
-
return {
|
|
85
|
-
value: value,
|
|
86
|
-
type: 'email'
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
const phoneMatch = value.match(/^((\+880)|0)?(13|15|16|17|18|19)\d{8}$/);
|
|
90
|
-
if (phoneMatch) {
|
|
91
|
-
let phone = phoneMatch[0];
|
|
92
|
-
if (!phone.startsWith('+88')) {
|
|
93
|
-
phone = '+88' + phone;
|
|
94
|
-
}
|
|
95
|
-
return {
|
|
96
|
-
value: phone,
|
|
97
|
-
type: 'phone'
|
|
98
|
-
};
|
|
45
|
+
const trackingKey = this.buildTrackingKey(entityName, entityId, tenantId);
|
|
46
|
+
const keys = await cacheManager.get(trackingKey) || [];
|
|
47
|
+
await Promise.allSettled(keys.map((key)=>cacheManager.del(key)));
|
|
48
|
+
await cacheManager.del(trackingKey);
|
|
49
|
+
} catch {
|
|
50
|
+
// Silent failure - cache invalidation is non-critical
|
|
99
51
|
}
|
|
100
|
-
return {
|
|
101
|
-
value: null,
|
|
102
|
-
type: null
|
|
103
|
-
};
|
|
104
52
|
}
|
|
53
|
+
// ---------------- STRING HELPERS ----------------
|
|
105
54
|
/**
|
|
106
|
-
*
|
|
107
|
-
* transformToSlug
|
|
55
|
+
* Transform string to URL-friendly slug
|
|
108
56
|
*/ transformToSlug(value, salt) {
|
|
109
57
|
const slug = value?.trim().replace(/[^A-Z0-9]+/gi, '-').toLowerCase();
|
|
110
58
|
return salt ? `${slug}-${this.getRandomInt(1, 100)}` : slug;
|
|
111
59
|
}
|
|
112
60
|
/**
|
|
113
|
-
*
|
|
114
|
-
* getRandomInt
|
|
115
|
-
* generateRandomId
|
|
116
|
-
* getRandomOtpCode
|
|
61
|
+
* Generate random integer between min and max (inclusive)
|
|
117
62
|
*/ getRandomInt(min, max) {
|
|
118
63
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
119
64
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
for(let i = 0; i < length; i++){
|
|
124
|
-
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
|
125
|
-
}
|
|
126
|
-
return result;
|
|
127
|
-
}
|
|
128
|
-
getRandomOtpCode() {
|
|
129
|
-
return Math.floor(Math.random() * (9999 - 1000 + 1)) + 1000;
|
|
130
|
-
}
|
|
131
|
-
extractColumnNameFromError(detail) {
|
|
132
|
-
const match = detail.match(/\((.*?)\)=\((.*?)\)/);
|
|
133
|
-
return {
|
|
134
|
-
columnName: match ? match[1] : 'unknown',
|
|
135
|
-
value: match ? match[2] : 'unknown'
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
getOtpEmailFormat(otp, userName) {
|
|
139
|
-
return `
|
|
140
|
-
<!DOCTYPE html>
|
|
141
|
-
<html lang="en">
|
|
142
|
-
<head>
|
|
143
|
-
<meta charset="UTF-8" />
|
|
144
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
145
|
-
<title>Your OTP Code</title>
|
|
146
|
-
<style>
|
|
147
|
-
body {
|
|
148
|
-
font-family: Arial, sans-serif;
|
|
149
|
-
background-color: #f4f4f7;
|
|
150
|
-
margin: 0;
|
|
151
|
-
padding: 0;
|
|
152
|
-
}
|
|
153
|
-
.email-container {
|
|
154
|
-
max-width: 500px;
|
|
155
|
-
margin: 40px auto;
|
|
156
|
-
background-color: #ffffff;
|
|
157
|
-
padding: 30px;
|
|
158
|
-
border-radius: 8px;
|
|
159
|
-
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
|
160
|
-
text-align: center;
|
|
161
|
-
}
|
|
162
|
-
h1 {
|
|
163
|
-
color: #333;
|
|
164
|
-
}
|
|
165
|
-
p {
|
|
166
|
-
font-size: 16px;
|
|
167
|
-
color: #555;
|
|
168
|
-
}
|
|
169
|
-
.otp-box {
|
|
170
|
-
display: inline-block;
|
|
171
|
-
margin: 20px 0;
|
|
172
|
-
padding: 14px 28px;
|
|
173
|
-
font-size: 24px;
|
|
174
|
-
letter-spacing: 6px;
|
|
175
|
-
background-color: #007BFF;
|
|
176
|
-
color: white;
|
|
177
|
-
border-radius: 8px;
|
|
178
|
-
font-weight: bold;
|
|
179
|
-
}
|
|
180
|
-
</style>
|
|
181
|
-
</head>
|
|
182
|
-
<body>
|
|
183
|
-
<div class="email-container">
|
|
184
|
-
<p>Hi ${userName || 'Sir/Madam'},</p>
|
|
185
|
-
<p>Use the code below to verify your identity:</p>
|
|
186
|
-
<div class="otp-box">${otp}</div>
|
|
187
|
-
<p>This OTP is valid for a limited time. Do not share it with anyone.</p>
|
|
188
|
-
<p>If you didn’t request this, please ignore this email.</p>
|
|
189
|
-
</div>
|
|
190
|
-
</body>
|
|
191
|
-
</html>
|
|
192
|
-
`;
|
|
65
|
+
// ---------------- PRIVATE HELPERS ----------------
|
|
66
|
+
buildTenantPrefix(tenantId) {
|
|
67
|
+
return tenantId ? `tenant_${tenantId}_` : '';
|
|
193
68
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
<html lang="en">
|
|
198
|
-
<head>
|
|
199
|
-
<meta charset="UTF-8" />
|
|
200
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
201
|
-
<title>Reset Your Password</title>
|
|
202
|
-
<style>
|
|
203
|
-
body {
|
|
204
|
-
font-family: Arial, sans-serif;
|
|
205
|
-
background-color: #f4f4f7;
|
|
206
|
-
margin: 0;
|
|
207
|
-
padding: 0;
|
|
208
|
-
}
|
|
209
|
-
.email-container {
|
|
210
|
-
max-width: 500px;
|
|
211
|
-
margin: 40px auto;
|
|
212
|
-
background-color: #ffffff;
|
|
213
|
-
padding: 30px;
|
|
214
|
-
border-radius: 8px;
|
|
215
|
-
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
|
216
|
-
text-align: center;
|
|
217
|
-
}
|
|
218
|
-
h1 {
|
|
219
|
-
color: #333;
|
|
220
|
-
}
|
|
221
|
-
p {
|
|
222
|
-
font-size: 16px;
|
|
223
|
-
color: #555;
|
|
224
|
-
}
|
|
225
|
-
.reset-button {
|
|
226
|
-
display: inline-block;
|
|
227
|
-
margin: 20px 0;
|
|
228
|
-
padding: 14px 28px;
|
|
229
|
-
font-size: 16px;
|
|
230
|
-
background-color: #007BFF;
|
|
231
|
-
color: #ffffff !important;
|
|
232
|
-
text-decoration: none;
|
|
233
|
-
border-radius: 6px;
|
|
234
|
-
font-weight: bold;
|
|
235
|
-
}
|
|
236
|
-
</style>
|
|
237
|
-
</head>
|
|
238
|
-
<body>
|
|
239
|
-
<div class="email-container">
|
|
240
|
-
<p>Hi ${userName || 'Sir/Madam'},</p>
|
|
241
|
-
<p>We received a request to reset your password. Click the button below to reset it:</p>
|
|
242
|
-
<a href="${resetLink}" class="reset-button" target="_blank">Reset Password</a>
|
|
243
|
-
<p>If you didn’t request a password reset, you can safely ignore this email.</p>
|
|
244
|
-
</div>
|
|
245
|
-
</body>
|
|
246
|
-
</html>
|
|
247
|
-
`;
|
|
69
|
+
buildTrackingKey(entityName, entityId, tenantId) {
|
|
70
|
+
const prefix = this.buildTenantPrefix(tenantId);
|
|
71
|
+
return entityId ? `${prefix}entity_${entityName}_id_${entityId}_keys` : `${prefix}entity_${entityName}_keys`;
|
|
248
72
|
}
|
|
249
|
-
constructor(){}
|
|
250
73
|
};
|
|
251
74
|
UtilsService = _ts_decorate([
|
|
252
|
-
(0, _common.Injectable)()
|
|
253
|
-
_ts_metadata("design:type", Function),
|
|
254
|
-
_ts_metadata("design:paramtypes", [])
|
|
75
|
+
(0, _common.Injectable)()
|
|
255
76
|
], UtilsService);
|