adapt-authoring-jsonschema 1.1.4 → 1.2.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.
@@ -1,16 +1,12 @@
1
- import _ from 'lodash'
2
- import { AbstractModule, Hook } from 'adapt-authoring-core'
3
- import Ajv from 'ajv/dist/2020.js'
1
+ import { AbstractModule, App, Hook } from 'adapt-authoring-core'
4
2
  import { glob } from 'glob'
5
- import JsonSchema from './JsonSchema.js'
6
- import Keywords from './Keywords.js'
7
3
  import path from 'path'
8
- import safeRegex from 'safe-regex'
9
- import XSSDefaults from './XSSDefaults.js'
4
+ import { Schemas, SchemaError, XSSDefaults } from 'adapt-schemas'
10
5
 
11
- const BASE_SCHEMA_PATH = './schema/base.schema.json'
12
6
  /**
13
- * Module which add support for the JSON Schema specification
7
+ * Module which adds support for the JSON Schema specification.
8
+ * This is a thin wrapper around the adapt-schemas library providing
9
+ * Adapt framework integration (hooks, logging, config, errors).
14
10
  * @memberof jsonschema
15
11
  * @extends {AbstractModule}
16
12
  */
@@ -18,99 +14,141 @@ class JsonSchemaModule extends AbstractModule {
18
14
  /** @override */
19
15
  async init () {
20
16
  this.app.jsonschema = this
21
- /**
22
- * Reference to all registed schemas
23
- * @type {Object}
24
- */
25
- this.schemas = {}
26
- /**
27
- * Temporary store of extension schemas
28
- * @type {Object}
29
- */
30
- this.schemaExtensions = {}
31
17
  /**
32
18
  * Invoked when schemas are registered
33
19
  * @type {Hook}
34
20
  */
35
21
  this.registerSchemasHook = new Hook()
36
22
  /**
37
- * Tags and attributes to be whitelisted by the XSS filter
38
- * @type {Object}
39
- */
40
- this.xssWhitelist = {}
41
- /**
42
- * Reference to the Ajv instance
43
- * @type {external:Ajv}
23
+ * Internal schema library instance
24
+ * @type {Schemas}
44
25
  */
45
- this.validator = new Ajv({
46
- addUsedSchema: false,
47
- allErrors: true,
48
- allowUnionTypes: true,
49
- loadSchema: this.getSchema.bind(this),
50
- removeAdditional: 'all',
51
- strict: false,
52
- verbose: true,
53
- keywords: Keywords.all
26
+ this._library = new Schemas({
27
+ enableCache: true // Will be overridden from config when ready
54
28
  })
55
- this.addStringFormats({
56
- 'date-time': /[A-za-z0-9:+()]+/,
57
- time: /^(\d{2}):(\d{2}):(\d{2})\+(\d{2}):(\d{2})$/,
58
- uri: /^(.+):\/\/(www\.)?[-a-zA-Z0-9@:%_+.~#?&//=]{1,256}/
29
+ // Forward library events to module logging
30
+ this._library.on('warning', msg => this.log('warn', msg))
31
+ this._library.on('schemaRegistered', (name, filePath) => this.log('verbose', 'REGISTER_SCHEMA', name, filePath))
32
+ this._library.on('schemaDeregistered', name => this.log('debug', 'DEREGISTER_SCHEMA', name))
33
+ this._library.on('schemaExtended', (base, ext) => this.log('verbose', 'EXTEND_SCHEMA', base, ext))
34
+ this._library.on('reset', () => this.log('debug', 'RESET_SCHEMAS'))
35
+
36
+ await this._library.init()
37
+
38
+ this._library.addKeyword({
39
+ keyword: 'isDirectory',
40
+ type: 'string',
41
+ modifying: true,
42
+ schemaType: 'boolean',
43
+ compile: function () {
44
+ const doReplace = value => {
45
+ const app = App.instance
46
+ return [
47
+ ['$ROOT', app.rootDir],
48
+ ['$DATA', app.getConfig('dataDir')],
49
+ ['$TEMP', app.getConfig('tempDir')]
50
+ ].reduce((m, [k, v]) => {
51
+ return m.startsWith(k) ? path.resolve(v, m.replace(k, '').slice(1)) : m
52
+ }, value)
53
+ }
54
+ return (value, { parentData, parentDataProperty }) => {
55
+ try {
56
+ parentData[parentDataProperty] = doReplace(value)
57
+ } catch (e) {}
58
+ return true
59
+ }
60
+ }
59
61
  })
60
- await this.resetSchemaRegistry()
61
62
 
62
63
  this.onReady()
63
64
  .then(() => this.app.waitForModule('config', 'errors'))
64
65
  .then(() => {
65
- Object.assign(this.xssWhitelist,
66
+ // Update library options from config
67
+ this._library.options.enableCache = this.getConfig('enableCache')
68
+
69
+ // Update XSS whitelist
70
+ Object.assign(
71
+ this._library.xssWhitelist,
66
72
  this.getConfig('xssWhitelistOverride') ? {} : XSSDefaults,
67
- this.getConfig('xssWhitelist'))
73
+ this.getConfig('xssWhitelist')
74
+ )
68
75
  })
69
- .then(() => this.addStringFormats(this.getConfig('formatOverrides')))
70
- .then(() => this.registerSchemas({ quiet: true })) // note: supress logging here as other schemas will likely be added
76
+ .then(() => {
77
+ // Add format overrides from config
78
+ const formatOverrides = this.getConfig('formatOverrides')
79
+ if (formatOverrides) {
80
+ this._library.addStringFormats(formatOverrides)
81
+ }
82
+ })
83
+ .then(() => this.registerSchemas({ quiet: true }))
71
84
  .catch(e => this.log('error', e))
72
85
 
73
86
  this.app.onReady()
74
87
  .then(() => this.logSchemas())
75
88
  }
76
89
 
90
+ /**
91
+ * Reference to all registered schemas
92
+ * @type {Object}
93
+ */
94
+ get schemas () {
95
+ return this._library.schemas
96
+ }
97
+
98
+ /**
99
+ * Temporary store of extension schemas
100
+ * @type {Object}
101
+ */
102
+ get schemaExtensions () {
103
+ return this._library.schemaExtensions
104
+ }
105
+
106
+ /**
107
+ * Tags and attributes to be whitelisted by the XSS filter
108
+ * @type {Object}
109
+ */
110
+ get xssWhitelist () {
111
+ return this._library.xssWhitelist
112
+ }
113
+
114
+ /**
115
+ * Reference to the Ajv instance
116
+ * @type {external:Ajv}
117
+ */
118
+ get validator () {
119
+ return this._library.validator
120
+ }
121
+
77
122
  /**
78
123
  * Empties the schema registry (with the exception of the base schema)
79
124
  */
80
125
  async resetSchemaRegistry () {
81
- this.log('debug', 'RESET_SCHEMAS')
82
- this.schemas = {
83
- base: await this.createSchema(path.resolve(this.rootDir, BASE_SCHEMA_PATH), { enableCache: true })
84
- }
126
+ await this._library.resetSchemaRegistry()
85
127
  }
86
128
 
87
129
  /**
88
130
  * Adds string formats to the Ajv validator
131
+ * @param {Object} formats Object mapping format names to RegExp patterns
89
132
  */
90
133
  addStringFormats (formats) {
91
- Object.entries(formats).forEach(([name, re]) => {
92
- const isUnsafe = !safeRegex(re)
93
- if (isUnsafe) this.log('warn', `unsafe RegExp for format '${name}' (${re}), using default`)
94
- this.validator.addFormat(name, isUnsafe ? /.*/ : re)
95
- })
134
+ this._library.addStringFormats(formats)
96
135
  }
97
136
 
98
137
  /**
99
138
  * Adds a new keyword to be used in JSON schemas
100
- * @param {AjvKeyword} definition
139
+ * @param {Object} definition AJV keyword definition
140
+ * @param {Object} options Configuration options
141
+ * @param {Boolean} options.override Whether to override an existing definition
101
142
  */
102
- addKeyword (definition) {
103
- try {
104
- this.validator.addKeyword(definition)
105
- } catch (e) {
106
- this.log('warn', `failed to define keyword '${definition.keyword}', ${e}`)
107
- }
143
+ addKeyword (definition, options) {
144
+ this._library.addKeyword(definition, options)
108
145
  }
109
146
 
110
147
  /**
111
- * Searches all Adapt dependencies for any local JSON schemas and registers them for use in the app. Schemas must be located in in a `/schema` folder, and be named appropriately: `*.schema.json`.
112
- * @param options {object}
113
- * @param options.quiet {Boolean} Set to true to suppress logs
148
+ * Searches all Adapt dependencies for any local JSON schemas and registers them for use in the app.
149
+ * Schemas must be located in a `/schema` folder, and be named appropriately: `*.schema.json`.
150
+ * @param {Object} options
151
+ * @param {Boolean} options.quiet Set to true to suppress logs
114
152
  * @return {Promise}
115
153
  */
116
154
  async registerSchemas (options = {}) {
@@ -118,7 +156,8 @@ class JsonSchemaModule extends AbstractModule {
118
156
  await Promise.all(Object.values(this.app.dependencies).map(async d => {
119
157
  if (d.name === this.name) return
120
158
  const files = await glob('schema/*.schema.json', { cwd: d.rootDir, absolute: true })
121
- ;(await Promise.allSettled(files.map(f => this.registerSchema(f))))
159
+ const results = await Promise.allSettled(files.map(f => this.registerSchema(f)))
160
+ results
122
161
  .filter(r => r.status === 'rejected')
123
162
  .forEach(r => this.log('warn', r.reason))
124
163
  }))
@@ -129,57 +168,44 @@ class JsonSchemaModule extends AbstractModule {
129
168
  /**
130
169
  * Registers a single JSON schema for use in the app
131
170
  * @param {String} filePath Path to the schema file
132
- * @param {RegisterSchemaOptions} options Extra options
133
- * @return {Promise}
171
+ * @param {Object} options Extra options
172
+ * @param {Boolean} options.replace Replace existing schema with same name
173
+ * @return {Promise<Schema>}
134
174
  */
135
175
  async registerSchema (filePath, options = {}) {
136
- if (!_.isString(filePath)) {
137
- throw this.app.errors.INVALID_PARAMS.setData({ params: ['filePath'] })
138
- }
139
- const schema = await this.createSchema(filePath, options)
140
-
141
- if (this.schemas[schema.name]) {
142
- if (options.replace) this.deregisterSchema(schema.name)
143
- else throw this.app.errors.SCHEMA_EXISTS.setData({ schemaName: schema.name, filePath })
176
+ try {
177
+ return await this._library.registerSchema(filePath, options)
178
+ } catch (e) {
179
+ // Convert library errors to app errors
180
+ if (e instanceof SchemaError) {
181
+ const appError = this.app.errors[e.code]
182
+ if (appError) {
183
+ throw appError.setData(e.data)
184
+ }
185
+ }
186
+ throw e
144
187
  }
145
- this.schemas[schema.name] = schema
146
- this.schemaExtensions?.[schema.name]?.forEach(s => schema.addExtension(s))
147
- if (schema.raw.$patch) this.extendSchema(schema.raw.$patch?.source?.$ref, schema.name)
148
-
149
- this.log('verbose', 'REGISTER_SCHEMA', schema.name, filePath)
150
188
  }
151
189
 
152
190
  /**
153
- * deregisters a single JSON schema
154
- * @param {String} name Schem name to deregister
155
- * @return {Promise} Resolves with schema data
191
+ * Deregisters a single JSON schema
192
+ * @param {String} name Schema name to deregister
156
193
  */
157
194
  deregisterSchema (name) {
158
- if (this.schemas[name]) delete this.schemas[name]
159
- // remove schema from any extensions lists
160
- Object.entries(this.schemaExtensions).forEach(([base, extensions]) => {
161
- this.schemaExtensions[base] = extensions.filter(s => s !== name)
162
- })
163
- this.log('debug', 'DEREGISTER_SCHEMA', name)
195
+ this._library.deregisterSchema(name)
164
196
  }
165
197
 
166
198
  /**
167
- * Creates a new JsonSchema instance
199
+ * Creates a new Schema instance
168
200
  * @param {String} filePath Path to the schema file
169
- * @param {Object} options Options passed to JsonSchema constructor
170
- * @returns {JsonSchema}
201
+ * @param {Object} options Options passed to Schema constructor
202
+ * @returns {Promise<Schema>}
171
203
  */
172
- createSchema (filePath, options) {
173
- const schema = new JsonSchema({
204
+ createSchema (filePath, options = {}) {
205
+ return this._library.createSchema(filePath, {
174
206
  enableCache: this.getConfig('enableCache'),
175
- filePath,
176
- validator: this.validator,
177
- xssWhitelist: this.xssWhitelist,
178
207
  ...options
179
208
  })
180
- this.schemaExtensions?.[schema.name]?.forEach(s => schema.addExtension(s))
181
- delete this.schemaExtensions?.[schema.name]
182
- return schema.load()
183
209
  }
184
210
 
185
211
  /**
@@ -188,27 +214,26 @@ class JsonSchemaModule extends AbstractModule {
188
214
  * @param {String} extSchemaName The name of the schema to extend with
189
215
  */
190
216
  extendSchema (baseSchemaName, extSchemaName) {
191
- const baseSchema = this.schemas[baseSchemaName]
192
- if (baseSchema) {
193
- baseSchema.addExtension(extSchemaName)
194
- } else {
195
- if (!this.schemaExtensions[baseSchemaName]) this.schemaExtensions[baseSchemaName] = []
196
- this.schemaExtensions[baseSchemaName].push(extSchemaName)
197
- }
198
- this.log('verbose', 'EXTEND_SCHEMA', baseSchemaName, extSchemaName)
217
+ this._library.extendSchema(baseSchemaName, extSchemaName)
199
218
  }
200
219
 
201
220
  /**
202
- * Retrieves the specified schema. Recursively applies any schema merge/patch schemas. Will returned cached data if enabled.
221
+ * Retrieves the specified schema. Recursively applies any schema merge/patch schemas.
222
+ * Will return cached data if enabled.
203
223
  * @param {String} schemaName The name of the schema to return
204
- * @param {LoadSchemaOptions} options
224
+ * @param {Object} options
205
225
  * @param {Boolean} options.compiled If false, the raw schema will be returned
206
- * @return {Promise} The compiled schema validation function (default) or the raw schema
226
+ * @return {Promise<Schema>} The schema instance
207
227
  */
208
228
  async getSchema (schemaName, options = {}) {
209
- const schema = this.schemas[schemaName]
210
- if (!schema) throw this.app.errors.MISSING_SCHEMA.setData({ schemaName })
211
- return schema.build(options)
229
+ try {
230
+ return await this._library.getSchema(schemaName, options)
231
+ } catch (e) {
232
+ if (e instanceof SchemaError && e.code === 'MISSING_SCHEMA') {
233
+ throw this.app.errors.MISSING_SCHEMA.setData({ schemaName })
234
+ }
235
+ throw e
236
+ }
212
237
  }
213
238
 
214
239
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adapt-authoring-jsonschema",
3
- "version": "1.1.4",
3
+ "version": "1.2.0",
4
4
  "description": "Module to add support for the JSON schema specification",
5
5
  "homepage": "https://github.com/adapt-security/adapt-authoring-jsonschema",
6
6
  "license": "GPL-3.0",
@@ -11,26 +11,15 @@
11
11
  },
12
12
  "repository": "github:adapt-security/adapt-authoring-jsonschema",
13
13
  "dependencies": {
14
- "ajv": "^8.12.0",
15
- "bytes": "^3.1.2",
16
- "glob": "^13.0.0",
17
- "lodash": "^4.17.21",
18
- "ms": "^2.1.3",
19
- "safe-regex": "2.1.1",
20
- "xss": "^1.0.14"
14
+ "adapt-schemas": "^1.0.0"
21
15
  },
22
16
  "peerDependencies": {
23
17
  "adapt-authoring-core": "github:adapt-security/adapt-authoring-core"
24
18
  },
25
19
  "devDependencies": {
26
- "@semantic-release/commit-analyzer": "^13.0.1",
27
20
  "@semantic-release/git": "^10.0.1",
28
- "@semantic-release/github": "^12.0.2",
29
- "@semantic-release/npm": "^13.1.2",
30
- "@semantic-release/release-notes-generator": "^14.1.0",
31
21
  "conventional-changelog-eslint": "^6.0.0",
32
22
  "semantic-release": "^25.0.2",
33
- "semantic-release-replace-plugin": "^1.2.7",
34
23
  "standard": "^17.1.0"
35
24
  },
36
25
  "release": {
@@ -49,15 +38,7 @@
49
38
  ],
50
39
  "@semantic-release/npm",
51
40
  "@semantic-release/github",
52
- [
53
- "@semantic-release/git",
54
- {
55
- "assets": [
56
- "package.json"
57
- ],
58
- "message": "Chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
59
- }
60
- ]
41
+ "@semantic-release/git"
61
42
  ]
62
43
  }
63
44
  }
package/lib/JsonSchema.js DELETED
@@ -1,273 +0,0 @@
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 = _.defaults(_.cloneDeep(dataToValidate), opts.useDefaults ? this.getObjectDefaults() : {})
206
- if (!this.compiled) { // fallback in the case that the compiled function is missing
207
- this.log('warn', 'NO_COMPILED_FUNC', this.name)
208
- this.validator.compile(this.built)
209
- }
210
- this.compiled(data)
211
-
212
- const errors = this.compiled.errors && this.compiled.errors
213
- .filter(e => e.keyword === 'required' ? !opts.ignoreRequired : true)
214
- .map(e => e.instancePath ? `${e.instancePath} ${e.message}` : e.message)
215
- .reduce((s, e) => `${s}${e}, `, '')
216
-
217
- if (errors?.length) { throw App.instance.errors.VALIDATION_FAILED.setData({ schemaName: this.name, errors, data }) }
218
-
219
- return data
220
- }
221
-
222
- /**
223
- * Sanitises data by removing attributes according to the context (provided by options)
224
- * @param {Object} dataToValidate The data to be sanitised
225
- * @param {SchemaSanitiseOptions} options
226
- * @return {Object} The sanitised data
227
- */
228
- sanitise (dataToSanitise, options = {}, schema) {
229
- const opts = _.defaults(options, { isInternal: false, isReadOnly: false, sanitiseHtml: true, strict: true })
230
- schema = schema ?? this.built
231
- const sanitised = {}
232
- for (const prop in schema.properties) {
233
- const schemaData = schema.properties[prop]
234
- const value = dataToSanitise[prop]
235
- const ignore = (opts.isInternal && schemaData.isInternal) || (opts.isReadOnly && schemaData.isReadOnly)
236
- if (value === undefined || (ignore && !opts.strict)) {
237
- continue
238
- }
239
- if (ignore && opts.strict) {
240
- throw App.instance.errors.MODIFY_PROTECTED_ATTR.setData({ attribute: prop, value })
241
- }
242
- sanitised[prop] =
243
- schemaData.type === 'object' && schemaData.properties
244
- ? this.sanitise(value, opts, schemaData)
245
- : schemaData.type === 'string' && opts.sanitiseHtml
246
- ? this.xss.process(value)
247
- : value
248
- }
249
- return sanitised
250
- }
251
-
252
- /**
253
- * Adds an extension schema
254
- * @param {String} extSchemaName
255
- */
256
- addExtension (extSchemaName) {
257
- !this.extensions.includes(extSchemaName) && this.extensions.push(extSchemaName)
258
- }
259
-
260
- /**
261
- * Returns all schema defaults as a correctly structured object
262
- * @param {Object} schema
263
- * @param {Object} memo For recursion
264
- * @returns {Object} The defaults object
265
- */
266
- getObjectDefaults (schema) {
267
- schema = schema ?? this.built
268
- const props = schema.properties ?? schema.$merge?.with?.properties ?? schema.$patch?.with?.properties
269
- return _.mapValues(props, s => s.type === 'object' && s.properties ? this.getObjectDefaults(s) : s.default)
270
- }
271
- }
272
-
273
- export default JsonSchema
package/lib/Keywords.js DELETED
@@ -1,74 +0,0 @@
1
- import { App } from 'adapt-authoring-core'
2
- import bytes from 'bytes'
3
- import ms from 'ms'
4
- import path from 'path'
5
- /**
6
- * Adds some useful schema keywords
7
- * @memberof jsonschema
8
- * @extends {AbstractModule}
9
- */
10
- class Keywords {
11
- static get all () {
12
- const keywords = {
13
- isBytes: function () {
14
- return (value, { parentData, parentDataProperty }) => {
15
- try {
16
- parentData[parentDataProperty] = bytes.parse(value)
17
- return true
18
- } catch (e) {
19
- return false
20
- }
21
- }
22
- },
23
- isDate: function () {
24
- return (value, { parentData, parentDataProperty }) => {
25
- try {
26
- parentData[parentDataProperty] = new Date(value)
27
- return true
28
- } catch (e) {
29
- return false
30
- }
31
- }
32
- },
33
- isDirectory: function () {
34
- const doReplace = value => {
35
- const app = App.instance
36
- return [
37
- ['$ROOT', app.rootDir],
38
- ['$DATA', app.getConfig('dataDir')],
39
- ['$TEMP', app.getConfig('tempDir')]
40
- ].reduce((m, [k, v]) => {
41
- return m.startsWith(k) ? path.resolve(v, m.replace(k, '').slice(1)) : m
42
- }, value)
43
- }
44
- return (value, { parentData, parentDataProperty }) => {
45
- try {
46
- parentData[parentDataProperty] = doReplace(value)
47
- } catch (e) {}
48
- return true
49
- }
50
- },
51
- isTimeMs: function () {
52
- return (value, { parentData, parentDataProperty }) => {
53
- try {
54
- parentData[parentDataProperty] = ms(value)
55
- return true
56
- } catch (e) {
57
- return false
58
- }
59
- }
60
- }
61
- }
62
- return Object.entries(keywords).map(([keyword, compile]) => {
63
- return {
64
- keyword,
65
- type: 'string',
66
- modifying: true,
67
- schemaType: 'boolean',
68
- compile
69
- }
70
- })
71
- }
72
- }
73
-
74
- export default Keywords
@@ -1,142 +0,0 @@
1
- export default {
2
- a: ['class', 'href', 'rel', 'target', 'title'],
3
- abbr: ['title'],
4
- address: [],
5
- area: ['alt', 'coords', 'href', 'shape'],
6
- article: [],
7
- aside: ['aria-hidden', 'class', 'role'],
8
- audio: [
9
- 'autoplay',
10
- 'controls',
11
- 'crossorigin',
12
- 'loop',
13
- 'muted',
14
- 'preload',
15
- 'src'
16
- ],
17
- b: [],
18
- bdi: ['dir'],
19
- bdo: ['dir'],
20
- big: [],
21
- blockquote: ['cite'],
22
- br: [],
23
- button: ['class'],
24
- caption: [],
25
- center: [],
26
- cite: [],
27
- code: [],
28
- col: ['align', 'span', 'valign', 'width'],
29
- colgroup: ['align', 'span', 'valign', 'width'],
30
- data: [],
31
- dd: [],
32
- del: ['datetime'],
33
- dfn: [],
34
- details: ['open'],
35
- div: [
36
- 'aria-describedby',
37
- 'aria-description',
38
- 'aria-label',
39
- 'aria-hidden',
40
- 'class',
41
- 'role',
42
- 'tabindex'
43
- ],
44
- dl: [],
45
- dt: [],
46
- em: [],
47
- figcaption: [],
48
- figure: ['class'],
49
- font: ['color', 'face', 'size'],
50
- footer: [],
51
- h1: ['class'],
52
- h2: ['class'],
53
- h3: ['class'],
54
- h4: ['class'],
55
- h5: ['class'],
56
- h6: ['class'],
57
- header: [],
58
- hr: [],
59
- i: [],
60
- img: [
61
- 'alt',
62
- 'aria-hidden',
63
- 'aria-label',
64
- 'class',
65
- 'height',
66
- 'loading',
67
- 'src',
68
- 'title',
69
- 'width'
70
- ],
71
- ins: ['datetime'],
72
- kbd: [],
73
- li: ['class'],
74
- mark: [],
75
- math: [],
76
- mfrac: [],
77
- mi: [],
78
- mn: [],
79
- mo: [],
80
- mover: [],
81
- mrow: [],
82
- ms: [],
83
- mspace: [],
84
- msub: [],
85
- msubsup: [],
86
- msup: [],
87
- mtext: [],
88
- munder: [],
89
- munderover: [],
90
- nav: [],
91
- ol: ['class'],
92
- p: ['lang'],
93
- pre: [],
94
- q: [],
95
- rp: [],
96
- rt: [],
97
- ruby: [],
98
- s: [],
99
- samp: [],
100
- section: [],
101
- small: [],
102
- span: [
103
- 'aria-describedby',
104
- 'aria-description',
105
- 'aria-label',
106
- 'aria-hidden',
107
- 'class',
108
- 'role',
109
- 'tabindex'
110
- ],
111
- sub: [],
112
- summary: [],
113
- sup: [],
114
- strong: [],
115
- strike: [],
116
- table: ['align', 'border', 'width', 'valign'],
117
- tbody: ['align', 'valign'],
118
- td: ['align', 'colspan', 'rowspan', 'valign', 'width'],
119
- tfoot: ['align', 'valign'],
120
- th: ['align', 'colspan', 'rowspan', 'valign', 'width'],
121
- thead: ['align', 'valign'],
122
- time: [],
123
- tr: ['align', 'rowspan', 'valign'],
124
- tt: [],
125
- u: [],
126
- ul: ['class'],
127
- var: [],
128
- video: [
129
- 'autoplay',
130
- 'controls',
131
- 'crossorigin',
132
- 'loop',
133
- 'muted',
134
- 'playsinline',
135
- 'poster',
136
- 'preload',
137
- 'src',
138
- 'height',
139
- 'width'
140
- ],
141
- wbr: []
142
- }
@@ -1,13 +0,0 @@
1
- {
2
- "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$anchor": "base",
4
- "description": "The base schema inherited by all other schemas",
5
- "type": "object",
6
- "properties": {
7
- "_id": {
8
- "description": "Unique identifier",
9
- "type": "string",
10
- "isObjectId": true
11
- }
12
- }
13
- }