mongoose populate two simple schemas - node.js

I am stuck with mongoose populate() method.
Here is my schemas:
const GroupSchema = Schema({
title: String,
ips: [
{
ip: String,
hostname: String,
added : { type : Date, default: Date.now }
}
]
});
module.exports = mongoose.model('groups', GroupSchema);
const UserSchema = Schema({
username: String,
ip: String,
groups: [
{
_id : { type: Schema.Types.ObjectId, ref: 'groups'},
added : { type : Date, default: Date.now }
}
]
});
module.exports = mongoose.model('users', UserSchema);
I am trying to join users.groups[]._id with groups._id but absolutely no luck.
Here is how I tried:
User.find().
populate('groups').
exec(function (err, user) {
if (err) return handleError(err);
console.log(user);
});
getting this result:
{ _id: 5b3e039a2f714d38ccf66cax,
username: 'rrrr',
ip: '10.1.1.1',
groups: [ [Object], [Object] ],
__v: 0 } ]
I want to get like this:
{ _id: 5b3e039a2f714d38ccf66cax,
username: 'rrrr',
ip: '10.1.1.1',
groups: [{ title: sssss, added: ssss }, {title: xxxx, added: ssss}] ],
__v: 0 } ]

You can try using $lookup aggregation
If you are using mongodb version 3.4 and below
User.aggregate([
{ "$unwind": "$groups" },
{ "$lookup": {
"from": Groups.collection.name,
"let": { "groupId": "$groups._id" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$_id", "$$groupId" ] } } }
],
"as": "groups._id"
}},
{ "$unwind": "$groups._id" },
{ "$group": {
"_id": "$_id",
"username": { "$first": "$username" },
"ip": { "$first": "$ip" },
"groups": { "$push": "$groups" }
}}
])
If you are using mongodb version 3.6 and above
User.aggregate([
{ "$unwind": "$groups" },
{ "$lookup": {
"from": Groups.collection.name,
"localField": "groups._id",
"foreignField": "_id",
"as": "groups._id"
}},
{ "$unwind": "$groups._id" },
{ "$group": {
"_id": "$_id",
"username": { "$first": "$username" },
"ip": { "$first": "$ip" },
"groups": { "$push": "$groups" }
}}
])

Related

How to conditionally return a field if it is not empty using mongoose

I have 2 mongoose models, one for books and one for authors. The author is embedded in the book document
const mongoose = require("mongoose");
const BookSchema = mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
title: {
type: String,
minLength: 3,
maxlength: 80,
required: true
},
and to query the books I am doing this
async getBook(pid) {
let book = await Book.findOne({
_id: pid
})
.populate('user', 'name username')
;
if (!book) {
return false;
}
return book;
}
This works as expected returning name and username of the author.
However, what I would like to do is to return the name if username is empty, or only return the username if username is not empty. How can I do that please?
The desired behaviour is achievable through $ifNull when wrangling the lookup subpipeline.
db.Book.aggregate([
{
$match: {
_id: {
$in: [
"b1",
"b2",
"b3"
]
}
}
},
{
"$lookup": {
"from": "User",
"let": {
user: "$user"
},
"pipeline": [
{
$match: {
$expr: {
$eq: [
"$$user",
"$_id"
]
}
}
},
{
$project: {
_id: 0,
name: {
"$ifNull": [
"$username",
"$name"
]
}
}
}
],
"as": "user"
}
},
{
"$unwind": "$user"
}
])
Here is the Mongo playground for your reference.

$lookup with condition in mongoose

I have 2 schemas, this is parent collection schema:
const TimesheetSchema = Schema({
managersComment: {
type: String,
},
weekNum: {
type: Number,
},
year: {
type: Number,
},
user: { type: Schema.Types.ObjectId, ref: userModel },
status: {
type: String,
enum: ["Saved", "Submitted", "Approved", "Rejected"],
},
data: [{ type: Schema.Types.ObjectId, ref: TimesheetIndividualData }]
});
This is child collection schema
const TimesheetDataSchema = new Schema(
{
workingDate: {
type: Date,
},
dayVal: {
type: Number,
},
user: { type: Schema.Types.ObjectId, ref: userModel },
parentId: { type: String }
},
{ timestamps: true }
);
In TimesheetDataSchema parentId is basically the _id from TimesheetSchema.
Now i need to run a query which return docs from TimesheetDataSchema, but only the docs in which parentId(ObjectId) of TimesheetSchema has status Approved.
I am trying to do $lookup, but currently no success. Please help.
EDIT: Based upon #ashh suggestion tried this: but getting empty array.
const result = await TimesheetIndividualData.aggregate([
{
"$lookup": {
"from": "timesheetModel",
"let": { "parentId": "$parentId" },
"pipeline": [
{ "$match": { "status": "Approved", "$expr": { "$eq": ["$weekNum", "$parentId"] } } },
],
"as": "timesheet"
}
},
{ "$match": { "timesheet": { "$ne": [] } } }
])
You can use below aggregation
const result = await db.TimesheetDataSchema.aggregate([
{ "$lookup": {
"from": "TimesheetSchema",
"let": { "parentId": "$parentId" },
"pipeline": [
{ "$match": { "status": "approved", "$expr": { "$eq": ["$_id", "$$parentId"] }}},
],
"as": "timesheet"
}},
{ "$match": { "timesheet": { "$ne": [] }} }
])
But I would prefer two queries for better performance here
const timesheets = (await db.TimesheetSchema.find({ status: "approved" }, { _id: 1 })).map(({ _id }) => _id)
const result = await db.TimesheetDataSchema.find({ parentId: { $in: timesheets } })

mongoose get count of relation with condition

