Mongoose sorting by populated field - node.js

In my Task schema I have fields like this:
{
name: String
},
{
user: ObjectID
}
I need to sort tasks. If I want to do it by name field it's easy:
await Tasks.find().sort({name: 1})
That works. But the problem is when I want to sort by user.name. To get fields from User I can populate them, so:
await Tasks.find().populate('user', 'name').sort({'user.name': 1})
And it doesn't work. I cannot sort by fields added by populate function. I've been searching a lot in documentation and in other users' questions. I've found that I can pass sorting option to populate function but it doesn't work also. I guess it sort fields in populated field.
When I've tried to use aggregate with lookup like this:
Tasks.aggregate([{ $lookup: {
{
from: 'User',
localField: 'user',
foreignField: '_id',
as: 'someField'
}}}])
it returns someField: []
Can somebody help me? Thanks a lot!

In aggregate query, you should reference your collection with it's real name, NOT with the model name. So, instead of from: 'User', it should be from: 'users':
Tasks.aggregate([
{
$lookup: {
from: 'users',
localField: 'user',
foreignField: '_id',
as: 'user'
}
},
{
$set: {
user: { $first: '$user' }
}
},
{
$sort: {
'user.name': 1
}
}
])

Related

Aggregate data from different collections

I am currently working on a project that has the following schema using mongoose.
User schema
const userSchema = {
name: string
email: string
medicalVisits: [{type: Schema.ObjectId, ref: "records"}]
createdAt: Date
}
Records schema
const recordSchema = {
medication: [String],
rating: Number
user: [{type: Schema.ObjectId, ref: "user"}]
tests: [{type: Schema.ObjectId, ref: "tests"}]
createdAt: Date
}
Tests schema
testScore: Number
answers: Object
user: [{type: Schema.ObjectId, ref: "user"}]
createdAt: Date
From the little schema above, I have a setup where a patient can take tests multiple times and their respective tests are saved in the Tests collection. Also, the date is recorded for all tests they take. A doctor can request to see a patient's record, in this case, the patient has only one record document that has their tests records embedded in them. Currently, I am faced with the problem of getting a patient's newest and oldest test score alongside their initial details.
I can do a mongoose populate to get all information regarding a user, e.g
await User.findById(userId).populate({
path: "medicalVisits"
model: "records"
populate: {
path: "tests"
model: "test"
}
})
And that operation returns the patient's record and all the tests they have taken since they signed up to date. But when I make such a call to the Database, I just want to retrieve the patient's newest and oldest score. In other words, I want to get the patients, Initial test score, and their most recent test score. I am new to Mongoose aggregation, I tried to use the Mongoose aggregate function, but it returns an empty array, I guess I am missing something.
Currently, this is what my aggregate pipeline looks like.
const user = await Doctor.aggregate([
{ $match: { _id: docId } },
{
$lookup: {
from: "users",
localField: "patients",
foreignField: "_id",
as: "patients",
},
},
{ $unwind: "$patients" },
{ $unwind: "$patients.medicalVisits" },
{
$lookup: {
from: "records",
localField: "patients.user",
foreignField: "_id",
as: "patientRecord",
},
},
{ $unwind: "$patientRecord" },
// { $sort: { createdAt: 1 } },
{
$group: {
_id: docId,
user: { $last: "$patients" },
record: { $last: "$patientRecord"}
},
},
]);
return user[0];
From the above snippet, my intention is:
given a doctor Id, they can see a list of their patients and also see their newest and oldest test score.
Expected Output
const output = {
userId: 6e12euido....
name: "John doe"
email: "john#john.com"
rating: 2
initialTestScore: 10
recentTestScore: 30
}
How do I go about this? Or what could be a better alternative? Thank you very much.
tried my best to understand your case, and I think your aggregation pipeline should be like:
const patientsWithNewestRecord = await Doctor.aggregate([
{ $match: { _id: docId } },
{
$lookup: {
from: "users",
localField: "patients",
foreignField: "_id",
as: "patients",
},
},
// one patient, per doc
{ $unwind: "$patients" },
// one patient with all his/her visit records, per doc
{
$lookup: {
from: "records",
localField: "patients.medicalVisits",
foreignField: "_id",
as: "patientRecords",
},
},
// one patient with one visit record, per doc
{ $unwind: "$patientRecords" },
// sort by patient first, createdAt second
{ $sort: { 'patientRecords.user': 1, 'patientRecords.createdAt': 1 } },
{
$group: {
_id: { patient: '$patientRecords.user' },
user: { $last: "$patients" },
record: { $last: "$patientRecords"}
},
},
]);
this pipeline return a list of a doctor's patients and also see their newest test record. Oldest test record should be in similar war.
Based on these collections (as I understand them from your question):
// doctor collection:
{ _id: "doc1", patients: ["user1"] }
// user collection:
{
_id: "user1", name: "John", email: "john#gmail.com",
medicalVisits: ["record1", "record2"]
}
// record collection:
{ _id: "record1", rating: 2, tests: ["test1", "test2"] }
{ _id: "record2", rating: 4, tests: ["test3"] }
// test collection:
{ _id: "test1", testScore: 12, createdAt: ISODate("2021-12-04") }
{ _id: "test2", testScore: 9, createdAt: ISODate("2021-12-05") }
{ _id: "test3", testScore: 15, createdAt: ISODate("2021-12-24") }
we can apply:
db.doctor.aggregate([
{ $match: { _id: "doc1" } }
{ $lookup: {
from: "user",
localField: "patients", foreignField: "_id",
as: "patients"
}},
{ $unwind: "$patients" }, { $unwind: "$patients.medicalVisits" },
{ $lookup: {
from: "record",
localField: "patients.medicalVisits", foreignField: "_id",
as: "records"
}},
{ $unwind: "$records" }, { $unwind: "$records.tests" },
{ $lookup: {
from: "test",
localField: "records.tests", foreignField: "_id",
as: "tests"
}},
{ $unwind: "$tests" },
{ $sort: { "tests.createdAt": 1 } },
{ $group: {
_id: "$patients._id",
name: { $first: "$patients.name" },
email: { $first: "$patients.email" },
rating: { $first: "$records.rating" },
initialTestScore: { $first: "$tests.testScore" },
recentTestScore: { $last: "$tests.testScore" }
}},
{ $set: { "userId": "$_id" } }, { $unset: "_id" }
])
in order to extract:
{
userId: "user1",
name: "John",
email: "john#gmail.com",
rating: 2,
initialTestScore: 12,
recentTestScore: 15
}
Differences compared to your query:
I $lookup the test collection as it seems you information from there to get both test dates and test scores.
I $sort by test date (createdAt) before the $group by user such that we'll be able to define the right order for selecting the $first and $last test scores.
I extract user's information by using a $first on each group on user's field (since all unwind records for a given user have the same user information): for instance email: { $first: "$patients.email" }
I extract the $first and $last test scores for a user as defined by the $sort order: initialTestScore: { $first: "$tests.testScore" } and recentTestScore: { $last: "$tests.testScore" }.
I finally $set/$unset to rename the _id field into userId
I would suggest to do the following once you have the userId / patientId:
Get their tests (all) from the database in a sorted order
Take the first and last element of the array for your initial and final test report based on the sorting order you have applied (ascending or descending)
If you can just retrieve the user details and all the tests without any sorting, then you can proceed the following way:
Run a loop through all the tests and sort the tests according to test date.
Take the first and last element of the array for your initial and final test report based on the sorting order you have applied (ascending or descending)
You will not be performing the operations on DB end, so there might be a minor speed issue, but the difference would still come out to be in milliseconds unless a user takes a billion tests.
Let me know if this helps, let me know if it doesn't

