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

mia-platform / crud-service / 8941506258

03 May 2024 03:45PM UTC coverage: 97.071% (-0.06%) from 97.128%
8941506258

push

github

web-flow
fix: resolve export bugs (#300)

* Fix xls export

* Refactor resolveProjection in httpInterface

* Fix export

* Fix PR and changelog

* Fix changelog

---------

Co-authored-by: Paola Nicosia <paola.nicosia@mia-platform.eu>

1851 of 2003 branches covered (92.41%)

Branch coverage included in aggregate %.

39 of 60 new or added lines in 3 files covered. (65.0%)

4 existing lines in 1 file now uncovered.

9086 of 9264 relevant lines covered (98.08%)

7214.43 hits per line

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

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

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

94✔
19
'use strict'
94✔
20

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

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

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

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

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

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

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

94✔
75

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

94✔
412
function getExportColumns(projection) {
1,196✔
413
  const columns = Object.keys(projection).filter(key => projection[key] !== 0)
1,196✔
414
  if (!columns.includes('_id') && projection['_id'] !== 0) {
1,196✔
415
    columns.unshift('_id')
375✔
416
  }
375✔
417
  return columns
1,196✔
418
}
1,196✔
419

94✔
420
// eslint-disable-next-line max-statements
94✔
421
async function handleGetListLookup(request, reply) {
40✔
422
  if (this.customMetrics) {
40✔
423
    this.customMetrics.collectionInvocation.inc({
40✔
424
      collection_name: this.modelName,
40✔
425
      type: PROMETHEUS_OP_TYPE.FETCH,
40✔
426
    })
40✔
427
  }
40✔
428

40✔
429
  const {
40✔
430
    query,
40✔
431
    headers,
40✔
432
    crudContext,
40✔
433
    log,
40✔
434
    routeOptions: { config: { replyType, streamValidator } },
40✔
435
  } = request
40✔
436

40✔
437
  const {
40✔
438
    [QUERY]: clientQueryString,
40✔
439
    [PROJECTION]: clientProjectionString = '',
40✔
440
    [SORT]: sortQuery,
40✔
441
    [LIMIT]: limit,
40✔
442
    [SKIP]: skip,
40✔
443
    [STATE]: state,
40✔
444
    [EXPORT_OPTIONS]: exportOpts = '',
40✔
445
    ...otherParams
40✔
446
  } = query
40✔
447
  const { acl_rows, acl_read_columns } = headers
40✔
448

40✔
449
  let projection = resolveProjection(
40✔
450
    clientProjectionString,
40✔
451
    acl_read_columns,
40✔
452
    this.allFieldNames,
40✔
453
    '',
40✔
454
    log
40✔
455
  )
40✔
NEW
456
  delete projection._id
40✔
457

40✔
NEW
458
  projection = this.lookupProjection.reduce((acc, proj) => {
40✔
NEW
459
    if (projection[Object.keys(proj)[0]]) {
120✔
NEW
460
      return { ...acc, ...proj }
68✔
NEW
461
    }
68✔
NEW
462
    return acc
52✔
NEW
463
  }, {})
40✔
NEW
464
  if (Object.keys(projection).length === 0) {
40✔
465
    reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, 'No allowed colums')
2✔
466
  }
2✔
467

40✔
NEW
468
  const lookupProjectionFieldsToOmit = this.lookupProjection.reduce((acc, field) => {
40✔
NEW
469
    if (Object.values(field).shift() === 0) {
120✔
NEW
470
      return { ...acc, ...field }
40✔
NEW
471
    }
40✔
NEW
472
    return acc
80✔
NEW
473
  },
40✔
NEW
474
  {})
40✔
NEW
475
  projection = {
40✔
NEW
476
    ...projection,
40✔
NEW
477
    ...lookupProjectionFieldsToOmit,
40✔
NEW
478
  }
40✔
479

40✔
480
  const isTextSearchQuery = query._q && this.queryParser.isTextSearchQuery(JSON.parse(query._q))
40✔
481
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, isTextSearchQuery)
40✔
482
  let sort
40✔
483
  if (sortQuery) {
40✔
484
    sort = Object.fromEntries(sortQuery.toString().split(',')
4✔
485
      .map((param) => (param[0] === '-' ? [param.substr(1), -1] : [param, 1])))
4✔
486
  }
4✔
487

40✔
488
  const stateArr = state?.split(',')
40✔
489
  const contentType = replyType()
40✔
490
  const parsingOptions = contentType === 'text/csv' && exportOpts ? JSON.parse(exportOpts) : {}
