UNPKG

31.2 kBPlain TextView Raw
1// Copyright IBM Corp. and LoopBack contributors 2017,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 debugFactory from 'debug';
7import {EventEmitter} from 'events';
8import {bindingTemplateFor} from './binding-inspector';
9import {BindingAddress, BindingKey} from './binding-key';
10import {Context} from './context';
11import {inspectInjections} from './inject';
12import {createProxyWithInterceptors} from './interception-proxy';
13import {invokeMethod} from './invocation';
14import {JSONObject} from './json-types';
15import {ContextTags} from './keys';
16import {Provider} from './provider';
17import {
18 asResolutionOptions,
19 ResolutionContext,
20 ResolutionError,
21 ResolutionOptions,
22 ResolutionOptionsOrSession,
23 ResolutionSession,
24} from './resolution-session';
25import {instantiateClass} from './resolver';
26import {
27 BoundValue,
28 Constructor,
29 isPromiseLike,
30 MapObject,
31 transformValueOrPromise,
32 ValueOrPromise,
33} from './value-promise';
34
35const debug = debugFactory('loopback:context:binding');
36
37/**
38 * Scope for binding values
39 */
40export enum BindingScope {
41 /**
42 * The binding provides a value that is calculated each time. This will be
43 * the default scope if not set.
44 *
45 * For example, with the following context hierarchy:
46 *
47 * - `app` (with a binding `'b1'` that produces sequential values 0, 1, ...)
48 * - req1
49 * - req2
50 *
51 * Now `'b1'` is resolved to a new value each time for `app` and its
52 * descendants `req1` and `req2`:
53 * - app.get('b1') ==> 0
54 * - req1.get('b1') ==> 1
55 * - req2.get('b1') ==> 2
56 * - req2.get('b1') ==> 3
57 * - app.get('b1') ==> 4
58 */
59 TRANSIENT = 'Transient',
60
61 /**
62 * @deprecated Finer-grained scopes such as `APPLICATION`, `SERVER`, or
63 * `REQUEST` should be used instead to ensure the scope of sharing of resolved
64 * binding values.
65 *
66 * The binding provides a value as a singleton within each local context. The
67 * value is calculated only once per context and cached for subsequential
68 * uses. Child contexts have their own value and do not share with their
69 * ancestors.
70 *
71 * For example, with the following context hierarchy:
72 *
73 * - `app` (with a binding `'b1'` that produces sequential values 0, 1, ...)
74 * - req1
75 * - req2
76 *
77 * 1. `0` is the resolved value for `'b1'` within the `app` afterward
78 * - app.get('b1') ==> 0 (always)
79 *
80 * 2. `'b1'` is resolved in `app` but not in `req1`, a new value `1` is
81 * calculated and used for `req1` afterward
82 * - req1.get('b1') ==> 1 (always)
83 *
84 * 3. `'b1'` is resolved in `app` but not in `req2`, a new value `2` is
85 * calculated and used for `req2` afterward
86 * - req2.get('b1') ==> 2 (always)
87 *
88 */
89 CONTEXT = 'Context',
90
91 /**
92 * The binding provides a value as a singleton within the context hierarchy
93 * (the owning context and its descendants). The value is calculated only
94 * once for the owning context and cached for subsequential uses. Child
95 * contexts share the same value as their ancestors.
96 *
97 * For example, with the following context hierarchy:
98 *
99 * - `app` (with a binding `'b1'` that produces sequential values 0, 1, ...)
100 * - req1
101 * - req2
102 *
103 * 1. `0` is the singleton for `app` afterward
104 * - app.get('b1') ==> 0 (always)
105 *
106 * 2. `'b1'` is resolved in `app`, reuse it for `req1`
107 * - req1.get('b1') ==> 0 (always)
108 *
109 * 3. `'b1'` is resolved in `app`, reuse it for `req2`
110 * - req2.get('b1') ==> 0 (always)
111 */
112 SINGLETON = 'Singleton',
113
114 /*
115 * The following scopes are checked against the context hierarchy to find
116 * the first matching context for a given scope in the chain. Resolved binding
117 * values will be cached and shared on the scoped context. This ensures a
118 * binding to have the same value for the scoped context.
119 */
120
121 /**
122 * Application scope
123 *
124 * @remarks
125 * The binding provides an application-scoped value within the context
126 * hierarchy. Resolved value for this binding will be cached and shared for
127 * the same application context (denoted by its scope property set to
128 * `BindingScope.APPLICATION`).
129 *
130 */
131 APPLICATION = 'Application',
132
133 /**
134 * Server scope
135 *
136 * @remarks
137 * The binding provides an server-scoped value within the context hierarchy.
138 * Resolved value for this binding will be cached and shared for the same
139 * server context (denoted by its scope property set to
140 * `BindingScope.SERVER`).
141 *
142 * It's possible that an application has more than one servers configured,
143 * such as a `RestServer` and a `GrpcServer`. Both server contexts are created
144 * with `scope` set to `BindingScope.SERVER`. Depending on where a binding
145 * is resolved:
146 * - If the binding is resolved from the RestServer or below, it will be
147 * cached using the RestServer context as the key.
148 * - If the binding is resolved from the GrpcServer or below, it will be
149 * cached using the GrpcServer context as the key.
150 *
151 * The same binding can resolved/shared/cached for all servers, each of which
152 * has its own value for the binding.
153 */
154 SERVER = 'Server',
155
156 /**
157 * Request scope
158 *
159 * @remarks
160 * The binding provides an request-scoped value within the context hierarchy.
161 * Resolved value for this binding will be cached and shared for the same
162 * request context (denoted by its scope property set to
163 * `BindingScope.REQUEST`).
164 *
165 * The `REQUEST` scope is very useful for controllers, services and artifacts
166 * that want to have a single instance/value for a given request.
167 */
168 REQUEST = 'Request',
169}
170
171/**
172 * Type of the binding source
173 */
174export enum BindingType {
175 /**
176 * A fixed value
177 */
178 CONSTANT = 'Constant',
179 /**
180 * A function to get the value
181 */
182 DYNAMIC_VALUE = 'DynamicValue',
183 /**
184 * A class to be instantiated as the value
185 */
186 CLASS = 'Class',
187 /**
188 * A provider class with `value()` function to get the value
189 */
190 PROVIDER = 'Provider',
191 /**
192 * A alias to another binding key with optional path
193 */
194 ALIAS = 'Alias',
195}
196
197/**
198 * Binding source for `to`
199 */
200export type ConstantBindingSource<T> = {
201 type: BindingType.CONSTANT;
202 value: T;
203};
204
205/**
206 * Binding source for `toDynamicValue`
207 */
208export type DynamicValueBindingSource<T> = {
209 type: BindingType.DYNAMIC_VALUE;
210 value: ValueFactory<T> | DynamicValueProviderClass<T>;
211};
212
213/**
214 * Binding source for `toClass`
215 */
216export type ClassBindingSource<T> = {
217 type: BindingType.CLASS;
218 value: Constructor<T>;
219};
220
221/**
222 * Binding source for `toProvider`
223 */
224export type ProviderBindingSource<T> = {
225 type: BindingType.PROVIDER;
226 value: Constructor<Provider<T>>;
227};
228
229/**
230 * Binding source for `toAlias`
231 */
232export type AliasBindingSource<T> = {
233 type: BindingType.ALIAS;
234 value: BindingAddress<T>;
235};
236
237/**
238 * Source for the binding, including the type and value
239 */
240export type BindingSource<T> =
241 | ConstantBindingSource<T>
242 | DynamicValueBindingSource<T>
243 | ClassBindingSource<T>
244 | ProviderBindingSource<T>
245 | AliasBindingSource<T>;
246
247// eslint-disable-next-line @typescript-eslint/no-explicit-any
248export type TagMap = MapObject<any>;
249
250/**
251 * Binding tag can be a simple name or name/value pairs
252 */
253export type BindingTag = TagMap | string;
254
255/**
256 * A function as the template to configure bindings
257 */
258export type BindingTemplate<T = unknown> = (binding: Binding<T>) => void;
259
260/**
261 * Information for a binding event
262 */
263export type BindingEvent = {
264 /**
265 * Event type
266 */
267 type: 'changed' | string;
268 /**
269 * Source binding that emits the event
270 */
271 binding: Readonly<Binding<unknown>>;
272 /**
273 * Operation that triggers the event
274 */
275 operation: 'tag' | 'scope' | 'value' | string;
276};
277
278/**
279 * Event listeners for binding events
280 */
281export type BindingEventListener = (
282 /**
283 * Binding event
284 */
285 event: BindingEvent,
286) => void;
287
288/**
289 * A factory function for `toDynamicValue`
290 */
291export type ValueFactory<T = unknown> = (
292 resolutionCtx: ResolutionContext,
293) => ValueOrPromise<T | undefined>;
294
295/**
296 * A class with a static `value` method as the factory function for
297 * `toDynamicValue`.
298 *
299 * @example
300 * ```ts
301 * import {inject} from '@loopback/context';
302 *
303 * export class DynamicGreetingProvider {
304 * static value(@inject('currentUser') user: string) {
305 * return `Hello, ${user}`;
306 * }
307 * }
308 * ```
309 */
310export interface DynamicValueProviderClass<T = unknown>
311 extends Constructor<unknown>,
312 Function {
313 value: (...args: BoundValue[]) => ValueOrPromise<T>;
314}
315
316/**
317 * Adapt the ValueFactoryProvider class to be a value factory
318 * @param provider - ValueFactoryProvider class
319 */
320function toValueFactory<T = unknown>(
321 provider: DynamicValueProviderClass<T>,
322): ValueFactory<T> {
323 return resolutionCtx =>
324 invokeMethod(provider, 'value', resolutionCtx.context, [], {
325 skipInterceptors: true,
326 session: resolutionCtx.options.session,
327 });
328}
329
330/**
331 * Check if the factory is a value factory provider class
332 * @param factory - A factory function or a dynamic value provider class
333 */
334export function isDynamicValueProviderClass<T = unknown>(
335 factory: unknown,
336): factory is DynamicValueProviderClass<T> {
337 // Not a class
338 if (typeof factory !== 'function' || !String(factory).startsWith('class ')) {
339 return false;
340 }
341 const valueMethod = (factory as DynamicValueProviderClass).value;
342 return typeof valueMethod === 'function';
343}
344
345/**
346 * Binding represents an entry in the `Context`. Each binding has a key and a
347 * corresponding value getter.
348 */
349export class Binding<T = BoundValue> extends EventEmitter {
350 /**
351 * Key of the binding
352 */
353 public readonly key: string;
354
355 /**
356 * Map for tag name/value pairs
357 */
358 public readonly tagMap: TagMap = {};
359
360 private _scope?: BindingScope;
361 /**
362 * Scope of the binding to control how the value is cached/shared
363 */
364 public get scope(): BindingScope {
365 // Default to TRANSIENT if not set
366 return this._scope ?? BindingScope.TRANSIENT;
367 }
368
369 /**
370 * Type of the binding value getter
371 */
372 public get type(): BindingType | undefined {
373 return this._source?.type;
374 }
375
376 private _cache: WeakMap<Context, ValueOrPromise<T>>;
377 private _getValue?: ValueFactory<T>;
378
379 /**
380 * The original source value received from `to`, `toClass`, `toDynamicValue`,
381 * `toProvider`, or `toAlias`.
382 */
383 private _source?: BindingSource<T>;
384
385 public get source() {
386 return this._source;
387 }
388
389 /**
390 * For bindings bound via `toClass()`, this property contains the constructor
391 * function of the class
392 */
393 public get valueConstructor(): Constructor<T> | undefined {
394 return this._source?.type === BindingType.CLASS
395 ? this._source?.value
396 : undefined;
397 }
398
399 /**
400 * For bindings bound via `toProvider()`, this property contains the
401 * constructor function of the provider class
402 */
403 public get providerConstructor(): Constructor<Provider<T>> | undefined {
404 return this._source?.type === BindingType.PROVIDER
405 ? this._source?.value
406 : undefined;
407 }
408
409 constructor(
410 key: BindingAddress<T>,
411 public isLocked: boolean = false,
412 ) {
413 super();
414 BindingKey.validate(key);
415 this.key = key.toString();
416 }
417
418 /**
419 * Cache the resolved value by the binding scope
420 * @param resolutionCtx - The resolution context
421 * @param result - The calculated value for the binding
422 */
423 private _cacheValue(
424 resolutionCtx: Context,
425 result: ValueOrPromise<T>,
426 ): ValueOrPromise<T> {
427 // Initialize the cache as a weakmap keyed by context
428 if (!this._cache) this._cache = new WeakMap<Context, ValueOrPromise<T>>();
429 if (this.scope !== BindingScope.TRANSIENT) {
430 this._cache.set(resolutionCtx!, result);
431 }
432 return result;
433 }
434
435 /**
436 * Clear the cache
437 */
438 private _clearCache() {
439 if (!this._cache) return;
440 // WeakMap does not have a `clear` method
441 this._cache = new WeakMap();
442 }
443
444 /**
445 * Invalidate the binding cache so that its value will be reloaded next time.
446 * This is useful to force reloading a cached value when its configuration or
447 * dependencies are changed.
448 * **WARNING**: The state held in the cached value will be gone.
449 *
450 * @param ctx - Context object
451 */
452 refresh(ctx: Context) {
453 if (!this._cache) return;
454 if (this.scope !== BindingScope.TRANSIENT) {
455 const resolutionCtx = ctx.getResolutionContext(this);
456 if (resolutionCtx != null) {
457 this._cache.delete(resolutionCtx);
458 }
459 }
460 }
461
462 /**
463 * This is an internal function optimized for performance.
464 * Users should use `@inject(key)` or `ctx.get(key)` instead.
465 *
466 * Get the value bound to this key. Depending on `isSync`, this
467 * function returns either:
468 * - the bound value
469 * - a promise of the bound value
470 *
471 * Consumers wishing to consume sync values directly should use `isPromiseLike`
472 * to check the type of the returned value to decide how to handle it.
473 *
474 * @example
475 * ```
476 * const result = binding.getValue(ctx);
477 * if (isPromiseLike(result)) {
478 * result.then(doSomething)
479 * } else {
480 * doSomething(result);
481 * }
482 * ```
483 *
484 * @param ctx - Context for the resolution
485 * @param session - Optional session for binding and dependency resolution
486 */
487 getValue(ctx: Context, session?: ResolutionSession): ValueOrPromise<T>;
488
489 /**
490 * Returns a value or promise for this binding in the given context. The
491 * resolved value can be `undefined` if `optional` is set to `true` in
492 * `options`.
493 * @param ctx - Context for the resolution
494 * @param options - Optional options for binding and dependency resolution
495 */
496 getValue(
497 ctx: Context,
498 options?: ResolutionOptions,
499 ): ValueOrPromise<T | undefined>;
500
501 // Implementation
502 getValue(
503 ctx: Context,
504 optionsOrSession?: ResolutionOptionsOrSession,
505 ): ValueOrPromise<T | undefined> {
506 /* istanbul ignore if */
507 if (debug.enabled) {
508 debug('Get value for binding %s', this.key);
509 }
510
511 const options = asResolutionOptions(optionsOrSession);
512 const resolutionCtx = this.getResolutionContext(ctx, options);
513 if (resolutionCtx == null) return undefined;
514
515 // Keep a snapshot for proxy
516 const savedSession =
517 ResolutionSession.fork(options.session) ?? new ResolutionSession();
518
519 // First check cached value for non-transient
520 if (this._cache) {
521 if (this.scope !== BindingScope.TRANSIENT) {
522 if (resolutionCtx && this._cache.has(resolutionCtx)) {
523 const value = this._cache.get(resolutionCtx)!;
524 return this.getValueOrProxy(
525 resolutionCtx,
526 {...options, session: savedSession},
527 value,
528 );
529 }
530 }
531 }
532 const resolutionMetadata = {
533 context: resolutionCtx!,
534 binding: this,
535 options,
536 };
537 if (typeof this._getValue === 'function') {
538 const result = ResolutionSession.runWithBinding(
539 s => {
540 const optionsWithSession = {
541 ...options,
542 session: s,
543 // Force to be the non-proxy version
544 asProxyWithInterceptors: false,
545 };
546 // We already test `this._getValue` is a function. It's safe to assert
547 // that `this._getValue` is not undefined.
548 return this._getValue!({
549 ...resolutionMetadata,
550 options: optionsWithSession,
551 });
552 },
553 this,
554 options.session,
555 );
556 const value = this._cacheValue(resolutionCtx!, result);
557 return this.getValueOrProxy(
558 resolutionCtx,
559 {...options, session: savedSession},
560 value,
561 );
562 }
563 // `@inject.binding` adds a binding without _getValue
564 if (options.optional) return undefined;
565 return Promise.reject(
566 new ResolutionError(
567 `No value was configured for binding ${this.key}.`,
568 resolutionMetadata,
569 ),
570 );
571 }
572
573 private getValueOrProxy(
574 resolutionCtx: Context,
575 options: ResolutionOptions,
576 value: ValueOrPromise<T>,
577 ): ValueOrPromise<T> {
578 const session = options.session!;
579 session.pushBinding(this);
580 return Binding.valueOrProxy(
581 {
582 context: resolutionCtx,
583 binding: this,
584 options,
585 },
586 value,
587 );
588 }
589
590 /**
591 * Locate and validate the resolution context
592 * @param ctx - Current context
593 * @param options - Resolution options
594 */
595 private getResolutionContext(ctx: Context, options: ResolutionOptions) {
596 const resolutionCtx = ctx.getResolutionContext(this);
597 switch (this.scope) {
598 case BindingScope.APPLICATION:
599 case BindingScope.SERVER:
600 case BindingScope.REQUEST:
601 if (resolutionCtx == null) {
602 const msg =
603 `Binding "${this.key}" in context "${ctx.name}" cannot` +
604 ` be resolved in scope "${this.scope}"`;
605 if (options.optional) {
606 debug(msg);
607 return undefined;
608 }
609 throw new Error(msg);
610 }
611 }
612
613 const ownerCtx = ctx.getOwnerContext(this.key);
614 if (ownerCtx != null && !ownerCtx.isVisibleTo(resolutionCtx!)) {
615 const msg =
616 `Resolution context "${resolutionCtx?.name}" does not have ` +
617 `visibility to binding "${this.key} (scope:${this.scope})" in context "${ownerCtx.name}"`;
618 if (options.optional) {
619 debug(msg);
620 return undefined;
621 }
622 throw new Error(msg);
623 }
624 return resolutionCtx;
625 }
626
627 /**
628 * Lock the binding so that it cannot be rebound
629 */
630 lock(): this {
631 this.isLocked = true;
632 return this;
633 }
634
635 /**
636 * Emit a `changed` event
637 * @param operation - Operation that makes changes
638 */
639 private emitChangedEvent(operation: string) {
640 const event: BindingEvent = {binding: this, operation, type: 'changed'};
641 this.emit('changed', event);
642 }
643
644 /**
645 * Tag the binding with names or name/value objects. A tag has a name and
646 * an optional value. If not supplied, the tag name is used as the value.
647 *
648 * @param tags - A list of names or name/value objects. Each
649 * parameter can be in one of the following forms:
650 * - string: A tag name without value
651 * - string[]: An array of tag names
652 * - TagMap: A map of tag name/value pairs
653 *
654 * @example
655 * ```ts
656 * // Add a named tag `controller`
657 * binding.tag('controller');
658 *
659 * // Add two named tags: `controller` and `rest`
660 * binding.tag('controller', 'rest');
661 *
662 * // Add two tags
663 * // - `controller` (name = 'controller')
664 * // `{name: 'my-controller'}` (name = 'name', value = 'my-controller')
665 * binding.tag('controller', {name: 'my-controller'});
666 *
667 * ```
668 */
669 tag(...tags: BindingTag[]): this {
670 for (const t of tags) {
671 if (typeof t === 'string') {
672 this.tagMap[t] = t;
673 } else if (Array.isArray(t)) {
674 // Throw an error as TypeScript cannot exclude array from TagMap
675 throw new Error(
676 'Tag must be a string or an object (but not array): ' + t,
677 );
678 } else {
679 Object.assign(this.tagMap, t);
680 }
681 }
682 this.emitChangedEvent('tag');
683 return this;
684 }
685
686 /**
687 * Get an array of tag names
688 */
689 get tagNames() {
690 return Object.keys(this.tagMap);
691 }
692
693 /**
694 * Set the binding scope
695 * @param scope - Binding scope
696 */
697 inScope(scope: BindingScope): this {
698 if (this._scope !== scope) this._clearCache();
699 this._scope = scope;
700 this.emitChangedEvent('scope');
701 return this;
702 }
703
704 /**
705 * Apply default scope to the binding. It only changes the scope if it's not
706 * set yet
707 * @param scope - Default binding scope
708 */
709 applyDefaultScope(scope: BindingScope): this {
710 if (!this._scope) {
711 this.inScope(scope);
712 }
713 return this;
714 }
715
716 /**
717 * Set the `_getValue` function
718 * @param getValue - getValue function
719 */
720 private _setValueGetter(getValue: ValueFactory<T>) {
721 // Clear the cache
722 this._clearCache();
723 this._getValue = resolutionCtx => {
724 return getValue(resolutionCtx);
725 };
726 this.emitChangedEvent('value');
727 }
728
729 /**
730 * Bind the key to a constant value. The value must be already available
731 * at binding time, it is not allowed to pass a Promise instance.
732 *
733 * @param value - The bound value.
734 *
735 * @example
736 *
737 * ```ts
738 * ctx.bind('appName').to('CodeHub');
739 * ```
740 */
741 to(value: T): this {
742 if (isPromiseLike(value)) {
743 // Promises are a construct primarily intended for flow control:
744 // In an algorithm with steps 1 and 2, we want to wait for the outcome
745 // of step 1 before starting step 2.
746 //
747 // Promises are NOT a tool for storing values that may become available
748 // in the future, depending on the success or a failure of a background
749 // async task.
750 //
751 // Values stored in bindings are typically accessed only later,
752 // in a different turn of the event loop or the Promise micro-queue.
753 // As a result, when a promise is stored via `.to()` and is rejected
754 // later, then more likely than not, there will be no error (catch)
755 // handler registered yet, and Node.js will print
756 // "Unhandled Rejection Warning".
757 throw new Error(
758 'Promise instances are not allowed for constant values ' +
759 'bound via ".to()". Register an async getter function ' +
760 'via ".toDynamicValue()" instead.',
761 );
762 }
763 /* istanbul ignore if */
764 if (debug.enabled) {
765 debug('Bind %s to constant:', this.key, value);
766 }
767 this._source = {
768 type: BindingType.CONSTANT,
769 value,
770 };
771 this._setValueGetter(resolutionCtx => {
772 return Binding.valueOrProxy(resolutionCtx, value);
773 });
774 return this;
775 }
776
777 /**
778 * Bind the key to a computed (dynamic) value.
779 *
780 * @param factoryFn - The factory function creating the value.
781 * Both sync and async functions are supported.
782 *
783 * @example
784 *
785 * ```ts
786 * // synchronous
787 * ctx.bind('now').toDynamicValue(() => Date.now());
788 *
789 * // asynchronous
790 * ctx.bind('something').toDynamicValue(
791 * async () => Promise.delay(10).then(doSomething)
792 * );
793 * ```
794 */
795 toDynamicValue(
796 factory: ValueFactory<T> | DynamicValueProviderClass<T>,
797 ): this {
798 /* istanbul ignore if */
799 if (debug.enabled) {
800 debug('Bind %s to dynamic value:', this.key, factory);
801 }
802 this._source = {
803 type: BindingType.DYNAMIC_VALUE,
804 value: factory,
805 };
806
807 let factoryFn: ValueFactory<T>;
808 if (isDynamicValueProviderClass(factory)) {
809 factoryFn = toValueFactory(factory);
810 } else {
811 factoryFn = factory;
812 }
813 this._setValueGetter(resolutionCtx => {
814 const value = factoryFn(resolutionCtx);
815 return Binding.valueOrProxy(resolutionCtx, value);
816 });
817 return this;
818 }
819
820 private static valueOrProxy<V>(
821 resolutionCtx: ResolutionContext,
822 value: ValueOrPromise<V>,
823 ) {
824 if (!resolutionCtx.options.asProxyWithInterceptors) return value;
825 return createInterceptionProxyFromInstance(
826 value,
827 resolutionCtx.context,
828 resolutionCtx.options.session,
829 );
830 }
831
832 /**
833 * Bind the key to a value computed by a Provider.
834 *
835 * * @example
836 *
837 * ```ts
838 * export class DateProvider implements Provider<Date> {
839 * constructor(@inject('stringDate') private param: String){}
840 * value(): Date {
841 * return new Date(param);
842 * }
843 * }
844 * ```
845 *
846 * @param provider - The value provider to use.
847 */
848 toProvider(providerClass: Constructor<Provider<T>>): this {
849 /* istanbul ignore if */
850 if (debug.enabled) {
851 debug('Bind %s to provider %s', this.key, providerClass.name);
852 }
853 this._source = {
854 type: BindingType.PROVIDER,
855 value: providerClass,
856 };
857 this._setValueGetter(resolutionCtx => {
858 const providerOrPromise = instantiateClass<Provider<T>>(
859 providerClass,
860 resolutionCtx.context,
861 resolutionCtx.options.session,
862 );
863 const value = transformValueOrPromise(providerOrPromise, p => p.value());
864 return Binding.valueOrProxy(resolutionCtx, value);
865 });
866 return this;
867 }
868
869 /**
870 * Bind the key to an instance of the given class.
871 *
872 * @param ctor - The class constructor to call. Any constructor
873 * arguments must be annotated with `@inject` so that
874 * we can resolve them from the context.
875 */
876 toClass<C extends T & object>(ctor: Constructor<C>): this {
877 /* istanbul ignore if */
878 if (debug.enabled) {
879 debug('Bind %s to class %s', this.key, ctor.name);
880 }
881 this._source = {
882 type: BindingType.CLASS,
883 value: ctor,
884 };
885 this._setValueGetter(resolutionCtx => {
886 const value = instantiateClass(
887 ctor,
888 resolutionCtx.context,
889 resolutionCtx.options.session,
890 );
891 return Binding.valueOrProxy(resolutionCtx, value);
892 });
893 return this;
894 }
895
896 /**
897 * Bind to a class optionally decorated with `@injectable`. Based on the
898 * introspection of the class, it calls `toClass/toProvider/toDynamicValue`
899 * internally. The current binding key will be preserved (not being overridden
900 * by the key inferred from the class or options).
901 *
902 * This is similar to {@link createBindingFromClass} but applies to an
903 * existing binding.
904 *
905 * @example
906 *
907 * ```ts
908 * @injectable({scope: BindingScope.SINGLETON, tags: {service: 'MyService}})
909 * class MyService {
910 * // ...
911 * }
912 *
913 * const ctx = new Context();
914 * ctx.bind('services.MyService').toInjectable(MyService);
915 * ```
916 *
917 * @param ctor - A class decorated with `@injectable`.
918 */
919 toInjectable(
920 ctor: DynamicValueProviderClass<T> | Constructor<T | Provider<T>>,
921 ) {
922 this.apply(bindingTemplateFor(ctor));
923 return this;
924 }
925
926 /**
927 * Bind the key to an alias of another binding
928 * @param keyWithPath - Target binding key with optional path,
929 * such as `servers.RestServer.options#apiExplorer`
930 */
931 toAlias(keyWithPath: BindingAddress<T>) {
932 /* istanbul ignore if */
933 if (debug.enabled) {
934 debug('Bind %s to alias %s', this.key, keyWithPath);
935 }
936 this._source = {
937 type: BindingType.ALIAS,
938 value: keyWithPath,
939 };
940 this._setValueGetter(({context, options}) => {
941 return context.getValueOrPromise(keyWithPath, options);
942 });
943 return this;
944 }
945
946 /**
947 * Unlock the binding
948 */
949 unlock(): this {
950 this.isLocked = false;
951 return this;
952 }
953
954 /**
955 * Apply one or more template functions to set up the binding with scope,
956 * tags, and other attributes as a group.
957 *
958 * @example
959 * ```ts
960 * const serverTemplate = (binding: Binding) =>
961 * binding.inScope(BindingScope.SINGLETON).tag('server');
962 *
963 * const serverBinding = new Binding<RestServer>('servers.RestServer1');
964 * serverBinding.apply(serverTemplate);
965 * ```
966 * @param templateFns - One or more functions to configure the binding
967 */
968 apply(...templateFns: BindingTemplate<T>[]): this {
969 for (const fn of templateFns) {
970 fn(this);
971 }
972 return this;
973 }
974
975 /**
976 * Convert to a plain JSON object
977 */
978 toJSON(): JSONObject {
979 const json: JSONObject = {
980 key: this.key,
981 scope: this.scope,
982 tags: this.tagMap,
983 isLocked: this.isLocked,
984 };
985 if (this.type != null) {
986 json.type = this.type;
987 }
988 switch (this._source?.type) {
989 case BindingType.CLASS:
990 json.valueConstructor = this._source?.value.name;
991 break;
992 case BindingType.PROVIDER:
993 json.providerConstructor = this._source?.value.name;
994 break;
995 case BindingType.ALIAS:
996 json.alias = this._source?.value.toString();
997 break;
998 }
999 return json;
1000 }
1001
1002 /**
1003 * Inspect the binding to return a json representation of the binding information
1004 * @param options - Options to control what information should be included
1005 */
1006 inspect(options: BindingInspectOptions = {}): JSONObject {
1007 options = {
1008 includeInjections: false,
1009 ...options,
1010 };
1011 const json = this.toJSON();
1012 if (options.includeInjections) {
1013 const injections = inspectInjections(this);
1014 if (Object.keys(injections).length) json.injections = injections;
1015 }
1016 return json;
1017 }
1018
1019 /**
1020 * A static method to create a binding so that we can do
1021 * `Binding.bind('foo').to('bar');` as `new Binding('foo').to('bar')` is not
1022 * easy to read.
1023 * @param key - Binding key
1024 */
1025 static bind<V = unknown>(key: BindingAddress<V>): Binding<V> {
1026 return new Binding(key);
1027 }
1028
1029 /**
1030 * Create a configuration binding for the given key
1031 *
1032 * @example
1033 * ```ts
1034 * const configBinding = Binding.configure('servers.RestServer.server1')
1035 * .to({port: 3000});
1036 * ```
1037 *
1038 * @typeParam V Generic type for the configuration value (not the binding to
1039 * be configured)
1040 *
1041 * @param key - Key for the binding to be configured
1042 */
1043 static configure<V = unknown>(key: BindingAddress): Binding<V> {
1044 return new Binding(BindingKey.buildKeyForConfig<V>(key)).tag({
1045 [ContextTags.CONFIGURATION_FOR]: key.toString(),
1046 });
1047 }
1048
1049 /**
1050 * The "changed" event is emitted by methods such as `tag`, `inScope`, `to`,
1051 * and `toClass`.
1052 *
1053 * @param eventName The name of the event - always `changed`.
1054 * @param listener The listener function to call when the event is emitted.
1055 */
1056 on(eventName: 'changed', listener: BindingEventListener): this;
1057
1058 // The generic variant inherited from EventEmitter
1059 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1060 on(event: string | symbol, listener: (...args: any[]) => void): this;
1061
1062 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1063 on(event: string | symbol, listener: (...args: any[]) => void): this {
1064 return super.on(event, listener);
1065 }
1066
1067 /**
1068 * The "changed" event is emitted by methods such as `tag`, `inScope`, `to`,
1069 * and `toClass`.
1070 *
1071 * @param eventName The name of the event - always `changed`.
1072 * @param listener The listener function to call when the event is emitted.
1073 */
1074 once(eventName: 'changed', listener: BindingEventListener): this;
1075
1076 // The generic variant inherited from EventEmitter
1077 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1078 once(event: string | symbol, listener: (...args: any[]) => void): this;
1079
1080 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1081 once(event: string | symbol, listener: (...args: any[]) => void): this {
1082 return super.once(event, listener);
1083 }
1084}
1085
1086/**
1087 * Options for binding.inspect()
1088 */
1089export interface BindingInspectOptions {
1090 /**
1091 * The flag to control if injections should be inspected
1092 */
1093 includeInjections?: boolean;
1094}
1095
1096function createInterceptionProxyFromInstance<T>(
1097 instOrPromise: ValueOrPromise<T>,
1098 context: Context,
1099 session?: ResolutionSession,
1100) {
1101 return transformValueOrPromise(instOrPromise, inst => {
1102 if (typeof inst !== 'object' || inst == null) return inst;
1103 return createProxyWithInterceptors(
1104 // Cast inst from `T` to `object`
1105 inst as unknown as object,
1106 context,
1107 session,
1108 ) as unknown as T;
1109 });
1110}