Skip to content

Command

ahanusa edited this page Jan 28, 2019 · 61 revisions

Command is the actor responsible for orchestrating the execution of initialization logic, validation and business rule execution, and command logic (data proxy invocations, workflow logic, etc.), respectively.

Command implementations can be consumed directly or exposed via command functions of your business service implementations.

A command is executed asynchronously and returns a promise (or alternatively accepts a completion callback function) that completes with an ExecutionResult. The ExecutionResult contains a success status, a list of potential validation errors, and optionally, a value that is the result from the command execution.

Sample consumption scenario

var customer = { name:  "Frank Zappa" };
var dataProxy = new CustomerDataProxy();
var command = new InsertCustomerCommand(customer, dataProxy);

command.execute().then(result => {
  if (result.success) {
    customer = result.value;
  } else {
    console.log(result.errors);
  }
});

In the above example, an instance of an insert customer command is created that is responsible for performing an insert against the supplied data proxy in the event of successful rule 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 command

Below are common ways to create commands:

SubmitCommand

Via the Command constructor

Creating a command via the Command constructor is a simple way to expose command logic from your business services. Creating a command in this fashion typically favors convenience over reusability.

To create a command:

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

  • Create an instance of a command by invoking the Command constructor and supply an object containing the following functions:

    var id = 123;
    var submitCommand = new Command({
      _onInitialization: function(context) {
        // do something
        return Promise.resolve();
      },
      _getRules: function(context) {
        return dataProxy.getById(id).then(item => {
          return new CanSubmitOrderItemRule(item);
        });
      },
      _onValidationSuccess: function(context) {
        return dataProxy.submit(id);
      }
    });

    In this example, we take advantage of the Command constructor, in which we provide function implementations for _onInitialization(), _getRules(), and _onValidationSuccess(), respectively.

    Notice that we have wired up a business rule method for the command. As a result, the call to dataProxy.submit() will only occur if the validation result for CanSubmitOrderItemRule is successful.

    You can view how to consume the SubmitCommand here.

Creating a command derived from Command

To create a command:

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

  • If using ES6 or TypeScript:

    ES6
    class SubmitCommand extends Command {
      constructor(id, dataProxy) {
        super();
        this.id = id;
        this.dataProxy = dataProxy;
      }
    
      _getRules(context) {
        return this.dataProxy.getById(this.id).then(result => {
          return [
            new CanSubmitOrderItemRule(result)
          ];
        });
      };
    
      _onValidationSuccess(context) {
        return this.dataProxy.submit(this.id);
      };
    }
    TypeScript
    class SubmitCommand extends Command<OrderItem> {
      constructor(private id: number, private dataProxy: IDataProxy<OrderItem, number>) {
        super();
      }
    
      protected async _getRules(context: any): Promise<IRule[]> {
        const result = await this.dataProxy.getById(this.id);
        return [
          new CanSubmitOrderItemRule(result)
        ];
      };
    
      protected _onValidationSuccess(context: any): Promise<OrderItem> {
        return this.dataProxy.submit(this.id);
      };
    }
  • OR, if using the peasy-js constructor creation API, create a constructor function by invoking Command.extend() and supply an object containing the following configuration:

    peasy-js constructor creation API
    var SubmitCommand = Command.extend({
      functions: {
        _getRules: function(id, dataproxy, context) {
          return dataProxy.getById(id).then(result => {
            return [
              new CanSubmitOrderItemRule(result)
            ];
          });
        },
        _onValidationSuccess: function(id, dataproxy, context) {
          return dataProxy.submit(id);
        }
      }
    });

    In this example, we consume Command.extend() to create a reusable command. Using this approach saves you from having to apply classical inheritance techniques if you aren't comfortable using them.

    Note that we have wired up a business rule method for the command. As a result, the call to dataProxy.submit() will only occur if the validation result for CanSubmitOrderItemRule is successful.

    Also note that with the exception of the context parameter, the parameters of _getRules() and _onValidationSuccess() are determined by the arguments passed to the constructor of the SubmitCommand.

    For example, if the following command is instantiated with arguments like so:

      var command = new SomeCommand("val1", 2, "val3", { name: 'lenny' });

    Then the function signature for _onInitialize(), _getRules(), and _onValidationSuccess() will look like this:

      function(param1, param2, param3, param4, context) {} // the parameters can be named however you wish

Consuming a command

var itemId = 5;
var dataProxy = new OrderItemDatabaseProxy();
var command = new SubmitCommand(itemId, dataProxy);

