command pattern

This commit is contained in:
adlerhurst
2025-03-26 16:08:02 +01:00
parent a41a998028
commit 77c4cc8185
21 changed files with 463 additions and 5 deletions

View File

@@ -0,0 +1,22 @@
package command
import "context"
type Command interface {
Execute(context.Context) error
Name() string
}
type Batch struct {
commands []Command
}
func (b *Batch) Execute(ctx context.Context) error {
for _, command := range b.commands {
if err := command.Execute(ctx); err != nil {
// TODO: undo?
return err
}
}
return nil
}

View File

@@ -0,0 +1 @@
package command

View File

@@ -0,0 +1,33 @@
package command
import (
"slices"
"github.com/zitadel/zitadel/backend/command/receiver"
)
type SetPrimaryDomain struct {
Domains []*receiver.Domain
Domain string
}
func (s *SetPrimaryDomain) Execute() error {
for domain := range slices.Values(s.Domains) {
domain.IsPrimary = domain.Name == s.Domain
}
return nil
}
type RemoveDomain struct {
Domains []*receiver.Domain
Domain string
}
func (r *RemoveDomain) Execute() error {
r.Domains = slices.DeleteFunc(r.Domains, func(domain *receiver.Domain) bool {
return domain.Name == r.Domain
})
return nil
}

View File

@@ -0,0 +1,97 @@
package command
import (
"context"
"github.com/zitadel/zitadel/backend/command/receiver"
)
type createInstance struct {
receiver receiver.InstanceManipulator
*receiver.Instance
}
func CreateInstance(receiver receiver.InstanceManipulator, instance *receiver.Instance) *createInstance {
return &createInstance{
Instance: instance,
receiver: receiver,
}
}
func (c *createInstance) Execute(ctx context.Context) error {
c.State = receiver.InstanceStateActive
return c.receiver.Create(ctx, c.Instance)
}
func (c *createInstance) Name() string {
return "CreateInstance"
}
type deleteInstance struct {
receiver receiver.InstanceManipulator
*receiver.Instance
}
func DeleteInstance(receiver receiver.InstanceManipulator, instance *receiver.Instance) *deleteInstance {
return &deleteInstance{
Instance: instance,
receiver: receiver,
}
}
func (d *deleteInstance) Execute(ctx context.Context) error {
return d.receiver.Delete(ctx, d.Instance)
}
func (c *deleteInstance) Name() string {
return "DeleteInstance"
}
type updateInstance struct {
receiver receiver.InstanceManipulator
*receiver.Instance
name string
}
func UpdateInstance(receiver receiver.InstanceManipulator, instance *receiver.Instance, name string) *updateInstance {
return &updateInstance{
Instance: instance,
receiver: receiver,
name: name,
}
}
func (u *updateInstance) Execute(ctx context.Context) error {
u.Instance.Name = u.name
// return u.receiver.(ctx, u.Instance)
return nil
}
func (c *updateInstance) Name() string {
return "UpdateInstance"
}
type addDomain struct {
receiver receiver.InstanceManipulator
*receiver.Instance
*receiver.Domain
}
func AddDomain(receiver receiver.InstanceManipulator, instance *receiver.Instance, domain *receiver.Domain) *addDomain {
return &addDomain{
Instance: instance,
Domain: domain,
receiver: receiver,
}
}
func (a *addDomain) Execute(ctx context.Context) error {
return a.receiver.AddDomain(ctx, a.Instance, a.Domain)
}
func (c *addDomain) Name() string {
return "AddDomain"
}

View File

@@ -0,0 +1,37 @@
package command
import (
"context"
"log/slog"
"time"
"github.com/zitadel/zitadel/backend/telemetry/logging"
)
type Logger struct {
level slog.Level
*logging.Logger
cmd Command
}
func Activity(l *logging.Logger, command Command) *Logger {
return &Logger{
Logger: l.With(slog.String("type", "activity")),
level: slog.LevelInfo,
cmd: command,
}
}
func (l *Logger) Execute(ctx context.Context) error {
start := time.Now()
log := l.Logger.With(slog.String("command", l.cmd.Name()))
log.InfoContext(ctx, "execute")
err := l.cmd.Execute(ctx)
log = log.With(slog.Duration("took", time.Since(start)))
if err != nil {
log.Log(ctx, l.level, "failed", slog.Any("cause", err))
return err
}
log.Log(ctx, l.level, "successful")
return nil
}

