// Copyright IBM Corp. and LoopBack contributors 2017,2020. All Rights Reserved. // Node module: @loopback/core // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT import { asResolutionOptions, assertTargetType, Binding, BindingFilter, BindingFromClassOptions, BindingSpec, BindingTemplate, Constructor, Context, ContextTags, ContextView, createBindingFromClass, createViewGetter, filterByTag, includesTagValue, inject, injectable, Injection, InjectionMetadata, ResolutionSession, } from '@loopback/context'; import {CoreTags} from './keys'; /** * Decorate a class as a named extension point. If the decoration is not * present, the name of the class will be used. * * @example * ```ts * import {extensionPoint} from '@loopback/core'; * * @extensionPoint(GREETER_EXTENSION_POINT_NAME) * export class GreetingService { * // ... * } * ``` * * @param name - Name of the extension point */ export function extensionPoint(name: string, ...specs: BindingSpec[]) { return injectable({tags: {[CoreTags.EXTENSION_POINT]: name}}, ...specs); } /** * Shortcut to inject extensions for the given extension point. * * @example * ```ts * import {Getter} from '@loopback/context'; * import {extensionPoint, extensions} from '@loopback/core'; * * @extensionPoint(GREETER_EXTENSION_POINT_NAME) * export class GreetingService { * constructor( * @extensions() // Inject extensions for the extension point * private getGreeters: Getter, * // ... * ) { * // ... * } * ``` * * @param extensionPointName - Name of the extension point. If not supplied, we * use the `name` tag from the extension point binding or the class name of the * extension point class. If a class needs to inject extensions from multiple * extension points, use different `extensionPointName` for different types of * extensions. * @param metadata - Optional injection metadata */ export function extensions( extensionPointName?: string, metadata?: InjectionMetadata, ) { return inject( '', {...metadata, decorator: '@extensions'}, (ctx, injection, session) => { assertTargetType(injection, Function, 'Getter function'); const bindingFilter = filterByExtensionPoint( injection, session, extensionPointName, ); return createViewGetter( ctx, bindingFilter, injection.metadata.bindingComparator, {...metadata, ...asResolutionOptions(session)}, ); }, ); } export namespace extensions { /** * Inject a `ContextView` for extensions of the extension point. The view can * then be listened on events such as `bind`, `unbind`, or `refresh` to react * on changes of extensions. * * @example * ```ts * import {extensionPoint, extensions} from '@loopback/core'; * * @extensionPoint(GREETER_EXTENSION_POINT_NAME) * export class GreetingService { * constructor( * @extensions.view() // Inject a context view for extensions of the extension point * private greetersView: ContextView, * // ... * ) { * // ... * } * ``` * @param extensionPointName - Name of the extension point. If not supplied, we * use the `name` tag from the extension point binding or the class name of the * extension point class. If a class needs to inject extensions from multiple * extension points, use different `extensionPointName` for different types of * extensions. * @param metadata - Optional injection metadata */ export function view( extensionPointName?: string, metadata?: InjectionMetadata, ) { return inject( '', {...metadata, decorator: '@extensions.view'}, (ctx, injection, session) => { assertTargetType(injection, ContextView); const bindingFilter = filterByExtensionPoint( injection, session, extensionPointName, ); return ctx.createView( bindingFilter, injection.metadata.bindingComparator, metadata, ); }, ); } /** * Inject an array of resolved extension instances for the extension point. * The list is a snapshot of registered extensions when the injection is * fulfilled. Extensions added or removed afterward won't impact the list. * * @example * ```ts * import {extensionPoint, extensions} from '@loopback/core'; * * @extensionPoint(GREETER_EXTENSION_POINT_NAME) * export class GreetingService { * constructor( * @extensions.list() // Inject an array of extensions for the extension point * private greeters: Greeter[], * // ... * ) { * // ... * } * ``` * @param extensionPointName - Name of the extension point. If not supplied, we * use the `name` tag from the extension point binding or the class name of the * extension point class. If a class needs to inject extensions from multiple * extension points, use different `extensionPointName` for different types of * extensions. * @param metadata - Optional injection metadata */ export function list( extensionPointName?: string, metadata?: InjectionMetadata, ) { return inject( '', {...metadata, decorator: '@extensions.instances'}, (ctx, injection, session) => { assertTargetType(injection, Array); const bindingFilter = filterByExtensionPoint( injection, session, extensionPointName, ); const viewForExtensions = new ContextView( ctx, bindingFilter, injection.metadata.bindingComparator, ); return viewForExtensions.resolve({ ...metadata, ...asResolutionOptions(session), }); }, ); } } /** * Create a binding filter for `@extensions.*` * @param injection - Injection object * @param session - Resolution session * @param extensionPointName - Extension point name */ function filterByExtensionPoint( injection: Readonly>, session: ResolutionSession, extensionPointName?: string, ) { extensionPointName = extensionPointName ?? inferExtensionPointName(injection.target, session.currentBinding); return extensionFilter(extensionPointName); } /** * Infer the extension point name from binding tags/class name * @param injectionTarget - Target class or prototype * @param currentBinding - Current binding */ function inferExtensionPointName( injectionTarget: object, currentBinding?: Readonly>, ): string { if (currentBinding) { const name = currentBinding.tagMap[CoreTags.EXTENSION_POINT] || currentBinding.tagMap[ContextTags.NAME]; if (name) return name; } let target: Function; if (typeof injectionTarget === 'function') { // Constructor injection target = injectionTarget; } else { // Injection on the prototype target = injectionTarget.constructor; } return target.name; } /** * A factory function to create binding filter for extensions of a named * extension point * @param extensionPointNames - A list of names of extension points */ export function extensionFilter( ...extensionPointNames: string[] ): BindingFilter { return filterByTag({ [CoreTags.EXTENSION_FOR]: includesTagValue(...extensionPointNames), }); } /** * A factory function to create binding template for extensions of the given * extension point * @param extensionPointNames - Names of the extension point */ export function extensionFor( ...extensionPointNames: string[] ): BindingTemplate { return binding => { if (extensionPointNames.length === 0) return; let extensionPoints = binding.tagMap[CoreTags.EXTENSION_FOR]; // Normalize extensionPoints to string[] if (extensionPoints == null) { extensionPoints = []; } else if (typeof extensionPoints === 'string') { extensionPoints = [extensionPoints]; } // Add extension points for (const extensionPointName of extensionPointNames) { if (!extensionPoints.includes(extensionPointName)) { extensionPoints.push(extensionPointName); } } if (extensionPoints.length === 1) { // Keep the value as string for backward compatibility extensionPoints = extensionPoints[0]; } binding.tag({[CoreTags.EXTENSION_FOR]: extensionPoints}); }; } /** * Register an extension for the given extension point to the context * @param context - Context object * @param extensionPointName - Name of the extension point * @param extensionClass - Class or a provider for an extension * @param options - Options Options for the creation of binding from class */ export function addExtension( context: Context, extensionPointName: string, extensionClass: Constructor, options?: BindingFromClassOptions, ) { const binding = createBindingFromClass(extensionClass, options).apply( extensionFor(extensionPointName), ); context.add(binding); return binding; }