@loopback/core
Version:
Define and implement core constructs such as Application and Component
313 lines (299 loc) • 9.11 kB
text/typescript
// 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<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 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<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 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<Injection<unknown>>,
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<Binding<unknown>>,
): 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<unknown>,
options?: BindingFromClassOptions,
) {
const binding = createBindingFromClass(extensionClass, options).apply(
extensionFor(extensionPointName),
);
context.add(binding);
return binding;
}