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

mia-platform / crud-service / 13366462928

17 Feb 2025 08:58AM UTC coverage: 97.209% (+0.02%) from 97.19%
13366462928

push

github

danibix95
7.2.3-rc.1

1440 of 1546 branches covered (93.14%)

Branch coverage included in aggregate %.

9321 of 9524 relevant lines covered (97.87%)

7869.71 hits per line

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

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

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

96✔
19
'use strict'
96✔
20

96✔
21
const { pipeline } = require('stream/promises')
96✔
22
const { isEmpty: lisEmpty } = require('lodash')
96✔
23
const through2 = require('through2')
96✔
24

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

96✔
50
const { getReplyTypeCallback } = require('./acceptHeaderParser')
96✔
51
const BadRequestError = require('./BadRequestError')
96✔
52
const BatchWritableStream = require('./BatchWritableStream')
96✔
53

96✔
54
const { getAjvResponseValidationFunction, shouldValidateStream, shouldValidateItem } = require('./validatorGetters')
96✔
55
const { getFileMimeParser, getFileMimeStringifiers } = require('./mimeTypeTransform')
96✔
56
const resolveMongoQuery = require('./resolveMongoQuery')
96✔
57
const { addValidatorCompiler } = require('./compilers')
96✔
58

96✔
59
const OPTIONS_INCOMPATIBILITY_ERROR_CODE = 2
96✔
60
const UNIQUE_INDEX_MONGO_ERROR_CODE = 11000
96✔
61

96✔
62
const PROMETHEUS_OP_TYPE = {
96✔
63
  FETCH: 'fetch',
96✔
64
  INSERT_OR_UPDATE: 'insert_or_update',
96✔
65
  DELETE: 'delete',
96✔
66
  CHANGE_STATE: 'change_state',
96✔
67
}
96✔
68

96✔
69

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

4,500✔
83
  const {
4,500✔
84
    registerGetters = true,
4,500✔
85
    registerSetters = true,
4,500✔
86
    registerLookup = false,
4,500✔
87
  } = options
4,500✔
88

4,500✔
89
  const validateOutput = fastify.validateOutput ?? false
4,500!
90

4,500✔
91

4,500✔
92
  addValidatorCompiler(fastify, fastify.models, { HELPERS_PREFIX: fastify.config.HELPERS_PREFIX })
4,500✔
93

4,500✔
94
  if (registerSetters) {
4,500✔
95
    fastify.post(
3,846✔
96
      '/',
3,846✔
97
      { schema: fastify.jsonSchemaGenerator.generatePostJSONSchema() },
3,846✔
98
      handleInsertOne
3,846✔
99
    )
3,846✔
100
    fastify.post(
3,846✔
101
      '/validate',
3,846✔
102
      { schema: fastify.jsonSchemaGenerator.generateValidateJSONSchema() },
3,846✔
103
      handleValidate
3,846✔
104
    )
3,846✔
105
    fastify.delete(
3,846✔
106
      '/:id',
3,846✔
107
      { schema: fastify.jsonSchemaGenerator.generateDeleteJSONSchema() },
3,846✔
108
      handleDeleteId
3,846✔
109
    )
3,846✔
110
    fastify.delete(
3,846✔
111
      '/',
3,846✔
112
      { schema: fastify.jsonSchemaGenerator.generateDeleteListJSONSchema() },
3,846✔
113
      handleDeleteList
3,846✔
114
    )
3,846✔
115

3,846✔
116
    const patchIdSchema = fastify.jsonSchemaGenerator.generatePatchJSONSchema()
3,846✔
117
    fastify.patch(
3,846✔
118
      '/:id',
3,846✔
119
      {
3,846✔
120
        schema: patchIdSchema,
3,846✔
121
        config: {
3,846✔
122
          itemValidator: shouldValidateItem(patchIdSchema.response['200'], validateOutput),
3,846✔
123
        },
3,846✔
124
      },
3,846✔
125
      handlePatchId
3,846✔
126
    )
3,846✔
127
    fastify.patch(
3,846✔
128
      '/',
3,846✔
129
      { schema: fastify.jsonSchemaGenerator.generatePatchManyJSONSchema() },
3,846✔
130
      handlePatchMany
3,846✔
131
    )
3,846✔
132

3,846✔
133
    const upsertOneSchema = fastify.jsonSchemaGenerator.generateUpsertOneJSONSchema()
3,846✔
134
    fastify.post(
3,846✔
135
      '/upsert-one', {
3,846✔
136
        schema: upsertOneSchema,
3,846✔
137
        config: {
3,846✔
138
          itemValidator: shouldValidateItem(upsertOneSchema.response['200'], validateOutput),
3,846✔
139
        },
3,846✔
140
      },
3,846✔
141
      handleUpsertOne
3,846✔
142
    )
3,846✔
143

3,846✔
144
    fastify.post('/bulk', {
3,846✔
145
      schema: fastify.jsonSchemaGenerator.generateBulkJSONSchema(),
3,846✔
146
    }, handleInsertMany)
3,846✔
147
    fastify.patch('/bulk', {
3,846✔
148
      schema: fastify.jsonSchemaGenerator.generatePatchBulkJSONSchema(),
3,846✔
149
    }, handlePatchBulk)
3,846✔
150
    fastify.post(
3,846✔
151
      '/:id/state',
3,846✔
152
      { schema: fastify.jsonSchemaGenerator.generateChangeStateJSONSchema() },
3,846✔
153
      handleChangeStateById
3,846✔
154
    )
3,846✔
155
    fastify.post(
3,846✔
156
      '/state',
3,846✔
157
      { schema: fastify.jsonSchemaGenerator.generateChangeStateManyJSONSchema() },
3,846✔
158
      handleChangeStateMany
3,846✔
159
    )
3,846✔
160

3,846✔
161
    const importPostSchema = fastify.jsonSchemaGenerator.generatePostImportJSONSchema()
3,846✔
162
    fastify.post(
3,846✔
163
      '/import',
3,846✔
164
      {
3,846✔
165
        schema: importPostSchema,
3,846✔
166
        config: {
3,846✔
167
          itemValidator: getAjvResponseValidationFunction(importPostSchema.streamBody),
3,846✔
168
          validateImportOptions: getAjvResponseValidationFunction(importPostSchema.optionSchema,
3,846✔
169
            true
3,846✔
170
          ),
3,846✔
171
        },
3,846✔
172
      },
3,846✔
173
      handleCollectionImport
3,846✔
174
    )
3,846✔
175

3,846✔
176
    const importPatchSchema = fastify.jsonSchemaGenerator.generatePatchImportJSONSchema()
3,846✔
177
    fastify.patch(
3,846✔
178
      '/import',
3,846✔
179
      {
3,846✔
180
        schema: importPatchSchema,
3,846✔
181
        config: {
3,846✔
182
          itemValidator: getAjvResponseValidationFunction(importPatchSchema.streamBody),
3,846✔
183
          validateImportOptions: getAjvResponseValidationFunction(importPatchSchema.optionSchema,
3,846✔
184
            true
3,846✔
185
          ),
3,846✔
186
        },
3,846✔
187
      },
3,846✔
188
      handleCollectionImport
3,846✔
189
    )
3,846✔
190

3,846✔
191
    fastify.log.debug({ collection: fastify?.modelName }, 'setters endpoints registered')
3,846✔
192
  }
