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

mia-platform / crud-service / 7555241289

17 Jan 2024 11:48AM UTC coverage: 96.995% (-0.01%) from 97.009%
7555241289

push

github

danibix95
build(deps): bump the miaplatform group with 1 update

Bumps the miaplatform group with 1 update: [@mia-platform/lc39](https://github.com/mia-platform/lc39).


Updates `@mia-platform/lc39` from 7.0.2 to 7.0.3
- [Release notes](https://github.com/mia-platform/lc39/releases)
- [Changelog](https://github.com/mia-platform/lc39/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mia-platform/lc39/compare/v7.0.2...v7.0.3)

---
updated-dependencies:
- dependency-name: "@mia-platform/lc39"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: miaplatform
...

Signed-off-by: dependabot[bot] <support@github.com>

1709 of 1854 branches covered (0.0%)

Branch coverage included in aggregate %.

8588 of 8762 relevant lines covered (98.01%)

7314.73 hits per line

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

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

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

88✔
19
'use strict'
88✔
20

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

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

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

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

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

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

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

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

88✔
72

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

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

4,128✔
92
  const validateOutput = fastify.validateOutput ?? false
4,128!
93

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

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

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

141,744✔
135
    // this is made to prevent to shows on swagger all properties with dot notation of RawObject with schema.
141,744✔
136
    return ajv.compile(subSchema)
141,744✔
137
  })
4,128✔
138

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

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

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

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

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

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

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

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

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

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

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

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

4,104✔
287
    fastify.log.debug({ collection: fastify?.modelName }, 'getters endpoints registered')
4,104✔
288
  }
4,104✔
289
}
4,128✔
290

88✔
291
// eslint-disable-next-line max-statements
88✔
292
async function handleCollectionImport(request, reply) {
72✔
293
  if (this.customMetrics) {
72!
294
    this.customMetrics.collectionInvocation.inc({
×
295
      collection_name: this.modelName,
×
296
      type: PROMETHEUS_OP_TYPE.IMPORT,
×
297
    })
×
298
  }
×
299

72✔
300
  if (!request.isMultipart()) {
72!
301
    return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, 'Request is not multipart')
×
302
  }
×
303

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

72✔
313
  const { itemValidator, validateImportOptions } = reply.context.config
72✔
314
  const isValid = validateImportOptions(parsingOptions)
72✔
315
  if (!isValid) {
72✔
316
    return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, `Invalid options`)
12✔
317
  }
12✔
318

60✔
319
  const bodyParser = getFileMimeParser(mimetype, parsingOptions)
60✔
320
  if (!bodyParser) {
72!
321
    return reply.getHttpError(UNSUPPORTED_MIME_TYPE_STATUS_CODE, `Unsupported file type ${mimetype}`)
×
322
  }
×
323

60✔
324
  const { crudService, queryParser } = this
60✔
325
  const { log, crudContext } = request
60✔
326

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

60✔
340
  // POST
60✔
341
  let returnCode = 201
60✔
342
  let processBatch = async(batch) => crudService.insertMany(crudContext, batch)
60✔
343

60✔
344
  // PATCH
60✔
345
  if (request.method === 'PATCH') {
66✔
346
    returnCode = 200
39✔
347
    processBatch = async(batch) => {
39✔
348
      return crudService.upsertMany(crudContext, batch)
36✔
349
    }
36✔
350
  }
39✔
351

60✔
352
  const batchConsumer = new BatchWritableStream({
60✔
353
    batchSize: 5000,
60✔
354
    highWaterMark: 1000,
60✔
355
    objectMode: true,
60✔
356
    processBatch,
60✔
357
  })
60✔
358

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

9✔
372
    if (error.code === UNIQUE_INDEX_MONGO_ERROR_CODE) {
9✔
373
      log.debug('unique index violation')
3✔
374
      return reply.getHttpError(UNIQUE_INDEX_ERROR_STATUS_CODE, error.message)
3✔
375
    }
3✔
376

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

×
385
    return reply.getHttpError(INTERNAL_SERVER_ERROR_STATUS_CODE, error.message || 'something went wrong')
9✔
386
  }
9✔
387

