How can I update mongoose subdocuments via form post request - node.js

I have a Schema system with nested schemas to create a one to many relationship.
User Schema
const expenseSchema = new Schema({
expense: String,
value: Number
});
const budgetSchema = new Schema({
earnings: Number,
expenses: [expenseSchema]
});
const userSchema = new Schema({
username: String,
name: String,
googleId: String,
budget: [budgetSchema]
});
I am trying to push a value to these nested schemas but been having some trouble. Note The req.user value is of authenticated users through Passport authentication
This is my attempt to add data into the budget schema's earning property. The weird thing is that when I log the user after the post request it shows undefined.
router.post("/", (req, res) => {
User.findOneAndUpdate(req.user, {
$push: {
budget: {
earnings: req.body.earning
}
}
});
console.log(req.user);
});
Vue form
submitBudget() {
const expenses = this.mergeKeyAndValue();
axios
.post("http://localhost:3000/budget", {
earning: this.earnings,
expenses
})
.then(response => {
console.log(response.data);
})
.catch(e => {
console.error(e);
});
}

router.post("/", (req, res) => {
User.findOneAndUpdate(req.user,
{$push:{
budget: {
earnings: req.body.earning
}
}
}
)
console.log(req.user);
})
In here, you are not providing a callback function to User.findOneAndUpdate() and without such function, all you're doing is querying the database without saving anything.
Taken from Mongoose docs about Model.findOneAndUpdate()
A.findOneAndUpdate(conditions, update, options, callback) // executes
A.findOneAndUpdate(conditions, update, options) // returns Query
A.findOneAndUpdate(conditions, update, callback) // executes
A.findOneAndUpdate(conditions, update) // returns Query -> This is what you are doing right now.
A.findOneAndUpdate() // returns Query
Additionally, your syntax is incorrect that's probably why you're getting undefined. It should be Users.findOneAndUpdate({ username: req.user },..... or wherever your req.user username key the request object holds is stored.
You can rewrite the whole function using async/await in the following way:
router.post("/", async (req, res) => {
const update = await User.findOneAndUpdate(
{ username: req.user.username },
{
$push: {
budget: {
earnings: req.body.earning
}
}
}
)
console.log(update);
});
I don't have an easy way to test the $push update so I'm going to assume you know what you are doing but the rest should be as described.

Related

why is findOneAndUpdate not updating in the DB?

I'm creating a project using the mern stack. I'm trying to update a project from my frontend to my backend. When I update it it will return success but when I check the database nothing is updated? I'm trying to update the product with the prodID that is entered in the frontend
This is my post route
router.post("/updateStock", (req, res) => {
const prodID = req.body.prodID
const product = new Product(req.body)
Product.findOneAndUpdate(prodID, { new: true }, {returnOriginal: false}, function(err, products) {
if (err) {
console.log("err", err);
res.status(500).send(err);
} else {
console.log("success");
res.send(product);
}
});
});
This is my schema
const mongoose = require("mongoose");
const Product = mongoose.model(
"Product",
new mongoose.Schema({
title: String,
manufacturer: String,
price: String,
catergory: String,
quantity: String,
prodID: String,
images: Array
})
);
module.exports = Product;
Following Mongoose Docs here
1st:
You added filter as a string, however it should've been an object like below
const filter = {prodID: req.body.prodID}
2nd:
no need to instantiate the Product schema, use the update object
const update = req.body
3rd:
You used the same option, also new:true took the place of the update object
returnOriginal: false is equivalent to new: true
4th:
Use promise not callbacks, however you have a typo in the callback you called products and you sent product
Product.findOneAndUpdate(filter, update, {new: true}).then((product) => {
console.log("success");
res.send(product);
}).catch(err => {
console.log("err", err);
res.status(500).send(err);
})
You are not passing the new updated body to findOneAndUpdate. The findOneAndUpdate is expecting db.collection.findOneAndUpdate( filter, update, options ). The code should be like this:-
router.post("/updateStock", (req, res) => {
const product = new Product(req.body);
const filter = {prodID: req.body.prodID}
Product.findOneAndUpdate(filter, product, { new: true, returnOriginal: false}, function(err, products) {
if (err) {
console.log("err", err);
res.status(500).send(err);
} else {
console.log("success");
res.send(product);
}
});
Follow this Schema =>
db.collection.findOneAndUpdate(filter, update, options)
Pass Update object

Accessing dynamic collection in mongodb through mongoose

I have been trying to access a collection that was dynamically created in the database through {$out: "Accepted"} in nodejs using mongoose. the collection was successfully created.
from the controller:
exports.accepted = async (req, res, next)=>{
await Pupil.aggregate([{$match: {status: "Accepted"}}, {$out: "Accepted" }])
**Accepted.find({}).then(result=>{
res.render('home/accepted', {results: result, pageTitle: 'accepted page'})
}).catch(error=>{
console.log(error)*emphasized text*
});**
}
I want to retrieve the documents in that 'Accepted' collection and render them to the ejs file.
the error message is:
Accepted.find({}).then(result=>{
^
ReferenceError: Accepted is not defined at exports.accepted...
can someone please help me out?
Thanks!
Welcome to StackOverflow #Jerevick
There are two possible cases here:
Case 1 - You don't need to write to a new collection
You just want to find documents of accepted pupils and render them in the EJS template, in which case:
A) You don't need to write your documents into a new collection, you can just get them from the result and pass them to your template.
exports.accepted = async (req, res, next) => {
try {
const result = await Pupil.aggregate([{ $match: { status: 'Accepted' } }]);
res.render('home/accepted', { results: result, pageTitle: 'accepted page' });
} catch (err) {
console.log(err);
}
};
B) You don't even need the aggregation framework, you can just do a simple find:
exports.accepted = async (req, res, next) => {
try {
const result = await Pupil.find({ status: 'Accepted' });
res.render('home/accepted', { results: result, pageTitle: 'accepted page' });
} catch (err) {
console.log(err);
}
};
Case 2 - you need to write to a new collection, and your example was just to simplify
If that's the case, it's important to emphasize the difference between mongoose and MongoDB. Mongoose is a wrapper around the native MongoDB driver to help with casting, and provide a nicer API.
When you add a new collection to the database using the $out stage, the mongoose isn't aware of it and it doesn't assign a model for it and has no idea what kind of data live there, in which case you would need to bypass mongoose and use the native MongoDB driver directly.
I highly advise against this approach, since you'd be giving up all the convenience mongoose provides, don't take this approach unless you really know what you're doing, there's probably a better solution than using the native driver directly.
exports.accepted = async (req, res, next) => {
try {
await Pupil.aggregate([{ $match: { status: 'Accepted' } }, { $out: 'Accepted' }]);
const acceptedCollection = mongoose.connection.collection('Accepted');
const result = await acceptedCollection.find({}).toArray();
res.render('home/accepted', { results: result, pageTitle: 'accepted page' });
} catch (err) {
console.log(err);
}
};
Here's a full reproduction script you can play with:
65209755.js
'use strict';
const mongoose = require('mongoose');
const { Schema } = mongoose;
const assert = require('assert');
run().catch(console.error);
async function run () {
await mongoose.connect('mongodb://localhost:27017/test', {
useNewUrlParser: true,
useUnifiedTopology: true
});
await mongoose.connection.dropDatabase();
const studentSchema = new Schema({
name: String,
status: String
});
const User = mongoose.model('User', studentSchema);
await User.insertMany([
{ name: 'Hafez1', status: 'Accepted' },
{ name: 'Hafez2', status: 'Accepted' },
{ name: 'Hafez3', status: 'Accepted' },
{ name: 'Hafez4', status: 'Rejected' },
{ name: 'Hafez5', status: 'Rejected' }
]);
await User.aggregate([
{ $match: { status: 'Accepted' } },
{ $out: 'acceptedstudents' }
]);
const db = mongoose.connection;
const acceptedStudents = await db.collection('acceptedstudents').find({}).toArray();
assert.deepStrictEqual(
acceptedStudents.map(student => student.name),
['Hafez1', 'Hafez2', 'Hafez3']
);
console.log('All assertions passed.');
}
Output
$ node 65209755.js
All assertions passed.

Mongoose pre hooks for save and update do not get called when i used a Model.findOneAndUpdate

I have created a an express application with mongoose. I also created a save and update hook as follows :
userSchema.pre("update", async function save(next) {
console.log("inside update")
});
userSchema.pre("update", async function save(next) {
console.log("inside save")
});
but anytime I call a Model.findOneAndUpdate() the pre hooks are not called, does the save and update prehook not work for findOneAndUpdate ?
As stated in mongoose docs Pre and post save() hooks are not executed on update() and findOneAndUpdate().
You need to use findOneAndUpdate hook for this. But you cannot access document which will be updated using this keyword. If you need to access the document that will be updated, you need to execute an explicit query for the document.
userSchema.pre("findOneAndUpdate", async function() {
console.log("I am working");
const docToUpdate = await this.model.findOne(this.getQuery());
console.log(docToUpdate); // The document that `findOneAndUpdate()` will modify
});
Or if you can set a field value using this.set() like this:
userSchema.pre("findOneAndUpdate", async function() {
console.log("I am working");
this.set({ updatedAt: new Date() });
});
Let's say we have this user schema:
const mongoose = require("mongoose");
const userSchema = new mongoose.Schema({
name: String,
updatedAt: {
type: Date,
default: Date.now
}
});
userSchema.pre("findOneAndUpdate", async function() {
console.log("I am working");
this.set({ updatedAt: new Date() });
});
module.exports = mongoose.model("User", userSchema);
And this user document:
{
"updatedAt": "2020-01-30T19:48:46.207Z",
"_id": "5e33332ba7c5ee3b98ec6efb",
"name": "User 1",
"__v": 0
}
When we update this user's name like this:
router.put("/users/:id", async (req, res) => {
let result = await User.findOneAndUpdate(
{ _id: req.params.id },
{ name: req.body.name },
{ new: true }
);
res.send(result);
});
The updatedAt field value will be set to the user, and it will be updated.
you can use save() function with findOne():
const newName = 'whatever';
const user = await User.findOne({id});
user.name = newName;
user.save();
This will call pre hook but you need to watch for the paths you are changing in pre save function

MongoDB/Mongoose search and create of specific sub documents

I'm building a REST API in nodejs/express/mongodb/mongoose. I've started using MongoDB/Mongoose recently and I still have some doubts.
What I'm trying to achieve is to access a specific user bag (a user can have multiple bags) and also I want to be able to add to that bags participants/payers. (a user bag can have multiple participants/payers)
My mongoose user modal contains the rest of the schemas. I created a schema for each one because I believe it would be easier to find a given bag or participant directly because of the ObjectId (not sure if this is correct).
Mongoose Modal/Schemas:
const PayerSchema = new Schema({
name: {
type: String
},
amount: {
type: Number
}
});
const BagSchema = new Schema({
name: {
type: String
},
type: {
type: String
},
payers: [PayerSchema]
});
const UserSchema = new Schema({
name: {
type: String,
required: [true, 'User name field is required']
},
bags: [BagSchema]
});
module.exports = mongoose.model('User', UserSchema);
I was able to create the CRUD controller methods for a new user, but I still not sure on:
Creating a new bag for a specific user (I was able to do this but not sure if it's the right way)
Creating a new participant in a specific bag for a specific user. (addPayer method is wrong need help here)
Check out my controller user/bags/participants methods:
const User = require('../models/userModel');
getAllUserBags: (req, res, next) => {
User.findById({ _id: req.params.id }).then((user) => {
res.send(user.bags);
})
.catch(next);
},
getOneUserBag: (req, res, next) => {
console.log(req.params.bagid);
User.find({ 'bags._id': req.params.bagid}, {"bags.$" : 1}).then((obj) => {
res.send(obj);
})
.catch(next);
},
createBag: (req, res, next) => {
let bag = req.body.bag;
User.findOneAndUpdate(
{_id: req.body.id},
{$push: {bags: bag}
}).then(() => {
//Unnecessary - just to return update user with new bag.
User.findOne({_id: req.body.id}).then((user) => {
res.send(user);
})
}).catch(next);
},
addPayer: (req, res, next) => {
let payer = req.body.payer;
User.find(
{'bags._id': req.params.bagid},
{"bags.$" : 1},
{$push: {payers: payer}
}).then((obj) => {
console.log(obj);
//Unnecessary - just to return update user with new bag.
// User.findOne({_id: req.body.id}).then((user) => {
// res.send(user);
// })
}).catch(next);
}
Thanks for the help
Base on what we discuss, your User schema is good enough for your requirements, as long as making sure that one User document does not exceed the 16MB limit of MongoDB document.
Creating a new bag for a specific user (I was able to do this but not sure if it's the right way)
Yours is fine. However, there are some improvements:
createBag: (req, res, next) => {
User.findByIdAndUpdate(req.body.id, {
$push: { bags: req.body.bag }
}, {
new: true // this will make the query getting the updated document
})
.then(user => {
res.json(user);
})
.catch(next);
})
Creating a new participant in a specific bag for a specific user. (addPayer method is wrong need help here)
Since you decided to nest the 'bags', the bag.id might be duplicated among User documents. See this to understand the possibility. Thus, I recommend using an userId along with bagId:
getOneUserBag: (req, res, next) => {
User.findOne({
_id: req.params.userId,
bags._id: req.params.bagId
})
.then(user => {
if (!user) res.status(404).end();
let bag = user.bags.id(req.params.bagId);
res.json(bag);
})
.catch(next);
}
addPayer: (req, res, next) => {
User.findOneAndUpdate({
_id: req.params.userId,
bags: $elemMatch: {
_id: req.params.bagId
}
}, {
$push: { 'bags.$.payers': req.body.payer } // Use 'positional $' operator along with $elemMatch in the query to update only the matched bag
}, {
new: true // Do not forget the 'new' options to get the updated document
})
.then(user => {
if (!user) res.status(404).end();
res.json(user);
})
.catch(next);
}
and in the router
router.get('/users/:userId/bags/:bagId', getOneUserBag);
router.post('/users/:userId/bags/:bagId/payers', addPayer);
In the getAllUserBags(), you use the wrong syntax for User.findById():
getAllUserBags: (req, res, next) => {
User.findById(req.params.id) // Not { _id: req.params.id }
.then((user) => {
res.json(user.bags);
})
.catch(next);
}

Chaining promise to update a reference document in Mongoose

Given the following schema:
user
{ uid:12345, name:myname, account=null }
account
{ _id:6789, name:"myaccount", _owner:12345 }
How can I update the user.account to have the value of its referenced field account._owner. When the account document is created I want to find and replace the user.account value. The route I have looks like this:
app.post('/accounts', authenticate, (req, res) => {
var account = new Account({
name: req.body.name,
_owner: req.body._owner,
});
account.save().then((doc) => {
//here i wasnt to update a refernce to a
// an account field in a User document and set
//it to the account.owner created above.
res.send(doc);
}, (e) => {
res.status(400).send(e);
});
});
In my example when the account is created
I want to update user.account to 6789 (the value of the created account id)
Mongoose handles promises : http://mongoosejs.com/docs/promises.html
So you can simply :
app.post('/accounts', authenticate, (req, res) => {
var account = new Account({
name: req.body.name,
_owner: req.body._owner,
});
account.save()
.then((doc) => User.findOneAndUpdate(
{ uid: req.body._owner },
{ $set: { account: doc._id } },
{ new: true }
)
.then(() => doc);
}).then((account) => {
res.send(account);
}, (e) => {
res.status(400).send(e);
});
});
Another solution would be to attach a hook to the save action of account model
var Owner = require('path/to/owner/model');
var schema = new Schema({name:String,_owner:{type: Schema.Types.ObjectId,ref: 'owner'}}); // ref will be useful if you want to use populate later
schema.post('save', function(account) {
return Owner.findOne({uid:account._owner})
.then(owner => {
owner.account = account._id; // assign account id to user
return owner.save();
})
});
Then you just have to create a new account object and the hook will do it in the background.
app.post('/accounts', authenticate, (req, res) => {
var account = new Account({
name: req.body.name,
_owner: req.body._owner,
});
account.save().then((doc) => {
res.send(doc);
}, (e) => {
res.status(400).send(e);
});
});
IMO, the routes look cleaner this way, you could try it out.

Resources