Mongoose Model find using an attribute from anohter Schema - javascript

Basically I have 2 Schemas.
User and Post.
User have an array which contains _ids from posts.
And post have an attribute that tells if he's an active post. -> is_active.
So, i want to filter User that have at least, one active post.
UserSchema
const UserSchema = new Schema(
{
name: {
type: String,
trim: true,
required: true
},
posts: [
{
type: Schema.Types.ObjectId,
ref: 'Post'
}
],
created_at: {
type: Date,
required: true,
default: Date.now()
}
}
)
export default mongoose.model<User>('User', UserSchema)
Post Schema
const postSchema = new Schema(
{
name: String,
is_active: boolean
}
)

As an alternative to #Tunmee's answer
Since the pipeline $lookup is available from v3.6 and as of v4.2 still has some performance issues. You could also use the "regular" $lookup available from v3.2
db.Users.aggregate([
{
$lookup: {
from: "Posts",
localField: "posts",
foreignField: "_id",
as: "posts"
}
},
{
$match: {
"posts.is_active": true
}
}
])

You can try this:
Users.aggregate([
{
$lookup: {
from: "Posts",
let: { postIds: "$posts", },
pipeline: [
{
$match: {
$expr: {
$and: [
{
$in: [ "$_id", "$$postIds" ]
},
{
$eq: [ "$is_active", true ]
},
]
}
},
},
// You can remove the projection below
// if you need the actual posts data in the final result
{
$project: { _id: 1 }
}
],
as: "posts"
}
},
{
$match: {
$expr: {
$gt: [ { $size: "$posts" }, 0 ]
}
}
}
])
You can test it out in a playground here
I'm not sure about your application's query requirement but you can add a compound index on _id and is_active properties in Posts collection to make the query faster.
You can read more about MongoDB data aggregation here.

Related

Use mongoDB $lookup to find documents in another collection not present inside an array

I'm using the aggregate framework to query a collection and create an array of active players (up until the last $lookup) after which I'm trying to use $lookup and $pipeline to select all the players from another collection (users) that are not present inside the activeUsers array.
Is there any way of doing this with my current setup?
Game.aggregate[{
$match: {
date: {
$gte: ISODate('2021-04-10T00:00:00.355Z')
},
gameStatus: 'played'
}
}, {
$unwind: {
path: '$players',
preserveNullAndEmptyArrays: false
}
}, {
$group: {
_id: '$players'
}
}, {
$group: {
_id: null,
activeUsers: {
$push: '$_id'
}
}
}, {
$project: {
activeUsers: true,
_id: false
}
}, {
$lookup: {
from: 'users',
'let': {
active: '$activeUsers'
},
pipeline: [{
$match: {
deactivated: false,
// The rest of the query works fine but here I would like to
// select only the elements that *aren't* inside
// the array (instead of the ones that *are* inside)
// but if I use '$nin' here mongoDB throws
// an 'unrecognized' error
$expr: {
$in: [
'$_id',
'$$active'
]
}
}
},
{
$project: {
_id: 1
}
}
],
as: 'users'
}
}]
Thanks
For negative condition use $not before $in operator,
{ $expr: { $not: { $in: ['$_id', '$$active'] } } }

How can I get the total sum ($sum) of an array from a nested field?