51✔
388
  return reply.code(returnCode).send({ message: 'File uploaded successfully' })
51✔
389
}
72✔
390

88✔
391
// eslint-disable-next-line max-statements
88✔
392
async function handleGetListLookup(request, reply) {
40✔
393
  if (this.customMetrics) {
40✔
394
    this.customMetrics.collectionInvocation.inc({
40✔
395
      collection_name: this.modelName,
40✔
396
      type: PROMETHEUS_OP_TYPE.FETCH,
40✔
397
    })
40✔
398
  }
40✔
399

40✔
400
  const { query, headers, crudContext, log } = request
40✔
401

40✔
402
  const {
40✔
403
    [QUERY]: clientQueryString,
40✔
404
    [PROJECTION]: clientProjectionString = '',
40✔
405
    [SORT]: sortQuery,
40✔
406
    [LIMIT]: limit,
40✔
407
    [SKIP]: skip,
40✔
408
    [STATE]: state,
40✔
409
    ...otherParams
40✔
410
  } = query
40✔
411
  const { acl_rows, acl_read_columns } = headers
40✔
412

40✔
413
  let projection = resolveProjection(
40✔
414
    clientProjectionString,
40✔
415
    acl_read_columns,
40✔
416
    this.allFieldNames,
40✔
417
    '',
40✔
418
    log
40✔
419
  )
40✔
420

40✔
421
  projection = this.lookupProjection.filter(proj => projection.includes(Object.keys(proj)[0]))
40✔
422
  if (projection.length === 0) {
40✔
423
    reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, 'No allowed colums')
2✔
424
  }
2✔
425

40✔
426
  const LookupProjectionFieldsToOmit = this.lookupProjection.filter(field => Object.values(field).shift() === 0)
40✔
427
  projection.push(...LookupProjectionFieldsToOmit)
40✔
428

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

40✔
437
  const stateArr = state?.split(',')
40✔
438
  const { replyType, streamValidator } = reply.context.config
40✔
439
  const contentType = replyType()
40✔
440
  const responseParser = getFileMimeStringify(contentType)
40✔
441
  if (!responseParser) {
40!
442
    return reply.getHttpError(UNSUPPORTED_MIME_TYPE_STATUS_CODE, `Unsupported file type ${contentType}`)
×
443
  }
×
444

40✔
445
  reply.raw.setHeader('Content-Type', contentType)
40✔
446

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

88✔
466
async function handleGetList(request, reply) {
1,183✔
467
  if (this.customMetrics) {
1,183✔
468
    this.customMetrics.collectionInvocation.inc({
67✔
469
      collection_name: this.modelName,
67✔
470
      type: PROMETHEUS_OP_TYPE.FETCH,
67✔
471
    })
67✔
472
  }
67✔
473

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

1,183✔
489
  const responseParser = getFileMimeStringify(contentType, {})
1,183✔
490
  if (!responseParser) {
1,183✔
491
    return reply.getHttpError(NOT_ACCEPTABLE, `unsupported file type ${contentType}`)
156✔
492
  }
156✔
493

1,027✔
494
  const projection = resolveProjection(
1,027✔
495
    clientProjectionString,
1,027✔
496
    acl_read_columns,
1,027✔
497
    this.allFieldNames,
1,027✔
498
    clientRawProjectionString,
1,027✔
499
    log
1,027✔
500
  )
1,027✔
501

1,027✔
502
  const isTextSearchQuery = query._q && this.queryParser.isTextSearchQuery(JSON.parse(query._q))
1,183✔
503
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, isTextSearchQuery)
1,183✔
504

1,183✔
505
  let sort
1,183✔
506
  if (sortQuery) {
1,183✔
507
    sort = Object.fromEntries(sortQuery.toString().split(',')
122✔
508
      .map((param) => (param[0] === '-' ? [param.substr(1), -1] : [param, 1])))
122✔
509
  }
122✔
510

961✔
511
  const stateArr = state.split(',')
961✔
512

961✔
513
  reply.raw.setHeader('Content-Type', contentType)
961✔
514

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

