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

mia-platform / crud-service / 7744802866

01 Feb 2024 04:51PM UTC coverage: 97.123% (+0.001%) from 97.122%
7744802866

push

github

danibix95
7.0.0

1817 of 1964 branches covered (0.0%)

Branch coverage included in aggregate %.

8984 of 9157 relevant lines covered (98.11%)

7355.55 hits per line

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

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

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

94✔
19
'use strict'
94✔
20

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

94✔
25
const { pipeline } = require('stream/promises')
94✔
26
const { get: lget, isEmpty: lisEmpty } = require('lodash')
94✔
27
const through2 = require('through2')
94✔
28

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

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

94✔
57
const { SCHEMAS_ID } = require('./schemaGetters')
94✔
58
const { getAjvResponseValidationFunction, shouldValidateStream, shouldValidateItem } = require('./validatorGetters')
94✔
59
const { getFileMimeParser, getFileMimeStringifiers } = require('./mimeTypeTransform')
94✔
60

94✔
61
const OPTIONS_INCOMPATIBILITY_ERROR_CODE = 2
94✔
62
const UNIQUE_INDEX_MONGO_ERROR_CODE = 11000
94✔
63

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

94✔
71

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

4,368✔
85
  const {
4,368✔
86
    registerGetters = true,
4,368✔
87
    registerSetters = true,
4,368✔
88
    registerLookup = false,
4,368✔
89
  } = options
4,368✔
90

4,368✔
91
  const validateOutput = fastify.validateOutput ?? false
4,368!
92

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

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

4,368✔
127
  fastify.setValidatorCompiler(({ schema }) => {
4,368✔
128
    const uniqueId = schema[SCHEMA_CUSTOM_KEYWORDS.UNIQUE_OPERATION_ID]
150,576✔
129
    const [collectionName, schemaId, subSchemaPath] = uniqueId.split('__MIA__')
150,576✔
130
    const nestedSchema = NESTED_SCHEMAS_BY_ID[schemaId]
150,576✔
131
    const subSchema = lget(nestedSchema, subSchemaPath)
150,576✔
132
    fastify.log.trace({ collectionName, schemaPath: subSchemaPath, schemaId }, 'collection schema info')
150,576✔
133

150,576✔
134
    // this is made to prevent to shows on swagger all properties with dot notation of RawObject with schema.
150,576✔
135
    return ajv.compile(subSchema)
150,576✔
136
  })
4,368✔
137

4,368✔
138
  fastify.addHook('preHandler', injectContextInRequest)
4,368✔
139
  fastify.addHook('preHandler', request => parseEncodedJsonQueryParams(fastify.log, request))
4,368✔
140
  fastify.setErrorHandler(customErrorHandler)
4,368✔
141

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

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

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

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

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

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

3,738✔
239
    fastify.log.debug({ collection: fastify?.modelName }, 'setters endpoints registered')
3,738✔
240
  }
3,738✔
241

4,368✔
242
  if (registerLookup) {
4,368✔
243
    if (!fastify.lookupProjection) { throw new Error('`fastify.lookupProjection` is undefined') }
12!
244
    const listLookupSchema = fastify.jsonSchemaGenerator.generateGetListLookupJSONSchema()
12✔
245
    fastify.get('/', {
12✔
246
      schema: listLookupSchema,
12✔
247
      config: {
12✔
248
        streamValidator: shouldValidateStream(listLookupSchema.response['200'], validateOutput),
12✔
249
        replyType: () => 'application/json',
12✔
250
      },
12✔
251
    }, handleGetListLookup)
12✔
252
    fastify.log.debug({ collection: fastify?.modelName }, 'lookup endpoint registered')
12✔
253
  }
12✔
254

4,368✔
255
  if (registerGetters) {
4,368✔
256
    const getItemJSONSchema = fastify.jsonSchemaGenerator.generateGetItemJSONSchema()
4,344✔
257
    fastify.get('/export', {
4,344✔
258
      schema: fastify.jsonSchemaGenerator.generateExportJSONSchema(),
4,344✔
259
      config: {
4,344✔
260
        streamValidator: shouldValidateStream(getItemJSONSchema.response['200'], validateOutput),
4,344✔
261
        replyType: (acceptHeader) => {
4,344✔
262
          const accept = getAccept(acceptHeader)
858✔
263

858✔
264
          if (!accept || accept === '*/*') { return 'application/x-ndjson' }
858!
265

858✔
266
          return accept
858✔
267
        },
4,344✔
268
      },
4,344✔
269
    }, handleGetList)
4,344✔
270
    fastify.get('/count', { schema: fastify.jsonSchemaGenerator.generateCountJSONSchema() }, handleCount)
4,344✔
271
    fastify.get(
4,344✔
272
      '/schema',
4,344✔
273
      {
4,344✔
274
        schema: fastify.jsonSchemaGenerator.generateGetSchemaJSONSchema(),
4,344✔
275
      },
4,344✔
276
      () => ({
4,344✔
277
        type: getItemJSONSchema.response['200'].type,
3✔
278
        properties: getItemJSONSchema.response['200'].properties,
3✔
279
      })
3✔
280
    )
4,344✔
281
    fastify.setNotFoundHandler(notFoundHandler)
4,344✔
282
    fastify.get('/', {
4,344✔
283
      schema: fastify.jsonSchemaGenerator.generateGetListJSONSchema(),
4,344✔
284
      config: {
4,344✔
285
        streamValidator: shouldValidateStream(getItemJSONSchema.response['200'], validateOutput),
4,344✔
286
        replyType: () => 'application/json',
4,344✔
287
      },
4,344✔
288
    }, handleGetList)
4,344✔
289
    fastify.get('/:id', {
4,344✔
290
      schema: getItemJSONSchema,
4,344✔
291
      config: {
4,344✔
292
        itemValidator: shouldValidateItem(getItemJSONSchema.response['200'], validateOutput),
4,344✔
293
      },
4,344✔
294
    }, handleGetId)
4,344✔
295

4,344✔
296
    fastify.log.debug({ collection: fastify?.modelName }, 'getters endpoints registered')
4,344✔
297
  }
