0
# Extension Points
1
2
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.
3
4
## Capabilities
5
6
### Extension Point Decorator
7
8
Decorator for marking classes as extension points that can accept extensions from other parts of the application.
9
10
```typescript { .api }
11
/**
12
* Decorate a class as a named extension point. If the decoration is not
13
* present, the name of the class will be used.
14
*/
15
function extensionPoint(name: string, ...specs: BindingSpec[]): ClassDecorator;
16
```
17
18
**Usage Examples:**
19
20
```typescript
21
import { extensionPoint, extensions, Getter } from "@loopback/core";
22
23
// Basic extension point
24
const GREETER_EXTENSION_POINT = 'greeter';
25
26
@extensionPoint(GREETER_EXTENSION_POINT)
27
class GreetingService {
28
constructor(
29
@extensions() private getGreeters: Getter<Greeter[]>
30
) {}
31
32
async greet(name: string): Promise<string[]> {
33
const greeters = await this.getGreeters();
34
return Promise.all(
35
greeters.map(greeter => greeter.greet(name))
36
);
37
}
38
}
39
40
// Extension point with binding specifications
41
@extensionPoint('validator', {scope: BindingScope.SINGLETON})
42
class ValidationService {
43
constructor(
44
@extensions() private getValidators: Getter<Validator[]>
45
) {}
46
47
async validate(data: any): Promise<ValidationResult> {
48
const validators = await this.getValidators();
49
const results = await Promise.all(
50
validators.map(validator => validator.validate(data))
51
);
52
53
return this.combineResults(results);
54
}
55
56
private combineResults(results: ValidationResult[]): ValidationResult {
57
// Combine validation results
58
return {
59
isValid: results.every(r => r.isValid),
60
errors: results.flatMap(r => r.errors)
61
};
62
}
63
}
64
```
65
66
### Extensions Injection
67
68
Decorator and utility functions for injecting extensions into extension points.
69
70
```typescript { .api }
71
/**
72
* Shortcut to inject extensions for the given extension point.
73
*/
74
function extensions(
75
extensionPointName?: string,
76
metadata?: InjectionMetadata
77
): PropertyDecorator & ParameterDecorator;
78
```
79
80
**Usage Examples:**
81
82
```typescript
83
import { extensionPoint, extensions, Getter } from "@loopback/core";
84
85
interface AuthenticationStrategy {
86
name: string;
87
authenticate(credentials: any): Promise<UserProfile | null>;
88
}
89
90
@extensionPoint('authentication-strategy')
91
class AuthenticationService {
92
constructor(
93
@extensions() // Extension point name inferred from class
94
private getStrategies: Getter<AuthenticationStrategy[]>
95
) {}
96
97
async authenticate(strategyName: string, credentials: any): Promise<UserProfile | null> {
98
const strategies = await this.getStrategies();
99
const strategy = strategies.find(s => s.name === strategyName);
100
101
if (!strategy) {
102
throw new Error(`Authentication strategy '${strategyName}' not found`);
103
}
104
105
return strategy.authenticate(credentials);
106
}
107
108
async getAvailableStrategies(): Promise<string[]> {
109
const strategies = await this.getStrategies();
110
return strategies.map(s => s.name);
111
}
112
}
113
114
// Explicit extension point name
115
@extensionPoint('custom-processor')
116
class DataProcessingService {
117
constructor(
118
@extensions('data-processor') // Explicit extension point name
119
private getProcessors: Getter<DataProcessor[]>
120
) {}
121
122
async processData(data: any): Promise<any> {
123
const processors = await this.getProcessors();
124
let result = data;
125
126
for (const processor of processors) {
127
result = await processor.process(result);
128
}
129
130
return result;
131
}
132
}
133
```
134
135
### Extensions Context View
136
137
Inject a ContextView for extensions to listen for dynamic changes in available extensions.
138
139
```typescript { .api }
140
namespace extensions {
141
/**
142
* Inject a ContextView for extensions of the extension point. The view can
143
* then be listened on events such as bind, unbind, or refresh to react
144
* on changes of extensions.
145
*/
146
function view(
147
extensionPointName?: string,
148
metadata?: InjectionMetadata
149
): PropertyDecorator & ParameterDecorator;
150
}
151
```
152
153
**Usage Examples:**
154
155
```typescript
156
import { extensionPoint, extensions, ContextView } from "@loopback/core";
157
158
interface Plugin {
159
name: string;
160
version: string;
161
initialize(): Promise<void>;
162
destroy(): Promise<void>;
163
}
164
165
@extensionPoint('plugin-system')
166
class PluginManager {
167
private initializedPlugins = new Set<string>();
168
169
constructor(
170
@extensions.view() private pluginsView: ContextView<Plugin>
171
) {
172
// Listen for plugin additions and removals
173
this.pluginsView.on('bind', this.onPluginAdded.bind(this));
174
this.pluginsView.on('unbind', this.onPluginRemoved.bind(this));
175
}
176
177
private async onPluginAdded(event: ContextViewEvent<Plugin>): Promise<void> {
178
const plugin = await event.binding.getValue(this.pluginsView.context);
179
console.log(`Plugin added: ${plugin.name} v${plugin.version}`);
180
181
await plugin.initialize();
182
this.initializedPlugins.add(plugin.name);
183
}
184
185
private async onPluginRemoved(event: ContextViewEvent<Plugin>): Promise<void> {
186
const plugin = await event.binding.getValue(this.pluginsView.context);
187
console.log(`Plugin removed: ${plugin.name}`);
188
189
if (this.initializedPlugins.has(plugin.name)) {
190
await plugin.destroy();
191
this.initializedPlugins.delete(plugin.name);
192
}
193
}
194
195
async getActivePlugins(): Promise<Plugin[]> {
196
return this.pluginsView.values();
197
}
198
}
199
```
200
201
### Extensions List Injection
202
203
Inject a snapshot array of resolved extension instances.
204
205
```typescript { .api }
206
namespace extensions {
207
/**
208
* Inject an array of resolved extension instances for the extension point.
209
* The list is a snapshot of registered extensions when the injection is
210
* fulfilled. Extensions added or removed afterward won't impact the list.
211
*/
212
function list(
213
extensionPointName?: string,
214
metadata?: InjectionMetadata
215
): PropertyDecorator & ParameterDecorator;
216
}
217
```
218
219
**Usage Examples:**
220
221
```typescript
222
import { extensionPoint, extensions } from "@loopback/core";
223
224
interface Middleware {
225
name: string;
226
priority: number;
227
execute(context: RequestContext, next: () => Promise<void>): Promise<void>;
228
}
229
230
@extensionPoint('middleware')
231
class MiddlewareChain {
232
constructor(
233
@extensions.list() private middlewareList: Middleware[]
234
) {
235
// Sort middleware by priority
236
this.middlewareList.sort((a, b) => a.priority - b.priority);
237
}
238
239
async executeChain(context: RequestContext): Promise<void> {
240
let index = 0;
241
242
const next = async (): Promise<void> => {
243
if (index < this.middlewareList.length) {
244
const middleware = this.middlewareList[index++];
245
await middleware.execute(context, next);
246
}
247
};
248
249
await next();
250
}
251
252
getMiddlewareNames(): string[] {
253
return this.middlewareList.map(m => m.name);
254
}
255
}
256
```
257
258
### Extension Registration
259
260
Functions for programmatically registering extensions to extension points.
261
262
```typescript { .api }
263
/**
264
* Register an extension for the given extension point to the context
265
*/
266
function addExtension(
267
context: Context,
268
extensionPointName: string,
269
extensionClass: Constructor<unknown>,
270
options?: BindingFromClassOptions
271
): Binding<unknown>;
272
273
/**
274
* A factory function to create binding template for extensions of the given
275
* extension point
276
*/
277
function extensionFor(
278
...extensionPointNames: string[]
279
): BindingTemplate;
280
281
/**
282
* A factory function to create binding filter for extensions of a named
283
* extension point
284
*/
285
function extensionFilter(
286
...extensionPointNames: string[]
287
): BindingFilter;
288
```
289
290
**Usage Examples:**
291
292
```typescript
293
import {
294
addExtension,
295
extensionFor,
296
extensionFilter,
297
Context,
298
Application
299
} from "@loopback/core";
300
301
// Extension implementations
302
class EmailNotificationProvider implements NotificationProvider {
303
async send(message: string, recipient: string): Promise<void> {
304
console.log(`Email to ${recipient}: ${message}`);
305
}
306
}
307
308
class SmsNotificationProvider implements NotificationProvider {
309
async send(message: string, recipient: string): Promise<void> {
310
console.log(`SMS to ${recipient}: ${message}`);
311
}
312
}
313
314
class SlackNotificationProvider implements NotificationProvider {
315
async send(message: string, channel: string): Promise<void> {
316
console.log(`Slack to ${channel}: ${message}`);
317
}
318
}
319
320
const app = new Application();
321
const NOTIFICATION_EXTENSION_POINT = 'notification-provider';
322
323
// Register extensions using addExtension
324
addExtension(
325
app,
326
NOTIFICATION_EXTENSION_POINT,
327
EmailNotificationProvider,
328
{ name: 'email-provider' }
329
);
330
331
addExtension(
332
app,
333
NOTIFICATION_EXTENSION_POINT,
334
SmsNotificationProvider,
335
{ name: 'sms-provider' }
336
);
337
338
// Register extension using binding template
339
app.bind('notification.slack')
340
.toClass(SlackNotificationProvider)
341
.apply(extensionFor(NOTIFICATION_EXTENSION_POINT));
342
343
// Find all extensions using filter
344
const extensionBindings = app.find(extensionFilter(NOTIFICATION_EXTENSION_POINT));
345
console.log('Found extensions:', extensionBindings.map(b => b.key));
346
```
347
348
### Multi-Extension Point Support
349
350
Extensions can contribute to multiple extension points simultaneously.
351
352
**Usage Examples:**
353
354
```typescript
355
import { extensionFor, addExtension } from "@loopback/core";
356
357
// Extension that implements multiple interfaces
358
class LoggingAuditProvider implements Logger, AuditTrail {
359
// Logger interface
360
log(level: string, message: string): void {
361
console.log(`[${level}] ${message}`);
362
}
363
364
// AuditTrail interface
365
recordEvent(event: AuditEvent): void {
366
console.log(`Audit: ${event.action} by ${event.user}`);
367
}
368
}
369
370
const app = new Application();
371
372
// Register for multiple extension points
373
app.bind('logging-audit-provider')
374
.toClass(LoggingAuditProvider)
375
.apply(extensionFor('logger', 'audit-trail'));
376
377
// Or using addExtension (requires multiple calls)
378
const context = app;
379
addExtension(context, 'logger', LoggingAuditProvider);
380
addExtension(context, 'audit-trail', LoggingAuditProvider);
381
```
382
383
### Extension Point Discovery
384
385
How to discover and work with multiple extension points.
386
387
**Usage Examples:**
388
389
```typescript
390
import {
391
extensionPoint,
392
extensions,
393
extensionFilter,
394
Context,
395
ContextView
396
} from "@loopback/core";
397
398
// Service that manages multiple extension points
399
class ExtensionManager {
400
constructor(
401
@inject.context() private context: Context
402
) {}
403
404
async getExtensionsForPoint(extensionPointName: string): Promise<any[]> {
405
const filter = extensionFilter(extensionPointName);
406
const view = new ContextView(this.context, filter);
407
return view.values();
408
}
409
410
async getAllExtensionPoints(): Promise<string[]> {
411
const bindings = this.context.find(binding =>
412
binding.tagMap[CoreTags.EXTENSION_POINT]
413
);
414
415
return bindings.map(binding =>
416
binding.tagMap[CoreTags.EXTENSION_POINT]
417
).filter(Boolean);
418
}
419
420
async getExtensionInfo(): Promise<ExtensionInfo[]> {
421
const extensionPoints = await this.getAllExtensionPoints();
422
const info: ExtensionInfo[] = [];
423
424
for (const point of extensionPoints) {
425
const extensions = await this.getExtensionsForPoint(point);
426
info.push({
427
extensionPoint: point,
428
extensionCount: extensions.length,
429
extensions: extensions.map(ext => ext.constructor.name)
430
});
431
}
432
433
return info;
434
}
435
}
436
437
interface ExtensionInfo {
438
extensionPoint: string;
439
extensionCount: number;
440
extensions: string[];
441
}
442
```
443
444
### Advanced Extension Patterns
445
446
Complex extension scenarios with filtering, sorting, and conditional loading.
447
448
**Usage Examples:**
449
450
```typescript
451
import {
452
extensionPoint,
453
extensions,
454
ContextView,
455
BindingFilter
456
} from "@loopback/core";
457
458
interface Handler {
459
name: string;
460
priority: number;
461
condition?: (context: any) => boolean;
462
handle(data: any): Promise<any>;
463
}
464
465
@extensionPoint('request-handler')
466
class RequestProcessor {
467
constructor(
468
@extensions.view() private handlersView: ContextView<Handler>
469
) {}
470
471
async processRequest(data: any, context: any): Promise<any> {
472
const handlers = await this.getApplicableHandlers(context);
473
474
// Sort by priority
475
handlers.sort((a, b) => a.priority - b.priority);
476
477
let result = data;
478
for (const handler of handlers) {
479
console.log(`Processing with handler: ${handler.name}`);
480
result = await handler.handle(result);
481
}
482
483
return result;
484
}
485
486
private async getApplicableHandlers(context: any): Promise<Handler[]> {
487
const allHandlers = await this.handlersView.values();
488
489
return allHandlers.filter(handler =>
490
!handler.condition || handler.condition(context)
491
);
492
}
493
494
async getHandlerInfo(): Promise<Array<{name: string, priority: number}>> {
495
const handlers = await this.handlersView.values();
496
return handlers.map(h => ({ name: h.name, priority: h.priority }));
497
}
498
}
499
500
// Conditional handler example
501
class AuthHandler implements Handler {
502
name = 'auth-handler';
503
priority = 1;
504
505
condition(context: any): boolean {
506
return context.requiresAuth === true;
507
}
508
509
async handle(data: any): Promise<any> {
510
// Authentication logic
511
return { ...data, authenticated: true };
512
}
513
}
514
515
class ValidationHandler implements Handler {
516
name = 'validation-handler';
517
priority = 0; // Run first
518
519
async handle(data: any): Promise<any> {
520
// Validation logic
521
if (!data.isValid) {
522
throw new Error('Validation failed');
523
}
524
return data;
525
}
526
}
527
```
528
529
## Types
530
531
```typescript { .api }
532
interface InjectionMetadata {
533
optional?: boolean;
534
asProxyWithInterceptors?: boolean;
535
bindingComparator?: BindingComparator;
536
}
537
538
type BindingSpec =
539
| BindingFromClassOptions
540
| BindingTemplate
541
| { [tag: string]: any };
542
543
interface BindingFromClassOptions {
544
name?: string;
545
namespace?: string;
546
type?: string;
547
defaultScope?: BindingScope;
548
}
549
550
type BindingTemplate<T = unknown> = (binding: Binding<T>) => void;
551
552
type BindingFilter = (binding: Readonly<Binding<unknown>>) => boolean;
553
554
type Constructor<T = {}> = new (...args: any[]) => T;
555
```