40!
491

40✔
492
  const responseStringifiers = getFileMimeStringifiers(contentType, parsingOptions)
40✔
493
  if (!responseStringifiers) {
40!
494
    return reply.getHttpError(UNSUPPORTED_MIME_TYPE_STATUS_CODE, `Unsupported file type ${contentType}`)
×
495
  }
×
496

40✔
497
  reply.raw.setHeader('Content-Type', contentType)
40✔
498

40✔
499
  try {
40✔
500
    return await pipeline(
40✔
501
      this.crudService
40✔
502
        .aggregate(crudContext, mongoQuery, projection, sort, skip, limit, stateArr, isTextSearchQuery)
40✔
503
        .stream(),
40✔
504
      this.castResultsAsStream(),
40✔
505
      streamValidator(),
40✔
NEW
506
      ...responseStringifiers({ fields: getExportColumns(projection) }),
40✔
507
      reply.raw
40✔
508
    )
40✔
509
  } catch (error) {
40✔
510
    request.log.error({ error }, 'Error during findAll lookup stream')
2✔
511
    request.log.debug({ error: { ...error, message: error.message } }, 'Error during findAll lookup stream with message')
2✔
512
    if (error.code === OPTIONS_INCOMPATIBILITY_ERROR_CODE) {
2!
513
      request.log.info(BAD_REQUEST_ERROR_STATUS_CODE)
×
514
    }
×
515
  }
2✔
516
}
40✔
517

94✔
518
// eslint-disable-next-line max-statements
94✔
519
async function handleGetList(request, reply) {
1,384✔
520
  if (this.customMetrics) {
1,384✔
521
    this.customMetrics.collectionInvocation.inc({
67✔
522
      collection_name: this.modelName,
67✔
523
      type: PROMETHEUS_OP_TYPE.FETCH,
67✔
524
    })
67✔
525
  }
67✔
526

1,384✔
527
  const {
1,384✔
528
    query,
1,384✔
529
    headers,
1,384✔
530
    crudContext,
1,384✔
531
    log,
1,384✔
532
    routeOptions: { config: { replyType, streamValidator } },
1,384✔
533
  } = request
1,384✔
534
  const {
1,384✔
535
    [QUERY]: clientQueryString,
1,384✔
536
    [PROJECTION]: clientProjectionString = '',
1,384✔
537
    [RAW_PROJECTION]: clientRawProjectionString = '',
1,384✔
538
    [SORT]: sortQuery,
1,384✔
539
    [LIMIT]: limit,
1,384✔
540
    [SKIP]: skip,
1,384✔
541
    [STATE]: state,
1,384✔
542
    [EXPORT_OPTIONS]: exportOpts = '',
1,384✔
543
    ...otherParams
1,384✔
544
  } = query
1,384✔
545
  const { acl_rows, acl_read_columns, accept } = headers
1,384✔
546
  const contentType = replyType(accept)
1,384✔
547
  const parsingOptions = contentType === 'text/csv' && exportOpts ? JSON.parse(exportOpts) : {}
1,384✔
548

1,384✔
549
  const responseStringifiers = getFileMimeStringifiers(contentType, parsingOptions)
1,384✔
550
  if (!responseStringifiers) {
1,384✔
551
    return reply.getHttpError(NOT_ACCEPTABLE, `unsupported file type ${contentType}`)
162✔
552
  }
162✔
553

1,222✔
554
  const projection = resolveProjection(
1,222✔
555
    clientProjectionString,
1,222✔
556
    acl_read_columns,
1,222✔
557
    this.allFieldNames,
1,222✔
558
    clientRawProjectionString,
1,222✔
559
    log
1,222✔
560
  )
1,222✔
561

1,222✔
562
  const isTextSearchQuery = query._q && this.queryParser.isTextSearchQuery(JSON.parse(query._q))
1,384✔
563
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, isTextSearchQuery)
1,384✔
564

1,384✔
565
  let sort
1,384✔
566
  if (sortQuery) {
1,384✔
567
    sort = Object.fromEntries(sortQuery.toString().split(',')
146✔
568
      .map((param) => (param[0] === '-' ? [param.substr(1), -1] : [param, 1])))
146✔
569
  }
146✔
570

1,156✔
571
  const stateArr = state.split(',')
1,156✔
572

1,156✔
573
  reply.raw.setHeader('Content-Type', contentType)
1,156✔
574

