Ansjovis86
Ansjovis86

Reputation: 1545

Mongoose: Upsert into array of objects and take $max for 1 key

I have a complex upsert task to perform in Mongoose next to some other updates. My model looks like this:

const UserSchema = new Schema({
  username: { type: String, index: true, unique: true },
  email: String,
  password: { type: String, select: false },
  level: { type: Number, default: 0 },
  highscores: [{
    id: { type: Number, index: true },
    stars: { type: Number },
    // add a score also 
  }],
});

And my current database update is like this:

module.exports.addRecord = (newRecord, callback) => {
  const query = { username: newRecord.username };
  const update = {
    $max: { level: newRecord.data.ex },
    //update highscore here
  };
  const options = { safe: true, new: true };
  User.findOneAndUpdate(query, update, options, callback);
};

I now want to upsert an object into the highscores array. So 2 situations:

  1. If an object doesn't have the id yet, then insert it.
  2. If id already exists, then update stars with the maximum between the value in the database and the incoming value in newRecord.highscores.stars.

I know how to upset and I know how to take the max, but I don't know how to combine when I need to perform I guess a $push to the highscores array. How do I do this?


Update: I managed to get it working in mongoclient, like this:

db.users.update({username: 'test', "highscores.ex": { $nin: [ 3 ]}}, {$addToSet: {  "highscores": { "ex":5, "stars": 0}}})
db.users.update({username: 'test', highscores: {$elemMatch: {ex: {$in: [ 5 ] }}}}, { $max: {"highscores.$.stars":12 } });

But when I want to apply it in my model function, it will somehow not execute these operations. I'm not getting any errors.

User.update({ username: newRecord.username, 'highscores.ex': { $nin: [newRecord.highscore.ex] } }, { $addToSet: { highscores: { ex: newRecord.highscore.ex, stars: 0 } } });
User.update({ username: newRecord.username, highscores: { $elemMatch: { ex: { $in: [newRecord.highscore.ex] } } } }, { $max: { 'highscores.$.stars': newRecord.highscore.stars } });

I've noticed that when I give a callback to the first update line it will execute. But I can't give them all the callback, otherwise it will crash. What's going wrong?

Upvotes: 1

Views: 679

Answers (2)

Ansjovis86
Ansjovis86

Reputation: 1545

This works too:

User.update({ username: newRecord.username, 'highscores.ex': { $nin: [newRecord.highscore.ex] } }, { $addToSet: { highscores: { ex: newRecord.highscore.ex, stars: 0 } } }, () => {
  User.update({ username: newRecord.username, highscores: { $elemMatch: { ex: { $in: [newRecord.highscore.ex] } } } }, { $max: { 'highscores.$.stars': newRecord.highscore.stars } });
  });
});

Upvotes: 0

dnickless
dnickless

Reputation: 10918

You cannot access existing values of your document as part of an update just yet.

You can, however achieve what you want by retrieving the matching documents from the database first, then patching them and sending them back to the server.

However, I would advise against that and suggest go for the following approach instead which involves two queries.

Let's assume you have the following document in your database:

{
    "_id" : ObjectId("59adc2aef86587449164b66a"),
    "highscores" : [ 
        {
            "id" : 1,
            "level" : 100
        }
    ]
}

Query #1 inserts the missing ids:

User.update({
    // add any other required filters here like e.g. "username": newRecord.username
    "highscores.id": { // find records that do not have an id 2 in their highscores array
        $nin: [ 2 ]
    }
}, {
    $addToSet: { // add new item to list of highscores
        "highscores": {
            "id":2,
            "level": 200
            // set any other required fields here like e.g. "stars"
        }
    }
})

Query #2 will update all records that have a matching id in their highscores array if their current level is less than the new one.

User.update({
    // add any other required filters here like e.g. "username": newRecord.username
    "highscores": {
        $elemMatch: {
           "id": {
                $in: [ 2 ] // find matching array elements
            },
            "level": {
                $lt: 200 // that have a lower "level" value than your new level
            }
        }
    }
}, {
    $set: {
        "highscores.$.level": 200 // use your new "level" value here, the same as in the above $lt filter
    }
    // add set operations for any other required fields here like e.g. "stars"
})

Upvotes: 1

Related Questions