Skip to content

Commit efd7aaf

Browse files
authored
fix(parser): internal cache improvements to dereferencing circular refs (#973)
## 🧰 Changes We uncovered a problem with the dereferencing functionality within our OpenAPI parser on schemas with an extensive amount of circular references where under these circumstances it could take upwards to an hour to dereference a schema. I was able to trace it down to being a problem within [@apidevtools/json-schema-ref-parser](https://npm.im/@apidevtools/json-schema-ref-parser) where because it was not always using its internal cache it would get into situations where, when parsing a circular reference, it would often attempt to re-parse that same reference multiple times. I submitted a fix for this over in APIDevTools/json-schema-ref-parser#380, and this pulls that in along with builds a unit test around one of these problematic schemas.
1 parent 0237f26 commit efd7aaf

File tree

6 files changed

+2925
-14
lines changed

6 files changed

+2925
-14
lines changed

package-lock.json

+4-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/oas/test/analyzer/queries/openapi.test.ts

-5
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,8 @@ describe('analyzer queries (OpenAPI)', () => {
8989
describe('circularRefs', () => {
9090
it('should determine if a definition has circular refs when it does', async () => {
9191
await expect(QUERIES.circularRefs(circular)).resolves.toStrictEqual([
92-
'#/components/schemas/BodyPart/properties/parent',
93-
'#/components/schemas/MultiPart/properties/bodyParts/items',
9492
'#/components/schemas/MultiPart/properties/parent',
9593
'#/components/schemas/ZoneOffset/properties/rules',
96-
'#/components/schemas/ZoneOffsetTransition/properties/offsetAfter',
97-
'#/components/schemas/ZoneOffsetTransition/properties/offsetBefore',
98-
'#/components/schemas/ZoneRules/properties/transitions/items',
9994
]);
10095
});
10196

packages/oas/test/index.test.ts

-4
Original file line numberDiff line numberDiff line change
@@ -1753,11 +1753,7 @@ describe('Oas', () => {
17531753

17541754
expect(oas.getCircularReferences()).toStrictEqual([
17551755
'#/components/schemas/offsetTransition/properties/offsetAfter',
1756-
'#/components/schemas/offsetTransition/properties/offsetBefore',
17571756
'#/components/schemas/ProductStock/properties/test_param/items',
1758-
'#/components/schemas/rules/properties/transitions/items',
1759-
'#/components/schemas/offset/properties/rules',
1760-
'#/components/schemas/SalesLine/properties/stock',
17611757
]);
17621758
});
17631759

packages/parser/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
"test": "echo 'Please run tests from the root!' && exit 1"
5959
},
6060
"dependencies": {
61-
"@apidevtools/json-schema-ref-parser": "^11.9.2",
61+
"@apidevtools/json-schema-ref-parser": "^12.0.1",
6262
"@readme/better-ajv-errors": "^2.3.2",
6363
"@readme/openapi-schemas": "^3.1.0",
6464
"@types/json-schema": "^7.0.15",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import type { ValidAPIDefinition } from '../../utils.js';
2+
3+
import { describe, it, expect, assert } from 'vitest';
4+
5+
import { dereference, validate } from '../../../src/index.js';
6+
import { relativePath } from '../../utils.js';
7+
import { toValidate } from '../../vitest.matchers.js';
8+
9+
expect.extend({ toValidate });
10+
11+
describe('API with extensive circular $refs that cause slowdowns', () => {
12+
it('should validate successfully', async () => {
13+
await expect(relativePath('specs/circular-slowdowns/schema.json')).toValidate();
14+
});
15+
16+
it('should dereference successfully', async () => {
17+
const circularRefs = new Set<string>();
18+
19+
const schema = await dereference<ValidAPIDefinition>(relativePath('specs/circular-slowdowns/schema.json'), {
20+
dereference: {
21+
onCircular: (ref: string) => circularRefs.add(ref),
22+
},
23+
});
24+
25+
// Ensure that a non-circular $ref was dereferenced.
26+
expect(schema.components?.schemas?.ArrayOfMappedData).toStrictEqual({
27+
type: 'array',
28+
items: {
29+
type: 'object',
30+
properties: {
31+
mappingTypeName: { type: 'string' },
32+
sourceSystemValue: { type: 'string' },
33+
mappedValueID: { type: 'string' },
34+
mappedValue: { type: 'string' },
35+
},
36+
additionalProperties: false,
37+
},
38+
});
39+
40+
// Ensure that a circular $ref **was** dereferenced.
41+
expect(circularRefs).toHaveLength(23);
42+
expect(schema.components?.schemas?.Customer?.properties?.customerNode).toStrictEqual({
43+
type: 'array',
44+
items: {
45+
type: 'object',
46+
properties: {
47+
customerNodeGuid: expect.any(Object),
48+
customerGuid: expect.any(Object),
49+
nodeId: expect.any(Object),
50+
customerGu: expect.any(Object),
51+
},
52+
additionalProperties: false,
53+
},
54+
});
55+
});
56+
57+
it('should not dereference circular $refs if "options.dereference.circular" is "ignore"', async () => {
58+
const circularRefs = new Set<string>();
59+
60+
const schema = await dereference<ValidAPIDefinition>(relativePath('specs/circular-slowdowns/schema.json'), {
61+
dereference: {
62+
circular: 'ignore',
63+
onCircular: (ref: string) => circularRefs.add(ref),
64+
},
65+
});
66+
67+
// Ensure that a non-circular $ref was dereferenced.
68+
expect(schema.components?.schemas?.ArrayOfMappedData).toStrictEqual({
69+
type: 'array',
70+
items: {
71+
type: 'object',
72+
properties: {
73+
mappingTypeName: { type: 'string' },
74+
sourceSystemValue: { type: 'string' },
75+
mappedValueID: { type: 'string' },
76+
mappedValue: { type: 'string' },
77+
},
78+
additionalProperties: false,
79+
},
80+
});
81+
82+
// Ensure that a circular $ref was **not** dereferenced.
83+
expect(circularRefs).toHaveLength(23);
84+
expect(schema.components?.schemas?.Customer?.properties?.customerNode).toStrictEqual({
85+
type: 'array',
86+
items: {
87+
$ref: '#/components/schemas/CustomerNode',
88+
},
89+
});
90+
});
91+
92+
it('should fail validation if "options.dereference.circular" is false', async () => {
93+
try {
94+
await validate(relativePath('specs/circular-slowdowns/schema.json'), { dereference: { circular: false } });
95+
assert.fail();
96+
} catch (err) {
97+
expect(err).toBeInstanceOf(ReferenceError);
98+
expect(err.message).toBe(
99+
'The API contains circular references but the validator is configured to not permit them.',
100+
);
101+
}
102+
});
103+
});

0 commit comments

Comments
 (0)