• 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

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

88✔
17
/* eslint-disable max-lines */
88✔
18

88✔
19
'use strict'
88✔
20

88✔
21
const Ajv = require('ajv')
88✔
22
const ajvFormats = require('ajv-formats')
88✔
23
const ajvKeywords = require('ajv-keywords')
88✔
24

88✔
25
const { pipeline } = require('stream/promises')
88✔
26
const lget = require('lodash.get')
88✔
27
const through2 = require('through2')
88✔
28

88✔
29
const lisEmpty = require('lodash.isempty')
88✔
30

88✔
31
const {
88✔
32
  SORT,
88✔
33
  PROJECTION,
88✔
34
  RAW_PROJECTION,
88✔
35
  QUERY,
88✔
36
  LIMIT,
88✔
37
  SKIP,
88✔
38
  STATE,
88✔
39
  INVALID_USERID,
88✔
40
  UPDATERID,
88✔
41
  UPDATEDAT,
88✔
42
  CREATORID,
88✔
43
  CREATEDAT,
88✔
44
  __STATE__,
88✔
45
  SCHEMA_CUSTOM_KEYWORDS,
88✔
46
  rawProjectionDictionary,
88✔
47
  BAD_REQUEST_ERROR_STATUS_CODE,
88✔
48
  INTERNAL_SERVER_ERROR_STATUS_CODE,
88✔
49
  UNIQUE_INDEX_ERROR_STATUS_CODE,
88✔
50
  NOT_ACCEPTABLE,
88✔
51
  UNSUPPORTED_MIME_TYPE_STATUS_CODE,
88✔
52
} = require('./consts')
88✔
53

88✔
54
const getAccept = require('./acceptHeaderParser')
88✔
55
const BadRequestError = require('./BadRequestError')
88✔
56
const BatchWritableStream = require('./BatchWritableStream')
88✔
57

88✔
58
const { SCHEMAS_ID } = require('./schemaGetters')
88✔
59
const { getAjvResponseValidationFunction, shouldValidateStream, shouldValidateItem } = require('./validatorGetters')
88✔
60
const { getFileMimeParser, getFileMimeStringify } = require('./mimeTypeTransform')
88✔
61

88✔
62
const OPTIONS_INCOMPATIBILITY_ERROR_CODE = 2
88✔
63
const UNIQUE_INDEX_MONGO_ERROR_CODE = 11000
88✔
64

88✔
65
const PROMETHEUS_OP_TYPE = {
88✔
66
  FETCH: 'fetch',
88✔
67
  INSERT_OR_UPDATE: 'insert_or_update',
88✔
68
  DELETE: 'delete',
88✔
69
  CHANGE_STATE: 'change_state',
88✔
70
}
88✔
71

88✔
72