3,846✔
193

4,500✔
194
  if (registerLookup) {
4,500✔
195
    if (!fastify.lookupProjection) { throw new Error('`fastify.lookupProjection` is undefined') }
18!
196
    const listLookupSchema = fastify.jsonSchemaGenerator.generateGetListLookupJSONSchema()
18✔
197
    fastify.get('/', {
18✔
198
      schema: listLookupSchema,
18✔
199
      config: {
18✔
200
        streamValidator: shouldValidateStream(listLookupSchema.response['200'], validateOutput),
18✔
201
        replyType: () => 'application/json',
18✔
202
      },
18✔
203
    }, handleGetListLookup)
18✔
204
    fastify.log.debug({ collection: fastify?.modelName }, 'lookup endpoint registered')
18✔
205
  }
18✔
206

4,500✔
207
  if (registerGetters) {
4,500✔
208
    const getItemJSONSchemaWithoutRequired = fastify.jsonSchemaGenerator.generateGetItemJSONSchema()
4,464✔
209
    const getItemJSONSchemaWithRequired = fastify.jsonSchemaGenerator.generateGetItemJSONSchema(true)
4,464✔
210
    const defaultAccept = 'application/x-ndjson'
4,464✔
211

4,464✔
212
    fastify.get('/export', {
4,464✔
213
      schema: fastify.jsonSchemaGenerator.generateExportJSONSchema(defaultAccept),
4,464✔
214
      config: {
4,464✔
215
        streamValidator: shouldValidateStream(getItemJSONSchemaWithoutRequired.response['200'], validateOutput),
4,464✔
216
        replyType: getReplyTypeCallback(defaultAccept),
4,464✔
217
      },
4,464✔
218
    }, handleGetList)
4,464✔
219
    fastify.get('/count', { schema: fastify.jsonSchemaGenerator.generateCountJSONSchema() }, handleCount)
4,464✔
220
    fastify.get(
4,464✔
221
      '/schema',
4,464✔
222
      {
4,464✔
223
        schema: fastify.jsonSchemaGenerator.generateGetSchemaJSONSchema(),
4,464✔
224
      },
4,464✔
225
      () => ({
4,464✔
226
        type: getItemJSONSchemaWithRequired.response['200'].type,
3✔
227
        properties: getItemJSONSchemaWithRequired.response['200'].properties,
3✔
228
        required: getItemJSONSchemaWithRequired.response['200'].required,
3✔
229
      })
3✔
230
    )
4,464✔
231

4,464✔
232
    fastify.get('/', {
4,464✔
233
      schema: fastify.jsonSchemaGenerator.generateGetListJSONSchema(),
4,464✔
234
      config: {
4,464✔
235
        streamValidator: shouldValidateStream(getItemJSONSchemaWithoutRequired.response['200'], validateOutput),
4,464✔
236
        replyType: () => 'application/json',
4,464✔
237
      },
4,464✔
238
    }, handleGetList)
4,464✔
239
    fastify.get('/:id', {
4,464✔
240
      schema: getItemJSONSchemaWithoutRequired,
4,464✔
241
      config: {
4,464✔
242
        itemValidator: shouldValidateItem(getItemJSONSchemaWithoutRequired.response['200'], validateOutput),
4,464✔
243
      },
4,464✔
244
    }, handleGetId)
4,464✔
245

4,464✔
246
    fastify.log.debug({ collection: fastify?.modelName }, 'getters endpoints registered')
4,464✔
247
  }
4,464✔
248
}
4,500✔
249

