Home > Software design >  MongoDB transaction with @NestJs/mongoose not working
MongoDB transaction with @NestJs/mongoose not working

Time:04-18

I really need your help. My MongoDB transaction with @NestJs/mongoose not working...When My stripe payment fails rollback is not working... Still, my order collection saved the data...How can I fix this issue..?

  async create(orderData: CreateOrderServiceDto): Promise<any> {
    const session = await this.connection.startSession();
    session.startTransaction();
    try {
      const createOrder = new this.orderModel(orderData);
      const order = await createOrder.save();

      await this.stripeService.charge(
        orderData.amount,
        orderData.paymentMethodId,
        orderData.stripeCustomerId,
      );
      await session.commitTransaction();
      return order;
    } catch (error) {
      await session.abortTransaction();
      throw error;
    } finally {
      await session.endSession();
    }
  }

CodePudding user response:

I had the same issue and i found that on github: Mongo DB Transactions With Mongoose & Nestjs

So I think, according this issue, you have to call the create method of your model, like that:

const order = await this.orderModel.create(orderData, { session });

as you can see, the Model.create method has an overload with SaveOptions as parameter:

create(docs: (AnyKeys<T> | AnyObject)[], options?: SaveOptions): Promise<HydratedDocument<T, TMethodsAndOverrides, TVirtuals>[]>;

it takes an optional SaveOptions parameter that can contain the session:

interface SaveOptions {
  checkKeys?: boolean;
  j?: boolean;
  safe?: boolean | WriteConcern;
  session?: ClientSession | null;
  timestamps?: boolean;
  validateBeforeSave?: boolean;
  validateModifiedOnly?: boolean;
  w?: number | string;
  wtimeout?: number;
}

Please note that Model.save() can also take a SaveOptions parameter. So you can also do as you did like that:

const createOrder = new this.orderModel(orderData);
const order = await createOrder.save({ session });

A little further...

As i do many things that require a transaction, I came up with this helper to avoid many code duplication:

import { InternalServerErrorException } from "@nestjs/common"
import { Connection, ClientSession } from "mongoose"

export const mongooseTransactionHandler = async <T = any>(
  method: (session: ClientSession) => Promise<T>,
  one rror: (error: any) => any,
  connection: Connection, session?: ClientSession
): Promise<T> => {
  const isSessionFurnished = session === undefined ? false : true
  if (isSessionFurnished === false) {
    session = await connection.startSession()
    session.startTransaction()
  }

  let error
  let result: T
  try {
    result = await method(session)

    if (isSessionFurnished === false) {
      await session.commitTransaction()
    }
  } catch (err) {
    error = err
    if (isSessionFurnished === false) {
      await session.abortTransaction()
    }
  } finally {
    if (isSessionFurnished === false) {
      await session.endSession()
    }

    if (error) {
      one rror(error)
    }

    return result
  }
}

Details

the optional parameter session is in case you are doing nested nested transaction. that's why i check if the session is provided. If it is, it means we are in a nested transaction. So we'll let the main transaction commit, abord and end the session.

Example

for example: you delete a User model, and then the user's avatar which is a File model.

/** UserService **/
async deleteById(id: string): Promise<void> {
  const transactionHandlerMethod = async (session: ClientSession): Promise<void> => {
    const user = await this.userModel.findOneAndDelete(id, { session })
    await this.fileService.deleteById(user.avatar._id.toString(), session)
  }

  const one rror = (error: any) => {
    throw error
  }

  await mongooseTransactionHandler<void>(
    transactionHandlerMethod,
    one rror,
    this.connection
  )
}

/** FileService **/
async deleteById(id: string, session?: ClientSession): Promise<void> {
  const transactionHandlerMethod = async (session: ClientSession): Promise<void> => {
    await this.fileModel.findOneAndRemove(id, { session })
  }

  const one rror = (error: any) => {
    throw error
  }

  await mongooseTransactionHandler<void>(
    transactionHandlerMethod,
    one rror,
    this.connection,
    session
  )
}

So, in short:

You can use it like this:

async create(orderData: CreateOrderServiceDto): Promise<any> {
  const transactionHandlerMethod = async (session: ClientSession): Promise<Order> => {
    const createOrder = new this.orderModel(orderData);
    const order = await createOrder.save({ session });

    await this.stripeService.charge(
      orderData.amount,
      orderData.paymentMethodId,
      orderData.stripeCustomerId,
    );

    return order
  }

  const one rror = (error: any): void => {
    throw error
  }

  const order = await mongooseTransactionHandler<Order>(
    transactionHandlerMethod,
    one rror,
    this.connection
  )

  return order
}

Hope it'll help.

  • Related