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

mia-platform / crud-service / 5666458141

pending completion
5666458141

push

github

web-flow
feat: export collection (#137)

* feat(core): export in different file format

* docs(core): set response contentType swagger

* fix(core): error code on unsupported mimetype

* fix(core): export default mimetype

* breaking(core): changed the delimiter in csv export/import

* fix(core): changed the object parsing for export

* refactor(core): mime types stream

* test(core): fix broken test due to change on delimiter

* test(core): export

* refactor(core): mimeType stream api

* docs(core): default accept /export swagger

* test(core): expected schema

* deps(core): remove unused dependencies

* fix(core): removed unused code

* docs(core): readme

* fix(core): post rebase

* docs(core): fix swagger for import

1064 of 1188 branches covered (89.56%)

Branch coverage included in aggregate %.

42 of 42 new or added lines in 2 files covered. (100.0%)

2205 of 2277 relevant lines covered (96.84%)

20579.82 hits per line

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

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

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

19
'use strict'
20

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

25

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

30
const lisEmpty = require('lodash.isempty')
116✔
31

32

33
const {
34
  SORT,
35
  PROJECTION,
36
  RAW_PROJECTION,
37
  QUERY,
38
  LIMIT,
39
  SKIP,
40
  STATE,
41
  INVALID_USERID,
42
  UPDATERID,
43
  UPDATEDAT,
44
  CREATORID,
45
  CREATEDAT,
46
  __STATE__,
47
  SCHEMA_CUSTOM_KEYWORDS,
48
  rawProjectionDictionary,
49
} = require('./consts')
116✔
50

51
const BadRequestError = require('./BadRequestError')
116✔
52
const { SCHEMAS_ID } = require('./schemaGetters')
116✔
53
const { getAjvResponseValidationFunction, shouldValidateStream, shouldValidateItem } = require('./validatorGetters')
116✔
54
const BatchWritableStream = require('./BatchWritableStream')
116✔
55
const { getFileMimeParser, getFileMimeStringify } = require('./mimeTypeTransform')
116✔
56

57
const BAD_REQUEST_ERROR_STATUS_CODE = 400
116✔
58
const UNSUPPORTED_MIME_TYPE_STATUS_CODE = 415
116✔
59
const INTERNAL_SERVER_ERROR_STATUS_CODE = 500
116✔
60
const OPTIONS_INCOMPATIBILITY_ERROR_CODE = 2
116✔
61
const UNIQUE_INDEX_ERROR_STATUS_CODE = 422
116✔
62
const UNIQUE_INDEX_MONGO_ERROR_CODE = 11000
116✔
63

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

71

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

85
  const {
86
    registerGetters = true,
×
87
    registerSetters = true,
×
88
    registerLookup = false,
4,482✔
89
  } = options
4,486✔
90

91
  const validateOutput = fastify.validateOutput ?? false
4,486!
92

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

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

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

134
    // this is made to prevent to shows on swagger all properties with dot notation of RawObject with schema.
135
    return ajv.compile(subSchema)
152,780✔
136
  })
137

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

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

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

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

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

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

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

240
  if (registerLookup) {
4,486✔
241
    if (!fastify.lookupProjection) { throw new Error('`fastify.lookupProjection` is undefined') }
4!
242
    const listLookupSchema = fastify.jsonSchemaGenerator.generateGetListLookupJSONSchema()
4✔
243
    fastify.get('/', {
4✔
244
      schema: listLookupSchema,
245
      config: {
246
        streamValidator: shouldValidateStream(listLookupSchema.response['200'], validateOutput),
247
        replyType: () => 'application/json',
40✔
248
      },
249
    }, handleGetListLookup)
250
  }
251

252
  if (registerGetters) {
4,486✔
253
    const getItemJSONSchema = fastify.jsonSchemaGenerator.generateGetItemJSONSchema()
4,478✔
254
    fastify.get('/export', {
4,478✔
255
      schema: fastify.jsonSchemaGenerator.generateExportJSONSchema(),
256
      config: {
257
        streamValidator: shouldValidateStream(getItemJSONSchema.response['200'], validateOutput),
258
        replyType: (accept) => {
259
          if (!accept || accept === '*/*') { return 'application/x-ndjson' }
705!
260
          return accept
705✔
261
        },
262
      },
263
    }, handleGetList)
264
    fastify.get('/count', { schema: fastify.jsonSchemaGenerator.generateCountJSONSchema() }, handleCount)
4,478✔
265
    fastify.setNotFoundHandler(notFoundHandler)
4,478✔
266
    fastify.get('/', {
4,478✔
267
      schema: fastify.jsonSchemaGenerator.generateGetListJSONSchema(),
268
      config: {
269
        streamValidator: shouldValidateStream(getItemJSONSchema.response['200'], validateOutput),
270
        replyType: () => 'application/json',
423✔
271
      },
272
    }, handleGetList)
273
    fastify.get('/:id', {
4,478✔
274
      schema: getItemJSONSchema,
275
      config: {
276
        itemValidator: shouldValidateItem(getItemJSONSchema.response['200'], validateOutput),
277
      },
278
    }, handleGetId)
279
  }
