@squiz/dx-json-schema-lib 1.82.2 → 1.82.4
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/CHANGELOG.md +12 -0
- package/README.md +132 -0
- package/lib/JsonSchemaService.allOf.spec.d.ts +1 -0
- package/lib/JsonSchemaService.allOf.spec.js +528 -0
- package/lib/JsonSchemaService.allOf.spec.js.map +1 -0
- package/lib/JsonSchemaService.d.ts +24 -0
- package/lib/JsonSchemaService.js +211 -2
- package/lib/JsonSchemaService.js.map +1 -1
- package/lib/hasAllOfCombinator.spec.d.ts +1 -0
- package/lib/hasAllOfCombinator.spec.js +394 -0
- package/lib/hasAllOfCombinator.spec.js.map +1 -0
- package/package.json +4 -2
- package/src/JsonSchemaService.allOf.spec.ts +573 -0
- package/src/JsonSchemaService.ts +231 -2
- package/src/hasAllOfCombinator.spec.ts +456 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/JsonSchemaService.ts
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import JSONQuery, { Input } from '@sagold/json-query';
|
2
|
+
import cloneDeep from 'lodash.clonedeep';
|
2
3
|
|
3
4
|
import DxComponentInputSchema from './manifest/v1/DxComponentInputSchema.json';
|
4
5
|
import DxComponentIcons from './manifest/v1/DxComponentIcons.json';
|
@@ -72,7 +73,37 @@ export class JSONSchemaService<P extends AnyPrimitiveType, R extends AnyResolvab
|
|
72
73
|
schema = core.resolveRef(schema);
|
73
74
|
callback(schema, data, pointer);
|
74
75
|
} else {
|
75
|
-
|
76
|
+
// CRITICAL FIX: Preserve data integrity during allOf schema traversal
|
77
|
+
// Only apply protection for allOf schemas that contain arrays with primitive types
|
78
|
+
if (schema?.allOf && Array.isArray(schema.allOf) && this.hasArraysWithPrimitiveTypes(schema)) {
|
79
|
+
// Get the original input data stored at the beginning of resolveInput
|
80
|
+
const originalInputData = (this as any).__originalInputData;
|
81
|
+
|
82
|
+
// Create a deep clone to prevent Draft library from corrupting original data
|
83
|
+
const safeDataForTraversal = JSON.parse(JSON.stringify(data));
|
84
|
+
|
85
|
+
// Use the safe clone for traversal, but preserve callbacks with original data
|
86
|
+
defaultConfig.each(
|
87
|
+
core,
|
88
|
+
safeDataForTraversal,
|
89
|
+
(resolvedSchema, corruptedValue, resolvedPointer) => {
|
90
|
+
// Only preserve data for paths that contain arrays of SquizLink/SquizImage
|
91
|
+
if (this.isProtectedPrimitivePath(resolvedSchema, resolvedPointer)) {
|
92
|
+
const originalValueAtPointer = originalInputData
|
93
|
+
? this.getValueAtPointer(originalInputData, resolvedPointer)
|
94
|
+
: corruptedValue;
|
95
|
+
callback(resolvedSchema, originalValueAtPointer, resolvedPointer);
|
96
|
+
} else {
|
97
|
+
// Use normal resolution for non-primitive arrays
|
98
|
+
callback(resolvedSchema, corruptedValue, resolvedPointer);
|
99
|
+
}
|
100
|
+
},
|
101
|
+
schema,
|
102
|
+
pointer,
|
103
|
+
);
|
104
|
+
} else {
|
105
|
+
defaultConfig.each(core, data, callback, schema, pointer);
|
106
|
+
}
|
76
107
|
}
|
77
108
|
},
|
78
109
|
},
|
@@ -103,6 +134,42 @@ export class JSONSchemaService<P extends AnyPrimitiveType, R extends AnyResolvab
|
|
103
134
|
return this.schema.compileSchema(fullValidationSchema);
|
104
135
|
}
|
105
136
|
|
137
|
+
/**
|
138
|
+
* Recursively check if a schema contains allOf combinators that could cause mutation
|
139
|
+
*/
|
140
|
+
private hasAllOfCombinator(schema: any, visited: WeakSet<object> = new WeakSet()): boolean {
|
141
|
+
if (!schema || typeof schema !== 'object') return false;
|
142
|
+
|
143
|
+
// Prevent infinite recursion from circular references
|
144
|
+
if (visited.has(schema)) return false;
|
145
|
+
visited.add(schema);
|
146
|
+
|
147
|
+
// Direct allOf check
|
148
|
+
if (schema.allOf) return true;
|
149
|
+
|
150
|
+
// Check in properties
|
151
|
+
if (schema.properties) {
|
152
|
+
for (const prop of Object.values(schema.properties)) {
|
153
|
+
if (this.hasAllOfCombinator(prop, visited)) return true;
|
154
|
+
}
|
155
|
+
}
|
156
|
+
|
157
|
+
// Check in items (arrays)
|
158
|
+
if (schema.items && this.hasAllOfCombinator(schema.items, visited)) return true;
|
159
|
+
|
160
|
+
// Check in nested combinators
|
161
|
+
if (schema.oneOf?.some((subSchema: any) => this.hasAllOfCombinator(subSchema, visited))) return true;
|
162
|
+
if (schema.anyOf?.some((subSchema: any) => this.hasAllOfCombinator(subSchema, visited))) return true;
|
163
|
+
if (schema.not && this.hasAllOfCombinator(schema.not, visited)) return true;
|
164
|
+
|
165
|
+
// Check in conditional schemas (if/then/else)
|
166
|
+
if (schema.if && this.hasAllOfCombinator(schema.if, visited)) return true;
|
167
|
+
if (schema.then && this.hasAllOfCombinator(schema.then, visited)) return true;
|
168
|
+
if (schema.else && this.hasAllOfCombinator(schema.else, visited)) return true;
|
169
|
+
|
170
|
+
return false;
|
171
|
+
}
|
172
|
+
|
106
173
|
/**
|
107
174
|
* Validate an input value against a specified schema
|
108
175
|
* @throws {SchemaValidationError} if the input is invalid
|
@@ -110,7 +177,13 @@ export class JSONSchemaService<P extends AnyPrimitiveType, R extends AnyResolvab
|
|
110
177
|
*/
|
111
178
|
public validateInput(input: unknown, inputSchema: JSONSchema = this.schema.rootSchema): true | never {
|
112
179
|
inputSchema = this.schema.compileSchema(inputSchema);
|
113
|
-
|
180
|
+
|
181
|
+
// Only clone if schema contains allOf combinators that could cause mutation
|
182
|
+
// This optimizes performance by avoiding unnecessary cloning for simple schemas
|
183
|
+
const needsCloning = this.hasAllOfCombinator(inputSchema);
|
184
|
+
const inputToValidate = needsCloning ? cloneDeep(input) : input;
|
185
|
+
|
186
|
+
const errors = this.schema.validate(inputToValidate, inputSchema);
|
114
187
|
return processValidationResult(errors);
|
115
188
|
}
|
116
189
|
|
@@ -121,17 +194,50 @@ export class JSONSchemaService<P extends AnyPrimitiveType, R extends AnyResolvab
|
|
121
194
|
* @returns the input object with all resolvable shapes resolved
|
122
195
|
*/
|
123
196
|
public async resolveInput(input: Input, inputSchema: JSONSchema, ctx: ResolverContext = {}) {
|
197
|
+
// Store the original input data to prevent corruption during nested allOf processing
|
198
|
+
(this as any).__originalInputData = JSON.parse(JSON.stringify(input));
|
199
|
+
|
124
200
|
const setters: Array<Promise<(input: Input) => Input>> = [];
|
125
201
|
this.schema.each(
|
126
202
|
input,
|
127
203
|
async (schema, value, pointer) => {
|
204
|
+
// Debug logging for investigation of resolution path
|
205
|
+
// console.debug('JSONSchemaService.each', {
|
206
|
+
// pointer,
|
207
|
+
// title: (schema as any)?.title,
|
208
|
+
// type: (schema as any)?.type,
|
209
|
+
// value,
|
210
|
+
// });
|
211
|
+
// Guard: if value is an array of already-resolved primitives (e.g., SquizLink/SquizImage), skip
|
212
|
+
if (Array.isArray(value)) {
|
213
|
+
const isLinkLike = (v: any) =>
|
214
|
+
v && typeof v === 'object' && typeof v.text === 'string' && typeof v.url === 'string';
|
215
|
+
const isImageLike = (v: any) =>
|
216
|
+
v &&
|
217
|
+
typeof v === 'object' &&
|
218
|
+
typeof v.name === 'string' &&
|
219
|
+
v.imageVariations &&
|
220
|
+
typeof v.imageVariations === 'object';
|
221
|
+
const allResolvedPrimitives = value.every((v) => isLinkLike(v) || isImageLike(v));
|
222
|
+
if (allResolvedPrimitives) {
|
223
|
+
return;
|
224
|
+
}
|
225
|
+
}
|
128
226
|
// Bug in library for Array item schemas which won't resolve the oneOf schema
|
129
227
|
if (Array.isArray(schema?.oneOf)) {
|
130
228
|
const oldSchema = schema;
|
131
229
|
schema = this.schema.resolveOneOf(value, schema);
|
132
230
|
schema.oneOfSchema = oldSchema;
|
133
231
|
}
|
232
|
+
|
233
|
+
// Skip resolution if this is already a complete SquizLink or SquizImage
|
234
|
+
// This prevents primitive types from being incorrectly processed when they're already in final form
|
235
|
+
if (this.isAlreadyResolvedPrimitive(schema, value)) {
|
236
|
+
return;
|
237
|
+
}
|
238
|
+
|
134
239
|
if (!this.typeResolver.isResolvableSchema(schema)) return;
|
240
|
+
|
135
241
|
// If its a resolvable schema, it should exist in a oneOf array with other schemas
|
136
242
|
// Including a primitive schema
|
137
243
|
const allPossibleSchemaTitles: Array<string> = schema.oneOfSchema.oneOf.map((o: JSONSchema) =>
|
@@ -140,6 +246,7 @@ export class JSONSchemaService<P extends AnyPrimitiveType, R extends AnyResolvab
|
|
140
246
|
|
141
247
|
const primitiveSchema = allPossibleSchemaTitles.find((title) => this.typeResolver.isPrimitiveType(title));
|
142
248
|
if (!primitiveSchema) return;
|
249
|
+
|
143
250
|
const resolver = this.typeResolver.tryGetResolver(primitiveSchema, schema as any);
|
144
251
|
if (!resolver) return;
|
145
252
|
const setResolvedData = Promise.resolve()
|
@@ -163,6 +270,128 @@ export class JSONSchemaService<P extends AnyPrimitiveType, R extends AnyResolvab
|
|
163
270
|
if (potentialResolutionErrors.length) {
|
164
271
|
throw new Error(`Error(s) occurred when resolving JSON:\n${potentialResolutionErrors.join('\n')}`);
|
165
272
|
}
|
273
|
+
|
274
|
+
// Clean up stored original data to prevent memory leaks
|
275
|
+
delete (this as any).__originalInputData;
|
276
|
+
|
166
277
|
return input;
|
167
278
|
}
|
279
|
+
|
280
|
+
/**
|
281
|
+
* Check if the allOf schema contains arrays with primitive types (SquizLink/SquizImage)
|
282
|
+
* Used to determine if we need to apply data preservation
|
283
|
+
*/
|
284
|
+
private hasArraysWithPrimitiveTypes(schema: any): boolean {
|
285
|
+
if (!schema?.allOf || !Array.isArray(schema.allOf)) return false;
|
286
|
+
|
287
|
+
const checkForPrimitiveArrays = (obj: any): boolean => {
|
288
|
+
if (!obj || typeof obj !== 'object') return false;
|
289
|
+
|
290
|
+
// Check if this is an array with SquizLink/SquizImage items
|
291
|
+
if (obj.type === 'array' && obj.items?.type) {
|
292
|
+
return obj.items.type === 'SquizLink' || obj.items.type === 'SquizImage';
|
293
|
+
}
|
294
|
+
|
295
|
+
// Recursively check properties
|
296
|
+
if (obj.properties) {
|
297
|
+
return Object.values(obj.properties).some((prop: any) => checkForPrimitiveArrays(prop));
|
298
|
+
}
|
299
|
+
|
300
|
+
// Check in then/else branches and nested allOf
|
301
|
+
if (obj.then && checkForPrimitiveArrays(obj.then)) return true;
|
302
|
+
if (obj.else && checkForPrimitiveArrays(obj.else)) return true;
|
303
|
+
if (obj.allOf && Array.isArray(obj.allOf)) {
|
304
|
+
return obj.allOf.some((item: any) => checkForPrimitiveArrays(item));
|
305
|
+
}
|
306
|
+
|
307
|
+
return false;
|
308
|
+
};
|
309
|
+
|
310
|
+
// Check both inside allOf conditions AND in the main schema properties
|
311
|
+
// because primitive arrays in main properties can be affected by allOf processing
|
312
|
+
const hasInAllOf = schema.allOf.some((condition: any) => checkForPrimitiveArrays(condition));
|
313
|
+
const hasInMainSchema = checkForPrimitiveArrays(schema);
|
314
|
+
|
315
|
+
return hasInAllOf || hasInMainSchema;
|
316
|
+
}
|
317
|
+
|
318
|
+
/**
|
319
|
+
* Check if a specific schema path should be protected from data corruption
|
320
|
+
* Only protects arrays of SquizLink/SquizImage types
|
321
|
+
*/
|
322
|
+
private isProtectedPrimitivePath(resolvedSchema: any, _resolvedPointer: string): boolean {
|
323
|
+
if (!resolvedSchema) return false;
|
324
|
+
|
325
|
+
// Check if this is an array of SquizLink/SquizImage
|
326
|
+
if (resolvedSchema.type === 'array' && resolvedSchema.items?.type) {
|
327
|
+
return resolvedSchema.items.type === 'SquizLink' || resolvedSchema.items.type === 'SquizImage';
|
328
|
+
}
|
329
|
+
|
330
|
+
// Check if this is a direct SquizLink/SquizImage type
|
331
|
+
if (resolvedSchema.type === 'SquizLink' || resolvedSchema.type === 'SquizImage') {
|
332
|
+
return true;
|
333
|
+
}
|
334
|
+
|
335
|
+
return false;
|
336
|
+
}
|
337
|
+
|
338
|
+
/**
|
339
|
+
* Get value at a specific JSON pointer path from the data structure
|
340
|
+
* Used to preserve original data during schema traversal
|
341
|
+
*/
|
342
|
+
private getValueAtPointer(data: any, pointer: string): any {
|
343
|
+
if (pointer === '#') return data;
|
344
|
+
|
345
|
+
// Remove the '#/' prefix and split by '/'
|
346
|
+
const path = pointer.replace(/^#\//, '').split('/');
|
347
|
+
let current = data;
|
348
|
+
|
349
|
+
for (const segment of path) {
|
350
|
+
if (current === null || current === undefined) return undefined;
|
351
|
+
|
352
|
+
// Handle array indices
|
353
|
+
if (Array.isArray(current)) {
|
354
|
+
const index = parseInt(segment, 10);
|
355
|
+
if (isNaN(index) || index < 0 || index >= current.length) return undefined;
|
356
|
+
current = current[index];
|
357
|
+
} else if (typeof current === 'object') {
|
358
|
+
// Handle object properties
|
359
|
+
current = current[segment];
|
360
|
+
} else {
|
361
|
+
return undefined;
|
362
|
+
}
|
363
|
+
}
|
364
|
+
|
365
|
+
return current;
|
366
|
+
}
|
367
|
+
|
368
|
+
/**
|
369
|
+
* Check if the value is already a resolved primitive type (SquizLink, SquizImage)
|
370
|
+
* and doesn't need further resolution
|
371
|
+
*/
|
372
|
+
private isAlreadyResolvedPrimitive(schema: JSONSchema, value: unknown): boolean {
|
373
|
+
if (!value || typeof value !== 'object') return false;
|
374
|
+
|
375
|
+
// Schema hints: check either title or type when present
|
376
|
+
const schemaType = (schema as any)?.type ?? (schema as any)?.title;
|
377
|
+
|
378
|
+
// Shape-based detection for SquizLink
|
379
|
+
const v: any = value;
|
380
|
+
const looksLikeSquizLink = typeof v.text === 'string' && typeof v.url === 'string';
|
381
|
+
if (looksLikeSquizLink) {
|
382
|
+
if (schemaType === 'SquizLink' || schemaType === undefined) return true;
|
383
|
+
// Even if schema hint differs (e.g., through allOf), preserve already-formed link objects
|
384
|
+
return true;
|
385
|
+
}
|
386
|
+
|
387
|
+
// Shape-based detection for SquizImage
|
388
|
+
const looksLikeSquizImage =
|
389
|
+
typeof v.name === 'string' && v.imageVariations && typeof v.imageVariations === 'object';
|
390
|
+
if (looksLikeSquizImage) {
|
391
|
+
if (schemaType === 'SquizImage' || schemaType === undefined) return true;
|
392
|
+
return true;
|
393
|
+
}
|
394
|
+
|
395
|
+
return false;
|
396
|
+
}
|
168
397
|
}
|