From 0f3f27af513fd1332d5e5de11591e237d3dcb273 Mon Sep 17 00:00:00 2001
From: Bert Casier <bert@ernie.be>
Date: Wed, 4 Nov 2015 11:05:24 +0100
Subject: [PATCH] Make automatic validation optional

Make automatic validation optional on all CRUD methods in a loopback
model. This can be done in 2 ways

- set `automaticValidation` in the model settings

- set `validate` on the options passed when calling the crud methods

The options take precedence on the model setting.
By default the automatic validation remains true to be backwards
compatible
---
 lib/dao.js                       |  73 ++++-
 test/optional-validation.test.js | 514 +++++++++++++++++++++++++++++++
 2 files changed, 578 insertions(+), 9 deletions(-)
 create mode 100644 test/optional-validation.test.js

diff --git a/lib/dao.js b/lib/dao.js
index fc096aed..4a812e45 100644
--- a/lib/dao.js
+++ b/lib/dao.js
@@ -257,6 +257,16 @@ DataAccessObject.create = function (data, options, cb) {
 
     data = obj.toObject(true);
 
+    // options has precedence on model-setting
+    if (options.validate === false) {
+      return create();
+    }
+
+    // only when options.validate is not set, take model-setting into consideration
+    if (options.validate === undefined && Model.settings.automaticValidation === false) {
+      return create();
+    }
+    
     // validation required
     obj.isValid(function (valid) {
       if (valid) {
@@ -457,12 +467,25 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data
 
         var connector = self.getConnector();
 
-        if (Model.settings.validateUpsert === false) {
+        var doValidate = undefined;
+        if (options.validate === undefined) {
+          if (Model.settings.validateUpsert === undefined) {
+            if (Model.settings.automaticValidation !== undefined) {
+              doValidate = Model.settings.automaticValidation;
+            }
+          } else {
+            doValidate = Model.settings.validateUpsert
+          }
+        } else {
+          doValidate = options.validate;
+        }
+
+        if (doValidate === false) {
           callConnector();
         } else {
           inst.isValid(function(valid) {
             if (!valid) {
-              if (Model.settings.validateUpsert) {
+              if (doValidate) { // backwards compatibility with validateUpsert:undefined
                 return cb(new ValidationError(inst), inst);
               } else {
                 // TODO(bajtos) Remove validateUpsert:undefined in v3.0
@@ -732,6 +755,16 @@ DataAccessObject.findOrCreate = function findOrCreate(query, data, options, cb)
         var obj = ctx.instance;
         var data = obj.toObject(true);
 
+        // options has precedence on model-setting
+        if (options.validate === false) {
+          _findOrCreate(query, data, obj);
+        }
+
+        // only when options.validate is not set, take model-setting into consideration
+        if (options.validate === undefined && Model.settings.automaticValidation === false) {
+          _findOrCreate(query, data, obj);
+        }
+
         // validation required
         obj.isValid(function (valid) {
           if (valid) {
@@ -1876,8 +1909,13 @@ DataAccessObject.prototype.save = function (options, cb) {
   var hookState = {};
 
   if (options.validate === undefined) {
-    options.validate = true;
+    if (Model.settings.automaticValidation === undefined) {
+      options.validate = true;
+    } else {
+      options.validate = Model.settings.automaticValidation;
+    }
   }
+
   if (options.throws === undefined) {
     options.throws = false;
   }
@@ -2422,15 +2460,32 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, op
       data = removeUndefined(result);
     }
 
+    var doValidate = true;
+    if (options.validate === undefined) {
+      if (Model.settings.automaticValidation !== undefined) {
+        doValidate = Model.settings.automaticValidation;
+      }
+    } else {
+      doValidate = options.validate;
+    }
+
     // update instance's properties
     inst.setAttributes(data);
 
-    inst.isValid(function (valid) {
-      if (!valid) {
-        cb(new ValidationError(inst), inst);
-        return;
-      }
+    if (doValidate){
+      inst.isValid(function (valid) {
+        if (!valid) {
+          cb(new ValidationError(inst), inst);
+          return;
+        }
 
+        triggerSave();
+      }, data);
+    } else {
+        triggerSave();
+    }
+
+    function triggerSave(){
       inst.trigger('save', function (saveDone) {
         inst.trigger('update', function (done) {
           var typedData = {};
@@ -2506,7 +2561,7 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, op
           });
         }, data, cb);
       }, data, cb);
-    }, data);
+    }
   });
 return cb.promise;
 };
diff --git a/test/optional-validation.test.js b/test/optional-validation.test.js
new file mode 100644
index 00000000..ac432eb6
--- /dev/null
+++ b/test/optional-validation.test.js
@@ -0,0 +1,514 @@
+// This test written in mocha+should.js
+var should = require('./init.js');
+var db, User, options, whereCount = 0;
+var j = require('../');
+var ValidationError = j.ValidationError;
+
+var INITIAL_NAME = 'Bert';
+var NEW_NAME = 'Ernie';
+var INVALID_DATA = {name: null};
+var VALID_DATA = {name: INITIAL_NAME};
+
+describe('optional-validation', function () {
+
+  before(function (done) {
+    db = getSchema();
+    User = db.define('User', {
+      seq: {type: Number, index: true},
+      name: {type: String, index: true, sort: true},
+      email: {type: String, index: true},
+      birthday: {type: Date, index: true},
+      role: {type: String, index: true},
+      order: {type: Number, index: true, sort: true},
+      vip: {type: Boolean}
+    }, { forceId: true, strict: true });
+
+    db.automigrate(['User'], done);
+
+  });
+
+  beforeEach(function (done) {
+    User.destroyAll(function () {
+      delete User.validations;
+      User.validatesPresenceOf('name');
+      done();
+    });
+  });
+
+  function expectValidationError(done) {
+    return function (err, result) {
+      should.exist(err);
+      err.should.be.instanceOf(Error);
+      err.should.be.instanceOf(ValidationError);
+      done();
+    };
+  }
+
+  function expectCreateSuccess(data, done) {
+    if (done === undefined && typeof data === 'function') {
+      done = data;
+      data = { name: INITIAL_NAME };
+    }
+    return function(err, instance) {
+      if (err) return done(err);
+      instance.should.be.instanceOf(User);
+      if (data.name) {
+        instance.name.should.eql(data.name || INITIAL_NAME);
+      } else {
+        should.not.exist(instance.name);
+      }
+      done();
+    };
+  }
+
+  function expectChangeSuccess(data, done) {
+    if (done === undefined && typeof data === 'function') {
+      done = data;
+      data = { name: NEW_NAME };
+    }
+    return function(err, instance) {
+      if (err) return done(err);
+      instance.should.be.instanceOf(User);
+      if (data.name) {
+        instance.name.should.eql(data.name || NEW_NAME);
+      } else {
+        should.not.exist(instance.name);
+      }
+      done();
+    };
+  }
+
+  function createUserAndChangeName(name, cb) {
+    User.create(VALID_DATA, {validate: true}, function (err, d) {
+      d.name = name;
+      cb(err, d);
+    });
+  }
+
+  function createUser(cb) {
+    User.create(VALID_DATA, {validate: true}, cb);
+  }
+
+  function callUpdateOrCreateWithExistingUserId(name, options, cb){
+    User.create({'name': 'Groover'}, function(err, user){
+      if (err) return cb(err);
+      var data = {name: name};
+      data.id = user.id;
+      User.updateOrCreate(data, options, cb);
+    });
+  }
+
+  function getNewWhere() {
+    return {name: 'DoesNotExist' + (whereCount++)};
+  }
+
+  describe('no model setting', function () {
+
+    describe('method create', function() {
+      it('should throw on create with validate:true with invalid data', function (done) {
+        User.create(INVALID_DATA, {validate: true}, expectValidationError(done));
+      });
+
+      it('should NOT throw on create with validate:false with invalid data', function (done) {
+        User.create(INVALID_DATA, {validate: false}, expectCreateSuccess(INVALID_DATA, done));
+      });
+
+      it('should NOT throw on create with validate:true with valid data', function (done) {
+        User.create(VALID_DATA, {validate: true}, expectCreateSuccess(done));
+      });
+
+      it('should NOT throw on create with validate:false with valid data', function (done) {
+        User.create(VALID_DATA, {validate: false}, expectCreateSuccess(done));
+      });
+
+      it('should throw on create with invalid data', function (done) {
+        User.create(INVALID_DATA, expectValidationError(done));
+      });
+
+      it('should NOT throw on create with valid data', function (done) {
+        User.create(VALID_DATA, expectCreateSuccess(done));
+      });
+    });
+
+    describe('method findOrCreate', function() {
+      it('should throw on findOrCreate with validate:true with invalid data', function (done) {
+        User.findOrCreate(getNewWhere(), INVALID_DATA, {validate: true}, expectValidationError(done));
+      });
+
+      it('should NOT throw on findOrCreate with validate:false with invalid data', function (done) {
+        User.findOrCreate(getNewWhere(), INVALID_DATA, {validate: false}, expectCreateSuccess(INVALID_DATA, done));
+      });
+
+      it('should NOT throw on findOrCreate with validate:true with valid data', function (done) {
+        User.findOrCreate(getNewWhere(), VALID_DATA, {validate: true}, expectCreateSuccess(done));
+      });
+
+      it('should NOT throw on findOrCreate with validate:false with valid data', function (done) {
+        User.findOrCreate(getNewWhere(), VALID_DATA, {validate: false}, expectCreateSuccess(done));
+      });
+
+      it('should throw on findOrCreate with invalid data', function (done) {
+        User.findOrCreate(getNewWhere(), INVALID_DATA, expectValidationError(done));
+      });
+
+      it('should NOT throw on findOrCreate with valid data', function (done) {
+        User.findOrCreate(getNewWhere(), VALID_DATA, expectCreateSuccess(done));
+      });
+    });
+
+    describe('method updateOrCreate on existing data', function() {
+      it('should throw on updateOrCreate(id) with validate:true with invalid data', function (done) {
+        callUpdateOrCreateWithExistingUserId(null, {validate: true}, expectValidationError(done));
+      });
+
+      it('should NOT throw on updateOrCreate(id) with validate:false with invalid data', function (done) {
+        callUpdateOrCreateWithExistingUserId(null, {validate: false}, expectChangeSuccess(INVALID_DATA, done));
+      });
+
+      it('should NOT throw on updateOrCreate(id) with validate:true with valid data', function (done) {
+        callUpdateOrCreateWithExistingUserId(NEW_NAME, {validate: true}, expectChangeSuccess(done));
+      });
+
+      it('should NOT throw on updateOrCreate(id) with validate:false with valid data', function (done) {
+        callUpdateOrCreateWithExistingUserId(NEW_NAME, {validate: false}, expectChangeSuccess(done));
+      });
+
+      // backwards compatible with validateUpsert
+      it('should NOT throw on updateOrCreate(id) with invalid data', function (done) {
+        callUpdateOrCreateWithExistingUserId(null, expectChangeSuccess(INVALID_DATA, done));
+      });
+
+      it('should NOT throw on updateOrCreate(id) with valid data', function (done) {
+        callUpdateOrCreateWithExistingUserId(NEW_NAME, expectChangeSuccess(done));
+      });
+    });
+
+    describe('method save', function() {
+      it('should throw on save with {validate:true} with invalid data', function (done) {
+        createUserAndChangeName(null, function (err, d) {
+          d.save({validate: true}, expectValidationError(done));
+        });
+      });
+
+      it('should NOT throw on save with {validate:false} with invalid data', function (done) {
+        createUserAndChangeName(null, function (err, d) {
+          d.save({validate: false}, expectChangeSuccess(INVALID_DATA, done));
+        });
+      });
+
+      it('should NOT throw on save with {validate:true} with valid data', function (done) {
+        createUserAndChangeName(NEW_NAME, function (err, d) {
+          d.save({validate: true}, expectChangeSuccess(done));
+        });
+      });
+
+      it('should NOT throw on save with {validate:false} with valid data', function (done) {
+        createUserAndChangeName(NEW_NAME, function (err, d) {
+          d.save({validate: false}, expectChangeSuccess(done));
+        });
+      });
+
+      it('should throw on save(cb) with invalid data', function (done) {
+        createUserAndChangeName(null, function (err, d) {
+          d.save(expectValidationError(done));
+        });
+      });
+
+      it('should NOT throw on save(cb) with valid data', function (done) {
+        createUserAndChangeName(NEW_NAME, function (err, d) {
+          d.save(expectChangeSuccess(done));
+        });
+      });
+    });
+
+    describe('method updateAttributes', function() {
+      it('should throw on updateAttributes with {validate:true} with invalid data', function (done) {
+        createUser(function (err, d) {
+          d.updateAttributes(INVALID_DATA, {validate: true}, expectValidationError(done));
+        });
+      });
+
+      it('should NOT throw on updateAttributes with {validate:false} with invalid data', function (done) {
+        createUser(function (err, d) {
+          d.updateAttributes(INVALID_DATA, {validate: false}, expectChangeSuccess(INVALID_DATA, done));
+        });
+      });
+
+      it('should NOT throw on updateAttributes with {validate:true} with valid data', function (done) {
+        createUser(function (err, d) {
+          d.updateAttributes({'name': NEW_NAME}, {validate: true}, expectChangeSuccess(done));
+        });
+      });
+
+      it('should NOT throw on updateAttributes with {validate:false} with valid data', function (done) {
+        createUser(function (err, d) {
+          d.updateAttributes({'name': NEW_NAME}, {validate: false}, expectChangeSuccess(done));
+        });
+      });
+
+      it('should throw on updateAttributes(cb) with invalid data', function (done) {
+        createUser(function (err, d) {
+          d.updateAttributes(INVALID_DATA, expectValidationError(done));
+        });
+      });
+
+      it('should NOT throw on updateAttributes(cb) with valid data', function (done) {
+        createUser(function (err, d) {
+          d.updateAttributes({'name': NEW_NAME}, expectChangeSuccess(done));
+        });
+      });
+    });
+
+  });
+
+  describe('model setting: automaticValidation: false', function () {
+
+    before(function (done) {
+      User.settings.automaticValidation = false;
+      done();
+    });
+
+    describe('method create', function() {
+      it('should throw on create with validate:true with invalid data', function (done) {
+        User.create(INVALID_DATA, {validate: true}, expectValidationError(done));
+      });
+
+      it('should NOT throw on create with validate:false with invalid data', function (done) {
+        User.create(INVALID_DATA, {validate: false}, expectCreateSuccess(INVALID_DATA, done));
+      });
+
+      it('should NOT throw on create with validate:true with valid data', function (done) {
+        User.create(VALID_DATA, {validate: true}, expectCreateSuccess(done));
+      });
+
+      it('should NOT throw on create with validate:false with valid data', function (done) {
+        User.create(VALID_DATA, {validate: false}, expectCreateSuccess(done));
+      });
+
+      it('should NOT throw on create with invalid data', function (done) {
+        User.create(INVALID_DATA, expectCreateSuccess(INVALID_DATA, done));
+      });
+
+      it('should NOT throw on create with valid data', function (done) {
+        User.create(VALID_DATA, expectCreateSuccess(done));
+      });
+    });
+
+    describe('method findOrCreate', function() {
+      it('should throw on findOrCreate with validate:true with invalid data', function (done) {
+        User.findOrCreate(getNewWhere(), INVALID_DATA, {validate: true}, expectValidationError(done));
+      });
+
+      it('should NOT throw on findOrCreate with validate:false with invalid data', function (done) {
+        User.findOrCreate(getNewWhere(), INVALID_DATA, {validate: false}, expectCreateSuccess(INVALID_DATA, done));
+      });
+
+      it('should NOT throw on findOrCreate with validate:true with valid data', function (done) {
+        User.findOrCreate(getNewWhere(), VALID_DATA, {validate: true}, expectCreateSuccess(done));
+      });
+
+      it('should NOT throw on findOrCreate with validate:false with valid data', function (done) {
+        User.findOrCreate(getNewWhere(), VALID_DATA, {validate: false}, expectCreateSuccess(done));
+      });
+
+      it('should NOT throw on findOrCreate with invalid data', function (done) {
+        User.findOrCreate(getNewWhere(), INVALID_DATA, expectCreateSuccess(INVALID_DATA, done));
+      });
+
+      it('should NOT throw on findOrCreate with valid data', function (done) {
+        User.findOrCreate(getNewWhere(), VALID_DATA, expectCreateSuccess(done));
+      });
+    });
+
+    describe('method updateOrCreate on existing data', function() {
+      it('should throw on updateOrCreate(id) with validate:true with invalid data', function (done) {
+        callUpdateOrCreateWithExistingUserId(null, {validate: true}, expectValidationError(done));
+      });
+
+      it('should NOT throw on updateOrCreate(id) with validate:false with invalid data', function (done) {
+        callUpdateOrCreateWithExistingUserId(null, {validate: false}, expectChangeSuccess(INVALID_DATA, done));
+      });
+
+      it('should NOT throw on updateOrCreate(id) with validate:true with valid data', function (done) {
+        callUpdateOrCreateWithExistingUserId(NEW_NAME, {validate: true}, expectChangeSuccess(done));
+      });
+
+      it('should NOT throw on updateOrCreate(id) with validate:false with valid data', function (done) {
+        callUpdateOrCreateWithExistingUserId(NEW_NAME, {validate: false}, expectChangeSuccess(done));
+      });
+
+      it('should NOT throw on updateOrCreate(id) with invalid data', function (done) {
+        callUpdateOrCreateWithExistingUserId(null, expectChangeSuccess(INVALID_DATA, done));
+      });
+
+      it('should NOT throw on updateOrCreate(id) with valid data', function (done) {
+        callUpdateOrCreateWithExistingUserId(NEW_NAME, expectChangeSuccess(done));
+      });
+    });
+
+    describe('method save', function() {
+      it('should throw on save with {validate:true} with invalid data', function (done) {
+        createUserAndChangeName(null, function (err, d) {
+          d.save({validate: true}, expectValidationError(done));
+        });
+      });
+
+      it('should NOT throw on save with {validate:false} with invalid data', function (done) {
+        createUserAndChangeName(null, function (err, d) {
+          d.save({validate: false}, expectChangeSuccess(INVALID_DATA, done));
+        });
+      });
+
+      it('should NOT throw on save with {validate:true} with valid data', function (done) {
+        createUserAndChangeName(NEW_NAME, function (err, d) {
+          d.save({validate: true}, expectChangeSuccess(done));
+        });
+      });
+
+      it('should NOT throw on save with {validate:false} with valid data', function (done) {
+        createUserAndChangeName(NEW_NAME, function (err, d) {
+          d.save({validate: false}, expectChangeSuccess(done));
+        });
+      });
+
+      it('should NOT throw on save(cb) with invalid data', function (done) {
+        createUserAndChangeName(null, function (err, d) {
+          d.save(expectChangeSuccess(INVALID_DATA, done));
+        });
+      });
+
+      it('should NOT throw on save(cb) with valid data', function (done) {
+        createUserAndChangeName(NEW_NAME, function (err, d) {
+          d.save(expectChangeSuccess(done));
+        });
+      });
+    });
+
+  });
+
+  describe('model setting: automaticValidation: true', function () {
+
+    before(function (done) {
+      User.settings.automaticValidation = true;
+      done();
+    });
+
+    describe('method create', function() {
+      it('should throw on create with validate:true with invalid data', function (done) {
+        User.create(INVALID_DATA, {validate: true}, expectValidationError(done));
+      });
+
+      it('should NOT throw on create with validate:false with invalid data', function (done) {
+        User.create(INVALID_DATA, {validate: false}, expectCreateSuccess(INVALID_DATA, done));
+      });
+
+      it('should NOT throw on create with validate:true with valid data', function (done) {
+        User.create(VALID_DATA, {validate: true}, expectCreateSuccess(done));
+      });
+
+      it('should NOT throw on create with validate:false with valid data', function (done) {
+        User.create(VALID_DATA, {validate: false}, expectCreateSuccess(done));
+      });
+
+      it('should throw on create with invalid data', function (done) {
+        User.create(INVALID_DATA, expectValidationError(done));
+      });
+
+      it('should NOT throw on create with valid data', function (done) {
+        User.create(VALID_DATA, expectCreateSuccess(done));
+      });
+    });
+
+    describe('method findOrCreate', function() {
+      it('should throw on findOrCreate with validate:true with invalid data', function (done) {
+        User.findOrCreate(getNewWhere(), INVALID_DATA, {validate: true}, expectValidationError(done));
+      });
+
+      it('should NOT throw on findOrCreate with validate:false with invalid data', function (done) {
+        User.findOrCreate(getNewWhere(), INVALID_DATA, {validate: false}, expectCreateSuccess(INVALID_DATA, done));
+      });
+
+      it('should NOT throw on findOrCreate with validate:true with valid data', function (done) {
+        User.findOrCreate(getNewWhere(), VALID_DATA, {validate: true}, expectCreateSuccess(done));
+      });
+
+      it('should NOT throw on findOrCreate with validate:false with valid data', function (done) {
+        User.findOrCreate(getNewWhere(), VALID_DATA, {validate: false}, expectCreateSuccess(done));
+      });
+
+      it('should throw on findOrCreate with invalid data', function (done) {
+        User.findOrCreate(getNewWhere(), INVALID_DATA, expectValidationError(done));
+      });
+
+      it('should NOT throw on findOrCreate with valid data', function (done) {
+        User.findOrCreate(getNewWhere(), VALID_DATA, expectCreateSuccess(done));
+      });
+    });
+
+    describe('method updateOrCreate on existing data', function() {
+      it('should throw on updateOrCreate(id) with validate:true with invalid data', function (done) {
+        callUpdateOrCreateWithExistingUserId(null, {validate: true}, expectValidationError(done));
+      });
+
+      it('should NOT throw on updateOrCreate(id) with validate:false with invalid data', function (done) {
+        callUpdateOrCreateWithExistingUserId(null, {validate: false}, expectChangeSuccess(INVALID_DATA, done));
+      });
+
+      it('should NOT throw on updateOrCreate(id) with validate:true with valid data', function (done) {
+        callUpdateOrCreateWithExistingUserId(NEW_NAME, {validate: true}, expectChangeSuccess(done));
+      });
+
+      it('should NOT throw on updateOrCreate(id) with validate:false with valid data', function (done) {
+        callUpdateOrCreateWithExistingUserId(NEW_NAME, {validate: false}, expectChangeSuccess(done));
+      });
+
+      it('should throw on updateOrCreate(id) with invalid data', function (done) {
+        callUpdateOrCreateWithExistingUserId(null, expectValidationError(done));
+      });
+
+      it('should NOT throw on updateOrCreate(id) with valid data', function (done) {
+        callUpdateOrCreateWithExistingUserId(NEW_NAME, expectChangeSuccess(done));
+      });
+    });
+
+    describe('method save', function() {
+      it('should throw on save with {validate:true} with invalid data', function (done) {
+        createUserAndChangeName(null, function (err, d) {
+          d.save(options, expectValidationError(done));
+        });
+      });
+
+      it('should NOT throw on save with {validate:false} with invalid data', function (done) {
+        createUserAndChangeName(null, function (err, d) {
+          d.save({validate: false}, expectChangeSuccess(INVALID_DATA, done));
+        });
+      });
+
+      it('should NOT throw on save with {validate:true} with valid data', function (done) {
+        createUserAndChangeName(NEW_NAME, function (err, d) {
+          d.save({validate: true}, expectChangeSuccess(done));
+        });
+      });
+
+      it('should NOT throw on save with {validate:false} with valid data', function (done) {
+        createUserAndChangeName(NEW_NAME, function (err, d) {
+          d.save({validate: false}, expectChangeSuccess(done));
+        });
+      });
+
+      it('should throw on save(cb) with invalid data', function (done) {
+        createUserAndChangeName(null, function (err, d) {
+          d.save(expectValidationError(done));
+        });
+      });
+
+      it('should NOT throw on save(cb) with valid data', function (done) {
+        createUserAndChangeName(NEW_NAME, function (err, d) {
+          d.save(expectChangeSuccess(done));
+        });
+      });
+    });
+
+  });
+
+});