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 17, 2018
1 parent 70a78fe commit b2ca7dc
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 17 deletions.
13 changes: 8 additions & 5 deletions packages/explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,20 @@
"copyright.owner": "IBM Corp.",
"license": "MIT",
"dependencies": {
"@loopback/context": "^0.12.7",
"@loopback/core": "^0.11.7",
"@loopback/dist-util": "^0.3.6",
"@loopback/rest": "^0.22.2",
"@types/express": "^4.16.0",
"ejs": "^2.6.1",
"serve-static": "^1.13.2",
"swagger-ui-dist": "^3.18.2"
"swagger-ui-dist": "^3.19.0"
},
"devDependencies": {
"@loopback/build": "^0.7.1",
"@loopback/dist-util": "^0.3.6",
"@loopback/testlab": "^0.11.5",
"@loopback/build": "^0.7.2",
"@loopback/testlab": "^0.12.1",
"@types/ejs": "^2.6.0",
"@types/node": "^10.1.1",
"@types/node": "^10.10.1",
"@types/serve-static": "^1.13.2",
"express": "^4.16.3"
},
Expand Down
46 changes: 46 additions & 0 deletions packages/explorer/src/explorer.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// 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} from '@loopback/context';
import {Application, Component, CoreBindings} from '@loopback/core';
import {RestServer} from '@loopback/rest';
import {ApiExplorerUIOptions, apiExplorerUI} from './explorer';
import {ExplorerBindings} from './keys';

/**
* We have a few options:
*
* 1. The Explorer component contributes an express middleware so that REST
* servers can mount the UI
*
* 2. The Explorer component contributes a route to REST servers
*
* 3. The Explorer component proactively mount itself with the RestApplication
*/
export class ExplorerComponent implements Component {
constructor(
@inject(CoreBindings.APPLICATION_INSTANCE) private application: Application,
@inject(ExplorerBindings.CONFIG, {optional: true})
private config: ApiExplorerUIOptions,
) {
this.init();
}

init() {
// FIXME: We should be able to receive the servers via injection
const restServerBindings = this.application.find(
binding =>
binding.key.startsWith(CoreBindings.SERVERS) &&
binding.valueConstructor === RestServer,
);
for (const binding of restServerBindings) {
const restServer = this.application.getSync<RestServer>(binding.key);
restServer.bind(ExplorerBindings.MIDDLEWARE).to({
path: this.config.path || '/explorer',
handler: apiExplorerUI(this.config),
});
}
}
}
5 changes: 5 additions & 0 deletions packages/explorer/src/explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ export type ApiExplorerUIOptions = {
* Options for serve-static middleware
*/
serveStaticOptions?: serveStatic.ServeStaticOptions;

/**
* Path for the explorer UI
*/
path?: string;
};

/**
Expand Down
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';
21 changes: 21 additions & 0 deletions packages/explorer/src/keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// 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 {ApiExplorerUIOptions} from './explorer';
import {Middleware} from '@loopback/rest';

/**
* Binding keys used by this component.
*/
export namespace ExplorerBindings {
export const MIDDLEWARE = BindingKey.create<Middleware>(
'middleware.explorer',
);

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
40 changes: 40 additions & 0 deletions packages/explorer/test/integration/rest.integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// 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 {createClientForRestServer} 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: {port: 0},
});
});

after(async () => {
await server.stop();
});

it('exposes "GET /explorer"', async () => {
const test = await createClientForRestServer(server);
test
.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);
app.bind(ExplorerBindings.CONFIG).to({});
// FIXME: Can we mount the ExplorerComponent to a given server?
app.component(ExplorerComponent);
const server = await app.getServer(RestServer);
return server;
}
5 changes: 5 additions & 0 deletions packages/rest/src/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
import {HttpProtocol} from '@loopback/http-server';
import * as https from 'https';
import {ErrorWriterOptions} from 'strong-error-handler';
import {Application} from 'express';

/**
* RestServer-specific bindings
Expand Down Expand Up @@ -65,6 +66,10 @@ export namespace RestBindings {
* Internal binding key for http-handler
*/
export const HANDLER = BindingKey.create<HttpHandler>('rest.handler');
/**
* Binding key for express app
*/
export const EXPRESS_APP = BindingKey.create<Application>('rest.express.app');
/**
* Binding key for setting and injecting Reject action's error handling
* options.
Expand Down
65 changes: 54 additions & 11 deletions packages/rest/src/rest.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ export class RestServer extends Context implements Server, HttpServerLike {

protected _setupRequestHandler() {
this._expressApp = express();
this.bind(RestBindings.EXPRESS_APP).to(this._expressApp);

// Disable express' built-in query parser, we parse queries ourselves
// Note that when disabled, express sets query to an empty object,
Expand All @@ -202,17 +203,8 @@ export class RestServer extends Context implements Server, HttpServerLike {

this.requestHandler = this._expressApp;

// Allow CORS support for all endpoints so that users
// can test with online SwaggerUI instance
const corsOptions = this.config.cors || {
origin: '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
preflightContinue: false,
optionsSuccessStatus: 204,
maxAge: 86400,
credentials: true,
};
this._expressApp.use(cors(corsOptions));
// Enable CORS
this._setupCORS();

// Place the assets router here before controllers
this._setupRouterForStaticAssets();
Expand All @@ -233,6 +225,24 @@ export class RestServer extends Context implements Server, HttpServerLike {
);
}

/**
* Set up CORS middleware
*/
protected _setupCORS() {
// Allow CORS support for all endpoints so that users
// can test with online SwaggerUI instance
const corsOptions = this.config.cors || {
origin: '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
preflightContinue: false,
optionsSuccessStatus: 204,
maxAge: 86400,
credentials: true,
};
// Set up CORS
this._expressApp.use(cors(corsOptions));
}

/**
* Set up an express router for all static assets so that middleware for
* all directories are invoked at the same phase
Expand Down Expand Up @@ -272,13 +282,29 @@ export class RestServer extends Context implements Server, HttpServerLike {
return this.httpHandler.handleRequest(request, response);
}

protected _registerMiddleware() {
for (const b of this.find('middleware.*')) {
const middleware = this.getSync<Middleware>(b.key);
if (!middleware.method) {
this._expressApp.use(middleware.path || '/', middleware.handler);
} else {
this._expressApp[middleware.method](
middleware.path || '/',
middleware.handler,
);
}
}
}

protected _setupHandlerIfNeeded() {
// TODO(bajtos) support hot-reloading of controllers
// after the app started. The idea is to rebuild the HttpHandler
// instance whenever a controller was added/deleted.
// See https://github.com/strongloop/loopback-next/issues/433
if (this._httpHandler) return;

this._registerMiddleware();

this._httpHandler = new HttpHandler(this);
for (const b of this.find('controllers.*')) {
const controllerName = b.key.replace(/^controllers\./, '');
Expand Down Expand Up @@ -824,3 +850,20 @@ export interface RestServerOptions {
* @interface RestServerConfig
*/
export type RestServerConfig = RestServerOptions & HttpServerOptions;

/**
* Middleware registration entry
*/
export interface Middleware {
method?:
| 'all'
| 'get'
| 'post'
| 'put'
| 'delete'
| 'patch'
| 'options'
| 'head';
path?: PathParams;
handler: express.RequestHandler;
}

0 comments on commit b2ca7dc

Please sign in to comment.