Handle mongo/mongoose error in nestjs not - mongodb

I would like to set up a filter to catch mongo errors (I'm using mongoose on this project) but nothing I do works and my research / test of what is on the internet does nothing.
mongoExceptionFilter.ts
import {
ArgumentsHost,
ConflictException,
BadRequestException,
Catch,
ExceptionFilter
} from '#nestjs/common';
import { MongoError } from 'mongodb';
#Catch(MongoError)
export class MongoExceptionFilter implements ExceptionFilter {
catch(exception: MongoError, host: ArgumentsHost): unknown {
switch (exception.code) {
case 11000: // duplicate exception
throw new ConflictException();
default:
throw new BadRequestException(`error ${exception.code}`);
}
}
}
I test a call here main.ts :
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
app.useGlobalFilters(new MongoExceptionFilter());
await app.listen(3001);
}
and here users.controller.ts :
#Post()
#UseFilters(MongoExceptionFilter)
createUser(#Body() body: UsersDto): Promise<Users> {
return this.userService.createUser(body.name, body.password);
}
Some link i found just for information :
How to handle mongoose error with nestjs
NestJs - Catching MongoDB errors with a custom filter and #Catch(MongoError)

Same problem here. The only solution for me now is to catch the MongoError imported from mongodb in the global filter and check for it there

As per this MongoError from MongoDb is different from MongoError from mongoose package. So it seems you are using both the packages i.e. Mongodb and mongoose.

With Custom messages, I combined solutions from answers How to handle mongoose error with nestjs
import { ArgumentsHost, Catch, ExceptionFilter, RpcExceptionFilter } from '#nestjs/common';
import { Error } from 'mongoose';
import { IDTOError } from '../errors/bad-request-exception.error';
import ValidationError = Error.ValidationError;
import { MongoError } from 'mongodb';
#Catch(MongoError)
export class MongoExceptionFilter implements ExceptionFilter {
catch(exception: MongoError, host: ArgumentsHost) {
// switch (exception.code) {
// case 11000:
// default: console.log(exception,'ALERT ERROR CATCHED');
// // duplicate exception
// // do whatever you want here, for instance send error to client
// /** MAIGOD */
// }
const ctx = host.switchToHttp(),
response = ctx.getResponse();
return response.status(400).json(<IDTOError>{
statusCode: 400,
createdBy: 'ValidationErrorFilter, Schema or Model definition',
errors: exception,
});
}
}
#Catch(ValidationError)
export class ValidationErrorFilter implements RpcExceptionFilter {
catch(exception: ValidationError, host: ArgumentsHost): any {
const ctx = host.switchToHttp(),
response = ctx.getResponse();
return response.status(400).json(<IDTOError>{
statusCode: 400,
createdBy: 'ValidationErrorFilter, Schema or Model definition',
errors: exception.errors,
});
}
}

Related

Nest JS user authentication issue with parameter name

