@flusys/nestjs-shared 1.1.0-beta → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +501 -720
  2. package/cjs/classes/api-controller.class.js +9 -24
  3. package/cjs/classes/api-service.class.js +59 -92
  4. package/cjs/classes/index.js +1 -0
  5. package/cjs/classes/winston-logger-adapter.class.js +23 -40
  6. package/cjs/constants/index.js +14 -0
  7. package/cjs/constants/permissions.js +184 -0
  8. package/cjs/decorators/api-response.decorator.js +1 -1
  9. package/cjs/decorators/index.js +1 -0
  10. package/cjs/decorators/sanitize-html.decorator.js +36 -0
  11. package/cjs/dtos/delete.dto.js +10 -0
  12. package/cjs/dtos/filter-and-pagination.dto.js +24 -34
  13. package/cjs/dtos/pagination.dto.js +4 -8
  14. package/cjs/dtos/response-payload.dto.js +0 -116
  15. package/cjs/entities/identity.js +4 -4
  16. package/cjs/entities/user-root.js +13 -14
  17. package/cjs/guards/permission.guard.js +51 -105
  18. package/cjs/interceptors/index.js +1 -3
  19. package/cjs/interceptors/set-user-field-on-body.interceptor.js +60 -0
  20. package/cjs/interceptors/slug.interceptor.js +30 -9
  21. package/cjs/interfaces/datasource.interface.js +4 -0
  22. package/cjs/interfaces/index.js +2 -1
  23. package/cjs/interfaces/module-config.interface.js +4 -0
  24. package/cjs/middlewares/logger.middleware.js +50 -89
  25. package/cjs/modules/cache/cache.module.js +3 -3
  26. package/cjs/modules/datasource/datasource.module.js +11 -14
  27. package/cjs/modules/datasource/multi-tenant-datasource.service.js +29 -113
  28. package/cjs/modules/utils/utils.service.js +40 -203
  29. package/cjs/utils/error-handler.util.js +35 -12
  30. package/cjs/utils/html-sanitizer.util.js +64 -0
  31. package/cjs/utils/index.js +4 -0
  32. package/cjs/utils/query-helpers.util.js +53 -0
  33. package/cjs/utils/request.util.js +70 -0
  34. package/cjs/utils/string.util.js +63 -0
  35. package/classes/api-controller.class.d.ts +5 -5
  36. package/classes/api-service.class.d.ts +7 -5
  37. package/classes/index.d.ts +1 -0
  38. package/classes/request-scoped-api.service.d.ts +3 -2
  39. package/classes/winston-logger-adapter.class.d.ts +2 -0
  40. package/constants/index.d.ts +1 -0
  41. package/constants/permissions.d.ts +179 -0
  42. package/decorators/index.d.ts +1 -0
  43. package/decorators/sanitize-html.decorator.d.ts +2 -0
  44. package/dtos/delete.dto.d.ts +1 -0
  45. package/dtos/filter-and-pagination.dto.d.ts +0 -2
  46. package/dtos/response-payload.dto.d.ts +0 -20
  47. package/fesm/classes/api-controller.class.js +9 -24
  48. package/fesm/classes/api-service.class.js +59 -92
  49. package/fesm/classes/index.js +2 -0
  50. package/fesm/classes/winston-logger-adapter.class.js +23 -40
  51. package/fesm/constants/index.js +2 -0
  52. package/fesm/constants/permissions.js +128 -0
  53. package/fesm/decorators/api-response.decorator.js +1 -1
  54. package/fesm/decorators/index.js +1 -0
  55. package/fesm/decorators/sanitize-html.decorator.js +45 -0
  56. package/fesm/dtos/delete.dto.js +12 -2
  57. package/fesm/dtos/filter-and-pagination.dto.js +26 -47
  58. package/fesm/dtos/pagination.dto.js +4 -8
  59. package/fesm/dtos/response-payload.dto.js +0 -107
  60. package/fesm/entities/identity.js +4 -4
  61. package/fesm/entities/user-root.js +13 -14
  62. package/fesm/guards/permission.guard.js +51 -105
  63. package/fesm/interceptors/index.js +1 -3
  64. package/fesm/interceptors/set-user-field-on-body.interceptor.js +39 -0
  65. package/fesm/interceptors/slug.interceptor.js +31 -10
  66. package/fesm/interfaces/datasource.interface.js +20 -0
  67. package/fesm/interfaces/index.js +2 -1
  68. package/fesm/interfaces/module-config.interface.js +5 -0
  69. package/fesm/middlewares/logger.middleware.js +50 -83
  70. package/fesm/modules/cache/cache.module.js +2 -2
  71. package/fesm/modules/datasource/datasource.module.js +11 -14
  72. package/fesm/modules/datasource/multi-tenant-datasource.service.js +29 -113
  73. package/fesm/modules/utils/utils.service.js +41 -204
  74. package/fesm/utils/error-handler.util.js +36 -13
  75. package/fesm/utils/html-sanitizer.util.js +69 -0
  76. package/fesm/utils/index.js +4 -0
  77. package/fesm/utils/query-helpers.util.js +78 -0
  78. package/fesm/utils/request.util.js +58 -0
  79. package/fesm/utils/string.util.js +71 -0
  80. package/guards/permission.guard.d.ts +2 -0
  81. package/interceptors/index.d.ts +1 -3
  82. package/interceptors/set-user-field-on-body.interceptor.d.ts +5 -0
  83. package/interceptors/slug.interceptor.d.ts +2 -1
  84. package/interfaces/api.interface.d.ts +2 -2
  85. package/interfaces/datasource.interface.d.ts +5 -0
  86. package/interfaces/identity.interface.d.ts +4 -4
  87. package/interfaces/index.d.ts +2 -1
  88. package/interfaces/logged-user-info.interface.d.ts +0 -2
  89. package/interfaces/module-config.interface.d.ts +6 -0
  90. package/interfaces/permission.interface.d.ts +0 -1
  91. package/middlewares/logger.middleware.d.ts +2 -2
  92. package/modules/datasource/datasource.module.d.ts +1 -0
  93. package/modules/datasource/multi-tenant-datasource.service.d.ts +0 -1
  94. package/modules/utils/utils.service.d.ts +4 -14
  95. package/package.json +4 -4
  96. package/utils/error-handler.util.d.ts +14 -19
  97. package/utils/html-sanitizer.util.d.ts +2 -0
  98. package/utils/index.d.ts +4 -0
  99. package/utils/query-helpers.util.d.ts +16 -0
  100. package/utils/request.util.d.ts +4 -0
  101. package/utils/string.util.d.ts +2 -0
  102. package/cjs/interceptors/set-create-by-on-body.interceptor.js +0 -40
  103. package/cjs/interceptors/set-delete-by-on-body.interceptor.js +0 -40
  104. package/cjs/interceptors/set-update-by-on-body.interceptor.js +0 -40
  105. package/cjs/interfaces/base-query.interface.js +0 -6
  106. package/fesm/interceptors/set-create-by-on-body.interceptor.js +0 -30
  107. package/fesm/interceptors/set-delete-by-on-body.interceptor.js +0 -30
  108. package/fesm/interceptors/set-update-by-on-body.interceptor.js +0 -30
  109. package/fesm/interfaces/base-query.interface.js +0 -3
  110. package/interceptors/set-create-by-on-body.interceptor.d.ts +0 -5
  111. package/interceptors/set-delete-by-on-body.interceptor.d.ts +0 -5
  112. package/interceptors/set-update-by-on-body.interceptor.d.ts +0 -5
  113. package/interfaces/base-query.interface.d.ts +0 -7
