adapt-schemas 1.0.0 → 1.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/.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/lib/Keywords.js +6 -25
- package/lib/Schema.js +12 -12
- package/lib/Schemas.js +28 -22
- package/package.json +26 -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/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
|
@@ -9,7 +9,7 @@ const BASE_SCHEMA_NAME = 'base'
|
|
|
9
9
|
* Schema-specific error class
|
|
10
10
|
*/
|
|
11
11
|
export class SchemaError extends Error {
|
|
12
|
-
constructor(code, message, data = {}) {
|
|
12
|
+
constructor (code, message, data = {}) {
|
|
13
13
|
super(message)
|
|
14
14
|
this.code = code
|
|
15
15
|
this.data = data
|
|
@@ -29,7 +29,7 @@ class Schema extends EventEmitter {
|
|
|
29
29
|
* @param {Object} options.xssWhitelist XSS whitelist configuration
|
|
30
30
|
* @param {SchemaLibrary} options.schemaLibrary Reference to the parent library
|
|
31
31
|
*/
|
|
32
|
-
constructor({ enableCache, filePath, validator, xssWhitelist, schemaLibrary }) {
|
|
32
|
+
constructor ({ enableCache, filePath, validator, xssWhitelist, schemaLibrary }) {
|
|
33
33
|
super()
|
|
34
34
|
|
|
35
35
|
/**
|
|
@@ -115,7 +115,7 @@ class Schema extends EventEmitter {
|
|
|
115
115
|
* Determines whether the current schema build is valid using last modification timestamp
|
|
116
116
|
* @returns {Promise<boolean>}
|
|
117
117
|
*/
|
|
118
|
-
async isBuildValid() {
|
|
118
|
+
async isBuildValid () {
|
|
119
119
|
if (!this.built) return false
|
|
120
120
|
|
|
121
121
|
let schema = this
|
|
@@ -131,7 +131,7 @@ class Schema extends EventEmitter {
|
|
|
131
131
|
* Returns the parent schema if $merge is defined (or the base schema if a root schema)
|
|
132
132
|
* @returns {Promise<Schema|undefined>}
|
|
133
133
|
*/
|
|
134
|
-
async getParent() {
|
|
134
|
+
async getParent () {
|
|
135
135
|
if (this.name === BASE_SCHEMA_NAME) return undefined
|
|
136
136
|
|
|
137
137
|
const parentRef = this.raw?.$merge?.source?.$ref ?? BASE_SCHEMA_NAME
|
|
@@ -142,7 +142,7 @@ class Schema extends EventEmitter {
|
|
|
142
142
|
* Loads the schema file
|
|
143
143
|
* @returns {Promise<Schema>} This instance
|
|
144
144
|
*/
|
|
145
|
-
async load() {
|
|
145
|
+
async load () {
|
|
146
146
|
try {
|
|
147
147
|
const content = await fs.readFile(this.filePath, 'utf-8')
|
|
148
148
|
this.raw = JSON.parse(content)
|
|
@@ -178,7 +178,7 @@ class Schema extends EventEmitter {
|
|
|
178
178
|
* @param {function} options.extensionFilter Filter function for extensions
|
|
179
179
|
* @returns {Promise<Schema>}
|
|
180
180
|
*/
|
|
181
|
-
async build(options = {}) {
|
|
181
|
+
async build (options = {}) {
|
|
182
182
|
if (options.useCache !== false && this.enableCache && await this.isBuildValid()) {
|
|
183
183
|
return this
|
|
184
184
|
}
|
|
@@ -196,7 +196,7 @@ class Schema extends EventEmitter {
|
|
|
196
196
|
|
|
197
197
|
while (parent) {
|
|
198
198
|
const parentBuilt = _.cloneDeep((await parent.build({ ...options, compile: false })).built)
|
|
199
|
-
built = this.patch(parentBuilt, built, { strict:
|
|
199
|
+
built = this.patch(parentBuilt, built, { strict: false })
|
|
200
200
|
parent = await parent.getParent()
|
|
201
201
|
}
|
|
202
202
|
|
|
@@ -237,7 +237,7 @@ class Schema extends EventEmitter {
|
|
|
237
237
|
* @param {boolean} options.strict Require $patch or $merge
|
|
238
238
|
* @returns {Object} The base schema
|
|
239
239
|
*/
|
|
240
|
-
patch(baseSchema, patchSchema, options = {}) {
|
|
240
|
+
patch (baseSchema, patchSchema, options = {}) {
|
|
241
241
|
const opts = _.defaults(options, {
|
|
242
242
|
extendAnnotations: patchSchema.$anchor !== BASE_SCHEMA_NAME,
|
|
243
243
|
overwriteProperties: true,
|
|
@@ -284,7 +284,7 @@ class Schema extends EventEmitter {
|
|
|
284
284
|
* @param {boolean} options.ignoreRequired Ignore required field errors
|
|
285
285
|
* @returns {Object} The validated data
|
|
286
286
|
*/
|
|
287
|
-
validate(dataToValidate, options = {}) {
|
|
287
|
+
validate (dataToValidate, options = {}) {
|
|
288
288
|
const opts = _.defaults(options, { useDefaults: true, ignoreRequired: false })
|
|
289
289
|
const data = _.defaults(_.cloneDeep(dataToValidate), opts.useDefaults ? this.getObjectDefaults() : {})
|
|
290
290
|
|
|
@@ -322,7 +322,7 @@ class Schema extends EventEmitter {
|
|
|
322
322
|
* @param {Object} schema Schema to use (defaults to built schema)
|
|
323
323
|
* @returns {Object} The sanitised data
|
|
324
324
|
*/
|
|
325
|
-
sanitise(dataToSanitise, options = {}, schema) {
|
|
325
|
+
sanitise (dataToSanitise, options = {}, schema) {
|
|
326
326
|
const opts = _.defaults(options, {
|
|
327
327
|
isInternal: false,
|
|
328
328
|
isReadOnly: false,
|
|
@@ -363,7 +363,7 @@ class Schema extends EventEmitter {
|
|
|
363
363
|
* Adds an extension schema
|
|
364
364
|
* @param {string} extSchemaName Extension schema name
|
|
365
365
|
*/
|
|
366
|
-
addExtension(extSchemaName) {
|
|
366
|
+
addExtension (extSchemaName) {
|
|
367
367
|
if (!this.extensions.includes(extSchemaName)) {
|
|
368
368
|
this.extensions.push(extSchemaName)
|
|
369
369
|
}
|
|
@@ -374,7 +374,7 @@ class Schema extends EventEmitter {
|
|
|
374
374
|
* @param {Object} schema Schema to extract defaults from
|
|
375
375
|
* @returns {Object} The defaults object
|
|
376
376
|
*/
|
|
377
|
-
getObjectDefaults(schema) {
|
|
377
|
+
getObjectDefaults (schema) {
|
|
378
378
|
schema = schema ?? this.built
|
|
379
379
|
const props = schema.properties ?? schema.$merge?.with?.properties ?? schema.$patch?.with?.properties
|
|
380
380
|
|
package/lib/Schemas.js
CHANGED
|
@@ -16,7 +16,7 @@ const BASE_SCHEMA_PATH = '../schema/base.schema.json'
|
|
|
16
16
|
* Schema library errors
|
|
17
17
|
*/
|
|
18
18
|
export class SchemaError extends Error {
|
|
19
|
-
constructor(code, message, data = {}) {
|
|
19
|
+
constructor (code, message, data = {}) {
|
|
20
20
|
super(message)
|
|
21
21
|
this.code = code
|
|
22
22
|
this.data = data
|
|
@@ -35,17 +35,15 @@ class Schemas extends EventEmitter {
|
|
|
35
35
|
* @param {Object} options.xssWhitelist Custom XSS whitelist tags/attributes
|
|
36
36
|
* @param {Boolean} options.xssWhitelistOverride Replace default whitelist instead of extending
|
|
37
37
|
* @param {Object} options.formatOverrides Custom string format RegExp patterns
|
|
38
|
-
* @param {Object} options.directoryReplacements Replacements for isDirectory keyword (e.g. { '$ROOT': '/app' })
|
|
39
38
|
*/
|
|
40
|
-
constructor(options = {}) {
|
|
39
|
+
constructor (options = {}) {
|
|
41
40
|
super()
|
|
42
41
|
|
|
43
42
|
this.options = _.defaults(options, {
|
|
44
43
|
enableCache: true,
|
|
45
44
|
xssWhitelist: {},
|
|
46
45
|
xssWhitelistOverride: false,
|
|
47
|
-
formatOverrides: {}
|
|
48
|
-
directoryReplacements: {}
|
|
46
|
+
formatOverrides: {}
|
|
49
47
|
})
|
|
50
48
|
|
|
51
49
|
/**
|
|
@@ -82,7 +80,7 @@ class Schemas extends EventEmitter {
|
|
|
82
80
|
removeAdditional: 'all',
|
|
83
81
|
strict: false,
|
|
84
82
|
verbose: true,
|
|
85
|
-
keywords: Keywords.all(
|
|
83
|
+
keywords: Keywords.all()
|
|
86
84
|
})
|
|
87
85
|
|
|
88
86
|
this.addStringFormats({
|
|
@@ -102,7 +100,7 @@ class Schemas extends EventEmitter {
|
|
|
102
100
|
* Initializes the schema library by loading the base schema
|
|
103
101
|
* @returns {Promise<SchemaLibrary>}
|
|
104
102
|
*/
|
|
105
|
-
async init() {
|
|
103
|
+
async init () {
|
|
106
104
|
if (this._initialized) return this
|
|
107
105
|
|
|
108
106
|
await this.resetSchemaRegistry()
|
|
@@ -115,7 +113,7 @@ class Schemas extends EventEmitter {
|
|
|
115
113
|
* Empties the schema registry (with the exception of the base schema)
|
|
116
114
|
* @returns {Promise<void>}
|
|
117
115
|
*/
|
|
118
|
-
async resetSchemaRegistry() {
|
|
116
|
+
async resetSchemaRegistry () {
|
|
119
117
|
this.schemas = {
|
|
120
118
|
base: await this.createSchema(path.resolve(__dirname, BASE_SCHEMA_PATH), { enableCache: true })
|
|
121
119
|
}
|
|
@@ -126,7 +124,7 @@ class Schemas extends EventEmitter {
|
|
|
126
124
|
* Adds string formats to the Ajv validator
|
|
127
125
|
* @param {Object<string, RegExp>} formats
|
|
128
126
|
*/
|
|
129
|
-
addStringFormats(formats) {
|
|
127
|
+
addStringFormats (formats) {
|
|
130
128
|
Object.entries(formats).forEach(([name, re]) => {
|
|
131
129
|
const isUnsafe = !safeRegex(re)
|
|
132
130
|
if (isUnsafe) {
|
|
@@ -139,9 +137,17 @@ class Schemas extends EventEmitter {
|
|
|
139
137
|
/**
|
|
140
138
|
* Adds a new keyword to be used in JSON schemas
|
|
141
139
|
* @param {Object} definition AJV keyword definition
|
|
140
|
+
* @param {Object} options Configuration options
|
|
141
|
+
* @param {Boolean} options.override Whether an existing keyword should be overridden
|
|
142
142
|
*/
|
|
143
|
-
addKeyword(definition) {
|
|
143
|
+
addKeyword (definition, options = {}) {
|
|
144
144
|
try {
|
|
145
|
+
if (this.validator.getKeyword(definition.keyword)) {
|
|
146
|
+
if (options.override !== true) {
|
|
147
|
+
throw new SchemaError('KEYWORD_EXISTS', 'Keyword already exists')
|
|
148
|
+
}
|
|
149
|
+
this.validator.removeKeyword(definition.keyword)
|
|
150
|
+
}
|
|
145
151
|
this.validator.addKeyword(definition)
|
|
146
152
|
} catch (e) {
|
|
147
153
|
this.emit('warning', `Failed to define keyword '${definition.keyword}', ${e}`)
|
|
@@ -156,7 +162,7 @@ class Schemas extends EventEmitter {
|
|
|
156
162
|
* @param {string} options.cwd Base directory for glob patterns
|
|
157
163
|
* @returns {Promise<void>}
|
|
158
164
|
*/
|
|
159
|
-
async loadSchemas(patterns, options = {}) {
|
|
165
|
+
async loadSchemas (patterns, options = {}) {
|
|
160
166
|
if (!this._initialized) {
|
|
161
167
|
await this.init()
|
|
162
168
|
}
|
|
@@ -196,7 +202,7 @@ class Schemas extends EventEmitter {
|
|
|
196
202
|
* @param {boolean} options.replace Replace existing schema with same name
|
|
197
203
|
* @returns {Promise<Schema>}
|
|
198
204
|
*/
|
|
199
|
-
async registerSchema(filePath, options = {}) {
|
|
205
|
+
async registerSchema (filePath, options = {}) {
|
|
200
206
|
if (!_.isString(filePath)) {
|
|
201
207
|
throw new SchemaError('INVALID_PARAMS', 'filePath must be a string', { params: ['filePath'] })
|
|
202
208
|
}
|
|
@@ -229,7 +235,7 @@ class Schemas extends EventEmitter {
|
|
|
229
235
|
* Deregisters a single JSON schema
|
|
230
236
|
* @param {string} name Schema name to deregister
|
|
231
237
|
*/
|
|
232
|
-
deregisterSchema(name) {
|
|
238
|
+
deregisterSchema (name) {
|
|
233
239
|
if (this.schemas[name]) {
|
|
234
240
|
delete this.schemas[name]
|
|
235
241
|
}
|
|
@@ -246,7 +252,7 @@ class Schemas extends EventEmitter {
|
|
|
246
252
|
* @param {Object} options Options passed to Schema constructor
|
|
247
253
|
* @returns {Promise<Schema>}
|
|
248
254
|
*/
|
|
249
|
-
async createSchema(filePath, options = {}) {
|
|
255
|
+
async createSchema (filePath, options = {}) {
|
|
250
256
|
const schema = new Schema({
|
|
251
257
|
enableCache: options.enableCache ?? this.options.enableCache,
|
|
252
258
|
filePath,
|
|
@@ -267,7 +273,7 @@ class Schemas extends EventEmitter {
|
|
|
267
273
|
* @param {string} baseSchemaName The name of the schema to extend
|
|
268
274
|
* @param {string} extSchemaName The name of the schema to extend with
|
|
269
275
|
*/
|
|
270
|
-
extendSchema(baseSchemaName, extSchemaName) {
|
|
276
|
+
extendSchema (baseSchemaName, extSchemaName) {
|
|
271
277
|
const baseSchema = this.schemas[baseSchemaName]
|
|
272
278
|
if (baseSchema) {
|
|
273
279
|
baseSchema.addExtension(extSchemaName)
|
|
@@ -290,7 +296,7 @@ class Schemas extends EventEmitter {
|
|
|
290
296
|
* @param {function} options.extensionFilter Filter function for extensions
|
|
291
297
|
* @returns {Promise<Schema>}
|
|
292
298
|
*/
|
|
293
|
-
async getSchema(schemaName, options = {}) {
|
|
299
|
+
async getSchema (schemaName, options = {}) {
|
|
294
300
|
const schema = this.schemas[schemaName]
|
|
295
301
|
if (!schema) {
|
|
296
302
|
throw new SchemaError('MISSING_SCHEMA', `Schema '${schemaName}' not found`, { schemaName })
|
|
@@ -305,7 +311,7 @@ class Schemas extends EventEmitter {
|
|
|
305
311
|
* @param {Object} options Validation options
|
|
306
312
|
* @returns {Promise<Object>} The validated data with defaults applied
|
|
307
313
|
*/
|
|
308
|
-
async validate(schemaName, data, options = {}) {
|
|
314
|
+
async validate (schemaName, data, options = {}) {
|
|
309
315
|
const schema = await this.getSchema(schemaName)
|
|
310
316
|
return schema.validate(data, options)
|
|
311
317
|
}
|
|
@@ -316,7 +322,7 @@ class Schemas extends EventEmitter {
|
|
|
316
322
|
* @param {Object} options Build options
|
|
317
323
|
* @returns {Promise<Object>} The built schema object
|
|
318
324
|
*/
|
|
319
|
-
async getBuiltSchema(schemaName, options = {}) {
|
|
325
|
+
async getBuiltSchema (schemaName, options = {}) {
|
|
320
326
|
const schema = await this.getSchema(schemaName)
|
|
321
327
|
return schema.built
|
|
322
328
|
}
|
|
@@ -326,7 +332,7 @@ class Schemas extends EventEmitter {
|
|
|
326
332
|
* @param {string} schemaName The schema name
|
|
327
333
|
* @returns {Promise<Object>} The defaults object
|
|
328
334
|
*/
|
|
329
|
-
async getSchemaDefaults(schemaName) {
|
|
335
|
+
async getSchemaDefaults (schemaName) {
|
|
330
336
|
const schema = await this.getSchema(schemaName)
|
|
331
337
|
return schema.getObjectDefaults()
|
|
332
338
|
}
|
|
@@ -336,7 +342,7 @@ class Schemas extends EventEmitter {
|
|
|
336
342
|
* @param {string} schemaName Schema name (defaults to 'course')
|
|
337
343
|
* @returns {Promise<Object>} The _globals defaults
|
|
338
344
|
*/
|
|
339
|
-
async getGlobalsDefaults(schemaName = 'course') {
|
|
345
|
+
async getGlobalsDefaults (schemaName = 'course') {
|
|
340
346
|
const schema = await this.getSchema(schemaName)
|
|
341
347
|
const defaults = schema.getObjectDefaults()
|
|
342
348
|
return defaults._globals || {}
|
|
@@ -346,7 +352,7 @@ class Schemas extends EventEmitter {
|
|
|
346
352
|
* Returns list of all registered schema names
|
|
347
353
|
* @returns {string[]}
|
|
348
354
|
*/
|
|
349
|
-
getSchemaNames() {
|
|
355
|
+
getSchemaNames () {
|
|
350
356
|
return Object.keys(this.schemas)
|
|
351
357
|
}
|
|
352
358
|
|
|
@@ -354,7 +360,7 @@ class Schemas extends EventEmitter {
|
|
|
354
360
|
* Returns information about all registered schemas
|
|
355
361
|
* @returns {Object}
|
|
356
362
|
*/
|
|
357
|
-
getSchemaInfo() {
|
|
363
|
+
getSchemaInfo () {
|
|
358
364
|
return Object.entries(this.schemas).reduce((info, [name, schema]) => {
|
|
359
365
|
info[name] = {
|
|
360
366
|
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.1",
|
|
4
4
|
"description": "Standalone JSON Schema library for the Adapt framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -29,5 +29,30 @@
|
|
|
29
29
|
"ms": "^2.1.3",
|
|
30
30
|
"safe-regex": "^2.1.1",
|
|
31
31
|
"xss": "^1.0.14"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@semantic-release/git": "^10.0.1",
|
|
35
|
+
"conventional-changelog-eslint": "^6.0.0",
|
|
36
|
+
"semantic-release": "^25.0.3",
|
|
37
|
+
"standard": "^17.1.2"
|
|
38
|
+
},
|
|
39
|
+
"release": {
|
|
40
|
+
"plugins": [
|
|
41
|
+
[
|
|
42
|
+
"@semantic-release/commit-analyzer",
|
|
43
|
+
{
|
|
44
|
+
"preset": "eslint"
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
[
|
|
48
|
+
"@semantic-release/release-notes-generator",
|
|
49
|
+
{
|
|
50
|
+
"preset": "eslint"
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"@semantic-release/npm",
|
|
54
|
+
"@semantic-release/github",
|
|
55
|
+
"@semantic-release/git"
|
|
56
|
+
]
|
|
32
57
|
}
|
|
33
58
|
}
|
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
|
|