-
Notifications
You must be signed in to change notification settings - Fork 10
BusinessService
BusinessService is the main actor within the peasy-js framework. A business service implementation exposes CRUD and other command functions (defined by you).
BusinessService is responsible for exposing commands 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 functions 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().then(result => {
if (result.success) {
customer = result.value;
} else {
console.log(result.errors);
}
});
In the above example, an instance of a customer service is created and 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.
-
If using ES6 or TypeScript, inherit from BusinessServiceBase
class CustomerService extends BusinessService { constructor(dataProxy) { super(dataProxy); } }
class PersonService extends BusinessService<IPerson, number> { constructor(dataProxy: IDataProxy<IPerson, number>) { super(dataProxy); } }
-
OR, use the peasy-js API and 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 provide implementations for.
Here is the general format:
{ params: ['id'], functions: { _getRulesForInsertCommand: function(context, done) {}, _onInsertCommandInitialization: function(context, done) {} } }
var CustomerService = BusinessService.extend({ params: ['dataProxy'] }).service;
Accepts the id of the entity that you want to query and returns a command. Upon execution, the returned command invokes the following functions of BusinessService in the order specified below:
- _onGetByIdCommandInitialization for custom initialization logic.
- _getRulesForGetByIdCommand for business and validation rule retrieval.
- _getById for business logic execution (data proxy invocations, workflow logic, etc.).
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. Upon execution, the returned command invokes the following functions of BusinessService in the order specified below:
- _onGetAllCommandInitialization for custom initialization logic.
- _getRulesForGetAllCommand for business and validation rule retrieval.
- _getAll for business logic execution (data proxy invocations, workflow logic, etc.).
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. Upon execution, the returned command invokes the following functions of BusinessService in the order specified below:
- _onInsertCommandInitialization for custom initialization logic.
- _getRulesForInsertCommand for business and validation rule retrieval.
- _insert for business logic execution (data proxy invocations, workflow logic, etc.).
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. Upon execution, the returned command invokes the following functions of BusinessService in the order specified below:
- _onUpdateCommandInitialization for custom initialization logic.
- _getRulesForUpdateCommand for business and validation rule retrieval.
- _update for business logic execution (data proxy invocations, workflow logic, etc.).
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. Upon execution, the returned command invokes the following functions of BusinessService in the order specified below:
- _onDestroyCommandInitialization for custom initialization logic.
- _getRulesForDestroyCommand for business and validation rule retrieval.
- _destroy for business logic execution (data proxy invocations, workflow logic, etc.).
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 business service constructor and createCommand functions:
- params (optional) 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: {
_getRulesForInsertCommand: function(data, context) {
return Promise.resolve(new SomeRuleForInsertValidation(data));
},
_getRulesForUpdateCommand: function(data, context) {
return Promise.resolve(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 a data proxy:
var personDataProxy = new PersonDataProxy();
var PersonService = result.service;
var personService = new PersonService(personDataProxy);
var command = personService.insertCommand({name: "Django Reinhardt"});
command.execute(function...
Because a param of dataProxy
was specified in the params
array of the BusinessService.extend()
function, the supplied personDataProxy will be accessible from within your function implementations via the dataProxy
member of the business service instance (this.dataProxy
).
Creates a command function to be exposed from the supplied business service constructor function argument. 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 business service constructor function that will expose 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(null, []);
},
_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 both new 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: {
_getRulesForInsertCommand: function(context, done) {
var person = this.data;
done(null, new ValidCityRule(person.city));
}
},{
_getRulesForUpdateCommand: function(context, done) {
var person = this.data;
done(null, 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._getRulesForInsertCommand = function(context, done) {
var person = this.data;
done(null, new ValidCityRule(person.city));
}
CustomerService.prototype._getRulesForUpdateCommand = function(context, done) {
var person = this.data;
done(null, new ValidCityRule(person.city));
}
class CustomerService extends BusinessService {
constructor(dataProxy) {
super(dataProxy);
}
_getRulesForInsertCommand(context, done) {
var person = this.data;
done(null, new ValidCityRule(person.city));
}
_getRulesForUpdateCommand(context, done) {
var person = this.data;
done(null, new ValidCityRule(person.city));
}
}
In the following example, we simply provide implementations for the _getRulesForInsertCommand()
and _getRulesForUpdateCommand()
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 supply it to the done()
function.
Here's how that looks:
var CustomerService = BusinessService.extend({
functions: {
_getRulesForInsertCommand: function(context, done) {
var person = this.data;
done(null, [
new ValidCityRule(person.city),
new PersonNameRule(person.name)
]);
}
},{
_getRulesForUpdateCommand: function(context, done) {
var person = this.data;
done(null, [
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._getRulesForInsertCommand = function(context, done) {
var person = this.data;
done(null, [
new ValidCityRule(person.city),
new PersonNameRule(person.name)
]);
}
CustomerService.prototype._getRulesForUpdateCommand = function(context, done) {
var person = this.data;
done(null, [
new ValidCityRule(person.city),
new PersonNameRule(person.name)
]);
}
class CustomerService extends BusinessService {
constructor(dataProxy) {
super(dataProxy);
}
_getRulesForInsertCommand(context, done) {
var person = this.data;
done(null, [
new ValidCityRule(person.city),
new PersonNameRule(person.name)
]);
}
_getRulesForUpdateCommand(context, done) {
var person = this.data;
done(null, [
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: {
_getRulesForUpdateCommand: function(context, done) {
var person = this.data;
this.dataProxy.getById(person.id, function(err, fetchedPerson) {
done(null, [
new SomeRule(fetchedPerson),
new AnotherRule(fetchedPerson)
]);
});
}
}
}).service;
If classical inheritance is your thing:
var CustomerService = function(dataProxy) {
BusinessService.call(this, dataProxy);
}
CustomerService.prototype = new BusinessService();
CustomerService.prototype._getRulesForUpdateCommand = function(context, done) {
var person = this.data;
this.dataProxy.getById(person.id, function(err, fetchedPerson) {
done(null, [
new SomeRule(fetchedPerson),
new AnotherRule(fetchedPerson)
]);
});
}
class CustomerService extends BusinessService {
constructor(dataProxy) {
super(dataProxy);
}
_getRulesForUpdate(context, done) {
var person = this.data;
this.dataProxy.getById(person.id, (err, fetchedPerson) => {
done(null, [
new SomeRule(fetchedPerson),
new AnotherRule(fetchedPerson)
])
});
}
}
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(context, done) {
var orderItem = this.data;
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(context, done) {
var orderItem = this.data;
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(context, done) {
var id = this.id;
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(context, done) {
var id = this.id;
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(context, done) {
var id = this.id;
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 functions 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(null, 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 from the order item service on our behalf.
An important thing to note is that the this
references in the above function implementations 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 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
});
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(null, 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(null, 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(null, 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(null, 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(null, 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(null, 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);
};