88✔
73
// eslint-disable-next-line max-statements
88✔
74
module.exports = async function getHttpInterface(fastify, options) {
88✔
75
  if (!fastify.crudService) { throw new Error('`fastify.crudService` is undefined') }
3,668!
76
  if (!fastify.queryParser) { throw new Error('`fastify.queryParser` is undefined') }
3,668!
77
  if (!fastify.castCollectionId) { throw new Error('`fastify.castCollectionId` is undefined') }
3,668!
78
  if (!fastify.castResultsAsStream) { throw new Error('`fastify.castResultsAsStream` is undefined') }
3,668!
79
  if (!fastify.castItem) { throw new Error('`fastify.castItem` is undefined') }
3,668!
80
  if (!fastify.allFieldNames) { throw new Error('`fastify.allFieldNames` is undefined') }
3,668!
81
  if (!fastify.jsonSchemaGenerator) { throw new Error('`fastify.jsonSchemaGenerator` is undefined') }
3,668!
82
  if (!fastify.jsonSchemaGeneratorWithNested) { throw new Error('`fastify.jsonSchemaGeneratorWithNested` is undefined') }
3,668!
83
  if (!fastify.userIdHeaderKey) { throw new Error('`fastify.userIdHeaderKey` is undefined') }
3,668!
84
  if (!fastify.modelName) { throw new Error('`fastify.modelName` is undefined') }
3,668!
85

3,668✔
86
  const {
3,668✔
87
    registerGetters = true,
3,668✔
88
    registerSetters = true,
3,668✔
89
    registerLookup = false,
3,668✔
90
  } = options
3,668✔
91

3,668✔
92
  const validateOutput = fastify.validateOutput ?? false
3,668!
93

3,668✔
94
  const NESTED_SCHEMAS_BY_ID = {
3,668✔
95
    [SCHEMAS_ID.GET_LIST]: fastify.jsonSchemaGeneratorWithNested.generateGetListJSONSchema(),
3,668✔
96
    [SCHEMAS_ID.GET_LIST_LOOKUP]: fastify.jsonSchemaGeneratorWithNested.generateGetListLookupJSONSchema(),
3,668✔
97
    [SCHEMAS_ID.GET_ITEM]: fastify.jsonSchemaGeneratorWithNested.generateGetItemJSONSchema(),
3,668✔
98
    [SCHEMAS_ID.EXPORT]: fastify.jsonSchemaGeneratorWithNested.generateExportJSONSchema(),
3,668✔
99
    [SCHEMAS_ID.POST_ITEM]: fastify.jsonSchemaGeneratorWithNested.generatePostJSONSchema(),
3,668✔
100
    [SCHEMAS_ID.POST_BULK]: fastify.jsonSchemaGeneratorWithNested.generateBulkJSONSchema(),
3,668✔
101
    // it is not possible to validate a stream
3,668✔
102
    [SCHEMAS_ID.POST_FILE]: { body: {} },
3,668✔
103
    [SCHEMAS_ID.PATCH_FILE]: { body: {} },
3,668✔
104
    [SCHEMAS_ID.DELETE_ITEM]: fastify.jsonSchemaGeneratorWithNested.generateDeleteJSONSchema(),
3,668✔
105
    [SCHEMAS_ID.DELETE_LIST]: fastify.jsonSchemaGeneratorWithNested.generateDeleteListJSONSchema(),
3,668✔
106
    [SCHEMAS_ID.PATCH_ITEM]: fastify.jsonSchemaGeneratorWithNested.generatePatchJSONSchema(),
3,668✔
107
    [SCHEMAS_ID.PATCH_MANY]: fastify.jsonSchemaGeneratorWithNested.generatePatchManyJSONSchema(),
3,668✔
108
    [SCHEMAS_ID.PATCH_BULK]: fastify.jsonSchemaGeneratorWithNested.generatePatchBulkJSONSchema(),
3,668✔
109
    [SCHEMAS_ID.UPSERT_ONE]: fastify.jsonSchemaGeneratorWithNested.generateUpsertOneJSONSchema(),
3,668✔
110
    [SCHEMAS_ID.COUNT]: fastify.jsonSchemaGeneratorWithNested.generateCountJSONSchema(),
3,668✔
111
    [SCHEMAS_ID.VALIDATE]: fastify.jsonSchemaGeneratorWithNested.generateValidateJSONSchema(),
3,668✔
112
    [SCHEMAS_ID.CHANGE_STATE]: fastify.jsonSchemaGeneratorWithNested.generateChangeStateJSONSchema(),
3,668✔
113
    [SCHEMAS_ID.CHANGE_STATE_MANY]: fastify.jsonSchemaGeneratorWithNested.generateChangeStateManyJSONSchema(),
3,668✔
114
  }
3,668✔
115

3,668✔
116
  // for each collection define its dedicated validator instance
3,668✔
117
  const ajv = new Ajv({
3,668✔
118
    coerceTypes: true,
3,668✔
119
    useDefaults: true,
3,668✔
120
    allowUnionTypes: true,
3,668✔
121
    // allow properties and pattern properties to overlap -> this should help validating nested fields
3,668✔
122
    allowMatchingProperties: true,
3,668✔
123
  })
3,668✔
124
  ajvFormats(ajv)
3,668✔
125
  ajvKeywords(ajv, 'instanceof')
3,668✔
126
  ajv.addVocabulary(Object.values(SCHEMA_CUSTOM_KEYWORDS))
3,668✔
127

3,668✔
128
  fastify.setValidatorCompiler(({ schema }) => {
3,668✔
129
    const uniqueId = schema[SCHEMA_CUSTOM_KEYWORDS.UNIQUE_OPERATION_ID]
124,888✔
130
    const [collectionName, schemaId, subSchemaPath] = uniqueId.split('__MIA__')
124,888✔
131
    const nestedSchema = NESTED_SCHEMAS_BY_ID[schemaId]
124,888✔
132
    const subSchema = lget(nestedSchema, subSchemaPath)
124,888✔
133
    fastify.log.debug({ collectionName, schemaPath: subSchemaPath, schemaId }, 'collection schema info')
124,888✔
134

124,888✔
135
    // this is made to prevent to shows on swagger all properties with dot notation of RawObject with schema.
124,888✔
136
    return ajv.compile(subSchema)
124,888✔
137
  })
3,668✔
138

3,668✔
139
  fastify.addHook('preHandler', injectContextInRequest)
3,668✔
140
  fastify.addHook('preHandler', request => parseEncodedJsonQueryParams(fastify.log, request))
3,668✔
141
  fastify.setErrorHandler(customErrorHandler)
3,668✔
142

3,668✔
143
  if (registerSetters) {
3,668✔
144
    fastify.post(
3,068✔
145
      '/',
3,068✔
146
      { schema: fastify.jsonSchemaGenerator.generatePostJSONSchema() },
3,068✔
147
      handleInsertOne
3,068✔
148
    )
3,068✔
149
    fastify.post(
3,068✔
150
      '/validate',
3,068✔
151
      { schema: fastify.jsonSchemaGenerator.generateValidateJSONSchema() },
3,068✔
152
      handleValidate
3,068✔
153
    )
3,068✔
154
    fastify.delete(
3,068✔
155
      '/:id',
3,068✔
156
      { schema: fastify.jsonSchemaGenerator.generateDeleteJSONSchema() },
3,068✔
157
      handleDeleteId
3,068✔
158
    )
3,068✔
159
    fastify.delete(
3,068✔
160
      '/',
3,068✔
161
      { schema: fastify.jsonSchemaGenerator.generateDeleteListJSONSchema() },
3,068✔
162
      handleDeleteList
3,068✔
163
    )
3,068✔
164

3,068✔
165
    const patchIdSchema = fastify.jsonSchemaGenerator.generatePatchJSONSchema()
3,068✔
166
    fastify.patch(
3,068✔
167
      '/:id',
3,068✔
168
      {
3,068✔
169
        schema: patchIdSchema,
3,068✔
170
        config: {
3,068✔
171
          itemValidator: shouldValidateItem(patchIdSchema.response['200'], validateOutput),
3,068✔
172
        },
3,068✔
173
      },
3,068✔
174
      handlePatchId
3,068✔
175
    )
3,068✔
176
    fastify.patch(
3,068✔
177
      '/',
3,068✔
178
      { schema: fastify.jsonSchemaGenerator.generatePatchManyJSONSchema() },
3,068✔
179
      handlePatchMany
3,068✔
180
    )
3,068✔
181

3,068✔
182
    const upsertOneSchema = fastify.jsonSchemaGenerator.generateUpsertOneJSONSchema()
3,068✔
183
    fastify.post(
3,068✔
184
      '/upsert-one', {
3,068✔
185
        schema: upsertOneSchema,
3,068✔
186
        config: {
3,068✔
187
          itemValidator: shouldValidateItem(upsertOneSchema.response['200'], validateOutput),
3,068✔
188
        },
3,068✔
189
      },
3,068✔
190
      handleUpsertOne
3,068✔
191
    )
3,068✔
192

3,068✔
193
    fastify.post('/bulk', {
3,068✔
194
      schema: fastify.jsonSchemaGenerator.generateBulkJSONSchema(),
3,068✔
195
    }, handleInsertMany)
3,068✔
196
    fastify.patch('/bulk', {
3,068✔
197
      schema: fastify.jsonSchemaGenerator.generatePatchBulkJSONSchema(),
3,068✔
198
    }, handlePatchBulk)
3,068✔
199
    fastify.post(
3,068✔
200
      '/:id/state',
3,068✔
201
      { schema: fastify.jsonSchemaGenerator.generateChangeStateJSONSchema() },
3,068✔
202
      handleChangeStateById
3,068✔
203
    )
3,068✔
204
    fastify.post(
3,068✔
205
      '/state',
3,068✔
206
      { schema: fastify.jsonSchemaGenerator.generateChangeStateManyJSONSchema() },
3,068✔
207
      handleChangeStateMany
3,068✔
208
    )
3,068✔
209

3,068✔
210
    const importPostSchema = fastify.jsonSchemaGenerator.generatePostImportJSONSchema()
3,068✔
211
    fastify.post(
3,068✔
212
      '/import',
3,068✔
213
      {
3,068✔
214
        schema: importPostSchema,
3,068✔
215
        config: {
3,068✔
216
          itemValidator: getAjvResponseValidationFunction(importPostSchema.streamBody),
3,068✔
217
          validateImportOptions: getAjvResponseValidationFunction(importPostSchema.optionSchema,
3,068✔
218
            { removeAdditional: false }
3,068✔
219
          ),
3,068✔
220
        },
3,068✔
221
      },
3,068✔
222
      handleCollectionImport
3,068✔
223
    )
3,068✔
224

3,068✔
225
    const importPatchSchema = fastify.jsonSchemaGenerator.generatePatchImportJSONSchema()
3,068✔
226
    fastify.patch(
3,068✔
227
      '/import',
3,068✔
228
      {
3,068✔
229
        schema: importPatchSchema,
3,068✔
230
        config: {
3,068✔
231
          itemValidator: getAjvResponseValidationFunction(importPatchSchema.streamBody),
3,068✔
232
          validateImportOptions: getAjvResponseValidationFunction(importPatchSchema.optionSchema,
3,068✔
233
            { removeAdditional: false }
3,068✔
234
          ),
3,068✔
235
        },
3,068✔
236
      },
3,068✔
237
      handleCollectionImport
3,068✔
238
    )
3,068✔
239
  }
3,068✔
240

3,668✔
241
  if (registerLookup) {
3,668✔
242
    if (!fastify.lookupProjection) { throw new Error('`fastify.lookupProjection` is undefined') }
4!
243
    const listLookupSchema = fastify.jsonSchemaGenerator.generateGetListLookupJSONSchema()
4✔
244
    fastify.get('/', {
4✔
245
      schema: listLookupSchema,
4✔
246
      config: {
4✔
247
        streamValidator: shouldValidateStream(listLookupSchema.response['200'], validateOutput),
4✔
248
        replyType: () => 'application/json',
4✔
249
      },
4✔
250
    }, handleGetListLookup)
4✔
251
  }
4✔
252

3,668✔
253
  if (registerGetters) {
3,668✔
254
    const getItemJSONSchema = fastify.jsonSchemaGenerator.generateGetItemJSONSchema()
3,660✔
255
    fastify.get('/export', {
3,660✔
256
      schema: fastify.jsonSchemaGenerator.generateExportJSONSchema(),
3,660✔
257
      config: {
3,660✔
258
        streamValidator: shouldValidateStream(getItemJSONSchema.response['200'], validateOutput),
3,660✔
259
        replyType: (acceptHeader) => {
3,660✔
260
          const accept = getAccept(acceptHeader)
852✔
261

852✔
262
          if (!accept || accept === '*/*') { return 'application/x-ndjson' }
852!
263

852✔
264
          return accept
852✔
265
        },
3,660✔
266
      },
3,660✔
267
    }, handleGetList)
3,660✔
268
    fastify.get('/count', { schema: fastify.jsonSchemaGenerator.generateCountJSONSchema() }, handleCount)
3,660✔
269
    fastify.setNotFoundHandler(notFoundHandler)
3,660✔
270
    fastify.get('/', {
3,660✔
271
      schema: fastify.jsonSchemaGenerator.generateGetListJSONSchema(),
3,660✔
272
      config: {
3,660✔
273
        streamValidator: shouldValidateStream(getItemJSONSchema.response['200'], validateOutput),
3,660✔
274
        replyType: () => 'application/json',
3,660✔
275
      },
3,660✔
276
    }, handleGetList)
3,660✔
277
    fastify.get('/:id', {
3,660✔
278
      schema: getItemJSONSchema,
3,660✔
279
      config: {
3,660✔
280
        itemValidator: shouldValidateItem(getItemJSONSchema.response['200'], validateOutput),
3,660✔
281
      },
3,660✔
282
    }, handleGetId)
3,660✔
283
  }
3,660✔
284
}
3,668✔
285