4,344✔
298
}
4,368✔
299

94✔
300
// eslint-disable-next-line max-statements
94✔
301
async function handleCollectionImport(request, reply) {
72✔
302
  if (this.customMetrics) {
72!
303
    this.customMetrics.collectionInvocation.inc({
×
304
      collection_name: this.modelName,
×
305
      type: PROMETHEUS_OP_TYPE.IMPORT,
×
306
    })
×
307
  }
×
308

72✔
309
  if (!request.isMultipart()) {
72!
310
    return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, 'Request is not multipart')
×
311
  }
×
312

72✔
313
  const data = await request.file()
72✔
314
  if (!data) {
72!
315
    return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, 'Missing file')
×
316
  }
×
317
  const { file, mimetype, fields } = data
72✔
318
  const parsingOptions = Object.fromEntries(Object.values(fields)
72✔
319
    .filter(field => field.type === 'field')
72✔
320
    .map(({ fieldname, value }) => [fieldname, value]))
72✔
321

72✔
322
  const {
72✔
323
    log,
72✔
324
    crudContext,
72✔
325
    routeOptions: { config: { itemValidator, validateImportOptions } },
72✔
326
  } = request
72✔
327
  const isValid = validateImportOptions(parsingOptions)
72✔
328
  if (!isValid) {
72✔
329
    return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, `Invalid options`)
12✔
330
  }
12✔
331

60✔
332
  const bodyParser = getFileMimeParser(mimetype, parsingOptions)
60✔
333
  if (!bodyParser) {
72!
334
    return reply.getHttpError(UNSUPPORTED_MIME_TYPE_STATUS_CODE, `Unsupported file type ${mimetype}`)
×
335
  }
×
336

60✔
337
  const { crudService, queryParser } = this
60✔
338

60✔
339
  let documentIndex = 0
60✔
340
  const parseDocument = through2.obj((chunk, _enc, callback) => {
60✔
341
    try {
144✔
342
      itemValidator(chunk)
144✔
343
      if (itemValidator.errors) { throw itemValidator.errors }
144✔
344
      queryParser.parseAndCastBody(chunk)
138✔
345
    } catch (error) {
144✔
346
      return callback(error, chunk)
6✔
347
    }
6✔
348
    documentIndex += 1
138✔
349
    return callback(null, chunk)
138✔
350
  })
60✔
351

60✔
352
  // POST
60✔
353
  let returnCode = 201
60✔
354
  let processBatch = async(batch) => crudService.insertMany(crudContext, batch)
60✔
355

60✔
356
  // PATCH
60✔
357
  if (request.method === 'PATCH') {
66✔
358
    returnCode = 200
39✔
359
    processBatch = async(batch) => {
39✔
360
      return crudService.upsertMany(crudContext, batch)
36✔
361
    }
36✔
362
  }
39✔
363

60✔
364
  const batchConsumer = new BatchWritableStream({
60✔
365
    batchSize: 5000,
60✔
366
    highWaterMark: 1000,
60✔
367
    objectMode: true,
60✔
368
    processBatch,
60✔
369
  })
60✔
370

60✔
371
  try {
60✔
372
    await pipeline(
60✔
373
      file,
60✔
374
      bodyParser(),
60✔
375
      parseDocument,
60✔
376
      batchConsumer
60✔
377
    )
60✔
378
  } catch (error) {
72✔
379
    if (error.code === OPTIONS_INCOMPATIBILITY_ERROR_CODE) {
9!
380
      log.debug('stream error')
×
381
      return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, error.message)
×
382
    }
×
383

9✔
384
    if (error.code === UNIQUE_INDEX_MONGO_ERROR_CODE) {
9✔
385
      log.debug('unique index violation')
3✔
386
      return reply.getHttpError(UNIQUE_INDEX_ERROR_STATUS_CODE, error.message)
3✔
387
    }
3✔
388

6✔
389
    if (Array.isArray(error)) {
6✔
390
      log.debug('error parsing input file')
6✔
391
      const { message, instancePath } = error?.[0] ?? {}
6!
392
      const errorDetails = instancePath ? `, ${instancePath}` : ''
6!
393
      const errorMessage = `(index: ${documentIndex}${errorDetails}) ${message ?? 'error in parsing record'}`
6!
394
      return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, errorMessage)
6✔
395
    }
6✔
396

×
397
    return reply.getHttpError(INTERNAL_SERVER_ERROR_STATUS_CODE, error.message || 'something went wrong')
9✔
398
  }
9✔
399

51✔
400
  return reply.code(returnCode).send({ message: 'File uploaded successfully' })
51✔
401
}
72✔
402