how too use $lookup in mongoose to fetch array object

I have the following aggregation and It does not return the user profiles properly
const newConverSation = await Messenger.Messenger.aggregate([
{ $match: {
users: mongoose.Types.ObjectId("6084036ad4d4cd40a47afba4")}},
{ $sort: { updatedAt: -1 } },
{
$group: {
_id: { $setUnion: "$users" },
message: { $first: "$$ROOT" }
}
},
{
$lookup: {
from: 'users',
localField: 'users',
foreignField: '_id',
as: 'users'
}
}
])
In the outcome, there is an users array like this
"users" : [
ObjectId("60841d03f6ccad2b0400f619"),
ObjectId("6084036ad4d4cd40a47afba4")
],
and I just want to fetch user profiles depending on these two Id's but it does not return profiles in the current way.
Try adding the .populate after the .find property where you want to show this data.

Filter nested populated array

Lets say I have a classroom that look like this:
{
courseName: String,
teacher: ObjectId,
students: [{type: ObjectId, ref: 'Student'}]
}
and the "Student" schema looks like this:
{
personalInfo: PersonalInfo,
proffesionalInfo: {type: ObjectId, ref: 'ProffesionalInfo'}
}
and the "ProffesionalInfo" schema looks like this:
{
allgrades: [Number],
averageGrade: Number,
...
...
}
I have the classroom _id and I want to get only the students that their average is lower than 90.
The way I am doing it now is like this:
Classroom.findOne({ _id })
.populate("teacher", "_id name")
.populate({
path: 'ridesstudents',
select: 'proffesionalInfo',
populate: [
{
path: "proffesionalInfo",
select: "allgrades averageGrade"
}
]
})
.then(classroom => {
classroom.students = classroom.students.filter(student => student.proffesionalInfo.averageGrade < 90);
resolve(user);
})
.catch(err => reject(err));
As you can see I am filtering after bringing all student.
In case I have 10 million student and only 2 with grade less than 90 It is "not good" approach.
How can I filter the students within the query and not bringing all of them and then filtering??
side note - the data above is only an exaple which target is to bring the general idea. dont get into details.
Try using aggregate:
let classroom = Classroom.aggregate([
{ $match: { _id: classroomId } },
{ $lookup: {
from: "students",
localField: "_id",
foreignField: "students",
pipeline:[
{ $lookup: {
from: "professionalInfo",
localField: "_id",
foreignField: "proffesionalInfo",
as:"professionalInfo"
}},
{ $match: {$lte: ["professionalInfo.averageGrade",90]}}
]
as: "students"
}}])

