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

mia-platform / crud-service / 8505753951

01 Apr 2024 08:19AM UTC coverage: 97.127%. Remained the same
8505753951

Pull #288

github

web-flow
Merge cb43af620 into 18bf8b65f
Pull Request #288: build(deps-dev): bump tap from 18.7.1 to 18.7.2

1828 of 1977 branches covered (92.46%)

Branch coverage included in aggregate %.

9058 of 9231 relevant lines covered (98.13%)

7219.36 hits per line

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

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

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

94✔
19
'use strict'
94✔
20

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

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

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

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

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

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

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

94✔
74

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

40✔
463
  const stateArr = state?.split(',')
40✔
464
  const contentType = replyType()
40✔
465
  const responseStringifiers = getFileMimeStringifiers(contentType)
40✔
466
  if (!responseStringifiers) {
40!
467
    return reply.getHttpError(UNSUPPORTED_MIME_TYPE_STATUS_CODE, `Unsupported file type ${contentType}`)
×
468
  }
×
469

40✔
470
  reply.raw.setHeader('Content-Type', contentType)
40✔
471

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

94✔
491
async function handleGetList(request, reply) {
1,192✔
492
  if (this.customMetrics) {
1,192✔
493
    this.customMetrics.collectionInvocation.inc({
67✔
494
      collection_name: this.modelName,
67✔
495
      type: PROMETHEUS_OP_TYPE.FETCH,
67✔
496
    })
67✔
497
  }
67✔
498

1,192✔
499
  const {
1,192✔
500
    query,
1,192✔
501
    headers,
1,192✔
502
    crudContext,
1,192✔
503
    log,
1,192✔
504
    routeOptions: { config: { replyType, streamValidator } },
1,192✔
505
  } = request
1,192✔
506
  const {
1,192✔
507
    [QUERY]: clientQueryString,
1,192✔
508
    [PROJECTION]: clientProjectionString = '',
1,192✔
509
    [RAW_PROJECTION]: clientRawProjectionString = '',
1,192✔
510
    [SORT]: sortQuery,
1,192✔
511
    [LIMIT]: limit,
1,192✔
512
    [SKIP]: skip,
1,192✔
513
    [STATE]: state,
1,192✔
514
    ...otherParams
1,192✔
515
  } = query
1,192✔
516
  const { acl_rows, acl_read_columns, accept } = headers
1,192✔
517
  const contentType = replyType(accept)
1,192✔
518

1,192✔
519
  const responseStringifiers = getFileMimeStringifiers(contentType, {})
1,192✔
520
  if (!responseStringifiers) {
1,192✔
521
    return reply.getHttpError(NOT_ACCEPTABLE, `unsupported file type ${contentType}`)
156✔
522
  }
156✔
523

1,036✔
524
  const projection = resolveProjection(
1,036✔
525
    clientProjectionString,
1,036✔
526
    acl_read_columns,
1,036✔
527
    this.allFieldNames,
1,036✔
528
    clientRawProjectionString,
1,036✔
529
    log
1,036✔
530
  )
1,036✔
531

1,036✔
532
  const isTextSearchQuery = query._q && this.queryParser.isTextSearchQuery(JSON.parse(query._q))
1,192✔
533
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, isTextSearchQuery)
1,192✔
534

1,192✔
535
  let sort
1,192✔
536
  if (sortQuery) {
1,192✔
537
    sort = Object.fromEntries(sortQuery.toString().split(',')
122✔
538
      .map((param) => (param[0] === '-' ? [param.substr(1), -1] : [param, 1])))
122✔
539
  }
122✔
540

970✔
541
  const stateArr = state.split(',')
970✔
542

970✔
543
  reply.raw.setHeader('Content-Type', contentType)
970✔
544

970✔
545
  try {
970✔
546
    await pipeline(
970✔
547
      this.crudService
970✔
548
        .findAll(crudContext, mongoQuery, projection, sort, skip, limit, stateArr, isTextSearchQuery)
970✔
549
        .stream(),
970✔
550
      this.castResultsAsStream(),
970✔
551
      streamValidator(),
970✔
552
      ...responseStringifiers({ fields: this.allFieldNames }),
970✔
553
      reply.raw
970✔
554
    )
970✔
555
  } catch (error) {
1,192✔
556
    request.log.error({ error }, 'Error during findAll stream')
3✔
557
    request.log.debug({ error: { ...error, message: error.message } }, 'Error during findAll stream with message')
3✔
558
    if (error.code === OPTIONS_INCOMPATIBILITY_ERROR_CODE) {
3!
559
      request.log.info(BAD_REQUEST_ERROR_STATUS_CODE)
×
560
    }
×
561
  }
