I have an app that allows users to rate books. The books are called from Google Books API. I only save a copy of the book in my DB when the user submits their rating.
reviews.put("/:id/new", async (req, res) => {
let url = await `https://www.googleapis.com/books/v1/volumes/${req.params.id}`;
console.log(url);
await request(url, { json: true }, async (error, response, data) => {
let newRating;
Book.findOne({ id: req.params.id }, (err, result) => {
if (err) console.log(err.message);
if (result) {
if (req.body.stars !== undefined) {
newRating = /* some math formula */
} else {
newRating = /* some math formula */
}
} else {
newRating = req.body.stars;
}
});
Book.findOneAndUpdate(
{
id: req.params.id
},
{
id: data.id,
id: data.id,
title: data.volumeInfo.title,
description: data.volumeInfo.description,
img: data.volumeInfo.imageLinks.thumbnail,
author: data.volumeInfo.authors,
rating: newRating,
$inc: {
ratingCount: 1
}
},
{
upsert: true,
returnNewDocument: true
},
(err, book) => {
console.log(book) // If its creating a new document, book returns null. If the book is already in the DB, book returns the document.
Review.create({
rating: req.body.stars,
review: req.body.review,
reviewer: req.session.currentUser._id,
book: book._id // <-- ERROR, cannot read property "_.id" of null
});
}
);
});
res.redirect("/");
});
The issue is that book returns null when it's newly created. But this works fine if someone else has already rated it. I've tried using .save() but that did not work. How else can I get the _.id of the newly created book?
Any help is appreciated. Thanks!
You are passing through the incorrect query options. You need to be using:
new: bool - if true, return the modified document rather than the original.
upsert: bool - creates the object if it doesn't exist. defaults to false.
Update the method as follows:
Book.findOneAndUpdate(
{
id: req.params.id
},
{
id: data.id,
id: data.id,
title: data.volumeInfo.title,
description: data.volumeInfo.description,
img: data.volumeInfo.imageLinks.thumbnail,
author: data.volumeInfo.authors,
rating: newRating,
$inc: {
ratingCount: 1
}
},
{
upsert: true,
new: true
},
(err, book) => {
console.log(book) // If its creating a new document, book returns null. If the book is already in the DB, book returns the document.
Review.create({
rating: req.body.stars,
review: req.body.review,
reviewer: req.session.currentUser._id,
book: book.id // <-- ERROR, cannot read property "_.id" of null
});
}
See the docs
Use 'new': true like this: { upsert: true, 'new': true }.
This should return upserted document.
Related
I am trying to increment a simple number field, but it is telling me it is failing to to a casting error.
CastError: Cast to Number failed for value "{ '$inc': 1 }" (type Object) at path "times_dealt"
Says it's an object?
This is my schema for Answer
const answerSchema = new mongoose.Schema({
body: {
type: String,
trim: true,
required: true,
},
times_dealt: {
type: Number,
required: true,
},
times_picked: {
type: Number,
required: true,
},
times_won: {
type: Number,
required: true,
},
}, {
timestamps: true,
});
module.exports = { answerSchema };
This is my route for me the admin to add new answers (it's a game so only I can add them, that why the auth. Figured I'll include the complete code.)
router.post("/answers", async(req, res) => {
try {
const isMatch = await bcrypt.compare(
req.body.password,
process.env.ADMIN_PASSWORD
);
if (isMatch) {
const answer = new Answer({
body: req.body.answer.trim(),
times_dealt: 0,
times_picked: 0,
times_won: 0,
});
await answer.save();
res.status(201).send(answer);
}
res.status(401).send();
} catch (e) {
console.log("failed to save", e);
res.status(400).send(e);
}
});
Then whenever a card is dealt, I want to increase the count for times_dealt, and this is when I get the error. This is how I do it:
async function getOneAnswerCard(room) {
if (room.unused_answer_cards.length !== 0) {
// We pick a random answer's ID from our array of unused answers
const randomAnswerID = getRandomElement(room.unused_answer_cards);
// We get that answer's full object from our DB
const newAnswer = await Answer.findById(randomAnswerID);
// const newAnswer = await Answer.findByIdAndUpdate(randomAnswerID, {
// times_dealt: { $inc: 1 },
// });
await Answer.findByIdAndUpdate(randomAnswerID, {
times_dealt: { $inc: 1 },
});
// We remove it from the unused cards array
room.unused_answer_cards = room.unused_answer_cards.filter(
(answerID) => answerID !== randomAnswerID
);
// We add it to the dealt cards array
room.dealt_answer_cards.push(randomAnswerID);
// We serialize the answer (we don't want the user to get info on our answer stats)
const serializedAnswer = { _id: newAnswer._id, body: newAnswer.body };
return serializedAnswer;
}
}
Just getting the answer by itself is no issue. Getting a random ID and fetching an answer object works just fine. It's only when I've added the increment functionality that it started crashing.
I think you're using $inc with a wrong syntax. Try this:
await Answer.findByIdAndUpdate(randomAnswerID, {
{ $inc: { times_dealt: 1 } },
});
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.
I'm trying to update the subdocument within the array without success. The new data doesn't get saved.
Express:
router.put('/:id/:bookid', (req, res) => {
library.findOneAndUpdate(
{ "_id": req.params.id, "books._id": req.params.bookid},
{
"$set": {
"title.$": 'new title'
}
}
});
LibraryScema:
const LibarySchema = new Library({
Name: {
type: String,
required: false
},
books: [BookSchema]
});
bookScema:
const BookSchema = new Schema({
title: {
type: String,
required: false
},
Chapters: [
{
chapterTitle: {
type: String,
required: false
}
}
]
});
I only aim to update the sub-document, not parent- and sub-document at same time.
I had a similar issue. I believe there is something wrong with the $set when it comes to nested arrays (There was an entire issue thread on GitHub). This is how I solved my issue.
var p = req.params;
var b = req.body;
Account.findById(req.user._id, function (err, acc) {
if (err) {
console.log(err);
} else {
acc.websites.set(req.params._id, req.body.url); //This solved it for me
acc.save((err, webs) => {
if (err) {
console.log(err);
} else {
console.log('all good');
res.redirect('/websites');
}
});
}
});
I have a user with a nested array.
Try this code
router.put('/:id/:bookid', (req, res) => {
library.findById(
req.params.id, (err, obj) => {
if (err) console.log(err); // Debugging
obj.books.set(req.params.bookid, {
"title": 'new title',
'Chapters': 'your chapters array'
});
obj.save((err,obj)=>{
if(err) console.log(err); // Debugging
else {
console.log(obj); // See if the saved object is what expected;
res.redirect('...') // Do smth here
}
})
})
});
Let me know if it works, and I'll add explanation.
Explanation: You start by finding the right object (library in this case), then you find the correct object in the array called books.
Using .set you set the whole object to the new state. You'll need to take the data that's not changing from a previous instance of the library object.
I believe this way will overwrite and remove any data that's not passed into the .set() method. And then you save() the changed.
Found the problem while writing this.
As it may help others not searching in the right place like me, you will find the answer below.
I can't seem to make a mongoose Model.update() request work, when a Model.findOne() with the same condition does.
To illustrate, here is my code:
Schema:
var GeolocSchema = new mongoose.Schema({
id: {
type: String,
required: true,
unique: true
},
count: Number
});
An express test route showing the seemingly broken update attempt:
router.get('/test', function(req, res, next) {
var doc = {
id: '50_633-20_059',
count: 3,
density: 1
};
var promise = Geoloc.update({
id: doc.id
}, {
$set: {
density: doc.density
}
}).exec();
promise.then((result, err) => {
if (err) return next(err, null);
res.status(201).json(result);
});
});
Results in:
{
"ok": 0,
"n": 0,
"nModified": 0
}
An express test route showing the working findOne call:
router.get('/test2', function(req, res, next) {
var doc = {
id: '50_633-20_059',
count: 3,
density: 1
};
var promise = Geoloc.findOne({
id: doc.id
}).exec();
promise.then((result, err) => {
if (err) return next(err, null);
console.log('Update result: ', result);
res.status(200).json(result);
});
});
Results in:
{
"_id": "5885d33239f30034de9a38d0",
"id": "50_633-20_059",
"count": 3,
"__v": 0,
}
Credits to this unaccepted answer for putting me on the right track.
The culprit was my schema.
Interestingly enough (for a schema-less database), an update operation can't be made on non-existing fields: probably a fail safe to prevent a DB flood from an external source.
Note that it works as expected via the Mongo CLI.
Which means that the only necessary change in the code above was the following:
Schema:
var GeolocSchema = new mongoose.Schema({
id: {
type: String,
required: true,
unique: true
},
count: Number,
density: Number // This is mandatory!
});
More infos here.
I am in a bit of a pickle. Whenever I create a new resume as a logged in user it doesn't add the resume id as an array. I.e, ["20293", "2932392", "32903239"]
Instead, it overwrites the current resume id in the users schema. Here is the code
UserSchema
const UserSchema = new Schema({
_vId: {
type: String,
default: id.generate()
},
firstName: {
type: String,
required: true
},
lastName: {
type: String,
required: true
},
accountType: {
type: String,
enum: ['Alphaneer', 'Administrator', 'Support', 'PRO'],
default: 'Alphaneer'
},
email: {
type: String,
required: true,
trim: true
},
username: {
type: String,
required: true,
trim: true,
unique: true
},
bio: {
type: String,
default: "No bio provided."
},
// TODO: Hash the password before inserting as a document :)
password: {
type: String,
required: true
},
createdAt: {
type: String,
default: moment(new Date()).format("MMM DD, YYYY") // "Sun, 3PM 17"
},
resume: [ { type: mongoose.Schema.ObjectId, ref: "Resume" } ]
});
Where I post my resume
// POST /dashboard/resume/create
router.post('/resume/create', (req, res, next) => {
Resume.create(req.body, (err, resume) => {
if (err) {
var err = new Error("Error:" + err);
err.status = 404;
next(err);
} else {
req.user = jwtDecode.decode(req.session.tokenID, 'secret');
//I am assuming that you have saved your resume and getting the saved object in `resume`, now update the logged in user in req.user
var user = req.user.sessionId;
var updateData = {
resume: resume._id
}
//save the updated user
User.findByIdAndUpdate(user, updateData, function(err, user) {
console.log(user);
if (err) {
res.json(err);
} else {
res.json(user);
}
})
}
})
});
gif of submitting new resumes
UPDATE:
error picture
UPDATED CODE:
// POST /dashboard/resume/create
router.post('/resume/create', (req, res, next) => {
Resume.create(req.body, (err, resume) => {
if (err) {
var err = new Error("Error:" + err);
err.status = 404;
next(err);
} else {
req.user = jwtDecode.decode(req.session.tokenID, 'secret');
//I am assuming that you have saved your resume and getting the saved object in `resume`, now update the logged in user in req.user
var user = req.user.sessionId;
var updateData = {
resume: resume._id
}
//save the updated user
User.findById(user, function(err, user) {
console.log(user);
if (err) {
res.json(err);
} else {
user.resume.push(resume.id)
user.save(function(user) {
return res.json(user);
});
}
})
}
})
});
This is wrong:
var user = req.user.sessionId;
var updateData = {
resume: resume._id
}
//save the updated user
User.findByIdAndUpdate(user, updateData, function(err, user) {
console.log(user);
if (err) {
res.json(err);
} else {
res.json(user);
}
});
The resume field is an array and you are manipulating it as a string field. The method findOneAndUpdate do two things:
Find the document by it's id
Update it with the new data
The second argument is the new data to set. So, the second step is translated to:
User.upate({ _id: user }, { resume: resume._id });
Can you see what's wrong? resume must store an array of resume's id and your are setting a id as value. Obviously this will throw an MongooseError.
Your second shot is correct but has a typo error:
User.findById(user, function(err, user) {
console.log(user);
if (err) {
res.json(err);
} else {
user.resume.push(resume.id)
user.save(function(user) {
return res.json(user);
});
}
});
You must add the _id field since this is the ObjectID of the new created document (resume). So, you need to do user.resume.push(resume._id) instead.
Update
According with your last comment, you want to populate your User model, that is, through association id's retrieve all model data. In this case, is recommended that the resumes array change like this:
...
resumes: [
{
resume: {
type: Schema.Types.ObjectId,
ref: 'Resume'
}
}
]
To populate the User document with all Resume data you just need to reference the resume key in resumes field array.
User.findById(user, function(err, user) {
if (err) {
return res.json({ success: false, message: err.message });
}
user.resume.push(resume.id)
user.save(function(err, user) {
if (err) {
return res.json({ success: false, message: err.message });
}
// save was fine, finally return the user document populated
User.findById(user).populate('resumes.resume').exec(function(err, u) {
return res.json(u);
});
});
}
});
The populate method accepts a string with the fields that we want fill with it model data. In your case is an only field (resume). After run the query, you will get something like this:
{
_id: a939v0240mf0205jf48ut84sdfdjg4,
...,
resumes: [
resume: {
_id: f940tndfq4ut84jofgh03ut85dg9454g,
title: 'Some title'
},
...
]
}
Just to follow up on my comment regarding how I suggest you solve the issue:
router.post('/resume/create', (req, res, next) => {
Resume.create(req.body, (err, resume) => {
if (err) {
var err = new Error("Error:" + err);
err.status = 404;
next(err);
} else {
req.user = jwtDecode.decode(req.session.tokenID, 'secret');
//Here, instead of creating a new key entry for resume, you rather push new resume-id into the resume property of the "found user".
//find, update and save the user
User.findOne({_id: req.user.sessionId}, function (err, userToUpdate) {
userToUpdate.toJSON().resume.push(resume.id);
userToUpdate.save(function (err) {
if(err) {
console.error('ERROR!');
}
});
});
}
})
});
I left the rest of your code (saving new resume) untouched - I assume that part works. Give this a try and let me know if you encounter some problems.