@flusys/nestjs-shared 1.1.0-beta → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +501 -720
- package/cjs/classes/api-controller.class.js +9 -24
- package/cjs/classes/api-service.class.js +59 -92
- package/cjs/classes/index.js +1 -0
- package/cjs/classes/winston-logger-adapter.class.js +23 -40
- package/cjs/constants/index.js +14 -0
- package/cjs/constants/permissions.js +184 -0
- package/cjs/decorators/api-response.decorator.js +1 -1
- package/cjs/decorators/index.js +1 -0
- package/cjs/decorators/sanitize-html.decorator.js +36 -0
- package/cjs/dtos/delete.dto.js +10 -0
- package/cjs/dtos/filter-and-pagination.dto.js +24 -34
- package/cjs/dtos/pagination.dto.js +4 -8
- package/cjs/dtos/response-payload.dto.js +0 -116
- package/cjs/entities/identity.js +4 -4
- package/cjs/entities/user-root.js +13 -14
- package/cjs/guards/permission.guard.js +51 -105
- package/cjs/interceptors/index.js +1 -3
- package/cjs/interceptors/set-user-field-on-body.interceptor.js +60 -0
- package/cjs/interceptors/slug.interceptor.js +30 -9
- package/cjs/interfaces/datasource.interface.js +4 -0
- package/cjs/interfaces/index.js +2 -1
- package/cjs/interfaces/module-config.interface.js +4 -0
- package/cjs/middlewares/logger.middleware.js +50 -89
- package/cjs/modules/cache/cache.module.js +3 -3
- package/cjs/modules/datasource/datasource.module.js +11 -14
- package/cjs/modules/datasource/multi-tenant-datasource.service.js +29 -113
- package/cjs/modules/utils/utils.service.js +40 -203
- package/cjs/utils/error-handler.util.js +35 -12
- package/cjs/utils/html-sanitizer.util.js +64 -0
- package/cjs/utils/index.js +4 -0
- package/cjs/utils/query-helpers.util.js +53 -0
- package/cjs/utils/request.util.js +70 -0
- package/cjs/utils/string.util.js +63 -0
- package/classes/api-controller.class.d.ts +5 -5
- package/classes/api-service.class.d.ts +7 -5
- package/classes/index.d.ts +1 -0
- package/classes/request-scoped-api.service.d.ts +3 -2
- package/classes/winston-logger-adapter.class.d.ts +2 -0
- package/constants/index.d.ts +1 -0
- package/constants/permissions.d.ts +179 -0
- package/decorators/index.d.ts +1 -0
- package/decorators/sanitize-html.decorator.d.ts +2 -0
- package/dtos/delete.dto.d.ts +1 -0
- package/dtos/filter-and-pagination.dto.d.ts +0 -2
- package/dtos/response-payload.dto.d.ts +0 -20
- package/fesm/classes/api-controller.class.js +9 -24
- package/fesm/classes/api-service.class.js +59 -92
- package/fesm/classes/index.js +2 -0
- package/fesm/classes/winston-logger-adapter.class.js +23 -40
- package/fesm/constants/index.js +2 -0
- package/fesm/constants/permissions.js +128 -0
- package/fesm/decorators/api-response.decorator.js +1 -1
- package/fesm/decorators/index.js +1 -0
- package/fesm/decorators/sanitize-html.decorator.js +45 -0
- package/fesm/dtos/delete.dto.js +12 -2
- package/fesm/dtos/filter-and-pagination.dto.js +26 -47
- package/fesm/dtos/pagination.dto.js +4 -8
- package/fesm/dtos/response-payload.dto.js +0 -107
- package/fesm/entities/identity.js +4 -4
- package/fesm/entities/user-root.js +13 -14
- package/fesm/guards/permission.guard.js +51 -105
- package/fesm/interceptors/index.js +1 -3
- package/fesm/interceptors/set-user-field-on-body.interceptor.js +39 -0
- package/fesm/interceptors/slug.interceptor.js +31 -10
- package/fesm/interfaces/datasource.interface.js +20 -0
- package/fesm/interfaces/index.js +2 -1
- package/fesm/interfaces/module-config.interface.js +5 -0
- package/fesm/middlewares/logger.middleware.js +50 -83
- package/fesm/modules/cache/cache.module.js +2 -2
- package/fesm/modules/datasource/datasource.module.js +11 -14
- package/fesm/modules/datasource/multi-tenant-datasource.service.js +29 -113
- package/fesm/modules/utils/utils.service.js +41 -204
- package/fesm/utils/error-handler.util.js +36 -13
- package/fesm/utils/html-sanitizer.util.js +69 -0
- package/fesm/utils/index.js +4 -0
- package/fesm/utils/query-helpers.util.js +78 -0
- package/fesm/utils/request.util.js +58 -0
- package/fesm/utils/string.util.js +71 -0
- package/guards/permission.guard.d.ts +2 -0
- package/interceptors/index.d.ts +1 -3
- package/interceptors/set-user-field-on-body.interceptor.d.ts +5 -0
- package/interceptors/slug.interceptor.d.ts +2 -1
- package/interfaces/api.interface.d.ts +2 -2
- package/interfaces/datasource.interface.d.ts +5 -0
- package/interfaces/identity.interface.d.ts +4 -4
- package/interfaces/index.d.ts +2 -1
- package/interfaces/logged-user-info.interface.d.ts +0 -2
- package/interfaces/module-config.interface.d.ts +6 -0
- package/interfaces/permission.interface.d.ts +0 -1
- package/middlewares/logger.middleware.d.ts +2 -2
- package/modules/datasource/datasource.module.d.ts +1 -0
- package/modules/datasource/multi-tenant-datasource.service.d.ts +0 -1
- package/modules/utils/utils.service.d.ts +4 -14
- package/package.json +4 -4
- package/utils/error-handler.util.d.ts +14 -19
- package/utils/html-sanitizer.util.d.ts +2 -0
- package/utils/index.d.ts +4 -0
- package/utils/query-helpers.util.d.ts +16 -0
- package/utils/request.util.d.ts +4 -0
- package/utils/string.util.d.ts +2 -0
- package/cjs/interceptors/set-create-by-on-body.interceptor.js +0 -40
- package/cjs/interceptors/set-delete-by-on-body.interceptor.js +0 -40
- package/cjs/interceptors/set-update-by-on-body.interceptor.js +0 -40
- package/cjs/interfaces/base-query.interface.js +0 -6
- package/fesm/interceptors/set-create-by-on-body.interceptor.js +0 -30
- package/fesm/interceptors/set-delete-by-on-body.interceptor.js +0 -30
- package/fesm/interceptors/set-update-by-on-body.interceptor.js +0 -30
- package/fesm/interfaces/base-query.interface.js +0 -3
- package/interceptors/set-create-by-on-body.interceptor.d.ts +0 -5
- package/interceptors/set-delete-by-on-body.interceptor.d.ts +0 -5
- package/interceptors/set-update-by-on-body.interceptor.d.ts +0 -5
- package/interfaces/base-query.interface.d.ts +0 -7
|
@@ -29,26 +29,47 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
29
29
|
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;
|
|
30
30
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
31
31
|
}
|
|
32
|
+
function _ts_metadata(k, v) {
|
|
33
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
34
|
+
}
|
|
35
|
+
function _ts_param(paramIndex, decorator) {
|
|
36
|
+
return function(target, key) {
|
|
37
|
+
decorator(target, key, paramIndex);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
32
40
|
let Slug = class Slug {
|
|
33
41
|
intercept(context, next) {
|
|
34
42
|
const request = context.switchToHttp().getRequest();
|
|
35
43
|
if (Array.isArray(request.body)) {
|
|
36
|
-
request.body = request.body.map((item)=>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
request.body = request.body.map((item)=>{
|
|
45
|
+
// Only generate slug if not provided and name exists
|
|
46
|
+
if (!item.slug && item?.name) {
|
|
47
|
+
return {
|
|
48
|
+
...item,
|
|
49
|
+
slug: this.utilsService.transformToSlug(item.name)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return item;
|
|
53
|
+
});
|
|
54
|
+
} else if (!request.body?.slug && request.body?.name) {
|
|
55
|
+
// Only generate slug if not provided and name exists
|
|
41
56
|
request.body = {
|
|
42
57
|
...request.body,
|
|
43
|
-
slug:
|
|
58
|
+
slug: this.utilsService.transformToSlug(request.body.name)
|
|
44
59
|
};
|
|
45
60
|
}
|
|
46
61
|
return next.handle();
|
|
47
62
|
}
|
|
48
|
-
constructor(){
|
|
49
|
-
_define_property(this, "utilsService",
|
|
63
|
+
constructor(utilsService){
|
|
64
|
+
_define_property(this, "utilsService", void 0);
|
|
65
|
+
this.utilsService = utilsService;
|
|
50
66
|
}
|
|
51
67
|
};
|
|
52
68
|
Slug = _ts_decorate([
|
|
53
|
-
(0, _common.Injectable)()
|
|
69
|
+
(0, _common.Injectable)(),
|
|
70
|
+
_ts_param(0, (0, _common.Inject)(_utilsservice.UtilsService)),
|
|
71
|
+
_ts_metadata("design:type", Function),
|
|
72
|
+
_ts_metadata("design:paramtypes", [
|
|
73
|
+
typeof _utilsservice.UtilsService === "undefined" ? Object : _utilsservice.UtilsService
|
|
74
|
+
])
|
|
54
75
|
], Slug);
|
package/cjs/interfaces/index.js
CHANGED
|
@@ -3,10 +3,11 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
3
3
|
value: true
|
|
4
4
|
});
|
|
5
5
|
_export_star(require("./api.interface"), exports);
|
|
6
|
-
_export_star(require("./
|
|
6
|
+
_export_star(require("./datasource.interface"), exports);
|
|
7
7
|
_export_star(require("./identity.interface"), exports);
|
|
8
8
|
_export_star(require("./logged-user-info.interface"), exports);
|
|
9
9
|
_export_star(require("./logger.interface"), exports);
|
|
10
|
+
_export_star(require("./module-config.interface"), exports);
|
|
10
11
|
_export_star(require("./permission.interface"), exports);
|
|
11
12
|
function _export_star(from, to) {
|
|
12
13
|
Object.keys(from).forEach(function(k) {
|
|
@@ -26,12 +26,6 @@ _export(exports, {
|
|
|
26
26
|
},
|
|
27
27
|
get requestContext () {
|
|
28
28
|
return requestContext;
|
|
29
|
-
},
|
|
30
|
-
get setCompanyId () {
|
|
31
|
-
return setCompanyId;
|
|
32
|
-
},
|
|
33
|
-
get setUserId () {
|
|
34
|
-
return setUserId;
|
|
35
29
|
}
|
|
36
30
|
});
|
|
37
31
|
const _config = require("@flusys/nestjs-core/config");
|
|
@@ -78,14 +72,6 @@ const getRequestId = ()=>requestContext.getStore()?.requestId;
|
|
|
78
72
|
const getTenantId = ()=>requestContext.getStore()?.tenantId;
|
|
79
73
|
const getUserId = ()=>requestContext.getStore()?.userId;
|
|
80
74
|
const getCompanyId = ()=>requestContext.getStore()?.companyId;
|
|
81
|
-
const setUserId = (userId)=>{
|
|
82
|
-
const store = requestContext.getStore();
|
|
83
|
-
if (store) store.userId = userId;
|
|
84
|
-
};
|
|
85
|
-
const setCompanyId = (companyId)=>{
|
|
86
|
-
const store = requestContext.getStore();
|
|
87
|
-
if (store) store.companyId = companyId;
|
|
88
|
-
};
|
|
89
75
|
// Helper Functions
|
|
90
76
|
function sanitizeHeaders(headers) {
|
|
91
77
|
const sanitized = {};
|
|
@@ -107,84 +93,62 @@ let LoggerMiddleware = class LoggerMiddleware {
|
|
|
107
93
|
const requestId = req.headers[_constants.REQUEST_ID_HEADER] || (0, _uuid.v4)();
|
|
108
94
|
const tenantId = req.headers[TENANT_ID_HEADER];
|
|
109
95
|
const startTime = Date.now();
|
|
110
|
-
// Set response header
|
|
111
96
|
res.setHeader(_constants.REQUEST_ID_HEADER, requestId);
|
|
112
|
-
// Create context
|
|
113
97
|
const context = {
|
|
114
98
|
requestId,
|
|
115
99
|
tenantId,
|
|
116
100
|
startTime
|
|
117
101
|
};
|
|
118
|
-
// Run in AsyncLocalStorage context
|
|
119
102
|
requestContext.run(context, ()=>{
|
|
120
|
-
// Check if we should skip logging
|
|
121
103
|
const shouldSkipLogging = EXCLUDED_PATHS.some((path)=>req.originalUrl.startsWith(path));
|
|
122
104
|
if (!shouldSkipLogging) {
|
|
123
105
|
this.logRequest(req, requestId, tenantId);
|
|
124
|
-
|
|
125
|
-
let responseLogged = false;
|
|
126
|
-
// Capture response using multiple hooks to ensure we catch it
|
|
127
|
-
const originalSend = res.send;
|
|
128
|
-
const originalJson = res.json;
|
|
129
|
-
const originalEnd = res.end;
|
|
130
|
-
// Store reference to this for use in callbacks
|
|
131
|
-
const self = this;
|
|
132
|
-
// Override res.send
|
|
133
|
-
res.send = function(body) {
|
|
134
|
-
if (!responseLogged) {
|
|
135
|
-
responseLogged = true;
|
|
136
|
-
self.logResponse(req, res, startTime, body, requestId, tenantId);
|
|
137
|
-
}
|
|
138
|
-
return originalSend.call(this, body);
|
|
139
|
-
};
|
|
140
|
-
// Override res.json
|
|
141
|
-
res.json = function(body) {
|
|
142
|
-
if (!responseLogged) {
|
|
143
|
-
responseLogged = true;
|
|
144
|
-
self.logResponse(req, res, startTime, body, requestId, tenantId);
|
|
145
|
-
}
|
|
146
|
-
return originalJson.call(this, body);
|
|
147
|
-
};
|
|
148
|
-
// Override res.end as fallback
|
|
149
|
-
res.end = function(...args) {
|
|
150
|
-
if (!responseLogged) {
|
|
151
|
-
responseLogged = true;
|
|
152
|
-
self.logResponse(req, res, startTime, args[0], requestId, tenantId);
|
|
153
|
-
}
|
|
154
|
-
return originalEnd.apply(this, args);
|
|
155
|
-
};
|
|
156
|
-
// Handle errors
|
|
157
|
-
res.on('error', (error)=>{
|
|
158
|
-
this.logger.error('Response error', {
|
|
159
|
-
context: 'HTTP',
|
|
160
|
-
requestId,
|
|
161
|
-
tenantId,
|
|
162
|
-
method: req.method,
|
|
163
|
-
url: req.originalUrl,
|
|
164
|
-
path: req.path,
|
|
165
|
-
error: error.message,
|
|
166
|
-
stack: error.stack
|
|
167
|
-
});
|
|
168
|
-
});
|
|
106
|
+
this.setupResponseLogging(req, res, startTime, requestId, tenantId);
|
|
169
107
|
}
|
|
170
108
|
next();
|
|
171
109
|
});
|
|
172
110
|
}
|
|
111
|
+
setupResponseLogging(req, res, startTime, requestId, tenantId) {
|
|
112
|
+
let responseLogged = false;
|
|
113
|
+
const originalSend = res.send;
|
|
114
|
+
const originalJson = res.json;
|
|
115
|
+
const originalEnd = res.end;
|
|
116
|
+
const self = this;
|
|
117
|
+
const logOnce = (body)=>{
|
|
118
|
+
if (!responseLogged) {
|
|
119
|
+
responseLogged = true;
|
|
120
|
+
self.logResponse(req, res, startTime, body, requestId, tenantId);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
res.send = function(body) {
|
|
124
|
+
logOnce(body);
|
|
125
|
+
return originalSend.call(this, body);
|
|
126
|
+
};
|
|
127
|
+
res.json = function(body) {
|
|
128
|
+
logOnce(body);
|
|
129
|
+
return originalJson.call(this, body);
|
|
130
|
+
};
|
|
131
|
+
res.end = function(...args) {
|
|
132
|
+
logOnce(args[0]);
|
|
133
|
+
return originalEnd.apply(this, args);
|
|
134
|
+
};
|
|
135
|
+
res.on('error', (error)=>{
|
|
136
|
+
this.logger.error('Response error', {
|
|
137
|
+
...this.buildBaseLogData(req, requestId, tenantId),
|
|
138
|
+
error: error.message,
|
|
139
|
+
stack: error.stack
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
173
143
|
logRequest(req, requestId, tenantId) {
|
|
174
144
|
const logData = {
|
|
175
|
-
|
|
176
|
-
requestId,
|
|
177
|
-
tenantId,
|
|
178
|
-
method: req.method,
|
|
179
|
-
url: req.originalUrl,
|
|
180
|
-
path: req.path,
|
|
145
|
+
...this.buildBaseLogData(req, requestId, tenantId),
|
|
181
146
|
query: Object.keys(req.query).length > 0 ? req.query : undefined,
|
|
182
147
|
ip: getClientIp(req),
|
|
183
148
|
userAgent: req.headers['user-agent'],
|
|
184
149
|
contentType: req.headers['content-type'],
|
|
185
150
|
contentLength: req.headers['content-length']
|
|
186
151
|
};
|
|
187
|
-
// Add debug details if enabled
|
|
188
152
|
if (IS_DEBUG) {
|
|
189
153
|
logData.headers = sanitizeHeaders(req.headers);
|
|
190
154
|
logData.body = truncateBody(req.body);
|
|
@@ -196,13 +160,12 @@ let LoggerMiddleware = class LoggerMiddleware {
|
|
|
196
160
|
const duration = Date.now() - startTime;
|
|
197
161
|
const statusCode = res.statusCode;
|
|
198
162
|
const level = statusCode >= 500 ? 'error' : statusCode >= 400 ? 'warn' : 'info';
|
|
163
|
+
const userId = getUserId();
|
|
164
|
+
const companyId = getCompanyId();
|
|
199
165
|
const logData = {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
method: req.method,
|
|
204
|
-
url: req.originalUrl,
|
|
205
|
-
path: req.path,
|
|
166
|
+
...this.buildBaseLogData(req, requestId, tenantId),
|
|
167
|
+
userId,
|
|
168
|
+
companyId,
|
|
206
169
|
statusCode,
|
|
207
170
|
statusMessage: res.statusMessage,
|
|
208
171
|
duration: `${duration}ms`,
|
|
@@ -210,33 +173,31 @@ let LoggerMiddleware = class LoggerMiddleware {
|
|
|
210
173
|
contentType: res.getHeader('content-type'),
|
|
211
174
|
contentLength: res.getHeader('content-length')
|
|
212
175
|
};
|
|
213
|
-
// Add user context if available
|
|
214
|
-
const userId = getUserId();
|
|
215
|
-
const companyId = getCompanyId();
|
|
216
|
-
if (userId) logData.userId = userId;
|
|
217
|
-
if (companyId) logData.companyId = companyId;
|
|
218
|
-
// Add response body for errors or debug mode
|
|
219
176
|
if (statusCode >= 400 || IS_DEBUG) {
|
|
220
177
|
logData.responseBody = truncateBody(body);
|
|
221
178
|
}
|
|
222
179
|
this.logger.log(level, `Response [${statusCode}]`, logData);
|
|
223
|
-
// Log slow requests separately
|
|
224
180
|
if (duration > 3000 && statusCode < 400) {
|
|
225
181
|
this.logger.warn('Slow request detected', {
|
|
226
|
-
|
|
227
|
-
requestId,
|
|
228
|
-
tenantId,
|
|
182
|
+
...this.buildBaseLogData(req, requestId, tenantId),
|
|
229
183
|
userId,
|
|
230
184
|
companyId,
|
|
231
|
-
method: req.method,
|
|
232
|
-
url: req.originalUrl,
|
|
233
|
-
path: req.path,
|
|
234
185
|
duration: `${duration}ms`,
|
|
235
186
|
durationMs: duration,
|
|
236
187
|
threshold: '3000ms'
|
|
237
188
|
});
|
|
238
189
|
}
|
|
239
190
|
}
|
|
191
|
+
buildBaseLogData(req, requestId, tenantId) {
|
|
192
|
+
return {
|
|
193
|
+
context: 'HTTP',
|
|
194
|
+
requestId,
|
|
195
|
+
tenantId,
|
|
196
|
+
method: req.method,
|
|
197
|
+
url: req.originalUrl,
|
|
198
|
+
path: req.path
|
|
199
|
+
};
|
|
200
|
+
}
|
|
240
201
|
constructor(){
|
|
241
202
|
_define_property(this, "logger", _winstonloggerclass.instance);
|
|
242
203
|
}
|
|
@@ -8,8 +8,8 @@ Object.defineProperty(exports, "CacheModule", {
|
|
|
8
8
|
return CacheModule;
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
|
-
const
|
|
12
|
-
const _constants = require("
|
|
11
|
+
const _hybridcacheclass = require("../../classes/hybrid-cache.class");
|
|
12
|
+
const _constants = require("../../constants");
|
|
13
13
|
const _common = require("@nestjs/common");
|
|
14
14
|
function _ts_decorate(decorators, target, key, desc) {
|
|
15
15
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
@@ -25,7 +25,7 @@ let CacheModule = class CacheModule {
|
|
|
25
25
|
providers: [
|
|
26
26
|
{
|
|
27
27
|
provide: _constants.CACHE_INSTANCE,
|
|
28
|
-
useFactory: ()=>new
|
|
28
|
+
useFactory: ()=>new _hybridcacheclass.HybridCache(memoryTtl, memorySize)
|
|
29
29
|
}
|
|
30
30
|
],
|
|
31
31
|
exports: [
|
|
@@ -87,24 +87,12 @@ let DataSourceModule = class DataSourceModule {
|
|
|
87
87
|
provide: options.useClass,
|
|
88
88
|
useClass: options.useClass
|
|
89
89
|
},
|
|
90
|
-
|
|
91
|
-
provide: _nestjscore.MODULE_OPTIONS,
|
|
92
|
-
useFactory: async (factory)=>factory.createOptions(),
|
|
93
|
-
inject: [
|
|
94
|
-
options.useClass
|
|
95
|
-
]
|
|
96
|
-
}
|
|
90
|
+
this.createFactoryProvider(options.useClass)
|
|
97
91
|
];
|
|
98
92
|
}
|
|
99
93
|
if (options.useExisting) {
|
|
100
94
|
return [
|
|
101
|
-
|
|
102
|
-
provide: _nestjscore.MODULE_OPTIONS,
|
|
103
|
-
useFactory: async (factory)=>factory.createOptions(),
|
|
104
|
-
inject: [
|
|
105
|
-
options.useExisting
|
|
106
|
-
]
|
|
107
|
-
}
|
|
95
|
+
this.createFactoryProvider(options.useExisting)
|
|
108
96
|
];
|
|
109
97
|
}
|
|
110
98
|
return [
|
|
@@ -114,6 +102,15 @@ let DataSourceModule = class DataSourceModule {
|
|
|
114
102
|
}
|
|
115
103
|
];
|
|
116
104
|
}
|
|
105
|
+
static createFactoryProvider(factoryClass) {
|
|
106
|
+
return {
|
|
107
|
+
provide: _nestjscore.MODULE_OPTIONS,
|
|
108
|
+
useFactory: async (factory)=>factory.createOptions(),
|
|
109
|
+
inject: [
|
|
110
|
+
factoryClass
|
|
111
|
+
]
|
|
112
|
+
};
|
|
113
|
+
}
|
|
117
114
|
};
|
|
118
115
|
DataSourceModule = _ts_decorate([
|
|
119
116
|
(0, _common.Module)({})
|
|
@@ -42,7 +42,6 @@ function _ts_param(paramIndex, decorator) {
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
let MultiTenantDataSourceService = class MultiTenantDataSourceService {
|
|
45
|
-
// Initialization
|
|
46
45
|
initializeFromOptions() {
|
|
47
46
|
if (!this.options) return;
|
|
48
47
|
if (!MultiTenantDataSourceService.initialized) {
|
|
@@ -55,100 +54,57 @@ let MultiTenantDataSourceService = class MultiTenantDataSourceService {
|
|
|
55
54
|
MultiTenantDataSourceService.tenantsRegistry.set(tenant.id, tenant);
|
|
56
55
|
});
|
|
57
56
|
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Set custom tenant header name
|
|
61
|
-
*/ setTenantHeader(header) {
|
|
57
|
+
setTenantHeader(header) {
|
|
62
58
|
this.tenantHeader = header;
|
|
63
59
|
}
|
|
64
|
-
|
|
65
|
-
* Get current database mode
|
|
66
|
-
*/ getDatabaseMode() {
|
|
60
|
+
getDatabaseMode() {
|
|
67
61
|
return this.options?.bootstrapAppConfig?.databaseMode ?? 'single';
|
|
68
62
|
}
|
|
69
|
-
|
|
70
|
-
* Check if running in multi-tenant mode
|
|
71
|
-
*/ isMultiTenant() {
|
|
63
|
+
isMultiTenant() {
|
|
72
64
|
return this.getDatabaseMode() === 'multi-tenant';
|
|
73
65
|
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Get current tenant ID from request header
|
|
77
|
-
* Validates tenant ID format for security (alphanumeric, hyphens, underscores only)
|
|
78
|
-
*/ getCurrentTenantId() {
|
|
66
|
+
getCurrentTenantId() {
|
|
79
67
|
if (!this.request) return null;
|
|
80
68
|
const tenantId = this.request.headers[this.tenantHeader];
|
|
81
69
|
if (!tenantId) return null;
|
|
82
|
-
// Validate tenant ID format to prevent injection attacks
|
|
83
70
|
if (!/^[a-zA-Z0-9_-]+$/.test(tenantId)) {
|
|
84
|
-
throw new _common.BadRequestException('Invalid tenant ID format
|
|
71
|
+
throw new _common.BadRequestException('Invalid tenant ID format');
|
|
85
72
|
}
|
|
86
73
|
return tenantId;
|
|
87
74
|
}
|
|
88
|
-
|
|
89
|
-
* Get current tenant config from request header
|
|
90
|
-
*/ getCurrentTenant() {
|
|
75
|
+
getCurrentTenant() {
|
|
91
76
|
const tenantId = this.getCurrentTenantId();
|
|
92
77
|
return tenantId ? this.getTenant(tenantId) : null;
|
|
93
78
|
}
|
|
94
|
-
|
|
95
|
-
* Get tenant config by ID
|
|
96
|
-
*/ getTenant(tenantId) {
|
|
79
|
+
getTenant(tenantId) {
|
|
97
80
|
return MultiTenantDataSourceService.tenantsRegistry.get(tenantId) ?? null;
|
|
98
81
|
}
|
|
99
|
-
|
|
100
|
-
* Get all registered tenants
|
|
101
|
-
*/ getAllTenants() {
|
|
82
|
+
getAllTenants() {
|
|
102
83
|
return Array.from(MultiTenantDataSourceService.tenantsRegistry.values());
|
|
103
84
|
}
|
|
104
|
-
|
|
105
|
-
* Get only active tenants
|
|
106
|
-
*/ getActiveTenants() {
|
|
85
|
+
getActiveTenants() {
|
|
107
86
|
return this.getAllTenants();
|
|
108
87
|
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Get DataSource for current context (tenant or single)
|
|
112
|
-
*/ async getDataSource() {
|
|
88
|
+
async getDataSource() {
|
|
113
89
|
return this.isMultiTenant() ? this.getTenantDataSource() : this.getSingleDataSource();
|
|
114
90
|
}
|
|
115
|
-
|
|
116
|
-
* Get DataSource for specific tenant
|
|
117
|
-
*/ async getDataSourceForTenant(tenantId) {
|
|
91
|
+
async getDataSourceForTenant(tenantId) {
|
|
118
92
|
const tenant = this.getTenant(tenantId);
|
|
119
|
-
if (!tenant) {
|
|
120
|
-
throw new Error(`Tenant '${tenantId}' not found`);
|
|
121
|
-
}
|
|
93
|
+
if (!tenant) throw new Error(`Tenant '${tenantId}' not found`);
|
|
122
94
|
return this.getOrCreateTenantConnection(tenant);
|
|
123
95
|
}
|
|
124
|
-
|
|
125
|
-
* Set external DataSource (for single-tenant mode)
|
|
126
|
-
*/ setDataSource(dataSource) {
|
|
96
|
+
setDataSource(dataSource) {
|
|
127
97
|
MultiTenantDataSourceService.singleDataSource = dataSource;
|
|
128
98
|
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Get repository for entity in current context
|
|
132
|
-
*/ async getRepository(entity) {
|
|
99
|
+
async getRepository(entity) {
|
|
133
100
|
const dataSource = await this.getDataSource();
|
|
134
101
|
return dataSource.getRepository(entity);
|
|
135
102
|
}
|
|
136
|
-
|
|
137
|
-
* Get repository for entity in specific tenant
|
|
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) {
|
|
103
|
+
async withTenant(tenantId, callback) {
|
|
146
104
|
const dataSource = await this.getDataSourceForTenant(tenantId);
|
|
147
105
|
return callback(dataSource);
|
|
148
106
|
}
|
|
149
|
-
|
|
150
|
-
* Execute callback for all active tenants
|
|
151
|
-
*/ async forAllTenants(callback) {
|
|
107
|
+
async forAllTenants(callback) {
|
|
152
108
|
const results = new Map();
|
|
153
109
|
for (const tenant of this.getActiveTenants()){
|
|
154
110
|
try {
|
|
@@ -160,22 +116,14 @@ let MultiTenantDataSourceService = class MultiTenantDataSourceService {
|
|
|
160
116
|
}
|
|
161
117
|
return results;
|
|
162
118
|
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Register a new tenant at runtime
|
|
166
|
-
*/ registerTenant(tenant) {
|
|
119
|
+
registerTenant(tenant) {
|
|
167
120
|
MultiTenantDataSourceService.tenantsRegistry.set(tenant.id, tenant);
|
|
168
121
|
}
|
|
169
|
-
|
|
170
|
-
* Remove tenant and close its connection
|
|
171
|
-
*/ async removeTenant(tenantId) {
|
|
122
|
+
async removeTenant(tenantId) {
|
|
172
123
|
await this.closeTenantConnection(tenantId);
|
|
173
124
|
MultiTenantDataSourceService.tenantsRegistry.delete(tenantId);
|
|
174
125
|
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Close specific tenant connection
|
|
178
|
-
*/ async closeTenantConnection(tenantId) {
|
|
126
|
+
async closeTenantConnection(tenantId) {
|
|
179
127
|
const connection = MultiTenantDataSourceService.tenantConnections.get(tenantId);
|
|
180
128
|
if (connection?.isInitialized) {
|
|
181
129
|
await connection.destroy();
|
|
@@ -183,16 +131,12 @@ let MultiTenantDataSourceService = class MultiTenantDataSourceService {
|
|
|
183
131
|
this.logger.log(`Closed connection for tenant: ${tenantId}`);
|
|
184
132
|
}
|
|
185
133
|
}
|
|
186
|
-
|
|
187
|
-
* Lifecycle hook - cleanup on module destroy
|
|
188
|
-
*/ async onModuleDestroy() {
|
|
134
|
+
async onModuleDestroy() {
|
|
189
135
|
for (const [tenantId] of MultiTenantDataSourceService.tenantConnections){
|
|
190
136
|
await this.closeTenantConnection(tenantId);
|
|
191
137
|
}
|
|
192
138
|
}
|
|
193
|
-
|
|
194
|
-
* Reset all static state (useful for testing)
|
|
195
|
-
*/ static reset() {
|
|
139
|
+
static reset() {
|
|
196
140
|
MultiTenantDataSourceService.initialized = false;
|
|
197
141
|
MultiTenantDataSourceService.singleDataSource = null;
|
|
198
142
|
MultiTenantDataSourceService.singleConnectionLock = null;
|
|
@@ -236,20 +180,11 @@ let MultiTenantDataSourceService = class MultiTenantDataSourceService {
|
|
|
236
180
|
}
|
|
237
181
|
return this.getOrCreateTenantConnection(tenant);
|
|
238
182
|
}
|
|
239
|
-
|
|
240
|
-
* Get or create connection for tenant with locking to prevent race conditions
|
|
241
|
-
*/ async getOrCreateTenantConnection(tenant) {
|
|
242
|
-
// Return existing initialized connection
|
|
183
|
+
async getOrCreateTenantConnection(tenant) {
|
|
243
184
|
const existing = MultiTenantDataSourceService.tenantConnections.get(tenant.id);
|
|
244
|
-
if (existing?.isInitialized)
|
|
245
|
-
return existing;
|
|
246
|
-
}
|
|
247
|
-
// If another request is creating this tenant's connection, wait for it
|
|
185
|
+
if (existing?.isInitialized) return existing;
|
|
248
186
|
const pendingConnection = MultiTenantDataSourceService.connectionLocks.get(tenant.id);
|
|
249
|
-
if (pendingConnection)
|
|
250
|
-
return pendingConnection;
|
|
251
|
-
}
|
|
252
|
-
// Create connection with lock to prevent race conditions
|
|
187
|
+
if (pendingConnection) return pendingConnection;
|
|
253
188
|
const config = this.buildTenantDatabaseConfig(tenant);
|
|
254
189
|
const connectionPromise = this.createDataSourceFromConfig(config);
|
|
255
190
|
MultiTenantDataSourceService.connectionLocks.set(tenant.id, connectionPromise);
|
|
@@ -262,18 +197,12 @@ let MultiTenantDataSourceService = class MultiTenantDataSourceService {
|
|
|
262
197
|
MultiTenantDataSourceService.connectionLocks.delete(tenant.id);
|
|
263
198
|
}
|
|
264
199
|
}
|
|
265
|
-
|
|
266
|
-
* Get default database config (supports both naming conventions)
|
|
267
|
-
*/ getDefaultDatabaseConfig() {
|
|
200
|
+
getDefaultDatabaseConfig() {
|
|
268
201
|
return this.options?.defaultDatabaseConfig ?? this.options?.tenantDefaultDatabaseConfig;
|
|
269
202
|
}
|
|
270
|
-
|
|
271
|
-
* Build database config for tenant (merges with default)
|
|
272
|
-
*/ buildTenantDatabaseConfig(tenant) {
|
|
203
|
+
buildTenantDatabaseConfig(tenant) {
|
|
273
204
|
const defaultConfig = this.getDefaultDatabaseConfig();
|
|
274
|
-
if (!defaultConfig)
|
|
275
|
-
throw new Error('No default database config for multi-tenant mode.');
|
|
276
|
-
}
|
|
205
|
+
if (!defaultConfig) throw new Error('No default database config for multi-tenant mode.');
|
|
277
206
|
return {
|
|
278
207
|
type: defaultConfig.type,
|
|
279
208
|
host: tenant.host ?? defaultConfig.host,
|
|
@@ -283,15 +212,7 @@ let MultiTenantDataSourceService = class MultiTenantDataSourceService {
|
|
|
283
212
|
database: tenant.database
|
|
284
213
|
};
|
|
285
214
|
}
|
|
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 = []) {
|
|
215
|
+
async createDataSourceFromConfig(config, entities = []) {
|
|
295
216
|
const dataSource = new _typeorm.DataSource({
|
|
296
217
|
type: config.type,
|
|
297
218
|
host: config.host,
|
|
@@ -303,16 +224,13 @@ let MultiTenantDataSourceService = class MultiTenantDataSourceService {
|
|
|
303
224
|
synchronize: false,
|
|
304
225
|
namingStrategy: new _typeormnamingstrategies.SnakeNamingStrategy()
|
|
305
226
|
});
|
|
306
|
-
if (!dataSource.isInitialized)
|
|
307
|
-
await dataSource.initialize();
|
|
308
|
-
}
|
|
227
|
+
if (!dataSource.isInitialized) await dataSource.initialize();
|
|
309
228
|
return dataSource;
|
|
310
229
|
}
|
|
311
230
|
constructor(options, request){
|
|
312
231
|
_define_property(this, "options", void 0);
|
|
313
232
|
_define_property(this, "request", void 0);
|
|
314
233
|
_define_property(this, "logger", void 0);
|
|
315
|
-
// Instance state
|
|
316
234
|
_define_property(this, "tenantHeader", void 0);
|
|
317
235
|
this.options = options;
|
|
318
236
|
this.request = request;
|
|
@@ -321,12 +239,10 @@ let MultiTenantDataSourceService = class MultiTenantDataSourceService {
|
|
|
321
239
|
this.initializeFromOptions();
|
|
322
240
|
}
|
|
323
241
|
};
|
|
324
|
-
// Static state shared across all instances
|
|
325
242
|
_define_property(MultiTenantDataSourceService, "tenantConnections", new Map());
|
|
326
243
|
_define_property(MultiTenantDataSourceService, "singleDataSource", null);
|
|
327
244
|
_define_property(MultiTenantDataSourceService, "tenantsRegistry", new Map());
|
|
328
245
|
_define_property(MultiTenantDataSourceService, "initialized", false);
|
|
329
|
-
// Connection locks to prevent race conditions during concurrent connection creation
|
|
330
246
|
_define_property(MultiTenantDataSourceService, "connectionLocks", new Map());
|
|
331
247
|
_define_property(MultiTenantDataSourceService, "singleConnectionLock", null);
|
|
332
248
|
MultiTenantDataSourceService = _ts_decorate([
|