280
}
281

282
// eslint-disable-next-line max-statements
283
async function handleCollectionImport(request, reply) {
284
  if (this.customMetrics) {
76!
285
    this.customMetrics.collectionInvocation.inc({
×
286
      collection_name: this.modelName,
287
      type: PROMETHEUS_OP_TYPE.IMPORT,
288
    })
289
  }
290

291
  if (!request.isMultipart()) {
76!
292
    return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, 'Request is not multipart')
×
293
  }
294

295
  const data = await request.file()
76✔
296
  if (!data) {
76!
297
    return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, 'Missing file')
×
298
  }
299
  const { file, mimetype, fields } = data
76✔
300
  const parsingOptions = Object.fromEntries(Object.values(fields)
76✔
301
    .filter(field => field.type === 'field')
124✔
302
    .map(({ fieldname, value }) => [fieldname, value]))
48✔
303

304
  const { itemValidator, validateImportOptions } = reply.context.config
76✔
305
  const isValid = validateImportOptions(parsingOptions)
76✔
306
  if (!isValid) {
76✔
307
    return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, `Invalid options`)
16✔
308
  }
309

310
  const bodyParser = getFileMimeParser(mimetype, parsingOptions)
60✔
311
  if (!bodyParser) {
60!
312
    return reply.getHttpError(UNSUPPORTED_MIME_TYPE_STATUS_CODE, `Unsupported file type ${mimetype}`)
×
313
  }
314

315
  const { crudService, queryParser } = this
60✔
316
  const { log, crudContext } = request
60✔
317

318
  let documentIndex = 0
60✔
319
  const parseDocument = through2.obj((chunk, _enc, callback) => {
60✔
320
    try {
148✔
321
      itemValidator(chunk)
148✔
322
      if (itemValidator.errors) { throw itemValidator.errors }
148✔
323
      queryParser.parseAndCastBody(chunk)
136✔
324
    } catch (error) {
325
      return callback(error, chunk)
12✔
326
    }
327
    documentIndex += 1
136✔
328
    return callback(null, chunk)
136✔
329
  })
330

331
  // POST
332
  let returnCode = 201
60✔
333
  let processBatch = async(batch) => crudService.insertMany(crudContext, batch)
60✔
334
  if (request.method === 'PATCH') {
60✔
335
    returnCode = 200
32✔
336
    processBatch = async(batch) => {
32✔
337
      return crudService.patchBulk(crudContext, batch.map(document => ({
64✔
338
        _id: document._id,
339
        commands: {
340
          $set: document,
341
        },
342
      })))
343
    }
344
  }
345

346
  const batchConsumer = new BatchWritableStream({
60✔
347
    batchSize: 5000,
348
    highWaterMark: 1000,
349
    objectMode: true,
350
    processBatch,
351
  })
352

353
  try {
60✔
354
    await pipeline(
60✔
355
      file,
356
      bodyParser(),
357
      parseDocument,
358
      batchConsumer
359
    )
360
  } catch (error) {
361
    if (error.code === OPTIONS_INCOMPATIBILITY_ERROR_CODE) {
16!
362
      log.debug('stream error')
×
363
      return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, error.message)
×
364
    }
365

366
    if (error.code === UNIQUE_INDEX_MONGO_ERROR_CODE) {
16✔
367
      log.debug('unique index violation')
4✔
368
      return reply.getHttpError(UNIQUE_INDEX_ERROR_STATUS_CODE, error.message)
4✔
369
    }
370

371
    if (Array.isArray(error)) {
12!
372
      log.debug('error parsing input file')
12✔
373
      const { message, instancePath } = error?.[0] ?? {}
12!
374
      const errorDetails = instancePath ? `, ${instancePath}` : ''
12✔
375
      const errorMessage = `(index: ${documentIndex}${errorDetails}) ${message ?? 'error in parsing record'}`
12!
376
      return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, errorMessage)
12✔
377
    }
378

379
    return reply.getHttpError(INTERNAL_SERVER_ERROR_STATUS_CODE, error.message || 'something went wrong')
×
380
  }
381

382
  return reply.code(returnCode).send({ message: 'File uploaded successfully' })
44✔
383
}
384

