adapt-schemas 3.0.0 → 3.1.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/lib/Schema.js CHANGED
@@ -272,10 +272,11 @@ class Schema extends EventEmitter {
272
272
  * @param {Object} options
273
273
  * @param {boolean} options.useDefaults Apply schema defaults (default: true)
274
274
  * @param {boolean} options.ignoreRequired Ignore required field errors
275
+ * @param {boolean} options.ignoreErrors Ignore all validation errors (implies ignoreRequired)
275
276
  * @returns {Object} The validated data
276
277
  */
277
278
  validate (dataToValidate, options = {}) {
278
- const opts = _.defaults(options, { useDefaults: true, ignoreRequired: false })
279
+ const opts = _.defaults(options, { useDefaults: true, ignoreRequired: false, ignoreErrors: false })
279
280
  const data = _.cloneDeep(dataToValidate)
280
281
  let compiled = opts.useDefaults ? this.compiledWithDefaults : this.compiled
281
282
 
@@ -287,17 +288,19 @@ class Schema extends EventEmitter {
287
288
 
288
289
  compiled(data)
289
290
 
290
- const errors = compiled.errors && compiled.errors
291
- .filter(e => e.keyword === 'required' ? !opts.ignoreRequired : true)
292
- .map(e => e.instancePath ? `${e.instancePath} ${e.message}` : e.message)
293
- .join(', ')
291
+ if (!opts.ignoreErrors) {
292
+ const errors = compiled.errors && compiled.errors
293
+ .filter(e => e.keyword === 'required' ? !opts.ignoreRequired : true)
294
+ .map(e => e.instancePath ? `${e.instancePath} ${e.message}` : e.message)
295
+ .join(', ')
294
296
 
295
- if (errors?.length) {
296
- throw new SchemaError('VALIDATION_FAILED', `Validation failed for schema: ${this.name} with errors: ${errors}`, {
297
- schemaName: this.name,
298
- errors,
299
- data
300
- })
297
+ if (errors?.length) {
298
+ throw new SchemaError('VALIDATION_FAILED', `Validation failed for schema: ${this.name} with errors: ${errors}`, {
299
+ schemaName: this.name,
300
+ errors,
301
+ data
302
+ })
303
+ }
301
304
  }
302
305
 
303
306
  return data
package/lib/Schemas.js CHANGED
@@ -110,6 +110,12 @@ class Schemas extends EventEmitter {
110
110
  * Empties the schema registry (with the exception of the base schema)
111
111
  */
112
112
  resetSchemaRegistry () {
113
+ // Remove previously registered schemas from AJV instances
114
+ for (const name of Object.keys(this.schemas)) {
115
+ for (const v of [this.validator, this.validatorWithDefaults]) {
116
+ if (v.getSchema(name)) v.removeSchema(name)
117
+ }
118
+ }
113
119
  this.schemas = {
114
120
  base: this.createSchema(path.resolve(__dirname, BASE_SCHEMA_PATH), { enableCache: true })
115
121
  }
@@ -224,6 +230,16 @@ class Schemas extends EventEmitter {
224
230
  }
225
231
 
226
232
  this.schemas[schema.name] = schema
233
+
234
+ // AJV's sync compile() can only resolve $refs against schemas in its internal store to allow `$ref: "schemaName`"
235
+ // Note we only add simple schemas (no $merge/$patch)
236
+ if (!schema.raw.$merge && !schema.raw.$patch) {
237
+ for (const v of [this.validator, this.validatorWithDefaults]) {
238
+ if (v.getSchema(schema.name)) v.removeSchema(schema.name)
239
+ v.addSchema(schema.raw, schema.name)
240
+ }
241
+ }
242
+
227
243
  this.schemaExtensions?.[schema.name]?.forEach(s => schema.addExtension(s))
228
244
 
229
245
  if (schema.raw.$patch) {
@@ -242,6 +258,10 @@ class Schemas extends EventEmitter {
242
258
  if (this.schemas[name]) {
243
259
  delete this.schemas[name]
244
260
  }
261
+ // Remove from AJV instances
262
+ for (const v of [this.validator, this.validatorWithDefaults]) {
263
+ if (v.getSchema(name)) v.removeSchema(name)
264
+ }
245
265
  // Remove schema from any extensions lists
246
266
  Object.entries(this.schemaExtensions).forEach(([base, extensions]) => {
247
267
  this.schemaExtensions[base] = extensions.filter(s => s !== name)
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "type": "git",
5
5
  "url": "https://github.com/cgkineo/adapt-schemas"
6
6
  },
7
- "version": "3.0.0",
7
+ "version": "3.1.1",
8
8
  "description": "Standalone JSON Schema library for the Adapt framework",
9
9
  "type": "module",
10
10
  "main": "index.js",
package/test.js CHANGED
@@ -165,6 +165,63 @@ async function setupTestSchemas () {
165
165
  }
166
166
  }
167
167
 
168
+ // Create a config extension that mimics adapt-contrib-languagePicker
169
+ const languagePickerConfigSchema = {
170
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
171
+ $anchor: 'languagePicker-config',
172
+ $patch: {
173
+ source: { $ref: 'config' },
174
+ with: {
175
+ properties: {
176
+ _languagePicker: {
177
+ type: 'object',
178
+ default: {},
179
+ properties: {
180
+ _isEnabled: { type: 'boolean', default: false },
181
+ _showOnCourseLoad: { type: 'boolean', default: true },
182
+ _languagePickerIconClass: { type: 'string', default: 'icon-language-2' },
183
+ _restoreStateOnLanguageChange: { type: 'boolean', default: false },
184
+ _classes: { type: 'string', default: '' },
185
+ _display: {
186
+ type: 'object',
187
+ // deliberately no default: {} — tests Case 3
188
+ properties: {
189
+ _iconClass: { type: 'string', default: 'icon-default' },
190
+ _position: { type: 'string', default: 'left' }
191
+ }
192
+ },
193
+ _languages: {
194
+ type: 'array',
195
+ items: {
196
+ type: 'object',
197
+ properties: {
198
+ _language: { type: 'string', default: '' },
199
+ _direction: { type: 'string', default: 'ltr' },
200
+ _isDisabled: { type: 'boolean', default: false },
201
+ displayName: { type: 'string', default: '' },
202
+ _buttons: {
203
+ type: 'object',
204
+ default: {},
205
+ properties: {
206
+ yes: { type: 'string', default: 'Yes' },
207
+ no: { type: 'string', default: 'No' }
208
+ }
209
+ }
210
+ }
211
+ }
212
+ }
213
+ }
214
+ }
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ await fs.writeFile(
221
+ path.join(testSchemaDir, 'languagePicker-config.schema.json'),
222
+ JSON.stringify(languagePickerConfigSchema, null, 2)
223
+ )
224
+
168
225
  // Create a patch schema that extends course
169
226
  const coursePatchSchema = {
170
227
  $schema: 'https://json-schema.org/draft/2020-12/schema',
@@ -413,6 +470,110 @@ async function runTests () {
413
470
  }
414
471
  console.log('')
415
472
 
473
+ // Test 14: ignoreErrors applies defaults without throwing on invalid data
474
+ console.log('Test 14: ignoreErrors applies defaults without throwing')
475
+ const invalidData = await library.validate('course', {
476
+ title: 12345 // wrong type — would normally throw
477
+ }, { ignoreErrors: true })
478
+ const defaultApplied = invalidData.description === ''
479
+ const invalidPreserved = invalidData.title === 12345
480
+ console.log(` ✓ Default description applied despite type error: ${defaultApplied}`)
481
+ console.log(` ✓ Invalid title value preserved: ${invalidPreserved}`)
482
+ if (!defaultApplied || !invalidPreserved) {
483
+ throw new Error('ignoreErrors did not apply defaults or preserve existing data')
484
+ }
485
+
486
+ // Verify it would throw without ignoreErrors
487
+ try {
488
+ await library.validate('course', { title: 12345 })
489
+ throw new Error('Should have thrown without ignoreErrors')
490
+ } catch (e) {
491
+ if (e.code !== 'VALIDATION_FAILED') throw e
492
+ console.log(' ✓ Same data throws without ignoreErrors: true')
493
+ }
494
+ console.log('')
495
+
496
+ // Test 15: ignoreErrors with missing required fields
497
+ console.log('Test 15: ignoreErrors ignores required field errors')
498
+ const missingRequired = await library.validate('course', {
499
+ description: 'No title provided'
500
+ }, { useDefaults: false, ignoreErrors: true })
501
+ const descPreserved = missingRequired.description === 'No title provided'
502
+ console.log(` ✓ Data returned despite missing required "title": ${descPreserved}`)
503
+ if (!descPreserved) {
504
+ throw new Error('ignoreErrors did not return data with missing required fields')
505
+ }
506
+ console.log('')
507
+
508
+ // Test 16: Issue #30 Case 1 — Partially populated object missing sibling defaults
509
+ console.log('Test 16: Issue #30 Case 1 — Partially populated plugin config')
510
+ const partialConfig = await library.validate('config', {
511
+ _languagePicker: {
512
+ _isEnabled: true,
513
+ _languages: [
514
+ {
515
+ _language: 'en',
516
+ _direction: 'ltr',
517
+ displayName: 'English'
518
+ }
519
+ ]
520
+ }
521
+ }, { ignoreErrors: true })
522
+ const lp = partialConfig._languagePicker
523
+ const case1Checks = [
524
+ ['_isEnabled preserved', lp._isEnabled === true],
525
+ ['_showOnCourseLoad defaulted to true', lp._showOnCourseLoad === true],
526
+ ['_languagePickerIconClass defaulted', lp._languagePickerIconClass === 'icon-language-2'],
527
+ ['_restoreStateOnLanguageChange defaulted', lp._restoreStateOnLanguageChange === false],
528
+ ['_classes defaulted to empty string', lp._classes === ''],
529
+ ['_languages[0]._isDisabled defaulted', lp._languages[0]._isDisabled === false],
530
+ ['_languages[0]._buttons defaulted', lp._languages[0]._buttons?.yes === 'Yes'],
531
+ ['_languages[0]._buttons.no defaulted', lp._languages[0]._buttons?.no === 'No']
532
+ ]
533
+ case1Checks.forEach(([desc, ok]) => console.log(` ${ok ? '✓' : '✗'} ${desc}: ${ok}`))
534
+ const case1Failed = case1Checks.filter(([, ok]) => !ok)
535
+ if (case1Failed.length) {
536
+ throw new Error(`Case 1 failures: ${case1Failed.map(([d]) => d).join(', ')}`)
537
+ }
538
+ console.log('')
539
+
540
+ // Test 17: Issue #30 Case 2 — Plugin object entirely absent
541
+ console.log('Test 17: Issue #30 Case 2 — Plugin object entirely absent')
542
+ const emptyConfig = await library.validate('config', {}, { ignoreErrors: true })
543
+ const lp2 = emptyConfig._languagePicker
544
+ const case2Checks = [
545
+ ['_languagePicker created from default: {}', lp2 !== undefined],
546
+ ['_isEnabled defaulted to false', lp2?._isEnabled === false],
547
+ ['_showOnCourseLoad defaulted to true', lp2?._showOnCourseLoad === true],
548
+ ['_languagePickerIconClass defaulted', lp2?._languagePickerIconClass === 'icon-language-2']
549
+ ]
550
+ case2Checks.forEach(([desc, ok]) => console.log(` ${ok ? '✓' : '✗'} ${desc}: ${ok}`))
551
+ const case2Failed = case2Checks.filter(([, ok]) => !ok)
552
+ if (case2Failed.length) {
553
+ throw new Error(`Case 2 failures: ${case2Failed.map(([d]) => d).join(', ')}`)
554
+ }
555
+ console.log('')
556
+
557
+ // Test 18: Issue #30 Case 3 — Nested object without default: {}
558
+ console.log('Test 18: Issue #30 Case 3 — Nested object without default: {}')
559
+ const noNestedDefault = await library.validate('config', {
560
+ _languagePicker: { _isEnabled: true }
561
+ }, { ignoreErrors: true })
562
+ const lp3 = noNestedDefault._languagePicker
563
+ // _display has no default: {} so it should NOT be created by AJV
564
+ const displayMissing = lp3._display === undefined
565
+ console.log(` ✓ _display not created (no default: {}): ${displayMissing}`)
566
+ if (!displayMissing) {
567
+ console.log(` Note: _display was unexpectedly created: ${JSON.stringify(lp3._display)}`)
568
+ }
569
+ // But sibling defaults should still apply
570
+ const siblingsApplied = lp3._showOnCourseLoad === true && lp3._languagePickerIconClass === 'icon-language-2'
571
+ console.log(` ✓ Sibling defaults still applied: ${siblingsApplied}`)
572
+ if (!siblingsApplied) {
573
+ throw new Error('Sibling defaults not applied when nested object has no default')
574
+ }
575
+ console.log('')
576
+
416
577
  console.log('=== All tests passed! ===')
417
578
  } finally {
418
579
  if (!hasSpecifiedPath) {