findOneAndUpdate subdocument fields are overwriten - node.js

I have a shema with nested documents:
var BlogSchema = new mongoose.Schema({
...
comments : [Comments],
...
});
and trying to update a comment
router.post('/comments/:id', function (req, res, next) {
Blog.findOneAndUpdate(
{ 'comments._id': req.params.id },
{ "$set": {"comments.$": req.body}},
function (err, post) {
if (err) {
return next(err);
}
res.json(post);
});
});
with request body
{"text": "test comment 2"}
On the request launch it returns a blog post with unchanged comments. Launching GET request I receive updated blog post with the correct comment updated, but: it lost all fields, except text with new content. Even _id is removed.
Can I use findAndUpdate function with such "partial" input objects, modifying only fields mentioned there, but keeping others as is?
And how may I return already modified object from the function without additional find-like requests?

Related

deleteMany with Mongoose using array

I am trying to batch delete using deleteMany via Mongoose. Currently I have a few rows with check-boxes and a submit button which POSTs an array of IDs to my deleteMany endpoint like this,
router.get('/list/batchDelete', secured()).delete(function(req, res) {
Booking.deleteMany(
{
_id: {$in: [req.params.ids]},
},
function(err, rowsToDelete) {
if (!err) {
res.send(rowsToDelete);
res.redirect('/list');
} else {
res.send(err);
console.log('Error in batch delete :' + err);
}
},
);
});
I can see the req.params.ids payload.
POSThttp://localhost:8000/list/batchDelete
[HTTP/1.1 404 Not Found 34ms]
Request payload
ids%5B%5D=5e1da4db2f11682b506fc6c8&ids%5B%5D=5e1da522becbb13f24748012&ids%5B%5D=5e1da57a5c7f911db82e5731
But I keep getting Cannot POST /list/batchDelete
Please, what am I missing?
I referred to:
Mongoose Delete Many by Id
Mongoose Docs: Query.prototype.deleteMany()
UPDATE:
I added a post route like this, which now produces 200OK but in the browser JSON view and still no change to the dataset.
router.post('/list/batchDelete', function(req, res) {
const ids = req.body.ids;
res.send(ids);
res.redirect('/list');
});
Use your route like this
router.post('/list/batchDelete', async (req, res) {
const {ids} = req.body;
await Booking.deleteMany(
{
_id: {$in: ids},
})
return res.send('record deleted');
});
Use postman for api call
Call should be POST and on /list/batchDelete route
Body should contain array of ids
e.g {"ids":['id1','id2']}
This will solve your problem of deleting records.

How can I retrieve documents' properties from a pre hook?

