2

I'm writing alexa skill and would like to check if user exists in MongoDB. My code works but I don't know how to define situation if user is already in a database :(

Everytime when I execute code I get: "Hello Anna you are new here"

My user Anna is saved in MongoDB

But I would like to distinguish when my user is already in a database and react for that.

Does anybody smart has a solution for my problem?

    var myName = "Anan1";
    var userID = this.event.session.user.userId;
    console.log(userID);

    self = this;
    User.findOneAndUpdate(
        {userId:  userID},
        {$set:{name:myName}},
        {upsert: true, new: false, runValidators: true},
        function(err, doc){
            if(err){
                console.log("eeoror");
            }

            console.log(doc);
            if (doc==null){
                self.emit(':ask',
                    "Hello "+ myName +"you are new here")
            }else {
                self.emit(':ask',
                    "Hello "+ myName +"you are not new here")
            }

        });
Neil Lunn
  • 148,042
  • 36
  • 346
  • 317
Anna K
  • 1,666
  • 4
  • 23
  • 47
  • Well you could do `new: false` and simply check if the returned document is `null` or not. i.e not `null` means it existed before and, `null` means it was created. You should essentially have all the document details in this case, so you probably don't need the matched return. Alternately you can ask for the `rawResponse` which should indicate whether the `upsert` occurred or not. Also you probably really want `$setOnInsert` here. – Neil Lunn Apr 24 '18 at 22:03
  • It doesnt work :( Im getting always Hello you are not new here. I updated my code, could you please take a look :) – Anna K Apr 24 '18 at 23:21

2 Answers2

4

It sounds like what you really want is a unique key constraint and not an upsert.

The unique key can be set in [mongoose] with either the schema field options:

const s = new Schema({ name: { type: String, unique: true }});

or by the index method:

Schema.path('name').index({ unique: true });

If an attempt is made to create a document that already has an entry for that key then an error will be thrown:

NOTE: violating the constraint returns an E11000 error from MongoDB when saving, not a Mongoose validation error.

Jason Cust
  • 10,743
  • 2
  • 33
  • 45
  • This really does not matter as the operation is already stated to use `upsert` as an option. You cannot throw a Duplicate key error on an "upsert" where the specified "unique" field is used as the query constraint for determining whether the document exists or whether it is created. The OP is actually asking something else, which is essentially *"How do I tell whether it was created or not"*. And "upserts" are generally a bit "smarter" for this action than trapping errors. See the listing I answered with. – Neil Lunn Apr 25 '18 at 00:36
  • @NeilLunn I politely disagree. My answer was based off of the title the OP used to describe their intention. Assuming that is correct, then their implementation using an upsert would be incorrect altogether hence my note about "not an upsert". Regarding your assumption that upserts "are generally a bit 'smarter'", that comes with litany of caveats. Needless to say, assumptions can be dangerous. – Jason Cust Apr 25 '18 at 00:59
  • I really think you should read my answer in full as you might learn something. There are solid reasons behind the logic. – Neil Lunn Apr 25 '18 at 01:38
  • @NeilLunn Assuming I "might learn something" is quite an interesting take. But sure, let's assume I can learn something from your answer then I assume maybe you can also learn something as well. For example, at some future time another process for creating a user outside of this implementation is needed and the implementor doesn't know about this special handling. This could be a serious risk of creating a duplicate record. On the other hand given MongoDB provides a built in way of preventing this from occurring no matter how it's implemented, why would it be "smarter" to reinvent the wheel? – Jason Cust Apr 25 '18 at 01:49
  • IMO this is the correct answer. They key should be unique. – Mick May 04 '21 at 11:20
4

As noted in comment earlier, you have two basic approaches to work out whether something was "created" or not. These are either to:

  • Return the rawResult in the response and check the updatedExisting property which tells you if it's an "upsert" or not

  • Set new: false so that "no document" is actually returned in result when it's actually an "upsert"

As a listing to demonstrate:

const { Schema } = mongoose = require('mongoose');

const uri = 'mongodb://localhost/thereornot';

mongoose.set('debug', true);
mongoose.Promise = global.Promise;

const userSchema = new Schema({
  username: { type: String, unique: true },   // Just to prove a point really
  password: String
});

const User = mongoose.model('User', userSchema);

const log = data => console.log(JSON.stringify(data, undefined, 2));

(async function() {

  try {

    const conn = await mongoose.connect(uri);

    await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));

    // Shows updatedExisting as false - Therefore "created"

    let bill1 = await User.findOneAndUpdate(
      { username: 'Bill' },
      { $setOnInsert: { password: 'password' } },
      { upsert: true, new: true, rawResult: true }
    );
    log(bill1);

    // Shows updatedExisting as true - Therefore "existing"

    let bill2 = await User.findOneAndUpdate(
      { username: 'Bill' },
      { $setOnInsert: { password: 'password' } },
      { upsert: true, new: true, rawResult: true }
    );
    log(bill2);

    // Test with something like:
    // if ( bill2.lastErrorObject.updatedExisting ) throw new Error("already there");


    // Return will be null on "created"
    let ted1 = await User.findOneAndUpdate(
      { username: 'Ted' },
      { $setOnInsert: { password: 'password' } },
      { upsert: true, new: false }
    );
    log(ted1);

    // Return will be an object where "existing" and found
    let ted2 = await User.findOneAndUpdate(
      { username: 'Ted' },
      { $setOnInsert: { password: 'password' } },
      { upsert: true, new: false }
    );
    log(ted2);

    // Test with something like:
    // if (ted2 !== null) throw new Error("already there");

    // Demonstrating "why" we reserve the "Duplicate" error
    let fred1 = await User.findOneAndUpdate(
      { username: 'Fred', password: 'password' },
      { $setOnInsert: { } },
      { upsert: true, new: false }
    );
    log(fred1);       // null - so okay

    let fred2 = await User.findOneAndUpdate(
      { username: 'Fred', password: 'badpassword' }, // <-- dup key for wrong password
      { $setOnInsert: { } },
      { upsert: true, new: false }
    );

    mongoose.disconnect();

  } catch(e) {
    console.error(e)
  } finally {
    process.exit()
  }


})()