3✔
562
}
1,192✔
563

94✔
564
async function handleGetId(request, reply) {
375✔
565
  if (this.customMetrics) {
375!
566
    this.customMetrics.collectionInvocation.inc({
×
567
      collection_name: this.modelName,
×
568
      type: PROMETHEUS_OP_TYPE.FETCH,
×
569
    })
×
570
  }
×
571

375✔
572
  const {
375✔
573
    crudContext,
375✔
574
    log,
375✔
575
    routeOptions: { config: { itemValidator } },
375✔
576
  } = request
375✔
577
  const docId = request.params.id
375✔
578
  const { acl_rows, acl_read_columns } = request.headers
375✔
579

375✔
580
  const {
375✔
581
    [QUERY]: clientQueryString,
375✔
582
    [PROJECTION]: clientProjectionString = '',
375✔
583
    [RAW_PROJECTION]: clientRawProjectionString = '',
375✔
584
    [STATE]: state,
375✔
585
    ...otherParams
375✔
586
  } = request.query
375✔
587

375✔
588
  const projection = resolveProjection(
375✔
589
    clientProjectionString,
375✔
590
    acl_read_columns,
375✔
591
    this.allFieldNames,
375✔
592
    clientRawProjectionString,
375✔
593
    log
375✔
594
  )
375✔
595
  const filter = resolveMongoQuery(
375✔
596
    this.queryParser,
375✔
597
    clientQueryString,
375✔
598
    acl_rows,
375✔
599
    otherParams,
375✔
600
    false
375✔
601
  )
375✔
602
  const _id = this.castCollectionId(docId)
375✔
603

375✔
604
  const stateArr = state.split(',')
375✔
605
  const doc = await this.crudService.findById(crudContext, _id, filter, projection, stateArr)
375✔
606
  if (!doc) {
375✔
607
    return reply.notFound()
78✔
608
  }
78✔
609

276✔
610
  const response = this.castItem(doc)
276✔
611
  itemValidator(response)
276✔
612
  return response
276✔
613
}
375✔
614

94✔
615
async function handleInsertOne(request, reply) {
76✔
616
  if (this.customMetrics) {
76✔
617
    this.customMetrics.collectionInvocation.inc({
13✔
618
      collection_name: this.modelName,
13✔
619
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
13✔
620
    })
13✔
621
  }
13✔
622

76✔
623
  const { body: doc, crudContext } = request
76✔
624

76✔
625
  this.queryParser.parseAndCastBody(doc)
76✔
626

76✔
627
  try {
76✔
628
    const insertedDoc = await this.crudService.insertOne(crudContext, doc)
76✔
629
    return mapToObjectWithOnlyId(insertedDoc)
73✔
630
  } catch (error) {
76✔
631
    if (error.code === UNIQUE_INDEX_MONGO_ERROR_CODE) {
3✔
632
      request.log.error('unique index violation')
3✔
633
      return reply.getHttpError(UNIQUE_INDEX_ERROR_STATUS_CODE, error.message)
3✔
634
    }
3✔
635
    throw error
×
636
  }
×
637
}
76✔
638

94✔
639
async function handleValidate() {
3✔
640
  return { result: 'ok' }
3✔
641
}
3✔
642

94✔
643
async function handleDeleteId(request, reply) {
56✔
644
  if (this.customMetrics) {
56!
645
    this.customMetrics.collectionInvocation.inc({
2✔
646
      collection_name: this.modelName,
2✔
647
      type: PROMETHEUS_OP_TYPE.DELETE,
2✔
648
    })
2✔
649
  }
2✔
650

56✔
651
  const { query, headers, params, crudContext } = request
56✔
652

56✔
653
  const docId = params.id
56✔
654
  const _id = this.castCollectionId(docId)
56✔
655

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

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

56✔
665
  const stateArr = state.split(',')
56✔
666
  const doc = await this.crudService.deleteById(crudContext, _id, filter, stateArr)
56✔
667

53✔
668
  if (!doc) {
56✔
669
    return reply.notFound()
18✔
670
  }
18✔
671

35✔
672
  // the document should not be returned:
35✔
673
  // we don't know which projection the user is able to see
35✔
674
  reply.code(204)
35✔
675
}
56✔
676

