@flusys/nestjs-shared 9.1.1 → 9.2.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 (46) hide show
  1. package/README.md +119 -18
  2. package/classes/api-controller.class.d.ts +1 -0
  3. package/classes/api-service.class.d.ts +7 -2
  4. package/classes/hybrid-cache.class.d.ts +17 -4
  5. package/constants/index.d.ts +1 -0
  6. package/constants/permissions.d.ts +9 -0
  7. package/fesm/183.js +7 -7
  8. package/fesm/47.js +1520 -0
  9. package/fesm/470.js +333 -382
  10. package/fesm/667.js +17 -17
  11. package/fesm/681.js +326 -129
  12. package/fesm/{948.js → 997.js} +45 -23
  13. package/fesm/chunks/123.js +17 -9
  14. package/fesm/chunks/37.js +5 -5
  15. package/fesm/classes/index.js +4 -3
  16. package/fesm/constants/index.js +18 -7
  17. package/fesm/decorators/index.js +120 -39
  18. package/fesm/dtos/index.js +1 -1
  19. package/fesm/entities/index.js +8 -8
  20. package/fesm/enums/index.js +2 -2
  21. package/fesm/exceptions/index.js +12 -12
  22. package/fesm/guards/index.js +201 -68
  23. package/fesm/index.js +95 -42
  24. package/fesm/interceptors/index.js +290 -590
  25. package/fesm/interfaces/index.js +97 -8
  26. package/fesm/middlewares/index.js +4 -4
  27. package/fesm/modules/index.js +2 -2
  28. package/fesm/pipes/index.js +8 -8
  29. package/fesm/utils/index.js +150 -478
  30. package/interceptors/idempotency.interceptor.d.ts +14 -2
  31. package/interceptors/slug.interceptor.d.ts +1 -0
  32. package/interfaces/expression.interface.d.ts +45 -0
  33. package/interfaces/index.d.ts +1 -0
  34. package/interfaces/permission-action-registry.interface.d.ts +1 -0
  35. package/interfaces/permission.interface.d.ts +1 -0
  36. package/modules/shared-permission-cache/shared-permission-cache.service.d.ts +3 -1
  37. package/modules/utils/utils.service.d.ts +6 -6
  38. package/package.json +3 -2
  39. package/utils/date-time.util.d.ts +0 -1
  40. package/utils/event-pattern.util.d.ts +1 -0
  41. package/utils/expression.util.d.ts +35 -0
  42. package/utils/index.d.ts +2 -0
  43. package/utils/permission-cache-key.util.d.ts +3 -0
  44. package/utils/query-helpers.util.d.ts +2 -2
  45. package/utils/safe-regex.util.d.ts +10 -0
  46. package/fesm/270.js +0 -659
@@ -1,5 +1,3 @@
1
- import * as __rspack_external__flusys_nestjs_core_config_197fa0a9 from "@flusys/nestjs-core/config";
2
- import * as __rspack_external__nestjs_common_a04939da from "@nestjs/common";
3
1
  import * as __rspack_external_crypto from "crypto";
4
2
  import * as __rspack_external__keyv_redis_139d03ca from "@keyv/redis";
5
3
  import * as __rspack_external_cacheable from "cacheable";
@@ -9,7 +7,7 @@ import * as __rspack_external_rxjs_operators_2eaa0997 from "rxjs/operators";
9
7
  export const __rspack_esm_id = 971;
10
8
  export const __rspack_esm_ids = [971];
