Skip to content

Business and Validation Rules

Aaron Hanusa edited this page Jun 4, 2016 · 61 revisions

When a command is invoked, its arguments are subjected to business and validation rules. These rules are generally considered to be rules that govern changing the state of data.

Business and validation rules cover a wide array of use cases. For example, you may want a business rule that states that new customers have to be at least 18 years old, or you may want to ensure that an item can't be deleted from a data store if it has been shipped.

You may also want authorization rules to allow or prohibit access to particular service commands. For example, administrators might be the only users that can insert or update product information.

And of course, don't forget common validation rules, which often check for the presence of data, the length of fields, correct data types, etc.

The peasy-js rules engine is at the heart of the peasy-js framework, and offers an efficient and reusable way to create rules that are easily maintainable and testable. Rules themselves are easy to create, reuse, consume, and are flexible in that they cover many common use cases.

Creating a rule

To create a rule:

  • Import or reference peasy-js.Rule from peasy.js.
  • Create a constructor function by invoking Rule.extend(), and supply an object with the following configuration:
    • association (optional) - a string that associates and instance of the rule with a field. This is helpful for validation errors.
    • params (optional) - represents an array of strings of the arguments expected to be passed to the constructor of a rule.
    • functions - an object containing the functions below
      • _onValidate (required) - represents the function that will be invoked by the rules engine.
        • Invoke _invalidate() if the rule does not pass.
        • Invoke the supplied done() method (required).

Here is a sample rule implementation. The ValidCityVerificationRule simply checks that the supplied city argument is 'New York', 'Rome', 'Paris', 'London', or 'Tokyo'.

ValidCityVerificationRule
var ValidCityVerificationRule = Rule.extend({
  association: "city",
  params: ['city'],
  functions: {
    _onValidate: function(done) {
      var validCities = ['New York', 'Rome', 'Paris', 'London', 'Tokyo'];
      if (validCities.indexOf(this.city) === -1) {
        this._invalidate("The city specified is invalid.");
      }
      done();
    }
  }
});

If classical inheritance is your thing:

ES5
var ValidCityVerificationRule = function(city) {
  Rule.call(this, { association: "city "});
  this.city = city;
};

ValidCityVerificationRule.prototype = new Rule();
ValidCityVerificationRule.prototype._onValidate = function(done) {
  var validCities = ['New York', 'Rome', 'Paris', 'London', 'Tokyo'];
  if (validCities.indexOf(this.city) === -1) {
    this._invalidate("The city specified is invalid.");
  }
  done();
};
ES6
class ValidCityVerificationRule extends Rule {
  constructor(city) {
    super();
    this.association = "city";
    this.city = city;
  }
  
  _onValidate(done) {
    var validCities = ['New York', 'Rome', 'Paris', 'London', 'Tokyo'];
    if (validCities.indexOf(this.city) === -1) {
      this._invalidate("The city specified is invalid.");
    }
    done(); 
  }
}

Public functions (instance)

validate(callback)

Asynchronously executes the _onValidate() function, resulting in the rule valid member being set to a boolean value.

Public functions (static)

extend(callback)

Accepts an object containing the members outlined below and returns a rule implementation function:

  • association (optional) - a string that associates and instance of the rule with a field. This is helpful for validation errors.
  • params (optional) - represents an array of strings of the arguments expected to be passed to the constructor of a rule.
  • functions - an object containing the functions below
    • _onValidate (required) - represents the function that will be invoked by the rules engine.
      • Invoke _invalidate() if the rule does not pass.
      • Invoke the supplied done() method (required).

You can view how to use Command.extend() here.

Testing a business rule

Here is how we can easily test the ValidCityVerificationRule:

var rule = new ValidCityVerificationRule('Neeww Yorck');
rule.validate(function() {
  expect(rule.errors.length).toEqual(1);
  var error = rule.errors[0];
  expect(error.association).toEqual("city");
  expect(error.error).toEqual("The city specified is invalid.");
});

var rule = new ValidCityVerificationRule('New York');
rule.validate(function() {
  expect(rule.errors.length).toEqual(0);
});