94✔
677
async function handleDeleteList(request) {
41✔
678
  if (this.customMetrics) {
41!
679
    this.customMetrics.collectionInvocation.inc({
2✔
680
      collection_name: this.modelName,
2✔
681
      type: PROMETHEUS_OP_TYPE.DELETE,
2✔
682
    })
2✔
683
  }
2✔
684

41✔
685
  const { query, headers, crudContext } = request
41✔
686

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

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

41✔
696
  const stateArr = state.split(',')
41✔
697
  return this.crudService.deleteAll(crudContext, filter, stateArr)
41✔
698
}
41✔
699

94✔
700
async function handleCount(request) {
51✔
701
  if (this.customMetrics) {
51✔
702
    this.customMetrics.collectionInvocation.inc({
3✔
703
      collection_name: this.modelName,
3✔
704
      type: PROMETHEUS_OP_TYPE.FETCH,
3✔
705
    })
3✔
706
  }
3✔
707

51✔
708
  const { query, headers, crudContext } = request
51✔
709
  const {
51✔
710
    [QUERY]: clientQueryString,
51✔
711
    [STATE]: state,
51✔
712
    [USE_ESTIMATE]: useEstimate,
51✔
713
    ...otherParams
51✔
714
  } = query
51✔
715

51✔
716
  const { acl_rows } = headers
51✔
717

51✔
718
  if (useEstimate) {
51✔
719
    return this.crudService.estimatedDocumentCount(crudContext)
6✔
720
  }
6✔
721

45✔
722
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
45✔
723
  const stateArr = state.split(',')
45✔
724

45✔
725
  return this.crudService.count(crudContext, mongoQuery, stateArr)
45✔
726
}
51✔
727

94✔
728
async function handlePatchId(request, reply) {
226✔
729
  if (this.customMetrics) {
226!
730
    this.customMetrics.collectionInvocation.inc({
4✔
731
      collection_name: this.modelName,
4✔
732
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
4✔
733
    })
4✔
734
  }
4✔
735

226✔
736
  const {
226✔
737
    query,
226✔
738
    headers,
226✔
739
    params,
226✔
740
    crudContext,
226✔
741
    log,
226✔
742
    routeOptions: { config: { itemValidator } },
226✔
743
  } = request
226✔
744

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

226✔
756
  const commands = request.body
226✔
757

226✔
758
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
226✔
759

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

226✔
762
  this.queryParser.parseAndCastCommands(commands, editableFields)
226✔
763
  const projection = resolveProjection('', aclColumns, this.allFieldNames, '', log)
226✔
764

226✔
765
  const docId = params.id
226✔
766
  const _id = this.castCollectionId(docId)
226✔
767

226✔
768
  const stateArr = state.split(',')
226✔
769
  const doc = await this.crudService.patchById(crudContext, _id, commands, mongoQuery, projection, stateArr)
226✔
770

220✔
771
  if (!doc) {
226✔
772
    return reply.notFound()
51✔
773
  }
51✔
774

169✔
775
  const response = this.castItem(doc)
169✔
776
  itemValidator(response)
169✔
777
  return response
169✔
778
}
226✔
779

94✔
780
async function handlePatchMany(request) {
90✔
781
  if (this.customMetrics) {
90!
782
    this.customMetrics.collectionInvocation.inc({
×
783
      collection_name: this.modelName,
×
784
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
785
    })
×
786
  }
×
787

90✔
788
  const { query, headers, crudContext } = request
90✔
789
  const {
90✔
790
    [QUERY]: clientQueryString,
90✔
791
    [STATE]: state,
90✔
792
    ...otherParams
90✔
793
  } = query
90✔
794
  const {
90✔
795
    acl_rows,
90✔
796
    acl_write_columns: aclWriteColumns,
90✔
797
  } = headers
90✔
798

90✔
799
  const commands = request.body
90✔
800
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
90✔
801
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
90✔
802
  this.queryParser.parseAndCastCommands(commands, editableFields)
90✔
803

90✔
804
  const stateArr = state.split(',')
90✔
805
  const nModified = await this.crudService.patchMany(crudContext, commands, mongoQuery, stateArr)
90✔
806

84✔
807
  return nModified
84✔
808
}
90✔
809

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

66✔
818
  const {
66✔
819
    query,
66✔
820
    headers,
66✔
821
    crudContext,
66✔
822
    log,
66✔
823
    routeOptions: { config: { itemValidator } },
66✔
824
  } = request
