Finite State Machine module for Nest.
Install package:
$ npm i --save @depthlabs/nestjs-state-machine
For example, we will map the following state machine:
After installation, import StateMachineModule into your root module with state machine graph configurations:
// app.module.ts
import { StateMachineModule } from '@depthlabs/nestjs-state-machine';
@Module({
imports: [
// .forRoot takes array of graph configurations
StateMachineModule.forRoot([
{
// Name of graph
name: 'project-graph',
// Initial state of graph
initialState: 'new',
// Available states in graph
states: [
'new',
'in-progress',
'done'
],
// Available transitions in graph
transitions: [
{
// Name of transistion
name: 'start',
// Source states
from: ['new'],
// Target state of transition
to: 'in-progress',
},
{
name: 'finish',
from: ['in-progress'],
to: 'done',
}
],
},
]),
]
})
export class AppModule {}
It's good idea to define graph, state and transition names in one place e.g.:
// constants.ts
export const PROJECT_GRAPH = 'project-graph'
export enum ProjectState {
NEW = 'new',
IN_PROGRESS = 'in-progress',
DONE = 'done'
}
export enum ProjectTransition {
START = 'start',
FINISH = 'finish'
}
and then:
import { StateMachineModule } from '@depthlabs/nestjs-state-machine';
import { PROJECT_GRAPH, ProjectState, ProjectTransition } from './constants';
// ...
StateMachineModule.forRoot([
{
name: PROJECT_GRAPH,
initialState: ProjectState.NEW,
states: [
ProjectState.NEW,
ProjectState.IN_PROGRESS,
ProjectState.DONE
],
transitions: [
{
name: ProjectTransition.START,
from: [ProjectState.NEW],
to: ProjectState.IN_PROGRESS,
},
{
name: ProjectTransition.FINISH,
from: [ProjectState.IN_PROGRESS],
to: ProjectState.DONE,
}
],
}
]);
Next, create model or use your exisiting model and decorate property which will be responsible for storing state of model:
// project.model.ts
import { StateStore } from '@depthlabs/nestjs-state-machine';
import { PROJECT_GRAPH, ProjectState } from './constants';
export class Project {
name: string;
@StateStore(PROJECT_GRAPH)
state: string = ProjectState.NEW;
}
@StateStore
takes one argument with graph name (string). Thanks to this one model can handle more then many graphs:
@StateStore(PROJECT_GRAPH)
state: string = ProjectState.NEW;
@StateStore(PROJECT_STATUS_GRAPH)
status: string = ProjectStatusState.ACTIVE;
At this point we can create our state machine. First inject StateMachineFactory
using standard constructor injection:
import { StateMachineFactory } from '@depthlabs/nestjs-state-machine';
// ...
constructor(
private readonly stateMachineFactory: StateMachineFactory,
) {}
Create state machine with instance of Project
model as subject in first argument of factory and with graph name in second.
const projectStateMachine = this.stateMachineFactory.create<Project>(project, PROJECT_GRAPH)
Apply transition:
// takes transition name in argument
await projectStateMachine.apply(ProjectTransition.START)
// return void but project.state is now equal ProjectState.IN_PROGRESS
Check if transition is possible:
// takes transition name in argument
await projectStateMachine.can(ProjectTransition.START)
// return true or false, can() don't throw Errors
Get all possible transitions:
await projectStateMachine.getAvailableTransitions()
// return TransitionInterface[];
You can create guards to block transitions.
To declare an Guard
, decorate a method with the @OnGuard()
decorator:
import { GuardEvent, OnGuard } from '@depthlabs/nestjs-state-machine';
import { ProjectTransition, PROJECT_GRAPH } from '../constance';
import { Project } from '../project.model';
export class ProjectCantBeNamedBlockmeGuard {
// Graph name in first argument, transition name in second
@OnGuard(PROJECT_GRAPH, ProjectTransition.START)
handle(event: GuardEvent<Project>) {
// event.subject is our Project instance
if (event.subject.name == 'blockme') { // if name isn't allowed for some reasons
event.setBlocked('transition-blocked'); // block transition using setBlocked() method
}
}
}
Than you need to register ProjectCantBeNamedBlockmeGuard
in module as provider:
@Module({
imports: [
StateMachineModule.forRoot([
// ...
])
],
providers: [
ProjectCantBeNamedBlockmeGuard
],
})
export class AppModule {}
Now, if you create StateMachine
instance and try to apply START
transition you will get:
const project = new Project()
project.name = 'blockme'
const projectStateMachine = this.stateMachineFactory.create<Project>(project, PROJECT_GRAPH);
await projectStateMachine.can(ProjectTransition.START)
// false
await projectStateMachine.apply(ProjectTransition.START)
// Throw TransitionBlockedByGuardException with .blockingReasons property that contain ['transition-blocked']
projectStateMachine.getAvailableTransitions()
// [] - empty
You can create transition event listeners to do additional actions when a state machine operation happened (e.g. sending emails, recalculate)
When a state transition is initiated, the events are dispatched in the following order:
Order | Event | Decorator | Decorator second argument |
---|---|---|---|
1 | LeaveState (The subject is about to leave a place). |
OnLeaveState | State name |
2 | BeginTransition (The subject is going through this transition.) |
OnBeginTransition | Transition name |
3 | EnterState (The subject is about to enter a new place. This event is triggered right before the subject places are updated.) |
OnEnterState | State name |
4 | -> Change of state | ||
5 | EnteredState (The subject has entered in the places and the marking is updated.) |
OnEnteredState | State name |
6 | CompletedTransition (The object has completed this transition.) |
OnCompletedTransition | Transition name |
7 | AnnounceTransitions (Triggered for each transition that now is accessible for the subject.) |
OnAnnounceTransitions | State name |
Example:
import { OnCompletedTransition, CompletedTransitionEvent } from '@depthlabs/nestjs-state-machine';
import { ProjectTransition, PROJECT_GRAPH } from '../constance';
import { Project } from '../project.model';
export class NotifyTeamAboutFinishedTask {
// Graph name in first argument, transition name in second. Third if truem method is async.
@OnCompletedTransition(PROJECT_GRAPH, ProjectTransition.FINISH, true)
async handle(event: CompletedTransitionEvent<Project>) {
// Send emails to all users in team
}
}
and of course:
@Module({
// ...
providers: [
NotifyTeamAboutFinishedTask
],
// ...
})
export class AppModule {}
Nestjs State Machine is MIT licensed.