MongoDB/Mongoose search and create of specific sub documents - node.js

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

Related

mongo DB always update first document

I have a posts collection that has array of likes.I want to push object into likes array if user have not liked and pull if user has liked the post.I test my API but it always update first document of collection though I provided postId of other document.
schema.js
likes: [
{
userId: String,
isNotified: {
type: Boolean,
default: false,
},
email: String,
types: String,
},
],
API
router.post("/like", (req, res) => {
postModel.find(
{
"_Id": req.body.postId,
"likes.userId": req.body.userId,
},
(err, doc) => {
// console.log(doc)
if (!doc.length) {
postModel.updateOne(
{ "_Id": req.body.postId,},
{
$push: {
likes: {
userId: req.body.userId,
email: req.body.email,
// types: req.body.types,
},
},
},
(err, doc) => {
res.send("like");
}
);
} else {
// console.log("pull")
postModel.find(
{
"_Id": req.body.postId,
"likes.userId": req.body.userId,
},
(err, doc) => {
doc.map((e) => {
e.likes.map((x) => {
if (x.userId == req.body.userId) {
postModel.updateOne(
{
"_Id": req.body.postId,
"likes.userId": req.body.userId,
},
{
$pull: {
likes: {
userId: req.body.userId,
email:req.body.email
},
},
},
(err, doc) => {
res.send("unlike");
}
);
}
});
});
}
);
}
// res.send(doc);
}
);
// });
});
postman request
{
"email":"mahima#gmail.com",
"types":"like",
"postId":"6312c2d1842444a707b6902f",
"userId":"631452d0e1c2acf0be28ce43"
}
How to fix this,suggest an advice.Thanks in advance.
I'm not sure if I undrestand the logic, but here are couple of things that I think you can improve:
You are using find method to get a single document, you should use findOne method which return a single document (if exists) and not an array of documents. But in general when you have the _id value of a document, it's better to just use findById method which is much faster.
When you find a document, you can just modify it and call it's save method to write your changes to the database, there is no need to use updateOne. (please note that partital update has many advantages but in your case they don't seem necessary, you can read about it online.)
your API code can be something like this:
router.post("/like", (req, res) => {
const postId = req.body.postId
const userId = req.body.userId
postModel.findById(postId) // get the post
.then(post => {
if (post) { // check if post exists
// check if user has already liked the post
if (post.likes.find(like => like.userId == userId)){
// user has already liked the post, so we want to
// remove it from likes (unlike the post).
// I know this is not the best way to remove an item
// from an array, but it's easy to understand and it
// also removes all duplications (just in case).
post.likes = post.likes.filter(like => like.userId != userId)
// save the modified post document and return
return post.save(_ => {
// send success message to client
res.send("unlike")
})
} else {
// user has not liked the post, so we want to add a
// like object to post's likes array
post.likes.push({
userId: userId,
email: req.body.email // you can other properties here
})
// save the modified post document and return
return post.save(_ => {
// send success message to client
res.send("like")
})
}
} else { // in case post doesn't exist
res.status(404).send("post not found.")
}
})
.catch(err => {
// you can handle errors here
console.log(err.message)
res.send("an error occurred")
})
})
I didn't run the code, but it should work.

Mongoose delete document field instead of whole document

Instead of deleting the otp attribute the Query is deleting whole document
Schema. I want to delete otp after verified
const LoginSchema = new mongoose.Schema({
email: String,
password:String,
verified: Boolean,
otp:Number
});
const Model = mongoose.model("Users", LoginSchema);
Query in Post Request
app.post("/verify/:id", async(req, res) => {
var User = await Model.findOne({ _id: req.params.id, otp:req.body.otp });
Model.deleteOne({ otp: User.otp }, (err, res) => {
if (err) console.log(err)
else console.log("succesful");
})
})
That because you deleting a document when you using 'deleteOne'. You need to $unset the otp attribute.
Try this one:
Model.findOneAndUpdate({ otp: User.otp }, {$unset: {otp: 1 }},(err, res) => {
if (err) console.log(err)
else console.log("succesful");
})
})

How can I update mongoose subdocuments via form post request

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.

One to many save no response (promises)

I'm stuck saving a object with an array of object Id. I'm using node.js with restify and mongoose.
I have an outfit-request with many outfit-choices:
Model:
const outfitRequestsSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
title: {
type: String
},
maxCost: {
type: Number
},
outfitChoices: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'OutfitChoice'
}]
}, {usePushEach: true});
Router:
application.post('/outfit-choices', (req, resp, next) => {
OutfitRequest.findById(req.body.outfitRequest)
.then(outfitRequest => {
console.log(outfitRequest);
const outfitChoice = new OutfitChoice(req.body);
outfitChoice.save().then(outfitChoice => {
outfitRequest.outfitChoices.push(outfitChoice._id);
outfitRequest.save()
.then(outfitRequestNew => {
console.log(outfitRequestNew);
this.render(resp, next);
})
.catch(next);
}).catch(next);
})
.catch(next);
});
Render method:
render(response: restify.Response, next: restify.Next) {
return (document) => {
if (document) {
this.emit('beforeRender', document);
response.json(document);
} else {
response.send(404);
}
return next();
}
}
Restlet screen
It keeps in a loop, but the record is saved in the db
DB screen
I think that is a problem with chained promises, but I can't find out how to solve this.
I got it like this, I returned the last promise and put the print in a .then
application.post('/outfit-choices', (req, resp, next) => {
OutfitRequest.findById(req.body.outfitRequest)
.then(outfitRequest => {
const outfitChoice = new OutfitChoice(req.body);
outfitChoice.save()
.then(outfitChoice => {
outfitRequest.outfitChoices.push(outfitChoice._id);
return outfitRequest.save()
})
.then(this.render(resp, next))
.catch(next);
})
.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