UNPKG

9.11 kBPlain TextView Raw
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
6import {
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';
28import {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 */
46export 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 */
76export 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
100export 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 */
210function 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 */
226function 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 */
254export 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 */
267export 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 */
301export 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}