Query nested array - node.js

I have the below User document. I want to return a list of all 'friends' where friends.name is equal to "Bob".
{
"_id" : ObjectId("5a4be9f200471a49d2e23ce4"),
"name": "James"
"friends" : [
{
"_id" : ObjectId("5a4be9f200471a49d2e23ce6"),
"dob" : ISODate("2018-01-02T00:00:00.000Z"),
"name" : "Bob"
},
{
"_id" : ObjectId("5a4be9f200471a49d2e23ce5"),
"dob" : ISODate("2018-01-02T00:00:00.000Z"),
"name" : "Fred"
}
],
"__v" : 0
}
When I try to query using the below, its working but its returning the whole friends list, not just Bob.
User.findOne({ "friends.name": "Bob" }, function(err, friends) {
if(err) return next(err);
res.send(friends);
});
How can I query so I only to return Bob object and not Fred?

Your query is correct, but it returns all user documents having at least one friend matching your condition.
If you just want matching items from the friends collection, you might do something like this.
db.User.aggregate([
{ $unwind: "$friends" },
{ $replaceRoot: { newRoot: "$friends" } },
{ $match: { name: "Bob" }}
])

Related

Category hierarchy aggregation using mongodb and nodejs

My document structure is as follows:
{
"_id" : ObjectId("54d81827e4a4449d023b4e34"),
"cat_id" : 1,
"description" : "Refridgerator",
"image" : "refridgerator",
"parent" : null,
"slug" : "refridgerator"
}
{
"_id" : ObjectId("54dc38bce4a4449d023b4e58"),
"name" : "Ice Cream",
"description" : "Ice Cream",
"image" : "ice-cream.jpg",
"slug" : "ice-cream",
"parent" : "54d81827e4a4449d023b4e34"
}
{
"_id" : ObjectId("54dc3705e4a4449d023b4e56"),
"name" : "Chocolate",
"description" : "Chocolate",
"image" : "chocolate.jpg",
"slug" : "chocolate",
"parent" : "54d81827e4a4449d023b4e34"
}
I’m making a category hierarchy using mongodb and nodejs.
Now I wish to query for _id = ‘54d81827e4a4449d023b4e34’ (Refridgerator) and should get back all the child categories
How to achieve the above in nodejs?
Also, nodejs uses async call to the database, I’m unable to get the json structured with parent – child relations.
How would I do the async call for this?
You want the refridgerator and all the subcategories?
And async is also a problem?
I think you can use aggregation here.
Say you're looking for a category with _id variable which is an ObjectId of what you want, and it's subcategories.
db.yourCollection.aggregate({
// get stuff where you have the parent or subcats.
$match: {
$or: [
{_id: ObjectId("54de8b9f022ff38bbf5e0530")},
{parent: ObjectId("54de8b9f022ff38bbf5e0530")}
]
}
},
// reshape the data you'll need further on from each mached doc
{
$project: {
_id: false,
data: {
id: '$_id',
name: '$name'
// I guess you'll also want the `slug` and `image` here.
// but that's homework :)
},
parent: '$parent'
}
},
// now put a common _id so you can group them, and also put stuff into arrays
{
$project: {
id: {$literal: 'id'},
mainCategory: {
// if our parent is null, put our data.
// otherwise put null here.
$cond: [{$eq: [null, '$parent']}, {_id: '$data.id', name: '$data.name'}, undefined]
},
subcat: {
// here is the other way around.
$cond: [{$ne: [null, '$parent']}, {_id: '$data.id', name: '$data.name'}, null]
}
}
// that stage produces for each doc either a mainCat or subcat
// (and the other prop equals to null)
},
// finally, group the things so you can have them together
{
$group: {
_id: '$id',
// a bit hacky, but mongo will yield to it
mainCategory: {$max: '$mainCategory'},
subCategories: {
// this will, unfortunately, also add the `null` we have
// assigned to main category up there
$addToSet: '$subcat'
}
}
},
// so we get rid of the unwanted _id = 'id' and the null from subcats.
{
$project: {
_id: false,
mainCategory: 1,
subCategories: {
$setDifference: ['$subCategories', [null]]
}
}
})
Given this data set:
[{
"_id" : ObjectId("54de8b9f022ff38bbf5e0530"),
"name" : "Fridge",
"parent" : null
},
{
"_id" : ObjectId("54de8bba022ff38bbf5e0531"),
"name" : "choco",
"parent" : ObjectId("54de8b9f022ff38bbf5e0530")
},
{
"_id" : ObjectId("54de8bc8022ff38bbf5e0532"),
"name" : "apple",
"parent" : ObjectId("54de8b9f022ff38bbf5e0530")
}
I get this result:
{
"result" : [
{
"mainCategory" : {
"_id" : ObjectId("54de8b9f022ff38bbf5e0530"),
"name" : "Fridge"
},
"subCategories" : [
{
"_id" : ObjectId("54de8bc8022ff38bbf5e0532"),
"name" : "apple"
},
{
"_id" : ObjectId("54de8bba022ff38bbf5e0531"),
"name" : "choco"
}
]
}
],
"ok" : 1
}
As for async, typically you'd do something like this:
db.collection.aggregate(thePipeLineAbove, function(err, results) {
// handle err
if (err) {
// deal with it
} else {
console.log(results);
}
});
But that depends a bit on your MongoDB driver.
You could expand this even if you have deeper hierarchy structure.
This has nothing to do with NodeJS, it's your data structure that matters.
refer to my answer to this question, the first part is about how to implement it efficiently.

Getting the number of unique values of a query

I have some documents with the following structure:
{
"_id": "53ad76d70ddd13e015c0aed1",
"action": "login",
"actor": {
"name": "John",
"id": 21337037
}
}
How can I make a query in Node.js that will return the number of the unique actors that have done a specific action. For example if I have a activity stream log, that shows all the actions done by the actors, and a actorscan make a specific action multiple times, how can I get the number of all the unique actors that have done the "login" action. The actors are identified by actor.id
db.collection.distinct()
db.collection.distinct("actor.id", { action: "login"})
will return all unique occiriences and then you can get count of a result set.
PS
do not forget about db.collection.ensureIndex({action: 1})
You can use aggregation framework for this:
db.coll.aggregate([
/* Filter only actions you're looking for */
{ $match : { action : "login" }},
/* finally group the documents by actors to calculate the num. of actions */
{ $group : { _id : "$actor", numActions: { $sum : 1 }}}
]);
This query will group the documents by the entire actor sub-document and calculate the number of actions by using $sum. The $match operator will filter only documents with specific action.
However, that query will work only if your actor sub-documents are the same. You said that you're identifying your actors by id field. So if, for some reason, actor sub-documents are not exactly the same, you will have problems with your results.
Consider these these three documents:
{
...
"actor": {
"name": "John",
"id": 21337037
}
},
{
...
"actor": {
"name": "john",
"id": 21337037
}
},
{
...
"actor": {
"surname" : "Nash",
"name": "John",
"id": 21337037
}
}
They will be grouped in three different groups, even though the id field is the same.
To overcome this problem, you will need to group by actor.id.
db.coll.aggregate([
/* Filter only actions you're looking for */
{ $match : { action : "login" }},
/* finally group the documents to calculate the num. of actions */
{ $group : { _id : "$actor.id", numActions: { $sum : 1 }}}
]);
This query will correctly group your documents by looking only at the actor.id field.
Edit
You didn't specify what driver you were using so I wrote the examples for MongoDB shell.
Aggregation with Node.js driver is very similar but with one difference: Node.js is async The results of the aggregation are returned in the callback. You can check the Node.js aggregation documentation for more examples:
So the aggregation command in Node.js will look like this:
var MongoClient = require('mongodb').MongoClient;
MongoClient.connect('mongodb://127.0.0.1:27017/test', function(err, db) {
if(err) throw err;
var collection = db.collection('auditlogs');
collection.aggregate([
{ $match : { action : "login" }},
{ $group : { _id : "$actor.id", numActions: { $sum : 1 }}} ],
function(err, docs) {
if (err) console.error(err);
console.log(docs);
// do something with results
}
);
});
For these test documents:
{
"_id" : ObjectId("53b162ea698171cc1677fab8"),
"action" : "login",
"actor" : {
"name" : "John",
"id" : 21337037
}
},
{
"_id" : ObjectId("53b162ee698171cc1677fab9"),
"action" : "login",
"actor" : {
"name" : "john",
"id" : 21337037
}
},
{
"_id" : ObjectId("53b162f7698171cc1677faba"),
"action" : "login",
"actor" : {
"name" : "john",
"surname" : "nash",
"id" : 21337037
}
},
{
"_id" : ObjectId("53b16319698171cc1677fabb"),
"action" : "login",
"actor" : {
"name" : "foo",
"id" : 10000
}
}
It will return the following result:
[ { _id: 10000, numActions: 1 },
{ _id: 21337037, numActions: 3 } ]
The aggregation framework is your answer:
db.actors.aggregate([
// If you really need to filter
{ "$match": { "action": "login" } },
// Then group
{ "$group": {
"_id": {
"action": "$action",
"actor": "$actor"
},
"count": { "$sum": 1 }
}}
])
Your "actor" combination is "unique", so all you need to do it have the common "grouping keys" under the _id value for the $group pipeline stage and count those "distinct" combinations with $sum.

mongoDB - Update query adding an array document to an existing document

The userlist collection contains documents of the following format.
{
"_id" : ObjectId("5381d32ce72cc794166eede2"),
"name" : "Haseeb",
"password" : "dgkhan",
"email" : "hasseeb#yahoo.com",
"address" : "237 D, Faisal Town , Lahore",
"phone" : "5162806"
}
I intend to add another member in the existing document such that the resultant document looks like this.
{
"_id" : ObjectId("5381d32ce72cc794166eede2"),
"name" : "Haseeb",
"password" : "dgkhan",
"email" : "hasseeb#yahoo.com",
"address" : "237 D, Faisal Town , Lahore",
"phone" : "5162806",
"purchases" : [{
"itemID": xyz,
"quantity": 142
},
{
"itemID": kjh,
"quantity": 987
}
}]
}
For this I have written the following mongoskin query, but it is not performing any updates.
db.collection('userlist').update(
{_id:req.session._id},
{
'$push': { purchases: {
itemID: item.ID,
quantity: item.quantity
}
}
}, function(err) {
if (err) throw err;
console.log('Updated!');
});
}
Value of req.session._id = 5381d32ce72cc794166eede2 i.e a valid _id field of a docuemnt in my collection whereas item.ID and item.quantity are also valid strings.
Any help would be really appreciated.
I'm more familiar with Mongoose, but looking at the docs you could probably either use:
collection.updateById(req.session._id, ...)
or
collection.update({_id: toObjectID(req.session_id)}, ...)
I think you should be using $set instead of $push.
A good resource for this specific question is: http://docs.mongodb.org/manual/reference/method/db.collection.update/#db.collection.update
Something like this. You need to include the $each syntax.
db.collection('userlist').update(
{_id:req.session._id},
{
$push { purchases: { $each: {
itemID: item.ID,
quantity: item.quantity
}
}
}
}, function(err) {
if (err) throw err;
console.log('Updated!');
});
}

