Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

(mongoose/promises) How do you check if document was created using findOneAndUpdate with upsert

Consider this code, where I need to either create or update a particular document.

Inbox.model.findOneAndUpdate({ number: req.phone.number }, {
    number: req.phone.number,
    country: req.phone.country,
    token: hat(),
    appInstalled: true
}, { new: true, upsert: true }).then(function(inbox){
    /*
       do something here with inbox, but only if the inbox was created (not updated)
    */
});

Does mongoose have a facility to be able to make the distinction between whether or not the documented was created or updated? I require new: true because I need to call functions on the inbox.

like image 934
Matt Way Avatar asked Aug 29 '15 01:08

Matt Way


1 Answers

In the case of .findOneAndUpdate() or any of the .findAndModify() core driver variants for mongoose, the actual callback signature has "three" arguments:

 function(err,result,raw)

With the first being any error response, then the modified or original document depending on options and the third which is a write result of the issued statement.

That third argument should return data much like this:

{ lastErrorObject:
   { updatedExisting: false,
     n: 1,
     upserted: 55e12c65f6044f57c8e09a46 },
  value: { _id: 55e12c65f6044f57c8e09a46, 
           number: 55555555, 
           country: 'US', 
           token: "XXX", 
           appInstalled: true,
           __v: 0 },
  ok: 1 }

With the consistent field in there as lastErrorObject.updatedExisting being either true/false depending on the result of whether an upsert occured. Note that there is also a "upserted" value containing the _id response for the new document when this property is false, but not when it is true.

As such you would then modify your handling to consider the third condition, but this only works with a callback and not a promise:

Inbox.model.findOneAndUpdate(
    { "number": req.phone.number },
    { 
      "$set": {
          "country": req.phone.country,
          "token": hat(),
          "appInstalled": true
      }
    }, 
    { "new": true, "upsert": true },
    function(err,doc,raw) {

      if ( !raw.lastErrorObject.updatedExitsing ) {
         // do things with the new document created
      }
    }
);

Where I would also strongly suggest you use update operators rather than raw objects here, as a raw object will always overwrite the entire document, yet operators like $set just affect the listed fields.

Also noting that any matching "query arguments" to the statement are automatically assigned in the new document as long as their value is an exact match that was not found.

Given that using a promise does not seem to return the additional information for some reason, then do not see how this is possible with a promise other than setting { new: false} and basically when no document is returned then it's a new one.

You have all the document data expected to be inserted anyway, so it is not like you really need that data returned anyway. It is in fact how the native driver methods handle this at the core, and only respond with the "upserted" _id value when an upsert occurs.

This really comes down to another issue discussed on this site, under:

Can promises have multiple arguments to onFulfilled?

Where this really comes down to the resolution of multiple objects in a promise response, which is something not directly supported in the native speicification but there are approaches listed there.

So if you implement Bluebird promises and use the .spread() method there, then everything is fine:

var async = require('async'),
    Promise = require('bluebird'),
    mongoose = require('mongoose'),
    Schema = mongoose.Schema;

mongoose.connect('mongodb://localhost/test');

var testSchema = new Schema({
  name: String
});

var Test = mongoose.model('Test',testSchema,'test');
Promise.promisifyAll(Test);
Promise.promisifyAll(Test.prototype);

async.series(
  [
    function(callback) {
      Test.remove({},callback);
    },
    function(callback) {
      var promise = Test.findOneAndUpdateAsync(
        { "name": "Bill" },
        { "$set": { "name": "Bill" } },
        { "new": true, "upsert": true }
      );

      promise.spread(function(doc,raw) {
        console.log(doc);
        console.log(raw);
        if ( !raw.lastErrorObject.updatedExisting ) {
          console.log( "new document" );
        }
        callback();
      });
    }
  ],
  function(err) {
    if (err) throw err;
    mongoose.disconnect();
  }
);

Which of course returns both objects and you can access then consistently:

{ _id: 55e14b7af6044f57c8e09a4e, name: 'Bill', __v: 0 }
{ lastErrorObject:
   { updatedExisting: false,
     n: 1,
     upserted: 55e14b7af6044f57c8e09a4e },
  value: { _id: 55e14b7af6044f57c8e09a4e, name: 'Bill', __v: 0 },
  ok: 1 }

Here is a full listing demonstrating the normal behaviour:

var async = require('async'),
    mongoose = require('mongoose'),
    Schema = mongoose.Schema;

mongoose.connect('mongodb://localhost/test');

var testSchema = new Schema({
  name: String
});

var Test = mongoose.model('Test',testSchema,'test');

async.series(
  [
    function(callback) {
      Test.remove({},callback);
    },
    function(callback) {
      Test.findOneAndUpdate(
        { "name": "Bill" },
        { "$set": { "name": "Bill" } },
        { "new": true, "upsert": true }
      ).then(function(doc,raw) {
        console.log(doc);
        console.log(raw);
        if ( !raw.lastErrorObject.updatedExisting ) {
          console.log( "new document" );
        }
        callback();
      });
    }
  ],
  function(err) {
    if (err) throw err;
    mongoose.disconnect();
  }
);

For the record, the native driver itself does not have this issue as the response object is in fact it's only object returned aside from any error:

var async = require('async'),
    mongodb = require('mongodb'),
    MongoClient = mongodb.MongoClient;

MongoClient.connect('mongodb://localhost/test',function(err,db) {

  var collection = db.collection('test');

  collection.findOneAndUpdate(
    { "name": "Bill" },
    { "$set": { "name": "Bill" } },
    { "upsert": true, "returnOriginal": false }
  ).then(function(response) {
    console.log(response);
  });
});

So it is always something like this:

{ lastErrorObject:
   { updatedExisting: false,
     n: 1,
     upserted: 55e13bcbf6044f57c8e09a4b },
  value: { _id: 55e13bcbf6044f57c8e09a4b, name: 'Bill' },
  ok: 1 }
like image 190
Blakes Seven Avatar answered Sep 24 '22 23:09

Blakes Seven