88✔
286
// eslint-disable-next-line max-statements
88✔
287
async function handleCollectionImport(request, reply) {
72✔
288
  if (this.customMetrics) {
72!
289
    this.customMetrics.collectionInvocation.inc({
×
290
      collection_name: this.modelName,
×
291
      type: PROMETHEUS_OP_TYPE.IMPORT,
×
292
    })
×
293
  }
×
294

72✔
295
  if (!request.isMultipart()) {
72!
296
    return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, 'Request is not multipart')
×
297
  }
×
298

72✔
299
  const data = await request.file()
72✔
300
  if (!data) {
72!
301
    return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, 'Missing file')
×
302
  }
×
303
  const { file, mimetype, fields } = data
72✔
304
  const parsingOptions = Object.fromEntries(Object.values(fields)
72✔
305
    .filter(field => field.type === 'field')
72✔
306
    .map(({ fieldname, value }) => [fieldname, value]))
72✔
307

72✔
308
  const { itemValidator, validateImportOptions } = reply.context.config
72✔
309
  const isValid = validateImportOptions(parsingOptions)
72✔
310
  if (!isValid) {
72✔
311
    return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, `Invalid options`)
12✔
312
  }
12✔
313

60✔
314
  const bodyParser = getFileMimeParser(mimetype, parsingOptions)
60✔
315
  if (!bodyParser) {
72!
316
    return reply.getHttpError(UNSUPPORTED_MIME_TYPE_STATUS_CODE, `Unsupported file type ${mimetype}`)
×
317
  }
×
318

60✔
319
  const { crudService, queryParser } = this
60✔
320
  const { log, crudContext } = request
60✔
321

60✔
322
  let documentIndex = 0
60✔
323
  const parseDocument = through2.obj((chunk, _enc, callback) => {
60✔
324
    try {
144✔
325
      itemValidator(chunk)
144✔
326
      if (itemValidator.errors) { throw itemValidator.errors }
144✔
327
      queryParser.parseAndCastBody(chunk)
138✔
328
    } catch (error) {
144✔
329
      return callback(error, chunk)
6✔
330
    }
6✔
331
    documentIndex += 1
138✔
332
    return callback(null, chunk)
138✔
333
  })
60✔
334

60✔
335
  // POST
60✔
336
  let returnCode = 201
60✔
337
  let processBatch = async(batch) => crudService.insertMany(crudContext, batch)
60✔
338

60✔
339
  // PATCH
60✔
340
  if (request.method === 'PATCH') {
66✔
341
    returnCode = 200
39✔
342
    processBatch = async(batch) => {
39✔
343
      return crudService.upsertMany(crudContext, batch)
36✔
344
    }
36✔
345
  }
39✔
346

60✔
347
  const batchConsumer = new BatchWritableStream({
60✔
348
    batchSize: 5000,
60✔
349
    highWaterMark: 1000,
60✔
350
    objectMode: true,
60✔
351
    processBatch,
60✔
352
  })
60✔
353

60✔
354
  try {
60✔
355
    await pipeline(
60✔
356
      file,
60✔
357
      bodyParser(),
60✔
358
      parseDocument,
60✔
359
      batchConsumer
60✔
360
    )
60✔
361
  } catch (error) {
72✔
362
    if (error.code === OPTIONS_INCOMPATIBILITY_ERROR_CODE) {
9!
363
      log.debug('stream error')
×
364
      return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, error.message)
×
365
    }
×
366

9✔
367
    if (error.code === UNIQUE_INDEX_MONGO_ERROR_CODE) {
9✔
368
      log.debug('unique index violation')
3✔
369
      return reply.getHttpError(UNIQUE_INDEX_ERROR_STATUS_CODE, error.message)
3✔
370
    }
3✔
371

6✔
372
    if (Array.isArray(error)) {
6✔
373
      log.debug('error parsing input file')
6✔
374
      const { message, instancePath } = error?.[0] ?? {}
6!
375
      const errorDetails = instancePath ? `, ${instancePath}` : ''
6!
376
      const errorMessage = `(index: ${documentIndex}${errorDetails}) ${message ?? 'error in parsing record'}`
6!
377
      return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, errorMessage)
6✔
378
    }
6✔
379

×
380
    return reply.getHttpError(INTERNAL_SERVER_ERROR_STATUS_CODE, error.message || 'something went wrong')
9✔
381
  }
9✔
382

51✔
383
  return reply.code(returnCode).send({ message: 'File uploaded successfully' })
