Nestjs can't create dynamic module with config import - dependency-injection

I am trying to create a DynamicModule in Nestjs, but it seems I can't properly use useFactory to inject ConfigModule in the process.
If I use a hardcoded boolean instead of config.get('cache').enabled everything works as expected, but I receive the following error if I try to use config:
TypeError: Cannot read properties of undefined (reading 'get')
Here's the code I arranged so far:
app.module.ts
#Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
validate,
}),
CoreModule.registerAsync({
useFactory: (config: ConfigService) => ({
cacheEnabled: config.get('cache').enabled,
}),
imports: [ConfigModule],
injects: [ConfigService],
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
core.module.ts
#Module({})
export class CoreModule {
static registerAsync = (options: {
useFactory: (...args: any[]) => { cacheEnabled: boolean };
imports: any[];
injects: any[];
}): DynamicModule => {
const imports = [];
const providers = [];
if (options.useFactory().cacheEnabled) imports.push(HttpCacheModule);
return {
module: CoreModule,
imports,
providers,
};
};
}

Related

Angular 12. Inject service via forRoot into an external library, loaded from a module which has been lazy loaded by Compiler

I've created a library with a directive that injects a service. This library is loaded with a forRoot method in each lazy loaded component where is going to be used.
*** library.module ***
export const SERVICE_INYECTION_TOKEN: InjectionToken<any> = new InjectionToken('service')
export interface IDirectiveModuleConfig {
serviceAdapterConfiguration?: {provider: Provider, moduleName: string};
}
#NgModule({
imports: [
CommonModule
],
declarations: [DirectiveDirective],
exports: [DirectiveDirective]
})
export class LibraryModule {
public static forRoot(config: IDirectiveModuleConfig = {}): ModuleWithProviders<LibraryModule> {
console.log("Library loaded in module " + config.serviceAdapterConfiguration.moduleName)
return {
ngModule: LibraryModule,
providers: [
config.serviceAdapterConfiguration.provider
]
};
}
}
*** directive.directive ***
#Directive({
selector: '[directive]',
})
export class DirectiveDirective implements AfterViewInit {
#Input() methodName: string;
constructor(
private element: ElementRef,
private renderer: Renderer2,
#Inject(SERVICE_INYECTION_TOKEN) private service: any
) {}
ngAfterViewInit(): void {
this.element.nativeElement.innerText += this.service[this.methodName]()
this.renderer.setValue(this.element.nativeElement, this.service[this.methodName]())
}
}
In my main project, I have two lazy-loadeds modules, and each one have a component. One of this modules and its component are lazylodaded by the RouterModules. It works OK
*** app-routing.module ***
const routes: Routes = [
{
path: 'a',
loadChildren: () =>
import('./modules/module-a/module-a.module').then((m) => m.ModuleAModule),
},
{
path: 'b',
loadChildren: () =>
import('./modules/module-b/module-b.module').then((m) => m.ModuleBModule),
},
];
#NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
The other one is created by compileModuleAndAllComponentsAsync() and viewContainerRef.createComponent() in the parent component. It works ok without the service inection, but when I inject the service I get a NullInjectorError.
*** app.component ***
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
#ViewChild("viewContainerRef", { read: ViewContainerRef }) viewContainerRef: ViewContainerRef
component = null;
title = 'component-overview';
constructor(private compiler: Compiler, private injector: Injector) {}
async createModuleAndComponetC() {
const componentInjector: Injector = Injector.create({providers:[{provide:'service', useExisting: ServiceCService}]})
this.viewContainerRef.clear()
const module = (await import('./modules/module-c/module-c.module'))
.ModuleCModule;
this.compiler.compileModuleAndAllComponentsAsync(module).then((factory) => {
factory.ngModuleFactory.create(this.injector);
const componentFactory = factory.componentFactories[0]
const component: ComponentRef<any> = this.viewContainerRef.createComponent(componentFactory);
});
}
}
MODULE A (lazy loaded by routerModule working OK) with its component and service
const serviceConfig: IDirectiveModuleConfig = {
serviceAdapterConfiguration: {
provider: { provide: SERVICE_INYECTION_TOKEN, useClass: ServiceAService },
moduleName: 'A',
}
};
#NgModule({
imports: [
LibraryModule.forRoot(serviceConfig),
CommonModule,
ModuleARoutingModuleModule,
],
declarations: [ComponentAComponent],
exports: [ComponentAComponent],
})
export class ModuleAModule {
constructor(){
console.log("moduleA loaded")
}
}
#Component({
selector: 'app-component-a',
templateUrl: './component-a.component.html',
styleUrls: ['./component-a.component.css'],
})
export class ComponentAComponent implements OnInit {
constructor() {}
ngOnInit() {}
}
#Injectable({
providedIn: 'root'
})
export class ServiceAService {
constructor() { }
serviceA(){
return(" service A!")
}
}
MODULE C (loaded manually with compileModuleAndAllComponentsAsync() and viewContainerRef.createComponent()
export const serviceConfig: IDirectiveModuleConfig = {
serviceAdapterConfiguration: {
provider: { provide: SERVICE_INYECTION_TOKEN, useClass: ServiceCService },
moduleName: 'C',
},
};
#NgModule({
imports: [CommonModule, LibraryModule.forRoot(serviceConfig)],
declarations: [ComponentCComponent],
})
export class ModuleCModule {
constructor() {
console.log('moduleC loaded');
}
static
}
#Component({
selector: 'app-component-c',
templateUrl: './component-c.component.html',
styleUrls: ['./component-c.component.css'],
providers: [ServiceCService],
})
export class ComponentCComponent implements OnInit {
constructor() {
console.log('component C constructor');
}
ngOnInit() {
console.log('component C OnInit');
}
}
#Injectable({
providedIn: 'root',
})
export class ServiceCService {
constructor() {}
serviceC() {
return ' service C!';
}
}
In this example Modules A and B are used with router outlet, and module C is loaded with Compiler and the component is used in a *ngCompilerOutlet
I think that the problem is in the way I load my ComponentC... but I'm a little bit lost...
In adition... i've founded that the module C create a new instance each time I load this, and is not working like singleton...
stackblitz with the test project
Finally, I got success!
I saw that I could pass an injector to the viewContainerRef. CreateComponent () method. I tried with the same injector I had used to create the module in the noModuleFactory. Create () method, but it was still wrong.
Finally y realized that NgModule class exports an injector, I suposed this injector provide al the providers in this module and it works ok!!
Now my createModuleAndComponetC() is:
async createModuleAndComponetC() {
this.viewContainerRef.clear();
const module = (await import('./modules/module-c/module-c.module'))
.ModuleCModule;
this.compiler.compileModuleAndAllComponentsAsync(module).then((factory) => {
const module = factory.ngModuleFactory.create(this.injector);
const componentFactory = factory.componentFactories[0];
const component: ComponentRef<any> =
this.viewContainerRef.createComponent(
componentFactory,
0,
module.injector
);
});
}
here is the corrected stackbliz

