1

For a project where we have actions and donations. We store the donations in an array in the related action. For the connection we use Mongoose.

The schema for an action is as follows, for readability I've removed some fields which are not related to this problem:

const donationSchema = new Schema(
  {
    id: {
      type: String,
      unique: true,
      required: true,
      index: true,
    },
    amount: { type: Number },
    status: {
      type: String,
      enum: ['pending', 'collected', 'failed'],
      default: 'pending',
    },
  },
  { timestamps: true, versionKey: false, _id: false },
);

const schema = new Schema(
  {
    donations: { type: [donationSchema], default: [] },
    target: { type: Number, default: 0 },
    collected: { type: Number, default: 0 },
  },
  {
    timestamps: true,
    versionKey: false,
  },
);
const Action = model<IAction>('Action', schema);

Let say I have an Action with three donations, one in every state:

{
  "_id": "6098fb22101f22cfcbd31e3b"
  "target": 10000,
  "collected": 25,
  "donations": [
    {
      "uuid": "dd90f6f1-56d7-4d8b-a51f-f9e5382d3cd9",
      "amount": 25,
      "status": "collected"
    },
    {
      "uuid": "eea0ac5e-1e52-4eba-aa1f-c1f4d072a37a",
      "amount": 10,
      "status": "failed"
    },
    {
      "uuid": "215237bd-bfe6-4d5a-934f-90e3ec9d2aa1",
      "amount": 50,
      "status": "pending"
    }
  ]
}

Now I want to update the pending donation to collected.

This would be

Action.findOneAndUpdate(
  {
    _id: '6098fb22101f22cfcbd31e3b',
    'donations.id': '215237bd-bfe6-4d5a-934f-90e3ec9d2aa1',
  },
  {
    $set: {
      'donations.$.status': 'collected',
    },
  },
  {
    upsert: false,
    returnOriginal: false,
  }
).then((action) => console.log(action);

I want to update the status to collected, but also update the collected so that it is the same as all the donations with status equal to collected. I thought of using the $inc operator, but this keeps saying that donations.$.amount is not a number and therefore not able to increment collected.

Is there a way to do this in the same update call? The reason why I cannot get the object and just count collected amount is that maybe two donation callbacks occur at the same time, so we don't want the to overwrite the previous given amount.

1 Answer 1

1

This aggregation can help you I believe:

db.collection.aggregate([
  {
    "$match": {
      _id: "6098fb22101f22cfcbd31e3b"
    }
  },
  {
    "$set": {
      "donations.status": {
        "$reduce": {
          "input": "$donations",
          "initialValue": {
            uuid: "215237bd-bfe6-4d5a-934f-90e3ec9d2aa1"
          },
          "in": {
            $cond: [
              {
                $eq: [
                  "$$this.uuid",
                  "$$value.uuid"
                ]
              },
              "collected",
              "$$this.status"
            ]
          }
        }
      }
    }
  },
  {
    "$set": {
      "collected": {
        "$reduce": {
          "input": "$donations",
          "initialValue": "$collected",
          "in": {
            $cond: [
              {
                $eq: [
                  "$$this.status",
                  "collected"
                ]
              },
              {
                $sum: [
                  "$$value",
                  "$$this.amount"
                ]
              },
              "$$value"
            ]
          }
        }
      }
    }
  }
])

Edit: Above aggregation wasn't properly update status field to "collected" dunno why..

But update query below should work. I couldn't test it too. So, please let me know if something goes wrong.

db.collection.update({
  "_id": "6098fb22101f22cfcbd31e3b"
},
{
  "$set": {
    "donations.$[element].status": "collected",
    "$inc": {
      "donations.$[element].amount": {
        "$cond": [
          {
            "$eq": [
              "donations.$[element].status",
              "collected"
            ]
          },
          "donations.$[element].amount",
          "collected"
        ]
      }
    }
  }
},
{
  "arrayFilters": [
    {
      "element.uuid": "215237bd-bfe6-4d5a-934f-90e3ec9d2aa1"
    }
  ]
})
Sign up to request clarification or add additional context in comments.

7 Comments

This didn't work unfortunately. When I log the result of the aggregation, I do get the object I want, so the $match step works, but the collected stayed on 25 and the state didn't get changed to collected. Another thing was that when I changed some names around, I still didn't get new (not correctly filled) fields to save. To the DB even though they were logged.
Oh, I see. I didn't test though. I'll check this
@Bas I updated my answer. Please let me know if it won't work.
Thanks for the help! This one works beter, the status of the donation is updating correctly, but the collected field on Action is not updating. This stays on 25. After reading more into it, I'm thinking that $set and $inc should be separate properties, but not sure.
What I currently do as temporary solution, is getting the donation object, and then just use {$set: {"donations.$.status": "collected"}, $inc: {'collected': "THE_AMOUNT_HERE" }} as update object. If you know a way to not get the donationObject at first, that'd be great. Also, when I'm thinking more of it, a recount every time might be beter then a increment, seeing that I could send the same call twice.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.