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

bpatrik / pigallery2 / 20438310607

22 Dec 2025 04:49PM UTC coverage: 68.02% (+0.05%) from 67.974%
20438310607

push

github

bpatrik
Fix tests #1104

1467 of 2438 branches covered (60.17%)

Branch coverage included in aggregate %.

5354 of 7590 relevant lines covered (70.54%)

4067.87 hits per line

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

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

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

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

140

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

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

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

241
    return unshorten(JSON.parse(urlQuery));
6✔
242

243

244
  }
245
};
246

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