1 | // Copyright IBM Corp. and LoopBack contributors 2017,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 | BindingScope,
|
8 | config,
|
9 | Context,
|
10 | inject,
|
11 | injectable,
|
12 | ValueOrPromise,
|
13 | } from '@loopback/core';
|
14 | import {
|
15 | InvokeMiddleware,
|
16 | InvokeMiddlewareOptions,
|
17 | MiddlewareGroups,
|
18 | MiddlewareView,
|
19 | } from '@loopback/express';
|
20 | import debugFactory from 'debug';
|
21 | import {RestBindings, RestTags} from './keys';
|
22 | import {RequestContext} from './request-context';
|
23 | import {FindRoute, InvokeMethod, ParseParams, Reject, Send} from './types';
|
24 | const debug = debugFactory('loopback:rest:sequence');
|
25 |
|
26 | const SequenceActions = RestBindings.SequenceActions;
|
27 |
|
28 | /**
|
29 | * A sequence function is a function implementing a custom
|
30 | * sequence of actions to handle an incoming request.
|
31 | */
|
32 | export type SequenceFunction = (
|
33 | context: RequestContext,
|
34 | sequence: DefaultSequence,
|
35 | ) => ValueOrPromise<void>;
|
36 |
|
37 | /**
|
38 | * A sequence handler is a class implementing sequence of actions
|
39 | * required to handle an incoming request.
|
40 | */
|
41 | export interface SequenceHandler {
|
42 | /**
|
43 | * Handle the request by running the configured sequence of actions.
|
44 | *
|
45 | * @param context - The request context: HTTP request and response objects,
|
46 | * per-request IoC container and more.
|
47 | */
|
48 | handle(context: RequestContext): Promise<void>;
|
49 | }
|
50 |
|
51 | /**
|
52 | * The default implementation of SequenceHandler.
|
53 | *
|
54 | * @remarks
|
55 | * This class implements default Sequence for the LoopBack framework.
|
56 | * Default sequence is used if user hasn't defined their own Sequence
|
57 | * for their application.
|
58 | *
|
59 | * Sequence constructor() and run() methods are invoked from [[http-handler]]
|
60 | * when the API request comes in. User defines APIs in their Application
|
61 | * Controller class.
|
62 | *
|
63 | * @example
|
64 | * User can bind their own Sequence to app as shown below
|
65 | * ```ts
|
66 | * app.bind(CoreBindings.SEQUENCE).toClass(MySequence);
|
67 | * ```
|
68 | */
|
69 | export class DefaultSequence implements SequenceHandler {
|
70 | /**
|
71 | * Optional invoker for registered middleware in a chain.
|
72 | * To be injected via SequenceActions.INVOKE_MIDDLEWARE.
|
73 | */
|
74 | true})
(SequenceActions.INVOKE_MIDDLEWARE, {optional: |
75 | protected invokeMiddleware: InvokeMiddleware = () => false;
|
76 |
|
77 | /**
|
78 | * Constructor: Injects findRoute, invokeMethod & logError
|
79 | * methods as promises.
|
80 | *
|
81 | * @param findRoute - Finds the appropriate controller method,
|
82 | * spec and args for invocation (injected via SequenceActions.FIND_ROUTE).
|
83 | * @param parseParams - The parameter parsing function (injected
|
84 | * via SequenceActions.PARSE_PARAMS).
|
85 | * @param invoke - Invokes the method specified by the route
|
86 | * (injected via SequenceActions.INVOKE_METHOD).
|
87 | * @param send - The action to merge the invoke result with the response
|
88 | * (injected via SequenceActions.SEND)
|
89 | * @param reject - The action to take if the invoke returns a rejected
|
90 | * promise result (injected via SequenceActions.REJECT).
|
91 | */
|
92 | constructor(
|
93 | protected findRoute: FindRoute,
(SequenceActions.FIND_ROUTE) |
94 | protected parseParams: ParseParams,
(SequenceActions.PARSE_PARAMS) |
95 | protected invoke: InvokeMethod,
(SequenceActions.INVOKE_METHOD) |
96 | public send: Send,
(SequenceActions.SEND) |
97 | public reject: Reject,
(SequenceActions.REJECT) |
98 | ) {}
|
99 |
|
100 | /**
|
101 | * Runs the default sequence. Given a handler context (request and response),
|
102 | * running the sequence will produce a response or an error.
|
103 | *
|
104 | * Default sequence executes these steps
|
105 | * - Executes middleware for CORS, OpenAPI spec endpoints
|
106 | * - Finds the appropriate controller method, swagger spec
|
107 | * and args for invocation
|
108 | * - Parses HTTP request to get API argument list
|
109 | * - Invokes the API which is defined in the Application Controller
|
110 | * - Writes the result from API into the HTTP response
|
111 | * - Error is caught and logged using 'logError' if any of the above steps
|
112 | * in the sequence fails with an error.
|
113 | *
|
114 | * @param context - The request context: HTTP request and response objects,
|
115 | * per-request IoC container and more.
|
116 | */
|
117 | async handle(context: RequestContext): Promise<void> {
|
118 | try {
|
119 | const {request, response} = context;
|
120 | // Invoke registered Express middleware
|
121 | const finished = await this.invokeMiddleware(context);
|
122 | if (finished) {
|
123 | // The response been produced by the middleware chain
|
124 | return;
|
125 | }
|
126 | const route = this.findRoute(request);
|
127 | const args = await this.parseParams(request, route);
|
128 | const result = await this.invoke(route, args);
|
129 |
|
130 | debug('%s result -', route.describe(), result);
|
131 | this.send(response, result);
|
132 | } catch (error) {
|
133 | this.reject(context, error);
|
134 | }
|
135 | }
|
136 | }
|
137 |
|
138 | /**
|
139 | * Built-in middleware groups for the REST sequence
|
140 | */
|
141 | export namespace RestMiddlewareGroups {
|
142 | /**
|
143 | * Invoke downstream middleware to get the result or catch errors so that it
|
144 | * can produce the http response
|
145 | */
|
146 | export const SEND_RESPONSE = 'sendResponse';
|
147 |
|
148 | /**
|
149 | * Enforce CORS
|
150 | */
|
151 | export const CORS = MiddlewareGroups.CORS;
|
152 |
|
153 | /**
|
154 | * Server OpenAPI specs
|
155 | */
|
156 | export const API_SPEC = MiddlewareGroups.API_SPEC;
|
157 |
|
158 | /**
|
159 | * Default middleware group
|
160 | */
|
161 | export const MIDDLEWARE = MiddlewareGroups.MIDDLEWARE;
|
162 | export const DEFAULT = MIDDLEWARE;
|
163 |
|
164 | /**
|
165 | * Find the route that can serve the request
|
166 | */
|
167 | export const FIND_ROUTE = 'findRoute';
|
168 |
|
169 | /**
|
170 | * Perform authentication
|
171 | */
|
172 | export const AUTHENTICATION = 'authentication';
|
173 |
|
174 | /**
|
175 | * Parse the http request to extract parameter values for the operation
|
176 | */
|
177 | export const PARSE_PARAMS = 'parseParams';
|
178 |
|
179 | /**
|
180 | * Invoke the target controller method or handler function
|
181 | */
|
182 | export const INVOKE_METHOD = 'invokeMethod';
|
183 | }
|
184 |
|
185 | /**
|
186 | * A sequence implementation using middleware chains
|
187 | */
|
188 | ({scope: BindingScope.SINGLETON})
|
189 | export class MiddlewareSequence implements SequenceHandler {
|
190 | private middlewareView: MiddlewareView;
|
191 |
|
192 | static defaultOptions: InvokeMiddlewareOptions = {
|
193 | chain: RestTags.REST_MIDDLEWARE_CHAIN,
|
194 | orderedGroups: [
|
195 | // Please note that middleware is cascading. The `sendResponse` is
|
196 | // added first to invoke downstream middleware to get the result or
|
197 | // catch errors so that it can produce the http response.
|
198 | RestMiddlewareGroups.SEND_RESPONSE,
|
199 |
|
200 | RestMiddlewareGroups.CORS,
|
201 | RestMiddlewareGroups.API_SPEC,
|
202 | RestMiddlewareGroups.MIDDLEWARE,
|
203 |
|
204 | RestMiddlewareGroups.FIND_ROUTE,
|
205 |
|
206 | // authentication depends on the route
|
207 | RestMiddlewareGroups.AUTHENTICATION,
|
208 |
|
209 | RestMiddlewareGroups.PARSE_PARAMS,
|
210 |
|
211 | RestMiddlewareGroups.INVOKE_METHOD,
|
212 | ],
|
213 |
|
214 | /**
|
215 | * Reports an error if there are middleware groups are unreachable as they
|
216 | * are ordered after the `invokeMethod` group.
|
217 | */
|
218 | validate: groups => {
|
219 | const index = groups.indexOf(RestMiddlewareGroups.INVOKE_METHOD);
|
220 | if (index !== -1) {
|
221 | const unreachableGroups = groups.slice(index + 1);
|
222 | if (unreachableGroups.length > 0) {
|
223 | throw new Error(
|
224 | `Middleware groups "${unreachableGroups.join(
|
225 | ',',
|
226 | )}" are not invoked as they are ordered after "${
|
227 | RestMiddlewareGroups.INVOKE_METHOD
|
228 | }"`,
|
229 | );
|
230 | }
|
231 | }
|
232 | },
|
233 | };
|
234 |
|
235 | /**
|
236 | * Constructor: Injects `InvokeMiddleware` and `InvokeMiddlewareOptions`
|
237 | *
|
238 | * @param invokeMiddleware - invoker for registered middleware in a chain.
|
239 | * To be injected via RestBindings.INVOKE_MIDDLEWARE_SERVICE.
|
240 | */
|
241 | constructor(
|
242 | .context()
|
243 | context: Context,
|
244 |
|
245 | (RestBindings.INVOKE_MIDDLEWARE_SERVICE)
|
246 | readonly invokeMiddleware: InvokeMiddleware,
|
247 | ()
|
248 | readonly options: InvokeMiddlewareOptions = MiddlewareSequence.defaultOptions,
|
249 | ) {
|
250 | this.middlewareView = new MiddlewareView(context, options);
|
251 | debug('Discovered middleware', this.middlewareView.middlewareBindingKeys);
|
252 | }
|
253 |
|
254 | /**
|
255 | * Runs the default sequence. Given a handler context (request and response),
|
256 | * running the sequence will produce a response or an error.
|
257 | *
|
258 | * Default sequence executes these groups of middleware:
|
259 | *
|
260 | * - `cors`: Enforces `CORS`
|
261 | * - `openApiSpec`: Serves OpenAPI specs
|
262 | * - `findRoute`: Finds the appropriate controller method, swagger spec and
|
263 | * args for invocation
|
264 | * - `parseParams`: Parses HTTP request to get API argument list
|
265 | * - `invokeMethod`: Invokes the API which is defined in the Application
|
266 | * controller method
|
267 | *
|
268 | * In front of the groups above, we have a special middleware called
|
269 | * `sendResponse`, which first invokes downstream middleware to get a result
|
270 | * and handles the result or error respectively.
|
271 | *
|
272 | * - Writes the result from API into the HTTP response (if the HTTP response
|
273 | * has not been produced yet by the middleware chain.
|
274 | * - Catches error logs it using 'logError' if any of the above steps
|
275 | * in the sequence fails with an error.
|
276 | *
|
277 | * @param context - The request context: HTTP request and response objects,
|
278 | * per-request IoC container and more.
|
279 | */
|
280 | async handle(context: RequestContext): Promise<void> {
|
281 | debug(
|
282 | 'Invoking middleware chain %s with groups %s',
|
283 | this.options.chain,
|
284 | this.options.orderedGroups,
|
285 | );
|
286 | const options: InvokeMiddlewareOptions = {
|
287 | middlewareList: this.middlewareView.middlewareBindingKeys,
|
288 | validate: MiddlewareSequence.defaultOptions.validate,
|
289 | ...this.options,
|
290 | };
|
291 | await this.invokeMiddleware(context, options);
|
292 | }
|
293 | }
|