Skip to content

Commit

Permalink
chore(context): address review comments
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Apr 29, 2019
1 parent 7b15940 commit a8517fa
Show file tree
Hide file tree
Showing 11 changed files with 486 additions and 298 deletions.
40 changes: 34 additions & 6 deletions docs/site/Interceptors.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ const msg = await proxy.greet('John');
```

There is also an `asProxyWithInterceptors` option for binding resolution or
dependency injection to return a proxy to apply interceptors as methods are
invoked.
dependency injection to return a proxy for the class to apply interceptors when
methods are invoked.

```ts
class DummyController {
Expand All @@ -92,12 +92,40 @@ const msg = await dummyController.myController.greet('John');
Or:

```ts
const proxy = await ctx.get<MyController>(
'my-controller',
{asProxyWithInterceptors: true},
const proxy = await ctx.get<MyController>('my-controller', {
asProxyWithInterceptors: true,
});
const msg = await proxy.greet('John');
```

Please note synchronous methods (which don't return `Promise`) are converted to
be asynchronous in the proxy so that interceptors can be applied. For example,

```ts
class MyController {
name: string;

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

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

The proxy from an instance of `MyController` has the `AsyncProxy<MyController>`
type:

```ts
{
name: string; // the same as MyController
greet(name: string): Promise<string>; // the return type becomes `Promise<string>`
hello(name: string): Promise<string>; // the same as MyController
}
```

### Use `invokeMethod` to apply interceptors

To explicitly invoke a method with interceptors, use `invokeMethod` from
Expand Down Expand Up @@ -129,7 +157,7 @@ used by `invokeMethod` or `invokeWithMethodWithInterceptors` functions to
trigger interceptors around the target method. The original method stays intact.
Invoking it directly won't apply any interceptors.

### @intercept
### `@intercept`

Syntax: `@intercept(...interceptorFunctionsOrBindingKeys)`

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright IBM Corp. 2019. All Rights Reserved.
// Node module: @loopback/context
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {expect} from '@loopback/testlab';
import {
AsyncProxy,
Context,
createProxyWithInterceptors,
inject,
intercept,
Interceptor,
} from '../..';

describe('Interception proxy', () => {
let ctx: Context;

beforeEach(givenContextAndEvents);

it('invokes async interceptors on an async method', async () => {
// Apply `log` to all methods on the class
@intercept(log)
class MyController {
// Apply multiple interceptors. The order of `log` will be preserved as it
// explicitly listed at method level
@intercept(convertName, log)
async greet(name: string) {
return `Hello, ${name}`;
}
}
const proxy = createProxyWithInterceptors(new MyController(), ctx);
const msg = await proxy.greet('John');
expect(msg).to.equal('Hello, JOHN');
expect(events).to.eql([
'convertName: before-greet',
'log: before-greet',
'log: after-greet',
'convertName: after-greet',
]);
});

it('creates a proxy that converts sync method to be async', async () => {
// Apply `log` to all methods on the class
@intercept(log)
class MyController {
// Apply multiple interceptors. The order of `log` will be preserved as it
// explicitly listed at method level
@intercept(convertName, log)
greet(name: string) {
return `Hello, ${name}`;
}
}
const proxy = createProxyWithInterceptors(new MyController(), ctx);
const msg = await proxy.greet('John');
expect(msg).to.equal('Hello, JOHN');
expect(events).to.eql([
'convertName: before-greet',
'log: before-greet',
'log: after-greet',
'convertName: after-greet',
]);
});

it('invokes interceptors on a static method', async () => {
// Apply `log` to all methods on the class
@intercept(log)
class MyController {
// The class level `log` will be applied
static greetStatic(name: string) {
return `Hello, ${name}`;
}
}
ctx.bind('name').to('John');
const proxy = createProxyWithInterceptors(MyController, ctx);
const msg = await proxy.greetStatic('John');
expect(msg).to.equal('Hello, John');
expect(events).to.eql([
'log: before-greetStatic',
'log: after-greetStatic',
]);
});

it('supports asProxyWithInterceptors resolution option', async () => {
// Apply `log` to all methods on the class
@intercept(log)
class MyController {
// Apply multiple interceptors. The order of `log` will be preserved as it
// explicitly listed at method level
@intercept(convertName, log)
async greet(name: string) {
return `Hello, ${name}`;
}
}
ctx.bind('my-controller').toClass(MyController);
const proxy = await ctx.get<MyController>('my-controller', {
asProxyWithInterceptors: true,
});
const msg = await proxy!.greet('John');
expect(msg).to.equal('Hello, JOHN');
expect(events).to.eql([
'convertName: before-greet',
'log: before-greet',
'log: after-greet',
'convertName: after-greet',
]);
});

it('supports asProxyWithInterceptors resolution option for @inject', async () => {
// Apply `log` to all methods on the class
@intercept(log)
class MyController {
// Apply multiple interceptors. The order of `log` will be preserved as it
// explicitly listed at method level
@intercept(convertName, log)
async greet(name: string) {
return `Hello, ${name}`;
}
}

class DummyController {
constructor(
@inject('my-controller', {asProxyWithInterceptors: true})
public readonly myController: AsyncProxy<MyController>,
) {}
}
ctx.bind('my-controller').toClass(MyController);
ctx.bind('dummy-controller').toClass(DummyController);
const dummyController = await ctx.get<DummyController>('dummy-controller');
const msg = await dummyController.myController.greet('John');
expect(msg).to.equal('Hello, JOHN');
expect(events).to.eql([
'convertName: before-greet',
'log: before-greet',
'log: after-greet',
'convertName: after-greet',
]);
});

let events: string[];

const log: Interceptor = async (invocationCtx, next) => {
events.push('log: before-' + invocationCtx.methodName);
const result = await next();
events.push('log: after-' + invocationCtx.methodName);
return result;
};

// An interceptor to convert the 1st 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;
};

function givenContextAndEvents() {
ctx = new Context();
events = [];
}
});
131 changes: 3 additions & 128 deletions packages/context/src/__tests__/acceptance/interceptor.acceptance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@

import {expect} from '@loopback/testlab';
import {
asInterceptor,
AsyncProxy,
asGlobalInterceptor,
Context,
createProxyWithInterceptors,
inject,
intercept,
Interceptor,
Expand Down Expand Up @@ -95,7 +93,7 @@ describe('Interceptor', () => {
'John',
]);
expect(msg).to.equal('Hello, John');
expect(
await expect(
invokeMethodWithInterceptors(ctx, controller, 'greet', ['Smith']),
).to.be.rejectedWith(/Name 'Smith' is not on the list/);
});
Expand Down Expand Up @@ -339,129 +337,6 @@ describe('Interceptor', () => {
});
});

context('proxy with interceptors', () => {
it('invokes async interceptors on an async method', async () => {
// Apply `log` to all methods on the class
@intercept(log)
class MyController {
// Apply multiple interceptors. The order of `log` will be preserved as it
// explicitly listed at method level
@intercept(convertName, log)
async greet(name: string) {
return `Hello, ${name}`;
}
}
const proxy = createProxyWithInterceptors(new MyController(), ctx);
const msg = await proxy.greet('John');
expect(msg).to.equal('Hello, JOHN');
expect(events).to.eql([
'convertName: before-greet',
'log: before-greet',
'log: after-greet',
'convertName: after-greet',
]);
});

it('creates a proxy that converts sync method to be async', async () => {
// Apply `log` to all methods on the class
@intercept(log)
class MyController {
// Apply multiple interceptors. The order of `log` will be preserved as it
// explicitly listed at method level
@intercept(convertName, log)
greet(name: string) {
return `Hello, ${name}`;
}
}
const proxy = createProxyWithInterceptors(new MyController(), ctx);
const msg = await proxy.greet('John');
expect(msg).to.equal('Hello, JOHN');
expect(events).to.eql([
'convertName: before-greet',
'log: before-greet',
'log: after-greet',
'convertName: after-greet',
]);
});

it('invokes interceptors on a static method', async () => {
// Apply `log` to all methods on the class
@intercept(log)
class MyController {
// The class level `log` will be applied
static greetStatic(name: string) {
return `Hello, ${name}`;
}
}
ctx.bind('name').to('John');
const proxy = createProxyWithInterceptors(MyController, ctx);
const msg = await proxy.greetStatic('John');
expect(msg).to.equal('Hello, John');
expect(events).to.eql([
'log: before-greetStatic',
'log: after-greetStatic',
]);
});

it('supports asProxyWithInterceptors resolution option', async () => {
// Apply `log` to all methods on the class
@intercept(log)
class MyController {
// Apply multiple interceptors. The order of `log` will be preserved as it
// explicitly listed at method level
@intercept(convertName, log)
async greet(name: string) {
return `Hello, ${name}`;
}
}
ctx.bind('my-controller').toClass(MyController);
const proxy = await ctx.get<MyController>('my-controller', {
asProxyWithInterceptors: true,
});
const msg = await proxy!.greet('John');
expect(msg).to.equal('Hello, JOHN');
expect(events).to.eql([
'convertName: before-greet',
'log: before-greet',
'log: after-greet',
'convertName: after-greet',
]);
});

it('supports asProxyWithInterceptors resolution option for @inject', async () => {
// Apply `log` to all methods on the class
@intercept(log)
class MyController {
// Apply multiple interceptors. The order of `log` will be preserved as it
// explicitly listed at method level
@intercept(convertName, log)
async greet(name: string) {
return `Hello, ${name}`;
}
}

class DummyController {
constructor(
@inject('my-controller', {asProxyWithInterceptors: true})
public readonly myController: AsyncProxy<MyController>,
) {}
}
ctx.bind('my-controller').toClass(MyController);
ctx.bind('dummy-controller').toClass(DummyController);
const dummyController = await ctx.get<DummyController>(
'dummy-controller',
);
const msg = await dummyController.myController.greet('John');
expect(msg).to.equal('Hello, JOHN');
expect(events).to.eql([
'convertName: before-greet',
'log: before-greet',
'log: after-greet',
'convertName: after-greet',
]);
});
});

context('global interceptors', () => {
beforeEach(givenGlobalInterceptor);

Expand Down Expand Up @@ -576,7 +451,7 @@ describe('Interceptor', () => {
ctx
.bind('globalLog')
.to(globalLog)
.apply(asInterceptor);
.apply(asGlobalInterceptor);
}
});

Expand Down
Loading

0 comments on commit a8517fa

Please sign in to comment.