How to remove an element from an array within Mongo - javascript

I'm completely stuck with Mongoose and remove method.
I have a page with comments and form with a button Delete. My goal is to delete only that comment which was clicked. Below is my MongoDB file (By the way I use method override of the express library to handle both request post and delete).
{
"_id": {
"$oid": "5a455cf460414f548f3d1afb"
},
"title": "Tets",
"body": "tes",
"user": {
"$oid": "5a440bae124b7e4626aeeb70"
},
"date": {
"$date": "2017-12-28T21:07:00.194Z"
},
"comments": [
{
"commentBody": "ets",
"commentUser": {
"$oid": "5a440bae124b7e4626aeeb70"
},
"_id": {
"$oid": "5a455cf660414f548f3d1afc"
},
"commentDate": {
"$date": "2017-12-28T21:07:02.143Z"
}
}
],
"allowComments": true,
"status": "public",
"__v": 1
}
my Schema
const mongoose = require('mongoose')
const Schema = mongoose.Schema;
//Create Schema
const StorySchema = new Schema({
title: {
type: String,
required: true
},
body: {
type: String,
required: true
},
status: {
type: String,
default: 'public'
},
allowComments: {
type: Boolean,
default: true
},
comments: [{
commentBody: {
type: String,
required: true
},
commentDate: {
type: Date,
default: Date.now
},
commentUser: {
type: Schema.Types.ObjectId,
ref: 'users'
}
}],
user: {
type: Schema.Types.ObjectId,
ref: 'users'
},
date: {
type: Date,
default: Date.now
}
});
mongoose.model('stories',StorySchema, 'stories');
And my JS file ,my post method works exactly how I wish but delete doesn't work at all (Cannot read property 'comments' of undefined)
router.post('/comment/:id' , (req , res) => {
Story.findOne({
_id: req.params.id
})
.then(story => {
const newComment = {
commentBody: req.body.commentBody,
commentUser: req.user.id
}
//Push to comments array
story.comments.unshift(newComment);
story.save()
.then(story => {
res.redirect(`/stories/show/${story.id}`)
})
});
})
router.delete('/comment/:id', (req, res) => {
Story.remove({
_id: req.body.id.comments
})
.then(() => {
req.flash('success_msg', 'Comments Removed!');
res.redirect('/dashboard');
})
});
here is my handlebars file with form
{{#each story.comments}}
<form action="/stories/comment/{{id}}?_method=DELETE" method="post" id="delete-form">
<input type="hidden" name="_method" value="DELETE">
<button type="submit" class="btn red"><i class="fa fa-remove"></i> Delete</button>
</form>
{{/each}}
The error I got
TypeError: Cannot read property 'comments' of undefined
at router.delete (/Users/ar2z/Desktop/fierce-caverns-70427/routes/stories.js:197:20)
Help me please. I'm completely lost.

I actually encountered the same error recently. I have found you need to do this:
router.delete("/comments/:id", function(req,res){
var Model = require("myModel");
//Run a find and update query to delete the comment
Model.findOne({_id:req.params.id}, function(err,doc){
if(doc && !err){
doc.comments = doc.comments.filter(function(comment){
//This will filter out the comment you want to delete
return comment._id != req.body.commentId
})
}
res.redirect("/comments/")
})
}

My fault was from the beginning when I was trying work with array element the same way as i'm working with Mongo object
Story.remove({
_id: req.body.id.comments
})
Code above is not working for array element it's working with object but for delete element from array I use:
Story.update( { }, { $pull: { comments: { _id: req.params.id }}}, { multi: true } )
This code Remove Items from an Array of Documents
$pull MongoDb documentation

Related

How to add an object to an array of object, using addToSet, or push operators in mongodb

I have an array of reviews, I want to add a review using addToSet that will check if user is present in the array, then we do not want to add since one user can only review once.
My schema looks like this:
const sellerSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
unique: true,
},
reviews: [
{
by: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
unique: true,
},
title: {
type: String,
},
message: {
type: String,
},
rating: Number,
imagesUri: [{ String }],
timestamp: {
type: Date,
default: Date.now,
},
},
],
});
I might be doing the query wrong, but can't figure out how to add a review and check if current user has not reviewed before.
Here is the query where I add the review:
router.post("/review/:_id/", async (req, res) => {
try {
const stylist_id = mongoose.Types.ObjectId(req.params._id);
const review = {
by: req.user._id,
title: req.body.title,
message: req.body.message,
rating: parseFloat(req.body.rating),
};
if (req.body.imagesUri) {
//if there is images, we need to set it up
review.imagesUri = req.body.imagesUri;
}
await Seller.updateOne(
{ _id: seller_id },
{ $addToSet: { reviews: review } } //get the review that matches to the user_id
);
return res.status(200).send(true);
}catch(err){
res.status(502).send({
error: "Error creating a review.",
});
}
});
I'm thinking of checking for seller's id and also check that no review is by current user, but it is not working.
const userID = req.user._id;
await Seller.updateOne(
{ _id: seller_id, reviews: { $elemMatch: { by: { $ne: { userID } } } } },
{ $addToSet: { reviews: review } } //get the review that matches to the user_id
);
ANSWER:
I was able to solve the issue, in case other people have same issue. I did this:
await Seller.updateOne(
{
_id: seller_id,
"reviews.by": { $nin: [req.user.id] },
//knowing req.user._id is a mongoose.Types.ObjectId.
//You can also use [id1, id2, ...] to the array to check for other id's
},
{ $addToSet: { reviews: review } } //get the review that matches to the user_id
);
Here is the documentation for $nin operator: https://www.mongodb.com/docs/manual/reference/operator/query/nin/
You are pushing the review object inside an object.
Instead do this:
await Seller.updateOne(
{ _id: seller_id },
{ $addToSet: { reviews: review } }
);

