Skip to content

Commit

Permalink
Merge c353a63 into a16ae34
Browse files Browse the repository at this point in the history
  • Loading branch information
ericzon committed Sep 26, 2019
2 parents a16ae34 + c353a63 commit 7bc5181
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 15 deletions.
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

0 comments on commit 7bc5181

Please sign in to comment.