How to build query for relating parent document's field to embedded array document's field. Using MongoDB aggregation Operators

Consider the following document in the collection named 'CityAssociation'
{
"_id" : "MY_ID",
"ThisCityID" : "001",
"CityIDs" : [{
"CityID" : "001",
"CityName" : "Bangalore"
}, {
"CityID" : "002",
"CityName" : "Mysore"
}],
"CityUserDetails": {
"User" : "ABCD"
}
}
Now I have User value i.e. in above case I have value ABCD and want to find it with only city where the first level's field ThisCityID matches to the embedded array documnet's field CityID. Finally I need to project as follows (for the above case):
{
'UserName': 'ABCD',
'HomeTown':'Bangalore'
}
In Node.js + MongoDB native drive, I wrote a aggregation query as follows which is not working as expected.
collection.aggregate([
{ $match: { 'CityUserDetails.User': 'ABCD', 'CityIDs': { $elemMatch: { CityID: ThisCityID}}} },
{ $unwind: "$CityIDs" },
{ $group: {
_id: '$_id',
CityUserDetails: { $first: "$CityUserDetails" },
CityIDs: { $first: "$CityIDs" }
}
},
{ $project: {
_id: 0,
"UserName": "$CityUserDetails.User",
"HomeTown": "$CityIDs.CityName"
}
}
], function (err, doc) {
if (err) return console.error(err);
console.dir(doc);
}
);
Can anyone tell me how this can be done with query.
Note: On MongoDB schema we don't have control to change it.
You can use the $eq operator to check if the first level's field ThisCityID matches embedded array document's field CityID.
db.city.aggregate([
{ $match : { "CityUserDetails.User" : "ABCD" }},
{ $unwind : "$CityIDs" },
{ $project : {
matches : { $eq: ["$CityIDs.CityID","$ThisCityID"]},
UserName : "$CityUserDetails.User",
HomeTown : "$CityIDs.CityName"
}},
{ $match : { matches : true }},
{ $project : {
_id : 0,
UserName : 1,
HomeTown : 1
}},
])
And the result is:
{
"result" : [
{
"UserName" : "ABCD",
"HomeTown" : "Bangalore"
}
],
"ok" : 1
}

