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

mia-platform / crud-service / 7744629622

01 Feb 2024 04:41PM UTC coverage: 97.122% (-0.008%) from 97.13%
7744629622

push

github

web-flow
feat: prepare for v7 (#264)

* chore: update dependencies

* docs: add migration guide to v7

* ci(github): remove actions execution on branch v6.x

* chore: update CHANGELOG.md

* ci(github): review docker dependabot config to prevent semver-major PRs

* docs: configure guides folder name

1817 of 1964 branches covered (0.0%)

Branch coverage included in aggregate %.

8980 of 9153 relevant lines covered (98.11%)

7357.34 hits per line

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

91.47
/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.setNotFoundHandler(notFoundHandler)
4,344✔
272
    fastify.get('/', {
4,344✔
273
      schema: fastify.jsonSchemaGenerator.generateGetListJSONSchema(),
4,344✔
274
      config: {
4,344✔
275
        streamValidator: shouldValidateStream(getItemJSONSchema.response['200'], validateOutput),
4,344✔
276
        replyType: () => 'application/json',
4,344✔
277
      },
4,344✔
278
    }, handleGetList)
4,344✔
279
    fastify.get('/:id', {
4,344✔
280
      schema: getItemJSONSchema,
4,344✔
281
      config: {
4,344✔
282
        itemValidator: shouldValidateItem(getItemJSONSchema.response['200'], validateOutput),
4,344✔
283
      },
4,344✔
284
    }, handleGetId)
4,344✔
285
    fastify.get('/schema', {
4,344✔
286
      schema: fastify.jsonSchemaGenerator.generateGetSchemaJSONSchema(),
4,344✔
287
    }, () => ({
4,344✔
288
      type: getItemJSONSchema.response['200'].type,
3✔
289
      properties: getItemJSONSchema.response['200'].properties,
3✔
290
    }))
4,344✔
291

4,344✔
292
    fastify.log.debug({ collection: fastify?.modelName }, 'getters endpoints registered')
4,344✔
293
  }
4,344✔
294
}
4,368✔
295

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

72✔
305
  if (!request.isMultipart()) {
72!
306
    return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, 'Request is not multipart')
×
307
  }
×
308

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

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

60✔
328
  const bodyParser = getFileMimeParser(mimetype, parsingOptions)
60✔
329
  if (!bodyParser) {
72!
330
    return reply.getHttpError(UNSUPPORTED_MIME_TYPE_STATUS_CODE, `Unsupported file type ${mimetype}`)
×
331
  }
×
332

60✔
333
  const { crudService, queryParser } = this
60✔
334

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

60✔
348
  // POST
60✔
349
  let returnCode = 201
60✔
350
  let processBatch = async(batch) => crudService.insertMany(crudContext, batch)
60✔
351

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

60✔
360
  const batchConsumer = new BatchWritableStream({
60✔
361
    batchSize: 5000,
60✔
362
    highWaterMark: 1000,
60✔
363
    objectMode: true,
60✔
364
    processBatch,
60✔
365
  })
60✔
366

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

9✔
380
    if (error.code === UNIQUE_INDEX_MONGO_ERROR_CODE) {
9✔
381
      log.debug('unique index violation')
3✔
382
      return reply.getHttpError(UNIQUE_INDEX_ERROR_STATUS_CODE, error.message)
3✔
383
    }
3✔
384

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

×
393
    return reply.getHttpError(INTERNAL_SERVER_ERROR_STATUS_CODE, error.message || 'something went wrong')
9✔
394
  }
9✔
395

51✔
396
  return reply.code(returnCode).send({ message: 'File uploaded successfully' })
51✔
397
}
72✔
398

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

40✔
408
  const {
40✔
409
    query,
40✔
410
    headers,
40✔
411
    crudContext,
40✔
412
    log,
40✔
413
    routeOptions: { config: { replyType, streamValidator } },
40✔
414
  } = request
40✔
415

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

40✔
427
  let projection = resolveProjection(
40✔
428
    clientProjectionString,
40✔
429
    acl_read_columns,
40✔
430
    this.allFieldNames,
40✔
431
    '',
40✔
432
    log
40✔
433
  )
40✔
434

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

40✔
440
  const LookupProjectionFieldsToOmit = this.lookupProjection.filter(field => Object.values(field).shift() === 0)
40✔
441
  projection.push(...LookupProjectionFieldsToOmit)
40✔
442

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

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