NestJS Standalone app can't inject Sequelize instance using connection token

I'm still quite new to NestJS. I'm trying to implement a standalone app that connect to both external/remote source DB and the app DB.
Now I got stuck at Nest can't resolve dependencies of the SourceDbQueryService (ModuleRef, ?). Please make sure that the argument {{token}} at index [1] is available in the EtlModule context.
The {{token}} here is supposedly a string returned from getConnectionToken(connectionName), ex.: sourceDbConnection when connectionName = sourceDb
Here are my modules setup example:
/src/db/source-db-module.ts
#Module({
imports: [
ConfigModule,
LoggingModule,
SequelizeModule.forRootAsync({
imports: [ConfigModule],
inject: [SourceDbConfig],
useFactory: (config: SourceDbConfig) => {
return {
...config,
name: SourceDbConfig.DefaultConnectionName,
autoLoadModels: false,
}
},
}),
],
exports: [SequelizeModule],
})
export class SourceDbModule {}
/src/jobs/etl-module.ts
#Module({
imports: [
ConfigModule,
LocalDbModule,
/** Contains Local repositories with decorated Models, using connection from LocalDbModule */
RepositoryModule,
SourceDbModule,
SequelizeModule.forFeature([], SourceDbConfig.DefaultConnectionName),
],
providers: [
{
provide: SourceDbQueryService,
inject: [ModuleRef, getConnectionToken(SourceDbConfig.DefaultConnectionName)],
useFactory(moduleRef: ModuleRef, sequelize: Sequelize) {
return new SourceDbQueryService(moduleRef, sequelize)
},
},
],
exports: [SourceDbQueryService],
})
export class EtlModule {}
/src/jobs/test-query-source-db.ts
async function bootstrap(): Promise<void> {
try {
const appContext = await NestFactory.createApplicationContext(EtlModule)
appContext.init()
const sourceDb = appContext.get(SourceDbQueryService)
const totalRecordsCount = await sourceDb.count({
// ...filters,
})
console.log(
`retrieved source DB results: (total items: ${totalItemsCount})`
)
appContext.close()
} catch (err) {
console.error(err)
process.exit(-1)
}
}
bootstrap()
Please help, what am I missing here?
Thanks!
Update: Workaround
For now I'm using a workaround by providing Sequelize instance directly from my own factory like this:
/src/db/source-db-module.ts
#Module({
imports: [
ConfigModule,
LoggingModule,
],
providers: [
// WORKAROUND: For SequelizeModule.forRootAsync() injection by connection token not working
{
provide: getConnectionToken(SourceDbConfig.DefaultConnectionName),
inject: [SourceDbConfig],
useFactory(config: SourceDbConfig) {
const { host, port, username, password, database, dialect } = config
return new Sequelize({
host,
port,
username,
password,
database,
dialect,
})
},
},
{
provide: SourceDbQueryService,
inject: [ModuleRef, SourceDbConfig, getConnectionToken(SourceDbConfig.DefaultConnectionName)],
useFactory(moduleRef: ModuleRef, config: SourceDbConfig, sequelize: Sequelize) {
const { schema, viewName } = config
return new SourceDbQueryService(moduleRef, sequelize, { schema, viewName })
},
},
],
exports: [
getConnectionToken(SourceDbConfig.DefaultConnectionName),
SourceDbQueryService,
],
})
export class SourceDbModule {}
/src/jobs/etl-module.ts
#Module({
imports: [
ConfigModule,
LocalDbModule,
/** Contains Local repositories with decorated Models, using connection from LocalDbModule */
RepositoryModule,
SourceDbModule,
],
})
export class EtlModule {}

