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 Oct 31, 2018
1 parent bf10d3a commit 2e9730e
Show file tree
Hide file tree
Showing 11 changed files with 193 additions and 27 deletions.
2 changes: 1 addition & 1 deletion packages/explorer/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

export * from './dist8';
export * from './dist';
2 changes: 1 addition & 1 deletion packages/explorer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

module.exports = require('@loopback/dist-util').loadDist(__dirname);
module.exports = require('./dist');
24 changes: 12 additions & 12 deletions packages/explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,33 @@
"node": ">=8.9"
},
"scripts": {
"build:all-dist": "npm run build:dist8 && npm run build:dist10",
"build:apidocs": "lb-apidocs",
"build": "lb-tsc",
"build:dist8": "lb-tsc es2017",
"build:dist10": "lb-tsc es2018",
"build": "lb-tsc es2017 --outDir dist --copy-resources",
"clean": "lb-clean loopback-explorer*.tgz dist* package api-docs",
"pretest": "npm run build",
"integration": "lb-mocha \"DIST/test/integration/**/*.js\"",
"test": "lb-mocha \"DIST/test/unit/**/*.js\" \"DIST/test/integration/**/*.js\"",
"unit": "lb-mocha \"DIST/test/unit/**/*.js\"",
"integration": "lb-mocha \"dist/test/integration/**/*.js\"",
"test": "lb-mocha \"dist/test/unit/**/*.js\" \"dist/test/integration/**/*.js\"",
"unit": "lb-mocha \"dist/test/unit/**/*.js\"",
"verify": "npm pack && tar xf loopback-explorer*.tgz && tree package && npm run clean"
},
"author": "IBM",
"copyright.owner": "IBM Corp.",
"license": "MIT",
"dependencies": {
"@loopback/context": "^1.0.0",
"@loopback/core": "^1.0.0",
"@loopback/dist-util": "^0.3.6",
"@loopback/rest": "^1.1.0",
"@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": "^1.0.0",
"@loopback/testlab": "^1.0.0",
"@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
44 changes: 44 additions & 0 deletions packages/explorer/test/integration/rest.integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// 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 {createRestAppClient} from '@loopback/testlab';
import {
RestServerConfig,
RestComponent,
RestServer,
RestApplication,
} from '@loopback/rest';
import {Application} from '@loopback/core';
import {ExplorerComponent, ExplorerBindings} from '../..';

describe('API Explorer for REST Server', () => {
let restApp: RestApplication;

before(async () => {
restApp = await givenRestApp({
rest: {port: 0},
});
});

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

it('exposes "GET /explorer"', async () => {
const test = await createRestAppClient(restApp);
test
.get('/explorer')
.expect(200, /\<title\>LoopBack API Explorer<\/title\>/)
.expect('content-type', /text\/html.*/);
});
});

async function givenRestApp(options?: {rest: RestServerConfig}) {
const app = new RestApplication(options);
app.bind(ExplorerBindings.CONFIG).to({});
// FIXME: Can we mount the ExplorerComponent to a given server?
app.component(ExplorerComponent);
await app.start();
return app;
}
5 changes: 5 additions & 0 deletions packages/rest/src/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {HttpProtocol} from '@loopback/http-server';
import * as https from 'https';
import {ErrorWriterOptions} from 'strong-error-handler';
import {RestRouter} from './router';
import {Application} from 'express';

/**
* RestServer-specific bindings
Expand Down Expand Up @@ -73,6 +74,10 @@ export namespace RestBindings {
*/
export const ROUTER = BindingKey.create<RestRouter>('rest.router');

/**
* 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
67 changes: 55 additions & 12 deletions packages/rest/src/rest.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,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 @@ -203,17 +204,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();

// Set up endpoints for OpenAPI spec/ui
this._setupOpenApiSpecEndpoints();
Expand All @@ -232,7 +224,25 @@ export class RestServer extends Context implements Server, HttpServerLike {
}

/**
* Mount /openapi.json, /openapi.yaml for specs and /swagger-ui, /explorer
* 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));
}

/**
* Mount /openapi.json, /openapi.yaml for specs and /swagger-ui, /api-explorer
* to redirect to externally hosted API explorer
*/
protected _setupOpenApiSpecEndpoints() {
Expand All @@ -259,6 +269,20 @@ 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
Expand All @@ -273,6 +297,8 @@ export class RestServer extends Context implements Server, HttpServerLike {
const routingTable = new RoutingTable(router);

this._httpHandler = new HttpHandler(this, routingTable);
this._registerMiddleware();

for (const b of this.find('controllers.*')) {
const controllerName = b.key.replace(/^controllers\./, '');
const ctor = b.valueConstructor;
Expand Down Expand Up @@ -844,3 +870,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 2e9730e

Please sign in to comment.