94✔
403
// eslint-disable-next-line max-statements
94✔
404
async function handleGetListLookup(request, reply) {
40✔
405
  if (this.customMetrics) {
40✔
406
    this.customMetrics.collectionInvocation.inc({
40✔
407
      collection_name: this.modelName,
40✔
408
      type: PROMETHEUS_OP_TYPE.FETCH,
40✔
409
    })
40✔
410
  }
40✔
411

40✔
412
  const {
40✔
413
    query,
40✔
414
    headers,
40✔
415
    crudContext,
40✔
416
    log,
40✔
417
    routeOptions: { config: { replyType, streamValidator } },
40✔
418
  } = request
40✔
419

40✔
420
  const {
40✔
421
    [QUERY]: clientQueryString,
40✔
422
    [PROJECTION]: clientProjectionString = '',
40✔
423
    [SORT]: sortQuery,
40✔
424
    [LIMIT]: limit,
40✔
425
    [SKIP]: skip,
40✔
426
    [STATE]: state,
40✔
427
    ...otherParams
40✔
428
  } = query
40✔
429
  const { acl_rows, acl_read_columns } = headers
40✔
430

40✔
431
  let projection = resolveProjection(
40✔
432
    clientProjectionString,
40✔
433
    acl_read_columns,
40✔
434
    this.allFieldNames,
40✔
435
    '',
40✔
436
    log
40✔
437
  )
40✔
438

40✔
439
  projection = this.lookupProjection.filter(proj => projection.includes(Object.keys(proj)[0]))
40✔
440
  if (projection.length === 0) {
40✔
441
    reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, 'No allowed colums')
2✔
442
  }
2✔
443

40✔
444
  const LookupProjectionFieldsToOmit = this.lookupProjection.filter(field => Object.values(field).shift() === 0)
40✔
445
  projection.push(...LookupProjectionFieldsToOmit)
40✔
446

40✔
447
  const isTextSearchQuery = query._q && this.queryParser.isTextSearchQuery(JSON.parse(query._q))
40✔
448
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, isTextSearchQuery)
40✔
449
  let sort
40✔
450
  if (sortQuery) {
40✔
451
    sort = Object.fromEntries(sortQuery.toString().split(',')
4✔
452
      .map((param) => (param[0] === '-' ? [param.substr(1), -1] : [param, 1])))
4✔
453
  }
4✔
454

40✔
455
  const stateArr = state?.split(',')
40✔
456
  const contentType = replyType()
40✔
457
  const responseStringifiers = getFileMimeStringifiers(contentType)
40✔
458
  if (!responseStringifiers) {
40!
459
    return reply.getHttpError(UNSUPPORTED_MIME_TYPE_STATUS_CODE, `Unsupported file type ${contentType}`)
×
460
  }
×
461

40✔
462
  reply.raw.setHeader('Content-Type', contentType)
40✔
463

40✔
464
  try {
40✔
465
    return await pipeline(
40✔
466
      this.crudService
40✔
467
        .aggregate(crudContext, mongoQuery, projection, sort, skip, limit, stateArr, isTextSearchQuery)
40✔
468
        .stream(),
40✔
469
      this.castResultsAsStream(),
40✔
470
      streamValidator(),
40✔
471
      ...responseStringifiers({ fields: this.allFieldNames }),
40✔
472
      reply.raw
40✔
473
    )
40✔
474
  } catch (error) {
40✔
475
    request.log.error({ error }, 'Error during findAll lookup stream')
2✔
476
    request.log.debug({ error: { ...error, message: error.message } }, 'Error during findAll lookup stream with message')
2✔
477
    if (error.code === OPTIONS_INCOMPATIBILITY_ERROR_CODE) {
2!
478
      request.log.info(BAD_REQUEST_ERROR_STATUS_CODE)
×
479
    }
×
480
  }
2✔
481
}
40✔
482

94✔
483
async function handleGetList(request, reply) {
1,192✔
484
  if (this.customMetrics) {
1,192✔
485
    this.customMetrics.collectionInvocation.inc({
67✔
486
      collection_name: this.modelName,
67✔
487
      type: PROMETHEUS_OP_TYPE.FETCH,
67✔
488
    })
67✔
489
  }
67✔
490

1,192✔
491
  const {
1,192✔
492
    query,
1,192✔
493
    headers,
1,192✔
494
    crudContext,
1,192✔
495
    log,
1,192✔
496
    routeOptions: { config: { replyType, streamValidator } },
1,192✔
497
  } = request
1,192✔
498
  const {
1,192✔
499
    [QUERY]: clientQueryString,
1,192✔
500
    [PROJECTION]: clientProjectionString = '',
1,192✔
501
    [RAW_PROJECTION]: clientRawProjectionString = '',
1,192✔
502
    [SORT]: sortQuery,
1,192✔
503
    [LIMIT]: limit,
1,192✔
504
    [SKIP]: skip,
1,192✔
505
    [STATE]: state,
1,192✔
506
    ...otherParams
1,192✔
507
  } = query
1,192✔
508
  const { acl_rows, acl_read_columns, accept } = headers
1,192✔
509
  const contentType = replyType(accept)
1,192✔
510

1,192✔
511
  const responseStringifiers = getFileMimeStringifiers(contentType, {})
1,192✔
512
  if (!responseStringifiers) {
1,192✔
513
    return reply.getHttpError(NOT_ACCEPTABLE, `unsupported file type ${contentType}`)
156✔
514
  }
156✔
515

1,036✔
516
  const projection = resolveProjection(
1,036✔
517
    clientProjectionString,
1,036✔
518
    acl_read_columns,
1,036✔
519
    this.allFieldNames,
1,036✔
520
    clientRawProjectionString,
1,036✔
521
    log
1,036✔
522
  )
1,036✔
523

1,036✔
524
  const isTextSearchQuery = query._q && this.queryParser.isTextSearchQuery(JSON.parse(query._q))
1,192✔
525
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, isTextSearchQuery)
1,192✔
526

1,192✔
527
  let sort
1,192✔
528
  if (sortQuery) {
1,192✔
529
    sort = Object.fromEntries(sortQuery.toString().split(',')
122✔
530
      .map((param) => (param[0] === '-' ? [param.substr(1), -1] : [param, 1])))
122✔
531
  }
122✔
532

970✔
533
  const stateArr = state.split(',')
970✔
534

970✔
535
  reply.raw.setHeader('Content-Type', contentType)
970✔
536

