Skip to content

Commit

Permalink
feat(context): add support for method interceptors
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Apr 3, 2019
1 parent ed3d104 commit 0486447
Show file tree
Hide file tree
Showing 7 changed files with 745 additions and 2 deletions.
3 changes: 3 additions & 0 deletions docs/site/Concepts.md
Expand Up @@ -37,6 +37,9 @@ LoopBack 4 introduces some new concepts that are important to understand:
Controller operates only on processed input and abstractions of backend
services / databases.

- [**Interceptors**](Interceptors.md): A function that intercepts static or
instance method invocations on a class or object.

- [**Route**](Routes.md): The mapping between your API specification and an
Operation. It tells LoopBack which Operation to `invoke()` when given an HTTP
request.
Expand Down
212 changes: 212 additions & 0 deletions docs/site/Interceptors.md
@@ -0,0 +1,212 @@
---
lang: en
title: 'Interceptors'
keywords: LoopBack 4.0, LoopBack 4
sidebar: lb4_sidebar
permalink: /doc/en/lb4/Interceptors.html
---

## Overview

Interceptors are reusable functions to provide aspect-oriented logic around
method invocations. There are many use cases for interceptors, such as:

- Add extra logic before / after method invocation, for example, logging or
measuring method invocations.
- Validate/transform arguments
- Validate/transform return values
- Catch/transform errors, for example, normalize error objects
- Override the method invocation, for example, return from cache

## Interceptor functions

The interceptor function has the following signature:

```ts
/**
* Interceptor function
*/
export interface Interceptor {
<T>(
context: InvocationContext,
next: () => ValueOrPromise<T>,
): ValueOrPromise<T>;
}
```

An interceptor is responsible for calling `next()` if it wants to proceed with
next interceptor or the target method invocation.

### Invocation context

The invocation context provides access to metadata for the given invocation in
addition to the parent `Context` that can be used to locate other bindings.

```ts
/**
* InvocationContext for method invocations
*/
export class InvocationContext extends Context {
/**
* Construct a new instance
* @param parent Parent context
* @param target Target class or object
* @param methodName Method name
* @param args An array of arguments
*/
constructor(
parent: Context,
public readonly target: object,
public readonly methodName: string,
public readonly args: any[],
) {
super(parent);
}
}
```

### Example interceptors

```ts
let events: string[] = [];
const logSync: Interceptor = (invocationCtx, next) => {
events.push('logSync: before-' + invocationCtx.methodName);
// We don't wait for `next()` to return even when if the method or an
// interceptor is async
const result = next();
// It's possible that the statement below is executed before the method
// returns
events.push('logSync: after-' + invocationCtx.methodName);
return result;
};

const log: Interceptor = async (invocationCtx, next) => {
events.push('log: before-' + invocationCtx.methodName);
// Wait until the interceptor/method chain returns
const result = await next();
events.push('log: after-' + invocationCtx.methodName);
return result;
};

const logError: Interceptor = async (invocationCtx, next) => {
events.push('logError: before-' + invocationCtx.methodName);
try {
const result = await next();
events.push('logError: after-' + invocationCtx.methodName);
return result;
} catch (err) {
events.push('logError: error-' + invocationCtx.methodName);
throw err;
}
};

// An interceptor to convert `name` arg to upper case
const convertName: Interceptor = async (invocationCtx, next) => {
events.push('convertName: before-' + invocationCtx.methodName);
invocationCtx.args[0] = (invocationCtx.args[0] as string).toUpperCase();
const result = await next();
events.push('convertName: after-' + invocationCtx.methodName);
return result;
};
```

## @intercept

The `@intercept` decorator adds interceptors to a class or its methods including
static and instance methods.

### Method level interceptors

A static or prototype method on a class can be decorated with `@intercept` to
apply interceptors. For example,

```ts
class MyController {
@intercept(log)
static async greetStatic(name: string) {
return `Hello, ${name}`;
}

@intercept(log)
static async greetStaticWithDI(@inject('name') name: string) {
return `Hello, ${name}`;
}

@intercept(logSync)
greetSync(name: string) {
return `Hello, ${name}`;
}

@intercept(log)
greet(name: string) {
return `Hello, ${name}`;
}

@intercept('log')
async hello(name: string) {
return `Hello, ${name}`;
}

@intercept(log)
async greetWithDI(@inject('name') name: string) {
return `Hello, ${name}`;
}

@intercept('log', logSync)
async helloWithTwoInterceptors(name: string) {
return `Hello, ${name}`;
}

async helloWithoutInterceptors(name: string) {
return `Hello, ${name}`;
}

@intercept(changeName)
async helloWithChangeName(name: string) {
return `Hello, ${name}`;
}

@intercept(logWithErrorHandling)
async helloWithError(name: string) {
throw new Error('error: ' + name);
}
}
```

### Class level interceptors

To apply interceptors to be invoked for all methods on a class, we can use
`@intercept` to decorate the class. When a method is invoked, class level
interceptors (if not explicitly listed at method level) are invoked before
method level ones.

```ts
@intercept(log)
class MyControllerWithClassLevelInterceptors {
static async greetStatic(name: string) {
return `Hello, ${name}`;
}

@intercept(log)
static async greetStaticWithDI(@inject('name') name: string) {
return `Hello, ${name}`;
}

@intercept(log, logSync)
greetSync(name: string) {
return `Hello, ${name}`;
}

@intercept(convertName, log)
async greet(name: string) {
return `Hello, ${name}`;
}
}
```

Here is the list of interceptors invoked for each method:

- greetStatic: `log`
- greetStaticWithDI: `log`
- greetSync: `log`, `logSync`
- greet: `convertName`, `log`
4 changes: 4 additions & 0 deletions docs/site/sidebars/lb4_sidebar.yml
Expand Up @@ -141,6 +141,10 @@ children:
url: Controllers.html
output: 'web, pdf'

- title: 'Interceptors'
url: Interceptors.html
output: 'web, pdf'

- title: 'DataSources'
url: DataSources.html
output: 'web, pdf'
Expand Down

0 comments on commit 0486447

Please sign in to comment.