1,156✔
575
  try {
1,156✔
576
    await pipeline(
1,156✔
577
      this.crudService
1,156✔
578
        .findAll(crudContext, mongoQuery, projection, sort, skip, limit, stateArr, isTextSearchQuery)
1,156✔
579
        .stream(),
1,156✔
580
      this.castResultsAsStream(),
1,156✔
581
      streamValidator(),
1,156✔
582
      ...responseStringifiers({ fields: getExportColumns(projection) }),
1,156✔
583
      reply.raw
1,156✔
584
    )
1,156✔
585
  } catch (error) {
1,384✔
586
    request.log.error({ error }, 'Error during findAll stream')
3✔
587
    request.log.debug({ error: { ...error, message: error.message } }, 'Error during findAll stream with message')
3✔
588
    if (error.code === OPTIONS_INCOMPATIBILITY_ERROR_CODE) {
3!
589
      request.log.info(BAD_REQUEST_ERROR_STATUS_CODE)
×
590
    }
×
591
  }
3✔
592
}
1,384✔
593

94✔
594
async function handleGetId(request, reply) {
375✔
595
  if (this.customMetrics) {
375!
596
    this.customMetrics.collectionInvocation.inc({
×
597
      collection_name: this.modelName,
×
598
      type: PROMETHEUS_OP_TYPE.FETCH,
×
599
    })
×
600
  }
×
601

375✔
602
  const {
375✔
603
    crudContext,
375✔
604
    log,
375✔
605
    routeOptions: { config: { itemValidator } },
375✔
606
  } = request
375✔
607
  const docId = request.params.id
375✔
608
  const { acl_rows, acl_read_columns } = request.headers
375✔
609

375✔
610
  const {
375✔
611
    [QUERY]: clientQueryString,
375✔
612
    [PROJECTION]: clientProjectionString = '',
375✔
613
    [RAW_PROJECTION]: clientRawProjectionString = '',
375✔
614
    [STATE]: state,
375✔
615
    ...otherParams
375✔
616
  } = request.query
375✔
617

375✔
618
  const projection = resolveProjection(
375✔
619
    clientProjectionString,
375✔
620
    acl_read_columns,
375✔
621
    this.allFieldNames,
375✔
622
    clientRawProjectionString,
375✔
623
    log
375✔
624
  )
375✔
625
  const filter = resolveMongoQuery(
375✔
626
    this.queryParser,
375✔
627
    clientQueryString,
375✔
628
    acl_rows,
375✔
629
    otherParams,
375✔
630
    false
375✔
631
  )
375✔
632
  const _id = this.castCollectionId(docId)
375✔
633

375✔
634
  const stateArr = state.split(',')
375✔
635
  const doc = await this.crudService.findById(crudContext, _id, filter, projection, stateArr)
375✔
636
  if (!doc) {
375✔
637
    return reply.notFound()
78✔
638
  }
78✔
639

276✔
640
  const response = this.castItem(doc)
276✔
641
  itemValidator(response)
276✔
642
  return response
276✔
643
}
375✔
644

94✔
645
async function handleInsertOne(request, reply) {
76✔
646
  if (this.customMetrics) {
76✔
647
    this.customMetrics.collectionInvocation.inc({
13✔
648
      collection_name: this.modelName,
13✔
649
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
13✔
650
    })
13✔
651
  }
13✔
652

76✔
653
  const { body: doc, crudContext } = request
76✔
654

76✔
655
  this.queryParser.parseAndCastBody(doc)
76✔
656

76✔
657
  try {
76✔
658
    const insertedDoc = await this.crudService.insertOne(crudContext, doc)
76✔
659
    return mapToObjectWithOnlyId(insertedDoc)
73✔
660
  } catch (error) {
76✔
661
    if (error.code === UNIQUE_INDEX_MONGO_ERROR_CODE) {
3✔
662
      request.log.error('unique index violation')
3✔
663
      return reply.getHttpError(UNIQUE_INDEX_ERROR_STATUS_CODE, error.message)
3✔
664
    }
3✔
665
    throw error
×
666
  }
×
667
}
76✔
668

94✔
669
async function handleValidate() {
3✔
670
  return { result: 'ok' }
3✔
671
}
3✔
672