88✔
534
async function handleGetId(request, reply) {
372✔
535
  if (this.customMetrics) {
372!
536
    this.customMetrics.collectionInvocation.inc({
×
537
      collection_name: this.modelName,
×
538
      type: PROMETHEUS_OP_TYPE.FETCH,
×
539
    })
×
540
  }
×
541

372✔
542
  const { crudContext, log } = request
372✔
543
  const docId = request.params.id
372✔
544
  const { acl_rows, acl_read_columns } = request.headers
372✔
545
  const {
372✔
546
    itemValidator,
372✔
547
  } = reply.context.config
372✔
548

372✔
549
  const {
372✔
550
    [QUERY]: clientQueryString,
372✔
551
    [PROJECTION]: clientProjectionString = '',
372✔
552
    [RAW_PROJECTION]: clientRawProjectionString = '',
372✔
553
    [STATE]: state,
372✔
554
    ...otherParams
372✔
555
  } = request.query
372✔
556

372✔
557
  const projection = resolveProjection(
372✔
558
    clientProjectionString,
372✔
559
    acl_read_columns,
372✔
560
    this.allFieldNames,
372✔
561
    clientRawProjectionString,
372✔
562
    log
372✔
563
  )
372✔
564
  const filter = resolveMongoQuery(
372✔
565
    this.queryParser,
372✔
566
    clientQueryString,
372✔
567
    acl_rows,
372✔
568
    otherParams,
372✔
569
    false
372✔
570
  )
372✔
571
  const _id = this.castCollectionId(docId)
372✔
572

372✔
573
  const stateArr = state.split(',')
372✔
574
  const doc = await this.crudService.findById(crudContext, _id, filter, projection, stateArr)
372✔
575
  if (!doc) {
372✔
576
    return reply.notFound()
78✔
577
  }
78✔
578

273✔
579
  const response = this.castItem(doc)
273✔
580
  itemValidator(response)
273✔
581
  return response
273✔
582
}
372✔
583

88✔
584
async function handleInsertOne(request, reply) {
76✔
585
  if (this.customMetrics) {
76✔
586
    this.customMetrics.collectionInvocation.inc({
13✔
587
      collection_name: this.modelName,
13✔
588
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
13✔
589
    })
13✔
590
  }
13✔
591

76✔
592
  const { body: doc, crudContext } = request
76✔
593

76✔
594
  this.queryParser.parseAndCastBody(doc)
76✔
595

76✔
596
  try {
76✔
597
    const insertedDoc = await this.crudService.insertOne(crudContext, doc)
76✔
598
    return mapToObjectWithOnlyId(insertedDoc)
73✔
599
  } catch (error) {
76✔
600
    if (error.code === UNIQUE_INDEX_MONGO_ERROR_CODE) {
3✔
601
      request.log.error('unique index violation')
3✔
602
      return reply.getHttpError(UNIQUE_INDEX_ERROR_STATUS_CODE, error.message)
3✔
603
    }
3✔
604
    throw error
×
605
  }
×
606
}
76✔
607

88✔
608
async function handleValidate() {
3✔
609
  return { result: 'ok' }
3✔
610
}
3✔
611

88✔
612
async function handleDeleteId(request, reply) {
56✔
613
  if (this.customMetrics) {
56!
614
    this.customMetrics.collectionInvocation.inc({
2✔
615
      collection_name: this.modelName,
2✔
616
      type: PROMETHEUS_OP_TYPE.DELETE,
2✔
617
    })
2✔
618
  }
2✔
619

56✔
620
  const { query, headers, params, crudContext } = request
56✔
621

56✔
622
  const docId = params.id
56✔
623
  const _id = this.castCollectionId(docId)
56✔
624

56✔
625
  const {
56✔
626
    [QUERY]: clientQueryString,
56✔
627
    [STATE]: state,
56✔
628
    ...otherParams
56✔
629
  } = query
56✔
630
  const { acl_rows } = headers
56✔
631

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

56✔
634
  const stateArr = state.split(',')
56✔
635
  const doc = await this.crudService.deleteById(crudContext, _id, filter, stateArr)
56✔
636

53✔
637
  if (!doc) {
56✔
638
    return reply.notFound()
18✔
639
  }
18✔
640

35✔
641
  // the document should not be returned:
35✔
642
  // we don't know which projection the user is able to see
35✔
643
  reply.code(204)
35✔
644
}
56✔
645

