1 | // Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved.
|
2 | // Node module: @loopback/rest
|
3 | // This file is licensed under the MIT License.
|
4 | // License text available at https://opensource.org/licenses/MIT
|
5 |
|
6 | import {
|
7 | Application,
|
8 | ApplicationConfig,
|
9 | Binding,
|
10 | BindingAddress,
|
11 | Constructor,
|
12 | Context,
|
13 | Provider,
|
14 | Server,
|
15 | } from '@loopback/core';
|
16 | import {
|
17 | ExpressMiddlewareFactory,
|
18 | ExpressRequestHandler,
|
19 | Middleware,
|
20 | MiddlewareBindingOptions,
|
21 | } from '@loopback/express';
|
22 | import {OpenApiSpec, OperationObject} from '@loopback/openapi-v3';
|
23 | import {PathParams} from 'express-serve-static-core';
|
24 | import {ServeStaticOptions} from 'serve-static';
|
25 | import {format} from 'util';
|
26 | import {BodyParser} from './body-parsers';
|
27 | import {RestBindings} from './keys';
|
28 | import {RestComponent} from './rest.component';
|
29 | import {HttpRequestListener, HttpServerLike, RestServer} from './rest.server';
|
30 | import {ControllerClass, ControllerFactory, RouteEntry} from './router';
|
31 | import {RouterSpec} from './router/router-spec';
|
32 | import {SequenceFunction, SequenceHandler} from './sequence';
|
33 |
|
34 | export const ERR_NO_MULTI_SERVER = format(
|
35 | 'RestApplication does not support multiple servers!',
|
36 | 'To create your own server bindings, please extend the Application class.',
|
37 | );
|
38 |
|
39 | // To help cut down on verbosity!
|
40 | export const SequenceActions = RestBindings.SequenceActions;
|
41 |
|
42 | /**
|
43 | * An implementation of the Application class that automatically provides
|
44 | * an instance of a REST server. This application class is intended to be
|
45 | * a single-server implementation. Any attempt to bind additional servers
|
46 | * will throw an error.
|
47 | *
|
48 | */
|
49 | export class RestApplication extends Application implements HttpServerLike {
|
50 | /**
|
51 | * The main REST server instance providing REST API for this application.
|
52 | */
|
53 | get restServer(): RestServer {
|
54 | // FIXME(kjdelisle): I attempted to mimic the pattern found in RestServer
|
55 | // with no success, so until I've got a better way, this is functional.
|
56 | return this.getSync<RestServer>('servers.RestServer');
|
57 | }
|
58 |
|
59 | /**
|
60 | * Handle incoming HTTP(S) request by invoking the corresponding
|
61 | * Controller method via the configured Sequence.
|
62 | *
|
63 | * @example
|
64 | *
|
65 | * ```ts
|
66 | * const app = new RestApplication();
|
67 | * // setup controllers, etc.
|
68 | *
|
69 | * const server = http.createServer(app.requestHandler);
|
70 | * server.listen(3000);
|
71 | * ```
|
72 | *
|
73 | * @param req - The request.
|
74 | * @param res - The response.
|
75 | */
|
76 | get requestHandler(): HttpRequestListener {
|
77 | return this.restServer.requestHandler;
|
78 | }
|
79 |
|
80 | /**
|
81 | * Create a REST application with the given parent context
|
82 | * @param parent - Parent context
|
83 | */
|
84 | constructor(parent: Context);
|
85 | /**
|
86 | * Create a REST application with the given configuration and parent context
|
87 | * @param config - Application configuration
|
88 | * @param parent - Parent context
|
89 | */
|
90 | constructor(config?: ApplicationConfig, parent?: Context);
|
91 |
|
92 | constructor(configOrParent?: ApplicationConfig | Context, parent?: Context) {
|
93 | super(configOrParent, parent);
|
94 | this.component(RestComponent);
|
95 | }
|
96 |
|
97 | server(server: Constructor<Server>, name?: string): Binding {
|
98 | if (this.findByTag('server').length > 0) {
|
99 | throw new Error(ERR_NO_MULTI_SERVER);
|
100 | }
|
101 | return super.server(server, name);
|
102 | }
|
103 |
|
104 | sequence(sequence: Constructor<SequenceHandler>): Binding {
|
105 | return this.restServer.sequence(sequence);
|
106 | }
|
107 |
|
108 | handler(handlerFn: SequenceFunction) {
|
109 | this.restServer.handler(handlerFn);
|
110 | }
|
111 |
|
112 | /**
|
113 | * Mount static assets to the REST server.
|
114 | * See https://expressjs.com/en/4x/api.html#express.static
|
115 | * @param path - The path(s) to serve the asset.
|
116 | * See examples at https://expressjs.com/en/4x/api.html#path-examples
|
117 | * To avoid performance penalty, `/` is not allowed for now.
|
118 | * @param rootDir - The root directory from which to serve static assets
|
119 | * @param options - Options for serve-static
|
120 | */
|
121 | static(path: PathParams, rootDir: string, options?: ServeStaticOptions) {
|
122 | this.restServer.static(path, rootDir, options);
|
123 | }
|
124 |
|
125 | /**
|
126 | * Bind a body parser to the server context
|
127 | * @param parserClass - Body parser class
|
128 | * @param address - Optional binding address
|
129 | */
|
130 | bodyParser(
|
131 | bodyParserClass: Constructor<BodyParser>,
|
132 | address?: BindingAddress<BodyParser>,
|
133 | ): Binding<BodyParser> {
|
134 | return this.restServer.bodyParser(bodyParserClass, address);
|
135 | }
|
136 |
|
137 | /**
|
138 | * Configure the `basePath` for the rest server
|
139 | * @param path - Base path
|
140 | */
|
141 | basePath(path = '') {
|
142 | this.restServer.basePath(path);
|
143 | }
|
144 |
|
145 | /**
|
146 | * Bind an Express middleware to this server context
|
147 | *
|
148 | * @example
|
149 | * ```ts
|
150 | * import myExpressMiddlewareFactory from 'my-express-middleware';
|
151 | * const myExpressMiddlewareConfig= {};
|
152 | * const myExpressMiddleware = myExpressMiddlewareFactory(myExpressMiddlewareConfig);
|
153 | * server.expressMiddleware('middleware.express.my', myExpressMiddleware);
|
154 | * ```
|
155 | * @param key - Middleware binding key
|
156 | * @param middleware - Express middleware handler function(s)
|
157 | *
|
158 | */
|
159 | expressMiddleware(
|
160 | key: BindingAddress,
|
161 | middleware: ExpressRequestHandler | ExpressRequestHandler[],
|
162 | options?: MiddlewareBindingOptions,
|
163 | ): Binding<Middleware>;
|
164 |
|
165 | /**
|
166 | * Bind an Express middleware to this server context
|
167 | *
|
168 | * @example
|
169 | * ```ts
|
170 | * import myExpressMiddlewareFactory from 'my-express-middleware';
|
171 | * const myExpressMiddlewareConfig= {};
|
172 | * server.expressMiddleware(myExpressMiddlewareFactory, myExpressMiddlewareConfig);
|
173 | * ```
|
174 | * @param middlewareFactory - Middleware module name or factory function
|
175 | * @param middlewareConfig - Middleware config
|
176 | * @param options - Options for registration
|
177 | *
|
178 | * @typeParam CFG - Configuration type
|
179 | */
|
180 | expressMiddleware<CFG>(
|
181 | middlewareFactory: ExpressMiddlewareFactory<CFG>,
|
182 | middlewareConfig?: CFG,
|
183 | options?: MiddlewareBindingOptions,
|
184 | ): Binding<Middleware>;
|
185 |
|
186 | expressMiddleware<CFG>(
|
187 | factoryOrKey: ExpressMiddlewareFactory<CFG> | BindingAddress<Middleware>,
|
188 | configOrHandlers: CFG | ExpressRequestHandler | ExpressRequestHandler[],
|
189 | options: MiddlewareBindingOptions = {},
|
190 | ): Binding<Middleware> {
|
191 | return this.restServer.expressMiddleware(
|
192 | factoryOrKey,
|
193 | configOrHandlers,
|
194 | options,
|
195 | );
|
196 | }
|
197 |
|
198 | /**
|
199 | * Register a middleware function or provider class
|
200 | *
|
201 | * @example
|
202 | * ```ts
|
203 | * const log: Middleware = async (requestCtx, next) {
|
204 | * // ...
|
205 | * }
|
206 | * server.middleware(log);
|
207 | * ```
|
208 | *
|
209 | * @param middleware - Middleware function or provider class
|
210 | * @param options - Middleware binding options
|
211 | */
|
212 | middleware(
|
213 | middleware: Middleware | Constructor<Provider<Middleware>>,
|
214 | options: MiddlewareBindingOptions = {},
|
215 | ): Binding<Middleware> {
|
216 | return this.restServer.middleware(middleware, options);
|
217 | }
|
218 |
|
219 | /**
|
220 | * Register a new Controller-based route.
|
221 | *
|
222 | * @example
|
223 | * ```ts
|
224 | * class MyController {
|
225 | * greet(name: string) {
|
226 | * return `hello ${name}`;
|
227 | * }
|
228 | * }
|
229 | * app.route('get', '/greet', operationSpec, MyController, 'greet');
|
230 | * ```
|
231 | *
|
232 | * @param verb - HTTP verb of the endpoint
|
233 | * @param path - URL path of the endpoint
|
234 | * @param spec - The OpenAPI spec describing the endpoint (operation)
|
235 | * @param controllerCtor - Controller constructor
|
236 | * @param controllerFactory - A factory function to create controller instance
|
237 | * @param methodName - The name of the controller method
|
238 | */
|
239 | route<T extends object>(
|
240 | verb: string,
|
241 | path: string,
|
242 | spec: OperationObject,
|
243 | controllerCtor: ControllerClass<T>,
|
244 | controllerFactory: ControllerFactory<T>,
|
245 | methodName: string,
|
246 | ): Binding;
|
247 |
|
248 | /**
|
249 | * Register a new route invoking a handler function.
|
250 | *
|
251 | * @example
|
252 | * ```ts
|
253 | * function greet(name: string) {
|
254 | * return `hello ${name}`;
|
255 | * }
|
256 | * app.route('get', '/', operationSpec, greet);
|
257 | * ```
|
258 | *
|
259 | * @param verb - HTTP verb of the endpoint
|
260 | * @param path - URL path of the endpoint
|
261 | * @param spec - The OpenAPI spec describing the endpoint (operation)
|
262 | * @param handler - The function to invoke with the request parameters
|
263 | * described in the spec.
|
264 | */
|
265 | route(
|
266 | verb: string,
|
267 | path: string,
|
268 | spec: OperationObject,
|
269 | handler: Function,
|
270 | ): Binding;
|
271 |
|
272 | /**
|
273 | * Register a new route.
|
274 | *
|
275 | * @example
|
276 | * ```ts
|
277 | * function greet(name: string) {
|
278 | * return `hello ${name}`;
|
279 | * }
|
280 | * const route = new Route('get', '/', operationSpec, greet);
|
281 | * app.route(route);
|
282 | * ```
|
283 | *
|
284 | * @param route - The route to add.
|
285 | */
|
286 | route(route: RouteEntry): Binding;
|
287 |
|
288 | /**
|
289 | * Register a new route.
|
290 | *
|
291 | * @example
|
292 | * ```ts
|
293 | * function greet(name: string) {
|
294 | * return `hello ${name}`;
|
295 | * }
|
296 | * app.route('get', '/', operationSpec, greet);
|
297 | * ```
|
298 | */
|
299 | route(
|
300 | verb: string,
|
301 | path: string,
|
302 | spec: OperationObject,
|
303 | handler: Function,
|
304 | ): Binding;
|
305 |
|
306 | route<T extends object>(
|
307 | routeOrVerb: RouteEntry | string,
|
308 | path?: string,
|
309 | spec?: OperationObject,
|
310 | controllerCtorOrHandler?: ControllerClass<T> | Function,
|
311 | controllerFactory?: ControllerFactory<T>,
|
312 | methodName?: string,
|
313 | ): Binding {
|
314 | const server = this.restServer;
|
315 | if (typeof routeOrVerb === 'object') {
|
316 | return server.route(routeOrVerb);
|
317 | } else if (arguments.length === 4) {
|
318 | return server.route(
|
319 | routeOrVerb,
|
320 | path!,
|
321 | spec!,
|
322 | controllerCtorOrHandler as Function,
|
323 | );
|
324 | } else {
|
325 | return server.route(
|
326 | routeOrVerb,
|
327 | path!,
|
328 | spec!,
|
329 | controllerCtorOrHandler as ControllerClass<T>,
|
330 | controllerFactory!,
|
331 | methodName!,
|
332 | );
|
333 | }
|
334 | }
|
335 |
|
336 | /**
|
337 | * Register a route redirecting callers to a different URL.
|
338 | *
|
339 | * @example
|
340 | * ```ts
|
341 | * app.redirect('/explorer', '/explorer/');
|
342 | * ```
|
343 | *
|
344 | * @param fromPath - URL path of the redirect endpoint
|
345 | * @param toPathOrUrl - Location (URL path or full URL) where to redirect to.
|
346 | * If your server is configured with a custom `basePath`, then the base path
|
347 | * is prepended to the target location.
|
348 | * @param statusCode - HTTP status code to respond with,
|
349 | * defaults to 303 (See Other).
|
350 | */
|
351 | redirect(
|
352 | fromPath: string,
|
353 | toPathOrUrl: string,
|
354 | statusCode?: number,
|
355 | ): Binding {
|
356 | return this.restServer.redirect(fromPath, toPathOrUrl, statusCode);
|
357 | }
|
358 |
|
359 | /**
|
360 | * Set the OpenAPI specification that defines the REST API schema for this
|
361 | * application. All routes, parameter definitions and return types will be
|
362 | * defined in this way.
|
363 | *
|
364 | * Note that this will override any routes defined via decorators at the
|
365 | * controller level (this function takes precedent).
|
366 | *
|
367 | * @param spec - The OpenAPI specification, as an object.
|
368 | * @returns Binding for the api spec
|
369 | */
|
370 | api(spec: OpenApiSpec): Binding {
|
371 | return this.restServer.bind(RestBindings.API_SPEC).to(spec);
|
372 | }
|
373 |
|
374 | /**
|
375 | * Mount an Express router to expose additional REST endpoints handled
|
376 | * via legacy Express-based stack.
|
377 | *
|
378 | * @param basePath - Path where to mount the router at, e.g. `/` or `/api`.
|
379 | * @param router - The Express router to handle the requests.
|
380 | * @param spec - A partial OpenAPI spec describing endpoints provided by the
|
381 | * router. LoopBack will prepend `basePath` to all endpoints automatically.
|
382 | * This argument is optional. You can leave it out if you don't want to
|
383 | * document the routes.
|
384 | */
|
385 | mountExpressRouter(
|
386 | basePath: string,
|
387 | router: ExpressRequestHandler,
|
388 | spec?: RouterSpec,
|
389 | ): void {
|
390 | this.restServer.mountExpressRouter(basePath, router, spec);
|
391 | }
|
392 |
|
393 | /**
|
394 | * Export the OpenAPI spec to the given json or yaml file
|
395 | * @param outFile - File name for the spec. The extension of the file
|
396 | * determines the format of the file.
|
397 | * - `yaml` or `yml`: YAML
|
398 | * - `json` or other: JSON
|
399 | * If the outFile is not provided or its value is `''` or `'-'`, the spec is
|
400 | * written to the console using the `log` function.
|
401 | * @param log - Log function, default to `console.log`
|
402 | */
|
403 | async exportOpenApiSpec(outFile = '', log = console.log): Promise<void> {
|
404 | return this.restServer.exportOpenApiSpec(outFile, log);
|
405 | }
|
406 | }
|