94✔
673
async function handleDeleteId(request, reply) {
56✔
674
  if (this.customMetrics) {
56!
675
    this.customMetrics.collectionInvocation.inc({
2✔
676
      collection_name: this.modelName,
2✔
677
      type: PROMETHEUS_OP_TYPE.DELETE,
2✔
678
    })
2✔
679
  }
2✔
680

56✔
681
  const { query, headers, params, crudContext } = request
56✔
682

56✔
683
  const docId = params.id
56✔
684
  const _id = this.castCollectionId(docId)
56✔
685

56✔
686
  const {
56✔
687
    [QUERY]: clientQueryString,
56✔
688
    [STATE]: state,
56✔
689
    ...otherParams
56✔
690
  } = query
56✔
691
  const { acl_rows } = headers
56✔
692

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

56✔
695
  const stateArr = state.split(',')
56✔
696
  const doc = await this.crudService.deleteById(crudContext, _id, filter, stateArr)
56✔
697

53✔
698
  if (!doc) {
56✔
699
    return reply.notFound()
18✔
700
  }
18✔
701

35✔
702
  // the document should not be returned:
35✔
703
  // we don't know which projection the user is able to see
35✔
704
  reply.code(204)
35✔
705
}
56✔
706

94✔
707
async function handleDeleteList(request) {
41✔
708
  if (this.customMetrics) {
41!
709
    this.customMetrics.collectionInvocation.inc({
2✔
710
      collection_name: this.modelName,
2✔
711
      type: PROMETHEUS_OP_TYPE.DELETE,
2✔
712
    })
2✔
713
  }
2✔
714

41✔
715
  const { query, headers, crudContext } = request
41✔
716

41✔
717
  const {
41✔
718
    [QUERY]: clientQueryString,
41✔
719
    [STATE]: state,
41✔
720
    ...otherParams
41✔
721
  } = query
41✔
722
  const { acl_rows } = headers
41✔
723

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

41✔
726
  const stateArr = state.split(',')
41✔
727
  return this.crudService.deleteAll(crudContext, filter, stateArr)
41✔
728
}
41✔
729

94✔
730
async function handleCount(request) {
51✔
731
  if (this.customMetrics) {
51✔
732
    this.customMetrics.collectionInvocation.inc({
3✔
733
      collection_name: this.modelName,
3✔
734
      type: PROMETHEUS_OP_TYPE.FETCH,
3✔
735
    })
3✔
736
  }
3✔
737

51✔
738
  const { query, headers, crudContext } = request
51✔
739
  const {
51✔
740
    [QUERY]: clientQueryString,
51✔
741
    [STATE]: state,
51✔
742
    [USE_ESTIMATE]: useEstimate,
51✔
743
    ...otherParams
51✔
744
  } = query
51✔
745

51✔
746
  const { acl_rows } = headers
51✔
747

51✔
748
  if (useEstimate) {
51✔
749
    return this.crudService.estimatedDocumentCount(crudContext)
6✔
750
  }
6✔
751

45✔
752
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
45✔
753
  const stateArr = state.split(',')
45✔
754

45✔
755
  return this.crudService.count(crudContext, mongoQuery, stateArr)
45✔
756
}
51✔
757

94✔
758
async function handlePatchId(request, reply) {
226✔
759
  if (this.customMetrics) {
226!
760
    this.customMetrics.collectionInvocation.inc({
4✔
761
      collection_name: this.modelName,
4✔
762
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
4✔
763
    })
4✔
764
  }
4✔
765

226✔
766
  const {
226✔
767
    query,
226✔
768
    headers,
226✔
769
    params,
226✔
770
    crudContext,
226✔
771
    log,
226✔
772
    routeOptions: { config: { itemValidator } },
226✔
773
  } = request
226✔
774

226✔
775
  const {
226✔
776
    [QUERY]: clientQueryString,
226✔
777
    [STATE]: state,
226✔
778
    ...otherParams
226✔
779
  } = query
226✔
780
  const {
226✔
781
    acl_rows,
226✔
782
    acl_write_columns: aclWriteColumns,
226✔
783
    acl_read_columns: aclColumns = '',
226✔
784
  } = headers
226✔
785

226✔
786
  const commands = request.body
226✔
787

226✔
788
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
226✔
789

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

226✔
792
  this.queryParser.parseAndCastCommands(commands, editableFields)
226✔
793
  const projection = resolveProjection('', aclColumns, this.allFieldNames, '', log)
226✔
794

226✔
795
  const docId = params.id
226✔
796
  const _id = this.castCollectionId(docId)
226✔
797

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

220✔
801
  if (!doc) {
226✔
802
    return reply.notFound()
51✔
803
  }
51✔
804

169✔
805
  const response = this.castItem(doc)
169✔
806
  itemValidator(response)
169✔
807
  return response
169✔
808
}
226✔
809

