Mongoose not saving record after array is updated - node.js

I've seen a couple similar posts, but I can't get anything to work. The following is for a podcast episode topic suggestion app. It's meant to upvote a topic by adding a user ID to an array of user IDs saved to the topic object. Everything seems like it works, but topic.save() isn't actually saving.
router.post('/upvote/:id', auth, async (req, res) => {
try{
var topic = await Topic.findById(req.params.id);
const reqId = req.body._id;
if(topic.upvotes.includes(reqId)){
res.status(409).send('Topic already upvoted.');
}
console.log(`pre-update: ${topic}`);
topic.set({
upvotes: topic.upvotes.push(reqId)
});
console.log(`post-update: ${topic}`);
try{
//topic.markModified('topic.upvotes');
topic = await topic.save();
res.status(201).send(topic);
} catch{
next();
};
} catch{
res.status(404).send('Topic with given ID not found.');
};
});
I tried a few different variations on topic.markModified() because I saw that suggested on other posts, but nothing worked.
Here's what those two console.log()s show:
pre-update: {
upvotes: [],
_id: 612d701dd6bbfd3c5c36c906,
name: 'a topic',
description: 'is described',
category: 61217a75f30c6c826af9076b,
__v: 0
}
post-update: {
upvotes: [ 612996b46f21d2086c9d4d52 ],
_id: 612d701dd6bbfd3c5c36c906,
name: 'a topic',
description: 'is described',
category: 61217a75f30c6c826af9076b,
__v: 0
}
These look like it should work perfectly.
The 404 response at the very end is what's actually getting sent when I try this. I'm using express-async-errors & if the next() in the nested catch block was getting called, it would send 500.
Any suggestions?

I am actually not sure what exactly your trying to do. If you want to add a new value to a field only at a particular place then put or patch is to be used not post. post will update the whole document. and patch put is for partial updation.
Can you refer the sample code which I have given, hope that would be helpful for you in one or the other way.
router.put("/:id", [auth, validateObjectId], async (req, res) => {
const { error } = validateMovie(req.body);
if (error) {
return res.status(400).send(error.details[0].message);
}
let genre = await Genre.findById(req.body.genreId);
console.log(genre);
if (!genre) {
return res.status(400).send("No genre found with given id");
}
let movieDetails = await Movie.findByIdAndUpdate(
req.params.id,
{
title: req.body.title,
numberInStock: req.body.numberInStock,
dailyRentalRate: req.body.dailyRentalRate,
liked: req.body.liked,
genre: {
_id: genre.id,
name: genre.name,
},
}, //when using patch method, then you need not have to write this whole thing. instead just write req.body
{ new: true }
);
if (!movieDetails) {
return res.status(404).send("No such movie details found.");
}
res.send(movieDetails);
});

I figured it out. I think mongoose doesn't like it if you try to push() a new element like a normal array.
I used addToSet() instead and it worked.
https://mongoosejs.com/docs/api/array.html#mongoosearray_MongooseArray-addToSet

Related

MongoDB nodejs updateOne always returning modifiedCount: 0