Wiring up business rules

Information regarding wiring up business rules can be found here, here, and here.

Passing lookup data to a business rule

Rules can be passed any form of data from any data source imaginable. Often times, you'll want the rule itself to be responsible for obtaining data that it will use to determine validity.

Here's an example:

var Rule = require('peasy-js').Rule;

var CanDeleteOrderItemRule = Rule.extend({
  params: ['itemId', 'orderItemDataProxy'],
  functions: {
    _onValidate: function(done) {
      var self = this;
      this.orderItemDataProxy.getById(this.itemId, function(err, result) {
        if (result.status === 'Shipped') {
          self._invalidate("This item has been shipped and cannot be deleted");
        }
        done();
      });
    }
  }
});

module.exports = CanDeleteOrderItemRule;

Testing the rule ...

var proxy = {
  getById: function(id, done) {
    done(null, { status: 'Shipped'});
  }
};

var rule = new CanDeleteOrderItemRule(1, proxy);
rule.validate(function() {
  expect(rule.errors.length).toEqual(1);
  var error = rule.errors[0];
  expect(error.error).toEqual("This item has been shipped and cannot be deleted");
});

Chaining business rules

Business rule execution can be expensive, especially if a rule requires data from a data source which could result in a hit to a database or a call to a an external service, such as an HTTP or SOAP service. To help circumvent potentially expensive data retrievals, peasy-js.Rule exposes IfValidThenValidate(), which accepts a single or an array of peasy-js.Rule, and will only be validated in the event that the parent rule's validation is successful.

Let's take a look at an example:

function getRulesForInsert(data, context, done) {
  done(new SomeRule(data)
             .ifValidThenValidate(new ExpensiveRule(data, this.someDataProxy)));
}

In this example, we create a parent rule SomeRule and specify that upon successful validation, it should validate ExpensiveRule, who requires a data proxy and will most likely perform a method invocation to retrieve data for validation.

It's important to note that the error message of a parent rule will be set to it's child rule should it's child fail validation.

Let's look at another example and introduce another rule that's really expensive to validate, as it requires getting data from two data proxies.

function getRulesForInsert(data, context, done) {
  done(new SomeRule(data)
             .ifValidThenValidate([
               new ExpensiveRule(data, this.someDataProxy),
               new TerriblyExpensiveRule(data, this.anotherDataProxy, this.yetAnotherDataProxy)
             ])
      );
}

In this example, both ExpensiveRule and TerriblyExpensiveRule will only be validated upon successful validation of SomeRule. But what if we only wanted each rule to be validated upon successful validation of its predecessor?

Here's how that would look:

function getRulesForInsert(data, context, done) {
  done(new someRule(data)
             .ifValidThenValidate(new ExpensiveRule(data, this.someDataProxy)
                                        .ifValidThenValidate(new TerriblyExpensiveRule(data, this.someDataProxy, this.anotherDataProxy))));
}

or alternatively ...

function getRulesForInsert(data, context, done) {
  var rule = new SomeRule(data);
  var expensiveRule = new ExpensiveRule(data, this.someDataProxy);
  var terriblyExpense = new TerriblyExpensiveRule(data, this.someDataProxy, this.anotherDataProxy);
  rule.ifValidThenValidate(expensiveRule);
  expensiveRule.ifValidThenValidate(terriblyExpense);
  done([rule]);
}

Executing code on failed validation of a business rule

Sometimes you might want to execute some logic based on the failed validation of a business rule.

Here's how that might look:

function getRulesForInsert(data, context, done) {
  done(new SomeRule(data).IfInvalidThenExecute(function(rule) {
    this.logger.logError(rule.errors);
  }));
}

Executing code on successful validation of a business rule

Sometimes you might want to execute some logic based on the successful validation of a business rule.

Here's how that might look:

function getRulesForInsert(data, context, done) {
  done(new SomeRule(data).IfValidThenExecute(function(rule) {
    this.logger.logSuccess("Your success details");
  }));
}
Clone this wiki locally