diff --git a/packages/rest/src/__tests__/integration/rest.server.integration.ts b/packages/rest/src/__tests__/integration/rest.server.integration.ts index e2f34bf3c49d..9196c644de4c 100644 --- a/packages/rest/src/__tests__/integration/rest.server.integration.ts +++ b/packages/rest/src/__tests__/integration/rest.server.integration.ts @@ -787,9 +787,9 @@ paths: }); it('controls server urls', async () => { - const response = await createClientForHandler(server.requestHandler).get( - '/openapi.json', - ); + const response = await createClientForHandler(server.requestHandler) + .get('openapi.json') + .expect(200); expect(response.body.servers).to.containEql({url: '/api'}); }); diff --git a/packages/rest/src/rest.server.ts b/packages/rest/src/rest.server.ts index fdea83f6190f..294f4bc9fe96 100644 --- a/packages/rest/src/rest.server.ts +++ b/packages/rest/src/rest.server.ts @@ -5,11 +5,11 @@ import { Binding, + BindingAddress, + BindingScope, Constructor, Context, inject, - BindingScope, - BindingAddress, } from '@loopback/context'; import {Application, CoreBindings, Server} from '@loopback/core'; import {HttpServer, HttpServerOptions} from '@loopback/http-server'; @@ -26,7 +26,6 @@ import * as express from 'express'; import {PathParams} from 'express-serve-static-core'; import {IncomingMessage, ServerResponse} from 'http'; import {ServerOptions} from 'https'; -import {safeDump} from 'js-yaml'; import {ServeStaticOptions} from 'serve-static'; import {BodyParser, REQUEST_BODY_PARSER_TAG} from './body-parsers'; import {HttpHandler} from './http-handler'; @@ -38,12 +37,13 @@ import { ControllerInstance, ControllerRoute, createControllerFactoryForBinding, + ExternalExpressRoutes, + RedirectRoute, Route, RouteEntry, RoutingTable, - ExternalExpressRoutes, - RedirectRoute, } from './router'; +import {ServeApiSpecRoute} from './router/serve-api-spec.route'; import {DefaultSequence, SequenceFunction, SequenceHandler} from './sequence'; import { FindRoute, @@ -51,9 +51,9 @@ import { ParseParams, Reject, Request, + RequestBodyParserOptions, Response, Send, - RequestBodyParserOptions, } from './types'; const debug = debugFactory('loopback:rest:server'); @@ -236,21 +236,13 @@ export class RestServer extends Context implements Server, HttpServerLike { */ protected _setupOpenApiSpecEndpoints() { if (this.config.openApiSpec.disabled) return; - // 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 mapping = this.config.openApiSpec.endpointMapping!; // Serving OpenAPI spec + const mapping = this.config.openApiSpec.endpointMapping!; for (const p in mapping) { - this._expressApp.get(p, (req, res) => - this._serveOpenApiSpec(req, res, mapping[p]), - ); + this.route(new ServeApiSpecRoute(p, this, mapping[p])); } + // Redirect to externally hosted swagger-ui instance const explorerPaths = ['/swagger-ui', '/explorer']; this._expressApp.get(explorerPaths, (req, res, next) => this._redirectToSwaggerUI(req, res, next), @@ -361,45 +353,6 @@ export class RestServer extends Context implements Server, HttpServerLike { ); } - private async _serveOpenApiSpec( - request: Request, - response: Response, - specForm?: OpenApiSpecForm, - ) { - const requestContext = new RequestContext( - request, - response, - this, - this.config, - ); - - specForm = specForm || {version: '3.0.0', format: 'json'}; - let specObj = this.getApiSpec(); - if (this.config.openApiSpec.setServersFromRequest) { - specObj = Object.assign({}, specObj); - specObj.servers = [{url: requestContext.requestedBaseUrl}]; - } - - const basePath = requestContext.basePath; - if (specObj.servers && basePath) { - for (const s of specObj.servers) { - // Update the default server url to honor `basePath` - if (s.url === '/') { - s.url = basePath; - } - } - } - - if (specForm.format === 'json') { - const spec = JSON.stringify(specObj, null, 2); - response.setHeader('content-type', 'application/json; charset=utf-8'); - response.end(spec, 'utf-8'); - } else { - const yaml = safeDump(specObj, {}); - response.setHeader('content-type', 'text/yaml; charset=utf-8'); - response.end(yaml, 'utf-8'); - } - } private async _redirectToSwaggerUI( request: Request, response: Response, diff --git a/packages/rest/src/router/serve-api-spec.route.ts b/packages/rest/src/router/serve-api-spec.route.ts new file mode 100644 index 000000000000..3cd904a0b747 --- /dev/null +++ b/packages/rest/src/router/serve-api-spec.route.ts @@ -0,0 +1,69 @@ +// Copyright IBM Corp. 2017, 2018. All Rights Reserved. +// Node module: @loopback/rest +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {OperationObject} from '@loopback/openapi-v3-types'; +import {safeDump} from 'js-yaml'; +import {RouteEntry} from '.'; +import {RequestContext} from '../request-context'; +import {OpenApiSpecForm, RestServer} from '../rest.server'; +import {OperationArgs, OperationRetval} from '../types'; + +export class ServeApiSpecRoute implements RouteEntry { + readonly verb: string = 'get'; + readonly spec: OperationObject = { + description: 'LoopBack route serving OpenAPI spec', + 'x-visibility': 'undocumented', + responses: {}, + }; + + constructor( + public readonly path: string, + private readonly server: RestServer, + private readonly specForm: OpenApiSpecForm = { + version: '3.0.0', + format: 'json', + }, + ) {} + + async invokeHandler( + requestContext: RequestContext, + args: OperationArgs, + ): Promise { + const {response, requestedBaseUrl, basePath} = requestContext; + console.log('requested url %s basePath %s', requestedBaseUrl, basePath); + let specObj = this.server.getApiSpec(); + if (requestContext.serverConfig.openApiSpec.setServersFromRequest) { + specObj = Object.assign({}, specObj); + specObj.servers = [{url: requestedBaseUrl}]; + } + + if (specObj.servers && basePath) { + for (const s of specObj.servers) { + // Update the default server url to honor `basePath` + if (s.url === '/') { + s.url = basePath; + } + } + } + + if (this.specForm.format === 'json') { + const spec = JSON.stringify(specObj, null, 2); + response.setHeader('content-type', 'application/json; charset=utf-8'); + response.end(spec, 'utf-8'); + } else { + const yaml = safeDump(specObj, {}); + response.setHeader('content-type', 'text/yaml; charset=utf-8'); + response.end(yaml, 'utf-8'); + } + } + + updateBindings(requestContext: RequestContext) { + // no-op + } + + describe(): string { + return `${this.spec.description} at ${this.path}`; + } +}