add documentation

This commit is contained in:
adlerhurst
2025-05-08 19:01:55 +02:00
parent 47e63ed801
commit c6db6dc4b7
27 changed files with 126 additions and 21 deletions

View File

@@ -7,15 +7,22 @@ import (
"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
@@ -95,6 +102,8 @@ func DefaultOpts(invoker Invoker) *CommandOpts {
}
}
// commandBatch is a batch of commands.
// It uses the [Invoker] provided by the opts to execute each command.
type commandBatch struct {
Commands []Commander
}

View File

@@ -6,6 +6,10 @@ import (
"github.com/zitadel/zitadel/backend/v3/storage/eventstore"
)
// CreateUserCommand adds a new user including the email verification for humans.
// In the future it might make sense to separate the command into two commands:
// - CreateHumanCommand: creates a new human user
// - CreateMachineCommand: creates a new machine user
type CreateUserCommand struct {
user *User
email *SetEmailCommand
@@ -16,6 +20,7 @@ var (
_ eventer = (*CreateUserCommand)(nil)
)
// opts heavily reduces the complexity for email verification because each type of verification is a simple option which implements the [Commander] interface.
func NewCreateHumanCommand(username string, opts ...CreateHumanOpt) *CreateUserCommand {
cmd := &CreateUserCommand{
user: &User{

View File

@@ -11,6 +11,10 @@ type generateCodeCommand struct {
value *crypto.CryptoValue
}
// I didn't update this repository to the solution proposed please view one of the following interfaces for correct usage:
// - [UserRepository]
// - [InstanceRepository]
// - [OrgRepository]
type CryptoRepository interface {
GetEncryptionConfig(ctx context.Context) (*crypto.GeneratorConfig, error)
}

View File

@@ -11,6 +11,8 @@ import (
"github.com/zitadel/zitadel/internal/crypto"
)
// The variables could also be moved to a struct.
// I just started with the singleton pattern and kept it like this.
var (
pool database.Pool
userCodeAlgorithm crypto.EncryptionAlgorithm

View File

@@ -19,6 +19,7 @@ import (
"github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
)
// These tests give an overview of how to use the domain package.
func TestExample(t *testing.T) {
ctx := context.Background()

View File

@@ -5,6 +5,7 @@ import (
"time"
)
// EmailVerifiedCommand verifies an email address for a user.
type EmailVerifiedCommand struct {
UserID string `json:"userId"`
Email *Email `json:"email"`
@@ -42,6 +43,8 @@ func (cmd *EmailVerifiedCommand) applyOnSetEmail(setEmailCmd *SetEmailCommand) {
setEmailCmd.verification = cmd
}
// SendCodeCommand sends a verification code to the user's email address.
// If the URLTemplate is not set it will use the default of the organization / instance.
type SendCodeCommand struct {
UserID string `json:"userId"`
Email string `json:"email"`
@@ -113,6 +116,8 @@ func (cmd *SendCodeCommand) applyOnSetEmail(setEmailCmd *SetEmailCommand) {
setEmailCmd.verification = cmd
}
// ReturnCodeCommand creates the code and returns it to the caller.
// The caller gets the code by calling the Code field after the command got executed.
type ReturnCodeCommand struct {
UserID string `json:"userId"`
Email string `json:"email"`

View File

@@ -33,6 +33,7 @@ func (i *Instance) Keys(index instanceCacheIndex) (key []string) {
var _ cache.Entry[instanceCacheIndex, string] = (*Instance)(nil)
// instanceColumns define all the columns of the instance table.
type instanceColumns interface {
// IDColumn returns the column for the id field.
IDColumn() database.Column
@@ -46,6 +47,7 @@ type instanceColumns interface {
DeletedAtColumn() database.Column
}
// instanceConditions define all the conditions for the instance table.
type instanceConditions interface {
// IDCondition returns an equal filter on the id field.
IDCondition(instanceID string) database.Condition
@@ -53,16 +55,19 @@ type instanceConditions interface {
NameCondition(op database.TextOperation, name string) database.Condition
}
// instanceChanges define all the changes for the instance table.
type instanceChanges interface {
// SetName sets the name column.
SetName(name string) database.Change
}
// InstanceRepository is the interface for the instance repository.
type InstanceRepository interface {
instanceColumns
instanceConditions
instanceChanges
// Member returns the member repository which is a sub repository of the instance repository.
Member() MemberRepository
Get(ctx context.Context, opts ...database.QueryOption) (*Instance, error)

View File

@@ -7,6 +7,10 @@ import (
"github.com/zitadel/zitadel/backend/v3/storage/eventstore"
)
// 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, and event publishing.
// If you want to invoke multiple commands in a single transaction, you can use the [commandBatch].
func Invoke(ctx context.Context, cmd Commander) error {
invoker := newEventStoreInvoker(newLoggingInvoker(newTraceInvoker(nil)))
opts := &CommandOpts{
@@ -16,6 +20,8 @@ func Invoke(ctx context.Context, cmd Commander) error {
return invoker.Invoke(ctx, cmd, opts)
}
// eventStoreInvoker checks if the command implements the [eventer] interface.
// If it does, it collects the events and publishes them to the event store.
type eventStoreInvoker struct {
collector *eventCollector
}
@@ -38,6 +44,7 @@ func (i *eventStoreInvoker) Invoke(ctx context.Context, command Commander, opts
return nil
}
// eventCollector collects events from all commands. The [eventStoreInvoker] pushes the collected events after all commands are executed.
type eventCollector struct {
next Invoker
events []*eventstore.Event
@@ -64,6 +71,7 @@ func (i *eventCollector) Invoke(ctx context.Context, command Commander, opts *Co
return command.Execute(ctx, opts)
}
// traceInvoker decorates each command with tracing.
type traceInvoker struct {
next Invoker
}
@@ -87,6 +95,8 @@ func (i *traceInvoker) Invoke(ctx context.Context, command Commander, opts *Comm
return command.Execute(ctx, opts)
}
// loggingInvoker decorates each command with logging.
// It is an example implementation and logs the command name at the beginning and success or failure after the command got executed.
type loggingInvoker struct {
next Invoker
}
@@ -123,6 +133,10 @@ func (i *noopInvoker) Invoke(ctx context.Context, command Commander, opts *Comma
return command.Execute(ctx, opts)
}
// cacheInvoker could be used in the future to do the caching.
// My goal would be to have two interfaces:
// - cacheSetter: which caches an object
// - cacheGetter: which gets an object from the cache, this should also skip the command execution
type cacheInvoker struct {
next Invoker
}

View File

@@ -15,6 +15,7 @@ const (
OrgStateInactive
)
// Org is used by all other packages to represent an organization.
type Org struct {
ID string `json:"id"`
Name string `json:"name"`
@@ -42,6 +43,7 @@ func (o *Org) Keys(index orgCacheIndex) (key []string) {
var _ cache.Entry[orgCacheIndex, string] = (*Org)(nil)
// orgColumns define all the columns of the org table.
type orgColumns interface {
// InstanceIDColumn returns the column for the instance id field.
InstanceIDColumn() database.Column
@@ -59,6 +61,7 @@ type orgColumns interface {
DeletedAtColumn() database.Column
}
// orgConditions define all the conditions for the org table.
type orgConditions interface {
// InstanceIDCondition returns an equal filter on the instance id field.
InstanceIDCondition(instanceID string) database.Condition
@@ -70,6 +73,7 @@ type orgConditions interface {
StateCondition(op database.NumberOperation, state OrgState) database.Condition
}
// orgChanges define all the changes for the org table.
type orgChanges interface {
// SetName sets the name column.
SetName(name string) database.Change
@@ -77,12 +81,14 @@ type orgChanges interface {
SetState(state OrgState) database.Change
}
// OrgRepository is the interface for the org repository.
// It is used to interact with the org table in the database.
type OrgRepository interface {
orgColumns
orgConditions
orgChanges
// Member returns the admin repository.
// Member returns the member repository.
Member() MemberRepository
// Domain returns the domain repository.
Domain() DomainRepository
@@ -99,19 +105,14 @@ type OrgRepository interface {
Update(ctx context.Context, condition database.Condition, changes ...database.Change) error
}
type OrgOperation interface {
MemberRepository
DomainRepository
Update(ctx context.Context, org *Org) error
Delete(ctx context.Context) error
}
// MemberRepository is a sub repository of the org repository and maybe the instance repository.
type MemberRepository interface {
AddMember(ctx context.Context, orgID, userID string, roles []string) error
SetMemberRoles(ctx context.Context, orgID, userID string, roles []string) error
RemoveMember(ctx context.Context, orgID, userID string) error
}
// DomainRepository is a sub repository of the org repository and maybe the instance repository.
type DomainRepository interface {
AddDomain(ctx context.Context, domain string) error
SetDomainVerified(ctx context.Context, domain string) error

View File

@@ -6,6 +6,8 @@ import (
"github.com/zitadel/zitadel/backend/v3/storage/eventstore"
)
// AddOrgCommand adds a new organization.
// I'm unsure if we should add the Admins here or if this should be a separate command.
type AddOrgCommand struct {
ID string `json:"id"`
Name string `json:"name"`
@@ -86,6 +88,8 @@ func (cmd *AddOrgCommand) ensureID() (err error) {
return err
}
// AddMemberCommand adds a new member to an organization.
// I'm not sure if we should make it more generic to also use it for instances.
type AddMemberCommand struct {
orgID string
UserID string `json:"userId"`

View File

@@ -6,6 +6,10 @@ import (
"github.com/zitadel/zitadel/backend/v3/storage/eventstore"
)
// SetEmailCommand sets the email address of a user.
// If allows verification as a sub command.
// The verification command is executed after the email address is set.
// The verification command is executed in the same transaction as the email address update.
type SetEmailCommand struct {
UserID string `json:"userId"`
Email string `json:"email"`

View File

@@ -7,6 +7,7 @@ import (
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
// userColumns define all the columns of the user table.
type userColumns interface {
// InstanceIDColumn returns the column for the instance id field.
InstanceIDColumn() database.Column
@@ -24,6 +25,7 @@ type userColumns interface {
DeletedAtColumn() database.Column
}
// userConditions define all the conditions for the user table.
type userConditions interface {
// InstanceIDCondition returns an equal filter on the instance id field.
InstanceIDCondition(instanceID string) database.Condition
@@ -43,11 +45,13 @@ type userConditions interface {
DeletedAtCondition(op database.NumberOperation, deletedAt time.Time) database.Condition
}
// userChanges define all the changes for the user table.
type userChanges interface {
// SetUsername sets the username column.
SetUsername(username string) database.Change
}
// UserRepository is the interface for the user repository.
type UserRepository interface {
userColumns
userConditions
@@ -66,6 +70,7 @@ type UserRepository interface {
Machine() MachineRepository
}
// humanColumns define all the columns of the human table which inherits the user table.
type humanColumns interface {
userColumns
// FirstNameColumn returns the column for the first name field.
@@ -82,6 +87,7 @@ type humanColumns interface {
PhoneVerifiedAtColumn() database.Column
}
// humanConditions define all the conditions for the human table which inherits the user table.
type humanConditions interface {
userConditions
// FirstNameCondition returns a filter on the first name field.
@@ -103,6 +109,7 @@ type humanConditions interface {
PhoneVerifiedAtCondition(op database.NumberOperation, phoneVerifiedAt time.Time) database.Condition
}
// humanChanges define all the changes for the human table which inherits the user table.
type humanChanges interface {
userChanges
// SetFirstName sets the first name field of the human.
@@ -129,6 +136,7 @@ type humanChanges interface {
SetPhoneVerifiedAt(at time.Time) database.Change
}
// HumanRepository is the interface for the human repository it inherits the user repository.
type HumanRepository interface {
humanColumns
humanConditions
@@ -140,24 +148,28 @@ type HumanRepository interface {
Update(ctx context.Context, condition database.Condition, changes ...database.Change) error
}
// machineColumns define all the columns of the machine table which inherits the user table.
type machineColumns interface {
userColumns
// DescriptionColumn returns the column for the description field.
DescriptionColumn() database.Column
}
// machineConditions define all the conditions for the machine table which inherits the user table.
type machineConditions interface {
userConditions
// DescriptionCondition returns a filter on the description field.
DescriptionCondition(op database.TextOperation, description string) database.Condition
}
// machineChanges define all the changes for the machine table which inherits the user table.
type machineChanges interface {
userChanges
// SetDescription sets the description field of the machine.
SetDescription(description string) database.Change
}
// MachineRepository is the interface for the machine repository it inherits the user repository.
type MachineRepository interface {
// Update updates machine users based on the given condition and changes.
Update(ctx context.Context, condition database.Condition, changes ...database.Change) error
@@ -167,6 +179,7 @@ type MachineRepository interface {
machineChanges
}
// UserTraits is implemented by [Human] and [Machine].
type UserTraits interface {
Type() UserType
}