96✔
250
// eslint-disable-next-line max-statements
96✔
251
async function handleCollectionImport(request, reply) {
72✔
252
  if (this.customMetrics) {
72!
253
    this.customMetrics.collectionInvocation.inc({
×
254
      collection_name: this.modelName,
×
255
      type: PROMETHEUS_OP_TYPE.IMPORT,
×
256
    })
×
257
  }
×
258

72✔
259
  if (!request.isMultipart()) {
72!
260
    return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, 'Request is not multipart')
×
261
  }
×
262

72✔
263
  const data = await request.file()
72✔
264
  if (!data) {
72!
265
    return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, 'Missing file')
×
266
  }
×
267
  const { file, mimetype, fields } = data
72✔
268
  const parsingOptions = Object.fromEntries(Object.values(fields)
72✔
269
    .filter(field => field.type === 'field')
72✔
270
    .map(({ fieldname, value }) => [fieldname, value]))
72✔
271

72✔
272
  const {
72✔
273
    log,
72✔
274
    crudContext,
72✔
275
    routeOptions: { config: { itemValidator, validateImportOptions } },
72✔
276
  } = request
72✔
277
  const isValid = validateImportOptions(parsingOptions)
72✔
278
  if (!isValid) {
72✔
279
    return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, `Invalid options`)
12✔
280
  }
12✔
281

60✔
282
  const bodyParser = getFileMimeParser(mimetype, parsingOptions)
60✔
283
  if (!bodyParser) {
72!
284
    return reply.getHttpError(UNSUPPORTED_MIME_TYPE_STATUS_CODE, `Unsupported file type ${mimetype}`)
×
285
  }
×
286

60✔
287
  const { crudService, queryParser } = this
60✔
288

60✔
289
  let documentIndex = 0
60✔
290
  const parseDocument = through2.obj((chunk, _enc, callback) => {
60✔
291
    try {
144✔
292
      itemValidator(chunk)
144✔
293
      if (itemValidator.errors) { throw itemValidator.errors }
144✔
294
    } catch (error) {
144✔
295
      return callback(error, chunk)
6✔
296
    }
6✔
297
    documentIndex += 1
138✔
298
    return callback(null, chunk)
138✔
299
  })
60✔
300

60✔
301
  // POST
60✔
302
  let returnCode = 201
60✔
303
  let processBatch = async(batch) => crudService.insertMany(crudContext, batch, queryParser)
60✔
304

60✔
305
  // PATCH
60✔
306
  if (request.method === 'PATCH') {
66✔
307
    returnCode = 200
39✔
308
    processBatch = async(batch) => {
39✔
309
      return crudService.upsertMany(crudContext, batch, queryParser)
36✔
310
    }
36✔
311
  }
39✔
312

60✔
313
  const batchConsumer = new BatchWritableStream({
60✔
314
    batchSize: 5000,
60✔
315
    highWaterMark: 1000,
60✔
316
    objectMode: true,
60✔
317
    processBatch,
60✔
318
  })
60✔
319

60✔
320
  const ac = new AbortController()
60✔
321
  const { signal } = ac
60✔
322

60✔
323
  // ensure that the pipeline is destroyed
60✔
324
  // in case the response stream is destroyed
60✔
325
  file.on('error', () => ac.abort())
60✔
326

60✔
327
  try {
60✔
328
    await pipeline(
60✔
329
      file,
60✔
330
      bodyParser(),
60✔
331
      parseDocument,
60✔
332
      batchConsumer,
60✔
333
      { signal }
60✔
334
    )
60✔
335
  } catch (error) {
72✔
336
    if (error.code === OPTIONS_INCOMPATIBILITY_ERROR_CODE) {
9!
337
      log.debug('stream error')
×
338
      return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, error.message)
×
339
    }
×
340

9✔
341
    if (error.code === UNIQUE_INDEX_MONGO_ERROR_CODE) {
9✔
342
      log.debug('unique index violation')
3✔
343
      return reply.getHttpError(UNIQUE_INDEX_ERROR_STATUS_CODE, error.message)
3✔
344
    }
3✔
345

6✔
346
    if (Array.isArray(error)) {
6✔
347
      log.debug('error parsing input file')
6✔
348
      const { message, instancePath } = error?.[0] ?? {}
6!
349
      const errorDetails = instancePath ? `, ${instancePath}` : ''
6!
350
      const errorMessage = `(index: ${documentIndex}${errorDetails}) ${message ?? 'error in parsing record'}`
6!
351
      return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, errorMessage)
6✔
352
    }
6✔
353

×
354
    return reply.getHttpError(INTERNAL_SERVER_ERROR_STATUS_CODE, error.message || 'something went wrong')
9✔
355
  }
9✔
356

51✔
357
  return reply.code(returnCode).send({ message: 'File uploaded successfully' })
51✔
358
}
72✔
359

96✔
360
function getExportColumns(projection) {
1,221✔
361
  const columns = Object.keys(projection).filter(key => projection[key] !== 0)
1,221✔
362
  if (!columns.includes('_id') && projection['_id'] !== 0) {
1,221✔
363
    columns.unshift('_id')
375✔
364
  }
375✔
365
  return columns
1,221✔
366
}
1,221✔
367

