node.jsmongodbexpressmongoosemongoose-schema

Mongoose runValidators: true is giving wrong validate error when findOneAndUpdate() is using


I have a function in express js to update my project like below:

projectController.ts

export async function updateProjectStatus(req: Request, res: Response) {
  try {
    const projectCode = req.params.projectCode;
    const { status } = req.body;

    const updatedProject = await Project.findOneAndUpdate(
      { projectCode },
      { status },
      { new: true }
    );

    if (!updatedProject) {
      return res.status(404).send('Project not found');
    }

    res.status(200).send(updatedProject);
  } catch (error) {
    let errorMessage = 'Failed to update project status';
    if (error instanceof Error) {
      errorMessage = error.message;
    }

    res.status(500).send({ message: errorMessage });
  }
}

// PATCH: update project details

type ProjectKeys = keyof ProjectType;

const allowedUpdates: ProjectKeys[] = [
  'name',
  'budget',
  'advance',
  'clientName',
  'clientPhone',
  'clientEmail',
  'clientAddress',
  'clientDetails',
  'endDate',
  'demoLink',
  'typeOfWeb',
  'description',
];

export async function updateProjectDetails(req: Request, res: Response) {
  const projectCode = req.params.projectCode;
  const {
    name,
    budget,
    advance,
    clientName,
    clientPhone,
    clientEmail,
    clientAddress,
    clientDetails,
    endDate,
    demoLink,
    typeOfWeb,
    description,
  } = req.body;

  console.log(req.body);
  console.log(projectCode);

  // Extract valid updates from request body
  const updates = Object.keys(req.body);

  const isValidOperation = updates.every((update) => {
    return allowedUpdates.includes(update as ProjectKeys);
  });

  if (!isValidOperation) {
    return res.status(400).send({ error: 'Invalid updates!' });
  }

  try {
    const project = await Project.findOne({ projectCode });
    if (!project) {
      return res.status(404).send({ error: 'Project not found' });
    }

    const updatedProject = await Project.findOneAndUpdate(
      { projectCode },
      {
        name,
        budget,
        advance,
        clientName,
        clientPhone,
        clientEmail,
        clientAddress,
        clientDetails,
        endDate,
        demoLink,
        typeOfWeb,
        description,
      },
      { new: true}
    );

    res.status(200).send(updatedProject);
  } catch (error) {
    res.status(500).send({
      message:
        error instanceof Error
          ? error.message
          : 'Failed to update project details',
    });
    console.log(error);
  }
}

here I get the updated project details then find the project and then update it. While updating it I use runValidators: true to run my validator in my project model. I have some validator in my projectModel.ts like below:

import mongoose from 'mongoose';
import { ProjectType } from '../types/projectDocumentType';

const projectSchema = new mongoose.Schema<ProjectType>(
  {
    projectCode: {
      type: String,
      required: true,
      unique: true,
    },
    name: {
      type: String,
      required: true,
      trim: true,
    },
    budget: {
      type: Number,
      required: true,
      validate: {
        validator: function (value: number) {
          return value >= 0;
        },
        message: 'Budget must be a positive number',
      },
    },
    advance: {
      type: Number,
      required: true,
      validate: {
        validator: function (value: number) {
          return value >= 0 && value <= this.budget;
        },
        message:
          'Advance must be a positive number and less than or equal to the budget',
      },
    },
    due: {
      type: Number,

      validate: {
        validator: function (value: number) {
          return value >= 0 && value <= this.budget;
        },
        message:
          'Due must be a positive number and less than or equal to the budget',
      },
    },
    totalPaid: {
      type: Number,
      default: 0,
      validate: {
        validator: function (value: number) {
          return value >= 0 && value <= this.budget;
        },
        message:
          'Total Paid must be a positive number and less than or equal to the budget',
      },
    },
    clientName: {
      type: String,
      required: true,
      ref: 'Client',
    },
    clientPhone: {
      type: String,
      required: true,
    },
    clientEmail: {
      type: String,
      required: true,
    },
    clientAddress: {
      type: String,
    },
    clientDetails: {
      type: String,
    },
    startDate: {
      type: String,
      required: true,
      default: new Date().toISOString().split('T')[0],
    },

    endDate: {
      type: String,
      required: true,
    },
    demoLink: {
      type: String,
    },
    typeOfWeb: {
      type: String,
    },
    description: {
      type: String,
    },
    status: {
      type: Boolean,
      required: true,
      default: false,
    },

    verifiedClientList: [
      {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'Client',
      },
    ],
    projectManager: {
      type: mongoose.Schema.Types.ObjectId,
      ref: 'User',
      required: true,
    },
    paymentList: [
      {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'Payment',
      },
    ],
  },
  {
    timestamps: true,
  }
);