51✔
384
}
72✔
385

88✔
386
// eslint-disable-next-line max-statements
88✔
387
async function handleGetListLookup(request, reply) {
40✔
388
  if (this.customMetrics) {
40✔
389
    this.customMetrics.collectionInvocation.inc({
40✔
390
      collection_name: this.modelName,
40✔
391
      type: PROMETHEUS_OP_TYPE.FETCH,
40✔
392
    })
40✔
393
  }
40✔
394

40✔
395
  const { query, headers, crudContext, log } = request
40✔
396

40✔
397
  const {
40✔
398
    [QUERY]: clientQueryString,
40✔
399
    [PROJECTION]: clientProjectionString = '',
40✔
400
    [SORT]: sortQuery,
40✔
401
    [LIMIT]: limit,
40✔
402
    [SKIP]: skip,
40✔
403
    [STATE]: state,
40✔
404
    ...otherParams
40✔
405
  } = query
40✔
406
  const { acl_rows, acl_read_columns } = headers
40✔
407

40✔
408
  let projection = resolveProjection(
40✔
409
    clientProjectionString,
40✔
410
    acl_read_columns,
40✔
411
    this.allFieldNames,
40✔
412
    '',
40✔
413
    log
40✔
414
  )
40✔
415

40✔
416
  projection = this.lookupProjection.filter(proj => projection.includes(Object.keys(proj)[0]))
40✔
417
  if (projection.length === 0) {
40✔
418
    reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, 'No allowed colums')
2✔
419
  }
2✔
420

40✔
421
  const LookupProjectionFieldsToOmit = this.lookupProjection.filter(field => Object.values(field).shift() === 0)
40✔
422
  projection.push(...LookupProjectionFieldsToOmit)
40✔
423

40✔
424
  const isTextSearchQuery = query._q && this.queryParser.isTextSearchQuery(JSON.parse(query._q))
40✔
425
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, isTextSearchQuery)
40✔
426
  let sort
40✔
427
  if (sortQuery) {
40✔
428
    sort = Object.fromEntries(sortQuery.toString().split(',')
4✔
429
      .map((param) => (param[0] === '-' ? [param.substr(1), -1] : [param, 1])))
4✔
430
  }
4✔
431

40✔
432
  const stateArr = state?.split(',')
40✔
433
  const { replyType, streamValidator } = reply.context.config
40✔
434
  const contentType = replyType()
40✔
435
  const responseParser = getFileMimeStringify(contentType)
40✔
436
  if (!responseParser) {
40!
437
    return reply.getHttpError(UNSUPPORTED_MIME_TYPE_STATUS_CODE, `Unsupported file type ${contentType}`)
×
438
  }
×
439

40✔
440
  reply.raw.setHeader('Content-Type', contentType)
40✔
441

40✔
442
  try {
40✔
443
    return await pipeline(
40✔
444
      this.crudService
40✔
445
        .aggregate(crudContext, mongoQuery, projection, sort, skip, limit, stateArr, isTextSearchQuery)
40✔
446
        .stream(),
40✔
447
      this.castResultsAsStream(),
40✔
448
      streamValidator(),
40✔
449
      responseParser(),
40✔
450
      reply.raw
40✔
451
    )
40✔
452
  } catch (error) {
40✔
453
    request.log.error({ error }, 'Error during findAll lookup stream')
2✔
454
    request.log.debug({ error: { ...error, message: error.message } }, 'Error during findAll lookup stream with message')
2✔
455
    if (error.code === OPTIONS_INCOMPATIBILITY_ERROR_CODE) {
2!
456
      request.log.info(BAD_REQUEST_ERROR_STATUS_CODE)
×
457
    }
×
458
  }
2✔
459
}
40✔
460

88✔
461
async function handleGetList(request, reply) {
1,179✔
462
  if (this.customMetrics) {
1,179✔
463
    this.customMetrics.collectionInvocation.inc({
63✔
464
      collection_name: this.modelName,
63✔
465
      type: PROMETHEUS_OP_TYPE.FETCH,
63✔
466
    })
63✔
467
  }
63✔
468

1,179✔
469
  const { query, headers, crudContext, log } = request
1,179✔
470
  const {
1,179✔
471
    [QUERY]: clientQueryString,
1,179✔
472
    [PROJECTION]: clientProjectionString = '',
1,179✔
473
    [RAW_PROJECTION]: clientRawProjectionString = '',
1,179✔
474
    [SORT]: sortQuery,
1,179✔
475
    [LIMIT]: limit,
1,179✔
476
    [SKIP]: skip,
1,179✔
477
    [STATE]: state,
1,179✔
478
    ...otherParams
1,179✔
479
  } = query
1,179✔
480
  const { acl_rows, acl_read_columns, accept } = headers
1,179✔
481
  const { replyType, streamValidator } = reply.context.config
1,179✔
482
  const contentType = replyType(accept)
1,179✔
483

1,179✔
484
  const responseParser = getFileMimeStringify(contentType, {})
1,179✔
485
  if (!responseParser) {
1,179✔
486
    return reply.getHttpError(NOT_ACCEPTABLE, `unsupported file type ${contentType}`)
156✔
487
  }
156✔
488

1,023✔
489
  const projection = resolveProjection(
1,023✔
490
    clientProjectionString,
1,023✔
491
    acl_read_columns,
1,023✔
492
    this.allFieldNames,
1,023✔
493
    clientRawProjectionString,
1,023✔
494
    log
1,023✔
495
  )
1,023✔
496

1,023✔
497
  const isTextSearchQuery = query._q && this.queryParser.isTextSearchQuery(JSON.parse(query._q))
1,179✔
498
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, isTextSearchQuery)
1,179✔
499

1,179✔
500
  let sort
1,179✔
501
  if (sortQuery) {
1,179✔
502
    sort = Object.fromEntries(sortQuery.toString().split(',')
122✔
503
      .map((param) => (param[0] === '-' ? [param.substr(1), -1] : [param, 1])))
122✔
504
  }
122✔
505

957✔
506
  const stateArr = state.split(',')
957✔
507

957✔
508
  reply.raw.setHeader('Content-Type', contentType)
957✔
509

957✔
510
  try {
957✔
511
    await pipeline(
957✔
512
      this.crudService
957✔
513
        .findAll(crudContext, mongoQuery, projection, sort, skip, limit, stateArr, isTextSearchQuery)
957✔
514
        .stream(),
957✔
515
      this.castResultsAsStream(),
957✔
516
      streamValidator(),
957✔
517
      responseParser(),
957✔
518
      reply.raw
957✔
519
    )
957✔
520
  } catch (error) {
1,179✔
521
    request.log.error({ error }, 'Error during findAll stream')
3✔
522
    request.log.debug({ error: { ...error, message: error.message } }, 'Error during findAll stream with message')
3✔
523
    if (error.code === OPTIONS_INCOMPATIBILITY_ERROR_CODE) {
3!
524
      request.log.info(BAD_REQUEST_ERROR_STATUS_CODE)
×
525
    }
×
526
  }
3✔
527
}
1,179✔
528

