From 85a751f8c80edfa53c231e92592f8b48239dc89b Mon Sep 17 00:00:00 2001 From: Ehsan Rezaei Date: Fri, 21 Aug 2026 11:49:14 +0200 Subject: [PATCH] AAE-49653 fixed the DI coupling and type safety --- .../filter-counters-cloud.service.spec.ts | 70 +++++++++++++------ .../services/filter-counters-cloud.service.ts | 28 +++++--- .../services/notification-cloud.service.ts | 14 +++- 3 files changed, 78 insertions(+), 34 deletions(-) diff --git a/lib/process-services-cloud/src/lib/services/filter-counters-cloud.service.spec.ts b/lib/process-services-cloud/src/lib/services/filter-counters-cloud.service.spec.ts index ec62178432..57ac73e65a 100644 --- a/lib/process-services-cloud/src/lib/services/filter-counters-cloud.service.spec.ts +++ b/lib/process-services-cloud/src/lib/services/filter-counters-cloud.service.spec.ts @@ -53,6 +53,9 @@ describe('FilterCountersCloudService', () => { /** Payload the batched count endpoint was called with. */ const countRequest = (): FilterCountersRequest => postSpy.calls.mostRecent().args[1]; const countUrl = (): string => postSpy.calls.mostRecent().args[0]; + /** Queries the batched count endpoint was called with for an entity type. */ + const countQueries = (entityType: FilterCounterEntityType): FilterCountersQuery[] => countRequest()[entityType] ?? []; + const countRequestIds = (entityType: FilterCounterEntityType): string[] => countQueries(entityType).map((query) => query.requestId); let getTaskListFiltersSpy: jasmine.Spy; let getProcessFiltersSpy: jasmine.Spy; @@ -168,23 +171,20 @@ describe('FilterCountersCloudService', () => { await taskCounters(); expect(countUrl()).toBe('https://fake-bpm-host.com/mock-app/query/v1/count'); - const body = countRequest(); - expect(Object.keys(body)).toEqual([FilterCounterEntityType.TASK, FilterCounterEntityType.PROCESS_INSTANCE]); + expect(Object.keys(countRequest())).toEqual([FilterCounterEntityType.TASK, FilterCounterEntityType.PROCESS_INSTANCE]); }); it('should identify the query of every filter by the key of the filter', async () => { await taskCounters(); - const body = countRequest(); - expect(body.TASK.map((query: FilterCountersQuery) => query.requestId)).toEqual(['my-tasks', 'queued-tasks']); - expect(body.PROCESS_INSTANCE.map((query: FilterCountersQuery) => query.requestId)).toEqual(['running-processes']); + expect(countRequestIds(FilterCounterEntityType.TASK)).toEqual(['my-tasks', 'queued-tasks']); + expect(countRequestIds(FilterCounterEntityType.PROCESS_INSTANCE)).toEqual(['running-processes']); }); it('should send the criteria of every filter along with its request id', async () => { await taskCounters(); - const body = countRequest(); - expect(body.TASK[0]).toEqual({ + expect(countQueries(FilterCounterEntityType.TASK)[0]).toEqual({ requestId: 'my-tasks', status: ['ASSIGNED'], assignee: ['mock-user'], @@ -195,8 +195,7 @@ describe('FilterCountersCloudService', () => { it('should not send the filters without a counter enabled', async () => { await taskCounters(); - const body = countRequest(); - expect(body.TASK.map((query: FilterCountersQuery) => query.requestId)).not.toContain('completed-tasks'); + expect(countRequestIds(FilterCounterEntityType.TASK)).not.toContain('completed-tasks'); }); it('should send the query of a filter targeting every status', async () => { @@ -204,8 +203,7 @@ describe('FilterCountersCloudService', () => { await processCounters(); - const body = countRequest(); - expect(body.PROCESS_INSTANCE.map((query: FilterCountersQuery) => query.requestId)).toEqual(['all-processes']); + expect(countRequestIds(FilterCounterEntityType.PROCESS_INSTANCE)).toEqual(['all-processes']); }); it('should omit an entity type without filters with a counter enabled', async () => { @@ -213,8 +211,7 @@ describe('FilterCountersCloudService', () => { await taskCounters(); - const body = countRequest(); - expect(body.PROCESS_INSTANCE).toBeUndefined(); + expect(countRequest().PROCESS_INSTANCE).toBeUndefined(); }); it('should leave out a filter the query cannot be built for', async () => { @@ -224,8 +221,7 @@ describe('FilterCountersCloudService', () => { await taskCounters(); - const body = countRequest(); - expect(body.TASK.map((query: FilterCountersQuery) => query.requestId)).toEqual(['queued-tasks']); + expect(countRequestIds(FilterCounterEntityType.TASK)).toEqual(['queued-tasks']); }); it('should leave out a filter without a key, since it holds no request id', async () => { @@ -233,8 +229,7 @@ describe('FilterCountersCloudService', () => { await processCounters(); - const body = countRequest(); - expect(body.PROCESS_INSTANCE).toBeUndefined(); + expect(countRequest().PROCESS_INSTANCE).toBeUndefined(); }); it('should resolve the counters of an entity type when the filters of the other one fail to load', async () => { @@ -242,9 +237,8 @@ describe('FilterCountersCloudService', () => { await taskCounters(); - const body = countRequest(); - expect(body.TASK.map((query: FilterCountersQuery) => query.requestId)).toEqual(['my-tasks', 'queued-tasks']); - expect(body.PROCESS_INSTANCE).toBeUndefined(); + expect(countRequestIds(FilterCounterEntityType.TASK)).toEqual(['my-tasks', 'queued-tasks']); + expect(countRequest().PROCESS_INSTANCE).toBeUndefined(); }); it('should resolve no counter when no filter has a counter enabled', async () => { @@ -311,6 +305,42 @@ describe('FilterCountersCloudService', () => { }); }); + describe('when only one of the two filter families is wired', () => { + /* An app holding only the task filters provides the task preferences service alone. */ + const configureTasksOnly = () => { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + imports: [NoopAuthModule, ApolloTestingModule], + providers: [{ provide: TASK_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }] + }); + + const tasksOnlyService = TestBed.inject(FilterCountersCloudService); + TestBed.inject(AppConfigService).config.bpmHost = 'https://fake-bpm-host.com'; + spyOn(TestBed.inject(NotificationCloudService), 'makeGQLQuery').and.returnValue(new Subject().asObservable()); + spyOn(TestBed.inject(TaskFilterCloudService), 'getTaskListFilters').and.returnValue(of(taskFiltersMock)); + postSpy = spyOn(tasksOnlyService as unknown as CountEndpoint, 'post').and.returnValue(of(countersMock)); + + return tasksOnlyService; + }; + + it('should resolve the counters of the wired family', async () => { + const tasksOnlyService = configureTasksOnly(); + + const result = await firstValueFrom(tasksOnlyService.getFilterCounters('mock-app', FilterCounterEntityType.TASK)); + + expect(result).toEqual({ counters: { 'my-tasks': 5, 'queued-tasks': 0 }, batched: true }); + }); + + it('should leave the filters of the family that is not wired out of the request', async () => { + const tasksOnlyService = configureTasksOnly(); + + await firstValueFrom(tasksOnlyService.getFilterCounters('mock-app', FilterCounterEntityType.TASK)); + + expect(countRequestIds(FilterCounterEntityType.TASK)).toEqual(['my-tasks', 'queued-tasks']); + expect(countRequest().PROCESS_INSTANCE).toBeUndefined(); + }); + }); + describe('getEngineEvents', () => { it('should return EMPTY when appName is not set', () => { let completed = false; diff --git a/lib/process-services-cloud/src/lib/services/filter-counters-cloud.service.ts b/lib/process-services-cloud/src/lib/services/filter-counters-cloud.service.ts index 60354b07ba..13bc1c3477 100644 --- a/lib/process-services-cloud/src/lib/services/filter-counters-cloud.service.ts +++ b/lib/process-services-cloud/src/lib/services/filter-counters-cloud.service.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { inject, Injectable } from '@angular/core'; +import { inject, Injectable, Injector } from '@angular/core'; import { combineLatest, defer, EMPTY, merge, Observable, of, Subject } from 'rxjs'; import { catchError, debounceTime, map, shareReplay, switchMap, take } from 'rxjs/operators'; import { BaseCloudService } from './base-cloud.service'; @@ -37,7 +37,6 @@ import { FilterCountersRequest, FilterCountersResult } from '../models/filter-counters-cloud.model'; -import { FetchResult } from '@apollo/client/core'; /** * Single subscription covering both the task and the process engine events, so that a batch of @@ -51,8 +50,10 @@ interface FilterCountersFilters { [FilterCounterEntityType.PROCESS_INSTANCE]: ProcessFilterCloudModel[]; } -/** Payload of the engine event subscription. */ -type EngineEventsResult = FetchResult<{ engineEvents?: TaskCloudEngineEvent[] }>; +/** Data selected by the engine event subscription. */ +interface EngineEventsData { + engineEvents?: TaskCloudEngineEvent[]; +} const FILTER_COUNTERS_EVENT_SUBSCRIPTION_QUERY = ` subscription { @@ -87,11 +88,13 @@ const FILTER_COUNTERS_EVENT_SUBSCRIPTION_QUERY = ` @Injectable({ providedIn: 'root' }) export class FilterCountersCloudService extends BaseCloudService { private readonly notificationCloudService = inject(NotificationCloudService); - - private readonly taskFilterCloudService = inject(TaskFilterCloudService); - private readonly processFilterCloudService = inject(ProcessFilterCloudService); private readonly taskListCloudService = inject(TaskListCloudService); private readonly processListCloudService = inject(ProcessListCloudService); + /** + * The filter services are resolved on demand, so that an app holding only one of the two filter + * components is not forced to provide the preferences service of the other one. + */ + private readonly injector = inject(Injector); private readonly eventsPerApp = new Map>(); private readonly refreshPerApp = new Map>(); @@ -112,7 +115,7 @@ export class FilterCountersCloudService extends BaseCloudService { * @returns Task filters of the app */ getTaskFilters(appName: string): Observable { - return this.shareFilters(this.taskFiltersPerApp, appName, () => this.taskFilterCloudService.getTaskListFilters(appName)); + return this.shareFilters(this.taskFiltersPerApp, appName, () => this.injector.get(TaskFilterCloudService).getTaskListFilters(appName)); } /** @@ -123,7 +126,7 @@ export class FilterCountersCloudService extends BaseCloudService { * @returns Process filters of the app */ getProcessFilters(appName: string): Observable { - return this.shareFilters(this.processFiltersPerApp, appName, () => this.processFilterCloudService.getProcessFilters(appName)); + return this.shareFilters(this.processFiltersPerApp, appName, () => this.injector.get(ProcessFilterCloudService).getProcessFilters(appName)); } /** @@ -165,9 +168,12 @@ export class FilterCountersCloudService extends BaseCloudService { let events$ = this.eventsPerApp.get(appName); if (!events$) { - events$ = defer(() => this.notificationCloudService.makeGQLQuery(appName, FILTER_COUNTERS_EVENT_SUBSCRIPTION_QUERY)).pipe( - map((result: EngineEventsResult) => result.data?.engineEvents ?? []), + events$ = defer(() => + this.notificationCloudService.makeGQLQuery(appName, FILTER_COUNTERS_EVENT_SUBSCRIPTION_QUERY) + ).pipe( + map((result) => result.data?.engineEvents ?? []), debounceTime(this.notificationDebounceTime), + catchError(() => EMPTY), shareReplay({ bufferSize: 1, refCount: true }) ); this.eventsPerApp.set(appName, events$); diff --git a/lib/process-services-cloud/src/lib/services/notification-cloud.service.ts b/lib/process-services-cloud/src/lib/services/notification-cloud.service.ts index ae0cfe68fc..612fd8be58 100644 --- a/lib/process-services-cloud/src/lib/services/notification-cloud.service.ts +++ b/lib/process-services-cloud/src/lib/services/notification-cloud.service.ts @@ -15,8 +15,9 @@ * limitations under the License. */ -import { gql } from '@apollo/client/core'; +import { FetchResult, gql } from '@apollo/client/core'; import { Injectable, inject } from '@angular/core'; +import { Observable } from 'rxjs'; import { WebSocketService } from './web-socket.service'; @Injectable({ providedIn: 'root' @@ -24,8 +25,15 @@ import { WebSocketService } from './web-socket.service'; export class NotificationCloudService { private readonly webSocketService = inject(WebSocketService); - makeGQLQuery(appName: string, gqlQuery: string) { - return this.webSocketService.getSubscription({ + /** + * Opens a GraphQL subscription over the notifications of an app. + * + * @param appName Name of the target app + * @param gqlQuery Subscription to make + * @returns Results of the subscription, holding the data the subscription selected + */ + makeGQLQuery(appName: string, gqlQuery: string): Observable> { + return this.webSocketService.getSubscription({ apolloClientName: appName, wsUrl: `${appName}/notifications`, httpUrl: `${appName}/notifications/v2/ws/graphql`,