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

bpatrik / pigallery2 / 21267307497

22 Jan 2026 10:34PM UTC coverage: 68.591% (+0.02%) from 68.57%
21267307497

push

github

bpatrik
Fixing query validation

1505 of 2461 branches covered (61.15%)

Branch coverage included in aggregate %.

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

1 existing line in 1 file now uncovered.

5444 of 7670 relevant lines covered (70.98%)

4221.11 hits per line

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

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

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

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

143

144
    const sp = new SearchQueryParser();
83✔
145
    try {
83✔
146
      const parsed = sp.parse(sp.stringify(query));
83✔
147
      const normParsed = SearchQueryUtils.stripDefault(parsed) as SearchQueryDTO;
83✔
148
      const normQuery = SearchQueryUtils.stripDefault(query) as SearchQueryDTO;
83✔
149
      if (!Utils.equalsFilter(normParsed, normQuery) || !Utils.equalsFilter(normQuery, normParsed)) {
83!
UNCOV
150
        throw new Error(
×
151
          `${what} is not valid. Expected: ${JSON.stringify(parsed)} to equal: ${JSON.stringify(query)}`
152
        );
153
      }
154
    } catch (e) {
155
      if (e && (e as Error).message && (e as Error).message.startsWith(what)) {
×
156
        throw e;
×
157
      }
158
      throw new Error(`${what} is not valid. ${(e as Error)?.message ?? e}`);
×
159
    }
160
  },
161
  URLMap: {
162
    type: 't',
163
    list: 'l',
164
    negate: 'n',
165
    matchType: 'mt',
166
    distance: 'd',
167
    from: 'f',
168
    value: 'v',
169
    min: 'm',
170
    max: 'x',
171
    landscape: 'ls',
172
    daysLength: 'dl',
173
    frequency: 'fq',
174
    agoNumber: 'an',
175
    GPSData: 'g',
176
    latitude: 'lat',
177
    longitude: 'lng',
178
  } as Record<string, string>,
179
  /**
180
   * Make the SearchQuery URL friendly and shorter
181
   * @param query
182
   */
183
  urlify: (query: SearchQueryDTO): string => {
184
    if (!query) {
14!
185
      return '';
×
186
    }
187
    query=SearchQueryUtils.stripDefault(query);
14✔
188
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
189
    const shorten = (obj: any): any => {
14✔
190
      if (Array.isArray(obj)) {
88✔
191
        return obj.map(shorten);
5✔
192
      }
193
      if (obj && typeof obj === 'object') {
83✔
194
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
195
        const res: any = {};
28✔
196
        for (const key of Object.keys(obj)) {
28✔
197
          if (key === 'negate' && obj[key] === false) {
64!
198
            continue;
×
199
          }
200
          res[SearchQueryUtils.URLMap[key] || key] = shorten(obj[key]);
64!
201
        }
202
        return res;
28✔
203
      }
204
      return obj;
55✔
205
    };
206

207
    return JSON.stringify(shorten(query));
14✔
208
  },
209
  /**
210
   * Pareses the shortened urls and returns with the original SearchQueryDTO
211
   * @param urlQuery
212
   */
213
  parseURLifiedQuery: (urlQuery: string): SearchQueryDTO => {
214
    if (!urlQuery || urlQuery === '') {
6!
215
      return null;
×
216
    }
217

218
    const map: Record<string, string> = {};
6✔
219
    Object.keys(SearchQueryUtils.URLMap).forEach(key => {
6✔
220
      map[SearchQueryUtils.URLMap[key]] = key;
96✔
221
    });
222
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
223
    const unshorten = (obj: any): any => {
6✔
224
      if (Array.isArray(obj)) {
38✔
225
        return obj.map(unshorten);
2✔
226
      }
227
      if (obj && typeof obj === 'object') {
36✔
228
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
229
        const res: any = {};
12✔
230
        for (const key of Object.keys(obj)) {
12✔
231
          let unshortenedKey = map[key] || key;
28!
232
          if (unshortenedKey === 'text' && obj.t !== undefined && !TextSearchQueryTypes.includes(obj.t)) {
28!
233
            // If it's not a text search, 'v' should map to 'value'
234
            if (RangeSearchQueryTypes.includes(obj.t)) {
×
235
              unshortenedKey = 'value';
×
236
            }
237
          }
238
          res[unshortenedKey] = unshorten(obj[key]);
28✔
239
        }
240
        return res;
12✔
241
      }
242
      return obj;
24✔
243
    };
244

245
    return unshorten(JSON.parse(urlQuery));
6✔
246

247

248
  }
249
};
250

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