Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/site/Binding.md
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,8 @@ parameter of `BindingFromClassOptions` type with the following settings:
}
```

- defaultNamespace: Default namespace if namespace or namespace tag does not
exist
- defaultScope: Default scope if the binding does not have an explicit scope
set. The `scope` from `@bind` of the bound class takes precedence.

Expand Down
97 changes: 87 additions & 10 deletions docs/site/Interceptors.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,11 @@ method level interceptors. For example, the following code registers a global
`caching-interceptor` for all methods.

```ts
app
.bind('caching-interceptor')
.toProvider(CachingInterceptorProvider)
.apply(asGlobalInterceptor('caching'));
app.interceptor(CachingInterceptorProvider, {
global: true,
group: 'caching',
key: 'caching-interceptor',
});
```

Global interceptors are also executed for route handler functions without a
Expand Down Expand Up @@ -332,13 +333,13 @@ class MyControllerWithClassLevelInterceptors {
### Global interceptors

Global interceptors are discovered from the `InvocationContext`. They are
registered as bindings with `interceptor` tag. For example,
registered as bindings with `globalInterceptor` tag. For example,

```ts
import {asGlobalInterceptor} from '@loopback/context';

app
.bind('interceptors.MetricsInterceptor')
.bind('globalInterceptors.MetricsInterceptor')
.toProvider(MetricsInterceptorProvider)
.apply(asGlobalInterceptor('metrics'));
```
Expand All @@ -352,6 +353,12 @@ templates that mark bindings of global interceptors. It takes an optional
- - `asGlobalInterceptor()`: mark a binding as a global interceptor in the
default group

The registration can be further simplified as:

```ts
app.interceptor(MetricsInterceptorProvider, {global: true, group: 'metrics'});
```

### Order of invocation for interceptors

Multiple `@intercept` decorators can be applied to a class or a method. The
Expand Down Expand Up @@ -435,10 +442,11 @@ Global interceptors can be sorted as follows:
example:

```ts
app
.bind('globalInterceptors.authInterceptor')
.to(authInterceptor)
.apply(asGlobalInterceptor('auth'));
app.interceptor(authInterceptor, {
name: 'authInterceptor',
global: true,
group: 'auth',
});
```

If the group tag does not exist, the value is default to `''`.
Expand Down Expand Up @@ -732,3 +740,72 @@ Here are some example interceptor functions:
return result;
};
```

### Compose multiple interceptors

Sometimes we want to apply more than one interceptors together as a whole. It
can be done by `composeInterceptors`:

```ts
import {composeInterceptors} from '@loopback/context';

const interceptor = composeInterceptors(
interceptorFn1,
'interceptors.my-interceptor',
interceptorFn2,
);
```

The code above composes `interceptorFn1` and `interceptorFn2` functions with
`interceptors.my-interceptor` binding key into a single interceptor.

### Build your own interceptor chains

