Creating and updating documents synchronously with mongoose - node.js

I'm wondering what the best approach would be to realize the following situation in Node.js and mongoose:
I'm having a collection with users and a collection with groups. I want users to be able to create new groups and add people to them (very similar to people creating a new whatsapp group). I use the following schemas for the user and group documents:
var userSchema = mongoose.Schema({
email: String,
hashed_password: String,
salt: String,
name: String,
groups: [{
_id: { type: Schema.Types.ObjectId, required: true, ref: 'groups' },
name: String
}]
});
var groupSchema= mongoose.Schema({
name: String,
creator: Schema.Types.ObjectId,
users: [{
_id: { type: Schema.Types.ObjectId, required: true, ref: 'users' },
rankInGroup: { type: Number, required: true }
}]
});
At this moment I have a function that takes the following arguments: the email address (groupCreator) of the user who is creating the group, the name of the new group (newGroupName), and the userids of the users (newGroupMembers) that need to be added to the group. This function first finds the users that need to be added to the group (using this method) and then adds the user ids of these users to the users array in the group document like this:
function(groupCreator, newGroupName, newGroupMembers , callback) {
userModel.findOne({
email: emailGroupCreator
}, function(err,creator){
//load document of user who creates the group into creator variable
var newGroup = new groupModel({
name: newGroupName,
creator: creator._id,
users: [{_id: creator._id, rank: 0}] //add group creator to the group
});
userModel.find({
email: { $in: newGroupMembers }
}, function(err,users){
//for now assume no error occurred fetching users
users.forEach(function(user) {
newGroup.users.push({_id: user._id, rank: 0}); //add user to group
user.groups.push({_id:newGroup._id,name:newGroup.name}); //add group to user
}
newGroup.save(function (err) {
//for now assume no error occurred
creator.groups.push({_id: newGroup._id, name: newGroup.name}); //add group to creator
creator.save(function (err) {
//for now assume no error occurred
/* save/update the user documents here by doing something like
newMembers.forEach(function(newMember) {
newMember.save(function (err) {
if(err){
callback(500, {success: false, response: "Group created but failure while adding group to user: "+newMember.email});
}
});
});
callback(200, {success: true, response: "Group created successfully!"});
*/
});
}
});
}
So I want this function to:
Find the user document of the group creator based on its email address stored in groupCreator
Create a new group (newGroup) and add the creator to its users array
Find all the users that need to be added to the group
Update the groups array in all the user documents with the groupid (newGroup._id) of the newly created group
Make a callback if all this is successfully executed
The problem here is that the updating of all the user documents of the users added to the group happens asynchronously, but I want to be sure that all the user documents are updated correctly before I return a success or failure callback. How can I update all the user documents before I continue with the rest of the code (maybe not using a foreach)? Is my initial approach of retrieving all the user documents good or are there better ways to do this?
So the bottom line question is; how can I save multiple user documents and continue with the rest of the code (send a callback to notify success or failure) after all the save actions are performed, or is there a way to save all the documents at once?
NB The reason why I want (some) of the same information in both the user and the group document is because I don't want to load all the group info for a user if he logs in, only the basic group information. See also this source under the section many-to-many relationships.

JohnnyHK pointed me in the right direction; async.each make it possible to iterate over the documents and update them one at a time. After that the rest of the code gets executed. This is how my code looks now:
async.each(groupMembersDocs, function (newMember, loopCallback) {
//console.log('Processing user ' + newMember.email + '\n');
userModel.update({
email: newMember.email
}, {
$push: { 'groups' : {_id: newGroup._id, name: newGroup.name} }
}, function (err, data) {
if(err){
console.log("error: "+err);
loopCallback('Failure.. :'+err);
} else{
newGroup.users.push({_id: newMember._id, rank: -1});
loopCallback();
}
});
}, function (err){
if(err){
console.log("error: "+err);
callback(500, {success: false, response: "Error while adding users to group"});
} else{
newGroup.save(function (err) {
callback(201, {success: true, response: "Group created successfully"});
});
}
})

Related

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
},
});

Custom Validation on a field that checks if field already exists and is active

I have a mongodb Collection "Users" having "Name", "Email", "Active" fields.
I want to add a validation that for every document email should be unique. However if a document is invalid i.e Active is false then the email can be accepted.
Here is the model
email: { type: String, validate: {
validator: function(v) {
return new Promise((resolve, reject)=> {
console.log("validating email")
const UserModel = mongoose.model('User');
UserModel.find({email : v, active: true}, function (err, docs)
{
if (!docs.length){
resolve();
}else{
console.log('user exists: ',v);
reject(new Error("User exists!"));
}
});
})
},
message: '{VALUE} already exists!'
}
},
name: {
type: String,
required: true
},
active: {
type: Boolean,
default: true
}
Problem is whenever i do any updation on this model then this validation is called.
So if i update the name then also this validation is called and it gives the error that email already exists.
How do I add a validation on email field so if someone adds a new entry to database or updates email it checks in database if existing user has same email id and is active?
I would first call Mongoose findOne function if the User is already registered the Mongo DB, for example;
let foundUser = await User.findOne({email});
if (!foundUser) {
// update user, create user etc.
...
}
I think it is better to not use logic inside the Mongoose document object. Maybe there is a way to achieve it but I prefer to do these validations in the code, not in the document, it is just my preference.
Also you can try making email unique as follows:
email: {
type: String,
unique: true
}
I'd use unique compound index, instead of having one more additional query to your db. Your code would look like this:
const schema = = new Schema(...);
schema.index({email: 1, active: 1}, {unique: true});
Mongo itself will reject your documents and you can catch it in your code like this:
const {MongoError} = require('mongodb'); // native driver
try {
await model.updateOne(...).exec(); // or .save()
} catch (err) {
//11000 is error code for unique constraints
if (err instanceof MongoError && err.code === 11000)
console.error('Duplicate email/active pair');
}

Trying to work out mongoose relationship coding for REST API

This is a stupid question but I have tried to wrap my head around this via Google, code snippits, tutorials, and all of them lead me to examples in which the models are too shallow for the coding I want to do.
I have an app I want to develop where data is in the form of parents and children:-
- Organisation
- Projects that belong to those organisations
- Releases that belong to those projects
and so on, but I don't fully understand how I can write a route in express that follows said hierachy and I come from an SQL relational background. Do I use cookies, or part of the route? I know how to set up the model, from what I understand, using:
var organisationSchema = ({
name: String,
email: String,
description: String,
users: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}]
});
for Organisation and
var projectSchema = ({
name: String,
description: String,
users: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}]
}
for project but then how do I set up my post route to add the project to the correct organisation
router.route('/api/project')
.post(function(req, res){
project = new Project();
project.name = req.body.name;
project.organisation = req.body.organisation;
if (err)
res.send(err);
})
project.save(function(err){
if (err)
res.send(err);
res.json({ message: 'Project ' + project.name + ' created.'});
})
})
Do I need a cookie to populate the organisation in this example?
If your projects belong to organizations, you'll either want to include an array of objectIds in your organization schema which will contain project IDs or a field in your project schema that will contain the relevant organization ID.
You can send the proper organization either in the body, as you are, or in the URL parameters.
For example, something similar to this:
router.route('/api/:organizationId/project')
.post(function(req, res, next) {
var project = new Project({
name: req.body.name,
organization: req.params.organizationId
});
if(err) { return next(err); }
project.save(function(err, savedProject) {
if(err) { return next(err); }
return res.status(200).json({ message: "Project " + savedProject.name + " created." });
}
}
If you pay attention to the communication/messaging model and routes in this repository, it might help: https://github.com/joshuaslate/mern-starter/tree/master/server

How to get the newly created embedded document with Mongoose?

I have a schema with embedded documents, the setup looks like this:
var Guests = new Schema({
email: {
type: String,
required: true
},
time: {
type: Date,
default: Date.now
}
});
var Devices = new Schema({
user: {
type: String,
required: true
},
time: {
type: Date,
default: Date.now
},
name: {
type: String,
required: true
},
guests: [Guests]
});
I create a new guest with the following code:
// Check if already invited
Device.findOne({ '_id': req.body.device_id, 'user': req.user.href, 'guests.email': req.body.guest_email }, function (err, guest) {
// Check for error
if (err) return handleError(err);
// If already invited
if (guest) {
return res.status(402).send({code: 'already_invited', message: 'This guests is already invited.'});
} else {
// If not invited yet, lets create the new guest
device.guests.push({
"email": req.body.guest_email
});
// Save the new guest to device
device.save(function (err) {
if (err) res.status(400).send(err);
// Get the saved guest ID, but how?
});
}
});
Everything works, but i don't know how to get the newly created embedded guest document. I especially need the ID, but i want the whole new guest in my response. Inside the device.save function, device.guests already has the ID of the newly created record, so i could loop through it and find it that way, but i'm wondering if theres an easier way to do it.
device.save(function (device, err) {
if (err) res.status(400).send(err);
// use device here. i.e res.status(200).json(device);
});
If you actually create a Guests entity with the new operator, the _id property will be populated. Try something like this:
var guest = new Guests({guest_email: "foo"});
device.guests.push(guest);

Mongoose - trying to do 'JOINS' in MEAN stack

I am having a hard time understanding the async nature of NodeJS.
So, I have an articles object with this schema:
var ArticleSchema = new Schema({
created: {
type: Date,
default: Date.now
},
title: {
type: String,
default: '',
trim: true,
required: 'Title cannot be blank'
},
content: {
type: String,
default: '',
trim: true
},
creator: {
type: Schema.ObjectId,
ref: 'User'
}
});
and the User schema is:
var UserSchema = new Schema({
firstName: String,
lastName: String,
...
});
The problem is when I query for all the documents like so:
exports.list = function(req, res) {
// Use the model 'find' method to get a list of articles
Article.find().sort('-created').populate('creator', 'firstName lastName fullName').exec(function(err, articles) {
if (err) {
// If an error occurs send the error message
return res.status(400).send({
message: getErrorMessage(err)
});
} else {
// Send a JSON representation of the article
res.json(articles);
}
});
};
I get all the articles back successfully, but for some reasons, the article creator is returning different results
for locally authenticated users (localStrategy) and facebook authenticated users (facebook strategy) for locally authenticated users, I get:
articles = {
creator: {
id: 123,
firstName: 'Jason',
lastName: 'Dinh'
},
...
}
for fb authenticated users, I get:
articles = {
creator: {
id: 123
},
...
}
I can't seem to get a grip on PassportJS API, so what I want to do is
iterate through articles and for each article, find the user document using the article creator ID and add the user firstName and lastName to the articles object:
for each article in articles {
User.findOne({ '_id': articles[i].creator._id }, function(err, person){
//add user firstName and lastName to article
});
}
res.json(articles);
You can probably already see the problem here... my loop finishes before the documents are returned.
Now, I know that MongoDB doesn't have any 'joins' and what I want to do is essentially return a query that 'joins' two collections. I think I'm running into problems because I don't fundamentally understand the async nature of
node.
Any help?
You can use find instead of findOne and iterate inside your callback function.
User.find({ }, function(err, personList){
for each person in personList {
for each article in articles {
if (person._id === article.creator._id) {
//add user firstName and lastName to article
}
}
}
res.json(articles);
});
UPDATE:
Considering the scenario that #roco-ctz proposed (10M users), you could set a count variable and wait for it to be equal to articles.length:
var count = 0;
for each article in articles {
User.findOne({ '_id': articles[i].creator._id }, function(err, person){
//add user firstName and lastName to article
count += 1;
});
}
while (count < articles.length) {
continue;
}
res.json(articles);

Resources