diff --git a/packages/rest/src/rest-application.ts b/packages/rest/src/rest-application.ts index 1894beeab647..251901517e6c 100644 --- a/packages/rest/src/rest-application.ts +++ b/packages/rest/src/rest-application.ts @@ -9,6 +9,9 @@ import {SequenceHandler, SequenceFunction} from './sequence'; import {Binding, Constructor} from '@loopback/context'; import {format} from 'util'; import {RestBindings} from './keys'; +import {RouteEntry, RestServer} from '.'; +import {ControllerClass} from './router/routing-table'; +import {OperationObject, OpenApiSpec} from '@loopback/openapi-spec'; export const ERR_NO_MULTI_SERVER = format( 'RestApplication does not support multiple servers!', @@ -46,7 +49,79 @@ export class RestApplication extends Application { handler(handlerFn: SequenceFunction) { // FIXME(kjdelisle): I attempted to mimic the pattern found in RestServer // with no success, so until I've got a better way, this is functional. - const server = this.getSync('servers.RestServer'); + const server: RestServer = this.getSync('servers.RestServer'); server.handler(handlerFn); } + + /** + * Register a new Controller-based route. + * + * ```ts + * class MyController { + * greet(name: string) { + * return `hello ${name}`; + * } + * } + * app.route('get', '/greet', operationSpec, MyController, 'greet'); + * ``` + * + * @param verb HTTP verb of the endpoint + * @param path URL path of the endpoint + * @param spec The OpenAPI spec describing the endpoint (operation) + * @param controller Controller constructor + * @param methodName The name of the controller method + */ + route( + verb: string, + path: string, + spec: OperationObject, + controller: ControllerClass, + methodName: string, + ): Binding; + + /** + * Register a new route. + * + * ```ts + * function greet(name: string) { + * return `hello ${name}`; + * } + * const route = new Route('get', '/', operationSpec, greet); + * app.route(route); + * ``` + * + * @param route The route to add. + */ + route(route: RouteEntry): Binding; + + route( + routeOrVerb: RouteEntry | string, + path?: string, + spec?: OperationObject, + controller?: ControllerClass, + methodName?: string, + ): Binding { + // FIXME(bajtos): This is a workaround based on app.handler() above + const server: RestServer = this.getSync('servers.RestServer'); + if (typeof routeOrVerb === 'object') { + return server.route(routeOrVerb); + } else { + return server.route(routeOrVerb, path!, spec!, controller!, methodName!); + } + } + + /** + * Set the OpenAPI specification that defines the REST API schema for this + * application. All routes, parameter definitions and return types will be + * defined in this way. + * + * Note that this will override any routes defined via decorators at the + * controller level (this function takes precedent). + * + * @param {OpenApiSpec} spec The OpenAPI specification, as an object. + * @returns {Binding} + */ + api(spec: OpenApiSpec): Binding { + return this.bind(RestBindings.API_SPEC).to(spec); + } } diff --git a/packages/rest/src/rest-component.ts b/packages/rest/src/rest-component.ts index 0de73e9dd447..27aab58a4177 100644 --- a/packages/rest/src/rest-component.ts +++ b/packages/rest/src/rest-component.ts @@ -23,6 +23,8 @@ import { SendProvider, } from './providers'; import {RestServer, RestServerConfig} from './rest-server'; +import {DefaultSequence} from '.'; +import {createEmptyApiSpec} from '@loopback/openapi-spec'; export class RestComponent implements Component { providers: ProviderMap = { @@ -45,7 +47,8 @@ export class RestComponent implements Component { @inject(CoreBindings.APPLICATION_INSTANCE) app: Application, @inject(RestBindings.CONFIG) config?: RestComponentConfig, ) { - if (!config) config = {}; + app.bind(RestBindings.SEQUENCE).toClass(DefaultSequence); + app.bind(RestBindings.API_SPEC).to(createEmptyApiSpec()); } } diff --git a/packages/rest/src/rest-server.ts b/packages/rest/src/rest-server.ts index 328cbf7cb32b..6cadcc4c7215 100644 --- a/packages/rest/src/rest-server.ts +++ b/packages/rest/src/rest-server.ts @@ -9,11 +9,7 @@ import {safeDump} from 'js-yaml'; import {Binding, Context, Constructor, inject} from '@loopback/context'; import {Route, ControllerRoute, RouteEntry} from './router/routing-table'; import {ParsedRequest} from './internal-types'; -import { - OpenApiSpec, - createEmptyApiSpec, - OperationObject, -} from '@loopback/openapi-spec'; +import {OpenApiSpec, OperationObject} from '@loopback/openapi-spec'; import {ServerRequest, ServerResponse, createServer} from 'http'; import * as Http from 'http'; import {Application, CoreBindings, Server} from '@loopback/core'; @@ -141,9 +137,10 @@ export class RestServer extends Context implements Server { } this.bind(RestBindings.PORT).to(options.port); this.bind(RestBindings.HOST).to(options.host); - this.api(createEmptyApiSpec()); - this.sequence(options.sequence ? options.sequence : DefaultSequence); + if (options.sequence) { + this.sequence(options.sequence); + } this.handleHttp = (req: ServerRequest, res: ServerResponse) => { try { diff --git a/packages/rest/test/acceptance/routing/routing.acceptance.ts b/packages/rest/test/acceptance/routing/routing.acceptance.ts index f0c25fdf1b7a..c665efd8f0ea 100644 --- a/packages/rest/test/acceptance/routing/routing.acceptance.ts +++ b/packages/rest/test/acceptance/routing/routing.acceptance.ts @@ -10,6 +10,7 @@ import { RestBindings, RestServer, RestComponent, + RestApplication, } from '../../..'; import {api, get, param} from '@loopback/openapi-v2'; @@ -487,6 +488,83 @@ describe('Routing', () => { await client.get('/greet?name=world').expect(200, 'hello world'); }); + describe('RestApplication', () => { + it('supports function-based routes declared via app.route()', async () => { + const app = new RestApplication(); + + const routeSpec = { + parameters: [ + {name: 'name', in: 'query', type: 'string'}, + ], + responses: { + 200: { + description: 'greeting text', + schema: {type: 'string'}, + }, + }, + }; + + function greet(name: string) { + return `hello ${name}`; + } + + const route = new Route('get', '/greet', routeSpec, greet); + app.route(route); + + const server = await givenAServer(app); + const client = whenIMakeRequestTo(server); + await client.get('/greet?name=world').expect(200, 'hello world'); + }); + + it('supports controller routes declared via app.api()', async () => { + const app = new RestApplication(); + + class MyController { + greet(name: string) { + return `hello ${name}`; + } + } + + const spec = anOpenApiSpec() + .withOperation( + 'get', + '/greet', + anOperationSpec() + .withParameter({name: 'name', in: 'query', type: 'string'}) + .withExtension('x-operation-name', 'greet') + .withExtension('x-controller-name', 'MyController'), + ) + .build(); + + app.api(spec); + app.controller(MyController); + + const server = await givenAServer(app); + const client = whenIMakeRequestTo(server); + await client.get('/greet?name=world').expect(200, 'hello world'); + }); + + it('supports controller routes defined via app.route()', async () => { + const app = new RestApplication(); + + class MyController { + greet(name: string) { + return `hello ${name}`; + } + } + + const spec = anOperationSpec() + .withParameter({name: 'name', in: 'query', type: 'string'}) + .build(); + + app.route('get', '/greet', spec, MyController, 'greet'); + + const server = await givenAServer(app); + const client = whenIMakeRequestTo(server); + await client.get('/greet?name=world').expect(200, 'hello world'); + }); + }); + /* ===== HELPERS ===== */ function givenAnApplication() { diff --git a/packages/rest/test/acceptance/sequence/sequence.acceptance.ts b/packages/rest/test/acceptance/sequence/sequence.acceptance.ts index 8f36d2484c54..c44411bee688 100644 --- a/packages/rest/test/acceptance/sequence/sequence.acceptance.ts +++ b/packages/rest/test/acceptance/sequence/sequence.acceptance.ts @@ -16,6 +16,7 @@ import { RestBindings, RestServer, RestComponent, + RestApplication, } from '../../..'; import {api} from '@loopback/openapi-v2'; import {Application} from '@loopback/core'; @@ -36,7 +37,7 @@ describe('Sequence', () => { let server: RestServer; beforeEach(givenAppWithController); it('provides a default sequence', async () => { - whenIMakeRequestTo(app) + whenIRequest() .get('/name') .expect('SequenceApp'); }); @@ -45,7 +46,7 @@ describe('Sequence', () => { server.handler((sequence, request, response) => { sequence.send(response, 'hello world'); }); - return whenIMakeRequestTo(app) + return whenIRequest() .get('/') .expect('hello world'); }); @@ -61,7 +62,7 @@ describe('Sequence', () => { // bind user defined sequence server.sequence(MySequence); - whenIMakeRequestTo(app) + whenIRequest() .get('/') .expect('hello world'); }); @@ -86,11 +87,29 @@ describe('Sequence', () => { server.sequence(MySequence); - return whenIMakeRequestTo(app) + return whenIRequest() .get('/name') .expect('MySequence SequenceApp'); }); + it('allows users to bind a custom sequence class via app.sequence()', async () => { + class MySequence { + constructor(@inject(SequenceActions.SEND) protected send: Send) {} + + async handle(req: ParsedRequest, res: ServerResponse) { + this.send(res, 'MySequence was invoked.'); + } + } + + const restApp = new RestApplication(); + restApp.sequence(MySequence); + + const appServer = await restApp.getServer(RestServer); + await whenIRequest(appServer) + .get('/name') + .expect('MySequence was invoked.'); + }); + it('user-defined Send', () => { const send: Send = (response, result) => { response.setHeader('content-type', 'text/plain'); @@ -98,7 +117,7 @@ describe('Sequence', () => { }; server.bind(SequenceActions.SEND).to(send); - return whenIMakeRequestTo(app) + return whenIRequest() .get('/name') .expect('CUSTOM FORMAT: SequenceApp'); }); @@ -110,7 +129,7 @@ describe('Sequence', () => { }; server.bind(SequenceActions.REJECT).to(reject); - return whenIMakeRequestTo(app) + return whenIRequest() .get('/unknown-url') .expect(418); }); @@ -122,7 +141,7 @@ describe('Sequence', () => { sequence.send(response, sequence.ctx.getSync('test')); }); - return whenIMakeRequestTo(app) + return whenIRequest() .get('/') .expect('hello world'); }); @@ -149,7 +168,7 @@ describe('Sequence', () => { server.sequence(MySequence); app.bind('test').to('hello world'); - return whenIMakeRequestTo(app) + return whenIRequest() .get('/') .expect('hello world'); }); @@ -194,7 +213,7 @@ describe('Sequence', () => { app.controller(controller); } - function whenIMakeRequestTo(application: Application): Client { - return createClientForHandler(server.handleHttp); + function whenIRequest(restServer: RestServer = server): Client { + return createClientForHandler(restServer.handleHttp); } });