adapt-schemas 1.0.0 → 1.0.2
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/.github/workflows/releases.yml +32 -0
- package/.github/workflows/standardjs.yml +13 -0
- package/.github/workflows/tests.yml +13 -0
- package/README.md +17 -7
- package/index.js +4 -3
- package/lib/Keywords.js +6 -25
- package/lib/Schema.js +12 -23
- package/lib/SchemaError.js +13 -0
- package/lib/Schemas.js +30 -35
- package/package.json +27 -1
- package/test.js +96 -99
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches:
|
|
5
|
+
- master
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
release:
|
|
9
|
+
name: Release
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: write # to be able to publish a GitHub release
|
|
13
|
+
issues: write # to be able to comment on released issues
|
|
14
|
+
pull-requests: write # to be able to comment on released pull requests
|
|
15
|
+
id-token: write # to enable use of OIDC for trusted publishing and npm provenance
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout
|
|
18
|
+
uses: actions/checkout@v3
|
|
19
|
+
with:
|
|
20
|
+
fetch-depth: 0
|
|
21
|
+
- name: Setup Node.js
|
|
22
|
+
uses: actions/setup-node@v3
|
|
23
|
+
with:
|
|
24
|
+
node-version: 'lts/*'
|
|
25
|
+
- name: Update npm
|
|
26
|
+
run: npm install -g npm@latest
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: npm ci
|
|
29
|
+
- name: Release
|
|
30
|
+
env:
|
|
31
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
32
|
+
run: npx semantic-release
|
package/README.md
CHANGED
|
@@ -43,11 +43,7 @@ const library = new Schemas({
|
|
|
43
43
|
enableCache: true, // Enable schema build caching (default: true)
|
|
44
44
|
xssWhitelist: {}, // Custom XSS whitelist tags/attributes
|
|
45
45
|
xssWhitelistOverride: false, // Replace defaults instead of extending
|
|
46
|
-
formatOverrides: {}
|
|
47
|
-
directoryReplacements: { // Replacements for isDirectory keyword
|
|
48
|
-
'$ROOT': '/app',
|
|
49
|
-
'$DATA': '/app/data'
|
|
50
|
-
}
|
|
46
|
+
formatOverrides: {} // Custom string format RegExp patterns
|
|
51
47
|
})
|
|
52
48
|
```
|
|
53
49
|
|
|
@@ -153,7 +149,7 @@ Manually extends a schema with another.
|
|
|
153
149
|
library.extendSchema('course', 'my-course-extension')
|
|
154
150
|
```
|
|
155
151
|
|
|
156
|
-
##### `addKeyword(definition)`
|
|
152
|
+
##### `addKeyword(definition, options)`
|
|
157
153
|
Adds a custom AJV keyword.
|
|
158
154
|
|
|
159
155
|
```javascript
|
|
@@ -162,6 +158,20 @@ library.addKeyword({
|
|
|
162
158
|
type: 'number',
|
|
163
159
|
validate: (schema, data) => data > 0
|
|
164
160
|
})
|
|
161
|
+
|
|
162
|
+
// Override an existing keyword
|
|
163
|
+
library.addKeyword({
|
|
164
|
+
keyword: 'isPositive',
|
|
165
|
+
type: 'number',
|
|
166
|
+
validate: (schema, data) => data >= 0
|
|
167
|
+
}, { override: true })
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
##### `deregisterSchema(name)`
|
|
171
|
+
Removes a schema from the registry.
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
library.deregisterSchema('my-schema')
|
|
165
175
|
```
|
|
166
176
|
|
|
167
177
|
##### `addStringFormats(formats)`
|
|
@@ -257,7 +267,6 @@ The library includes these custom AJV keywords:
|
|
|
257
267
|
| `isBytes` | Parses byte strings | `"1MB"` → `1048576` |
|
|
258
268
|
| `isDate` | Parses date strings | `"2024-01-01"` → `Date` |
|
|
259
269
|
| `isTimeMs` | Parses duration strings | `"7d"` → `604800000` |
|
|
260
|
-
| `isDirectory` | Resolves path tokens | `"$ROOT/data"` → `"/app/data"` |
|
|
261
270
|
| `isObjectId` | Marks ObjectId fields | No transformation |
|
|
262
271
|
|
|
263
272
|
## Error Handling
|
|
@@ -272,6 +281,7 @@ The library throws `SchemaError` with the following codes:
|
|
|
272
281
|
| `INVALID_SCHEMA` | Schema fails JSON Schema validation |
|
|
273
282
|
| `MISSING_SCHEMA` | Requested schema not found |
|
|
274
283
|
| `VALIDATION_FAILED` | Data fails schema validation |
|
|
284
|
+
| `KEYWORD_EXISTS` | Keyword already defined |
|
|
275
285
|
| `MODIFY_PROTECTED_ATTR` | Attempt to modify internal/read-only field |
|
|
276
286
|
|
|
277
287
|
```javascript
|
package/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import Schemas, { SchemaError } from './lib/Schemas.js'
|
|
2
|
-
import Schema from './lib/Schema.js'
|
|
3
1
|
import Keywords from './lib/Keywords.js'
|
|
2
|
+
import Schema from './lib/Schema.js'
|
|
3
|
+
import SchemaError from './lib/SchemaError.js'
|
|
4
|
+
import Schemas from './lib/Schemas.js'
|
|
4
5
|
import XSSDefaults from './lib/XSSDefaults.js'
|
|
5
6
|
|
|
6
|
-
export {
|
|
7
|
+
export { Keywords, Schema, SchemaError, Schemas, XSSDefaults }
|
|
7
8
|
export default Schemas
|
package/lib/Keywords.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import bytes from 'bytes'
|
|
2
2
|
import ms from 'ms'
|
|
3
|
-
import path from 'path'
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Custom JSON schema keywords for AJV
|
|
@@ -8,16 +7,16 @@ import path from 'path'
|
|
|
8
7
|
class Keywords {
|
|
9
8
|
/**
|
|
10
9
|
* Returns all custom keywords
|
|
11
|
-
* @param {Object} directoryReplacements Replacements for isDirectory (e.g. { '$ROOT': '/app' })
|
|
12
10
|
* @returns {Object[]} Array of AJV keyword definitions
|
|
13
11
|
*/
|
|
14
|
-
static all(
|
|
12
|
+
static all () {
|
|
15
13
|
const keywords = {
|
|
16
14
|
/**
|
|
17
15
|
* Parses byte string values (e.g., "1MB" -> 1048576)
|
|
18
16
|
*/
|
|
19
17
|
isBytes: function () {
|
|
20
18
|
return (value, { parentData, parentDataProperty }) => {
|
|
19
|
+
if (value === false) return false
|
|
21
20
|
try {
|
|
22
21
|
parentData[parentDataProperty] = bytes.parse(value)
|
|
23
22
|
return true
|
|
@@ -32,6 +31,7 @@ class Keywords {
|
|
|
32
31
|
*/
|
|
33
32
|
isDate: function () {
|
|
34
33
|
return (value, { parentData, parentDataProperty }) => {
|
|
34
|
+
if (value === false) return false
|
|
35
35
|
try {
|
|
36
36
|
parentData[parentDataProperty] = new Date(value)
|
|
37
37
|
return true
|
|
@@ -41,31 +41,12 @@ class Keywords {
|
|
|
41
41
|
}
|
|
42
42
|
},
|
|
43
43
|
|
|
44
|
-
/**
|
|
45
|
-
* Resolves directory path tokens ($ROOT, $DATA, $TEMP, etc.)
|
|
46
|
-
*/
|
|
47
|
-
isDirectory: function () {
|
|
48
|
-
const doReplace = value => {
|
|
49
|
-
const replacements = Object.entries(directoryReplacements)
|
|
50
|
-
return replacements.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
|
-
// Keep original value on error
|
|
59
|
-
}
|
|
60
|
-
return true
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
|
|
64
44
|
/**
|
|
65
45
|
* Parses time duration strings into milliseconds (e.g., "7d" -> 604800000)
|
|
66
46
|
*/
|
|
67
47
|
isTimeMs: function () {
|
|
68
48
|
return (value, { parentData, parentDataProperty }) => {
|
|
49
|
+
if (value === false) return false
|
|
69
50
|
try {
|
|
70
51
|
parentData[parentDataProperty] = ms(value)
|
|
71
52
|
return true
|
|
@@ -78,8 +59,8 @@ class Keywords {
|
|
|
78
59
|
/**
|
|
79
60
|
* Marker for ObjectId fields (no transformation, just marks the field)
|
|
80
61
|
*/
|
|
81
|
-
isObjectId: function () {
|
|
82
|
-
return () =>
|
|
62
|
+
isObjectId: function (value) {
|
|
63
|
+
return () => value
|
|
83
64
|
}
|
|
84
65
|
}
|
|
85
66
|
|
package/lib/Schema.js
CHANGED
|
@@ -1,22 +1,11 @@
|
|
|
1
1
|
import _ from 'lodash'
|
|
2
2
|
import { EventEmitter } from 'events'
|
|
3
3
|
import fs from 'fs/promises'
|
|
4
|
+
import SchemaError from './SchemaError.js'
|
|
4
5
|
import xss from 'xss'
|
|
5
6
|
|
|
6
7
|
const BASE_SCHEMA_NAME = 'base'
|
|
7
8
|
|
|
8
|
-
/**
|
|
9
|
-
* Schema-specific error class
|
|
10
|
-
*/
|
|
11
|
-
export class SchemaError extends Error {
|
|
12
|
-
constructor(code, message, data = {}) {
|
|
13
|
-
super(message)
|
|
14
|
-
this.code = code
|
|
15
|
-
this.data = data
|
|
16
|
-
this.name = 'SchemaError'
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
9
|
/**
|
|
21
10
|
* Represents an individual JSON schema with validation and build capabilities
|
|
22
11
|
*/
|
|
@@ -29,7 +18,7 @@ class Schema extends EventEmitter {
|
|
|
29
18
|
* @param {Object} options.xssWhitelist XSS whitelist configuration
|
|
30
19
|
* @param {SchemaLibrary} options.schemaLibrary Reference to the parent library
|
|
31
20
|
*/
|
|
32
|
-
constructor({ enableCache, filePath, validator, xssWhitelist, schemaLibrary }) {
|
|
21
|
+
constructor ({ enableCache, filePath, validator, xssWhitelist, schemaLibrary }) {
|
|
33
22
|
super()
|
|
34
23
|
|
|
35
24
|
/**
|
|
@@ -115,7 +104,7 @@ class Schema extends EventEmitter {
|
|
|
115
104
|
* Determines whether the current schema build is valid using last modification timestamp
|
|
116
105
|
* @returns {Promise<boolean>}
|
|
117
106
|
*/
|
|
118
|
-
async isBuildValid() {
|
|
107
|
+
async isBuildValid () {
|
|
119
108
|
if (!this.built) return false
|
|
120
109
|
|
|
121
110
|
let schema = this
|
|
@@ -131,7 +120,7 @@ class Schema extends EventEmitter {
|
|
|
131
120
|
* Returns the parent schema if $merge is defined (or the base schema if a root schema)
|
|
132
121
|
* @returns {Promise<Schema|undefined>}
|
|
133
122
|
*/
|
|
134
|
-
async getParent() {
|
|
123
|
+
async getParent () {
|
|
135
124
|
if (this.name === BASE_SCHEMA_NAME) return undefined
|
|
136
125
|
|
|
137
126
|
const parentRef = this.raw?.$merge?.source?.$ref ?? BASE_SCHEMA_NAME
|
|
@@ -142,7 +131,7 @@ class Schema extends EventEmitter {
|
|
|
142
131
|
* Loads the schema file
|
|
143
132
|
* @returns {Promise<Schema>} This instance
|
|
144
133
|
*/
|
|
145
|
-
async load() {
|
|
134
|
+
async load () {
|
|
146
135
|
try {
|
|
147
136
|
const content = await fs.readFile(this.filePath, 'utf-8')
|
|
148
137
|
this.raw = JSON.parse(content)
|
|
@@ -178,7 +167,7 @@ class Schema extends EventEmitter {
|
|
|
178
167
|
* @param {function} options.extensionFilter Filter function for extensions
|
|
179
168
|
* @returns {Promise<Schema>}
|
|
180
169
|
*/
|
|
181
|
-
async build(options = {}) {
|
|
170
|
+
async build (options = {}) {
|
|
182
171
|
if (options.useCache !== false && this.enableCache && await this.isBuildValid()) {
|
|
183
172
|
return this
|
|
184
173
|
}
|
|
@@ -196,7 +185,7 @@ class Schema extends EventEmitter {
|
|
|
196
185
|
|
|
197
186
|
while (parent) {
|
|
198
187
|
const parentBuilt = _.cloneDeep((await parent.build({ ...options, compile: false })).built)
|
|
199
|
-
built = this.patch(parentBuilt, built, { strict:
|
|
188
|
+
built = this.patch(parentBuilt, built, { strict: false })
|
|
200
189
|
parent = await parent.getParent()
|
|
201
190
|
}
|
|
202
191
|
|
|
@@ -237,7 +226,7 @@ class Schema extends EventEmitter {
|
|
|
237
226
|
* @param {boolean} options.strict Require $patch or $merge
|
|
238
227
|
* @returns {Object} The base schema
|
|
239
228
|
*/
|
|
240
|
-
patch(baseSchema, patchSchema, options = {}) {
|
|
229
|
+
patch (baseSchema, patchSchema, options = {}) {
|
|
241
230
|
const opts = _.defaults(options, {
|
|
242
231
|
extendAnnotations: patchSchema.$anchor !== BASE_SCHEMA_NAME,
|
|
243
232
|
overwriteProperties: true,
|
|
@@ -284,7 +273,7 @@ class Schema extends EventEmitter {
|
|
|
284
273
|
* @param {boolean} options.ignoreRequired Ignore required field errors
|
|
285
274
|
* @returns {Object} The validated data
|
|
286
275
|
*/
|
|
287
|
-
validate(dataToValidate, options = {}) {
|
|
276
|
+
validate (dataToValidate, options = {}) {
|
|
288
277
|
const opts = _.defaults(options, { useDefaults: true, ignoreRequired: false })
|
|
289
278
|
const data = _.defaults(_.cloneDeep(dataToValidate), opts.useDefaults ? this.getObjectDefaults() : {})
|
|
290
279
|
|
|
@@ -322,7 +311,7 @@ class Schema extends EventEmitter {
|
|
|
322
311
|
* @param {Object} schema Schema to use (defaults to built schema)
|
|
323
312
|
* @returns {Object} The sanitised data
|
|
324
313
|
*/
|
|
325
|
-
sanitise(dataToSanitise, options = {}, schema) {
|
|
314
|
+
sanitise (dataToSanitise, options = {}, schema) {
|
|
326
315
|
const opts = _.defaults(options, {
|
|
327
316
|
isInternal: false,
|
|
328
317
|
isReadOnly: false,
|
|
@@ -363,7 +352,7 @@ class Schema extends EventEmitter {
|
|
|
363
352
|
* Adds an extension schema
|
|
364
353
|
* @param {string} extSchemaName Extension schema name
|
|
365
354
|
*/
|
|
366
|
-
addExtension(extSchemaName) {
|
|
355
|
+
addExtension (extSchemaName) {
|
|
367
356
|
if (!this.extensions.includes(extSchemaName)) {
|
|
368
357
|
this.extensions.push(extSchemaName)
|
|
369
358
|
}
|
|
@@ -374,7 +363,7 @@ class Schema extends EventEmitter {
|
|
|
374
363
|
* @param {Object} schema Schema to extract defaults from
|
|
375
364
|
* @returns {Object} The defaults object
|
|
376
365
|
*/
|
|
377
|
-
getObjectDefaults(schema) {
|
|
366
|
+
getObjectDefaults (schema) {
|
|
378
367
|
schema = schema ?? this.built
|
|
379
368
|
const props = schema.properties ?? schema.$merge?.with?.properties ?? schema.$patch?.with?.properties
|
|
380
369
|
|
package/lib/Schemas.js
CHANGED
|
@@ -1,29 +1,18 @@
|
|
|
1
1
|
import _ from 'lodash'
|
|
2
2
|
import Ajv from 'ajv/dist/2020.js'
|
|
3
3
|
import { EventEmitter } from 'events'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
4
5
|
import { glob } from 'glob'
|
|
6
|
+
import Keywords from './Keywords.js'
|
|
5
7
|
import path from 'path'
|
|
6
|
-
import { fileURLToPath } from 'url'
|
|
7
8
|
import safeRegex from 'safe-regex'
|
|
8
9
|
import Schema from './Schema.js'
|
|
9
|
-
import
|
|
10
|
+
import SchemaError from './SchemaError.js'
|
|
10
11
|
import XSSDefaults from './XSSDefaults.js'
|
|
11
12
|
|
|
12
13
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
13
14
|
const BASE_SCHEMA_PATH = '../schema/base.schema.json'
|
|
14
15
|
|
|
15
|
-
/**
|
|
16
|
-
* Schema library errors
|
|
17
|
-
*/
|
|
18
|
-
export class SchemaError extends Error {
|
|
19
|
-
constructor(code, message, data = {}) {
|
|
20
|
-
super(message)
|
|
21
|
-
this.code = code
|
|
22
|
-
this.data = data
|
|
23
|
-
this.name = 'SchemaError'
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
16
|
/**
|
|
28
17
|
* Standalone JSON Schema library for the Adapt framework
|
|
29
18
|
*/
|
|
@@ -35,17 +24,15 @@ class Schemas extends EventEmitter {
|
|
|
35
24
|
* @param {Object} options.xssWhitelist Custom XSS whitelist tags/attributes
|
|
36
25
|
* @param {Boolean} options.xssWhitelistOverride Replace default whitelist instead of extending
|
|
37
26
|
* @param {Object} options.formatOverrides Custom string format RegExp patterns
|
|
38
|
-
* @param {Object} options.directoryReplacements Replacements for isDirectory keyword (e.g. { '$ROOT': '/app' })
|
|
39
27
|
*/
|
|
40
|
-
constructor(options = {}) {
|
|
28
|
+
constructor (options = {}) {
|
|
41
29
|
super()
|
|
42
30
|
|
|
43
31
|
this.options = _.defaults(options, {
|
|
44
32
|
enableCache: true,
|
|
45
33
|
xssWhitelist: {},
|
|
46
34
|
xssWhitelistOverride: false,
|
|
47
|
-
formatOverrides: {}
|
|
48
|
-
directoryReplacements: {}
|
|
35
|
+
formatOverrides: {}
|
|
49
36
|
})
|
|
50
37
|
|
|
51
38
|
/**
|
|
@@ -82,7 +69,7 @@ class Schemas extends EventEmitter {
|
|
|
82
69
|
removeAdditional: 'all',
|
|
83
70
|
strict: false,
|
|
84
71
|
verbose: true,
|
|
85
|
-
keywords: Keywords.all(
|
|
72
|
+
keywords: Keywords.all()
|
|
86
73
|
})
|
|
87
74
|
|
|
88
75
|
this.addStringFormats({
|
|
@@ -102,7 +89,7 @@ class Schemas extends EventEmitter {
|
|
|
102
89
|
* Initializes the schema library by loading the base schema
|
|
103
90
|
* @returns {Promise<SchemaLibrary>}
|
|
104
91
|
*/
|
|
105
|
-
async init() {
|
|
92
|
+
async init () {
|
|
106
93
|
if (this._initialized) return this
|
|
107
94
|
|
|
108
95
|
await this.resetSchemaRegistry()
|
|
@@ -115,7 +102,7 @@ class Schemas extends EventEmitter {
|
|
|
115
102
|
* Empties the schema registry (with the exception of the base schema)
|
|
116
103
|
* @returns {Promise<void>}
|
|
117
104
|
*/
|
|
118
|
-
async resetSchemaRegistry() {
|
|
105
|
+
async resetSchemaRegistry () {
|
|
119
106
|
this.schemas = {
|
|
120
107
|
base: await this.createSchema(path.resolve(__dirname, BASE_SCHEMA_PATH), { enableCache: true })
|
|
121
108
|
}
|
|
@@ -126,7 +113,7 @@ class Schemas extends EventEmitter {
|
|
|
126
113
|
* Adds string formats to the Ajv validator
|
|
127
114
|
* @param {Object<string, RegExp>} formats
|
|
128
115
|
*/
|
|
129
|
-
addStringFormats(formats) {
|
|
116
|
+
addStringFormats (formats) {
|
|
130
117
|
Object.entries(formats).forEach(([name, re]) => {
|
|
131
118
|
const isUnsafe = !safeRegex(re)
|
|
132
119
|
if (isUnsafe) {
|
|
@@ -139,9 +126,17 @@ class Schemas extends EventEmitter {
|
|
|
139
126
|
/**
|
|
140
127
|
* Adds a new keyword to be used in JSON schemas
|
|
141
128
|
* @param {Object} definition AJV keyword definition
|
|
129
|
+
* @param {Object} options Configuration options
|
|
130
|
+
* @param {Boolean} options.override Whether an existing keyword should be overridden
|
|
142
131
|
*/
|
|
143
|
-
addKeyword(definition) {
|
|
132
|
+
addKeyword (definition, options = {}) {
|
|
144
133
|
try {
|
|
134
|
+
if (this.validator.getKeyword(definition.keyword)) {
|
|
135
|
+
if (options.override !== true) {
|
|
136
|
+
throw new SchemaError('KEYWORD_EXISTS', 'Keyword already exists')
|
|
137
|
+
}
|
|
138
|
+
this.validator.removeKeyword(definition.keyword)
|
|
139
|
+
}
|
|
145
140
|
this.validator.addKeyword(definition)
|
|
146
141
|
} catch (e) {
|
|
147
142
|
this.emit('warning', `Failed to define keyword '${definition.keyword}', ${e}`)
|
|
@@ -156,7 +151,7 @@ class Schemas extends EventEmitter {
|
|
|
156
151
|
* @param {string} options.cwd Base directory for glob patterns
|
|
157
152
|
* @returns {Promise<void>}
|
|
158
153
|
*/
|
|
159
|
-
async loadSchemas(patterns, options = {}) {
|
|
154
|
+
async loadSchemas (patterns, options = {}) {
|
|
160
155
|
if (!this._initialized) {
|
|
161
156
|
await this.init()
|
|
162
157
|
}
|
|
@@ -196,7 +191,7 @@ class Schemas extends EventEmitter {
|
|
|
196
191
|
* @param {boolean} options.replace Replace existing schema with same name
|
|
197
192
|
* @returns {Promise<Schema>}
|
|
198
193
|
*/
|
|
199
|
-
async registerSchema(filePath, options = {}) {
|
|
194
|
+
async registerSchema (filePath, options = {}) {
|
|
200
195
|
if (!_.isString(filePath)) {
|
|
201
196
|
throw new SchemaError('INVALID_PARAMS', 'filePath must be a string', { params: ['filePath'] })
|
|
202
197
|
}
|
|
@@ -229,7 +224,7 @@ class Schemas extends EventEmitter {
|
|
|
229
224
|
* Deregisters a single JSON schema
|
|
230
225
|
* @param {string} name Schema name to deregister
|
|
231
226
|
*/
|
|
232
|
-
deregisterSchema(name) {
|
|
227
|
+
deregisterSchema (name) {
|
|
233
228
|
if (this.schemas[name]) {
|
|
234
229
|
delete this.schemas[name]
|
|
235
230
|
}
|
|
@@ -246,7 +241,7 @@ class Schemas extends EventEmitter {
|
|
|
246
241
|
* @param {Object} options Options passed to Schema constructor
|
|
247
242
|
* @returns {Promise<Schema>}
|
|
248
243
|
*/
|
|
249
|
-
async createSchema(filePath, options = {}) {
|
|
244
|
+
async createSchema (filePath, options = {}) {
|
|
250
245
|
const schema = new Schema({
|
|
251
246
|
enableCache: options.enableCache ?? this.options.enableCache,
|
|
252
247
|
filePath,
|
|
@@ -267,7 +262,7 @@ class Schemas extends EventEmitter {
|
|
|
267
262
|
* @param {string} baseSchemaName The name of the schema to extend
|
|
268
263
|
* @param {string} extSchemaName The name of the schema to extend with
|
|
269
264
|
*/
|
|
270
|
-
extendSchema(baseSchemaName, extSchemaName) {
|
|
265
|
+
extendSchema (baseSchemaName, extSchemaName) {
|
|
271
266
|
const baseSchema = this.schemas[baseSchemaName]
|
|
272
267
|
if (baseSchema) {
|
|
273
268
|
baseSchema.addExtension(extSchemaName)
|
|
@@ -290,7 +285,7 @@ class Schemas extends EventEmitter {
|
|
|
290
285
|
* @param {function} options.extensionFilter Filter function for extensions
|
|
291
286
|
* @returns {Promise<Schema>}
|
|
292
287
|
*/
|
|
293
|
-
async getSchema(schemaName, options = {}) {
|
|
288
|
+
async getSchema (schemaName, options = {}) {
|
|
294
289
|
const schema = this.schemas[schemaName]
|
|
295
290
|
if (!schema) {
|
|
296
291
|
throw new SchemaError('MISSING_SCHEMA', `Schema '${schemaName}' not found`, { schemaName })
|
|
@@ -305,7 +300,7 @@ class Schemas extends EventEmitter {
|
|
|
305
300
|
* @param {Object} options Validation options
|
|
306
301
|
* @returns {Promise<Object>} The validated data with defaults applied
|
|
307
302
|
*/
|
|
308
|
-
async validate(schemaName, data, options = {}) {
|
|
303
|
+
async validate (schemaName, data, options = {}) {
|
|
309
304
|
const schema = await this.getSchema(schemaName)
|
|
310
305
|
return schema.validate(data, options)
|
|
311
306
|
}
|
|
@@ -316,7 +311,7 @@ class Schemas extends EventEmitter {
|
|
|
316
311
|
* @param {Object} options Build options
|
|
317
312
|
* @returns {Promise<Object>} The built schema object
|
|
318
313
|
*/
|
|
319
|
-
async getBuiltSchema(schemaName, options = {}) {
|
|
314
|
+
async getBuiltSchema (schemaName, options = {}) {
|
|
320
315
|
const schema = await this.getSchema(schemaName)
|
|
321
316
|
return schema.built
|
|
322
317
|
}
|
|
@@ -326,7 +321,7 @@ class Schemas extends EventEmitter {
|
|
|
326
321
|
* @param {string} schemaName The schema name
|
|
327
322
|
* @returns {Promise<Object>} The defaults object
|
|
328
323
|
*/
|
|
329
|
-
async getSchemaDefaults(schemaName) {
|
|
324
|
+
async getSchemaDefaults (schemaName) {
|
|
330
325
|
const schema = await this.getSchema(schemaName)
|
|
331
326
|
return schema.getObjectDefaults()
|
|
332
327
|
}
|
|
@@ -336,7 +331,7 @@ class Schemas extends EventEmitter {
|
|
|
336
331
|
* @param {string} schemaName Schema name (defaults to 'course')
|
|
337
332
|
* @returns {Promise<Object>} The _globals defaults
|
|
338
333
|
*/
|
|
339
|
-
async getGlobalsDefaults(schemaName = 'course') {
|
|
334
|
+
async getGlobalsDefaults (schemaName = 'course') {
|
|
340
335
|
const schema = await this.getSchema(schemaName)
|
|
341
336
|
const defaults = schema.getObjectDefaults()
|
|
342
337
|
return defaults._globals || {}
|
|
@@ -346,7 +341,7 @@ class Schemas extends EventEmitter {
|
|
|
346
341
|
* Returns list of all registered schema names
|
|
347
342
|
* @returns {string[]}
|
|
348
343
|
*/
|
|
349
|
-
getSchemaNames() {
|
|
344
|
+
getSchemaNames () {
|
|
350
345
|
return Object.keys(this.schemas)
|
|
351
346
|
}
|
|
352
347
|
|
|
@@ -354,7 +349,7 @@ class Schemas extends EventEmitter {
|
|
|
354
349
|
* Returns information about all registered schemas
|
|
355
350
|
* @returns {Object}
|
|
356
351
|
*/
|
|
357
|
-
getSchemaInfo() {
|
|
352
|
+
getSchemaInfo () {
|
|
358
353
|
return Object.entries(this.schemas).reduce((info, [name, schema]) => {
|
|
359
354
|
info[name] = {
|
|
360
355
|
filePath: schema.filePath,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adapt-schemas",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Standalone JSON Schema library for the Adapt framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
".": "./index.js",
|
|
9
9
|
"./Schemas": "./lib/Schemas.js",
|
|
10
10
|
"./Schema": "./lib/Schema.js",
|
|
11
|
+
"./SchemaError": "./lib/SchemaError.js",
|
|
11
12
|
"./Keywords": "./lib/Keywords.js",
|
|
12
13
|
"./XSSDefaults": "./lib/XSSDefaults.js"
|
|
13
14
|
},
|
|
@@ -29,5 +30,30 @@
|
|
|
29
30
|
"ms": "^2.1.3",
|
|
30
31
|
"safe-regex": "^2.1.1",
|
|
31
32
|
"xss": "^1.0.14"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@semantic-release/git": "^10.0.1",
|
|
36
|
+
"conventional-changelog-eslint": "^6.0.0",
|
|
37
|
+
"semantic-release": "^25.0.3",
|
|
38
|
+
"standard": "^17.1.2"
|
|
39
|
+
},
|
|
40
|
+
"release": {
|
|
41
|
+
"plugins": [
|
|
42
|
+
[
|
|
43
|
+
"@semantic-release/commit-analyzer",
|
|
44
|
+
{
|
|
45
|
+
"preset": "eslint"
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
[
|
|
49
|
+
"@semantic-release/release-notes-generator",
|
|
50
|
+
{
|
|
51
|
+
"preset": "eslint"
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"@semantic-release/npm",
|
|
55
|
+
"@semantic-release/github",
|
|
56
|
+
"@semantic-release/git"
|
|
57
|
+
]
|
|
32
58
|
}
|
|
33
59
|
}
|
package/test.js
CHANGED
|
@@ -9,7 +9,7 @@ import fs from 'fs/promises'
|
|
|
9
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
10
10
|
const hasSpecifiedPath = Boolean(process.argv[2])
|
|
11
11
|
|
|
12
|
-
async function setupTestSchemas() {
|
|
12
|
+
async function setupTestSchemas () {
|
|
13
13
|
if (hasSpecifiedPath) return
|
|
14
14
|
// Create test schema directory
|
|
15
15
|
const testSchemaDir = path.join(__dirname, 'test-schemas')
|
|
@@ -17,51 +17,51 @@ async function setupTestSchemas() {
|
|
|
17
17
|
|
|
18
18
|
// Create a course schema with _globals
|
|
19
19
|
const courseSchema = {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
21
|
+
$anchor: 'course',
|
|
22
|
+
$merge: {
|
|
23
|
+
source: { $ref: 'base' },
|
|
24
|
+
with: {
|
|
25
|
+
properties: {
|
|
26
|
+
title: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
description: 'Course title',
|
|
29
|
+
default: 'Untitled Course'
|
|
30
30
|
},
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
description: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
description: 'Course description',
|
|
34
|
+
default: ''
|
|
35
35
|
},
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
36
|
+
_globals: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
description: 'Global settings',
|
|
39
|
+
properties: {
|
|
40
|
+
_accessibility: {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
_isEnabled: {
|
|
44
|
+
type: 'boolean',
|
|
45
|
+
default: true
|
|
46
46
|
},
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
skipNavigationText: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
default: 'Skip navigation'
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
required: [
|
|
53
|
+
'skipNavigationText'
|
|
54
54
|
]
|
|
55
55
|
},
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
_extensions: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
_trickle: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
properties: {
|
|
62
|
+
incompleteContent: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
default: 'There is incomplete content above'
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -70,31 +70,31 @@ async function setupTestSchemas() {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
},
|
|
73
|
-
|
|
73
|
+
required: ['title']
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// Create a content schema
|
|
79
79
|
const contentSchema = {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
80
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
81
|
+
$anchor: 'content',
|
|
82
|
+
$merge: {
|
|
83
|
+
source: { $ref: 'base' },
|
|
84
|
+
with: {
|
|
85
|
+
properties: {
|
|
86
|
+
_type: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
description: 'Content type'
|
|
89
89
|
},
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
body: {
|
|
91
|
+
type: 'string',
|
|
92
|
+
description: 'Content body',
|
|
93
|
+
default: ''
|
|
94
94
|
},
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
_isOptional: {
|
|
96
|
+
type: 'boolean',
|
|
97
|
+
default: false
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
}
|
|
@@ -103,39 +103,39 @@ async function setupTestSchemas() {
|
|
|
103
103
|
|
|
104
104
|
// Create a component schema that extends content
|
|
105
105
|
const componentSchema = {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
106
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
107
|
+
$anchor: 'component',
|
|
108
|
+
$merge: {
|
|
109
|
+
source: { $ref: 'content' },
|
|
110
|
+
with: {
|
|
111
|
+
properties: {
|
|
112
|
+
_component: {
|
|
113
|
+
type: 'string',
|
|
114
|
+
description: 'Component type'
|
|
115
115
|
}
|
|
116
116
|
},
|
|
117
|
-
|
|
117
|
+
required: ['_component']
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
// Create a patch schema that extends course
|
|
123
123
|
const coursePatchSchema = {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
124
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
125
|
+
$anchor: 'course-extension',
|
|
126
|
+
$patch: {
|
|
127
|
+
source: { $ref: 'course' },
|
|
128
|
+
with: {
|
|
129
|
+
properties: {
|
|
130
|
+
_globals: {
|
|
131
|
+
type: 'object',
|
|
132
|
+
properties: {
|
|
133
|
+
_myPlugin: {
|
|
134
|
+
type: 'object',
|
|
135
|
+
properties: {
|
|
136
|
+
buttonLabel: {
|
|
137
|
+
type: 'string',
|
|
138
|
+
default: 'Click me'
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
}
|
|
@@ -166,10 +166,10 @@ async function setupTestSchemas() {
|
|
|
166
166
|
return testSchemaDir
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
async function runTests() {
|
|
169
|
+
async function runTests () {
|
|
170
170
|
console.log('=== Adapt Schema Library Tests ===\n')
|
|
171
171
|
|
|
172
|
-
const testSchemaDir =
|
|
172
|
+
const testSchemaDir = hasSpecifiedPath
|
|
173
173
|
? path.join(__dirname, process.argv[2])
|
|
174
174
|
: await setupTestSchemas()
|
|
175
175
|
|
|
@@ -177,10 +177,7 @@ async function runTests() {
|
|
|
177
177
|
// Test 1: Initialize library
|
|
178
178
|
console.log('Test 1: Initialize library')
|
|
179
179
|
const library = new Schemas({
|
|
180
|
-
enableCache: true
|
|
181
|
-
directoryReplacements: {
|
|
182
|
-
'$ROOT': process.cwd()
|
|
183
|
-
}
|
|
180
|
+
enableCache: true
|
|
184
181
|
})
|
|
185
182
|
await library.init()
|
|
186
183
|
console.log(' ✓ Library initialized\n')
|
|
@@ -202,12 +199,12 @@ async function runTests() {
|
|
|
202
199
|
// Test 4: Get defaults
|
|
203
200
|
console.log('Test 4: Get schema defaults')
|
|
204
201
|
const courseDefaults = await library.getSchemaDefaults('course')
|
|
205
|
-
console.log(
|
|
202
|
+
console.log(' ✓ Course defaults:', JSON.stringify(courseDefaults, null, 4).split('\n').map(l => ' ' + l).join('\n'), '\n')
|
|
206
203
|
|
|
207
204
|
// Test 5: Get _globals defaults
|
|
208
205
|
console.log('Test 5: Get _globals defaults')
|
|
209
206
|
const globalsDefaults = await library.getGlobalsDefaults('course')
|
|
210
|
-
console.log(
|
|
207
|
+
console.log(' ✓ _globals defaults:', JSON.stringify(globalsDefaults, null, 4).split('\n').map(l => ' ' + l).join('\n'), '\n')
|
|
211
208
|
|
|
212
209
|
// Test 6: Validate data
|
|
213
210
|
console.log('Test 6: Validate data')
|
|
@@ -234,7 +231,7 @@ async function runTests() {
|
|
|
234
231
|
console.log('Test 7b: Type validation error')
|
|
235
232
|
try {
|
|
236
233
|
await library.validate('course', {
|
|
237
|
-
title: 12345
|
|
234
|
+
title: 12345 // Should be string, not number
|
|
238
235
|
})
|
|
239
236
|
console.log(' ✗ Should have thrown validation error\n')
|
|
240
237
|
} catch (e) {
|
|
@@ -267,22 +264,22 @@ async function runTests() {
|
|
|
267
264
|
// Create another test schema to trigger the event
|
|
268
265
|
const newSchemaPath = path.join(testSchemaDir, 'test-event.schema.json')
|
|
269
266
|
await fs.writeFile(newSchemaPath, JSON.stringify({
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
267
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
268
|
+
$anchor: 'test-event',
|
|
269
|
+
$merge: {
|
|
270
|
+
source: { $ref: 'base' },
|
|
271
|
+
with: { properties: { test: { type: 'string' } } }
|
|
275
272
|
}
|
|
276
273
|
}))
|
|
277
274
|
await library.registerSchema(newSchemaPath)
|
|
278
275
|
console.log('')
|
|
279
276
|
|
|
280
277
|
console.log('=== All tests passed! ===')
|
|
281
|
-
|
|
282
278
|
} finally {
|
|
283
|
-
if (hasSpecifiedPath)
|
|
284
|
-
|
|
285
|
-
|
|
279
|
+
if (!hasSpecifiedPath) {
|
|
280
|
+
// Cleanup
|
|
281
|
+
await fs.rm(testSchemaDir, { recursive: true, force: true })
|
|
282
|
+
}
|
|
286
283
|
}
|
|
287
284
|
}
|
|
288
285
|
|