385
// eslint-disable-next-line max-statements
386
async function handleGetListLookup(request, reply) {
387
  if (this.customMetrics) {
40!
388
    this.customMetrics.collectionInvocation.inc({
40✔
389
      collection_name: this.modelName,
390
      type: PROMETHEUS_OP_TYPE.FETCH,
391
    })
392
  }
393

394
  const { query, headers, crudContext, log } = request
40✔
395

396
  const {
397
    [QUERY]: clientQueryString,
398
    [PROJECTION]: clientProjectionString = '',
32✔
399
    [SORT]: sortQuery,
400
    [LIMIT]: limit,
401
    [SKIP]: skip,
402
    [STATE]: state,
403
    ...otherParams
404
  } = query
40✔
405
  const { acl_rows, acl_read_columns } = headers
40✔
406

407
  let projection = resolveProjection(
40✔
408
    clientProjectionString,
409
    acl_read_columns,
410
    this.allFieldNames,
411
    '',
412
    log
413
  )
414

415
  projection = this.lookupProjection.filter(proj => projection.includes(Object.keys(proj)[0]))
120✔
416
  if (projection.length === 0) {
40✔
417
    reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, 'No allowed colums')
2✔
418
  }
419

420
  const LookupProjectionFieldsToOmit = this.lookupProjection.filter(field => Object.values(field).shift() === 0)
120✔
421
  projection.push(...LookupProjectionFieldsToOmit)
40✔
422

423
  const isTextSearchQuery = query._q && this.queryParser.isTextSearchQuery(JSON.parse(query._q))
40✔
424
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, isTextSearchQuery)
40✔
425
  let sort
426
  if (sortQuery) {
40✔
427
    sort = Object.fromEntries(sortQuery.toString().split(',')
4✔
428
      .map((param) => (param[0] === '-' ? [param.substr(1), -1] : [param, 1])))
4✔
429
  }
430

431
  const stateArr = state?.split(',')
40✔
432
  const { replyType, streamValidator } = reply.context.config
40✔
433
  const contentType = replyType()
40✔
434
  const responseParser = getFileMimeStringify(contentType)
40✔
435
  if (!responseParser) {
40!
436
    return reply.getHttpError(UNSUPPORTED_MIME_TYPE_STATUS_CODE, `Unsupported file type ${contentType}`)
×
437
  }
438

439
  reply.raw.setHeader('Content-Type', contentType)
40✔
440

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

460
async function handleGetList(request, reply) {
461
  if (this.customMetrics) {
1,128✔
462
    this.customMetrics.collectionInvocation.inc({
81✔
463
      collection_name: this.modelName,
464
      type: PROMETHEUS_OP_TYPE.FETCH,
465
    })
466
  }
467

468
  const { query, headers, crudContext, log } = request
1,128✔
469
  const {
470
    [QUERY]: clientQueryString,
471
    [PROJECTION]: clientProjectionString = '',
812✔
472
    [RAW_PROJECTION]: clientRawProjectionString = '',
901✔
473
    [SORT]: sortQuery,
474
    [LIMIT]: limit,
475
    [SKIP]: skip,
476
    [STATE]: state,
477
    ...otherParams
478
  } = query
1,128✔
479
  const { acl_rows, acl_read_columns, accept } = headers
1,128✔
480
  const { replyType, streamValidator } = reply.context.config
1,128✔
481
  const contentType = replyType(accept)
1,128✔
482

483
  const projection = resolveProjection(
1,128✔
484
    clientProjectionString,
485
    acl_read_columns,
486
    this.allFieldNames,
487
    clientRawProjectionString,
488
    log
489
  )
490

491
  const isTextSearchQuery = query._q && this.queryParser.isTextSearchQuery(JSON.parse(query._q))
1,044✔
492
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, isTextSearchQuery)
1,044✔
493

494
  let sort
495
  if (sortQuery) {
1,044✔
496
    sort = Object.fromEntries(sortQuery.toString().split(',')
130✔
497
      .map((param) => (param[0] === '-' ? [param.substr(1), -1] : [param, 1])))
242✔
498
  }
499

500
  const stateArr = state.split(',')
1,044✔
501

502
  const responseParser = getFileMimeStringify(contentType, {})
1,044✔
503
  if (!responseParser) {
1,044!
504
    return reply.getHttpError(UNSUPPORTED_MIME_TYPE_STATUS_CODE, `Unsupported file type ${contentType}`)
×
505
  }
506

507
  reply.raw.setHeader('Content-Type', contentType)
1,044✔
508

509
  try {
1,044✔
510
    await pipeline(
1,044✔
511
      this.crudService
512
        .findAll(crudContext, mongoQuery, projection, sort, skip, limit, stateArr, isTextSearchQuery)
513
        .stream(),
514
      this.castResultsAsStream(),
515
      streamValidator(),
516
      responseParser(),
517
      reply.raw
518
    )
519
  } catch (error) {
520
    request.log.error({ error }, 'Error during findAll stream')
4✔
521
    request.log.debug({ error: { ...error, message: error.message } }, 'Error during findAll stream with message')
4✔
522
    if (error.code === OPTIONS_INCOMPATIBILITY_ERROR_CODE) {
4!
523
      request.log.info(BAD_REQUEST_ERROR_STATUS_CODE)
×
524
    }
525
  }
