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

CBIIT / INS-REST-WebService / 25511205793

07 May 2026 05:19PM UTC coverage: 48.98% (+4.5%) from 44.469%
25511205793

Pull #62

github

web-flow
Merge 4fe2bd201 into 61c98c8d0
Pull Request #62: INS-1615

253 of 446 branches covered (56.73%)

Branch coverage included in aggregate %.

182 of 344 new or added lines in 6 files covered. (52.91%)

24 existing lines in 1 file now uncovered.

443 of 975 relevant lines covered (45.44%)

17.17 hits per line

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

62.12
/Services/resourceQueryGenerator.js
1
const {
2
  RESOURCE_HIGHLIGHT_FIELDS,
3
  RESOURCE_IDENTIFIER_FIELD,
4
  RESOURCE_SEARCH_FIELDS,
5
} = require('../Utils/resourceFields.js');
2✔
6

7
let queryGenerator = {};
2✔
8

9
queryGenerator.getSearchAggregationQuery = (searchText) => {
2✔
NEW
10
  let body = {
×
11
    size: 10,
12
    from: 0
13
  };
14

NEW
15
  let compoundQuery = {};
×
NEW
16
  compoundQuery.bool = {};
×
NEW
17
  compoundQuery.bool.must = [];
×
18

NEW
19
  const strArr = searchText.trim().split(" ");
×
NEW
20
  const result = [];
×
NEW
21
  strArr.forEach((term) => {
×
NEW
22
    const t = term.trim();
×
NEW
23
    if (t.length > 2) {
×
NEW
24
      result.push(t);
×
25
    }
26
  });
NEW
27
  const keywords = result.length === 0 ? "" : result.join(" ");
×
NEW
28
  if(keywords != ""){
×
NEW
29
    const termArr = keywords.split(" ");
×
NEW
30
    termArr.forEach((term) => {
×
NEW
31
      let searchTerm = term.trim();
×
NEW
32
      if(searchTerm != ""){
×
NEW
33
        let clause = {};
×
NEW
34
        clause.bool = {};
×
NEW
35
        clause.bool.should = [];
×
NEW
36
        let dsl = {};
×
NEW
37
        dsl.multi_match = {};
×
NEW
38
        dsl.multi_match.query = searchTerm;
×
39
        //dsl.multi_match.analyzer = "standard_analyzer";
NEW
40
        dsl.multi_match.fields = [
×
41
          'resource_title',
42
          // "data_resource_name",
43
          // "resource_name",
44
          // "desc",
45
          // "primary_resource_scope",
46
          // "poc",
47
          // "poc_email",
48
          // "published_in",
49
          // "program_name",
50
          // "project_name"
51
        ];
52
        // clause.bool.should.push(dsl);
NEW
53
        let nestedFields = [
×
54
        ];
NEW
55
        nestedFields.map((f) => {
×
NEW
56
          let idx = f.indexOf('.');
×
NEW
57
          let parent = f.substring(0, idx);
×
NEW
58
          dsl = {};
×
NEW
59
          dsl.nested = {};
×
NEW
60
          dsl.nested.path = parent;
×
NEW
61
          dsl.nested.query = {};
×
NEW
62
          dsl.nested.query.match = {};
×
NEW
63
          dsl.nested.query.match[f] = {"query":searchTerm};
×
64
          // clause.bool.should.push(dsl);
65
        });
NEW
66
        dsl = {};
×
NEW
67
        dsl.nested = {};
×
NEW
68
        dsl.nested.path = "projects";
×
NEW
69
        dsl.nested.query = {};
×
NEW
70
        dsl.nested.query.bool = {};
×
NEW
71
        dsl.nested.query.bool.should = [];
×
NEW
72
        let m = {};
×
NEW
73
        m.match = {
×
74
          "projects.p_k": searchTerm
75
        };
76
        // dsl.nested.query.bool.should.push(m);
NEW
77
        m = {};
×
NEW
78
        m.nested = {};
×
NEW
79
        m.nested.path = "projects.p_v";
×
NEW
80
        m.nested.query = {};
×
NEW
81
        m.nested.query.match = {};
×
NEW
82
        m.nested.query.match["projects.p_v.k"] = {"query":searchTerm};
×
83
        // dsl.nested.query.bool.should.push(m);
84
        // clause.bool.should.push(dsl);
85
    
NEW
86
        dsl = {};
×
NEW
87
        dsl.nested = {};
×
NEW
88
        dsl.nested.path = "additional";
×
NEW
89
        dsl.nested.query = {};
×
NEW
90
        dsl.nested.query.bool = {};
×
NEW
91
        dsl.nested.query.bool.should = [];
×
NEW
92
        m = {};
×
NEW
93
        m.match = {
×
94
          "additional.attr_name": searchTerm
95
        };
96
        // dsl.nested.query.bool.should.push(m);
NEW
97
        m = {};
×
NEW
98
        m.nested = {};
×
NEW
99
        m.nested.path = "additional.attr_set";
×
NEW
100
        m.nested.query = {};
×
NEW
101
        m.nested.query.match = {};
×
NEW
102
        m.nested.query.match["additional.attr_set.k"] = {"query":searchTerm};
×
103
        // dsl.nested.query.bool.should.push(m);
104
        // clause.bool.should.push(dsl);
NEW
105
        compoundQuery.bool.must.push(clause);
×
106
      }
107
    });
108
  } else {
NEW
109
    return null;
×
110
  }
111

NEW
112
  if (compoundQuery.bool.must.length > 0) {
×
NEW
113
    body.query = compoundQuery;
×
114
  }
115
  
NEW
116
  let agg = {};
×
NEW
117
  agg.myAgg = {};
×
NEW
118
  agg.myAgg.terms = {};
×
NEW
119
  agg.myAgg.terms.field = "dbGaP_phs";
×
NEW
120
  agg.myAgg.terms.size = 1000;
×
121

122
  // body.aggs = agg;
NEW
123
  return body;
×
124
};
125

