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

mia-platform / crud-service / 8817358943

24 Apr 2024 01:26PM UTC coverage: 97.128%. Remained the same
8817358943

push

github

web-flow
build(deps): bump node (#293)

Bumps node from 20.12.1-bullseye-slim to 20.12.2-bullseye-slim.

---
updated-dependencies:
- dependency-name: node
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

1817 of 1966 branches covered (92.42%)

Branch coverage included in aggregate %.

9071 of 9244 relevant lines covered (98.13%)

7212.22 hits per line

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

90.94
/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
  EXPORT_OPTIONS,
94✔
34
  QUERY,
94✔
35
  LIMIT,
94✔
36
  SKIP,
94✔
37
  STATE,
94✔
38
  INVALID_USERID,
94✔
39
  UPDATERID,
94✔
40
  UPDATEDAT,
94✔
41
  CREATORID,
94✔
42
  CREATEDAT,
94✔
43
  __STATE__,
94✔
44
  SCHEMA_CUSTOM_KEYWORDS,
94✔
45
  rawProjectionDictionary,
94✔
46
  USE_ESTIMATE,
94✔
47
  BAD_REQUEST_ERROR_STATUS_CODE,
94✔
48
  INTERNAL_SERVER_ERROR_STATUS_CODE,
94✔
49
  UNIQUE_INDEX_ERROR_STATUS_CODE,
94✔
50
  NOT_ACCEPTABLE,
94✔
51
  UNSUPPORTED_MIME_TYPE_STATUS_CODE,
94✔
52
  ACL_WRITE_COLUMNS,
94✔
53
  ACL_ROWS,
94✔
54
} = require('./consts')
94✔
55

94✔
56
const getAccept = require('./acceptHeaderParser')
94✔
57
const BadRequestError = require('./BadRequestError')
94✔
58
const BatchWritableStream = require('./BatchWritableStream')
94✔
59

94✔
60
const { SCHEMAS_ID } = require('./schemaGetters')
94✔
61
const { getAjvResponseValidationFunction, shouldValidateStream, shouldValidateItem } = require('./validatorGetters')
94✔
62
const { getFileMimeParser, getFileMimeStringifiers } = require('./mimeTypeTransform')
94✔
63
const resolveMongoQuery = require('./resolveMongoQuery')
94✔
64

94✔
65
const OPTIONS_INCOMPATIBILITY_ERROR_CODE = 2
94✔
66
const UNIQUE_INDEX_MONGO_ERROR_CODE = 11000
94✔
67

94✔
68
const PROMETHEUS_OP_TYPE = {
94✔
69
  FETCH: 'fetch',
94✔
70
  INSERT_OR_UPDATE: 'insert_or_update',
94✔
71
  DELETE: 'delete',
94✔
72
  CHANGE_STATE: 'change_state',
94✔
73
}
94✔
74

94✔
75

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

4,428✔
89
  const {
4,428✔
90
    registerGetters = true,
4,428✔
91
    registerSetters = true,
4,428✔
92
    registerLookup = false,
4,428✔
93
  } = options
4,428✔
94

4,428✔
95
  const validateOutput = fastify.validateOutput ?? false
4,428!
96

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

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

4,428✔
131
  fastify.setValidatorCompiler(({ schema }) => {
4,428✔
132
    const uniqueId = schema[SCHEMA_CUSTOM_KEYWORDS.UNIQUE_OPERATION_ID]
152,640✔
133
    const [collectionName, schemaId, subSchemaPath] = uniqueId.split('__MIA__')
152,640✔
134
    const nestedSchema = NESTED_SCHEMAS_BY_ID[schemaId]
152,640✔
135
    const subSchema = lget(nestedSchema, subSchemaPath)
152,640✔
136
    fastify.log.trace({ collectionName, schemaPath: subSchemaPath, schemaId }, 'collection schema info')
152,640✔
137

152,640✔
138
    // this is made to prevent to shows on swagger all properties with dot notation of RawObject with schema.
152,640✔
139
    return ajv.compile(subSchema)
152,640✔
140
  })
4,428✔
141

4,428✔
142
  fastify.addHook('preHandler', injectContextInRequest)
4,428✔
143
  fastify.addHook('preHandler', request => parseEncodedJsonQueryParams(fastify.log, request))
4,428✔
144
  fastify.setErrorHandler(customErrorHandler)
4,428✔
145

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

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

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

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

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

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

3,789✔
243
    fastify.log.debug({ collection: fastify?.modelName }, 'setters endpoints registered')
3,789✔
244
  }
3,789✔
245

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

