Cascaded join using mongoose node js - node.js

I am trying to fetch data from MongoDB using Node Js. I have three schemas: Projects, Users, and Teams.
I need to retrieve the project details based on it's type with the worker users.
I got stuck in making join for these schemas:
Projects:
const Project = new Schema({
projectName: { type: String, required: true, trim: true },
type: { type: String, required: true, trim: true },
teamID: { type: Schema.Types.ObjectId, required: true },
});
Teams
const Team = new Schema({
teamId: { type: Schema.Types.ObjectId, required: true, trim: true },
users: { type: [Schema.Types.ObjectId], required: true, trim: true },
teamName: { type: String, required: true },
});
Users:
const User = new Schema({
userId: { type: Schema.Types.ObjectId, required: true, trim: true },
name: { type: String, required: true, trim: true },
profilePicture: { type: String, required: true, trim: true },
});
I am trying to find a way to get
[
{
projectName: "s",
type: "w",
users: ["Jon", "Ali", "Mark"]
},
{
projectName: "a",
type: "w",
users: ["Jon", "Mark"]
}, {
projectName: "s",
type: "w",
users: ["Jon", "Ali", "Mark"]
},
]
I tried to use $lookup, but I can not use it because the relation is complex many to many relations.
Is there a way more efficient than retrieving all users, all teams, and all projects?

I think there is no other efficient way except aggregation and without lookup we can't join collections, You can use nested lookup,
$match condition for type
$lookup to join Team collection using teamID
$match teamID
$lookup to join User collection using users array
$project to convert user's name array using $map
$addFields to get users array in users using $arrayElemAt
db.Project.aggregate([
{ $match: { type: "w" } },
{
$lookup: {
from: "Team",
let: { teamID: "$teamID" },
as: "users",
pipeline: [
{ $match: { $expr: { $eq: ["$$teamID", "$teamId"] } } },
{
$lookup: {
from: "User",
localField: "users",
foreignField: "userId",
as: "users"
}
},
{
$project: {
users: {
$map: {
input: "$users",
in: "$$this.name"
}
}
}
}
]
}
},
{ $addFields: { users: { $arrayElemAt: ["$users.users", 0] } } }
])
Playground
Second possible way, you can combine $project and $addFields stages in single stage,
{
$addFields: {
users: {
$arrayElemAt: [
{
$map: {
input: "$users.users",
in: "$$this.name"
}
},
0
]
}
}
}
Playground

Related

How to find sum in Mongo aggregate method with match in different collection in an array of object?

I am trying to find the sum by matching array of object using mongoose. I have 2 collection such as
const accountSchema = new mongoose.Schema({
groupId: {
type: Number,
required: true
},
account_no: {
type: String,
required: true
},
account_name: {
type: String,
required: true
},
opening_balance: {
type: Number,
default: 0
}
})
And second collection as:
const mongoose = require('mongoose')
const AutoIncrement = require('mongoose-sequence')(mongoose);
const accountJournalSchema = new mongoose.Schema({
journal_no: {
type: Number
},
user: {
type: mongoose.Schema.ObjectId,
ref: 'Users',
required: [true, 'User ID is required.'],
},
groupId: {
type: Number,
required: true
},
date: {
type: Date,
required: true
},
receipt: [
{
account_no: {
type: mongoose.Schema.ObjectId,
ref: 'Accounts',
required: true
},
debit: {
type: Number,
default: 0
},
credit: {
type: Number,
default: 0
},
}
]
})
And my aggregate method is:
await Accounts.aggregate([
{
$match: {
$and: [
{ groupId: {$eq: parseInt(req.params.group_id)} },
{ 'Account_jour.groupId': { $eq: parseInt(req.params.group_id) } }
]
}
},
{ unwind: '$Account_jour' },
{
$lookup: {
from : 'account_journals',
localField: '_id',
foreignField: 'receipt.account_no',
as: 'Account_jour'
}
}
])
I am getting error from the above statement:
Arguments must be aggregate pipeline operators
And after solving the issue I also want to find the sum of debit and credit.
Thank you!!
Try this:
await Accounts.aggregate([
{
$match: {
$and: [{ groupId: { $eq: parseInt(req.params.group_id) } }, { "Account_jour.groupId": { $eq: parseInt(req.params.group_id) } }]
}
},
{ $unwind: '$Account_jour' },
{
$lookup: {
from : 'account_journals',
localField: '_id',
foreignField: 'receipt.account_no',
as: 'Account_jour'
}
}
])

