UNPKG

10.4 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 // Clone the templates array to avoid updating the cached metadata
178 const templateFunctions = [...(spec?.templates ?? [])];
179 if (spec?.target !== cls) {
180 // Make sure the subclass is used as the binding source
181 templateFunctions.push(asClassOrProvider(cls) as BindingTemplate<unknown>);
182 }
183 return function applyBindingTemplatesFromMetadata(binding) {
184 for (const t of templateFunctions) {
185 binding.apply(t);
186 }
187 if (spec?.target !== cls) {
188 // Remove name/key tags inherited from base classes
189 binding.apply(removeNameAndKeyTags);
190 }
191 if (options != null) {
192 applyClassBindingOptions(binding, options);
193 }
194 };
195}
196
197/**
198 * Mapping artifact types to binding key namespaces (prefixes).
199 *
200 * @example
201 * ```ts
202 * {
203 * repository: 'repositories'
204 * }
205 * ```
206 */
207export type TypeNamespaceMapping = {[name: string]: string};
208
209export const DEFAULT_TYPE_NAMESPACES: TypeNamespaceMapping = {
210 class: 'classes',
211 provider: 'providers',
212 dynamicValueProvider: 'dynamicValueProviders',
213};
214
215/**
216 * Options to customize the binding created from a class
217 */
218export type BindingFromClassOptions = {
219 /**
220 * Binding key
221 */
222 key?: BindingAddress;
223 /**
224 * Artifact type, such as `server`, `controller`, `repository` or `service`
225 */
226 type?: string;
227 /**
228 * Artifact name, such as `my-rest-server` and `my-controller`. It
229 * overrides the name tag
230 */
231 name?: string;
232 /**
233 * Namespace for the binding key, such as `servers` and `controllers`. It
234 * overrides the default namespace or namespace tag
235 */
236 namespace?: string;
237 /**
238 * Mapping artifact type to binding key namespaces
239 */
240 typeNamespaceMapping?: TypeNamespaceMapping;
241 /**
242 * Default namespace if the binding does not have an explicit namespace
243 */
244 defaultNamespace?: string;
245 /**
246 * Default scope if the binding does not have an explicit scope
247 */
248 defaultScope?: BindingScope;
249};
250
251/**
252 * Create a binding from a class with decorated metadata. The class is attached
253 * to the binding as follows:
254 * - `binding.toClass(cls)`: if `cls` is a plain class such as `MyController`
255 * - `binding.toProvider(cls)`: if `cls` is a value provider class with a
256 * prototype method `value()`
257 * - `binding.toDynamicValue(cls)`: if `cls` is a dynamic value provider class
258 * with a static method `value()`
259 *
260 * @param cls - A class. It can be either a plain class, a value provider class,
261 * or a dynamic value provider class
262 * @param options - Options to customize the binding key
263 *
264 * @typeParam T - Value type
265 */
266export function createBindingFromClass<T>(
267 cls: Constructor<T | Provider<T>> | DynamicValueProviderClass<T>,
268 options: BindingFromClassOptions = {},
269): Binding<T> {
270 debug('create binding from class %s with options', cls.name, options);
271 try {
272 const templateFn = bindingTemplateFor(cls, options);
273 const key = buildBindingKey(cls, options);
274 const binding = Binding.bind<T>(key).apply(templateFn);
275 return binding;
276 } catch (err) {
277 err.message += ` (while building binding for class ${cls.name})`;
278 throw err;
279 }
280}
281
282function applyClassBindingOptions<T>(
283 binding: Binding<T>,
284 options: BindingFromClassOptions,
285) {
286 if (options.name) {
287 binding.tag({name: options.name});
288 }
289 if (options.type) {
290 binding.tag({type: options.type}, options.type);
291 }
292 if (options.defaultScope) {
293 binding.applyDefaultScope(options.defaultScope);
294 }
295}
296
297/**
298 * Find/infer binding key namespace for a type
299 * @param type - Artifact type, such as `controller`, `datasource`, or `server`
300 * @param typeNamespaces - An object mapping type names to namespaces
301 */
302function getNamespace(type: string, typeNamespaces = DEFAULT_TYPE_NAMESPACES) {
303 if (type in typeNamespaces) {
304 return typeNamespaces[type];
305 } else {
306 // Return the plural form
307 return `${type}s`;
308 }
309}
310
311/**
312 * Build the binding key for a class with optional binding metadata.
313 * The binding key is resolved in the following steps:
314 *
315 * 1. Check `options.key`, if it exists, return it
316 * 2. Check if the binding metadata has `key` tag, if yes, return its tag value
317 * 3. Identify `namespace` and `name` to form the binding key as
318 * `<namespace>.<name>`.
319 * - namespace
320 * - `options.namespace`
321 * - `namespace` tag value
322 * - Map `options.type` or `type` tag value to a namespace, for example,
323 * 'controller` to 'controller'.
324 * - name
325 * - `options.name`
326 * - `name` tag value
327 * - the class name
328 *
329 * @param cls - A class to be bound
330 * @param options - Options to customize how to build the key
331 *
332 * @typeParam T - Value type
333 */
334function buildBindingKey<T>(
335 cls: Constructor<T | Provider<T>>,
336 options: BindingFromClassOptions = {},
337) {
338 if (options.key) return options.key;
339
340 const templateFn = bindingTemplateFor(cls);
341 // Create a temporary binding
342 const bindingTemplate = new Binding('template').apply(templateFn);
343 // Is there a `key` tag?
344 let key: string = bindingTemplate.tagMap[ContextTags.KEY];
345 if (key) return key;
346
347 let namespace =
348 options.namespace ??
349 bindingTemplate.tagMap[ContextTags.NAMESPACE] ??
350 options.defaultNamespace;
351 if (!namespace) {
352 const namespaces = Object.assign(
353 {},
354 DEFAULT_TYPE_NAMESPACES,
355 options.typeNamespaceMapping,
356 );
357 // Derive the key from type + name
358 let type = options.type ?? bindingTemplate.tagMap[ContextTags.TYPE];
359 if (!type) {
360 type =
361 bindingTemplate.tagNames.find(t => namespaces[t] != null) ??
362 ContextTags.CLASS;
363 }
364 namespace = getNamespace(type, namespaces);
365 }
366
367 const name =
368 options.name ?? (bindingTemplate.tagMap[ContextTags.NAME] || cls.name);
369 key = `${namespace}.${name}`;
370
371 return key;
372}