1

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?

Ansjovis86
  • 1,506
  • 5
  • 17
  • 48

2 Answers2

1

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"
})
dnickless
  • 10,733
  • 1
  • 19
  • 34
  • For query 1 I need a query that finds a user by username and only then check whether there's an object in the highscores array that has a certain id. – Ansjovis86 Sep 04 '17 at 23:11
  • Yes, of course, and there's nothing stopping you from adding any number of additionally required filters. Also, you might need to set additional fields. I simply didn't want to clutter my example with basics that are not related to your core problem. I updated my answer, though, and added a comment in the code so it's a bit clearer. – dnickless Sep 05 '17 at 04:51
  • Could you look at my update, I encountered some problems. – Ansjovis86 Sep 05 '17 at 10:01
  • Read this: http://mongoosejs.com/docs/queries.html It's important to understand the difference between a call with a callback and without one. Actually, this link here might help understanding that topic even better: https://stackoverflow.com/questions/31549857/mongoose-what-does-the-exec-function-do – dnickless Sep 05 '17 at 10:29
  • Ok, I've changed your answer a bit. I'll add that answer to this question, but I'll accept yours as it also works and got me to my final answer. Thanks so much. – Ansjovis86 Sep 05 '17 at 10:47
0

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 } });
  });
});
Ansjovis86
  • 1,506
  • 5
  • 17
  • 48