package domain import ( "context" "fmt" "github.com/zitadel/zitadel/backend/v3/storage/database" ) // Commander is the all it needs 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. type Commander interface { Execute(ctx context.Context, opts *CommandOpts) (err error) fmt.Stringer } // 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 Commander, opts *CommandOpts) error } // CommandOpts are passed to each command // the provide common fields used by commands like the database client. type CommandOpts struct { DB database.QueryExecutor Invoker Invoker } type ensureTxOpts struct { *database.TransactionOptions } type EnsureTransactionOpt func(*ensureTxOpts) // EnsureTx ensures that the DB is a transaction. If it is not, it will start a new transaction. // The returned close function will end the transaction. If the DB is already a transaction, the close function // will do nothing because another [Commander] is already responsible for ending the transaction. func (o *CommandOpts) EnsureTx(ctx context.Context, opts ...EnsureTransactionOpt) (close func(context.Context, error) error, err error) { beginner, ok := o.DB.(database.Beginner) if !ok { // db is already a transaction return func(_ context.Context, err error) error { return err }, nil } txOpts := &ensureTxOpts{ TransactionOptions: new(database.TransactionOptions), } for _, opt := range opts { opt(txOpts) } tx, err := beginner.Begin(ctx, txOpts.TransactionOptions) if err != nil { return nil, err } o.DB = tx return func(ctx context.Context, err error) error { return tx.End(ctx, err) }, nil } // EnsureClient ensures that the o.DB is a client. If it is not, it will get a new client from the [database.Pool]. // The returned close function will release the client. If the o.DB is already a client or transaction, the close function // will do nothing because another [Commander] is already responsible for releasing the client. func (o *CommandOpts) EnsureClient(ctx context.Context) (close func(_ context.Context) error, err error) { pool, ok := o.DB.(database.Pool) if !ok { // o.DB is already a client return func(_ context.Context) error { return nil }, nil } client, err := pool.Acquire(ctx) if err != nil { return nil, err } o.DB = client return func(ctx context.Context) error { return client.Release(ctx) }, nil } func (o *CommandOpts) Invoke(ctx context.Context, command Commander) error { if o.Invoker == nil { return command.Execute(ctx, o) } return o.Invoker.Invoke(ctx, command, o) } func DefaultOpts(invoker Invoker) *CommandOpts { if invoker == nil { invoker = &noopInvoker{} } return &CommandOpts{ DB: pool, Invoker: invoker, } } // commandBatch is a batch of commands. // It uses the [Invoker] provided by the opts to execute each command. type commandBatch struct { Commands []Commander } func BatchCommands(cmds ...Commander) *commandBatch { return &commandBatch{ Commands: cmds, } } // String implements [Commander]. func (cmd *commandBatch) String() string { return "commandBatch" } func (b *commandBatch) Execute(ctx context.Context, opts *CommandOpts) (err error) { for _, cmd := range b.Commands { if err = opts.Invoke(ctx, cmd); err != nil { return err } } return nil } var _ Commander = (*commandBatch)(nil)