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

mia-platform / crud-service / 6145099756

11 Sep 2023 10:22AM UTC coverage: 94.328% (-0.03%) from 94.354%
6145099756

push

github

web-flow
feat: not allowed state transitions returns error 400 (#122)

* feat: return 400 in case of forbidden state transition

* refactor: updated unit tests

* feat: improving state mefchanism + passing error with statusCode

* feat: applied suggestion on state assert in CrudService.changeStateById

* chore: changelog

* feat: updated documentation to explain the 404 returned and the patch of metadata fields in case of transition to same state

* Update CHANGELOG.md

Following suggestions in code review.

* docs: update README.md

Co-authored-by: Demetrio Marino <demetrio.marino1985@gmail.com>

* chore: update CHANGELOG.md

* refactor: ensure state field can only be modified in case no other update on it has occurred

---------

Co-authored-by: Demetrio Marino <demetrio.marino@mia-platform.eu>
Co-authored-by: Daniele Bissoli <danibix95@users.noreply.github.com>
Co-authored-by: Daniele Bissoli <daniele.bissoli@mia-platform.eu>

1082 of 1210 branches covered (0.0%)

Branch coverage included in aggregate %.

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

2244 of 2316 relevant lines covered (96.89%)

9982.94 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')
58✔
22
const ajvFormats = require('ajv-formats')
58✔
23
const ajvKeywords = require('ajv-keywords')
58✔
24

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

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

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

49
const getAccept = require('./acceptHeaderParser')
58✔
50
const BadRequestError = require('./BadRequestError')
58✔
51
const BatchWritableStream = require('./BatchWritableStream')
58✔
52

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

57
const BAD_REQUEST_ERROR_STATUS_CODE = 400
58✔
58
const NOT_ACCEPTABLE = 406
58✔
59
const UNSUPPORTED_MIME_TYPE_STATUS_CODE = 415
58✔
60
const INTERNAL_SERVER_ERROR_STATUS_CODE = 500
58✔
61
const OPTIONS_INCOMPATIBILITY_ERROR_CODE = 2
58✔
62
const UNIQUE_INDEX_ERROR_STATUS_CODE = 409
58✔
63
const UNIQUE_INDEX_MONGO_ERROR_CODE = 11000
58✔
64

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

72

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

86
  const {
87
    registerGetters = true,
×
88
    registerSetters = true,
×
89
    registerLookup = false,
2,142✔
90
  } = options
2,144✔
91

92
  const validateOutput = fastify.validateOutput ?? false
2,144!
93

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

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

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

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

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

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

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

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

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

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

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

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

253
  if (registerGetters) {
2,144✔
254
    const getItemJSONSchema = fastify.jsonSchemaGenerator.generateGetItemJSONSchema()
2,140✔
255
    fastify.get('/export', {
2,140✔
256
      schema: fastify.jsonSchemaGenerator.generateExportJSONSchema(),
257
      config: {
258
        streamValidator: shouldValidateStream(getItemJSONSchema.response['200'], validateOutput),
259
        replyType: (acceptHeader) => {
260
          const accept = getAccept(acceptHeader)
568✔
261

262
          if (!accept || accept === '*/*') { return 'application/x-ndjson' }
568!
263

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

286
// eslint-disable-next-line max-statements
287
async function handleCollectionImport(request, reply) {
288
  if (this.customMetrics) {
48!
289
    this.customMetrics.collectionInvocation.inc({
×
290
      collection_name: this.modelName,
291
      type: PROMETHEUS_OP_TYPE.IMPORT,
292
    })
293
  }
294

295
  if (!request.isMultipart()) {
48!
296
    return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, 'Request is not multipart')
×
297
  }
298

299
  const data = await request.file()
48✔
300
  if (!data) {
48!
301
    return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, 'Missing file')
×
302
  }
303
  const { file, mimetype, fields } = data
48✔
304
  const parsingOptions = Object.fromEntries(Object.values(fields)
48✔
305
    .filter(field => field.type === 'field')
72✔
306
    .map(({ fieldname, value }) => [fieldname, value]))
24✔
307

308
  const { itemValidator, validateImportOptions } = reply.context.config
48✔
309
  const isValid = validateImportOptions(parsingOptions)
48✔
310
  if (!isValid) {
48✔
311
    return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, `Invalid options`)
8✔
312
  }
313

314
  const bodyParser = getFileMimeParser(mimetype, parsingOptions)
40✔
315
  if (!bodyParser) {
40!
316
    return reply.getHttpError(UNSUPPORTED_MIME_TYPE_STATUS_CODE, `Unsupported file type ${mimetype}`)
×
317
  }
318

319
  const { crudService, queryParser } = this
40✔
320
  const { log, crudContext } = request
40✔
321

322
  let documentIndex = 0
40✔
323
  const parseDocument = through2.obj((chunk, _enc, callback) => {
40✔
324
    try {
96✔
325
      itemValidator(chunk)
96✔
326
      if (itemValidator.errors) { throw itemValidator.errors }
96✔
327
      queryParser.parseAndCastBody(chunk)
92✔
328
    } catch (error) {
329
      return callback(error, chunk)
4✔
330
    }
331
    documentIndex += 1
92✔
332
    return callback(null, chunk)
92✔
333
  })
334

335
  // POST
336
  let returnCode = 201
40✔
337
  let processBatch = async(batch) => crudService.insertMany(crudContext, batch)
40✔
338

339
  // PATCH
340
  if (request.method === 'PATCH') {
40✔
341
    returnCode = 200
26✔
342
    processBatch = async(batch) => {
26✔
343
      return crudService.upsertMany(crudContext, batch)
24✔
344
    }
345
  }
346

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

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

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

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

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

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

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

395
  const { query, headers, crudContext, log } = request
20✔
396

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

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

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

421
  const LookupProjectionFieldsToOmit = this.lookupProjection.filter(field => Object.values(field).shift() === 0)
60✔
422
  projection.push(...LookupProjectionFieldsToOmit)
20✔
423

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

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

440
  reply.raw.setHeader('Content-Type', contentType)
20✔
441

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

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

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

484
  const responseParser = getFileMimeStringify(contentType, {})
783✔
485
  if (!responseParser) {
783✔
486
    return reply.getHttpError(NOT_ACCEPTABLE, `unsupported file type ${contentType}`)
104✔
487
  }
488

489
  const projection = resolveProjection(
679✔
490
    clientProjectionString,
491
    acl_read_columns,
492
    this.allFieldNames,
493
    clientRawProjectionString,
494
    log
495
  )
496

497
  const isTextSearchQuery = query._q && this.queryParser.isTextSearchQuery(JSON.parse(query._q))
637✔
498
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, isTextSearchQuery)
637✔
499

500
  let sort
501
  if (sortQuery) {
637✔
502
    sort = Object.fromEntries(sortQuery.toString().split(',')
81✔
503
      .map((param) => (param[0] === '-' ? [param.substr(1), -1] : [param, 1])))
151✔
504
  }
505

506
  const stateArr = state.split(',')
637✔
507

508
  reply.raw.setHeader('Content-Type', contentType)
637✔
509

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

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

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

544
  const {
545
    [QUERY]: clientQueryString,
546
    [PROJECTION]: clientProjectionString = '',
218✔
547
    [RAW_PROJECTION]: clientRawProjectionString = '',
224✔
548
    [STATE]: state,
549
    ...otherParams
550
  } = request.query
246✔
551

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

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

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

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

587
  const { body: doc, crudContext } = request
49✔
588

589
  this.queryParser.parseAndCastBody(doc)
49✔
590

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

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

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

615
  const { query, headers, params, crudContext } = request
35✔
616

617
  const docId = params.id
35✔
618
  const _id = this.castCollectionId(docId)
35✔
619

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

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

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

632
  if (!doc) {
35✔
633
    return reply.notFound()
12✔
634
  }
635

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

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

649
  const { query, headers, crudContext } = request
27✔
650

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

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

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

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

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

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

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

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

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

709
  const commands = request.body
147✔
710

711
  const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
147✔
712

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

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

718
  const docId = params.id
147✔
719
  const _id = this.castCollectionId(docId)
147✔
720

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

724
  if (!doc) {
145✔
725
    return reply.notFound()
34✔
726
  }
727

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

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

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

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

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

760
  return nModified
56✔
761
}
762

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

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

786
  const commands = request.body
44✔
787

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

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

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

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

798
  const response = this.castItem(doc)
44✔
799

800
  itemValidator(response)
44✔
801
  return response
44✔
802
}
803

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

812
  const { body: filterUpdateCommands, crudContext, headers } = request
60✔
813

814
  const {
815
    acl_rows,
816
    acl_write_columns: aclWriteColumns,
817
  } = headers
60✔
818

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

829
    const commands = update
40,076✔
830

831
    const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
40,076✔
832

833
    const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, false)
