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

mia-platform / crud-service / 5265497397

pending completion
5265497397

Pull #92

github

web-flow
Merge 325f32589 into 9a6116684
Pull Request #92: Feat/writable views

956 of 1073 branches covered (89.1%)

Branch coverage included in aggregate %.

168 of 168 new or added lines in 5 files covered. (100.0%)

1942 of 2014 relevant lines covered (96.43%)

16150.69 hits per line

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

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

19
/* eslint-disable max-lines */
20

21
'use strict'
22

23
const { ObjectId } = require('mongodb')
104✔
24
const { v4: uuidv4 } = require('uuid')
104✔
25
const { omit } = require('ramda')
104✔
26

27
const {
28
  ARRAY,
29
  GEOPOINT,
30
  DATE,
31
  OBJECTID,
32
  RAWOBJECTTYPE,
33
  SORT,
34
  PROJECTION,
35
  RAW_PROJECTION,
36
  QUERY,
37
  LIMIT,
38
  SKIP,
39
  STATE,
40
  MONGOID,
41
  UPDATERID,
42
  UPDATEDAT,
43
  CREATORID,
44
  CREATEDAT,
45
  SETCMD,
46
  UNSETCMD,
47
  INCCMD,
48
  MULCMD,
49
  CURDATECMD,
50
  SETONINSERTCMD,
51
  PUSHCMD,
52
  ADDTOSETCMD,
53
  __STATE__,
54
  STATES,
55
  ARRAY_MERGE_ELEMENT_OPERATOR,
56
  ARRAY_REPLACE_ELEMENT_OPERATOR,
57
  JSON_SCHEMA_ARRAY_TYPE,
58
  JSON_SCHEMA_OBJECT_TYPE,
59
  SCHEMA_CUSTOM_KEYWORDS,
60
  DATE_FORMATS,
61
  PULLCMD,
62
} = require('./consts')
104✔
63

64
const EXAMPLE_DATE = new Date('2020-09-16T12:00:00Z').toISOString()
104✔
65
const mandatoryFields = new Set([MONGOID, UPDATERID, UPDATEDAT, CREATORID, CREATEDAT, __STATE__])
104✔
66
const mandatoryFieldsWithoutId = (isNewSchema) => ({
24,600✔
67
  [CREATORID]: {
68
    type: 'string',
69
    description: 'User id that has created this object',
70
  },
71
  [CREATEDAT]: {
72
    type: 'string',
73
    ...isNewSchema ? { format: 'date-time' } : { pattern: '^\\d{4}-\\d{2}-\\d{2}(T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,3})?(Z|[+-]\\d{2}:\\d{2}))?$' },
24,600✔
74
    description: 'Date of the request that has performed the object creation',
75
    examples: [
76
      '2020-09-16T12:00:00.000Z',
77
    ],
78
  },
79
  [UPDATERID]: {
80
    type: 'string',
81
    description: 'User id that has requested the last change successfully',
82
  },
83
  [UPDATEDAT]: {
84
    type: 'string',
85
    ...isNewSchema ? { format: 'date-time' } : { pattern: '^\\d{4}-\\d{2}-\\d{2}(T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,3})?(Z|[+-]\\d{2}:\\d{2}))?$' },
24,600✔
86
    description: 'Date of the request that has performed the last change',
87
    examples: [
88
      '2020-09-16T12:00:00.000Z',
89
    ],
90
  },
91
})
92

93
const objectIdType = () => {
104✔
94
  return {
72,366✔
95
    type: 'string',
96
    pattern: '^[a-fA-F\\d]{24}$',
97
    description: 'Hexadecimal identifier of the document in the collection',
98
    examples: [new ObjectId().toString()],
99
  }
100
}
101

102
const stringType = () => ({
7,806✔
103
  type: 'string',
104
  pattern: '^(?!\\s*$).+',
105
  description: 'String identifier of the document in the collection',
106
  examples: [uuidv4()],
107
})
108

109
const mongoIdTypeValidator = {
104✔
110
  ObjectId: objectIdType,
111
  string: stringType,
112
}
113

114
const geoPointValidateSchema = () => ({
1,900✔
115
  type: 'array',
116
  items: {
117
    type: 'number',
118
  },
119
  minItems: 2,
120
  maxItems: 3,
121
})
122

123
const geoPointSerializeSchema = () => ({
1,900✔
124
  type: 'array',
125
  items: {
126
    type: 'number',
127
  },
128
})
129

130
function validateDateSchema() {
131
  return {
2,780✔
132
    type: 'string',
133
    pattern: '^\\d{4}-\\d{2}-\\d{2}(T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,3})?(Z|[+-]\\d{2}:\\d{2}))?$',
134
    description: '"date-time" according with https://tools.ietf.org/html/rfc3339#section-5.6',
135
    examples: [EXAMPLE_DATE],
136
  }
137
}
138

139
const serializeDateSchema = () => ({
35,244✔
140
  type: 'string',
141
  format: 'date-time',
142
  examples: [EXAMPLE_DATE],
143
})
144

145
const dateTypeSchema = (jsonSchema) => ({
448✔
146
  type: 'string',
147
  format: jsonSchema?.schema?.format ?? 'date-time',
448!
148
  examples: [EXAMPLE_DATE],
149
})
150

151
// any unformatted, unshaped and schemaless object, with no particular validation to perform
152
const rawObjectTypeSchema = ({ schema } = {}) => {
104!
153
  const { required, additionalProperties = true, properties } = schema || {}
43,576✔
154
  return {
43,576✔
155
    type: 'object',
156
    additionalProperties,
157
    ...properties ? { properties } : {},
43,576✔
158
    ...required !== undefined ? { required } : {},
43,576✔
159
  }
160
}
161

162
const stateCreateValidationSchema = () => ({
8,200✔
163
  type: 'string',
164
  enum: Array.from(Object.values(STATES)),
165
  description: 'The state of the document',
166
})
167

168
const specialTypesValidationCompatibility = {
104✔
169
  [OBJECTID]: objectIdType,
170
  [GEOPOINT]: geoPointValidateSchema,
171
  [DATE]: validateDateSchema,
172
  [RAWOBJECTTYPE]: rawObjectTypeSchema,
173
}
174

175
const specialTypesSerializationCompatibility = {
104✔
176
  [OBJECTID]: objectIdType,
177
  [GEOPOINT]: geoPointSerializeSchema,
178
  [DATE]: serializeDateSchema,
179
  [RAWOBJECTTYPE]: rawObjectTypeSchema,
180
}
181

182
const specialTypesValidation = {
104✔
183
  [OBJECTID]: objectIdType,
184
  [GEOPOINT]: geoPointValidateSchema,
185
  [DATE]: dateTypeSchema,
186
  [RAWOBJECTTYPE]: rawObjectTypeSchema,
187
}
188

189
const specialTypesSerialization = {
104✔
190
  [OBJECTID]: objectIdType,
191
  [GEOPOINT]: geoPointSerializeSchema,
192
  [DATE]: dateTypeSchema,
193
  [RAWOBJECTTYPE]: rawObjectTypeSchema,
194
}
195

196
const getInheritedType = (jsonSchema) => {
104✔
197
  if (jsonSchema.__mia_configuration?.type) { return jsonSchema.__mia_configuration.type }
238,408✔
198
  if (jsonSchema.type === 'string' && DATE_FORMATS.includes(jsonSchema.format ?? '')) { return DATE }
238,040✔
199
  if (jsonSchema.type === JSON_SCHEMA_OBJECT_TYPE) { return RAWOBJECTTYPE }
237,592✔
200
  return jsonSchema.type
236,640✔
201
}
202

203
const defaultGetQueryParams = {
104✔
204
  [QUERY]: {
205
    type: 'string',
206
    description: 'Additional query part to forward to MongoDB',
207
  },
208
}
209

210
function defaultGetListQueryParams(enableLimitConstraints, maxLimit) {
211
  const limitConstraints = enableLimitConstraints
16,400✔
212
    ? {
213
      default: 25,
214
      maximum: maxLimit,
215
      minimum: 1,
216
      description: `Limits the number of documents, max ${maxLimit} elements, minimum 1`,
217
    }
218
    : {}
219
  return {
16,400✔
220
    [LIMIT]: {
221
      type: 'integer',
222
      minimum: 1,
223
      description: 'Limits the number of documents',
224
      ...limitConstraints,
225
    },
226
    [SKIP]: {
227
      type: 'integer',
228
      minimum: 0,
229
      description: 'Skip the specified number of documents',
230
    },
231
    ...defaultGetQueryParams,
232
  }
233
}
234