526
}
527

528
async function handleGetId(request, reply) {
529
  if (this.customMetrics) {
491!
530
    this.customMetrics.collectionInvocation.inc({
×
531
      collection_name: this.modelName,
532
      type: PROMETHEUS_OP_TYPE.FETCH,
533
    })
534
  }
535

536
  const { crudContext, log } = request
491✔
537
  const docId = request.params.id
491✔
538
  const { acl_rows, acl_read_columns } = request.headers
491✔
539
  const {
540
    itemValidator,
541
  } = reply.context.config
491✔
542

543
  const {
544
    [QUERY]: clientQueryString,
545
    [PROJECTION]: clientProjectionString = '',
435✔
546
    [RAW_PROJECTION]: clientRawProjectionString = '',
448✔
547
    [STATE]: state,
548
    ...otherParams
549
  } = request.query
491✔
550

551
  const projection = resolveProjection(
491✔
552
    clientProjectionString,
553
    acl_read_columns,
554
    this.allFieldNames,
555
    clientRawProjectionString,
556
    log
557
  )
558
  const filter = resolveMongoQuery(
463✔
559
    this.queryParser,
560
    clientQueryString,
561
    acl_rows,
562
    otherParams,
563
    false
564
  )
565
  const _id = this.castCollectionId(docId)
463✔
566

567
  const stateArr = state.split(',')
463✔
568
  const doc = await this.crudService.findById(crudContext, _id, filter, projection, stateArr)
463✔
569
  if (!doc) {
463✔
570
    return reply.notFound()
104✔
571
  }
572

573
  const response = this.castItem(doc)
359✔
574
  itemValidator(response)
359✔
575
  return response
359✔
576
}
577

578
async function handleInsertOne(request, reply) {
579
  if (this.customMetrics) {
98✔
580
    this.customMetrics.collectionInvocation.inc({
14✔
581
      collection_name: this.modelName,
582
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
583
    })
584
  }
585

586
  const { body: doc, crudContext } = request
98✔
587

588
  this.queryParser.parseAndCastBody(doc)
98✔
589

590
  try {
98✔
591
    const insertedDoc = await this.crudService.insertOne(crudContext, doc)
98✔
592
    return mapToObjectWithOnlyId(insertedDoc)
94✔
593
  } catch (error) {
594
    if (error.code === UNIQUE_INDEX_MONGO_ERROR_CODE) {
4!
595
      request.log.error('unique index violation')
4✔
596
      return reply.getHttpError(UNIQUE_INDEX_ERROR_STATUS_CODE, error.message)
4✔
597
    }
598
    throw error
×
599
  }
600
}
601

602
async function handleValidate() {
603
  return { result: 'ok' }
4✔
604
}
605

606
async function handleDeleteId(request, reply) {
607
  if (this.customMetrics) {
70✔
608
    this.customMetrics.collectionInvocation.inc({
2✔
609
      collection_name: this.modelName,
610
      type: PROMETHEUS_OP_TYPE.DELETE,
611
    })
612
  }
613

614
  const { query, headers, params, crudContext } = request
70✔
615

616
  const docId = params.id
70✔
617
  const _id = this.castCollectionId(docId)
70✔
618

619
  const {
620
    [QUERY]: clientQueryString,
621
    [STATE]: state,
622
    ...otherParams
623
  } = query
70✔
624
  const { acl_rows } = headers
70✔
625

626
  const filter = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
70✔
627

628
  const stateArr = state.split(',')
70✔
629
  const doc = await this.crudService.deleteById(crudContext, _id, filter, stateArr)
70✔
630

631
  if (!doc) {
70✔
632
    return reply.notFound()
24✔
633
  }
634

635
  // the document should not be returned:
636
  // we don't know which projection the user is able to see
637
  reply.code(204)
46✔
638
}
639

640
async function handleDeleteList(request) {
641
  if (this.customMetrics) {
54✔
642
    this.customMetrics.collectionInvocation.inc({
2✔
643
      collection_name: this.modelName,
644
      type: PROMETHEUS_OP_TYPE.DELETE,
645
    })
646
  }
647

648
  const { query, headers, crudContext } = request
54✔
649

650
  const {
651
    [QUERY]: clientQueryString,
652
    [STATE]: state,
653
    ...otherParams
654
  } = query
54✔
655
  const { acl_rows } = headers
54✔
656

657
  const filter = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
54✔
658

659
  const stateArr = state.split(',')
54✔
660
  return this.crudService.deleteAll(crudContext, filter, stateArr)
54✔
661
}
662

