all files / lib/waterline/model/lib/defaultMethods/ save.js

93.26% Statements 83/89
84.09% Branches 37/44
100% Functions 17/17
95.35% Lines 82/86
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221                                 145×   145× 142× 142×     145×     145×               145×   145×         145×   145×                                       145×   145× 145×                     145×         145×     160×     155× 112×       155× 17×       145× 145×             144× 144× 144×     144× 112×     144×                 144× 144× 144×     144× 17×     144×             145×           144× 144×   144×     144×     144× 10× 10×   10×     10×     134×               132× 132× 639×       132×       132×     145×      
var _ = require('lodash');
var async = require('async');
var deep = require('deep-diff');
var updateInstance = require('../associationMethods/update');
var addAssociation = require('../associationMethods/add');
var removeAssociation = require('../associationMethods/remove');
var hop = require('../../../utils/helpers').object.hasOwnProperty;
var defer = require('../../../utils/defer');
var WLError = require('../../../error/WLError');
var noop = function() {};
 
/**
 * Model.save()
 *
 * Takes the currently set attributes and updates the database.
 * Shorthand for Model.update({ attributes }, cb)
 *
 * @param {Object} context
 * @param {Object} proto
 * @param {Function} callback
 * @param {Object} options
 * @return {Promise}
 * @api public
 */
 
module.exports = function(context, proto, options, cb) {
 
  var deferred;
 
  if (typeof options === 'function') {
    cb = options;
    options = {};
  }
 
  if (typeof cb !== 'function') {
    deferred = defer();
  }
 
  cb = cb || noop;
 
  /**
   * TO-DO:
   * This should all be wrapped in a transaction. That's coming next but for the meantime
   * just hope we don't get in a nasty state where the operation fails!
   */
 
  var mutatedModels = [];
 
  async.auto({
 
    // Compare any populated model values to their current state.
    // If they have been mutated then the values will need to be synced.
    compareModelValues: function(next) {
      var modelKeys = Object.keys(proto.associationsCache);
 
      async.each(modelKeys, function(key, nextKey) {
        if (!hop(proto, key) || proto[key] === undefined) {
          return async.setImmediate(function() {
            nextKey();
          });
        }
 
        var currentVal = proto[key];
        var previousVal = proto.associationsCache[key];
 
        // Normalize previousVal to an object
        Iif (Array.isArray(previousVal)) {
          previousVal = previousVal[0];
        }
 
        Iif (deep(currentVal, previousVal)) {
          mutatedModels.push(key);
        }
 
        return async.setImmediate(function() {
          nextKey();
        });
      }, next);
    },
 
    // Update The Current Record
    updateRecord: ['compareModelValues', function(next) {
 
      // Shallow clone proto.toObject() to remove all the functions
      var data = _.clone(proto.toObject());
 
      new updateInstance(context, data, mutatedModels, function(err, data) {
        next(err, data);
      });
    }],
 
 
    // Build a set of associations to add and remove.
    // These are populated from using model[associationKey].add() and
    // model[associationKey].remove().
    buildAssociationOperations: ['compareModelValues', function(next) {
 
      // Build a dictionary to hold operations based on association key
      var operations = {
        addKeys: {},
        removeKeys: {}
      };
 
      Object.keys(proto.associations).forEach(function(key) {
 
        // Ignore belongsTo associations
        if (proto.associations[key].hasOwnProperty('model')) return;
 
        // Grab what records need adding
        if (proto.associations[key].addModels.length > 0) {
          operations.addKeys[key] = proto.associations[key].addModels;
        }
 
        // Grab what records need removing
        if (proto.associations[key].removeModels.length > 0) {
          operations.removeKeys[key] = proto.associations[key].removeModels;
        }
      });
 
      return async.setImmediate(function() {
        return next(null, operations);
      });
 
    }],
 
    // Create new associations for each association key
    addAssociations: ['buildAssociationOperations', 'updateRecord', function(next, results) {
      var keys = results.buildAssociationOperations.addKeys;
      return new addAssociation(context, proto, keys, function(err, failedTransactions) {
        Iif (err) return next(err);
 
        // reset addKeys
        for (var key in results.buildAssociationOperations.addKeys) {
          proto.associations[key].addModels = [];
        }
 
        next(null, failedTransactions);
      });
    }],
 
    // Remove associations for each association key
    // Run after the addAssociations so that the connection pools don't get exhausted.
    // Once transactions are ready we can remove this restriction as they will be run on the same
    // connection.
    removeAssociations: ['buildAssociationOperations', 'addAssociations', function(next, results) {
      var keys = results.buildAssociationOperations.removeKeys;
      return new removeAssociation(context, proto, keys, function(err, failedTransactions) {
        Iif (err) return next(err);
 
        // reset removeKeys
        for (var key in results.buildAssociationOperations.removeKeys) {
          proto.associations[key].removeModels = [];
        }
 
        next(null, failedTransactions);
      });
    }]
 
  },
 
  function(err, results) {
    if (err) {
      Iif (deferred) {
        deferred.reject(err);
      }
      return cb(err);
    }
 
    // Collect all failed transactions if any
    var failedTransactions = [];
    var error;
 
    if (results.addAssociations) {
      failedTransactions = failedTransactions.concat(results.addAssociations);
    }
 
    if (results.removeAssociations) {
      failedTransactions = failedTransactions.concat(results.removeAssociations);
    }
 
    if (failedTransactions.length > 0) {
      error = new Error('Some associations could not be added or destroyed during save().');
      error.failedTransactions = failedTransactions;
 
      Iif (deferred) {
        deferred.reject(new WLError(error));
      }
      return cb(new WLError(error));
    }
 
    if (!results.updateRecord.length) {
      error = new Error('Error updating a record.');
      Eif (deferred) {
        deferred.reject(new WLError(error));
      }
      return cb(new WLError(error));
    }
 
    // Reset the model attribute values with the new values.
    // This is needed because you could have a lifecycle callback that has
    // changed the data since last time you accessed it.
    // Attach attributes to the model instance
    var newData = results.updateRecord[0];
    _.each(newData, function(val, key) {
      proto[key] = val;
    });
 
    // If a promise, resolve it
    if (deferred) {
      deferred.resolve();
    }
 
    // Return the callback
    return cb();
  });
 
  if (deferred) {
    return deferred.promise;
  }
};