And the output:

Mongoose: users.remove({}, {})
Mongoose: users.findAndModify({ username: 'Bill' }, [], { '$setOnInsert': { password: 'password', __v: 0 } }, { upsert: true, new: true, rawResult: true, remove: false, fields: {} })
{
  "lastErrorObject": {
    "n": 1,
    "updatedExisting": false,
    "upserted": "5adfc8696878cfc4992e7634"
  },
  "value": {
    "_id": "5adfc8696878cfc4992e7634",
    "username": "Bill",
    "__v": 0,
    "password": "password"
  },
  "ok": 1,
  "operationTime": "6548172736517111811",
  "$clusterTime": {
    "clusterTime": "6548172736517111811",
    "signature": {
      "hash": "AAAAAAAAAAAAAAAAAAAAAAAAAAA=",
      "keyId": 0
    }
  }
}
Mongoose: users.findAndModify({ username: 'Bill' }, [], { '$setOnInsert': { password: 'password', __v: 0 } }, { upsert: true, new: true, rawResult: true, remove: false, fields: {} })
{
  "lastErrorObject": {
    "n": 1,
    "updatedExisting": true
  },
  "value": {
    "_id": "5adfc8696878cfc4992e7634",
    "username": "Bill",
    "__v": 0,
    "password": "password"
  },
  "ok": 1,
  "operationTime": "6548172736517111811",
  "$clusterTime": {
    "clusterTime": "6548172736517111811",
    "signature": {
      "hash": "AAAAAAAAAAAAAAAAAAAAAAAAAAA=",
      "keyId": 0
    }
  }
}
Mongoose: users.findAndModify({ username: 'Ted' }, [], { '$setOnInsert': { password: 'password', __v: 0 } }, { upsert: true, new: false, remove: false, fields: {} })
null
Mongoose: users.findAndModify({ username: 'Ted' }, [], { '$setOnInsert': { password: 'password', __v: 0 } }, { upsert: true, new: false, remove: false, fields: {} })
{
  "_id": "5adfc8696878cfc4992e7639",
  "username": "Ted",
  "__v": 0,
  "password": "password"
}

So the first case actually considers this code:

User.findOneAndUpdate(
  { username: 'Bill' },
  { $setOnInsert: { password: 'password' } },
  { upsert: true, new: true, rawResult: true }
)

Most options are standard here as "all" "upsert" actions will result in the field content being used to "match" ( i.e the username ) is "always" created in the new document, so you don't need to $set that field. In order to not actually "modify" other fields on subsequent requests you can use $setOnInsert, which only adds these properties during an "upsert" action where no match is found.

Here the standard new: true is used to return the "modified" document from the action, but the difference is in the rawResult as is shown in the returned response:

{
  "lastErrorObject": {
    "n": 1,
    "updatedExisting": false,
    "upserted": "5adfc8696878cfc4992e7634"
  },
  "value": {
    "_id": "5adfc8696878cfc4992e7634",
    "username": "Bill",
    "__v": 0,
    "password": "password"
  },
  "ok": 1,
  "operationTime": "6548172736517111811",
  "$clusterTime": {
    "clusterTime": "6548172736517111811",
    "signature": {
      "hash": "AAAAAAAAAAAAAAAAAAAAAAAAAAA=",
      "keyId": 0
    }
  }
}