88✔
529
async function handleGetId(request, reply) {
372✔
530
  if (this.customMetrics) {
372!
531
    this.customMetrics.collectionInvocation.inc({
×
532
      collection_name: this.modelName,
×
533
      type: PROMETHEUS_OP_TYPE.FETCH,
×
534
    })
×
535
  }
×
536

372✔
537
  const { crudContext, log } = request
372✔
538
  const docId = request.params.id
372✔
539
  const { acl_rows, acl_read_columns } = request.headers
372✔
540
  const {
372✔
541
    itemValidator,
372✔
542
  } = reply.context.config
372✔
543

372✔
544
  const {
372✔
545
    [QUERY]: clientQueryString,
372✔
546
    [PROJECTION]: clientProjectionString = '',
372✔
547
    [RAW_PROJECTION]: clientRawProjectionString = '',
372✔
548
    [STATE]: state,
372✔
549
    ...otherParams
372✔
550
  } = request.query
372✔
551

372✔
552
  const projection = resolveProjection(
372✔
553
    clientProjectionString,
372✔
554
    acl_read_columns,
372✔
555
    this.allFieldNames,
372✔
556
    clientRawProjectionString,
372✔
557
    log
372✔
558
  )
372✔
559
  const filter = resolveMongoQuery(
372✔
560
    this.queryParser,
372✔
561
    clientQueryString,
372✔
562
    acl_rows,
372✔
563
    otherParams,
372✔
564
    false
372✔
565
  )
372✔
566
  const _id = this.castCollectionId(docId)
372✔
567

372✔
568
  const stateArr = state.split(',')
372✔
569
  const doc = await this.crudService.findById(crudContext, _id, filter, projection, stateArr)
372✔
570
  if (!doc) {
372✔
571
    return reply.notFound()
78✔
572
  }
78✔
573

273✔
574
  const response = this.castItem(doc)
273✔
575
  itemValidator(response)
273✔
576
  return response
273✔
577
}
372✔
578

88✔
579
async function handleInsertOne(request, reply) {
74✔
580
  if (this.customMetrics) {
74✔
581
    this.customMetrics.collectionInvocation.inc({
11✔
582
      collection_name: this.modelName,
11✔
583
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
11✔
584
    })
11✔
585
  }
11✔
586

74✔
587
  const { body: doc, crudContext } = request
74✔
588

74✔
589
  this.queryParser.parseAndCastBody(doc)
74✔
590

74✔
591
  try {
74✔
592
    const insertedDoc = await this.crudService.insertOne(crudContext, doc)
74✔
593
    return mapToObjectWithOnlyId(insertedDoc)
71✔
594
  } catch (error) {
74✔
595
    if (error.code === UNIQUE_INDEX_MONGO_ERROR_CODE) {
3✔
596
      request.log.error('unique index violation')
3✔
597
      return reply.getHttpError(UNIQUE_INDEX_ERROR_STATUS_CODE, error.message)
3✔
598
    }
3✔
599
    throw error
×
600
  }
×
601
}
74✔
602

88✔
603
async function handleValidate() {
3✔
604
  return { result: 'ok' }
3✔
605
}
3✔
606

88✔
607
async function handleDeleteId(request, reply) {
56✔
608
  if (this.customMetrics) {
56!
609
    this.customMetrics.collectionInvocation.inc({
2✔
610
      collection_name: this.modelName,
2✔
611
      type: PROMETHEUS_OP_TYPE.DELETE,
2✔
612
    })
2✔
613
  }
2✔
614

56✔
615
  const { query, headers, params, crudContext } = request
56✔
616

56✔
617
  const docId = params.id
56✔
618
  const _id = this.castCollectionId(docId)
56✔
619

56✔
620
  const {
56✔
621
    [QUERY]: clientQueryString,
56✔
622
    [STATE]: state,
56✔
623
    ...otherParams
56✔
624
  } = query
56✔
625
  const { acl_rows } = headers
56✔
626

56✔
627
  const filter = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
56✔
628

56✔
629
  const stateArr = state.split(',')
56✔
630
  const doc = await this.crudService.deleteById(crudContext, _id, filter, stateArr)
56✔
631

53✔
632
  if (!doc) {
56✔
633
    return reply.notFound()
18✔
634
  }
18✔
635

35✔
636
  // the document should not be returned:
35✔
637
  // we don't know which projection the user is able to see
35✔
638
  reply.code(204)
35✔
639
}
56✔
640

88✔
641
async function handleDeleteList(request) {
41✔
642
  if (this.customMetrics) {
41!
643
    this.customMetrics.collectionInvocation.inc({
2✔
644
      collection_name: this.modelName,
2✔
645
      type: PROMETHEUS_OP_TYPE.DELETE,
2✔
646
    })
2✔
647
  }
2✔
648

41✔
649
  const { query, headers, crudContext } = request
41✔
650

41✔
651
  const {
41✔
652
    [QUERY]: clientQueryString,
41✔
653
    [STATE]: state,
41✔
654
    ...otherParams
41✔
655
  } = query
41✔
656
  const { acl_rows } = headers
41✔
657

41✔
658
  const filter = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
41✔
659

41✔
660
  const stateArr = state.split(',')
41✔
661
  return this.crudService.deleteAll(crudContext, filter, stateArr)
41✔
662
}
41✔
663

88✔
664
async function handleCount(request) {
45✔
665
  if (this.customMetrics) {
45✔
666
    this.customMetrics.collectionInvocation.inc({
3✔
667
      collection_name: this.modelName,
3✔
668
      type: PROMETHEUS_OP_TYPE.FETCH,
3✔
669
    })
3✔
670
  }
3✔
671

45✔
672
  const { query, headers, crudContext } = request
45✔
673
  const {
45✔
674
    [QUERY]: clientQueryString,
45✔
675
    [STATE]: state,
45✔
676
    ...otherParams
45✔
677
  } = query
45✔
678
  const { acl_rows } = headers
45✔
679
  const stateArr = state.split(',')
45✔
680

45✔
681
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
45✔
682
  return this.crudService.count(crudContext, mongoQuery, stateArr)
45✔
683
}
45✔
684

88✔
685
async function handlePatchId(request, reply) {
224✔
686
  if (this.customMetrics) {
224!
687
    this.customMetrics.collectionInvocation.inc({
2✔
688
      collection_name: this.modelName,
2✔
689
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
2✔
690
    })
2✔
691
  }
2✔
692

224✔
693
  const { query, headers, params, crudContext, log } = request
224✔
694
  const {
224✔
695
    [QUERY]: clientQueryString,
224✔
696
    [STATE]: state,
224✔
697
    ...otherParams
224✔
698
  } = query
224✔
699
  const {
224✔
700
    acl_rows,
224✔
701
    acl_write_columns: aclWriteColumns,
224✔
702
    acl_read_columns: aclColumns = '',
224✔
703
  } = headers
224✔
704
  const {
224✔
705
    itemValidator,
224✔
706
  } = reply.context.config
224✔
707

224✔
708
  const commands = request.body
224✔
709

224✔
710
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
224✔
711

224✔
712
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
224✔
713

224✔
714
  this.queryParser.parseAndCastCommands(commands, editableFields)
224✔
715
  const projection = resolveProjection('', aclColumns, this.allFieldNames, '', log)
224✔
716

224✔
717
  const docId = params.id
224✔
718
  const _id = this.castCollectionId(docId)
224✔
719

224✔
720
  const stateArr = state.split(',')
224✔
721
  const doc = await this.crudService.patchById(crudContext, _id, commands, mongoQuery, projection, stateArr)
224✔
722

218✔
723
  if (!doc) {
224✔
724
    return reply.notFound()
51✔
725
  }
51✔
726

167✔
727
  const response = this.castItem(doc)
167✔
728
  itemValidator(response)
167✔
729
  return response
167✔
730
}
224✔
731