I need the total sum of all the elements in an array that is nestet in my schema.
This is the schema:
const mongoose = require('mongoose');
let historySchema = new mongoose.Schema({
time: {
type:String
}
})
//users schema
let userSchema = new mongoose.Schema({
name:{
type:String,
},
dob:{
type:String,
},
email:{
type:String,
},
noOfpeopleClimbing:{
type: Number,
default:0
},
details:{
type:String,
},
status:{
type: Boolean,
default: false
},
timeIn:{
type: Number,
default: 0
},
timeOut:{
type: Number,
default: 0
},
timeFinal:{
type: Number,
default: 0
},
history:[{
time:{
type: Number
},
date:{
type:Date,
default:Date.now()
},
climbers:{
type: Number
},
names:{
type: String
}
}]
})
let User = module.exports = mongoose.model("User", userSchema);
The nested field in disscusion is:
history:[{
time:{
type: Number
}]
And the find method is:
app.get('/user/:id', function(req,res){
Users.findById(req.params.id, function(err, users){
res.render("user",
{
title:users.name,
users:users,
});
})
})
Can I attach to my find route an aggregate with $sum in order for me to send the data with the sum to my render view?.
For example totalTimeHistory:$sum aggregate data.
Use the following snippet below:
const result = Users.aggregate([
{
$match: {
//Your find block here
}
},
{
$unwind: "$history"
},
{
$project: {
totalTimeHistory: { $sum: "$history.time"}
}
}
])
Try this query:
db.collection.aggregate([
{
"$match": {
"name": "name" //or whatever you want
}
},
{
"$project": {
"total": {
"$sum": "$history.time"
}
}
}
])
In this way you don't need $unwind
Example here
db.getCollection('users').aggregate([
{
$match: {
<your find query goes here>
}
},
{
$unwind: '$history'
},
{
$group: {
_id: <your user object id (or) null>,
history: { $push: "$$ROOT.history" }
}
},
{
$addFields: {
totalSumOfHistoryTypes: { $sum: "$history.type" }
}
},
])
your output will look like
explanation:
$match: to find in the collection
$unwind: to unwind the history array so that the values of history can be grouped
$group: here we have created an array called history and pushed the history object($$ROOT.history) into it
$addFiled: used to add a new field which is not present on the schema
Hope this explains cheers

Wrong implementation of $lookup from MongoDB in NodeJs

I have an Entity model and a Review model, they are related by entityId field which is part Review model.
I am trying to find all the reviews from a specific entity and then calculate the average of all the rating of all reviews. (rating is another field of Review model, given below)
This is how Entity model looks:
const entitySchema = new Schema({
name: {
type: String,
required: true,
trim: true,
unique: true,
}
});
and this is Review model:
const reviewSchema = new Schema({
rating: {
type: Number,
min: 0,
max: 5,
required: true,
},
comment: {
type: String,
trim: true,
},
public: {
type: Boolean,
required: true,
default: false,
},
entityId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Entity',
required: true,
}
}, {
timestamps: true,
});
I want to $lookup function here and this is what I have tried till now:
router.get('/entities/reviews/average', async (req, res) => {
try {
const entity = await Entity.find();
const entityId = [];
Object.keys(entity).forEach((key) => {
entityId.push(entity[key]._id);
});
Object.keys(entityId).forEach((key) => {
const reviews = Review.aggregate([
{ $match: { entityId: ObjectId(entityId[key]) } },
{
$lookup: {
from: 'entity',
localField: '_id',
foriegnField: 'entityId',
as: 'rating',
},
},
{
$group: {
_id: null,
avg: { $avg: '$rating' },
},
},
]);
res.send(reviews);
});
} catch (e) {
res.status(500).send();
}
});
But this doesn't work it gives this response back
{
"_pipeline": [
{
"$match": {
"entityId": "5eb658d7"
}
},
{
"$lookup": {
"from": "entity",
"localField": "_id",
"foriegnField": "entityId",
"as": "rating"
}
},
{
"$group": {
"_id": null,
"avg": {
"$avg": "$rating"
}
}
}
],
"options": {}
}
How to do this? What am I doing wrong?
I am not getting the reason behind that you are getting same query in return,
If i am not wrong then you are doing average of rating for entity, my suggestion is you can combine query and do it in single query,
$lookup to join rating collection
$addFields to do average, make array of rating using $map and then do average using $avg
router.get('/entities/reviews/average', async (req, res) => {
try {
let reviews = await Entity.aggregate([
{
$lookup: {
from: "Review",
localField: "_id",
foreignField: "entityId",
as: "avgRating"
}
},
{
$addFields: {
avgRating: {
$avg: {
$map: {
input: "$avgRating",
in: "$$this.rating"
}
}
}
}
}
])
res.send(reviews);
} catch (e) {
res.status(500).send();
}
});
Playground
https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/
Lookup is doing a sql type join so the two fields you want to join on would have to match. I couldn't get you query working in mongo shell but I did get the following to work.
Reviews.aggregate([
{
$group: {
_id: { entityId: "5f56460d567f27054739c3bb" },
averageRating: { $avg: "$rating" },
},
},
])
It's run in mongo shell as well.

