@flusys/nestjs-shared 1.1.0 → 1.2.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +139 -1103
  2. package/cjs/classes/api-controller.class.js +320 -29
  3. package/cjs/classes/api-service.class.js +219 -64
  4. package/cjs/classes/handle-custome-db.js +37 -0
  5. package/cjs/classes/index.js +1 -2
  6. package/cjs/classes/winston.logger.class.js +103 -70
  7. package/cjs/constants/index.js +1 -4
  8. package/cjs/constants/message-keys.js +68 -0
  9. package/cjs/constants/permissions.js +114 -11
  10. package/cjs/decorators/api-response.decorator.js +1 -2
  11. package/cjs/decorators/index.js +1 -1
  12. package/cjs/decorators/log-action.decorator.js +149 -0
  13. package/cjs/decorators/require-permission.decorator.js +5 -5
  14. package/cjs/dtos/get-by-ids.dto.js +65 -0
  15. package/cjs/dtos/identity-response.dto.js +4 -8
  16. package/cjs/dtos/index.js +1 -0
  17. package/cjs/dtos/response-payload.dto.js +72 -0
  18. package/cjs/entities/identity.js +8 -2
  19. package/cjs/entities/index.js +1 -0
  20. package/cjs/entities/raw-type.js +4 -0
  21. package/cjs/entities/user-root.js +2 -1
  22. package/cjs/enums/index.js +20 -0
  23. package/cjs/enums/notification-type.enum.js +17 -0
  24. package/cjs/enums/participant-status.enum.js +17 -0
  25. package/cjs/enums/recurrence-type.enum.js +18 -0
  26. package/cjs/exceptions/base-app.exception.js +151 -0
  27. package/cjs/exceptions/index.js +1 -0
  28. package/cjs/exceptions/permission.exception.js +13 -9
  29. package/cjs/filters/global-exception.filter.js +279 -0
  30. package/cjs/filters/index.js +18 -0
  31. package/cjs/guards/jwt-auth.guard.js +4 -1
  32. package/cjs/guards/permission.guard.js +71 -77
  33. package/cjs/index.js +2 -0
  34. package/cjs/interceptors/idempotency.interceptor.js +1 -1
  35. package/cjs/interceptors/index.js +0 -1
  36. package/cjs/interfaces/event-manager-adapter.interface.js +11 -0
  37. package/cjs/interfaces/index.js +2 -1
  38. package/cjs/interfaces/notification-adapter.interface.js +11 -0
  39. package/cjs/interfaces/permission.interface.js +1 -1
  40. package/cjs/middlewares/logger.middleware.js +83 -26
  41. package/cjs/modules/datasource/index.js +0 -1
  42. package/cjs/modules/datasource/multi-tenant-datasource.service.js +46 -85
  43. package/cjs/modules/utils/utils.service.js +4 -20
  44. package/cjs/utils/date-time.util.js +94 -0
  45. package/cjs/utils/index.js +1 -1
  46. package/cjs/utils/query-helpers.util.js +8 -1
  47. package/cjs/utils/request.util.js +25 -0
  48. package/classes/api-controller.class.d.ts +14 -6
  49. package/classes/api-service.class.d.ts +25 -22
  50. package/classes/handle-custome-db.d.ts +3 -0
  51. package/classes/index.d.ts +1 -2
  52. package/classes/winston.logger.class.d.ts +1 -0
  53. package/constants/index.d.ts +1 -1
  54. package/constants/message-keys.d.ts +45 -0
  55. package/constants/permissions.d.ts +127 -0
  56. package/decorators/api-response.decorator.d.ts +1 -1
  57. package/decorators/current-user.decorator.d.ts +1 -1
  58. package/decorators/index.d.ts +1 -1
  59. package/decorators/log-action.decorator.d.ts +8 -0
  60. package/decorators/require-permission.decorator.d.ts +2 -2
  61. package/dtos/get-by-ids.dto.d.ts +4 -0
  62. package/dtos/index.d.ts +1 -0
  63. package/dtos/response-payload.dto.d.ts +8 -0
  64. package/entities/index.d.ts +1 -0
  65. package/entities/raw-type.d.ts +1 -0
  66. package/enums/index.d.ts +3 -0
  67. package/enums/notification-type.enum.d.ts +6 -0
  68. package/enums/participant-status.enum.d.ts +6 -0
  69. package/enums/recurrence-type.enum.d.ts +7 -0
  70. package/exceptions/base-app.exception.d.ts +41 -0
  71. package/exceptions/index.d.ts +1 -0
  72. package/exceptions/permission.exception.d.ts +2 -2
  73. package/fesm/classes/api-controller.class.js +321 -30
  74. package/fesm/classes/api-service.class.js +222 -67
  75. package/fesm/classes/handle-custome-db.js +19 -0
  76. package/fesm/classes/index.js +1 -2
  77. package/fesm/classes/winston.logger.class.js +100 -68
  78. package/fesm/constants/index.js +2 -1
  79. package/fesm/constants/message-keys.js +49 -0
  80. package/fesm/constants/permissions.js +86 -14
  81. package/fesm/decorators/api-response.decorator.js +2 -20
  82. package/fesm/decorators/index.js +1 -1
  83. package/fesm/decorators/log-action.decorator.js +139 -0
  84. package/fesm/decorators/require-permission.decorator.js +17 -65
  85. package/fesm/dtos/get-by-ids.dto.js +55 -0
  86. package/fesm/dtos/identity-response.dto.js +4 -8
  87. package/fesm/dtos/index.js +1 -0
  88. package/fesm/dtos/response-payload.dto.js +72 -0
  89. package/fesm/entities/identity.js +8 -2
  90. package/fesm/entities/index.js +1 -0
  91. package/fesm/entities/raw-type.js +1 -0
  92. package/fesm/entities/user-root.js +2 -1
  93. package/fesm/enums/index.js +3 -0
  94. package/fesm/enums/notification-type.enum.js +7 -0
  95. package/fesm/enums/participant-status.enum.js +7 -0
  96. package/fesm/enums/recurrence-type.enum.js +8 -0
  97. package/fesm/exceptions/base-app.exception.js +115 -0
  98. package/fesm/exceptions/index.js +1 -0
  99. package/fesm/exceptions/permission.exception.js +16 -18
  100. package/fesm/filters/global-exception.filter.js +269 -0
  101. package/fesm/filters/index.js +1 -0
  102. package/fesm/guards/jwt-auth.guard.js +5 -2
  103. package/fesm/guards/permission.guard.js +73 -79
  104. package/fesm/index.js +2 -0
  105. package/fesm/interceptors/idempotency.interceptor.js +2 -2
  106. package/fesm/interceptors/index.js +0 -1
  107. package/fesm/interfaces/datasource.interface.js +1 -20
  108. package/fesm/interfaces/event-manager-adapter.interface.js +1 -0
  109. package/fesm/interfaces/index.js +2 -1
  110. package/fesm/interfaces/notification-adapter.interface.js +1 -0
  111. package/fesm/interfaces/permission.interface.js +1 -3
  112. package/fesm/middlewares/logger.middleware.js +83 -26
  113. package/fesm/modules/datasource/index.js +0 -1
  114. package/fesm/modules/datasource/multi-tenant-datasource.service.js +47 -86
  115. package/fesm/modules/utils/utils.service.js +5 -21
  116. package/fesm/utils/date-time.util.js +70 -0
  117. package/fesm/utils/index.js +1 -1
  118. package/fesm/utils/query-helpers.util.js +8 -1
  119. package/fesm/utils/request.util.js +22 -0
  120. package/filters/global-exception.filter.d.ts +9 -0
  121. package/filters/index.d.ts +1 -0
  122. package/guards/permission.guard.d.ts +3 -7
  123. package/index.d.ts +2 -0
  124. package/interceptors/idempotency.interceptor.d.ts +1 -1
  125. package/interceptors/index.d.ts +0 -1
  126. package/interfaces/api.interface.d.ts +2 -0
  127. package/interfaces/event-manager-adapter.interface.d.ts +43 -0
  128. package/interfaces/index.d.ts +2 -1
  129. package/interfaces/notification-adapter.interface.d.ts +21 -0
  130. package/interfaces/permission.interface.d.ts +13 -8
  131. package/modules/datasource/index.d.ts +0 -1
  132. package/modules/datasource/multi-tenant-datasource.service.d.ts +5 -15
  133. package/modules/utils/utils.service.d.ts +0 -1
  134. package/package.json +21 -14
  135. package/utils/date-time.util.d.ts +8 -0
  136. package/utils/index.d.ts +1 -1
  137. package/utils/request.util.d.ts +2 -0
  138. package/cjs/classes/request-scoped-api.service.js +0 -61
  139. package/cjs/classes/winston-logger-adapter.class.js +0 -104
  140. package/cjs/decorators/sanitize-html.decorator.js +0 -36
  141. package/cjs/interceptors/query-performance.interceptor.js +0 -66
  142. package/cjs/interfaces/logger.interface.js +0 -7
  143. package/cjs/modules/datasource/datasource.module.js +0 -117
  144. package/cjs/utils/error-handler.util.js +0 -90
  145. package/classes/request-scoped-api.service.d.ts +0 -14
  146. package/classes/winston-logger-adapter.class.d.ts +0 -22
  147. package/decorators/sanitize-html.decorator.d.ts +0 -2
  148. package/fesm/classes/request-scoped-api.service.js +0 -51
  149. package/fesm/classes/winston-logger-adapter.class.js +0 -107
  150. package/fesm/decorators/sanitize-html.decorator.js +0 -45
  151. package/fesm/interceptors/query-performance.interceptor.js +0 -56
  152. package/fesm/interfaces/logger.interface.js +0 -4
  153. package/fesm/modules/datasource/datasource.module.js +0 -107
  154. package/fesm/utils/error-handler.util.js +0 -82
  155. package/interceptors/query-performance.interceptor.d.ts +0 -8
  156. package/interfaces/logger.interface.d.ts +0 -7
  157. package/modules/datasource/datasource.module.d.ts +0 -19
  158. package/utils/error-handler.util.d.ts +0 -19
@@ -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 IS_DEBUG = _config.envConfig.getLogConfig().level === 'debug';
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 sanitizeHeaders(headers) {
77
- const sanitized = {};
78
- for (const [key, value] of Object.entries(headers)){
79
- sanitized[key] = EXCLUDED_HEADERS.includes(key.toLowerCase()) ? '[REDACTED]' : value;
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
- return sanitized;
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 truncateBody(body) {
107
+ function summarizeResponseBody(body) {
84
108
  if (!body) return undefined;
85
- const str = typeof body === 'string' ? body : JSON.stringify(body);
86
- return str.length > MAX_BODY_LOG_SIZE ? str.substring(0, MAX_BODY_LOG_SIZE) + '...' : body;
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: getClientIp(req),
202
+ ip,
148
203
  userAgent: req.headers['user-agent'],
149
- contentType: req.headers['content-type'],
204
+ contentType,
150
205
  contentLength: req.headers['content-length']
151
206
  };
152
207
  if (IS_DEBUG) {
153
- logData.headers = sanitizeHeaders(req.headers);
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('Incoming request', logData);
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: res.getHeader('content-type'),
174
- contentLength: res.getHeader('content-length')
230
+ contentType,
231
+ contentLength
175
232
  };
176
233
  if (statusCode >= 400 || IS_DEBUG) {
177
- logData.responseBody = truncateBody(body);
234
+ logData.responseBody = summarizeResponseBody(body);
178
235
  }
179
- this.logger.log(level, `Response [${statusCode}]`, logData);
236
+ this.logger.log(level, message, logData);
180
237
  if (duration > 3000 && statusCode < 400) {
181
- this.logger.warn('Slow request detected', {
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,
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", {
3
3
  value: true
4
4
  });
5
- _export_star(require("./datasource.module"), exports);
6
5
  _export_star(require("./multi-tenant-datasource.service"), exports);
7
6
  function _export_star(from, to) {
8
7
  Object.keys(from).forEach(function(k) {
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "MultiTenantDataSourceService", {
10
10
  });
11
11
  const _nestjscore = require("@flusys/nestjs-core");
12
12
  const _common = require("@nestjs/common");
13
+ const _constants = require("../../constants");
13
14
  const _core = require("@nestjs/core");
14
15
  const _express = require("express");
15
16
  const _typeorm = require("typeorm");
@@ -42,21 +43,21 @@ function _ts_param(paramIndex, decorator) {
42
43
  };
43
44
  }
44
45
  let MultiTenantDataSourceService = class MultiTenantDataSourceService {
46
+ get self() {
47
+ return this.constructor;
48
+ }
45
49
  initializeFromOptions() {
46
50
  if (!this.options) return;
47
- if (!MultiTenantDataSourceService.initialized) {
51
+ if (!this.self.initialized) {
48
52
  this.registerTenants();
49
- MultiTenantDataSourceService.initialized = true;
53
+ this.self.initialized = true;
50
54
  }
51
55
  }
52
56
  registerTenants() {
53
57
  this.options?.tenants?.forEach((tenant)=>{
54
- MultiTenantDataSourceService.tenantsRegistry.set(tenant.id, tenant);
58
+ this.self.tenantsRegistry.set(tenant.id, tenant);
55
59
  });
56
60
  }
57
- setTenantHeader(header) {
58
- this.tenantHeader = header;
59
- }
60
61
  getDatabaseMode() {
61
62
  return this.options?.bootstrapAppConfig?.databaseMode ?? 'single';
62
63
  }
@@ -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('Invalid tenant ID format');
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
  }
@@ -77,124 +81,78 @@ let MultiTenantDataSourceService = class MultiTenantDataSourceService {
77
81
  return tenantId ? this.getTenant(tenantId) : null;
78
82
  }
79
83
  getTenant(tenantId) {
80
- return MultiTenantDataSourceService.tenantsRegistry.get(tenantId) ?? null;
81
- }
82
- getAllTenants() {
83
- return Array.from(MultiTenantDataSourceService.tenantsRegistry.values());
84
- }
85
- getActiveTenants() {
86
- return this.getAllTenants();
84
+ return this.self.tenantsRegistry.get(tenantId) ?? null;
87
85
  }
88
86
  async getDataSource() {
89
87
  return this.isMultiTenant() ? this.getTenantDataSource() : this.getSingleDataSource();
90
88
  }
91
- async getDataSourceForTenant(tenantId) {
92
- const tenant = this.getTenant(tenantId);
93
- if (!tenant) throw new Error(`Tenant '${tenantId}' not found`);
94
- return this.getOrCreateTenantConnection(tenant);
95
- }
96
- setDataSource(dataSource) {
97
- MultiTenantDataSourceService.singleDataSource = dataSource;
98
- }
99
89
  async getRepository(entity) {
100
90
  const dataSource = await this.getDataSource();
101
91
  return dataSource.getRepository(entity);
102
92
  }
103
- async withTenant(tenantId, callback) {
104
- const dataSource = await this.getDataSourceForTenant(tenantId);
105
- return callback(dataSource);
106
- }
107
- async forAllTenants(callback) {
108
- const results = new Map();
109
- for (const tenant of this.getActiveTenants()){
110
- try {
111
- const dataSource = await this.getOrCreateTenantConnection(tenant);
112
- results.set(tenant.id, await callback(tenant, dataSource));
113
- } catch (error) {
114
- this.logger.error(`Error processing tenant ${tenant.id}: ${error}`);
115
- }
116
- }
117
- return results;
118
- }
119
- registerTenant(tenant) {
120
- MultiTenantDataSourceService.tenantsRegistry.set(tenant.id, tenant);
121
- }
122
- async removeTenant(tenantId) {
123
- await this.closeTenantConnection(tenantId);
124
- MultiTenantDataSourceService.tenantsRegistry.delete(tenantId);
125
- }
126
93
  async closeTenantConnection(tenantId) {
127
- const connection = MultiTenantDataSourceService.tenantConnections.get(tenantId);
94
+ const connection = this.self.tenantConnections.get(tenantId);
128
95
  if (connection?.isInitialized) {
129
96
  await connection.destroy();
130
- MultiTenantDataSourceService.tenantConnections.delete(tenantId);
131
- this.logger.log(`Closed connection for tenant: ${tenantId}`);
97
+ this.self.tenantConnections.delete(tenantId);
132
98
  }
133
99
  }
134
100
  async onModuleDestroy() {
135
- for (const [tenantId] of MultiTenantDataSourceService.tenantConnections){
101
+ for (const [tenantId] of this.self.tenantConnections){
136
102
  await this.closeTenantConnection(tenantId);
137
103
  }
138
104
  }
139
- static reset() {
140
- MultiTenantDataSourceService.initialized = false;
141
- MultiTenantDataSourceService.singleDataSource = null;
142
- MultiTenantDataSourceService.singleConnectionLock = null;
143
- MultiTenantDataSourceService.tenantConnections.clear();
144
- MultiTenantDataSourceService.tenantsRegistry.clear();
145
- MultiTenantDataSourceService.connectionLocks.clear();
146
- }
147
- // Protected Methods (for subclasses)
148
- /**
149
- * Get single database DataSource with connection locking to prevent race conditions
150
- */ async getSingleDataSource() {
151
- // Return existing initialized connection
152
- if (MultiTenantDataSourceService.singleDataSource?.isInitialized) {
153
- return MultiTenantDataSourceService.singleDataSource;
105
+ async getSingleDataSource() {
106
+ if (this.self.singleDataSource?.isInitialized) {
107
+ return this.self.singleDataSource;
154
108
  }
155
- // If another request is creating the connection, wait for it
156
- if (MultiTenantDataSourceService.singleConnectionLock) {
157
- return MultiTenantDataSourceService.singleConnectionLock;
109
+ if (this.self.singleConnectionLock) {
110
+ return this.self.singleConnectionLock;
158
111
  }
159
112
  const config = this.getDefaultDatabaseConfig();
160
113
  if (!config) {
161
- throw new Error('No database config available. Provide defaultDatabaseConfig or tenantDefaultDatabaseConfig.');
114
+ throw new _common.InternalServerErrorException({
115
+ message: 'No database config available. Provide defaultDatabaseConfig or tenantDefaultDatabaseConfig.',
116
+ messageKey: _constants.SYSTEM_MESSAGES.DATABASE_CONFIG_NOT_AVAILABLE
117
+ });
162
118
  }
163
- // Create connection with lock to prevent race conditions
164
119
  const connectionPromise = this.createDataSourceFromConfig(config);
165
- MultiTenantDataSourceService.singleConnectionLock = connectionPromise;
120
+ this.self.singleConnectionLock = connectionPromise;
166
121
  try {
167
122
  const dataSource = await connectionPromise;
168
- MultiTenantDataSourceService.singleDataSource = dataSource;
123
+ this.self.singleDataSource = dataSource;
169
124
  return dataSource;
170
125
  } finally{
171
- MultiTenantDataSourceService.singleConnectionLock = null;
126
+ this.self.singleConnectionLock = null;
172
127
  }
173
128
  }
174
- /**
175
- * Get tenant-specific DataSource
176
- */ async getTenantDataSource() {
129
+ async getTenantDataSource() {
177
130
  const tenant = this.getCurrentTenant();
178
131
  if (!tenant) {
179
- throw new Error(`Tenant not found. Ensure '${this.tenantHeader}' header is set.`);
132
+ throw new _common.BadRequestException({
133
+ message: `Tenant not found. Ensure '${this.tenantHeader}' header is set.`,
134
+ messageKey: _constants.SYSTEM_MESSAGES.TENANT_HEADER_REQUIRED,
135
+ messageVariables: {
136
+ header: this.tenantHeader
137
+ }
138
+ });
180
139
  }
181
140
  return this.getOrCreateTenantConnection(tenant);
182
141
  }
183
142
  async getOrCreateTenantConnection(tenant) {
184
- const existing = MultiTenantDataSourceService.tenantConnections.get(tenant.id);
143
+ const existing = this.self.tenantConnections.get(tenant.id);
185
144
  if (existing?.isInitialized) return existing;
186
- const pendingConnection = MultiTenantDataSourceService.connectionLocks.get(tenant.id);
145
+ const pendingConnection = this.self.connectionLocks.get(tenant.id);
187
146
  if (pendingConnection) return pendingConnection;
188
147
  const config = this.buildTenantDatabaseConfig(tenant);
189
148
  const connectionPromise = this.createDataSourceFromConfig(config);
190
- MultiTenantDataSourceService.connectionLocks.set(tenant.id, connectionPromise);
149
+ this.self.connectionLocks.set(tenant.id, connectionPromise);
191
150
  try {
192
151
  const dataSource = await connectionPromise;
193
- MultiTenantDataSourceService.tenantConnections.set(tenant.id, dataSource);
194
- this.logger.log(`Created connection for tenant: ${tenant.id}`);
152
+ this.self.tenantConnections.set(tenant.id, dataSource);
195
153
  return dataSource;
196
154
  } finally{
197
- MultiTenantDataSourceService.connectionLocks.delete(tenant.id);
155
+ this.self.connectionLocks.delete(tenant.id);
198
156
  }
199
157
  }
200
158
  getDefaultDatabaseConfig() {
@@ -202,7 +160,12 @@ let MultiTenantDataSourceService = class MultiTenantDataSourceService {
202
160
  }
203
161
  buildTenantDatabaseConfig(tenant) {
204
162
  const defaultConfig = this.getDefaultDatabaseConfig();
205
- if (!defaultConfig) throw new Error('No default database config for multi-tenant mode.');
163
+ if (!defaultConfig) {
164
+ throw new _common.InternalServerErrorException({
165
+ message: 'No default database config for multi-tenant mode.',
166
+ messageKey: _constants.SYSTEM_MESSAGES.DATABASE_CONFIG_NOT_AVAILABLE
167
+ });
168
+ }
206
169
  return {
207
170
  type: defaultConfig.type,
208
171
  host: tenant.host ?? defaultConfig.host,
@@ -230,11 +193,9 @@ let MultiTenantDataSourceService = class MultiTenantDataSourceService {
230
193
  constructor(options, request){
231
194
  _define_property(this, "options", void 0);
232
195
  _define_property(this, "request", void 0);
233
- _define_property(this, "logger", void 0);
234
196
  _define_property(this, "tenantHeader", void 0);
235
197
  this.options = options;
236
198
  this.request = request;
237
- this.logger = new _common.Logger(this.constructor.name);
238
199
  this.tenantHeader = _nestjscore.DEFAULT_TENANT_HEADER;
239
200
  this.initializeFromOptions();
240
201
  }
@@ -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 (error) {
51
- this.logger.error(`Cache tracking failed for ${entityName}`, error instanceof Error ? error.stack : String(error));
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 (error) {
63
- this.logger.error(`Cache invalidation failed for ${entityName}`, error instanceof Error ? error.stack : String(error));
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)()
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ function _export(target, all) {
6
+ for(var name in all)Object.defineProperty(target, name, {
7
+ enumerable: true,
8
+ get: Object.getOwnPropertyDescriptor(all, name).get
9
+ });
10
+ }
11
+ _export(exports, {
12
+ get formatInTimezone () {
13
+ return formatInTimezone;
14
+ },
15
+ get getTodayInTimezone () {
16
+ return getTodayInTimezone;
17
+ },
18
+ get localToUtc () {
19
+ return localToUtc;
20
+ },
21
+ get utcToLocal () {
22
+ return utcToLocal;
23
+ }
24
+ });
25
+ function getOffsetMinutes(utcDate, timezone) {
26
+ const extract = (tz)=>{
27
+ const parts = new Intl.DateTimeFormat('en-US', {
28
+ timeZone: tz,
29
+ year: 'numeric',
30
+ month: '2-digit',
31
+ day: '2-digit',
32
+ hour: '2-digit',
33
+ minute: '2-digit',
34
+ second: '2-digit',
35
+ hour12: false
36
+ }).formatToParts(utcDate);
37
+ const p = Object.fromEntries(parts.filter((x)=>x.type !== 'literal').map((x)=>[
38
+ x.type,
39
+ parseInt(x.value)
40
+ ]));
41
+ return Date.UTC(p['year'], p['month'] - 1, p['day'], p['hour'], p['minute'], p['second']);
42
+ };
43
+ return (extract(timezone) - extract('UTC')) / 60_000;
44
+ }
45
+ function localToUtc(dateStr, timeStr, timezone) {
46
+ const [year, month, day] = dateStr.split('-').map(Number);
47
+ const [hours, minutes] = timeStr.split(':').map(Number);
48
+ const approx = new Date(Date.UTC(year, month - 1, day, hours, minutes, 0));
49
+ const offset = getOffsetMinutes(approx, timezone);
50
+ const utcDate = new Date(approx.getTime() - offset * 60_000);
51
+ // DST boundary guard: re-check offset after adjustment
52
+ const finalOffset = getOffsetMinutes(utcDate, timezone);
53
+ return finalOffset !== offset ? new Date(approx.getTime() - finalOffset * 60_000) : utcDate;
54
+ }
55
+ function utcToLocal(utcDate, timezone) {
56
+ const d = typeof utcDate === 'string' ? new Date(utcDate) : utcDate;
57
+ const parts = new Intl.DateTimeFormat('en-CA', {
58
+ timeZone: timezone,
59
+ year: 'numeric',
60
+ month: '2-digit',
61
+ day: '2-digit',
62
+ hour: '2-digit',
63
+ minute: '2-digit',
64
+ hour12: false
65
+ }).formatToParts(d);
66
+ const p = Object.fromEntries(parts.filter((x)=>x.type !== 'literal').map((x)=>[
67
+ x.type,
68
+ x.value
69
+ ]));
70
+ // en-CA formats midnight hour as '24' — normalize to '00'
71
+ const hour = p['hour'] === '24' ? '00' : p['hour'];
72
+ return {
73
+ date: `${p['year']}-${p['month']}-${p['day']}`,
74
+ time: `${hour}:${p['minute']}`,
75
+ display: new Intl.DateTimeFormat('en-US', {
76
+ timeZone: timezone,
77
+ dateStyle: 'medium',
78
+ timeStyle: 'short'
79
+ }).format(d)
80
+ };
81
+ }
82
+ function formatInTimezone(utcDate, timezone, options = {
83
+ dateStyle: 'medium',
84
+ timeStyle: 'short'
85
+ }) {
86
+ const d = typeof utcDate === 'string' ? new Date(utcDate) : utcDate;
87
+ return new Intl.DateTimeFormat('en-US', {
88
+ ...options,
89
+ timeZone: timezone
90
+ }).format(d);
91
+ }
92
+ function getTodayInTimezone(timezone) {
93
+ return utcToLocal(new Date(), timezone).date;
94
+ }
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", {
3
3
  value: true
4
4
  });
5
- _export_star(require("./error-handler.util"), exports);
5
+ _export_star(require("./date-time.util"), exports);
6
6
  _export_star(require("./html-sanitizer.util"), exports);
7
7
  _export_star(require("./query-helpers.util"), exports);
8
8
  _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(`${entityName} belongs to another company`);
51
+ throw new _common.BadRequestException({
52
+ message: `${entityName} belongs to another company`,
53
+ messageKey: _constants.AUTH_MESSAGES.ENTITY_BELOG_ANOTHER_COMPANY,
54
+ messageVariables: {
55
+ entity: entityName
56
+ }
57
+ });
51
58
  }
52
59
  }
53
60
  }
@@ -12,6 +12,12 @@ _export(exports, {
12
12
  get buildCookieOptions () {
13
13
  return buildCookieOptions;
14
14
  },
15
+ get buildFrontendUrl () {
16
+ return buildFrontendUrl;
17
+ },
18
+ get extractFrontendUrl () {
19
+ return extractFrontendUrl;
20
+ },
15
21
  get isBrowserRequest () {
16
22
  return isBrowserRequest;
17
23
  },
@@ -69,3 +75,22 @@ function parseDurationToMs(duration, defaultMs = TIME_UNIT_MS.w) {
69
75
  const unit = match[2];
70
76
  return value * (TIME_UNIT_MS[unit] ?? TIME_UNIT_MS.d);
71
77
  }
78
+ function extractFrontendUrl(req) {
79
+ const allowedOrigins = _config.envConfig.getOrigins();
80
+ const origin = (req.get('origin') ?? req.get('referer') ?? '').trim();
81
+ if (origin && allowedOrigins.length) {
82
+ const matched = allowedOrigins.find((u)=>origin.startsWith(u));
83
+ if (matched) {
84
+ return matched;
85
+ }
86
+ }
87
+ const host = req.get('host');
88
+ if (!host) {
89
+ throw new Error('Unable to determine frontend URL from request');
90
+ }
91
+ return `${req.protocol}://${host}`;
92
+ }
93
+ function buildFrontendUrl(req, path) {
94
+ const baseUrl = extractFrontendUrl(req);
95
+ return `${baseUrl}${path}`;
96
+ }
@@ -1,30 +1,38 @@
1
- import { BulkResponseDto, DeleteDto, FilterAndPaginationDto, GetByIdBodyDto, ListResponseDto, MessageResponseDto, SingleResponseDto } from '../dtos';
2
- import { Identity } from '../entities';
3
- import { ILoggedUserInfo, IService, PermissionCondition, PermissionOperator } from '../interfaces';
4
1
  import { Type } from '@nestjs/common';
5
- export type ApiEndpoint = 'insert' | 'insertMany' | 'getById' | 'getAll' | 'update' | 'updateMany' | 'delete';
2
+ import { BulkResponseDto, DeleteDto, FilterAndPaginationDto, GetByIdBodyDto, GetByIdsDto, ListResponseDto, MessageResponseDto, SingleResponseDto } from '../dtos';
3
+ import { Identity } from '../entities';
4
+ import { ILoggedUserInfo, IPermissionLogic, IService } from '../interfaces';
5
+ export type ApiEndpoint = 'insert' | 'insertMany' | 'getById' | 'getByIds' | 'getAll' | 'bulkUpsert' | 'getByFilter' | 'update' | 'updateMany' | 'delete';
6
6
  export type SecurityLevel = 'public' | 'jwt' | 'permission';
7
7
  export interface EndpointSecurity {
8
8
  level: SecurityLevel;
9
9
  permissions?: string[];
10
- operator?: PermissionOperator;
11
- condition?: PermissionCondition;
10
+ operator?: 'AND' | 'OR';
11
+ logic?: IPermissionLogic;
12
12
  }
13
13
  export type ApiSecurityConfig = {
14
14
  [K in ApiEndpoint]?: EndpointSecurity | SecurityLevel;
15
15
  };
16
16
  export interface ApiControllerOptions {
17
17
  security?: ApiSecurityConfig | EndpointSecurity | SecurityLevel;
18
+ entityName?: string;
19
+ enabledEndpoints?: ApiEndpoint[] | 'all';
20
+ excludeExtraneousValues?: boolean;
18
21
  }
19
22
  export declare function createApiController<CreateDtoT extends object, UpdateDtoT extends {
20
23
  id: string;
21
24
  }, ResponseDtoT extends object, InterfaceT extends Identity, ServiceT extends IService<CreateDtoT, UpdateDtoT, InterfaceT>>(createDtoClass: Type<CreateDtoT>, updateDtoClass: Type<UpdateDtoT>, responseDtoClass: Type<ResponseDtoT>, options?: ApiControllerOptions): abstract new (service: ServiceT) => {
25
+ readonly enabledEndpoints: ApiEndpoint[] | "all";
22
26
  service: ServiceT;
27
+ isEnabled(endpoint: ApiEndpoint): boolean;
23
28
  insert(addDto: CreateDtoT, user: ILoggedUserInfo | null): Promise<SingleResponseDto<ResponseDtoT>>;
24
29
  insertMany(addDto: CreateDtoT[], user: ILoggedUserInfo | null): Promise<BulkResponseDto<ResponseDtoT>>;
25
30
  getById(id: string, body: GetByIdBodyDto, user: ILoggedUserInfo | null): Promise<SingleResponseDto<ResponseDtoT>>;
31
+ getByIds(body: GetByIdsDto, user: ILoggedUserInfo | null): Promise<ListResponseDto<ResponseDtoT>>;
26
32
  update(updateDto: UpdateDtoT, user: ILoggedUserInfo | null): Promise<SingleResponseDto<ResponseDtoT>>;
27
33
  updateMany(updateDtos: UpdateDtoT[], user: ILoggedUserInfo | null): Promise<BulkResponseDto<ResponseDtoT>>;
34
+ bulkUpsert(dtos: (CreateDtoT | UpdateDtoT)[], user: ILoggedUserInfo | null): Promise<BulkResponseDto<ResponseDtoT>>;
35
+ getByFilter(filter: Record<string, any>, user: ILoggedUserInfo | null): Promise<SingleResponseDto<ResponseDtoT>>;
28
36
  getAll(filterAndPaginationDto: FilterAndPaginationDto, user: ILoggedUserInfo | null, search?: string): Promise<ListResponseDto<ResponseDtoT>>;
29
37
  delete(deleteDto: DeleteDto, user: ILoggedUserInfo | null): Promise<MessageResponseDto>;
30
38
  };