From 4a302f6ad06444f6532eed904c5e9208a5f75001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 5 Mar 2018 13:45:29 +0100 Subject: [PATCH] feat(rest): expose app.requestHandler function Each RestServer provides an HTTP handler function that can be used with the core HTTP server or any compatible framework like Express. This change exposes the handler function on the RestApplication class to simplify the usage. BREAKING CHANGE: `RestServer#handleHttp` was renamed to `RestServer#requestHandler`. --- .../test/acceptance/basic-auth.ts | 2 +- .../controller.booter.acceptance.ts | 2 +- .../test/ping.controller.test.ts.ejs | 2 +- packages/cli/test/app-run.test.js | 2 +- .../test/acceptance/application.test.ts | 2 +- .../acceptance/log.extension.acceptance.ts | 15 +++---- packages/rest/src/rest-application.ts | 42 +++++++++++++++---- packages/rest/src/rest-server.ts | 23 +++++++--- .../acceptance/routing/routing.acceptance.ts | 32 +++++++++----- .../sequence/sequence.acceptance.ts | 8 ++-- .../integration/rest-server.integration.ts | 14 +++---- 11 files changed, 94 insertions(+), 50 deletions(-) diff --git a/packages/authentication/test/acceptance/basic-auth.ts b/packages/authentication/test/acceptance/basic-auth.ts index 4848498afcc9..46c0a5f0fda7 100644 --- a/packages/authentication/test/acceptance/basic-auth.ts +++ b/packages/authentication/test/acceptance/basic-auth.ts @@ -188,7 +188,7 @@ describe('Basic Authentication', () => { } function whenIMakeRequestTo(restServer: RestServer): Client { - return createClientForHandler(restServer.handleHttp); + return createClientForHandler(restServer.requestHandler); } }); diff --git a/packages/boot/test/acceptance/controller.booter.acceptance.ts b/packages/boot/test/acceptance/controller.booter.acceptance.ts index 8e9727013e11..439f72ed8482 100644 --- a/packages/boot/test/acceptance/controller.booter.acceptance.ts +++ b/packages/boot/test/acceptance/controller.booter.acceptance.ts @@ -23,7 +23,7 @@ describe('controller booter acceptance tests', () => { await app.start(); const server: RestServer = await app.getServer(RestServer); - const client: Client = createClientForHandler(server.handleHttp); + const client: Client = createClientForHandler(server.requestHandler); // Default Controllers = /controllers with .controller.js ending (nested = true); await client.get('/one').expect(200, 'ControllerOne.one()'); diff --git a/packages/cli/generators/app/templates/test/ping.controller.test.ts.ejs b/packages/cli/generators/app/templates/test/ping.controller.test.ts.ejs index 46101343f04b..c55abfc63d4a 100644 --- a/packages/cli/generators/app/templates/test/ping.controller.test.ts.ejs +++ b/packages/cli/generators/app/templates/test/ping.controller.test.ts.ejs @@ -17,7 +17,7 @@ describe('PingController', () => { }); before(() => { - client = createClientForHandler(server.handleHttp); + client = createClientForHandler(server.requestHandler); }); after(async () => { diff --git a/packages/cli/test/app-run.test.js b/packages/cli/test/app-run.test.js index f7929e793ccb..5d077a71c494 100644 --- a/packages/cli/test/app-run.test.js +++ b/packages/cli/test/app-run.test.js @@ -11,7 +11,7 @@ const helpers = require('yeoman-test'); const lerna = require('lerna'); const build = require('@loopback/build'); -describe('app-generator', function() { +describe('app-generator (SLOW)', function() { const generator = path.join(__dirname, '../generators/app'); const rootDir = path.join(__dirname, '../../..'); const sandbox = path.join(__dirname, '../../_sandbox'); diff --git a/packages/example-getting-started/test/acceptance/application.test.ts b/packages/example-getting-started/test/acceptance/application.test.ts index 72f87b386b68..13ba89a9d504 100644 --- a/packages/example-getting-started/test/acceptance/application.test.ts +++ b/packages/example-getting-started/test/acceptance/application.test.ts @@ -19,7 +19,7 @@ describe('Application', () => { before(givenARestServer); before(givenTodoRepository); before(() => { - client = createClientForHandler(server.handleHttp); + client = createClientForHandler(server.requestHandler); }); after(async () => { await app.stop(); diff --git a/packages/example-log-extension/test/acceptance/log.extension.acceptance.ts b/packages/example-log-extension/test/acceptance/log.extension.acceptance.ts index 96150c034ab0..3ee3b7330e74 100644 --- a/packages/example-log-extension/test/acceptance/log.extension.acceptance.ts +++ b/packages/example-log-extension/test/acceptance/log.extension.acceptance.ts @@ -5,7 +5,6 @@ import { RestApplication, - RestServer, SequenceHandler, RestBindings, FindRoute, @@ -42,7 +41,6 @@ import {logToMemory, resetLogs} from '../in-memory-logger'; describe('log extension acceptance test', () => { let app: LogApp; - let server: RestServer; let spy: SinonSpy; class LogApp extends LogMixin(RestApplication) {} @@ -73,7 +71,7 @@ describe('log extension acceptance test', () => { it('logs information at DEBUG or higher', async () => { setAppLogToDebug(); - const client: Client = createClientForHandler(server.handleHttp); + const client: Client = createClientForHandler(app.requestHandler); await client.get('/nolog').expect(200, 'nolog called'); expect(spy.called).to.be.False(); @@ -99,7 +97,7 @@ describe('log extension acceptance test', () => { it('logs information at INFO or higher', async () => { setAppLogToInfo(); - const client: Client = createClientForHandler(server.handleHttp); + const client: Client = createClientForHandler(app.requestHandler); await client.get('/nolog').expect(200, 'nolog called'); expect(spy.called).to.be.False(); @@ -125,7 +123,7 @@ describe('log extension acceptance test', () => { it('logs information at WARN or higher', async () => { setAppLogToWarn(); - const client: Client = createClientForHandler(server.handleHttp); + const client: Client = createClientForHandler(app.requestHandler); await client.get('/nolog').expect(200, 'nolog called'); expect(spy.called).to.be.False(); @@ -151,7 +149,7 @@ describe('log extension acceptance test', () => { it('logs information at ERROR', async () => { setAppLogToError(); - const client: Client = createClientForHandler(server.handleHttp); + const client: Client = createClientForHandler(app.requestHandler); await client.get('/nolog').expect(200, 'nolog called'); expect(spy.called).to.be.False(); @@ -177,7 +175,7 @@ describe('log extension acceptance test', () => { it('logs no information when logLevel is set to OFF', async () => { setAppLogToOff(); - const client: Client = createClientForHandler(server.handleHttp); + const client: Client = createClientForHandler(app.requestHandler); await client.get('/nolog').expect(200, 'nolog called'); expect(spy.called).to.be.False(); @@ -234,14 +232,13 @@ describe('log extension acceptance test', () => { } } - server.sequence(LogSequence); + app.sequence(LogSequence); } async function createApp() { app = new LogApp(); app.bind(EXAMPLE_LOG_BINDINGS.TIMER).to(timer); app.bind(EXAMPLE_LOG_BINDINGS.LOGGER).to(logToMemory); - server = await app.getServer(RestServer); } function setAppLogToDebug() { diff --git a/packages/rest/src/rest-application.ts b/packages/rest/src/rest-application.ts index 8a1e0ffd0db6..45a74a4cc0c3 100644 --- a/packages/rest/src/rest-application.ts +++ b/packages/rest/src/rest-application.ts @@ -9,7 +9,7 @@ import {SequenceHandler, SequenceFunction} from './sequence'; import {Binding, Constructor} from '@loopback/context'; import {format} from 'util'; import {RestBindings} from './keys'; -import {RouteEntry, RestServer} from '.'; +import {RouteEntry, RestServer, HttpRequestListener, HttpServerLike} from '.'; import {ControllerClass} from './router/routing-table'; import {OperationObject, OpenApiSpec} from '@loopback/openapi-v3-types'; @@ -28,7 +28,37 @@ export const SequenceActions = RestBindings.SequenceActions; * will throw an error. * */ -export class RestApplication extends Application { +export class RestApplication extends Application implements HttpServerLike { + /** + * The main REST server instance providing REST API for this application. + */ + get restServer(): RestServer { + // 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. + return this.getSync('servers.RestServer'); + } + + /** + * Handle incoming HTTP(S) request by invoking the corresponding + * Controller method via the configured Sequence. + * + * @example + * + * ```ts + * const app = new RestApplication(); + * // setup controllers, etc. + * + * const server = http.createServer(app.requestHandler); + * server.listen(3000); + * ``` + * + * @param req The request. + * @param res The response. + */ + get requestHandler(): HttpRequestListener { + return this.restServer.requestHandler; + } + constructor(config?: ApplicationConfig) { const cfg = Object.assign({}, config); super(cfg); @@ -47,10 +77,7 @@ 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: RestServer = this.getSync('servers.RestServer'); - server.handler(handlerFn); + this.restServer.handler(handlerFn); } /** @@ -101,8 +128,7 @@ export class RestApplication extends Application { controller?: ControllerClass, methodName?: string, ): Binding { - // FIXME(bajtos): This is a workaround based on app.handler() above - const server: RestServer = this.getSync('servers.RestServer'); + const server = this.restServer; if (typeof routeOrVerb === 'object') { return server.route(routeOrVerb); } else { diff --git a/packages/rest/src/rest-server.ts b/packages/rest/src/rest-server.ts index 03aa0c1c14a9..678bce20d1bb 100644 --- a/packages/rest/src/rest-server.ts +++ b/packages/rest/src/rest-server.ts @@ -26,6 +26,15 @@ import { import {ControllerClass} from './router/routing-table'; import {RestBindings} from './keys'; +export type HttpRequestListener = ( + req: ServerRequest, + res: ServerResponse, +) => void; + +export interface HttpServerLike { + requestHandler: HttpRequestListener; +} + const SequenceActions = RestBindings.SequenceActions; // NOTE(bajtos) we cannot use `import * as cloneDeep from 'lodash/cloneDeep' @@ -80,7 +89,7 @@ const OPENAPI_SPEC_MAPPING: {[key: string]: OpenApiSpecOptions} = { * @extends {Context} * @implements {Server} */ -export class RestServer extends Context implements Server { +export class RestServer extends Context implements Server, HttpServerLike { /** * Handle incoming HTTP(S) request by invoking the corresponding * Controller method via the configured Sequence. @@ -89,16 +98,18 @@ export class RestServer extends Context implements Server { * * ```ts * const app = new Application(); + * app.component(RestComponent); * // setup controllers, etc. * - * const server = http.createServer(app.handleHttp); - * server.listen(3000); + * const restServer = await app.getServer(RestServer); + * const httpServer = http.createServer(restServer.requestHandler); + * httpServer.listen(3000); * ``` * * @param req The request. * @param res The response. */ - public handleHttp: (req: ServerRequest, res: ServerResponse) => void; + public requestHandler: HttpRequestListener; protected _httpHandler: HttpHandler; protected get httpHandler(): HttpHandler { @@ -140,7 +151,7 @@ export class RestServer extends Context implements Server { this.sequence(options.sequence); } - this.handleHttp = (req: ServerRequest, res: ServerResponse) => { + this.requestHandler = (req: ServerRequest, res: ServerResponse) => { try { this._handleHttpRequest(req, res, options!).catch(err => this._onUnhandledError(req, res, err), @@ -540,7 +551,7 @@ export class RestServer extends Context implements Server { const httpPort = await this.get(RestBindings.PORT); const httpHost = await this.get(RestBindings.HOST); - this._httpServer = createServer(this.handleHttp); + this._httpServer = createServer(this.requestHandler); const httpServer = this._httpServer; // TODO(bajtos) support httpHostname too diff --git a/packages/rest/test/acceptance/routing/routing.acceptance.ts b/packages/rest/test/acceptance/routing/routing.acceptance.ts index 5297b13847d9..422fdd47bc1c 100644 --- a/packages/rest/test/acceptance/routing/routing.acceptance.ts +++ b/packages/rest/test/acceptance/routing/routing.acceptance.ts @@ -12,6 +12,7 @@ import { RestComponent, RestApplication, SequenceActions, + HttpServerLike, } from '../../..'; import {api, get, post, param, requestBody} from '@loopback/openapi-v3'; @@ -552,9 +553,9 @@ describe('Routing', () => { 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'); + await whenIMakeRequestTo(app) + .get('/greet?name=world') + .expect(200, 'hello world'); }); it('supports controller routes declared via app.api()', async () => { @@ -580,9 +581,9 @@ describe('Routing', () => { 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'); + await whenIMakeRequestTo(app) + .get('/greet?name=world') + .expect(200, 'hello world'); }); it('supports controller routes defined via app.route()', async () => { @@ -600,9 +601,18 @@ describe('Routing', () => { 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'); + await whenIMakeRequestTo(app) + .get('/greet?name=world') + .expect(200, 'hello world'); + }); + + it('provides httpHandler compatible with HTTP server API', async () => { + const app = new RestApplication(); + app.handler((sequence, req, res) => res.end('hello')); + + await createClientForHandler(app.requestHandler) + .get('/') + .expect(200, 'hello'); }); }); @@ -631,7 +641,7 @@ describe('Routing', () => { app.controller(controller); } - function whenIMakeRequestTo(server: RestServer): Client { - return createClientForHandler(server.handleHttp); + function whenIMakeRequestTo(serverOrApp: HttpServerLike): Client { + return createClientForHandler(serverOrApp.requestHandler); } }); diff --git a/packages/rest/test/acceptance/sequence/sequence.acceptance.ts b/packages/rest/test/acceptance/sequence/sequence.acceptance.ts index 814728f85cad..99f5d55f7c44 100644 --- a/packages/rest/test/acceptance/sequence/sequence.acceptance.ts +++ b/packages/rest/test/acceptance/sequence/sequence.acceptance.ts @@ -17,6 +17,7 @@ import { RestServer, RestComponent, RestApplication, + HttpServerLike, } from '../../..'; import {api} from '@loopback/openapi-v3'; import {Application} from '@loopback/core'; @@ -104,8 +105,7 @@ describe('Sequence', () => { const restApp = new RestApplication(); restApp.sequence(MySequence); - const appServer = await restApp.getServer(RestServer); - await whenIRequest(appServer) + await whenIRequest(restApp) .get('/name') .expect('MySequence was invoked.'); }); @@ -213,7 +213,7 @@ describe('Sequence', () => { app.controller(controller); } - function whenIRequest(restServer: RestServer = server): Client { - return createClientForHandler(restServer.handleHttp); + function whenIRequest(restServerOrApp: HttpServerLike = server): Client { + return createClientForHandler(restServerOrApp.requestHandler); } }); diff --git a/packages/rest/test/integration/rest-server.integration.ts b/packages/rest/test/integration/rest-server.integration.ts index eee4ebc03ae2..4edda6e0f817 100644 --- a/packages/rest/test/integration/rest-server.integration.ts +++ b/packages/rest/test/integration/rest-server.integration.ts @@ -32,7 +32,7 @@ describe('RestServer (integration)', () => { } }); - return createClientForHandler(server.handleHttp) + return createClientForHandler(server.requestHandler) .get('/') .expect(500); }); @@ -44,7 +44,7 @@ describe('RestServer (integration)', () => { response.end(); }); - await createClientForHandler(server.handleHttp) + await createClientForHandler(server.requestHandler) .get('/') .expect(200, 'Hello') .expect('Access-Control-Allow-Origin', '*') @@ -58,7 +58,7 @@ describe('RestServer (integration)', () => { response.end(); }); - await createClientForHandler(server.handleHttp) + await createClientForHandler(server.requestHandler) .options('/') .expect(204) .expect('Access-Control-Allow-Origin', '*') @@ -78,7 +78,7 @@ describe('RestServer (integration)', () => { }; server.route(new Route('get', '/greet', greetSpec, function greet() {})); - const response = await createClientForHandler(server.handleHttp).get( + const response = await createClientForHandler(server.requestHandler).get( '/openapi.json', ); expect(response.body).to.containDeep({ @@ -118,7 +118,7 @@ describe('RestServer (integration)', () => { }; server.route(new Route('get', '/greet', greetSpec, function greet() {})); - const response = await createClientForHandler(server.handleHttp).get( + const response = await createClientForHandler(server.requestHandler).get( '/openapi.yaml', ); const expected = yaml.safeLoad(` @@ -159,7 +159,7 @@ servers: }; server.route(new Route('get', '/greet', greetSpec, function greet() {})); - const response = await createClientForHandler(server.handleHttp).get( + const response = await createClientForHandler(server.requestHandler).get( '/swagger-ui', ); await server.get(RestBindings.PORT); @@ -190,7 +190,7 @@ servers: }; server.route(new Route('get', '/greet', greetSpec, function greet() {})); - const response = await createClientForHandler(server.handleHttp).get( + const response = await createClientForHandler(server.requestHandler).get( '/swagger-ui', ); await server.get(RestBindings.PORT);