Access the collection of providers for a module in nestjs - nestjs

Is there any way for a method in a class decorated with #Module() to iterate over all provider objects defined for that module?

You can access metadata by a key and module class.
For example:
console.log(Reflect.getMetadata('providers', UserModule));
Output will be like:
[class UserService], [class ConfigService] ]
Underhood, #Module decorator looks like this:
function Module(metadata) {
const propsKeys = Object.keys(metadata);
validate_module_keys_util_1.validateModuleKeys(propsKeys);
return (target) => {
for (const property in metadata) {
if (metadata.hasOwnProperty(property)) {
Reflect.defineMetadata(property, metadata[property], target);
}
}
};
}
It defines a metadata by a property key (providers, controllers, imports and etc.) and saves it for a target (module class).
Metadata API: https://github.com/rbuckton/reflect-metadata#api

This might not be the best solution, but at least it 'works for me'. I ended up defining all the provider classes in an array:
const MyProviders = [ ProviderClass1, ProviderClass2, ...];
Then used that constant in the module definition:
#Module({
imports: [],
providers: MyProviders,
exports: MyProviders,
controllers: []
})
export class MyModule {
I defined the constructor to include a ModuleRef:
public constructor(private readonly moduleRef: ModuleRef) {}
And then it was a relatively simple method to iterate over the instantiated provider objects:
public someMethod(): void {
for (const cls of MyProviders) {
const provider = this.moduleRef.get(cls.name);
//
// Can now call methods, etc on provider
//
}
}
If there's a better way of doing this, I'd love to hear it.

Related

Nest.js inject service into plain typescript class

I have a NestJS App and I have a Class where the app generates multiple instances from. Inside that class I need to access a service method but I dont know how to inject a service into a plain class.
This is the service I want to use inside the class "Stream"
import { Injectable } from '#nestjs/common';
import * as technicalindicators from 'technicalindicators';
import { CandleIndex } from 'src/utils/CandleIndex';
import * as ccxt from 'ccxt';
#Injectable()
export class AnalysisService {
async RSI(ohlcv: ccxt.OHLCV[], period: number) {
const close = ohlcv.map((candle) => candle[CandleIndex.CLOSE]);
const result = technicalindicators.rsi({ values: close, period: period });
return result;
}
}
import { AnalysisService } from './analysis.service';
import { Module } from '#nestjs/common';
#Module({
imports: [],
controllers: [],
providers: [AnalysisService],
exports: [AnalysisService],
})
export class AnalysisModule {}
Here is the class I want to get access to the analysis service. The class is a normal typescript class, its not part of any module.
export class Stream {
private exchange: ccxt.Exchange;
private market: ccxt.Market;
private timeframe: string;
private ohlcv_cache: ccxt.OHLCV[];
private createdAt: Date;
private stream;
#Inject(AnalysisService)
private readonly analysisService: AnalysisService;
constructor(exchange: ccxt.Exchange, market: ccxt.Market, timeframe: string) {
this.exchange = exchange;
this.market = market;
this.timeframe = timeframe;
this.createdAt = new Date();
this.initialize();
console.log(this.analysisService);
}
}
When I do this and call a method of the analysis service inside of the Stream class, analysisService is undefined.
As you're calling new Stream() yourself, Nest will do no injection for you. You'd need to either pass the AnalysisService instance yourself, or you'd need to create a setter for that property before running any methods that need the AnalysisService.

Nestjs: calling service functions from Model / Entity with sequelize hooks

