Skip to content

Commit a8f326c

Browse files
committed
feat(context): use invocation context for method dependency injection
Use case: interceptors may add or change bindings to influence how the method parameter injection is resolved.
1 parent 44b4875 commit a8f326c

File tree

4 files changed

+150
-28
lines changed

4 files changed

+150
-28
lines changed

packages/context/src/__tests__/acceptance/interceptor.acceptance.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,94 @@ describe('Interceptor', () => {
367367
});
368368
});
369369

370+
context('invocation options for invokeMethodWithInterceptors', () => {
371+
it('can skip parameter injection', async () => {
372+
const controller = givenController();
373+
374+
ctx.bind('name').to('Jane');
375+
const msg = await invokeMethodWithInterceptors(
376+
ctx,
377+
controller,
378+
'greetWithDI',
379+
// 'John' is passed in as an arg
380+
['John'],
381+
{
382+
skipParameterInjection: true,
383+
},
384+
);
385+
expect(msg).to.equal('Hello, John');
386+
expect(events).to.eql([
387+
'log: before-greetWithDI',
388+
'log: after-greetWithDI',
389+
]);
390+
});
391+
392+
it('can support parameter injection', async () => {
393+
const controller = givenController();
394+
ctx.bind('name').to('Jane');
395+
const msg = await invokeMethodWithInterceptors(
396+
ctx,
397+
controller,
398+
'greetWithDI',
399+
// No name is passed in here as it will be provided by the injection
400+
[],
401+
{
402+
skipParameterInjection: false,
403+
},
404+
);
405+
// `Jane` is bound to `name` in the current context
406+
expect(msg).to.equal('Hello, Jane');
407+
expect(events).to.eql([
408+
'log: before-greetWithDI',
409+
'log: after-greetWithDI',
410+
]);
411+
});
412+
413+
it('does not allow skipInterceptors', async () => {
414+
const controller = givenController();
415+
expect(() => {
416+
invokeMethodWithInterceptors(ctx, controller, 'greetWithDI', ['John'], {
417+
skipInterceptors: true,
418+
});
419+
}).to.throw(/skipInterceptors is not allowed/);
420+
});
421+
422+
function givenController() {
423+
class MyController {
424+
// Apply `log` to an async instance method with parameter injection
425+
@intercept(log)
426+
async greetWithDI(@inject('name') name: string) {
427+
return `Hello, ${name}`;
428+
}
429+
}
430+
return new MyController();
431+
}
432+
});
433+
434+
context('controller method with both interception and injection', () => {
435+
it('allows interceptor to influence parameter injection', async () => {
436+
const result = await invokeMethodWithInterceptors(
437+
ctx,
438+
new MyController(),
439+
'interceptedHello',
440+
[],
441+
{skipParameterInjection: false},
442+
);
443+
// `Mary` is bound to `name` by the interceptor
444+
expect(result).to.eql('Hello, Mary');
445+
});
446+
447+
class MyController {
448+
@intercept(async (invocationCtx, next) => {
449+
invocationCtx.bind('name').to('Mary');
450+
return await next();
451+
})
452+
async interceptedHello(@inject('name') name: string) {
453+
return `Hello, ${name}`;
454+
}
455+
}
456+
});
457+
370458
context('class level interceptors', () => {
371459
it('invokes sync and async interceptors', async () => {
372460
// Apply `log` to all methods on the class

packages/context/src/__tests__/unit/invocation-context.unit.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44
// License text available at https://opensource.org/licenses/MIT
55

66
import {expect} from '@loopback/testlab';
7-
import {Context, InvocationContext} from '../..';
7+
import {Context, inject, InvocationContext} from '../..';
88

99
describe('InvocationContext', () => {
1010
let ctx: Context;
1111
let invocationCtxForGreet: InvocationContext;
12+
let invocationCtxForHello: InvocationContext;
1213
let invocationCtxForCheckName: InvocationContext;
1314
let invalidInvocationCtx: InvocationContext;
1415
let invalidInvocationCtxForStaticMethod: InvocationContext;
@@ -60,6 +61,14 @@ describe('InvocationContext', () => {
6061
expect(invocationCtxForCheckName.invokeTargetMethod()).to.eql(true);
6162
});
6263

64+
it('invokes target method with injection', async () => {
65+
expect(
66+
await invocationCtxForHello.invokeTargetMethod({
67+
skipParameterInjection: false,
68+
}),
69+
).to.eql('Hello, Jane');
70+
});
71+
6372
it('does not close when an interceptor is in processing', () => {
6473
const result = invocationCtxForGreet.invokeTargetMethod();
6574
expect(invocationCtxForGreet.isBound('abc'));
@@ -75,6 +84,10 @@ describe('InvocationContext', () => {
7584
async greet(name: string) {
7685
return `Hello, ${name}`;
7786
}
87+
88+
async hello(@inject('name') name: string) {
89+
return `Hello, ${name}`;
90+
}
7891
}
7992

8093
function givenContext() {
@@ -90,6 +103,14 @@ describe('InvocationContext', () => {
90103
['John'],
91104
);
92105

106+
invocationCtxForHello = new InvocationContext(
107+
ctx,
108+
new MyController(),
109+
'hello',
110+
[],
111+
);
112+
invocationCtxForHello.bind('name').to('Jane');
113+
93114
invocationCtxForCheckName = new InvocationContext(
94115
ctx,
95116
MyController,

packages/context/src/interceptor.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
MetadataMap,
1212
MethodDecoratorFactory,
1313
} from '@loopback/metadata';
14+
import * as assert from 'assert';
1415
import * as debugFactory from 'debug';
1516
import {Binding, BindingTemplate} from './binding';
1617
import {bind} from './binding-decorator';
@@ -26,6 +27,7 @@ import {
2627
import {
2728
InvocationArgs,
2829
InvocationContext,
30+
InvocationOptions,
2931
InvocationResult,
3032
} from './invocation';
3133
import {
@@ -284,13 +286,18 @@ export function intercept(...interceptorOrKeys: InterceptorOrKey[]) {
284286
* @param target - Target class (for static methods) or object (for instance methods)
285287
* @param methodName - Method name
286288
* @param args - An array of argument values
289+
* @param options - Options for the invocation
287290
*/
288291
export function invokeMethodWithInterceptors(
289292
context: Context,
290293
target: object,
291294
methodName: string,
292295
args: InvocationArgs,
296+
options: InvocationOptions = {},
293297
): ValueOrPromise<InvocationResult> {
298+
// Do not allow `skipInterceptors` as it's against the function name
299+
// `invokeMethodWithInterceptors`
300+
assert(!options.skipInterceptors, 'skipInterceptors is not allowed');
294301
const invocationCtx = new InterceptedInvocationContext(
295302
context,
296303
target,
@@ -302,7 +309,8 @@ export function invokeMethodWithInterceptors(
302309
return tryWithFinally(
303310
() => {
304311
const interceptors = invocationCtx.loadInterceptors();
305-
const targetMethodInvoker = () => invocationCtx.invokeTargetMethod();
312+
const targetMethodInvoker = () =>
313+
invocationCtx.invokeTargetMethod(options);
306314
interceptors.push(targetMethodInvoker);
307315
return invokeInterceptors(invocationCtx, interceptors);
308316
},

packages/context/src/invocation.ts

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,20 @@ export class InvocationContext extends Context {
9494
/**
9595
* Invoke the target method with the given context
9696
* @param context - Invocation context
97+
* @param options - Options for the invocation
9798
*/
98-
invokeTargetMethod() {
99+
invokeTargetMethod(
100+
options: InvocationOptions = {skipParameterInjection: true},
101+
) {
99102
const targetWithMethods = this.assertMethodExists();
103+
if (!options.skipParameterInjection) {
104+
return invokeTargetMethodWithInjection(
105+
this,
106+
targetWithMethods,
107+
this.methodName,
108+
this.args,
109+
);
110+
}
100111
return invokeTargetMethod(
101112
this,
102113
targetWithMethods,
@@ -137,44 +148,38 @@ export function invokeMethod(
137148
nonInjectedArgs: InvocationArgs = [],
138149
options: InvocationOptions = {},
139150
): ValueOrPromise<InvocationResult> {
140-
if (options.skipParameterInjection) {
141-
if (options.skipInterceptors) {
151+
if (options.skipInterceptors) {
152+
if (options.skipParameterInjection) {
142153
// Invoke the target method directly without injection or interception
143154
return invokeTargetMethod(ctx, target, method, nonInjectedArgs);
144155
} else {
145-
// Invoke the target method with interception but no injection
146-
return invokeMethodWithInterceptors(ctx, target, method, nonInjectedArgs);
156+
return invokeTargetMethodWithInjection(
157+
ctx,
158+
target,
159+
method,
160+
nonInjectedArgs,
161+
);
147162
}
148163
}
149-
// Parameter injection is required
150-
const invoker = options.skipInterceptors
151-
? invokeTargetMethod
152-
: invokeMethodWithInterceptors;
153-
return invokeMethodWithInvoker(invoker, ctx, target, method, nonInjectedArgs);
164+
// Invoke the target method with interception but no injection
165+
return invokeMethodWithInterceptors(
166+
ctx,
167+
target,
168+
method,
169+
nonInjectedArgs,
170+
options,
171+
);
154172
}
155173

156174
/**
157-
* An invoker for a method within the given context and arguments
158-
*/
159-
type MethodInvoker = (
160-
ctx: Context,
161-
target: Record<string, Function>,
162-
methodName: string,
163-
args: InvocationArgs,
164-
) => ValueOrPromise<InvocationResult>;
165-
166-
/**
167-
* Invoke a method with the given invoker. Method parameter dependency injection
168-
* is honored.
169-
* @param invoker - Method invoker
175+
* Invoke a method. Method parameter dependency injection is honored.
170176
* @param target - Target of the method, it will be the class for a static
171177
* method, and instance or class prototype for a prototype method
172178
* @param method - Name of the method
173179
* @param ctx - Context
174180
* @param nonInjectedArgs - Optional array of args for non-injected parameters
175181
*/
176-
function invokeMethodWithInvoker(
177-
invoker: MethodInvoker,
182+
function invokeTargetMethodWithInjection(
178183
ctx: Context,
179184
target: object,
180185
method: string,
@@ -205,7 +210,7 @@ function invokeMethodWithInvoker(
205210
if (debug.enabled) {
206211
debug('Injected arguments for %s:', methodName, args);
207212
}
208-
return invoker(ctx, targetWithMethods, method, args);
213+
return invokeTargetMethod(ctx, targetWithMethods, method, args);
209214
});
210215
}
211216

0 commit comments

Comments
 (0)