1 | // Copyright IBM Corp. and LoopBack contributors 2017,2020. All Rights Reserved.
|
2 | // Node module: @loopback/core
|
3 | // This file is licensed under the MIT License.
|
4 | // License text available at https://opensource.org/licenses/MIT
|
5 |
|
6 | import {
|
7 | asResolutionOptions,
|
8 | assertTargetType,
|
9 | Binding,
|
10 | BindingFilter,
|
11 | BindingFromClassOptions,
|
12 | BindingSpec,
|
13 | BindingTemplate,
|
14 | Constructor,
|
15 | Context,
|
16 | ContextTags,
|
17 | ContextView,
|
18 | createBindingFromClass,
|
19 | createViewGetter,
|
20 | filterByTag,
|
21 | includesTagValue,
|
22 | inject,
|
23 | injectable,
|
24 | Injection,
|
25 | InjectionMetadata,
|
26 | ResolutionSession,
|
27 | } from '@loopback/context';
|
28 | import {CoreTags} from './keys';
|
29 |
|
30 | /**
|
31 | * Decorate a class as a named extension point. If the decoration is not
|
32 | * present, the name of the class will be used.
|
33 | *
|
34 | * @example
|
35 | * ```ts
|
36 | * import {extensionPoint} from '@loopback/core';
|
37 | *
|
38 | * @extensionPoint(GREETER_EXTENSION_POINT_NAME)
|
39 | * export class GreetingService {
|
40 | * // ...
|
41 | * }
|
42 | * ```
|
43 | *
|
44 | * @param name - Name of the extension point
|
45 | */
|
46 | export function extensionPoint(name: string, ...specs: BindingSpec[]) {
|
47 | return injectable({tags: {[CoreTags.EXTENSION_POINT]: name}}, ...specs);
|
48 | }
|
49 |
|
50 | /**
|
51 | * Shortcut to inject extensions for the given extension point.
|
52 | *
|
53 | * @example
|
54 | * ```ts
|
55 | * import {Getter} from '@loopback/context';
|
56 | * import {extensionPoint, extensions} from '@loopback/core';
|
57 | *
|
58 | * @extensionPoint(GREETER_EXTENSION_POINT_NAME)
|
59 | * export class GreetingService {
|
60 | * constructor(
|
61 | * @extensions() // Inject extensions for the extension point
|
62 | * private getGreeters: Getter<Greeter[]>,
|
63 | * // ...
|
64 | * ) {
|
65 | * // ...
|
66 | * }
|
67 | * ```
|
68 | *
|
69 | * @param extensionPointName - Name of the extension point. If not supplied, we
|
70 | * use the `name` tag from the extension point binding or the class name of the
|
71 | * extension point class. If a class needs to inject extensions from multiple
|
72 | * extension points, use different `extensionPointName` for different types of
|
73 | * extensions.
|
74 | * @param metadata - Optional injection metadata
|
75 | */
|
76 | export function extensions(
|
77 | extensionPointName?: string,
|
78 | metadata?: InjectionMetadata,
|
79 | ) {
|
80 | return inject(
|
81 | '',
|
82 | {...metadata, decorator: '@extensions'},
|
83 | (ctx, injection, session) => {
|
84 | assertTargetType(injection, Function, 'Getter function');
|
85 | const bindingFilter = filterByExtensionPoint(
|
86 | injection,
|
87 | session,
|
88 | extensionPointName,
|
89 | );
|
90 | return createViewGetter(
|
91 | ctx,
|
92 | bindingFilter,
|
93 | injection.metadata.bindingComparator,
|
94 | {...metadata, ...asResolutionOptions(session)},
|
95 | );
|
96 | },
|
97 | );
|
98 | }
|
99 |
|
100 | export namespace extensions {
|
101 | /**
|
102 | * Inject a `ContextView` for extensions of the extension point. The view can
|
103 | * then be listened on events such as `bind`, `unbind`, or `refresh` to react
|
104 | * on changes of extensions.
|
105 | *
|
106 | * @example
|
107 | * ```ts
|
108 | * import {extensionPoint, extensions} from '@loopback/core';
|
109 | *
|
110 | * @extensionPoint(GREETER_EXTENSION_POINT_NAME)
|
111 | * export class GreetingService {
|
112 | * constructor(
|
113 | * @extensions.view() // Inject a context view for extensions of the extension point
|
114 | * private greetersView: ContextView<Greeter>,
|
115 | * // ...
|
116 | * ) {
|
117 | * // ...
|
118 | * }
|
119 | * ```
|
120 | * @param extensionPointName - Name of the extension point. If not supplied, we
|
121 | * use the `name` tag from the extension point binding or the class name of the
|
122 | * extension point class. If a class needs to inject extensions from multiple
|
123 | * extension points, use different `extensionPointName` for different types of
|
124 | * extensions.
|
125 | * @param metadata - Optional injection metadata
|
126 | */
|
127 | export function view(
|
128 | extensionPointName?: string,
|
129 | metadata?: InjectionMetadata,
|
130 | ) {
|
131 | return inject(
|
132 | '',
|
133 | {...metadata, decorator: '@extensions.view'},
|
134 | (ctx, injection, session) => {
|
135 | assertTargetType(injection, ContextView);
|
136 | const bindingFilter = filterByExtensionPoint(
|
137 | injection,
|
138 | session,
|
139 | extensionPointName,
|
140 | );
|
141 | return ctx.createView(
|
142 | bindingFilter,
|
143 | injection.metadata.bindingComparator,
|
144 | metadata,
|
145 | );
|
146 | },
|
147 | );
|
148 | }
|
149 |
|
150 | /**
|
151 | * Inject an array of resolved extension instances for the extension point.
|
152 | * The list is a snapshot of registered extensions when the injection is
|
153 | * fulfilled. Extensions added or removed afterward won't impact the list.
|
154 | *
|
155 | * @example
|
156 | * ```ts
|
157 | * import {extensionPoint, extensions} from '@loopback/core';
|
158 | *
|
159 | * @extensionPoint(GREETER_EXTENSION_POINT_NAME)
|
160 | * export class GreetingService {
|
161 | * constructor(
|
162 | * @extensions.list() // Inject an array of extensions for the extension point
|
163 | * private greeters: Greeter[],
|
164 | * // ...
|
165 | * ) {
|
166 | * // ...
|
167 | * }
|
168 | * ```
|
169 | * @param extensionPointName - Name of the extension point. If not supplied, we
|
170 | * use the `name` tag from the extension point binding or the class name of the
|
171 | * extension point class. If a class needs to inject extensions from multiple
|
172 | * extension points, use different `extensionPointName` for different types of
|
173 | * extensions.
|
174 | * @param metadata - Optional injection metadata
|
175 | */
|
176 | export function list(
|
177 | extensionPointName?: string,
|
178 | metadata?: InjectionMetadata,
|
179 | ) {
|
180 | return inject(
|
181 | '',
|
182 | {...metadata, decorator: '@extensions.instances'},
|
183 | (ctx, injection, session) => {
|
184 | assertTargetType(injection, Array);
|
185 | const bindingFilter = filterByExtensionPoint(
|
186 | injection,
|
187 | session,
|
188 | extensionPointName,
|
189 | );
|
190 | const viewForExtensions = new ContextView(
|
191 | ctx,
|
192 | bindingFilter,
|
193 | injection.metadata.bindingComparator,
|
194 | );
|
195 | return viewForExtensions.resolve({
|
196 | ...metadata,
|
197 | ...asResolutionOptions(session),
|
198 | });
|
199 | },
|
200 | );
|
201 | }
|
202 | }
|
203 |
|
204 | /**
|
205 | * Create a binding filter for `@extensions.*`
|
206 | * @param injection - Injection object
|
207 | * @param session - Resolution session
|
208 | * @param extensionPointName - Extension point name
|
209 | */
|
210 | function filterByExtensionPoint(
|
211 | injection: Readonly<Injection<unknown>>,
|
212 | session: ResolutionSession,
|
213 | extensionPointName?: string,
|
214 | ) {
|
215 | extensionPointName =
|
216 | extensionPointName ??
|
217 | inferExtensionPointName(injection.target, session.currentBinding);
|
218 | return extensionFilter(extensionPointName);
|
219 | }
|
220 |
|
221 | /**
|
222 | * Infer the extension point name from binding tags/class name
|
223 | * @param injectionTarget - Target class or prototype
|
224 | * @param currentBinding - Current binding
|
225 | */
|
226 | function inferExtensionPointName(
|
227 | injectionTarget: object,
|
228 | currentBinding?: Readonly<Binding<unknown>>,
|
229 | ): string {
|
230 | if (currentBinding) {
|
231 | const name =
|
232 | currentBinding.tagMap[CoreTags.EXTENSION_POINT] ||
|
233 | currentBinding.tagMap[ContextTags.NAME];
|
234 |
|
235 | if (name) return name;
|
236 | }
|
237 |
|
238 | let target: Function;
|
239 | if (typeof injectionTarget === 'function') {
|
240 | // Constructor injection
|
241 | target = injectionTarget;
|
242 | } else {
|
243 | // Injection on the prototype
|
244 | target = injectionTarget.constructor;
|
245 | }
|
246 | return target.name;
|
247 | }
|
248 |
|
249 | /**
|
250 | * A factory function to create binding filter for extensions of a named
|
251 | * extension point
|
252 | * @param extensionPointNames - A list of names of extension points
|
253 | */
|
254 | export function extensionFilter(
|
255 | ...extensionPointNames: string[]
|
256 | ): BindingFilter {
|
257 | return filterByTag({
|
258 | [CoreTags.EXTENSION_FOR]: includesTagValue(...extensionPointNames),
|
259 | });
|
260 | }
|
261 |
|
262 | /**
|
263 | * A factory function to create binding template for extensions of the given
|
264 | * extension point
|
265 | * @param extensionPointNames - Names of the extension point
|
266 | */
|
267 | export function extensionFor(
|
268 | ...extensionPointNames: string[]
|
269 | ): BindingTemplate {
|
270 | return binding => {
|
271 | if (extensionPointNames.length === 0) return;
|
272 | let extensionPoints = binding.tagMap[CoreTags.EXTENSION_FOR];
|
273 | // Normalize extensionPoints to string[]
|
274 | if (extensionPoints == null) {
|
275 | extensionPoints = [];
|
276 | } else if (typeof extensionPoints === 'string') {
|
277 | extensionPoints = [extensionPoints];
|
278 | }
|
279 |
|
280 | // Add extension points
|
281 | for (const extensionPointName of extensionPointNames) {
|
282 | if (!extensionPoints.includes(extensionPointName)) {
|
283 | extensionPoints.push(extensionPointName);
|
284 | }
|
285 | }
|
286 | if (extensionPoints.length === 1) {
|
287 | // Keep the value as string for backward compatibility
|
288 | extensionPoints = extensionPoints[0];
|
289 | }
|
290 | binding.tag({[CoreTags.EXTENSION_FOR]: extensionPoints});
|
291 | };
|
292 | }
|
293 |
|
294 | /**
|
295 | * Register an extension for the given extension point to the context
|
296 | * @param context - Context object
|
297 | * @param extensionPointName - Name of the extension point
|
298 | * @param extensionClass - Class or a provider for an extension
|
299 | * @param options - Options Options for the creation of binding from class
|
300 | */
|
301 | export function addExtension(
|
302 | context: Context,
|
303 | extensionPointName: string,
|
304 | extensionClass: Constructor<unknown>,
|
305 | options?: BindingFromClassOptions,
|
306 | ) {
|
307 | const binding = createBindingFromClass(extensionClass, options).apply(
|
308 | extensionFor(extensionPointName),
|
309 | );
|
310 | context.add(binding);
|
311 | return binding;
|
312 | }
|