npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

typeorm-fsm

v1.8.1

Published

Strong typed state machine for your TypeORM Entities

Downloads

7

Readme

TypeORM State Machine

Commitizen friendly Maintainability Test Coverage

Package migrated to monorepo and published with new name @fsmoothy/typeorm. This repo in maintenance mode. Only critical bugs will be fixed.

typeorm-fsm is a strongly typed state machine designed for TypeORM entities. It allows you to define and manage state transitions in a declarative manner. The library is using fsmoothy package to provide the best DX.

Index

Usage

Let's create a basic order state machine to showcase the features of the library. The diagram below illustrates the states and transitions of the state machine.

stateDiagram-v2
  draft --> assembly: create
  assembly --> warehouse: assemble
  assembly --> shipping: ship
  warehouse --> warehouse: transfer
  warehouse --> shipping: ship
  shipping --> delivered: deliver

Events and States

The library was initially designed to use enums for events and states. However, using string enums would provide more convenient method names. It is also possible to use string or number as event or state types, but this approach is not recommended.

enum OrderItemState {
  draft = 'draft',
  assembly = 'assembly',
  warehouse = 'warehouse',
  shipping = 'shipping',
  delivered = 'delivered',
}

enum OrderItemEvent {
  create = 'create',
  assemble = 'assemble',
  transfer = 'transfer',
  ship = 'ship',
  deliver = 'deliver',
}

interface IOrderItemContext = FSMContext<{
  place: string;
}>

Entity

To create an entity class, it must extend StateMachineEntity and have defined initial state and transitions. Additionally, you can combine StateMachineEntity with your own BaseEntity, which should be extended from TypeORM's base entity.

class BaseEntity extends TypeOrmBaseEntity {
  @PrimaryGeneratedColumn()
  id: string;
}

@Entity('order')
class Order extends StateMachineEntity(
  {
    itemsStatus: state({
      id: 'orderItemsStatus',
      initial: OrderItemState.draft,
      persistContext: true,
      data: () => ({
        place: 'My warehouse',
      }),
      transitions: [
        t(OrderItemState.draft, OrderItemEvent.create, OrderItemState.assembly),
        t(
          OrderItemState.assembly,
          OrderItemEvent.assemble,
          OrderItemState.warehouse,
        ),
        {
          from: OrderItemState.warehouse,
          event: OrderItemEvent.transfer,
          to: OrderItemState.warehouse,
          guard(context: IOrderItemContext, place: string) {
            return context.data.place !== place;
          },
          onExit(context: IOrderItemContext, place: string) {
            context.data.place = place;
          },
        },
        t(
          [OrderItemState.assembly, OrderItemState.warehouse],
          OrderItemEvent.ship,
          OrderItemState.shipping,
        ),
        t(
          OrderItemState.shipping,
          OrderItemEvent.deliver,
          OrderItemState.delivered,
        ),
      ],
    }),
  },
  BaseEntity, // It's optional
) {
  @Column({
    default: 0,
  })
  price: number;
}

StateMachineEntity

Let's take a look at the StateMachineEntity mixin. It accepts an object with the following properties:

  • id - a unique identifier for the state machine (used for debugging purposes)
  • initial - the initial state of the state machine
  • persistContext - if set to true, the state machine context will be saved to the database. Default value is false
  • saveAfterTransition - if true, the state machine will be saved to the database after each transition. Default value is true
  • data - initial data for the state machine context
  • transitions - an array of transitions
  • subscribers - an object with subscribers array for events

It also support extend your own BaseEntity class by passing it as a second argument.

Transitions

The most common way to define a transition is by using the t function, which requires three arguments (guard is optional).

t(from: State | State[], event: Event, to: State, guard?: (context: Context) => boolean);

We also able to pass optional onEnter and onExit functions to the transition as options:

