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

mia-platform / crud-service / 6143624440

11 Sep 2023 08:04AM UTC coverage: 89.963% (-4.4%) from 94.354%
6143624440

push

github

danibix95
test: refactor patchById test to reduce its execution time

1039 of 1204 branches covered (0.0%)

Branch coverage included in aggregate %.

2116 of 2303 relevant lines covered (91.88%)

4962.15 hits per line

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

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

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

29
const lisEmpty = require('lodash.isempty')
28✔
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')
28✔
48

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

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

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

65
const PROMETHEUS_OP_TYPE = {
28✔
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) {
28✔
75
  if (!fastify.crudService) { throw new Error('`fastify.crudService` is undefined') }
1,034!
76
  if (!fastify.queryParser) { throw new Error('`fastify.queryParser` is undefined') }
1,034!
77
  if (!fastify.castCollectionId) { throw new Error('`fastify.castCollectionId` is undefined') }
1,034!
78
  if (!fastify.castResultsAsStream) { throw new Error('`fastify.castResultsAsStream` is undefined') }
1,034!
79
  if (!fastify.castItem) { throw new Error('`fastify.castItem` is undefined') }
1,034!
80
  if (!fastify.allFieldNames) { throw new Error('`fastify.allFieldNames` is undefined') }
1,034!
81
  if (!fastify.jsonSchemaGenerator) { throw new Error('`fastify.jsonSchemaGenerator` is undefined') }
1,034!
82
  if (!fastify.jsonSchemaGeneratorWithNested) { throw new Error('`fastify.jsonSchemaGeneratorWithNested` is undefined') }
1,034!
83
  if (!fastify.userIdHeaderKey) { throw new Error('`fastify.userIdHeaderKey` is undefined') }
1,034!
84
  if (!fastify.modelName) { throw new Error('`fastify.modelName` is undefined') }
1,034!
85

86
  const {
87
    registerGetters = true,
×
88
    registerSetters = true,
×
89
    registerLookup = false,
1,034✔
90
  } = options
1,034✔
91

92
  const validateOutput = fastify.validateOutput ?? false
1,034!
93

94
  const NESTED_SCHEMAS_BY_ID = {
1,034✔
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({
1,034✔
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)
1,034✔
125
  ajvKeywords(ajv, 'instanceof')
1,034✔
126
  ajv.addVocabulary(Object.values(SCHEMA_CUSTOM_KEYWORDS))
1,034✔
127

128
  fastify.setValidatorCompiler(({ schema }) => {
1,034✔
129
    const uniqueId = schema[SCHEMA_CUSTOM_KEYWORDS.UNIQUE_OPERATION_ID]
35,236✔
130
    const [collectionName, schemaId, subSchemaPath] = uniqueId.split('__MIA__')
35,236✔
131
    const nestedSchema = NESTED_SCHEMAS_BY_ID[schemaId]
35,236✔
132
    const subSchema = lget(nestedSchema, subSchemaPath)
35,236✔
133
    fastify.log.debug({ collectionName, schemaPath: subSchemaPath, schemaId }, 'collection schema info')
35,236✔
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)
35,236✔
137
  })
138

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

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

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

182
    const upsertOneSchema = fastify.jsonSchemaGenerator.generateUpsertOneJSONSchema()
865✔
183
    fastify.post(
865✔
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', {
865✔
194
      schema: fastify.jsonSchemaGenerator.generateBulkJSONSchema(),
195
    }, handleInsertMany)
196
    fastify.patch('/bulk', {
865✔
197
      schema: fastify.jsonSchemaGenerator.generatePatchBulkJSONSchema(),
198
    }, handlePatchBulk)
199
    fastify.post(
865✔
200
      '/:id/state',
201
      { schema: fastify.jsonSchemaGenerator.generateChangeStateJSONSchema() },
202
      handleChangeStateById
203
    )
204
    fastify.post(
865✔
205
      '/state',
206
      { schema: fastify.jsonSchemaGenerator.generateChangeStateManyJSONSchema() },
207
      handleChangeStateMany
208
    )
209

210
    const importPostSchema = fastify.jsonSchemaGenerator.generatePostImportJSONSchema()
865✔
211
    fastify.post(
865✔
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()
865✔
226
    fastify.patch(
865✔
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) {
1,034!
242
    if (!fastify.lookupProjection) { throw new Error('`fastify.lookupProjection` is undefined') }
×
243
    const listLookupSchema = fastify.jsonSchemaGenerator.generateGetListLookupJSONSchema()
×
244
    fastify.get('/', {
×
245
      schema: listLookupSchema,
246
      config: {
247
        streamValidator: shouldValidateStream(listLookupSchema.response['200'], validateOutput),
248
        replyType: () => 'application/json',
×
249
      },
250
    }, handleGetListLookup)
251
  }
252

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

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

264
          return accept
284✔
265
        },
266
      },
267
    }, handleGetList)
