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