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

bpatrik / pigallery2 / 8179020003

06 Mar 2024 09:29PM UTC coverage: 64.618% (+0.2%) from 64.464%
8179020003

push

github

web-flow
Merge pull request #845 from grasdk/feature/timestamp_rework_test_fix

fix leap year searching and tests. small metadatabuffer size optimization

1486 of 2627 branches covered (56.57%)

Branch coverage included in aggregate %.

259 of 319 new or added lines in 3 files covered. (81.19%)

216 existing lines in 13 files now uncovered.

4329 of 6372 relevant lines covered (67.94%)

13519.57 hits per line

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

83.33
/src/backend/model/database/SearchManager.ts
1
/* eslint-disable no-case-declarations */
2
import {AutoCompleteItem} from '../../../common/entities/AutoCompleteItem';
1✔
3
import {SearchResultDTO} from '../../../common/entities/SearchResultDTO';
4
import {SQLConnection} from './SQLConnection';
1✔
5
import {PhotoEntity} from './enitites/PhotoEntity';
1✔
6
import {DirectoryEntity} from './enitites/DirectoryEntity';
1✔
7
import {MediaEntity} from './enitites/MediaEntity';
1✔
8
import {PersonEntry} from './enitites/PersonEntry';
1✔
9
import {Brackets, SelectQueryBuilder, WhereExpression} from 'typeorm';
1✔
10
import {Config} from '../../../common/config/private/Config';
1✔
11
import {
1✔
12
  ANDSearchQuery,
13
  DatePatternFrequency,
14
  DatePatternSearch,
15
  DistanceSearch,
16
  FromDateSearch,
17
  MaxPersonCountSearch,
18
  MaxRatingSearch,
19
  MaxResolutionSearch,
20
  MinPersonCountSearch,
21
  MinRatingSearch,
22
  MinResolutionSearch,
23
  OrientationSearch,
24
  ORSearchQuery,
25
  SearchListQuery,
26
  SearchQueryDTO,
27
  SearchQueryTypes,
28
  SomeOfSearchQuery,
29
  TextSearch,
30
  TextSearchQueryMatchTypes,
31
  ToDateSearch,
32
} from '../../../common/entities/SearchQueryDTO';
33
import {GalleryManager} from './GalleryManager';
1✔
34
import {ObjectManagers} from '../ObjectManagers';
1✔
35
import {DatabaseType} from '../../../common/config/private/PrivateConfig';
1✔
36
import {Utils} from '../../../common/Utils';
1✔
37
import {FileEntity} from './enitites/FileEntity';
1✔
38
import {SQL_COLLATE} from './enitites/EntityUtils';
1✔
39
import {GroupSortByTypes, SortByTypes, SortingMethod} from '../../../common/entities/SortingMethods';
1✔
40

