diff --git a/packages/explorer/package.json b/packages/explorer/package.json index 710f6fb1c27c..66b68930285f 100644 --- a/packages/explorer/package.json +++ b/packages/explorer/package.json @@ -22,6 +22,9 @@ "copyright.owner": "IBM Corp.", "license": "MIT", "dependencies": { + "@loopback/context": "^0.12.5", + "@loopback/core": "^0.11.5", + "@loopback/dist-util": "^0.3.6", "@types/express": "^4.16.0", "ejs": "^2.6.1", "serve-static": "^1.13.2", @@ -29,8 +32,8 @@ }, "devDependencies": { "@loopback/build": "^0.7.1", - "@loopback/dist-util": "^0.3.6", "@loopback/testlab": "^0.11.5", + "@loopback/rest": "^0.19.6", "@types/ejs": "^2.6.0", "@types/node": "^10.1.1", "@types/serve-static": "^1.13.2", diff --git a/packages/explorer/src/explorer.component.ts b/packages/explorer/src/explorer.component.ts new file mode 100644 index 000000000000..f75c357e9cbb --- /dev/null +++ b/packages/explorer/src/explorer.component.ts @@ -0,0 +1,30 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/explorer +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {inject, Setter, Provider} from '@loopback/context'; +import {Component, ProviderMap} from '@loopback/core'; +import {ApiExplorerUIOptions, apiExplorerUI} from './explorer'; +import {Handler} from 'express'; +import {ExplorerBindings} from './keys'; + +export class ExplorerProvider implements Provider { + constructor( + @inject(ExplorerBindings.CONFIG, {optional: true}) + private config: ApiExplorerUIOptions, + ) {} + + value() { + return apiExplorerUI(this.config); + } +} + +export class ExplorerComponent implements Component { + providers?: ProviderMap; + constructor() { + this.providers = { + [ExplorerBindings.HANDLER.key]: ExplorerProvider, + }; + } +} diff --git a/packages/explorer/src/index.ts b/packages/explorer/src/index.ts index b5570c86dc1a..845bef34e20c 100644 --- a/packages/explorer/src/index.ts +++ b/packages/explorer/src/index.ts @@ -4,3 +4,5 @@ // License text available at https://opensource.org/licenses/MIT export * from './explorer'; +export * from './keys'; +export * from './explorer.component'; diff --git a/packages/explorer/src/keys.ts b/packages/explorer/src/keys.ts new file mode 100644 index 000000000000..1d98d8677b11 --- /dev/null +++ b/packages/explorer/src/keys.ts @@ -0,0 +1,19 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/explorer +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {BindingKey} from '@loopback/context'; +import {RequestHandler} from 'express'; +import {ApiExplorerUIOptions} from './explorer'; + +/** + * Binding keys used by this component. + */ +export namespace ExplorerBindings { + export const HANDLER = BindingKey.create('explorer.handler'); + + export const CONFIG = BindingKey.create( + 'explorer.config', + ); +} diff --git a/packages/explorer/test/integration/explorer.integration.ts b/packages/explorer/test/integration/explorer.integration.ts index 665db4019e45..c300544aa5ea 100644 --- a/packages/explorer/test/integration/explorer.integration.ts +++ b/packages/explorer/test/integration/explorer.integration.ts @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import {createClientForHandler} from '@loopback/testlab'; -import {apiExplorerUI} from '../../src/explorer'; +import {apiExplorerUI} from '../../'; import * as express from 'express'; import * as path from 'path'; diff --git a/packages/explorer/test/integration/rest.integration.ts b/packages/explorer/test/integration/rest.integration.ts new file mode 100644 index 000000000000..8e3a5f1dd3ec --- /dev/null +++ b/packages/explorer/test/integration/rest.integration.ts @@ -0,0 +1,45 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/explorer +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT +import {createClientForHandler} from '@loopback/testlab'; +import {RestServerConfig, RestComponent, RestServer} from '@loopback/rest'; +import {Application} from '@loopback/core'; +import {ExplorerComponent, ExplorerBindings} from '../..'; + +describe('API Explorer for REST Server', () => { + let server: RestServer; + + before(async () => { + server = await givenAServer({ + rest: {}, + }); + + // FIXME: How do we configure the API Explorer UI? + server.bind(ExplorerBindings.CONFIG).to({}); + + // FIXME: Should the ExplorerComponent contribute a handler or route? + const handler = await server.get(ExplorerBindings.HANDLER); + + // FIXME: How to register routes to the rest server by phase or order + // FIXME: Should REST server automatically mounts the route based on registration + // via bindings such as `rest.server.routes`? + server.staticRouter.use('/explorer', handler); + }); + + it('exposes "GET /explorer"', async () => { + await createClientForHandler(server.requestHandler) + .get('/explorer') + .expect(200, /\LoopBack API Explorer<\/title\>/) + .expect('content-type', /text\/html.*/); + }); +}); + +async function givenAServer(options?: {rest: RestServerConfig}) { + const app = new Application(options); + app.component(RestComponent); + // FIXME: Can we mount the ExplorerComponent to a given server? + app.component(ExplorerComponent); + const server = await app.getServer(RestServer); + return server; +} diff --git a/packages/rest/src/rest.server.ts b/packages/rest/src/rest.server.ts index 492f145c4a02..038c3b38b630 100644 --- a/packages/rest/src/rest.server.ts +++ b/packages/rest/src/rest.server.ts @@ -202,11 +202,24 @@ export class RestServer extends Context implements Server, HttpServerLike { maxAge: 86400, credentials: true, }; + // Set up CORS this._expressApp.use(cors(corsOptions)); // Place the assets router here before controllers this._setupRouterForStaticAssets(); + const mapping = this.config.openApiSpec!.endpointMapping!; + // Serving OpenAPI spec + for (const p in mapping) { + this._expressApp.use(p, (req, res) => + this._serveOpenApiSpec(req, res, mapping[p]), + ); + } + + this._expressApp.get(['/swagger-ui', '/api-explorer'], (req, res) => + this._redirectToSwaggerUI(req, res), + ); + // Mount our router & request handler this._expressApp.use((req, res, next) => { this._handleHttpRequest(req, res).catch(next); @@ -232,25 +245,6 @@ export class RestServer extends Context implements Server, HttpServerLike { } protected _handleHttpRequest(request: Request, response: Response) { - const mapping = this.config.openApiSpec!.endpointMapping!; - if (request.method === 'GET' && request.url && request.url in mapping) { - // NOTE(bajtos) Regular routes are handled through Sequence. - // IMO, this built-in endpoint should not run through a Sequence, - // because it's not part of the application API itself. - // E.g. if the app implements access/audit logs, I don't want - // this endpoint to trigger a log entry. If the server implements - // content-negotiation to support XML clients, I don't want the OpenAPI - // spec to be converted into an XML response. - const form = mapping[request.url]; - return this._serveOpenApiSpec(request, response, form); - } - if ( - request.method === 'GET' && - request.url && - request.url === '/swagger-ui' - ) { - return this._redirectToSwaggerUI(request, response); - } return this.httpHandler.handleRequest(request, response); } @@ -396,7 +390,7 @@ export class RestServer extends Context implements Server, HttpServerLike { // 127.0.0.1 let host = (request.get('x-forwarded-host') || '').split(',')[0] || - request.headers.host!.replace(/:[0-9]+$/, ''); + request.headers.host!.replace(/:([0-9]+)$/, ''); let port = (request.get('x-forwarded-port') || '').split(',')[0] || this.config.port || @@ -561,6 +555,10 @@ export class RestServer extends Context implements Server, HttpServerLike { this._routerForStaticAssets.use(path, express.static(rootDir, options)); } + get staticRouter() { + return this._routerForStaticAssets; + } + /** * Set the OpenAPI specification that defines the REST API schema for this * server. All routes, parameter definitions and return types will be defined @@ -758,13 +756,14 @@ export interface OpenApiSpecOptions { export interface ApiExplorerOptions { /** - * The url for hosted API explorer UI + * The url for API explorer UI * default to https://loopback.io/api-explorer */ url?: string; /** - * URL for the API explorer served over plain http to deal with mixed content - * security imposed by browsers as the spec is exposed over `http` by default. + * URL for the externally hosted API explorer UI API explorer served over + * plain http to deal with mixed content security imposed by browsers as the + * spec is exposed over `http` by default. * https://github.com/strongloop/loopback-next/issues/1603 */ urlForHttp?: string;