@kravc/schema 2.7.6 → 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.
- package/README.md +19 -14
- package/dist/CredentialFactory.d.ts +345 -0
- package/dist/CredentialFactory.d.ts.map +1 -0
- package/dist/CredentialFactory.js +381 -0
- package/dist/CredentialFactory.js.map +1 -0
- package/dist/Schema.d.ts +448 -0
- package/dist/Schema.d.ts.map +1 -0
- package/dist/Schema.js +506 -0
- package/dist/Schema.js.map +1 -0
- package/dist/ValidationError.d.ts +70 -0
- package/dist/ValidationError.d.ts.map +1 -0
- package/dist/ValidationError.js +78 -0
- package/dist/ValidationError.js.map +1 -0
- package/dist/Validator.d.ts +483 -0
- package/dist/Validator.d.ts.map +1 -0
- package/dist/Validator.js +570 -0
- package/dist/Validator.js.map +1 -0
- package/dist/helpers/JsonSchema.d.ts +99 -0
- package/dist/helpers/JsonSchema.d.ts.map +1 -0
- package/dist/helpers/JsonSchema.js +3 -0
- package/dist/helpers/JsonSchema.js.map +1 -0
- package/dist/helpers/cleanupAttributes.d.ts +34 -0
- package/dist/helpers/cleanupAttributes.d.ts.map +1 -0
- package/dist/helpers/cleanupAttributes.js +113 -0
- package/dist/helpers/cleanupAttributes.js.map +1 -0
- package/dist/helpers/cleanupNulls.d.ts +27 -0
- package/dist/helpers/cleanupNulls.d.ts.map +1 -0
- package/dist/helpers/cleanupNulls.js +96 -0
- package/dist/helpers/cleanupNulls.js.map +1 -0
- package/dist/helpers/getReferenceIds.d.ts +169 -0
- package/dist/helpers/getReferenceIds.d.ts.map +1 -0
- package/dist/helpers/getReferenceIds.js +241 -0
- package/dist/helpers/getReferenceIds.js.map +1 -0
- package/dist/helpers/got.d.ts +60 -0
- package/dist/helpers/got.d.ts.map +1 -0
- package/dist/helpers/got.js +72 -0
- package/dist/helpers/got.js.map +1 -0
- package/dist/helpers/mapObjectProperties.d.ts +150 -0
- package/dist/helpers/mapObjectProperties.d.ts.map +1 -0
- package/dist/helpers/mapObjectProperties.js +229 -0
- package/dist/helpers/mapObjectProperties.js.map +1 -0
- package/dist/helpers/normalizeAttributes.d.ts +213 -0
- package/dist/helpers/normalizeAttributes.d.ts.map +1 -0
- package/dist/helpers/normalizeAttributes.js +243 -0
- package/dist/helpers/normalizeAttributes.js.map +1 -0
- package/dist/helpers/normalizeProperties.d.ts +168 -0
- package/dist/helpers/normalizeProperties.d.ts.map +1 -0
- package/dist/helpers/normalizeProperties.js +223 -0
- package/dist/helpers/normalizeProperties.js.map +1 -0
- package/dist/helpers/normalizeRequired.d.ts +159 -0
- package/dist/helpers/normalizeRequired.d.ts.map +1 -0
- package/dist/helpers/normalizeRequired.js +206 -0
- package/dist/helpers/normalizeRequired.js.map +1 -0
- package/dist/helpers/normalizeType.d.ts +81 -0
- package/dist/helpers/normalizeType.d.ts.map +1 -0
- package/dist/helpers/normalizeType.js +210 -0
- package/dist/helpers/normalizeType.js.map +1 -0
- package/dist/helpers/nullifyEmptyValues.d.ts +139 -0
- package/dist/helpers/nullifyEmptyValues.d.ts.map +1 -0
- package/dist/helpers/nullifyEmptyValues.js +191 -0
- package/dist/helpers/nullifyEmptyValues.js.map +1 -0
- package/dist/helpers/removeRequiredAndDefault.d.ts +106 -0
- package/dist/helpers/removeRequiredAndDefault.d.ts.map +1 -0
- package/dist/helpers/removeRequiredAndDefault.js +138 -0
- package/dist/helpers/removeRequiredAndDefault.js.map +1 -0
- package/dist/helpers/validateId.d.ts +39 -0
- package/dist/helpers/validateId.d.ts.map +1 -0
- package/dist/helpers/validateId.js +51 -0
- package/dist/helpers/validateId.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/ld/documentLoader.d.ts +8 -0
- package/dist/ld/documentLoader.d.ts.map +1 -0
- package/dist/ld/documentLoader.js +24 -0
- package/dist/ld/documentLoader.js.map +1 -0
- package/dist/ld/getLinkedDataAttributeType.d.ts +10 -0
- package/dist/ld/getLinkedDataAttributeType.d.ts.map +1 -0
- package/dist/ld/getLinkedDataAttributeType.js +32 -0
- package/dist/ld/getLinkedDataAttributeType.js.map +1 -0
- package/dist/ld/getLinkedDataContext.d.ts +19 -0
- package/dist/ld/getLinkedDataContext.d.ts.map +1 -0
- package/dist/ld/getLinkedDataContext.js +50 -0
- package/dist/ld/getLinkedDataContext.js.map +1 -0
- package/eslint.config.mjs +32 -52
- package/examples/credentials/createAccountCredential.ts +27 -0
- package/examples/credentials/createMineSweeperScoreCredential.ts +115 -0
- package/examples/index.ts +7 -0
- package/examples/schemas/FavoriteItemSchema.ts +27 -0
- package/examples/{Preferences.yaml → schemas/Preferences.yaml} +2 -0
- package/examples/schemas/PreferencesSchema.ts +29 -0
- package/examples/schemas/ProfileSchema.ts +91 -0
- package/examples/schemas/Status.yaml +3 -0
- package/examples/schemas/StatusSchema.ts +12 -0
- package/jest.config.mjs +5 -0
- package/package.json +27 -20
- package/src/CredentialFactory.ts +392 -0
- package/src/Schema.ts +583 -0
- package/src/ValidationError.ts +90 -0
- package/src/Validator.ts +603 -0
- package/src/__tests__/CredentialFactory.test.ts +588 -0
- package/src/__tests__/Schema.test.ts +371 -0
- package/src/__tests__/ValidationError.test.ts +235 -0
- package/src/__tests__/Validator.test.ts +787 -0
- package/src/helpers/JsonSchema.ts +119 -0
- package/src/helpers/__tests__/cleanupAttributes.test.ts +943 -0
- package/src/helpers/__tests__/cleanupNulls.test.ts +772 -0
- package/src/helpers/__tests__/getReferenceIds.test.ts +975 -0
- package/src/helpers/__tests__/got.test.ts +193 -0
- package/src/helpers/__tests__/mapObjectProperties.test.ts +1126 -0
- package/src/helpers/__tests__/normalizeAttributes.test.ts +1435 -0
- package/src/helpers/__tests__/normalizeProperties.test.ts +727 -0
- package/src/helpers/__tests__/normalizeRequired.test.ts +669 -0
- package/src/helpers/__tests__/normalizeType.test.ts +772 -0
- package/src/helpers/__tests__/nullifyEmptyValues.test.ts +735 -0
- package/src/helpers/__tests__/removeRequiredAndDefault.test.ts +734 -0
- package/src/helpers/__tests__/validateId.test.ts +118 -0
- package/src/helpers/cleanupAttributes.ts +151 -0
- package/src/helpers/cleanupNulls.ts +106 -0
- package/src/helpers/getReferenceIds.ts +273 -0
- package/src/helpers/got.ts +73 -0
- package/src/helpers/mapObjectProperties.ts +272 -0
- package/src/helpers/normalizeAttributes.ts +247 -0
- package/src/helpers/normalizeProperties.ts +249 -0
- package/src/helpers/normalizeRequired.ts +233 -0
- package/src/helpers/normalizeType.ts +235 -0
- package/src/helpers/nullifyEmptyValues.ts +207 -0
- package/src/helpers/removeRequiredAndDefault.ts +151 -0
- package/src/helpers/validateId.ts +53 -0
- package/src/index.ts +13 -0
- package/src/ld/__tests__/documentLoader.test.ts +57 -0
- package/src/ld/__tests__/getLinkedDataAttributeType.test.ts +212 -0
- package/src/ld/__tests__/getLinkedDataContext.test.ts +378 -0
- package/src/ld/documentLoader.ts +28 -0
- package/src/ld/getLinkedDataAttributeType.ts +46 -0
- package/src/ld/getLinkedDataContext.ts +80 -0
- package/tsconfig.json +27 -0
- package/types/credentials-context.d.ts +14 -0
- package/types/security-context.d.ts +6 -0
- package/examples/Status.yaml +0 -3
- package/examples/createAccountCredential.js +0 -27
- package/examples/createMineSweeperScoreCredential.js +0 -63
- package/examples/index.js +0 -9
- package/src/CredentialFactory.js +0 -67
- package/src/CredentialFactory.spec.js +0 -131
- package/src/Schema.js +0 -104
- package/src/Schema.spec.js +0 -172
- package/src/ValidationError.js +0 -31
- package/src/Validator.js +0 -128
- package/src/Validator.spec.js +0 -355
- package/src/helpers/cleanupAttributes.js +0 -71
- package/src/helpers/cleanupNulls.js +0 -42
- package/src/helpers/getReferenceIds.js +0 -71
- package/src/helpers/mapObject.js +0 -65
- package/src/helpers/normalizeAttributes.js +0 -28
- package/src/helpers/normalizeProperties.js +0 -61
- package/src/helpers/normalizeRequired.js +0 -37
- package/src/helpers/normalizeType.js +0 -41
- package/src/helpers/nullifyEmptyValues.js +0 -57
- package/src/helpers/removeRequiredAndDefault.js +0 -30
- package/src/helpers/validateId.js +0 -19
- package/src/index.d.ts +0 -25
- package/src/index.js +0 -8
- package/src/ld/documentLoader.js +0 -25
- package/src/ld/documentLoader.spec.js +0 -12
- package/src/ld/getLinkedDataContext.js +0 -63
- package/src/ld/getLinkedDataType.js +0 -38
- /package/examples/{FavoriteItem.yaml → schemas/FavoriteItem.yaml} +0 -0
- /package/examples/{Profile.yaml → schemas/Profile.yaml} +0 -0
package/src/Schema.spec.js
DELETED
|
@@ -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
|
-
})
|
package/src/ValidationError.js
DELETED
|
@@ -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
|
package/src/Validator.spec.js
DELETED
|
@@ -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
|
-
})
|