In NestJS, I have to use a module service into an entity/model to populate data into elastic-search index. populating elastic search index logic is written in Job.service.ts.
I want to call that onCreate method from Job.service.ts from sequelize hooks present in models.
Here is code for Job.ts model/entity -
import { Table, Model, Column, AutoIncrement, PrimaryKey } from "sequelize-typescript";
#Table({ schema: "job", tableName: "job" })
export class Job extends Model<Job> {
#AutoIncrement
#PrimaryKey
#Column
id: number;
#Column
title: string;
#AfterCreate
static async jobAfterCreate(instance, options) {
// <--- need to call job service onCreate method here
}
#AfterUpdate
static async jobAfterUpdate() {}
#AfterDestroy
static async jobAfterDestroy() {}
}
and here is code for Job.service.ts -
//imports not added
#Injectable()
export class JobService {
constructor(
#Inject("SEQUELIZE")
private readonly sequelizeInstance: Sequelize,
#Inject(forwardRef(() => ElasticsearchService))
private readonly elasticsearchService: ElasticsearchService,
#InjectModel(Job)
private jobModel: typeof Job
) {}
// here will write logic for updating elastic search index
async onCreate(instance, options){
console.log("ON CREATE INSTANCE:", instance);
console.log("ON CREATE OPTIONS:", options);
}
async onDestroy(instance, options){
console.log("ON DESTROY INSTANCE:", instance);
console.log("ON DESTROY OPTIONS:", options);
}
}
I tried injecting service into Job model but it did not worked.
And I cannot write elastic search logic inside model directly because for that I need ElasticsearchService.
The Solution is To Override the provider
The primary way to inject information into the models is by overriding the injection behavior.
First, you would need to add a static property referencing the service in your model.
I am going to use the event emitter as an example here.
Your Model Class
import {Model, Table, Column, AfterCreate} from "sequelize-typescript";
import { EventEmitter2 } from "#nestjs/event-emitter";
#Table()
export class SomeModel extends <SomeModel> {
// this would be your referencing
public static EventEmitter: EventEmitter2;
#Column
public someColumn: string;
#AfterCreate
public static triggerSomeEvent(instance: SomeModel) {
SomeModel.EventEmitter.emit('YourEvent', instance);
}
}
The module where you are going to use the model
Now we are overriding the default injection process.
import { EntitiesMetadataStorage } from '#nestjs/sequelize/dist/entities-metadata.storage';
import {
getConnectionToken,
getModelToken,
SequelizeModule,
} from '#nestjs/sequelize';
import { EventEmitter2 } from '#nestjs/event-emitter';
// The provider override
const modelInjector: Provider = {
provide: getModelToken(AccountabilityPartnerModel, DEFAULT_CONNECTION_NAME),
useFactory: (connection: Sequelize, eventEmitter: EventEmitter2) => {
SomeModel.EventEmitter = eventEmitter;
if (!connection.repositoryMode) {
return SomeModel;
}
return connection.getRepository(SomeModelas any);
},
inject: [getConnectionToken(DEFAULT_CONNECTION_NAME), EventEmitter2],
};
// Updating the meta information of sequelize-typescript package to handle connection injection in to the model overridden.
EntitiesMetadataStorage.addEntitiesByConnection(DEFAULT_CONNECTION_NAME, [
SomeModel,
]);
// our custom module being used rather than the Sequelize.forFeature([SomeModel])
const someModelModule: DynamicModule = {
module: SequelizeModule,
providers: [modelInjector],
exports: [modelInjector],
};
#Module({
imports: [someModelModule],
providers: [SomeService],
})
export class SomeModule {
}
Inject your model into your service as you would do using Sequlize.forFeature and InjectModel indicated as below.
#Injectable()
export class SomeService {
constructor(#InjectModel(SomeModel) someModel: typeof SomeModel) {}
public someFunction(data: any) {
this.someModel.EventEmitter.emit('YourEvent', data);
}
}

NestJS with MongoDB and NestJsxAutomapper resulting in error 'cannot read property plugin of undefined'

I am working on an API with NestJS, and because I have DTO's I am using an AutoMapper (made by #nartc and/or nestjsx), I have tried to make my example as small as I could with the Foo example, because I use multiple files.
This is my module:
// foo.module.ts
import { Module } from "#nestjs/common";
import { MongooseModule } from "#nestjs/mongoose";
import { Foo, FooSchema } from "./foo.entity.ts";
import { FooController } from "./foo.controller.ts";
import { FooService } from "./foo.service.ts";
import { FooProfile } from "./foo.profile.ts";
#Module({
imports: [
MongooseModule.forFeature([
{
name: Foo.name,
schema: FooSchema,
collection: "foos",
}
])
// FooProfile <-- if I uncomment this, the program will give the error (shown at the bottom of this question)
],
controllers: [FooController],
providers: [FooProivder],
})
export class FooModule {}
This is my entity:
// foo.entity.ts
import { Schema, SchemaFactory, Prop } from "#nestjs/mongoose";
import { Document } from "mongoose";
#Schema()
export class Foo extends Document { // if I remove the `extends Document` it works just fine
#Prop({ required: true })
name: string;
#Prop()
age: number
}
export const FooSchema = SchemaFactory.createForClass(Foo);
This is my DTO:
// foo.dto.ts
export class FooDTO {
name: string;
}
This is my controller:
// foo.controller.ts
import { Controller, Get } from "#nestjs/common";
import { InjectMapper, AutoMapper } from "nestjsx-automapper";
import { Foo } from "./foo.entity";
import { FooService } from "./foo.service";
import { FooDTO } from "./dto/foo.dto";
#Controller("foos")
export class FooController {
constructor(
private readonly fooService: FooService
#InjectMapper() private readonly mapper: AutoMapper
) {}
#Get()
async findAll() {
const foos = await this.fooService.findAll();
const mappedFoos = this.mapper.mapArray(foos, Foo, FooDTO);
// ^^ this throws an error of the profile being undefined (duh)
return mappedFoos;
}
}
This is my profile:
// foo.profile.ts
import { Profile, ProfileBase, InjectMapper, AutoMapper } from "nestjsx-automapper";
import { Foo } from "./foo.entity";
import { FooDTO } from "./foo.dto";
#Profile()
export class FooProfile extends ProfileBase {
constructor(#InjectMapper() private readonly mapper: AutoMapper) {
// I've read somewhere that the `#InjectMapper() private readonly` part isn't needed,
// but if I exclude that, it doesn't get the mapper instance. (mapper will be undefined)
super();
this.mapper.createMap(Foo, FooDTO);
}
}
If I uncomment the line I highlighted in the module, it will result in the following error..
[Nest] 11360 - 2020-08-18 15:53:06 [ExceptionHandler] Cannot read property 'plugin' of undefined +1ms
TypeError: Cannot read property 'plugin' of undefined
at Foo.Document.$__setSchema ($MYPATH\node_modules\mongoose\lib\document.js:2883:10)
at new Document ($MYPATH\node_modules\mongoose\lib\document.js:82:10)
at new Foo($MYPATH\dist\foo\foo.entity.js:15:17)
I have also referred to this answer on stackoverflow, but that doesn't work for me either. I have also combined that with the documentation, but with no luck.. How would I get the AutoMapper to register my profiles?
Update
The error seems to originate from the foo entity, if I remove the extends Document and the Schema(), Prop({ ... }) from the class it works fine, it seems like I have to inject mongoose or something?
In your module, just import the path to the profile like below:
import 'relative/path/to/foo.profile';
By importing the path to file, TypeScript will include the file in the bundle and then the #Profile() decorator will be executed. When #Profile() is executed, AutoMapperModule keeps track of all the Profiles then when it's turn for NestJS to initialize AutoMapperModule (with withMapper() method), AutoMapperModule will automatically add the Profiles to the Mapper instance.
With that said, in your FooProfile's constructor, you'll get AutoMapper instance that this profile will be added to
#Profile()
export class FooProfile extends ProfileBase {
// this is the correct syntax. You would only need private/public access modifier
// if you're not going to use this.mapper outside of the constructor
// You DON'T need #InjectMapper() because that's Dependency Injection of NestJS.
// Profile isn't a part of NestJS's DI
constructor(mapper: AutoMapper) {
}
}
The above answer will solve your problems with AutoMapper. As far as your Mongoose problem, I would need a sample repro to tell for sure. And also, visit our Discord for this kind of question.
What worked for me.
1. Updated all the absolute paths for models, schemas, entities (is easy if you search for from '/src in your projects, and update all the routes to relative paths)
from:
import { User } from 'src/app/auth/models/user/user.entity';
to:
import { User } from './../../auth/models/user/user.entity';
2. mongoose imports:
from:
import mongoose from 'mongoose';
to:
import * as mongoose from 'mongoose';
3. Remove validation pipe if you don't use it. For some reason (I think i don't use them on the controller, I didn't investigate, I've removed from one controller the Validation Pipe) so If you have this try it:
from:
#Controller('someroute')
#UsePipes(new ValidationPipe())
export class SomeController {}
to:
#Controller('someroute')
export class SomeController {}
I hope my solution worked for you ^_^

