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.
@@ -1,209 +1,209 @@
1
- import _ from 'lodash'
2
- import { AbstractModule, Hook } from 'adapt-authoring-core'
3
- import Ajv from 'ajv/dist/2020.js'
4
- import { glob } from 'glob'
5
- import JsonSchema from './JsonSchema.js'
6
- import Keywords from './Keywords.js'
7
- import path from 'path'
8
- import safeRegex from 'safe-regex'
9
- import XSSDefaults from './XSSDefaults.js'
10
-
11
- const BASE_SCHEMA_PATH = './schema/base.schema.json'
12
- /**
13
- * Module which add support for the JSON Schema specification
14
- * @memberof jsonschema
15
- * @extends {AbstractModule}
16
- */
17
- class JsonSchemaModule extends AbstractModule {
18
- /** @override */
19
- async init () {
20
- 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
- /**
32
- * Invoked when schemas are registered
33
- * @type {Hook}
34
- */
35
- this.registerSchemasHook = new Hook
36
- /**
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}
44
- */
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
54
- })
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}/
59
- })
60
- await this.resetSchemaRegistry()
61
-
62
- this.onReady()
63
- .then(() => this.app.waitForModule('config', 'errors'))
64
- .then(() => {
65
- Object.assign(this.xssWhitelist,
66
- this.getConfig('xssWhitelistOverride') ? {} : XSSDefaults,
67
- this.getConfig('xssWhitelist'))
68
- })
69
- .then(() => this.addStringFormats(this.getConfig('formatOverrides')))
70
- .then(() => this.registerSchemas())
71
- .catch(e => this.log('error', e))
72
- }
73
-
74
- /**
75
- * Empties the schema registry (with the exception of the base schema)
76
- */
77
- async resetSchemaRegistry () {
78
- this.log('debug', 'RESET_SCHEMAS')
79
- this.schemas = {
80
- base: await this.createSchema(path.resolve(this.rootDir, BASE_SCHEMA_PATH), { enableCache: true })
81
- }
82
- }
83
-
84
- /**
85
- * Adds string formats to the Ajv validator
86
- */
87
- addStringFormats (formats) {
88
- Object.entries(formats).forEach(([name, re]) => {
89
- const isUnsafe = !safeRegex(re)
90
- if (isUnsafe) this.log('warn', `unsafe RegExp for format '${name}' (${re}), using default`)
91
- this.validator.addFormat(name, isUnsafe ? /.*/ : re)
92
- })
93
- }
94
-
95
- /**
96
- * Adds a new keyword to be used in JSON schemas
97
- * @param {AjvKeyword} definition
98
- */
99
- addKeyword (definition) {
100
- try {
101
- this.validator.addKeyword(definition)
102
- } catch (e) {
103
- this.log('warn', `failed to define keyword '${definition.keyword}', ${e}`)
104
- }
105
- }
106
-
107
- /**
108
- * 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`.
109
- * @return {Promise}
110
- */
111
- async registerSchemas () {
112
- await this.resetSchemaRegistry()
113
- await Promise.all(Object.values(this.app.dependencies).map(async d => {
114
- if(d.name === this.name) return
115
- const files = await glob('schema/*.schema.json', { cwd: d.rootDir, absolute: true })
116
- ;(await Promise.allSettled(files.map(f => this.registerSchema(f))))
117
- .filter(r => r.status === 'rejected')
118
- .forEach(r => this.log('warn', r.reason))
119
- }))
120
- this.registerSchemasHook.invoke();
121
- }
122
-
123
- /**
124
- * Registers a single JSON schema for use in the app
125
- * @param {String} filePath Path to the schema file
126
- * @param {RegisterSchemaOptions} options Extra options
127
- * @return {Promise}
128
- */
129
- async registerSchema (filePath, options = {}) {
130
- if (!_.isString(filePath)) {
131
- throw this.app.errors.INVALID_PARAMS.setData({ params: ['filePath'] })
132
- }
133
- const schema = await this.createSchema(filePath, options)
134
-
135
- if (this.schemas[schema.name]) {
136
- if (options.replace) this.deregisterSchema(schema.name)
137
- else throw this.app.errors.SCHEMA_EXISTS.setData({ schemaName: schema.name, filePath })
138
- }
139
- this.schemas[schema.name] = schema
140
- this.schemaExtensions?.[schema.name]?.forEach(s => schema.addExtension(s))
141
- if (schema.raw.$patch) this.extendSchema(schema.raw.$patch?.source?.$ref, schema.name)
142
-
143
- this.log('debug', 'REGISTER_SCHEMA', schema.name, filePath)
144
- }
145
-
146
- /**
147
- * deregisters a single JSON schema
148
- * @param {String} name Schem name to deregister
149
- * @return {Promise} Resolves with schema data
150
- */
151
- deregisterSchema (name) {
152
- if (this.schemas[name]) delete this.schemas[name]
153
- // remove schema from any extensions lists
154
- Object.entries(this.schemaExtensions).forEach(([base, extensions]) => {
155
- this.schemaExtensions[base] = extensions.filter(s => s !== name)
156
- })
157
- this.log('debug', 'DEREGISTER_SCHEMA', name)
158
- }
159
-
160
- /**
161
- * Creates a new JsonSchema instance
162
- * @param {String} filePath Path to the schema file
163
- * @param {Object} options Options passed to JsonSchema constructor
164
- * @returns {JsonSchema}
165
- */
166
- createSchema (filePath, options) {
167
- const schema = new JsonSchema({
168
- enableCache: this.getConfig('enableCache'),
169
- filePath,
170
- validator: this.validator,
171
- xssWhitelist: this.xssWhitelist,
172
- ...options
173
- })
174
- this.schemaExtensions?.[schema.name]?.forEach(s => schema.addExtension(s))
175
- delete this.schemaExtensions?.[schema.name]
176
- return schema.load()
177
- }
178
-
179
- /**
180
- * Extends an existing schema with extra properties
181
- * @param {String} baseSchemaName The name of the schema to extend
182
- * @param {String} extSchemaName The name of the schema to extend with
183
- */
184
- extendSchema (baseSchemaName, extSchemaName) {
185
- const baseSchema = this.schemas[baseSchemaName]
186
- if (baseSchema) {
187
- baseSchema.addExtension(extSchemaName)
188
- } else {
189
- if (!this.schemaExtensions[baseSchemaName]) this.schemaExtensions[baseSchemaName] = []
190
- this.schemaExtensions[baseSchemaName].push(extSchemaName)
191
- }
192
- this.log('debug', 'EXTEND_SCHEMA', baseSchemaName, extSchemaName)
193
- }
194
-
195
- /**
196
- * Retrieves the specified schema. Recursively applies any schema merge/patch schemas. Will returned cached data if enabled.
197
- * @param {String} schemaName The name of the schema to return
198
- * @param {LoadSchemaOptions} options
199
- * @param {Boolean} options.compiled If false, the raw schema will be returned
200
- * @return {Promise} The compiled schema validation function (default) or the raw schema
201
- */
202
- async getSchema (schemaName, options = {}) {
203
- const schema = this.schemas[schemaName]
204
- if (!schema) throw this.app.errors.MISSING_SCHEMA.setData({ schemaName })
205
- return schema.build(options)
206
- }
207
- }
208
-
209
- export default JsonSchemaModule
1
+ import _ from 'lodash'
2
+ import { AbstractModule, Hook } from 'adapt-authoring-core'
3
+ import Ajv from 'ajv/dist/2020.js'
4
+ import { glob } from 'glob'
5
+ import JsonSchema from './JsonSchema.js'
6
+ import Keywords from './Keywords.js'
7
+ import path from 'path'
8
+ import safeRegex from 'safe-regex'
9
+ import XSSDefaults from './XSSDefaults.js'
10
+
11
+ const BASE_SCHEMA_PATH = './schema/base.schema.json'
12
+ /**
13
+ * Module which add support for the JSON Schema specification
14
+ * @memberof jsonschema
15
+ * @extends {AbstractModule}
16
+ */
17
+ class JsonSchemaModule extends AbstractModule {
18
+ /** @override */
19
+ async init () {
20
+ 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
+ /**
32
+ * Invoked when schemas are registered
33
+ * @type {Hook}
34
+ */
35
+ this.registerSchemasHook = new Hook
36
+ /**
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}
44
+ */
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
54
+ })
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}/
59
+ })
60
+ await this.resetSchemaRegistry()
61
+
62
+ this.onReady()
63
+ .then(() => this.app.waitForModule('config', 'errors'))
64
+ .then(() => {
65
+ Object.assign(this.xssWhitelist,
66
+ this.getConfig('xssWhitelistOverride') ? {} : XSSDefaults,
67
+ this.getConfig('xssWhitelist'))
68
+ })
69
+ .then(() => this.addStringFormats(this.getConfig('formatOverrides')))
70
+ .then(() => this.registerSchemas())
71
+ .catch(e => this.log('error', e))
72
+ }
73
+
74
+ /**
75
+ * Empties the schema registry (with the exception of the base schema)
76
+ */
77
+ async resetSchemaRegistry () {
78
+ this.log('debug', 'RESET_SCHEMAS')
79
+ this.schemas = {
80
+ base: await this.createSchema(path.resolve(this.rootDir, BASE_SCHEMA_PATH), { enableCache: true })
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Adds string formats to the Ajv validator
86
+ */
87
+ addStringFormats (formats) {
88
+ Object.entries(formats).forEach(([name, re]) => {
89
+ const isUnsafe = !safeRegex(re)
90
+ if (isUnsafe) this.log('warn', `unsafe RegExp for format '${name}' (${re}), using default`)
91
+ this.validator.addFormat(name, isUnsafe ? /.*/ : re)
92
+ })
93
+ }
94
+
95
+ /**
96
+ * Adds a new keyword to be used in JSON schemas
97
+ * @param {AjvKeyword} definition
98
+ */
99
+ addKeyword (definition) {
100
+ try {
101
+ this.validator.addKeyword(definition)
102
+ } catch (e) {
103
+ this.log('warn', `failed to define keyword '${definition.keyword}', ${e}`)
104
+ }
105
+ }
106
+
107
+ /**
108
+ * 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`.
109
+ * @return {Promise}
110
+ */
111
+ async registerSchemas () {
112
+ await this.resetSchemaRegistry()
113
+ await Promise.all(Object.values(this.app.dependencies).map(async d => {
114
+ if(d.name === this.name) return
115
+ const files = await glob('schema/*.schema.json', { cwd: d.rootDir, absolute: true })
116
+ ;(await Promise.allSettled(files.map(f => this.registerSchema(f))))
117
+ .filter(r => r.status === 'rejected')
118
+ .forEach(r => this.log('warn', r.reason))
119
+ }))
120
+ this.registerSchemasHook.invoke();
121
+ }
122
+
123
+ /**
124
+ * Registers a single JSON schema for use in the app
125
+ * @param {String} filePath Path to the schema file
126
+ * @param {RegisterSchemaOptions} options Extra options
127
+ * @return {Promise}
128
+ */
129
+ async registerSchema (filePath, options = {}) {
130
+ if (!_.isString(filePath)) {
131
+ throw this.app.errors.INVALID_PARAMS.setData({ params: ['filePath'] })
132
+ }
133
+ const schema = await this.createSchema(filePath, options)
134
+
135
+ if (this.schemas[schema.name]) {
136
+ if (options.replace) this.deregisterSchema(schema.name)
137
+ else throw this.app.errors.SCHEMA_EXISTS.setData({ schemaName: schema.name, filePath })
138
+ }
139
+ this.schemas[schema.name] = schema
140
+ this.schemaExtensions?.[schema.name]?.forEach(s => schema.addExtension(s))
141
+ if (schema.raw.$patch) this.extendSchema(schema.raw.$patch?.source?.$ref, schema.name)
142
+
143
+ this.log('debug', 'REGISTER_SCHEMA', schema.name, filePath)
144
+ }
145
+
146
+ /**
147
+ * deregisters a single JSON schema
148
+ * @param {String} name Schem name to deregister
149
+ * @return {Promise} Resolves with schema data
150
+ */
151
+ deregisterSchema (name) {
152
+ if (this.schemas[name]) delete this.schemas[name]
153
+ // remove schema from any extensions lists
154
+ Object.entries(this.schemaExtensions).forEach(([base, extensions]) => {
155
+ this.schemaExtensions[base] = extensions.filter(s => s !== name)
156
+ })
157
+ this.log('debug', 'DEREGISTER_SCHEMA', name)
158
+ }
159
+
160
+ /**
161
+ * Creates a new JsonSchema instance
162
+ * @param {String} filePath Path to the schema file
163
+ * @param {Object} options Options passed to JsonSchema constructor
164
+ * @returns {JsonSchema}
165
+ */
166
+ createSchema (filePath, options) {
167
+ const schema = new JsonSchema({
168
+ enableCache: this.getConfig('enableCache'),
169
+ filePath,
170
+ validator: this.validator,
171
+ xssWhitelist: this.xssWhitelist,
172
+ ...options
173
+ })
174
+ this.schemaExtensions?.[schema.name]?.forEach(s => schema.addExtension(s))
175
+ delete this.schemaExtensions?.[schema.name]
176
+ return schema.load()
177
+ }
178
+
179
+ /**
180
+ * Extends an existing schema with extra properties
181
+ * @param {String} baseSchemaName The name of the schema to extend
182
+ * @param {String} extSchemaName The name of the schema to extend with
183
+ */
184
+ extendSchema (baseSchemaName, extSchemaName) {
185
+ const baseSchema = this.schemas[baseSchemaName]
186
+ if (baseSchema) {
187
+ baseSchema.addExtension(extSchemaName)
188
+ } else {
189
+ if (!this.schemaExtensions[baseSchemaName]) this.schemaExtensions[baseSchemaName] = []
190
+ this.schemaExtensions[baseSchemaName].push(extSchemaName)
191
+ }
192
+ this.log('debug', 'EXTEND_SCHEMA', baseSchemaName, extSchemaName)
193
+ }
194
+
195
+ /**
196
+ * Retrieves the specified schema. Recursively applies any schema merge/patch schemas. Will returned cached data if enabled.
197
+ * @param {String} schemaName The name of the schema to return
198
+ * @param {LoadSchemaOptions} options
199
+ * @param {Boolean} options.compiled If false, the raw schema will be returned
200
+ * @return {Promise} The compiled schema validation function (default) or the raw schema
201
+ */
202
+ async getSchema (schemaName, options = {}) {
203
+ const schema = this.schemas[schemaName]
204
+ if (!schema) throw this.app.errors.MISSING_SCHEMA.setData({ schemaName })
205
+ return schema.build(options)
206
+ }
207
+ }
208
+
209
+ export default JsonSchemaModule
package/lib/Keywords.js CHANGED
@@ -1,74 +1,74 @@
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
+ 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