96✔
368
// eslint-disable-next-line max-statements
96✔
369
async function handleGetListLookup(request, reply) {
60✔
370
  if (this.customMetrics) {
60✔
371
    this.customMetrics.collectionInvocation.inc({
60✔
372
      collection_name: this.modelName,
60✔
373
      type: PROMETHEUS_OP_TYPE.FETCH,
60✔
374
    })
60✔
375
  }
60✔
376

60✔
377
  const {
60✔
378
    query,
60✔
379
    headers,
60✔
380
    crudContext,
60✔
381
    log,
60✔
382
    routeOptions: { config: { replyType, streamValidator } },
60✔
383
  } = request
60✔
384

60✔
385
  const {
60✔
386
    [QUERY]: clientQueryString,
60✔
387
    [PROJECTION]: clientProjectionString = '',
60✔
388
    [SORT]: sortQuery,
60✔
389
    [LIMIT]: limit,
60✔
390
    [SKIP]: skip,
60✔
391
    [STATE]: state,
60✔
392
    [EXPORT_OPTIONS]: exportOpts = '',
60✔
393
    ...otherParams
60✔
394
  } = query
60✔
395
  const { acl_rows, acl_read_columns } = headers
60✔
396

60✔
397
  let projection = resolveProjection(
60✔
398
    clientProjectionString,
60✔
399
    acl_read_columns,
60✔
400
    this.allFieldNames,
60✔
401
    '',
60✔
402
    log
60✔
403
  )
60✔
404
  delete projection._id
60✔
405

60✔
406
  projection = this.lookupProjection.reduce((acc, proj) => {
60✔
407
    if (projection[Object.keys(proj)[0]]) {
180✔
408
      return { ...acc, ...proj }
102✔
409
    }
102✔
410
    return acc
78✔
411
  }, {})
60✔
412
  if (Object.keys(projection).length === 0) {
60✔
413
    reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, 'No allowed colums')
3✔
414
  }
3✔
415

60✔
416
  const lookupProjectionFieldsToOmit = this.lookupProjection.reduce((acc, field) => {
60✔
417
    if (Object.values(field).shift() === 0) {
180✔
418
      return { ...acc, ...field }
60✔
419
    }
60✔
420
    return acc
120✔
421
  },
60✔
422
  {})
60✔
423
  projection = {
60✔
424
    ...projection,
60✔
425
    ...lookupProjectionFieldsToOmit,
60✔
426
  }
60✔
427

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

60✔
436
  const stateArr = state?.split(',')
60✔
437
  const contentType = replyType()
60✔
438
  const parsingOptions = contentType === 'text/csv' && exportOpts ? JSON.parse(exportOpts) : {}
60!
439

60✔
440
  const responseStringifiers = getFileMimeStringifiers(contentType, parsingOptions)
60✔
441
  if (!responseStringifiers) {
60!
442
    return reply.getHttpError(UNSUPPORTED_MIME_TYPE_STATUS_CODE, `Unsupported file type ${contentType}`)
×
443
  }
×
444

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

60✔
447
  const ac = new AbortController()
60✔
448
  const { signal } = ac
60✔
449

60✔
450
  const dataStream = this.crudService
60✔
451
    .aggregate(crudContext, mongoQuery, projection, sort, skip, limit, stateArr, isTextSearchQuery)
60✔
452
    .stream()
60✔
453
  // ensure that the pipeline is destroyed
60✔
454
  // in case the response stream is destroyed
60✔
455
  dataStream.on('error', () => ac.abort())
60✔
456

60✔
457
  try {
60✔
458
    return await pipeline(
60✔
459
      dataStream,
60✔
460
      this.castResultsAsStream(),
60✔
461
      streamValidator(),
60✔
462
      ...responseStringifiers({ fields: getExportColumns(projection) }),
60✔
463
      reply.raw,
60✔
464
      { signal }
60✔
465
    )
60✔
466
  } catch (error) {
60✔
467
    request.log.error({ error }, 'Error during findAll lookup stream')
3✔
468
    request.log.debug({ error: { ...error, message: error.message } }, 'Error during findAll lookup stream with message')
3✔
469
    if (error.code === OPTIONS_INCOMPATIBILITY_ERROR_CODE) {
3!
470
      request.log.info(BAD_REQUEST_ERROR_STATUS_CODE)
×
471
    }
×
472
  }
3✔
473
}
60✔
474

96✔
475
// eslint-disable-next-line max-statements
96✔
476
async function handleGetList(request, reply) {
1,389✔
477
  if (this.customMetrics) {
1,389✔
478
    this.customMetrics.collectionInvocation.inc({
72✔
479
      collection_name: this.modelName,
72✔
480
      type: PROMETHEUS_OP_TYPE.FETCH,
72✔
481
    })
72✔
482
  }
72✔
483

1,389✔
484
  const {
1,389✔
485
    query,
1,389✔
486
    headers,
1,389✔
487
    crudContext,
1,389✔
488
    log,
1,389✔
489
    routeOptions: { config: { replyType, streamValidator } },
1,389✔
490
  } = request
1,389✔
491
  const {
1,389✔
492
    [QUERY]: clientQueryString,
1,389✔
493
    [PROJECTION]: clientProjectionString = '',
1,389✔
494
    [RAW_PROJECTION]: clientRawProjectionString = '',
1,389✔
495
    [SORT]: sortQuery,
1,389✔
496
    [LIMIT]: limit,
1,389✔
497
    [SKIP]: skip,
1,389✔
498
    [STATE]: state,
1,389✔
499
    [EXPORT_OPTIONS]: exportOpts = '',
1,389✔
500
    ...otherParams
1,389✔
501
  } = query
1,389✔
502
  const { acl_rows, acl_read_columns, accept } = headers
1,389✔
503
  const contentType = replyType(accept)
1,389✔
504
  const parsingOptions = contentType === 'text/csv' && exportOpts ? JSON.parse(exportOpts) : {}
1,389✔
505

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

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

1,227✔
519
  const isTextSearchQuery = query._q && this.queryParser.isTextSearchQuery(JSON.parse(query._q))
1,389✔
520
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, isTextSearchQuery)
1,389✔
521

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

