Update by id with mongoose doesn't find the document - node.js

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.

Related

Mongoose auto-increment fails because of a cast error

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

How to push data in mongodb array?

This is my schema:
const productSchema = new mongoose.Schema({
name: String,
imageUrl: String,
category: String,
price: Number,
description: String,
featured: {
default: false
},
rating: [
{
userName: String,
score: Number,
comment: String
}
]
});
And this is how I was trying to push data into my database.
app.route("/review").post(function(req, res) {
const score = req.body.score;
const comment = req.body.comment;
if (req.isAuthenticated()) {
const review = {
$push: {
rating: {
userName: req.user.fName,
score: score,
comment: comment
}
}
};
console.log(review, req.body.productName);
Product.updateOne({ name: req.body.productName }, review, function(
err,
done
) {
if (err) {
console.log(err);
} else {
res.redirect("/products");
}
});
} else {
res.redirect("/login");
}
});
In the official documentation, it says that this is the way to push data in the MongoDB array. But still having no luck.
Help me to push reviews in the rating array field.
Thank You.
for my answer im using mongoose function findOneAndUpdate you can use updateOne instead , The writing to the database will be in a async function in order to not block the node process (will be alot faster).
Puting all the code in a try-catch block will allow you to control any errors if accrued.
You can read more about mongoose driver function and findOneAndUpdate here:
https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndUpdate/
more about async function:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
app.route('/review').post(async function(req, res) {
//destructuring values from body-parser
const { score, comment, productName } = req.body;
if (req.isAuthenticated()) {
//creating the rating obj
const rating = {
userName: req.user.fName,
score: score,
comment: comment
};
console.log(review, productName);
//setting it all in a try-catch block
try {
const product = await Product.findOneAndUpdate(
{name: productName},
{ $set: { rating: rating } },
{ new: true }
).then(err => {
if (err) throw err;
return res.redirect('/products');
});
res.redirect('/login');
} catch (error) {
console.log(error);
}
}
});
Hopefully this makes sense, good luck!

NodeJS MongoDB Mongoose - Getting _.id of newly created Schema

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.

Query not giving updated result after database update in mongoDB

I have a server setup in node.js that uses mongoose. The problem i am having is that when i update a database as follows
Questions.update({id:questionID}, {$push: {answers: {answer:answer,answeredBy:username}}}, callback);
I see that the data has been added in MongoCompass but when i query it using
Questions.find({tags:interests},callback);
the updated data is not returned. But if i restart the server and again query, i receive the updated result.
And if i just save the data and query by using .save method and query, the database is updated.
So the problem has to be with my .update method right? I really cant find what i am doing wrong
my database is as follows
let mongoose =new require('mongoose');
const uuidv4 = require('uuid/v4');
//Define a schema
let Schema = mongoose.Schema;
let QuestionsSchema = new Schema({
id: {
type: String,
unique: true
},
title: String,
details: String,
askedDate: {type: Date, default: Date.now},
tags: [{
type: String
}],
askedBy: String,
answers: [{
answeredBy: String,
answer: String,
answeredDate: {type: Date, default: Date.now}
}]
});
let Questions = module.exports = mongoose.model('Questions', QuestionsSchema);
module.exports.addQuestion = function (newQuestion, callback) {
//generating a new random id for each question
newQuestion.id = uuidv4();
newQuestion.save(callback);
};
module.exports.addAnswer = function (username,answer, questionID, callback) {
Questions.update({id:questionID}, {$push: {answers: {answer:answer,answeredBy:username}}}, callback);
};
module.exports.getQuestions = function (interests, callback) {
Questions.find({tags:interests},callback);
};
The methods are called as follows
Questions.getQuestions(user.tags[i], function (err, questions) {
if (err) {
let output = {
error: {
status: "fail",
name: err.name,
message: err.message,
text: err.toString()
}
};
let statusCode = err.status || 500;
res.json(output);
res.send();
}
else {
questions.map((data) => {
if (!sentTags.includes(data.id)) {
jsonResponse.push(data);
sentTags.push(data.id);
}
});
if (i === user.tags.length - 1) {
res.send(JSON.parse(JSON.stringify(jsonResponse)));
}
}
});
and
Questions.addAnswer(user.username,req.body.answer, req.body.id, function (err) {
if (err) {
let output = {
error: {
status: "fail",
name: err.name,
message: err.message,
text: err.toString()
}
};
let statusCode = err.status || 500;
res.status(statusCode).json(output);
res.send();
}
else {
res.json({status: "success", message: "Answer Added"});
res.send();
}
});
Since MongoDB is eventually consistent, if the Question is saved and then immediately updated, it could be the case that the changes haven't been fully propagated yet.
In Mongoose, try specifying the read concern to 'primary' with this:
Questions.find({tags:interests}).read('p').exec(callback)
How to specify read concerns in Mongoose
MongoDB Read Concern documentation
Depending on the use-case, you may also want to explore the write concerns:
MongoDB Write Concerns

Mongodb and Mongoose error: E11000 duplicate key error index