t(
  from: State | State[],
  event: Event,
  to: State,
  options?: {
    guard?: (context: Context) => boolean;
    onEnter?: (context: Context) => void;
    onExit?: (context: Context) => void;
  },
);

In such cases, we're using next options:

  • from - represents the state from which the transition is permitted
  • event - denotes the event that triggers the transition
  • to - indicates the state to which the transition leads
  • guard - a function that verifies if the transition is permissible
  • onEnter - a function that executes when the transition is triggered
  • onExit - a function that executes when the transition is completed
  • onLeave - a function that executes when the next transition is triggered (before onEnter)

Make transition

To make a transition, we need to call the transition method of the entity or use methods with the same name as the event. State changes will persist to the database by default.

const order = new Order();
await order.fsm.itemsStatus.create();
await order.fsm.itemsStatus.assemble();
await order.fsm.itemsStatus.transfer('Another warehouse');
await order.fsm.itemsStatus.ship();

We're passing the place argument to the transfer method. It will be passed to the guard and onExit functions.

Dynamic add transitions

We can add transition dynamically using the addTransition method.

orderItemFSM.addTransition([
  t(
    OrderItemState.shipping,
    OrderItemEvent.transfer,
    OrderItemState.shipping,
    {
      guard(context: IOrderItemContext, place: string) {
        return context.data.place !== place;
      },
      onExit(context: IOrderItemContext, place: string) {
        context.data.place = place;
      },
    },
  ),
]);

Current state

You can get the current state of the state machine using the current property.

const order = new Order();
console.log(order.fsm.itemsStatus.current); // draft

Also you can use is + state name method to check the current state.

const order = new Order();
console.log(order.fsm.itemsStatus.isDraft()); // true

Also is(state: State) method is available.

Transition availability

You can check if the transition is available using the can + event name method.

const order = new Order();

console.log(order.fsm.itemsStatus.canCreate()); // true
await order.fsm.itemsStatus.create();
console.log(order.fsm.itemsStatus.canCreate()); // false
await order.fsm.itemsStatus.assemble();

Arguments are passed to the guard function.

await order.fsm.itemsStatus.transfer('Another warehouse');
console.log(order.fsm.itemsStatus.canTransfer('Another warehouse')); // false

Also can(event: Event, ...args) method is available.

Subscribers

You can subscribe to transition using the on method. And unsubscribe using the off method.

const order = new Order();

const subscriber = (state: OrderItemState) => {
  console.log(state);
};
order.fsm.itemsStatus.on(OrderItemEvent.create, subscriber);

await order.fsm.itemsStatus.create();

order.fsm.itemsStatus.off(OrderItemEvent.create, subscriber);

Lifecycle

The state machine has the following lifecycle methods in the order of execution:

- guard
- onLeave (from previous transition)
- onEnter
- transition
- subscribers
- onExit

Bound lifecycle methods

The entity instance will be bound to the lifecycle methods. You can access the entity instance using this keyword.

const order = new Order();

order.fsm.itemsStatus.onEnter(function (this: Order) {
  console.log(this.id);
});
order.fsm.itemStatus.on(OrderItemEvent.create, function (this: Order) {
  console.log(this.id);
});

await order.fsm.itemsStatus.create();

You also able to use bind method to bind your own this keyword to the function.

order.fsm.itemsStatus.on(function () {
  console.log(this.current);
}.bind({ current: 'test' }));

Error handling

Library throws StateMachineError if transition is not available. It can be caught using try/catch and checked using isStateMachineError function.

import { isStateMachineError } from 'typeorm-fsm';

try {
  await order.fsm.itemsStatus.create();
} catch (error) {
  if (isStateMachineError(error)) {
    console.log(error.message);
  }
}

Installation

npm install typeorm fsm-typeorm

Examples

Check out the examples directory for more examples.

Latest Changes

Take a look at the CHANGELOG for details about recent changes to the current version.

Thanks

This project was inspired by aasm and typescript-fsm.

And thank you for reading this far. I hope you find this library useful.