4,428✔
259
  if (registerGetters) {
4,428✔
260
    const getItemJSONSchema = fastify.jsonSchemaGenerator.generateGetItemJSONSchema()
4,404✔
261
    fastify.get('/export', {
4,404✔
262
      schema: fastify.jsonSchemaGenerator.generateExportJSONSchema(),
4,404✔
263
      config: {
4,404✔
264
        streamValidator: shouldValidateStream(getItemJSONSchema.response['200'], validateOutput),
4,404✔
265
        replyType: (acceptHeader) => {
4,404✔
266
          const accept = getAccept(acceptHeader)
873✔
267

873✔
268
          if (!accept || accept === '*/*') { return 'application/x-ndjson' }
873!
269

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

4,404✔
300
    fastify.log.debug({ collection: fastify?.modelName }, 'getters endpoints registered')
4,404✔
301
  }
4,404✔
302

4,428✔
303
  fastify.addHook('onRequest', async(request) => {
4,428✔
304
    if (request.headers.acl_rows) {
2,765✔
305
      request.headers.acl_rows = JSON.parse(request.headers.acl_rows)
309✔
306
    }
309✔
307
  })
4,428✔
308
}
4,428✔
309

94✔
310
// eslint-disable-next-line max-statements
94✔
311
async function handleCollectionImport(request, reply) {
72✔
312
  if (this.customMetrics) {
72!
313
    this.customMetrics.collectionInvocation.inc({
×
314
      collection_name: this.modelName,
×
315
      type: PROMETHEUS_OP_TYPE.IMPORT,
×
316
    })
×
317
  }
×
318

72✔
319
  if (!request.isMultipart()) {
72!
320
    return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, 'Request is not multipart')
×
321
  }
×
322

72✔
323
  const data = await request.file()
72✔
324
  if (!data) {
72!
325
    return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, 'Missing file')
×
326
  }
×
327
  const { file, mimetype, fields } = data
72✔
328
  const parsingOptions = Object.fromEntries(Object.values(fields)
72✔
329
    .filter(field => field.type === 'field')
72✔
330
    .map(({ fieldname, value }) => [fieldname, value]))
72✔
331

72✔
332
  const {
72✔
333
    log,
72✔
334
    crudContext,
72✔
335
    routeOptions: { config: { itemValidator, validateImportOptions } },
72✔
336
  } = request
72✔
337
  const isValid = validateImportOptions(parsingOptions)
72✔
338
  if (!isValid) {
72✔
339
    return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, `Invalid options`)
12✔
340
  }
12✔
341

60✔
342
  const bodyParser = getFileMimeParser(mimetype, parsingOptions)
60✔
343
  if (!bodyParser) {
72!
344
    return reply.getHttpError(UNSUPPORTED_MIME_TYPE_STATUS_CODE, `Unsupported file type ${mimetype}`)
×
345
  }
×
346

60✔
347
  const { crudService, queryParser } = this
60✔
348

60✔
349
  let documentIndex = 0
60✔
350
  const parseDocument = through2.obj((chunk, _enc, callback) => {
60✔
351
    try {
144✔
352
      itemValidator(chunk)
144✔
353
      if (itemValidator.errors) { throw itemValidator.errors }
144✔
354
    } catch (error) {
144✔
355
      return callback(error, chunk)
6✔
356
    }
6✔
357
    documentIndex += 1
138✔
358
    return callback(null, chunk)
138✔
359
  })
60✔
360

60✔
361
  // POST
60✔
362
  let returnCode = 201
60✔
363
  let processBatch = async(batch) => crudService.insertMany(crudContext, batch, queryParser)
60✔
364

60✔
365
  // PATCH
60✔
366
  if (request.method === 'PATCH') {
66✔
367
    returnCode = 200
39✔
368
    processBatch = async(batch) => {
39✔
369
      return crudService.upsertMany(crudContext, batch, queryParser)
36✔
370
    }
36✔
371
  }
39✔
372

60✔
373
  const batchConsumer = new BatchWritableStream({
60✔
374
    batchSize: 5000,
60✔
375
    highWaterMark: 1000,
60✔
376
    objectMode: true,
60✔
377
    processBatch,
60✔
378
  })
60✔
379

60✔
380
  try {
60✔
381
    await pipeline(
60✔
382
      file,
60✔
383
      bodyParser(),
60✔
384
      parseDocument,
60✔
385
      batchConsumer
60✔
386
    )
60✔
387
  } catch (error) {
72✔
388
    if (error.code === OPTIONS_INCOMPATIBILITY_ERROR_CODE) {
9!
389
      log.debug('stream error')
×
390
      return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, error.message)
×
391
    }
×
392

9✔
393
    if (error.code === UNIQUE_INDEX_MONGO_ERROR_CODE) {
9✔
394
      log.debug('unique index violation')
3✔
395
      return reply.getHttpError(UNIQUE_INDEX_ERROR_STATUS_CODE, error.message)
3✔
396
    }
3✔
397

6✔
398
    if (Array.isArray(error)) {
6✔
399
      log.debug('error parsing input file')
6✔
400
      const { message, instancePath } = error?.[0] ?? {}
6!
401
      const errorDetails = instancePath ? `, ${instancePath}` : ''
6!
402
      const errorMessage = `(index: ${documentIndex}${errorDetails}) ${message ?? 'error in parsing record'}`
6!
403
      return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, errorMessage)
6✔
404
    }
6✔
405

×
406
    return reply.getHttpError(INTERNAL_SERVER_ERROR_STATUS_CODE, error.message || 'something went wrong')
9✔
407
  }
9✔
408

51✔
409
  return reply.code(returnCode).send({ message: 'File uploaded successfully' })
51✔
410
}
72✔
411