how to sort after lookup and project aggregation in mongodb?

I have 2 Collections one for users and other for posts(Posts colllection have _id of users as postedBy).
In users collection each user is having friends array which have _id of users in it.I want to get all the Posts of My friends and mine post in sorted order(sorted By CreatedAt).
This is my Userschema in which i am having friends array of mongoose object type ref to user collection,
here i'm storing users id who is friend.
`//UserSchema
const userSchema = new Schema({
profileImg : {
type: String,
},
name: {
type: String,
required: [true, 'Please Enter Your Name!']
},
about: {
type: String,
},
email: {
type: String,
required: [true, 'Please Enter Email!'],
unique: [true, 'Already Registered!'],
match: [/\S+#\S+\.\S+/, 'is invalid!']
},
password: {
type: String,
required: [true, 'Please Enter Your Password!'],
},
friends: [{
type: mongoose.Types.ObjectId,
ref: 'USER'
}],
address: {
line1: {
type: String,
required: [true, 'Please Enter Your Address!']
},
line2: {
type: String
},
city: {
type: String,
required: [true, 'Please Enter Your City!']
},
state: {
type: String,
required: [true, 'Please Enter Your State!']
},
}
}, { timestamps: true })
This is my Post Schema where userId is ref to users collection and here the _id of user who is uploading post is saved.
//POST SCHEMA
const postSchema = new Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: "USER",
required: true
},
postImage: {
type: String,
required: [true, 'Please Upload the Image!']
},
caption: {
type: String
},
likes: [likeSchema],
comments: [commentSchema]
}, { timestamps: true })
`
What I am Doing:
1st I am finding the user through _id
2nd from found user's friend array ,lookup in posts collection to get post of friends
3rd Now to get owns post again look up in post collection with own _id
4th concat the both array obtain from friend post and user post as Posts
Now here after step 4 i want to sort the Posts by createdAt but its not working..
How to sort it?
const posts = await User.aggregate([
{
$match: {
_id: mongoose.Types.ObjectId(req.user_id)
}
},
{
$lookup: {
from: "posts",
localField: "friends",
foreignField: "userId",
as: "friendposts"
}
},
{
$lookup: {
from: "posts",
localField: "_id",
foreignField: "userId",
as: "userposts"
}
},
{
$project: {
"Posts": {
$concatArrays: ["$friendposts", "$userposts"]
},
_id: 0
}
}
])
you can use 1 lookup instead of 2 .
for sorting you have 3 ways
sort in the code level (using sort function)
use $unwind $sort and group (if mongo db version is less than 5.2)
use $sortArray (applicable for mongodb version 5.2+)
if using 2nd method.
User.aggregate([
{
'$match': {
'_id': mongoose.Types.ObjectId(req.user_id)
}
}, {
'$addFields': {
'users': {
'$concatArrays': [
'$friends', [
mongoose.Types.ObjectId(req.user_id)
]
]
}
}
}, {
'$lookup': {
'from': 'posts',
'localField': 'users',
'foreignField': 'userId',
'as': 'posts'
}
}, {
'$unwind': {
'path': '$posts'
}
}, {
'$sort': {
'posts.createdAt': -1
}
}, {
'$group': {
'_id': '$_id',
'posts': {
'$push': '$posts'
},
'name': {
'$first': '$name'
}
}
}
])
you can add any other field needed in final response like wise i added name .

how to select a property depending another property - mongodb

