UNPKG

8.15 kBPlain TextView Raw
1// Copyright IBM Corp. and LoopBack contributors 2019,2020. All Rights Reserved.
2// Node module: @loopback/context
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6import debugFactory from 'debug';
7import {BindingFilter} from './binding-filter';
8import {BindingAddress} from './binding-key';
9import {BindingComparator} from './binding-sorter';
10import {Context} from './context';
11import {InvocationResult} from './invocation';
12import {transformValueOrPromise, ValueOrPromise} from './value-promise';
13const debug = debugFactory('loopback:context:interceptor-chain');
14
15/**
16 * Any type except `void`. We use this type to enforce that interceptor functions
17 * always return a value (including undefined or null).
18 */
19export type NonVoid = string | number | boolean | null | undefined | object;
20
21/**
22 * The `next` function that can be used to invoke next generic interceptor in
23 * the chain
24 */
25export type Next = () => ValueOrPromise<NonVoid>;
26
27/**
28 * An interceptor function to be invoked in a chain for the given context.
29 * It serves as the base interface for various types of interceptors, such
30 * as method invocation interceptor or request/response processing interceptor.
31 *
32 * @remarks
33 * We choose `NonVoid` as the return type to avoid possible bugs that an
34 * interceptor forgets to return the value from `next()`. For example, the code
35 * below will fail to compile.
36 *
37 * ```ts
38 * const myInterceptor: Interceptor = async (ctx, next) {
39 * // preprocessing
40 * // ...
41 *
42 * // There is a subtle bug that the result from `next()` is not further
43 * // returned back to the upstream interceptors
44 * const result = await next();
45 *
46 * // postprocessing
47 * // ...
48 * // We must have `return ...` here
49 * // either return `result` or another value if the interceptor decides to
50 * // have its own response
51 * }
52 * ```
53 *
54 * @typeParam C - `Context` class or a subclass of `Context`
55 * @param context - Context object
56 * @param next - A function to proceed with downstream interceptors or the
57 * target operation
58 *
59 * @returns The invocation result as a value (sync) or promise (async).
60 */
61export type GenericInterceptor<C extends Context = Context> = (
62 context: C,
63 next: Next,
64) => ValueOrPromise<NonVoid>;
65
66/**
67 * Interceptor function or a binding key that resolves a generic interceptor
68 * function
69 * @typeParam C - `Context` class or a subclass of `Context`
70 * @typeParam T - Return type of `next()`
71 */
72export type GenericInterceptorOrKey<C extends Context = Context> =
73 | BindingAddress<GenericInterceptor<C>>
74 | GenericInterceptor<C>;
75
76/**
77 * Invocation state of an interceptor chain
78 */
79class InterceptorChainState<C extends Context = Context> {
80 private _index = 0;
81 /**
82 * Create a state for the interceptor chain
83 * @param interceptors - Interceptor functions or binding keys
84 * @param finalHandler - An optional final handler
85 */
86 constructor(
87 public readonly interceptors: GenericInterceptorOrKey<C>[],
88 public readonly finalHandler: Next = () => undefined,
89 ) {}
90
91 /**
92 * Get the index for the current interceptor
93 */
94 get index() {
95 return this._index;
96 }
97
98 /**
99 * Check if the chain is done - all interceptors are invoked
100 */
101 done() {
102 return this._index === this.interceptors.length;
103 }
104
105 /**
106 * Get the next interceptor to be invoked
107 */
108 next() {
109 if (this.done()) {
110 throw new Error('No more interceptor is in the chain');
111 }
112 return this.interceptors[this._index++];
113 }
114}
115
116/**
117 * A chain of generic interceptors to be invoked for the given context
118 *
119 * @typeParam C - `Context` class or a subclass of `Context`
120 */
121export class GenericInterceptorChain<C extends Context = Context> {
122 /**
123 * A getter for an array of interceptor functions or binding keys
124 */
125 protected getInterceptors: () => GenericInterceptorOrKey<C>[];
126
127 /**
128 * Create an invocation chain with a list of interceptor functions or
129 * binding keys
130 * @param context - Context object
131 * @param interceptors - An array of interceptor functions or binding keys
132 */
133 constructor(context: C, interceptors: GenericInterceptorOrKey<C>[]);
134
135 /**
136 * Create an invocation interceptor chain with a binding filter and comparator.
137 * The interceptors are discovered from the context using the binding filter and
138 * sorted by the comparator (if provided).
139 *
140 * @param context - Context object
141 * @param filter - A binding filter function to select interceptors
142 * @param comparator - An optional comparator to sort matched interceptor bindings
143 */
144 constructor(
145 context: C,
146 filter: BindingFilter,
147 comparator?: BindingComparator,
148 );
149
150 // Implementation
151 constructor(
152 private context: C,
153 interceptors: GenericInterceptorOrKey<C>[] | BindingFilter,
154 comparator?: BindingComparator,
155 ) {
156 if (typeof interceptors === 'function') {
157 const interceptorsView = context.createView(interceptors, comparator);
158 this.getInterceptors = () => {
159 const bindings = interceptorsView.bindings;
160 if (comparator) {
161 bindings.sort(comparator);
162 }
163 return bindings.map(b => b.key);
164 };
165 } else if (Array.isArray(interceptors)) {
166 this.getInterceptors = () => interceptors;
167 }
168 }
169
170 /**
171 * Invoke the interceptor chain
172 */
173 invokeInterceptors(finalHandler?: Next): ValueOrPromise<InvocationResult> {
174 // Create a state for each invocation to provide isolation
175 const state = new InterceptorChainState<C>(
176 this.getInterceptors(),
177 finalHandler,
178 );
179 return this.next(state);
180 }
181
182 /**
183 * Use the interceptor chain as an interceptor
184 */
185 asInterceptor(): GenericInterceptor<C> {
186 return (ctx, next) => {
187 return this.invokeInterceptors(next);
188 };
189 }
190
191 /**
192 * Invoke downstream interceptors or the target method
193 */
194 private next(
195 state: InterceptorChainState<C>,
196 ): ValueOrPromise<InvocationResult> {
197 if (state.done()) {
198 // No more interceptors
199 return state.finalHandler();
200 }
201 // Invoke the next interceptor in the chain
202 return this.invokeNextInterceptor(state);
203 }
204
205 /**
206 * Invoke downstream interceptors
207 */
208 private invokeNextInterceptor(
209 state: InterceptorChainState<C>,
210 ): ValueOrPromise<InvocationResult> {
211 const index = state.index;
212 const interceptor = state.next();
213 const interceptorFn = this.loadInterceptor(interceptor);
214 return transformValueOrPromise(interceptorFn, fn => {
215 /* istanbul ignore if */
216 if (debug.enabled) {
217 debug('Invoking interceptor %d (%s) on %s', index, fn.name);
218 }
219 return fn(this.context, () => this.next(state));
220 });
221 }
222
223 /**
224 * Return the interceptor function or resolve the interceptor function as a binding
225 * from the context
226 *
227 * @param interceptor - Interceptor function or binding key
228 */
229 private loadInterceptor(interceptor: GenericInterceptorOrKey<C>) {
230 if (typeof interceptor === 'function') return interceptor;
231 debug('Resolving interceptor binding %s', interceptor);
232 return this.context.getValueOrPromise(interceptor) as ValueOrPromise<
233 GenericInterceptor<C>
234 >;
235 }
236}
237
238/**
239 * Invoke a chain of interceptors with the context
240 * @param context - Context object
241 * @param interceptors - An array of interceptor functions or binding keys
242 */
243export function invokeInterceptors<
244 C extends Context = Context,
245 T = InvocationResult,
246>(
247 context: C,
248 interceptors: GenericInterceptorOrKey<C>[],
249): ValueOrPromise<T | undefined> {
250 const chain = new GenericInterceptorChain(context, interceptors);
251 return chain.invokeInterceptors();
252}
253
254/**
255 * Compose a list of interceptors as a single interceptor
256 * @param interceptors - A list of interceptor functions or binding keys
257 */
258export function composeInterceptors<C extends Context = Context>(
259 ...interceptors: GenericInterceptorOrKey<C>[]
260): GenericInterceptor<C> {
261 return (ctx, next) => {
262 const interceptor = new GenericInterceptorChain(
263 ctx,
264 interceptors,
265 ).asInterceptor();
266 return interceptor(ctx, next);
267 };
268}