94✔
412
// eslint-disable-next-line max-statements
94✔
413
async function handleGetListLookup(request, reply) {
40✔
414
  if (this.customMetrics) {
40✔
415
    this.customMetrics.collectionInvocation.inc({
40✔
416
      collection_name: this.modelName,
40✔
417
      type: PROMETHEUS_OP_TYPE.FETCH,
40✔
418
    })
40✔
419
  }
40✔
420

40✔
421
  const {
40✔
422
    query,
40✔
423
    headers,
40✔
424
    crudContext,
40✔
425
    log,
40✔
426
    routeOptions: { config: { replyType, streamValidator } },
40✔
427
  } = request
40✔
428

40✔
429
  const {
40✔
430
    [QUERY]: clientQueryString,
40✔
431
    [PROJECTION]: clientProjectionString = '',
40✔
432
    [SORT]: sortQuery,
40✔
433
    [LIMIT]: limit,
40✔
434
    [SKIP]: skip,
40✔
435
    [STATE]: state,
40✔
436
    [EXPORT_OPTIONS]: exportOpts = '',
40✔
437
    ...otherParams
40✔
438
  } = query
40✔
439
  const { acl_rows, acl_read_columns } = headers
40✔
440

40✔
441
  let projection = resolveProjection(
40✔
442
    clientProjectionString,
40✔
443
    acl_read_columns,
40✔
444
    this.allFieldNames,
40✔
445
    '',
40✔
446
    log
40✔
447
  )
40✔
448

40✔
449
  projection = this.lookupProjection.filter(proj => projection.includes(Object.keys(proj)[0]))
40✔
450
  if (projection.length === 0) {
40✔
451
    reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, 'No allowed colums')
2✔
452
  }
2✔
453

40✔
454
  const LookupProjectionFieldsToOmit = this.lookupProjection.filter(field => Object.values(field).shift() === 0)
40✔
455
  projection.push(...LookupProjectionFieldsToOmit)
40✔
456

40✔
457
  const isTextSearchQuery = query._q && this.queryParser.isTextSearchQuery(JSON.parse(query._q))
40✔
458
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, isTextSearchQuery)
40✔
459
  let sort
40✔
460
  if (sortQuery) {
40✔
461
    sort = Object.fromEntries(sortQuery.toString().split(',')
4✔
462
      .map((param) => (param[0] === '-' ? [param.substr(1), -1] : [param, 1])))
4✔
463
  }
4✔
464

40✔
465
  const stateArr = state?.split(',')
40✔
466
  const contentType = replyType()
40✔
467
  const parsingOptions = contentType === 'text/csv' && exportOpts ? JSON.parse(exportOpts) : {}
40!
468

40✔
469
  const responseStringifiers = getFileMimeStringifiers(contentType, parsingOptions)
40✔
470
  if (!responseStringifiers) {
40!
471
    return reply.getHttpError(UNSUPPORTED_MIME_TYPE_STATUS_CODE, `Unsupported file type ${contentType}`)
×
472
  }
×
473

40✔
474
  reply.raw.setHeader('Content-Type', contentType)
40✔
475

40✔
476
  try {
40✔
477
    return await pipeline(
40✔
478
      this.crudService
40✔
479
        .aggregate(crudContext, mongoQuery, projection, sort, skip, limit, stateArr, isTextSearchQuery)
40✔
480
        .stream(),
40✔
481
      this.castResultsAsStream(),
40✔
482
      streamValidator(),
40✔
483
      ...responseStringifiers({ fields: this.allFieldNames }),
40✔
484
      reply.raw
40✔
485
    )
40✔
486
  } catch (error) {
40✔
487
    request.log.error({ error }, 'Error during findAll lookup stream')
2✔
488
    request.log.debug({ error: { ...error, message: error.message } }, 'Error during findAll lookup stream with message')
2✔
489
    if (error.code === OPTIONS_INCOMPATIBILITY_ERROR_CODE) {
2!
490
      request.log.info(BAD_REQUEST_ERROR_STATUS_CODE)
×
491
    }
×
492
  }
2✔
493
}
40✔
494

94✔
495
async function handleGetList(request, reply) {
1,207✔
496
  if (this.customMetrics) {
1,207✔
497
    this.customMetrics.collectionInvocation.inc({
67✔
498
      collection_name: this.modelName,
67✔
499
      type: PROMETHEUS_OP_TYPE.FETCH,
67✔
500
    })
67✔
501
  }
67✔
502

1,207✔
503
  const {
1,207✔
504
    query,
1,207✔
505
    headers,
1,207✔
506
    crudContext,
1,207✔
507
    log,
1,207✔
508
    routeOptions: { config: { replyType, streamValidator } },
1,207✔
509
  } = request
1,207✔
510
  const {
1,207✔
511
    [QUERY]: clientQueryString,
1,207✔
512
    [PROJECTION]: clientProjectionString = '',
1,207✔
513
    [RAW_PROJECTION]: clientRawProjectionString = '',
1,207✔
514
    [SORT]: sortQuery,
1,207✔
515
    [LIMIT]: limit,
1,207✔
516
    [SKIP]: skip,
1,207✔
517
    [STATE]: state,
1,207✔
518
    [EXPORT_OPTIONS]: exportOpts = '',
1,207✔
519
    ...otherParams
1,207✔
520
  } = query
1,207✔
521
  const { acl_rows, acl_read_columns, accept } = headers
1,207✔
522
  const contentType = replyType(accept)
1,207✔
523
  const parsingOptions = contentType === 'text/csv' && exportOpts ? JSON.parse(exportOpts) : {}
1,207✔
524

1,207✔
525
  const responseStringifiers = getFileMimeStringifiers(contentType, parsingOptions)
1,207✔
526
  if (!responseStringifiers) {
1,207✔
527
    return reply.getHttpError(NOT_ACCEPTABLE, `unsupported file type ${contentType}`)
159✔
528
  }
159✔
529

1,048✔
530
  const projection = resolveProjection(
1,048✔
531
    clientProjectionString,
1,048✔
532
    acl_read_columns,
1,048✔
533
    this.allFieldNames,
1,048✔
534
    clientRawProjectionString,
1,048✔
535
    log
1,048✔
536
  )
1,048✔
537

1,048✔
538
  const isTextSearchQuery = query._q && this.queryParser.isTextSearchQuery(JSON.parse(query._q))
1,207✔
539
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, isTextSearchQuery)
1,207✔
540

