mongoose update nested value - javascript

I have some nested properties on my mogoose schema like this:
const userSchemaValues = {
username: {
type: String,
required: [true, 'Username required'],
trim: true,
unique: true,
lowercase: true
},
password: {
type: String,
required: [true, 'Password required'],
trim: true,
minlength: 8
},
...
prop: {
prop_1: String,
prop_2: String
}
};
valuesToUpdate.prop = _.pick(req.body, 'prop_1', 'prop_2');
log.debug(JSON.stringify(valuesToUpdate));
User.update({_id: req.params.id}, {$set: valuesToUpdate})
.then((data) => {
return res.json({message: data});
})
.catch(err => {
log.error(err);
return next({message: 'Error updating User.'});
});
But when I do User.update({_id: req.params.id}, {$set: valuesToUpdate}) on an User with prop_1 and _2 set with an object like this ({"prop":{"prop_1": "somevalue"}), it is not looking for what is in prop, it just overwrites it. How can I circumvent this?

Your find needs to include the property to update. Also the update statement needs the Positional Update Operator Change it to (outta my head):
valuesToUpdate.prop = _.pick(req.body, 'prop_1', 'prop_2');
log.debug(JSON.stringify(valuesToUpdate));
User.update({$and : [{_id: req.params.id}, {prop.prop1 : "%oldvalue%"}]}, {$set: { "prop.$.prop1" : "%newvalue""}})
.then((data) => {
return res.json({message: data});
})
.catch(err => {
log.error(err);
return next({message: 'Error updating User.'});
});
Note that the positional update only updates the FIRST occurrence!
UPDATE: After rereading your question i saw that the props is not an array... My bad sorry.
Luckily this makes it much easier :)
You do not require the field to be present in the find
for the set : { $set : { "prop.prop1" : "newvalue" }
Also the positional update is not required because it's not an Array.
This makes the following:
valuesToUpdate.prop = _.pick(req.body, 'prop_1', 'prop_2');
log.debug(JSON.stringify(valuesToUpdate));
User.update({_id: req.params.id}, {$set: { "prop.prop1" : "%newvalue%"}})
.then((data) => {
return res.json({message: data});
})
.catch(err => {
log.error(err);
return next({message: 'Error updating User.'});
});
UPDATE2: More information about the update statements.
Because of the comment's i'll clarify the update commands.
When you want to update a field in you document you use the $set command.
This updates one or more fields.
When you want to update multiple field you can do it with the command like:
$set : { "prop.prop1" : "newvalueforprop1", "prop.prop2" : "newvalueforprop2"} }
BUT when you use the command above and specify one one field it generates a command like:
$set : { "prop.prop1" : "newvalueforprop1", "prop.prop2" : null} }
This is all about how you create the update command. When you don't know if it's 1 or 2 properties you need to update your code so it generate a command dynamically.
But another thing you could do is let mongoose handle the update.
Use something like:
User.findById(req.params.id, function (err, user) {
if (err) {
handleError(err)
}
else {
//you should to some checking if the supplied value is present (!= undefined) and if it differs from the currently stored one
user.prop.prop1 = "your value";
user.prop.prop1 = "2nd value"
user.save(function (err) {
if (err) {
handleError(err)
}
else {
res.json(user);
}
});
}
});
Hope it's clear now.

Related

Mongoose: attempting to remove last element from an array results in seemingly bizarre behavior

Update: I switched from updateOne and $pull to simply filtering the array and saving it. I don't know why but that solves the first issue of html elements being removed. The same error occurs when deleting the last Item in a Menu however.
I have the following Express router to remove an Item element from an array in a Menu:
router.put('/menus/:menuid/items/:itemid', async (req, res, next) => {
console.log('attempting to delete menu item...')
const menu = await Menu.findById(req.params.menuid)
const item = menu.items.find(item => item.id === req.params.itemid)
console.log(item)
console.log('updating...')
const response = await Menu.updateOne(
{ _id: menu._id },
{ $pull: { items: { _id: item._id } } }
)
console.log(response)
req.menu = await Menu.findById(req.params.menuid)
})
This successfully removes the desired array element, however the function that called fetch() on this request doesn't proceed with then(); the elements on the page don't change until I refresh the browser:
function redirectThenDeleteElement(path, id, method) {
console.log("redirect to " + path + " using " + method)
fetch(path, { method: method })
.then(response => {
if (response.ok) {
console.log('fetch successful')
const itemToDelete = document.getElementById(id)
itemToDelete.remove()
} else {
console.log('error with request')
}
}).catch(error => {
console.error('error with fetch call: \n' + error)
})
}
From here it gets weirder. If i delete the last item from a Menu, it behaves as expected. Same if I add a new item then delete it again.
But if I delete all the items from one Menu then delete the last item from another, "updating..." is logged and I get the error:
MongoServerError: E11000 duplicate key error collection: pfa-site.menus index: items.title_1 dup key: { items.title: null }
This also happens if I seed one Menu with an empty items array and then delete the last item from another Menu. It refers to items.title_dup/items.title, and I don't have a coherent idea why it would, and I don't know what an index means in this context.
My first thought was that if items.title was meant to be the title property of Item, which is unique, the error makes sense if multiple menus are for some reason trying to update their items.title property to null. But the result is the same if I remove the unique parameter from the schema:
const menuItemSchema = new mongoose.Schema({
title: {
type: String,
required: true,
unique: false
},
content: String,
sanitizedHtml: {
type: String,
required: true
},
slug: {
type: String,
required: true,
unique: true
}
})
const menuSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
order: {
type: Number,
required: true
},
items: {
type: [menuItemSchema]
},
slug: {
type: String,
required: true,
unique: true
}
})
menuSchema.pre('validate', function(next) {
if (this.title) {
this.slug = slugify(this.title, { lower: true, strict: true })
}
this.items.forEach((item) => {
console.log('content being sanitized...')
if (item.content) {
item.sanitizedHtml = dompurify.sanitize(marked.parse(item.content))
}
if (item.title) {
item.slug = slugify(item.title, { lower: true, strict: true })
}
})
next()
})
module.exports = mongoose.model('Menu', menuSchema)
Maybe the weirdest part is that if I add a callback function to updateOne that simply logs any error/result, and then attempt to delete any item under any condition, i get the error:
MongooseError: Query was already executed: Menu.updateOne({ _id: new ObjectId("63b3737f6d748ace63beef8a... at model.Query._wrappedThunk [as _updateOne]
Here is the code with the callback added:
router.patch('/menus/:menuid/items/:itemid', async (req, res, next) => {
console.log('attempting to delete menu item...')
const menu = await Menu.findById(req.params.menuid)
const item = menu.items.find(item => item.id === req.params.itemid)
console.log(item)
console.log('updating...')
const response = await Menu.updateOne(
{ _id: menu._id },
{ $pull: { items: { _id: item._id } } },
function(error, result) {
if (error) {
console.log(error)
} else {
console.log(result)
}
}
)
console.log(response)
req.menu = await Menu.findById(req.params.menuid)
})
Thank you for taking the time to read all of this. You're a real one and I hope you have any idea what's happening!
I've tried using findOneAndUpdate instead of updateOne and using different http request methods, among other things that failed and I forgot about. Results are similar or broken for I think unrelated reasons.
I think this is related to how you handle your requests.
As a better and cleaner code, I would suggest making the "put" method to be "delete" so the api will explicitly be meant for deleting an item from a menu.
Also, I don't know where the result is being sent back to the frontend.
I would suggest something like that:
router.delete('/menus/:menuid/items/:itemid', async (req, res, next) => {
console.log('attempting to delete menu item...')
const {menuid, itemid} = req.params;
const menu = await Menu.findById(menuid)
const item = menu.items.find(item => item.id === itemid)
if(!item){
throw new Error('item not found')
}
console.log(item)
console.log('updating...')
const response = await Menu.updateOne(
{ _id: menu._id },
{ $pull: { items: { _id: itemid } } },
{ new: true}
)
console.log(response)
return res.status(200).json(response);
})
Solution: it turns out that since the Item schema had unique fields, MongoDB automatically added indices to the parent schema where the index name is e.g. title_1 and the key:value, in the case of an empty items array, is 'items.title': null. So when the last item is deleted from a second menu some conflict happens where there are duplicate indices? I'm not sure, so any clarification on that would be helpful.
Anyway, the solution was to add sparse: true in addition to unique: true. This directly prevents an index from having a null value. Again, further clarification on exactly how this works would be great. See BahaEddine Ayadi's comment for further details.

How to check if there are no more documents to update using findOneAndUpdate

So I am learning CRUD for a school project and I followed a tutorial that was really useful. However, when I completed it I noticed that when there are no more quotes to update, it still updates quotes. How can I change this so that it will stop updating quotes that arent even there?
app.put('/quotes', (req, res) => {
quoteCollection.findOneAndUpdate(
{ name: 'Yoda' },
{
$set: {
name: req.body.name,
quote: req.body.quote
}
},
{upsert: true}
)
.then(result => {
//The if block that i am trying
if (result.deletedCount === 0) {
return res.json('No quote to delete')
}
})
.catch(error => console.error(error))
})
Why are you passing {name: "Yoda}? This route is supposed to only update the quote with "Yoda" as its name? If not, then you need to grab from the request object the quote that should be updated.
I tried to create a different version, based on the assumption that the quote that should be updated will come from the req.body:
app.put("/quotes", async (req, res) => {
//Grab the name/id/identifier for the quote you want to update from the body
const query = req.body.name;
// Try to update the document on the database
try {
const result = await quoteCollection.findOneAndUpdate(
query,
{
name: req.body.name,
quote: req.body.quote,
},
{
upsert: true,
new: true,
}
);
// If it worked, it will return the updated quote
res.status(200).json({
status: 200,
data: {
result,
},
});
} catch (err) {
res.status(400).json({
status: 400,
message: "Something went wrong",
});
}
});

not able to use $pull (Mongoose) to remove an object in an array for a user model

I have tried various methods and ways to do it, I am trying to use the $pull method
This is my user object
I previously only had the object id in the user object , but now that I am removing a full object, I search for the ID of the posting, then try to pull that out
router.delete('/testing', (req, res, next) => {
postModel.findByIdAndRemove(req.query.postid, (error, returnedDocuments) => {
if (error) return next(error);
userModel.findOneAndUpdate(
// { email: req.query.email, posts: req.query.postid },
{ email: req.query.email },
{ $pull: { posts: { number: mongoose.Types.ObjectId(req.query.postid) } } },
{ new: true },
function(error, user) {
if (error) {
res.json(error);
}
console.log(`Post id ===== ${req.query.postid}`);
console.log(`Email===== ${req.query.email}`);
console.log(`returning user====${user}`);
res.json('Successfully updated user');
}
);
});
});
the post is being deleted from the postmodel, but I am not able to remove the object from the users postings made array.
any alternative solutions or insights as to why this is happening would be appreciated.
The problem is that MongoDB compares types before values and there's no implicit type conversion applied so instead of having:
{ $pull: { posts: { number: req.query.postid } } }
you need to run:
{ $pull: { posts: { number: mongoose.Types.ObjectId(req.query.postid) } } }

Trying to populate user Document with post information, but I only have the user ID that I pushed manually

so I am trying to get the title of the post, as when I display an array of posts made on user dashboard, I can only show the ID of the post when I redirect them, this works... but I think it would make more sense to have the TITLE in the place of that. Population does not seem to push the object into the array, maybe I am understanding population incorrectly, or there is a better way to go about it... thanks .
this is the react code for that list
<ol>
{ user ? user.posts.map((item,i) => (
<React.Fragment key ={i}>
<li><Link to={`/api/posts/item/${item}`}>{item}</Link></li>
<button onClick={() => { setId(item) }}>Delete this post</button>
</React.Fragment>
)) : null }
</ol>
this is the backend code which gave me the user ID , and the populate f(n)
userModel.findOne({ email: req.body.author }, function(error, user) {
const locationURL = req.files.map((item) => item.location);
postModel.create({ ...req.body, image: locationURL }, (error, returnedDocuments) => {
if (error) {
throw new Error(error);
}
user.posts.push(returnedDocuments._id);
user.save((err) => {
console.log(err);
});
});
// this will populate the posts field in our userSchema (which contain the id references to our posts)
userModel.findOne({ email: req.body.author }).populate('posts').exec((err, user) => {
user.save((err) => {
console.log(err);
});
});
});
in the user document it looks like this, an array of ID's
[
{
"premium": false,
"max_posts": 62,
"posts_made": 57,
"posts": [
"5e21252ac51ac82838947875",
"5e212a6c3b1619294832a3f2"
],
"_id": "5e0fe3f33c2edb2f5824ddf2",
"email": "myemail#gmail.com",
"createdAt": "2020-01-04T01:01:39.840Z",
"updatedAt": "2020-01-17T03:36:36.086Z",
"__v": 22
}
]
in my userSchema I refrence Posts... but maybe I am doing it wrong and somehow can access that information...
let User = new Schema(
{
email: {
type: String,
unique: true
},
premium: {
type: Boolean,
default:false
},
max_posts: {
type: Number,
default:3
},
posts_made: {
type: Number,
default:0
},
posts: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Post'
}
]
},
{
timestamps: true
}
);
Should I just call a axios request to get the names of those ID's that I have, I had that thought, but thought it might be unnessary calls to the DB which might become expensive. thanks.
Actually you are populating correctly, but not in the correct place. You should move the populate code inside the callback, and send the result like this.
postModel.create({ ...req.body, image: locationURL }, (error, returnedDocuments) => {
if (error) {
throw new Error(error);
}
user.posts.push(returnedDocuments._id);
user.save((err) => {
if (err) {
console.log(err);
throw new Error(err);
}
userModel.findOne({ email: req.body.author }).populate('posts').exec((err, user) => {
if (err) {
console.log(err);
throw new Error(err);
}
res.send(user); //user will have posts populated
});
});
});

Mongo $addToSet with multiple values correct syntax

I have this mongoose schema:
var listingSchema = new Schema({
street : String,
buildingNumber : Number,
apartmentNumber : Number,
UsersAndQuestions: [{
userID: String,
questionID: [String]
}]
});
And I just want to update it with a new entry to UsersAndQuestions which will consist of a userID which is a String, and a questionID which is also a String (but needs to be inserted into an array).
I am using this PUT request:
app.put('/api/listing/:street/:buildingNumber/:apartmentNumber/addUserInput/:userid/:listingid/:questionid')
So I have all the necessary parameters in hand.
Usually, when I wanted to update a field in a schema I used this code that I wrote:
app.put('/api/listing/:street/:buildingNumber/:apartmentNumber/addReportedUser/:userid/:listingid', function (req, res) {
var listingToUpdate = req.params.listingid;
var idToAdd = req.params.userid;
Listing.update({_id: ObjectId(listingToUpdate)},
{$addToSet: {reportedUsersIDs: ObjectId(idToAdd)}}
, function (err) {
if (err) {
res.send("There was a problem adding the reportedUserID to the listing" + err);
}
else {
console.log("Success adding reportedUserID to listing!");
}
})
});
You can see I used $addToSet and it worked well. But now I want to add two parameters to a field which is an array. I thought about doing something like this:
app.put('/api/listing/:street/:buildingNumber/:apartmentNumber/addUserInput/:userid/:listingid/:questionid', function(req,res){
var listingToUpdate = req.params.listingid;
var idToAdd = req.params.userid;
var questionToAdd = req.params.questionid;
Listing.update({_id: ObjectId(listingToUpdate)},
{$addToSet: {UsersAndQuestions.userID : ObjectId(idToAdd), UsersAndQuestions.questionID : ObjectId(questionToAdd)}}
, function (err) {
if (err) {
res.send("There was a problem adding the user and question to the listing" + err);
}
else{
console.log("Success adding user and question to the listing!");
}
})
});
But I'm obviously getting a SyntaxError.
What is the correct syntax for doing what I tried to do?
Thanks a lot! :)
You need to add object to set UsersAndQuestions:
{$addToSet: {UsersAndQuestions: { userID: idToAdd, questionID: questionToAdd } }}
UPDATE.
I would do it with two queries:
Listing.update({_id: ObjectId(listingToUpdate), 'UsersAndQuestions.userID': idToAdd},
{"$addToSet": {"UsersAndQuestions.$.questionID": questionToAdd}}
, function (err, result) {
if(result.n === 0){
//we haven't found document with the userId - idToAdd
//we need to insert to UsersAndQuestions document with this user
Listing.update({_id: ObjectId(listingToUpdate)},
{$addToSet: {UsersAndQuestions: { userID: idToAdd, questionID: questionToAdd } }},
function(err, res){
})
}
})

Categories