88✔
646
async function handleDeleteList(request) {
41✔
647
  if (this.customMetrics) {
41!
648
    this.customMetrics.collectionInvocation.inc({
2✔
649
      collection_name: this.modelName,
2✔
650
      type: PROMETHEUS_OP_TYPE.DELETE,
2✔
651
    })
2✔
652
  }
2✔
653

41✔
654
  const { query, headers, crudContext } = request
41✔
655

41✔
656
  const {
41✔
657
    [QUERY]: clientQueryString,
41✔
658
    [STATE]: state,
41✔
659
    ...otherParams
41✔
660
  } = query
41✔
661
  const { acl_rows } = headers
41✔
662

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

41✔
665
  const stateArr = state.split(',')
41✔
666
  return this.crudService.deleteAll(crudContext, filter, stateArr)
41✔
667
}
41✔
668

88✔
669
async function handleCount(request) {
45✔
670
  if (this.customMetrics) {
45✔
671
    this.customMetrics.collectionInvocation.inc({
3✔
672
      collection_name: this.modelName,
3✔
673
      type: PROMETHEUS_OP_TYPE.FETCH,
3✔
674
    })
3✔
675
  }
3✔
676

45✔
677
  const { query, headers, crudContext } = request
45✔
678
  const {
45✔
679
    [QUERY]: clientQueryString,
45✔
680
    [STATE]: state,
45✔
681
    ...otherParams
45✔
682
  } = query
45✔
683
  const { acl_rows } = headers
45✔
684
  const stateArr = state.split(',')
45✔
685

45✔
686
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
45✔
687
  return this.crudService.count(crudContext, mongoQuery, stateArr)
45✔
688
}
45✔
689

88✔
690
async function handlePatchId(request, reply) {
226✔
691
  if (this.customMetrics) {
226!
692
    this.customMetrics.collectionInvocation.inc({
4✔
693
      collection_name: this.modelName,
4✔
694
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
4✔
695
    })
4✔
696
  }
4✔
697

226✔
698
  const { query, headers, params, crudContext, log } = request
226✔
699
  const {
226✔
700
    [QUERY]: clientQueryString,
226✔
701
    [STATE]: state,
226✔
702
    ...otherParams
226✔
703
  } = query
226✔
704
  const {
226✔
705
    acl_rows,
226✔
706
    acl_write_columns: aclWriteColumns,
226✔
707
    acl_read_columns: aclColumns = '',
226✔
708
  } = headers
226✔
709
  const {
226✔
710
    itemValidator,
226✔
711
  } = reply.context.config
226✔
712

226✔
713
  const commands = request.body
226✔
714

226✔
715
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
226✔
716

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

226✔
719
  this.queryParser.parseAndCastCommands(commands, editableFields)
226✔
720
  const projection = resolveProjection('', aclColumns, this.allFieldNames, '', log)
226✔
721

226✔
722
  const docId = params.id
226✔
723
  const _id = this.castCollectionId(docId)
226✔
724

226✔
725
  const stateArr = state.split(',')
226✔
726
  const doc = await this.crudService.patchById(crudContext, _id, commands, mongoQuery, projection, stateArr)
226✔
727

220✔
728
  if (!doc) {
226✔
729
    return reply.notFound()
51✔
730
  }
51✔
731

169✔
732
  const response = this.castItem(doc)
169✔
733
  itemValidator(response)
169✔
734
  return response
169✔
735
}
226✔
736

88✔
737
async function handlePatchMany(request) {
90✔
738
  if (this.customMetrics) {
90!
739
    this.customMetrics.collectionInvocation.inc({
×
740
      collection_name: this.modelName,
×
741
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
742
    })
×
743
  }
×
744

90✔
745
  const { query, headers, crudContext } = request
90✔
746
  const {
90✔
747
    [QUERY]: clientQueryString,
90✔
748
    [STATE]: state,
90✔
749
    ...otherParams
90✔
750
  } = query
90✔
751
  const {
90✔
752
    acl_rows,
90✔
753
    acl_write_columns: aclWriteColumns,
90✔
754
  } = headers
90✔
755

90✔
756
  const commands = request.body
90✔
757
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
90✔
758
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
90✔
759
  this.queryParser.parseAndCastCommands(commands, editableFields)
90✔
760

90✔
761
  const stateArr = state.split(',')
90✔
762
  const nModified = await this.crudService.patchMany(crudContext, commands, mongoQuery, stateArr)
90✔
763

84✔
764
  return nModified
84✔
765
}
90✔
766