1,207✔
541
  let sort
1,207✔
542
  if (sortQuery) {
1,207✔
543
    sort = Object.fromEntries(sortQuery.toString().split(',')
122✔
544
      .map((param) => (param[0] === '-' ? [param.substr(1), -1] : [param, 1])))
122✔
545
  }
122✔
546

982✔
547
  const stateArr = state.split(',')
982✔
548

982✔
549
  reply.raw.setHeader('Content-Type', contentType)
982✔
550

982✔
551
  try {
982✔
552
    await pipeline(
982✔
553
      this.crudService
982✔
554
        .findAll(crudContext, mongoQuery, projection, sort, skip, limit, stateArr, isTextSearchQuery)
982✔
555
        .stream(),
982✔
556
      this.castResultsAsStream(),
982✔
557
      streamValidator(),
982✔
558
      ...responseStringifiers({ fields: this.allFieldNames }),
982✔
559
      reply.raw
982✔
560
    )
982✔
561
  } catch (error) {
1,207✔
562
    request.log.error({ error }, 'Error during findAll stream')
3✔
563
    request.log.debug({ error: { ...error, message: error.message } }, 'Error during findAll stream with message')
3✔
564
    if (error.code === OPTIONS_INCOMPATIBILITY_ERROR_CODE) {
3!
565
      request.log.info(BAD_REQUEST_ERROR_STATUS_CODE)
×
566
    }
×
567
  }
3✔
568
}
1,207✔
569

94✔
570
async function handleGetId(request, reply) {
375✔
571
  if (this.customMetrics) {
375!
572
    this.customMetrics.collectionInvocation.inc({
×
573
      collection_name: this.modelName,
×
574
      type: PROMETHEUS_OP_TYPE.FETCH,
×
575
    })
×
576
  }
×
577

375✔
578
  const {
375✔
579
    crudContext,
375✔
580
    log,
375✔
581
    routeOptions: { config: { itemValidator } },
375✔
582
  } = request
375✔
583
  const docId = request.params.id
375✔
584
  const { acl_rows, acl_read_columns } = request.headers
375✔
585

375✔
586
  const {
375✔
587
    [QUERY]: clientQueryString,
375✔
588
    [PROJECTION]: clientProjectionString = '',
375✔
589
    [RAW_PROJECTION]: clientRawProjectionString = '',
375✔
590
    [STATE]: state,
375✔
591
    ...otherParams
375✔
592
  } = request.query
375✔
593

375✔
594
  const projection = resolveProjection(
375✔
595
    clientProjectionString,
375✔
596
    acl_read_columns,
375✔
597
    this.allFieldNames,
375✔
598
    clientRawProjectionString,
375✔
599
    log
375✔
600
  )
375✔
601
  const filter = resolveMongoQuery(
375✔
602
    this.queryParser,
375✔
603
    clientQueryString,
375✔
604
    acl_rows,
375✔
605
    otherParams,
375✔
606
    false
375✔
607
  )
375✔
608
  const _id = this.castCollectionId(docId)
375✔
609

375✔
610
  const stateArr = state.split(',')
375✔
611
  const doc = await this.crudService.findById(crudContext, _id, filter, projection, stateArr)
375✔
612
  if (!doc) {
375✔
613
    return reply.notFound()
78✔
614
  }
78✔
615

276✔
616
  const response = this.castItem(doc)
276✔
617
  itemValidator(response)
276✔
618
  return response
276✔
619
}
375✔
620

94✔
621
async function handleInsertOne(request, reply) {
76✔
622
  if (this.customMetrics) {
76✔
623
    this.customMetrics.collectionInvocation.inc({
13✔
624
      collection_name: this.modelName,
13✔
625
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
13✔
626
    })
13✔
627
  }
13✔
628

76✔
629
  const { body: doc, crudContext } = request
76✔
630

76✔
631
  this.queryParser.parseAndCastBody(doc)
76✔
632

76✔
633
  try {
76✔
634
    const insertedDoc = await this.crudService.insertOne(crudContext, doc)
76✔
635
    return mapToObjectWithOnlyId(insertedDoc)
73✔
636
  } catch (error) {
76✔
637
    if (error.code === UNIQUE_INDEX_MONGO_ERROR_CODE) {
3✔
638
      request.log.error('unique index violation')
3✔
639
      return reply.getHttpError(UNIQUE_INDEX_ERROR_STATUS_CODE, error.message)
3✔
640
    }
3✔
641
    throw error
×
642
  }
×
643
}
76✔
644

94✔
645
async function handleValidate() {
3✔
646
  return { result: 'ok' }
3✔
647
}
3✔
648

