Moogose complex query joins - node.js

I'm stuck with some complex query joins in mongoose (using nodejs)
I have the model User
{ active_payment: {type: mongoose.Schema.Types.ObjectId, ref: 'Payment'}}
And the model payment:
{status : Number, creation_date: Date}
I want to find all the users with the a paument of a status = 2.
I've tried:
User.find({'active_payment.status': 2,}).populate('active_payment')
But its not working. Is there any way to do it without having to sort all the users by a for loop?
I would also like to sort users by the creation_date of the payment.

You can not reach status in User.find() directly, Please try this one
User
.find({})
.populate({
path: 'active_payment',
match: { status: 2},
})
.exec(function(err, users) {
users = users.filter(function(user) {
return user.active_payment; // filter the user by active_payment field
});
});
With match to filter the status is 2 populate documents, then filter user after populate though active_payment.

Related

How to model a collection in nodejs+mongodb

Hello I am new to nodejs and mongodb.
I have 3 models:
"user" with fields "name phone"
"Shop" with fields "name, address"
"Member" with fields "shop user status". (shop and user hold the "id" of respective collections).
Now when I create "shops" api to fetch all shop, then I need to add extra field "isShopJoined" which is not part of the model. This extra field will true if user who see that shop is joined it otherwise it will be false.
The problem happens when I share my model with frontend developers like Android/iOS and others, They will not aware of that extra field until they see the API response.
So is it ok if I add extra field in shops listing which is not part of the model? Or do I need to add that extra field in model?
Important note
All the code below has NOT been tested (yet, I'll do it when I can setup a minimal environment) and should be adapted to your project. Keep in mind that I'm no expert when it comes to aggregation with MongoDB, let alone with Mongoose, the code is only here to grasp the general idea and algorithm.
If I understood correctly, you don't have to do anything since the info is stored in the Member collection. But it forces the front-end to do an extra-request (or many extra-requests) to have both the list of Shops and to check (one by one) if the current logged user is a Member of the shop.
Keep in mind that the front-end in general is driven by the data (and so, the API/back-end), not the contrary. The front-end will have to adapt to what you give it.
If you're happy with what you have, you can just keep it that way and it will work, but that might not be very effective.
Assuming this:
import mongoose from "mongoose";
const MemberSchema = new mongoose.Schema({
shopId: {
type: ObjectId,
ref: 'ShopSchema',
required: true
},
userId: {
type: ObjectId,
ref: 'UserSchema',
required: true
},
status: {
type: String,
required: true
}
});
const ShopSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
address: {
//your address model
}
});
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
phone: {
type: String,
required: true,
},
// Add something like this
shopsJoined: {
type: Array,
default: [],
required: true
}
});
You could tackle this problem via 2 ways:
MongoDB Aggregates
When retrieving (back-end side) the list of shops, if you know the user that made the request, instead of simply returning the list of Shops, you could return an aggregate of Shops and Members resulting in an hybrid document containing both the info of Shops and Models. That way, the front-end have all the info it needs with one back-end request.
Important note
The following code might not work as-is and you'll have to adapt it, I currently have nothing to test it against. Keep in mind I'm not very familiar with aggregates, let alone with Mongoose, but you'll get the general idea by looking the code and comments.
const aggregateShops = async (req, res, next) => {
try {
// $lookup will merge the "Model" and "Shop" documents into one
// $match will return only the results matching the condition
const aggreg = await Model.aggregate({$lookup: {
from: 'members', //the name of the mongodb collection
localField: '_id', //the "Shop" field to match with foreign collection
foreignField: 'shopId', //the "Member" field to match with local collection
as: 'memberInfo' //the field name in which to store the "Member" fields;
}, {
$match: {memberInfo: {userId: myUserId}}
}});
// the result should be an array of object looking like this:
/*{
_id: SHOP_OBJECT_ID,
name: SHOP_NAME,
address: SHOP_ADDRESS,
memberInfo: {
shopId: SHOP_OBJECT_ID,
userId: USER_OBJECT_ID,
status: STATUS_JOINED_OR_NOT
}
}*/
// send back the aggregated result to front-end
} catch (e) {
return next(e);
}
}
Drop the Members collection and store the info elsewhere
Instinctively, I would've gone this way. The idea is to either store an array field shopsJoined in the User model, or a membersJoined array field in the Shops model. That way, the info is retrieved no matter what, since you still have to retrieve the Shops and you already have your User.
// Your PATCH route should look like this
const patchUser = async (req, res, next) => {
try {
// How you chose to proceed here is up to you
// I tend to facilitate front-end work, so get them to send you (via req.body) the shopId to join OR "un-join"
// They should already know what shops are joined or not as they have the User
// For example, req.body.shopId = "+ID" if it's a join, or req.body.shopId = "-ID" if it's an un-join
if (req.body.shopId.startsWith("+")) {
await User.findOneAndUpdate(
{ _id: my_user_id },
{ $push: { shopsJoined: req.body.shopId } }
);
} else if (req.body.shopId.startsWith("-")) {
await User.findOneAndUpdate(
{ _id: my_user_id },
{ $pull: { shopsJoined: req.body.shopId } }
);
} else {
// not formatted correctly, return error
}
// return OK here depending on the framework you use
} catch (e) {
return next(e);
}
};
Of course, the above code is for the User model, but you can do the same thing for the Shop model.
Useful links:
MongoDB aggregation pipelines
Mongoose aggregates
MongoDB $push operator
MongoDB $pull operator
Yes you have to add the field to the model because adding it to the response will be only be a temporary display of the key but what if you need that in the future or in some list filters, so its good to add it to the model.
If you are thinking that front-end will have to be informed so just go it, and also you can set some default values to the "isShopJoined" key let it be flase for the time.

