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

mia-platform / crud-service / 6650818505

26 Oct 2023 07:24AM UTC coverage: 96.95% (+2.6%) from 94.301%
6650818505

push

github

web-flow
fix: review of ci to update to NodeJS v20 (#211)

* ci: update setup-node action to v4

* ci: separate tests of previous MongoDB versions from v7

* ci: update setup-node action to v4

* ci: set a fixed version for coverall github action

* ci: fix coverallapps version

1645 of 1787 branches covered (0.0%)

Branch coverage included in aggregate %.

8494 of 8671 relevant lines covered (97.96%)

6911.51 hits per line

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

94.6
/lib/QueryParser.js
1
/* eslint-disable no-underscore-dangle */
91✔
2
/*
91✔
3
 * Copyright 2023 Mia s.r.l.
91✔
4
 *
91✔
5
 * Licensed under the Apache License, Version 2.0 (the "License");
91✔
6
 * you may not use this file except in compliance with the License.
91✔
7
 * You may obtain a copy of the License at
91✔
8
 *
91✔
9
 *     http://www.apache.org/licenses/LICENSE-2.0
91✔
10
 *
91✔
11
 * Unless required by applicable law or agreed to in writing, software
91✔
12
 * distributed under the License is distributed on an "AS IS" BASIS,
91✔
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
91✔
14
 * See the License for the specific language governing permissions and
91✔
15
 * limitations under the License.
91✔
16
 */
91✔
17

91✔
18
'use strict'
91✔
19

91✔
20
const { ObjectId } = require('mongodb')
91✔
21
const {
91✔
22
  ARRAY_MERGE_ELEMENT_OPERATOR,
91✔
23
  ARRAY_REPLACE_ELEMENT_OPERATOR,
91✔
24
  RAWOBJECTTYPE,
91✔
25
  ARRAY,
91✔
26
  JSON_SCHEMA_ARRAY_TYPE,
91✔
27
  TEXT_INDEX,
91✔
28
  NORMAL_INDEX,
91✔
29
  JSON_SCHEMA_OBJECT_TYPE,
91✔
30
  DATE,
91✔
31
  DATE_FORMATS,
91✔
32
  OBJECTID,
91✔
33
  GEOPOINT,
91✔
34
} = require('../lib/consts')
91✔
35

91✔
36
class QueryParser {
91✔
37
  constructor(collectionDefinition, pathsForRawSchema) {
91✔
38
    this._fieldDefinition = getFieldDefinition(collectionDefinition)
3,681✔
39
    this._nullableFields = getNullableFields(collectionDefinition)
3,681✔
40
    this._rawObjectAccesses = getRawObjectFields(collectionDefinition)
3,681✔
41
    this._pathsForRawSchema = pathsForRawSchema
3,681✔
42
    this._textIndexFields = getTextIndexes(collectionDefinition, TEXT_INDEX)
3,681✔
43
    this._normalIndexes = getTextIndexes(collectionDefinition, NORMAL_INDEX)
3,681✔
44

3,681✔
45
    this.traverseBinded = value => {
3,681✔
46
      traverse(this._fieldDefinition, this.traverseBinded, value)
64,337✔
47
    }
64,337✔
48

3,681✔
49
    this.traverseBody = value => {
3,681✔
50
      traverseBody(this._fieldDefinition, this._nullableFields, value)
150,356✔
51
    }
150,356✔
52

3,681✔
53
    this.traverseCommands = (commands, editableFields) => {
3,681✔
54
      traverseCommands(
60,608✔
55
        this._fieldDefinition,
60,608✔
56
        this._nullableFields,
60,608✔
57
        this._rawObjectAccesses,
60,608✔
58
        commands,
60,608✔
59
        editableFields,
60,608✔
60
        this._pathsForRawSchema
60,608✔
61
      )
60,608✔
62
    }
60,608✔
63

3,681✔
64
    this.traverseTextSearchQuery = (query) => {
3,681✔
65
      startTraverseTextSearchQuery(query, this._normalIndexes, this._fieldDefinition, this.traverseBinded)
78✔
66
    }
78✔
67

3,681✔
68
    this.isTextSearchQuery = (query) => checkIfTextSearchQuery(query)
3,681✔
69

3,681✔
70
    this.parseAndCastBody = this.parseAndCastBody.bind(this)
3,681✔
71
  }
3,681✔
72

91✔
73
  parseAndCast(query) {
91✔
74
    this.traverseBinded(query)
62,443✔
75
  }
62,443✔
76

91✔
77
  parseAndCastBody(doc) {
91✔
78
    this.traverseBody(doc)
150,356✔
79
  }
150,356✔
80

91✔
81
  parseAndCastCommands(commands, editableFields) {
91✔
82
    this.traverseCommands(commands, editableFields)
60,608✔
83
  }
60,608✔
84

91✔
85
  parseAndCastTextSearchQuery(query) {
91✔
86
    this.traverseTextSearchQuery(query)
78✔
87
  }
78✔
88

91✔
89
  // TODO: understand why this!
91✔
90
  isTextSearchQuery(query) {
91✔
91
    return this.isTextSearchQuery(query)
×
92
  }
×
93
}
91✔
94

91✔
95
function castDate(value) {
252✔
96
  if (value === null) {
252✔
97
    return null
6✔
98
  }
6✔
99

246✔
100
  const type = typeof value
246✔
101

246✔
102
  if (type === 'number' || type === 'string') {
252✔
103
    const date = new Date(value)
246✔
104
    if (!isNaN(date.getTime())) { return date }
246✔
105
  }
246✔
106
  throw new Error('Invalid Date')
3✔
107
}
252✔
108

91✔
109
function castToGeoPoint(value) {
246✔
110
  return { type: 'Point', coordinates: value }
246✔
111
}
246✔
112

91✔
113
function castArray(value) {
506✔
114
  return value
506✔
115
}
506✔
116

91✔
117
function castToObjectId(value) {
366✔
118
  if (value === null) {
366✔
119
    return value
6✔
120
  }
6✔
121

360✔
122
  // Number are accepted by mongodb as timestamp
360✔
123
  // for generating the ObjectId in that time
360✔
124
  if (ObjectId.isValid(value) && typeof value !== 'number') {
366✔
125
    return new ObjectId(value)
354✔
126
  }
354✔
127
  throw new Error('Invalid objectId')
6✔
128
}
366✔
129

91✔
130
function callCastFunctionOnValue(value, castFunction) {
599✔
131
  return castFunction(value)
599✔
132
}
599✔
133
callCastFunctionOnValue.supportedTypes = {
91✔
134
  string: true,
91✔
135
  boolean: true,
91✔
136
  number: true,
91✔
137
  Date: true,
91✔
138
  ObjectId: true,
91✔
139
  Array: false,
91✔
140
  GeoPoint: true,
91✔
141
}
91✔
142

91✔
143
function callCastFunctionOnArray(array, castFunction) {
66✔
144
  return array.map(castFunction)
66✔
145
}
66✔
146
callCastFunctionOnArray.supportedTypes = {
91✔
147
  string: true,
91✔
148
  boolean: true,
91✔
149
  number: true,
91✔
150
  Date: true,
91✔
151
  ObjectId: true,
91✔
152
  Array: true,
91✔
153
  GeoPoint: true,
91✔
154
}
91✔
155

91✔
156
function castNearSphere(value, castFunction) {
69✔
157
  const nearShpere = { $geometry: castFunction(value.from) }
69✔
158

69✔
159
  if (value.minDistance) {
69✔
160
    nearShpere.$minDistance = value.minDistance
12✔
161
  }
12✔
162
  if (value.maxDistance) {
69✔
163
    nearShpere.$maxDistance = value.maxDistance
27✔
164
  }
27✔
165

69✔
166
  return nearShpere
69✔
167
}
69✔
168
castNearSphere.supportedTypes = {
91✔
169
  string: false,
91✔
170
  boolean: false,
91✔
171
  number: false,
91✔
172
  Date: false,
91✔
173
  ObjectId: false,
91✔
174
  Array: false,
91✔
175
  GeoPoint: true,
91✔
176
}
91✔
177

91✔
178
function callExists(value) {
15✔
179
  return value
15✔
180
}
15✔
181
callExists.supportedTypes = {
91✔
182
  string: true,
91✔
183
  boolean: true,
91✔
184
  number: true,
91✔
185
  Date: true,
91✔
186
  ObjectId: true,
91✔
187
  Array: true,
91✔
188
  GeoPoint: true,
91✔
189
}
91✔
190

91✔
191
function callElemMatch(value) {
×
192
  return value
×
193
}
×
194
callElemMatch.supportedTypes = {
91✔
195
  string: false,
91✔
196
  boolean: false,
91✔
197
  number: false,
91✔
198
  Date: false,
91✔
199
  ObjectId: false,
91✔
200
  Array: true,
91✔
201
  GeoPoint: false,
91✔
202
}
91✔
203

91✔
204
const castValueFunctions = {
91✔
205
  [DATE]: castDate,
91✔
206
  [OBJECTID]: castToObjectId,
91✔
207
  [GEOPOINT]: castToGeoPoint,
91✔
208
  [ARRAY]: castArray,
91✔
209
}
91✔
210

91✔
211
const traverseOperatorFunctions = {
91✔
212
  $gt: callCastFunctionOnValue,
91✔
213
  $lt: callCastFunctionOnValue,
91✔
214
  $gte: callCastFunctionOnValue,
91✔
215
  $lte: callCastFunctionOnValue,
91✔
216
  $eq: callCastFunctionOnValue,
91✔
217
  $ne: callCastFunctionOnValue,
91✔
218
  $in: callCastFunctionOnArray,
91✔
219
  $nin: callCastFunctionOnArray,
91✔
220
  $all: callCastFunctionOnArray,
91✔
221
  $exists: callExists,
91✔
222
  $nearSphere: castNearSphere,
91✔
223
  $regex: callCastFunctionOnValue,
91✔
224
  $elemMatch: callElemMatch,
91✔
225
  // for $regex
91✔
226
  $options: callCastFunctionOnValue,
91✔
227
}
91✔
228

91✔
229
function identity(value) { return value }
1,052✔
230

91✔
231
// eslint-disable-next-line max-statements
91✔
232
function traverse(fieldDefinition, traverseBinded, query) {
64,415✔
233
  for (const key of Object.keys(query)) {
64,415✔
234
    const fieldType = fieldDefinition[key.split('.')[0]]
64,397✔
235
    if (fieldType === RAWOBJECTTYPE || fieldType === ARRAY) { continue }
64,397✔
236

63,894✔
237
    if (key === '$and' || key === '$or') {
64,397✔
238
      query[key].forEach(traverseBinded)
62,488✔
239
      continue
62,488✔
240
    }
62,488✔
241
    if (key === '$text') {
64,170✔
242
      continue
78✔
243
    }
78✔
244

1,328✔
245
    if (key[0] === '$') {
64,170✔
246
      throw new Error(`Unknown operator: ${key}`)
3✔
247
    }
3✔
248
    if (!fieldDefinition[key]) {
64,170✔
249
      throw new Error(`Unknown field: ${key}`)
27✔
250
    }
27✔
251

1,298✔
252
    const type = fieldDefinition[key]
1,298✔
253
    const castValueFunction = castValueFunctions[type] || identity
3,072✔
254

64,397✔
255
    const value = query[key]
64,397✔
256
    if (value === undefined) { continue }
64,397!
257
    if (value === null || (value && value.constructor !== Object)) {
64,397✔
258
      query[key] = castValueFunction(value, fieldDefinition[key])
548✔
259
      continue
548✔
260
    }
548✔
261

750✔
262
    const queryKey = Object.keys(value)
750✔
263
    for (const operator of queryKey) {
62,490✔
264
      const traverseOperatorFunction = traverseOperatorFunctions[operator]
761✔
265
      if (!traverseOperatorFunction) {
761✔
266
        throw new Error(`Unsupported operator: ${operator}`)
12✔
267
      }
12✔
268
      if (!traverseOperatorFunction.supportedTypes[type]) {
761!
269
        throw new Error(`Unsupported operator: ${operator} for ${type} field`)
×
270
      }
×
271

749✔
272
      const operatorValue = value[operator]
749✔
273
      query[key][operator] = traverseOperatorFunction(operatorValue, castValueFunction)
749✔
274
    }
749✔
275
  }
738✔
276
}
64,415✔
277

91✔
278
function traverseBody(fieldDefinition, nullableFields, doc) {
150,356✔
279
  const keys = Object.keys(doc)
150,356✔
280
  for (const key of keys) {
150,356✔
281
    if (!fieldDefinition[key]) {
452,913✔
282
      throw new Error(`Unknown field: ${key}`)
3✔
283
    }
3✔
284

452,910✔
285
    const type = fieldDefinition[key]
452,910✔
286
    const castValueFunction = castValueFunctions[type]
452,910✔
287
    if (!castValueFunction) {
452,913✔
288
      continue
451,766✔
289
    }
451,766✔
290

1,144✔
291
    const value = doc[key]
1,144✔
292
    if (value === null && nullableFields[key]) {
452,913✔
293
      continue
15✔
294
    }
15✔
295

1,129✔
296
    doc[key] = castValueFunction(value)
1,129✔
297
  }
1,129✔
298
}
150,356✔
299

91✔
300
function getFieldDefinitionCompatibility(collectionDefinition) {
3,650✔
301
  return collectionDefinition
3,650✔
302
    .fields
3,650✔
303
    .reduce((acc, field) => {
3,650✔
304
      acc[field.name] = field.type
39,484✔
305
      return acc
39,484✔
306
    }, {})
3,650✔
307
}
3,650✔
308

91✔
309
function getFieldDefinition(collectionDefinition) {
3,681✔
310
  if (!collectionDefinition.schema) {
3,681✔
311
    return getFieldDefinitionCompatibility(collectionDefinition)
3,650✔
312
  }
3,650✔
313

31✔
314
  return Object
31✔
315
    .entries(collectionDefinition.schema.properties)
31✔
316
    .reduce((acc, [propertyName, jsonSchema]) => {
31✔
317
      if (jsonSchema.__mia_configuration?.type) {
455✔
318
        acc[propertyName] = jsonSchema.__mia_configuration.type
57✔
319
        return acc
57✔
320
      }
57✔
321

398✔
322
      if (jsonSchema.type === 'string' && DATE_FORMATS.includes(jsonSchema.format ?? '')) {
455✔
323
        acc[propertyName] = DATE
75✔
324
        return acc
75✔
325
      }
75✔
326

323✔
327
      if (jsonSchema.type === JSON_SCHEMA_OBJECT_TYPE) {
455✔
328
        acc[propertyName] = RAWOBJECTTYPE
45✔
329
        return acc
45✔
330
      }
45✔
331

278✔
332
      if (jsonSchema.type === JSON_SCHEMA_ARRAY_TYPE) {
455✔
333
        acc[propertyName] = ARRAY
66✔
334
        return acc
66✔
335
      }
66✔
336

212✔
337
      acc[propertyName] = jsonSchema.type
212✔
338
      return acc
212✔
339
    }, {})
31✔
340
}
3,681✔
341

91✔
342
function getNullableFieldsCompatibility(collectionDefinition) {
3,650✔
343
  return collectionDefinition
3,650✔
344
    .fields
3,650✔
345
    .reduce((acc, field) => {
3,650✔
346
      if (field.nullable === true) {
39,484✔
347
        acc[field.name] = true
3,232✔
348
      }
3,232✔
349
      return acc
39,484✔
350
    }, {})
3,650✔
351
}
3,650✔
352

91✔
353
function getNullableFields(collectionDefinition) {
3,681✔
354
  if (!collectionDefinition.schema) {
3,681✔
355
    return getNullableFieldsCompatibility(collectionDefinition)
3,650✔
356
  }
3,650✔
357

31✔
358
  return Object
31✔
359
    .entries(collectionDefinition.schema.properties)
31✔
360
    .reduce((acc, [propertyName, jsonSchema]) => {
31✔
361
      acc[propertyName] = jsonSchema.nullable
455✔
362
      return acc
455✔
363
    }, {})
31✔
364
}
3,681✔
365

91✔
366
function getRawObjectFieldsCompatibility(collectionDefinition) {
3,650✔
367
  return collectionDefinition
3,650✔
368
    .fields
3,650✔
369
    .reduce((acc, field) => {
3,650✔
370
      if (field.type !== RAWOBJECTTYPE) {
39,484✔
371
        return acc
37,662✔
372
      }
37,662✔
373
      acc.push(new RegExp(`^${field.name}\\.`))
1,822✔
374
      return acc
1,822✔
375
    }, [])
3,650✔
376
}
3,650✔
377

91✔
378
function getRawObjectFields(collectionDefinition) {
3,681✔
379
  if (!collectionDefinition.schema) {
3,681✔
380
    return getRawObjectFieldsCompatibility(collectionDefinition)
3,650✔
381
  }
3,650✔
382

31✔
383
  return Object
31✔
384
    .entries(collectionDefinition.schema.properties)
31✔
385
    .reduce((acc, [propertyName, jsonSchema]) => {
31✔
386
      if (jsonSchema.type !== JSON_SCHEMA_OBJECT_TYPE) {
455✔
387
        return acc
389✔
388
      }
389✔
389
      acc.push(new RegExp(`^${propertyName}\\.`))
66✔
390
      return acc
66✔
391
    }, [])
31✔
392
}
3,681✔
393

91✔
394
function getTextIndexes(collectionDefinition, type) {
7,362✔
395
  if (!collectionDefinition.indexes) {
7,362✔
396
    return []
404✔
397
  }
404✔
398
  return collectionDefinition
6,958✔
399
    .indexes
6,958✔
400
    .filter(index => index.type === type)
6,958✔
401
    .reduce((acc, index) => [...acc, ...index.fields.map(el => el.name)], [])
6,958✔
402
}
7,362✔
403

91✔
404
function transformArrayMergeCommands(arrayName, arrayElementFields, fieldName, changesForCommand) {
36✔
405
  Object.keys(arrayElementFields).forEach((key) => {
36✔
406
    changesForCommand[`${arrayName}.$.${key}`] = arrayElementFields[key]
51✔
407
  })
36✔
408
  delete changesForCommand[fieldName]
36✔
409
}
36✔
410

91✔
411
function transformReplaceCommands(arrayName, fieldName, changesForCommand) {
39✔
412
  changesForCommand[`${arrayName}.$`] = changesForCommand[fieldName]
39✔
413
  delete changesForCommand[fieldName]
39✔
414
}
39✔
415

91✔
416
function transformArrayCommands(arrayData, changesForCommand) {
453✔
417
  const { arrayName, arrayElementFields, fieldName, arrayOperation } = arrayData
453✔
418
  if (!arrayElementFields) {
453✔
419
    throw new Error('Invalid value for array operation')
6✔
420
  }
6✔
421
  const isAMerge = (arrayOperation === `.${ARRAY_MERGE_ELEMENT_OPERATOR}`)
447✔
422
  const isAReplace = (arrayOperation === `.${ARRAY_REPLACE_ELEMENT_OPERATOR}`)
447✔
423
  if (isAMerge) {
453✔
424
    transformArrayMergeCommands(arrayName, arrayElementFields, fieldName, changesForCommand)
36✔
425
  }
36✔
426
  if (isAReplace) {
453✔
427
    transformReplaceCommands(arrayName, fieldName, changesForCommand)
39✔
428
  }
39✔
429
}
453✔
430

91✔
431
// eslint-disable-next-line max-statements
91✔
432
function traverseCommands(
60,608✔
433
  fieldDefinition,
60,608✔
434
  nullableFields,
60,608✔
435
  rawObjectAccesses,
60,608✔
436
  commands,
60,608✔
437
  editableFields,
60,608✔
438
  pathsForRawSchema
60,608✔
439
) {
60,608✔
440
  for (const key of Object.keys(commands)) {
60,608✔
441
    if (key === '$unset' || key === '$currentDate') {
60,620✔
442
      continue
42✔
443
    }
42✔
444

60,578✔
445
    const changesForCommand = commands[key]
60,578✔
446
    const fieldKeys = Object.keys(changesForCommand)
60,578✔
447

60,578✔
448
    for (const fieldName of fieldKeys) {
60,605✔
449
      const splitFieldName = fieldName.split('.$')
60,920✔
450
      let isFieldAnArray = (fieldDefinition[splitFieldName[0]] === ARRAY)
60,920✔
451
      if (!isFieldAnArray) {
60,920✔
452
        const rawSchemaForField = getRawSchemaFieldPath(splitFieldName[0], pathsForRawSchema)
60,617✔
453
        isFieldAnArray = rawSchemaForField && rawSchemaForField.type === JSON_SCHEMA_ARRAY_TYPE
60,617✔
454
      }
60,617✔
455
      if (isFieldAnArray) {
60,920✔
456
        const arrayData = {
453✔
457
          arrayName: splitFieldName[0],
453✔
458
          arrayElementFields: commands[key][fieldName],
453✔
459
          fieldName,
453✔
460
          arrayOperation: splitFieldName[1],
453✔
461
        }
453✔
462
        transformArrayCommands(arrayData, changesForCommand)
453✔
463

453✔
464
        continue
453✔
465
      }
453✔
466

60,467✔
467
      if (Boolean(
60,467✔
468
        getRawSchemaFieldPath(fieldName, pathsForRawSchema))
60,467✔
469
        || rawObjectAccesses.some(objKey => objKey.test(fieldName))
60,806✔
470
      ) {
60,920✔
471
        // Allow anything
93✔
472
        continue
93✔
473
      }
93✔
474
      if (!fieldDefinition[fieldName]) {
60,920✔
475
        throw new Error('Unknown fields')
3✔
476
      }
3✔
477
      const type = fieldDefinition[fieldName]
60,371✔
478

60,371✔
479
      if (!editableFields.includes(fieldName)) {
60,920✔
480
        throw new Error(`You cannot edit "${fieldName}" field`)
3✔
481
      }
3✔
482

60,368✔
483
      const castValueFunction = castValueFunctions[type]
60,368✔
484
      if (!castValueFunction) {
60,494✔
485
        continue
60,354✔
486
      }
60,354✔
487
      const value = changesForCommand[fieldName]
14✔
488
      if (value === null && nullableFields[fieldName]) {
60,920✔
489
        continue
3✔
490
      }
3✔
491
      changesForCommand[fieldName] = castValueFunction(value)
11✔
492
    }
11✔
493
  }
60,566✔
494
}
60,608✔
495

91✔
496
function getRawSchemaFieldPath(fieldPath, rawSchemas = {}) {
121,084✔
497
  const { paths } = rawSchemas
121,084✔
498
  if (paths[fieldPath]) {
121,084✔
499
    return paths[fieldPath]
147✔
500
  }
147✔
501

120,937✔
502
  // we do this sort to match the strictest regex
120,937✔
503
  // in some cases we could have a shorter regex matching the pattern
120,937✔
504
  // and returning the element.
120,937✔
505
  const patternProperties = Object
120,937✔
506
    .keys(rawSchemas.patternProperties ?? {})
121,084!
507
    .sort((firstRegex, secondRegex) => secondRegex.length - firstRegex.length)
121,084✔
508

121,084✔
509
  const fromPatternProperties = patternProperties.find(pattern => new RegExp(pattern).test(fieldPath))
121,084✔
510
  if (fromPatternProperties) {
121,084✔
511
    return rawSchemas.patternProperties[fromPatternProperties]
165✔
512
  }
165✔
513

120,772✔
514
  return null
120,772✔
515
}
121,084✔
516

91✔
517

91✔
518
// eslint-disable-next-line max-statements
91✔
519
function traverseTextSearchQuery(query, normalIndexes) {
231✔
520
  const keys = Object.keys(query)
231✔
521
  for (const key of keys) {
231✔
522
    if (key === '$text') {
231✔
523
      if (countTextOccurrences(query[key]) > 1) {
75!
524
        throw new Error(`Query ${query} has more than one $text expression`)
×
525
      }
×
526

75✔
527
      const textQueryKeys = Object.keys(query[key])
75✔
528
      if (!textQueryKeys.includes('$search')) {
75!
529
        throw new Error(`$text search query ${query[key]} must include $search field`)
×
530
      }
×
531

75✔
532
      for (const textOptionKey of textQueryKeys) {
75✔
533
        if (!['$search', '$language', '$caseSensitive', '$diacriticSensitive'].includes(textOptionKey)) {
135!
534
          throw new Error(`Unknown option for $text search query: ${textOptionKey}`)
×
535
        }
×
536
      }
135✔
537

75✔
538
      continue
75✔
539
    }
75✔
540
    if (key === '$nor') {
231!
541
      query.$nor.forEach(clause => {
×
542
        if (checkIfTextSearchQuery(clause)) {
×
543
          throw new Error('$text can not appear in a $nor expression')
×
544
        }
×
545
      })
×
546

×
547
      continue
×
548
    }
×
549
    if (key === '$elemMatch') {
231!
550
      query.$elemMatch.split(',').forEach(clause => {
×
551
        if (checkIfTextSearchQuery(clause)) {
×
552
          throw new Error('$text query can not appear in a $elemMatch query expression')
×
553
        }
×
554
      })
×
555

×
556
      continue
×
557
    }
×
558
    if (key === '$or') {
231✔
559
      query[key].forEach(clause => {
18✔
560
        Object.keys(clause).forEach(orKey => {
33✔
561
          if (orKey[0] !== '$' && !normalIndexes.includes(orKey)) {
33✔
562
            throw new Error('To use a $text query in an $or expression, all clauses in the $or array must be indexed')
3✔
563
          }
3✔
564
          traverseTextSearchQuery(clause, normalIndexes)
30✔
565
        })
33✔
566
      })
18✔
567

18✔
568
      continue
18✔
569
    }
18✔
570
    if (key[0] === '$') {
231✔
571
      const clauses = query[key]
99✔
572
      if (Array.isArray(clauses)) {
99✔
573
        clauses.forEach(clause => traverseTextSearchQuery(clause, normalIndexes))
99✔
574
        continue
99✔
575
      }
99✔
576

×
577
      if ((typeof clauses) === 'object') {
×
578
        // eslint-disable-next-line id-length
×
579
        Object.keys(clauses).forEach(k => traverseTextSearchQuery(clauses[k], normalIndexes))
×
580
        continue
×
581
      }
×
582
    }
99✔
583
  }
231✔
584
}
231✔
585

91✔
586
function startTraverseTextSearchQuery(query, normalIndexes, fieldDefinition, traverseBinded) {
78✔
587
  // $text query should conform both to standard query rules and to $text query rules
78✔
588
  traverse(fieldDefinition, traverseBinded, query)
78✔
589
  traverseTextSearchQuery(query, normalIndexes)
78✔
590
}
78✔
591

91✔
592
const recursiveSearch = (query, searchKey, results = []) => {
91✔
593
  Object.keys(query).forEach(key => {
894✔
594
    const value = query[key]
1,089✔
595
    if (key === searchKey) {
1,089✔
596
      results.push(value)
78✔
597
    } else if (value && typeof value === 'object') {
1,089✔
598
      recursiveSearch(value, searchKey, results)
413✔
599
    }
413✔
600
  })
894✔
601
  return results
894✔
602
}
894✔
603

91✔
604
function checkIfTextSearchQuery(query) {
406✔
605
  const occurrences = recursiveSearch(query, '$text', [])
406✔
606
  return Array.isArray(occurrences) && occurrences.length > 0
406✔
607
}
406✔
608

91✔
609
function countTextOccurrences(query) {
75✔
610
  const occurrences = recursiveSearch(query, '$text', [])
75✔
611
  return occurrences.length
75✔
612
}
75✔
613

91✔
614
// eslint-disable-next-line max-lines
91✔
615
module.exports = QueryParser
91✔
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

© 2025 Coveralls, Inc