663
async function handleCount(request) {
664
  if (this.customMetrics) {
56✔
665
    this.customMetrics.collectionInvocation.inc({
4✔
666
      collection_name: this.modelName,
667
      type: PROMETHEUS_OP_TYPE.FETCH,
668
    })
669
  }
670

671
  const { query, headers, crudContext } = request
56✔
672
  const {
673
    [QUERY]: clientQueryString,
674
    [STATE]: state,
675
    ...otherParams
676
  } = query
56✔
677
  const { acl_rows } = headers
56✔
678

679
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
56✔
680

681
  const stateArr = state.split(',')
56✔
682
  return this.crudService.count(crudContext, mongoQuery, stateArr)
56✔
683
}
684

685
async function handlePatchId(request, reply) {
686
  if (this.customMetrics) {
294✔
687
    this.customMetrics.collectionInvocation.inc({
2✔
688
      collection_name: this.modelName,
689
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
690
    })
691
  }
692

693
  const { query, headers, params, crudContext, log } = request
294✔
694
  const {
695
    [QUERY]: clientQueryString,
696
    [STATE]: state,
697
    ...otherParams
698
  } = query
294✔
699
  const {
700
    acl_rows,
701
    acl_write_columns: aclWriteColumns,
702
    acl_read_columns: aclColumns = '',
290✔
703
  } = headers
294✔
704
  const {
705
    itemValidator,
706
  } = reply.context.config
294✔
707

708
  const commands = request.body
294✔
709

710
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
294✔
711

712
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
294✔
713

714
  this.queryParser.parseAndCastCommands(commands, editableFields)
294✔
715
  const projection = resolveProjection('', aclColumns, this.allFieldNames, '', log)
294✔
716

717
  const docId = params.id
294✔
718
  const _id = this.castCollectionId(docId)
294✔
719

720
  const stateArr = state.split(',')
294✔
721
  const doc = await this.crudService.patchById(crudContext, _id, commands, mongoQuery, projection, stateArr)
294✔
722

723
  if (!doc) {
290✔
724
    return reply.notFound()
68✔
725
  }
726

727
  const response = this.castItem(doc)
222✔
728
  itemValidator(response)
222✔
729
  return response
222✔
730
}
731

732
async function handlePatchMany(request) {
733
  if (this.customMetrics) {
112!
734
    this.customMetrics.collectionInvocation.inc({
×
735
      collection_name: this.modelName,
736
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
737
    })
738
  }
739

740
  const { query, headers, crudContext } = request
112✔
741
  const {
742
    [QUERY]: clientQueryString,
743
    [STATE]: state,
744
    ...otherParams
745
  } = query
112✔
746
  const {
747
    acl_rows,
748
    acl_write_columns: aclWriteColumns,
749
  } = headers
112✔
750

751
  const commands = request.body
112✔
752
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
112✔
753
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
112✔
754
  this.queryParser.parseAndCastCommands(commands, editableFields)
112✔
755

756
  const stateArr = state.split(',')
112✔
757
  const nModified = await this.crudService.patchMany(crudContext, commands, mongoQuery, stateArr)
112✔
758

759
  return nModified
112✔
760
}
761

762
async function handleUpsertOne(request, reply) {
763
  if (this.customMetrics) {
88!
764
    this.customMetrics.collectionInvocation.inc({
×
765
      collection_name: this.modelName,
766
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
767
    })
768
  }
769

770
  const { query, headers, crudContext, log } = request
88✔
771
  const {
772
    [QUERY]: clientQueryString,
773
    [STATE]: state,
774
    ...otherParams
775
  } = query
88✔
776
  const {
777
    acl_rows,
778
    acl_write_columns: aclWriteColumns,
779
    acl_read_columns: aclColumns = '',
72✔
780
  } = headers
88✔
781
  const {
782
    itemValidator,
783
  } = reply.context.config
88✔
784

785
  const commands = request.body
88✔
786

787
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
88✔
788

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

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

794
  const stateArr = state.split(',')
88✔
795
  const doc = await this.crudService.upsertOne(crudContext, commands, mongoQuery, projection, stateArr)
88✔
796

797
  const response = this.castItem(doc)
88✔
798

799
  itemValidator(response)
88✔
800
  return response
88✔
801
}
802

