UNPKG

10.3 kBPlain TextView Raw
1// Copyright IBM Corp. and LoopBack contributors 2018,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 {MetadataAccessor, MetadataInspector} from '@loopback/metadata';
7import debugFactory from 'debug';
8import {
9 Binding,
10 BindingScope,
11 BindingTag,
12 BindingTemplate,
13 DynamicValueProviderClass,
14 isDynamicValueProviderClass,
15} from './binding';
16import {BindingAddress} from './binding-key';
17import {ContextTags} from './keys';
18import {Provider} from './provider';
19import {Constructor} from './value-promise';
20
21const debug = debugFactory('loopback:context:binding-inspector');
22
23/**
24 * Binding metadata from `@injectable`
25 *
26 * @typeParam T - Value type
27 */
28export type BindingMetadata<T = unknown> = {
29 /**
30 * An array of template functions to configure a binding
31 */
32 templates: BindingTemplate<T>[];
33 /**
34 * The target class where binding metadata is decorated
35 */
36 target: Constructor<T>;
37};
38
39/**
40 * Metadata key for binding metadata
41 */
42export const BINDING_METADATA_KEY = MetadataAccessor.create<
43 BindingMetadata,
44 ClassDecorator
45>('binding.metadata');
46
47/**
48 * An object to configure binding scope and tags
49 */
50export type BindingScopeAndTags = {
51 scope?: BindingScope;
52 tags?: BindingTag | BindingTag[];
53};
54
55/**
56 * Specification of parameters for `@injectable()`
57 */
58export type BindingSpec<T = unknown> = BindingTemplate<T> | BindingScopeAndTags;
59
60/**
61 * Check if a class implements `Provider` interface
62 * @param cls - A class
63 *
64 * @typeParam T - Value type
65 */
66export function isProviderClass<T>(
67 cls: unknown,
68): cls is Constructor<Provider<T>> {
69 return (
70 typeof cls === 'function' && typeof cls.prototype?.value === 'function'
71 );
72}
73
74/**
75 * A factory function to create a template function to bind the target class
76 * as a `Provider`.
77 * @param target - Target provider class
78 *
79 * @typeParam T - Value type
80 */
81export function asProvider<T>(
82 target: Constructor<Provider<T>>,
83): BindingTemplate<T> {
84 return function bindAsProvider(binding) {
85 binding.toProvider(target).tag(ContextTags.PROVIDER, {
86 [ContextTags.TYPE]: ContextTags.PROVIDER,
87 });
88 };
89}
90
91/**
92 * A factory function to create a template function to bind the target class
93 * as a class or `Provider`.
94 * @param target - Target class, which can be an implementation of `Provider`
95 * or `DynamicValueProviderClass`
96 *
97 * @typeParam T - Value type
98 */
99export function asClassOrProvider<T>(
100 target: Constructor<T | Provider<T>> | DynamicValueProviderClass<T>,
101): BindingTemplate<T> {
102 // Add a template to bind to a class or provider
103 return function bindAsClassOrProvider(binding) {
104 if (isProviderClass(target)) {
105 asProvider(target)(binding);
106 } else if (isDynamicValueProviderClass<T>(target)) {
107 binding.toDynamicValue(target).tag(ContextTags.DYNAMIC_VALUE_PROVIDER, {
108 [ContextTags.TYPE]: ContextTags.DYNAMIC_VALUE_PROVIDER,
109 });
110 } else {
111 binding.toClass(target as Constructor<T & object>);
112 }
113 };
114}
115
116/**
117 * Convert binding scope and tags as a template function
118 * @param scopeAndTags - Binding scope and tags
119 *
120 * @typeParam T - Value type
121 */
122export function asBindingTemplate<T = unknown>(
123 scopeAndTags: BindingScopeAndTags,
124): BindingTemplate<T> {
125 return function applyBindingScopeAndTag(binding) {
126 if (scopeAndTags.scope) {
127 binding.inScope(scopeAndTags.scope);
128 }
129 if (scopeAndTags.tags) {
130 if (Array.isArray(scopeAndTags.tags)) {
131 binding.tag(...scopeAndTags.tags);
132 } else {
133 binding.tag(scopeAndTags.tags);
134 }
135 }
136 };
137}
138
139/**
140 * Get binding metadata for a class
141 * @param target - The target class
142 *
143 * @typeParam T - Value type
144 */
145export function getBindingMetadata<T = unknown>(
146 target: Function,
147): BindingMetadata<T> | undefined {
148 return MetadataInspector.getClassMetadata<BindingMetadata<T>>(
149 BINDING_METADATA_KEY,
150 target,
151 );
152}
153
154/**
155 * A binding template function to delete `name` and `key` tags
156 */
157export function removeNameAndKeyTags(binding: Binding<unknown>) {
158 if (binding.tagMap) {
159 delete binding.tagMap.name;
160 delete binding.tagMap.key;
161 }
162}
163
164/**
165 * Get the binding template for a class with binding metadata
166 *
167 * @param cls - A class with optional `@injectable`
168 *
169 * @typeParam T - Value type
170 */
171export function bindingTemplateFor<T>(
172 cls: Constructor<T | Provider<T>> | DynamicValueProviderClass<T>,
173 options?: BindingFromClassOptions,
174): BindingTemplate<T> {
175 const spec = getBindingMetadata(cls);
176 debug('class %s has binding metadata', cls.name, spec);
177 const templateFunctions = spec?.templates ?? [];
178 if (spec?.target !== cls) {
179 // Make sure the subclass is used as the binding source
180 templateFunctions.push(asClassOrProvider(cls) as BindingTemplate<unknown>);
181 }
182 return function applyBindingTemplatesFromMetadata(binding) {
183 for (const t of templateFunctions) {
184 binding.apply(t);
185 }
186 if (spec?.target !== cls) {
187 // Remove name/key tags inherited from base classes
188 binding.apply(removeNameAndKeyTags);
189 }
190 if (options != null) {
191 applyClassBindingOptions(binding, options);
192 }
193 };
194}
195
196/**
197 * Mapping artifact types to binding key namespaces (prefixes).
198 *
199 * @example
200 * ```ts
201 * {
202 * repository: 'repositories'
203 * }
204 * ```
205 */
206export type TypeNamespaceMapping = {[name: string]: string};
207
208export const DEFAULT_TYPE_NAMESPACES: TypeNamespaceMapping = {
209 class: 'classes',
210 provider: 'providers',
211 dynamicValueProvider: 'dynamicValueProviders',
212};
213
214/**
215 * Options to customize the binding created from a class
216 */
217export type BindingFromClassOptions = {
218 /**
219 * Binding key
220 */
221 key?: BindingAddress;
222 /**
223 * Artifact type, such as `server`, `controller`, `repository` or `service`
224 */
225 type?: string;
226 /**
227 * Artifact name, such as `my-rest-server` and `my-controller`. It
228 * overrides the name tag
229 */
230 name?: string;
231 /**
232 * Namespace for the binding key, such as `servers` and `controllers`. It
233 * overrides the default namespace or namespace tag
234 */
235 namespace?: string;
236 /**
237 * Mapping artifact type to binding key namespaces
238 */
239 typeNamespaceMapping?: TypeNamespaceMapping;
240 /**
241 * Default namespace if the binding does not have an explicit namespace
242 */
243 defaultNamespace?: string;
244 /**
245 * Default scope if the binding does not have an explicit scope
246 */
247 defaultScope?: BindingScope;
248};
249
250/**
251 * Create a binding from a class with decorated metadata. The class is attached
252 * to the binding as follows:
253 * - `binding.toClass(cls)`: if `cls` is a plain class such as `MyController`
254 * - `binding.toProvider(cls)`: if `cls` is a value provider class with a
255 * prototype method `value()`
256 * - `binding.toDynamicValue(cls)`: if `cls` is a dynamic value provider class
257 * with a static method `value()`
258 *
259 * @param cls - A class. It can be either a plain class, a value provider class,
260 * or a dynamic value provider class
261 * @param options - Options to customize the binding key
262 *
263 * @typeParam T - Value type
264 */
265export function createBindingFromClass<T>(
266 cls: Constructor<T | Provider<T>> | DynamicValueProviderClass<T>,
267 options: BindingFromClassOptions = {},
268): Binding<T> {
269 debug('create binding from class %s with options', cls.name, options);
270 try {
271 const templateFn = bindingTemplateFor(cls, options);
272 const key = buildBindingKey(cls, options);
273 const binding = Binding.bind<T>(key).apply(templateFn);
274 return binding;
275 } catch (err) {
276 err.message += ` (while building binding for class ${cls.name})`;
277 throw err;
278 }
279}
280
281function applyClassBindingOptions<T>(
282 binding: Binding<T>,
283 options: BindingFromClassOptions,
284) {
285 if (options.name) {
286 binding.tag({name: options.name});
287 }
288 if (options.type) {
289 binding.tag({type: options.type}, options.type);
290 }
291 if (options.defaultScope) {
292 binding.applyDefaultScope(options.defaultScope);
293 }
294}
295
296/**
297 * Find/infer binding key namespace for a type
298 * @param type - Artifact type, such as `controller`, `datasource`, or `server`
299 * @param typeNamespaces - An object mapping type names to namespaces
300 */
301function getNamespace(type: string, typeNamespaces = DEFAULT_TYPE_NAMESPACES) {
302 if (type in typeNamespaces) {
303 return typeNamespaces[type];
304 } else {
305 // Return the plural form
306 return `${type}s`;
307 }
308}
309
310/**
311 * Build the binding key for a class with optional binding metadata.
312 * The binding key is resolved in the following steps:
313 *
314 * 1. Check `options.key`, if it exists, return it
315 * 2. Check if the binding metadata has `key` tag, if yes, return its tag value
316 * 3. Identify `namespace` and `name` to form the binding key as
317 * `<namespace>.<name>`.
318 * - namespace
319 * - `options.namespace`
320 * - `namespace` tag value
321 * - Map `options.type` or `type` tag value to a namespace, for example,
322 * 'controller` to 'controller'.
323 * - name
324 * - `options.name`
325 * - `name` tag value
326 * - the class name
327 *
328 * @param cls - A class to be bound
329 * @param options - Options to customize how to build the key
330 *
331 * @typeParam T - Value type
332 */
333function buildBindingKey<T>(
334 cls: Constructor<T | Provider<T>>,
335 options: BindingFromClassOptions = {},
336) {
337 if (options.key) return options.key;
338
339 const templateFn = bindingTemplateFor(cls);
340 // Create a temporary binding
341 const bindingTemplate = new Binding('template').apply(templateFn);
342 // Is there a `key` tag?
343 let key: string = bindingTemplate.tagMap[ContextTags.KEY];
344 if (key) return key;
345
346 let namespace =
347 options.namespace ??
348 bindingTemplate.tagMap[ContextTags.NAMESPACE] ??
349 options.defaultNamespace;
350 if (!namespace) {
351 const namespaces = Object.assign(
352 {},
353 DEFAULT_TYPE_NAMESPACES,
354 options.typeNamespaceMapping,
355 );
356 // Derive the key from type + name
357 let type = options.type ?? bindingTemplate.tagMap[ContextTags.TYPE];
358 if (!type) {
359 type =
360 bindingTemplate.tagNames.find(t => namespaces[t] != null) ??
361 ContextTags.CLASS;
362 }
363 namespace = getNamespace(type, namespaces);
364 }
365
366 const name =
367 options.name ?? (bindingTemplate.tagMap[ContextTags.NAME] || cls.name);
368 key = `${namespace}.${name}`;
369
370 return key;
371}