94✔
649
async function handleDeleteId(request, reply) {
56✔
650
  if (this.customMetrics) {
56!
651
    this.customMetrics.collectionInvocation.inc({
2✔
652
      collection_name: this.modelName,
2✔
653
      type: PROMETHEUS_OP_TYPE.DELETE,
2✔
654
    })
2✔
655
  }
2✔
656

56✔
657
  const { query, headers, params, crudContext } = request
56✔
658

56✔
659
  const docId = params.id
56✔
660
  const _id = this.castCollectionId(docId)
56✔
661

56✔
662
  const {
56✔
663
    [QUERY]: clientQueryString,
56✔
664
    [STATE]: state,
56✔
665
    ...otherParams
56✔
666
  } = query
56✔
667
  const { acl_rows } = headers
56✔
668

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

56✔
671
  const stateArr = state.split(',')
56✔
672
  const doc = await this.crudService.deleteById(crudContext, _id, filter, stateArr)
56✔
673

53✔
674
  if (!doc) {
56✔
675
    return reply.notFound()
18✔
676
  }
18✔
677

35✔
678
  // the document should not be returned:
35✔
679
  // we don't know which projection the user is able to see
35✔
680
  reply.code(204)
35✔
681
}
56✔
682

94✔
683
async function handleDeleteList(request) {
41✔
684
  if (this.customMetrics) {
41!
685
    this.customMetrics.collectionInvocation.inc({
2✔
686
      collection_name: this.modelName,
2✔
687
      type: PROMETHEUS_OP_TYPE.DELETE,
2✔
688
    })
2✔
689
  }
2✔
690

41✔
691
  const { query, headers, crudContext } = request
41✔
692

41✔
693
  const {
41✔
694
    [QUERY]: clientQueryString,
41✔
695
    [STATE]: state,
41✔
696
    ...otherParams
41✔
697
  } = query
41✔
698
  const { acl_rows } = headers
41✔
699

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

41✔
702
  const stateArr = state.split(',')
41✔
703
  return this.crudService.deleteAll(crudContext, filter, stateArr)
41✔
704
}
41✔
705

94✔
706
async function handleCount(request) {
51✔
707
  if (this.customMetrics) {
51✔
708
    this.customMetrics.collectionInvocation.inc({
3✔
709
      collection_name: this.modelName,
3✔
710
      type: PROMETHEUS_OP_TYPE.FETCH,
3✔
711
    })
3✔
712
  }
3✔
713

51✔
714
  const { query, headers, crudContext } = request
51✔
715
  const {
51✔
716
    [QUERY]: clientQueryString,
51✔
717
    [STATE]: state,
51✔
718
    [USE_ESTIMATE]: useEstimate,
51✔
719
    ...otherParams
51✔
720
  } = query
51✔
721

51✔
722
  const { acl_rows } = headers
51✔
723

51✔
724
  if (useEstimate) {
51✔
725
    return this.crudService.estimatedDocumentCount(crudContext)
6✔
726
  }
6✔
727

45✔
728
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
45✔
729
  const stateArr = state.split(',')
45✔
730

45✔
731
  return this.crudService.count(crudContext, mongoQuery, stateArr)
45✔
732
}
51✔
733

94✔
734
async function handlePatchId(request, reply) {
226✔
735
  if (this.customMetrics) {
226!
736
    this.customMetrics.collectionInvocation.inc({
4✔
737
      collection_name: this.modelName,
4✔
738
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
4✔
739
    })
4✔
740
  }
4✔
741

226✔
742
  const {
226✔
743
    query,
226✔
744
    headers,
226✔
745
    params,
226✔
746
    crudContext,
226✔
747
    log,
226✔
748
    routeOptions: { config: { itemValidator } },
226✔
749
  } = request
226✔
750

226✔
751
  const {
226✔
752
    [QUERY]: clientQueryString,
226✔
753
    [STATE]: state,
226✔
754
    ...otherParams
226✔
755
  } = query
226✔
756
  const {
226✔
757
    acl_rows,
226✔
758
    acl_write_columns: aclWriteColumns,
226✔
759
    acl_read_columns: aclColumns = '',
226✔
760
  } = headers
226✔
761

226✔
762
  const commands = request.body
226✔
763

226✔
764
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
226✔
765

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

226✔
768
  this.queryParser.parseAndCastCommands(commands, editableFields)
226✔
769
  const projection = resolveProjection('', aclColumns, this.allFieldNames, '', log)
226✔
770

226✔
771
  const docId = params.id
226✔
772
  const _id = this.castCollectionId(docId)
226✔
773

226✔
774
  const stateArr = state.split(',')
226✔
775
  const doc = await this.crudService.patchById(crudContext, _id, commands, mongoQuery, projection, stateArr)
226✔
776

220✔
777
  if (!doc) {
226✔
778
    return reply.notFound()
51✔
779
  }
51✔
780

169✔
781
  const response = this.castItem(doc)
169✔
782
  itemValidator(response)
169✔
783
  return response
169✔
784
}
226✔
785

94✔
786
async function handlePatchMany(request) {
90✔
787
  if (this.customMetrics) {
90!
788
    this.customMetrics.collectionInvocation.inc({
×
789
      collection_name: this.modelName,
×
790
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
791
    })
×
792
  }
×
793

90✔
794
  const { query, headers, crudContext } = request
90✔
795
  const {
90✔
796
    [QUERY]: clientQueryString,
90✔
797
    [STATE]: state,
90✔
798
    ...otherParams
90✔
799
  } = query
90✔
800
  const {
90✔
801
    acl_rows,
90✔
802
    acl_write_columns: aclWriteColumns,
90✔
803
  } = headers
90✔
804

90✔
805
  const commands = request.body
90✔
806
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
90✔
807
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
90✔
808
  this.queryParser.parseAndCastCommands(commands, editableFields)
90✔
809

90✔
810
  const stateArr = state.split(',')
90✔
811
  const nModified = await this.crudService.patchMany(crudContext, commands, mongoQuery, stateArr)
90✔
812

84✔
813
  return nModified
84✔
814
}
90✔
815