Mongoose populate returns an empty array | multiple levels of embedded documents

I am trying to populate my ChatRoom model with the User reference. However, it returns a ChatRoom object with only _ids where I expected usernames, as if I never applied populate on it.
Here is an extract of my ChatRoom model :
const ChatRoom = mongoose.model("ChatRoom", {
sender: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
roomname: { type: String, default: "new room" },
messages: [
{
messages: {
type: mongoose.Schema.Types.ObjectId,
ref: "Message",
},
meta: [
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
delivered: Boolean,
read: Boolean,
},
],
},
],
participants: [
{
user: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
},
],
isPrivate: { type: Boolean, default: "false" },
});
My User model :
const User = mongoose.model("User", {
username: { required: true, unique: true, type: String },
avatar: Object,
token: String,
hash: String,
salt: String,
chatroom: {
type: mongoose.Schema.Types.ObjectId,
ref: "ChatRoom",
},
});
As this seems to be a recurrent issue, I tested several StackOverflow answers for my populate code :
Using populate('participants.user') and 'model: user' or just populate('participants.user'), same solution here:
const chatroom = await ChatRoom.findById(req.params.id)
.populate([
{
path: "participants.user",
model: "User",
},
])
.exec((err, user) => {
if (err) {
console.log("error", err);
} else {
console.log("Populated User " + user);
}
});
The console.log returns :
Populated User { _id: new ObjectId("62262b342e28298eb438d9eb"),
sender: new ObjectId("6225d86c9340237fe2a3f067"), roomname:
'Hosmeade', participants: [ { _id: new
ObjectId("6225d86c9340237fe2a3f067") } ], isPrivate: false,
messages: [], __v: 0 }
As if no populate method was ever applied. On the client side, I get an empty string.
Checking my documents aren't empty, this link mentions that Mongoose get problems with detecting referring model across multiple files but the solution doesn't work for me :
_id:6225d86c9340237fe2a3f067 username:"Berlioz" token:"rTCLAiU7jq3Smi3B"
hash:"wvJdqq25jYSaJjfiHAV4YRn/Yub+s1KHXzGrkDpaPus="
salt:"hZdiqIQQXGM1ryYK" avatar:Object
__v:0
If I remove the .exec(...) part, I get the info on the client side, but participants is still filled with only id :
chatroom response : Object { _id: "62262bb14e66d86fb8a041e8",
sender: "6225d86c9340237fe2a3f067", roomname: "Very secret room",
participants: (1) […], isPrivate: false, messages: [], __v: 0 }
I also tried with select: 'username' and get the same result as above :
const chatroom = await ChatRoom.findById(req.params.id).populate({
path: "participants.user",
select: "username",
});
Populating it "as an array"
Changing type of participants.user in my ChatRoom model into an Object (nothing changes)
If needed hereafter are my repos:
Client side and Backend
I run out of ideas on how to debbug my code. Any help would be great !

