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!
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'}