-
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 execute()
function, which returns a promise (or alternatively accepts a completion callback function). Upon promise completion, a result is returned that represents the result of the execution pipeline, and may contain errors (not exceptions) that were produced as a result of 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, if using the peasy-js constructor creation API, 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(data, context) {}, _onInsertCommandInitialization: function(data, context) {} } }
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(id, context) for custom initialization logic.
- _getRulesForGetByIdCommand(id, context) for business and validation rule retrieval.
- _getById(id, context) 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(context) for custom initialization logic.
- _getRulesForGetAllCommand(context) for business and validation rule retrieval.
- _getAll(context) 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(data, context) for custom initialization logic.
- _getRulesForInsertCommand(data, context) for business and validation rule retrieval.
- _insert(data, context) 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(data, context) for custom initialization logic.
- _getRulesForUpdateCommand(data, context) for business and validation rule retrieval.
- _update(data, context) 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(id, context) for custom initialization logic.
- _getRulesForDestroyCommand(id, context) for business and validation rule retrieval.
- _destroy(id, context) 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:
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.
- 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
object from the extend()
invocation returns 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().then(result => { ...
});
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 (required) 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.
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.
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(incoming, context) {
context.testValue = "4";
return Promise.resolve();
},
_getRules: function(incoming, context) {
context.testValue += "2";
return Promise.resolve([]);
},
_onValidationSuccess: function(incoming, context) {
return Promise.resolve(context.testValue + incoming);
}
}
});
Here is how to consume the testCommand function exposed from CustomerService as a result of the above BusinessService.createCommand()
invocation.
var dataProxy = new CustomerFileProxy();
var service = new CustomerService(dataProxy);
var command = personService.testCommand("!");
command.execute().then(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:
class CustomerService extends BusinessService {
constructor(dataProxy) {
super(dataProxy);
}
_getRulesForInsertCommand(customer, context) {
return Promise.resolve([
new ValidCityRule(customer.city);
]);
}
_getRulesForUpdateCommand(customer, context) {
return Promise.resolve([
new ValidCityRule(customer.city);
]);
}
}
class CustomerService extends BusinessService<Customer, number> {
constructor(dataProxy: IDataProxy<Customer, number>) {
super(dataProxy);
}
protected _getRulesForInsertCommand(customer: Customer, context: any): Promise<IRule[]>{
return Promise.resolve([
new ValidCityRule(customer.city);
]);
}
protected _getRulesForUpdateCommand(customer: Customer, context: any): Promise<IRule[]>{
return Promise.resolve([
new ValidCityRule(customer.city);
]);
}
}
var CustomerService = BusinessService.extend({
functions: {
_getRulesForInsertCommand: function(customer, context) {
return Promise.resolve([
new ValidCityRule(customer.city);
]);
}
},{
_getRulesForUpdateCommand: function(customer, context) {
return Promise.resolve([
new ValidCityRule(customer.city);
]);
}
}
}).service;
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 return it wrapped in a promise (or alternatively pass it to a completion callback function)
Here's how that looks:
class CustomerService extends BusinessService {
constructor(dataProxy) {
super(dataProxy);
}
_getRulesForInsertCommand(customer, context) {
return Promise.resolve([
new ValidCityRule(customer.city),
new PersonNameRule(customer.name)
]);
}
_getRulesForUpdateCommand(customer, context) {
return Promise.resolve([
new ValidCityRule(customer.city),
new PersonNameRule(customer.name)
]);
}
}
class CustomerService extends BusinessService<Customer, number> {
constructor(dataProxy: IDataProxy<Customer, number>) {
super(dataProxy);
}
protected _getRulesForInsertCommand(customer: Customer, context: any): Promise<IRule[]>{
return Promise.resolve([
new ValidCityRule(customer.city),
new PersonNameRule(customer.name)
]);
}
protected _getRulesForUpdateCommand(customer: Customer, context: any): Promise<IRule[]>{
return Promise.resolve([
new ValidCityRule(customer.city),
new PersonNameRule(customer.name)
]);
}
}
var CustomerService = BusinessService.extend({
functions: {
_getRulesForInsertCommand: function(customer, context) {
return Promise.resolve([
new ValidCityRule(customer.city),
new PersonNameRule(customer.name)
]);
}
},{
_getRulesForUpdateCommand: function(customer, context) {
return Promise.resolve([
new ValidCityRule(customer.city),
new PersonNameRule(customer.name)
]);
}
}
}).service;
Sometimes rules require data from data proxies for validation.
Here's how that might look:
class CustomerService extends BusinessService {
constructor(dataProxy) {
super(dataProxy);
}
_getRulesForUpdateCommand(customer, context) {
return this.dataProxy.getById(customer.id).then(fetchedCustomer => {
return [
new SomeRule(fetchedCustomer),
new AnotherRule(fetchedCustomer)
];
});
}
}
class CustomerService extends BusinessService<Customer, number> {
constructor(dataProxy: IDataProxy<Customer, number>) {
super(dataProxy);
}
protected async _getRulesForUpdateCommand(customer: Customer, context: any): Promise<IRule[]> {
const fetchedCustomer = await this.dataProxy.getById(customer.id);
return [
new SomeRule(fetchedCustomer),
new AnotherRule(fetchedCustomer)
];
}
}
var CustomerService = BusinessService.extend({
functions: {
_getRulesForUpdateCommand: function(customer, context) {
return this.dataProxy.getById(customer.id).then(fetchedCustomer => {
return [
new SomeRule(fetchedCustomer),
new AnotherRule(fetchedCustomer)
];
});
}
}
}).service;
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.
class OrderItemService extends BusinessService {
constructor(dataProxy) {
super(dataProxy);
}
_onInsertCommandInitialization(orderItem, context) {
orderItem.status = STATUS.pending;
orderItem.createdDate = new Date();
return Promise.resolve();
}
}
class OrderItemService extends BusinessService<OrderItem, number> {
constructor(dataProxy: IDataProxy<OrderItem, number>) {
super(dataProxy);
}
protected _onInsertCommandInitialization(orderItem: OrderItem, context: any): Promise<void> {
orderItem.status = STATUS.pending;
orderItem.createdDate = new Date();
return Promise.resolve();
}
}
var OrderItemService = BusinessService.extend({
functions: {
_onInsertCommandInitialization: function(orderItem, context) {
orderItem.status = STATUS.pending;
orderItem.createdDate = new Date();
return Promise.resolve();
}
}
}).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.
class OrderItemService extends BusinessService {
constructor(dataProxy) {
super(dataProxy);
}
_onInsertCommandInitialization(orderItem, context) {
const allowableFields = ['id', 'quantity'];
Object.keys(orderItem).forEach(field => {
if (allowableFields.indexOf(field) === -1) {
delete orderItem[field];
}
});
return Promise.resolve();
}
}
var OrderItemService = BusinessService.extend({
functions: {
_onUpdateCommandInitialization: function(orderItem, context) {
var allowableFields = ['id', 'quantity'];
Object.keys(orderItem).forEach(field => {
if (allowableFields.indexOf(field) === -1) {
delete orderItem[field];
}
});
return Promise.resolve();
}
}
}).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.
Note: this example mutates the state. You might consider copying the object first and replacing the reference with the copy before mutation if you need to perform similar operations.
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:
class CustomerService extends BusinessService {
constructor(dataProxy) {
super(dataProxy);
}
_destroy(id, context) {
console.log("DELETING...");
return this.dataProxy.destroy(id).then(result => {
console.log("DELETE COMPLETE");
});
}
}
class CustomerService extends BusinessService<IPerson, number> {
constructor(dataProxy: IDataProxy<IPerson, number>) {
super(dataProxy);
}
protected async _destroy(id: number, context: any): Promise<void> {
console.log("DELETING...");
await this.dataProxy.destroy(id);
console.log("DELETE COMPLETE");
}
}
var CustomerService = BusinessService.extend({
functions: {
_destroy: function(id, context, done) {
console.log("DELETING...");
return this.dataProxy.destroy(id).then(result => {
console.log("DELETE COMPLETE");
});
}
}
}).service;
There will be cases where you'll 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:
When exposing new commands from our services, we can easily expose command functions by taking advantage of the Command constructor. While convenient, this approach does not promote easy code reuse as do some of the other methods discussed later in this section.
In the example below, we take advantage of the Command constructor by providing implementations for _getRules()
and _onValidationSuccess()
. Here 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.
class OrderItemService extends BusinessService {
constructor(dataProxy) {
super(dataProxy);
}
submitCommand(id) {
const dataProxy = this.dataProxy;
return new Command({
_getRules: () => {
return dataProxy.getById(id).then(item => {
return [
new CanSubmitOrderItemRule(item)
];
});
},
_onValidationSuccess: () => {
return dataProxy.submit(id);
}
});
}
}
class OrderItemService extends BusinessService<OrderItem, number> {
constructor(dataProxy: IOrderItemDataProxy) {
super(dataProxy);
}
public submitCommand(id: number): Command<OrderItem> {
const dataProxy = this.dataProxy as IOrderItemDataProxy;
return new Command<OrderItem>({
_getRules: async function(): Promise<IRule[]> {
const item = await dataProxy.getById(id);
return [
new CanSubmitOrderItemRule(item)
];
},
_onValidationSuccess: function(): Promise<OrderItem> {
return dataProxy.submit(id);
}
});
}
}
Note: This example serves as an illustration if using the peasy-js constructor creation API. This method can safely be ignored if using ES6 or TypeScript class inheritance.
var OrderItemService = BusinessService
.extend()
.createCommand({
name: 'submitCommand',
functions:
{
_getRules: function(id, context) {
return this.dataProxy.getById(id).then(item => {
return new CanSubmitOrderItemRule(item);
});
},
_onValidationSuccess: function(id, context) {
return this.dataProxy.submit(id);
}
}
}).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.
Below is how you can consume your command:
var orderItemService = new OrderItemService(new OrderItemDataProxy());
var command = orderItemService.submitCommand(123);
command.execute().then(result => {
// do something here
});
This example extends the createCommand() example by chaining createCommand() functions.
var OrderItemService = BusinessService
.extend()
.createCommand({
name: 'submitCommand',
functions:
{
_getRules: function(id, context) {
return this.dataProxy.getById(id).then(item => {
return new CanSubmitOrderItemRule(item);
});
},
_onValidationSuccess: function(id, context) {
return this.dataProxy.submit(id);
}
}
})
.createCommand({
name: 'shipCommand',
functions:
{
_getRules: function(id, context) {
return this.dataProxy.getById(id).then(item => {
return new CanShipOrderItemRule(item);
});
},
_onValidationSuccess: function(id, context) {
return this.dataProxy.ship(id);
}
}
}).service;
Below is how you can consume your commands:
var orderItemService = new OrderItemService(new OrderItemDataProxy());
var submitCommand = orderItemService.submitCommand(123);
var shipCommand = orderItemService.shipCommand(123);
submitCommand.execute().then(result => {
// now ship it
shipCommand.execute().then(result => {
// do something here
});
});
While slightly more effort, Commands can be created as first class citizens. This allows us to consume them from multiple services and other commands, providing us with greater reusability.
The following example illustrates this:
// submitCommand.js
class SubmitCommand extends Command {
constructor(id, dataProxy) {
super();
this.id = id;
this.dataProxy = dataProxy;
}
_getRules() {
return this.dataProxy.getById(this.id).then(item => {
return new CanSubmitOrderItemRule(item);
});
};
_onValidationSuccess() {
return this.dataProxy.submit(this.id);
};
}
// orderItemService.js
class OrderItemService extends BusinessService {
constructor(dataProxy) {
super(dataProxy);
}
submitCommand(id) {
return new SubmitCommand(id, this.dataProxy);
}
}
// submitCommand.ts
class SubmitCommand extends Command<OrderItem> {
constructor(private id: number, private dataProxy: IOrderItemDataProxy) {
super();
}
protected async _getRules(): Promise<IRule[]> {
const item = await this.dataProxy.getById(this.id);
return new CanSubmitOrderItemRule(item);
};
protected _onValidationSuccess(): Promise<OrderItem> {
return this.dataProxy.submit(this.id);
};
}
// orderItemService.ts
class OrderItemService extends BusinessService<OrderItem, number> {
constructor(dataProxy: IOrderItemDataProxy) {
super(dataProxy);
}
submitCommand(id: number) {
return new SubmitCommand(id, this.dataProxy as IOrderItemDataProxy);
}
}
Note: This example serves as an illustration if using the peasy-js constructor creation API. This method can safely be ignored if using ES6 or TypeScript class inheritance.
var SubmitCommand = Command.extend({
functions: {
_getRules: function(id, dataProxy, context) {
return dataProxy.getById(id).then(item => {
return new CanSubmitOrderItemRule(item);
});
},
_onValidationSuccess: function(id, dataProxy, context) {
return dataProxy.submit(this.id);
}
}
});
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 service instance, giving us access to the data proxy.
Below is how you can consume your command:
var orderItemService = new OrderItemService(new OrderItemDataProxy());
var command = orderItemService.submitCommand(123);
command.execute().then(result => {
// do something here
});