Sort docs by array length in Mongoose - node.js

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 }}
])

Related

$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 } })

$lookup returns an empty array mongoose

I am using the following code with $lookup function.
postSchemaModel.aggregate([{
"$geoNear": {
"near": { "type": "Point", "coordinates": [6.7336665, 79.8994071], "Typology": "post" },
"distanceField": "dist.calculated",
"maxDistance": 5000,
"includeLocs": "dist.location",
"spherical": true
}
},
{ "$limit": limit },
{ "$skip": startIndex },
{ "$sort": { "createdAt": -1 } },
{
"$lookup": {
"from": userSchemaModel.collection.name,
"localField": "user_id",
"foreignField": "_id",
"as": "user_id"
}
},
{
"$project": {
"post_data": 1,
"likes": 1,
"commentsCount": 1,
"post_img": 1,
"isUserLiked": 1,
"usersLiked": 1,
'exp_date': 1,
"has_img": 1,
"user_id": "$user_id",
"typology": 1,
"geometry": 1,
"category": 1,
"created": 1,
"createdAt": 1,
"updatedAt": 1,
}
},
]).then(async function(posts) {
//some code here
});
The problem is this gives me an empty array for user_id. The following is one output I receive.
{ _id: 5ee1f89732fd2c33bccfec55,
post_data: 'vvhhh',
likes: 1,
commentsCount: 0,
post_img: null,
isUserLiked: false,
usersLiked: [ 5edf43b93859680cf815e577 ],
exp_date: 2020-06-12T18:30:00.000Z,
has_img: false,
typology: 'chat',
geometry:
{ pintype: 'Point',
_id: 5ee1f89732fd2c33bccfec56,
coordinates: [Array] },
category: [],
created: 1591867543468,
createdAt: 2020-06-11T09:25:43.478Z,
updatedAt: 2020-06-15T10:01:01.133Z,
user_id: [] }
In my case I don't want it to be null and I am expecting a output like below.
{ _id: 5ee1f89732fd2c33bccfec55,
post_data: 'vvhhh',
likes: 1,
commentsCount: 0,
post_img: null,
isUserLiked: false,
usersLiked: [ 5edf43b93859680cf815e577 ],
exp_date: 2020-06-12T18:30:00.000Z,
has_img: false,
typology: 'chat',
geometry:
{ pintype: 'Point',
_id: 5ee1f89732fd2c33bccfec56,
coordinates: [Array] },
category: [],
created: 1591867543468,
createdAt: 2020-06-11T09:25:43.478Z,
updatedAt: 2020-06-15T10:01:01.133Z,
user_id: { img: 'default-user-profile-image.png',
_id: 5edd103214ce223088a59236,
user_name: 'Puka' }
}
My userSchema is something like below
var userSchema = mongoose.Schema({
//some other fields
user_name: {
type: String,
max: 30,
min: 5
},
img: {
type: String,
default: 'default-user-profile-image.png'
},
//some other fields
});
userSchema.plugin(uniqueValidator);
var userSchemaModel = mongoose.model('users', userSchema);
module.exports = {
userSchemaModel,
}
According to the other answers here I tried using mongoose.Types.ObjectId(userId), but it gives complete empty set.
What can be the problem here and it will be really helpful if someone can help me with this as I'm stuck with this for days.
Update :
post schema
var postSchema = mongoose.Schema({
user_id: {
type: String,
required: true,
ref: 'users'
},
//other fields
var postSchemaModel = mongoose.model('posts', postSchema);
module.exports = {
postSchemaModel,
}
Since the data type of user._id(ObjectId) and post.user_id(String) are not the same you can not join those fields using $lookup. You have to make sure they are the same type before doing $lookup
If you are allowed to change the schema, it's recommended to use ObjectId for post.user_id
var postSchema = mongoose.Schema({
user_id: {
type: mongoose.Types.ObjectId,
required: true,
ref: 'users'
},
// ...other fields
But do remember to change the existing data type to ObjectId as well.
If you are really not allowed to change the schema and existing data, for some reason. You can convert the post.user_id to ObjectId in case that the data contains valid hexadecimal representation of ObjectId (available from MongoDB v4.0)
[
// prior pipeline stages
{ "$sort": { "createdAt": -1 } },
{
"$addFields": {
"user_id": { "$toObjectId": "$user_id" }
}
},
{
"$lookup": {
"from": userSchemaModel.collection.name,
"localField": "user_id",
"foreignField": "_id",
"as": "user_id"
}
},
// other stages
As the _id field in mongodb is stored as type ObjectId but in the posts collection user_id is stored as type string, therefore it is not able find the user information and bring blank array.
To resolve this save a plain string version of _id in user collection when a user is created. for example
{
_id: ObjectId("5ee1f89732fd2c33bccfec55"),
doc_id: "5ee1f89732fd2c33bccfec55",
//other user info
}
and then use this doc_id field in $lookup
{
"$lookup": {
"from": userSchemaModel.collection.name,
"localField": "user_id",
"foreignField": "doc_id",
"as": "user_id"
}
}
In this way both user_id and doc_id will be of type string and will not need any type conversion hassles.

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
}}
])

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 }
])

mongoose populate two simple schemas

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" }
}}
])

Resources