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.
@@ -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
@@ -0,0 +1,13 @@
1
+ name: Standard.js
2
+ on: push
3
+ jobs:
4
+ default:
5
+ runs-on: ubuntu-latest
6
+ steps:
7
+ - uses: actions/checkout@master
8
+ - uses: actions/setup-node@master
9
+ with:
10
+ node-version: 'lts/*'
11
+ cache: 'npm'
12
+ - run: npm ci
13
+ - run: npx standard
@@ -0,0 +1,13 @@
1
+ name: Tests
2
+ on: push
3
+ jobs:
4
+ default:
5
+ runs-on: ubuntu-latest
6
+ steps:
7
+ - uses: actions/checkout@master
8
+ - uses: actions/setup-node@master
9
+ with:
10
+ node-version: 'lts/*'
11
+ cache: 'npm'
12
+ - run: npm ci
13
+ - run: npm test
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: {}, // Custom string format RegExp patterns
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 { Schemas, Schema, SchemaError, Keywords, XSSDefaults }
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(directoryReplacements = {}) {
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 () => true
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: !parent.name == BASE_SCHEMA_NAME })
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
 
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Schema-specific error class
3
+ */
4
+ class SchemaError extends Error {
5
+ constructor (code, message, data = {}) {
6
+ super(message)
7
+ this.code = code
8
+ this.data = data
9
+ this.name = 'SchemaError'
10
+ }
11
+ }
12
+
13
+ export default SchemaError
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 Keywords from './Keywords.js'
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(this.options.directoryReplacements)
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.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
- "$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"
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
- "description": {
32
- "type": "string",
33
- "description": "Course description",
34
- "default": ""
31
+ description: {
32
+ type: 'string',
33
+ description: 'Course description',
34
+ default: ''
35
35
  },
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
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
- "skipNavigationText": {
48
- "type": "string",
49
- "default": "Skip navigation"
47
+ skipNavigationText: {
48
+ type: 'string',
49
+ default: 'Skip navigation'
50
50
  }
51
51
  },
52
- "required": [
53
- "skipNavigationText"
52
+ required: [
53
+ 'skipNavigationText'
54
54
  ]
55
55
  },
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"
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
- "required": ["title"]
73
+ required: ['title']
74
74
  }
75
75
  }
76
76
  }
77
77
 
78
78
  // Create a content schema
79
79
  const contentSchema = {
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"
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
- "body": {
91
- "type": "string",
92
- "description": "Content body",
93
- "default": ""
90
+ body: {
91
+ type: 'string',
92
+ description: 'Content body',
93
+ default: ''
94
94
  },
95
- "_isOptional": {
96
- "type": "boolean",
97
- "default": false
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
- "$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"
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
- "required": ["_component"]
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
- "$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"
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 = hasSpecifiedPath
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(` ✓ Course defaults:`, JSON.stringify(courseDefaults, null, 4).split('\n').map(l => ' ' + l).join('\n'), '\n')
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(` ✓ _globals defaults:`, JSON.stringify(globalsDefaults, null, 4).split('\n').map(l => ' ' + l).join('\n'), '\n')
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 // Should be string, not number
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
- "$schema": "https://json-schema.org/draft/2020-12/schema",
271
- "$anchor": "test-event",
272
- "$merge": {
273
- "source": { "$ref": "base" },
274
- "with": { "properties": { "test": { "type": "string" } } }
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) return
284
- // Cleanup
285
- await fs.rm(testSchemaDir, { recursive: true, force: true })
279
+ if (!hasSpecifiedPath) {
280
+ // Cleanup
281
+ await fs.rm(testSchemaDir, { recursive: true, force: true })
282
+ }
286
283
  }
287
284
  }
288
285