I am just learning nestjs for about a day and I came across this strange bug, probably has something to do with me not understanding what Im doing and rushing the project so please bear with me. My main issue is that while using JWT authentication, JSON coming from body is "username" and I can't change it. I want to log in using {"email":"test#gmail.com", "password": "password123"}, but instead it only accepts {"username":"test#gmail.com", "password": "password123"}. The word "username" is not defined or mentioned anywhere in my codebase
users.controller.ts
import { Controller, Get, Post, Body, Param, UseGuards } from '#nestjs/common';
import { UsersService} from './users.service';
import { CreateUserDto} from './dto/create-user.dto';
import { AuthGuard} from '#nestjs/passport';
#Controller('/users')
export class UsersController {
// constructor(private readonly usersService: UsersService) {}
constructor(private readonly userService: UsersService) {}
#UseGuards(AuthGuard('jwt'))
#Get('username')
getUserByEmail(#Param() param) {
return this.userService.getUserByEmail(param.email);
}
#Post('register')
registerUser(#Body() createUserDto: CreateUserDto) {
return this.userService.registerUser(createUserDto);
}
}
users.service.ts
import { Injectable, BadRequestException } from '#nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { Model } from 'mongoose';
import { InjectModel } from '#nestjs/mongoose';
import { HashService } from './hash.service';
import { User, UserDocument} from '../schemas/user.schema'
#Injectable()
export class UsersService {
constructor(#InjectModel(User.name) private userModel: Model < UserDocument > , private hashService: HashService) {}
async getUserByEmail(email: string) {
return this.userModel.findOne({
email
})
.exec();
}
async registerUser(createUserDto: CreateUserDto) {
// validate DTO
const createUser = new this.userModel(createUserDto);
// check if user exists
const user = await this.getUserByEmail(createUser.email);
if (user) {
throw new BadRequestException();
}
// Hash Password
createUser.password = await this.hashService.hashPassword(createUser.password);
return createUser.save();
}
}
auth.controller.ts
import { AuthService} from './auth.service';
import { Controller, Request, UseGuards, Post} from '#nestjs/common';
import { AuthGuard } from '#nestjs/passport';
#Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
#UseGuards(AuthGuard('local'))
#Post(`/login`)
async login(#Request() req) {
console.log(req.user, "here")
return this.authService.login(req.user);
}
}
Here is the source code https://github.com/networkdavit/pillicam_test
Any help or suggestion is highly appreciated!
I tried changing all the parameter names, user schemas, adding a DTO, I googled how to add a custom parameter name or override it, tried to find if "default username param" actually exists. Nothing has worked for me so far
It's in there username in your code. https://github.com/networkdavit/pillicam_test/blob/main/src/users/entities/user.entity.ts#:~:text=class%20User%20%7B-,username%3A%20string%3B,-password%3A%20string
You can change it.
Or you can refer to this article for JWT implementation in nest.js
Just in case anyone ever gets this problem, I found a solution.
All I had to do was to add this to my local.strategy.ts file in constructor
super({
usernameField: 'email',
passwordField: 'password'
});
The default expects a username and password, so have to modify it manually

Opening Mongoose connection in AdonisJS provider times out

