Skip to content

BusinessService

ahanusa edited this page Jan 28, 2019 · 122 revisions

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

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.

Creating a business service

To create a business service:

  • Import or reference peasy-js.BusinessService from peasy.js.

  • If using ES6 or TypeScript, inherit from BusinessServiceBase

    ES6
    class CustomerService extends BusinessService {
      constructor(dataProxy) {
        super(dataProxy);
      }
    }
    TypeScript
    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) {}
      }
    }
    peasy-js constructor creation API
    var CustomerService = BusinessService.extend({ params: ['dataProxy'] }).service;

Public functions (instance)

getByIdCommand(id)

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:

  1. _onGetByIdCommandInitialization(id, context) for custom initialization logic.
  2. _getRulesForGetByIdCommand(id, context) for business and validation rule retrieval.
  3. _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.

getAllCommand()

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:

  1. _onGetAllCommandInitialization(context) for custom initialization logic.
  2. _getRulesForGetAllCommand(context) for business and validation rule retrieval.
  3. _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.

insertCommand(data)

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:

  1. _onInsertCommandInitialization(data, context) for custom initialization logic.
  2. _getRulesForInsertCommand(data, context) for business and validation rule retrieval.
  3. _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.

updateCommand(data)

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:

  1. _onUpdateCommandInitialization(data, context) for custom initialization logic.
  2. _getRulesForUpdateCommand(data, context) for business and validation rule retrieval.
  3. _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.

destroyCommand(id)

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:

  1. _onDestroyCommandInitialization(id, context) for custom initialization logic.
  2. _getRulesForDestroyCommand(id, context) for business and validation rule retrieval.
  3. _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.

Public functions (static)

extend(options)

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

createCommand(options)

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!"
});

Wiring up rules

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:

ES6
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);
    ]);
  }
}
TypeScript
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);
    ]);
  }
}
peasy-js constructor creation API
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.

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 wrapped in a promise (or alternatively pass it to a completion callback function)

Here's how that looks:

ES6
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)
    ]);
  }
}
TypeScript
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)
    ]);
  }
}
peasy-js constructor creation API
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;

Wiring up rules that consume data proxy data

Sometimes rules require data from data proxies for validation.

Here's how that might look:

ES6
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)
      ];
    });
  }
}
TypeScript
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)
    ];
  }
}
peasy-js constructor creation API
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;

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

Providing defaults example
ES6
class OrderItemService extends BusinessService {
  constructor(dataProxy) {
    super(dataProxy);
  }

  _onInsertCommandInitialization(orderItem, context) {
    orderItem.status = STATUS.pending;
    orderItem.createdDate = new Date();
    return Promise.resolve();
  }
}
TypeScript
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();
  }
}
peasy-js constructor creation API
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.

Whitelisting fields example
ES6
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();
  }
}
peasy-js constructor creation API
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.

Overriding default command logic

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:

ES6
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");
    });
  }
}
TypeScript
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");
  }
}
peasy-js constructor creation API
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;

Exposing new command functions

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:

Solutions that favor convenience

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.

Returning new command example
ES6
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);
      }
    });
  }
}
TypeScript
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);
      }
    });
  }
}
createCommand() example

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.

peasy-js constructor creation API
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
});
createCommand() chaining example

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

Solutions that favor reusability

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:

ES6
// 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);
  }
}
TypeScript
// 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);
  }
}
peasy-js constructor creation API

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