40,076✔
834

835
    this.queryParser.parseAndCastCommands(commands, editableFields)
40,076✔
836

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

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

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

859
  const { body: docs, crudContext } = request
40✔
860

861
  docs.forEach(this.queryParser.parseAndCastBody)
40✔
862

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

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

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

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

892
  const docId = request.params.id
32✔
893
  const _id = this.castCollectionId(docId)
32✔
894

895
  try {
32✔
896
    const doc = await this.crudService.changeStateById(crudContext, _id, body.stateTo, mongoQuery)
32✔
897
    if (!doc) {
22✔
898
      return reply.notFound()
6✔
899
    }
900

901
    reply.code(204)
16✔
902
  } catch (error) {
903
    if (error.statusCode) {
10!
904
      return reply.getHttpError(error.statusCode, error.message)
10✔
905
    }
906

907
    throw error
×
908
  }
909
}
910

911
async function handleChangeStateMany(request) {
912
  if (this.customMetrics) {
32!
913
    this.customMetrics.collectionInvocation.inc({
×
914
      collection_name: this.modelName,
915
      type: PROMETHEUS_OP_TYPE.CHANGE_STATE,
916
    })
917
  }
918

919
  const { body: filterUpdateCommands, crudContext, headers } = request
32✔
920

921
  const {
922
    acl_rows,
923
  } = headers
32✔
924

925
  const parsedAndCastedCommands = new Array(filterUpdateCommands.length)
32✔
926
  for (let i = 0; i < filterUpdateCommands.length; i++) {
32✔
927
    const {
928
      filter,
929
      stateTo,
930
    } = filterUpdateCommands[i]
40✔
931

932
    const mongoQuery = resolveMongoQuery(this.queryParser, null, acl_rows, filter, false)
40✔
933

934
    parsedAndCastedCommands[i] = {
40✔
935
      query: mongoQuery,
936
      stateTo,
937
    }
938
  }
939

940
  return this.crudService.changeStateMany(crudContext, parsedAndCastedCommands)
32✔
941
}
942