command.execute().then(result => {
  if (result.success) {
    console.log(result.value);
  } else {
    console.log(result.errors);
  }
}

In this example, we instantiate the SubmitCommand, supplying it with an id and data proxy. We then execute it and consume the returned result.

Note that result represents the result of the execution pipeline, and may contain errors (not exceptions) that were produced as a result of rule executions.

Generating Errors

Sometimes it's necessary to provide immediate feedback as a result of broken business or validation rules. This can be achieved by consuming the getErrors function of a command instance, as can be seen below:

var command = service.insertCommand({ requiredField: null});

command.getErrors().then(errors => {
  console.log("ERRORS", errors);
});

In this example, the rules configured to execute in the returned insert command's execution pipeline are validated, returning an array of potential errors that occurred.

Testing a command

Here is how you might test the SubmitCommand using Jasmine:

describe("SubmitCommand", function() {
  var orderItem = { id: 1, itemId: 1234, quantity: 4 };
  var dataProxy = {
    getById: function(id, context) {
      return Promise.resolve(orderItem);
    },
    submit: function(id, context) {
      return Promise.resolve(orderItem);
    }
  }

  it("submits the order item when the configured rule evaluates to true", function(onComplete) {
    CanSubmitOrderItemRule.prototype._onValidate = function() {
      return Promise.resolve();
    };
    var command = new SubmitCommand(1, dataProxy);
    command.execute().then(result => {
      expect(result.success).toEqual(true);
      expect(result.value).toEqual(orderItem);
      onComplete();
    });
  });

  it("does not submit the order item when the configured rule evaluates to false", function(onComplete) {
    CanSubmitOrderItemRule.prototype._onValidate = function() {
      this._invalidate('cannot submit order item');
      return Promise.resolve();
    };
    var command = new SubmitCommand(1, dataProxy);
    command.execute().then(result => {
      expect(result.success).toEqual(false);
      expect(result.errors.length).toEqual(1);
      onComplete();
    });
  });
});

The above code tests how to make the command execution pass and fail by manipulating the configured rule.

Notice that we simply stubbed the _onValidate() function of the CanSubmitOrderItemRule in each test to manipulate the execution flow.

Public functions (instance)

getErrors()

Asynchronously executes validation and business rule, returning an array of potential errors generated during validation.

execute()

Asynchronously executes initialization logic, validation and business rule execution, and command logic (data proxy invocations, workflow logic, etc.). Returns a promise (or alternatively accepts a completion callback function) that resolves with an ExecutionResult.

Public functions (static)

extend()

Accepts an object containing the members outlined below and returns a command constructor 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.

You can view an example of how to use Command.extend() here.

Command execution pipeline

During command execution, initialization logic, validation and business rules, and command logic (data proxy invocations, workflow logic, etc.) is executed in a specific order (or sometimes not at all).

command execution pipeline

The following functions all participate in the command execution pipeline, and offer hooks into them when creating commands.

_onInitialization()

The first function to execute in the command execution pipeline, represents the function that will be invoked before rule evaluations. This function allows you to initialize your data with required fields, timestamps, etc. You can also inject cross-cutting concerns such as logging and instrumentation here as well. This function must return a promise or accept a completion callback.

_getRules()

The second function to execute in the command execution pipeline, represents the function that returns rule(s) for rule evaluations. This function allows you to supply a single or multiple rules either via an array of rules or rule chaining. This function must return a promise or accept a completion callback.

_onValidationSuccess()

The third function to execute in the command execution pipeline, represents the function that will be invoked upon successful execution of rules. This function allows you to perform workflow logic, data proxy communications, or any other business logic that you have to perform as a functional unit of work. This function must return a promise or accept a completion callback.

Execution Context

Often times you will need to obtain data that rules rely on for validation. This same data is often needed for functions that participate in the command execution pipeline for various reasons.

The execution context is an object that is passed through the command execution pipeline and can carry with it data to be shared between functions throughout the command execution workflow.

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

ES6
class UpdateCustomerCommand {

  constructor(customerId, dataProxy) {
    super();
    this.customerId = customerId;
    this.dataProxy = dataProxy;
  }

  _getRules(context) {
    return this.dataProxy.getById(this.customerId).then(customer => {
      context.currentCustomer = customer;
      return new ValidAgeRule(customer);
    });
  }

  _onValidationSuccess(context) {
    var customer = context.currentCustomer;
    return this.dataProxy.update(customer);
  }
}
TypeScript
class UpdateCustomerCommand extends Command<Customer> {

  constructor(private customerId: number, private dataProxy: IDataProxy<Customer, number>) {
    super();
  }

  protected async _getRules(context: any): Promise<IRule[]> {
    const customer = await this.dataProxy.getById(this.customerId);
    context.currentCustomer = customer;
    return new ValidAgeRule(customer);
  }

  protected _onValidationSuccess(context: any): Promise<Customer> {
    const customer = context.currentCustomer;
    return this.dataProxy.update(customer);
  }
}
peasy-js constructor creation API
var UpdateCustomerCommand = Command.extend({
  functions:
  {
    _getRules: function(customerId, dataProxy, context) {
      return dataProxy.getById(customerId).then(customer => {
        context.currentCustomer = customer;
        return new ValidAgeRule(customer);
      });
    },
    _onValidationSuccess: function(customerId, dataProxy, context) {
      var customer = context.currentCustomer;
      return dataProxy.update(customer);
    }
  }
});

In this example, we have configured the UpdateCustomerCommand to subject the current customer in our data store to the ValidAgeRule before we perform an update against our data store.

There are two points of interest here:

First we retrieved a customer from our data proxy in our _getRules() function implementation. We then assigned it to the execution context and passed it as an argument to the ValidAgeRule.

In addition, we retrieved the customer object from the execution context in our _onValidationSuccess function implementation.

In this example, the execution context allowed us to minimize our hits to the data store by sharing state between functions in the command execution pipeline.