UNPKG

9.65 kBPlain TextView Raw
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
6import {
7 BindingScope,
8 config,
9 Context,
10 inject,
11 injectable,
12 ValueOrPromise,
13} from '@loopback/core';
14import {
15 InvokeMiddleware,
16 InvokeMiddlewareOptions,
17 MiddlewareGroups,
18 MiddlewareView,
19} from '@loopback/express';
20import debugFactory from 'debug';
21import {RestBindings, RestTags} from './keys';
22import {RequestContext} from './request-context';
23import {FindRoute, InvokeMethod, ParseParams, Reject, Send} from './types';
24const debug = debugFactory('loopback:rest:sequence');
25
26const 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 */
32export 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 */
41export 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 */
69export class DefaultSequence implements SequenceHandler {
70 /**
71 * Optional invoker for registered middleware in a chain.
72 * To be injected via SequenceActions.INVOKE_MIDDLEWARE.
73 */
74 @inject(SequenceActions.INVOKE_MIDDLEWARE, {optional: true})
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 @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
94 @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams,
95 @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
96 @inject(SequenceActions.SEND) public send: Send,
97 @inject(SequenceActions.REJECT) public reject: 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 */
141export 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@injectable({scope: BindingScope.SINGLETON})
189export 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 @inject.context()
243 context: Context,
244
245 @inject(RestBindings.INVOKE_MIDDLEWARE_SERVICE)
246 readonly invokeMiddleware: InvokeMiddleware,
247 @config()
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}