943
async function injectContextInRequest(request) {
944
  const userIdHeader = request.headers[this.userIdHeaderKey]
1,661✔
945
  const isUserHeaderInvalid = INVALID_USERID.includes(userIdHeader)
1,661✔
946

947
  let userId = 'public'
1,661✔
948

949
  if (userIdHeader && !isUserHeaderInvalid) {
1,661✔
950
    userId = userIdHeader
436✔
951
  }
952

953
  request.crudContext = {
1,661✔
954
    log: request.log,
955
    userId,
956
    now: new Date(),
957
  }
958
}
959

960
async function parseEncodedJsonQueryParams(logger, request) {
961
  if (request.headers.json_query_params_encoding) {
1,661!
962
    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.')
×
963
  }
964

965
  // TODO remove request.headers.json_query_params_encoding fallback in v7.0.0
966
  const jsonQueryParamsEncoding = request.headers['json-query-params-encoding'] || request.headers.json_query_params_encoding
1,661✔
967
  switch (jsonQueryParamsEncoding) {
1,661✔
968
  case 'base64': {
969
    const queryJsonFields = [QUERY, RAW_PROJECTION]
6✔
970
    for (const field of queryJsonFields) {
6✔
971
      if (request.query[field]) {
12✔
972
        request.query[field] = Buffer.from(request.query[field], jsonQueryParamsEncoding).toString()
6✔
973
      }
974
    }
975
    break
6✔
976
  }
977
  default: break
1,655✔
978
  }
979
}
980

981
async function notFoundHandler(request, reply) {
982
  reply
116✔
983
    .code(404)
984
    .send({
985
      error: 'not found',
986
    })
987
}
988