970✔
537
  try {
970✔
538
    await pipeline(
970✔
539
      this.crudService
970✔
540
        .findAll(crudContext, mongoQuery, projection, sort, skip, limit, stateArr, isTextSearchQuery)
970✔
541
        .stream(),
970✔
542
      this.castResultsAsStream(),
970✔
543
      streamValidator(),
970✔
544
      ...responseStringifiers({ fields: this.allFieldNames }),
970✔
545
      reply.raw
970✔
546
    )
970✔
547
  } catch (error) {
1,192✔
548
    request.log.error({ error }, 'Error during findAll stream')
3✔
549
    request.log.debug({ error: { ...error, message: error.message } }, 'Error during findAll stream with message')
3✔
550
    if (error.code === OPTIONS_INCOMPATIBILITY_ERROR_CODE) {
3!
551
      request.log.info(BAD_REQUEST_ERROR_STATUS_CODE)
×
552
    }
×
553
  }
3✔
554
}
1,192✔
555

94✔
556
async function handleGetId(request, reply) {
375✔
557
  if (this.customMetrics) {
375!
558
    this.customMetrics.collectionInvocation.inc({
×
559
      collection_name: this.modelName,
×
560
      type: PROMETHEUS_OP_TYPE.FETCH,
×
561
    })
×
562
  }
×
563

375✔
564
  const {
375✔
565
    crudContext,
375✔
566
    log,
375✔
567
    routeOptions: { config: { itemValidator } },
375✔
568
  } = request
375✔
569
  const docId = request.params.id
375✔
570
  const { acl_rows, acl_read_columns } = request.headers
375✔
571

375✔
572
  const {
375✔
573
    [QUERY]: clientQueryString,
375✔
574
    [PROJECTION]: clientProjectionString = '',
375✔
575
    [RAW_PROJECTION]: clientRawProjectionString = '',
375✔
576
    [STATE]: state,
375✔
577
    ...otherParams
375✔
578
  } = request.query
375✔
579

375✔
580
  const projection = resolveProjection(
375✔
581
    clientProjectionString,
375✔
582
    acl_read_columns,
375✔
583
    this.allFieldNames,
375✔
584
    clientRawProjectionString,
375✔
585
    log
375✔
586
  )
375✔
587
  const filter = resolveMongoQuery(
375✔
588
    this.queryParser,
375✔
589
    clientQueryString,
375✔
590
    acl_rows,
375✔
591
    otherParams,
375✔
592
    false
375✔
593
  )
375✔
594
  const _id = this.castCollectionId(docId)
375✔
595

375✔
596
  const stateArr = state.split(',')
375✔
597
  const doc = await this.crudService.findById(crudContext, _id, filter, projection, stateArr)
375✔
598
  if (!doc) {
375✔
599
    return reply.notFound()
78✔
600
  }
78✔
601

276✔
602
  const response = this.castItem(doc)
276✔
603
  itemValidator(response)
276✔
604
  return response
276✔
605
}
375✔
606

94✔
607
async function handleInsertOne(request, reply) {
76✔
608
  if (this.customMetrics) {
76✔
609
    this.customMetrics.collectionInvocation.inc({
13✔
610
      collection_name: this.modelName,
13✔
611
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
13✔
612
    })
13✔
613
  }
13✔
614

76✔
615
  const { body: doc, crudContext } = request
76✔
616

76✔
617
  this.queryParser.parseAndCastBody(doc)
76✔
618

76✔
619
  try {
76✔
620
    const insertedDoc = await this.crudService.insertOne(crudContext, doc)
76✔
621
    return mapToObjectWithOnlyId(insertedDoc)
73✔
622
  } catch (error) {
76✔
623
    if (error.code === UNIQUE_INDEX_MONGO_ERROR_CODE) {
3✔
624
      request.log.error('unique index violation')
3✔
625
      return reply.getHttpError(UNIQUE_INDEX_ERROR_STATUS_CODE, error.message)
3✔
626
    }
3✔
627
    throw error
×
628
  }
×
629
}
76✔
630

94✔
631
async function handleValidate() {
3✔
632
  return { result: 'ok' }
3✔
633
}
3✔
634

94✔
635
async function handleDeleteId(request, reply) {
56✔
636
  if (this.customMetrics) {
56!
637
    this.customMetrics.collectionInvocation.inc({
2✔
638
      collection_name: this.modelName,
2✔
639
      type: PROMETHEUS_OP_TYPE.DELETE,
2✔
640
    })
2✔
641
  }
2✔
642

56✔
643
  const { query, headers, params, crudContext } = request
56✔
644

56✔
645
  const docId = params.id
56✔
646
  const _id = this.castCollectionId(docId)
56✔
647

56✔
648
  const {
56✔
649
    [QUERY]: clientQueryString,
56✔
650
    [STATE]: state,
56✔
651
    ...otherParams
56✔
652
  } = query
56✔
653
  const { acl_rows } = headers
56✔
654

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

56✔
657
  const stateArr = state.split(',')
56✔
658
  const doc = await this.crudService.deleteById(crudContext, _id, filter, stateArr)
56✔
659

53✔
660
  if (!doc) {
56✔
661
    return reply.notFound()
18✔
662
  }
18✔
663

35✔
664
  // the document should not be returned:
35✔
665
  // we don't know which projection the user is able to see
35✔
666
  reply.code(204)
35✔
667
}
56✔
668