94✔
810
async function handlePatchMany(request) {
90✔
811
  if (this.customMetrics) {
90!
812
    this.customMetrics.collectionInvocation.inc({
×
813
      collection_name: this.modelName,
×
814
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
815
    })
×
816
  }
×
817

90✔
818
  const { query, headers, crudContext } = request
90✔
819
  const {
90✔
820
    [QUERY]: clientQueryString,
90✔
821
    [STATE]: state,
90✔
822
    ...otherParams
90✔
823
  } = query
90✔
824
  const {
90✔
825
    acl_rows,
90✔
826
    acl_write_columns: aclWriteColumns,
90✔
827
  } = headers
90✔
828

90✔
829
  const commands = request.body
90✔
830
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
90✔
831
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
90✔
832
  this.queryParser.parseAndCastCommands(commands, editableFields)
90✔
833

90✔
834
  const stateArr = state.split(',')
90✔
835
  const nModified = await this.crudService.patchMany(crudContext, commands, mongoQuery, stateArr)
90✔
836

84✔
837
  return nModified
84✔
838
}
90✔
839

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

66✔
848
  const {
66✔
849
    query,
66✔
850
    headers,
66✔
851
    crudContext,
66✔
852
    log,
66✔
853
    routeOptions: { config: { itemValidator } },
66✔
854
  } = request
66✔
855
  const {
66✔
856
    [QUERY]: clientQueryString,
66✔
857
    [STATE]: state,
66✔
858
    ...otherParams
66✔
859
  } = query
66✔
860
  const {
66✔
861
    acl_rows,
66✔
862
    acl_write_columns: aclWriteColumns,
66✔
863
    acl_read_columns: aclColumns = '',
66✔
864
  } = headers
66✔
865

66✔
866
  const commands = request.body
66✔
867

66✔
868
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
66✔
869
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
66✔
870

66✔
871
  this.queryParser.parseAndCastCommands(commands, editableFields)
66✔
872
  const projection = resolveProjection('', aclColumns, this.allFieldNames, '', log)
66✔
873

66✔
874
  const stateArr = state.split(',')
66✔
875
  const doc = await this.crudService.upsertOne(crudContext, commands, mongoQuery, projection, stateArr)
66✔
876

66✔
877
  const response = this.castItem(doc)
66✔
878

66✔
879
  itemValidator(response)
66✔
880
  return response
66✔
881
}
66✔
882

94✔
883
async function handlePatchBulk(request) {
90✔
884
  if (this.customMetrics) {
90!
885
    this.customMetrics.collectionInvocation.inc({
×
886
      collection_name: this.modelName,
×
887
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
888
    })
×
889
  }
×
890

90✔
891
  const { body: filterUpdateCommands, crudContext, headers } = request
90✔
892

90✔
893

90✔
894
  const nModified = await this.crudService.patchBulk(
90✔
895
    crudContext,
90✔
896
    filterUpdateCommands,
90✔
897
    this.queryParser,
90✔
898
    this.castCollectionId,
90✔
899
    getEditableFields(headers[ACL_WRITE_COLUMNS], this.allFieldNames),
90✔
900
    headers[ACL_ROWS],
90✔
901
  )
90✔
902
  return nModified
90✔
903
}
90✔
904

94✔
905
async function handleInsertMany(request, reply) {
60✔
906
  if (this.customMetrics) {
60!
907
    this.customMetrics.collectionInvocation.inc({
×
908
      collection_name: this.modelName,
×
909
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
910
    })
×
911
  }
×
912

60✔
913
  const { body: docs, crudContext } = request
60✔
914

60✔
915
  try {
60✔
916
    const ids = await this.crudService.insertMany(
60✔
917
      crudContext,
60✔
918
      docs,
60✔
919
      this.queryParser,
60✔
920
      { idOnly: true }
60✔
921
    )
60✔
922
    return ids
57✔
923
  } catch (error) {
60✔
924
    if (error.code === UNIQUE_INDEX_MONGO_ERROR_CODE) {
3✔
925
      request.log.error('unique index violation')
3✔
926
      return reply.getHttpError(UNIQUE_INDEX_ERROR_STATUS_CODE, error.message)
3✔
927
    }
3✔
928
    throw error
×
929
  }
×
930
}
60✔
931

