diff --git a/packages/openapi-v3/src/__tests__/unit/filter-schema.unit.ts b/packages/openapi-v3/src/__tests__/unit/filter-schema.unit.ts index 3a7373190ca2..3c031328143c 100644 --- a/packages/openapi-v3/src/__tests__/unit/filter-schema.unit.ts +++ b/packages/openapi-v3/src/__tests__/unit/filter-schema.unit.ts @@ -21,13 +21,54 @@ describe('filterSchema', () => { const schema = getFilterSchemaFor(MyUserModel); expect(MyUserModel.definition.name).to.eql('my-user-model'); expect(schema).to.eql({ + title: 'my-user-model.Filter', properties: { where: { type: 'object', + title: 'my-user-model.WhereFilter', additionalProperties: true, }, fields: { type: 'object', + title: 'my-user-model.Fields', + properties: { + id: {type: 'boolean'}, + age: {type: 'boolean'}, + }, + additionalProperties: false, + }, + offset: {type: 'integer', minimum: 0}, + limit: {type: 'integer', minimum: 1, example: 100}, + skip: {type: 'integer', minimum: 0}, + order: {type: 'array', items: {type: 'string'}}, + }, + additionalProperties: false, + }); + }); + + @model({ + name: 'CustomUserModel', + }) + class CustomUserModel extends Entity { + @property() id: string; + + @property() age: number; + } + + it('generates filter schema with custom name', () => { + const schema = getFilterSchemaFor(CustomUserModel); + expect(CustomUserModel.definition.name).to.eql('CustomUserModel'); + expect(schema).to.eql({ + title: 'CustomUserModel.Filter', + properties: { + where: { + type: 'object', + title: 'CustomUserModel.WhereFilter', + additionalProperties: true, + }, + fields: { + type: 'object', + title: 'CustomUserModel.Fields', properties: { id: {type: 'boolean'}, age: {type: 'boolean'}, diff --git a/packages/repository-json-schema/src/__tests__/unit/filter-json-schema.unit.ts b/packages/repository-json-schema/src/__tests__/unit/filter-json-schema.unit.ts index 908d9983aebf..424f4509769c 100644 --- a/packages/repository-json-schema/src/__tests__/unit/filter-json-schema.unit.ts +++ b/packages/repository-json-schema/src/__tests__/unit/filter-json-schema.unit.ts @@ -8,6 +8,7 @@ import {expect} from '@loopback/testlab'; import Ajv from 'ajv'; import {JsonSchema} from '../..'; import { + getFieldsJsonSchemaFor, getFilterJsonSchemaFor, getWhereJsonSchemaFor, } from '../../filter-json-schema'; @@ -145,6 +146,24 @@ describe('getFilterJsonSchemaFor', () => { ]); }); + it('returns "title" when no options were provided', () => { + expect(orderFilterSchema.title).to.equal('Order.Filter'); + }); + + it('returns "include.title" when no options were provided', () => { + expect(customerFilterSchema.properties) + .to.have.propertyByPath(...['include', 'title']) + .to.equal('Customer.IncludeFilter'); + }); + + it('returns "scope.title" when no options were provided', () => { + expect(customerFilterSchema.properties) + .to.have.propertyByPath( + ...['include', 'items', 'properties', 'scope', 'title'], + ) + .to.equal('Customer.ScopeFilter'); + }); + function expectSchemaToAllowFilter(schema: JsonSchema, value: T) { const isValid = ajv.validate(schema, value); const SUCCESS_MSG = 'Filter instance is valid according to Filter schema'; @@ -153,6 +172,58 @@ describe('getFilterJsonSchemaFor', () => { } }); +describe('getFilterJsonSchemaForOptionsSetTitle', () => { + let customerFilterSchema: JsonSchema; + + beforeEach(() => { + customerFilterSchema = getFilterJsonSchemaFor(Customer, {setTitle: true}); + }); + + it('returns "title" when a single option "setTitle" is set', () => { + expect(customerFilterSchema.title).to.equal('Customer.Filter'); + }); + + it('returns "include.title" when a single option "setTitle" is set', () => { + expect(customerFilterSchema.properties) + .to.have.propertyByPath(...['include', 'title']) + .to.equal('Customer.IncludeFilter'); + }); + + it('returns "scope.title" when a single option "setTitle" is set', () => { + expect(customerFilterSchema.properties) + .to.have.propertyByPath( + ...['include', 'items', 'properties', 'scope', 'title'], + ) + .to.equal('Customer.ScopeFilter'); + }); +}); + +describe('getFilterJsonSchemaForOptionsUnsetTitle', () => { + let customerFilterSchema: JsonSchema; + + beforeEach(() => { + customerFilterSchema = getFilterJsonSchemaFor(Customer, {setTitle: false}); + }); + + it('"title" undefined when a single option "setTitle" is false', () => { + expect(customerFilterSchema.title).to.equal(undefined); + }); + + it('"include.title" undefined when single option "setTitle" is false', () => { + expect(customerFilterSchema.properties) + .to.have.propertyByPath(...['include', 'title']) + .to.equal(undefined); + }); + + it('"scope.title" undefined when single option "setTitle" is false', () => { + expect(customerFilterSchema.properties) + .to.have.propertyByPath( + ...['include', 'items', 'properties', 'scope', 'title'], + ) + .to.equal(undefined); + }); +}); + describe('getWhereJsonSchemaFor', () => { let ajv: Ajv.Ajv; let customerWhereSchema: JsonSchema; @@ -169,6 +240,51 @@ describe('getWhereJsonSchemaFor', () => { const result = isValid ? SUCCESS_MSG : ajv.errorsText(ajv.errors!); expect(result).to.equal(SUCCESS_MSG); }); + + it('returns "title" when no options were provided', () => { + expect(customerWhereSchema.title).to.equal('Customer.WhereFilter'); + }); +}); + +describe('getWhereJsonSchemaForOptions', () => { + let customerWhereSchema: JsonSchema; + + it('returns "title" when a single option "setTitle" is set', () => { + customerWhereSchema = getWhereJsonSchemaFor(Customer, { + setTitle: true, + }); + expect(customerWhereSchema.title).to.equal('Customer.WhereFilter'); + }); + + it('leaves out "title" when a single option "setTitle" is false', () => { + customerWhereSchema = getWhereJsonSchemaFor(Customer, { + setTitle: false, + }); + expect(customerWhereSchema.title).to.equal(undefined); + }); +}); + +describe('getFieldsJsonSchemaFor', () => { + let customerFieldsSchema: JsonSchema; + + it('returns "title" when no options were provided', () => { + customerFieldsSchema = getFieldsJsonSchemaFor(Customer); + expect(customerFieldsSchema.title).to.equal('Customer.Fields'); + }); + + it('returns "title" when a single option "setTitle" is set', () => { + customerFieldsSchema = getFieldsJsonSchemaFor(Customer, { + setTitle: true, + }); + expect(customerFieldsSchema.title).to.equal('Customer.Fields'); + }); + + it('leaves out "title" when a single option "setTitle" is false', () => { + customerFieldsSchema = getFieldsJsonSchemaFor(Customer, { + setTitle: false, + }); + expect(customerFieldsSchema.title).to.equal(undefined); + }); }); @model() diff --git a/packages/repository-json-schema/src/filter-json-schema.ts b/packages/repository-json-schema/src/filter-json-schema.ts index 284c8215e532..7b8ba8c4eb7e 100644 --- a/packages/repository-json-schema/src/filter-json-schema.ts +++ b/packages/repository-json-schema/src/filter-json-schema.ts @@ -6,10 +6,42 @@ import {getModelRelations, Model, model} from '@loopback/repository'; import {JSONSchema6 as JsonSchema} from 'json-schema'; -@model({settings: {strict: false}}) -class EmptyModel extends Model {} +export interface FilterSchemaOptions { + /** + * Set this flag if you want the schema to include title property. + * + * By default the setting is enabled. (e.g. {setTitle: true}) + * + */ + setTitle?: boolean; +} + +/** + * Build a JSON schema describing the format of the "scope" object + * used to query model instances. + * + * Note we don't take the model properties into account yet and return + * a generic json schema allowing any "where" condition. + * + * @param modelCtor - The model constructor to build the filter schema for. + */ +export function getScopeFilterJsonSchemaFor( + modelCtor: typeof Model, + options: FilterSchemaOptions = {}, +): JsonSchema { + @model({settings: {strict: false}}) + class EmptyModel extends Model {} -const scopeFilter = getFilterJsonSchemaFor(EmptyModel); + const schema: JsonSchema = { + ...getFilterJsonSchemaFor(EmptyModel, {setTitle: false}), + title: + options.setTitle !== false + ? `${modelCtor.modelName}.ScopeFilter` + : undefined, + }; + + return schema; +} /** * Build a JSON schema describing the format of the "filter" object @@ -20,12 +52,17 @@ const scopeFilter = getFilterJsonSchemaFor(EmptyModel); * * @param modelCtor - The model constructor to build the filter schema for. */ -export function getFilterJsonSchemaFor(modelCtor: typeof Model): JsonSchema { +export function getFilterJsonSchemaFor( + modelCtor: typeof Model, + options: FilterSchemaOptions = {}, +): JsonSchema { const schema: JsonSchema = { + title: + options.setTitle !== false ? `${modelCtor.modelName}.Filter` : undefined, properties: { - where: getWhereJsonSchemaFor(modelCtor), + where: getWhereJsonSchemaFor(modelCtor, options), - fields: getFieldsJsonSchemaFor(modelCtor), + fields: getFieldsJsonSchemaFor(modelCtor, options), offset: { type: 'integer', @@ -58,6 +95,10 @@ export function getFilterJsonSchemaFor(modelCtor: typeof Model): JsonSchema { if (hasRelations) { schema.properties!.include = { + title: + options.setTitle !== false + ? `${modelCtor.modelName}.IncludeFilter` + : undefined, type: 'array', items: { type: 'object', @@ -65,7 +106,7 @@ export function getFilterJsonSchemaFor(modelCtor: typeof Model): JsonSchema { // TODO(bajtos) restrict values to relations defined by "model" relation: {type: 'string'}, // TODO(bajtos) describe the filter for the relation target model - scope: scopeFilter, + scope: getScopeFilterJsonSchemaFor(modelCtor, options), }, }, }; @@ -83,13 +124,21 @@ export function getFilterJsonSchemaFor(modelCtor: typeof Model): JsonSchema { * * @param modelCtor - The model constructor to build the filter schema for. */ -export function getWhereJsonSchemaFor(modelCtor: typeof Model): JsonSchema { +export function getWhereJsonSchemaFor( + modelCtor: typeof Model, + options: FilterSchemaOptions = {}, +): JsonSchema { const schema: JsonSchema = { + title: + options.setTitle !== false + ? `${modelCtor.modelName}.WhereFilter` + : undefined, type: 'object', // TODO(bajtos) enumerate "model" properties and operators like "and" // See https://github.com/strongloop/loopback-next/issues/1748 additionalProperties: true, }; + return schema; } @@ -100,9 +149,15 @@ export function getWhereJsonSchemaFor(modelCtor: typeof Model): JsonSchema { * @param modelCtor - The model constructor to build the filter schema for. */ -export function getFieldsJsonSchemaFor(modelCtor: typeof Model): JsonSchema { +export function getFieldsJsonSchemaFor( + modelCtor: typeof Model, + options: FilterSchemaOptions = {}, +): JsonSchema { const schema: JsonSchema = { + title: + options.setTitle !== false ? `${modelCtor.modelName}.Fields` : undefined, type: 'object', + properties: Object.assign( {}, ...Object.keys(modelCtor.definition.properties).map(k => ({ @@ -111,5 +166,6 @@ export function getFieldsJsonSchemaFor(modelCtor: typeof Model): JsonSchema { ), additionalProperties: modelCtor.definition.settings.strict === false, }; + return schema; }