94✔
816
async function handleUpsertOne(request) {
66✔
817
  if (this.customMetrics) {
66!
818
    this.customMetrics.collectionInvocation.inc({
×
819
      collection_name: this.modelName,
×
820
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
821
    })
×
822
  }
×
823

66✔
824
  const {
66✔
825
    query,
66✔
826
    headers,
66✔
827
    crudContext,
66✔
828
    log,
66✔
829
    routeOptions: { config: { itemValidator } },
66✔
830
  } = request
66✔
831
  const {
66✔
832
    [QUERY]: clientQueryString,
66✔
833
    [STATE]: state,
66✔
834
    ...otherParams
66✔
835
  } = query
66✔
836
  const {
66✔
837
    acl_rows,
66✔
838
    acl_write_columns: aclWriteColumns,
66✔
839
    acl_read_columns: aclColumns = '',
66✔
840
  } = headers
66✔
841

66✔
842
  const commands = request.body
66✔
843

66✔
844
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
66✔
845
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
66✔
846

66✔
847
  this.queryParser.parseAndCastCommands(commands, editableFields)
66✔
848
  const projection = resolveProjection('', aclColumns, this.allFieldNames, '', log)
66✔
849

66✔
850
  const stateArr = state.split(',')
66✔
851
  const doc = await this.crudService.upsertOne(crudContext, commands, mongoQuery, projection, stateArr)
66✔
852

66✔
853
  const response = this.castItem(doc)
66✔
854

66✔
855
  itemValidator(response)
66✔
856
  return response
66✔
857
}
66✔
858

94✔
859
async function handlePatchBulk(request) {
90✔
860
  if (this.customMetrics) {
90!
861
    this.customMetrics.collectionInvocation.inc({
×
862
      collection_name: this.modelName,
×
863
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
864
    })
×
865
  }
×
866

90✔
867
  const { body: filterUpdateCommands, crudContext, headers } = request
90✔
868

90✔
869

90✔
870
  const nModified = await this.crudService.patchBulk(
90✔
871
    crudContext,
90✔
872
    filterUpdateCommands,
90✔
873
    this.queryParser,
90✔
874
    this.castCollectionId,
90✔
875
    getEditableFields(headers[ACL_WRITE_COLUMNS], this.allFieldNames),
90✔
876
    headers[ACL_ROWS],
90✔
877
  )
90✔
878
  return nModified
90✔
879
}
90✔
880

94✔
881
async function handleInsertMany(request, reply) {
60✔
882
  if (this.customMetrics) {
60!
883
    this.customMetrics.collectionInvocation.inc({
×
884
      collection_name: this.modelName,
×
885
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
886
    })
×
887
  }
×
888

60✔
889
  const { body: docs, crudContext } = request
60✔
890

60✔
891
  try {
60✔
892
    const ids = await this.crudService.insertMany(
60✔
893
      crudContext,
60✔
894
      docs,
60✔
895
      this.queryParser,
60✔
896
      { idOnly: true }
60✔
897
    )
60✔
898
    return ids
57✔
899
  } catch (error) {
60✔
900
    if (error.code === UNIQUE_INDEX_MONGO_ERROR_CODE) {
3✔
901
      request.log.error('unique index violation')
3✔
902
      return reply.getHttpError(UNIQUE_INDEX_ERROR_STATUS_CODE, error.message)
3✔
903
    }
3✔
904
    throw error
×
905
  }
×
906
}
60✔
907

94✔
908
async function handleChangeStateById(request, reply) {
51✔
909
  if (this.customMetrics) {
51!
910
    this.customMetrics.collectionInvocation.inc({
×
911
      collection_name: this.modelName,
×
912
      type: PROMETHEUS_OP_TYPE.CHANGE_STATE,
×
913
    })
×
914
  }
×
915

51✔
916
  const { body, crudContext, headers, query } = request
51✔
917
  const {
51✔
918
    [QUERY]: clientQueryString,
51✔
919
    ...otherParams
51✔
920
  } = query
51✔
921

51✔
922
  const { acl_rows } = headers
51✔
923
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
51✔
924

51✔
925
  const docId = request.params.id
51✔
926
  const _id = this.castCollectionId(docId)
51✔
927

51✔
928
  try {
51✔
929
    const doc = await this.crudService.changeStateById(crudContext, _id, body.stateTo, mongoQuery)
51✔
930
    if (!doc) {
51✔
931
      return reply.notFound()
9✔
932
    }
9✔
933

24✔
934
    reply.code(204)
24✔
935
  } catch (error) {
51✔
936
    if (error.statusCode) {
15✔
937
      return reply.getHttpError(error.statusCode, error.message)
15✔
938
    }
15✔
939

×
940
    throw error
×
941
  }
×
942
}
51✔
943

