1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | import {
|
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';
|
22 | import {BaseMiddlewareRegistry, ExpressRequestHandler} from '@loopback/express';
|
23 | import {HttpServer, HttpServerOptions} from '@loopback/http-server';
|
24 | import {
|
25 | getControllerSpec,
|
26 | OASEnhancerBindings,
|
27 | OASEnhancerService,
|
28 | OpenAPIObject,
|
29 | OpenApiSpec,
|
30 | OperationObject,
|
31 | ServerObject,
|
32 | } from '@loopback/openapi-v3';
|
33 | import assert, {AssertionError} from 'assert';
|
34 | import cors from 'cors';
|
35 | import debugFactory from 'debug';
|
36 | import express, {ErrorRequestHandler} from 'express';
|
37 | import {PathParams} from 'express-serve-static-core';
|
38 | import fs from 'fs';
|
39 | import {IncomingMessage, ServerResponse} from 'http';
|
40 | import {ServerOptions} from 'https';
|
41 | import {dump} from 'js-yaml';
|
42 | import {cloneDeep} from 'lodash';
|
43 | import {ServeStaticOptions} from 'serve-static';
|
44 | import {writeErrorToResponse} from 'strong-error-handler';
|
45 | import {BodyParser, REQUEST_BODY_PARSER_TAG} from './body-parsers';
|
46 | import {HttpHandler} from './http-handler';
|
47 | import {RestBindings, RestTags} from './keys';
|
48 | import {RequestContext} from './request-context';
|
49 | import {
|
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';
|
64 | import {assignRouterSpec} from './router/router-spec';
|
65 | import {
|
66 | DefaultSequence,
|
67 | MiddlewareSequence,
|
68 | RestMiddlewareGroups,
|
69 | SequenceFunction,
|
70 | SequenceHandler,
|
71 | } from './sequence';
|
72 | import {Request, RequestBodyParserOptions, Response} from './types';
|
73 |
|
74 | const debug = debugFactory('loopback:rest:server');
|
75 |
|
76 | export type HttpRequestListener = (
|
77 | req: IncomingMessage,
|
78 | res: ServerResponse,
|
79 | ) => void;
|
80 |
|
81 | export interface HttpServerLike {
|
82 | requestHandler: HttpRequestListener;
|
83 | }
|
84 |
|
85 | const SequenceActions = RestBindings.SequenceActions;
|
86 |
|
87 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 | export class RestServer
|
115 | extends BaseMiddlewareRegistry
|
116 | implements Server, HttpServerLike
|
117 | {
|
118 | |
119 |
|
120 |
|
121 |
|
122 |
|
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 | protected oasEnhancerService: OASEnhancerService;
|
139 |
|
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 |
|
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 |
|
181 |
|
182 |
|
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 |
|
193 |
|
194 |
|
195 | get rootUrl(): string | undefined {
|
196 | return this._httpServer?.url;
|
197 | }
|
198 |
|
199 | |
200 |
|
201 |
|
202 |
|
203 |
|
204 |
|
205 |
|
206 |
|
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 |
|
266 |
|
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 |
|
279 | this._setupOpenApiSpecEndpoints();
|
280 |
|
281 |
|
282 | this._expressApp.use(this._basePath, (req, res, next) => {
|
283 | this._handleHttpRequest(req, res).catch(next);
|
284 | });
|
285 |
|
286 |
|
287 | this._expressApp.use(this._unexpectedErrorHandler());
|
288 | }
|
289 |
|
290 | |
291 |
|
292 |
|
293 | protected _unexpectedErrorHandler(): ErrorRequestHandler {
|
294 | const handleUnExpectedError: ErrorRequestHandler = (
|
295 | err,
|
296 | req,
|
297 | res,
|
298 | next,
|
299 | ) => {
|
300 |
|
301 |
|
302 | this.get(SequenceActions.REJECT, {optional: true})
|
303 | .then(reject => {
|
304 | if (reject) {
|
305 |
|
306 |
|
307 | return reject({request: req, response: res}, err);
|
308 | }
|
309 |
|
310 | writeErrorToResponse(err, req, res);
|
311 | })
|
312 | .catch(unexpectedErr => next(unexpectedErr));
|
313 | };
|
314 | return handleUnExpectedError;
|
315 | }
|
316 |
|
317 | |
318 |
|
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 |
|
333 |
|
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 |
|
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 |
|
361 |
|
362 |
|
363 |
|
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 |
|
401 |
|
402 | const routesObserver: ContextObserver = {
|
403 | filter: binding =>
|
404 | filterByKey(RestBindings.API_SPEC.key)(binding) ||
|
405 | (filterByKey(/^(controllers|routes)\..+/)(binding) &&
|
406 |
|
407 | !filterByTag(RestTags.CONTROLLER_ROUTE)(binding)),
|
408 | observe: () => {
|
409 |
|
410 |
|
411 | this._createHttpHandler();
|
412 | },
|
413 | };
|
414 | this._routesEventSubscription = this.subscribe(routesObserver);
|
415 |
|
416 | this._createHttpHandler();
|
417 | }
|
418 |
|
419 | |
420 |
|
421 |
|
422 | private _createHttpHandler() {
|
423 | |
424 |
|
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 |
|
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 |
|
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 |
|
473 | const route = this.getSync<RouteEntry>(b.key);
|
474 | this._httpHandler.registerRoute(route);
|
475 | }
|
476 |
|
477 |
|
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 |
|
494 |
|
495 |
|
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 |
|
595 |
|
596 |
|
597 |
|
598 |
|
599 |
|
600 |
|
601 |
|
602 |
|
603 |
|
604 |
|
605 |
|
606 |
|
607 |
|
608 |
|
609 |
|
610 | controller(controllerCtor: ControllerClass<ControllerInstance>): Binding {
|
611 | return this.bind('controllers.' + controllerCtor.name).toClass(
|
612 | controllerCtor,
|
613 | );
|
614 | }
|
615 |
|
616 | |
617 |
|
618 |
|
619 |
|
620 |
|
621 |
|
622 |
|
623 |
|
624 |
|
625 |
|
626 |
|
627 |
|
628 |
|
629 |
|
630 |
|
631 |
|
632 |
|
633 |
|
634 |
|
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 |
|
647 |
|
648 |
|
649 |
|
650 |
|
651 |
|
652 |
|
653 |
|
654 |
|
655 |
|
656 |
|
657 |
|
658 |
|
659 |
|
660 |
|
661 |
|
662 | route(
|
663 | verb: string,
|
664 | path: string,
|
665 | spec: OperationObject,
|
666 | handler: Function,
|
667 | ): Binding;
|
668 |
|
669 | |
670 |
|
671 |
|
672 |
|
673 |
|
674 |
|
675 |
|
676 |
|
677 |
|
678 |
|
679 |
|
680 |
|
681 |
|
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 |
|
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 |
|
757 |
|
758 |
|
759 |
|
760 |
|
761 |
|
762 |
|
763 |
|
764 |
|
765 |
|
766 |
|
767 |
|
768 |
|
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 |
|
782 |
|
783 | private _externalRoutes = new ExternalExpressRoutes();
|
784 |
|
785 | |
786 |
|
787 |
|
788 |
|
789 |
|
790 |
|
791 |
|
792 |
|
793 | static(path: PathParams, rootDir: string, options?: ServeStaticOptions) {
|
794 | this._externalRoutes.registerAssets(path, rootDir, options);
|
795 | }
|
796 |
|
797 | |
798 |
|
799 |
|
800 |
|
801 |
|
802 |
|
803 |
|
804 |
|
805 |
|
806 |
|
807 |
|
808 |
|
809 | api(spec: OpenApiSpec): Binding {
|
810 | return this.bind(RestBindings.API_SPEC).to(spec);
|
811 | }
|
812 |
|
813 | |
814 |
|
815 |
|
816 |
|
817 |
|
818 |
|
819 |
|
820 |
|
821 |
|
822 |
|
823 |
|
824 |
|
825 |
|
826 |
|
827 |
|
828 |
|
829 |
|
830 |
|
831 |
|
832 |
|
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 |
|
840 |
|
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 |
|
855 | this.OASEnhancer.spec = spec;
|
856 | spec = await this.OASEnhancer.applyAllEnhancers();
|
857 |
|
858 | return spec;
|
859 | }
|
860 |
|
861 | |
862 |
|
863 |
|
864 |
|
865 |
|
866 |
|
867 |
|
868 |
|
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 |
|
883 | if (s.url === '/') {
|
884 | s.url = basePath;
|
885 | }
|
886 | }
|
887 | }
|
888 |
|
889 | return spec;
|
890 | }
|
891 |
|
892 | |
893 |
|
894 |
|
895 |
|
896 |
|
897 |
|
898 |
|
899 |
|
900 |
|
901 |
|
902 |
|
903 |
|
904 |
|
905 |
|
906 |
|
907 |
|
908 |
|
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 |
|
920 |
|
921 |
|
922 |
|
923 |
|
924 |
|
925 |
|
926 |
|
927 |
|
928 |
|
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 |
|
942 |
|
943 |
|
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 |
|
956 |
|
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 |
|
965 | path = path.replace(/(^\/)|(\/$)/, '');
|
966 | if (path) path = '/' + path;
|
967 | this._basePath = path;
|
968 | this.config.basePath = path;
|
969 | }
|
970 |
|
971 | |
972 |
|
973 |
|
974 | async start(): Promise<void> {
|
975 |
|
976 | this._setupRequestHandlerIfNeeded();
|
977 |
|
978 |
|
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 |
|
1007 |
|
1008 | async stop() {
|
1009 |
|
1010 | if (!this._httpServer) return;
|
1011 | await this._httpServer.stop();
|
1012 | this._httpServer = undefined;
|
1013 | }
|
1014 |
|
1015 | |
1016 |
|
1017 |
|
1018 |
|
1019 |
|
1020 |
|
1021 |
|
1022 |
|
1023 |
|
1024 |
|
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 |
|
1036 |
|
1037 |
|
1038 |
|
1039 |
|
1040 |
|
1041 |
|
1042 |
|
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 |
|
1065 |
|
1066 |
|
1067 |
|
1068 |
|
1069 | function 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 |
|
1075 |
|
1076 |
|
1077 |
|
1078 | export 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 |
|
1092 |
|
1093 | export interface OpenApiSpecForm {
|
1094 | version?: string;
|
1095 | format?: string;
|
1096 | }
|
1097 |
|
1098 | const 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 |
|
1105 |
|
1106 | export interface OpenApiSpecOptions {
|
1107 | |
1108 |
|
1109 |
|
1110 |
|
1111 |
|
1112 |
|
1113 |
|
1114 |
|
1115 |
|
1116 |
|
1117 |
|
1118 |
|
1119 | endpointMapping?: {[key: string]: OpenApiSpecForm};
|
1120 |
|
1121 | |
1122 |
|
1123 |
|
1124 |
|
1125 | setServersFromRequest?: boolean;
|
1126 |
|
1127 | |
1128 |
|
1129 |
|
1130 | servers?: ServerObject[];
|
1131 | |
1132 |
|
1133 |
|
1134 | disabled?: true;
|
1135 |
|
1136 | |
1137 |
|
1138 |
|
1139 |
|
1140 | consolidate?: boolean;
|
1141 | }
|
1142 |
|
1143 | export interface ApiExplorerOptions {
|
1144 | |
1145 |
|
1146 |
|
1147 |
|
1148 | url?: string;
|
1149 |
|
1150 | |
1151 |
|
1152 |
|
1153 |
|
1154 |
|
1155 |
|
1156 | httpUrl?: string;
|
1157 |
|
1158 | |
1159 |
|
1160 |
|
1161 |
|
1162 | disabled?: true;
|
1163 | }
|
1164 |
|
1165 |
|
1166 |
|
1167 |
|
1168 | export type RestServerOptions = Partial<RestServerResolvedOptions>;
|
1169 |
|
1170 | export interface RestServerResolvedOptions {
|
1171 | port: number;
|
1172 | path?: string;
|
1173 |
|
1174 | |
1175 |
|
1176 |
|
1177 | basePath?: string;
|
1178 | cors: cors.CorsOptions;
|
1179 | openApiSpec: OpenApiSpecOptions;
|
1180 | apiExplorer: ApiExplorerOptions;
|
1181 | requestBodyParser?: RequestBodyParserOptions;
|
1182 | sequence?: Constructor<SequenceHandler>;
|
1183 |
|
1184 | expressSettings: {[name: string]: any};
|
1185 | router: RestRouterOptions;
|
1186 |
|
1187 | |
1188 |
|
1189 |
|
1190 |
|
1191 |
|
1192 | listenOnStart?: boolean;
|
1193 | }
|
1194 |
|
1195 |
|
1196 |
|
1197 |
|
1198 | export type RestServerConfig = RestServerOptions & HttpServerOptions;
|
1199 |
|
1200 | export type RestServerResolvedConfig = RestServerResolvedOptions &
|
1201 | HttpServerOptions;
|
1202 |
|
1203 | const 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 |
|
1220 | function resolveRestServerConfig(
|
1221 | config: RestServerConfig,
|
1222 | ): RestServerResolvedConfig {
|
1223 | const result: RestServerResolvedConfig = Object.assign(
|
1224 | cloneDeep(DEFAULT_CONFIG),
|
1225 | config,
|
1226 | );
|
1227 |
|
1228 |
|
1229 | if (result.port == null) {
|
1230 | result.port = 3000;
|
1231 | }
|
1232 |
|
1233 | if (result.host == null) {
|
1234 |
|
1235 | result.host = undefined;
|
1236 | }
|
1237 |
|
1238 | if (!result.openApiSpec.endpointMapping) {
|
1239 |
|
1240 |
|
1241 | result.openApiSpec.endpointMapping = cloneDeep(OPENAPI_SPEC_MAPPING);
|
1242 | }
|
1243 |
|
1244 | result.apiExplorer = normalizeApiExplorerConfig(config.apiExplorer);
|
1245 |
|
1246 | if (result.openApiSpec.disabled) {
|
1247 |
|
1248 | result.apiExplorer.disabled = true;
|
1249 | }
|
1250 |
|
1251 | return result;
|
1252 | }
|
1253 |
|
1254 | function 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 | }
|