CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-loopback--core

Define and implement core constructs such as Application and Component for LoopBack 4 framework

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

extensions.mddocs/

Extension Points

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.

Capabilities

Extension Point Decorator

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)
    };
  }
}

Extensions Injection

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;
  }
}

Extensions Context View

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();
  }
}

Extensions List Injection

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);
  }
}

Extension Registration

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));

Multi-Extension Point Support

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);

Extension Point Discovery

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[];
}

Advanced Extension Patterns

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;
  }
}

Types

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;

docs

application.md

components.md

context-api.md

extensions.md

index.md

lifecycle.md

services.md

tile.json