View File

@@ -0,0 +1,22 @@
package command
import (
"context"
"github.com/zitadel/zitadel/backend/telemetry/tracing"
)
type Trace struct {
command Command
tracer *tracing.Tracer
}
func (t *Trace) Execute(ctx context.Context) error {
ctx, span := t.tracer.Start(ctx, t.command.Name())
defer span.End()
err := t.command.Execute(ctx)
if err != nil {
span.RecordError(err)
}
return err
}

View File

@@ -0,0 +1,46 @@
package command
import "github.com/zitadel/zitadel/backend/command/receiver"
type ChangeUsername struct {
*receiver.User
Username string
}
func (c *ChangeUsername) Execute() error {
c.User.Username = c.Username
return nil
}
func (c *ChangeUsername) Name() string {
return "ChangeUsername"
}
type SetEmail struct {
*receiver.User
*receiver.Email
}
func (s *SetEmail) Execute() error {
s.User.Email = s.Email
return nil
}
func (s *SetEmail) Name() string {
return "SetEmail"
}
type SetPhone struct {
*receiver.User
*receiver.Phone
}
func (s *SetPhone) Execute() error {
s.User.Phone = s.Phone
return nil
}
func (s *SetPhone) Name() string {
return "SetPhone"
}

View File

@@ -0,0 +1,38 @@
package invoker
import (
"context"
"github.com/zitadel/zitadel/backend/command/command"
"github.com/zitadel/zitadel/backend/command/query"
"github.com/zitadel/zitadel/backend/command/receiver"
"github.com/zitadel/zitadel/backend/command/receiver/db"
"github.com/zitadel/zitadel/backend/storage/database"
)
type api struct {
db database.Pool
manipulator receiver.InstanceManipulator
reader receiver.InstanceReader
}
func (a *api) CreateInstance(ctx context.Context) error {
cmd := command.CreateInstance(db.NewInstance(a.db), &receiver.Instance{
ID: "123",
Name: "test",
})
return cmd.Execute(ctx)
}
func (a *api) DeleteInstance(ctx context.Context) error {
cmd := command.DeleteInstance(db.NewInstance(a.db), &receiver.Instance{
ID: "123",
})
return cmd.Execute(ctx)
}
func (a *api) InstanceByID(ctx context.Context) (*receiver.Instance, error) {
q := query.InstanceByID(a.reader, "123")
return q.Execute(ctx)
}

View File

@@ -0,0 +1,32 @@
package query
import (
"context"
"github.com/zitadel/zitadel/backend/command/receiver"
)
type instanceByID struct {
receiver receiver.InstanceReader
id string
}
// InstanceByID returns a new instanceByID query.
func InstanceByID(receiver receiver.InstanceReader, id string) *instanceByID {
return &instanceByID{
receiver: receiver,
id: id,
}
}
// Execute implements Query.
func (i *instanceByID) Execute(ctx context.Context) (*receiver.Instance, error) {
return i.receiver.ByID(ctx, i.id)
}
// Name implements Query.
func (i *instanceByID) Name() string {
return "instanceByID"
}
var _ Query[*receiver.Instance] = (*instanceByID)(nil)

View File

@@ -0,0 +1,8 @@
package query
import "context"
type Query[T any] interface {
Execute(ctx context.Context) (T, error)
Name() string
}

View File