94✔
944
async function handleChangeStateMany(request) {
48✔
945
  if (this.customMetrics) {
48!
946
    this.customMetrics.collectionInvocation.inc({
×
947
      collection_name: this.modelName,
×
948
      type: PROMETHEUS_OP_TYPE.CHANGE_STATE,
×
949
    })
×
950
  }
×
951

48✔
952
  const { body: filterUpdateCommands, crudContext, headers } = request
48✔
953

48✔
954
  const {
48✔
955
    acl_rows,
48✔
956
  } = headers
48✔
957

48✔
958
  const parsedAndCastedCommands = new Array(filterUpdateCommands.length)
48✔
959
  for (let i = 0; i < filterUpdateCommands.length; i++) {
48✔
960
    const {
60✔
961
      filter,
60✔
962
      stateTo,
60✔
963
    } = filterUpdateCommands[i]
60✔
964

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

60✔
967
    parsedAndCastedCommands[i] = {
60✔
968
      query: mongoQuery,
60✔
969
      stateTo,
60✔
970
    }
60✔
971
  }
60✔
972

48✔
973
  return this.crudService.changeStateMany(crudContext, parsedAndCastedCommands)
48✔
974
}
48✔
975

94✔
976
async function injectContextInRequest(request) {
2,573✔
977
  const userIdHeader = request.headers[this.userIdHeaderKey]
2,573✔
978
  const isUserHeaderInvalid = INVALID_USERID.includes(userIdHeader)
2,573✔
979

2,573✔
980
  let userId = 'public'
2,573✔
981

2,573✔
982
  if (userIdHeader && !isUserHeaderInvalid) {
2,573✔
983
    userId = userIdHeader
666✔
984
  }
666✔
985

2,573✔
986
  request.crudContext = {
2,573✔
987
    log: request.log,
2,573✔
988
    userId,
2,573✔
989
    now: new Date(),
2,573✔
990
  }
2,573✔
991
}
2,573✔
992

94✔
993
async function parseEncodedJsonQueryParams(logger, request) {
2,573✔
994
  if (request.headers.json_query_params_encoding) {
2,573!
995
    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.')
×
996
  }
×
997

2,573✔
998
  // TODO remove request.headers.json_query_params_encoding fallback in v7.0.0
2,573✔
999
  const jsonQueryParamsEncoding = request.headers['json-query-params-encoding'] || request.headers.json_query_params_encoding
2,573✔
1000
  switch (jsonQueryParamsEncoding) {
2,573✔
1001
  case 'base64': {
2,573✔
1002
    const queryJsonFields = [QUERY, RAW_PROJECTION]
9✔
1003
    for (const field of queryJsonFields) {
9✔
1004
      if (request.query[field]) {
18✔
1005
        request.query[field] = Buffer.from(request.query[field], jsonQueryParamsEncoding).toString()
9✔
1006
      }
9✔
1007
    }
18✔
1008
    break
9✔
1009
  }
9✔
1010
  default: break
2,573✔
1011
  }
2,573✔
1012
}
2,573✔
1013

94✔
1014
async function notFoundHandler(request, reply) {
174✔
1015
  reply
174✔
1016
    .code(404)
174✔
1017
    .send({
174✔
1018
      error: 'not found',
174✔
1019
    })
174✔
1020
}
174✔
1021

94✔
1022
async function customErrorHandler(error, request, reply) {
662✔
1023
  if (error.statusCode === 404) {
662✔
1024
    return notFoundHandler(request, reply)
156✔
1025
  }
156✔
1026

506✔
1027
  if (error.validation?.[0]?.message === 'must NOT have additional properties') {
662✔
1028
    reply.code(error.statusCode)
108✔
1029
    throw new Error(`${error.message}. Property "${error.validation[0].params.additionalProperty}" is not defined in validation schema`)
108✔
1030
  }
108✔
1031

398✔
1032
  throw error
398✔
1033
}
662✔
1034

94✔
1035
function resolveProjection(clientProjectionString, aclColumns, allFieldNames, rawProjection, log) {
1,752✔
1036
  log.debug('Resolving projections')
1,752✔
1037
  const acls = splitACLs(aclColumns)
1,752✔
1038

1,752✔
1039
  if (clientProjectionString && rawProjection) {
1,752✔
1040
    log.error('Use of both _p and _rawp is not permitted')
18✔
1041
    throw new BadRequestError(
18✔
1042
      'Use of both _rawp and _p parameter is not allowed')
18✔
1043
  }
18✔
1044

1,734✔
1045
  if (!clientProjectionString && !rawProjection) {
1,752✔
1046
    return removeAclColumns(allFieldNames, acls)
1,177✔
1047
  } else if (rawProjection) {
1,752✔
1048
    return resolveRawProjectionString(rawProjection, acls, allFieldNames, log)
231✔
1049
  } else if (clientProjectionString) {
557✔
1050
    return resolveClientProjectionString(clientProjectionString, acls)
326✔
1051
  }
326✔
1052
}
1,752✔
1053

94✔
1054
function resolveClientProjectionString(clientProjectionString, _acls) {
326✔
1055
  const clientProjection = getClientProjection(clientProjectionString)
326✔
1056
  return removeAclColumns(clientProjection, _acls)
326✔
1057
}
326✔
1058