88✔
767
async function handleUpsertOne(request, reply) {
66✔
768
  if (this.customMetrics) {
66!
769
    this.customMetrics.collectionInvocation.inc({
×
770
      collection_name: this.modelName,
×
771
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
772
    })
×
773
  }
×
774

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

66✔
790
  const commands = request.body
66✔
791

66✔
792
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
66✔
793
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
66✔
794

66✔
795
  this.queryParser.parseAndCastCommands(commands, editableFields)
66✔
796
  const projection = resolveProjection('', aclColumns, this.allFieldNames, '', log)
66✔
797

66✔
798
  const stateArr = state.split(',')
66✔
799
  const doc = await this.crudService.upsertOne(crudContext, commands, mongoQuery, projection, stateArr)
66✔
800

66✔
801
  const response = this.castItem(doc)
66✔
802

66✔
803
  itemValidator(response)
66✔
804
  return response
66✔
805
}
66✔
806

88✔
807
async function handlePatchBulk(request) {
90✔
808
  if (this.customMetrics) {
90!
809
    this.customMetrics.collectionInvocation.inc({
×
810
      collection_name: this.modelName,
×
811
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
812
    })
×
813
  }
×
814

90✔
815
  const { body: filterUpdateCommands, crudContext, headers } = request
90✔
816

90✔
817
  const {
90✔
818
    acl_rows,
90✔
819
    acl_write_columns: aclWriteColumns,
90✔
820
  } = headers
90✔
821

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

60,114✔
832
    const commands = update
60,114✔
833

60,114✔
834
    const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
60,114✔
835

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

60,114✔
838
    this.queryParser.parseAndCastCommands(commands, editableFields)
60,114✔
839

60,114✔
840
    parsedAndCastedCommands[i] = {
60,114✔
841
      commands,
60,114✔
842
      state: state.split(','),
60,114✔
843
      query: mongoQuery,
60,114✔
844
    }
60,114✔
845
    if (_id) {
60,114✔
846
      parsedAndCastedCommands[i].query._id = this.castCollectionId(_id)
60,105✔
847
    }
60,105✔
848
  }
60,114✔
849

90✔
850
  const nModified = await this.crudService.patchBulk(crudContext, parsedAndCastedCommands)
90✔
851
  return nModified
90✔
852
}
90✔
853

