1

I have a Mongoose model like this:

const centerSchema = mongoose.Schema({
  centerName: {
    type: String,
    required: true,
  },
  candidates: [
    {
      candidateName: String,
      voteReceived: {
        type: Number,
        default: 0,
      },
      candidateQR: {
        type: String,
        default: null,
      },
    },
  ],
  totalVote: {
    type: Number,
    default: 0,
  },
  centerQR: String,
});

I have a Node.JS controller function like this:

exports.createCenter = async (req, res, next) => {
  const newCenter = await Center.create(req.body);

  newCenter.candidates.forEach(async (candidate, i) => {
    const candidateQRGen = await promisify(qrCode.toDataURL)(
      candidate._id.toString()
    );
    candidate.candidateQR = candidateQRGen;

    // ** Tried these: **
    // newCenter.markModified("candidates." + i);
    // candidate.markModified("candidateQR");
  });

  // * Also tried this *
  // newCenter.markModified("candidates");
  const upDatedCenter = await newCenter.save();
  res.status(201).json(upDatedCenter);
};

Simply, I want to modify the candidateQR field on the subdocument. The result should be like this:

{
    "centerName": "Omuk Center",
    "candidates": [
        {
            "candidateName": "A",
            "voteReceived": 0,
            "candidateQR": "some random qr code text",
            "_id": "624433fc5bd40f70a4fda276"
        },
        {
            "candidateName": "B",
            "voteReceived": 0,
            "candidateQR": "some random qr code text",
            "_id": "624433fc5bd40f70a4fda277"
        },
        {
            "candidateName": "C",
            "voteReceived": 0,
            "candidateQR": "some random qr code text",
            "_id": "624433fc5bd40f70a4fda278"
        }
    ],
    "totalVote": 0,
    "_id": "624433fc5bd40f70a4fda275",
    "__v": 1,
}

But I am getting the candidateQR still as null in the Database. I tried markModified() method. But that didn't help (showed in the comment section in the code above). I didn't get any error message. In response I get the expected result. But that result is not being saved on the database. I just want candidateQR field to be changed. But couldn't figure out how.

Sagor Mahtab
  • 147
  • 3
  • 11

4 Answers4

2

forEach loop was the culprit here. After replacing the forEach with for...of it solved the issue. Basically, forEach takes a callback function which is marked as async in the codebase which returns a Promise initially and gets executed later.

As for...of doesn't take any callback function so the await inside of it falls under the controller function's scope and gets executed immediately. Thanks to Indraraj26 for pointing this out. So, the final working version of the controller would be like this:

exports.createCenter = async (req, res, next) => {
  const newCenter = await Center.create(req.body);

  for(const candidate of newCenter.candidates) {
    const candidateQRGen = await promisify(qrCode.toDataURL)(
      candidate._id.toString()
    );
    candidate.candidateQR = candidateQRGen;
  };

  newCenter.markModified("candidates");
  const upDatedCenter = await newCenter.save();
  res.status(201).json(upDatedCenter);
};

Also, shoutout to Moniruzzaman Dipto for showing a different approach to solve the issue using async.eachSeries() method.

Sagor Mahtab
  • 147
  • 3
  • 11
1

You can use this before save()

newCenter.markModified('candidates');
Indraraj26
  • 1,726
  • 1
  • 14
  • 29
  • I already tried this one. Please check the question. I highlighted that part in the code while still mentioning it in the last paragraph of my question. – Sagor Mahtab Mar 31 '22 at 03:44
  • 1
    Marking the callback function passed to `forEach()` as async only makes that callback return a promise, it doesn't make `forEach()` run asynchronously. it is not waiting for your other changes. you can add console log after the forEach that will run before the `forEach` execution use `for...of` https://stackoverflow.com/a/69901794/10842900 – Indraraj26 Mar 31 '22 at 04:41
  • Wow. So that was the case. I console logged the result earlier and noticed that anomality but didn't go that far to think about forEach's callback. That was really insightful. Worked like a charm after using for...of. – Sagor Mahtab Mar 31 '22 at 07:27
1

You can use eachSeries instead of the forEach loop.

const async = require("async");

exports.createCenter = async (req, res, next) => {
  const newCenter = await Center.create(req.body);

  async.eachSeries(newCenter.candidates, async (candidate, done) => {
    const candidateQRGen = await promisify(qrCode.toDataURL)(
      candidate._id.toString(),
    );
    candidate.candidateQR = candidateQRGen;
    newCenter.markModified("candidates");
    await newCenter.save(done);
  });

  res.status(201).json(newCenter);
};
0

As far as I understand, you are just looping through the candidates array but you are not storing the updated array. You need to store the updated data in a variable as well. Please give it a try with the solution below using map.

exports.createCenter = async (req, res, next) => {
  const newCenter = await Center.create(req.body);

  let candidates = newCenter.candidates;
  
  candidates = candidates.map(candidate => {
      const candidateQRGen = await promisify(qrCode.toDataURL)(
          candidate._id.toString()
      );

      return {
          ...candidate,
          candidateQR: candidateQRGen  
      }
  });

  newCenter.candidates = candidates;

  const upDatedCenter = await newCenter.save();

  res.status(201).json(upDatedCenter);
};
Zak
  • 860
  • 16
  • 39