Mongoose - Increment a value inside an array of objects - javascript

I am having hard time figuring out how to increment a value in an object within an array
For instance I have this document based on Poll schema:
{
"_id": "584b2cc6817758118e9557d8",
"title": "Number of Skittles",
"description": "Test1",
"date": "Dec 9, 2016",
"__v": 0,
"labelOptions": [
{
"Bob": 112
},
{
"Billy": 32
},
{
"Joe": 45
}
]
}
Using express, I am able to get this far:
app.put('/polls/:id', function(req, res){
let id = req.params.id;
let labelOption = req.query.labelOption;
Poll.findOneAndUpdate(
{'_id' : id},
{$inc: {`labelOptions.$.${labelOption}`: 1 }},
function(err){
console.log(err)
})
where labelOption is the one that I would like to increment its value
To be more concise, I am having trouble transversing inside the document.

It is not possible to directly increment the value in the .find query if labelOptions is an Array of Object. To make it easier, you should change the labelOptions type from Array of Objects to Object:
"labelOptions": {
"Bob": 112,
"Billy": 32,
"Joe": 45
};
Also consider using .findByIdAndUpdate instead of .findOneAndUpdate if you are querying by the document's _id. And then, you can achieve what you want by:
Poll.findByIdAndUpdate(
id,
{$inc: {`labelOptions.${labelOption}`: 1 }},
function(err, document) {
console.log(err);
});
UPDATE: If you are persistent on using Array of Objects for labelOptions, there is a workaround:
Poll.findById(
id,
function (err, _poll) {
/** Temporarily store labelOptions in a new variable because we cannot directly modify the document */
let _updatedLabelOptions = _poll.labelOptions;
/** We need to iterate over the labelOptions array to check where Bob is */
_updatedLabelOptions.forEach(function (_label) {
/** Iterate over key,value of the current object */
for (let _name in _label) {
/** Make sure that the object really has a property _name */
if (_label.hasOwnProperty(_name)) {
/** If name matches the person we want to increment, update it's value */
if (_name === labelOption) ++_label._name;
}
}
});
/** Update the documents labelOptions property with the temporary one we've created */
_poll.update({labelOptions: _updatedLabelOptions}, function (err) {
console.log(err);
});
});

There is another way to do this which allows a more flexible document model. If you add a field to your object like:
{
"_id": "584b2cc6817758118e9557d8",
"title": "Number of Skittles",
"description": "Test1",
"date": "Dec 9, 2016",
"__v": 0,
"labelOptions": [
{
"name": "Bob",
"number": 112
},
{
"name": "Billy",
"number": 32
},
{
"name": "Joe"
"number": 45
}
]
}
Then you can do:
app.put('/polls/:id', function(req, res){
let id = req.params.id;
let labelOption = req.query.labelOption;
Poll.findOneAndUpdate(
{
'_id' : id,
'labelOptions.name
},
{$inc: {
`labelOptions.$.number`: 1
}},
function(err){
console.log(err)
})

Related

Using elemMatch to return one object from array node.js

I am trying to query a data collection to return just one object from an array of objects using elemMatch. I have this data:
[
{
"_id": "5ba10e24e1e9f4062801ddeb",
"user": {
"_id": "5b9b9097650c3414ac96bacc",
"firstName": "blah",
"lastName": "blah blah",
"email": "blah#gmail.com"
},
"appointments": [
{
"date": "2018-09-18T14:39:36.949Z",
"_id": "5ba10e28e1e9f4062801dded",
"treatment": "LVL",
"cost": 30
},
{
"date": "2018-09-18T14:39:32.314Z",
"_id": "5ba10e24e1e9f4062801ddec",
"treatment": "LVL",
"cost": 30
}
],
"__v": 1
}
]
I need to query the DB and pull out just one appointment depending on the id passed to params. I am using the code below based on the docs here.
router.get(
"/booked/:app_id",
passport.authenticate("jwt", { session: false }),
(req, res) => {
Appointment.find()
.elemMatch("appointments", { id: req.params.app_id })
.then(app => res.json(app));
}
);
This is returning an empty array though when I test with postman. This is the first time I have used node.js and mongoDB so any help would be appreciated. Thanks.
You are using elemmatch as query operator, but you need to use it as projection operator
Change your function to this :
Appointment.find(
{"appointments": {$elemMatch: {_id: req.params.app_id }}},
{"appointments": {$elemMatch: {_id: req.params.app_id }}}
)
Why twice??
First object in find query param is $elemmatch query operator. It will return all documents, that have at least one entry matching _id in there appointments array. (but will return whole documents)
Second object (the same) is elemmatch projection oprator, which 'filters' appointments array and returns only the first matching element in array.
PS : cannot test this command. Try on your side

MongoDB update nested object dependent on json input

I have json data that is structured in the following form:
[
{"size":100,"year":2015,"geography":"London","age":"21","gender":"Female"},
{"size":80,"year":2015,"geography":"Cardiff","age":"38","gender":"Male"},
{"size":80,"year":2013,"geography":"Edinburgh","age":"36","gender":"All"}
]
And I am trying to add it to a database collection with the following schema:
const Schema = new Schema({
geography: String,
size: {
2013: {
male: {
age: {
}
}
female: {
age: {
}
}
all: {
age: {
}
}
}
}
});
I'm currently trying to set up my update to work in the following way:
query = { geography : geography};
update = { geography : geography, $set: { "year.gender.age" : size}
Schema.updateOne( query, update, { upsert: true };
The $set here obviously does not work as the update does not know to pull the values from the json data. I have tried making year, gender and age variables and then refactoring the set (see below) but this does not work either.
$set: { `${year}.${gender{.$age}` }
So the question is how can I use the values in my JSON data to determine which embedded field to update?
The better option here would be a different schema which is not dynamic, as suggested in this answer.
Nonetheless, if you are to go with the current schema, you can map the values in the JSON as follows:
For example, if you have an object like
const obj = {
"size": 100,
"year": 2015,
"geography": "London",
"age": "21",
"gender": "Female"
}
to transform it to an update object of the form
{
"geography": "London",
"2015.female.21": 100
}
requires the following map:
const obj = {
"size": 100,
"year": 2015,
"geography": "London",
"age": "21",
"gender": "Female"
}
const doc = Object.keys(obj).reduce((acc, curr) => {
if (curr === "geography") {
acc["geography"] = obj[curr];
return acc;
};
switch(curr) {
case "year":
case "gender":
case "age":
acc[`${obj['year']}.${obj['gender'].toLowerCase()}.${obj['age']}`] = obj['size']
}
return acc;
}, {});
console.log(JSON.stringify(doc, null, 4));
which you can then use in your update operation as
Model.updateOne( query, { '$set': doc }, { upsert: true }, callback );
in order to create object like this-
{"size":100,"year":2015,"geography":"London","age":"21","gender":"Female"}
I think you need to set your schema to:
const Schema = new Schema({
geography: String,
size: Number,
year,: String,
gender: String,
age: String
});
Then to update use something like:
update = { {$and: [{year:"2015"}, {age:"33"} /*and more*/]}, $set: { size : size}

MongoDB Mongoose aggregation / math operations

I am having a problem with understanding how the $aggregation method works in Mongoose. To be honest, I couldn't found any code examples in mongoose docs (I haven't found even [search] option on their site [google site:mongoosejs.com helps me])
So, I hope someone will help me to explain this and answer my question.
For example, I have various documents in my collection, with fields:
{ "_id": 1, "item":47139, "total_price": 560000, "quantity": 56, "lastModified" : 1491748073000 }
{ "_id": 3, "item":47140, "total_price": 1750000, "quantity": 150, "lastModified" : 1491748073000 }
and I would like to $sum all the "quantity" for documents with id: 47139 and timestamp: 1491748073000. As for now this code works fine and provides me the necessary data:
var server = mongoose.model('Name_of_my_Schema', Schema);
server.find({ lastModified : 1491748073000, item : 47139 }, null, {sort: 'buyout'},function (err, res) {
total = 0;
for (i = 0; i < res.length; i++) { //sorry, map.reduce, but not this time
total += res[i].quantity;
}
console.log(total); //in this case total = 256
});
but is it possible to do this operation via mongoose? According to mongo docs I should use this code for matching the necessary array of docs, like these:
server.aggregate().match({
lastModified : 1491748073000,
item : 47139
}).exec(function (err, result){
console.log(result);
console.log(err);
});
and then use the $group to group the necessary data and { $sum: "quantity" }, but what I should do next? Why should I use a $group, if I just want to receive a sum of all quantity?
Could someone give me a clue what am I missing?
As you have rightly said, you are missing the $group pipeline step to do the sum aggregate. Complete the pipeline as:
server.aggregate()
.match({
"lastModified": 1491748073000,
"item": 47139
})
.group({
"_id": null,
"total": { "$sum": "$quantity" }
})
.exec(function (err, result){
console.log(result);
console.log(err);
});
or as an array of operators:
server.aggregate([
{ "$match": { "lastModified": 1491748073000, "item": 47139 } },
{ "$group": { "_id": null, "total": { "$sum": "$quantity" } } }
]).exec(function (err, result){
console.log(result);
console.log(err);
});

update array of object with multiple condition in mongoose failed

[{
"date": "18/12/2010",
"babies": [{
"id":1,
"name": "James",
"age": 8,
}, {
"id":2,
"name": "John",
"age": 4,
}]
}]
I want to set the age of John to 10 but failed. I have to do multi condition to be more specified.
Babies.update({"date":date, 'babies.id': 1}, {'$set': {age:10}, function(err, response){
res.json(response);
})
The first condition is date and the second condition is the array of object of babies, which in this case it's the id. Above query has no error and no effect, where did I do wrong?
I debug with doing this query
Babies.find({'babies.id': 1}, function(err, response){
res.json(response);
})
and it couldn't find the correct target, maybe that's the problem
Use {'$set': {'babies.$.age':10}} instead of {'$set': {age:10}}.
Babies.update({"date":date, 'babies.id': 1},
{'$set': {
'babies.$.age':10
}
},
function(err, response){
res.json(response);
})
The positional $ operator identifies an element in an array to update without explicitly specifying the position of the element in the array.
Refer to MongoDB Positional Operator for more information.
Instead of passing only object of field value {age:10} in $set flag, Pass the value in format of Array.index.field. So it would become like this -
{ $set: { 'babies.$.age': 10 } }

Omit certain results in a subdocument MongoDB

I am querying a list of items and only returning the items with contain a given ID in the provider_cost_dict. For instance, if I pass providerId = 10001, then only items with an entry in provider_cost_dict that match the provider ID will return.
How can I modify my code so that I can omit all the provider_cost_dict's that do not match the provider ID?
Here is my current code:
var procedures = db.collection('procedures');
var query = {};
query['provider_cost_dict.' + req.query.providerId] = {$exists: true };
procedures.find({}).toArray(function(err, result) {
// Send the result back via JSON.
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify(result, null, 3));
});
Here is what my response looks like:
{
"_id": "57c62cb53673aaf5f6beacf9",
"natl_total_cost": 1274787840,
"natl_average": 8338.487,
"natl_report_count": 152880,
"name": "COPD (WITH MAJOR COMPLICATIONS)",
"provider_cost_dict": {
"10001": {
"report_count": 144,
"total_cost": 957334,
"average_cost": 6648.153
},
"10005": {
"report_count": 200,
"total_cost": 1321644,
"average_cost": 6608.22
},
"10006": {
"report_count": 214,
"total_cost": 1345658,
"average_cost": 6288.1216
If I passed `10001 how could I make my return look like:
{
"_id": "57c62cb53673aaf5f6beacf9",
"natl_total_cost": 1274787840,
"natl_average": 8338.487,
"natl_report_count": 152880,
"name": "COPD (WITH MAJOR COMPLICATIONS)",
"provider_cost_dict": {
"10001": {
"report_count": 144,
"total_cost": 957334,
"average_cost": 6648.153
}
}
You can specify a projection to the query so that only your desired cost dict is shown like so
var query = { 'provider_cost_dict.10001': { $exists: true } };
var project = {
'natl_total_cost': 1,
'natl_average': 1,
'natl_report_count': 1,
'name': 1,
'provider_cost_dict.10001': 1
};
procedures.find(query, project).toArray( ...

Categories