94✔
932
async function handleChangeStateById(request, reply) {
51✔
933
  if (this.customMetrics) {
51!
934
    this.customMetrics.collectionInvocation.inc({
×
935
      collection_name: this.modelName,
×
936
      type: PROMETHEUS_OP_TYPE.CHANGE_STATE,
×
937
    })
×
938
  }
×
939

51✔
940
  const { body, crudContext, headers, query } = request
51✔
941
  const {
51✔
942
    [QUERY]: clientQueryString,
51✔
943
    ...otherParams
51✔
944
  } = query
51✔
945

51✔
946
  const { acl_rows } = headers
51✔
947
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
51✔
948

51✔
949
  const docId = request.params.id
51✔
950
  const _id = this.castCollectionId(docId)
51✔
951

51✔
952
  try {
51✔
953
    const doc = await this.crudService.changeStateById(crudContext, _id, body.stateTo, mongoQuery)
51✔
954
    if (!doc) {
51✔
955
      return reply.notFound()
9✔
956
    }
9✔
957

24✔
958
    reply.code(204)
24✔
959
  } catch (error) {
51✔
960
    if (error.statusCode) {
15✔
961
      return reply.getHttpError(error.statusCode, error.message)
15✔
962
    }
15✔
963

×
964
    throw error
×
965
  }
×
966
}
51✔
967

94✔
968
async function handleChangeStateMany(request) {
48✔
969
  if (this.customMetrics) {
48!
970
    this.customMetrics.collectionInvocation.inc({
×
971
      collection_name: this.modelName,
×
972
      type: PROMETHEUS_OP_TYPE.CHANGE_STATE,
×
973
    })
×
974
  }
×
975

48✔
976
  const { body: filterUpdateCommands, crudContext, headers } = request
48✔
977

48✔
978
  const {
48✔
979
    acl_rows,
48✔
980
  } = headers
48✔
981

48✔
982
  const parsedAndCastedCommands = new Array(filterUpdateCommands.length)
48✔
983
  for (let i = 0; i < filterUpdateCommands.length; i++) {
48✔
984
    const {
60✔
985
      filter,
60✔
986
      stateTo,
60✔
987
    } = filterUpdateCommands[i]
60✔
988

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

60✔
991
    parsedAndCastedCommands[i] = {
60✔
992
      query: mongoQuery,
60✔
993
      stateTo,
60✔
994
    }
60✔
995
  }
60✔
996

48✔
997
  return this.crudService.changeStateMany(crudContext, parsedAndCastedCommands)
48✔
998
}
48✔
999

94✔
1000
async function injectContextInRequest(request) {
2,750✔
1001
  const userIdHeader = request.headers[this.userIdHeaderKey]
2,750✔
1002
  const isUserHeaderInvalid = INVALID_USERID.includes(userIdHeader)
2,750✔
1003

2,750✔
1004
  let userId = 'public'
2,750✔
1005

2,750✔
1006
  if (userIdHeader && !isUserHeaderInvalid) {
2,750✔
1007
    userId = userIdHeader
666✔
1008
  }
666✔
1009

2,750✔
1010
  request.crudContext = {
2,750✔
1011
    log: request.log,
2,750✔
1012
    userId,
2,750✔
1013
    now: new Date(),
2,750✔
1014
  }
2,750✔
1015
}
2,750✔
1016

94✔
1017
async function parseEncodedJsonQueryParams(logger, request) {
2,750✔
1018
  if (request.headers.json_query_params_encoding) {
2,750!
1019
    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.')
×
1020
  }
×
1021

2,750✔
1022
  // TODO remove request.headers.json_query_params_encoding fallback in v7.0.0
2,750✔
1023
  const jsonQueryParamsEncoding = request.headers['json-query-params-encoding'] || request.headers.json_query_params_encoding
2,750✔
1024
  switch (jsonQueryParamsEncoding) {
2,750✔
1025
  case 'base64': {
2,750✔
1026
    const queryJsonFields = [QUERY, RAW_PROJECTION]
9✔
1027
    for (const field of queryJsonFields) {
9✔
1028
      if (request.query[field]) {
18✔
1029
        request.query[field] = Buffer.from(request.query[field], jsonQueryParamsEncoding).toString()
9✔
1030
      }
9✔
1031
    }
18✔
1032
    break
9✔
1033
  }
9✔
1034
  default: break
2,750✔
1035
  }
2,750✔
1036
}
2,750✔
1037

94✔
1038
async function notFoundHandler(request, reply) {
174✔
1039
  reply
174✔
1040
    .code(404)
174✔
1041
    .send({
174✔
1042
      error: 'not found',
174✔
1043
    })
174✔
1044
}
174✔
1045