findByIdAndUpdate pull from array objects equal to a specific value

I want to remove one or more objects of type tweet from the timeline list within the user model. The tweet objects that I want to remove are those whose author id matches a specific id user._id.
I have tried this:
router.get("/follow/:userId", isLoggedIn, catchAsync(async (req, res) => {
try {
const currentUser = await User.findById(req.user._id).populate("timeline")
const user = await User.findById(req.params.userId).populate("followers tweets")
for (let tweet of currentUser.timeline) {
if (tweet.author._id.equals(user._id)) {
currentUser.timeline.pull(tweet._id)
}
}
req.flash("error", `Unfollowed to ${user.username}`)
user.save();
currentUser.save()
res.redirect(`/${user._id}`)
} catch (err) {
req.flash("error", err.message);
res.redirect("back")
}
}));
and this:
await User.findbyIdAndUpdate(currentuser._id, { $pull: { timeline: { author : user._id } } }
but none of them are working.
My user model:
const userSchema = new Schema({
name: {
type: String,
required: true
},
biography: { type: String, maxlength: 160 },
location: {type: String, maxlength: 30 },
email: {
type: String,
unique: true,
required: true
},
image: {
url: String,
filename: String,
},
followers: [{ type: Schema.Types.ObjectId, ref: "User" }],
following: [{ type: Schema.Types.ObjectId, ref: "User" }],
tweets: [{ type: Schema.Types.ObjectId, ref: "Tweet"}],
timeline: [{ type: Schema.Types.ObjectId, ref: "Tweet"}]
});
My tweet model :
const tweetSchema = new Schema({
images: [{
url: String,
filename : String
}],
text: { type: String, maxlength: 260},
date: { type: Date, default: Date.now },
author: { type: Schema.Types.ObjectId, ref: "User" },
parent: { type: Schema.Types.ObjectId, ref: "Tweet", default:null },
replies: [{ type: Schema.Types.ObjectId, ref: "Tweet" }],
likes: [{ type: Schema.Types.ObjectId, ref: "User" }],
retweets: [{ type: Schema.Types.ObjectId, ref: "Tweet" }],
retweetStatus: {type: Schema.Types.ObjectId, ref: "Tweet", default: null}
});
If your collection looks like this:
[
{
"_id" : ObjectId("60254276259a60228cbe5707"),
"name" : "Mary",
"timeline" : [
ObjectId("60254276259a60228cbe5703"),
ObjectId("60254276259a60228cbe5704"),
ObjectId("60254276259a60228cbe5705")
]
},
{
"_id" : ObjectId("60254276259a60228cbe5706"),
"name" : "Dheemanth",
"timeline" : [
ObjectId("60254276259a60228cbe5700"),
ObjectId("60254276259a60228cbe5701"),
ObjectId("60254276259a60228cbe5702")
]
}
]
then the solution is:
usersSchema.updateOne(
{
"_id": ObjectId("60254276259a60228cbe5706"),
"timeline": ObjectId("60254276259a60228cbe5700"),
},
{
$pull: {
"timeline": ObjectId("60254276259a60228cbe5700")
}
}
)
.then()
.catch()
// or
usersSchema.findOneAndUpdate(
{
"_id": ObjectId("60254276259a60228cbe5706"),
"timeline": ObjectId("60254276259a60228cbe5700"),
},
{
$pull: {
"timeline": ObjectId("60254276259a60228cbe5700")
}
},
{
new: true
}
)
.then()
.catch()
I finally found the issue! The problem I was having is that I was trying to remove items from a list of objects while looping through that list. The solution is easy: you can just create an auxiliar empty array and push the items that you want to remove, then loop through that auxiliar array and pull the items from the original array.
In my case, I've already had an array with the tweets that I wanted to remove, user.tweets. The solution is:
router.get("/follow/:userId", isLoggedIn, catchAsync(async (req, res) => {
try {
const currentUser = await User.findById(req.user._id).populate("timeline")
const user = await User.findById(req.params.userId).populate("followers tweets")
for (let tweet of user.tweets) {
currentUser.timeline.pull(tweet._id)
}
req.flash("error", `Unfollowed to ${user.username}`)
user.save();
currentUser.save()
res.redirect(`/${user._id}`)
} catch (err) {
req.flash("error", err.message);
res.redirect("back")
}
}));

Send nested object on GET

I have a very basic schema which has another object called Vehicle, inside
let rentSchema = new Schema({
code: {
type: Number
},
vehicle: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Vehicle'
},
ongoing: {
type: Boolean,
default: false
}
}, {collection: 'RentCollection'});
Find all in the controller
exports.getRent = function (req, res) {
// Find in the DB
rentSchema.find({}, function (err, rent) {
if (err) res.status(400).send(err);
res.json(rent);
});
};
The response comes as an array of Rents but Vehicle object is missing from the Object Rent. Why is that?
_id: "5e04c19d0a0a100f58bd64b5"
__v: 0
ongoing: false
Here is step by step explanations to make it work:
1-) First you need to create a model and export it like this:
rent.js
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
let rentSchema = new Schema(
{
code: {
type: Number
},
vehicle: {
type: mongoose.Schema.Types.ObjectId,
ref: "Vehicle"
},
ongoing: {
type: Boolean,
default: false
}
},
{ collection: "RentCollection" }
);
module.exports = mongoose.model("Rent", rentSchema);
2-) Let's say you have this Vehicle model:
vehicle.js
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
let vehicleSchema = new Schema(
{
name: String
},
{ collection: "VehicleCollection" }
);
module.exports = mongoose.model("Vehicle", vehicleSchema);
3-) First let's create a vehicle like this in the VehicleCollection like this:
{
"_id": "5e0f465205515667746fd51a",
"name": "Vehicle 1",
"__v": 0
}
4-) Then let's create a rent document in RentCollection using this vehicle id like this:
{
"ongoing": false,
"_id": "5e0f46b805515667746fd51d",
"code": 1,
"vehicle": "5e0f465205515667746fd51a",
"__v": 0
}
5-) Now we can use the following code, to populate the vehicle with the rents.
const Rent = require("../models/rent"); //todo: change to path to the rent.js
exports.getRent = function(req, res) {
Rent.find({})
.populate("vehicle")
.exec(function(err, rent) {
if (err) {
res.status(500).send(err);
} else {
if (!rent) {
res.status(404).send("No rent found");
} else {
res.json(rent);
}
}
});
};
6-) The result will be:
[
{
"ongoing": false,
"_id": "5e0f46b805515667746fd51d",
"code": 1,
"vehicle": {
"_id": "5e0f465205515667746fd51a",
"name": "Vehicle 1",
"__v": 0
},
"__v": 0
}
]
You will have to use the populate method to populate a vehicle object.
From docs:
rentSchema.
findOne({}).
populate('vehicle').
exec(function (err, obj) {
if (err) return handleError(err);
console.log(obj);
});
Also in your current code, you havent setted up model:
RentCollection = mongoose.model('RentCollection', rentSchema);

