adapt-authoring-jsonschema 0.0.1
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 -0
- package/.eslintrc +14 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +55 -0
- package/.github/ISSUE_TEMPLATE/config.yml +1 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +22 -0
- package/.github/dependabot.yml +11 -0
- package/.github/pull_request_template.md +25 -0
- package/.github/workflows/labelled_prs.yml +16 -0
- package/.github/workflows/new.yml +19 -0
- package/adapt-authoring.json +13 -0
- package/bin/check.js +43 -0
- package/conf/config.schema.json +26 -0
- package/docs/plugins/schemas-reference.js +74 -0
- package/docs/plugins/schemas-reference.md +7 -0
- package/docs/schema-examples.md +144 -0
- package/docs/schemas-introduction.md +78 -0
- package/docs/writing-a-schema.md +194 -0
- package/errors/errors.json +52 -0
- package/index.js +5 -0
- package/lib/JsonSchema.js +268 -0
- package/lib/JsonSchemaModule.js +209 -0
- package/lib/Keywords.js +74 -0
- package/lib/XSSDefaults.js +142 -0
- package/lib/typedefs.js +48 -0
- package/package.json +29 -0
- package/schema/base.schema.json +13 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Writing a schema
|
|
2
|
+
|
|
3
|
+
This page outlines the various elements of an Adapt data schema, and gives tips on how to start writing your own schemas. If you're new to schemas, head over to [this page](/introduction-to-schemas), which goes over the basics.
|
|
4
|
+
|
|
5
|
+
> For some specific schema examples, see [this page](/schema-examples).
|
|
6
|
+
|
|
7
|
+
## Quick links
|
|
8
|
+
- [Defining a schema](#defining-a-schema)
|
|
9
|
+
- [Defining schema inheritance](#defining-schema-inheritance)
|
|
10
|
+
- [Custom schema keywords](#custom-schema-keywords)
|
|
11
|
+
- [Custom Adapt properties](#custom-adapt-properties)
|
|
12
|
+
- [Custom Backbone Forms properties](#custom-backbone-forms-properties)
|
|
13
|
+
|
|
14
|
+
## Defining schema inheritance
|
|
15
|
+
|
|
16
|
+
You may find when defining your schemas that you want to extend or modify existing schemas, or split up your schemas in such a way as you can share properties across multiple schemas. For this purpose, you can use the `$merge` and `$patch` keywords.
|
|
17
|
+
|
|
18
|
+
The main difference between merge and patch schemas is how they're accessed:
|
|
19
|
+
- `$merge` schemas are considered 'complete' schemas, and are accessed directly (e.g. the UI will request the MCQ schema by name when rendering the MCQ form page)
|
|
20
|
+
- `$patch` schemas are not considered 'complete' schemas, but rather are 'attached' to another schema when that schema is requested (e.g. the UI will request the `course` schema which will include the relevant extension `$patch` schemas).
|
|
21
|
+
|
|
22
|
+
### `$merge` schemas
|
|
23
|
+
|
|
24
|
+
Merge schemas are useful when you have a schema which needs to directly inherit from another existing schema. As an example, all Adapt framework content schemas extend from a base `content` schema, which defines the basic attributes such as `title` and `body`.
|
|
25
|
+
|
|
26
|
+
Every `$merge` schema must define the base schema it inherits from (as a `source` attribute), and any additional properties (nested under a `with` attribute). For example:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
{
|
|
30
|
+
"$anchor": "example-schema",
|
|
31
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
32
|
+
"type": "object",
|
|
33
|
+
"$merge": {
|
|
34
|
+
"source": {
|
|
35
|
+
"$ref": "base-schema"
|
|
36
|
+
}
|
|
37
|
+
"with": {
|
|
38
|
+
"properties": {
|
|
39
|
+
"myAttribute": {
|
|
40
|
+
"type": "string"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### `$patch` schemas
|
|
49
|
+
|
|
50
|
+
`$patch` schemas are useful when looking to augment an existing schema with extra attributes or override existing schema properties (as an example, many Adapt framework extensions will define their own `$patch`schema for the `course` schema which will define extra `_globals` properties specific to that extension).
|
|
51
|
+
|
|
52
|
+
`$patch` schemas are almost identical to `$merge` schemas, with the only difference being that the `$merge` is replaced with the `$patch` keyword:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
{
|
|
56
|
+
"$anchor": "example-schema",
|
|
57
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
58
|
+
"type": "object",
|
|
59
|
+
"$patch": {
|
|
60
|
+
"source": {
|
|
61
|
+
"$ref": "base-schema"
|
|
62
|
+
}
|
|
63
|
+
"with": {
|
|
64
|
+
"properties": {
|
|
65
|
+
"myAttribute": {
|
|
66
|
+
"type": "string"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Custom schema keywords
|
|
75
|
+
|
|
76
|
+
In addition to the standard keywords defined in the JSON schema specification, the Adapt authoring tool jsonschema module defines a number of extra custom keywords which add extra convenient functionality when validating incoming data.
|
|
77
|
+
|
|
78
|
+
The following custom keywords are available:
|
|
79
|
+
|
|
80
|
+
### `isDate`
|
|
81
|
+
This keyword will parse any string value into a valid JavaScript Date.
|
|
82
|
+
|
|
83
|
+
#### Example
|
|
84
|
+
```
|
|
85
|
+
"myDateAttribute": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"isDate": true
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### `isDirectory`
|
|
92
|
+
This keyword will resolve any path values using a number of default directory values. This is very useful when making use of the existing app directories (e.g. you want to store data in the app's temp folder). The following are supported values:
|
|
93
|
+
- `$ROOT` will resolve to the main app root folder
|
|
94
|
+
- `$DATA` will resolve to the app's data folder
|
|
95
|
+
- `$TEMP` will resolve to the app's temp folder
|
|
96
|
+
|
|
97
|
+
#### Example
|
|
98
|
+
```
|
|
99
|
+
"myDirectoryAttribute": {
|
|
100
|
+
"type": "string",
|
|
101
|
+
"isDirectory": true,
|
|
102
|
+
"default": "$TEMP/myfolder" // will be replace $TEMP with the correct path to the temp folder
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### `isInternal`
|
|
107
|
+
This keyword will ensure that the attribute is **not** returned when a web API request is made. This is useful for restricting sensitive information like passwords. Note that this keyword only applies to the web APIs, and not when accessing data programatically.
|
|
108
|
+
|
|
109
|
+
#### Example
|
|
110
|
+
```
|
|
111
|
+
"myInternalAttribute": {
|
|
112
|
+
"type": "string",
|
|
113
|
+
"isInternal": true
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### `isReadOnly`
|
|
118
|
+
This keyword will ensures that the attribute is **not** modified when a web API request is made. Note that this keyword only applies to the web APIs, and not when accessing data programatically.
|
|
119
|
+
|
|
120
|
+
#### Example
|
|
121
|
+
```
|
|
122
|
+
"myReadOnlyAttribute": {
|
|
123
|
+
"type": "string",
|
|
124
|
+
"isReadOnly": true
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### `isTimeMs`
|
|
129
|
+
This keyword is very useful when defining time values, as it allows a human-readable input value to be automatically converted into milliseconds.
|
|
130
|
+
|
|
131
|
+
#### Example
|
|
132
|
+
```
|
|
133
|
+
"myTimeAttribute": {
|
|
134
|
+
"type": "string",
|
|
135
|
+
"isTimeMs": true,
|
|
136
|
+
"default": "7d" // will be converted to the equivalent of 7 days in milliseconds (604800000)
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Custom Adapt properties
|
|
141
|
+
|
|
142
|
+
The `_adapt` keyword is used to group schema properties which are non-standard to the JSON schema specification and related to various Adapt-specific features.
|
|
143
|
+
|
|
144
|
+
Property | Type | Description
|
|
145
|
+
--- | --- | ---
|
|
146
|
+
`editorOnly` | `Boolean` | Determines whether the attribute should be included in output JSON
|
|
147
|
+
`isSetting` | `Boolean` | Attribute will appear in the ‘Settings’ section of the form
|
|
148
|
+
`translatable` | `Boolean` | Whether the attribute is a language-specific string for translation
|
|
149
|
+
|
|
150
|
+
### Example
|
|
151
|
+
```
|
|
152
|
+
"myAttribute": {
|
|
153
|
+
"type": "string",
|
|
154
|
+
"_adapt": {
|
|
155
|
+
"editorOnly": true,
|
|
156
|
+
"translatable": true
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Custom Backbone forms properties
|
|
162
|
+
|
|
163
|
+
The Adapt authoring tool uses the [Backbone Forms](https://github.com/powmedia/backbone-forms) library to render forms from the data schemas into a user-friendly HTML form. The `_backboneForms` keyword is used to group schema properties which apply to Backbone Forms.
|
|
164
|
+
|
|
165
|
+
Property | Type | Description
|
|
166
|
+
--- | --- | ---
|
|
167
|
+
`type` | `String` | Override the type of Backbone Forms input to be used (by default, the type will be inferred from the schema property's type value). Accepted types: `Asset`, `Text`, `Number`, `Password`, `TextArea`, `Checkbox`, `Checkboxes`, `Select`, `Radio`, `Object`, `Date`, `DateTime`, `List`. See the [Backbone Forms docs](https://github.com/powmedia/backbone-forms#schema-definition) for more information.
|
|
168
|
+
`showInUi` | `Boolean` | Determines whether the attribute will be rendered in forms
|
|
169
|
+
`media` | `String` | When using a `type` of `Asset`, you can also restrict the AAT UI to only show assets of a specific type. The AAT stores the asset type based in its [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) (e.g. files with a MIME type of `image/png` and `image/jpeg` would both have a type of `image`).
|
|
170
|
+
`editorAttrs` | `Object` | Extra options passed to the Backbone Forms input. See the [Backbone Forms docs](https://github.com/powmedia/backbone-forms#schema-definition) for more information.
|
|
171
|
+
|
|
172
|
+
> The Adapt authoring tool will attempt to infer the type of a form input using the `type` value in the schema. In most cases this will suffice, so check that you definitely need to override the default behaviour before defining additional `_backboneForms` properties.
|
|
173
|
+
|
|
174
|
+
### Example
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
"myAttribute": {
|
|
178
|
+
"type": "string",
|
|
179
|
+
"_backboneForms": {
|
|
180
|
+
"type": "Asset",
|
|
181
|
+
"showInUi": false,
|
|
182
|
+
"media": "image"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
In many cases, you'll only want to customise the type of an input. If this is the case, the `_backboneForms` property can also be a string value, e.g.
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
"myAttribute": {
|
|
191
|
+
"type": "string",
|
|
192
|
+
"_backboneForms": "Number"
|
|
193
|
+
}
|
|
194
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"INVALID_SCHEMA": {
|
|
3
|
+
"data": {
|
|
4
|
+
"errors": "all validation errors",
|
|
5
|
+
"schemaName": "Schema name"
|
|
6
|
+
},
|
|
7
|
+
"description": "Invalid schema",
|
|
8
|
+
"statusCode": 400
|
|
9
|
+
},
|
|
10
|
+
"MISSING_SCHEMA": {
|
|
11
|
+
"data": {
|
|
12
|
+
"schemaName": "Schema name"
|
|
13
|
+
},
|
|
14
|
+
"description": "Schema is not registered in the cache",
|
|
15
|
+
"statusCode": 500
|
|
16
|
+
},
|
|
17
|
+
"MISSING_SCHEMA_NAME": {
|
|
18
|
+
"description": "Schema name is not defined",
|
|
19
|
+
"statusCode": 400
|
|
20
|
+
},
|
|
21
|
+
"MODIFY_PROTECTED_ATTR": {
|
|
22
|
+
"data": {
|
|
23
|
+
"attribute": "The protected attribute"
|
|
24
|
+
},
|
|
25
|
+
"description": "Attempted to modify a protected data attribute",
|
|
26
|
+
"statusCode": 400
|
|
27
|
+
},
|
|
28
|
+
"SCHEMA_EXISTS": {
|
|
29
|
+
"data": {
|
|
30
|
+
"filepath": "File path to the schema",
|
|
31
|
+
"schemaName": "Schema name"
|
|
32
|
+
},
|
|
33
|
+
"description": "A schema already exists with the specified name",
|
|
34
|
+
"statusCode": 400
|
|
35
|
+
},
|
|
36
|
+
"SCHEMA_LOAD_FAILED": {
|
|
37
|
+
"data": {
|
|
38
|
+
"schemaName": "Schema name"
|
|
39
|
+
},
|
|
40
|
+
"description": "Failed to load a JSON schema",
|
|
41
|
+
"statusCode": 500
|
|
42
|
+
},
|
|
43
|
+
"VALIDATION_FAILED": {
|
|
44
|
+
"data": {
|
|
45
|
+
"data": "the data failing validation",
|
|
46
|
+
"errors": "all validation errors",
|
|
47
|
+
"schemaName": "Schema name"
|
|
48
|
+
},
|
|
49
|
+
"description": "Data validation failed",
|
|
50
|
+
"statusCode": 400
|
|
51
|
+
}
|
|
52
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,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)).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
|
+
this.compiled = await this.validator.compileAsync(built)
|
|
153
|
+
this.isBuilding = false
|
|
154
|
+
this.lastBuildTime = Date.now()
|
|
155
|
+
|
|
156
|
+
this.buildHook.invoke(this)
|
|
157
|
+
return this
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Applies a patch schema to another schema
|
|
162
|
+
* @param {Object} baseSchema The base schema to apply the patch
|
|
163
|
+
* @param {Object} patchSchema The patch schema to apply to the base
|
|
164
|
+
* @param {ApplyPatchOptions} options
|
|
165
|
+
* @return {Object} The base schema
|
|
166
|
+
*/
|
|
167
|
+
patch (baseSchema, patchSchema, options = {}) {
|
|
168
|
+
const opts = _.defaults(options, {
|
|
169
|
+
extendAnnotations: patchSchema.$anchor !== BASE_SCHEMA_NAME,
|
|
170
|
+
overwriteProperties: true,
|
|
171
|
+
strict: true
|
|
172
|
+
})
|
|
173
|
+
const patchData = patchSchema.$patch?.with ?? patchSchema.$merge?.with ?? (!opts.strict && patchSchema)
|
|
174
|
+
if (!patchData) {
|
|
175
|
+
throw App.instance.errors.INVALID_SCHEMA.setData({ schemaName: patchSchema.$anchor })
|
|
176
|
+
}
|
|
177
|
+
if (opts.extendAnnotations) {
|
|
178
|
+
['$anchor', 'title', 'description'].forEach(p => {
|
|
179
|
+
if (patchSchema[p]) baseSchema[p] = patchSchema[p]
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
if (patchData.properties) {
|
|
183
|
+
const mergeFunc = opts.overwriteProperties ? _.merge : _.defaultsDeep
|
|
184
|
+
mergeFunc(baseSchema.properties, patchData.properties)
|
|
185
|
+
}
|
|
186
|
+
['allOf', 'anyOf', 'oneOf'].forEach(p => {
|
|
187
|
+
if (patchData[p]?.length) baseSchema[p] = (baseSchema[p] ?? []).concat(_.cloneDeep(patchData[p]))
|
|
188
|
+
})
|
|
189
|
+
if (patchData.required) {
|
|
190
|
+
baseSchema.required = _.uniq([...(baseSchema.required ?? []), ...patchData.required])
|
|
191
|
+
}
|
|
192
|
+
return baseSchema
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Checks passed data against the specified schema (if it exists)
|
|
197
|
+
* @param {Object} dataToValidate The data to be validated
|
|
198
|
+
* @param {SchemaValidateOptions} options
|
|
199
|
+
* @return {Object} The validated data
|
|
200
|
+
*/
|
|
201
|
+
validate (dataToValidate, options) {
|
|
202
|
+
const opts = _.defaults(options, { useDefaults: true, ignoreRequired: false })
|
|
203
|
+
const data = _.defaultsDeep(_.cloneDeep(dataToValidate), opts.useDefaults ? this.getObjectDefaults() : {})
|
|
204
|
+
|
|
205
|
+
this.compiled(data)
|
|
206
|
+
|
|
207
|
+
const errors = this.compiled.errors && this.compiled.errors
|
|
208
|
+
.filter(e => e.keyword === 'required' ? !opts.ignoreRequired : true)
|
|
209
|
+
.map(e => e.instancePath ? `${e.instancePath} ${e.message}` : e.message)
|
|
210
|
+
.reduce((s, e) => `${s}${e}, `, '')
|
|
211
|
+
|
|
212
|
+
if (errors?.length) { throw App.instance.errors.VALIDATION_FAILED.setData({ schemaName: this.name, errors, data }) }
|
|
213
|
+
|
|
214
|
+
return data
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Sanitises data by removing attributes according to the context (provided by options)
|
|
219
|
+
* @param {Object} dataToValidate The data to be sanitised
|
|
220
|
+
* @param {SchemaSanitiseOptions} options
|
|
221
|
+
* @return {Object} The sanitised data
|
|
222
|
+
*/
|
|
223
|
+
sanitise (dataToSanitise, options = {}, schema) {
|
|
224
|
+
const opts = _.defaults(options, { isInternal: false, isReadOnly: false, sanitiseHtml: true, strict: true })
|
|
225
|
+
schema = schema ?? this.built
|
|
226
|
+
const sanitised = {}
|
|
227
|
+
for (const prop in schema.properties) {
|
|
228
|
+
const schemaData = schema.properties[prop]
|
|
229
|
+
const value = dataToSanitise[prop]
|
|
230
|
+
const ignore = (opts.isInternal && schemaData.isInternal) || (opts.isReadOnly && schemaData.isReadOnly)
|
|
231
|
+
if (value === undefined || (ignore && !opts.strict)) {
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
if (ignore && opts.strict) {
|
|
235
|
+
throw App.instance.errors.MODIFY_PROTECTED_ATTR.setData({ attribute: prop, value })
|
|
236
|
+
}
|
|
237
|
+
sanitised[prop] =
|
|
238
|
+
schemaData.type === 'object' && schemaData.properties
|
|
239
|
+
? this.sanitise(value, opts, schemaData)
|
|
240
|
+
: schemaData.type === 'string' && opts.sanitiseHtml
|
|
241
|
+
? this.xss.process(value)
|
|
242
|
+
: value
|
|
243
|
+
}
|
|
244
|
+
return sanitised
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Adds an extension schema
|
|
249
|
+
* @param {String} extSchemaName
|
|
250
|
+
*/
|
|
251
|
+
addExtension (extSchemaName) {
|
|
252
|
+
!this.extensions.includes(extSchemaName) && this.extensions.push(extSchemaName)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Returns all schema defaults as a correctly structured object
|
|
257
|
+
* @param {Object} schema
|
|
258
|
+
* @param {Object} memo For recursion
|
|
259
|
+
* @returns {Object} The defaults object
|
|
260
|
+
*/
|
|
261
|
+
getObjectDefaults (schema) {
|
|
262
|
+
schema = schema ?? this.built
|
|
263
|
+
const props = schema.properties ?? schema.$merge?.with?.properties ?? schema.$patch?.with?.properties
|
|
264
|
+
return _.mapValues(props, s => s.type === 'object' && s.properties ? this.getObjectDefaults(s) : s.default)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export default JsonSchema
|