How to filter mongo document based on nested object?

How can I find room by ID and make sure that the room has the current player in it?
My mongodb has a document of rooms which has players and a player is a user.
const RoomSchema = new Schema({
players: [{ type: Schema.Types.ObjectId, ref: "Player" }]
})
const PlayerSchema = new Schema({
user: { type: Schema.Types.ObjectId, ref: "User" }
})
const UserSchema = new Schema({
username: { type: String}
})
I want to find room where id === roomId and room has a player with user._id === userId
My query so far just finds one room by ID but I want to make sure that the room returned has the current user in as a player
RoomModel
.findOne({_id: roomId})
.populate({
path: 'players',
populate: {
path: 'user',
model: 'User',
select: ["_id", "username"]
}
})
You can use mongodb aggregation framework for this task.
Playground
const result = await RoomModel.aggregate([
{
$match: {
_id: "1", // match by room id
},
},
{
$lookup: {
from: "players", // must be physical collection name, check if different
localField: "players",
foreignField: "_id",
as: "players",
},
},
{
$unwind: "$players",
},
{
$match: {
"players.user": "100", //match by user id
},
},
{
$lookup: {
from: "users",
localField: "players.user",
foreignField: "_id",
as: "user"
}
}
]);
if (result.length > 0) {
console.log("found"); //todo: add your logic when found
} else {
console.log("not found"); //todo: add your logic when not found
}
This will give a result like this when the user found, you may need some transformation.
[
{
"_id": "1",
"players": {
"_id": "10",
"user": "100"
},
"user": [
{
"_id": "100",
"username": "user1"
}
]
}
]

mongoose mongodb query find

i'm new in mongo and mongoose.
i want to do a simple query in relational database but i have strong problems to do in mongo
here is me schema:
const GroupSchema = new Schema({
name: { type: String , required:true},
image: { type: String , required:true},
location : {type:String , required: true},
locationCode: {type:String , required:true },
created: {type:Date, default:Date.now() , required:true },
createdBy: {type: Schema.Types.ObjectId , ref:'User' , required:true},
//category: [{type:String,enum:[ config.TYPES ]}],
pendingUsers: [
{ text:String,
user:{type: Schema.Types.ObjectId , ref: 'User'}
}
],
rejectedUsers: [ {type: Schema.Types.ObjectId , ref: 'User'} ],
users: [ {type: Schema.Types.ObjectId , ref: 'User' , required:true } ],
adminUsers:[{type:Schema.Types.ObjectId , ref:'User', required:true}],
events :[Event],
activity:[Activity]
})
and i my controller file i want to do the following query:
let groupId = '123123hgvhgj
let userId = 'asdfsadf3434
Group.find()
.where('_id').equals(groupId)
.where('pendingUsers.user')
.in(userId)
.where('users')
.in(userId)
.where('adminUsers')
.in(userId)
.where('rejectedUsers')
.in(userId)
.exec(function (err, records) {
//make magic happen
console.log(records)
});
i have to get the record WHERE _id match with a group id AND (userId exists in pendingUsers OR userid exists in rejectedUsers OR userid exists in users OR userid exists in adminUsers )
i know that seems to be a simple query but returns empty when should be returned the record i have something wrong in the query?
thanks
Even if mongoose seems to support some simple and + or operations (docs)
it seems to me as if you would still need to mix some pure mongodb query into it.
Considering that, i would go with a pure mongodb style query. This one should fit your needs(untested) or will at least point you in the right direction:
Group.find({
$and: [
{_id: groupId},
{
$or: [
{ pendingUsers.user: { $elemMatch: {userId} } },
{ rejectedUsers: { $elemMatch: {userId} } },
{ users: { $elemMatch: {userId} } },
{ adminUsers: { $elemMatch: {userId} } },
]
}
]
});
Group.find({
$and: [
{ _id: groupId},
{
$or: [
{ "pendingUsers": { $elemMatch: { "user": userId } } },
//{ 'pendingUsers.user' : { $elemMatch: {userId} } },
{ rejectedUsers: { $elemMatch: {userId} } },
{ users: { $elemMatch: {userId} } },
{ adminUsers: { $elemMatch: {userId} } },
]
}
]
})

Categories

Resources