803
async function handlePatchBulk(request) {
804
  if (this.customMetrics) {
120!
805
    this.customMetrics.collectionInvocation.inc({
×
806
      collection_name: this.modelName,
807
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
808
    })
809
  }
810

811
  const { body: filterUpdateCommands, crudContext, headers } = request
120✔
812

813
  const {
814
    acl_rows,
815
    acl_write_columns: aclWriteColumns,
816
  } = headers
120✔
817

818
  const parsedAndCastedCommands = new Array(filterUpdateCommands.length)
120✔
819
  for (let i = 0; i < filterUpdateCommands.length; i++) {
120✔
820
    const { filter, update } = filterUpdateCommands[i]
80,152✔
821
    const {
822
      _id,
823
      [QUERY]: clientQueryString,
824
      [STATE]: state,
825
      ...otherParams
826
    } = filter
80,152✔
827

828
    const commands = update
80,152✔
829

830
    const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
80,152✔
831

832
    const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
80,152✔
833

834
    this.queryParser.parseAndCastCommands(commands, editableFields)
80,152✔
835

836
    parsedAndCastedCommands[i] = {
80,152✔
837
      commands,
838
      state: state.split(','),
839
      query: mongoQuery,
840
    }
841
    if (_id) {
80,152✔
842
      parsedAndCastedCommands[i].query._id = this.castCollectionId(_id)
80,140✔
843
    }
844
  }
845

846
  const nModified = await this.crudService.patchBulk(crudContext, parsedAndCastedCommands)
120✔
847
  return nModified
120✔
848
}
849

850
async function handleInsertMany(request, reply) {
851
  if (this.customMetrics) {
80!
852
    this.customMetrics.collectionInvocation.inc({
×
853
      collection_name: this.modelName,
854
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
855
    })
856
  }
857

858
  const { body: docs, crudContext } = request
80✔
859

860
  docs.forEach(this.queryParser.parseAndCastBody)
80✔
861

862
  try {
80✔
863
    const insertedDocs = await this.crudService.insertMany(crudContext, docs)
80✔
864
    return insertedDocs.map(mapToObjectWithOnlyId)
76✔
865
  } catch (error) {
866
    if (error.code === UNIQUE_INDEX_MONGO_ERROR_CODE) {
4!
867
      request.log.error('unique index violation')
4✔
868
      return reply.getHttpError(UNIQUE_INDEX_ERROR_STATUS_CODE, error.message)
4✔
869
    }
870
    throw error
×
871
  }
872
}
873

874
async function handleChangeStateById(request, reply) {
875
  if (this.customMetrics) {
44!
876
    this.customMetrics.collectionInvocation.inc({
×
877
      collection_name: this.modelName,
878
      type: PROMETHEUS_OP_TYPE.CHANGE_STATE,
879
    })
880
  }
881

882
  const { body, crudContext, headers, query } = request
44✔
883
  const {
884
    [QUERY]: clientQueryString,
885
    ...otherParams
886
  } = query
44✔
887

888
  const { acl_rows } = headers
44✔
889
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
44✔
890

891
  const docId = request.params.id
44✔
892
  const _id = this.castCollectionId(docId)
44✔
893

894
  const doc = await this.crudService.changeStateById(crudContext, _id, body.stateTo, mongoQuery)
44✔
895

896
  if (!doc) {
44✔
897
    return reply.notFound()
16✔
898
  }
899

900
  reply.code(204)
28✔
901
}
902

903
async function handleChangeStateMany(request) {
904
  if (this.customMetrics) {
64!
905
    this.customMetrics.collectionInvocation.inc({
×
906
      collection_name: this.modelName,
907
      type: PROMETHEUS_OP_TYPE.CHANGE_STATE,
908
    })
909
  }
910

911
  const { body: filterUpdateCommands, crudContext, headers } = request
64✔
912

913
  const {
914
    acl_rows,
915
  } = headers
64✔
916

917
  const parsedAndCastedCommands = new Array(filterUpdateCommands.length)
64✔
918
  for (let i = 0; i < filterUpdateCommands.length; i++) {
64✔
919
    const {
920
      filter,
921
      stateTo,
922
    } = filterUpdateCommands[i]
80✔
923

924
    const mongoQuery = resolveMongoQuery(this.queryParser, null, acl_rows, filter, false)
80✔
925

926
    parsedAndCastedCommands[i] = {
80✔
927
      query: mongoQuery,
928
      stateTo,
929
    }
930
  }
931

932
  return this.crudService.changeStateMany(crudContext, parsedAndCastedCommands)
64✔
933
}
934

935
async function injectContextInRequest(request) {
936
  const userIdHeader = request.headers[this.userIdHeaderKey]
2,843✔
937
  const isUserHeaderInvalid = INVALID_USERID.includes(userIdHeader)
2,843✔
938

939
  let userId = 'public'
2,843✔
940

941
  if (userIdHeader && !isUserHeaderInvalid) {
2,843✔
942
    userId = userIdHeader
852✔
943
  }
944

945
  request.crudContext = {
2,843✔
946
    log: request.log,
947
    userId,
948
    now: new Date(),
949
  }
950
}
951

