Using elemMatch for a nested condition query in Mongoose - node.js

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.

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.

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 model.findbyid and query inside it?

This is a simple question for you guys...I am wondering how can I acess a specific Id that is inside the "followers", after querying the User by its id. In other words i am searching for the user by its Id, and then I want to check the ids that are inside the followers.
For example: I have an Id saved in one variable "x", and I want to check if this Id "x" is inside of the followers array.
The model looks like this:
var UserSchema = new mongoose.Schema({
followers: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}
]
});
module.exports = mongoose.model("User", UserSchema);
And the code that I am using is this one:
User.findById(req.params.id,'followers', function(err,name){
if(err){
console.log(err)
} else{
console.log(name);
}
});
It looks like I can print the user id and the id's that are inside the "followers", but i am not managing to see if the desired Id is inside of "followers" array or not.Can anyone help me with it?
Thank you for your attention!
I think you need to create nested for loop because you have an array of users according to user schema and each user has an array of followers and to solve this, you need to create for loop for user schema and inside that loop you will loop through all followers inside that user and check whether the id that you have equals to their id.
Since followers is an array of IDs (document references), if the query is for a single follower match you can use a shorthand of {followers: SOME_ID} or if it is an array then you can use {followers: {$all: [SOME_ID, SOME_OTHER_ID]}}. See the Mongo documentation for more information.
Using an array to match for example would look like the following.
User.findOne({_id: req.params.id, followers: {$all: [SOME_ID, SOME_OTHER_ID]}}, function(err, name) {
if (err) {
console.log(err)
} else {
console.log(name);
}
});

Moogose complex query joins

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.

What is populate in SailsJs?

I recently began to develop on sailsJs and not understanding the subtleties
Please explain to me what is populate in SailsJs and who can please do simple example
Thanks in advance ?
whats is ?
User.find({ name: 'foo' })
.populate('pets', { name: 'fluffy' })
.exec(function(err, users) {
if(err) return res.serverError(err);
res.json(users);
});
populate is used for associations. When your model is something like this:
// User.js
module.exports = {
attributes: {
name: {
type: "string"
},
pet: {
model: "pet"
}
}
}
Here pet attribute of user collection is a reference to pet table. In user table it will store only the id column of pet. However, when you do a populate while find, then it will fetch the entire record of the pet entry and display it here. This is just for one to one association. You can have many to one associations as well as many to many. See this documentation for more details
sailsjs project is use the wateline ORM. you can see the document. if you want to use 'populate()', you need define Associations in the model.
.populate()
populate is used with associations to include any related values specified in a model definition. If a collection attribute is defined in a many-to-many, one-to-many or many-to-many-through association the populate option also accepts a full criteria object. This allows you to filter associations and run limit and skip on the results.
as you example, you need do like this:
User.js
// A user may have many pets
var User = Waterline.Collection.extend({
identity: 'user',
connection: 'local-postgresql',
attributes: {
firstName: 'string',
lastName: 'string',
// Add a reference to Pets
pets: {
collection: 'pet',
via: 'owner'
}
}
});
Pet.js
// A pet may only belong to a single user
var Pet = Waterline.Collection.extend({
identity: 'pet',
connection: 'local-postgresql',
attributes: {
breed: 'string',
type: 'string',
name: 'string',
// Add a reference to User
owner: {
model: 'user'
}
}
});
you can ready the doc, and you can use it very easy
To resume what is .populate() (used by waterline) is a little what join is used by SQL.
.populate() allows you to join the tables in your database.
The link identifier is to be defined in your model.
In other words, to associate a user (who is in the "User" table) with a dog (who is in the "Dog" table), you use populate.
To resume your example:
You are looking for the user => User.find ({name: my_user})
You are looking for the dog named "fluffy" => {name: 'fluffy'}
You are looking for the dog 'fluffy' which is associated with your user (belongs) => populate ('pets')
Which give:
User.find({ name: 'foo' })
.populate('pets', { name: 'fluffy' })
.exec(function(err, users) {
if(err) return res.serverError(err);
res.json(users);
}
This association ("pets"), you define it in your models "User" and "Pets" like the example above.
Populate is all about displaying the content of the (id) on which it was refreed
"abc": [{
type: ObjectId,
ref: "xyz"
}],

Resources