94✔
669
async function handleDeleteList(request) {
41✔
670
  if (this.customMetrics) {
41!
671
    this.customMetrics.collectionInvocation.inc({
2✔
672
      collection_name: this.modelName,
2✔
673
      type: PROMETHEUS_OP_TYPE.DELETE,
2✔
674
    })
2✔
675
  }
2✔
676

41✔
677
  const { query, headers, crudContext } = request
41✔
678

41✔
679
  const {
41✔
680
    [QUERY]: clientQueryString,
41✔
681
    [STATE]: state,
41✔
682
    ...otherParams
41✔
683
  } = query
41✔
684
  const { acl_rows } = headers
41✔
685

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

41✔
688
  const stateArr = state.split(',')
41✔
689
  return this.crudService.deleteAll(crudContext, filter, stateArr)
41✔
690
}
41✔
691

94✔
692
async function handleCount(request) {
51✔
693
  if (this.customMetrics) {
51✔
694
    this.customMetrics.collectionInvocation.inc({
3✔
695
      collection_name: this.modelName,
3✔
696
      type: PROMETHEUS_OP_TYPE.FETCH,
3✔
697
    })
3✔
698
  }
3✔
699

51✔
700
  const { query, headers, crudContext } = request
51✔
701
  const {
51✔
702
    [QUERY]: clientQueryString,
51✔
703
    [STATE]: state,
51✔
704
    [USE_ESTIMATE]: useEstimate,
51✔
705
    ...otherParams
51✔
706
  } = query
51✔
707

51✔
708
  const { acl_rows } = headers
51✔
709

51✔
710
  if (useEstimate) {
51✔
711
    return this.crudService.estimatedDocumentCount(crudContext)
6✔
712
  }
6✔
713

45✔
714
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
45✔
715
  const stateArr = state.split(',')
45✔
716

45✔
717
  return this.crudService.count(crudContext, mongoQuery, stateArr)
45✔
718
}
51✔
719

94✔
720
async function handlePatchId(request, reply) {
226✔
721
  if (this.customMetrics) {
226!
722
    this.customMetrics.collectionInvocation.inc({
4✔
723
      collection_name: this.modelName,
4✔
724
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
4✔
725
    })
4✔
726
  }
4✔
727

226✔
728
  const {
226✔
729
    query,
226✔
730
    headers,
226✔
731
    params,
226✔
732
    crudContext,
226✔
733
    log,
226✔
734
    routeOptions: { config: { itemValidator } },
226✔
735
  } = request
226✔
736

226✔
737
  const {
226✔
738
    [QUERY]: clientQueryString,
226✔
739
    [STATE]: state,
226✔
740
    ...otherParams
226✔
741
  } = query
226✔
742
  const {
226✔
743
    acl_rows,
226✔
744
    acl_write_columns: aclWriteColumns,
226✔
745
    acl_read_columns: aclColumns = '',
226✔
746
  } = headers
226✔
747

226✔
748
  const commands = request.body
226✔
749

226✔
750
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
226✔
751

226✔
752
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
226✔
753

226✔
754
  this.queryParser.parseAndCastCommands(commands, editableFields)
226✔
755
  const projection = resolveProjection('', aclColumns, this.allFieldNames, '', log)
226✔
756

226✔
757
  const docId = params.id
226✔
758
  const _id = this.castCollectionId(docId)
226✔
759

226✔
760
  const stateArr = state.split(',')
226✔
761
  const doc = await this.crudService.patchById(crudContext, _id, commands, mongoQuery, projection, stateArr)
226✔
762

220✔
763
  if (!doc) {
226✔
764
    return reply.notFound()
51✔
765
  }
51✔
766

169✔
767
  const response = this.castItem(doc)
169✔
768
  itemValidator(response)
169✔
769
  return response
169✔
770
}
226✔
771

94✔
772
async function handlePatchMany(request) {
90✔
773
  if (this.customMetrics) {
90!
774
    this.customMetrics.collectionInvocation.inc({
×
775
      collection_name: this.modelName,
×
776
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
777
    })
×
778
  }
×
779

90✔
780
  const { query, headers, crudContext } = request
90✔
781
  const {
90✔
782
    [QUERY]: clientQueryString,
90✔
783
    [STATE]: state,
90✔
784
    ...otherParams
90✔
785
  } = query
90✔
786
  const {
90✔
787
    acl_rows,
90✔
788
    acl_write_columns: aclWriteColumns,
90✔
789
  } = headers
90✔
790

90✔
791
  const commands = request.body
90✔
792
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
90✔
793
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
90✔
794
  this.queryParser.parseAndCastCommands(commands, editableFields)
90✔
795

90✔
796
  const stateArr = state.split(',')
90✔
797
  const nModified = await this.crudService.patchMany(crudContext, commands, mongoQuery, stateArr)
90✔
798

84✔
799
  return nModified
84✔
800
}
90✔
801

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

66✔
810
  const {
66✔
811
    query,
66✔
812
    headers,
66✔
813
    crudContext,
66✔
814
    log,
66✔
815
    routeOptions: { config: { itemValidator } },
66✔
816
  } = request
66✔
817
  const {
66✔
818
    [QUERY]: clientQueryString,
66✔
819
    [STATE]: state,
66✔
820
    ...otherParams
66✔
821
  } = query
66✔
822
  const {
66✔
823
    acl_rows,
66✔
824
    acl_write_columns: aclWriteColumns,
66✔
825
    acl_read_columns: aclColumns = '',
66✔
826
  } = headers
66✔
827

66✔
828
  const commands = request.body
66✔
829

66✔
830
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
66✔
831
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
66✔
832

66✔
833
  this.queryParser.parseAndCastCommands(commands, editableFields)
66✔
834
  const projection = resolveProjection('', aclColumns, this.allFieldNames, '', log)
66✔
835

66✔
836
  const stateArr = state.split(',')
66✔
837
  const doc = await this.crudService.upsertOne(crudContext, commands, mongoQuery, projection, stateArr)
66✔
838

66✔
839
  const response = this.castItem(doc)
66✔
840

66✔
841
  itemValidator(response)
