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 c584129b6423..d07213d01e03 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 @@ -770,6 +770,7 @@ describe('build-schema', () => { definitions: { ProductWithRelations: { title: 'ProductWithRelations', + description: `(Schema options: { includeRelations: true })`, properties: { id: {type: 'number'}, categoryId: {type: 'number'}, @@ -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); @@ -809,6 +811,7 @@ describe('build-schema', () => { definitions: { ProductWithRelations: { title: 'ProductWithRelations', + description: `(Schema options: { includeRelations: true })`, properties: { id: {type: 'number'}, categoryId: {type: 'number'}, @@ -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 @@ -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', () => { @@ -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' ] })`, ); }); @@ -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', () => { @@ -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' ] })`, ); }); @@ -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', () => { @@ -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', () => { diff --git a/packages/repository-json-schema/src/__tests__/unit/build-schema.unit.ts b/packages/repository-json-schema/src/__tests__/unit/build-schema.unit.ts index 061db295bc46..d3adae171112 100644 --- a/packages/repository-json-schema/src/__tests__/unit/build-schema.unit.ts +++ b/packages/repository-json-schema/src/__tests__/unit/build-schema.unit.ts @@ -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', () => { @@ -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', () => { @@ -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', () => { @@ -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', ); }); diff --git a/packages/repository-json-schema/src/build-schema.ts b/packages/repository-json-schema/src/build-schema.ts index d98358b99363..eb51fd2acb66 100644 --- a/packages/repository-json-schema/src/build-schema.ts +++ b/packages/repository-json-schema/src/build-schema.ts @@ -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'); @@ -290,15 +291,20 @@ function buildSchemaTitle( return title + getTitleSuffix(options); } +/** + * Checks the options and generates a descriptive suffix using compatible chars + * @param options json schema options + */ function getTitleSuffix(options: JsonSchemaOptions = {}) { 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'; @@ -307,6 +313,37 @@ function getTitleSuffix(options: JsonSchemaOptions = {}) { 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( + rawOptions: JsonSchemaOptions = {}, +) { + 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 @@ -345,8 +382,14 @@ export function modelToJsonSchema( 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) {