Files
zitadel/backend/v3/domain/invoke.go
T
c4221a9c0f refactor: invoker implementations and clean up code (#10962)
This pull request refactors the command execution and transaction
handling logic within the `domain` package. The primary goal is to
simplify the invoker chain, clarify transaction management, and improve
the overall structure for better maintainability and readability.

### Key Changes:

- **Simplified Invoker Chain**: The previous chain of invokers has been
refactored. A new `transactionInvoker` is introduced to explicitly
manage database transactions for commands that require them. This
responsibility is no longer spread across different parts of the
invocation flow.
- **`Transactional` Interface**: A new `Transactional` interface has
been introduced. Commands that need to run within a database transaction
now implement this interface. This provides a clear and explicit
contract for transactional operations.
- **Consolidated Transaction Logic**: The logic for starting,
committing, and rolling back transactions is now centralized within the
`transactionInvoker` and the `eventStoreInvoker`. The `EnsureTx` method
on `InvokeOpts` has been removed, and transaction handling is now
managed automatically by the invocation framework.
- **Improved Event Handling**: The `eventStoreInvoker` and its
`eventCollector` have been rewritten to be more robust and to correctly
handle nested and batched commands, ensuring that all events are
collected and published within the same transaction.
- **Code Cleanup**: A significant amount of now-unused code, including
the old `invoker_test.go`, has been removed. Various comments and
variable declarations have also been cleaned up.
- **Updated Command Implementations**: All commands in the `domain`
package (e.g., `ActivateOrgCommand`, `DeactivateOrgCommand`,
`DeleteOrgCommand`, `UpdateOrgCommand`) have been updated to use the new
`Transactional` interface where appropriate, removing manual transaction
management.
- **Enhanced Testing**: Tests have been added and updated to cover the
new transaction and event handling logic, ensuring the refactoring is
sound.
- **Use `PrimaryKeyCondition`**: Use the
`repository.PrimaryKeyCondition(...)` method instead of using
`database.And(...)` where possible.

These changes lead to a cleaner, more decoupled architecture for command
handling in ZITADEL.

---------

Co-authored-by: Marco A. <[email protected]>
2025-10-27 14:04:06 +00:00

101 lines
3.0 KiB
Go

package domain
import (
"context"
"fmt"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
//go:generate mockgen -typed -package domainmock -destination ./mock/executor.mock.go . Executor
type Executor interface {
Execute(ctx context.Context, opts *InvokeOpts) (err error)
fmt.Stringer
}
// Commander is all that is needed to implement the command pattern.
// It is the interface all manipulations need to implement.
// If possible it should also be used for queries. We will find out if this is possible in the future.
//
//go:generate mockgen -typed -package domainmock -destination ./mock/commander.mock.go . Commander
type Commander interface {
Validator
EventProducer
Executor
}
// Querier used to query data.
//
//go:generate mockgen -typed -package domainmock -destination ./mock/querier.mock.go . Querier
type Querier[T any] interface {
Validator
Executor
// Result returns the result of the query.
// If `Execute` returns an error, the result is nil.
Result() T
}
// Invoke provides a way to execute commands within the domain package.
// It uses a chain of responsibility pattern to handle the command execution.
// The default chain includes logging, tracing, event publishing, transaction handling and validation.
// If you want to invoke multiple commands in a single transaction, you can use the [BatchExecutors] helper.
func Invoke(ctx context.Context, executor Executor, opts ...InvokeOpt) error {
invokeOpts := DefaultOpts(
NewLoggingInvoker(
NewTraceInvoker(
NewEventStoreInvoker(
NewTransactionInvoker(
NewValidatorInvoker(nil),
),
),
),
),
)
for _, opt := range opts {
opt(invokeOpts)
}
return invokeOpts.Invoke(ctx, executor)
}
// Invoker is part of the command pattern.
// It is the interface that is used to execute commands.
type Invoker interface {
Invoke(ctx context.Context, command Executor, opts *InvokeOpts) error
}
type invoker struct {
next Invoker
}
func (i invoker) execute(ctx context.Context, executor Executor, opts *InvokeOpts) error {
if i.next != nil {
return i.next.Invoke(ctx, executor, opts)
}
return executor.Execute(ctx, opts)
}
// ensureTx ensures that the InvokeOpts has a transaction.
//
// If [Commander] or [Querier] requires a transaction implement [Transactional]
// the close function ends the transaction and resets [InvokeOpts].db.
// If a new transaction is started, it returns a function to end the transaction.
// The caller is responsible to call the returned function to end the transaction.
// If no new transaction is started, the returned function is a no-op and still safe to call.
func (i invoker) ensureTx(ctx context.Context, opts *InvokeOpts) (close func(err error) error, err error) {
beginner, ok := opts.DB().(database.Beginner)
if !ok {
return func(err error) error { return err }, nil
}
previousDB := opts.DB()
tx, err := beginner.Begin(ctx, nil)
if err != nil {
return nil, err
}
opts.db = tx
return func(err error) error {
opts.db = previousDB
return tx.End(ctx, err)
}, nil
}