So I've been working on a project and I finished most of it, but then this error popped up, saying there is something that is undefined, here is the error:
E11000 duplicate key error index: build-a-voting-app.polls.$votedIp_1 dup key: { : undefined }
Here is my code for my create new mongo schema file (polls.model.js)
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const pollSchema = new Schema({
title: { type: String, unique: true, required: true },
choices: [
{
title: { type: String, required: true },
count: { type: Number, default: 0 }
}
],
votedIp: [{ type: String, unique: true }],
createdAt: {type:Date, default:Date.now()},
createdBy: String
});
const Poll = mongoose.model('polls', pollSchema);
module.exports = Poll;
Here is the function where I add the inputs
function submitVote(field, res, ip) {
Poll.findOneAndUpdate(
{ choices: { $elemMatch: { title: field } } },
{ $inc: { 'choices.$.count': 1 }, $addToSet: { 'votedIp': ip } },
{ new: true },
function (err, poll) {
if (err) throw err;
res.json({ updated: poll });
}
);
}
Here is how I first created it
var newPoll = new Poll({
title: req.body.title,
choices: choicesArr,
createdBy: req.session.user.username || req.session.user
}).save(function (err, poll) {
if (err) throw err
res.redirect('/mypolls')
});
If you want to see the full code please go to https://github.com/ElisaLuo/Freecodecamp-Build-A-Voting-App
I'm using the ip addresses for checking if the user has voted or not (I'm building a voting app), but right now, I cannot even create a new schema / poll. Does anyone know why the error happens and how I can solve it?
#Elisa l - you may want to read this - mongoose enforce unique attribute on subdocument property
However, I did manage to test with mongoose-mock and the behavior is as expected - test results below (please do check the two versions of votedIp in the test code snippets)
and as described in the MongoDb document referenced in the above link. Mongoose does not enforce the unique integrity, MongoDb does.
With the mocha test below (inserted as snippets, not to run the code but just for better readability, please ignore the messy look of the comments in the code but the permutation and combination had to be worked out!), I did manage to create the mongoose schema by adding a create method in "Poll". please note the change in the schema - votedIp: { type: String, unique: true }, you can change it to array in the test code.
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var pollSchema = new Schema({
title: { type: String, unique: true, required: true },
choices: [
{
title: { type: String, required: true },
count: { type: Number, default: 0 }
}
],
votedIp: { type: String, unique: true },
createdAt: {type:Date, default:Date.now()},
createdBy: String
});
// Below code added by RJv (ie. me :)
var NewPoll = new mongoose.Schema(pollSchema);
NewPoll.statics.create = function (params, callback) {
var newUpdate = new NewPoll(params);
newUpdate.save(function(err, result) {
callback(err, result);
});
return newUpdate;
};
var Poll = mongoose.model('Model', NewPoll);
module.exports = Poll;
var expect = require('chai').expect,mongooseMock = require('mongoose-mock'),proxyquire=require('proxyquire'),
sinon = require('sinon'), chai=require('chai'),sinonChai = require("sinon-chai");chai.use(sinonChai);
var Poll;
before(function(done){
Poll = proxyquire('./Poll', {'mongoose': mongooseMock});
done();
})
describe('model', function() {
/* beforeEach(function (done) {
Poll = proxyquire('./Poll', {'mongoose': mongooseMock});
done();
});*/
it("should be called once",function(done){
setTimeout(done, 15000);
var callback = sinon.spy();
var poll1 = Poll.create({ "title": 'jv', "choices": [{"title":"jv#gmail.com","count":"1"}],
"votedIp":"9.1.2.1","createdAt":"23/07/2017","createdBy":"Lisa"}, callback);
// Below to pass data for votedIp as an array as described in the original schema by Elisa
//"votedIp":[{"add1":"9.","add2":"1.","add3":"2.","add4":"1"}],"createdAt":"23/07/2017","createdBy":"Lisa"}, callback);
//expect(poll1.votedIp[0].add1+poll1.votedIp[0].add2+poll1.votedIp[0].add3+poll1.votedIp[0].add4).equals("9.1.2.1");
expect(poll1.save).calledOnce;
console.log(JSON.stringify(poll1));
expect(poll1.votedIp).equals("9.1.2.1");
done();
});
it('should expect same ip to get added', function(done) {
this.timeout(5000);
setTimeout(done, 15000);
var callback = sinon.spy();//mock(new Poll({ title: 'jv', choices: [{title:"jv#gmail.com","count":"1"}], votedIp:[{ad1:"9.",add2:"1.",add3:"2.",add4:"1"}],createdAt:"25/07/2017",createdBy:"Lisa"}));
var poll = Poll.create({ "title": 'jv', "choices": [{"title":"jv#gmail.com","count":"1"}],
"votedIp":"9.1.2.1","createdAt":"23/07/2017","createdBy":"Lisa"}, callback);
// var poll = Poll.create({ "title": 'jv', "choices": [{"title":"jv#gmail.com","count":"1"}],
// Below to pass data for votedIp as an array as described in the original schema by Elisa
// "votedIp":[{"add1":"9.","add2":"1.","add3":"2.","add4":"1"}],"createdAt":"25/07/2017","createdBy":"Lisa"}, callback);
// expect(poll.votedIp[0].add1+poll.votedIp[0].add2+poll.votedIp[0].add3+poll.votedIp[0].add4).equals("9.1.2.1");
expect(poll.save).calledOnce;
expect(poll.votedIp).equals("9.1.2.1");
//assert(spy.calledOnce);
done();
});
});
Are you calling submitVote multiple times in quick succession? You might be running into https://jira.mongodb.org/browse/SERVER-14322.
The suggested fix for this is to check the error and if one of the calls fails retry it.
https://docs.mongodb.com/manual/reference/method/db.collection.update/#use-unique-indexes

Resources