Instead of a "mongoose document" you get the actual "raw" response from the driver. The actual document content is under the "value" property, but it's the "lastErrorObject" we are interested in.

Here we see the property updatedExisting: false. This indicates that "no match" was actually found, thus a new document was "created". So you can use this to determine that creation actually happened.

When you issue the same query options again, the result will be different:

{
  "lastErrorObject": {
    "n": 1,
    "updatedExisting": true             // <--- Now I'm true
  },
  "value": {
    "_id": "5adfc8696878cfc4992e7634",
    "username": "Bill",
    "__v": 0,
    "password": "password"
  },
  "ok": 1,
  "operationTime": "6548172736517111811",
  "$clusterTime": {
    "clusterTime": "6548172736517111811",
    "signature": {
      "hash": "AAAAAAAAAAAAAAAAAAAAAAAAAAA=",
      "keyId": 0
    }
  }
}

The updatedExisting value is now true, and this is because there already was a document that matched the username: 'Bill' in the query statement. This tells you the document was already there, so you can then branch your logic to return an "Error" or whatever response you want.

In the other case, it may be desirable to "not" return the "raw" response and use a returned "mongoose document" instead. In this case we vary the value to be new: false without the rawResult option.

User.findOneAndUpdate(
  { username: 'Ted' },
  { $setOnInsert: { password: 'password' } },
  { upsert: true, new: false }
)

Most of the same things apply except that now the action is the original state of the document is returned as opposed to the "modified" state of the document "after" the action. Therefore when there is no document that actually matches the "query" statement, the returned result is null:

Mongoose: users.findAndModify({ username: 'Ted' }, [], { '$setOnInsert': { password: 'password', __v: 0 } }, { upsert: true, new: false, remove: false, fields: {} })
null           // <-- Got null in response :(

This tells you the document was "created", and it's arguable that you already know what the content of the document should be since you sent that data with the statement ( ideally in the $setOnInsert ). Point being, you already know what to return "should" you require to actually return the document content.

By contrast, a "found" document returns the "original state" showing the document "before" it was modified:

{
  "_id": "5adfc8696878cfc4992e7639",
  "username": "Ted",
  "__v": 0,
  "password": "password"
}

Therefore any response which is "not null" is therefore an indication that the document was already present, and again you can branch your logic depending on what was actually received in response.

So those are the two basic approaches to what you are asking, and they most certainly "do work"! And just as is demonstrated and reproducible with the same statements here.


Addendum - Reserve Duplicate Key for bad passwords

There is one more valid approach that is hinted at in the full listing as well, which is essentially to simply .insert() ( or .create() from mongoose models ) new data and have a "duplicate key" error throw where the "unique" property by index is actually encountered. It's a valid approach but there is one particular use case in "user validation" which is a handy piece of logic handling, and that is "validating passwords".

So it's a pretty common pattern to retrieve user information by the username and password combination. In the case of an "upsert" this combination justifies as "unique" and therefore an "insert" is attempted if no match is found. This is exactly what makes matching the password a useful implementation here.

Consider the following:

    // Demonstrating "why" we reserve the "Duplicate" error
    let fred1 = await User.findOneAndUpdate(
      { username: 'Fred', password: 'password' },
      { $setOnInsert: { } },
      { upsert: true, new: false }
    );
    log(fred1);       // null - so okay

    let fred2 = await User.findOneAndUpdate(
      { username: 'Fred', password: 'badpassword' }, // <-- dup key for wrong password
      { $setOnInsert: { } },
      { upsert: true, new: false }
    );

On the first attempt we don't actually have a username for "Fred", so the "upsert" would occur and all the other things as already described above happen to identify whether it was a creation or a found document.

The statement that follows uses the same username value but provides a different password to what is recorded. Here MongoDB attempts to "create" the new document since it did not match on the combination, but because the username is expected to be "unique" you receive a "Duplicate key error":

{ MongoError: E11000 duplicate key error collection: thereornot.users index: username_1 dup key: { : "Fred" }

So what you should realize is you now get three conditions to evaluate for "free". Being:

  • The "upsert" was recorded by either the updatedExisting: false or null result depending on the method.
  • You know the document ( by combination ) "exists" via either the updatedExisting: true or where the document returns was "not null".
  • If the password provided was not a match for what already existed for the username, then you would get the "duplicate key error" which you can trap and respond accordingly, advising the user in response that the "password is incorrect".

All of that from one request.

That's the main reasoning for using "upserts" as opposed to simply throwing inserts at a collection, as you can get different branching of the logic without making additional requests to the database to determine "which" of those conditions should be the actual response.

Neil Lunn
  • 148,042
  • 36
  • 346
  • 317