88✔
732
async function handlePatchMany(request) {
90✔
733
  if (this.customMetrics) {
90!
734
    this.customMetrics.collectionInvocation.inc({
×
735
      collection_name: this.modelName,
×
736
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
737
    })
×
738
  }
×
739

90✔
740
  const { query, headers, crudContext } = request
90✔
741
  const {
90✔
742
    [QUERY]: clientQueryString,
90✔
743
    [STATE]: state,
90✔
744
    ...otherParams
90✔
745
  } = query
90✔
746
  const {
90✔
747
    acl_rows,
90✔
748
    acl_write_columns: aclWriteColumns,
90✔
749
  } = headers
90✔
750

90✔
751
  const commands = request.body
90✔
752
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
90✔
753
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
90✔
754
  this.queryParser.parseAndCastCommands(commands, editableFields)
90✔
755

90✔
756
  const stateArr = state.split(',')
90✔
757
  const nModified = await this.crudService.patchMany(crudContext, commands, mongoQuery, stateArr)
90✔
758

84✔
759
  return nModified
84✔
760
}
90✔
761

88✔
762
async function handleUpsertOne(request, reply) {
66✔
763
  if (this.customMetrics) {
66!
764
    this.customMetrics.collectionInvocation.inc({
×
765
      collection_name: this.modelName,
×
766
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
767
    })
×
768
  }
×
769

66✔
770
  const { query, headers, crudContext, log } = request
66✔
771
  const {
66✔
772
    [QUERY]: clientQueryString,
66✔
773
    [STATE]: state,
66✔
774
    ...otherParams
66✔
775
  } = query
66✔
776
  const {
66✔
777
    acl_rows,
66✔
778
    acl_write_columns: aclWriteColumns,
66✔
779
    acl_read_columns: aclColumns = '',
66✔
780
  } = headers
66✔
781
  const {
66✔
782
    itemValidator,
66✔
783
  } = reply.context.config
66✔
784

66✔
785
  const commands = request.body
66✔
786

66✔
787
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
66✔
788
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
66✔
789

66✔
790
  this.queryParser.parseAndCastCommands(commands, editableFields)
66✔
791
  const projection = resolveProjection('', aclColumns, this.allFieldNames, '', log)
66✔
792

66✔
793
  const stateArr = state.split(',')
66✔
794
  const doc = await this.crudService.upsertOne(crudContext, commands, mongoQuery, projection, stateArr)
66✔
795

66✔
796
  const response = this.castItem(doc)
66✔
797

66✔
798
  itemValidator(response)
66✔
799
  return response
66✔
800
}
66✔
801

88✔
802
async function handlePatchBulk(request) {
90✔
803
  if (this.customMetrics) {
90!
804
    this.customMetrics.collectionInvocation.inc({
×
805
      collection_name: this.modelName,
×
806
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
807
    })
×
808
  }
×
809

90✔
810
  const { body: filterUpdateCommands, crudContext, headers } = request
90✔
811

90✔
812
  const {
90✔
813
    acl_rows,
90✔
814
    acl_write_columns: aclWriteColumns,
90✔
815
  } = headers
90✔
816

90✔
817
  const parsedAndCastedCommands = new Array(filterUpdateCommands.length)
90✔
818
  for (let i = 0; i < filterUpdateCommands.length; i++) {
90✔
819
    const { filter, update } = filterUpdateCommands[i]
60,114✔
820
    const {
60,114✔
821
      _id,
60,114✔
822
      [QUERY]: clientQueryString,
60,114✔
823
      [STATE]: state,
60,114✔
824
      ...otherParams
60,114✔
825
    } = filter
60,114✔
826

60,114✔
827
    const commands = update
60,114✔
828

60,114✔
829
    const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
60,114✔
830

60,114✔
831
    const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
60,114✔
832

60,114✔
833
    this.queryParser.parseAndCastCommands(commands, editableFields)
60,114✔
834

60,114✔
835
    parsedAndCastedCommands[i] = {
60,114✔
836
      commands,
60,114✔
837
      state: state.split(','),
60,114✔
838
      query: mongoQuery,
60,114✔
839
    }
60,114✔
840
    if (_id) {
60,114✔
841
      parsedAndCastedCommands[i].query._id = this.castCollectionId(_id)
60,105✔
842
    }
60,105✔
843
  }
60,114✔
844

90✔
845
  const nModified = await this.crudService.patchBulk(crudContext, parsedAndCastedCommands)
90✔
846
  return nModified
90✔
847
}
90✔
848

88✔
849
async function handleInsertMany(request, reply) {
60✔
850
  if (this.customMetrics) {
60!
851
    this.customMetrics.collectionInvocation.inc({
×
852
      collection_name: this.modelName,
×
853
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
854
    })
×
855
  }
×
856

60✔
857
  const { body: docs, crudContext } = request
60✔
858

60✔
859
  docs.forEach(this.queryParser.parseAndCastBody)
60✔
860

60✔
861
  try {
60✔
862
    const insertedDocs = await this.crudService.insertMany(crudContext, docs)
60✔
863
    return insertedDocs.map(mapToObjectWithOnlyId)
57✔
864
  } catch (error) {
60✔
865
    if (error.code === UNIQUE_INDEX_MONGO_ERROR_CODE) {
3✔
866
      request.log.error('unique index violation')
3✔
867
      return reply.getHttpError(UNIQUE_INDEX_ERROR_STATUS_CODE, error.message)
3✔
868
    }
3✔
869
    throw error
×
870
  }
×
871
}
60✔
872

88✔
873
async function handleChangeStateById(request, reply) {
51✔
874
  if (this.customMetrics) {
51!
875
    this.customMetrics.collectionInvocation.inc({
×
876
      collection_name: this.modelName,
×
877
      type: PROMETHEUS_OP_TYPE.CHANGE_STATE,
×
878
    })
×
879
  }
×
880

51✔
881
  const { body, crudContext, headers, query } = request
51✔
882
  const {
51✔
883
    [QUERY]: clientQueryString,
51✔
884
    ...otherParams
51✔
885
  } = query
51✔
886

51✔
887
  const { acl_rows } = headers
51✔
888
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
51✔
889

51✔
890
  const docId = request.params.id
51✔
891
  const _id = this.castCollectionId(docId)
51✔
892

51✔
893
  try {
51✔
894
    const doc = await this.crudService.changeStateById(crudContext, _id, body.stateTo, mongoQuery)
51✔
895
    if (!doc) {
51✔
896
      return reply.notFound()
9✔
897
    }
9✔
898

24✔
899
    reply.code(204)
24✔
900
  } catch (error) {
51✔
901
    if (error.statusCode) {
15✔
902
      return reply.getHttpError(error.statusCode, error.message)
15✔
903
    }
15✔
904

×
905
    throw error
×
906
  }
×
907
}
51✔
908

