FSM core
Machine and its definition.
How To Use
Install package
Run npm install @opuscapita/fsm-workflow-core
to get up and running.
Use in code
import { MachineDefinition, Machine } from '@opuscapita/fsm-workflow-core';
Machine definition
Machine definition consists of:
Example
const machineDefinition = new MachineDefinition({
schema: {
name: "invoice approval",
initialState: "open",
finalStates: ["approved"],
states: [
{
name: "open",
description: "Open",
release: [
{
guards: [
{
expression: "object.enabled === true"
},
{
name: "someFunction",
params: [
{
name: "param1",
value: 100
}
]
}
]
},
{
to: 'approved',
guards: [...]
},
{
to: ['approved', 'awaitingConfirmation'],
guards: [...]
}
]
},
{ name: "approved", description: "Approved" }
],
transitions: [
{
from: "open",
to: "approved",
event: "approve",
guards: [
{
"name": "validate",
"params": [
{
"name": "param1",
"value": "value1"
},
{
"name": "param2",
"value": "value2"
}
]
},
{
"expression": "invoice.netAmount < 10000"
}
],
actions: [
{
"name": "archive",
"params": [
{
"name": "param1",
"value": "value1"
},
{
"name": "param2",
"value": "value2"
}
]
}
],
automatic: [
{
"name": "lastlyUpdatedMoreThan24hAgo",
"params": [
{
"name": "param1",
"value": "value1"
},
{
"name": "param2",
"value": "value2"
}
],
"negate": true
}
]
}
]
},
actions: {
archive: function({ param1, param2 }) {}
},
conditions: {
validate: function({ param1, param2 }) {},
lastlyUpdatedMoreThan24hAgo: function({ param1, param2 }) {}
},
objectConfiguration: {
stateFieldName: "status",
alias: "invoice",
example: {
"invoiceNo": "1111",
"customerId": "wefwefewfew",
"supplierId": "33333",
"netAmount": 1000,
"status": "reviewRequired"
},
schema: {
{
title: "Invoice",
type: "object",
properties: {
invoiceNo: {
type: "string"
},
customerId: {
type: "string"
},
supplierId: {
type: "string"
},
netAmount: {
type: "number"
},
status: {
type: "string"
}
},
required: ["invoiceNo"]
}
}
}
});
Schema
Defines machine transitions and initialization options. Could be presented as oriented graph, where each node represents state and directed edges are used to represent transition from one state to another.
Transitions
In schema you needs to define an array of available machine transitions. Typically a transition is triggered by an event and happens between from and to states. Optionally each transition can have actions, guards and/or automatic (conditions).
Initial state
You can define the initial state by setting the initialState property:
var machineDefinition = new MachineDefinition({
schema: {
name: 'sprint'
initialState: 'start'
transitions: [
{ from: 'start', event: 'run', to: 'finish' }
]
}
});
const machine = new Machine({ machineDefinition });
machine.start({ object }).then(({ status: 'none' }) => {
console.log(machine.currentState({ object }));
});
if initial state is not specified, then 'none' will be used (TBD)
Final states
You can define the final states (one or many) by setting the finalStates property:
var machineDefinition = new MachineDefinition({
schema: {
initialState: 'start',
finalStates: ['finish'],
transitions: [
{from: 'start', event: 'run', to: 'finish'}
]
}
});
Code (Actions and Conditions(guards/automatic))
Action
Actions (action = function) are executed during transition (not while leaving/entering state). Action references specific function by name. Action implemented separately from schema. Each action accepts named arguments explicitly defined in transition and implicit arguments like object, from, to, etc. During transition machine executes each action in defined order. Each action gets actionExecutionResults argument which serves as an accumulator from perviously called actions, where each property is an action name and value is value returned by action.
Guard (conditions)
Guards are used to protect transitions. Guard works as 'if' condition.
Technically guard is defined the same way like as action, it is a function.
The difference is that it should always return boolean value (true or false).
Condition(function) result could be inverted if its property negate is set to true.
Guards could be also sync and async functions. In case you want to implement async guard, pay additional attention
to the value resolved by a guard - it should be only boolean value. In case your guard rejects some value
(error or smth else) - it will be taken as an error and findAvailableTransitions will be rejected with error.
Note: similar to Spring State Machine Guards
Automatic (conditions)
Transition could be marked as automatic using corresponding property. It could be:
- true (boolean value) - e.g. this transition is always automatic
- array of conditions(functions, each return true or false), condition(function) result could be inverted if its property negate is set to true
Check for whether object in current state has (at least one) automatic transition could be done via task manager (inside the application). Basing on evaluated results task manager will be able to take a decision to send event without user interaction.
Stateful object as a process
Machine does not have own state, all the transitions are performed over object which state is changed by machine. Object is used by Machine as a mutable parameter passed to guards and actions.
var machineDefinition = new MachineDefinition({
schema: {
initialState: 'start'
finalStates: ['finish'],
transitions: [
{ from: 'start', event: 'run', to: 'finish' }
]
}
});
const object = { status: 'none' };
const machine = new Machine(machineDefinition);
machine.start({ object }).then(({ object }) => {
console.log(machine.currentState({ object }));
return machine.sendEvent({ object, event: 'start' })
}).then(({ object }) => {
console.log(machine.currentState({ object }));
});
Object Workflow History
Machine configuration
var machine = new Machine({ history, ... });
history is a DAO that provides a possibility to create and read object workflow history records. You can find its API (and DB specific implementation) here.
Machine writes history records for all object transitions within the workflow.
It happens when you start workflow
machine.start({ object, user, description })
or you send an event
machine.sendEvent({ object, event, user, description })
In both cases, new history records are created.
Here
- object (required) - business object of the following structure
- user (required) - user identifier who initiated an event
- description (optional) - custom text that describes transition/object
All this info together is stored in workflow history.
Note: In case of start method call history fields from and event are filled with string value NULL (4 upper cases letters: N, U, L, L)
Machine provides possibility to get(search) history records via getHistory method:
Getting/searching specific workflow history. You can either search by:
- specific object history
- initiated by specific user
- additionally, you can restrict query using finishedBy to get history within a specific period of time.
machine.getHistory(searchParameters, paging, sorting)
where
{
object,
user,
finishedOn: {
gte,
gt,
lt,
lte
}
}
{
max,
offset
}
{
by,
order
}
as a result you get a promise that results into array of objects of the following structure:
{
event,
from,
to,
object: {
businessObjectId,
businessObjectType
},
user,
description,
finishedOn
}
Note: while writing workflow object history or searching history records by
object machine uses configured convertObjectToReference callback to convert
real business object into reference object that has the following structure
{businessObjType, businessObjId}
Debugging schema
In case one needs to find out why particular transitions are available or unavailable there's a lower-level function MachineDefinition.inspectTransitions
. According to provided input params it returns all analyzed transitions with their guards and results of their evaluation. This helps to determine why particular transition was (un)available in particular case.
In case one needs to inspect release guards
defined for states, there's MachineDefinition.inspectReleaseConditions
function which is similar to MachineDefinition.inspectTransitions
.
For details about these functions see MachineDefinition.js
source.
Machine
API
var machineDefinition = new MachineDefinition({ schema, conditions, actions })
var machine = new Machine(machineDefinition, context);
machine.start({ object })
machine.availableTransitions({ object })
machine.availableAutomaticTransitions({})
machine.sendEvent({object, event, request})
machine.currentState({ object })
machine.is({ object, state})
machine.isInFinalState({ object })
machine.can({ object, event })
machine.cannot({ object, event })
machine.canBeReleased({ object, to, request })
machine.onStartTransition()
machine.onFinishTransition()
Contributors
License
OpusCapita FSM Workflow is licensed under the Apache License, Version 2.0. See LICENSE for the full license text.