94✔
1059
function resolveRawProjectionString(rawProjection, _acls, allFieldNames, log) {
231✔
1060
  try {
231✔
1061
    checkAllowedOperators(
231✔
1062
      rawProjection,
231✔
1063
      rawProjectionDictionary,
231✔
1064
      _acls.length > 0 ? _acls : allFieldNames, log)
231✔
1065

231✔
1066
    const rawProjectionObject = resolveRawProjection(rawProjection)
231✔
1067
    const projection = removeAclColumnsFromRawProjection(rawProjectionObject, _acls)
231✔
1068

231✔
1069
    return !lisEmpty(projection) ? [projection] : []
231✔
1070
  } catch (errorMessage) {
231✔
1071
    log.error(errorMessage.message)
66✔
1072
    throw new BadRequestError(errorMessage.message)
66✔
1073
  }
66✔
1074
}
231✔
1075

94✔
1076
function splitACLs(acls) {
1,752✔
1077
  if (acls) { return acls.split(',') }
1,752✔
1078
  return []
1,576✔
1079
}
1,752✔
1080

94✔
1081
function removeAclColumns(fieldsInProjection, aclColumns) {
1,668✔
1082
  if (aclColumns.length > 0) {
1,668✔
1083
    return fieldsInProjection.filter(field => {
170✔
1084
      return aclColumns.indexOf(field) > -1
960✔
1085
    })
170✔
1086
  }
170✔
1087

1,498✔
1088
  return fieldsInProjection
1,498✔
1089
}
1,668✔
1090

94✔
1091
function removeAclColumnsFromRawProjection(rawProjectionObject, aclColumns) {
171✔
1092
  const isRawProjectionOverridingACLs = checkIfRawProjectionOverridesAcls(rawProjectionObject, aclColumns)
171✔
1093
  if (isRawProjectionOverridingACLs) {
171✔
1094
    throw Error('_rawp exclusive projection is overriding at least one acl_read_column value')
6✔
1095
  }
6✔
1096

165✔
1097
  const rawProjectionFields = Object.keys(rawProjectionObject)
165✔
1098
  const filteredFields = removeAclColumns(rawProjectionFields, aclColumns)
165✔
1099

165✔
1100
  return filteredFields.reduce((acc, current) => {
165✔
1101
    if (rawProjectionObject[current] === 0 || rawProjectionObject[current]) {
249✔
1102
      acc[current] = rawProjectionObject[current]
249✔
1103
    }
249✔
1104
    return acc
249✔
1105
  }, {})
165✔
1106
}
171✔
1107

94✔
1108
function getClientProjection(clientProjectionString) {
326✔
1109
  if (clientProjectionString) {
326✔
1110
    return clientProjectionString.split(',')
326✔
1111
  }
326✔
1112
  return []
×
1113
}
326✔
1114

94✔
1115
function resolveRawProjection(clientRawProjectionString) {
171✔
1116
  if (clientRawProjectionString) {
171✔
1117
    return JSON.parse(clientRawProjectionString)
171✔
1118
  }
171✔
1119
  return {}
×
1120
}
171✔
1121

94✔
1122
function checkAllowedOperators(rawProjection, projectionDictionary, additionalFields, log) {
231✔
1123
  if (!rawProjection) {
231!
1124
    log.debug('No raw projection found: checkAllowedOperators returns true')
×
1125
    return true
×
1126
  }
×
1127

231✔
1128
  const { allowedOperators, notAllowedOperators } = projectionDictionary
231✔
1129
  const allowedFields = [...allowedOperators]
231✔
1130

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

231✔
1133
  log.debug({ allowedOperators: allowedFields }, 'Allowed operators for projection')
231✔
1134
  log.debug({ notAllowedOperators }, 'Not allowed operators for projection')
231✔
1135

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

231✔
1140
  if (!matches) {
231✔
1141
    log.debug('No operators found in raw projection: checkAllowedOperators returns true')
93✔
1142
    return true
93✔
1143
  }
93✔
1144

138✔
1145
  return !matches.some(match => {
138✔
1146
    if (match.startsWith('$$')) {
336✔
1147
      log.debug({ match }, 'Found $$ match in raw projection')
96✔
1148
      if (notAllowedOperators.includes(match)) {
96✔
1149
        throw Error(`Operator ${match} is not allowed in raw projection`)
48✔
1150
      }
48✔
1151

48✔
1152
      return notAllowedOperators.includes(match)
48✔
1153
    }
48✔
1154

240✔
1155
    if (!allowedFields.includes(match)) {
336✔
1156
      throw Error(`Operator ${match} is not allowed in raw projection`)
12✔
1157
    }
12✔
1158

228✔
1159
    return !allowedFields.includes(match)
228✔
1160
  })
138✔
1161
}
231✔
1162

94✔
1163
function checkIfRawProjectionOverridesAcls(rawProjection, acls) {
171✔
1164
  return Object.keys(rawProjection).some(field =>
171✔
1165
    acls.includes(field) && rawProjection[field] === 0
351✔
1166
  )
171✔
1167
}
171✔
1168

94✔
1169
function mapToObjectWithOnlyId(doc) {
73✔
1170
  return { _id: doc._id.toString() }
73✔
1171
}
73✔
1172

94✔
1173
const internalFields = [
94✔
1174
  UPDATERID,
94✔
1175
  UPDATEDAT,
94✔
1176
  CREATORID,
94✔
1177
  CREATEDAT,
94✔
1178
  __STATE__,
94✔
1179
]
94✔
1180
function getEditableFields(aclWriteColumns, allFieldNames) {
472✔
1181
  const editableFields = aclWriteColumns ? aclWriteColumns.split(',') : allFieldNames
472!
1182
  return editableFields.filter(ef => !internalFields.includes(ef))
472✔
1183
}
472✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc