Subdocument does not update - mongoose - node.js

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.

Related

Query was already executed

I want to calculate averageRating from my reviews collection. So, firstly I make an aggregation pipeline to find the avgRating and ratingQuantity by matching with item ID.
Then I make an post middleware(document middleware) and when any one create a new review then the averageRating and ratingQuantity fields are get updated, but the problem is that this only works on save not on update or delete. So, i make a query middleware and then for getting the document I execute the query but got error Query was already executed Please Help!!!
My reviewModel.js code
const mongoose = require('mongoose');
const movieModel = require('./movieModel');
const reviewSchema =new mongoose.Schema({
review:{
type:String,
required:[true,"review can't be blank"],
maxlength:100,
minlength:10
},
rating:{
type:Number,
required:[true,"review must have a rating"],
max:10,
min:1
},
movie:{
type:mongoose.Schema.ObjectId,
ref:'movies',
required:[true,'review must belong to a movie']
},
user:{
type:mongoose.Schema.ObjectId,
ref:'users',
required:[true,'review must belong to a user']
}
},
{
toJSON:{virtuals:true},
toObject:{virtuals:true}
});
reviewSchema.pre(/^find/,function(next){
this.populate({
path:'movie',
select:'name'
}).populate({
path:'user',
select:'name'
});
next();
})
reviewSchema.index({movie:1,user:1},{unique:true});
reviewSchema.statics.calcAvgRating = async function(movieId){
console.log(movieId);
const stats = await this.aggregate([
{
$match:{movie:movieId}
},
{
$group:{
_id:'$movie',
nRating:{$sum:1},
avgRating:{$avg:'$rating'}
}
}
])
console.log(stats);
const movie = await movieModel.findByIdAndUpdate(movieId,{
ratingsQuantity:stats[0].nRating,
avgRating:stats[0].avgRating
});
}
reviewSchema.post('save',function(){
this.constructor.calcAvgRating(this.movie);
})
reviewSchema.pre(/^findOneAnd/,async function(next){
const r = await this.findOne();
console.log(r);
next();
})
const reviewModel = mongoose.model('reviews',reviewSchema);
module.exports = reviewModel;
My updateOne controller
exports.updateOne = Model=> catchAsync(async(req,res,next)=>{
console.log("handler");
const doc = await Model.findByIdAndUpdate(req.params.id,req.body,{
new:true,
runValidators:true
});
if(!doc)
return next(new appError('Ooops! doc not found',404));
sendResponse(res,200,'success',doc);
})
Try this
reviewSchema.post(/^findOneAnd/,async function(doc){
const model=doc.constructor;
})
Here doc is actually the current executed document and by doing doc.constructor you got its model. On that model you can use the calcAvgRating

Mongoose not saving record after array is updated

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

How to create a Post Request for Express/Mongoose Normalized Many-to-Many relationships

I am trying to learn how to create a Normalized many to many Post request in express/router and Mongoose.
I have three collections: User, Building and Room which is also the order of parent to child documents.
My Building document schema includes both User and Room id's as follows:
const mongoose = require('mongoose');
const BuildingSchema = new mongoose.Schema({
user: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "user"
}
],
room: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "room"
}
],
My Room document schema includes the Building ID and another child document as follows:
const mongoose = require('mongoose');
const RoomSchema = new mongoose.Schema({
building: {
type: mongoose.Schema.Types.ObjectId,
ref: "building"
},
furniture: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "furniture"
}
],
I am having trouble understanding how to create a post request that allows a user/users to create a Building instance and multiple Room instances associated with it after...
My express code so far can create a new Building instance but I am unsure how to handle including the Room id's:
router.post('/createbuilding', [auth, [
check('name', 'Building Name is required').not().isEmpty(),
]
], async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const {
name,
type,
website,
location,
bio,
} = req.body;
const buildingFields = {
user: req.user.id,
//room: req.room.id,
name,
type,
website: website && website !== '' ? normalize(website, { forceHttps: true }) : '',
location,
bio
};
try {
let building = await Building.findOneAndUpdate(
{ user: req.user.id },
//{ room: req.room.id },
{ $set: buildingFields },
{ new: true, upsert: true }
);
res.json(building);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
}
);
Note: The references to room ID are commented out on purpose because this is what I am unsure of how to include in the Post request.
With Mongoose's .findOneAndUpdate() the first parameter is to query for the document, similar to .findOne(). Your second parameter is what fields to update. $set is redundant since Mongoose will do that for you.
If you want to add a room to the building rather than replacing all the associations, you'll want to add the room with a $push.
const buildingFields = {
$push: {
room: {
req.room.id
}
},
name,
type,
...
}
I am also assuming that you intended that the user field in BuildingSchema is a single association rather than a many associations. If not, you'd need to use a $elemMatch to query for that document:
Mongoose Mongodb querying an array of objects

Saving data to array in mongoose

