Skip to content

lithictech/sequel-state-machine

Repository files navigation

Gem Version Status

sequel-state-machine

sequel-state-machines is a pair of plugins that supercharge your usage of the Sequel ORM combined with the state_machines Gem.

The functionality it adds is:

  • Automatic audit logging of all state transitions, plus adhoc audit logging.
  • Helpers to isolate processing in a transaction with row locking.
  • Validation methods to ensure the state machine status column value is included in the state machine specification.
  • Accessors to keep track of when particular transitions happened (uses the audit log).
  • RSpec helpers to test that a transition does or does not happen.
  • Supports multiple state machines on the same model!

Usage

State machines have a main model and an audit log model. The audit log model requires a particular schema, as shown below:

class FundingTransaction < Sequel::Model(:funding_transactions)
  plugin :state_machine
  one_to_many :audit_logs, class: "FundingTransaction::AuditLog", order: Sequel.desc(:at)

  state_machine :status, initial: :created do
    state :created,
      :collecting,
      :cleared,
      :needs_review,
      :canceled

    event :collect_funds do
      transition created: :collecting
      transition collecting: :cleared, if: :funds_cleared?
    end

    event :cancel do
      transition [:created, :needs_review] => :canceled
    end
    event :put_into_review do
      transition (any - :needs_review) => :needs_review
    end
    event :remove_from_review do
      transition needs_review: :created
    end

    after_transition(&:commit_audit_log)
    after_failure(&:commit_audit_log)
  end

  timestamp_accessors(
    [
      [{to: "collecting"}, :funds_collecting_at],
      [{to: "cleared"}, :funds_cleared_at],
      [{to: "needs_review"}, :put_into_review_at],
      [{to: "canceled"}, :canceled_at],
    ],
  )
end

class FundingTransaction::AuditLog < Sequel::Model(:funding_transaction_audit_logs)
  plugin :state_machine_audit_log
  many_to_one :funding_transaction, class: "FundingTransaction"
end

# Table: funding_transaction_audit_logs
# ---------------------------------------------------------------------------------------------------------------------------------------
# Columns:
#  id                     | integer                  | PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY
#  at                     | timestamp with time zone | NOT NULL
#  event                  | text                     | NOT NULL
#  to_state               | text                     | NOT NULL
#  from_state             | text                     | NOT NULL
#  reason                 | text                     | NOT NULL DEFAULT ''::text
#  messages               | jsonb                    | DEFAULT '[]'::jsonb
#  funding_transaction_id | integer                  | NOT NULL
#  actor_id               | integer                  |

Then you can use it as below:

o = FundingTransaction.create
o.audit('New member', reason: 'fraud_detector')
o.process(:put_into_review)

# Someone reviews it
o.audit('Looks good')
o.process(:remove_from_review)

o.process(:collect_funds)
# expect(o).to transition_on(:collect_funds).to('collecting')

o.audit_logs
# {event: 'put_into_review', from_state: 'created', to_state: 'in_review', reason: 'fraud_detector'}
# {event: 'remove_from_review', from_state: 'in_review', to_state: 'created'}
# {event: 'collect_funds', from_state: 'created', to_state: 'collecting'}