UNPKG

32.8 kBJavaScriptView Raw
1"use strict";
2// Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved.
3// Node module: @loopback/rest
4// This file is licensed under the MIT License.
5// License text available at https://opensource.org/licenses/MIT
6Object.defineProperty(exports, "__esModule", { value: true });
7exports.createBodyParserBinding = exports.RestServer = void 0;
8const tslib_1 = require("tslib");
9const core_1 = require("@loopback/core");
10const express_1 = require("@loopback/express");
11const http_server_1 = require("@loopback/http-server");
12const openapi_v3_1 = require("@loopback/openapi-v3");
13const assert_1 = tslib_1.__importStar(require("assert"));
14const cors_1 = tslib_1.__importDefault(require("cors"));
15const debug_1 = tslib_1.__importDefault(require("debug"));
16const express_2 = tslib_1.__importDefault(require("express"));
17const fs_1 = tslib_1.__importDefault(require("fs"));
18const js_yaml_1 = require("js-yaml");
19const lodash_1 = require("lodash");
20const strong_error_handler_1 = require("strong-error-handler");
21const body_parsers_1 = require("./body-parsers");
22const http_handler_1 = require("./http-handler");
23const keys_1 = require("./keys");
24const request_context_1 = require("./request-context");
25const router_1 = require("./router");
26const router_spec_1 = require("./router/router-spec");
27const sequence_1 = require("./sequence");
28const debug = (0, debug_1.default)('loopback:rest:server');
29const SequenceActions = keys_1.RestBindings.SequenceActions;
30/**
31 * A REST API server for use with Loopback.
32 * Add this server to your application by importing the RestComponent.
33 *
34 * @example
35 * ```ts
36 * const app = new MyApplication();
37 * app.component(RestComponent);
38 * ```
39 *
40 * To add additional instances of RestServer to your application, use the
41 * `.server` function:
42 * ```ts
43 * app.server(RestServer, 'nameOfYourServer');
44 * ```
45 *
46 * By default, one instance of RestServer will be created when the RestComponent
47 * is bootstrapped. This instance can be retrieved with
48 * `app.getServer(RestServer)`, or by calling `app.get('servers.RestServer')`
49 * Note that retrieving other instances of RestServer must be done using the
50 * server's name:
51 * ```ts
52 * const server = await app.getServer('foo')
53 * // OR
54 * const server = await app.get('servers.foo');
55 * ```
56 */
57let RestServer = class RestServer extends express_1.BaseMiddlewareRegistry {
58 /**
59 *
60 * Creates an instance of RestServer.
61 *
62 * @param app - The application instance (injected via
63 * CoreBindings.APPLICATION_INSTANCE).
64 * @param config - The configuration options (injected via
65 * RestBindings.CONFIG).
66 *
67 */
68 constructor(app, config = {}) {
69 var _a;
70 super(app);
71 /*
72 * Registry of external routes & static assets
73 */
74 this._externalRoutes = new router_1.ExternalExpressRoutes();
75 this.scope = core_1.BindingScope.SERVER;
76 this.config = resolveRestServerConfig(config);
77 this.bind(keys_1.RestBindings.PORT).to(this.config.port);
78 this.bind(keys_1.RestBindings.HOST).to(config.host);
79 this.bind(keys_1.RestBindings.PATH).to(config.path);
80 this.bind(keys_1.RestBindings.PROTOCOL).to((_a = config.protocol) !== null && _a !== void 0 ? _a : 'http');
81 this.bind(keys_1.RestBindings.HTTPS_OPTIONS).to(config);
82 if (config.requestBodyParser) {
83 this.bind(keys_1.RestBindings.REQUEST_BODY_PARSER_OPTIONS).to(config.requestBodyParser);
84 }
85 if (config.sequence) {
86 this.sequence(config.sequence);
87 }
88 else {
89 this.sequence(sequence_1.MiddlewareSequence);
90 }
91 if (config.router) {
92 this.bind(keys_1.RestBindings.ROUTER_OPTIONS).to(config.router);
93 }
94 this.basePath(config.basePath);
95 this.bind(keys_1.RestBindings.BASE_PATH).toDynamicValue(() => this._basePath);
96 this.bind(keys_1.RestBindings.HANDLER).toDynamicValue(() => this.httpHandler);
97 }
98 // eslint-disable-next-line @typescript-eslint/naming-convention
99 get OASEnhancer() {
100 this._setupOASEnhancerIfNeeded();
101 return this.oasEnhancerService;
102 }
103 get requestHandler() {
104 if (this._requestHandler == null) {
105 this._setupRequestHandlerIfNeeded();
106 }
107 return this._requestHandler;
108 }
109 get httpHandler() {
110 this._setupHandlerIfNeeded();
111 return this._httpHandler;
112 }
113 get listening() {
114 return this._httpServer ? this._httpServer.listening : false;
115 }
116 get httpServer() {
117 return this._httpServer;
118 }
119 /**
120 * The base url for the server, including the basePath if set. For example,
121 * the value will be 'http://localhost:3000/api' if `basePath` is set to
122 * '/api'.
123 */
124 get url() {
125 let serverUrl = this.rootUrl;
126 if (!serverUrl)
127 return serverUrl;
128 serverUrl = serverUrl + (this._basePath || '');
129 return serverUrl;
130 }
131 /**
132 * The root url for the server without the basePath. For example, the value
133 * will be 'http://localhost:3000' regardless of the `basePath`.
134 */
135 get rootUrl() {
136 var _a;
137 return (_a = this._httpServer) === null || _a === void 0 ? void 0 : _a.url;
138 }
139 _setupOASEnhancerIfNeeded() {
140 if (this.oasEnhancerService != null)
141 return;
142 this.add((0, core_1.createBindingFromClass)(openapi_v3_1.OASEnhancerService, {
143 key: openapi_v3_1.OASEnhancerBindings.OAS_ENHANCER_SERVICE,
144 }));
145 this.oasEnhancerService = this.getSync(openapi_v3_1.OASEnhancerBindings.OAS_ENHANCER_SERVICE);
146 }
147 _setupRequestHandlerIfNeeded() {
148 if (this._expressApp != null)
149 return;
150 this._expressApp = (0, express_2.default)();
151 this._applyExpressSettings();
152 this._requestHandler = this._expressApp;
153 // Allow CORS support for all endpoints so that users
154 // can test with online SwaggerUI instance
155 this.expressMiddleware(cors_1.default, this.config.cors, {
156 injectConfiguration: false,
157 key: 'middleware.cors',
158 group: sequence_1.RestMiddlewareGroups.CORS,
159 }).apply((0, core_1.extensionFor)(keys_1.RestTags.REST_MIDDLEWARE_CHAIN, keys_1.RestTags.ACTION_MIDDLEWARE_CHAIN));
160 // Set up endpoints for OpenAPI spec/ui
161 this._setupOpenApiSpecEndpoints();
162 // Mount our router & request handler
163 this._expressApp.use(this._basePath, (req, res, next) => {
164 this._handleHttpRequest(req, res).catch(next);
165 });
166 // Mount our error handler
167 this._expressApp.use(this._unexpectedErrorHandler());
168 }
169 /**
170 * Get an Express handler for unexpected errors
171 */
172 _unexpectedErrorHandler() {
173 const handleUnExpectedError = (err, req, res, next) => {
174 // Handle errors reported by Express middleware such as CORS
175 // First try to use the `REJECT` action
176 this.get(SequenceActions.REJECT, { optional: true })
177 .then(reject => {
178 if (reject) {
179 // TODO(rfeng): There is a possibility that the error is thrown
180 // from the `REJECT` action in the sequence
181 return reject({ request: req, response: res }, err);
182 }
183 // Use strong-error handler directly
184 (0, strong_error_handler_1.writeErrorToResponse)(err, req, res);
185 })
186 .catch(unexpectedErr => next(unexpectedErr));
187 };
188 return handleUnExpectedError;
189 }
190 /**
191 * Apply express settings.
192 */
193 _applyExpressSettings() {
194 assertExists(this._expressApp, 'this._expressApp');
195 const settings = this.config.expressSettings;
196 for (const key in settings) {
197 this._expressApp.set(key, settings[key]);
198 }
199 if (this.config.router && typeof this.config.router.strict === 'boolean') {
200 this._expressApp.set('strict routing', this.config.router.strict);
201 }
202 }
203 /**
204 * Mount /openapi.json, /openapi.yaml for specs and /swagger-ui, /explorer
205 * to redirect to externally hosted API explorer
206 */
207 _setupOpenApiSpecEndpoints() {
208 assertExists(this._expressApp, 'this._expressApp');
209 if (this.config.openApiSpec.disabled)
210 return;
211 const router = express_2.default.Router();
212 const mapping = this.config.openApiSpec.endpointMapping;
213 // Serving OpenAPI spec
214 for (const p in mapping) {
215 this.addOpenApiSpecEndpoint(p, mapping[p], router);
216 }
217 const explorerPaths = ['/swagger-ui', '/explorer'];
218 router.get(explorerPaths, (req, res, next) => this._redirectToSwaggerUI(req, res, next));
219 this.expressMiddleware('middleware.apiSpec.defaults', router, {
220 group: sequence_1.RestMiddlewareGroups.API_SPEC,
221 upstreamGroups: sequence_1.RestMiddlewareGroups.CORS,
222 }).apply((0, core_1.extensionFor)(keys_1.RestTags.REST_MIDDLEWARE_CHAIN, keys_1.RestTags.ACTION_MIDDLEWARE_CHAIN));
223 }
224 /**
225 * Add a new non-controller endpoint hosting a form of the OpenAPI spec.
226 *
227 * @param path Path at which to host the copy of the OpenAPI
228 * @param form Form that should be rendered from that path
229 */
230 addOpenApiSpecEndpoint(path, form, router) {
231 if (router == null) {
232 const key = `middleware.apiSpec.${path}.${form}`;
233 if (this.contains(key)) {
234 throw new Error(`The path ${path} is already configured for OpenApi hosting`);
235 }
236 const newRouter = express_2.default.Router();
237 newRouter.get(path, (req, res) => this._serveOpenApiSpec(req, res, form));
238 this.expressMiddleware(() => newRouter, {}, {
239 injectConfiguration: false,
240 key: `middleware.apiSpec.${path}.${form}`,
241 group: 'apiSpec',
242 });
243 }
244 else {
245 router.get(path, (req, res) => this._serveOpenApiSpec(req, res, form));
246 }
247 }
248 _handleHttpRequest(request, response) {
249 return this.httpHandler.handleRequest(request, response);
250 }
251 _setupHandlerIfNeeded() {
252 if (this._httpHandler)
253 return;
254 // Watch for binding events
255 // See https://github.com/loopbackio/loopback-next/issues/433
256 const routesObserver = {
257 filter: binding => (0, core_1.filterByKey)(keys_1.RestBindings.API_SPEC.key)(binding) ||
258 ((0, core_1.filterByKey)(/^(controllers|routes)\..+/)(binding) &&
259 // Exclude controller routes to avoid circular events
260 !(0, core_1.filterByTag)(keys_1.RestTags.CONTROLLER_ROUTE)(binding)),
261 observe: () => {
262 // Rebuild the HttpHandler instance whenever a controller/route was
263 // added/deleted.
264 this._createHttpHandler();
265 },
266 };
267 this._routesEventSubscription = this.subscribe(routesObserver);
268 this._createHttpHandler();
269 }
270 /**
271 * Create an instance of HttpHandler and populates it with routes
272 */
273 _createHttpHandler() {
274 /**
275 * Check if there is custom router in the context
276 */
277 const router = this.getSync(keys_1.RestBindings.ROUTER, { optional: true });
278 const routingTable = new router_1.RoutingTable(router, this._externalRoutes);
279 this._httpHandler = new http_handler_1.HttpHandler(this, this.config, routingTable);
280 // Remove controller routes
281 for (const b of this.findByTag(keys_1.RestTags.CONTROLLER_ROUTE)) {
282 this.unbind(b.key);
283 }
284 for (const b of this.find(`${core_1.CoreBindings.CONTROLLERS}.*`)) {
285 const controllerName = b.key.replace(/^controllers\./, '');
286 const ctor = b.valueConstructor;
287 if (!ctor) {
288 throw new Error(`The controller ${controllerName} was not bound via .toClass()`);
289 }
290 const apiSpec = (0, openapi_v3_1.getControllerSpec)(ctor);
291 if (!apiSpec) {
292 // controller methods are specified through app.api() spec
293 debug('Skipping controller %s - no API spec provided', controllerName);
294 continue;
295 }
296 debug('Registering controller %s', controllerName);
297 if (apiSpec.components) {
298 this._httpHandler.registerApiComponents(apiSpec.components);
299 }
300 const controllerFactory = (0, router_1.createControllerFactoryForBinding)(b.key);
301 const routes = (0, router_1.createRoutesForController)(apiSpec, ctor, controllerFactory);
302 for (const route of routes) {
303 const binding = this.bindRoute(route);
304 binding
305 .tag(keys_1.RestTags.CONTROLLER_ROUTE)
306 .tag({ [keys_1.RestTags.CONTROLLER_BINDING]: b.key });
307 }
308 }
309 for (const b of this.findByTag(keys_1.RestTags.REST_ROUTE)) {
310 // TODO(bajtos) should we support routes defined asynchronously?
311 const route = this.getSync(b.key);
312 this._httpHandler.registerRoute(route);
313 }
314 // TODO(bajtos) should we support API spec defined asynchronously?
315 const spec = this.getSync(keys_1.RestBindings.API_SPEC);
316 if (spec.components) {
317 this._httpHandler.registerApiComponents(spec.components);
318 }
319 for (const path in spec.paths) {
320 for (const verb in spec.paths[path]) {
321 const routeSpec = spec.paths[path][verb];
322 this._setupOperation(verb, path, routeSpec);
323 }
324 }
325 }
326 _setupOperation(verb, path, spec) {
327 const handler = spec['x-operation'];
328 if (typeof handler === 'function') {
329 // Remove a field value that cannot be represented in JSON.
330 // Start by creating a shallow-copy of the spec, so that we don't
331 // modify the original spec object provided by user.
332 spec = Object.assign({}, spec);
333 delete spec['x-operation'];
334 const route = new router_1.Route(verb, path, spec, handler);
335 this._httpHandler.registerRoute(route);
336 return;
337 }
338 const controllerName = spec['x-controller-name'];
339 if (typeof controllerName === 'string') {
340 const b = this.getBinding(`controllers.${controllerName}`, {
341 optional: true,
342 });
343 if (!b) {
344 throw new Error(`Unknown controller ${controllerName} used by "${verb} ${path}"`);
345 }
346 const ctor = b.valueConstructor;
347 if (!ctor) {
348 throw new Error(`The controller ${controllerName} was not bound via .toClass()`);
349 }
350 const controllerFactory = (0, router_1.createControllerFactoryForBinding)(b.key);
351 const route = new router_1.ControllerRoute(verb, path, spec, ctor, controllerFactory);
352 this._httpHandler.registerRoute(route);
353 return;
354 }
355 throw new Error(`There is no handler configured for operation "${verb} ${path}`);
356 }
357 async _serveOpenApiSpec(request, response, specForm) {
358 const requestContext = new request_context_1.RequestContext(request, response, this, this.config);
359 specForm = specForm !== null && specForm !== void 0 ? specForm : { version: '3.0.0', format: 'json' };
360 const specObj = await this.getApiSpec(requestContext);
361 if (specForm.format === 'json') {
362 const spec = JSON.stringify(specObj, null, 2);
363 response.setHeader('content-type', 'application/json; charset=utf-8');
364 response.end(spec, 'utf-8');
365 }
366 else {
367 const yaml = (0, js_yaml_1.dump)(specObj, {});
368 response.setHeader('content-type', 'text/yaml; charset=utf-8');
369 response.end(yaml, 'utf-8');
370 }
371 }
372 async _redirectToSwaggerUI(request, response, next) {
373 const config = this.config.apiExplorer;
374 if (config.disabled) {
375 debug('Redirect to swagger-ui was disabled by configuration.');
376 next();
377 return;
378 }
379 debug('Redirecting to swagger-ui from %j.', request.originalUrl);
380 const requestContext = new request_context_1.RequestContext(request, response, this, this.config);
381 const protocol = requestContext.requestedProtocol;
382 const baseUrl = protocol === 'http' ? config.httpUrl : config.url;
383 const openApiUrl = `${requestContext.requestedBaseUrl}/openapi.json`;
384 const fullUrl = `${baseUrl}?url=${openApiUrl}`;
385 response.redirect(302, fullUrl);
386 }
387 /**
388 * Register a controller class with this server.
389 *
390 * @param controllerCtor - The controller class
391 * (constructor function).
392 * @returns The newly created binding, you can use the reference to
393 * further modify the binding, e.g. lock the value to prevent further
394 * modifications.
395 *
396 * @example
397 * ```ts
398 * class MyController {
399 * }
400 * app.controller(MyController).lock();
401 * ```
402 *
403 */
404 controller(controllerCtor) {
405 return this.bind('controllers.' + controllerCtor.name).toClass(controllerCtor);
406 }
407 route(routeOrVerb, path, spec, controllerCtorOrHandler, controllerFactory, methodName) {
408 if (typeof routeOrVerb === 'object') {
409 const r = routeOrVerb;
410 // Encode the path to escape special chars
411 return this.bindRoute(r);
412 }
413 if (!path) {
414 throw new assert_1.AssertionError({
415 message: 'path is required for a controller-based route',
416 });
417 }
418 if (!spec) {
419 throw new assert_1.AssertionError({
420 message: 'spec is required for a controller-based route',
421 });
422 }
423 if (arguments.length === 4) {
424 if (!controllerCtorOrHandler) {
425 throw new assert_1.AssertionError({
426 message: 'handler function is required for a handler-based route',
427 });
428 }
429 return this.route(new router_1.Route(routeOrVerb, path, spec, controllerCtorOrHandler));
430 }
431 if (!controllerCtorOrHandler) {
432 throw new assert_1.AssertionError({
433 message: 'controller is required for a controller-based route',
434 });
435 }
436 if (!methodName) {
437 throw new assert_1.AssertionError({
438 message: 'methodName is required for a controller-based route',
439 });
440 }
441 return this.route(new router_1.ControllerRoute(routeOrVerb, path, spec, controllerCtorOrHandler, controllerFactory, methodName));
442 }
443 bindRoute(r) {
444 const namespace = keys_1.RestBindings.ROUTES;
445 const encodedPath = encodeURIComponent(r.path).replace(/\./g, '%2E');
446 return this.bind(`${namespace}.${r.verb} ${encodedPath}`)
447 .to(r)
448 .tag(keys_1.RestTags.REST_ROUTE)
449 .tag({ [keys_1.RestTags.ROUTE_VERB]: r.verb, [keys_1.RestTags.ROUTE_PATH]: r.path });
450 }
451 /**
452 * Register a route redirecting callers to a different URL.
453 *
454 * @example
455 * ```ts
456 * server.redirect('/explorer', '/explorer/');
457 * ```
458 *
459 * @param fromPath - URL path of the redirect endpoint
460 * @param toPathOrUrl - Location (URL path or full URL) where to redirect to.
461 * If your server is configured with a custom `basePath`, then the base path
462 * is prepended to the target location.
463 * @param statusCode - HTTP status code to respond with,
464 * defaults to 303 (See Other).
465 */
466 redirect(fromPath, toPathOrUrl, statusCode) {
467 return this.route(new router_1.RedirectRoute(fromPath, this._basePath + toPathOrUrl, statusCode));
468 }
469 /**
470 * Mount static assets to the REST server.
471 * See https://expressjs.com/en/4x/api.html#express.static
472 * @param path - The path(s) to serve the asset.
473 * See examples at https://expressjs.com/en/4x/api.html#path-examples
474 * @param rootDir - The root directory from which to serve static assets
475 * @param options - Options for serve-static
476 */
477 static(path, rootDir, options) {
478 this._externalRoutes.registerAssets(path, rootDir, options);
479 }
480 /**
481 * Set the OpenAPI specification that defines the REST API schema for this
482 * server. All routes, parameter definitions and return types will be defined
483 * in this way.
484 *
485 * Note that this will override any routes defined via decorators at the
486 * controller level (this function takes precedent).
487 *
488 * @param spec - The OpenAPI specification, as an object.
489 * @returns Binding for the spec
490 *
491 */
492 api(spec) {
493 return this.bind(keys_1.RestBindings.API_SPEC).to(spec);
494 }
495 /**
496 * Get the OpenAPI specification describing the REST API provided by
497 * this application.
498 *
499 * This method merges operations (HTTP endpoints) from the following sources:
500 * - `app.api(spec)`
501 * - `app.controller(MyController)`
502 * - `app.route(route)`
503 * - `app.route('get', '/greet', operationSpec, MyController, 'greet')`
504 *
505 * If the optional `requestContext` is provided, then the `servers` list
506 * in the returned spec will be updated to work in that context.
507 * Specifically:
508 * 1. if `config.openApi.setServersFromRequest` is enabled, the servers
509 * list will be replaced with the context base url
510 * 2. Any `servers` entries with a path of `/` will have that path
511 * replaced with `requestContext.basePath`
512 *
513 * @param requestContext - Optional context to update the `servers` list
514 * in the returned spec
515 */
516 async getApiSpec(requestContext) {
517 let spec = await this.get(keys_1.RestBindings.API_SPEC);
518 spec = (0, lodash_1.cloneDeep)(spec);
519 const components = this.httpHandler.getApiComponents();
520 // Apply deep clone to prevent getApiSpec() callers from
521 // accidentally modifying our internal routing data
522 const paths = (0, lodash_1.cloneDeep)(this.httpHandler.describeApiPaths());
523 spec.paths = { ...paths, ...spec.paths };
524 if (components) {
525 const defs = (0, lodash_1.cloneDeep)(components);
526 spec.components = { ...spec.components, ...defs };
527 }
528 (0, router_spec_1.assignRouterSpec)(spec, this._externalRoutes.routerSpec);
529 if (requestContext) {
530 spec = this.updateSpecFromRequest(spec, requestContext);
531 }
532 // Apply OAS enhancers to the OpenAPI specification
533 this.OASEnhancer.spec = spec;
534 spec = await this.OASEnhancer.applyAllEnhancers();
535 return spec;
536 }
537 /**
538 * Update or rebuild OpenAPI Spec object to be appropriate for the context of
539 * a specific request for the spec, leveraging both app config and request
540 * path information.
541 *
542 * @param spec base spec object from which to start
543 * @param requestContext request to use to infer path information
544 * @returns Updated or rebuilt spec object to use in the context of the request
545 */
546 updateSpecFromRequest(spec, requestContext) {
547 if (this.config.openApiSpec.setServersFromRequest) {
548 spec = Object.assign({}, spec);
549 spec.servers = [{ url: requestContext.requestedBaseUrl }];
550 }
551 const basePath = requestContext.basePath;
552 if (spec.servers && basePath) {
553 for (const s of spec.servers) {
554 // Update the default server url to honor `basePath`
555 if (s.url === '/') {
556 s.url = basePath;
557 }
558 }
559 }
560 return spec;
561 }
562 /**
563 * Configure a custom sequence class for handling incoming requests.
564 *
565 * @example
566 * ```ts
567 * class MySequence implements SequenceHandler {
568 * constructor(
569 * @inject('send) public send: Send)) {
570 * }
571 *
572 * public async handle({response}: RequestContext) {
573 * send(response, 'hello world');
574 * }
575 * }
576 * ```
577 *
578 * @param sequenceClass - The sequence class to invoke for each incoming request.
579 */
580 sequence(sequenceClass) {
581 const sequenceBinding = (0, core_1.createBindingFromClass)(sequenceClass, {
582 key: keys_1.RestBindings.SEQUENCE,
583 });
584 this.add(sequenceBinding);
585 return sequenceBinding;
586 }
587 /**
588 * Configure a custom sequence function for handling incoming requests.
589 *
590 * @example
591 * ```ts
592 * app.handler(({request, response}, sequence) => {
593 * sequence.send(response, 'hello world');
594 * });
595 * ```
596 *
597 * @param handlerFn - The handler to invoke for each incoming request.
598 */
599 handler(handlerFn) {
600 class SequenceFromFunction extends sequence_1.DefaultSequence {
601 async handle(context) {
602 return handlerFn(context, this);
603 }
604 }
605 this.sequence(SequenceFromFunction);
606 }
607 /**
608 * Bind a body parser to the server context
609 * @param parserClass - Body parser class
610 * @param address - Optional binding address
611 */
612 bodyParser(bodyParserClass, address) {
613 const binding = createBodyParserBinding(bodyParserClass, address);
614 this.add(binding);
615 return binding;
616 }
617 /**
618 * Configure the `basePath` for the rest server
619 * @param path - Base path
620 */
621 basePath(path = '') {
622 if (this._requestHandler != null) {
623 throw new Error('Base path cannot be set as the request handler has been created');
624 }
625 // Trim leading and trailing `/`
626 path = path.replace(/(^\/)|(\/$)/, '');
627 if (path)
628 path = '/' + path;
629 this._basePath = path;
630 this.config.basePath = path;
631 }
632 /**
633 * Start this REST API's HTTP/HTTPS server.
634 */
635 async start() {
636 // Set up the Express app if not done yet
637 this._setupRequestHandlerIfNeeded();
638 // Setup the HTTP handler so that we can verify the configuration
639 // of API spec, controllers and routes at startup time.
640 this._setupHandlerIfNeeded();
641 const port = await this.get(keys_1.RestBindings.PORT);
642 const host = await this.get(keys_1.RestBindings.HOST);
643 const path = await this.get(keys_1.RestBindings.PATH);
644 const protocol = await this.get(keys_1.RestBindings.PROTOCOL);
645 const httpsOptions = await this.get(keys_1.RestBindings.HTTPS_OPTIONS);
646 if (this.config.listenOnStart === false) {
647 debug('RestServer is not listening as listenOnStart flag is set to false.');
648 return;
649 }
650 const serverOptions = { ...httpsOptions, port, host, protocol, path };
651 this._httpServer = new http_server_1.HttpServer(this.requestHandler, serverOptions);
652 await this._httpServer.start();
653 this.bind(keys_1.RestBindings.PORT).to(this._httpServer.port);
654 this.bind(keys_1.RestBindings.HOST).to(this._httpServer.host);
655 this.bind(keys_1.RestBindings.URL).to(this._httpServer.url);
656 debug('RestServer listening at %s', this._httpServer.url);
657 }
658 /**
659 * Stop this REST API's HTTP/HTTPS server.
660 */
661 async stop() {
662 // Kill the server instance.
663 if (!this._httpServer)
664 return;
665 await this._httpServer.stop();
666 this._httpServer = undefined;
667 }
668 /**
669 * Mount an Express router to expose additional REST endpoints handled
670 * via legacy Express-based stack.
671 *
672 * @param basePath - Path where to mount the router at, e.g. `/` or `/api`.
673 * @param router - The Express router to handle the requests.
674 * @param spec - A partial OpenAPI spec describing endpoints provided by the
675 * router. LoopBack will prepend `basePath` to all endpoints automatically.
676 * This argument is optional. You can leave it out if you don't want to
677 * document the routes.
678 */
679 mountExpressRouter(basePath, router, spec) {
680 this._externalRoutes.mountRouter(basePath, router, spec);
681 }
682 /**
683 * Export the OpenAPI spec to the given json or yaml file
684 * @param outFile - File name for the spec. The extension of the file
685 * determines the format of the file.
686 * - `yaml` or `yml`: YAML
687 * - `json` or other: JSON
688 * If the outFile is not provided or its value is `''` or `'-'`, the spec is
689 * written to the console using the `log` function.
690 * @param log - Log function, default to `console.log`
691 */
692 async exportOpenApiSpec(outFile = '', log = console.log) {
693 const spec = await this.getApiSpec();
694 if (outFile === '-' || outFile === '') {
695 const json = JSON.stringify(spec, null, 2);
696 log('%s', json);
697 return;
698 }
699 const fileName = outFile.toLowerCase();
700 if (fileName.endsWith('.yaml') || fileName.endsWith('.yml')) {
701 const yaml = (0, js_yaml_1.dump)(spec);
702 fs_1.default.writeFileSync(outFile, yaml, 'utf-8');
703 }
704 else {
705 const json = JSON.stringify(spec, null, 2);
706 fs_1.default.writeFileSync(outFile, json, 'utf-8');
707 }
708 log('The OpenAPI spec has been saved to %s.', outFile);
709 }
710};
711RestServer = tslib_1.__decorate([
712 tslib_1.__param(0, (0, core_1.inject)(core_1.CoreBindings.APPLICATION_INSTANCE)),
713 tslib_1.__param(1, (0, core_1.inject)(keys_1.RestBindings.CONFIG, { optional: true })),
714 tslib_1.__metadata("design:paramtypes", [core_1.Application, Object])
715], RestServer);
716exports.RestServer = RestServer;
717/**
718 * An assertion type guard for TypeScript to instruct the compiler that the
719 * given value is not `null` or `undefined.
720 * @param val - A value can be `undefined` or `null`
721 * @param name - Name of the value
722 */
723function assertExists(val, name) {
724 (0, assert_1.default)(val != null, `The value of ${name} cannot be null or undefined`);
725}
726/**
727 * Create a binding for the given body parser class
728 * @param parserClass - Body parser class
729 * @param key - Optional binding address
730 */
731function createBodyParserBinding(parserClass, key) {
732 const address = key !== null && key !== void 0 ? key : `${keys_1.RestBindings.REQUEST_BODY_PARSER}.${parserClass.name}`;
733 return core_1.Binding.bind(address)
734 .toClass(parserClass)
735 .inScope(core_1.BindingScope.TRANSIENT)
736 .tag(body_parsers_1.REQUEST_BODY_PARSER_TAG);
737}
738exports.createBodyParserBinding = createBodyParserBinding;
739const OPENAPI_SPEC_MAPPING = {
740 '/openapi.json': { version: '3.0.0', format: 'json' },
741 '/openapi.yaml': { version: '3.0.0', format: 'yaml' },
742};
743const DEFAULT_CONFIG = {
744 port: 3000,
745 openApiSpec: {},
746 apiExplorer: {},
747 cors: {
748 origin: '*',
749 methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
750 preflightContinue: false,
751 optionsSuccessStatus: 204,
752 maxAge: 86400,
753 credentials: true,
754 },
755 expressSettings: {},
756 router: {},
757 listenOnStart: true,
758};
759function resolveRestServerConfig(config) {
760 const result = Object.assign((0, lodash_1.cloneDeep)(DEFAULT_CONFIG), config);
761 // Can't check falsiness, 0 is a valid port.
762 if (result.port == null) {
763 result.port = 3000;
764 }
765 if (result.host == null) {
766 // Set it to '' so that the http server will listen on all interfaces
767 result.host = undefined;
768 }
769 if (!result.openApiSpec.endpointMapping) {
770 // mapping may be mutated by addOpenApiSpecEndpoint, be sure that doesn't
771 // pollute the default mapping configuration
772 result.openApiSpec.endpointMapping = (0, lodash_1.cloneDeep)(OPENAPI_SPEC_MAPPING);
773 }
774 result.apiExplorer = normalizeApiExplorerConfig(config.apiExplorer);
775 if (result.openApiSpec.disabled) {
776 // Disable apiExplorer if the OpenAPI spec endpoint is disabled
777 result.apiExplorer.disabled = true;
778 }
779 return result;
780}
781function normalizeApiExplorerConfig(input) {
782 var _a, _b, _c;
783 const config = input !== null && input !== void 0 ? input : {};
784 const url = (_a = config.url) !== null && _a !== void 0 ? _a : 'https://explorer.loopback.io';
785 config.httpUrl =
786 (_c = (_b = config.httpUrl) !== null && _b !== void 0 ? _b : config.url) !== null && _c !== void 0 ? _c : 'http://explorer.loopback.io';
787 config.url = url;
788 return config;
789}
790//# sourceMappingURL=rest.server.js.map
\No newline at end of file