1,161✔
528
  const stateArr = state.split(',')
1,161✔
529

1,161✔
530
  reply.raw.setHeader('Content-Type', contentType)
1,161✔
531

1,161✔
532
  const ac = new AbortController()
1,161✔
533
  const { signal } = ac
1,161✔
534

1,161✔
535
  const dataStream = this.crudService
1,161✔
536
    .findAll(crudContext, mongoQuery, projection, sort, skip, limit, stateArr, isTextSearchQuery)
1,161✔
537
    .stream()
1,161✔
538

1,161✔
539
  // ensure that the pipeline is destroyed
1,161✔
540
  // in case the response stream is destroyed
1,161✔
541
  dataStream.on('error', () => ac.abort())
1,161✔
542

1,161✔
543
  try {
1,161✔
544
    await pipeline(
1,161✔
545
      dataStream,
1,161✔
546
      this.castResultsAsStream(),
1,161✔
547
      streamValidator(),
1,161✔
548
      ...responseStringifiers({ fields: getExportColumns(projection) }),
1,161✔
549
      reply.raw,
1,161✔
550
      { signal }
1,161✔
551
    )
1,161✔
552
  } catch (error) {
1,389✔
553
    request.log.error({ error }, 'Error during findAll stream')
3✔
554
    request.log.debug({ error: { ...error, message: error.message } }, 'Error during findAll stream with message')
3✔
555
    if (error.code === OPTIONS_INCOMPATIBILITY_ERROR_CODE) {
3!
556
      request.log.info(BAD_REQUEST_ERROR_STATUS_CODE)
×
557
    }
×
558
  }
3✔
559
}
1,389✔
560

96✔
561
async function handleGetId(request, reply) {
375✔
562
  if (this.customMetrics) {
375!
563
    this.customMetrics.collectionInvocation.inc({
×
564
      collection_name: this.modelName,
×
565
      type: PROMETHEUS_OP_TYPE.FETCH,
×
566
    })
×
567
  }
×
568

375✔
569
  const {
375✔
570
    crudContext,
375✔
571
    log,
375✔
572
  } = request
375✔
573
  const docId = request.params.id
375✔
574
  const { acl_rows, acl_read_columns } = request.headers
375✔
575

375✔
576
  const {
375✔
577
    [QUERY]: clientQueryString,
375✔
578
    [PROJECTION]: clientProjectionString = '',
375✔
579
    [RAW_PROJECTION]: clientRawProjectionString = '',
375✔
580
    [STATE]: state,
375✔
581
    ...otherParams
375✔
582
  } = request.query
375✔
583

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

375✔
600
  const stateArr = state.split(',')
375✔
601
  const doc = await this.crudService.findById(crudContext, _id, filter, projection, stateArr)
375✔
602
  if (!doc) {
375✔
603
    return reply.notFound()
78✔
604
  }
78✔
605

276✔
606
  return doc
276✔
607
}
375✔
608

96✔
609
async function handleInsertOne(request, reply) {
78✔
610
  if (this.customMetrics) {
78✔
611
    this.customMetrics.collectionInvocation.inc({
15✔
612
      collection_name: this.modelName,
15✔
613
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
15✔
614
    })
15✔
615
  }
15✔
616

78✔
617
  const { body: doc, crudContext } = request
78✔
618

78✔
619
  this.queryParser.parseAndCastBody(doc)
78✔
620

78✔
621
  try {
78✔
622
    const insertedDoc = await this.crudService.insertOne(crudContext, doc)
78✔
623
    return mapToObjectWithOnlyId(insertedDoc)
75✔
624
  } catch (error) {
78✔
625
    if (error.code === UNIQUE_INDEX_MONGO_ERROR_CODE) {
3✔
626
      request.log.error('unique index violation')
3✔
627
      return reply.getHttpError(UNIQUE_INDEX_ERROR_STATUS_CODE, error.message)
3✔
628
    }
3✔
629
    throw error
×
630
  }
×
631
}
78✔
632

96✔
633
async function handleValidate() {
3✔
634
  return { result: 'ok' }
3✔
635
}
3✔
636

96✔
637
async function handleDeleteId(request, reply) {
57✔
638
  if (this.customMetrics) {
57✔
639
    this.customMetrics.collectionInvocation.inc({
3✔
640
      collection_name: this.modelName,
3✔
641
      type: PROMETHEUS_OP_TYPE.DELETE,
3✔
642
    })
3✔
643
  }
3✔
644

57✔
645
  const { query, headers, params, crudContext } = request
57✔
646

57✔
647
  const docId = params.id
57✔
648
  const _id = this.castCollectionId(docId)
57✔
649

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

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

57✔
659
  const stateArr = state.split(',')
57✔
660
  const doc = await this.crudService.deleteById(crudContext, _id, filter, stateArr)
57✔
661

54✔
662
  if (!doc) {
57✔
663
    return reply.notFound()
18✔
664
  }
18✔
665

36✔
666
  // the document should not be returned:
36✔
667
  // we don't know which projection the user is able to see
36✔
668
  reply.code(204)
36✔
669
}
57✔
670

96✔
671
async function handleDeleteList(request) {
42✔
672
  if (this.customMetrics) {
42✔
673
    this.customMetrics.collectionInvocation.inc({
3✔
674
      collection_name: this.modelName,
3✔
675
      type: PROMETHEUS_OP_TYPE.DELETE,
3✔
676
    })
3✔
677
  }
3✔
678

42✔
679
  const { query, headers, crudContext } = request
42✔
680

42✔
681
  const {
42✔
682
    [QUERY]: clientQueryString,
42✔
683
    [STATE]: state,
42✔
684
    ...otherParams
42✔
685
  } = query
42✔
686
  const { acl_rows } = headers
42✔
687

42✔
688
  const filter = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
42✔
689

42✔
690
  const stateArr = state.split(',')
42✔
691
  return this.crudService.deleteAll(crudContext, filter, stateArr)
42✔
692
}
42✔
693

