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();
Related
I have this opportunity model that has this field likes, which is an array of users. How do I check first if the user exist already in that array and if they do I pull them and if they don't I push them back I am building a like creteria for posts
Here is my opportunity model
const mongoose = require("mongoose");
const OpportunityModel = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
content: {
type: String,
required: true,
trim: true,
},
likes: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
],
image: [
{
type: Object,
},
],
share_with_thoughts: {
type: mongoose.Schema.Types.ObjectId,
ref: "Shares",
},
comments: {
type: mongoose.Schema.Types.ObjectId,
ref: "Comment",
},
is_opportunity_applied: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
],
});
const Opportunity = mongoose.model("Opportunity", OpportunityModel);
module.exports = Opportunity;
What I tried doing but did not work
const likeOrUnlikeOpportunity = expressAsyncHandler(async (req, res) => {
let userId = req.user._id;
let opportunityId = req.params.opportunityId;
let isUserExist = await Opportunity.find({
$and: [{ _id: opportunityId }, { likes: { $elemMatch: { $eq: userId } } }],
}).populate("user", "user_id user_name");
if (isUserExist.length > 0) {
const unliked = await Opportunity.findByIdAndUpdate(
opportunityId,
{
$pull: { users: userId },
},
{ new: true }
).populate("user", "user_id user_name");
if (!unliked) {
res.status(500).send({ ErrMessaage: "an error occured" });
} else {
res.status(200).json(unliked);
}
} else {
const added = await Opportunity.findByIdAndUpdate(
opportunityId,
{
$push: { users: userId },
},
{ new: true }
).populate("user", "user_id user_name");
if (!added) {
res.status(500).send({ ErrMessaage: "an error occured" });
} else {
res.status(200).json(added);
}
}
});
In this case, the action should be dynamic [like | unlike] function, You don't have to chain the function since the like and unlike function/api can't be called at the same time.
Make Your Query Conditional;
Just make sure you have a way to identify between like and unlike.
const {like, postId} = req.body;
let query = {[`${'$' + (like ? 'push' : 'pull')}`]: {likes: userId}};
//assumes that you have the post id
// you can decide not to wait for it to update, just to be sure it did update
await post.findByIdAndUpdate(postId, query).exec()
I hope this helps.
i'm trying to do a pretty simple operation, pull an item from an array with Mongoose on a Mongo database like so:
User.update({ _id: fromUserId }, { $pull: { linkedUsers: [idToDelete] } });
fromUserId & idToDelete are both Objects Ids.
The schema for Users goes like this:
var UserSchema = new Schema({
groups: [],
linkedUsers: [],
name: { type: String, required: true, index: { unique: true } }
});
linkedUsers is an array that only receives Ids of other users.
I've tried this as well:
User.findOne({ _id: fromUserId }, function(err, user) {
user.linkedUsers.pull(idToDelete);
user.save();
});
But with no luck.
The second option seem to almost work when i console the lenghts of the array at different positions but after calling save and checking, the length is still at 36:
User.findOne({ _id: fromUserId }, function(err, user) {
console.log(user.linkedUsers.length); // returns 36
user.linkedUsers.pull(idToDelete);
console.log(user.linkedUsers.length); // returns 35
user.save();
});
So it looks like i'm close but still, no luck. Both Ids are sent via the frontend side of the app.
I'm running those versions:
"mongodb": "^2.2.29",
"mongoose": "^5.0.7",
Thanks in advance.
You need to explicitly define the types in your schema definition i.e.
groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }],
linkedUsers: [{ type: Schema.Types.ObjectId, ref: 'User' }]
and then use either
User.findOneAndUpdate(
{ _id: fromUserId },
{ $pullAll: { linkedUsers: [idToDelete] } },
{ new: true },
function(err, data) {}
);
or
User.findByIdAndUpdate(fromUserId,
{ $pullAll: { linkedUsers: [idToDelete] } },
{ new: true },
function(err, data) {}
);
I had a similar issue. I wanted to delete an object from an array, using the default _id from mongo, but my query was wrong:
const update = { $pull: { cities: cityId }};
It should be:
const update = { $pull: { cities: {_id: cityId} }};
withe the following Group Schema,
group.model.js
const Role = new mongoose.Schema({
name: { type: String, required: true }, // ensure uniqueness withn group instance using addToSet
description: { type: String, required: false }
});
const GroupSchema = new mongoose.Schema({
name: { type: String, index: { unique: true, required: true, dropDups: true } },
description: { type: String, required: false },
roles: [Role],
createdAt: {
type: Date,
default: Date.now
}
});
I am trying to list all roles ( subdocument) got a specific group
group.controller.js
function listRoles(req, res) {
const group = req.group;
console.log('GROUP: %j', group);
const limit = parseInt(req.query.limit, 10) || 50;
const skip = parseInt(req.query.skip, 10) || 0;
Group.aggregate([
{ $match: { _id: req.params.groupId } },
{ $unwind: '$roles' },
{ $skip: skip },
{ $limit: limit }
], (err, result) => {
if (err) {
res.status(500);
res.json({ message: 'Error. Cannot list roles', errror: err });
}
res.status(200);
console.log('RESULT: %j', result);
res.json(result);
});
}
I should get an array with one role, but I get an empty array
what's wrong with my aggregate code ? thanks for feedback
note: I tried to aggregate only with the $match in the pipe and I also get an empty array... so I guess. the issue comes from the req.params.groupId should be an ObjectId .. how can I cast it ?
console.log
GROUP: {"_id":"5923e2e83afd4149bdf16c61","name":"Admin","description":"Administration group","__v":1,"createdAt":"2017-05-23T07:21:12.470Z","roles":[{"name":"Role1","description":"description role1","_id":"5923e2e83afd4149bdf16c62"}]}
RESULT: []
To better diagnose this, I'd recommend removing steps from your aggregation pipeline and seeing what the result is. However, I suspect your problem is because you have no match at the first stage because you're comparing a string to an ObjectId. Try this:
const mongoose = require('mongoose')
// and in the aggregation:
{ $match: { _id: mongoose.Types.ObjectId(req.params.groupId) } }
I am trying to obtain the object id for any article already in db so that I can validate that the article exists before comments are made.
The issue is on the router (/blog/article/comment). I cannot get the article object id from /blog/article/:postid. I want to pass this id to articleId like this:
articleId: req.params.postid
I have also tried:
articleId: req.article._id
model structure: comment.js
var mongoose = require('mongoose');
var CommentSchema = new mongoose.Schema({
content: { type: String },
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
articleId: { type: mongoose.Schema.Types.ObjectId, ref:'Article' },
dateCommented: { type: Date, default : Date.now }
});
Article model: article.js
var ArticleSchema = new mongoose.Schema({
category: { type: mongoose.Schema.Types.ObjectId, ref: 'Category' },
commentId:{type: mongoose.Schema.Types.ObjectId, ref:'Comment'},
title: String,
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User'},
blog: [{
topic: { type: String, unique: false, lowercase: true },
body: { type: String, unique: false, lowercase: true },
tags: [ 'first', 'mongodb', 'express'],
created: Date,
modified: { type : Date, default : Date.now },
state: { type: String, unique: false, lowercase: true }
}]
});
main.js
router.param('postid', function(req, res, next, id) {
if (id.length !=24) return next(new Error ('The post id is not having the correct length'));
//articleId: req.param('postid'),
Article.findOne({ _id: ObjectId(id)}, function(err, article) {
if (err) return next(new Error('Make sure you provided correct post id'));
req.article = article;
next();
});
});
router.get('/blog/article/:postid', function (req, res, next) {
Article.findById({ _id: req.params.postid }, function (err, article) {
if (err) return next(err);
res.render('main/publishedArticle', {
article: article
});
});
});
router.post('/blog/article/comment', function(req, res, next) {
async.waterfall([
function(callback) {
var comment = new Comment({
articleId: req.params.postid,
content: req.body.content,
user: req.user._id
});
comment.save(function(err) {
if (err) return next (err);
req.flash('success', 'Thank you for your comment');
callback(err, comment);
});
},
function(comment) {
Article.update({_id : comment.articleId }, { $set: { commentId: {} }}, function(err, updated) {
if (updated) {
res.redirect('/')
}
});
}
]);
});
Another issue I have is how to update the commentId for each comment in the Article
Article.update({_id : comment.articleId }, { $set: { commentId: {} }}, function(err, updated)
Since the /blog/article/comment route is a post request. Just submit your articleId in the body of that request. You'll have to send it up from the client. You can access it with req.body.articleID (If that is what you call the variable).
See here for more info on POST requests in node.
For your second question:
Within your article schema you have commentId, That is a single record. What you want is an array of comments. Something like this:
comments: [{type: mongoose.Schema.Types.ObjectId, ref:'Comment'}]
Then within your code...
...
function(comment) {
//comment should contain all the comments
//Grab the article
Article.findOne({ _id: comment.articleId}, function(err, article){
//Go through all the comments in 'comment' compare them with the ones in artcle.comments.
//The ones that aren't already in the article object get put into newComments...
var newComments = [];
Article.update({ _id: comment.articleId }, { $addToSet: { comments: newComments } }, function(err, updated) {
if (updated) {
res.redirect('/')
}
});
});
}
...
I didn't fully implement the code, but it should get you off to the right start.
addToSet Documentation
Some more examples of add to set
I'm trying to update a mongoose schema. Basically I have two api's '/follow/:user_id' and '/unfollow/:user_id'. What I'm trying to achieve is whenever user A follows user B , user B followers field in mongoose will increment as one.
As for now I managed to get only following fields increment by one but not the followers fields.
schema.js
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
});
Updated version: I tried my solution, but whenever I post it, it just fetching the data ( I tried the api's on POSTMAN chrome app ).
api.js
// follow a user
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!" });
}
});
});
These codes only manage to increment the user's following fields, and without duplication. How do I update logged in user's following fields and as well as other user's followers fields at the same time?
Updated version: it keeps fetching the data.
May be this is how you want to. Instead of using update, you can also use findOneAndUpdate from Mongoose queries.
apiRouter.post('/follow/:user_id', function(req, res) {
User.findOneAndUpdate(
{
_id: req.decoded.id
},
{
$push: {following: req.params.user_id},
$inc: {followingCount: 1}
},
function (err, user) {
if (err)
res.send(err);
User.findOneAndUpdate(
{
_id: req.params.user_id
},
{
$push: {followers: req.decoded.id},
$inc: {followersCount: 1}
},
function (err, anotherUser) {
if (err)
res.send(err);
res.json({message: "Successfully Followed!"})
});
});
}
If you are not sure about it is updated or not, you can just use console.log() for both user and anotherUser variables to see changes.