Add nested collections with aggregation in mongoose? - javascript

I'm new to mongoose, I'm confuse while create the query. Can you help me?
I have video collection like this:
{
_id: 603dea86cef0aed372cd9ce6,
category: [
"603dea86cef0aed372cd9cd8", // array of category objectId
"603dea86cef0aed372cd9cd9"
],
hashtag: [
"603dea86cef0aed372cd9cee" // array of hashtag objectId
],
video_id: '6925666264463576320',
share_id: 'greate_video',
author: 603dea86cef0aed372cd9cd8, // ref to Author objectId
cover: 'https://path.to/img.jpg',
post_by: 60083f24edae03650406ad48, // ref to User objectId
status: 1, // enum status [0, 1, 2]
date: 2021-03-02T07:34:30.635Z
}
I want to query to get data with structure like below. I mean, I will find by _id and get related data form other collections, more than that, I want the video list show with status 1, 2 (not 0) and sort by video _id: -1.
{
_id: 603dea86cef0aed372cd9ce6,
category: [
{_id: "603dea86cef0aed372cd9cd8", category_name: Tech},
{_id: "603dea86cef0aed372cd9cd9", category_name: Mobile},
],
hashtag: [
{_id: "603dea86cef0aed372cd9cee", hashtag_name: tech},
],
video_id: '6925666264463576320',
share_id: 'greate_video',
author: {_id: "603dea86cef0aed372cd9cd8", author_name: Nani, avatar: 'https://path.to/avatar.jpg'},
cover: 'https://path.to/img.jpg',
post_by: {_id: "603dea86cef0aed372cd9cd8", user_name: Username, avatar: 'https://path.to/avatar.jpg'},
status: 1,
date: 2021-03-02T07:34:30.635Z
}
How do I write the aggregation query? I tried with query like this but doesn't work, it show empty [] result.
const videoList = await Video.aggregate([
{
$lookup:
{
from: 'Author',
localField: "author",
foreignField: "_id",
as: "author_info"
}
}
])
Thank you

name of collection usually written in plural lowercase letters so I think you should change Author to authors

Related

Remove object of array schema if the ID does not exist from another array schema

I do have 2 different schemas, one for the user and one for the jobs.
Job has something like manage which is an array of objects and there goes the ID of the user that applied for that job and the ID of the job into user which has an array messages.
If I delete that Object of manage from database then when I get the messages of the user I want to check if that my ID it is into manage, if not then $pull/delete the object with the remaining ID.
Below is what I tried
Job Schema.
manage_: [{
_id: false,
createdAt: Date,
userId: String,
contactedID: String,
listPosition: Number,
note: String,
}]
User Schema.
{
firstName: String,
lastName: String,
messages: [{
_id: mongoose.Schema.Types.ObjectId,
userId: String,
messageID: String,
jobId: String,
timestamp: { type: Date, default: Date.now },
},
],
}
This is the function of the controller which is getting called from the API.
async userJobMessages(req, res , next) {
//req.params.id it is always the authenticated ID
let user = await User.findById(req.params.id);
let fromDate = new Date(Date.now() - 60 * 60 * 24 * 30 * 1000);
user.messages.map(async (t) => {
let job = await Job.find({ _id: t.jobId });
console.log(job, "T")
if (job.length < 1) {
let test2 = await User.findByIdAndUpdate(req.params.id, {
$pull: {
messages: {
jobId: t.jobId,
}
}
}, { new: true });
}});
res.status(200).json({
messages: user.messages,
});
},
I tried somehow to find if my ID exists into manage if not then delete that jobID from my messages.
Based on what #nimrodserok said this is the following which still not working.
I am getting an error message like this.
(node:47922) UnhandledPromiseRejectionWarning: MongoError: let not supported
async userJobMessages(req, res , next) {
let user = await User.findById(req.params.id);
let fromDate = new Date(Date.now() - 60 * 60 * 24 * 30 * 1000);
User.aggregate([
{$match: {_id: req.params.id}},
{$lookup: {
from: "jobs",
let: {jobIds: "$messages.jobId", userId: {$first: "$messages.userId"}},
pipeline: [
{$match: {$expr: {$in: [{$toString: "$_id"}, "$$jobIds"]}}},
{$project: {manage_: {
$filter: {
input: "$manage_",
cond: {$eq: ["$$this.userId", "$$userId"]}
}
}}},
{$match: {"manage_.0": {$exists: true}}},
{$project: {_id: {$toString: "$_id"}}}
],
as: "jobs"
}},
{$set: {
messages: {$filter: {
input: "$messages",
cond: {$in: ["$$this.jobId", "$jobs._id"]}
}}
}},
{$unset: "jobs"},
{$merge: {into: "users"}}
])
res.status(200).json({
messages: user.messages,
});
},
One option is to do it on a single query with $lookup:
Fins the relevant user
Using $lookup, find all jobs listed on this user, but keep only the _ids of jobs that the user is listed on their manage_. Assign them to jobs key.
Filter the user messages to keep only jobs with _ids from our jobs list.
$unset the jobs key
$merge back into the users collection to update the user
const user = await User.aggregate([
{$match: {_id: req.params.id}},
{$lookup: {
from: "jobs",
let: {jobIds: "$messages.jobId", userId: {$first: "$messages.userId"}},
pipeline: [
{$match: {$expr: {$in: [{$toString: "$_id"}, "$$jobIds"]}}},
{$project: {manage_: {
$filter: {
input: "$manage_",
cond: {$eq: ["$$this.userId", "$$userId"]}
}
}}},
{$match: {"manage_.0": {$exists: true}}},
{$project: {_id: {$toString: "$_id"}}}
],
as: "jobs"
}},
{$set: {
messages: {$filter: {
input: "$messages",
cond: {$in: ["$$this.jobId", "$jobs._id"]}
}}
}},
{$unset: "jobs"},
{$merge: {into: "users"}}
])
See how it works on the playground example
EDIT:
For mongoDB version 4.2 or older you can use playground example