126
queryGenerator.getFiltersClause = (filters) => {
2✔
127
  // Handle null or invalid parameter
128
  if (
54✔
129
    !filters ||
147✔
130
    typeof filters !== 'object' ||
131
    Array.isArray(filters)
132
  ) {
133
    return null;
9✔
134
  }
135

136
  // Ignore filters with no values selected
137
  const cleanedFilters = Object.fromEntries(
45✔
138
    Object.entries(filters).filter(([field, values]) => values.length > 0)
74✔
139
  );
140

141
  // If no filters, then return null
142
  if (Object.entries(cleanedFilters).length <= 0) {
45✔
143
    return null;
9✔
144
  }
145

146
  const clause = Object.entries(cleanedFilters).map(([field, values]) => ({
72✔
147
    'terms': {
148
      [field]: values
149
    }
150
  }));
151

152
  return clause;
36✔
153
}
154

155
queryGenerator.getHighlightClause = () => {
2✔
156
  const fieldsMap = RESOURCE_HIGHLIGHT_FIELDS.reduce((acc, field) => {
33✔
157
    acc[field] = { number_of_fragments: 0 };
396✔
158
    return acc;
396✔
159
  }, {});
160

161
  return {
33✔
162
    pre_tags: ["<b>"],
163
    post_tags: ["</b>"],
164
    fields: fieldsMap,
165
  };
166
};
167

168
queryGenerator.getSortClause = (options) => {
2✔
169
  // Handle null or wrong type
170
  if (!options || typeof options !== 'object' || Array.isArray(options)) {
32✔
171
    return null;
3✔
172
  }
173

174
  // Check whether a sort object exists
175
  if (!options.sort || typeof options.sort !== 'object' || Array.isArray(options.sort)) {
29✔
176
    return null;
3✔
177
  }
178

179
  // Check whether the sort object has a 'k' and 'v' property
180
  if (!options.sort.k || !options.sort.v) {
26!
NEW
181
    return null;
×
182
  }
183

184
  // Check whether the sort property and direction are strings
185
  if (typeof options.sort.k !== 'string' || typeof options.sort.v !== 'string') {
26!
NEW
186
    return null;
×
187
  }
188

189
  // Return the sort clause
190
  return {
26✔
191
    [options.sort.k]: options.sort.v,
192
  };
193
};
194

