Skip to content

Commit

Permalink
feat(rest): add local UI for api explorer
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Sep 4, 2018
1 parent 44b5576 commit cba9e21
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 25 deletions.
5 changes: 4 additions & 1 deletion packages/explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,18 @@
"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",
"swagger-ui-dist": "^3.18.2"
},
"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",
Expand Down
30 changes: 30 additions & 0 deletions packages/explorer/src/explorer.component.ts
Original file line number Diff line number Diff line change
@@ -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<Handler> {
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,
};
}
}
2 changes: 2 additions & 0 deletions packages/explorer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@
// License text available at https://opensource.org/licenses/MIT

export * from './explorer';
export * from './keys';
export * from './explorer.component';
19 changes: 19 additions & 0 deletions packages/explorer/src/keys.ts
Original file line number Diff line number Diff line change
@@ -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<RequestHandler>('explorer.handler');

export const CONFIG = BindingKey.create<ApiExplorerUIOptions | undefined>(
'explorer.config',
);
}
2 changes: 1 addition & 1 deletion packages/explorer/test/integration/explorer.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
45 changes: 45 additions & 0 deletions packages/explorer/test/integration/rest.integration.ts
Original file line number Diff line number Diff line change
@@ -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, /\<title\>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;
}
45 changes: 22 additions & 23 deletions packages/rest/src/rest.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}

Expand Down Expand Up @@ -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 ||
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit cba9e21

Please sign in to comment.