88✔
909
async function handleChangeStateMany(request) {
48✔
910
  if (this.customMetrics) {
48!
911
    this.customMetrics.collectionInvocation.inc({
×
912
      collection_name: this.modelName,
×
913
      type: PROMETHEUS_OP_TYPE.CHANGE_STATE,
×
914
    })
×
915
  }
×
916

48✔
917
  const { body: filterUpdateCommands, crudContext, headers } = request
48✔
918

48✔
919
  const {
48✔
920
    acl_rows,
48✔
921
  } = headers
48✔
922

48✔
923
  const parsedAndCastedCommands = new Array(filterUpdateCommands.length)
48✔
924
  for (let i = 0; i < filterUpdateCommands.length; i++) {
48✔
925
    const {
60✔
926
      filter,
60✔
927
      stateTo,
60✔
928
    } = filterUpdateCommands[i]
60✔
929

60✔
930
    const mongoQuery = resolveMongoQuery(this.queryParser, null, acl_rows, filter, false)
60✔
931

60✔
932
    parsedAndCastedCommands[i] = {
60✔
933
      query: mongoQuery,
60✔
934
      stateTo,
60✔
935
    }
60✔
936
  }
60✔
937

48✔
938
  return this.crudService.changeStateMany(crudContext, parsedAndCastedCommands)
48✔
939
}
48✔
940

88✔
941
async function injectContextInRequest(request) {
2,529✔
942
  const userIdHeader = request.headers[this.userIdHeaderKey]
2,529✔
943
  const isUserHeaderInvalid = INVALID_USERID.includes(userIdHeader)
2,529✔
944

2,529✔
945
  let userId = 'public'
2,529✔
946

2,529✔
947
  if (userIdHeader && !isUserHeaderInvalid) {
2,529✔
948
    userId = userIdHeader
666✔
949
  }
666✔
950

2,529✔
951
  request.crudContext = {
2,529✔
952
    log: request.log,
2,529✔
953
    userId,
2,529✔
954
    now: new Date(),
2,529✔
955
  }
2,529✔
956
}
2,529✔
957

88✔
958
async function parseEncodedJsonQueryParams(logger, request) {
2,529✔
959
  if (request.headers.json_query_params_encoding) {
2,529!
960
    logger.warn('You\'re using the json_query_params_encoding header but it\'s deprecated and its support is going to be dropped in the next major release. Use json-query-params-encoding instead.')
×
961
  }
×
962

2,529✔
963
  // TODO remove request.headers.json_query_params_encoding fallback in v7.0.0
2,529✔
964
  const jsonQueryParamsEncoding = request.headers['json-query-params-encoding'] || request.headers.json_query_params_encoding
2,529✔
965
  switch (jsonQueryParamsEncoding) {
2,529✔
966
  case 'base64': {
2,529✔
967
    const queryJsonFields = [QUERY, RAW_PROJECTION]
9✔
968
    for (const field of queryJsonFields) {
9✔
969
      if (request.query[field]) {
18✔
970
        request.query[field] = Buffer.from(request.query[field], jsonQueryParamsEncoding).toString()
9✔
971
      }
9✔
972
    }
18✔
973
    break
9✔
974
  }
9✔
975
  default: break
2,529✔
976
  }
2,529✔
977
}
2,529✔
978

88✔
979
async function notFoundHandler(request, reply) {
174✔
980
  reply
174✔
981
    .code(404)
174✔
982
    .send({
174✔
983
      error: 'not found',
174✔
984
    })
174✔
985
}
174✔
986

88✔
987
async function customErrorHandler(error, request, reply) {
659✔
988
  if (error.statusCode === 404) {
659✔
989
    return notFoundHandler(request, reply)
156✔
990
  }
156✔
991

503✔
992
  if (error.validation?.[0]?.message === 'must NOT have additional properties') {
659✔
993
    reply.code(error.statusCode)
108✔
994
    throw new Error(`${error.message}. Property "${error.validation[0].params.additionalProperty}" is not defined in validation schema`)
108✔
995
  }
108✔
996

395✔
997
  throw error
395✔
998
}
659✔
999

88✔
1000
function resolveMongoQuery(
62,098✔
1001
  queryParser,
62,098✔
1002
  clientQueryString,
62,098✔
1003
  rawAclRows,
62,098✔
1004
  otherParams,
62,098✔
1005
  textQuery
62,098✔
1006
) {
62,098✔
1007
  const mongoQuery = {
62,098✔
1008
    $and: [],
62,098✔
1009
  }
62,098✔
1010

62,098✔
1011
  if (clientQueryString) {
62,098✔
1012
    const clientQuery = JSON.parse(clientQueryString)
649✔
1013
    mongoQuery.$and.push(clientQuery)
649✔
1014
  }
649✔
1015
  if (otherParams) {
62,098✔
1016
    for (const key of Object.keys(otherParams)) {
62,098✔
1017
      const value = otherParams[key]
525✔
1018
      mongoQuery.$and.push({ [key]: value })
525✔
1019
    }
525✔
1020
  }
62,098✔
1021

62,098✔
1022
  if (rawAclRows) {
62,098✔
1023
    const aclRows = JSON.parse(rawAclRows)
300✔
1024
    if (rawAclRows[0] === '[') {
300✔
1025
      mongoQuery.$and.push({ $and: aclRows })
288✔
1026
    } else {
300✔
1027
      mongoQuery.$and.push(aclRows)
12✔
1028
    }
12✔
1029
  }
300✔
1030

62,098✔
1031
  try {
62,098✔
1032
    if (textQuery) {
62,098✔
1033
      queryParser.parseAndCastTextSearchQuery(mongoQuery)
72✔
1034
    } else {
62,098✔
1035
      queryParser.parseAndCast(mongoQuery)
62,026✔
1036
    }
62,026✔
1037
  } catch (error) {
62,098✔
1038
    throw new BadRequestError(error.message)
21✔
1039
  }
21✔
1040

62,077✔
1041
  if (!mongoQuery.$and.length) {
62,098✔
1042
    return {}
61,000✔
1043
  }
61,000✔
1044

1,077✔
1045
  return mongoQuery
1,077✔
1046
}
62,098✔
1047