I posted this question yesterday because I didn't know how to solve my problem.
Change variable value in document after some time passes?
I was told I need to use a pre hook. I tried to do it, but "this" would refer to the query, not to the document. So I couldn't retrieve the documents to check if the 4 weeks passed. (check the question, you will get it)
Because I don't know how to make this .pre('find') to use variables from each of my document (so it checks if the 4 weeks passed) I was thinking about looping through all of them and checking if 4 weeks passed.
router.get('/judet/:id([0-9]{2})', middleware.access2, function(req, res)
{
var title = "Dashboard";
Somer.find({}, function(err, someri)
{
if(err)
{
console.log(err);
}
else
{
res.render("dashboard", {title: title, id:req.params.id, someri:someri});
}
});
}); ///get route
var someriSchema = new mongoose.Schema({
nume: {type: String, required: true},
dateOfIntroduction: {type:Date, default: Date.now, get: formatareData},
});
someriSchema.pre('find', function(next) {
console.log(this.dateOfIntroduction); <- this will return undefined, because this refers to the query, actually
next();
});///schema and the pre hook. I thought I could use it like this, and inside the body of the pre hook I can check for the date
Here's what I am talking about:
router.get('/judet/:id([0-9]{2})', middleware.access2, function(req, res)
{
var title = "Dashboard | Best DAVNIC73";
Somer.find({}, function(err, someri)
{
if(err)
{
console.log(err);
}
else
{
someri.forEach(function(somer)
{
///check if 4 weeks passed and then update the deactivate variable
})
res.render("dashboard", {title: title, id:req.params.id, someri:someri});
}
});
});
but I think this will be very bad performance-wise if I will get many entries in my DBs and I don't think this is the best way to do this.
So, if I was told correctly and I should use a pre hook for obtaining what I've said, how can I make it refer to the document?
Ok, I think I understood your requirements. this is what you could do:
/*
this will always set a documents `statusFlag` to false, if the
`dateOfIntroduction` was before Date.now()
*/
const mongoose = require('mongoose')
someriSchema.pre('find', function(next) {
mongoose.models.Somer.update(
{ datofIntroduction: { $lte: new Date() }},
{ statusFlag : false})
.exec()
.then((err, result) => {
// handle err and result
next();
});
});
The only problem I see, is that you are firing this request on every find.
in query middleware, mongoose doesn't necessarily have a reference to
the document being updated, so this refers to the query object rather
than the document being updated.
Taken straight from the documentation of mongoose
I pointed you yesterday to their documentation; but here is a more concrete answer.
someriSchema.post('find', function(res) {
// res will have all documents that were found
if (res.length > 0) {
res.forEach(function(someri){
// Do your logic of checking if 4 weeks have passed then do the following
someri.deactivated = true
someri.save()
})
}
})
What this basically do is for every found schema you would update their properties accordingly, your res can have only 1 object if you only queried 1 object. your second solution would be to do the cron
EDIT: This is what you would do to solve the async issue
const async = require('async')
someriSchema.post('find', function(res) {
async.forEach(res, function(someri, callback) {
// Do your logic of checking if 4 weeks have passed
// then do the following - or even better check if Date.now()
// is equal to expiryDate if created in the model as suggested
// by `BenSow`
// Then ONLY if the expiry is true do the following
someri.deactivated = true
someri.save(function (err) {
err ? callback(err) : callback(null)
})
}, function(err){
err ? console.log(err) : console.log('Loop Completed')
})
})

Need to send response after forEach is done

