Mongoose sorting subdocument populated - node.js

I have a Mongoose model which have paths which need to be populated :
var adsSchema = new Schema({
price: { type: Number, min: 0, required: true },
author: { type: ObjectId, ref: 'Users', required: true },
title: { type: String, required: true },
date: { type: Date, default: Date.now },
offers: [{
price: { type: Number, required: true },
date: { type: Date, default: Date.now },
offerBy: { type: ObjectId, ref: 'Users', required: true }
}],
category: { type: ObjectId },
images: [{
type: ObjectId,
ref: 'Images'
}],
videos: [String]
});
In some GET request, I need to populated numerous fields as I was saying, in particular offers with a sorting by 'price' by ascending.
As the documentation of Mongoose is showing there (http://mongoosejs.com/docs/api.html#query_Query-populate), you can sort by a subpath.
My problem is that it isn't the case.
There is my code which is doing it :
Ads.findOne({ _id: adId })
.populate({ path: 'author', select: 'firstName lastName' })
.populate('images')
.populate({ path: 'offers', options: { sort: { 'price': -1 } })
.populate({ path: 'category', select: 'name' })
.exec(function (err, ad) {
if (err)
deferred.reject({ status: 500, message: 'Database error.' });
else if (!ad)
deferred.reject({ status: 500, message: 'The ad doesn\'t exist!.' });
else {
deferred.resolve(ad.toJSON());
}
})
I have read as much as possible questions/answers giving here or in the Mongoose mailing-list :
https://stackoverflow.com/a/19450541/6247732
I know it's not possible to sort documents results from subdocument result, OK. But I just want to sort that subdocument only, and only this one. Some responses here seems to say it's possible :
https://stackoverflow.com/a/31757600/6247732
https://stackoverflow.com/a/16353424/6247732
I have 2 questions around it :
Is it possible (as the Mongoose documentation is written) to sort a subdocument during the population ?
Is it linked to the fact I didn't just put a ObjectId linked to an other Schema ?
Thanks for your answer ;)

Personally, I do not use 'populate' as possible as I can. I had experienced many trouble in the past. So, I do not know how to sort while populating.
Instead of using populate, you can use $lookup method on aggregation and you can easily sort any field. Its almost same way with 'population'.
Ads.aggregate([
{
$lookup: {
from: 'Users',
localField: 'author',
foreignField: '_id',
as: 'authorName'
}
},
{
$lookup: {
from: 'Images',
localField: 'images',
foreignField: '_id',
as: 'image'
}
},
{
$lookup: {
from: 'categories',
localField: 'category',
foreignField: '_id',
as: 'category'
}
},
{
$project: {
'authorName.firstName': 1,
'image.imagePath': 1,
'category.categoryName': 1,
'_id': 0
}
},
{
$sort : { 'authorName.firstName' : -1}
}
]).exec(function(err, adss) {
});
I did not check all fields properly. Please implement this way to your model and hope this can give you some idea. Good luck

I found the solution, and it was the response of my second question.
When you are writing a subdocument in your schema like in my question, you are not creating a "relation" between one Model and an other one. Which means that the populate method doesn't work at all.
You have to make a reference to an ObjectId and the model associated to be able to use populate, and so the options which offers you Mongoose. This is an example :
var adsSchema = new Schema({
price: { type: Number, min: 0, required: true },
author: { type: ObjectId, ref: 'Users', required: true },
offers: [{
type: ObjectId,
ref: 'Offers'
}],
videos: [String]
});
Now, the .populate method is working, because Mongoose (and so MongoDB) can perform the subquery on an other Collection and sort it. It wasn't the case before because it was in the same Model, so Mongoose doesn't perform a subquery on the same Collection (as I seems to understand it).

Related

How do I find object based on child populated property on mongoose

I'm new in mongoose.
I have a Schema like this:
const sessionSchema = mongoose.Schema({
createdBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
registers: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Register',
},
],
date: {
type: Date,
default: Date.now,
immutable: true,
},
});
And register model is this one:
const registerSchema = mongoose.Schema({
sets: [
{
weight: {
type: Number,
},
weightUnit: {
type: String,
default: 'kg',
},
repetitions: {
type: Number,
},
duration: {
type: Number,
},
},
],
session: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Session',
},
exercise: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Exercise',
},
creationDate: {
type: Date,
default: Date.now,
immutable: true,
},
});
I want to find all the sessions created by a user and has an register with an specificied exercise id. I tried this:
const result = Session.find({createdBy: userId, 'registers.exercise': exerciseId}).populate('registers');
But doesn't work. ¿Any suggestion?
Thanks :P
Try this:
Session.aggregate([
{
$match: { createdBy: userId }
},
{
$lookup: {
from: 'Register',
localField: 'registers',
foreignField: '_id',
as: 'registers'
}
},
{
$match: { 'registers.exercise': exerciseId }
}
])
.then((result) => {
console.log('Result: ', result);
})
.catch((err) => {
console.log('Error: ', err);
});
If you are not familiar with MongoDB aggregation framework, the query above might be a bit foreign to you. However, for cross collection queries like you described in your question, the aggregation framework is your best shot(for now).
The $match is basically doing what you would do with a Model.find(), while the $lookup is doing what you would do with a Model.<query>.populate()
You can read more about MongoDB aggregation framework here.

