Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(repository-json-schema): update to pass openapi validation in azure #3504

Merged
merged 1 commit into from Sep 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -770,6 +770,7 @@ describe('build-schema', () => {
definitions: {
ProductWithRelations: {
title: 'ProductWithRelations',
description: `(Schema options: { includeRelations: true })`,
properties: {
id: {type: 'number'},
categoryId: {type: 'number'},
Expand All @@ -785,6 +786,7 @@ describe('build-schema', () => {
},
},
title: 'CategoryWithRelations',
description: `(Schema options: { includeRelations: true })`,
};
const jsonSchema = getJsonSchema(Category, {includeRelations: true});
expect(jsonSchema).to.deepEqual(expectedSchema);
Expand All @@ -809,6 +811,7 @@ describe('build-schema', () => {
definitions: {
ProductWithRelations: {
title: 'ProductWithRelations',
description: `(Schema options: { includeRelations: true })`,
properties: {
id: {type: 'number'},
categoryId: {type: 'number'},
Expand All @@ -825,6 +828,7 @@ describe('build-schema', () => {
},
},
title: 'CategoryWithoutPropWithRelations',
description: `(Schema options: { includeRelations: true })`,
};

// To check for case when there are no other properties than relational
Expand Down Expand Up @@ -988,7 +992,10 @@ describe('build-schema', () => {
name: {type: 'string'},
description: {type: 'string'},
});
expect(excludeIdSchema.title).to.equal('ProductExcluding[id]');
expect(excludeIdSchema.title).to.equal('ProductExcluding_id_');
expect(excludeIdSchema.description).to.endWith(
`(Schema options: { exclude: [ 'id' ] })`,
);
});

it('excludes multiple properties when the option "exclude" is set to exclude multiple properties', () => {
Expand All @@ -1007,7 +1014,10 @@ describe('build-schema', () => {
description: {type: 'string'},
});
expect(excludeIdAndNameSchema.title).to.equal(
'ProductExcluding[id,name]',
'ProductExcluding_id-name_',
);
expect(excludeIdAndNameSchema.description).to.endWith(
`(Schema options: { exclude: [ 'id', 'name' ] })`,
);
});

Expand Down Expand Up @@ -1050,7 +1060,10 @@ describe('build-schema', () => {

const optionalIdSchema = getJsonSchema(Product, {optional: ['id']});
expect(optionalIdSchema.required).to.deepEqual(['name']);
expect(optionalIdSchema.title).to.equal('ProductOptional[id]');
expect(optionalIdSchema.title).to.equal('ProductOptional_id_');
expect(optionalIdSchema.description).to.endWith(
`(Schema options: { optional: [ 'id' ] })`,
);
});

it('makes multiple properties optional when the option "optional" includes multiple properties', () => {
Expand All @@ -1063,7 +1076,10 @@ describe('build-schema', () => {
});
expect(optionalIdAndNameSchema.required).to.equal(undefined);
expect(optionalIdAndNameSchema.title).to.equal(
'ProductOptional[id,name]',
'ProductOptional_id-name_',
);
expect(optionalIdAndNameSchema.description).to.endWith(
`(Schema options: { optional: [ 'id', 'name' ] })`,
);
});

Expand All @@ -1087,14 +1103,20 @@ describe('build-schema', () => {
optional: ['name'],
});
expect(optionalNameSchema.required).to.deepEqual(['id']);
expect(optionalNameSchema.title).to.equal('ProductOptional[name]');
expect(optionalNameSchema.title).to.equal('ProductOptional_name_');
expect(optionalNameSchema.description).to.endWith(
`(Schema options: { optional: [ 'name' ] })`,
);

optionalNameSchema = getJsonSchema(Product, {
partial: false,
optional: ['name'],
});
expect(optionalNameSchema.required).to.deepEqual(['id']);
expect(optionalNameSchema.title).to.equal('ProductOptional[name]');
expect(optionalNameSchema.title).to.equal('ProductOptional_name_');
expect(optionalNameSchema.description).to.endWith(
`(Schema options: { optional: [ 'name' ] })`,
);
});

it('uses "partial" option, if provided, when "optional" option is set but empty', () => {
Expand All @@ -1109,6 +1131,23 @@ describe('build-schema', () => {
expect(optionalNameSchema.required).to.equal(undefined);
expect(optionalNameSchema.title).to.equal('ProductPartial');
});

it('can work with "optional" and "exclude" options together', () => {
const originalSchema = getJsonSchema(Product);
expect(originalSchema.required).to.deepEqual(['id', 'name']);
expect(originalSchema.title).to.equal('Product');

const bothOptionsSchema = getJsonSchema(Product, {
exclude: ['id'],
optional: ['name'],
});
expect(bothOptionsSchema.title).to.equal(
'ProductOptional_name_Excluding_id_',
);
expect(bothOptionsSchema.description).to.endWith(
`(Schema options: { exclude: [ 'id' ], optional: [ 'name' ] })`,
);
});
});

it('creates new cache entry for each custom title', () => {
Expand Down
Expand Up @@ -269,9 +269,9 @@ describe('build-schema', () => {
expect(key).to.equal('modelPartial');
});

it('returns "excluding[id,_rev]" when a single option "exclude" is set', () => {
it('returns "excluding_id-_rev_" when a single option "exclude" is set', () => {
const key = buildModelCacheKey({exclude: ['id', '_rev']});
expect(key).to.equal('modelExcluding[id,_rev]');
expect(key).to.equal('modelExcluding_id-_rev_');
});

it('does not include "exclude" in concatenated option names if it is empty', () => {
Expand All @@ -283,9 +283,9 @@ describe('build-schema', () => {
expect(key).to.equal('modelPartialWithRelations');
});

it('returns "optional[id,_rev]" when "optional" is set with two items', () => {
it('returns "optional_id-_rev_" when "optional" is set with two items', () => {
const key = buildModelCacheKey({optional: ['id', '_rev']});
expect(key).to.equal('modelOptional[id,_rev]');
expect(key).to.equal('modelOptional_id-_rev_');
});

it('does not include "optional" in concatenated option names if it is empty', () => {
Expand All @@ -302,7 +302,7 @@ describe('build-schema', () => {
partial: true,
optional: ['name'],
});
expect(key).to.equal('modelOptional[name]');
expect(key).to.equal('modelOptional_name_');
});

it('includes "partial" in option names if "optional" is empty', () => {
Expand All @@ -322,7 +322,7 @@ describe('build-schema', () => {
includeRelations: true,
});
expect(key).to.equal(
'modelOptional[name]Excluding[id,_rev]WithRelations',
'modelOptional_name_Excluding_id-_rev_WithRelations',
);
});

Expand Down
49 changes: 46 additions & 3 deletions packages/repository-json-schema/src/build-schema.ts
Expand Up @@ -14,6 +14,7 @@ import {
} from '@loopback/repository';
import * as debugFactory from 'debug';
import {JSONSchema6 as JSONSchema} from 'json-schema';
import {inspect} from 'util';
import {JSON_SCHEMA_KEY, MODEL_TYPE_KEYS} from './keys';
const debug = debugFactory('loopback:repository-json-schema:build-schema');

Expand Down Expand Up @@ -290,15 +291,20 @@ function buildSchemaTitle<T extends object>(
return title + getTitleSuffix(options);
}

/**
* Checks the options and generates a descriptive suffix using compatible chars
* @param options json schema options
*/
function getTitleSuffix<T extends object>(options: JsonSchemaOptions<T> = {}) {
let suffix = '';

if (options.optional && options.optional.length) {
suffix += 'Optional[' + options.optional + ']';
suffix += `Optional_${options.optional.join('-')}_`;
} else if (options.partial) {
suffix += 'Partial';
}
if (options.exclude && options.exclude.length) {
suffix += 'Excluding[' + options.exclude + ']';
suffix += `Excluding_${options.exclude.join('-')}_`;
}
if (options.includeRelations) {
suffix += 'WithRelations';
Expand All @@ -307,6 +313,37 @@ function getTitleSuffix<T extends object>(options: JsonSchemaOptions<T> = {}) {
return suffix;
}

function stringifyOptions(modelSettings: object = {}) {
return inspect(modelSettings, {
depth: Infinity,
maxArrayLength: Infinity,
breakLength: Infinity,
});
}

function isEmptyJson(obj: object) {
return !(obj && Object.keys(obj).length);
}

/**
* Checks the options and generates a descriptive suffix
* @param options json schema options
*/
function getDescriptionSuffix<T extends object>(
rawOptions: JsonSchemaOptions<T> = {},
) {
const options = {...rawOptions};

delete options.visited;
if (options.optional && !options.optional.length) {
delete options.optional;
}

return !isEmptyJson(options)
? `(Schema options: ${stringifyOptions(options)})`
: '';
}

// NOTE(shimks) no metadata for: union, optional, nested array, any, enum,
// string literal, anonymous types, and inherited properties

Expand Down Expand Up @@ -345,8 +382,14 @@ export function modelToJsonSchema<T extends object>(
const result: JSONSchema = {title};
options.visited[title] = result;

const descriptionSuffix = getDescriptionSuffix(options);

if (meta.description) {
result.description = meta.description;
const formatSuffix = descriptionSuffix ? ` ${descriptionSuffix}` : '';

result.description = meta.description + formatSuffix;
} else if (descriptionSuffix) {
result.description = descriptionSuffix;
}

for (const p in meta.properties) {
Expand Down