-
Notifications
You must be signed in to change notification settings - Fork 10
BusinessService
BusinessService is the main actor within the peasy-js framework. A concrete implementation becomes what is called a business service class, and exposes CRUD and other command methods (defined by you).
BusinessService is responsible for exposing [commands] (https://github.com/peasy/peasy-js/wiki/Command) that subject data proxy operations (and other logic) to business and validation rules rules via the command execution pipeline before execution.
The commands returned by the methods can be asynchronously consumed by multiple clients. You can think of a an implementation of BusinessService as a CRUD command factory.
- Sample consumption scenario
- Creating a business service
- Public functions
- Wiring up rules
- Wiring up multiple rules
- Wiring up rules that consume data proxy data
- Providing initialization logic
- Overriding default command logic
- Exposing new command functions
var service = new CustomerService(new CustomerHttpDataProxy());
var customer = { name: "Frank Zappa" };
var command = service.insertCommand(customer);
command.execute(function(err, result) {
if (result.success) {
customer = result.value;
} else {
console.log(result.errors);
}
});
In the above example, an instance of a customer service is created, supplied with a required data proxy.
Next, an insert command is created that is responsible for performing an insert against the supplied data proxy in the event of successful rule(s) execution.
Finally, the command is executed via the Command.execute()
method, which requires a callback function that accepts the err
and result
parameters. err
represents handled exceptions, whereas result
represents the result of the execution pipeline, and may contain errors (not exceptions) that were produced as a result of validation rule executions.
To create a business service:
- Import or reference peasy-js.BusinessService from peasy.js.
- Create a constructor function by invoking BusinessService.
extend()
, and supply an object containing the following configuration:- params (optional) - represents an array of strings representing the named arguments expected to be passed to the constructor of a service.
- functions (optional) - represents an object containing the BusinessService functions to override, and a function implementation associated with each override. Here is the general format:
{
params: ['id'],
functions: {
_getRulesForInsert: function(person, context, done) {},
_onInsertCommandInitialization: function(context, done) {}
}
}
Here is a sample service implementation:
var CustomerService = BusinessService.extend({ params: ['dataProxy'] }).service;
If classical inheritance is your thing:
var CustomerService = function(dataProxy) {
BusinessService.call(this, dataProxy);
}
CustomerService.prototype = new BusinessService();
class CustomerService extends BusinessService {
constructor(dataProxy) {
super(dataProxy);
}
}
Accepts the id of the entity that you want to query and returns a command. The returned command from getByIdCommand
invokes the following methods of BusinessService upon execution in the order specified below:
- _onGetByIdCommandInitialization for custom initialization logic
- _getRulesForGetById for business and validation rule retrieval
- _getById on successful validation of rules.
The command subjects the supplied id to business and validation rules (if any) before marshaling the call to the injected data proxy's getById function.
Returns a command that delivers all values from a data source and is especially useful for lookup data. The returned command from getAllCommand
invokes the following methods of BusinessService upon execution in the order specified below:
- _onGetAllCommandInitialization for custom initialization logic
- _getRulesForGetAll for business and validation rule retrieval
- _getAll on successful validation of rules.
The command invokes business and validation rules (if any) before marshaling the call to the injected data proxy's getAll function.
Accepts an object that you want inserted into a data store and returns a command. The returned command from insertCommand
invokes the following methods of BusinessService upon execution in the order specified below:
- _onInsertCommandInitialization for custom initialization logic
- _getRulesForInsert for business and validation rule retrieval
- _insert on successful validation of rules.
The command subjects the supplied object to business and validation rules (if any) before marshaling the call to the injected data proxy's insert function.
Accepts an object that you want updated within a data store and returns a command. The returned command from updateCommand
invokes the following methods of BusinessService upon execution in the order specified below:
- _onUpdateCommandInitialization for custom initialization logic
- _getRulesForUpdate for business and validation rule retrieval
- _update on successful validation of rules.
The command subjects the supplied object to business and validation rules (if any) before marshaling the call to the injected data proxy's update function.
Accepts the id of the entity that you want to delete from the data store and returns a command. The returned command from destroyCommand
invokes the following methods of BusinessService upon execution in the order specified below:
- _onDestroyCommandInitialization for custom initialization logic
- _getRulesForDestroy for business and validation rule retrieval
- _destroy on successful validation of rules.
The command subjects the supplied id to business and validation rules (if any) before marshaling the call to the injected data proxy's destroy function.
Accepts an object containing the members outlined below and returns an object containing the service implementation as well as a createCommand function:
- params (optional) represents an array of strings representing the arguments expected to be passed to the constructor of a business service.
- functions (optional) an object containing the function implementations for BusinessBase that you choose to supply functionality for.
var result = BusinessService.extend({
params: ['dataProxy'],
functions: {
_getRulesForInsert: function(data, context, done) {
done(new SomeRuleForInsertValidation(data));
},
_getRulesForInsert: function(data, context, done) {
done(new SomeRuleForUpdateValidation(data, this.dataProxy));
}
}
});
console.log(result); // prints -> { createCommand: [Function], service: [Function] }
In the above example, an invocation of BusinessService.extend()
accepts an object containing params
and functions
.
Notice that the result from the invocation results in an object containing the following functions:
- createCommand() - allows you to easily extend your business services by dynamically exposing custom command functions on your behalf in a chainable fashion.
- service() - the actual business service implementation (constructor function).
Below illustrates consuming the created service by instantiating it and supplying it with the personDataProxy. Because a param of dataProxy
was specified in the params array of the BusinessService.extend()
function, the personDataProxy will be accessible from within your function implementations via the dataProxy member of the business service instance (this.dataProxy
):
var personDataProxy = new PersonDataProxy();
var PersonService = result.service;
var personService = new PersonService(personDataProxy);
var command = personService.insertCommand({name: "Django Reinhardt"});
command.execute(function...
Creates a command function that is exposed via the supplied business service. Accepts an object containing the following members:
- name (required) specifies the name of the command function that will be exposed from the business service.
- functions (optional) an object literal containing the function implementations for command hooks.
- service (required) represents a constructor function that will house the dynamically created command function.
- params (optional) represents the names of the parameters which are expected to be received as arguments to the invocation of the dynamically created command.
Below illustrates exposing a command from a BusinessService by invoking the BusinessService.createCommand()
function.
var CustomerService = BusinessService.extend().service;
BusinessService.createCommand({
name: 'testCommand',
service: CustomerService,
functions: {
onInitialization: function(context, done) {
context.testValue = "4";
done();
},
getRules: function(context, done) {
context.testValue += "2";
done([]);
},
onValidationSuccess: function(context, done) {
done(null, context.testValue + this.incoming);
}
},
params: ['incoming']
});
Here is how you might consume the testCommand function exposed from as a result of the above BusinessService.createCommand()
invocation.
var dataProxy = new CustomerFileProxy();
var service = new CustomerService(dataProxy);
var command = personService.testCommand("!");
command.execute(function(err, result) {
console.log(result.value); // prints "42!"
});
BusinessService exposes commands for invoking create, retrieve, update, and delete (CRUD) operations against its injected data proxies. These operations ensure that all validation and business rules are valid before marshaling the call to their respective data proxy CRUD operations.
For example, we may want to ensure that new customers and existing customers are subjected to a city verification check before successfully persisting them into our data store.
Let's consume the ValidCityVerificationRule, here's how that looks:
var CustomerService = BusinessService.extend({
functions: {
_getRulesForInsert: function(person, context, done) {
done(new ValidCityRule(person.city));
}
},{
_getRulesForUpdate: function(person, context, done) {
done(new ValidCityRule(person.city));
}
}
}).service;
If classical inheritance is your thing:
var CustomerService = function(dataProxy) {
BusinessService.call(this, dataProxy);
}
CustomerService.prototype = new BusinessService();
CustomerService.prototype._getRulesForInsert = function(person, context, done) {
done(new ValidCityRule(person.city));
}
CustomerService.prototype._getRulesForUpdate = function(person, context, done) {
done(new ValidCityRule(person.city));
}
class CustomerService extends BusinessService {
constructor(dataProxy) {
super(dataProxy);
}
_getRulesForInsert(person, context, done) {
done(new ValidCityRule(person.city));
}
_getRulesForUpdate(person, context, done) {
done(new ValidCityRule(person.city));
}
}
In the following example, we simply provide implementations for the _getRulesForInsert()
and _getRulesForUpdate()
functions and provide the rule that we want to pass validation before marshaling the call to the data proxy.
What we've essentially done is inject business rules into the command execution pipeline, providing clarity as to what business rules are executed for each type of CRUD operation.
There's really not much difference between returning one or multiple business rules. Simply construct an array
of rules to be validated and return it.
Here's how that looks:
var CustomerService = BusinessService.extend({
functions: {
_getRulesForInsert: function(person, context, done) {
done([
new ValidCityRule(person.city),
new PersonNameRule(person.name)
]);
}
},{
_getRulesForUpdate: function(person, context, done) {
done([
new ValidCityRule(person.city),
new PersonNameRule(person.name)
]);
}
}
}).service;
If classical inheritance is your thing:
var CustomerService = function(dataProxy) {
BusinessService.call(this, dataProxy);
}
CustomerService.prototype = new BusinessService();
CustomerService.prototype._getRulesForInsert = function(person, context, done) {
done([
new ValidCityRule(person.city),
new PersonNameRule(person.name)
]);
}
CustomerService.prototype._getRulesForUpdate = function(person, context, done) {
done([
new ValidCityRule(person.city),
new PersonNameRule(person.name)
]);
}
class CustomerService extends BusinessService {
constructor(dataProxy) {
super(dataProxy);
}
_getRulesForInsert(person, context, done) {
done([
new ValidCityRule(person.city),
new PersonNameRule(person.name)
]);
}
_getRulesForUpdate(person, context, done) {
done([
new ValidCityRule(person.city),
new PersonNameRule(person.name)
]);
}
}
Sometimes rules require data from data proxies for validation.
Here's how that might look:
var CustomerService = BusinessService.extend({
functions: {
_getRulesForUpdate: function(person, context, done) {
this.dataProxy.getById(person.id, function(err, person) {
done([
new SomeRule(person),
new AnotherRule(person)
]);
});
}
}
}).service;
If classical inheritance is your thing:
var CustomerService = function(dataProxy) {
BusinessService.call(this, dataProxy);
}
CustomerService.prototype = new BusinessService();
CustomerService.prototype._getRulesForUpdate = function(person, context, done) {
this.dataProxy.getById(person.id, function(err, person) {
done([
new SomeRule(person),
new AnotherRule(person)
]);
});
}
class CustomerService extends BusinessService {
constructor(dataProxy) {
super(dataProxy);
}
_getRulesForUpdate(person, context, done) {
this.dataProxy.getById(person.id, (err, person) => {
done([
new SomeRule(person),
new AnotherRule(person)
])
});
}
}
Initialization logic can be helpful when you need to initialize your data with required values before it is subjected to rule validations, remove(delete) non-updatable fields, or to provide other cross-cutting concerns.
Within the command execution pipeline, you have the opportunity to inject initialization logic that occurs before business and validation rules are executed.
Below are examples that inject initialization behavior into the command execution pipeline of the command returned by BusinessService.insertCommand() and BusinessService.updateCommand() in an OrderItemService, respectively.
var OrderItemService = BusinessService.extend({
functions: {
_onInsertCommandInitialization: function(orderItem, context, done) {
orderItem.status = STATUS.pending;
orderItem.createdDate = new Date();
done();
}
}
}).service;
In this example we provide an implementation for BusinessService._onInsertCommandInitialization()
and set some default values to satisfy required fields that may not have been set by the consumer of the application.
var OrderItemService = BusinessService.extend({
functions: {
_onUpdateCommandInitialization: function(orderItem, context, done) {
var allowableFields = ['id', 'quantity'];
Object.keys(orderItem).forEach(function(field) {
if (allowableFields.indexOf(field) === -1) {
delete orderItem[field];
}
});
done();
}
}
}).service;
In this example we provide an implementation for BusinessService._onUpdateCommandInitialization
and remove any fields that exist on the supplied orderItem that don't belong to the whitelist.
By default, all service command functions of a default implementation of BusinessService are wired up to invoke data proxy functions. There will be times when you need to invoke extra command logic before and/or after execution occurs. For example, you might want to perform logging before and after communication with a data proxy during the command's execution to obtain performance metrics for your application.
Here is an example that allows you to achieve this behavior:
var CustomerService = BusinessService.extend({
functions: {
_destroy: function(id, context, done) {
var onComplete = function(err, result) {
done(err, result);
console.log("DELETE COMPLETE");
}
console.log("DELETING...");
this.dataProxy.destroy(id, onComplete);
}
}
}).service;
If classical inheritance is your thing:
var CustomerService = function(dataProxy) {
BusinessService.call(this, dataProxy);
}
CustomerService.prototype = new BusinessService();
CustomerService.prototype._destroy = function(id, context, done) {
var onComplete = function(err, result) {
done(err, result);
console.log("DELETE COMPLETE");
}
console.log("DELETING...");
this.dataProxy.destroy(id, onComplete);
};
class CustomerService extends BusinessService {
constructor(dataProxy) {
super(dataProxy);
}
_destroy(id, context, done) {
var onComplete = function(err, result) {
done(err, result);
console.log("DELETE COMPLETE");
}
console.log("DELETING...");
this.dataProxy.destroy(id, onComplete);
}
}
There will be cases when you want to create new command function in addition to the default command functions. For example, you might want your Orders Service to return all orders placed on a specific date. In this case, you could provide a getOrdersPlacedOnCommand(date)
method.
There will also be times when you want to disallow updates to certain fields on your data in updateCommand(), however, you still need to provide a way to update the field within a different context.
For example, let's suppose your orderItem
data exposes a status
field that you don't want updated via updateCommand
for security or special auditing purposes, but you still need to allow order items to progress through states (Pending, Submitted, Shipped, etc.)
Below is how you might expose a new service command function to expose this functionality from a business service:
var OrderItemService = BusinessService
.extend()
.createCommand({
name: 'submitCommand',
params: ['id'],
functions:
{
getRules: function(context, done) {
this.dataProxy.getById(this.id, function(err, item) {
done(new CanSubmitOrderItemRule(item));
});
},
onValidationSuccess: function(context, done) {
this.dataProxy.submit(this.id, function(err) {
done();
});
}
}
}).service;
Here we invoke BusinessService.createCommand()
exposed by the result of the BusinessService.extend()
function, which will create the command submitCommand
function exposed via the order item service on our behalf.
An important thing to note is that the this
references in the above function declarations are actually a reference to an instance of the business service that creates it, which gives you access to instance members of the hosting business service that you might need during command execution, such as data proxies and other instance state.
One final note is that we have wired up a business rule method for the submit command. This means that the call to datProxy.submit
will only occur if the validation result for CanSubmitOrderItemRule
is successful.
It should be noted that this approach of creating commands, while convenient, does not promote easy code reuse as do some of the other methods discussed in this section.
Below is how you can consume your command:
var orderItemService = new OrderItemService(new OrderItemDataProxy());
var command = orderItemService.submitCommand(123);
command.execute(function(err, result) {
// do something here
});
This example extends the createCommand() example by chaining createCommand() functions.
var OrderItemService = BusinessService
.extend()
.createCommand({
name: 'submitCommand',
params: ['id'],
functions:
{
getRules: function(context, done) {
this.dataProxy.getById(this.id, function(err, item) {
done(new CanSubmitOrderItemRule(item));
});
},
onValidationSuccess: function(context, done) {
this.dataProxy.submit(this.id, function(err) {
done();
});
}
}
})
.createCommand({
name: 'shipCommand',
params: ['id'],
functions:
{
getRules: function(context, done) {
this.dataProxy.getById(this.id, function(err, item) {
done(new CanShipOrderItemRule(item));
});
},
onValidationSuccess: function(context, done) {
this.dataProxy.ship(this.id, function(err) {
done();
});
}
}
}).service;
Below is how you can consume your commands:
var orderItemService = new OrderItemService(new OrderItemDataProxy());
var command = orderItemService.submitCommand(123);
command.execute(function(err, result) {
// do something here
});
command = orderItemService.shipCommand(123);
command.execute(function(err, result) {
// do something here
});
var OrderItemService = BusinessService.extend().service;
OrderItemService.prototype.submitCommand = function(id) {
var self = this;
return new Command({
getRules: function(context, done) {
self.dataProxy.getById(id, function(err, item) {
done(new CanSubmitOrderItemRule(item));
});
},
onValidationSuccess: function(context, done) {
self.dataProxy.submit(id, function(err) {
done();
});
}
});
}
In this example, we expose the submitCommand function from the OrderItemService prototype. We then take advantage of the Command constructor, in which we specify function implementations for getRules()
and onValidationSuccess()
.
Notice that within the submitCommand method, we capture a self reference that can later be consumed by the returned command, as the command methods require access to the business service's data proxy.
One final note is that we have wired up a business rule method for the submit command. This means that the call to dataProxy.submit()
will only occur if the validation result for CanSubmitOrderItemRule
is successful.
It should be noted that this approach of creating commands, while convenient, does not promote easy code reuse as do some of the other methods discussed in this section.
Below is how you can consume your command:
var orderItemService = new OrderItemService(new OrderItemDataProxy());
var command = orderItemService.submitCommand(123);
command.execute(function(err, result) {
// do something here
});
The following example illustrates creating a reusable command that can be defined elsewhere and easily consumed by multiple business services:
var SubmitCommand = Command.extend({
params: ['id', 'dataProxy'],
functions: {
_getRules: function(context, done) {
this.dataProxy.getById(this.id, function(err, item) {
done(new CanSubmitOrderItemRule(item));
});
},
_onValidationSuccess: function(context, done) {
this.dataProxy.submit(this.id, function(err) {
done();
});
}
}
});
var OrderItemService = BusinessService.extend().service;
OrderItemService.prototype.submitCommand = function(id) {
return new SubmitCommand(id, this.dataProxy);
};
In this example, we consume Command.extend() to create a reusable command.
It should be noted that the this
reference refers to the command instance, and any parameters passed to the instantiation of the command will be accessible from this
by name as specified in the params
array.
Below is how you can consume your command:
var orderItemService = new OrderItemService(new OrderItemDataProxy());
var command = orderItemService.submitCommand(123);
command.execute(function(err, result) {
// do something here
});
If classical inheritance is your thing:
var SubmitCommand = function(id, dataProxy) {
this.id = id;
this.dataProxy = dataProxy;
}
SubmitCommand.prototype = new Command();
SubmitCommand.prototype._getRules = function(context, done) {
this.dataProxy.getById(this.id, function(err, item) {
done(new CanSubmitOrderItemRule(item));
});
};
SubmitCommand.prototype._onValidationSuccess = function(context, done) {
this.dataProxy.submit(this.id, function(err) {
done();
});
};
var OrderItemService = BusinessService.extend().service;
OrderItemService.prototype.submitCommand = function(id) {
return new SubmitCommand(id, this.dataProxy);
};
class SubmitCommand extends Command {
constructor(id, dataProxy) {
super();
this.id = id;
this.dataProxy = dataProxy;
}
_getRules(context, done) {
this.dataProxy.getById(this.id, function(err, item) {
done(new CanSubmitOrderItemRule(item));
});
};
_onValidationSuccess(context, done) {
this.dataProxy.submit(this.id, function(err) {
done();
});
};
}
var OrderItemService = BusinessService.extend().service;
OrderItemService.prototype.submitCommand = function(id) {
return new SubmitCommand(id, this.dataProxy);
};