41
export class SearchManager {
1✔
42
  private DIRECTORY_SELECT = [
139✔
43
    'directory.id',
44
    'directory.name',
45
    'directory.path',
46
  ];
47
  // makes all search query params unique, so typeorm won't mix them
48
  private queryIdBase = 0;
139✔
49

50
  private static autoCompleteItemsUnique(
51
    array: Array<AutoCompleteItem>
52
  ): Array<AutoCompleteItem> {
53
    const a = array.concat();
16✔
54
    for (let i = 0; i < a.length; ++i) {
16✔
55
      for (let j = i + 1; j < a.length; ++j) {
42✔
56
        if (a[i].equals(a[j])) {
80✔
57
          a.splice(j--, 1);
19✔
58
        }
59
      }
60
    }
61

62
    return a;
16✔
63
  }
64

65
  async autocomplete(
66
    text: string,
67
    type: SearchQueryTypes
68
  ): Promise<AutoCompleteItem[]> {
69
    const connection = await SQLConnection.getConnection();
16✔
70

71
    const photoRepository = connection.getRepository(PhotoEntity);
16✔
72
    const mediaRepository = connection.getRepository(MediaEntity);
16✔
73
    const personRepository = connection.getRepository(PersonEntry);
16✔
74
    const directoryRepository = connection.getRepository(DirectoryEntity);
16✔
75

76
    const partialResult: AutoCompleteItem[][] = [];
16✔
77

78
    if (
16!
79
      type === SearchQueryTypes.any_text ||
16!
80
      type === SearchQueryTypes.keyword
81
    ) {
82
      const acList: AutoCompleteItem[] = [];
16✔
83
      (
16✔
84
        await photoRepository
85
          .createQueryBuilder('photo')
86
          .select('DISTINCT(photo.metadata.keywords)')
87
          .where('photo.metadata.keywords LIKE :text COLLATE ' + SQL_COLLATE, {
88
            text: '%' + text + '%',
89
          })
90
          .limit(Config.Search.AutoComplete.ItemsPerCategory.keyword)
91
          .getRawMany()
92
      )
93
        .map(
94
          (r): Array<string> =>
95
            (r.metadataKeywords as string).split(',') as Array<string>
32✔
96
        )
97
        .forEach((keywords): void => {
98
          acList.push(
32✔
99
            ...this.encapsulateAutoComplete(
100
              keywords.filter(
101
                (k): boolean =>
102
                  k.toLowerCase().indexOf(text.toLowerCase()) !== -1
104✔
103
              ),
104
              SearchQueryTypes.keyword
105
            )
106
          );
107
        });
108
      partialResult.push(acList);
16✔
109
    }
110

111
    if (
16!
112
      type === SearchQueryTypes.any_text ||
16!
113
      type === SearchQueryTypes.person
114
    ) {
115
      partialResult.push(
16✔
116
        this.encapsulateAutoComplete(
117
          (
118
            await personRepository
119
              .createQueryBuilder('person')
120
              .select('DISTINCT(person.name), person.count')
121
              .where('person.name LIKE :text COLLATE ' + SQL_COLLATE, {
122
                text: '%' + text + '%',
123
              })
124
              .limit(
125
                Config.Search.AutoComplete.ItemsPerCategory.person
126
              )
127
              .orderBy('person.count', 'DESC')
128
              .getRawMany()
129
          ).map((r) => r.name),
14✔
130
          SearchQueryTypes.person
131
        )
132
      );
133
    }
134

135
    if (
16!
136
      type === SearchQueryTypes.any_text ||
16!
137
      type === SearchQueryTypes.position ||
138
      type === SearchQueryTypes.distance
139
    ) {
140
      const acList: AutoCompleteItem[] = [];
16✔
141
      (
16✔
142
        await photoRepository
143
          .createQueryBuilder('photo')
144
          .select(
145
            'photo.metadata.positionData.country as country, ' +
146
            'photo.metadata.positionData.state as state, photo.metadata.positionData.city as city'
147
          )
148
          .where(
149
            'photo.metadata.positionData.country LIKE :text COLLATE ' +
150
            SQL_COLLATE,
151
            {text: '%' + text + '%'}
152
          )
153
          .orWhere(
154
            'photo.metadata.positionData.state LIKE :text COLLATE ' +
155
            SQL_COLLATE,
156
            {text: '%' + text + '%'}
157
          )
158
          .orWhere(
159
            'photo.metadata.positionData.city LIKE :text COLLATE ' +
160
            SQL_COLLATE,
161
            {text: '%' + text + '%'}
162
          )
163
          .groupBy(
164
            'photo.metadata.positionData.country, photo.metadata.positionData.state, photo.metadata.positionData.city'
165
          )
166
          .limit(Config.Search.AutoComplete.ItemsPerCategory.position)
167
          .getRawMany()
168
      )
169
        .filter((pm): boolean => !!pm)
10✔
170
        .map(
171
          (pm): Array<string> =>
172
            [pm.city || '', pm.country || '', pm.state || ''] as Array<string>
10!
173
        )
174
        .forEach((positions): void => {
175
          acList.push(
10✔
176
            ...this.encapsulateAutoComplete(
177
              positions.filter(
178
                (p): boolean =>
179
                  p.toLowerCase().indexOf(text.toLowerCase()) !== -1
30✔
180
              ),
181
              type === SearchQueryTypes.distance
10!
182
                ? type
183
                : SearchQueryTypes.position
184
            )
185
          );
186
        });
187
      partialResult.push(acList);
16✔
188
    }
189

190
    if (
16!
191
      type === SearchQueryTypes.any_text ||
16!
192
      type === SearchQueryTypes.file_name
193
    ) {
194
      partialResult.push(
16✔
195
        this.encapsulateAutoComplete(
196
          (
197
            await mediaRepository
198
              .createQueryBuilder('media')
199
              .select('DISTINCT(media.name)')
200
              .where('media.name LIKE :text COLLATE ' + SQL_COLLATE, {
201
                text: '%' + text + '%',
202
              })
203
              .limit(
204
                Config.Search.AutoComplete.ItemsPerCategory.fileName
205
              )
206
              .getRawMany()
207
          ).map((r) => r.name),
12✔
208
          SearchQueryTypes.file_name
209
        )
210
      );
211
    }
212

213
    if (
16!
214
      type === SearchQueryTypes.any_text ||
16!
215
      type === SearchQueryTypes.caption
216
    ) {
217
      partialResult.push(
16✔
218
        this.encapsulateAutoComplete(
219
          (
220
            await photoRepository
221
              .createQueryBuilder('media')
222
              .select('DISTINCT(media.metadata.caption) as caption')
223
              .where(
224
                'media.metadata.caption LIKE :text COLLATE ' + SQL_COLLATE,
225
                {text: '%' + text + '%'}
226
              )
227
              .limit(
228
                Config.Search.AutoComplete.ItemsPerCategory.caption
229
              )
230
              .getRawMany()
231
          ).map((r) => r.caption),
6✔
232
          SearchQueryTypes.caption
233
        )
234
      );
235
    }
236

237
    if (
16!
238
      type === SearchQueryTypes.any_text ||
16!
239
      type === SearchQueryTypes.directory
240
    ) {
241
      partialResult.push(
16✔
242
        this.encapsulateAutoComplete(
243
          (
244
            await directoryRepository
245
              .createQueryBuilder('dir')
246
              .select('DISTINCT(dir.name)')
247
              .where('dir.name LIKE :text COLLATE ' + SQL_COLLATE, {
248
                text: '%' + text + '%',
249
              })
250
              .limit(
251
                Config.Search.AutoComplete.ItemsPerCategory.directory
252
              )
253
              .getRawMany()
254
          ).map((r) => r.name),
8✔
255
          SearchQueryTypes.directory
256
        )
257
      );
258
    }
259

260
    const result: AutoCompleteItem[] = [];
16✔
261

262
    while (result.length < Config.Search.AutoComplete.ItemsPerCategory.maxItems) {
16✔
263
      let adding = false;
55✔
264
      for (let i = 0; i < partialResult.length; ++i) {
55✔
265
        if (partialResult[i].length <= 0) {
330✔
266
          continue;
269✔
267
        }
268
        result.push(partialResult[i].pop());
61✔
269
        adding = true;
61✔
270
      }
271
      if (!adding) {
55✔
272
        break;
10✔
273
      }
274
    }
275

276

277
    return SearchManager.autoCompleteItemsUnique(result);
16✔
278
  }
279

280
  async search(queryIN: SearchQueryDTO): Promise<SearchResultDTO> {
281
    const query = await this.prepareQuery(queryIN);
174✔
282
    const connection = await SQLConnection.getConnection();
174✔
283

284
    const result: SearchResultDTO = {
174✔
285
      searchQuery: queryIN,
286
      directories: [],
287
      media: [],
288
      metaFile: [],
289
      resultOverflow: false,
290
    };
291

292
    result.media = await connection
174✔
293
      .getRepository(MediaEntity)
294
      .createQueryBuilder('media')
295
      .select(['media', ...this.DIRECTORY_SELECT])
296
      .where(this.buildWhereQuery(query))
297
      .leftJoin('media.directory', 'directory')
298
      .limit(Config.Search.maxMediaResult + 1)
299
      .getMany();
300

301

302
    if (result.media.length > Config.Search.maxMediaResult) {
174!
303
      result.resultOverflow = true;
×
304
    }
305

306

307
    if (Config.Search.listMetafiles === true) {
174✔
308
      const dIds = Array.from(new Set(result.media.map(m => (m.directory as unknown as { id: number }).id)));
6✔
309
      result.metaFile = await connection
2✔
310
        .getRepository(FileEntity)
311
        .createQueryBuilder('file')
312
        .select(['file', ...this.DIRECTORY_SELECT])
313
        .where(`file.directoryId IN(${dIds})`)
314
        .leftJoin('file.directory', 'directory')
315
        .getMany();
316
    }
317

318
    if (Config.Search.listDirectories === true) {
174✔
319
      const dirQuery = this.filterDirectoryQuery(query);
10✔
320
      if (dirQuery !== null) {
10✔
321
        result.directories = await connection
2✔
322
          .getRepository(DirectoryEntity)
323
          .createQueryBuilder('directory')
324
          .where(this.buildWhereQuery(dirQuery, true))
325
          .leftJoinAndSelect('directory.cover', 'cover')
326
          .leftJoinAndSelect('cover.directory', 'coverDirectory')
327
          .limit(Config.Search.maxDirectoryResult + 1)
328
          .select([
329
            'directory',
330
            'cover.name',
331
            'coverDirectory.name',
332
            'coverDirectory.path',
333
          ])
334
          .getMany();
335

336
        // setting covers
337
        if (result.directories) {
2!
338
          for (const item of result.directories) {
2✔
339
            await ObjectManagers.getInstance().GalleryManager.fillCoverForSubDir(connection, item as DirectoryEntity);
2✔
340
          }
341
        }
342
        if (
2!
343
          result.directories.length > Config.Search.maxDirectoryResult
344
        ) {
345
          result.resultOverflow = true;
×
346
        }
347
      }
348
    }
349

350
    return result;
174✔
351
  }
352

353

354
  public static setSorting<T>(
355
    query: SelectQueryBuilder<T>,
356
    sortings: SortingMethod[]
357
  ): SelectQueryBuilder<T> {
358
    if (!sortings || !Array.isArray(sortings)) {
124!
359
      return query;
×
360
    }
361
    if (sortings.findIndex(s => s.method == SortByTypes.Random) !== -1 && sortings.length > 1) {
208!
362
      throw new Error('Error during applying sorting: Can\' randomize and also sort the result. Bad input:' + sortings.map(s => GroupSortByTypes[s.method]).join(', '));
×
363
    }
364
    for (const sort of sortings) {
124✔
365
      switch (sort.method) {
208!
366
        case SortByTypes.Date:
367
          query.addOrderBy('media.metadata.creationDate', sort.ascending ? 'ASC' : 'DESC'); //If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). If taken into account, it will alter the sort order. Probably should not be done.
60!
368
          break;
60✔
369
        case SortByTypes.Rating:
370
          query.addOrderBy('media.metadata.rating', sort.ascending ? 'ASC' : 'DESC');
116!
371
          break;
116✔
372
        case SortByTypes.Name:
373
          query.addOrderBy('media.name', sort.ascending ? 'ASC' : 'DESC');
2!
374
          break;
2✔
375
        case SortByTypes.PersonCount:
376
          query.addOrderBy('media.metadata.personsLength', sort.ascending ? 'ASC' : 'DESC');
26!
377
          break;
26✔
378
        case SortByTypes.FileSize:
379
          query.addOrderBy('media.metadata.fileSize', sort.ascending ? 'ASC' : 'DESC');
×
380
          break;
×
381
        case SortByTypes.Random:
382
          if (Config.Database.type === DatabaseType.mysql) {
4✔
383
            query.groupBy('RAND(), media.id');
2✔
384
          } else {
385
            query.groupBy('RANDOM()');
2✔
386
          }
387
          break;
4✔
388
      }
389
    }
390

391
    return query;
124✔
392
  }
393

394
  public async getNMedia(query: SearchQueryDTO, sortings: SortingMethod[], take: number, photoOnly = false) {
×
395
    const connection = await SQLConnection.getConnection();
4✔
396
    const sqlQuery: SelectQueryBuilder<PhotoEntity> = connection
4✔
397
      .getRepository(photoOnly ? PhotoEntity : MediaEntity)
4!
398
      .createQueryBuilder('media')
399
      .select(['media', ...this.DIRECTORY_SELECT])
400
      .innerJoin('media.directory', 'directory')
401
      .where(await this.prepareAndBuildWhereQuery(query));
402
    SearchManager.setSorting(sqlQuery, sortings);
4✔
403

404
    return sqlQuery.limit(take).getMany();
4✔
405

406
  }
407

408
  public async getCount(query: SearchQueryDTO): Promise<number> {
409
    const connection = await SQLConnection.getConnection();
20✔
410

411
    return await connection
20✔
412
      .getRepository(MediaEntity)
413
      .createQueryBuilder('media')
414
      .innerJoin('media.directory', 'directory')
415
      .where(await this.prepareAndBuildWhereQuery(query))
416
      .getCount();
417
  }
418

419
  public async prepareAndBuildWhereQuery(
420
    queryIN: SearchQueryDTO,
421
    directoryOnly = false
70✔
422
  ): Promise<Brackets> {
423
    const query = await this.prepareQuery(queryIN);
70✔
424
    return this.buildWhereQuery(query, directoryOnly);
70✔
425
  }
426

427
  public async prepareQuery(queryIN: SearchQueryDTO): Promise<SearchQueryDTO> {
428
    let query: SearchQueryDTO = this.assignQueryIDs(Utils.clone(queryIN)); // assign local ids before flattening SOME_OF queries
244✔
429
    query = this.flattenSameOfQueries(query);
244✔
430
    query = await this.getGPSData(query);
244✔
431
    return query;
244✔
432
  }
433

434
  /**
435
   * Builds the SQL Where query from search query
436
   * @param query input search query
437
   * @param directoryOnly Only builds directory related queries
438
   * @private
439
   */
440
  public buildWhereQuery(
441
    query: SearchQueryDTO,
442
    directoryOnly = false
174✔
443
  ): Brackets {
444
    const queryId = (query as SearchQueryDTOWithID).queryId;
74,332✔
445
    switch (query.type) {
74,332!
446
      case SearchQueryTypes.AND:
447
        return new Brackets((q): unknown => {
11,648✔
448
          (query as ANDSearchQuery).list.forEach((sq) => {
11,648✔
449
              q.andWhere(this.buildWhereQuery(sq, directoryOnly));
23,642✔
450
            }
451
          );
452
          return q;
11,648✔
453
        });
454
      case SearchQueryTypes.OR:
455
        return new Brackets((q): unknown => {
9,718✔
456
          (query as ANDSearchQuery).list.forEach((sq) => {
9,718✔
457
              q.orWhere(this.buildWhereQuery(sq, directoryOnly));
50,444✔
458
            }
459
          );
460
          return q;
9,718✔
461
        });
462

463
      case SearchQueryTypes.distance:
464
        if (directoryOnly) {
10!
465
          throw new Error('not supported in directoryOnly mode');
×
466
        }
467
        /**
468
         * This is a best effort calculation, not fully accurate in order to have higher performance.
469
         * see: https://stackoverflow.com/a/50506609
470
         */
471
        const earth = 6378.137; // radius of the earth in kilometer
10✔
472
        const latDelta = 1 / (((2 * Math.PI) / 360) * earth); // 1 km in degree
10✔
473
        const lonDelta = 1 / (((2 * Math.PI) / 360) * earth); // 1 km in degree
10✔
474

475
        // TODO: properly handle latitude / longitude boundaries
476
        const trimRange = (value: number, min: number, max: number): number => {
10✔
477
          return Math.min(Math.max(value, min), max);
40✔
478
        };
479

480
        const minLat = trimRange(
10✔
481
          (query as DistanceSearch).from.GPSData.latitude -
482
          (query as DistanceSearch).distance * latDelta,
483
          -90,
484
          90
485
        );
486
        const maxLat = trimRange(
10✔
487
          (query as DistanceSearch).from.GPSData.latitude +
488
          (query as DistanceSearch).distance * latDelta,
489
          -90,
490
          90
491
        );
492
        const minLon = trimRange(
10✔
493
          (query as DistanceSearch).from.GPSData.longitude -
494
          ((query as DistanceSearch).distance * lonDelta) /
495
          Math.cos(minLat * (Math.PI / 180)),
496
          -180,
497
          180
498
        );
499
        const maxLon = trimRange(
10✔
500
          (query as DistanceSearch).from.GPSData.longitude +
501
          ((query as DistanceSearch).distance * lonDelta) /
502
          Math.cos(maxLat * (Math.PI / 180)),
503
          -180,
504
          180
505
        );
506

507
        return new Brackets((q): unknown => {
10✔
508
          const textParam: { [key: string]: number | string } = {};
10✔
509
          textParam['maxLat' + queryId] = maxLat;
10✔
510
          textParam['minLat' + queryId] = minLat;
10✔
511
          textParam['maxLon' + queryId] = maxLon;
10✔
512
          textParam['minLon' + queryId] = minLon;
10✔
513
          if (!(query as DistanceSearch).negate) {
10✔
514
            q.where(
8✔
515
              `media.metadata.positionData.GPSData.latitude < :maxLat${queryId}`,
516
              textParam
517
            );
518
            q.andWhere(
8✔
519
              `media.metadata.positionData.GPSData.latitude > :minLat${queryId}`,
520
              textParam
521
            );
522
            q.andWhere(
8✔
523
              `media.metadata.positionData.GPSData.longitude < :maxLon${queryId}`,
524
              textParam
525
            );
526
            q.andWhere(
8✔
527
              `media.metadata.positionData.GPSData.longitude > :minLon${queryId}`,
528
              textParam
529
            );
530
          } else {
531
            q.where(
2✔
532
              `media.metadata.positionData.GPSData.latitude > :maxLat${queryId}`,
533
              textParam
534
            );
535
            q.orWhere(
2✔
536
              `media.metadata.positionData.GPSData.latitude < :minLat${queryId}`,
537
              textParam
538
            );
539
            q.orWhere(
2✔
540
              `media.metadata.positionData.GPSData.longitude > :maxLon${queryId}`,
541
              textParam
542
            );
543
            q.orWhere(
2✔
544
              `media.metadata.positionData.GPSData.longitude < :minLon${queryId}`,
545
              textParam
546
            );
547
          }
548
          return q;
10✔
549
        });
550

551
      case SearchQueryTypes.from_date:
552
        if (directoryOnly) {
4!
553
          throw new Error('not supported in directoryOnly mode');
×
554
        }
555
        return new Brackets((q): unknown => {
4✔
556
          if (typeof (query as FromDateSearch).value === 'undefined') {
4!
557
            throw new Error(
×
558
              'Invalid search query: Date Query should contain from value'
559
            );
560
          }
561
          const relation = (query as TextSearch).negate ? '<' : '>=';
4✔
562

563
          const textParam: { [key: string]: unknown } = {};
4✔
564
          textParam['from' + queryId] = (query as FromDateSearch).value;
4✔
565
          q.where(
4✔
566
            `media.metadata.creationDate ${relation} :from${queryId}`, //TODO: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). 
567
                                                                       //Example: -600 means in the database UTC-10:00. The time 20:00 in the evening in the UTC-10 timezone, is actually 06:00 the next morning 
568
                                                                       //in UTC+00:00. To make search take that into account, one can subtract the offset from the creationDate to "pretend" the photo is taken
569
                                                                       //in UTC time. Subtracting -600 minutes (because it's the -10:00 timezone), corresponds to adding 10 hours to the photo's timestamp, thus
570
                                                                       //bringing it into the next day as if it was taken at UTC+00:00. Similarly subtracting a positive timezone from a timestamp will "pretend"
571
                                                                       //the photo is taken earlier in time (e.g. subtracting 300 from the UTC+05:00 timezone).
572
            textParam
573
          );
574

575
          return q;
4✔
576
        });
577

578
      case SearchQueryTypes.to_date:
579
        if (directoryOnly) {
4!
UNCOV
580
          throw new Error('not supported in directoryOnly mode');
×
581
        }
582
        return new Brackets((q): unknown => {
4✔
583
          if (typeof (query as ToDateSearch).value === 'undefined') {
4!
UNCOV
584
            throw new Error(
×
585
              'Invalid search query: Date Query should contain to value'
586
            );
587
          }
588
          const relation = (query as TextSearch).negate ? '>' : '<=';
4!
589

590
          const textParam: { [key: string]: unknown } = {};
4✔
591
          textParam['to' + queryId] = (query as ToDateSearch).value;
4✔
592
          q.where(
4✔
593
            `media.metadata.creationDate ${relation} :to${queryId}`, //TODO: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
594
            textParam 
595
          );
596

597
          return q;
4✔
598
        });
599

600
      case SearchQueryTypes.min_rating:
601
        if (directoryOnly) {
4!
UNCOV
602
          throw new Error('not supported in directoryOnly mode');
×
603
        }
604
        return new Brackets((q): unknown => {
4✔
605
          if (typeof (query as MinRatingSearch).value === 'undefined') {
4!
UNCOV
606
            throw new Error(
×
607
              'Invalid search query: Rating Query should contain minvalue'
608
            );
609
          }
610

611
          const relation = (query as TextSearch).negate ? '<' : '>=';
4✔
612

613
          const textParam: { [key: string]: unknown } = {};
4✔
614
          textParam['min' + queryId] = (query as MinRatingSearch).value;
4✔
615
          q.where(
4✔
616
            `media.metadata.rating ${relation}  :min${queryId}`,
617
            textParam
618
          );
619

620
          return q;
4✔
621
        });
622
      case SearchQueryTypes.max_rating:
623
        if (directoryOnly) {
6!
UNCOV
624
          throw new Error('not supported in directoryOnly mode');
×
625
        }
626
        return new Brackets((q): unknown => {
6✔
627
          if (typeof (query as MaxRatingSearch).value === 'undefined') {
6!
UNCOV
628
            throw new Error(
×
629
              'Invalid search query: Rating Query should contain  max value'
630
            );
631
          }
632

633
          const relation = (query as TextSearch).negate ? '>' : '<=';
6✔
634

635
          if (typeof (query as MaxRatingSearch).value !== 'undefined') {
6!
636
            const textParam: { [key: string]: unknown } = {};
6✔
637
            textParam['max' + queryId] = (query as MaxRatingSearch).value;
6✔
638
            q.where(
6✔
639
              `media.metadata.rating ${relation}  :max${queryId}`,
640
              textParam
641
            );
642
          }
643
          return q;
6✔
644
        });
645

646
      case SearchQueryTypes.min_person_count:
647
        if (directoryOnly) {
6!
UNCOV
648
          throw new Error('not supported in directoryOnly mode');
×
649
        }
650
        return new Brackets((q): unknown => {
6✔
651
          if (typeof (query as MinPersonCountSearch).value === 'undefined') {
6!
UNCOV
652
            throw new Error(
×
653
              'Invalid search query: Person count Query should contain minvalue'
654
            );
655
          }
656

657
          const relation = (query as TextSearch).negate ? '<' : '>=';
6✔
658

659
          const textParam: { [key: string]: unknown } = {};
6✔
660
          textParam['min' + queryId] = (query as MinPersonCountSearch).value;
6✔
661
          q.where(
6✔
662
            `media.metadata.personsLength ${relation}  :min${queryId}`,
663
            textParam
664
          );
665

666
          return q;
6✔
667
        });
668
      case SearchQueryTypes.max_person_count:
669
        if (directoryOnly) {
8!
UNCOV
670
          throw new Error('not supported in directoryOnly mode');
×
671
        }
672
        return new Brackets((q): unknown => {
8✔
673
          if (typeof (query as MaxPersonCountSearch).value === 'undefined') {
8!
UNCOV
674
            throw new Error(
×
675
              'Invalid search query: Person count Query should contain max value'
676
            );
677
          }
678

679
          const relation = (query as TextSearch).negate ? '>' : '<=';
8✔
680

681
          if (typeof (query as MaxRatingSearch).value !== 'undefined') {
8!
682
            const textParam: { [key: string]: unknown } = {};
8✔
683
            textParam['max' + queryId] = (query as MaxPersonCountSearch).value;
8✔
684
            q.where(
8✔
685
              `media.metadata.personsLength ${relation}  :max${queryId}`,
686
              textParam
687
            );
688
          }
689
          return q;
8✔
690
        });
691

692
      case SearchQueryTypes.min_resolution:
693
        if (directoryOnly) {
4!
UNCOV
694
          throw new Error('not supported in directoryOnly mode');
×
695
        }
696
        return new Brackets((q): unknown => {
4✔
697
          if (typeof (query as MinResolutionSearch).value === 'undefined') {
4!
UNCOV
698
            throw new Error(
×
699
              'Invalid search query: Resolution Query should contain min value'
700
            );
701
          }
702

703
          const relation = (query as TextSearch).negate ? '<' : '>=';
4✔
704

705
          const textParam: { [key: string]: unknown } = {};
4✔
706
          textParam['min' + queryId] =
4✔
707
            (query as MinResolutionSearch).value * 1000 * 1000;
708
          q.where(
4✔
709
            `media.metadata.size.width * media.metadata.size.height ${relation} :min${queryId}`,
710
            textParam
711
          );
712

713
          return q;
4✔
714
        });
715

716
      case SearchQueryTypes.max_resolution:
717
        if (directoryOnly) {
6!
UNCOV
718
          throw new Error('not supported in directoryOnly mode');
×
719
        }
720
        return new Brackets((q): unknown => {
6✔
721
          if (typeof (query as MaxResolutionSearch).value === 'undefined') {
6!
UNCOV
722
            throw new Error(
×
723
              'Invalid search query: Rating Query should contain min or max value'
724
            );
725
          }
726

727
          const relation = (query as TextSearch).negate ? '>' : '<=';
6✔
728

729
          const textParam: { [key: string]: unknown } = {};
6✔
730
          textParam['max' + queryId] =
6✔
731
            (query as MaxResolutionSearch).value * 1000 * 1000;
732
          q.where(
6✔
733
            `media.metadata.size.width * media.metadata.size.height ${relation} :max${queryId}`,
734
            textParam
735
          );
736

737
          return q;
6✔
738
        });
739

740
      case SearchQueryTypes.orientation:
741
        if (directoryOnly) {
4!
UNCOV
742
          throw new Error('not supported in directoryOnly mode');
×
743
        }
744
        return new Brackets((q): unknown => {
4✔
745
          if ((query as OrientationSearch).landscape) {
4✔
746
            q.where('media.metadata.size.width >= media.metadata.size.height');
2✔
747
          } else {
748
            q.where('media.metadata.size.width <= media.metadata.size.height');
2✔
749
          }
750
          return q;
4✔
751
        });
752

753
      case SearchQueryTypes.date_pattern: {
754
        if (directoryOnly) {
34!
UNCOV
755
          throw new Error('not supported in directoryOnly mode');
×
756
        }
757
        const tq = query as DatePatternSearch;
34✔
758

759
        return new Brackets((q): unknown => {
34✔
760
          // Fixed frequency
761
          if ((tq.frequency === DatePatternFrequency.years_ago ||
34✔
762
            tq.frequency === DatePatternFrequency.months_ago ||
763
            tq.frequency === DatePatternFrequency.weeks_ago ||
764
            tq.frequency === DatePatternFrequency.days_ago)) {
765

766
            if (isNaN(tq.agoNumber)) {
12!
NEW
767
              throw new Error('ago number is missing on date pattern search query with frequency: ' + DatePatternFrequency[tq.frequency] + ', ago number: ' + tq.agoNumber);
×
768
            }
769
            const to = new Date();
12✔
770
            to.setHours(0, 0, 0, 0);
12✔
771
            to.setUTCDate(to.getUTCDate() + 1);
12✔
772

773
            switch (tq.frequency) {
12!
774
              case DatePatternFrequency.days_ago:
775
                to.setUTCDate(to.getUTCDate() - tq.agoNumber);
4✔
776
                break;
4✔
777
              case DatePatternFrequency.weeks_ago:
UNCOV
778
                to.setUTCDate(to.getUTCDate() - tq.agoNumber * 7);
×
UNCOV
779
                break;
×
780

781
              case DatePatternFrequency.months_ago:
782
                to.setUTCMonth(to.getUTCMonth() - tq.agoNumber);
8✔
783
                break;
8✔
784

785
              case DatePatternFrequency.years_ago:
UNCOV
786
                to.setUTCFullYear(to.getUTCFullYear() - tq.agoNumber);
×
UNCOV
787
                break;
×
788
            }
789
            const from = new Date(to);
12✔
790
            from.setUTCDate(from.getUTCDate() - tq.daysLength);
12✔
791

792
            const textParam: { [key: string]: unknown } = {};
12✔
793
            textParam['to' + queryId] = to.getTime();
12✔
794
            textParam['from' + queryId] = from.getTime();
12✔
795
            if (tq.negate) {
12✔
796

797
              q.where(
6✔
798
                `media.metadata.creationDate >= :to${queryId}`,            //TODO: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
799
                textParam
800
              ).orWhere(`media.metadata.creationDate < :from${queryId}`,   //TODO: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
801
                textParam);
802
            } else {
803
              q.where(
6✔
804
                `media.metadata.creationDate < :to${queryId}`,             //TODO: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
805
                textParam
806
              ).andWhere(`media.metadata.creationDate >= :from${queryId}`, //TODO: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
807
                textParam);
808
            }
809

810
          } else {
811
            // recurring
812

813
            const textParam: { [key: string]: unknown } = {};
22✔
814
            textParam['diff' + queryId] = tq.daysLength;
22✔
815
            const addWhere = (duration: string, crossesDateBoundary: boolean) => {
22✔
816

817
              const relationEql = tq.negate ? '!=' : '=';
16✔
818

819
              // reminder: !(a&&b) = (!a || !b), that is what happening here if negate is true
820
              const relationTop = tq.negate ? '>' : '<=';
16✔
821
              const relationBottom = tq.negate ? '<=' : '>';
16✔
822
              // this is an XoR. during date boundary crossing we swap boolean logic again
823
              const whereFN = !!tq.negate !== crossesDateBoundary ? 'orWhere' : 'andWhere';
16✔
824

825

826
              if (Config.Database.type === DatabaseType.sqlite) {
16✔
827
                if (tq.daysLength == 0) {
8✔
828
                  q.where(
2✔
829
                    //TODO: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
830
                    `CAST(strftime('${duration}',media.metadataCreationDate/1000, 'unixepoch') AS INTEGER) ${relationEql} CAST(strftime('${duration}','now') AS INTEGER)`
831
                  );
832
                } else {
833
                  q.where(
6✔
834
                    //TODO: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
835
                    `CAST(strftime('${duration}',media.metadataCreationDate/1000, 'unixepoch') AS INTEGER) ${relationTop} CAST(strftime('${duration}','now') AS INTEGER)`
836
                  )[whereFN](`CAST(strftime('${duration}',media.metadataCreationDate/1000, 'unixepoch') AS INTEGER) ${relationBottom} CAST(strftime('${duration}','now','-:diff${queryId} day') AS INTEGER)`,
837
                    textParam);
838
                }
839
              } else {
840
                if (tq.daysLength == 0) {
8✔
841
                  q.where(
2✔
842
                    //TODO: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
843
                    `CAST(FROM_UNIXTIME(media.metadataCreationDate/1000, '${duration}') AS SIGNED) ${relationEql} CAST(DATE_FORMAT(CURDATE(),'${duration}') AS SIGNED)`
844
                  );
845
                } else {
846
                  q.where(
6✔
847
                    //TODO: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
848
                    `CAST(FROM_UNIXTIME(media.metadataCreationDate/1000, '${duration}') AS SIGNED) ${relationTop} CAST(DATE_FORMAT(CURDATE(),'${duration}') AS SIGNED)`
849
                  )[whereFN](`CAST(FROM_UNIXTIME(media.metadataCreationDate/1000, '${duration}') AS SIGNED) ${relationBottom} CAST(DATE_FORMAT((DATE_ADD(curdate(), INTERVAL -:diff${queryId} DAY)),'${duration}') AS SIGNED)`,
850
                    textParam);
851
                }
852
              }
853
            };
854
            switch (tq.frequency) {
22!
855
              case DatePatternFrequency.every_year:
856
                const d = new Date();
20✔
857
                if (tq.daysLength >= (Utils.isDateFromLeapYear(d) ? 366: 365)) { // trivial result includes all photos
20!
858
                  if (tq.negate) {
4✔
859
                    q.andWhere('FALSE');
2✔
860
                  }
861
                  return q;
4✔
862
                }
863
                
864
                const dayOfYear = Utils.getDayOfYear(d);
16✔
865
                addWhere('%m%d', dayOfYear - tq.daysLength < 0);
16✔
866
                break;
16✔
867
              case DatePatternFrequency.every_month:
868
                if (tq.daysLength >= 31) { // trivial result includes all photos
2!
869
                  if (tq.negate) {
2!
870
                    q.andWhere('FALSE');
×
871
                  }
872
                  return q;
2✔
873
                }
874
                addWhere('%d', (new Date()).getUTCDate() - tq.daysLength < 0);
×
875
                break;
×
876
              case DatePatternFrequency.every_week:
UNCOV
877
                if (tq.daysLength >= 7) { // trivial result includes all photos
×
UNCOV
878
                  if (tq.negate) {
×
UNCOV
879
                    q.andWhere('FALSE');
×
880
                  }
UNCOV
881
                  return q;
×
882
                }
UNCOV
883
                addWhere('%w', (new Date()).getUTCDay() - tq.daysLength < 0);
×
UNCOV
884
                break;
×
885
            }
886

887

888
          }
889

890
          return q;
28✔
891
        });
892
      }
893

894
      case SearchQueryTypes.SOME_OF:
UNCOV
895
        throw new Error('Some of not supported');
×
896
    }
897

898
    return new Brackets((q: WhereExpression) => {
52,876✔
899
      const createMatchString = (str: string): string => {
52,880✔
900
        if (
53,046✔
901
          (query as TextSearch).matchType ===
902
          TextSearchQueryMatchTypes.exact_match
903
        ) {
904
          return str;
44✔
905
        }
906
        // MySQL uses C escape syntax in strings, details:
907
        // https://stackoverflow.com/questions/14926386/how-to-search-for-slash-in-mysql-and-why-escaping-not-required-for-wher
908
        if (Config.Database.type === DatabaseType.mysql) {
53,002✔
909
          /// this reqExp replaces the "\\" to "\\\\\"
910
          return '%' + str.replace(new RegExp('\\\\', 'g'), '\\\\') + '%';
26,501✔
911
        }
912
        return `%${str}%`;
26,501✔
913
      };
914

915
      const LIKE = (query as TextSearch).negate ? 'NOT LIKE' : 'LIKE';
52,880✔
916
      // if the expression is negated, we use AND instead of OR as nowhere should that match
917
      const whereFN = (query as TextSearch).negate ? 'andWhere' : 'orWhere';
52,880✔
918
      const whereFNRev = (query as TextSearch).negate ? 'orWhere' : 'andWhere';
52,880✔
919

920
      const textParam: { [key: string]: unknown } = {};
52,880✔
921
      textParam['text' + queryId] = createMatchString(
52,880✔
922
        (query as TextSearch).text
923
      );
924

925
      if (
52,880✔
926
        query.type === SearchQueryTypes.any_text ||
105,690✔
927
        query.type === SearchQueryTypes.directory
928
      ) {
929
        const dirPathStr = (query as TextSearch).text.replace(
80✔
930
          new RegExp('\\\\', 'g'),
931
          '/'
932
        );
933

934
        textParam['fullPath' + queryId] = createMatchString(dirPathStr);
80✔
935
        q[whereFN](
80✔
936
          `directory.path ${LIKE} :fullPath${queryId} COLLATE ` + SQL_COLLATE,
937
          textParam
938
        );
939

940
        const directoryPath = GalleryManager.parseRelativeDirePath(dirPathStr);
80✔
941
        q[whereFN](
80✔
942
          new Brackets((dq): unknown => {
943
            textParam['dirName' + queryId] = createMatchString(
80✔
944
              directoryPath.name
945
            );
946
            dq[whereFNRev](
80✔
947
              `directory.name ${LIKE} :dirName${queryId} COLLATE ${SQL_COLLATE}`,
948
              textParam
949
            );
950
            if (dirPathStr.includes('/')) {
80✔
951
              textParam['parentName' + queryId] = createMatchString(
6✔
952
                directoryPath.parent
953
              );
954
              dq[whereFNRev](
6✔
955
                `directory.path ${LIKE} :parentName${queryId} COLLATE ${SQL_COLLATE}`,
956
                textParam
957
              );
958
            }
959
            return dq;
80✔
960
          })
961
        );
962
      }
963

964
      if (
52,880✔
965
        (query.type === SearchQueryTypes.any_text && !directoryOnly) ||
105,762✔
966
        query.type === SearchQueryTypes.file_name
967
      ) {
968
        q[whereFN](
52,744✔
969
          `media.name ${LIKE} :text${queryId} COLLATE ${SQL_COLLATE}`,
970
          textParam
971
        );
972
      }
973

974
      if (
52,880✔
975
        (query.type === SearchQueryTypes.any_text && !directoryOnly) ||
105,762✔
976
        query.type === SearchQueryTypes.caption
977
      ) {
978
        q[whereFN](
86✔
979
          `media.metadata.caption ${LIKE} :text${queryId} COLLATE ${SQL_COLLATE}`,
980
          textParam
981
        );
982
      }
983

984
      if (
52,880✔
985
        (query.type === SearchQueryTypes.any_text && !directoryOnly) ||
105,762✔
986
        query.type === SearchQueryTypes.position
987
      ) {
988
        q[whereFN](
76✔
989
          `media.metadata.positionData.country ${LIKE} :text${queryId} COLLATE ${SQL_COLLATE}`,
990
          textParam
991
        )[whereFN](
992
          `media.metadata.positionData.state ${LIKE} :text${queryId} COLLATE ${SQL_COLLATE}`,
993
          textParam
994
        )[whereFN](
995
          `media.metadata.positionData.city ${LIKE} :text${queryId} COLLATE ${SQL_COLLATE}`,
996
          textParam
997
        );
998
      }
999

1000
      // Matching for array type fields
1001
      const matchArrayField = (fieldName: string): void => {
52,880✔
1002
        q[whereFN](
234✔
1003
          new Brackets((qbr): void => {
1004
            if (
234✔
1005
              (query as TextSearch).matchType !==
1006
              TextSearchQueryMatchTypes.exact_match
1007
            ) {
1008
              qbr[whereFN](
212✔
1009
                `${fieldName} ${LIKE} :text${queryId} COLLATE ${SQL_COLLATE}`,
1010
                textParam
1011
              );
1012
            } else {
1013
              qbr[whereFN](
22✔
1014
                new Brackets((qb): void => {
1015
                  textParam['CtextC' + queryId] = `%,${
22✔
1016
                    (query as TextSearch).text
1017
                  },%`;
1018
                  textParam['Ctext' + queryId] = `%,${
22✔
1019
                    (query as TextSearch).text
1020
                  }`;
1021
                  textParam['textC' + queryId] = `${
22✔
1022
                    (query as TextSearch).text
1023
                  },%`;
1024
                  textParam['text_exact' + queryId] = `${
22✔
1025
                    (query as TextSearch).text
1026
                  }`;
1027

1028
                  qb[whereFN](
22✔
1029
                    `${fieldName} ${LIKE} :CtextC${queryId} COLLATE ${SQL_COLLATE}`,
1030
                    textParam
1031
                  );
1032
                  qb[whereFN](
22✔
1033
                    `${fieldName} ${LIKE} :Ctext${queryId} COLLATE ${SQL_COLLATE}`,
1034
                    textParam
1035
                  );
1036
                  qb[whereFN](
22✔
1037
                    `${fieldName} ${LIKE} :textC${queryId} COLLATE ${SQL_COLLATE}`,
1038
                    textParam
1039
                  );
1040
                  qb[whereFN](
22✔
1041
                    `${fieldName} ${LIKE} :text_exact${queryId} COLLATE ${SQL_COLLATE}`,
1042
                    textParam
1043
                  );
1044
                })
1045
              );
1046
            }
1047
            if ((query as TextSearch).negate) {
234✔
1048
              qbr.orWhere(`${fieldName} IS NULL`);
12✔
1049
            }
1050
          })
1051
        );
1052
      };
1053

1054
      if (
52,880✔
1055
        (query.type === SearchQueryTypes.any_text && !directoryOnly) ||
105,762✔
1056
        query.type === SearchQueryTypes.person
1057
      ) {
1058
        matchArrayField('media.metadata.persons');
144✔
1059
      }
1060

1061
      if (
52,880✔
1062
        (query.type === SearchQueryTypes.any_text && !directoryOnly) ||
105,762✔
1063
        query.type === SearchQueryTypes.keyword
1064
      ) {
1065
        matchArrayField('media.metadata.keywords');
90✔
1066
      }
1067
      return q;
52,880✔
1068
    });
1069
  }
1070

1071
  protected flattenSameOfQueries(query: SearchQueryDTO): SearchQueryDTO {
1072
    switch (query.type) {
5,630,364✔
1073
      case SearchQueryTypes.AND:
1074
      case SearchQueryTypes.OR:
1075
        return {
2,088,518✔
1076
          type: query.type,
1077
          list: ((query as SearchListQuery).list || []).map(
2,088,518!
1078
            (q): SearchQueryDTO => this.flattenSameOfQueries(q)
5,630,068✔
1079
          ),
1080
        } as SearchListQuery;
1081
      case SearchQueryTypes.SOME_OF:
1082
        const someOfQ = query as SomeOfSearchQuery;
32✔
1083
        someOfQ.min = someOfQ.min || 1;
32✔
1084

1085
        if (someOfQ.min === 1) {
32✔
1086
          return this.flattenSameOfQueries({
6✔
1087
            type: SearchQueryTypes.OR,
1088
            list: (someOfQ as SearchListQuery).list,
1089
          } as ORSearchQuery);
1090
        }
1091

1092
        if (someOfQ.min === ((query as SearchListQuery).list || []).length) {
26!
1093
          return this.flattenSameOfQueries({
2✔
1094
            type: SearchQueryTypes.AND,
1095
            list: (someOfQ as SearchListQuery).list,
1096
          } as ANDSearchQuery);
1097
        }
1098

1099
        const getAllCombinations = (
24✔
1100
          num: number,
1101
          arr: SearchQueryDTO[],
1102
          start = 0
24✔
1103
        ): SearchQueryDTO[] => {
1104
          if (num <= 0 || num > arr.length || start >= arr.length) {
1,745,182✔
1105
            return null;
341,476✔
1106
          }
1107
          if (num <= 1) {
1,403,706✔
1108
            return arr.slice(start);
747,202✔
1109
          }
1110
          if (num === arr.length - start) {
656,504✔
1111
            return [
126,316✔
1112
              {
1113
                type: SearchQueryTypes.AND,
1114
                list: arr.slice(start),
1115
              } as ANDSearchQuery,
1116
            ];
1117
          }
1118
          const ret: ANDSearchQuery[] = [];
530,188✔
1119
          for (let i = start; i < arr.length; ++i) {
530,188✔
1120
            const subRes = getAllCombinations(num - 1, arr, i + 1);
1,745,158✔
1121
            if (subRes === null) {
1,745,158✔
1122
              break;
530,188✔
1123
            }
1124
            const and: ANDSearchQuery = {
1,214,970✔
1125
              type: SearchQueryTypes.AND,
1126
              list: [arr[i]],
1127
            };
1128
            if (subRes.length === 1) {
1,214,970✔
1129
              if (subRes[0].type === SearchQueryTypes.AND) {
341,476✔
1130
                and.list.push(...(subRes[0] as ANDSearchQuery).list);
126,316✔
1131
              } else {
1132
                and.list.push(subRes[0]);
215,160✔
1133
              }
1134
            } else {
1135
              and.list.push({
873,494✔
1136
                type: SearchQueryTypes.OR,
1137
                list: subRes,
1138
              } as ORSearchQuery);
1139
            }
1140
            ret.push(and);
1,214,970✔
1141
          }
1142

1143
          if (ret.length === 0) {
530,188✔
1144
            return null;
188,712✔
1145
          }
1146
          return ret;
341,476✔
1147
        };
1148

1149
        return this.flattenSameOfQueries({
24✔
1150
          type: SearchQueryTypes.OR,
1151
          list: getAllCombinations(
1152
            someOfQ.min,
1153
            (query as SearchListQuery).list
1154
          ),
1155
        } as ORSearchQuery);
1156
    }
1157
    return query;
3,541,814✔
1158
  }
1159

1160
  /**
1161
   * Assigning IDs to search queries. It is a help / simplification to typeorm,
1162
   * so less parameters are needed to pass down to SQL.
1163
   * Witch SOME_OF query the number of WHERE constrains have O(N!) complexity
1164
   */
1165
  private assignQueryIDs(
1166
    queryIN: SearchQueryDTO,
1167
    id = {value: 1}
244✔
1168
  ): SearchQueryDTO {
1169
    // It is possible that one SQL query contains multiple searchQueries
1170
    // (like: where (<searchQuery1> AND (<searchQuery2>))
1171
    // lets make params unique across multiple queries
1172
    if (id.value === 1) {
360✔
1173
      this.queryIdBase++;
278✔
1174
      if (this.queryIdBase > 10000) {
278!
UNCOV
1175
        this.queryIdBase = 0;
×
1176
      }
1177
    }
1178
    if ((queryIN as SearchListQuery).list) {
360✔
1179
      (queryIN as SearchListQuery).list.forEach((q) =>
34✔
1180
        this.assignQueryIDs(q, id)
116✔
1181
      );
1182
      return queryIN;
34✔
1183
    }
1184
    (queryIN as SearchQueryDTOWithID).queryId =
326✔
1185
      this.queryIdBase + '_' + id.value;
1186
    id.value++;
326✔
1187
    return queryIN;
326✔
1188
  }
1189

1190
  /**
1191
   * Returns only those part of a query tree that only contains directory related search queries
1192
   */
1193
  private filterDirectoryQuery(query: SearchQueryDTO): SearchQueryDTO {
1194
    switch (query.type) {
26!
1195
      case SearchQueryTypes.AND:
1196
        const andRet = {
8✔
1197
          type: SearchQueryTypes.AND,
1198
          list: (query as SearchListQuery).list.map((q) =>
1199
            this.filterDirectoryQuery(q)
16✔
1200
          ),
1201
        } as ANDSearchQuery;
1202
        // if any of the queries contain non dir query thw whole and query is a non dir query
1203
        if (andRet.list.indexOf(null) !== -1) {
8!
1204
          return null;
8✔
1205
        }
1206
        return andRet;
×
1207

1208
      case SearchQueryTypes.OR:
1209
        const orRet = {
×
1210
          type: SearchQueryTypes.OR,
1211
          list: (query as SearchListQuery).list
UNCOV
1212
            .map((q) => this.filterDirectoryQuery(q))
×
UNCOV
1213
            .filter((q) => q !== null),
×
1214
        } as ORSearchQuery;
UNCOV
1215
        if (orRet.list.length === 0) {
×
1216
          return null;
×
1217
        }
UNCOV
1218
        return orRet;
×
1219

1220
      case SearchQueryTypes.any_text:
1221
      case SearchQueryTypes.directory:
1222
        return query;
2✔
1223

1224
      case SearchQueryTypes.SOME_OF:
UNCOV
1225
        throw new Error('"Some of" queries should have been already flattened');
×
1226
    }
1227
    // of none of the above, its not a directory search
1228
    return null;
16✔
1229
  }
1230

1231
  private async getGPSData(query: SearchQueryDTO): Promise<SearchQueryDTO> {
1232
    if ((query as ANDSearchQuery | ORSearchQuery).list) {
74,330✔
1233
      for (
21,366✔
1234
        let i = 0;
21,366✔
1235
        i < (query as ANDSearchQuery | ORSearchQuery).list.length;
1236
        ++i
1237
      ) {
1238
        (query as ANDSearchQuery | ORSearchQuery).list[i] =
74,086✔
1239
          await this.getGPSData(
1240
            (query as ANDSearchQuery | ORSearchQuery).list[i]
1241
          );
1242
      }
1243
    }
1244
    if (
74,330✔
1245
      query.type === SearchQueryTypes.distance &&
74,340✔
1246
      (query as DistanceSearch).from.text
1247
    ) {
1248
      (query as DistanceSearch).from.GPSData =
2✔
1249
        await ObjectManagers.getInstance().LocationManager.getGPSData(
1250
          (query as DistanceSearch).from.text
1251
        );
1252
    }
1253
    return query;
74,330✔
1254
  }
1255

1256
  private encapsulateAutoComplete(
1257
    values: string[],
1258
    type: SearchQueryTypes
1259
  ): Array<AutoCompleteItem> {
1260
    const res: AutoCompleteItem[] = [];
106✔
1261
    values.forEach((value): void => {
106✔
1262
      res.push(new AutoCompleteItem(value, type));
106✔
1263
    });
1264
    return res;
106✔
1265
  }
1266
}
1267

1268
export interface SearchQueryDTOWithID extends SearchQueryDTO {
1269
  queryId: string;
1270
}
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