40✔
458
  reply.raw.setHeader('Content-Type', contentType)
40✔
459

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

94✔
479
async function handleGetList(request, reply) {
1,192✔
480
  if (this.customMetrics) {
1,192✔
481
    this.customMetrics.collectionInvocation.inc({
67✔
482
      collection_name: this.modelName,
67✔
483
      type: PROMETHEUS_OP_TYPE.FETCH,
67✔
484
    })
67✔
485
  }
67✔
486

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

1,192✔
507
  const responseStringifiers = getFileMimeStringifiers(contentType, {})
1,192✔
508
  if (!responseStringifiers) {
1,192✔
509
    return reply.getHttpError(NOT_ACCEPTABLE, `unsupported file type ${contentType}`)
156✔
510
  }
156✔
511

1,036✔
512
  const projection = resolveProjection(
1,036✔
513
    clientProjectionString,
1,036✔
514
    acl_read_columns,
1,036✔
515
    this.allFieldNames,
1,036✔
516
    clientRawProjectionString,
1,036✔
517
    log
1,036✔
518
  )
1,036✔
519

1,036✔
520
  const isTextSearchQuery = query._q && this.queryParser.isTextSearchQuery(JSON.parse(query._q))
1,192✔
521
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, isTextSearchQuery)
1,192✔
522

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

970✔
529
  const stateArr = state.split(',')
970✔
530

970✔
531
  reply.raw.setHeader('Content-Type', contentType)
970✔
532

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

94✔
552
async function handleGetId(request, reply) {
375✔
553
  if (this.customMetrics) {
375!
554
    this.customMetrics.collectionInvocation.inc({
×
555
      collection_name: this.modelName,
×
556
      type: PROMETHEUS_OP_TYPE.FETCH,
×
557
    })
×
558
  }
×
559

375✔
560
  const {
375✔
561
    crudContext,
375✔
562
    log,
375✔
563
    routeOptions: { config: { itemValidator } },
375✔
564
  } = request
375✔
565
  const docId = request.params.id
375✔
566
  const { acl_rows, acl_read_columns } = request.headers
375✔
567

375✔
568
  const {
375✔
569
    [QUERY]: clientQueryString,
375✔
570
    [PROJECTION]: clientProjectionString = '',
375✔
571
    [RAW_PROJECTION]: clientRawProjectionString = '',
375✔
572
    [STATE]: state,
375✔
573
    ...otherParams
375✔
574
  } = request.query
375✔
575

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

375✔
592
  const stateArr = state.split(',')
375✔
593
  const doc = await this.crudService.findById(crudContext, _id, filter, projection, stateArr)
375✔
594
  if (!doc) {
375✔
595
    return reply.notFound()
78✔
596
  }
78✔
597

276✔
598
  const response = this.castItem(doc)
276✔
599
  itemValidator(response)
276✔
600
  return response
276✔
601
}
375✔
602

