@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
@@ -24,18 +24,15 @@ import { v4 as uuidv4 } from 'uuid';
24
24
  import { instance as winstonLogger } from '../classes/winston.logger.class';
25
25
  import { REQUEST_ID_HEADER } from '../constants';
26
26
  // Configuration
27
- const IS_DEBUG = envConfig.getLogConfig().level === 'debug';
27
+ const logConfig = envConfig.getLogConfig();
28
+ const IS_DEBUG = logConfig.level === 'debug';
29
+ const DISABLE_HTTP_LOGGING = logConfig.disableHttpLogging;
28
30
  const TENANT_ID_HEADER = 'x-tenant-id';
29
31
  const EXCLUDED_PATHS = [
30
32
  '/health',
31
33
  '/metrics',
32
34
  '/favicon.ico'
33
35
  ];
34
- const EXCLUDED_HEADERS = [
35
- 'authorization',
36
- 'cookie',
37
- 'x-api-key'
38
- ];
39
36
  const MAX_BODY_LOG_SIZE = 1000;
40
37
  export const requestContext = new AsyncLocalStorage();
41
38
  // Context accessors
@@ -44,17 +41,71 @@ export const getTenantId = ()=>requestContext.getStore()?.tenantId;
44
41
  export const getUserId = ()=>requestContext.getStore()?.userId;
45
42
  export const getCompanyId = ()=>requestContext.getStore()?.companyId;
46
43
  // Helper Functions
