AAE-49653 fixed the DI coupling and type safety

This commit is contained in:
Ehsan Rezaei
2026-08-21 11:49:14 +02:00
parent 5c4abba87c
commit 85a751f8c8
3 changed files with 78 additions and 34 deletions
@@ -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<EngineEventsResult>().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;
@@ -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<string, Observable<TaskCloudEngineEvent[]>>();
private readonly refreshPerApp = new Map<string, Subject<void>>();
@@ -112,7 +115,7 @@ export class FilterCountersCloudService extends BaseCloudService {
* @returns Task filters of the app
*/
getTaskFilters(appName: string): Observable<TaskFilterCloudModel[]> {
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<ProcessFilterCloudModel[]> {
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<EngineEventsData>(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$);
@@ -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<T = unknown>(appName: string, gqlQuery: string): Observable<FetchResult<T>> {
return this.webSocketService.getSubscription<T>({
apolloClientName: appName,
wsUrl: `${appName}/notifications`,
httpUrl: `${appName}/notifications/v2/ws/graphql`,