How to select parent based on children's value using mongoose and GraphQL?

I'm trying to achieve something equivalent to a conditional JOIN query, but then with GraphQL.
I'm using Mongoose for my db model and MongoDB as database.
I'll illustrate my problem with the following graphQL schema:
type Booking {
_id: ID!
client: Client!
rooms: Room!
activities: Activity!
nrOfAdults: Int!
arrivalDate: String!
departureDate: String!
}
type Room {
_id: ID!
name: String!
contents: String
priceNight: Float!
maxAdults: Int!
reservations: [Booking]
}
The Mongoose schema:
const bookingSchema = new Schema(
{
client: {
type: Schema.Types.ObjectId,
ref: 'Client'
},
rooms: [{
type: Schema.Types.ObjectId,
ref: 'Rooms'
}],
nrOfAdults: {
type: Number,
required: true
},
arrivalDate: {
type: Date,
required: true
},
departureDate: {
type: Date,
required: true
}
},
{ timestamps: true }
);
const roomSchema = new Schema({
name: {
type: String,
required: true
},
priceNight: {
type: Number,
required: true
},
maxAdults: {
type: Number,
required: true
},
reservations: [
{
type: Schema.Types.ObjectId,
ref: 'Booking'
}
]
});
I can query rooms, for example, if I want to get the rooms for 3 or more adults I run:
Room.find({
maxAdults: { $gte: 3 }
});
This works fine.
However, I'd also like to show the available rooms, which means I need to impose a condition on the booking objects which are hold in reservation.
I thought this would be fairly easy, using something like:
Room.find({
maxAdults: { $gte: 3 },
reservations: { $elemMatch: { arrivalDate: { $gte: *some date*}}}
});
But it returns an empty array, while it should return some value, based on the data in mongodb:
To make things a little more clear, I'd like to achieve the same outcome as the following SQL query would give me:
SELECT *
FROM room
JOIN booking ON room.id = booking.roomId
WHERE
room.maxAdults >= 3
AND
(
booking.arrivalDate > CAST('2020-05-15' AS DATE)
OR
booking.departureDare < CAST(2020-05-06' AS DATE)
)
Assuming that you have saved the values similar to what you have mentioned in the mongoose schema.
Explore the how to do join in mongodb. Aim is to do the join before executing the query on the sub fields from the different collection.
Relevant Answer: How do I perform the SQL Join equivalent in MongoDB?
I suggest using aggregate pipeline for accomplishing what you want.
Suggested code :
Room.aggregate([
{
$match: {
maxAdults: { $gte: 3 }
}
},
{
$lookup: {
from: "bookings",
localField: "reservations",
foreignField: "_id",
as: "booking"
}
},
{
$unwind: '$booking'
},
{
$match: {
booking.arrivalDate: { $gte: *some date* }
}
},
])

mongoose aggregation not returning expected result

I am currently learning aggregation and some related methods, after following mongoose and mongodb docs i tried it. but i am having problem.
Follow.aggregate([
{ $match: { user: mongoose.Types.ObjectId(userId) } },
{ $unwind: '$followers' },
{
$lookup: {
from: 'accounts',
localField: 'followers',
foreignField: '_id',
as: 'followers'
}
},
{ $project: { name: 1, photo: 1 } },
]).exec((err, followers) => {
if (err) throw err;
console.log(followers);
res.send(followers);
});
I want to get the followers of that userID and select the followers names and photo, but i am only getting the objectid of the matched document
[ { _id: 5bfe2c529419a560fb3e92eb } ]
expected output
[ { _id: 5bfe2c529419a560fb3e92eb , name: 'john doe", photo: 'cat.jpg'} ]
Follow Model
const FollowSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'Account',
required: true,
},
following: [{
type: Schema.Types.ObjectId,
ref: 'Account',
required: true,
}],
followers: [{
type: Schema.Types.ObjectId,
ref: 'Account',
required: true,
}],
});
When the $project stage is wrong you would get that result. Try removing it first and see the output. To me it seems you should have something like this there:
{ $project: { "followers.name": 1, "followers.photo": 1 } },

Check against multiple fields when using Mongoose Virtual schema?

