UNPKG

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