Updating multiple documents in mongoose at once

I have an array of User documents each user has followers attribute which is a number, I just want to increment this attribute by 1 and then update all those user documents in the DB at once.
More details:
In the request I have an array of user ids, I query with those ids to get an array of user documents.
const users = await User.find({"_id": {$in: req.body.ids}});
each user document has an attribute called followers which is a number.
const userSchema = new mongoose.Schema({
// other attributes...........,
followers: {
type: Number,
default: 0
}
});
I want to increment the number of followers in each user in the users array and update the DB at once without sending a request to update each user separately, Is this even possible?
Thanks in advance
You can use $inc and run updateMany:
await User.updateMany({"_id": {$in: req.body.ids}}, { $inc: { followers: 1 } });

How do I prevent Schema fields from being inserted into subdocument?

I'm making a dating app in node js and vue, and everything works however I wish to exclude password from being inserted into subdocument upon creation of a conversation. Right now I know that i can say .select('-password') when using User.findOne() but it doesn't work, when adding the user schema as a subdoc to my Conversations schema, which has user_one and user_two, each referring to a User schema. I need the password field, so I can't ommit it when creating a schema. Right Now my code looks like this:
User.findOne({ _id: fromUserId }, (errUserOne, userOne) => {
User.findOne({ _id: toUserId }, (errUserTwo, userTwo) => {
conversation = new Conversation({
_id: mongoose.Types.ObjectId(),
user_one: userOne,
user_two: userTwo
});
conversation.save();
const message = new Message({
_id: mongoose.Types.ObjectId(),
conversation_id: conversation._id,
message: req.body.message,
user_id: fromUserId
});
message.save();
res.sendStatus(201);
});
});
However this code saves the password to the Conversation collection, which I don't want.
User.find({ _id: :{ $in : [fromUserId,toUserId] }, { password:0 } , (err, userArray) => {
//your code goes here
});
Two things, You are querying two time for getting users. You can merge it into single query and for excluding the password field you can pass {password:0}. Which will exclude it in the documents.
also while you define Conversation schema don't make user_one and user_two type of user. Instead define only whatever properties of user you want to save like:
var Conversation = new Schema({
_id : ObjectId,
user_one : {
_id: ObjectId,
//all other fields
},
user_two : {
_id: ObjectId,
//all other fields
},
});

How to use ref to store a document correctly in mongodb - mongoose?

I am trying to store a ref of a user model in my users - followers Array:
The model looks something like this:
User = require('../models/user.js');
ObjectId = mongoose.Schema.Types.ObjectId;
User = new Schema({
followers: [{ type: ObjectId, ref: 'User' }],
Now I am trying to store every time a user follows a user like so:
User.findByIdAndUpdate({_id: myId}, { $addToSet: {followers: user._id,
However, this only stores the string and not the whole user object? Is that how it is suppose to be? If so, then to grab it out the rest of the info do I just need to do a populate query?
If it is suppose to store the whole object then does it update as well as the object gets updated?
I was going to remove the question as I figured it out, but I see that people up-voted it so to help them out, I shall post my discoveries.
Apparently it will just store the id, and to then call it I used a basic populate call on it like so:
User
.findOne({ _id: myId })
.populate('followers', '_id name count') //
.exec(function (err, doc) {
if (err) return handleError(err);
})

Using elemMatch for a nested condition query in Mongoose

I have two schemas Student, User - where user can be class teacher or other designated persons.
Student schema
var StudentSchema = new Schema({
name : String,
.
.
.
owner: {
type: Schema.ObjectId,
ref: 'User'
}
})
Now I want to group the students for a particular owner.(classTeacher)
I have written a server api route -method for this.
get ==> localhost:3000/api/---/user/:userId/students
userId is the classTeacherid. = req.queryUserStudentsId
exports.getUserStudents=function(req,res){
Student
.find()
.select({
'owner': {
$elemMatch: {
_id: req.queryUserStudentsId
}
}
})
.populate('owner')
.exec(function(err, students) {
};
This results in all the students. Not only those which belong to the given owner id.
Please let me know where I am wrong.
Thanks.
select is used to select the fields to include in the returned docs. You need to provide the owner filter to the find itself to filter the student docs to just those owned by req.queryUserStudentsId:
Student.find({owner: req.queryUserStudentsId})
.populate('owner')
.exec(function(err, students) {
...
});
You can omit the .populate('owner') call if you don't need to the full details of the owner.

Resources