235
function formatEndpointTag(endpointBasePath) {
236
  return endpointBasePath
8,200✔
237
    .replace(/\//g, '')
238
    .replace(/\W|_/g, ' ')
239
    .replace(/ (\w)/g, found => ` ${found[1].toUpperCase()}`)
9,570✔
240
    .replace(/^(\w)/g, found => found[0].toUpperCase())
8,200✔
241
}
242

243
const SCHEMAS_ID = {
104✔
244
  GET_LIST: 'getList',
245
  GET_LIST_LOOKUP: 'getListLookup',
246
  EXPORT: 'export',
247
  GET_ITEM: 'getItem',
248
  POST_ITEM: 'postItem',
249
  VALIDATE: 'validate',
250
  DELETE_ITEM: 'deleteItem',
251
  DELETE_LIST: 'deleteList',
252
  COUNT: 'count',
253
  POST_BULK: 'postBulk',
254
  PATCH_ITEM: 'patchItem',
255
  UPSERT_ONE: 'upsertOne',
256
  PATCH_BULK: 'patchBulk',
257
  PATCH_MANY: 'patchMany',
258
  CHANGE_STATE: 'changeState',
259
  CHANGE_STATE_MANY: 'changeStateMany',
260
}
261

262
module.exports = class JSONSchemaGenerator {
104✔
263
  constructor(
264
    collectionDefinition,
265
    pathFieldsRawSchema,
266
    enableLimitConstraint = true,
84✔
267
    maxLimit = 200
84✔
268
  ) {
269
    const {
270
      endpointBasePath,
271
      name: collectionName,
272
      defaultState: collectionDefaultState,
273
    } = collectionDefinition
8,200✔
274

275
    const idType = getIdType(collectionDefinition) ?? 'ObjectId'
8,200✔
276
    const {
277
      collectionFields: validationProperties,
278
      collectionRawObject: patternProperties,
279
      required,
280
      sort,
281
      changeStateProperties,
282
      collectionFieldsArrayOperations,
283
      unsetPatternProperties,
284
      unsetProperties,
285
      dateProperties,
286
    } = collectionFieldsProperties(collectionDefinition, true)
8,200✔
287

288
    const {
289
      collectionFields: serializationProperties,
290
    } = collectionFieldsProperties(collectionDefinition, false)
8,200✔
291
    const validationGetProperties = propertiesGetValidation(
8,200✔
292
      structuredClone(validationProperties),
293
      Boolean(collectionDefinition.schema)
294
    )
295
    const validationGetListProperties = propertiesGetListValidation(
8,200✔
296
      structuredClone(validationGetProperties),
297
      enableLimitConstraint,
298
      maxLimit,
299
      idType,
300
      sort
301
    )
302
    const patchCommandsProperties = propertiesPatchCommandsValidation(
8,200✔
303
      collectionDefinition,
304
      validationProperties,
305
      patternProperties,
306
      collectionFieldsArrayOperations,
307
      unsetPatternProperties,
308
      unsetProperties,
309
      dateProperties,
310
      pathFieldsRawSchema
311
    )
312
    const deleteProperties = propertiesDeleteValidation(structuredClone(validationGetProperties))
8,200✔
313
    const countAndQueryValidation = {
8,200✔
314
      [MONGOID]: { ...mongoIdTypeValidator[idType]() },
315
      ...structuredClone(deleteProperties),
316
    }
317

318
    this._idType = idType
8,200✔
319
    this._pathFieldsRawSchema = pathFieldsRawSchema
8,200✔
320
    this._collectionName = collectionName
8,200✔
321
    this._collectionFieldsRawObject = patternProperties
8,200✔
322
    this._serializationProperties = serializationProperties
8,200✔
323
    this._collectionBasePath = formatEndpointTag(endpointBasePath)
8,200✔
324
    this._queryStringFromPatternProperties = getQueryStringFromRawSchema(pathFieldsRawSchema.patternProperties)
8,200✔
325
    this._queryStringFromPaths = getQueryStringFromRawSchema(pathFieldsRawSchema.paths)
8,200✔
326

327
    this._propertiesGetValidation = validationGetProperties
8,200✔
328
    this._requiredFields = required
8,200✔
329

330
    this._propertiesGetListValidation = validationGetListProperties
8,200✔
331
    this._propertiesGetExportValidation = propertiesGetExportValidation(
8,200✔
332
      validationGetListProperties,
333
      idType,
334
      sort
335
    )
336
    this._propertiesDeleteValidation = deleteProperties
8,200✔
337
    this._propertiesPatchCommandsValidation = patchCommandsProperties
8,200✔
338
    this._propertiesUpsertCommandsValidation = propertiesUpsertCommandsValidation(
8,200✔
339
      patchCommandsProperties,
340
      validationProperties
341
    )
342
    this._propertiesPostValidation = {
8,200✔
343
      ...validationProperties,
344
      __STATE__: {
345
        ...stateCreateValidationSchema(),
346
        default: collectionDefaultState,
347
      },
348
    }
349
    this._propertiesPatchQueryValidation = deleteProperties
8,200✔
350
    this._propertiesFilterChangeStateMany = changeStateProperties
8,200✔
351
    this._propertiesCountValidation = { ...countAndQueryValidation }
8,200✔
352
    this._propertiesPatchManyQueryValidation = { ...countAndQueryValidation }
8,200✔
353
  }
354

355
  getSchemaDetail(operationName) {
356
    // TODO: follow comment below
357
    /* schemaDetail identifies the subschemas (params, querystring, response.200, body)
358
     * in the "setValidatorCompiler" function.
359
     * It could be removed after update to fastify 3.x and change to `setValidatorCompiler`
360
     * const that have schema, method, url, httpPart to identify the schema.
361
     * */
362
    const schemaDetail = (subSchemaPath) =>
118,699✔
363
      ({ [SCHEMA_CUSTOM_KEYWORDS.UNIQUE_OPERATION_ID]: `${this._collectionName}__MIA__${operationName}__MIA__${subSchemaPath}` })
474,796✔
364
    return {
118,699✔
365
      params: schemaDetail('params'),
366
      querystring: schemaDetail('querystring'),
367
      'response.200': schemaDetail('response.200'),
368
      body: schemaDetail('body'),
369
    }
370
  }
371

372
  generateGetListJSONSchema() {
373
    const schemaDetail = this.getSchemaDetail(SCHEMAS_ID.GET_LIST)
8,132✔
374

375
    return {
8,132✔
376
      summary: `Returns a list of documents in ${this._collectionName}`,
377
      description: 'Results can be filtered specifying the following parameters:',
378
      tags: [this._collectionBasePath],
379
      querystring: {
380
        ...schemaDetail.querystring,
381
        type: 'object',
382
        properties: {
383
          ...this._propertiesGetListValidation,
384
          ...this._queryStringFromPaths,
385
        },
386
        ...Object.keys(this._queryStringFromPatternProperties).length > 0
8,132✔
387
          ? { patternProperties: this._queryStringFromPatternProperties }
388
          : {},
389
        additionalProperties: false,
390
      },
391
      response: {
392
        200: {
393
          ...schemaDetail['response.200'],
394
          type: 'array',
395
          items: {
396
            type: 'object',
397
            properties: this._serializationProperties,
398
          },
399
        },
400
      },
401
    }
402
  }
403

404
  generateGetListLookupJSONSchema() {
405
    const patternProperties = getQueryStringFromRawSchema(this._pathFieldsRawSchema.patternProperties)
4,058✔
406
    const schemaDetail = this.getSchemaDetail(SCHEMAS_ID.GET_LIST_LOOKUP)
4,058✔
407

408
    const unsupportedQueryParams = ['creatorId', 'createdAt', 'updaterId', 'updatedAt', '_rawp']
4,058✔
409

410
    const supportMongoID = this._collectionDefinition?.schema?.properties[MONGOID]
4,058✔
411
    || this._collectionDefinition?.fields?.findIndex((field) => field.name === MONGOID) > -1
×
412

413
    if (!supportMongoID) {
4,058!
414
      unsupportedQueryParams.push(MONGOID)
4,058✔
415
    }
416

417
    return {
4,058✔
418
      summary: `Returns a list of documents in ${this._collectionName}`,
419
      description: 'Results can be filtered specifying the following parameters:',
420
      tags: [this._collectionBasePath],
421
      querystring: {
422
        ...schemaDetail.querystring,
423
        type: 'object',
424
        properties: {
425
          ...omit(unsupportedQueryParams, this._propertiesGetListValidation),
426
          ...getQueryStringFromRawSchema(this._pathFieldsRawSchema.paths),
427
        },
428
        ...Object.keys(patternProperties).length > 0 ? { patternProperties } : {},
4,058✔
429
        additionalProperties: false,
430
      },
431
      response: {
432
        200: {
433
          ...schemaDetail['response.200'],
434
          type: 'array',
435
          items: {
436
            type: 'object',
437
            properties: this._serializationProperties,
438
          },
439
        },
440
      },
441
    }
442
  }
443

444
  generateExportJSONSchema() {
445
    const schemaDetail = this.getSchemaDetail(SCHEMAS_ID.EXPORT)
8,124✔
446

447
    return {
8,124✔
448
      summary: `Export the ${this._collectionName} collection`,
449
      description: 'The exported documents are sent as newline separated JSON objects to facilitate large dataset streaming and parsing',
450
      tags: [this._collectionBasePath],
451
      querystring: {
452
        ...schemaDetail.querystring,
453
        type: 'object',
454
        properties: {
455
          ...this._propertiesGetExportValidation,
456
          ...this._queryStringFromPaths,
457
        },
458
        ...Object.keys(this._queryStringFromPatternProperties).length > 0
8,124✔
459
          ? { patternProperties: this._queryStringFromPatternProperties }
460
          : {},
461
        additionalProperties: false,
462
      },
463
    }
464
  }
465

466
  generateGetItemJSONSchema() {
467
    const patternProperties = {
8,124✔
468
      ...this._collectionFieldsRawObject,
469
      ...this._queryStringFromPatternProperties,
470
    }
471
    const schemaDetail = this.getSchemaDetail(SCHEMAS_ID.GET_ITEM)
8,124✔
472

473
    return {
8,124✔
474
      summary: `Returns the item with specific ID from the ${this._collectionName} collection.`,
475
      tags: [this._collectionBasePath],
476
      params: {
477
        type: 'object',
478
        properties: {
479
          id: {
480
            type: 'string',
481
            description: 'The ID of the item to retrieve information for',
482
          },
483
        },
484
        ...schemaDetail.params,
485
      },
486
      querystring: {
487
        ...schemaDetail.querystring,
488
        type: 'object',
489
        properties: {
490
          ...this._propertiesGetValidation,
491
          ...this._queryStringFromPaths,
492
        },
493
        ...Object.keys(patternProperties).length > 0 ? { patternProperties } : {},
8,124✔
494
        additionalProperties: false,
495
      },
496
      response: {
497
        200: {
498
          ...schemaDetail['response.200'],
499
          type: 'object',
500
          properties: this._serializationProperties,
501
        },
502
      },
503
    }
504
  }
505

506
  generatePostJSONSchema() {
507
    const requiredFields = this._requiredFields
7,491✔
508
    const schemaDetail = this.getSchemaDetail(SCHEMAS_ID.POST_ITEM)
7,491✔
509

510
    return {
7,491✔
511
      summary: `Add a new item to the ${this._collectionName} collection.`,
512
      tags: [this._collectionBasePath],
513
      body: {
514
        ...schemaDetail.body,
515
        type: 'object',
516
        ...requiredFields.length > 0 ? { required: requiredFields } : {},
7,491✔
517
        properties: this._propertiesPostValidation,
518
        additionalProperties: false,
519
      },
520
      response: {
521
        200: {
522
          ...schemaDetail['response.200'],
523
          type: 'object',
524
          properties: {
525
            [MONGOID]: { ...mongoIdTypeValidator[this._idType]() },
526
          },
527
        },
528
      },
529
    }
530
  }
531

532
  generateValidateJSONSchema() {
533
    const requiredFields = this._requiredFields
7,447✔
534
    const schemaDetail = this.getSchemaDetail(SCHEMAS_ID.VALIDATE)
7,447✔
535

536
    return {
7,447✔
537
      summary: `Verify if the body is valid for an insertion in the ${this._collectionName} collection.`,
538
      tags: [this._collectionBasePath],
539
      body: {
540
        ...schemaDetail.body,
541
        type: 'object',
542
        ...requiredFields.length > 0 ? { required: requiredFields } : {},
7,447✔
543
        properties: this._propertiesPostValidation,
544
        additionalProperties: false,
545
      },
546
      response: {
547
        200: {
548
          ...schemaDetail['response.200'],
549
          type: 'object',
550
          properties: {
551
            result: {
552
              type: 'string',
553
              enum: ['ok'],
554
            },
555
          },
556
        },
557
      },
558
    }
559
  }
560

561
  generateDeleteJSONSchema() {
562
    const patternProperties = this._queryStringFromPatternProperties
7,471✔
563
    const schemaDetail = this.getSchemaDetail(SCHEMAS_ID.DELETE_ITEM)
7,471✔
564

565
    return {
7,471✔
566
      summary: `Delete an item with specific ID from the ${this._collectionName} collection.`,
567
      tags: [this._collectionBasePath],
568
      params: {
569
        type: 'object',
570
        properties: {
571
          id: {
572
            type: 'string',
573
            description: 'The ID of the item to delete',
574
          },
575
        },
576
        ...schemaDetail.params,
577
      },
578
      querystring: {
579
        ...schemaDetail.querystring,
580
        type: 'object',
581
        properties: {
582
          ...this._propertiesDeleteValidation,
583
          ...this._queryStringFromPaths,
584
        },
585
        ...Object.keys(patternProperties).length > 0 ? { patternProperties } : {},
7,471✔
586
        additionalProperties: false,
587
      },
588
    }
589
  }
590

591
  generateDeleteListJSONSchema() {
592
    const schemaDetail = this.getSchemaDetail(SCHEMAS_ID.DELETE_LIST)
7,447✔
593

594
    return {
7,447✔
595
      summary: `Delete multiple items from the ${this._collectionName} collection.`,
596
      tags: [this._collectionBasePath],
597
      querystring: {
598
        ...schemaDetail.querystring,
599
        type: 'object',
600
        properties: {
601
          ...this._propertiesDeleteValidation,
602
          ...this._queryStringFromPaths,
603
        },
604
        ...Object.keys(this._queryStringFromPatternProperties).length > 0
7,447✔
605
          ? { patternProperties: this._queryStringFromPatternProperties }
606
          : {},
607
        additionalProperties: false,
608
      },
609
    }
610
  }
611

612
  generateCountJSONSchema() {
613
    const schemaDetail = this.getSchemaDetail(SCHEMAS_ID.COUNT)
8,144✔
614

615
    return {
8,144✔
616
      summary: `Returns the number of items in the ${this._collectionName} collection.`,
617
      tags: [this._collectionBasePath],
618
      querystring: {
619
        ...schemaDetail.querystring,
620
        type: 'object',
621
        properties: {
622
          ...this._propertiesCountValidation,
623
          ...this._queryStringFromPaths,
624
        },
625
        ...Object.keys(this._queryStringFromPatternProperties).length > 0
8,144✔
626
          ? { patternProperties: this._queryStringFromPatternProperties }
627
          : {},
628
        additionalProperties: false,
629
      },
630
      response: {
631
        200: {
632
          ...schemaDetail['response.200'],
633
          type: 'integer',
634
          minimum: 0,
635
        },
636
      },
637
    }
638
  }
639

640
  generateBulkJSONSchema() {
641
    const schemaDetail = this.getSchemaDetail(SCHEMAS_ID.POST_BULK)
7,471✔
642

643
    return {
7,471✔
644
      summary: `Insert new items in the ${this._collectionName} collection.`,
645
      tags: [this._collectionBasePath],
646
      body: {
647
        ...schemaDetail.body,
648
        type: 'array',
649
        items: {
650
          type: 'object',
651
          ...this._requiredFields.length > 0 ? { required: this._requiredFields } : {},
7,471✔
652
          properties: this._propertiesPostValidation,
653
          additionalProperties: false,
654
        },
655
      },
656
      response: {
657
        200: {
658
          ...schemaDetail['response.200'],
659
          type: 'array',
660
          items: {
661
            type: 'object',
662
            properties: {
663
              [MONGOID]: { ...mongoIdTypeValidator[this._idType]() },
664
            },
665
          },
666
        },
667
      },
668
    }
669
  }
670

671
  generatePatchJSONSchema() {
672
    const schemaDetail = this.getSchemaDetail(SCHEMAS_ID.PATCH_ITEM)
7,471✔
673

674
    return {
7,471✔
675
      summary: `Update the item with specific ID in the ${this._collectionName} collection.`,
676
      tags: [this._collectionBasePath],
677
      params: {
678
        properties: {
679
          id: {
680
            type: 'string',
681
            description: 'The ID of the item to update information for',
682
          },
683
        },
684
        type: 'object',
685
        ...schemaDetail.params,
686
      },
687
      querystring: {
688
        ...schemaDetail.querystring,
689
        type: 'object',
690
        properties: {
691
          ...this._propertiesPatchQueryValidation,
692
          ...this._queryStringFromPaths,
693
        },
694
        ...Object.keys(this._queryStringFromPatternProperties).length > 0
7,471✔
695
          ? { patternProperties: this._queryStringFromPatternProperties }
696
          : {},
697
        additionalProperties: false,
698
      },
699
      body: {
700
        ...schemaDetail.body,
701
        type: 'object',
702
        properties: this._propertiesPatchCommandsValidation,
703
        additionalProperties: false,
704
      },
705
      response: {
706
        200: {
707
          ...schemaDetail['response.200'],
708
          type: 'object',
709
          properties: this._serializationProperties,
710
        },
711
      },
712
    }
713
  }
714

715
  generateUpsertOneJSONSchema() {
716
    const schemaDetail = this.getSchemaDetail(SCHEMAS_ID.UPSERT_ONE)
7,447✔
717

718
    return {
7,447✔
719
      summary: `Update an item in the ${this._collectionName} collection. If the item is not in the collection, it will be inserted.`,
720
      tags: [this._collectionBasePath],
721
      querystring: {
722
        ...schemaDetail.querystring,
723
        type: 'object',
724
        properties: {
725
          ...this._propertiesPatchQueryValidation,
726
          ...this._queryStringFromPaths,
727
        },
728
        ...Object.keys(this._queryStringFromPatternProperties).length > 0
7,447✔
729
          ? { patternProperties: this._queryStringFromPatternProperties }
730
          : {},
731
        additionalProperties: false,
732
      },
733
      body: {
734
        ...schemaDetail.body,
735
        type: 'object',
736
        properties: this._propertiesUpsertCommandsValidation,
737
        additionalProperties: false,
738
      },
739
      response: {
740
        200: {
741
          ...schemaDetail['response.200'],
742
          type: 'object',
743
          properties: this._serializationProperties,
744
        },
745
      },
746
    }
747
  }
748

749
  generatePatchBulkJSONSchema() {
750
    const filterPatternProperties = getFilterFromRawSchema(this._pathFieldsRawSchema.patternProperties)
7,483✔
751
    const schemaDetail = this.getSchemaDetail(SCHEMAS_ID.PATCH_BULK)
7,483✔
752

753
    return {
7,483✔
754
      summary: `Update multiple items of ${this._collectionName}, each one with its own modifications`,
755
      tags: [this._collectionBasePath],
756
      body: {
757
        ...schemaDetail.body,
758
        type: 'array',
759
        items: {
760
          type: 'object',
761
          properties: {
762
            filter: {
763
              type: 'object',
764
              properties: {
765
                [MONGOID]: { ...mongoIdTypeValidator[this._idType]() },
766
                [STATE]: { enum: Object.keys(STATES), default: 'PUBLIC' },
767
                ...copyPropertiesFilteringAttributes(this._propertiesPatchQueryValidation),
768
                ...getFilterFromRawSchema(this._pathFieldsRawSchema.paths),
769
              },
770
              ...Object.keys(filterPatternProperties).length > 0 ? { patternProperties: filterPatternProperties } : {},
7,483✔
771
              additionalProperties: false,
772
            },
773
            update: {
774
              type: 'object',
775
              properties: this._propertiesPatchCommandsValidation,
776
              additionalProperties: false,
777
            },
778
          },
779
          required: ['filter', 'update'],
780
        },
781
        minItems: 1,
782
      },
783
      response: {
784
        200: {
785
          ...schemaDetail['response.200'],
786
          type: 'integer',
787
          minimum: 0,
788
        },
789
      },
790
    }
791
  }
792

793
  generatePatchManyJSONSchema() {
794
    const schemaDetail = this.getSchemaDetail(SCHEMAS_ID.PATCH_MANY)
7,447✔
795

796
    return {
7,447✔
797
      summary: `Update the items of the ${this._collectionName} collection that match the query.`,
798
      tags: [this._collectionBasePath],
799
      querystring: {
800
        ...schemaDetail.querystring,
801
        type: 'object',
802
        properties: {
803
          ...this._propertiesPatchManyQueryValidation,
804
          ...this._queryStringFromPaths,
805
        },
806
        ...Object.keys(this._queryStringFromPatternProperties).length > 0
7,447✔
807
          ? { patternProperties: this._queryStringFromPatternProperties }
808
          : {},
809
        additionalProperties: false,
810
      },
811
      body: {
812
        ...schemaDetail.body,
813
        type: 'object',
814
        properties: this._propertiesPatchCommandsValidation,
815
        additionalProperties: false,
816
      },
817
      response: {
818
        200: {
819
          ...schemaDetail['response.200'],
820
          type: 'number',
821
          description: 'the number of documents that were modified',
822
        },
823
      },
824
    }
825
  }
826

827
  generateChangeStateJSONSchema() {
828
    const properties = {
7,471✔
829
      ...copyPropertiesFilteringAttributes(this._propertiesPatchQueryValidation),
830
    }
831

832
    delete properties[STATE]
7,471✔
833
    const schemaDetail = this.getSchemaDetail(SCHEMAS_ID.CHANGE_STATE)
7,471✔
834

835
    return {
7,471✔
836
      summary: `Change state of an item of ${this._collectionName} collection.`,
837
      tags: [this._collectionBasePath],
838
      params: {
839
        ...schemaDetail.params,
840
        properties: {
841
          id: {
842
            type: 'string',
843
            description: 'the ID of the item to have the property __STATE__ updated',
844
          },
845
        },
846
        type: 'object',
847
      },
848
      querystring: {
849
        ...schemaDetail.querystring,
850
        type: 'object',
851
        properties: {
852
          ...properties,
853
          ...this._queryStringFromPaths,
854
        },
855
        ...Object.keys(this._queryStringFromPatternProperties).length > 0
7,471✔
856
          ? { patternProperties: this._queryStringFromPatternProperties }
857
          : {},
858
        additionalProperties: false,
859
      },
860
      body: {
861
        ...schemaDetail.body,
862
        type: 'object',
863
        required: ['stateTo'],
864
        properties: {
865
          stateTo: {
866
            type: 'string',
867
            enum: ['PUBLIC', 'TRASH', 'DRAFT', 'DELETED'],
868
          },
869
        },
870
      },
871
    }
872
  }
873

874
  generateChangeStateManyJSONSchema() {
875
    const otherParams = {
7,471✔
876
      ...copyPropertiesFilteringAttributes(this._propertiesFilterChangeStateMany),
877
    }
878
    const filterPatternProperties = this._pathFieldsRawSchema.patternProperties || {}
7,471✔
879
    const schemaDetail = this.getSchemaDetail(SCHEMAS_ID.CHANGE_STATE_MANY)
7,471✔
880

881
    return {
7,471✔
882
      summary: `Change state of multiple items of ${this._collectionName}.`,
883
      tags: [this._collectionBasePath],
884
      body: {
885
        ...schemaDetail.body,
886
        type: 'array',
887
        items: {
888
          type: 'object',
889
          properties: {
890
            filter: {
891
              type: 'object',
892
              properties: {
893
                [MONGOID]: { ...mongoIdTypeValidator[this._idType]() },
894
                ...otherParams,
895
                ...this._pathFieldsRawSchema.paths,
896
              },
897
              ...Object.keys(filterPatternProperties).length > 0 ? { patternProperties: filterPatternProperties } : {},
7,471✔
898
            },
899
            stateTo: {
900
              type: 'string',
901
              enum: ['PUBLIC', 'DRAFT', 'TRASH', 'DELETED'],
902
            },
903
          },
904
          required: ['filter', 'stateTo'],
905
          additionalProperties: false,
906
        },
907
        minItems: 1,
908
      },
909
      response: {
910
        200: {
911
          ...schemaDetail['response.200'],
912
          type: 'integer',
913
          minimum: 0,
914
          description: `Number of updated ${this._collectionName}`,
915
        },
916
      },
917
    }
918
  }
919
}
920

921
module.exports.SCHEMAS_ID = SCHEMAS_ID
104✔
922

923
function getQueryStringFromRawSchema(pathsMap = {}) {
8,140✔
924
  const TYPES_TO_SKIP = [JSON_SCHEMA_OBJECT_TYPE]
24,516✔
925
  const ITEMS_TYPE_TO_SKIP = [JSON_SCHEMA_OBJECT_TYPE, JSON_SCHEMA_ARRAY_TYPE]
24,516✔
926

927
  return Object.keys(pathsMap).reduce((acc, path) => {
24,516✔
928
    const { type } = pathsMap[path]
46,340✔
929
    if (
46,340✔
930
      TYPES_TO_SKIP.includes(type)
93,126✔
931
      || (type === JSON_SCHEMA_ARRAY_TYPE && ITEMS_TYPE_TO_SKIP.includes(pathsMap[path].items.type))
932
    ) {
933
      return acc
11,854✔
934
    }
935
    if (type === JSON_SCHEMA_ARRAY_TYPE) {
34,486✔
936
      return {
5,704✔
937
        ...acc,
938
        [path]: pathsMap[path].items,
939
      }
940
    }
941
    return {
28,782✔
942
      ...acc,
943
      [path]: pathsMap[path],
944
    }
945
  }, {})
946
}
947

948
function getFilterFromRawSchema(pathsMap = {}) {
6,802✔
949
  return Object.keys(pathsMap).reduce((acc, path) => {
14,966✔
950
    const { type } = pathsMap[path]
22,926✔
951
    if (type !== JSON_SCHEMA_ARRAY_TYPE) {
22,926✔
952
      return {
18,473✔
953
        ...acc,
954
        [path]: pathsMap[path],
955
      }
956
    }
957

958
    return {
4,453✔
959
      ...acc,
960
      [path]: {
961
        oneOf: [
962
          pathsMap[path],
963
          pathsMap[path].items,
964
        ],
965
      },
966
    }
967
  }, {})
968
}
969

970
function propertiesGetValidation(validationProperties, isNewSchema) {
971
  const properties = {
8,200✔
972
    ...mandatoryFieldsWithoutId(isNewSchema),
973
    ...validationProperties,
974
    ...defaultGetQueryParams,
975
    [PROJECTION]: {
976
      type: 'string',
977
      description: 'Return only the properties specified in a comma separated list',
978
      examples: ['field1,field2,field3.nestedField'],
979
    },
980
    [STATE]: {
981
      type: 'string',
982
      pattern: '(PUBLIC|DRAFT|TRASH|DELETED)(,(PUBLIC|DRAFT|TRASH|DELETED))*',
983
      default: 'PUBLIC',
984
      description: 'Filter by \\_\\_STATE__, multiple states can be specified in OR by providing a comma separated list',
985
    },
986
    [RAW_PROJECTION]: {
987
      type: 'string',
988
      description: 'Additional raw stringified projection for MongoDB',
989
    },
990
  }
991

992
  Object.keys(properties).forEach(property => {
8,200✔
993
    if ([
105,612✔
994
      JSON_SCHEMA_ARRAY_TYPE,
995
      JSON_SCHEMA_OBJECT_TYPE,
996
    ].includes(properties[property].type)) { delete properties[property] }
14,180✔
997
  })
998
  return properties
8,200✔
999
}
1000

1001
function propertiesDeleteValidation(propertiesGet) {
1002
  delete propertiesGet[PROJECTION]
8,200✔
1003
  return propertiesGet
8,200✔
1004
}
1005

1006
function propertiesGetListValidation(propertiesGet, enableLimitConstraints, maxLimit, idType, sort) {
1007
  const properties = {
8,200✔
1008
    ...propertiesGet,
1009
    ...defaultGetListQueryParams(enableLimitConstraints, maxLimit),
1010
    [SORT]: {
1011
      anyOf: [
1012
        {
1013
          type: 'string',
1014
          pattern: sort,
1015
        },
1016
        {
1017
          type: 'array',
1018
          items: { type: 'string', pattern: sort },
1019
        },
1020
      ],
1021
      description: 'Sort by the specified property/properties (Start with a "-" to invert the sort order)',
1022
    },
1023
  }
1024

1025
  return {
8,200✔
1026
    [MONGOID]: { ...mongoIdTypeValidator[idType]() },
1027
    ...properties,
1028
  }
1029
}
1030

1031
function propertiesGetExportValidation(propertiesGetList, idType, sort) {
1032
  return {
8,200✔
1033
    [MONGOID]: { ...mongoIdTypeValidator[idType]() },
1034
    ...propertiesGetList,
1035
    ...defaultGetListQueryParams(false),
1036
    [SORT]: {
1037
      anyOf: [
1038
        {
1039
          type: 'string',
1040
          pattern: sort,
1041
        },
1042
        {
1043
          type: 'array',
1044
          items: { type: 'string', pattern: sort },
1045
        },
1046
      ],
1047
      description: 'Sort by the specified property/properties (Start with a "-" to invert the sort order)',
1048
    },
1049
  }
1050
}
1051

1052

1053
function propertiesPatchCommandsValidation(
1054
  collectionDefinition,
1055
  validationProperties,
1056
  patternProperties,
1057
  collectionFieldsArrayOperations,
1058
  unsetPatternProperties,
1059
  unsetProperties,
1060
  dateProperties,
1061
  rawPathsAndPatternProperties = {}
×
1062
) {
1063
  const {
1064
    paths: rawPaths,
1065
    pathsOperators: rawPathsOperators,
1066
    patternProperties: rawPatternProperties,
1067
    patternPropertiesOperators: rawPatternPropertiesOperators,
1068
  } = rawPathsAndPatternProperties
8,200✔
1069

1070
  const {
1071
    fromFields: arrayProperties,
1072
    fromFieldsWithMongoSupport: arrayPropertiesWithMongoConditions,
1073
  } = getArrayProperties(collectionDefinition, rawPaths)
8,200✔
1074
  const {
1075
    withMongoDBConditionsSupport: arrayPatternPropertiesWithMongoConditions,
1076
    withoutMongoDBConditionsSupport: arrayPatternProperties,
1077
  } = getArrayPatternProperties(rawPatternProperties)
8,200✔
1078

1079
  const propertiesTypeNumber = {
8,200✔
1080
    ...getPropertiesFilteredByType(collectionDefinition, 'number'),
1081
    ...getRawSchemaPathsFilteredByType(rawPaths, 'number'),
1082
  }
1083
  const patternPropertiesTypeNumber = getRawSchemaPathsFilteredByType(rawPatternProperties, 'number')
8,200✔
1084

1085
  return {
8,200✔
1086
    [SETCMD]: {
1087
      type: 'object',
1088
      properties: {
1089
        ...validationProperties,
1090
        ...collectionFieldsArrayOperations,
1091
        ...rawPaths,
1092
        ...rawPathsOperators,
1093
      },
1094
      additionalProperties: false,
1095
      patternProperties: {
1096
        ...patternProperties,
1097
        ...rawPatternProperties,
1098
        ...rawPatternPropertiesOperators,
1099
      },
1100
    },
1101
    [UNSETCMD]: {
1102
      type: 'object',
1103
      properties: unsetProperties,
1104
      additionalProperties: false,
1105
      patternProperties: {
1106
        ...patternProperties,
1107
        ...unsetPatternProperties,
1108
      },
1109
    },
1110
    [INCCMD]: {
1111
      type: 'object',
1112
      properties: propertiesTypeNumber,
1113
      additionalProperties: false,
1114
      patternProperties: {
1115
        ...patternProperties,
1116
        ...patternPropertiesTypeNumber,
1117
      },
1118
    },
1119
    [MULCMD]: {
1120
      type: 'object',
1121
      properties: propertiesTypeNumber,
1122
      additionalProperties: false,
1123
      patternProperties: {
1124
        ...patternProperties,
1125
        ...patternPropertiesTypeNumber,
1126
      },
1127
    },
1128
    [CURDATECMD]: {
1129
      type: 'object',
1130
      properties: dateProperties,
1131
      additionalProperties: false,
1132
    },
1133
    [PUSHCMD]: {
1134
      type: 'object',
1135
      properties: arrayProperties,
1136
      ...Object.keys(arrayPatternProperties).length > 0 ? { patternProperties: arrayPatternProperties } : {},
8,200✔
1137
      additionalProperties: false,
1138
    },
1139
    [PULLCMD]: {
1140
      type: 'object',
1141
      properties: arrayPropertiesWithMongoConditions,
1142
      ...Object.keys(arrayPatternProperties).length > 0
8,200✔
1143
        ? { patternProperties: arrayPatternPropertiesWithMongoConditions }
1144
        : {},
1145
      additionalProperties: false,
1146
    },
1147
    [ADDTOSETCMD]: {
1148
      type: 'object',
1149
      properties: arrayPropertiesWithMongoConditions,
1150
      ...Object.keys(arrayPatternProperties).length > 0
8,200✔
1151
        ? { patternProperties: arrayPatternPropertiesWithMongoConditions }
1152
        : {},
1153
      additionalProperties: false,
1154
    },
1155
  }
1156
}
1157

1158
function propertiesUpsertCommandsValidation(propertiesPatchCommands, validationProperties) {
1159
  return {
8,200✔
1160
    ...propertiesPatchCommands,
1161
    [SETONINSERTCMD]: {
1162
      type: 'object',
1163
      properties: validationProperties,
1164
      additionalProperties: false,
1165
    },
1166
  }
1167
}
1168

1169
function notMandatory(field) {
1170
  return !mandatoryFields.has(field.name)
87,968✔
1171
}
1172

1173
function getUnsetProperties() {
1174
  return { type: 'boolean', enum: [true] }
73,736✔
1175
}
1176

1177
const mongoOperatorSchema = { type: 'object', patternProperties: { '^$': {} } }
104✔
1178

1179
function getArrayPropertiesCompatibility(collectionDefinition, rawPaths = {}) {
×
1180
  const fromFieldsWithMongoSupport = {}
8,116✔
1181
  const fromFields = {}
8,116✔
1182

1183
  for (const field of collectionDefinition.fields) {
8,116✔
1184
    if (field.type !== ARRAY) { continue }
87,968✔
1185
    const itemDefinitions = {
7,818✔
1186
      schema: field.items.schema,
1187
    }
1188

1189
    const fieldItemType = field.items.type
7,818✔
1190

1191
    fromFields[field.name] = specialTypesValidation[fieldItemType]
7,818✔
1192
      ? specialTypesValidation[fieldItemType](itemDefinitions)
1193
      : { type: fieldItemType }
1194
    fromFieldsWithMongoSupport[field.name] = specialTypesValidation[fieldItemType]
7,818✔
1195
      ? specialTypesValidation[fieldItemType](itemDefinitions)
1196
      : { oneOf: [{ type: fieldItemType }, mongoOperatorSchema] }
1197
  }
1198

1199
  const fromRaw = {}
8,116✔
1200

1201
  for (const key of Object.keys(rawPaths)) {
8,116✔
1202
    if (rawPaths[key].type !== JSON_SCHEMA_ARRAY_TYPE) {
5,776✔
1203
      continue
3,613✔
1204
    }
1205
    fromRaw[key] = rawPaths[key].items
2,163✔
1206
  }
1207

1208
  return {
8,116✔
1209
    fromFields: {
1210
      ...fromFields,
1211
      ...fromRaw,
1212
    },
1213
    fromFieldsWithMongoSupport: {
1214
      ...fromFieldsWithMongoSupport,
1215
      ...fromRaw,
1216
    },
1217
  }
1218
}
1219

1220
function getArrayProperties(collectionDefinition, rawPaths = {}) {
4,058✔
1221
  if (!collectionDefinition.schema) {
8,200✔
1222
    return getArrayPropertiesCompatibility(collectionDefinition, rawPaths)
8,116✔
1223
  }
1224

1225
  const fromFieldsWithMongoSupport = {}
84✔
1226
  const fromFields = {}
84✔
1227

1228
  for (const [propertyName, jsonSchema] of Object.entries(collectionDefinition.schema.properties)) {
84✔
1229
    if (!(jsonSchema.type === JSON_SCHEMA_ARRAY_TYPE)) { continue }
1,196✔
1230
    const itemDefinitions = {
164✔
1231
      schema: jsonSchema.items,
1232
    }
1233

1234
    const inheritedType = getInheritedType(jsonSchema.items)
164✔
1235
    fromFields[propertyName] = specialTypesValidation[inheritedType]
164✔
1236
      ? specialTypesValidation[inheritedType](itemDefinitions)
1237
      : { type: inheritedType }
1238
    fromFieldsWithMongoSupport[propertyName] = specialTypesValidation[inheritedType]
164✔
1239
      ? specialTypesValidation[inheritedType](itemDefinitions)
1240
      : { oneOf: [{ type: inheritedType }, mongoOperatorSchema] }
1241
  }
1242

1243
  const fromRaw = {}
84✔
1244

1245
  for (const key of Object.keys(rawPaths)) {
84✔
1246
    if (rawPaths[key].type !== JSON_SCHEMA_ARRAY_TYPE) {
152✔
1247
      continue
92✔
1248
    }
1249
    fromRaw[key] = rawPaths[key].items
60✔
1250
  }
1251

1252
  return {
84✔
1253
    fromFields: {
1254
      ...fromFields,
1255
      ...fromRaw,
1256
    },
1257
    fromFieldsWithMongoSupport: {
1258
      ...fromFieldsWithMongoSupport,
1259
      ...fromRaw,
1260
    },
1261
  }
1262
}
1263

1264
function getArrayPatternProperties(rawPatternProperties = {}) {
4,058✔
1265
  const withMongoDBConditionsSupport = {}
8,200✔
1266
  const withoutMongoDBConditionsSupport = {}
8,200✔
1267
  Object.keys(rawPatternProperties).forEach((key) => {
8,200✔
1268
    if (rawPatternProperties[key].type !== JSON_SCHEMA_ARRAY_TYPE) {
18,086✔
1269
      return
15,640✔
1270
    }
1271

1272
    const originalFieldItemSchema = rawPatternProperties[key].items
2,446✔
1273

1274
    withMongoDBConditionsSupport[key] = { oneOf: [originalFieldItemSchema, mongoOperatorSchema] }
2,446✔
1275
    withoutMongoDBConditionsSupport[key] = originalFieldItemSchema
2,446✔
1276
  })
1277

1278
  return {
8,200✔
1279
    withMongoDBConditionsSupport,
1280
    withoutMongoDBConditionsSupport,
1281
  }
1282
}
1283

1284
function getPropertiesFilteredByTypeCompatibility(collectionDefinition, type) {
1285
  return collectionDefinition
8,116✔
1286
    .fields
1287
    .filter(notMandatory)
1288
    .filter(field => field.type === type)
39,272✔
1289
    .reduce((properties, field) => {
1290
      return Object.assign(properties, {
4,978✔
1291
        [field.name]: specialTypesSerializationCompatibility[type] || { type },
9,956✔
1292
      })
1293
    }, {})
1294
}
1295

1296
function getPropertiesFilteredByType(collectionDefinition, type) {
1297
  if (!collectionDefinition.schema) {
8,200✔
1298
    return getPropertiesFilteredByTypeCompatibility(collectionDefinition, type)
8,116✔
1299
  }
1300

1301
  return Object
84✔
1302
    .entries(collectionDefinition.schema.properties)
1303
    .filter(([propertyName, jsonSchema]) => !mandatoryFields.has(propertyName) && jsonSchema.type === type)
1,196✔
1304
    .reduce((properties, [propertyName, jsonSchema]) => {
1305
      const inheritedType = getInheritedType(jsonSchema)
76✔
1306
      return {
76✔
1307
        ...properties,
1308
        [propertyName]: specialTypesSerialization[inheritedType] || { type: inheritedType },
152✔
1309
      }
1310
    }, {})
1311
}
1312

1313
function getRawSchemaPathsFilteredByType(rawPaths = {}, type) {
8,116✔
1314
  return Object.keys(rawPaths).reduce((acc, path) => {
16,400✔
1315
    if (rawPaths[path].type !== type) {
24,014✔
1316
      return acc
17,345✔
1317
    }
1318
    return {
6,669✔
1319
      ...acc,
1320
      [path]: rawPaths[path],
1321
    }
1322
  }, {})
1323
}
1324

1325
function sortRegex(orFields, propertyName, type) {
1326
  const typesToIgnore = new Set([GEOPOINT])
129,176✔
1327
  if (typesToIgnore.has(type)) { return orFields }
129,176✔
1328
  return orFields.concat(propertyName)
125,376✔
1329
}
1330

1331
function buildSortRegex(orFields) {
1332
  const fields = [...mandatoryFields, ...orFields]
16,400✔
1333
  const subFieldSuffix = '(\\.([^\\.,])+)*'
16,400✔
1334
  const singleFieldMatcher = `-?(${fields.join('|')})${subFieldSuffix}`
16,400✔
1335
  return `^${singleFieldMatcher}(,${singleFieldMatcher})*$`
16,400✔
1336
}
1337

1338
function collectionFieldsArrayOperationsPropertiesCompatibility(field, collectionFieldsArrayOperations = {}) {
×
1339
  const itemType = field.items.type
15,636✔
1340
  const replacePropertyName = `${field.name}.$.${ARRAY_REPLACE_ELEMENT_OPERATOR}`
15,636✔
1341
  const replaceFieldItemsOption = {
15,636✔
1342
    schema: field.items.schema,
1343
  }
1344

1345
  const propertyOperation = {
15,636✔
1346
    [replacePropertyName]: specialTypesValidationCompatibility[itemType]
15,636✔
1347
      ? ({ ...specialTypesValidationCompatibility[itemType](replaceFieldItemsOption) })
1348
      : { type: itemType },
1349
  }
1350

1351
  if (itemType === RAWOBJECTTYPE) {
15,636✔
1352
    const mergePropertyName = `${field.name}.$.${ARRAY_MERGE_ELEMENT_OPERATOR}`
6,452✔
1353
    propertyOperation[mergePropertyName] = {
6,452✔
1354
      type: 'object',
1355
      ...field.items.schema ? { properties: field.items.schema.properties } : {},
6,452✔
1356
      // additionalProperties true to support dot notation
1357
      additionalProperties: true,
1358
    }
1359
  }
1360
  return { ...collectionFieldsArrayOperations, ...propertyOperation }
15,636✔
1361
}
1362

1363
function collectionFieldsArrayOperationsProperties(propertyName, jsonSchema, collectionFieldsArrayOperations = {}) {
×
1364
  const replacePropertyName = `${propertyName}.$.${ARRAY_REPLACE_ELEMENT_OPERATOR}`
328✔
1365
  const itemDefinitions = {
328✔
1366
    schema: jsonSchema.items,
1367
  }
1368

1369
  const inheritedType = getInheritedType(jsonSchema.items)
328✔
1370

1371
  const propertyOperation = {
328✔
1372
    [replacePropertyName]: specialTypesValidation[inheritedType]
328✔
1373
      ? { ...specialTypesValidation[inheritedType](itemDefinitions) }
1374
      : { type: inheritedType },
1375
  }
1376

1377
  if (inheritedType === RAWOBJECTTYPE) {
328✔
1378
    const mergePropertyName = `${propertyName}.$.${ARRAY_MERGE_ELEMENT_OPERATOR}`
144✔
1379
    propertyOperation[mergePropertyName] = {
144✔
1380
      type: 'object',
1381
      ...jsonSchema.items.properties ? { properties: jsonSchema.items.properties } : {},
144✔
1382
      // additionalProperties true to support dot notation
1383
      additionalProperties: true,
1384
    }
1385
  }
1386

1387
  return { ...collectionFieldsArrayOperations, ...propertyOperation }
328✔
1388
}
1389

1390
function collectionFieldsPropertiesCompatibility(collectionDefinition, validation) {
1391
  const specialTypes = validation ? specialTypesValidationCompatibility : specialTypesSerializationCompatibility
16,232✔
1392
  const collectionRawObject = {}
16,232✔
1393
  const required = []
16,232✔
1394
  let changeStateProperties = mandatoryFieldsWithoutId(false)
16,232✔
1395
  const unsetPatternProperties = {}
16,232✔
1396
  const unsetProperties = {}
16,232✔
1397
  const dateProperties = {}
16,232✔
1398

1399
  let collectionFields = {}
16,232✔
1400
  let orFields = []
16,232✔
1401
  let collectionFieldsArrayOperations = {}
16,232✔
1402

1403
  for (let i = 0; i < collectionDefinition.fields.length; i++) {
16,232✔
1404
    if (validation && mandatoryFields.has(collectionDefinition.fields[i].name)) {
175,936✔
1405
      continue
48,696✔
1406
    }
1407
    collectionFields = generateCollectionFields(
127,240✔
1408
      collectionDefinition.fields[i],
1409
      collectionDefinition.fields[i].name,
1410
      specialTypes,
1411
      collectionFields,
1412
      true
1413
    )
1414
    changeStateProperties = generateChangeStateProperties(
127,240✔
1415
      collectionDefinition.fields[i],
1416
      collectionDefinition.fields[i].name,
1417
      specialTypes,
1418
      changeStateProperties,
1419
      true
1420
    )
1421

1422
    orFields = sortRegex(orFields, collectionDefinition.fields[i].name, collectionDefinition.fields[i].type)
127,240✔
1423

1424
    if (collectionDefinition.fields[i].type === ARRAY) {
127,240✔
1425
      collectionFieldsArrayOperations = collectionFieldsArrayOperationsPropertiesCompatibility(
15,636✔
1426
        collectionDefinition.fields[i],
1427
        collectionFieldsArrayOperations
1428
      )
1429
      if (collectionDefinition.fields[i].items.type === RAWOBJECTTYPE) {
15,636✔
1430
        if (collectionDefinition.fields[i].items.schema) {
6,452✔
1431
          unsetPatternProperties[`^${collectionDefinition.fields[i].name}\\..+`] = getUnsetProperties()
3,672✔
1432
        }
1433
      }
1434
    }
1435

1436

1437
    if (collectionDefinition.fields[i].type === RAWOBJECTTYPE) {
127,240✔
1438
      if (!collectionDefinition.fields[i].schema) {
8,372✔
1439
        collectionRawObject[`${collectionDefinition.fields[i].name}.`] = true
3,688✔
1440
      }
1441
      if (collectionDefinition.fields[i].schema) {
8,372✔
1442
        unsetPatternProperties[`^${collectionDefinition.fields[i].name}\\..+`] = getUnsetProperties()
4,684✔
1443
      }
1444
    }
1445

1446
    if (collectionDefinition.fields[i].type === DATE) {
127,240✔
1447
      dateProperties[collectionDefinition.fields[i].name] = getUnsetProperties()
19,012✔
1448
    }
1449

1450
    if (!mandatoryFields.has(collectionDefinition.fields[i].name)) {
127,240✔
1451
      if (collectionDefinition.fields[i].required) {
78,544✔
1452
        required.push(collectionDefinition.fields[i].name)
34,184✔
1453
      }
1454
      if (!collectionDefinition.fields[i].required) {
78,544✔
1455
        unsetProperties[collectionDefinition.fields[i].name] = getUnsetProperties()
44,360✔
1456
      }
1457
    }
1458
  }
1459

1460
  const sort = buildSortRegex(orFields)
16,232✔
1461

1462
  return {
16,232✔
1463
    collectionFields,
1464
    collectionRawObject,
1465
    required,
1466
    sort,
1467
    changeStateProperties,
1468
    collectionFieldsArrayOperations,
1469
    unsetPatternProperties,
1470
    unsetProperties,
1471
    dateProperties,
1472
  }
1473
}
1474

1475
function collectionFieldsProperties(collectionDefinition, validation) {
1476
  if (!collectionDefinition.schema) {
16,400✔
1477
    return collectionFieldsPropertiesCompatibility(collectionDefinition, validation)
16,232✔
1478
  }
1479
  const specialTypes = validation ? specialTypesValidation : specialTypesSerialization
168✔
1480
  const collectionRawObject = {}
168✔
1481
  const required = collectionDefinition
168!
1482
    .schema
1483
    .required
1484
    ?.filter(propertyName => !mandatoryFields.has(propertyName)) ?? []
1,096✔
1485
  const unsetPatternProperties = {}
168✔
1486
  const unsetProperties = {}
168✔
1487
  const dateProperties = {}
168✔
1488

1489
  let orFields = []
168✔
1490
  let collectionFieldsArrayOperations = {}
168✔
1491
  let collectionFields = {}
168✔
1492
  let changeStateProperties = mandatoryFieldsWithoutId(true)
168✔
1493

1494
  for (const [propertyName, jsonSchema] of Object.entries(collectionDefinition.schema.properties)) {
168✔
1495
    if (validation && mandatoryFields.has(propertyName)) {
2,392✔
1496
      continue
456✔
1497
    }
1498

1499
    collectionFields = generateCollectionFields(jsonSchema, propertyName, specialTypes, collectionFields, false)
1,936✔
1500
    changeStateProperties = generateChangeStateProperties(
1,936✔
1501
      jsonSchema,
1502
      propertyName,
1503
      specialTypes,
1504
      changeStateProperties,
1505
      false
1506
    )
1507
    orFields = sortRegex(orFields, propertyName, jsonSchema.__mia_configuration?.type)
1,936✔
1508

1509
    if (!required.includes(propertyName)) {
1,936✔
1510
      unsetProperties[propertyName] = getUnsetProperties()
1,752✔
1511
    }
1512
    if (!mandatoryFields.has(propertyName) && DATE_FORMATS.includes(jsonSchema.format)) {
1,936✔
1513
      dateProperties[propertyName] = getUnsetProperties()
72✔
1514
    }
1515

1516
    if (jsonSchema.type === JSON_SCHEMA_ARRAY_TYPE) {
1,936✔
1517
      collectionFieldsArrayOperations = collectionFieldsArrayOperationsProperties(
328✔
1518
        propertyName,
1519
        jsonSchema,
1520
        collectionFieldsArrayOperations
1521
      )
1522
      if (jsonSchema.items.type === JSON_SCHEMA_OBJECT_TYPE && jsonSchema.items.properties) {
328✔
1523
        unsetPatternProperties[`^${propertyName}\\..+`] = getUnsetProperties()
72✔
1524
      }
1525
    }
1526

1527
    if (jsonSchema.type === JSON_SCHEMA_OBJECT_TYPE && !jsonSchema.__mia_configuration?.type) {
1,936✔
1528
      if (!jsonSchema.properties) {
224✔
1529
        collectionRawObject[`${propertyName}.`] = true
112✔
1530
      }
1531
      if (jsonSchema.properties) {
224✔
1532
        unsetPatternProperties[`^${propertyName}\\..+`] = getUnsetProperties()
112✔
1533
      }
1534
    }
1535
  }
1536

1537
  const sort = buildSortRegex(orFields)
168✔
1538

1539
  return {
168✔
1540
    collectionFields,
1541
    collectionRawObject,
1542
    required,
1543
    sort,
1544
    changeStateProperties,
1545
    collectionFieldsArrayOperations,
1546
    unsetPatternProperties,
1547
    unsetProperties,
1548
    dateProperties,
1549
  }
1550
}
1551

1552
function generateCollectionFields(jsonSchema, propertyName, specialTypes, collectionFields, compatibility) {
1553
  collectionFields[propertyName] = {}
129,176✔
1554
  if (jsonSchema.description) {
129,176✔
1555
    collectionFields[propertyName].description = jsonSchema.description
92,084✔
1556
  }
1557
  if (jsonSchema.nullable) {
129,176✔
1558
    collectionFields[propertyName].nullable = jsonSchema.nullable
14,612✔
1559
  }
1560
  if ([JSON_SCHEMA_ARRAY_TYPE, ARRAY].includes(jsonSchema.type)) {
129,176✔
1561
    collectionFields[propertyName] = {
15,964✔
1562
      ...generateArrayProperty(jsonSchema, specialTypes),
1563
      ...collectionFields[propertyName],
1564
    }
1565
    return collectionFields
15,964✔
1566
  }
1567
  const inheritedType = getInheritedType(jsonSchema)
113,212✔
1568
  const itemDefinitions = inheritedType === RAWOBJECTTYPE
113,212✔
1569
    ? { schema: compatibility ? jsonSchema.schema : jsonSchema }
8,596✔
1570
    : { schema: jsonSchema }
1571
  const specialTypesType = specialTypes[inheritedType] ? specialTypes[inheritedType](itemDefinitions) : undefined
113,212✔
1572
  collectionFields[propertyName] = specialTypes[inheritedType]
113,212✔
1573
    ? { ...specialTypesType, ...collectionFields[propertyName] }
1574
    : { type: inheritedType, ...collectionFields[propertyName] }
1575
  return collectionFields
113,212✔
1576
}
1577

1578
function generateChangeStateProperties(jsonSchema, propertyName, specialTypes, changeStateProperties, compatibility) {
1579
  const typesToIgnore = new Set([GEOPOINT])
129,176✔
1580
  if (
129,176✔
1581
    typesToIgnore.has(jsonSchema.__mia_configuration?.type ?? jsonSchema.type)
528,628✔
1582
    || (
1583
      [ARRAY, JSON_SCHEMA_ARRAY_TYPE].includes(jsonSchema.type)
1584
      && ![RAWOBJECTTYPE, JSON_SCHEMA_OBJECT_TYPE].includes(jsonSchema.items?.type)
1585
    )) {
1586
    return changeStateProperties
13,168✔
1587
  }
1588

1589
  changeStateProperties[propertyName] = {}
116,008✔
1590

1591
  if (jsonSchema.description) {
116,008✔
1592
    changeStateProperties[propertyName].description = jsonSchema.description
79,872✔
1593
  }
1594

1595
  if (
116,008✔
1596
    [ARRAY, JSON_SCHEMA_ARRAY_TYPE].includes(jsonSchema.type)
122,604✔
1597
  && [RAWOBJECTTYPE, JSON_SCHEMA_OBJECT_TYPE].includes(jsonSchema.items?.type)
1598
  ) {
1599
    changeStateProperties[propertyName] = {
6,596✔
1600
      ...generateArrayProperty(jsonSchema, specialTypes),
1601
      ...changeStateProperties[propertyName],
1602
    }
1603
    return changeStateProperties
6,596✔
1604
  }
1605

1606
  const inheritedType = getInheritedType(jsonSchema)
109,412✔
1607
  const itemDefinitions = inheritedType === RAWOBJECTTYPE
109,412✔
1608
    ? { schema: compatibility ? jsonSchema.schema : jsonSchema }
8,596✔
1609
    : { schema: jsonSchema }
1610
  const specialTypesType = specialTypes[inheritedType] ? specialTypes[inheritedType](itemDefinitions) : undefined
109,412✔
1611
  changeStateProperties[propertyName] = specialTypes[inheritedType]
109,412✔
1612
    ? { ...specialTypesType, ...changeStateProperties[propertyName] }
1613
    : { type: inheritedType, ...changeStateProperties[propertyName] }
1614

1615
  return changeStateProperties
109,412✔
1616
}
1617

1618
function generateArrayPropertyCompatibility(field, typesMap) {
1619
  const itemType = field.items.type
7,344✔
1620
  const itemDefinitions = {
7,344✔
1621
    schema: field.items.schema,
1622
  }
1623

1624
  return {
7,344✔
1625
    type: 'array',
1626
    items: typesMap[itemType] ? ({ ...typesMap[itemType](itemDefinitions) }) : { type: itemType },
7,344!
1627
  }
1628
}
1629

1630
function generateArrayProperty(jsonSchema, typesMap) {
1631
  if (!jsonSchema.items.properties && jsonSchema.items.schema) {
22,560✔
1632
    return generateArrayPropertyCompatibility(jsonSchema, typesMap)
7,344✔
1633
  }
1634

1635
  const itemType = getInheritedType(jsonSchema.items)
15,216✔
1636
  const itemDefinitions = {
15,216✔
1637
    schema: jsonSchema.items,
1638
  }
1639
  return {
15,216✔
1640
    type: 'array',
1641
    items: typesMap[itemType] ? ({ ...typesMap[itemType](itemDefinitions) }) : { type: itemType },
15,216✔
1642
  }
1643
}
1644

1645
function copyPropertiesFilteringAttributes(properties) {
1646
  return Object.keys(properties).reduce(
22,425✔
1647
    (acc, key) => {
1648
      const item = properties[key]
214,372✔
1649

1650
      const itemCopy = { ...item }
214,372✔
1651
      delete itemCopy.in
214,372✔
1652
      delete itemCopy.name
214,372✔
1653

1654
      acc[key] = itemCopy
214,372✔
1655
      return acc
214,372✔
1656
    },
1657
    {}
1658
  )
1659
}
1660

1661
function getIdTypeCompatibility(collectionDefinition) {
1662
  const legacyReponse = (collectionDefinition.fields.find(field => field.name === MONGOID) ?? {})
8,116!
1663
  const { type: idType } = legacyReponse
8,116✔
1664
  return idType
8,116✔
1665
}
1666

1667
function getIdType(collectionDefinition) {
1668
  if (!collectionDefinition.schema) {
8,200✔
1669
    return getIdTypeCompatibility(collectionDefinition)
8,116✔
1670
  }
1671

1672
  const response = collectionDefinition.schema.properties[MONGOID]
84✔
1673
  const { __mia_configuration: miaConfiguration, type } = response ?? {}
84✔
1674
  const { type: specialType } = miaConfiguration ?? {}
84✔
1675
  return specialType ?? type
84✔
1676
}
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