I'm using mongoose. I want to select users property depending on another property at this here type.
for example when my type is private I want to select users.
Conversation.find({
users: {
$elemMatch: {
user: _id
}
}
},{
title: 1,
type: 1,
users:1 // when `type` is `private` I want to this field to be one.
});
my Schema:
const ConversationSchema = new Schema({
type: {type: String, enum: ['private', 'group'], default: 'private'},
creator: {type: Schema.Types.ObjectId, ref: 'User', index: true, required: true}, // creator
// for group,
title: String,
picture: String,
description: String,
users: [
{
user: { type: Schema.Types.ObjectId, index: true, reuqired: true, unique: true },
role: { type: String, enum: ['admin', 'member'], default: 'member' },
mute: { type: Boolean, default: false },
type: {type: String, enum: ['private', 'group'], default: 'private'},
}
],
}, { timestamps: true });
You can conditionally exclude fields by using REMOVE in aggregation. In your case, it should be:
Conversation.aggregate([
{$match: {"users.user": id}},
{
$project: {
title: 1,
type: 1,
users: {
$cond: {
if: { $eq: [ "$type", "private" ] },
then: "$users",
else: "$$REMOVE"
}
}
}
}
])
Side note: If you specify only a single condition in the $elemMatch expression, you do not need to use $elemMatch.
You can use aggregate like this, it will select all users if you want to users detail then will have to use populate.
db.getCollection("Conversation").aggregate([
{
$unwind: "$users"
},
{
$match: {
"type": 'private'
}
}
]);

aggregate base on match in the second collection

I'm using mongodb.
And I have collection Treatments and collection Patients.
I want to find all treatments that their patient.createdBy is equal to the user who asking the data.
So I tried this
const reminders = await Treatment.aggregate([
{
$lookup: {
from: 'patients',
localField: 'patientId',
foreignField: '_id',
as: 'patient'
}
},
{ $project: { reminders: 1, reminderDate: 1 } },
{ $match: { 'patient.createdBy': { $eq: req.user._id } } }
]);
According to some examples that i saw, it's should work like this.
But it's return me an empty array
If I remove the $match its return me object like this
{
"_id": "5d1e64bdc1506a00045c6a6f",
"date": "2019-07-04T00:00:00.000Z",
"visitReason": "wewwe",
"treatmentNumber": 2,
"referredBy": "wewew",
"findings": "ewewe",
"recommendations": "ewew",
"remarks": "wwewewe",
"patientId": "5cc9a50fd915120004bf2f4e",
"__v": 0,
"patient": [
{
"_id": "5cc9a50fd915120004bf2f4e",
"lastName": "לאון",
"momName": "רןת",
"age": "11",
"phone": "",
"email": "",
"createdAt": "2019-05-01T13:54:23.261Z",
"createdBy": "5cc579d71c9d44000018151f",
"__v": 0,
"firstName": "שרה",
"lastTreatment": "2019-08-02T14:20:08.957Z",
"lastTreatmentCall": true,
"lastTreatmentCallDate": "2019-08-04T15:17:35.000Z"
}
]
}
this is patient schema
const patientSchema = new mongoose.Schema({
firstName: { type: String, trim: true, required: true },
lastName: { type: String, trim: true, required: true },
momName: { type: String, trim: true },
birthday: { type: Date },
age: { type: String, trim: true },
lastAgeUpdate: { type: Date },
phone: { type: String, trim: true },
email: { type: String, trim: true },
createdAt: { type: Date, default: Date.now },
createdBy: { type: mongoose.Schema.Types.ObjectId, required: true },
lastTreatment: { type: Date },
lastTreatmentCall: { type: Boolean },
lastTreatmentCallDate: { type: Date }
});
And this is treatment schema
const treatmentSchema = new mongoose.Schema({
date: { type: Date, default: new Date().toISOString().split('T')[0] },
visitReason: { type: String, trim: true },
treatmentNumber: { type: Number, required: true },
referredBy: { type: String, trim: true },
findings: { type: String, trim: true },
recommendations: { type: String, trim: true },
remarks: { type: String, trim: true },
reminders: { type: String, trim: true },
reminderDate: { type: Date },
patientId: { type: mongoose.Schema.Types.ObjectId }
});
what I'm missing
You have vanished your patient field in the second last $project stage. So instead use it at the end of the pipeline. Also you need to cast your req.user._id to mongoose objectId
import mongoose from 'mongoose'
const reminders = await Treatment.aggregate([
{
$lookup: {
from: 'patients',
localField: 'patientId',
foreignField: '_id',
as: 'patient'
}
},
{ $match: { 'patient.createdBy': { $eq: mongoose.Types.ObjectId(req.user._id) } } },
{ $project: { reminders: 1, reminderDate: 1 } }
])
I think you can add using the pipeline, like below
const reminders = await Treatment.aggregate([
{
$lookup: {
from: 'patients',
localField: 'patientId',
foreignField: '_id',
as: 'patient',
pipeline: [{ $match: { 'age': { $eq: "100" } } }]
}
},
]);

