We’ve been using dry-transactions a greenfield rewrite of our app for the last 6 months, and we’ve discovered several useful step types we created that I wanted to share with everyone else. I’d love to share the code for these, but I’m not sure how useful it’d be to others, since several of these had to be created as class methods, and we had to add a few things to a base transaction class.
The vast majority of our transaction & steps accept and return a symbol-keyed Hash object, and each step uses kwargs. Each transaction starts with a validate
step, and then our most common steps are merge
to build up the hash, and tap
to trigger side-effects that we still care about failures.
tap
It works similarly to tee
, in that on Success it will just return the input to the method and discard any return value. If the method returns a Failure, however, the step fails with that Failure.
tap :notify
def notify(message:)
FlakeyClient.new.post(message)
rescue FlakeyClient::Error => ex
Failure(ex)
end
validate
Takes a block, which is a dry-validation
schema. If the validation succeeds, it takes the output of the validation result as Success
, otherwise it wraps the validation errors in Failure
. For more details on validations, see “Validations” below. Also, unlike the built-in types, this step also has no corresponding method implementation.
validate do
required(:user).filled
required(:account).filled
end
authorize
Uses Pundit in a similar fashion to the controller helper to find a valid policy and authorize the user with it. It requires that its input be a symbol-key hash with a user:
and the object to be authorized. The name of this key is provided as the argument to the authorize
step. Also takes an optional argument for the policy query method to use.
authorize :message
merge
Since most of our steps take keyword-args, it is often useful for steps to add more kwargs to the hash to be passed into the next step. If the return value of the method is a hash, that hash is merged into the input hash as the step result. If the method returns another value, then the input hash gets a new key that matches the step name with the return value as the value. If the method returns nil
, then the step result is the input hash.
merge :user
merge :lookup_metadata
# input: { user_id: 42, name: "Chuck" }
def user(user_id:, **)
User.find(user_id)
end
# output: { user_id: 42, name: "Chuck", user: #<User id:42> }
def lookup_metadata(user:,.**)
resp = APIClient.get_email(user.client_id)
{ email: resp["user_email"], fists: resp["fists"]["items"].size }
end
# output: { user_id: 42, name: "Chuck", user: #<User id:42>, email: "chuck@example", fists: 2 }
use
Calls another transaction. When that transaction is Success and it returns a hash, it gets merged into the input, otherwise it returns the result.
use MyOtherTransaction
async
Works like use
, but instead of doing .call
on the argument, does .perform_later
. We’ve implemented the perform_later
method for our transactions so they’ll quack like any other ActiveJob.
async SendEmailJob
async NotifyFrontendTransaction