@kravc/schema 2.4.0 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  name:
2
2
  required: true
3
+ minLength: 3
3
4
 
4
5
  # NOTE: Referenced enum:
5
6
  status:
@@ -20,12 +21,18 @@ contactDetails:
20
21
 
21
22
  properties:
22
23
  email:
23
- type: string
24
+ type: 'string'
25
+ format: 'email'
24
26
  required: true
25
27
 
28
+ secondaryEmail:
29
+ type: 'string'
30
+ format: 'email'
31
+
26
32
  mobileNumber:
27
33
  type: string
28
- default: '+380504112171'
34
+ pattern: '^[0-9]{1,20}$'
35
+ default: '380504112171'
29
36
 
30
37
  # NOTE: Array of nested objects:
31
38
  locations:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kravc/schema",
3
- "version": "2.4.0",
3
+ "version": "2.5.1",
4
4
  "description": "Advanced JSON schema manipulation and validation library.",
5
5
  "keywords": [
6
6
  "JSON",
@@ -32,6 +32,7 @@
32
32
  "lodash.isundefined": "^3.0.1",
33
33
  "lodash.keyby": "^4.6.0",
34
34
  "lodash.pick": "^4.4.0",
35
+ "lodash.set": "^4.3.2",
35
36
  "lodash.uniq": "^4.5.0",
36
37
  "security-context": "^4.0.0",
37
38
  "validator": "^13.9.0",
package/src/Validator.js CHANGED
@@ -6,6 +6,7 @@ const ZSchema = require('z-schema')
6
6
  const getReferenceIds = require('./helpers/getReferenceIds')
7
7
  const ValidationError = require('./ValidationError')
8
8
  const cleanupAttributes = require('./helpers/cleanupAttributes')
9
+ const nullifyEmptyValues = require('./helpers/nullifyEmptyValues')
9
10
  const normalizeAttributes = require('./helpers/normalizeAttributes')
10
11
 
11
12
  class Validator {
@@ -25,7 +26,10 @@ class Validator {
25
26
  }
26
27
  }
27
28
 
28
- this._engine = new ZSchema({ ignoreUnknownFormats: true })
29
+ this._engine = new ZSchema({
30
+ reportPathAsArray: true,
31
+ ignoreUnknownFormats: true,
32
+ })
29
33
 
30
34
  const jsonSchemas = schemas.map(({ jsonSchema }) => jsonSchema)
31
35
  const isValid = this._engine.validateSchema(jsonSchemas)
@@ -39,7 +43,7 @@ class Validator {
39
43
  this._jsonSchemasMap = keyBy(jsonSchemas, 'id')
40
44
  }
41
45
 
42
- validate(object, schemaId) {
46
+ validate(object, schemaId, shouldNullifyEmptyValues = false) {
43
47
  const jsonSchema = this._jsonSchemasMap[schemaId]
44
48
 
45
49
  if (!jsonSchema) {
@@ -72,12 +76,25 @@ class Validator {
72
76
 
73
77
  const isValid = this._engine.validate(result, jsonSchema)
74
78
 
75
- if (!isValid) {
76
- const validationErrors = this._engine.getLastErrors()
79
+ if (isValid) {
80
+ return result
81
+ }
82
+
83
+ let validationErrors = this._engine.getLastErrors()
84
+
85
+ if (!shouldNullifyEmptyValues) {
77
86
  throw new ValidationError(schemaId, result, validationErrors)
78
87
  }
79
88
 
80
- return result
89
+ const [ updatedResult, updatedValidationErrors ] = nullifyEmptyValues(result, validationErrors)
90
+
91
+ const hasValidationErrors = updatedValidationErrors.length > 0
92
+
93
+ if (hasValidationErrors) {
94
+ throw new ValidationError(schemaId, result, updatedValidationErrors)
95
+ }
96
+
97
+ return updatedResult
81
98
  }
82
99
 
83
100
  normalize(object, schemaId) {
@@ -40,7 +40,7 @@ describe('Validator', () => {
40
40
  })
41
41
  })
42
42
 
43
- describe('.validate(object, schemaId)', () => {
43
+ describe('.validate(object, schemaId, shouldNullifyEmptyValues = false)', () => {
44
44
  it('returns validated, cleaned and normalized object', () => {
45
45
  const validator = new Validator(SCHEMAS)
46
46
 
@@ -99,7 +99,7 @@ describe('Validator', () => {
99
99
  expect(validInput.favoriteItems[0].categories).to.deep.eql([ 'Education' ])
100
100
  expect(validInput.favoriteItems[0].status).to.eql('Pending')
101
101
  expect(validInput.contactDetails.email).to.eql('a@kra.vc')
102
- expect(validInput.contactDetails.mobileNumber).to.eql('+380504112171')
102
+ expect(validInput.contactDetails.mobileNumber).to.eql('380504112171')
103
103
  expect(validInput.preferences.height).to.eql(180)
104
104
  expect(validInput.preferences.isNotificationEnabled).to.eql(true)
105
105
  })
@@ -227,6 +227,84 @@ describe('Validator', () => {
227
227
  })
228
228
  })
229
229
 
230
+ describe('.validate(object, schemaId, shouldNullifyEmptyValues = false)', () => {
231
+ it('throws validation error for attributes not matching format or pattern', () => {
232
+ const validator = new Validator(SCHEMAS)
233
+
234
+ const input = {
235
+ name: 'Oleksandr',
236
+ gender: '',
237
+ contactDetails: {
238
+ email: 'a@kra.vc',
239
+ secondaryEmail: '',
240
+ mobileNumber: '',
241
+ },
242
+ }
243
+
244
+ expect(
245
+ () => validator.validate(input, 'Profile')
246
+ ).to.throw('"Profile" validation failed')
247
+ })
248
+ })
249
+
250
+ describe('.validate(object, schemaId, shouldNullifyEmptyValues = true)', () => {
251
+ it('returns input with cleaned up null values for not required attributes', () => {
252
+ const validator = new Validator(SCHEMAS)
253
+
254
+ const input = {
255
+ name: 'Oleksandr',
256
+ gender: '', // ENUM
257
+ contactDetails: {
258
+ email: 'a@kra.vc',
259
+ mobileNumber: '', // PATTERN
260
+ secondaryEmail: '', // FORMAT
261
+ },
262
+ }
263
+
264
+ const validInput = validator.validate(input, 'Profile', true)
265
+
266
+ expect(validInput.gender).to.eql(null)
267
+ expect(validInput.contactDetails.mobileNumber).to.eql(null)
268
+ expect(validInput.contactDetails.secondaryEmail).to.eql(null)
269
+ })
270
+
271
+ it('throws validation errors for other attributes', () => {
272
+ const validator = new Validator(SCHEMAS)
273
+
274
+ const input = {
275
+ name: '', // code: MIN_LENGTH
276
+ gender: 'NONE', // code: ENUM_MISMATCH
277
+ contactDetails: {
278
+ email: 'a@kra.vc',
279
+ mobileNumber: 'abc', // code: PATTERN
280
+ secondaryEmail: '',
281
+ },
282
+ preferences: {
283
+ age: 'a' // code: INVALID_TYPE
284
+ },
285
+ }
286
+
287
+ try {
288
+ validator.validate(input, 'Profile', true)
289
+
290
+ } catch (validationError) {
291
+ const error = validationError.toJSON()
292
+
293
+ expect(error.message).to.eql('"Profile" validation failed')
294
+ expect(error.validationErrors).to.have.lengthOf(4)
295
+
296
+ expect(error.validationErrors[0].code).to.eql('INVALID_TYPE')
297
+ expect(error.validationErrors[1].code).to.eql('PATTERN')
298
+ expect(error.validationErrors[2].code).to.eql('ENUM_MISMATCH')
299
+ expect(error.validationErrors[3].code).to.eql('MIN_LENGTH')
300
+
301
+ return
302
+ }
303
+
304
+ throw new Error('Validation error is not thrown')
305
+ })
306
+ })
307
+
230
308
  describe('.normalize(object, schemaId)', () => {
231
309
  it('returns normalized object clone', () => {
232
310
  const validator = new Validator(SCHEMAS)
@@ -10,6 +10,7 @@ const normalizeRequired = jsonSchema => {
10
10
  const property = properties[name]
11
11
 
12
12
  if (property.required) {
13
+ property['x-required'] = true
13
14
  required.push(name)
14
15
  }
15
16
 
@@ -0,0 +1,55 @@
1
+ 'use strict'
2
+
3
+ const get = require('lodash.get')
4
+ const set = require('lodash.set')
5
+ const { schemaSymbol, jsonSymbol } = require('z-schema')
6
+
7
+ const FORMAT_ERROR_CODES = [
8
+ 'PATTERN',
9
+ 'ENUM_MISMATCH',
10
+ 'INVALID_FORMAT'
11
+ ]
12
+
13
+ const EMPTY_VALUES = [
14
+ '',
15
+ ]
16
+
17
+ const nullifyEmptyValues = (object, validationErrors) => {
18
+ const objectJson = JSON.stringify(object)
19
+ const result = JSON.parse(objectJson)
20
+
21
+ const otherValidationErrors = []
22
+
23
+ for (const error of validationErrors) {
24
+ const { code, path } = error
25
+
26
+ const isAttributeRequired = error[schemaSymbol]['x-required'] === true
27
+ const isFormatError = FORMAT_ERROR_CODES.includes(code)
28
+
29
+ if (isAttributeRequired) {
30
+ otherValidationErrors.push(error)
31
+ continue
32
+ }
33
+
34
+ if (!isFormatError) {
35
+ otherValidationErrors.push(error)
36
+ continue
37
+ }
38
+
39
+ const json = error[jsonSymbol]
40
+ const value = get(json, path)
41
+
42
+ const isEmptyValue = EMPTY_VALUES.includes(value)
43
+
44
+ if (!isEmptyValue) {
45
+ otherValidationErrors.push(error)
46
+ continue
47
+ }
48
+
49
+ set(result, path, null)
50
+ }
51
+
52
+ return [ result, otherValidationErrors ]
53
+ }
54
+
55
+ module.exports = nullifyEmptyValues