Delete user id from documents after deleting user - node.js

I'm building social network app using nodejs and mongodb. Now in my user schema i have an array of ids of users who are following certain user and ids of users who are followed by certain user. When i delete the user i want to delete his id from all arrays inside of users who are following him. So that if he deletes his accont he is not going to be followed by any user anymore
following: [{ type: mongoose.Schema.ObjectId, ref: "User" }],
followers: [{ type: mongoose.Schema.ObjectId, ref: "User" }],

exports.deleteUser = catchAsync(async (req, res, next) => {
await User.findByIdAndRemove(req.params.id);
const data = await User.updateMany(
{
following: {
$in: [req.params.id],
},
},
{
$pull: {
following: req.params.id,
},
}
);
res.status(200).json({
status: "success",
data,
});
});
It worked like this. I'm just interested is this the right way.

Mongoose middlewares are used for uses cases similar to yours, you can specify a middleware function to run before a defined operation (here we specify "remove" and the middleware runs before the operation using "pre" here is a simple implementation:
const userSchema = new Schema(
{
username: {
type: Schema.Types.String,
required: true,
},
//.....
});
userSchema.pre('remove', async function (next) {
// remove userid from all following arrays in users collection
// reference user with "this"
await userModel.updateMany(
{
following: {
$in: [this._id]
}
},
{
$pull: {
following: { _id: this._id }
}
});
// calling next will call next middleware which will delete user
next();
});

Related

How to find value inside array of object with mongoose?

I have events system with different role for each event (same user could be different role in different events).
I created collection of the users and this is the schema that i used:
const userSchema = new mongoose.Schema(
{
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
permissions: [{
eventId: { type: mongoose.Schema.Types.ObjectId, required: false, ref: 'Event' },
role: { type: String, required: false }
}]
},
{timestamps: true}
);
For check if the user is allowed to get this event I created middleware that need to check if the eventId is exist in the User collection under "permissions"
so this is the code that I was create:
const authorization = async (req, res, next) => {
try {
const eventId = req.params.id;
const token = req.headers.authorization.split(' ')[1]
const tokenDecoded = jwt.verify(token, process.env.JWT_SECRET);
const userId = tokenDecoded.id
console.log(userId)
const userPermissionCheck = await User.find({ _id: userId, 'permissions.eventId': { $in: eventId } } );
console.log(userPermissionCheck)
next();
} catch (error) {
res.status(401).json({ message: 'Auth failed.' })
}
}
My problem is that my find function in the authorization middleware is not working...
What the correct way to search key of object in array with mongoose?
thanks
It seems that you are on the right track from your code, but you do not need the $in operator. You should be able to do the following:
const userPermissionCheck = await User.find({ _id: userId, 'permissions.eventId': eventId });

Delete all nested items in database when deleting a top item using MongoDB and Express

I have three collections in my mongodb: clients, programs, and data. Data is nested in Programs and Programs are nested in Clients. That is a single client can have multiple programs and every such program can have a number of Data sets. Here is how clients' schema looks like:
const ClientSchema = new Schema({
fullName: { type: String, required: true },
dateEnrolled: { type: Date, required: true },
isCurrent: { type: String, required: true },
Programs: [{ type: Schema.Types.ObjectId, ref: 'Program' }]
});
// and my Programs schema looks like:
const ProgramSchema = new Schema({
programName: { type: String, required: true },
dateStarted: { type: Date, required: true },
dateFinished: { type: Date },
Data: [{ type: Schema.Types.ObjectId, ref: 'Answer' }]
});
Now, when I delete a client from my database I want to delete all the programs that belong to the clients and all the data sets that were created for those programs. Please, help me.
Try something like this:
router.delete('/:clientId', (req, res, next) => {
const id = req.params.id;
Client.findById(id).then((client) => {
client.Programs.forEach((programId) => {
Program.findById(programId).then((program) => {
Answer.deleteMany({ _id: { $in: program.Data } });
});
Program.deleteOne({ _id: programId })
});
Client.deleteOne({ _id: client._id })
})
});
You can register a middleware function for remove in ClientSchema.
ClientSchema.pre('remove', { query: true, document: true }, async function() {
// call deleteMany on ProgramModel
await ProgramModel.deleteMany({ _id: { $in: this.Programs } });
});
You can do the same thing for ProgramSchema if you want to cascade delete Answer.
Nenad, thank you very much, again! You gave me an idea to loop through first program ids, then through programs. My solution comes in two parts:
Part 1:
`router.delete('/:id', (async (req, res, next) => {
const { id } = req.params;
const client = await Client.findById(id);
await client.Programs.forEach(async (element) => {
const programs = [];
const program = await Program.findById(element);
programs.push(program);
programs.forEach(async (program) => {
await Answer.deleteMany({ _id: { $in: program.Data } });
})
});
await Client.findByIdAndDelete(id);
res.redirect('/clients');
}))`
part 2 that goes to Client schema file and makes sure that all the programs are deleted as well:
`ClientSchema.post('findOneAndDelete', async function (doc) {
if (doc) { await Program.deleteMany({ _id: { $in: doc.Programs } }) }
})`
Now, when I delete a client all instances of related programs are deleted and all instances of data-sets related to each program are deleted, too. Thank you, guys, are all awesome!

How to trigger a function whenever a mongoose document is updated

I have a user model schema in mongoose which contains a list of friends and groups and stats info like so...
var user = new Schema({
email: { type: String, required: true, unique: true },
password: { type: String, required: true, select: false },
roles: [{ type: String, required: true }],
friends: [{ type: Schema.Types.ObjectId, ref: 'User' }],
groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }],
stats : {
nbrFriends: { type: Number, required: false },
nbrGroups: { type: Number, required: false }
}
}, {
timestamps: true
});
I need to update the users stats whenever a change is made to the friends or groups fields to contain the new number of friends or groups etc. For example, when the following function is called on a user:
var addGroup = function(user, group, cb) {
user.groups.push(group);
User.findOneAndUpdate({ _id: user._id }, { $set: { groups: user.groups }}, { new: true }, function(err, savedResult) {
if(err) {
return cb(err);
}
console.log('updated user: ' + JSON.stringify(savedResult));
return cb(null, savedResult);
});
};
How could I make sure the stats is automatically updated to contain the new number of groups the user has? It seems like a middleware function would be the best approach here. I tried the following but this never seems to get called...
user.pre('save', function(next) {
var newStats = {
nbrGroups: this.groups.length,
nbrPatients: this.friends.length
};
this.stats = newStats;
this.save(function(err, result) {
if(err) {
console.log('error saving: ' + err);
} else {
console.log('saved');
}
next();
});
});
You need to use the middleware a.k.a. hooks:
Middleware (also called pre and post hooks) are functions which are passed control during execution of asynchronous functions.
See the docs:
http://mongoosejs.com/docs/middleware.html
From version 3.6, you can use change streams.
Like:
const Users = require('./models/users.js')
var filter = [{
$match: {
$and: [{
$or:[
{ "updateDescription.updatedFields.friends": { $exists: true } },
{ "updateDescription.updatedFields.groups": { $exists: true } },
]
{ operationType: "update" }]
}
}];
var options = { fullDocument: 'updateLookup' };
let userStream = Users.watch(filter,options)
userStream.on('change',next=>{
//Something useful!
})
You should update with vanilla JS and then save the document updated to trigger the pre-save hooks.
See Mongoose docs
If you have many keys to update you could loop through the keys in the body and update one by one.
const user = await User.findById(id);
Object.keys(req.body).forEach(key => {
user[key] = req.body[key];
}
const saved = await user.save();

add a item inside a nested schema mongoose with addToSet

I know populating schemas is not a new question but I am having a little trouble following the logic on this in regards to multiple schemas. I am working with
"mongoose": "^4.8.5",
"express": "^4.15.0",
I have a schema with a collection of caffeine drinks. When a user selects a drink i would like for that drink to be assigned to the user.
** If at any point I am missing something simple in the architecture please let me know. This project has been my intro to mongodb.
I am reading through populating on the mongoose documentation http://mongoosejs.com/docs/populate.html.
Essentially, if I am to assign the drinks to the list it looks like I want to add them as a reference in an array. This was my approach with caffeine_list
const SelectedDrinks = require('./userDrinks');
const UserSchema = mongoose.Schema({
name: {
type: String
},
email: {
type: String,
required: true
},
username: {
type: String,
required: true
},
password: {
type: String,
required: true
},
caffeine_list: caffeine_list: [ // attempting to reference selected drinks
{
type: mongoose.Schema.Types.ObjectId,
ref: 'SelectedDrinks'
}
]
})
SelectedDrinks comes from the schema below. I added a reference to the user as the creator below
const User = require('./user');
let userDrinkSchema = new mongoose.Schema({
creator : {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
caffeine: Number,
mgFloz: Number,
name: String,
size: Number,
updated_at: {
type: Date,
default: Date.now()
}
});
This is where I start to get confused. I initially tried populate but could not get it going. If that was correct please let me know.
In regards to my task of adding a selected drink to the user I used addToSet. I was hoping that this would give me the drink info. I did my set up like so....
const User = require('../../models/user');
const UserDrinks = require('../../models/userDrinks');
router.post('/addDrink', (req, res, next) => {
let newDrink = new UserDrinks({
creator: req.body.creator,
caffeine: req.body.caffeine,
mgFloz: req.body.mgFloz,
name: req.body.name,
size: req.body.size,
updated_at: req.body.updated_at
});
newDrink.save( (err) => {
if(err) {
res.send(err);
} else {
User.findOne({ _id: newDrink.creator}, (err, user) => {
user.caffeine_list.addToSet(newDrink)
user.save( function (err) {
if(err) {
console.log(err);
}else {
res.status(201).json(newDrink);
}
})
})
}
})
});
However, after i do a post in postman I check caffeine_list and the result is
"caffeine_list" : [
ObjectId("58d82a5ff2f85e3f21822ab5"),
ObjectId("58d82c15bfdaf03f853f3864")
],
Ideally I would like to have an array of objects being passed with the caffeine info like so
"caffeine_list" : [
{
"creator": "58d6245cc02b0a0e6db8d257",
"caffeine": 412,
"mgFloz": 218.7,
"name": "1.95 Perfect Drink!",
"size": 42.93,
"updated_at": "2017-03-24T18:04:06.357Z"
}
]
Change your else part with below code instead of findOne and save use update
User.update(
{ _id: newDrink.creator},
{ $addToSet:{
caffeine_list: newDrink
}}).exec(function (err, updatedrink){
if(err) {
console.log(err);
}else {
res.status(201).json(updatedrink);
}
})
Although I am not sure this is the best approach I did find this to be give me the result that I was desiring. I had to make two small changes and I was able to get the caffeine_list to give me the desired response
I had to access the schema for selected drinks
const SelectedDrinks = require('./userDrinks').schema; //** need schema
Afterwards I was able to change
caffeine_list: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'UserDrinks' // name of the file
}
]
to
caffeine_list: [SelectedDrinks]
Now that I have the schema I am able to add the drinks directly into the caffeine_list on the UserSchema.

Updating 2 mongoose schemas in an api call

Currently I'm trying to update Two different User Schema's in an api call.
The first schema is logged in user schema, we give it a name = Tom
The second schema is other users who signup for the app, we give it a name = John
The schema code
schema.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var bcrypt = require('bcrypt-nodejs');
var UserSchema = new Schema({
name: String,
username: { type: String, required: true, index: { unique: true }},
password: { type: String, required: true, select: false },
followers: [{ type: Schema.Types.ObjectId, ref: 'User'}],
following: [{ type: Schema.Types.ObjectId, ref: 'User'}],
followersCount: Number,
followingCount: Number
});
module.exports = mongoose.model('User', UserSchema);
The api name is '/follow/:user_id', what I want to achieve is . Whenever user Tom follows other user's like John, Tom's following field will be updated as well as John's follower field.
My current attempt (req.decoded.id is the logged in user)
api.js
// The first way
apiRouter.post('/follow/:user_id', function(req, res) {
User.findOneAndUpdate(
{
_id: req.decoded.id,
following: { $ne: req.params.user_id }
},
{
$push: { following: req.params.user_id},
$inc: { followingCount: 1}
},
function(err, currentUser) {
if (err) {
res.send(err);
return;
}
console.log(currentUser);
});
User.findOneAndUpdate(
{
_id: req.params.user_id,
followers: { $ne: req.decoded.id }
},
{
$push: { followers: req.decoded.id },
$inc: { followersCount: 1}
}, function(err, user) {
if(err) {
res.send(err);
return;
}
res.json({
message: "Successfully followed"
});
}
)
});
//Second way
apiRouter.post('/follow/:user_id', function(req, res) {
// find a current user that has logged in
User.update(
{
_id: req.decoded.id,
following: { $ne: req.params.user_id }
},
{
$push: { following: req.params.user_id},
$inc: { followingCount: 1}
},
function(err) {
if (err) {
res.send(err);
return;
}
User.update(
{
_id: req.params.user_id,
followers: { $ne: req.decoded.id }
},
{
$push: { followers: req.decoded.id },
$inc: { followersCount: 1}
}
), function(err) {
if(err) return res.send(err);
res.json({ message: "Successfully Followed!" });
}
});
});
Both have problems,
The first way: The problem is, 'Can't set headers that already sent', because of the two separate mongoose query in one api call, it response twice that's why I get that error.
The second way: The problem is, the following field of logged in user(Tom) gets updated while the other user's followers field (John) return null. I console log both value and as well test it with POSTMAN chrome app.
Lend me your thoughts fellas!
The first route you took seems to be fine.
However, as #cdbajorin mentioned, the error "can't send headers that already sent" has nothing to do with mongoose but the fact that you're trying to set the header after sending a response to the client already. (see this lovely answer)
My suggestion would be to make sure that both database calls are successful before you send a response.
You may also want to look into a two phase commit in this situation, as MongoDB does not support traditional DB transactions and you're updating two documents, one at a time. If for some reason either database call fails, a procedure to recover to a stable state should be taken.
The first way can be improved in two ways. One is updating followers field inside the callback of updating following field. The other way is using async-waterfall. I suggest to go with async-waterfall(npm async-waterfall).
The second way it is correct (could be improved running both of them in parallel) I guess the problem is in another place. I don't know which framework you are using but i guess the field _id is from mongoDB and is an ObjectId and looks like that the decoded.id can be an objectId while the one that comes from the request is of course just a string. So I guess it is empty because it does not find any user with that string.
Try do make it an objectId out of that string ( reffering to req.params.user_id in the second query)

Resources