diff --git a/packages/rest/src/http-handler.ts b/packages/rest/src/http-handler.ts index 6ba348ef7266..34b0bc238832 100644 --- a/packages/rest/src/http-handler.ts +++ b/packages/rest/src/http-handler.ts @@ -4,9 +4,9 @@ // License text available at https://opensource.org/licenses/MIT import {Context} from '@loopback/context'; -import {PathsObject} from '@loopback/openapi-spec'; +import {PathsObject, ControllerSpec} from '@loopback/openapi-spec'; import {ServerRequest, ServerResponse} from 'http'; -import {ControllerSpec} from './router/metadata'; +// import {ControllerSpec} from './router/metadata'; import {SequenceHandler} from './sequence'; import { diff --git a/packages/rest/src/index.ts b/packages/rest/src/index.ts index abf3ab3e38be..90ba07d2b275 100644 --- a/packages/rest/src/index.ts +++ b/packages/rest/src/index.ts @@ -16,7 +16,7 @@ export { parseRequestUrl, } from './router/routing-table'; -export * from './router/metadata'; +export * from '@loopback/openapi-spec'; export * from './providers'; // import all errors from external http-errors package diff --git a/packages/rest/src/rest-server.ts b/packages/rest/src/rest-server.ts index 78d562b28c3a..6103856434f3 100644 --- a/packages/rest/src/rest-server.ts +++ b/packages/rest/src/rest-server.ts @@ -16,7 +16,8 @@ import { import {ServerRequest, ServerResponse, createServer} from 'http'; import * as Http from 'http'; import {Application, CoreBindings, Server} from '@loopback/core'; -import {getControllerSpec} from './router/metadata'; +import {getControllerSpec} from '@loopback/openapi-spec'; +// import {getControllerSpec} from './router/metadata'; import {HttpHandler} from './http-handler'; import {DefaultSequence, SequenceHandler, SequenceFunction} from './sequence'; import { diff --git a/packages/rest/src/router/metadata.ts b/packages/rest/src/router/metadata.ts deleted file mode 100644 index 912afbda4502..000000000000 --- a/packages/rest/src/router/metadata.ts +++ /dev/null @@ -1,540 +0,0 @@ -// Copyright IBM Corp. 2017. All Rights Reserved. -// Node module: @loopback/rest -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import * as assert from 'assert'; -import * as _ from 'lodash'; - -import {Reflector} from '@loopback/context'; -import { - OperationObject, - ParameterLocation, - ParameterObject, - SchemaObject, - ParameterType, - PathsObject, -} from '@loopback/openapi-spec'; - -const debug = require('debug')('loopback:core:router:metadata'); - -const ENDPOINTS_KEY = 'rest:endpoints'; -const API_SPEC_KEY = 'rest:api-spec'; - -// tslint:disable:no-any - -function cloneDeep(val: T): T { - if (val === undefined) { - return {} as T; - } - return _.cloneDeepWith(val, v => { - // Do not clone functions - if (typeof v === 'function') return v; - return undefined; - }); -} - -export interface ControllerSpec { - /** - * The base path on which the Controller API is served. - * If it is not included, the API is served directly under the host. - * The value MUST start with a leading slash (/). - */ - basePath?: string; - - /** - * The available paths and operations for the API. - */ - paths: PathsObject; -} - -/** - * Decorate the given Controller constructor with metadata describing - * the HTTP/REST API the Controller implements/provides. - * - * `@api` can be applied to controller classes. For example, - * ``` - * @api({basePath: '/my'}) - * class MyController { - * // ... - * } - * ``` - * - * @param spec OpenAPI specification describing the endpoints - * handled by this controller - * - * @decorator - */ -export function api(spec: ControllerSpec) { - return function(constructor: Function) { - assert( - typeof constructor === 'function', - 'The @api decorator can be applied to constructors only.', - ); - const apiSpec = resolveControllerSpec(constructor, spec); - Reflector.defineMetadata(API_SPEC_KEY, apiSpec, constructor); - }; -} - -/** - * Data structure for REST related metadata - */ -interface RestEndpoint { - verb: string; - path: string; - spec?: OperationObject; - target: any; -} - -function getEndpoints( - target: any, -): {[property: string]: Partial} { - let endpoints = Reflector.getOwnMetadata(ENDPOINTS_KEY, target); - if (!endpoints) { - // Clone the endpoints so that subclasses won't mutate the metadata - // in the base class - const baseEndpoints = Reflector.getMetadata(ENDPOINTS_KEY, target); - endpoints = cloneDeep(baseEndpoints); - Reflector.defineMetadata(ENDPOINTS_KEY, endpoints, target); - } - return endpoints; -} - -/** - * Build the api spec from class and method level decorations - * @param constructor Controller class - * @param spec API spec - */ -function resolveControllerSpec( - constructor: Function, - spec?: ControllerSpec, -): ControllerSpec { - debug(`Retrieving OpenAPI specification for controller ${constructor.name}`); - - if (spec) { - debug(' using class-level spec defined via @api()', spec); - spec = cloneDeep(spec); - } else { - spec = {paths: {}}; - } - - const endpoints = getEndpoints(constructor.prototype); - - for (const op in endpoints) { - const endpoint = endpoints[op]; - const verb = endpoint.verb!; - const path = endpoint.path!; - - let endpointName = ''; - if (debug.enabled) { - const className = - endpoint.target.constructor.name || - constructor.name || - ''; - const fullMethodName = `${className}.${op}`; - endpointName = `${fullMethodName} (${verb} ${path})`; - } - - let operationSpec = endpoint.spec; - if (!operationSpec) { - // The operation was defined via @operation(verb, path) with no spec - operationSpec = { - responses: {}, - }; - endpoint.spec = operationSpec; - } - - operationSpec['x-operation-name'] = op; - - if (!spec.paths[path]) { - spec.paths[path] = {}; - } - - if (spec.paths[path][verb]) { - // Operations from subclasses override those from the base - debug(` Overriding ${endpointName} - endpoint was already defined`); - } - - debug(` adding ${endpointName}`, operationSpec); - spec.paths[path][verb] = operationSpec; - } - return spec; -} - -/** - * Get the controller spec for the given class - * @param constructor Controller class - */ -export function getControllerSpec(constructor: Function): ControllerSpec { - let spec = Reflector.getOwnMetadata(API_SPEC_KEY, constructor); - if (!spec) { - spec = resolveControllerSpec(constructor, spec); - Reflector.defineMetadata(API_SPEC_KEY, spec, constructor); - } - return spec; -} - -/** - * Expose a Controller method as a REST API operation - * mapped to `GET` request method. - * - * @param path The URL path of this operation, e.g. `/product/{id}` - * @param spec The OpenAPI specification describing parameters and responses - * of this operation. - */ -export function get(path: string, spec?: OperationObject) { - return operation('get', path, spec); -} - -/** - * Expose a Controller method as a REST API operation - * mapped to `POST` request method. - * - * @param path The URL path of this operation, e.g. `/product/{id}` - * @param spec The OpenAPI specification describing parameters and responses - * of this operation. - */ -export function post(path: string, spec?: OperationObject) { - return operation('post', path, spec); -} - -/** - * Expose a Controller method as a REST API operation - * mapped to `PUT` request method. - * - * @param path The URL path of this operation, e.g. `/product/{id}` - * @param spec The OpenAPI specification describing parameters and responses - * of this operation. - */ -export function put(path: string, spec?: OperationObject) { - return operation('put', path, spec); -} - -/** - * Expose a Controller method as a REST API operation - * mapped to `PATCH` request method. - * - * @param path The URL path of this operation, e.g. `/product/{id}` - * @param spec The OpenAPI specification describing parameters and responses - * of this operation. - */ -export function patch(path: string, spec?: OperationObject) { - return operation('patch', path, spec); -} - -/** - * Expose a Controller method as a REST API operation - * mapped to `DELETE` request method. - * - * @param path The URL path of this operation, e.g. `/product/{id}` - * @param spec The OpenAPI specification describing parameters and responses - * of this operation. - */ -export function del(path: string, spec?: OperationObject) { - return operation('delete', path, spec); -} - -/** - * Expose a Controller method as a REST API operation. - * - * @param verb HTTP verb, e.g. `GET` or `POST`. - * @param path The URL path of this operation, e.g. `/product/{id}` - * @param spec The OpenAPI specification describing parameters and responses - * of this operation. - */ -export function operation(verb: string, path: string, spec?: OperationObject) { - return function( - target: any, - propertyKey: string, - descriptor: PropertyDescriptor, - ) { - assert( - typeof target[propertyKey] === 'function', - '@operation decorator can be applied to methods only', - ); - - const endpoints = getEndpoints(target); - let endpoint = endpoints[propertyKey]; - if (!endpoint || endpoint.target !== target) { - // Add the new endpoint metadata for the method - endpoint = {verb, path, spec, target}; - endpoints[propertyKey] = endpoint; - } else { - // Update the endpoint metadata - // It can be created by @param - endpoint.verb = verb; - endpoint.path = path; - endpoint.target = target; - } - - if (!spec) { - // Users can define parameters and responses using decorators - return; - } - - // Decorator are invoked in reverse order of their definition. - // For example, a method decorated with @operation() @param() - // will invoke param() decorator first and operation() second. - // As a result, we need to preserve any partial definitions - // already provided by other decorators. - editOperationSpec(endpoint, overrides => { - const mergedSpec = Object.assign({}, spec, overrides); - - // Merge "responses" definitions - mergedSpec.responses = Object.assign( - {}, - spec.responses, - overrides.responses, - ); - - return mergedSpec; - }); - }; -} - -const paramDecoratorStyle = Symbol('ParamDecoratorStyle'); - -/** - * Describe an input parameter of a Controller method. - * - * `@param` can be applied to method itself or specific parameters. For example, - * ``` - * class MyController { - * @get('/') - * @param(offsetSpec) - * @param(pageSizeSpec) - * list(offset?: number, pageSize?: number) {} - * } - * ``` - * or - * ``` - * class MyController { - * @get('/') - * list( - * @param(offsetSpec) offset?: number, - * @param(pageSizeSpec) pageSize?: number, - * ) {} - * } - * ``` - * Please note mixed usage of `@param` at method/parameter level is not allowed. - * - * @param paramSpec Parameter specification. - */ -export function param(paramSpec: ParameterObject) { - return function( - target: any, - propertyKey: string, - descriptorOrParameterIndex: PropertyDescriptor | number, - ) { - assert( - typeof target[propertyKey] === 'function', - '@param decorator can be applied to methods only', - ); - - const endpoints = getEndpoints(target); - let endpoint = endpoints[propertyKey]; - if (!endpoint || endpoint.target !== target) { - const baseEndpoint = endpoint; - // Add the new endpoint metadata for the method - endpoint = cloneDeep(baseEndpoint); - endpoint.target = target; - endpoints[propertyKey] = endpoint; - } - - editOperationSpec(endpoint, operationSpec => { - let decoratorStyle; - if (typeof descriptorOrParameterIndex === 'number') { - decoratorStyle = 'parameter'; - } else { - decoratorStyle = 'method'; - } - if (!operationSpec.parameters) { - operationSpec.parameters = []; - // Record the @param decorator style to ensure consistency - operationSpec[paramDecoratorStyle] = decoratorStyle; - } else { - // Mixed usage of @param at method/parameter level is not allowed - if (operationSpec[paramDecoratorStyle] !== decoratorStyle) { - throw new Error( - 'Mixed usage of @param at method/parameter level' + - ' is not allowed.', - ); - } - } - - if (typeof descriptorOrParameterIndex === 'number') { - operationSpec.parameters[descriptorOrParameterIndex] = paramSpec; - } else { - operationSpec.parameters.unshift(paramSpec); - } - - return operationSpec; - }); - }; -} - -function editOperationSpec( - endpoint: Partial, - updateFn: (spec: OperationObject) => OperationObject, -) { - let spec = endpoint.spec; - if (!spec) { - spec = { - responses: {}, - }; - } - - spec = updateFn(spec); - endpoint.spec = spec; -} - -export namespace param { - export const query = { - /** - * Define a parameter of "string" type that's read from the query string. - * - * @param name Parameter name. - */ - string: createParamShortcut('query', 'string'), - - /** - * Define a parameter of "number" type that's read from the query string. - * - * @param name Parameter name. - */ - number: createParamShortcut('query', 'number'), - - /** - * Define a parameter of "integer" type that's read from the query string. - * - * @param name Parameter name. - */ - integer: createParamShortcut('query', 'integer'), - - /** - * Define a parameter of "boolean" type that's read from the query string. - * - * @param name Parameter name. - */ - boolean: createParamShortcut('query', 'boolean'), - }; - - export const header = { - /** - * Define a parameter of "string" type that's read from a request header. - * - * @param name Parameter name, it must match the header name - * (e.g. `Content-Type`). - */ - string: createParamShortcut('header', 'string'), - - /** - * Define a parameter of "number" type that's read from a request header. - * - * @param name Parameter name, it must match the header name - * (e.g. `Content-Length`). - */ - number: createParamShortcut('header', 'number'), - - /** - * Define a parameter of "integer" type that's read from a request header. - * - * @param name Parameter name, it must match the header name - * (e.g. `Content-Length`). - */ - integer: createParamShortcut('header', 'integer'), - - /** - * Define a parameter of "boolean" type that's read from a request header. - * - * @param name Parameter name, it must match the header name, - * (e.g. `DNT` or `X-Do-Not-Track`). - */ - boolean: createParamShortcut('header', 'boolean'), - }; - - export const path = { - /** - * Define a parameter of "string" type that's read from request path. - * - * @param name Parameter name matching one of the placeholders in the path - * string. - */ - string: createParamShortcut('path', 'string'), - - /** - * Define a parameter of "number" type that's read from request path. - * - * @param name Parameter name matching one of the placeholders in the path - * string. - */ - number: createParamShortcut('path', 'number'), - - /** - * Define a parameter of "integer" type that's read from request path. - * - * @param name Parameter name matching one of the placeholders in the path - * string. - */ - integer: createParamShortcut('path', 'integer'), - - /** - * Define a parameter of "boolean" type that's read from request path. - * - * @param name Parameter name matching one of the placeholders in the path - * string. - */ - boolean: createParamShortcut('path', 'boolean'), - }; - - export const formData = { - /** - * Define a parameter of "string" type that's read - * from a field in the request body. - * - * @param name Parameter name. - */ - string: createParamShortcut('formData', 'string'), - - /** - * Define a parameter of "number" type that's read - * from a field in the request body. - * - * @param name Parameter name. - */ - number: createParamShortcut('formData', 'number'), - - /** - * Define a parameter of "integer" type that's read - * from a field in the request body. - * - * @param name Parameter name. - */ - integer: createParamShortcut('formData', 'integer'), - - /** - * Define a parameter of "boolean" type that's read - * from a field in the request body. - * - * @param name Parameter name. - */ - boolean: createParamShortcut('formData', 'boolean'), - }; - - /** - * Define a parameter that's set to the full request body. - * - * @param name Parameter name - * @param schema The schema defining the type used for the body parameter. - */ - export const body = function(name: string, schema: SchemaObject) { - return param({name, in: 'body', schema}); - }; -} - -function createParamShortcut(source: ParameterLocation, type: ParameterType) { - // TODO(bajtos) @param.IN.TYPE('foo', {required: true}) - return (name: string) => { - return param({name, in: source, type}); - }; -} diff --git a/packages/rest/src/router/routing-table.ts b/packages/rest/src/router/routing-table.ts index 846495e27b6c..1c608a2de87d 100644 --- a/packages/rest/src/router/routing-table.ts +++ b/packages/rest/src/router/routing-table.ts @@ -19,7 +19,8 @@ import { OperationRetval, } from '../internal-types'; -import {ControllerSpec} from './metadata'; +import {ControllerSpec} from '@loopback/openapi-spec'; +// import {ControllerSpec} from './metadata'; import * as assert from 'assert'; import * as url from 'url'; diff --git a/packages/rest/test/unit/router/metadata.test.ts b/packages/rest/test/unit/router/metadata.test.ts deleted file mode 100644 index c00653602e08..000000000000 --- a/packages/rest/test/unit/router/metadata.test.ts +++ /dev/null @@ -1,357 +0,0 @@ -// Copyright IBM Corp. 2013,2017. All Rights Reserved. -// Node module: @loopback/rest -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import { - get, - api, - getControllerSpec, - operation, - post, - put, - patch, - del, - param, -} from '../../..'; -import {expect} from '@loopback/testlab'; -import {anOpenApiSpec, anOperationSpec} from '@loopback/openapi-spec-builder'; - -describe('Routing metadata', () => { - it('returns spec defined via @api()', () => { - const expectedSpec = anOpenApiSpec() - .withOperationReturningString('get', '/greet', 'greet') - .build(); - - @api(expectedSpec) - class MyController { - greet() { - return 'Hello world!'; - } - } - - const actualSpec = getControllerSpec(MyController); - expect(actualSpec).to.eql(expectedSpec); - }); - - it('returns spec defined via @get decorator', () => { - const operationSpec = anOperationSpec() - .withStringResponse() - .build(); - - class MyController { - @get('/greet', operationSpec) - greet() { - return 'Hello world!'; - } - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec).to.eql({ - paths: { - '/greet': { - get: { - 'x-operation-name': 'greet', - ...operationSpec, - }, - }, - }, - }); - }); - - it('returns spec defined via @post decorator', () => { - const operationSpec = anOperationSpec() - .withStringResponse() - .build(); - - class MyController { - @post('/greeting', operationSpec) - createGreeting() {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec).to.eql({ - paths: { - '/greeting': { - post: { - 'x-operation-name': 'createGreeting', - ...operationSpec, - }, - }, - }, - }); - }); - - it('returns spec defined via @put decorator', () => { - const operationSpec = anOperationSpec() - .withStringResponse() - .build(); - - class MyController { - @put('/greeting', operationSpec) - updateGreeting() {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec).to.eql({ - paths: { - '/greeting': { - put: { - 'x-operation-name': 'updateGreeting', - ...operationSpec, - }, - }, - }, - }); - }); - - it('returns spec defined via @patch decorator', () => { - const operationSpec = anOperationSpec() - .withStringResponse() - .build(); - - class MyController { - @patch('/greeting', operationSpec) - patchGreeting() {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec).to.eql({ - paths: { - '/greeting': { - patch: { - 'x-operation-name': 'patchGreeting', - ...operationSpec, - }, - }, - }, - }); - }); - - it('returns spec defined via @del decorator', () => { - const operationSpec = anOperationSpec() - .withStringResponse() - .build(); - - class MyController { - @del('/greeting', operationSpec) - deleteGreeting() {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec).to.eql({ - paths: { - '/greeting': { - delete: { - 'x-operation-name': 'deleteGreeting', - ...operationSpec, - }, - }, - }, - }); - }); - - it('returns spec defined via @operation decorator', () => { - const operationSpec = anOperationSpec() - .withStringResponse() - .build(); - - class MyController { - @operation('post', '/greeting', operationSpec) - createGreeting() {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec).to.eql({ - paths: { - '/greeting': { - post: { - 'x-operation-name': 'createGreeting', - ...operationSpec, - }, - }, - }, - }); - }); - - it('returns default spec for @get with no spec', () => { - class MyController { - @get('/greet') - greet() {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet']['get']).to.eql({ - 'x-operation-name': 'greet', - responses: {}, - }); - }); - - it('returns default spec for @operation with no spec', () => { - class MyController { - @operation('post', '/greeting') - createGreeting() {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greeting']['post']).to.eql({ - 'x-operation-name': 'createGreeting', - responses: {}, - }); - }); - - it('honours specifications from inherited methods', () => { - const operationSpec = anOperationSpec() - .withStringResponse() - .build(); - - class Parent { - @get('/parent', operationSpec) - getParentName() { - return 'The Parent'; - } - } - - class Child extends Parent { - @get('/child', operationSpec) - getChildName() { - return 'The Child'; - } - } - - const actualSpec = getControllerSpec(Child); - - expect(actualSpec).to.eql({ - paths: { - '/parent': { - get: { - 'x-operation-name': 'getParentName', - ...operationSpec, - }, - }, - '/child': { - get: { - 'x-operation-name': 'getChildName', - ...operationSpec, - }, - }, - }, - }); - }); - - it('allows children to override parent REST endpoints', () => { - const operationSpec = anOperationSpec() - .withStringResponse() - .build(); - - class Parent { - @get('/name', operationSpec) - getParentName() { - return 'The Parent'; - } - } - - class Child extends Parent { - @get('/name', operationSpec) - getChildName() { - return 'The Child'; - } - } - - const actualSpec = getControllerSpec(Child); - - expect(actualSpec.paths['/name']['get']).to.have.property( - 'x-operation-name', - 'getChildName', - ); - }); - - it('allows children to override parent REST operations', () => { - const operationSpec = anOperationSpec() - .withStringResponse() - .build(); - - class Parent { - @get('/parent-name', operationSpec) - getName() { - return 'The Parent'; - } - } - - class Child extends Parent { - @get('/child-name', operationSpec) - getName() { - return 'The Child'; - } - } - - const childSpec = getControllerSpec(Child); - const parentSpec = getControllerSpec(Parent); - - expect(childSpec.paths['/child-name']['get']).to.have.property( - 'x-operation-name', - 'getName', - ); - - // The parent endpoint has been overridden - expect(childSpec.paths).to.not.have.property('/parent-name'); - - expect(parentSpec.paths['/parent-name']['get']).to.have.property( - 'x-operation-name', - 'getName', - ); - - // The parent endpoint should not be polluted - expect(parentSpec.paths).to.not.have.property('/child-name'); - }); - - it('allows children to override parent REST parameters', () => { - const operationSpec = anOperationSpec() - .withStringResponse() - .build(); - - class Parent { - @get('/greet', operationSpec) - greet(@param.query.string('msg') msg: string) { - return `Parent: ${msg}`; - } - } - - class Child extends Parent { - greet(@param.query.string('message') msg: string) { - return `Child: ${msg}`; - } - } - - const childSpec = getControllerSpec(Child); - const parentSpec = getControllerSpec(Parent); - - const childGreet = childSpec.paths['/greet']['get']; - expect(childGreet).to.have.property('x-operation-name', 'greet'); - - expect(childGreet.parameters).to.have.property('length', 1); - - expect(childGreet.parameters[0]).to.containEql({ - name: 'message', - in: 'query', - }); - - const parentGreet = parentSpec.paths['/greet']['get']; - expect(parentGreet).to.have.property('x-operation-name', 'greet'); - - expect(parentGreet.parameters).to.have.property('length', 1); - - expect(parentGreet.parameters[0]).to.containEql({ - name: 'msg', - in: 'query', - }); - }); -}); diff --git a/packages/rest/test/unit/router/metadata/param-body.test.ts b/packages/rest/test/unit/router/metadata/param-body.test.ts deleted file mode 100644 index 82c0c85d98e4..000000000000 --- a/packages/rest/test/unit/router/metadata/param-body.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright IBM Corp. 2013,2017. All Rights Reserved. -// Node module: @loopback/rest -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {post, param, getControllerSpec} from '../../../..'; -import {expect} from '@loopback/testlab'; - -describe('Routing metadata for parameters', () => { - describe('@param.body', () => { - it('defines a parameter with in:body', () => { - class MyController { - @post('/greeting') - @param.body('data', {type: 'object'}) - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greeting']['post'].parameters).to.eql([ - { - name: 'data', - in: 'body', - schema: {type: 'object'}, - }, - ]); - }); - }); -}); diff --git a/packages/rest/test/unit/router/metadata/param-form-data.test.ts b/packages/rest/test/unit/router/metadata/param-form-data.test.ts deleted file mode 100644 index 0cd239204ce3..000000000000 --- a/packages/rest/test/unit/router/metadata/param-form-data.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright IBM Corp. 2013,2017. All Rights Reserved. -// Node module: @loopback/rest -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {post, param, getControllerSpec} from '../../../..'; -import {expect} from '@loopback/testlab'; - -describe('Routing metadata for parameters', () => { - describe('@param.formData.string', () => { - it('defines a parameter with in:formData type:string', () => { - class MyController { - @post('/greeting') - @param.formData.string('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greeting']['post'].parameters).to.eql([ - { - name: 'name', - type: 'string', - in: 'formData', - }, - ]); - }); - }); - - describe('@param.formData.number', () => { - it('defines a parameter with in:formData type:number', () => { - class MyController { - @post('/greeting') - @param.formData.number('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greeting']['post'].parameters).to.eql([ - { - name: 'name', - type: 'number', - in: 'formData', - }, - ]); - }); - }); - - describe('@param.formData.integer', () => { - it('defines a parameter with in:formData type:integer', () => { - class MyController { - @post('/greeting') - @param.formData.integer('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greeting']['post'].parameters).to.eql([ - { - name: 'name', - type: 'integer', - in: 'formData', - }, - ]); - }); - }); - - describe('@param.formData.boolean', () => { - it('defines a parameter with in:formData type:boolean', () => { - class MyController { - @post('/greeting') - @param.formData.boolean('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greeting']['post'].parameters).to.eql([ - { - name: 'name', - type: 'boolean', - in: 'formData', - }, - ]); - }); - }); -}); diff --git a/packages/rest/test/unit/router/metadata/param-header.test.ts b/packages/rest/test/unit/router/metadata/param-header.test.ts deleted file mode 100644 index e993d56c23f9..000000000000 --- a/packages/rest/test/unit/router/metadata/param-header.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright IBM Corp. 2013,2017. All Rights Reserved. -// Node module: @loopback/rest -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {get, param, getControllerSpec} from '../../../..'; -import {expect} from '@loopback/testlab'; - -describe('Routing metadata for parameters', () => { - describe('@param.header.string', () => { - it('defines a parameter with in:header type:string', () => { - class MyController { - @get('/greet') - @param.header.string('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet']['get'].parameters).to.eql([ - { - name: 'name', - type: 'string', - in: 'header', - }, - ]); - }); - }); - - describe('@param.header.number', () => { - it('defines a parameter with in:header type:number', () => { - class MyController { - @get('/greet') - @param.header.number('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet']['get'].parameters).to.eql([ - { - name: 'name', - type: 'number', - in: 'header', - }, - ]); - }); - }); - - describe('@param.header.integer', () => { - it('defines a parameter with in:header type:integer', () => { - class MyController { - @get('/greet') - @param.header.integer('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet']['get'].parameters).to.eql([ - { - name: 'name', - type: 'integer', - in: 'header', - }, - ]); - }); - }); - - describe('@param.header.boolean', () => { - it('defines a parameter with in:header type:boolean', () => { - class MyController { - @get('/greet') - @param.header.boolean('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet']['get'].parameters).to.eql([ - { - name: 'name', - type: 'boolean', - in: 'header', - }, - ]); - }); - }); -}); diff --git a/packages/rest/test/unit/router/metadata/param-path.test.ts b/packages/rest/test/unit/router/metadata/param-path.test.ts deleted file mode 100644 index 4db8bb8de30a..000000000000 --- a/packages/rest/test/unit/router/metadata/param-path.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright IBM Corp. 2013,2017. All Rights Reserved. -// Node module: @loopback/rest -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {get, param, getControllerSpec} from '../../../..'; -import {expect} from '@loopback/testlab'; - -describe('Routing metadata for parameters', () => { - describe('@param.path.string', () => { - it('defines a parameter with in:path type:string', () => { - class MyController { - @get('/greet/{name}') - @param.path.string('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet/{name}']['get'].parameters).to.eql([ - { - name: 'name', - type: 'string', - in: 'path', - }, - ]); - }); - }); - - describe('@param.path.number', () => { - it('defines a parameter with in:path type:number', () => { - class MyController { - @get('/greet/{name}') - @param.path.number('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet/{name}']['get'].parameters).to.eql([ - { - name: 'name', - type: 'number', - in: 'path', - }, - ]); - }); - }); - - describe('@param.path.integer', () => { - it('defines a parameter with in:path type:integer', () => { - class MyController { - @get('/greet/{name}') - @param.path.integer('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet/{name}']['get'].parameters).to.eql([ - { - name: 'name', - type: 'integer', - in: 'path', - }, - ]); - }); - }); - - describe('@param.path.boolean', () => { - it('defines a parameter with in:path type:boolean', () => { - class MyController { - @get('/greet/{name}') - @param.path.boolean('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet/{name}']['get'].parameters).to.eql([ - { - name: 'name', - type: 'boolean', - in: 'path', - }, - ]); - }); - }); -}); diff --git a/packages/rest/test/unit/router/metadata/param-query.test.ts b/packages/rest/test/unit/router/metadata/param-query.test.ts deleted file mode 100644 index 8a7ff881f0dc..000000000000 --- a/packages/rest/test/unit/router/metadata/param-query.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright IBM Corp. 2013,2017. All Rights Reserved. -// Node module: @loopback/rest -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {get, param, getControllerSpec} from '../../../..'; -import {expect} from '@loopback/testlab'; - -describe('Routing metadata for parameters', () => { - describe('@param.query.string', () => { - it('defines a parameter with in:query type:string', () => { - class MyController { - @get('/greet') - @param.query.string('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet']['get'].parameters).to.eql([ - { - name: 'name', - type: 'string', - in: 'query', - }, - ]); - }); - }); - - describe('@param.query.number', () => { - it('defines a parameter with in:query type:number', () => { - class MyController { - @get('/greet') - @param.query.number('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet']['get'].parameters).to.eql([ - { - name: 'name', - type: 'number', - in: 'query', - }, - ]); - }); - }); - - describe('@param.query.integer', () => { - it('defines a parameter with in:query type:integer', () => { - class MyController { - @get('/greet') - @param.query.integer('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet']['get'].parameters).to.eql([ - { - name: 'name', - type: 'integer', - in: 'query', - }, - ]); - }); - }); - - describe('@param.query.boolean', () => { - it('defines a parameter with in:query type:boolean', () => { - class MyController { - @get('/greet') - @param.query.boolean('name') - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/greet']['get'].parameters).to.eql([ - { - name: 'name', - type: 'boolean', - in: 'query', - }, - ]); - }); - }); -}); diff --git a/packages/rest/test/unit/router/metadata/param.test.ts b/packages/rest/test/unit/router/metadata/param.test.ts deleted file mode 100644 index 61d54377852d..000000000000 --- a/packages/rest/test/unit/router/metadata/param.test.ts +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright IBM Corp. 2013,2017. All Rights Reserved. -// Node module: @loopback/rest -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {get, param, getControllerSpec, operation} from '../../../..'; -import { - OperationObject, - ParameterObject, - ResponsesObject, -} from '@loopback/openapi-spec'; -import {expect} from '@loopback/testlab'; -import {anOperationSpec} from '@loopback/openapi-spec-builder'; - -describe('Routing metadata for parameters', () => { - describe('@param', () => { - it('defines a new parameter', () => { - const paramSpec: ParameterObject = { - name: 'name', - type: 'string', - in: 'query', - }; - - class MyController { - @get('/greet') - @param(paramSpec) - greet(name: string) {} - } - - const actualSpec = getControllerSpec(MyController); - - const expectedSpec = anOperationSpec() - .withOperationName('greet') - .withParameter(paramSpec) - .build(); - - expect(actualSpec.paths['/greet']['get']).to.eql(expectedSpec); - }); - - it('can define multiple parameters in order', () => { - const offsetSpec: ParameterObject = { - name: 'offset', - type: 'number', - in: 'query', - }; - - const pageSizeSpec: ParameterObject = { - name: 'pageSize', - type: 'number', - in: 'query', - }; - - class MyController { - @get('/') - @param(offsetSpec) - @param(pageSizeSpec) - list(offset?: number, pageSize?: number) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/']['get'].parameters).to.eql([ - offsetSpec, - pageSizeSpec, - ]); - }); - - it('can define multiple parameters by arguments', () => { - const offsetSpec: ParameterObject = { - name: 'offset', - type: 'number', - in: 'query', - }; - - const pageSizeSpec: ParameterObject = { - name: 'pageSize', - type: 'number', - in: 'query', - }; - - class MyController { - @get('/') - list( - @param(offsetSpec) offset?: number, - @param(pageSizeSpec) pageSize?: number, - ) {} - } - - const actualSpec = getControllerSpec(MyController); - - expect(actualSpec.paths['/']['get'].parameters).to.eql([ - offsetSpec, - pageSizeSpec, - ]); - }); - // tslint:disable-next-line:max-line-length - it('throws an error if @param is used at both method and parameter level', () => { - expect(() => { - const offsetSpec: ParameterObject = { - name: 'offset', - type: 'number', - in: 'query', - }; - - const pageSizeSpec: ParameterObject = { - name: 'pageSize', - type: 'number', - in: 'query', - }; - // tslint:disable-next-line:no-unused-variable - class MyController { - @get('/') - @param(offsetSpec) - list(offset?: number, @param(pageSizeSpec) pageSize?: number) {} - } - }).to.throw( - /Mixed usage of @param at method\/parameter level is not allowed/, - ); - }); - - it('adds to existing spec provided via @operation', () => { - const offsetSpec: ParameterObject = { - name: 'offset', - type: 'number', - in: 'query', - }; - - const pageSizeSpec: ParameterObject = { - name: 'pageSize', - type: 'number', - in: 'query', - }; - - const responses: ResponsesObject = { - 200: { - schema: { - type: 'string', - }, - description: 'a string response', - }, - }; - - class MyController { - @operation('get', '/', {responses}) - @param(offsetSpec) - @param(pageSizeSpec) - list(offset?: number, pageSize?: number) {} - } - - const apiSpec = getControllerSpec(MyController); - const opSpec: OperationObject = apiSpec.paths['/']['get']; - - expect(opSpec.responses).to.eql(responses); - expect(opSpec.parameters).to.eql([offsetSpec, pageSizeSpec]); - }); - }); -});