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