@flusys/nestjs-shared 9.1.2 → 9.2.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 (40) hide show
  1. package/README.md +94 -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/{282.js → 47.js} +98 -158
  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} +46 -24
  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 +19 -8
  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 +50 -41
  24. package/fesm/interceptors/index.js +290 -590
  25. package/fesm/interfaces/index.js +17 -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 +137 -478
  30. package/interceptors/idempotency.interceptor.d.ts +14 -2
  31. package/interceptors/slug.interceptor.d.ts +1 -0
  32. package/interfaces/permission-action-registry.interface.d.ts +1 -0
  33. package/interfaces/permission.interface.d.ts +1 -0
  34. package/modules/shared-permission-cache/shared-permission-cache.service.d.ts +3 -1
  35. package/modules/utils/utils.service.d.ts +6 -6
  36. package/package.json +2 -2
  37. package/utils/date-time.util.d.ts +0 -1
  38. package/utils/event-pattern.util.d.ts +1 -0
  39. package/utils/permission-cache-key.util.d.ts +3 -0
  40. package/utils/query-helpers.util.d.ts +2 -2
package/fesm/681.js CHANGED
@@ -9,7 +9,7 @@ import * as __rspack_external_events from "events";
9
9
  export const __rspack_esm_id = 681;
10
10
  export const __rspack_esm_ids = [681];