Finding a nested object in an array of object using mongoose

{
_id: new ObjectId("61da0ab855483312e8f4483b"),
products: [
{
createdAt: 2022-01-08T22:05:44.635Z,
_id: new ObjectId("61da0ab855483312e8f4483c"),
productCode: 'otf',
productName: 'facebookmeta',
claims: [Array],
permissions: []
},
{
createdAt: 2022-01-08T22:05:44.635Z,
_id: new ObjectId("61da0ab855483312e8f4483f"),
productCode: '4pf',
productName: 'twitteroauth',
claims: [Array],
permissions: [Array]
}
],
__v: 0
}
Now i’ve been trying to get just one object from this array with the find() and findOne method without any luck. if i pass in a certain conditions, it still ends up giving me back an array with both objects. i just want to be able to dynamically pass conditions that belongs to a single object in the array and retrieve that object
MongoDB is applying query conditions on collection and returns the result with the matching documents. As both products twitteroauth & facebookmeta are part of the same document so the whole matching document will be returned.
If you want only a single matching entry in the document then you can use the MongoDB aggregation pipeline (with $unwind) where you can modify the result set.
For example:
db.collection_name.aggregate([
{
"$match": {
// pass matching criteria/conditions here
}
},
{
"$unwind": "$products" //to deconstruct products field in the result
},
{
"$match": {
"products.productName": "twitteroauth"
}
}
])
Note that the second match condition is used to add additional matching criteria/conditions on the deconstructed result set so that products can be filtered.
This will get you the result something like this:-
{
_id: new ObjectId("61da0ab855483312e8f4483b"),
products: {
createdAt: 2022-01-08T22:05:44.635Z,
_id: new ObjectId("61da0ab855483312e8f4483f"),
productCode: '4pf',
productName: 'twitteroauth',
claims: [Array],
permissions: [Array]
},
__v: 0
}
Reference: https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/

MongoDB count all likes and posts for a single user using the aggregation pipeline

I need an endpoint that returns the user's info, amount of post's they've submitted and the sum of all likes their post's have received.
I'm using MongoDB for my database along with Mongoose for my Node server.
These are my models:
var userSchema = new Schema({
'username' : String,
'email' : String,
'password' : String
});
var photoSchema = new Schema({
'text' : String,
'path' : String,
'timestamp': Number,
'postedBy' : {type: Schema.Types.ObjectId, ref: "user"}
});
var likeSchema = new Schema({
'timestamp' : Number,
'photo' : {type: Schema.Types.ObjectId, ref: 'photo'},
'user' : {type: Schema.Types.ObjectId, ref: 'user'}
});
I've already accomplished this by using Javascript and multiple queries:
// Get user
let user = await userModel.findOne({_id: mongoose.Types.ObjectId(id)})
.select("_id, username")
.lean()
.exec();
// Get all users photos
let photos = await photoModel.find({postedBy: mongoose.Types.ObjectId(user._id)})
.select("_id")
.exec();
// Sum likes for all photos
let likes = 0;
for (let i = 0; i < photos.length; i++)
likes += (await likeModel.find({photo: photos[i]._id}).exec()).length;
But I want to try it with the aggregate pipeline. This is my failed attempt:
let user = await userModel.aggregate(
[
{
$match: {_id: mongoose.Types.ObjectId(id)}
},
{
$unset: ["email", "password", "__v"]
},
{
$lookup: {
from: "photos",
localField: "_id",
foreignField: "postedBy",
as: "photos"
}
},
{
$lookup: {
from: "likes",
localField: "photos._id",
foreignField: "photo",
as: "likes"
}
},
{
$unwind: "$photos"
},
{
$group: {
_id: "$_id",
username: {$first: "$username"},
postCount: {$sum: 1},
likeCount: {$sum: 1}
}
}
]);
I don't know how to get the number of likes for each photo post. Is it possible in a single aggregation pipeline?
Would it make more sense to build it with multiple aggegation pipelines or even just with multiple queries?
If you use $size to get the size of each array, you won't need the unwind or group stages.
[
{$match: {_id: mongoose.Types.ObjectId(id)}},
{$lookup: {
from: "photos",
localField: "_id",
foreignField: "postedBy",
as: "photos"
}},
{$lookup: {
from: "likes",
localField: "photos._id",
foreignField: "photo",
as: "likes"
}},
{$project: {
likeCount: {$size:"$likes"},
postCount: {$size:"$photos"}
}}
]
Playground
Alternately, since the only field you are getting from the user collection is _id, which you already have, you can skip the match and lookup, and just aggregate the photos collection directly:
photoModel.aggregate([
{$match: {postedBy: mongoose.Types.ObjectId(id)}},
{$lookup: {
from: "likes",
localField: "_id",
foreignField: "photo",
as: "likes"
}},
{$group: {
_id: "$postedBy",
likeCount: {$sum:{$size:"$likes"}},
postCount: {$sum:1}
}}
])
Playground