96✔
694
async function handleCount(request) {
51✔
695
  if (this.customMetrics) {
51✔
696
    this.customMetrics.collectionInvocation.inc({
3✔
697
      collection_name: this.modelName,
3✔
698
      type: PROMETHEUS_OP_TYPE.FETCH,
3✔
699
    })
3✔
700
  }
3✔
701

51✔
702
  const { query, headers, crudContext } = request
51✔
703
  const {
51✔
704
    [QUERY]: clientQueryString,
51✔
705
    [STATE]: state,
51✔
706
    [USE_ESTIMATE]: useEstimate,
51✔
707
    ...otherParams
51✔
708
  } = query
51✔
709

51✔
710
  const { acl_rows } = headers
51✔
711

51✔
712
  if (useEstimate) {
51✔
713
    return this.crudService.estimatedDocumentCount(crudContext)
6✔
714
  }
6✔
715

45✔
716
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
45✔
717
  const stateArr = state.split(',')
45✔
718

45✔
719
  return this.crudService.count(crudContext, mongoQuery, stateArr)
45✔
720
}
51✔
721

96✔
722
async function handlePatchId(request, reply) {
228✔
723
  if (this.customMetrics) {
228✔
724
    this.customMetrics.collectionInvocation.inc({
6✔
725
      collection_name: this.modelName,
6✔
726
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
6✔
727
    })
6✔
728
  }
6✔
729

228✔
730
  const {
228✔
731
    query,
228✔
732
    headers,
228✔
733
    params,
228✔
734
    crudContext,
228✔
735
    log,
228✔
736
  } = request
228✔
737

228✔
738
  const {
228✔
739
    [QUERY]: clientQueryString,
228✔
740
    [STATE]: state,
228✔
741
    ...otherParams
228✔
742
  } = query
228✔
743
  const {
228✔
744
    acl_rows,
228✔
745
    acl_write_columns: aclWriteColumns,
228✔
746
    acl_read_columns: aclColumns = '',
228✔
747
  } = headers
228✔
748

228✔
749
  const commands = request.body
228✔
750

228✔
751
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
228✔
752

228✔
753
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
228✔
754

228✔
755
  this.queryParser.parseAndCastCommands(commands, editableFields)
228✔
756
  const projection = resolveProjection('', aclColumns, this.allFieldNames, '', log)
228✔
757

228✔
758
  const docId = params.id
228✔
759
  const _id = this.castCollectionId(docId)
228✔
760

228✔
761
  const stateArr = state.split(',')
228✔
762
  const doc = await this.crudService.patchById(crudContext, _id, commands, mongoQuery, projection, stateArr)
228✔
763

222✔
764
  if (!doc) {
228✔
765
    return reply.notFound()
51✔
766
  }
51✔
767

171✔
768
  return doc
171✔
769
}
228✔
770

96✔
771
async function handlePatchMany(request) {
90✔
772
  if (this.customMetrics) {
90!
773
    this.customMetrics.collectionInvocation.inc({
×
774
      collection_name: this.modelName,
×
775
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
776
    })
×
777
  }
×
778

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

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

90✔
795
  const stateArr = state.split(',')
90✔
796
  const nModified = await this.crudService.patchMany(crudContext, commands, mongoQuery, stateArr)
90✔
797

84✔
798
  return nModified
84✔
799
}
90✔
800

96✔
801
async function handleUpsertOne(request) {
66✔
802
  if (this.customMetrics) {
66!
803
    this.customMetrics.collectionInvocation.inc({
×
804
      collection_name: this.modelName,
×
805
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
×
806
    })
×
807
  }
×
808

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

66✔
827
  const commands = request.body
66✔
828

66✔
829
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
66✔
830
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
66✔
831

66✔
832
  this.queryParser.parseAndCastCommands(commands, editableFields)
66✔
833
  const projection = resolveProjection('', aclColumns, this.allFieldNames, '', log)
66✔
834

66✔
835
  const stateArr = state.split(',')
66✔
836
  const doc = await this.crudService.upsertOne(crudContext, commands, mongoQuery, projection, stateArr)
66✔
837

66✔
838
  itemValidator(doc)
66✔
839
  return doc
66✔
840
}
66✔
841

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

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

90✔
852

90✔
853
  const nModified = await this.crudService.patchBulk(
90✔
854
    crudContext,
90✔
855
    filterUpdateCommands,
90✔
856
    this.queryParser,
90✔
857
    this.castCollectionId,
90✔
858
    getEditableFields(headers[ACL_WRITE_COLUMNS], this.allFieldNames),
90✔
859
    headers[ACL_ROWS],
90✔
860
  )
90✔
861
  return nModified
90✔
862
}
90✔
863

