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: 1 addition & 1 deletion packages/context/src/__tests__/unit/interceptor.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ describe('globalInterceptors', () => {
});

it('generates binding key for the interceptor function', () => {
const binding = registerInterceptor(ctx, () => {});
const binding = registerInterceptor(ctx, () => undefined);
expect(binding.key).to.match(
new RegExp(`interceptors.${UUID_PATTERN.source}`, 'i'),
);
Expand Down
33 changes: 30 additions & 3 deletions packages/context/src/interceptor-chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,55 @@ import {InvocationResult} from './invocation';
import {transformValueOrPromise, ValueOrPromise} from './value-promise';
const debug = debugFactory('loopback:context:interceptor-chain');

/**
* Any type except `void`. We use this type to enforce that interceptor functions
* always return a value (including undefined or null).
*/
export type NonVoid = string | number | boolean | null | undefined | object;

/**
* The `next` function that can be used to invoke next generic interceptor in
* the chain
*/
export type Next = () => ValueOrPromise<InvocationResult>;
export type Next = () => ValueOrPromise<NonVoid>;

/**
* An interceptor function to be invoked in a chain for the given context.
* It serves as the base interface for various types of interceptors, such
* as method invocation interceptor or request/response processing interceptor.
*
* We choose `NonVoid` as the return type to avoid possible bugs that an
* interceptor forgets to return the value from `next()`. For example, the code
* below will fail to compile.
*
* ```ts
* const myInterceptor: Interceptor = async (ctx, next) {
* // preprocessing
* // ...
*
* // There is a subtle bug that the result from `next()` is not further
* // returned back to the upstream interceptors
* const result = await next();
*
* // postprocessing
* // ...
* // We must have `return ...` here
* // either return `result` or another value if the interceptor decides to
* // have its own response
* }
* ```
*
* @typeParam C - `Context` class or a subclass of `Context`
* @param context - Context object
* @param next - A function to proceed with downstream interceptors or the
* target operation
*
* @returns The invocation result as a value (sync) or promise (async)
* @returns The invocation result as a value (sync) or promise (async).
*/
export type GenericInterceptor<C extends Context = Context> = (
context: C,
next: Next,
) => ValueOrPromise<InvocationResult>;
) => ValueOrPromise<NonVoid>;

/**
* Interceptor function or a binding key that resolves a generic interceptor
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/__tests__/unit/application.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,9 @@ describe('Application', () => {
});
});

function logInterceptor(ctx: InvocationContext, next: Next) {}
function logInterceptor(ctx: InvocationContext, next: Next) {
return undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this redundant and a bit silly, because in JavaScript, functions return undefined when there is no return statement. I guess the extra verbosity is worth the stronger type safety your change provides.

}

@bind(asGlobalInterceptor())
class LogInterceptorProvider implements Provider<Interceptor> {
Expand Down