@flusys/nestjs-shared 5.1.2 → 5.2.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/cjs/classes/api-service.class.js +64 -30
- package/cjs/classes/index.js +0 -1
- package/cjs/modules/datasource/index.js +0 -1
- package/cjs/modules/datasource/multi-tenant-datasource.service.js +24 -85
- package/classes/api-service.class.d.ts +13 -6
- package/classes/index.d.ts +0 -1
- package/decorators/current-user.decorator.d.ts +1 -1
- package/fesm/classes/api-service.class.js +67 -33
- package/fesm/classes/index.js +0 -1
- package/fesm/interfaces/datasource.interface.js +1 -20
- package/fesm/modules/datasource/index.js +0 -1
- package/fesm/modules/datasource/multi-tenant-datasource.service.js +24 -85
- package/guards/permission.guard.d.ts +1 -1
- package/interceptors/idempotency.interceptor.d.ts +1 -1
- package/interfaces/notification-adapter.interface.d.ts +13 -14
- package/modules/datasource/index.d.ts +0 -1
- package/modules/datasource/multi-tenant-datasource.service.d.ts +4 -13
- package/package.json +15 -15
- package/cjs/classes/request-scoped-api.service.js +0 -61
- package/cjs/modules/datasource/datasource.module.js +0 -117
- package/classes/request-scoped-api.service.d.ts +0 -14
- package/fesm/classes/request-scoped-api.service.js +0 -51
- package/fesm/modules/datasource/datasource.module.js +0 -107
- package/modules/datasource/datasource.module.d.ts +0 -19
|
@@ -20,12 +20,40 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
20
20
|
function _ts_metadata(k, v) {
|
|
21
21
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
22
22
|
}
|
|
23
|
-
import { InternalServerErrorException, NotFoundException } from '@nestjs/common';
|
|
24
|
-
import { In } from 'typeorm';
|
|
23
|
+
import { Injectable, InternalServerErrorException, NotFoundException, Scope } from '@nestjs/common';
|
|
24
|
+
import { EntityTarget, In } from 'typeorm';
|
|
25
25
|
import { ERROR_MESSAGES, SYSTEM_MESSAGES } from '../constants';
|
|
26
26
|
import { LogAction } from '../decorators/log-action.decorator';
|
|
27
27
|
import { DeleteDto } from '../dtos';
|
|
28
|
-
|
|
28
|
+
import { IDataSourceProvider } from '../interfaces';
|
|
29
|
+
import { UtilsService } from '../modules/utils/utils.service';
|
|
30
|
+
import { HybridCache } from './hybrid-cache.class';
|
|
31
|
+
export class ApiService {
|
|
32
|
+
resolveEntity() {
|
|
33
|
+
return this._entity;
|
|
34
|
+
}
|
|
35
|
+
getDataSourceProvider() {
|
|
36
|
+
return this._dataSourceProvider;
|
|
37
|
+
}
|
|
38
|
+
async ensureDataSourceRepository() {
|
|
39
|
+
if (!this.repositoryInitialized) {
|
|
40
|
+
const entity = this.resolveEntity();
|
|
41
|
+
const provider = this.getDataSourceProvider();
|
|
42
|
+
this.repository = await provider.getRepository(entity);
|
|
43
|
+
this.repositoryInitialized = true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async resolveRepositories(entities) {
|
|
47
|
+
const provider = this.getDataSourceProvider();
|
|
48
|
+
const repositories = [];
|
|
49
|
+
for (const entity of entities){
|
|
50
|
+
repositories.push(await provider.getRepository(entity));
|
|
51
|
+
}
|
|
52
|
+
return repositories;
|
|
53
|
+
}
|
|
54
|
+
async getDataSourceForService() {
|
|
55
|
+
return this.getDataSourceProvider().getDataSource();
|
|
56
|
+
}
|
|
29
57
|
async insert(dto, user) {
|
|
30
58
|
return this.executeInTransaction('insert', async (qr)=>{
|
|
31
59
|
await this.beforeInsertOperation(dto, user, qr);
|
|
@@ -98,7 +126,7 @@ import { DeleteDto } from '../dtos';
|
|
|
98
126
|
});
|
|
99
127
|
}
|
|
100
128
|
async findByIds(ids, user, select) {
|
|
101
|
-
await this.
|
|
129
|
+
await this.ensureDataSourceRepository();
|
|
102
130
|
try {
|
|
103
131
|
const query = this.repository.createQueryBuilder(this.entityName);
|
|
104
132
|
query.where({
|
|
@@ -126,7 +154,7 @@ import { DeleteDto } from '../dtos';
|
|
|
126
154
|
}
|
|
127
155
|
}
|
|
128
156
|
async findById(id, user, select) {
|
|
129
|
-
await this.
|
|
157
|
+
await this.ensureDataSourceRepository();
|
|
130
158
|
try {
|
|
131
159
|
const query = this.repository.createQueryBuilder(this.entityName);
|
|
132
160
|
query.where({
|
|
@@ -140,18 +168,17 @@ import { DeleteDto } from '../dtos';
|
|
|
140
168
|
result = await this.cacheManager.get(cacheKey);
|
|
141
169
|
}
|
|
142
170
|
if (!result) {
|
|
143
|
-
let output;
|
|
144
171
|
if (isRaw) {
|
|
145
|
-
|
|
146
|
-
const convertResult = this.convertEntityListToResponseListDto(
|
|
172
|
+
const rawOutput = await finalQuery.getRawMany();
|
|
173
|
+
const convertResult = this.convertEntityListToResponseListDto(rawOutput, isRaw);
|
|
147
174
|
if (convertResult.length) result = convertResult[0];
|
|
148
175
|
} else {
|
|
149
|
-
|
|
150
|
-
if (!
|
|
176
|
+
const singleOutput = await finalQuery.getOne();
|
|
177
|
+
if (!singleOutput) throw new NotFoundException({
|
|
151
178
|
message: 'No Data Found',
|
|
152
179
|
messageKey: ERROR_MESSAGES.NOT_FOUND
|
|
153
180
|
});
|
|
154
|
-
result = this.convertEntityToResponseDto(
|
|
181
|
+
result = this.convertEntityToResponseDto(singleOutput, false);
|
|
155
182
|
}
|
|
156
183
|
if (this.isCacheable) {
|
|
157
184
|
await this.cacheManager.set(cacheKey, result);
|
|
@@ -167,41 +194,34 @@ import { DeleteDto } from '../dtos';
|
|
|
167
194
|
}
|
|
168
195
|
}
|
|
169
196
|
async getAll(search, filterAndPaginationDto, user) {
|
|
170
|
-
await this.
|
|
197
|
+
await this.ensureDataSourceRepository();
|
|
171
198
|
try {
|
|
172
199
|
let data;
|
|
173
200
|
let total;
|
|
174
201
|
let isRaw = false;
|
|
175
202
|
let query = this.repository.createQueryBuilder(this.entityName);
|
|
176
|
-
// Apply filters
|
|
177
203
|
if (filterAndPaginationDto.filter) {
|
|
178
204
|
const { query: filterQuery, isRaw: filterIsRaw } = await this.getFilterQuery(query, filterAndPaginationDto.filter, user);
|
|
179
205
|
query = filterQuery;
|
|
180
206
|
isRaw = isRaw || filterIsRaw;
|
|
181
207
|
}
|
|
182
|
-
// Apply search
|
|
183
208
|
if (search) {
|
|
184
209
|
const { query: searchQuery, isRaw: searchIsRaw } = await this.getGlobalSearchQuery(query, search, user);
|
|
185
210
|
query = searchQuery;
|
|
186
211
|
isRaw = isRaw || searchIsRaw;
|
|
187
212
|
}
|
|
188
|
-
// Apply select — always call so subclasses can define a default column set
|
|
189
213
|
const { query: selectQuery, isRaw: selectIsRaw } = await this.getSelectQuery(query, user, filterAndPaginationDto.select);
|
|
190
214
|
query = selectQuery;
|
|
191
215
|
isRaw = isRaw || selectIsRaw;
|
|
192
|
-
// Apply sort
|
|
193
216
|
if (filterAndPaginationDto.sort) {
|
|
194
217
|
const { query: sortQuery, isRaw: sortIsRaw } = await this.getSortQuery(query, filterAndPaginationDto.sort, user);
|
|
195
218
|
query = sortQuery;
|
|
196
219
|
isRaw = isRaw || sortIsRaw;
|
|
197
220
|
}
|
|
198
|
-
// Include soft-deleted if requested
|
|
199
221
|
if (filterAndPaginationDto.withDeleted) query.withDeleted();
|
|
200
|
-
// Extra manipulation
|
|
201
222
|
const { query: extraQuery, isRaw: extraIsRaw } = await this.getExtraManipulateQuery(query, filterAndPaginationDto, user);
|
|
202
223
|
query = extraQuery;
|
|
203
224
|
isRaw = isRaw || extraIsRaw;
|
|
204
|
-
// Pagination
|
|
205
225
|
if (filterAndPaginationDto.pagination) {
|
|
206
226
|
const offset = filterAndPaginationDto.pagination.currentPage * filterAndPaginationDto.pagination.pageSize;
|
|
207
227
|
const limit = filterAndPaginationDto.pagination.pageSize;
|
|
@@ -211,7 +231,6 @@ import { DeleteDto } from '../dtos';
|
|
|
211
231
|
query.skip(offset).take(limit);
|
|
212
232
|
}
|
|
213
233
|
}
|
|
214
|
-
// Generate cache key
|
|
215
234
|
let cacheKey = '';
|
|
216
235
|
if (this.isCacheable) {
|
|
217
236
|
cacheKey = this.utilsService.getCacheKey(this.entityName, {
|
|
@@ -225,7 +244,6 @@ import { DeleteDto } from '../dtos';
|
|
|
225
244
|
}
|
|
226
245
|
}
|
|
227
246
|
if (isRaw) {
|
|
228
|
-
// Handle raw query count manually
|
|
229
247
|
const rawSubQuery = query.clone().offset(undefined).limit(undefined).distinct(true);
|
|
230
248
|
const countSql = `
|
|
231
249
|
SELECT COUNT(*) as count
|
|
@@ -273,7 +291,7 @@ import { DeleteDto } from '../dtos';
|
|
|
273
291
|
}
|
|
274
292
|
}
|
|
275
293
|
async delete(option, user) {
|
|
276
|
-
await this.
|
|
294
|
+
await this.ensureDataSourceRepository();
|
|
277
295
|
const queryRunner = this.repository.manager.connection.createQueryRunner();
|
|
278
296
|
await queryRunner.connect();
|
|
279
297
|
await queryRunner.startTransaction();
|
|
@@ -305,7 +323,6 @@ import { DeleteDto } from '../dtos';
|
|
|
305
323
|
this.clearCacheForAll(),
|
|
306
324
|
this.clearCacheForId(ids)
|
|
307
325
|
]);
|
|
308
|
-
// return null per IService contract
|
|
309
326
|
return null;
|
|
310
327
|
} catch (error) {
|
|
311
328
|
await queryRunner.rollbackTransaction();
|
|
@@ -314,7 +331,6 @@ import { DeleteDto } from '../dtos';
|
|
|
314
331
|
await queryRunner.release();
|
|
315
332
|
}
|
|
316
333
|
}
|
|
317
|
-
// Caching
|
|
318
334
|
async clearCacheForAll() {
|
|
319
335
|
await this.utilsService.clearCache(this.entityName, this.cacheManager);
|
|
320
336
|
}
|
|
@@ -330,13 +346,13 @@ import { DeleteDto } from '../dtos';
|
|
|
330
346
|
messageKey: ERROR_MESSAGES.INTERNAL
|
|
331
347
|
});
|
|
332
348
|
}
|
|
333
|
-
|
|
349
|
+
ensureArray(value) {
|
|
334
350
|
return Array.isArray(value) ? value : [
|
|
335
351
|
value
|
|
336
352
|
];
|
|
337
353
|
}
|
|
338
|
-
|
|
339
|
-
await this.
|
|
354
|
+
async executeInTransaction(operation, fn) {
|
|
355
|
+
await this.ensureDataSourceRepository();
|
|
340
356
|
const qr = this.repository.manager.connection.createQueryRunner();
|
|
341
357
|
await qr.connect();
|
|
342
358
|
await qr.startTransaction();
|
|
@@ -361,8 +377,6 @@ import { DeleteDto } from '../dtos';
|
|
|
361
377
|
await qr.release();
|
|
362
378
|
}
|
|
363
379
|
}
|
|
364
|
-
// Hooks (override in child classes)
|
|
365
|
-
async ensureRepositoryInitialized() {}
|
|
366
380
|
async beforeInsertOperation(_dto, _user, _queryRunner) {}
|
|
367
381
|
async afterInsertOperation(_entity, _user, _queryRunner) {}
|
|
368
382
|
async beforeUpdateOperation(_dto, _user, _queryRunner) {}
|
|
@@ -411,7 +425,6 @@ import { DeleteDto } from '../dtos';
|
|
|
411
425
|
isRaw: false
|
|
412
426
|
};
|
|
413
427
|
}
|
|
414
|
-
// DTO conversion
|
|
415
428
|
async convertRequestDtoToEntity(dto, user) {
|
|
416
429
|
return Array.isArray(dto) ? await this.convertArrayDtoToEntities(dto, user) : [
|
|
417
430
|
await this.convertSingleDtoToEntity(dto, user)
|
|
@@ -446,21 +459,26 @@ import { DeleteDto } from '../dtos';
|
|
|
446
459
|
async getEntityClass() {
|
|
447
460
|
return await this.repository.create();
|
|
448
461
|
}
|
|
449
|
-
constructor(entityName,
|
|
462
|
+
constructor(entityName, cacheManager, utilsService, _loggerName, isCacheable = false, moduleName, entity, dataSourceProvider){
|
|
450
463
|
_define_property(this, "entityName", void 0);
|
|
451
|
-
_define_property(this, "repository", void 0);
|
|
452
464
|
_define_property(this, "cacheManager", void 0);
|
|
453
465
|
_define_property(this, "utilsService", void 0);
|
|
454
466
|
_define_property(this, "_loggerName", void 0);
|
|
455
467
|
_define_property(this, "isCacheable", void 0);
|
|
456
468
|
_define_property(this, "moduleName", void 0);
|
|
469
|
+
_define_property(this, "repository", void 0);
|
|
470
|
+
_define_property(this, "repositoryInitialized", void 0);
|
|
471
|
+
_define_property(this, "_entity", void 0);
|
|
472
|
+
_define_property(this, "_dataSourceProvider", void 0);
|
|
457
473
|
this.entityName = entityName;
|
|
458
|
-
this.repository = repository;
|
|
459
474
|
this.cacheManager = cacheManager;
|
|
460
475
|
this.utilsService = utilsService;
|
|
461
476
|
this._loggerName = _loggerName;
|
|
462
477
|
this.isCacheable = isCacheable;
|
|
463
478
|
this.moduleName = moduleName;
|
|
479
|
+
this.repositoryInitialized = false;
|
|
480
|
+
this._entity = entity;
|
|
481
|
+
this._dataSourceProvider = dataSourceProvider;
|
|
464
482
|
}
|
|
465
483
|
}
|
|
466
484
|
_ts_decorate([
|
|
@@ -530,3 +548,19 @@ _ts_decorate([
|
|
|
530
548
|
]),
|
|
531
549
|
_ts_metadata("design:returntype", Promise)
|
|
532
550
|
], ApiService.prototype, "delete", null);
|
|
551
|
+
ApiService = _ts_decorate([
|
|
552
|
+
Injectable({
|
|
553
|
+
scope: Scope.REQUEST
|
|
554
|
+
}),
|
|
555
|
+
_ts_metadata("design:type", Function),
|
|
556
|
+
_ts_metadata("design:paramtypes", [
|
|
557
|
+
String,
|
|
558
|
+
typeof HybridCache === "undefined" ? Object : HybridCache,
|
|
559
|
+
typeof UtilsService === "undefined" ? Object : UtilsService,
|
|
560
|
+
String,
|
|
561
|
+
Boolean,
|
|
562
|
+
String,
|
|
563
|
+
typeof EntityTarget === "undefined" ? Object : EntityTarget,
|
|
564
|
+
typeof IDataSourceProvider === "undefined" ? Object : IDataSourceProvider
|
|
565
|
+
])
|
|
566
|
+
], ApiService);
|
package/fesm/classes/index.js
CHANGED
|
@@ -2,7 +2,6 @@ export * from './api-controller.class';
|
|
|
2
2
|
export * from './api-service.class';
|
|
3
3
|
export * from './handle-custome-db-column-type';
|
|
4
4
|
export * from './hybrid-cache.class';
|
|
5
|
-
export * from './request-scoped-api.service';
|
|
6
5
|
export * from './winston.logger.class';
|
|
7
6
|
// Re-export permission constants for convenience
|
|
8
7
|
export * from '../constants/permissions';
|
|
@@ -1,20 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Interface for DataSource providers used by RequestScopedApiService.
|
|
3
|
-
* Implement this interface in each module's DataSourceProvider to ensure type safety.
|
|
4
|
-
*
|
|
5
|
-
* @example
|
|
6
|
-
* ```typescript
|
|
7
|
-
* @Injectable()
|
|
8
|
-
* export class IAMDataSourceProvider implements IDataSourceProvider {
|
|
9
|
-
* constructor(private readonly dataSource: DataSource) {}
|
|
10
|
-
*
|
|
11
|
-
* async getRepository<T extends ObjectLiteral>(entity: EntityTarget<T>): Promise<Repository<T>> {
|
|
12
|
-
* return this.dataSource.getRepository(entity);
|
|
13
|
-
* }
|
|
14
|
-
*
|
|
15
|
-
* async getDataSource(): Promise<DataSource> {
|
|
16
|
-
* return this.dataSource;
|
|
17
|
-
* }
|
|
18
|
-
* }
|
|
19
|
-
* ```
|
|
20
|
-
*/ export { };
|
|
1
|
+
export { };
|
|
@@ -33,21 +33,21 @@ import { Request } from 'express';
|
|
|
33
33
|
import { DataSource } from 'typeorm';
|
|
34
34
|
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
|
|
35
35
|
export class MultiTenantDataSourceService {
|
|
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
|
-
setTenantHeader(header) {
|
|
49
|
-
this.tenantHeader = header;
|
|
50
|
-
}
|
|
51
51
|
getDatabaseMode() {
|
|
52
52
|
return this.options?.bootstrapAppConfig?.databaseMode ?? 'single';
|
|
53
53
|
}
|
|
@@ -71,91 +71,33 @@ export class MultiTenantDataSourceService {
|
|
|
71
71
|
return tenantId ? this.getTenant(tenantId) : null;
|
|
72
72
|
}
|
|
73
73
|
getTenant(tenantId) {
|
|
74
|
-
return
|
|
75
|
-
}
|
|
76
|
-
getAllTenants() {
|
|
77
|
-
return Array.from(MultiTenantDataSourceService.tenantsRegistry.values());
|
|
78
|
-
}
|
|
79
|
-
getActiveTenants() {
|
|
80
|
-
return this.getAllTenants();
|
|
74
|
+
return this.self.tenantsRegistry.get(tenantId) ?? null;
|
|
81
75
|
}
|
|
82
76
|
async getDataSource() {
|
|
83
77
|
return this.isMultiTenant() ? this.getTenantDataSource() : this.getSingleDataSource();
|
|
84
78
|
}
|
|
85
|
-
async getDataSourceForTenant(tenantId) {
|
|
86
|
-
const tenant = this.getTenant(tenantId);
|
|
87
|
-
if (!tenant) {
|
|
88
|
-
throw new BadRequestException({
|
|
89
|
-
message: `Tenant '${tenantId}' not found`,
|
|
90
|
-
messageKey: SYSTEM_MESSAGES.TENANT_NOT_FOUND,
|
|
91
|
-
messageVariables: {
|
|
92
|
-
tenantId
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
return this.getOrCreateTenantConnection(tenant);
|
|
97
|
-
}
|
|
98
|
-
setDataSource(dataSource) {
|
|
99
|
-
MultiTenantDataSourceService.singleDataSource = dataSource;
|
|
100
|
-
}
|
|
101
79
|
async getRepository(entity) {
|
|
102
80
|
const dataSource = await this.getDataSource();
|
|
103
81
|
return dataSource.getRepository(entity);
|
|
104
82
|
}
|
|
105
|
-
async withTenant(tenantId, callback) {
|
|
106
|
-
const dataSource = await this.getDataSourceForTenant(tenantId);
|
|
107
|
-
return callback(dataSource);
|
|
108
|
-
}
|
|
109
|
-
async forAllTenants(callback) {
|
|
110
|
-
const results = new Map();
|
|
111
|
-
for (const tenant of this.getActiveTenants()){
|
|
112
|
-
try {
|
|
113
|
-
const dataSource = await this.getOrCreateTenantConnection(tenant);
|
|
114
|
-
results.set(tenant.id, await callback(tenant, dataSource));
|
|
115
|
-
} catch {
|
|
116
|
-
// Silent failure for individual tenant
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return results;
|
|
120
|
-
}
|
|
121
|
-
registerTenant(tenant) {
|
|
122
|
-
MultiTenantDataSourceService.tenantsRegistry.set(tenant.id, tenant);
|
|
123
|
-
}
|
|
124
|
-
async removeTenant(tenantId) {
|
|
125
|
-
await this.closeTenantConnection(tenantId);
|
|
126
|
-
MultiTenantDataSourceService.tenantsRegistry.delete(tenantId);
|
|
127
|
-
}
|
|
128
83
|
async closeTenantConnection(tenantId) {
|
|
129
|
-
const connection =
|
|
84
|
+
const connection = this.self.tenantConnections.get(tenantId);
|
|
130
85
|
if (connection?.isInitialized) {
|
|
131
86
|
await connection.destroy();
|
|
132
|
-
|
|
87
|
+
this.self.tenantConnections.delete(tenantId);
|
|
133
88
|
}
|
|
134
89
|
}
|
|
135
90
|
async onModuleDestroy() {
|
|
136
|
-
for (const [tenantId] of
|
|
91
|
+
for (const [tenantId] of this.self.tenantConnections){
|
|
137
92
|
await this.closeTenantConnection(tenantId);
|
|
138
93
|
}
|
|
139
94
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
MultiTenantDataSourceService.singleConnectionLock = null;
|
|
144
|
-
MultiTenantDataSourceService.tenantConnections.clear();
|
|
145
|
-
MultiTenantDataSourceService.tenantsRegistry.clear();
|
|
146
|
-
MultiTenantDataSourceService.connectionLocks.clear();
|
|
147
|
-
}
|
|
148
|
-
// Protected Methods (for subclasses)
|
|
149
|
-
/**
|
|
150
|
-
* Get single database DataSource with connection locking to prevent race conditions
|
|
151
|
-
*/ async getSingleDataSource() {
|
|
152
|
-
// Return existing initialized connection
|
|
153
|
-
if (MultiTenantDataSourceService.singleDataSource?.isInitialized) {
|
|
154
|
-
return MultiTenantDataSourceService.singleDataSource;
|
|
95
|
+
async getSingleDataSource() {
|
|
96
|
+
if (this.self.singleDataSource?.isInitialized) {
|
|
97
|
+
return this.self.singleDataSource;
|
|
155
98
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return MultiTenantDataSourceService.singleConnectionLock;
|
|
99
|
+
if (this.self.singleConnectionLock) {
|
|
100
|
+
return this.self.singleConnectionLock;
|
|
159
101
|
}
|
|
160
102
|
const config = this.getDefaultDatabaseConfig();
|
|
161
103
|
if (!config) {
|
|
@@ -164,20 +106,17 @@ export class MultiTenantDataSourceService {
|
|
|
164
106
|
messageKey: SYSTEM_MESSAGES.DATABASE_CONFIG_NOT_AVAILABLE
|
|
165
107
|
});
|
|
166
108
|
}
|
|
167
|
-
// Create connection with lock to prevent race conditions
|
|
168
109
|
const connectionPromise = this.createDataSourceFromConfig(config);
|
|
169
|
-
|
|
110
|
+
this.self.singleConnectionLock = connectionPromise;
|
|
170
111
|
try {
|
|
171
112
|
const dataSource = await connectionPromise;
|
|
172
|
-
|
|
113
|
+
this.self.singleDataSource = dataSource;
|
|
173
114
|
return dataSource;
|
|
174
115
|
} finally{
|
|
175
|
-
|
|
116
|
+
this.self.singleConnectionLock = null;
|
|
176
117
|
}
|
|
177
118
|
}
|
|
178
|
-
|
|
179
|
-
* Get tenant-specific DataSource
|
|
180
|
-
*/ async getTenantDataSource() {
|
|
119
|
+
async getTenantDataSource() {
|
|
181
120
|
const tenant = this.getCurrentTenant();
|
|
182
121
|
if (!tenant) {
|
|
183
122
|
throw new BadRequestException({
|
|
@@ -191,19 +130,19 @@ export class MultiTenantDataSourceService {
|
|
|
191
130
|
return this.getOrCreateTenantConnection(tenant);
|
|
192
131
|
}
|
|
193
132
|
async getOrCreateTenantConnection(tenant) {
|
|
194
|
-
const existing =
|
|
133
|
+
const existing = this.self.tenantConnections.get(tenant.id);
|
|
195
134
|
if (existing?.isInitialized) return existing;
|
|
196
|
-
const pendingConnection =
|
|
135
|
+
const pendingConnection = this.self.connectionLocks.get(tenant.id);
|
|
197
136
|
if (pendingConnection) return pendingConnection;
|
|
198
137
|
const config = this.buildTenantDatabaseConfig(tenant);
|
|
199
138
|
const connectionPromise = this.createDataSourceFromConfig(config);
|
|
200
|
-
|
|
139
|
+
this.self.connectionLocks.set(tenant.id, connectionPromise);
|
|
201
140
|
try {
|
|
202
141
|
const dataSource = await connectionPromise;
|
|
203
|
-
|
|
142
|
+
this.self.tenantConnections.set(tenant.id, dataSource);
|
|
204
143
|
return dataSource;
|
|
205
144
|
} finally{
|
|
206
|
-
|
|
145
|
+
this.self.connectionLocks.delete(tenant.id);
|
|
207
146
|
}
|
|
208
147
|
}
|
|
209
148
|
getDefaultDatabaseConfig() {
|
|
@@ -6,7 +6,7 @@ export declare class PermissionGuard implements CanActivate {
|
|
|
6
6
|
private readonly reflector;
|
|
7
7
|
private readonly cache?;
|
|
8
8
|
private readonly config;
|
|
9
|
-
constructor(reflector: Reflector, cache?: HybridCache, config?: PermissionGuardConfig);
|
|
9
|
+
constructor(reflector: Reflector, cache?: HybridCache | undefined, config?: PermissionGuardConfig);
|
|
10
10
|
canActivate(context: ExecutionContext): Promise<boolean>;
|
|
11
11
|
private normalizeToLogicNode;
|
|
12
12
|
private evaluateLogicNode;
|
|
@@ -5,6 +5,6 @@ export declare class IdempotencyInterceptor implements NestInterceptor {
|
|
|
5
5
|
private readonly cache?;
|
|
6
6
|
private readonly CACHE_TTL;
|
|
7
7
|
private readonly PROCESSING_MARKER;
|
|
8
|
-
constructor(cache?: HybridCache);
|
|
8
|
+
constructor(cache?: HybridCache | undefined);
|
|
9
9
|
intercept<T = any>(context: ExecutionContext, next: CallHandler<T>): Promise<Observable<T>>;
|
|
10
10
|
}
|
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
import { NotificationType } from '../enums/notification-type.enum';
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
sendToMany(options: NotificationBulkSendOptions): Promise<void>;
|
|
5
|
-
broadcastToCompany?(companyId: string, title: string, message?: string, type?: NotificationType, data?: Record<string, unknown>): Promise<void>;
|
|
6
|
-
isUserOnline?(userId: string): boolean;
|
|
7
|
-
}
|
|
8
|
-
interface NotificationBaseOptions {
|
|
2
|
+
export declare const NOTIFICATION_ADAPTER = "NOTIFICATION_ADAPTER";
|
|
3
|
+
export interface INotificationContent {
|
|
9
4
|
title: string;
|
|
10
|
-
message?: string;
|
|
5
|
+
message?: string | null;
|
|
11
6
|
type?: NotificationType;
|
|
12
|
-
data?: Record<string, unknown
|
|
13
|
-
companyId?: string;
|
|
7
|
+
data?: Record<string, unknown> | null;
|
|
8
|
+
companyId?: string | null;
|
|
9
|
+
realtime?: boolean;
|
|
14
10
|
}
|
|
15
|
-
export interface
|
|
11
|
+
export interface INotificationSendOptions extends INotificationContent {
|
|
16
12
|
userId: string;
|
|
17
13
|
}
|
|
18
|
-
export interface
|
|
14
|
+
export interface INotificationBulkSendOptions extends INotificationContent {
|
|
19
15
|
userIds: string[];
|
|
20
16
|
}
|
|
21
|
-
export
|
|
22
|
-
|
|
17
|
+
export interface INotificationAdapter {
|
|
18
|
+
send(options: INotificationSendOptions): Promise<void>;
|
|
19
|
+
sendToMany(options: INotificationBulkSendOptions): Promise<void>;
|
|
20
|
+
isUserOnline?(userId: string): boolean;
|
|
21
|
+
}
|
|
@@ -3,8 +3,8 @@ import { OnModuleDestroy } from '@nestjs/common';
|
|
|
3
3
|
import { Request } from 'express';
|
|
4
4
|
import { DataSource, EntityTarget, ObjectLiteral, Repository } from 'typeorm';
|
|
5
5
|
export declare class MultiTenantDataSourceService implements OnModuleDestroy {
|
|
6
|
-
protected readonly options?: IDataSourceServiceOptions;
|
|
7
|
-
protected readonly request?: Request;
|
|
6
|
+
protected readonly options?: IDataSourceServiceOptions | undefined;
|
|
7
|
+
protected readonly request?: Request | undefined;
|
|
8
8
|
protected static readonly tenantConnections: Map<string, DataSource>;
|
|
9
9
|
protected static singleDataSource: DataSource | null;
|
|
10
10
|
protected static readonly tenantsRegistry: Map<string, ITenantDatabaseConfig>;
|
|
@@ -12,28 +12,19 @@ export declare class MultiTenantDataSourceService implements OnModuleDestroy {
|
|
|
12
12
|
protected static readonly connectionLocks: Map<string, Promise<DataSource>>;
|
|
13
13
|
protected static singleConnectionLock: Promise<DataSource> | null;
|
|
14
14
|
protected tenantHeader: string;
|
|
15
|
-
constructor(options?: IDataSourceServiceOptions, request?: Request);
|
|
15
|
+
constructor(options?: IDataSourceServiceOptions | undefined, request?: Request | undefined);
|
|
16
|
+
private get self();
|
|
16
17
|
private initializeFromOptions;
|
|
17
18
|
private registerTenants;
|
|
18
|
-
setTenantHeader(header: string): void;
|
|
19
19
|
getDatabaseMode(): DatabaseMode;
|
|
20
20
|
isMultiTenant(): boolean;
|
|
21
21
|
getCurrentTenantId(): string | null;
|
|
22
22
|
getCurrentTenant(): ITenantDatabaseConfig | null;
|
|
23
23
|
getTenant(tenantId: string): ITenantDatabaseConfig | null;
|
|
24
|
-
getAllTenants(): ITenantDatabaseConfig[];
|
|
25
|
-
getActiveTenants(): ITenantDatabaseConfig[];
|
|
26
24
|
getDataSource(): Promise<DataSource>;
|
|
27
|
-
getDataSourceForTenant(tenantId: string): Promise<DataSource>;
|
|
28
|
-
setDataSource(dataSource: DataSource): void;
|
|
29
25
|
getRepository<T extends ObjectLiteral>(entity: EntityTarget<T>): Promise<Repository<T>>;
|
|
30
|
-
withTenant<T>(tenantId: string, callback: (ds: DataSource) => Promise<T>): Promise<T>;
|
|
31
|
-
forAllTenants<T>(callback: (tenant: ITenantDatabaseConfig, ds: DataSource) => Promise<T>): Promise<Map<string, T>>;
|
|
32
|
-
registerTenant(tenant: ITenantDatabaseConfig): void;
|
|
33
|
-
removeTenant(tenantId: string): Promise<void>;
|
|
34
26
|
closeTenantConnection(tenantId: string): Promise<void>;
|
|
35
27
|
onModuleDestroy(): Promise<void>;
|
|
36
|
-
static reset(): void;
|
|
37
28
|
protected getSingleDataSource(): Promise<DataSource>;
|
|
38
29
|
protected getTenantDataSource(): Promise<DataSource>;
|
|
39
30
|
protected getOrCreateTenantConnection(tenant: ITenantDatabaseConfig): Promise<DataSource>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flusys/nestjs-shared",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"description": "Common shared utilities for Flusys NestJS applications",
|
|
5
5
|
"main": "cjs/index.js",
|
|
6
6
|
"module": "fesm/index.js",
|
|
@@ -93,25 +93,25 @@
|
|
|
93
93
|
}
|
|
94
94
|
},
|
|
95
95
|
"peerDependencies": {
|
|
96
|
-
"@keyv/redis": "^5.0
|
|
97
|
-
"@nestjs/common": "^
|
|
98
|
-
"@nestjs/config": "^
|
|
99
|
-
"@nestjs/core": "^
|
|
100
|
-
"@nestjs/jwt": "^
|
|
101
|
-
"@nestjs/passport": "^
|
|
102
|
-
"@nestjs/swagger": "^
|
|
103
|
-
"@nestjs/typeorm": "^
|
|
104
|
-
"cacheable": "^2.
|
|
105
|
-
"class-transformer": "^0.5.
|
|
106
|
-
"class-validator": "^0.14.0",
|
|
107
|
-
"keyv": "^5.0.
|
|
96
|
+
"@keyv/redis": "^5.1.0",
|
|
97
|
+
"@nestjs/common": "^11.0.0",
|
|
98
|
+
"@nestjs/config": "^4.0.0",
|
|
99
|
+
"@nestjs/core": "^11.0.0",
|
|
100
|
+
"@nestjs/jwt": "^11.0.0",
|
|
101
|
+
"@nestjs/passport": "^11.0.0",
|
|
102
|
+
"@nestjs/swagger": "^11.0.0",
|
|
103
|
+
"@nestjs/typeorm": "^11.0.0",
|
|
104
|
+
"cacheable": "^2.1.0",
|
|
105
|
+
"class-transformer": "^0.5.1",
|
|
106
|
+
"class-validator": "^0.14.0 || ^0.15.0",
|
|
107
|
+
"keyv": "^5.0.0",
|
|
108
108
|
"typeorm": "^0.3.0",
|
|
109
109
|
"winston": "^3.0.0",
|
|
110
110
|
"winston-daily-rotate-file": "^5.0.0",
|
|
111
|
-
"express": "^4.18.0",
|
|
111
|
+
"express": "^4.18.0 || ^5.0.0",
|
|
112
112
|
"rxjs": "^7.8.0"
|
|
113
113
|
},
|
|
114
114
|
"dependencies": {
|
|
115
|
-
"@flusys/nestjs-core": "5.
|
|
115
|
+
"@flusys/nestjs-core": "5.2.0"
|
|
116
116
|
}
|
|
117
117
|
}
|