or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

application.mdcomponents.mdcontext-api.mdextensions.mdindex.mdlifecycle.mdservices.md

extensions.mddocs/

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

```