66✔
842
  return response
66✔
843
}
66✔
844

94✔
845
async function handlePatchBulk(request) {
90✔
846
  if (this.customMetrics) {
90!
847
    this.customMetrics.collectionInvocation.inc({
×
848
      collection_name: this.modelName,
×
849
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
850
    })
×
851
  }
×
852

90✔
853
  const { body: filterUpdateCommands, crudContext, headers } = request
90✔
854

90✔
855
  const {
90✔
856
    acl_rows,
90✔
857
    acl_write_columns: aclWriteColumns,
90✔
858
  } = headers
90✔
859

90✔
860
  const parsedAndCastedCommands = new Array(filterUpdateCommands.length)
90✔
861
  for (let i = 0; i < filterUpdateCommands.length; i++) {
90✔
862
    const { filter, update } = filterUpdateCommands[i]
60,114✔
863
    const {
60,114✔
864
      _id,
60,114✔
865
      [QUERY]: clientQueryString,
60,114✔
866
      [STATE]: state,
60,114✔
867
      ...otherParams
60,114✔
868
    } = filter
60,114✔
869

60,114✔
870
    const commands = update
60,114✔
871

60,114✔
872
    const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
60,114✔
873

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

60,114✔
876
    this.queryParser.parseAndCastCommands(commands, editableFields)
60,114✔
877

60,114✔
878
    parsedAndCastedCommands[i] = {
60,114✔
879
      commands,
60,114✔
880
      state: state.split(','),
60,114✔
881
      query: mongoQuery,
60,114✔
882
    }
60,114✔
883
    if (_id) {
60,114✔
884
      parsedAndCastedCommands[i].query._id = this.castCollectionId(_id)
60,105✔
885
    }
60,105✔
886
  }
60,114✔
887

90✔
888
  const nModified = await this.crudService.patchBulk(crudContext, parsedAndCastedCommands)
90✔
889
  return nModified
90✔
890
}
90✔
891

94✔
892
async function handleInsertMany(request, reply) {
60✔
893
  if (this.customMetrics) {
60!
894
    this.customMetrics.collectionInvocation.inc({
×
895
      collection_name: this.modelName,
×
896
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
897
    })
×
898
  }
×
899

60✔
900
  const { body: docs, crudContext } = request
60✔
901

60✔
902
  docs.forEach(this.queryParser.parseAndCastBody)
60✔
903

60✔
904
  try {
60✔
905
    const insertedDocs = await this.crudService.insertMany(crudContext, docs)
60✔
906
    return insertedDocs.map(mapToObjectWithOnlyId)
57✔
907
  } catch (error) {
60✔
908
    if (error.code === UNIQUE_INDEX_MONGO_ERROR_CODE) {
3✔
909
      request.log.error('unique index violation')
3✔
910
      return reply.getHttpError(UNIQUE_INDEX_ERROR_STATUS_CODE, error.message)
3✔
911
    }
3✔
912
    throw error
×
913
  }
×
914
}
60✔
915

94✔
916
async function handleChangeStateById(request, reply) {
51✔
917
  if (this.customMetrics) {
51!
918
    this.customMetrics.collectionInvocation.inc({
×
919
      collection_name: this.modelName,
×
920
      type: PROMETHEUS_OP_TYPE.CHANGE_STATE,
×
921
    })
×
922
  }
×
923

51✔
924
  const { body, crudContext, headers, query } = request
51✔
925
  const {
51✔
926
    [QUERY]: clientQueryString,
51✔
927
    ...otherParams
51✔
928
  } = query
51✔
929

51✔
930
  const { acl_rows } = headers
51✔
931
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
51✔
932

51✔
933
  const docId = request.params.id
51✔
934
  const _id = this.castCollectionId(docId)
51✔
935

51✔
936
  try {
51✔
937
    const doc = await this.crudService.changeStateById(crudContext, _id, body.stateTo, mongoQuery)
51✔
938
    if (!doc) {
51✔
939
      return reply.notFound()
9✔
940
    }
9✔
941

24✔
942
    reply.code(204)
24✔
943
  } catch (error) {
51✔
944
    if (error.statusCode) {
15✔
945
      return reply.getHttpError(error.statusCode, error.message)
15✔
946
    }
15✔
947

×
948
    throw error
×
949
  }
×
950
}
51✔
951

94✔
952
async function handleChangeStateMany(request) {
48✔
953
  if (this.customMetrics) {
48!
954
    this.customMetrics.collectionInvocation.inc({
×
955
      collection_name: this.modelName,
×
956
      type: PROMETHEUS_OP_TYPE.CHANGE_STATE,
×
957
    })
×
958
  }
×
959

48✔
960
  const { body: filterUpdateCommands, crudContext, headers } = request
48✔
961

48✔
962
  const {
48✔
963
    acl_rows,
48✔
964
  } = headers
48✔
965

48✔
966
  const parsedAndCastedCommands = new Array(filterUpdateCommands.length)
48✔
967
  for (let i = 0; i < filterUpdateCommands.length; i++) {
48✔
968
    const {
60✔
969
      filter,
60✔
970
      stateTo,
60✔
971
    } = filterUpdateCommands[i]
60✔
972

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

60✔
975
    parsedAndCastedCommands[i] = {
60✔
976
      query: mongoQuery,
60✔
977
      stateTo,
60✔
978
    }
60✔
979
  }
60✔
980

48✔
981
  return this.crudService.changeStateMany(crudContext, parsedAndCastedCommands)
48✔
982
}
48✔
983