66✔
825
  const {
66✔
826
    [QUERY]: clientQueryString,
66✔
827
    [STATE]: state,
66✔
828
    ...otherParams
66✔
829
  } = query
66✔
830
  const {
66✔
831
    acl_rows,
66✔
832
    acl_write_columns: aclWriteColumns,
66✔
833
    acl_read_columns: aclColumns = '',
66✔
834
  } = headers
66✔
835

66✔
836
  const commands = request.body
66✔
837

66✔
838
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
66✔
839
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
66✔
840

66✔
841
  this.queryParser.parseAndCastCommands(commands, editableFields)
66✔
842
  const projection = resolveProjection('', aclColumns, this.allFieldNames, '', log)
66✔
843

66✔
844
  const stateArr = state.split(',')
66✔
845
  const doc = await this.crudService.upsertOne(crudContext, commands, mongoQuery, projection, stateArr)
66✔
846

66✔
847
  const response = this.castItem(doc)
66✔
848

66✔
849
  itemValidator(response)
66✔
850
  return response
66✔
851
}
66✔
852

94✔
853
async function handlePatchBulk(request) {
90✔
854
  if (this.customMetrics) {
90!
855
    this.customMetrics.collectionInvocation.inc({
×
856
      collection_name: this.modelName,
×
857
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
858
    })
×
859
  }
×
860

90✔
861
  const { body: filterUpdateCommands, crudContext, headers } = request
90✔
862

90✔
863

90✔
864
  const nModified = await this.crudService.patchBulk(
90✔
865
    crudContext,
90✔
866
    filterUpdateCommands,
90✔
867
    this.queryParser,
90✔
868
    this.castCollectionId,
90✔
869
    getEditableFields(headers[ACL_WRITE_COLUMNS], this.allFieldNames),
90✔
870
    headers[ACL_ROWS],
90✔
871
  )
90✔
872
  return nModified
90✔
873
}
90✔
874