195
queryGenerator.getTextSearchConditions = (searchText) => {
2✔
196
  // Handle null parameter
197
  if (!searchText || typeof searchText !== 'string') {
54✔
198
    return null;
15✔
199
  }
200

201
  const conditions = [];
39✔
202
  const searchTerms = searchText.trim().split(' ').map(
39✔
203
    term => term.trim()
75✔
204
  ).filter(
205
    term => term.length > 2
75✔
206
  );
207
  const uniqueSearchTerms = searchTerms.filter((term, idx) => {
39✔
208
    return searchTerms.indexOf(term) === idx;
72✔
209
  });
210

211
  // Check again that actual search terms exist
212
  if (uniqueSearchTerms.length <= 0) {
39✔
213
    return null;
3✔
214
  }
215

216
  // Add a search condition for finding each term in any of the resource fields
217
  uniqueSearchTerms.forEach((term) => {
36✔
218
    const dsl = {
72✔
219
      'multi_match': {
220
        'query': term,
221
        'fields': RESOURCE_SEARCH_FIELDS,
222
      }
223
    };
224

225
    conditions.push(dsl);
72✔
226
  });
227

228
  return conditions;
36✔
229
};
230

231
/**
232
 * Constructs a search query for the resources index
233
 * @param {String} searchText The text to search for
234
 * @param {Object} filters The filters to apply
235
 * @param {Object} options Sort and pagination options
236
 * @param {Array} returnFields The fields to return
237
 * @returns {Object|null} The OpenSearch query body object, or a scroll request descriptor when deep pagination is required, or null if validation fails.
238
 */
239
queryGenerator.getSearchQueryV2 = (searchText, filters, options, returnFields) => {
2✔
240
  const MAX_RESULT_WINDOW = 10000;
41✔
241
  const DEFAULT_SCROLL_KEEPALIVE = '2m';
41✔
242

243
  const body = {
41✔
244
    from: 0,
245
    size: 10,
246
  };
247
  const compoundQuery = {
41✔
248
    'bool': {
249
    },
250
  };
251
  let filtersClause;
252
  let sortClause;
253
  let textSearchClause;
254

255
  // Check searchText type
256
  // Loose null equality treats undefined as null
257
  if (searchText != null && typeof searchText !== 'string') {
41✔
258
    return null;
1✔
259
  }
260

261
  // Check filters type
262
  if (filters != null && (typeof filters !== 'object' || Array.isArray(filters))) {
40✔
263
    return null;
2✔
264
  }
265

266
  // Check options type
267
  if (options != null && (typeof options !== 'object' || Array.isArray(options))) {
38✔
268
    return null;
2✔
269
  }
270

271
  // Check returnFields type and length
272
  if (!Array.isArray(returnFields) || returnFields.length <= 0) {
36✔
273
    return null;
4✔
274
  }
275

276
  filtersClause = queryGenerator.getFiltersClause(filters);
32✔
277
  textSearchClause = queryGenerator.getTextSearchConditions(searchText);
32✔
278

279
  body['_source'] = returnFields ?? false;
32!
280

281
  // We already verified that options is the right type if it's not null
282
  // We must still verify that options is truthy
283
  if (options) {
29!
284
    const pageSize = Number(options.pageInfo?.pageSize);
29✔
285
    if (Number.isFinite(pageSize) && pageSize > 0) {
29✔
286
      body.size = Math.trunc(pageSize);
21✔
287
    }
288

289
    const page = Number(options.pageInfo?.page);
29✔
290
    if (Number.isFinite(page) && page > 0) {
29✔
291
      body.from = body.size * (Math.trunc(page) - 1);
22✔
292
    }
293
  }
294

295
  sortClause = queryGenerator.getSortClause(options);
32✔
296

297
  if (sortClause != null) {
32✔
298
    body.sort = [sortClause];
26✔
299
  }
300

301
  if (filtersClause != null) {
32✔
302
    compoundQuery.bool['filter'] = filtersClause;
26✔
303
  }
304

305
  if (textSearchClause != null) {
32✔
306
    compoundQuery.bool.must = textSearchClause;
23✔
307
  }
308

309
  if (compoundQuery.bool.must?.length > 0 || compoundQuery.bool.filter) {
32✔
310
    body.query = compoundQuery;
28✔
311
  }
312

313
  body.highlight = queryGenerator.getHighlightClause();
32✔
314

315
  const requestedFrom = Number(body.from ?? 0);
32✔
316
  const requestedSize = Number(body.size ?? 10);
41✔
317
  const safeRequestedFrom = Number.isFinite(requestedFrom) ? requestedFrom : 0;
41✔
318
  const safeRequestedSize = Number.isFinite(requestedSize) ? requestedSize : 10;
41✔
319
  const needsScroll = (safeRequestedFrom + safeRequestedSize) > MAX_RESULT_WINDOW;
41✔
320

321
  if (!needsScroll) {
28!
322
    return body;
4✔
323
  }
324

325
  // Scroll requests should not rely on `from`/deep pagination; we fetch batches and discard until offset.
326
  const scrollBody = {
4✔
327
    ...body,
328
    from: 0,
329
  };
330

331
  return {
4✔
332
    useScroll: true,
333
    scroll: DEFAULT_SCROLL_KEEPALIVE,
334
    requestedFrom: safeRequestedFrom,
335
    requestedSize: safeRequestedSize,
336
    body: scrollBody,
337
  };
338
};
339