94✔
603
async function handleInsertOne(request, reply) {
76✔
604
  if (this.customMetrics) {
76✔
605
    this.customMetrics.collectionInvocation.inc({
13✔
606
      collection_name: this.modelName,
13✔
607
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
13✔
608
    })
13✔
609
  }
13✔
610

76✔
611
  const { body: doc, crudContext } = request
76✔
612

76✔
613
  this.queryParser.parseAndCastBody(doc)
76✔
614

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

94✔
627
async function handleValidate() {
3✔
628
  return { result: 'ok' }
3✔
629
}
3✔
630

94✔
631
async function handleDeleteId(request, reply) {
56✔
632
  if (this.customMetrics) {
56!
633
    this.customMetrics.collectionInvocation.inc({
2✔
634
      collection_name: this.modelName,
2✔
635
      type: PROMETHEUS_OP_TYPE.DELETE,
2✔
636
    })
2✔
637
  }
2✔
638

56✔
639
  const { query, headers, params, crudContext } = request
56✔
640

56✔
641
  const docId = params.id
56✔
642
  const _id = this.castCollectionId(docId)
56✔
643

56✔
644
  const {
56✔
645
    [QUERY]: clientQueryString,
56✔
646
    [STATE]: state,
56✔
647
    ...otherParams
56✔
648
  } = query
56✔
649
  const { acl_rows } = headers
56✔
650

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

56✔
653
  const stateArr = state.split(',')
56✔
654
  const doc = await this.crudService.deleteById(crudContext, _id, filter, stateArr)
56✔
655

53✔
656
  if (!doc) {
56✔
657
    return reply.notFound()
18✔
658
  }
18✔
659

35✔
660
  // the document should not be returned:
35✔
661
  // we don't know which projection the user is able to see
35✔
662
  reply.code(204)
35✔
663
}
56✔
664

94✔
665
async function handleDeleteList(request) {
41✔
666
  if (this.customMetrics) {
41!
667
    this.customMetrics.collectionInvocation.inc({
2✔
668
      collection_name: this.modelName,
2✔
669
      type: PROMETHEUS_OP_TYPE.DELETE,
2✔
670
    })
2✔
671
  }
2✔
672

41✔
673
  const { query, headers, crudContext } = request
41✔
674

41✔
675
  const {
41✔
676
    [QUERY]: clientQueryString,
41✔
677
    [STATE]: state,
41✔
678
    ...otherParams
41✔
679
  } = query
41✔
680
  const { acl_rows } = headers
41✔
681

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

41✔
684
  const stateArr = state.split(',')
41✔
685
  return this.crudService.deleteAll(crudContext, filter, stateArr)
41✔
686
}
41✔
687

94✔
688
async function handleCount(request) {
51✔
689
  if (this.customMetrics) {
51✔
690
    this.customMetrics.collectionInvocation.inc({
3✔
691
      collection_name: this.modelName,
3✔
692
      type: PROMETHEUS_OP_TYPE.FETCH,
3✔
693
    })
3✔
694
  }
3✔
695

51✔
696
  const { query, headers, crudContext } = request
51✔
697
  const {
51✔
698
    [QUERY]: clientQueryString,
51✔
699
    [STATE]: state,
51✔
700
    [USE_ESTIMATE]: useEstimate,
51✔
701
    ...otherParams
51✔
702
  } = query
51✔
703

51✔
704
  const { acl_rows } = headers
51✔
705

51✔
706
  if (useEstimate) {
51✔
707
    return this.crudService.estimatedDocumentCount(crudContext)
6✔
708
  }
6✔
709

45✔
710
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
45✔
711
  const stateArr = state.split(',')
45✔
712

45✔
713
  return this.crudService.count(crudContext, mongoQuery, stateArr)
45✔
714
}
51✔
715

94✔
716
async function handlePatchId(request, reply) {
226✔
717
  if (this.customMetrics) {
226!
718
    this.customMetrics.collectionInvocation.inc({
4✔
719
      collection_name: this.modelName,
4✔
720
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
4✔
721
    })
4✔
722
  }
4✔
723

226✔
724
  const {
226✔
725
    query,
226✔
726
    headers,
226✔
727
    params,
226✔
728
    crudContext,
226✔
729
    log,
226✔
730
    routeOptions: { config: { itemValidator } },
226✔
731
  } = request
226✔
732

226✔
733
  const {
226✔
734
    [QUERY]: clientQueryString,
226✔
735
    [STATE]: state,
226✔
736
    ...otherParams
226✔
737
  } = query
226✔
738
  const {
226✔
739
    acl_rows,
226✔
740
    acl_write_columns: aclWriteColumns,
226✔
741
    acl_read_columns: aclColumns = '',
226✔
742
  } = headers
226✔
743

226✔
744
  const commands = request.body
226✔
745

226✔
746
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
226✔
747

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

226✔
750
  this.queryParser.parseAndCastCommands(commands, editableFields)
226✔
751
  const projection = resolveProjection('', aclColumns, this.allFieldNames, '', log)
226✔
752

226✔
753
  const docId = params.id
226✔
754
  const _id = this.castCollectionId(docId)
226✔
755

226✔
756
  const stateArr = state.split(',')
226✔
757
  const doc = await this.crudService.patchById(crudContext, _id, commands, mongoQuery, projection, stateArr)
226✔
758

220✔
759
  if (!doc) {
226✔
760
    return reply.notFound()
51✔
761
  }
51✔
762

169✔
763
  const response = this.castItem(doc)
169✔
764
  itemValidator(response)
169✔
765
  return response
169✔
766
}
226✔
767

94✔
768
async function handlePatchMany(request) {
90✔
769
  if (this.customMetrics) {
90!
770
    this.customMetrics.collectionInvocation.inc({
×
771
      collection_name: this.modelName,
×
772
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
773
    })
×
774
  }
×
775

90✔
776
  const { query, headers, crudContext } = request
90✔
777
  const {
90✔
778
    [QUERY]: clientQueryString,
90✔
779
    [STATE]: state,
90✔
780
    ...otherParams
90✔
781
  } = query
90✔
782
  const {
90✔
783
    acl_rows,
90✔
784
    acl_write_columns: aclWriteColumns,
90✔
785
  } = headers
90✔
786

90✔
787
  const commands = request.body
90✔
788
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
90✔
789
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
90✔
790
  this.queryParser.parseAndCastCommands(commands, editableFields)
90✔
791

90✔
792
  const stateArr = state.split(',')
90✔
793
  const nModified = await this.crudService.patchMany(crudContext, commands, mongoQuery, stateArr)
90✔
794

84✔
795
  return nModified
84✔
796
}
90✔
797

94✔
798
async function handleUpsertOne(request) {
66✔
799
  if (this.customMetrics) {
66!
800
    this.customMetrics.collectionInvocation.inc({
×
801
      collection_name: this.modelName,
×
802
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
803
    })
×
804
  }
×
805

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

66✔
824
  const commands = request.body
66✔
825

66✔
826
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
66✔
827
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
66✔
828

66✔
829
  this.queryParser.parseAndCastCommands(commands, editableFields)
66✔
830
  const projection = resolveProjection('', aclColumns, this.allFieldNames, '', log)
66✔
831

66✔
832
  const stateArr = state.split(',')
66✔
833
  const doc = await this.crudService.upsertOne(crudContext, commands, mongoQuery, projection, stateArr)
66✔
834

66✔
835
  const response = this.castItem(doc)
66✔
836

66✔
837
  itemValidator(response)
66✔
838
  return response
66✔
839
}
66✔
840

94✔
841
async function handlePatchBulk(request) {
90✔
842
  if (this.customMetrics) {
90!
843
    this.customMetrics.collectionInvocation.inc({
×
844
      collection_name: this.modelName,
×
845
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
846
    })
×
847
  }
×
848

90✔
849
  const { body: filterUpdateCommands, crudContext, headers } = request
90✔
850

90✔
851
  const {
90✔
852
    acl_rows,
90✔
853
    acl_write_columns: aclWriteColumns,
90✔
854
  } = headers
90✔
855

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

60,114✔
866
    const commands = update
60,114✔
867

60,114✔
868
    const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
60,114✔
869

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

60,114✔
872
    this.queryParser.parseAndCastCommands(commands, editableFields)
60,114✔
873

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

90✔
884
  const nModified = await this.crudService.patchBulk(crudContext, parsedAndCastedCommands)
90✔
885
  return nModified
90✔
886
}
90✔
887

94✔
888
async function handleInsertMany(request, reply) {
60✔
889
  if (this.customMetrics) {
60!
890
    this.customMetrics.collectionInvocation.inc({
×
891
      collection_name: this.modelName,
×
892
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
893
    })
×
894
  }
×
895

60✔
896
  const { body: docs, crudContext } = request
60✔
897

60✔
898
  docs.forEach(this.queryParser.parseAndCastBody)
60✔
899

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

94✔
912
async function handleChangeStateById(request, reply) {
51✔
913
  if (this.customMetrics) {
51!
914
    this.customMetrics.collectionInvocation.inc({
×
915
      collection_name: this.modelName,
×
916
      type: PROMETHEUS_OP_TYPE.CHANGE_STATE,
×
917
    })
×
918
  }
×
919

51✔
920
  const { body, crudContext, headers, query } = request
51✔
921
  const {
51✔
922
    [QUERY]: clientQueryString,
51✔
923
    ...otherParams
51✔
924
  } = query
51✔
925

51✔
926
  const { acl_rows } = headers
51✔
927
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
51✔
928

51✔
929
  const docId = request.params.id
51✔
930
  const _id = this.castCollectionId(docId)
51✔
931

51✔
932
  try {
51✔
933
    const doc = await this.crudService.changeStateById(crudContext, _id, body.stateTo, mongoQuery)
51✔
934
    if (!doc) {
51✔
935
      return reply.notFound()
9✔
936
    }
9✔
937

24✔
938
    reply.code(204)
24✔
939
  } catch (error) {
51✔
940
    if (error.statusCode) {
15✔
941
      return reply.getHttpError(error.statusCode, error.message)
15✔
942
    }
15✔
943

×
944
    throw error
×
945
  }
×
946
}
51✔
947

94✔
948
async function handleChangeStateMany(request) {
48✔
949
  if (this.customMetrics) {
48!
950
    this.customMetrics.collectionInvocation.inc({
×
951
      collection_name: this.modelName,
×
952
      type: PROMETHEUS_OP_TYPE.CHANGE_STATE,
×
953
    })
×
954
  }
×
955

48✔
956
  const { body: filterUpdateCommands, crudContext, headers } = request
48✔
957

48✔
958
  const {
48✔
959
    acl_rows,
48✔
960
  } = headers
48✔
961

48✔
962
  const parsedAndCastedCommands = new Array(filterUpdateCommands.length)
48✔
963
  for (let i = 0; i < filterUpdateCommands.length; i++) {
48✔
964
    const {
60✔
965
      filter,
60✔
966
      stateTo,
60✔
967
    } = filterUpdateCommands[i]
60✔
968

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

60✔
971
    parsedAndCastedCommands[i] = {
60✔
972
      query: mongoQuery,
60✔
973
      stateTo,
60✔
974
    }
60✔
975
  }
60✔
976

48✔
977
  return this.crudService.changeStateMany(crudContext, parsedAndCastedCommands)
48✔
978
}
48✔
979

94✔
980
async function injectContextInRequest(request) {
2,558✔
981
  const userIdHeader = request.headers[this.userIdHeaderKey]
2,558✔
982
  const isUserHeaderInvalid = INVALID_USERID.includes(userIdHeader)
2,558✔
983

2,558✔
984
  let userId = 'public'
2,558✔
985

2,558✔
986
  if (userIdHeader && !isUserHeaderInvalid) {
2,558✔
987
    userId = userIdHeader
666✔
988
  }
666✔
989

2,558✔
990
  request.crudContext = {
2,558✔
991
    log: request.log,
2,558✔
992
    userId,
2,558✔
993
    now: new Date(),
2,558✔
994
  }
2,558✔
995
}
2,558✔
996

94✔
997
async function parseEncodedJsonQueryParams(logger, request) {
2,558✔
998
  if (request.headers.json_query_params_encoding) {
2,558!
999
    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.')
×
1000
  }
×
1001

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

94✔
1018
async function notFoundHandler(request, reply) {
174✔
1019
  reply
174✔
1020
    .code(404)
174✔
1021
    .send({
174✔
1022
      error: 'not found',
174✔
1023
    })
174✔
1024
}
174✔
1025

94✔
1026
async function customErrorHandler(error, request, reply) {
659✔
1027
  if (error.statusCode === 404) {
659✔
1028
    return notFoundHandler(request, reply)
156✔
1029
  }
156✔
1030

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

395✔
1036
  throw error
395✔
1037
}
659✔
1038

94✔
1039
function resolveMongoQuery(
62,116✔
1040
  queryParser,
62,116✔
1041
  clientQueryString,
62,116✔
1042
  rawAclRows,
62,116✔
1043
  otherParams,
62,116✔
1044
  textQuery
62,116✔
1045
) {
62,116✔
1046
  const mongoQuery = {
62,116✔
1047
    $and: [],
62,116✔
1048
  }
62,116✔
1049

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

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

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

62,095✔
1080
  if (!mongoQuery.$and.length) {
62,116✔
1081
    return {}
61,004✔
1082
  }
61,004✔
1083

1,091✔
1084
  return mongoQuery
1,091✔
1085
}
62,116✔
1086

94✔
1087
function resolveProjection(clientProjectionString, aclColumns, allFieldNames, rawProjection, log) {
1,740✔
1088
  log.debug('Resolving projections')
1,740✔
1089
  const acls = splitACLs(aclColumns)
1,740✔
1090

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

1,722✔
1097
  if (!clientProjectionString && !rawProjection) {
1,740✔
1098
    return removeAclColumns(allFieldNames, acls)
1,165✔
1099
  } else if (rawProjection) {
1,740✔
1100
    return resolveRawProjectionString(rawProjection, acls, allFieldNames, log)
231✔
1101
  } else if (clientProjectionString) {
557✔
1102
    return resolveClientProjectionString(clientProjectionString, acls)
326✔
1103
  }
326✔
1104
}
1,740✔
1105

94✔
1106
function resolveClientProjectionString(clientProjectionString, _acls) {
326✔
1107
  const clientProjection = getClientProjection(clientProjectionString)
326✔
1108
  return removeAclColumns(clientProjection, _acls)
326✔
1109
}
326✔
1110

94✔
1111
function resolveRawProjectionString(rawProjection, _acls, allFieldNames, log) {
231✔
1112
  try {
231✔
1113
    checkAllowedOperators(
231✔
1114
      rawProjection,
231✔
1115
      rawProjectionDictionary,
231✔
1116
      _acls.length > 0 ? _acls : allFieldNames, log)
231✔
1117

231✔
1118
    const rawProjectionObject = resolveRawProjection(rawProjection)
231✔
1119
    const projection = removeAclColumnsFromRawProjection(rawProjectionObject, _acls)
231✔
1120

231✔
1121
    return !lisEmpty(projection) ? [projection] : []
231✔
1122
  } catch (errorMessage) {
231✔
1123
    log.error(errorMessage.message)
66✔
1124
    throw new BadRequestError(errorMessage.message)
66✔
1125
  }
66✔
1126
}
231✔
1127

94✔
1128
function splitACLs(acls) {
1,740✔
1129
  if (acls) { return acls.split(',') }
1,740✔
1130
  return []
1,564✔
1131
}
1,740✔
1132

94✔
1133
function removeAclColumns(fieldsInProjection, aclColumns) {
1,656✔
1134
  if (aclColumns.length > 0) {
1,656✔
1135
    return fieldsInProjection.filter(field => {
170✔
1136
      return aclColumns.indexOf(field) > -1
960✔
1137
    })
170✔
1138
  }
170✔
1139

1,486✔
1140
  return fieldsInProjection
1,486✔
1141
}
1,656✔
1142

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

165✔
1149
  const rawProjectionFields = Object.keys(rawProjectionObject)
165✔
1150
  const filteredFields = removeAclColumns(rawProjectionFields, aclColumns)
165✔
1151

165✔
1152
  return filteredFields.reduce((acc, current) => {
165✔
1153
    if (rawProjectionObject[current] === 0 || rawProjectionObject[current]) {
249✔
1154
      acc[current] = rawProjectionObject[current]
249✔
1155
    }
249✔
1156
    return acc
249✔
1157
  }, {})
165✔
1158
}
171✔
1159

94✔
1160
function getClientProjection(clientProjectionString) {
326✔
1161
  if (clientProjectionString) {
326✔
1162
    return clientProjectionString.split(',')
326✔
1163
  }
326✔
1164
  return []
×
1165
}
326✔
1166

94✔
1167
function resolveRawProjection(clientRawProjectionString) {
171✔
1168
  if (clientRawProjectionString) {
171✔
1169
    return JSON.parse(clientRawProjectionString)
171✔
1170
  }
171✔
1171
  return {}
×
1172
}
171✔
1173

94✔
1174
function checkAllowedOperators(rawProjection, projectionDictionary, additionalFields, log) {
231✔
1175
  if (!rawProjection) {
231!
1176
    log.debug('No raw projection found: checkAllowedOperators returns true')
×
1177
    return true
×
1178
  }
×
1179

231✔
1180
  const { allowedOperators, notAllowedOperators } = projectionDictionary
231✔
1181
  const allowedFields = [...allowedOperators]
231✔
1182

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

231✔
1185
  log.debug({ allowedOperators: allowedFields }, 'Allowed operators for projection')
231✔
1186
  log.debug({ notAllowedOperators }, 'Not allowed operators for projection')
231✔
1187

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

231✔
1192
  if (!matches) {
231✔
1193
    log.debug('No operators found in raw projection: checkAllowedOperators returns true')
93✔
1194
    return true
93✔
1195
  }
93✔
1196

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

48✔
1204
      return notAllowedOperators.includes(match)
48✔
1205
    }
48✔
1206

240✔
1207
    if (!allowedFields.includes(match)) {
336✔
1208
      throw Error(`Operator ${match} is not allowed in raw projection`)
12✔
1209
    }
12✔
1210

228✔
1211
    return !allowedFields.includes(match)
228✔
1212
  })
138✔
1213
}
231✔
1214

94✔
1215
function checkIfRawProjectionOverridesAcls(rawProjection, acls) {
171✔
1216
  return Object.keys(rawProjection).some(field =>
171✔
1217
    acls.includes(field) && rawProjection[field] === 0
351✔
1218
  )
171✔
1219
}
171✔
1220

94✔
1221
function mapToObjectWithOnlyId(doc) {
150,184✔
1222
  return { _id: doc._id.toString() }
150,184✔
1223
}
150,184✔
1224

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

© 2026 Coveralls, Inc