NestJS E2E tests with Jest. Injected service returns undefined (only tests)

I have a problem with the end-to-end testing of my users module. I want to validate if there is a "companyCode" when a user makes a GET request in /users and sends this code in the query params. This validator searches the database if this company code exists, if it does not exist it returns an error. The problem is that in the test this validation doesn't happen, because "companiesService" returns undefined (only in the test), what's missing?
Possible Solution: something related to useContainer(class-validator).
Thanks.
users.e2e-spec.ts
describe('UsersController (e2e)', () => {
let app: INestApplication;
let repository: Repository<User>;
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [UsersModule, AuthModule, TypeOrmModule.forRoot(ormConfig)],
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard,
},
],
}).compile();
app = module.createNestApplication();
app.useGlobalPipes(new ValidationPipe());
useContainer(app.select(UsersModule), { fallbackOnErrors: true });
repository = module.get('UserRepository');
await app.init();
});
afterAll(async () => {
await app.close();
});
describe('/users (GET)', () => {
it('should return users if requesting user sent "companyCode" in the request body', async (done) => {
return request(app.getHttpServer())
.get('/users')
.auth('admin', 'admin')
.query({ companyCode: '2322661870558778503' }) // should return 200 because companyCode exists but is returning 400
.expect(200)
.then((res) => {
expect(res.body.users).toHaveLength(1);
done();
})
.catch((err) => done(err));
});
});
});
users.module.ts
#Module({
controllers: [UsersController],
providers: [UsersService, UserExistsRule],
imports: [
TypeOrmModule.forFeature([
User,
Person,
Type,
Profile,
UserProfile,
Company,
]),
CompaniesModule,
],
exports: [UsersService],
})
export class UsersModule {}
read-users.dto.ts
export class ReadUsersDto {
#IsOptional()
#IsNotEmpty()
#IsString()
#MinLength(1)
#MaxLength(255)
public name?: string;
#IsOptional()
#IsNotEmpty()
#IsNumberString()
#Type(() => String)
#Validate(CompanyExistsRule)
public companyCode?: string;
}
companies.module.ts
#Module({
providers: [CompaniesService, CompanyExistsRule],
imports: [TypeOrmModule.forFeature([Company, Person])],
exports: [CompaniesService],
})
export class CompaniesModule {}
companies.decorator.ts
#ValidatorConstraint({ name: 'CompanyExists', async: true })
#Injectable()
export class CompanyExistsRule implements ValidatorConstraintInterface {
constructor(private companiesService: CompaniesService) {}
async validate(code: string) {
try {
console.log('companiesService', this.companiesService); // returns undefined on test
await this.companiesService.findOneByCode(code);
} catch (e) {
return false;
}
return true;
}
defaultMessage() {
return `companyCode doesn't exist`;
}
}
I found that I imported useContainer from typeorm instead of the class-validator hahahahha.
// incorrectly imported
import { useContainer } from 'typeorm';
// correctly imported
import { useContainer } from 'class-validator';

