UNPKG

37.2 kBPlain TextView Raw
1// Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved.
2// Node module: @loopback/rest
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6import {
7 Application,
8 Binding,
9 BindingAddress,
10 BindingScope,
11 Constructor,
12 ContextObserver,
13 CoreBindings,
14 createBindingFromClass,
15 extensionFor,
16 filterByKey,
17 filterByTag,
18 inject,
19 Server,
20 Subscription,
21} from '@loopback/core';
22import {BaseMiddlewareRegistry, ExpressRequestHandler} from '@loopback/express';
23import {HttpServer, HttpServerOptions} from '@loopback/http-server';
24import {
25 getControllerSpec,
26 OASEnhancerBindings,
27 OASEnhancerService,
28 OpenAPIObject,
29 OpenApiSpec,
30 OperationObject,
31 ServerObject,
32} from '@loopback/openapi-v3';
33import assert, {AssertionError} from 'assert';
34import cors from 'cors';
35import debugFactory from 'debug';
36import express, {ErrorRequestHandler} from 'express';
37import {PathParams} from 'express-serve-static-core';
38import fs from 'fs';
39import {IncomingMessage, ServerResponse} from 'http';
40import {ServerOptions} from 'https';
41import {dump} from 'js-yaml';
42import {cloneDeep} from 'lodash';
43import {ServeStaticOptions} from 'serve-static';
44import {writeErrorToResponse} from 'strong-error-handler';
45import {BodyParser, REQUEST_BODY_PARSER_TAG} from './body-parsers';
46import {HttpHandler} from './http-handler';
47import {RestBindings, RestTags} from './keys';
48import {RequestContext} from './request-context';
49import {
50 ControllerClass,
51 ControllerFactory,
52 ControllerInstance,
53 ControllerRoute,
54 createControllerFactoryForBinding,
55 createRoutesForController,
56 ExternalExpressRoutes,
57 RedirectRoute,
58 RestRouterOptions,
59 Route,
60 RouteEntry,
61 RouterSpec,
62 RoutingTable,
63} from './router';
64import {assignRouterSpec} from './router/router-spec';
65import {
66 DefaultSequence,
67 MiddlewareSequence,
68 RestMiddlewareGroups,
69 SequenceFunction,
70 SequenceHandler,
71} from './sequence';
72import {Request, RequestBodyParserOptions, Response} from './types';
73
74const debug = debugFactory('loopback:rest:server');
75
76export type HttpRequestListener = (
77 req: IncomingMessage,
78 res: ServerResponse,
79) => void;
80
81export interface HttpServerLike {
82 requestHandler: HttpRequestListener;
83}
84
85const SequenceActions = RestBindings.SequenceActions;
86
87/**
88 * A REST API server for use with Loopback.
89 * Add this server to your application by importing the RestComponent.
90 *
91 * @example
92 * ```ts
93 * const app = new MyApplication();
94 * app.component(RestComponent);
95 * ```
96 *
97 * To add additional instances of RestServer to your application, use the
98 * `.server` function:
99 * ```ts
100 * app.server(RestServer, 'nameOfYourServer');
101 * ```
102 *
103 * By default, one instance of RestServer will be created when the RestComponent
104 * is bootstrapped. This instance can be retrieved with
105 * `app.getServer(RestServer)`, or by calling `app.get('servers.RestServer')`
106 * Note that retrieving other instances of RestServer must be done using the
107 * server's name:
108 * ```ts
109 * const server = await app.getServer('foo')
110 * // OR
111 * const server = await app.get('servers.foo');
112 * ```
113 */
114export class RestServer
115 extends BaseMiddlewareRegistry
116 implements Server, HttpServerLike
117{
118 /**
119 * Handle incoming HTTP(S) request by invoking the corresponding
120 * Controller method via the configured Sequence.
121 *
122 * @example
123 *
124 * ```ts
125 * const app = new Application();
126 * app.component(RestComponent);
127 * // setup controllers, etc.
128 *
129 * const restServer = await app.getServer(RestServer);
130 * const httpServer = http.createServer(restServer.requestHandler);
131 * httpServer.listen(3000);
132 * ```
133 *
134 * @param req - The request.
135 * @param res - The response.
136 */
137
138 protected oasEnhancerService: OASEnhancerService;
139 // eslint-disable-next-line @typescript-eslint/naming-convention
140 public get OASEnhancer(): OASEnhancerService {
141 this._setupOASEnhancerIfNeeded();
142 return this.oasEnhancerService;
143 }
144
145 protected _requestHandler: HttpRequestListener;
146 public get requestHandler(): HttpRequestListener {
147 if (this._requestHandler == null) {
148 this._setupRequestHandlerIfNeeded();
149 }
150 return this._requestHandler;
151 }
152
153 public readonly config: RestServerResolvedConfig;
154 private _basePath: string;
155
156 protected _httpHandler: HttpHandler;
157 protected get httpHandler(): HttpHandler {
158 this._setupHandlerIfNeeded();
159 return this._httpHandler;
160 }
161
162 /**
163 * Context event subscriptions for route related changes
164 */
165 private _routesEventSubscription: Subscription;
166
167 protected _httpServer: HttpServer | undefined;
168
169 protected _expressApp?: express.Application;
170
171 get listening(): boolean {
172 return this._httpServer ? this._httpServer.listening : false;
173 }
174
175 get httpServer(): HttpServer | undefined {
176 return this._httpServer;
177 }
178
179 /**
180 * The base url for the server, including the basePath if set. For example,
181 * the value will be 'http://localhost:3000/api' if `basePath` is set to
182 * '/api'.
183 */
184 get url(): string | undefined {
185 let serverUrl = this.rootUrl;
186 if (!serverUrl) return serverUrl;
187 serverUrl = serverUrl + (this._basePath || '');
188 return serverUrl;
189 }
190
191 /**
192 * The root url for the server without the basePath. For example, the value
193 * will be 'http://localhost:3000' regardless of the `basePath`.
194 */
195 get rootUrl(): string | undefined {
196 return this._httpServer?.url;
197 }
198
199 /**
200 *
201 * Creates an instance of RestServer.
202 *
203 * @param app - The application instance (injected via
204 * CoreBindings.APPLICATION_INSTANCE).
205 * @param config - The configuration options (injected via
206 * RestBindings.CONFIG).
207 *
208 */
209 constructor(
210 @inject(CoreBindings.APPLICATION_INSTANCE) app: Application,
211 @inject(RestBindings.CONFIG, {optional: true})
212 config: RestServerConfig = {},
213 ) {
214 super(app);
215 this.scope = BindingScope.SERVER;
216
217 this.config = resolveRestServerConfig(config);
218
219 this.bind(RestBindings.PORT).to(this.config.port);
220 this.bind(RestBindings.HOST).to(config.host);
221 this.bind(RestBindings.PATH).to(config.path);
222 this.bind(RestBindings.PROTOCOL).to(config.protocol ?? 'http');
223 this.bind(RestBindings.HTTPS_OPTIONS).to(config as ServerOptions);
224
225 if (config.requestBodyParser) {
226 this.bind(RestBindings.REQUEST_BODY_PARSER_OPTIONS).to(
227 config.requestBodyParser,
228 );
229 }
230
231 if (config.sequence) {
232 this.sequence(config.sequence);
233 } else {
234 this.sequence(MiddlewareSequence);
235 }
236
237 if (config.router) {
238 this.bind(RestBindings.ROUTER_OPTIONS).to(config.router);
239 }
240
241 this.basePath(config.basePath);
242
243 this.bind(RestBindings.BASE_PATH).toDynamicValue(() => this._basePath);
244 this.bind(RestBindings.HANDLER).toDynamicValue(() => this.httpHandler);
245 }
246
247 protected _setupOASEnhancerIfNeeded() {
248 if (this.oasEnhancerService != null) return;
249 this.add(
250 createBindingFromClass(OASEnhancerService, {
251 key: OASEnhancerBindings.OAS_ENHANCER_SERVICE,
252 }),
253 );
254 this.oasEnhancerService = this.getSync(
255 OASEnhancerBindings.OAS_ENHANCER_SERVICE,
256 );
257 }
258
259 protected _setupRequestHandlerIfNeeded() {
260 if (this._expressApp != null) return;
261 this._expressApp = express();
262 this._applyExpressSettings();
263 this._requestHandler = this._expressApp;
264
265 // Allow CORS support for all endpoints so that users
266 // can test with online SwaggerUI instance
267 this.expressMiddleware(cors, this.config.cors, {
268 injectConfiguration: false,
269 key: 'middleware.cors',
270 group: RestMiddlewareGroups.CORS,
271 }).apply(
272 extensionFor(
273 RestTags.REST_MIDDLEWARE_CHAIN,
274 RestTags.ACTION_MIDDLEWARE_CHAIN,
275 ),
276 );
277
278 // Set up endpoints for OpenAPI spec/ui
279 this._setupOpenApiSpecEndpoints();
280
281 // Mount our router & request handler
282 this._expressApp.use(this._basePath, (req, res, next) => {
283 // eslint-disable-next-line no-void
284 void this._handleHttpRequest(req, res).catch(next);
285 });
286
287 // Mount our error handler
288 this._expressApp.use(this._unexpectedErrorHandler());
289 }
290
291 /**
292 * Get an Express handler for unexpected errors
293 */
294 protected _unexpectedErrorHandler(): ErrorRequestHandler {
295 const handleUnExpectedError: ErrorRequestHandler = (
296 err,
297 req,
298 res,
299 next,
300 ) => {
301 // Handle errors reported by Express middleware such as CORS
302 // First try to use the `REJECT` action
303 this.get(SequenceActions.REJECT, {optional: true})
304 .then(reject => {
305 if (reject) {
306 // TODO(rfeng): There is a possibility that the error is thrown
307 // from the `REJECT` action in the sequence
308 return reject({request: req, response: res}, err);
309 }
310 // Use strong-error handler directly
311 writeErrorToResponse(err, req, res);
312 })
313 .catch(unexpectedErr => next(unexpectedErr));
314 };
315 return handleUnExpectedError;
316 }
317
318 /**
319 * Apply express settings.
320 */
321 protected _applyExpressSettings() {
322 assertExists(this._expressApp, 'this._expressApp');
323 const settings = this.config.expressSettings;
324 for (const key in settings) {
325 this._expressApp.set(key, settings[key]);
326 }
327 if (this.config.router && typeof this.config.router.strict === 'boolean') {
328 this._expressApp.set('strict routing', this.config.router.strict);
329 }
330 }
331
332 /**
333 * Mount /openapi.json, /openapi.yaml for specs and /swagger-ui, /explorer
334 * to redirect to externally hosted API explorer
335 */
336 protected _setupOpenApiSpecEndpoints() {
337 assertExists(this._expressApp, 'this._expressApp');
338 if (this.config.openApiSpec.disabled) return;
339 const router = express.Router();
340 const mapping = this.config.openApiSpec.endpointMapping!;
341 // Serving OpenAPI spec
342 for (const p in mapping) {
343 this.addOpenApiSpecEndpoint(p, mapping[p], router);
344 }
345 const explorerPaths = ['/swagger-ui', '/explorer'];
346 router.get(explorerPaths, (req, res, next) =>
347 this._redirectToSwaggerUI(req, res, next),
348 );
349 this.expressMiddleware('middleware.apiSpec.defaults', router, {
350 group: RestMiddlewareGroups.API_SPEC,
351 upstreamGroups: RestMiddlewareGroups.CORS,
352 }).apply(
353 extensionFor(
354 RestTags.REST_MIDDLEWARE_CHAIN,
355 RestTags.ACTION_MIDDLEWARE_CHAIN,
356 ),
357 );
358 }
359
360 /**
361 * Add a new non-controller endpoint hosting a form of the OpenAPI spec.
362 *
363 * @param path Path at which to host the copy of the OpenAPI
364 * @param form Form that should be rendered from that path
365 */
366 addOpenApiSpecEndpoint(
367 path: string,
368 form: OpenApiSpecForm,
369 router?: express.Router,
370 ) {
371 if (router == null) {
372 const key = `middleware.apiSpec.${path}.${form}`;
373 if (this.contains(key)) {
374 throw new Error(
375 `The path ${path} is already configured for OpenApi hosting`,
376 );
377 }
378 const newRouter = express.Router();
379 newRouter.get(path, (req, res) => this._serveOpenApiSpec(req, res, form));
380 this.expressMiddleware(
381 () => newRouter,
382 {},
383 {
384 injectConfiguration: false,
385 key: `middleware.apiSpec.${path}.${form}`,
386 group: 'apiSpec',
387 },
388 );
389 } else {
390 router.get(path, (req, res) => this._serveOpenApiSpec(req, res, form));
391 }
392 }
393
394 protected _handleHttpRequest(request: Request, response: Response) {
395 return this.httpHandler.handleRequest(request, response);
396 }
397
398 protected _setupHandlerIfNeeded() {
399 if (this._httpHandler) return;
400
401 // Watch for binding events
402 // See https://github.com/loopbackio/loopback-next/issues/433
403 const routesObserver: ContextObserver = {
404 filter: binding =>
405 filterByKey(RestBindings.API_SPEC.key)(binding) ||
406 (filterByKey(/^(controllers|routes)\..+/)(binding) &&
407 // Exclude controller routes to avoid circular events
408 !filterByTag(RestTags.CONTROLLER_ROUTE)(binding)),
409 observe: () => {
410 // Rebuild the HttpHandler instance whenever a controller/route was
411 // added/deleted.
412 this._createHttpHandler();
413 },
414 };
415 this._routesEventSubscription = this.subscribe(routesObserver);
416
417 this._createHttpHandler();
418 }
419
420 /**
421 * Create an instance of HttpHandler and populates it with routes
422 */
423 private _createHttpHandler() {
424 /**
425 * Check if there is custom router in the context
426 */
427 const router = this.getSync(RestBindings.ROUTER, {optional: true});
428 const routingTable = new RoutingTable(router, this._externalRoutes);
429
430 this._httpHandler = new HttpHandler(this, this.config, routingTable);
431
432 // Remove controller routes
433 for (const b of this.findByTag(RestTags.CONTROLLER_ROUTE)) {
434 this.unbind(b.key);
435 }
436
437 for (const b of this.find(`${CoreBindings.CONTROLLERS}.*`)) {
438 const controllerName = b.key.replace(/^controllers\./, '');
439 const ctor = b.valueConstructor;
440 if (!ctor) {
441 throw new Error(
442 `The controller ${controllerName} was not bound via .toClass()`,
443 );
444 }
445 const apiSpec = getControllerSpec(ctor);
446 if (!apiSpec) {
447 // controller methods are specified through app.api() spec
448 debug('Skipping controller %s - no API spec provided', controllerName);
449 continue;
450 }
451
452 debug('Registering controller %s', controllerName);
453 if (apiSpec.components) {
454 this._httpHandler.registerApiComponents(apiSpec.components);
455 }
456 const controllerFactory = createControllerFactoryForBinding<object>(
457 b.key,
458 );
459 const routes = createRoutesForController(
460 apiSpec,
461 ctor,
462 controllerFactory,
463 );
464 for (const route of routes) {
465 const binding = this.bindRoute(route);
466 binding
467 .tag(RestTags.CONTROLLER_ROUTE)
468 .tag({[RestTags.CONTROLLER_BINDING]: b.key});
469 }
470 }
471
472 for (const b of this.findByTag(RestTags.REST_ROUTE)) {
473 // TODO(bajtos) should we support routes defined asynchronously?
474 const route = this.getSync<RouteEntry>(b.key);
475 this._httpHandler.registerRoute(route);
476 }
477
478 // TODO(bajtos) should we support API spec defined asynchronously?
479 const spec: OpenApiSpec = this.getSync(RestBindings.API_SPEC);
480 if (spec.components) {
481 this._httpHandler.registerApiComponents(spec.components);
482 }
483 for (const path in spec.paths) {
484 for (const verb in spec.paths[path]) {
485 const routeSpec: OperationObject = spec.paths[path][verb];
486 this._setupOperation(verb, path, routeSpec);
487 }
488 }
489 }
490
491 private _setupOperation(verb: string, path: string, spec: OperationObject) {
492 const handler = spec['x-operation'];
493 if (typeof handler === 'function') {
494 // Remove a field value that cannot be represented in JSON.
495 // Start by creating a shallow-copy of the spec, so that we don't
496 // modify the original spec object provided by user.
497 spec = Object.assign({}, spec);
498 delete spec['x-operation'];
499
500 const route = new Route(verb, path, spec, handler);
501 this._httpHandler.registerRoute(route);
502 return;
503 }
504
505 const controllerName = spec['x-controller-name'];
506 if (typeof controllerName === 'string') {
507 const b = this.getBinding(`controllers.${controllerName}`, {
508 optional: true,
509 });
510 if (!b) {
511 throw new Error(
512 `Unknown controller ${controllerName} used by "${verb} ${path}"`,
513 );
514 }
515
516 const ctor = b.valueConstructor;
517 if (!ctor) {
518 throw new Error(
519 `The controller ${controllerName} was not bound via .toClass()`,
520 );
521 }
522
523 const controllerFactory = createControllerFactoryForBinding<object>(
524 b.key,
525 );
526 const route = new ControllerRoute(
527 verb,
528 path,
529 spec,
530 ctor as ControllerClass<object>,
531 controllerFactory,
532 );
533 this._httpHandler.registerRoute(route);
534 return;
535 }
536
537 throw new Error(
538 `There is no handler configured for operation "${verb} ${path}`,
539 );
540 }
541
542 private async _serveOpenApiSpec(
543 request: Request,
544 response: Response,
545 specForm?: OpenApiSpecForm,
546 ) {
547 const requestContext = new RequestContext(
548 request,
549 response,
550 this,
551 this.config,
552 );
553
554 specForm = specForm ?? {version: '3.0.0', format: 'json'};
555 const specObj = await this.getApiSpec(requestContext);
556
557 if (specForm.format === 'json') {
558 const spec = JSON.stringify(specObj, null, 2);
559 response.setHeader('content-type', 'application/json; charset=utf-8');
560 response.end(spec, 'utf-8');
561 } else {
562 const yaml = dump(specObj, {});
563 response.setHeader('content-type', 'text/yaml; charset=utf-8');
564 response.end(yaml, 'utf-8');
565 }
566 }
567 private async _redirectToSwaggerUI(
568 request: Request,
569 response: Response,
570 next: express.NextFunction,
571 ) {
572 const config = this.config.apiExplorer;
573
574 if (config.disabled) {
575 debug('Redirect to swagger-ui was disabled by configuration.');
576 next();
577 return;
578 }
579
580 debug('Redirecting to swagger-ui from %j.', request.originalUrl);
581 const requestContext = new RequestContext(
582 request,
583 response,
584 this,
585 this.config,
586 );
587 const protocol = requestContext.requestedProtocol;
588 const baseUrl = protocol === 'http' ? config.httpUrl : config.url;
589 const openApiUrl = `${requestContext.requestedBaseUrl}/openapi.json`;
590 const fullUrl = `${baseUrl}?url=${openApiUrl}`;
591 response.redirect(302, fullUrl);
592 }
593
594 /**
595 * Register a controller class with this server.
596 *
597 * @param controllerCtor - The controller class
598 * (constructor function).
599 * @returns The newly created binding, you can use the reference to
600 * further modify the binding, e.g. lock the value to prevent further
601 * modifications.
602 *
603 * @example
604 * ```ts
605 * class MyController {
606 * }
607 * app.controller(MyController).lock();
608 * ```
609 *
610 */
611 controller(controllerCtor: ControllerClass<ControllerInstance>): Binding {
612 return this.bind('controllers.' + controllerCtor.name).toClass(
613 controllerCtor,
614 );
615 }
616
617 /**
618 * Register a new Controller-based route.
619 *
620 * @example
621 * ```ts
622 * class MyController {
623 * greet(name: string) {
624 * return `hello ${name}`;
625 * }
626 * }
627 * app.route('get', '/greet', operationSpec, MyController, 'greet');
628 * ```
629 *
630 * @param verb - HTTP verb of the endpoint
631 * @param path - URL path of the endpoint
632 * @param spec - The OpenAPI spec describing the endpoint (operation)
633 * @param controllerCtor - Controller constructor
634 * @param controllerFactory - A factory function to create controller instance
635 * @param methodName - The name of the controller method
636 */
637 route<I extends object>(
638 verb: string,
639 path: string,
640 spec: OperationObject,
641 controllerCtor: ControllerClass<I>,
642 controllerFactory: ControllerFactory<I>,
643 methodName: string,
644 ): Binding;
645
646 /**
647 * Register a new route invoking a handler function.
648 *
649 * @example
650 * ```ts
651 * function greet(name: string) {
652 * return `hello ${name}`;
653 * }
654 * app.route('get', '/', operationSpec, greet);
655 * ```
656 *
657 * @param verb - HTTP verb of the endpoint
658 * @param path - URL path of the endpoint
659 * @param spec - The OpenAPI spec describing the endpoint (operation)
660 * @param handler - The function to invoke with the request parameters
661 * described in the spec.
662 */
663 route(
664 verb: string,
665 path: string,
666 spec: OperationObject,
667 handler: Function,
668 ): Binding;
669
670 /**
671 * Register a new generic route.
672 *
673 * @example
674 * ```ts
675 * function greet(name: string) {
676 * return `hello ${name}`;
677 * }
678 * const route = new Route('get', '/', operationSpec, greet);
679 * app.route(route);
680 * ```
681 *
682 * @param route - The route to add.
683 */
684 route(route: RouteEntry): Binding;
685
686 route<T extends object>(
687 routeOrVerb: RouteEntry | string,
688 path?: string,
689 spec?: OperationObject,
690 controllerCtorOrHandler?: ControllerClass<T> | Function,
691 controllerFactory?: ControllerFactory<T>,
692 methodName?: string,
693 ): Binding {
694 if (typeof routeOrVerb === 'object') {
695 const r = routeOrVerb;
696 // Encode the path to escape special chars
697 return this.bindRoute(r);
698 }
699
700 if (!path) {
701 throw new AssertionError({
702 message: 'path is required for a controller-based route',
703 });
704 }
705
706 if (!spec) {
707 throw new AssertionError({
708 message: 'spec is required for a controller-based route',
709 });
710 }
711
712 if (arguments.length === 4) {
713 if (!controllerCtorOrHandler) {
714 throw new AssertionError({
715 message: 'handler function is required for a handler-based route',
716 });
717 }
718 return this.route(
719 new Route(routeOrVerb, path, spec, controllerCtorOrHandler as Function),
720 );
721 }
722
723 if (!controllerCtorOrHandler) {
724 throw new AssertionError({
725 message: 'controller is required for a controller-based route',
726 });
727 }
728
729 if (!methodName) {
730 throw new AssertionError({
731 message: 'methodName is required for a controller-based route',
732 });
733 }
734
735 return this.route(
736 new ControllerRoute(
737 routeOrVerb,
738 path,
739 spec,
740 controllerCtorOrHandler as ControllerClass<T>,
741 controllerFactory,
742 methodName,
743 ),
744 );
745 }
746
747 private bindRoute(r: RouteEntry) {
748 const namespace = RestBindings.ROUTES;
749 const encodedPath = encodeURIComponent(r.path).replace(/\./g, '%2E');
750 return this.bind(`${namespace}.${r.verb} ${encodedPath}`)
751 .to(r)
752 .tag(RestTags.REST_ROUTE)
753 .tag({[RestTags.ROUTE_VERB]: r.verb, [RestTags.ROUTE_PATH]: r.path});
754 }
755
756 /**
757 * Register a route redirecting callers to a different URL.
758 *
759 * @example
760 * ```ts
761 * server.redirect('/explorer', '/explorer/');
762 * ```
763 *
764 * @param fromPath - URL path of the redirect endpoint
765 * @param toPathOrUrl - Location (URL path or full URL) where to redirect to.
766 * If your server is configured with a custom `basePath`, then the base path
767 * is prepended to the target location.
768 * @param statusCode - HTTP status code to respond with,
769 * defaults to 303 (See Other).
770 */
771 redirect(
772 fromPath: string,
773 toPathOrUrl: string,
774 statusCode?: number,
775 ): Binding {
776 return this.route(
777 new RedirectRoute(fromPath, this._basePath + toPathOrUrl, statusCode),
778 );
779 }
780
781 /*
782 * Registry of external routes & static assets
783 */
784 private _externalRoutes = new ExternalExpressRoutes();
785
786 /**
787 * Mount static assets to the REST server.
788 * See https://expressjs.com/en/4x/api.html#express.static
789 * @param path - The path(s) to serve the asset.
790 * See examples at https://expressjs.com/en/4x/api.html#path-examples
791 * @param rootDir - The root directory from which to serve static assets
792 * @param options - Options for serve-static
793 */
794 static(path: PathParams, rootDir: string, options?: ServeStaticOptions) {
795 this._externalRoutes.registerAssets(path, rootDir, options);
796 }
797
798 /**
799 * Set the OpenAPI specification that defines the REST API schema for this
800 * server. All routes, parameter definitions and return types will be defined
801 * in this way.
802 *
803 * Note that this will override any routes defined via decorators at the
804 * controller level (this function takes precedent).
805 *
806 * @param spec - The OpenAPI specification, as an object.
807 * @returns Binding for the spec
808 *
809 */
810 api(spec: OpenApiSpec): Binding {
811 return this.bind(RestBindings.API_SPEC).to(spec);
812 }
813
814 /**
815 * Get the OpenAPI specification describing the REST API provided by
816 * this application.
817 *
818 * This method merges operations (HTTP endpoints) from the following sources:
819 * - `app.api(spec)`
820 * - `app.controller(MyController)`
821 * - `app.route(route)`
822 * - `app.route('get', '/greet', operationSpec, MyController, 'greet')`
823 *
824 * If the optional `requestContext` is provided, then the `servers` list
825 * in the returned spec will be updated to work in that context.
826 * Specifically:
827 * 1. if `config.openApi.setServersFromRequest` is enabled, the servers
828 * list will be replaced with the context base url
829 * 2. Any `servers` entries with a path of `/` will have that path
830 * replaced with `requestContext.basePath`
831 *
832 * @param requestContext - Optional context to update the `servers` list
833 * in the returned spec
834 */
835 async getApiSpec(requestContext?: RequestContext): Promise<OpenApiSpec> {
836 let spec = await this.get<OpenApiSpec>(RestBindings.API_SPEC);
837 spec = cloneDeep(spec);
838 const components = this.httpHandler.getApiComponents();
839
840 // Apply deep clone to prevent getApiSpec() callers from
841 // accidentally modifying our internal routing data
842 const paths = cloneDeep(this.httpHandler.describeApiPaths());
843 spec.paths = {...paths, ...spec.paths};
844 if (components) {
845 const defs = cloneDeep(components);
846 spec.components = {...spec.components, ...defs};
847 }
848
849 assignRouterSpec(spec, this._externalRoutes.routerSpec);
850
851 if (requestContext) {
852 spec = this.updateSpecFromRequest(spec, requestContext);
853 }
854
855 // Apply OAS enhancers to the OpenAPI specification
856 this.OASEnhancer.spec = spec;
857 spec = await this.OASEnhancer.applyAllEnhancers();
858
859 return spec;
860 }
861
862 /**
863 * Update or rebuild OpenAPI Spec object to be appropriate for the context of
864 * a specific request for the spec, leveraging both app config and request
865 * path information.
866 *
867 * @param spec base spec object from which to start
868 * @param requestContext request to use to infer path information
869 * @returns Updated or rebuilt spec object to use in the context of the request
870 */
871 private updateSpecFromRequest(
872 spec: OpenAPIObject,
873 requestContext: RequestContext,
874 ) {
875 if (this.config.openApiSpec.setServersFromRequest) {
876 spec = Object.assign({}, spec);
877 spec.servers = [{url: requestContext.requestedBaseUrl}];
878 }
879
880 const basePath = requestContext.basePath;
881 if (spec.servers && basePath) {
882 for (const s of spec.servers) {
883 // Update the default server url to honor `basePath`
884 if (s.url === '/') {
885 s.url = basePath;
886 }
887 }
888 }
889
890 return spec;
891 }
892
893 /**
894 * Configure a custom sequence class for handling incoming requests.
895 *
896 * @example
897 * ```ts
898 * class MySequence implements SequenceHandler {
899 * constructor(
900 * @inject('send) public send: Send)) {
901 * }
902 *
903 * public async handle({response}: RequestContext) {
904 * send(response, 'hello world');
905 * }
906 * }
907 * ```
908 *
909 * @param sequenceClass - The sequence class to invoke for each incoming request.
910 */
911 public sequence(sequenceClass: Constructor<SequenceHandler>) {
912 const sequenceBinding = createBindingFromClass(sequenceClass, {
913 key: RestBindings.SEQUENCE,
914 });
915 this.add(sequenceBinding);
916 return sequenceBinding;
917 }
918
919 /**
920 * Configure a custom sequence function for handling incoming requests.
921 *
922 * @example
923 * ```ts
924 * app.handler(({request, response}, sequence) => {
925 * sequence.send(response, 'hello world');
926 * });
927 * ```
928 *
929 * @param handlerFn - The handler to invoke for each incoming request.
930 */
931 public handler(handlerFn: SequenceFunction) {
932 class SequenceFromFunction extends DefaultSequence {
933 async handle(context: RequestContext): Promise<void> {
934 return handlerFn(context, this);
935 }
936 }
937
938 this.sequence(SequenceFromFunction);
939 }
940
941 /**
942 * Bind a body parser to the server context
943 * @param parserClass - Body parser class
944 * @param address - Optional binding address
945 */
946 bodyParser(
947 bodyParserClass: Constructor<BodyParser>,
948 address?: BindingAddress<BodyParser>,
949 ): Binding<BodyParser> {
950 const binding = createBodyParserBinding(bodyParserClass, address);
951 this.add(binding);
952 return binding;
953 }
954
955 /**
956 * Configure the `basePath` for the rest server
957 * @param path - Base path
958 */
959 basePath(path = '') {
960 if (this._requestHandler != null) {
961 throw new Error(
962 'Base path cannot be set as the request handler has been created',
963 );
964 }
965 // Trim leading and trailing `/`
966 path = path.replace(/(^\/)|(\/$)/, '');
967 if (path) path = '/' + path;
968 this._basePath = path;
969 this.config.basePath = path;
970 }
971
972 /**
973 * Start this REST API's HTTP/HTTPS server.
974 */
975 async start(): Promise<void> {
976 // Set up the Express app if not done yet
977 this._setupRequestHandlerIfNeeded();
978 // Setup the HTTP handler so that we can verify the configuration
979 // of API spec, controllers and routes at startup time.
980 this._setupHandlerIfNeeded();
981
982 const port = await this.get(RestBindings.PORT);
983 const host = await this.get(RestBindings.HOST);
984 const path = await this.get(RestBindings.PATH);
985 const protocol = await this.get(RestBindings.PROTOCOL);
986 const httpsOptions = await this.get(RestBindings.HTTPS_OPTIONS);
987
988 if (this.config.listenOnStart === false) {
989 debug(
990 'RestServer is not listening as listenOnStart flag is set to false.',
991 );
992 return;
993 }
994
995 const serverOptions = {...httpsOptions, port, host, protocol, path};
996 this._httpServer = new HttpServer(this.requestHandler, serverOptions);
997
998 await this._httpServer.start();
999
1000 this.bind(RestBindings.PORT).to(this._httpServer.port);
1001 this.bind(RestBindings.HOST).to(this._httpServer.host);
1002 this.bind(RestBindings.URL).to(this._httpServer.url);
1003 debug('RestServer listening at %s', this._httpServer.url);
1004 }
1005
1006 /**
1007 * Stop this REST API's HTTP/HTTPS server.
1008 */
1009 async stop() {
1010 // Kill the server instance.
1011 if (!this._httpServer) return;
1012 await this._httpServer.stop();
1013 this._httpServer = undefined;
1014 }
1015
1016 /**
1017 * Mount an Express router to expose additional REST endpoints handled
1018 * via legacy Express-based stack.
1019 *
1020 * @param basePath - Path where to mount the router at, e.g. `/` or `/api`.
1021 * @param router - The Express router to handle the requests.
1022 * @param spec - A partial OpenAPI spec describing endpoints provided by the
1023 * router. LoopBack will prepend `basePath` to all endpoints automatically.
1024 * This argument is optional. You can leave it out if you don't want to
1025 * document the routes.
1026 */
1027 mountExpressRouter(
1028 basePath: string,
1029 router: ExpressRequestHandler,
1030 spec?: RouterSpec,
1031 ): void {
1032 this._externalRoutes.mountRouter(basePath, router, spec);
1033 }
1034
1035 /**
1036 * Export the OpenAPI spec to the given json or yaml file
1037 * @param outFile - File name for the spec. The extension of the file
1038 * determines the format of the file.
1039 * - `yaml` or `yml`: YAML
1040 * - `json` or other: JSON
1041 * If the outFile is not provided or its value is `''` or `'-'`, the spec is
1042 * written to the console using the `log` function.
1043 * @param log - Log function, default to `console.log`
1044 */
1045 async exportOpenApiSpec(outFile = '', log = console.log): Promise<void> {
1046 const spec = await this.getApiSpec();
1047 if (outFile === '-' || outFile === '') {
1048 const json = JSON.stringify(spec, null, 2);
1049 log('%s', json);
1050 return;
1051 }
1052 const fileName = outFile.toLowerCase();
1053 if (fileName.endsWith('.yaml') || fileName.endsWith('.yml')) {
1054 const yaml = dump(spec);
1055 fs.writeFileSync(outFile, yaml, 'utf-8');
1056 } else {
1057 const json = JSON.stringify(spec, null, 2);
1058 fs.writeFileSync(outFile, json, 'utf-8');
1059 }
1060 log('The OpenAPI spec has been saved to %s.', outFile);
1061 }
1062}
1063
1064/**
1065 * An assertion type guard for TypeScript to instruct the compiler that the
1066 * given value is not `null` or `undefined.
1067 * @param val - A value can be `undefined` or `null`
1068 * @param name - Name of the value
1069 */
1070function assertExists<T>(val: T, name: string): asserts val is NonNullable<T> {
1071 assert(val != null, `The value of ${name} cannot be null or undefined`);
1072}
1073
1074/**
1075 * Create a binding for the given body parser class
1076 * @param parserClass - Body parser class
1077 * @param key - Optional binding address
1078 */
1079export function createBodyParserBinding(
1080 parserClass: Constructor<BodyParser>,
1081 key?: BindingAddress<BodyParser>,
1082): Binding<BodyParser> {
1083 const address =
1084 key ?? `${RestBindings.REQUEST_BODY_PARSER}.${parserClass.name}`;
1085 return Binding.bind<BodyParser>(address)
1086 .toClass(parserClass)
1087 .inScope(BindingScope.TRANSIENT)
1088 .tag(REQUEST_BODY_PARSER_TAG);
1089}
1090
1091/**
1092 * The form of OpenAPI specs to be served
1093 */
1094export interface OpenApiSpecForm {
1095 version?: string;
1096 format?: string;
1097}
1098
1099const OPENAPI_SPEC_MAPPING: {[key: string]: OpenApiSpecForm} = {
1100 '/openapi.json': {version: '3.0.0', format: 'json'},
1101 '/openapi.yaml': {version: '3.0.0', format: 'yaml'},
1102};
1103
1104/**
1105 * Options to customize how OpenAPI specs are served
1106 */
1107export interface OpenApiSpecOptions {
1108 /**
1109 * Mapping of urls to spec forms, by default:
1110 * <br>
1111 * {
1112 * <br>
1113 * '/openapi.json': {version: '3.0.0', format: 'json'},
1114 * <br>
1115 * '/openapi.yaml': {version: '3.0.0', format: 'yaml'},
1116 * <br>
1117 * }
1118 *
1119 */
1120 endpointMapping?: {[key: string]: OpenApiSpecForm};
1121
1122 /**
1123 * A flag to force `servers` to be set from the http request for the OpenAPI
1124 * spec
1125 */
1126 setServersFromRequest?: boolean;
1127
1128 /**
1129 * Configure servers for OpenAPI spec
1130 */
1131 servers?: ServerObject[];
1132 /**
1133 * Set this flag to disable the endpoint for OpenAPI spec
1134 */
1135 disabled?: true;
1136
1137 /**
1138 * Set this flag to `false` to disable OAS schema consolidation. If not set,
1139 * the value defaults to `true`.
1140 */
1141 consolidate?: boolean;
1142}
1143
1144export interface ApiExplorerOptions {
1145 /**
1146 * URL for the hosted API explorer UI
1147 * default to https://loopback.io/api-explorer
1148 */
1149 url?: string;
1150
1151 /**
1152 * URL for the API explorer served over `http` protocol to deal with mixed
1153 * content security imposed by browsers as the spec is exposed over `http` by
1154 * default.
1155 * See https://github.com/loopbackio/loopback-next/issues/1603
1156 */
1157 httpUrl?: string;
1158
1159 /**
1160 * Set this flag to disable the built-in redirect to externally
1161 * hosted API Explorer UI.
1162 */
1163 disabled?: true;
1164}
1165
1166/**
1167 * RestServer options
1168 */
1169export type RestServerOptions = Partial<RestServerResolvedOptions>;
1170
1171export interface RestServerResolvedOptions {
1172 port: number;
1173 path?: string;
1174
1175 /**
1176 * Base path for API/static routes
1177 */
1178 basePath?: string;
1179 cors: cors.CorsOptions;
1180 openApiSpec: OpenApiSpecOptions;
1181 apiExplorer: ApiExplorerOptions;
1182 requestBodyParser?: RequestBodyParserOptions;
1183 sequence?: Constructor<SequenceHandler>;
1184 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1185 expressSettings: {[name: string]: any};
1186 router: RestRouterOptions;
1187
1188 /**
1189 * Set this flag to `false` to not listen on connections when the REST server
1190 * is started. It's useful to mount a LoopBack REST server as a route to the
1191 * facade Express application. If not set, the value is default to `true`.
1192 */
1193 listenOnStart?: boolean;
1194}
1195
1196/**
1197 * Valid configuration for the RestServer constructor.
1198 */
1199export type RestServerConfig = RestServerOptions & HttpServerOptions;
1200
1201export type RestServerResolvedConfig = RestServerResolvedOptions &
1202 HttpServerOptions;
1203
1204const DEFAULT_CONFIG: RestServerResolvedConfig = {
1205 port: 3000,
1206 openApiSpec: {},
1207 apiExplorer: {},
1208 cors: {
1209 origin: '*',
1210 methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
1211 preflightContinue: false,
1212 optionsSuccessStatus: 204,
1213 maxAge: 86400,
1214 credentials: true,
1215 },
1216 expressSettings: {},
1217 router: {},
1218 listenOnStart: true,
1219};
1220
1221function resolveRestServerConfig(
1222 config: RestServerConfig,
1223): RestServerResolvedConfig {
1224 const result: RestServerResolvedConfig = Object.assign(
1225 cloneDeep(DEFAULT_CONFIG),
1226 config,
1227 );
1228
1229 // Can't check falsiness, 0 is a valid port.
1230 if (result.port == null) {
1231 result.port = 3000;
1232 }
1233
1234 if (result.host == null) {
1235 // Set it to '' so that the http server will listen on all interfaces
1236 result.host = undefined;
1237 }
1238
1239 if (!result.openApiSpec.endpointMapping) {
1240 // mapping may be mutated by addOpenApiSpecEndpoint, be sure that doesn't
1241 // pollute the default mapping configuration
1242 result.openApiSpec.endpointMapping = cloneDeep(OPENAPI_SPEC_MAPPING);
1243 }
1244
1245 result.apiExplorer = normalizeApiExplorerConfig(config.apiExplorer);
1246
1247 if (result.openApiSpec.disabled) {
1248 // Disable apiExplorer if the OpenAPI spec endpoint is disabled
1249 result.apiExplorer.disabled = true;
1250 }
1251
1252 return result;
1253}
1254
1255function normalizeApiExplorerConfig(
1256 input: ApiExplorerOptions | undefined,
1257): ApiExplorerOptions {
1258 const config = input ?? {};
1259 const url = config.url ?? 'https://explorer.loopback.io';
1260
1261 config.httpUrl =
1262 config.httpUrl ?? config.url ?? 'http://explorer.loopback.io';
1263
1264 config.url = url;
1265
1266 return config;
1267}