$lookup returns an empty array mongoose - node.js

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.

Related

How can I populate the fields after running the aggregation mongodb

I am trying to populate the fields in the result that I got from running the $geoNear aggregation but I don't know how can I do it. I tried $lookup but it gave me the same result.
const users = await locations.aggregate([
{
$geoNear: {
near: {
type: "Point",
coordinates: req.body.coordinates,
},
maxDistance: req.body.maxDistance,
distanceField: "dist.calculated",
spherical: true,
},
{
$lookup: {
from: "users",
localField: "userId", // getting empty array of users
foreignField: "_id",
as: "users",
},
}, // getting all users data
{
$project: {
_id: 1,
name: 1,
profession: 1,
},
}, // only getting `_id`
},
]);
Result:
{
{
"_id": "63555c4f29820cf3c7667eb5",
"userId": "63555c2629820cf3c7667eac",
"location": {
"coordinates": [
2.346688,
48.858888
],
"type": "Point"
},
"createdAt": "2022-10-23T15:22:55.820Z",
"updatedAt": "2022-10-23T16:08:59.979Z",
"__v": 2,
"dist": {
"calculated": 42.95013302539912
}
}
}
I want to populate the name field from the users schema and to do so I have used the ref of the users schema in the locations schema.
users schema:
{
name: {
type: String,
required: true,
},
email: {
type: String,
required: true,
unique: true,
index: true,
},
}
location Schema:
{
userId: {
type: mongoose.Schema.ObjectId,
ref: "users",
required: true,
},
location: {
type: {
type: String,
enum: ["Point"],
default: "Point",
},
coordinates: {
type: [Number],
default: [0, 0],
},
},
},
How can I populate fields in my aggregation result?
Also, How can I remove all the fields and output the names and email only from the users schema?
You should include your $lookup stage as following:
{
$lookup: {
from: "users",
localField: "userId",
foreignField: "_id",
as: "user",
}
},
{
"$replaceRoot": {
"newRoot": {
"$arrayElemAt": [
"$user",
0
]
}
}
},
{
$project: {
_id: 1,
name: 1,
profession: 1
}
}
It seems like you tried to match an ObjectId to a String (property name), which will never match. Using the aggregation step above you should receive a populated user array containing exactly one entry. After that you can project the result and transform the array to a plain object (if needed).

mongoose move ref document to main document as DTO

I wanna to move sub document to main documents, and return single DTO without any nested document, below is my sample data.js
data.js
const mongoose = require('mongoose');
//city
const citySchema = new mongoose.Schema({
cityName: { type: String, required: true, unique: true },
});
const City = mongoose.model('City', citySchema);
//country
const countrySchema = new mongoose.Schema({
countryName: { type: String, required: true, unique: true },
});
const Country = mongoose.model('Country', countrySchema);
//user
const userSchema = new mongoose.Schema({
username: { type: String, required: true },
city: {
type: mongoose.Schema.Types.ObjectId,
ref: 'City',
required: true,
},
country: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Country',
required: true,
},
createdAt: { type: Date, required: true },
updatedAt: { type: Date, required: true },
});
const User = mongoose.model('User', userSchema);
function getUser(id) {
return User.findById(id)
.populate('city')
.populate('country')
.exec();
};
Current return JSON Response for User:
{
"_id": "6321ac3d14a57c2716f7f4a0",
"name": "David",
"city": {
"_id": "63218ce557336b03540c9ce9",
"cityName": "New York",
"__v": 0
},
"country": {
"_id": "632185bbe499d5505cafdcbc",
"countryName": "USA",
"__v": 0
},
"createdAt": "2022-09-14T10:26:05.000Z",
"__v": 0
}
How do I move the cityName and countryName to main model, and response JSON as below format?
{
"_id": "6321ac3d14a57c2716f7f4a0",
"username": "David",
"cityName": "New York",
"countryName": "USA",
"createdAt": "2022-09-14T10:26:05.000Z",
}
Using aggregation you can try something like this:
db.user.aggregate([
{
"$match": {
_id: "6321ac3d14a57c2716f7f4a0"
}
},
{
"$lookup": {
"from": "city",
"localField": "city",
"foreignField": "_id",
"as": "city"
}
},
{
"$lookup": {
"from": "country",
"localField": "country",
"foreignField": "_id",
"as": "country"
}
},
{
"$addFields": {
"country": {
"$arrayElemAt": [
"$country",
0
]
},
"city": {
"$arrayElemAt": [
"$city",
0
]
}
}
},
{
"$addFields": {
"countryName": "$country.countryName",
"cityName": "$city.cityName"
}
},
{
"$unset": [
"country",
"city"
]
}
])
Here's the playground link.
The other probably simpler way of doing this would be, modify your function like this:
function getUser(id) {
const user = User.findById(id)
.populate('city')
.populate('country')
.exec();
if(user.country) {
user.countryName = user.country.countryName;
}
if(user.city) {
user.cityName = user.city.cityName;
}
delete user.country;
delete user.city;
return user
};

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