Can't get populate() to fill out array in Mongoose

Let me begin by saying I know that this seems to be a frequently asked question and I've spent a couple of days trying to figure it out but no answer seems to work so I'm trying on here.
I have two models, User and Chapter: a Chapter can have have many members (Users). When I do router.get('/chapters') I want to see an array of all the Users associated with a Chapter as a property along with the other Chapter properties, like so:
[
{
"leads": [],
"members": [
{"_id":"someString1","firstName":"...", "lastName":"..."},
{"_id":"someString2","firstName":"...", "lastName":"..."},
],
"date": "2018-12-12T15:24:45.877Z",
"_id": "5c11283d7d13687e60c186b3",
"country": "5c11283d7d13687e60c185d6",
"city": "Buckridgestad",
"twitterURL": "qui",
"bannerPic": "http://lorempixel.com/640/480/people",
"__v": 0
}
]
But what I'm getting is this:
[
{
"leads": [],
"members": [],
"date": "2018-12-12T15:24:45.877Z",
"_id": "5c11283d7d13687e60c186b3",
"country": "5c11283d7d13687e60c185d6",
"city": "Buckridgestad",
"twitterURL": "qui",
"bannerPic": "http://lorempixel.com/640/480/people",
"__v": 0
}
]
These are my Schemas:
Chapter
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
// Create Schema
const ChapterSchema = new Schema({
country: {
type: Schema.Types.ObjectId,
ref: "countries"
},
city: {
type: String,
required: true
},
leads: [
{
type: Schema.Types.ObjectId,
ref: "users"
}
],
members: [
{
type: Schema.Types.ObjectId,
ref: "users"
}
],
twitterURL: {
type: String,
required: true
},
bannerPic: {
type: String,
required: true
},
date: {
type: Date,
default: Date.now()
}
});
module.exports = Chapter = mongoose.model("chapters", ChapterSchema);
User
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
// Create Schema
const UserSchema = new Schema({
username: {
type: String,
required: true
},
firstName: {
type: String,
required: true
},
lastName: {
type: String,
required: true
},
organisation: {
type: String,
required: true
},
chapter: {
type: Schema.Types.ObjectId,
ref: "chapters"
},
email: {
type: String,
required: true
},
admin: {
type: Boolean,
default: false
},
lead: {
type: Boolean,
default: false
},
password: {
type: String,
required: true
},
date: {
type: Date,
default: Date.now()
}
});
module.exports = User = mongoose.model("users", UserSchema);
Like I said, when I call the endpoint, I want it to return me all the Chapters with all the Users as a populated property.
I've tried a lot of variations of .populate() but to know luck. The closest I got was going through the early levels of callback hell which I know isn't necessary with today's tech, but nothing is working!
My routes/api/chapters.js
// #route GET api/chapters
// #desc Get all Chapters
// #access Public
router.get("/", (req, res) => {
Chapter.find()
.populate("members")
.then(chapters => {
return res.json(chapters);
})
.catch(err =>
res.status(404).json({ nochaptersfound: "No Chapters found" })
);
});
I can get it to work the other way around:
My routes/api/users.js
// #route GET api/users
// #desc Return all users
// #access Public
router.get("/", (req, res) => {
User.find()
.populate("chapter")
.exec()
.then(users => res.status(200).json(users))
.catch(err => console.log(err));
Returns a user with the populated Chapter, but I can't populate the chapter.members array
Any help would be greatly appreciated!
Thanks!!
From your comment, I believe you are not actually storing users in your chapters. What you are doing is this:
User.create({..., chapter: id})...
And assuming chapter now has a user. Its not the way it works with mongoose, so if you want to actually save in both place, you will need to do it yourself. You are thinking about this as if it were a relational database
You will need to do something like:
const user = await User.create({..., chapter: id})
const chapter = await Chapter.findOne({ _id: id })
chapter.members.push(user)
chapter.save()
If your populate wasn't working, you'd not get an empty array, you'd get an array with ids. Your current populate query is fine, you just don't have any data to populate
With promises, it would look like this:
var userPromise = User.create({..., chapter: id}).exec()
var chapterPromise = Chapter.findOne({ _id: id }).exec()
Promise.all([userPromise, chapterPromise]).then((user, chapter) => {
chapter.members.push(user)
return chapter.save()
}).then(chapter => {
// send response
})
If you need 10 chapters with 10 to 50 users, I'd create 50 users, then push all of them into the chapters and save the chapter.

Categories

Resources