Users are able to post items which other users can request. So, a user creates one item and many users can request it. So, I thought the best way would be to put an array of users into the product schema for who has requested it. And for now I just want to store that users ID and first name. Here is the schema:
const Schema = mongoose.Schema;
const productSchema = new Schema({
title: {
type: String,
required: true
},
category: {
type: String,
required: true
},
description: {
type: String,
required: true
},
userId: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
requests: [
{
userId: {type: Object},
firstName: {type: String}
}
],
});
module.exports = mongoose.model('Product', productSchema);
In my controller I am first finding the item and then calling save().
exports.postRequest = (req, res, next) => {
const productId = req.body.productId;
const userId = req.body.userId;
const firstName = req.body.firstName;
const data = {userId: userId, firstName: firstName};
Product.findById(productId).then(product => {
product.requests.push(data);
return product
.save()
.then(() => {
res.status(200).json({ message: "success" });
})
.catch(err => {
res.status(500).json({message: 'Something went wrong'});
});
});
};
Firstly, is it okay to do it like this? I found a few posts about this but they don't find and call save, they use findByIdAndUpdate() and $push. Is it 'wrong' to do it how I have done it? This is the second way I tried it and I get the same result in the database:
exports.postRequest = (req, res, next) => {
const productId = req.body.productId;
const userId = req.body.userId;
const firstName = req.body.firstName;
const data = {userId: userId, firstName: firstName};
Product.findByIdAndUpdate(productId, {
$push: {requests: data}
})
.then(() => {
console.log('succes');
})
.catch(err => {
console.log(err);
})
};
And secondly, if you look at the screen shot is the data in the correct format and structure? I don't know why there is _id in there as well instead of just the user ID and first name.
Normally, Developers will save only the reference of other collection(users) in the collection(product). In addition, you had saved username also. Thats fine.
Both of your methods work. But, second method has been added in MongoDB exactly for your specific need. So, no harm in using second method.
There is nothing wrong doing it the way you have done it. using save after querying gives you the chance to validate some things in the data as well for one.
and you can add additional fields as well (if included in the Schema). for an example if your current json return doesn't have a field called last_name then you can add that and save the doc as well so that's a benefit..
When using findById() you don't actually have the power to make a change other than what you program it to do
One thing I noticed.. In your Schema, after you compile it using mongoose.modal()
export the compiled model so that you can use it everywhere it's required using import. like this..
const Product = module.exports = mongoose.model('Product', productSchema);

Automatically remove referencing objects on deletion in MongoDB

Let's suppose I have a schema like this:
var Person = new Schema({
name: String
});
var Assignment = new Schema({
name: String,
person: ObjectID
});
If I delete a person, there can still be orphaned assignments left that reference a person that does not exist, which creates extraneous clutter in the database.
Is there a simple way to ensure that when a person is deleted, all corresponding references to that person will also be deleted?
You can add your own 'remove' Mongoose middleware on the Person schema to remove that person from all other documents that reference it. In your middleware function, this is the Person document that's being removed.
Person.pre('remove', function(next) {
// Remove all the assignment docs that reference the removed person.
this.model('Assignment').remove({ person: this._id }, next);
});
If by "simple" you mean "built-in", then no. MongoDB is not a relational database after all. You need to implement your own cleaning mechanism.
The remove() method is deprecated.
So using 'remove' in your Mongoose middleware is probably not best practice anymore.
Mongoose has created updates to provide hooks for deleteMany() and deleteOne().
You can those instead.
Person.pre('deleteMany', function(next) {
var person = this;
person.model('Assignment').deleteOne({ person: person._id }, next);
});
In case if anyone looking for the pre hook but for deleteOne and deleteMany functions this is a solution that works for me:
const mongoose = require('mongoose');
...
const PersonSchema = new mongoose.Schema({
name: {type: String},
assignments: [{type: mongoose.Schema.Types.ObjectId, ref: 'Assignment'}]
});
mongoose.model('Person', PersonSchema);
....
const AssignmentSchema = new mongoose.Schema({
name: {type: String},
person: {type: mongoose.Schema.Types.ObjectId, ref: 'Person'}
});
mongoose.model('Assignment', AssignmentSchema)
...
PersonSchema.pre('deleteOne', function (next) {
const personId = this.getQuery()["_id"];
mongoose.model("Assignment").deleteMany({'person': personId}, function (err, result) {
if (err) {
console.log(`[error] ${err}`);
next(err);
} else {
console.log('success');
next();
}
});
});
Invoking deleteOne function somewhere in service:
try {
const deleted = await Person.deleteOne({_id: id});
} catch(e) {
console.error(`[error] ${e}`);
throw Error('Error occurred while deleting Person');
}
You can leave the document as is, even when the referenced person document is deleted. Mongodb clears references which point to non-existing documents, this doesn't happen immediately after deleting the referenced document. Instead, when you perform action on the document, e.g., update. Moreover, even if you query the database before the references are cleared, the return is empty, instead of null value.
Second option is to use $unset operator as shown below.
{ $unset: { person: "<person id>"} }
Note the use of person id to represent the value of the reference in the query.
you can use soft delete. Do not delete person from Person Collection instead use isDelete boolean flag to true.
Use $pull. Suppose you have a structure like this.
Stuff Collection:
_id: ObjectId('63dd23c633c17a718c4c5db7')
item: "Item 1"
user: ObjectID('63de669153bc12ecb9081b9e')
User collection:
_id: ObjectId('63de669153bc12ecb9081b9e')
stuff: array[ObjectId('63dd23c633c17a718c4c5db7'), ObjectId('63de3a69715ec134e161b0ea')]
Then after you remove the stuff:
const stuff = Stuff.findById(req.params.id)
const user = User.findById(req.params.id)
await stuff.remove()
// here you can use $pull to update
await user.updateOne({
$pull: {
stuff: stuff.id
}
})
you can simply call the model that needs to be deleted and delete that document like this:
PS: This answer is not specific to the question schema.
const Profiles = require('./profile');
userModal.pre('deleteOne', function (next) {
const userId = this.getQuery()['_id'];
try {
Profiles.deleteOne({ user: userId }, next);
} catch (err) {
next(err);
}
});
// in user delete route
exports.deleteParticularUser = async (req, res, next) => {
try {
await User.deleteOne({
_id: req.params.id,
});
return res.status(200).json('user deleted');
} catch (error) {
console.log(`error`, error);
return next(error);
}
};

Resources