Query a document reference in mongodb if condition is met

I have two mongo collections within my database named user and order. Within the user collection, there is an array of object references to orders. (the code snippet is a reduced down version of my schema for each collection).
User ({
user_id
username
email
firstname
surname
...
orders : [{type: Schema.Types.ObjectId, ref: "ordersmodel"}]
})
Order ({
order_id
current_status
date_ordered
...
})
What I am looking to do is to access the order information for a specific user when passed a user_id. This was my thinking so far:
User.aggregate([
{
$lookup: {
from: 'order',
localfield: 'orders',
foreignField: 'order_id',
as: 'order'
}},
{
$unwind: '$order'
},
{$project: {
_id: 0,
order_id: '$order.order_id',
status: '$order.status'
}}
]).toArray();
and am not sure what to do next in order to return the orders for a specific user.
when you use ref in schema you can using populate like this:
let result = await User.findById(user._id).populate("orders").lean()
console.log(result )

Mongoose join two different collections with different foreign key

Users Schema:
{
username: "milkeypony",
_id: "_mongodbID",
id: "random_30_characters_string"
...
}
Blog Schema
{
title: "_title",
_id: "_mongodbID",
author: "random_30_characters_string"
...
}
The Blogs.author is the same ID as with in Users.id
And what I'm trying to do is when I use Blogs.findOne() to fetch some blog post, Mongoose will also help me fetch some user data.
And I already successfully done this with raw Mongo shell command
db.blogs.aggregate([
{
$lookup: {
from: "users",
localField: "author",
foreignField: "id",
as: "author"
}
}
])
And I try the mongoose populate method, but it didn't work out for me
make sure Blogs schema like have
author:{
type:Schema.Types.ObjectId,
ref: 'Users'
}
and populate like below
Blogs.findAll({})
.populate({
path:author
})
.exec((err, blogs)=>{
console.log(err,blogs);
}))
more info check offical doc

Resources