I'm working with NodeJS + Mongoose and I'm trying to populate an array of objects and then send it to the client, but I can't do it, response is always empty because it is sent before forEach ends.
router.get('/', isAuthenticated, function(req, res) {
Order.find({ seller: req.session.passport.user }, function(err, orders) {
//handle error
var response = [];
orders.forEach(function(doc) {
doc.populate('customer', function(err, order) {
//handle error
response.push(order);
});
});
res.json(response);
});
});
Is there any way to send it after the loop has finished?
Basically, you could use any solution for async control flow management like async or promises (see laggingreflex's answer for details), but I would recommend you to use specialized Mongoose methods to populate the whole array in one MongoDB query.
The most straightforward solution is to use Query#populate method to get already populated documents:
Order.find({
seller: req.session.passport.user
}).populate('customer').exec(function(err, orders) {
//handle error
res.json(orders);
});
But if, for some reason, you can't use this method, you could call Model.populate method yourself to populate an array of already fetched docs:
Order.populate(orders, [{
path: 'customer'
}], function(err, populated) {
// ...
});
One solution is to use Promises.
var Promise = require('bluebird');
Promise.promisifyAll(Order);
router.get('/', isAuthenticated, function(req, res) {
Order.findAsync({ seller: req.session.passport.user })
.then(function(orders) {
return Promise.all(orders.map(function(doc){
return Promise.promisify(doc.populate).bind(doc)('customer');
}));
}).then(function(orders){
// You might also wanna convert them to JSON
orders = orders.map(function(doc){ return doc.toJSON() });
res.json(orders);
}).catch(function(err){
//handle error
});
});
BlueBird's .promisifyAll creates an …Async version of all functions of an object, which saves you an extra step in configuring the initial promise. So instead of Order.find I used Order.findAsync in above example

Sails.js populate nested associations

I've got myself a question regarding associations in Sails.js version 0.10-rc5. I've been building an app in which multiple models are associated to one another, and I've arrived at a point where I need to get to nest associations somehow.
There's three parts:
First there's something like a blog post, that's being written by a user. In the blog post I want to show the associated user's information like their username. Now, everything works fine here. Until the next step: I'm trying to show comments which are associated with the post.
The comments are a separate Model, called Comment. Each of which also has an author (user) associated with it. I can easily show a list of the Comments, although when I want to display the User's information associated with the comment, I can't figure out how to populate the Comment with the user's information.
In my controller i'm trying to do something like this:
Post
.findOne(req.param('id'))
.populate('user')
.populate('comments') // I want to populate this comment with .populate('user') or something
.exec(function(err, post) {
// Handle errors & render view etc.
});
In my Post's 'show' action i'm trying to retrieve the information like this (simplified):
<ul>
<%- _.each(post.comments, function(comment) { %>
<li>
<%= comment.user.name %>
<%= comment.description %>
</li>
<% }); %>
</ul>
The comment.user.name will be undefined though. If I try to just access the 'user' property, like comment.user, it'll show it's ID. Which tells me it's not automatically populating the user's information to the comment when I associate the comment with another model.
Anyone any ideals to solve this properly :)?
Thanks in advance!
P.S.
For clarification, this is how i've basically set up the associations in different models:
// User.js
posts: {
collection: 'post'
},
hours: {
collection: 'hour'
},
comments: {
collection: 'comment'
}
// Post.js
user: {
model: 'user'
},
comments: {
collection: 'comment',
via: 'post'
}
// Comment.js
user: {
model: 'user'
},
post: {
model: 'post'
}
Or you can use the built-in Blue Bird Promise feature to make it. (Working on Sails#v0.10.5)
See the codes below:
var _ = require('lodash');
...
Post
.findOne(req.param('id'))
.populate('user')
.populate('comments')
.then(function(post) {
var commentUsers = User.find({
id: _.pluck(post.comments, 'user')
//_.pluck: Retrieves the value of a 'user' property from all elements in the post.comments collection.
})
.then(function(commentUsers) {
return commentUsers;
});
return [post, commentUsers];
})
.spread(function(post, commentUsers) {
commentUsers = _.indexBy(commentUsers, 'id');
//_.indexBy: Creates an object composed of keys generated from the results of running each element of the collection through the given callback. The corresponding value of each key is the last element responsible for generating the key
post.comments = _.map(post.comments, function(comment) {
comment.user = commentUsers[comment.user];
return comment;
});
res.json(post);
})
.catch(function(err) {
return res.serverError(err);
});
Some explanation:
I'm using the Lo-Dash to deal with the arrays. For more details, please refer to the Official Doc
Notice the return values inside the first "then" function, those objects "[post, commentUsers]" inside the array are also "promise" objects. Which means that they didn't contain the value data when they first been executed, until they got the value. So that "spread" function will wait the acture value come and continue doing the rest stuffs.
At the moment, there's no built in way to populate nested associations. Your best bet is to use async to do a mapping:
async.auto({
// First get the post
post: function(cb) {
Post
.findOne(req.param('id'))
.populate('user')
.populate('comments')
.exec(cb);
},
// Then all of the comment users, using an "in" query by
// setting "id" criteria to an array of user IDs
commentUsers: ['post', function(cb, results) {
User.find({id: _.pluck(results.post.comments, 'user')}).exec(cb);
}],
// Map the comment users to their comments
map: ['commentUsers', function(cb, results) {
// Index comment users by ID
var commentUsers = _.indexBy(results.commentUsers, 'id');
// Get a plain object version of post & comments
var post = results.post.toObject();
// Map users onto comments
post.comments = post.comments.map(function(comment) {
comment.user = commentUsers[comment.user];
return comment;
});
return cb(null, post);
}]
},
// After all the async magic is finished, return the mapped result
// (or an error if any occurred during the async block)
function finish(err, results) {
if (err) {return res.serverError(err);}
return res.json(results.map);
}
);
It's not as pretty as nested population (which is in the works, but probably not for v0.10), but on the bright side it's actually fairly efficient.
I created an NPM module for this called nested-pop. You can find it at the link below.
https://www.npmjs.com/package/nested-pop
Use it in the following way.
var nestedPop = require('nested-pop');
User.find()
.populate('dogs')
.then(function(users) {
return nestedPop(users, {
dogs: [
'breed'
]
}).then(function(users) {
return users
}).catch(function(err) {
throw err;
});
}).catch(function(err) {
throw err;
);
Worth saying there's a pull request to add nested population: https://github.com/balderdashy/waterline/pull/1052
Pull request isn't merged at the moment but you can use it installing one directly with
npm i Atlantis-Software/waterline#deepPopulate
With it you can do something like .populate('user.comments ...)'.
sails v0.11 doesn't support _.pluck and _.indexBy use sails.util.pluck and sails.util.indexBy instead.
async.auto({
// First get the post
post: function(cb) {
Post
.findOne(req.param('id'))
.populate('user')
.populate('comments')
.exec(cb);
},
// Then all of the comment users, using an "in" query by
// setting "id" criteria to an array of user IDs
commentUsers: ['post', function(cb, results) {
User.find({id:sails.util.pluck(results.post.comments, 'user')}).exec(cb);
}],
// Map the comment users to their comments
map: ['commentUsers', function(cb, results) {
// Index comment users by ID
var commentUsers = sails.util.indexBy(results.commentUsers, 'id');
// Get a plain object version of post & comments
var post = results.post.toObject();
// Map users onto comments
post.comments = post.comments.map(function(comment) {
comment.user = commentUsers[comment.user];
return comment;
});
return cb(null, post);
}]
},
// After all the async magic is finished, return the mapped result
// (or an error if any occurred during the async block)
function finish(err, results) {
if (err) {return res.serverError(err);}
return res.json(results.map);
}
);
You could use async library which is very clean and simple to understand. For each comment related to a post you can populate many fields as you want with dedicated tasks, execute them in parallel and retrieve the results when all tasks are done. Finally, you only have to return the final result.
Post
.findOne(req.param('id'))
.populate('user')
.populate('comments') // I want to populate this comment with .populate('user') or something
.exec(function (err, post) {
// populate each post in parallel
async.each(post.comments, function (comment, callback) {
// you can populate many elements or only one...
var populateTasks = {
user: function (cb) {
User.findOne({ id: comment.user })
.exec(function (err, result) {
cb(err, result);
});
}
}
async.parallel(populateTasks, function (err, resultSet) {
if (err) { return next(err); }
post.comments = resultSet.user;
// finish
callback();
});
}, function (err) {// final callback
if (err) { return next(err); }
return res.json(post);
});
});
As of sailsjs 1.0 the "deep populate" pull request is still open, but the following async function solution looks elegant enough IMO:
const post = await Post
.findOne({ id: req.param('id') })
.populate('user')
.populate('comments');
if (post && post.comments.length > 0) {
const ids = post.comments.map(comment => comment.id);
post.comments = await Comment
.find({ id: commentId })
.populate('user');
}
Granted this is an old question, but a much simpler solution would be to loop over the comments,replacing each comment's 'user' property (which is an id) with the user's full detail using async await.
async function getPost(postId){
let post = await Post.findOne(postId).populate('user').populate('comments');
for(let comment of post.comments){
comment.user = await User.findOne({id:comment.user});
}
return post;
}
Hope this helps!
In case anyone is looking to do the same but for multiple posts, here's one
way of doing it:
find all user IDs in posts
query all users in 1 go from DB
update posts with those users
Given that same user can write multiple comments, we're making sure we're reusing those objects. Also we're only making 1 additional query (whereas if we'd do it for each post separately, that would be multiple queries).
await Post.find()
.populate('comments')
.then(async (posts) => {
// Collect all comment user IDs
const userIDs = posts.reduce((acc, curr) => {
for (const comment of post.comments) {
acc.add(comment.user);
}
return acc;
}, new Set());
// Get users
const users = await User.find({ id: Array.from(userIDs) });
const usersMap = users.reduce((acc, curr) => {
acc[curr.id] = curr;
return acc;
}, {});
// Assign users to comments
for (const post of posts) {
for (const comment of post.comments) {
if (comment.user) {
const userID = comment.user;
comment.user = usersMap[userID];
}
}
}
return posts;
});

Model.find().toArray() claiming to not have .toArray() method

I am very new to Node.js and MongoDB and am trying to piece together my own blogging application. I have a problem trying to query through my 'Blog' model for ones with a specific username. When I try to run:
var userBlogs = function(username) {
ub = Blog.find({author: username}).toArray();
ub = ub.reverse();
};
I get an error:
TypeError: Object #<Query> has no method 'toArray'
I know globals are bad but I've just been trying to get it to work. The Mongo documentation claims that a cursor is returned which can have the toArray() method called on it. I have no idea why it won't work.
Here is my schema/model creation:
var blogSchema = mongoose.Schema({
title: {type:String, required: true},
author: String,
content: {type:String, required: true},
timestamp: String
});
var Blog = mongoose.model('Blog', blogSchema);
Here are the /login and /readblog requests
app.get('/readblog', ensureAuthenticated, function(req, res) {
res.render('readblog', {user: req.user, blogs: ub})
})
app.get('/login', function(req, res){
res.render('login', { user: req.user, message: req.session.messages });
});
app.post('/login',
passport.authenticate('local', { failureRedirect: '/login'}),
function(req, res) {
userBlogs(req.user.username);
res.redirect('/');
});
});
The end result is supposed to work with this Jade:
extends layout
block content
if blogs
for blog in blogs
h2= blog[title]
h4= blog[author]
p= blog[content]
h4= blog[timestamp]
a(href="/writeblog") Write a new blog
How can I get the query to output an array, or even work as an object?
The toArray function exists on the Cursor class from the Native MongoDB NodeJS driver (reference). The find method in MongooseJS returns a Query object (reference). There are a few ways you can do searches and return results.
As there are no synchronous calls in the NodeJS driver for MongoDB, you'll need to use an asynchronous pattern in all cases. Examples for MongoDB, which are often in JavaScript using the MongoDB Console imply that the native driver also supports similar functionality, which it does not.
var userBlogs = function(username, callback) {
Blog.find().where("author", username).
exec(function(err, blogs) {
// docs contains an array of MongooseJS Documents
// so you can return that...
// reverse does an in-place modification, so there's no reason
// to assign to something else ...
blogs.reverse();
callback(err, blogs);
});
};
Then, to call it:
userBlogs(req.user.username, function(err, blogs) {
if (err) {
/* panic! there was an error fetching the list of blogs */
return;
}
// do something with the blogs here ...
res.redirect('/');
});
You could also do sorting on a field (like a blog post date for example):
Blog.find().where("author", username).
sort("-postDate").exec(/* your callback function */);
The above code would sort in descending order based on a field called postDate (alternate syntax: sort({ postDate: -1}).
Try something along the lines of:
Blog.find({}).lean().exec(function (err, blogs) {
// ... do something awesome...
}
You should utilize the callback of find:
var userBlogs = function(username, next) {
Blog.find({author: username}, function(err, blogs) {
if (err) {
...
} else {
next(blogs)
}
})
}
Now you can get your blogs calling this function:
userBlogs(username, function(blogs) {
...
})

Resources