Skip to content

Commit 9b49773

Browse files
jannyHouJanny
authored andcommitted
fix(repository-json-schema): resolve the circular reference
1 parent fe8ef46 commit 9b49773

File tree

2 files changed

+128
-17
lines changed

2 files changed

+128
-17
lines changed

packages/repository-json-schema/src/__tests__/integration/build-schema.integration.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,45 @@ describe('build-schema', () => {
591591
});
592592
});
593593

594+
context('model conversion', () => {
595+
@model()
596+
class Category {
597+
@property.array(() => Product)
598+
products?: Product[];
599+
}
600+
601+
@model()
602+
class Product {
603+
@property(() => Category)
604+
category?: Category;
605+
}
606+
607+
const expectedSchema = {
608+
title: 'Category',
609+
properties: {
610+
products: {
611+
type: 'array',
612+
items: {$ref: '#/definitions/Product'},
613+
},
614+
},
615+
definitions: {
616+
Product: {
617+
title: 'Product',
618+
properties: {
619+
category: {
620+
$ref: '#/definitions/Category',
621+
},
622+
},
623+
},
624+
},
625+
};
626+
627+
it('handles circular references', () => {
628+
const schema = modelToJsonSchema(Category);
629+
expect(schema).to.deepEqual(expectedSchema);
630+
});
631+
});
632+
594633
function expectValidJsonSchema(schema: JsonSchema) {
595634
const ajv = new Ajv();
596635
const validate = ajv.compile(
@@ -641,5 +680,54 @@ describe('build-schema', () => {
641680
},
642681
});
643682
});
683+
it('does not pollute the JSON schema options', () => {
684+
@model()
685+
class Category {
686+
@property()
687+
name: string;
688+
}
689+
690+
const JSON_SCHEMA_OPTIONS = {};
691+
getJsonSchema(Category, JSON_SCHEMA_OPTIONS);
692+
expect(JSON_SCHEMA_OPTIONS).to.be.empty();
693+
});
694+
context('circular reference', () => {
695+
@model()
696+
class Category {
697+
@property.array(() => Product)
698+
products?: Product[];
699+
}
700+
701+
@model()
702+
class Product {
703+
@property(() => Category)
704+
category?: Category;
705+
}
706+
707+
const expectedSchemaForCategory = {
708+
title: 'Category',
709+
properties: {
710+
products: {
711+
type: 'array',
712+
items: {$ref: '#/definitions/Product'},
713+
},
714+
},
715+
definitions: {
716+
Product: {
717+
title: 'Product',
718+
properties: {
719+
category: {
720+
$ref: '#/definitions/Category',
721+
},
722+
},
723+
},
724+
},
725+
};
726+
727+
it('generates the schema without running into infinite loop', () => {
728+
const schema = getJsonSchema(Category);
729+
expect(schema).to.deepEqual(expectedSchemaForCategory);
730+
});
731+
});
644732
});
645733
});

packages/repository-json-schema/src/build-schema.ts

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,27 @@ import {
1414
import {JSONSchema6 as JSONSchema} from 'json-schema';
1515
import {JSON_SCHEMA_KEY} from './keys';
1616

17+
export interface JsonSchemaOptions {
18+
visited?: {[key: string]: JSONSchema};
19+
}
20+
1721
/**
1822
* Gets the JSON Schema of a TypeScript model/class by seeing if one exists
1923
* in a cache. If not, one is generated and then cached.
2024
* @param ctor Contructor of class to get JSON Schema from
2125
*/
22-
export function getJsonSchema(ctor: Function): JSONSchema {
23-
// NOTE(shimks) currently impossible to dynamically update
24-
const jsonSchema = MetadataInspector.getClassMetadata(JSON_SCHEMA_KEY, ctor);
25-
if (jsonSchema) {
26-
return jsonSchema;
26+
export function getJsonSchema(
27+
ctor: Function,
28+
options?: JsonSchemaOptions,
29+
): JSONSchema {
30+
// In the near future the metadata will be an object with
31+
// different titles as keys
32+
const cached = MetadataInspector.getClassMetadata(JSON_SCHEMA_KEY, ctor);
33+
34+
if (cached) {
35+
return cached;
2736
} else {
28-
const newSchema = modelToJsonSchema(ctor);
37+
const newSchema = modelToJsonSchema(ctor, options);
2938
MetadataInspector.defineMetadata(JSON_SCHEMA_KEY.key, newSchema, ctor);
3039
return newSchema;
3140
}
@@ -142,16 +151,26 @@ export function metaToJsonProperty(meta: PropertyDefinition): JSONSchema {
142151
* reflection API
143152
* @param ctor Constructor of class to convert from
144153
*/
145-
export function modelToJsonSchema(ctor: Function): JSONSchema {
154+
export function modelToJsonSchema(
155+
ctor: Function,
156+
jsonSchemaOptions: JsonSchemaOptions = {},
157+
): JSONSchema {
158+
const options = {...jsonSchemaOptions};
159+
options.visited = options.visited || {};
160+
146161
const meta: ModelDefinition | {} = ModelMetadataHelper.getModelMetadata(ctor);
147-
const result: JSONSchema = {};
148162

149163
// returns an empty object if metadata is an empty object
150164
if (!(meta instanceof ModelDefinition)) {
151165
return {};
152166
}
153167

154-
result.title = meta.title || ctor.name;
168+
const title = meta.title || ctor.name;
169+
170+
if (options.visited[title]) return options.visited[title];
171+
172+
const result: JSONSchema = {title};
173+
options.visited[title] = result;
155174

156175
if (meta.description) {
157176
result.description = meta.description;
@@ -190,20 +209,24 @@ export function modelToJsonSchema(ctor: Function): JSONSchema {
190209
continue;
191210
}
192211

193-
const propSchema = getJsonSchema(referenceType);
212+
const propSchema = getJsonSchema(referenceType, options);
213+
214+
includeReferencedSchema(referenceType.name, propSchema);
194215

195-
if (propSchema && Object.keys(propSchema).length > 0) {
216+
function includeReferencedSchema(name: string, schema: JSONSchema) {
217+
if (!schema || !Object.keys(schema).length) return;
196218
result.definitions = result.definitions || {};
197219

198-
// delete nested definition
199-
if (propSchema.definitions) {
200-
for (const key in propSchema.definitions) {
201-
result.definitions[key] = propSchema.definitions[key];
220+
// promote nested definition to the top level
221+
if (schema.definitions) {
222+
for (const key in schema.definitions) {
223+
if (key === title) continue;
224+
result.definitions[key] = schema.definitions[key];
202225
}
203-
delete propSchema.definitions;
226+
delete schema.definitions;
204227
}
205228

206-
result.definitions[referenceType.name] = propSchema;
229+
result.definitions[name] = schema;
207230
}
208231
}
209232
return result;

0 commit comments

Comments
 (0)