94✔
1046
async function customErrorHandler(error, request, reply) {
665✔
1047
  if (error.statusCode === 404) {
665✔
1048
    return notFoundHandler(request, reply)
156✔
1049
  }
156✔
1050

509✔
1051
  if (error.validation?.[0]?.message === 'must NOT have additional properties') {
665✔
1052
    reply.code(error.statusCode)
108✔
1053
    throw new Error(`${error.message}. Property "${error.validation[0].params.additionalProperty}" is not defined in validation schema`)
108✔
1054
  }
108✔
1055

401✔
1056
  throw error
401✔
1057
}
665✔
1058

94✔
1059
function resolveProjection(clientProjectionString, aclColumns, allFieldNames, rawProjection, log) {
1,926✔
1060
  log.debug('Resolving projections')
1,926✔
1061
  const acls = splitACLs(aclColumns)
1,926✔
1062

1,926✔
1063
  if (clientProjectionString && rawProjection) {
1,926✔
1064
    log.error('Use of both _p and _rawp is not permitted')
18✔
1065
    throw new BadRequestError(
18✔
1066
      'Use of both _rawp and _p parameter is not allowed')
18✔
1067
  }
18✔
1068

1,908✔
1069
  let projection
1,908✔
1070
  if (!clientProjectionString && !rawProjection) {
1,926✔
1071
    projection = removeAclColumns(allFieldNames, acls)
1,252✔
1072
  } else if (rawProjection) {
1,482✔
1073
    projection = resolveRawProjectionString(rawProjection, acls, allFieldNames, log)
276✔
1074
  } else if (clientProjectionString) {
656✔
1075
    projection = resolveClientProjectionString(clientProjectionString, acls)
380✔
1076
  }
380✔
1077

1,842✔
1078
  return getProjection(projection)
1,842✔
1079
}
1,926✔
1080

94✔
1081
function getProjection(projection) {
1,842✔
1082
  // In case of empty projection, we project only the _id
1,842✔
1083
  if (!projection?.length) { return { _id: 1 } }
1,842✔
1084

1,795✔
1085
  return projection.reduce((acc, val) => {
1,795✔
1086
    const propertiesToInclude = typeof val === 'string'
23,959✔
1087
      // a string represents the name of a field to be projected
23,959✔
1088
      ? { [val]: 1 }
23,959✔
1089
      // an object represents a raw projection to be passed as it is
23,959✔
1090
      : val
23,959✔
1091

23,959✔
1092
    return { ...acc, ...propertiesToInclude }
23,959✔
1093
  }, {})
1,795✔
1094
}
1,842✔
1095

94✔
1096
function resolveClientProjectionString(clientProjectionString, _acls) {
380✔
1097
  const clientProjection = getClientProjection(clientProjectionString)
380✔
1098
  return removeAclColumns(clientProjection, _acls)
380✔
1099
}
380✔
1100

94✔
1101
function resolveRawProjectionString(rawProjection, _acls, allFieldNames, log) {
276✔
1102
  try {
276✔
1103
    checkAllowedOperators(
276✔
1104
      rawProjection,
276✔
1105
      rawProjectionDictionary,
276✔
1106
      _acls.length > 0 ? _acls : allFieldNames, log)
276✔
1107

276✔
1108
    const rawProjectionObject = resolveRawProjection(rawProjection)
276✔
1109
    const projection = removeAclColumnsFromRawProjection(rawProjectionObject, _acls)
276✔
1110

276✔
1111
    return !lisEmpty(projection) ? [projection] : []
276✔
1112
  } catch (errorMessage) {
276✔
1113
    log.error(errorMessage.message)
66✔
1114
    throw new BadRequestError(errorMessage.message)
66✔
1115
  }
66✔
1116
}
276✔
1117

94✔
1118
function splitACLs(acls) {
1,926✔
1119
  if (acls) { return acls.split(',') }
1,926✔
1120
  return []
1,729✔
1121
}
1,926✔
1122

94✔
1123
function removeAclColumns(fieldsInProjection, aclColumns) {
1,842✔
1124
  if (aclColumns.length > 0) {
1,842✔
1125
    return fieldsInProjection.filter(field => {
191✔
1126
      return aclColumns.indexOf(field) > -1
1,065✔
1127
    })
191✔
1128
  }
191✔
1129

1,651✔
1130
  return fieldsInProjection
1,651✔
1131
}
1,842✔
1132

