Skip to content

Commit

Permalink
feat: make possible to pass from as array
Browse files Browse the repository at this point in the history
  • Loading branch information
bondiano committed Aug 30, 2023
1 parent 0b48d73 commit 43f319f
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 18 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Let's create a basic order state machine to showcase the features of the library
stateDiagram-v2
draft --> assembly: create
assembly --> warehouse: assemble
assembly --> shipping: ship
warehouse --> warehouse: transfer
warehouse --> shipping: ship
shipping --> delivered: deliver
Expand Down Expand Up @@ -90,7 +91,7 @@ class Order extends StateMachineEntity({
context.place = place;
},
},
t(OrderItemState.warehouse, OrderItemEvent.ship, OrderItemState.shipping),
t([OrderItemState.assembly, OrderItemState.warehouse], OrderItemEvent.ship, OrderItemState.shipping),
t(
OrderItemState.shipping,
OrderItemEvent.deliver,
Expand Down
6 changes: 5 additions & 1 deletion src/__tests__/fsm.entity.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ class Order extends StateMachineEntity({
context.place = place;
},
},
t(OrderItemState.warehouse, OrderItemEvent.ship, OrderItemState.shipping),
t(
[OrderItemState.assembly, OrderItemState.warehouse],
OrderItemEvent.ship,
OrderItemState.shipping,
),
t(
OrderItemState.shipping,
OrderItemEvent.deliver,
Expand Down
12 changes: 12 additions & 0 deletions src/__tests__/fsm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ describe('StateMachine', () => {
expect(stateMachine.current).toBe(State.idle);
});

it('should be possible to pass array of from', () => {
const stateMachine = new StateMachine({
initial: State.pending,
transitions: [
t([State.idle, State.pending], Event.fetch, State.pending),
t(State.pending, Event.resolve, State.idle),
],
});

expect(stateMachine.current).toBe(State.pending);
});

describe('transition', () => {
it('should change current state', async () => {
const stateMachine = new StateMachine({
Expand Down
40 changes: 39 additions & 1 deletion src/fsm.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export interface IStateMachineEntityColumnParameters<
State,
Event,
Context,
Array<ITransition<State, Event, any>>
ITransition<State, Event, any>,
[ITransition<State, Event, any>, ...Array<ITransition<State, Event, any>>]
> {
persistContext?: boolean;
/**
Expand Down Expand Up @@ -99,6 +100,11 @@ function initializeStateMachine<
ctx,
} = parameters;

if (!Array.isArray(transitions) || transitions.length === 0) {
throw new Error('Transitions are not defined');
}

// @ts-expect-error - we're using variadic tuple
parameters.transitions = transitions.map((transition) => {
return {
...transition,
Expand Down Expand Up @@ -150,6 +156,38 @@ function initializeStateMachine<
};
}

/**
* Mixin to extend your entity with state machine. Extends BaseEntity.
* @param parameters - state machine parameters
*
* @example
* import { StateMachineEntity, t } from 'typeorm-fsm';
*
* enum OrderState {
* draft = 'draft',
* pending = 'pending',
* paid = 'paid',
* completed = 'completed',
* }
*
* enum OrderEvent {
* create = 'create',
* pay = 'pay',
* complete = 'complete',
* }
*
* @Entity()
* class Order extends StateMachineEntity({
* status: {
* id: 'orderStatus',
* initial: OrderState.draft,
* transitions: [
* t(OrderState.draft, OrderEvent.create, OrderState.pending),
* t(OrderState.pending, OrderEvent.pay, OrderState.paid),
* t(OrderState.paid, OrderEvent.complete, OrderState.completed),
* ],
* }}) {}
*/
export const StateMachineEntity = function <
Parameters extends {
[Column in Columns]: IStateMachineEntityColumnParameters<
Expand Down
43 changes: 31 additions & 12 deletions src/fsm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ import { StateMachineError } from './fsm.error';
import { AllowedNames, Guard, Callback, ITransition } from './types';

export interface IStateMachineParameters<
State extends AllowedNames,
State extends AllowedNames | Array<AllowedNames>,
Event extends AllowedNames,
Context extends object,
Transitions extends Array<ITransition<State, Event, Context>> = Array<
ITransition<State, Event, Context>
Transition extends ITransition<State, Event, Context> = ITransition<
State,
Event,
Context
>,
Transitions extends [Transition, ...Array<Transition>] = [
Transition,
...Array<Transition>,
],
> {
ctx?: Context;
initial: State;
Expand All @@ -28,7 +34,7 @@ type StateMachineTransitionCheckers<Event extends AllowedNames> = {
/**
* @param arguments_ - Arguments to pass to guard.
*/
[key in `can${CapitalizeString<Event>}`]: () => boolean;
[key in `can${CapitalizeString<Event>}`]: () => Promise<boolean>;
};

type StateMachineCheckers<State extends AllowedNames> = {
Expand Down Expand Up @@ -69,16 +75,17 @@ function capitalize(parameter: unknown) {
/**
* Creates a new transition.
* @param from - From state.
* @param from[] - From states.
* @param event - Event name.
* @param to - To state.
* @param guard - Guard function.
*/
export function t<
Context extends object,
State extends AllowedNames,
Event extends AllowedNames,
Context extends object,
>(
from: State,
from: Array<State> | State,
event: Event,
to: State,
guard?: Guard<Context>,
Expand Down Expand Up @@ -308,10 +315,15 @@ export class _StateMachine<
) {
return transitions.reduce((accumulator, transition) => {
const { from, event } = transition;
if (!accumulator.has(from)) {
accumulator.set(from, new Set<Event>());
const froms = Array.isArray(from) ? from : [from];

for (const from of froms) {
if (!accumulator.has(from)) {
accumulator.set(from, new Set<Event>());
}

accumulator.get(from)?.add(event);
}
accumulator.get(from)?.add(event);

return accumulator;
}, new Map<State, Set<Event>>());
Expand All @@ -322,13 +334,19 @@ export class _StateMachine<
) {
return transitions.reduce((accumulator, transition) => {
const { from, event } = transition;

const froms = Array.isArray(from) ? from : [from];

if (!accumulator.has(event)) {
accumulator.set(
event,
new Map<State, ITransition<State, Event, Context>>(),
);
}
accumulator.get(event)?.set(from, this.bindToCallbacks(transition));

for (const from of froms) {
accumulator.get(event)?.set(from, this.bindToCallbacks(transition));
}

return accumulator;
}, new Map<Event, Map<State, ITransition<State, Event, Context>>>());
Expand Down Expand Up @@ -425,6 +443,7 @@ export class _StateMachine<
* @param parameters.ctx - Context object.
* @param parameters.transitions - Transitions.
* @param parameters.transitions[].from - From state.
* @param parameters.transitions[].from[] - From states.
* @param parameters.transitions[].event - Event name.
* @param parameters.transitions[].to - To state.
* @param parameters.transitions[].onEnter - Callback to execute on enter.
Expand All @@ -444,8 +463,8 @@ export class _StateMachine<
* @returns New state machine.
*/
export const StateMachine = function <
State extends AllowedNames,
Event extends AllowedNames,
const State extends AllowedNames,
const Event extends AllowedNames,
Context extends object,
>(
this: _StateMachine<State, Event, Context>,
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { StateMachine, t } from './fsm';
export { StateMachine, t, IStateMachine } from './fsm';
export type { ITransition } from './types';
export { StateMachineEntity } from './fsm.entity';
export { isStateMachineError } from './fsm.error';
4 changes: 2 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ export type Guard<Context extends object, T extends Array<any> = Array<any>> =
| ((context: Context, ...arguments_: T) => Promise<boolean>);

export interface ITransition<
State extends AllowedNames,
State extends AllowedNames | Array<AllowedNames>,
Event extends AllowedNames,
Context extends object,
> {
from: State;
from: Array<State> | State;
event: Event;
to: State;
onEnter?: Callback<Context>;
Expand Down

0 comments on commit 43f319f

Please sign in to comment.