AAE-49653 Making filter key mandatory string

This commit is contained in:
Ehsan Rezaei
2026-08-25 18:02:45 +02:00
parent 416a61fb9e
commit ba916b7c22
8 changed files with 30 additions and 83 deletions
@@ -15,9 +15,6 @@
* limitations under the License.
*/
/**
* Entity types accepted by the `POST /query/v1/count` endpoint.
*/
export const FilterCounterEntityType = {
TASK: 'TASK',
PROCESS_INSTANCE: 'PROCESS_INSTANCE'
@@ -31,7 +28,6 @@ export interface FilterCountersQuerySort {
isProcessVariable: boolean;
}
/** A single query of the batched count request, holding the criteria of one filter. */
export interface FilterCountersQuery {
/** Identifies the query, so its counter can be read back from the response. */
requestId: string;
@@ -41,28 +37,19 @@ export interface FilterCountersQuery {
[criteria: string]: unknown;
}
/**
* Payload of the batched count request, one entry per counter to be resolved.
*/
export type FilterCountersRequest = {
[entityType in FilterCounterEntityType]?: FilterCountersQuery[];
};
/** Shape of a task or process filter the counters are resolved for. Its key is the `requestId`. */
export interface FilterCounterCandidate {
key?: string | null;
key: string;
showCounter?: boolean;
}
/**
* Counts returned by the batched count request, keyed by entity type and then by `requestId`.
* e.g. `{ TASK: { 'my-tasks': 5 }, PROCESS_INSTANCE: { 'running-processes': 5 } }`
*/
export type FilterCounters = {
[entityType in FilterCounterEntityType]?: { [requestId: string]: number };
};
/** Counters of the filters of one entity type, keyed by filter key. */
export interface FilterCountersResult {
[filterKey: string]: number;
}
@@ -133,7 +133,6 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
}
});
/* Read along with the filters, not once they arrive, so both components share one request. */
this.loadFilterCounters(appName, filters$);
}
@@ -141,11 +140,7 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
* Initialize counter collection for filters
*/
initFilterCounters(): void {
this.filters.forEach((filter) => {
if (filter.key) {
this.counters[filter.key] = 0;
}
});
this.filters.forEach((filter) => (this.counters[filter.key] = 0));
}
/**
@@ -283,7 +278,6 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
private loadFilterCounters(appName: string, filters$: Observable<ProcessFilterCloudModel[]>): void {
this.countersSubscription?.unsubscribe();
/* Counters are keyed by filter key, so they are applied once the filters are known. */
this.countersSubscription = combineLatest([
filters$.pipe(catchError(() => EMPTY)),
this.filterCountersCloudService.getFilterCounters(appName, FilterCounterEntityType.PROCESS_INSTANCE)
@@ -293,21 +287,16 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges {
}
private applyFilterCounters(counters: FilterCountersResult): void {
this.filters.forEach((filter) => {
/* A filter without a key holds no request id. */
const filterKey = filter?.showCounter ? filter.key : undefined;
if (!filterKey) {
return;
}
this.filters
.filter((filter) => filter?.showCounter)
.forEach((filter) => {
const counter = counters[filter.key];
if (counter === undefined) {
return;
}
const counter = counters[filterKey];
/* A filter the request left out keeps the counter it holds, rather than showing a wrong one. */
if (counter === undefined) {
return;
}
this.checkIfFilterValuesHasBeenUpdated(filterKey, counter);
this.counters = { ...this.counters, [filterKey]: counter };
});
this.checkIfFilterValuesHasBeenUpdated(filter.key, counter);
this.counters = { ...this.counters, [filter.key]: counter };
});
}
}
@@ -25,7 +25,7 @@ import { ProcessVariableFilterModel } from '../../../models/process-variable-fil
export class ProcessFilterCloudModel {
id: string;
name: string | null;
key: string | null;
key: string;
icon: string | null;
index: number | null;
appName: string | null;
@@ -79,7 +79,7 @@ export class ProcessFilterCloudModel {
this.id = obj.id || Math.random().toString(36).substring(2, 9);
this.name = obj.name || null;
this.key = obj.key || null;
this.key = obj.key;
this.environmentId = obj.environmentId || null;
this.showCounter = obj.showCounter || false;
this.icon = obj.icon || null;
@@ -88,7 +88,6 @@ describe('FilterCountersCloudService', () => {
const counters = (entityType: FilterCounterEntityType, appName = 'mock-app') => firstValueFrom(service.getFilterCounters(appName, entityType));
const taskCounters = (appName = 'mock-app') => counters(FilterCounterEntityType.TASK, appName);
const processCounters = (appName = 'mock-app') => counters(FilterCounterEntityType.PROCESS_INSTANCE, appName);
/** As read when both filter components are on screen. */
const bothCounters = (appName = 'mock-app') =>
firstValueFrom(
combineLatest([
@@ -114,7 +113,6 @@ describe('FilterCountersCloudService', () => {
taskEvents$ = new Subject<EngineEventsResult>();
processEvents$ = new Subject<EngineEventsResult>();
makeGQLQuerySpy = spyOn(notificationCloudService, 'makeGQLQuery');
/* Every entity type holds its own subscription. */
makeGQLQuerySpy.and.callFake((_appName: string, query: string) =>
(query.includes('TASK_CREATED') ? taskEvents$ : processEvents$).asObservable()
);
@@ -146,7 +144,6 @@ describe('FilterCountersCloudService', () => {
});
it('should share the filters with the batched count request', async () => {
/* The filter component holds its subscription while the counters are resolved. */
const subscription = service.getTaskFilters('mock-app').subscribe();
await taskCounters();
subscription.unsubscribe();
@@ -232,13 +229,6 @@ describe('FilterCountersCloudService', () => {
expect(countRequestIds(FilterCounterEntityType.TASK)).toEqual(['queued-tasks']);
});
it('should leave out a filter without a key, since it holds no request id', async () => {
getProcessFiltersSpy.and.returnValue(of([processFilter({ key: null, status: 'RUNNING', showCounter: true })]));
expect(await processCounters()).toEqual({});
expect(postSpy).not.toHaveBeenCalled();
});
it('should resolve the counters of an entity type when the filters of the other one fail to load', async () => {
getProcessFiltersSpy.and.returnValue(throwError(() => new Error('filters failed')));
@@ -330,7 +320,6 @@ describe('FilterCountersCloudService', () => {
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK).subscribe();
tick(0);
/* Opened again, so the first subscription was closed rather than left behind. */
expect(makeGQLQuerySpy).toHaveBeenCalledTimes(2);
}));
@@ -348,11 +337,9 @@ describe('FilterCountersCloudService', () => {
}));
it('should release the filters subscription once nothing reads them', fakeAsync(() => {
/* `TaskFilterCloudService.filters$` never completes, so a subscription left behind would be held. */
const filters$ = new BehaviorSubject(taskFiltersMock);
getTaskListFiltersSpy.and.returnValue(filters$.asObservable());
/* As the filter component does: the filters are held while the counters are read. */
const subscriptions = [
service.getTaskFilters('mock-app').subscribe(),
service.getFilterCounters('mock-app', FilterCounterEntityType.TASK).subscribe()
@@ -47,7 +47,6 @@ interface EngineEventsData {
engineEvents?: TaskCloudEngineEvent[];
}
/** One subscription per entity type, so an app showing one of them is not notified of the other. */
const ENGINE_EVENTS_SUBSCRIPTION_QUERIES: Record<FilterCounterEntityType, string> = {
[FilterCounterEntityType.TASK]: `
subscription {
@@ -264,7 +263,6 @@ export class FilterCountersCloudService extends BaseCloudService {
this.recountTrigger(appName).next();
}
// The filters of an entity type that fails to load are left out, so the other one is still counted.
private getFiltersForCounters(appName: string): Observable<FilterCountersFilters> {
const activeEntityTypes = this.activeEntityTypes(appName);
@@ -305,7 +303,6 @@ export class FilterCountersCloudService extends BaseCloudService {
return this.getFiltersForCounters(appName).pipe(
take(1),
switchMap((filters) => this.fetchFilterCounters(appName, this.buildRequest(filters))),
/* A failed count leaves the counters as they are, rather than breaking the stream. */
catchError(() => of({}))
);
}
@@ -314,7 +311,6 @@ export class FilterCountersCloudService extends BaseCloudService {
return merge(
/* Reads landing in the same task are merged, so both filter components share one request. */
merge(of(undefined), this.recountTrigger(appName)).pipe(debounceTime(0, asapScheduler)),
/* One debounce over every entity type, so a batch of events also results in one request. */
this.eventRecountTrigger(appName).pipe(debounceTime(this.notificationDebounceTime))
);
}
@@ -364,12 +360,12 @@ export class FilterCountersCloudService extends BaseCloudService {
buildQuery: (filter: T) => Omit<FilterCountersQuery, 'requestId'>
): FilterCountersQuery[] {
return (filters ?? [])
.filter((filter) => filter?.showCounter && this.isCounterBatched(filter))
.filter((filter) => filter?.showCounter)
.map((filter) => {
try {
return { ...buildQuery(filter), requestId: filter.key as string };
return { ...buildQuery(filter), requestId: filter.key };
} catch {
/* A malformed filter is left without a counter, so the others still hold one. */
/* Left without a counter, so the other filters still hold theirs. */
return undefined;
}
})
@@ -385,9 +381,4 @@ export class FilterCountersCloudService extends BaseCloudService {
return this.post<FilterCountersRequest, FilterCounters>(queryUrl, request).pipe(map((counters) => counters || {}));
}
// A filter without a key holds no `requestId` its counter could be keyed by.
private isCounterBatched(filter: FilterCounterCandidate): boolean {
return !!filter?.key;
}
}
@@ -108,7 +108,7 @@ export class ServiceTaskFiltersCloudComponent extends BaseTaskFiltersCloudCompon
this.filters.find(
(filter, index) =>
paramFilter.index === index ||
paramFilter.key === filter.key ||
(!!paramFilter.key && paramFilter.key === filter.key) ||
paramFilter.id === filter.id ||
(paramFilter.name && paramFilter.name.toLocaleLowerCase() === this.translationService.instant(filter.name).toLocaleLowerCase())
); // fallback to preserve the previous behavior
@@ -118,7 +118,6 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
}
});
/* Read along with the filters, not once they arrive, so both components share one request. */
this.loadFilterCounters(appName, filters$);
}
@@ -170,7 +169,7 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
this.filters.find(
(filter, index) =>
paramFilter.index === index ||
paramFilter.key === filter.key ||
(!!paramFilter.key && paramFilter.key === filter.key) ||
paramFilter.id === filter.id ||
(paramFilter.name && paramFilter.name.toLocaleLowerCase() === this.translationService.instant(filter.name).toLocaleLowerCase())
); // fallback to preserve the previous behavior
@@ -241,7 +240,6 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
private loadFilterCounters(appName: string, filters$: Observable<TaskFilterCloudModel[]>): void {
this.countersSubscription?.unsubscribe();
/* Counters are keyed by filter key, so they are applied once the filters are known. */
this.countersSubscription = combineLatest([
filters$.pipe(catchError(() => of([]))),
this.filterCountersCloudService.getFilterCounters(appName, FilterCounterEntityType.TASK)
@@ -251,22 +249,17 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
}
private applyFilterCounters(counters: FilterCountersResult): void {
this.filters.forEach((filter) => {
/* A filter without a key holds no request id. */
const filterKey = filter?.showCounter ? filter.key : undefined;
if (!filterKey) {
return;
}
this.filters
.filter((filter) => filter?.showCounter)
.forEach((filter) => {
const counter = counters[filter.key];
if (counter === undefined) {
return;
}
const counter = counters[filterKey];
/* A filter the request left out keeps the counter it holds, rather than showing a wrong one. */
if (counter === undefined) {
return;
}
this.checkIfFilterValuesHasBeenUpdated(filterKey, counter);
this.counters = { ...this.counters, [filterKey]: counter };
});
this.checkIfFilterValuesHasBeenUpdated(filter.key, counter);
this.counters = { ...this.counters, [filter.key]: counter };
});
}
/**
@@ -84,7 +84,7 @@ export class TaskFilterCloudModel {
if (obj) {
this.id = obj.id || Math.random().toString(36).substr(2, 9);
this.name = obj.name || null;
this.key = obj.key || null;
this.key = obj.key;
this.environmentId = obj.environmentId || null;
this.icon = obj.icon || null;
this.index = obj.index || null;