i have two schema
vehicle schema :
const VehicleSchema = new Schema({
title: {
type: String,
required: true
},
price: {
type: Number,
required: true
},
);
VehicleSchema.virtual('booking', {
ref: 'Booking',
localField: '_id',
foreignField: 'vehicle',
options: {sort: {created_at: 1}}
});
export default mongoose.model('Vehicle', VehicleSchema);
Booking Schema :
const BookingSchema = new Schema({
start_at:{
type:Date,
required:true
},
end_at:{
type:Date,
required:true
},
status: {
type: String,
enum: ["APPROVED", "REJECTED",],
default: "REJECTED"
},
vehicle:{
type: Schema.Types.ObjectId,
ref: 'Vehicle'
},
});
export default mongoose.model('Booking', BookingSchema);
every vehicle have multi booking
i need to get all Vehicles with counts of rejected and approved status :
[
{
"title":"vehicle_1",
"price":2500,
"rejected_count":10
"approved_count":55
},{
"title":"vehicle_2",
"price":2500,
"rejected_count":15
"approved_count":5
},{
"title":"vehicle_3",
"price":2500,
"rejected_count":1
"approved_count":30
},{
"title":"vehicle_4",
"price":2500,
"rejected_count":5
"approved_count":15
},
]
You can use below aggregation
Vehicle.aggregate([
{ "$lookup": {
"from": Booking.collection.name,
"let": { "vehicle": "$_id" },
"pipeline": [
{ "$match": {
"$expr": { "$eq": [ "$vehicle", "$$vehicle" ] },
"status": "APPROVED"
}}
],
"as": "approved"
}},
{ "$lookup": {
"from": Booking.collection.name,
"let": { "vehicle": "$_id" },
"pipeline": [
{ "$match": {
"$expr": { "$eq": [ "$vehicle", "$$vehicle" ] },
"status": "REJECTED"
}}
],
"as": "rejected"
}},
{ "$project": {
"rejected_count": { "$size": "$rejected" },
"approved_count": { "$size": "$approved" },
"title": 1,
"price": 1
}}
])

Sort docs by array length in Mongoose

I have a simple schema of post, which contains an array of Users ID who liked this post :
const PostSchema = new Schema({
title:{type: String, required: true},
content: {type: String, required: true },
tags: [{type:String}],
author: {type:mongoose.Schema.Types.ObjectId, ref:"User", required:true},
likes: [{ type:mongoose.Schema.Types.ObjectId, ref:"User", required:false}],
createTime: {type:Date, default:Date.now}
})
I want to order my docs my likes count, in other words sort my posts by likes array length. I try something like this but it doesn't work:
// #route GET api/posts
router.get('/',(req, res)=>{
Post.aggregate([{ $addFields: {likesCount:{$size:"likes"}} }]);
Post.find()
.populate('author','name email')
.sort({likesCount:1})
.then(posts=> res.json(posts))
.catch(err=>console.log(err))
})
I do not have idea how make it correctly. Please any help. Thank you in advance :)
You can use below aggregation
Post.aggregate([
{ "$lookup": {
"from": Author.collection.name,
"let": { "author": "$author" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$_id", "$$author" ] } } },
{ "$project": { "name": 1, "email": 1 }}
],
"as": "author",
}},
{ "$unwind": "$author" },
{ "$addFields": { "likesCount": { "$size": "$likes" }}},
{ "$sort": { "likesCount": 1 }}
])

Mongodb aggregate pipeline to return multiple fields with $lookup from array

I'm trying to get a list of sorted comments by createdAt from a Post doc where an aggregate pipeline would be used to populate the owner of a comment in comments field with displayName and profilePhoto fields.
Post Schema:
{
_owner: { type: Schema.Types.ObjectId, ref: 'User', required: true },
...
comments: [
{
_owner: { type: Schema.Types.ObjectId, ref: 'User' },
createdAt: Number,
body: { type: String, maxlength: 200 }
}
]
}
User schema:
{
_id: '123abc'
profilePhoto: String,
displayName: String,
...
}
What I want to return:
[
{
"_id": "5bb5e99e040bf10b884b9653",
"_owner": {
"_id": "5bb51a97fb250722d4f5d5e1",
"profilePhoto": "https://...",
"displayName": "displayname"
},
"createdAt": 1538648478544,
"body": "Another comment"
},
{
"_id": "5bb5e96686f1973274c03880",
"_owner": {
"_id": "5bb51a97fb250722d4f5d5e1",
"profilePhoto": "https://...",
"displayName": "displayname"
},
"createdAt": 1538648422471,
"body": "A new comment"
}
]
I have some working code that goes from aggregate to get sorted comments first, then I populate separately but I want to be able to get this query just by using aggregate pipeline.
Current solution looks like this:
const postComments = await Post.aggregate([
{ $match: { _id: mongoose.Types.ObjectId(postId) } },
{ $unwind: '$comments' },
{ $limit: 50 },
{ $skip: 50 * page },
{ $sort: { 'comments.createdAt': -1 } },
{$replaceRoot: {newRoot: '$comments'}},
{
$project: {
_owner: 1,
createdAt: 1,
body: 1
}
}
]);
await Post.populate(postComments, {path: 'comments._owner', select: 'profilePhoto displayName' } )
You can try below aggregation
const postComments = await Post.aggregate([
{ "$match": { "_id": mongoose.Types.ObjectId(postId) } },
{ "$unwind": "$comments" },
{ "$lookup": {
"from": "users",
"localField": "comments._owner",
"foreignField": "_id",
"as": "comments._owner"
}},
{ "$unwind": "$comments._owner" },
{ "$replaceRoot": { "newRoot": "$comments" }},
{ "$sort": { "createdAt": -1 } }
{ "$limit": 50 }
])

Resources