94✔
984
async function injectContextInRequest(request) {
2,558✔
985
  const userIdHeader = request.headers[this.userIdHeaderKey]
2,558✔
986
  const isUserHeaderInvalid = INVALID_USERID.includes(userIdHeader)
2,558✔
987

2,558✔
988
  let userId = 'public'
2,558✔
989

2,558✔
990
  if (userIdHeader && !isUserHeaderInvalid) {
2,558✔
991
    userId = userIdHeader
666✔
992
  }
666✔
993

2,558✔
994
  request.crudContext = {
2,558✔
995
    log: request.log,
2,558✔
996
    userId,
2,558✔
997
    now: new Date(),
2,558✔
998
  }
2,558✔
999
}
2,558✔
1000

94✔
1001
async function parseEncodedJsonQueryParams(logger, request) {
2,558✔
1002
  if (request.headers.json_query_params_encoding) {
2,558!
1003
    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.')
×
1004
  }
×
1005

2,558✔
1006
  // TODO remove request.headers.json_query_params_encoding fallback in v7.0.0
2,558✔
1007
  const jsonQueryParamsEncoding = request.headers['json-query-params-encoding'] || request.headers.json_query_params_encoding
2,558✔
1008
  switch (jsonQueryParamsEncoding) {
2,558✔
1009
  case 'base64': {
2,558✔
1010
    const queryJsonFields = [QUERY, RAW_PROJECTION]
9✔
1011
    for (const field of queryJsonFields) {
9✔
1012
      if (request.query[field]) {
18✔
1013
        request.query[field] = Buffer.from(request.query[field], jsonQueryParamsEncoding).toString()
9✔
1014
      }
9✔
1015
    }
18✔
1016
    break
9✔
1017
  }
9✔
1018
  default: break
2,558✔
1019
  }
2,558✔
1020
}
2,558✔
1021

94✔
1022
async function notFoundHandler(request, reply) {
174✔
1023
  reply
174✔
1024
    .code(404)
174✔
1025
    .send({
174✔
1026
      error: 'not found',
174✔
1027
    })
174✔
1028
}
174✔
1029

94✔
1030
async function customErrorHandler(error, request, reply) {
659✔
1031
  if (error.statusCode === 404) {
659✔
1032
    return notFoundHandler(request, reply)
156✔
1033
  }
156✔
1034

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

395✔
1040
  throw error
395✔
1041
}
659✔
1042

94✔
1043
function resolveMongoQuery(
62,116✔
1044
  queryParser,
62,116✔
1045
  clientQueryString,
62,116✔
1046
  rawAclRows,
62,116✔
1047
  otherParams,
62,116✔
1048
  textQuery
62,116✔
1049
) {
62,116✔
1050
  const mongoQuery = {
62,116✔
1051
    $and: [],
62,116✔
1052
  }
62,116✔
1053

62,116✔
1054
  if (clientQueryString) {
62,116✔
1055
    const clientQuery = JSON.parse(clientQueryString)
657✔
1056
    mongoQuery.$and.push(clientQuery)
657✔
1057
  }
657✔
1058
  if (otherParams) {
62,116✔
1059
    for (const key of Object.keys(otherParams)) {
62,116✔
1060
      const value = otherParams[key]
531✔
1061
      mongoQuery.$and.push({ [key]: value })
531✔
1062
    }
531✔
1063
  }
62,116✔
1064

62,116✔
1065
  if (rawAclRows) {
62,116✔
1066
    const aclRows = JSON.parse(rawAclRows)
300✔
1067
    if (rawAclRows[0] === '[') {
300✔
1068
      mongoQuery.$and.push({ $and: aclRows })
288✔
1069
    } else {
300✔
1070
      mongoQuery.$and.push(aclRows)
12✔
1071
    }
12✔
1072
  }
300✔
1073

62,116✔
1074
  try {
62,116✔
1075
    if (textQuery) {
62,116✔
1076
      queryParser.parseAndCastTextSearchQuery(mongoQuery)
72✔
1077
    } else {
62,116✔
1078
      queryParser.parseAndCast(mongoQuery)
62,044✔
1079
    }
62,044✔
1080
  } catch (error) {
62,116✔
1081
    throw new BadRequestError(error.message)
21✔
1082
  }
21✔
1083

62,095✔
1084
  if (!mongoQuery.$and.length) {
62,116✔
1085
    return {}
61,004✔
1086
  }
61,004✔
1087

1,091✔
1088
  return mongoQuery
1,091✔
1089
}
62,116✔
1090

94✔
1091
function resolveProjection(clientProjectionString, aclColumns, allFieldNames, rawProjection, log) {
1,740✔
1092
  log.debug('Resolving projections')
1,740✔
1093
  const acls = splitACLs(aclColumns)
1,740✔
1094

1,740✔
1095
  if (clientProjectionString && rawProjection) {
1,740✔
1096
    log.error('Use of both _p and _rawp is not permitted')
18✔
1097
    throw new BadRequestError(
18✔
1098
      'Use of both _rawp and _p parameter is not allowed')
18✔
1099
  }
18✔
1100

1,722✔
1101
  if (!clientProjectionString && !rawProjection) {
1,740✔
1102
    return removeAclColumns(allFieldNames, acls)
1,165✔
1103
  } else if (rawProjection) {
1,740✔
1104
    return resolveRawProjectionString(rawProjection, acls, allFieldNames, log)
231✔
1105
  } else if (clientProjectionString) {
557✔
1106
    return resolveClientProjectionString(clientProjectionString, acls)
326✔
1107
  }
326✔
1108
}
1,740✔
1109

94✔
1110
function resolveClientProjectionString(clientProjectionString, _acls) {
326✔
1111
  const clientProjection = getClientProjection(clientProjectionString)
326✔
1112
  return removeAclColumns(clientProjection, _acls)
326✔
1113
}
326✔
1114