96✔
864
async function handleInsertMany(request, reply) {
63✔
865
  if (this.customMetrics) {
63✔
866
    this.customMetrics.collectionInvocation.inc({
3✔
867
      collection_name: this.modelName,
3✔
868
      type: PROMETHEUS_OP_TYPE.INSERT_OR_UPDATE,
3✔
869
    })
3✔
870
  }
3✔
871

63✔
872
  const { body: docs, crudContext } = request
63✔
873

63✔
874
  try {
63✔
875
    const ids = await this.crudService.insertMany(
63✔
876
      crudContext,
63✔
877
      docs,
63✔
878
      this.queryParser,
63✔
879
      { idOnly: true }
63✔
880
    )
63✔
881
    return ids
60✔
882
  } catch (error) {
63✔
883
    if (error.code === UNIQUE_INDEX_MONGO_ERROR_CODE) {
3✔
884
      request.log.error('unique index violation')
3✔
885
      return reply.getHttpError(UNIQUE_INDEX_ERROR_STATUS_CODE, error.message)
3✔
886
    }
3✔
887
    throw error
×
888
  }
×
889
}
63✔
890

96✔
891
async function handleChangeStateById(request, reply) {
51✔
892
  if (this.customMetrics) {
51!
893
    this.customMetrics.collectionInvocation.inc({
×
894
      collection_name: this.modelName,
×
895
      type: PROMETHEUS_OP_TYPE.CHANGE_STATE,
×
896
    })
×
897
  }
×
898

51✔
899
  const { body, crudContext, headers, query } = request
51✔
900
  const {
51✔
901
    [QUERY]: clientQueryString,
51✔
902
    ...otherParams
51✔
903
  } = query
51✔
904

51✔
905
  const { acl_rows } = headers
51✔
906
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
51✔
907

51✔
908
  const docId = request.params.id
51✔
909
  const _id = this.castCollectionId(docId)
51✔
910

51✔
911
  try {
51✔
912
    const doc = await this.crudService.changeStateById(crudContext, _id, body.stateTo, mongoQuery)
51✔
913
    if (!doc) {
51✔
914
      return reply.notFound()
9✔
915
    }
9✔
916

24✔
917
    reply.code(204)
24✔
918
  } catch (error) {
51✔
919
    if (error.statusCode) {
15✔
920
      return reply.getHttpError(error.statusCode, error.message)
15✔
921
    }
15✔
922

×
923
    throw error
×
924
  }
×
925
}
51✔
926

96✔
927
async function handleChangeStateMany(request) {
48✔
928
  if (this.customMetrics) {
48!
929
    this.customMetrics.collectionInvocation.inc({
×
930
      collection_name: this.modelName,
×
931
      type: PROMETHEUS_OP_TYPE.CHANGE_STATE,
×
932
    })
×
933
  }
×
934

48✔
935
  const { body: filterUpdateCommands, crudContext, headers } = request
48✔
936

48✔
937
  const {
48✔
938
    acl_rows,
48✔
939
  } = headers
48✔
940

48✔
941
  const parsedAndCastedCommands = new Array(filterUpdateCommands.length)
48✔
942
  for (let i = 0; i < filterUpdateCommands.length; i++) {
48✔
943
    const {
60✔
944
      filter,
60✔
945
      stateTo,
60✔
946
    } = filterUpdateCommands[i]
60✔
947

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

60✔
950
    parsedAndCastedCommands[i] = {
60✔
951
      query: mongoQuery,
60✔
952
      stateTo,
60✔
953
    }
60✔
954
  }
60✔
955

48✔
956
  return this.crudService.changeStateMany(crudContext, parsedAndCastedCommands)
48✔
957
}
48✔
958

96✔
959
function resolveProjection(clientProjectionString, aclColumns, allFieldNames, rawProjection, log) {
1,953✔
960
  log.debug('Resolving projections')
1,953✔
961
  const acls = splitACLs(aclColumns)
1,953✔
962

1,953✔
963
  if (clientProjectionString && rawProjection) {
1,953✔
964
    log.error('Use of both _p and _rawp is not permitted')
18✔
965
    throw new BadRequestError(
18✔
966
      'Use of both _rawp and _p parameter is not allowed')
18✔
967
  }
18✔
968

1,935✔
969
  let projection
1,935✔
970
  if (!clientProjectionString && !rawProjection) {
1,953✔
971
    projection = removeAclColumns(allFieldNames, acls)
1,275✔
972
  } else if (rawProjection) {
1,509✔
973
    projection = resolveRawProjectionString(rawProjection, acls, allFieldNames, log)
276✔
974
  } else if (clientProjectionString) {
660✔
975
    projection = resolveClientProjectionString(clientProjectionString, acls)
384✔
976
  }
384✔
977

1,869✔
978
  return getProjection(projection)
1,869✔
979
}
1,953✔
980

96✔
981
function getProjection(projection) {
1,869✔
982
  // In case of empty projection, we project only the _id
1,869✔
983
  if (!projection?.length) { return { _id: 1 } }
1,869✔
984

1,821✔
985
  return projection.reduce((acc, val) => {
1,821✔
986
    const propertiesToInclude = typeof val === 'string'
25,029✔
987
      // a string represents the name of a field to be projected
25,029✔
988
      ? { [val]: 1 }
25,029✔
989
      // an object represents a raw projection to be passed as it is
25,029✔
990
      : val
25,029✔
991

25,029✔
992
    return { ...acc, ...propertiesToInclude }
25,029✔
993
  }, {})
1,821✔
994
}
1,869✔
995

96✔
996
function resolveClientProjectionString(clientProjectionString, _acls) {
384✔
997
  const clientProjection = getClientProjection(clientProjectionString)
384✔
998
  return removeAclColumns(clientProjection, _acls)
384✔
999
}
384✔
1000