340
/**
341
 * Generates a bucket aggregation query on resource properties
342
 * @param {String} searchText The text to search for
343
 * @param {Object} searchFilters The filters to apply
344
 * @param {String} excludedField The field to exclude from the filters
345
 * @returns {Object} Opensearch query to retrieve filter counts
346
 */
347
queryGenerator.getResourceFiltersQuery = (searchText, searchFilters, excludedField) => {
2✔
348
  // Borrow some of the search query
NEW
349
  const body = {};
×
NEW
350
  const compoundQuery = {
×
351
    'bool': {
352
      'must': [],
353
    },
354
  };
NEW
355
  const filtersClause = queryGenerator.getFiltersClause(Object.fromEntries(
×
356
    Object.entries(searchFilters).filter( // Remove excluded field from filters
NEW
357
      ([filterName]) => filterName != excludedField
×
358
    )
359
  ));
NEW
360
  const textSearchClause = queryGenerator.getTextSearchConditions(searchText);
×
361

NEW
362
  if (filtersClause != null) {
×
NEW
363
    compoundQuery.bool['filter'] = filtersClause;
×
364
  }
365

NEW
366
  if (textSearchClause != null) {
×
NEW
367
    compoundQuery.bool.must = textSearchClause;
×
368
  }
369

NEW
370
  if (compoundQuery.bool.must.length > 0 || compoundQuery.bool.filter) {
×
NEW
371
    body.query = compoundQuery;
×
372
  }
373

374
  // Customize search query
NEW
375
  body.aggs = {};
×
NEW
376
  body.size = 0;
×
377

378
  // Aggregate on the target field
NEW
379
  body.aggs[excludedField] = {
×
380
    'terms': {
381
      'field': excludedField,
382
      'order': {
383
        '_key': 'asc'
384
      },
385
      'size': 100000
386
    }
387
  };
388

389
  return body;
2✔
390
};
391
/**
392
 * Generates a count query for Opensearch using the same filters as getSearchQueryV2 and getResourceFiltersQuery.
393
 * @param {String} searchText The text to search for
394
 * @param {Object} searchFilters The filters to apply
395
 * @returns {Object} Opensearch count query
396
 */
397
queryGenerator.getResourceCountQuery = (searchText, searchFilters) => {
2✔
398
  const body = {};
22✔
399

400
  // Build the main compound query using existing query logic
401
  const compoundQuery = {
22✔
402
    'bool': {
403
      'must': [],
404
    },
405
  };
406

407
  const filtersClause = queryGenerator.getFiltersClause(searchFilters);
22✔
408
  const textSearchClause = queryGenerator.getTextSearchConditions(searchText);
22✔
409

410
  if (filtersClause != null) {
22✔
411
    compoundQuery.bool['filter'] = filtersClause;
10✔
412
  }
413

414
  if (textSearchClause != null) {
22✔
415
    compoundQuery.bool.must = textSearchClause;
13✔
416
  }
417

418
  if (compoundQuery.bool.must.length > 0 || compoundQuery.bool.filter) {
22✔
419
    body.query = compoundQuery;
16✔
420
  }
421

422
  return body;
2✔
423
};
424

425
queryGenerator.getResourceByIdQuery = (id) => {
2✔
NEW
426
  return {
×
427
    size: 1,
428
    from: 0,
429
    query: {
430
      term: {
431
        [RESOURCE_IDENTIFIER_FIELD]: id,
432
      },
433
    },
434
  };
435
};
436

437
module.exports = queryGenerator;
2✔
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