989
async function customErrorHandler(error, request, reply) {
990
  if (error.statusCode === 404) {
425✔
991
    return notFoundHandler(request, reply)
104✔
992
  }
993

994
  if (error.validation?.[0]?.message === 'must NOT have additional properties') {
321✔
995
    reply.code(error.statusCode)
72✔
996
    throw new Error(`${error.message}. Property "${error.validation[0].params.additionalProperty}" is not defined in validation schema`)
72✔
997
  }
998

999
  throw error
249✔
1000
}
1001

1002
function resolveMongoQuery(
1003
  queryParser,
1004
  clientQueryString,
1005
  rawAclRows,
1006
  otherParams,
1007
  textQuery
1008
) {
1009
  const mongoQuery = {
41,374✔
1010
    $and: [],
1011
  }
1012
  if (clientQueryString) {
41,374✔
1013
    const clientQuery = JSON.parse(clientQueryString)
417✔
1014
    mongoQuery.$and.push(clientQuery)
417✔
1015
  }
1016
  if (otherParams) {
41,374!
1017
    for (const key of Object.keys(otherParams)) {
41,374✔
1018
      const value = otherParams[key]
349✔
1019
      mongoQuery.$and.push({ [key]: value })
349✔
1020
    }
1021
  }
1022

1023
  if (rawAclRows) {
41,374✔
1024
    const aclRows = JSON.parse(rawAclRows)
199✔
1025
    if (rawAclRows[0] === '[') {
199✔
1026
      mongoQuery.$and.push({ $and: aclRows })
191✔
1027
    } else {
1028
      mongoQuery.$and.push(aclRows)
8✔
1029
    }
1030
  }
1031

1032
  if (textQuery) {
41,374✔
1033
    queryParser.parseAndCastTextSearchQuery(mongoQuery)
48✔
1034
  } else {
1035
    queryParser.parseAndCast(mongoQuery)
41,326✔
1036
  }
1037

1038
  if (mongoQuery.$and && !mongoQuery.$and.length) {
41,374✔
1039
    return { }
40,659✔
1040
  }
1041

1042
  return mongoQuery
715✔
1043
}
1044

1045
function resolveProjection(clientProjectionString, aclColumns, allFieldNames, rawProjection, log) {
1046
  log.debug('Resolving projections')
1,136✔
1047
  const acls = splitACLs(aclColumns)
1,136✔
1048

1049
  if (clientProjectionString && rawProjection) {
1,136✔
1050
    log.error('Use of both _p and _rawp is not permitted')
12✔
1051
    throw new BadRequestError(
12✔
1052
      'Use of both _rawp and _p parameter is not allowed')
1053
  }
1054

1055
  if (!clientProjectionString && !rawProjection) {
1,124✔
1056
    return removeAclColumns(allFieldNames, acls)
756✔
1057
  } else if (rawProjection) {
368✔
1058
    return resolveRawProjectionString(rawProjection, acls, allFieldNames, log)
154✔
1059
  } else if (clientProjectionString) {
214!
1060
    return resolveClientProjectionString(clientProjectionString, acls)
214✔
1061
  }
1062
}
1063

1064
function resolveClientProjectionString(clientProjectionString, _acls) {
1065
  const clientProjection = getClientProjection(clientProjectionString)
214✔
1066
  return removeAclColumns(clientProjection, _acls)
214✔
1067
}
1068

1069
function resolveRawProjectionString(rawProjection, _acls, allFieldNames, log) {
1070
  try {
154✔
1071
    checkAllowedOperators(
154✔
1072
      rawProjection,
1073
      rawProjectionDictionary,
1074
      _acls.length > 0 ? _acls : allFieldNames, log)
154✔
1075

1076
    const rawProjectionObject = resolveRawProjection(rawProjection)
114✔
1077
    const projection = removeAclColumnsFromRawProjection(rawProjectionObject, _acls)
114✔
1078

1079
    return !lisEmpty(projection) ? [projection] : []
110✔
1080
  } catch (errorMessage) {
1081
    log.error(errorMessage.message)
44✔
1082
    throw new BadRequestError(errorMessage.message)
44✔
1083
  }
1084
}
1085

1086
function splitACLs(acls) {
1087
  if (acls) { return acls.split(',') }
1,136✔
1088
  return []
1,020✔
1089
}
1090

1091
function removeAclColumns(fieldsInProjection, aclColumns) {
1092
  if (aclColumns.length > 0) {
1,080✔
1093
    return fieldsInProjection.filter(field => {
112✔
1094
      return aclColumns.indexOf(field) > -1
638✔
1095
    })
1096
  }
1097

1098
  return fieldsInProjection
968✔
1099
}
1100

