UNPKG

12.9 kBJavaScriptView Raw
1"use strict";
2// Copyright IBM Corp. and LoopBack contributors 2019,2020. All Rights Reserved.
3// Node module: @loopback/context
4// This file is licensed under the MIT License.
5// License text available at https://opensource.org/licenses/MIT
6Object.defineProperty(exports, "__esModule", { value: true });
7exports.registerInterceptor = exports.invokeMethodWithInterceptors = exports.intercept = exports.INTERCEPT_CLASS_KEY = exports.mergeInterceptors = exports.INTERCEPT_METHOD_KEY = exports.globalInterceptor = exports.asGlobalInterceptor = exports.InterceptedInvocationContext = void 0;
8const tslib_1 = require("tslib");
9const metadata_1 = require("@loopback/metadata");
10const assert_1 = tslib_1.__importDefault(require("assert"));
11const debug_1 = tslib_1.__importDefault(require("debug"));
12const binding_decorator_1 = require("./binding-decorator");
13const binding_inspector_1 = require("./binding-inspector");
14const binding_key_1 = require("./binding-key");
15const binding_sorter_1 = require("./binding-sorter");
16const interceptor_chain_1 = require("./interceptor-chain");
17const invocation_1 = require("./invocation");
18const keys_1 = require("./keys");
19const value_promise_1 = require("./value-promise");
20const debug = (0, debug_1.default)('loopback:context:interceptor');
21/**
22 * A specialized InvocationContext for interceptors
23 */
24class InterceptedInvocationContext extends invocation_1.InvocationContext {
25 /**
26 * Discover all binding keys for global interceptors (tagged by
27 * ContextTags.GLOBAL_INTERCEPTOR)
28 */
29 getGlobalInterceptorBindingKeys() {
30 let bindings = this.findByTag(keys_1.ContextTags.GLOBAL_INTERCEPTOR);
31 bindings = bindings.filter(binding =>
32 // Only include interceptors that match the source type of the invocation
33 this.applicableTo(binding));
34 this.sortGlobalInterceptorBindings(bindings);
35 const keys = bindings.map(b => b.key);
36 debug('Global interceptor binding keys:', keys);
37 return keys;
38 }
39 /**
40 * Check if the binding for a global interceptor matches the source type
41 * of the invocation
42 * @param binding - Binding
43 */
44 applicableTo(binding) {
45 var _a;
46 const sourceType = (_a = this.source) === null || _a === void 0 ? void 0 : _a.type;
47 // Unknown source type, always apply
48 if (sourceType == null)
49 return true;
50 const allowedSource = binding.tagMap[keys_1.ContextTags.GLOBAL_INTERCEPTOR_SOURCE];
51 return (
52 // No tag, always apply
53 allowedSource == null ||
54 // source matched
55 allowedSource === sourceType ||
56 // source included in the string[]
57 (Array.isArray(allowedSource) && allowedSource.includes(sourceType)));
58 }
59 /**
60 * Sort global interceptor bindings by `globalInterceptorGroup` tags
61 * @param bindings - An array of global interceptor bindings
62 */
63 sortGlobalInterceptorBindings(bindings) {
64 var _a;
65 // Get predefined ordered groups for global interceptors
66 const orderedGroups = (_a = this.getSync(keys_1.ContextBindings.GLOBAL_INTERCEPTOR_ORDERED_GROUPS, {
67 optional: true,
68 })) !== null && _a !== void 0 ? _a : [];
69 return (0, binding_sorter_1.sortBindingsByPhase)(bindings, keys_1.ContextTags.GLOBAL_INTERCEPTOR_GROUP, orderedGroups);
70 }
71 /**
72 * Load all interceptors for the given invocation context. It adds
73 * interceptors from possibly three sources:
74 * 1. method level `@intercept`
75 * 2. class level `@intercept`
76 * 3. global interceptors discovered in the context
77 */
78 loadInterceptors() {
79 var _a, _b;
80 let interceptors = (_a = metadata_1.MetadataInspector.getMethodMetadata(exports.INTERCEPT_METHOD_KEY, this.target, this.methodName)) !== null && _a !== void 0 ? _a : [];
81 const targetClass = typeof this.target === 'function' ? this.target : this.target.constructor;
82 const classInterceptors = (_b = metadata_1.MetadataInspector.getClassMetadata(exports.INTERCEPT_CLASS_KEY, targetClass)) !== null && _b !== void 0 ? _b : [];
83 // Inserting class level interceptors before method level ones
84 interceptors = mergeInterceptors(classInterceptors, interceptors);
85 const globalInterceptors = this.getGlobalInterceptorBindingKeys();
86 // Inserting global interceptors
87 interceptors = mergeInterceptors(globalInterceptors, interceptors);
88 debug('Interceptors for %s', this.targetName, interceptors);
89 return interceptors;
90 }
91}
92exports.InterceptedInvocationContext = InterceptedInvocationContext;
93/**
94 * The `BindingTemplate` function to configure a binding as a global interceptor
95 * by tagging it with `ContextTags.INTERCEPTOR`
96 * @param group - Group for ordering the interceptor
97 */
98function asGlobalInterceptor(group) {
99 return binding => {
100 binding
101 // Tagging with `GLOBAL_INTERCEPTOR` is required.
102 .tag(keys_1.ContextTags.GLOBAL_INTERCEPTOR)
103 // `GLOBAL_INTERCEPTOR_NAMESPACE` is to make the binding key more readable.
104 .tag({ [keys_1.ContextTags.NAMESPACE]: keys_1.GLOBAL_INTERCEPTOR_NAMESPACE });
105 if (group)
106 binding.tag({ [keys_1.ContextTags.GLOBAL_INTERCEPTOR_GROUP]: group });
107 };
108}
109exports.asGlobalInterceptor = asGlobalInterceptor;
110/**
111 * `@globalInterceptor` decorator to mark the class as a global interceptor
112 * @param group - Group for ordering the interceptor
113 * @param specs - Extra binding specs
114 */
115function globalInterceptor(group, ...specs) {
116 return (0, binding_decorator_1.injectable)(asGlobalInterceptor(group), ...specs);
117}
118exports.globalInterceptor = globalInterceptor;
119/**
120 * Metadata key for method-level interceptors
121 */
122exports.INTERCEPT_METHOD_KEY = metadata_1.MetadataAccessor.create('intercept:method');
123/**
124 * Adding interceptors from the spec to the front of existing ones. Duplicate
125 * entries are eliminated from the spec side.
126 *
127 * For example:
128 *
129 * - [log] + [cache, log] => [cache, log]
130 * - [log] + [log, cache] => [log, cache]
131 * - [] + [cache, log] => [cache, log]
132 * - [cache, log] + [] => [cache, log]
133 * - [log] + [cache] => [log, cache]
134 *
135 * @param interceptorsFromSpec - Interceptors from `@intercept`
136 * @param existingInterceptors - Interceptors already applied for the method
137 */
138function mergeInterceptors(interceptorsFromSpec, existingInterceptors) {
139 const interceptorsToApply = new Set(interceptorsFromSpec);
140 const appliedInterceptors = new Set(existingInterceptors);
141 // Remove interceptors that already exist
142 for (const i of interceptorsToApply) {
143 if (appliedInterceptors.has(i)) {
144 interceptorsToApply.delete(i);
145 }
146 }
147 // Add existing interceptors after ones from the spec
148 for (const i of appliedInterceptors) {
149 interceptorsToApply.add(i);
150 }
151 return Array.from(interceptorsToApply);
152}
153exports.mergeInterceptors = mergeInterceptors;
154/**
155 * Metadata key for method-level interceptors
156 */
157exports.INTERCEPT_CLASS_KEY = metadata_1.MetadataAccessor.create('intercept:class');
158/**
159 * A factory to define `@intercept` for classes. It allows `@intercept` to be
160 * used multiple times on the same class.
161 */
162class InterceptClassDecoratorFactory extends metadata_1.ClassDecoratorFactory {
163 mergeWithOwn(ownMetadata, target) {
164 ownMetadata = ownMetadata || [];
165 return mergeInterceptors(this.spec, ownMetadata);
166 }
167}
168/**
169 * A factory to define `@intercept` for methods. It allows `@intercept` to be
170 * used multiple times on the same method.
171 */
172class InterceptMethodDecoratorFactory extends metadata_1.MethodDecoratorFactory {
173 mergeWithOwn(ownMetadata, target, methodName, methodDescriptor) {
174 ownMetadata = ownMetadata || {};
175 const interceptors = ownMetadata[methodName] || [];
176 // Adding interceptors to the list
177 ownMetadata[methodName] = mergeInterceptors(this.spec, interceptors);
178 return ownMetadata;
179 }
180}
181/**
182 * Decorator function `@intercept` for classes/methods to apply interceptors. It
183 * can be applied on a class and its public methods. Multiple occurrences of
184 * `@intercept` are allowed on the same target class or method. The decorator
185 * takes a list of `interceptor` functions or binding keys.
186 *
187 * @example
188 * ```ts
189 * @intercept(log, metrics)
190 * class MyController {
191 * @intercept('caching-interceptor')
192 * @intercept('name-validation-interceptor')
193 * greet(name: string) {
194 * return `Hello, ${name}`;
195 * }
196 * }
197 * ```
198 *
199 * @param interceptorOrKeys - One or more interceptors or binding keys that are
200 * resolved to be interceptors
201 */
202function intercept(...interceptorOrKeys) {
203 return function interceptDecoratorForClassOrMethod(
204 // Class or a prototype
205 // eslint-disable-next-line @typescript-eslint/no-explicit-any
206 target, method,
207 // Use `any` to for `TypedPropertyDescriptor`
208 // See https://github.com/loopbackio/loopback-next/pull/2704
209 // eslint-disable-next-line @typescript-eslint/no-explicit-any
210 methodDescriptor) {
211 if (method && methodDescriptor) {
212 // Method
213 return InterceptMethodDecoratorFactory.createDecorator(exports.INTERCEPT_METHOD_KEY, interceptorOrKeys, { decoratorName: '@intercept' })(target, method, methodDescriptor);
214 }
215 if (typeof target === 'function' && !method && !methodDescriptor) {
216 // Class
217 return InterceptClassDecoratorFactory.createDecorator(exports.INTERCEPT_CLASS_KEY, interceptorOrKeys, { decoratorName: '@intercept' })(target);
218 }
219 // Not on a class or method
220 throw new Error('@intercept cannot be used on a property: ' +
221 metadata_1.DecoratorFactory.getTargetName(target, method, methodDescriptor));
222 };
223}
224exports.intercept = intercept;
225/**
226 * Invoke a method with the given context
227 * @param context - Context object
228 * @param target - Target class (for static methods) or object (for instance methods)
229 * @param methodName - Method name
230 * @param args - An array of argument values
231 * @param options - Options for the invocation
232 */
233function invokeMethodWithInterceptors(context, target, methodName, args, options = {}) {
234 // Do not allow `skipInterceptors` as it's against the function name
235 // `invokeMethodWithInterceptors`
236 (0, assert_1.default)(!options.skipInterceptors, 'skipInterceptors is not allowed');
237 const invocationCtx = new InterceptedInvocationContext(context, target, methodName, args, options.source);
238 invocationCtx.assertMethodExists();
239 return (0, value_promise_1.tryWithFinally)(() => {
240 const interceptors = invocationCtx.loadInterceptors();
241 const targetMethodInvoker = () => invocationCtx.invokeTargetMethod(options);
242 interceptors.push(targetMethodInvoker);
243 return (0, interceptor_chain_1.invokeInterceptors)(invocationCtx, interceptors);
244 }, () => invocationCtx.close());
245}
246exports.invokeMethodWithInterceptors = invokeMethodWithInterceptors;
247/**
248 * Register an interceptor function or provider class to the given context
249 * @param ctx - Context object
250 * @param interceptor - An interceptor function or provider class
251 * @param options - Options for the interceptor binding
252 */
253function registerInterceptor(ctx, interceptor, options = {}) {
254 var _a, _b, _c;
255 let { global } = options;
256 const { group, source } = options;
257 if (group != null || source != null) {
258 // If group or source is set, assuming global
259 global = global !== false;
260 }
261 const namespace = ((_b = (_a = options.namespace) !== null && _a !== void 0 ? _a : options.defaultNamespace) !== null && _b !== void 0 ? _b : global)
262 ? keys_1.GLOBAL_INTERCEPTOR_NAMESPACE
263 : keys_1.LOCAL_INTERCEPTOR_NAMESPACE;
264 let binding;
265 if ((0, binding_inspector_1.isProviderClass)(interceptor)) {
266 binding = (0, binding_inspector_1.createBindingFromClass)(interceptor, {
267 defaultNamespace: namespace,
268 ...options,
269 });
270 if (binding.tagMap[keys_1.ContextTags.GLOBAL_INTERCEPTOR]) {
271 global = true;
272 }
273 ctx.add(binding);
274 }
275 else {
276 let key = options.key;
277 if (!key) {
278 const name = (_c = options.name) !== null && _c !== void 0 ? _c : interceptor.name;
279 if (!name) {
280 key = binding_key_1.BindingKey.generate(namespace).key;
281 }
282 else {
283 key = `${namespace}.${name}`;
284 }
285 }
286 binding = ctx
287 .bind(key)
288 .to(interceptor);
289 }
290 if (global) {
291 binding.apply(asGlobalInterceptor(group));
292 if (source) {
293 binding.tag({ [keys_1.ContextTags.GLOBAL_INTERCEPTOR_SOURCE]: source });
294 }
295 }
296 return binding;
297}
298exports.registerInterceptor = registerInterceptor;
299//# sourceMappingURL=interceptor.js.map
\No newline at end of file