Skip to content

Commit

Permalink
fix(repository-json-schema): resolve the circular reference
Browse files Browse the repository at this point in the history
  • Loading branch information
jannyHou committed Apr 22, 2019
1 parent fe8ef46 commit acf6da6
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -641,5 +680,54 @@ describe('build-schema', () => {
},
});
});
it('does not pollute the JSON schema options', () => {
@model()
class Category {
@property()
name: string;
}

const JSON_SCHEMA_OPTIONS = {};
getJsonSchema(Category, JSON_SCHEMA_OPTIONS);
expect(JSON_SCHEMA_OPTIONS).to.be.empty();
});
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);
});
});
});
});
57 changes: 40 additions & 17 deletions packages/repository-json-schema/src/build-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -142,16 +151,26 @@ 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,
jsonSchemaOptions: JsonSchemaOptions = {},
): JSONSchema {
const options = {...jsonSchemaOptions};
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;
Expand Down Expand Up @@ -190,20 +209,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;
Expand Down

0 comments on commit acf6da6

Please sign in to comment.