96✔
1001
function resolveRawProjectionString(rawProjection, _acls, allFieldNames, log) {
276✔
1002
  try {
276✔
1003
    checkAllowedOperators(
276✔
1004
      rawProjection,
276✔
1005
      rawProjectionDictionary,
276✔
1006
      _acls.length > 0 ? _acls : allFieldNames, log)
276✔
1007

276✔
1008
    const rawProjectionObject = resolveRawProjection(rawProjection)
276✔
1009
    const projection = removeAclColumnsFromRawProjection(rawProjectionObject, _acls)
276✔
1010

276✔
1011
    return !lisEmpty(projection) ? [projection] : []
276✔
1012
  } catch (errorMessage) {
276✔
1013
    log.error(errorMessage.message)
66✔
1014
    throw new BadRequestError(errorMessage.message)
66✔
1015
  }
66✔
1016
}
276✔
1017

96✔
1018
function splitACLs(acls) {
1,953✔
1019
  if (acls) { return acls.split(',') }
1,953✔
1020
  return []
1,752✔
1021
}
1,953✔
1022

96✔
1023
function removeAclColumns(fieldsInProjection, aclColumns) {
1,869✔
1024
  if (aclColumns.length > 0) {
1,869✔
1025
    return fieldsInProjection.filter(field => {
195✔
1026
      return aclColumns.indexOf(field) > -1
1,104✔
1027
    })
195✔
1028
  }
195✔
1029

1,674✔
1030
  return fieldsInProjection
1,674✔
1031
}
1,869✔
1032

96✔
1033
function removeAclColumnsFromRawProjection(rawProjectionObject, aclColumns) {
216✔
1034
  const isRawProjectionOverridingACLs = checkIfRawProjectionOverridesAcls(rawProjectionObject, aclColumns)
216✔
1035
  if (isRawProjectionOverridingACLs) {
216✔
1036
    throw Error('_rawp exclusive projection is overriding at least one acl_read_column value')
6✔
1037
  }
6✔
1038

210✔
1039
  const rawProjectionFields = Object.keys(rawProjectionObject)
210✔
1040
  const filteredFields = removeAclColumns(rawProjectionFields, aclColumns)
210✔
1041

210✔
1042
  return filteredFields.reduce((acc, current) => {
210✔
1043
    if (rawProjectionObject[current] === 0 || rawProjectionObject[current]) {
324✔
1044
      acc[current] = rawProjectionObject[current]
324✔
1045
    }
324✔
1046
    return acc
324✔
1047
  }, {})
210✔
1048
}
216✔
1049

96✔
1050
function getClientProjection(clientProjectionString) {
384✔
1051
  if (clientProjectionString) {
384✔
1052
    return clientProjectionString.split(',')
384✔
1053
  }
384✔
1054
  return []
×
1055
}
384✔
1056

96✔
1057
function resolveRawProjection(clientRawProjectionString) {
216✔
1058
  if (clientRawProjectionString) {
216✔
1059
    return JSON.parse(clientRawProjectionString)
216✔
1060
  }
216✔
1061
  return {}
×
1062
}
216✔
1063

96✔
1064
function checkAllowedOperators(rawProjection, projectionDictionary, additionalFields, log) {
276✔
1065
  if (!rawProjection) {
276!
1066
    log.debug('No raw projection found: checkAllowedOperators returns true')
×
1067
    return true
×
1068
  }
×
1069

276✔
1070
  const { allowedOperators, notAllowedOperators } = projectionDictionary
276✔
1071
  const allowedFields = [...allowedOperators]
276✔
1072

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

276✔
1075
  log.debug({ allowedOperators: allowedFields }, 'Allowed operators for projection')
276✔
1076
  log.debug({ notAllowedOperators }, 'Not allowed operators for projection')
276✔
1077

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

276✔
1082
  if (!matches) {
276✔
1083
    log.debug('No operators found in raw projection: checkAllowedOperators returns true')
108✔
1084
    return true
108✔
1085
  }
108✔
1086

168✔
1087
  return !matches.some(match => {
168✔
1088
    if (match.startsWith('$$')) {
444✔
1089
      log.debug({ match }, 'Found $$ match in raw projection')
120✔
1090
      if (notAllowedOperators.includes(match)) {
120✔
1091
        throw Error(`Operator ${match} is not allowed in raw projection`)
48✔
1092
      }
48✔
1093

72✔
1094
      return notAllowedOperators.includes(match)
72✔
1095
    }
72✔
1096

324✔
1097
    if (!allowedFields.includes(match)) {
444✔
1098
      throw Error(`Operator ${match} is not allowed in raw projection`)
12✔
1099
    }
12✔
1100

312✔
1101
    return !allowedFields.includes(match)
312✔
1102
  })
168✔
1103
}
276✔
1104

96✔
1105
function checkIfRawProjectionOverridesAcls(rawProjection, acls) {
216✔
1106
  return Object.keys(rawProjection).some(field =>
216✔
1107
    acls.includes(field) && rawProjection[field] === 0
441✔
1108
  )
216✔
1109
}
216✔
1110

96✔
1111
function mapToObjectWithOnlyId(doc) {
75✔
1112
  return { _id: doc._id.toString() }
75✔
1113
}
75✔
1114

96✔
1115
const internalFields = [
96✔
1116
  UPDATERID,
96✔
1117
  UPDATEDAT,
96✔
1118
  CREATORID,
96✔
1119
  CREATEDAT,
96✔
1120
  __STATE__,
96✔
1121
]
96✔
1122
function getEditableFields(aclWriteColumns, allFieldNames) {
474✔
1123
  const editableFields = aclWriteColumns ? aclWriteColumns.split(',') : allFieldNames
474!
1124
  return editableFields.filter(ef => !internalFields.includes(ef))
474✔
1125
}
474✔
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