Mongodb project one field as collective array in one document as result - node.js

This is data
[ {
"_id": "5c75802b1312ca10e63d2ca7",
"external_user_id": "5cbc86081e06c111f9b16fbf"
},
{
"_id": "5c75a35a3cd9af224c0622c1",
"external_user_id": "5cbc86081e06c111f9b16fbf"
},
{
"_id": "5c82c3bede451c0fd74e6739",
"external_user_id": "5cbc86081e06c111f9b16fd5"
},
{
"_id": "5c85432c1a515a17f2d7a2e7",
"external_user_id": "5cbc86081e06c111f9b16fbc"
},
{
"_id": "5c8e8132bfda140998c2f1c4",
"external_user_id": "5cbc86081e06c111f9b16fbf"
}]
I want mongodb query of something like which results different document fields into one field array according to query following:
{
external_user_id: ["5cbc86081e06c111f9b16fbc", "5cbc86081e06c111f9b16fbf", "5cbc86081e06c111f9b16fd5"]
}

You can use below aggregation
db.collection.aggregate([
{ "$group": {
"_id": null,
"external_user_id": {
"$addToSet": "$external_user_id"
}
}}
])

Related

Update document with aggregation pipeline with $lookup

I have a product collection which has fields -userId,referenceProductId,
I want to add new field buyerUserId to all doc where its value will be equal to userId for documents where its _id is equal to referenceProduct_id
For example-for following 2 doc
{
"_id": { "$oid": "61ded34c1e7007b17a86f889" },
"userId": { "$oid": "6190b06b113314ad2183db09" },
"referenceProductId": { "$oid": "61ded15fdd1363aa1ce09c55" }
}
{
"_id": { "$oid": "61ded15fdd1363aa1ce09c55" },
"userId": { "$oid": "6190b06b113314ad2183db09" },
"referenceProductId": { "$oid": "61ded34c1e7007b17a86f889" }
}
BuyerUserId for doc1 will be 6190b06b113314ad2183db09 since doc1's _id is equal to referenceProductId of doc2
I am new to mongoDB, trying to update with below code but doesn't work
{ "$match": { "status": "purchased" }},
{ $lookup:{
from:"product",
let:{
"id":"$Id",
"referenceProductId":"$ReferenceProductId",
"userId":"$UserId",
},
pipeline:[
{
$match:{
$expr:{
$eq: ["$$id", "$referenceProductId"] ,
}
}
},
],
as:"products"
}
},{
$project:{
"buyerUserId":"$products.userId"
}
}
])
As noted in a comment above, $merge can be used as "doc to doc" update mechanism. Starting in v4.4 you may output the results of the merge directly back onto the collection being aggregated.
db.foo.aggregate([
{$lookup: {from: "foo",
let: {rid: "$_id"},
pipeline: [
{$match:{$expr:{$eq: ["$$rid", "$referenceProductId"]}}}
],
as: "Z"}}
// If _id->refId match happened, set buyerUserId else do not set
// anything not even null ($$REMOVE means "do nothing"):
,{$project: {buyerUserId: {$cond: [ {$ne:[0,{$size: "$Z"}]}, "$_id", "$$REMOVE"] }} }
// At this point in pipeline we only have _id and maybe buyerUserId. Use _id
// from each doc to merge buyerUserId back into the main collection.
// Since the docs came from this collection, we should fail if something
// does not match.
,{$merge: {
into: "foo",
on: [ "_id" ],
whenMatched: "merge",
whenNotMatched: "fail"
}}
]);

Fetch grouped items from database