I’m having an issue where I’m attempting to update my document and the change is not being reflected. I suspect MongoDB is finding that my value is somehow the same even though I’ve changed it
const User = require('./assets/models/User.js');
var message = user.messages;
//should be an empty array for right now, can be something like
//['erin': [{ from: ‘erin’, to: ‘erin’, content: ‘test’ }]]
//in the future
if (!message[otherPerson]) message[otherPerson] = [];
await message[otherPerson].push(msg);
//where msg is a msg object
//pushes message into new index
//updates messages with new data
const test = await User.updateOne({ usertag: person }, {
$set: { messages: message }
});
console.log(await test);
I’ve tried multiple formats of updating such as
User.updateOne({ usertag: person }, {
messages
});
where the messages variable is called message in the earlier example
or
User.updateOne({ usertag: person }, {
$set: { messages }
});
and nothing seems to work
I will also mention that this is some rather old code that used to work pretty well. Has something changed in how MongoDB handles updates or am I doing something wrong?
If you want to add a new value to the messages array you should use $push:
const test = await User.updateOne({ usertag: person }, {
$push: { messages: msg }
});
If you want to edit a specific message you should filter by its id and reference the specific array element (I'm assuming that _id is your identifier for message elements):
const test = await User.updateOne({ usertag: person, messages._id: msg._id }, {
$set: {
messages.$.from: msg.from,
messages.$.to: msg.to,
messages.$.content: msg.content,
}
});
Also, you should not await the test result since you already resolved the Promise awaiting the updateOne:
console.log(test);

Subdocument does not update - mongoose

I'm trying to update a subdocument of the parent document.
I have a document called "Post" and I reference the "User" document like this:
const PostSchema = new mongoose.Schema({
title: String,
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
},
{ collection: 'posts' })
const Post = mongoose.model('Post', PostSchema);
module.exports = Post;
I'm trying to change the name, for example of whoever posted it. The name field is in "User".
I'm trying to change it this way:
exports.update = async (req, res) => {
//find user by its id, update its post with what's in req.body
Post.findById(req.body.id, function(err, result) {
console.log(result)
if (!err) {
if (!result){
res.status(404).send('User was not found');
}
else{
result.user.nome = "User Name";
result.markModified("user");
result.save(function(saveerr, saveresult) {
if (!saveerr) {
res.status(200).send(saveresult);
} else {
res.status(400).send(saveerr.message);
}
});
}
} else {
res.status(400).send(err.message);
}
}).populate("user");
}
This is my route.js
app.put(
"/api/produtor/update",
controller.update
);
When I run it on the postman, I get status 200 and the name appears modified in the return, but it is not saved in the bank.
I would appreciate it if someone could help me analyze it!
This might be outdated but as per this GitHub comment, this might not be possible out of the box.
I would suggest to update the User by using User schema instead of going through the Post Schema.

Can't push object into the array after running mongodb query

I was trying to set up the logic for adding some items into an array, which id's express server receives from a client. My program receives the id of the product and then I was fetching the product details from MongoDB query findOne, and then with some customized details I used to push that item into an array but it's not working, whenever I try to push any element after MongoDB query it's not working, I don't know why, but please help me, Sorry for my bad English !
It's an ExpressJS server using MongoDB
Items received: (But it is actually received from the client in the form of JSON) :
const items= [
{
productId:"61e01e7e24612b56c33b06c3",
quantity:"4"
},
{
productId:"61e01e9024612b56c33b06c6",
quantity:"10"
}
]
The actual code here is the problem
let itemsData = [];
items.forEach(async (item) => {
const itemData = await findProduct({ _id: item.productId });
// Check if product found
if (!itemData) return res.status(400).json({ message: "Product is Invalid" });
// If found add that in object
itemsData.push({
productId: itemData._id,
name: itemData.name,
price: itemData.price,
quantity: item.quantity,
unit: "Nos",
totalPrice: parseInt(itemData.price) * parseInt(item.quantity)
});
});
The code above doesn't push that object into the itemsData
array
findProduct Function
// Service to find Product
async findProduct(filter) {
return await ProductModel.findOne(filter);
}
If I used that push method and tried only to itemsData.push("hello"); before the MongoDB query it works, but if I put it after the findProduct Query it doesn't work! I don't know what is wrong with it! Somebody help me!
I just want to push those items with detail into itemData object happily which is not happening I tried to console.log(itemsData) it just return [], what should I do?
Try using For Of instead of forEach (don't forget to add async)
let itemsData = [];
for (const item of items) {
const itemData = await findProduct({ _id: item.productId });
// Check if product found
if (!itemData) return res.status(400).json({ message: "Product is Invalid" });
// If found add that in object
itemsData.push({
productId: itemData._id,
name: itemData.name,
price: itemData.price,
quantity: item.quantity,
unit: "Nos",
totalPrice: parseInt(itemData.price) * parseInt(item.quantity)
});
}
It's because forEach function is not designed to work well with async calls.
You could use map instead.
This should work:
let itemsData = [];
const promises = items.map(async (item) => {
const itemData = await findProduct({ _id: item.productId });
// Check if product found
if (!itemData) return res.status(400).json({ message: "Product is Invalid" });
return {
productId: itemData._id,
name: itemData.name,
price: itemData.price,
quantity: item.quantity,
unit: "Nos",
totalPrice: parseInt(itemData.price) * parseInt(item.quantity)
};
});
itemsData = await Promise.all(promises);
When you use map with async, you will have an array of promises, so you can use Promise.all to wait for the values to get resolved.
Check this out for more details.

MERN - update specific string in an array's object

I am using mongoose to connect my backend (Express) server to database. I want to do normal CRUD operations - but I am able to do it only for direct data in object, but I need to be able to access also array data.
Example of my model:
const LeewaySchema = new mongoose.Schema({
name: {
type: String,
},
shirt: [
{
name: String,
image: String,
},
],
With the following code I am able to update only name of the object, but I need to be able to update also name in shirt array
Here is working approach when changing name of object:
app.put('/update', async (req, res) => {
const updateName = req.body.updateName;
const id = req.body.id;
console.log(updateName, id);
try {
await ClosetModel.findById(id, (error, closetToUpdate) => {
closetToUpdate.name = updateName;
closetToUpdate.save();
});
} catch (err) {
console.log(err);
}
res.send('success');
});
And I tried the same with shirt array, just specifying the correct path
app.put('/update-shirt', async (req, res) => {
const updateShirtName = req.body.updateShirtName;
const id = req.body.id;
try {
await ClosetModel.findById(id, (error, closetToUpdate) => {
closetToUpdate.shirt.name = updateShirtName; // different path here
closetToUpdate.save();
});
} catch (err) {
console.log(err);
}
res.send('success');
});
The server crashes and /update-shirt conflicts with /update path
I am using the same route and frontend for READ
useEffect(() => {
axios
.get('http://localhost:8000/read')
.then((response) => {
setListOfClosets(response.data);
})
.catch(() => {
console.log('error');
});
}, []);
And update name function calling with button onClick:
const updateCloset = (id) => {
const updateName = prompt('Enter new data');
axios
.put('http://localhost:8000/update', {
updateName: updateName,
id: id,
})
.then(() => {
setListOfClosets(
listOfClosets.map((val) => {
return val._id === id
? {
_id: id,
name: updateName,
email: val.email,
}
: val;
})
);
});
};
I don't really know how to do update for shirt's name, I tried to copy paste and just change path and url of course, but it did not work.
The question doesn't actually describe what specific transformation (update) you are attempting to apply to the document. Without knowing what you are attempting to do, there is no way for us to help advise on how to do it.
Say, for example, that the document of interest looks like this:
{
_id: 1,
shirt: [
{ name: "first shirt", image: "path to first shirt" },
{ name: "second shirt", image: "path to second shirt" },
{ name: "third shirt", image: "path to third shirt" }
]
}
Also let's say that the application hits the /update-shirt endpoint with an id of 1 and a updateShirtName of "updated shirt name". Which entry in the array is that string supposed to be applied to? Similarly, how would that information be passed to the server for it to construct the appropriate update.
It is absolutely possible to update documents in an array, here is some documentation about that specifically. But the actual structure of the command depends on the logic that you are attempting to provide from the application itself.
The only other thing that comes to mind here is that the motivation for the schema described in the question seems a little unclear. Why is the shirt field defined as an array here? Perhaps it should instead just be an embedded document. If so then the mechanics of updating the field in the subdocument are more straightforward and none of the aforementioned concerns about updating arrays remain relevant.
just make an update api where you just have to pass the id and and pass the shirt in the findByIdAndUpdate query and hit the postman by passing the below code.
shirt: [
{
name: "jhrh",
image: String,
},
],

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

Resources