1101
function removeAclColumnsFromRawProjection(rawProjectionObject, aclColumns) {
1102
  const isRawProjectionOverridingACLs = checkIfRawProjectionOverridesAcls(rawProjectionObject, aclColumns)
114✔
1103
  if (isRawProjectionOverridingACLs) {
114✔
1104
    throw Error('_rawp exclusive projection is overriding at least one acl_read_column value')
4✔
1105
  }
1106

1107
  const rawProjectionFields = Object.keys(rawProjectionObject)
110✔
1108
  const filteredFields = removeAclColumns(rawProjectionFields, aclColumns)
110✔
1109

1110
  return filteredFields.reduce((acc, current) => {
110✔
1111
    if (rawProjectionObject[current] === 0 || rawProjectionObject[current]) {
166!
1112
      acc[current] = rawProjectionObject[current]
166✔
1113
    }
1114
    return acc
166✔
1115
  }, {})
1116
}
1117

1118
function getClientProjection(clientProjectionString) {
1119
  if (clientProjectionString) {
214!
1120
    return clientProjectionString.split(',')
214✔
1121
  }
1122
  return []
×
1123
}
1124

1125
function resolveRawProjection(clientRawProjectionString) {
1126
  if (clientRawProjectionString) {
114!
1127
    return JSON.parse(clientRawProjectionString)
114✔
1128
  }
1129
  return {}
×
1130
}
1131

1132
function checkAllowedOperators(rawProjection, projectionDictionary, additionalFields, log) {
1133
  if (!rawProjection) {
154!
1134
    log.debug('No raw projection found: checkAllowedOperators returns true')
×
1135
    return true
×
1136
  }
1137

1138
  const { allowedOperators, notAllowedOperators } = projectionDictionary
154✔
1139
  const allowedFields = [...allowedOperators]
154✔
1140

1141
  additionalFields.forEach(field => allowedFields.push(`$${field}`))
2,706✔
1142

1143
  log.debug({ allowedOperators: allowedFields }, 'Allowed operators for projection')
154✔
1144
  log.debug({ notAllowedOperators }, 'Not allowed operators for projection')
154✔
1145

1146
  // to match both camelCase operators and snake mongo_systems variables
1147
  const operatorsRegex = /\${1,2}[a-zA-Z_]+/g
154✔
1148
  const matches = rawProjection.match(operatorsRegex)
154✔
1149

1150
  if (!matches) {
154✔
1151
    log.debug('No operators found in raw projection: checkAllowedOperators returns true')
62✔
1152
    return true
62✔
1153
  }
1154

1155
  return !matches.some(match => {
92✔
1156
    if (match.startsWith('$$')) {
224✔
1157
      log.debug({ match }, 'Found $$ match in raw projection')
64✔
1158
      if (notAllowedOperators.includes(match)) {
64✔
1159
        throw Error(`Operator ${match} is not allowed in raw projection`)
32✔
1160
      }
1161

1162
      return notAllowedOperators.includes(match)
32✔
1163
    }
1164

1165
    if (!allowedFields.includes(match)) {
160✔
1166
      throw Error(`Operator ${match} is not allowed in raw projection`)
8✔
1167
    }
1168

1169
    return !allowedFields.includes(match)
152✔
1170
  })
1171
}
1172

1173
function checkIfRawProjectionOverridesAcls(rawProjection, acls) {
1174
  return Object.keys(rawProjection).some(field =>
114✔
1175
    acls.includes(field) && rawProjection[field] === 0
234✔
1176
  )
1177
}
1178

1179
function mapToObjectWithOnlyId(doc) {
1180
  return { _id: doc._id.toString() }
100,121✔
1181
}
1182

1183
const internalFields = [
58✔
1184
  UPDATERID,
1185
  UPDATEDAT,
1186
  CREATORID,
1187
  CREATEDAT,
1188
  __STATE__,
1189
]
1190
function getEditableFields(aclWriteColumns, allFieldNames) {
1191
  const editableFields = aclWriteColumns ? aclWriteColumns.split(',') : allFieldNames
40,323!
1192
  return editableFields.filter(ef => !internalFields.includes(ef))
846,696✔
1193
}
1194

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