From c3a108679024473ebd53b67d7ea63140d584ef24 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Wed, 8 Jul 2026 15:08:27 +0200 Subject: [PATCH 1/2] TUL/fix: default filter operator to 'equals' for operator-less URLs (#781) A search URL whose filter carries no operator (e.g. a legacy or crawled "/search?f.subject=foo" instead of "f.subject=foo,equals") was forwarded to the backend verbatim. The backend correctly rejects the operator-less filter with HTTP 422 per the DSpace REST Contract, producing an error on every affected search render (dataquest-dev/dspace-customers#781). When reading active filters from the URL in SearchConfigurationService. getCurrentFilters, default the operator to 'equals' (mirroring the existing range-filter branch). Values that already embed an operator - i.e. contain a comma - are left untouched by SearchOptions.toRestUrl, so explicit operators (equals/notequals/contains/authority/...) are preserved. Operator-less legacy URLs now resolve to ",equals" and return results instead of 422. Updated the getCurrentFilters expectations in the SearchConfigurationService and MyDSpaceConfigurationService specs accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../core/shared/search/search-configuration.service.spec.ts | 2 +- src/app/core/shared/search/search-configuration.service.ts | 6 +++++- .../my-dspace-page/my-dspace-configuration.service.spec.ts | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/app/core/shared/search/search-configuration.service.spec.ts b/src/app/core/shared/search/search-configuration.service.spec.ts index d0fbd46d3a2..12729d4f65b 100644 --- a/src/app/core/shared/search/search-configuration.service.spec.ts +++ b/src/app/core/shared/search/search-configuration.service.spec.ts @@ -31,7 +31,7 @@ describe('SearchConfigurationService', () => { }); const backendFilters = [ - new SearchFilter('f.author', ['another value']), + new SearchFilter('f.author', ['another value'], 'equals'), new SearchFilter('f.date', ['[2013 TO 2018]'], 'equals') ]; diff --git a/src/app/core/shared/search/search-configuration.service.ts b/src/app/core/shared/search/search-configuration.service.ts index eed93ae201c..948631e13f1 100644 --- a/src/app/core/shared/search/search-configuration.service.ts +++ b/src/app/core/shared/search/search-configuration.service.ts @@ -196,7 +196,11 @@ export class SearchConfigurationService implements OnDestroy { filters.push(new SearchFilter(realKey, ['[' + min + ' TO ' + max + ']'], 'equals')); } } else { - filters.push(new SearchFilter(key, filterParams[key])); + // Default to the "equals" operator when a filter value carries none (e.g. a legacy or + // crawled URL like "f.subject=foo" instead of "f.subject=foo,equals"). Without this the + // backend rejects the operator-less filter with HTTP 422. Values that already embed an + // operator (contain a comma) keep it - see SearchOptions.toRestUrl. dspace-customers#781. + filters.push(new SearchFilter(key, filterParams[key], 'equals')); } }); return filters; diff --git a/src/app/my-dspace-page/my-dspace-configuration.service.spec.ts b/src/app/my-dspace-page/my-dspace-configuration.service.spec.ts index c388d2d3561..fcbb46c5912 100644 --- a/src/app/my-dspace-page/my-dspace-configuration.service.spec.ts +++ b/src/app/my-dspace-page/my-dspace-configuration.service.spec.ts @@ -30,7 +30,7 @@ describe('MyDSpaceConfigurationService', () => { }); const backendFilters = [ - new SearchFilter('f.namedresourcetype', ['another value']), + new SearchFilter('f.namedresourcetype', ['another value'], 'equals'), new SearchFilter('f.dateSubmitted', ['[2013 TO 2018]'], 'equals') ]; From 3fef503a1b5c1d10ecbabb52ede4acd4fc99fde3 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Wed, 8 Jul 2026 15:19:13 +0200 Subject: [PATCH 2/2] TUL/fix: address review - only default operator when value has none Previously getCurrentFilters set operator='equals' on every non-range filter, even when the URL value already embedded an operator (e.g. "foo,contains"), which could confuse consumers reading filter.operator directly (e.g. CSV export). Now default to 'equals' only when no value carries an operator suffix (contains a comma), matching SearchOptions.toRestUrl. Added a spec fixture for an operator-embedded value asserting the operator stays unset. Per Copilot review on #1368. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../search/search-configuration.service.spec.ts | 7 +++++-- .../shared/search/search-configuration.service.ts | 14 +++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/app/core/shared/search/search-configuration.service.spec.ts b/src/app/core/shared/search/search-configuration.service.spec.ts index 12729d4f65b..0b1804a3e6f 100644 --- a/src/app/core/shared/search/search-configuration.service.spec.ts +++ b/src/app/core/shared/search/search-configuration.service.spec.ts @@ -20,7 +20,8 @@ describe('SearchConfigurationService', () => { const prefixFilter = { 'f.author': ['another value'], 'f.date.min': ['2013'], - 'f.date.max': ['2018'] + 'f.date.max': ['2018'], + 'f.subject': ['some subject,contains'] }; const defaults = new PaginatedSearchOptions({ pagination: Object.assign(new PaginationComponentOptions(), { id: 'page-id', currentPage: 1, pageSize: 20 }), @@ -32,7 +33,9 @@ describe('SearchConfigurationService', () => { const backendFilters = [ new SearchFilter('f.author', ['another value'], 'equals'), - new SearchFilter('f.date', ['[2013 TO 2018]'], 'equals') + new SearchFilter('f.date', ['[2013 TO 2018]'], 'equals'), + // value already carries an operator suffix -> operator must stay unset (not overridden to equals) + new SearchFilter('f.subject', ['some subject,contains']) ]; const routeService = jasmine.createSpyObj('RouteService', { diff --git a/src/app/core/shared/search/search-configuration.service.ts b/src/app/core/shared/search/search-configuration.service.ts index 948631e13f1..8f91e1b4977 100644 --- a/src/app/core/shared/search/search-configuration.service.ts +++ b/src/app/core/shared/search/search-configuration.service.ts @@ -196,11 +196,15 @@ export class SearchConfigurationService implements OnDestroy { filters.push(new SearchFilter(realKey, ['[' + min + ' TO ' + max + ']'], 'equals')); } } else { - // Default to the "equals" operator when a filter value carries none (e.g. a legacy or - // crawled URL like "f.subject=foo" instead of "f.subject=foo,equals"). Without this the - // backend rejects the operator-less filter with HTTP 422. Values that already embed an - // operator (contain a comma) keep it - see SearchOptions.toRestUrl. dspace-customers#781. - filters.push(new SearchFilter(key, filterParams[key], 'equals')); + // Default to the "equals" operator only when a filter value carries none (e.g. a legacy + // or crawled URL like "f.subject=foo" instead of "f.subject=foo,equals"), which the + // backend would otherwise reject with HTTP 422. Values that already embed an operator + // (contain a comma, e.g. "foo,contains") keep it and leave SearchFilter.operator unset, + // matching SearchOptions.toRestUrl and consumers that read filter.operator directly + // (e.g. CSV export). dspace-customers#781. + const values = filterParams[key]; + const operator = values.every((value) => value.includes(',')) ? undefined : 'equals'; + filters.push(new SearchFilter(key, values, operator)); } }); return filters;