serradura/u-case

Micro::Cases.flow(db_transaction: true)

serradura opened this issue ยท 8 comments

Wraps a Micro::Cases::Flow inside of an ActiveRecord::Transactions and if an exception happening or any step returns a failure, this flow will be halted because an ActiveRecord::Rollback will be raised.

MyFlow = Micro::Cases.flow([
  NormalizeParams,
  ValidatePassword,
  Micro::Cases.flow(db_transaction: true, steps: [
    CreateUser,
    CreateUserProfile
  ]),
  EnqueueIndexingJob
])

# ---

MyFlowWrappedInATransaction = Micro::Cases.flow(db_transaction: true, steps: [
  CreateUser,
  CeateUserProfile
])

MyFlow = Micro::Cases.flow([
  NormalizeParams,
  ValidatePassword,
  MyFlowWrappedInATransaction,
  EnqueueIndexingJob
])
class MyFlow < Micro::Case
  flow(db_transaction: true, steps: [
    NormalizeParams,
    ValidatePassword,
    CreateUser,
    CreateUserProfile
  ])
])

# ---

MyFlowWrappedInATransaction =
  Micro::Cases.flow(db_transaction: true, steps: [
    CreateUser,
    CeateUserProfile
  ])

class MyFlow < Micro::Case
  flow([
    NormalizeParams,
    ValidatePassword,
    MyFlowWrappedInATransaction
  ])
])

Definition of done:

  • This mode/plugin will be disabled by default, that is, it will be enabled to be available. Except for its core dependencies kind and u-attributes, this gem never will require any external dependency by default.

Thanks, @marlosirapuan, @josuetex, @marcosgz, @MatheusRich, @mrbongiolo for helping me to elaborate on this idea. ๐Ÿš€

why not just transaction? cause it's already a flow.. ๐Ÿค”

@marlosirapuan I'm not sure about the method name yet. But this mode could be enabled using a kwarg. e.g:

Micro::Cases.flow(transaction: true, [
  CreateUser,
  CeateUserProfile
])

# --

class MyCase < Micro::Case
  flow(transaction: true, [
  CreateUser,
  CeateUserProfile
])

What do you think? cc: @MatheusRich

To me flow_in_a_db_transaction is too much verbose.

An alternative:

MyFlowWrappedInATransaction = Micro::Cases.flow(db_transaction: true, [
  CreateUser,
  CeateUserProfile
])

@MatheusRich I liked the usage of db_transaction kwarg. Much better and explicit!

@serradura how about?

MyFlow = Micro::Cases.flow(transaction: :db, [])
# or
MyFlow = Micro::Cases.flow(transaction: Micro::Cases::DB_TRANSACTION, [])
# or
MyFlow = Micro::Cases.flow(transaction: MyCustomImplementation, [])

@mrbongiolo I can think of different types of transactions. But I would like to start solving this first one: DB transactions. As you proposed, I believe that if a new kind of transaction appears, we could make db_transaction: true become an alias for transaction: :db. But its an excellent suggestion. Thank you!

@serradura I totally agree with @mrbongiolo suggestion of :transaction naming: MyFlow = Micro::Cases.flow(transaction: MyCustomImplementation, [])

I've worked on applications that had more than one ActiveRecord or Sequel db connections. The transaction itself would not even need to be part of the project. And just provide mechanisms to group the a list of cases in a single transaction. And good examples in the documentation.

I don't think someone is using two ORM like this in same application. But a generic API would make that possible:

class SequelTransaction < Micro::Cases::Transaction
  def call!
    result = nil
    SequelDB.transaction do
      result = yield
      raise Sequel::Rollback if result.failure?
    end
    result
  end
end

class ActiveRecordTransaction < Micro::Cases::Transaction
  def call!
    result = nil
    ApplicationRecord.transaction do
      result = yield
      raise ActiveRecord::Rollback if result.failure?
    end
    result
  end
end

SignInFlow = Micro::Cases.flow(transaction: ActiveRecordTransaction, [CreateUser, CreateProfile])
SubscribeFlow = Micro::Cases.flow(transaction: SequelTransaction, [CreateOrder, CreateSubscription])

Ignore the syntax itself of this example. I'm talking the idea of a generic transaction.