Instead of Singletons, I want to create dynamically class instances in NestJs.
I found two ways:
1) Directly create the class (ChripSensor is then not #Injectable)
import { ChirpSensor } from './chirp-sensor/chirp-sensor';
#Injectable()
export class SensorsService {
registeredSensors: any;
constructor(
#InjectModel('Sensor') private readonly sensorModel: Model<ISensor>,
private i2cService: I2cService) {
const sensors = this.i2cService.getSensors();
sensors.forEach((sensor) => {this.registeredSensors[sensor._id] = new ChirpSensor({name: sensor.name})});
}
I'm wondering if that is consistent with the DI way of nest.js
2) The second solution would be via a factory, but here I don't know how to pass the options.
export const chirpFactory = {
provide: 'CHIRP_SENSOR',
useFactory: (options) => {
console.log('USING FACTORY CHIRP, options', options)
if (process.env.SIMULATION === 'true') {
return new ChirpSensorMock(options);
}
else {
return new ChirpSensor(options);
}
}
};
Not quite sure how to continue here/ inject the factory properly as the examples create the object in the constructor without options?
Question:
What is the NestJs way to create those class instances?
Edit - for B12Toastr
Module - get the Mock or Original on Compile time
providers: [
{
provide: 'CHIRP_SENSOR',
useValue: process.env.SIMULATION === 'true'
? ChirpSensorMock
: ChirpSensor
},
],
Sensor Service
#Injectable()
export class SensorsService {
registeredSensors: any;
constructor(
#Inject('CHIRP_SENSOR') private ChirpSensorClass: any, // any works but ChirpSensorMock | ChirpSensor not
private i2cService: I2cService
) {
const sensors = this.i2cService.getSensors();
sensors.forEach((sensor) => {this.registeredSensors[sensor._id] = new ChirpSensorClass({name: sensor.name})});
}
You can pass options to your factory via DI via useValue or useClass
providers: [
{
provide: MyOptions,
useValue: options
},
{
provide: 'CHIRP_SENSOR',
useFactory: (options: MyOptions) => {
console.log('USING FACTORY CHIRP, options', options);
if (process.env.SIMULATION === 'true') {
return new ChirpSensorMock(options);
} else {
return new ChirpSensor(options);
}
},
},
],
Alternatively, you could also avoid using a factory altogether and make the decision which class to use at compile time via:
providers: [
{
provide: MyOptions,
useValue: options
},
{
provide: 'CHIRP_SENSOR',
useValue: process.env.SIMULATION === 'true'
? ChirpSensorMock
: ChirpSensor
},
],
or simply:
providers: [
{
provide: MyOptions,
useValue: options
},
{
process.env.SIMULATION === 'true' ? ChirpSensorMock : ChirpSensor
},
],
In case you are not using a factory as described above, you would then inject the options in your ChirpSensor (or the Mocked Sensor)` using typical constructor-based dependency injection:
#Injectable()
export class ChripSensor {
constructor(#inject(MyOptions) private options: MyOptions) {
}
// ...
}
Depending on whether your options are wrapped in a class or a simple object you would either use useValue or useClass. With useClass you have to write less code and do not have to use the #Inject decorator since the class itself is used as DI token. However, it seems if MyOptions is a class, you do not need to use #Inject in any case to inject the dependency because NestJS uses the class as DI token, regardless whether you used useValue or useClass to provide the dependency...
Related
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
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,
};
};
}
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 {}
I'm having an issue with Nest + Swagger. When I open my swagger docs I see all of the endpoints I expect but am having two issues:
When I click on a method it expands all of the methods for that controller
The post method says No parameters despite having a DTO defined for the body
Ultimately I think the issue is: Swagger + Nest is not creating unique operationId's for each method. My understanding is that methods will only fail to get unique operationId's when they are not sufficiently unique: 2 methods with identical call signatures for example.
In the past when I've had issues like this it was either because I was missing the #ApiTags decorator, or I had accidentally included duplicate endpoints.
In general it feels like I missed a step in configuring Swagger, or I didn't set it up properly with Fastify. I installed fastify-swagger but I'm not actually using it anywhere, but according the docs on Nest's site the route for the swagger JSON should be /api/json when using Fastify, which it is for me.
Things that didn't work:
Annotating method with unique #ApiOperation
Adding a addTag to the DocumentBuilder chain
Deleting the swagger-ui-express and #nestjs/platform-express dependencies
Removing all of the Fastify deps and switching to the Express equivalents
Update:
Adding #ApiOperation({ operationId: 'test' }) to a method does fix this, but I was under impression that #nest/swagger did this automatically. Are my methods not unique enough?
main.ts
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))); // allows automatic serialization
app.enableCors();
const config = new DocumentBuilder().setTitle('PIM API').build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document);
await app.listen(process.env.APP_PORT || 3001);
}
bootstrap();
some controller
#ApiTags('chat-messages')
#Controller('chat-messages')
export class ChatMessagesController {
constructor(
private readonly service: ChatMessagesService,
) {}
#Post()
create(#Body() createChatMessageDto: CreateChatMessageDto) {
return this.service.create(createChatMessageDto);
}
#Get(':stream_id')
findByStreamId(#Param('stream_id') streamId: string) {
return this.service.findByStreamId(streamId);
}
ChatMessageDto
export class CreateChatMessageDto {
constructor(partial: Partial<CreateChatMessageDto>) {
Object.assign(this, partial);
}
#IsString()
value: string;
#IsRFC3339()
timestamp: Date;
#IsNotEmpty()
streamId: string;
}
Swagger JSON
{
"/chat-messages":{
"post":{
"operationId":"ChatMessagesController_",
"parameters":[
],
"responses":{
"201":{
"description":"",
"content":{
"application/json":{
"schema":{
"$ref":"#/components/schemas/ChatMessage"
}
}
}
}
},
"tags":[
"chat-messages"
]
}
},
"/chat-messages/{stream_id}":{
"get":{
"operationId":"ChatMessagesController_",
"parameters":[
],
"responses":{
"200":{
"description":"",
"content":{
"application/json":{
"schema":{
"type":"array",
"items":{
"$ref":"#/components/schemas/ChatMessage"
}
}
}
}
}
},
"tags":[
"chat-messages"
]
}
}
}
Did you try putting #ApiProperty in your dto ?
Like this:
export class CreateChatMessageDto {
constructor(partial: Partial<CreateChatMessageDto>) {
Object.assign(this, partial);
}
#ApiProperty()
#IsString()
value: string;
#ApiProperty()
#IsRFC3339()
timestamp: Date;
#ApiProperty()
#IsNotEmpty()
streamId: string;
}
This allows Swagger to see the properties.
This is what NestJs recommends in their documentation See here
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';