Skip to content

Business and Validation Rules

ahanusa edited this page Jan 28, 2019 · 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 verifies that new customers are 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

ValidCityVerificationRule

Let's create a rule to verifiy that the supplied city argument is 'New York', 'Rome', 'Paris', 'London', or 'Tokyo'.

To create a rule:

  • Import or reference peasy-js.Rule from peasy.js.

  • If using ES6 or TypeScript:

    • Inherit from Rule.
    • Provide an implementation for _onValidate (required) - represents the function that will be invoked by the rules engine. _onValidate must return a promise.
    ES6
    class ValidCityRule extends Rule {
    
      constructor(private city: string) {
        super();
        this.city = city;
        this.association = "city"; // optional
      }
    
      _onValidate() {
        var validCities = ['New York', 'Rome', 'Paris', 'London', 'Tokyo'];
        if (validCities.indexOf(this.city) === -1) {
          this._invalidate("The city specified is invalid.");
        }
        return Promise.resolve();
      }
    }
    TypeScript
    class ValidCityRule extends Rule {
    
      constructor(private city: string) {
        super();
        this.association = "city"; // optional
      }
    
      protected _onValidate(): Promise<void> {
        var validCities = ['New York', 'Rome', 'Paris', 'London', 'Tokyo'];
        if (validCities.indexOf(this.city) === -1) {
          this._invalidate("The city specified is invalid.");
        }
        return Promise.resolve();
      }
    }
  • OR, if using the peasy-js constructor creation API, create a constructor function by invoking Rule.extend(), and supply an object with the following configuration:

    • association (optional) - a string that associates an instance of the rule with a field. This is helpful for validation errors.
    • functions - an object containing the functions below
      • _onValidate (required) - represents the function that will be invoked by the rules engine. _onValidate must return a promise or accept a completion callback.
    peasy-js constructor creation API
    var ValidCityVerificationRule = Rule.extend({
      association: "city",
      functions: {
        _onValidate: function(city) {
          var validCities = ['New York', 'Rome', 'Paris', 'London', 'Tokyo'];
          if (validCities.indexOf(city) === -1) {
            this._invalidate("The city specified is invalid.");
          }
          return Promise.resolve();
        }
      }
    });

Testing a rule

Here is how we can easily test the ValidCityVerificationRule:

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

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

Public functions (instance)

validate()

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

Public functions (static)

extend()

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

Note: This function is meant for use using the peasy-js constructor creation API. This method can safely be ignored if using ES6 or TypeScript class inheritance.

  • 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.

Viewing the ValidCityVerificationRule showcases how to consume Rule.extend().

Passing a data source to a 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:

ES6
class CanDeleteOrderItemRule extends Rule {
  constructor(itemId, orderItemDataProxy) {
    super();
    this.itemId = itemId;
    this.orderItemDataProxy = orderItemDataProxy;
  }
  _onValidate() {
    return this.orderItemDataProxy.getById(this.itemId).then(item => {
      if (item.status === 'Shipped') {
        this._invalidate("This item has been shipped and cannot be deleted");
      }
    });
  }
}
TypeScript
class CanDeleteOrderItemRule extends Rule {

  constructor(private itemId: number, private orderItemDataProxy: IDataProxy<OrderItem, number>) {
    super();
  }

  protected async _onValidate(): Promise<void> {
    const item = await this.orderItemDataProxy.getById(this.itemId);
    if (item.status === 'Shipped') {
      this._invalidate("This item has been shipped and cannot be deleted");
    }
  }
peasy-js constructor creation API
var CanDeleteOrderItemRule = Rule.extend({
  functions: {
    _onValidate: function(itemId, orderItemDataProxy) {
      return orderItemDataProxy.getById(this.itemId).then(item => {
        if (item.status === 'Shipped') {
          this._invalidate("This item has been shipped and cannot be deleted");
        }
      });
    }
  }
});

Testing the rule ...

var proxy = {
  getById: function(id) {
    return Promise.resolve({ status: 'Shipped'});
  }
};
var rule = new CanDeleteOrderItemRule(1, proxy);

rule.validate().then(() => {
  expect(rule.errors.length).toEqual(1);
  var error = rule.errors[0];
  expect(error.message).toEqual("This item has been shipped and cannot be deleted");
});

Chaining 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) {

  var rule = new SomeRule(data)
    .ifValidThenValidate(new ExpensiveRule(data, this.someDataProxy));

  return Promise.resolve(rule);
}

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) {
  return Promise.resolve(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) {
  var rule = new someRule(data)
    .ifValidThenValidate(
      new ExpensiveRule(data, this.someDataProxy)
        .ifValidThenValidate(
          new TerriblyExpensiveRule(data, this.someDataProxy, this.anotherDataProxy)
        )
    );

  return Promise.resolve(rule);
}

or alternatively ...

function getRulesForInsert(data, context) {
  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);
  return Promise.resolve(rule);
}

Executing code on failed validation of a 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) {
  return Promise.resolve(new SomeRule(data).IfInvalidThenExecute(rule => {
    this.logger.logError(rule.errors);
  }));
}

Executing code on successful validation of a 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) {
  return Promise.resolve(new SomeRule(data).IfValidThenExecute(rule => {
    this.logger.logSuccess("Your success details");
  }));
}