$lookup with condition in mongoose - node.js

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

Related

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
};

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.

aggregate nested array of objects using mongoose

I have the following model and I want to query a specific user on _id field and populate the inbox.messages array with the necessary data that matches the corresponding _id field in the users model and more importantly i also want to group each message by the 'from' field and return that result
const UserSchema = new Schema({
username: {
type: String,
required: true,
},
blockedUsers: {
users: [
{
userId: {type: Schema.Types.ObjectId, ref: 'User', required: true },
}
]
},
favorites: {
users: [
{
userId: {type: Schema.Types.ObjectId, ref: 'User', required: true },
}
]
},
profileViews: {
views: [
{
userId: {type: Schema.Types.ObjectId, ref: 'User', required: true },
date: {type: Date}
}
]
},
inbox: {
messages: [
{
messageId: {type: Schema.Types.ObjectId},
from: {type: Schema.Types.ObjectId, ref: 'User', required: true },
content: {type: String, required: true},
date: {type: Date}
}
]
},
images: {
"imagePaths": [
{
imageId: {type: Schema.Types.ObjectId},
path: { type: String, required: true},
date: {type: Date}
}
],
}
})
what I have so far
let incomingId = '5e29fd75fdfd5320d0e42bc4';
let myUser = await User.aggregate([
{ $match: {"_id": mongoose.Types.ObjectId(incomingId) }},
{ $lookup: { }}
])
Not sure exactly what to put in the $lookup field or if this is even correct.
As a sample I would like the documents to look like:
[
{
"from": "5e240f7480a24e07d832c7bd",
"username":"hable0",
"images": {
imagePaths: [
'images/2020-09-24-Z_34234342_12.jpg'
],
},
"inbox": {
"messages": [
{
"messageId": "5e2a110a21c64d63f451e39e",
"content": "Message content",
"date": "2020-01-23T21:32:58.126Z"
},
{
"messageId": "5e2a111321c64d63f451e3a0",
"content": "Message content",
"date": "2020-01-23T21:33:07.378Z"
},
{
"messageId": "5e2a112321c64d63f451e3a2",
"content": "Message content",
"date": "2020-01-23T21:33:23.036Z"
}
]
}
}
]
You could try the following pipeline with aggregate().
Find the document that matches the id
Unwind inbox.messages
Group by from field
Perform a $lookup to get another document
Perform a $unwind to destruct the array
Specify fields to be included in the output
let myUser = await User.aggregate([
{
$match: { "_id": mongoose.Types.ObjectId(incomingId) }
},
{
$unwind: "$inbox.messages"
},
{
$group: {
_id: { from: "$inbox.messages.from" },
messages: {
$push: {
messageId: "$inbox.messages.messageId"
// Add more info of the message here as needed
}
}
},
},
{
$lookup: {
from: "User",
localField: "_id.from",
foreignField: "_id",
as: "extraUserInfo"
}
},
{
$unwind: "$extraUserInfo"
},
{
$project: {
_id: 0,
from: "$_id.from",
inbox: { messages: "$messages" },
username: "$extraUserInfo.username",
images: "$extraUserInfo.images"
}
}
]);
Sample output:
{
"from": "user1",
"inbox": {
"messages": [{
"messageId": "message1-from-user1"
}]
},
"username": "user1-username",
"images": {
"imagePaths": ["image-path-user1"]
}
} {
"from": "user2",
"inbox": {
"messages": [{
"messageId": "message1-from-user2"
}, {
"messageId": "message2-from-user2"
}, {
"messageId": "message3-from-user2"
}]
},
"username": "user2-username",
"images": {
"imagePaths": ["image-path-user2"]
}
} {
"from": "user3",
"inbox": {
"messages": [{
"messageId": "message1-from-user3"
}, {
"messageId": "message2-from-user3"
}]
},
"username": "user3-username",
"images": {
"imagePaths": ["image-path-user3"]
}
}
Hope this answers part of your question. Though I'm not very clear how you would like to populate the messages array with the user info who sent the messages. But you can perform a $lookup() with a pipeline after $group() operation to attach additional info from the sender to the result.
Read more about $unwind, $group, $project and $lookup.

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

Resources