94✔
1115
function resolveRawProjectionString(rawProjection, _acls, allFieldNames, log) {
231✔
1116
  try {
231✔
1117
    checkAllowedOperators(
231✔
1118
      rawProjection,
231✔
1119
      rawProjectionDictionary,
231✔
1120
      _acls.length > 0 ? _acls : allFieldNames, log)
231✔
1121

231✔
1122
    const rawProjectionObject = resolveRawProjection(rawProjection)
231✔
1123
    const projection = removeAclColumnsFromRawProjection(rawProjectionObject, _acls)
231✔
1124

231✔
1125
    return !lisEmpty(projection) ? [projection] : []
231✔
1126
  } catch (errorMessage) {
231✔
1127
    log.error(errorMessage.message)
66✔
1128
    throw new BadRequestError(errorMessage.message)
66✔
1129
  }
66✔
1130
}
231✔
1131

94✔
1132
function splitACLs(acls) {
1,740✔
1133
  if (acls) { return acls.split(',') }
1,740✔
1134
  return []
1,564✔
1135
}
1,740✔
1136

94✔
1137
function removeAclColumns(fieldsInProjection, aclColumns) {
1,656✔
1138
  if (aclColumns.length > 0) {
1,656✔
1139
    return fieldsInProjection.filter(field => {
170✔
1140
      return aclColumns.indexOf(field) > -1
960✔
1141
    })
170✔
1142
  }
170✔
1143

1,486✔
1144
  return fieldsInProjection
1,486✔
1145
}
1,656✔
1146

94✔
1147
function removeAclColumnsFromRawProjection(rawProjectionObject, aclColumns) {
171✔
1148
  const isRawProjectionOverridingACLs = checkIfRawProjectionOverridesAcls(rawProjectionObject, aclColumns)
171✔
1149
  if (isRawProjectionOverridingACLs) {
171✔
1150
    throw Error('_rawp exclusive projection is overriding at least one acl_read_column value')
6✔
1151
  }
6✔
1152

165✔
1153
  const rawProjectionFields = Object.keys(rawProjectionObject)
165✔
1154
  const filteredFields = removeAclColumns(rawProjectionFields, aclColumns)
165✔
1155

165✔
1156
  return filteredFields.reduce((acc, current) => {
165✔
1157
    if (rawProjectionObject[current] === 0 || rawProjectionObject[current]) {
249✔
1158
      acc[current] = rawProjectionObject[current]
249✔
1159
    }
249✔
1160
    return acc
249✔
1161
  }, {})
165✔
1162
}
171✔
1163

94✔
1164
function getClientProjection(clientProjectionString) {
326✔
1165
  if (clientProjectionString) {
326✔
1166
    return clientProjectionString.split(',')
326✔
1167
  }
326✔
1168
  return []
×
1169
}
326✔
1170

94✔
1171
function resolveRawProjection(clientRawProjectionString) {
171✔
1172
  if (clientRawProjectionString) {
171✔
1173
    return JSON.parse(clientRawProjectionString)
171✔
1174
  }
171✔
1175
  return {}
×
1176
}
171✔
1177

94✔
1178
function checkAllowedOperators(rawProjection, projectionDictionary, additionalFields, log) {
231✔
1179
  if (!rawProjection) {
231!
1180
    log.debug('No raw projection found: checkAllowedOperators returns true')
×
1181
    return true
×
1182
  }
×
1183

231✔
1184
  const { allowedOperators, notAllowedOperators } = projectionDictionary
231✔
1185
  const allowedFields = [...allowedOperators]
231✔
1186

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

231✔
1189
  log.debug({ allowedOperators: allowedFields }, 'Allowed operators for projection')
231✔
1190
  log.debug({ notAllowedOperators }, 'Not allowed operators for projection')
231✔
1191

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

231✔
1196
  if (!matches) {
231✔
1197
    log.debug('No operators found in raw projection: checkAllowedOperators returns true')
93✔
1198
    return true
93✔
1199
  }
93✔
1200

138✔
1201
  return !matches.some(match => {
138✔
1202
    if (match.startsWith('$$')) {
336✔
1203
      log.debug({ match }, 'Found $$ match in raw projection')
96✔
1204
      if (notAllowedOperators.includes(match)) {
96✔
1205
        throw Error(`Operator ${match} is not allowed in raw projection`)
48✔
1206
      }
48✔
1207

48✔
1208
      return notAllowedOperators.includes(match)
48✔
1209
    }
48✔
1210

240✔
1211
    if (!allowedFields.includes(match)) {
336✔
1212
      throw Error(`Operator ${match} is not allowed in raw projection`)
12✔
1213
    }
12✔
1214

228✔
1215
    return !allowedFields.includes(match)
228✔
1216
  })
138✔
1217
}
231✔
1218

94✔
1219
function checkIfRawProjectionOverridesAcls(rawProjection, acls) {
171✔
1220
  return Object.keys(rawProjection).some(field =>
171✔
1221
    acls.includes(field) && rawProjection[field] === 0
351✔
1222
  )
171✔
1223
}
171✔
1224

94✔
1225
function mapToObjectWithOnlyId(doc) {
150,184✔
1226
  return { _id: doc._id.toString() }
150,184✔
1227
}
150,184✔
1228

94✔
1229
const internalFields = [
94✔
1230
  UPDATERID,
94✔
1231
  UPDATEDAT,
94✔
1232
  CREATORID,
94✔
1233
  CREATEDAT,
94✔
1234
  __STATE__,
94✔
1235
]
94✔
1236
function getEditableFields(aclWriteColumns, allFieldNames) {
60,496✔
1237
  const editableFields = aclWriteColumns ? aclWriteColumns.split(',') : allFieldNames
60,496!
1238
  return editableFields.filter(ef => !internalFields.includes(ef))
60,496✔
1239
}
60,496✔
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