How to use '$match' Aggregation Operators of MongoDB to match with the id of embedded document?

Scenario: Consider the document present in the MongoDB in collection named 'MyCollection'
{
"_id" : ObjectId("512bc95fe835e68f199c8686"),
"author": "dave",
"score" : 80,
"USER" : {
"UserID": "Test1",
"UserName": "ABCD"
}
},
{ "_id" : ObjectId("512bc962e835e68f199c8687"),
"author" : "dave",
"score" : 85,
"USER" : {
"UserID": "Test2",
"UserName": "XYZ"
}
},
...
I know the UserID and want to fetch based on that.
Issue: I tried the following code with Node.js + MongoDB-native driver:
db.Collection('MyCollection', function (err, collection) {
if (err) return console.error(err);
collection.aggregate([
{ $match: { '$USER.UserID': 'Test2'} },
{$group: {
_id: '$_id'
}
},
{
$project: {
_id: 1
}
}
], function (err, doc) {
if (err) return console.error(err);
console.dir(doc);
});
});
But its not working as expected.
Question: Can anyone know how to do the same with $match operator in MongoDB query?
Update: I am not getting any error. But the object will be blank i.e. []
I tried in the shell and your $match statement is wrong - trying in the shell
> db.MyCollection.find()
{ "_id" : ObjectId("512bc95fe835e68f199c8686"), "author" : "dave", "score" : 80, "USER" : { "UserID" : "Test1", "UserName" : "ABCD" } }
{ "_id" : ObjectId("512bc962e835e68f199c8687"), "author" : "dave", "score" : 85, "USER" : { "UserID" : "Test2", "UserName" : "XYZ" } }
> db.MyCollection.aggregate([{$match: {"$USER.UserID": "Test2"}}])
{ "result" : [ ], "ok" : 1 }
> db.MyCollection.aggregate([{$match: {"USER.UserID": "Test2"}}])
{
"result" : [
{
"_id" : ObjectId("512bc962e835e68f199c8687"),
"author" : "dave",
"score" : 85,
"USER" : {
"UserID" : "Test2",
"UserName" : "XYZ"
}
}
],
"ok" : 1
}
So the full aggregation would be:
db.MyCollection.aggregate([
{$match: {"USER.UserID": "Test2"}},
{$group: {"_id": "$_id"}},
{$project: {"_id": 1}}
])
(You don't need the extra $project as you only project _id in the $group but equally as _id is unique you should just have the $project and remove the $group)

Resources