@@ -0,0 +1,58 @@
package db
import (
"context"
"github.com/zitadel/zitadel/backend/command/receiver"
"github.com/zitadel/zitadel/backend/storage/database"
)
// NewInstance returns a new instance receiver.
func NewInstance(client database.QueryExecutor) receiver.InstanceManipulator {
return &instance{client: client}
}
// instance is the sql interface for instances.
type instance struct {
client database.QueryExecutor
}
// ByID implements receiver.InstanceReader.
func (i *instance) ByID(ctx context.Context, id string) (*receiver.Instance, error) {
var instance receiver.Instance
err := i.client.QueryRow(ctx, "SELECT id, name, state FROM instances WHERE id = $1", id).
Scan(
&instance.ID,
&instance.Name,
&instance.State,
)
if err != nil {
return nil, err
}
return &instance, nil
}
// AddDomain implements [receiver.InstanceManipulator].
func (i *instance) AddDomain(ctx context.Context, instance *receiver.Instance, domain *receiver.Domain) error {
return i.client.Exec(ctx, "INSERT INTO instance_domains (instance_id, domain, is_primary) VALUES ($1, $2, $3)", instance.ID, domain.Name, domain.IsPrimary)
}
// Create implements [receiver.InstanceManipulator].
func (i *instance) Create(ctx context.Context, instance *receiver.Instance) error {
return i.client.Exec(ctx, "INSERT INTO instances (id, name, state) VALUES ($1, $2, $3)", instance.ID, instance.Name, instance.State)
}
// Delete implements [receiver.InstanceManipulator].
func (i *instance) Delete(ctx context.Context, instance *receiver.Instance) error {
return i.client.Exec(ctx, "DELETE FROM instances WHERE id = $1", instance.ID)
}
// SetPrimaryDomain implements [receiver.InstanceManipulator].
func (i *instance) SetPrimaryDomain(ctx context.Context, instance *receiver.Instance, domain *receiver.Domain) error {
return i.client.Exec(ctx, "UPDATE instance_domains SET is_primary = domain = $1 WHERE instance_id = $2", domain.Name, instance.ID)
}
var (
_ receiver.InstanceManipulator = (*instance)(nil)
_ receiver.InstanceReader = (*instance)(nil)
)

View File

@@ -0,0 +1,6 @@
package receiver
type Domain struct {
Name string
IsPrimary bool
}

View File

@@ -0,0 +1,7 @@
package receiver
type Email struct {
Verifiable
Address string
}

View File

@@ -0,0 +1,28 @@
package receiver
import "context"
type InstanceState uint8
const (
InstanceStateActive InstanceState = iota
InstanceStateDeleted
)
type Instance struct {
ID string
Name string
State InstanceState
Domains []*Domain
}
type InstanceManipulator interface {
Create(ctx context.Context, instance *Instance) error
Delete(ctx context.Context, instance *Instance) error
AddDomain(ctx context.Context, instance *Instance, domain *Domain) error
SetPrimaryDomain(ctx context.Context, instance *Instance, domain *Domain) error
}
type InstanceReader interface {
ByID(ctx context.Context, id string) (*Instance, error)
}

View File

@@ -0,0 +1,7 @@
package receiver
type Phone struct {
Verifiable
Number string
}

View File

@@ -0,0 +1,9 @@
package receiver
type User struct {
ID string
Username string
Email *Email
Phone *Phone
}

View File

@@ -0,0 +1,8 @@
package receiver
import "github.com/zitadel/zitadel/internal/crypto"
type Verifiable struct {
IsVerified bool
Code *crypto.CryptoValue
}

View File

@@ -21,7 +21,7 @@ type Transaction interface {
Rollback(ctx context.Context) error
End(ctx context.Context, err error) error
Begin(ctx context.Context, opts *TransactionOptions) (Transaction, error)
Begin(ctx context.Context) (Transaction, error)
QueryExecutor
}

View File

@@ -52,7 +52,7 @@ func (tx *sqlTx) Exec(ctx context.Context, sql string, args ...any) error {
// Begin implements [database.Transaction].
// it is unimplemented
func (tx *sqlTx) Begin(ctx context.Context, opts *database.TransactionOptions) (database.Transaction, error) {
func (tx *sqlTx) Begin(ctx context.Context) (database.Transaction, error) {
return nil, errors.New("nested transactions are not supported")
}

View File

@@ -53,8 +53,7 @@ func (tx *pgxTx) Exec(ctx context.Context, sql string, args ...any) error {
// Begin implements [database.Transaction].
// As postgres does not support nested transactions we use savepoints to emulate them.
// TransactionOptions are ignored as savepoints do not support changing isolation levels.
func (tx *pgxTx) Begin(ctx context.Context, _ *database.TransactionOptions) (database.Transaction, error) {
func (tx *pgxTx) Begin(ctx context.Context) (database.Transaction, error) {
savepoint, err := tx.Tx.Begin(ctx)
if err != nil {
return nil, err

View File

@@ -120,7 +120,7 @@ func (tx *Transaction) Exec(ctx context.Context, stmt string, args ...any) error
// Begin implements [database.Transaction].
// it is unimplemented
func (tx *Transaction) Begin(ctx context.Context, opts *database.TransactionOptions) (database.Transaction, error) {
func (tx *Transaction) Begin(ctx context.Context) (database.Transaction, error) {
return nil, errors.New("nested transactions are not supported")
}