Mongoose populate referencing object id, not the object itself

Background
Here's part of my User model:
const Group = require("./Group")
...
groups: {
type: [{ type: Schema.ObjectId, ref: Group }],
default: [],
},
And here's my Group model:
module.exports = mongoose.model(
"Group",
new Schema(
{
name: {
type: String,
required: true,
unique: true,
},
/**
* Array of User ObjectIDs that have owner rights on this group
*/
owners: {
type: [{ type: Schema.ObjectId, ref: User }],
default: [],
},
},
{
timestamps: true,
}
)
)
The Code
Here's the code I'm running to try and populate:
const user = await (await User.findOne({ _id: ... })).execPopulate("Group")
console.log(user.groups)
My console.log is outputting an array of object IDs, when I'd like it to output an actual Group document.
Attempted solutions
I've tried changing my ref to be using the string ("Group"), I've tried arranging my query differently, etc. I'm not sure how I'd go about doing this.
Apologies in advance if this is a duplicate, I've done my best to search but can't really find a solution that works for me.
Specifically, what do I need help with?
I'm trying to create a 'link' between a user model and a group model. In my console.log, I expect it to output a Group document; but it outputs an object ID (which is how it's stored raw in the database, meaning that Mongoose isn't transforming it correctly)
When you change execPopulate to populate like:
async function findUserAndPopulate(userId){
const response = await User.findOne({
_id: userId,
}).populate('groups')
console.log("response",response)
}
You got:
{
groups: [
{
owners: [Array],
_id: 5ecc637916a2223f15581ec7,
name: 'Crazy',
createdAt: 2020-05-26T00:31:53.379Z,
updatedAt: 2020-05-26T00:31:53.379Z,
__v: 0
}
],
_id: 5ecc6206820d583b99b6b595,
fullname: 'James R',
createdAt: 2020-05-26T00:25:42.948Z,
updatedAt: 2020-05-26T00:36:12.186Z,
__v: 1
}
So you can access the user.groups
See the doc: https://mongoosejs.com/docs/populate.html

equivalent of capture group for matched subdocument

I have a Chats collection with a participants subdocument like so..
{
_id: '1',
participants: [ { _id: 'A', seen: false }, { _id: 'B', seen: false } ],
messages: []
}
There are only 2 participants and one of them is the currentUser. I don't know which. Also, there is only ever one chat for any pair of users.
In order to find a chat I use both user ids. So a find query looks like this:
Chats.find(
{ $and: [
{ participants: {$elemMatch: {_id: otherUserId}}},
{ participants: {$elemMatch: {_id: currentUserId}}}
]}
)
I'd like to allow the currentUser to update his own seen field in one update operation.
Currently I have to find the chat first, determine which index of the two users is the currentUser, then build a document to update that user and update it in a separate operation.
Is there something similar to a regex capture group so I can know the id of the element that matched currentUserId? Possibly like this.....
Chats.update( { $and: [
{ participants: {$elemMatch: {_id: otherUserId}}},
{ participants: capture({$elemMatch: {_id: currentUserId}})}
]},
{
$set: {"participants.(capture[0]).seen": true}
})
Or is there a better way?
Well this may not be a solution you're looking for but I thought I should suggest in case it helps.
Chats.update( { $and: [
{ participants: {$elemMatch: {_id: otherUserId}}},
{ participants: {$elemMatch: {_id: currentUserId}}}
]},
{
$set: {"participants.$.seen": true}
})
This will work for you because the $elemMatch stores the index of the matched array. As your anding the $eleMatch, it will store the index matched by the last $eleMatch which will be current user in your case. So, when you use the positional opertor, it will update the seen field for current user.
A simple way to easily update the 'seen' status would be to structure the data like this:
{
_id: '1',
participants: [ 'A', 'B'],
seen: [],
messages: []
}
Then you can update the 'A' has seen the message like this:
Chats.update(
{ $and: [ {participants: 'A'}, {participants: 'B'} ] },
{ $addToSet: { seen: 'A' } }
)
$elemMatch is not required if you have only a single query
condition inside it
$addToSet will only add 'A' if it does not
already exist.

Categories