Mongoose Update Instance from Nested Array - mongodb

In Mongoose, lets say I have a User object pulled from MongoDB and that user has an array of Interests. Now I get an instance of one of that user's Interests.
var user = ...
var interest = ...
... //Make some changes to interest.
How do I update that Interest object (after making some changes to it) within the User array in the DB?
Edit
Here is my current code. It doesn't work and doesn't give an error.
User.update(
{
'_id': user._id,
'interests._id': interest._id
},
{
'$set': {
'interests.$.xyzProperty': interest.xyzProperty
}
},
function(err,obj){//some error checking}
);

If you set an if for each interest, you can access the interest by the $ operator.
user document
{
_id: ObectId('54b568531ef35a7c348f21f2'),
interests: [
{
_id: 12345,
title: 'Tacos',
description: 'I Love tacos'
},
{...},
{...},
]
}
If I know which interest sub document I want to update, I simply query it like so:
UserModel.find({_id: ObectId('54b568531ef35a7c348f21f2'), 'interests.i_d': 12345}).lean().exec(function (err, user) {
var interest = ... //find specific interest
interest.description = 'I love tacos... Like, a lot'.
UserModel.update(
{
_id: user._id,
'interests._id': interest._id
},
{
$set: {
'interests.$.description': interest.description
}
},
function (err, update) {
console.log(err, update);
}
);
});
This uses the $ positional operator and updates the specific sub document(or item in an array).

Related

Can't remove object in array using Mongoose

This has been extensively covered here, but none of the solutions seems to be working for me. I'm attempting to remove an object from an array using that object's id. Currently, my Schema is:
const scheduleSchema = new Schema({
//unrelated
_id: ObjectId
shifts: [
{
_id: Types.ObjectId,
name: String,
shift_start: Date,
shift_end: Date,
},
],
});
I've tried almost every variation of something like this:
.findOneAndUpdate(
{ _id: req.params.id },
{
$pull: {
shifts: { _id: new Types.ObjectId(req.params.id) },
},
}
);
Database:
Database Format
Within these variations, the usual response I've gotten has been either an empty array or null.
I was able slightly find a way around this and accomplish the deletion by utilizing the main _id of the Schema (instead of the nested one:
.findOneAndUpdate(
{ _id: <main _id> },
{ $pull: { shifts: { _id: new Types.ObjectId(<nested _id>) } } },
{ new: true }
);
But I was hoping to figure out a way to do this by just using the nested _id. Any suggestions?
The problem you are having currently is you are using the same _id.
Using mongo, update method allows three objects: query, update and options.
query object is the object into collection which will be updated.
update is the action to do into the object (add, change value...).
options different options to add.
Then, assuming you have this collection:
[
{
"_id": 1,
"shifts": [
{
"_id": 2
},
{
"_id": 3
}
]
}
]
If you try to look for a document which _id is 2, obviously response will be empty (example).
Then, if none document has been found, none document will be updated.
What happens if we look for a document using shifts._id:2?
This tells mongo "search a document where shifts field has an object with _id equals to 2". This query works ok (example) but be careful, this returns the WHOLE document, not only the array which match the _id.
This not return:
[
{
"_id": 1,
"shifts": [
{
"_id": 2
}
]
}
]
Using this query mongo returns the ENTIRE document where exists a field called shifts that contains an object with an _id with value 2. This also include the whole array.
So, with tat, you know why find object works. Now adding this to an update query you can create the query:
This one to remove all shifts._id which are equal to 2.
db.collection.update({
"shifts._id": 2
},
{
$pull: {
shifts: {
_id: 2
}
}
})
Example
Or this one to remove shifts._id if parent _id is equal to 1
db.collection.update({
"_id": 1
},
{
$pull: {
shifts: {
_id: 2
}
}
})
Example

Update a mongo field based on another collections data

I'm looking for a way to update a field based on the sum of the data of another collection.
I tried to bring all the meals and use forEach to call the Products collection for each meal, tested if it was working, but I got a time out.
meals.find().forEach(meal => {
var products = db.Products.find(
{ sku: { $in: meal.products } },
{ _id: 1, name: 1, sku: 1, nutritional_facts: 1 }
)
printjson(products)
})
My goal was to execute something like this below to get the desired result, but I got "SyntaxError: invalid for/in left-hand side". Is not possible to use for in inside a mongo query?
db.Meals.find({}).forEach(meal => {
const nutri_facts = {};
db.Products.find({ sku: { $in: meal.products } },
{ _id: 1, name: 1, sku: 1, nutri_facts: 1 }).forEach(product => {
for (let nutriFact in product.nutri_facts) {
nutri_facts[nutriFact] =
parseFloat(nutri_facts[nutriFact]) +
parseFloat(product.nutri_facts[nutriFact]);
}
}
});
for (let nutriFact in nutri_facts) {
meal.nutri_facts[nutriFact] =
nutri_facts[nutriFact];
}
}
db.Meals.updateOne({ _id: meal._id }, meal)
});
I also had a hard time trying to figure out how to use aggregate and lookup in this case but was not successful.
Is it possible to do that?
Example - Meals Document
{
_id: ObjectId("..."),
products : ["P068","L021","L026"], //these SKUs are part of this meal
nutri_facts: {
total_fat: 5g,
calories: 100kcal
(...other properties)
}
}
For each meal I need to look for its products on 'Products' collections using 'sku' field.
Then I will sum the nutritional facts of all products to get the meal nutritional facts.
Example Products Document
{
_id: ObjectId("..."),
sku: 'A010'
nutri_facts: {
total_fat: 2g,
calories: 40kcal
(...other properties)
}
}
I know that mongo might not be the best option in this case, but the entire application is already built using it.
For each meal I need to look for its products on 'Products'
collections using 'sku' field. Then I will sum the nutritional facts
of all products to get the meal nutritional facts.
db.Meals.find( { } ).forEach( meal => {
// print(meal._id);
const nutri_facts_var = { };
db.Products.find( { sku: { $in: meal.products } }, { nutri_facts: 1 }.forEach( product => {
// printjson(product.nutri_facts);
for ( let nutriFact in product.nutri_facts ) {
let units = (product.nutri_facts[nutriFact].split(/\d+/)).pop();
// print(units)
// Checks for the existence of the field and then adds or assigns
if ( nutri_facts_var[nutriFact] ) {
nutri_facts_var[nutriFact] = parseFloat( nutri_facts_var[nutriFact] ) + parseFloat( product.nutri_facts[nutriFact] );
}
else {
nutri_facts_var[nutriFact] = parseFloat( product.nutri_facts[nutriFact] );
}
nutri_facts_var[nutriFact] = nutri_facts_var[nutriFact] + units;
}
} );
// printjson(nutri_facts_var);
db.Meals.updateOne( { _id: meal._id }, { $set: { nutri_facts: nutri_facts_var } } );
} );
NOTES:
I use the variable nutri_facts_var name ( the var suffixed) so
that we can distinguish the user defined variable names easily from
the document fields names.
{ _id: 1, name: 1, sku: 1, nutri_facts: 1 } changed to {
nutri_facts: 1 }. The _id is included by default in a
projection. The fields name and sku are not needed.
db.Meals.updateOne({ _id: meal._id }, meal) is not a correct
syntax. The update operations use Update
Operators.
The corrected code: db.Meals.updateOne( { _id: meal._id }, { $set:
{ nutri_facts: nutri_facts_v } } ). Note we are updating the
nutifacts only, not all details.
Since the individual nutrifacts are stored as strings (e.g.,
"100kcal"), during arithmetic the string parts are stripped. So, we
capture the string units (e.g., "kcal") for each nutrifact and
append it later after the arithmetic. The following code strips and
stores the units part: let units =
(product.nutri_facts[nutriFact].split(/\d+/)).pop().
Use the mongo shell methods print and printjson to print the
contents of a variable or an object respectively - for debugging
purposes.
Note the query updates the Meal collection even if the nutri_facts field is not defined in it; the $set update operator creates new fields and sets the values in case the fields do not exist.

How can I query the value of an object rather than the key value pair in MongoDB

I've store an array on my user object which holds all of the data
{
_id: ObjectId(#############)
fname: 'Bob',
lname: 'Vargas',
data: [
// the data I want
]
}
I am using express to get his data like this:
db.users.findOne( { _id: ObjectId(#############) }, { data: 1, _id: 0 } );
but that is giving me an object rather than the array:
{ data: [ /* my data */ ]}
how can I get just the array?
UPDATE
app.get('/user/:id/data', function (req, res, next) {
db.users.findOne(
{ _id: mongojs.ObjectId(req.params.id) },
{ data: 1, _id: 0 },
function (err, userData) {
if (err) {
res.send(err);
}
res.json(userData);
}
);
});
Add projection to query result:
db.users.findOne( { _id: ObjectId(#############) }, {_id:0, data:1} )
Use 0 to exlude field from result (_id is included by default), and 1 to include field in result.
MongoDB returns object per document. But you can manually map objects on client side:
db.users.findOne( { _id: ObjectId(#############) }, {_id:0, data:1} )
.map(function(d){ return d.data; }))
MongoDB's findOne() will only return an object, not an array; thus the One. Instead you will need to receive it as and object and then get the value.
If you are doing this from mongo shell then there is no way around unless you want to move the data into its own collection. Otherwise you can get the array from the object in your application.
UPDATE:
in your express response, only encode the data value, like this
res.json(userData.data);

Meteor publish: secret field of subdocument

I have the following collection with an array of subdocument:
{
_id:
players: [
{
_id: 1
answer
score:
},
{
_id: 2
answer:
score:
}]
}
I want to perform a publish function in order that it excludes the field answer of the other player. I.e player 1 should have on his local minimongo this doc:
{
_id:
players: [
{
_id: 1
answer
score:
},
{
_id: 2
score:
}]
}
I tried something like this:
Meteor.publish('game', function (id) {
return Game.find({_id: id}, {players.player_id: 0});
});
But I don't know how to only remove the field answer for the specific player.
I hate working with arrays like this in MongoDB. Personally I would use another collection GamePlayers with a document for each player in each game e.g.
Game ({ _id: g1 })
GamePlayers ({ _id: 0, gameId: g1, playerId: p1, answer: x, score: 0 });
GamePlayers ({ _id: 1, gameId: g1, playerId: p2, answer: y, score: 5 });
This would make things a lot easier.
But to actually answer your question here is a way to do it. I'm sure there is a more elegant way to do it but again I struggle using arrays in MongoDB so I can't think of it.
Since meteor publishes are effectively observeChanges functions we can do this:
note: this assumes that the _id of each player in the players array is equal to the user's Meteor.userId(), if it's not then you will need to provide the playerId as another argument to the publish along with gameId and change as appropriate.
I also assume your Games collection is called "games"
Games = new Meteor.Collection("games")
Meteor.publish('game', function(gameId) {
var self = this;
var handle = Games.find(gameId).observeChanges({
added: function(id, fields) {
self.added("games", id, removeSecretPlayerInfo(fields, self.userId));
},
changed: function(id, fields) {
self.changed("games", id, removeSecretPlayerInfo(fields, self.userId));
},
removed: function(id) {
self.removed("games", id);
}
});
self.ready();
self.onStop(function() {
handle.stop();
});
});
//this function takes all the fields that would be sent to the client,
//goes through the player array and if the player's id _id is not equal to
//the id of the user making the subscription we remove the answer from the array
//before sending it to them
var removeSecretPlayerInfo = function(fields, playerId) {
if (fields.players) {
for (var i = 0; i < fields.players.length; i++) {
if (fields.players[i]._id !== playerId)
delete fields.players[i].answer;
}
}
return fields;
}
You need to use quotations when querying a subfield. Also note that the second parameter to find is on options object that should have a property fields.
return Game.find({_id: id}, {fields: {'players.answer': 0}});

Resolving a ref inside of a MongoDB aggregate

I have a Product model object that has the following field in its schema:
category : { type: ObjectId, turnOn: false, ref: "category" }
It references a category model that has a title field in it:
var categorySchema = Schema({
title : { type: String }
});
I'm using the product.category property (which is of type ObjectId as shown above) in a MongoDB aggregate but really want the category.title property from the category model rather than _id in the final resultset.
The following code gets the job done, but you'll see that I'm having to do some looping at the end to "resolve" the title field for the given product.category (ObjectId). Is there anyway to do all of that within the aggregate? In other words, is there a way to get the category model object's title field in the groups that are returned rather than having to do the extra looping work? Based on posts I've researched I don't see a built-in way but wanted to double-check.
getProductsGroupedByCategory = function(callback) {
Category.find(function(err, cats) {
var aggregate = [
{
$group: {
_id: "$category",
products: {
$push: {
title: "$title",
authors: "$authors",
publishDate: "$publishDate",
description: "$description"
}
}
}
},
{
$sort: {
"_id": 1
}
}
];
Product.aggregate(aggregate, function(err, catProducts) {
//Grab name of category and associate with each group
//since we only have the category_id at this point
for (var i = 0; i<catProducts.length;i++) {
var catProduct = catProducts[i];
for (var j=0;j<cats.length;j++) {
if (catProduct._id.toString() === cats[j]._id.toString()) {
catProduct.category = cats[j].title;
}
}
};
callback(err, catProducts);
});
});
}, //more code follows
An example datum would be helpful along with what you need out of it. From What I understand you are looking to get the title in to the grouping criteria and that should be doing by having a compound grouping criteria i.e.
_id: {category: "$category", title: "$title"}
If the title is within an array, you should do unwind, group and then wind again to achieve the result.