Fixing query validation

This commit is contained in:
Patrik J. Braun
2026-01-22 23:34:00 +01:00
parent a1812d2156
commit 4c1ece50ef
5 changed files with 34 additions and 14 deletions
+13 -8
View File
@@ -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<string, unknown> = {};
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)) {
+1 -1
View File
@@ -63,7 +63,7 @@ export const MetadataSearchQueryTypes = [
export enum TextSearchQueryMatchTypes {
exact_match = 1,
like = 2,
like = 2, // default value
}
export interface SearchQueryDTO {
@@ -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));
}
}
@@ -148,7 +148,7 @@ export class GallerySearchComponent implements OnDestroy {
async saveSearch(): Promise<void> {
await this.albumService.addSavedSearch(
this.saveSearchName,
SearchQueryUtils.stripFalseNegate(this.searchQueryDTO)
SearchQueryUtils.stripDefault(this.searchQueryDTO)
);
this.hideSaveSearchModal();
}
+18 -3
View File
@@ -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', () => {