@loopback/rest
Version:
Expose controllers as REST endpoints and route REST API requests to controller methods
294 lines (266 loc) • 9.65 kB
text/typescript
// Copyright IBM Corp. and LoopBack contributors 2017,2020. All Rights Reserved.
// Node module: @loopback/rest
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
import {
BindingScope,
config,
Context,
inject,
injectable,
ValueOrPromise,
} from '@loopback/core';
import {
InvokeMiddleware,
InvokeMiddlewareOptions,
MiddlewareGroups,
MiddlewareView,
} from '@loopback/express';
import debugFactory from 'debug';
import {RestBindings, RestTags} from './keys';
import {RequestContext} from './request-context';
import {FindRoute, InvokeMethod, ParseParams, Reject, Send} from './types';
const debug = debugFactory('loopback:rest:sequence');
const SequenceActions = RestBindings.SequenceActions;
/**
* A sequence function is a function implementing a custom
* sequence of actions to handle an incoming request.
*/
export type SequenceFunction = (
context: RequestContext,
sequence: DefaultSequence,
) => ValueOrPromise<void>;
/**
* A sequence handler is a class implementing sequence of actions
* required to handle an incoming request.
*/
export interface SequenceHandler {
/**
* Handle the request by running the configured sequence of actions.
*
* @param context - The request context: HTTP request and response objects,
* per-request IoC container and more.
*/
handle(context: RequestContext): Promise<void>;
}
/**
* The default implementation of SequenceHandler.
*
* @remarks
* This class implements default Sequence for the LoopBack framework.
* Default sequence is used if user hasn't defined their own Sequence
* for their application.
*
* Sequence constructor() and run() methods are invoked from [[http-handler]]
* when the API request comes in. User defines APIs in their Application
* Controller class.
*
* @example
* User can bind their own Sequence to app as shown below
* ```ts
* app.bind(CoreBindings.SEQUENCE).toClass(MySequence);
* ```
*/
export class DefaultSequence implements SequenceHandler {
/**
* Optional invoker for registered middleware in a chain.
* To be injected via SequenceActions.INVOKE_MIDDLEWARE.
*/
(SequenceActions.INVOKE_MIDDLEWARE, {optional: true})
protected invokeMiddleware: InvokeMiddleware = () => false;
/**
* Constructor: Injects findRoute, invokeMethod & logError
* methods as promises.
*
* @param findRoute - Finds the appropriate controller method,
* spec and args for invocation (injected via SequenceActions.FIND_ROUTE).
* @param parseParams - The parameter parsing function (injected
* via SequenceActions.PARSE_PARAMS).
* @param invoke - Invokes the method specified by the route
* (injected via SequenceActions.INVOKE_METHOD).
* @param send - The action to merge the invoke result with the response
* (injected via SequenceActions.SEND)
* @param reject - The action to take if the invoke returns a rejected
* promise result (injected via SequenceActions.REJECT).
*/
constructor(
protected findRoute: FindRoute,
(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams,
(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
(SequenceActions.SEND) public send: Send,
(SequenceActions.REJECT) public reject: Reject,
) {}
(SequenceActions.FIND_ROUTE) /**
* Runs the default sequence. Given a handler context (request and response),
* running the sequence will produce a response or an error.
*
* Default sequence executes these steps
* - Executes middleware for CORS, OpenAPI spec endpoints
* - Finds the appropriate controller method, swagger spec
* and args for invocation
* - Parses HTTP request to get API argument list
* - Invokes the API which is defined in the Application Controller
* - Writes the result from API into the HTTP response
* - Error is caught and logged using 'logError' if any of the above steps
* in the sequence fails with an error.
*
* @param context - The request context: HTTP request and response objects,
* per-request IoC container and more.
*/
async handle(context: RequestContext): Promise<void> {
try {
const {request, response} = context;
// Invoke registered Express middleware
const finished = await this.invokeMiddleware(context);
if (finished) {
// The response been produced by the middleware chain
return;
}
const route = this.findRoute(request);
const args = await this.parseParams(request, route);
const result = await this.invoke(route, args);
debug('%s result -', route.describe(), result);
this.send(response, result);
} catch (error) {
this.reject(context, error);
}
}
}
/**
* Built-in middleware groups for the REST sequence
*/
export namespace RestMiddlewareGroups {
/**
* Invoke downstream middleware to get the result or catch errors so that it
* can produce the http response
*/
export const SEND_RESPONSE = 'sendResponse';
/**
* Enforce CORS
*/
export const CORS = MiddlewareGroups.CORS;
/**
* Server OpenAPI specs
*/
export const API_SPEC = MiddlewareGroups.API_SPEC;
/**
* Default middleware group
*/
export const MIDDLEWARE = MiddlewareGroups.MIDDLEWARE;
export const DEFAULT = MIDDLEWARE;
/**
* Find the route that can serve the request
*/
export const FIND_ROUTE = 'findRoute';
/**
* Perform authentication
*/
export const AUTHENTICATION = 'authentication';
/**
* Parse the http request to extract parameter values for the operation
*/
export const PARSE_PARAMS = 'parseParams';
/**
* Invoke the target controller method or handler function
*/
export const INVOKE_METHOD = 'invokeMethod';
}
/**
* A sequence implementation using middleware chains
*/
({scope: BindingScope.SINGLETON})
export class MiddlewareSequence implements SequenceHandler {
private middlewareView: MiddlewareView;
static defaultOptions: InvokeMiddlewareOptions = {
chain: RestTags.REST_MIDDLEWARE_CHAIN,
orderedGroups: [
// Please note that middleware is cascading. The `sendResponse` is
// added first to invoke downstream middleware to get the result or
// catch errors so that it can produce the http response.
RestMiddlewareGroups.SEND_RESPONSE,
RestMiddlewareGroups.CORS,
RestMiddlewareGroups.API_SPEC,
RestMiddlewareGroups.MIDDLEWARE,
RestMiddlewareGroups.FIND_ROUTE,
// authentication depends on the route
RestMiddlewareGroups.AUTHENTICATION,
RestMiddlewareGroups.PARSE_PARAMS,
RestMiddlewareGroups.INVOKE_METHOD,
],
/**
* Reports an error if there are middleware groups are unreachable as they
* are ordered after the `invokeMethod` group.
*/
validate: groups => {
const index = groups.indexOf(RestMiddlewareGroups.INVOKE_METHOD);
if (index !== -1) {
const unreachableGroups = groups.slice(index + 1);
if (unreachableGroups.length > 0) {
throw new Error(
`Middleware groups "${unreachableGroups.join(
',',
)}" are not invoked as they are ordered after "${
RestMiddlewareGroups.INVOKE_METHOD
}"`,
);
}
}
},
};
/**
* Constructor: Injects `InvokeMiddleware` and `InvokeMiddlewareOptions`
*
* @param invokeMiddleware - invoker for registered middleware in a chain.
* To be injected via RestBindings.INVOKE_MIDDLEWARE_SERVICE.
*/
constructor(
context: Context,
(RestBindings.INVOKE_MIDDLEWARE_SERVICE)
readonly invokeMiddleware: InvokeMiddleware,
()
readonly options: InvokeMiddlewareOptions = MiddlewareSequence.defaultOptions,
) {
.context()
this.middlewareView = new MiddlewareView(context, options);
debug('Discovered middleware', this.middlewareView.middlewareBindingKeys);
}
/**
* Runs the default sequence. Given a handler context (request and response),
* running the sequence will produce a response or an error.
*
* Default sequence executes these groups of middleware:
*
* - `cors`: Enforces `CORS`
* - `openApiSpec`: Serves OpenAPI specs
* - `findRoute`: Finds the appropriate controller method, swagger spec and
* args for invocation
* - `parseParams`: Parses HTTP request to get API argument list
* - `invokeMethod`: Invokes the API which is defined in the Application
* controller method
*
* In front of the groups above, we have a special middleware called
* `sendResponse`, which first invokes downstream middleware to get a result
* and handles the result or error respectively.
*
* - Writes the result from API into the HTTP response (if the HTTP response
* has not been produced yet by the middleware chain.
* - Catches error logs it using 'logError' if any of the above steps
* in the sequence fails with an error.
*
* @param context - The request context: HTTP request and response objects,
* per-request IoC container and more.
*/
async handle(context: RequestContext): Promise<void> {
debug(
'Invoking middleware chain %s with groups %s',
this.options.chain,
this.options.orderedGroups,
);
const options: InvokeMiddlewareOptions = {
middlewareList: this.middlewareView.middlewareBindingKeys,
validate: MiddlewareSequence.defaultOptions.validate,
...this.options,
};
await this.invokeMiddleware(context, options);
}
}