• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bpatrik / pigallery2 / 17526778077

07 Sep 2025 09:31AM UTC coverage: 65.017% (+0.07%) from 64.943%
17526778077

push

github

bpatrik
Fix search query negate:false validation error #1015

1244 of 2180 branches covered (57.06%)

Branch coverage included in aggregate %.

18 of 18 new or added lines in 2 files covered. (100.0%)

5 existing lines in 2 files now uncovered.

4640 of 6870 relevant lines covered (67.54%)

4435.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

65.38
/src/common/SearchQueryUtils.ts
1
import {
1✔
2
  NegatableSearchQuery,
3
  OrientationSearch,
4
  SearchListQuery,
5
  SearchQueryDTO,
6
  SearchQueryTypes,
7
  SomeOfSearchQuery,
8
  TextSearch
9
} from './entities/SearchQueryDTO';
10
import {SearchQueryParser} from './SearchQueryParser';
1✔
11
import {Utils} from './Utils';
1✔
12

13
export const SearchQueryUtils = {
1✔
14
  negate: (query: SearchQueryDTO): SearchQueryDTO => {
15
    switch (query.type) {
6!
16
      case SearchQueryTypes.AND:
17
        query.type = SearchQueryTypes.OR;
×
18
        (query as SearchListQuery).list = (query as SearchListQuery).list.map(
×
19
          (q) => SearchQueryUtils.negate(q)
×
20
        );
21
        return query;
×
22
      case SearchQueryTypes.OR:
23
        query.type = SearchQueryTypes.AND;
×
24
        (query as SearchListQuery).list = (query as SearchListQuery).list.map(
×
25
          (q) => SearchQueryUtils.negate(q)
×
26
        );
27
        return query;
×
28
      case SearchQueryTypes.orientation:
29
        (query as OrientationSearch).landscape = !(query as OrientationSearch).landscape;
×
30
        return query;
×
31
      case SearchQueryTypes.from_date:
32
      case SearchQueryTypes.to_date:
33
      case SearchQueryTypes.min_rating:
34
      case SearchQueryTypes.max_rating:
35
      case SearchQueryTypes.min_resolution:
36
      case SearchQueryTypes.max_resolution:
37
      case SearchQueryTypes.distance:
38
      case SearchQueryTypes.any_text:
39
      case SearchQueryTypes.person:
40
      case SearchQueryTypes.position:
41
      case SearchQueryTypes.keyword:
42
      case SearchQueryTypes.caption:
43
      case SearchQueryTypes.file_name:
44
      case SearchQueryTypes.directory:
45
        (query as NegatableSearchQuery).negate = !(query as NegatableSearchQuery).negate;
6✔
46
        return query;
6✔
47
      case SearchQueryTypes.SOME_OF:
48
        throw new Error('Some of not supported');
×
49
      default:
50
        throw new Error('Unknown type' + (query).type);
×
51
    }
52
  },
53
  sortQuery(queryIN: SearchQueryDTO): SearchQueryDTO {
54
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
    const canonicalize = (value: any): any => {
116✔
56
      if (Array.isArray(value)) {
577✔
57
        return value.map((v) => canonicalize(v));
45✔
58
      }
59
      if (value && typeof value === 'object') {
556✔
60
        const out: Record<string, unknown> = {};
167✔
61
        const keys = Object.keys(value).sort();
167✔
62
        for (const k of keys) {
167✔
63
          const v = canonicalize(value[k]);
416✔
64
          if (v !== undefined) {
416✔
65
            out[k] = v;
415✔
66
          }
67
        }
68
        return out;
167✔
69
      }
70
      return value;
389✔
71
    };
72

73
    if (!queryIN || (queryIN).type === undefined) {
116!
74
      return queryIN;
×
75
    }
76
    if (
116✔
77
      queryIN.type === SearchQueryTypes.AND ||
325✔
78
      queryIN.type === SearchQueryTypes.OR ||
79
      queryIN.type === SearchQueryTypes.SOME_OF
80
    ) {
81
      const ql = queryIN as SearchListQuery;
17✔
82
      const children = (ql.list || []).map((c) => SearchQueryUtils.sortQuery(c));
37!
83
      const withKeys = children.map((c) => ({key: JSON.stringify(c), value: c}));
37✔
84
      withKeys.sort((a, b) => a.key.localeCompare(b.key));
25✔
85
      if (queryIN.type === SearchQueryTypes.SOME_OF) {
17✔
86
        const so = queryIN as SomeOfSearchQuery;
3✔
87
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
88
        const res: any = {type: queryIN.type};
3✔
89
        if (so.min !== undefined) {
3✔
90
          res.min = so.min;
3✔
91
        }
92
        res.list = withKeys.map((kv) => kv.value);
9✔
93
        return canonicalize(res) as SearchQueryDTO;
3✔
94
      } else {
95
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
96
        const res: any = {type: queryIN.type};
14✔
97
        res.list = withKeys.map((kv) => kv.value);
28✔
98
        return canonicalize(res) as SearchQueryDTO;
14✔
99
      }
100
    }
101
    return canonicalize(queryIN) as SearchQueryDTO;
99✔
102
  },
103
  stringifyForComparison(queryIN: SearchQueryDTO): string {
104
    return JSON.stringify(SearchQueryUtils.sortQuery(queryIN));
56✔
105
  },
106
  isQueryEmpty(query: SearchQueryDTO): boolean {
107
    return !query ||
760✔
108
      query.type === undefined ||
109
      (query.type === SearchQueryTypes.any_text && !(query as TextSearch).text);
110
  },
111
  // Recursively strip negate:false so that optional false flags do not break validation
112
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
113
  stripFalseNegate: (val: any): any => {
114
    if (Array.isArray(val)) {
388✔
115
      return val.map((v) => SearchQueryUtils.stripFalseNegate(v));
16✔
116
    }
117
    if (val && typeof val === 'object') {
380✔
118
      const out: Record<string, unknown> = {};
120✔
119
      for (const k of Object.keys(val)) {
120✔
120
        const v = SearchQueryUtils.stripFalseNegate(val[k]);
268✔
121
        if (k === 'negate' && v === false) {
268✔
122
          // drop negate:false
123
          continue;
4✔
124
        }
125
        // keep everything else, including negate:true and undefined-handled by equalsFilter
126
        out[k] = v;
264✔
127
      }
128
      return out;
120✔
129
    }
130
    return val;
260✔
131
  },
132
  validateSearchQuery(query: SearchQueryDTO, what = 'SearchQuery'): void {
17✔
133
    if (!query) {
52!
134
      return;
×
135
    }
136

137

138
    const sp = new SearchQueryParser();
52✔
139
    try {
52✔
140
      const parsed = sp.parse(sp.stringify(query));
52✔
141
      const normParsed = SearchQueryUtils.stripFalseNegate(parsed) as SearchQueryDTO;
52✔
142
      const normQuery = SearchQueryUtils.stripFalseNegate(query) as SearchQueryDTO;
52✔
143
      if (!Utils.equalsFilter(normParsed, normQuery)) {
52!
UNCOV
144
        throw new Error(
×
145
          `${what} is not valid. Expected: ${JSON.stringify(parsed)} to equal: ${JSON.stringify(query)}`
146
        );
147
      }
148
    } catch (e) {
UNCOV
149
      if (e && (e as Error).message && (e as Error).message.startsWith(what)) {
×
UNCOV
150
        throw e;
×
151
      }
UNCOV
152
      throw new Error(`${what} is not valid. ${(e as Error)?.message ?? e}`);
×
153
    }
154
  }
155
};
156

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc