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 |
|
178 | const templateFunctions = [...(spec?.templates ?? [])];
|
179 | if (spec?.target !== cls) {
|
180 |
|
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 |
|
189 | binding.apply(removeNameAndKeyTags);
|
190 | }
|
191 | if (options != null) {
|
192 | applyClassBindingOptions(binding, options);
|
193 | }
|
194 | };
|
195 | }
|
196 |
|
197 |
|
198 |
|
199 |
|
200 |
|
201 |
|
202 |
|
203 |
|
204 |
|
205 |
|
206 |
|
207 | export type TypeNamespaceMapping = {[name: string]: string};
|
208 |
|
209 | export const DEFAULT_TYPE_NAMESPACES: TypeNamespaceMapping = {
|
210 | class: 'classes',
|
211 | provider: 'providers',
|
212 | dynamicValueProvider: 'dynamicValueProviders',
|
213 | };
|
214 |
|
215 |
|
216 |
|
217 |
|
218 | export type BindingFromClassOptions = {
|
219 | |
220 |
|
221 |
|
222 | key?: BindingAddress;
|
223 | |
224 |
|
225 |
|
226 | type?: string;
|
227 | |
228 |
|
229 |
|
230 |
|
231 | name?: string;
|
232 | |
233 |
|
234 |
|
235 |
|
236 | namespace?: string;
|
237 | |
238 |
|
239 |
|
240 | typeNamespaceMapping?: TypeNamespaceMapping;
|
241 | |
242 |
|
243 |
|
244 | defaultNamespace?: string;
|
245 | |
246 |
|
247 |
|
248 | defaultScope?: BindingScope;
|
249 | };
|
250 |
|
251 |
|
252 |
|
253 |
|
254 |
|
255 |
|
256 |
|
257 |
|
258 |
|
259 |
|
260 |
|
261 |
|
262 |
|
263 |
|
264 |
|
265 |
|
266 | export 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 |
|
282 | function 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 |
|
299 |
|
300 |
|
301 |
|
302 | function getNamespace(type: string, typeNamespaces = DEFAULT_TYPE_NAMESPACES) {
|
303 | if (type in typeNamespaces) {
|
304 | return typeNamespaces[type];
|
305 | } else {
|
306 |
|
307 | return `${type}s`;
|
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 |
|
334 | function 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 |
|
342 | const bindingTemplate = new Binding('template').apply(templateFn);
|
343 |
|
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 |
|
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 | }
|