88✔
854
async function handleInsertMany(request, reply) {
60✔
855
  if (this.customMetrics) {
60!
856
    this.customMetrics.collectionInvocation.inc({
×
857
      collection_name: this.modelName,
×
858
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
859
    })
×
860
  }
×
861

60✔
862
  const { body: docs, crudContext } = request
60✔
863

60✔
864
  docs.forEach(this.queryParser.parseAndCastBody)
60✔
865

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

88✔
878
async function handleChangeStateById(request, reply) {
51✔
879
  if (this.customMetrics) {
51!
880
    this.customMetrics.collectionInvocation.inc({
×
881
      collection_name: this.modelName,
×
882
      type: PROMETHEUS_OP_TYPE.CHANGE_STATE,
×
883
    })
×
884
  }
×
885

51✔
886
  const { body, crudContext, headers, query } = request
51✔
887
  const {
51✔
888
    [QUERY]: clientQueryString,
51✔
889
    ...otherParams
51✔
890
  } = query
51✔
891

51✔
892
  const { acl_rows } = headers
51✔
893
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
51✔
894

51✔
895
  const docId = request.params.id
51✔
896
  const _id = this.castCollectionId(docId)
51✔
897

51✔
898
  try {
51✔
899
    const doc = await this.crudService.changeStateById(crudContext, _id, body.stateTo, mongoQuery)
51✔
900
    if (!doc) {
51✔
901
      return reply.notFound()
9✔
902
    }
9✔
903

24✔
904
    reply.code(204)
24✔
905
  } catch (error) {
51✔
906
    if (error.statusCode) {
15✔
907
      return reply.getHttpError(error.statusCode, error.message)
15✔
908
    }
15✔
909

×
910
    throw error
×
911
  }
×
912
}
51✔
913

88✔
914
async function handleChangeStateMany(request) {
48✔
915
  if (this.customMetrics) {
48!
916
    this.customMetrics.collectionInvocation.inc({
×
917
      collection_name: this.modelName,
×
918
      type: PROMETHEUS_OP_TYPE.CHANGE_STATE,
×
919
    })
×
920
  }
×
921

48✔
922
  const { body: filterUpdateCommands, crudContext, headers } = request
48✔
923

48✔
924
  const {
48✔
925
    acl_rows,
48✔
926
  } = headers
48✔
927

48✔
928
  const parsedAndCastedCommands = new Array(filterUpdateCommands.length)
48✔
929
  for (let i = 0; i < filterUpdateCommands.length; i++) {
48✔
930
    const {
60✔
931
      filter,
60✔
932
      stateTo,
60✔
933
    } = filterUpdateCommands[i]
60✔
934

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

60✔
937
    parsedAndCastedCommands[i] = {
60✔
938
      query: mongoQuery,
60✔
939
      stateTo,
60✔
940
    }
60✔
941
  }
60✔
942

48✔
943
  return this.crudService.changeStateMany(crudContext, parsedAndCastedCommands)
48✔
944
}
48✔
945

88✔
946
async function injectContextInRequest(request) {
2,537✔
947
  const userIdHeader = request.headers[this.userIdHeaderKey]
2,537✔
948
  const isUserHeaderInvalid = INVALID_USERID.includes(userIdHeader)
2,537✔
949

2,537✔
950
  let userId = 'public'
2,537✔
951

2,537✔
952
  if (userIdHeader && !isUserHeaderInvalid) {
2,537✔
953
    userId = userIdHeader
666✔
954
  }
666✔
955

2,537✔
956
  request.crudContext = {
2,537✔
957
    log: request.log,
2,537✔
958
    userId,
2,537✔
959
    now: new Date(),
2,537✔
960
  }
2,537✔
961
}
2,537✔
962

88✔
963
async function parseEncodedJsonQueryParams(logger, request) {
2,537✔
964
  if (request.headers.json_query_params_encoding) {
2,537!
965
    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.')
×
966
  }
×
967

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

88✔
984
async function notFoundHandler(request, reply) {
174✔
985
  reply
174✔
986
    .code(404)
174✔
987
    .send({
174✔
988
      error: 'not found',
174✔
989
    })
174✔
990
}
174✔
991

88✔
992
async function customErrorHandler(error, request, reply) {
659✔
993
  if (error.statusCode === 404) {
659✔
994
    return notFoundHandler(request, reply)
156✔
995
  }
156✔
996

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

395✔
1002
  throw error
395✔
1003
}
659✔
1004

88✔
1005
function resolveMongoQuery(
62,104✔
1006
  queryParser,
62,104✔
1007
  clientQueryString,
62,104✔
1008
  rawAclRows,
62,104✔
1009
  otherParams,
62,104✔
1010
  textQuery
62,104✔
1011
) {
62,104✔
1012
  const mongoQuery = {
62,104✔
1013
    $and: [],
62,104✔
1014
  }
62,104✔
1015

62,104✔
1016
  if (clientQueryString) {
62,104✔
1017
    const clientQuery = JSON.parse(clientQueryString)
651✔
1018
    mongoQuery.$and.push(clientQuery)
651✔
1019
  }
651✔
1020
  if (otherParams) {
62,104✔
1021
    for (const key of Object.keys(otherParams)) {
62,104✔
1022
      const value = otherParams[key]
525✔
1023
      mongoQuery.$and.push({ [key]: value })
525✔
1024
    }
525✔
1025
  }
62,104✔
1026

62,104✔
1027
  if (rawAclRows) {
62,104✔
1028
    const aclRows = JSON.parse(rawAclRows)
300✔
1029
    if (rawAclRows[0] === '[') {
300✔
1030
      mongoQuery.$and.push({ $and: aclRows })
288✔
1031
    } else {
300✔
1032
      mongoQuery.$and.push(aclRows)
12✔
1033
    }
12✔
1034
  }
300✔
1035

62,104✔
1036
  try {
62,104✔
1037
    if (textQuery) {
62,104✔
1038
      queryParser.parseAndCastTextSearchQuery(mongoQuery)
72✔
1039
    } else {
62,104✔
1040
      queryParser.parseAndCast(mongoQuery)
62,032✔
1041
    }
62,032✔
1042
  } catch (error) {
62,104✔
1043
    throw new BadRequestError(error.message)
21✔
1044
  }
21✔
1045

62,083✔
1046
  if (!mongoQuery.$and.length) {
62,104✔
1047
    return {}
61,004✔
1048
  }
61,004✔
1049

1,079✔
1050
  return mongoQuery
1,079✔
1051
}
62,104✔
1052

88✔
1053
function resolveProjection(clientProjectionString, aclColumns, allFieldNames, rawProjection, log) {
1,728✔
1054
  log.debug('Resolving projections')
1,728✔
1055
  const acls = splitACLs(aclColumns)
1,728✔
1056

1,728✔
1057
  if (clientProjectionString && rawProjection) {
1,728✔
1058
    log.error('Use of both _p and _rawp is not permitted')
18✔
1059
    throw new BadRequestError(
18✔
1060
      'Use of both _rawp and _p parameter is not allowed')
18✔
1061
  }
18✔
1062

1,710✔
1063
  if (!clientProjectionString && !rawProjection) {
1,728✔
1064
    return removeAclColumns(allFieldNames, acls)
1,153✔
1065
  } else if (rawProjection) {
1,728✔
1066
    return resolveRawProjectionString(rawProjection, acls, allFieldNames, log)
231✔
1067
  } else if (clientProjectionString) {
557✔
1068
    return resolveClientProjectionString(clientProjectionString, acls)
326✔
1069
  }
326✔
1070
}
1,728✔
1071

88✔
1072
function resolveClientProjectionString(clientProjectionString, _acls) {
326✔
1073
  const clientProjection = getClientProjection(clientProjectionString)
326✔
1074
  return removeAclColumns(clientProjection, _acls)
326✔
1075
}
326✔
1076

88✔
1077
function resolveRawProjectionString(rawProjection, _acls, allFieldNames, log) {
231✔
1078
  try {
231✔
1079
    checkAllowedOperators(
231✔
1080
      rawProjection,
231✔
1081
      rawProjectionDictionary,
231✔
1082
      _acls.length > 0 ? _acls : allFieldNames, log)
231✔
1083

231✔
1084
    const rawProjectionObject = resolveRawProjection(rawProjection)
231✔
1085
    const projection = removeAclColumnsFromRawProjection(rawProjectionObject, _acls)
231✔
1086

231✔
1087
    return !lisEmpty(projection) ? [projection] : []
231✔
1088
  } catch (errorMessage) {
231✔
1089
    log.error(errorMessage.message)
66✔
1090
    throw new BadRequestError(errorMessage.message)
66✔
1091
  }
66✔
1092
}
231✔
1093

88✔
1094
function splitACLs(acls) {
1,728✔
1095
  if (acls) { return acls.split(',') }
1,728✔
1096
  return []
1,552✔
1097
}
1,728✔
1098

88✔
1099
function removeAclColumns(fieldsInProjection, aclColumns) {
1,644✔
1100
  if (aclColumns.length > 0) {
1,644✔
1101
    return fieldsInProjection.filter(field => {
170✔
1102
      return aclColumns.indexOf(field) > -1
960✔
1103
    })
170✔
1104
  }
170✔
1105

1,474✔
1106
  return fieldsInProjection
1,474✔
1107
}
1,644✔
1108

88✔
1109
function removeAclColumnsFromRawProjection(rawProjectionObject, aclColumns) {
171✔
1110
  const isRawProjectionOverridingACLs = checkIfRawProjectionOverridesAcls(rawProjectionObject, aclColumns)
171✔
1111
  if (isRawProjectionOverridingACLs) {
171✔
1112
    throw Error('_rawp exclusive projection is overriding at least one acl_read_column value')
6✔
1113
  }
6✔
1114

165✔
1115
  const rawProjectionFields = Object.keys(rawProjectionObject)
165✔
1116
  const filteredFields = removeAclColumns(rawProjectionFields, aclColumns)
165✔
1117

165✔
1118
  return filteredFields.reduce((acc, current) => {
165✔
1119
    if (rawProjectionObject[current] === 0 || rawProjectionObject[current]) {
249✔
1120
      acc[current] = rawProjectionObject[current]
249✔
1121
    }
249✔
1122
    return acc
249✔
1123
  }, {})
165✔
1124
}
171✔
1125

88✔
1126
function getClientProjection(clientProjectionString) {
326✔
1127
  if (clientProjectionString) {
326✔
1128
    return clientProjectionString.split(',')
326✔
1129
  }
326✔
1130
  return []
×
1131
}
326✔
1132

88✔
1133
function resolveRawProjection(clientRawProjectionString) {
171✔
1134
  if (clientRawProjectionString) {
171✔
1135
    return JSON.parse(clientRawProjectionString)
171✔
1136
  }
171✔
1137
  return {}
×
1138
}
171✔
1139

88✔
1140
function checkAllowedOperators(rawProjection, projectionDictionary, additionalFields, log) {
231✔
1141
  if (!rawProjection) {
231!
1142
    log.debug('No raw projection found: checkAllowedOperators returns true')
×
1143
    return true
×
1144
  }
×
1145

231✔
1146
  const { allowedOperators, notAllowedOperators } = projectionDictionary
231✔
1147
  const allowedFields = [...allowedOperators]
231✔
1148

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

231✔
1151
  log.debug({ allowedOperators: allowedFields }, 'Allowed operators for projection')
231✔
1152
  log.debug({ notAllowedOperators }, 'Not allowed operators for projection')
231✔
1153

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

231✔
1158
  if (!matches) {
231✔
1159
    log.debug('No operators found in raw projection: checkAllowedOperators returns true')
93✔
1160
    return true
93✔
1161
  }
93✔
1162

138✔
1163
  return !matches.some(match => {
138✔
1164
    if (match.startsWith('$$')) {
336✔
1165
      log.debug({ match }, 'Found $$ match in raw projection')
96✔
1166
      if (notAllowedOperators.includes(match)) {
96✔
1167
        throw Error(`Operator ${match} is not allowed in raw projection`)
48✔
1168
      }
48✔
1169

48✔
1170
      return notAllowedOperators.includes(match)
48✔
1171
    }
48✔
1172

240✔
1173
    if (!allowedFields.includes(match)) {
336✔
1174
      throw Error(`Operator ${match} is not allowed in raw projection`)
12✔
1175
    }
12✔
1176

228✔
1177
    return !allowedFields.includes(match)
228✔
1178
  })
138✔
1179
}
231✔
1180

88✔
1181
function checkIfRawProjectionOverridesAcls(rawProjection, acls) {
171✔
1182
  return Object.keys(rawProjection).some(field =>
171✔
1183
    acls.includes(field) && rawProjection[field] === 0
351✔
1184
  )
171✔
1185
}
171✔
1186

88✔
1187
function mapToObjectWithOnlyId(doc) {
150,184✔
1188
  return { _id: doc._id.toString() }
150,184✔
1189
}
150,184✔
1190

88✔
1191
const internalFields = [
88✔
1192
  UPDATERID,
88✔
1193
  UPDATEDAT,
88✔
1194
  CREATORID,
88✔
1195
  CREATEDAT,
88✔
1196
  __STATE__,
88✔
1197
]
88✔
1198
function getEditableFields(aclWriteColumns, allFieldNames) {
60,496✔
1199
  const editableFields = aclWriteColumns ? aclWriteColumns.split(',') : allFieldNames
60,496!
1200
  return editableFields.filter(ef => !internalFields.includes(ef))
60,496✔
1201
}
60,496✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc