From 5ab7796060d71ddbfaaec54a3976edc27e35701d Mon Sep 17 00:00:00 2001 From: jannyHou Date: Thu, 4 Apr 2019 15:33:02 -0400 Subject: [PATCH] fix(repository-json-schema): resolve the circular reference --- .vscode/settings.json | 4 +- .../integration/build-schema.integration.ts | 77 +++++++++++++++++++ .../src/build-schema.ts | 56 ++++++++++---- 3 files changed, 119 insertions(+), 18 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index cbfd84958981..369b0e48f815 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,7 @@ { - "editor.rulers": [80], + "editor.rulers": [ + 80 + ], "editor.tabCompletion": "on", "editor.tabSize": 2, "editor.trimAutoWhitespace": true, diff --git a/packages/repository-json-schema/src/__tests__/integration/build-schema.integration.ts b/packages/repository-json-schema/src/__tests__/integration/build-schema.integration.ts index a61230f0b56d..14caf3755bda 100644 --- a/packages/repository-json-schema/src/__tests__/integration/build-schema.integration.ts +++ b/packages/repository-json-schema/src/__tests__/integration/build-schema.integration.ts @@ -591,6 +591,45 @@ describe('build-schema', () => { }); }); + context('model conversion', () => { + @model() + class Category { + @property.array(() => Product) + products?: Product[]; + } + + @model() + class Product { + @property(() => Category) + category?: Category; + } + + const expectedSchema = { + title: 'Category', + properties: { + products: { + type: 'array', + items: {$ref: '#/definitions/Product'}, + }, + }, + definitions: { + Product: { + title: 'Product', + properties: { + category: { + $ref: '#/definitions/Category', + }, + }, + }, + }, + }; + + it('handles circular references', () => { + const schema = modelToJsonSchema(Category); + expect(schema).to.deepEqual(expectedSchema); + }); + }); + function expectValidJsonSchema(schema: JsonSchema) { const ajv = new Ajv(); const validate = ajv.compile( @@ -641,5 +680,43 @@ describe('build-schema', () => { }, }); }); + context('circular reference', () => { + @model() + class Category { + @property.array(() => Product) + products?: Product[]; + } + + @model() + class Product { + @property(() => Category) + category?: Category; + } + + const expectedSchemaForCategory = { + title: 'Category', + properties: { + products: { + type: 'array', + items: {$ref: '#/definitions/Product'}, + }, + }, + definitions: { + Product: { + title: 'Product', + properties: { + category: { + $ref: '#/definitions/Category', + }, + }, + }, + }, + }; + + it('generates the schema without running into infinite loop', () => { + const schema = getJsonSchema(Category); + expect(schema).to.deepEqual(expectedSchemaForCategory); + }); + }); }); }); diff --git a/packages/repository-json-schema/src/build-schema.ts b/packages/repository-json-schema/src/build-schema.ts index 647a100cf6e8..b8dfd55e8503 100644 --- a/packages/repository-json-schema/src/build-schema.ts +++ b/packages/repository-json-schema/src/build-schema.ts @@ -14,18 +14,27 @@ import { import {JSONSchema6 as JSONSchema} from 'json-schema'; import {JSON_SCHEMA_KEY} from './keys'; +export interface JsonSchemaOptions { + visited?: {[key: string]: JSONSchema}; +} + /** * Gets the JSON Schema of a TypeScript model/class by seeing if one exists * in a cache. If not, one is generated and then cached. * @param ctor Contructor of class to get JSON Schema from */ -export function getJsonSchema(ctor: Function): JSONSchema { - // NOTE(shimks) currently impossible to dynamically update - const jsonSchema = MetadataInspector.getClassMetadata(JSON_SCHEMA_KEY, ctor); - if (jsonSchema) { - return jsonSchema; +export function getJsonSchema( + ctor: Function, + options?: JsonSchemaOptions, +): JSONSchema { + // In the near future the metadata will be an object with + // different titles as keys + const cached = MetadataInspector.getClassMetadata(JSON_SCHEMA_KEY, ctor); + + if (cached) { + return cached; } else { - const newSchema = modelToJsonSchema(ctor); + const newSchema = modelToJsonSchema(ctor, options); MetadataInspector.defineMetadata(JSON_SCHEMA_KEY.key, newSchema, ctor); return newSchema; } @@ -142,16 +151,25 @@ export function metaToJsonProperty(meta: PropertyDefinition): JSONSchema { * reflection API * @param ctor Constructor of class to convert from */ -export function modelToJsonSchema(ctor: Function): JSONSchema { +export function modelToJsonSchema( + ctor: Function, + options: JsonSchemaOptions = {}, +): JSONSchema { + options.visited = options.visited || {}; + const meta: ModelDefinition | {} = ModelMetadataHelper.getModelMetadata(ctor); - const result: JSONSchema = {}; // returns an empty object if metadata is an empty object if (!(meta instanceof ModelDefinition)) { return {}; } - result.title = meta.title || ctor.name; + const title = meta.title || ctor.name; + + if (options.visited[title]) return options.visited[title]; + + const result: JSONSchema = {title}; + options.visited[title] = result; if (meta.description) { result.description = meta.description; @@ -190,20 +208,24 @@ export function modelToJsonSchema(ctor: Function): JSONSchema { continue; } - const propSchema = getJsonSchema(referenceType); + const propSchema = getJsonSchema(referenceType, options); + + includeReferencedSchema(referenceType.name, propSchema); - if (propSchema && Object.keys(propSchema).length > 0) { + function includeReferencedSchema(name: string, schema: JSONSchema) { + if (!schema || !Object.keys(schema).length) return; result.definitions = result.definitions || {}; - // delete nested definition - if (propSchema.definitions) { - for (const key in propSchema.definitions) { - result.definitions[key] = propSchema.definitions[key]; + // promote nested definition to the top level + if (schema.definitions) { + for (const key in schema.definitions) { + if (key === title) continue; + result.definitions[key] = schema.definitions[key]; } - delete propSchema.definitions; + delete schema.definitions; } - result.definitions[referenceType.name] = propSchema; + result.definitions[name] = schema; } } return result;