@squiz/dx-json-schema-lib 1.82.1 → 1.82.3
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 -2
- package/lib/JsonSchemaService.d.ts +4 -0
- package/lib/JsonSchemaService.js +46 -1
- package/lib/JsonSchemaService.js.map +1 -1
- package/lib/formatted-text/v1/formattedText.d.ts +2 -2
- package/lib/formatted-text/v1/formattedText.json +2 -2
- 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 +3 -1
- package/src/JsonSchemaService.ts +44 -1
- package/src/formatted-text/v1/formattedText.json +2 -2
- package/src/formatted-text/v1/formattedText.ts +2 -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';
|
@@ -103,6 +104,42 @@ export class JSONSchemaService<P extends AnyPrimitiveType, R extends AnyResolvab
|
|
103
104
|
return this.schema.compileSchema(fullValidationSchema);
|
104
105
|
}
|
105
106
|
|
107
|
+
/**
|
108
|
+
* Recursively check if a schema contains allOf combinators that could cause mutation
|
109
|
+
*/
|
110
|
+
private hasAllOfCombinator(schema: any, visited: WeakSet<object> = new WeakSet()): boolean {
|
111
|
+
if (!schema || typeof schema !== 'object') return false;
|
112
|
+
|
113
|
+
// Prevent infinite recursion from circular references
|
114
|
+
if (visited.has(schema)) return false;
|
115
|
+
visited.add(schema);
|
116
|
+
|
117
|
+
// Direct allOf check
|
118
|
+
if (schema.allOf) return true;
|
119
|
+
|
120
|
+
// Check in properties
|
121
|
+
if (schema.properties) {
|
122
|
+
for (const prop of Object.values(schema.properties)) {
|
123
|
+
if (this.hasAllOfCombinator(prop, visited)) return true;
|
124
|
+
}
|
125
|
+
}
|
126
|
+
|
127
|
+
// Check in items (arrays)
|
128
|
+
if (schema.items && this.hasAllOfCombinator(schema.items, visited)) return true;
|
129
|
+
|
130
|
+
// Check in nested combinators
|
131
|
+
if (schema.oneOf?.some((subSchema: any) => this.hasAllOfCombinator(subSchema, visited))) return true;
|
132
|
+
if (schema.anyOf?.some((subSchema: any) => this.hasAllOfCombinator(subSchema, visited))) return true;
|
133
|
+
if (schema.not && this.hasAllOfCombinator(schema.not, visited)) return true;
|
134
|
+
|
135
|
+
// Check in conditional schemas (if/then/else)
|
136
|
+
if (schema.if && this.hasAllOfCombinator(schema.if, visited)) return true;
|
137
|
+
if (schema.then && this.hasAllOfCombinator(schema.then, visited)) return true;
|
138
|
+
if (schema.else && this.hasAllOfCombinator(schema.else, visited)) return true;
|
139
|
+
|
140
|
+
return false;
|
141
|
+
}
|
142
|
+
|
106
143
|
/**
|
107
144
|
* Validate an input value against a specified schema
|
108
145
|
* @throws {SchemaValidationError} if the input is invalid
|
@@ -110,7 +147,13 @@ export class JSONSchemaService<P extends AnyPrimitiveType, R extends AnyResolvab
|
|
110
147
|
*/
|
111
148
|
public validateInput(input: unknown, inputSchema: JSONSchema = this.schema.rootSchema): true | never {
|
112
149
|
inputSchema = this.schema.compileSchema(inputSchema);
|
113
|
-
|
150
|
+
|
151
|
+
// Only clone if schema contains allOf combinators that could cause mutation
|
152
|
+
// This optimizes performance by avoiding unnecessary cloning for simple schemas
|
153
|
+
const needsCloning = this.hasAllOfCombinator(inputSchema);
|
154
|
+
const inputToValidate = needsCloning ? cloneDeep(input) : input;
|
155
|
+
|
156
|
+
const errors = this.schema.validate(inputToValidate, inputSchema);
|
114
157
|
return processValidationResult(errors);
|
115
158
|
}
|
116
159
|
|
@@ -243,7 +243,7 @@
|
|
243
243
|
},
|
244
244
|
"formattingOptions": { "$ref": "#/definitions/FormattingOptions" },
|
245
245
|
"type": { "const": "link-to-dam-asset" },
|
246
|
-
"damSystemType": { "
|
246
|
+
"damSystemType": { "type": "string", "minLength": 1 },
|
247
247
|
"damSystemIdentifier": { "type": "string", "minLength": 1 },
|
248
248
|
"damObjectId": { "type": "string", "minLength": 1 },
|
249
249
|
"damAdditional": { "type": "object", "properties": { "variant": { "type": "string", "minLength": 1 } } },
|
@@ -309,7 +309,7 @@
|
|
309
309
|
|
310
310
|
"properties": {
|
311
311
|
"type": { "const": "dam-image" },
|
312
|
-
"damSystemType": { "
|
312
|
+
"damSystemType": { "type": "string", "minLength": 1 },
|
313
313
|
"damSystemIdentifier": { "type": "string", "minLength": 1 },
|
314
314
|
"damObjectId": { "type": "string", "minLength": 1 },
|
315
315
|
"damAdditional": { "type": "object", "properties": { "variant": { "type": "string", "minLength": 1 } } },
|
@@ -40,7 +40,7 @@ export interface FormattedTextLinkToDamAsset {
|
|
40
40
|
children: WithChildrenNode;
|
41
41
|
formattingOptions?: FormattingOptions;
|
42
42
|
type: 'link-to-dam-asset';
|
43
|
-
damSystemType:
|
43
|
+
damSystemType: string;
|
44
44
|
damSystemIdentifier: string;
|
45
45
|
damObjectId: string;
|
46
46
|
damAdditional?: {
|
@@ -72,7 +72,7 @@ export interface FormattedTextMatrixImage {
|
|
72
72
|
}
|
73
73
|
export interface FormattedTextDamImage {
|
74
74
|
type: 'dam-image';
|
75
|
-
damSystemType:
|
75
|
+
damSystemType: string;
|
76
76
|
damSystemIdentifier: string;
|
77
77
|
damObjectId: string;
|
78
78
|
damAdditional?: {
|
@@ -0,0 +1,456 @@
|
|
1
|
+
import { JSONSchemaService } from './JsonSchemaService';
|
2
|
+
import { ComponentInputMetaSchema } from './JsonSchemaService';
|
3
|
+
import { TypeResolverBuilder } from './jsonTypeResolution/TypeResolverBuilder';
|
4
|
+
|
5
|
+
describe('JSONSchemaService - hasAllOfCombinator', () => {
|
6
|
+
let jsonSchemaService: JSONSchemaService<any, any>;
|
7
|
+
|
8
|
+
beforeAll(() => {
|
9
|
+
const typeResolverBuilder = new TypeResolverBuilder();
|
10
|
+
const typeResolver = typeResolverBuilder.build();
|
11
|
+
jsonSchemaService = new JSONSchemaService(typeResolver, ComponentInputMetaSchema);
|
12
|
+
});
|
13
|
+
|
14
|
+
// Helper to access private method
|
15
|
+
const hasAllOfCombinator = (schema: any) => (jsonSchemaService as any).hasAllOfCombinator(schema);
|
16
|
+
|
17
|
+
describe('Direct allOf detection', () => {
|
18
|
+
it('should detect direct allOf usage', () => {
|
19
|
+
const schema = {
|
20
|
+
type: 'object',
|
21
|
+
allOf: [{ properties: { name: { type: 'string' } } }, { properties: { age: { type: 'number' } } }],
|
22
|
+
};
|
23
|
+
|
24
|
+
expect(hasAllOfCombinator(schema)).toBe(true);
|
25
|
+
});
|
26
|
+
|
27
|
+
it('should detect empty allOf array', () => {
|
28
|
+
const schema = {
|
29
|
+
type: 'object',
|
30
|
+
allOf: [],
|
31
|
+
};
|
32
|
+
|
33
|
+
expect(hasAllOfCombinator(schema)).toBe(true);
|
34
|
+
});
|
35
|
+
});
|
36
|
+
|
37
|
+
describe('Nested allOf detection', () => {
|
38
|
+
it('should detect allOf in properties', () => {
|
39
|
+
const schema = {
|
40
|
+
type: 'object',
|
41
|
+
properties: {
|
42
|
+
user: {
|
43
|
+
allOf: [{ properties: { name: { type: 'string' } } }, { properties: { email: { type: 'string' } } }],
|
44
|
+
},
|
45
|
+
},
|
46
|
+
};
|
47
|
+
|
48
|
+
expect(hasAllOfCombinator(schema)).toBe(true);
|
49
|
+
});
|
50
|
+
|
51
|
+
it('should detect allOf in deeply nested properties', () => {
|
52
|
+
const schema = {
|
53
|
+
type: 'object',
|
54
|
+
properties: {
|
55
|
+
componentContent: {
|
56
|
+
type: 'object',
|
57
|
+
properties: {
|
58
|
+
heroSection: {
|
59
|
+
allOf: [
|
60
|
+
{ properties: { title: { type: 'string' } } },
|
61
|
+
{ properties: { subtitle: { type: 'string' } } },
|
62
|
+
],
|
63
|
+
},
|
64
|
+
},
|
65
|
+
},
|
66
|
+
},
|
67
|
+
};
|
68
|
+
|
69
|
+
expect(hasAllOfCombinator(schema)).toBe(true);
|
70
|
+
});
|
71
|
+
|
72
|
+
it('should detect allOf in array items', () => {
|
73
|
+
const schema = {
|
74
|
+
type: 'object',
|
75
|
+
properties: {
|
76
|
+
items: {
|
77
|
+
type: 'array',
|
78
|
+
items: {
|
79
|
+
allOf: [{ properties: { id: { type: 'number' } } }, { properties: { name: { type: 'string' } } }],
|
80
|
+
},
|
81
|
+
},
|
82
|
+
},
|
83
|
+
};
|
84
|
+
|
85
|
+
expect(hasAllOfCombinator(schema)).toBe(true);
|
86
|
+
});
|
87
|
+
});
|
88
|
+
|
89
|
+
describe('Combinator traversal', () => {
|
90
|
+
it('should detect allOf within oneOf', () => {
|
91
|
+
const schema = {
|
92
|
+
type: 'object',
|
93
|
+
oneOf: [
|
94
|
+
{
|
95
|
+
allOf: [{ properties: { type: { const: 'A' } } }, { properties: { valueA: { type: 'string' } } }],
|
96
|
+
},
|
97
|
+
{ properties: { type: { const: 'B' } } },
|
98
|
+
],
|
99
|
+
};
|
100
|
+
|
101
|
+
expect(hasAllOfCombinator(schema)).toBe(true);
|
102
|
+
});
|
103
|
+
|
104
|
+
it('should detect allOf within anyOf', () => {
|
105
|
+
const schema = {
|
106
|
+
type: 'object',
|
107
|
+
anyOf: [
|
108
|
+
{ properties: { simple: { type: 'string' } } },
|
109
|
+
{
|
110
|
+
allOf: [{ properties: { complex: { type: 'object' } } }, { properties: { nested: { type: 'array' } } }],
|
111
|
+
},
|
112
|
+
],
|
113
|
+
};
|
114
|
+
|
115
|
+
expect(hasAllOfCombinator(schema)).toBe(true);
|
116
|
+
});
|
117
|
+
|
118
|
+
it('should detect allOf within not schema', () => {
|
119
|
+
const schema = {
|
120
|
+
type: 'object',
|
121
|
+
not: {
|
122
|
+
allOf: [{ properties: { forbidden: { type: 'string' } } }, { properties: { invalid: { type: 'number' } } }],
|
123
|
+
},
|
124
|
+
};
|
125
|
+
|
126
|
+
expect(hasAllOfCombinator(schema)).toBe(true);
|
127
|
+
});
|
128
|
+
});
|
129
|
+
|
130
|
+
describe('Conditional schema detection', () => {
|
131
|
+
it('should detect allOf in if condition', () => {
|
132
|
+
const schema = {
|
133
|
+
type: 'object',
|
134
|
+
if: {
|
135
|
+
allOf: [{ properties: { type: { const: 'special' } } }, { properties: { mode: { const: 'advanced' } } }],
|
136
|
+
},
|
137
|
+
then: { properties: { specialValue: { type: 'string' } } },
|
138
|
+
};
|
139
|
+
|
140
|
+
expect(hasAllOfCombinator(schema)).toBe(true);
|
141
|
+
});
|
142
|
+
|
143
|
+
it('should detect allOf in then branch', () => {
|
144
|
+
const schema = {
|
145
|
+
type: 'object',
|
146
|
+
if: { properties: { type: { const: 'special' } } },
|
147
|
+
then: {
|
148
|
+
allOf: [{ properties: { requiredProp: { type: 'string' } } }, { required: ['requiredProp'] }],
|
149
|
+
},
|
150
|
+
};
|
151
|
+
|
152
|
+
expect(hasAllOfCombinator(schema)).toBe(true);
|
153
|
+
});
|
154
|
+
|
155
|
+
it('should detect allOf in else branch', () => {
|
156
|
+
const schema = {
|
157
|
+
type: 'object',
|
158
|
+
if: { properties: { type: { const: 'simple' } } },
|
159
|
+
then: { properties: { simpleValue: { type: 'string' } } },
|
160
|
+
else: {
|
161
|
+
allOf: [
|
162
|
+
{ properties: { complexValue: { type: 'object' } } },
|
163
|
+
{ properties: { metadata: { type: 'object' } } },
|
164
|
+
],
|
165
|
+
},
|
166
|
+
};
|
167
|
+
|
168
|
+
expect(hasAllOfCombinator(schema)).toBe(true);
|
169
|
+
});
|
170
|
+
});
|
171
|
+
|
172
|
+
describe('Complex nested scenarios', () => {
|
173
|
+
it('should detect allOf in Hero Banner-like schema', () => {
|
174
|
+
const heroBannerSchema = {
|
175
|
+
type: 'object',
|
176
|
+
properties: {
|
177
|
+
componentContent: {
|
178
|
+
allOf: [
|
179
|
+
{
|
180
|
+
if: { properties: { heroType: { const: 'Supporting content' } } },
|
181
|
+
then: {
|
182
|
+
type: 'object',
|
183
|
+
properties: {
|
184
|
+
heroType: { type: 'string' },
|
185
|
+
heading: { type: 'string' },
|
186
|
+
content: { type: 'string' },
|
187
|
+
links: {
|
188
|
+
type: 'array',
|
189
|
+
items: {
|
190
|
+
type: 'object',
|
191
|
+
properties: {
|
192
|
+
text: { type: 'string' },
|
193
|
+
url: { type: 'string' },
|
194
|
+
target: { type: 'string' },
|
195
|
+
},
|
196
|
+
},
|
197
|
+
},
|
198
|
+
},
|
199
|
+
},
|
200
|
+
},
|
201
|
+
{
|
202
|
+
if: { properties: { heroType: { const: 'Supporting image' } } },
|
203
|
+
then: {
|
204
|
+
properties: {
|
205
|
+
image: { type: 'object' },
|
206
|
+
},
|
207
|
+
},
|
208
|
+
},
|
209
|
+
],
|
210
|
+
},
|
211
|
+
},
|
212
|
+
};
|
213
|
+
|
214
|
+
expect(hasAllOfCombinator(heroBannerSchema)).toBe(true);
|
215
|
+
});
|
216
|
+
|
217
|
+
it('should detect allOf in very deeply nested structure', () => {
|
218
|
+
const deepSchema = {
|
219
|
+
type: 'object',
|
220
|
+
properties: {
|
221
|
+
level1: {
|
222
|
+
properties: {
|
223
|
+
level2: {
|
224
|
+
oneOf: [
|
225
|
+
{
|
226
|
+
properties: {
|
227
|
+
level3: {
|
228
|
+
anyOf: [
|
229
|
+
{
|
230
|
+
allOf: [{ properties: { deep: { type: 'string' } } }],
|
231
|
+
},
|
232
|
+
],
|
233
|
+
},
|
234
|
+
},
|
235
|
+
},
|
236
|
+
],
|
237
|
+
},
|
238
|
+
},
|
239
|
+
},
|
240
|
+
},
|
241
|
+
};
|
242
|
+
|
243
|
+
expect(hasAllOfCombinator(deepSchema)).toBe(true);
|
244
|
+
});
|
245
|
+
});
|
246
|
+
|
247
|
+
describe('Negative cases - should return false', () => {
|
248
|
+
it('should return false for simple object schema', () => {
|
249
|
+
const schema = {
|
250
|
+
type: 'object',
|
251
|
+
properties: {
|
252
|
+
name: { type: 'string' },
|
253
|
+
age: { type: 'number' },
|
254
|
+
},
|
255
|
+
required: ['name'],
|
256
|
+
};
|
257
|
+
|
258
|
+
expect(hasAllOfCombinator(schema)).toBe(false);
|
259
|
+
});
|
260
|
+
|
261
|
+
it('should return false for schema with only oneOf', () => {
|
262
|
+
const schema = {
|
263
|
+
type: 'object',
|
264
|
+
oneOf: [{ properties: { typeA: { type: 'string' } } }, { properties: { typeB: { type: 'number' } } }],
|
265
|
+
};
|
266
|
+
|
267
|
+
expect(hasAllOfCombinator(schema)).toBe(false);
|
268
|
+
});
|
269
|
+
|
270
|
+
it('should return false for schema with only anyOf', () => {
|
271
|
+
const schema = {
|
272
|
+
type: 'object',
|
273
|
+
anyOf: [{ properties: { option1: { type: 'string' } } }, { properties: { option2: { type: 'boolean' } } }],
|
274
|
+
};
|
275
|
+
|
276
|
+
expect(hasAllOfCombinator(schema)).toBe(false);
|
277
|
+
});
|
278
|
+
|
279
|
+
it('should return false for array schema without allOf', () => {
|
280
|
+
const schema = {
|
281
|
+
type: 'array',
|
282
|
+
items: {
|
283
|
+
type: 'object',
|
284
|
+
properties: {
|
285
|
+
id: { type: 'number' },
|
286
|
+
name: { type: 'string' },
|
287
|
+
},
|
288
|
+
},
|
289
|
+
};
|
290
|
+
|
291
|
+
expect(hasAllOfCombinator(schema)).toBe(false);
|
292
|
+
});
|
293
|
+
|
294
|
+
it('should return false for conditional schema without allOf', () => {
|
295
|
+
const schema = {
|
296
|
+
type: 'object',
|
297
|
+
if: { properties: { type: { const: 'special' } } },
|
298
|
+
then: { properties: { specialValue: { type: 'string' } } },
|
299
|
+
else: { properties: { normalValue: { type: 'string' } } },
|
300
|
+
};
|
301
|
+
|
302
|
+
expect(hasAllOfCombinator(schema)).toBe(false);
|
303
|
+
});
|
304
|
+
});
|
305
|
+
|
306
|
+
describe('Edge cases', () => {
|
307
|
+
it('should return false for null', () => {
|
308
|
+
expect(hasAllOfCombinator(null)).toBe(false);
|
309
|
+
});
|
310
|
+
|
311
|
+
it('should return false for undefined', () => {
|
312
|
+
expect(hasAllOfCombinator(undefined)).toBe(false);
|
313
|
+
});
|
314
|
+
|
315
|
+
it('should return false for string', () => {
|
316
|
+
expect(hasAllOfCombinator('not an object')).toBe(false);
|
317
|
+
});
|
318
|
+
|
319
|
+
it('should return false for number', () => {
|
320
|
+
expect(hasAllOfCombinator(42)).toBe(false);
|
321
|
+
});
|
322
|
+
|
323
|
+
it('should return false for boolean', () => {
|
324
|
+
expect(hasAllOfCombinator(true)).toBe(false);
|
325
|
+
});
|
326
|
+
|
327
|
+
it('should return false for array', () => {
|
328
|
+
expect(hasAllOfCombinator(['not', 'a', 'schema'])).toBe(false);
|
329
|
+
});
|
330
|
+
|
331
|
+
it('should return false for empty object', () => {
|
332
|
+
expect(hasAllOfCombinator({})).toBe(false);
|
333
|
+
});
|
334
|
+
|
335
|
+
it('should handle circular references gracefully', () => {
|
336
|
+
const schema: any = {
|
337
|
+
type: 'object',
|
338
|
+
properties: {
|
339
|
+
self: null,
|
340
|
+
},
|
341
|
+
};
|
342
|
+
schema.properties.self = schema; // Create circular reference
|
343
|
+
|
344
|
+
// Should not crash and should return false (no allOf)
|
345
|
+
expect(hasAllOfCombinator(schema)).toBe(false);
|
346
|
+
});
|
347
|
+
|
348
|
+
it('should handle circular references with allOf', () => {
|
349
|
+
const schema: any = {
|
350
|
+
type: 'object',
|
351
|
+
allOf: [{ properties: { name: { type: 'string' } } }],
|
352
|
+
properties: {
|
353
|
+
self: null,
|
354
|
+
},
|
355
|
+
};
|
356
|
+
schema.properties.self = schema; // Create circular reference
|
357
|
+
|
358
|
+
// Should detect allOf despite circular reference
|
359
|
+
expect(hasAllOfCombinator(schema)).toBe(true);
|
360
|
+
});
|
361
|
+
|
362
|
+
it('should handle deeply circular references', () => {
|
363
|
+
const schemaA: any = {
|
364
|
+
type: 'object',
|
365
|
+
properties: {
|
366
|
+
b: null,
|
367
|
+
},
|
368
|
+
};
|
369
|
+
|
370
|
+
const schemaB: any = {
|
371
|
+
type: 'object',
|
372
|
+
properties: {
|
373
|
+
a: schemaA,
|
374
|
+
c: null,
|
375
|
+
},
|
376
|
+
};
|
377
|
+
|
378
|
+
const schemaC: any = {
|
379
|
+
type: 'object',
|
380
|
+
properties: {
|
381
|
+
b: schemaB,
|
382
|
+
},
|
383
|
+
};
|
384
|
+
|
385
|
+
// Create circular chain: A -> B -> C -> B
|
386
|
+
schemaA.properties.b = schemaB;
|
387
|
+
schemaB.properties.c = schemaC;
|
388
|
+
|
389
|
+
// Should not crash and should return false (no allOf in any of them)
|
390
|
+
expect(hasAllOfCombinator(schemaA)).toBe(false);
|
391
|
+
expect(hasAllOfCombinator(schemaB)).toBe(false);
|
392
|
+
expect(hasAllOfCombinator(schemaC)).toBe(false);
|
393
|
+
});
|
394
|
+
|
395
|
+
it('should handle malformed properties object', () => {
|
396
|
+
const schema = {
|
397
|
+
type: 'object',
|
398
|
+
properties: null, // Malformed properties
|
399
|
+
};
|
400
|
+
|
401
|
+
expect(hasAllOfCombinator(schema)).toBe(false);
|
402
|
+
});
|
403
|
+
});
|
404
|
+
|
405
|
+
describe('Performance characteristics', () => {
|
406
|
+
it('should quickly process large schema without allOf', () => {
|
407
|
+
const largeSchema = {
|
408
|
+
type: 'object',
|
409
|
+
properties: {},
|
410
|
+
};
|
411
|
+
|
412
|
+
// Generate 100 properties
|
413
|
+
for (let i = 0; i < 100; i++) {
|
414
|
+
(largeSchema.properties as any)[`prop${i}`] = {
|
415
|
+
type: 'object',
|
416
|
+
properties: {
|
417
|
+
[`nested${i}`]: { type: 'string' },
|
418
|
+
[`array${i}`]: {
|
419
|
+
type: 'array',
|
420
|
+
items: { type: 'number' },
|
421
|
+
},
|
422
|
+
},
|
423
|
+
};
|
424
|
+
}
|
425
|
+
|
426
|
+
const startTime = performance.now();
|
427
|
+
const result = hasAllOfCombinator(largeSchema);
|
428
|
+
const endTime = performance.now();
|
429
|
+
|
430
|
+
expect(result).toBe(false);
|
431
|
+
expect(endTime - startTime).toBeLessThan(10); // Should be very fast (< 10ms)
|
432
|
+
});
|
433
|
+
|
434
|
+
it('should quickly detect allOf in large schema', () => {
|
435
|
+
const largeSchemaWithAllOf = {
|
436
|
+
type: 'object',
|
437
|
+
allOf: [{ properties: { detected: { type: 'string' } } }],
|
438
|
+
properties: {},
|
439
|
+
};
|
440
|
+
|
441
|
+
// Generate 100 properties
|
442
|
+
for (let i = 0; i < 100; i++) {
|
443
|
+
(largeSchemaWithAllOf.properties as any)[`prop${i}`] = {
|
444
|
+
type: 'string',
|
445
|
+
};
|
446
|
+
}
|
447
|
+
|
448
|
+
const startTime = performance.now();
|
449
|
+
const result = hasAllOfCombinator(largeSchemaWithAllOf);
|
450
|
+
const endTime = performance.now();
|
451
|
+
|
452
|
+
expect(result).toBe(true);
|
453
|
+
expect(endTime - startTime).toBeLessThan(5); // Should be very fast due to early detection
|
454
|
+
});
|
455
|
+
});
|
456
|
+
});
|