Is there a trick to getting useValue
dependency injection working properly for Nest.js interceptors? I have a dynamic module similar to this:
@Module({})
export class SomeFeatureModule {
static register({
perRequestParams,
...clientOptions
}: ModuleOptions): DynamicModule {
const provider = new SomeClientProvider(clientOptions);
return {
module: SomeFeatureModule,
providers: [
{
provide: SomeClientProvider,
useValue: provider,
},
{
provide: SomeInterceptor,
useValue: new SomeInterceptor(provider, perRequestParams),
},
],
exports: [SomeClientProvider, SomeInterceptor],
};
}
}
...where the SomeInterceptor
class is something like this:
@Injectable()
export class SomeInterceptor implements NestInterceptor {
constructor(
private readonly someClientProvider: SomeClientProvider,
private readonly perRequestParams: (
context: ExecutionContext,
) => EvaluationCriteria | Promise<EvaluationCriteria>,
) {}
async intercept(
execContext: ExecutionContext,
next: CallHandler<any>,
): Promise<Observable<any>> {
const params = await this.perRequestParams(execContext);
return this.someClientProvider.injectLocalStorageData(params, () => next.handle());
}
}
...but then when I try to use the interceptor on my app's controller:
@UseInterceptors(SomeInterceptor)
...I get the error:
Error: Nest can't resolve dependencies of the SomeInterceptor (SomeClientProvider, ?). Please make sure that the argument Function at index [1] is available in the AppModule context.
I am specifically importing SomeFeatureModule.register(...)
in my AppModule
:
@Module({})
export class AppModule {
static register(env: Environment): DynamicModule {
// ...
return {
module: AppModule,
imports: [
SomeFeatureModule.register({
...clientConfig,
async perRequestParams(ctx) {
// ...
},
}),
],
// ...
};
}
}
Why is the dependency injection system trying to resolve the constructor parameters for SomeInterceptor
even though I'm already manually providing one?
Note that if I remove @Injectable()
I don't get the same startup error, but the interceptor's constructor is called with no arguments, so that is also broken.
I found a workaround. Since it seems that the registered SomeInterceptor
is being ignored, instead I declared a new Symbol
that the user of the interceptor has to register a value for:
export const PER_REQUEST_PARAMS = Symbol('PER_REQUEST_PARAMS');
New SomeInterceptor
:
@Injectable()
export class SomeInterceptor implements NestInterceptor {
constructor(
private readonly clientProvider: SomeClientProvider,
@Inject(PER_REQUEST_PARAMS)
private readonly perRequestParams: (
context: ExecutionContext,
) => EvaluationCriteria | Promise<EvaluationCriteria>,
) {}
// ...
}
...and now AppModule
has to do this:
@Module({})
export class AppModule {
static register(env: Environment): DynamicModule {
// ...
return {
module: AppModule,
imports: [
SomeFeatureModule.register(clientConfig),
// ...
],
providers: [
// ...
{
provide: PER_REQUEST_PARAMS,
useValue: (ctx: ExecutionContext) => {
// ...
}
}
]
// ...
};
}
}
Strangely, providing PER_REQUEST_PARAMS
from within SomeFeatureModule
doesn't work; it seems that I have to do it in AppModule
or it doesn't resolve. Hopefully this is a bug because it's not very intuitive.
Update:
I found that if I do register a value for PER_REQUEST_PARAMS
within SomeFeatureModule
it does work, but I also have to make sure to export PER_REQUEST_PARAMS
, otherwise it isn't seen when instantiating an instance of SomeInterceptor
.