adapt-schemas 1.2.1 → 2.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/README.md +2 -21
- package/lib/Schema.js +27 -28
- package/lib/Schemas.js +25 -33
- package/package.json +1 -1
- package/test.js +142 -24
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# adapt-schemas
|
|
2
2
|
|
|
3
|
-
A standalone JSON Schema library for the Adapt framework stack. Load schemas from plugin folders via glob patterns
|
|
3
|
+
A standalone JSON Schema library for the Adapt framework stack. Load schemas from plugin folders via glob patterns and validate JSON data with automatic defaults application via AJV.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -23,13 +23,10 @@ await library.loadSchemas('**/schema/*.schema.json', {
|
|
|
23
23
|
ignore: ['**/node_modules/**']
|
|
24
24
|
})
|
|
25
25
|
|
|
26
|
-
// Validate data against a schema
|
|
26
|
+
// Validate data against a schema (defaults are applied automatically)
|
|
27
27
|
const validatedData = await library.validate('course', {
|
|
28
28
|
title: 'My Course'
|
|
29
29
|
})
|
|
30
|
-
|
|
31
|
-
// Get _globals defaults from the course schema
|
|
32
|
-
const globals = await library.getGlobalsDefaults('course')
|
|
33
30
|
```
|
|
34
31
|
|
|
35
32
|
## API Reference
|
|
@@ -110,22 +107,6 @@ const validated = await library.validate('course', inputData, {
|
|
|
110
107
|
})
|
|
111
108
|
```
|
|
112
109
|
|
|
113
|
-
##### `getSchemaDefaults(schemaName)`
|
|
114
|
-
Returns all defaults as a structured object.
|
|
115
|
-
|
|
116
|
-
```javascript
|
|
117
|
-
const defaults = await library.getSchemaDefaults('course')
|
|
118
|
-
// { title: 'Untitled', _globals: { ... } }
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
##### `getGlobalsDefaults(schemaName)`
|
|
122
|
-
Extracts `_globals` defaults from a schema.
|
|
123
|
-
|
|
124
|
-
```javascript
|
|
125
|
-
const globals = await library.getGlobalsDefaults('course')
|
|
126
|
-
// { _accessibility: { _isEnabled: true, ... }, _extensions: { ... } }
|
|
127
|
-
```
|
|
128
|
-
|
|
129
110
|
##### `getSchemaNames()`
|
|
130
111
|
Returns list of all registered schema names.
|
|
131
112
|
|
package/lib/Schema.js
CHANGED
|
@@ -18,7 +18,7 @@ class Schema extends EventEmitter {
|
|
|
18
18
|
* @param {Object} options.xssWhitelist XSS whitelist configuration
|
|
19
19
|
* @param {SchemaLibrary} options.schemaLibrary Reference to the parent library
|
|
20
20
|
*/
|
|
21
|
-
constructor ({ enableCache, filePath, validator, xssWhitelist, schemaLibrary }) {
|
|
21
|
+
constructor ({ enableCache, filePath, validatorWithDefaults, validator, xssWhitelist, schemaLibrary }) {
|
|
22
22
|
super()
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -28,11 +28,17 @@ class Schema extends EventEmitter {
|
|
|
28
28
|
this.built = undefined
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
31
|
+
* Compiled validation function
|
|
32
32
|
* @type {function}
|
|
33
33
|
*/
|
|
34
34
|
this.compiled = undefined
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Compiled validation function with useDefaults enabled
|
|
38
|
+
* @type {function}
|
|
39
|
+
*/
|
|
40
|
+
this.compiledWithDefaults = undefined
|
|
41
|
+
|
|
36
42
|
/**
|
|
37
43
|
* Whether caching is enabled for this schema
|
|
38
44
|
* @type {boolean}
|
|
@@ -76,7 +82,13 @@ class Schema extends EventEmitter {
|
|
|
76
82
|
this.name = undefined
|
|
77
83
|
|
|
78
84
|
/**
|
|
79
|
-
*
|
|
85
|
+
* Ajv instance with useDefaults enabled
|
|
86
|
+
* @type {Ajv}
|
|
87
|
+
*/
|
|
88
|
+
this.validatorWithDefaults = validatorWithDefaults
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Ajv instance without useDefaults
|
|
80
92
|
* @type {Ajv}
|
|
81
93
|
*/
|
|
82
94
|
this.validator = validator
|
|
@@ -143,8 +155,8 @@ class Schema extends EventEmitter {
|
|
|
143
155
|
})
|
|
144
156
|
}
|
|
145
157
|
|
|
146
|
-
if (this.
|
|
147
|
-
const errors = this.
|
|
158
|
+
if (this.validatorWithDefaults.validateSchema(this.raw)?.errors) {
|
|
159
|
+
const errors = this.validatorWithDefaults.errors
|
|
148
160
|
.map(e => e.instancePath ? `${e.instancePath} ${e.message}` : e.message)
|
|
149
161
|
|
|
150
162
|
if (errors.length) {
|
|
@@ -202,6 +214,7 @@ class Schema extends EventEmitter {
|
|
|
202
214
|
this.built = built
|
|
203
215
|
|
|
204
216
|
if (options.compile !== false) {
|
|
217
|
+
this.compiledWithDefaults = await this.validatorWithDefaults.compileAsync(built)
|
|
205
218
|
this.compiled = await this.validator.compileAsync(built)
|
|
206
219
|
}
|
|
207
220
|
|
|
@@ -275,22 +288,24 @@ class Schema extends EventEmitter {
|
|
|
275
288
|
*/
|
|
276
289
|
validate (dataToValidate, options = {}) {
|
|
277
290
|
const opts = _.defaults(options, { useDefaults: true, ignoreRequired: false })
|
|
278
|
-
const data = _.
|
|
291
|
+
const data = _.cloneDeep(dataToValidate)
|
|
292
|
+
let compiled = opts.useDefaults ? this.compiledWithDefaults : this.compiled
|
|
279
293
|
|
|
280
|
-
if (!
|
|
294
|
+
if (!compiled) {
|
|
281
295
|
this.emit('warning', `No compiled function for ${this.name}, compiling now`)
|
|
282
|
-
this.
|
|
296
|
+
const v = opts.useDefaults ? this.validatorWithDefaults : this.validator
|
|
297
|
+
compiled = v.compile(this.built)
|
|
283
298
|
}
|
|
284
299
|
|
|
285
|
-
|
|
300
|
+
compiled(data)
|
|
286
301
|
|
|
287
|
-
const errors =
|
|
302
|
+
const errors = compiled.errors && compiled.errors
|
|
288
303
|
.filter(e => e.keyword === 'required' ? !opts.ignoreRequired : true)
|
|
289
304
|
.map(e => e.instancePath ? `${e.instancePath} ${e.message}` : e.message)
|
|
290
|
-
.
|
|
305
|
+
.join(', ')
|
|
291
306
|
|
|
292
307
|
if (errors?.length) {
|
|
293
|
-
throw new SchemaError('VALIDATION_FAILED', `Validation failed for schema: ${this.name}`, {
|
|
308
|
+
throw new SchemaError('VALIDATION_FAILED', `Validation failed for schema: ${this.name} with errors: ${errors}`, {
|
|
294
309
|
schemaName: this.name,
|
|
295
310
|
errors,
|
|
296
311
|
data
|
|
@@ -358,22 +373,6 @@ class Schema extends EventEmitter {
|
|
|
358
373
|
}
|
|
359
374
|
}
|
|
360
375
|
|
|
361
|
-
/**
|
|
362
|
-
* Returns all schema defaults as a correctly structured object
|
|
363
|
-
* @param {Object} schema Schema to extract defaults from
|
|
364
|
-
* @returns {Object} The defaults object
|
|
365
|
-
*/
|
|
366
|
-
getObjectDefaults (schema) {
|
|
367
|
-
schema = schema ?? this.built
|
|
368
|
-
const props = schema.properties ?? schema.$merge?.with?.properties ?? schema.$patch?.with?.properties
|
|
369
|
-
|
|
370
|
-
return _.mapValues(props, s => {
|
|
371
|
-
return s.type === 'object' && s.properties
|
|
372
|
-
? this.getObjectDefaults(s)
|
|
373
|
-
: s.default
|
|
374
|
-
})
|
|
375
|
-
}
|
|
376
|
-
|
|
377
376
|
/**
|
|
378
377
|
* Walks the built schema alongside data, returning fields matching a predicate.
|
|
379
378
|
* @param {Object} data - The data object to walk
|
package/lib/Schemas.js
CHANGED
|
@@ -57,11 +57,7 @@ class Schemas extends EventEmitter {
|
|
|
57
57
|
this.options.xssWhitelist
|
|
58
58
|
)
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
* Reference to the Ajv instance
|
|
62
|
-
* @type {Ajv}
|
|
63
|
-
*/
|
|
64
|
-
this.validator = new Ajv({
|
|
60
|
+
const ajvOptions = {
|
|
65
61
|
addUsedSchema: false,
|
|
66
62
|
allErrors: true,
|
|
67
63
|
allowUnionTypes: true,
|
|
@@ -70,7 +66,19 @@ class Schemas extends EventEmitter {
|
|
|
70
66
|
strict: false,
|
|
71
67
|
verbose: true,
|
|
72
68
|
keywords: Keywords.all()
|
|
73
|
-
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Ajv instance
|
|
73
|
+
* @type {Ajv}
|
|
74
|
+
*/
|
|
75
|
+
this.validator = new Ajv({ ...ajvOptions })
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Ajv instance with useDefaults enabled (applies schema defaults during validation)
|
|
79
|
+
* @type {Ajv}
|
|
80
|
+
*/
|
|
81
|
+
this.validatorWithDefaults = new Ajv({ ...ajvOptions, useDefaults: true })
|
|
74
82
|
|
|
75
83
|
this.addStringFormats({
|
|
76
84
|
'date-time': /[A-Za-z0-9:+()]+/,
|
|
@@ -119,7 +127,9 @@ class Schemas extends EventEmitter {
|
|
|
119
127
|
if (isUnsafe) {
|
|
120
128
|
this.emit('warning', `Unsafe RegExp for format '${name}' (${re}), using default`)
|
|
121
129
|
}
|
|
122
|
-
|
|
130
|
+
const format = isUnsafe ? /.*/ : re
|
|
131
|
+
this.validatorWithDefaults.addFormat(name, format)
|
|
132
|
+
this.validator.addFormat(name, format)
|
|
123
133
|
})
|
|
124
134
|
}
|
|
125
135
|
|
|
@@ -131,13 +141,15 @@ class Schemas extends EventEmitter {
|
|
|
131
141
|
*/
|
|
132
142
|
addKeyword (definition, options = {}) {
|
|
133
143
|
try {
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
|
|
144
|
+
for (const v of [this.validatorWithDefaults, this.validator]) {
|
|
145
|
+
if (v.getKeyword(definition.keyword)) {
|
|
146
|
+
if (options.override !== true) {
|
|
147
|
+
throw new SchemaError('KEYWORD_EXISTS', 'Keyword already exists')
|
|
148
|
+
}
|
|
149
|
+
v.removeKeyword(definition.keyword)
|
|
137
150
|
}
|
|
138
|
-
|
|
151
|
+
v.addKeyword(definition)
|
|
139
152
|
}
|
|
140
|
-
this.validator.addKeyword(definition)
|
|
141
153
|
} catch (e) {
|
|
142
154
|
this.emit('warning', `Failed to define keyword '${definition.keyword}', ${e}`)
|
|
143
155
|
}
|
|
@@ -245,6 +257,7 @@ class Schemas extends EventEmitter {
|
|
|
245
257
|
const schema = new Schema({
|
|
246
258
|
enableCache: options.enableCache ?? this.options.enableCache,
|
|
247
259
|
filePath,
|
|
260
|
+
validatorWithDefaults: this.validatorWithDefaults,
|
|
248
261
|
validator: this.validator,
|
|
249
262
|
xssWhitelist: this.xssWhitelist,
|
|
250
263
|
schemaLibrary: this,
|
|
@@ -316,27 +329,6 @@ class Schemas extends EventEmitter {
|
|
|
316
329
|
return schema.built
|
|
317
330
|
}
|
|
318
331
|
|
|
319
|
-
/**
|
|
320
|
-
* Returns schema defaults as a structured object
|
|
321
|
-
* @param {string} schemaName The schema name
|
|
322
|
-
* @returns {Promise<Object>} The defaults object
|
|
323
|
-
*/
|
|
324
|
-
async getSchemaDefaults (schemaName) {
|
|
325
|
-
const schema = await this.getSchema(schemaName)
|
|
326
|
-
return schema.getObjectDefaults()
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Extracts _globals defaults from the course schema
|
|
331
|
-
* @param {string} schemaName Schema name (defaults to 'course')
|
|
332
|
-
* @returns {Promise<Object>} The _globals defaults
|
|
333
|
-
*/
|
|
334
|
-
async getGlobalsDefaults (schemaName = 'course') {
|
|
335
|
-
const schema = await this.getSchema(schemaName)
|
|
336
|
-
const defaults = schema.getObjectDefaults()
|
|
337
|
-
return defaults._globals || {}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
332
|
/**
|
|
341
333
|
* Returns list of all registered schema names
|
|
342
334
|
* @returns {string[]}
|
package/package.json
CHANGED
package/test.js
CHANGED
|
@@ -119,6 +119,52 @@ async function setupTestSchemas () {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
// Create a config schema with nested defaults (mimics trickle pattern)
|
|
123
|
+
const configSchema = {
|
|
124
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
125
|
+
$anchor: 'config',
|
|
126
|
+
$merge: {
|
|
127
|
+
source: { $ref: 'base' },
|
|
128
|
+
with: {
|
|
129
|
+
properties: {
|
|
130
|
+
_trickle: {
|
|
131
|
+
type: 'object',
|
|
132
|
+
properties: {
|
|
133
|
+
_isEnabled: {
|
|
134
|
+
type: 'boolean',
|
|
135
|
+
default: true
|
|
136
|
+
},
|
|
137
|
+
_scrollDuration: {
|
|
138
|
+
type: 'number',
|
|
139
|
+
default: 500
|
|
140
|
+
},
|
|
141
|
+
_button: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
default: {},
|
|
144
|
+
properties: {
|
|
145
|
+
_isEnabled: {
|
|
146
|
+
type: 'boolean',
|
|
147
|
+
default: true
|
|
148
|
+
},
|
|
149
|
+
_styleBeforeCompletion: {
|
|
150
|
+
type: 'string',
|
|
151
|
+
default: 'hidden'
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
_completionOrder: {
|
|
156
|
+
type: 'array',
|
|
157
|
+
items: { type: 'number' },
|
|
158
|
+
default: [1, 2, 3]
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
required: ['_scrollDuration']
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
122
168
|
// Create a patch schema that extends course
|
|
123
169
|
const coursePatchSchema = {
|
|
124
170
|
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
@@ -158,6 +204,10 @@ async function setupTestSchemas () {
|
|
|
158
204
|
path.join(testSchemaDir, 'component.schema.json'),
|
|
159
205
|
JSON.stringify(componentSchema, null, 2)
|
|
160
206
|
)
|
|
207
|
+
await fs.writeFile(
|
|
208
|
+
path.join(testSchemaDir, 'config.schema.json'),
|
|
209
|
+
JSON.stringify(configSchema, null, 2)
|
|
210
|
+
)
|
|
161
211
|
await fs.writeFile(
|
|
162
212
|
path.join(testSchemaDir, 'course-extension.schema.json'),
|
|
163
213
|
JSON.stringify(coursePatchSchema, null, 2)
|
|
@@ -196,26 +246,16 @@ async function runTests () {
|
|
|
196
246
|
const courseBuilt = await library.getBuiltSchema('course')
|
|
197
247
|
console.log(` ✓ Course schema has properties: ${Object.keys(courseBuilt.properties).join(', ')}\n`)
|
|
198
248
|
|
|
199
|
-
// Test 4:
|
|
200
|
-
console.log('Test 4:
|
|
201
|
-
const courseDefaults = await library.getSchemaDefaults('course')
|
|
202
|
-
console.log(' ✓ Course defaults:', JSON.stringify(courseDefaults, null, 4).split('\n').map(l => ' ' + l).join('\n'), '\n')
|
|
203
|
-
|
|
204
|
-
// Test 5: Get _globals defaults
|
|
205
|
-
console.log('Test 5: Get _globals defaults')
|
|
206
|
-
const globalsDefaults = await library.getGlobalsDefaults('course')
|
|
207
|
-
console.log(' ✓ _globals defaults:', JSON.stringify(globalsDefaults, null, 4).split('\n').map(l => ' ' + l).join('\n'), '\n')
|
|
208
|
-
|
|
209
|
-
// Test 6: Validate data
|
|
210
|
-
console.log('Test 6: Validate data')
|
|
249
|
+
// Test 4: Validate data
|
|
250
|
+
console.log('Test 4: Validate data')
|
|
211
251
|
const validData = await library.validate('course', {
|
|
212
252
|
title: 'My Course'
|
|
213
253
|
})
|
|
214
254
|
console.log(` ✓ Validated data has title: "${validData.title}"`)
|
|
215
255
|
console.log(` ✓ Default description applied: "${validData.description}"\n`)
|
|
216
256
|
|
|
217
|
-
// Test
|
|
218
|
-
console.log('Test
|
|
257
|
+
// Test 5: Validate with error (missing required field without defaults)
|
|
258
|
+
console.log('Test 5: Validation error handling')
|
|
219
259
|
try {
|
|
220
260
|
await library.validate('course', {
|
|
221
261
|
// Missing required title - disable defaults to trigger required error
|
|
@@ -227,8 +267,8 @@ async function runTests () {
|
|
|
227
267
|
console.log(` ✓ Error message: ${e.message}\n`)
|
|
228
268
|
}
|
|
229
269
|
|
|
230
|
-
// Test
|
|
231
|
-
console.log('Test
|
|
270
|
+
// Test 5b: Validate with type error
|
|
271
|
+
console.log('Test 5b: Type validation error')
|
|
232
272
|
try {
|
|
233
273
|
await library.validate('course', {
|
|
234
274
|
title: 12345 // Should be string, not number
|
|
@@ -238,16 +278,16 @@ async function runTests () {
|
|
|
238
278
|
console.log(` ✓ Caught type validation error: ${e.code}\n`)
|
|
239
279
|
}
|
|
240
280
|
|
|
241
|
-
// Test
|
|
242
|
-
console.log('Test
|
|
281
|
+
// Test 6: Schema inheritance
|
|
282
|
+
console.log('Test 6: Schema inheritance')
|
|
243
283
|
const componentBuilt = await library.getBuiltSchema('component')
|
|
244
284
|
const hasInheritedBody = componentBuilt.properties.body !== undefined
|
|
245
285
|
const hasOwnComponent = componentBuilt.properties._component !== undefined
|
|
246
286
|
console.log(` ✓ Component has inherited 'body' property: ${hasInheritedBody}`)
|
|
247
287
|
console.log(` ✓ Component has own '_component' property: ${hasOwnComponent}\n`)
|
|
248
288
|
|
|
249
|
-
// Test
|
|
250
|
-
console.log('Test
|
|
289
|
+
// Test 7: Schema info
|
|
290
|
+
console.log('Test 7: Schema info')
|
|
251
291
|
const info = library.getSchemaInfo()
|
|
252
292
|
console.log(' ✓ Schema info:')
|
|
253
293
|
Object.entries(info).forEach(([name, details]) => {
|
|
@@ -255,8 +295,8 @@ async function runTests () {
|
|
|
255
295
|
})
|
|
256
296
|
console.log('')
|
|
257
297
|
|
|
258
|
-
// Test
|
|
259
|
-
console.log('Test
|
|
298
|
+
// Test 8: Events
|
|
299
|
+
console.log('Test 8: Event handling')
|
|
260
300
|
library.on('schemaRegistered', (name) => {
|
|
261
301
|
console.log(` ✓ Event received: schemaRegistered (${name})`)
|
|
262
302
|
})
|
|
@@ -274,8 +314,8 @@ async function runTests () {
|
|
|
274
314
|
await library.registerSchema(newSchemaPath)
|
|
275
315
|
console.log('')
|
|
276
316
|
|
|
277
|
-
// Test
|
|
278
|
-
console.log('Test
|
|
317
|
+
// Test 9: Schema.walk
|
|
318
|
+
console.log('Test 9: Schema.walk')
|
|
279
319
|
const courseSchema = await library.getSchema('course')
|
|
280
320
|
const walkData = {
|
|
281
321
|
title: 'Test',
|
|
@@ -295,6 +335,84 @@ async function runTests () {
|
|
|
295
335
|
console.log(` ✓ Includes top-level field: ${hasTitle}`)
|
|
296
336
|
console.log(` ✓ Includes nested field: ${hasNested}\n`)
|
|
297
337
|
|
|
338
|
+
// Test 10: Deep defaults on partial nested objects
|
|
339
|
+
console.log('Test 10: Deep defaults on partial nested objects')
|
|
340
|
+
const configData = await library.validate('config', {
|
|
341
|
+
_trickle: { _isEnabled: false }
|
|
342
|
+
})
|
|
343
|
+
const hasScrollDuration = configData._trickle._scrollDuration === 500
|
|
344
|
+
const hasButtonDefaults = configData._trickle._button?._styleBeforeCompletion === 'hidden'
|
|
345
|
+
const preservedExplicit = configData._trickle._isEnabled === false
|
|
346
|
+
console.log(` ✓ Nested default _scrollDuration applied: ${hasScrollDuration}`)
|
|
347
|
+
console.log(` ✓ Deep nested _button defaults applied: ${hasButtonDefaults}`)
|
|
348
|
+
console.log(` ✓ Explicitly set _isEnabled preserved: ${preservedExplicit}`)
|
|
349
|
+
if (!hasScrollDuration || !hasButtonDefaults || !preservedExplicit) {
|
|
350
|
+
throw new Error('Deep defaults not applied correctly to partial nested objects')
|
|
351
|
+
}
|
|
352
|
+
console.log('')
|
|
353
|
+
|
|
354
|
+
// Test 11: Array defaults replaced, not merged
|
|
355
|
+
console.log('Test 11: Array defaults are replaced, not merged')
|
|
356
|
+
const arrayData = await library.validate('config', {
|
|
357
|
+
_trickle: { _completionOrder: [10, 20] }
|
|
358
|
+
})
|
|
359
|
+
const arrayNotMerged = JSON.stringify(arrayData._trickle._completionOrder) === '[10,20]'
|
|
360
|
+
console.log(` ✓ Array preserved as [10,20], not merged with default: ${arrayNotMerged}`)
|
|
361
|
+
if (!arrayNotMerged) {
|
|
362
|
+
throw new Error(`Array was merged with defaults: got ${JSON.stringify(arrayData._trickle._completionOrder)}, expected [10,20]`)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const arrayDefaultApplied = await library.validate('config', {
|
|
366
|
+
_trickle: { _isEnabled: false }
|
|
367
|
+
})
|
|
368
|
+
const defaultArrayApplied = JSON.stringify(arrayDefaultApplied._trickle._completionOrder) === '[1,2,3]'
|
|
369
|
+
console.log(` ✓ Missing array gets default [1,2,3]: ${defaultArrayApplied}`)
|
|
370
|
+
if (!defaultArrayApplied) {
|
|
371
|
+
throw new Error(`Default array not applied: got ${JSON.stringify(arrayDefaultApplied._trickle._completionOrder)}`)
|
|
372
|
+
}
|
|
373
|
+
console.log('')
|
|
374
|
+
|
|
375
|
+
// Test 12: useDefaults: false produces no defaults
|
|
376
|
+
console.log('Test 12: useDefaults: false produces no defaults')
|
|
377
|
+
const noDefaultsData = await library.validate('content', {
|
|
378
|
+
body: 'Hello'
|
|
379
|
+
}, { useDefaults: false, ignoreRequired: true })
|
|
380
|
+
const noIsOptional = noDefaultsData._isOptional === undefined
|
|
381
|
+
console.log(` ✓ _isOptional not filled in: ${noIsOptional}`)
|
|
382
|
+
if (!noIsOptional) {
|
|
383
|
+
throw new Error(`useDefaults: false still applied defaults: _isOptional = ${noDefaultsData._isOptional}`)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const withDefaultsData = await library.validate('content', {
|
|
387
|
+
body: 'Hello'
|
|
388
|
+
}, { useDefaults: true, ignoreRequired: true })
|
|
389
|
+
const hasIsOptional = withDefaultsData._isOptional === false
|
|
390
|
+
console.log(` ✓ _isOptional filled in with useDefaults: true: ${hasIsOptional}`)
|
|
391
|
+
if (!hasIsOptional) {
|
|
392
|
+
throw new Error(`useDefaults: true did not apply defaults: _isOptional = ${withDefaultsData._isOptional}`)
|
|
393
|
+
}
|
|
394
|
+
console.log('')
|
|
395
|
+
|
|
396
|
+
// Test 13: Partial nested object with required+default field (issue #21)
|
|
397
|
+
console.log('Test 13: Required+default field in partial nested object (issue #21)')
|
|
398
|
+
try {
|
|
399
|
+
const issueData = await library.validate('config', {
|
|
400
|
+
_trickle: { _isEnabled: false }
|
|
401
|
+
})
|
|
402
|
+
const hasRequired = issueData._trickle._scrollDuration === 500
|
|
403
|
+
console.log(' ✓ Validation passed (did not throw): true')
|
|
404
|
+
console.log(` ✓ Required+default _scrollDuration applied: ${hasRequired}`)
|
|
405
|
+
if (!hasRequired) {
|
|
406
|
+
throw new Error('Required+default field _scrollDuration was not applied')
|
|
407
|
+
}
|
|
408
|
+
} catch (e) {
|
|
409
|
+
if (e.code === 'VALIDATION_FAILED') {
|
|
410
|
+
throw new Error(`Issue #21 regression: validation failed on partial nested object: ${e.data?.errors || e.message}`)
|
|
411
|
+
}
|
|
412
|
+
throw e
|
|
413
|
+
}
|
|
414
|
+
console.log('')
|
|
415
|
+
|
|
298
416
|
console.log('=== All tests passed! ===')
|
|
299
417
|
} finally {
|
|
300
418
|
if (!hasSpecifiedPath) {
|