projectSchema.pre('save', function (next) {
  if (this.isNew) {
    // Only calculate due when creating a new project
    this.due = this.budget - this.advance;
  }
  next();
});

const Project = mongoose.model<ProjectType>('Project', projectSchema);

export default Project;

in projectMode.ts in advance and due section I have validator function. It is working perfectly whenever I try to add new project. But when I try to update the project using postman it gives me validation error though my json is right. For example I send patch request for json as:

{
  "name": "Project Alpha",
  "budget": 10000,
  "advance": 900,
  "clientName": "Rezaul",
  "clientPhone": "1234567890",
  "clientEmail": "john@example.com",
  "clientAddress": "123 Main St",
  "clientDetails": "Long-term client",

  "endDate": "2024-06-01",
  "demoLink": "http://example.com/demo",
  "typeOfWeb": "E-commerce",
  "description": "A high-end e-commerce platform"
 
}

but when I send it it shows me Error: Validation failed: advance: Advance must be a positive number and less than or equal to the budget but here advance (900) < budget (10000). so why my validator is not working with runValidators: true and how to fix that without using save()

I am expecting when I use

{
  "name": "Project Alpha",
  "budget": 10000,
  "advance": 900,
  "clientName": "Rezaul",
  "clientPhone": "1234567890",
  "clientEmail": "john@example.com",
  "clientAddress": "123 Main St",
  "clientDetails": "Long-term client",

  "endDate": "2024-06-01",
  "demoLink": "http://example.com/demo",
  "typeOfWeb": "E-commerce",
  "description": "A high-end e-commerce platform"
 
}

it will update the project successfully by checking validator.


Solution

  • I solve the problem by using Post Schema Definition Validation or Path-based validation using path().validate():. for example, for advance in projectModel.ts I use:

    projectSchema.path('advance').validate(function (value: number) {
      console.log('this.budget', this.get('budget'));
    
      return value >= 0 && value <= this.get('budget');
    }, 'Advance must be a positive number and less than or equal to the budget');
    

    here I get the updated budget value by using this.get('budget'). This gives me the current budget value. As per mongoose official documentation, When running in validate() or validateSync(), the validator can access the document using this. When running with update validators, this is the Query, not the document being updated! Queries have a get() method that lets you get the updated value. Link So I use this.get() to get the updated value inside validation before update the document which I was passing in findOneAndUpdate() method of mongoose.

     const updatedProject = await Project.findOneAndUpdate(
          { projectCode },
          {
            name,
            budget,
            advance,
            clientName,
            clientPhone,
            clientEmail,
            clientAddress,
            clientDetails,
            endDate,
            demoLink,
            typeOfWeb,
            description,
          },
          { new: true, runValidators: true }
        );
    

    This validation is now working for both save() method and findOneAndUpdate() in mongoose. Make sure you remove the schema-based validation if you use Path-based validation. I mean I also remove the validate and validator function in advance section.

    validate: {
            validator: function (value: number) {
              return value >= 0 && value <= this.budget;
            },
            message:
              'Advance must be a positive number and less than or equal to the budget',
          },
    

    and also use runValidators: true inside findOneAndUpdate() as an option.