-
Notifications
You must be signed in to change notification settings - Fork 10
Business and Validation Rules
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
- Public functions
- Testing a rule
- Passing a data source to a rule
- Chaining rules
- Executing code on failed validation of a rule
- Executing code on successful validation of 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.
Here is a sample rule implementation. The ValidCityVerificationRule simply checks that the supplied city argument is 'New York', 'Rome', 'Paris', 'London', or 'Tokyo'.
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:
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();
};
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();
}
}
Asynchronously executes the _onValidate()
function, resulting in rule.valid
being set to a boolean value.
Accepts an object containing the members outlined below and returns a rule constructor 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.
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.message).toEqual("The city specified is invalid.");
});
var rule = new ValidCityVerificationRule('New York');
rule.validate(function() {
expect(rule.errors.length).toEqual(0);
});
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, item) {
if (item.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.message).toEqual("This item has been shipped and cannot be deleted");
});
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]);
}
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);
}));
}
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");
}));
}