Skip to content

BusinessService

Aaron Hanusa edited this page May 29, 2016 · 122 revisions

BusinessService is the main actor within the peasy-js framework. A concrete implementation becomes what is called a 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/ServiceCommand) 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 in a thread-safe manner by multiple clients. You can think of a an implementation of BusinessService as a CRUD command factory.

Sample consumption scenario

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 validation rule execution.

Finally, the command is executed via the 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.

Creating a business service

To create a business service:

  • Import or reference peasy-js.BusinessService
  • Create a constructor function by invoking BusinessService.extend(), and supply the following configuration via an object literal:
    • params (optional) - represents an array of strings of the named arguments expected to be passed to the constructor of a service.
    • functions (optional) - represents an object literal 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:

ES5
var CustomerService = function(dataProxy) {
  BusinessService.call(this, dataProxy);
}

CustomerService.prototype = new BusinessService();
ES6
class CustomerService extends BusinessService {
  constructor(dataProxy) {
    super(dataProxy);
  }
}

Public functions (instance)

Accepts the id of the entity that you want to query and returns a constructed command. The returned command from getByIdCommand invokes the following methods of BusinessService upon execution in the order specified below:

  1. _onGetByIdCommandInitialization for custom initialization logic
  2. _getRulesForGetById for business and validation rule retrieval
  3. _getById on successful validation of rules.

The command subjects the supplied id to validation and business 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:

  1. _onGetAllCommandInitialization for custom initialization logic
  2. _getRulesForGetAll for business and validation rule retrieval
  3. _getAll on successful validation of rules.

The command invokes validation and business 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 constructed command. The returned command from insertCommand invokes the following methods of BusinessService upon execution in the order specified below:

  1. _onInsertCommandInitialization for custom initialization logic
  2. _getRulesForInsert for business and validation rule retrieval
  3. _insert on successful validation of rules.

The command subjects the supplied object to validation and business 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 constructed command. The returned command from updateCommand invokes the following methods of BusinessService upon execution in the order specified below:

  1. _onUpdateCommandInitialization for custom initialization logic
  2. _getRulesForUpdate for business and validation rule retrieval
  3. _update on successful validation of rules.

The command subjects the supplied object to validation and business 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 constructed command. The returned command from destroyCommand invokes the following methods of BusinessService upon execution in the order specified below:

  1. _onDestroyCommandInitialization for custom initialization logic
  2. _getRulesForDestroy for business and validation rule retrieval
  3. _destroy on successful validation of rules.

The command subjects the supplied id to validation and business rules (if any) before marshaling the call to the injected data proxy's destroy function.

Public functions (static)

extend(options)

Accepts an object literal containing the members outlined below and returns an object literal containing the service implementation as well as a createCommand function:

  1. params (optional) represents the names of the parameters which are expected to be received as arguments to a new instance of the business service (constructor function).
  2. functions (optional) an object literal 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) {
      console.log("DATA PROXY", this.dataProxy);
      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 literal containing params and functions.

Notice that the result from the invocation results in an object literal containing the following functions:

  1. createCommand() - allows you to easily extend your business services by dynamically exposing custom command functions on your behalf in a chainable fashion.
  2. 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, 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...

createCommand(options)

Creates a command function that is exposed via the supplied business service. Accepts an object literal containing the following members:

  1. name (required) specifies the name of the command function that will be exposed from the business service.
  2. functions (optional) an object literal containing the function implementations for command hooks.
  3. service (required) represents a constructor function that will house the dynamically created command function.
  4. params (optional) represents the names of the parameters which are expected to be received as arguments to the invocation of the dynamically created command.

Wiring up rules

BusinessService exposes commands for invoking create, retrieve, update, and delete (CRUD) operations against the injected [data proxies] (https://github.com/peasy/peasy-js/wiki/Data-Proxy). 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 an city verification check before successfully persisting it into our data store entity.

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:

ES5
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));
}
ES6
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(s) 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 [service command] (https://github.com/peasy/peasy-js/wiki/ServiceCommand) execution pipeline, providing clarity as to what business rules are executed for each type of CRUD operation.

Wiring up multiple rules

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:

ES5
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)
  ]);
}
ES6
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)
    ]);
  }
}

Wiring up rules that consume data proxy data

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:

ES5
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)
    ]);
  });
}
ES6
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)
      ])
    });
  }
}

Providing initialization logic

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

Providing defaults example
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.

Whitelisting fields example
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.

Overriding default command logic

By default, all service command methods of a default implementation of BusinessService are wired up to invoke data proxy methods. 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:

ES5
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);
};
ES6
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);
  }
}

Exposing new command functions

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:

Solutions that favor convenience

createCommand() example
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
});
createCommand() chaining example

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
});
Returning new command example
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 convenient Command constructor, in which we specify a couple of 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 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
});

Solutions that favor reusability

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:

ES5
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);
};
ES6
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);
};

Execution Context

Often times you will need to obtain data that rules rely on for validation. This same data is often needed for service command methods that require it for various reasons. The execution context is passed through the command execution pipeline and can carry data with it to be shared between functions throughout the command execution workflow.

Here's what this might look like in an OrderItemService:

protected override IEnumerable<IRule> GetBusinessRulesForUpdate(OrderItem entity, ExecutionContext<OrderItem> context)
{
    var item = base.DataProxy.GetByID(entity.ID);
    context.CurrentEntity = item;
    yield return new ValidOrderItemStatusForUpdateRule(item);
}

protected override OrderItem Update(OrderItem entity, ExecutionContext<OrderItem> context)
{
    var current = context.CurrentEntity;
    entity.RevertNonEditableValues(current);
    return base.DataProxy.Update(entity);
}

In this example, we have overridden BusinessService.GetBusinessRulesForUpdate to subject a rule to our synchronous command execution pipeline. We can see that the rule requires the current state of the requested order item. We also have overridden BusinessService.Update (invoked by BusinessService.UpdateCommand to supply additional functionality that occurs on update).

There are two things to notice here. First we retrieve an order item from our data proxy and assign to [ExecutionContext.CurrentEntity] (https://github.com/peasy/peasy-js/blob/master/Peasy.Core/ExecutionContext.cs#L7) in our overridden GetBusinessRulesForUpdate method. In addition, we retrieve the order item from the execution context in our overridden BusinessService.Update method. In this example, the execution context allowed us to minimize our hits to the data store by sharing state between methods in the command execution pipeline.

In addition to ExecutionContext.CurrentEntity, the execution context exposes a Data property of type IDictionary<string, object> which allows the sharing of any kind of data between methods in the command execution pipeline.

Clone this wiki locally