Nestjs - Typeorm custom connection name

I have a Nestjs db Module and it works perfectly
#Module({
imports: [
TypeOrmModule.forRootAsync({
useFactory: () => {
return {
name: 'default', // <=== here
type: "mysql",
...
};
},
}),
TypeOrmModule.forFeature(entities, 'default'), // <=== here
],
exports: [TypeOrmModule],
})
export class DBModule {}
if I change the connection name to anything else rather then 'default' say 'test' I get an error
#Module({
imports: [
TypeOrmModule.forRootAsync({
useFactory: () => {
return {
name: 'test', // <=== here
type: "mysql",
...
};
},
}),
TypeOrmModule.forFeature(entities, 'test'), // <=== here
],
exports: [TypeOrmModule],
})
export class DBModule {}
[Nest] 10746 - 05/15/2021, 5:55:34 PM [ExceptionHandler] Nest can't resolve dependencies of the test_UserEntityRepository (?). Please make sure that the argument testConnection at index [0] is available in the TypeOrmModule context.
Potential solutions:
- If testConnection is a provider, is it part of the current TypeOrmModule?
- If testConnection is exported from a separate #Module, is that module imported within TypeOrmModule?
#Module({
imports: [ /* the Module containing testConnection */ ]
})
The error seams to only show up if I use TypeOrmModule.forRootAsync
For TypeOrmModule.forRoot if works!
Is there any different way to indicate the connection name? I need to add another connection and can't do it because of this error. Really would like to use 'forRootAsync'
Pass the connection name as follows.
#Module({
imports: [
TypeOrmModule.forRootAsync({
name: 'test', // <=== here
useFactory: () => {
return {
type: "mysql",
...
};
},
}),
TypeOrmModule.forFeature(entities, 'test'), // <=== here
],
exports: [TypeOrmModule],
})
export class DBModule {}

getting error `Property 'initializeData' does not exist on type of AppConfig` on `useFactory`

I am fetching data from service on APP_INITIALIZER, but getting error as
Property 'initializeData' does not exist on type of AppConfig
don't know what is the exact issue here. any one help me?
here is my module file:
import { AppConfig } from "./shared-components/auth/AdalService";
import { AppComponent } from './app.component';
import { RoutesModule } from './routes/routes.module';
import { SignInComponent } from './shared-components/user/sign-in/sign-in.component';
export function initializeApp() {
return () => AppConfig.initializeData(); //getting error here
}
#NgModule({
declarations: [
AppComponent,
SignInComponent
],
imports: [
BrowserModule,
AngularFontAwesomeModule,
MsAdalAngular6Module,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: (createTranslateLoader),
deps: [HttpClient]
},
isolate: true
}),
SharedModule,
HttpClientModule,
iboCalendarModule,
RoutesModule,
// HttpClientInMemoryWebApiModule.forRoot(EventData),
StoreModule.forRoot({}),
EffectsModule.forRoot([]),
StoreDevtoolsModule.instrument({
name:'IBO App',
maxAge:25
})
],
providers: [
{
provide: APP_INITIALIZER,
useFactory: initializeApp,
multi: true,
deps: [AppConfig, SignInComponent ]
},
MsAdalAngular6Service,
{
provide: 'adalConfig',
useFactory: getAdalConfig,
deps: []
},
{
provide: HTTP_INTERCEPTORS,
useClass: InsertAuthTokenInterceptor,
multi: true
}
],
bootstrap: [AppComponent]
})
export class AppModule { }
Here is my service.ts:
import { Injectable, OnInit } from '#angular/core';
import { ShareOption } from "./../user/sign-in/sign-in.component";
import { Store, select } from '#ngrx/store';
import { StateShared } from "./../models/models";
#Injectable({
providedIn: 'root'
})
export class AppConfig {
constructor(){}
initializeData() {
return new Promise((resolve, reject) => resolve(true));
}
}
I bought the service in parameter, got issue fixed. my updated chunk is:
export function initializeApp(service:AppConfig) { //getting service object in param
return () => service.initializeData();
}

Resources