mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 18:07:31 +00:00
command pattern
This commit is contained in:
22
backend/command/command/command.go
Normal file
22
backend/command/command/command.go
Normal 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
|
||||
}
|
1
backend/command/command/database.go
Normal file
1
backend/command/command/database.go
Normal file
@@ -0,0 +1 @@
|
||||
package command
|
33
backend/command/command/domain.go
Normal file
33
backend/command/command/domain.go
Normal 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
|
||||
}
|
97
backend/command/command/instance.go
Normal file
97
backend/command/command/instance.go
Normal 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"
|
||||
}
|
37
backend/command/command/logging.go
Normal file
37
backend/command/command/logging.go
Normal 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
|
||||
}
|
22
backend/command/command/tracing.go
Normal file
22
backend/command/command/tracing.go
Normal 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
|
||||
}
|
46
backend/command/command/user.go
Normal file
46
backend/command/command/user.go
Normal 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"
|
||||
}
|
38
backend/command/invoker/api.go
Normal file
38
backend/command/invoker/api.go
Normal 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)
|
||||
}
|
32
backend/command/query/instance.go
Normal file
32
backend/command/query/instance.go
Normal 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)
|
8
backend/command/query/query.go
Normal file
8
backend/command/query/query.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package query
|
||||
|
||||
import "context"
|
||||
|
||||
type Query[T any] interface {
|
||||
Execute(ctx context.Context) (T, error)
|
||||
Name() string
|
||||
}
|
58
backend/command/receiver/db/instance.go
Normal file
58
backend/command/receiver/db/instance.go
Normal 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)
|
||||
)
|
6
backend/command/receiver/domain.go
Normal file
6
backend/command/receiver/domain.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package receiver
|
||||
|
||||
type Domain struct {
|
||||
Name string
|
||||
IsPrimary bool
|
||||
}
|
7
backend/command/receiver/email.go
Normal file
7
backend/command/receiver/email.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package receiver
|
||||
|
||||
type Email struct {
|
||||
Verifiable
|
||||
|
||||
Address string
|
||||
}
|
28
backend/command/receiver/instance.go
Normal file
28
backend/command/receiver/instance.go
Normal 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)
|
||||
}
|
7
backend/command/receiver/phone.go
Normal file
7
backend/command/receiver/phone.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package receiver
|
||||
|
||||
type Phone struct {
|
||||
Verifiable
|
||||
|
||||
Number string
|
||||
}
|
9
backend/command/receiver/user.go
Normal file
9
backend/command/receiver/user.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package receiver
|
||||
|
||||
type User struct {
|
||||
ID string
|
||||
Username string
|
||||
|
||||
Email *Email
|
||||
Phone *Phone
|
||||
}
|
8
backend/command/receiver/verifiable.go
Normal file
8
backend/command/receiver/verifiable.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package receiver
|
||||
|
||||
import "github.com/zitadel/zitadel/internal/crypto"
|
||||
|
||||
type Verifiable struct {
|
||||
IsVerified bool
|
||||
Code *crypto.CryptoValue
|
||||
}
|
@@ -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
|
||||
}
|
||||
|
@@ -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")
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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")
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user