@flusys/nestjs-shared 9.1.2 → 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.
- package/README.md +94 -18
- package/classes/api-controller.class.d.ts +1 -0
- package/classes/api-service.class.d.ts +7 -2
- package/classes/hybrid-cache.class.d.ts +17 -4
- package/constants/index.d.ts +1 -0
- package/constants/permissions.d.ts +9 -0
- package/fesm/183.js +7 -7
- package/fesm/{282.js → 47.js} +98 -158
- package/fesm/470.js +333 -382
- package/fesm/667.js +17 -17
- package/fesm/681.js +326 -129
- package/fesm/{948.js → 997.js} +45 -23
- package/fesm/chunks/123.js +17 -9
- package/fesm/chunks/37.js +5 -5
- package/fesm/classes/index.js +4 -3
- package/fesm/constants/index.js +18 -7
- package/fesm/decorators/index.js +120 -39
- package/fesm/dtos/index.js +1 -1
- package/fesm/entities/index.js +8 -8
- package/fesm/enums/index.js +2 -2
- package/fesm/exceptions/index.js +12 -12
- package/fesm/guards/index.js +201 -68
- package/fesm/index.js +50 -41
- package/fesm/interceptors/index.js +290 -590
- package/fesm/interfaces/index.js +17 -8
- package/fesm/middlewares/index.js +4 -4
- package/fesm/modules/index.js +2 -2
- package/fesm/pipes/index.js +8 -8
- package/fesm/utils/index.js +137 -478
- package/interceptors/idempotency.interceptor.d.ts +14 -2
- package/interceptors/slug.interceptor.d.ts +1 -0
- package/interfaces/permission-action-registry.interface.d.ts +1 -0
- package/interfaces/permission.interface.d.ts +1 -0
- package/modules/shared-permission-cache/shared-permission-cache.service.d.ts +3 -1
- package/modules/utils/utils.service.d.ts +6 -6
- package/package.json +2 -2
- package/utils/date-time.util.d.ts +0 -1
- package/utils/event-pattern.util.d.ts +1 -0
- package/utils/permission-cache-key.util.d.ts +3 -0
- 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
|
-
|
|
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
|
-
|
|
32
|
-
/* import */ var _utils_event_pattern_util_js__rspack_import_0 = __webpack_require__(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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__(
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
if (
|
|
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 (
|
|
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:
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
229
|
-
|
|
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,
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
258
|
-
/* import */ var _flusys_nestjs_core_config__rspack_import_0 = __webpack_require__(
|
|
259
|
-
/* import */ var _nestjs_common__rspack_import_1 = __webpack_require__(
|
|
260
|
-
/* import */ var _utils_event_pattern_util_js__rspack_import_4 = __webpack_require__(
|
|
261
|
-
/* import */ var _event_transports_event_transport_registry_js__rspack_import_2 = __webpack_require__(
|
|
262
|
-
/* import */ var _event_transports_memory_transport_js__rspack_import_3 = __webpack_require__(
|
|
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__,
|
|
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
|
-
|
|
488
|
-
/* import */ var _nestjs_common__rspack_import_0 = __webpack_require__(
|
|
489
|
-
/* import */ var _constants_message_keys_js__rspack_import_1 = __webpack_require__(
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
/**
|
|
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/* .
|
|
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 (
|
|
587
|
-
return
|
|
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/* .
|
|
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
|
|
705
|
+
/** Clear the cached permission codes of a user - every company and branch scope */ async clearPermissions(options) {
|
|
595
706
|
if (!this.cache) return;
|
|
596
|
-
|
|
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
|
-
|
|
651
|
-
/* import */ var _nestjs_common__rspack_import_0 = __webpack_require__(
|
|
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
|
-
*
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
-
|
|
884
|
+
matches = next;
|
|
785
885
|
}
|
|
786
|
-
|
|
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
|
|
821
|
-
if (SENSITIVE_EXACT_KEYS.includes(
|
|
822
|
-
return SENSITIVE_KEYS.some((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
|
-
|
|
834
|
-
/* import */ var _constants_index_js__rspack_import_0 = __webpack_require__(
|
|
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
|
-
|
|
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__(
|
|
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
|
-
|
|
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
|
-
|
|
1140
|
+
3075(module) {
|
|
944
1141
|
|
|
945
1142
|
module.exports = __rspack_external__nestjs_core_d7b00a1a;
|
|
946
1143
|
|
|
947
1144
|
|
|
948
1145
|
},
|
|
949
|
-
|
|
1146
|
+
7823(module) {
|
|
950
1147
|
|
|
951
1148
|
module.exports = __rspack_external_crypto;
|
|
952
1149
|
|
|
953
1150
|
|
|
954
1151
|
},
|
|
955
|
-
|
|
1152
|
+
7158(module) {
|
|
956
1153
|
|
|
957
1154
|
module.exports = __rspack_external_typeorm;
|
|
958
1155
|
|