0
votes

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?

2
1. It is defined as unique:true so what else would you get? 2. var mockPoll = sinon.mock(new Poll({ title: 'jv', choices: [{title:"xxxx","count":"1"}], votedIp:[{ad1:"9.",add2:"1.",add3:"2.",add4:"1"}], createdAt:"25/07/2017",createdBy:"Elisa"})); I was trying to mock test your schema but ran into how you have defined the "votedIp". Can you share the values as they are sent to the db? - user2347763
You don't use "unique indexes" for this purpose. Instead you actually use the $addToSet just like you are doing in order to enforce "uniqueness within the array". A "unique index" means across ALL documents. Therefore it is not possible to share a value within that array in any other document. This also includes undefined and null values. Which is what happens when you create an "empty" array. Remove the index from your database and schema and simply use $addToSet to enforce the logic in your application instead. - Neil Lunn
Also see Unique Constraint Across Separate Documents which basically says that "even if" your intent was to be unique across all documents, this does not enforce the constrain on the "array itself". Therefore the method is done using $addToSet or with $push checking for the lack of presence of the value in the array before update. - Neil Lunn
In fact. This "should" be a $push, as in .findOneAndUpdate({ "choices.title": field, "votedIp": { "$ne": ip } },{ "$inc": { "choices.$.count": 1 }, "$push": { "votedIp": ip } },{ "new": true },(err,poll) => { .... The reason being that without that $ne check you $inc the vote count even when the "votedIp" entry already exists. So your logic allows the same "votedIp" value to vote more than once, where you seem to intend that this not be allowed to happen. - Neil Lunn
Thanks to everyone that responded! I ended up setting the votedIp to a combination of the schema id and the user ip, and that also solves the unique index issue! - Elisa L

2 Answers

1
votes

@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)

enter image description here

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":"[email protected]","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:"[email protected]","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":"[email protected]","count":"1"}],
            "votedIp":"9.1.2.1","createdAt":"23/07/2017","createdBy":"Lisa"}, callback);
        // var poll = Poll.create({ "title": 'jv', "choices": [{"title":"[email protected]","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();
    });
});
0
votes

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