952
async function parseEncodedJsonQueryParams(logger, request) {
953
  if (request.headers.json_query_params_encoding) {
2,843!
954
    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.')
×
955
  }
956

957
  // TODO remove request.headers.json_query_params_encoding fallback in v7.0.0
958
  const jsonQueryParamsEncoding = request.headers['json-query-params-encoding'] || request.headers.json_query_params_encoding
2,843✔
959
  switch (jsonQueryParamsEncoding) {
2,843✔
960
  case 'base64': {
961
    const queryJsonFields = [QUERY, RAW_PROJECTION]
12✔
962
    for (const field of queryJsonFields) {
12✔
963
      if (request.query[field]) {
24✔
964
        request.query[field] = Buffer.from(request.query[field], jsonQueryParamsEncoding).toString()
12✔
965
      }
966
    }
967
    break
12✔
968
  }
969
  default: break
2,831✔
970
  }
971
}
972

973
async function notFoundHandler(request, reply) {
974
  reply
236✔
975
    .code(404)
976
    .send({
977
      error: 'not found',
978
    })
979
}
980

981
async function customErrorHandler(error, request, reply) {
982
  if (error.statusCode === 404) {
630✔
983
    return notFoundHandler(request, reply)
212✔
984
  }
985

986
  if (error.validation?.[0]?.message === 'must NOT have additional properties') {
418✔
987
    reply.code(error.statusCode)
144✔
988
    throw new Error(`${error.message}. Property "${error.validation[0].params.additionalProperty}" is not defined in validation schema`)
144✔
989
  }
990

991
  throw error
274✔
992
}
993

994
function resolveMongoQuery(
995
  queryParser,
996
  clientQueryString,
997
  rawAclRows,
998
  otherParams,
999
  textQuery
1000
) {
1001
  const mongoQuery = {
82,497✔
1002
    $and: [],
1003
  }
1004
  if (clientQueryString) {
82,497✔
1005
    const clientQuery = JSON.parse(clientQueryString)
741✔
1006
    mongoQuery.$and.push(clientQuery)
741✔
1007
  }
1008
  if (otherParams) {
82,497!
1009
    for (const key of Object.keys(otherParams)) {
82,497✔
1010
      const value = otherParams[key]
670✔
1011
      mongoQuery.$and.push({ [key]: value })
670✔
1012
    }
1013
  }
1014

1015
  if (rawAclRows) {
82,497✔
1016
    const aclRows = JSON.parse(rawAclRows)
370✔
1017
    if (rawAclRows[0] === '[') {
370✔
1018
      mongoQuery.$and.push({ $and: aclRows })
354✔
1019
    } else {
1020
      mongoQuery.$and.push(aclRows)
16✔
1021
    }
1022
  }
1023

1024
  if (textQuery) {
82,497✔
1025
    queryParser.parseAndCastTextSearchQuery(mongoQuery)
96✔
1026
  } else {
1027
    queryParser.parseAndCast(mongoQuery)
82,401✔
1028
  }
1029

1030
  if (mongoQuery.$and && !mongoQuery.$and.length) {
82,497✔
1031
    return { }
81,192✔
1032
  }
1033

1034
  return mongoQuery
1,305✔
1035
}
1036

1037
function resolveProjection(clientProjectionString, aclColumns, allFieldNames, rawProjection, log) {
1038
  log.debug('Resolving projections')
2,041✔
1039
  const acls = splitACLs(aclColumns)
2,041✔
1040

1041
  if (clientProjectionString && rawProjection) {
2,041✔
1042
    log.error('Use of both _p and _rawp is not permitted')
24✔
1043
    throw new BadRequestError(
24✔
1044
      'Use of both _rawp and _p parameter is not allowed')
1045
  }
1046

1047
  if (!clientProjectionString && !rawProjection) {
2,017✔
1048
    return removeAclColumns(allFieldNames, acls)
1,415✔
1049
  } else if (rawProjection) {
602✔
1050
    return resolveRawProjectionString(rawProjection, acls, allFieldNames, log)
246✔
1051
  } else if (clientProjectionString) {
356!
1052
    return resolveClientProjectionString(clientProjectionString, acls)
356✔
1053
  }
1054
}
1055

1056
function resolveClientProjectionString(clientProjectionString, _acls) {
1057
  const clientProjection = getClientProjection(clientProjectionString)
356✔
1058
  return removeAclColumns(clientProjection, _acls)
356✔
1059
}
1060

1061
function resolveRawProjectionString(rawProjection, _acls, allFieldNames, log) {
1062
  try {
246✔
1063
    checkAllowedOperators(
246✔
1064
      rawProjection,
1065
      rawProjectionDictionary,
1066
      _acls.length > 0 ? _acls : allFieldNames, log)
246✔
1067

1068
    const rawProjectionObject = resolveRawProjection(rawProjection)
166✔
1069
    const projection = removeAclColumnsFromRawProjection(rawProjectionObject, _acls)
166✔
1070

1071
    return !lisEmpty(projection) ? [projection] : []
158✔
1072
  } catch (errorMessage) {
1073
    log.error(errorMessage.message)
88✔
1074
    throw new BadRequestError(errorMessage.message)
88✔
1075
  }
1076
}
1077

