From 32c4a821a330a479e409b77eefe59e9b67b9ee5a Mon Sep 17 00:00:00 2001 From: Ehsan Rezaei Date: Thu, 20 Aug 2026 16:27:53 +0200 Subject: [PATCH] AAE-49653 Improving type safety --- .../lib/models/filter-counters-cloud.model.ts | 10 +-- .../process-filters-cloud.component.spec.ts | 14 ++-- .../process-filters-cloud.component.ts | 2 +- .../filter-counters-cloud.service.spec.ts | 78 ++++++++++++------- .../services/filter-counters-cloud.service.ts | 13 +++- .../task-filters-cloud.component.spec.ts | 3 +- 6 files changed, 72 insertions(+), 48 deletions(-) diff --git a/lib/process-services-cloud/src/lib/models/filter-counters-cloud.model.ts b/lib/process-services-cloud/src/lib/models/filter-counters-cloud.model.ts index 4fb8232d2a..9cd1f54b0c 100644 --- a/lib/process-services-cloud/src/lib/models/filter-counters-cloud.model.ts +++ b/lib/process-services-cloud/src/lib/models/filter-counters-cloud.model.ts @@ -41,7 +41,8 @@ export interface FilterCountersQuery { status?: string[]; assignee?: string[]; sort?: FilterCountersQuerySort; - [criteria: string]: any; + /** Every other criteria of the filter the query was built from. */ + [criteria: string]: unknown; } /** @@ -61,13 +62,6 @@ export interface FilterCounterCandidate { showCounter?: boolean; } -/** - * Filters of every entity type the counters are resolved for. - */ -export type FilterCountersFilters = { - [entityType in FilterCounterEntityType]: any[]; -}; - /** * Counts returned by the batched count request, keyed by entity type and then by the `requestId` * of the query the count was resolved for. diff --git a/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters/process-filters-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters/process-filters-cloud.component.spec.ts index ed6d1327be..cbc420112e 100644 --- a/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters/process-filters-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters/process-filters-cloud.component.spec.ts @@ -33,7 +33,7 @@ import { MatIconHarness } from '@angular/material/icon/testing'; import { ActivatedRoute, provideRouter, Router } from '@angular/router'; import { RouterTestingHarness } from '@angular/router/testing'; import { FilterCountersCloudService } from '../../../../services/filter-counters-cloud.service'; -import { FilterCounterEntityType } from '../../../../models/filter-counters-cloud.model'; +import { FilterCounterEntityType, FilterCountersResult } from '../../../../models/filter-counters-cloud.model'; import { ProcessFilterCloudModel } from '../../models/process-filter-cloud.model'; @Component({ selector: 'adf-cloud-dummy', template: '' }) @@ -560,12 +560,12 @@ describe('ProcessFiltersCloudComponent', () => { }); it('should keep the counters in sync with the counters stream', fakeAsync(() => { - const counters$ = new Subject(); + const counters$ = new Subject(); getFilterCountersSpy.and.returnValue(counters$.asObservable()); component.appName = 'mock-app-name'; fixture.detectChanges(); - component.filters = mockProcessFilters.map((filter) => ({ ...filter, showCounter: true })) as any; + component.filters = mockProcessFilters.map((filter) => new ProcessFilterCloudModel({ ...filter, showCounter: true })); counters$.next({ counters: { FakeRunningProcesses: 7 }, batched: true }); @@ -920,12 +920,12 @@ describe('ProcessFiltersCloudComponent', () => { }); it('should keep the counters in sync with the counters stream', fakeAsync(() => { - const counters$ = new Subject(); + const counters$ = new Subject(); getFilterCountersSpy.and.returnValue(counters$.asObservable()); component.appName = 'mock-app-name'; fixture.detectChanges(); - component.filters = mockProcessFilters.map((filter) => ({ ...filter, showCounter: true })) as any; + component.filters = mockProcessFilters.map((filter) => new ProcessFilterCloudModel({ ...filter, showCounter: true })); counters$.next({ counters: { FakeRunningProcesses: 7 }, batched: true }); @@ -934,12 +934,12 @@ describe('ProcessFiltersCloudComponent', () => { })); it('should resolve the counters one filter at a time when the batched endpoint is not available', fakeAsync(() => { - const counters$ = new Subject(); + const counters$ = new Subject(); getFilterCountersSpy.and.returnValue(counters$.asObservable()); component.appName = 'mock-app-name'; fixture.detectChanges(); - component.filters = mockProcessFilters.map((filter) => ({ ...filter, showCounter: true })) as any; + component.filters = mockProcessFilters.map((filter) => new ProcessFilterCloudModel({ ...filter, showCounter: true })); getProcessCounterSpy.calls.reset(); counters$.next({ counters: {}, batched: false }); diff --git a/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters/process-filters-cloud.component.ts b/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters/process-filters-cloud.component.ts index a8ef496ee9..ee8c1eac40 100644 --- a/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters/process-filters-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters/process-filters-cloud.component.ts @@ -143,7 +143,7 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges { this.success.emit(res); this.loadFilterCounters(appName); }, - error: (err: any) => { + error: (err: unknown) => { this.error.emit(err); } }); 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 5c5750eafd..ec62178432 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 @@ -17,7 +17,7 @@ import { fakeAsync, TestBed, tick } from '@angular/core/testing'; import { AppConfigService, NoopAuthModule } from '@alfresco/adf-core'; -import { firstValueFrom, of, Subject, throwError } from 'rxjs'; +import { firstValueFrom, Observable, of, Subject, throwError } from 'rxjs'; import { ApolloTestingModule } from 'apollo-angular/testing'; import { FilterCountersCloudService } from './filter-counters-cloud.service'; import { NotificationCloudService } from './notification-cloud.service'; @@ -27,16 +27,32 @@ import { TaskFilterCloudService } from '../task/task-filters/services/task-filte import { ProcessFilterCloudService } from '../process/process-filters/services/process-filter-cloud.service'; import { TaskFilterCloudModel } from '../task/task-filters/models/filter-cloud.model'; import { ProcessFilterCloudModel } from '../process/process-filters/models/process-filter-cloud.model'; -import { FilterCounterEntityType, FilterCounters } from '../models/filter-counters-cloud.model'; +import { + FilterCounterEntityType, + FilterCounters, + FilterCountersQuery, + FilterCountersRequest, + FilterCountersResult +} from '../models/filter-counters-cloud.model'; import { TaskCloudEngineEvent } from '../models/engine-event-cloud.model'; +import { FetchResult } from '@apollo/client/core'; + +type EngineEventsResult = FetchResult<{ engineEvents?: TaskCloudEngineEvent[] }>; + +interface CountEndpoint { + post: (url: string, request: FilterCountersRequest) => Observable; +} describe('FilterCountersCloudService', () => { let service: FilterCountersCloudService; let notificationCloudService: NotificationCloudService; let appConfigService: AppConfigService; - let engineEvents$: Subject; + let engineEvents$: Subject; let makeGQLQuerySpy: jasmine.Spy; let postSpy: jasmine.Spy; + /** Payload the batched count endpoint was called with. */ + const countRequest = (): FilterCountersRequest => postSpy.calls.mostRecent().args[1]; + const countUrl = (): string => postSpy.calls.mostRecent().args[0]; let getTaskListFiltersSpy: jasmine.Spy; let getProcessFiltersSpy: jasmine.Spy; @@ -45,8 +61,10 @@ describe('FilterCountersCloudService', () => { PROCESS_INSTANCE: { 'running-processes': 5 } }; - const taskFilter = (filter: any) => new TaskFilterCloudModel({ appName: 'mock-app', sort: 'createdDate', order: 'DESC', ...filter }); - const processFilter = (filter: any) => new ProcessFilterCloudModel({ appName: 'mock-app', sort: 'startDate', order: 'DESC', ...filter }); + const taskFilter = (filter: Partial) => + new TaskFilterCloudModel({ appName: 'mock-app', sort: 'createdDate', order: 'DESC', ...filter }); + const processFilter = (filter: Partial) => + new ProcessFilterCloudModel({ appName: 'mock-app', sort: 'startDate', order: 'DESC', ...filter }); const taskFiltersMock = [ taskFilter({ key: 'my-tasks', status: 'ASSIGNED', assignee: 'mock-user', showCounter: true }), @@ -58,7 +76,8 @@ describe('FilterCountersCloudService', () => { processFilter({ key: 'all-processes', status: '', showCounter: false }) ]; - const emitEvent = (eventType = 'TASK_CREATED') => engineEvents$.next({ data: { engineEvents: [{ eventType, entity: {} }] } }); + const emitEvent = (eventType = 'TASK_CREATED') => + engineEvents$.next({ data: { engineEvents: [{ eventType, entity: {} } as TaskCloudEngineEvent] } }); beforeEach(() => { TestBed.configureTestingModule({ @@ -74,9 +93,10 @@ describe('FilterCountersCloudService', () => { appConfigService = TestBed.inject(AppConfigService); appConfigService.config.bpmHost = 'https://fake-bpm-host.com'; - engineEvents$ = new Subject(); - makeGQLQuerySpy = spyOn(notificationCloudService, 'makeGQLQuery').and.returnValue(engineEvents$.asObservable() as any); - postSpy = spyOn(service, 'post').and.returnValue(of(countersMock)); + engineEvents$ = new Subject(); + makeGQLQuerySpy = spyOn(notificationCloudService, 'makeGQLQuery').and.returnValue(engineEvents$.asObservable()); + /* `post` is protected on BaseCloudService, so it is reached through the shape it is spied on. */ + postSpy = spyOn(service as unknown as CountEndpoint, 'post').and.returnValue(of(countersMock)); getTaskListFiltersSpy = spyOn(TestBed.inject(TaskFilterCloudService), 'getTaskListFilters').and.returnValue(of(taskFiltersMock)); getProcessFiltersSpy = spyOn(TestBed.inject(ProcessFilterCloudService), 'getProcessFilters').and.returnValue(of(processFiltersMock)); }); @@ -132,7 +152,7 @@ describe('FilterCountersCloudService', () => { }); it('should resolve the counters of both entity types with a single request', () => { - const results: any[] = []; + const results: FilterCountersResult[] = []; /* Both filter components hold their subscription, so one request resolves the counters of both. */ service.getFilterCounters('mock-app', FilterCounterEntityType.TASK).subscribe((result) => results.push(result)); service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE).subscribe((result) => results.push(result)); @@ -147,23 +167,23 @@ describe('FilterCountersCloudService', () => { it('should send the queries of both entity types to the batched count endpoint', async () => { await taskCounters(); - const [url, body] = postSpy.calls.mostRecent().args; - expect(url).toBe('https://fake-bpm-host.com/mock-app/query/v1/count'); + 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]); }); it('should identify the query of every filter by the key of the filter', async () => { await taskCounters(); - const [, body] = postSpy.calls.mostRecent().args; - expect(body.TASK.map((query: any) => query.requestId)).toEqual(['my-tasks', 'queued-tasks']); - expect(body.PROCESS_INSTANCE.map((query: any) => query.requestId)).toEqual(['running-processes']); + 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']); }); it('should send the criteria of every filter along with its request id', async () => { await taskCounters(); - const [, body] = postSpy.calls.mostRecent().args; + const body = countRequest(); expect(body.TASK[0]).toEqual({ requestId: 'my-tasks', status: ['ASSIGNED'], @@ -175,8 +195,8 @@ describe('FilterCountersCloudService', () => { it('should not send the filters without a counter enabled', async () => { await taskCounters(); - const [, body] = postSpy.calls.mostRecent().args; - expect(body.TASK.map((query: any) => query.requestId)).not.toContain('completed-tasks'); + const body = countRequest(); + expect(body.TASK.map((query: FilterCountersQuery) => query.requestId)).not.toContain('completed-tasks'); }); it('should send the query of a filter targeting every status', async () => { @@ -184,8 +204,8 @@ describe('FilterCountersCloudService', () => { await processCounters(); - const [, body] = postSpy.calls.mostRecent().args; - expect(body.PROCESS_INSTANCE.map((query: any) => query.requestId)).toEqual(['all-processes']); + const body = countRequest(); + expect(body.PROCESS_INSTANCE.map((query: FilterCountersQuery) => query.requestId)).toEqual(['all-processes']); }); it('should omit an entity type without filters with a counter enabled', async () => { @@ -193,7 +213,7 @@ describe('FilterCountersCloudService', () => { await taskCounters(); - const [, body] = postSpy.calls.mostRecent().args; + const body = countRequest(); expect(body.PROCESS_INSTANCE).toBeUndefined(); }); @@ -204,8 +224,8 @@ describe('FilterCountersCloudService', () => { await taskCounters(); - const [, body] = postSpy.calls.mostRecent().args; - expect(body.TASK.map((query: any) => query.requestId)).toEqual(['queued-tasks']); + const body = countRequest(); + expect(body.TASK.map((query: FilterCountersQuery) => query.requestId)).toEqual(['queued-tasks']); }); it('should leave out a filter without a key, since it holds no request id', async () => { @@ -213,7 +233,7 @@ describe('FilterCountersCloudService', () => { await processCounters(); - const [, body] = postSpy.calls.mostRecent().args; + const body = countRequest(); expect(body.PROCESS_INSTANCE).toBeUndefined(); }); @@ -222,8 +242,8 @@ describe('FilterCountersCloudService', () => { await taskCounters(); - const [, body] = postSpy.calls.mostRecent().args; - expect(body.TASK.map((query: any) => query.requestId)).toEqual(['my-tasks', 'queued-tasks']); + const body = countRequest(); + expect(body.TASK.map((query: FilterCountersQuery) => query.requestId)).toEqual(['my-tasks', 'queued-tasks']); expect(body.PROCESS_INSTANCE).toBeUndefined(); }); @@ -274,7 +294,7 @@ describe('FilterCountersCloudService', () => { describe('refreshFilterCounters', () => { it('should resolve the counters again with a single request', fakeAsync(() => { - const results: any[] = []; + const results: FilterCountersResult[] = []; service.getFilterCounters('mock-app', FilterCounterEntityType.TASK).subscribe((result) => results.push(result)); service.getFilterCounters('mock-app', FilterCounterEntityType.PROCESS_INSTANCE).subscribe(); @@ -363,7 +383,7 @@ describe('FilterCountersCloudService', () => { })); it('should emit the counters resolved for the batch of events', fakeAsync(() => { - const results: any[] = []; + const results: FilterCountersResult[] = []; service.getFilterCounters('mock-app', FilterCounterEntityType.TASK).subscribe((result) => results.push(result)); postSpy.and.returnValue(of({ TASK: { 'my-tasks': 9 } })); @@ -375,7 +395,7 @@ describe('FilterCountersCloudService', () => { })); it('should not subscribe to the engine events when notifications are disabled', fakeAsync(() => { - spyOn(appConfigService, 'get').and.callFake((key: string, defaultValue: any) => (key === 'notifications' ? false : defaultValue)); + appConfigService.config.notifications = false; service.getFilterCounters('mock-app', FilterCounterEntityType.TASK).subscribe(); tick(3000); 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 7ddf6e7d09..c7d71f8eb0 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 @@ -33,11 +33,11 @@ import { FilterCounterCandidate, FilterCounterEntityType, FilterCounters, - FilterCountersFilters, FilterCountersQuery, 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 @@ -45,6 +45,15 @@ import { */ const BATCHED_COUNTERS_UNAVAILABLE_STATUSES = [404, 501]; +/** Filters of both entity types, to resolve the counters of both filter components with one request. */ +interface FilterCountersFilters { + [FilterCounterEntityType.TASK]: TaskFilterCloudModel[]; + [FilterCounterEntityType.PROCESS_INSTANCE]: ProcessFilterCloudModel[]; +} + +/** Payload of the engine event subscription. */ +type EngineEventsResult = FetchResult<{ engineEvents?: TaskCloudEngineEvent[] }>; + const FILTER_COUNTERS_EVENT_SUBSCRIPTION_QUERY = ` subscription { engineEvents(eventType: [ @@ -157,7 +166,7 @@ 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((events: any) => (events?.data?.engineEvents ?? []) as TaskCloudEngineEvent[]), + map((result: EngineEventsResult) => result.data?.engineEvents ?? []), debounceTime(this.notificationDebounceTime), shareReplay({ bufferSize: 1, refCount: true }) ); diff --git a/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters/task-filters-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters/task-filters-cloud.component.spec.ts index 338c71e4cd..8a1d598393 100644 --- a/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters/task-filters-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/task/task-filters/components/task-filters/task-filters-cloud.component.spec.ts @@ -37,6 +37,7 @@ import { ActivatedRoute, provideRouter, Router } from '@angular/router'; import { RouterTestingHarness } from '@angular/router/testing'; import { FilterCountersCloudService } from '../../../../services/filter-counters-cloud.service'; import { FilterCounterEntityType } from '../../../../models/filter-counters-cloud.model'; +import { TaskCloudEngineEvent } from '../../../../models/engine-event-cloud.model'; @Component({ selector: 'adf-cloud-dummy', template: '' }) class DummyComponent {} @@ -334,7 +335,7 @@ describe('TaskFiltersCloudComponent', () => { }); it('should emit the events of the debounced batch', fakeAsync(() => { - const events$ = new Subject(); + const events$ = new Subject(); getEngineEventsSpy.and.returnValue(events$.asObservable()); const filterCounterUpdatedSpy = spyOn(component.filterCounterUpdated, 'emit'); component.appName = 'my-app-1';