-
Notifications
You must be signed in to change notification settings - Fork 10
Command
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
- Creating a command
- Consuming a command
- Generating Errors
- Testing a command
- Public functions
- Command execution pipeline
- Execution Context
- Exposing commands from a business service
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.
Below are common ways to create commands:
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:
-
Create an instance of a command by invoking the Command constructor and supply an object containing the following functions:
- _onInitialization(context) (optional) - The first function to execute in the command execution pipeline.
- _getRules(context) (optional) - The second function to execute in the command execution pipeline.
- _onValidationSuccess(context) (required) - The third function to execute in the command execution pipeline.
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 forCanSubmitOrderItemRule
is successful.You can view how to consume the SubmitCommand here.
To create a command:
-
If using ES6 or TypeScript:
- Inherit from Command.
- Provide implementations for
- _onInitialization(context) (optional) - The first function to execute in the command execution pipeline.
- _getRules(context) (optional) - The second function to execute in the command execution pipeline.
- _onValidationSuccess(context) (required) - The third function to execute in the command execution pipeline.
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); }; }
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:-
functions (required) - an object containing the functions below
- _onInitialization() (optional) - The first function to execute in the command execution pipeline.
- _getRules() (optional) - The second function to execute in the command execution pipeline.
- _onValidationSuccess() (required) - The third function to execute in the command execution pipeline.
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 forCanSubmitOrderItemRule
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 theSubmitCommand
.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
-
functions (required) - an object containing the functions below
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.
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.
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.
Asynchronously executes validation and business rule, returning an array of potential errors generated during validation.
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.
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.
- params (optional) - represents an array of strings representing the arguments expected to be passed to the constructor of a command.
-
functions (required) - an object containing the functions below
- _onInitialization() (optional) - The first function to execute in the command execution pipeline.
- _getRules() (optional) - The second function to execute in the command execution pipeline.
- _onValidationSuccess() (required) - The third function to execute in the command execution pipeline.
You can view an example of how to use Command.extend() here.
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).
The following functions all participate in the command execution pipeline, and offer hooks into them when creating commands.
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.
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.
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.
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:
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);
}
}
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);
}
}
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.