1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | import {MetadataAccessor, MetadataInspector} from '@loopback/metadata';
|
7 | import debugFactory from 'debug';
|
8 | import {
|
9 | Binding,
|
10 | BindingScope,
|
11 | BindingTag,
|
12 | BindingTemplate,
|
13 | DynamicValueProviderClass,
|
14 | isDynamicValueProviderClass,
|
15 | } from './binding';
|
16 | import {BindingAddress} from './binding-key';
|
17 | import {ContextTags} from './keys';
|
18 | import {Provider} from './provider';
|
19 | import {Constructor} from './value-promise';
|
20 |
|
21 | const debug = debugFactory('loopback:context:binding-inspector');
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 | export type BindingMetadata<T = unknown> = {
|
29 | |
30 |
|
31 |
|
32 | templates: BindingTemplate<T>[];
|
33 | |
34 |
|
35 |
|
36 | target: Constructor<T>;
|
37 | };
|
38 |
|
39 |
|
40 |
|
41 |
|
42 | export const BINDING_METADATA_KEY = MetadataAccessor.create<
|
43 | BindingMetadata,
|
44 | ClassDecorator
|
45 | >('binding.metadata');
|
46 |
|
47 |
|
48 |
|
49 |
|
50 | export type BindingScopeAndTags = {
|
51 | scope?: BindingScope;
|
52 | tags?: BindingTag | BindingTag[];
|
53 | };
|
54 |
|
55 |
|
56 |
|
57 |
|
58 | export type BindingSpec<T = unknown> = BindingTemplate<T> | BindingScopeAndTags;
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 | export 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 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 | export 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 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 |
|
99 | export function asClassOrProvider<T>(
|
100 | target: Constructor<T | Provider<T>> | DynamicValueProviderClass<T>,
|
101 | ): BindingTemplate<T> {
|
102 |
|
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 |
|
118 |
|
119 |
|
120 |
|
121 |
|
122 | export 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 |
|
141 |
|
142 |
|
143 |
|
144 |
|
145 | export 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 |
|
156 |
|
157 | export function removeNameAndKeyTags(binding: Binding<unknown>) {
|
158 | if (binding.tagMap) {
|
159 | delete binding.tagMap.name;
|
160 | delete binding.tagMap.key;
|
161 | }
|
162 | }
|
163 |
|
164 |
|
165 |
|
166 |
|
167 |
|
168 |
|
169 |
|
170 |
|
171 | export 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 |
|
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 |
|
188 | binding.apply(removeNameAndKeyTags);
|
189 | }
|
190 | if (options != null) {
|
191 | applyClassBindingOptions(binding, options);
|
192 | }
|
193 | };
|
194 | }
|
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 |
|
201 |
|
202 |
|
203 |
|
204 |
|
205 |
|
206 | export type TypeNamespaceMapping = {[name: string]: string};
|
207 |
|
208 | export const DEFAULT_TYPE_NAMESPACES: TypeNamespaceMapping = {
|
209 | class: 'classes',
|
210 | provider: 'providers',
|
211 | dynamicValueProvider: 'dynamicValueProviders',
|
212 | };
|
213 |
|
214 |
|
215 |
|
216 |
|
217 | export type BindingFromClassOptions = {
|
218 | |
219 |
|
220 |
|
221 | key?: BindingAddress;
|
222 | |
223 |
|
224 |
|
225 | type?: string;
|
226 | |
227 |
|
228 |
|
229 |
|
230 | name?: string;
|
231 | |
232 |
|
233 |
|
234 |
|
235 | namespace?: string;
|
236 | |
237 |
|
238 |
|
239 | typeNamespaceMapping?: TypeNamespaceMapping;
|
240 | |
241 |
|
242 |
|
243 | defaultNamespace?: string;
|
244 | |
245 |
|
246 |
|
247 | defaultScope?: BindingScope;
|
248 | };
|
249 |
|
250 |
|
251 |
|
252 |
|
253 |
|
254 |
|
255 |
|
256 |
|
257 |
|
258 |
|
259 |
|
260 |
|
261 |
|
262 |
|
263 |
|
264 |
|
265 | export 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 |
|
281 | function 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 |
|
298 |
|
299 |
|
300 |
|
301 | function getNamespace(type: string, typeNamespaces = DEFAULT_TYPE_NAMESPACES) {
|
302 | if (type in typeNamespaces) {
|
303 | return typeNamespaces[type];
|
304 | } else {
|
305 |
|
306 | return `${type}s`;
|
307 | }
|
308 | }
|
309 |
|
310 |
|
311 |
|
312 |
|
313 |
|
314 |
|
315 |
|
316 |
|
317 |
|
318 |
|
319 |
|
320 |
|
321 |
|
322 |
|
323 |
|
324 |
|
325 |
|
326 |
|
327 |
|
328 |
|
329 |
|
330 |
|
331 |
|
332 |
|
333 | function 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 |
|
341 | const bindingTemplate = new Binding('template').apply(templateFn);
|
342 |
|
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 |
|
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 | }
|