How to create a NestJs Pipe with a config object and dependency?

I would Like to pass a configuration string to a Pipe but also want to inject a service. The NesJs docs describe how to do both of these independent of each other but not together. Take the following example:
pipe.ts
#Injectable()
export class FileExistsPipe implements PipeTransform {
constructor(private filePath: string, db: DatabaseService) { }
async transform(value: any, metadata: ArgumentMetadata) {
const path = value[this.filePath];
const doesExist = await this.db.file(path).exists()
if(!doesExist) throw new BadRequestException();
return value;
}
}
controller.ts
#Controller('transcode')
export class TranscodeController {
#Post()
async transcode (
#Body( new FileExistsPipe('input')) transcodeRequest: JobRequest) {
return await this.videoProducer.addJob(transcodeRequest);
}
Basically, I want to be able to pass a property name to my pipe (e.g.'input') and then have the pipe look up the value of the property in the request (e.g.const path = value[this.filePath]) and then look to see if the file exists or not in the database. If it doesn't, throw a Bad Request error, otherwise continue.
The issue I am facing is that I need NestJs to inject my DataBaseService. With the current example, It won't and my IDE gives me an error that new FileExistsPipe('input') only has one argument passed but was expecting two (e.g. DatabaseService).
Is there anyway to achieve this?
EDIT: I just checked your repo (sorry for missing it before). Your DatabaseService is undefined in the FIleExistPipe because you use the pipe in AppController. AppController will be resolved before the DatabaseModule gets resolved. You can use forwardRef() to inject the DatabaseService in your pipe if you are going to use the pipe in AppController. The good practice here is to have feature controllers provided in feature modules.
export const FileExistPipe: (filePath: string) => PipeTransform = memoize(
createFileExistPipe
);
function createFileExistPipe(filePath: string): Type<PipeTransform> {
class MixinFileExistPipe implements PipeTransform {
constructor(
// use forwardRef here
#Inject(forwardRef(() => DatabaseService)) private db: DatabaseService
) {
console.log(db);
}
async transform(value: ITranscodeRequest, metadata: ArgumentMetadata) {
console.log(filePath, this.db);
const doesExist = await this.db.checkFileExists(filePath);
if (!doesExist) throw new BadRequestException();
return value;
}
}
return mixin(MixinFileExistPipe);
}
You can achieve this with Mixin. Instead of exporting an injectable class, you'd export a factory function that would return such class.
export const FileExistPipe: (filePath: string) => PipeTransform = memoize(createFileExistPipe);
function createFileExistPipe(filePath: string) {
class MixinFileExistPipe implements PipeTransform {
constructor(private db: DatabaseService) {}
...
}
return mixin(MixinFileExistPipe);
}
memoize is just a simple function to cache the created mixin-pipe with the filePath. So for each filePath, you'd only have a single version of that pipe.
mixin is a helper function imported from nestjs/common which will wrap the MixinFileExistPipe class and make the DI container available (so DatabaseService can be injected).
Usage:
#Controller('transcode')
export class TranscodeController {
#Post()
async transcode (
// notice, there's no "new"
#Body(FileExistsPipe('input')) transcodeRequest: JobRequest) {
return await this.videoProducer.addJob(transcodeRequest);
}
a mixin guard injecting the MongoDB Connection
the console shows the connection being logged

nestjs instantiate not injectable class

i'm pretty new in nestJS, so you can address me directly to documentation if my question was covered there, but i can't figure out how to create instance of none-injectable class.
Here is simplified version of code
export default class BoardingPass {
constructor(
orderId: string,
companyIATa: string,
token: string,
private readonly checkinOrder = new CheckinOrder(orderId, companyIATa, token) // <-- i need to instantiate CheckinOrder
) {}
}
So i need to instantiate the CheckinOrder class but it requires ApolloClient which can be provided only via nestJS DI mechanism
export default class CheckinOrder implements ICheckinOrder {
private order: CheckinOrderObject;
constructor(
private readonly id: string,
private readonly carrierIATACode: string,
private readonly accessToken: string,
private readonly apolloClient: ApolloClient<NormalizedCacheObject> // can't figure out how to pass it via DI
) {}
}
update
Read about custom providers, it seems like it what i need, but as you can see there is some dynamic arguments passed to init method of CheckinOrder
const checkinOrderProvider: Provider<CheckinOrder> = {
useFactory: apolloClient => new CheckinOrder('1', '2', '3', apolloClient),
provide: CheckinOrder,
inject: [ApolloClient]
};
#Module({
imports: [ConfigModule.forRoot(), ApolloClientModule],
providers: [checkinOrderProvider],
exports: [checkinOrderProvider]
})
export class GooglePayModule {}
and only Apollo client is static, so its still unclear how to implement regular interface composition when class Boarding pass has instance of class CheckinOrder as property :(
You can provide the factory method and use it somewhere to create your service just in time. As we do here:
https://github.com/valueadd-poland/pimp-my-pr/blob/master/libs/server/repository/infrastructure/src/lib/repositories/repository-repository.adapter.ts#L18

Resources