From bdfa28ab3843b36f3ca6b1cf471256a367599eb2 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 25 Feb 2020 16:12:54 -0800 Subject: [PATCH] feat(openapi-v3): add sugar decorators for filter/where params Implements https://github.com/strongloop/loopback-next/issues/1749 --- .../unit/decorators/query.decorator.unit.ts | 187 ++++++++++++++++++ .../src/decorators/parameter.decorator.ts | 70 +++++++ 2 files changed, 257 insertions(+) create mode 100644 packages/openapi-v3/src/__tests__/unit/decorators/query.decorator.unit.ts diff --git a/packages/openapi-v3/src/__tests__/unit/decorators/query.decorator.unit.ts b/packages/openapi-v3/src/__tests__/unit/decorators/query.decorator.unit.ts new file mode 100644 index 000000000000..02b2b84fdf76 --- /dev/null +++ b/packages/openapi-v3/src/__tests__/unit/decorators/query.decorator.unit.ts @@ -0,0 +1,187 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/openapi-v3 +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + Count, + Filter, + FilterExcludingWhere, + model, + Model, + property, + Where, +} from '@loopback/repository'; +import {expect} from '@loopback/testlab'; +import {ControllerSpec, get, getControllerSpec, param} from '../../..'; + +describe('sugar decorators for filter and where', () => { + let controllerSpec: ControllerSpec; + + before(() => { + controllerSpec = getControllerSpec(MyController); + }); + + it('allows @param.filter', () => { + expect(controllerSpec.paths['/'].get.parameters).to.eql([ + { + name: 'filter', + in: 'query', + content: { + 'application/json': { + schema: { + type: 'object', + title: 'MyModel.Filter', + properties: { + fields: { + title: 'MyModel.Fields', + type: 'object', + properties: {name: {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'}}, + where: { + title: 'MyModel.WhereFilter', + type: 'object', + additionalProperties: true, + }, + }, + additionalProperties: false, + }, + }, + }, + }, + ]); + }); + + it('allows @param.filter with a custom name', () => { + expect(controllerSpec.paths['/find'].get.parameters).to.eql([ + { + name: 'query', + in: 'query', + content: { + 'application/json': { + schema: { + type: 'object', + title: 'MyModel.Filter', + properties: { + fields: { + title: 'MyModel.Fields', + type: 'object', + properties: {name: {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'}}, + where: { + title: 'MyModel.WhereFilter', + type: 'object', + additionalProperties: true, + }, + }, + additionalProperties: false, + }, + }, + }, + }, + ]); + }); + + it('allows @param.filterExcludingWhere', () => { + expect(controllerSpec.paths['/{id}'].get.parameters).to.eql([ + {name: 'id', in: 'path', schema: {type: 'string'}, required: true}, + { + name: 'filter', + in: 'query', + content: { + 'application/json': { + schema: { + type: 'object', + title: 'MyModel.Filter', + properties: { + fields: { + title: 'MyModel.Fields', + type: 'object', + properties: {name: {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, + }, + }, + }, + }, + ]); + }); + + it('allows @param.where', () => { + expect(controllerSpec.paths['/count'].get.parameters).to.eql([ + { + name: 'where', + in: 'query', + content: { + 'application/json': { + schema: { + type: 'object', + title: 'MyModel.WhereFilter', + additionalProperties: true, + }, + }, + }, + }, + ]); + }); + + @model() + class MyModel extends Model { + constructor(data: Partial) { + super(data); + } + @property() + name: string; + } + + class MyController { + @get('/') + async find( + @param.filter(MyModel) + filter?: Filter, + ): Promise { + throw new Error('Not implemented'); + } + + @get('/find') + async findByQuery( + @param.filter(MyModel, 'query') + query?: Filter, + ): Promise { + throw new Error('Not implemented'); + } + + @get('/{id}') + async findById( + @param.path.string('id') id: string, + @param.filterExcludingWhere(MyModel) + filter?: FilterExcludingWhere, + ): Promise { + throw new Error('Not implemented'); + } + + @get('/count') + async count( + @param.where(MyModel) + where?: Where, + ): Promise { + throw new Error('Not implemented'); + } + } +}); diff --git a/packages/openapi-v3/src/decorators/parameter.decorator.ts b/packages/openapi-v3/src/decorators/parameter.decorator.ts index 40bb3ecb52e2..760783cc1cfc 100644 --- a/packages/openapi-v3/src/decorators/parameter.decorator.ts +++ b/packages/openapi-v3/src/decorators/parameter.decorator.ts @@ -4,6 +4,12 @@ // License text available at https://opensource.org/licenses/MIT import {MetadataInspector, ParameterDecoratorFactory} from '@loopback/core'; +import {Model} from '@loopback/repository-json-schema'; +import { + getFilterExcludingWhereSchemaFor, + getFilterSchemaFor, + getWhereSchemaFor, +} from '../filter-schema'; import {resolveSchema} from '../generate-schema'; import {OAI3Keys} from '../keys'; import { @@ -437,6 +443,70 @@ export namespace param { schema: {type: 'array', items: itemSpec}, }); }; + + /** + * Sugar decorator for `filter` query parameter + * + * @example + * ```ts + * async find( + * @param.filter(modelCtor)) filter?: Filter, + * ): Promise<(T & Relations)[]> { + * // ... + * } + * ``` + * @param modelCtor - Model class + * @param name - Custom name for the parameter, default to `filter` + * + */ + export function filter(modelCtor: typeof Model, name = 'filter') { + return param.query.object(name, getFilterSchemaFor(modelCtor)); + } + + /** + * Sugar decorator for `filter` query parameter excluding `where` + * + * @example + * ```ts + * async findById( + * @param(idPathParam) id: IdType, + * @param.filter(modelCtor)) filter?: FilterExcludingWhere, + * ): Promise<(T & Relations)[]> { + * // ... + * } + * ``` + * @param modelCtor - Model class + * @param name - Custom name for the parameter, default to `filter` + * + */ + export function filterExcludingWhere( + modelCtor: typeof Model, + name = 'filter', + ) { + return param.query.object( + name, + getFilterExcludingWhereSchemaFor(modelCtor), + ); + } + + /** + * Sugar decorator for `filter` query parameter excluding `where` + * + * @example + * ```ts + * async count( + * @param.where(modelCtor)) where?: Where, + * ): Promise { + * // ... + * } + * ``` + * @param modelCtor - Model class + * @param name - Custom name for the parameter, default to `where` + * + */ + export function where(modelCtor: typeof Model, name = 'where') { + return param.query.object(name, getWhereSchemaFor(modelCtor)); + } } interface ParamShortcutOptions {