Return fields from different collection Mongoose - javascript

Imagine a function that finds users by their name and returns them.
User.aggregate(
[
{ $sort: { userFirstName: 1, userLastName: 1 } },
{
$addFields: {
firstLastName: { $concat: ['$userFirstName', ' ', '$userLastName'] },
lastFirstName: { $concat: ['$userLastName', ' ', '$userFirstName'] }
}
},
{
$match: $match // Set from above with match crit
},
{
$group: {
_id: null,
total: { $sum: 1 },
data: {
$push: {
'_id': '$_id',
'userFirstName': '$userFirstName',
'userLastName': '$userLastName',
'userProfileImage': '$userProfileImage',
'userVihorCategory': '$userVihorCategory'
}
}
}
},
{
$project: {
total: 1,
data: { $slice: ['$data', start, limit] }
}
}
]
).exec((errAgg, results) => {...
This works, it splices them and returns them correctly.
There is another collection that tracks user connections.
{
user: { type: Schema.Types.ObjectId, ref: 'User' },
userConnection: { type: Schema.Types.ObjectId, ref: 'User' },
userConnectionStatus: {
type: String,
enum: ['following', 'blocked', 'requested']
}
}
Eg User: me, userConnection: 'someone', userConnectionStatus: 'following'
What I am trying to achive is to return 2 more fields,
1. My userConnectionStatus to him
2. His userConnectionStatus to me
And not to return users who have blocked me.
What is the best approach when it comes to this DB structure.
Thank you for your time

Preventing blocked users was solved by selecting all blocked users, and adding $nin in match inside aggregate.
For connection status, I have resolved the problem by adding 2 virtual fields to User.
UserMongoSchema.virtual('userConnectionStatus', {
ref: 'UserConnection',
localField: '_id',
foreignField: 'user',
justOne: true
});
UserMongoSchema.virtual('connectionStatus', {
ref: 'UserConnection',
localField: '_id',
foreignField: 'userConnection',
justOne: true
});
And populating them on results
...
.exec((errAgg, results) => {
User.populate(results[0].data, [
{ path: 'userConnectionStatus', match: { userConnection: req.userCode }, select: 'userConnectionStatus' },
{ path: 'connectionStatus', match: { user: req.userCode }, select: 'userConnectionStatus' },
], (errPop, populateResponse) => {
if (errPop) { return next(errPop); }
populateResponse = populateResponse.map((row) => {
row['userConnectionStatus'] = row.userConnectionStatus ? row.userConnectionStatus.userConnectionStatus : null;
row['connectionStatus'] = row.connectionStatus ? row.connectionStatus.userConnectionStatus : null;
return row;
});
...
Looking at the order of actions, I think this won't affect performance since I am running populate only on those matched top X (max 100) results.
I won't mark this as Answer yet. If you have any opinion about if this is bad practice or if there is a better way of doing it, feel free to comment.

Related

MongoDB - How to combine findOne (in array) with aggregate

I currently have a Mongo query that looks like this:
const user = await User.findOne({ userId }).lean() || []
const contributions = await Launch.aggregate([
{ $sort: { addedAt: -1 } },
{ $limit: 10 },
{
$match: {
_id: { $in: user.contributions }
}
},
{
$addFields: {
activity: 'contribution',
launchName: '$name',
launchId: '$_id',
date: '$addedAt',
content: '$description'
}
}
])
But instead of having two different Mongo queries (findOne and aggregate), how can I combine them into one query?
I tried this but it just errors out immediately in the lookup part:
const contributions = await Launch.aggregate([
{ $sort: { addedAt: -1 } },
{ $limit: 10 },
{
$lookup: {
from: 'user',
let: { id: $user.contributions },
pipeline: [
{ $match: { $expr: { $in: [$_id, $$user.contributions] } } }
],
localField: '_id',
foreignField: 'userId',
as: 'user'
}
},
{
$addFields: {
activity: 'contribution',
launchName: '$name',
launchId: '$_id',
date: '$addedAt',
content: '$description'
}
}
])
I've never used the pipeline option so a little confused onn how to fix this problem?
Enclose these $user.contributions, $_id with quotes in order to make the query valid.
Since you declare the id variable with the value of user.contributions. You should use the variable with $$id instead of $$user.contributions.
I don't think the localField and foreignField are needed as you are mapping/joining with pipeline.
Your aggregation query should be looked as below:
const contributions = await Launch.aggregate([
{ $sort: { addedAt: -1 } },
{ $limit: 10 },
{
$lookup: {
from: 'user',
let: { id: "$user.contributions" },
pipeline: [
{ $match: { $expr: { $in: ["$_id", "$$id"] } } }
],
as: 'user'
}
},
{
$addFields: {
activity: 'contribution',
launchName: '$name',
launchId: '$_id',
date: '$addedAt',
content: '$description'
}
}
])

Conditional joins on collections using mongoose

I'm new to mongoDB, I am trying to achieve the following SQL query on it. but could not find anything useful so far. can anyone tell equivalent mongoose query
select * from interviews
inner join candidate on interviews.clientId = candidate._id
inner join billing on appointment._id = billing.appointmentId
where ('
interviews.status= "upcoming",
interviews.startTime= "2017-01-01",
candidate.clientAgeGroup= "adult",
candidate.candidatetatus= "new",
billing.paymentStatus= "paid"
')
what I got so far is following
const [result, err] = await of(Interview.find({ ...filterQuery }).limit(perPage)
.skip(perPage * page)
.sort({
startTime: 'asc'
})
.populate([{ path: 'candidateId', model: 'Candidate', select: 'firstName status avatar' },
{ path: 'billingId', model: 'Billing', select: "status" }]));
UPDATE
I have following name and export scheme
//interview.model.js => mongodb show name as interview
module.exports = mongoose.model('Interview', interviewSchema);
//candidate.model.js => mongodb show name as candidate
module.exports = mongoose.model('Candidate', candidateSchema);
You can use filter out objects included in resulting array using match but in the case if it couldn't find any, it would still return a null value. So in comparison this works similar to sql left join.
const [result, err] = await of(Interview.find({ ...filterQuery }).limit(perPage)
.skip(perPage * page)
.sort({
startTime: 'asc'
})
.populate([{ path: 'candidateId', model: 'Candidate', select: 'firstName status avatar', match: {clientAgeGroup: "adult", candidatetatus: "new"} },
{ path: 'billingId', model: 'Billing', select: "status", match: {paymentStatus: "paid"} }]));
Also see https://mongoosejs.com/docs/populate.html#query-conditions
If you need strictly a inner join then you can use mongodb aggregate pipeline.
Interview.aggregate([
{
"$match": {
status: "upcoming",
startTime: "2017-01-01",
}
},
{
'$lookup': {
'from': 'candidates', // this should be your collection name for candidates.
'localField': 'candidateId', // there should be an attribute named candidateId in interview model that refer to candidate collection
'foreignField': '_id',
'as': 'candidates'
}
}, {
'$match': {
'candidates.clientAgeGroup': "adult",
'candidates.candidatetatus': "new"
}
},
{
'$lookup': {
'from': 'billing', // this should be your collection name for billing.
'localField': 'billingId', // there should be an attribute named billingId in interview model that refer to billing collection
'foreignField': '_id',
'as': 'billing'
}
}, {
'$match': {
'billing.paymentStatus': "paid"
}
},
{ "$sort": { startTime: 1 } },
{ "$limit": perPage },
{ "$skip": perPage * page }
])

Retrieve all information of a specific tv show episode including actors and director using aggregate function mongoose

I want to retrieve all information of a specific tv show episode including actors and director.
This is my tv show model.js:
const tvShowSchema = new Schema({
name: String,
year: Number,
country: String,
seasons: [{
number: Number,
year: Number,
episodes: [{
title: String,
number: Number,
releasedOn: Date,
description: String,
cast: [{
type: Schema.Types.ObjectId,
ref: 'actors'
}],
director: {
type: Schema.Types.ObjectId,
ref: 'directors'
}
},
{
timeStamps: true,
versionKey: false
}
]
}]
});
this is the tv show Routes.js:
router.get("/showid/tvepisodeid/:tvshowid/seasonid/:seasonId/episodeid/:episodeId",
tvShowsCtrl.getTvShowEpisode);
And this is the tv show controller.js:
import TVShow from "../models/TVShow";
import {
Types
} from "mongoose";
export const getTvShowEpisode = async (req, res) => {
try {
const id = req.params.id;
const seasonId = req.params.seasonId;
const episodeId = req.params.episodeId;
const tvShowEpisode = await TVShow.aggregate([
{
$match: {
"_id": Types.ObjectId(id),
"seasons._id": Types.ObjectId(seasonId),
"episodes._id": Types.ObjectId(episodeId)
}
}, {
$addFields: {
"isEpisode": {
$eq: ['$seasons.episodes', selectedEpisode]
}
}
}, {
$addFields: {
"cast.isEpisode": {
$eq: ['$seasons.episodes', selectedEpisode]
}
}
}, {
$lookup: {
from: 'actors',
localField: 'seasons.episodes.cast',
foreignField: '_id',
as: 'actors'
}
},
{
$addFields: {
"director.isEpisode": {
$eq: ['$seasons.episodes', selectedEpisode]
}
}
}, {
$lookup: {
from: 'directors',
localField: 'seasons.episodes.director',
foreignField: '_id',
as: 'director'
}
},
/* {$unwind: '$seasons'}, */
{
$match: {
"seasons._id": Types.ObjectId(seasonId)
}
}, {
$project: {
"_id": Types.ObjectId(id)
},
"episode": '$seasons.episodes'
}
]);
if (tvShowEpisode) {
res.send(tvShowEpisode);
} else {
res
.status(404)
.send({
mensaje: `tv show episode not found`
});
}
} catch (error) {
console.log(error);
res.status(500).send({
message: `error: \n\n${error}`
});
}
};
the expected output should be like this:
{
"seasons":[
"number":1,
"year":2019,
"episodes":[
"title":"the restaurant",
"number":2,
"releasedOn":"2019-05-10T14:00:00.025Z",
"description":"the restaurant is new",
"cast":[
{
"_id":"84834h4hrtbb54y4hu4u5h9",
"fullName":"Keith Price",
"age":25,
"nationality":"american"
},
{
"_id":"82j4hy43u45hu54huuh539",
"fullName":"Cindy Kat",
"age":28,
"nationality":"welsh"
}
],
"director":{
"_id":"urirnjr43u242344343",
"fullName":"maurice klossman",
"nationality":"polish"
}
]
]
}
How can I fixed these issues?, why the code is not working?, I want when I'm typing url passing respective parameters got the expected output but in postman I've got the message '500 Internal Server Error'.
I've got some issues. I've tried many ways using aggregate function to show only a specific show tv including actors and director but I don't know how to do to code works.
I'd like to know what is wrong with my tv show controller method.
The log error: "Arguments must be aggregate pipeline operators"

how to count records in mongodb

I am new in mongodb.
i have a simple email Schema which implemented like below :
const emailSchema = new Schema({
from:{
type: String
},
to: {
type: String
},
subject: {
type: String
},
content: {
type: String
},
provider:{
type: String
},
success:{
type: Boolean
}
now i want to query all email records and seprate them in two array. one for provider=x and provider = y . then count each one's success field.
how can i write such query ?
If you only want to count success: true for any provider
emailSchema.aggregate([{
$match: {
{ 'success': true }
},
$group: {
_id: {
provider: '$provider'
}, //group by provider
count: {
$sum: 1
}
} //sum of success fields
]);
If you only want to count success field exists or not it may be true or false for any provider
emailSchema.aggregate([{
$match: {
{'success': { $exists: true }}
},
$group: {
_id: {
provider: '$provider'
}, //group by provider
count: {
$sum: 1
}
} //sum of success fields
]);
Use aggregate query for group and count
emailSchema.aggregate([{
$group: {
_id: {
provider: '$provider'
}, //group by provider
count: {
$sum: '$success'
}
} //sum of success fields
])

Mongoose update conditions matching wrong object

Model Schema
const PollSchema = new Schema({
title: { type: String, trim: true, required: true },
choices: [
{
title: { type: String, trim: true, required: true },
votes: { type: Number, default: 0, min: 0 },
}
],
date: { type: Date, default: Date.now },
url: { type: String, required: true, unique: true },
})
Update call
async vote (pollId, choiceId, unvoteId = '') {
try {
await pollModel.update(
{ '_id': pollId, 'choices._id': choiceId },
{ $inc: { 'choices.$.votes': 1 } },
)
if (unvoteId) {
await pollModel.update(
{
"$and": [
{ '_id': pollId },
{ 'choices._id': unvoteId },
{ 'choices.votes': { $gt: 0 } }
],
},
{ $inc: { 'choices.$.votes': -1 } },
)
}
return await pollModel.findById(pollId)
} catch (e) {
throw new ApiError(
500, 'Error: Poll:Vote', e
)
}
}
I have been trying a plethora of combinations trying to get this to work. Voting +1 works as intended, but when trying to -1, the query conditions are not properly matched. I have tried $and, $elemMatch, plain object with the 3 conditions (according to the docs this is sufficient and implicitly means and too.
Whenever I send through an unvoteId, no matter which _id I choose in the array, it will always match the FIRST element that has $gt 0 votes. It is as if choices._id is completely ignored, and as soon as it meets a choice that has > 0 votes, it returns that for the $ positional param.
Is this intended? I assumed $and would only match if all 3 conditions were satisfied.
What I am trying to do is update the votes atomically using $inc, while also ensuring that when someone votes, they cannot bring the value below 0. As the mongoose validators do not get run during updates, I am trying to validate this via the query itself.
Try something like this.
pollModel.update({
_id: pollId,
choices: {
$elemMatch: {
_id: unvoteId,
votes: {
$gt: 0
}
}
}
}, {
$inc: {
"choices.$.votes": -1
}
})

Categories