11
11
  export const __webpack_modules__ = {
12
- 864(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
12
+ 1864(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
13
13
  /**
14
14
  * Stands in for HybridEventBus when the subsystem is switched off.
15
15
  *
@@ -28,8 +28,8 @@ __webpack_require__.d(__webpack_exports__, {
28
28
 
29
29
 
30
30
  },
31
- 228(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
32
- /* import */ var _utils_event_pattern_util_js__rspack_import_0 = __webpack_require__(456);
31
+ 3228(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
32
+ /* import */ var _utils_event_pattern_util_js__rspack_import_0 = __webpack_require__(8456);
33
33
 
34
34
  const DISABLED = {
35
35
  enabled: false
@@ -88,7 +88,7 @@ __webpack_require__.d(__webpack_exports__, {
88
88
 
89
89
 
90
90
  },
91
- 149(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
91
+ 7149(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
92
92
  /**
93
93
  * Static registry for event transports.
94
94
  * Broker transports register themselves on import so nestjs-shared never has to
@@ -118,7 +118,7 @@ __webpack_require__.d(__webpack_exports__, {
118
118
 
119
119
 
120
120
  },
121
- 346(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
121
+ 9346(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
122
122
 
123
123
  // EXPORTS
124
124
  __webpack_require__.d(__webpack_exports__, {
@@ -153,7 +153,7 @@ const MAX_LISTENERS = 100;
153
153
  async bind(_pattern) {}
154
154
  onMessage(handler) {
155
155
  this.emitter.on(CHANNEL, (event)=>{
156
- void handler(event);
156
+ void Promise.resolve(handler(event)).catch(()=>undefined);
157
157
  });
158
158
  }
159
159
  async close() {
@@ -167,7 +167,7 @@ const MAX_LISTENERS = 100;
167
167
 
168
168
 
169
169
  },
170
- 375(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
170
+ 5375(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
171
171
 
172
172
  // EXPORTS
173
173
  __webpack_require__.d(__webpack_exports__, {
@@ -175,25 +175,41 @@ __webpack_require__.d(__webpack_exports__, {
175
175
  });
176
176
 
177
177
  // EXTERNAL MODULE: external "@flusys/nestjs-core/config"
178
- var config_ = __webpack_require__(307);
178
+ var config_ = __webpack_require__(8307);
179
179
  ;// CONCATENATED MODULE: external "@keyv/redis"
180
180
 
181
181
  ;// CONCATENATED MODULE: external "cacheable"
182
182
 
183
183
  ;// CONCATENATED MODULE: external "keyv"
184
184
 
185
+ // EXTERNAL MODULE: external "node:crypto"
186
+ var external_node_crypto_ = __webpack_require__(2561);
185
187
  ;// CONCATENATED MODULE: ./projects/nestjs-shared/src/classes/hybrid-cache.class.ts
186
188
 
187
189
 
188
190
 
189
191
 
190
- class HybridCache {
192
+
193
+ const RAW_KEY_PREFIX = 'flusys';
194
+ const STAMP_TTL_MS = 30 * 24 * 60 * 60 * 1000;
195
+ /**
196
+ * L1 in-process memory + L2 Redis, chosen by `USE_CACHE_LABEL` (memory | redis | hybrid).
197
+ *
198
+ * Plain `get`/`set` may be served by L1, which other instances cannot invalidate, so a value
199
+ * read that way must be immutable for its key - which is what version stamps give: data keys
200
+ * embed the current stamp, and invalidation moves the stamp instead of deleting entries.
201
+ * Stamps and claims live outside Keyv so they can be set atomically (Redis `SET NX`).
202
+ */ class HybridCache {
203
+ mode;
204
+ defaultTtl;
191
205
  memory;
192
206
  redis;
207
+ redisStore;
208
+ localAtomic;
193
209
  constructor(memoryTtl = 60 * 1000, memorySize = 5000){
194
- // Default to 'memory' if not specified
195
- const useCacheLabel = config_.envConfig.tryGetValue('USE_CACHE_LABEL', false) || 'memory';
196
- if (useCacheLabel === 'memory' || useCacheLabel === 'hybrid') {
210
+ this.mode = config_.envConfig.tryGetValue('USE_CACHE_LABEL', false) || 'memory';
211
+ this.defaultTtl = memoryTtl;
212
+ if (this.mode === 'memory' || this.mode === 'hybrid') {
197
213
  this.memory = new __rspack_external_keyv["default"]({
198
214
  store: new __rspack_external_cacheable.CacheableMemory({
199
215
  ttl: memoryTtl,
@@ -201,35 +217,51 @@ class HybridCache {
201
217
  })
202
218
  });
203
219
  }
204
- if (useCacheLabel === 'redis' || useCacheLabel === 'hybrid') {
220
+ if (this.mode === 'redis' || this.mode === 'hybrid') {
221
+ this.redisStore = new __rspack_external__keyv_redis_139d03ca["default"](config_.envConfig.getRedisUrl());
205
222
  this.redis = new __rspack_external_keyv["default"]({
206
- store: new __rspack_external__keyv_redis_139d03ca["default"](config_.envConfig.getRedisUrl())
223
+ store: this.redisStore
224
+ });
225
+ } else {
226
+ // Stamps and claims must never be LRU-evicted while still referenced
227
+ this.localAtomic = new __rspack_external_cacheable.CacheableMemory({
228
+ ttl: STAMP_TTL_MS,
229
+ lruSize: 0,
230
+ checkInterval: 60_000
207
231
  });
208
232
  }
209
233
  }
210
- async get(key) {
211
- // Prefer memory first if available
212
- if (this.memory) {
234
+ getMode() {
235
+ return this.mode;
236
+ }
237
+ async get(key, options = {}) {
238
+ const useMemory = this.memory && !(options.skipMemory && this.redis);
239
+ if (useMemory) {
213
240
  const value = await this.memory.get(key);
214
241
  if (value !== undefined) return value;
215
242
  }
216
- // Fallback to redis
217
243
  if (this.redis) {
218
- const value = await this.redis.get(key);
219
- if (value !== undefined && this.memory) {
220
- // backfill L1 memory cache
221
- await this.memory.set(key, value);
244
+ const raw = await this.redis.get(key, {
245
+ raw: true
246
+ });
247
+ if (raw === undefined) return undefined;
248
+ if (useMemory) {
249
+ // Never let the L1 copy outlive the Redis entry (time-bound values such as permissions)
250
+ const remaining = raw.expires ? raw.expires - Date.now() : this.defaultTtl;
251
+ if (remaining > 0) await this.memory.set(key, raw.value, Math.min(remaining, this.defaultTtl));
222
252
  }
223
- return value;
253
+ return raw.value;
224
254
  }
225
255
  return undefined;
226
256
  }
227
- async set(key, value, ttl) {
228
- if (this.memory) {
229
- await this.memory.set(key, value, ttl);
257
+ async set(key, value, ttl, options = {}) {
258
+ const effectiveTtl = ttl ?? this.defaultTtl;
259
+ const useMemory = this.memory && !(options.skipMemory && this.redis);
260
+ if (useMemory) {
261
+ await this.memory.set(key, value, effectiveTtl);
230
262
  }
231
263
  if (this.redis) {
232
- await this.redis.set(key, value, ttl);
264
+ await this.redis.set(key, value, effectiveTtl);
233
265
  }
234
266
  }
235
267
  async del(key) {
@@ -240,26 +272,91 @@ class HybridCache {
240
272
  await this.redis.delete(key);
241
273
  }
242
274
  }
243
- async reset() {
244
- if (this.memory) {
245
- await this.memory.clear();
275
+ /**
276
+ * The current version stamp for `name`, creating one when absent. A missing stamp is always
277
+ * replaced by a fresh random one - never a fixed default - so entries cached under a stamp that
278
+ * expired or was lost can never become current again.
279
+ */ async getStamp(name) {
280
+ const key = this.rawKey('stamp', name);
281
+ if (this.localAtomic) {
282
+ let stamp = this.localAtomic.get(key);
283
+ if (!stamp) {
284
+ stamp = (0,external_node_crypto_.randomUUID)();
285
+ this.localAtomic.set(key, stamp);
286
+ }
287
+ return stamp;
246
288
  }
289
+ const client = await this.redisStore.getClient();
290
+ const existing = await client.get(key);
291
+ if (existing) return String(existing);
292
+ const candidate = (0,external_node_crypto_.randomUUID)();
293
+ await client.set(key, candidate, {
294
+ NX: true,
295
+ PX: STAMP_TTL_MS
296
+ });
297
+ // A key evicted between SET NX and GET must not turn into the fixed stamp "null"
298
+ return String(await client.get(key) ?? candidate);
247
299
  }
248
- async resetL2() {
249
- if (this.redis) {
250
- await this.redis.clear();
300
+ /** Moves the stamp for `name`, orphaning every entry cached under the previous one on all instances */ async bumpStamp(name) {
301
+ const key = this.rawKey('stamp', name);
302
+ if (this.localAtomic) {
303
+ this.localAtomic.set(key, (0,external_node_crypto_.randomUUID)());
304
+ return;
305
+ }
306
+ const client = await this.redisStore.getClient();
307
+ await client.set(key, (0,external_node_crypto_.randomUUID)(), {
308
+ PX: STAMP_TTL_MS
309
+ });
310
+ }
311
+ /** Atomically claims `name` for `ttl` ms; true only for the first caller while the claim lives */ async claim(name, ttl) {
312
+ const key = this.rawKey('claim', name);
313
+ if (this.localAtomic) {
314
+ if (this.localAtomic.has(key)) return false;
315
+ this.localAtomic.set(key, true, ttl);
316
+ return true;
251
317
  }
318
+ const client = await this.redisStore.getClient();
319
+ return await client.set(key, '1', {
320
+ NX: true,
321
+ PX: ttl
322
+ }) !== null;
323
+ }
324
+ async releaseClaim(name) {
325
+ const key = this.rawKey('claim', name);
326
+ if (this.localAtomic) {
327
+ this.localAtomic.delete(key);
328
+ return;
329
+ }
330
+ const client = await this.redisStore.getClient();
331
+ await client.del(key);
332
+ }
333
+ /**
334
+ * Reads and removes a one-time value; only one caller across all instances ever receives it.
335
+ * The claim is per value, so a fresh value later stored under the same key (a resent code)
336
+ * can be taken again while the claim on the consumed one is still alive.
337
+ */ async take(key, ttl) {
338
+ const value = await this.get(key, {
339
+ skipMemory: true
340
+ });
341
+ if (value === undefined) return undefined;
342
+ const digest = (0,external_node_crypto_.createHash)('sha256').update(JSON.stringify(value) ?? '').digest('hex');
343
+ if (!await this.claim(`take:${key}:${digest}`, ttl)) return undefined;
344
+ await this.del(key);
345
+ return value;
346
+ }
347
+ rawKey(kind, name) {
348
+ return `${RAW_KEY_PREFIX}:${kind}:${name}`;
252
349
  }
253
350
  }
254
351
 
255
352
 
256
353
  },
257
- 710(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
258
- /* import */ var _flusys_nestjs_core_config__rspack_import_0 = __webpack_require__(307);
259
- /* import */ var _nestjs_common__rspack_import_1 = __webpack_require__(157);
260
- /* import */ var _utils_event_pattern_util_js__rspack_import_4 = __webpack_require__(456);
261
- /* import */ var _event_transports_event_transport_registry_js__rspack_import_2 = __webpack_require__(149);
262
- /* import */ var _event_transports_memory_transport_js__rspack_import_3 = __webpack_require__(346);
354
+ 5710(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
355
+ /* import */ var _flusys_nestjs_core_config__rspack_import_0 = __webpack_require__(8307);
356
+ /* import */ var _nestjs_common__rspack_import_1 = __webpack_require__(6157);
357
+ /* import */ var _utils_event_pattern_util_js__rspack_import_4 = __webpack_require__(8456);
358
+ /* import */ var _event_transports_event_transport_registry_js__rspack_import_2 = __webpack_require__(7149);
359
+ /* import */ var _event_transports_memory_transport_js__rspack_import_3 = __webpack_require__(9346);
263
360
 
264
361
 
265
362
 
@@ -418,7 +515,7 @@ const DEFAULT_DEDUPE_SIZE = 5000;
418
515
  */ async loadOptionalTransport(name) {
419
516
  if (_event_transports_event_transport_registry_js__rspack_import_2/* .EventTransportRegistry.has */.P.has(name)) return;
420
517
  if (name === 'rabbitmq') {
421
- const { RabbitMqTransport } = await __webpack_require__.e(/* import() */ 123).then(__webpack_require__.bind(__webpack_require__, 46));
518
+ const { RabbitMqTransport } = await __webpack_require__.e(/* import() */ 123).then(__webpack_require__.bind(__webpack_require__, 2046));
422
519
  _event_transports_event_transport_registry_js__rspack_import_2/* .EventTransportRegistry.register */.P.register('rabbitmq', (config)=>new RabbitMqTransport(config));
423
520
  return;
424
521
  }
@@ -484,9 +581,9 @@ __webpack_require__.d(__webpack_exports__, {
484
581
 
485
582
 
486
583
  },
487
- 374(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
488
- /* import */ var _nestjs_common__rspack_import_0 = __webpack_require__(157);
489
- /* import */ var _constants_message_keys_js__rspack_import_1 = __webpack_require__(389);
584
+ 8374(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
585
+ /* import */ var _nestjs_common__rspack_import_0 = __webpack_require__(6157);
586
+ /* import */ var _constants_message_keys_js__rspack_import_1 = __webpack_require__(4389);
490
587
 
491
588
 
492
589
  class PermissionSystemUnavailableException extends _nestjs_common__rspack_import_0.InternalServerErrorException {
@@ -531,12 +628,23 @@ __webpack_require__.d(__webpack_exports__, {
531
628
 
532
629
 
533
630
  },
534
- 657(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
535
- /* import */ var _nestjs_common__rspack_import_0 = __webpack_require__(157);
536
- /* import */ var _classes_hybrid_cache_class_js__rspack_import_1 = __webpack_require__(375);
537
- /* import */ var _constants_index_js__rspack_import_2 = __webpack_require__(43);
538
- /* import */ var _exceptions_permission_exception_js__rspack_import_3 = __webpack_require__(374);
539
- /* import */ var _utils_permission_cache_key_util_js__rspack_import_4 = __webpack_require__(199);
631
+ 1647(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
632
+ const PERMISSION_RESOLVER = 'PERMISSION_RESOLVER';
633
+
634
+ __webpack_require__.d(__webpack_exports__, {
635
+ }, {
636
+ j: PERMISSION_RESOLVER
637
+ });
638
+
639
+
640
+ },
641
+ 4657(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
642
+ /* import */ var _nestjs_common__rspack_import_0 = __webpack_require__(6157);
643
+ /* import */ var _classes_hybrid_cache_class_js__rspack_import_1 = __webpack_require__(5375);
644
+ /* import */ var _constants_index_js__rspack_import_2 = __webpack_require__(6043);
645
+ /* import */ var _exceptions_permission_exception_js__rspack_import_3 = __webpack_require__(8374);
646
+ /* import */ var _interfaces_permission_resolver_interface_js__rspack_import_6 = __webpack_require__(1647);
647
+ /* import */ var _utils_permission_cache_key_util_js__rspack_import_4 = __webpack_require__(3199);
540
648
  /* import */ var _utils_permission_match_util_js__rspack_import_5 = __webpack_require__(788);
541
649
  function _ts_decorate(decorators, target, key, desc) {
542
650
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -567,11 +675,14 @@ function _ts_param(paramIndex, decorator) {
567
675
 
568
676
 
569
677
 
678
+
570
679
  class SharedPermissionCacheService {
571
680
  cache;
681
+ resolver;
572
682
  config;
573
- constructor(cache, config){
683
+ constructor(cache, config, resolver){
574
684
  this.cache = cache;
685
+ this.resolver = resolver;
575
686
  this.config = {
576
687
  enableCompanyFeature: false,
577
688
  userPermissionKeyFormat: _utils_permission_cache_key_util_js__rspack_import_4/* .DEFAULT_USER_PERMISSION_KEY_FORMAT */.q6,
@@ -579,22 +690,21 @@ class SharedPermissionCacheService {
579
690
  ...config
580
691
  };
581
692
  }
582
- /** Read the cached permission codes for a user; empty array when cache is unavailable or unset */ async getPermissions(options) {
693
+ /** The user's permission codes in a scope: cached, else rebuilt by the resolver; empty when neither has them */ async getPermissions(options) {
583
694
  if (!this.cache) return [];
584
- const cacheKey = (0,_utils_permission_cache_key_util_js__rspack_import_4/* .buildPermissionCacheKey */.nt)(options, this.config);
695
+ const cacheKey = await (0,_utils_permission_cache_key_util_js__rspack_import_4/* .resolvePermissionCacheKey */.Rw)(this.cache, options, this.config);
585
696
  const cached = await this.cache.get(cacheKey);
586
- if (!cached) return [];
587
- return Array.isArray(cached) ? cached : cached.backendCodes || [];
697
+ if (cached) return Array.isArray(cached) ? cached : cached.backendCodes || [];
698
+ return this.resolver ? this.resolver.getPermissionCodes(options) : [];
588
699
  }
589
700
  /** Overwrite the cached permission codes for a user with a fixed list */ async setPermissions(options, permissions, ttl) {
590
701
  if (!this.cache) return;
591
- const cacheKey = (0,_utils_permission_cache_key_util_js__rspack_import_4/* .buildPermissionCacheKey */.nt)(options, this.config);
702
+ const cacheKey = await (0,_utils_permission_cache_key_util_js__rspack_import_4/* .resolvePermissionCacheKey */.Rw)(this.cache, options, this.config);
592
703
  await this.cache.set(cacheKey, permissions, ttl);
593
704
  }
594
- /** Clear the cached permission codes for a user */ async clearPermissions(options) {
705
+ /** Clear the cached permission codes of a user - every company and branch scope */ async clearPermissions(options) {
595
706
  if (!this.cache) return;
596
- const cacheKey = (0,_utils_permission_cache_key_util_js__rspack_import_4/* .buildPermissionCacheKey */.nt)(options, this.config);
597
- await this.cache.del(cacheKey);
707
+ await (0,_utils_permission_cache_key_util_js__rspack_import_4/* .invalidatePermissionCache */.xp)(this.cache, options.userId, this.config);
598
708
  }
599
709
  /** Check a single permission against the cached list, honoring `*` and `prefix.*` wildcards */ async hasPermission(options, requiredPermission) {
600
710
  const permissions = await this.getPermissions(options);
@@ -634,10 +744,13 @@ SharedPermissionCacheService = _ts_decorate([
634
744
  _ts_param(0, (0,_nestjs_common__rspack_import_0.Inject)(_constants_index_js__rspack_import_2/* .CACHE_INSTANCE */.zl)),
635
745
  _ts_param(1, (0,_nestjs_common__rspack_import_0.Optional)()),
636
746
  _ts_param(1, (0,_nestjs_common__rspack_import_0.Inject)(_constants_index_js__rspack_import_2/* .PERMISSION_GUARD_CONFIG */.Lw)),
747
+ _ts_param(2, (0,_nestjs_common__rspack_import_0.Optional)()),
748
+ _ts_param(2, (0,_nestjs_common__rspack_import_0.Inject)(_interfaces_permission_resolver_interface_js__rspack_import_6/* .PERMISSION_RESOLVER */.j)),
637
749
  _ts_metadata("design:type", Function),
638
750
  _ts_metadata("design:paramtypes", [
639
751
  typeof _classes_hybrid_cache_class_js__rspack_import_1/* .HybridCache */.l === "undefined" ? Object : _classes_hybrid_cache_class_js__rspack_import_1/* .HybridCache */.l,
640
- typeof PermissionGuardConfig === "undefined" ? Object : PermissionGuardConfig
752
+ typeof PermissionGuardConfig === "undefined" ? Object : PermissionGuardConfig,
753
+ typeof IPermissionResolver === "undefined" ? Object : IPermissionResolver
641
754
  ])
642
755
  ], SharedPermissionCacheService);
643
756
 
@@ -647,8 +760,10 @@ __webpack_require__.d(__webpack_exports__, {
647
760
 
648
761
 
649
762
  },
650
- 169(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
651
- /* import */ var _nestjs_common__rspack_import_0 = __webpack_require__(157);
763
+ 1169(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
764
+ /* import */ var _nestjs_common__rspack_import_0 = __webpack_require__(6157);
765
+ /* import */ var node_crypto__rspack_import_1 = __webpack_require__(2561);
766
+ /* import */ var _utils_string_util_js__rspack_import_2 = __webpack_require__(3366);
652
767
  function _ts_decorate(decorators, target, key, desc) {
653
768
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
654
769
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -663,61 +778,42 @@ function _ts_decorate(decorators, target, key, desc) {
663
778
  return c > 3 && r && Object.defineProperty(target, key, r), r;
664
779
  }
665
780
 
781
+
782
+
666
783
  class UtilsService {
784
+ logger = new _nestjs_common__rspack_import_0.Logger(UtilsService.name);
667
785
  // ---------------- CACHE HELPERS ----------------
668
786
  /**
669
- * Generate cache key with optional tenant prefix for multi-tenant isolation
670
- */ getCacheKey(entityName, params, entityId, tenantId) {
671
- const prefix = this.buildTenantPrefix(tenantId);
672
- if (entityId) {
673
- return `${prefix}entity_${entityName}_id_${entityId}${params ? '_select_' + JSON.stringify(params) : ''}`;
674
- }
675
- return `${prefix}entity_${entityName}_all_${JSON.stringify(params)}`;
676
- }
677
- /**
678
- * Track cache key for later invalidation with optional tenant prefix
679
- */ async trackCacheKey(cacheKey, entityName, cacheManager, entityId, tenantId) {
787
+ * The combined version stamp of every entity a cached read depends on. Read it BEFORE querying
788
+ * the database: a write that commits mid-read moves the stamp, so the stale result is stored
789
+ * under a key no later read asks for.
790
+ */ async getCacheStamp(cacheManager, entityNames, tenantId) {
791
+ const stamps = await Promise.all(entityNames.map((name)=>cacheManager.getStamp(this.getStampName(name, tenantId))));
792
+ return stamps.join('.');
793
+ }
794
+ getCacheKey(entityName, params, entityId, tenantId, stamp) {
795
+ const scope = entityId ? `id_${entityId}` : 'all';
796
+ const digest = (0,node_crypto__rspack_import_1.createHash)('sha256').update(JSON.stringify(params ?? null)).digest('hex');
797
+ return `${this.buildTenantPrefix(tenantId)}entity_${entityName}_${scope}_${stamp}_${digest}`;
798
+ }
799
+ /** Invalidates every cached list and row of `entityName` in the tenant, on every instance */ async clearCache(entityName, cacheManager, tenantId) {
680
800
  try {
681
- const trackingKey = this.buildTrackingKey(entityName, entityId, tenantId);
682
- const keys = await cacheManager.get(trackingKey) || [];
683
- if (!keys.includes(cacheKey)) keys.push(cacheKey);
684
- await cacheManager.set(trackingKey, keys);
685
- } catch {
686
- // Silent failure - cache tracking is non-critical
801
+ await cacheManager.bumpStamp(this.getStampName(entityName, tenantId));
802
+ } catch (error) {
803
+ this.logger.error(`Cache invalidation failed for '${entityName}': ${error.message}`);
687
804
  }
688
805
  }
689
- /**
690
- * Clear cache for entity with optional tenant prefix
691
- */ async clearCache(entityName, cacheManager, entityId, tenantId) {
692
- try {
693
- const trackingKey = this.buildTrackingKey(entityName, entityId, tenantId);
694
- const keys = await cacheManager.get(trackingKey) || [];
695
- await Promise.allSettled(keys.map((key)=>cacheManager.del(key)));
696
- await cacheManager.del(trackingKey);
697
- } catch {
698
- // Silent failure - cache invalidation is non-critical
699
- }
806
+ /** The stamp `clearCache` bumps; for callers that must see the bump fail instead of having it logged */ getStampName(entityName, tenantId) {
807
+ return `${this.buildTenantPrefix(tenantId)}entity_${entityName}`;
700
808
  }
701
809
  // ---------------- STRING HELPERS ----------------
702
- /**
703
- * Transform string to URL-friendly slug
704
- */ transformToSlug(value, salt) {
705
- const slug = value?.trim().replace(/[^A-Z0-9]+/gi, '-').toLowerCase();
706
- return salt ? `${slug}-${this.getRandomInt(1, 100)}` : slug;
707
- }
708
- /**
709
- * Generate random integer between min and max (inclusive)
710
- */ getRandomInt(min, max) {
711
- return Math.floor(Math.random() * (max - min + 1)) + min;
810
+ transformToSlug(value) {
811
+ return (0,_utils_string_util_js__rspack_import_2/* .generateSlug */.z)(value);
712
812
  }
713
813
  // ---------------- PRIVATE HELPERS ----------------
714
814
  buildTenantPrefix(tenantId) {
715
815
  return tenantId ? `tenant_${tenantId}_` : '';
716
816
  }
717
- buildTrackingKey(entityName, entityId, tenantId) {
718
- const prefix = this.buildTenantPrefix(tenantId);
719
- return entityId ? `${prefix}entity_${entityName}_id_${entityId}_keys` : `${prefix}entity_${entityName}_keys`;
720
- }
721
817
  }
722
818
  UtilsService = _ts_decorate([
723
819
  (0,_nestjs_common__rspack_import_0.Injectable)()
@@ -729,7 +825,7 @@ __webpack_require__.d(__webpack_exports__, {
729
825
 
730
826
 
731
827
  },
732
- 456(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
828
+ 8456(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
733
829
  const SENSITIVE_KEYS = [
734
830
  'password',
735
831
  'secret',
@@ -767,25 +863,27 @@ const MAX_DEPTH = 8;
767
863
  * `*` matches exactly one segment, `#` and `**` match zero or more segments.
768
864
  */ function matchEventPattern(pattern, eventName) {
769
865
  if (pattern === eventName) return true;
770
- const patternSegments = pattern.split('.');
771
- const nameSegments = eventName.split('.');
772
- return matchSegments(patternSegments, 0, nameSegments, 0);
866
+ return matchSegments(pattern.split('.'), eventName.split('.'));
773
867
  }
774
868
  function isMultiWildcard(segment) {
775
869
  return segment === '#' || segment === '**';
776
870
  }
777
- function matchSegments(pattern, patternIndex, name, nameIndex) {
778
- if (patternIndex === pattern.length) return nameIndex === name.length;
779
- const segment = pattern[patternIndex];
780
- if (isMultiWildcard(segment)) {
781
- for(let skip = nameIndex; skip <= name.length; skip++){
782
- if (matchSegments(pattern, patternIndex + 1, name, skip)) return true;
871
+ /** Linear in pattern x name length: each (pattern, name) position is decided once, however many `#` there are. */ function matchSegments(pattern, name) {
872
+ let matches = new Array(name.length + 1).fill(false);
873
+ matches[name.length] = true;
874
+ for(let p = pattern.length - 1; p >= 0; p--){
875
+ const segment = pattern[p];
876
+ const next = new Array(name.length + 1).fill(false);
877
+ for(let n = name.length; n >= 0; n--){
878
+ if (isMultiWildcard(segment)) {
879
+ next[n] = matches[n] || n < name.length && next[n + 1];
880
+ } else {
881
+ next[n] = n < name.length && (segment === '*' || segment === name[n]) && matches[n + 1];
882
+ }
783
883
  }
784
- return false;
884
+ matches = next;
785
885
  }
786
- if (nameIndex === name.length) return false;
787
- if (segment !== '*' && segment !== name[nameIndex]) return false;
788
- return matchSegments(pattern, patternIndex + 1, name, nameIndex + 1);
886
+ return matches[0];
789
887
  }
790
888
  /**
791
889
  * Strips credential-like fields before a row leaves the process on an event.
@@ -816,13 +914,14 @@ function sanitizeValue(value, ancestors, depth) {
816
914
  ancestors.delete(value);
817
915
  }
818
916
  }
819
- function isSensitiveKey(key) {
820
- const lowered = key.toLowerCase();
821
- if (SENSITIVE_EXACT_KEYS.includes(lowered)) return true;
822
- return SENSITIVE_KEYS.some((sensitive)=>lowered.includes(sensitive));
917
+ /** Case and separator blind, so `apiKey`, `api_key`, `X-API-KEY` and `private-key` all match. */ function isSensitiveKey(key) {
918
+ const normalized = key.toLowerCase().replace(/[\s_.-]/g, '');
919
+ if (SENSITIVE_EXACT_KEYS.includes(normalized)) return true;
920
+ return SENSITIVE_KEYS.some((sensitive)=>normalized.includes(sensitive));
823
921
  }
824
922
 
825
923
  __webpack_require__.d(__webpack_exports__, {
924
+ Gd: () => (isSensitiveKey),
826
925
  IN: () => (sanitizeEventPayload),
827
926
  _c: () => (isMultiWildcard),
828
927
  qe: () => (matchEventPattern)
@@ -830,8 +929,10 @@ __webpack_require__.d(__webpack_exports__, {
830
929
 
831
930
 
832
931
  },
833
- 199(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
834
- /* import */ var _constants_index_js__rspack_import_0 = __webpack_require__(43);
932
+ 3199(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
933
+ /* import */ var _constants_index_js__rspack_import_0 = __webpack_require__(6043);
934
+ /* import */ var _middlewares_logger_middleware_js__rspack_import_1 = __webpack_require__(8244);
935
+
835
936
 
836
937
  const DEFAULT_USER_PERMISSION_KEY_FORMAT = `${_constants_index_js__rspack_import_0/* .PERMISSIONS_CACHE_PREFIX */.fG}:user:{userId}`;
837
938
  const DEFAULT_COMPANY_PERMISSION_KEY_FORMAT = `${_constants_index_js__rspack_import_0/* .PERMISSIONS_CACHE_PREFIX */.fG}:company:{companyId}:branch:{branchId}:user:{userId}`;
@@ -842,9 +943,31 @@ const DEFAULT_COMPANY_PERMISSION_KEY_FORMAT = `${_constants_index_js__rspack_imp
842
943
  }
843
944
  return (config?.userPermissionKeyFormat || DEFAULT_USER_PERMISSION_KEY_FORMAT).replace('{userId}', options.userId);
844
945
  }
946
+ /**
947
+ * Tenant databases can share user ids (they are cloned from one template), so multi-tenant keys
948
+ * carry the request's tenant. A single database never does: a stray tenant header must not move
949
+ * a user onto a cache entry that invalidation never reaches.
950
+ */ function permissionTenant(config) {
951
+ return config?.multiTenant ? `tenant:${(0,_middlewares_logger_middleware_js__rspack_import_1/* .getTenantId */.EO)() ?? 'none'}:` : '';
952
+ }
953
+ function permissionStampName(userId, config) {
954
+ return `${_constants_index_js__rspack_import_0/* .PERMISSIONS_CACHE_PREFIX */.fG}:${permissionTenant(config)}user:${userId}`;
955
+ }
956
+ /**
957
+ * The key a user's permissions are cached under right now: tenant + scope + the user's version
958
+ * stamp. Resolve it once, BEFORE reading grants from the database, and write under that same key.
959
+ */ async function resolvePermissionCacheKey(cache, options, config) {
960
+ const stamp = await cache.getStamp(permissionStampName(options.userId, config));
961
+ return `${permissionTenant(config)}${buildPermissionCacheKey(options, config)}:v:${stamp}`;
962
+ }
963
+ /** Drops every cached permission scope (each company and branch) of the user, on every instance */ async function invalidatePermissionCache(cache, userId, config) {
964
+ await cache.bumpStamp(permissionStampName(userId, config));
965
+ }
845
966
 
846
967
  __webpack_require__.d(__webpack_exports__, {
847
- nt: () => (buildPermissionCacheKey)
968
+ Rw: () => (resolvePermissionCacheKey),
969
+ nt: () => (buildPermissionCacheKey),
970
+ xp: () => (invalidatePermissionCache)
848
971
  }, {
849
972
  q6: DEFAULT_USER_PERMISSION_KEY_FORMAT,
850
973
  s8: DEFAULT_COMPANY_PERMISSION_KEY_FORMAT
@@ -853,7 +976,7 @@ __webpack_require__.d(__webpack_exports__, {
853
976
 
854
977
  },
855
978
  788(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
856
- /* import */ var _exceptions_permission_exception_js__rspack_import_0 = __webpack_require__(374);
979
+ /* import */ var _exceptions_permission_exception_js__rspack_import_0 = __webpack_require__(8374);
857
980
 
858
981
  /** Whether `granted` covers `required`, honoring `*` (everything) and `prefix.*` (everything under prefix) wildcards */ function matchesPermission(granted, required) {
859
982
  if (granted.includes(required)) return true;
@@ -934,25 +1057,99 @@ __webpack_require__.d(__webpack_exports__, {
934
1057
 
935
1058
 
936
1059
  },
937
- 436(module) {
1060
+ 3366(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
1061
+ /**
1062
+ * URL-friendly slug: lower case, words joined by single hyphens. Latin accents are folded
1063
+ * (`Café` -> `cafe`) and letters of other scripts are kept, so a non-Latin name never
1064
+ * collapses to an empty slug. Never starts or ends with a hyphen, also after truncation.
1065
+ *
1066
+ * @example
1067
+ * generateSlug('My Company Name') // 'my-company-name'
1068
+ * generateSlug('Hello World!') // 'hello-world'
1069
+ */ function generateSlug(text, maxLength = 100) {
1070
+ 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');
1071
+ return Array.from(slug).slice(0, Math.max(maxLength, 0)).join('').replace(/-+$/, '');
1072
+ }
1073
+ /**
1074
+ * Generate unique slug using single database query (optimized for large datasets)
1075
+ * Appends numeric suffix if collision exists (e.g., 'my-company-1', 'my-company-2')
1076
+ *
1077
+ * Performance: O(1) database query regardless of collision count
1078
+ * - Works efficiently with millions of records
1079
+ * - Requires index on slug column for best performance
1080
+ *
1081
+ * @param text - The text to convert to slug
1082
+ * @param findMatchingSlugs - Async function that returns all slugs matching base pattern
1083
+ * Should query: WHERE slug = baseSlug OR slug LIKE 'baseSlug-%'
1084
+ * @param maxLength - Maximum length of the base slug (default: 100)
1085
+ * @returns Unique URL-friendly slug
1086
+ *
1087
+ * @example
1088
+ * const slug = await generateUniqueSlug('My Company', async (baseSlug) => {
1089
+ * return repo
1090
+ * .createQueryBuilder('c')
1091
+ * .select('c.slug')
1092
+ * .where('c.slug = :base OR c.slug LIKE :pattern', {
1093
+ * base: baseSlug,
1094
+ * pattern: `${baseSlug}-%`,
1095
+ * })
1096
+ * .getMany()
1097
+ * .then(rows => rows.map(r => r.slug));
1098
+ * });
1099
+ */ async function generateUniqueSlug(text, findMatchingSlugs, maxLength = 100) {
1100
+ const baseSlug = generateSlug(text, maxLength);
1101
+ // Single query to get all matching slugs
1102
+ const existingSlugs = await findMatchingSlugs(baseSlug);
1103
+ // No collisions - base slug is available
1104
+ if (existingSlugs.length === 0) {
1105
+ return baseSlug;
1106
+ }
1107
+ // Build set for O(1) lookup
1108
+ const existingSet = new Set(existingSlugs);
1109
+ // Base slug not taken (only suffixed versions exist)
1110
+ if (!existingSet.has(baseSlug)) {
1111
+ return baseSlug;
1112
+ }
1113
+ // Find next available suffix in memory (fast)
1114
+ let counter = 1;
1115
+ while(counter < 10000){
1116
+ const slugWithSuffix = `${baseSlug}-${counter}`;
1117
+ if (!existingSet.has(slugWithSuffix)) {
1118
+ return slugWithSuffix;
1119
+ }
1120
+ counter++;
1121
+ }
1122
+ // Fallback: append random string (extremely rare edge case)
1123
+ const randomSuffix = Math.random().toString(36).substring(2, 8);
1124
+ return `${baseSlug}-${randomSuffix}`;
1125
+ }
1126
+
1127
+ __webpack_require__.d(__webpack_exports__, {
1128
+ o: () => (generateUniqueSlug),
1129
+ z: () => (generateSlug)
1130
+ });
1131
+
1132
+
1133
+ },
1134
+ 8436(module) {
938
1135
 
939
1136
  module.exports = __rspack_external__flusys_nestjs_core_ff4ca4e2;
940
1137
 
941
1138
 
942
1139
  },
943
- 75(module) {
1140
+ 3075(module) {
944
1141
 
945
1142
  module.exports = __rspack_external__nestjs_core_d7b00a1a;
946
1143
 
947
1144
 
948
1145
  },
949
- 823(module) {
1146
+ 7823(module) {
950
1147
 
951
1148
  module.exports = __rspack_external_crypto;
952
1149
 
953
1150
 
954
1151
  },
955
- 158(module) {
1152
+ 7158(module) {
956
1153
 
957
1154
  module.exports = __rspack_external_typeorm;
958
1155