Mongoose $group & $lookup aggregation returns empty array with $unwind

I have 2 schemas TravelRoute & Flights. I am trying to find the $min of Flight.fare.total_price and return that result with some details from TravelRoute which is ref-ed in the Flights schema. I am using Mongo 3.4.1 and Mongoose 4.8.5
const TravelRoute = new mongoose.Schema({
departureAirport: {
type: String,
required: true,
trim: true,
},
arrivalAirport: {
type: String,
required: true,
trim: true,
},
durationDays: {
type: Number
},
fromNow: {
type: Number
},
isActive: {
type: Boolean
},
class: {
type: String,
enum: ['economy', 'business', 'first', 'any']
}
}
const Flights = new mongoose.Schema({
route: {
type: mongoose.Schema.Types.ObjectId, ref: 'TravelRoute'
},
departure_date: {
type: Date
},
return_date: {
type: Date
},
fare: {
total_price: {
type: Number
}
}
}
I have the following code:
Flights.aggregate(
{$group: {
_id: {
route: '$route',
departure_date: '$departure_date',
return_date: '$return_date'
},
total_price: {$min: '$fare.total_price'}
}
},
{$lookup: {
from: 'TravelRoute',
localField: '_id.route',
foreignField: '_id',
as: 'routes'
}},
{$unwind: '$routes'},
{$project: {
'routes.departureAirport': 1,
'routes.destinationAirport': 1,
'departure_date': 1,
'return_date': 1,
'total_price': 1,
'_id': 1
}},
{$sort: {'total_price': 1}},
function(err, cheapFlights){
if (err) {
log.error(err)
return next(new errors.InvalidContentError(err.errors.name.message))
}
res.send(cheapFlights)
next()
})
The above returns an empty array [] when I use $unwind. When I comment out the $unwind stage, it returns the following:
[
{
"_id": {
"route": "58b30cac0efb1c7ebcd14e0a",
"departure_date": "2017-04-03T00:00:00.000Z",
"return_date": "2017-04-08T00:00:00.000Z"
},
"total_price": 385.6,
"routes": []
},
{
"_id": {
"route": "58ae47ddc30b175150d94eef",
"departure_date": "2017-04-03T00:00:00.000Z",
"return_date": "2017-04-10T00:00:00.000Z"
},
"total_price": 823.68,
"routes": []
}
...
I'm reasonably new to Mongo and Mongoose. I am confounded by the the fact that it returns "routes" even though I don't project that. And it doesn't return departure_date (etc) even though I ask for it? I am not sure I need $unwind as there will only be one TravelRoute per Flight.
Thanks...
Edit
Here was the final solution:
{$lookup: {
from: 'travelroutes', //<-- this is the collection name in the DB
localField: '_id.route',
foreignField: '_id',
as: 'routes'
}},
{$unwind: '$routes'},
{$project: {
departureAirport: '$routes.departureAirport',
arrivalAirport: '$routes.arrivalAirport',
departureDate: '$_id.departure_date',
returnDate: '$_id.return_date',
'total_price': 1,
'routeID': '$_id.route',
'_id': 0
}},
{$sort: {'total_price': 1}},

Resources