diff --git a/src/common/SearchQueryUtils.ts b/src/common/SearchQueryUtils.ts index 09d612ac..dfee3bcb 100644 --- a/src/common/SearchQueryUtils.ts +++ b/src/common/SearchQueryUtils.ts @@ -7,6 +7,7 @@ import { SearchQueryTypes, SomeOfSearchQuery, TextSearch, + TextSearchQueryMatchTypes, TextSearchQueryTypes } from './entities/SearchQueryDTO'; import {SearchQueryParser} from './SearchQueryParser'; @@ -109,20 +110,24 @@ export const SearchQueryUtils = { query.type === undefined || (query.type === SearchQueryTypes.any_text && !(query as TextSearch).value); }, - // Recursively strip negate:false so that optional false flags do not break validation + // Recursively strip negate:false and matchType:like so that optional/default flags do not break validation // eslint-disable-next-line @typescript-eslint/no-explicit-any - stripFalseNegate: (val: any): any => { + stripDefault: (val: any): any => { if (Array.isArray(val)) { - return val.map((v) => SearchQueryUtils.stripFalseNegate(v)); + return val.map((v) => SearchQueryUtils.stripDefault(v)); } if (val && typeof val === 'object') { const out: Record = {}; for (const k of Object.keys(val)) { - const v = SearchQueryUtils.stripFalseNegate(val[k]); + const v = SearchQueryUtils.stripDefault(val[k]); if (k === 'negate' && v === false) { // drop negate:false continue; } + if (k === 'matchType' && v === TextSearchQueryMatchTypes.like) { + // drop matchType:like (this is the default) + continue; + } // keep everything else, including negate:true and undefined-handled by equalsFilter out[k] = v; } @@ -139,9 +144,9 @@ export const SearchQueryUtils = { const sp = new SearchQueryParser(); try { const parsed = sp.parse(sp.stringify(query)); - const normParsed = SearchQueryUtils.stripFalseNegate(parsed) as SearchQueryDTO; - const normQuery = SearchQueryUtils.stripFalseNegate(query) as SearchQueryDTO; - if (!Utils.equalsFilter(normParsed, normQuery)) { + const normParsed = SearchQueryUtils.stripDefault(parsed) as SearchQueryDTO; + const normQuery = SearchQueryUtils.stripDefault(query) as SearchQueryDTO; + if (!Utils.equalsFilter(normParsed, normQuery) || !Utils.equalsFilter(normQuery, normParsed)) { throw new Error( `${what} is not valid. Expected: ${JSON.stringify(parsed)} to equal: ${JSON.stringify(query)}` ); @@ -179,7 +184,7 @@ export const SearchQueryUtils = { if (!query) { return ''; } - query=SearchQueryUtils.stripFalseNegate(query); + query=SearchQueryUtils.stripDefault(query); // eslint-disable-next-line @typescript-eslint/no-explicit-any const shorten = (obj: any): any => { if (Array.isArray(obj)) { diff --git a/src/common/entities/SearchQueryDTO.ts b/src/common/entities/SearchQueryDTO.ts index aef1954b..c01f8c40 100644 --- a/src/common/entities/SearchQueryDTO.ts +++ b/src/common/entities/SearchQueryDTO.ts @@ -63,7 +63,7 @@ export const MetadataSearchQueryTypes = [ export enum TextSearchQueryMatchTypes { exact_match = 1, - like = 2, + like = 2, // default value } export interface SearchQueryDTO { diff --git a/src/frontend/app/ui/gallery/search/search-query-parser.service.ts b/src/frontend/app/ui/gallery/search/search-query-parser.service.ts index 5f4ebfe8..61a3844b 100644 --- a/src/frontend/app/ui/gallery/search/search-query-parser.service.ts +++ b/src/frontend/app/ui/gallery/search/search-query-parser.service.ts @@ -16,6 +16,6 @@ export class SearchQueryParserService { } stringify(query: SearchQueryDTO): string { - return this.parser.stringify(SearchQueryUtils.stripFalseNegate(query)); + return this.parser.stringify(SearchQueryUtils.stripDefault(query)); } } diff --git a/src/frontend/app/ui/gallery/search/search.gallery.component.ts b/src/frontend/app/ui/gallery/search/search.gallery.component.ts index 49c7a930..a4a59507 100644 --- a/src/frontend/app/ui/gallery/search/search.gallery.component.ts +++ b/src/frontend/app/ui/gallery/search/search.gallery.component.ts @@ -148,7 +148,7 @@ export class GallerySearchComponent implements OnDestroy { async saveSearch(): Promise { await this.albumService.addSavedSearch( this.saveSearchName, - SearchQueryUtils.stripFalseNegate(this.searchQueryDTO) + SearchQueryUtils.stripDefault(this.searchQueryDTO) ); this.hideSaveSearchModal(); } diff --git a/test/common/unit/SearchQueryDTOUtils.spec.ts b/test/common/unit/SearchQueryDTOUtils.spec.ts index 3d4a1859..cab4f1f4 100644 --- a/test/common/unit/SearchQueryDTOUtils.spec.ts +++ b/test/common/unit/SearchQueryDTOUtils.spec.ts @@ -151,11 +151,26 @@ describe('SearchQueryDTOUtils.sortQuery', () => { // Added tests to cover validateSearchQuery behavior with negate:false // The validation should not fail merely because a negate:false property exists, even deeply nested. -describe('SearchQueryDTOUtils.validateSearchQuery with negate:false', () => { +describe('SearchQueryDTOUtils.validateSearchQuery', () => { const assertValid = (q: SearchQueryDTO) => { expect(() => SearchQueryUtils.validateSearchQuery(q, 'SearchQuery')).to.not.throw(); }; + it('should validate a top-level leaf with mathtype exact_match', () => { + const q: TextSearch = {type: SearchQueryTypes.person, value: 'alice', matchType: TextSearchQueryMatchTypes.exact_match}; + assertValid(q); + }); + + it('should validate a top-level leaf with mathtype like', () => { + const q: TextSearch = {type: SearchQueryTypes.person, value: 'alice', matchType: TextSearchQueryMatchTypes.like}; + assertValid(q); + }); + + it('should validate a top-level leaf with mathtype like and spaces', () => { + const q: TextSearch = {type: SearchQueryTypes.person, value: 'alice doe', matchType: TextSearchQueryMatchTypes.like}; + assertValid(q); + }); + it('should validate a top-level leaf with negate:false', () => { const q: TextSearch = {type: SearchQueryTypes.person, value: 'alice', negate: false}; assertValid(q); @@ -205,8 +220,8 @@ describe('SearchQueryDTOUtils.validateSearchQuery with negate:false', () => { describe('SearchQueryDTOUtils.urlify', () => { const assertValid = (q: SearchQueryDTO) => { - expect(SearchQueryUtils.urlify(q).length).to.be.lessThan(JSON.stringify(SearchQueryUtils.stripFalseNegate(q)).length); - expect(SearchQueryUtils.parseURLifiedQuery(SearchQueryUtils.urlify(q))).to.deep.equal(SearchQueryUtils.stripFalseNegate(q)); + expect(SearchQueryUtils.urlify(q).length).to.be.lessThan(JSON.stringify(SearchQueryUtils.stripDefault(q)).length); + expect(SearchQueryUtils.parseURLifiedQuery(SearchQueryUtils.urlify(q))).to.deep.equal(SearchQueryUtils.stripDefault(q)); }; it('should validate a top-level leaf with negate:false', () => {