I am trying to do something fancy: a DynamicModule that chooses a Controller/Providers depending on a provider. The provider reads a config in a DB at onModuleInit, so there is no way to hard-code its values.
It is similar to this issue: https://github.com/nestjs/nest/issues/601 which was never answered...
I have tried a few approaches and the closest I have got is this:
export class FancyModule implements DynamicModule {
public module: Type<FancyModule>;
public controllers: Type<unknown>[];
public providers: Provider<unknown>[];
constructor (
#Inject(StartupConfigService) private readonly startup: StartupConfigService,
) {
this.module = FancyModule;
if (startup.featureFlagEnabled) {
this.controllers = [ FancyFeatureController ];
this.providers = [ FancyFeatureService ];
} else {
this.controllers = [ ErrorController ];
this.providers = [];
}
}
}
Second approach:
app.module.ts
#Module({
imports: [
StartupConfigModule.register(startupConfigOption),
FancyModule.registerAsync({
imports: [StartupConfigModule],
useFactory: function (startupConfigService: StartupConfigService) {
return startupConfigService.config
},
inject: [StartupConfigService],
}),
],
})
fancy.module.ts
export class FancyModule {
static registerAsync (config: IAppConfig): DynamicModule {
if (config.featureFlagEnabled) {
return {
module: FancyModule,
controllers: [ FancyFeatureController ],
providers: [ FancyFeatureService ],
};
} else {
return {
module: FancyModule,
controllers: [ ],
providers: [ ],
};
}
}
}
This injects as expected but the controllers and providers are never spun up. Is the meta-data evaluated before the constructor? Is that a bug?
It turns out what I'm trying to do is, in fact, impossible. The evaluation of the FancyModule.registerAsync() method happens before the onModuleInit od the StartupconfigService (which sets the feature flag).
A DynamicModule cannot use the contents of an instantiated provider to register() itself.
Other reading:
Fundamentals - Dynamic Modules
Advanced - Dynamic Modules
Related
I have created a custom provider in one of my modules.
I need to export that custom provider to make it available in other modules and I'm having trouble doing so. I do not know what to put in the exports array.
Below is my custom provider.
#Module({
imports: [TypeOrmModule.forFeature([TypeOrmUser])],
providers: [
UserMapper,
UserService,
{
provide: getRepositoryToken(TypeOrmUser),
inject: [getDataSourceToken()],
useFactory(dataSource: DataSource) {
return dataSource.getRepository(TypeOrmUser).extend(userRepositoryImpl);
},
},
],
exports: [UserService, 'what to put here to export custom user repository?'],
})
export class UsersModule {}
Thank you for your help.
To export a custom provider, all that is needed to be added to the exports array is the provider's injection token.
In this case, that's getRepositoryToken(TypeOrmUser)
You can export it either with token name, or with the whole provider object. In your case it would be something like this:
exports: [UserService, getRepositoryToken(TypeOrmUser)],
or:
const customProvider = {
provide: getRepositoryToken(TypeOrmUser),
inject: [getDataSourceToken()],
useFactory(dataSource: DataSource) {
return dataSource.getRepository(TypeOrmUser).extend(userRepositoryImpl);
},
};
#Module({
imports: [TypeOrmModule.forFeature([TypeOrmUser])],
providers: [
UserMapper,
UserService,
customProvider,
],
exports: [UserService, customProvider],
})
I've got two UseCases named, FirstUseCase and SecondUseCase.
FirstUseCases
export class FirstUsecases {
constructor(
private readonly something: Something
) {}
async execute(): Promise<any> {
console.log("This is FirstUsecases class.");
}
}
SecondUseCases
export class SecondUsecases {
constructor(
// How to inject FirstUsecases here?
private readonly firstUseCases: FirstUsecases ) {}
async execute(): Promise<any> {
this.firstUseCases.execute();
}
}
I wanna inject FirstUsecases into the SecondUsecases class.
Note: the FirstUsecases and SecondUsecases are in the same module named MyProxyModule.
MyProxyModule
static register(): DynamicModule {
return {
module: MyProxyModule,
providers: [
{
inject: [FirstRepository],
provide: "FIRST_USECASE",
useFactory: (firstRepository: FirstRepository) => new FirstUsecases(firstRepository),
},
{
inject: [SecondRepository],
provide: "SECOND_USECASE",
useFactory: (secondRepository: SecondRepository) => new SecondUsecases(secondRepository),
},,
]
}
}
How can I inject FirstUsecases into SecondUsecases?
If you're using new like that, you're in charge of passing the dependencies the class needs. To get an instance of the FirstUsecases you can add its injection token to the inject array, like how you added SecondRepository and then pass it on to the constructor.
static register(): DynamicModule {
return {
module: MyProxyModule,
providers: [
{
inject: [FirstRepository],
provide: "FIRST_USECASE",
useFactory: (firstRepository: FirstRepository) => new FirstUsecases(firstRepository),
},
{
inject: [SecondRepository "FIRST_USECASE"],
provide: "SECOND_USECASE",
useFactory: (secondRepository: SecondRepository, firstUsecase: FirstUsercase) => new SecondUsecases(secondRepository, firstUsecase),
},,
]
}
}
I am writing end-to-end tests in NestJs and using the "overrideProvider" function to setup test versions of various services (e.g. services that require database connections). I noticed though that even when I do this, the original implementation that injects the real database is still instantiated.
Is there a way to tell Nest to not create transitive dependencies that are overridden?
For example, I have a test that starts like:
...
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [ServiceModule],
})
// Works if I uncomment these lines:
// .overrideProvider('Database')
// .useValue(new TestDatabase())
.overrideProvider('ServiceUsingDatabase')
.useValue(new TestService())
.compile();
...
Where the module setup is like:
import { Inject, Injectable, Module } from '#nestjs/common';
interface Database {}
#Injectable()
class ProductionDatabase implements Database {
constructor() {
throw('Cannot create a production database.');
}
}
#Injectable()
export class TestDatabase implements Database {
constructor() {
console.log('Creating the test database.');
}
}
#Module({
providers: [
{
provide: 'Database',
useClass: ProductionDatabase
}
],
exports: ['Database']
})
class DatabaseModule {}
interface Service {}
#Injectable()
class ProductionService implements Service {
constructor(#Inject('Database') private readonly database: Database) {}
}
#Injectable()
export class TestService implements Service {
// Test implementation of the service does not Inject anything.
}
#Module({
imports: [DatabaseModule],
providers: [
{
provide: 'ServiceUsingDatabase',
useClass: ProductionService
}
],
})
export class ServiceModule {}
But, the DI system is still seeming to try and instantiate ProductionDatabase. If I explicitly override the provider for the 'Database' it works, but I'd like to avoid having to explicitly list all transitive dependencies as such.
I ended up deciding to make a "Test" Module for every Module e.g.:
import { Inject, Injectable, Module } from '#nestjs/common';
interface Database {}
#Injectable()
class ProductionDatabase implements Database {
}
#Injectable()
export class TestDatabase implements Database {
}
#Module({
providers: [
{
provide: 'Database',
useClass: ProductionDatabase
}
],
exports: ['Database']
})
class DatabaseModule {}
#Module({
providers: [
{
provide: 'Database',
useClass: TestDatabase
}
],
exports: ['Database']
})
class TestDatabaseModule {}
interface Service {}
#Injectable()
class ProductionService implements Service {
constructor(#Inject('Database') private readonly database: Database) {}
}
#Injectable()
export class TestService implements Service {
}
#Module({
imports: [DatabaseModule],
providers: [
{
provide: 'ServiceUsingDatabase',
useClass: ProductionService
}
],
})
export class ServiceModule {}
#Module({
providers: [
{
provide: 'ServiceUsingDatabase',
useClass: TestService
}
],
})
export class TestServiceModule {}
etc... Though it turned out after some refactorings that the "Test" module wasn't needed as some Modules became pure business logic.
I created my Dynamic configModule to extract environment variables from a different path. It extracts from an yml file. Everything works properly if a add in some module. Here is my ConfigModule:
import { DynamicModule } from '#nestjs/common';
import { ConfigModule } from '#nestjs/config';
import { EnvConfigService } from './env.config.service';
export class EnvConfigModule {
/**
* Create an static function to call directly from the class without instantiation
* #param options: Our config module attributes or properties
* #returns DynamicModule
*/
static register(options): DynamicModule {
return {
module: ConfigModule,
providers: [
{
provide: 'CONFIG_OPTIONS',
useValue: options,
},
EnvConfigService,
],
exports: [EnvConfigService],
};
}
}
Now when I want to add that configuration in the new custom JwtModule, CustomJwtModule:
...
import { EnvConfigModule } from 'src/utils/environment/env.config.module';
import { EnvConfigService } from 'src/utils/environment/env.config.service';
#Module({
imports: [
JwtModule.registerAsync({
imports: [EnvConfigModule],
inject: [EnvConfigService],
useFactory: (configService: EnvConfigService) => {
const base64_pubKey = configService.get(ACCESS_PUBLIC_KEY_NAME);
return {
publicKey: Buffer.from(base64_pubKey, KEY_ENCODING),
signOptions: {
algorithm: ENCRYPTION_ALGORITHM,
},
};
},
}),
],
providers: [
{
provide: JWT_ACCESS_SERVICE,
useExisting: JwtService,
},
],
exports: [JWT_ACCESS_SERVICE],
})
export class JWTAccessModule {}
And here is my error:
Error: Nest can't resolve dependencies of the JWT_MODULE_OPTIONS (?). Please make sure that the argument EnvConfigService at index [0] is available in the JwtModule context.
I guess, I do get that error because it does not inject properly in JWT module my service.
My app.module.ts is implemented in that way
...
import { EnvConfigModule } from './utils/environment/env.config.module';
#Module({
imports: [
IamModule,
EnvConfigModule.register({
folder: 'config/environment/',
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Ok, it seems I was importing wrongly. It was missing some part of the configuration. Here is the working example of CustomJwtModule:
...
import { EnvConfigModule } from 'src/utils/environment/env.config.module';
import { EnvConfigService } from 'src/utils/environment/env.config.service';
#Module({
imports: [
JwtModule.registerAsync({
imports: [EnvConfigModule.register({
folder: 'config/environment/',
}),
],
inject: [EnvConfigService],
...
I would like to use a service in the export function. This gives me a HTTP_INTERCEPTOR error.
How could I use this service without causing the cyclic dependency error. The reason I would like to use a service here is to obtain dynamic values from a rest call.
Module:
export function MSALInstanceFactory(service: AzureService): IPublicClientApplication {
return new PublicClientApplication({
auth: service.getConfiguration(),
cache: {
cacheLocation: BrowserCacheLocation.LocalStorage,
storeAuthStateInCookie: isIE, // set to true for IE 11. Remove this line to use Angular Universal
}
});
}
#NgModule({
declarations: [
AzureComponent
],
exports: [
AzureComponent
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: MsalInterceptor,
multi: true
},
{
provide: MSAL_INSTANCE,
useFactory: MSALInstanceFactory,
deps: [AzureService]
},
AzureService,
IdentityManagerApiRestService,
]
})
export class AzureModule { }
Service:
#Injectable()
export class AzureService {
constructor(private identityManagerApiRestService: IdentityManagerApiRestService) { }
getConfiguration(): BrowserAuthOptions {
let options: BrowserAuthOptions;
this.identityManagerApiRestService.getAuthenticationConfiguration().subscribe(response => {
options = {
clientId: response.authenticationConfiguration.clientId,
authority: response.authenticationConfiguration.authority,
redirectUri: response.authenticationConfiguration.redirectUri,
}
});
return options;
}
}