What causes Mongoose `updateMany` to return `{ acknowledged: false }`
Asked Answered
N

4

8

I'm trying to set up notifications for an Express app using MongoDB.

I have an API endpoint where I $push a user's id to the readBy field in MongoDB to "mark" it as read after retrieving a user's notifications. When I make a request to this endpoint, it returns 200 and their notifications, but it isn't making any updates to the notification document in MongoDB. console.loging the query response in the callback gave me { acknowledged: false }. According to the Mongoose docs, acknowledged is a Boolean indicating everything went smoothly, but information about what acknowledged is and at which point in the query/write process caused it to occur was sparse. Since it didn't return any errors I couldn't find a way to troubleshoot it.

Would someone be able to shed some light on what exactly acknowledged: false is and in what typically causes it, and why it doesn't throw an error.

Model:

const notificationSchema = new Schema({
  timestamp: {
    type: Date,
    required: true
  },
  type: {
    type: String,
    required: true,
    enum: [
      'newCustomer',
      'contractSigned',
      'invoicePaid',
      'warrantyExp',
      'assignedProject'
    ]
  },
  recipients: [{
    type: Schema.Types.ObjectId,
    ref: 'Employee',
    required: true,
  }],
  customer: {
    type: Schema.Types.ObjectId,
    ref: 'Customer',
    required: true,
  },
  readBy: [{
    type: String
  }],
  uuid: {
    type: String,
    default: uuid.v4,
    immutable: true,
    required: true,
  },
  company: {
    type: Schema.Types.ObjectId, ref: 'Company'
  }
});

Route:

router.get("/notification/all", withAuth, async (req, res) => {
  const FOURTEEN_DAYS = new Date().setDate(new Date().getDate() + 14);
  try {
    const { uuid, userId } = req.loggedInUser;

    // Fetch notifications that have the user as a recipient.
    Notification.find({
      recipients: userId,
    })
      .populate("customer")
      .exec((err, notifs) => {
        if (err)
          return res.status(500).json({
            success: false,
            message: "Error: Failed to retrieve notifications.",
          });

        const result = [];
        const notifIds = [];

        for (const notif of notifs) {
          // Filter notif
          result.push({
            timestamp: notif.timestamp,
            customer: notif.customer,
            type: notif.type,
            read: notif.readBy.includes(uuid),
          });
          // Add the user as read
          notifIds.push(notif.uuid);
        }

        console.log(notifIds);

        /* THIS RETURNS ACKNOWLEDGED: FALSE */         
        // Write to DB that user has read these notifications
        Notification.updateMany(
          { uuid: { $in: notifIds } },
          { $push: { readBy: uuid } },
          (err, resultUpdate) => {
            if (err)
              return res.status(500).json({
                success: false,
                message:
                  "Error: Failed to add check off notifications as read.",
              });

            console.log(resultUpdate);

            // Delete notifications past 14 days and has been read by all recipients
            Notification.deleteMany(
              {
                timestamp: { $gte: FOURTEEN_DAYS },
                $expr: {
                  $eq: [{ $size: "$readBy" }, { $size: "$recipients" }],
                },
              },
              (err) => {
                if (err)
                  return res.status(500).json({
                    success: false,
                    message: "Error: Failed to delete old notifications.",
                  });

                return res.status(200).json({
                  success: true,
                  notifications: result,
                  message: "Fetched notifications",
                });
              }
            );
          }
        );
      });
  } catch (err) {
    res.status(500).json({ success: false, message: err.toString() });
  }
});
Natividad answered 1/12, 2021 at 22:43 Comment(0)
N
18

So it turns out that this issue was unrelated to write concern. acknowledged: false was being returned because the value we were trying to $push was undefined. So essentially Mongoose was refusing to write undefined values, but doesn't throw an error for the input value being undefined. Putting this here in case someone else runs into this issue.

Natividad answered 3/12, 2021 at 18:1 Comment(1)
thank you so much this was exactly my issue. turns out i forgot to 'await' my promise xDHardball
E
3

From the docs:

The method returns a document that contains:

  • A boolean acknowledged as true if the operation ran with write concern or false if write concern was disabled
  • matchedCount containing the number of matched documents
  • modifiedCount containing the number of modified documents
  • upsertedId containing the _id for the upserted document
Eward answered 2/12, 2021 at 0:2 Comment(5)
So if I'm getting this right, acknowledged being false means that it failed to write to the database, right? Why does MongoDB use this to communicate an error rather than raising an exception like for other MongoErrors? Do you happen to know any good practices for troubleshooting cases where acknowledged is false?Natividad
No, it does not indicate anything about the success or failure of the operation. Check out write concern in the mongodb docs, and the writeConcern option in mongoose. If you didn't request a write concern of w:1 or higher, acknowledged will be false.Eward
Thanks for the follow-up @Joe. writeConcern is totally new for me so I'm sorry about all these questions. After reading through it, am I right in that the write transaction (updateMany) needs to be acknowledged at the transactional level in the options? I'm wondering why I've never encountered this on my other project or in any of the other endpoints. Does this have to do with the fact that the default w in the MongoDB 5.0 is w: 'majority' rather than w: 1?Natividad
unacknowledged would mean w: 0, i.e. instructs the server not to acknowledge the operation, which would make it really fast but impossible to handle failure.Eward
@DerekKim From what I've read, I believe 'majority' is fine, the issues we had were both unrelated to writing, so that was definitely not the issue in our cases.Undersize
U
2

I had the same issue, after many hours of head-scratching, it turns out that if the field you are setting is not defined in the Model's schema, it will give this error with 0 explanation. I still can't seem to understand why no error is thrown or no error message is given. I just hope that the next person who gets this problem sees this answer

Undersize answered 12/12, 2023 at 8:43 Comment(0)
D
0

By default MongoDB documents are schema-less, however mongoose is schema oriented, which means mongoose can do 'stuff' on defined fields only (except for getter queries)

import mongoose from 'mongoose';

await mongoose.connect('mongodb://127.0.0.1:27017/people')
  console.log('DB connection estabilished');

const personSchema = new mongoose.Schema({   // no 'sex' field defined
  name: String,           
  age: Number,
})

const Person = new mongoose.model('friends', personSchema);


await Person.updateMany({ sex: { $exists: 1 } }, { $set: { sex: 'N' } })  
  // -> { acknowledged: false }

IMO this is an expected behavior (with unssufficient error handling)

Dzerzhinsk answered 13/7 at 17:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.