268
    fastify.get('/count', { schema: fastify.jsonSchemaGenerator.generateCountJSONSchema() }, handleCount)
1,034✔
269
    fastify.setNotFoundHandler(notFoundHandler)
1,034✔
270
    fastify.get('/', {
1,034✔
271
      schema: fastify.jsonSchemaGenerator.generateGetListJSONSchema(),
272
      config: {
273
        streamValidator: shouldValidateStream(getItemJSONSchema.response['200'], validateOutput),
274
        replyType: () => 'application/json',
106✔
275
      },
276
    }, handleGetList)
277
    fastify.get('/:id', {
1,034✔
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) {
24!
289
    this.customMetrics.collectionInvocation.inc({
×
290
      collection_name: this.modelName,
291
      type: PROMETHEUS_OP_TYPE.IMPORT,
292
    })
293
  }
294

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

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

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

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

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

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

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

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

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

354
  try {
20✔
355
    await pipeline(
20✔
356
      file,
357
      bodyParser(),
358
      parseDocument,
359
      batchConsumer
360
    )
361
  } catch (error) {
362
    if (error.code === OPTIONS_INCOMPATIBILITY_ERROR_CODE) {
3!
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) {
3✔
368
      log.debug('unique index violation')
1✔
369
      return reply.getHttpError(UNIQUE_INDEX_ERROR_STATUS_CODE, error.message)
1✔
370
    }
371

372
    if (Array.isArray(error)) {
2!
373
      log.debug('error parsing input file')
2✔
374
      const { message, instancePath } = error?.[0] ?? {}
2!
375
      const errorDetails = instancePath ? `, ${instancePath}` : ''
2!
376
      const errorMessage = `(index: ${documentIndex}${errorDetails}) ${message ?? 'error in parsing record'}`
2!
377
      return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, errorMessage)
2✔
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' })
17✔
384
}
385

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

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

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

408
  let projection = resolveProjection(
×
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]))
×
417
  if (projection.length === 0) {
×
418
    reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, 'No allowed colums')
×
419
  }
420

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

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

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

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

442
  try {
×
443
    return await pipeline(
×
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')
×
454
    request.log.debug({ error: { ...error, message: error.message } }, 'Error during findAll lookup stream with message')
×
455
    if (error.code === OPTIONS_INCOMPATIBILITY_ERROR_CODE) {
×
456
      request.log.info(BAD_REQUEST_ERROR_STATUS_CODE)
×
457
    }
458
  }
459
}
460

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

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

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

489
  const projection = resolveProjection(
338✔
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))
317✔
498
  const mongoQuery = resolveMongoQuery(this.queryParser, clientQueryString, acl_rows, otherParams, isTextSearchQuery)
317✔
499

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

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

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

510
  try {
317✔
511
    await pipeline(
317✔
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')
1✔
522
    request.log.debug({ error: { ...error, message: error.message } }, 'Error during findAll stream with message')
1✔
523
    if (error.code === OPTIONS_INCOMPATIBILITY_ERROR_CODE) {
1!
524
      request.log.info(BAD_REQUEST_ERROR_STATUS_CODE)
×
525
    }
526
  }
527
}
528

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

632
  if (!doc) {
17✔
633
    return reply.notFound()
6✔
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)
11✔
639
}
640

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

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

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

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

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

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

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

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

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

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

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

709
  const commands = request.body
73✔
710

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

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

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

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

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

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

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

