@kravc/schema 2.7.5 → 2.8.0-alpha.0

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.
Files changed (170) hide show
  1. package/README.md +19 -14
  2. package/dist/CredentialFactory.d.ts +345 -0
  3. package/dist/CredentialFactory.d.ts.map +1 -0
  4. package/dist/CredentialFactory.js +381 -0
  5. package/dist/CredentialFactory.js.map +1 -0
  6. package/dist/Schema.d.ts +448 -0
  7. package/dist/Schema.d.ts.map +1 -0
  8. package/dist/Schema.js +506 -0
  9. package/dist/Schema.js.map +1 -0
  10. package/dist/ValidationError.d.ts +70 -0
  11. package/dist/ValidationError.d.ts.map +1 -0
  12. package/dist/ValidationError.js +78 -0
  13. package/dist/ValidationError.js.map +1 -0
  14. package/dist/Validator.d.ts +483 -0
  15. package/dist/Validator.d.ts.map +1 -0
  16. package/dist/Validator.js +570 -0
  17. package/dist/Validator.js.map +1 -0
  18. package/dist/helpers/JsonSchema.d.ts +99 -0
  19. package/dist/helpers/JsonSchema.d.ts.map +1 -0
  20. package/dist/helpers/JsonSchema.js +3 -0
  21. package/dist/helpers/JsonSchema.js.map +1 -0
  22. package/dist/helpers/cleanupAttributes.d.ts +34 -0
  23. package/dist/helpers/cleanupAttributes.d.ts.map +1 -0
  24. package/dist/helpers/cleanupAttributes.js +113 -0
  25. package/dist/helpers/cleanupAttributes.js.map +1 -0
  26. package/dist/helpers/cleanupNulls.d.ts +27 -0
  27. package/dist/helpers/cleanupNulls.d.ts.map +1 -0
  28. package/dist/helpers/cleanupNulls.js +96 -0
  29. package/dist/helpers/cleanupNulls.js.map +1 -0
  30. package/dist/helpers/getReferenceIds.d.ts +169 -0
  31. package/dist/helpers/getReferenceIds.d.ts.map +1 -0
  32. package/dist/helpers/getReferenceIds.js +241 -0
  33. package/dist/helpers/getReferenceIds.js.map +1 -0
  34. package/dist/helpers/got.d.ts +60 -0
  35. package/dist/helpers/got.d.ts.map +1 -0
  36. package/dist/helpers/got.js +72 -0
  37. package/dist/helpers/got.js.map +1 -0
  38. package/dist/helpers/mapObjectProperties.d.ts +150 -0
  39. package/dist/helpers/mapObjectProperties.d.ts.map +1 -0
  40. package/dist/helpers/mapObjectProperties.js +229 -0
  41. package/dist/helpers/mapObjectProperties.js.map +1 -0
  42. package/dist/helpers/normalizeAttributes.d.ts +213 -0
  43. package/dist/helpers/normalizeAttributes.d.ts.map +1 -0
  44. package/dist/helpers/normalizeAttributes.js +243 -0
  45. package/dist/helpers/normalizeAttributes.js.map +1 -0
  46. package/dist/helpers/normalizeProperties.d.ts +168 -0
  47. package/dist/helpers/normalizeProperties.d.ts.map +1 -0
  48. package/dist/helpers/normalizeProperties.js +223 -0
  49. package/dist/helpers/normalizeProperties.js.map +1 -0
  50. package/dist/helpers/normalizeRequired.d.ts +159 -0
  51. package/dist/helpers/normalizeRequired.d.ts.map +1 -0
  52. package/dist/helpers/normalizeRequired.js +206 -0
  53. package/dist/helpers/normalizeRequired.js.map +1 -0
  54. package/dist/helpers/normalizeType.d.ts +81 -0
  55. package/dist/helpers/normalizeType.d.ts.map +1 -0
  56. package/dist/helpers/normalizeType.js +210 -0
  57. package/dist/helpers/normalizeType.js.map +1 -0
  58. package/dist/helpers/nullifyEmptyValues.d.ts +139 -0
  59. package/dist/helpers/nullifyEmptyValues.d.ts.map +1 -0
  60. package/dist/helpers/nullifyEmptyValues.js +191 -0
  61. package/dist/helpers/nullifyEmptyValues.js.map +1 -0
  62. package/dist/helpers/removeRequiredAndDefault.d.ts +106 -0
  63. package/dist/helpers/removeRequiredAndDefault.d.ts.map +1 -0
  64. package/dist/helpers/removeRequiredAndDefault.js +138 -0
  65. package/dist/helpers/removeRequiredAndDefault.js.map +1 -0
  66. package/dist/helpers/validateId.d.ts +39 -0
  67. package/dist/helpers/validateId.d.ts.map +1 -0
  68. package/dist/helpers/validateId.js +51 -0
  69. package/dist/helpers/validateId.js.map +1 -0
  70. package/dist/index.d.ts +7 -0
  71. package/dist/index.d.ts.map +1 -0
  72. package/dist/index.js +17 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/ld/documentLoader.d.ts +8 -0
  75. package/dist/ld/documentLoader.d.ts.map +1 -0
  76. package/dist/ld/documentLoader.js +24 -0
  77. package/dist/ld/documentLoader.js.map +1 -0
  78. package/dist/ld/getLinkedDataAttributeType.d.ts +10 -0
  79. package/dist/ld/getLinkedDataAttributeType.d.ts.map +1 -0
  80. package/dist/ld/getLinkedDataAttributeType.js +32 -0
  81. package/dist/ld/getLinkedDataAttributeType.js.map +1 -0
  82. package/dist/ld/getLinkedDataContext.d.ts +19 -0
  83. package/dist/ld/getLinkedDataContext.d.ts.map +1 -0
  84. package/dist/ld/getLinkedDataContext.js +50 -0
  85. package/dist/ld/getLinkedDataContext.js.map +1 -0
  86. package/eslint.config.mjs +32 -52
  87. package/examples/credentials/createAccountCredential.ts +27 -0
  88. package/examples/credentials/createMineSweeperScoreCredential.ts +115 -0
  89. package/examples/index.ts +7 -0
  90. package/examples/schemas/FavoriteItemSchema.ts +27 -0
  91. package/examples/{Preferences.yaml → schemas/Preferences.yaml} +2 -0
  92. package/examples/schemas/PreferencesSchema.ts +29 -0
  93. package/examples/schemas/ProfileSchema.ts +91 -0
  94. package/examples/schemas/Status.yaml +3 -0
  95. package/examples/schemas/StatusSchema.ts +12 -0
  96. package/jest.config.mjs +5 -0
  97. package/package.json +28 -21
  98. package/src/CredentialFactory.ts +392 -0
  99. package/src/Schema.ts +583 -0
  100. package/src/ValidationError.ts +90 -0
  101. package/src/Validator.ts +603 -0
  102. package/src/__tests__/CredentialFactory.test.ts +588 -0
  103. package/src/__tests__/Schema.test.ts +371 -0
  104. package/src/__tests__/ValidationError.test.ts +235 -0
  105. package/src/__tests__/Validator.test.ts +787 -0
  106. package/src/helpers/JsonSchema.ts +119 -0
  107. package/src/helpers/__tests__/cleanupAttributes.test.ts +943 -0
  108. package/src/helpers/__tests__/cleanupNulls.test.ts +772 -0
  109. package/src/helpers/__tests__/getReferenceIds.test.ts +975 -0
  110. package/src/helpers/__tests__/got.test.ts +193 -0
  111. package/src/helpers/__tests__/mapObjectProperties.test.ts +1126 -0
  112. package/src/helpers/__tests__/normalizeAttributes.test.ts +1435 -0
  113. package/src/helpers/__tests__/normalizeProperties.test.ts +727 -0
  114. package/src/helpers/__tests__/normalizeRequired.test.ts +669 -0
  115. package/src/helpers/__tests__/normalizeType.test.ts +772 -0
  116. package/src/helpers/__tests__/nullifyEmptyValues.test.ts +735 -0
  117. package/src/helpers/__tests__/removeRequiredAndDefault.test.ts +734 -0
  118. package/src/helpers/__tests__/validateId.test.ts +118 -0
  119. package/src/helpers/cleanupAttributes.ts +151 -0
  120. package/src/helpers/cleanupNulls.ts +106 -0
  121. package/src/helpers/getReferenceIds.ts +273 -0
  122. package/src/helpers/got.ts +73 -0
  123. package/src/helpers/mapObjectProperties.ts +272 -0
  124. package/src/helpers/normalizeAttributes.ts +247 -0
  125. package/src/helpers/normalizeProperties.ts +249 -0
  126. package/src/helpers/normalizeRequired.ts +233 -0
  127. package/src/helpers/normalizeType.ts +235 -0
  128. package/src/helpers/nullifyEmptyValues.ts +207 -0
  129. package/src/helpers/removeRequiredAndDefault.ts +151 -0
  130. package/src/helpers/validateId.ts +53 -0
  131. package/src/index.ts +13 -0
  132. package/src/ld/__tests__/documentLoader.test.ts +57 -0
  133. package/src/ld/__tests__/getLinkedDataAttributeType.test.ts +212 -0
  134. package/src/ld/__tests__/getLinkedDataContext.test.ts +378 -0
  135. package/src/ld/documentLoader.ts +28 -0
  136. package/src/ld/getLinkedDataAttributeType.ts +46 -0
  137. package/src/ld/getLinkedDataContext.ts +80 -0
  138. package/tsconfig.json +27 -0
  139. package/types/credentials-context.d.ts +14 -0
  140. package/types/security-context.d.ts +6 -0
  141. package/examples/Status.yaml +0 -3
  142. package/examples/createAccountCredential.js +0 -27
  143. package/examples/createMineSweeperScoreCredential.js +0 -63
  144. package/examples/index.js +0 -9
  145. package/src/CredentialFactory.js +0 -67
  146. package/src/CredentialFactory.spec.js +0 -131
  147. package/src/Schema.js +0 -104
  148. package/src/Schema.spec.js +0 -172
  149. package/src/ValidationError.js +0 -31
  150. package/src/Validator.js +0 -128
  151. package/src/Validator.spec.js +0 -355
  152. package/src/helpers/cleanupAttributes.js +0 -71
  153. package/src/helpers/cleanupNulls.js +0 -42
  154. package/src/helpers/getReferenceIds.js +0 -71
  155. package/src/helpers/mapObject.js +0 -65
  156. package/src/helpers/normalizeAttributes.js +0 -28
  157. package/src/helpers/normalizeProperties.js +0 -61
  158. package/src/helpers/normalizeRequired.js +0 -37
  159. package/src/helpers/normalizeType.js +0 -41
  160. package/src/helpers/nullifyEmptyValues.js +0 -57
  161. package/src/helpers/removeRequiredAndDefault.js +0 -30
  162. package/src/helpers/validateId.js +0 -19
  163. package/src/index.d.ts +0 -25
  164. package/src/index.js +0 -8
  165. package/src/ld/documentLoader.js +0 -25
  166. package/src/ld/documentLoader.spec.js +0 -12
  167. package/src/ld/getLinkedDataContext.js +0 -63
  168. package/src/ld/getLinkedDataType.js +0 -38
  169. /package/examples/{FavoriteItem.yaml → schemas/FavoriteItem.yaml} +0 -0
  170. /package/examples/{Profile.yaml → schemas/Profile.yaml} +0 -0