1078
function splitACLs(acls) {
1079
  if (acls) { return acls.split(',') }
2,041✔
1080
  return []
1,838✔
1081
}
1082

1083
function removeAclColumns(fieldsInProjection, aclColumns) {
1084
  if (aclColumns.length > 0) {
1,929✔
1085
    return fieldsInProjection.filter(field => {
195✔
1086
      return aclColumns.indexOf(field) > -1
1,133✔
1087
    })
1088
  }
1089

1090
  return fieldsInProjection
1,734✔
1091
}
1092

1093
function removeAclColumnsFromRawProjection(rawProjectionObject, aclColumns) {
1094
  const isRawProjectionOverridingACLs = checkIfRawProjectionOverridesAcls(rawProjectionObject, aclColumns)
166✔
1095
  if (isRawProjectionOverridingACLs) {
166✔
1096
    throw Error('_rawp exclusive projection is overriding at least one acl_read_column value')
8✔
1097
  }
1098

1099
  const rawProjectionFields = Object.keys(rawProjectionObject)
158✔
1100
  const filteredFields = removeAclColumns(rawProjectionFields, aclColumns)
158✔
1101

1102
  return filteredFields.reduce((acc, current) => {
158✔
1103
    if (rawProjectionObject[current] === 0 || rawProjectionObject[current]) {
250!
1104
      acc[current] = rawProjectionObject[current]
250✔
1105
    }
1106
    return acc
250✔
1107
  }, {})
1108
}
1109

1110
function getClientProjection(clientProjectionString) {
1111
  if (clientProjectionString) {
356!
1112
    return clientProjectionString.split(',')
356✔
1113
  }
1114
  return []
×
1115
}
1116

1117
function resolveRawProjection(clientRawProjectionString) {
1118
  if (clientRawProjectionString) {
166!
1119
    return JSON.parse(clientRawProjectionString)
166✔
1120
  }
1121
  return {}
×
1122
}
1123

1124
function checkAllowedOperators(rawProjection, projectionDictionary, additionalFields, log) {
1125
  if (!rawProjection) {
246!
1126
    log.debug('No raw projection found: checkAllowedOperators returns true')
×
1127
    return true
×
1128
  }
1129

1130
  const { allowedOperators, notAllowedOperators } = projectionDictionary
246✔
1131
  const allowedFields = [...allowedOperators]
246✔
1132

1133
  additionalFields.forEach(field => allowedFields.push(`$${field}`))
4,280✔
1134

1135
  log.debug({ allowedOperators: allowedFields }, 'Allowed operators for projection')
246✔
1136
  log.debug({ notAllowedOperators }, 'Not allowed operators for projection')
246✔
1137

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

1142
  if (!matches) {
246✔
1143
    log.debug('No operators found in raw projection: checkAllowedOperators returns true')
103✔
1144
    return true
103✔
1145
  }
1146

1147
  return !matches.some(match => {
143✔
1148
    if (match.startsWith('$$')) {
316✔
1149
      log.debug({ match }, 'Found $$ match in raw projection')
103✔
1150
      if (notAllowedOperators.includes(match)) {
103✔
1151
        throw Error(`Operator ${match} is not allowed in raw projection`)
64✔
1152
      }
1153

1154
      return notAllowedOperators.includes(match)
39✔
1155
    }
1156

1157
    if (!allowedFields.includes(match)) {
213✔
1158
      throw Error(`Operator ${match} is not allowed in raw projection`)
16✔
1159
    }
1160

1161
    return !allowedFields.includes(match)
197✔
1162
  })
1163
}
1164

1165
function checkIfRawProjectionOverridesAcls(rawProjection, acls) {
1166
  return Object.keys(rawProjection).some(field =>
166✔
1167
    acls.includes(field) && rawProjection[field] === 0
364✔
1168
  )
1169
}
1170

1171
function mapToObjectWithOnlyId(doc) {
1172
  return { _id: doc._id.toString() }
200,242✔
1173
}
1174

1175
const internalFields = [
116✔
1176
  UPDATERID,
1177
  UPDATEDAT,
1178
  CREATORID,
1179
  CREATEDAT,
1180
  __STATE__,
1181
]
1182
function getEditableFields(aclWriteColumns, allFieldNames) {
1183
  const editableFields = aclWriteColumns ? aclWriteColumns.split(',') : allFieldNames
80,646!
1184
  return editableFields.filter(ef => !internalFields.includes(ef))
1,693,392✔
1185
}
1186

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc