Skip to content

Commit

Permalink
feat(openapi-v3): add sugar decorators for filter/where params
Browse files Browse the repository at this point in the history
Implements #1749
  • Loading branch information
raymondfeng committed Feb 28, 2020
1 parent be73660 commit f61896e
Show file tree
Hide file tree
Showing 2 changed files with 250 additions and 0 deletions.
@@ -0,0 +1,201 @@
// 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.filter with a custom name via options', () => {
expect(controllerSpec.paths['/find'].get.parameters[0].name).to.eql(
'query',
);
});

it('allows @param.filter() excluding where', () => {
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<MyModel>) {
super(data);
}
@property()
name: string;
}

class MyController {
@get('/')
async find(
@param.filter(MyModel)
filter?: Filter<MyModel>,
): Promise<MyModel[]> {
throw new Error('Not implemented');
}

@get('/find')
async findByQuery(
@param.filter(MyModel, 'query')
query?: Filter<MyModel>,
): Promise<MyModel[]> {
throw new Error('Not implemented');
}

@get('/search')
async search(
@param.filter(MyModel, {name: 'query'})
query?: Filter<MyModel>,
): Promise<MyModel[]> {
throw new Error('Not implemented');
}

@get('/{id}')
async findById(
@param.path.string('id') id: string,
@param.filter(MyModel, {exclude: 'where'})
filter?: FilterExcludingWhere<MyModel>,
): Promise<MyModel> {
throw new Error('Not implemented');
}

@get('/count')
async count(
@param.where(MyModel)
where?: Where<MyModel>,
): Promise<Count> {
throw new Error('Not implemented');
}
}
});
49 changes: 49 additions & 0 deletions packages/openapi-v3/src/decorators/parameter.decorator.ts
Expand Up @@ -4,6 +4,8 @@
// License text available at https://opensource.org/licenses/MIT

import {MetadataInspector, ParameterDecoratorFactory} from '@loopback/core';
import {FilterSchemaOptions, Model} from '@loopback/repository-json-schema';
import {getFilterSchemaFor, getWhereSchemaFor} from '../filter-schema';
import {resolveSchema} from '../generate-schema';
import {OAI3Keys} from '../keys';
import {
Expand Down Expand Up @@ -437,6 +439,53 @@ export namespace param {
schema: {type: 'array', items: itemSpec},
});
};

/**
* Sugar decorator for `filter` query parameter
*
* @example
* ```ts
* async find(
* @param.filter(modelCtor)) filter?: Filter<T>,
* ): Promise<(T & Relations)[]> {
* // ...
* }
* ```
* @param modelCtor - Model class
* @param options - Options to customize the parameter name or filter schema
*
*/
export function filter(
modelCtor: typeof Model,
options?: string | (FilterSchemaOptions & {name?: string}),
) {
let name = 'filter';
if (typeof options === 'string') {
name = options;
options = {};
}
name = options?.name ?? name;
return param.query.object(name, getFilterSchemaFor(modelCtor, options));
}

/**
* Sugar decorator for `where` query parameter
*
* @example
* ```ts
* async count(
* @param.where(modelCtor)) where?: Where<T>,
* ): Promise<Count> {
* // ...
* }
* ```
* @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 {
Expand Down

0 comments on commit f61896e

Please sign in to comment.