I've created a saga to react on a given event. In that case, multiple commands need to be issued.
My Saga looks like this:
#Injectable()
export class SomeSagas {
public constructor() {}
onSomeEvent(events$: EventObservable<any>): Observable<ICommand> {
return events$.ofType(SomeEvent).pipe(
map((event: SomeEvent) => {
return of(new SomeCommand(uuid()), new SomeCommand(uuid()));
}),
);
}
}
When debugging I found that there is an error thrown 'CommandHandler not found exception!', which is kind of confusing because in case I return only one instance of SomeCommand the command handler is called correctly.
Do I miss something or is the saga implementation just not supporting issuing multiple commands?
Seems like I found the answer to it - it's related to RxJS:
#Injectable()
export class SomeSagas {
public constructor() {}
onSomeEvent(events$: EventObservable<any>): Observable<ICommand> {
return events$.ofType(SomeEvent).pipe(
map((event: SomeEvent) => {
const commands: ICommand[] = [
new SomeCommand(uuid()),
new SomeCommand(uuid()),
new SomeCommand(uuid()),
];
return commands;
}),
flatMap(c => c), // piping to flatMap RxJS operator is solving the issue I had
);
}
}
You can use a mergeMap() to emit another observable. It will continue to emit that observable until it completes, but in your case that will happen all at once if you use an of() observable.
onSomeEvent(events$: EventObservable<any>): Observable<ICommand> {
return events$.ofType(SomeEvent).pipe(
mergeMap((event: SomeEvent) => of(
new SomeCommand(uuid()),
new SomeCommand(uuid()),
new SomeCommand(uuid()),
))
);
}
}
Since flatMap is deprecated you can use mergeMap instead:
#Injectable()
export class Saga {
public constructor() {}
onSomeEvent(events$: EventObservable<any>): Observable<ICommand> {
return events$.ofType(SomeEvent).pipe(
map((event: SomeEvent) => {
const commands: ICommand[] = [
new FirstCommans(uuid()),
new SecondCommand(uuid()),
];
return commands;
}),
mergeMap(c => c),
);
}
}
Related
I am having a small question about NestJS. In my code, there is a service which looks something like:
`
import { Inject, Injectable } from '#nestjs/common';
import neo4j, { Driver, int, Result, Transaction } from 'neo4j-driver';
import { Neo4jConfig } from './neo4j-config.interface';
import { NEO4J_CONFIG, NEO4J_DRIVER } from './neo4j.constants';
#Injectable()
export class Neo4jService {
constructor(
#Inject(NEO4J_CONFIG) private readonly config: Neo4jConfig,
#Inject(NEO4J_DRIVER) private readonly driver: Driver,
) {}
onApplicationBootstrap() {
console.log('Hello');
}
getDriver(): Driver {
return this.driver;
}
getConfig(): Neo4jConfig {
return this.config;
}
int(value: number) {
return int(value);
}
beginTransaction(database?: string): Transaction {
const session = this.getWriteSession(database);
return session.beginTransaction();
}
getReadSession(database?: string) {
return this.driver.session({
database: database || this.config.database,
defaultAccessMode: neo4j.session.READ,
});
}
getWriteSession(database?: string) {
return this.driver.session({
database: database || this.config.database,
defaultAccessMode: neo4j.session.WRITE,
});
}
read(
cypher: string,
params?: Record<string, unknown>,
databaseOrTransaction?: string | Transaction,
): Result {
if (databaseOrTransaction instanceof Transaction) {
return (<Transaction>databaseOrTransaction).run(cypher, params);
}
const session = this.getReadSession(<string>databaseOrTransaction);
return session.run(cypher, params);
}
write(
cypher: string,
params?: Record<string, unknown>,
databaseOrTransaction?: string | Transaction,
): Result {
if (databaseOrTransaction instanceof Transaction) {
return (<Transaction>databaseOrTransaction).run(cypher, params);
}
const session = this.getWriteSession(<string>databaseOrTransaction);
return session.run(cypher, params);
}
private onApplicationShutdown() {
console.log('Goodbye')
return this.driver.close();
}
}
`
Then in my main.ts file I have this method called:
`
await app.listen(port);
`
As you can see my service does not implement neither onApplicationBootstrap nor onApplicationShutdown.
How does it come that those methods still get invoked? Should I implement onApplicationBootstrap and onApplicationShutdown or not?
As you can also see I' d like that my onApplicationBootstrap is a private method which would not be possible if I implement the interface.
So, I would like to ask you:
Why the two lifecycle methods get called event without implementing the interface?
Should I implement those interfaces at all or just go on and use the methods which would allow me to define them as private?
I expected those methods to not work without implementing the interfaces
The Typescript interface is there to help us as devs. It doesn't exist at runtime, there's no information about it, so the only thing Nest can do is just check "Hey, does this class have the onModuleInit method?" If yes, add it to a list of classes to call onModuleInit. Do the same with the other lifecycle methods.
The interfaces aren't explicitly necessary, but they do give us devs a better idea of the class by just looking at the export class... line because we can see what is implemented/extended.
I'm trying to develop a healthcheck endpoint with NestJS (in which I have no experience). One of the dependencies I want to check is Twilio's SMS service. So far, the best URL I've found to gather this information is https://status.twilio.com/api/v2/status.json. The problem here is that I don't want to merely ping this address, but to gather it's JSON response and present some of the information it provides, namely these:
Is it possible, using (or not) the Terminus module? In the official docs I didn't find anything regarding this, only simpler examples using pingCheck / responseCheck: https://docs.nestjs.com/recipes/terminus
Yes, it is possible.
I have never used this, but HttpHealthIndicator has responseCheck method to check depends on the API response message. You can specify a callback function to analyze responses from the API. The callback function should return boolean represents the status of the API.
I couldn't find this in the documents, but you can see it here.
Although meanwhile the logic for this healthcheck has changed (and so this question became obsolete), this was the temporary solution I've found, before it happened (basically a regular endpoint using axios, as pointed out in one of the comments above):
Controller
import { Controller, Get } from '#nestjs/common';
import { TwilioStatusService } from './twilio-status.service';
#Controller('status')
export class TwilioStatusController {
constructor(private readonly twilioStatusService: TwilioStatusService) {}
#Get('twilio')
getTwilioStatus() {
const res = this.twilioStatusService.getTwilioStatus();
return res;
}
}
Service
import { HttpService } from '#nestjs/axios';
import { Injectable } from '#nestjs/common';
import { map } from 'rxjs/operators';
#Injectable()
export class TwilioStatusService {
constructor(private httpService: HttpService) {}
getTwilioStatus() {
return this.httpService
.get('https://status.twilio.com/api/v2/status.json')
.pipe(map((response) => response.data.status));
}
}
Of course this wasn't an optimal solution, since I had to do this endpoint + a separated one for checking MongoDB's availability (a regular NestJS healthcheck, using Terminus), the goal being an healthcheck that glued both endpoints together.
It is possible to merge in any property to the resulting object. You can see that in the TypeScript Interface
/**
* The result object of a health indicator
* #publicApi
*/
export declare type HealthIndicatorResult = {
/**
* The key of the health indicator which should be uniqe
*/
[key: string]: {
/**
* The status if the given health indicator was successful or not
*/
status: HealthIndicatorStatus;
/**
* Optional settings of the health indicator result
*/
[optionalKeys: string]: any;
};
};
And here is an example:
diagnostics/health/healthcheck.controller
import { Controller, Get } from '#nestjs/common'
import { ApiTags } from '#nestjs/swagger'
import { HttpService } from '#nestjs/axios'
import { HealthCheckService, HealthCheck, HealthIndicatorStatus, HealthCheckError } from '#nestjs/terminus'
#ApiTags('diagnostics')
#Controller('diagnostics/health')
export class HealthController {
constructor(
private health: HealthCheckService,
private httpService: HttpService,
) { }
#Get()
#HealthCheck()
check() {
return this.health.check([
() => this.httpService.get('http://localhost:9002/api/v1/diagnostics/health').toPromise().then(({ statusText, config: { url }, data }) => {
const status: HealthIndicatorStatus = statusText === 'OK' ? 'up' : 'down'
return { 'other-service': { status, url, data } }
}).catch(({ code, config: { url } }) => {
throw new HealthCheckError('Other service check failed', { 'other-service': { status: 'down', code, url } })
}),
])
}
}
diagnostics/diagnostics.module.ts
import { Module } from '#nestjs/common'
import { TerminusModule } from '#nestjs/terminus'
import { HttpModule } from '#nestjs/axios'
import { HealthController } from './health/health.controller'
#Module({
imports: [
HttpModule,
TerminusModule,
],
controllers: [HealthController],
})
export class DiagnosticsModule { }
I have some Service classes as follows:
//Cat Service:
import { Injectable } from '#nestjs/common';
import { InjectRepository } from '#nestjs/typeorm';
import { Repository, getManager } from 'typeorm';
import { CatRepo } from '../tables/catrepo.entity';
import { CatInterface } from './cat.interface';
#Injectable()
export class CatService {
constructor(
#InjectRepository(CatRepo)
private catRepo: Repository<CatRepo>,
) {}
async customFindAll(offset:number, limit: number): Promise<CatRepo[]> {
const entityManager = getManager();
const catRows = await entityManager.query(
`
SELECT * FROM CATREPO
${offset ? ` OFFSET ${offset} ROWS ` : ''}
${limit ? `FETCH NEXT ${limit} ROWS ONLY` : ''}
`,
);
return catRows;
}
formResponse(cats: CatRepo[]): CatInterface[] {
const catsResults: CatInterface[] = [];
.
//form cat response etc.
.
//then return
return catsResults;
}
}
//Pet Service:
import { Injectable } from '#nestjs/common';
import { getManager } from 'typeorm';
import { PetInterface } from './pet.interface';
#Injectable()
export class PetService {
async customFindAll(offset:number, limit: number) {
const entityManager = getManager();
const petRows = await entityManager.query(
`
JOIN ON TABLES......
${offset ? ` OFFSET ${offset} ROWS ` : ''}
${limit ? `FETCH NEXT ${limit} ROWS ONLY` : ''}
`,
);
//returns list of objects
return petRows;
}
formResponse(pets): PetInteface[] {
const petsResults: PetInteface[] = [];
.
. //form pet response etc.
.
//then return
return petsResults;
}
}
I am running a cron BatchService that uses these two services subsequently saving the data into respective batch files.
I'm calling CatService and PetService from the BatchService as follows:
/Start the Batch job for Cats.
if(resource === "Cat") {
//Call Cat Service
result = await this.catService.findAllWithOffest(startFrom, fetchRows);
finalResult = this.catService.formResponse(result);
}
//Start the batch job for Pets.
if(resource === "Pet") {
//Call Pet Service
result = await this.petService.findAllWithOffest(startFrom, fetchRows);
finalResult = this.petService.formResponse(result);
}
However, instead of the above I want to use these Services dynamically.
In order to achieve the CatService and PetService now extends AbstractService...
export abstract class AbstractService {
public batchForResource(startFrom, fetchRows) {}
}
//The new CatService is as follows:
export class CatService extends AbstractService{
constructor(
#InjectRepository(CatRepo)
private catRepo: Repository<CatRepo>,
) {}
.
.
.
}
//the new PetService is:
export class PetService extends AbstractService{
constructor(
) {super()}
.
.
.
}
//the BatchService...
public getService(context: string) : AbstractService {
switch(context) {
case 'Cat': return new CatService();
case 'Pet': return new PetService();
default: throw new Error(`No service found for: "${context}"`);
}
}
However in the CatService I'm getting the a compilation error...(Expected 1 Argument but got 0). What should be the argument passed in the CatService.
Also, the larger question is if this can be achieved by using NestJS useValue/useFactory...If so how to do it?
You can probably use useFactory to dynamically retrieve your dependencies but there are some gotcha's.
You must make the lifecycle of your services transient, since NestJS dependencies are registered as singletons by default. If not, you would get the same first service injected each time, regardless of the context of subsequent calls.
Your context must come from another injected dependency - ExecutionContext, Request or something similarly dynamic, or something you register yourself.
Alternative
As an alternative, you can implement the "servicelocator/factory" pattern. You're already halfway there with your BatchService. Instead of your service creating instances of the CatService and PetService, you have it injected and just return the injected services depending on the context. Like so:
#Injectable()
export class BatchService {
constructor(
private readonly catService: CatService,
private readonly petService: PetService
)
public getService(context: string) : AbstractService {
switch(context) {
case 'Cat': return this.catService;
case 'Pet': return this.petService;
default: throw new Error(`No service found for: "${context}"`);
}
}
}
The alternative is more flexible than using useFactory, since your context is not limited to what is available in the DI container. On the negative side, it does expose some (usually unwanted) infrastructure details to the calling code, but that's the tradeoff you'll have to make.
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
I have a little probleme, I use the node(express) and angular,
I want display all classes in angular but it's show an error like this
and it's not show json of back-end, what is wrong I don't understand
Return of method http get clasRoutes.js
res.json({ success: true, classes: postQuery, total: nbClasses });
Clas.interface.ts
export interface Clas {
room: string;
img?: string;
}
Clas.service.ts
private urlGetClasses = environment.url + 'classes';
constructor( private http: HttpClient ) { }
getClass(): Observable<Clas[]> {
return this.http.get(this.urlGetClasses).pipe(map(a => console.log(a)));
}
Clas.component.ts
public clas: Clas[] = [];
constructor(private clasService: ClasService) {}
ngOnInit() {
this.getClass().subscribe(res => {
this.clas = res.classes
});
}
private getClass(): Observable < Clas[] > {
return this.clasService.getClass().pipe(tap((value) => {
console.log(value)
}));
}
Clas.component.html
<p *ngFor="let cla of clas">
zefd {{cla.room}}
</p>
The problem is with Clas.service.ts. In your getClass() method, you are mapping to void as the return type of console.log is void; i.e. you are returning void whereas you've specified the return type of getClas() is Observable<Clas[]>. Therefore, you need to change your getClas() method to:
getClass(): Observable<Clas[]> {
return this.http.get(this.urlGetClasses);
}
Or as Nikola mentioned, you can use tap to log like so:
private getClass(): Observable<Clas[]> {
return this.clasService.getClass().pipe(tap((value) => {console.log(value)}));
}
There is new RxJS syntax. You should write it as
private getClass(): Observable<Clas[]> {
return this.clasService.getClass().pipe(tap((value) => {console.log(value)}));
}
make sure you import pipe and tap from rxjs.