88✔
1048
function resolveProjection(clientProjectionString, aclColumns, allFieldNames, rawProjection, log) {
1,722✔
1049
  log.debug('Resolving projections')
1,722✔
1050
  const acls = splitACLs(aclColumns)
1,722✔
1051

1,722✔
1052
  if (clientProjectionString && rawProjection) {
1,722✔
1053
    log.error('Use of both _p and _rawp is not permitted')
18✔
1054
    throw new BadRequestError(
18✔
1055
      'Use of both _rawp and _p parameter is not allowed')
18✔
1056
  }
18✔
1057

1,704✔
1058
  if (!clientProjectionString && !rawProjection) {
1,722✔
1059
    return removeAclColumns(allFieldNames, acls)
1,147✔
1060
  } else if (rawProjection) {
1,722✔
1061
    return resolveRawProjectionString(rawProjection, acls, allFieldNames, log)
231✔
1062
  } else if (clientProjectionString) {
557✔
1063
    return resolveClientProjectionString(clientProjectionString, acls)
326✔
1064
  }
326✔
1065
}
1,722✔
1066

88✔
1067
function resolveClientProjectionString(clientProjectionString, _acls) {
326✔
1068
  const clientProjection = getClientProjection(clientProjectionString)
326✔
1069
  return removeAclColumns(clientProjection, _acls)
326✔
1070
}
326✔
1071

88✔
1072
function resolveRawProjectionString(rawProjection, _acls, allFieldNames, log) {
231✔
1073
  try {
231✔
1074
    checkAllowedOperators(
231✔
1075
      rawProjection,
231✔
1076
      rawProjectionDictionary,
231✔
1077
      _acls.length > 0 ? _acls : allFieldNames, log)
231✔
1078

231✔
1079
    const rawProjectionObject = resolveRawProjection(rawProjection)
231✔
1080
    const projection = removeAclColumnsFromRawProjection(rawProjectionObject, _acls)
231✔
1081

231✔
1082
    return !lisEmpty(projection) ? [projection] : []
231✔
1083
  } catch (errorMessage) {
231✔
1084
    log.error(errorMessage.message)
66✔
1085
    throw new BadRequestError(errorMessage.message)
66✔
1086
  }
66✔
1087
}
231✔
1088

88✔
1089
function splitACLs(acls) {
1,722✔
1090
  if (acls) { return acls.split(',') }
1,722✔
1091
  return []
1,546✔
1092
}
1,722✔
1093

88✔
1094
function removeAclColumns(fieldsInProjection, aclColumns) {
1,638✔
1095
  if (aclColumns.length > 0) {
1,638✔
1096
    return fieldsInProjection.filter(field => {
170✔
1097
      return aclColumns.indexOf(field) > -1
960✔
1098
    })
170✔
1099
  }
170✔
1100

1,468✔
1101
  return fieldsInProjection
1,468✔
1102
}
1,638✔
1103

88✔
1104
function removeAclColumnsFromRawProjection(rawProjectionObject, aclColumns) {
171✔
1105
  const isRawProjectionOverridingACLs = checkIfRawProjectionOverridesAcls(rawProjectionObject, aclColumns)
171✔
1106
  if (isRawProjectionOverridingACLs) {
171✔
1107
    throw Error('_rawp exclusive projection is overriding at least one acl_read_column value')
6✔
1108
  }
6✔
1109

165✔
1110
  const rawProjectionFields = Object.keys(rawProjectionObject)
165✔
1111
  const filteredFields = removeAclColumns(rawProjectionFields, aclColumns)
165✔
1112

165✔
1113
  return filteredFields.reduce((acc, current) => {
165✔
1114
    if (rawProjectionObject[current] === 0 || rawProjectionObject[current]) {
249✔
1115
      acc[current] = rawProjectionObject[current]
249✔
1116
    }
249✔
1117
    return acc
249✔
1118
  }, {})
165✔
1119
}
171✔
1120

88✔
1121
function getClientProjection(clientProjectionString) {
326✔
1122
  if (clientProjectionString) {
326✔
1123
    return clientProjectionString.split(',')
326✔
1124
  }
326✔
1125
  return []
×
1126
}
326✔
1127

88✔
1128
function resolveRawProjection(clientRawProjectionString) {
171✔
1129
  if (clientRawProjectionString) {
171✔
1130
    return JSON.parse(clientRawProjectionString)
171✔
1131
  }
171✔
1132
  return {}
×
1133
}
171✔
1134

88✔
1135
function checkAllowedOperators(rawProjection, projectionDictionary, additionalFields, log) {
231✔
1136
  if (!rawProjection) {
231!
1137
    log.debug('No raw projection found: checkAllowedOperators returns true')
×
1138
    return true
×
1139
  }
×
1140

231✔
1141
  const { allowedOperators, notAllowedOperators } = projectionDictionary
231✔
1142
  const allowedFields = [...allowedOperators]
231✔
1143

231✔
1144
  additionalFields.forEach(field => allowedFields.push(`$${field}`))
231✔
1145

231✔
1146
  log.debug({ allowedOperators: allowedFields }, 'Allowed operators for projection')
231✔
1147
  log.debug({ notAllowedOperators }, 'Not allowed operators for projection')
231✔
1148

231✔
1149
  // to match both camelCase operators and snake mongo_systems variables
231✔
1150
  const operatorsRegex = /\${1,2}[a-zA-Z_]+/g
231✔
1151
  const matches = rawProjection.match(operatorsRegex)
231✔
1152

231✔
1153
  if (!matches) {
231✔
1154
    log.debug('No operators found in raw projection: checkAllowedOperators returns true')
93✔
1155
    return true
93✔
1156
  }
93✔
1157

138✔
1158
  return !matches.some(match => {
138✔
1159
    if (match.startsWith('$$')) {
336✔
1160
      log.debug({ match }, 'Found $$ match in raw projection')
96✔
1161
      if (notAllowedOperators.includes(match)) {
96✔
1162
        throw Error(`Operator ${match} is not allowed in raw projection`)
48✔
1163
      }
48✔
1164

48✔
1165
      return notAllowedOperators.includes(match)
48✔
1166
    }
48✔
1167

240✔
1168
    if (!allowedFields.includes(match)) {
336✔
1169
      throw Error(`Operator ${match} is not allowed in raw projection`)
12✔
1170
    }
12✔
1171

228✔
1172
    return !allowedFields.includes(match)
228✔
1173
  })
138✔
1174
}
231✔
1175

88✔
1176
function checkIfRawProjectionOverridesAcls(rawProjection, acls) {
171✔
1177
  return Object.keys(rawProjection).some(field =>
171✔
1178
    acls.includes(field) && rawProjection[field] === 0
351✔
1179
  )
171✔
1180
}
171✔
1181

88✔
1182
function mapToObjectWithOnlyId(doc) {
150,182✔
1183
  return { _id: doc._id.toString() }
150,182✔
1184
}
150,182✔
1185

88✔
1186
const internalFields = [
88✔
1187
  UPDATERID,
88✔
1188
  UPDATEDAT,
88✔
1189
  CREATORID,
88✔
1190
  CREATEDAT,
88✔
1191
  __STATE__,
88✔
1192
]
88✔
1193
function getEditableFields(aclWriteColumns, allFieldNames) {
60,494✔
1194
  const editableFields = aclWriteColumns ? aclWriteColumns.split(',') : allFieldNames
60,494!
1195
  return editableFields.filter(ef => !internalFields.includes(ef))
60,494✔
1196
}
60,494✔
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