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

bpatrik / pigallery2 / 17685697493

12 Sep 2025 08:48PM UTC coverage: 65.828% (+1.7%) from 64.102%
17685697493

push

github

web-flow
Merge pull request #1030 from bpatrik/db-projection

Implement projected (scoped/filtered) gallery. Fixes #1015

1305 of 2243 branches covered (58.18%)

Branch coverage included in aggregate %.

706 of 873 new or added lines in 54 files covered. (80.87%)

16 existing lines in 10 files now uncovered.

4794 of 7022 relevant lines covered (68.27%)

4355.01 hits per line

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

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

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

138

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

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