@flusys/nestjs-shared 3.0.0 → 4.0.0-rc
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 +160 -80
- package/cjs/classes/api-controller.class.js +26 -8
- package/cjs/classes/api-service.class.js +100 -17
- package/cjs/classes/winston-logger-adapter.class.js +15 -20
- package/cjs/classes/winston.logger.class.js +103 -70
- package/cjs/constants/index.js +1 -0
- package/cjs/constants/message-keys.js +80 -0
- package/cjs/constants/permissions.js +65 -11
- package/cjs/decorators/index.js +1 -0
- package/cjs/decorators/log-action.decorator.js +149 -0
- package/cjs/dtos/response-payload.dto.js +72 -0
- 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 +145 -0
- package/cjs/exceptions/index.js +1 -0
- package/cjs/exceptions/permission.exception.js +12 -8
- package/cjs/filters/global-exception.filter.js +167 -0
- package/cjs/filters/index.js +18 -0
- package/cjs/guards/jwt-auth.guard.js +4 -1
- package/cjs/guards/permission.guard.js +6 -13
- package/cjs/index.js +2 -0
- package/cjs/interceptors/idempotency.interceptor.js +1 -1
- package/cjs/interceptors/index.js +0 -1
- package/cjs/interfaces/event-manager-adapter.interface.js +11 -0
- package/cjs/interfaces/index.js +2 -0
- package/cjs/interfaces/logger.interface.js +1 -4
- package/cjs/interfaces/notification-adapter.interface.js +11 -0
- package/cjs/middlewares/logger.middleware.js +83 -26
- package/cjs/modules/datasource/multi-tenant-datasource.service.js +33 -11
- package/cjs/modules/utils/utils.service.js +4 -20
- package/cjs/utils/index.js +0 -1
- package/cjs/utils/query-helpers.util.js +8 -1
- package/classes/api-controller.class.d.ts +1 -0
- package/classes/api-service.class.d.ts +5 -10
- package/classes/winston-logger-adapter.class.d.ts +12 -11
- package/classes/winston.logger.class.d.ts +1 -0
- package/constants/index.d.ts +1 -0
- package/constants/message-keys.d.ts +81 -0
- package/constants/permissions.d.ts +72 -0
- package/decorators/index.d.ts +1 -0
- package/decorators/log-action.decorator.d.ts +8 -0
- package/dtos/response-payload.dto.d.ts +8 -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 +1 -1
- package/fesm/classes/api-controller.class.js +26 -8
- package/fesm/classes/api-service.class.js +101 -18
- package/fesm/classes/winston-logger-adapter.class.js +18 -44
- package/fesm/classes/winston.logger.class.js +100 -68
- package/fesm/constants/index.js +2 -0
- package/fesm/constants/message-keys.js +59 -0
- package/fesm/constants/permissions.js +51 -14
- package/fesm/decorators/index.js +1 -0
- package/fesm/decorators/log-action.decorator.js +139 -0
- package/fesm/dtos/response-payload.dto.js +72 -0
- 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 +109 -0
- package/fesm/exceptions/index.js +1 -0
- package/fesm/exceptions/permission.exception.js +15 -17
- package/fesm/filters/global-exception.filter.js +157 -0
- package/fesm/filters/index.js +1 -0
- package/fesm/guards/jwt-auth.guard.js +5 -2
- package/fesm/guards/permission.guard.js +8 -15
- package/fesm/index.js +2 -0
- package/fesm/interceptors/idempotency.interceptor.js +2 -2
- package/fesm/interceptors/index.js +0 -1
- package/fesm/interfaces/event-manager-adapter.interface.js +1 -0
- package/fesm/interfaces/index.js +2 -0
- package/fesm/interfaces/logger.interface.js +1 -4
- package/fesm/interfaces/notification-adapter.interface.js +1 -0
- package/fesm/middlewares/logger.middleware.js +83 -26
- package/fesm/modules/datasource/multi-tenant-datasource.service.js +34 -12
- package/fesm/modules/utils/utils.service.js +5 -21
- package/fesm/utils/index.js +0 -1
- package/fesm/utils/query-helpers.util.js +8 -1
- package/filters/global-exception.filter.d.ts +10 -0
- package/filters/index.d.ts +1 -0
- package/guards/permission.guard.d.ts +1 -3
- package/index.d.ts +2 -0
- package/interceptors/index.d.ts +0 -1
- package/interfaces/event-manager-adapter.interface.d.ts +43 -0
- package/interfaces/index.d.ts +2 -0
- package/interfaces/logger.interface.d.ts +5 -5
- package/interfaces/notification-adapter.interface.d.ts +22 -0
- package/modules/datasource/multi-tenant-datasource.service.d.ts +1 -2
- package/modules/utils/utils.service.d.ts +0 -1
- package/package.json +7 -2
- package/utils/index.d.ts +0 -1
- package/cjs/interceptors/query-performance.interceptor.js +0 -66
- package/cjs/utils/error-handler.util.js +0 -90
- package/fesm/interceptors/query-performance.interceptor.js +0 -56
- package/fesm/utils/error-handler.util.js +0 -82
- package/interceptors/query-performance.interceptor.d.ts +0 -8
- package/utils/error-handler.util.d.ts +0 -19
|
@@ -61,7 +61,7 @@ let IdempotencyInterceptor = class IdempotencyInterceptor {
|
|
|
61
61
|
throw new _common.ConflictException({
|
|
62
62
|
success: false,
|
|
63
63
|
message: 'Request is already being processed',
|
|
64
|
-
|
|
64
|
+
messageKey: _constants.SYSTEM_MESSAGES.DUPLICATE_REQUEST
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
67
|
// Return cached response
|
|
@@ -4,7 +4,6 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
});
|
|
5
5
|
_export_star(require("./delete-empty-id-from-body.interceptor"), exports);
|
|
6
6
|
_export_star(require("./idempotency.interceptor"), exports);
|
|
7
|
-
_export_star(require("./query-performance.interceptor"), exports);
|
|
8
7
|
_export_star(require("./response-meta.interceptor"), exports);
|
|
9
8
|
_export_star(require("./set-user-field-on-body.interceptor"), exports);
|
|
10
9
|
_export_star(require("./slug.interceptor"), exports);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "EVENT_MANAGER_ADAPTER", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return EVENT_MANAGER_ADAPTER;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const EVENT_MANAGER_ADAPTER = 'EVENT_MANAGER_ADAPTER';
|
package/cjs/interfaces/index.js
CHANGED
|
@@ -4,10 +4,12 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
});
|
|
5
5
|
_export_star(require("./api.interface"), exports);
|
|
6
6
|
_export_star(require("./datasource.interface"), exports);
|
|
7
|
+
_export_star(require("./event-manager-adapter.interface"), exports);
|
|
7
8
|
_export_star(require("./identity.interface"), exports);
|
|
8
9
|
_export_star(require("./logged-user-info.interface"), exports);
|
|
9
10
|
_export_star(require("./logger.interface"), exports);
|
|
10
11
|
_export_star(require("./module-config.interface"), exports);
|
|
12
|
+
_export_star(require("./notification-adapter.interface"), exports);
|
|
11
13
|
_export_star(require("./permission.interface"), exports);
|
|
12
14
|
function _export_star(from, to) {
|
|
13
15
|
Object.keys(from).forEach(function(k) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "NOTIFICATION_ADAPTER", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return NOTIFICATION_ADAPTER;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const NOTIFICATION_ADAPTER = 'NOTIFICATION_ADAPTER';
|
|
@@ -54,18 +54,15 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
54
54
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
55
55
|
}
|
|
56
56
|
// Configuration
|
|
57
|
-
const
|
|
57
|
+
const logConfig = _config.envConfig.getLogConfig();
|
|
58
|
+
const IS_DEBUG = logConfig.level === 'debug';
|
|
59
|
+
const DISABLE_HTTP_LOGGING = logConfig.disableHttpLogging;
|
|
58
60
|
const TENANT_ID_HEADER = 'x-tenant-id';
|
|
59
61
|
const EXCLUDED_PATHS = [
|
|
60
62
|
'/health',
|
|
61
63
|
'/metrics',
|
|
62
64
|
'/favicon.ico'
|
|
63
65
|
];
|
|
64
|
-
const EXCLUDED_HEADERS = [
|
|
65
|
-
'authorization',
|
|
66
|
-
'cookie',
|
|
67
|
-
'x-api-key'
|
|
68
|
-
];
|
|
69
66
|
const MAX_BODY_LOG_SIZE = 1000;
|
|
70
67
|
const requestContext = new _async_hooks.AsyncLocalStorage();
|
|
71
68
|
const getRequestId = ()=>requestContext.getStore()?.requestId;
|
|
@@ -73,17 +70,71 @@ const getTenantId = ()=>requestContext.getStore()?.tenantId;
|
|
|
73
70
|
const getUserId = ()=>requestContext.getStore()?.userId;
|
|
74
71
|
const getCompanyId = ()=>requestContext.getStore()?.companyId;
|
|
75
72
|
// Helper Functions
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
function summarizeRequestBody(body) {
|
|
74
|
+
if (!body) return undefined;
|
|
75
|
+
const parsed = typeof body === 'string' ? tryParseJson(body) : body;
|
|
76
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
77
|
+
const str = String(body);
|
|
78
|
+
return str.length > MAX_BODY_LOG_SIZE ? str.substring(0, MAX_BODY_LOG_SIZE) + '...' : str;
|
|
79
|
+
}
|
|
80
|
+
// For API request bodies, provide a concise summary
|
|
81
|
+
const summary = {};
|
|
82
|
+
// Pagination info
|
|
83
|
+
if (parsed.pagination) {
|
|
84
|
+
summary.page = parsed.pagination.currentPage;
|
|
85
|
+
summary.pageSize = parsed.pagination.pageSize;
|
|
86
|
+
}
|
|
87
|
+
// Filter keys only (not values - may contain sensitive data)
|
|
88
|
+
if (parsed.filter && typeof parsed.filter === 'object') {
|
|
89
|
+
const filterKeys = Object.keys(parsed.filter);
|
|
90
|
+
if (filterKeys.length > 0) summary.filterKeys = filterKeys;
|
|
91
|
+
}
|
|
92
|
+
// Select fields count
|
|
93
|
+
if (Array.isArray(parsed.select) && parsed.select.length > 0) {
|
|
94
|
+
summary.selectCount = parsed.select.length;
|
|
95
|
+
}
|
|
96
|
+
// Sort keys
|
|
97
|
+
if (parsed.sort && typeof parsed.sort === 'object') {
|
|
98
|
+
const sortKeys = Object.keys(parsed.sort);
|
|
99
|
+
if (sortKeys.length > 0) summary.sortKeys = sortKeys;
|
|
80
100
|
}
|
|
81
|
-
|
|
101
|
+
// ID for single item operations
|
|
102
|
+
if (parsed.id) summary.id = parsed.id;
|
|
103
|
+
// IDs for bulk operations
|
|
104
|
+
if (Array.isArray(parsed.ids)) summary.idsCount = parsed.ids.length;
|
|
105
|
+
return Object.keys(summary).length > 0 ? JSON.stringify(summary) : '{}';
|
|
82
106
|
}
|
|
83
|
-
function
|
|
107
|
+
function summarizeResponseBody(body) {
|
|
84
108
|
if (!body) return undefined;
|
|
85
|
-
const
|
|
86
|
-
|
|
109
|
+
const parsed = typeof body === 'string' ? tryParseJson(body) : body;
|
|
110
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
111
|
+
const str = String(body);
|
|
112
|
+
return str.length > MAX_BODY_LOG_SIZE ? str.substring(0, MAX_BODY_LOG_SIZE) + '...' : str;
|
|
113
|
+
}
|
|
114
|
+
const summary = {};
|
|
115
|
+
if ('success' in parsed) summary.success = parsed.success;
|
|
116
|
+
if ('message' in parsed) summary.message = parsed.message;
|
|
117
|
+
if ('code' in parsed) summary.code = parsed.code;
|
|
118
|
+
if (Array.isArray(parsed.data)) {
|
|
119
|
+
summary.dataCount = parsed.data.length;
|
|
120
|
+
} else if (parsed.data && typeof parsed.data === 'object') {
|
|
121
|
+
summary.hasData = true;
|
|
122
|
+
}
|
|
123
|
+
if (parsed.meta) {
|
|
124
|
+
summary.total = parsed.meta.total;
|
|
125
|
+
summary.page = parsed.meta.page;
|
|
126
|
+
}
|
|
127
|
+
if (parsed.errors) {
|
|
128
|
+
summary.errorsCount = Array.isArray(parsed.errors) ? parsed.errors.length : 1;
|
|
129
|
+
}
|
|
130
|
+
return JSON.stringify(summary);
|
|
131
|
+
}
|
|
132
|
+
function tryParseJson(str) {
|
|
133
|
+
try {
|
|
134
|
+
return JSON.parse(str);
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
87
138
|
}
|
|
88
139
|
function getClientIp(req) {
|
|
89
140
|
return req.headers['x-forwarded-for']?.split(',')[0] || req.socket?.remoteAddress || 'unknown';
|
|
@@ -100,7 +151,7 @@ let LoggerMiddleware = class LoggerMiddleware {
|
|
|
100
151
|
startTime
|
|
101
152
|
};
|
|
102
153
|
requestContext.run(context, ()=>{
|
|
103
|
-
const shouldSkipLogging = EXCLUDED_PATHS.some((path)=>req.originalUrl.startsWith(path));
|
|
154
|
+
const shouldSkipLogging = DISABLE_HTTP_LOGGING || EXCLUDED_PATHS.some((path)=>req.originalUrl.startsWith(path));
|
|
104
155
|
if (!shouldSkipLogging) {
|
|
105
156
|
this.logRequest(req, requestId, tenantId);
|
|
106
157
|
this.setupResponseLogging(req, res, startTime, requestId, tenantId);
|
|
@@ -141,20 +192,22 @@ let LoggerMiddleware = class LoggerMiddleware {
|
|
|
141
192
|
});
|
|
142
193
|
}
|
|
143
194
|
logRequest(req, requestId, tenantId) {
|
|
195
|
+
const ip = getClientIp(req);
|
|
196
|
+
const contentType = req.headers['content-type'] || 'none';
|
|
197
|
+
// Build descriptive message like: Started processing request "POST" "/api/users" from "192.168.1.1"
|
|
198
|
+
const message = `Started processing request "${req.method}" "${req.originalUrl}" from "${ip}"`;
|
|
144
199
|
const logData = {
|
|
145
200
|
...this.buildBaseLogData(req, requestId, tenantId),
|
|
146
201
|
query: Object.keys(req.query).length > 0 ? req.query : undefined,
|
|
147
|
-
ip
|
|
202
|
+
ip,
|
|
148
203
|
userAgent: req.headers['user-agent'],
|
|
149
|
-
contentType
|
|
204
|
+
contentType,
|
|
150
205
|
contentLength: req.headers['content-length']
|
|
151
206
|
};
|
|
152
207
|
if (IS_DEBUG) {
|
|
153
|
-
logData.
|
|
154
|
-
logData.body = truncateBody(req.body);
|
|
155
|
-
logData.params = Object.keys(req.params).length > 0 ? req.params : undefined;
|
|
208
|
+
logData.body = summarizeRequestBody(req.body);
|
|
156
209
|
}
|
|
157
|
-
this.logger.info(
|
|
210
|
+
this.logger.info(message, logData);
|
|
158
211
|
}
|
|
159
212
|
logResponse(req, res, startTime, body, requestId, tenantId) {
|
|
160
213
|
const duration = Date.now() - startTime;
|
|
@@ -162,6 +215,10 @@ let LoggerMiddleware = class LoggerMiddleware {
|
|
|
162
215
|
const level = statusCode >= 500 ? 'error' : statusCode >= 400 ? 'warn' : 'info';
|
|
163
216
|
const userId = getUserId();
|
|
164
217
|
const companyId = getCompanyId();
|
|
218
|
+
const contentType = res.getHeader('content-type') || 'none';
|
|
219
|
+
const contentLength = res.getHeader('content-length') || 'null';
|
|
220
|
+
// Build descriptive message like: Request finished "HTTP/1.1" "POST" "/api/users" - 200 "application/json" 534.29ms
|
|
221
|
+
const message = `Request finished "HTTP/1.1" "${req.method}" "${req.originalUrl}" - ${statusCode} ${contentLength} "${contentType}" ${duration.toFixed(2)}ms`;
|
|
165
222
|
const logData = {
|
|
166
223
|
...this.buildBaseLogData(req, requestId, tenantId),
|
|
167
224
|
userId,
|
|
@@ -170,15 +227,15 @@ let LoggerMiddleware = class LoggerMiddleware {
|
|
|
170
227
|
statusMessage: res.statusMessage,
|
|
171
228
|
duration: `${duration}ms`,
|
|
172
229
|
durationMs: duration,
|
|
173
|
-
contentType
|
|
174
|
-
contentLength
|
|
230
|
+
contentType,
|
|
231
|
+
contentLength
|
|
175
232
|
};
|
|
176
233
|
if (statusCode >= 400 || IS_DEBUG) {
|
|
177
|
-
logData.responseBody =
|
|
234
|
+
logData.responseBody = summarizeResponseBody(body);
|
|
178
235
|
}
|
|
179
|
-
this.logger.log(level,
|
|
236
|
+
this.logger.log(level, message, logData);
|
|
180
237
|
if (duration > 3000 && statusCode < 400) {
|
|
181
|
-
this.logger.warn(
|
|
238
|
+
this.logger.warn(`Slow request detected: "${req.method}" "${req.originalUrl}" took ${duration}ms (threshold: 3000ms)`, {
|
|
182
239
|
...this.buildBaseLogData(req, requestId, tenantId),
|
|
183
240
|
userId,
|
|
184
241
|
companyId,
|
|
@@ -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");
|
|
@@ -68,7 +69,10 @@ let MultiTenantDataSourceService = class MultiTenantDataSourceService {
|
|
|
68
69
|
const tenantId = this.request.headers[this.tenantHeader];
|
|
69
70
|
if (!tenantId) return null;
|
|
70
71
|
if (!/^[a-zA-Z0-9_-]+$/.test(tenantId)) {
|
|
71
|
-
throw new _common.BadRequestException(
|
|
72
|
+
throw new _common.BadRequestException({
|
|
73
|
+
message: 'Invalid tenant ID format',
|
|
74
|
+
messageKey: _constants.SYSTEM_MESSAGES.INVALID_TENANT_ID
|
|
75
|
+
});
|
|
72
76
|
}
|
|
73
77
|
return tenantId;
|
|
74
78
|
}
|
|
@@ -90,7 +94,15 @@ let MultiTenantDataSourceService = class MultiTenantDataSourceService {
|
|
|
90
94
|
}
|
|
91
95
|
async getDataSourceForTenant(tenantId) {
|
|
92
96
|
const tenant = this.getTenant(tenantId);
|
|
93
|
-
if (!tenant)
|
|
97
|
+
if (!tenant) {
|
|
98
|
+
throw new _common.BadRequestException({
|
|
99
|
+
message: `Tenant '${tenantId}' not found`,
|
|
100
|
+
messageKey: _constants.SYSTEM_MESSAGES.TENANT_NOT_FOUND,
|
|
101
|
+
messageParams: {
|
|
102
|
+
tenantId
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
94
106
|
return this.getOrCreateTenantConnection(tenant);
|
|
95
107
|
}
|
|
96
108
|
setDataSource(dataSource) {
|
|
@@ -110,8 +122,8 @@ let MultiTenantDataSourceService = class MultiTenantDataSourceService {
|
|
|
110
122
|
try {
|
|
111
123
|
const dataSource = await this.getOrCreateTenantConnection(tenant);
|
|
112
124
|
results.set(tenant.id, await callback(tenant, dataSource));
|
|
113
|
-
} catch
|
|
114
|
-
|
|
125
|
+
} catch {
|
|
126
|
+
// Silent failure for individual tenant
|
|
115
127
|
}
|
|
116
128
|
}
|
|
117
129
|
return results;
|
|
@@ -128,7 +140,6 @@ let MultiTenantDataSourceService = class MultiTenantDataSourceService {
|
|
|
128
140
|
if (connection?.isInitialized) {
|
|
129
141
|
await connection.destroy();
|
|
130
142
|
MultiTenantDataSourceService.tenantConnections.delete(tenantId);
|
|
131
|
-
this.logger.log(`Closed connection for tenant: ${tenantId}`);
|
|
132
143
|
}
|
|
133
144
|
}
|
|
134
145
|
async onModuleDestroy() {
|
|
@@ -158,7 +169,10 @@ let MultiTenantDataSourceService = class MultiTenantDataSourceService {
|
|
|
158
169
|
}
|
|
159
170
|
const config = this.getDefaultDatabaseConfig();
|
|
160
171
|
if (!config) {
|
|
161
|
-
throw new
|
|
172
|
+
throw new _common.InternalServerErrorException({
|
|
173
|
+
message: 'No database config available. Provide defaultDatabaseConfig or tenantDefaultDatabaseConfig.',
|
|
174
|
+
messageKey: _constants.SYSTEM_MESSAGES.DATABASE_CONFIG_NOT_AVAILABLE
|
|
175
|
+
});
|
|
162
176
|
}
|
|
163
177
|
// Create connection with lock to prevent race conditions
|
|
164
178
|
const connectionPromise = this.createDataSourceFromConfig(config);
|
|
@@ -176,7 +190,13 @@ let MultiTenantDataSourceService = class MultiTenantDataSourceService {
|
|
|
176
190
|
*/ async getTenantDataSource() {
|
|
177
191
|
const tenant = this.getCurrentTenant();
|
|
178
192
|
if (!tenant) {
|
|
179
|
-
throw new
|
|
193
|
+
throw new _common.BadRequestException({
|
|
194
|
+
message: `Tenant not found. Ensure '${this.tenantHeader}' header is set.`,
|
|
195
|
+
messageKey: _constants.SYSTEM_MESSAGES.TENANT_HEADER_REQUIRED,
|
|
196
|
+
messageParams: {
|
|
197
|
+
header: this.tenantHeader
|
|
198
|
+
}
|
|
199
|
+
});
|
|
180
200
|
}
|
|
181
201
|
return this.getOrCreateTenantConnection(tenant);
|
|
182
202
|
}
|
|
@@ -191,7 +211,6 @@ let MultiTenantDataSourceService = class MultiTenantDataSourceService {
|
|
|
191
211
|
try {
|
|
192
212
|
const dataSource = await connectionPromise;
|
|
193
213
|
MultiTenantDataSourceService.tenantConnections.set(tenant.id, dataSource);
|
|
194
|
-
this.logger.log(`Created connection for tenant: ${tenant.id}`);
|
|
195
214
|
return dataSource;
|
|
196
215
|
} finally{
|
|
197
216
|
MultiTenantDataSourceService.connectionLocks.delete(tenant.id);
|
|
@@ -202,7 +221,12 @@ let MultiTenantDataSourceService = class MultiTenantDataSourceService {
|
|
|
202
221
|
}
|
|
203
222
|
buildTenantDatabaseConfig(tenant) {
|
|
204
223
|
const defaultConfig = this.getDefaultDatabaseConfig();
|
|
205
|
-
if (!defaultConfig)
|
|
224
|
+
if (!defaultConfig) {
|
|
225
|
+
throw new _common.InternalServerErrorException({
|
|
226
|
+
message: 'No default database config for multi-tenant mode.',
|
|
227
|
+
messageKey: _constants.SYSTEM_MESSAGES.DATABASE_CONFIG_NOT_AVAILABLE
|
|
228
|
+
});
|
|
229
|
+
}
|
|
206
230
|
return {
|
|
207
231
|
type: defaultConfig.type,
|
|
208
232
|
host: tenant.host ?? defaultConfig.host,
|
|
@@ -230,11 +254,9 @@ let MultiTenantDataSourceService = class MultiTenantDataSourceService {
|
|
|
230
254
|
constructor(options, request){
|
|
231
255
|
_define_property(this, "options", void 0);
|
|
232
256
|
_define_property(this, "request", void 0);
|
|
233
|
-
_define_property(this, "logger", void 0);
|
|
234
257
|
_define_property(this, "tenantHeader", void 0);
|
|
235
258
|
this.options = options;
|
|
236
259
|
this.request = request;
|
|
237
|
-
this.logger = new _common.Logger(this.constructor.name);
|
|
238
260
|
this.tenantHeader = _nestjscore.DEFAULT_TENANT_HEADER;
|
|
239
261
|
this.initializeFromOptions();
|
|
240
262
|
}
|
|
@@ -9,19 +9,6 @@ Object.defineProperty(exports, "UtilsService", {
|
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
11
|
const _common = require("@nestjs/common");
|
|
12
|
-
function _define_property(obj, key, value) {
|
|
13
|
-
if (key in obj) {
|
|
14
|
-
Object.defineProperty(obj, key, {
|
|
15
|
-
value: value,
|
|
16
|
-
enumerable: true,
|
|
17
|
-
configurable: true,
|
|
18
|
-
writable: true
|
|
19
|
-
});
|
|
20
|
-
} else {
|
|
21
|
-
obj[key] = value;
|
|
22
|
-
}
|
|
23
|
-
return obj;
|
|
24
|
-
}
|
|
25
12
|
function _ts_decorate(decorators, target, key, desc) {
|
|
26
13
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
27
14
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -47,8 +34,8 @@ let UtilsService = class UtilsService {
|
|
|
47
34
|
const keys = await cacheManager.get(trackingKey) || [];
|
|
48
35
|
if (!keys.includes(cacheKey)) keys.push(cacheKey);
|
|
49
36
|
await cacheManager.set(trackingKey, keys);
|
|
50
|
-
} catch
|
|
51
|
-
|
|
37
|
+
} catch {
|
|
38
|
+
// Silent failure - cache tracking is non-critical
|
|
52
39
|
}
|
|
53
40
|
}
|
|
54
41
|
/**
|
|
@@ -59,8 +46,8 @@ let UtilsService = class UtilsService {
|
|
|
59
46
|
const keys = await cacheManager.get(trackingKey) || [];
|
|
60
47
|
await Promise.allSettled(keys.map((key)=>cacheManager.del(key)));
|
|
61
48
|
await cacheManager.del(trackingKey);
|
|
62
|
-
} catch
|
|
63
|
-
|
|
49
|
+
} catch {
|
|
50
|
+
// Silent failure - cache invalidation is non-critical
|
|
64
51
|
}
|
|
65
52
|
}
|
|
66
53
|
// ---------------- STRING HELPERS ----------------
|
|
@@ -83,9 +70,6 @@ let UtilsService = class UtilsService {
|
|
|
83
70
|
const prefix = this.buildTenantPrefix(tenantId);
|
|
84
71
|
return entityId ? `${prefix}entity_${entityName}_id_${entityId}_keys` : `${prefix}entity_${entityName}_keys`;
|
|
85
72
|
}
|
|
86
|
-
constructor(){
|
|
87
|
-
_define_property(this, "logger", new _common.Logger(UtilsService.name));
|
|
88
|
-
}
|
|
89
73
|
};
|
|
90
74
|
UtilsService = _ts_decorate([
|
|
91
75
|
(0, _common.Injectable)()
|
package/cjs/utils/index.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", {
|
|
3
3
|
value: true
|
|
4
4
|
});
|
|
5
|
-
_export_star(require("./error-handler.util"), exports);
|
|
6
5
|
_export_star(require("./html-sanitizer.util"), exports);
|
|
7
6
|
_export_star(require("./query-helpers.util"), exports);
|
|
8
7
|
_export_star(require("./request.util"), exports);
|
|
@@ -23,6 +23,7 @@ _export(exports, {
|
|
|
23
23
|
}
|
|
24
24
|
});
|
|
25
25
|
const _common = require("@nestjs/common");
|
|
26
|
+
const _constants = require("../constants");
|
|
26
27
|
function applyCompanyFilter(query, config, user) {
|
|
27
28
|
const columnName = config.columnName ?? 'companyId';
|
|
28
29
|
if (config.isCompanyFeatureEnabled && user?.companyId) {
|
|
@@ -47,7 +48,13 @@ function hasCompanyId(entity) {
|
|
|
47
48
|
function validateCompanyOwnership(entity, user, isCompanyFeatureEnabled, entityName) {
|
|
48
49
|
if (isCompanyFeatureEnabled && user?.companyId && hasCompanyId(entity)) {
|
|
49
50
|
if (entity.companyId && entity.companyId !== user.companyId) {
|
|
50
|
-
throw new _common.BadRequestException(
|
|
51
|
+
throw new _common.BadRequestException({
|
|
52
|
+
message: `${entityName} belongs to another company`,
|
|
53
|
+
messageKey: _constants.AUTH_MESSAGES.COMPANY_NO_ACCESS,
|
|
54
|
+
messageParams: {
|
|
55
|
+
entity: entityName
|
|
56
|
+
}
|
|
57
|
+
});
|
|
51
58
|
}
|
|
52
59
|
}
|
|
53
60
|
}
|
|
@@ -15,6 +15,7 @@ export type ApiSecurityConfig = {
|
|
|
15
15
|
};
|
|
16
16
|
export interface ApiControllerOptions {
|
|
17
17
|
security?: ApiSecurityConfig | EndpointSecurity | SecurityLevel;
|
|
18
|
+
entityName?: string;
|
|
18
19
|
}
|
|
19
20
|
export declare function createApiController<CreateDtoT extends object, UpdateDtoT extends {
|
|
20
21
|
id: string;
|
|
@@ -2,7 +2,6 @@ import { DeleteDto, FilterAndPaginationDto } from '../dtos';
|
|
|
2
2
|
import { Identity } from '../entities';
|
|
3
3
|
import { ILoggedUserInfo, IService } from '../interfaces';
|
|
4
4
|
import { UtilsService } from '../modules/utils/utils.service';
|
|
5
|
-
import { Logger } from '@nestjs/common';
|
|
6
5
|
import { QueryRunner, Repository, SelectQueryBuilder } from 'typeorm';
|
|
7
6
|
import { HybridCache } from './hybrid-cache.class';
|
|
8
7
|
export declare abstract class ApiService<CreateDtoT extends object, UpdateDtoT extends {
|
|
@@ -12,10 +11,10 @@ export declare abstract class ApiService<CreateDtoT extends object, UpdateDtoT e
|
|
|
12
11
|
protected repository: RepositoryT;
|
|
13
12
|
protected cacheManager: HybridCache;
|
|
14
13
|
protected utilsService: UtilsService;
|
|
15
|
-
protected
|
|
14
|
+
protected _loggerName: string;
|
|
16
15
|
protected isCacheable: boolean;
|
|
17
|
-
protected
|
|
18
|
-
constructor(entityName: string, repository: RepositoryT, cacheManager: HybridCache, utilsService: UtilsService,
|
|
16
|
+
protected moduleName?: string;
|
|
17
|
+
constructor(entityName: string, repository: RepositoryT, cacheManager: HybridCache, utilsService: UtilsService, _loggerName: string, isCacheable?: boolean, moduleName?: string);
|
|
19
18
|
insert(dto: CreateDtoT, user: ILoggedUserInfo | null): Promise<InterfaceT>;
|
|
20
19
|
insertMany(dtos: Array<CreateDtoT>, user: ILoggedUserInfo | null): Promise<InterfaceT[]>;
|
|
21
20
|
update(dto: UpdateDtoT, user: ILoggedUserInfo | null): Promise<InterfaceT>;
|
|
@@ -41,15 +40,11 @@ export declare abstract class ApiService<CreateDtoT extends object, UpdateDtoT e
|
|
|
41
40
|
protected afterDeleteOperation(_entity: Array<{
|
|
42
41
|
id: string;
|
|
43
42
|
}>, _user: ILoggedUserInfo | null, _queryRunner: QueryRunner): Promise<void>;
|
|
44
|
-
protected getFilterQuery(query: SelectQueryBuilder<EntityT>, filter: {
|
|
45
|
-
[key: string]: any;
|
|
46
|
-
}, _user: ILoggedUserInfo | null): Promise<{
|
|
43
|
+
protected getFilterQuery(query: SelectQueryBuilder<EntityT>, filter: Record<string, unknown>, _user: ILoggedUserInfo | null): Promise<{
|
|
47
44
|
query: SelectQueryBuilder<EntityT>;
|
|
48
45
|
isRaw: boolean;
|
|
49
46
|
}>;
|
|
50
|
-
protected getSortQuery(query: SelectQueryBuilder<EntityT>, sort: {
|
|
51
|
-
[key: string]: 'ASC' | 'DESC';
|
|
52
|
-
}, _user: ILoggedUserInfo | null): Promise<{
|
|
47
|
+
protected getSortQuery(query: SelectQueryBuilder<EntityT>, sort: Record<string, 'ASC' | 'DESC'>, _user: ILoggedUserInfo | null): Promise<{
|
|
53
48
|
query: SelectQueryBuilder<EntityT>;
|
|
54
49
|
isRaw: boolean;
|
|
55
50
|
}>;
|
|
@@ -2,21 +2,22 @@ import { Logger } from '@nestjs/common';
|
|
|
2
2
|
import { ILogger } from '../interfaces/logger.interface';
|
|
3
3
|
export declare class WinstonLoggerAdapter implements ILogger {
|
|
4
4
|
private readonly context?;
|
|
5
|
-
|
|
5
|
+
private readonly logger;
|
|
6
|
+
constructor(context?: string, moduleName?: string);
|
|
6
7
|
private buildLogMeta;
|
|
7
|
-
log(message: string, context?: string, ...args:
|
|
8
|
-
error(message: string, trace?: string, context?: string, ...args:
|
|
9
|
-
warn(message: string, context?: string, ...args:
|
|
10
|
-
debug(message: string, context?: string, ...args:
|
|
11
|
-
verbose(message: string, context?: string, ...args:
|
|
8
|
+
log(message: string, context?: string, ...args: unknown[]): void;
|
|
9
|
+
error(message: string, trace?: string, context?: string, ...args: unknown[]): void;
|
|
10
|
+
warn(message: string, context?: string, ...args: unknown[]): void;
|
|
11
|
+
debug(message: string, context?: string, ...args: unknown[]): void;
|
|
12
|
+
verbose(message: string, context?: string, ...args: unknown[]): void;
|
|
12
13
|
}
|
|
13
14
|
export declare class NestLoggerAdapter implements ILogger {
|
|
14
15
|
private readonly logger;
|
|
15
16
|
constructor(logger: Logger);
|
|
16
17
|
private formatMessage;
|
|
17
|
-
log(message: string, context?: string, ...args:
|
|
18
|
-
error(message: string, trace?: string, context?: string, ...args:
|
|
19
|
-
warn(message: string, context?: string, ...args:
|
|
20
|
-
debug(message: string, context?: string, ...args:
|
|
21
|
-
verbose(message: string, context?: string, ...args:
|
|
18
|
+
log(message: string, context?: string, ...args: unknown[]): void;
|
|
19
|
+
error(message: string, trace?: string, context?: string, ...args: unknown[]): void;
|
|
20
|
+
warn(message: string, context?: string, ...args: unknown[]): void;
|
|
21
|
+
debug(message: string, context?: string, ...args: unknown[]): void;
|
|
22
|
+
verbose(message: string, context?: string, ...args: unknown[]): void;
|
|
22
23
|
}
|
package/constants/index.d.ts
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export declare const AUTH_MESSAGES: {
|
|
2
|
+
readonly TOKEN_REQUIRED: "auth.token.required";
|
|
3
|
+
readonly TOKEN_INVALID: "auth.token.invalid";
|
|
4
|
+
readonly TOKEN_EXPIRED: "auth.token.expired";
|
|
5
|
+
readonly COMPANY_NO_ACCESS: "auth.company.no.access";
|
|
6
|
+
};
|
|
7
|
+
export declare const ERROR_MESSAGES: {
|
|
8
|
+
readonly NOT_FOUND: "error.not.found";
|
|
9
|
+
readonly VALIDATION: "error.validation";
|
|
10
|
+
readonly UNAUTHORIZED: "error.unauthorized";
|
|
11
|
+
readonly FORBIDDEN: "error.forbidden";
|
|
12
|
+
readonly CONFLICT: "error.conflict";
|
|
13
|
+
readonly INTERNAL: "error.internal";
|
|
14
|
+
readonly SERVICE_UNAVAILABLE: "error.service.unavailable";
|
|
15
|
+
readonly UNKNOWN: "error.unknown";
|
|
16
|
+
readonly HTTP: "error.http";
|
|
17
|
+
readonly GENERIC: "error.generic";
|
|
18
|
+
readonly PERMISSION_SYSTEM_UNAVAILABLE: "error.permission.system.unavailable";
|
|
19
|
+
readonly INSUFFICIENT_PERMISSIONS: "error.insufficient.permissions";
|
|
20
|
+
readonly INSUFFICIENT_PERMISSIONS_OR: "error.insufficient.permissions.or";
|
|
21
|
+
readonly NO_PERMISSIONS_FOUND: "error.no.permissions.found";
|
|
22
|
+
};
|
|
23
|
+
export declare const SYSTEM_MESSAGES: {
|
|
24
|
+
readonly REPOSITORY_NOT_AVAILABLE: "system.repository.not.available";
|
|
25
|
+
readonly DATASOURCE_NOT_AVAILABLE: "system.datasource.not.available";
|
|
26
|
+
readonly DATABASE_CONFIG_NOT_AVAILABLE: "system.database.config.not.available";
|
|
27
|
+
readonly SERVICE_NOT_AVAILABLE: "system.service.not.available";
|
|
28
|
+
readonly CONFIG_REQUIRED: "system.config.required";
|
|
29
|
+
readonly INTERNAL_ERROR: "system.internal.error";
|
|
30
|
+
readonly NOT_FOUND: "system.not.found";
|
|
31
|
+
readonly DUPLICATE_REQUEST: "system.duplicate.request";
|
|
32
|
+
readonly INVALID_TENANT_ID: "system.invalid.tenant.id";
|
|
33
|
+
readonly TENANT_NOT_FOUND: "system.tenant.not.found";
|
|
34
|
+
readonly TENANT_HEADER_REQUIRED: "system.tenant.header.required";
|
|
35
|
+
readonly MISSING_PARAMETER: "system.missing.parameter";
|
|
36
|
+
readonly SDK_NOT_INSTALLED: "system.sdk.not.installed";
|
|
37
|
+
readonly PATH_TRAVERSAL_DETECTED: "system.path.traversal.detected";
|
|
38
|
+
readonly INVALID_FILE_KEY: "system.invalid.file.key";
|
|
39
|
+
};
|
|
40
|
+
export declare const MESSAGE_KEYS: {
|
|
41
|
+
readonly AUTH: {
|
|
42
|
+
readonly TOKEN_REQUIRED: "auth.token.required";
|
|
43
|
+
readonly TOKEN_INVALID: "auth.token.invalid";
|
|
44
|
+
readonly TOKEN_EXPIRED: "auth.token.expired";
|
|
45
|
+
readonly COMPANY_NO_ACCESS: "auth.company.no.access";
|
|
46
|
+
};
|
|
47
|
+
readonly ERROR: {
|
|
48
|
+
readonly NOT_FOUND: "error.not.found";
|
|
49
|
+
readonly VALIDATION: "error.validation";
|
|
50
|
+
readonly UNAUTHORIZED: "error.unauthorized";
|
|
51
|
+
readonly FORBIDDEN: "error.forbidden";
|
|
52
|
+
readonly CONFLICT: "error.conflict";
|
|
53
|
+
readonly INTERNAL: "error.internal";
|
|
54
|
+
readonly SERVICE_UNAVAILABLE: "error.service.unavailable";
|
|
55
|
+
readonly UNKNOWN: "error.unknown";
|
|
56
|
+
readonly HTTP: "error.http";
|
|
57
|
+
readonly GENERIC: "error.generic";
|
|
58
|
+
readonly PERMISSION_SYSTEM_UNAVAILABLE: "error.permission.system.unavailable";
|
|
59
|
+
readonly INSUFFICIENT_PERMISSIONS: "error.insufficient.permissions";
|
|
60
|
+
readonly INSUFFICIENT_PERMISSIONS_OR: "error.insufficient.permissions.or";
|
|
61
|
+
readonly NO_PERMISSIONS_FOUND: "error.no.permissions.found";
|
|
62
|
+
};
|
|
63
|
+
readonly SYSTEM: {
|
|
64
|
+
readonly REPOSITORY_NOT_AVAILABLE: "system.repository.not.available";
|
|
65
|
+
readonly DATASOURCE_NOT_AVAILABLE: "system.datasource.not.available";
|
|
66
|
+
readonly DATABASE_CONFIG_NOT_AVAILABLE: "system.database.config.not.available";
|
|
67
|
+
readonly SERVICE_NOT_AVAILABLE: "system.service.not.available";
|
|
68
|
+
readonly CONFIG_REQUIRED: "system.config.required";
|
|
69
|
+
readonly INTERNAL_ERROR: "system.internal.error";
|
|
70
|
+
readonly NOT_FOUND: "system.not.found";
|
|
71
|
+
readonly DUPLICATE_REQUEST: "system.duplicate.request";
|
|
72
|
+
readonly INVALID_TENANT_ID: "system.invalid.tenant.id";
|
|
73
|
+
readonly TENANT_NOT_FOUND: "system.tenant.not.found";
|
|
74
|
+
readonly TENANT_HEADER_REQUIRED: "system.tenant.header.required";
|
|
75
|
+
readonly MISSING_PARAMETER: "system.missing.parameter";
|
|
76
|
+
readonly SDK_NOT_INSTALLED: "system.sdk.not.installed";
|
|
77
|
+
readonly PATH_TRAVERSAL_DETECTED: "system.path.traversal.detected";
|
|
78
|
+
readonly INVALID_FILE_KEY: "system.invalid.file.key";
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
export type MessageKey = (typeof MESSAGE_KEYS)[keyof typeof MESSAGE_KEYS][keyof (typeof MESSAGE_KEYS)[keyof typeof MESSAGE_KEYS]];
|