0

Desired Behaviour

I am trying to update the position property of each object in an array of objects when one of their positions changes. For example, object with position 5 is moved to position 0 and therefore other objects' positions must also change.

I haven't been able to find an "array iterator" for MongoDB so have been trying to approach the problem from other angles but I think I am over-complicating it.

Schema

statements: [
{
    "position": 0,
    "id": "a"
},
{
    "position": 1,
    "id": "t"
},
{
    "position": 2,
    "id": "z"
},
{
    "position": 3,
    "id": "q"
},
{
    "position": 4,
    "id": "l"
},
{
    "position": 5,
    "id": "b"
}
]

Frontend Scenario

Figure A: Starting state of ordered divs
enter image description here

Figure B: Changed state of ordered divs (statement5has been moved to position0)
enter image description here

What I've Tried

Pseudo Code

I think the logic can be expressed as:

The objects position will change from the old_position to the new_position by an arbitrary amount of places.

If the reposition_direction is backwards, all objects with position greater than or equal to the new_position should increment (except for the object being moved which should be assigned the new_position)

If the reposition_direction is forwards, all objects with position less than or equal to the new_position should decrement (except for the object being moved which should be assigned the new_position)

(update: this logic is incorrect, working logic added to answer)

This is what I have so far:

if (reposition_direction === "backwards") {

    var new_filter = { _id: o_id, "statements.position": { $gte: new_position } };

    var new_update = { $inc: { "statements.$[elem].position": 1 } };

    var array_filters = { "arrayFilters": [{ "elem.position": { $gte: new_position } }], "multi": true };

} else if (reposition_direction === "forwards") {

    var new_filter = { _id: o_id, "statements.position": { $lte: new_position } };

    var new_update = { $inc: { "statements.$[elem].position": -1 } };

    var array_filters = { "arrayFilters": [{ "elem.position": { $lte: new_position } }], "multi": true };
}

collection.updateOne(new_filter, new_update, array_filters, function(err, doc) {
    if (err) {
        res.send(err);
    } else {
        res.json({ response: "hurrah" });
    }
});
user1063287
  • 10,265
  • 25
  • 122
  • 218
  • 1
    Why hold onto the position as a separate "position" property on the objects themselves. Their position is known because they are in an array. Anytime you need the position you are iterating over the array anyway so you know it from your index into the array. – bhspencer Sep 11 '18 at 13:59
  • One year later, i realise the simpler solution was `$pull` object from array, then `$push` it to desired index using `$each` and `$position`: https://stackoverflow.com/a/36084944 – user1063287 Jul 04 '19 at 03:46

2 Answers2

0

Each time you change your array set the position on each of its members:

for (var i = 0; i < statements.length; i++) {
    statements[i].position = i;
}
bhspencer
  • 13,086
  • 5
  • 35
  • 44
  • yes, that is how it can be achieved on the frontend, but not in mongodb. – user1063287 Sep 11 '18 at 14:06
  • To change the data in mongodb first get the entire object out of mongodb, change the object with the above code and then put it back in mongodb. – bhspencer Sep 11 '18 at 14:07
  • Or are you saying that each statement is stored in mongodb as a separate mongodb object? If that it the case you could either retrieve all of the assiocated statements out of mongo, assign position with the above code and then store them all back in mongo. However you would be better just storing the array in mongo. – bhspencer Sep 11 '18 at 14:09
  • Either way I certainly wouldn't do what you are attempting with collection.updateOne and a set of incomprehensible filters. – bhspencer Sep 11 '18 at 14:11
  • yes, i wondered if that would need to be the approach (ie, get the whole array of objects, iterate over them and perform the updates, then `$set` the whole array value). the array can be quite large however, with many objects, themselves with nested arrays, so it seemed like a bit of a "large" operation to do. – user1063287 Sep 11 '18 at 14:14
  • 1
    How large? If it is less than 1000 objects in the array I wouldn't give it a second thought. Just do the get modify set. – bhspencer Sep 11 '18 at 14:17
0

It was suggested the better idea was just to get the array, iterate over the result, and then set the value of the whole array, but I was intrigued to see if i could get the sorting logic right, and i think the following works. Just pasting below for reference.

var new_position = Number(request_body.new_position);
var old_position = Number(request_body.old_position);
var reposition_direction = request_body.reposition_direction;
var statement_id = request_body.statement_id;
var filter = { _id: o_id };

if (reposition_direction === "backwards") {

    // if backwards, position filter is $gte new_position AND $lt old_position
    var new_filter = { _id: o_id, "statements.position": { $gte: new_position, $lt: old_position } };

    // increment
    var new_update = { $inc: { "statements.$[elem].position": 1 } };

    var array_filters = { "arrayFilters": [{ "elem.position": { $gte: new_position, $lt: old_position } }], "multi": true };

} else if (reposition_direction === "forwards") {

    // if forwards, position filter is $lte new_position AND $gt old_position
    var new_filter = { _id: o_id, "statements.position": { $lte: new_position, $gt: old_position } };

    // decrement
    var new_update = { $inc: { "statements.$[elem].position": -1 } };

    var array_filters = { "arrayFilters": [{ "elem.position": { $lte: new_position, $gt: old_position } }], "multi": true };

}

collection.updateOne(new_filter, new_update, array_filters, function(err, doc) {
    if (err) {
        res.send(err);
    } else {

        // set the moved objects property to the new value
        var new_filter = { _id: o_id, "statements.id": statement_id };
        var new_update = { $set: { "statements.$.position": new_position } };

        collection.findOneAndUpdate(new_filter, new_update, function(err, doc) {
            if (err) {
                res.send(err);
            } else {
                res.json({ response: "hurrah" });
            }
        });
    }
});

The success handler call to findOneAndUpdate() was inspired by this answer:

https://stackoverflow.com/a/30387038

I tested the following combinations and they seem to work:

move 1 to 4
move 4 to 1

move 0 to 5
move 5 to 0

move 1 to 2
move 2 to 1

move 0 to 1
move 1 to 0

move 4 to 5
move 5 to 4

user1063287
  • 10,265
  • 25
  • 122
  • 218