733
async function handlePatchMany(request) {
734
  if (this.customMetrics) {
28!
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
28✔
742
  const {
743
    [QUERY]: clientQueryString,
744
    [STATE]: state,
745
    ...otherParams
746
  } = query
28✔
747
  const {
748
    acl_rows,
749
    acl_write_columns: aclWriteColumns,
750
  } = headers
28✔
751

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

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

760
  return nModified
28✔
761
}
762

763
async function handleUpsertOne(request, reply) {
764
  if (this.customMetrics) {
22!
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
22✔
772
  const {
773
    [QUERY]: clientQueryString,
774
    [STATE]: state,
775
    ...otherParams
776
  } = query
22✔
777
  const {
778
    acl_rows,
779
    acl_write_columns: aclWriteColumns,
780
    acl_read_columns: aclColumns = '',
18✔
781
  } = headers
22✔
782
  const {
783
    itemValidator,
784
  } = reply.context.config
22✔
785

786
  const commands = request.body
22✔
787

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

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

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

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

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

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

804
async function handlePatchBulk(request) {
805
  if (this.customMetrics) {
30!
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
30✔
813

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

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

829
    const commands = update
20,038✔
830

831
    const editableFields = getEditableFields(aclWriteColumns, this.allFieldNames)
20,038✔
832

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

835
    this.queryParser.parseAndCastCommands(commands, editableFields)
20,038✔
836

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

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

851
async function handleInsertMany(request, reply) {
852
  if (this.customMetrics) {
20!
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
20✔
860

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

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

875
async function handleChangeStateById(request, reply) {
876
  if (this.customMetrics) {
11!
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
11✔
884
  const {
885
    [QUERY]: clientQueryString,
886
    ...otherParams
887
  } = query
11✔
888

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

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

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

897
  if (!doc) {
11✔
898
    return reply.notFound()
4✔
899
  }
900

901
  reply.code(204)
7✔
902
}
903

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

912
  const { body: filterUpdateCommands, crudContext, headers } = request
16✔
913

914
  const {
915
    acl_rows,
916
  } = headers
16✔
917

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

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

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

933
  return this.crudService.changeStateMany(crudContext, parsedAndCastedCommands)
16✔
934
}
935

936
async function injectContextInRequest(request) {
937
  const userIdHeader = request.headers[this.userIdHeaderKey]
812✔
938
  const isUserHeaderInvalid = INVALID_USERID.includes(userIdHeader)
812✔
939

940
  let userId = 'public'
812✔
941

942
  if (userIdHeader && !isUserHeaderInvalid) {
812✔
943
    userId = userIdHeader
213✔
944
  }
945

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

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

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

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

982
async function customErrorHandler(error, request, reply) {
983
  if (error.statusCode === 404) {
208✔
984
    return notFoundHandler(request, reply)
53✔
985
  }
986

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

992
  throw error
119✔
993
}
994

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

1016
  if (rawAclRows) {
20,669✔
1017
    const aclRows = JSON.parse(rawAclRows)
98✔
1018
    if (rawAclRows[0] === '[') {
98✔
1019
      mongoQuery.$and.push({ $and: aclRows })
94✔
1020
    } else {
1021
      mongoQuery.$and.push(aclRows)
4✔
1022
    }
1023
  }
1024

1025
  if (textQuery) {
20,669✔
1026
    queryParser.parseAndCastTextSearchQuery(mongoQuery)
24✔
1027
  } else {
1028
    queryParser.parseAndCast(mongoQuery)
20,645✔
1029
  }
1030

1031
  if (mongoQuery.$and && !mongoQuery.$and.length) {
20,669✔
1032
    return { }
20,316✔
1033
  }
1034

1035
  return mongoQuery
353✔
1036
}
1037

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

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

1048
  if (!clientProjectionString && !rawProjection) {
550✔
1049
    return removeAclColumns(allFieldNames, acls)
368✔
1050
  } else if (rawProjection) {
182✔
1051
    return resolveRawProjectionString(rawProjection, acls, allFieldNames, log)
77✔
1052
  } else if (clientProjectionString) {
105!
1053
    return resolveClientProjectionString(clientProjectionString, acls)
105✔
1054
  }
1055
}
1056

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

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

1069
    const rawProjectionObject = resolveRawProjection(rawProjection)
57✔
1070
    const projection = removeAclColumnsFromRawProjection(rawProjectionObject, _acls)
57✔
1071

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

1079
function splitACLs(acls) {
1080
  if (acls) { return acls.split(',') }
556✔
1081
  return []
500✔
1082
}
1083

1084
function removeAclColumns(fieldsInProjection, aclColumns) {
1085
  if (aclColumns.length > 0) {
528✔
1086
    return fieldsInProjection.filter(field => {
54✔
1087
      return aclColumns.indexOf(field) > -1
316✔
1088
    })
1089
  }
1090

1091
  return fieldsInProjection
474✔
1092
}
1093

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

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

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

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

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

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

1131
  const { allowedOperators, notAllowedOperators } = projectionDictionary
77✔
1132
  const allowedFields = [...allowedOperators]
77✔
1133

1134
  additionalFields.forEach(field => allowedFields.push(`$${field}`))
1,353✔
1135

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

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

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

1148
  return !matches.some(match => {
46✔
1149
    if (match.startsWith('$$')) {
112✔
1150
      log.debug({ match }, 'Found $$ match in raw projection')
32✔
1151
      if (notAllowedOperators.includes(match)) {
32✔
1152
        throw Error(`Operator ${match} is not allowed in raw projection`)
16✔
1153
      }
1154

1155
      return notAllowedOperators.includes(match)
16✔
1156
    }
1157

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

1162
    return !allowedFields.includes(match)
76✔
1163
  })
1164
}
1165

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

1172
function mapToObjectWithOnlyId(doc) {
1173
  return { _id: doc._id.toString() }
50,060✔
1174
}
1175

1176
const internalFields = [
28✔
1177
  UPDATERID,
1178
  UPDATEDAT,
1179
  CREATORID,
1180
  CREATEDAT,
1181
  __STATE__,
1182
]
1183
function getEditableFields(aclWriteColumns, allFieldNames) {
1184
  const editableFields = aclWriteColumns ? aclWriteColumns.split(',') : allFieldNames
20,161!
1185
  return editableFields.filter(ef => !internalFields.includes(ef))
423,344✔
1186
}
1187

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