@csalasb86/nestjs-state-machine-v9
TypeScript icon, indicating that this package has built-in type declarations

0.0.7 • Public • Published

Description

Finite State Machine module for Nest.

Getting started

Install package:

$ npm i --save @csalasb86/nestjs-state-machine-v9

For example, we will map the following state machine:

example state machine

After installation, import StateMachineModule into your root module with state machine graph configurations:

// app.module.ts
import { StateMachineModule } from '@csalasb86/nestjs-state-machine-v9';

@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 '@csalasb86/nestjs-state-machine-v9';
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 '@csalasb86/nestjs-state-machine-v9';
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 '@csalasb86/nestjs-state-machine-v9';

// ...

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

State Machine methods

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[];

Guards

You can create guards to block transitions.
To declare an Guard, decorate a method with the @OnGuard() decorator:

import { GuardEvent, OnGuard } from '@csalasb86/nestjs-state-machine-v9';
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

Transition Events

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 '@csalasb86/nestjs-state-machine-v9';
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 {}

License

Nestjs State Machine is MIT licensed.

Readme

Keywords

Package Sidebar

Install

npm i @csalasb86/nestjs-state-machine-v9

Weekly Downloads

3

Version

0.0.7

License

MIT

Unpacked Size

90.8 kB

Total Files

68

Last publish

Collaborators

  • csalasb86