From f1da2353f358fefa43d6a174e23990f8324568ae Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Sun, 24 May 2020 12:52:30 -0700 Subject: [PATCH 1/9] feat(express): sort middleware by group dependencies and ordered groups --- packages/express/package-lock.json | 11 +++ packages/express/package.json | 2 + .../middleware-registeration.acceptance.ts | 14 ++++ .../src/__tests__/unit/group-order.unit.ts | 55 ++++++++++++++ packages/express/src/group-sorter.ts | 35 +++++++++ packages/express/src/index.ts | 1 + packages/express/src/keys.ts | 4 +- packages/express/src/middleware.ts | 30 +++++++- packages/express/src/types.ts | 71 ++++++++++++++++++- 9 files changed, 217 insertions(+), 6 deletions(-) create mode 100644 packages/express/src/__tests__/unit/group-order.unit.ts create mode 100644 packages/express/src/group-sorter.ts diff --git a/packages/express/package-lock.json b/packages/express/package-lock.json index f7bd8d90e15c..fd92d0d19801 100644 --- a/packages/express/package-lock.json +++ b/packages/express/package-lock.json @@ -113,6 +113,12 @@ "@types/mime": "*" } }, + "@types/toposort": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/toposort/-/toposort-2.0.3.tgz", + "integrity": "sha512-jRtyvEu0Na/sy0oIxBW0f6wPQjidgVqlmCTJVHEGTNEUdL1f0YSvdPzHY7nX7MUWAZS6zcAa0KkqofHjy/xDZQ==", + "dev": true + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -584,6 +590,11 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, + "toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=" + }, "tslib": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", diff --git a/packages/express/package.json b/packages/express/package.json index 71dbcceac07c..88ac98fdb3d1 100644 --- a/packages/express/package.json +++ b/packages/express/package.json @@ -48,6 +48,7 @@ "express": "^4.17.1", "http-errors": "^1.8.0", "on-finished": "^2.3.0", + "toposort": "^2.0.2", "tslib": "^2.0.0" }, "devDependencies": { @@ -57,6 +58,7 @@ "@types/node": "^10.17.28", "@types/on-finished": "^2.3.1", "http-errors": "^1.8.0", + "@types/toposort": "^2.0.3", "source-map-support": "^0.5.19", "typescript": "~3.9.7" } diff --git a/packages/express/src/__tests__/acceptance/middleware-registeration.acceptance.ts b/packages/express/src/__tests__/acceptance/middleware-registeration.acceptance.ts index c1484d9d8bf3..5b5679ea94a2 100644 --- a/packages/express/src/__tests__/acceptance/middleware-registeration.acceptance.ts +++ b/packages/express/src/__tests__/acceptance/middleware-registeration.acceptance.ts @@ -83,6 +83,20 @@ describe('Express middleware registry', () => { await testSpyLog(); }); + it('reports error for circular dependencies', async () => { + server.middleware(spyMiddleware, { + key: 'middleware.spy', + downstreamGroups: ['x'], + upstreamGroups: ['x'], + }); + const res = await client + .post('/hello') + .send('"World"') + .set('content-type', 'application/json') + .expect(500); + expect(res.text).to.match(/Error\: Cyclic dependency/); + }); + it('registers a LoopBack middleware provider', async () => { class SpyMiddlewareProvider implements Provider { value() { diff --git a/packages/express/src/__tests__/unit/group-order.unit.ts b/packages/express/src/__tests__/unit/group-order.unit.ts new file mode 100644 index 000000000000..37c4edc45e52 --- /dev/null +++ b/packages/express/src/__tests__/unit/group-order.unit.ts @@ -0,0 +1,55 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/express +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect} from '@loopback/testlab'; +import {sortListOfGroups} from '../../'; + +describe('sortGroups', () => { + it('sorts groups across lists', () => { + const result = sortListOfGroups(['first', 'end'], ['start', 'end', 'last']); + expect(result).to.eql(['first', 'start', 'end', 'last']); + }); + + it('add new groups after existing groups', () => { + const result = sortListOfGroups( + ['initial', 'session', 'auth'], + ['initial', 'added', 'auth'], + ); + expect(result).to.eql(['initial', 'session', 'added', 'auth']); + }); + + it('merges arrays preserving the order', () => { + const target = ['initial', 'session', 'auth', 'routes', 'files', 'final']; + const result = sortListOfGroups(target, [ + 'initial', + 'postinit', + 'preauth', // add + 'auth', + 'routes', + 'subapps', // add + 'final', + 'last', // add + ]); + + expect(result).to.eql([ + 'initial', + 'session', + 'postinit', + 'preauth', + 'auth', + 'routes', + 'files', + 'subapps', + 'final', + 'last', + ]); + }); + + it('throws on conflicting order', () => { + expect(() => { + sortListOfGroups(['one', 'two'], ['two', 'one']); + }).to.throw(/Cyclic dependency/); + }); +}); diff --git a/packages/express/src/group-sorter.ts b/packages/express/src/group-sorter.ts new file mode 100644 index 000000000000..7ae5d0a62765 --- /dev/null +++ b/packages/express/src/group-sorter.ts @@ -0,0 +1,35 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/express +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import debugFactory from 'debug'; +import toposort from 'toposort'; +const debug = debugFactory('loopback:middleware'); +/** + * Sort the groups by their relative order + * @param orderedGroups - A list of arrays - each of which represents a partial + * order of groups. + */ +export function sortListOfGroups(...orderedGroups: string[][]) { + if (debug.enabled) { + debug( + 'Dependency graph: %s', + orderedGroups.map(edge => edge.join('->')).join(', '), + ); + } + const graph: [string, string][] = []; + for (const groups of orderedGroups) { + if (groups.length >= 2) { + groups.reduce((prev: string | undefined, group) => { + if (typeof prev === 'string') { + graph.push([prev, group]); + } + return group; + }, undefined); + } + } + const sorted = toposort(graph); + debug('Sorted groups: %s', sorted.join('->')); + return sorted; +} diff --git a/packages/express/src/index.ts b/packages/express/src/index.ts index a61c0332f074..ccf10bb7d03f 100644 --- a/packages/express/src/index.ts +++ b/packages/express/src/index.ts @@ -22,6 +22,7 @@ */ export * from './express.application'; export * from './express.server'; +export * from './group-sorter'; export * from './keys'; export * from './middleware'; export * from './middleware-interceptor'; diff --git a/packages/express/src/keys.ts b/packages/express/src/keys.ts index 139b94140841..9f9895f896ee 100644 --- a/packages/express/src/keys.ts +++ b/packages/express/src/keys.ts @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import {BindingKey} from '@loopback/core'; -import {MiddlewareContext} from './types'; +import {MiddlewareContext, MiddlewareGroups} from './types'; export namespace MiddlewareBindings { /** @@ -34,4 +34,4 @@ export const MIDDLEWARE_INTERCEPTOR_NAMESPACE = 'globalInterceptors.middleware'; /** * Default order group name for Express middleware based global interceptors */ -export const DEFAULT_MIDDLEWARE_GROUP = 'middleware'; +export const DEFAULT_MIDDLEWARE_GROUP = MiddlewareGroups.DEFAULT; diff --git a/packages/express/src/middleware.ts b/packages/express/src/middleware.ts index 2865b8707e27..baa4bf62602c 100644 --- a/packages/express/src/middleware.ts +++ b/packages/express/src/middleware.ts @@ -21,6 +21,7 @@ import { ValueOrPromise, } from '@loopback/core'; import debugFactory from 'debug'; +import {sortListOfGroups} from './group-sorter'; import {DEFAULT_MIDDLEWARE_GROUP, MIDDLEWARE_NAMESPACE} from './keys'; import { createInterceptor, @@ -133,6 +134,20 @@ export function asMiddleware( binding .apply(extensionFor(options.chain ?? DEFAULT_MIDDLEWARE_CHAIN)) .tag({group: options.group ?? DEFAULT_MIDDLEWARE_GROUP}); + const groupsBefore = options.upstreamGroups; + if (groupsBefore != null) { + binding.tag({ + upstreamGroups: + typeof groupsBefore === 'string' ? [groupsBefore] : groupsBefore, + }); + } + const groupsAfter = options.downstreamGroups; + if (groupsAfter != null) { + binding.tag({ + downstreamGroups: + typeof groupsAfter === 'string' ? [groupsAfter] : groupsAfter, + }); + } }; } @@ -199,9 +214,19 @@ export function invokeMiddleware( middlewareCtx.request.originalUrl, options, ); - const {chain = DEFAULT_MIDDLEWARE_CHAIN, orderedGroups} = options ?? {}; + const {chain = DEFAULT_MIDDLEWARE_CHAIN, orderedGroups = []} = options ?? {}; // Find extensions for the given extension point binding const filter = extensionFilter(chain); + + // Calculate orders from middleware dependencies + const ordersFromDependencies: string[][] = []; + middlewareCtx.find(filter).forEach(b => { + const group: string = b.tagMap.group ?? DEFAULT_MIDDLEWARE_GROUP; + const groupsBefore: string[] = b.tagMap.upstreamGroups ?? []; + groupsBefore.forEach(d => ordersFromDependencies.push([d, group])); + const groupsAfter: string[] = b.tagMap.downstreamGroups ?? []; + groupsAfter.forEach(d => ordersFromDependencies.push([group, d])); + }); if (debug.enabled) { debug( 'Middleware for extension point "%s":', @@ -209,10 +234,11 @@ export function invokeMiddleware( middlewareCtx.find(filter).map(b => b.key), ); } + const order = sortListOfGroups(orderedGroups, ...ordersFromDependencies); const mwChain = new MiddlewareChain( middlewareCtx, filter, - compareBindingsByTag('group', orderedGroups), + compareBindingsByTag('group', order), ); return mwChain.invokeInterceptors(options?.next); } diff --git a/packages/express/src/types.ts b/packages/express/src/types.ts index f12242999bbe..5546853e9458 100644 --- a/packages/express/src/types.ts +++ b/packages/express/src/types.ts @@ -37,6 +37,11 @@ export type ExpressRequestHandler = RequestHandler; * context (request, response, etc.). */ export class MiddlewareContext extends Context implements HandlerContext { + /** + * A flag to tell if the response is finished. + */ + responseFinished = false; + /** * Constructor for `MiddlewareContext` * @param request - Express request object @@ -53,6 +58,7 @@ export class MiddlewareContext extends Context implements HandlerContext { super(parent, name); this.setupBindings(); onFinished(this.response, () => { + this.responseFinished = true; // Close the request context when the http response is finished so that // it can be recycled by GC this.emit('close'); @@ -68,7 +74,28 @@ export class MiddlewareContext extends Context implements HandlerContext { /** * Interface LoopBack 4 middleware to be executed within sequence of actions. * A middleware for LoopBack is basically a generic interceptor that uses - * `RequestContext`. + * `MiddlewareContext`. + * + * @remarks + * + * The middleware function is responsible for processing HTTP requests and + * responses. It typically includes the following logic. + * + * 1. Process the request with one of the following outcome + * - Reject the request by throwing an error if request is invalid, such as + * validation or authentication failures + * - Produce a response by itself, such as from the cache + * - Proceed by calling `await next()` to invoke downstream middleware. When + * `await next()` returns, it goes to step 2. If an error thrown from + * `await next()`, step 3 handles the error. + * + * 2. Process the response with one the following outcome + * - Reject the response by throwing an error + * - Replace the response with its own value + * - Return the response to upstream middleware + * + * 3. Catch the error thrown from `await next()`. If the `catch` block does not + * exist, the error will be bubbled up to upstream middleware * * The signature of a middleware function is described at * {@link https://loopback.io/doc/en/lb4/apidocs.express.middleware.html | Middleware}. @@ -116,7 +143,8 @@ export interface InvokeMiddlewareOptions { */ chain?: string; /** - * An array of group names to denote the order of execution + * An array of group names to denote the order of execution, such as + * `['cors', 'caching', 'rate-limiting']`. */ orderedGroups?: string[]; @@ -227,6 +255,24 @@ export interface MiddlewareBindingOptions * Name of the middleware extension point. Default to `DEFAULT_MIDDLEWARE_CHAIN`. */ chain?: string; + + /** + * An array of group names for upstream middleware in the cascading order. + * + * For example, the `invokeMethod` depends on `parseParams` for request + * processing. The `upstreamGroups` for `invokeMethod` should be + * `['parseParams']`. The order of groups in the array does not matter. + */ + upstreamGroups?: string | string[]; + + /** + * An array of group names for downstream middleware in the cascading order. + * + * For example, the `sendResponse` depends on `invokeMethod` for response + * processing. The `downstreamGroups` for `sendResponse` should be + * `['invokeMethod']`. The order of groups in the array does not matter. + */ + downstreamGroups?: string | string[]; } /** @@ -241,3 +287,24 @@ export interface ExpressMiddlewareFactory { * A symbol to store `MiddlewareContext` on the request object */ export const MIDDLEWARE_CONTEXT = Symbol('loopback.middleware.context'); + +/** + * Constants for middleware groups + */ +export namespace MiddlewareGroups { + /** + * Enforce CORS + */ + export const CORS = 'cors'; + + /** + * Server OpenAPI specs + */ + export const API_SPEC = 'apiSpec'; + + /** + * Default middleware group + */ + export const MIDDLEWARE = 'middleware'; + export const DEFAULT = MIDDLEWARE; +} From 26e6f17edc4e70fede1d67f5e576bc801d218724 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Thu, 7 May 2020 10:18:33 -0700 Subject: [PATCH 2/9] feat(rest): add middleware for REST actions and MiddlewareSequence --- .../middleware-registeration.acceptance.ts | 6 +- .../middleware-sequence.acceptance.ts | 9 +- .../integration/rest.server.integration.ts | 4 +- packages/rest/src/keys.ts | 37 ++++- .../rest/src/providers/find-route.provider.ts | 29 +++- .../src/providers/invoke-method.provider.ts | 34 ++++- .../src/providers/parse-params.provider.ts | 29 +++- packages/rest/src/providers/send.provider.ts | 47 +++++- packages/rest/src/rest.component.ts | 52 +++++-- packages/rest/src/rest.server.ts | 27 +++- packages/rest/src/sequence.ts | 136 +++++++++++++++++- packages/rest/src/types.ts | 2 +- 12 files changed, 374 insertions(+), 38 deletions(-) diff --git a/packages/rest/src/__tests__/acceptance/middleware/middleware-registeration.acceptance.ts b/packages/rest/src/__tests__/acceptance/middleware/middleware-registeration.acceptance.ts index c51cfd557f7e..2c06c593dc32 100644 --- a/packages/rest/src/__tests__/acceptance/middleware/middleware-registeration.acceptance.ts +++ b/packages/rest/src/__tests__/acceptance/middleware/middleware-registeration.acceptance.ts @@ -59,8 +59,9 @@ describe('Express middleware registry', () => { const spyMiddleware: Middleware = async (middlewareCtx, next) => { const {request, response} = middlewareCtx; response.set('x-spy-log-req', `${request.method} ${request.path}`); - await next(); + const result = await next(); response.set('x-spy-log-res', `${request.method} ${request.path}`); + return result; }; it('registers a LoopBack middleware handler', async () => { @@ -93,11 +94,12 @@ describe('Express middleware registry', () => { `${this.options.headerName}-req`, `${request.method} ${request.path}`, ); - await next(); + const result = await next(); response.set( `${this.options.headerName}-res`, `${request.method} ${request.path}`, ); + return result; }; } } diff --git a/packages/rest/src/__tests__/acceptance/middleware/middleware-sequence.acceptance.ts b/packages/rest/src/__tests__/acceptance/middleware/middleware-sequence.acceptance.ts index 34631d6ca224..71fad312cf6a 100644 --- a/packages/rest/src/__tests__/acceptance/middleware/middleware-sequence.acceptance.ts +++ b/packages/rest/src/__tests__/acceptance/middleware/middleware-sequence.acceptance.ts @@ -5,6 +5,7 @@ import {BindingScope, Constructor, CoreTags, inject} from '@loopback/core'; import {InvokeMiddleware, InvokeMiddlewareProvider} from '@loopback/express'; +import {RestTags} from '../../../keys'; import {RequestContext} from '../../../request-context'; import {DefaultSequence} from '../../../sequence'; import {SpyAction} from '../../fixtures/middleware/spy-config'; @@ -18,7 +19,9 @@ describe('Middleware in sequence', () => { afterEach(() => helper?.stop()); it('registers a middleware in default slot', () => { - const binding = helper.app.expressMiddleware(spy, undefined); + const binding = helper.app.expressMiddleware(spy, undefined, { + chain: RestTags.ACTION_MIDDLEWARE_CHAIN, + }); return helper.testSpyLog(binding); }); @@ -46,7 +49,9 @@ describe('Middleware in sequence', () => { it('registers a middleware in default slot with sequence 2', () => { helper.app.sequence(SequenceWithTwoInvokeMiddleware); - const binding = helper.app.expressMiddleware(spy, undefined); + const binding = helper.app.expressMiddleware(spy, undefined, { + chain: RestTags.ACTION_MIDDLEWARE_CHAIN, + }); return helper.testSpyLog(binding); }); diff --git a/packages/rest/src/__tests__/integration/rest.server.integration.ts b/packages/rest/src/__tests__/integration/rest.server.integration.ts index d83de4a552ab..18c7b5525592 100644 --- a/packages/rest/src/__tests__/integration/rest.server.integration.ts +++ b/packages/rest/src/__tests__/integration/rest.server.integration.ts @@ -1406,7 +1406,9 @@ paths: async function dummyRequestHandler(requestContext: RequestContext) { const {response} = requestContext; - const result = await invokeMiddleware(requestContext); + const result = await invokeMiddleware(requestContext, { + chain: RestTags.ACTION_MIDDLEWARE_CHAIN, + }); if (result === response) return; response.write('Hello'); response.end(); diff --git a/packages/rest/src/keys.ts b/packages/rest/src/keys.ts index a1fdc8755342..3970f130363c 100644 --- a/packages/rest/src/keys.ts +++ b/packages/rest/src/keys.ts @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import {BindingKey, Context, CoreBindings} from '@loopback/core'; -import {InvokeMiddleware} from '@loopback/express'; +import {DEFAULT_MIDDLEWARE_CHAIN, InvokeMiddleware} from '@loopback/express'; import {HttpProtocol} from '@loopback/http-server'; import {OpenApiSpec, OperationObject} from '@loopback/openapi-v3'; import https from 'https'; @@ -12,13 +12,15 @@ import {ErrorWriterOptions} from 'strong-error-handler'; import {BodyParser, RequestBodyParser} from './body-parsers'; import {HttpHandler} from './http-handler'; import {RestServer, RestServerConfig} from './rest.server'; -import {RestRouter, RestRouterOptions} from './router'; +import {ResolvedRoute, RestRouter, RestRouterOptions} from './router'; import {SequenceHandler} from './sequence'; import { AjvFactory, FindRoute, InvokeMethod, LogError, + OperationArgs, + OperationRetval, ParseParams, Reject, Request, @@ -179,12 +181,20 @@ export namespace RestBindings { */ export const SEQUENCE = BindingKey.create('rest.sequence'); + /** + * Binding key for setting and injecting a `invokeMiddleware` function for + * middleware based sequence + */ + export const INVOKE_MIDDLEWARE_SERVICE = BindingKey.create( + 'rest.invokeMiddleware', + ); + /** * Bindings for potential actions that could be used in a sequence */ export namespace SequenceActions { /** - * Binding key for setting and injecting a route finding function + * Binding key for setting and injecting `invokeMiddleware` function */ export const INVOKE_MIDDLEWARE = BindingKey.create( 'rest.sequence.actions.invokeMiddleware', @@ -225,6 +235,20 @@ export namespace RestBindings { ); } + export namespace Operation { + export const ROUTE = BindingKey.create( + 'rest.operation.route', + ); + + export const PARAMS = BindingKey.create( + 'rest.operation.params', + ); + + export const RETURN_VALUE = BindingKey.create( + 'rest.operation.returnValue', + ); + } + /** * Request-specific bindings */ @@ -285,4 +309,11 @@ export namespace RestTags { export const AJV_KEYWORD = 'ajvKeyword'; export const AJV_FORMAT = 'ajvFormat'; + + export const REST_MIDDLEWARE_CHAIN = DEFAULT_MIDDLEWARE_CHAIN; + + /** + * Legacy middleware chain for action-based REST sequence + */ + export const ACTION_MIDDLEWARE_CHAIN = 'middlewareChain.rest.actions'; } diff --git a/packages/rest/src/providers/find-route.provider.ts b/packages/rest/src/providers/find-route.provider.ts index bc02702db279..7428a79924ce 100644 --- a/packages/rest/src/providers/find-route.provider.ts +++ b/packages/rest/src/providers/find-route.provider.ts @@ -3,11 +3,13 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Context, inject, Provider} from '@loopback/core'; -import {FindRoute, Request} from '../types'; +import {bind, Context, inject, Provider} from '@loopback/core'; +import {asMiddleware, Middleware} from '@loopback/express'; import {HttpHandler} from '../http-handler'; -import {RestBindings} from '../keys'; +import {RestBindings, RestTags} from '../keys'; import {ResolvedRoute} from '../router'; +import {RestMiddlewareGroups} from '../sequence'; +import {FindRoute, Request} from '../types'; export class FindRouteProvider implements Provider { constructor( @@ -25,3 +27,24 @@ export class FindRouteProvider implements Provider { return found; } } + +@bind( + asMiddleware({ + group: RestMiddlewareGroups.FIND_ROUTE, + chain: RestTags.REST_MIDDLEWARE_CHAIN, + }), +) +export class FindRouteMiddlewareProvider implements Provider { + constructor( + @inject(RestBindings.SequenceActions.FIND_ROUTE) + protected findRoute: FindRoute, + ) {} + + value(): Middleware { + return async (ctx, next) => { + const route = this.findRoute(ctx.request); + ctx.bind(RestBindings.Operation.ROUTE).to(route); + return next(); + }; + } +} diff --git a/packages/rest/src/providers/invoke-method.provider.ts b/packages/rest/src/providers/invoke-method.provider.ts index 3bf4a1b74865..b12deb317868 100644 --- a/packages/rest/src/providers/invoke-method.provider.ts +++ b/packages/rest/src/providers/invoke-method.provider.ts @@ -3,10 +3,12 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Context, inject, Provider} from '@loopback/core'; -import {InvokeMethod, OperationArgs, OperationRetval} from '../types'; -import {RestBindings} from '../keys'; +import {bind, Context, inject, Provider} from '@loopback/core'; +import {asMiddleware, Middleware} from '@loopback/express'; +import {RestBindings, RestTags} from '../keys'; import {RouteEntry} from '../router'; +import {RestMiddlewareGroups} from '../sequence'; +import {InvokeMethod, OperationArgs, OperationRetval} from '../types'; export class InvokeMethodProvider implements Provider { constructor(@inject(RestBindings.Http.CONTEXT) protected context: Context) {} @@ -19,3 +21,29 @@ export class InvokeMethodProvider implements Provider { return route.invokeHandler(this.context, args); } } + +@bind( + asMiddleware({ + group: RestMiddlewareGroups.INVOKE_METHOD, + upstreamGroups: RestMiddlewareGroups.PARSE_PARAMS, + chain: RestTags.REST_MIDDLEWARE_CHAIN, + }), +) +export class InvokeMethodMiddlewareProvider implements Provider { + constructor( + @inject(RestBindings.SequenceActions.INVOKE_METHOD) + protected invokeMethod: InvokeMethod, + ) {} + + value(): Middleware { + return async (ctx, next) => { + const route: RouteEntry = await ctx.get(RestBindings.Operation.ROUTE); + const params: OperationArgs = await ctx.get( + RestBindings.Operation.PARAMS, + ); + const retVal = await this.invokeMethod(route, params); + ctx.bind(RestBindings.Operation.RETURN_VALUE).to(retVal); + return retVal; + }; + } +} diff --git a/packages/rest/src/providers/parse-params.provider.ts b/packages/rest/src/providers/parse-params.provider.ts index f7668976d6e6..70b3b6d35fb7 100644 --- a/packages/rest/src/providers/parse-params.provider.ts +++ b/packages/rest/src/providers/parse-params.provider.ts @@ -3,11 +3,13 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {inject, Provider} from '@loopback/core'; +import {bind, inject, Provider} from '@loopback/core'; +import {asMiddleware, Middleware} from '@loopback/express'; import {RequestBodyParser} from '../body-parsers'; -import {RestBindings} from '../keys'; +import {RestBindings, RestTags} from '../keys'; import {parseOperationArgs} from '../parser'; import {ResolvedRoute} from '../router'; +import {RestMiddlewareGroups} from '../sequence'; import {AjvFactory, ParseParams, Request, ValidationOptions} from '../types'; import {DEFAULT_AJV_VALIDATION_OPTIONS} from '../validation/ajv-factory.provider'; /** @@ -36,3 +38,26 @@ export class ParseParamsProvider implements Provider { }); } } + +@bind( + asMiddleware({ + group: RestMiddlewareGroups.PARSE_PARAMS, + upstreamGroups: RestMiddlewareGroups.FIND_ROUTE, + chain: RestTags.REST_MIDDLEWARE_CHAIN, + }), +) +export class ParseParamsMiddlewareProvider implements Provider { + constructor( + @inject(RestBindings.SequenceActions.PARSE_PARAMS) + protected parseParams: ParseParams, + ) {} + + value(): Middleware { + return async (ctx, next) => { + const route: ResolvedRoute = await ctx.get(RestBindings.Operation.ROUTE); + const params = await this.parseParams(ctx.request, route); + ctx.bind(RestBindings.Operation.PARAMS).to(params); + return next(); + }; + } +} diff --git a/packages/rest/src/providers/send.provider.ts b/packages/rest/src/providers/send.provider.ts index 2d3f549b7dce..9ca2c239e529 100644 --- a/packages/rest/src/providers/send.provider.ts +++ b/packages/rest/src/providers/send.provider.ts @@ -3,7 +3,11 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Provider, BoundValue} from '@loopback/core'; +import {bind, inject, Provider} from '@loopback/core'; +import {asMiddleware, Middleware} from '@loopback/express'; +import {RestBindings, RestTags} from '../keys'; +import {RestMiddlewareGroups} from '../sequence'; +import {Reject, Send} from '../types'; import {writeResultToResponse} from '../writer'; /** * Provides the function that populates the response object with @@ -12,8 +16,47 @@ import {writeResultToResponse} from '../writer'; * @returns The handler function that will populate the * response with operation results. */ -export class SendProvider implements Provider { +export class SendProvider implements Provider { value() { return writeResultToResponse; } } + +@bind( + asMiddleware({ + group: RestMiddlewareGroups.SEND_RESPONSE, + downstreamGroups: [ + RestMiddlewareGroups.CORS, + RestMiddlewareGroups.INVOKE_METHOD, + ], + chain: RestTags.REST_MIDDLEWARE_CHAIN, + }), +) +export class SendResponseMiddlewareProvider implements Provider { + constructor( + @inject(RestBindings.SequenceActions.SEND) + protected send: Send, + @inject(RestBindings.SequenceActions.REJECT) + protected reject: Reject, + ) {} + + value(): Middleware { + return async (ctx, next) => { + try { + /** + * Invoke downstream middleware to produce the result + */ + const result = await next(); + /** + * Write the result to HTTP response + */ + this.send(ctx.response, result); + } catch (err) { + /** + * Write the error to HTTP response + */ + this.reject(ctx, err); + } + }; + } +} diff --git a/packages/rest/src/rest.component.ts b/packages/rest/src/rest.component.ts index 91f3806ae029..3e2eeaa8958e 100644 --- a/packages/rest/src/rest.component.ts +++ b/packages/rest/src/rest.component.ts @@ -4,15 +4,14 @@ // License text available at https://opensource.org/licenses/MIT import { + Application, Binding, + Component, Constructor, + CoreBindings, + CoreTags, createBindingFromClass, inject, -} from '@loopback/core'; -import { - Application, - Component, - CoreBindings, ProviderMap, Server, } from '@loopback/core'; @@ -26,21 +25,25 @@ import { UrlEncodedBodyParser, } from './body-parsers'; import {RawBodyParser} from './body-parsers/body-parser.raw'; -import {RestBindings} from './keys'; +import {RestBindings, RestTags} from './keys'; import { + FindRouteMiddlewareProvider, FindRouteProvider, + InvokeMethodMiddlewareProvider, InvokeMethodProvider, LogErrorProvider, + ParseParamsMiddlewareProvider, ParseParamsProvider, RejectProvider, SendProvider, + SendResponseMiddlewareProvider, } from './providers'; import { createBodyParserBinding, RestServer, RestServerConfig, } from './rest.server'; -import {DefaultSequence} from './sequence'; +import {MiddlewareSequence} from './sequence'; import {ConsolidationEnhancer} from './spec-enhancers/consolidate.spec-enhancer'; import {InfoSpecEnhancer} from './spec-enhancers/info.spec-enhancer'; import {AjvFactoryProvider} from './validation/ajv-factory.provider'; @@ -48,8 +51,6 @@ import {AjvFactoryProvider} from './validation/ajv-factory.provider'; export class RestComponent implements Component { providers: ProviderMap = { [RestBindings.SequenceActions.LOG_ERROR.key]: LogErrorProvider, - [RestBindings.SequenceActions.INVOKE_MIDDLEWARE - .key]: InvokeMiddlewareProvider, [RestBindings.SequenceActions.FIND_ROUTE.key]: FindRouteProvider, [RestBindings.SequenceActions.INVOKE_METHOD.key]: InvokeMethodProvider, [RestBindings.SequenceActions.REJECT.key]: RejectProvider, @@ -86,6 +87,8 @@ export class RestComponent implements Component { ), createBindingFromClass(InfoSpecEnhancer), createBindingFromClass(ConsolidationEnhancer), + + ...getRestMiddlewareBindings(), ]; servers: { [name: string]: Constructor; @@ -97,7 +100,27 @@ export class RestComponent implements Component { @inject(CoreBindings.APPLICATION_INSTANCE) app: Application, @inject(RestBindings.CONFIG) config?: RestComponentConfig, ) { - app.bind(RestBindings.SEQUENCE).toClass(DefaultSequence); + // Register the `InvokeMiddleware` with default to `ACTION_MIDDLEWARE_CHAIN` + // to keep backward compatibility with action based sequence + const invokeMiddlewareActionBinding = createBindingFromClass( + InvokeMiddlewareProvider, + { + key: RestBindings.SequenceActions.INVOKE_MIDDLEWARE, + }, + ).tag({[CoreTags.EXTENSION_POINT]: RestTags.ACTION_MIDDLEWARE_CHAIN}); + app.add(invokeMiddlewareActionBinding); + + // Register the `InvokeMiddleware` with default to `DEFAULT_MIDDLEWARE_CHAIN` + // for the middleware based sequence + const invokeMiddlewareServiceBinding = createBindingFromClass( + InvokeMiddlewareProvider, + { + key: RestBindings.INVOKE_MIDDLEWARE_SERVICE, + }, + ).tag({[CoreTags.EXTENSION_POINT]: RestTags.REST_MIDDLEWARE_CHAIN}); + app.add(invokeMiddlewareServiceBinding); + + app.bind(RestBindings.SEQUENCE).toClass(MiddlewareSequence); const apiSpec = createEmptyApiSpec(); // Merge the OpenAPI `servers` spec from the config into the empty one if (config?.openApiSpec?.servers) { @@ -107,5 +130,14 @@ export class RestComponent implements Component { } } +function getRestMiddlewareBindings() { + return [ + SendResponseMiddlewareProvider, + FindRouteMiddlewareProvider, + ParseParamsMiddlewareProvider, + InvokeMethodMiddlewareProvider, + ].map(cls => createBindingFromClass(cls)); +} + // TODO(kevin): Extend this interface def to include multiple servers? export type RestComponentConfig = RestServerConfig; diff --git a/packages/rest/src/rest.server.ts b/packages/rest/src/rest.server.ts index 3899a61d513c..e5b38585abef 100644 --- a/packages/rest/src/rest.server.ts +++ b/packages/rest/src/rest.server.ts @@ -12,6 +12,7 @@ import { ContextObserver, CoreBindings, createBindingFromClass, + extensionFor, filterByKey, filterByTag, inject, @@ -61,7 +62,12 @@ import { RoutingTable, } from './router'; import {assignRouterSpec} from './router/router-spec'; -import {DefaultSequence, SequenceFunction, SequenceHandler} from './sequence'; +import { + DefaultSequence, + RestMiddlewareGroups, + SequenceFunction, + SequenceHandler, +} from './sequence'; import {Request, RequestBodyParserOptions, Response} from './types'; const debug = debugFactory('loopback:rest:server'); @@ -249,8 +255,13 @@ export class RestServer extends BaseMiddlewareRegistry this.expressMiddleware(cors, this.config.cors, { injectConfiguration: false, key: 'middleware.cors', - group: 'cors', - }); + group: RestMiddlewareGroups.CORS, + }).apply( + extensionFor( + RestTags.REST_MIDDLEWARE_CHAIN, + RestTags.ACTION_MIDDLEWARE_CHAIN, + ), + ); // Set up endpoints for OpenAPI spec/ui this._setupOpenApiSpecEndpoints(); @@ -323,8 +334,14 @@ export class RestServer extends BaseMiddlewareRegistry this._redirectToSwaggerUI(req, res, next), ); this.expressMiddleware('middleware.apiSpec.defaults', router, { - group: 'apiSpec', - }); + group: RestMiddlewareGroups.API_SPEC, + upstreamGroups: RestMiddlewareGroups.CORS, + }).apply( + extensionFor( + RestTags.REST_MIDDLEWARE_CHAIN, + RestTags.ACTION_MIDDLEWARE_CHAIN, + ), + ); } /** diff --git a/packages/rest/src/sequence.ts b/packages/rest/src/sequence.ts index abaef7ffe673..f25bb2f01a31 100644 --- a/packages/rest/src/sequence.ts +++ b/packages/rest/src/sequence.ts @@ -3,12 +3,17 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -const debug = require('debug')('loopback:rest:sequence'); -import {inject, ValueOrPromise} from '@loopback/core'; -import {InvokeMiddleware} from '@loopback/express'; -import {RestBindings} from './keys'; +import {config, inject, ValueOrPromise} from '@loopback/core'; +import { + InvokeMiddleware, + InvokeMiddlewareOptions, + MiddlewareGroups, +} 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; @@ -121,3 +126,126 @@ export class DefaultSequence implements SequenceHandler { } } } + +/** + * 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 + */ +export class MiddlewareSequence implements SequenceHandler { + 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, + ], + }; + + /** + * Constructor: Injects `InvokeMiddleware` and `InvokeMiddlewareOptions` + * + * @param invokeMiddleware - invoker for registered middleware in a chain. + * To be injected via RestBindings.INVOKE_MIDDLEWARE_SERVICE. + */ + constructor( + @inject(RestBindings.INVOKE_MIDDLEWARE_SERVICE) + readonly invokeMiddleware: InvokeMiddleware, + @config() + readonly options: InvokeMiddlewareOptions = MiddlewareSequence.defaultOptions, + ) {} + + /** + * 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 { + debug( + 'Invoking middleware chain %s with groups %s', + this.options.chain, + this.options.orderedGroups, + ); + await this.invokeMiddleware(context, this.options); + } +} diff --git a/packages/rest/src/types.ts b/packages/rest/src/types.ts index 74b59a092ce5..253f386cf4cd 100644 --- a/packages/rest/src/types.ts +++ b/packages/rest/src/types.ts @@ -26,7 +26,7 @@ export * from '@loopback/express'; export type FindRoute = (request: Request) => ResolvedRoute; /** - * + * A function to parse OpenAPI operation parameters for a given route */ export type ParseParams = ( request: Request, From 10ca3b2e1841a5f4b7a3b7790a1223baa1bb7c67 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Thu, 7 May 2020 12:15:31 -0700 Subject: [PATCH 3/9] feat(example-todo): switch to middleware based sequence --- examples/todo/src/application.ts | 3 ++- examples/todo/src/sequence.ts | 46 ++------------------------------ 2 files changed, 4 insertions(+), 45 deletions(-) diff --git a/examples/todo/src/application.ts b/examples/todo/src/application.ts index f514a10675eb..18da73a63045 100644 --- a/examples/todo/src/application.ts +++ b/examples/todo/src/application.ts @@ -6,7 +6,7 @@ import {BootMixin} from '@loopback/boot'; import {ApplicationConfig} from '@loopback/core'; import {RepositoryMixin} from '@loopback/repository'; -import {Request, Response, RestApplication} from '@loopback/rest'; +import {Request, Response, RestApplication, RestTags} from '@loopback/rest'; import {RestExplorerComponent} from '@loopback/rest-explorer'; import {ServiceMixin} from '@loopback/service-proxy'; import morgan from 'morgan'; @@ -62,6 +62,7 @@ export class TodoListApplication extends BootMixin( this.expressMiddleware(morganFactory, defaultConfig, { injectConfiguration: 'watch', key: 'middleware.morgan', + chain: RestTags.REST_MIDDLEWARE_CHAIN, }); } } diff --git a/examples/todo/src/sequence.ts b/examples/todo/src/sequence.ts index f1905a154542..f11a7062d49f 100644 --- a/examples/todo/src/sequence.ts +++ b/examples/todo/src/sequence.ts @@ -3,48 +3,6 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {inject} from '@loopback/core'; -import { - FindRoute, - InvokeMethod, - InvokeMiddleware, - ParseParams, - Reject, - RequestContext, - RestBindings, - Send, - SequenceHandler, -} from '@loopback/rest'; +import {MiddlewareSequence} from '@loopback/rest'; -const SequenceActions = RestBindings.SequenceActions; - -export class MySequence implements SequenceHandler { - /** - * Optional invoker for registered middleware in a chain. - * To be injected via SequenceActions.INVOKE_MIDDLEWARE. - */ - @inject(SequenceActions.INVOKE_MIDDLEWARE, {optional: true}) - protected invokeMiddleware: InvokeMiddleware = () => false; - - constructor( - @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute, - @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams, - @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, - @inject(SequenceActions.SEND) public send: Send, - @inject(SequenceActions.REJECT) public reject: Reject, - ) {} - - async handle(context: RequestContext) { - try { - const {request, response} = context; - const finished = await this.invokeMiddleware(context); - if (finished) return; - const route = this.findRoute(request); - const args = await this.parseParams(request, route); - const result = await this.invoke(route, args); - this.send(response, result); - } catch (err) { - this.reject(context, err); - } - } -} +export class MySequence extends MiddlewareSequence {} From 31281aff43c0c36a3a0403d0fbc50b1b8600777e Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Sat, 9 May 2020 14:33:46 -0700 Subject: [PATCH 4/9] feat(cli): update template for `src/sequence.ts` --- .../app/templates/src/sequence.ts.ejs | 46 +------------------ .../generators/app.integration.snapshots.js | 16 +++++-- .../integration/generators/app.integration.js | 1 + 3 files changed, 15 insertions(+), 48 deletions(-) diff --git a/packages/cli/generators/app/templates/src/sequence.ts.ejs b/packages/cli/generators/app/templates/src/sequence.ts.ejs index f564ebcfe84d..2fe7751cc1a5 100644 --- a/packages/cli/generators/app/templates/src/sequence.ts.ejs +++ b/packages/cli/generators/app/templates/src/sequence.ts.ejs @@ -1,45 +1,3 @@ -import {inject} from '@loopback/core'; -import { - FindRoute, - InvokeMethod, - InvokeMiddleware, - ParseParams, - Reject, - RequestContext, - RestBindings, - Send, - SequenceHandler, -} from '@loopback/rest'; +import {MiddlewareSequence} from '@loopback/rest'; -const SequenceActions = RestBindings.SequenceActions; - -export class MySequence implements SequenceHandler { - /** - * Optional invoker for registered middleware in a chain. - * To be injected via SequenceActions.INVOKE_MIDDLEWARE. - */ - @inject(SequenceActions.INVOKE_MIDDLEWARE, {optional: true}) - protected invokeMiddleware: InvokeMiddleware = () => false; - - constructor( - @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute, - @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams, - @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, - @inject(SequenceActions.SEND) public send: Send, - @inject(SequenceActions.REJECT) public reject: Reject, - ) {} - - async handle(context: RequestContext) { - try { - const {request, response} = context; - const finished = await this.invokeMiddleware(context); - if (finished) return; - const route = this.findRoute(request); - const args = await this.parseParams(request, route); - const result = await this.invoke(route, args); - this.send(response, result); - } catch (err) { - this.reject(context, err); - } - } -} +export class MySequence extends MiddlewareSequence {} diff --git a/packages/cli/snapshots/integration/generators/app.integration.snapshots.js b/packages/cli/snapshots/integration/generators/app.integration.snapshots.js index d166eb2920da..b80dc76995de 100644 --- a/packages/cli/snapshots/integration/generators/app.integration.snapshots.js +++ b/packages/cli/snapshots/integration/generators/app.integration.snapshots.js @@ -57,6 +57,14 @@ export class MyAppApplication extends BootMixin( exports[`app-generator specific files generates all the proper files 2`] = ` +import {MiddlewareSequence} from '@loopback/rest'; + +export class MySequence extends MiddlewareSequence {} + +`; + + +exports[`app-generator specific files generates all the proper files 3`] = ` import {ApplicationConfig, MyAppApplication} from './application'; export * from './application'; @@ -100,7 +108,7 @@ if (require.main === module) { `; -exports[`app-generator specific files generates all the proper files 3`] = ` +exports[`app-generator specific files generates all the proper files 4`] = ` import {Request, RestBindings, get, ResponseObject} from '@loopback/rest'; import {inject} from '@loopback/core'; @@ -157,7 +165,7 @@ export class PingController { `; -exports[`app-generator specific files generates all the proper files 4`] = ` +exports[`app-generator specific files generates all the proper files 5`] = ` import {Client, expect} from '@loopback/testlab'; import {MyAppApplication} from '../..'; import {setupApplication} from './test-helper'; @@ -183,7 +191,7 @@ describe('PingController', () => { `; -exports[`app-generator specific files generates all the proper files 5`] = ` +exports[`app-generator specific files generates all the proper files 6`] = ` import {Client} from '@loopback/testlab'; import {MyAppApplication} from '../..'; import {setupApplication} from './test-helper'; @@ -219,7 +227,7 @@ describe('HomePage', () => { `; -exports[`app-generator specific files generates all the proper files 6`] = ` +exports[`app-generator specific files generates all the proper files 7`] = ` import {MyAppApplication} from '../..'; import { createRestAppClient, diff --git a/packages/cli/test/integration/generators/app.integration.js b/packages/cli/test/integration/generators/app.integration.js index 66fadf6db456..34c7f240d599 100644 --- a/packages/cli/test/integration/generators/app.integration.js +++ b/packages/cli/test/integration/generators/app.integration.js @@ -42,6 +42,7 @@ describe('app-generator specific files', () => { assertFilesToMatchSnapshot( {}, 'src/application.ts', + 'src/sequence.ts', 'src/index.ts', 'src/controllers/ping.controller.ts', 'src/__tests__/acceptance/ping.controller.acceptance.ts', From d2099d0f3fddf8ecbbb2fe6e1932df93303c1e18 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Sun, 24 May 2020 12:55:04 -0700 Subject: [PATCH 5/9] feat(authentication): add a middleware for authentication --- ...ic-auth-extension.middleware.acceptance.ts | 271 ++++++++++ ...wt-auth-extension.middleware.acceptance.ts | 503 ++++++++++++++++++ .../authentication.middleware.sequence.ts | 8 + .../src/authentication.component.ts | 19 +- packages/authentication/src/keys.ts | 8 + .../src/providers/auth-action.provider.ts | 41 +- 6 files changed, 838 insertions(+), 12 deletions(-) create mode 100644 packages/authentication/src/__tests__/acceptance/basic-auth-extension.middleware.acceptance.ts create mode 100644 packages/authentication/src/__tests__/acceptance/jwt-auth-extension.middleware.acceptance.ts create mode 100644 packages/authentication/src/__tests__/fixtures/sequences/authentication.middleware.sequence.ts diff --git a/packages/authentication/src/__tests__/acceptance/basic-auth-extension.middleware.acceptance.ts b/packages/authentication/src/__tests__/acceptance/basic-auth-extension.middleware.acceptance.ts new file mode 100644 index 000000000000..22fe7deacc33 --- /dev/null +++ b/packages/authentication/src/__tests__/acceptance/basic-auth-extension.middleware.acceptance.ts @@ -0,0 +1,271 @@ +// Copyright IBM Corp. 2019,2020. All Rights Reserved. +// Node module: @loopback/authentication +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Application, inject} from '@loopback/core'; +import {anOpenApiSpec} from '@loopback/openapi-spec-builder'; +import {api, get, Request, RestServer} from '@loopback/rest'; +import {SecurityBindings, securityId, UserProfile} from '@loopback/security'; +import {Client, createClientForHandler, expect} from '@loopback/testlab'; +import { + authenticate, + AuthenticationBindings, + registerAuthenticationStrategy, +} from '../..'; +import {AuthenticationStrategy} from '../../types'; +import { + createBasicAuthorizationHeaderValue, + getApp, + getUserRepository, + myUserProfileFactory, +} from '../fixtures/helper'; +import {BasicAuthenticationStrategyBindings, USER_REPO} from '../fixtures/keys'; +import {AuthenticationMiddlewareSequence} from '../fixtures/sequences/authentication.middleware.sequence'; +import {BasicAuthenticationUserService} from '../fixtures/services/basic-auth-user-service'; +import {BasicAuthenticationStrategy} from '../fixtures/strategies/basic-strategy'; +import {User} from '../fixtures/users/user'; +import {UserRepository} from '../fixtures/users/user.repository'; + +describe('Basic Authentication', () => { + let app: Application; + let server: RestServer; + let users: UserRepository; + let joeUser: User; + beforeEach(givenAServer); + beforeEach(givenControllerInApp); + beforeEach(givenAuthenticatedSequence); + beforeEach(givenProviders); + + it(`authenticates successfully for correct credentials of user 'jack'`, async () => { + const client = whenIMakeRequestTo(server); + await client + .get('/whoAmI') + .set('Authorization', createBasicAuthorizationHeaderValue(joeUser)) + .expect(joeUser.id); + }); + + it('returns error for missing Authorization header', async () => { + const client = whenIMakeRequestTo(server); + + await client.get('/whoAmI').expect({ + error: { + message: 'Authorization header not found.', + name: 'UnauthorizedError', + statusCode: 401, + }, + }); + }); + + it(`returns error for missing 'Basic ' portion of Authorization header value`, async () => { + const client = whenIMakeRequestTo(server); + await client + .get('/whoAmI') + .set( + 'Authorization', + createBasicAuthorizationHeaderValue(joeUser, {prefix: 'NotB@sic '}), + ) + .expect({ + error: { + message: `Authorization header is not of type 'Basic'.`, + name: 'UnauthorizedError', + statusCode: 401, + }, + }); + }); + + it(`returns error for too many parts in Authorization header value`, async () => { + const client = whenIMakeRequestTo(server); + await client + .get('/whoAmI') + .set( + 'Authorization', + createBasicAuthorizationHeaderValue(joeUser) + ' someOtherValue', + ) + .expect({ + error: { + message: `Authorization header value has too many parts. It must follow the pattern: 'Basic xxyyzz' where xxyyzz is a base64 string.`, + name: 'UnauthorizedError', + statusCode: 401, + }, + }); + }); + + it(`returns error for missing ':' in decrypted Authorization header credentials value`, async () => { + const client = whenIMakeRequestTo(server); + await client + .get('/whoAmI') + .set( + 'Authorization', + createBasicAuthorizationHeaderValue(joeUser, {separator: '|'}), + ) + .expect({ + error: { + message: `Authorization header 'Basic' value does not contain two parts separated by ':'.`, + name: 'UnauthorizedError', + statusCode: 401, + }, + }); + }); + + it(`returns error for too many parts in decrypted Authorization header credentials value`, async () => { + const client = whenIMakeRequestTo(server); + await client + .get('/whoAmI') + .set( + 'Authorization', + createBasicAuthorizationHeaderValue(joeUser, { + extraSegment: 'extraPart', + }), + ) + .expect({ + error: { + message: `Authorization header 'Basic' value does not contain two parts separated by ':'.`, + name: 'UnauthorizedError', + statusCode: 401, + }, + }); + }); + + it('allows anonymous requests to methods with no decorator', async () => { + class InfoController { + @get('/status') + status() { + return {running: true}; + } + } + + app.controller(InfoController); + await whenIMakeRequestTo(server) + .get('/status') + .expect(200, {running: true}); + }); + + it('returns error for unknown authentication strategy', async () => { + class InfoController { + @get('/status') + @authenticate('doesnotexist') + status() { + return {running: true}; + } + } + + app.controller(InfoController); + await whenIMakeRequestTo(server) + .get('/status') + .expect({ + error: { + message: `The strategy 'doesnotexist' is not available.`, + name: 'Error', + statusCode: 401, + code: 'AUTHENTICATION_STRATEGY_NOT_FOUND', + }, + }); + }); + + it('returns error when undefined user profile returned from authentication strategy', async () => { + class BadBasicStrategy implements AuthenticationStrategy { + name = 'badbasic'; + async authenticate(request: Request): Promise { + return undefined; + } + } + registerAuthenticationStrategy(server, BadBasicStrategy); + + class InfoController { + @get('/status') + @authenticate('badbasic') + status() { + return {running: true}; + } + } + + app.controller(InfoController); + await whenIMakeRequestTo(server) + .get('/status') + .expect({ + error: { + message: `User profile not returned from strategy's authenticate function`, + name: 'Error', + statusCode: 401, + code: 'USER_PROFILE_NOT_FOUND', + }, + }); + }); + + it('adds security scheme component to apiSpec', async () => { + const EXPECTED_SPEC = { + components: { + securitySchemes: { + basic: { + type: 'http', + scheme: 'basic', + }, + }, + }, + }; + const spec = await server.getApiSpec(); + expect(spec).to.containDeep(EXPECTED_SPEC); + }); + + async function givenAServer() { + app = getApp(); + server = await app.getServer(RestServer); + } + + function givenControllerInApp() { + const apispec = anOpenApiSpec() + .withOperation('get', '/whoAmI', { + 'x-operation-name': 'whoAmI', + responses: { + '200': { + description: '', + schema: { + type: 'string', + }, + }, + }, + }) + .build(); + + @api(apispec) + class MyController { + constructor() {} + + @authenticate('basic') + async whoAmI( + @inject(SecurityBindings.USER) userProfile: UserProfile, + ): Promise { + if (!userProfile) return 'userProfile is undefined'; + if (!userProfile[securityId]) return 'userProfile id is undefined'; + return userProfile[securityId]; + } + } + app.controller(MyController); + } + + function givenAuthenticatedSequence() { + // bind user defined sequence + server.sequence(AuthenticationMiddlewareSequence); + } + + function givenProviders() { + registerAuthenticationStrategy(server, BasicAuthenticationStrategy); + + server + .bind(BasicAuthenticationStrategyBindings.USER_SERVICE) + .toClass(BasicAuthenticationUserService); + + users = getUserRepository(); + joeUser = users.list['joe888']; + server.bind(USER_REPO).to(users); + + server + .bind(AuthenticationBindings.USER_PROFILE_FACTORY) + .to(myUserProfileFactory); + } + + function whenIMakeRequestTo(restServer: RestServer): Client { + return createClientForHandler(restServer.requestHandler); + } +}); diff --git a/packages/authentication/src/__tests__/acceptance/jwt-auth-extension.middleware.acceptance.ts b/packages/authentication/src/__tests__/acceptance/jwt-auth-extension.middleware.acceptance.ts new file mode 100644 index 000000000000..e3066da598f6 --- /dev/null +++ b/packages/authentication/src/__tests__/acceptance/jwt-auth-extension.middleware.acceptance.ts @@ -0,0 +1,503 @@ +// Copyright IBM Corp. 2019,2020. All Rights Reserved. +// Node module: @loopback/authentication +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Application, inject} from '@loopback/core'; +import {get, post, Request, RestServer} from '@loopback/rest'; +import {SecurityBindings, securityId, UserProfile} from '@loopback/security'; +import {Client, createClientForHandler, expect} from '@loopback/testlab'; +import { + authenticate, + AuthenticationBindings, + AuthenticationStrategy, + registerAuthenticationStrategy, +} from '../..'; +import {UserProfileFactory} from '../../types'; +import { + createBearerAuthorizationHeaderValue, + getApp, + getUserRepository, + myUserProfileFactory, +} from '../fixtures/helper'; +import {JWTAuthenticationStrategyBindings, USER_REPO} from '../fixtures/keys'; +import {AuthenticationMiddlewareSequence} from '../fixtures/sequences/authentication.middleware.sequence'; +import {JWTService} from '../fixtures/services/jwt-service'; +import {JWTAuthenticationStrategy} from '../fixtures/strategies/jwt-strategy'; +import {User} from '../fixtures/users/user'; +import {UserRepository} from '../fixtures/users/user.repository'; + +describe('JWT Authentication', () => { + let app: Application; + let server: RestServer; + let testUsers: UserRepository; + let joeUser: User; + let token: string; + const TOKEN_SECRET_VALUE = 'myjwts3cr3t'; + const TOKEN_EXPIRES_IN_VALUE = '600'; + + beforeEach(givenAServer); + beforeEach(givenAuthenticatedSequence); + beforeEach(givenProviders); + + it('authenticates successfully with valid token', async () => { + class InfoController { + constructor( + @inject(JWTAuthenticationStrategyBindings.TOKEN_SERVICE) + public tokenService: JWTService, + @inject(USER_REPO) + public users: UserRepository, + @inject(AuthenticationBindings.USER_PROFILE_FACTORY) + public userProfileFactory: UserProfileFactory, + ) {} + + @post('/login') + async logIn() { + // + // ...Other code for verifying a valid user (e.g. basic or local strategy)... + // + + // Now with a valid userProfile, let's create a JSON web token + return this.tokenService.generateToken( + this.userProfileFactory(joeUser), + ); + } + + @get('/whoAmI') + @authenticate('jwt') + whoAmI(@inject(SecurityBindings.USER) userProfile: UserProfile) { + if (!userProfile) return 'userProfile is undefined'; + if (!userProfile[securityId]) + return 'userProfile[securityId] is undefined'; + return userProfile[securityId]; + } + } + + app.controller(InfoController); + + token = (await whenIMakeRequestTo(server).post('/login').expect(200)).text; + + expect(token).to.be.not.null(); + expect(token).to.be.String(); + + const id = ( + await whenIMakeRequestTo(server) + .get('/whoAmI') + .set('Authorization', createBearerAuthorizationHeaderValue(token)) + .expect(200) + ).text; + + expect(id).to.equal(joeUser.id); + }); + + it('returns error for missing Authorization header', async () => { + class InfoController { + constructor( + @inject(JWTAuthenticationStrategyBindings.TOKEN_SERVICE) + public tokenService: JWTService, + @inject(USER_REPO) + public users: UserRepository, + @inject(AuthenticationBindings.USER_PROFILE_FACTORY) + public userProfileFactory: UserProfileFactory, + ) {} + + @post('/login') + async logIn() { + // + // ...Other code for verifying a valid user (e.g. basic or local strategy)... + // + + // Now with a valid userProfile, let's create a JSON web token + return this.tokenService.generateToken( + this.userProfileFactory(joeUser), + ); + } + + @get('/whoAmI') + @authenticate('jwt') + whoAmI(@inject(SecurityBindings.USER) userProfile: UserProfile) { + if (!userProfile) return 'userProfile is undefined'; + if (!userProfile[securityId]) + return 'userProfile[securityId] is undefined'; + return userProfile[securityId]; + } + } + + app.controller(InfoController); + + token = (await whenIMakeRequestTo(server).post('/login').expect(200)).text; + + expect(token).to.be.not.null(); + expect(token).to.be.String(); + + await whenIMakeRequestTo(server) + .get('/whoAmI') + .expect({ + error: { + message: 'Authorization header not found.', + name: 'UnauthorizedError', + statusCode: 401, + }, + }); + }); + + it(`returns error for invalid 'Bearer ' portion of Authorization header value`, async () => { + class InfoController { + constructor( + @inject(JWTAuthenticationStrategyBindings.TOKEN_SERVICE) + public tokenService: JWTService, + @inject(USER_REPO) + public users: UserRepository, + @inject(AuthenticationBindings.USER_PROFILE_FACTORY) + public userProfileFactory: UserProfileFactory, + ) {} + + @post('/login') + async logIn() { + // + // ...Other code for verifying a valid user (e.g. basic or local strategy)... + // + + // Now with a valid userProfile, let's create a JSON web token + return this.tokenService.generateToken( + this.userProfileFactory(joeUser), + ); + } + + @get('/whoAmI') + @authenticate('jwt') + whoAmI(@inject(SecurityBindings.USER) userProfile: UserProfile) { + if (!userProfile) return 'userProfile is undefined'; + if (!userProfile[securityId]) + return 'userProfile[securityId] is undefined'; + return userProfile[securityId]; + } + } + + app.controller(InfoController); + + token = (await whenIMakeRequestTo(server).post('/login').expect(200)).text; + + expect(token).to.be.not.null(); + expect(token).to.be.String(); + + await whenIMakeRequestTo(server) + .get('/whoAmI') + .set( + 'Authorization', + createBearerAuthorizationHeaderValue(token, 'NotB3ar3r '), + ) + .expect({ + error: { + message: `Authorization header is not of type 'Bearer'.`, + name: 'UnauthorizedError', + statusCode: 401, + }, + }); + }); + + it(`returns error for too many parts in Authorization header value`, async () => { + class InfoController { + constructor( + @inject(JWTAuthenticationStrategyBindings.TOKEN_SERVICE) + public tokenService: JWTService, + @inject(USER_REPO) + public users: UserRepository, + @inject(AuthenticationBindings.USER_PROFILE_FACTORY) + public userProfileFactory: UserProfileFactory, + ) {} + + @post('/login') + async logIn() { + // + // ...Other code for verifying a valid user (e.g. basic or local strategy)... + // + + return this.tokenService.generateToken( + this.userProfileFactory(joeUser), + ); + } + + @get('/whoAmI') + @authenticate('jwt') + whoAmI(@inject(SecurityBindings.USER) userProfile: UserProfile) { + if (!userProfile) return 'userProfile is undefined'; + if (!userProfile[securityId]) + return 'userProfile[securityId] is undefined'; + return userProfile[securityId]; + } + } + + app.controller(InfoController); + + token = (await whenIMakeRequestTo(server).post('/login').expect(200)).text; + + expect(token).to.be.not.null(); + expect(token).to.be.String(); + + await whenIMakeRequestTo(server) + .get('/whoAmI') + .set( + 'Authorization', + createBearerAuthorizationHeaderValue(token) + ' someOtherValue', + ) + .expect({ + error: { + message: `Authorization header value has too many parts. It must follow the pattern: 'Bearer xx.yy.zz' where xx.yy.zz is a valid JWT token.`, + name: 'UnauthorizedError', + statusCode: 401, + }, + }); + }); + + it('returns error due to expired token', async () => { + class InfoController { + constructor() {} + + @get('/whoAmI') + @authenticate('jwt') + whoAmI(@inject(SecurityBindings.USER) userProfile: UserProfile) { + if (!userProfile) return 'userProfile is undefined'; + if (!userProfile[securityId]) + return 'userProfile[securityId] is undefined'; + return userProfile[securityId]; + } + } + + app.controller(InfoController); + + const expiredToken = await getExpiredToken(); + + await whenIMakeRequestTo(server) + .get('/whoAmI') + .set('Authorization', createBearerAuthorizationHeaderValue(expiredToken)) + .expect({ + error: { + message: `Error verifying token : jwt expired`, + name: 'UnauthorizedError', + statusCode: 401, + }, + }); + }); + + it('returns error due to invalid token #1', async () => { + class InfoController { + constructor() {} + + @get('/whoAmI') + @authenticate('jwt') + whoAmI(@inject(SecurityBindings.USER) userProfile: UserProfile) { + if (!userProfile) return 'userProfile is undefined'; + if (!userProfile[securityId]) + return 'userProfile[securityId] is undefined'; + return userProfile[securityId]; + } + } + + app.controller(InfoController); + + const invalidToken = 'aaa.bbb.ccc'; + + await whenIMakeRequestTo(server) + .get('/whoAmI') + .set('Authorization', createBearerAuthorizationHeaderValue(invalidToken)) + .expect({ + error: { + message: 'Error verifying token : invalid token', + name: 'UnauthorizedError', + statusCode: 401, + }, + }); + }); + + it('returns error due to invalid token #2', async () => { + class InfoController { + constructor() {} + + @get('/whoAmI') + @authenticate('jwt') + whoAmI(@inject(SecurityBindings.USER) userProfile: UserProfile) { + if (!userProfile) return 'userProfile is undefined'; + if (!userProfile[securityId]) + return 'userProfile[securityId] is undefined'; + return userProfile[securityId]; + } + } + + app.controller(InfoController); + + const invalidToken = 'aaa.bbb.ccc.ddd'; + + await whenIMakeRequestTo(server) + .get('/whoAmI') + .set('Authorization', createBearerAuthorizationHeaderValue(invalidToken)) + .expect({ + error: { + message: 'Error verifying token : jwt malformed', + name: 'UnauthorizedError', + statusCode: 401, + }, + }); + }); + + it('creates a json web token and throws error for userProfile that is undefined', async () => { + class InfoController { + constructor( + @inject(JWTAuthenticationStrategyBindings.TOKEN_SERVICE) + public tokenService: JWTService, + @inject(USER_REPO) + public users: UserRepository, + @inject(AuthenticationBindings.USER_PROFILE_FACTORY) + public userProfileFactory: UserProfileFactory, + ) {} + + @get('/createtoken') + async createToken() { + return this.tokenService.generateToken(undefined); + } + } + + app.controller(InfoController); + + await whenIMakeRequestTo(server) + .get('/createtoken') + .expect({ + error: { + message: `Error generating token : userProfile is null`, + name: 'UnauthorizedError', + statusCode: 401, + }, + }); + }); + + it('allows anonymous requests to methods with no decorator', async () => { + class InfoController { + @get('/status') + status() { + return {running: true}; + } + } + + app.controller(InfoController); + await whenIMakeRequestTo(server) + .get('/status') + .expect(200, {running: true}); + }); + + it('returns error for unknown authentication strategy', async () => { + class InfoController { + @get('/status') + @authenticate('doesnotexist') + status() { + return {running: true}; + } + } + + app.controller(InfoController); + await whenIMakeRequestTo(server) + .get('/status') + .expect({ + error: { + message: `The strategy 'doesnotexist' is not available.`, + name: 'Error', + statusCode: 401, + code: 'AUTHENTICATION_STRATEGY_NOT_FOUND', + }, + }); + }); + + it('returns error when undefined user profile returned from authentication strategy', async () => { + class BadJWTStrategy implements AuthenticationStrategy { + name = 'badjwt'; + async authenticate(request: Request): Promise { + return undefined; + } + } + registerAuthenticationStrategy(server, BadJWTStrategy); + + class InfoController { + @get('/status') + @authenticate('badjwt') + status() { + return {running: true}; + } + } + + app.controller(InfoController); + await whenIMakeRequestTo(server) + .get('/status') + .expect({ + error: { + message: `User profile not returned from strategy's authenticate function`, + name: 'Error', + statusCode: 401, + code: 'USER_PROFILE_NOT_FOUND', + }, + }); + }); + + it('adds security scheme component to apiSpec', async () => { + const EXPECTED_SPEC = { + components: { + securitySchemes: { + jwt: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + }, + }, + }; + const spec = await server.getApiSpec(); + expect(spec).to.containDeep(EXPECTED_SPEC); + }); + + async function givenAServer() { + app = getApp(); + server = await app.getServer(RestServer); + } + + /** + * Creates an expired token + * + * Specifying a negative value for 'expiresIn' so the + * token is automatically expired + */ + async function getExpiredToken() { + const userProfile = myUserProfileFactory(joeUser); + const tokenService = new JWTService(TOKEN_SECRET_VALUE, '-10'); + return tokenService.generateToken(userProfile); + } + + function givenAuthenticatedSequence() { + // bind user defined sequence + server.sequence(AuthenticationMiddlewareSequence); + } + + function givenProviders() { + registerAuthenticationStrategy(server, JWTAuthenticationStrategy); + + server + .bind(JWTAuthenticationStrategyBindings.TOKEN_SECRET) + .to(TOKEN_SECRET_VALUE); + + server + .bind(JWTAuthenticationStrategyBindings.TOKEN_EXPIRES_IN) + .to(TOKEN_EXPIRES_IN_VALUE); + + server + .bind(JWTAuthenticationStrategyBindings.TOKEN_SERVICE) + .toClass(JWTService); + + testUsers = getUserRepository(); + joeUser = testUsers.list['joe888']; + server.bind(USER_REPO).to(testUsers); + server + .bind(AuthenticationBindings.USER_PROFILE_FACTORY) + .to(myUserProfileFactory); + } + + function whenIMakeRequestTo(restServer: RestServer): Client { + return createClientForHandler(restServer.requestHandler); + } +}); diff --git a/packages/authentication/src/__tests__/fixtures/sequences/authentication.middleware.sequence.ts b/packages/authentication/src/__tests__/fixtures/sequences/authentication.middleware.sequence.ts new file mode 100644 index 000000000000..ba7b9b99d21e --- /dev/null +++ b/packages/authentication/src/__tests__/fixtures/sequences/authentication.middleware.sequence.ts @@ -0,0 +1,8 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/authentication +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {MiddlewareSequence} from '@loopback/rest'; + +export class AuthenticationMiddlewareSequence extends MiddlewareSequence {} diff --git a/packages/authentication/src/authentication.component.ts b/packages/authentication/src/authentication.component.ts index df158aaf35bd..f3638c17e987 100644 --- a/packages/authentication/src/authentication.component.ts +++ b/packages/authentication/src/authentication.component.ts @@ -3,23 +3,22 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {bind, Component, ContextTags, ProviderMap} from '@loopback/core'; +import {bind, Component, ContextTags} from '@loopback/core'; import {AuthenticationBindings} from './keys'; import { AuthenticateActionProvider, + AuthenticationMiddlewareProvider, AuthenticationStrategyProvider, AuthMetadataProvider, } from './providers'; @bind({tags: {[ContextTags.KEY]: AuthenticationBindings.COMPONENT}}) export class AuthenticationComponent implements Component { - providers?: ProviderMap; - - constructor() { - this.providers = { - [AuthenticationBindings.AUTH_ACTION.key]: AuthenticateActionProvider, - [AuthenticationBindings.STRATEGY.key]: AuthenticationStrategyProvider, - [AuthenticationBindings.METADATA.key]: AuthMetadataProvider, - }; - } + providers = { + [AuthenticationBindings.AUTH_ACTION.key]: AuthenticateActionProvider, + [AuthenticationBindings.STRATEGY.key]: AuthenticationStrategyProvider, + [AuthenticationBindings.METADATA.key]: AuthMetadataProvider, + [AuthenticationBindings.AUTHENTICATION_MIDDLEWARE + .key]: AuthenticationMiddlewareProvider, + }; } diff --git a/packages/authentication/src/keys.ts b/packages/authentication/src/keys.ts index 28f1bb7e55fa..615e0dfdb608 100644 --- a/packages/authentication/src/keys.ts +++ b/packages/authentication/src/keys.ts @@ -5,6 +5,7 @@ import {BindingKey, MetadataAccessor} from '@loopback/core'; import {SecurityBindings, UserProfile} from '@loopback/security'; +import {Middleware} from '@loopback/rest'; import {AuthenticationComponent} from './authentication.component'; import { AuthenticateFn, @@ -88,6 +89,13 @@ export namespace AuthenticationBindings { 'authentication.actions.authenticate', ); + /** + * Binding key for AUTHENTICATION_MIDDLEWARE + */ + export const AUTHENTICATION_MIDDLEWARE = BindingKey.create( + 'middleware.authentication', + ); + /** * Key used to inject authentication metadata, which is used to determine * whether a request requires authentication or not. diff --git a/packages/authentication/src/providers/auth-action.provider.ts b/packages/authentication/src/providers/auth-action.provider.ts index 40e54f0fbbfe..601c1c789fbf 100644 --- a/packages/authentication/src/providers/auth-action.provider.ts +++ b/packages/authentication/src/providers/auth-action.provider.ts @@ -3,13 +3,20 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Getter, inject, Provider, Setter} from '@loopback/core'; -import {Request, RedirectRoute} from '@loopback/rest'; +import {bind, Getter, inject, Provider, Setter} from '@loopback/core'; +import { + asMiddleware, + Middleware, + RedirectRoute, + Request, + RestMiddlewareGroups, +} from '@loopback/rest'; import {SecurityBindings, UserProfile} from '@loopback/security'; import {AuthenticationBindings} from '../keys'; import { AuthenticateFn, AuthenticationStrategy, + AUTHENTICATION_STRATEGY_NOT_FOUND, USER_PROFILE_NOT_FOUND, } from '../types'; /** @@ -80,3 +87,33 @@ export class AuthenticateActionProvider implements Provider { } } } + +@bind( + asMiddleware({ + group: RestMiddlewareGroups.AUTHENTICATION, + upstreamGroups: [RestMiddlewareGroups.FIND_ROUTE], + }), +) +export class AuthenticationMiddlewareProvider implements Provider { + constructor( + @inject(AuthenticationBindings.AUTH_ACTION) + private authenticate: AuthenticateFn, + ) {} + + value(): Middleware { + return async (ctx, next) => { + try { + await this.authenticate(ctx.request); + } catch (error) { + if ( + error.code === AUTHENTICATION_STRATEGY_NOT_FOUND || + error.code === USER_PROFILE_NOT_FOUND + ) { + error.statusCode = 401; + } + throw error; + } + return next(); + }; + } +} From 5c3dd0ebfd656dbc317b18750224fc578e18bae6 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 15 Jul 2020 10:34:14 -0700 Subject: [PATCH 6/9] feat(example-multi-tenancy): refactor the code to use middleware --- .../user.controller.header.acceptance.ts | 8 +-- .../user.controller.jwt.acceptance.ts | 4 +- examples/multi-tenancy/src/application.ts | 2 +- examples/multi-tenancy/src/index.ts | 1 + .../src/multi-tenancy/component.ts | 6 +-- .../multi-tenancy/src/multi-tenancy/index.ts | 2 +- .../multi-tenancy/src/multi-tenancy/keys.ts | 7 +-- .../multi-tenancy-middleware.provider.ts} | 46 +++++++++-------- .../multi-tenancy/src/multi-tenancy/types.ts | 9 +--- examples/multi-tenancy/src/sequence.ts | 51 +------------------ 10 files changed, 44 insertions(+), 92 deletions(-) rename examples/multi-tenancy/src/multi-tenancy/{actions/multi-tenancy-action.provider.ts => middleware/multi-tenancy-middleware.provider.ts} (75%) diff --git a/examples/multi-tenancy/src/__tests__/acceptance/user.controller.header.acceptance.ts b/examples/multi-tenancy/src/__tests__/acceptance/user.controller.header.acceptance.ts index 9c96e403ce99..b15d00130f6b 100644 --- a/examples/multi-tenancy/src/__tests__/acceptance/user.controller.header.acceptance.ts +++ b/examples/multi-tenancy/src/__tests__/acceptance/user.controller.header.acceptance.ts @@ -4,11 +4,11 @@ // License text available at https://opensource.org/licenses/MIT import {Client, expect} from '@loopback/testlab'; -import {ExampleMultiTenancyApplication} from '../..'; import { - MultiTenancyActionOptions, + ExampleMultiTenancyApplication, MultiTenancyBindings, -} from '../../multi-tenancy'; + MultiTenancyMiddlewareOptions, +} from '../..'; import {setupApplication} from './test-helper'; describe('UserController with header-based multi-tenancy', () => { @@ -18,7 +18,7 @@ describe('UserController with header-based multi-tenancy', () => { before('setupApplication', async () => { ({app, client} = await setupApplication()); app - .configure(MultiTenancyBindings.ACTION) + .configure(MultiTenancyBindings.MIDDLEWARE) .to({strategyNames: ['jwt', 'header', 'query']}); }); diff --git a/examples/multi-tenancy/src/__tests__/acceptance/user.controller.jwt.acceptance.ts b/examples/multi-tenancy/src/__tests__/acceptance/user.controller.jwt.acceptance.ts index cf676c4b800e..37e348c8b472 100644 --- a/examples/multi-tenancy/src/__tests__/acceptance/user.controller.jwt.acceptance.ts +++ b/examples/multi-tenancy/src/__tests__/acceptance/user.controller.jwt.acceptance.ts @@ -7,8 +7,8 @@ import {Client, expect, supertest} from '@loopback/testlab'; import {sign} from 'jsonwebtoken'; import {ExampleMultiTenancyApplication} from '../..'; import { - MultiTenancyActionOptions, MultiTenancyBindings, + MultiTenancyMiddlewareOptions, } from '../../multi-tenancy'; import {setupApplication} from './test-helper'; @@ -19,7 +19,7 @@ describe('UserController with jwt-based multi-tenancy', () => { before('setupApplication', async () => { ({app, client} = await setupApplication()); app - .configure(MultiTenancyBindings.ACTION) + .configure(MultiTenancyBindings.MIDDLEWARE) .to({strategyNames: ['jwt', 'header', 'query']}); }); diff --git a/examples/multi-tenancy/src/application.ts b/examples/multi-tenancy/src/application.ts index eb07ef816058..f465f95612a8 100644 --- a/examples/multi-tenancy/src/application.ts +++ b/examples/multi-tenancy/src/application.ts @@ -37,7 +37,7 @@ export class ExampleMultiTenancyApplication extends BootMixin( this.component(RestExplorerComponent); /* - * app.configure(MultiTenancyBindings.ACTION) + * app.configure(MultiTenancyBindings.MIDDLEWARE) * .to({strategyName: ['jwt', 'header', 'query']}); */ this.component(MultiTenancyComponent); diff --git a/examples/multi-tenancy/src/index.ts b/examples/multi-tenancy/src/index.ts index ac4e2894e528..a8abcd7b808f 100644 --- a/examples/multi-tenancy/src/index.ts +++ b/examples/multi-tenancy/src/index.ts @@ -6,6 +6,7 @@ import {ApplicationConfig, ExampleMultiTenancyApplication} from './application'; export * from './application'; +export * from './multi-tenancy'; export async function main(options: ApplicationConfig = {}) { const app = new ExampleMultiTenancyApplication(options); diff --git a/examples/multi-tenancy/src/multi-tenancy/component.ts b/examples/multi-tenancy/src/multi-tenancy/component.ts index 9f5571cd2e28..6add45c52d94 100644 --- a/examples/multi-tenancy/src/multi-tenancy/component.ts +++ b/examples/multi-tenancy/src/multi-tenancy/component.ts @@ -9,8 +9,8 @@ import { createBindingFromClass, extensionFor, } from '@loopback/core'; -import {MultiTenancyActionProvider} from './actions/multi-tenancy-action.provider'; import {MultiTenancyBindings, MULTI_TENANCY_STRATEGIES} from './keys'; +import {MultiTenancyMiddlewareProvider} from './middleware/multi-tenancy-middleware.provider'; import { HeaderStrategy, HostStrategy, @@ -20,8 +20,8 @@ import { export class MultiTenancyComponent implements Component { bindings: Binding[] = [ - createBindingFromClass(MultiTenancyActionProvider, { - key: MultiTenancyBindings.ACTION, + createBindingFromClass(MultiTenancyMiddlewareProvider, { + key: MultiTenancyBindings.MIDDLEWARE, }), createBindingFromClass(JWTStrategy).apply( extensionFor(MULTI_TENANCY_STRATEGIES), diff --git a/examples/multi-tenancy/src/multi-tenancy/index.ts b/examples/multi-tenancy/src/multi-tenancy/index.ts index 479d98bca678..83b47a78f547 100644 --- a/examples/multi-tenancy/src/multi-tenancy/index.ts +++ b/examples/multi-tenancy/src/multi-tenancy/index.ts @@ -3,6 +3,6 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -export * from './actions/multi-tenancy-action.provider'; export * from './keys'; +export * from './middleware/multi-tenancy-middleware.provider'; export * from './types'; diff --git a/examples/multi-tenancy/src/multi-tenancy/keys.ts b/examples/multi-tenancy/src/multi-tenancy/keys.ts index 5ff357e43363..27a56f743880 100644 --- a/examples/multi-tenancy/src/multi-tenancy/keys.ts +++ b/examples/multi-tenancy/src/multi-tenancy/keys.ts @@ -4,11 +4,12 @@ // License text available at https://opensource.org/licenses/MIT import {BindingKey} from '@loopback/core'; -import {MultiTenancyAction, Tenant} from './types'; +import {Middleware} from '@loopback/rest'; +import {Tenant} from './types'; export namespace MultiTenancyBindings { - export const ACTION = BindingKey.create( - 'sequence.actions.multi-tenancy', + export const MIDDLEWARE = BindingKey.create( + 'middleware.multi-tenancy', ); export const CURRENT_TENANT = BindingKey.create( diff --git a/examples/multi-tenancy/src/multi-tenancy/actions/multi-tenancy-action.provider.ts b/examples/multi-tenancy/src/multi-tenancy/middleware/multi-tenancy-middleware.provider.ts similarity index 75% rename from examples/multi-tenancy/src/multi-tenancy/actions/multi-tenancy-action.provider.ts rename to examples/multi-tenancy/src/multi-tenancy/middleware/multi-tenancy-middleware.provider.ts index 75ab0b80da8f..e807502ca033 100644 --- a/examples/multi-tenancy/src/multi-tenancy/actions/multi-tenancy-action.provider.ts +++ b/examples/multi-tenancy/src/multi-tenancy/middleware/multi-tenancy-middleware.provider.ts @@ -6,42 +6,46 @@ import { config, ContextTags, - Getter, - Provider, extensionPoint, extensions, + Getter, + Provider, } from '@loopback/core'; -import {RequestContext} from '@loopback/rest'; +import {asMiddleware, Middleware, RequestContext} from '@loopback/rest'; import debugFactory from 'debug'; import {MultiTenancyBindings, MULTI_TENANCY_STRATEGIES} from '../keys'; -import { - MultiTenancyAction, - MultiTenancyActionOptions, - MultiTenancyStrategy, -} from '../types'; -const debug = debugFactory('loopback:multi-tenancy:action'); +import {MultiTenancyMiddlewareOptions, MultiTenancyStrategy} from '../types'; +const debug = debugFactory('loopback:multi-tenancy'); /** * Provides the multi-tenancy action for a sequence */ -@extensionPoint(MULTI_TENANCY_STRATEGIES, { - tags: { - [ContextTags.KEY]: MultiTenancyBindings.ACTION, +@extensionPoint( + MULTI_TENANCY_STRATEGIES, + { + tags: { + [ContextTags.KEY]: MultiTenancyBindings.MIDDLEWARE, + }, }, -}) -export class MultiTenancyActionProvider - implements Provider { + asMiddleware({ + group: 'tenancy', + downstreamGroups: 'findRoute', + }), +) +export class MultiTenancyMiddlewareProvider implements Provider { constructor( @extensions() private readonly getMultiTenancyStrategies: Getter, @config() - private options: MultiTenancyActionOptions = {strategyNames: ['header']}, + private options: MultiTenancyMiddlewareOptions = { + strategyNames: ['header'], + }, ) {} - /** - * @returns MultiTenancyStrategyFactory - */ - value(): MultiTenancyAction { - return this.action.bind(this); + value(): Middleware { + return async (ctx, next) => { + await this.action(ctx as RequestContext); + return next(); + }; } /** diff --git a/examples/multi-tenancy/src/multi-tenancy/types.ts b/examples/multi-tenancy/src/multi-tenancy/types.ts index 14892f2172d5..cf906f544c30 100644 --- a/examples/multi-tenancy/src/multi-tenancy/types.ts +++ b/examples/multi-tenancy/src/multi-tenancy/types.ts @@ -14,14 +14,7 @@ export interface Tenant { [attribute: string]: unknown; } -/** - * Resolve a tenant for the given request - */ -export interface MultiTenancyAction { - (requestContext: RequestContext): ValueOrPromise; -} - -export interface MultiTenancyActionOptions { +export interface MultiTenancyMiddlewareOptions { strategyNames: string[]; } diff --git a/examples/multi-tenancy/src/sequence.ts b/examples/multi-tenancy/src/sequence.ts index 2097c7cc7e50..35c1bbfaa202 100644 --- a/examples/multi-tenancy/src/sequence.ts +++ b/examples/multi-tenancy/src/sequence.ts @@ -3,53 +3,6 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {inject} from '@loopback/core'; -import { - FindRoute, - InvokeMethod, - InvokeMiddleware, - ParseParams, - Reject, - RequestContext, - RestBindings, - Send, - SequenceHandler, -} from '@loopback/rest'; -import {MultiTenancyAction, MultiTenancyBindings} from './multi-tenancy'; +import {MiddlewareSequence} from '@loopback/rest'; -const SequenceActions = RestBindings.SequenceActions; - -export class MySequence implements SequenceHandler { - /** - * Optional invoker for registered middleware in a chain. - * To be injected via SequenceActions.INVOKE_MIDDLEWARE. - */ - @inject(SequenceActions.INVOKE_MIDDLEWARE, {optional: true}) - protected invokeMiddleware: InvokeMiddleware = () => false; - - constructor( - @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute, - @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams, - @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, - @inject(SequenceActions.SEND) public send: Send, - @inject(SequenceActions.REJECT) public reject: Reject, - @inject(MultiTenancyBindings.ACTION) - public multiTenancy: MultiTenancyAction, - ) {} - - async handle(context: RequestContext) { - try { - const {request, response} = context; - const finished = await this.invokeMiddleware(context); - if (finished) return; - - await this.multiTenancy(context); - const route = this.findRoute(request); - const args = await this.parseParams(request, route); - const result = await this.invoke(route, args); - this.send(response, result); - } catch (err) { - this.reject(context, err); - } - } -} +export class MySequence extends MiddlewareSequence {} From 687e7202b5575a5098db7feab9d1ad0dcb915da1 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 15 Jul 2020 10:34:49 -0700 Subject: [PATCH 7/9] feat(example-validation-app): refactor the code to use middleware --- examples/validation-app/src/application.ts | 5 +- .../middleware/validation-error.middleware.ts | 89 +++++++++++++++ examples/validation-app/src/sequence.ts | 105 +----------------- 3 files changed, 95 insertions(+), 104 deletions(-) create mode 100644 examples/validation-app/src/middleware/validation-error.middleware.ts diff --git a/examples/validation-app/src/application.ts b/examples/validation-app/src/application.ts index bcf0ead152a6..cf7e27e59297 100644 --- a/examples/validation-app/src/application.ts +++ b/examples/validation-app/src/application.ts @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import {BootMixin} from '@loopback/boot'; -import {ApplicationConfig} from '@loopback/core'; +import {ApplicationConfig, createBindingFromClass} from '@loopback/core'; import {RepositoryMixin} from '@loopback/repository'; import {RestApplication} from '@loopback/rest'; import { @@ -13,6 +13,7 @@ import { } from '@loopback/rest-explorer'; import {ServiceMixin} from '@loopback/service-proxy'; import path from 'path'; +import {ValidationErrorMiddlewareProvider} from './middleware/validation-error.middleware'; import {MySequence} from './sequence'; export {ApplicationConfig}; @@ -23,6 +24,8 @@ export class ValidationApplication extends BootMixin( constructor(options: ApplicationConfig = {}) { super(options); + this.add(createBindingFromClass(ValidationErrorMiddlewareProvider)); + // Set up the custom sequence this.sequence(MySequence); diff --git a/examples/validation-app/src/middleware/validation-error.middleware.ts b/examples/validation-app/src/middleware/validation-error.middleware.ts new file mode 100644 index 000000000000..d17760728b1a --- /dev/null +++ b/examples/validation-app/src/middleware/validation-error.middleware.ts @@ -0,0 +1,89 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/example-validation-app +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {bind, inject, Provider} from '@loopback/core'; +import { + asMiddleware, + ErrorWriterOptions, + HttpErrors, + LogError, + Middleware, + MiddlewareContext, + Response, + RestBindings, + RestMiddlewareGroups, +} from '@loopback/rest'; + +@bind( + asMiddleware({ + group: 'validationError', + upstreamGroups: RestMiddlewareGroups.SEND_RESPONSE, + downstreamGroups: RestMiddlewareGroups.CORS, + }), +) +export class ValidationErrorMiddlewareProvider implements Provider { + constructor( + @inject(RestBindings.SequenceActions.LOG_ERROR) + protected logError: LogError, + @inject(RestBindings.ERROR_WRITER_OPTIONS, {optional: true}) + protected errorWriterOptions?: ErrorWriterOptions, + ) {} + + async value() { + const middleware: Middleware = async (ctx, next) => { + try { + return await next(); + } catch (err) { + return this.handleError(ctx, err); + } + }; + return middleware; + } + + /** + * Handle errors + * If the request url is `/coffee-shops`, customize the error message. + * @param context + * @param err + */ + handleError( + context: MiddlewareContext, + err: HttpErrors.HttpError, + ): Response | undefined { + // 2. customize error for particular endpoint + if (context.request.url === '/coffee-shops') { + // if this is a validation error from the PATCH method, customize it + // for other validation errors, the default AJV error object will be sent + if (err.statusCode === 422 && context.request.method === 'PATCH') { + const customizedMessage = 'My customized validation error message'; + + let customizedProps = {}; + if (this.errorWriterOptions?.debug) { + customizedProps = {stack: err.stack}; + } + + // 3. Create a new error with customized properties + // you can change the status code here too + const errorData = { + statusCode: 422, + message: customizedMessage, + resolution: 'Contact your admin for troubleshooting.', + code: 'VALIDATION_FAILED', + ...customizedProps, + }; + + context.response.status(422).send(errorData); + + // 4. log the error using RestBindings.SequenceActions.LOG_ERROR + this.logError(err, err.statusCode, context.request); + + // The error was handled + return context.response; + } else { + throw err; + } + } + } +} diff --git a/examples/validation-app/src/sequence.ts b/examples/validation-app/src/sequence.ts index 76d22904de9b..8faa85602c21 100644 --- a/examples/validation-app/src/sequence.ts +++ b/examples/validation-app/src/sequence.ts @@ -3,107 +3,6 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {inject} from '@loopback/core'; -import { - FindRoute, - HttpErrors, - InvokeMethod, - InvokeMiddleware, - LogError, - ParseParams, - Reject, - RequestContext, - RestBindings, - Send, - SequenceHandler, -} from '@loopback/rest'; -import {ErrorWriterOptions} from 'strong-error-handler'; +import {MiddlewareSequence} from '@loopback/rest'; -const SequenceActions = RestBindings.SequenceActions; - -/** - * A few things to note on top of the generated sequence - * 1. inject RestBindings.SequenceActions.LOG_ERROR for logging error - * 2. customize error for particular endpoints - * 3. create a new error with customized properties - * 4. log the error using RestBindings.SequenceActions.LOG_ERROR - */ -export class MySequence implements SequenceHandler { - /** - * Optional invoker for registered middleware in a chain. - * To be injected via SequenceActions.INVOKE_MIDDLEWARE. - */ - @inject(SequenceActions.INVOKE_MIDDLEWARE, {optional: true}) - protected invokeMiddleware: InvokeMiddleware = () => false; - - // 1. inject RestBindings.SequenceActions.LOG_ERROR for logging error - // and RestBindings.ERROR_WRITER_OPTIONS for options - constructor( - @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute, - @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams, - @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, - @inject(SequenceActions.SEND) public send: Send, - @inject(SequenceActions.REJECT) public reject: Reject, - @inject(RestBindings.SequenceActions.LOG_ERROR) - protected logError: LogError, - @inject(RestBindings.ERROR_WRITER_OPTIONS, {optional: true}) - protected errorWriterOptions?: ErrorWriterOptions, - ) {} - - async handle(context: RequestContext) { - try { - const {request, response} = context; - const finished = await this.invokeMiddleware(context); - if (finished) return; - const route = this.findRoute(request); - const args = await this.parseParams(request, route); - const result = await this.invoke(route, args); - this.send(response, result); - } catch (err) { - this.handleError(context, err); - } - } - - /** - * Handle errors - * If the request url is `/coffee-shops`, customize the error message. - * @param context - * @param err - */ - handleError(context: RequestContext, err: HttpErrors.HttpError) { - // 2. customize error for particular endpoint - if (context.request.url === '/coffee-shops') { - // if this is a validation error from the PATCH method, customize it - // for other validation errors, the default AJV error object will be sent - if (err.statusCode === 422 && context.request.method === 'PATCH') { - const customizedMessage = 'My customized validation error message'; - - let customizedProps = {}; - if (this.errorWriterOptions?.debug) { - customizedProps = {stack: err.stack}; - } - - // 3. Create a new error with customized properties - // you can change the status code here too - const errorData = { - statusCode: 422, - message: customizedMessage, - resolution: 'Contact your admin for troubleshooting.', - code: 'VALIDATION_FAILED', - ...customizedProps, - }; - - context.response.status(422).send(errorData); - - // 4. log the error using RestBindings.SequenceActions.LOG_ERROR - this.logError(err, err.statusCode, context.request); - - // The error was handled - return; - } - } - - // Otherwise fall back to the default error handler - this.reject(context, err); - } -} +export class MySequence extends MiddlewareSequence {} From 7d49953cbc737c6c5afedd26dbf1dc1218cd4966 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 15 Jul 2020 10:35:30 -0700 Subject: [PATCH 8/9] feat: switch to middleware based sequence for examples --- .../access-control-migration/src/sequence.ts | 66 +------------------ examples/express-composition/src/sequence.ts | 46 +------------ examples/file-transfer/src/sequence.ts | 46 +------------ examples/lb3-application/src/sequence.ts | 46 +------------ examples/rest-crud/src/sequence.ts | 46 +------------ examples/soap-calculator/src/sequence.ts | 46 +------------ examples/todo-jwt/src/sequence.ts | 66 +------------------ examples/todo-list/src/sequence.ts | 46 +------------ examples/todo/src/application.ts | 3 +- 9 files changed, 17 insertions(+), 394 deletions(-) diff --git a/examples/access-control-migration/src/sequence.ts b/examples/access-control-migration/src/sequence.ts index 31d2a5a552d3..d17f5e9fd00b 100644 --- a/examples/access-control-migration/src/sequence.ts +++ b/examples/access-control-migration/src/sequence.ts @@ -3,68 +3,6 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import { - AuthenticateFn, - AuthenticationBindings, - AUTHENTICATION_STRATEGY_NOT_FOUND, - USER_PROFILE_NOT_FOUND, -} from '@loopback/authentication'; -import {Context, inject} from '@loopback/core'; -import { - FindRoute, - InvokeMethod, - InvokeMiddleware, - ParseParams, - Reject, - RequestContext, - RestBindings, - Send, - SequenceHandler, -} from '@loopback/rest'; +import {MiddlewareSequence} from '@loopback/rest'; -const SequenceActions = RestBindings.SequenceActions; - -export class MySequence implements SequenceHandler { - /** - * Optional invoker for registered middleware in a chain. - * To be injected via SequenceActions.INVOKE_MIDDLEWARE. - */ - @inject(SequenceActions.INVOKE_MIDDLEWARE, {optional: true}) - protected invokeMiddleware: InvokeMiddleware = () => false; - - constructor( - @inject(RestBindings.Http.CONTEXT) public ctx: Context, - @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute, - @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams, - @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, - @inject(SequenceActions.SEND) public send: Send, - @inject(SequenceActions.REJECT) public reject: Reject, - @inject(AuthenticationBindings.AUTH_ACTION) - protected authenticateRequest: AuthenticateFn, - ) {} - - async handle(context: RequestContext) { - try { - const {request, response} = context; - const finished = await this.invokeMiddleware(context); - if (finished) return; - - const route = this.findRoute(request); - - //call authentication action - await this.authenticateRequest(request); - - const args = await this.parseParams(request, route); - const result = await this.invoke(route, args); - this.send(response, result); - } catch (error) { - if ( - error.code === AUTHENTICATION_STRATEGY_NOT_FOUND || - error.code === USER_PROFILE_NOT_FOUND - ) { - Object.assign(error, {statusCode: 401 /* Unauthorized */}); - } - this.reject(context, error); - } - } -} +export class MySequence extends MiddlewareSequence {} diff --git a/examples/express-composition/src/sequence.ts b/examples/express-composition/src/sequence.ts index ef3adaf25a6e..bae08eee74b8 100644 --- a/examples/express-composition/src/sequence.ts +++ b/examples/express-composition/src/sequence.ts @@ -3,48 +3,6 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {inject} from '@loopback/core'; -import { - FindRoute, - InvokeMethod, - InvokeMiddleware, - ParseParams, - Reject, - RequestContext, - RestBindings, - Send, - SequenceHandler, -} from '@loopback/rest'; +import {MiddlewareSequence} from '@loopback/rest'; -const SequenceActions = RestBindings.SequenceActions; - -export class MySequence implements SequenceHandler { - /** - * Optional invoker for registered middleware in a chain. - * To be injected via SequenceActions.INVOKE_MIDDLEWARE. - */ - @inject(SequenceActions.INVOKE_MIDDLEWARE, {optional: true}) - protected invokeMiddleware: InvokeMiddleware = () => false; - - constructor( - @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute, - @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams, - @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, - @inject(SequenceActions.SEND) public send: Send, - @inject(SequenceActions.REJECT) public reject: Reject, - ) {} - - async handle(context: RequestContext) { - try { - const {request, response} = context; - const finished = await this.invokeMiddleware(context); - if (finished) return; - const route = this.findRoute(request); - const args = await this.parseParams(request, route); - const result = await this.invoke(route, args); - this.send(response, result); - } catch (err) { - this.reject(context, err); - } - } -} +export class MySequence extends MiddlewareSequence {} diff --git a/examples/file-transfer/src/sequence.ts b/examples/file-transfer/src/sequence.ts index ccb39e3f8490..06d2c3eb2c8b 100644 --- a/examples/file-transfer/src/sequence.ts +++ b/examples/file-transfer/src/sequence.ts @@ -3,48 +3,6 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {inject} from '@loopback/core'; -import { - FindRoute, - InvokeMethod, - InvokeMiddleware, - ParseParams, - Reject, - RequestContext, - RestBindings, - Send, - SequenceHandler, -} from '@loopback/rest'; +import {MiddlewareSequence} from '@loopback/rest'; -const SequenceActions = RestBindings.SequenceActions; - -export class MySequence implements SequenceHandler { - /** - * Optional invoker for registered middleware in a chain. - * To be injected via SequenceActions.INVOKE_MIDDLEWARE. - */ - @inject(SequenceActions.INVOKE_MIDDLEWARE, {optional: true}) - protected invokeMiddleware: InvokeMiddleware = () => false; - - constructor( - @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute, - @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams, - @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, - @inject(SequenceActions.SEND) public send: Send, - @inject(SequenceActions.REJECT) public reject: Reject, - ) {} - - async handle(context: RequestContext) { - try { - const {request, response} = context; - const finished = await this.invokeMiddleware(context); - if (finished) return; - const route = this.findRoute(request); - const args = await this.parseParams(request, route); - const result = await this.invoke(route, args); - this.send(response, result); - } catch (err) { - this.reject(context, err); - } - } -} +export class MySequence extends MiddlewareSequence {} diff --git a/examples/lb3-application/src/sequence.ts b/examples/lb3-application/src/sequence.ts index 2b16903ac3f3..a00a84247d30 100644 --- a/examples/lb3-application/src/sequence.ts +++ b/examples/lb3-application/src/sequence.ts @@ -3,48 +3,6 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {inject} from '@loopback/core'; -import { - FindRoute, - InvokeMethod, - InvokeMiddleware, - ParseParams, - Reject, - RequestContext, - RestBindings, - Send, - SequenceHandler, -} from '@loopback/rest'; +import {MiddlewareSequence} from '@loopback/rest'; -const SequenceActions = RestBindings.SequenceActions; - -export class MySequence implements SequenceHandler { - /** - * Optional invoker for registered middleware in a chain. - * To be injected via SequenceActions.INVOKE_MIDDLEWARE. - */ - @inject(SequenceActions.INVOKE_MIDDLEWARE, {optional: true}) - protected invokeMiddleware: InvokeMiddleware = () => false; - - constructor( - @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute, - @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams, - @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, - @inject(SequenceActions.SEND) public send: Send, - @inject(SequenceActions.REJECT) public reject: Reject, - ) {} - - async handle(context: RequestContext) { - try { - const {request, response} = context; - const finished = await this.invokeMiddleware(context); - if (finished) return; - const route = this.findRoute(request); - const args = await this.parseParams(request, route); - const result = await this.invoke(route, args); - this.send(response, result); - } catch (err) { - this.reject(context, err); - } - } -} +export class MySequence extends MiddlewareSequence {} diff --git a/examples/rest-crud/src/sequence.ts b/examples/rest-crud/src/sequence.ts index 6da07a6c1511..1bd5171aa378 100644 --- a/examples/rest-crud/src/sequence.ts +++ b/examples/rest-crud/src/sequence.ts @@ -3,48 +3,6 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {inject} from '@loopback/core'; -import { - FindRoute, - InvokeMethod, - InvokeMiddleware, - ParseParams, - Reject, - RequestContext, - RestBindings, - Send, - SequenceHandler, -} from '@loopback/rest'; +import {MiddlewareSequence} from '@loopback/rest'; -const SequenceActions = RestBindings.SequenceActions; - -export class MySequence implements SequenceHandler { - /** - * Optional invoker for registered middleware in a chain. - * To be injected via SequenceActions.INVOKE_MIDDLEWARE. - */ - @inject(SequenceActions.INVOKE_MIDDLEWARE, {optional: true}) - protected invokeMiddleware: InvokeMiddleware = () => false; - - constructor( - @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute, - @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams, - @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, - @inject(SequenceActions.SEND) public send: Send, - @inject(SequenceActions.REJECT) public reject: Reject, - ) {} - - async handle(context: RequestContext) { - try { - const {request, response} = context; - const finished = await this.invokeMiddleware(context); - if (finished) return; - const route = this.findRoute(request); - const args = await this.parseParams(request, route); - const result = await this.invoke(route, args); - this.send(response, result); - } catch (err) { - this.reject(context, err); - } - } -} +export class MySequence extends MiddlewareSequence {} diff --git a/examples/soap-calculator/src/sequence.ts b/examples/soap-calculator/src/sequence.ts index b21da36501c3..1011bc88bf98 100644 --- a/examples/soap-calculator/src/sequence.ts +++ b/examples/soap-calculator/src/sequence.ts @@ -3,48 +3,6 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {inject} from '@loopback/core'; -import { - FindRoute, - InvokeMethod, - InvokeMiddleware, - ParseParams, - Reject, - RequestContext, - RestBindings, - Send, - SequenceHandler, -} from '@loopback/rest'; +import {MiddlewareSequence} from '@loopback/rest'; -const SequenceActions = RestBindings.SequenceActions; - -export class MySequence implements SequenceHandler { - /** - * Optional invoker for registered middleware in a chain. - * To be injected via SequenceActions.INVOKE_MIDDLEWARE. - */ - @inject(SequenceActions.INVOKE_MIDDLEWARE, {optional: true}) - protected invokeMiddleware: InvokeMiddleware = () => false; - - constructor( - @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute, - @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams, - @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, - @inject(SequenceActions.SEND) public send: Send, - @inject(SequenceActions.REJECT) public reject: Reject, - ) {} - - async handle(context: RequestContext) { - try { - const {request, response} = context; - const finished = await this.invokeMiddleware(context); - if (finished) return; - const route = this.findRoute(request); - const args = await this.parseParams(request, route); - const result = await this.invoke(route, args); - this.send(response, result); - } catch (err) { - this.reject(context, err); - } - } -} +export class MySequence extends MiddlewareSequence {} diff --git a/examples/todo-jwt/src/sequence.ts b/examples/todo-jwt/src/sequence.ts index a19f201cef99..30158dce771c 100644 --- a/examples/todo-jwt/src/sequence.ts +++ b/examples/todo-jwt/src/sequence.ts @@ -3,68 +3,6 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import { - AuthenticateFn, - AuthenticationBindings, - AUTHENTICATION_STRATEGY_NOT_FOUND, - USER_PROFILE_NOT_FOUND, -} from '@loopback/authentication'; -import {Context, inject} from '@loopback/core'; -import { - FindRoute, - InvokeMethod, - InvokeMiddleware, - ParseParams, - Reject, - RequestContext, - RestBindings, - Send, - SequenceHandler, -} from '@loopback/rest'; +import {MiddlewareSequence} from '@loopback/rest'; -const SequenceActions = RestBindings.SequenceActions; - -export class MySequence implements SequenceHandler { - /** - * Optional invoker for registered middleware in a chain. - * To be injected via SequenceActions.INVOKE_MIDDLEWARE. - */ - @inject(SequenceActions.INVOKE_MIDDLEWARE, {optional: true}) - protected invokeMiddleware: InvokeMiddleware = () => false; - - constructor( - @inject(RestBindings.Http.CONTEXT) public ctx: Context, - @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute, - @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams, - @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, - @inject(SequenceActions.SEND) public send: Send, - @inject(SequenceActions.REJECT) public reject: Reject, - @inject(AuthenticationBindings.AUTH_ACTION) - protected authenticateRequest: AuthenticateFn, - ) {} - - async handle(context: RequestContext) { - try { - const {request, response} = context; - const finished = await this.invokeMiddleware(context); - if (finished) return; - - const route = this.findRoute(request); - - await this.authenticateRequest(request); - const args = await this.parseParams(request, route); - const result = await this.invoke(route, args); - this.send(response, result); - } catch (error) { - // if error is coming from the JWT authentication extension - // make the statusCode 401 - if ( - error.code === AUTHENTICATION_STRATEGY_NOT_FOUND || - error.code === USER_PROFILE_NOT_FOUND - ) { - Object.assign(error, {statusCode: 401 /* Unauthorized */}); - } - this.reject(context, error); - } - } -} +export class MySequence extends MiddlewareSequence {} diff --git a/examples/todo-list/src/sequence.ts b/examples/todo-list/src/sequence.ts index 7de21d79a76d..0a7ff1d0bd79 100644 --- a/examples/todo-list/src/sequence.ts +++ b/examples/todo-list/src/sequence.ts @@ -3,48 +3,6 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {inject} from '@loopback/core'; -import { - FindRoute, - InvokeMethod, - InvokeMiddleware, - ParseParams, - Reject, - RequestContext, - RestBindings, - Send, - SequenceHandler, -} from '@loopback/rest'; +import {MiddlewareSequence} from '@loopback/rest'; -const SequenceActions = RestBindings.SequenceActions; - -export class MySequence implements SequenceHandler { - /** - * Optional invoker for registered middleware in a chain. - * To be injected via SequenceActions.INVOKE_MIDDLEWARE. - */ - @inject(SequenceActions.INVOKE_MIDDLEWARE, {optional: true}) - protected invokeMiddleware: InvokeMiddleware = () => false; - - constructor( - @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute, - @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams, - @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, - @inject(SequenceActions.SEND) public send: Send, - @inject(SequenceActions.REJECT) public reject: Reject, - ) {} - - async handle(context: RequestContext) { - try { - const {request, response} = context; - const finished = await this.invokeMiddleware(context); - if (finished) return; - const route = this.findRoute(request); - const args = await this.parseParams(request, route); - const result = await this.invoke(route, args); - this.send(response, result); - } catch (err) { - this.reject(context, err); - } - } -} +export class MySequence extends MiddlewareSequence {} diff --git a/examples/todo/src/application.ts b/examples/todo/src/application.ts index 18da73a63045..f514a10675eb 100644 --- a/examples/todo/src/application.ts +++ b/examples/todo/src/application.ts @@ -6,7 +6,7 @@ import {BootMixin} from '@loopback/boot'; import {ApplicationConfig} from '@loopback/core'; import {RepositoryMixin} from '@loopback/repository'; -import {Request, Response, RestApplication, RestTags} from '@loopback/rest'; +import {Request, Response, RestApplication} from '@loopback/rest'; import {RestExplorerComponent} from '@loopback/rest-explorer'; import {ServiceMixin} from '@loopback/service-proxy'; import morgan from 'morgan'; @@ -62,7 +62,6 @@ export class TodoListApplication extends BootMixin( this.expressMiddleware(morganFactory, defaultConfig, { injectConfiguration: 'watch', key: 'middleware.morgan', - chain: RestTags.REST_MIDDLEWARE_CHAIN, }); } } From 951408a5cf73572448a53ac26aaf5bc75ace8fe3 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 17 Jun 2020 14:28:13 -0700 Subject: [PATCH 9/9] feat: add docs for middleware-based sequence --- docs/site/Authentication-component-action.md | 6 + docs/site/Authentication-overview.md | 6 + docs/site/REST-action-sequence.md | 592 +++++++++++ docs/site/REST-middleware-sequence.md | 985 +++++++++++++++++++ docs/site/Sequence.md | 594 +---------- docs/site/imgs/cascading-middleware.png | Bin 0 -> 103114 bytes docs/site/imgs/middleware-sequence.png | Bin 0 -> 120367 bytes docs/site/sidebars/lb4_sidebar.yml | 8 + 8 files changed, 1619 insertions(+), 572 deletions(-) create mode 100644 docs/site/REST-action-sequence.md create mode 100644 docs/site/REST-middleware-sequence.md create mode 100644 docs/site/imgs/cascading-middleware.png create mode 100644 docs/site/imgs/middleware-sequence.png diff --git a/docs/site/Authentication-component-action.md b/docs/site/Authentication-component-action.md index 1675f53c8017..d70d3a3d0e75 100644 --- a/docs/site/Authentication-component-action.md +++ b/docs/site/Authentication-component-action.md @@ -6,6 +6,12 @@ sidebar: lb4_sidebar permalink: /doc/en/lb4/Authentication-component-action.html --- +{% include note.html content=" +This is not needed for [middleware-based +sequence](REST-middleware-sequence.md) as the authentication is enforced by a +middleware that's automatically discovered and added to the sequence. +" %} + ## Adding an Authentication Action to a Custom Sequence In a LoopBack 4 application with REST API endpoints, each request passes through diff --git a/docs/site/Authentication-overview.md b/docs/site/Authentication-overview.md index 800b80af91fa..572a84ae2c09 100644 --- a/docs/site/Authentication-overview.md +++ b/docs/site/Authentication-overview.md @@ -44,6 +44,12 @@ snippet: - Decorate the controller endpoint with `@authenticate()` and inject the user passed from the authentication layer. +{% include note.html content=" +For [middleware-based sequence](REST-middleware-sequence.md), there is no longer +needed to add the authenticate action as the authentication is enforced by a +middleware that's automatically discovered and added to the sequence. +" %} + The rest will be handled by the authentication component `@loopback/authentication`, which incorporates the authentication mechanism, and the JWT extension `@loopback/jwt-authentication`, which helps in implementing diff --git a/docs/site/REST-action-sequence.md b/docs/site/REST-action-sequence.md new file mode 100644 index 000000000000..65bdbb9feba4 --- /dev/null +++ b/docs/site/REST-action-sequence.md @@ -0,0 +1,592 @@ +--- +lang: en +title: 'Action based Sequence for REST Server' +keywords: LoopBack 4.0, LoopBack 4, Node.js, TypeScript, OpenAPI +sidebar: lb4_sidebar +permalink: /doc/en/lb4/REST-action-based-sequence.html +--- + +{% include warning.html content="Action-based sequence is now being phased out. +Please use [middleware-based sequence](REST-middleware-sequence.md)."%} + +## What is a Sequence? + +A `Sequence` is a stateless grouping of [Actions](#actions) that control how a +`Server` responds to requests. + +The contract of a `Sequence` is simple: it must produce a response to a request. +Creating your own `Sequence` gives you full control over how your `Server` +instances handle requests and responses. The `DefaultSequence` looks like this: + +```ts +export class DefaultSequence implements SequenceHandler { + /** + * Optional invoker for registered middleware in a chain. + * To be injected via SequenceActions.INVOKE_MIDDLEWARE. + */ + @inject(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( + @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute, + @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams, + @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, + @inject(SequenceActions.SEND) public send: Send, + @inject(SequenceActions.REJECT) public reject: Reject, + ) {} + + /** + * 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 { + 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); + } + } +} +``` + +## Elements + +In the example above, `route`, `params`, and `result` are all Elements. When +building sequences, you use LoopBack Elements to respond to a request: + +- [`InvokeMiddleware`](https://loopback.io/doc/en/lb4/apidocs.express.invokemiddleware.html) +- [`FindRoute`](https://loopback.io/doc/en/lb4/apidocs.rest.findroute.html) +- [`Request`](http://apidocs.strongloop.com/loopback-next/) - (TBD) missing API + docs link +- [`Response`](http://apidocs.strongloop.com/loopback-next/) - (TBD) missing API + docs link +- [`OperationRetVal`](https://loopback.io/doc/en/lb4/apidocs.rest.operationretval.html) +- [`ParseParams`](https://loopback.io/doc/en/lb4/apidocs.rest.parseparams.html) +- [`OpenAPISpec`](https://loopback.io/doc/en/lb4/apidocs.openapi-v3.openapispec.html) + +## Actions + +Actions are JavaScript functions that only accept or return `Elements`. Since +the input of one action (an Element) is the output of another action (Element) +you can easily compose them. Below is an example that uses several built-in +Actions: + +```ts +class MySequence extends DefaultSequence { + async handle(context: RequestContext) { + try { + // Invoke registered Express middleware + const finished = await this.invokeMiddleware(context); + if (finished) { + // The response been produced by the middleware chain + return; + } + // findRoute() produces an element + const route = this.findRoute(context.request); + // parseParams() uses the route element and produces the params element + const params = await this.parseParams(context.request, route); + // invoke() uses both the route and params elements to produce the result (OperationRetVal) element + const result = await this.invoke(route, params); + // send() uses the result element + this.send(context.response, result); + } catch (error) { + this.reject(context, error); + } + } +} +``` + +{% include warning.html content="Starting from v4.0.0 of `@loopback/rest`. The +sequence adds an `InvokeMiddleware` action for CORS and OpenAPI spec endpoints +as well as other middleware. See [Middleware](Middleware.md) and +[Express Middleware](Express-middleware.md) for more details. For applications +generated using old version of `lb4`, the `src/sequence.ts` needs to be manually +updated with the code above." %} + +## Custom Sequences + +Most use cases can be accomplished with `DefaultSequence` or by slightly +customizing it. When an app is generated by the command `lb4 app`, a sequence +file extending `DefaultSequence` at `src/sequence.ts` is already generated and +bound for you so that you can easily customize it. + +Here is an example where the application logs out a message before and after a +request is handled: + +```ts +import {DefaultSequence, Request, Response} from '@loopback/rest'; + +class MySequence extends DefaultSequence { + log(msg: string) { + console.log(msg); + } + async handle(context: RequestContext) { + this.log('before request'); + await super.handle(context); + this.log('after request'); + } +} +``` + +In order for LoopBack to use your custom sequence, you must register it before +starting your `Application`: + +```js +import {RestApplication} from '@loopback/rest'; + +const app = new RestApplication(); +app.sequence(MySequencce); + +app.start(); +``` + +## Advanced topics + +### Customizing Sequence Actions + +There might be scenarios where the default sequence _ordering_ is not something +you want to change, but rather the individual actions that the sequence will +execute. + +To do this, you'll need to override one or more of the sequence action bindings +used by the `RestServer`, under the `RestBindings.SequenceActions` constants. + +As an example, we'll implement a custom sequence action to replace the default +"send" action. This action is responsible for returning the response from a +controller to the client making the request. + +To do this, we'll register a custom send action by binding a +[Provider](https://loopback.io/doc/en/lb4/apidocs.context.provider.html) to the +`RestBindings.SequenceActions.SEND` key. + +First, let's create our `CustomSendProvider` class, which will provide the send +function upon injection. + +{% include code-caption.html content="/src/providers/custom-send.provider.ts" %} +**custom-send.provider.ts** + +```ts +import {Send, Response} from '@loopback/rest'; +import {Provider, BoundValue, inject} from '@loopback/core'; +import {writeResultToResponse, RestBindings, Request} from '@loopback/rest'; + +// Note: This is an example class; we do not provide this for you. +import {Formatter} from '../utils'; + +export class CustomSendProvider implements Provider { + // In this example, the injection key for formatter is simple + constructor( + @inject('utils.formatter') public formatter: Formatter, + @inject(RestBindings.Http.REQUEST) public request: Request, + ) {} + + value() { + // Use the lambda syntax to preserve the "this" scope for future calls! + return (response: Response, result: OperationRetval) => { + this.action(response, result); + }; + } + /** + * Use the mimeType given in the request's Accept header to convert + * the response object! + * @param response - The response object used to reply to the client. + * @param result - The result of the operation carried out by the controller's + * handling function. + */ + action(response: Response, result: OperationRetval) { + if (result) { + // Currently, the headers interface doesn't allow arbitrary string keys! + const headers = (this.request.headers as any) || {}; + const header = headers.accept || 'application/json'; + const formattedResult = this.formatter.convertToMimeType(result, header); + response.setHeader('Content-Type', header); + response.end(formattedResult); + } else { + response.end(); + } + } +} +``` + +Our custom provider will automatically read the `Accept` header from the request +context, and then transform the result object so that it matches the specified +MIME type. + +Next, in our application class, we'll inject this provider on the +`RestBindings.SequenceActions.SEND` key. + +{% include code-caption.html content="/src/application.ts" %} + +```ts +import {RestApplication, RestBindings} from '@loopback/rest'; +import { + RepositoryMixin, + Class, + Repository, + juggler, +} from '@loopback/repository'; +import {CustomSendProvider} from './providers/custom-send.provider'; +import {Formatter} from './utils'; +import {BindingScope} from '@loopback/core'; + +export class YourApp extends RepositoryMixin(RestApplication) { + constructor() { + super(); + // Assume your controller setup and other items are in here as well. + this.bind('utils.formatter') + .toClass(Formatter) + .inScope(BindingScope.SINGLETON); + this.bind(RestBindings.SequenceActions.SEND).toProvider(CustomSendProvider); + } +} +``` + +As a result, whenever the send action of the +[`DefaultSequence`](https://loopback.io/doc/en/lb4/apidocs.rest.defaultsequence.html) +is called, it will make use of your function instead! You can use this approach +to override any of the actions listed under the `RestBindings.SequenceActions` +namespace. + +### Query string parameters and path parameters + +OAI 3.0.x describes the data from a request’s header, query and path in an +operation specification’s parameters property. In a Controller method, such an +argument is typically decorated by @param(). We've made multiple shortcuts +available to the `@param()` decorator in the form of +`@param..`. Using this notation, path +parameters can be described as `@param.path.string`. Here is an example of a +controller method which retrieves a Note model instance by obtaining the `id` +from the path object. + +```ts +@get('/notes/{id}', { + responses: { + '200': { + description: 'Note model instance', + content: { + 'application/json': { + schema: getModelSchemaRef(Note, {includeRelations: true}), + }, + }, + }, + }, +}) +async findById( + @param.path.string('id') id: string, + @param.filter(Note, {exclude: 'where'}) filter?: FilterExcludingWhere +): Promise { + return this.noteRepository.findById(id, filter); +} +``` + +(Notice: the filter for `findById()` method only supports the `include` clause +for now.) + +You can also specify a parameter which is an object value encoded as a JSON +string or in multiple nested keys. For a JSON string, a sample value would be +`location={"lang": 23.414, "lat": -98.1515}`. For the same `location` object, it +can also be represented as `location[lang]=23.414&location[lat]=-98.1515`. Here +is the equivalent usage for `@param.query.object()` decorator. It takes in the +name of the parameter and an optional schema or reference object for it. + +```ts +@param.query.object('location', { + type: 'object', + properties: {lat: {type: 'number', format: 'float'}, long: {type: 'number', format: 'float'}}, +}) +``` + +The parameters are retrieved as the result of `parseParams` Sequence action. +Please note that deeply nested properties are not officially supported by OAS +yet and is tracked by +[OAI/OpenAPI-Specification#1706](https://github.com/OAI/OpenAPI-Specification/issues/1706). +Therefore, our REST API Explorer does not allow users to provide values for such +parameters and unfortunately has no visible indication of that. This problem is +tracked and discussed in +[swagger-api/swagger-js#1385](https://github.com/swagger-api/swagger-js/issues/1385). + +### Parsing Requests + +Parsing and validating arguments from the request url, headers, and body. See +page [Parsing requests](Parsing-requests.md). + +### Invoking controller methods + +The `invoke` sequence action simply takes the parsed request parameters from the +`parseParams` action along with non-decorated arguments, calls the corresponding +controller method or route handler method, and returns the value from it. The +default implementation of +[invoke](https://github.com/strongloop/loopback-next/blob/6bafa0774662991199090219913c3dc77ad5b149/packages/rest/src/providers/invoke-method.provider.ts) +action calls the handler function for the route with the request specific +context and the arguments for the function. It is important to note that +controller methods use `invokeMethod` from `@loopback/core` and can be used with +global and custom interceptors. See +[Interceptor docs](Interceptors.md#use-invokemethod-to-apply-interceptors) for +more details. The request flow for two route flavours is explained below. + +For controller methods: + +- A controller instance is instantiated from the context. As part of the + instantiation, constructor and property dependencies are injected. The + appropriate controller method is invoked via the chain of interceptors. +- Arguments decorated with `@param` are resolved using data parsed from the + request. Arguments decorated with `@inject` are resolved from the context. + Arguments with no decorators are set to undefined, which is replaced by the + argument default value if it's provided. + +For route handlers, the handler function is invoked via the chain of +interceptors. The array of method arguments is constructed using OpenAPI spec +provided at handler registration time (either via `.api()` for full schema or +`.route()` for individual route registration). + +### Writing the response + +The +[send](https://github.com/strongloop/loopback-next/blob/6bafa0774662991199090219913c3dc77ad5b149/packages/rest/src/providers/send.provider.ts) +sequence action is responsible for writing the result of the `invoke` action to +the HTTP response object. The default sequence calls send with (transformed) +data. Under the hood, send performs all steps required to send back the +response, from content-negotiation to serialization of the response body. In +Express, the handler is responsible for setting response status code, headers +and writing the response body. In LoopBack, controller methods and route +handlers return data describing the response and it's the responsibility of the +Sequence to send that data back to the client. This design makes it easier to +transform the response before it is sent. + +LoopBack 4 does not yet provide first-class support for streaming responses, see +[Issue#2230](https://github.com/strongloop/loopback-next/issues/2230). As a +short-term workaround, controller methods are allowed to send the response +directly, effectively bypassing send action. The default implementation of send +is prepared to handle this case +[here](https://github.com/strongloop/loopback-next/blob/bf07ff959a1f90577849b61221b292d3127696d6/packages/rest/src/writer.ts#L22-L26). + +### Handling errors + +There are many reasons why the application may not be able to handle an incoming +request: + +- The requested endpoint (method + URL path) was not found. +- Parameters provided by the client were not valid. +- A backend database or a service cannot be reached. +- The response object cannot be converted to JSON because of cyclic + dependencies. +- A programmer made a mistake and a `TypeError` is thrown by the runtime. +- And so on. + +In the Sequence implementation described above, all errors are handled by a +single `catch` block at the end of the sequence, using the Sequence Action +called `reject`. + +The default implementation of `reject` does the following steps: + +- Call + [strong-error-handler](https://github.com/strongloop/strong-error-handler) to + send back an HTTP response describing the error. +- Log the error to `stderr` if the status code was 5xx (an internal server + error, not a bad request). + +To prevent the application from leaking sensitive information like filesystem +paths and server addresses, the error handler is configured to hide error +details. + +- For 5xx errors, the output contains only the status code and the status name + from the HTTP specification. For example: + + ```json + { + "error": { + "statusCode": 500, + "message": "Internal Server Error" + } + } + ``` + +- For 4xx errors, the output contains the full error message (`error.message`) + and the contents of the `details` property (`error.details`) that + `ValidationError` typically uses to provide machine-readable details about + validation problems. It also includes `error.code` to allow a machine-readable + error code to be passed through which could be used, for example, for + translation. + + ```json + { + "error": { + "statusCode": 422, + "name": "Unprocessable Entity", + "message": "Missing required fields", + "code": "MISSING_REQUIRED_FIELDS" + } + } + ``` + +During development and testing, it may be useful to see all error details in the +HTTP response returned by the server. This behavior can be enabled by enabling +the `debug` flag in error-handler configuration as shown in the code example +below. See strong-error-handler +[docs](https://github.com/strongloop/strong-error-handler#options) for a list of +all available options. + +```ts +app.bind(RestBindings.ERROR_WRITER_OPTIONS).to({debug: true}); +``` + +An example error message when the debug mode is enabled: + +```json +{ + "error": { + "statusCode": 500, + "name": "Error", + "message": "ENOENT: no such file or directory, open '/etc/passwords'", + "errno": -2, + "syscall": "open", + "code": "ENOENT", + "path": "/etc/passwords", + "stack": "Error: a test error message\n at Object.openSync (fs.js:434:3)\n at Object.readFileSync (fs.js:339:35)" + } +} +``` + +### Keeping your Sequences + +For most use cases, the +[default](https://github.com/strongloop/loopback-next/blob/6bafa0774662991199090219913c3dc77ad5b149/packages/rest/src/sequence.ts) +sequence supplied with LoopBack 4 applications is good enough for +request-response handling pipeline. Check out +[Custom Sequences](#custom-sequences) on how to extend it and implement custom +actions. + +## Working with Express middleware + +{% include warning.html content="First-class support for Express middleware has +been added to LoopBack since v4.0.0 of `@loopback/rest`. Please refer to +[Using Express Middleware](Express-middleware.md). The following information +only applies to earlier versions of `@loopback/rest`." %} + +Under the hood, LoopBack leverages [Express](https://expressjs.com) framework +and its concept of middleware. To avoid common pitfalls, it is not possible to +mount Express middleware directly on a LoopBack application. Instead, LoopBack +provides and enforces a higher-level structure. + +In a typical Express application, there are four kinds of middleware invoked in +the following order: + +1. Request-preprocessing middleware like + [cors](https://www.npmjs.com/package/cors) or + [body-parser](https://www.npmjs.com/package/body-parser). +2. Route handlers handling requests and producing responses. +3. Middleware serving static assets (files). +4. Error handling middleware. + +In LoopBack, we handle the request in the following steps: + +1. The built-in request-preprocessing middleware is invoked. +2. The registered Sequence is started. The default implementation of `findRoute` + and `invoke` actions will try to match the incoming request against the + following resources: + 1. Native LoopBack routes (controller methods, route handlers). + 2. External Express routes (registered via `mountExpressRouter` API) + 3. Static assets +3. Errors are handled by the Sequence using `reject` action. + +Let's see how different kinds of Express middleware can be mapped to LoopBack +concepts: + +### Request-preprocessing middleware + +At the moment, LoopBack does not provide API for mounting arbitrary middleware, +we are discussing this feature in issues +[#1293](https://github.com/strongloop/loopback-next/issues/1293) and +[#2035](https://github.com/strongloop/loopback-next/issues/2035). Please up-vote +them if you are interested in using Express middleware in LoopBack applications. + +All applications come with [cors](https://www.npmjs.com/package/cors) enabled, +this middleware can be configured via RestServer options - see +[Customize CORS](Server.md#customize-cors). + +While it is not possible to add additional middleware to a LoopBack application, +it is possible to mount the entire LoopBack application as component of a parent +top-level Express application where you can add arbitrary middleware as needed. +You can find more details about this approach in +[Creating an Express Application with LoopBack REST API](express-with-lb4-rest-tutorial.md) + +### Route handlers + +In Express, a route handler is a middleware function that serves the response +and does not call `next()`. Handlers can be registered using APIs like +`app.get()`, `app.post()`, but also a more generic `app.use()`. + +In LoopBack, we typically use [Controllers](Controllers.md) and +[Route handlers](Routes.md) to implement request handling logic. + +To support interoperability with Express, it is also possible to take an Express +Router instance and add it to a LoopBack application as an external router - see +[Mounting an Express Router](Routes.md#mounting-an-express-router). This way it +is possible to implement server endpoints using Express APIs. + +### Static files + +LoopBack provides native API for registering static assets as described in +[Serve static files](Application.md#serve-static-files). Under the hood, static +assets are served by [serve-static](https://www.npmjs.com/package/serve-static) +middleware from Express. + +The main difference between LoopBack and vanilla Express applications: LoopBack +ensures that static-asset middleware is always invoked as the last one, only +when no other route handled the request. This is important for performance +reasons to avoid costly filesystem calls. + +### Error handling middleware + +In Express, errors are handled by a special form of middleware, one that's +accepting four arguments: `err`, `request`, `response`, `next`. It's up to the +application developer to ensure that error handler is registered as the last +middleware in the chain, otherwise not all errors may be routed to it. + +In LoopBack, we use async functions instead of callbacks and thus can use simple +`try`/`catch` flow to receive both sync and async errors from individual +sequence actions. A typical Sequence implementation then passes these errors to +the Sequence action `reject`. + +You can learn more about error handling in +[Handling errors](Sequence.md#handling-errors). diff --git a/docs/site/REST-middleware-sequence.md b/docs/site/REST-middleware-sequence.md new file mode 100644 index 000000000000..f75566bac377 --- /dev/null +++ b/docs/site/REST-middleware-sequence.md @@ -0,0 +1,985 @@ +--- +lang: en +title: 'Middleware-based Sequence for REST Server' +keywords: LoopBack 4.0, LoopBack 4, Node.js, TypeScript, OpenAPI, Middleware +sidebar: lb4_sidebar +permalink: /doc/en/lb4/REST-middleware-sequence.html +--- + +## What is a Sequence? + +A `Sequence` is a series of steps to control how a specific type of `Server` +responds to incoming requests. Each types of servers, such as RestServer, +GraphQLServer, GRPCServer, and WebSocketServer, will have its own flavor of +sequence. The sequence represents the pipeline for inbound connections. + +The contract of a `Sequence` is simple: it must produce a response for a +request. The signature will vary by server types. + +Each server type has a default sequence. It's also possible to create your own +`Sequence` to have full control over how your `Server` instances handle requests +and responses. + +This page describes the middleware-based sequence for REST server. + +The `handle` method receives an instance of `RequestContext`, which is a +subclass of `Context` that wraps the `Request` and `Response` objects from the +underlying Express server. + +## Use the sequence for your REST Application + +When a LoopBack application is scaffolded using `lb4 app` command, a +`MySequence` class is generated in `src/sequence.ts`. + +```ts +import {MiddlewareSequence} from '@loopback/rest'; + +export class MySequence extends MiddlewareSequence {} +``` + +`MySequence` is then used by the `RestApplication` in `src/application.ts`: + +```ts +import {BootMixin} from '@loopback/boot'; +import {ApplicationConfig} from '@loopback/core'; +import {RepositoryMixin} from '@loopback/repository'; +import {RestApplication} from '@loopback/rest'; +import {ServiceMixin} from '@loopback/service-proxy'; +import {MySequence} from './sequence'; + +export {ApplicationConfig}; + +export class TodoListApplication extends BootMixin( + ServiceMixin(RepositoryMixin(RestApplication)), +) { + constructor(options: ApplicationConfig = {}) { + super(options); + + // Set up the custom sequence + this.sequence(MySequence); + + // ... + } +} +``` + +## The default sequence + +Since version 6.0.0 of `@loopback/rest`, we have switched to a middleware-based +sequence as the default for flexibility, composability, and consistency. The +sequence itself is basically a named middleware chain. Each middleware serves as +an action within the sequence. The `handle` function executes registered +middleware in cascading fashion. + +```ts +/** + * A sequence implementation using middleware chains + */ +export class MiddlewareSequence implements SequenceHandler { + static defaultOptions: InvokeMiddlewareOptions = { + chain: 'middlewareChain.rest', + 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. + 'sendResponse', + + // default + 'cors', + 'apiSpec', + + // default + 'middleware', + + // rest + 'findRoute', + + // authentication + 'authentication', + + // rest + 'parseParams', + 'invokeMethod', + ], + }; + + /** + * Constructor: Injects `InvokeMiddleware` and `InvokeMiddlewareOptions` + * + * @param invokeMiddleware - invoker for registered middleware in a chain. + * To be injected via SequenceActions.INVOKE_MIDDLEWARE. + */ + constructor( + @inject(SequenceActions.INVOKE_MIDDLEWARE) + readonly invokeMiddleware: InvokeMiddleware, + @config() + readonly options: InvokeMiddlewareOptions = MiddlewareSequence.defaultOptions, + ) {} + + /** + * Runs the default sequence. Given a handler context (request and response), + * running the sequence will produce a response or an error. + * + * @param context - The request context: HTTP request and response objects, + * per-request IoC container and more. + */ + async handle(context: RequestContext): Promise { + debug( + 'Invoking middleware chain %s with groups %s', + this.options.chain, + this.options.orderedGroups, + ); + await this.invokeMiddleware(context, this.options); + } +} +``` + +![middleware-sequence](imgs/middleware-sequence.png) + +## Middleware as actions + +The middleware function is responsible for processing HTTP requests and +responses. It typically includes the following logic. + +1. Process the request from the server or upstream middleware with one of the + following outcomes: + + - Reject the request by throwing an error if the request is invalid + + ```ts + import {Middleware} from '@loopback/rest'; + const middleware = async (ctx, next) => { + // validate input + throw new Error('invalid input'); + }; + ``` + + - Produce a response by itself, such as from the cache + + ```ts + import {Middleware} from '@loopback/rest'; + const middleware = async (ctx, next) => { + // Find the response from cache + const cachedResponse = {}; + return cachedResponse; + }; + ``` + + - Proceed by calling `await next()` to invoke downstream middleware. When + `await next()` returns, it goes to step 2. If an error thrown from + `await next()`, step 3 handles the error. + + ```ts + import {Middleware} from '@loopback/rest'; + const middleware = async (ctx, next) => { + const result = await next(); + return result; + }; + ``` + +2. Process the response from downstream middleware or the target operation with + one the following outcomes: + + - Reject the response by throwing an error + + ```ts + import {Middleware} from '@loopback/rest'; + const middleware = async (ctx, next) => { + const result = await next(); + // validate result + throw new Error('...'); + }; + ``` + + - Transform the response to a different value + + ```ts + import {Middleware} from '@loopback/rest'; + const middleware = async (ctx, next) => { + const result = await next(); + return {data: result}; + }; + ``` + + - Return the response to upstream middleware + + ```ts + import {Middleware} from '@loopback/rest'; + const middleware = async (ctx, next) => { + const result = await next(); + return result; + }; + ``` + +3. Catch the error thrown from `await next()`. If the `catch` block does not + exist, the error will be bubbled up to upstream middleware. + + ```ts + import {Middleware} from '@loopback/rest'; + const middleware = async (ctx, next) => { + try { + const result = await next(); + return result; + } catch (err) { + // handle err + // either return a new value or throw an error + throw err; + } + }; + ``` + +![cascading-middleware](imgs/cascading-middleware.png) + +Default sequence executes these groups of middleware in order: + +- `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. + +## Migrate from legacy sequence + +The `Sequence` generated before `@loopback/rest@6.0.0` comes with hard-coded +actions as follows: + +```ts +import {inject} from '@loopback/core'; +import { + FindRoute, + InvokeMethod, + InvokeMiddleware, + ParseParams, + Reject, + RequestContext, + RestBindings, + Send, + SequenceHandler, +} from '@loopback/rest'; + +const SequenceActions = RestBindings.SequenceActions; + +export class MySequence implements SequenceHandler { + /** + * Optional invoker for registered middleware in a chain. + * To be injected via SequenceActions.INVOKE_MIDDLEWARE. + */ + @inject(SequenceActions.INVOKE_MIDDLEWARE, {optional: true}) + protected invokeMiddleware: InvokeMiddleware = () => false; + + constructor( + @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute, + @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams, + @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, + @inject(SequenceActions.SEND) public send: Send, + @inject(SequenceActions.REJECT) public reject: Reject, + ) {} + + async handle(context: RequestContext) { + try { + const {request, response} = context; + const finished = await this.invokeMiddleware(context); + if (finished) return; + const route = this.findRoute(request); + const args = await this.parseParams(request, route); + const result = await this.invoke(route, args); + this.send(response, result); + } catch (err) { + this.reject(context, err); + } + } +} +``` + +The legacy `Sequence` is now deprecated but it should continue to work without +any changes. + +If you have never customized `src/sequence.ts` generated by `lb4 app`, or you +have only modified the code by adding +[`authenticate`](Authentication-component-action.md) action per our docs, you +can simply migrate to the middleware based sequence by replacing the content of +`src/sequence.ts` with the code below. + +```ts +import {MiddlewareSequence} from '@loopback/rest'; + +export class MySequence extends MiddlewareSequence {} +``` + +If you have other actions in your sequence, you'll have to write a middleware to +wrap your action and register it with the middleware chain for the +middleware-based sequence. For example, the `invokeMethod` action is a function +with the following signature: + +```ts +export type InvokeMethod = ( + route: RouteEntry, + args: OperationArgs, +) => Promise; +``` + +As part of the legacy action-based sequence, the `invoke` function takes `route` +and `args` as input parameters and returns `result`. + +```ts +const route = this.findRoute(request); +const args = await this.parseParams(request, route); +const result = await this.invoke(route, args); +this.send(response, result); +``` + +The corresponding middleware for `invokeMethod` looks like the following. It now +uses the `context` to retrieve `route` and `params` instead. The return value is +also bound to `RestBindings.Operation.RETURN_VALUE` to expose to other +middleware in the chain. + +```ts +async (ctx, next) => { + // Locate or inject input parameters from the request context + const route: RouteEntry = await ctx.get(RestBindings.Operation.ROUTE); + const params: OperationArgs = await ctx.get(RestBindings.Operation.PARAMS); + const retVal = await this.invokeMethod(route, params); + // Bind the return value to the request context + ctx.bind(RestBindings.Operation.RETURN_VALUE).to(retVal); + return retVal; +}; +``` + +## Extend the middleware sequence + +The middleware based sequence is a middleware chain that accepts contribution of +middleware against `RestTags.REST_MIDDLEWARE_CHAIN`. + +### Add middleware to the chain + +LoopBack middleware (including Express middleware) can be added to the sequence. +See +[Middleware](Middleware.md#register-middleware-to-be-executed-by-invokemiddleware-actions) +for more details. + +### Sort middleware by groups + +The middleware for the sequence are executed in the order of groups in a +cascading style. The order of groups is determined by two factors: + +1. The relative order specified for a middleware binding. + + - upstreamGroups: An array of group names that should be upstream to this + middleware + + ```ts + @bind( + asMiddleware({ + chain: RestTags.REST_MIDDLEWARE_CHAIN, + group: 'authentication', + upstreamGroups: ['cors', 'findRoute'], + }), + ) + export class AuthenticationMiddlewareProvider + implements Provider {} + ``` + + - downstreamGroups: An array of group names that should be downstream to + this middleware + + ```ts + @bind( + asMiddleware({ + group: 'sendResponse', + downstreamGroups: ['cors', 'invokeMethod'], + chain: RestTags.REST_MIDDLEWARE_CHAIN, + }), + ) + export class SendResponseMiddlewareProvider + implements Provider {} + ``` + +2. The overall order of groups for the sequence + +- It can be set as in `InvokeMiddlewareOptions`, which is the configuration for + the middleware-based sequence. For example: + + ```ts + import {BootMixin} from '@loopback/boot'; + import {ApplicationConfig} from '@loopback/core'; + import {RepositoryMixin} from '@loopback/repository'; + import { + InvokeMiddlewareOptions, + Request, + Response, + RestApplication, + RestTags, + } from '@loopback/rest'; + import {RestExplorerComponent} from '@loopback/rest-explorer'; + import {ServiceMixin} from '@loopback/service-proxy'; + import {MySequence} from './sequence'; + + export class TodoListApplication extends BootMixin( + ServiceMixin(RepositoryMixin(RestApplication)), + ) { + constructor(options: ApplicationConfig = {}) { + super(options); + + const middlewareOptions: InvokeMiddlewareOptions = { + chain: 'middlewareChain.rest', + 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. + + 'sendResponse', + + // default + 'cors', + 'apiSpec', + + // default + 'middleware', + + // rest + 'findRoute', + + // authentication + 'authentication', + + // rest + 'parseParams', + 'invokeMethod', + ], + }; + this.configure(RestBindings.SEQUENCE).to(middlewareOptions); // Set up the custom sequence + this.sequence(MySequence); + } + } + ``` + +When each middleware is added to the chain, its settings of `downstreamGroups` +and `upstreamGroups` are honored in conjunction with the overall order. If there +is a conflict, an error will be thrown. + +Here are some examples: + +1. Form a middleware chain with the execution order of + `sendResponse => group2 => cors => group1`: + +``` +orderedGroups: ['sendResponse', 'cors'] +middleware 1: + - group: 'group1' + - upstreamGroups: ['cors'] + +middleware 2: + - group: 'group2' + - downstreamGroups: ['cors'] +``` + +2. a middleware chain with the execution order of + `sendResponse => group2 => cors => group1`: + +``` +orderedGroups: ['sendResponse', 'cors'] +middleware 1: + - group: 'group1' + - upstreamGroups: ['group2', 'cors'] + +middleware 2: + - group: 'group2' + - downstreamGroups: ['cors'] +``` + +2. a middleware chain with an invalid order as `group1` and `group2` creates a + circular dependency: + +``` +orderedGroups: ['sendResponse', 'cors'] +middleware 1: + - group: 'group1' + - upstreamGroups: ['group2', 'cors'] + +middleware 2: + - group: 'group2' + - downstreamGroups: ['group1'] +``` + +## Custom Sequences + +Most use cases can be accomplished with `MiddlewareSequence`. When an app is +generated by the command `lb4 app`, a sequence file extending +`MiddlewareSequence` at `src/sequence.ts` is already generated and bound for you +so that you can easily customize it. + +A `Sequence` class for REST server is required to implement the +`SequenceHandler` interface: + +```ts +import {RequestContext} from '@loopback/rest'; +/** + * 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; +} +``` + +Here is an example where the application logs out a message before and after a +request is handled: + +```ts +import {MiddlewareSequence, Request, Response} from '@loopback/rest'; + +class MySequence extends MiddlewareSequence { + log(msg: string) { + console.log(msg); + } + async handle(context: RequestContext) { + this.log('before request'); + await super.handle(context); + this.log('after request'); + } +} +``` + +In order for LoopBack to use your custom sequence, you must register it before +starting your `Application`: + +```js +import {RestApplication} from '@loopback/rest'; + +const app = new RestApplication(); +app.sequence(MySequence); + +app.start(); +``` + +## Advanced topics + +### Customizing Sequence Actions + +There might be scenarios where the default sequence _ordering_ is not something +you want to change, but rather the individual actions that the sequence will +execute. + +To do this, you'll need to override one or more of the sequence action bindings +used by the `RestServer`, under the `RestBindings.SequenceActions` constants. + +As an example, we'll implement a custom sequence action to replace the default +"send" action. This action is responsible for returning the response from a +controller to the client making the request. + +To do this, we'll register a custom send action by binding a +[Provider](https://loopback.io/doc/en/lb4/apidocs.context.provider.html) to the +`RestBindings.SequenceActions.SEND` key. + +First, let's create our `CustomSendProvider` class, which will provide the send +function upon injection. + +{% include code-caption.html content="/src/providers/custom-send.provider.ts" %} +**custom-send.provider.ts** + +```ts +import {Send, Response} from '@loopback/rest'; +import {Provider, BoundValue, inject} from '@loopback/core'; +import {writeResultToResponse, RestBindings, Request} from '@loopback/rest'; + +// Note: This is an example class; we do not provide this for you. +import {Formatter} from '../utils'; + +export class CustomSendProvider implements Provider { + // In this example, the injection key for formatter is simple + constructor( + @inject('utils.formatter') public formatter: Formatter, + @inject(RestBindings.Http.REQUEST) public request: Request, + ) {} + + value() { + // Use the lambda syntax to preserve the "this" scope for future calls! + return (response: Response, result: OperationRetval) => { + this.action(response, result); + }; + } + /** + * Use the mimeType given in the request's Accept header to convert + * the response object! + * @param response - The response object used to reply to the client. + * @param result - The result of the operation carried out by the controller's + * handling function. + */ + action(response: Response, result: OperationRetval) { + if (result) { + // Currently, the headers interface doesn't allow arbitrary string keys! + const headers = (this.request.headers as any) || {}; + const header = headers.accept || 'application/json'; + const formattedResult = this.formatter.convertToMimeType(result, header); + response.setHeader('Content-Type', header); + response.end(formattedResult); + } else { + response.end(); + } + } +} +``` + +Our custom provider will automatically read the `Accept` header from the request +context, and then transform the result object so that it matches the specified +MIME type. + +Next, in our application class, we'll inject this provider on the +`RestBindings.SequenceActions.SEND` key. + +{% include code-caption.html content="/src/application.ts" %} + +```ts +import {RestApplication, RestBindings} from '@loopback/rest'; +import { + RepositoryMixin, + Class, + Repository, + juggler, +} from '@loopback/repository'; +import {CustomSendProvider} from './providers/custom-send.provider'; +import {Formatter} from './utils'; +import {BindingScope} from '@loopback/core'; + +export class YourApp extends RepositoryMixin(RestApplication) { + constructor() { + super(); + // Assume your controller setup and other items are in here as well. + this.bind('utils.formatter') + .toClass(Formatter) + .inScope(BindingScope.SINGLETON); + this.bind(RestBindings.SequenceActions.SEND).toProvider(CustomSendProvider); + } +} +``` + +As a result, whenever the send action of the +[`DefaultSequence`](https://loopback.io/doc/en/lb4/apidocs.rest.defaultsequence.html) +is called, it will make use of your function instead! You can use this approach +to override any of the actions listed under the `RestBindings.SequenceActions` +namespace. + +### Query string parameters and path parameters + +OAI 3.0.x describes the data from a request’s header, query and path in an +operation specification’s parameters property. In a Controller method, such an +argument is typically decorated by @param(). We've made multiple shortcuts +available to the `@param()` decorator in the form of +`@param..`. Using this notation, path +parameters can be described as `@param.path.string`. Here is an example of a +controller method which retrieves a Note model instance by obtaining the `id` +from the path object. + +```ts +@get('/notes/{id}', { + responses: { + '200': { + description: 'Note model instance', + content: { + 'application/json': { + schema: getModelSchemaRef(Note, {includeRelations: true}), + }, + }, + }, + }, +}) +async findById( + @param.path.string('id') id: string, + @param.filter(Note, {exclude: 'where'}) filter?: FilterExcludingWhere +): Promise { + return this.noteRepository.findById(id, filter); +} +``` + +(Notice: the filter for `findById()` method only supports the `include` clause +for now.) + +You can also specify a parameter which is an object value encoded as a JSON +string or in multiple nested keys. For a JSON string, a sample value would be +`location={"lang": 23.414, "lat": -98.1515}`. For the same `location` object, it +can also be represented as `location[lang]=23.414&location[lat]=-98.1515`. Here +is the equivalent usage for `@param.query.object()` decorator. It takes in the +name of the parameter and an optional schema or reference object for it. + +```ts +@param.query.object('location', { + type: 'object', + properties: {lat: {type: 'number', format: 'float'}, long: {type: 'number', format: 'float'}}, +}) +``` + +The parameters are retrieved as the result of `parseParams` Sequence action. +Please note that deeply nested properties are not officially supported by OAS +yet and is tracked by +[OAI/OpenAPI-Specification#1706](https://github.com/OAI/OpenAPI-Specification/issues/1706). +Therefore, our REST API Explorer does not allow users to provide values for such +parameters and unfortunately has no visible indication of that. This problem is +tracked and discussed in +[swagger-api/swagger-js#1385](https://github.com/swagger-api/swagger-js/issues/1385). + +### Parsing Requests + +Parsing and validating arguments from the request url, headers, and body. See +page [Parsing requests](Parsing-requests.md). + +### Invoking controller methods + +The `invoke` sequence action simply takes the parsed request parameters from the +`parseParams` action along with non-decorated arguments, calls the corresponding +controller method or route handler method, and returns the value from it. The +default implementation of +[invoke](https://github.com/strongloop/loopback-next/blob/6bafa0774662991199090219913c3dc77ad5b149/packages/rest/src/providers/invoke-method.provider.ts) +action calls the handler function for the route with the request specific +context and the arguments for the function. It is important to note that +controller methods use `invokeMethod` from `@loopback/core` and can be used with +global and custom interceptors. See +[Interceptor docs](Interceptors.md#use-invokemethod-to-apply-interceptors) for +more details. The request flow for two route flavours is explained below. + +For controller methods: + +- A controller instance is instantiated from the context. As part of the + instantiation, constructor and property dependencies are injected. The + appropriate controller method is invoked via the chain of interceptors. +- Arguments decorated with `@param` are resolved using data parsed from the + request. Arguments decorated with `@inject` are resolved from the context. + Arguments with no decorators are set to undefined, which is replaced by the + argument default value if it's provided. + +For route handlers, the handler function is invoked via the chain of +interceptors. The array of method arguments is constructed using OpenAPI spec +provided at handler registration time (either via `.api()` for full schema or +`.route()` for individual route registration). + +### Writing the response + +The +[send](https://github.com/strongloop/loopback-next/blob/6bafa0774662991199090219913c3dc77ad5b149/packages/rest/src/providers/send.provider.ts) +sequence action is responsible for writing the result of the `invoke` action to +the HTTP response object. The default sequence calls send with (transformed) +data. Under the hood, send performs all steps required to send back the +response, from content-negotiation to serialization of the response body. In +Express, the handler is responsible for setting response status code, headers +and writing the response body. In LoopBack, controller methods and route +handlers return data describing the response and it's the responsibility of the +Sequence to send that data back to the client. This design makes it easier to +transform the response before it is sent. + +LoopBack 4 does not yet provide first-class support for streaming responses, see +[Issue#2230](https://github.com/strongloop/loopback-next/issues/2230). As a +short-term workaround, controller methods are allowed to send the response +directly, effectively bypassing send action. The default implementation of send +is prepared to handle this case +[here](https://github.com/strongloop/loopback-next/blob/bf07ff959a1f90577849b61221b292d3127696d6/packages/rest/src/writer.ts#L22-L26). + +### Handling errors + +There are many reasons why the application may not be able to handle an incoming +request: + +- The requested endpoint (method + URL path) was not found. +- Parameters provided by the client were not valid. +- A backend database or a service cannot be reached. +- The response object cannot be converted to JSON because of cyclic + dependencies. +- A programmer made a mistake and a `TypeError` is thrown by the runtime. +- And so on. + +In the Sequence implementation described above, all errors are handled by a +single `catch` block at the end of the sequence, using the Sequence Action +called `reject`. + +The default implementation of `reject` does the following steps: + +- Call + [strong-error-handler](https://github.com/strongloop/strong-error-handler) to + send back an HTTP response describing the error. +- Log the error to `stderr` if the status code was 5xx (an internal server + error, not a bad request). + +To prevent the application from leaking sensitive information like filesystem +paths and server addresses, the error handler is configured to hide error +details. + +- For 5xx errors, the output contains only the status code and the status name + from the HTTP specification. For example: + + ```json + { + "error": { + "statusCode": 500, + "message": "Internal Server Error" + } + } + ``` + +- For 4xx errors, the output contains the full error message (`error.message`) + and the contents of the `details` property (`error.details`) that + `ValidationError` typically uses to provide machine-readable details about + validation problems. It also includes `error.code` to allow a machine-readable + error code to be passed through which could be used, for example, for + translation. + + ```json + { + "error": { + "statusCode": 422, + "name": "Unprocessable Entity", + "message": "Missing required fields", + "code": "MISSING_REQUIRED_FIELDS" + } + } + ``` + +During development and testing, it may be useful to see all error details in the +HTTP response returned by the server. This behavior can be enabled by enabling +the `debug` flag in error-handler configuration as shown in the code example +below. See strong-error-handler +[docs](https://github.com/strongloop/strong-error-handler#options) for a list of +all available options. + +```ts +app.bind(RestBindings.ERROR_WRITER_OPTIONS).to({debug: true}); +``` + +An example error message when the debug mode is enabled: + +```json +{ + "error": { + "statusCode": 500, + "name": "Error", + "message": "ENOENT: no such file or directory, open '/etc/passwords'", + "errno": -2, + "syscall": "open", + "code": "ENOENT", + "path": "/etc/passwords", + "stack": "Error: a test error message\n at Object.openSync (fs.js:434:3)\n at Object.readFileSync (fs.js:339:35)" + } +} +``` + +### Keeping your Sequences + +For most use cases, the +[default](https://github.com/strongloop/loopback-next/blob/6bafa0774662991199090219913c3dc77ad5b149/packages/rest/src/sequence.ts) +sequence supplied with LoopBack 4 applications is good enough for +request-response handling pipeline. Check out +[Custom Sequences](#custom-sequences) on how to extend it and implement custom +actions. + +## Working with Express middleware + +{% include warning.html content="First-class support for Express middleware has +been added to LoopBack since v4.0.0 of `@loopback/rest`. Please refer to +[Using Express Middleware](Express-middleware.md). The following information +only applies to earlier versions of `@loopback/rest`." %} + +Under the hood, LoopBack leverages [Express](https://expressjs.com) framework +and its concept of middleware. To avoid common pitfalls, it is not possible to +mount Express middleware directly on a LoopBack application. Instead, LoopBack +provides and enforces a higher-level structure. + +In a typical Express application, there are four kinds of middleware invoked in +the following order: + +1. Request-preprocessing middleware like + [cors](https://www.npmjs.com/package/cors) or + [body-parser](https://www.npmjs.com/package/body-parser). +2. Route handlers handling requests and producing responses. +3. Middleware serving static assets (files). +4. Error handling middleware. + +In LoopBack, we handle the request in the following steps: + +1. The built-in request-preprocessing middleware is invoked. +2. The registered Sequence is started. The default implementation of `findRoute` + and `invoke` actions will try to match the incoming request against the + following resources: + 1. Native LoopBack routes (controller methods, route handlers). + 2. External Express routes (registered via `mountExpressRouter` API) + 3. Static assets +3. Errors are handled by the Sequence using `reject` action. + +Let's see how different kinds of Express middleware can be mapped to LoopBack +concepts: + +### Request-preprocessing middleware + +At the moment, LoopBack does not provide API for mounting arbitrary middleware, +we are discussing this feature in issues +[#1293](https://github.com/strongloop/loopback-next/issues/1293) and +[#2035](https://github.com/strongloop/loopback-next/issues/2035). Please up-vote +them if you are interested in using Express middleware in LoopBack applications. + +All applications come with [cors](https://www.npmjs.com/package/cors) enabled, +this middleware can be configured via RestServer options - see +[Customize CORS](Server.md#customize-cors). + +While it is not possible to add additional middleware to a LoopBack application, +it is possible to mount the entire LoopBack application as component of a parent +top-level Express application where you can add arbitrary middleware as needed. +You can find more details about this approach in +[Creating an Express Application with LoopBack REST API](express-with-lb4-rest-tutorial.md) + +### Route handlers + +In Express, a route handler is a middleware function that serves the response +and does not call `next()`. Handlers can be registered using APIs like +`app.get()`, `app.post()`, but also a more generic `app.use()`. + +In LoopBack, we typically use [Controllers](Controllers.md) and +[Route handlers](Routes.md) to implement request handling logic. + +To support interoperability with Express, it is also possible to take an Express +Router instance and add it to a LoopBack application as an external router - see +[Mounting an Express Router](Routes.md#mounting-an-express-router). This way it +is possible to implement server endpoints using Express APIs. + +### Static files + +LoopBack provides native API for registering static assets as described in +[Serve static files](Application.md#serve-static-files). Under the hood, static +assets are served by [serve-static](https://www.npmjs.com/package/serve-static) +middleware from Express. + +The main difference between LoopBack and vanilla Express applications: LoopBack +ensures that static-asset middleware is always invoked as the last one, only +when no other route handled the request. This is important for performance +reasons to avoid costly filesystem calls. + +### Error handling middleware + +In Express, errors are handled by a special form of middleware, one that's +accepting four arguments: `err`, `request`, `response`, `next`. It's up to the +application developer to ensure that error handler is registered as the last +middleware in the chain, otherwise not all errors may be routed to it. + +In LoopBack, we use async functions instead of callbacks and thus can use simple +`try`/`catch` flow to receive both sync and async errors from individual +sequence actions. A typical Sequence implementation then passes these errors to +the Sequence action `reject`. + +You can learn more about error handling in +[Handling errors](Sequence.md#handling-errors). + +``` + +``` diff --git a/docs/site/Sequence.md b/docs/site/Sequence.md index 9cbf3a1ab74e..d2caa49eac1d 100644 --- a/docs/site/Sequence.md +++ b/docs/site/Sequence.md @@ -8,582 +8,32 @@ permalink: /doc/en/lb4/Sequence.html ## What is a Sequence? -A `Sequence` is a stateless grouping of [Actions](#actions) that control how a -`Server` responds to requests. +A `Sequence` is a series of steps to control how a specific type of `Server` +responds to incoming requests. Each types of servers, such as RestServer, +GraphQLServer, GRPCServer, and WebSocketServer, will have its own flavor of +sequence. The sequence represents the pipeline for inbound connections. -The contract of a `Sequence` is simple: it must produce a response to a request. -Creating your own `Sequence` gives you full control over how your `Server` -instances handle requests and responses. The `DefaultSequence` looks like this: +The contract of a `Sequence` is simple: it must produce a response for a +request. The signature will vary by server types. -```ts -export class DefaultSequence implements SequenceHandler { - /** - * Optional invoker for registered middleware in a chain. - * To be injected via SequenceActions.INVOKE_MIDDLEWARE. - */ - @inject(SequenceActions.INVOKE_MIDDLEWARE, {optional: true}) - protected invokeMiddleware: InvokeMiddleware = () => false; +Each server type has a default sequence. It's also possible to create your own +`Sequence` to have full control over how your `Server` instances handle requests +and responses. - /** - * 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( - @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute, - @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams, - @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, - @inject(SequenceActions.SEND) public send: Send, - @inject(SequenceActions.REJECT) public reject: Reject, - ) {} +For now, we focus on `Sequence` for REST Servers, which has two flavors. - /** - * 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 { - 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); +### Middleware-based sequence for REST Server - debug('%s result -', route.describe(), result); - this.send(response, result); - } catch (error) { - this.reject(context, error); - } - } -} -``` +The [middleware-based sequence](REST-middleware-sequence.md) is introduced by +@loopback/rest v6.0.0. It consists of groups of cascading middleware that allow +better extensibility and composability. Newly generated LoopBack applications +use this approach by default. -## Elements +### Action-based sequence for REST Server -In the example above, `route`, `params`, and `result` are all Elements. When -building sequences, you use LoopBack Elements to respond to a request: - -- [`InvokeMiddleware`](https://loopback.io/doc/en/lb4/apidocs.express.invokemiddleware.html) -- [`FindRoute`](https://loopback.io/doc/en/lb4/apidocs.rest.findroute.html) -- [`Request`](http://apidocs.strongloop.com/loopback-next/) - (TBD) missing API - docs link -- [`Response`](http://apidocs.strongloop.com/loopback-next/) - (TBD) missing API - docs link -- [`OperationRetVal`](https://loopback.io/doc/en/lb4/apidocs.rest.operationretval.html) -- [`ParseParams`](https://loopback.io/doc/en/lb4/apidocs.rest.parseparams.html) -- [`OpenAPISpec`](https://loopback.io/doc/en/lb4/apidocs.openapi-v3.openapispec.html) - -## Actions - -Actions are JavaScript functions that only accept or return `Elements`. Since -the input of one action (an Element) is the output of another action (Element) -you can easily compose them. Below is an example that uses several built-in -Actions: - -```ts -class MySequence extends DefaultSequence { - async handle(context: RequestContext) { - try { - // Invoke registered Express middleware - const finished = await this.invokeMiddleware(context); - if (finished) { - // The response been produced by the middleware chain - return; - } - // findRoute() produces an element - const route = this.findRoute(context.request); - // parseParams() uses the route element and produces the params element - const params = await this.parseParams(context.request, route); - // invoke() uses both the route and params elements to produce the result (OperationRetVal) element - const result = await this.invoke(route, params); - // send() uses the result element - this.send(context.response, result); - } catch (error) { - this.reject(context, error); - } - } -} -``` - -{% include warning.html content="Starting from v4.0.0 of `@loopback/rest`. The -sequence adds an `InvokeMiddleware` action for CORS and OpenAPI spec endpoints -as well as other middleware. See [Middleware](Middleware.md) and -[Express Middleware](Express-middleware.md) for more details. For applications -generated using old version of `lb4`, the `src/sequence.ts` needs be to manually -updated with the code above." %} - -## Custom Sequences - -Most use cases can be accomplished with `DefaultSequence` or by slightly -customizing it. When an app is generated by the command `lb4 app`, a sequence -file extending `DefaultSequence` at `src/sequence.ts` is already generated and -bound for you so that you can easily customize it. - -Here is an example where the application logs out a message before and after a -request is handled: - -```ts -import {DefaultSequence, Request, Response} from '@loopback/rest'; - -class MySequence extends DefaultSequence { - log(msg: string) { - console.log(msg); - } - async handle(context: RequestContext) { - this.log('before request'); - await super.handle(context); - this.log('after request'); - } -} -``` - -In order for LoopBack to use your custom sequence, you must register it before -starting your `Application`: - -```js -import {RestApplication} from '@loopback/rest'; - -const app = new RestApplication(); -app.sequence(MySequencce); - -app.start(); -``` - -## Advanced topics - -### Customizing Sequence Actions - -There might be scenarios where the default sequence _ordering_ is not something -you want to change, but rather the individual actions that the sequence will -execute. - -To do this, you'll need to override one or more of the sequence action bindings -used by the `RestServer`, under the `RestBindings.SequenceActions` constants. - -As an example, we'll implement a custom sequence action to replace the default -"send" action. This action is responsible for returning the response from a -controller to the client making the request. - -To do this, we'll register a custom send action by binding a -[Provider](https://loopback.io/doc/en/lb4/apidocs.context.provider.html) to the -`RestBindings.SequenceActions.SEND` key. - -First, let's create our `CustomSendProvider` class, which will provide the send -function upon injection. - -{% include code-caption.html content="/src/providers/custom-send.provider.ts" %} -**custom-send.provider.ts** - -```ts -import {Send, Response} from '@loopback/rest'; -import {Provider, BoundValue, inject} from '@loopback/core'; -import {writeResultToResponse, RestBindings, Request} from '@loopback/rest'; - -// Note: This is an example class; we do not provide this for you. -import {Formatter} from '../utils'; - -export class CustomSendProvider implements Provider { - // In this example, the injection key for formatter is simple - constructor( - @inject('utils.formatter') public formatter: Formatter, - @inject(RestBindings.Http.REQUEST) public request: Request, - ) {} - - value() { - // Use the lambda syntax to preserve the "this" scope for future calls! - return (response: Response, result: OperationRetval) => { - this.action(response, result); - }; - } - /** - * Use the mimeType given in the request's Accept header to convert - * the response object! - * @param response - The response object used to reply to the client. - * @param result - The result of the operation carried out by the controller's - * handling function. - */ - action(response: Response, result: OperationRetval) { - if (result) { - // Currently, the headers interface doesn't allow arbitrary string keys! - const headers = (this.request.headers as any) || {}; - const header = headers.accept || 'application/json'; - const formattedResult = this.formatter.convertToMimeType(result, header); - response.setHeader('Content-Type', header); - response.end(formattedResult); - } else { - response.end(); - } - } -} -``` - -Our custom provider will automatically read the `Accept` header from the request -context, and then transform the result object so that it matches the specified -MIME type. - -Next, in our application class, we'll inject this provider on the -`RestBindings.SequenceActions.SEND` key. - -{% include code-caption.html content="/src/application.ts" %} - -```ts -import {RestApplication, RestBindings} from '@loopback/rest'; -import { - RepositoryMixin, - Class, - Repository, - juggler, -} from '@loopback/repository'; -import {CustomSendProvider} from './providers/custom-send.provider'; -import {Formatter} from './utils'; -import {BindingScope} from '@loopback/core'; - -export class YourApp extends RepositoryMixin(RestApplication) { - constructor() { - super(); - // Assume your controller setup and other items are in here as well. - this.bind('utils.formatter') - .toClass(Formatter) - .inScope(BindingScope.SINGLETON); - this.bind(RestBindings.SequenceActions.SEND).toProvider(CustomSendProvider); - } -} -``` - -As a result, whenever the send action of the -[`DefaultSequence`](https://loopback.io/doc/en/lb4/apidocs.rest.defaultsequence.html) -is called, it will make use of your function instead! You can use this approach -to override any of the actions listed under the `RestBindings.SequenceActions` -namespace. - -### Query string parameters and path parameters - -OAI 3.0.x describes the data from a request’s header, query and path in an -operation specification’s parameters property. In a Controller method, such an -argument is typically decorated by @param(). We've made multiple shortcuts -available to the `@param()` decorator in the form of -`@param..`. Using this notation, path -parameters can be described as `@param.path.string`. Here is an example of a -controller method which retrieves a Note model instance by obtaining the `id` -from the path object. - -```ts -@get('/notes/{id}', { - responses: { - '200': { - description: 'Note model instance', - content: { - 'application/json': { - schema: getModelSchemaRef(Note, {includeRelations: true}), - }, - }, - }, - }, -}) -async findById( - @param.path.string('id') id: string, - @param.filter(Note, {exclude: 'where'}) filter?: FilterExcludingWhere -): Promise { - return this.noteRepository.findById(id, filter); -} -``` - -(Notice: the filter for `findById()` method only supports the `include` clause -for now.) - -You can also specify a parameter which is an object value encoded as a JSON -string or in multiple nested keys. For a JSON string, a sample value would be -`location={"lang": 23.414, "lat": -98.1515}`. For the same `location` object, it -can also be represented as `location[lang]=23.414&location[lat]=-98.1515`. Here -is the equivalent usage for `@param.query.object()` decorator. It takes in the -name of the parameter and an optional schema or reference object for it. - -```ts -@param.query.object('location', { - type: 'object', - properties: {lat: {type: 'number', format: 'float'}, long: {type: 'number', format: 'float'}}, -}) -``` - -The parameters are retrieved as the result of `parseParams` Sequence action. -Please note that deeply nested properties are not officially supported by OAS -yet and is tracked by -[OAI/OpenAPI-Specification#1706](https://github.com/OAI/OpenAPI-Specification/issues/1706). -Therefore, our REST API Explorer does not allow users to provide values for such -parameters and unfortunately has no visible indication of that. This problem is -tracked and discussed in -[swagger-api/swagger-js#1385](https://github.com/swagger-api/swagger-js/issues/1385). - -### Parsing Requests - -Parsing and validating arguments from the request url, headers, and body. See -page [Parsing requests](Parsing-requests.md). - -### Invoking controller methods - -The `invoke` sequence action simply takes the parsed request parameters from the -`parseParams` action along with non-decorated arguments, calls the corresponding -controller method or route handler method, and returns the value from it. The -default implementation of -[invoke](https://github.com/strongloop/loopback-next/blob/6bafa0774662991199090219913c3dc77ad5b149/packages/rest/src/providers/invoke-method.provider.ts) -action calls the handler function for the route with the request specific -context and the arguments for the function. It is important to note that -controller methods use `invokeMethod` from `@loopback/core` and can be used with -global and custom interceptors. See -[Interceptor docs](Interceptors.md#use-invokemethod-to-apply-interceptors) for -more details. The request flow for two route flavours is explained below. - -For controller methods: - -- A controller instance is instantiated from the context. As part of the - instantiation, constructor and property dependencies are injected. The - appropriate controller method is invoked via the chain of interceptors. -- Arguments decorated with `@param` are resolved using data parsed from the - request. Arguments decorated with `@inject` are resolved from the context. - Arguments with no decorators are set to undefined, which is replaced by the - argument default value if it's provided. - -For route handlers, the handler function is invoked via the chain of -interceptors. The array of method arguments is constructed using OpenAPI spec -provided at handler registration time (either via `.api()` for full schema or -`.route()` for individual route registration). - -### Writing the response - -The -[send](https://github.com/strongloop/loopback-next/blob/6bafa0774662991199090219913c3dc77ad5b149/packages/rest/src/providers/send.provider.ts) -sequence action is responsible for writing the result of the `invoke` action to -the HTTP response object. The default sequence calls send with (transformed) -data. Under the hood, send performs all steps required to send back the -response, from content-negotiation to serialization of the response body. In -Express, the handler is responsible for setting response status code, headers -and writing the response body. In LoopBack, controller methods and route -handlers return data describing the response and it's the responsibility of the -Sequence to send that data back to the client. This design makes it easier to -transform the response before it is sent. - -LoopBack 4 does not yet provide first-class support for streaming responses, see -[Issue#2230](https://github.com/strongloop/loopback-next/issues/2230). As a -short-term workaround, controller methods are allowed to send the response -directly, effectively bypassing send action. The default implementation of send -is prepared to handle this case -[here](https://github.com/strongloop/loopback-next/blob/bf07ff959a1f90577849b61221b292d3127696d6/packages/rest/src/writer.ts#L22-L26). - -### Handling errors - -There are many reasons why the application may not be able to handle an incoming -request: - -- The requested endpoint (method + URL path) was not found. -- Parameters provided by the client were not valid. -- A backend database or a service cannot be reached. -- The response object cannot be converted to JSON because of cyclic - dependencies. -- A programmer made a mistake and a `TypeError` is thrown by the runtime. -- And so on. - -In the Sequence implementation described above, all errors are handled by a -single `catch` block at the end of the sequence, using the Sequence Action -called `reject`. - -The default implementation of `reject` does the following steps: - -- Call - [strong-error-handler](https://github.com/strongloop/strong-error-handler) to - send back an HTTP response describing the error. -- Log the error to `stderr` if the status code was 5xx (an internal server - error, not a bad request). - -To prevent the application from leaking sensitive information like filesystem -paths and server addresses, the error handler is configured to hide error -details. - -- For 5xx errors, the output contains only the status code and the status name - from the HTTP specification. For example: - - ```json - { - "error": { - "statusCode": 500, - "message": "Internal Server Error" - } - } - ``` - -- For 4xx errors, the output contains the full error message (`error.message`) - and the contents of the `details` property (`error.details`) that - `ValidationError` typically uses to provide machine-readable details about - validation problems. It also includes `error.code` to allow a machine-readable - error code to be passed through which could be used, for example, for - translation. - - ```json - { - "error": { - "statusCode": 422, - "name": "Unprocessable Entity", - "message": "Missing required fields", - "code": "MISSING_REQUIRED_FIELDS" - } - } - ``` - -During development and testing, it may be useful to see all error details in the -HTTP response returned by the server. This behavior can be enabled by enabling -the `debug` flag in error-handler configuration as shown in the code example -below. See strong-error-handler -[docs](https://github.com/strongloop/strong-error-handler#options) for a list of -all available options. - -```ts -app.bind(RestBindings.ERROR_WRITER_OPTIONS).to({debug: true}); -``` - -An example error message when the debug mode is enabled: - -```json -{ - "error": { - "statusCode": 500, - "name": "Error", - "message": "ENOENT: no such file or directory, open '/etc/passwords'", - "errno": -2, - "syscall": "open", - "code": "ENOENT", - "path": "/etc/passwords", - "stack": "Error: a test error message\n at Object.openSync (fs.js:434:3)\n at Object.readFileSync (fs.js:339:35)" - } -} -``` - -### Keeping your Sequences - -For most use cases, the -[default](https://github.com/strongloop/loopback-next/blob/6bafa0774662991199090219913c3dc77ad5b149/packages/rest/src/sequence.ts) -sequence supplied with LoopBack 4 applications is good enough for -request-response handling pipeline. Check out -[Custom Sequences](#custom-sequences) on how to extend it and implement custom -actions. - -## Working with Express middleware - -{% include warning.html content="First-class support for Express middleware has -been added to LoopBack since v4.0.0 of `@loopback/rest`. Please refer to -[Using Express Middleware](Express-middleware.md). The following information -only applies to earlier versions of `@loopback/rest`." %} - -Under the hood, LoopBack leverages [Express](https://expressjs.com) framework -and its concept of middleware. To avoid common pitfalls, it is not possible to -mount Express middleware directly on a LoopBack application. Instead, LoopBack -provides and enforces a higher-level structure. - -In a typical Express application, there are four kinds of middleware invoked in -the following order: - -1. Request-preprocessing middleware like - [cors](https://www.npmjs.com/package/cors) or - [body-parser](https://www.npmjs.com/package/body-parser). -2. Route handlers handling requests and producing responses. -3. Middleware serving static assets (files). -4. Error handling middleware. - -In LoopBack, we handle the request in the following steps: - -1. The built-in request-preprocessing middleware is invoked. -2. The registered Sequence is started. The default implementation of `findRoute` - and `invoke` actions will try to match the incoming request against the - following resources: - 1. Native LoopBack routes (controller methods, route handlers). - 2. External Express routes (registered via `mountExpressRouter` API) - 3. Static assets -3. Errors are handled by the Sequence using `reject` action. - -Let's see how different kinds of Express middleware can be mapped to LoopBack -concepts: - -### Request-preprocessing middleware - -At the moment, LoopBack does not provide API for mounting arbitrary middleware, -we are discussing this feature in issues -[#1293](https://github.com/strongloop/loopback-next/issues/1293) and -[#2035](https://github.com/strongloop/loopback-next/issues/2035). Please up-vote -them if you are interested in using Express middleware in LoopBack applications. - -All applications come with [cors](https://www.npmjs.com/package/cors) enabled, -this middleware can be configured via RestServer options - see -[Customize CORS](Server.md#customize-cors). - -While it is not possible to add additional middleware to a LoopBack application, -it is possible to mount the entire LoopBack application as component of a parent -top-level Express application where you can add arbitrary middleware as needed. -You can find more details about this approach in -[Creating an Express Application with LoopBack REST API](express-with-lb4-rest-tutorial.md) - -### Route handlers - -In Express, a route handler is a middleware function that serves the response -and does not call `next()`. Handlers can be registered using APIs like -`app.get()`, `app.post()`, but also a more generic `app.use()`. - -In LoopBack, we typically use [Controllers](Controllers.md) and -[Route handlers](Routes.md) to implement request handling logic. - -To support interoperability with Express, it is also possible to take an Express -Router instance and add it to a LoopBack application as an external router - see -[Mounting an Express Router](Routes.md#mounting-an-express-router). This way it -is possible to implement server endpoints using Express APIs. - -### Static files - -LoopBack provides native API for registering static assets as described in -[Serve static files](Application.md#serve-static-files). Under the hood, static -assets are served by [serve-static](https://www.npmjs.com/package/serve-static) -middleware from Express. - -The main difference between LoopBack and vanilla Express applications: LoopBack -ensures that static-asset middleware is always invoked as the last one, only -when no other route handled the request. This is important for performance -reasons to avoid costly filesystem calls. - -### Error handling middleware - -In Express, errors are handled by a special form of middleware, one that's -accepting four arguments: `err`, `request`, `response`, `next`. It's up to the -application developer to ensure that error handler is registered as the last -middleware in the chain, otherwise not all errors may be routed to it. - -In LoopBack, we use async functions instead of callbacks and thus can use simple -`try`/`catch` flow to receive both sync and async errors from individual -sequence actions. A typical Sequence implementation then passes these errors to -the Sequence action `reject`. - -You can learn more about error handling in -[Handling errors](Sequence.md#handling-errors). +The [action-based sequence](REST-action-sequence.md) is the default +implementation for @loopback/rest version 5.x or below. The sequence is a +generated class that contains hard-coded actions in the `handle` method and can +be modified by application developers to extend or customize the steps. It is +supported for backward compatibility and will be deprecated and removed in +future releases. diff --git a/docs/site/imgs/cascading-middleware.png b/docs/site/imgs/cascading-middleware.png new file mode 100644 index 0000000000000000000000000000000000000000..3b2b34a1674e912e7ac8ed57727a6bfe430b5775 GIT binary patch literal 103114 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 literal 0 HcmV?d00001 diff --git a/docs/site/imgs/middleware-sequence.png b/docs/site/imgs/middleware-sequence.png new file mode 100644 index 0000000000000000000000000000000000000000..6868ac31bf231948e82a285d3e7b348ea0641eab GIT binary patch literal 120367 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�u#No@RU2w@zYGvA3#4Xhgz;qaM2_x7b9R$^Ni!4qyq8c~26Xp2 zjdGoBHFs|?2d$lMr{B4)5J0VW*H+xw0XLagnhc*)*qNqyoX(g-WHWM_Q#7O;_5c_b z)k@ta7Qh4eH+lli8;OupuWSHGiek;~g@Iu_EjK7-MX4PCWkc|6HlKlRu5$QTDYBHk zsArYb@&%>06nA`=NmW(HNKSf66L7w)ODh7NhI0!xB?7A2FkmC0Ib2v#3&^P2y`Lh$ zliq9y5W|#M5G|JDW?`l}_>*dyQc7+ee8=2Q+S5Oo65+WDeSy@iz@?0mtds~%zZ-F8 z2?I4G$911u!~=rJj|h%2wJ!Et-EBq2_JVBnr0^6b&B3nyC0(T@!gJe6-%+?Xgm@9WFM&tAeUfZ1Gl=9Y; za!`H%=*Lg1|3dnY6&^sZ>xL|!?C+b{;bZkh!uNvP>~9##bQ&qN&sG|-tj|-JS_Y?h zE#jF5x=PxrD_qxgbaaC7L3!+Gtz1}4Y$D2^!Wt2C8WP!}1Y9Esvh3hRoecf1Y{7Jc zr6{|)!1jyE|M8oGDx!>_V_IrJ%2g&QfDD9RR@^>}usth}ueWzsP1AM@`i8sn6nQV0 zKIkXX^+1xOB%}y1To(ySww9C)d5+=lQVK4-mKWm^H>;Bd;OOCHmtKW8+Y& zSj-&3VW2FJx}!aXEG`D8prj^SQCqnn{J5}2mdiK^fq}K?S6D|BPW;fOyZ7QUB|O59>4O+UCSl4wd-uKD$ZDKs zRhoj77f3fL`6ThWVS~>5kUp?aPao_QgRoqT_W>BlzN?P=^hG_qHPbv>@YDQ6Je0W{m-KRi>&{^lh7i;zsDu_Thm{#00I zn&X4_VaXYFIe}{v_1`V!Q7WR%$vZ{;8AH6=MuTc zx7d>~kRE1%FZdI=qe!!^pVoNjaN%nNabWf%Oxe3?$A?;2cWuKL>^{q|N$5wdg0Qy6 zdNE79NcUQ)yoSy{OUzTvRLk`B^D93}Z8^-8AdjIC4)+@ZZ0pY(g!n<;>M$Sy=}}!M zn%wrciY)u4Pmn^GEEd&zu69A)�%^y2___kKXVSe*8UTzF~^Af+W|jlbFgn=(=!V zl%SIUD@2R5v>-FH7^L+FUU0p~>mk0E8I z_&TX#6T)qP(7(0imObU_L5*=~u5v>4Vl9TlUdt3P*1GIG#;X_h^|!Jm)OM%WjtpXH zW#m6%B-H21MWU%^GO24=$gVbp_SDM1Bq!BmF+T*3)!}__Jau{X9bjuq;2-a`3Nw`2 zIwXi?qT(lg$Y`GG}^*3GL z1Cn9-pWYv>9*wy1L+p?&rjCc~=~*BhbadcNlXNh(Vl>X>t9eg$d%uBa#qDf1nx44Z zvs!mBUNOoO|G>#PK^@9a_}7GbrU#+!+m`9LM!IeP#O>j~gY^P?lB#upZ~ZvvH%p$c zkgq@b*Q6*YC4Jy|UNG>KMr0>Ii(XjlmMmwhREt*px_Qy71?7m(^9#e~V3I~#jyTr+ zoyLO7|8*as6~e+7tb$&mP)|M%cjuwh#Hs`7Dug_Cu?n7 zrnc3cjsl-6P2#HjY2iLTUap%Mt0APOO~dv%WE`R=HL}^$cwTsEY9*mDJxTI z2UB)Rw*7gwFlQT_ojqUm1r28s@4ke;UV&s3cvio4VX_$q<9ojSfY(frFa9`-Nl_$G zH$48Dy=OjL{nM$2ea+R2VQbA#2u+c?>!{+I_JwN@AfSO7BVsL6+bqMXh}$;2V^6?K z`~2DoW{~^Ujw?`7;8Oy56a*f-qEmjG@%LTxV4*Ud{#dlQ^B}iyg)YAdNoqkvyHyDc zGEAlAmzZ@5`t;J4-5}%+V#0Z8_&hZyaqv8qZ3#S1+Kkw!&p-m29X0UZGC65HZ!rU7 zgl$r`6^Ea6lk?vUf_vW$=O$%aEUX0Qvgrk87ns(#tIXDgXZPn!u)ns?ZmrSWOOuRV zrl*YZYS#1IGT{<{4y2$%?OSF7#TN^>jw;chw^p=33jv#H4YHZm?Q|QQcd%-L^Wfu8W@UjKLRcHON`1 zWlP{lq%e#2__LgVnQ5rYS8H8iG$?IQ!wdA%ywujCpVx55PcsXPEgM`pu#(t~Jp8|! z;V3->WSahPQ2}=n^fq$Yv=Dp?)B-!mCJ?OZm^%{_(kS`ONmx9{t$;Ce*R1!Yqw?nlZh?2o3XYpreS1joEM~vTkLt)F+|zlVG{cYE3C!1Al{;nBvjD}4iX zl-bkDEn?2e?Ro|$Vs)oJrVz`z;-X88TI+UoSnYa{z`P}u%(+sfRA|BZgiWWljEk3g zD4={>4AsQ(qP7j%K+EG)#TJL64!yjcddXNi++qH!`@{^-`vC^-TU}Gj%fDq?XKnKd zt?7iDoh1si8nNoDp}FYqU*W+6Z~PC-&d`}z*(UkcR!ahV(Kq%>22B+8T}V}CuVLa? zw!cQRm~1k%_1AJ$nKgg4cvc0u>D8S0_>}~#oIkjcvwEQM;<;EO^t}sThyhEQZ~>OI zFRg@Qg#>{n+N;-D=9*Y55Ihl0K=nfs-vJa-DS?q6u83)Gw)=qxI?C_+q-?BP8ggRYy-5#I)-gRU|uZbX>rb@66~&E5#2r0VzTeC+b^gP3|i4a~Z9bRO)65J)E`Q@p(M{3MUfLJKG$%-YagB$<=B3n#!aa-nV@9 z>)mbVtBejcM1u&t=2m&K^b!#`YRhpXhG`TWJE2Kt3^wblfxXRy=?d=pTiq$qgz9*n zR6IaAVOwj#`nuY$t5k@TwHiUKdR!oloDr9y05{ zJpyXuxeTJ>(wvGt8ZZ4vY~7<|kKQPeN(_qd79Iu@qU3-^M5_kpQ1LM6%%PhRuBu;k zwux0>;NS2-Awoc)urBg6;scG5V-}f9ce>#kC*Z#hiVtDySSTXb^8Q z+iW&TH6$5_7iw)k_U|ca=cPN(_TaEv1{ygP`7Y&SDkjLhKca-$Oq2Fyqz-jb%$mPi zVS?18J&9r(@l$xJV`R#IA%$@3F2wR~6m3Gzj>-wSs5rw~XIoR~g29EFZ;x~^RiNq% z8F9cHtY|4+n&TO#9t%=HSy2fZT;7zXb2@IZb{@@TR6l0E$>rN)=1)5(z>VEnio0M{ zeXLTeNTP}jznqn!?^-*K0S&`Savjlyox;O6VuIkBS$~+-cwItXkP7PyuZgQ%2rEO0%JS5&SITV&VDY!)~;^_#M-djLWhC+5|tZizn@oIQ#__KD!~EcZTN)| zP$^6w$&F!Ayw>m1Z4%$huxW%AOZB4D78>xgi{`d|4LVG2&?8pCAxFNDRMkpt^5~fR zZ~#}KL8iS_B8cjL|4{O{>Ov7yr7^e%AzvY5m#|5`d3ap0`iHmSUi3w{b#5yU;yjF4*!utWAB;*Yk(dVd(gf z>_uC+Gxeq!A%K2jHtw=}gBl22Qaf+J$Y0*j-g};mOzN5s&gc;97lEJKMR3dL<}j_wW*O@e zwpOy{Ny}>;ZTl*h<^0wIm3**b->)}?-)YSDrlj17$S|QeF43lHc2;FwY$L4w`{A46 zzY#5EJDnM^vgX^L?qxIBjqD4un6D6>LnUaa2?;LM=Lxm)zI*x0i$K{^?U!+a*o%Bs z5?lo2AAof>YlkhzmZAAO0b}bZgNH)QaNvWXiOAH(gg5N&l!xC;ie{n`vGL}s@6o@glQ42ta}-3h4CqA&cpR@jxaHr@*1Zh_ zn#OIKCndk2KdzI@DzRl@1f4~A_nw|@`Ancrf(L9_Hy1>w-PW;)KSeP=YmpBX_-%ps zd-5}%AtED_UboE8&aOiqznsqoeAJ+uhkh6Owgi1^KLaKu6#5A?IPR&q955eAH_+7Oj%rq zqzX8l;Eq(2%+gKzH{Nu#6S0M}lx@M*G=KbbH8Il_riax?n8_XHwSUoSvR84IPT08@ z)TiMYL5rb3DT@;4$JQZaZ-92Y(mfh{U&Hm+Q;65N9shj@Ouc6gJ1(JMVlhOqHA2GK zE~+%UlGYT8-*RNG?3a_jaQm!RU95USrFdk0tyg^XCMuDqjWQm|_`}r)ILE#ymwx(d zUc{YC*`ld4Fc3G8kh@!3x@LN9@p*}`za|op)m@AR_#m@|EYu-7X~XH6Ctk|o*OV8R znecLA(FKa}sz?tN3GP&YxC0g3+2iz#M{C z|30#w6@USWj*tL5KphEmg~;L!{mT(%drkW7;cInASVnj5Q>*2>GP7dfg;7(a4yaTN zCKtvF(}MQtV8nr0@UrCjxs+2FnV)4Yb_EdltdJ~xf~k!k7Y#ScZtV7lm1`z!4uzYZ zw%MRx-+#BDG$=N;iLRu#b2!$WL`q>ry`P_lSMAL8rEoffB9dvHFfyP|?;E;hA7JcOM2hb9nAmiLwg`Cn z0nBl-0^e+DHUZH0=D5F2B=+s&)VFcWNuSYSQKqW9e_#F?@GN@TX|a=&v7^MRD&3I%F?=`J{qy zb_Spp8|BY8MGO{}o>`mG`rDA<+HWKDfsyrZH&VH!IQnt22OTZ?pH=brmR#xS(paKs zbbOxtowEJk+k;@LAU+=`N14a~*4VX+gF(Zu_z8qzr8zD;G*5as+zjcqED2)Ofs2~A zmJ1k%kKqg9VD@M2MU2-}L>5C9h}(b#wZAyLX7`=OU)a|t042<$^~P_P=CSvV%5I~v z?-tJXQYpBs4c_WgJuG^DD{Igj;(nul`_nv`f-95gec(h*ts~UEu%HQDZYYyj#LLSY z@RsN&Y%C<-n#|#E9eb7{k;gE1g)ahSU zJ9fuW2Fekf-%v4Dy(hxyjQBVoebXRd9u;^kleeUiRBt0q8&Q;HXV{*-Qiz%gbzlZ8 zi3xa;R$C;B20mttdszkv*&yY--;lxx&VHy>ltYrNkvPg9Mrj~vzIwn{ zF9kxoNXB{^p45^l6bFvKMpwmP^7)8X6B8u;mPk=#}iK=L@cNU<+x_IzOuRhj@mp4dlo(r zWYKM6OmGZuY}~%08EI=Hg3V2))vT52!r*tJ54He9Y`&zkk120m2^75hIh=CD>)ETlo z^@WUiyDf*;CcVh(f%lakr(|0Q{A?uUF6$?JmghI2Pw^;LSZ$;cIa!ud-^kPk8=Bw~ zAbFK&6b>Hg0y=_3hnvV6-S3PPwcXfCrw}s=r#gH+hZE`}NxE(bJTUXUE;vJctV+wL z&)mo&Ehiq}j;4d&)>Y;Q+lwPU42N7G+vtPV$zP`^E>n}Exz8y`6T7f&0P@CX&~`Z4 z2<&|p4i9#TI1x>G4+P49Ff9zwkk@uQeWyI*g;4Q%#pS)9GX!JT<}$XrA4{+J@{3qG z;;1#U>)5-aGAhusO-^E|G`@c0(+*_;C(C{bXGRrhaV#rxKV_vYp7QlAikxLi5fvT) zKD(c(%h@n_)7#37K2r}9$?(Y#g1bQV%DI&Pl9+TfGZkmJz za4k3~n%cwgih`~!)=^C2I$~(EIm1bX*MXespNcEAn*w|$ zr$ELmIClS1vZzhPSjy?>P3dFhPXq~^u(7KOE6ai%<2}~Td=>*ExhyFeyCUNe-&I0r z*dgmjGue9f4xDDxS_8s2>kyvoAzW%fz@I84Z=<*A-}Sgn)^*rw7JY^z5AYixNX2{0 zUmf*=dzSk@$&w|MeKCg@)|(jGb*atMAI{M{D5qC@d>C4y-xqRI>YcFtvZa2{7)?5G zu*kTTWN`a&9dDfvM?N3WQCNZRFqa$NAT2>7Y^yg~{5F-zSnBogU;+!3#x#_adiaZ4 zUk+38KZyl3A{7PS2^_>J7Qa*Xn6HFJn(!Eg7^wWUiscmjd`-855*r7nd}jObU|jsW z=;T!*%mVY(TN+C7m0uGv3#T3-#h`S`=xC3wz9Y`unp(FiQkiI?@EqJQoTm{=`DJD} zB&I;&eTYhpPyrAYQWc>Wmys-O%q-@}DI&hIeawcoq}H9x?A%#ItF`SB!Tg?A=usReQvbAjWx6&0gWL4tgIoUCW+DVsG2-sR7+`x%8mBTrXKmOt3gPZh16 zRE?{v0%Lz>H6V$&DtpOH*RY|@-T+T&-19}ALG)j`h{(_Oz;F&z+jXNw{BgX$<7q%` zuQH^^td2gmu>NLWrW+94JFb0R(sxopPH?BhTf8MzrB6D8>52Bs!-Sf$+`hW#yjH1J z*WKKsC;tgBU7{M}_t%wK{;)#Ln~uV|MR&lM} z@!M`R>`Y^py;I9e(u$iu#9lX~ZJV$^+7ZvRW<)zIvN>_SkZD(_5xcTtQCfr6Jlu+B zWVpw=8oPh|53poV(d)AeHXicMJ1`f+nqf%CyL8)dg`Um1w?mbWC*eG4em|mh0=nSg z($4~!-<;lz{8}GzYEfE05qndr6APpbk6YG!AB{8CCnh1YS|oEt3-vfkJij7JO2(?< zF?g)$O2%`ZDNnhYsFSc`g=(Z!TsUjgT?q}E6`kvka!JZzoQslCF#71AOvA4qZ_KfK zoHb?_az^sj;OUjdqNi6;H?~B^T4&kgI<{t+r4@|&`trKN)7|G+qQglT3M*r{jfY{B#MW2*SDaGd<8Gu#oZCKM{_O5Z06=I& zFNu#>2Gkg4a+KTAafciB&qePmT{)^&V%GhD>oNgP`ZU(X@KB5(o3!j|B0>wtMS_q0 z|H%S~Car?ergs7pm4*wy5nBMRmJf8pYLPI%ha>BgbUzEnLTo(`eB&Ed z6NJ9P?$613?&98D!9b#^>&;k+9b=O&rmhZ|NXD9m(-br4i}{dXVpf@23#kxId&hA& z@tGO~X|lH0w&-%ly^4Cqh%Qv=Z&j1ch-|()i@7&cB=jz5p&TZnL%qG^gB z4{kG&v*WW4Qce6dpdH;G|Aq)8k?ETyB~NG)Ak+;l3c*x1b~Jc)&#B1yF4U}iiG_b4(ndy#CI>VHz7Sew5SUQWvxXQ6oZ@sp!Jq0Wc^}eCRYHe zq-EbTLBe_)MG!7gubSGEu4BE-)o`YkJn?r9--lEUJ8@vB*hR+2AB?+@l7+4PEP zKs1F4kgG4?JS{6eLr&T>=8hIt4|QJ*>~j;SFZ9b`q)`0f)Q>%)k&TAn{b>5p-mn>Bhp5gJ)> zewU@X{7S~g9Ia%u-QH^ehvjsirIO3Dt?)H@C74Br$w3ZgA&M+^7d8nGJ(>0+_C8}c z*GtQ; zm)A}_q=fT16ik(p2r2wJ9*A-f2e^-(!bdEVmSk)wSfP}=rft^Kic&xZ3!Ov!sQczB&z~*fEDwvt{ za4A?Tk?luT@WR~eGdQ@W%d|2R@b4?y(6C1I!bR%+r7eRsQ=BN|7E8MyN*=%B_s zzN;#Zth{0gcX(5AaAK#LX-I1n$m?|%6nS-6DoM%rYoSU!uqx*;M|oB@mC*G>d_Gni z`S`MbIadB{V*InwFl%?lR%gum;1e-5z(12cxVyDdSUO<)J67rC=QlI^Js$kL;jfP_ zX}I04q^wp;lp&+3dY05y#~R(^RUw`o0#>!S#T$zomAUsj63@=YdNqQA6iuz~9A z`y@-HgQ5Zx^hDj@Vt^^$cN$b0s0C|YSW)-7o8@?nL^mY7*wtt*${C3NnK0?iHmT~p zY7Q?a1{DtJKMO;pc^0g}mo-;g@sM7{oZz@5o*z%pMh~v5_Wt~WO7H5>%r>4Xp}e`+ z<&4VVaVV8&Z95Hw3B0T8C9H5GgR`3At=p({tdqK7YA-}1k$44N50MGd!F_jgb!`oP zkd7jk4XAX*1ghk6$61pU%_^u_4CI{=km;FSxtCD&W#B!>KX%XYr6Iul*}vLuDmj*}?p*u^tmQkKQMw>+GrbRH<3_T{4z#WuvR4>wQ#L z=~OgeeP><5y;Roun|Sz2uW)Sb=;Q%sybL5tNgKPp;DQjX?@mRovap_eyN(k?&J4z; z)&S*^&l!sE6?&Fb(&N8piZ2g3z*N{2LXV3DD^p8-`v40M(b`XKd{nU@-&QPU?qon! zeuIT)CYb$Qh5y!NE>wSix8dWegf-4dVrT2^=z$3NahJmH#BVIJ1xG9}iFv^?*;5w# zO{f+VAX(TGfC)6D1?`;~YE|9vq>lV~Nk6gGZE;f4OutDG+`uiE#YWBN_1(nwYr6Ep z31+#FINAAuX(9xCp)17XHCBYxDtN{I*JZB!^276nuGjZ0+=(F(s}8`DAiY!{R!bDK ze2BhRxVmxT(l86Q${wew*5E7qv08EC{$Gz9lz%jg2zwJqTvgwfdmwhBb-wM^qWe|3 z`4T1ZwdaF>E0rjkp{Z5>#8Q4(OB&(&=b(m%C{@q8jzB7G5S}4&d^f8IHVw7O@Vi0; z%-LVgpcE`DW)0!tzn)0GO=v zZks?ci_@)Bfq6zr(|K(NeLaOe2GMCV^!8rnOg{4^d;m;6H&c2o^M7PUIF(-Mexzwj zV8(WhsbIhoxorcH*0Xd_`e@6z?n=Ol=&HjE=W4P3EkMM=gS5g@0$~5?i<{nhoSdbO z9bX^dIoKVJLOSSnU{lt8+pS8EuX@(i<>!xLv(ZCTcGx|KNf8QynR+xCET&k0Bia!_ zbOOcB5nij#C&Pbr-ZIn4yaHRTB@%E*)wF!JbAN$?#S$$hiZ^sRLn+KQ~asBSZ7fwSrZEQ=b5kY>a1>R zDhhj_88gRfUbz=p@-P5x<_+El*zv05TBW}^(@U^AeL_~VOcn2le5P#U-6f;rI#4uO zQOlL$aYNO9PEh{@e=<>{+1P>sHw>QSkQRq#gF>R(y|==)FVj_Dot@0!&aYA zO=iQ37-|T)&um!!R@kQA5Ygsu=0Ku)jor z2#9cG+URU!5#~3E3&H?nPhXouWqds#eLMbAS{0}O6DGoIeRy?k6VABqVS(42^l5Ay zyJv#dje&P(T@V@Ty-mj@lPzQ}L#@5>@V87S#K%w)?DqIBBx9xdB?~$kd=Us#^P8u` zFz0$dCV;<~xeaca>W(KJq*b6i@b++-QuBJ==$XFK+yj-H*zkzL)boe9>*XQMppC_hFuRzxmcaa|p{ta!O%`G@tk3P=B?V6sILS zo1}iYjTxlQ$?KrQI{NWn5R4U7boT-aZSQ7J=KWAyuTF!pS%8R-_3xSu6(DGMJu{lq zyx}s7ZV3xLCZ0vHtCmk3WIXgo;Vr%73zl$(VacMgvU}FWbwtrt=YIespqJ^liSI0) z2?HS>UuO&gSJy`VrUhUPEHB<~rIdX;PH$4+D^L|iveFB<`5ZIFvuPFKk6@$8^e5h^ zKr;zUtcUjAfk5!5xdJ4cG{TR6I%EtYHMK`Dp z4C6MOJ#s=fG?I)h8+8!utHf0v$4LJ;v)t%>^4KsM%9=qDLyG_Bci?Om?yNSa4_)kz zO>!~?-o1*237?nO4#WxN&RnX7a#T-@z5QjmimY$$6C69rtt^`|L4oCC;!mDf!k5dj zFTMN%h8AnT$@b?jeI1KoT>o-B_tZ!vUW)O7UV~J7({mD-AQuEZ*};a}%&0~{?uKY- zKMs*lYX4_nOx~TuyBW)lR0zCMnOm7~`j0j@Ua`f^Yu!NgBK&t=d z){lKT9w3D$zd+2Eh~xRAz|lvcdN1v1@kcPAbtT-+`3}5v^-1Utt-TPUI$A%yVcA>X zuK5{qY0E>|iKt;vURTYW9tX*gquxEIzhT$W{+L_f&`hbjT_8Hss>;c`flSDrL-~G7 zm#>l|>q_&T4U}8+-KTd}~;9VhPh7d=oOHUY{|1&iaD& z2m~!q!Ikl_wfV?NVU}dYs@n%~iqDw?FOA`A<*P~gFQX_(s3cPjdg5PNQDb%OQxx%d z{4$qL@t%L=Km%((GReIH3gK7_j$=BQ+==qrdmN#6wNdw~nl!5STdCYy zlvGiRFm}7FQ7^1Y9~H+y_*7nx z(w_I4nwntQU>HGNgS1bXHN&F#S7Bc!S~qfXbijEyjBjsT1Q6cQro7G<2&$bS&0Og2 zvy<5{0RH=1o+Sias8Oh|44GW?Me0HmB+W*N8hF0G+w(Vq`b5OO%n#WTEByNiR|@~_ zi9A8wVW|A~=--#1+GyC3xlSSS#Ydqjt$srXS7ALG$aarMY zPJfJdN=L1uk^1GMCDxMz>M>5lz>DXcRdr9%kJU4cwqQg+09tmS69tBZi;r}2z@kWR z9;Iul;Wh!OteX~3QHqg(N+`k(Kpn@Kg->Yhj);$opfRGr+Oo3FHOOsPfHzysGO%4aNz3+faM)PW;Si{Qu)AOB_!8zPJ+?OeW~Z2jidG#HzA@WAvp6jeQGBx_@#?FbQ^`1_ymX$yV4HidNEL;d zsOtWWWn+*2C5X7t+3TLnl^gyx>a_pYW4mft$I!#xj0~e^H|-EN`+R>uWJI@<4_qAm zG+oVCD`)f}TdzVuLcoU0z#xKsT~|nOcyxY-=Ey2N9LdCKy5x7My{T2SITd@@RhPDA zv?u3ji4Q#0Gu8p5QdLd6 z*;GB{Z!Zt#SYetqH8g(nYSyB@aK1g?c`I&sAb|vuD=MoFyv2Coj%)9l_82yn8&OY5 zoxb}JbrJEZr%G@YYYUPXsb-LJDgai%z^ugn<75&am9ayOT0UW|gcL}z_<#i2FmHeE za9YU!S5A{OFs|x!6-A`{=2~54A{8$R#8&KkZzgZl9xR?=Pkz z8;#@p)VBdyL9!1)9RxP{Jg669o2>~x1I(I_9PYA_RZ%rp`y{nPGsG-_9iEd=^}U&s zjyoXm?GSja!U8Lb4wu@P@l#*v;HA&((Bzw`em0T0*VUklW1Lho8qriX9C_H-W3%9N z6wX7T`M2B5QJ}xajG4iuI{@X}rdK7zQhEU&W&FraG^|PF_FU=)uDwiEdLzkOp2rRl z%JVtg`){jKp<@Gev){=Fs1b@Ve7;A~*^wC@`eQKvyt^m0L#WLJ+zc z2Kocc(rNUI<3Atf+_8m>`59E*BFVNs6%m&3<~lA*raLeP6|W z9m{$`|Ic?8GXW=empTv_I*P7o-B0YW>||7UT#Z?I(AH@KfNkUr8`5?l5R$bvno>|S zG5o}?-3Z2C|ASi(aJa6ZhHigiM-_lHUEh%~7GA=?|MfpeE%OB}gZ}bBQwGb6V-O8? z+_&uag+|awx_=wG6fRa2uGY>qyWNk2XG4!7#c|%u<0KxcOE;?odV%@z9JyjR&&Xy& z31iCbI3gL6btC7vqV&S{iC45oUhjXhe7VZUZjy1FMwK4*M1Z?gB-s`?f(%7BdSy>+ zw+r8V@qK#4zW@OJ7g6(P^Z)eLABs)gFA}yDXOH`%`X;96mKsG90pZU2-jNZmmJc8$ zmwQV3D}nmEMYY*P;(XmNU0=X7_w1^}HTAsVtl@t;UJ-1cnlIFE?+RFcb<`!Lv}!iC z606cs8}}=Zr~0!YQoe#>4c?B=ocH2~U`iHQZs#CGy0LYbyJ7t?SC0HXmeO;<*p7fi zP8N9QTdBa_@86zF*eFVY&pU3yp>>pd7=FXm7pol5Ggtdh(|qFgmn|MW6%|>mu9MG- zc&&q<&OpGMOK+V0DSMhhZ?&zur{8x$+8O`+Ezr)(?QT0!KcqwlQn&PcN)iFA*0V&< zZJB=9jY{@Ka9@hiG(QRR!n7I7aE^pap4Caunn}&BHAOOwuvpUWWTM|{5XA-%VdlQR z!@Dfb{2cmtOzEZ;AutVl9M&XlSB{gwM?x5#hn+sp|0X%t@X!RzVNuc#oR%w@nJ>ywcy)ue+|Xa z8P?f0Rs0!*L3wYzff45(bz^FAwt}e2hJzHNLq0k1sLDaoPt}Xfc=Ugp*A!aLuIV)k z0)Mx@NHo;4y~hn>CQG?0oXrdSCY{4>igliZh4*Nk0EeYF&7hH*#sNEU+l-O!8c!MzWBpI-tD-Il|0s@ZY%+dhET9zh;U? zeeKOA;N*uEq8R(4U6U>ut?~G)( zuP&}z&Byb2@-&E15L6_~amed3QEskA`+IyK7 z3s^Hmlo|98WwW0jRSYS+&Uc`OWo^Mw$Ck9zM%Ofp9!9zYJ{H_it)ITw4=fu|q5!?y zbjlCoq-0xCkUA;q5oR9=r||lt!rBIRxj;rITR(@y?fj_rH}(l*C#xwssNd;vh#l)$)AjXeLwIm8&e>N+-}7oZ^k~b^awNmhQnZeBY4ZsiJcxXz%V*Bx zRqeSxon#UgYd5iaFHyd@AIT62>IiDpNA^Cn>*pcIH~QV9->x(7fkSSbWe!q_oS7}k zJ8$)xD+k^UXh`CY^%OP8e9&~wiesf0tvV-8ITw`X4RGV)g?GM}vT-l^sp0cqY#e9} z#(CVLjz73@>TJz<<=PHg-NehA)avD(a+!h_D>t4fr#&>gFLv2lNxHmH5*|VVh>fMS zO*hop-pxgW=}3#G+V|%208JrRNtr78vs*tSm+;u`WYaRo(6W4ac6IdIUZw=m?Jr(C zH{u&*7pzkQ|MynGg2?R&^x)bcl)u0}r%@M(KXco}*`05@yY5D=A2yzSuXsF|t`KhZ zn+=5pzj0$I#0kaC_W!N!fB^atKqU5BYG9udYPz$E3wXMLHMav~1iYOh!D7r7 z`vu3h<%0)PHzCAt-6AD#)5$hX8Ajb6Cb?Gt)!5DGI=qSxcx2vrjkR=%!(S${X)okz z$z*;a+}>lF;=XAMW^19@OGNdvQ76I zc!?duFmitn;SI!Ju+{KIn(hVW=dzmkL}3C0xN`a=*eF)6s+qB0;vE;Nf0~_bdC$uv z5|!@bD;H+VNqRf)sE+*~{UKm%rd@gJL$_Yt3jAyA*|@R19V%$*L34LPwq8)J{imot z-c3*+FA=#@V+uoxKb;@1oY>kyU)9*+i=c>=!upo%aB-ykj>X!uz-08Ze2m-bI}M%1 z7w5zx!zesAM283L?@^a1F2ke$pUp5Qk{WW<>bq|$^`BlJ?y8FHXrW`1Dwh9)641uT zjuWwmp7Cr_81FA?c1Li(oD+vPX*O{gULyUcQo<$Ot2)4< zTR$Bkeo6d#&9s-sP^$^$a~^@$_f=1HV7x)AdS5jN&bH!!TDriYdqfF&rxipuy}Q;W z<9DUKb=XUaRzv4xeB$UrlEQ{WeIfw}R&l#vF^A85t#kvR7t!ax3F>`Oo~8nVndjXv z6)45u(-F{K5qM)1{S>r1cTd|w;<47yYyYj0@B}hIYGWu1|J`O!2jj%~RWGJM13nES zE#Sv;GYTZ2`!4tUe=H0j;AOc)4qv^FZZ)}gyU}}=`8@|bUKtViIaclkoLe2P7&RS# z!Q`C;B2jxQtLjCk60|R-pw8T`+t4;-$iyzaunKwUCHgTkHv(C1)JH6S^>T+)6{^oA z1yG8IhL1oH{(8bZ4bp_#L-V=wulh_L@BuA%%%2rXZz~u&A+c}GT9Ajmft}`%h`kve zqNx<&u+P;(TANdV=~$|a*&bhS?wDh6&6-pTzU>NI`)=A#9FbNRRE^$-)Vv?v!VK2MwJwARCHA@h{2e-2P3-BD}{JzecZ>T*{Q17qxjwoC2zZQ_HM$GP@UYEw`J#Q|>7|~kpZ5n)WJI~Fg z_qEi&Fn54of3)v$fjA-Z3pv|}0*m+mXb!&4usP^iJGQS9Cxj4Q-KiSCBdKiTm6ExL z@Kw^y>yp+F#Leh@CuuP^r6$@%3!V2jN8#-gYjTe{LRprhNF?w zWj{yL)jloP(I7|Bs&qP6=(cTt0pjoNL06PN531-NMh@6l57gOWbp_#i^ZfTe6(+J3 zo^FHin4_kno{+F6$_%o?8nOJp$?!*`+Kku5Q%`I{OSI|IvaJARC&S{qX$16kEGZ4=mE<_ZJv489*+UaJv2 zWLSl}>|bkdNm0hgjUHCbe<=Q?e&*%<7j40nrD`y>+G3+uF6ftiYUX9wS6OoIaA^1J z>euR6i%cNz^!%w<#Oj|BMrmwkNVxXHg8&KTsHK#ZKL(u z6^4Z+a;kSq#N$M%T6k$6?oH|@f0${snL;)?Jnmq0>@E0E!)Gl!Ajoky`ZIS|_wFUG zB9CQMVwRB8=g#Y)X4S;5 z95Ktc%Dgu1iEtdhN=mEhJoMF8ySJ${tVYfJDdG6z<=P-21<$Ngx1#55>r2KdBz=vA z^fQho#>c(o-lZwBU&U6+bOdJ?9>j;&d-eOAd~TXC_#&EA*3$Rcw8BES8ru5BA)#E4 z;jM0R)%n+v+n2=43PkeI#I;PAR=C05(}Nk|+u8CTb!3|_5y$l=3b3QIgsQZn3ze&W z1gD&HT^G#>M=mPTv^f$IPvP6D9InK6g+%*Q{pz*6t6beh%s6ngV{EB3IXT@X?9eo` zvu+yP>#=L%60R-R%IVE%xH}MRkw-fE^nq};qrR@;j*oowZH#m`RXHR(7>=Eram6@kSvE8<+uYLOQ zk}k=<7HM~a>(xkEMpKl>QFVv^H`PMluzyo5XjVsNYQ%=NDR8W;H7 zxE}p28?)w}&TYbfSZ}8LjjjcS>)oj3sZC8N-iz8#aWZp5`EY`LSH$%3<%i$oP;)NK zXbXV|e9;%0kPozVX+$wnXt&M#kQhnjxArp90`~@tW10P76MeY!$5I`~EMnexpR;82 z+XGwuX*!a}Jz@0ah3WIDv<&_KpUO5VgWANKT50@}y8YRV96+>sn5r<8{~>+zrQtLp z-ujM2wcgz&%a_G^UPX%GjKY)AQtX?9q$gVaXefgfafMg=*UmH^%w?tyNuAQ;lofGp zplmay*WRn#v;Gi*%UpuCyr<GM5;KRSdX-F#G#*9<|O5bXOQ1H=Rx|3RGLTYxb%viPQ_G zXkzOTt|oK`@19P?thcr%U2AV!-z7}fhJdeF=y`^}^ zny41oQ4tnW3mI>hnMiP_TII^X=MmH%9f~g=Z1_5vZ$up4IvrK|u-!%2`}S&4v9~Dv zXzh7BZL~63c+}YP@zNb$H@VIZ&pRpMf7dGB)GoJ}3B#0VNeC;x`^|C)ju0}D?3Fs& zf&*J7EUXz7e|lY|-hLQ=Dddv+YEM?jC*w8*Yth8dW3H98YXG*(|Bt=*jB9H9wm_AG ziVYM*M1`m{6)A#%bd=r|1*Ar$D7^-xgs3P8QdFc1QHnIF0wGip5b3>!&`W4RY6v08 zTLI7io^$WH@7-_j%lYC@B_Y{!uQleFV~)A@(LOJ{oZP%=M?^r8Sr~lO96hoMtta_@ z|0uOoqRV^N5Oqyfg{pSv+9HZ4gIrTd&%wC#k%@#gP70Zj1O8H_z2fx6As>#vbi z=B)>L2!$Wb@_Vo3*(07R#|-WQdtUt6UPRN!Qi-^?&nfEze5nSqS4qTecJM=Wcg*|` z?$SjgJ!ZPpLvL_``oZL5YJpD|Wljg--PaBqj|DwlWJ~93xojNzg{PGD#!PwXYI<_2 z!`EqL?L$l5#KXA=qOv4OT|U6`$`>f5xQ*|~%DhntC!yiR_pKCIJmt2Vr35rL%f2)J zPK$I@etqG-!@Y65pF$p9@M+9G<8;}q%p(%aR;XEsc1s!Qr%Il5&y`q@ z=Uv=cy_kp27Z2^l2;D*|e~rQQK5>8Cp_d~wdQQ?MFhM!+TCQ!!r*R$l!UN*t#>q^N z!{HL4J`=2>84-ZTH_X*gj*8P>PL7q={V#Bg~g7_aAZoOJ%a?o%a{ ztft1za|h{)S08hiMt~<6#GKlR;hDr9CC^h4j?;~=C}zD8atTrg4CSwhRt5JGORGLy z#csE}Y5S~u)Kj}Qo0eR!zVi^N_8^%>s#4R9;*>mFRF0m$_NI0Ea>PURU+Y>I%Hd@m zNe!+i>BpG?B!>Fx$=|U}o=}9I7Ta|0cwi6fm{iVkYdg|ww-q>+WyhRJae+LPPd9Y( z8cXTQ%6-fp{tlPJE`Z;}oGKk8K<-<9K^ESh5+lAWLms8;n!1G<`{^9_skxV}Pd{d;71)J8 z{43Y(y9a3mE+k@I3DuT=Da|pbQaI`kYA!LNHsWAUQGMUeItid%pxEN4nnb$`zOth9 zrHuZSiRcUEuPL#x3aRotX*#CI%~>vl_o+W^doQ&dxjmWU_xb&h!q;RbxXq*-si|&S zH`L(q;p31qZmd^PC8^B1vt&Xyscia`GFo+LSqSC8 zhOa=HmM-nB?y8T0EIGOMq)we=C9axQEMYX58ErPi9$HTx7=fs#^%O)7WXK z)?5QUhVgbZ?QTACrV31MaQPWeg(VmlU*75ud3o4spfS=~r@6QDr|Gh3NeIO;02efM z7)kLQQyjK;T5U8Dv1&XKE9v(6i*eEA0`FPYvABg^r6+IbB~Wn;Oeol6_i2Xk5P6n; ztAJEAtCO`dLc>64Uayxnehc;4*VEe(kVwibqk?4m(jkmcGh|L33<5< z9>_xkwmNVs;6iW7tSN>{wk>2Y_Cs&q(mn?>_vC22#}-QRhm;%IED3$qji^Gb-AE;k z5B|EkVSC(j+kv|jLS18F_a$(+ye$4Pv31%9G7jZ3XO+gSX>KKBoo8K_y@s`JAMd?X z4r8V2v0x9Myz-MLZ#80IZ0pD>kyOLfQ?VK#Kw6GcB7~p(VxfSkYK45;oZSkagpIpFZSOP3VlhQb6zeO-7n@pq zP`qM>Crxjk3^!d2o7>68s3VekI>S~i+Y6`rJZ6}pUp7U7$6n>Fi!t|6id*8ZWNOuCyl%D$5uMXFe;6qu1;Ptl5%7xF|p@yk`>6_ZOQAic@rsIAyv z)3c8nJ*IusO5Cfq9}=x~3aL&^;rbX8!{t6D6mNPJ314=ZWnjdQl-(Tvh+1?LB=?Hv ztzul(V-6>%j)&6&+^RniA9V94R@46p|3d!yBF z1&MYTG0<3qfakxEuP$c?IY(?WvW$6W87e7NTyq!%#%i-%3gf#GR`p*nZDv!{dT|i@ z%>|iS9;43L-6M9uOp4x_ z1fJyq{MqW)QysUwnxlO`*+f4Z4tFFAK%j*4;Y`a;U0;lT{V|xQqmT6f`B7Yqy2Me6 zIoAPEd-2+j^BUx;mVwvS1Yi6zwD9p;t$1;5>ln)ocQ0oCGq&h8 zV-SAOE3y>D#K)qMZTF7w)$3+;sy`E%3(+bq`yj%vSZzSPSN_E1&1Nr)&X$BeRf|q$2>ey>osa{jxP_;BHJ71}yTs8G*ZvIs zePYi3IHWI-5b`P>r@UM|EL#3pi#76BhLFQRQT&j{=51Qe$VzvZDq9zwd5@5}0?|sm zOWezZ^4+21G)@O&msK?ue>`lqTL%)k4g;P67Rq7KPs;E zcs_f0gxl%vzU;Qp#Q-HUfkxGDntPeC<^eA?&L4~2<%~aXDpwnw$RpFO54=(GTugH7 z!#MrSrBLoKCQdpp=~W3!ebG}73?^CLRFBlE)>#W*@!zb9(2bTA$8((c=w|8YwW!a- z>M>h+;8mh?uX1jQ9emiR;KFxeu4d+4@Gz!1i1&9}WApJc;p3d^XU86_uD^)nE$b6A zRGy0*OK3(82VkU>O67*z!RcG-QG1Y=9LJ~TWNWnTf+2O$mED!fhl!I5tCkhLMe)TC zh6y80y?tf^EHWIW;&%!8LPTa#`^9XO+z<2Yh|`D!BsrMObg1;Tc?ZUR^OliWlh;eL zbup;LlH#^Qh?3`bMM4vHseJ14ha$PKA5hhx&hvF*&$10IW_n?mZ5w7E{7@X?p6R?l zo7eNOldQaXH745Tfe36VENk79jv1@`5zWs+tF@0?pAJ&c*~yEZVNtB~bAq4_GA5i> zHoOVypbBY^cMiQHA61&Ct#S30^Bz$(sD2G`dyF88dVHE~nU=|~h`OEID&3?N4PR3d zwK8+G<>XYT7v(QQ6WutYr)#cwz@z1|Dadhwcqb9|;QQdhBg%13PZJY}l(v-98w$7s{hMHy`6 zkKw@0KCuq+c1%Y>8`5ccLzU5XXe*~mDi(jym!rt0i#NJZFzP2b4|x99gg6^BR$S~7 zzd8tP#0i19Usj@aehc~UuO}B=+RsrJC;?@1Ps_A{p*W*Zma%Z>I|uu z412dzM>H|3fzq)W+@9?h_`wR#npNC>?X2$;O?fkh{Zy0cQ?`L7dz)X?`_YydiP^BJ z>+8?D4X7VqNA`#)Z*!sms+c4zMOK>4tqQBNZ;+)(wL4I_u!<_Ui*2R zRvFXTQ>Gc%0xM0sdj6tw%-B?oeCQw`e`x|qg$JI^?cNJ;{zx)Iw+6Tug$4&a%SD6m zbJb_!_%98MyCpba9mx0V&ooE>_!Le}-r}pdqR*dv%KqR2Ya1aaX4q z9tpheG+b^wVfbaJbSO>nC>7lV&c4(GylgG*CN({Njd1GnrRE z2_1#3u@FI3Go88F&j)FEq)e}WpQXYyGMq}nbMyPQ>NpVOj+@a2sk-|4uIriigAXB9 z6Aw=$mI++#$i8!QHe995efB~E%$+Bqyw^ngVIX45zbi9Jpm?YtGw?zcQpt0&_UyMi z0J{_S2iTrJ_I4M$U(qS&5A0yK(OXbubfBLg$yO=EuBk4YJ65Z9IbVqCR~N@t5Bkrk zjmyQ}GQ)PRogyc1KRzivuuW4;tf+7mxvOp-GeX`xEufS|_p-te@FfmOd^KssbKO=p zgPd$yJ7t~o>%O2u0*mm6!|g+J@VIk*JDHug#Z0gzUV8?zPhTW(%^7!#)^=H=`?hL)M; z-_Dst;Cdm?=BTiBTYGZXSWRC~Rj`A$FVAzoKYW&9Hd)U63JKIwajj6I~ap{oSNJN^xgn(T{ICV$xV?<^N1AsQ%p-fVOjS zqyQ1UJ~{EVHT&8L`+I{kU1<%LZZPMp&)O+g6TAgRGlEI0wkHL>#S+Z*XXf8^Q;w$s zfnTe_oLw&Y!g0A8?kbq9R>=Iu7o)FAGpwE!IN?z56(OpSR)A-^+M9U`DNAoNonxhr z(V-K|u@4pVr$uP8pdxh3ev?=|6Ra|s9mHbECpzL$ayJ?$N97~^oFYL^;4hys_5K7X zkpyb7D|iRFF=`$GLHHljnLjhQa^G)zm7u9!ptd-yqo{R*8P;%%j`I+)fo#L(LZ=bG7Nlf; zy817VQjVN>OmDV{s^9mHmwSlDv=og260<3x;#OYw82PqQ@Lp`c`T z9UCT1ZA?*fC4=7?_`a&(O$7%(n(4u_25mYDtVCS!A4bPuEo$dDjJL1rW>ann;p_Sc zZ%ZbieM$r;2r+(Wd3xR@5H&afs9d%?faDj_?MEzflbhE?D~%ZLtdcQHkM07pz;h4S`tXPP)`1Xgb3Cy<$|votYWDtz3AYQ&(~4UzQ zE_|*3Q887HTo90-#ik9?jUczPO*hx7lmm}ye+N7LEj?zi+l8`Ctd1OuTkCz$QBxJm zIzL7pK~Uii!9|r$TyDy5dCP0$D{RXXD6ypH>(A@s#2sua81-3yKGM(^@mOlx)ro&p ztNyep!zUgd4$ZCPengqiA61R61K(6>T|1EDQq_P>^pM-3T>3WI(T3gKZpfC%I~}MF z6!gadLXO|nHyHc-s zP7b7`x{?KGesIWr=kpqyQP6OZ|uV zeCdvjfl)LXnOzFaFVl&zJ#|#0+4UKoS7sU z8U9l{x6*W04k^A<(i7d)R(-_Gn;4gZLse>|&ul9rD}3flfv0 zw$xuO9jL7@!iKQfv*X@8n+uX|mSMa?=>6mq*wHazAcF~J9 zyrqN6qW>k=6kSkI-G1k?lk{*WYlk>SCtpaU3yr#U*%i{z>lsoCdSjK9pdF! zHKD!8%nNG1Ik#2NW;~k^`}$;qKyK9qLpkaf+*z!g5DhoP)#gAGc}E$|;~%Owy_#kS zn~!v>X#Vvnv_&`x8asP*t+s#>bZbsU6oM`wcSAp+bgton_LBpMY5HIGqNb(1_5O?7 zu(o&ObD4bpCPlWa+%;7ku5XsQGt!};tM5ys2KfzdFHLr(zE-z=p?N9Dkt{Pv{4sA? z-I+SpRbIZnQzGwQpKr=aV~k7?pEJ+uw`rEBn%0QD?x~ZVIOF{Y0Pgq0^b!+6(Nr7euQlXy40xa&Nw!s1Qx#vnSzfP%p65)2wO{hY5W_YHR2n}Tm zmw~u>dd~Ssgbx1R+F_xe7)|bb}G6YxxIZyax+IBJQDRaNh7E#H258^_>&qt z1Ggd&LPf@v1iJWk^>jG=#mTyhw05G!9a|Z9fXHUXs3`oRRQF?Md>)N7GmO&&OiwSw zJm^z9yaH8o!QwHP*BX=bcPYNKYUWIlt0>BqediJr_Vh?_++jv z3VTti zIY0Mxc)lS&bQ~IAQe@kC*IaU)P+-;Ph)!ECf0z=2l;sEXtdZD)p*J*clc))>6Y+UQ zY|1tqN=xpnEvVh5zLE~xj@|aIH*?h{T8~yr+@+I zh@Tp&`3SoFS#0Cj$Cj-oC(qO89GVN6h)sZCnFtTLP~dXeq*FfYLDH1dr0mihs71QF zIo>+-bP$DMHLnGAf`feCv@rMDnXBC+#~X60c)r*!pahHy89r;+q;(|~+VKfFVTF37 zD#)a-R-~iiN9I1t-))Cnd0#MOn?onxV0x5OaIqC4=vGBoRVC$^lekN3hQA5W&4L;t zfpV(rru=SYOk#gz8+mJ|4aQ#Z0f!&XsW^h`xMR*KQH<*m3~fo+om zKloxH*5~aF_(X5!Esdp*K*`UOvQKA(WY)au^E7g+5%L5t_su`UJO-FW((ew-PpwG&U)f*2fr#7fp{lZ|iMZ`qG>M|c0KQ=rQh zw)Kz6S`LpWf!_Jc-gL_?K`fJF;c{={&Eb+mbwKrX|N%xdlSl~y09HeUL}qiNq59}3GY6hgj?GP6%rd>ysY zFfcPc?MC(P)tZG(@zi;&kIMm|6?Ul~QAr|shRbjx{8q(;#!U;EdALjCW2gu_Jt?)j zfd}V;U%+BY1j`+U%e^{kdZs}Sm)f1H8%5r7Jbl+|tRS_s*EBZU5Z70@y}CK_^AovG zD?24wwov9#%$F-;USg4F>Lzv;Yw{~js|%xPdx9M0CvIkU3hkmS86I#_il!LA?yVa` zF53m+u|6B`_;j|jajZ;Z?~u4&$zSe*x==@~;sQ51Y$qLjJb0$}VG%et&Sg&Iy3@r_lf$n!4(xj|(StzN7F+`s+ z_qrV*M+tZCD#N5~FfEYxVa9@`ZY{8M zbiB||50hf@uG<^vD{auH*amD42^>p0&J|StdNoF%;+ZJYJu>+T`v~0H&qIqEwgSA?gOb zv&vQb9aF-BTjP;{H|)l@m;F%#;~B>j^ke*@Gv44<^Itl*m25^{b}X@f7$DpG;0xSz zJdB*FpJh4QnF=1o@@{mAv{c39sJpq~e-G{6ztu#bx-iVeT~0o@=+ttJ(|aFvg13R@ z$mv~s-wz3w-sss|OWqRNewN(}tF3}WpMiQAMdg_08}tx{Q*yT#`zKNpi1tUL%SOC3 zb)MiTV2ObK9f-CWF5?*8TUxY3qMjd5H`vwNi-u%gDJ;?)Id-1zC?61Q__xpT4OIwO zr|~x$lN{O}Y;tXS!!d}u=EfrE=%T-8YPiocN})a{e(AQF4QB0}8xj?je{uh5wLF8;#cz)^ zUX&1(XLw3imx|Fj2iG1R{3U#zQ|_eWyn|?jaNmqrMu|h_JnIxaf$hsY9Jm`U8ar(1 z-99<*xwT1U0jqjjJ*^?JLi}L)OpKC!;9=EQ=AX>Ek`Cq~9gDQ{;u%f8)UeG9|Bw^Z zuk$0WOcgYnzO2-8(!OAf5?T^+5oVG^gohy%wFM7$|1bFWmp=$M=_= z#Bn!&7=-Gw{<6aZGa(myzf86=N)Z_8npv19_KOP^H)eikB+@O=V7 z<40>O`@h{lS1vbIk@fL#!s3ueJ}bwkxr9%TnxwSj_c0;l;BjlD304|VS0?JpL};1^ zs@qoxfaA_>{!nvQZOnQ}+AL){vsg0^ox$~@OPh>*k%H=&9fMrc>(~J42!Yz(NEv?v&F2c6sRuwFdpk&oDkqXRWi^zY!8c(Tm%3(f%#Jf7b6a?-w`B|CRf^9- zF{TNw3W?6DJ|x<_ag}L^p9yi-_uishLT_Xm!mR*Dh;#NDC42hMyub`&x3>J*!_7(Y zaT(4VvtKh}b1MRefC#X%9g}kr32>{!2tHxCU-hnBWtR#_idnAQO;{9^0>2Mv3VqX40lCd1yi&I=5>vVv9D z+2X!?<>7sn9Ef%Ph@+g;o-pYqDuXt*kRZ)43;VB?#S#rWvi1j94Zens1es;*R;Zs2piYxXXlJ!vV`RQ#eCLFyBsA zQ=<3;F#d72)ul`+@MuoVl1D!JXrF(5i-q@@VKy1_t0B(UKR7?hvigyzK)5(C#hZDR zqpsdE?LhugXK9hISL;i3*4G-GuLvv{w_PR$aTE4+!4$i;e4ZQMen-%)u|^6Bm=s(2NjS&a*0NCMJuX0&J0(-Duo%slbnxlBn98osVOGb4tQI^kN9L$G6(eNz6g zN?)I6pNyJEX^?fZYLMiBQQ>-GEv3Z4^NmlqT=8R8yVBKars2bsT)i&8d^vOFg)?Hd zC$xAzs^flyp3&^$)lSJLcFpc{ugD-C|0H*kBG%>LhH#GeM3g|k29pj!`X)}8_->xP z^ns!>AB)8KtrvcpzPyxJmnkF^$6rPuuL_v+$(Z>DoT$4rrUMyALbVfAv{j$n0}QhJjdJq_R*TR0XW4(BA$i1iQ^w-C)`OrlN9I5wT#piD$`V{j(2&9ysGi3a3`Ts)4|xRrqyWS4*2M2@?-;)EJ*!S)Eh1?|8qND z-4Ar$cT9os(AXAlbg+DoWBl#pEA-nmKZ>Vsx|#QG=+aQR?FFZ35N`4KW&=3pEs6AL zA}l(&c!)QzLlQ5Jp400%-weB+2MDx7tJcgE-g%5+wm&KL$eHVEDn|yhIxQB3PVMv) zX+&>v+VZ(W$2eHh!Da8gmqPu+-*kD9T`C>miHSC5rNDJ4C?q*+z%hK z7<14k;U-pfud6^d9@^Zhs{Byil;a948q=Q@Gcq4;)s#vP`(jOrq!&2P(<5xq5!G?O zMWoby&yM5uE76`+2^ZO9$WC7PETO6V350+@$>}Zim+?H6GA=5KYsK`HjzmlQhE{9= z`)KCF6nS6VKxOjULi>%i`YT(140VWF|Loqkn&lmgB9zf$>r}RfRqy?x2Y9OK|b9cV)vY9OaGcM2%0e@M<%R{hjJ zaQ4c~0@~9NVb*{wE9?9t(S8TB(Upk+vs+7cjB#|KEiz#U3Agi{gNRkr8>!r-OTP0* ziR(2?(}@%FG}e%t*v&6Tv#|Bks=3Pd*vd(Z1?ggYFf3RaXx{;A`BewZY;4-k+wPU* zJ=`3(NuthdIyYKMJp1Z=f)=YVs;u=^JWb%x+3cK2jCzeE>gHkFYkhmC5b4)ENnTX1 z3o$5i_6U;aOtAyo0)Y$&q;GAzDSKr$7+Zh^OH+vXot7I;9)rpOOZn06c3r%L_Qswp zWzuuO!_5rbQV+2Mx-x=DZDbs#euj0247&+^bY$pfJ=fvT7twj_qW&%!&RAIpQ~-ut z=trKjBE@Dv`Pcn2zSn6DZ61TsugzL&NSA1_I&%qB8TrTxgFf#Mz*%`v2TEo~E z*3zO$F}$L%fl|!Y3MKD&bty&kM|0N@VXo;p)y}+-;OE**$&TV)=1TjlMJ0K0f z8arLbK^4~Vaq2Y0%gjACs?x^%%CU74Bf=;6g<|=s$P?sG{%x;!CSlNQwCe_!a`|el4r(4&^1FLalnFl{ zA>U~>Bf;+jAv)?S30SFFC2%R{vAvD5=4?2nahye&FB`4P*}^9qJ5M5j?JoYz7Dtsw>Y`nEoMHD;)gt2io#>%>qX}dE*?R zfVFq~9bJr!DN;f)E)fV5I&aR$JE2b|Q3lOsuKK{XOGfiw69%9vaaHIMp|_eyFxK>B zs;($;0>iPB0%%^m-*i+?r4A{@Q-s*$Qq%SY0~&SO+@Q631KQ|Fo9B_DU(d%lLetIx zEAD5RwJ{RxGFKxXX^S@Epo(<+WZ!9neM;#Fmw(TNui$432n>@p4;yriAKG~4IUGfD zNo}3aUiArV+8=Fxs(I?HBguWT`CGhc`1N1`-Z*U$H&9{$TJh9`uHM`g>P6yi|DT?|R5VpO{ z*h87C3!H%J6&NnbiF{OTyMG-eQ5AcRRyopUyIRX*iQ^?*D%h#)ppl>NrkdOcq;lH8 zl!HH*`RI2H&jql)psX?iS>v{-5;RK*mpqPk%<2_+)bP+K-!?Ktj4u*c;ms%;)uf=) zI@iEHGQWH2k^qe@23Z0rAE!C6NE_^-k+$^ez`jcix(MRS1u6p7n-OvmxiC*s8(KzC zQo1bL%;V|Tpm2$rkn0{sdBcSdxMF>mr#dT@Uz^^OGII}WHZ4@UCb=~hJEOJ~mhimZ zeeeVCBjoXryHS{dWLBs}t`gXq$3$(rC-}BM>lM-nGQxWnv4Ub6i5Pgzb|Ab+_^SNl zuN2q8oU*3YGwbPv)Kv?WCUextPA`l`!BM)m(*zlmz_5(8F;qkKtB;s%sco67I;9w=hbNKP)A4wPd%rjLnU^!!L` z%Ara1aYQsI5p9H#1SBK-1ryj}l#domGL8PD7BRtX$n)UJ^+emYURqbKI}-t>50IzN zWNz;TkN;es6jCLd`o;D=y2d^{_8l_g%G9(n)k|x2l9*lQo++#Wc|BQTnt|a2%kCP| ziX|Ppc!yR7H``wKGVW!;VsfgU+4rI~g|C_BGIFH4)C43|#r5%*$tQ^G%f-kvsGqgx zs0B>GWc_Fp%^h_wyl|vbevWet@JoX*j7Oj`>lz-#V$~O)8*RmJ*gNNy^&MeEDcVx-Oe5iDO_GRSrO86Bg zzeOv_#eCJt%s&{T+%|^F^yme>HZHN-P=Ng-w89&pY?>duV4=^m>zuo{6=f^h^xPe` z=3MdjQp%Y6M+s*>U>H0XfMM!1k_|tAQwLbvecsow4p|PUB%+Z1A)($U`CF!k_f-yRds1_T3hpgnVSZu_y2@bF#Q0_ zOcnJwbI;-!m<2k!{n#+@YAY1zWvpl_*;6^~@d|7=AQIQqo7Amet+ZO(ewd2_-rzG@ zU-e^f=7sjL_78K`axO_4n+@ESpSD{l!UEsH_P%H}g2$r&FL2Qlr@sWI{>K80i@tqVcuU(z z4RT2}&t`KIHH{3i%>@jrenHWrvwWHZvc2$~YUk&FVMt@R7E0V<68HzzvUs)mSw*~t zWxTe(hEHM8ZmAiuD4z*N4$CfB*{iUVw@Y5s=$ss0YVrWAM#!(b20~*swHJPGnbedz z^FTqlFVvGsHHLwAvS=9W+WZG0*e`mS7M$9^O{#mA!{jpyzf0@d_OAunLMS&k$}C#8 z=ZQgh;LzVssFHi&o1X?0;M{z}k6_>Z7GrOG8NSM;xCkf(Pu-%an?5yidXD031IHWb zGkS93S^BbR2|0}a`&qWV1h8$e@oq}=cX!7~$cO%Gj3gaQs2u<*vHA7cwac^tt1o=P zoWh36o8>fKhdK8*jjn0Y<9!*zLn~(w$IX>KCizpQm-H9tS153ai1@&>v1O+fkx$yB zof?*zut@$IR3xa!N6u#GZto!MjXx1qJroW=agzLiE5#~;_r@=G%go!k<&(T`tj+=t z&vEEDHH*VD3d2avz_AV!U-RZO0sn67S4(wglunjg*8|m`UCHv%{1lS?1r@GUFgAJG zb30dhQ!{YjF^F!Jj2sx7rU>Jjy-0Mso)*iudQ~-So{Tx}t$U}TcX#}z|h0<|g%(?>;D((KhEdVK`eOmkoXKqWozw+Ub%~mit{>td0 z+_N1O!}vc@3nTyLOact%6#Xj4B%`wA%`Lg{JKMB2l8^n6bjf#5O6QXQCmfW6T69H1 zzO)&-mPvs$`?jhZ(P{;NFf`ETl)}F-y$Ev;K#m~7alTl6fh{f!A7Z_1hwX*Od+ zfPqmgW^Cogp1GG#;b3!ZXkR-1OpktN8Z$bnRgU33txIe?{=i~D0MxQbhl*^O#{r1L z(W(3fKW(VK+$D_#cbW`DtR`8j6{Vt$`rghoZSZ!$b7R9%x@oYQG@7g9N?QEPX6a7# zKn7zqdAnmZ?;){Ci3n*O2c%dHLg8s=12rU<)}5&K54*u zN<7A&P-r=dSLHuESaL%umvA(YlapQ-k&Xca<;to_%Puq^YFH**ZLOP|1Td51QV&IN zld5B9VrJh+&EPm`14hIJ#H5AjBM`%K7Yks@GMoBdvB5&V{X%d{kMDsB{eU(EH}pMXr&5m@?9iF z&~In*o$)^lLTuxJLobxF+yqrm-hb4!+GGYat$;UUr@Dbd)NQxnz^4jZ7TYjh0zFq6 z;{vikdQjd%{h0(-7p)iF?CHTXrEnheEgi=gcJ+zCf}sodZ_uvwX591R}`hjhJ$7}#reQofM>WUM; zK&M~xR8wT*cc1<62G_KVr0WpUJEr`lq0A!F@-|B6N56{8lYZr8pcPszTpvHFjZO*o zLp}cV#is);kH-zf0f)o|$oP^``WM6czJAg%YL_R+#pM0Dv3}pA$}i=8Qg_e2(7~}! z!0N(Ng%FTJENW-kV^8i7uY_5=(uBidz%mDOkS!~;n%66*cy3T&G^>Smw-q6>YW`Al z=5<>n`(d_#cysI)ZO-DN4zGTi#S*ZX5~CD7A>0V5$vlmHLo0*)uJiM`JHt<<(A*>` zFakW8R6&Rkv^IX>>}5AjKVZ`h8~f~u0#1nG8+xDm_ibvqusjEDk@n~U3^+ZbBGj^v zyi~q@K;*KDdi^bOq!bN#o6A>ucU#cWP5 zpxo;Y+4!~Qy$iU~%JFk7c*k`A>aV8zjHSI6tz~L!bD7_#N)9YVR8(T;RRKW@`zvd>ej zoO$OiRf3-DL>Lt>>(lV21r>57u?n$$+lXAl^fnTzQJpj1);^W0Ad4_flNgyViwa9UPGJ^rn{phHmSuMlB@H22U z#`v@*Ll75LhkOhjF(_7A6AvZ*Uj(64StU@g?|k#&=n1Mv)zB*51YJfuk73CD-jvrm zt;VUyE$7}K{&J1HQUGk4w^J?+MY_=VlKa#Dx|47-X6(eHws$O?-U`6M(W`x|Nrt9n z$kkS9b=JzX0C(iMBVz3g6g360THw?ro{Pd;5C5k1dAY1*mhav#~e-7sxb`bAQglt1tfD z02n$VyYgn6*wPm)JJ|x?5fFp{z`i~y1bi7osl|kirGmsGkizJN5l>N@zH;MI*aUyd;{O)~Z&zNCTn?9`$RPrbQP*lgpJ*+Al7V3p z%z9#?)oz7<+2!Z0#!N{SZw~?VP%$2mI+<9c9-bx6o0JGd1SBg^hPm?X<9p{_MA}S*xE7NE@EO?rA$pcSdCRGA7F$n9g za?etWevgdHx(elx&%v=`b9GM%8Ol&Y*eOe|r8LUCN@+;IG76?|q&7H{5&d7aEYtyZ z*@WzK$czW81GdV)KCqLbA_vdR6(hspLH&IKpJ|qhemiUsBIEcKggn$jce^Rv#vyT* zwi4`EST~f<4FDjH;E=aPn(pS>hQ-6(Ly9!74pV$gmflGg?1PE;YxWmO>)5-L)w-F^ zOm!BoUojKEg)%yJr)iWAKC5G8r6hk0A5;Qt=^f(xfw%hidU)y6#{gKwF?sq!0Anwm z2(p&m3Etx^od+4faq|#*D)n(OJfx!d{iZ6*(zYHvRCc*7T+e-eCr6f^(i5Sp1b%@H zfeYPy{JYrtwbSi!8Vl_^@v@1m7mq2GW#GpP>9#wR?-hQ;VWjB1&l z-2`{-kaE&MD=oKOimSYHFOwD^HkC_p4#2`Az(eV%m9Dx=i_dEE32f;On->gtAM(@* zga`hbVZ9-ES8@BYc6M-9iI$$#Q=4QMg76|^GVr(^dfxW$C;uDme^=PQyXs#g0YcHg z==uL|#)w;2@-(2kA$|O3>C-GF&)x(Zc{+x3Pxt)&kEdq<5p?_@d1&{)i@}UA=3UBJ zdJ$gG2C&$hhW?*#QD~%9=Q7`}G3}*4rf?1h6675=z0)uL+pqfD8~@wS{{5JLgZA%A z`*+{`3pW2k=fC;p-|YD><@lFC?Xu2a4fT*FCi(3*`37C4fL`vQam>H`@$A~6?kq!H z@sWqGuZf3>pWorLZ)npD8Ak<KIG52o-EN` z+ql>NjUdzh`9|g0*803AwM9bLqU$ls?)x)<6)Rz|y;GPziFI?KOV_F=Q@?}rMd$Iq ze%(L6z4N*Dk2n69?W?Nt=W=2AeMxydK*fyWo;vsD>a#X^l9oY^Y3%=g+1Kp*Z;8+M z359AWYwuoMiUAn6wN=!tIrhq6v`MJ)4VZjY^*N@Y`j>e9`2`fdJyV$Y5t_R6IbP~I zbKsxf(FE{N2X#G0I(C**vfhPPOiHg=WPkn#(!Yn!=EUR9!i=@qF1~;M2KN_YuF@un z9y8sX{hz@HVZ46q@o+hMZ4Qg6etVsMH;4@V%)tDp`Ofv&&zNr|E)$j=-(RxZiM1&I z!zHLCD>NBor+@B3&Xj+1s zVcrwe`1g1HzAd-H6mo|vQoUBEr(_~$e!oOP3q;c5`Ai-Ix7mHLlvf7Qkh~S_-H`vR zpugS{+Dg9mxP8yTKR^Es__`9e7@$*?9k5pE;n;_uAztQ2` z1Mqg1lfs#=pX$CU3u4_I3xrIrw#l(1Yo{AK^=wf`+150By6^OM~d2gL%o+|+)*Bs~j+vh>i3FTIFhp8bFb zJ9#Ra-S8vvZ&&d9s?<;5x=^{-Y&yAv^wU~*UtwtuP+_`k&mM+uv-{<-X+ED~|JQ3^ zC=^+BB?zy&ef@J4h|s#2qK@6C_vX?^T`{^~Q6I9LsdIP#KgM8=-g_M{0FH~wb!6iN zN89XNK{)sgK4;Hn1}MCvuS~dpd%86tKIQTq!+(gtvyelD4j2RRe&?Ey;zaA+ZSsZ@ zY=PNVT-gfeJf5*OMK|F(lC40H^ZQ;N`sd{wsX*RZPt`@rb*8&aWO|JG{r)Jmr?f>$ zA9VS`n-hC0l1y?Jtp9O6ely%#@y;X{_vcTaS%Smmbv(AyW$A_w3co;mp8s_7pQ6u7 zcf?4?a2HC?$%6IQ#iD8a{3#n;RNgg6xN~qCt8P z_Rmgh*nL&X9k2+bCN#2+y)wHGh7Co)Xqk3_^v11Q|8OJEb%B{^{DYiH$DtCJ&m50s zc9*gZc=|>{@vBZwdKu<+{<8XTNSp99XWfg;f4B=q%gqZl zux=DhyZ0{h^0H2~y^$y_3s}Hx&|PryE$A$C z`{5|27o)-b?ELhj-Qu7Ec=8>YSAD4Ffy-Kfi4K>S`7U+s7Q0k6w{%J9>ad{oaeF)Z zTsA^|@?bY`TgQE6m%joh7Bs}&T^nAlf8E}A{r9e^+Nr5W?`M6!vFhsTr|h4%N9FrY zviTFN6`PJNV|@r$@bP<{`y+gRUG2*Be^N8{|9a;2wvAVst-kk6ZZhzsv*hBV+v2va z4KGCaUdv(8>^f}qC7KxU-Ura758_j?H5FjZ8)!f&T=bPk6T-&zm;ww>m)7o6&?*T_ z(7_!s;l(n3EcpOEDX4e>^WmaHuBq6X5NOHIqvSF>nh-Xw5U|x~9r$q3CUm>7I7emI zOJ*XJbpjh