Define and implement core constructs such as Application and Component for LoopBack 4 framework
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
The Extension Points system in LoopBack Core provides a plugin architecture that enables applications to define extension points and dynamically register extensions. This pattern allows for highly flexible and extensible applications where functionality can be added or modified without changing core code.
Decorator for marking classes as extension points that can accept extensions from other parts of the application.
/**
* Decorate a class as a named extension point. If the decoration is not
* present, the name of the class will be used.
*/
function extensionPoint(name: string, ...specs: BindingSpec[]): ClassDecorator;Usage Examples:
import { extensionPoint, extensions, Getter } from "@loopback/core";
// Basic extension point
const GREETER_EXTENSION_POINT = 'greeter';
@extensionPoint(GREETER_EXTENSION_POINT)
class GreetingService {
constructor(
@extensions() private getGreeters: Getter<Greeter[]>
) {}
async greet(name: string): Promise<string[]> {
const greeters = await this.getGreeters();
return Promise.all(
greeters.map(greeter => greeter.greet(name))
);
}
}
// Extension point with binding specifications
@extensionPoint('validator', {scope: BindingScope.SINGLETON})
class ValidationService {
constructor(
@extensions() private getValidators: Getter<Validator[]>
) {}
async validate(data: any): Promise<ValidationResult> {
const validators = await this.getValidators();
const results = await Promise.all(
validators.map(validator => validator.validate(data))
);
return this.combineResults(results);
}
private combineResults(results: ValidationResult[]): ValidationResult {
// Combine validation results
return {
isValid: results.every(r => r.isValid),
errors: results.flatMap(r => r.errors)
};
}
}Decorator and utility functions for injecting extensions into extension points.
/**
* Shortcut to inject extensions for the given extension point.
*/
function extensions(
extensionPointName?: string,
metadata?: InjectionMetadata
): PropertyDecorator & ParameterDecorator;Usage Examples:
import { extensionPoint, extensions, Getter } from "@loopback/core";
interface AuthenticationStrategy {
name: string;
authenticate(credentials: any): Promise<UserProfile | null>;
}
@extensionPoint('authentication-strategy')
class AuthenticationService {
constructor(
@extensions() // Extension point name inferred from class
private getStrategies: Getter<AuthenticationStrategy[]>
) {}
async authenticate(strategyName: string, credentials: any): Promise<UserProfile | null> {
const strategies = await this.getStrategies();
const strategy = strategies.find(s => s.name === strategyName);
if (!strategy) {
throw new Error(`Authentication strategy '${strategyName}' not found`);
}
return strategy.authenticate(credentials);
}
async getAvailableStrategies(): Promise<string[]> {
const strategies = await this.getStrategies();
return strategies.map(s => s.name);
}
}
// Explicit extension point name
@extensionPoint('custom-processor')
class DataProcessingService {
constructor(
@extensions('data-processor') // Explicit extension point name
private getProcessors: Getter<DataProcessor[]>
) {}
async processData(data: any): Promise<any> {
const processors = await this.getProcessors();
let result = data;
for (const processor of processors) {
result = await processor.process(result);
}
return result;
}
}Inject a ContextView for extensions to listen for dynamic changes in available extensions.
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.
*/
function view(
extensionPointName?: string,
metadata?: InjectionMetadata
): PropertyDecorator & ParameterDecorator;
}Usage Examples:
import { extensionPoint, extensions, ContextView } from "@loopback/core";
interface Plugin {
name: string;
version: string;
initialize(): Promise<void>;
destroy(): Promise<void>;
}
@extensionPoint('plugin-system')
class PluginManager {
private initializedPlugins = new Set<string>();
constructor(
@extensions.view() private pluginsView: ContextView<Plugin>
) {
// Listen for plugin additions and removals
this.pluginsView.on('bind', this.onPluginAdded.bind(this));
this.pluginsView.on('unbind', this.onPluginRemoved.bind(this));
}
private async onPluginAdded(event: ContextViewEvent<Plugin>): Promise<void> {
const plugin = await event.binding.getValue(this.pluginsView.context);
console.log(`Plugin added: ${plugin.name} v${plugin.version}`);
await plugin.initialize();
this.initializedPlugins.add(plugin.name);
}
private async onPluginRemoved(event: ContextViewEvent<Plugin>): Promise<void> {
const plugin = await event.binding.getValue(this.pluginsView.context);
console.log(`Plugin removed: ${plugin.name}`);
if (this.initializedPlugins.has(plugin.name)) {
await plugin.destroy();
this.initializedPlugins.delete(plugin.name);
}
}
async getActivePlugins(): Promise<Plugin[]> {
return this.pluginsView.values();
}
}Inject a snapshot array of resolved extension instances.
namespace extensions {
/**
* 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.
*/
function list(
extensionPointName?: string,
metadata?: InjectionMetadata
): PropertyDecorator & ParameterDecorator;
}Usage Examples:
import { extensionPoint, extensions } from "@loopback/core";
interface Middleware {
name: string;
priority: number;
execute(context: RequestContext, next: () => Promise<void>): Promise<void>;
}
@extensionPoint('middleware')
class MiddlewareChain {
constructor(
@extensions.list() private middlewareList: Middleware[]
) {
// Sort middleware by priority
this.middlewareList.sort((a, b) => a.priority - b.priority);
}
async executeChain(context: RequestContext): Promise<void> {
let index = 0;
const next = async (): Promise<void> => {
if (index < this.middlewareList.length) {
const middleware = this.middlewareList[index++];
await middleware.execute(context, next);
}
};
await next();
}
getMiddlewareNames(): string[] {
return this.middlewareList.map(m => m.name);
}
}Functions for programmatically registering extensions to extension points.
/**
* Register an extension for the given extension point to the context
*/
function addExtension(
context: Context,
extensionPointName: string,
extensionClass: Constructor<unknown>,
options?: BindingFromClassOptions
): Binding<unknown>;
/**
* A factory function to create binding template for extensions of the given
* extension point
*/
function extensionFor(
...extensionPointNames: string[]
): BindingTemplate;
/**
* A factory function to create binding filter for extensions of a named
* extension point
*/
function extensionFilter(
...extensionPointNames: string[]
): BindingFilter;Usage Examples:
import {
addExtension,
extensionFor,
extensionFilter,
Context,
Application
} from "@loopback/core";
// Extension implementations
class EmailNotificationProvider implements NotificationProvider {
async send(message: string, recipient: string): Promise<void> {
console.log(`Email to ${recipient}: ${message}`);
}
}
class SmsNotificationProvider implements NotificationProvider {
async send(message: string, recipient: string): Promise<void> {
console.log(`SMS to ${recipient}: ${message}`);
}
}
class SlackNotificationProvider implements NotificationProvider {
async send(message: string, channel: string): Promise<void> {
console.log(`Slack to ${channel}: ${message}`);
}
}
const app = new Application();
const NOTIFICATION_EXTENSION_POINT = 'notification-provider';
// Register extensions using addExtension
addExtension(
app,
NOTIFICATION_EXTENSION_POINT,
EmailNotificationProvider,
{ name: 'email-provider' }
);
addExtension(
app,
NOTIFICATION_EXTENSION_POINT,
SmsNotificationProvider,
{ name: 'sms-provider' }
);
// Register extension using binding template
app.bind('notification.slack')
.toClass(SlackNotificationProvider)
.apply(extensionFor(NOTIFICATION_EXTENSION_POINT));
// Find all extensions using filter
const extensionBindings = app.find(extensionFilter(NOTIFICATION_EXTENSION_POINT));
console.log('Found extensions:', extensionBindings.map(b => b.key));Extensions can contribute to multiple extension points simultaneously.
Usage Examples:
import { extensionFor, addExtension } from "@loopback/core";
// Extension that implements multiple interfaces
class LoggingAuditProvider implements Logger, AuditTrail {
// Logger interface
log(level: string, message: string): void {
console.log(`[${level}] ${message}`);
}
// AuditTrail interface
recordEvent(event: AuditEvent): void {
console.log(`Audit: ${event.action} by ${event.user}`);
}
}
const app = new Application();
// Register for multiple extension points
app.bind('logging-audit-provider')
.toClass(LoggingAuditProvider)
.apply(extensionFor('logger', 'audit-trail'));
// Or using addExtension (requires multiple calls)
const context = app;
addExtension(context, 'logger', LoggingAuditProvider);
addExtension(context, 'audit-trail', LoggingAuditProvider);How to discover and work with multiple extension points.
Usage Examples:
import {
extensionPoint,
extensions,
extensionFilter,
Context,
ContextView
} from "@loopback/core";
// Service that manages multiple extension points
class ExtensionManager {
constructor(
@inject.context() private context: Context
) {}
async getExtensionsForPoint(extensionPointName: string): Promise<any[]> {
const filter = extensionFilter(extensionPointName);
const view = new ContextView(this.context, filter);
return view.values();
}
async getAllExtensionPoints(): Promise<string[]> {
const bindings = this.context.find(binding =>
binding.tagMap[CoreTags.EXTENSION_POINT]
);
return bindings.map(binding =>
binding.tagMap[CoreTags.EXTENSION_POINT]
).filter(Boolean);
}
async getExtensionInfo(): Promise<ExtensionInfo[]> {
const extensionPoints = await this.getAllExtensionPoints();
const info: ExtensionInfo[] = [];
for (const point of extensionPoints) {
const extensions = await this.getExtensionsForPoint(point);
info.push({
extensionPoint: point,
extensionCount: extensions.length,
extensions: extensions.map(ext => ext.constructor.name)
});
}
return info;
}
}
interface ExtensionInfo {
extensionPoint: string;
extensionCount: number;
extensions: string[];
}Complex extension scenarios with filtering, sorting, and conditional loading.
Usage Examples:
import {
extensionPoint,
extensions,
ContextView,
BindingFilter
} from "@loopback/core";
interface Handler {
name: string;
priority: number;
condition?: (context: any) => boolean;
handle(data: any): Promise<any>;
}
@extensionPoint('request-handler')
class RequestProcessor {
constructor(
@extensions.view() private handlersView: ContextView<Handler>
) {}
async processRequest(data: any, context: any): Promise<any> {
const handlers = await this.getApplicableHandlers(context);
// Sort by priority
handlers.sort((a, b) => a.priority - b.priority);
let result = data;
for (const handler of handlers) {
console.log(`Processing with handler: ${handler.name}`);
result = await handler.handle(result);
}
return result;
}
private async getApplicableHandlers(context: any): Promise<Handler[]> {
const allHandlers = await this.handlersView.values();
return allHandlers.filter(handler =>
!handler.condition || handler.condition(context)
);
}
async getHandlerInfo(): Promise<Array<{name: string, priority: number}>> {
const handlers = await this.handlersView.values();
return handlers.map(h => ({ name: h.name, priority: h.priority }));
}
}
// Conditional handler example
class AuthHandler implements Handler {
name = 'auth-handler';
priority = 1;
condition(context: any): boolean {
return context.requiresAuth === true;
}
async handle(data: any): Promise<any> {
// Authentication logic
return { ...data, authenticated: true };
}
}
class ValidationHandler implements Handler {
name = 'validation-handler';
priority = 0; // Run first
async handle(data: any): Promise<any> {
// Validation logic
if (!data.isValid) {
throw new Error('Validation failed');
}
return data;
}
}interface InjectionMetadata {
optional?: boolean;
asProxyWithInterceptors?: boolean;
bindingComparator?: BindingComparator;
}
type BindingSpec =
| BindingFromClassOptions
| BindingTemplate
| { [tag: string]: any };
interface BindingFromClassOptions {
name?: string;
namespace?: string;
type?: string;
defaultScope?: BindingScope;
}
type BindingTemplate<T = unknown> = (binding: Binding<T>) => void;
type BindingFilter = (binding: Readonly<Binding<unknown>>) => boolean;
type Constructor<T = {}> = new (...args: any[]) => T;