47
- function sanitizeHeaders(headers) {
48
- const sanitized = {};
49
- for (const [key, value] of Object.entries(headers)){
50
- sanitized[key] = EXCLUDED_HEADERS.includes(key.toLowerCase()) ? '[REDACTED]' : value;
44
+ function summarizeRequestBody(body) {
45
+ if (!body) return undefined;
46
+ const parsed = typeof body === 'string' ? tryParseJson(body) : body;
47
+ if (!parsed || typeof parsed !== 'object') {
48
+ const str = String(body);
49
+ return str.length > MAX_BODY_LOG_SIZE ? str.substring(0, MAX_BODY_LOG_SIZE) + '...' : str;
50
+ }
51
+ // For API request bodies, provide a concise summary
52
+ const summary = {};
53
+ // Pagination info
54
+ if (parsed.pagination) {
55
+ summary.page = parsed.pagination.currentPage;
56
+ summary.pageSize = parsed.pagination.pageSize;
57
+ }
58
+ // Filter keys only (not values - may contain sensitive data)
59
+ if (parsed.filter && typeof parsed.filter === 'object') {
60
+ const filterKeys = Object.keys(parsed.filter);
61
+ if (filterKeys.length > 0) summary.filterKeys = filterKeys;
62
+ }
63
+ // Select fields count
64
+ if (Array.isArray(parsed.select) && parsed.select.length > 0) {
65
+ summary.selectCount = parsed.select.length;
66
+ }
67
+ // Sort keys
68
+ if (parsed.sort && typeof parsed.sort === 'object') {
69
+ const sortKeys = Object.keys(parsed.sort);
70
+ if (sortKeys.length > 0) summary.sortKeys = sortKeys;
51
71
  }
52
- return sanitized;
72
+ // ID for single item operations
73
+ if (parsed.id) summary.id = parsed.id;
74
+ // IDs for bulk operations
75
+ if (Array.isArray(parsed.ids)) summary.idsCount = parsed.ids.length;
76
+ return Object.keys(summary).length > 0 ? JSON.stringify(summary) : '{}';
53
77
  }
54
- function truncateBody(body) {
78
+ function summarizeResponseBody(body) {
55
79
  if (!body) return undefined;
56
- const str = typeof body === 'string' ? body : JSON.stringify(body);
57
- return str.length > MAX_BODY_LOG_SIZE ? str.substring(0, MAX_BODY_LOG_SIZE) + '...' : body;
80
+ const parsed = typeof body === 'string' ? tryParseJson(body) : body;
81
+ if (!parsed || typeof parsed !== 'object') {
82
+ const str = String(body);
83
+ return str.length > MAX_BODY_LOG_SIZE ? str.substring(0, MAX_BODY_LOG_SIZE) + '...' : str;
84
+ }
85
+ const summary = {};
86
+ if ('success' in parsed) summary.success = parsed.success;
87
+ if ('message' in parsed) summary.message = parsed.message;
88
+ if ('code' in parsed) summary.code = parsed.code;
89
+ if (Array.isArray(parsed.data)) {
90
+ summary.dataCount = parsed.data.length;
91
+ } else if (parsed.data && typeof parsed.data === 'object') {
92
+ summary.hasData = true;
93
+ }
94
+ if (parsed.meta) {
95
+ summary.total = parsed.meta.total;
96
+ summary.page = parsed.meta.page;
97
+ }
98
+ if (parsed.errors) {
99
+ summary.errorsCount = Array.isArray(parsed.errors) ? parsed.errors.length : 1;
100
+ }
101
+ return JSON.stringify(summary);
102
+ }
103
+ function tryParseJson(str) {
104
+ try {
105
+ return JSON.parse(str);
106
+ } catch {
107
+ return null;
108
+ }
58
109
  }
59
110
  function getClientIp(req) {
60
111
  return req.headers['x-forwarded-for']?.split(',')[0] || req.socket?.remoteAddress || 'unknown';
@@ -71,7 +122,7 @@ export class LoggerMiddleware {
71
122
  startTime
72
123
  };
73
124
  requestContext.run(context, ()=>{
74
- const shouldSkipLogging = EXCLUDED_PATHS.some((path)=>req.originalUrl.startsWith(path));
125
+ const shouldSkipLogging = DISABLE_HTTP_LOGGING || EXCLUDED_PATHS.some((path)=>req.originalUrl.startsWith(path));
75
126
  if (!shouldSkipLogging) {
76
127
  this.logRequest(req, requestId, tenantId);
77
128
  this.setupResponseLogging(req, res, startTime, requestId, tenantId);
@@ -112,20 +163,22 @@ export class LoggerMiddleware {
112
163
  });
113
164
  }
114
165
  logRequest(req, requestId, tenantId) {
166
+ const ip = getClientIp(req);
167
+ const contentType = req.headers['content-type'] || 'none';
168
+ // Build descriptive message like: Started processing request "POST" "/api/users" from "192.168.1.1"
169
+ const message = `Started processing request "${req.method}" "${req.originalUrl}" from "${ip}"`;
115
170
  const logData = {
116
171
  ...this.buildBaseLogData(req, requestId, tenantId),
117
172
  query: Object.keys(req.query).length > 0 ? req.query : undefined,
118
- ip: getClientIp(req),
173
+ ip,
119
174
  userAgent: req.headers['user-agent'],
120
- contentType: req.headers['content-type'],
175
+ contentType,
121
176
  contentLength: req.headers['content-length']
122
177
  };
123
178
  if (IS_DEBUG) {
124
- logData.headers = sanitizeHeaders(req.headers);
125
- logData.body = truncateBody(req.body);
126
- logData.params = Object.keys(req.params).length > 0 ? req.params : undefined;
179
+ logData.body = summarizeRequestBody(req.body);
127
180
  }
128
- this.logger.info('Incoming request', logData);
181
+ this.logger.info(message, logData);
129
182
  }
130
183
  logResponse(req, res, startTime, body, requestId, tenantId) {
131
184
  const duration = Date.now() - startTime;
@@ -133,6 +186,10 @@ export class LoggerMiddleware {
133
186
  const level = statusCode >= 500 ? 'error' : statusCode >= 400 ? 'warn' : 'info';
134
187
  const userId = getUserId();
135
188
  const companyId = getCompanyId();
189
+ const contentType = res.getHeader('content-type') || 'none';
190
+ const contentLength = res.getHeader('content-length') || 'null';
191
+ // Build descriptive message like: Request finished "HTTP/1.1" "POST" "/api/users" - 200 "application/json" 534.29ms
192
+ const message = `Request finished "HTTP/1.1" "${req.method}" "${req.originalUrl}" - ${statusCode} ${contentLength} "${contentType}" ${duration.toFixed(2)}ms`;
136
193
  const logData = {
137
194
  ...this.buildBaseLogData(req, requestId, tenantId),
138
195
  userId,
@@ -141,15 +198,15 @@ export class LoggerMiddleware {
141
198
  statusMessage: res.statusMessage,
142
199
  duration: `${duration}ms`,
143
200
  durationMs: duration,
144
- contentType: res.getHeader('content-type'),
145
- contentLength: res.getHeader('content-length')
201
+ contentType,
202
+ contentLength
146
203
  };
147
204
  if (statusCode >= 400 || IS_DEBUG) {
148
- logData.responseBody = truncateBody(body);
205
+ logData.responseBody = summarizeResponseBody(body);
149
206
  }
150
- this.logger.log(level, `Response [${statusCode}]`, logData);
207
+ this.logger.log(level, message, logData);
151
208
  if (duration > 3000 && statusCode < 400) {
152
- this.logger.warn('Slow request detected', {
209
+ this.logger.warn(`Slow request detected: "${req.method}" "${req.originalUrl}" took ${duration}ms (threshold: 3000ms)`, {
153
210
  ...this.buildBaseLogData(req, requestId, tenantId),
154
211
  userId,
155
212
  companyId,
@@ -1,2 +1 @@
1
- export * from './datasource.module';
2
1
  export * from './multi-tenant-datasource.service';
@@ -26,27 +26,28 @@ function _ts_param(paramIndex, decorator) {
26
26
  };
27
27
  }
28
28
  import { DEFAULT_TENANT_HEADER, IDataSourceServiceOptions, MODULE_OPTIONS } from '@flusys/nestjs-core';
29
- import { BadRequestException, Inject, Injectable, Logger, Optional, Scope } from '@nestjs/common';
29
+ import { BadRequestException, Inject, Injectable, InternalServerErrorException, Optional, Scope } from '@nestjs/common';
30
+ import { SYSTEM_MESSAGES } from '../../constants';
30
31
  import { REQUEST } from '@nestjs/core';
31
32
  import { Request } from 'express';
32
33
  import { DataSource } from 'typeorm';
33
34
  import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
34
35
  export class MultiTenantDataSourceService {
36
+ get self() {
37
+ return this.constructor;
38
+ }
35
39
  initializeFromOptions() {
36
40
  if (!this.options) return;
37
- if (!MultiTenantDataSourceService.initialized) {
41
+ if (!this.self.initialized) {
38
42
  this.registerTenants();
39
- MultiTenantDataSourceService.initialized = true;
43
+ this.self.initialized = true;
40
44
  }
41
45
  }
42
46
  registerTenants() {
43
47
  this.options?.tenants?.forEach((tenant)=>{
44
- MultiTenantDataSourceService.tenantsRegistry.set(tenant.id, tenant);
48
+ this.self.tenantsRegistry.set(tenant.id, tenant);
45
49
  });
46
50
  }
47
- setTenantHeader(header) {
48
- this.tenantHeader = header;
49
- }
50
51
  getDatabaseMode() {
51
52
  return this.options?.bootstrapAppConfig?.databaseMode ?? 'single';
52
53
  }
@@ -58,7 +59,10 @@ export class MultiTenantDataSourceService {
58
59
  const tenantId = this.request.headers[this.tenantHeader];
59
60
  if (!tenantId) return null;
60
61
  if (!/^[a-zA-Z0-9_-]+$/.test(tenantId)) {
61
- throw new BadRequestException('Invalid tenant ID format');
62
+ throw new BadRequestException({
63
+ message: 'Invalid tenant ID format',
64
+ messageKey: SYSTEM_MESSAGES.INVALID_TENANT_ID
65
+ });
62
66
  }
63
67
  return tenantId;
64
68
  }
@@ -67,124 +71,78 @@ export class MultiTenantDataSourceService {
67
71
  return tenantId ? this.getTenant(tenantId) : null;
68
72
  }
69
73
  getTenant(tenantId) {
70
- return MultiTenantDataSourceService.tenantsRegistry.get(tenantId) ?? null;
71
- }
72
- getAllTenants() {
73
- return Array.from(MultiTenantDataSourceService.tenantsRegistry.values());
74
- }
75
- getActiveTenants() {
76
- return this.getAllTenants();
74
+ return this.self.tenantsRegistry.get(tenantId) ?? null;
77
75
  }
78
76
  async getDataSource() {
79
77
  return this.isMultiTenant() ? this.getTenantDataSource() : this.getSingleDataSource();
80
78
  }
81
- async getDataSourceForTenant(tenantId) {
82
- const tenant = this.getTenant(tenantId);
83
- if (!tenant) throw new Error(`Tenant '${tenantId}' not found`);
84
- return this.getOrCreateTenantConnection(tenant);
85
- }
86
- setDataSource(dataSource) {
87
- MultiTenantDataSourceService.singleDataSource = dataSource;
88
- }
89
79
  async getRepository(entity) {
90
80
  const dataSource = await this.getDataSource();
91
81
  return dataSource.getRepository(entity);
92
82
  }
93
- async withTenant(tenantId, callback) {
94
- const dataSource = await this.getDataSourceForTenant(tenantId);
95
- return callback(dataSource);
96
- }
97
- async forAllTenants(callback) {
98
- const results = new Map();
99
- for (const tenant of this.getActiveTenants()){
100
- try {
101
- const dataSource = await this.getOrCreateTenantConnection(tenant);
102
- results.set(tenant.id, await callback(tenant, dataSource));
103
- } catch (error) {
104
- this.logger.error(`Error processing tenant ${tenant.id}: ${error}`);
105
- }
106
- }
107
- return results;
108
- }
109
- registerTenant(tenant) {
110
- MultiTenantDataSourceService.tenantsRegistry.set(tenant.id, tenant);
111
- }
112
- async removeTenant(tenantId) {
113
- await this.closeTenantConnection(tenantId);
114
- MultiTenantDataSourceService.tenantsRegistry.delete(tenantId);
115
- }
116
83
  async closeTenantConnection(tenantId) {
117
- const connection = MultiTenantDataSourceService.tenantConnections.get(tenantId);
84
+ const connection = this.self.tenantConnections.get(tenantId);
118
85
  if (connection?.isInitialized) {
119
86
  await connection.destroy();
120
- MultiTenantDataSourceService.tenantConnections.delete(tenantId);
121
- this.logger.log(`Closed connection for tenant: ${tenantId}`);
87
+ this.self.tenantConnections.delete(tenantId);
122
88
  }
123
89
  }
124
90
  async onModuleDestroy() {
125
- for (const [tenantId] of MultiTenantDataSourceService.tenantConnections){
91
+ for (const [tenantId] of this.self.tenantConnections){
126
92
  await this.closeTenantConnection(tenantId);
127
93
  }
128
94
  }
129
- static reset() {
130
- MultiTenantDataSourceService.initialized = false;
131
- MultiTenantDataSourceService.singleDataSource = null;
132
- MultiTenantDataSourceService.singleConnectionLock = null;
133
- MultiTenantDataSourceService.tenantConnections.clear();
134
- MultiTenantDataSourceService.tenantsRegistry.clear();
135
- MultiTenantDataSourceService.connectionLocks.clear();
136
- }
137
- // Protected Methods (for subclasses)
138
- /**
139
- * Get single database DataSource with connection locking to prevent race conditions
140
- */ async getSingleDataSource() {
141
- // Return existing initialized connection
142
- if (MultiTenantDataSourceService.singleDataSource?.isInitialized) {
143
- return MultiTenantDataSourceService.singleDataSource;
95
+ async getSingleDataSource() {
96
+ if (this.self.singleDataSource?.isInitialized) {
97
+ return this.self.singleDataSource;
144
98
  }
145
- // If another request is creating the connection, wait for it
146
- if (MultiTenantDataSourceService.singleConnectionLock) {
147
- return MultiTenantDataSourceService.singleConnectionLock;
99
+ if (this.self.singleConnectionLock) {
100
+ return this.self.singleConnectionLock;
148
101
  }
149
102
  const config = this.getDefaultDatabaseConfig();
150
103
  if (!config) {
151
- throw new Error('No database config available. Provide defaultDatabaseConfig or tenantDefaultDatabaseConfig.');
104
+ throw new InternalServerErrorException({
105
+ message: 'No database config available. Provide defaultDatabaseConfig or tenantDefaultDatabaseConfig.',
106
+ messageKey: SYSTEM_MESSAGES.DATABASE_CONFIG_NOT_AVAILABLE
107
+ });
152
108
  }
153
- // Create connection with lock to prevent race conditions
154
109
  const connectionPromise = this.createDataSourceFromConfig(config);
155
- MultiTenantDataSourceService.singleConnectionLock = connectionPromise;
110
+ this.self.singleConnectionLock = connectionPromise;
156
111
  try {
157
112
  const dataSource = await connectionPromise;
158
- MultiTenantDataSourceService.singleDataSource = dataSource;
113
+ this.self.singleDataSource = dataSource;
159
114
  return dataSource;
160
115
  } finally{
161
- MultiTenantDataSourceService.singleConnectionLock = null;
116
+ this.self.singleConnectionLock = null;
162
117
  }
163
118
  }
164
- /**
165
- * Get tenant-specific DataSource
166
- */ async getTenantDataSource() {
119
+ async getTenantDataSource() {
167
120
  const tenant = this.getCurrentTenant();
168
121
  if (!tenant) {
169
- throw new Error(`Tenant not found. Ensure '${this.tenantHeader}' header is set.`);
122
+ throw new BadRequestException({
123
+ message: `Tenant not found. Ensure '${this.tenantHeader}' header is set.`,
124
+ messageKey: SYSTEM_MESSAGES.TENANT_HEADER_REQUIRED,
125
+ messageVariables: {
126
+ header: this.tenantHeader
127
+ }
128
+ });
170
129
  }
171
130
  return this.getOrCreateTenantConnection(tenant);
172
131
  }
173
132
  async getOrCreateTenantConnection(tenant) {
174
- const existing = MultiTenantDataSourceService.tenantConnections.get(tenant.id);
133
+ const existing = this.self.tenantConnections.get(tenant.id);
175
134
  if (existing?.isInitialized) return existing;
176
- const pendingConnection = MultiTenantDataSourceService.connectionLocks.get(tenant.id);
135
+ const pendingConnection = this.self.connectionLocks.get(tenant.id);
177
136
  if (pendingConnection) return pendingConnection;
178
137
  const config = this.buildTenantDatabaseConfig(tenant);
179
138
  const connectionPromise = this.createDataSourceFromConfig(config);
180
- MultiTenantDataSourceService.connectionLocks.set(tenant.id, connectionPromise);
139
+ this.self.connectionLocks.set(tenant.id, connectionPromise);
181
140
  try {
182
141
  const dataSource = await connectionPromise;
183
- MultiTenantDataSourceService.tenantConnections.set(tenant.id, dataSource);
184
- this.logger.log(`Created connection for tenant: ${tenant.id}`);
142
+ this.self.tenantConnections.set(tenant.id, dataSource);
185
143
  return dataSource;
186
144
  } finally{
187
- MultiTenantDataSourceService.connectionLocks.delete(tenant.id);
145
+ this.self.connectionLocks.delete(tenant.id);
188
146
  }
189
147
  }
190
148
  getDefaultDatabaseConfig() {
@@ -192,7 +150,12 @@ export class MultiTenantDataSourceService {
192
150
  }
193
151
  buildTenantDatabaseConfig(tenant) {
194
152
  const defaultConfig = this.getDefaultDatabaseConfig();
195
- if (!defaultConfig) throw new Error('No default database config for multi-tenant mode.');
153
+ if (!defaultConfig) {
154
+ throw new InternalServerErrorException({
155
+ message: 'No default database config for multi-tenant mode.',
156
+ messageKey: SYSTEM_MESSAGES.DATABASE_CONFIG_NOT_AVAILABLE
157
+ });
158
+ }
196
159
  return {
197
160
  type: defaultConfig.type,
198
161
  host: tenant.host ?? defaultConfig.host,
@@ -220,11 +183,9 @@ export class MultiTenantDataSourceService {
220
183
  constructor(options, request){
221
184
  _define_property(this, "options", void 0);
222
185
  _define_property(this, "request", void 0);
223
- _define_property(this, "logger", void 0);
224
186
  _define_property(this, "tenantHeader", void 0);
225
187
  this.options = options;
226
188
  this.request = request;
227
- this.logger = new Logger(this.constructor.name);
228
189
  this.tenantHeader = DEFAULT_TENANT_HEADER;
229
190
  this.initializeFromOptions();
230
191
  }
@@ -1,23 +1,10 @@
1
- function _define_property(obj, key, value) {
2
- if (key in obj) {
3
- Object.defineProperty(obj, key, {
4
- value: value,
5
- enumerable: true,
6
- configurable: true,
7
- writable: true
8
- });
9
- } else {
10
- obj[key] = value;
11
- }
12
- return obj;
13
- }
14
1
  function _ts_decorate(decorators, target, key, desc) {
15
2
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
16
3
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
17
4
  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;
18
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
19
6
  }
20
- import { Injectable, Logger } from '@nestjs/common';
7
+ import { Injectable } from '@nestjs/common';
21
8
  export class UtilsService {
22
9
  // ---------------- CACHE HELPERS ----------------
23
10
  /**
@@ -37,8 +24,8 @@ export class UtilsService {
37
24
  const keys = await cacheManager.get(trackingKey) || [];
38
25
  if (!keys.includes(cacheKey)) keys.push(cacheKey);
39
26
  await cacheManager.set(trackingKey, keys);
40
- } catch (error) {
41
- this.logger.error(`Cache tracking failed for ${entityName}`, error instanceof Error ? error.stack : String(error));
27
+ } catch {
28
+ // Silent failure - cache tracking is non-critical
42
29
  }
43
30
  }
44
31
  /**
@@ -49,8 +36,8 @@ export class UtilsService {
49
36
  const keys = await cacheManager.get(trackingKey) || [];
50
37
  await Promise.allSettled(keys.map((key)=>cacheManager.del(key)));
51
38
  await cacheManager.del(trackingKey);
52
- } catch (error) {
53
- this.logger.error(`Cache invalidation failed for ${entityName}`, error instanceof Error ? error.stack : String(error));
39
+ } catch {
40
+ // Silent failure - cache invalidation is non-critical
54
41
  }
55
42
  }
56
43
  // ---------------- STRING HELPERS ----------------
@@ -73,9 +60,6 @@ export class UtilsService {
73
60
  const prefix = this.buildTenantPrefix(tenantId);
74
61
  return entityId ? `${prefix}entity_${entityName}_id_${entityId}_keys` : `${prefix}entity_${entityName}_keys`;
75
62
  }
76
- constructor(){
77
- _define_property(this, "logger", new Logger(UtilsService.name));
78
- }
79
63
  }
80
64
  UtilsService = _ts_decorate([
81
65
  Injectable()
@@ -0,0 +1,70 @@
1
+ function getOffsetMinutes(utcDate, timezone) {
2
+ const extract = (tz)=>{
3
+ const parts = new Intl.DateTimeFormat('en-US', {
4
+ timeZone: tz,
5
+ year: 'numeric',
6
+ month: '2-digit',
7
+ day: '2-digit',
8
+ hour: '2-digit',
9
+ minute: '2-digit',
10
+ second: '2-digit',
11
+ hour12: false
12
+ }).formatToParts(utcDate);
13
+ const p = Object.fromEntries(parts.filter((x)=>x.type !== 'literal').map((x)=>[
14
+ x.type,
15
+ parseInt(x.value)
16
+ ]));
17
+ return Date.UTC(p['year'], p['month'] - 1, p['day'], p['hour'], p['minute'], p['second']);
18
+ };
19
+ return (extract(timezone) - extract('UTC')) / 60_000;
20
+ }
21
+ export function localToUtc(dateStr, timeStr, timezone) {
22
+ const [year, month, day] = dateStr.split('-').map(Number);
23
+ const [hours, minutes] = timeStr.split(':').map(Number);
24
+ const approx = new Date(Date.UTC(year, month - 1, day, hours, minutes, 0));
25
+ const offset = getOffsetMinutes(approx, timezone);
26
+ const utcDate = new Date(approx.getTime() - offset * 60_000);
27
+ // DST boundary guard: re-check offset after adjustment
28
+ const finalOffset = getOffsetMinutes(utcDate, timezone);
29
+ return finalOffset !== offset ? new Date(approx.getTime() - finalOffset * 60_000) : utcDate;
30
+ }
31
+ export function utcToLocal(utcDate, timezone) {
32
+ const d = typeof utcDate === 'string' ? new Date(utcDate) : utcDate;
33
+ const parts = new Intl.DateTimeFormat('en-CA', {
34
+ timeZone: timezone,
35
+ year: 'numeric',
36
+ month: '2-digit',
37
+ day: '2-digit',
38
+ hour: '2-digit',
39
+ minute: '2-digit',
40
+ hour12: false
41
+ }).formatToParts(d);
42
+ const p = Object.fromEntries(parts.filter((x)=>x.type !== 'literal').map((x)=>[
43
+ x.type,
44
+ x.value
45
+ ]));
46
+ // en-CA formats midnight hour as '24' — normalize to '00'
47
+ const hour = p['hour'] === '24' ? '00' : p['hour'];
48
+ return {
49
+ date: `${p['year']}-${p['month']}-${p['day']}`,
50
+ time: `${hour}:${p['minute']}`,
51
+ display: new Intl.DateTimeFormat('en-US', {
52
+ timeZone: timezone,
53
+ dateStyle: 'medium',
54
+ timeStyle: 'short'
55
+ }).format(d)
56
+ };
57
+ }
58
+ export function formatInTimezone(utcDate, timezone, options = {
59
+ dateStyle: 'medium',
60
+ timeStyle: 'short'
61
+ }) {
62
+ const d = typeof utcDate === 'string' ? new Date(utcDate) : utcDate;
63
+ return new Intl.DateTimeFormat('en-US', {
64
+ ...options,
65
+ timeZone: timezone
66
+ }).format(d);
67
+ }
68
+ export function getTodayInTimezone(timezone) {
69
+ return utcToLocal(new Date(), timezone).date;
70
+ }
@@ -1,4 +1,4 @@
1
- export * from './error-handler.util';
1
+ export * from './date-time.util';
2
2
  export * from './html-sanitizer.util';
3
3
  export * from './query-helpers.util';
4
4
  export * from './request.util';
@@ -1,4 +1,5 @@
1
1
  import { BadRequestException } from '@nestjs/common';
2
+ import { AUTH_MESSAGES } from '../constants';
2
3
  /**
3
4
  * Apply company filter to a query builder if company feature is enabled.
4
5
  * Centralizes the common pattern of filtering by user's company.
@@ -72,7 +73,13 @@ import { BadRequestException } from '@nestjs/common';
72
73
  */ export function validateCompanyOwnership(entity, user, isCompanyFeatureEnabled, entityName) {
73
74
  if (isCompanyFeatureEnabled && user?.companyId && hasCompanyId(entity)) {
74
75
  if (entity.companyId && entity.companyId !== user.companyId) {
75
- throw new BadRequestException(`${entityName} belongs to another company`);
76
+ throw new BadRequestException({
77
+ message: `${entityName} belongs to another company`,
78
+ messageKey: AUTH_MESSAGES.ENTITY_BELOG_ANOTHER_COMPANY,
79
+ messageVariables: {
80
+ entity: entityName
81
+ }
82
+ });
76
83
  }
77
84
  }
78
85
  }
@@ -57,3 +57,25 @@ import { CLIENT_TYPE_HEADER } from '../constants';
57
57
  const unit = match[2];
58
58
  return value * (TIME_UNIT_MS[unit] ?? TIME_UNIT_MS.d);
59
59
  }
60
+ /**
61
+ * Determine the frontend base URL for auth-related links.
62
+ * Uses allowed origins from envConfig first, then falls back to request host.
63
+ */ export function extractFrontendUrl(req) {
64
+ const allowedOrigins = envConfig.getOrigins();
65
+ const origin = (req.get('origin') ?? req.get('referer') ?? '').trim();
66
+ if (origin && allowedOrigins.length) {
67
+ const matched = allowedOrigins.find((u)=>origin.startsWith(u));
68
+ if (matched) {
69
+ return matched;
70
+ }
71
+ }
72
+ const host = req.get('host');
73
+ if (!host) {
74
+ throw new Error('Unable to determine frontend URL from request');
75
+ }
76
+ return `${req.protocol}://${host}`;
77
+ }
78
+ export function buildFrontendUrl(req, path) {
79
+ const baseUrl = extractFrontendUrl(req);
80
+ return `${baseUrl}${path}`;
81
+ }
@@ -0,0 +1,9 @@
1
+ import { ArgumentsHost, ExceptionFilter } from '@nestjs/common';
2
+ export declare class GlobalExceptionFilter implements ExceptionFilter {
3
+ private readonly logger;
4
+ catch(exception: unknown, host: ArgumentsHost): void;
5
+ private resolveException;
6
+ private resolveHttpException;
7
+ private resolveQueryFailedError;
8
+ private toHumanReadable;
9
+ }
@@ -0,0 +1 @@
1
+ export * from './global-exception.filter';
@@ -1,19 +1,15 @@
1
1
  import { CanActivate, ExecutionContext } from '@nestjs/common';
2
2
  import { Reflector } from '@nestjs/core';
3
3
  import { HybridCache } from '../classes/hybrid-cache.class';
4
- import { ILogger } from '../interfaces/logger.interface';
5
4
  import { PermissionGuardConfig } from '../interfaces/permission.interface';
6
5
  export declare class PermissionGuard implements CanActivate {
7
6
  private readonly reflector;
8
7
  private readonly cache?;
9
8
  private readonly config;
10
- private readonly logger;
11
- constructor(reflector: Reflector, cache?: HybridCache, config?: PermissionGuardConfig, logger?: ILogger);
9
+ constructor(reflector: Reflector, cache?: HybridCache | undefined, config?: PermissionGuardConfig);
12
10
  canActivate(context: ExecutionContext): Promise<boolean>;
13
- private normalizePermissionConfig;
14
- private validateSimplePermissions;
15
- private isNestedCondition;
16
- private evaluateCondition;
11
+ private normalizeToLogicNode;
12
+ private evaluateLogicNode;
17
13
  private getUserPermissions;
18
14
  private buildPermissionCacheKey;
19
15
  private hasPermission;
package/index.d.ts CHANGED
@@ -3,7 +3,9 @@ export * from './constants';
3
3
  export * from './decorators';
4
4
  export * from './dtos';
5
5
  export * from './entities';
6
+ export * from './enums';
6
7
  export * from './exceptions';
8
+ export * from './filters';
7
9
  export * from './guards';
8
10
  export * from './interceptors';
9
11
  export * from './interfaces';
@@ -5,6 +5,6 @@ export declare class IdempotencyInterceptor implements NestInterceptor {
5
5
  private readonly cache?;
6
6
  private readonly CACHE_TTL;
7
7
  private readonly PROCESSING_MARKER;
8
- constructor(cache?: HybridCache);
8
+ constructor(cache?: HybridCache | undefined);
9
9
  intercept<T = any>(context: ExecutionContext, next: CallHandler<T>): Promise<Observable<T>>;
10
10
  }
@@ -1,6 +1,5 @@
1
1
  export * from './delete-empty-id-from-body.interceptor';
2
2
  export * from './idempotency.interceptor';
3
- export * from './query-performance.interceptor';
4
3
  export * from './response-meta.interceptor';
5
4
  export * from './set-user-field-on-body.interceptor';
6
5
  export * from './slug.interceptor';