I'm trying to group values together in mongoose. I have a "Review" schema with the following fields:
{ userId, rating, comment }
There are many documents with the same userId. How can I retrieve them in the following format:
{userId: [...allRatings]
Or even better, is there a way to retrieve the averages for each userId? so like this: {userId: 2.8}
I know it's possible and very simple to do in node.js, but is there a way of doing it with mongoose?
Mongoose is really just a vehicle to pass commands to your mondoDB server, so accomplishing what you want in mongoose isn't dissimilar to accomplishing it in the mongo shell.
Here is the aggregation you're looking for:
db.collection.aggregate([
{
"$group": {
"_id": "$userId",
"ratings": {
$push: "$rating"
}
}
},
{
"$project": {
"_id": false,
"userId": "$_id",
"avgRating": {
"$avg": "$ratings"
}
}
}
])
The first stage of the pipeline groups all ratings by useId. The second stage calculates the ratings average and pretties up the key display. That's it. The result will be this:
[
{
"avgRating": 2.8,
"userId": 110
},
{
"avgRating": 3.275,
"userId": 100
}
]
Here is a playground for you: https://mongoplayground.net/p/yXVxk4klabB
As for how to specifically run this command in mongoose, well that's pretty straightforward:
const YourModel = mongoose.model('your_model');
...
YourModel.aggregate([
{
"$group": {
"_id": "$userId",
"ratings": {
$push: "$rating"
}
}
},
{
"$project": {
"_id": false,
"userId": "$_id",
"avgRating": {
"$avg": "$ratings"
}
}
}
])
.then(result => {
console.log(result);
})

MongoDB - NodeJs - remove multiple Object elements from an array, based on another array

My mongodb collection "team" with a field "members" is an array of mongo Objects. I would like to send 1 request to remove multiple members based on an array of user id's
The the "team" document looks like this:
"team": {
"_id": "5defd67ae1536477ef4ce92b",
"name": "foo",
"created_by": "5da830b4b693172577d6fdf2",
"members": [
{
"user": "5d7a59e6eb0aa86571f057fe",
"added_by": "5da830b4b693172577d6fdf2",
"insertion_date": "2019-12-10T17:31:38.686Z"
},
{
"user": "5da718d16b32b5b9f314cb05",
"added_by": "5da830b4b693172577d6fdf2",
"insertion_date": "2019-12-10T17:31:38.686Z"
},
{
"user": "5da5e18a12f3d74b69660b07",
"added_by": "5da830b4b693172577d6fdf2",
"insertion_date": "2019-12-10T17:31:38.686Z"
},
{
"user": "5da6fb16fa8262371f504c1b",
"added_by": "5da830b4b693172577d6fdf2",
"insertion_date": "2019-12-10T17:31:38.686Z"
}
],
"creation_date": "2019-12-10T17:31:38.686Z"
}
I would like to be able to create 1 update request to $pull several "members" based on a list of "user" id's e.g. removeMemberList = ["5d7a59e6eb0aa86571f057fe", "5da718d16b32b5b9f314cb05"]
I've tried the following, but nothing updates.
Team.findByIdAndUpdate(team._id, { $pull : { 'members.$[]' : { user : removeMemberList} } });
Is there a condition such as $pull all elements where removeMemberList.includes(members.user) ?
There is the $pull array Update Operator. Find details here
Your Query looks lilke -
db.Team.findByIdAndUpdate(
{team._id },
{ $pull: { members.$[].user: { $in: removeMemberList }} }
)
Now your members array in document shall not contain those members which are in removeMemberList array.
EDITED:
Use Aggregation Instead: Query Looks like-
[
{
'$match': {
'_id': team._id
}
}, {
'$unwind': {
'path': '$members',
'preserveNullAndEmptyArrays': true
}
}, {
'$match': {
'members.user': {
'$nin': removeMemberList
}
}
}, {
'$group': {
'_id': {
'_id': '$_id',
'name': '$name',
'creation_date': '$creation_date'
},
'members': {
'$addToSet': '$members'
}
}
}, {
'$project': {
'_id': 1,
'name': '$_id.name',
'members': '$members',
'creation_date': '$_id.creation_date'
}
}
]

findOne() returns entire document, instead of a single object

I'm trying to query this set of data using findOne():
{
"_id": {
"$oid": "5c1a4ba1482bf501ed20ae4b"
},
"wardrobe": {
"items": [
{
"type": "T-shirt",
"colour": "Gray",
"material": "Wool",
"brand": "Filson",
"_id": "5c1a4b7d482bf501ed20ae4a"
},
{
"type": "T-shirt",
"colour": "White",
"material": "Acrylic",
"brand": "H&M",
"_id": "5c1a4b7d482bf501ed20ae4a"
}
]
},
"tokens": [],
"email": "another#new.email",
"password": "$2a$10$quEXGjbEMX.3ERdjPabIIuMIKu3zngHDl26tgRcCiIDBItSnC5jda",
"createdAt": {
"$date": "2018-12-19T13:46:09.365Z"
},
"updatedAt": {
"$date": "2018-12-19T13:47:30.123Z"
},
"__v": 2
}
I want to return a single object from the items array using _Id as a filter. This is how I'm doing that:
exports.deleteItem = (req, res, next) => {
User.findOne({ 'wardrobe.items': { $elemMatch: { "_id": "5c1a4b7d482bf501ed20ae4a",} } }, (err, item) => {
console.log(item);
if (err) {
return console.log("error: " + err);
}
res.redirect('/wardrobe');
});
};
However, console.log(item) returns the whole document—like so:
{ wardrobe: { items: [ [Object], [Object] ] },
tokens: [],
_id: 5c1a4ba1482bf501ed20ae4b,
email: 'another#new.email',
password:
'$2a$10$quEXGjbEMX.3ERdjPabIIuMIKu3zngHDl26tgRcCiIDBItSnC5jda',
createdAt: 2018-12-19T13:46:09.365Z,
updatedAt: 2018-12-19T13:47:30.123Z,
__v: 2 }
I want to eventually use this to delete single items, so I need to filter to the single object from the subdocument.
Concerning your question:
MongoDB always returns the full object matching your query, unless you add a projection specifying which fields should be returned.
If you really want to only return a nested object, you could use the aggregation pipeline with the $replaceRoot operator like this:
User.aggregate([
// you can directly query for array fields instead of $elemMatching them
{ $match: { 'wardrobe.items._id': "5c1a4b7d482bf501ed20ae4a"}}},
// this "lifts" the fields wardrobe up and makes it the new root
{ $replaceRoot: {newRoot: '$wardrobe'}
// this "splits" the array into separate objects
{ $unwind: '$items'},
// this'll remove all unwanted elements
{ $match: { 'items._id': "5c1a4b7d482bf501ed20ae4a" },
},
])
This should return only the wanted items.
A note though: If you plan to remove elements from arrays anyways, I'd rather suggest you have a look at the $pull operation, which can remove an element from an array if it matches a certain condition:
https://docs.mongodb.com/manual/reference/operator/update/pull/
User.update(
{ 'wardrobe.items._id': "5c1a4b7d482bf501ed20ae4a"},
{ $pull: { 'wardrobe.items': {_id: "5c1a4b7d482bf501ed20ae4a"}},
{ multi: true }
)

Finding only an item in an array of arrays by value with Mongoose

Here is an example of my Schema with some data:
client {
menus: [{
sections: [{
items: [{
slug: 'some-thing'
}]
}]
}]
}
And I am trying to select it like this:
Schema.findOne({ client._id: id, 'menus.sections.items.slug': 'some-thing' }).select('menus.sections.items.$').exec(function(error, docs){
console.log(docs.menus[0].sections[0].items[0].slug);
});
Of course "docs.menus[0].sections[0].items[0].slug" only works if there is only one thing in each array. How can I make this work if there is multiple items in each array without having to loop through everything to find it?
If you need more details let me know.
The aggregation framework is good for finding things in deeply nested arrays where the positional operator will fail you:
Model.aggregate(
[
// Match the "documents" that meet your criteria
{ "$match": {
"menus.sections.items.slug": "some-thing"
}},
// Unwind the arrays to de-normalize as documents
{ "$unwind": "$menus" },
{ "$unwind": "$menus.sections" },
{ "$unwind": "$menus.sections.items" }
// Match only the element(s) that meet the criteria
{ "$match": {
"menus.sections.items.slug": "some-thing"
}}
// Optionally group everything back to the nested array
// One step at a time
{ "$group": {
"_id": "$_id",
"items": { "$push": "$menus.sections.items.slug" }
}},
{ "$group": {
"_id": "$_id",
"sections": {
"$push": { "items": "$items" }
}
}},
{ "$group": {
"_id": "$_id",
"menus": {
"$push": { "sections": "$sections" }
}
}},
],
function(err,results) {
}
)
Also see the other aggregation operators such as $first for keeping other fields in your document when using $group.

Resources