2

I am trying to get an array of objects with a certain ID from my database inside a route of my server, and then add change a property of an object inside the array (instead of returning an objectID to my client I want to return the Document as an object with the specific ID).

This is my code:

        let orders = await Order.find({restaurant: restaurantID, status: 'PROCESSING'})

        for(let order of orders){   //iterate through all orders
            for(let element of order.dishes){     //iterate through order.dishes array (called 'element' since the array contains objects)
                let dish = await Dish.findOne({_id: element._id})
                element['dish'] = dish  //create new property for the dish object
                delete element._id      //remove the ID property since it already exists inside the element.dish object
            }
        }

Every order object inside orders contains an array called dishes, which contains objects with the properties amount and an id. Since my frontend can't do much with the ID, I want to delete the id property and add a new property called dish, which contains the dish object the id points to.

The problem I am facing is that I don't know how to manipulate the orders arrays content. Whenever I parse orders to JSON and sent it inside my response, I recieve a JSON array of order objects just like I want to. But when I add the code I pasted above, it does not change anything inside my orders.

Whenever I log the element inside my for loops it looks like this: EmbeddedDocument {__parentArray: Proxy, __index: 0, $__parent: model, $__: InternalCache, $isNew: false, …} but when I parse it to JSON I recieve exactely what I want, namely this: {"amount":1,"_id":"6183b84fec1c3e109a2271be"}

Is orders even an array in this case? If not, what would be the easiest way to manipulate it/get the documents as an array?

This is what orders looks like when I watch it in my debug window:

price (get):ƒ () {\n        return this[getSymbol].call(this.$__[scopeSymbol] || this, path);\n      }
price (set):ƒ (v) {\n        this.$set.call(this.$__[scopeSymbol] || this, path, v);\n      }
restaurant (get):ƒ () {\n        return this[getSymbol].call(this.$__[scopeSymbol] || this, path);\n      }
restaurant (set):ƒ (v) {\n        this.$set.call(this.$__[scopeSymbol] || this, path, v);\n      }
status (get):ƒ () {\n        return this[getSymbol].call(this.$__[scopeSymbol] || this, path);\n      }
status (set):ƒ (v) {\n        this.$set.call(this.$__[scopeSymbol] || this, path, v);\n      }
timestamp (get):ƒ () {\n        return this[getSymbol].call(this.$__[scopeSymbol] || this, path);\n      }
timestamp (set):ƒ (v) {\n        this.$set.call(this.$__[scopeSymbol] || this, path, v);\n      }
user (get):ƒ () {\n        return this[getSymbol].call(this.$__[scopeSymbol] || this, path);\n      }
user (set):ƒ (v) {\n        this.$set.call(this.$__[scopeSymbol] || this, path, v);\n      }
__v (get):ƒ () {\n        return this[getSymbol].call(this.$__[scopeSymbol] || this, path);\n      }
__v (set):ƒ (v) {\n        this.$set.call(this.$__[scopeSymbol] || this, path, v);\n      }
__proto__:Model
length:1

And in postman, the res.body looks like this (almost exactely like it should but instead of the dishes object it contains only the id):

[
    {
        "_id": "6183b84fec1c3e109a2271bd",
        "user": "6166bc426181646198fc483c",
        "restaurant": "6176947ce8b10986b018930e",
        "dishes": [
            {
                "amount": 1,
                "_id": "6183b84fec1c3e109a2271be"
            },
            {
                "amount": 2,
                "_id": "6183b84fec1c3e109a2271bf"
            }
        ],
        "price": 30,
        "status": "PROCESSING",
        "timestamp": "2021-11-04T10:39:11.800Z",
        "__v": 0
    }
]

If this was already solved, please send me a link to the question/answer. I spent the last few hours trying to solve this and I already looked at a lot of questions but none of the answers were helpful for me

  • What you want to do is a usual requirement and therefore supported natively. Provided that your order model is set up so that mongodb knows that `dishes` is an array of `Dish` document references, you can automatically let mongodb `.populate()` the dish array. See [here](https://stackoverflow.com/a/42976803/5734311) for a basic example. –  Nov 04 '21 at 13:33
  • Relevant docs: https://mongoosejs.com/docs/schematypes.html#arrays and https://mongoosejs.com/docs/populate.html –  Nov 04 '21 at 13:40
  • Okay, thank you. I did not know about this. If you want to, write an answer and I will mark it as solved – WantsToCodeEverthingInDotNet Nov 04 '21 at 13:42
  • If a question turns out to be answered by the docs it's no longer a useful addition to stackoverflow. Which means we aren't supposed to post answers, and the question will be marked for removal. (Note that if you are having trouble populating your query result that's perfectly fine to ask about here, so if that's the case, absolutely do edit your question accordingly!) –  Nov 04 '21 at 13:55
  • Okay, thank you. In this case, I apologise for this question, I did not know that populate() existed and I could not find anyone mentioning it when I looked for other SO posts on this subject – WantsToCodeEverthingInDotNet Nov 04 '21 at 14:11
  • It's fine, don't worry about it :) Make sure to check the docs though (i.e. read them from top to bottom). –  Nov 04 '21 at 14:12

1 Answers1

1

It is a common question. People are often disconcerted because they can't alter the data coming from MongoDB.

The thing is, Mongoose is kind of weird; it does not return simple JSON, but constructs and returns a collection of Mongoose objects. These objects have extra methods, like .save(), but above all their nested objects are immutable. Simply changing their properties have no effect.

In order to modify the data returned by Mongoose, you have two possibilities :

  1. Use .markModified()

It's a really weird concept, but the changes you make won't be applied until you mark them as modified. For instance :

element['dish'] = dish;
order.save();

This will have no effect. The name will remain unchanged in Mongo. You need .markModified() :

element['dish'] = dish;
order.markModified("dishes");
order.save(); // Works now :|
  1. Tell Mongoose to return simple JSON

You can prevent Mongoose from constructing its own objects by using .lean(). this will return simple JSON data that can be tampered with at will.

let orders = await Order
           .find({restaurant: restaurantID, status: 'PROCESSING'})
           .lean()
           .exec(); // Also add .exec(), it returns a true Promise and not a thenable 

Bonus : lean() is faster and lighter.

Jeremy Thille
  • 26,047
  • 12
  • 43
  • 63
  • I don't think this is what OP is actually looking for. OP is trying to change the returned data but only because they didn't know about populate(). What OP is trying to do is much simpler than assumed and doesn't require any of the methods outlined in your answer. –  Nov 04 '21 at 13:53
  • yeah, you are correct. I tried googling it but populate() was never mentioned in any answers – WantsToCodeEverthingInDotNet Nov 04 '21 at 14:09
  • @ChrisG Oh, indeed. Nice catch. You're right, `.populate('dishes')` does a much better job! But you have to admit, that's still a good Mongoose tutorial :) – Jeremy Thille Nov 04 '21 at 19:47