11
9
  export const __webpack_modules__ = {
12
- 375(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
10
+ 5375(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
13
11
 
14
12
  // EXPORTS
15
13
  __webpack_require__.d(__webpack_exports__, {
@@ -17,25 +15,41 @@ __webpack_require__.d(__webpack_exports__, {
17
15
  });
18
16
 
19
17
  // EXTERNAL MODULE: external "@flusys/nestjs-core/config"
20
- var config_ = __webpack_require__(307);
18
+ var config_ = __webpack_require__(8307);
21
19
  ;// CONCATENATED MODULE: external "@keyv/redis"
22
20
 
23
21
  ;// CONCATENATED MODULE: external "cacheable"
24
22
 
25
23
  ;// CONCATENATED MODULE: external "keyv"
26
24
 
25
+ // EXTERNAL MODULE: external "node:crypto"
26
+ var external_node_crypto_ = __webpack_require__(2561);
27
27
  ;// CONCATENATED MODULE: ./projects/nestjs-shared/src/classes/hybrid-cache.class.ts
28
28
 
29
29
 
30
30
 
31
31
 
32
- class HybridCache {
32
+
33
+ const RAW_KEY_PREFIX = 'flusys';
34
+ const STAMP_TTL_MS = 30 * 24 * 60 * 60 * 1000;
35
+ /**
36
+ * L1 in-process memory + L2 Redis, chosen by `USE_CACHE_LABEL` (memory | redis | hybrid).
37
+ *
38
+ * Plain `get`/`set` may be served by L1, which other instances cannot invalidate, so a value
39
+ * read that way must be immutable for its key - which is what version stamps give: data keys
40
+ * embed the current stamp, and invalidation moves the stamp instead of deleting entries.
41
+ * Stamps and claims live outside Keyv so they can be set atomically (Redis `SET NX`).
42
+ */ class HybridCache {
43
+ mode;
44
+ defaultTtl;
33
45
  memory;
34
46
  redis;
47
+ redisStore;
48
+ localAtomic;
35
49
  constructor(memoryTtl = 60 * 1000, memorySize = 5000){
36
- // Default to 'memory' if not specified
37
- const useCacheLabel = config_.envConfig.tryGetValue('USE_CACHE_LABEL', false) || 'memory';
38
- if (useCacheLabel === 'memory' || useCacheLabel === 'hybrid') {
50
+ this.mode = config_.envConfig.tryGetValue('USE_CACHE_LABEL', false) || 'memory';
51
+ this.defaultTtl = memoryTtl;
52
+ if (this.mode === 'memory' || this.mode === 'hybrid') {
39
53
  this.memory = new __rspack_external_keyv["default"]({
40
54
  store: new __rspack_external_cacheable.CacheableMemory({
41
55
  ttl: memoryTtl,
@@ -43,35 +57,51 @@ class HybridCache {
43
57
  })
44
58
  });
45
59
  }
46
- if (useCacheLabel === 'redis' || useCacheLabel === 'hybrid') {
60
+ if (this.mode === 'redis' || this.mode === 'hybrid') {
61
+ this.redisStore = new __rspack_external__keyv_redis_139d03ca["default"](config_.envConfig.getRedisUrl());
47
62
  this.redis = new __rspack_external_keyv["default"]({
48
- store: new __rspack_external__keyv_redis_139d03ca["default"](config_.envConfig.getRedisUrl())
63
+ store: this.redisStore
64
+ });
65
+ } else {
66
+ // Stamps and claims must never be LRU-evicted while still referenced
67
+ this.localAtomic = new __rspack_external_cacheable.CacheableMemory({
68
+ ttl: STAMP_TTL_MS,
69
+ lruSize: 0,
70
+ checkInterval: 60_000
49
71
  });
50
72
  }
51
73
  }
52
- async get(key) {
53
- // Prefer memory first if available
54
- if (this.memory) {
74
+ getMode() {
75
+ return this.mode;
76
+ }
77
+ async get(key, options = {}) {
78
+ const useMemory = this.memory && !(options.skipMemory && this.redis);
79
+ if (useMemory) {
55
80
  const value = await this.memory.get(key);
56
81
  if (value !== undefined) return value;
57
82
  }
58
- // Fallback to redis
59
83
  if (this.redis) {
60
- const value = await this.redis.get(key);
61
- if (value !== undefined && this.memory) {
62
- // backfill L1 memory cache
63
- await this.memory.set(key, value);
84
+ const raw = await this.redis.get(key, {
85
+ raw: true
86
+ });
87
+ if (raw === undefined) return undefined;
88
+ if (useMemory) {
89
+ // Never let the L1 copy outlive the Redis entry (time-bound values such as permissions)
90
+ const remaining = raw.expires ? raw.expires - Date.now() : this.defaultTtl;
91
+ if (remaining > 0) await this.memory.set(key, raw.value, Math.min(remaining, this.defaultTtl));
64
92
  }
65
- return value;
93
+ return raw.value;
66
94
  }
67
95
  return undefined;
68
96
  }
69
- async set(key, value, ttl) {
70
- if (this.memory) {
71
- await this.memory.set(key, value, ttl);
97
+ async set(key, value, ttl, options = {}) {
98
+ const effectiveTtl = ttl ?? this.defaultTtl;
99
+ const useMemory = this.memory && !(options.skipMemory && this.redis);
100
+ if (useMemory) {
101
+ await this.memory.set(key, value, effectiveTtl);
72
102
  }
73
103
  if (this.redis) {
74
- await this.redis.set(key, value, ttl);
104
+ await this.redis.set(key, value, effectiveTtl);
75
105
  }
76
106
  }
77
107
  async del(key) {
@@ -82,460 +112,86 @@ class HybridCache {
82
112
  await this.redis.delete(key);
83
113
  }
84
114
  }
85
- async reset() {
86
- if (this.memory) {
87
- await this.memory.clear();
115
+ /**
116
+ * The current version stamp for `name`, creating one when absent. A missing stamp is always
117
+ * replaced by a fresh random one - never a fixed default - so entries cached under a stamp that
118
+ * expired or was lost can never become current again.
119
+ */ async getStamp(name) {
120
+ const key = this.rawKey('stamp', name);
121
+ if (this.localAtomic) {
122
+ let stamp = this.localAtomic.get(key);
123
+ if (!stamp) {
124
+ stamp = (0,external_node_crypto_.randomUUID)();
125
+ this.localAtomic.set(key, stamp);
126
+ }
127
+ return stamp;
88
128
  }
129
+ const client = await this.redisStore.getClient();
130
+ const existing = await client.get(key);
131
+ if (existing) return String(existing);
132
+ const candidate = (0,external_node_crypto_.randomUUID)();
133
+ await client.set(key, candidate, {
134
+ NX: true,
135
+ PX: STAMP_TTL_MS
136
+ });
137
+ // A key evicted between SET NX and GET must not turn into the fixed stamp "null"
138
+ return String(await client.get(key) ?? candidate);
89
139
  }
90
- async resetL2() {
91
- if (this.redis) {
92
- await this.redis.clear();
140
+ /** Moves the stamp for `name`, orphaning every entry cached under the previous one on all instances */ async bumpStamp(name) {
141
+ const key = this.rawKey('stamp', name);
142
+ if (this.localAtomic) {
143
+ this.localAtomic.set(key, (0,external_node_crypto_.randomUUID)());
144
+ return;
145
+ }
146
+ const client = await this.redisStore.getClient();
147
+ await client.set(key, (0,external_node_crypto_.randomUUID)(), {
148
+ PX: STAMP_TTL_MS
149
+ });
150
+ }
151
+ /** Atomically claims `name` for `ttl` ms; true only for the first caller while the claim lives */ async claim(name, ttl) {
152
+ const key = this.rawKey('claim', name);
153
+ if (this.localAtomic) {
154
+ if (this.localAtomic.has(key)) return false;
155
+ this.localAtomic.set(key, true, ttl);
156
+ return true;
157
+ }
158
+ const client = await this.redisStore.getClient();
159
+ return await client.set(key, '1', {
160
+ NX: true,
161
+ PX: ttl
162
+ }) !== null;
163
+ }
164
+ async releaseClaim(name) {
165
+ const key = this.rawKey('claim', name);
166
+ if (this.localAtomic) {
167
+ this.localAtomic.delete(key);
168
+ return;
93
169
  }
170
+ const client = await this.redisStore.getClient();
171
+ await client.del(key);
172
+ }
173
+ /**
174
+ * Reads and removes a one-time value; only one caller across all instances ever receives it.
175
+ * The claim is per value, so a fresh value later stored under the same key (a resent code)
176
+ * can be taken again while the claim on the consumed one is still alive.
177
+ */ async take(key, ttl) {
178
+ const value = await this.get(key, {
179
+ skipMemory: true
180
+ });
181
+ if (value === undefined) return undefined;
182
+ const digest = (0,external_node_crypto_.createHash)('sha256').update(JSON.stringify(value) ?? '').digest('hex');
183
+ if (!await this.claim(`take:${key}:${digest}`, ttl)) return undefined;
184
+ await this.del(key);
185
+ return value;
186
+ }
187
+ rawKey(kind, name) {
188
+ return `${RAW_KEY_PREFIX}:${kind}:${name}`;
94
189
  }
95
190
  }
96
191
 
97
192
 
98
193
  },
99
- 43(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
100
- /* import */ var _permissions_js__rspack_import_0 = __webpack_require__(975);
101
- /* import */ var _message_keys_js__rspack_import_1 = __webpack_require__(389);
102
- // Metadata keys
103
- const IS_PUBLIC_KEY = 'isPublic';
104
- const PERMISSIONS_KEY = 'permissions';
105
- const DOMAIN_EVENT_HANDLER_KEY = 'domainEventHandler';
106
- // Injection tokens
107
- const CACHE_INSTANCE = 'CACHE_INSTANCE';
108
- const PERMISSION_GUARD_CONFIG = 'PERMISSION_GUARD_CONFIG';
109
- const EVENT_BUS_INSTANCE = 'EVENT_BUS_INSTANCE';
110
- const EVENT_BUS_MODULE_OPTIONS = 'EVENT_BUS_MODULE_OPTIONS';
111
- // Header names
112
- const IDEMPOTENCY_KEY_HEADER = 'x-idempotency-key';
113
- const REQUEST_ID_HEADER = 'x-request-id';
114
- const CLIENT_TYPE_HEADER = 'x-client-type';
115
- // Cache key prefixes
116
- const PERMISSIONS_CACHE_PREFIX = 'my-permissions';
117
- const IDEMPOTENCY_CACHE_PREFIX = 'idempotency';
118
- // Event bus defaults
119
- const DEFAULT_EVENT_EXCHANGE = 'flusys.events';
120
- const DEFAULT_EVENT_TOPIC = 'flusys.domain-events';
121
- const DEFAULT_EVENT_SERVICE_NAME = 'flusys';
122
- // Permission codes
123
-
124
- // Message keys for localization
125
-
126
-
127
- __webpack_require__.d(__webpack_exports__, {
128
- BC: () => (/* reexport safe */ _permissions_js__rspack_import_0.BC),
129
- Bz: () => (/* reexport safe */ _permissions_js__rspack_import_0.Bz),
130
- DP: () => (/* reexport safe */ _permissions_js__rspack_import_0.DP),
131
- Em: () => (/* reexport safe */ _permissions_js__rspack_import_0.Em),
132
- G0: () => (/* reexport safe */ _permissions_js__rspack_import_0.G0),
133
- GH: () => (/* reexport safe */ _permissions_js__rspack_import_0.GH),
134
- GT: () => (/* reexport safe */ _permissions_js__rspack_import_0.GT),
135
- Jj: () => (/* reexport safe */ _permissions_js__rspack_import_0.Jj),
136
- N3: () => (/* reexport safe */ _permissions_js__rspack_import_0.N3),
137
- ON: () => (/* reexport safe */ _permissions_js__rspack_import_0.ON),
138
- P0: () => (/* reexport safe */ _permissions_js__rspack_import_0.P0),
139
- Q5: () => (/* reexport safe */ _permissions_js__rspack_import_0.Q5),
140
- S6: () => (/* reexport safe */ _permissions_js__rspack_import_0.S6),
141
- T: () => (/* reexport safe */ _permissions_js__rspack_import_0.T),
142
- UB: () => (/* reexport safe */ _permissions_js__rspack_import_0.UB),
143
- UG: () => (/* reexport safe */ _permissions_js__rspack_import_0.UG),
144
- UU: () => (/* reexport safe */ _message_keys_js__rspack_import_1.UU),
145
- Ub: () => (/* reexport safe */ _permissions_js__rspack_import_0.Ub),
146
- V0: () => (/* reexport safe */ _permissions_js__rspack_import_0.V0),
147
- Wc: () => (/* reexport safe */ _permissions_js__rspack_import_0.Wc),
148
- Y3: () => (/* reexport safe */ _permissions_js__rspack_import_0.Y3),
149
- Y7: () => (/* reexport safe */ _permissions_js__rspack_import_0.Y7),
150
- _X: () => (/* reexport safe */ _permissions_js__rspack_import_0._X),
151
- aO: () => (/* reexport safe */ _permissions_js__rspack_import_0.aO),
152
- aT: () => (/* reexport safe */ _permissions_js__rspack_import_0.aT),
153
- fS: () => (/* reexport safe */ _permissions_js__rspack_import_0.fS),
154
- hg: () => (/* reexport safe */ _message_keys_js__rspack_import_1.hg),
155
- kA: () => (/* reexport safe */ _permissions_js__rspack_import_0.kA),
156
- kd: () => (/* reexport safe */ _permissions_js__rspack_import_0.kd),
157
- l3: () => (/* reexport safe */ _permissions_js__rspack_import_0.l3),
158
- mK: () => (/* reexport safe */ _message_keys_js__rspack_import_1.mK),
159
- mV: () => (/* reexport safe */ _permissions_js__rspack_import_0.mV),
160
- rH: () => (/* reexport safe */ _permissions_js__rspack_import_0.rH),
161
- t$: () => (/* reexport safe */ _permissions_js__rspack_import_0.t$),
162
- w7: () => (/* reexport safe */ _permissions_js__rspack_import_0.w7),
163
- wA: () => (/* reexport safe */ _permissions_js__rspack_import_0.wA),
164
- wM: () => (/* reexport safe */ _permissions_js__rspack_import_0.wM),
165
- z$: () => (/* reexport safe */ _permissions_js__rspack_import_0.z$),
166
- zd: () => (/* reexport safe */ _permissions_js__rspack_import_0.zd)
167
- }, {
168
- Hq: DEFAULT_EVENT_EXCHANGE,
169
- Lw: PERMISSION_GUARD_CONFIG,
170
- N7: PERMISSIONS_KEY,
171
- PT: DOMAIN_EVENT_HANDLER_KEY,
172
- Qj: EVENT_BUS_INSTANCE,
173
- SL: EVENT_BUS_MODULE_OPTIONS,
174
- fG: PERMISSIONS_CACHE_PREFIX,
175
- iH: IDEMPOTENCY_KEY_HEADER,
176
- jA: IS_PUBLIC_KEY,
177
- kC: REQUEST_ID_HEADER,
178
- mg: IDEMPOTENCY_CACHE_PREFIX,
179
- nx: DEFAULT_EVENT_TOPIC,
180
- xw: CLIENT_TYPE_HEADER,
181
- zl: CACHE_INSTANCE
182
- });
183
-
184
-
185
- },
186
- 389(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
187
- // ==================== AUTH (Shared across guards/interceptors) ====================
188
- // These are duplicated in nestjs-auth but needed here to avoid circular dependencies
189
- const AUTH_MESSAGES = {
190
- TOKEN_REQUIRED: 'auth.token.required',
191
- TOKEN_INVALID: 'auth.token.invalid',
192
- ENTITY_BELOG_ANOTHER_COMPANY: 'entity.belongs.another.company',
193
- COMPANY_NO_ACCESS: 'auth.company.no.access'
194
- };
195
- // ==================== ERROR (HTTP Exceptions) ====================
196
- const ERROR_MESSAGES = {
197
- NOT_FOUND: 'error.not.found',
198
- VALIDATION: 'error.validation',
199
- UNAUTHORIZED: 'error.unauthorized',
200
- FORBIDDEN: 'error.forbidden',
201
- CONFLICT: 'error.conflict',
202
- DB_FOREIGN_KEY: 'error.db.foreign.key',
203
- DB_NOT_NULL: 'error.db.not.null',
204
- DB_INVALID_INPUT: 'error.db.invalid.input',
205
- DB_VALUE_TOO_LONG: 'error.db.value.too.long',
206
- DB_CHECK_CONSTRAINT: 'error.db.check.constraint',
207
- DB_DEADLOCK: 'error.db.deadlock',
208
- INTERNAL: 'error.internal',
209
- SERVICE_UNAVAILABLE: 'error.service.unavailable',
210
- UNKNOWN: 'error.unknown',
211
- HTTP: 'error.http',
212
- GENERIC: 'error.generic',
213
- PERMISSION_SYSTEM_UNAVAILABLE: 'error.permission.system.unavailable',
214
- INSUFFICIENT_PERMISSIONS: 'error.insufficient.permissions',
215
- INSUFFICIENT_PERMISSIONS_OR: 'error.insufficient.permissions.or',
216
- NO_PERMISSIONS_FOUND: 'error.no.permissions.found',
217
- ENDPOINT_DISABLED: 'error.endpoint.disabled'
218
- };
219
- // ==================== SYSTEM (Infrastructure) ====================
220
- const SYSTEM_MESSAGES = {
221
- REPOSITORY_NOT_AVAILABLE: 'system.repository.not.available',
222
- DATASOURCE_NOT_AVAILABLE: 'system.datasource.not.available',
223
- DATABASE_CONFIG_NOT_AVAILABLE: 'system.database.config.not.available',
224
- SERVICE_NOT_AVAILABLE: 'system.service.not.available',
225
- CONFIG_REQUIRED: 'system.config.required',
226
- INTERNAL_ERROR: 'system.internal.error',
227
- DUPLICATE_REQUEST: 'system.duplicate.request',
228
- INVALID_TENANT_ID: 'system.invalid.tenant.id',
229
- TENANT_NOT_FOUND: 'system.tenant.not.found',
230
- TENANT_HEADER_REQUIRED: 'system.tenant.header.required',
231
- MISSING_PARAMETER: 'system.missing.parameter',
232
- SDK_NOT_INSTALLED: 'system.sdk.not.installed',
233
- PATH_TRAVERSAL_DETECTED: 'system.path.traversal.detected',
234
- INVALID_FILE_KEY: 'system.invalid.file.key'
235
- };
236
-
237
- __webpack_require__.d(__webpack_exports__, {
238
- }, {
239
- UU: ERROR_MESSAGES,
240
- hg: SYSTEM_MESSAGES,
241
- mK: AUTH_MESSAGES
242
- });
243
-
244
-
245
- },
246
- 975(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
247
- // ==================== AUTH MODULE ====================
248
- const USER_PERMISSIONS = {
249
- CREATE: 'user.create',
250
- READ: 'user.read',
251
- UPDATE: 'user.update',
252
- DELETE: 'user.delete',
253
- LOOKUP: 'user.lookup'
254
- };
255
- const COMPANY_PERMISSIONS = {
256
- CREATE: 'company.create',
257
- READ: 'company.read',
258
- UPDATE: 'company.update',
259
- DELETE: 'company.delete'
260
- };
261
- const BRANCH_PERMISSIONS = {
262
- CREATE: 'branch.create',
263
- READ: 'branch.read',
264
- UPDATE: 'branch.update',
265
- DELETE: 'branch.delete'
266
- };
267
- const SOCIAL_AUTH_CONFIG_PERMISSIONS = {
268
- CREATE: 'social-config.create',
269
- READ: 'social-config.read',
270
- UPDATE: 'social-config.update',
271
- DELETE: 'social-config.delete'
272
- };
273
- // ==================== IAM MODULE ====================
274
- const ACTION_PERMISSIONS = {
275
- CREATE: 'action.create',
276
- READ: 'action.read',
277
- UPDATE: 'action.update',
278
- DELETE: 'action.delete'
279
- };
280
- const ROLE_PERMISSIONS = {
281
- CREATE: 'role.create',
282
- READ: 'role.read',
283
- UPDATE: 'role.update',
284
- DELETE: 'role.delete'
285
- };
286
- const ROLE_ACTION_PERMISSIONS = {
287
- READ: 'role-action.read',
288
- ASSIGN: 'role-action.assign'
289
- };
290
- const USER_ROLE_PERMISSIONS = {
291
- READ: 'user-role.read',
292
- ASSIGN: 'user-role.assign'
293
- };
294
- const USER_ACTION_PERMISSIONS = {
295
- READ: 'user-action.read',
296
- ASSIGN: 'user-action.assign'
297
- };
298
- const COMPANY_ACTION_PERMISSIONS = {
299
- READ: 'company-action.read',
300
- ASSIGN: 'company-action.assign'
301
- };
302
- // ==================== STORAGE MODULE ====================
303
- const FILE_PERMISSIONS = {
304
- CREATE: 'file.create',
305
- READ: 'file.read',
306
- UPDATE: 'file.update',
307
- DELETE: 'file.delete',
308
- VIEW_PRIVATE_ACTION: 'file.view.private'
309
- };
310
- const FOLDER_PERMISSIONS = {
311
- CREATE: 'folder.create',
312
- READ: 'folder.read',
313
- UPDATE: 'folder.update',
314
- DELETE: 'folder.delete'
315
- };
316
- const STORAGE_CONFIG_PERMISSIONS = {
317
- CREATE: 'storage-config.create',
318
- READ: 'storage-config.read',
319
- UPDATE: 'storage-config.update',
320
- DELETE: 'storage-config.delete'
321
- };
322
- const UPLOAD_CONFIG_PERMISSIONS = {
323
- DELETE: 'upload-config.delete'
324
- };
325
- // ==================== EMAIL MODULE ====================
326
- const EMAIL_CONFIG_PERMISSIONS = {
327
- CREATE: 'email-config.create',
328
- READ: 'email-config.read',
329
- UPDATE: 'email-config.update',
330
- DELETE: 'email-config.delete'
331
- };
332
- const EMAIL_TEMPLATE_PERMISSIONS = {
333
- CREATE: 'email-template.create',
334
- READ: 'email-template.read',
335
- UPDATE: 'email-template.update',
336
- DELETE: 'email-template.delete'
337
- };
338
- // ==================== FORM BUILDER MODULE ====================
339
- const FORM_PERMISSIONS = {
340
- CREATE: 'form.create',
341
- READ: 'form.read',
342
- UPDATE: 'form.update',
343
- DELETE: 'form.delete'
344
- };
345
- const FORM_RESULT_PERMISSIONS = {
346
- CREATE: 'form-result.create',
347
- READ: 'form-result.read',
348
- UPDATE: 'form-result.update',
349
- DELETE: 'form-result.delete'
350
- };
351
- // ==================== ENTITY BUILDER MODULE ====================
352
- // Static metadata permissions only. Per-dynamic-entity record permissions
353
- // (`entity_builder.entity.<entityCode>.<action>`) are checked by flows running as the caller;
354
- // ENTITY_BUILDER_ALL_DYNAMIC is the wildcard that grants all of them (and nothing else).
355
- const ENTITY_DEFINITION_PERMISSIONS = {
356
- CREATE: 'entity_builder.entity_definition.create',
357
- READ: 'entity_builder.entity_definition.read',
358
- UPDATE: 'entity_builder.entity_definition.update',
359
- DELETE: 'entity_builder.entity_definition.delete'
360
- };
361
- const FIELD_DEFINITION_PERMISSIONS = {
362
- READ: 'entity_builder.field_definition.read',
363
- UPDATE: 'entity_builder.field_definition.update'
364
- };
365
- const FLOW_DEFINITION_PERMISSIONS = {
366
- CREATE: 'entity_builder.flow_definition.create',
367
- READ: 'entity_builder.flow_definition.read',
368
- UPDATE: 'entity_builder.flow_definition.update',
369
- DELETE: 'entity_builder.flow_definition.delete',
370
- TEST: 'entity_builder.flow_definition.test',
371
- /** Needed to save a flow containing a code node (sandboxed JavaScript). */ CODE: 'entity_builder.flow_definition.code'
372
- };
373
- const ENTITY_BUILDER_DYNAMIC_PREFIX = 'entity_builder.entity';
374
- const ENTITY_BUILDER_ALL_DYNAMIC = {
375
- ALL: `${ENTITY_BUILDER_DYNAMIC_PREFIX}.*`
376
- };
377
- const dynamicEntityPermissionParent = (entityCode)=>`${ENTITY_BUILDER_DYNAMIC_PREFIX}.${entityCode}`;
378
- const dynamicEntityPermission = (entityCode, action)=>`${dynamicEntityPermissionParent(entityCode)}.${action}`;
379
- // ==================== EVENT MANAGER MODULE ====================
380
- const EVENT_PERMISSIONS = {
381
- CREATE: 'event.create',
382
- READ: 'event.read',
383
- UPDATE: 'event.update',
384
- DELETE: 'event.delete'
385
- };
386
- // ==================== NOTIFICATION MODULE ====================
387
- const NOTIFICATION_PERMISSIONS = {
388
- CREATE: 'notification.create',
389
- READ: 'notification.read',
390
- UPDATE: 'notification.update',
391
- DELETE: 'notification.delete'
392
- };
393
- // ==================== LOCALIZATION MODULE ====================
394
- const LANGUAGE_PERMISSIONS = {
395
- CREATE: 'language.create',
396
- READ: 'language.read',
397
- UPDATE: 'language.update',
398
- DELETE: 'language.delete'
399
- };
400
- const TRANSLATION_KEY_PERMISSIONS = {
401
- CREATE: 'translation-key.create',
402
- READ: 'translation-key.read',
403
- UPDATE: 'translation-key.update',
404
- DELETE: 'translation-key.delete'
405
- };
406
- const TRANSLATION_PERMISSIONS = {
407
- CREATE: 'translation.create',
408
- READ: 'translation.read',
409
- UPDATE: 'translation.update',
410
- DELETE: 'translation.delete'
411
- };
412
- // ==================== TASK MANAGER MODULE ====================
413
- const TASK_BOARD_PERMISSIONS = {
414
- CREATE: 'task-board.create',
415
- READ: 'task-board.read',
416
- UPDATE: 'task-board.update',
417
- DELETE: 'task-board.delete',
418
- MANAGE_MEMBERS: 'task-board.manage-members'
419
- };
420
- const TASK_LIST_PERMISSIONS = {
421
- CREATE: 'task-list.create',
422
- READ: 'task-list.read',
423
- UPDATE: 'task-list.update',
424
- DELETE: 'task-list.delete'
425
- };
426
- const TASK_PERMISSIONS = {
427
- CREATE: 'task.create',
428
- READ: 'task.read',
429
- UPDATE: 'task.update',
430
- DELETE: 'task.delete',
431
- MOVE: 'task.move',
432
- ASSIGN: 'task.assign'
433
- };
434
- const TASK_LABEL_PERMISSIONS = {
435
- CREATE: 'task-label.create',
436
- READ: 'task-label.read',
437
- UPDATE: 'task-label.update',
438
- DELETE: 'task-label.delete'
439
- };
440
- const TASK_COMMENT_PERMISSIONS = {
441
- CREATE: 'task-comment.create',
442
- READ: 'task-comment.read',
443
- DELETE: 'task-comment.delete'
444
- };
445
- // ==================== AI ASSISTANT MODULE ====================
446
- const AI_ASSISTANT_PERMISSIONS = {
447
- VIEW_ALL_HISTORY: 'ai-assistant.view-all-history'
448
- };
449
- // ==================== AGGREGATED EXPORTS ====================
450
- const PERMISSIONS = {
451
- // Auth
452
- USER: USER_PERMISSIONS,
453
- COMPANY: COMPANY_PERMISSIONS,
454
- BRANCH: BRANCH_PERMISSIONS,
455
- SOCIAL_AUTH_CONFIG: SOCIAL_AUTH_CONFIG_PERMISSIONS,
456
- // IAM
457
- ACTION: ACTION_PERMISSIONS,
458
- ROLE: ROLE_PERMISSIONS,
459
- ROLE_ACTION: ROLE_ACTION_PERMISSIONS,
460
- USER_ROLE: USER_ROLE_PERMISSIONS,
461
- USER_ACTION: USER_ACTION_PERMISSIONS,
462
- COMPANY_ACTION: COMPANY_ACTION_PERMISSIONS,
463
- // Storage
464
- FILE: FILE_PERMISSIONS,
465
- FOLDER: FOLDER_PERMISSIONS,
466
- STORAGE_CONFIG: STORAGE_CONFIG_PERMISSIONS,
467
- // Email
468
- EMAIL_CONFIG: EMAIL_CONFIG_PERMISSIONS,
469
- EMAIL_TEMPLATE: EMAIL_TEMPLATE_PERMISSIONS,
470
- // Form Builder
471
- FORM: FORM_PERMISSIONS,
472
- FORM_RESULT: FORM_RESULT_PERMISSIONS,
473
- // Entity Builder
474
- ENTITY_DEFINITION: ENTITY_DEFINITION_PERMISSIONS,
475
- FIELD_DEFINITION: FIELD_DEFINITION_PERMISSIONS,
476
- FLOW_DEFINITION: FLOW_DEFINITION_PERMISSIONS,
477
- ENTITY_BUILDER_ALL_DYNAMIC: ENTITY_BUILDER_ALL_DYNAMIC,
478
- // Event Manager
479
- EVENT: EVENT_PERMISSIONS,
480
- // Notification
481
- NOTIFICATION: NOTIFICATION_PERMISSIONS,
482
- // Localization
483
- LANGUAGE: LANGUAGE_PERMISSIONS,
484
- TRANSLATION_KEY: TRANSLATION_KEY_PERMISSIONS,
485
- TRANSLATION: TRANSLATION_PERMISSIONS,
486
- // Task Manager
487
- TASK_BOARD: TASK_BOARD_PERMISSIONS,
488
- TASK_LIST: TASK_LIST_PERMISSIONS,
489
- TASK: TASK_PERMISSIONS,
490
- TASK_LABEL: TASK_LABEL_PERMISSIONS,
491
- TASK_COMMENT: TASK_COMMENT_PERMISSIONS,
492
- // AI Assistant
493
- AI_ASSISTANT: AI_ASSISTANT_PERMISSIONS
494
- };
495
-
496
- __webpack_require__.d(__webpack_exports__, {
497
- }, {
498
- BC: USER_PERMISSIONS,
499
- Bz: AI_ASSISTANT_PERMISSIONS,
500
- DP: ENTITY_DEFINITION_PERMISSIONS,
501
- Em: STORAGE_CONFIG_PERMISSIONS,
502
- G0: ENTITY_BUILDER_ALL_DYNAMIC,
503
- GH: dynamicEntityPermissionParent,
504
- GT: FORM_RESULT_PERMISSIONS,
505
- Jj: PERMISSIONS,
506
- N3: TRANSLATION_KEY_PERMISSIONS,
507
- ON: TASK_LIST_PERMISSIONS,
508
- P0: ROLE_ACTION_PERMISSIONS,
509
- Q5: LANGUAGE_PERMISSIONS,
510
- S6: FILE_PERMISSIONS,
511
- T: TASK_PERMISSIONS,
512
- UB: UPLOAD_CONFIG_PERMISSIONS,
513
- UG: TASK_LABEL_PERMISSIONS,
514
- Ub: EVENT_PERMISSIONS,
515
- V0: EMAIL_CONFIG_PERMISSIONS,
516
- Wc: FORM_PERMISSIONS,
517
- Y3: TASK_BOARD_PERMISSIONS,
518
- Y7: ACTION_PERMISSIONS,
519
- _X: USER_ACTION_PERMISSIONS,
520
- aO: COMPANY_ACTION_PERMISSIONS,
521
- aT: dynamicEntityPermission,
522
- fS: TRANSLATION_PERMISSIONS,
523
- kA: FIELD_DEFINITION_PERMISSIONS,
524
- kd: FLOW_DEFINITION_PERMISSIONS,
525
- l3: SOCIAL_AUTH_CONFIG_PERMISSIONS,
526
- mV: ROLE_PERMISSIONS,
527
- rH: NOTIFICATION_PERMISSIONS,
528
- t$: COMPANY_PERMISSIONS,
529
- w7: USER_ROLE_PERMISSIONS,
530
- wA: BRANCH_PERMISSIONS,
531
- wM: FOLDER_PERMISSIONS,
532
- z$: EMAIL_TEMPLATE_PERMISSIONS,
533
- zd: TASK_COMMENT_PERMISSIONS
534
- });
535
-
536
-
537
- },
538
- 863(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
194
+ 6863(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
539
195
 
540
196
  // EXPORTS
541
197
  __webpack_require__.d(__webpack_exports__, {
@@ -546,11 +202,12 @@ __webpack_require__.d(__webpack_exports__, {
546
202
  Ul: () => (/* reexport */ SetDeletedByOnBody),
547
203
  _x: () => (/* reexport */ SetUpdateByOnBody),
548
204
  Mw: () => (/* reexport */ Slug),
205
+ NM: () => (/* reexport */ buildIdempotencyCacheKey),
549
206
  BR: () => (/* reexport */ createSetUserFieldInterceptor)
550
207
  });
551
208
 
552
209
  // EXTERNAL MODULE: external "@nestjs/common"
553
- var common_ = __webpack_require__(157);
210
+ var common_ = __webpack_require__(6157);
554
211
  ;// CONCATENATED MODULE: ./projects/nestjs-shared/src/interceptors/delete-empty-id-from-body.interceptor.ts
555
212
  function _ts_decorate(decorators, target, key, desc) {
556
213
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -569,19 +226,13 @@ function _ts_decorate(decorators, target, key, desc) {
569
226
  class DeleteEmptyIdFromBodyInterceptor {
570
227
  intercept(context, next) {
571
228
  const request = context.switchToHttp().getRequest();
572
- if (Array.isArray(request.body)) {
573
- request.body = request.body.map((item)=>{
574
- if (!item?.id || item?.id === '' || item?.id === null) {
575
- delete item.id;
576
- }
577
- return item;
578
- });
579
- } else if (typeof request.body === 'object' && request.body !== null) {
580
- const item = request.body;
581
- if (!item?.id || item?.id === '' || item?.id === null) {
582
- delete item.id;
229
+ const items = Array.isArray(request.body) ? request.body : [
230
+ request.body
231
+ ];
232
+ for (const item of items){
233
+ if (typeof item === 'object' && item !== null && !item['id']) {
234
+ delete item['id'];
583
235
  }
584
- request.body = item;
585
236
  }
586
237
  return next.handle();
587
238
  }
@@ -595,9 +246,11 @@ DeleteEmptyIdFromBodyInterceptor = _ts_decorate([
595
246
  ;// CONCATENATED MODULE: external "rxjs/operators"
596
247
 
597
248
  // EXTERNAL MODULE: ./projects/nestjs-shared/src/classes/hybrid-cache.class.ts + 3 modules
598
- var hybrid_cache_class = __webpack_require__(375);
249
+ var hybrid_cache_class = __webpack_require__(5375);
599
250
  // EXTERNAL MODULE: ./projects/nestjs-shared/src/constants/index.ts
600
- var constants = __webpack_require__(43);
251
+ var constants = __webpack_require__(6043);
252
+ // EXTERNAL MODULE: ./projects/nestjs-shared/src/middlewares/logger.middleware.ts + 1 modules
253
+ var logger_middleware = __webpack_require__(8244);
601
254
  ;// CONCATENATED MODULE: ./projects/nestjs-shared/src/interceptors/idempotency.interceptor.ts
602
255
  function idempotency_interceptor_ts_decorate(decorators, target, key, desc) {
603
256
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -627,47 +280,54 @@ function _ts_param(paramIndex, decorator) {
627
280
 
628
281
 
629
282
 
283
+
284
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
285
+ /** Bounds how long a crashed instance can hold a key; no request runs this long */ const PROCESSING_TTL_MS = 10 * 60 * 1000;
286
+ /**
287
+ * Replays the stored response for a repeated `X-Idempotency-Key`. A key only ever replays for the
288
+ * same tenant, user and endpoint, and a failed request releases it so the client can retry.
289
+ */ function buildIdempotencyCacheKey(request, idempotencyKey) {
290
+ const tenant = (0,logger_middleware/* .getTenantId */.EO)() ?? '-';
291
+ const user = request.user?.id ?? 'anonymous';
292
+ const endpoint = `${request.method ?? ''} ${request.route?.path ?? request.originalUrl ?? request.url ?? ''}`;
293
+ return `${constants/* .IDEMPOTENCY_CACHE_PREFIX */.mg}:${tenant}:${user}:${endpoint}:${idempotencyKey}`;
294
+ }
630
295
  class IdempotencyInterceptor {
631
296
  cache;
632
- CACHE_TTL = 24 * 60 * 60 * 1000;
633
- PROCESSING_MARKER = '__PROCESSING__';
634
297
  constructor(cache){
635
298
  this.cache = cache;
636
299
  }
637
300
  async intercept(context, next) {
638
- // Skip if no cache available
639
- if (!this.cache) {
640
- return next.handle();
641
- }
301
+ const cache = this.cache;
302
+ if (!cache) return next.handle();
642
303
  const request = context.switchToHttp().getRequest();
643
304
  const idempotencyKey = request.headers[constants/* .IDEMPOTENCY_KEY_HEADER */.iH];
644
- // Skip if no idempotency key provided
645
- if (!idempotencyKey) {
646
- return next.handle();
305
+ if (!idempotencyKey || typeof idempotencyKey !== 'string') return next.handle();
306
+ const cacheKey = buildIdempotencyCacheKey(request, idempotencyKey);
307
+ const stored = await cache.get(cacheKey, {
308
+ skipMemory: true
309
+ });
310
+ if (stored !== undefined) return (0,__rspack_external_rxjs.of)(stored);
311
+ // Atomic across instances: of two concurrent requests with one key, exactly one runs
312
+ if (!await cache.claim(cacheKey, PROCESSING_TTL_MS)) {
313
+ throw new common_.ConflictException({
314
+ success: false,
315
+ message: 'Request is already being processed',
316
+ messageKey: constants/* .SYSTEM_MESSAGES.DUPLICATE_REQUEST */.hg.DUPLICATE_REQUEST
317
+ });
647
318
  }
648
- const cacheKey = `${constants/* .IDEMPOTENCY_CACHE_PREFIX */.mg}:${idempotencyKey}`;
649
- // Check if request already exists
650
- const cached = await this.cache.get(cacheKey);
651
- if (cached) {
652
- // If still processing, throw conflict
653
- if (cached === this.PROCESSING_MARKER) {
654
- throw new common_.ConflictException({
655
- success: false,
656
- message: 'Request is already being processed',
657
- messageKey: constants/* .SYSTEM_MESSAGES.DUPLICATE_REQUEST */.hg.DUPLICATE_REQUEST
658
- });
659
- }
660
- // Return cached response
661
- return (0,__rspack_external_rxjs.of)(cached);
319
+ const release = ()=>cache.releaseClaim(cacheKey).catch(()=>undefined);
320
+ // The first request may have stored its response and released the claim between our read and claim
321
+ const storedMeanwhile = await cache.get(cacheKey, {
322
+ skipMemory: true
323
+ }).catch(()=>undefined);
324
+ if (storedMeanwhile !== undefined) {
325
+ await release();
326
+ return (0,__rspack_external_rxjs.of)(storedMeanwhile);
662
327
  }
663
- // Mark as processing
664
- const cache = this.cache;
665
- await cache.set(cacheKey, this.PROCESSING_MARKER, this.CACHE_TTL);
666
- // Process request and cache response
667
- return next.handle().pipe((0,__rspack_external_rxjs_operators_2eaa0997.tap)(async (response)=>{
668
- // Cache the successful response
669
- await cache.set(cacheKey, response, this.CACHE_TTL);
670
- }));
328
+ return next.handle().pipe((0,__rspack_external_rxjs_operators_2eaa0997.concatMap)((response)=>(0,__rspack_external_rxjs.from)(cache.set(cacheKey, response, CACHE_TTL_MS, {
329
+ skipMemory: true
330
+ }).catch(()=>undefined).then(release).then(()=>response))), (0,__rspack_external_rxjs_operators_2eaa0997.catchError)((error)=>(0,__rspack_external_rxjs.from)(release()).pipe((0,__rspack_external_rxjs_operators_2eaa0997.concatMap)(()=>(0,__rspack_external_rxjs.throwError)(()=>error)))));
671
331
  }
672
332
  }
673
333
  IdempotencyInterceptor = idempotency_interceptor_ts_decorate([
@@ -681,7 +341,7 @@ IdempotencyInterceptor = idempotency_interceptor_ts_decorate([
681
341
  ], IdempotencyInterceptor);
682
342
 
683
343
  // EXTERNAL MODULE: external "crypto"
684
- var external_crypto_ = __webpack_require__(823);
344
+ var external_crypto_ = __webpack_require__(7823);
685
345
  ;// CONCATENATED MODULE: ./projects/nestjs-shared/src/interceptors/response-meta.interceptor.ts
686
346
  function response_meta_interceptor_ts_decorate(decorators, target, key, desc) {
687
347
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -772,7 +432,7 @@ const SetUpdateByOnBody = createSetUserFieldInterceptor('updatedById');
772
432
  const SetDeletedByOnBody = createSetUserFieldInterceptor('deletedById');
773
433
 
774
434
  // EXTERNAL MODULE: ./projects/nestjs-shared/src/modules/utils/utils.service.ts
775
- var utils_service = __webpack_require__(169);
435
+ var utils_service = __webpack_require__(1169);
776
436
  ;// CONCATENATED MODULE: ./projects/nestjs-shared/src/interceptors/slug.interceptor.ts
777
437
  function slug_interceptor_ts_decorate(decorators, target, key, desc) {
778
438
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -806,26 +466,18 @@ class Slug {
806
466
  }
807
467
  intercept(context, next) {
808
468
  const request = context.switchToHttp().getRequest();
809
- if (Array.isArray(request.body)) {
810
- request.body = request.body.map((item)=>{
811
- // Only generate slug if not provided and name exists
812
- if (!item.slug && item?.name) {
813
- return {
814
- ...item,
815
- slug: this.utilsService.transformToSlug(item.name)
816
- };
817
- }
818
- return item;
819
- });
820
- } else if (!request.body?.slug && request.body?.name) {
821
- // Only generate slug if not provided and name exists
822
- request.body = {
823
- ...request.body,
824
- slug: this.utilsService.transformToSlug(request.body.name)
825
- };
826
- }
469
+ request.body = Array.isArray(request.body) ? request.body.map((item)=>this.withSlug(item)) : this.withSlug(request.body);
827
470
  return next.handle();
828
471
  }
472
+ withSlug(item) {
473
+ if (typeof item !== 'object' || item === null || Array.isArray(item)) return item;
474
+ const body = item;
475
+ if (body['slug'] || typeof body['name'] !== 'string' || !body['name']) return item;
476
+ return {
477
+ ...body,
478
+ slug: this.utilsService.transformToSlug(body['name'])
479
+ };
480
+ }
829
481
  }
830
482
  Slug = slug_interceptor_ts_decorate([
831
483
  (0,common_.Injectable)(),
@@ -845,8 +497,10 @@ Slug = slug_interceptor_ts_decorate([
845
497
 
846
498
 
847
499
  },
848
- 169(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
849
- /* import */ var _nestjs_common__rspack_import_0 = __webpack_require__(157);
500
+ 1169(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
501
+ /* import */ var _nestjs_common__rspack_import_0 = __webpack_require__(6157);
502
+ /* import */ var node_crypto__rspack_import_1 = __webpack_require__(2561);
503
+ /* import */ var _utils_string_util_js__rspack_import_2 = __webpack_require__(3366);
850
504
  function _ts_decorate(decorators, target, key, desc) {
851
505
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
852
506
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -861,61 +515,42 @@ function _ts_decorate(decorators, target, key, desc) {
861
515
  return c > 3 && r && Object.defineProperty(target, key, r), r;
862
516
  }
863
517
 
518
+
519
+
864
520
  class UtilsService {
521
+ logger = new _nestjs_common__rspack_import_0.Logger(UtilsService.name);
865
522
  // ---------------- CACHE HELPERS ----------------
866
523
  /**
867
- * Generate cache key with optional tenant prefix for multi-tenant isolation
868
- */ getCacheKey(entityName, params, entityId, tenantId) {
869
- const prefix = this.buildTenantPrefix(tenantId);
870
- if (entityId) {
871
- return `${prefix}entity_${entityName}_id_${entityId}${params ? '_select_' + JSON.stringify(params) : ''}`;
872
- }
873
- return `${prefix}entity_${entityName}_all_${JSON.stringify(params)}`;
524
+ * The combined version stamp of every entity a cached read depends on. Read it BEFORE querying
525
+ * the database: a write that commits mid-read moves the stamp, so the stale result is stored
526
+ * under a key no later read asks for.
527
+ */ async getCacheStamp(cacheManager, entityNames, tenantId) {
528
+ const stamps = await Promise.all(entityNames.map((name)=>cacheManager.getStamp(this.getStampName(name, tenantId))));
529
+ return stamps.join('.');
874
530
  }
875
- /**
876
- * Track cache key for later invalidation with optional tenant prefix
877
- */ async trackCacheKey(cacheKey, entityName, cacheManager, entityId, tenantId) {
878
- try {
879
- const trackingKey = this.buildTrackingKey(entityName, entityId, tenantId);
880
- const keys = await cacheManager.get(trackingKey) || [];
881
- if (!keys.includes(cacheKey)) keys.push(cacheKey);
882
- await cacheManager.set(trackingKey, keys);
883
- } catch {
884
- // Silent failure - cache tracking is non-critical
885
- }
531
+ getCacheKey(entityName, params, entityId, tenantId, stamp) {
532
+ const scope = entityId ? `id_${entityId}` : 'all';
533
+ const digest = (0,node_crypto__rspack_import_1.createHash)('sha256').update(JSON.stringify(params ?? null)).digest('hex');
534
+ return `${this.buildTenantPrefix(tenantId)}entity_${entityName}_${scope}_${stamp}_${digest}`;
886
535
  }
887
- /**
888
- * Clear cache for entity with optional tenant prefix
889
- */ async clearCache(entityName, cacheManager, entityId, tenantId) {
536
+ /** Invalidates every cached list and row of `entityName` in the tenant, on every instance */ async clearCache(entityName, cacheManager, tenantId) {
890
537
  try {
891
- const trackingKey = this.buildTrackingKey(entityName, entityId, tenantId);
892
- const keys = await cacheManager.get(trackingKey) || [];
893
- await Promise.allSettled(keys.map((key)=>cacheManager.del(key)));
894
- await cacheManager.del(trackingKey);
895
- } catch {
896
- // Silent failure - cache invalidation is non-critical
538
+ await cacheManager.bumpStamp(this.getStampName(entityName, tenantId));
539
+ } catch (error) {
540
+ this.logger.error(`Cache invalidation failed for '${entityName}': ${error.message}`);
897
541
  }
898
542
  }
899
- // ---------------- STRING HELPERS ----------------
900
- /**
901
- * Transform string to URL-friendly slug
902
- */ transformToSlug(value, salt) {
903
- const slug = value?.trim().replace(/[^A-Z0-9]+/gi, '-').toLowerCase();
904
- return salt ? `${slug}-${this.getRandomInt(1, 100)}` : slug;
543
+ /** The stamp `clearCache` bumps; for callers that must see the bump fail instead of having it logged */ getStampName(entityName, tenantId) {
544
+ return `${this.buildTenantPrefix(tenantId)}entity_${entityName}`;
905
545
  }
906
- /**
907
- * Generate random integer between min and max (inclusive)
908
- */ getRandomInt(min, max) {
909
- return Math.floor(Math.random() * (max - min + 1)) + min;
546
+ // ---------------- STRING HELPERS ----------------
547
+ transformToSlug(value) {
548
+ return (0,_utils_string_util_js__rspack_import_2/* .generateSlug */.z)(value);
910
549
  }
911
550
  // ---------------- PRIVATE HELPERS ----------------
912
551
  buildTenantPrefix(tenantId) {
913
552
  return tenantId ? `tenant_${tenantId}_` : '';
914
553
  }
915
- buildTrackingKey(entityName, entityId, tenantId) {
916
- const prefix = this.buildTenantPrefix(tenantId);
917
- return entityId ? `${prefix}entity_${entityName}_id_${entityId}_keys` : `${prefix}entity_${entityName}_keys`;
918
- }
919
554
  }
920
555
  UtilsService = _ts_decorate([
921
556
  (0,_nestjs_common__rspack_import_0.Injectable)()
@@ -927,19 +562,81 @@ __webpack_require__.d(__webpack_exports__, {
927
562
 
928
563
 
929
564
  },
930
- 307(module) {
931
-
932
- module.exports = __rspack_external__flusys_nestjs_core_config_197fa0a9;
933
-
934
-
935
- },
936
- 157(module) {
565
+ 3366(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
566
+ /**
567
+ * URL-friendly slug: lower case, words joined by single hyphens. Latin accents are folded
568
+ * (`Café` -> `cafe`) and letters of other scripts are kept, so a non-Latin name never
569
+ * collapses to an empty slug. Never starts or ends with a hyphen, also after truncation.
570
+ *
571
+ * @example
572
+ * generateSlug('My Company Name') // 'my-company-name'
573
+ * generateSlug('Hello World!') // 'hello-world'
574
+ */ function generateSlug(text, maxLength = 100) {
575
+ const slug = text.normalize('NFKD').toLowerCase().replace(/([a-z0-9])\p{M}+/gu, '$1').replace(/[^\p{L}\p{N}\p{M}\s-]/gu, '').trim().replace(/[\s-]+/g, '-').replace(/^-+|-+$/g, '').normalize('NFC');
576
+ return Array.from(slug).slice(0, Math.max(maxLength, 0)).join('').replace(/-+$/, '');
577
+ }
578
+ /**
579
+ * Generate unique slug using single database query (optimized for large datasets)
580
+ * Appends numeric suffix if collision exists (e.g., 'my-company-1', 'my-company-2')
581
+ *
582
+ * Performance: O(1) database query regardless of collision count
583
+ * - Works efficiently with millions of records
584
+ * - Requires index on slug column for best performance
585
+ *
586
+ * @param text - The text to convert to slug
587
+ * @param findMatchingSlugs - Async function that returns all slugs matching base pattern
588
+ * Should query: WHERE slug = baseSlug OR slug LIKE 'baseSlug-%'
589
+ * @param maxLength - Maximum length of the base slug (default: 100)
590
+ * @returns Unique URL-friendly slug
591
+ *
592
+ * @example
593
+ * const slug = await generateUniqueSlug('My Company', async (baseSlug) => {
594
+ * return repo
595
+ * .createQueryBuilder('c')
596
+ * .select('c.slug')
597
+ * .where('c.slug = :base OR c.slug LIKE :pattern', {
598
+ * base: baseSlug,
599
+ * pattern: `${baseSlug}-%`,
600
+ * })
601
+ * .getMany()
602
+ * .then(rows => rows.map(r => r.slug));
603
+ * });
604
+ */ async function generateUniqueSlug(text, findMatchingSlugs, maxLength = 100) {
605
+ const baseSlug = generateSlug(text, maxLength);
606
+ // Single query to get all matching slugs
607
+ const existingSlugs = await findMatchingSlugs(baseSlug);
608
+ // No collisions - base slug is available
609
+ if (existingSlugs.length === 0) {
610
+ return baseSlug;
611
+ }
612
+ // Build set for O(1) lookup
613
+ const existingSet = new Set(existingSlugs);
614
+ // Base slug not taken (only suffixed versions exist)
615
+ if (!existingSet.has(baseSlug)) {
616
+ return baseSlug;
617
+ }
618
+ // Find next available suffix in memory (fast)
619
+ let counter = 1;
620
+ while(counter < 10000){
621
+ const slugWithSuffix = `${baseSlug}-${counter}`;
622
+ if (!existingSet.has(slugWithSuffix)) {
623
+ return slugWithSuffix;
624
+ }
625
+ counter++;
626
+ }
627
+ // Fallback: append random string (extremely rare edge case)
628
+ const randomSuffix = Math.random().toString(36).substring(2, 8);
629
+ return `${baseSlug}-${randomSuffix}`;
630
+ }
937
631
 
938
- module.exports = __rspack_external__nestjs_common_a04939da;
632
+ __webpack_require__.d(__webpack_exports__, {
633
+ o: () => (generateUniqueSlug),
634
+ z: () => (generateSlug)
635
+ });
939
636
 
940
637
 
941
638
  },
942
- 823(module) {
639
+ 7823(module) {
943
640
 
944
641
  module.exports = __rspack_external_crypto;
945
642
 
@@ -949,14 +646,17 @@ module.exports = __rspack_external_crypto;
949
646
  };
950
647
  import { __webpack_require__ } from '../runtime.js';
951
648
  var __webpack_exec__ = function(moduleId) { return __webpack_require__(__webpack_require__.s = moduleId); }
952
- import * as __rspack_chunk_1 from './index.js';
649
+ import * as __rspack_chunk_1 from '../997.js';
953
650
  __webpack_require__.C(__rspack_chunk_1);
954
- var __webpack_exports__ = __webpack_exec__(863);var __webpack_exports__DeleteEmptyIdFromBodyInterceptor = __webpack_exports__.nd;
651
+ import * as __rspack_chunk_2 from './index.js';
652
+ __webpack_require__.C(__rspack_chunk_2);
653
+ var __webpack_exports__ = __webpack_exec__(6863);var __webpack_exports__DeleteEmptyIdFromBodyInterceptor = __webpack_exports__.nd;
955
654
  var __webpack_exports__IdempotencyInterceptor = __webpack_exports__.Bw;
956
655
  var __webpack_exports__ResponseMetaInterceptor = __webpack_exports__.c_;
957
656
  var __webpack_exports__SetCreatedByOnBody = __webpack_exports__.VS;
958
657
  var __webpack_exports__SetDeletedByOnBody = __webpack_exports__.Ul;
959
658
  var __webpack_exports__SetUpdateByOnBody = __webpack_exports__._x;
960
659
  var __webpack_exports__Slug = __webpack_exports__.Mw;
660
+ var __webpack_exports__buildIdempotencyCacheKey = __webpack_exports__.NM;
961
661
  var __webpack_exports__createSetUserFieldInterceptor = __webpack_exports__.BR;
962
- export { __webpack_exports__DeleteEmptyIdFromBodyInterceptor as DeleteEmptyIdFromBodyInterceptor, __webpack_exports__IdempotencyInterceptor as IdempotencyInterceptor, __webpack_exports__ResponseMetaInterceptor as ResponseMetaInterceptor, __webpack_exports__SetCreatedByOnBody as SetCreatedByOnBody, __webpack_exports__SetDeletedByOnBody as SetDeletedByOnBody, __webpack_exports__SetUpdateByOnBody as SetUpdateByOnBody, __webpack_exports__Slug as Slug, __webpack_exports__createSetUserFieldInterceptor as createSetUserFieldInterceptor };
662
+ export { __webpack_exports__DeleteEmptyIdFromBodyInterceptor as DeleteEmptyIdFromBodyInterceptor, __webpack_exports__IdempotencyInterceptor as IdempotencyInterceptor, __webpack_exports__ResponseMetaInterceptor as ResponseMetaInterceptor, __webpack_exports__SetCreatedByOnBody as SetCreatedByOnBody, __webpack_exports__SetDeletedByOnBody as SetDeletedByOnBody, __webpack_exports__SetUpdateByOnBody as SetUpdateByOnBody, __webpack_exports__Slug as Slug, __webpack_exports__buildIdempotencyCacheKey as buildIdempotencyCacheKey, __webpack_exports__createSetUserFieldInterceptor as createSetUserFieldInterceptor };