UNPKG

7.34 kBPlain TextView Raw
1// Copyright IBM Corp. and LoopBack contributors 2019. 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 {BindingFilter} from './binding-filter';
7import {BindingAddress, BindingKey} from './binding-key';
8import {Context} from './context';
9import {ContextView} from './context-view';
10import {assertTargetType, inject, Injection, InjectionMetadata} from './inject';
11import {ResolutionSession} from './resolution-session';
12import {getDeepProperty, ValueOrPromise} from './value-promise';
13
14/**
15 * Injection metadata for `@config.*`
16 */
17export interface ConfigInjectionMetadata extends InjectionMetadata {
18 /**
19 * Property path to retrieve the configuration of the target binding, for
20 * example, `rest.host`.
21 */
22 propertyPath?: string;
23 /**
24 * Customize the target binding key from which the configuration is fetched.
25 * If not specified, the configuration of the current binding that contains
26 * the injection is used.
27 */
28 fromBinding?: BindingAddress;
29}
30
31/**
32 * Inject a property from `config` of the current binding. If no corresponding
33 * config value is present, `undefined` will be injected as the configuration
34 * binding is resolved with `optional: true` by default.
35 *
36 * @example
37 * ```ts
38 * class Store {
39 * constructor(
40 * @config('x') public optionX: number,
41 * @config('y') public optionY: string,
42 * ) { }
43 * }
44 *
45 * ctx.configure('store1', { x: 1, y: 'a' });
46 * ctx.configure('store2', { x: 2, y: 'b' });
47 *
48 * ctx.bind('store1').toClass(Store);
49 * ctx.bind('store2').toClass(Store);
50 *
51 * const store1 = ctx.getSync('store1');
52 * expect(store1.optionX).to.eql(1);
53 * expect(store1.optionY).to.eql('a');
54 *
55 * const store2 = ctx.getSync('store2');
56 * expect(store2.optionX).to.eql(2);
57 * expect(store2.optionY).to.eql('b');
58 * ```
59 *
60 * @param propertyPath - Optional property path of the config. If is `''` or not
61 * present, the `config` object will be returned.
62 * @param metadata - Optional metadata to help the injection
63 */
64export function config(
65 propertyPath?: string | ConfigInjectionMetadata,
66 metadata?: ConfigInjectionMetadata,
67) {
68 propertyPath = propertyPath ?? '';
69 if (typeof propertyPath === 'object') {
70 metadata = propertyPath;
71 propertyPath = '';
72 }
73 metadata = Object.assign(
74 {propertyPath, decorator: '@config', optional: true},
75 metadata,
76 );
77 return inject('', metadata, resolveFromConfig);
78}
79
80export namespace config {
81 /**
82 * `@inject.getter` decorator to inject a config getter function
83 * @param propertyPath - Optional property path of the config object
84 * @param metadata - Injection metadata
85 */
86 export const getter = function injectConfigGetter(
87 propertyPath?: string | ConfigInjectionMetadata,
88 metadata?: ConfigInjectionMetadata,
89 ) {
90 propertyPath = propertyPath ?? '';
91 if (typeof propertyPath === 'object') {
92 metadata = propertyPath;
93 propertyPath = '';
94 }
95 metadata = Object.assign(
96 {propertyPath, decorator: '@config.getter', optional: true},
97 metadata,
98 );
99 return inject('', metadata, resolveAsGetterFromConfig);
100 };
101
102 /**
103 * `@inject.view` decorator to inject a config context view to allow dynamic
104 * changes in configuration
105 * @param propertyPath - Optional property path of the config object
106 * @param metadata - Injection metadata
107 */
108 export const view = function injectConfigView(
109 propertyPath?: string | ConfigInjectionMetadata,
110 metadata?: ConfigInjectionMetadata,
111 ) {
112 propertyPath = propertyPath ?? '';
113 if (typeof propertyPath === 'object') {
114 metadata = propertyPath;
115 propertyPath = '';
116 }
117 metadata = Object.assign(
118 {propertyPath, decorator: '@config.view', optional: true},
119 metadata,
120 );
121 return inject('', metadata, resolveAsViewFromConfig);
122 };
123}
124
125/**
126 * Get the key for the current binding on which dependency injection is
127 * performed
128 * @param session - Resolution session
129 */
130function getCurrentBindingKey(session: ResolutionSession) {
131 // The current binding is not set if `instantiateClass` is invoked directly
132 return session.currentBinding?.key;
133}
134
135/**
136 * Get the target binding key from which the configuration should be resolved
137 * @param injection - Injection
138 * @param session - Resolution session
139 */
140function getTargetBindingKey(injection: Injection, session: ResolutionSession) {
141 return injection.metadata.fromBinding || getCurrentBindingKey(session);
142}
143
144/**
145 * Resolver for `@config`
146 * @param ctx - Context object
147 * @param injection - Injection metadata
148 * @param session - Resolution session
149 */
150function resolveFromConfig(
151 ctx: Context,
152 injection: Injection,
153 session: ResolutionSession,
154): ValueOrPromise<unknown> {
155 const bindingKey = getTargetBindingKey(injection, session);
156 // Return `undefined` if no current binding is present
157 if (!bindingKey) return undefined;
158 const meta = injection.metadata;
159 return ctx.getConfigAsValueOrPromise(bindingKey, meta.propertyPath, {
160 session,
161 optional: meta.optional,
162 });
163}
164
165/**
166 * Resolver from `@config.getter`
167 * @param ctx - Context object
168 * @param injection - Injection metadata
169 * @param session - Resolution session
170 */
171function resolveAsGetterFromConfig(
172 ctx: Context,
173 injection: Injection,
174 session: ResolutionSession,
175) {
176 assertTargetType(injection, Function, 'Getter function');
177 const bindingKey = getTargetBindingKey(injection, session);
178 const meta = injection.metadata;
179 return async function getter() {
180 // Return `undefined` if no current binding is present
181 if (!bindingKey) return undefined;
182 return ctx.getConfigAsValueOrPromise(bindingKey, meta.propertyPath, {
183 // https://github.com/loopbackio/loopback-next/issues/9041
184 // We should start with a new session for `getter` resolution to avoid
185 // possible circular dependencies
186 session: undefined,
187 optional: meta.optional,
188 });
189 };
190}
191
192/**
193 * Resolver for `@config.view`
194 * @param ctx - Context object
195 * @param injection - Injection metadata
196 * @param session - Resolution session
197 */
198function resolveAsViewFromConfig(
199 ctx: Context,
200 injection: Injection,
201 session: ResolutionSession,
202) {
203 assertTargetType(injection, ContextView);
204 const bindingKey = getTargetBindingKey(injection, session);
205 // Return `undefined` if no current binding is present
206 if (!bindingKey) return undefined;
207 const view = new ConfigView(
208 ctx,
209 binding =>
210 binding.key === BindingKey.buildKeyForConfig(bindingKey).toString(),
211 injection.metadata.propertyPath,
212 );
213 view.open();
214 return view;
215}
216
217/**
218 * A subclass of `ContextView` to handle dynamic configuration as its
219 * `values()` honors the `propertyPath`.
220 */
221class ConfigView extends ContextView {
222 constructor(
223 ctx: Context,
224 filter: BindingFilter,
225 private propertyPath?: string,
226 ) {
227 super(ctx, filter);
228 }
229
230 /**
231 * Get values for the configuration with a property path
232 * @param session - Resolution session
233 */
234 async values(session?: ResolutionSession) {
235 const configValues = await super.values(session);
236 const propertyPath = this.propertyPath;
237 if (!propertyPath) return configValues;
238 return configValues.map(v => getDeepProperty(v, propertyPath));
239 }
240}