I am a little stuck with trying to figure out the best way to populate a virtual schema in my db. In this instance, I want to check against multiple fields but I do not know any way to do this or alternative ways.
I had hoped I could just use some string arrays in the 'localField' & 'foreignField' keys of the virtual schema but this was not the case.
The user has a 'Series' _id saved, and the virtual schema 'leagues' gets all leagues that the user is entered into too, the catch is a league belongs to different Series. I want to only retrieve the leagues that the user is entered into & that match up with the user's series _id also...
As you can see currently this virtual schema just returns all the leagues the user is entered into regardless of the series. :(
Any ideas? I have been very confused about how to achieve this.
const schema: mongoose.Schema = new mongoose.Schema({
_id: {
type: mongoose.Schema.Types.ObjectId,
auto: true
},
username: {
type: String,
unique: true,
},
series: {
ref: 'Serie',
type: mongoose.Schema.Types.ObjectId,
autopopulate: {
maxDepth: 1,
select: ['title']
}
}
});
schema.virtual('leagues', {
ref: 'League',
localField: '_id',
foreignField: 'users',
autopopulate: {
maxDepth: 1,
select: ['title', 'engineSize', 'host', 'series']
}
});
The league schema looks like this
const schema: mongoose.Schema = new mongoose.Schema({
_id: {
type: mongoose.Schema.Types.ObjectId,
auto: true
},
title: String,
series: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Serie',
},
users: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
}]
});
This was my solution to checking against multiple fields in mongoose using a getter.
schema.virtual('motoduel').get(function () {
return motoduelModel.findOne({
event: this.series.upcomingEvent._id,
riderGroup: this.riderGroup._id
});
});

Populate nested array in mongoose - Node.js

These are my schemas (Topic is parent and contains a list of 'Thought's):
var TopicSchema = new mongoose.Schema({
title: { type: String, unique: true },
category: String,
thoughts: [ThoughtSchema]
}, {
timestamps: true,
toObject: {virtuals: true},
toJSON: {virtuals: true}
});
var ThoughtSchema = new mongoose.Schema({
text: String,
author: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},
votes:[{
_id:false,
voter: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},
up: Boolean,
date: {type: Date, default: Date.now}
}]
}, {
timestamps: true,
toObject: {virtuals: true},
toJSON: {virtuals: true}
});
....
I am trying to read the thought's author and change my get Topic api like this:
...
var cursor = Topic.find(query).populate({
path: 'thoughts',
populate: {
path: 'author',
model: 'User'
}
}).sort({popularity : -1, date: -1});
return cursor.exec()
.then(respondWithResult(res))
.catch(handleError(res));
...
But the author is null.. i also do not get any error in the console. What is wrong here?
Edit: Actually i do not need the Thought as a schema, it does not have its own collection in database. It will be saved in topics. But in order to use timestamps option with thoughts, i needed to extract its contents to a new local schema ThoughtSchema. But i have now defined the contents of thoughtSchema directly in the thoughts array of topics, it still does not work.
Edit2: This is the cursor object just before it is executed. Unfortunately i cannot debug in Webstorm, this is a screenshot from node inspector:
Did you try using Model.populate?
Topic.find(query).populate('thoughts')
.sort({popularity : -1, date: -1})
.exec(function(err, docs) {
// Multiple population per level
if(err) return callback(err);
Thought.populate(docs, {
path: 'thoughts.author',
model: 'User'
},
function(err, populatedDocs) {
if(err) return callback(err);
console.log(populatedDocs);
});
});
UPDATE:
You can try with deep populate like this:
Topic.find(query).populate({
path: 'thoughts',
populate: {
path: 'author',
model: 'User'
}
})
.sort({popularity : -1, date: -1})
.exec(function(err, docs) {
if(err) return callback(err);
console.log(docs);
});
How about
Topic.find(query).populate('thoughts')
.sort({popularity : -1, date: -1})
.exec(function(err, docs) {
// Multiple population per level
if(err) return callback(err);
Topic.populate(docs, {
path: 'thoughts.author',
model: 'User'
},
function(err, populatedDocs) {
if(err) return callback(err);
console.log(populatedDocs);
});
});
These are the schemas :
var TopicSchema = new mongoose.Schema({
title: { type: String, unique: true },
category: String,
thoughts: [ThoughtSchema]
}, {
timestamps: true,
toObject: {virtuals: true},
toJSON: {virtuals: true}
});
var ThoughtSchema = new mongoose.Schema({
text: String,
author: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},
votes:[{
_id:false,
voter: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},
up: Boolean,
date: {type: Date, default: Date.now}
}]
}, {
timestamps: true,
toObject: {virtuals: true},
toJSON: {virtuals: true}
});
Did you try Aggregation Instead of Populate. Aggregate Makes much easier for populating the embedded data using $lookup. Try the below code.
UPDATE
Topic.aggregate([{$unwind: "$thoughts"},{ $lookup: {from: 'users', localField: 'thoughts.author', foreignField: '_id', as: 'thoughts.author'}},{$sort:{{popularity : -1, date: -1}}}],function(err,topics){
console.log(topics) // `topics` is a cursor.
// Perform Other operations here.
})
Explanation:
$unwind: Deconstructs an array field from the input documents to output a document for each element.
$lookup: The $lookup stage does an equality match between a field from the input documents with a field from the documents of the “joined” collection. The lookup does the population's job.
$lookup works like
from : this says from which collection the data needs to be populated.(users in this scenario).
localField : this is the local field which needs to be populated. (thoughts.author in this scenario).
foreignField : this is the foreign field present in the collection from which data needs to be populated (_id field in users collection in this scenario).
as : this is the field as what you want to display the joined value as. (this will project thoughts.author's id as thoughts.author document).
Hope this works.

Resources