Behind the scenes, interceptors are chained one by one by their orders into an
invocation chain.
[GenericInvocationChain](https://loopback.io/doc/en/lb4/apidocs.context.genericinterceptorchain.html)
is the base class that can be extended to create your own flavor of interceptors
and chains. For example,

```ts
import {GenericInvocationChain, GenericInterceptor} from '@loopback/context';
import {RequestContext} from '@loopback/rest';

export interface RequestInterceptor
extends GenericInterceptor<RequestContext> {}

export class RequestInterceptorChain extends GenericInterceptorChain<
RequestContext
> {}
```

The interceptor chain can be instantiated in two styles:

- with a list of interceptor functions or binding keys
- with a binding filter function to discover matching interceptors within the
context

Once the chain is built, it can be invoked using `invokeInterceptors`:

```ts
const chain = new RequestInterceptorChain(requestCtx, interceptors);
await chain.invokeInterceptors();
```

It's also possible to pass in a final handler:

```ts
import {Next} from '@loopback/context';
const finalHandler: Next = async () => {
// return ...;
};
await chain.invokeInterceptors(finalHandler);
```

The invocation chain itself can be used a single interceptor so that it be
registered as one handler to another chain.

```ts
const chain = new RequestInterceptorChain(requestCtx, interceptors);
const interceptor = chain.asInterceptor();
```
7 changes: 1 addition & 6 deletions packages/boot/src/booters/interceptor.booter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@
// License text available at https://opensource.org/licenses/MIT

import {
BindingScope,
config,
Constructor,
createBindingFromClass,
inject,
Interceptor,
Provider,
Expand Down Expand Up @@ -59,10 +57,7 @@ export class InterceptorProviderBooter extends BaseArtifactBooter {
this.interceptors = this.classes as InterceptorProviderClass[];
for (const interceptor of this.interceptors) {
debug('Bind interceptor: %s', interceptor.name);
const binding = createBindingFromClass(interceptor, {
defaultScope: BindingScope.TRANSIENT,
});
this.app.add(binding);
const binding = this.app.interceptor(interceptor);
debug('Binding created for interceptor: %j', binding);
}
}
Expand Down
28 changes: 28 additions & 0 deletions packages/context/src/__tests__/unit/binding-inspector.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
createBindingFromClass,
Provider,
} from '../..';
import {ContextTags} from '../../keys';

describe('createBindingFromClass()', () => {
it('inspects classes', () => {
Expand Down Expand Up @@ -206,6 +207,33 @@ describe('createBindingFromClass()', () => {
expect(binding.key).to.eql('services.MyService');
});

it('honors default namespace with options', () => {
class MyService {}

@bind({tags: {[ContextTags.NAMESPACE]: 'my-services'}})
class MyServiceWithNS {}

const ctx = new Context();
let binding = givenBindingFromClass(MyService, ctx, {
defaultNamespace: 'services',
});

expect(binding.key).to.eql('services.MyService');

binding = givenBindingFromClass(MyService, ctx, {
namespace: 'my-services',
defaultNamespace: 'services',
});

expect(binding.key).to.eql('my-services.MyService');

binding = givenBindingFromClass(MyServiceWithNS, ctx, {
defaultNamespace: 'services',
});

expect(binding.key).to.eql('my-services.MyServiceWithNS');
});

it('includes class name in error messages', () => {
expect(() => {
// Reproduce a problem that @bajtos encountered when the project
Expand Down
7 changes: 3 additions & 4 deletions packages/context/src/__tests__/unit/binding-key.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import {expect} from '@loopback/testlab';
import {BindingKey} from '../..';
import {UUID_PATTERN} from '../../value-promise';

describe('BindingKey', () => {
describe('create', () => {
Expand Down Expand Up @@ -62,17 +63,15 @@ describe('BindingKey', () => {
describe('generate', () => {
it('generates binding key without namespace', () => {
const key1 = BindingKey.generate().key;
expect(key1).to.match(
/^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,
);
expect(key1).to.match(UUID_PATTERN);
const key2 = BindingKey.generate().key;
expect(key1).to.not.eql(key2);
});

it('generates binding key with namespace', () => {
const key1 = BindingKey.generate('services').key;
expect(key1).to.match(
/^services\.[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,
new RegExp(`^services\\.${UUID_PATTERN.source}$`, 'i'),
);
const key2 = BindingKey.generate('services').key;
expect(key1).to.not.eql(key2);
Expand Down
7 changes: 3 additions & 4 deletions packages/context/src/__tests__/unit/context.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
isPromiseLike,
Provider,
} from '../..';
import {UUID_PATTERN} from '../../value-promise';

/**
* Create a subclass of context so that we can access parents and registry
Expand Down Expand Up @@ -45,15 +46,13 @@ class TestContext extends Context {
describe('Context constructor', () => {
it('generates uuid name if not provided', () => {
const ctx = new Context();
expect(ctx.name).to.match(
/^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,
);
expect(ctx.name).to.match(UUID_PATTERN);
});

it('adds subclass name as the prefix', () => {
const ctx = new TestContext();
expect(ctx.name).to.match(
/^TestContext-[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i,
new RegExp(`^TestContext-${UUID_PATTERN.source}$`, 'i'),
);
});

Expand Down
49 changes: 49 additions & 0 deletions packages/context/src/__tests__/unit/interceptor-chain.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import {expect} from '@loopback/testlab';
import {
compareBindingsByTag,
composeInterceptors,
Context,
filterByTag,
GenericInterceptor,
Expand Down Expand Up @@ -54,6 +55,20 @@ describe('GenericInterceptorChain', () => {
expect(result).to.eql('ABC');
});

it('honors final handler', async () => {
givenInterceptorChain(
givenNamedInterceptor('interceptor1'),
async (context, next) => {
return next();
},
);
const finalHandler = () => {
return 'final';
};
const result = await interceptorChain.invokeInterceptors(finalHandler);
expect(result).to.eql('final');
});

it('skips downstream interceptors if next is not invoked', async () => {
givenInterceptorChain(async (context, next) => {
return 'ABC';
Expand Down Expand Up @@ -157,6 +172,40 @@ describe('GenericInterceptorChain', () => {
]);
});

it('can be used as an interceptor', async () => {
givenInterceptorChain(
givenNamedInterceptor('interceptor1'),
async (context, next) => {
await next();
return 'ABC';
},
);
const interceptor = interceptorChain.asInterceptor();
let invoked = false;
await interceptor(new Context(), () => {
invoked = true;
return invoked;
});
expect(invoked).to.be.true();
});

it('composes multiple interceptors as a single interceptor', async () => {
const interceptor = composeInterceptors(
givenNamedInterceptor('interceptor1'),
async (context, next) => {
await next();
return 'ABC';
},
);
let invoked = false;
const result = await interceptor(new Context(), () => {
invoked = true;
return invoked;
});
expect(invoked).to.be.true();
expect(result).to.eql('ABC');
});

function givenContext() {
events = [];
ctx = new Context();
Expand Down
Loading