@@ -9,247 +9,84 @@ Object.defineProperty(exports, "UtilsService", {
9
9
  }
10
10
  });
11
11
  const _common = require("@nestjs/common");
12
- const _classvalidator = require("class-validator");
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
+ }
13
25
  function _ts_decorate(decorators, target, key, desc) {
14
26
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
15
27
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
16
28
  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;
17
29
  return c > 3 && r && Object.defineProperty(target, key, r), r;
18
30
  }
19
- function _ts_metadata(k, v) {
20
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
21
- }
22
31
  let UtilsService = class UtilsService {
23
32
  // ---------------- CACHE HELPERS ----------------
24
33
  /**
25
34
  * Generate cache key with optional tenant prefix for multi-tenant isolation
26
- * @param entityName - Name of the entity being cached
27
- * @param params - Query parameters for cache key uniqueness
28
- * @param entityId - Optional entity ID for single entity caching
29
- * @param tenantId - Optional tenant ID for multi-tenant isolation
30
35
  */ getCacheKey(entityName, params, entityId, tenantId) {
31
- const tenantPrefix = tenantId ? `tenant_${tenantId}_` : '';
32
- if (entityId) return `${tenantPrefix}entity_${entityName}_id_${entityId}${params ? '_select_' + JSON.stringify(params) : ''}`;
33
- return `${tenantPrefix}entity_${entityName}_all_${JSON.stringify(params)}`;
36
+ const prefix = this.buildTenantPrefix(tenantId);
37
+ if (entityId) {
38
+ return `${prefix}entity_${entityName}_id_${entityId}${params ? '_select_' + JSON.stringify(params) : ''}`;
39
+ }
40
+ return `${prefix}entity_${entityName}_all_${JSON.stringify(params)}`;
34
41
  }
35
42
  /**
36
43
  * Track cache key for later invalidation with optional tenant prefix
37
44
  */ async trackCacheKey(cacheKey, entityName, cacheManager, entityId, tenantId) {
38
- const tenantPrefix = tenantId ? `tenant_${tenantId}_` : '';
39
45
  try {
40
- if (entityId) {
41
- const trackingKey = `${tenantPrefix}entity_${entityName}_id_${entityId}_keys`;
42
- const idKeys = await cacheManager.get(trackingKey) || [];
43
- if (!idKeys.includes(cacheKey)) idKeys.push(cacheKey);
44
- await cacheManager.set(trackingKey, idKeys);
45
- } else {
46
- const trackingKey = `${tenantPrefix}entity_${entityName}_keys`;
47
- const allKeys = await cacheManager.get(trackingKey) || [];
48
- if (!allKeys.includes(cacheKey)) allKeys.push(cacheKey);
49
- await cacheManager.set(trackingKey, allKeys);
50
- }
46
+ const trackingKey = this.buildTrackingKey(entityName, entityId, tenantId);
47
+ const keys = await cacheManager.get(trackingKey) || [];
48
+ if (!keys.includes(cacheKey)) keys.push(cacheKey);
49
+ await cacheManager.set(trackingKey, keys);
51
50
  } catch (error) {
52
- // Log but don't throw - cache tracking failure shouldn't break the request
53
- console.error(`Cache tracking failed for ${entityName}:`, error);
51
+ this.logger.error(`Cache tracking failed for ${entityName}`, error instanceof Error ? error.stack : String(error));
54
52
  }
55
53
  }
56
54
  /**
57
55
  * Clear cache for entity with optional tenant prefix
58
- * Uses Promise.allSettled to ensure all deletions are attempted even if some fail
59
56
  */ async clearCache(entityName, cacheManager, entityId, tenantId) {
60
- const tenantPrefix = tenantId ? `tenant_${tenantId}_` : '';
61
57
  try {
62
- if (entityId) {
63
- const trackingKey = `${tenantPrefix}entity_${entityName}_id_${entityId}_keys`;
64
- const idKeys = await cacheManager.get(trackingKey) || [];
65
- // Use Promise.allSettled to ensure all deletions are attempted
66
- await Promise.allSettled(idKeys.map((key)=>cacheManager.del(key)));
67
- await cacheManager.del(trackingKey);
68
- } else {
69
- const trackingKey = `${tenantPrefix}entity_${entityName}_keys`;
70
- const keySet = await cacheManager.get(trackingKey) || [];
71
- // Use Promise.allSettled to ensure all deletions are attempted
72
- await Promise.allSettled(keySet.map((key)=>cacheManager.del(key)));
73
- await cacheManager.del(trackingKey);
74
- }
58
+ const trackingKey = this.buildTrackingKey(entityName, entityId, tenantId);
59
+ const keys = await cacheManager.get(trackingKey) || [];
60
+ await Promise.allSettled(keys.map((key)=>cacheManager.del(key)));
61
+ await cacheManager.del(trackingKey);
75
62
  } catch (error) {
76
- // Log but don't throw - cache invalidation failure shouldn't break the request
77
- console.error(`Cache invalidation failed for ${entityName}:`, error);
78
- }
79
- }
80
- /**
81
- * Check Phone or Email
82
- */ checkPhoneOrEmail(value) {
83
- if ((0, _classvalidator.isEmail)(value)) {
84
- return {
85
- value: value,
86
- type: 'email'
87
- };
88
- }
89
- const phoneMatch = value.match(/^((\+880)|0)?(13|15|16|17|18|19)\d{8}$/);
90
- if (phoneMatch) {
91
- let phone = phoneMatch[0];
92
- if (!phone.startsWith('+88')) {
93
- phone = '+88' + phone;
94
- }
95
- return {
96
- value: phone,
97
- type: 'phone'
98
- };
63
+ this.logger.error(`Cache invalidation failed for ${entityName}`, error instanceof Error ? error.stack : String(error));
99
64
  }
100
- return {
101
- value: null,
102
- type: null
103
- };
104
65
  }
66
+ // ---------------- STRING HELPERS ----------------
105
67
  /**
106
- * STRING FUNCTIONS
107
- * transformToSlug
68
+ * Transform string to URL-friendly slug
108
69
  */ transformToSlug(value, salt) {
109
70
  const slug = value?.trim().replace(/[^A-Z0-9]+/gi, '-').toLowerCase();
110
71
  return salt ? `${slug}-${this.getRandomInt(1, 100)}` : slug;
111
72
  }
112
73
  /**
113
- * RANDOM FUNCTIONS
114
- * getRandomInt
115
- * generateRandomId
116
- * getRandomOtpCode
74
+ * Generate random integer between min and max (inclusive)
117
75
  */ getRandomInt(min, max) {
118
76
  return Math.floor(Math.random() * (max - min + 1)) + min;
119
77
  }
120
- generateRandomId(length) {
121
- const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
122
- let result = '';
123
- for(let i = 0; i < length; i++){
124
- result += characters.charAt(Math.floor(Math.random() * characters.length));
125
- }
126
- return result;
127
- }
128
- getRandomOtpCode() {
129
- return Math.floor(Math.random() * (9999 - 1000 + 1)) + 1000;
78
+ // ---------------- PRIVATE HELPERS ----------------
79
+ buildTenantPrefix(tenantId) {
80
+ return tenantId ? `tenant_${tenantId}_` : '';
130
81
  }
131
- extractColumnNameFromError(detail) {
132
- const match = detail.match(/\((.*?)\)=\((.*?)\)/);
133
- return {
134
- columnName: match ? match[1] : 'unknown',
135
- value: match ? match[2] : 'unknown'
136
- };
137
- }
138
- getOtpEmailFormat(otp, userName) {
139
- return `
140
- <!DOCTYPE html>
141
- <html lang="en">
142
- <head>
143
- <meta charset="UTF-8" />
144
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
145
- <title>Your OTP Code</title>
146
- <style>
147
- body {
148
- font-family: Arial, sans-serif;
149
- background-color: #f4f4f7;
150
- margin: 0;
151
- padding: 0;
152
- }
153
- .email-container {
154
- max-width: 500px;
155
- margin: 40px auto;
156
- background-color: #ffffff;
157
- padding: 30px;
158
- border-radius: 8px;
159
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
160
- text-align: center;
161
- }
162
- h1 {
163
- color: #333;
164
- }
165
- p {
166
- font-size: 16px;
167
- color: #555;
168
- }
169
- .otp-box {
170
- display: inline-block;
171
- margin: 20px 0;
172
- padding: 14px 28px;
173
- font-size: 24px;
174
- letter-spacing: 6px;
175
- background-color: #007BFF;
176
- color: white;
177
- border-radius: 8px;
178
- font-weight: bold;
179
- }
180
- </style>
181
- </head>
182
- <body>
183
- <div class="email-container">
184
- <p>Hi ${userName || 'Sir/Madam'},</p>
185
- <p>Use the code below to verify your identity:</p>
186
- <div class="otp-box">${otp}</div>
187
- <p>This OTP is valid for a limited time. Do not share it with anyone.</p>
188
- <p>If you didn’t request this, please ignore this email.</p>
189
- </div>
190
- </body>
191
- </html>
192
- `;
82
+ buildTrackingKey(entityName, entityId, tenantId) {
83
+ const prefix = this.buildTenantPrefix(tenantId);
84
+ return entityId ? `${prefix}entity_${entityName}_id_${entityId}_keys` : `${prefix}entity_${entityName}_keys`;
193
85
  }
194
- getResetPasswordEmailFormat(resetLink, userName) {
195
- return `
196
- <!DOCTYPE html>
197
- <html lang="en">
198
- <head>
199
- <meta charset="UTF-8" />
200
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
201
- <title>Reset Your Password</title>
202
- <style>
203
- body {
204
- font-family: Arial, sans-serif;
205
- background-color: #f4f4f7;
206
- margin: 0;
207
- padding: 0;
208
- }
209
- .email-container {
210
- max-width: 500px;
211
- margin: 40px auto;
212
- background-color: #ffffff;
213
- padding: 30px;
214
- border-radius: 8px;
215
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
216
- text-align: center;
217
- }
218
- h1 {
219
- color: #333;
220
- }
221
- p {
222
- font-size: 16px;
223
- color: #555;
224
- }
225
- .reset-button {
226
- display: inline-block;
227
- margin: 20px 0;
228
- padding: 14px 28px;
229
- font-size: 16px;
230
- background-color: #007BFF;
231
- color: #ffffff !important;
232
- text-decoration: none;
233
- border-radius: 6px;
234
- font-weight: bold;
235
- }
236
- </style>
237
- </head>
238
- <body>
239
- <div class="email-container">
240
- <p>Hi ${userName || 'Sir/Madam'},</p>
241
- <p>We received a request to reset your password. Click the button below to reset it:</p>
242
- <a href="${resetLink}" class="reset-button" target="_blank">Reset Password</a>
243
- <p>If you didn’t request a password reset, you can safely ignore this email.</p>
244
- </div>
245
- </body>
246
- </html>
247
- `;
86
+ constructor(){
87
+ _define_property(this, "logger", new _common.Logger(UtilsService.name));
248
88
  }
249
- constructor(){}
250
89
  };
251
90
  UtilsService = _ts_decorate([
252
- (0, _common.Injectable)(),
253
- _ts_metadata("design:type", Function),
254
- _ts_metadata("design:paramtypes", [])
91
+ (0, _common.Injectable)()
255
92
  ], UtilsService);
@@ -8,9 +8,17 @@ Object.defineProperty(exports, "ErrorHandler", {
8
8
  return ErrorHandler;
9
9
  }
10
10
  });
11
+ /** Sensitive keys that should be redacted from logs */ const SENSITIVE_KEYS = [
12
+ 'password',
13
+ 'secret',
14
+ 'token',
15
+ 'apiKey',
16
+ 'credential',
17
+ 'authorization'
18
+ ];
11
19
  let ErrorHandler = class ErrorHandler {
12
20
  /**
13
- * Safely extract error message from unknown error
21
+ * Safely extract error message from unknown error.
14
22
  */ static getErrorMessage(error) {
15
23
  if (error instanceof Error) {
16
24
  return error.message;
@@ -21,15 +29,25 @@ let ErrorHandler = class ErrorHandler {
21
29
  return 'Unknown error occurred';
22
30
  }
23
31
  /**
24
- * Safely extract error stack from unknown error
25
- */ static getErrorStack(error) {
26
- if (error instanceof Error) {
27
- return error.stack;
32
+ * Sanitize context data to redact sensitive fields from logs.
33
+ */ static sanitizeContextForLogging(context) {
34
+ const sanitized = {};
35
+ for (const [key, value] of Object.entries(context)){
36
+ const isSensitive = SENSITIVE_KEYS.some((sk)=>key.toLowerCase().includes(sk.toLowerCase()));
37
+ if (isSensitive) {
38
+ sanitized[key] = '[REDACTED]';
39
+ } else if (Array.isArray(value)) {
40
+ sanitized[key] = value.map((item)=>typeof item === 'object' && item !== null ? this.sanitizeContextForLogging(item) : item);
41
+ } else if (typeof value === 'object' && value !== null) {
42
+ sanitized[key] = this.sanitizeContextForLogging(value);
43
+ } else {
44
+ sanitized[key] = value;
45
+ }
28
46
  }
29
- return undefined;
47
+ return sanitized;
30
48
  }
31
49
  /**
32
- * Create error context object for logging
50
+ * Create error context object for internal logging.
33
51
  */ static createErrorContext(error, context) {
34
52
  const errorContext = {
35
53
  error: {
@@ -41,27 +59,32 @@ let ErrorHandler = class ErrorHandler {
41
59
  errorContext.error.name = error.name;
42
60
  }
43
61
  if (context && Object.keys(context).length > 0) {
44
- errorContext.context = context;
62
+ errorContext.context = this.sanitizeContextForLogging(context);
45
63
  }
46
64
  return errorContext;
47
65
  }
48
66
  /**
49
- * Log error with consistent format
67
+ * Log error with consistent format.
50
68
  */ static logError(logger, error, operation, context) {
51
69
  const errorContext = this.createErrorContext(error, {
52
70
  operation,
53
71
  ...context
54
72
  });
55
73
  const errorMessage = `Failed to ${operation}: ${errorContext.error.message}`;
56
- const loggerContext = logger.context || 'ErrorHandler';
57
- logger.error(errorMessage, errorContext.error.stack, loggerContext, errorContext);
74
+ logger.error(errorMessage, errorContext.error.stack, errorContext);
58
75
  }
59
76
  /**
60
- * Re-throw error with proper type checking
77
+ * Re-throw error with proper type checking.
61
78
  */ static rethrowError(error) {
62
79
  if (error instanceof Error) {
63
80
  throw error;
64
81
  }
65
82
  throw new Error(`Unexpected error: ${String(error)}`);
66
83
  }
84
+ /**
85
+ * Log error and re-throw (common pattern).
86
+ */ static logAndRethrow(logger, error, operation, context) {
87
+ this.logError(logger, error, operation, context);
88
+ this.rethrowError(error);
89
+ }
67
90
  };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * HTML Sanitizer Utilities
3
+ *
4
+ * Provides functions for escaping HTML content to prevent XSS attacks.
5
+ * Use these utilities when interpolating user-provided variables into HTML content.
6
+ */ /**
7
+ * HTML entity mapping for escaping special characters
8
+ */ "use strict";
9
+ Object.defineProperty(exports, "__esModule", {
10
+ value: true
11
+ });
12
+ function _export(target, all) {
13
+ for(var name in all)Object.defineProperty(target, name, {
14
+ enumerable: true,
15
+ get: Object.getOwnPropertyDescriptor(all, name).get
16
+ });
17
+ }
18
+ _export(exports, {
19
+ get escapeHtml () {
20
+ return escapeHtml;
21
+ },
22
+ get escapeHtmlVariables () {
23
+ return escapeHtmlVariables;
24
+ }
25
+ });
26
+ const HTML_ESCAPE_MAP = {
27
+ '&': '&amp;',
28
+ '<': '&lt;',
29
+ '>': '&gt;',
30
+ '"': '&quot;',
31
+ "'": '&#x27;',
32
+ '/': '&#x2F;',
33
+ '`': '&#x60;',
34
+ '=': '&#x3D;'
35
+ };
36
+ /**
37
+ * Regex pattern matching characters that need HTML escaping
38
+ */ const HTML_ESCAPE_REGEX = /[&<>"'`=/]/g;
39
+ function escapeHtml(str) {
40
+ if (!str || typeof str !== 'string') {
41
+ return str ?? '';
42
+ }
43
+ return str.replace(HTML_ESCAPE_REGEX, (char)=>HTML_ESCAPE_MAP[char]);
44
+ }
45
+ function escapeHtmlVariables(variables) {
46
+ if (!variables || typeof variables !== 'object') {
47
+ return {};
48
+ }
49
+ const escaped = {};
50
+ for (const [key, value] of Object.entries(variables)){
51
+ if (value === null || value === undefined) {
52
+ escaped[key] = '';
53
+ } else if (typeof value === 'string') {
54
+ escaped[key] = escapeHtml(value);
55
+ } else if (typeof value === 'object') {
56
+ // For objects/arrays, stringify and escape
57
+ escaped[key] = escapeHtml(JSON.stringify(value));
58
+ } else {
59
+ // For numbers, booleans, etc., convert to string (no escaping needed)
60
+ escaped[key] = String(value);
61
+ }
62
+ }
63
+ return escaped;
64
+ }
@@ -3,6 +3,10 @@ Object.defineProperty(exports, "__esModule", {
3
3
  value: true
4
4
  });
5
5
  _export_star(require("./error-handler.util"), exports);
6
+ _export_star(require("./html-sanitizer.util"), exports);
7
+ _export_star(require("./query-helpers.util"), exports);
8
+ _export_star(require("./request.util"), exports);
9
+ _export_star(require("./string.util"), exports);
6
10
  function _export_star(from, to) {
7
11
  Object.keys(from).forEach(function(k) {
8
12
  if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
@@ -0,0 +1,53 @@
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 applyCompanyFilter () {
13
+ return applyCompanyFilter;
14
+ },
15
+ get buildCompanyWhereCondition () {
16
+ return buildCompanyWhereCondition;
17
+ },
18
+ get hasCompanyId () {
19
+ return hasCompanyId;
20
+ },
21
+ get validateCompanyOwnership () {
22
+ return validateCompanyOwnership;
23
+ }
24
+ });
25
+ const _common = require("@nestjs/common");
26
+ function applyCompanyFilter(query, config, user) {
27
+ const columnName = config.columnName ?? 'companyId';
28
+ if (config.isCompanyFeatureEnabled && user?.companyId) {
29
+ query.andWhere(`${config.entityAlias}.${columnName} = :companyId`, {
30
+ companyId: user.companyId
31
+ });
32
+ }
33
+ return query;
34
+ }
35
+ function buildCompanyWhereCondition(baseWhere, isCompanyFeatureEnabled, user) {
36
+ if (isCompanyFeatureEnabled && user?.companyId) {
37
+ return {
38
+ ...baseWhere,
39
+ companyId: user.companyId
40
+ };
41
+ }
42
+ return baseWhere;
43
+ }
44
+ function hasCompanyId(entity) {
45
+ return entity !== null && typeof entity === 'object' && 'companyId' in entity;
46
+ }
47
+ function validateCompanyOwnership(entity, user, isCompanyFeatureEnabled, entityName) {
48
+ if (isCompanyFeatureEnabled && user?.companyId && hasCompanyId(entity)) {
49
+ if (entity.companyId && entity.companyId !== user.companyId) {
50
+ throw new _common.BadRequestException(`${entityName} belongs to another company`);
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,70 @@
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 buildCookieOptions () {
13
+ return buildCookieOptions;
14
+ },
15
+ get isBrowserRequest () {
16
+ return isBrowserRequest;
17
+ },
18
+ get parseDurationToMs () {
19
+ return parseDurationToMs;
20
+ }
21
+ });
22
+ const _constants = require("../constants");
23
+ /** Time unit multipliers in milliseconds */ const TIME_UNIT_MS = {
24
+ s: 1000,
25
+ m: 60 * 1000,
26
+ h: 60 * 60 * 1000,
27
+ d: 24 * 60 * 60 * 1000,
28
+ w: 7 * 24 * 60 * 60 * 1000
29
+ };
30
+ /** Get normalized client type from request header */ function getClientType(req) {
31
+ const clientType = req.headers[_constants.CLIENT_TYPE_HEADER];
32
+ return clientType ? clientType.toLowerCase() : null;
33
+ }
34
+ function isBrowserRequest(req) {
35
+ const clientType = getClientType(req);
36
+ if (clientType) {
37
+ return clientType === 'browser' || clientType === 'web';
38
+ }
39
+ const accept = req.headers['accept'] || '';
40
+ if (accept.includes('text/html')) return true;
41
+ const userAgent = req.headers['user-agent'] || '';
42
+ const browserPatterns = /mozilla|chrome|safari|firefox|edge|opera|msie/i;
43
+ return browserPatterns.test(userAgent) && !userAgent.includes('Postman');
44
+ }
45
+ function buildCookieOptions(req) {
46
+ const hostname = req.hostname || '';
47
+ const origin = req.headers.origin || '';
48
+ const isProduction = process.env.NODE_ENV === 'production';
49
+ const forwardedProto = req.headers['x-forwarded-proto'];
50
+ const isHttps = isProduction || forwardedProto === 'https' || origin.startsWith('https://') || req.secure;
51
+ let domain;
52
+ const domainParts = hostname.split('.');
53
+ if (domainParts.length > 2 && !hostname.includes('localhost')) {
54
+ domain = '.' + domainParts.slice(-2).join('.');
55
+ }
56
+ return {
57
+ secure: isHttps,
58
+ sameSite: isHttps ? 'strict' : 'lax',
59
+ ...domain && {
60
+ domain
61
+ }
62
+ };
63
+ }
64
+ function parseDurationToMs(duration, defaultMs = TIME_UNIT_MS.w) {
65
+ const match = duration.match(/^(\d+)(s|m|h|d|w)$/);
66
+ if (!match) return defaultMs;
67
+ const value = parseInt(match[1], 10);
68
+ const unit = match[2];
69
+ return value * (TIME_UNIT_MS[unit] ?? TIME_UNIT_MS.d);
70
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * String utility functions
3
+ */ /**
4
+ * Generate URL-friendly slug from a string
5
+ * @param text - The text to convert to slug
6
+ * @param maxLength - Maximum length of the slug (default: 100)
7
+ * @returns URL-friendly slug
8
+ *
9
+ * @example
10
+ * generateSlug('My Company Name') // 'my-company-name'
11
+ * generateSlug('Hello World!') // 'hello-world'
12
+ */ "use strict";
13
+ Object.defineProperty(exports, "__esModule", {
14
+ value: true
15
+ });
16
+ function _export(target, all) {
17
+ for(var name in all)Object.defineProperty(target, name, {
18
+ enumerable: true,
19
+ get: Object.getOwnPropertyDescriptor(all, name).get
20
+ });
21
+ }
22
+ _export(exports, {
23
+ get generateSlug () {
24
+ return generateSlug;
25
+ },
26
+ get generateUniqueSlug () {
27
+ return generateUniqueSlug;
28
+ }
29
+ });
30
+ function generateSlug(text, maxLength = 100) {
31
+ return text.toLowerCase().trim().replace(/[^a-z0-9\s-]/g, '') // Remove special characters
32
+ .replace(/\s+/g, '-') // Replace spaces with hyphens
33
+ .replace(/-+/g, '-') // Replace multiple hyphens with single
34
+ .replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
35
+ .substring(0, maxLength);
36
+ }
37
+ async function generateUniqueSlug(text, findMatchingSlugs, maxLength = 100) {
38
+ const baseSlug = generateSlug(text, maxLength);
39
+ // Single query to get all matching slugs
40
+ const existingSlugs = await findMatchingSlugs(baseSlug);
41
+ // No collisions - base slug is available
42
+ if (existingSlugs.length === 0) {
43
+ return baseSlug;
44
+ }
45
+ // Build set for O(1) lookup
46
+ const existingSet = new Set(existingSlugs);
47
+ // Base slug not taken (only suffixed versions exist)
48
+ if (!existingSet.has(baseSlug)) {
49
+ return baseSlug;
50
+ }
51
+ // Find next available suffix in memory (fast)
52
+ let counter = 1;
53
+ while(counter < 10000){
54
+ const slugWithSuffix = `${baseSlug}-${counter}`;
55
+ if (!existingSet.has(slugWithSuffix)) {
56
+ return slugWithSuffix;
57
+ }
58
+ counter++;
59
+ }
60
+ // Fallback: append random string (extremely rare edge case)
61
+ const randomSuffix = Math.random().toString(36).substring(2, 8);
62
+ return `${baseSlug}-${randomSuffix}`;
63
+ }
@@ -1,6 +1,6 @@
1
- import { BulkResponseDto, DeleteDto, FilterAndPaginationDto, GetByIdBodyDto, ListResponseDto, MessageResponseDto, SingleResponseDto } from '@flusys/nestjs-shared/dtos';
2
- import { Identity } from '@flusys/nestjs-shared/entities';
3
- import { ILoggedUserInfo, IService, PermissionCondition, PermissionOperator } from '@flusys/nestjs-shared/interfaces';
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
4
  import { Type } from '@nestjs/common';
5
5
  export type ApiEndpoint = 'insert' | 'insertMany' | 'getById' | 'getAll' | 'update' | 'updateMany' | 'delete';
6
6
  export type SecurityLevel = 'public' | 'jwt' | 'permission';
@@ -16,9 +16,9 @@ export type ApiSecurityConfig = {
16
16
  export interface ApiControllerOptions {
17
17
  security?: ApiSecurityConfig | EndpointSecurity | SecurityLevel;
18
18
  }
19
- export declare function createApiController<CreateDtoT extends Record<string, unknown>, UpdateDtoT extends {
19
+ export declare function createApiController<CreateDtoT extends object, UpdateDtoT extends {
20
20
  id: string;
21
- }, ResponseDtoT extends Record<string, unknown>, InterfaceT extends Identity, ServiceT extends IService<CreateDtoT, UpdateDtoT, InterfaceT>>(createDtoClass: Type<CreateDtoT>, updateDtoClass: Type<UpdateDtoT>, responseDtoClass: Type<ResponseDtoT>, options?: ApiControllerOptions): abstract new (service: ServiceT) => {
21
+ }, 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) => {
22
22
  service: ServiceT;
23
23
  insert(addDto: CreateDtoT, user: ILoggedUserInfo | null): Promise<SingleResponseDto<ResponseDtoT>>;
24
24
  insertMany(addDto: CreateDtoT[], user: ILoggedUserInfo | null): Promise<BulkResponseDto<ResponseDtoT>>;