I'm trying out a event sourced NestJS application.
I'm stuck at the following point:
In my GamesModule I'm setting up the Eventstore connection to my stream.
In these options there are write and read functions which are called by the library to update/ read the stream's last checkpoint position.
I'd like to call my service methods that write and read to/from the database from these functions.
I've tried injecting the service in the constructor, but because the register function is and has to be a static method, I don't have access to whatever is injected in the constructor.
Is it possible to use a service or repository in the Dynamic module's options?
Service writing to and reading from DB:
#Injectable()
export class EventStoreStateService {
private readonly logger = new Logger(EventStoreStateService.name);
constructor(
#InjectRepository(EventStoreState)
private eventStoreStateRepository: Repository<EventStoreState>,
) {}
updateCheckpoint(stream: string, position: number) {
const updated = this.eventStoreStateRepository.update(
{ lastCheckpoint: position },
{ streamName: stream },
);
this.logger.log({ updated });
return updated;
}
getLastCheckpoint(stream: string) {
const last = this.eventStoreStateRepository.findOne({
where: { streamName: stream },
});
this.logger.log({ last });
return last;
}
}
The module where I setup the event-store connection. In the useFactory store.write(key: string, value: number) I'd like to call my service methods
#Module({
imports: [EventStoreStateModule],
})
export class GamesModule {
constructor(
// no access to this service in the static method
//
#Inject(EventStoreStateService)
private readonly eventStoreStateService: EventStoreStateService,
) {}
static register(): // updateCheckpoint: (key: string, value: number) => Promise<number>,
// getLastCheckpoint: (key: string) => Promise<number>,
DynamicModule {
return {
module: GamesModule,
imports: [
CqrsModule,
EventStoreModule.registerFeatureAsync({
type: 'event-store',
useFactory: async (...args) => {
console.log({ args });
return {
featureStreamName: '$ce-game',
type: 'event-store',
subscriptions: [
{
type: EventStoreSubscriptionType.CatchUp, // research various types
stream: '$ce-game',
resolveLinkTos: true,
},
],
eventHandlers: EventStoreInstanciators,
store: {
storeKey: 'game',
write: async (key: string, value: number) => {
// TODO: on every new event for stream x this function
// is called with the last position number
// problem: we need access to the service that connects
// to ORM, but it's a static method so no access to whatever
// is injected in the constructor
//
},
read: async (key: string) => {
// same as write function
//
},
clear: () => null,
},
};
},
}),
TypeOrmModule.forFeature([GameProjection]),
],
controllers: [GamesController],
providers: [
GamesResolver,
GamesService,
GamesRepository,
...CommandHandlers,
...EventHandlers,
],
};
}
}
Using this library for event-store connection: https://github.com/juicycleff/nestjs-event-store.
Key things to know in NestJs.
If you want to access service within the same module, make sure you inject service in your constructor like constructor( private readonly eventStoreStateService: EventStoreStateService){}
If you want to access service from another module then you have to export the service in .module.ts exports: [EventStoreStateService] and then also inject in service or controller where you want to use it.
Related
I'm currently working on an API with NestJS and Sequelize, and it seems that there is a problem when injecting the repository.
For context, when the service calls this.repo.findByKey the function is undefined and the program fails, but when inspecting with VSCode's debugger, the proptotype had the function inside of it.
Here is a reproduction of the source :
// service.ts
#Injectable()
export class Service {
constructor(
#Inject('REPOSITORY') private repo: IRepository
) {
super();
}
async getByCode(code: string) {
console.log(this.repo); >> [class Repository extends BaseRepository]
console.log(this.repo.findByKey); >> undefined
return res = await this.repo.findByKey(code); >> this.repo.findByKey is not a function
}
}
// repository.ts
#Injectable()
export class Repository
extends BaseRepository
implements IRepository
{
constructor(
#Inject('DATABASE_HELPER')
databseHelper: DatabaseHelper,
private mapper: CountryMapper,
) {
super(databseHelper);
}
async findByKey(code: string) -> Promise<DTO> {
... never reached
}
}
I cannot find the origim of this problem, is there a dark magician here to help me?
Per request, here is the module configuration :
#Module({})
export class ExampleModule {
static register(typeClass: Type<IService>): DynamicModule {
return {
imports: [
HelperModule,
SequelizeModule.forFeature([Entity]),
],
module: ExampleModule,
providers: [
ExampleMapper,
{
provide: 'REPOSITORY',
useValue: Repository,
},
{
provide: 'SERVICE',
useClass: typeClass,
},
],
controllers: [Controller],
};
}
}
You are injecting the class itself. So this.repo is a constructor that never has been called.
{
provide: 'REPOSITORY',
useValue: Repository,
},
useValue is suitable for shared values like config objects, connection strings, etc as the name imply.
If you want to inject class instance you need to configure your provider like this
{
provide: 'REPOSITORY',
useClass: Repository, // <-- notice useClass
},
I have a problem with using multiple ClientKafka in one service, here is my implementation:
#Controller()
export class ApiController implements OnModuleInit {
constructor(
#Inject("ACCOUNT_SERVICE") private readonly accountKafkaClient: ClientKafka,
#Inject("WORKSPACE_SERVICE") private readonly workspaceKafkaClient: ClientKafka
) { }
async onModuleInit() {
const requestPatterns = [
'topic'
];
requestPatterns.forEach((pattern) => {
this.accountKafkaClient.subscribeToResponseOf(`account.${pattern}`);
});
await this.accountKafkaClient.connect();
}
async onModuleDestroy() {
await this.accountKafkaClient.close();
}
#Get()
async sendMessage() {
const data = {
msg: "account.topic"
}
const kafkaResponse = this.accountKafkaClient.send<any>('account.topic', JSON.stringify(data));
const response = await firstValueFrom(kafkaResponse);
const kafkaResponse2 = this.workspaceKafkaClient.send<any>('workspace.topic', JSON.stringify(response )) //THIS IS NOT RUNNING, WORKSPACE_SERVICE NOT RECEIVE ANY MESSAGE
return await firstValueFrom(kafkaResponse2);
}
}
can someone tell me why workspaceKafkaClient is not sending any message to WORKSPACE_SERVICE microservice? I try with passing this client in onModule... functions like accountKafkaClient but it didn't help me,
here is also my settings in module:
#Module({
imports: [
ClientsModule.register([
{
name: 'ACCOUNT_SERVICE',
transport: Transport.KAFKA,
options: {
client: {
clientId: 'account_service',
brokers: ['localhost:29092'],
},
consumer: {
groupId: 'account-consumer',
},
},
},
{
name: 'WORKSPACE_SERVICE',
transport: Transport.KAFKA,
options: {
client: {
clientId: 'workspace_service',
brokers: ['localhost:29092'],
},
consumer: {
groupId: 'workspace-consumer',
},
},
},
]),
],
controllers: [ApiController],
providers: [
ApiService,
// KafkaProducerProvider,
],
})
export class ApiModule {}
thanks for any help!
You only need one producer client per application, but Kafka producers never immediately send data to brokers.
You need to flush them for that to happen, which is what await firstValueFrom(...) should do, but you've not shown that method.
Otherwise, you seem to be trying to get the reponse from one topic to send to another, which is what a consumer should be used for, rather than blocking on one producer request.
At the moment, I have a very simple class-validator file with a ValidationPipe in Nest.js as follows:
import {
IsDateString,
IsEmail,
IsOptional,
IsString,
Length,
Max,
} from 'class-validator';
export class UpdateUserDto {
#IsString()
id: string;
#Length(2, 50)
#IsString()
firstName: string;
#IsOptional()
#Length(2, 50)
#IsString()
middleName?: string;
#Length(2, 50)
#IsString()
lastName: string;
#IsEmail()
#Max(255)
email: string;
#Length(8, 50)
password: string;
#IsDateString()
dateOfBirth: string | Date;
}
Lets say in the above "UpdateUserDto," the user passes an "email" field. I want to build a custom validation rule through class-validator such that:
Check if email address is already taken by a user from the DB
If the email address is already in use, check if the current user (using the value of 'id' property) is using it, if so, validation passes, otherwise, if it is already in use by another user, the validation fails.
While checking if the email address is already in use is a pretty simple task, how would you be able to pass the values of other properties within the DTO to a custom decorator #IsEmailUsed
It was pretty simple to solve, I solved it by creating a custom class-validation Decorator as below:
import { PrismaService } from '../../prisma/prisma.service';
import {
registerDecorator,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
} from 'class-validator';
import { Injectable } from '#nestjs/common';
#ValidatorConstraint({ name: 'Unique', async: true })
#Injectable()
export class UniqueConstraint implements ValidatorConstraintInterface {
constructor(private readonly prisma: PrismaService) {}
async validate(value: any, args: ValidationArguments): Promise<boolean> {
const [model, property = 'id', exceptField = null] = args.constraints;
if (!value || !model) return false;
const record = await this.prisma[model].findUnique({
where: {
[property]: value,
},
});
if (record === null) return true;
if (!exceptField) return false;
const exceptFieldValue = (args.object as any)[exceptField];
if (!exceptFieldValue) return false;
return record[exceptField] === exceptFieldValue;
}
defaultMessage(args: ValidationArguments) {
return `${args.property} entered is not valid`;
}
}
export function Unique(
model: string,
uniqueField: string,
exceptField: string = null,
validationOptions?: ValidationOptions,
) {
return function (object: any, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [model, uniqueField, exceptField],
validator: UniqueConstraint,
});
};
}
However, to allow DI to that particular Decorator, you need to also add this to your main.ts bootstrap function:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
...
// Line below needs to be added.
useContainer(app.select(AppModule), { fallbackOnErrors: true });
...
}
Also, make sure to import the "Constraint" in the app module:
#Module({
imports: ...,
controllers: [AppController],
providers: [
AppService,
PrismaService,
...,
// Line below added
UniqueConstraint,
],
})
export class AppModule {}
Finally, add it to your DTO as such:
export class UpdateUserDto {
#IsString()
id: string;
#IsEmail()
#Unique('user', 'email', 'id') // Adding this will check in the user table for a user with email entered, if it is already taken, it will check if it is taken by the same current user, and if so, no issues with validation, otherwise, validation fails.
email: string;
}
Luckily for us, the class-validator provides a very handy useContainer function, which allows setting the container to be used by the class-validor library.
So add this code in your main.ts file (app variable is your Nest application instance):
useContainer(app.select(AppModule), { fallbackOnErrors: true });
It allows the class-validator to use the NestJS dependency injection container.
#ValidatorConstraint({ name: 'emailId', async: true })
#Injectable()
export class CustomEmailvalidation implements ValidatorConstraintInterface {
constructor(private readonly prisma: PrismaService) {}
async validate(value: string, args: ValidationArguments): Promise<boolean> {
return this.prisma.user
.findMany({ where: { email: value } })
.then((user) => {
if (user) return false;
return true;
});
}
defaultMessage(args: ValidationArguments) {
return `Email already exist`;
}
}
Don't forget to declare your injectable classes as providers in the appropriate module.
Now you can use your custom validation constraint. Simply decorate the class property with #Validate(CustomEmailValidation) decorator:
export class CreateUserDto {
#Validate(customEmailValidation)
email: string;
name: string;
mobile: number;
}
If the email already exists in the database, you should get an error with the default message "Email already exists". Although using #Validate() is fine enough, you can write your own decorator, which will be much more convenient. Having written Validator Constraint is quick and easy. We need to just write decorator factory with registerDecorator() function.
export function Unique(validationOptions?: ValidationOptions) {
return function (object: any, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
validator: CustomEmailvalidation,
});
};
}
As you can see, you can either write new validator logic or use written before validator constraint (in our case - Unique class).
Now we can go back to our User class and use the #Unique validator instead of the #Validate(CustomEmailValidation) decorator.
export class CreateUserDto {
#Unique()
email: string;
name: string;
mobile: number;
}
I think your first use case (Check if email address is already taken by a user from the DB), can be solved by using custom-validator
For the second one there is no option to get the current user before the validation. Suppose you are getting the current user using the #CurrentUser decorator. Then once the normal dto validation is done, you need to check inside the controller or service if the current user is accessing your resource.
I created a NestJS and used TypeORM for the RDBMS(I used postgres in my project).
Post is an #Entity class, PostRepository is a Repository class for Post.
I was trying to create OnModuleInit service to initialize some data.
#Injectable()
export class PostsDataInitializer implements OnModuleInit {
private data: Post[] = [
{
title: 'Generate a NestJS project',
content: 'content',
},
{
title: 'Create GrapQL APIs',
content: 'content',
},
{
title: 'Connect to Postgres via TypeORM',
content: 'content',
},
];
constructor(private readonly postRepository: PostRepository) {}
async onModuleInit(): Promise<void> {
await this.postRepository.manager.transaction(async (manager) => {
// NOTE: you must perform all database operations using the given manager instance
// it's a special instance of EntityManager working with this transaction
// and don't forget to await things here
await manager.delete(Post, {});
console.log('deleted: {} ');
this.data.forEach(async (d) => await manager.save(d as Post));
const savedPosts = await manager.find<Post>(Post);
savedPosts.forEach((p) => {
console.log('saved: {}', p);
});
});
}
}
When starting up the application, I got the following error.
CannotDetermineEntityError: Cannot save, given value must be instance of entity class, instead object literal is given. Or you must specify an entity target to method call.
But the above save was accepting an instance of Post.
I think it is pretty much what the error says. You cannot pass literal objects to .save
private data = [
{
title: 'Generate a NestJS project',
content: 'content',
},
{
title: 'Create GrapQL APIs',
content: 'content',
},
{
title: 'Connect to Postgres via TypeORM',
content: 'content',
},
].map(data => {
const post = new Post();
Object.assign(post, data);
return post;
})
the above might solve this.
You can alternatively specify the target type as per the error message like so:
await manager.save(Post, d))
From the documentation of save() I see there is such a method as:
save<Entity, T extends DeepPartial<Entity>>(targetOrEntity: EntityTarget<Entity>, entity: T, options?: SaveOptions): Promise<T & Entity>;
nest version 9.1.8
I try to block some root if it's not an admin, but when I run the code I have a TypeError but I don't know how to resolve it.
Thanks
roles.guards.ts
import { Injectable, CanActivate, ExecutionContext } from '#nestjs/common';
import { Reflector } from '#nestjs/core';
import { Role } from './role.enums';
import { ROLES_KEY } from './roles.decorator';
#Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((Role) => user.roles?.includes(Role));
}
}
articles.controllers.ts
#UseGuards(JwtAuthGuard)
#Roles(Role.Admin)
async addArticle(
#Body('title') artTitle: string,
#Body('description') artDescription: string,
#Body('url') artUrl: string,
#Body('cover') artCover: string,
#Body('content') artContent: string,
#Body('category') artCategory: string,
){
const generatedId = await this.articlesService.insertArticle(
artTitle,
artDescription,
artUrl,
artCover,
artContent,
artCategory
);
return { id: generatedId };
}
when I run the code I have a TypeError but I don't know how to resolve it.
Thanks
I'd like to add more detail to Jay McDoniel's answer since it still took me a few hours to get around this issue.
Create JWT.module.ts (JwtModule is already used by #nestjs/jwt hence my use of caps) file with the following:
import { ConfigModule, ConfigService } from "#nestjs/config";
import { JwtModule } from "#nestjs/jwt";
#Module({
imports: [
{
...JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secretOrKeyProvider: () => (configService.get<string>("JWT_SECRET")),
signOptions: {
expiresIn: 3600,
},
}),
inject: [ConfigService],
}),
global: true
}
],
exports: [JwtModule]
})
export class JWTModule {}
Add this class to your app.module.ts's imports array.
if you have
{
provide: APP_GUARD,
useClass: RolesGuard,
},
in any of your modules... DELETE IT. declaring guards in any providers will automatically make it global and endpoints which you don't want to be guarded will end up getting guarded (I'm aware https://docs.nestjs.com/security/authorization#basic-rbac-implementation tells you to register the role guard in your providers but that just didn't work for me). You only need to import your strategies to the relevant routes.
In your controller, this should now work
#ApiBearerAuth()
#Roles(Role.Admin)
#UseGuards(JwtAuthGuard, RolesGuard)
#Get()
findAll() {
return this.usersService.findAll();
}
so this endpoint accepts users with a valid JWT and an admin role inside said JWT.
If I had to bet, your RolesGuard is bound to the global scope, whereas the JwtAuthGuard is bound to the route handler scope. Global guards execute first and foremost, so the RolesGuard executes before the JwtAuthGaurd can set req.user (passport is what does this under the hood). What you can do is either ensure that there is a req.user property (either via a middleware or jutt running the JwtAuthGuard globally) or you can move the RolesGuard to be scoped at the route handler level after the JwtAuthGuard runs.
Use JwtGuard and RoleGuard in the controller like #UseGuards(JwtAuthGuard, RolesGuard). The issue because of RoleGuards is not used in guard.
#Roles(Role.ADMIN)
#UseGuards(JwtAuthGuard,RolesGuard)
#Query(() => [User], { name: 'User' })
articles.module.ts
in this module file update in provider rolesGuards
providers: [AuthResolver, AuthService,LocalStrategy,JwtStrategy,RolesGuard],
use #Post() above your controller
#UseGuards(JwtAuthGuard)
#Roles(Role.Admin)
#Post('')
async addArticle(
#Body('title') artTitle: string,
#Body('description') artDescription: string,
#Body('url') artUrl: string,
#Body('cover') artCover: string,
#Body('content') artContent: string,
#Body('category') artCategory: string,
){
const generatedId = await this.articlesService.insertArticle(
artTitle,
artDescription,
artUrl,
artCover,
artContent,
artCategory
);
return { id: generatedId };
}