94✔
1133
function removeAclColumnsFromRawProjection(rawProjectionObject, aclColumns) {
216✔
1134
  const isRawProjectionOverridingACLs = checkIfRawProjectionOverridesAcls(rawProjectionObject, aclColumns)
216✔
1135
  if (isRawProjectionOverridingACLs) {
216✔
1136
    throw Error('_rawp exclusive projection is overriding at least one acl_read_column value')
6✔
1137
  }
6✔
1138

210✔
1139
  const rawProjectionFields = Object.keys(rawProjectionObject)
210✔
1140
  const filteredFields = removeAclColumns(rawProjectionFields, aclColumns)
210✔
1141

210✔
1142
  return filteredFields.reduce((acc, current) => {
210✔
1143
    if (rawProjectionObject[current] === 0 || rawProjectionObject[current]) {
324✔
1144
      acc[current] = rawProjectionObject[current]
324✔
1145
    }
324✔
1146
    return acc
324✔
1147
  }, {})
210✔
1148
}
216✔
1149

94✔
1150
function getClientProjection(clientProjectionString) {
380✔
1151
  if (clientProjectionString) {
380✔
1152
    return clientProjectionString.split(',')
380✔
1153
  }
380✔
1154
  return []
×
1155
}
380✔
1156

94✔
1157
function resolveRawProjection(clientRawProjectionString) {
216✔
1158
  if (clientRawProjectionString) {
216✔
1159
    return JSON.parse(clientRawProjectionString)
216✔
1160
  }
216✔
1161
  return {}
×
1162
}
216✔
1163

94✔
1164
function checkAllowedOperators(rawProjection, projectionDictionary, additionalFields, log) {
276✔
1165
  if (!rawProjection) {
276!
1166
    log.debug('No raw projection found: checkAllowedOperators returns true')
×
1167
    return true
×
1168
  }
×
1169

276✔
1170
  const { allowedOperators, notAllowedOperators } = projectionDictionary
276✔
1171
  const allowedFields = [...allowedOperators]
276✔
1172

276✔
1173
  additionalFields.forEach(field => allowedFields.push(`$${field}`))
276✔
1174

276✔
1175
  log.debug({ allowedOperators: allowedFields }, 'Allowed operators for projection')
276✔
1176
  log.debug({ notAllowedOperators }, 'Not allowed operators for projection')
276✔
1177

276✔
1178
  // to match both camelCase operators and snake mongo_systems variables
276✔
1179
  const operatorsRegex = /\${1,2}[a-zA-Z_]+/g
276✔
1180
  const matches = rawProjection.match(operatorsRegex)
276✔
1181

276✔
1182
  if (!matches) {
276✔
1183
    log.debug('No operators found in raw projection: checkAllowedOperators returns true')
108✔
1184
    return true
108✔
1185
  }
108✔
1186

168✔
1187
  return !matches.some(match => {
168✔
1188
    if (match.startsWith('$$')) {
444✔
1189
      log.debug({ match }, 'Found $$ match in raw projection')
120✔
1190
      if (notAllowedOperators.includes(match)) {
120✔
1191
        throw Error(`Operator ${match} is not allowed in raw projection`)
48✔
1192
      }
48✔
1193

72✔
1194
      return notAllowedOperators.includes(match)
72✔
1195
    }
72✔
1196

324✔
1197
    if (!allowedFields.includes(match)) {
444✔
1198
      throw Error(`Operator ${match} is not allowed in raw projection`)
12✔
1199
    }
12✔
1200

312✔
1201
    return !allowedFields.includes(match)
312✔
1202
  })
168✔
1203
}
276✔
1204

94✔
1205
function checkIfRawProjectionOverridesAcls(rawProjection, acls) {
216✔
1206
  return Object.keys(rawProjection).some(field =>
216✔
1207
    acls.includes(field) && rawProjection[field] === 0
441✔
1208
  )
216✔
1209
}
216✔
1210

94✔
1211
function mapToObjectWithOnlyId(doc) {
73✔
1212
  return { _id: doc._id.toString() }
73✔
1213
}
73✔
1214

94✔
1215
const internalFields = [
94✔
1216
  UPDATERID,
94✔
1217
  UPDATEDAT,
94✔
1218
  CREATORID,
94✔
1219
  CREATEDAT,
94✔
1220
  __STATE__,
94✔
1221
]
94✔
1222
function getEditableFields(aclWriteColumns, allFieldNames) {
472✔
1223
  const editableFields = aclWriteColumns ? aclWriteColumns.split(',') : allFieldNames
472!
1224
  return editableFields.filter(ef => !internalFields.includes(ef))
472✔
1225
}
472✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc