adapt-authoring-jsonschema 0.0.1 → 1.1.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/.eslintignore +1 -1
- package/.eslintrc +14 -14
- package/.github/ISSUE_TEMPLATE/bug_report.yml +55 -55
- package/.github/ISSUE_TEMPLATE/feature_request.yml +22 -22
- package/.github/dependabot.yml +11 -11
- package/.github/pull_request_template.md +25 -25
- package/.github/workflows/labelled_prs.yml +16 -16
- package/.github/workflows/new.yml +19 -19
- package/.github/workflows/releases.yml +25 -0
- package/adapt-authoring.json +13 -13
- package/bin/check.js +43 -43
- package/conf/config.schema.json +25 -25
- package/docs/plugins/schemas-reference.js +74 -74
- package/docs/plugins/schemas-reference.md +6 -6
- package/docs/schema-examples.md +143 -143
- package/docs/schemas-introduction.md +77 -77
- package/docs/writing-a-schema.md +193 -193
- package/errors/errors.json +52 -52
- package/index.js +5 -5
- package/lib/JsonSchema.js +274 -268
- package/lib/JsonSchemaModule.js +209 -209
- package/lib/Keywords.js +74 -74
- package/lib/XSSDefaults.js +142 -142
- package/lib/typedefs.js +47 -47
- package/package.json +64 -29
- package/schema/base.schema.json +13 -13
package/lib/JsonSchema.js
CHANGED
|
@@ -1,268 +1,274 @@
|
|
|
1
|
-
import _ from 'lodash'
|
|
2
|
-
import { App, Hook } from 'adapt-authoring-core'
|
|
3
|
-
import fs from 'fs/promises'
|
|
4
|
-
import xss from 'xss'
|
|
5
|
-
|
|
6
|
-
/** @ignore */ const BASE_SCHEMA_NAME = 'base'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Functionality related to JSON schema
|
|
10
|
-
* @memberof jsonschema
|
|
11
|
-
*/
|
|
12
|
-
class JsonSchema {
|
|
13
|
-
constructor ({ enableCache, filePath, validator, xssWhitelist }) {
|
|
14
|
-
/**
|
|
15
|
-
* The raw built JSON schema
|
|
16
|
-
* @type {Object}
|
|
17
|
-
*/
|
|
18
|
-
this.built = undefined
|
|
19
|
-
/**
|
|
20
|
-
* The compiled schema validation function
|
|
21
|
-
* @type {function}
|
|
22
|
-
*/
|
|
23
|
-
this.compiled = undefined
|
|
24
|
-
/**
|
|
25
|
-
* Whether caching is enabled for this schema
|
|
26
|
-
* @type {Boolean}
|
|
27
|
-
*/
|
|
28
|
-
this.enableCache = enableCache
|
|
29
|
-
/**
|
|
30
|
-
* List of extensions for this schema
|
|
31
|
-
* @type {Array<String>}
|
|
32
|
-
*/
|
|
33
|
-
this.extensions = []
|
|
34
|
-
/**
|
|
35
|
-
* File path to the schema
|
|
36
|
-
* @type {String}
|
|
37
|
-
*/
|
|
38
|
-
this.filePath = filePath
|
|
39
|
-
/**
|
|
40
|
-
* Whether the schema is currently building
|
|
41
|
-
* @type {Boolean}
|
|
42
|
-
*/
|
|
43
|
-
this.isBuilding = false
|
|
44
|
-
/**
|
|
45
|
-
* The last build time (in milliseconds)
|
|
46
|
-
* @type {Number}
|
|
47
|
-
*/
|
|
48
|
-
this.lastBuildTime = undefined
|
|
49
|
-
/**
|
|
50
|
-
* The raw schema data for this schema (with no inheritance/extensions)
|
|
51
|
-
* @type {Object}
|
|
52
|
-
*/
|
|
53
|
-
this.raw = undefined
|
|
54
|
-
/**
|
|
55
|
-
* Reference to the Ajv validator instance
|
|
56
|
-
* @type {external:Ajv}
|
|
57
|
-
*/
|
|
58
|
-
this.validator = validator
|
|
59
|
-
/**
|
|
60
|
-
* Reference to the local XSS sanitiser instance
|
|
61
|
-
* @type {Object}
|
|
62
|
-
*/
|
|
63
|
-
this.xss = new xss.FilterXSS({ whiteList: xssWhitelist })
|
|
64
|
-
/**
|
|
65
|
-
* Hook which invokes every time the schema is built
|
|
66
|
-
* @type {Hook}
|
|
67
|
-
*/
|
|
68
|
-
this.buildHook = new Hook()
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Determines whether the current schema build is valid using last modification timestamp
|
|
73
|
-
* @returns {Boolean}
|
|
74
|
-
*/
|
|
75
|
-
async isBuildValid () {
|
|
76
|
-
if (!this.built) return false
|
|
77
|
-
let schema = this
|
|
78
|
-
while (schema) {
|
|
79
|
-
const { mtimeMs } = await fs.stat(schema.filePath)
|
|
80
|
-
if (mtimeMs > this.lastBuildTime) return false
|
|
81
|
-
schema = await schema.getParent()
|
|
82
|
-
}
|
|
83
|
-
return true
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Returs the parent schema if $merge is defined (or the base schema if a root schema)
|
|
88
|
-
* @returns {JsonSchema}
|
|
89
|
-
*/
|
|
90
|
-
async getParent () {
|
|
91
|
-
if (this.name === BASE_SCHEMA_NAME) return // base schema always the root
|
|
92
|
-
const jsonschema = await App.instance.waitForModule('jsonschema')
|
|
93
|
-
return await jsonschema.getSchema(this.raw?.$merge?.source?.$ref ?? BASE_SCHEMA_NAME)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Loads the schema file
|
|
98
|
-
* @returns {JsonSchema} This instance
|
|
99
|
-
*/
|
|
100
|
-
async load () {
|
|
101
|
-
try {
|
|
102
|
-
this.raw = JSON.parse((await fs.readFile(this.filePath)).toString())
|
|
103
|
-
this.name = this.raw.$anchor
|
|
104
|
-
} catch (e) {
|
|
105
|
-
throw App.instance.errors?.SCHEMA_LOAD_FAILED?.setData({ schemaName: this.filePath }) ?? e
|
|
106
|
-
}
|
|
107
|
-
if (this.validator.validateSchema(this.raw)?.errors) {
|
|
108
|
-
const errors = this.validator.errors.map(e => e.instancePath ? `${e.instancePath} ${e.message}` : e.message)
|
|
109
|
-
if (errors.length) {
|
|
110
|
-
throw App.instance.errors.INVALID_SCHEMA
|
|
111
|
-
.setData({ schemaName: this.name, errors: errors.join(', ') })
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return this
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Builds and compiles the schema from the $merge and $patch schemas
|
|
119
|
-
* @param {LoadSchemaOptions}
|
|
120
|
-
* @return {JsonSchema}
|
|
121
|
-
*/
|
|
122
|
-
async build (options = {}) {
|
|
123
|
-
if (options.useCache !== false && this.enableCache && await this.isBuildValid()) {
|
|
124
|
-
return this
|
|
125
|
-
}
|
|
126
|
-
if (this.isBuilding) {
|
|
127
|
-
return new Promise(resolve => this.buildHook.tap(() => resolve(this)))
|
|
128
|
-
}
|
|
129
|
-
this.isBuilding = true
|
|
130
|
-
|
|
131
|
-
const jsonschema = await App.instance.waitForModule('jsonschema')
|
|
132
|
-
const { applyExtensions, extensionFilter } = options
|
|
133
|
-
|
|
134
|
-
let built = _.cloneDeep(this.raw)
|
|
135
|
-
let parent = await this.getParent()
|
|
136
|
-
|
|
137
|
-
while (parent) {
|
|
138
|
-
const parentBuilt = _.cloneDeep((await parent.build(options)).built)
|
|
139
|
-
built = await this.patch(parentBuilt, built, { strict: !parent.name === BASE_SCHEMA_NAME })
|
|
140
|
-
parent = await parent.getParent()
|
|
141
|
-
}
|
|
142
|
-
if (this.extensions.length) {
|
|
143
|
-
await Promise.all(this.extensions.map(async s => {
|
|
144
|
-
const applyPatch = typeof extensionFilter === 'function' ? extensionFilter(s) : applyExtensions !== false
|
|
145
|
-
if (applyPatch) {
|
|
146
|
-
const extSchema = await jsonschema.getSchema(s)
|
|
147
|
-
this.patch(built, extSchema.raw, { extendAnnotations: false })
|
|
148
|
-
}
|
|
149
|
-
}))
|
|
150
|
-
}
|
|
151
|
-
this.built = built
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
this.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
*
|
|
164
|
-
* @param {
|
|
165
|
-
* @
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
*
|
|
199
|
-
* @
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
this.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
import { App, Hook } from 'adapt-authoring-core'
|
|
3
|
+
import fs from 'fs/promises'
|
|
4
|
+
import xss from 'xss'
|
|
5
|
+
|
|
6
|
+
/** @ignore */ const BASE_SCHEMA_NAME = 'base'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Functionality related to JSON schema
|
|
10
|
+
* @memberof jsonschema
|
|
11
|
+
*/
|
|
12
|
+
class JsonSchema {
|
|
13
|
+
constructor ({ enableCache, filePath, validator, xssWhitelist }) {
|
|
14
|
+
/**
|
|
15
|
+
* The raw built JSON schema
|
|
16
|
+
* @type {Object}
|
|
17
|
+
*/
|
|
18
|
+
this.built = undefined
|
|
19
|
+
/**
|
|
20
|
+
* The compiled schema validation function
|
|
21
|
+
* @type {function}
|
|
22
|
+
*/
|
|
23
|
+
this.compiled = undefined
|
|
24
|
+
/**
|
|
25
|
+
* Whether caching is enabled for this schema
|
|
26
|
+
* @type {Boolean}
|
|
27
|
+
*/
|
|
28
|
+
this.enableCache = enableCache
|
|
29
|
+
/**
|
|
30
|
+
* List of extensions for this schema
|
|
31
|
+
* @type {Array<String>}
|
|
32
|
+
*/
|
|
33
|
+
this.extensions = []
|
|
34
|
+
/**
|
|
35
|
+
* File path to the schema
|
|
36
|
+
* @type {String}
|
|
37
|
+
*/
|
|
38
|
+
this.filePath = filePath
|
|
39
|
+
/**
|
|
40
|
+
* Whether the schema is currently building
|
|
41
|
+
* @type {Boolean}
|
|
42
|
+
*/
|
|
43
|
+
this.isBuilding = false
|
|
44
|
+
/**
|
|
45
|
+
* The last build time (in milliseconds)
|
|
46
|
+
* @type {Number}
|
|
47
|
+
*/
|
|
48
|
+
this.lastBuildTime = undefined
|
|
49
|
+
/**
|
|
50
|
+
* The raw schema data for this schema (with no inheritance/extensions)
|
|
51
|
+
* @type {Object}
|
|
52
|
+
*/
|
|
53
|
+
this.raw = undefined
|
|
54
|
+
/**
|
|
55
|
+
* Reference to the Ajv validator instance
|
|
56
|
+
* @type {external:Ajv}
|
|
57
|
+
*/
|
|
58
|
+
this.validator = validator
|
|
59
|
+
/**
|
|
60
|
+
* Reference to the local XSS sanitiser instance
|
|
61
|
+
* @type {Object}
|
|
62
|
+
*/
|
|
63
|
+
this.xss = new xss.FilterXSS({ whiteList: xssWhitelist })
|
|
64
|
+
/**
|
|
65
|
+
* Hook which invokes every time the schema is built
|
|
66
|
+
* @type {Hook}
|
|
67
|
+
*/
|
|
68
|
+
this.buildHook = new Hook()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Determines whether the current schema build is valid using last modification timestamp
|
|
73
|
+
* @returns {Boolean}
|
|
74
|
+
*/
|
|
75
|
+
async isBuildValid () {
|
|
76
|
+
if (!this.built) return false
|
|
77
|
+
let schema = this
|
|
78
|
+
while (schema) {
|
|
79
|
+
const { mtimeMs } = await fs.stat(schema.filePath)
|
|
80
|
+
if (mtimeMs > this.lastBuildTime) return false
|
|
81
|
+
schema = await schema.getParent()
|
|
82
|
+
}
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Returs the parent schema if $merge is defined (or the base schema if a root schema)
|
|
88
|
+
* @returns {JsonSchema}
|
|
89
|
+
*/
|
|
90
|
+
async getParent () {
|
|
91
|
+
if (this.name === BASE_SCHEMA_NAME) return // base schema always the root
|
|
92
|
+
const jsonschema = await App.instance.waitForModule('jsonschema')
|
|
93
|
+
return await jsonschema.getSchema(this.raw?.$merge?.source?.$ref ?? BASE_SCHEMA_NAME)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Loads the schema file
|
|
98
|
+
* @returns {JsonSchema} This instance
|
|
99
|
+
*/
|
|
100
|
+
async load () {
|
|
101
|
+
try {
|
|
102
|
+
this.raw = JSON.parse((await fs.readFile(this.filePath)).toString())
|
|
103
|
+
this.name = this.raw.$anchor
|
|
104
|
+
} catch (e) {
|
|
105
|
+
throw App.instance.errors?.SCHEMA_LOAD_FAILED?.setData({ schemaName: this.filePath }) ?? e
|
|
106
|
+
}
|
|
107
|
+
if (this.validator.validateSchema(this.raw)?.errors) {
|
|
108
|
+
const errors = this.validator.errors.map(e => e.instancePath ? `${e.instancePath} ${e.message}` : e.message)
|
|
109
|
+
if (errors.length) {
|
|
110
|
+
throw App.instance.errors.INVALID_SCHEMA
|
|
111
|
+
.setData({ schemaName: this.name, errors: errors.join(', ') })
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return this
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Builds and compiles the schema from the $merge and $patch schemas
|
|
119
|
+
* @param {LoadSchemaOptions}
|
|
120
|
+
* @return {JsonSchema}
|
|
121
|
+
*/
|
|
122
|
+
async build (options = {}) {
|
|
123
|
+
if (options.useCache !== false && this.enableCache && await this.isBuildValid()) {
|
|
124
|
+
return this
|
|
125
|
+
}
|
|
126
|
+
if (this.isBuilding) {
|
|
127
|
+
return new Promise(resolve => this.buildHook.tap(() => resolve(this)))
|
|
128
|
+
}
|
|
129
|
+
this.isBuilding = true
|
|
130
|
+
|
|
131
|
+
const jsonschema = await App.instance.waitForModule('jsonschema')
|
|
132
|
+
const { applyExtensions, extensionFilter } = options
|
|
133
|
+
|
|
134
|
+
let built = _.cloneDeep(this.raw)
|
|
135
|
+
let parent = await this.getParent()
|
|
136
|
+
|
|
137
|
+
while (parent) {
|
|
138
|
+
const parentBuilt = _.cloneDeep((await parent.build({ ...options, compile: false })).built)
|
|
139
|
+
built = await this.patch(parentBuilt, built, { strict: !parent.name === BASE_SCHEMA_NAME })
|
|
140
|
+
parent = await parent.getParent()
|
|
141
|
+
}
|
|
142
|
+
if (this.extensions.length) {
|
|
143
|
+
await Promise.all(this.extensions.map(async s => {
|
|
144
|
+
const applyPatch = typeof extensionFilter === 'function' ? extensionFilter(s) : applyExtensions !== false
|
|
145
|
+
if (applyPatch) {
|
|
146
|
+
const extSchema = await jsonschema.getSchema(s)
|
|
147
|
+
this.patch(built, extSchema.raw, { extendAnnotations: false })
|
|
148
|
+
}
|
|
149
|
+
}))
|
|
150
|
+
}
|
|
151
|
+
this.built = built
|
|
152
|
+
if(options.compile !== false) { // don't compile when option present (e.g. when running build recursively)
|
|
153
|
+
this.compiled = await this.validator.compileAsync(built)
|
|
154
|
+
}
|
|
155
|
+
this.isBuilding = false
|
|
156
|
+
this.lastBuildTime = Date.now()
|
|
157
|
+
|
|
158
|
+
this.buildHook.invoke(this)
|
|
159
|
+
return this
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Applies a patch schema to another schema
|
|
164
|
+
* @param {Object} baseSchema The base schema to apply the patch
|
|
165
|
+
* @param {Object} patchSchema The patch schema to apply to the base
|
|
166
|
+
* @param {ApplyPatchOptions} options
|
|
167
|
+
* @return {Object} The base schema
|
|
168
|
+
*/
|
|
169
|
+
patch (baseSchema, patchSchema, options = {}) {
|
|
170
|
+
const opts = _.defaults(options, {
|
|
171
|
+
extendAnnotations: patchSchema.$anchor !== BASE_SCHEMA_NAME,
|
|
172
|
+
overwriteProperties: true,
|
|
173
|
+
strict: true
|
|
174
|
+
})
|
|
175
|
+
const patchData = patchSchema.$patch?.with ?? patchSchema.$merge?.with ?? (!opts.strict && patchSchema)
|
|
176
|
+
if (!patchData) {
|
|
177
|
+
throw App.instance.errors.INVALID_SCHEMA.setData({ schemaName: patchSchema.$anchor })
|
|
178
|
+
}
|
|
179
|
+
if (opts.extendAnnotations) {
|
|
180
|
+
['$anchor', 'title', 'description'].forEach(p => {
|
|
181
|
+
if (patchSchema[p]) baseSchema[p] = patchSchema[p]
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
if (patchData.properties) {
|
|
185
|
+
const mergeFunc = opts.overwriteProperties ? _.merge : _.defaultsDeep
|
|
186
|
+
mergeFunc(baseSchema.properties, patchData.properties)
|
|
187
|
+
}
|
|
188
|
+
['allOf', 'anyOf', 'oneOf'].forEach(p => {
|
|
189
|
+
if (patchData[p]?.length) baseSchema[p] = (baseSchema[p] ?? []).concat(_.cloneDeep(patchData[p]))
|
|
190
|
+
})
|
|
191
|
+
if (patchData.required) {
|
|
192
|
+
baseSchema.required = _.uniq([...(baseSchema.required ?? []), ...patchData.required])
|
|
193
|
+
}
|
|
194
|
+
return baseSchema
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Checks passed data against the specified schema (if it exists)
|
|
199
|
+
* @param {Object} dataToValidate The data to be validated
|
|
200
|
+
* @param {SchemaValidateOptions} options
|
|
201
|
+
* @return {Object} The validated data
|
|
202
|
+
*/
|
|
203
|
+
validate (dataToValidate, options) {
|
|
204
|
+
const opts = _.defaults(options, { useDefaults: true, ignoreRequired: false })
|
|
205
|
+
const data = _.defaultsDeep(_.cloneDeep(dataToValidate), opts.useDefaults ? this.getObjectDefaults() : {})
|
|
206
|
+
|
|
207
|
+
if(!this.compiled) { // fallback in the case that the compiled function is missing
|
|
208
|
+
await this.log('warn', 'NO_COMPILED_FUNC', this.name)
|
|
209
|
+
await this.validator.compileAsync(this.built)
|
|
210
|
+
}
|
|
211
|
+
this.compiled(data)
|
|
212
|
+
|
|
213
|
+
const errors = this.compiled.errors && this.compiled.errors
|
|
214
|
+
.filter(e => e.keyword === 'required' ? !opts.ignoreRequired : true)
|
|
215
|
+
.map(e => e.instancePath ? `${e.instancePath} ${e.message}` : e.message)
|
|
216
|
+
.reduce((s, e) => `${s}${e}, `, '')
|
|
217
|
+
|
|
218
|
+
if (errors?.length) { throw App.instance.errors.VALIDATION_FAILED.setData({ schemaName: this.name, errors, data }) }
|
|
219
|
+
|
|
220
|
+
return data
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Sanitises data by removing attributes according to the context (provided by options)
|
|
225
|
+
* @param {Object} dataToValidate The data to be sanitised
|
|
226
|
+
* @param {SchemaSanitiseOptions} options
|
|
227
|
+
* @return {Object} The sanitised data
|
|
228
|
+
*/
|
|
229
|
+
sanitise (dataToSanitise, options = {}, schema) {
|
|
230
|
+
const opts = _.defaults(options, { isInternal: false, isReadOnly: false, sanitiseHtml: true, strict: true })
|
|
231
|
+
schema = schema ?? this.built
|
|
232
|
+
const sanitised = {}
|
|
233
|
+
for (const prop in schema.properties) {
|
|
234
|
+
const schemaData = schema.properties[prop]
|
|
235
|
+
const value = dataToSanitise[prop]
|
|
236
|
+
const ignore = (opts.isInternal && schemaData.isInternal) || (opts.isReadOnly && schemaData.isReadOnly)
|
|
237
|
+
if (value === undefined || (ignore && !opts.strict)) {
|
|
238
|
+
continue
|
|
239
|
+
}
|
|
240
|
+
if (ignore && opts.strict) {
|
|
241
|
+
throw App.instance.errors.MODIFY_PROTECTED_ATTR.setData({ attribute: prop, value })
|
|
242
|
+
}
|
|
243
|
+
sanitised[prop] =
|
|
244
|
+
schemaData.type === 'object' && schemaData.properties
|
|
245
|
+
? this.sanitise(value, opts, schemaData)
|
|
246
|
+
: schemaData.type === 'string' && opts.sanitiseHtml
|
|
247
|
+
? this.xss.process(value)
|
|
248
|
+
: value
|
|
249
|
+
}
|
|
250
|
+
return sanitised
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Adds an extension schema
|
|
255
|
+
* @param {String} extSchemaName
|
|
256
|
+
*/
|
|
257
|
+
addExtension (extSchemaName) {
|
|
258
|
+
!this.extensions.includes(extSchemaName) && this.extensions.push(extSchemaName)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Returns all schema defaults as a correctly structured object
|
|
263
|
+
* @param {Object} schema
|
|
264
|
+
* @param {Object} memo For recursion
|
|
265
|
+
* @returns {Object} The defaults object
|
|
266
|
+
*/
|
|
267
|
+
getObjectDefaults (schema) {
|
|
268
|
+
schema = schema ?? this.built
|
|
269
|
+
const props = schema.properties ?? schema.$merge?.with?.properties ?? schema.$patch?.with?.properties
|
|
270
|
+
return _.mapValues(props, s => s.type === 'object' && s.properties ? this.getObjectDefaults(s) : s.default)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export default JsonSchema
|