Mongoose sub field aggregation with full text search and project

I have a Mongoose model called Session with a field named course (Course model) and I want to perform full text search on sessions with full text search, also I wanna aggregate results using fields from course sub field and to select some fields like course, date, etc.
I tried the following:
Session.aggregate(
[
{
$match: { $text: { $search: 'web' } }
},
{ $unwind: '$course' },
{
$project: {
course: '$course',
date: '$date',
address: '$address',
available: '$available'
}
},
{
$group: {
_id: { title: '$course.title', category: '$course.courseCategory', language: '$course.language' }
}
}
],
function(err, result) {
if (err) {
console.error(err);
} else {
Session.deepPopulate(result, 'course course.trainer
course.courseCategory', function(err, sessions) {
res.json(sessions);
});
}
}
);
My models:
Session
schema = new mongoose.Schema(
{
date: {
type: Date,
required: true
},
course: {
type: mongoose.Schema.Types.ObjectId,
ref: 'course',
required: true
},
palnning: {
type: [Schedule]
},
attachments: {
type: [Attachment]
},
topics: {
type: [Topic]
},
trainer: {
type: mongoose.Schema.Types.ObjectId,
ref: 'trainer'
},
trainingCompany: {
type: mongoose.Schema.Types.ObjectId,
ref: 'training-company'
},
address: {
type: Address
},
quizzes: {
type: [mongoose.Schema.Types.ObjectId],
ref: 'quiz'
},
path: {
type: String
},
limitPlaces: {
type: Number
},
status: {
type: String
},
available: {
type: Boolean,
default: true
},
createdAt: {
type: Date,
default: new Date()
},
updatedAt: {
type: Date
}
},
{
versionKey: false
}
);
Course
let schema = new mongoose.Schema(
{
title: {
type: String,
required: true
},
description: {
type: String
},
shortDescription: {
type: String
},
duration: {
type: Duration
},
slug: {
type: String
},
slugs: {
type: [String]
},
program: {
content: {
type: String
},
file: {
type: String
}
},
audience: [String],
requirements: [String],
language: {
type: String,
enum: languages
},
price: {
type: Number
},
sections: [Section],
attachments: {
type: [Attachment]
},
tags: [String],
courseCategory: {
type: mongoose.Schema.Types.ObjectId,
ref: 'course-category',
required: true
},
trainer: {
type: mongoose.Schema.Types.ObjectId,
ref: 'trainer'
},
trainingCompany: {
type: mongoose.Schema.Types.ObjectId,
ref: 'training-company'
},
status: {
type: String,
default: 'draft',
enum: courseStatus
},
path: {
type: String
},
cover: {
type: String,
required: true
},
duration: {
type: Number,
min: 1
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date
}
},
{ versionKey: false }
);
I am not sure if what I tried is gonna bring me what I want and I am getting this error concerning the $unwind operator:
MongoError: exception: Value at end of $unwind field path '$course'
must be an Array, but is a OID
Any kind of help will be really appreciated.
You can try below aggregation.
You are missing $lookup required to pull course document by joining on course object id from session document to id in the course document.
$project stage to keep the desired fields in the output.
Session.aggregate([
{
"$match": {
"$text": {
"$search": "web"
}
}
},
{
"$lookup": {
"from": "courses",
"localField": "course",
"foreignField": "_id",
"as": "course"
}
},
{
"$project": {
"course": 1,
"date": 1,
"address": 1,
"available": 1
}
}
])
Course is an array with one course document. You can use the $arrayElemAt to project the document.
"course": {"$arrayElemAt":["$course", 0]}

Resources