94✔
875
async function handleInsertMany(request, reply) {
60✔
876
  if (this.customMetrics) {
60!
877
    this.customMetrics.collectionInvocation.inc({
×
878
      collection_name: this.modelName,
×
879
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
880
    })
×
881
  }
×
882

60✔
883
  const { body: docs, crudContext } = request
60✔
884

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

94✔
902
async function handleChangeStateById(request, reply) {
51✔
903
  if (this.customMetrics) {
51!
904
    this.customMetrics.collectionInvocation.inc({
×
905
      collection_name: this.modelName,
×
906
      type: PROMETHEUS_OP_TYPE.CHANGE_STATE,
×
907
    })
×
908
  }
×
909

51✔
910
  const { body, crudContext, headers, query } = request
51✔
911
  const {
51✔
912
    [QUERY]: clientQueryString,
51✔
913
    ...otherParams
51✔
914
  } = query
51✔
915

51✔
916
  const { acl_rows } = headers
51✔
917
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
51✔
918

51✔
919
  const docId = request.params.id
51✔
920
  const _id = this.castCollectionId(docId)
51✔
921

51✔
922
  try {
51✔
923
    const doc = await this.crudService.changeStateById(crudContext, _id, body.stateTo, mongoQuery)
51✔
924
    if (!doc) {
51✔
925
      return reply.notFound()
9✔
926
    }
9✔
927

24✔
928
    reply.code(204)
24✔
929
  } catch (error) {
51✔
930
    if (error.statusCode) {
15✔
931
      return reply.getHttpError(error.statusCode, error.message)
15✔
932
    }
15✔
933

×
934
    throw error
×
935
  }
×
936
}
51✔
937

94✔
938
async function handleChangeStateMany(request) {
48✔
939
  if (this.customMetrics) {
48!
940
    this.customMetrics.collectionInvocation.inc({
×
941
      collection_name: this.modelName,
×
942
      type: PROMETHEUS_OP_TYPE.CHANGE_STATE,
×
943
    })
×
944
  }
×
945

48✔
946
  const { body: filterUpdateCommands, crudContext, headers } = request
48✔
947

48✔
948
  const {
48✔
949
    acl_rows,
48✔
950
  } = headers
48✔
951

48✔
952
  const parsedAndCastedCommands = new Array(filterUpdateCommands.length)
48✔
953
  for (let i = 0; i < filterUpdateCommands.length; i++) {
48✔
954
    const {
60✔
955
      filter,
60✔
956
      stateTo,
60✔
957
    } = filterUpdateCommands[i]
60✔
958

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

60✔
961
    parsedAndCastedCommands[i] = {
60✔
962
      query: mongoQuery,
60✔
963
      stateTo,
60✔
964
    }
60✔
965
  }
60✔
966

48✔
967
  return this.crudService.changeStateMany(crudContext, parsedAndCastedCommands)
48✔
968
}
48✔
969

94✔
970
async function injectContextInRequest(request) {
2,558✔
971
  const userIdHeader = request.headers[this.userIdHeaderKey]
2,558✔
972
  const isUserHeaderInvalid = INVALID_USERID.includes(userIdHeader)
2,558✔
973

2,558✔
974
  let userId = 'public'
2,558✔
975

2,558✔
976
  if (userIdHeader && !isUserHeaderInvalid) {
2,558✔
977
    userId = userIdHeader
666✔
978
  }
666✔
979

2,558✔
980
  request.crudContext = {
2,558✔
981
    log: request.log,
2,558✔
982
    userId,
2,558✔
983
    now: new Date(),
2,558✔
984
  }
2,558✔
985
}
2,558✔
986

94✔
987
async function parseEncodedJsonQueryParams(logger, request) {
2,558✔
988
  if (request.headers.json_query_params_encoding) {
2,558!
989
    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.')
×
990
  }
×
991

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

94✔
1008
async function notFoundHandler(request, reply) {
174✔
1009
  reply
174✔
1010
    .code(404)
174✔
1011
    .send({
174✔
1012
      error: 'not found',
174✔
1013
    })
174✔
1014
}
174✔
1015

94✔
1016
async function customErrorHandler(error, request, reply) {
659✔
1017
  if (error.statusCode === 404) {
659✔
1018
    return notFoundHandler(request, reply)
156✔
1019
  }
156✔
1020

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

395✔
1026
  throw error
395✔
1027
}
659✔
1028

94✔
1029
function resolveProjection(clientProjectionString, aclColumns, allFieldNames, rawProjection, log) {
1,740✔
1030
  log.debug('Resolving projections')
1,740✔
1031
  const acls = splitACLs(aclColumns)
1,740✔
1032

1,740✔
1033
  if (clientProjectionString && rawProjection) {
1,740✔
1034
    log.error('Use of both _p and _rawp is not permitted')
18✔
1035
    throw new BadRequestError(
18✔
1036
      'Use of both _rawp and _p parameter is not allowed')
18✔
1037
  }
18✔
1038

1,722✔
1039
  if (!clientProjectionString && !rawProjection) {
1,740✔
1040
    return removeAclColumns(allFieldNames, acls)
1,165✔
1041
  } else if (rawProjection) {
1,740✔
1042
    return resolveRawProjectionString(rawProjection, acls, allFieldNames, log)
231✔
1043
  } else if (clientProjectionString) {
557✔
1044
    return resolveClientProjectionString(clientProjectionString, acls)
326✔
1045
  }
326✔
1046
}
1,740✔
1047

94✔
1048
function resolveClientProjectionString(clientProjectionString, _acls) {
326✔
1049
  const clientProjection = getClientProjection(clientProjectionString)
326✔
1050
  return removeAclColumns(clientProjection, _acls)
326✔
1051
}
326✔
1052

94✔
1053
function resolveRawProjectionString(rawProjection, _acls, allFieldNames, log) {
231✔
1054
  try {
231✔
1055
    checkAllowedOperators(
231✔
1056
      rawProjection,
231✔
1057
      rawProjectionDictionary,
231✔
1058
      _acls.length > 0 ? _acls : allFieldNames, log)
231✔
1059

231✔
1060
    const rawProjectionObject = resolveRawProjection(rawProjection)
231✔
1061
    const projection = removeAclColumnsFromRawProjection(rawProjectionObject, _acls)
231✔
1062

231✔
1063
    return !lisEmpty(projection) ? [projection] : []
231✔
1064
  } catch (errorMessage) {
231✔
1065
    log.error(errorMessage.message)
66✔
1066
    throw new BadRequestError(errorMessage.message)
66✔
1067
  }
66✔
1068
}
231✔
1069

94✔
1070
function splitACLs(acls) {
1,740✔
1071
  if (acls) { return acls.split(',') }
1,740✔
1072
  return []
1,564✔
1073
}
1,740✔
1074

94✔
1075
function removeAclColumns(fieldsInProjection, aclColumns) {
1,656✔
1076
  if (aclColumns.length > 0) {
1,656✔
1077
    return fieldsInProjection.filter(field => {
170✔
1078
      return aclColumns.indexOf(field) > -1
960✔
1079
    })
170✔
1080
  }
170✔
1081

1,486✔
1082
  return fieldsInProjection
1,486✔
1083
}
1,656✔
1084

94✔
1085
function removeAclColumnsFromRawProjection(rawProjectionObject, aclColumns) {
171✔
1086
  const isRawProjectionOverridingACLs = checkIfRawProjectionOverridesAcls(rawProjectionObject, aclColumns)
171✔
1087
  if (isRawProjectionOverridingACLs) {
171✔
1088
    throw Error('_rawp exclusive projection is overriding at least one acl_read_column value')
6✔
1089
  }
6✔
1090

165✔
1091
  const rawProjectionFields = Object.keys(rawProjectionObject)
165✔
1092
  const filteredFields = removeAclColumns(rawProjectionFields, aclColumns)
165✔
1093

165✔
1094
  return filteredFields.reduce((acc, current) => {
165✔
1095
    if (rawProjectionObject[current] === 0 || rawProjectionObject[current]) {
249✔
1096
      acc[current] = rawProjectionObject[current]
249✔
1097
    }
249✔
1098
    return acc
249✔
1099
  }, {})
165✔
1100
}
171✔
1101

94✔
1102
function getClientProjection(clientProjectionString) {
326✔
1103
  if (clientProjectionString) {
326✔
1104
    return clientProjectionString.split(',')
326✔
1105
  }
326✔
1106
  return []
×
1107
}
326✔
1108

94✔
1109
function resolveRawProjection(clientRawProjectionString) {
171✔
1110
  if (clientRawProjectionString) {
171✔
1111
    return JSON.parse(clientRawProjectionString)
171✔
1112
  }
171✔
1113
  return {}
×
1114
}
171✔
1115

94✔
1116
function checkAllowedOperators(rawProjection, projectionDictionary, additionalFields, log) {
231✔
1117
  if (!rawProjection) {
231!
1118
    log.debug('No raw projection found: checkAllowedOperators returns true')
×
1119
    return true
×
1120
  }
×
1121

231✔
1122
  const { allowedOperators, notAllowedOperators } = projectionDictionary
231✔
1123
  const allowedFields = [...allowedOperators]
231✔
1124

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

231✔
1127
  log.debug({ allowedOperators: allowedFields }, 'Allowed operators for projection')
231✔
1128
  log.debug({ notAllowedOperators }, 'Not allowed operators for projection')
231✔
1129

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

231✔
1134
  if (!matches) {
231✔
1135
    log.debug('No operators found in raw projection: checkAllowedOperators returns true')
93✔
1136
    return true
93✔
1137
  }
93✔
1138

138✔
1139
  return !matches.some(match => {
138✔
1140
    if (match.startsWith('$$')) {
336✔
1141
      log.debug({ match }, 'Found $$ match in raw projection')
96✔
1142
      if (notAllowedOperators.includes(match)) {
96✔
1143
        throw Error(`Operator ${match} is not allowed in raw projection`)
48✔
1144
      }
48✔
1145

48✔
1146
      return notAllowedOperators.includes(match)
48✔
1147
    }
48✔
1148

240✔
1149
    if (!allowedFields.includes(match)) {
336✔
1150
      throw Error(`Operator ${match} is not allowed in raw projection`)
12✔
1151
    }
12✔
1152

228✔
1153
    return !allowedFields.includes(match)
228✔
1154
  })
138✔
1155
}
231✔
1156

94✔
1157
function checkIfRawProjectionOverridesAcls(rawProjection, acls) {
171✔
1158
  return Object.keys(rawProjection).some(field =>
171✔
1159
    acls.includes(field) && rawProjection[field] === 0
351✔
1160
  )
171✔
1161
}
171✔
1162

94✔
1163
function mapToObjectWithOnlyId(doc) {
73✔
1164
  return { _id: doc._id.toString() }
73✔
1165
}
73✔
1166

94✔
1167
const internalFields = [
94✔
1168
  UPDATERID,
94✔
1169
  UPDATEDAT,
94✔
1170
  CREATORID,
94✔
1171
  CREATEDAT,
94✔
1172
  __STATE__,
94✔
1173
]
94✔
1174
function getEditableFields(aclWriteColumns, allFieldNames) {
472✔
1175
  const editableFields = aclWriteColumns ? aclWriteColumns.split(',') : allFieldNames
472!
1176
  return editableFields.filter(ef => !internalFields.includes(ef))
472✔
1177
}
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