From 5a198860e4501fa43a01c7483a9a5b1877430413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ricks?= Date: Thu, 16 Jul 2026 11:23:05 +0200 Subject: [PATCH 1/2] Change: Extract parsing a filter from a response element into new method The former fromElement method of the Filter model did mix parsing of different XML responses. First for the `` responses and second for all filters included in all other get responses like ``. --- src/gmp/models/__tests__/filter.test.ts | 28 ++++++-- src/gmp/models/filter.ts | 92 +++++++++++++++++++++---- 2 files changed, 101 insertions(+), 19 deletions(-) diff --git a/src/gmp/models/__tests__/filter.test.ts b/src/gmp/models/__tests__/filter.test.ts index a5769503d1..188376c998 100644 --- a/src/gmp/models/__tests__/filter.test.ts +++ b/src/gmp/models/__tests__/filter.test.ts @@ -263,7 +263,7 @@ describe('Filter parse from string tests', () => { }); }); -describe('Filter parse from keywords', () => { +describe('Filter fromResponseElement', () => { test('should parse approx relation without column', () => { const elem = { keywords: { @@ -276,7 +276,7 @@ describe('Filter parse from keywords', () => { ], }, }; - const filter = Filter.fromElement(elem); + const filter = Filter.fromResponseElement(elem); expect(filter.toFilterString()).toEqual('~abc'); }); @@ -307,7 +307,7 @@ describe('Filter parse from keywords', () => { ], }, }; - let filter = Filter.fromElement(elem); + let filter = Filter.fromResponseElement(elem); expect(filter.toFilterString()).toEqual('~abc and not ~def'); elem = { @@ -351,7 +351,7 @@ describe('Filter parse from keywords', () => { ], }, }; - filter = Filter.fromElement(elem); + filter = Filter.fromResponseElement(elem); expect(filter.toFilterString()).toEqual( '~abc and not ~def rows=10 first=1 sort=name', ); @@ -395,7 +395,7 @@ describe('Filter parse from keywords', () => { }, }; - const filter = Filter.fromElement(elem); + const filter = Filter.fromResponseElement(elem); const filterString = 'severity>3.9 and severity<7 first=1 rows=10 sort=name'; expect(filter.toFilterString()).toEqual(filterString); @@ -416,9 +416,20 @@ describe('Filter parse from keywords', () => { ], }, }; - const filter = Filter.fromElement(elem); + const filter = Filter.fromResponseElement(elem); expect(filter.toFilterString()).toEqual('_foo=abc'); }); + + test('should parse id', () => { + const filter1 = Filter.fromResponseElement(); + expect(filter1.id).toBeUndefined(); + + const filter2 = Filter.fromResponseElement({_id: '123'}); + expect(filter2.id).toBe('123'); + + const filter3 = Filter.fromResponseElement({_id: UNKNOWN_FILTER_ID}); + expect(filter3.id).toBeUndefined(); + }); }); describe('Filter set', () => { @@ -1613,6 +1624,7 @@ describe('should lower the case of capitalized keywords', () => { 'severity>3.9 and qod_min=70 rows=14', ); }); + test('should do the same for filters from arrays', () => { const element = { keywords: { @@ -1655,15 +1667,17 @@ describe('should lower the case of capitalized keywords', () => { ], }, }; - const filter = Filter.fromElement(element); + const filter = Filter.fromResponseElement(element); expect(filter.toFilterString()).toEqual( '~abc and not ~def rows=10 first=1 sort=name', ); }); + test('a more wacky scenario', () => { const filter1 = Filter.fromString('~abc SorT=name'); expect(filter1.toFilterString()).toEqual('~abc sort=name'); }); + test('just a value', () => { const filter2 = Filter.fromString('~AbC'); expect(filter2.toFilterString()).toEqual('~AbC'); diff --git a/src/gmp/models/filter.ts b/src/gmp/models/filter.ts index bb7569447b..91cfeb29df 100644 --- a/src/gmp/models/filter.ts +++ b/src/gmp/models/filter.ts @@ -11,6 +11,7 @@ import Model, {type ModelElement, type ModelProperties} from 'gmp/models/model'; import {parseInt} from 'gmp/parser'; import {map} from 'gmp/utils/array'; import {isDefined, isString, isArray, hasValue} from 'gmp/utils/identity'; +import {isEmpty} from 'gmp/utils/string'; export interface FilterKeyword { column?: string; @@ -18,18 +19,73 @@ export interface FilterKeyword { value?: string; } +/** + * XML Structure of a filter model element as returned by `` + * queries. + * + * Example XML Structure: + * ```xml + * + * + * ... + * apply_overrides=0 min_qod=70 sort=name first=1 rows=1 + * + * + * ``` + */ export interface FilterModelElement extends ModelElement { alerts?: { alert: ModelElement[]; }; filter_type?: string; - keywords?: { - keyword?: FilterKeyword | FilterKeyword[]; - }; term?: string; } +/** + * XML Structure of a filter response element + * as returned by all `` queries, for example ``. + * + * Example XML Structure: + * ```xml + * + * + * ... + * + * + * apply_overrides=0 min_qod=70 sort=name first=1 rows=1 + * + * + * apply_overrides + * = + * 0 + * + * + * min_qod + * = + * 70 + * + * + * sort + * = + * name + * + * + * first + * = + * 1 + * + * + * rows + * = + * 1 + * + * + * + * + * ``` + */ export interface FilterResponseElement { + _id?: string; keywords?: { keyword?: FilterKeyword | FilterKeyword[]; }; @@ -201,15 +257,7 @@ class Filter extends EntityModel implements FilterType { if (ret.id === UNKNOWN_FILTER_ID) { ret.id = undefined; } - if (isDefined(element.keywords)) { - ret.terms = map( - element.keywords.keyword, - ({relation, value, column: key}: FilterKeyword) => - new FilterTerm(convert(key, value, relation)), - ); - // @ts-expect-error - delete ret.keywords; - } else if (isDefined(element.term)) { + if (isDefined(element.term)) { ret.terms = parseFilterTermsFromString(element.term); // ret.term should not be part of the public api @@ -230,6 +278,26 @@ class Filter extends EntityModel implements FilterType { return new Filter(ret); } + static fromResponseElement(element: FilterResponseElement = {}): FilterType { + const id = + !isEmpty(element._id) && element._id !== UNKNOWN_FILTER_ID + ? element._id + : undefined; + + let terms: FilterTerm[] = []; + if (isDefined(element.keywords)) { + terms = map( + element.keywords.keyword, + ({relation, value, column: key}: FilterKeyword) => + new FilterTerm(convert(key, value, relation)), + ); + } else if (isDefined(element.term)) { + terms = parseFilterTermsFromString(element.term); + } + + return new Filter({id, terms}); + } + /** * @private * From f98ce00e5cf8b3a42d7f4ef33bde1880045cd33e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ricks?= Date: Thu, 16 Jul 2026 11:30:39 +0200 Subject: [PATCH 2/2] Parse filter from response element where necessary --- src/gmp/collection/parser.ts | 2 +- src/gmp/commands/__tests__/entities.test.ts | 2 +- src/gmp/commands/__tests__/entity.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gmp/collection/parser.ts b/src/gmp/collection/parser.ts index 598d05104a..11cbdad29a 100644 --- a/src/gmp/collection/parser.ts +++ b/src/gmp/collection/parser.ts @@ -154,7 +154,7 @@ export function parseInfoCounts(response: InfoWithCounts) { } export function parseFilter(element: FilterElement): FilterType { - return Filter.fromElement(element.filters); + return Filter.fromResponseElement(element.filters); } export function parseCounts( diff --git a/src/gmp/commands/__tests__/entities.test.ts b/src/gmp/commands/__tests__/entities.test.ts index 6974b485cc..08ec5d6086 100644 --- a/src/gmp/commands/__tests__/entities.test.ts +++ b/src/gmp/commands/__tests__/entities.test.ts @@ -63,7 +63,7 @@ describe('EntitiesCommand tests', () => { }); test('should prefer filter_id over filter parameter', async () => { - const filter = Filter.fromElement({ + const filter = Filter.fromResponseElement({ _id: 'bar', keywords: { keyword: {relation: '=', value: 'bar', column: 'foo'}, diff --git a/src/gmp/commands/__tests__/entity.test.ts b/src/gmp/commands/__tests__/entity.test.ts index 1a46aeb874..fb9eef7d22 100644 --- a/src/gmp/commands/__tests__/entity.test.ts +++ b/src/gmp/commands/__tests__/entity.test.ts @@ -112,7 +112,7 @@ describe('EntityCommand tests', () => { }); test('should get entity and prefer filter_id over filter parameter', async () => { - const filter = Filter.fromElement({ + const filter = Filter.fromResponseElement({ _id: 'bar', keywords: { keyword: {relation: '=', value: 'bar', column: 'foo'},