I was following this article to use Mongo in AdonisJS 5 project.
I have an AdonisJS provider which I have created by node ace make:provider Mongo (it is registered in .adonisrc.json):
import { ApplicationContract } from '#ioc:Adonis/Core/Application'
import { Mongoose } from 'mongoose'
export default class MongoProvider {
constructor(protected app: ApplicationContract) {}
public async register() {
// Register your own bindings
const mongoose = new Mongoose()
// Connect the instance to DB
await mongoose.connect('mongodb://docker_mongo:27017/mydb')
// Attach it to IOC container as singleton
this.app.container.singleton('Mongoose', () => mongoose)
}
public async boot() {
// All bindings are ready, feel free to use them
}
public async ready() {
// App is ready
}
public async shutdown() {
// Cleanup, since app is going down
// Going to take the Mongoose singleton from container
// and call disconnect() on it
// which tells Mongoose to gracefully disconnect from MongoBD server
await this.app.container.use('Mongoose').disconnect()
}
}
My model is:
import { Schema, model } from '#ioc:Mongoose'
// Document interface
interface User {
email: string
}
// Schema
export default model(
'User',
new Schema<User>({
email: String,
})
)
Controller:
import { HttpContextContract } from '#ioc:Adonis/Core/HttpContext'
import User from 'App/Models/User'
export default class UsersController {
public async index({}: HttpContextContract) {
// Create a cat with random name
const cat = new User({
email: Math.random().toString(36).substring(7),
})
// Save cat to DB
await cat.save()
// Return list of all saved cats
const cats = await User.find()
// Return all the cats (including the new one)
return cats
}
}
And it is timeouting.
It is working, when I open the connection in controller like this though:
import { HttpContextContract } from '#ioc:Adonis/Core/HttpContext'
import User from 'App/Models/User'
import mongoose from 'mongoose'
export default class UsersController {
public async index({}: HttpContextContract) {
await mongoose.connect('mongodb://docker_mongo:27017/mydb')
// Create a cat with random name
const cat = new User({
email: Math.random().toString(36).substring(7),
})
// Save cat to DB
await cat.save()
// Return list of all saved cats
const cats = await User.find()
// Close the connection
await mongoose.connection.close()
// Return all the cats (including the new one)
return cats
}
}
I have just created an AdonisJS provider, registered it in .adonisrc.json, created a contracts/Mongoose.ts with typings, and use the model in controller.
Any idea? I'm stuck for a day with this.
Thanks
I managed to resolve this issue by not storing mongoose in a variable. It seems the mongoose variable you declare in your MongoProvider is the root of your timeout error.
So I did as follow :
export default class MongoProvider {
constructor(protected app: ApplicationContract) {}
public async register() {
await mongoose.connect('mongodb://localhost:27017/dbName')
this.app.container.singleton('Mongoose', () => mongoose)
}
public async boot() {
// All bindings are ready, feel free to use them
}
public async ready() {
// App is ready
}
public async shutdown() {
await this.app.container.use('Mongoose').disconnect()
}
}
If someone would be interested:
with the help of the article author the reason why it is not working was missing Mongoose when creating the model (Mongoose.model instead of just model:
export default Mongoose.model(
'User',
new Schema<User>({
email: String,
})
)
I followed this article too, and I have the same issue you discussed. but resolved this by importing mongoose in my model a little differently.
import mongoose in the model like this import Mongoose, { Schema } from '#ioc:Mongoose' instead of import { Schema, model } from '#ioc:Mongoose'
Example:
import Mongoose, { Schema } from '#ioc:Mongoose'
// Document interface
interface User {
email: string
}
// Schema
export default model(
'User',
new Schema<User>({
email: String,
})
)

NestJS: This operation is not supported by Mongodb driver

I am trying to get all the elements from database but stucking with this error that says:
This operation is not supported by Mongodb driver.
I am using MongoDB along with Mongoose to interact with database.
Here is my code where the error happening:
import { GetTasksFilterDto } from './dto/get-tasks-filter.dto';
import { TaskStatus } from './task-status.enum';
import { CreateTaskDto } from './dto/create-task.dto';
import { TaskEntity } from './task.entity';
import { Repository, EntityRepository } from 'typeorm';
#EntityRepository(TaskEntity)
export class TaskRepository extends Repository<TaskEntity> {
async getTasks(filterDto: GetTasksFilterDto): Promise<TaskEntity[]> {
const { status, search } = filterDto;
const query = this.createQueryBuilder('task');
// The error happen in this line
const tasks = await query.getMany();
return tasks;
}
}
Will this TypeORM MongoDB Documentation did the job for me.
The issue was that, the way I am doing it is wrong, mongodb doesn't support that way of queries, instead I must use the proper way to do it which by using MongoRepository that allows us to create a manager for any Entity.
The code below shows how I solved the problem:
async getTasks(filterDto: GetTasksFilterDto): Promise<TaskEntity[]> {
const { status, search } = filterDto;
const manager = getMongoRepository(TaskEntity);
const tasks = await manager.find({
status: status,
where: {
$or: [
{ text: Like(`%${search.toString()}%`) },
{ title: Like(`%${search.toString()}%`) },
],
},
});
return tasks;
}

ParseObjectIdPipe for MongoDB's ObjectID

I want to create a NestJs API with TypeORM and MongoDB. My entity id fields are of type ObjectID. The controller routes should validate the incoming ids before passing them to the services. I know that Nest ships with the ParseIntPipe and ParseUUIDPipe but as far as I know there is nothing I can use for MongoDBs ObjectID.
So I created my own pipe for those fields as described here https://docs.nestjs.com/pipes#transformation-use-case
import { PipeTransform, Injectable, BadRequestException } from '#nestjs/common';
import { ObjectID } from 'typeorm';
#Injectable()
export class ParseObjectIdPipe implements PipeTransform<any, ObjectID> {
transform(value: any): ObjectID {
const validObjectId: boolean = ObjectID.isValid(value);
if (validObjectId) {
throw new BadRequestException('Invalid ObjectId');
}
const objectId: ObjectID = ObjectID.createFromHexString(value);
return objectId;
}
}
and hope this will do the trick, even for edge cases. I can use it for my route params like
#Get(':id')
public getUserById(#Param('id', ParseObjectIdPipe) id: ObjectID): Promise<User> {
return this.usersService.getUserById(id);
}
The problem I have is that some routes need a Body validation. I use the class-validator package as described here
https://docs.nestjs.com/techniques/validation
It seems that I have to create my own class-validator decorator for those ObjectID fields but that should be fine. Maybe I'll find something here on how to do it https://github.com/typestack/class-validator#custom-validation-classes. But how can I transform those fields to the type ObjectID? Can I use the custom pipe for that later on too?
Update:
I also tried to transform the value via class-transformer package. So the code for this is
import { ObjectID } from 'typeorm';
import { Type, Transform } from 'class-transformer';
import { BadRequestException } from '#nestjs/common';
export class FooDTO {
#Type(() => ObjectID)
#Transform(bar => {
if (ObjectID.isValid(bar)) {
throw new BadRequestException('Invalid ObjectId');
}
return ObjectID.createFromHexString(bar);
})
public bar: ObjectID;
}
Unfortunately the value bar is always undefined. But maybe this code might help for validation and transformation purposes...
I took your code and changed some parts. I tested it, It works fine.
import { PipeTransform, Injectable, BadRequestException } from '#nestjs/common';
import { Types } from 'mongoose';
#Injectable()
export class ParseObjectIdPipe implements PipeTransform<any, Types.ObjectId> {
transform(value: any): Types.ObjectId {
const validObjectId = Types.ObjectId.isValid(value);
if (!validObjectId) {
throw new BadRequestException('Invalid ObjectId');
}
return Types.ObjectId.createFromHexString(value);
}
}
For the class-transformer approach this worked for me
import { IsNotEmpty } from "class-validator";
import { Type, Transform } from 'class-transformer';
import { Types } from "mongoose"
export class CreateCommentDto {
#IsNotEmpty()
#Type(() => Types.ObjectId)
#Transform(toMongoObjectId)
articleId: Types.ObjectId
with this definition for 'toMongoObjectId':
export function toMongoObjectId({ value, key }): Types.ObjectId {
if ( Types.ObjectId.isValid(value) && ( Types.ObjectId(value).toString() === value)) {
return Types.ObjectId(value);
} else {
throw new BadRequestException(`${key} is not a valid MongoId`);
}
}
You are getting undefined because you are importing from wrong lib.
you need to change this:
import { ObjectID } from 'typeorm';
for this:
import { ObjectID } from 'mongodb';
Just create on pipe of object id in code by using below code :
import { PipeTransform, Injectable, BadRequestException } from
'#nestjs/common';
import { ObjectID } from 'mongodb';
#Injectable()
export class ParseObjectIdPipe implements PipeTransform<any, ObjectID> {
public transform(value: any): ObjectID {
try {
const transformedObjectId: ObjectID = ObjectID.createFromHexString(value);
return transformedObjectId;
} catch (error) {
throw new BadRequestException('Validation failed (ObjectId is expected)');
}
}
}
and add piple in controller method :
#Post('cat/:id')
async cat(
#Param('id', ParseObjectIdPipe) id: ObjectId,
#Res() res,
#Body() cat: catDTO[],)

What is the proper way to do seed mongoDB in NestJS, using mongoose and taking advantage of my already defined schmas

We are using NestJS with mongoose and want to seed mongoDB.
Wondering what is the proper way to seed the database, and use the db schemas already defined to ensure the data seeded is valid and properly maintained.
Seeding at the module level (just before the definition of the Module) feels hacky and ends in threadpool being destroyed, and therefore all following mongo operations fail
I've done using the nestjs-command library like that.
1. Install the library:
https://www.npmjs.com/package/nestjs-command
2. Then I've created a command to seed my userService like:
src/modules/user/seeds/user.seed.ts
import { Command, Positional } from 'nestjs-command';
import { Injectable } from '#nestjs/common';
import { UserService } from '../../../shared/services/user.service';
#Injectable()
export class UserSeed {
constructor(
private readonly userService: UserService,
) { }
#Command({ command: 'create:user', describe: 'create a user', autoExit: true })
async create() {
const user = await this.userService.create({
firstName: 'First name',
lastName: 'Last name',
mobile: 999999999,
email: 'test#test.com',
password: 'foo_b#r',
});
console.log(user);
}
}
3. Add that seed command into your module. I've created a SeedsModule in a shared folder to add more seeds in future
src/shared/seeds.module.ts
import { Module } from '#nestjs/common';
import { CommandModule } from 'nestjs-command';
import { UserSeed } from '../modules/user/seeds/user.seed';
import { SharedModule } from './shared.module';
#Module({
imports: [CommandModule, SharedModule],
providers: [UserSeed],
exports: [UserSeed],
})
export class SeedsModule {}
Btw I'm importing my userService into my SharedModule
4. Add the SeedsModule into your AppModule
On your AppModule usually at src/app.module.ts add the SeedsModule into imports
Final
If you followed the steps in the nestjs-command repo you should be able to run
npx nestjs-command create:user
That will bootstrap a new application and run that command and then seed to your mongo/mongoose
Hope that help others too.
actually you can do it easily with onModuleInit(), here i'm using Mongoose ORM. This all done with zero dependencies, hope it helps
import { Injectable, OnModuleInit } from '#nestjs/common';
import { UserRepository } from './repositories/user.repository';
#Injectable()
export class UserService implements OnModuleInit {
constructor(private readonly userRepository: UserRepository) {}
// onModuleInit() is executed before the app bootstraped
async onModuleInit() {
try {
const res = await this.userRepository.findAll(); // this method returns user data exist in database (if any)
// checks if any user data exist
if (res['data'] == 0) {
const newUser = {
name: 'yourname',
email: 'youremail#gmail.com',
username: 'yourusername',
};
const user = await this.userRepository.create(newUser); // this method creates new user in database
console.log(user);
}
} catch (error) {
throw error;
}
}
// your other methods
}
For my case, I needed to insert seed during the tests, the best I could find is to create a seed service, imported and used only during tests.
Here is my base class using the schema model, all is needed is to extend and pass the model.
// # base.seed.service.ts
import { Model, Document } from 'mongoose';
import { forceArray, toJson } from 'src/utils/code';
export abstract class BaseSeedService<D extends Document> {
constructor(protected entityModel: Model<D>) {}
async insert<T = any>(data: T | T[]): Promise<any[]> {
const docs = await this.entityModel.insertMany(forceArray(data));
return toJson(docs);
}
}
// # utils
const toJson = (arg: any) => JSON.parse(JSON.stringify(arg));
function forceArray<T = any>(instance: T | T[]): T[] {
if (instance instanceof Array) return instance;
return [instance];
}
// # dummy.seed.service.ts
import { Injectable } from '#nestjs/common';
import { InjectModel } from '#nestjs/mongoose';
import { Model } from 'mongoose';
import { DummyDocument } from './dummy.schema';
#Injectable()
export class DummySeedService extends BaseSeedService<DummyDocument> {
constructor(
#InjectModel(Dummy.name)
protected model: Model<DummyDocument>,
) {
super(model);
}
}
Then inside the tests
describe('Dymmy Seeds', () => {
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [DummySeedService],
imports: [
MongooseModule.forRoot(__connect_to_your_mongodb_test_db__),
MongooseModule.forFeature([
{
name: Dummy.name,
schema: DummySchema,
},
]),
],
}).compile();
const seeder = module.get<DummySeedService>(DummySeedService);
const initData = [__seed_data_here__];
const entities: Dummy[] = await seeder.insert(initData);
expect(entities.length > 0).toBeTruthy();
});
});