@@ -1,172 +0,0 @@
1
- 'use strict'
2
-
3
- const { load } = require('js-yaml')
4
- const { Schema } = require('src')
5
- const { expect } = require('chai')
6
- const { readFileSync } = require('fs')
7
-
8
- const loadSync = (yamlPath) => {
9
- const id = yamlPath.split('.')[0].split('/').reverse()[0]
10
- const source = load(readFileSync(yamlPath))
11
-
12
- return new Schema(source, id)
13
- }
14
-
15
- describe('Schema', () => {
16
- describe('Schema.constructor(source = {}, id = UNDEFINED_SCHEMA_ID)', () => {
17
- it('creates empty schema with default id', () => {
18
- const schema = new Schema()
19
-
20
- expect(schema.id).to.equal('UNDEFINED_SCHEMA_ID')
21
- expect(schema.source).to.be.empty
22
- })
23
-
24
- it('extends schema properties with types', () => {
25
- const schema = loadSync('examples/Profile.yaml')
26
-
27
- expect(schema.source.name.type).to.eql('string')
28
- expect(schema.source.gender.type).to.eql('string')
29
- expect(schema.source.tags.items.type).to.eql('string')
30
- expect(schema.source.favoriteItems.type).to.eql('array')
31
- expect(schema.source.favoriteItems.items.type).to.not.exist
32
- expect(schema.source.locations.type).to.eql('array')
33
- expect(schema.source.locations.items.type).to.eql('object')
34
- expect(schema.source.locations.items.properties.name.type).to.eql('string')
35
- expect(schema.source.locations.items.properties.address.type).to.eql('object')
36
- expect(schema.source.locations.items.properties.address.properties.zip.type).to.eql('string')
37
- expect(schema.source.locations.items.properties.address.properties.city.type).to.eql('string')
38
- expect(schema.source.locations.items.properties.address.properties.country.type).to.eql('string')
39
- expect(schema.source.locations.items.properties.address.properties.addressLine1.type).to.eql('string')
40
- expect(schema.source.locations.items.properties.address.properties.addressLine2.type).to.eql('string')
41
-
42
- const stringEnumSchema = new Schema({ enum: [ 'L', 'M', 'S'] }, 'Size')
43
-
44
- expect(stringEnumSchema.source.type).to.eql('string')
45
-
46
- const numbersEnumSchema = new Schema({ enum: [ 1, 2, 3 ], type: 'number' }, 'Points')
47
-
48
- expect(numbersEnumSchema.source.type).to.eql('number')
49
- })
50
-
51
- it('creates schema from other schemas source', () => {
52
- const entitySchema = new Schema({ name: { type: 'string' } }, 'Entity')
53
-
54
- const schema = new Schema(entitySchema, 'EntityClone')
55
-
56
- expect(schema.id).to.equal('EntityClone')
57
- expect(schema.source).to.deep.equal(entitySchema.source)
58
- })
59
- })
60
-
61
- describe('.pure(id)', () => {
62
- it('returns schema without required and default attributes', () => {
63
- const profileSchema = loadSync('examples/Profile.yaml')
64
- const updateProfileSchema = profileSchema.pure('UpdateProfile')
65
-
66
- expect(updateProfileSchema.id).to.eql('UpdateProfile')
67
-
68
- const { source } = updateProfileSchema
69
-
70
- expect(source.name.required).to.not.exist
71
- expect(source.gender.default).to.not.exist
72
- expect(source.contactDetails.required).to.not.exist
73
- expect(source.contactDetails.properties.email.required).to.not.exist
74
- expect(source.contactDetails.properties.mobileNumber.default).to.not.exist
75
- expect(source.locations.items.properties.name.require).to.not.exist
76
- expect(source.locations.items.properties.address.properties.country.required).to.not.exist
77
- expect(source.locations.items.properties.address.properties.country.default).to.not.exist
78
- expect(source.locations.items.properties.address.properties.city.required).to.not.exist
79
- expect(source.locations.items.properties.address.properties.addressLine1.required).to.not.exist
80
- expect(source.locations.items.properties.address.properties.addressLine2.required).to.not.exist
81
- expect(source.locations.items.properties.address.properties.zip.required).to.not.exist
82
- })
83
- })
84
-
85
- describe('.clone(id)', () => {
86
- it('returns schema clone', () => {
87
- const profileSchema = loadSync('examples/Profile.yaml')
88
-
89
- const schema = profileSchema.clone('ProfileClone')
90
- expect(schema.id).to.eql('ProfileClone')
91
- })
92
- })
93
-
94
- describe('.only(propertyNames, id)', () => {
95
- it('returns schema with only requested properties', () => {
96
- const profileSchema = loadSync('examples/Profile.yaml')
97
-
98
- const schema = profileSchema.only([ 'name', 'gender' ], 'ProfileClone')
99
- expect(schema.id).to.eql('ProfileClone')
100
- })
101
- })
102
-
103
- describe('.extend(properties, id)', () => {
104
- it('returns schema extended with specified properties', () => {
105
- const profileSchema = loadSync('examples/Profile.yaml')
106
-
107
- const documentSource = {
108
- createdAt: {
109
- type: 'string',
110
- format: 'date-time',
111
- required: true
112
- }
113
- }
114
-
115
- const profileDocumentSchema = profileSchema.extend(documentSource, 'ProfileDocument')
116
-
117
- expect(profileDocumentSchema.id).to.eql('ProfileDocument')
118
- expect(profileDocumentSchema.source.createdAt).to.exist
119
- })
120
- })
121
-
122
- describe('.wrap(propertyName, options = { required: true }, id)', () => {
123
- it('returns schema that wraps source schema with object property', () => {
124
- const profileSchema = loadSync('examples/Profile.yaml')
125
-
126
- const dataSchema = profileSchema.wrap('data')
127
- expect(dataSchema.id).to.eql('UNDEFINED_SCHEMA_ID')
128
- expect(dataSchema.source.data).to.exist
129
- expect(dataSchema.source.data.required).to.exist
130
-
131
- const alternativeDataSchema = profileSchema.wrap('data', { default: {} }, 'ResponseOutput')
132
- expect(alternativeDataSchema.id).to.eql('ResponseOutput')
133
- expect(alternativeDataSchema.source.data).to.exist
134
- expect(alternativeDataSchema.source.data.default).to.exist
135
- expect(alternativeDataSchema.source.data.required).to.not.exist
136
- })
137
- })
138
-
139
- describe('.jsonSchema', () => {
140
- it('returns json schema for enum type', () => {
141
- const source = {
142
- type: 'string',
143
- enum: [ 'L', 'M', 'S' ]
144
- }
145
-
146
- const sizeSchema = new Schema(source, 'Size')
147
-
148
- expect(sizeSchema.jsonSchema.id).to.eql('Size')
149
- expect(sizeSchema.jsonSchema.type).to.eql('string')
150
- expect(sizeSchema.jsonSchema.enum).to.deep.equal(source.enum)
151
- })
152
-
153
- it('returns json schema with normalized required attributes', () => {
154
- const profileSchema = loadSync('examples/Profile.yaml')
155
-
156
- const { jsonSchema } = profileSchema
157
-
158
- expect(jsonSchema).to.have.property('type', 'object')
159
- expect(jsonSchema).to.have.property('properties')
160
- expect(jsonSchema.required).to.deep.equal([ 'name', 'contactDetails' ])
161
- expect(jsonSchema.properties.contactDetails.required).to.deep.equal([ 'email' ])
162
- })
163
-
164
- it('returns json schema without required attributes', () => {
165
- const schema = new Schema({ name: { type: 'string' } }, 'Entity')
166
-
167
- const { jsonSchema } = schema
168
-
169
- expect(jsonSchema.required).to.not.exist
170
- })
171
- })
172
- })
@@ -1,31 +0,0 @@
1
- 'use strict'
2
-
3
- const { pick } = require('lodash')
4
-
5
- class ValidationError extends Error {
6
- constructor(schemaId, invalidObject, validationErrors) {
7
- super(`"${schemaId}" validation failed`)
8
-
9
- this._object = invalidObject
10
- this._schemaId = schemaId
11
- this._validationErrors = validationErrors.map(error => pick(error, [
12
- 'path',
13
- 'code',
14
- 'params',
15
- 'message',
16
- 'schemaId'
17
- ]))
18
- }
19
-
20
- toJSON() {
21
- return {
22
- code: this.constructor.name,
23
- object: this._object,
24
- message: this.message,
25
- schemaId: this._schemaId,
26
- validationErrors: this._validationErrors
27
- }
28
- }
29
- }
30
-
31
- module.exports = ValidationError
package/src/Validator.js DELETED
@@ -1,128 +0,0 @@
1
- 'use strict'
2
-
3
- const { keyBy, groupBy } = require('lodash')
4
- const ZSchema = require('z-schema')
5
- const cleanupNulls = require('./helpers/cleanupNulls')
6
- const getReferenceIds = require('./helpers/getReferenceIds')
7
- const ValidationError = require('./ValidationError')
8
- const cleanupAttributes = require('./helpers/cleanupAttributes')
9
- const nullifyEmptyValues = require('./helpers/nullifyEmptyValues')
10
- const normalizeAttributes = require('./helpers/normalizeAttributes')
11
-
12
- class Validator {
13
- constructor(schemas = []) {
14
- if (schemas.length === 0) {
15
- throw new Error('No schemas provided')
16
- }
17
-
18
- const groupsById = groupBy(schemas, 'id')
19
-
20
- for (const id in groupsById) {
21
- const schemas = groupsById[id]
22
- const hasDuplicates = schemas.length > 1
23
-
24
- if (hasDuplicates) {
25
- throw new Error(`Multiple "${id}" schemas provided`)
26
- }
27
- }
28
-
29
- this._engine = new ZSchema({
30
- reportPathAsArray: false,
31
- ignoreUnknownFormats: true,
32
- })
33
-
34
- const jsonSchemas = schemas.map(({ jsonSchema }) => jsonSchema)
35
- const isValid = this._engine.validateSchema(jsonSchemas)
36
-
37
- if (!isValid) {
38
- const json = JSON.stringify(this._engine.lastReport.errors, null, 2)
39
- throw new Error(`Schemas validation failed:\n${json}`)
40
- }
41
-
42
- this._schemasMap = keyBy(schemas, 'id')
43
- this._jsonSchemasMap = keyBy(jsonSchemas, 'id')
44
- }
45
-
46
- validate(object, schemaId, shouldNullifyEmptyValues = false, shouldCleanupNulls = false) {
47
- const jsonSchema = this._jsonSchemasMap[schemaId]
48
-
49
- if (!jsonSchema) {
50
- throw new Error(`Schema "${schemaId}" not found`)
51
- }
52
-
53
- const objectJson = JSON.stringify(object)
54
- let result = JSON.parse(objectJson)
55
-
56
- if (shouldCleanupNulls) {
57
- result = cleanupNulls(result)
58
- }
59
-
60
- try {
61
- // NOTE: Drop attributes from objects that are not defined in schema.
62
- // This is bad for FE developers, as they continue to send some
63
- // trash to endpoints, but good for integrations with third party
64
- // services, e.g. Telegram, when you do not want to define schema
65
- // for the full payload. This method currently fails for cases when
66
- // attribute is defined as object or array in schema, but value is
67
- // a string. In this case validation method below would catch that.
68
- cleanupAttributes(result, jsonSchema, this._jsonSchemasMap)
69
-
70
- // NOTE: Normalize method helps to integrate objects built from URLs,
71
- // where types are not defined, e.g. booleans are '1', 'yes' string
72
- // or numbers are '1', '2'... strings.
73
- normalizeAttributes(result, jsonSchema, this._jsonSchemasMap)
74
-
75
- // eslint-disable-next-line no-unused-vars
76
- } catch (error) {
77
- // NOTE: Skip errors in cleanup and normalize attributes methods,
78
- // validation fails for objects with invalid value types.
79
-
80
- }
81
-
82
- const isValid = this._engine.validate(result, jsonSchema)
83
-
84
- if (isValid) {
85
- return result
86
- }
87
-
88
- let validationErrors = this._engine.getLastErrors()
89
-
90
- if (!shouldNullifyEmptyValues) {
91
- throw new ValidationError(schemaId, result, validationErrors)
92
- }
93
-
94
- const [ updatedResult, updatedValidationErrors ] = nullifyEmptyValues(result, validationErrors)
95
-
96
- const hasValidationErrors = updatedValidationErrors.length > 0
97
-
98
- if (hasValidationErrors) {
99
- throw new ValidationError(schemaId, result, updatedValidationErrors)
100
- }
101
-
102
- return updatedResult
103
- }
104
-
105
- normalize(object, schemaId) {
106
- const jsonSchema = this._jsonSchemasMap[schemaId]
107
-
108
- if (!jsonSchema) {
109
- throw new Error(`Schema "${schemaId}" not found`)
110
- }
111
-
112
- const result = JSON.parse(JSON.stringify(object))
113
- normalizeAttributes(result, jsonSchema, this._jsonSchemasMap)
114
-
115
- return result
116
- }
117
-
118
- get schemasMap() {
119
- return this._schemasMap
120
- }
121
-
122
- getReferenceIds(schemaId) {
123
- const schema = this._schemasMap[schemaId]
124
- return getReferenceIds(schema, this._schemasMap)
125
- }
126
- }
127
-
128
- module.exports = Validator
@@ -1,355 +0,0 @@
1
- 'use strict'
2
-
3
- const { load } = require('js-yaml')
4
- const { expect } = require('chai')
5
- const { readFileSync } = require('fs')
6
- const { Schema, Validator } = require('src')
7
-
8
- const loadSync = (yamlPath) => {
9
- const id = yamlPath.split('.')[0].split('/').reverse()[0]
10
- const source = load(readFileSync(yamlPath))
11
-
12
- return new Schema(source, id)
13
- }
14
-
15
- const SCHEMAS = [
16
- 'examples/Status.yaml',
17
- 'examples/Profile.yaml',
18
- 'examples/Preferences.yaml',
19
- 'examples/FavoriteItem.yaml'
20
- ].map(path => loadSync(path))
21
-
22
- describe('Validator', () => {
23
- describe('Validator.constructor(schemas)', () => {
24
- it('create validator for schemas', () => {
25
- new Validator(SCHEMAS)
26
- })
27
-
28
- it('throws error if no schemas provided', () => {
29
- expect(
30
- () => new Validator()
31
- ).to.throw('No schemas provided')
32
- })
33
-
34
- it('throws error if referenced schema not found', () => {
35
- const entitySchema = new Schema({ name: { $ref: 'MissingSchema' } }, 'Entity')
36
-
37
- expect(
38
- () => new Validator([ ...SCHEMAS, entitySchema ])
39
- ).to.throw('Schemas validation failed:')
40
- })
41
- })
42
-
43
- describe('.validate(object, schemaId, shouldNullifyEmptyValues = false, shouldCleanupNulls = true)', () => {
44
- it('returns validated, cleaned and normalized object', () => {
45
- const validator = new Validator(SCHEMAS)
46
-
47
- const _createdAt = new Date().toISOString()
48
-
49
- const input = {
50
- name: 'Oleksandr',
51
- toBeRemoved: null,
52
- contactDetails: {
53
- email: 'a@kra.vc',
54
- toBeRemoved: null,
55
- },
56
- favoriteItems: [
57
- {
58
- id: '1',
59
- name: 'Student Book',
60
- categories: [ 'Education' ],
61
- toBeRemoved: null,
62
- _createdAt
63
- },
64
- ],
65
- locations: [{
66
- name: 'Home',
67
- address: {
68
- type: 'Primary',
69
- zip: '03119',
70
- city: 'Kyiv',
71
- addressLine1: 'Melnikova 83-D, 78',
72
- _createdAt
73
- },
74
- _createdAt
75
- }],
76
- preferences: {
77
- height: 180,
78
- isNotificationEnabled: true,
79
- _createdAt
80
- },
81
- status: 'Active',
82
- _createdAt
83
- }
84
-
85
- const validInput = validator.validate(input, 'Profile', false, true)
86
-
87
- expect(validInput.toBeRemoved).to.not.exist
88
- expect(validInput.contactDetails.toBeRemoved).to.not.exist
89
- expect(validInput.favoriteItems[0].toBeRemoved).to.not.exist
90
-
91
- expect(validInput._createdAt).to.not.exist
92
- expect(validInput.preferences._createdAt).to.not.exist
93
- expect(validInput.locations[0]._createdAt).to.not.exist
94
- expect(validInput.locations[0].address._createdAt).to.not.exist
95
- expect(validInput.favoriteItems[0]._createdAt).to.not.exist
96
-
97
- expect(validInput.name).to.eql('Oleksandr')
98
- expect(validInput.gender).to.eql('Other')
99
- expect(validInput.status).to.eql('Active')
100
- expect(validInput.locations[0].name).to.eql('Home')
101
- expect(validInput.locations[0].address.country).to.eql('Ukraine')
102
- expect(validInput.locations[0].address.zip).to.eql('03119')
103
- expect(validInput.locations[0].address.city).to.eql('Kyiv')
104
- expect(validInput.locations[0].address.addressLine1).to.eql('Melnikova 83-D, 78',)
105
- expect(validInput.locations[0].address.type).to.eql('Primary')
106
- expect(validInput.favoriteItems[0].id).to.eql('1')
107
- expect(validInput.favoriteItems[0].name).to.eql('Student Book')
108
- expect(validInput.favoriteItems[0].categories).to.deep.eql([ 'Education' ])
109
- expect(validInput.favoriteItems[0].status).to.eql('Pending')
110
- expect(validInput.contactDetails.email).to.eql('a@kra.vc')
111
- expect(validInput.contactDetails.mobileNumber).to.eql('380504112171')
112
- expect(validInput.preferences.height).to.eql(180)
113
- expect(validInput.preferences.isNotificationEnabled).to.eql(true)
114
- })
115
-
116
- it('normalizes object attributes according to property type', () => {
117
- const validator = new Validator(SCHEMAS)
118
-
119
- const input = {
120
- name: 'Oleksandr',
121
- contactDetails: {
122
- email: 'a@kra.vc'
123
- },
124
- preferences: {
125
- height: '180',
126
- isNotificationEnabled: 'true'
127
- }
128
- }
129
-
130
- let validInput
131
-
132
- validInput = validator.validate(input, 'Profile')
133
- expect(validInput.preferences.height).to.eql(180)
134
- expect(validInput.preferences.isNotificationEnabled).to.eql(true)
135
-
136
- input.preferences.isNotificationEnabled = '1'
137
- validInput = validator.validate(input, 'Profile')
138
- expect(validInput.preferences.isNotificationEnabled).to.eql(true)
139
-
140
- input.preferences.isNotificationEnabled = '0'
141
- validInput = validator.validate(input, 'Profile')
142
- expect(validInput.preferences.isNotificationEnabled).to.eql(false)
143
-
144
- input.preferences.isNotificationEnabled = 0
145
- validInput = validator.validate(input, 'Profile')
146
- expect(validInput.preferences.isNotificationEnabled).to.eql(false)
147
-
148
- expect(() => {
149
- input.preferences.isNotificationEnabled = 'NaN'
150
- validInput = validator.validate(input, 'Profile')
151
- expect(validInput.preferences.isNotificationEnabled).to.eql('NaN')
152
- }).to.throw('"Profile" validation failed')
153
-
154
- expect(() => {
155
- input.preferences.isNotificationEnabled = 0
156
- input.preferences.height = 'NaN'
157
- validInput = validator.validate(input, 'Profile')
158
- expect(validInput.preferences.height).to.eql('NaN')
159
- }).to.throw('"Profile" validation failed')
160
- })
161
-
162
- it('throws validation error if cleanup or normalize method failed', () => {
163
- const validator = new Validator(SCHEMAS)
164
-
165
- const input = {
166
- name: 'Oleksandr',
167
- contactDetails: {
168
- email: 'a@kra.vc'
169
- },
170
- favoriteItems: 'NOT_ARRAY_BUT_STRING'
171
- }
172
-
173
- try {
174
- validator.validate(input, 'Profile')
175
-
176
- } catch (validationError) {
177
- const error = validationError.toJSON()
178
-
179
- expect(error.object).to.exist
180
- expect(error.code).to.eql('ValidationError')
181
- expect(error.message).to.eql('"Profile" validation failed')
182
- expect(error.schemaId).to.eql('Profile')
183
-
184
- const errorMessage = error.validationErrors[0].message
185
- expect(errorMessage).to.eql('Expected type array but found type string')
186
-
187
- return
188
- }
189
-
190
- throw new Error('Validation error is not thrown')
191
- })
192
-
193
- it('throws error if validation failed', () => {
194
- const validator = new Validator(SCHEMAS)
195
-
196
- const input = {}
197
-
198
- try {
199
- validator.validate(input, 'Profile')
200
-
201
- } catch (validationError) {
202
- const error = validationError.toJSON()
203
-
204
- expect(error.object).to.exist
205
- expect(error.code).to.eql('ValidationError')
206
- expect(error.message).to.eql('"Profile" validation failed')
207
- expect(error.schemaId).to.eql('Profile')
208
-
209
- expect(error.validationErrors).to.have.lengthOf(2)
210
-
211
- return
212
- }
213
-
214
- throw new Error('Validation error is not thrown')
215
- })
216
-
217
- it('throws error if schema not found', () => {
218
- const validator = new Validator(SCHEMAS)
219
-
220
- expect(
221
- () => validator.validate({}, 'Account')
222
- ).to.throw('Schema "Account" not found')
223
- })
224
-
225
- it('throws error if multiple schemas with same id', async () => {
226
- const exampleSchema1 = new Schema({
227
- number: { required: true }
228
- }, 'Example')
229
-
230
- const exampleSchema2 = new Schema({
231
- id: {}
232
- }, 'Example')
233
-
234
- expect(() => new Validator([ exampleSchema1, exampleSchema2 ]))
235
- .to.throw('Multiple "Example" schemas provided')
236
- })
237
- })
238
-
239
- describe('.validate(object, schemaId, shouldNullifyEmptyValues = false)', () => {
240
- it('throws validation error for attributes not matching format or pattern', () => {
241
- const validator = new Validator(SCHEMAS)
242
-
243
- const input = {
244
- name: 'Oleksandr',
245
- gender: '',
246
- contactDetails: {
247
- email: 'a@kra.vc',
248
- secondaryEmail: '',
249
- mobileNumber: '',
250
- },
251
- }
252
-
253
- expect(
254
- () => validator.validate(input, 'Profile')
255
- ).to.throw('"Profile" validation failed')
256
- })
257
- })
258
-
259
- describe('.validate(object, schemaId, shouldNullifyEmptyValues = true)', () => {
260
- it('returns input with cleaned up null values for not required attributes', () => {
261
- const validator = new Validator(SCHEMAS)
262
-
263
- const input = {
264
- name: 'Oleksandr',
265
- gender: '', // ENUM
266
- contactDetails: {
267
- email: 'a@kra.vc',
268
- mobileNumber: '', // PATTERN
269
- secondaryEmail: '', // FORMAT
270
- },
271
- }
272
-
273
- const validInput = validator.validate(input, 'Profile', true)
274
-
275
- expect(validInput.gender).to.eql(null)
276
- expect(validInput.contactDetails.mobileNumber).to.eql(null)
277
- expect(validInput.contactDetails.secondaryEmail).to.eql(null)
278
- })
279
-
280
- it('throws validation errors for other attributes', () => {
281
- const validator = new Validator(SCHEMAS)
282
-
283
- const input = {
284
- name: '', // code: MIN_LENGTH
285
- gender: 'NONE', // code: ENUM_MISMATCH
286
- contactDetails: {
287
- email: 'a@kra.vc',
288
- mobileNumber: 'abc', // code: PATTERN
289
- secondaryEmail: '',
290
- },
291
- preferences: {
292
- age: 'a' // code: INVALID_TYPE
293
- },
294
- }
295
-
296
- try {
297
- validator.validate(input, 'Profile', true)
298
-
299
- } catch (validationError) {
300
- const error = validationError.toJSON()
301
-
302
- expect(error.message).to.eql('"Profile" validation failed')
303
-
304
- expect(error.validationErrors).to.have.lengthOf(4)
305
-
306
- expect(error.validationErrors[0].code).to.eql('INVALID_TYPE')
307
- expect(error.validationErrors[1].code).to.eql('PATTERN')
308
- expect(error.validationErrors[2].code).to.eql('ENUM_MISMATCH')
309
- expect(error.validationErrors[3].code).to.eql('MIN_LENGTH')
310
-
311
- return
312
- }
313
-
314
- throw new Error('Validation error is not thrown')
315
- })
316
- })
317
-
318
- describe('.normalize(object, schemaId)', () => {
319
- it('returns normalized object clone', () => {
320
- const validator = new Validator(SCHEMAS)
321
-
322
- const input = {}
323
-
324
- const normalizedInput = validator.normalize(input, 'Profile')
325
-
326
- expect(normalizedInput.gender).to.eql('Other')
327
- expect(normalizedInput.status).to.eql('Pending')
328
- })
329
-
330
- it('throws error if schema not found', () => {
331
- const validator = new Validator(SCHEMAS)
332
-
333
- expect(
334
- () => validator.normalize({}, 'Account')
335
- ).to.throw('Schema "Account" not found')
336
- })
337
- })
338
-
339
- describe('.schemasMap', () => {
340
- it('returns schemas map', () => {
341
- const validator = new Validator(SCHEMAS)
342
-
343
- expect(validator.schemasMap).to.exist
344
- })
345
- })
346
-
347
- describe('.getReferenceIds(schemaId)', () => {
348
- it('returns ids of referenced schemas', () => {
349
- const validator = new Validator(SCHEMAS)
350
- const referenceIds = validator.getReferenceIds('Profile')
351
-
352
- expect(referenceIds).to.eql([ 'Status', 'FavoriteItem', 'Preferences' ])
353
- })
354
- })
355
- })