Mongo DB Update Query in Meteor.js - mongodb

I have the following Meteor.js collection:
Game: {
_id: 1,
rounds: {
round: {
number: 1,
players: {
player: {
_id: someId,
cardSelection: someCard
}
player: {
_id: someId2,
cardSelection: someCard2
}
}
}
}
There are some other fields in the collection but these are the important ones. I'm trying to update a player's selection when they click a card. I'm unsure how to do this in MongoDB/Meteor.
So far, I've tried:
var currentRound = Games.findOne(gameId).rounds.length;
Games.update({_id: gameId,
"rounds.round": currentRound,
"rounds.round.$.players": playerId
},
{$set:
{"rounds.$.players.$": {id: playerId, selection: selection}}
}
);
The problem is that when calling the update, I don't know the rounds position (well I do, its the last round, but the round number changes), and I don't know the player's position inside the Players object inside the Round object.
Any help?

Assuming this structure for a Games document, you cannot update a player directly using the positional update operator.
{
_id: 1,
rounds: [{
number: 1,
players: [{
_id: 'someId',
cardSelection: 'someCard'
}, {
_id: 'someId2',
cardSelection: 'someCard2'
}]
}, {
number: 2,
players: []
}]
}
From the mongodb docs:
The positional $ operator cannot be used for queries which traverse more than one array, such as queries that traverse arrays nested within other arrays, because the replacement for the $ placeholder is a single value.
It's not as elegant, but you could set the entire players array. Luckily, underscore makes things easy.
var game = Games.findOne(gameId);
var currentRound = game.rounds.length;
var round = _.findWhere(game.rounds, {number: currentRound});
// find and modify the affected player
round.players = _.map(round.players, function (player) {
if (player.id === playerId) {
player.cardSelection = selection;
}
return player;
});
Games.update({
_id: gameId,
"rounds.number": currentRound
}, {
$set: {"rounds.$.players": round.players}
});
Alternatively, you could break rounds into a separate collection.

Related

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.

Unexpected result when trying to filter fields in mongo db

I have a document structured as follows:
{
_id: "someid",
games: [{
slug: "world-of-warcraft",
class: 'shaman'
}, {
slug: 'starcraft-ii',
class: 'zerg'
}],
roles: {
'starcraft-ii': ['player'],
'world-of-warcraft': ['player']
}
}
I am trying to filter it so that only starcraft-ii within the games array will show up for all players in the role of player in starcraft-ii. I do the following:
function getUsersInGame(slug) {
return Users.find({
'games.slug': slug,
[`roles.${slug}`]: 'player'
}, {
fields: {
'games.$': 1,
'roles': 1
}
});
}
However, this does not match within the games array, and instead returns a 1-element array with world-of-warcraft instead.
What is the appropriate way to filter this array in mongo?
Use $elemMatch in your fields, since the $ will return the first element of the array. So your query should look like this:
function getUsersInGame(slug) {
return Users.find(
{
'"roles.'+ slug + '"': { $in : ['player']}
},
{
'games': {
$elemMatch: {'slug': slug}
},
'roles': 1
});
Please note the difference from the docs:
"The $ operator projects the array elements based on some condition from the query statement.
The $elemMatch projection operator takes an explicit condition argument. This allows you to project based on a condition not in the query..."

Mongoose Update Instance from Nested Array

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).

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.