@yuuvis/client-core 2.20.1 → 2.21.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/fesm2022/yuuvis-client-core.mjs +158 -53
- package/fesm2022/yuuvis-client-core.mjs.map +1 -1
- package/lib/service/idm/idm.service.d.ts +106 -14
- package/lib/service/idm/models/idm-cached-user.model.d.ts +20 -4
- package/lib/service/idm/models/idm-user-cache.model.d.ts +22 -3
- package/lib/service/idm/models/idm-user-response.model.d.ts +27 -11
- package/lib/service/idm/models/organization-set-entry.model.d.ts +26 -5
- package/package.json +1 -1
|
@@ -4514,47 +4514,128 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
|
|
|
4514
4514
|
}] });
|
|
4515
4515
|
|
|
4516
4516
|
/**
|
|
4517
|
-
*
|
|
4518
|
-
*
|
|
4519
|
-
*
|
|
4517
|
+
* Centralized service for Identity Management (IDM) operations.
|
|
4518
|
+
*
|
|
4519
|
+
* Provides a typed API for querying users, roles, and organization entities
|
|
4520
|
+
* from the IDM backend, with a built-in client-side caching layer that
|
|
4521
|
+
* minimizes redundant HTTP calls.
|
|
4522
|
+
*
|
|
4523
|
+
* **Key Features:**
|
|
4524
|
+
* - User lookup by ID with automatic 1-hour TTL cache
|
|
4525
|
+
* - Unified search across users and roles via a single method
|
|
4526
|
+
* - Role listing with optional name filter
|
|
4527
|
+
* - Cache persisted to IndexedDB/localStorage (survives page refresh)
|
|
4528
|
+
* - In-memory cache hydrated on service creation for instant access
|
|
4529
|
+
*
|
|
4530
|
+
* **Caching Strategy:**
|
|
4531
|
+
* All cached users are stored as a single map ({@link IdmUserCache}) under one
|
|
4532
|
+
* storage key. This enables atomic load on startup — one read populates the
|
|
4533
|
+
* entire in-memory cache — and avoids N+1 reads when resolving multiple users
|
|
4534
|
+
* (e.g. audit trails, comment threads, assignment lists). Per-entry TTL is
|
|
4535
|
+
* enforced via {@link IdmCachedUser.expires}.
|
|
4536
|
+
*
|
|
4537
|
+
* ```
|
|
4538
|
+
* getUserById("abc")
|
|
4539
|
+
* ├─ Cache hit & valid → return immediately (no HTTP)
|
|
4540
|
+
* └─ Cache miss/expired → GET /idm/users/abc
|
|
4541
|
+
* ├─ Update in-memory cache
|
|
4542
|
+
* ├─ Persist to ClientCacheService
|
|
4543
|
+
* └─ Return YuvUser
|
|
4544
|
+
* ```
|
|
4545
|
+
*
|
|
4546
|
+
* **API Endpoints:**
|
|
4547
|
+
* | Method | Endpoint |
|
|
4548
|
+
* |----------------------------|----------------------|
|
|
4549
|
+
* | `queryOrganizationEntity` | `GET /idm/search` |
|
|
4550
|
+
* | `getRoles` | `GET /idm/roles` |
|
|
4551
|
+
* | `getUserById` | `GET /idm/users/:id` |
|
|
4552
|
+
*
|
|
4553
|
+
* **Usage:**
|
|
4554
|
+
* ```ts
|
|
4555
|
+
* const idm = inject(IdmService);
|
|
4556
|
+
*
|
|
4557
|
+
* // Search users and roles
|
|
4558
|
+
* idm.queryOrganizationEntity('admin', ['user', 'role'])
|
|
4559
|
+
* .subscribe(entries => console.log(entries));
|
|
4560
|
+
*
|
|
4561
|
+
* // Get a user by ID (cached)
|
|
4562
|
+
* idm.getUserById('user-uuid').subscribe(user => console.log(user?.title));
|
|
4563
|
+
*
|
|
4564
|
+
* // List roles
|
|
4565
|
+
* idm.getRoles('ADMIN').subscribe(roles => console.log(roles));
|
|
4566
|
+
* ```
|
|
4567
|
+
*
|
|
4568
|
+
* @see {@link ClientCacheService} for the underlying persistence layer
|
|
4569
|
+
* @see {@link YuvUser} for the application-level user model
|
|
4520
4570
|
*/
|
|
4521
4571
|
class IdmService {
|
|
4522
|
-
|
|
4523
|
-
#
|
|
4524
|
-
#
|
|
4525
|
-
|
|
4572
|
+
//#region Dependencies
|
|
4573
|
+
#backend = inject(BackendService);
|
|
4574
|
+
#clientCache = inject(ClientCacheService);
|
|
4575
|
+
//#endregion
|
|
4576
|
+
//#region Cache configuration
|
|
4577
|
+
#cacheKey = 'yuv-idm-user-cache';
|
|
4578
|
+
/**
|
|
4579
|
+
* Time-to-live for cached user entries (in milliseconds).
|
|
4580
|
+
*
|
|
4581
|
+
* After this period a cached entry is considered stale and the next
|
|
4582
|
+
* {@link getUserById} call for that user will re-fetch from the backend.
|
|
4583
|
+
*
|
|
4584
|
+
* **Timeline:**
|
|
4585
|
+
* ```
|
|
4586
|
+
* [User fetched] ─────────── 1 hour ──────────► [Entry expires]
|
|
4587
|
+
* cache hit returns instantly next access re-fetches
|
|
4588
|
+
* ```
|
|
4589
|
+
*
|
|
4590
|
+
* @default 1 hour (3600000 milliseconds)
|
|
4591
|
+
*/
|
|
4592
|
+
#cacheTtl = 60 * 60 * 1000;
|
|
4593
|
+
#userCache = {};
|
|
4594
|
+
//#endregion
|
|
4526
4595
|
constructor() {
|
|
4527
|
-
this.#
|
|
4528
|
-
this.#clientCache = inject(ClientCacheService);
|
|
4529
|
-
this.#IDM_USER_CACHE_KEY = 'yuv-idm-user-cache';
|
|
4530
|
-
this.#IDM_USER_CACHE_TTL = 60 * 60 * 1000; // 1 hour in milliseconds
|
|
4531
|
-
this.userCache = {};
|
|
4532
|
-
this.#loadIdmCache();
|
|
4596
|
+
this.#loadCacheFromStorage();
|
|
4533
4597
|
}
|
|
4598
|
+
//#region Public methods
|
|
4534
4599
|
/**
|
|
4535
|
-
*
|
|
4600
|
+
* Searches organization entities (users and/or roles) by a free-text term.
|
|
4601
|
+
*
|
|
4602
|
+
* Returns a unified {@link OrganizationSetEntry} array so consumers
|
|
4603
|
+
* (autocomplete fields, assignment dialogs, permission pickers) can
|
|
4604
|
+
* treat users and roles interchangeably.
|
|
4536
4605
|
*
|
|
4537
|
-
* @param term -
|
|
4538
|
-
* @param targetTypes -
|
|
4606
|
+
* @param term - Free-text search term to filter entities
|
|
4607
|
+
* @param targetTypes - Entity types to include in results (`'user'`, `'role'`, or both)
|
|
4539
4608
|
* @param size - Optional maximum number of results to return
|
|
4540
|
-
* @returns Observable
|
|
4609
|
+
* @returns Observable of matching organization entities
|
|
4610
|
+
*
|
|
4611
|
+
* @example
|
|
4612
|
+
* ```ts
|
|
4613
|
+
* // Search for users and roles matching "admin"
|
|
4614
|
+
* idmService.queryOrganizationEntity('admin', ['user', 'role'], 10)
|
|
4615
|
+
* .subscribe(entries => {
|
|
4616
|
+
* const users = entries.filter(e => e.type === 'user');
|
|
4617
|
+
* const roles = entries.filter(e => e.type === 'role');
|
|
4618
|
+
* });
|
|
4619
|
+
* ```
|
|
4541
4620
|
*/
|
|
4542
4621
|
queryOrganizationEntity(term, targetTypes, size) {
|
|
4543
|
-
const
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4622
|
+
const params = new URLSearchParams();
|
|
4623
|
+
params.set('search', term);
|
|
4624
|
+
if (size) {
|
|
4625
|
+
params.set('size', size.toString());
|
|
4626
|
+
}
|
|
4627
|
+
return this.#backend.get(`/idm/search?${params.toString()}`).pipe(map$1((res) => [
|
|
4547
4628
|
...(targetTypes.includes('user')
|
|
4548
|
-
? res.users.map((
|
|
4549
|
-
id:
|
|
4550
|
-
title: `${
|
|
4629
|
+
? res.users.map((user) => ({
|
|
4630
|
+
id: user.id,
|
|
4631
|
+
title: `${user.lastname}, ${user.firstname} (${user.username})`,
|
|
4551
4632
|
type: 'user'
|
|
4552
4633
|
}))
|
|
4553
4634
|
: []),
|
|
4554
4635
|
...(targetTypes.includes('role')
|
|
4555
|
-
? res.roles.map((
|
|
4556
|
-
id:
|
|
4557
|
-
title:
|
|
4636
|
+
? res.roles.map((role) => ({
|
|
4637
|
+
id: role.name,
|
|
4638
|
+
title: role.name,
|
|
4558
4639
|
type: 'role'
|
|
4559
4640
|
}))
|
|
4560
4641
|
: [])
|
|
@@ -4563,42 +4644,66 @@ class IdmService {
|
|
|
4563
4644
|
/**
|
|
4564
4645
|
* Retrieves available roles, optionally filtered by a search term.
|
|
4565
4646
|
*
|
|
4647
|
+
* Returns an empty array on error to ensure consumers can always
|
|
4648
|
+
* safely iterate the result without null-checking.
|
|
4649
|
+
*
|
|
4566
4650
|
* @param role - Optional search term to filter roles by name
|
|
4567
|
-
* @returns Observable
|
|
4651
|
+
* @returns Observable of role objects containing name and description
|
|
4652
|
+
*
|
|
4653
|
+
* @example
|
|
4654
|
+
* ```ts
|
|
4655
|
+
* // List all roles
|
|
4656
|
+
* idmService.getRoles().subscribe(roles => console.log(roles));
|
|
4657
|
+
*
|
|
4658
|
+
* // Filter roles by name
|
|
4659
|
+
* idmService.getRoles('ADMIN').subscribe(roles => console.log(roles));
|
|
4660
|
+
* ```
|
|
4568
4661
|
*/
|
|
4569
4662
|
getRoles(role) {
|
|
4570
|
-
const
|
|
4571
|
-
|
|
4572
|
-
|
|
4663
|
+
const params = new URLSearchParams();
|
|
4664
|
+
if (role) {
|
|
4665
|
+
params.set('search', role);
|
|
4666
|
+
}
|
|
4667
|
+
return this.#backend.get(`/idm/roles?${params.toString()}`).pipe(catchError$1(() => of([])));
|
|
4573
4668
|
}
|
|
4574
4669
|
/**
|
|
4575
|
-
* Retrieves user
|
|
4576
|
-
*
|
|
4670
|
+
* Retrieves a user by ID with automatic caching.
|
|
4671
|
+
*
|
|
4672
|
+
* Checks the in-memory cache first. If a valid (non-expired) entry exists,
|
|
4673
|
+
* it is returned immediately without an HTTP call. Otherwise, the user is
|
|
4674
|
+
* fetched from the backend, stored in both the in-memory and persistent
|
|
4675
|
+
* cache, and returned as a {@link YuvUser}.
|
|
4577
4676
|
*
|
|
4578
|
-
*
|
|
4579
|
-
*
|
|
4677
|
+
* Returns `null` if the user is not found or the request fails,
|
|
4678
|
+
* so consumers can safely use the result without try/catch.
|
|
4679
|
+
*
|
|
4680
|
+
* @param id - Unique user identifier (UUID)
|
|
4681
|
+
* @returns Observable of the user model, or `null` if not found / on error
|
|
4682
|
+
*
|
|
4683
|
+
* @example
|
|
4684
|
+
* ```ts
|
|
4685
|
+
* idmService.getUserById('550e8400-e29b-41d4-a716-446655440000')
|
|
4686
|
+
* .subscribe(user => {
|
|
4687
|
+
* if (user) {
|
|
4688
|
+
* console.log(user.getFullName());
|
|
4689
|
+
* }
|
|
4690
|
+
* });
|
|
4691
|
+
* ```
|
|
4580
4692
|
*/
|
|
4581
4693
|
getUserById(id) {
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
if (isCacheValid) {
|
|
4585
|
-
return of(new YuvUser(cachedUser.user));
|
|
4586
|
-
}
|
|
4587
|
-
else {
|
|
4588
|
-
return this.#backend.get(`/idm/users/${id}`).pipe(switchMap$1((user) => {
|
|
4589
|
-
this.userCache[id] = {
|
|
4590
|
-
expires: Date.now() + this.#IDM_USER_CACHE_TTL,
|
|
4591
|
-
user
|
|
4592
|
-
};
|
|
4593
|
-
return this.#clientCache.updateCache(this.#IDM_USER_CACHE_KEY, this.userCache).pipe(map$1(() => user));
|
|
4594
|
-
}), map$1((user) => (user ? new YuvUser(user) : null)), catchError$1(() => of(null)));
|
|
4694
|
+
if (id in this.#userCache && Date.now() < this.#userCache[id].expires) {
|
|
4695
|
+
return of(new YuvUser(this.#userCache[id].user));
|
|
4595
4696
|
}
|
|
4697
|
+
return this.#backend.get(`/idm/users/${id}`).pipe(switchMap$1((user) => {
|
|
4698
|
+
this.#userCache[id] = { expires: Date.now() + this.#cacheTtl, user };
|
|
4699
|
+
return this.#clientCache.updateCache(this.#cacheKey, this.#userCache).pipe(map$1(() => user));
|
|
4700
|
+
}), map$1((user) => new YuvUser(user)), catchError$1(() => of(null)));
|
|
4596
4701
|
}
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
}
|
|
4702
|
+
//#endregion
|
|
4703
|
+
//#region Cache
|
|
4704
|
+
#loadCacheFromStorage() {
|
|
4705
|
+
this.#clientCache.getFromCache(this.#cacheKey).subscribe({
|
|
4706
|
+
next: (cache) => (this.#userCache = cache || {})
|
|
4602
4707
|
});
|
|
4603
4708
|
}
|
|
4604
4709
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: IdmService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|