diff --git a/backend/command/client/grpc/api.go b/backend/command/client/grpc/api.go deleted file mode 100644 index 417aa0a9d0..0000000000 --- a/backend/command/client/grpc/api.go +++ /dev/null @@ -1,56 +0,0 @@ -package grpc - -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/cache" - "github.com/zitadel/zitadel/backend/storage/database" - "github.com/zitadel/zitadel/backend/telemetry/logging" - "github.com/zitadel/zitadel/backend/telemetry/tracing" -) - -type api struct { - db database.Pool - - manipulator receiver.InstanceManipulator - reader receiver.InstanceReader - tracer *tracing.Tracer - logger *logging.Logger - cache cache.Cache[receiver.InstanceIndex, string, *receiver.Instance] -} - -func (a *api) CreateInstance(ctx context.Context) error { - instance := &receiver.Instance{ - ID: "123", - Name: "test", - } - return command.Trace( - a.tracer, - command.SetCache(a.cache, - command.Activity(a.logger, command.CreateInstance(a.manipulator, instance)), - instance, - ), - ).Execute(ctx) -} - -func (a *api) DeleteInstance(ctx context.Context) error { - return command.Trace( - a.tracer, - command.DeleteCache(a.cache, - command.Activity( - a.logger, - command.DeleteInstance(a.manipulator, &receiver.Instance{ - ID: "123", - })), - receiver.InstanceByID, - "123", - )).Execute(ctx) -} - -func (a *api) InstanceByID(ctx context.Context) (*receiver.Instance, error) { - q := query.InstanceByID(a.reader, "123") - return q.Execute(ctx) -} diff --git a/backend/command/command/caching.go b/backend/command/command/caching.go deleted file mode 100644 index 880ce24ba0..0000000000 --- a/backend/command/command/caching.go +++ /dev/null @@ -1,102 +0,0 @@ -package command - -import ( - "context" - - "github.com/zitadel/zitadel/backend/command/receiver/cache" -) - -type setCache[I, K comparable, V cache.Entry[I, K]] struct { - cache cache.Cache[I, K, V] - command Command - entry V -} - -// SetCache decorates the command, if the command is executed without error it will set the cache entry. -func SetCache[I, K comparable, V cache.Entry[I, K]](cache cache.Cache[I, K, V], command Command, entry V) Command { - return &setCache[I, K, V]{ - cache: cache, - command: command, - entry: entry, - } -} - -var _ Command = (*setCache[any, any, cache.Entry[any, any]])(nil) - -// Execute implements [Command]. -func (s *setCache[I, K, V]) Execute(ctx context.Context) error { - if err := s.command.Execute(ctx); err != nil { - return err - } - s.cache.Set(ctx, s.entry) - return nil -} - -// Name implements [Command]. -func (s *setCache[I, K, V]) Name() string { - return s.command.Name() -} - -type deleteCache[I, K comparable, V cache.Entry[I, K]] struct { - cache cache.Cache[I, K, V] - command Command - index I - keys []K -} - -// DeleteCache decorates the command, if the command is executed without error it will delete the cache entry. -func DeleteCache[I, K comparable, V cache.Entry[I, K]](cache cache.Cache[I, K, V], command Command, index I, keys ...K) Command { - return &deleteCache[I, K, V]{ - cache: cache, - command: command, - index: index, - keys: keys, - } -} - -var _ Command = (*deleteCache[any, any, cache.Entry[any, any]])(nil) - -// Execute implements [Command]. -func (s *deleteCache[I, K, V]) Execute(ctx context.Context) error { - if err := s.command.Execute(ctx); err != nil { - return err - } - return s.cache.Delete(ctx, s.index, s.keys...) -} - -// Name implements [Command]. -func (s *deleteCache[I, K, V]) Name() string { - return s.command.Name() -} - -type invalidateCache[I, K comparable, V cache.Entry[I, K]] struct { - cache cache.Cache[I, K, V] - command Command - index I - keys []K -} - -// InvalidateCache decorates the command, if the command is executed without error it will invalidate the cache entry. -func InvalidateCache[I, K comparable, V cache.Entry[I, K]](cache cache.Cache[I, K, V], command Command, index I, keys ...K) Command { - return &invalidateCache[I, K, V]{ - cache: cache, - command: command, - index: index, - keys: keys, - } -} - -var _ Command = (*invalidateCache[any, any, cache.Entry[any, any]])(nil) - -// Execute implements [Command]. -func (s *invalidateCache[I, K, V]) Execute(ctx context.Context) error { - if err := s.command.Execute(ctx); err != nil { - return err - } - return s.cache.Invalidate(ctx, s.index, s.keys...) -} - -// Name implements [Command]. -func (s *invalidateCache[I, K, V]) Name() string { - return s.command.Name() -} diff --git a/backend/command/command/command.go b/backend/command/command/command.go deleted file mode 100644 index f0fd82e201..0000000000 --- a/backend/command/command/command.go +++ /dev/null @@ -1,31 +0,0 @@ -package command - -import ( - "context" - - "github.com/zitadel/zitadel/backend/command/receiver/cache" -) - -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 -} - -type CacheableCommand[I, K comparable, V cache.Entry[I, K]] interface { - Command - Entry() V -} diff --git a/backend/command/command/database.go b/backend/command/command/database.go deleted file mode 100644 index d47dcf0d9b..0000000000 --- a/backend/command/command/database.go +++ /dev/null @@ -1 +0,0 @@ -package command diff --git a/backend/command/command/domain.go b/backend/command/command/domain.go deleted file mode 100644 index ba6b596b8a..0000000000 --- a/backend/command/command/domain.go +++ /dev/null @@ -1,33 +0,0 @@ -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 -} diff --git a/backend/command/command/instance.go b/backend/command/command/instance.go deleted file mode 100644 index b5fd76b0b7..0000000000 --- a/backend/command/command/instance.go +++ /dev/null @@ -1,97 +0,0 @@ -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.Update(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" -} diff --git a/backend/command/command/logging.go b/backend/command/command/logging.go deleted file mode 100644 index a1b7512e4e..0000000000 --- a/backend/command/command/logging.go +++ /dev/null @@ -1,44 +0,0 @@ -package command - -import ( - "context" - "log/slog" - "time" - - "github.com/zitadel/zitadel/backend/telemetry/logging" -) - -type logger struct { - level slog.Level - *logging.Logger - cmd Command -} - -// Activity decorates the commands execute method with logging. -// It logs the command name, duration, and success or failure of the command. -func Activity(l *logging.Logger, command Command) Command { - return &logger{ - Logger: l.With(slog.String("type", "activity")), - level: slog.LevelInfo, - cmd: command, - } -} - -// Name implements [Command]. -func (l *logger) Name() string { - return l.cmd.Name() -} - -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 -} diff --git a/backend/command/command/tracing.go b/backend/command/command/tracing.go deleted file mode 100644 index 81a5735de6..0000000000 --- a/backend/command/command/tracing.go +++ /dev/null @@ -1,37 +0,0 @@ -package command - -import ( - "context" - - "github.com/zitadel/zitadel/backend/telemetry/tracing" -) - -type trace struct { - command Command - tracer *tracing.Tracer -} - -// Trace decorates the commands execute method with tracing. -// It creates a span with the command name and records any errors that occur during execution. -// The span is ended after the command is executed. -func Trace(tracer *tracing.Tracer, command Command) Command { - return &trace{ - command: command, - tracer: tracer, - } -} - -// Name implements [Command]. -func (l *trace) Name() string { - return l.command.Name() -} - -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 -} diff --git a/backend/command/command/user.go b/backend/command/command/user.go deleted file mode 100644 index aea56dc36b..0000000000 --- a/backend/command/command/user.go +++ /dev/null @@ -1,46 +0,0 @@ -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" -} diff --git a/backend/command/query/instance.go b/backend/command/query/instance.go deleted file mode 100644 index 13ee327d42..0000000000 --- a/backend/command/query/instance.go +++ /dev/null @@ -1,32 +0,0 @@ -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) diff --git a/backend/command/query/query.go b/backend/command/query/query.go deleted file mode 100644 index 733b372ac1..0000000000 --- a/backend/command/query/query.go +++ /dev/null @@ -1,8 +0,0 @@ -package query - -import "context" - -type Query[T any] interface { - Execute(ctx context.Context) (T, error) - Name() string -} diff --git a/backend/command/receiver/cache/cache.go b/backend/command/receiver/cache/cache.go deleted file mode 100644 index dc05208caa..0000000000 --- a/backend/command/receiver/cache/cache.go +++ /dev/null @@ -1,112 +0,0 @@ -// Package cache provides abstraction of cache implementations that can be used by zitadel. -package cache - -import ( - "context" - "time" - - "github.com/zitadel/logging" -) - -// Purpose describes which object types are stored by a cache. -type Purpose int - -//go:generate enumer -type Purpose -transform snake -trimprefix Purpose -const ( - PurposeUnspecified Purpose = iota - PurposeAuthzInstance - PurposeMilestones - PurposeOrganization - PurposeIdPFormCallback -) - -// Cache stores objects with a value of type `V`. -// Objects may be referred to by one or more indices. -// Implementations may encode the value for storage. -// This means non-exported fields may be lost and objects -// with function values may fail to encode. -// See https://pkg.go.dev/encoding/json#Marshal for example. -// -// `I` is the type by which indices are identified, -// typically an enum for type-safe access. -// Indices are defined when calling the constructor of an implementation of this interface. -// It is illegal to refer to an idex not defined during construction. -// -// `K` is the type used as key in each index. -// Due to the limitations in type constraints, all indices use the same key type. -// -// Implementations are free to use stricter type constraints or fixed typing. -type Cache[I, K comparable, V Entry[I, K]] interface { - // Get an object through specified index. - // An [IndexUnknownError] may be returned if the index is unknown. - // [ErrCacheMiss] is returned if the key was not found in the index, - // or the object is not valid. - Get(ctx context.Context, index I, key K) (V, bool) - - // Set an object. - // Keys are created on each index based in the [Entry.Keys] method. - // If any key maps to an existing object, the object is invalidated, - // regardless if the object has other keys defined in the new entry. - // This to prevent ghost objects when an entry reduces the amount of keys - // for a given index. - Set(ctx context.Context, value V) - - // Invalidate an object through specified index. - // Implementations may choose to instantly delete the object, - // defer until prune or a separate cleanup routine. - // Invalidated object are no longer returned from Get. - // It is safe to call Invalidate multiple times or on non-existing entries. - Invalidate(ctx context.Context, index I, key ...K) error - - // Delete one or more keys from a specific index. - // An [IndexUnknownError] may be returned if the index is unknown. - // The referred object is not invalidated and may still be accessible though - // other indices and keys. - // It is safe to call Delete multiple times or on non-existing entries - Delete(ctx context.Context, index I, key ...K) error - - // Truncate deletes all cached objects. - Truncate(ctx context.Context) error -} - -// Entry contains a value of type `V` to be cached. -// -// `I` is the type by which indices are identified, -// typically an enum for type-safe access. -// -// `K` is the type used as key in an index. -// Due to the limitations in type constraints, all indices use the same key type. -type Entry[I, K comparable] interface { - // Keys returns which keys map to the object in a specified index. - // May return nil if the index in unknown or when there are no keys. - Keys(index I) (key []K) -} - -type Connector int - -//go:generate enumer -type Connector -transform snake -trimprefix Connector -linecomment -text -const ( - // Empty line comment ensures empty string for unspecified value - ConnectorUnspecified Connector = iota // - ConnectorMemory - ConnectorPostgres - ConnectorRedis -) - -type Config struct { - Connector Connector - - // Age since an object was added to the cache, - // after which the object is considered invalid. - // 0 disables max age checks. - MaxAge time.Duration - - // Age since last use (Get) of an object, - // after which the object is considered invalid. - // 0 disables last use age checks. - LastUseAge time.Duration - - // Log allows logging of the specific cache. - // By default only errors are logged to stdout. - Log *logging.Config -} diff --git a/backend/command/receiver/cache/connector/connector.go b/backend/command/receiver/cache/connector/connector.go deleted file mode 100644 index 3cc5e852a6..0000000000 --- a/backend/command/receiver/cache/connector/connector.go +++ /dev/null @@ -1,49 +0,0 @@ -// Package connector provides glue between the [cache.Cache] interface and implementations from the connector sub-packages. -package connector - -import ( - "context" - "fmt" - - "github.com/zitadel/zitadel/backend/storage/cache" - "github.com/zitadel/zitadel/backend/storage/cache/connector/gomap" - "github.com/zitadel/zitadel/backend/storage/cache/connector/noop" -) - -type CachesConfig struct { - Connectors struct { - Memory gomap.Config - } - Instance *cache.Config - Milestones *cache.Config - Organization *cache.Config - IdPFormCallbacks *cache.Config -} - -type Connectors struct { - Config CachesConfig - Memory *gomap.Connector -} - -func StartConnectors(conf *CachesConfig) (Connectors, error) { - if conf == nil { - return Connectors{}, nil - } - return Connectors{ - Config: *conf, - Memory: gomap.NewConnector(conf.Connectors.Memory), - }, nil -} - -func StartCache[I ~int, K ~string, V cache.Entry[I, K]](background context.Context, indices []I, purpose cache.Purpose, conf *cache.Config, connectors Connectors) (cache.Cache[I, K, V], error) { - if conf == nil || conf.Connector == cache.ConnectorUnspecified { - return noop.NewCache[I, K, V](), nil - } - if conf.Connector == cache.ConnectorMemory && connectors.Memory != nil { - c := gomap.NewCache[I, K, V](background, indices, *conf) - connectors.Memory.Config.StartAutoPrune(background, c, purpose) - return c, nil - } - - return nil, fmt.Errorf("cache connector %q not enabled", conf.Connector) -} diff --git a/backend/command/receiver/cache/connector/gomap/connector.go b/backend/command/receiver/cache/connector/gomap/connector.go deleted file mode 100644 index a37055bd73..0000000000 --- a/backend/command/receiver/cache/connector/gomap/connector.go +++ /dev/null @@ -1,23 +0,0 @@ -package gomap - -import ( - "github.com/zitadel/zitadel/backend/storage/cache" -) - -type Config struct { - Enabled bool - AutoPrune cache.AutoPruneConfig -} - -type Connector struct { - Config cache.AutoPruneConfig -} - -func NewConnector(config Config) *Connector { - if !config.Enabled { - return nil - } - return &Connector{ - Config: config.AutoPrune, - } -} diff --git a/backend/command/receiver/cache/connector/gomap/gomap.go b/backend/command/receiver/cache/connector/gomap/gomap.go deleted file mode 100644 index d79e323801..0000000000 --- a/backend/command/receiver/cache/connector/gomap/gomap.go +++ /dev/null @@ -1,200 +0,0 @@ -package gomap - -import ( - "context" - "errors" - "log/slog" - "maps" - "os" - "sync" - "sync/atomic" - "time" - - "github.com/zitadel/zitadel/backend/storage/cache" -) - -type mapCache[I, K comparable, V cache.Entry[I, K]] struct { - config *cache.Config - indexMap map[I]*index[K, V] - logger *slog.Logger -} - -// NewCache returns an in-memory Cache implementation based on the builtin go map type. -// Object values are stored as-is and there is no encoding or decoding involved. -func NewCache[I, K comparable, V cache.Entry[I, K]](background context.Context, indices []I, config cache.Config) cache.PrunerCache[I, K, V] { - m := &mapCache[I, K, V]{ - config: &config, - indexMap: make(map[I]*index[K, V], len(indices)), - logger: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ - AddSource: true, - Level: slog.LevelError, - })), - } - if config.Log != nil { - m.logger = config.Log.Slog() - } - m.logger.InfoContext(background, "map cache logging enabled") - - for _, name := range indices { - m.indexMap[name] = &index[K, V]{ - config: m.config, - entries: make(map[K]*entry[V]), - } - } - return m -} - -func (c *mapCache[I, K, V]) Get(ctx context.Context, index I, key K) (value V, ok bool) { - i, ok := c.indexMap[index] - if !ok { - c.logger.ErrorContext(ctx, "map cache get", "err", cache.NewIndexUnknownErr(index), "index", index, "key", key) - return value, false - } - entry, err := i.Get(key) - if err == nil { - c.logger.DebugContext(ctx, "map cache get", "index", index, "key", key) - return entry.value, true - } - if errors.Is(err, cache.ErrCacheMiss) { - c.logger.InfoContext(ctx, "map cache get", "err", err, "index", index, "key", key) - return value, false - } - c.logger.ErrorContext(ctx, "map cache get", "err", cache.NewIndexUnknownErr(index), "index", index, "key", key) - return value, false -} - -func (c *mapCache[I, K, V]) Set(ctx context.Context, value V) { - now := time.Now() - entry := &entry[V]{ - value: value, - created: now, - } - entry.lastUse.Store(now.UnixMicro()) - - for name, i := range c.indexMap { - keys := value.Keys(name) - i.Set(keys, entry) - c.logger.DebugContext(ctx, "map cache set", "index", name, "keys", keys) - } -} - -func (c *mapCache[I, K, V]) Invalidate(ctx context.Context, index I, keys ...K) error { - i, ok := c.indexMap[index] - if !ok { - return cache.NewIndexUnknownErr(index) - } - i.Invalidate(keys) - c.logger.DebugContext(ctx, "map cache invalidate", "index", index, "keys", keys) - return nil -} - -func (c *mapCache[I, K, V]) Delete(ctx context.Context, index I, keys ...K) error { - i, ok := c.indexMap[index] - if !ok { - return cache.NewIndexUnknownErr(index) - } - i.Delete(keys) - c.logger.DebugContext(ctx, "map cache delete", "index", index, "keys", keys) - return nil -} - -func (c *mapCache[I, K, V]) Prune(ctx context.Context) error { - for name, index := range c.indexMap { - index.Prune() - c.logger.DebugContext(ctx, "map cache prune", "index", name) - } - return nil -} - -func (c *mapCache[I, K, V]) Truncate(ctx context.Context) error { - for name, index := range c.indexMap { - index.Truncate() - c.logger.DebugContext(ctx, "map cache truncate", "index", name) - } - return nil -} - -type index[K comparable, V any] struct { - mutex sync.RWMutex - config *cache.Config - entries map[K]*entry[V] -} - -func (i *index[K, V]) Get(key K) (*entry[V], error) { - i.mutex.RLock() - entry, ok := i.entries[key] - i.mutex.RUnlock() - if ok && entry.isValid(i.config) { - return entry, nil - } - return nil, cache.ErrCacheMiss -} - -func (c *index[K, V]) Set(keys []K, entry *entry[V]) { - c.mutex.Lock() - for _, key := range keys { - c.entries[key] = entry - } - c.mutex.Unlock() -} - -func (i *index[K, V]) Invalidate(keys []K) { - i.mutex.RLock() - for _, key := range keys { - if entry, ok := i.entries[key]; ok { - entry.invalid.Store(true) - } - } - i.mutex.RUnlock() -} - -func (c *index[K, V]) Delete(keys []K) { - c.mutex.Lock() - for _, key := range keys { - delete(c.entries, key) - } - c.mutex.Unlock() -} - -func (c *index[K, V]) Prune() { - c.mutex.Lock() - maps.DeleteFunc(c.entries, func(_ K, entry *entry[V]) bool { - return !entry.isValid(c.config) - }) - c.mutex.Unlock() -} - -func (c *index[K, V]) Truncate() { - c.mutex.Lock() - c.entries = make(map[K]*entry[V]) - c.mutex.Unlock() -} - -type entry[V any] struct { - value V - created time.Time - invalid atomic.Bool - lastUse atomic.Int64 // UnixMicro time -} - -func (e *entry[V]) isValid(c *cache.Config) bool { - if e.invalid.Load() { - return false - } - now := time.Now() - if c.MaxAge > 0 { - if e.created.Add(c.MaxAge).Before(now) { - e.invalid.Store(true) - return false - } - } - if c.LastUseAge > 0 { - lastUse := e.lastUse.Load() - if time.UnixMicro(lastUse).Add(c.LastUseAge).Before(now) { - e.invalid.Store(true) - return false - } - e.lastUse.CompareAndSwap(lastUse, now.UnixMicro()) - } - return true -} diff --git a/backend/command/receiver/cache/connector/gomap/gomap_test.go b/backend/command/receiver/cache/connector/gomap/gomap_test.go deleted file mode 100644 index 8ed4f0f30a..0000000000 --- a/backend/command/receiver/cache/connector/gomap/gomap_test.go +++ /dev/null @@ -1,329 +0,0 @@ -package gomap - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/zitadel/logging" - - "github.com/zitadel/zitadel/backend/storage/cache" -) - -type testIndex int - -const ( - testIndexID testIndex = iota - testIndexName -) - -var testIndices = []testIndex{ - testIndexID, - testIndexName, -} - -type testObject struct { - id string - names []string -} - -func (o *testObject) Keys(index testIndex) []string { - switch index { - case testIndexID: - return []string{o.id} - case testIndexName: - return o.names - default: - return nil - } -} - -func Test_mapCache_Get(t *testing.T) { - c := NewCache[testIndex, string, *testObject](context.Background(), testIndices, cache.Config{ - MaxAge: time.Second, - LastUseAge: time.Second / 4, - Log: &logging.Config{ - Level: "debug", - AddSource: true, - }, - }) - obj := &testObject{ - id: "id", - names: []string{"foo", "bar"}, - } - c.Set(context.Background(), obj) - - type args struct { - index testIndex - key string - } - tests := []struct { - name string - args args - want *testObject - wantOk bool - }{ - { - name: "ok", - args: args{ - index: testIndexID, - key: "id", - }, - want: obj, - wantOk: true, - }, - { - name: "miss", - args: args{ - index: testIndexID, - key: "spanac", - }, - want: nil, - wantOk: false, - }, - { - name: "unknown index", - args: args{ - index: 99, - key: "id", - }, - want: nil, - wantOk: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, ok := c.Get(context.Background(), tt.args.index, tt.args.key) - assert.Equal(t, tt.want, got) - assert.Equal(t, tt.wantOk, ok) - }) - } -} - -func Test_mapCache_Invalidate(t *testing.T) { - c := NewCache[testIndex, string, *testObject](context.Background(), testIndices, cache.Config{ - MaxAge: time.Second, - LastUseAge: time.Second / 4, - Log: &logging.Config{ - Level: "debug", - AddSource: true, - }, - }) - obj := &testObject{ - id: "id", - names: []string{"foo", "bar"}, - } - c.Set(context.Background(), obj) - err := c.Invalidate(context.Background(), testIndexName, "bar") - require.NoError(t, err) - got, ok := c.Get(context.Background(), testIndexID, "id") - assert.Nil(t, got) - assert.False(t, ok) -} - -func Test_mapCache_Delete(t *testing.T) { - c := NewCache[testIndex, string, *testObject](context.Background(), testIndices, cache.Config{ - MaxAge: time.Second, - LastUseAge: time.Second / 4, - Log: &logging.Config{ - Level: "debug", - AddSource: true, - }, - }) - obj := &testObject{ - id: "id", - names: []string{"foo", "bar"}, - } - c.Set(context.Background(), obj) - err := c.Delete(context.Background(), testIndexName, "bar") - require.NoError(t, err) - - // Shouldn't find object by deleted name - got, ok := c.Get(context.Background(), testIndexName, "bar") - assert.Nil(t, got) - assert.False(t, ok) - - // Should find object by other name - got, ok = c.Get(context.Background(), testIndexName, "foo") - assert.Equal(t, obj, got) - assert.True(t, ok) - - // Should find object by id - got, ok = c.Get(context.Background(), testIndexID, "id") - assert.Equal(t, obj, got) - assert.True(t, ok) -} - -func Test_mapCache_Prune(t *testing.T) { - c := NewCache[testIndex, string, *testObject](context.Background(), testIndices, cache.Config{ - MaxAge: time.Second, - LastUseAge: time.Second / 4, - Log: &logging.Config{ - Level: "debug", - AddSource: true, - }, - }) - - objects := []*testObject{ - { - id: "id1", - names: []string{"foo", "bar"}, - }, - { - id: "id2", - names: []string{"hello"}, - }, - } - for _, obj := range objects { - c.Set(context.Background(), obj) - } - // invalidate one entry - err := c.Invalidate(context.Background(), testIndexName, "bar") - require.NoError(t, err) - - err = c.(cache.Pruner).Prune(context.Background()) - require.NoError(t, err) - - // Other object should still be found - got, ok := c.Get(context.Background(), testIndexID, "id2") - assert.Equal(t, objects[1], got) - assert.True(t, ok) -} - -func Test_mapCache_Truncate(t *testing.T) { - c := NewCache[testIndex, string, *testObject](context.Background(), testIndices, cache.Config{ - MaxAge: time.Second, - LastUseAge: time.Second / 4, - Log: &logging.Config{ - Level: "debug", - AddSource: true, - }, - }) - objects := []*testObject{ - { - id: "id1", - names: []string{"foo", "bar"}, - }, - { - id: "id2", - names: []string{"hello"}, - }, - } - for _, obj := range objects { - c.Set(context.Background(), obj) - } - - err := c.Truncate(context.Background()) - require.NoError(t, err) - - mc := c.(*mapCache[testIndex, string, *testObject]) - for _, index := range mc.indexMap { - index.mutex.RLock() - assert.Len(t, index.entries, 0) - index.mutex.RUnlock() - } -} - -func Test_entry_isValid(t *testing.T) { - type fields struct { - created time.Time - invalid bool - lastUse time.Time - } - tests := []struct { - name string - fields fields - config *cache.Config - want bool - }{ - { - name: "invalid", - fields: fields{ - created: time.Now(), - invalid: true, - lastUse: time.Now(), - }, - config: &cache.Config{ - MaxAge: time.Minute, - LastUseAge: time.Second, - }, - want: false, - }, - { - name: "max age exceeded", - fields: fields{ - created: time.Now().Add(-(time.Minute + time.Second)), - invalid: false, - lastUse: time.Now(), - }, - config: &cache.Config{ - MaxAge: time.Minute, - LastUseAge: time.Second, - }, - want: false, - }, - { - name: "max age disabled", - fields: fields{ - created: time.Now().Add(-(time.Minute + time.Second)), - invalid: false, - lastUse: time.Now(), - }, - config: &cache.Config{ - LastUseAge: time.Second, - }, - want: true, - }, - { - name: "last use age exceeded", - fields: fields{ - created: time.Now().Add(-(time.Minute / 2)), - invalid: false, - lastUse: time.Now().Add(-(time.Second * 2)), - }, - config: &cache.Config{ - MaxAge: time.Minute, - LastUseAge: time.Second, - }, - want: false, - }, - { - name: "last use age disabled", - fields: fields{ - created: time.Now().Add(-(time.Minute / 2)), - invalid: false, - lastUse: time.Now().Add(-(time.Second * 2)), - }, - config: &cache.Config{ - MaxAge: time.Minute, - }, - want: true, - }, - { - name: "valid", - fields: fields{ - created: time.Now(), - invalid: false, - lastUse: time.Now(), - }, - config: &cache.Config{ - MaxAge: time.Minute, - LastUseAge: time.Second, - }, - want: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - e := &entry[any]{ - created: tt.fields.created, - } - e.invalid.Store(tt.fields.invalid) - e.lastUse.Store(tt.fields.lastUse.UnixMicro()) - got := e.isValid(tt.config) - assert.Equal(t, tt.want, got) - }) - } -} diff --git a/backend/command/receiver/cache/connector/noop/noop.go b/backend/command/receiver/cache/connector/noop/noop.go deleted file mode 100644 index e3cf69c8ec..0000000000 --- a/backend/command/receiver/cache/connector/noop/noop.go +++ /dev/null @@ -1,21 +0,0 @@ -package noop - -import ( - "context" - - "github.com/zitadel/zitadel/backend/storage/cache" -) - -type noop[I, K comparable, V cache.Entry[I, K]] struct{} - -// NewCache returns a cache that does nothing -func NewCache[I, K comparable, V cache.Entry[I, K]]() cache.Cache[I, K, V] { - return noop[I, K, V]{} -} - -func (noop[I, K, V]) Set(context.Context, V) {} -func (noop[I, K, V]) Get(context.Context, I, K) (value V, ok bool) { return } -func (noop[I, K, V]) Invalidate(context.Context, I, ...K) (err error) { return } -func (noop[I, K, V]) Delete(context.Context, I, ...K) (err error) { return } -func (noop[I, K, V]) Prune(context.Context) (err error) { return } -func (noop[I, K, V]) Truncate(context.Context) (err error) { return } diff --git a/backend/command/receiver/cache/connector_enumer.go b/backend/command/receiver/cache/connector_enumer.go deleted file mode 100644 index 7ea014db16..0000000000 --- a/backend/command/receiver/cache/connector_enumer.go +++ /dev/null @@ -1,98 +0,0 @@ -// Code generated by "enumer -type Connector -transform snake -trimprefix Connector -linecomment -text"; DO NOT EDIT. - -package cache - -import ( - "fmt" - "strings" -) - -const _ConnectorName = "memorypostgresredis" - -var _ConnectorIndex = [...]uint8{0, 0, 6, 14, 19} - -const _ConnectorLowerName = "memorypostgresredis" - -func (i Connector) String() string { - if i < 0 || i >= Connector(len(_ConnectorIndex)-1) { - return fmt.Sprintf("Connector(%d)", i) - } - return _ConnectorName[_ConnectorIndex[i]:_ConnectorIndex[i+1]] -} - -// An "invalid array index" compiler error signifies that the constant values have changed. -// Re-run the stringer command to generate them again. -func _ConnectorNoOp() { - var x [1]struct{} - _ = x[ConnectorUnspecified-(0)] - _ = x[ConnectorMemory-(1)] - _ = x[ConnectorPostgres-(2)] - _ = x[ConnectorRedis-(3)] -} - -var _ConnectorValues = []Connector{ConnectorUnspecified, ConnectorMemory, ConnectorPostgres, ConnectorRedis} - -var _ConnectorNameToValueMap = map[string]Connector{ - _ConnectorName[0:0]: ConnectorUnspecified, - _ConnectorLowerName[0:0]: ConnectorUnspecified, - _ConnectorName[0:6]: ConnectorMemory, - _ConnectorLowerName[0:6]: ConnectorMemory, - _ConnectorName[6:14]: ConnectorPostgres, - _ConnectorLowerName[6:14]: ConnectorPostgres, - _ConnectorName[14:19]: ConnectorRedis, - _ConnectorLowerName[14:19]: ConnectorRedis, -} - -var _ConnectorNames = []string{ - _ConnectorName[0:0], - _ConnectorName[0:6], - _ConnectorName[6:14], - _ConnectorName[14:19], -} - -// ConnectorString retrieves an enum value from the enum constants string name. -// Throws an error if the param is not part of the enum. -func ConnectorString(s string) (Connector, error) { - if val, ok := _ConnectorNameToValueMap[s]; ok { - return val, nil - } - - if val, ok := _ConnectorNameToValueMap[strings.ToLower(s)]; ok { - return val, nil - } - return 0, fmt.Errorf("%s does not belong to Connector values", s) -} - -// ConnectorValues returns all values of the enum -func ConnectorValues() []Connector { - return _ConnectorValues -} - -// ConnectorStrings returns a slice of all String values of the enum -func ConnectorStrings() []string { - strs := make([]string, len(_ConnectorNames)) - copy(strs, _ConnectorNames) - return strs -} - -// IsAConnector returns "true" if the value is listed in the enum definition. "false" otherwise -func (i Connector) IsAConnector() bool { - for _, v := range _ConnectorValues { - if i == v { - return true - } - } - return false -} - -// MarshalText implements the encoding.TextMarshaler interface for Connector -func (i Connector) MarshalText() ([]byte, error) { - return []byte(i.String()), nil -} - -// UnmarshalText implements the encoding.TextUnmarshaler interface for Connector -func (i *Connector) UnmarshalText(text []byte) error { - var err error - *i, err = ConnectorString(string(text)) - return err -} diff --git a/backend/command/receiver/cache/error.go b/backend/command/receiver/cache/error.go deleted file mode 100644 index b66b9447bf..0000000000 --- a/backend/command/receiver/cache/error.go +++ /dev/null @@ -1,29 +0,0 @@ -package cache - -import ( - "errors" - "fmt" -) - -type IndexUnknownError[I comparable] struct { - index I -} - -func NewIndexUnknownErr[I comparable](index I) error { - return IndexUnknownError[I]{index} -} - -func (i IndexUnknownError[I]) Error() string { - return fmt.Sprintf("index %v unknown", i.index) -} - -func (a IndexUnknownError[I]) Is(err error) bool { - if b, ok := err.(IndexUnknownError[I]); ok { - return a.index == b.index - } - return false -} - -var ( - ErrCacheMiss = errors.New("cache miss") -) diff --git a/backend/command/receiver/cache/pruner.go b/backend/command/receiver/cache/pruner.go deleted file mode 100644 index 959762d410..0000000000 --- a/backend/command/receiver/cache/pruner.go +++ /dev/null @@ -1,76 +0,0 @@ -package cache - -import ( - "context" - "math/rand" - "time" - - "github.com/jonboulle/clockwork" - "github.com/zitadel/logging" -) - -// Pruner is an optional [Cache] interface. -type Pruner interface { - // Prune deletes all invalidated or expired objects. - Prune(ctx context.Context) error -} - -type PrunerCache[I, K comparable, V Entry[I, K]] interface { - Cache[I, K, V] - Pruner -} - -type AutoPruneConfig struct { - // Interval at which the cache is automatically pruned. - // 0 or lower disables automatic pruning. - Interval time.Duration - - // Timeout for an automatic prune. - // It is recommended to keep the value shorter than AutoPruneInterval - // 0 or lower disables automatic pruning. - Timeout time.Duration -} - -func (c AutoPruneConfig) StartAutoPrune(background context.Context, pruner Pruner, purpose Purpose) (close func()) { - return c.startAutoPrune(background, pruner, purpose, clockwork.NewRealClock()) -} - -func (c *AutoPruneConfig) startAutoPrune(background context.Context, pruner Pruner, purpose Purpose, clock clockwork.Clock) (close func()) { - if c.Interval <= 0 { - return func() {} - } - background, cancel := context.WithCancel(background) - // randomize the first interval - timer := clock.NewTimer(time.Duration(rand.Int63n(int64(c.Interval)))) - go c.pruneTimer(background, pruner, purpose, timer) - return cancel -} - -func (c *AutoPruneConfig) pruneTimer(background context.Context, pruner Pruner, purpose Purpose, timer clockwork.Timer) { - defer func() { - if !timer.Stop() { - <-timer.Chan() - } - }() - - for { - select { - case <-background.Done(): - return - case <-timer.Chan(): - err := c.doPrune(background, pruner) - logging.OnError(err).WithField("purpose", purpose).Error("cache auto prune") - timer.Reset(c.Interval) - } - } -} - -func (c *AutoPruneConfig) doPrune(background context.Context, pruner Pruner) error { - ctx, cancel := context.WithCancel(background) - defer cancel() - if c.Timeout > 0 { - ctx, cancel = context.WithTimeout(background, c.Timeout) - defer cancel() - } - return pruner.Prune(ctx) -} diff --git a/backend/command/receiver/cache/pruner_test.go b/backend/command/receiver/cache/pruner_test.go deleted file mode 100644 index faaedeb88c..0000000000 --- a/backend/command/receiver/cache/pruner_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package cache - -import ( - "context" - "testing" - "time" - - "github.com/jonboulle/clockwork" - "github.com/stretchr/testify/assert" -) - -type testPruner struct { - called chan struct{} -} - -func (p *testPruner) Prune(context.Context) error { - p.called <- struct{}{} - return nil -} - -func TestAutoPruneConfig_startAutoPrune(t *testing.T) { - c := AutoPruneConfig{ - Interval: time.Second, - Timeout: time.Millisecond, - } - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - - pruner := testPruner{ - called: make(chan struct{}), - } - clock := clockwork.NewFakeClock() - close := c.startAutoPrune(ctx, &pruner, PurposeAuthzInstance, clock) - defer close() - clock.Advance(time.Second) - - select { - case _, ok := <-pruner.called: - assert.True(t, ok) - case <-ctx.Done(): - t.Fatal(ctx.Err()) - } -} diff --git a/backend/command/receiver/cache/purpose_enumer.go b/backend/command/receiver/cache/purpose_enumer.go deleted file mode 100644 index a93a978efb..0000000000 --- a/backend/command/receiver/cache/purpose_enumer.go +++ /dev/null @@ -1,90 +0,0 @@ -// Code generated by "enumer -type Purpose -transform snake -trimprefix Purpose"; DO NOT EDIT. - -package cache - -import ( - "fmt" - "strings" -) - -const _PurposeName = "unspecifiedauthz_instancemilestonesorganizationid_p_form_callback" - -var _PurposeIndex = [...]uint8{0, 11, 25, 35, 47, 65} - -const _PurposeLowerName = "unspecifiedauthz_instancemilestonesorganizationid_p_form_callback" - -func (i Purpose) String() string { - if i < 0 || i >= Purpose(len(_PurposeIndex)-1) { - return fmt.Sprintf("Purpose(%d)", i) - } - return _PurposeName[_PurposeIndex[i]:_PurposeIndex[i+1]] -} - -// An "invalid array index" compiler error signifies that the constant values have changed. -// Re-run the stringer command to generate them again. -func _PurposeNoOp() { - var x [1]struct{} - _ = x[PurposeUnspecified-(0)] - _ = x[PurposeAuthzInstance-(1)] - _ = x[PurposeMilestones-(2)] - _ = x[PurposeOrganization-(3)] - _ = x[PurposeIdPFormCallback-(4)] -} - -var _PurposeValues = []Purpose{PurposeUnspecified, PurposeAuthzInstance, PurposeMilestones, PurposeOrganization, PurposeIdPFormCallback} - -var _PurposeNameToValueMap = map[string]Purpose{ - _PurposeName[0:11]: PurposeUnspecified, - _PurposeLowerName[0:11]: PurposeUnspecified, - _PurposeName[11:25]: PurposeAuthzInstance, - _PurposeLowerName[11:25]: PurposeAuthzInstance, - _PurposeName[25:35]: PurposeMilestones, - _PurposeLowerName[25:35]: PurposeMilestones, - _PurposeName[35:47]: PurposeOrganization, - _PurposeLowerName[35:47]: PurposeOrganization, - _PurposeName[47:65]: PurposeIdPFormCallback, - _PurposeLowerName[47:65]: PurposeIdPFormCallback, -} - -var _PurposeNames = []string{ - _PurposeName[0:11], - _PurposeName[11:25], - _PurposeName[25:35], - _PurposeName[35:47], - _PurposeName[47:65], -} - -// PurposeString retrieves an enum value from the enum constants string name. -// Throws an error if the param is not part of the enum. -func PurposeString(s string) (Purpose, error) { - if val, ok := _PurposeNameToValueMap[s]; ok { - return val, nil - } - - if val, ok := _PurposeNameToValueMap[strings.ToLower(s)]; ok { - return val, nil - } - return 0, fmt.Errorf("%s does not belong to Purpose values", s) -} - -// PurposeValues returns all values of the enum -func PurposeValues() []Purpose { - return _PurposeValues -} - -// PurposeStrings returns a slice of all String values of the enum -func PurposeStrings() []string { - strs := make([]string, len(_PurposeNames)) - copy(strs, _PurposeNames) - return strs -} - -// IsAPurpose returns "true" if the value is listed in the enum definition. "false" otherwise -func (i Purpose) IsAPurpose() bool { - for _, v := range _PurposeValues { - if i == v { - return true - } - } - return false -} diff --git a/backend/command/receiver/db/instance.go b/backend/command/receiver/db/instance.go deleted file mode 100644 index 16122ea0b1..0000000000 --- a/backend/command/receiver/db/instance.go +++ /dev/null @@ -1,58 +0,0 @@ -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) -) diff --git a/backend/command/receiver/domain.go b/backend/command/receiver/domain.go deleted file mode 100644 index 29bcc59f0a..0000000000 --- a/backend/command/receiver/domain.go +++ /dev/null @@ -1,6 +0,0 @@ -package receiver - -type Domain struct { - Name string - IsPrimary bool -} diff --git a/backend/command/receiver/email.go b/backend/command/receiver/email.go deleted file mode 100644 index 2c16f2cb08..0000000000 --- a/backend/command/receiver/email.go +++ /dev/null @@ -1,7 +0,0 @@ -package receiver - -type Email struct { - Verifiable - - Address string -} diff --git a/backend/command/receiver/instance.go b/backend/command/receiver/instance.go deleted file mode 100644 index 728bc6773b..0000000000 --- a/backend/command/receiver/instance.go +++ /dev/null @@ -1,57 +0,0 @@ -package receiver - -import ( - "context" - - "github.com/zitadel/zitadel/backend/command/receiver/cache" -) - -type InstanceState uint8 - -const ( - InstanceStateActive InstanceState = iota - InstanceStateDeleted -) - -type Instance struct { - ID string - Name string - State InstanceState - Domains []*Domain -} - -type InstanceIndex uint8 - -var InstanceIndices = []InstanceIndex{ - InstanceByID, - InstanceByDomain, -} - -const ( - InstanceByID InstanceIndex = iota - InstanceByDomain -) - -var _ cache.Entry[InstanceIndex, string] = (*Instance)(nil) - -// Keys implements [cache.Entry]. -func (i *Instance) Keys(index InstanceIndex) (key []string) { - switch index { - case InstanceByID: - return []string{i.ID} - case InstanceByDomain: - return []string{i.Name} - } - return nil -} - -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) -} diff --git a/backend/command/receiver/phone.go b/backend/command/receiver/phone.go deleted file mode 100644 index b92d7bff6d..0000000000 --- a/backend/command/receiver/phone.go +++ /dev/null @@ -1,7 +0,0 @@ -package receiver - -type Phone struct { - Verifiable - - Number string -} diff --git a/backend/command/receiver/user.go b/backend/command/receiver/user.go deleted file mode 100644 index a2ea0695ee..0000000000 --- a/backend/command/receiver/user.go +++ /dev/null @@ -1,9 +0,0 @@ -package receiver - -type User struct { - ID string - Username string - - Email *Email - Phone *Phone -} diff --git a/backend/command/receiver/verifiable.go b/backend/command/receiver/verifiable.go deleted file mode 100644 index cf31d77fb8..0000000000 --- a/backend/command/receiver/verifiable.go +++ /dev/null @@ -1,8 +0,0 @@ -package receiver - -import "github.com/zitadel/zitadel/internal/crypto" - -type Verifiable struct { - IsVerified bool - Code *crypto.CryptoValue -} diff --git a/backend/command/v2/api/doc.go b/backend/command/v2/api/doc.go deleted file mode 100644 index f67198d466..0000000000 --- a/backend/command/v2/api/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// The API package implements the protobuf stubs - -// It uses the Chain of responsibility pattern to handle requests in a modular way -// It implements the client or invoker of the command pattern. -// The client is responsible for creating the concrete command and setting its receiver. -package api diff --git a/backend/command/v2/api/user/v2/email.go b/backend/command/v2/api/user/v2/email.go deleted file mode 100644 index 76e266e2c3..0000000000 --- a/backend/command/v2/api/user/v2/email.go +++ /dev/null @@ -1,35 +0,0 @@ -package userv2 - -import ( - "context" - - "github.com/muhlemmer/gu" - "github.com/zitadel/zitadel/backend/command/v2/domain" - "github.com/zitadel/zitadel/pkg/grpc/user/v2" -) - -func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp *user.SetEmailResponse, err error) { - request := &domain.SetUserEmail{ - UserID: req.GetUserId(), - Email: req.GetEmail(), - } - switch req.GetVerification().(type) { - case *user.SetEmailRequest_IsVerified: - request.IsVerified = gu.Ptr(req.GetIsVerified()) - case *user.SetEmailRequest_SendCode: - request.SendCode = &domain.SendCode{ - URLTemplate: req.GetSendCode().UrlTemplate, - } - case *user.SetEmailRequest_ReturnCode: - request.ReturnCode = new(domain.ReturnCode) - } - if err := s.domain.SetUserEmail(ctx, request); err != nil { - return nil, err - } - - response := new(user.SetEmailResponse) - if request.ReturnCode != nil { - response.VerificationCode = &request.ReturnCode.Code - } - return response, nil -} diff --git a/backend/command/v2/api/user/v2/server.go b/backend/command/v2/api/user/v2/server.go deleted file mode 100644 index 583149a6b6..0000000000 --- a/backend/command/v2/api/user/v2/server.go +++ /dev/null @@ -1,12 +0,0 @@ -package userv2 - -import ( - "go.opentelemetry.io/otel/trace" - - "github.com/zitadel/zitadel/backend/command/v2/domain" -) - -type Server struct { - tracer trace.Tracer - domain *domain.Domain -} diff --git a/backend/command/v2/domain/command/generate_code.go b/backend/command/v2/domain/command/generate_code.go deleted file mode 100644 index 7ec00bd2e2..0000000000 --- a/backend/command/v2/domain/command/generate_code.go +++ /dev/null @@ -1,41 +0,0 @@ -package command - -import ( - "context" - - "github.com/zitadel/zitadel/backend/command/v2/pattern" - "github.com/zitadel/zitadel/internal/crypto" -) - -type generateCode struct { - set func(code string) - generator pattern.Query[crypto.Generator] -} - -func GenerateCode(set func(code string), generator pattern.Query[crypto.Generator]) *generateCode { - return &generateCode{ - set: set, - generator: generator, - } -} - -var _ pattern.Command = (*generateCode)(nil) - -// Execute implements [pattern.Command]. -func (cmd *generateCode) Execute(ctx context.Context) error { - if err := cmd.generator.Execute(ctx); err != nil { - return err - } - value, code, err := crypto.NewCode(cmd.generator.Result()) - _ = value - if err != nil { - return err - } - cmd.set(code) - return nil -} - -// Name implements [pattern.Command]. -func (*generateCode) Name() string { - return "command.generate_code" -} diff --git a/backend/command/v2/domain/command/send_email_code.go b/backend/command/v2/domain/command/send_email_code.go deleted file mode 100644 index 9e752332b8..0000000000 --- a/backend/command/v2/domain/command/send_email_code.go +++ /dev/null @@ -1,41 +0,0 @@ -package command - -import ( - "context" - - "github.com/zitadel/zitadel/backend/command/v2/pattern" -) - -var _ pattern.Command = (*sendEmailCode)(nil) - -type sendEmailCode struct { - UserID string `json:"userId"` - Email string `json:"email"` - URLTemplate *string `json:"urlTemplate"` - code string `json:"-"` -} - -func SendEmailCode(userID, email string, urlTemplate *string) pattern.Command { - cmd := &sendEmailCode{ - UserID: userID, - Email: email, - URLTemplate: urlTemplate, - } - - return pattern.Batch(GenerateCode(cmd.SetCode, generateCode)) -} - -// Name implements [pattern.Command]. -func (c *sendEmailCode) Name() string { - return "user.v2.email.send_code" -} - -// Execute implements [pattern.Command]. -func (c *sendEmailCode) Execute(ctx context.Context) error { - // Implementation of the command execution - return nil -} - -func (c *sendEmailCode) SetCode(code string) { - c.code = code -} diff --git a/backend/command/v2/domain/command/set_email.go b/backend/command/v2/domain/command/set_email.go deleted file mode 100644 index 5e525d8ea3..0000000000 --- a/backend/command/v2/domain/command/set_email.go +++ /dev/null @@ -1,39 +0,0 @@ -package command - -import ( - "context" - - "github.com/zitadel/zitadel/backend/command/v2/storage/eventstore" -) - -var ( - _ eventstore.EventCommander = (*setEmail)(nil) -) - -type setEmail struct { - UserID string `json:"userId"` - Email string `json:"email"` -} - -func SetEmail(userID, email string) *setEmail { - return &setEmail{ - UserID: userID, - Email: email, - } -} - -// Event implements [eventstore.EventCommander]. -func (c *setEmail) Event() *eventstore.Event { - panic("unimplemented") -} - -// Name implements [pattern.Command]. -func (c *setEmail) Name() string { - return "user.v2.set_email" -} - -// Execute implements [pattern.Command]. -func (c *setEmail) Execute(ctx context.Context) error { - // Implementation of the command execution - return nil -} diff --git a/backend/command/v2/domain/command/verify_email.go b/backend/command/v2/domain/command/verify_email.go deleted file mode 100644 index 80ec2c1b04..0000000000 --- a/backend/command/v2/domain/command/verify_email.go +++ /dev/null @@ -1,32 +0,0 @@ -package command - -import ( - "context" - - "github.com/zitadel/zitadel/backend/command/v2/pattern" -) - -var _ pattern.Command = (*verifyEmail)(nil) - -type verifyEmail struct { - UserID string `json:"userId"` - Email string `json:"email"` -} - -func VerifyEmail(userID, email string) *verifyEmail { - return &verifyEmail{ - UserID: userID, - Email: email, - } -} - -// Name implements [pattern.Command]. -func (c *verifyEmail) Name() string { - return "user.v2.verify_email" -} - -// Execute implements [pattern.Command]. -func (c *verifyEmail) Execute(ctx context.Context) error { - // Implementation of the command execution - return nil -} diff --git a/backend/command/v2/domain/domain.go b/backend/command/v2/domain/domain.go deleted file mode 100644 index e6ee5d9c61..0000000000 --- a/backend/command/v2/domain/domain.go +++ /dev/null @@ -1,13 +0,0 @@ -package domain - -import ( - "github.com/zitadel/zitadel/backend/command/v2/storage/database" - "github.com/zitadel/zitadel/internal/crypto" - "go.opentelemetry.io/otel/trace" -) - -type Domain struct { - pool database.Pool - tracer trace.Tracer - userCodeAlg crypto.EncryptionAlgorithm -} diff --git a/backend/command/v2/domain/email.go b/backend/command/v2/domain/email.go deleted file mode 100644 index 14032e9d8e..0000000000 --- a/backend/command/v2/domain/email.go +++ /dev/null @@ -1,6 +0,0 @@ -package domain - -type Email struct { - Address string - Verified bool -} diff --git a/backend/command/v2/domain/query/encryption_generator.go b/backend/command/v2/domain/query/encryption_generator.go deleted file mode 100644 index dc9fe66056..0000000000 --- a/backend/command/v2/domain/query/encryption_generator.go +++ /dev/null @@ -1,42 +0,0 @@ -package query - -import ( - "context" - - "github.com/zitadel/zitadel/internal/crypto" -) - -type encryptionConfigReceiver interface { - GetEncryptionConfig(ctx context.Context) (*crypto.GeneratorConfig, error) -} - -type encryptionGenerator struct { - receiver encryptionConfigReceiver - algorithm crypto.EncryptionAlgorithm - - res crypto.Generator -} - -func QueryEncryptionGenerator(receiver encryptionConfigReceiver, algorithm crypto.EncryptionAlgorithm) *encryptionGenerator { - return &encryptionGenerator{ - receiver: receiver, - algorithm: algorithm, - } -} - -func (q *encryptionGenerator) Execute(ctx context.Context) error { - config, err := q.receiver.GetEncryptionConfig(ctx) - if err != nil { - return err - } - q.res = crypto.NewEncryptionGenerator(*config, q.algorithm) - return nil -} - -func (q *encryptionGenerator) Name() string { - return "query.encryption_generator" -} - -func (q *encryptionGenerator) Result() crypto.Generator { - return q.res -} diff --git a/backend/command/v2/domain/query/return_email_code.go b/backend/command/v2/domain/query/return_email_code.go deleted file mode 100644 index ae726fbe54..0000000000 --- a/backend/command/v2/domain/query/return_email_code.go +++ /dev/null @@ -1,38 +0,0 @@ -package query - -import ( - "context" - - "github.com/zitadel/zitadel/backend/command/v2/pattern" -) - -var _ pattern.Query[string] = (*returnEmailCode)(nil) - -type returnEmailCode struct { - UserID string `json:"userId"` - Email string `json:"email"` - code string `json:"-"` -} - -func ReturnEmailCode(userID, email string) *returnEmailCode { - return &returnEmailCode{ - UserID: userID, - Email: email, - } -} - -// Name implements [pattern.Command]. -func (c *returnEmailCode) Name() string { - return "user.v2.email.return_code" -} - -// Execute implements [pattern.Command]. -func (c *returnEmailCode) Execute(ctx context.Context) error { - // Implementation of the command execution - return nil -} - -// Result implements [pattern.Query]. -func (c *returnEmailCode) Result() string { - return c.code -} diff --git a/backend/command/v2/domain/query/user_by_id.go b/backend/command/v2/domain/query/user_by_id.go deleted file mode 100644 index 04a6b9ea77..0000000000 --- a/backend/command/v2/domain/query/user_by_id.go +++ /dev/null @@ -1,38 +0,0 @@ -package query - -import ( - "context" - - "github.com/zitadel/zitadel/backend/command/v2/domain" - "github.com/zitadel/zitadel/backend/command/v2/pattern" - "github.com/zitadel/zitadel/backend/command/v2/storage/database" -) - -type UserByIDQuery struct { - querier database.Querier - UserID string `json:"userId"` - res *domain.User -} - -var _ pattern.Query[*domain.User] = (*UserByIDQuery)(nil) - -// Name implements [pattern.Command]. -func (q *UserByIDQuery) Name() string { - return "user.v2.by_id" -} - -// Execute implements [pattern.Command]. -func (q *UserByIDQuery) Execute(ctx context.Context) error { - var res *domain.User - err := q.querier.QueryRow(ctx, "SELECT id, username, email FROM users WHERE id = $1", q.UserID).Scan(&res.ID, &res.Username, &res.Email.Address) - if err != nil { - return err - } - q.res = res - return nil -} - -// Result implements [pattern.Query]. -func (q *UserByIDQuery) Result() *domain.User { - return q.res -} diff --git a/backend/command/v2/domain/user.go b/backend/command/v2/domain/user.go deleted file mode 100644 index 9e4c3a6c87..0000000000 --- a/backend/command/v2/domain/user.go +++ /dev/null @@ -1,77 +0,0 @@ -package domain - -import ( - "context" - - "github.com/zitadel/zitadel/backend/command/v2/domain/command" - "github.com/zitadel/zitadel/backend/command/v2/domain/query" - "github.com/zitadel/zitadel/backend/command/v2/pattern" - "github.com/zitadel/zitadel/backend/command/v2/storage/database" - "github.com/zitadel/zitadel/backend/command/v2/telemetry/tracing" -) - -type User struct { - ID string - Username string - Email Email -} - -type SetUserEmail struct { - UserID string - Email string - - IsVerified *bool - ReturnCode *ReturnCode - SendCode *SendCode - - code string - client database.QueryExecutor -} - -func (e *SetUserEmail) SetCode(code string) { - e.code = code -} - -type ReturnCode struct { - // Code is the code to be sent to the user - Code string -} - -type SendCode struct { - // URLTemplate is the template for the URL that is rendered into the message - URLTemplate *string -} - -func (d *Domain) SetUserEmail(ctx context.Context, req *SetUserEmail) error { - batch := pattern.Batch( - tracing.Trace(d.tracer, command.SetEmail(req.UserID, req.Email)), - ) - - if req.IsVerified == nil { - batch.Append(command.GenerateCode( - req.SetCode, - query.QueryEncryptionGenerator( - database.Query(d.pool), - d.userCodeAlg, - ), - )) - } else { - batch.Append(command.VerifyEmail(req.UserID, req.Email)) - } - - // if !req.GetVerification().GetIsVerified() { - // batch. - - // switch req.GetVerification().(type) { - // case *user.SetEmailRequest_IsVerified: - // batch.Append(tracing.Trace(s.tracer, command.VerifyEmail(req.GetUserId(), req.GetEmail()))) - // case *user.SetEmailRequest_SendCode: - // batch.Append(tracing.Trace(s.tracer, command.SendEmailCode(req.GetUserId(), req.GetEmail(), req.GetSendCode().UrlTemplate))) - // case *user.SetEmailRequest_ReturnCode: - // batch.Append(tracing.Trace(s.tracer, query.ReturnEmailCode(req.GetUserId(), req.GetEmail()))) - // } - - // if err := batch.Execute(ctx); err != nil { - // return nil, err - // } -} diff --git a/backend/command/v2/pattern/command.go b/backend/command/v2/pattern/command.go deleted file mode 100644 index 787e05dc62..0000000000 --- a/backend/command/v2/pattern/command.go +++ /dev/null @@ -1,100 +0,0 @@ -package pattern - -import ( - "context" - - "github.com/zitadel/zitadel/backend/command/v2/storage/database" -) - -// Command implements the command pattern. -// It is used to encapsulate a request as an object, thereby allowing for parameterization of clients with queues, requests, and operations. -// The command pattern allows for the decoupling of the sender and receiver of a request. -// It is often used in conjunction with the invoker pattern, which is responsible for executing the command. -// The command pattern is a behavioral design pattern that turns a request into a stand-alone object. -// This object contains all the information about the request. -// The command pattern is useful for implementing undo/redo functionality, logging, and queuing requests. -// It is also useful for implementing the macro command pattern, which allows for the execution of a series of commands as a single command. -// The command pattern is also used in event-driven architectures, where events are encapsulated as commands. -type Command interface { - Execute(ctx context.Context) error - Name() string -} - -type Query[T any] interface { - Command - Result() T -} - -type Invoker struct{} - -// func bla() { -// sync.Pool{ -// New: func() any { -// return new(Invoker) -// }, -// } -// } - -type Transaction struct { - beginner database.Beginner - cmd Command - opts *database.TransactionOptions -} - -func (t *Transaction) Execute(ctx context.Context) error { - tx, err := t.beginner.Begin(ctx, t.opts) - if err != nil { - return err - } - defer func() { err = tx.End(ctx, err) }() - return t.cmd.Execute(ctx) -} - -func (t *Transaction) Name() string { - return t.cmd.Name() -} - -type batch struct { - Commands []Command -} - -func Batch(cmds ...Command) *batch { - return &batch{ - Commands: cmds, - } -} - -func (b *batch) Execute(ctx context.Context) error { - for _, cmd := range b.Commands { - if err := cmd.Execute(ctx); err != nil { - return err - } - } - return nil -} - -func (b *batch) Name() string { - return "batch" -} - -func (b *batch) Append(cmds ...Command) { - b.Commands = append(b.Commands, cmds...) -} - -type NoopCommand struct{} - -func (c *NoopCommand) Execute(_ context.Context) error { - return nil -} -func (c *NoopCommand) Name() string { - return "noop" -} - -type NoopQuery[T any] struct { - NoopCommand -} - -func (q *NoopQuery[T]) Result() T { - var zero T - return zero -} diff --git a/backend/command/v2/storage/database/config.go b/backend/command/v2/storage/database/config.go deleted file mode 100644 index d9aa99b869..0000000000 --- a/backend/command/v2/storage/database/config.go +++ /dev/null @@ -1,9 +0,0 @@ -package database - -import ( - "context" -) - -type Connector interface { - Connect(ctx context.Context) (Pool, error) -} diff --git a/backend/command/v2/storage/database/database.go b/backend/command/v2/storage/database/database.go deleted file mode 100644 index 6dddefe22c..0000000000 --- a/backend/command/v2/storage/database/database.go +++ /dev/null @@ -1,54 +0,0 @@ -package database - -import ( - "context" -) - -var ( - db *database -) - -type database struct { - connector Connector - pool Pool -} - -type Pool interface { - Beginner - QueryExecutor - - Acquire(ctx context.Context) (Client, error) - Close(ctx context.Context) error -} - -type Client interface { - Beginner - QueryExecutor - - Release(ctx context.Context) error -} - -type Querier interface { - Query(ctx context.Context, stmt string, args ...any) (Rows, error) - QueryRow(ctx context.Context, stmt string, args ...any) Row -} - -type Executor interface { - Exec(ctx context.Context, stmt string, args ...any) error -} - -type Row interface { - Scan(dest ...any) error -} - -type Rows interface { - Row - Next() bool - Close() error - Err() error -} - -type QueryExecutor interface { - Querier - Executor -} diff --git a/backend/command/v2/storage/database/dialect/config.go b/backend/command/v2/storage/database/dialect/config.go deleted file mode 100644 index a044f7bd4e..0000000000 --- a/backend/command/v2/storage/database/dialect/config.go +++ /dev/null @@ -1,92 +0,0 @@ -package dialect - -import ( - "context" - "errors" - "reflect" - - "github.com/mitchellh/mapstructure" - "github.com/spf13/viper" - - "github.com/zitadel/zitadel/backend/storage/database" - "github.com/zitadel/zitadel/backend/storage/database/dialect/postgres" -) - -type Hook struct { - Match func(string) bool - Decode func(config any) (database.Connector, error) - Name string - Constructor func() database.Connector -} - -var hooks = []Hook{ - { - Match: postgres.NameMatcher, - Decode: postgres.DecodeConfig, - Name: postgres.Name, - Constructor: func() database.Connector { return new(postgres.Config) }, - }, - // { - // Match: gosql.NameMatcher, - // Decode: gosql.DecodeConfig, - // Name: gosql.Name, - // Constructor: func() database.Connector { return new(gosql.Config) }, - // }, -} - -type Config struct { - Dialects map[string]any `mapstructure:",remain" yaml:",inline"` - - connector database.Connector -} - -func (c Config) Connect(ctx context.Context) (database.Pool, error) { - if len(c.Dialects) != 1 { - return nil, errors.New("Exactly one dialect must be configured") - } - - return c.connector.Connect(ctx) -} - -// Hooks implements [configure.Unmarshaller]. -func (c Config) Hooks() []viper.DecoderConfigOption { - return []viper.DecoderConfigOption{ - viper.DecodeHook(decodeHook), - } -} - -func decodeHook(from, to reflect.Value) (_ any, err error) { - if to.Type() != reflect.TypeOf(Config{}) { - return from.Interface(), nil - } - - config := new(Config) - if err = mapstructure.Decode(from.Interface(), config); err != nil { - return nil, err - } - - if err = config.decodeDialect(); err != nil { - return nil, err - } - - return config, nil -} - -func (c *Config) decodeDialect() error { - for _, hook := range hooks { - for name, config := range c.Dialects { - if !hook.Match(name) { - continue - } - - connector, err := hook.Decode(config) - if err != nil { - return err - } - - c.connector = connector - return nil - } - } - return errors.New("no dialect found") -} diff --git a/backend/command/v2/storage/database/dialect/postgres/config.go b/backend/command/v2/storage/database/dialect/postgres/config.go deleted file mode 100644 index 1007c09542..0000000000 --- a/backend/command/v2/storage/database/dialect/postgres/config.go +++ /dev/null @@ -1,80 +0,0 @@ -package postgres - -import ( - "context" - "errors" - "slices" - "strings" - - "github.com/jackc/pgx/v5/pgxpool" - "github.com/mitchellh/mapstructure" - - "github.com/zitadel/zitadel/backend/command/v2/storage/database" -) - -var ( - _ database.Connector = (*Config)(nil) - Name = "postgres" -) - -type Config struct { - config *pgxpool.Config - - // Host string - // Port int32 - // Database string - // MaxOpenConns uint32 - // MaxIdleConns uint32 - // MaxConnLifetime time.Duration - // MaxConnIdleTime time.Duration - // User User - // // Additional options to be appended as options= - // // The value will be taken as is. Multiple options are space separated. - // Options string - - configuredFields []string -} - -// Connect implements [database.Connector]. -func (c *Config) Connect(ctx context.Context) (database.Pool, error) { - pool, err := pgxpool.NewWithConfig(ctx, c.config) - if err != nil { - return nil, err - } - if err = pool.Ping(ctx); err != nil { - return nil, err - } - return &pgxPool{pool}, nil -} - -func NameMatcher(name string) bool { - return slices.Contains([]string{"postgres", "pg"}, strings.ToLower(name)) -} - -func DecodeConfig(input any) (database.Connector, error) { - switch c := input.(type) { - case string: - config, err := pgxpool.ParseConfig(c) - if err != nil { - return nil, err - } - return &Config{config: config}, nil - case map[string]any: - connector := new(Config) - decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ - DecodeHook: mapstructure.StringToTimeDurationHookFunc(), - WeaklyTypedInput: true, - Result: connector, - }) - if err != nil { - return nil, err - } - if err = decoder.Decode(c); err != nil { - return nil, err - } - return &Config{ - config: &pgxpool.Config{}, - }, nil - } - return nil, errors.New("invalid configuration") -} diff --git a/backend/command/v2/storage/database/dialect/postgres/conn.go b/backend/command/v2/storage/database/dialect/postgres/conn.go deleted file mode 100644 index e7bdc0741a..0000000000 --- a/backend/command/v2/storage/database/dialect/postgres/conn.go +++ /dev/null @@ -1,48 +0,0 @@ -package postgres - -import ( - "context" - - "github.com/jackc/pgx/v5/pgxpool" - - "github.com/zitadel/zitadel/backend/command/v2/storage/database" -) - -type pgxConn struct{ *pgxpool.Conn } - -var _ database.Client = (*pgxConn)(nil) - -// Release implements [database.Client]. -func (c *pgxConn) Release(_ context.Context) error { - c.Conn.Release() - return nil -} - -// Begin implements [database.Client]. -func (c *pgxConn) Begin(ctx context.Context, opts *database.TransactionOptions) (database.Transaction, error) { - tx, err := c.Conn.BeginTx(ctx, transactionOptionsToPgx(opts)) - if err != nil { - return nil, err - } - return &pgxTx{tx}, nil -} - -// Query implements sql.Client. -// Subtle: this method shadows the method (*Conn).Query of pgxConn.Conn. -func (c *pgxConn) Query(ctx context.Context, sql string, args ...any) (database.Rows, error) { - rows, err := c.Conn.Query(ctx, sql, args...) - return &Rows{rows}, err -} - -// QueryRow implements sql.Client. -// Subtle: this method shadows the method (*Conn).QueryRow of pgxConn.Conn. -func (c *pgxConn) QueryRow(ctx context.Context, sql string, args ...any) database.Row { - return c.Conn.QueryRow(ctx, sql, args...) -} - -// Exec implements [database.Pool]. -// Subtle: this method shadows the method (Pool).Exec of pgxPool.Pool. -func (c *pgxConn) Exec(ctx context.Context, sql string, args ...any) error { - _, err := c.Conn.Exec(ctx, sql, args...) - return err -} diff --git a/backend/command/v2/storage/database/dialect/postgres/pool.go b/backend/command/v2/storage/database/dialect/postgres/pool.go deleted file mode 100644 index aba0231213..0000000000 --- a/backend/command/v2/storage/database/dialect/postgres/pool.go +++ /dev/null @@ -1,57 +0,0 @@ -package postgres - -import ( - "context" - - "github.com/jackc/pgx/v5/pgxpool" - - "github.com/zitadel/zitadel/backend/command/v2/storage/database" -) - -type pgxPool struct{ *pgxpool.Pool } - -var _ database.Pool = (*pgxPool)(nil) - -// Acquire implements [database.Pool]. -func (c *pgxPool) Acquire(ctx context.Context) (database.Client, error) { - conn, err := c.Pool.Acquire(ctx) - if err != nil { - return nil, err - } - return &pgxConn{conn}, nil -} - -// Query implements [database.Pool]. -// Subtle: this method shadows the method (Pool).Query of pgxPool.Pool. -func (c *pgxPool) Query(ctx context.Context, sql string, args ...any) (database.Rows, error) { - rows, err := c.Pool.Query(ctx, sql, args...) - return &Rows{rows}, err -} - -// QueryRow implements [database.Pool]. -// Subtle: this method shadows the method (Pool).QueryRow of pgxPool.Pool. -func (c *pgxPool) QueryRow(ctx context.Context, sql string, args ...any) database.Row { - return c.Pool.QueryRow(ctx, sql, args...) -} - -// Exec implements [database.Pool]. -// Subtle: this method shadows the method (Pool).Exec of pgxPool.Pool. -func (c *pgxPool) Exec(ctx context.Context, sql string, args ...any) error { - _, err := c.Pool.Exec(ctx, sql, args...) - return err -} - -// Begin implements [database.Pool]. -func (c *pgxPool) Begin(ctx context.Context, opts *database.TransactionOptions) (database.Transaction, error) { - tx, err := c.Pool.BeginTx(ctx, transactionOptionsToPgx(opts)) - if err != nil { - return nil, err - } - return &pgxTx{tx}, nil -} - -// Close implements [database.Pool]. -func (c *pgxPool) Close(_ context.Context) error { - c.Pool.Close() - return nil -} diff --git a/backend/command/v2/storage/database/dialect/postgres/rows.go b/backend/command/v2/storage/database/dialect/postgres/rows.go deleted file mode 100644 index c5ec8aabfd..0000000000 --- a/backend/command/v2/storage/database/dialect/postgres/rows.go +++ /dev/null @@ -1,18 +0,0 @@ -package postgres - -import ( - "github.com/jackc/pgx/v5" - - "github.com/zitadel/zitadel/backend/command/v2/storage/database" -) - -var _ database.Rows = (*Rows)(nil) - -type Rows struct{ pgx.Rows } - -// Close implements [database.Rows]. -// Subtle: this method shadows the method (Rows).Close of Rows.Rows. -func (r *Rows) Close() error { - r.Rows.Close() - return nil -} diff --git a/backend/command/v2/storage/database/dialect/postgres/tx.go b/backend/command/v2/storage/database/dialect/postgres/tx.go deleted file mode 100644 index 677a433240..0000000000 --- a/backend/command/v2/storage/database/dialect/postgres/tx.go +++ /dev/null @@ -1,95 +0,0 @@ -package postgres - -import ( - "context" - - "github.com/jackc/pgx/v5" - - "github.com/zitadel/zitadel/backend/command/v2/storage/database" -) - -type pgxTx struct{ pgx.Tx } - -var _ database.Transaction = (*pgxTx)(nil) - -// Commit implements [database.Transaction]. -func (tx *pgxTx) Commit(ctx context.Context) error { - return tx.Tx.Commit(ctx) -} - -// Rollback implements [database.Transaction]. -func (tx *pgxTx) Rollback(ctx context.Context) error { - return tx.Tx.Rollback(ctx) -} - -// End implements [database.Transaction]. -func (tx *pgxTx) End(ctx context.Context, err error) error { - if err != nil { - tx.Rollback(ctx) - return err - } - return tx.Commit(ctx) -} - -// Query implements [database.Transaction]. -// Subtle: this method shadows the method (Tx).Query of pgxTx.Tx. -func (tx *pgxTx) Query(ctx context.Context, sql string, args ...any) (database.Rows, error) { - rows, err := tx.Tx.Query(ctx, sql, args...) - return &Rows{rows}, err -} - -// QueryRow implements [database.Transaction]. -// Subtle: this method shadows the method (Tx).QueryRow of pgxTx.Tx. -func (tx *pgxTx) QueryRow(ctx context.Context, sql string, args ...any) database.Row { - return tx.Tx.QueryRow(ctx, sql, args...) -} - -// Exec implements [database.Transaction]. -// Subtle: this method shadows the method (Pool).Exec of pgxPool.Pool. -func (tx *pgxTx) Exec(ctx context.Context, sql string, args ...any) error { - _, err := tx.Tx.Exec(ctx, sql, args...) - return err -} - -// Begin implements [database.Transaction]. -// As postgres does not support nested transactions we use savepoints to emulate them. -func (tx *pgxTx) Begin(ctx context.Context) (database.Transaction, error) { - savepoint, err := tx.Tx.Begin(ctx) - if err != nil { - return nil, err - } - return &pgxTx{savepoint}, nil -} - -func transactionOptionsToPgx(opts *database.TransactionOptions) pgx.TxOptions { - if opts == nil { - return pgx.TxOptions{} - } - - return pgx.TxOptions{ - IsoLevel: isolationToPgx(opts.IsolationLevel), - AccessMode: accessModeToPgx(opts.AccessMode), - } -} - -func isolationToPgx(isolation database.IsolationLevel) pgx.TxIsoLevel { - switch isolation { - case database.IsolationLevelSerializable: - return pgx.Serializable - case database.IsolationLevelReadCommitted: - return pgx.ReadCommitted - default: - return pgx.Serializable - } -} - -func accessModeToPgx(accessMode database.AccessMode) pgx.TxAccessMode { - switch accessMode { - case database.AccessModeReadWrite: - return pgx.ReadWrite - case database.AccessModeReadOnly: - return pgx.ReadOnly - default: - return pgx.ReadWrite - } -} diff --git a/backend/command/v2/storage/database/secret_generator.go b/backend/command/v2/storage/database/secret_generator.go deleted file mode 100644 index fddebdce44..0000000000 --- a/backend/command/v2/storage/database/secret_generator.go +++ /dev/null @@ -1,39 +0,0 @@ -package database - -import ( - "context" - - "github.com/zitadel/zitadel/internal/crypto" -) - -type query struct{ Querier } - -func Query(querier Querier) *query { - return &query{Querier: querier} -} - -const getEncryptionConfigQuery = "SELECT" + - " length" + - ", expiry" + - ", should_include_lower_letters" + - ", should_include_upper_letters" + - ", should_include_digits" + - ", should_include_symbols" + - " FROM encryption_config" - -func (q query) GetEncryptionConfig(ctx context.Context) (*crypto.GeneratorConfig, error) { - var config crypto.GeneratorConfig - row := q.QueryRow(ctx, getEncryptionConfigQuery) - err := row.Scan( - &config.Length, - &config.Expiry, - &config.IncludeLowerLetters, - &config.IncludeUpperLetters, - &config.IncludeDigits, - &config.IncludeSymbols, - ) - if err != nil { - return nil, err - } - return &config, nil -} diff --git a/backend/command/v2/storage/database/tx.go b/backend/command/v2/storage/database/tx.go deleted file mode 100644 index 02c945dc77..0000000000 --- a/backend/command/v2/storage/database/tx.go +++ /dev/null @@ -1,36 +0,0 @@ -package database - -import "context" - -type Transaction interface { - Commit(ctx context.Context) error - Rollback(ctx context.Context) error - End(ctx context.Context, err error) error - - Begin(ctx context.Context) (Transaction, error) - - QueryExecutor -} - -type Beginner interface { - Begin(ctx context.Context, opts *TransactionOptions) (Transaction, error) -} - -type TransactionOptions struct { - IsolationLevel IsolationLevel - AccessMode AccessMode -} - -type IsolationLevel uint8 - -const ( - IsolationLevelSerializable IsolationLevel = iota - IsolationLevelReadCommitted -) - -type AccessMode uint8 - -const ( - AccessModeReadWrite AccessMode = iota - AccessModeReadOnly -) diff --git a/backend/command/v2/storage/eventstore/event.go b/backend/command/v2/storage/eventstore/event.go deleted file mode 100644 index 52d0491558..0000000000 --- a/backend/command/v2/storage/eventstore/event.go +++ /dev/null @@ -1,13 +0,0 @@ -package eventstore - -import "github.com/zitadel/zitadel/backend/command/v2/pattern" - -type Event struct { - AggregateType string `json:"aggregateType"` - AggregateID string `json:"aggregateId"` -} - -type EventCommander interface { - pattern.Command - Event() *Event -} diff --git a/backend/command/v2/telemetry/tracing/command.go b/backend/command/v2/telemetry/tracing/command.go deleted file mode 100644 index 679f1b5512..0000000000 --- a/backend/command/v2/telemetry/tracing/command.go +++ /dev/null @@ -1,55 +0,0 @@ -package tracing - -import ( - "context" - - "go.opentelemetry.io/otel/trace" - - "github.com/zitadel/zitadel/backend/command/v2/pattern" -) - -type command struct { - trace.Tracer - cmd pattern.Command -} - -func Trace(tracer trace.Tracer, cmd pattern.Command) pattern.Command { - return &command{ - Tracer: tracer, - cmd: cmd, - } -} - -func (cmd *command) Name() string { - return cmd.cmd.Name() -} - -func (cmd *command) Execute(ctx context.Context) error { - ctx, span := cmd.Tracer.Start(ctx, cmd.Name()) - defer span.End() - - err := cmd.cmd.Execute(ctx) - if err != nil { - span.RecordError(err) - } - return err -} - -type query[T any] struct { - command - query pattern.Query[T] -} - -func Query[T any](tracer trace.Tracer, q pattern.Query[T]) pattern.Query[T] { - return &query[T]{ - command: command{ - Tracer: tracer, - cmd: q, - }, - query: q, - } -} - -func (q *query[T]) Result() T { - return q.query.Result() -} diff --git a/backend/domain/database.go b/backend/domain/database.go deleted file mode 100644 index 789386b24d..0000000000 --- a/backend/domain/database.go +++ /dev/null @@ -1,45 +0,0 @@ -package domain - -import ( - "context" - - "github.com/zitadel/zitadel/backend/storage/database" -) - -type poolHandler[T any] struct { - pool database.Pool - - client database.QueryExecutor -} - -func (h *poolHandler[T]) acquire(ctx context.Context, in T) (out T, _ func(context.Context, error) error, err error) { - client, err := h.pool.Acquire(ctx) - if err != nil { - return in, nil, err - } - h.client = client - - return in, func(ctx context.Context, _ error) error { return client.Release(ctx) }, nil -} - -func (h *poolHandler[T]) begin(ctx context.Context, in T) (out T, _ func(context.Context, error) error, err error) { - var beginner database.Beginner = h.pool - if h.client != nil { - beginner = h.client.(database.Beginner) - } - previousClient := h.client - tx, err := beginner.Begin(ctx, nil) - if err != nil { - return in, nil, err - } - h.client = tx - - return in, func(ctx context.Context, err error) error { - err = tx.End(ctx, err) - if err != nil { - return err - } - h.client = previousClient - return nil - }, nil -} diff --git a/backend/domain/domain.go b/backend/domain/domain.go deleted file mode 100644 index 2a85f9790b..0000000000 --- a/backend/domain/domain.go +++ /dev/null @@ -1,20 +0,0 @@ -package domain - -import ( - "context" - - "github.com/zitadel/zitadel/backend/storage/database" -) - -type defaults struct { - db database.Pool -} - -type clientSetter interface { - setClient(database.QueryExecutor) -} - -func (d *defaults) acquire(ctx context.Context, setter clientSetter) { - d.db.Acquire(ctx) - setter.setClient(d.db) -} diff --git a/backend/domain/instance.go b/backend/domain/instance.go deleted file mode 100644 index 17c966bfa2..0000000000 --- a/backend/domain/instance.go +++ /dev/null @@ -1,68 +0,0 @@ -package domain - -import ( - "context" - - "github.com/zitadel/zitadel/backend/repository" - "github.com/zitadel/zitadel/backend/storage/database" - "github.com/zitadel/zitadel/backend/telemetry/logging" - "github.com/zitadel/zitadel/backend/telemetry/tracing" -) - -type Instance struct { - db database.Pool - - instance instanceRepository - user userRepository -} - -type instanceRepository interface { - ByID(ctx context.Context, querier database.Querier, id string) (*repository.Instance, error) - ByDomain(ctx context.Context, querier database.Querier, domain string) (*repository.Instance, error) - Create(ctx context.Context, tx database.Transaction, instance *repository.Instance) (*repository.Instance, error) -} - -func NewInstance(db database.Pool, tracer *tracing.Tracer, logger *logging.Logger) *Instance { - b := &Instance{ - db: db, - instance: repository.NewInstance( - repository.WithLogger[repository.InstanceOptions](logger), - repository.WithTracer[repository.InstanceOptions](tracer), - ), - user: repository.NewUser( - repository.WithLogger[repository.UserOptions](logger), - repository.WithTracer[repository.UserOptions](tracer), - ), - } - - return b -} - -func (b *Instance) ByID(ctx context.Context, id string) (*repository.Instance, error) { - return b.instance.ByID(ctx, b.db, id) -} - -func (b *Instance) ByDomain(ctx context.Context, domain string) (*repository.Instance, error) { - return b.instance.ByDomain(ctx, b.db, domain) -} - -type SetUpInstance struct { - Instance *repository.Instance - User *repository.User -} - -func (b *Instance) SetUp(ctx context.Context, request *SetUpInstance) (err error) { - tx, err := b.db.Begin(ctx, nil) - if err != nil { - return err - } - defer func() { - err = tx.End(ctx, err) - }() - _, err = b.instance.Create(ctx, tx, request.Instance) - if err != nil { - return err - } - _, err = b.user.Create(ctx, tx, request.User) - return err -} diff --git a/backend/domain/user.go b/backend/domain/user.go deleted file mode 100644 index d5dfccde2f..0000000000 --- a/backend/domain/user.go +++ /dev/null @@ -1,45 +0,0 @@ -package domain - -import ( - "context" - - "github.com/zitadel/zitadel/backend/repository" - "github.com/zitadel/zitadel/backend/storage/database" - "github.com/zitadel/zitadel/internal/crypto" -) - -type User struct { - defaults - - userCodeAlg crypto.EncryptionAlgorithm - user userRepository - secretGenerator secretGeneratorRepository -} - -type UserRepositoryConstructor interface { - NewUserExecutor(database.Executor) userRepository - NewUserQuerier(database.Querier) userRepository -} - -type userRepository interface { - Create(ctx context.Context, tx database.Executor, user *repository.User) (*repository.User, error) - ByID(ctx context.Context, querier database.Querier, id string) (*repository.User, error) - - EmailVerificationCode(ctx context.Context, client database.Querier, userID string) (*repository.EmailVerificationCode, error) - EmailVerificationFailed(ctx context.Context, client database.Executor, code *repository.EmailVerificationCode) error - EmailVerificationSucceeded(ctx context.Context, client database.Executor, code *repository.EmailVerificationCode) error -} - -type secretGeneratorRepository interface { - GeneratorConfigByType(ctx context.Context, client database.Querier, typ repository.SecretGeneratorType) (*crypto.GeneratorConfig, error) -} - -func NewUser(db database.Pool) *User { - b := &User{ - db: db, - user: repository.NewUser(), - secretGenerator: repository.NewSecretGenerator(), - } - - return b -} diff --git a/backend/domain/user_email.go b/backend/domain/user_email.go deleted file mode 100644 index cf1ad2dfe1..0000000000 --- a/backend/domain/user_email.go +++ /dev/null @@ -1,250 +0,0 @@ -package domain - -import ( - "context" - "text/template" - - "github.com/zitadel/zitadel/backend/handler" - "github.com/zitadel/zitadel/backend/repository" - "github.com/zitadel/zitadel/backend/storage/database" - "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/domain" -) - -type VerifyEmail struct { - UserID string - Code string - - client database.QueryExecutor - config *crypto.GeneratorConfig - gen crypto.Generator - code *repository.EmailVerificationCode - verificationErr error -} - -type SetEmail struct { - *poolHandler[*SetEmail] - - UserID string - Email string - Verification handler.Handle[*SetEmail, *SetEmail] - - // config *crypto.GeneratorConfig - gen crypto.Generator - - code *crypto.CryptoValue - plainCode string - - currentEmail string -} - -func (u *User) WithEmailConfirmationURL(url template.Template) handler.Handle[*SetEmail, *SetEmail] { - return handler.Chain( - u.WithEmailReturnCode(), - func(ctx context.Context, in *SetEmail) (out *SetEmail, err error) { - // TODO: queue notification - return in, nil - }, - ) -} - -func (u *User) WithEmailReturnCode() handler.Handle[*SetEmail, *SetEmail] { - return handler.Chains( - handler.ErrFuncToHandle( - func(ctx context.Context, in *SetEmail) (err error) { - in.code, in.plainCode, err = crypto.NewCode(in.gen) - return err - }, - ), - handler.ErrFuncToHandle( - func(ctx context.Context, in *SetEmail) (err error) { - return u.user.SetEmailVerificationCode(ctx, in.poolHandler.client, in.UserID, in.code) - }, - ), - ) -} - -func (u *User) WithEmailVerified() handler.Handle[*SetEmail, *SetEmail] { - return handler.Chain( - handler.ErrFuncToHandle( - func(ctx context.Context, in *SetEmail) (err error) { - return repository.SetEmailVerificationCode(ctx, in.poolHandler.client, in.UserID, in.code) - }, - ), - handler.ErrFuncToHandle( - func(ctx context.Context, in *SetEmail) (err error) { - return u.user.EmailVerificationSucceeded(ctx, in.poolHandler.client, &repository.EmailVerificationCode{ - Code: in.code, - }) - }, - ), - ) -} - -func (u *User) WithDefaultEmailVerification() handler.Handle[*SetEmail, *SetEmail] { - return handler.Chain( - u.WithEmailReturnCode(), - func(ctx context.Context, in *SetEmail) (out *SetEmail, err error) { - // TODO: queue notification - return in, nil - }, - ) -} - -func (u *User) SetEmailDifferent(ctx context.Context, in *SetEmail) (err error) { - if in.Verification == nil { - in.Verification = u.WithDefaultEmailVerification() - } - - client, err := u.db.Acquire(ctx) - if err != nil { - return err - } - defer client.Release(ctx) - - config, err := u.secretGenerator.GeneratorConfigByType(ctx, client, domain.SecretGeneratorTypeVerifyEmailCode) - if err != nil { - return err - } - in.gen = crypto.NewEncryptionGenerator(*config, u.userCodeAlg) - - tx, err := client.Begin(ctx, nil) - if err != nil { - return err - } - defer tx.End(ctx, err) - - user, err := u.user.ByID(ctx, tx, in.UserID) - if err != nil { - return err - } - - if user.Email == in.Email { - return nil - } - - _, err = in.Verification(ctx, in) - return err -} - -func (u *User) SetEmail(ctx context.Context, in *SetEmail) error { - _, err := handler.Chain( - handler.HandleIf( - func(in *SetEmail) bool { - return in.Verification == nil - }, - func(ctx context.Context, in *SetEmail) (*SetEmail, error) { - in.Verification = u.WithDefaultEmailVerification() - return in, nil - }, - ), - handler.Deferrable( - in.poolHandler.acquire, - handler.Chains( - func(ctx context.Context, in *SetEmail) (_ *SetEmail, err error) { - config, err := u.secretGenerator.GeneratorConfigByType(ctx, in.poolHandler.client, domain.SecretGeneratorTypeVerifyEmailCode) - if err != nil { - return nil, err - } - in.gen = crypto.NewEncryptionGenerator(*config, u.userCodeAlg) - return in, nil - }, - handler.Deferrable( - in.poolHandler.begin, - handler.Chains( - func(ctx context.Context, in *SetEmail) (*SetEmail, error) { - // TODO: repository.EmailByUserID - user, err := u.user.ByID(ctx, in.poolHandler.client, in.UserID) - if err != nil { - return nil, err - } - in.currentEmail = user.Email - return in, nil - }, - handler.SkipIf( - func(in *SetEmail) bool { - return in.currentEmail == in.Email - }, - handler.Chains( - func(ctx context.Context, in *SetEmail) (*SetEmail, error) { - // TODO: repository.SetEmail - return in, nil - }, - in.Verification, - ), - ), - ), - ), - ), - ), - )(ctx, in) - return err -} - -func (u *User) VerifyEmail(ctx context.Context, in *VerifyEmail) error { - _, err := handler.Deferrable( - func(ctx context.Context, in *VerifyEmail) (_ *VerifyEmail, _ func(context.Context, error) error, err error) { - client, err := u.db.Acquire(ctx) - if err != nil { - return nil, nil, err - } - in.client = client - return in, func(ctx context.Context, _ error) error { return client.Release(ctx) }, err - }, - handler.Chains( - func(ctx context.Context, in *VerifyEmail) (_ *VerifyEmail, err error) { - in.config, err = u.secretGenerator.GeneratorConfigByType(ctx, in.client, domain.SecretGeneratorTypeVerifyEmailCode) - return in, err - }, - func(ctx context.Context, in *VerifyEmail) (_ *VerifyEmail, err error) { - in.gen = crypto.NewEncryptionGenerator(*in.config, u.userCodeAlg) - return in, nil - }, - handler.Deferrable( - func(ctx context.Context, in *VerifyEmail) (_ *VerifyEmail, _ func(context.Context, error) error, err error) { - client := in.client - tx, err := in.client.(database.Client).Begin(ctx, nil) - if err != nil { - return nil, nil, err - } - in.client = tx - return in, func(ctx context.Context, err error) error { - err = tx.End(ctx, err) - if err != nil { - return err - } - in.client = client - return nil - }, err - }, - handler.Chains( - func(ctx context.Context, in *VerifyEmail) (_ *VerifyEmail, err error) { - in.code, err = u.user.EmailVerificationCode(ctx, in.client, in.UserID) - return in, err - }, - func(ctx context.Context, in *VerifyEmail) (*VerifyEmail, error) { - in.verificationErr = crypto.VerifyCode(in.code.CreatedAt, in.code.Expiry, in.code.Code, in.Code, in.gen.Alg()) - return in, nil - }, - handler.HandleIf( - func(in *VerifyEmail) bool { - return in.verificationErr == nil - }, - func(ctx context.Context, in *VerifyEmail) (_ *VerifyEmail, err error) { - return in, u.user.EmailVerificationSucceeded(ctx, in.client, in.code) - }, - ), - handler.HandleIf( - func(in *VerifyEmail) bool { - return in.verificationErr != nil - }, - func(ctx context.Context, in *VerifyEmail) (_ *VerifyEmail, err error) { - return in, u.user.EmailVerificationFailed(ctx, in.client, in.code) - }, - ), - ), - ), - ), - )(ctx, in) - return err -} diff --git a/backend/handler/handle.go b/backend/handler/handle.go deleted file mode 100644 index 7c10447e9f..0000000000 --- a/backend/handler/handle.go +++ /dev/null @@ -1,162 +0,0 @@ -package handler - -import ( - "context" -) - -type Parameter[P, C any] struct { - Previous P - Current C -} - -// Handle is a function that handles the in. -type Handle[In, Out any] func(ctx context.Context, in In) (out Out, err error) - -type DeferrableHandle[In, Out any] func(ctx context.Context, in In) (out Out, deferrable func(context.Context, error) error, err error) - -type Defer[In, Out, NextOut any] func(handle DeferrableHandle[In, Out], next Handle[Out, NextOut]) Handle[In, NextOut] - -type HandleNoReturn[In any] func(ctx context.Context, in In) error - -// Middleware is a function that decorates the handle function. -// It must call the handle function but its up the the middleware to decide when and how. -type Middleware[In, Out any] func(ctx context.Context, in In, handle Handle[In, Out]) (out Out, err error) - -func Deferrable[In, Out, NextOut any](handle DeferrableHandle[In, Out], next Handle[Out, NextOut]) Handle[In, NextOut] { - return func(ctx context.Context, in In) (nextOut NextOut, err error) { - out, deferrable, err := handle(ctx, in) - if err != nil { - return nextOut, err - } - defer func() { - err = deferrable(ctx, err) - }() - return next(ctx, out) - } -} - -// Chain chains the handle function with the next handler. -// The next handler is called after the handle function. -func Chain[In, Out, NextOut any](handle Handle[In, Out], next Handle[Out, NextOut]) Handle[In, NextOut] { - return func(ctx context.Context, in In) (nextOut NextOut, err error) { - out, err := handle(ctx, in) - if err != nil { - return nextOut, err - } - return next(ctx, out) - } -} - -// Chains chains the handle function with the next handlers. -// The next handlers are called after the handle function. -// The order of the handlers is preserved. -func Chains[In, Out any](handle Handle[In, Out], chain ...Handle[Out, Out]) Handle[In, Out] { - return func(ctx context.Context, in In) (out Out, err error) { - for _, next := range chain { - handle = Chain(handle, next) - } - return handle(ctx, in) - } -} - -// Decorate decorates the handle function with the decorate function. -// The decorate function is called before the handle function. -func Decorate[In, Out any](handle Handle[In, Out], decorate Middleware[In, Out]) Handle[In, Out] { - return func(ctx context.Context, in In) (out Out, err error) { - return decorate(ctx, in, handle) - } -} - -// Decorates decorates the handle function with the decorate functions. -// The decorates function is called before the handle function. -func Decorates[In, Out any](handle Handle[In, Out], decorates ...Middleware[In, Out]) Handle[In, Out] { - return func(ctx context.Context, in In) (out Out, err error) { - for i := len(decorates) - 1; i >= 0; i-- { - handle = Decorate(handle, decorates[i]) - } - return handle(ctx, in) - } -} - -// SkipNext skips the next handler if the handle function returns a non-empty output or an error. -func SkipNext[In, Out any](handle Handle[In, Out], next Handle[In, Out]) Handle[In, Out] { - return func(ctx context.Context, in In) (out Out, err error) { - var empty Out - out, err = handle(ctx, in) - // TODO: does this work? - if any(out) != any(empty) || err != nil { - return out, err - } - return next(ctx, in) - } -} - -func HandleIf[In any](cond func(In) bool, handle Handle[In, In]) Handle[In, In] { - return func(ctx context.Context, in In) (out In, err error) { - if !cond(in) { - return in, nil - } - return handle(ctx, in) - } -} - -func SkipIf[In any](cond func(In) bool, handle Handle[In, In]) Handle[In, In] { - return func(ctx context.Context, in In) (out In, err error) { - if cond(in) { - return in, nil - } - return handle(ctx, in) - } -} - -// SkipNilHandler skips the handle function if the handler is nil. -// If handle is nil, an empty output is returned. -// The function is safe to call with nil handler. -func SkipNilHandler[O, In, Out any](handler *O, handle Handle[In, Out]) Handle[In, Out] { - return func(ctx context.Context, in In) (out Out, err error) { - if handler == nil { - return out, nil - } - return handle(ctx, in) - } -} - -// SkipReturnPreviousHandler skips the handle function if the handler is nil and returns the input. -// The function is safe to call with nil handler. -func SkipReturnPreviousHandler[O, In any](handler *O, handle Handle[In, In]) Handle[In, In] { - return func(ctx context.Context, in In) (out In, err error) { - if handler == nil { - return in, nil - } - return handle(ctx, in) - } -} - -func CtxFuncToHandle[Out any](fn func(context.Context) (Out, error)) Handle[struct{}, Out] { - return func(ctx context.Context, in struct{}) (out Out, err error) { - return fn(ctx) - } -} - -func ResFuncToHandle[In any, Out any](fn func(context.Context, In) Out) Handle[In, Out] { - return func(ctx context.Context, in In) (out Out, err error) { - return fn(ctx, in), nil - } -} - -func ErrFuncToHandle[In any](fn func(context.Context, In) error) Handle[In, In] { - return func(ctx context.Context, in In) (out In, err error) { - err = fn(ctx, in) - if err != nil { - return out, err - } - return in, nil - } -} - -func NoReturnToHandle[In any](fn func(context.Context, In)) Handle[In, In] { - return func(ctx context.Context, in In) (out In, err error) { - fn(ctx, in) - return in, nil - } -} diff --git a/backend/repository/database.go b/backend/repository/database.go deleted file mode 100644 index c9c877ed48..0000000000 --- a/backend/repository/database.go +++ /dev/null @@ -1,19 +0,0 @@ -package repository - -import "github.com/zitadel/zitadel/backend/storage/database" - -type executor struct { - client database.Executor -} - -func execute(client database.Executor) *executor { - return &executor{client: client} -} - -type querier struct { - client database.Querier -} - -func query(client database.Querier) *querier { - return &querier{client: client} -} diff --git a/backend/repository/event.go b/backend/repository/event.go deleted file mode 100644 index d6aef15e8e..0000000000 --- a/backend/repository/event.go +++ /dev/null @@ -1,16 +0,0 @@ -package repository - -import ( - "github.com/zitadel/zitadel/backend/storage/database" - "github.com/zitadel/zitadel/backend/storage/eventstore" -) - -type eventStore struct { - es *eventstore.Eventstore -} - -func events(client database.Executor) *eventStore { - return &eventStore{ - es: eventstore.New(client), - } -} diff --git a/backend/repository/instance.go b/backend/repository/instance.go deleted file mode 100644 index 7ee923d641..0000000000 --- a/backend/repository/instance.go +++ /dev/null @@ -1,115 +0,0 @@ -package repository - -import ( - "context" - - "github.com/zitadel/zitadel/backend/handler" - "github.com/zitadel/zitadel/backend/storage/database" - "github.com/zitadel/zitadel/backend/telemetry/logging" - "github.com/zitadel/zitadel/backend/telemetry/tracing" -) - -type Instance struct { - ID string - Name string -} - -type InstanceOptions struct { - cache *InstanceCache -} - -type instance struct { - options[InstanceOptions] - *InstanceOptions -} - -func NewInstance(opts ...Option[InstanceOptions]) *instance { - i := new(instance) - i.InstanceOptions = &i.options.custom - - for _, opt := range opts { - opt.apply(&i.options) - } - return i -} - -func WithInstanceCache(c *InstanceCache) Option[InstanceOptions] { - return func(opts *options[InstanceOptions]) { - opts.custom.cache = c - } -} - -func (i *instance) Create(ctx context.Context, tx database.Transaction, instance *Instance) (*Instance, error) { - return tracing.Wrap(i.tracer, "instance.SetUp", - handler.Chains( - handler.Decorates( - execute(tx).CreateInstance, - tracing.Decorate[*Instance, *Instance](i.tracer, tracing.WithSpanName("instance.sql.SetUp")), - logging.Decorate[*Instance, *Instance](i.logger, "instance.sql.SetUp"), - ), - handler.Decorates( - events(tx).CreateInstance, - tracing.Decorate[*Instance, *Instance](i.tracer, tracing.WithSpanName("instance.event.SetUp")), - logging.Decorate[*Instance, *Instance](i.logger, "instance.event.SetUp"), - ), - handler.SkipReturnPreviousHandler(i.cache, - handler.Decorates( - handler.NoReturnToHandle(i.cache.Set), - tracing.Decorate[*Instance, *Instance](i.tracer, tracing.WithSpanName("instance.cache.SetUp")), - logging.Decorate[*Instance, *Instance](i.logger, "instance.cache.SetUp"), - ), - ), - ), - )(ctx, instance) -} - -func (i *instance) ByID(ctx context.Context, querier database.Querier, id string) (*Instance, error) { - return tracing.Wrap(i.tracer, "instance.byID", - handler.SkipNext( - handler.SkipNilHandler(i.cache, - handler.ResFuncToHandle(i.cache.ByID), - ), - handler.Chain( - handler.Decorates( - query(querier).InstanceByID, - tracing.Decorate[string, *Instance](i.tracer, tracing.WithSpanName("instance.sql.ByID")), - logging.Decorate[string, *Instance](i.logger, "instance.sql.ByID"), - ), - handler.SkipNilHandler(i.cache, handler.NoReturnToHandle(i.cache.Set)), - ), - ), - )(ctx, id) -} - -func (i *instance) ByDomain(ctx context.Context, querier database.Querier, domain string) (*Instance, error) { - return tracing.Wrap(i.tracer, "instance.byDomain", - handler.SkipNext( - handler.SkipNilHandler(i.cache, - handler.ResFuncToHandle(i.cache.ByDomain), - ), - handler.Chain( - handler.Decorate( - query(querier).InstanceByDomain, - tracing.Decorate[string, *Instance](i.tracer, tracing.WithSpanName("instance.sql.ByDomain")), - ), - handler.SkipNilHandler(i.cache, handler.NoReturnToHandle(i.cache.Set)), - ), - ), - )(ctx, domain) -} - -type ListRequest struct { - Limit uint16 -} - -func (i *instance) List(ctx context.Context, querier database.Querier, request *ListRequest) ([]*Instance, error) { - return tracing.Wrap(i.tracer, "instance.list", - handler.Chains( - handler.Decorates( - query(querier).ListInstances, - tracing.Decorate[*ListRequest, []*Instance](i.tracer, tracing.WithSpanName("instance.sql.List")), - logging.Decorate[*ListRequest, []*Instance](i.logger, "instance.sql.List"), - ), - ), - )(ctx, request) -} diff --git a/backend/repository/instance_cache.go b/backend/repository/instance_cache.go deleted file mode 100644 index b956e55cff..0000000000 --- a/backend/repository/instance_cache.go +++ /dev/null @@ -1,58 +0,0 @@ -package repository - -import ( - "context" - "log" - - "github.com/zitadel/zitadel/backend/storage/cache" -) - -type InstanceCache struct { - cache.Cache[InstanceIndex, string, *Instance] -} - -type InstanceIndex uint8 - -var InstanceIndices = []InstanceIndex{ - InstanceByID, - InstanceByDomain, -} - -const ( - InstanceByID InstanceIndex = iota - InstanceByDomain -) - -var _ cache.Entry[InstanceIndex, string] = (*Instance)(nil) - -// Keys implements [cache.Entry]. -func (i *Instance) Keys(index InstanceIndex) (key []string) { - switch index { - case InstanceByID: - return []string{i.ID} - case InstanceByDomain: - return []string{i.Name} - } - return nil -} - -func NewInstanceCache(c cache.Cache[InstanceIndex, string, *Instance]) *InstanceCache { - return &InstanceCache{c} -} - -func (i *InstanceCache) ByID(ctx context.Context, id string) *Instance { - log.Println("cached.instance.byID") - instance, _ := i.Cache.Get(ctx, InstanceByID, id) - return instance -} - -func (i *InstanceCache) ByDomain(ctx context.Context, domain string) *Instance { - log.Println("cached.instance.byDomain") - instance, _ := i.Cache.Get(ctx, InstanceByDomain, domain) - return instance -} - -func (i *InstanceCache) Set(ctx context.Context, instance *Instance) { - log.Println("cached.instance.set") - i.Cache.Set(ctx, instance) -} diff --git a/backend/repository/instance_db.go b/backend/repository/instance_db.go deleted file mode 100644 index df9ed33bd3..0000000000 --- a/backend/repository/instance_db.go +++ /dev/null @@ -1,61 +0,0 @@ -package repository - -import ( - "context" - "log" -) - -const InstanceByIDStmt = `SELECT id, name FROM instances WHERE id = $1` - -func (q *querier) InstanceByID(ctx context.Context, id string) (*Instance, error) { - log.Println("sql.instance.byID") - row := q.client.QueryRow(ctx, InstanceByIDStmt, id) - var instance Instance - if err := row.Scan(&instance.ID, &instance.Name); err != nil { - return nil, err - } - return &instance, nil -} - -const instanceByDomainQuery = `SELECT i.id, i.name FROM instances i JOIN instance_domains id ON i.id = id.instance_id WHERE id.domain = $1` - -func (q *querier) InstanceByDomain(ctx context.Context, domain string) (*Instance, error) { - log.Println("sql.instance.byDomain") - row := q.client.QueryRow(ctx, instanceByDomainQuery, domain) - var instance Instance - if err := row.Scan(&instance.ID, &instance.Name); err != nil { - return nil, err - } - return &instance, nil -} - -func (q *querier) ListInstances(ctx context.Context, request *ListRequest) (res []*Instance, err error) { - log.Println("sql.instance.list") - rows, err := q.client.Query(ctx, "SELECT id, name FROM instances") - if err != nil { - return nil, err - } - defer rows.Close() - for rows.Next() { - var instance Instance - if err = rows.Scan(&instance.ID, &instance.Name); err != nil { - return nil, err - } - res = append(res, &instance) - } - if err = rows.Err(); err != nil { - return nil, err - } - return res, nil -} - -const InstanceCreateStmt = `INSERT INTO instances (id, name) VALUES ($1, $2)` - -func (e *executor) CreateInstance(ctx context.Context, instance *Instance) (*Instance, error) { - log.Println("sql.instance.create") - err := e.client.Exec(ctx, InstanceCreateStmt, instance.ID, instance.Name) - if err != nil { - return nil, err - } - return instance, nil -} diff --git a/backend/repository/instance_event.go b/backend/repository/instance_event.go deleted file mode 100644 index b976be881b..0000000000 --- a/backend/repository/instance_event.go +++ /dev/null @@ -1,15 +0,0 @@ -package repository - -import ( - "context" - "log" -) - -func (s *eventStore) CreateInstance(ctx context.Context, instance *Instance) (*Instance, error) { - log.Println("event.instance.create") - err := s.es.Push(ctx, instance) - if err != nil { - return nil, err - } - return instance, nil -} diff --git a/backend/repository/instance_test.go b/backend/repository/instance_test.go deleted file mode 100644 index 52b3c58bc2..0000000000 --- a/backend/repository/instance_test.go +++ /dev/null @@ -1,258 +0,0 @@ -package repository_test - -import ( - "context" - "fmt" - "log/slog" - "os" - "reflect" - "testing" - - "github.com/zitadel/zitadel/backend/repository" - "github.com/zitadel/zitadel/backend/storage/cache" - "github.com/zitadel/zitadel/backend/storage/cache/connector/gomap" - "github.com/zitadel/zitadel/backend/storage/database" - "github.com/zitadel/zitadel/backend/storage/database/mock" - "github.com/zitadel/zitadel/backend/telemetry/logging" - "github.com/zitadel/zitadel/backend/telemetry/tracing" -) - -func Test_instance_Create(t *testing.T) { - type args struct { - ctx context.Context - tx database.Transaction - instance *repository.Instance - } - tests := []struct { - name string - opts []repository.Option[repository.InstanceOptions] - args args - want *repository.Instance - wantErr bool - }{ - { - name: "simple", - opts: []repository.Option[repository.InstanceOptions]{ - repository.WithTracer[repository.InstanceOptions](tracing.NewTracer("test")), - repository.WithLogger[repository.InstanceOptions](logging.New(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})))), - repository.WithInstanceCache( - repository.NewInstanceCache(gomap.NewCache[repository.InstanceIndex, string, *repository.Instance](context.Background(), repository.InstanceIndices, cache.Config{})), - ), - }, - args: args{ - ctx: context.Background(), - tx: mock.NewTransaction(t, mock.ExpectExec(repository.InstanceCreateStmt, "ID", "Name")), - instance: &repository.Instance{ - ID: "ID", - Name: "Name", - }, - }, - want: &repository.Instance{ - ID: "ID", - Name: "Name", - }, - wantErr: false, - }, - { - name: "without cache", - opts: []repository.Option[repository.InstanceOptions]{ - repository.WithTracer[repository.InstanceOptions](tracing.NewTracer("test")), - repository.WithLogger[repository.InstanceOptions](logging.New(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})))), - }, - args: args{ - ctx: context.Background(), - tx: mock.NewTransaction(t, mock.ExpectExec(repository.InstanceCreateStmt, "ID", "Name")), - instance: &repository.Instance{ - ID: "ID", - Name: "Name", - }, - }, - want: &repository.Instance{ - ID: "ID", - Name: "Name", - }, - wantErr: false, - }, - { - name: "without cache, tracer", - opts: []repository.Option[repository.InstanceOptions]{ - repository.WithLogger[repository.InstanceOptions](logging.New(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})))), - }, - args: args{ - ctx: context.Background(), - tx: mock.NewTransaction(t, mock.ExpectExec(repository.InstanceCreateStmt, "ID", "Name")), - instance: &repository.Instance{ - ID: "ID", - Name: "Name", - }, - }, - want: &repository.Instance{ - ID: "ID", - Name: "Name", - }, - wantErr: false, - }, - { - name: "without cache, tracer, logger", - args: args{ - ctx: context.Background(), - tx: mock.NewTransaction(t, mock.ExpectExec(repository.InstanceCreateStmt, "ID", "Name")), - instance: &repository.Instance{ - ID: "ID", - Name: "Name", - }, - }, - want: &repository.Instance{ - ID: "ID", - Name: "Name", - }, - wantErr: false, - }, - { - name: "without cache, tracer, logger, eventStore", - args: args{ - ctx: context.Background(), - tx: mock.NewTransaction(t, mock.ExpectExec(repository.InstanceCreateStmt, "ID", "Name")), - instance: &repository.Instance{ - ID: "ID", - Name: "Name", - }, - }, - want: &repository.Instance{ - ID: "ID", - Name: "Name", - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - fmt.Printf("------------------------ %s ------------------------\n", tt.name) - i := repository.NewInstance(tt.opts...) - got, err := i.Create(tt.args.ctx, tt.args.tx, tt.args.instance) - if (err != nil) != tt.wantErr { - t.Errorf("instance.Create() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("instance.Create() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_instance_ByID(t *testing.T) { - type args struct { - ctx context.Context - tx database.Transaction - id string - } - tests := []struct { - name string - opts []repository.Option[repository.InstanceOptions] - args args - want *repository.Instance - wantErr bool - }{ - { - name: "simple, not cached", - opts: []repository.Option[repository.InstanceOptions]{ - repository.WithTracer[repository.InstanceOptions](tracing.NewTracer("test")), - repository.WithLogger[repository.InstanceOptions](logging.New(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})))), - repository.WithInstanceCache( - repository.NewInstanceCache(gomap.NewCache[repository.InstanceIndex, string, *repository.Instance](context.Background(), repository.InstanceIndices, cache.Config{})), - ), - }, - args: args{ - ctx: context.Background(), - tx: mock.NewTransaction(t, - mock.ExpectQueryRow(mock.NewRow(t, "id", "Name"), repository.InstanceByIDStmt, "id"), - ), - id: "id", - }, - want: &repository.Instance{ - ID: "id", - Name: "Name", - }, - wantErr: false, - }, - { - name: "simple, cached", - opts: []repository.Option[repository.InstanceOptions]{ - repository.WithTracer[repository.InstanceOptions](tracing.NewTracer("test")), - repository.WithLogger[repository.InstanceOptions](logging.New(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})))), - repository.WithInstanceCache( - func() *repository.InstanceCache { - c := repository.NewInstanceCache(gomap.NewCache[repository.InstanceIndex, string, *repository.Instance](context.Background(), repository.InstanceIndices, cache.Config{})) - c.Set(context.Background(), &repository.Instance{ - ID: "id", - Name: "Name", - }) - return c - }(), - ), - }, - args: args{ - ctx: context.Background(), - tx: mock.NewTransaction(t, - mock.ExpectQueryRow(mock.NewRow(t, "id", "Name"), repository.InstanceByIDStmt, "id"), - ), - id: "id", - }, - want: &repository.Instance{ - ID: "id", - Name: "Name", - }, - wantErr: false, - }, - // { - // name: "without cache, tracer", - // opts: []repository.Option[repository.InstanceOptions]{ - // repository.WithLogger[repository.InstanceOptions](logging.New(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})))), - // }, - // args: args{ - // ctx: context.Background(), - // tx: mock.NewTransaction(), - // id: &repository.Instance{ - // ID: "ID", - // Name: "Name", - // }, - // }, - // want: &repository.Instance{ - // ID: "ID", - // Name: "Name", - // }, - // wantErr: false, - // }, - // { - // name: "without cache, tracer, logger", - // args: args{ - // ctx: context.Background(), - // tx: mock.NewTransaction(), - // id: &repository.Instance{ - // ID: "ID", - // Name: "Name", - // }, - // }, - // want: &repository.Instance{ - // ID: "ID", - // Name: "Name", - // }, - // wantErr: false, - // }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - fmt.Printf("------------------------ %s ------------------------\n", tt.name) - i := repository.NewInstance(tt.opts...) - got, err := i.ByID(tt.args.ctx, tt.args.tx, tt.args.id) - if (err != nil) != tt.wantErr { - t.Errorf("instance.ByID() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("instance.ByID() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/backend/repository/option.go b/backend/repository/option.go deleted file mode 100644 index 5510985b4a..0000000000 --- a/backend/repository/option.go +++ /dev/null @@ -1,35 +0,0 @@ -package repository - -import ( - "github.com/zitadel/zitadel/backend/telemetry/logging" - "github.com/zitadel/zitadel/backend/telemetry/tracing" -) - -// options are the default options for orchestrators. -type options[T any] struct { - custom T - defaultOptions -} - -type defaultOptions struct { - tracer *tracing.Tracer - logger *logging.Logger -} - -type Option[T any] func(*options[T]) - -func WithTracer[T any](tracer *tracing.Tracer) Option[T] { - return func(o *options[T]) { - o.tracer = tracer - } -} - -func WithLogger[T any](logger *logging.Logger) Option[T] { - return func(o *options[T]) { - o.logger = logger - } -} - -func (o Option[T]) apply(opts *options[T]) { - o(opts) -} diff --git a/backend/repository/secret_generator.go b/backend/repository/secret_generator.go deleted file mode 100644 index 708dd611f5..0000000000 --- a/backend/repository/secret_generator.go +++ /dev/null @@ -1,33 +0,0 @@ -package repository - -import ( - "context" - - "github.com/zitadel/zitadel/backend/storage/database" - "github.com/zitadel/zitadel/backend/telemetry/tracing" - "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/domain" -) - -type SecretGeneratorOptions struct{} - -type SecretGenerator struct { - options[SecretGeneratorOptions] -} - -func NewSecretGenerator(opts ...Option[SecretGeneratorOptions]) *SecretGenerator { - i := new(SecretGenerator) - - for _, opt := range opts { - opt.apply(&i.options) - } - return i -} - -type SecretGeneratorType = domain.SecretGeneratorType - -func (sg *SecretGenerator) GeneratorConfigByType(ctx context.Context, client database.Querier, typ SecretGeneratorType) (*crypto.GeneratorConfig, error) { - return tracing.Wrap(sg.tracer, "secretGenerator.GeneratorConfigByType", - query(client).SecretGeneratorConfigByType, - )(ctx, typ) -} diff --git a/backend/repository/secret_generator_db.go b/backend/repository/secret_generator_db.go deleted file mode 100644 index c1fa987640..0000000000 --- a/backend/repository/secret_generator_db.go +++ /dev/null @@ -1,25 +0,0 @@ -package repository - -import ( - "context" - - "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/crypto" -) - -const secretGeneratorByTypeStmt = `SELECT * FROM secret_generators WHERE instance_id = $1 AND type = $2` - -func (q querier) SecretGeneratorConfigByType(ctx context.Context, typ SecretGeneratorType) (config *crypto.GeneratorConfig, err error) { - err = q.client.QueryRow(ctx, secretGeneratorByTypeStmt, authz.GetInstance(ctx).InstanceID, typ).Scan( - &config.Length, - &config.Expiry, - &config.IncludeLowerLetters, - &config.IncludeUpperLetters, - &config.IncludeDigits, - &config.IncludeSymbols, - ) - if err != nil { - return nil, err - } - return config, nil -} diff --git a/backend/repository/user.go b/backend/repository/user.go deleted file mode 100644 index 95184bad43..0000000000 --- a/backend/repository/user.go +++ /dev/null @@ -1,132 +0,0 @@ -package repository - -import ( - "context" - "time" - - "github.com/zitadel/zitadel/backend/handler" - "github.com/zitadel/zitadel/backend/storage/database" - "github.com/zitadel/zitadel/backend/telemetry/tracing" - "github.com/zitadel/zitadel/internal/crypto" -) - -type User struct { - ID string - Username string - Email string -} - -type UserOptions struct { - cache *UserCache -} - -type user struct { - options[UserOptions] - *UserOptions -} - -func NewUser(opts ...Option[UserOptions]) *user { - i := new(user) - i.UserOptions = &i.options.custom - - for _, opt := range opts { - opt(&i.options) - } - return i -} - -func WithUserCache(c *UserCache) Option[UserOptions] { - return func(i *options[UserOptions]) { - i.custom.cache = c - } -} - -func (u *user) Create(ctx context.Context, client database.Executor, user *User) (*User, error) { - return tracing.Wrap(u.tracer, "user.Create", - handler.Chain( - handler.Decorate( - execute(client).CreateUser, - tracing.Decorate[*User, *User](u.tracer, tracing.WithSpanName("user.sql.Create")), - ), - handler.Decorate( - events(client).CreateUser, - tracing.Decorate[*User, *User](u.tracer, tracing.WithSpanName("user.event.Create")), - ), - ), - )(ctx, user) -} - -func (u *user) ByID(ctx context.Context, client database.Querier, id string) (*User, error) { - return handler.SkipNext( - handler.SkipNilHandler(u.cache, - handler.ResFuncToHandle(u.cache.ByID), - ), - handler.Chain( - handler.Decorate( - query(client).UserByID, - tracing.Decorate[string, *User](u.tracer, tracing.WithSpanName("user.sql.ByID")), - ), - handler.SkipNilHandler(u.custom.cache, handler.NoReturnToHandle(u.cache.Set)), - ), - )(ctx, id) -} - -type ChangeEmail struct { - UserID string - Email string - // Opt *ChangeEmailOption -} - -// type ChangeEmailOption struct { -// returnCode bool -// isVerified bool -// sendCode bool -// } - -// type ChangeEmailVerifiedOption struct { -// isVerified bool -// } - -// type ChangeEmailReturnCodeOption struct { -// alg crypto.EncryptionAlgorithm -// } - -// type ChangeEmailSendCodeOption struct { -// alg crypto.EncryptionAlgorithm -// urlTemplate string -// } - -func (u *user) ChangeEmail(ctx context.Context, client database.Executor, change *ChangeEmail) { - -} - -type EmailVerificationCode struct { - Code *crypto.CryptoValue - CreatedAt time.Time - Expiry time.Duration -} - -func (u *user) EmailVerificationCode(ctx context.Context, client database.Querier, userID string) (*EmailVerificationCode, error) { - return tracing.Wrap(u.tracer, "user.EmailVerificationCode", - handler.Decorate( - query(client).EmailVerificationCode, - tracing.Decorate[string, *EmailVerificationCode](u.tracer, tracing.WithSpanName("user.sql.EmailVerificationCode")), - ), - )(ctx, userID) -} - -func (u *user) EmailVerificationFailed(ctx context.Context, client database.Executor, code *EmailVerificationCode) error { - _, err := tracing.Wrap(u.tracer, "user.EmailVerificationFailed", - handler.ErrFuncToHandle(execute(client).EmailVerificationFailed), - )(ctx, code) - - return err -} - -func (u *user) EmailVerificationSucceeded(ctx context.Context, client database.Executor, code *EmailVerificationCode) error { - _, err := tracing.Wrap(u.tracer, "user.EmailVerificationSucceeded", - handler.ErrFuncToHandle(execute(client).EmailVerificationSucceeded), - )(ctx, code) - - return err -} diff --git a/backend/repository/user_cache.go b/backend/repository/user_cache.go deleted file mode 100644 index 92fce16268..0000000000 --- a/backend/repository/user_cache.go +++ /dev/null @@ -1,52 +0,0 @@ -package repository - -import ( - "context" - "log" - - "github.com/zitadel/zitadel/backend/storage/cache" -) - -type UserCache struct { - cache.Cache[UserIndex, string, *User] -} - -type UserIndex uint8 - -var UserIndices = []UserIndex{ - UserByIDIndex, - UserByUsernameIndex, -} - -const ( - UserByIDIndex UserIndex = iota - UserByUsernameIndex -) - -var _ cache.Entry[UserIndex, string] = (*User)(nil) - -// Keys implements [cache.Entry]. -func (u *User) Keys(index UserIndex) (key []string) { - switch index { - case UserByIDIndex: - return []string{u.ID} - case UserByUsernameIndex: - return []string{u.Username} - } - return nil -} - -func NewUserCache(c cache.Cache[UserIndex, string, *User]) *UserCache { - return &UserCache{c} -} - -func (c *UserCache) ByID(ctx context.Context, id string) *User { - log.Println("cached.user.byID") - user, _ := c.Cache.Get(ctx, UserByIDIndex, id) - return user -} - -func (c *UserCache) Set(ctx context.Context, user *User) { - log.Println("cached.user.set") - c.Cache.Set(ctx, user) -} diff --git a/backend/repository/user_db.go b/backend/repository/user_db.go deleted file mode 100644 index 4d394e544a..0000000000 --- a/backend/repository/user_db.go +++ /dev/null @@ -1,58 +0,0 @@ -package repository - -import ( - "context" - "errors" - "log" -) - -const userByIDQuery = `SELECT id, username FROM users WHERE id = $1` - -func (q *querier) UserByID(ctx context.Context, id string) (res *User, err error) { - log.Println("sql.user.byID") - row := q.client.QueryRow(ctx, userByIDQuery, id) - var user User - if err := row.Scan(&user.ID, &user.Username); err != nil { - return nil, err - } - return &user, nil -} - -const emailVerificationCodeStmt = `SELECT created_at, expiry,code FROM email_verification_codes WHERE user_id = $1` - -func (q *querier) EmailVerificationCode(ctx context.Context, userID string) (res *EmailVerificationCode, err error) { - log.Println("sql.user.emailVerificationCode") - - res = new(EmailVerificationCode) - err = q.client.QueryRow(ctx, emailVerificationCodeStmt, userID). - Scan( - &res.CreatedAt, - &res.Expiry, - &res.Code, - ) - if err != nil { - return nil, err - } - return res, nil -} - -func (e *executor) CreateUser(ctx context.Context, user *User) (res *User, err error) { - log.Println("sql.user.create") - err = e.client.Exec(ctx, "INSERT INTO users (id, username) VALUES ($1, $2)", user.ID, user.Username) - if err != nil { - return nil, err - } - return user, nil -} - -func (e *executor) EmailVerificationFailed(ctx context.Context, code *EmailVerificationCode) error { - return errors.New("not implemented") -} - -func (e *executor) EmailVerificationSucceeded(ctx context.Context, code *EmailVerificationCode) error { - return errors.New("not implemented") -} - -func (e *executor) SetEmail(ctx context.Context, userID, email string) error { - return errors.New("not implemented") -} diff --git a/backend/repository/user_event.go b/backend/repository/user_event.go deleted file mode 100644 index 5f1949cbe0..0000000000 --- a/backend/repository/user_event.go +++ /dev/null @@ -1,15 +0,0 @@ -package repository - -import ( - "context" - "log" -) - -func (s *eventStore) CreateUser(ctx context.Context, user *User) (*User, error) { - log.Println("event.user.create") - err := s.es.Push(ctx, user) - if err != nil { - return nil, err - } - return user, nil -} diff --git a/backend/storage/01_events.sql b/backend/storage/01_events.sql deleted file mode 100644 index a9c4d106ab..0000000000 --- a/backend/storage/01_events.sql +++ /dev/null @@ -1,225 +0,0 @@ -DROP TABLE IF EXISTS properties; -DROP TABLE IF EXISTS parents; -DROP TABLE IF EXISTS objects; - -CREATE TABLE IF NOT EXISTS objects ( - type TEXT NOT NULL - , id TEXT NOT NULL - - , PRIMARY KEY (type, id) -); - -TRUNCATE objects CASCADE; -INSERT INTO objects VALUES - ('instance', 'i1') - , ('organization', 'o1') - , ('user', 'u1') - , ('user', 'u2') - , ('organization', 'o2') - , ('user', 'u3') - , ('project', 'p3') - - , ('instance', 'i2') - , ('organization', 'o3') - , ('user', 'u4') - , ('project', 'p1') - , ('project', 'p2') - , ('application', 'a1') - , ('application', 'a2') - , ('org_domain', 'od1') - , ('org_domain', 'od2') -; - -CREATE TABLE IF NOT EXISTS parents ( - parent_type TEXT NOT NULL - , parent_id TEXT NOT NULL - , child_type TEXT NOT NULL - , child_id TEXT NOT NULL - , PRIMARY KEY (parent_type, parent_id, child_type, child_id) - , FOREIGN KEY (parent_type, parent_id) REFERENCES objects(type, id) ON DELETE CASCADE - , FOREIGN KEY (child_type, child_id) REFERENCES objects(type, id) ON DELETE CASCADE -); - -INSERT INTO parents VALUES - ('instance', 'i1', 'organization', 'o1') - , ('organization', 'o1', 'user', 'u1') - , ('organization', 'o1', 'user', 'u2') - , ('instance', 'i1', 'organization', 'o2') - , ('organization', 'o2', 'user', 'u3') - , ('organization', 'o2', 'project', 'p3') - - , ('instance', 'i2', 'organization', 'o3') - , ('organization', 'o3', 'user', 'u4') - , ('organization', 'o3', 'project', 'p1') - , ('organization', 'o3', 'project', 'p2') - , ('project', 'p1', 'application', 'a1') - , ('project', 'p2', 'application', 'a2') - , ('organization', 'o3', 'org_domain', 'od1') - , ('organization', 'o3', 'org_domain', 'od2') -; - -CREATE TABLE properties ( - object_type TEXT NOT NULL - , object_id TEXT NOT NULL - , key TEXT NOT NULL - , value JSONB NOT NULL - , should_index BOOLEAN NOT NULL DEFAULT FALSE - - , PRIMARY KEY (object_type, object_id, key) - , FOREIGN KEY (object_type, object_id) REFERENCES objects(type, id) ON DELETE CASCADE -); - -CREATE INDEX properties_object_indexed ON properties (object_type, object_id) INCLUDE (value) WHERE should_index; -CREATE INDEX properties_value_indexed ON properties (object_type, key, value) WHERE should_index; - -TRUNCATE properties; -INSERT INTO properties VALUES - ('instance', 'i1', 'name', '"Instance 1"', TRUE) - , ('instance', 'i1', 'description', '"Instance 1 description"', FALSE) - , ('instance', 'i2', 'name', '"Instance 2"', TRUE) - , ('organization', 'o1', 'name', '"Organization 1"', TRUE) - , ('org_domain', 'od1', 'domain', '"example.com"', TRUE) - , ('org_domain', 'od1', 'is_primary', 'true', TRUE) - , ('org_domain', 'od1', 'is_verified', 'true', FALSE) - , ('org_domain', 'od2', 'domain', '"example.org"', TRUE) - , ('org_domain', 'od2', 'is_primary', 'false', TRUE) - , ('org_domain', 'od2', 'is_verified', 'false', FALSE) -; - -CREATE TABLE events ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid() - , type TEXT NOT NULL - , created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() - , revision SMALLINT NOT NULL - , creator TEXT NOT NULL - , payload JSONB - - , global_sequence NUMERIC NOT NULL DEFAULT pg_current_xact_id()::TEXT::NUMERIC - , sequence_order SMALLINT NOT NULL CHECK (sequence_order >= 0) - - -- , object_type TEXT NOT NULL - -- , object_id TEXT NOT NULL - - -- , FOREIGN KEY (object_type, object_id) REFERENCES objects(type, id) -); - -CREATE TYPE property ( - -- key must be a json path - key TEXT - -- value should be a primitive type - , value JSONB - -- indicates wheter the property should be indexed - , should_index BOOLEAN -); - -CREATE TYPE parent ( - parent_type TEXT - , parent_id TEXT -); - -CREATE TYPE object ( - type TEXT - , id TEXT - , properties property[] - -- an object automatically inherits the parents of its parent - , parents parent[] -); - -CREATE TYPE command ( - type TEXT - , revision SMALLINT - , creator TEXT - , payload JSONB - - -- if properties is null the objects and all its child objects get deleted - -- if the value of a property is null the property and all sub fields get deleted - -- for example if the key is 'a.b' and the value is null the property 'a.b.c' will be deleted as well - , objects object[] -); - -CREATE OR REPLACE PROCEDURE update_object(_object object) -AS $$ -DECLARE - _property property; -BEGIN - FOR _property IN ARRAY _object.properties LOOP - IF _property.value IS NULL THEN - DELETE FROM properties - WHERE object_type = _object.type - AND object_id = _object.id - AND key LIKE CONCAT(_property.key, '%'); - ELSE - INSERT INTO properties (object_type, object_id, key, value, should_index) - VALUES (_object.type, _object.id, _property.key, _property.value, _property.should_index) - ON CONFLICT (object_type, object_id, key) DO UPDATE SET (value, should_index) = (_property.value, _property.should_index); - END IF; - END LOOP; -END; - -CREATE OR REPLACE PROCEDURE delete_object(_type, _id) -AS $$ -BEGIN - WITH RECURSIVE objects_to_delete (_type, _id) AS ( - SELECT $1, $2 - - UNION - - SELECT p.child_type, p.child_id - FROM parents p - JOIN objects_to_delete o ON p.parent_type = o.type AND p.parent_id = o.id - ) - DELETE FROM objects - WHERE (type, id) IN (SELECT * FROM objects_to_delete) -END; - -CREATE OR REPLACE FUNCTION push(_commands command[]) -RETURNS NUMMERIC AS $$ -DECLARE - _command command; - _index INT; - - _object object; -BEGIN - FOR _index IN 1..array_length(_commands, 1) LOOP - _command := _commands[_index]; - INSERT INTO events (type, revision, creator, payload) - VALUES (_command.type, _command.revision, _command.creator, _command.payload); - - FOREACH _object IN ARRAY _command.objects LOOP - IF _object.properties IS NULL THEN - PERFORM delete_object(_object.type, _object.id); - ELSE - PERFORM update_object(_object); - END IF; - END LOOP; - RETURN pg_current_xact_id()::TEXT::NUMERIC; -END; -$$ LANGUAGE plpgsql; - - -BEGIN; - - -RETURNING * -; - -rollback; - -SELECT - * -FROM - properties -WHERE - (object_type, object_id) IN ( - SELECT - object_type - , object_id - FROM - properties - where - object_type = 'instance' - and key = 'name' - and value = '"Instance 1"' - and should_index - ) -; \ No newline at end of file diff --git a/backend/storage/02_next_try.sql b/backend/storage/02_next_try.sql deleted file mode 100644 index 2c7243508c..0000000000 --- a/backend/storage/02_next_try.sql +++ /dev/null @@ -1,310 +0,0 @@ --- postgres -DROP TABLE IF EXISTS properties; -DROP TABLE IF EXISTS parents CASCADE; -DROP TABLE IF EXISTS objects CASCADE; -DROP TABLE IF EXISTS indexed_properties; -DROP TABLE IF EXISTS events; -DROP TABLE IF EXISTS models; - -DROP TYPE IF EXISTS object CASCADe; -DROP TYPE IF EXISTS model CASCADE; - -CREATE TYPE model AS ( - name TEXT - , id TEXT -); - -CREATE TYPE object AS ( - model TEXT - , model_revision SMALLINT - , id TEXT - , payload JSONB - , parents model[] -); - -CREATE TABLE models ( - name TEXT - , revision SMALLINT NOT NULL CONSTRAINT positive_revision CHECK (revision > 0) - , indexed_paths TEXT[] - - , PRIMARY KEY (name, revision) -); - -CREATE TABLE objects ( - model TEXT NOT NULL - , model_revision SMALLINT NOT NULL - - , id TEXT NOT NULL - , payload JSONB - - , PRIMARY KEY (model, id) - , FOREIGN KEY (model, model_revision) REFERENCES models(name, revision) ON DELETE RESTRICT -); - -CREATE TABLE indexed_properties ( - model TEXT NOT NULL - , model_revision SMALLINT NOT NULL - , object_id TEXT NOT NULL - - , path TEXT NOT NULL - - , value JSONB - , text_value TEXT - , number_value NUMERIC - , boolean_value BOOLEAN - - , PRIMARY KEY (model, object_id, path) - , FOREIGN KEY (model, object_id) REFERENCES objects(model, id) ON DELETE CASCADE - , FOREIGN KEY (model, model_revision) REFERENCES models(name, revision) ON DELETE RESTRICT -); - -CREATE OR REPLACE FUNCTION ip_value_converter() -RETURNS TRIGGER AS $$ -BEGIN - CASE jsonb_typeof(NEW.value) - WHEN 'boolean' THEN - NEW.boolean_value := NEW.value::BOOLEAN; - NEW.value := NULL; - WHEN 'number' THEN - NEW.number_value := NEW.value::NUMERIC; - NEW.value := NULL; - WHEN 'string' THEN - NEW.text_value := (NEW.value#>>'{}')::TEXT; - NEW.value := NULL; - ELSE - -- do nothing - END CASE; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER ip_value_converter_before_insert -BEFORE INSERT -ON indexed_properties -FOR EACH ROW -EXECUTE FUNCTION ip_value_converter(); - -CREATE TRIGGER ip_value_converter_before_update -BEFORE UPDATE -ON indexed_properties -FOR EACH ROW -EXECUTE FUNCTION ip_value_converter(); - -CREATE INDEX ip_search_model_object ON indexed_properties (model, path, value) WHERE value IS NOT NULL; -CREATE INDEX ip_search_model_rev_object ON indexed_properties (model, model_revision, path, value) WHERE value IS NOT NULL; -CREATE INDEX ip_search_model_text ON indexed_properties (model, path, text_value) WHERE text_value IS NOT NULL; -CREATE INDEX ip_search_model_rev_text ON indexed_properties (model, model_revision, path, text_value) WHERE text_value IS NOT NULL; -CREATE INDEX ip_search_model_number ON indexed_properties (model, path, number_value) WHERE number_value IS NOT NULL; -CREATE INDEX ip_search_model_rev_number ON indexed_properties (model, model_revision, path, number_value) WHERE number_value IS NOT NULL; -CREATE INDEX ip_search_model_boolean ON indexed_properties (model, path, boolean_value) WHERE boolean_value IS NOT NULL; -CREATE INDEX ip_search_model_rev_boolean ON indexed_properties (model, model_revision, path, boolean_value) WHERE boolean_value IS NOT NULL; - -CREATE TABLE IF NOT EXISTS parents ( - parent_model TEXT NOT NULL - , parent_id TEXT NOT NULL - , child_model TEXT NOT NULL - , child_id TEXT NOT NULL - - , PRIMARY KEY (parent_model, parent_id, child_model, child_id) - , FOREIGN KEY (parent_model, parent_id) REFERENCES objects(model, id) ON DELETE CASCADE - , FOREIGN KEY (child_model, child_id) REFERENCES objects(model, id) ON DELETE CASCADE -); - -INSERT INTO models VALUES - ('instance', 1, ARRAY['name', 'domain.name']) - , ('organization', 1, ARRAY['name']) - , ('user', 1, ARRAY['username', 'email', 'firstname', 'lastname']) -; - -CREATE OR REPLACE FUNCTION jsonb_to_rows(j jsonb, _path text[] DEFAULT ARRAY[]::text[]) -RETURNS TABLE (path text[], value jsonb) -LANGUAGE plpgsql -AS $$ -DECLARE - k text; - v jsonb; -BEGIN - FOR k, v IN SELECT * FROM jsonb_each(j) LOOP - IF jsonb_typeof(v) = 'object' THEN - -- Recursive call for nested objects, appending the key to the path - RETURN QUERY SELECT * FROM jsonb_to_rows(v, _path || k) - UNION VALUES (_path, '{}'::JSONB); - ELSE - -- Base case: return the key path and value - CASE WHEN jsonb_typeof(v) = 'null' THEN - RETURN QUERY SELECT _path || k, NULL::jsonb; - ELSE - RETURN QUERY SELECT _path || k, v; - END CASE; - END IF; - END LOOP; -END; -$$; - -CREATE OR REPLACE FUNCTION merge_payload(_old JSONB, _new JSONB) -RETURNS JSONB -LANGUAGE plpgsql -AS $$ -DECLARE - _fields CURSOR FOR SELECT DISTINCT ON (path) - path - , last_value(value) over (partition by path) as value - FROM ( - SELECT path, value FROM jsonb_to_rows(_old) - UNION ALL - SELECT path, value FROM jsonb_to_rows(_new) - ); - _path text[]; - _value jsonb; -BEGIN - OPEN _fields; - LOOP - FETCH _fields INTO _path, _value; - EXIT WHEN NOT FOUND; - IF jsonb_typeof(_value) = 'object' THEN - IF _old #> _path IS NOT NULL THEN - CONTINUE; - END IF; - _old = jsonb_set_lax(_old, _path, '{}'::jsonb, TRUE); - CONTINUE; - END IF; - - _old = jsonb_set_lax(_old, _path, _value, TRUE, 'delete_key'); - END LOOP; - - RETURN _old; -END; -$$; - -CREATE OR REPLACE FUNCTION set_object(_object object) -RETURNS VOID AS $$ -DECLARE - _parent model; -BEGIN - INSERT INTO objects (model, model_revision, id, payload) - VALUES (_object.model, _object.model_revision, _object.id, _object.payload) - ON CONFLICT (model, id) DO UPDATE - SET - payload = merge_payload(objects.payload, EXCLUDED.payload) - , model_revision = EXCLUDED.model_revision; - - INSERT INTO indexed_properties (model, model_revision, object_id, path, value) - SELECT - * - FROM ( - SELECT - _object.model - , _object.model_revision - , _object.id - , UNNEST(m.indexed_paths) AS "path" - , _object.payload #> string_to_array(UNNEST(m.indexed_paths), '.') AS "value" - FROM - models m - WHERE - m.name = _object.model - AND m.revision = _object.model_revision - GROUP BY - m.name - , m.revision - ) - WHERE - "value" IS NOT NULL - ON CONFLICT (model, object_id, path) DO UPDATE - SET - value = EXCLUDED.value - , text_value = EXCLUDED.text_value - , number_value = EXCLUDED.number_value - , boolean_value = EXCLUDED.boolean_value - ; - - INSERT INTO parents (parent_model, parent_id, child_model, child_id) - VALUES - (_object.model, _object.id, _object.model, _object.id) - ON CONFLICT (parent_model, parent_id, child_model, child_id) DO NOTHING; - - IF _object.parents IS NULL THEN - RETURN; - END IF; - - FOREACH _parent IN ARRAY _object.parents - LOOP - INSERT INTO parents (parent_model, parent_id, child_model, child_id) - SELECT - p.parent_model - , p.parent_id - , _object.model - , _object.id - FROM parents p - WHERE - p.child_model = _parent.name - AND p.child_id = _parent.id - ON CONFLICT (parent_model, parent_id, child_model, child_id) DO NOTHING - ; - - INSERT INTO parents (parent_model, parent_id, child_model, child_id) - VALUES - (_parent.name, _parent.id, _object.model, _object.id) - ON CONFLICT (parent_model, parent_id, child_model, child_id) DO NOTHING; - END LOOP; -END; -$$ LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION set_objects(_objects object[]) -RETURNS VOID AS $$ -DECLARE - _object object; -BEGIN - FOREACH _object IN ARRAY _objects - LOOP - PERFORM set_object(_object); - END LOOP; -END; -$$ LANGUAGE plpgsql; - --- CREATE OR REPLACE FUNCTION set_objects(VARIADIC _objects object[]) --- RETURNS VOID AS $$ --- BEGIN --- PERFORM set_objectS(_objects); --- END; --- $$ LANGUAGE plpgsql; - -SELECT set_objects( - ARRAY[ - ROW('instance', 1::smallint, 'i1', '{"name": "i1", "domain": {"name": "example2.com", "isVerified": false}}', NULL)::object - , ROW('organization', 1::smallint, 'o1', '{"name": "o1", "description": "something useful"}', ARRAY[ - ROW('instance', 'i1')::model - ])::object - , ROW('user', 1::smallint, 'u1', '{"username": "u1", "description": "something useful", "firstname": "Silvan"}', ARRAY[ - ROW('instance', 'i1')::model - , ROW('organization', 'o1')::model - ])::object - ] -); - -SELECT set_objects( - ARRAY[ - ROW('instance', 1::smallint, 'i1', '{"domain": {"isVerified": true}}', NULL)::object - ] -); - - -SELECT - o.* -FROM - indexed_properties ip -JOIN - objects o -ON - ip.model = o.model - AND ip.object_id = o.id -WHERE - ip.model = 'instance' - AND ip.path = 'name' - AND ip.text_value = 'i1'; -; - -select * from merge_payload( - '{"a": "asdf", "b": {"c":{"d": 1, "g": {"h": [4,5,6]}}}, "f": [1,2,3]}'::jsonb - , '{"b": {"c":{"d": 1, "g": {"i": [4,5,6]}}}, "a": null}'::jsonb -); \ No newline at end of file diff --git a/backend/storage/03_properties.sql b/backend/storage/03_properties.sql deleted file mode 100644 index edc181ada5..0000000000 --- a/backend/storage/03_properties.sql +++ /dev/null @@ -1,272 +0,0 @@ --- postgres -DROP TABLE IF EXISTS properties; -DROP TABLE IF EXISTS parents CASCADE; -DROP TABLE IF EXISTS objects CASCADE; -DROP TABLE IF EXISTS indexed_properties; -DROP TABLE IF EXISTS events; -DROP TABLE IF EXISTS models; - -DROP TYPE IF EXISTS object CASCADe; -DROP TYPE IF EXISTS model CASCADE; - -CREATE TYPE model AS ( - name TEXT - , id TEXT -); - -CREATE TYPE object AS ( - model TEXT - , model_revision SMALLINT - , id TEXT - , payload JSONB - , parents model[] -); - -CREATE TABLE models ( - name TEXT - , revision SMALLINT NOT NULL CONSTRAINT positive_revision CHECK (revision > 0) - , indexed_paths TEXT[] - - , PRIMARY KEY (name, revision) -); - -CREATE TABLE objects ( - model TEXT NOT NULL - , model_revision SMALLINT NOT NULL - - , id TEXT NOT NULL - , payload JSONB - - , PRIMARY KEY (model, id) - , FOREIGN KEY (model, model_revision) REFERENCES models(name, revision) ON DELETE RESTRICT -); - -CREATE TABLE indexed_properties ( - model TEXT NOT NULL - , model_revision SMALLINT NOT NULL - , object_id TEXT NOT NULL - - , path TEXT[] NOT NULL - - , value JSONB - , text_value TEXT - , number_value NUMERIC - , boolean_value BOOLEAN - - , PRIMARY KEY (model, object_id, path) - , FOREIGN KEY (model, object_id) REFERENCES objects(model, id) ON DELETE CASCADE - , FOREIGN KEY (model, model_revision) REFERENCES models(name, revision) ON DELETE RESTRICT -); - -CREATE TABLE IF NOT EXISTS parents ( - parent_model TEXT NOT NULL - , parent_id TEXT NOT NULL - , child_model TEXT NOT NULL - , child_id TEXT NOT NULL - , PRIMARY KEY (parent_model, parent_id, child_model, child_id) - , FOREIGN KEY (parent_model, parent_id) REFERENCES objects(model, id) ON DELETE CASCADE - , FOREIGN KEY (child_model, child_id) REFERENCES objects(model, id) ON DELETE CASCADE -); - -CREATE OR REPLACE FUNCTION jsonb_to_rows(j jsonb, _path text[] DEFAULT ARRAY[]::text[]) -RETURNS TABLE (path text[], value jsonb) -LANGUAGE plpgsql -AS $$ -DECLARE - k text; - v jsonb; -BEGIN - FOR k, v IN SELECT * FROM jsonb_each(j) LOOP - IF jsonb_typeof(v) = 'object' THEN - -- Recursive call for nested objects, appending the key to the path - RETURN QUERY SELECT * FROM jsonb_to_rows(v, _path || k); - ELSE - -- Base case: return the key path and value - CASE WHEN jsonb_typeof(v) = 'null' THEN - RETURN QUERY SELECT _path || k, NULL::jsonb; - ELSE - RETURN QUERY SELECT _path || k, v; - END CASE; - END IF; - END LOOP; -END; -$$; - --- after insert trigger which is called after the object was inserted and then inserts the properties -CREATE OR REPLACE FUNCTION set_ip_from_object_insert() -RETURNS TRIGGER -LANGUAGE plpgsql -AS $$ -DECLARE - _property RECORD; - _model models; -BEGIN - SELECT * INTO _model FROM models WHERE name = NEW.model AND revision = NEW.model_revision; - - FOR _property IN SELECT * FROM jsonb_to_rows(NEW.payload) LOOP - IF ARRAY_TO_STRING(_property.path, '.') = ANY(_model.indexed_paths) THEN - INSERT INTO indexed_properties (model, model_revision, object_id, path, value) - VALUES (NEW.model, NEW.model_revision, NEW.id, _property.path, _property.value); - END IF; - END LOOP; - RETURN NULL; -END; -$$; - -CREATE TRIGGER set_ip_from_object_insert -AFTER INSERT ON objects -FOR EACH ROW -EXECUTE FUNCTION set_ip_from_object_insert(); - --- before update trigger with is called before an object is updated --- it updates the properties table first --- and computes the correct payload for the object --- partial update of the object is allowed --- if the value of a property is set to null the properties and all its children are deleted -CREATE OR REPLACE FUNCTION set_ip_from_object_update() -RETURNS TRIGGER -LANGUAGE plpgsql -AS $$ -DECLARE - _property RECORD; - _payload JSONB; - _model models; - _path_index INT; -BEGIN - _payload := OLD.payload; - SELECT * INTO _model FROM models WHERE name = NEW.model AND revision = NEW.model_revision; - - FOR _property IN SELECT * FROM jsonb_to_rows(NEW.payload) ORDER BY array_length(path, 1) LOOP - -- set the properties - CASE WHEN _property.value IS NULL THEN - RAISE NOTICE 'DELETE PROPERTY: %', _property; - DELETE FROM indexed_properties - WHERE model = NEW.model - AND model_revision = NEW.model_revision - AND object_id = NEW.id - AND path[:ARRAY_LENGTH(_property.path, 1)] = _property.path; - ELSE - RAISE NOTICE 'UPSERT PROPERTY: %', _property; - DELETE FROM indexed_properties - WHERE - model = NEW.model - AND model_revision = NEW.model_revision - AND object_id = NEW.id - AND ( - _property.path[:array_length(path, 1)] = path - OR path[:array_length(_property.path, 1)] = _property.path - ) - AND array_length(path, 1) <> array_length(_property.path, 1); - - -- insert property if should be indexed - IF ARRAY_TO_STRING(_property.path, '.') = ANY(_model.indexed_paths) THEN - RAISE NOTICE 'path should be indexed: %', _property.path; - INSERT INTO indexed_properties (model, model_revision, object_id, path, value) - VALUES (NEW.model, NEW.model_revision, NEW.id, _property.path, _property.value) - ON CONFLICT (model, object_id, path) DO UPDATE - SET value = EXCLUDED.value; - END IF; - END CASE; - - -- if property is updated we can set it directly - IF _payload #> _property.path IS NOT NULL THEN - _payload = jsonb_set_lax(COALESCE(_payload, '{}'::JSONB), _property.path, _property.value, TRUE); - EXIT; - END IF; - -- ensure parent object exists exists - FOR _path_index IN 1..(array_length(_property.path, 1)-1) LOOP - IF _payload #> _property.path[:_path_index] IS NOT NULL AND jsonb_typeof(_payload #> _property.path[:_path_index]) = 'object' THEN - CONTINUE; - END IF; - - _payload = jsonb_set(_payload, _property.path[:_path_index], '{}'::JSONB, TRUE); - EXIT; - END LOOP; - _payload = jsonb_set_lax(_payload, _property.path, _property.value, TRUE, 'delete_key'); - - END LOOP; - - -- update the payload - NEW.payload = _payload; - - RETURN NEW; -END; -$$; - -CREATE OR REPLACE TRIGGER set_ip_from_object_update -BEFORE UPDATE ON objects -FOR EACH ROW -EXECUTE FUNCTION set_ip_from_object_update(); - - -CREATE OR REPLACE FUNCTION set_object(_object object) -RETURNS VOID -LANGUAGE plpgsql -AS $$ -BEGIN - INSERT INTO objects (model, model_revision, id, payload) - VALUES (_object.model, _object.model_revision, _object.id, _object.payload) - ON CONFLICT (model, id) DO UPDATE - SET - payload = EXCLUDED.payload - , model_revision = EXCLUDED.model_revision - ; - - INSERT INTO parents (parent_model, parent_id, child_model, child_id) - SELECT - p.name - , p.id - , _object.model - , _object.id - FROM UNNEST(_object.parents) AS p - ON CONFLICT DO NOTHING; -END; -$$; - -CREATE OR REPLACE FUNCTION set_objects(_objects object[]) -RETURNS VOID -LANGUAGE plpgsql -AS $$ -DECLARE - _object object; -BEGIN - FOREACH _object IN ARRAY _objects LOOP - PERFORM set_object(_object); - END LOOP; -END; -$$; - - - - - -INSERT INTO models VALUES - ('instance', 1, ARRAY['name', 'domain.name']) - , ('organization', 1, ARRAY['name']) - , ('user', 1, ARRAY['username', 'email', 'firstname', 'lastname']) -; - -INSERT INTO objects VALUES - ('instance', 1, 'i2', '{"name": "i2", "domain": {"name": "example2.com", "isVerified": false}}') - , ('instance', 1, 'i3', '{"name": "i3", "domain": {"name": "example3.com", "isVerified": false}}') - , ('instance', 1, 'i4', '{"name": "i4", "domain": {"name": "example4.com", "isVerified": false}}') -; - - -begin; -UPDATE objects SET payload = '{"domain": {"isVerified": true}}' WHERE model = 'instance'; -rollback; - - -SELECT set_objects( - ARRAY[ - ROW('instance', 1::smallint, 'i1', '{"name": "i1", "domain": {"name": "example2.com", "isVerified": false}}', NULL)::object - , ROW('organization', 1::smallint, 'o1', '{"name": "o1", "description": "something useful"}', ARRAY[ - ROW('instance', 'i1')::model - ])::object - , ROW('user', 1::smallint, 'u1', '{"username": "u1", "description": "something useful", "firstname": "Silvan"}', ARRAY[ - ROW('instance', 'i1')::model - , ROW('organization', 'o1')::model - ])::object - ] -); \ No newline at end of file diff --git a/backend/storage/04_operations.sql b/backend/storage/04_operations.sql deleted file mode 100644 index 1fcd725499..0000000000 --- a/backend/storage/04_operations.sql +++ /dev/null @@ -1,280 +0,0 @@ --- postgres -DROP TABLE IF EXISTS properties; -DROP TABLE IF EXISTS parents CASCADE; -DROP TABLE IF EXISTS objects CASCADE; -DROP TABLE IF EXISTS indexed_properties; -DROP TABLE IF EXISTS events; -DROP TABLE IF EXISTS models; - -DROP TYPE IF EXISTS object CASCADe; -DROP TYPE IF EXISTS model CASCADE; - -CREATE TABLE models ( - name TEXT - , revision SMALLINT NOT NULL CONSTRAINT positive_revision CHECK (revision > 0) - , indexed_paths TEXT[] - - , PRIMARY KEY (name, revision) -); - -CREATE TABLE objects ( - model TEXT NOT NULL - , model_revision SMALLINT NOT NULL - - , id TEXT NOT NULL - , payload JSONB - - , PRIMARY KEY (model, id) - , FOREIGN KEY (model, model_revision) REFERENCES models(name, revision) ON DELETE RESTRICT -); - -CREATE TYPE operation_type AS ENUM ( - -- inserts a new object, if the object already exists the operation will fail - -- path is ignored - 'create' - -- if path is null an upsert is performed and the payload is overwritten - -- if path is not null the value is set at the given path - , 'set' - -- drops an object if path is null - -- if path is set but no value, the field at the given path is dropped - -- if path and value are set and the field is an array the value is removed from the array - , 'delete' - -- adds a value to an array - -- or a field if it does not exist, if the field exists the operation will fail - , 'add' -); - -CREATE TYPE object_manipulation AS ( - path TEXT[] - , operation operation_type - , value JSONB -); - -CREATE TABLE IF NOT EXISTS parents ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid() - , parent_model TEXT NOT NULL - , parent_id TEXT NOT NULL - , child_model TEXT NOT NULL - , child_id TEXT NOT NULL - - , FOREIGN KEY (parent_model, parent_id) REFERENCES objects(model, id) ON DELETE CASCADE - , FOREIGN KEY (child_model, child_id) REFERENCES objects(model, id) ON DELETE CASCADE -); - -CREATE TYPE parent_operation AS ENUM ( - 'add' - , 'remove' -); - -CREATE TYPE parent_manipulation AS ( - model TEXT - , id TEXT - , operation parent_operation -); - -CREATE OR REPLACE FUNCTION jsonb_set_ensure_path(_jsonb JSONB, _path TEXT[], _value JSONB) -RETURNS JSONB -LANGUAGE plpgsql -AS $$ -DECLARE - i INT; -BEGIN - IF _jsonb #> _path IS NOT NULL THEN - RETURN JSONB_SET(_jsonb, _path, _value); - END IF; - - FOR i IN REVERSE ARRAY_LENGTH(_path, 1)..1 LOOP - _value := JSONB_BUILD_OBJECT(_path[i], _value); - IF _jsonb #> _path[:i] IS NOT NULL THEN - EXIT; - END IF; - END LOOP; - - RETURN _jsonb || _value; -END; -$$; - --- current: {} --- '{"a": {"b": {"c": {"d": {"e": 1}}}}}'::JSONB #> '{a,b,c}' = 1 - -drop function manipulate_object; -drop function object_set; -CREATE OR REPLACE FUNCTION object_set( - _model TEXT - , _model_revision SMALLINT - , _id TEXT - - , _manipulations object_manipulation[] - , _parents parent_manipulation[] -) -RETURNS objects -LANGUAGE plpgsql -AS $$ -DECLARE - _manipulation object_manipulation; -BEGIN - FOREACH _manipulation IN ARRAY _manipulations LOOP - CASE _manipulation.operation - WHEN 'create' THEN - INSERT INTO objects (model, model_revision, id, payload) - VALUES (_model, _model_revision, _id, _manipulation.value); - WHEN 'delete' THEN - CASE - WHEN _manipulation.path IS NULL THEN - DELETE FROM objects - WHERE - model = _model - AND model_revision = _model_revision - AND id = _id; - WHEN _manipulation.value IS NULL THEN - UPDATE - objects - SET - payload = payload #- _manipulation.path - WHERE - model = _model - AND model_revision = _model_revision - AND id = _id; - ELSE - UPDATE - objects - SET - payload = jsonb_set(payload, _manipulation.path, (SELECT JSONB_AGG(v) FROM JSONB_PATH_QUERY(payload, ('$.' || ARRAY_TO_STRING(_manipulation.path, '.') || '[*]')::jsonpath) AS v WHERE v <> _manipulation.value)) - WHERE - model = _model - AND model_revision = _model_revision - AND id = _id; - END CASE; - WHEN 'set' THEN - IF _manipulation.path IS NULL THEN - INSERT INTO objects (model, model_revision, id, payload) - VALUES (_model, _model_revision, _id, _manipulation.value) - ON CONFLICT (model, model_revision, id) - DO UPDATE SET payload = _manipulation.value; - ELSE - UPDATE - objects - SET - payload = jsonb_set_ensure_path(payload, _manipulation.path, _manipulation.value) - WHERE - model = _model - AND model_revision = _model_revision - AND id = _id; - END IF; - WHEN 'add' THEN - UPDATE - objects - SET - -- TODO: parent field must exist - payload = CASE - WHEN jsonb_typeof(payload #> _manipulation.path) IS NULL THEN - jsonb_set_ensure_path(payload, _manipulation.path, _manipulation.value) - WHEN jsonb_typeof(payload #> _manipulation.path) = 'array' THEN - jsonb_set(payload, _manipulation.path, COALESCE(payload #> _manipulation.path, '[]'::JSONB) || _manipulation.value) - -- ELSE - -- RAISE EXCEPTION 'Field at path % is not an array', _manipulation.path; - END - WHERE - model = _model - AND model_revision = _model_revision - AND id = _id; - -- TODO: RAISE EXCEPTION 'Field at path % is not an array', _manipulation.path; - END CASE; - END LOOP; - - FOREACH _parent IN ARRAY _parents LOOP - CASE _parent.operation - WHEN 'add' THEN - -- insert the new parent and all its parents - INSERT INTO parents (parent_model, parent_id, child_model, child_id) - ( - SELECT - id - FROM parents p - WHERE - p.child_model = _parent.model - AND p.child_id = _parent.id - UNION - SELECT - _parent.model - , _parent.id - , _model - , _id - ) - ON CONFLICT (parent_model, parent_id, child_model, child_id) DO NOTHING; - WHEN 'remove' THEN - -- remove the parent including the objects childs parent - DELETE FROM parents - WHERE id IN ( - SELECT - id - FROM - parents p - WHERE - p.child_model = _model - AND p.child_id = _id - AND p.parent_model = _parent.model - AND p.parent_id = _parent.id - UNION - SELECT - id - FROM ( - SELECT - id - FROM - parents p - WHERE - p.parent_model = _model - AND p.parent_id = _id - ) - WHERE - - ); - END CASE; - END LOOP; - RETURN NULL; -END; -$$; - -INSERT INTO models VALUES - ('instance', 1, ARRAY['name', 'domain.name']) - , ('organization', 1, ARRAY['name']) - , ('user', 1, ARRAY['username', 'email', 'firstname', 'lastname']) -; - -rollback; -BEGIN; -SELECT * FROM manipulate_object( - 'instance' - , 1::SMALLINT - , 'i1' - , ARRAY[ - ROW(NULL, 'create', '{"name": "i1"}'::JSONB)::object_manipulation - , ROW(ARRAY['domain'], 'set', '{"name": "example.com", "isVerified": false}'::JSONB)::object_manipulation - , ROW(ARRAY['domain', 'isVerified'], 'set', 'true'::JSONB)::object_manipulation - , ROW(ARRAY['domain', 'name'], 'delete', NULL)::object_manipulation - , ROW(ARRAY['domain', 'name'], 'add', '"i1.com"')::object_manipulation - , ROW(ARRAY['managers'], 'set', '[]'::JSONB)::object_manipulation - , ROW(ARRAY['managers', 'objects'], 'add', '[{"a": "asdf"}, {"a": "qewr"}]'::JSONB)::object_manipulation - , ROW(ARRAY['managers', 'objects'], 'delete', '{"a": "asdf"}'::JSONB)::object_manipulation - , ROW(ARRAY['some', 'objects'], 'set', '{"a": "asdf"}'::JSONB)::object_manipulation - -- , ROW(NULL, 'delete', NULL)::object_manipulation - ] -); -select * from objects; -ROLLBACK; - -BEGIN; -SELECT * FROM manipulate_object( - 'instance' - , 1::SMALLINT - , 'i1' - , ARRAY[ - ROW(NULL, 'create', '{"name": "i1"}'::JSONB)::object_manipulation - , ROW(ARRAY['domain', 'name'], 'set', '"example.com"'::JSONB)::object_manipulation - ] -); -select * from objects; -ROLLBACK; - -select jsonb_path_query_array('{"a": [12, 13, 14, 15]}'::JSONB, ('$.a ? (@ != $val)')::jsonpath, jsonb_build_object('val', '12')); \ No newline at end of file diff --git a/backend/storage/05_event_from_manipulation.sql b/backend/storage/05_event_from_manipulation.sql deleted file mode 100644 index 371aa1bb7a..0000000000 --- a/backend/storage/05_event_from_manipulation.sql +++ /dev/null @@ -1,62 +0,0 @@ -CREATE TABLE IF NOT EXISTS aggregates( - id TEXT NOT NULL - , type TEXT NOT NULL - , instance_id TEXT NOT NULL - - , current_sequence INT NOT NULL DEFAULT 0 - - , PRIMARY KEY (instance_id, type, id) -); - -CREATE TABLE IF NOT EXISTS events ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid() - - -- object type that the event is related to - , aggregate TEXT NOT NULL - -- id of the object that the event is related to - , aggregate_id TEXT NOT NULL - , instance_id TEXT NOT NULL - - -- time the event was created - , created_at TIMESTAMPTZ NOT NULL DEFAULT now() - -- user that created the event - , creator TEXT - -- type of the event - , type TEXT NOT NULL - -- version of the event - , revision SMALLINT NOT NULL - -- changed fields or NULL - , payload JSONB - - , position NUMERIC NOT NULL DEFAULT pg_current_xact_id()::TEXT::NUMERIC - , in_position_order INT2 NOT NULL - - , FOREIGN KEY (instance_id, aggregate, aggregate_id) REFERENCES aggregates(instance_id, type, id) ON DELETE CASCADE -); - -CREATE TABLE IF NOT EXISTS instances( - id TEXT - , name TEXT NOT NULL - , created_at TIMESTAMPTZ NOT NULL - , updated_at TIMESTAMPTZ NOT NULL - - , default_org_id TEXT - , iam_project_id TEXT - , console_client_id TEXT - , console_app_id TEXT - , default_language VARCHAR(10) - - , PRIMARY KEY (id) -); - -CREATE TABLE IF NOT EXISTS instance_domains( - instance_id TEXT NOT NULL - , domain TEXT NOT NULL - , is_primary BOOLEAN NOT NULL DEFAULT FALSE - , is_verified BOOLEAN NOT NULL DEFAULT FALSE - - , PRIMARY KEY (instance_id, domain) - , FOREIGN KEY (instance_id) REFERENCES instances(id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS instance_domain_search_idx ON instance_domains (domain); \ No newline at end of file diff --git a/backend/storage/cache/cache.go b/backend/storage/cache/cache.go deleted file mode 100644 index dc05208caa..0000000000 --- a/backend/storage/cache/cache.go +++ /dev/null @@ -1,112 +0,0 @@ -// Package cache provides abstraction of cache implementations that can be used by zitadel. -package cache - -import ( - "context" - "time" - - "github.com/zitadel/logging" -) - -// Purpose describes which object types are stored by a cache. -type Purpose int - -//go:generate enumer -type Purpose -transform snake -trimprefix Purpose -const ( - PurposeUnspecified Purpose = iota - PurposeAuthzInstance - PurposeMilestones - PurposeOrganization - PurposeIdPFormCallback -) - -// Cache stores objects with a value of type `V`. -// Objects may be referred to by one or more indices. -// Implementations may encode the value for storage. -// This means non-exported fields may be lost and objects -// with function values may fail to encode. -// See https://pkg.go.dev/encoding/json#Marshal for example. -// -// `I` is the type by which indices are identified, -// typically an enum for type-safe access. -// Indices are defined when calling the constructor of an implementation of this interface. -// It is illegal to refer to an idex not defined during construction. -// -// `K` is the type used as key in each index. -// Due to the limitations in type constraints, all indices use the same key type. -// -// Implementations are free to use stricter type constraints or fixed typing. -type Cache[I, K comparable, V Entry[I, K]] interface { - // Get an object through specified index. - // An [IndexUnknownError] may be returned if the index is unknown. - // [ErrCacheMiss] is returned if the key was not found in the index, - // or the object is not valid. - Get(ctx context.Context, index I, key K) (V, bool) - - // Set an object. - // Keys are created on each index based in the [Entry.Keys] method. - // If any key maps to an existing object, the object is invalidated, - // regardless if the object has other keys defined in the new entry. - // This to prevent ghost objects when an entry reduces the amount of keys - // for a given index. - Set(ctx context.Context, value V) - - // Invalidate an object through specified index. - // Implementations may choose to instantly delete the object, - // defer until prune or a separate cleanup routine. - // Invalidated object are no longer returned from Get. - // It is safe to call Invalidate multiple times or on non-existing entries. - Invalidate(ctx context.Context, index I, key ...K) error - - // Delete one or more keys from a specific index. - // An [IndexUnknownError] may be returned if the index is unknown. - // The referred object is not invalidated and may still be accessible though - // other indices and keys. - // It is safe to call Delete multiple times or on non-existing entries - Delete(ctx context.Context, index I, key ...K) error - - // Truncate deletes all cached objects. - Truncate(ctx context.Context) error -} - -// Entry contains a value of type `V` to be cached. -// -// `I` is the type by which indices are identified, -// typically an enum for type-safe access. -// -// `K` is the type used as key in an index. -// Due to the limitations in type constraints, all indices use the same key type. -type Entry[I, K comparable] interface { - // Keys returns which keys map to the object in a specified index. - // May return nil if the index in unknown or when there are no keys. - Keys(index I) (key []K) -} - -type Connector int - -//go:generate enumer -type Connector -transform snake -trimprefix Connector -linecomment -text -const ( - // Empty line comment ensures empty string for unspecified value - ConnectorUnspecified Connector = iota // - ConnectorMemory - ConnectorPostgres - ConnectorRedis -) - -type Config struct { - Connector Connector - - // Age since an object was added to the cache, - // after which the object is considered invalid. - // 0 disables max age checks. - MaxAge time.Duration - - // Age since last use (Get) of an object, - // after which the object is considered invalid. - // 0 disables last use age checks. - LastUseAge time.Duration - - // Log allows logging of the specific cache. - // By default only errors are logged to stdout. - Log *logging.Config -} diff --git a/backend/storage/cache/connector/connector.go b/backend/storage/cache/connector/connector.go deleted file mode 100644 index 3cc5e852a6..0000000000 --- a/backend/storage/cache/connector/connector.go +++ /dev/null @@ -1,49 +0,0 @@ -// Package connector provides glue between the [cache.Cache] interface and implementations from the connector sub-packages. -package connector - -import ( - "context" - "fmt" - - "github.com/zitadel/zitadel/backend/storage/cache" - "github.com/zitadel/zitadel/backend/storage/cache/connector/gomap" - "github.com/zitadel/zitadel/backend/storage/cache/connector/noop" -) - -type CachesConfig struct { - Connectors struct { - Memory gomap.Config - } - Instance *cache.Config - Milestones *cache.Config - Organization *cache.Config - IdPFormCallbacks *cache.Config -} - -type Connectors struct { - Config CachesConfig - Memory *gomap.Connector -} - -func StartConnectors(conf *CachesConfig) (Connectors, error) { - if conf == nil { - return Connectors{}, nil - } - return Connectors{ - Config: *conf, - Memory: gomap.NewConnector(conf.Connectors.Memory), - }, nil -} - -func StartCache[I ~int, K ~string, V cache.Entry[I, K]](background context.Context, indices []I, purpose cache.Purpose, conf *cache.Config, connectors Connectors) (cache.Cache[I, K, V], error) { - if conf == nil || conf.Connector == cache.ConnectorUnspecified { - return noop.NewCache[I, K, V](), nil - } - if conf.Connector == cache.ConnectorMemory && connectors.Memory != nil { - c := gomap.NewCache[I, K, V](background, indices, *conf) - connectors.Memory.Config.StartAutoPrune(background, c, purpose) - return c, nil - } - - return nil, fmt.Errorf("cache connector %q not enabled", conf.Connector) -} diff --git a/backend/storage/cache/connector/gomap/connector.go b/backend/storage/cache/connector/gomap/connector.go deleted file mode 100644 index a37055bd73..0000000000 --- a/backend/storage/cache/connector/gomap/connector.go +++ /dev/null @@ -1,23 +0,0 @@ -package gomap - -import ( - "github.com/zitadel/zitadel/backend/storage/cache" -) - -type Config struct { - Enabled bool - AutoPrune cache.AutoPruneConfig -} - -type Connector struct { - Config cache.AutoPruneConfig -} - -func NewConnector(config Config) *Connector { - if !config.Enabled { - return nil - } - return &Connector{ - Config: config.AutoPrune, - } -} diff --git a/backend/storage/cache/connector/gomap/gomap.go b/backend/storage/cache/connector/gomap/gomap.go deleted file mode 100644 index d79e323801..0000000000 --- a/backend/storage/cache/connector/gomap/gomap.go +++ /dev/null @@ -1,200 +0,0 @@ -package gomap - -import ( - "context" - "errors" - "log/slog" - "maps" - "os" - "sync" - "sync/atomic" - "time" - - "github.com/zitadel/zitadel/backend/storage/cache" -) - -type mapCache[I, K comparable, V cache.Entry[I, K]] struct { - config *cache.Config - indexMap map[I]*index[K, V] - logger *slog.Logger -} - -// NewCache returns an in-memory Cache implementation based on the builtin go map type. -// Object values are stored as-is and there is no encoding or decoding involved. -func NewCache[I, K comparable, V cache.Entry[I, K]](background context.Context, indices []I, config cache.Config) cache.PrunerCache[I, K, V] { - m := &mapCache[I, K, V]{ - config: &config, - indexMap: make(map[I]*index[K, V], len(indices)), - logger: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ - AddSource: true, - Level: slog.LevelError, - })), - } - if config.Log != nil { - m.logger = config.Log.Slog() - } - m.logger.InfoContext(background, "map cache logging enabled") - - for _, name := range indices { - m.indexMap[name] = &index[K, V]{ - config: m.config, - entries: make(map[K]*entry[V]), - } - } - return m -} - -func (c *mapCache[I, K, V]) Get(ctx context.Context, index I, key K) (value V, ok bool) { - i, ok := c.indexMap[index] - if !ok { - c.logger.ErrorContext(ctx, "map cache get", "err", cache.NewIndexUnknownErr(index), "index", index, "key", key) - return value, false - } - entry, err := i.Get(key) - if err == nil { - c.logger.DebugContext(ctx, "map cache get", "index", index, "key", key) - return entry.value, true - } - if errors.Is(err, cache.ErrCacheMiss) { - c.logger.InfoContext(ctx, "map cache get", "err", err, "index", index, "key", key) - return value, false - } - c.logger.ErrorContext(ctx, "map cache get", "err", cache.NewIndexUnknownErr(index), "index", index, "key", key) - return value, false -} - -func (c *mapCache[I, K, V]) Set(ctx context.Context, value V) { - now := time.Now() - entry := &entry[V]{ - value: value, - created: now, - } - entry.lastUse.Store(now.UnixMicro()) - - for name, i := range c.indexMap { - keys := value.Keys(name) - i.Set(keys, entry) - c.logger.DebugContext(ctx, "map cache set", "index", name, "keys", keys) - } -} - -func (c *mapCache[I, K, V]) Invalidate(ctx context.Context, index I, keys ...K) error { - i, ok := c.indexMap[index] - if !ok { - return cache.NewIndexUnknownErr(index) - } - i.Invalidate(keys) - c.logger.DebugContext(ctx, "map cache invalidate", "index", index, "keys", keys) - return nil -} - -func (c *mapCache[I, K, V]) Delete(ctx context.Context, index I, keys ...K) error { - i, ok := c.indexMap[index] - if !ok { - return cache.NewIndexUnknownErr(index) - } - i.Delete(keys) - c.logger.DebugContext(ctx, "map cache delete", "index", index, "keys", keys) - return nil -} - -func (c *mapCache[I, K, V]) Prune(ctx context.Context) error { - for name, index := range c.indexMap { - index.Prune() - c.logger.DebugContext(ctx, "map cache prune", "index", name) - } - return nil -} - -func (c *mapCache[I, K, V]) Truncate(ctx context.Context) error { - for name, index := range c.indexMap { - index.Truncate() - c.logger.DebugContext(ctx, "map cache truncate", "index", name) - } - return nil -} - -type index[K comparable, V any] struct { - mutex sync.RWMutex - config *cache.Config - entries map[K]*entry[V] -} - -func (i *index[K, V]) Get(key K) (*entry[V], error) { - i.mutex.RLock() - entry, ok := i.entries[key] - i.mutex.RUnlock() - if ok && entry.isValid(i.config) { - return entry, nil - } - return nil, cache.ErrCacheMiss -} - -func (c *index[K, V]) Set(keys []K, entry *entry[V]) { - c.mutex.Lock() - for _, key := range keys { - c.entries[key] = entry - } - c.mutex.Unlock() -} - -func (i *index[K, V]) Invalidate(keys []K) { - i.mutex.RLock() - for _, key := range keys { - if entry, ok := i.entries[key]; ok { - entry.invalid.Store(true) - } - } - i.mutex.RUnlock() -} - -func (c *index[K, V]) Delete(keys []K) { - c.mutex.Lock() - for _, key := range keys { - delete(c.entries, key) - } - c.mutex.Unlock() -} - -func (c *index[K, V]) Prune() { - c.mutex.Lock() - maps.DeleteFunc(c.entries, func(_ K, entry *entry[V]) bool { - return !entry.isValid(c.config) - }) - c.mutex.Unlock() -} - -func (c *index[K, V]) Truncate() { - c.mutex.Lock() - c.entries = make(map[K]*entry[V]) - c.mutex.Unlock() -} - -type entry[V any] struct { - value V - created time.Time - invalid atomic.Bool - lastUse atomic.Int64 // UnixMicro time -} - -func (e *entry[V]) isValid(c *cache.Config) bool { - if e.invalid.Load() { - return false - } - now := time.Now() - if c.MaxAge > 0 { - if e.created.Add(c.MaxAge).Before(now) { - e.invalid.Store(true) - return false - } - } - if c.LastUseAge > 0 { - lastUse := e.lastUse.Load() - if time.UnixMicro(lastUse).Add(c.LastUseAge).Before(now) { - e.invalid.Store(true) - return false - } - e.lastUse.CompareAndSwap(lastUse, now.UnixMicro()) - } - return true -} diff --git a/backend/storage/cache/connector/gomap/gomap_test.go b/backend/storage/cache/connector/gomap/gomap_test.go deleted file mode 100644 index 8ed4f0f30a..0000000000 --- a/backend/storage/cache/connector/gomap/gomap_test.go +++ /dev/null @@ -1,329 +0,0 @@ -package gomap - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/zitadel/logging" - - "github.com/zitadel/zitadel/backend/storage/cache" -) - -type testIndex int - -const ( - testIndexID testIndex = iota - testIndexName -) - -var testIndices = []testIndex{ - testIndexID, - testIndexName, -} - -type testObject struct { - id string - names []string -} - -func (o *testObject) Keys(index testIndex) []string { - switch index { - case testIndexID: - return []string{o.id} - case testIndexName: - return o.names - default: - return nil - } -} - -func Test_mapCache_Get(t *testing.T) { - c := NewCache[testIndex, string, *testObject](context.Background(), testIndices, cache.Config{ - MaxAge: time.Second, - LastUseAge: time.Second / 4, - Log: &logging.Config{ - Level: "debug", - AddSource: true, - }, - }) - obj := &testObject{ - id: "id", - names: []string{"foo", "bar"}, - } - c.Set(context.Background(), obj) - - type args struct { - index testIndex - key string - } - tests := []struct { - name string - args args - want *testObject - wantOk bool - }{ - { - name: "ok", - args: args{ - index: testIndexID, - key: "id", - }, - want: obj, - wantOk: true, - }, - { - name: "miss", - args: args{ - index: testIndexID, - key: "spanac", - }, - want: nil, - wantOk: false, - }, - { - name: "unknown index", - args: args{ - index: 99, - key: "id", - }, - want: nil, - wantOk: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, ok := c.Get(context.Background(), tt.args.index, tt.args.key) - assert.Equal(t, tt.want, got) - assert.Equal(t, tt.wantOk, ok) - }) - } -} - -func Test_mapCache_Invalidate(t *testing.T) { - c := NewCache[testIndex, string, *testObject](context.Background(), testIndices, cache.Config{ - MaxAge: time.Second, - LastUseAge: time.Second / 4, - Log: &logging.Config{ - Level: "debug", - AddSource: true, - }, - }) - obj := &testObject{ - id: "id", - names: []string{"foo", "bar"}, - } - c.Set(context.Background(), obj) - err := c.Invalidate(context.Background(), testIndexName, "bar") - require.NoError(t, err) - got, ok := c.Get(context.Background(), testIndexID, "id") - assert.Nil(t, got) - assert.False(t, ok) -} - -func Test_mapCache_Delete(t *testing.T) { - c := NewCache[testIndex, string, *testObject](context.Background(), testIndices, cache.Config{ - MaxAge: time.Second, - LastUseAge: time.Second / 4, - Log: &logging.Config{ - Level: "debug", - AddSource: true, - }, - }) - obj := &testObject{ - id: "id", - names: []string{"foo", "bar"}, - } - c.Set(context.Background(), obj) - err := c.Delete(context.Background(), testIndexName, "bar") - require.NoError(t, err) - - // Shouldn't find object by deleted name - got, ok := c.Get(context.Background(), testIndexName, "bar") - assert.Nil(t, got) - assert.False(t, ok) - - // Should find object by other name - got, ok = c.Get(context.Background(), testIndexName, "foo") - assert.Equal(t, obj, got) - assert.True(t, ok) - - // Should find object by id - got, ok = c.Get(context.Background(), testIndexID, "id") - assert.Equal(t, obj, got) - assert.True(t, ok) -} - -func Test_mapCache_Prune(t *testing.T) { - c := NewCache[testIndex, string, *testObject](context.Background(), testIndices, cache.Config{ - MaxAge: time.Second, - LastUseAge: time.Second / 4, - Log: &logging.Config{ - Level: "debug", - AddSource: true, - }, - }) - - objects := []*testObject{ - { - id: "id1", - names: []string{"foo", "bar"}, - }, - { - id: "id2", - names: []string{"hello"}, - }, - } - for _, obj := range objects { - c.Set(context.Background(), obj) - } - // invalidate one entry - err := c.Invalidate(context.Background(), testIndexName, "bar") - require.NoError(t, err) - - err = c.(cache.Pruner).Prune(context.Background()) - require.NoError(t, err) - - // Other object should still be found - got, ok := c.Get(context.Background(), testIndexID, "id2") - assert.Equal(t, objects[1], got) - assert.True(t, ok) -} - -func Test_mapCache_Truncate(t *testing.T) { - c := NewCache[testIndex, string, *testObject](context.Background(), testIndices, cache.Config{ - MaxAge: time.Second, - LastUseAge: time.Second / 4, - Log: &logging.Config{ - Level: "debug", - AddSource: true, - }, - }) - objects := []*testObject{ - { - id: "id1", - names: []string{"foo", "bar"}, - }, - { - id: "id2", - names: []string{"hello"}, - }, - } - for _, obj := range objects { - c.Set(context.Background(), obj) - } - - err := c.Truncate(context.Background()) - require.NoError(t, err) - - mc := c.(*mapCache[testIndex, string, *testObject]) - for _, index := range mc.indexMap { - index.mutex.RLock() - assert.Len(t, index.entries, 0) - index.mutex.RUnlock() - } -} - -func Test_entry_isValid(t *testing.T) { - type fields struct { - created time.Time - invalid bool - lastUse time.Time - } - tests := []struct { - name string - fields fields - config *cache.Config - want bool - }{ - { - name: "invalid", - fields: fields{ - created: time.Now(), - invalid: true, - lastUse: time.Now(), - }, - config: &cache.Config{ - MaxAge: time.Minute, - LastUseAge: time.Second, - }, - want: false, - }, - { - name: "max age exceeded", - fields: fields{ - created: time.Now().Add(-(time.Minute + time.Second)), - invalid: false, - lastUse: time.Now(), - }, - config: &cache.Config{ - MaxAge: time.Minute, - LastUseAge: time.Second, - }, - want: false, - }, - { - name: "max age disabled", - fields: fields{ - created: time.Now().Add(-(time.Minute + time.Second)), - invalid: false, - lastUse: time.Now(), - }, - config: &cache.Config{ - LastUseAge: time.Second, - }, - want: true, - }, - { - name: "last use age exceeded", - fields: fields{ - created: time.Now().Add(-(time.Minute / 2)), - invalid: false, - lastUse: time.Now().Add(-(time.Second * 2)), - }, - config: &cache.Config{ - MaxAge: time.Minute, - LastUseAge: time.Second, - }, - want: false, - }, - { - name: "last use age disabled", - fields: fields{ - created: time.Now().Add(-(time.Minute / 2)), - invalid: false, - lastUse: time.Now().Add(-(time.Second * 2)), - }, - config: &cache.Config{ - MaxAge: time.Minute, - }, - want: true, - }, - { - name: "valid", - fields: fields{ - created: time.Now(), - invalid: false, - lastUse: time.Now(), - }, - config: &cache.Config{ - MaxAge: time.Minute, - LastUseAge: time.Second, - }, - want: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - e := &entry[any]{ - created: tt.fields.created, - } - e.invalid.Store(tt.fields.invalid) - e.lastUse.Store(tt.fields.lastUse.UnixMicro()) - got := e.isValid(tt.config) - assert.Equal(t, tt.want, got) - }) - } -} diff --git a/backend/storage/cache/connector/noop/noop.go b/backend/storage/cache/connector/noop/noop.go deleted file mode 100644 index e3cf69c8ec..0000000000 --- a/backend/storage/cache/connector/noop/noop.go +++ /dev/null @@ -1,21 +0,0 @@ -package noop - -import ( - "context" - - "github.com/zitadel/zitadel/backend/storage/cache" -) - -type noop[I, K comparable, V cache.Entry[I, K]] struct{} - -// NewCache returns a cache that does nothing -func NewCache[I, K comparable, V cache.Entry[I, K]]() cache.Cache[I, K, V] { - return noop[I, K, V]{} -} - -func (noop[I, K, V]) Set(context.Context, V) {} -func (noop[I, K, V]) Get(context.Context, I, K) (value V, ok bool) { return } -func (noop[I, K, V]) Invalidate(context.Context, I, ...K) (err error) { return } -func (noop[I, K, V]) Delete(context.Context, I, ...K) (err error) { return } -func (noop[I, K, V]) Prune(context.Context) (err error) { return } -func (noop[I, K, V]) Truncate(context.Context) (err error) { return } diff --git a/backend/storage/cache/connector_enumer.go b/backend/storage/cache/connector_enumer.go deleted file mode 100644 index 7ea014db16..0000000000 --- a/backend/storage/cache/connector_enumer.go +++ /dev/null @@ -1,98 +0,0 @@ -// Code generated by "enumer -type Connector -transform snake -trimprefix Connector -linecomment -text"; DO NOT EDIT. - -package cache - -import ( - "fmt" - "strings" -) - -const _ConnectorName = "memorypostgresredis" - -var _ConnectorIndex = [...]uint8{0, 0, 6, 14, 19} - -const _ConnectorLowerName = "memorypostgresredis" - -func (i Connector) String() string { - if i < 0 || i >= Connector(len(_ConnectorIndex)-1) { - return fmt.Sprintf("Connector(%d)", i) - } - return _ConnectorName[_ConnectorIndex[i]:_ConnectorIndex[i+1]] -} - -// An "invalid array index" compiler error signifies that the constant values have changed. -// Re-run the stringer command to generate them again. -func _ConnectorNoOp() { - var x [1]struct{} - _ = x[ConnectorUnspecified-(0)] - _ = x[ConnectorMemory-(1)] - _ = x[ConnectorPostgres-(2)] - _ = x[ConnectorRedis-(3)] -} - -var _ConnectorValues = []Connector{ConnectorUnspecified, ConnectorMemory, ConnectorPostgres, ConnectorRedis} - -var _ConnectorNameToValueMap = map[string]Connector{ - _ConnectorName[0:0]: ConnectorUnspecified, - _ConnectorLowerName[0:0]: ConnectorUnspecified, - _ConnectorName[0:6]: ConnectorMemory, - _ConnectorLowerName[0:6]: ConnectorMemory, - _ConnectorName[6:14]: ConnectorPostgres, - _ConnectorLowerName[6:14]: ConnectorPostgres, - _ConnectorName[14:19]: ConnectorRedis, - _ConnectorLowerName[14:19]: ConnectorRedis, -} - -var _ConnectorNames = []string{ - _ConnectorName[0:0], - _ConnectorName[0:6], - _ConnectorName[6:14], - _ConnectorName[14:19], -} - -// ConnectorString retrieves an enum value from the enum constants string name. -// Throws an error if the param is not part of the enum. -func ConnectorString(s string) (Connector, error) { - if val, ok := _ConnectorNameToValueMap[s]; ok { - return val, nil - } - - if val, ok := _ConnectorNameToValueMap[strings.ToLower(s)]; ok { - return val, nil - } - return 0, fmt.Errorf("%s does not belong to Connector values", s) -} - -// ConnectorValues returns all values of the enum -func ConnectorValues() []Connector { - return _ConnectorValues -} - -// ConnectorStrings returns a slice of all String values of the enum -func ConnectorStrings() []string { - strs := make([]string, len(_ConnectorNames)) - copy(strs, _ConnectorNames) - return strs -} - -// IsAConnector returns "true" if the value is listed in the enum definition. "false" otherwise -func (i Connector) IsAConnector() bool { - for _, v := range _ConnectorValues { - if i == v { - return true - } - } - return false -} - -// MarshalText implements the encoding.TextMarshaler interface for Connector -func (i Connector) MarshalText() ([]byte, error) { - return []byte(i.String()), nil -} - -// UnmarshalText implements the encoding.TextUnmarshaler interface for Connector -func (i *Connector) UnmarshalText(text []byte) error { - var err error - *i, err = ConnectorString(string(text)) - return err -} diff --git a/backend/storage/cache/error.go b/backend/storage/cache/error.go deleted file mode 100644 index b66b9447bf..0000000000 --- a/backend/storage/cache/error.go +++ /dev/null @@ -1,29 +0,0 @@ -package cache - -import ( - "errors" - "fmt" -) - -type IndexUnknownError[I comparable] struct { - index I -} - -func NewIndexUnknownErr[I comparable](index I) error { - return IndexUnknownError[I]{index} -} - -func (i IndexUnknownError[I]) Error() string { - return fmt.Sprintf("index %v unknown", i.index) -} - -func (a IndexUnknownError[I]) Is(err error) bool { - if b, ok := err.(IndexUnknownError[I]); ok { - return a.index == b.index - } - return false -} - -var ( - ErrCacheMiss = errors.New("cache miss") -) diff --git a/backend/storage/cache/pruner.go b/backend/storage/cache/pruner.go deleted file mode 100644 index 959762d410..0000000000 --- a/backend/storage/cache/pruner.go +++ /dev/null @@ -1,76 +0,0 @@ -package cache - -import ( - "context" - "math/rand" - "time" - - "github.com/jonboulle/clockwork" - "github.com/zitadel/logging" -) - -// Pruner is an optional [Cache] interface. -type Pruner interface { - // Prune deletes all invalidated or expired objects. - Prune(ctx context.Context) error -} - -type PrunerCache[I, K comparable, V Entry[I, K]] interface { - Cache[I, K, V] - Pruner -} - -type AutoPruneConfig struct { - // Interval at which the cache is automatically pruned. - // 0 or lower disables automatic pruning. - Interval time.Duration - - // Timeout for an automatic prune. - // It is recommended to keep the value shorter than AutoPruneInterval - // 0 or lower disables automatic pruning. - Timeout time.Duration -} - -func (c AutoPruneConfig) StartAutoPrune(background context.Context, pruner Pruner, purpose Purpose) (close func()) { - return c.startAutoPrune(background, pruner, purpose, clockwork.NewRealClock()) -} - -func (c *AutoPruneConfig) startAutoPrune(background context.Context, pruner Pruner, purpose Purpose, clock clockwork.Clock) (close func()) { - if c.Interval <= 0 { - return func() {} - } - background, cancel := context.WithCancel(background) - // randomize the first interval - timer := clock.NewTimer(time.Duration(rand.Int63n(int64(c.Interval)))) - go c.pruneTimer(background, pruner, purpose, timer) - return cancel -} - -func (c *AutoPruneConfig) pruneTimer(background context.Context, pruner Pruner, purpose Purpose, timer clockwork.Timer) { - defer func() { - if !timer.Stop() { - <-timer.Chan() - } - }() - - for { - select { - case <-background.Done(): - return - case <-timer.Chan(): - err := c.doPrune(background, pruner) - logging.OnError(err).WithField("purpose", purpose).Error("cache auto prune") - timer.Reset(c.Interval) - } - } -} - -func (c *AutoPruneConfig) doPrune(background context.Context, pruner Pruner) error { - ctx, cancel := context.WithCancel(background) - defer cancel() - if c.Timeout > 0 { - ctx, cancel = context.WithTimeout(background, c.Timeout) - defer cancel() - } - return pruner.Prune(ctx) -} diff --git a/backend/storage/cache/pruner_test.go b/backend/storage/cache/pruner_test.go deleted file mode 100644 index faaedeb88c..0000000000 --- a/backend/storage/cache/pruner_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package cache - -import ( - "context" - "testing" - "time" - - "github.com/jonboulle/clockwork" - "github.com/stretchr/testify/assert" -) - -type testPruner struct { - called chan struct{} -} - -func (p *testPruner) Prune(context.Context) error { - p.called <- struct{}{} - return nil -} - -func TestAutoPruneConfig_startAutoPrune(t *testing.T) { - c := AutoPruneConfig{ - Interval: time.Second, - Timeout: time.Millisecond, - } - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - - pruner := testPruner{ - called: make(chan struct{}), - } - clock := clockwork.NewFakeClock() - close := c.startAutoPrune(ctx, &pruner, PurposeAuthzInstance, clock) - defer close() - clock.Advance(time.Second) - - select { - case _, ok := <-pruner.called: - assert.True(t, ok) - case <-ctx.Done(): - t.Fatal(ctx.Err()) - } -} diff --git a/backend/storage/cache/purpose_enumer.go b/backend/storage/cache/purpose_enumer.go deleted file mode 100644 index a93a978efb..0000000000 --- a/backend/storage/cache/purpose_enumer.go +++ /dev/null @@ -1,90 +0,0 @@ -// Code generated by "enumer -type Purpose -transform snake -trimprefix Purpose"; DO NOT EDIT. - -package cache - -import ( - "fmt" - "strings" -) - -const _PurposeName = "unspecifiedauthz_instancemilestonesorganizationid_p_form_callback" - -var _PurposeIndex = [...]uint8{0, 11, 25, 35, 47, 65} - -const _PurposeLowerName = "unspecifiedauthz_instancemilestonesorganizationid_p_form_callback" - -func (i Purpose) String() string { - if i < 0 || i >= Purpose(len(_PurposeIndex)-1) { - return fmt.Sprintf("Purpose(%d)", i) - } - return _PurposeName[_PurposeIndex[i]:_PurposeIndex[i+1]] -} - -// An "invalid array index" compiler error signifies that the constant values have changed. -// Re-run the stringer command to generate them again. -func _PurposeNoOp() { - var x [1]struct{} - _ = x[PurposeUnspecified-(0)] - _ = x[PurposeAuthzInstance-(1)] - _ = x[PurposeMilestones-(2)] - _ = x[PurposeOrganization-(3)] - _ = x[PurposeIdPFormCallback-(4)] -} - -var _PurposeValues = []Purpose{PurposeUnspecified, PurposeAuthzInstance, PurposeMilestones, PurposeOrganization, PurposeIdPFormCallback} - -var _PurposeNameToValueMap = map[string]Purpose{ - _PurposeName[0:11]: PurposeUnspecified, - _PurposeLowerName[0:11]: PurposeUnspecified, - _PurposeName[11:25]: PurposeAuthzInstance, - _PurposeLowerName[11:25]: PurposeAuthzInstance, - _PurposeName[25:35]: PurposeMilestones, - _PurposeLowerName[25:35]: PurposeMilestones, - _PurposeName[35:47]: PurposeOrganization, - _PurposeLowerName[35:47]: PurposeOrganization, - _PurposeName[47:65]: PurposeIdPFormCallback, - _PurposeLowerName[47:65]: PurposeIdPFormCallback, -} - -var _PurposeNames = []string{ - _PurposeName[0:11], - _PurposeName[11:25], - _PurposeName[25:35], - _PurposeName[35:47], - _PurposeName[47:65], -} - -// PurposeString retrieves an enum value from the enum constants string name. -// Throws an error if the param is not part of the enum. -func PurposeString(s string) (Purpose, error) { - if val, ok := _PurposeNameToValueMap[s]; ok { - return val, nil - } - - if val, ok := _PurposeNameToValueMap[strings.ToLower(s)]; ok { - return val, nil - } - return 0, fmt.Errorf("%s does not belong to Purpose values", s) -} - -// PurposeValues returns all values of the enum -func PurposeValues() []Purpose { - return _PurposeValues -} - -// PurposeStrings returns a slice of all String values of the enum -func PurposeStrings() []string { - strs := make([]string, len(_PurposeNames)) - copy(strs, _PurposeNames) - return strs -} - -// IsAPurpose returns "true" if the value is listed in the enum definition. "false" otherwise -func (i Purpose) IsAPurpose() bool { - for _, v := range _PurposeValues { - if i == v { - return true - } - } - return false -} diff --git a/backend/storage/database/config.go b/backend/storage/database/config.go deleted file mode 100644 index 1604cda650..0000000000 --- a/backend/storage/database/config.go +++ /dev/null @@ -1,10 +0,0 @@ -package database - -import ( - "context" -) - -type Connector interface { - Connect(ctx context.Context) (Pool, error) - // bla4.Configurer -} diff --git a/backend/storage/database/database.go b/backend/storage/database/database.go deleted file mode 100644 index 45d9ece952..0000000000 --- a/backend/storage/database/database.go +++ /dev/null @@ -1,93 +0,0 @@ -package database - -import ( - "context" - "io/fs" -) - -type Row interface { - Scan(dest ...any) error -} - -type Rows interface { - Row - Next() bool - Close() error - Err() error -} - -type Transaction interface { - Commit(ctx context.Context) error - Rollback(ctx context.Context) error - End(ctx context.Context, err error) error - - Begin(ctx context.Context) (Transaction, error) - - QueryExecutor -} - -type Client interface { - Beginner - QueryExecutor - - Release(ctx context.Context) error -} - -type Pool interface { - Beginner - QueryExecutor - - Acquire(ctx context.Context) (Client, error) - Close(ctx context.Context) error -} - -type TransactionOptions struct { - IsolationLevel IsolationLevel - AccessMode AccessMode -} - -type IsolationLevel uint8 - -const ( - IsolationLevelSerializable IsolationLevel = iota - IsolationLevelReadCommitted -) - -type AccessMode uint8 - -const ( - AccessModeReadWrite AccessMode = iota - AccessModeReadOnly -) - -type Beginner interface { - Begin(ctx context.Context, opts *TransactionOptions) (Transaction, error) -} - -type QueryExecutor interface { - Querier - Executor -} - -type Querier interface { - Query(ctx context.Context, stmt string, args ...any) (Rows, error) - QueryRow(ctx context.Context, stmt string, args ...any) Row -} - -func Query[Out any](q Querier, fn func(q Querier) ([]Out, error)) ([]Out, error) { - return fn(q) -} - -func QueryRow[Out any](q Querier, fn func(q Querier) (Out, error)) (Out, error) { - return fn(q) -} - -type Executor interface { - Exec(ctx context.Context, stmt string, args ...any) error -} - -// LoadStatements sets the sql statements strings -// TODO: implement -func LoadStatements(fs.FS) error { - return nil -} diff --git a/backend/storage/database/dialect/config.go b/backend/storage/database/dialect/config.go deleted file mode 100644 index 593b06ec84..0000000000 --- a/backend/storage/database/dialect/config.go +++ /dev/null @@ -1,135 +0,0 @@ -package dialect - -import ( - "context" - "errors" - "reflect" - - "github.com/mitchellh/mapstructure" - "github.com/spf13/viper" - - "github.com/zitadel/zitadel/backend/storage/database" - "github.com/zitadel/zitadel/backend/storage/database/dialect/postgres" -) - -type Hook struct { - Match func(string) bool - Decode func(config any) (database.Connector, error) - Name string - Constructor func() database.Connector -} - -var hooks = []Hook{ - { - Match: postgres.NameMatcher, - Decode: postgres.DecodeConfig, - Name: postgres.Name, - Constructor: func() database.Connector { return new(postgres.Config) }, - }, - // { - // Match: gosql.NameMatcher, - // Decode: gosql.DecodeConfig, - // Name: gosql.Name, - // Constructor: func() database.Connector { return new(gosql.Config) }, - // }, -} - -type Config struct { - Dialects map[string]any `mapstructure:",remain" yaml:",inline"` - - connector database.Connector -} - -// Configure implements [configure.Configurer]. -// func (c *Config) Configure() (any, error) { -// possibilities := make([]string, len(hooks)) -// var cursor int -// for i, hook := range hooks { -// if _, ok := c.Dialects[hook.Name]; ok { -// cursor = i -// } -// possibilities[i] = hook.Name -// } - -// prompt := promptui.Select{ -// Label: "Select a dialect", -// Items: possibilities, -// CursorPos: cursor, -// } -// i, _, err := prompt.Run() -// if err != nil { -// return nil, err -// } - -// var config bla4.Configurer - -// if dialect, ok := c.Dialects[hooks[i].Name]; ok { -// config, err = hooks[i].Decode(dialect) -// if err != nil { -// return nil, err -// } -// } else { -// clear(c.Dialects) -// config = hooks[i].Constructor() -// } -// if c.Dialects == nil { -// c.Dialects = make(map[string]any) -// } -// c.Dialects[hooks[i].Name], err = config.Configure() -// if err != nil { -// return nil, err -// } - -// return c, nil -// } - -func (c Config) Connect(ctx context.Context) (database.Pool, error) { - if len(c.Dialects) != 1 { - return nil, errors.New("Exactly one dialect must be configured") - } - - return c.connector.Connect(ctx) -} - -// Hooks implements [configure.Unmarshaller]. -func (c Config) Hooks() []viper.DecoderConfigOption { - return []viper.DecoderConfigOption{ - viper.DecodeHook(decodeHook), - } -} - -func (c *Config) decodeDialect() error { - for _, hook := range hooks { - for name, config := range c.Dialects { - if !hook.Match(name) { - continue - } - - connector, err := hook.Decode(config) - if err != nil { - return err - } - - c.connector = connector - return nil - } - } - return errors.New("no dialect found") -} - -func decodeHook(from, to reflect.Value) (_ any, err error) { - if to.Type() != reflect.TypeOf(Config{}) { - return from.Interface(), nil - } - - config := new(Config) - if err = mapstructure.Decode(from.Interface(), config); err != nil { - return nil, err - } - - if err = config.decodeDialect(); err != nil { - return nil, err - } - - return config, nil -} diff --git a/backend/storage/database/dialect/gosql/config.go b/backend/storage/database/dialect/gosql/config.go deleted file mode 100644 index 77648ee236..0000000000 --- a/backend/storage/database/dialect/gosql/config.go +++ /dev/null @@ -1,52 +0,0 @@ -package gosql - -import ( - "context" - "database/sql" - "errors" - "strings" - - "github.com/zitadel/zitadel/backend/storage/database" -) - -var ( - _ database.Connector = (*Config)(nil) - - Name = "gosql" -) - -type Config struct { - db *sql.DB -} - -// Connect implements [database.Connector]. -func (c *Config) Connect(ctx context.Context) (database.Pool, error) { - if err := c.db.PingContext(ctx); err != nil { - return nil, err - } - return &sqlPool{c.db}, nil -} - -func NameMatcher(name string) bool { - name = strings.ToLower(name) - for _, driver := range sql.Drivers() { - if driver == name { - return true - } - } - return false -} - -func DecodeConfig(name string, config any) (database.Connector, error) { - switch c := config.(type) { - case string: - db, err := sql.Open(name, c) - if err != nil { - return nil, err - } - return &Config{db}, nil - case map[string]any: - return nil, errors.New("map configuration not implemented") - } - return nil, errors.New("invalid configuration") -} diff --git a/backend/storage/database/dialect/gosql/conn.go b/backend/storage/database/dialect/gosql/conn.go deleted file mode 100644 index d0cd025acd..0000000000 --- a/backend/storage/database/dialect/gosql/conn.go +++ /dev/null @@ -1,45 +0,0 @@ -package gosql - -import ( - "context" - "database/sql" - - "github.com/zitadel/zitadel/backend/storage/database" -) - -type sqlConn struct{ *sql.Conn } - -var _ database.Client = (*sqlConn)(nil) - -// Release implements [database.Client]. -func (c *sqlConn) Release(_ context.Context) error { - return c.Conn.Close() -} - -// Begin implements [database.Client]. -func (c *sqlConn) Begin(ctx context.Context, opts *database.TransactionOptions) (database.Transaction, error) { - tx, err := c.Conn.BeginTx(ctx, transactionOptionsToSql(opts)) - if err != nil { - return nil, err - } - return &sqlTx{tx}, nil -} - -// Query implements sql.Client. -// Subtle: this method shadows the method (*Conn).Query of pgxConn.Conn. -func (c *sqlConn) Query(ctx context.Context, sql string, args ...any) (database.Rows, error) { - return c.Conn.QueryContext(ctx, sql, args...) -} - -// QueryRow implements sql.Client. -// Subtle: this method shadows the method (*Conn).QueryRow of pgxConn.Conn. -func (c *sqlConn) QueryRow(ctx context.Context, sql string, args ...any) database.Row { - return c.Conn.QueryRowContext(ctx, sql, args...) -} - -// Exec implements [database.Pool]. -// Subtle: this method shadows the method (Pool).Exec of pgxPool.Pool. -func (c *sqlConn) Exec(ctx context.Context, sql string, args ...any) error { - _, err := c.Conn.ExecContext(ctx, sql, args...) - return err -} diff --git a/backend/storage/database/dialect/gosql/pool.go b/backend/storage/database/dialect/gosql/pool.go deleted file mode 100644 index 5fd4ad4b9e..0000000000 --- a/backend/storage/database/dialect/gosql/pool.go +++ /dev/null @@ -1,54 +0,0 @@ -package gosql - -import ( - "context" - "database/sql" - - "github.com/zitadel/zitadel/backend/storage/database" -) - -type sqlPool struct{ *sql.DB } - -var _ database.Pool = (*sqlPool)(nil) - -// Acquire implements [database.Pool]. -func (c *sqlPool) Acquire(ctx context.Context) (database.Client, error) { - conn, err := c.DB.Conn(ctx) - if err != nil { - return nil, err - } - return &sqlConn{conn}, nil -} - -// Query implements [database.Pool]. -// Subtle: this method shadows the method (Pool).Query of pgxPool.Pool. -func (c *sqlPool) Query(ctx context.Context, sql string, args ...any) (database.Rows, error) { - return c.DB.QueryContext(ctx, sql, args...) -} - -// QueryRow implements [database.Pool]. -// Subtle: this method shadows the method (Pool).QueryRow of pgxPool.Pool. -func (c *sqlPool) QueryRow(ctx context.Context, sql string, args ...any) database.Row { - return c.DB.QueryRowContext(ctx, sql, args...) -} - -// Exec implements [database.Pool]. -// Subtle: this method shadows the method (Pool).Exec of pgxPool.Pool. -func (c *sqlPool) Exec(ctx context.Context, sql string, args ...any) error { - _, err := c.DB.ExecContext(ctx, sql, args...) - return err -} - -// Begin implements [database.Pool]. -func (c *sqlPool) Begin(ctx context.Context, opts *database.TransactionOptions) (database.Transaction, error) { - tx, err := c.DB.BeginTx(ctx, transactionOptionsToSql(opts)) - if err != nil { - return nil, err - } - return &sqlTx{tx}, nil -} - -// Close implements [database.Pool]. -func (c *sqlPool) Close(_ context.Context) error { - return c.DB.Close() -} diff --git a/backend/storage/database/dialect/gosql/tx.go b/backend/storage/database/dialect/gosql/tx.go deleted file mode 100644 index c73fc3b470..0000000000 --- a/backend/storage/database/dialect/gosql/tx.go +++ /dev/null @@ -1,79 +0,0 @@ -package gosql - -import ( - "context" - "database/sql" - "errors" - - "github.com/zitadel/zitadel/backend/storage/database" -) - -type sqlTx struct{ *sql.Tx } - -var _ database.Transaction = (*sqlTx)(nil) - -// Commit implements [database.Transaction]. -func (tx *sqlTx) Commit(_ context.Context) error { - return tx.Tx.Commit() -} - -// Rollback implements [database.Transaction]. -func (tx *sqlTx) Rollback(_ context.Context) error { - return tx.Tx.Rollback() -} - -// End implements [database.Transaction]. -func (tx *sqlTx) End(ctx context.Context, err error) error { - if err != nil { - tx.Rollback(ctx) - return err - } - return tx.Commit(ctx) -} - -// Query implements [database.Transaction]. -// Subtle: this method shadows the method (Tx).Query of pgxTx.Tx. -func (tx *sqlTx) Query(ctx context.Context, sql string, args ...any) (database.Rows, error) { - return tx.Tx.QueryContext(ctx, sql, args...) -} - -// QueryRow implements [database.Transaction]. -// Subtle: this method shadows the method (Tx).QueryRow of pgxTx.Tx. -func (tx *sqlTx) QueryRow(ctx context.Context, sql string, args ...any) database.Row { - return tx.Tx.QueryRowContext(ctx, sql, args...) -} - -// Exec implements [database.Pool]. -// Subtle: this method shadows the method (Pool).Exec of pgxPool.Pool. -func (tx *sqlTx) Exec(ctx context.Context, sql string, args ...any) error { - _, err := tx.Tx.ExecContext(ctx, sql, args...) - return err -} - -// Begin implements [database.Transaction]. -// it is unimplemented -func (tx *sqlTx) Begin(ctx context.Context) (database.Transaction, error) { - return nil, errors.New("nested transactions are not supported") -} - -func transactionOptionsToSql(opts *database.TransactionOptions) *sql.TxOptions { - if opts == nil { - return nil - } - - return &sql.TxOptions{ - Isolation: isolationToSql(opts.IsolationLevel), - ReadOnly: opts.AccessMode == database.AccessModeReadOnly, - } -} - -func isolationToSql(isolation database.IsolationLevel) sql.IsolationLevel { - switch isolation { - case database.IsolationLevelSerializable: - return sql.LevelSerializable - case database.IsolationLevelReadCommitted: - return sql.LevelReadCommitted - default: - return sql.LevelSerializable - } -} diff --git a/backend/storage/database/dialect/postgres/config.go b/backend/storage/database/dialect/postgres/config.go deleted file mode 100644 index c0239f4fde..0000000000 --- a/backend/storage/database/dialect/postgres/config.go +++ /dev/null @@ -1,134 +0,0 @@ -package postgres - -import ( - "context" - "errors" - "slices" - "strings" - - "github.com/jackc/pgx/v5/pgxpool" - "github.com/manifoldco/promptui" - "github.com/mitchellh/mapstructure" - - "github.com/zitadel/zitadel/backend/cmd/configure/bla4" - "github.com/zitadel/zitadel/backend/storage/database" -) - -var ( - _ database.Connector = (*Config)(nil) - Name = "postgres" -) - -type Config struct { - config *pgxpool.Config - - // Host string - // Port int32 - // Database string - // MaxOpenConns uint32 - // MaxIdleConns uint32 - // MaxConnLifetime time.Duration - // MaxConnIdleTime time.Duration - // User User - // // Additional options to be appended as options= - // // The value will be taken as is. Multiple options are space separated. - // Options string - - configuredFields []string -} - -// FinishAllowed implements [bla4.Iterator]. -func (c *Config) FinishAllowed() bool { - // Option can be skipped - return len(c.configuredFields) < 2 -} - -// NextField implements [bla4.Iterator]. -func (c *Config) NextField() string { - if c.configuredFields == nil { - c.configuredFields = []string{"Host", "Port", "Database", "MaxOpenConns", "MaxIdleConns", "MaxConnLifetime", "MaxConnIdleTime", "User", "Options"} - } - if len(c.configuredFields) == 0 { - return "" - } - field := c.configuredFields[0] - c.configuredFields = c.configuredFields[1:] - return field -} - -// Configure implements [bla4.Configurer]. -func (c *Config) Configure() (value any, err error) { - typeSelect := promptui.Select{ - Label: "Configure the database connection", - Items: []string{"connection string", "fields"}, - } - i, _, err := typeSelect.Run() - if err != nil { - return nil, err - } - if i > 0 { - return nil, nil - } - - if c.config == nil { - c.config, _ = pgxpool.ParseConfig("host=localhost user=zitadel password= dbname=zitadel sslmode=disable") - } - - prompt := promptui.Prompt{ - Label: "Connection string", - Default: c.config.ConnString(), - AllowEdit: c.config.ConnString() != "", - Validate: func(input string) error { - _, err := pgxpool.ParseConfig(input) - return err - }, - } - - return prompt.Run() -} - -var _ bla4.Iterator = (*Config)(nil) - -// Connect implements [database.Connector]. -func (c *Config) Connect(ctx context.Context) (database.Pool, error) { - pool, err := pgxpool.NewWithConfig(ctx, c.config) - if err != nil { - return nil, err - } - if err = pool.Ping(ctx); err != nil { - return nil, err - } - return &pgxPool{pool}, nil -} - -func NameMatcher(name string) bool { - return slices.Contains([]string{"postgres", "pg"}, strings.ToLower(name)) -} - -func DecodeConfig(input any) (database.Connector, error) { - switch c := input.(type) { - case string: - config, err := pgxpool.ParseConfig(c) - if err != nil { - return nil, err - } - return &Config{config: config}, nil - case map[string]any: - connector := new(Config) - decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ - DecodeHook: mapstructure.StringToTimeDurationHookFunc(), - WeaklyTypedInput: true, - Result: connector, - }) - if err != nil { - return nil, err - } - if err = decoder.Decode(c); err != nil { - return nil, err - } - return &Config{ - config: &pgxpool.Config{}, - }, nil - } - return nil, errors.New("invalid configuration") -} diff --git a/backend/storage/database/dialect/postgres/conn.go b/backend/storage/database/dialect/postgres/conn.go deleted file mode 100644 index 134bb3e5f5..0000000000 --- a/backend/storage/database/dialect/postgres/conn.go +++ /dev/null @@ -1,48 +0,0 @@ -package postgres - -import ( - "context" - - "github.com/jackc/pgx/v5/pgxpool" - - "github.com/zitadel/zitadel/backend/storage/database" -) - -type pgxConn struct{ *pgxpool.Conn } - -var _ database.Client = (*pgxConn)(nil) - -// Release implements [database.Client]. -func (c *pgxConn) Release(_ context.Context) error { - c.Conn.Release() - return nil -} - -// Begin implements [database.Client]. -func (c *pgxConn) Begin(ctx context.Context, opts *database.TransactionOptions) (database.Transaction, error) { - tx, err := c.Conn.BeginTx(ctx, transactionOptionsToPgx(opts)) - if err != nil { - return nil, err - } - return &pgxTx{tx}, nil -} - -// Query implements sql.Client. -// Subtle: this method shadows the method (*Conn).Query of pgxConn.Conn. -func (c *pgxConn) Query(ctx context.Context, sql string, args ...any) (database.Rows, error) { - rows, err := c.Conn.Query(ctx, sql, args...) - return &Rows{rows}, err -} - -// QueryRow implements sql.Client. -// Subtle: this method shadows the method (*Conn).QueryRow of pgxConn.Conn. -func (c *pgxConn) QueryRow(ctx context.Context, sql string, args ...any) database.Row { - return c.Conn.QueryRow(ctx, sql, args...) -} - -// Exec implements [database.Pool]. -// Subtle: this method shadows the method (Pool).Exec of pgxPool.Pool. -func (c *pgxConn) Exec(ctx context.Context, sql string, args ...any) error { - _, err := c.Conn.Exec(ctx, sql, args...) - return err -} diff --git a/backend/storage/database/dialect/postgres/pool.go b/backend/storage/database/dialect/postgres/pool.go deleted file mode 100644 index a3e6033e2e..0000000000 --- a/backend/storage/database/dialect/postgres/pool.go +++ /dev/null @@ -1,57 +0,0 @@ -package postgres - -import ( - "context" - - "github.com/jackc/pgx/v5/pgxpool" - - "github.com/zitadel/zitadel/backend/storage/database" -) - -type pgxPool struct{ *pgxpool.Pool } - -var _ database.Pool = (*pgxPool)(nil) - -// Acquire implements [database.Pool]. -func (c *pgxPool) Acquire(ctx context.Context) (database.Client, error) { - conn, err := c.Pool.Acquire(ctx) - if err != nil { - return nil, err - } - return &pgxConn{conn}, nil -} - -// Query implements [database.Pool]. -// Subtle: this method shadows the method (Pool).Query of pgxPool.Pool. -func (c *pgxPool) Query(ctx context.Context, sql string, args ...any) (database.Rows, error) { - rows, err := c.Pool.Query(ctx, sql, args...) - return &Rows{rows}, err -} - -// QueryRow implements [database.Pool]. -// Subtle: this method shadows the method (Pool).QueryRow of pgxPool.Pool. -func (c *pgxPool) QueryRow(ctx context.Context, sql string, args ...any) database.Row { - return c.Pool.QueryRow(ctx, sql, args...) -} - -// Exec implements [database.Pool]. -// Subtle: this method shadows the method (Pool).Exec of pgxPool.Pool. -func (c *pgxPool) Exec(ctx context.Context, sql string, args ...any) error { - _, err := c.Pool.Exec(ctx, sql, args...) - return err -} - -// Begin implements [database.Pool]. -func (c *pgxPool) Begin(ctx context.Context, opts *database.TransactionOptions) (database.Transaction, error) { - tx, err := c.Pool.BeginTx(ctx, transactionOptionsToPgx(opts)) - if err != nil { - return nil, err - } - return &pgxTx{tx}, nil -} - -// Close implements [database.Pool]. -func (c *pgxPool) Close(_ context.Context) error { - c.Pool.Close() - return nil -} diff --git a/backend/storage/database/dialect/postgres/rows.go b/backend/storage/database/dialect/postgres/rows.go deleted file mode 100644 index 91ee2fc27a..0000000000 --- a/backend/storage/database/dialect/postgres/rows.go +++ /dev/null @@ -1,18 +0,0 @@ -package postgres - -import ( - "github.com/jackc/pgx/v5" - - "github.com/zitadel/zitadel/backend/storage/database" -) - -var _ database.Rows = (*Rows)(nil) - -type Rows struct{ pgx.Rows } - -// Close implements [database.Rows]. -// Subtle: this method shadows the method (Rows).Close of Rows.Rows. -func (r *Rows) Close() error { - r.Rows.Close() - return nil -} diff --git a/backend/storage/database/dialect/postgres/tx.go b/backend/storage/database/dialect/postgres/tx.go deleted file mode 100644 index 32f332d185..0000000000 --- a/backend/storage/database/dialect/postgres/tx.go +++ /dev/null @@ -1,95 +0,0 @@ -package postgres - -import ( - "context" - - "github.com/jackc/pgx/v5" - - "github.com/zitadel/zitadel/backend/storage/database" -) - -type pgxTx struct{ pgx.Tx } - -var _ database.Transaction = (*pgxTx)(nil) - -// Commit implements [database.Transaction]. -func (tx *pgxTx) Commit(ctx context.Context) error { - return tx.Tx.Commit(ctx) -} - -// Rollback implements [database.Transaction]. -func (tx *pgxTx) Rollback(ctx context.Context) error { - return tx.Tx.Rollback(ctx) -} - -// End implements [database.Transaction]. -func (tx *pgxTx) End(ctx context.Context, err error) error { - if err != nil { - tx.Rollback(ctx) - return err - } - return tx.Commit(ctx) -} - -// Query implements [database.Transaction]. -// Subtle: this method shadows the method (Tx).Query of pgxTx.Tx. -func (tx *pgxTx) Query(ctx context.Context, sql string, args ...any) (database.Rows, error) { - rows, err := tx.Tx.Query(ctx, sql, args...) - return &Rows{rows}, err -} - -// QueryRow implements [database.Transaction]. -// Subtle: this method shadows the method (Tx).QueryRow of pgxTx.Tx. -func (tx *pgxTx) QueryRow(ctx context.Context, sql string, args ...any) database.Row { - return tx.Tx.QueryRow(ctx, sql, args...) -} - -// Exec implements [database.Transaction]. -// Subtle: this method shadows the method (Pool).Exec of pgxPool.Pool. -func (tx *pgxTx) Exec(ctx context.Context, sql string, args ...any) error { - _, err := tx.Tx.Exec(ctx, sql, args...) - return err -} - -// Begin implements [database.Transaction]. -// As postgres does not support nested transactions we use savepoints to emulate them. -func (tx *pgxTx) Begin(ctx context.Context) (database.Transaction, error) { - savepoint, err := tx.Tx.Begin(ctx) - if err != nil { - return nil, err - } - return &pgxTx{savepoint}, nil -} - -func transactionOptionsToPgx(opts *database.TransactionOptions) pgx.TxOptions { - if opts == nil { - return pgx.TxOptions{} - } - - return pgx.TxOptions{ - IsoLevel: isolationToPgx(opts.IsolationLevel), - AccessMode: accessModeToPgx(opts.AccessMode), - } -} - -func isolationToPgx(isolation database.IsolationLevel) pgx.TxIsoLevel { - switch isolation { - case database.IsolationLevelSerializable: - return pgx.Serializable - case database.IsolationLevelReadCommitted: - return pgx.ReadCommitted - default: - return pgx.Serializable - } -} - -func accessModeToPgx(accessMode database.AccessMode) pgx.TxAccessMode { - switch accessMode { - case database.AccessModeReadWrite: - return pgx.ReadWrite - case database.AccessModeReadOnly: - return pgx.ReadOnly - default: - return pgx.ReadWrite - } -} diff --git a/backend/storage/database/handle.go b/backend/storage/database/handle.go deleted file mode 100644 index 7dab993ede..0000000000 --- a/backend/storage/database/handle.go +++ /dev/null @@ -1,52 +0,0 @@ -package database - -// import ( -// "context" -// "fmt" - -// "github.com/zitadel/zitadel/backend/handler" -// ) - -// func Begin[In, Out, NextOut any](ctx context.Context, beginner Beginner, opts *TransactionOptions) handler.Defer[In, Out, NextOut] { -// // func(ctx context.Context, in *VerifyEmail) (_ *VerifyEmail, _ func(context.Context, error) error, err error) { -// return func(handle handler.DeferrableHandle[In, Out], next handler.Handle[Out, NextOut]) handler.Handle[In, NextOut] { -// return func(ctx context.Context, in In) (out NextOut, err error) { -// tx, err := beginner.Begin(ctx, opts) -// if err != nil { -// return out, err -// } -// defer func() { -// if err != nil { -// rollbackErr := tx.Rollback(ctx) -// if rollbackErr != nil { -// err = fmt.Errorf("query failed: %w, rollback failed: %v", err, rollbackErr) -// } -// } else { -// err = tx.Commit(ctx) -// } -// }() -// return handle(ctx, in, tx) -// } -// } - -// } - -// type QueryExecutorSetter interface { -// SetQueryExecutor(QueryExecutor) -// } - -// func Begin[In QueryExecutorSetter](ctx context.Context, beginner Beginner, in In) (_ In, _ func(context.Context, error) error, err error) { -// tx, err := beginner.Begin(ctx, nil) -// if err != nil { -// return in, nil, err -// } -// in.SetQueryExecutor(tx) -// return in, func(ctx context.Context, err error) error { -// err = tx.End(ctx, err) -// if err != nil { -// return err -// } -// in.SetQueryExecutor(beginner) -// return nil -// }, err -// } diff --git a/backend/storage/database/mock/row.go b/backend/storage/database/mock/row.go deleted file mode 100644 index 3d95247393..0000000000 --- a/backend/storage/database/mock/row.go +++ /dev/null @@ -1,31 +0,0 @@ -package mock - -import ( - "reflect" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/zitadel/zitadel/backend/storage/database" -) - -type Row struct { - t *testing.T - - res []any -} - -func NewRow(t *testing.T, res ...any) *Row { - return &Row{t: t, res: res} -} - -// Scan implements [database.Row]. -func (r *Row) Scan(dest ...any) error { - require.Len(r.t, dest, len(r.res)) - for i := range dest { - reflect.ValueOf(dest[i]).Elem().Set(reflect.ValueOf(r.res[i])) - } - return nil -} - -var _ database.Row = (*Row)(nil) diff --git a/backend/storage/database/mock/transaction.go b/backend/storage/database/mock/transaction.go deleted file mode 100644 index f085a685b9..0000000000 --- a/backend/storage/database/mock/transaction.go +++ /dev/null @@ -1,154 +0,0 @@ -package mock - -import ( - "context" - "errors" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/zitadel/zitadel/backend/storage/database" -) - -type Transaction struct { - t *testing.T - - committed bool - rolledBack bool - - expectations []expecter -} - -func NewTransaction(t *testing.T, opts ...TransactionOption) *Transaction { - tx := &Transaction{t: t} - for _, opt := range opts { - opt(tx) - } - return tx -} - -func (tx *Transaction) nextExpecter() expecter { - if len(tx.expectations) == 0 { - tx.t.Error("no more expectations on transaction") - tx.t.FailNow() - } - - e := tx.expectations[0] - tx.expectations = tx.expectations[1:] - return e -} - -type TransactionOption func(tx *Transaction) - -type expecter interface { - assertArgs(ctx context.Context, stmt string, args ...any) -} - -func ExpectExec(stmt string, args ...any) TransactionOption { - return func(tx *Transaction) { - tx.expectations = append(tx.expectations, &expectation[struct{}]{ - t: tx.t, - expectedStmt: stmt, - expectedArgs: args, - }) - } -} - -func ExpectQuery(res database.Rows, stmt string, args ...any) TransactionOption { - return func(tx *Transaction) { - tx.expectations = append(tx.expectations, &expectation[database.Rows]{ - t: tx.t, - expectedStmt: stmt, - expectedArgs: args, - result: res, - }) - } -} - -func ExpectQueryRow(res database.Row, stmt string, args ...any) TransactionOption { - return func(tx *Transaction) { - tx.expectations = append(tx.expectations, &expectation[database.Row]{ - t: tx.t, - expectedStmt: stmt, - expectedArgs: args, - result: res, - }) - } -} - -type expectation[R any] struct { - t *testing.T - - expectedStmt string - expectedArgs []any - - result R -} - -func (e *expectation[R]) assertArgs(ctx context.Context, stmt string, args ...any) { - e.t.Helper() - assert.Equal(e.t, e.expectedStmt, stmt) - assert.Equal(e.t, e.expectedArgs, args) -} - -// Commit implements [database.Transaction]. -func (t *Transaction) Commit(ctx context.Context) error { - if t.hasEnded() { - return errors.New("transaction already committed or rolled back") - } - t.committed = true - return nil -} - -// End implements [database.Transaction]. -func (tx *Transaction) End(ctx context.Context, err error) error { - if tx.hasEnded() { - return errors.New("transaction already committed or rolled back") - } - if err != nil { - return tx.Rollback(ctx) - } - return tx.Commit(ctx) -} - -// Exec implements [database.Transaction]. -func (tx *Transaction) Exec(ctx context.Context, stmt string, args ...any) error { - tx.nextExpecter().assertArgs(ctx, stmt, args...) - - return nil -} - -// Begin implements [database.Transaction]. -// it is unimplemented -func (tx *Transaction) Begin(ctx context.Context) (database.Transaction, error) { - return nil, errors.New("nested transactions are not supported") -} - -// Query implements [database.Transaction]. -func (tx *Transaction) Query(ctx context.Context, stmt string, args ...any) (database.Rows, error) { - e := tx.nextExpecter() - e.assertArgs(ctx, stmt, args...) - return e.(*expectation[database.Rows]).result, nil -} - -// QueryRow implements [database.Transaction]. -func (tx *Transaction) QueryRow(ctx context.Context, stmt string, args ...any) database.Row { - e := tx.nextExpecter() - e.assertArgs(ctx, stmt, args...) - return e.(*expectation[database.Row]).result -} - -// Rollback implements [database.Transaction]. -func (tx *Transaction) Rollback(ctx context.Context) error { - if tx.hasEnded() { - return errors.New("transaction already committed or rolled back") - } - tx.rolledBack = true - return nil -} - -var _ database.Transaction = (*Transaction)(nil) - -func (tx *Transaction) hasEnded() bool { - return tx.committed || tx.rolledBack -} diff --git a/backend/storage/event_store.go b/backend/storage/event_store.go deleted file mode 100644 index cfe7e1ae96..0000000000 --- a/backend/storage/event_store.go +++ /dev/null @@ -1,155 +0,0 @@ -package storage - -// import ( -// "context" -// "time" - -// "github.com/jackc/pgx/v5" -// "github.com/jackc/pgx/v5/pgconn" -// "github.com/jackc/pgx/v5/pgxpool" -// ) - -// type EventStore interface { -// Write(ctx context.Context, commands ...Command) error -// } - -// type Command interface { -// Aggregate() Aggregate - -// Type() string -// Payload() any -// Revision() uint8 -// Creator() string - -// Operations() []Operation -// } - -// type Event interface { -// ID() string - -// Aggregate() Aggregate - -// CreatedAt() time.Time -// Type() string -// UnmarshalPayload(ptr any) error - -// Revision() uint8 -// Creator() string - -// Position() float64 -// InPositionOrder() uint16 -// } - -// type Aggregate struct { -// Type string -// ID string -// Instance string -// } - -// type Operation interface { -// Exec(ctx context.Context, tx TX) error -// } - -// type TX interface { -// Query() // Placeholder for future query methods -// } - -// type createInstance struct { -// id string -// creator string -// Name string -// } - -// var ( -// _ Command = (*createInstance)(nil) -// ) - -// func (c *createInstance) Aggregate() Aggregate { -// return Aggregate{ -// Type: "instance", -// ID: c.id, -// } -// } - -// func (c *createInstance) Type() string { -// return "instance.created" -// } - -// func (c *createInstance) Payload() any { -// return c -// } - -// func (c *createInstance) Revision() uint8 { -// return 1 -// } - -// func (c *createInstance) Creator() string { -// return c.creator -// } - -// func (c *createInstance) Operations() []Operation { -// return []Operation{} -// } - -// type executor[R any] interface { -// Exec(ctx context.Context, query string, args ...any) (R, error) -// } - -// type querier[R any, Q Query[R]] interface { -// Query(ctx context.Context, query Q) (R, error) -// // Query(ctx context.Context, query string, args ...any) (R, error) -// } - -// type Query[R any] interface { -// Query(ctx context.Context) (R, error) -// } - -// var _ executor[pgconn.CommandTag] = (*pgxExecutor)(nil) - -// type pgxExecutor struct { -// conn *pgx.Conn -// } - -// func (e *pgxExecutor) Exec(ctx context.Context, query string, args ...any) (pgconn.CommandTag, error) { -// return e.conn.Exec(ctx, query, args...) -// } - -// type pgxQuerier struct { -// conn *pgx.Conn -// } - -// type pgxCreateInstanceQuery struct { -// Name string -// } - -// type instance struct{ -// id string -// name string -// } - -// type instanceByIDQuery[R Rows] struct{ -// id string -// } - -// type Rows interface { -// Next() bool -// Scan(dest ...any) error -// } - -// func (q *instanceByIDQuery[R]) Query(ctx context.Context) (*instance, error) { -// return nil, nil -// } - -// type pgxInstanceRepository struct { -// pool pgxpool.Pool -// } - -// func (r *pgxInstanceRepository) InstanceByID(ctx context.Context, id string) (*instance, error) { -// query := &instanceByIDQuery[pgx.Rows]{} -// query. -// return nil, nil -// } - -// func (q *pgxQuerier) Query(ctx context.Context, query string, args ...any) (pgx.Rows, error) { -// return q.conn.Query(ctx, query, args...) -// } diff --git a/backend/storage/eventstore/event_store.go b/backend/storage/eventstore/event_store.go deleted file mode 100644 index 43afa9eaa0..0000000000 --- a/backend/storage/eventstore/event_store.go +++ /dev/null @@ -1,25 +0,0 @@ -package eventstore - -import ( - "context" - - "github.com/zitadel/zitadel/backend/storage/database" -) - -type Eventstore struct { - executor database.Executor -} - -func New(executor database.Executor) *Eventstore { - return &Eventstore{executor: executor} -} - -type Event interface{} - -func (e *Eventstore) Push(ctx context.Context, events ...Event) error { - return nil -} - -func Push(ctx context.Context, executor database.Executor, events ...Event) error { - return New(executor).Push(ctx, events...) -} diff --git a/backend/storage/instance_repository.go b/backend/storage/instance_repository.go deleted file mode 100644 index 2b6fc479ad..0000000000 --- a/backend/storage/instance_repository.go +++ /dev/null @@ -1,111 +0,0 @@ -package storage - -// import ( -// "context" - -// "github.com/jackc/pgx/v5" -// "github.com/jackc/pgx/v5/pgxpool" -// ) - -// type Instance struct { -// ID string -// Name string -// } - -// // type InstanceRepository interface { -// // InstanceByID(id string) (*Instance, error) -// // } - -// type Row interface { -// Scan(dest ...interface{}) error -// } - -// type RowQuerier[R Row] interface { -// QueryRow(ctx context.Context, query string, args ...any) R -// } - -// type Rows interface { -// Row -// Next() bool -// Close() -// Err() error -// } - -// type RowsQuerier[R Rows] interface { -// Query(ctx context.Context, query string, args ...any) (R, error) -// } - -// type Executor interface { -// Exec(ctx context.Context, query string, args ...any) error -// } - -// type instanceByIDQuery[R Row, Q RowQuerier[R]] struct { -// id string -// querier Q -// } - -// func (q instanceByIDQuery[R, Q]) Query(ctx context.Context) (*Instance, error) { -// row := q.querier.QueryRow(ctx, "SELECT * FROM instances WHERE id = $1", q.id) -// var instance Instance -// if err := row.Scan(&instance.ID, &instance.Name); err != nil { -// return nil, err -// } -// return &instance, nil -// } - -// type InstanceRepository interface { -// ByIDQuery(ctx context.Context, id string) (*Instance, error) -// ByDomainQuery(ctx context.Context, domain string) (*Instance, error) -// } - -// type InstanceRepositorySQL struct { -// pool *pgxpool.Pool -// } - -// type InstanceRepositoryMap struct { -// instances map[string]*Instance -// domains *InstanceDomainRepositoryMao -// } - -// type InstanceDomainRepositoryMao struct { -// domains map[string]string -// } - -// func GetInstanceByID[R Row, Q RowQuerier[R]](ctx context.Context, querier Q, id string) (*Instance, error) { -// row := querier.QueryRow(ctx, "SELECT * FROM instances WHERE id = $1", id) -// var instance Instance -// if err := row.Scan(&instance.ID, &instance.Name); err != nil { -// return nil, err -// } -// return &instance, nil -// } - -// const instanceByDomainQuery = `SELECT -// i.* -// FROM -// instances i -// JOIN -// instance_domains id -// ON -// id.instance_id = i.id -// WHERE -// id.domain = $1` - -// func GetInstanceByDomain[R Row, Q RowQuerier[R]](ctx context.Context, querier Q, domain string) (*Instance, error) { -// row := querier.QueryRow(ctx, instanceByDomainQuery, domain) -// var instance Instance -// if err := row.Scan(); err != nil { -// return nil, err -// } -// return &instance, nil -// } - -// func CreateInstance[E Executor](ctx context.Context, executor E, instance *Instance) error { -// return executor.Exec(ctx, "INSERT INTO instances (id, name) VALUES ($1, $2)", instance.ID, instance.Name) -// } - -// func bla(ctx context.Context) { -// var c *pgxpool.Tx -// instance, err := instanceByIDQuery[pgx.Row, *pgxpool.Tx]{querier: c}.Query(ctx) -// _, _ = instance, err -// } diff --git a/backend/storage/repo.go b/backend/storage/repo.go deleted file mode 100644 index d2b373202c..0000000000 --- a/backend/storage/repo.go +++ /dev/null @@ -1,75 +0,0 @@ -package storage - -// import ( -// "context" - -// "github.com/jackc/pgx/v5" -// "github.com/jackc/pgx/v5/pgxpool" -// ) - -// type row interface { -// Scan(dest ...any) error -// } - -// type rows interface { -// row -// Next() bool -// Close() -// Err() error -// } - -// type querier interface { -// NewRowQuery() -// NewRowsQuery() -// } - -// type Client interface { -// // querier -// InstanceRepository() instanceRepository -// } - -// type instanceRepository interface { -// ByIDQuery(ctx context.Context, id string) (*Instance, error) -// ByDomainQuery(ctx context.Context, domain string) (*Instance, error) -// } - -// type pgxClient pgxpool.Pool - -// func (c *pgxClient) Begin(ctx context.Context) (*pgxTx, error) { -// tx, err := (*pgxpool.Pool)(c).Begin(ctx) -// if err != nil { -// return nil, err -// } -// return (*pgxTx)(tx.(*pgxpool.Tx)), nil -// } - -// func (c *pgxClient) InstanceRepository() instanceRepository { -// return &pgxInstanceRepository[pgxpool.Pool]{client: (*pgxpool.Pool)(c)} -// } - -// type pgxTx pgxpool.Tx - -// func (c *pgxTx) InstanceRepository() instanceRepository { -// return &pgxInstanceRepository[pgxpool.Tx]{client: (*pgxpool.Tx)(c)} -// } - -// type pgxInstanceRepository[C pgxpool.Pool | pgxpool.Tx] struct { -// client *C -// } - -// func (r *pgxInstanceRepository[C]) ByIDQuery(ctx context.Context, id string) (*Instance, error) { -// // return r.client -// pgx.Tx -// return nil, nil -// } - -// func (r *pgxInstanceRepository[C]) ByDomainQuery(ctx context.Context, domain string) (*Instance, error) { - -// return nil, nil -// } - -// func blabla() { -// var client Client = &pgxClient{&pgxpool.Pool{}} - -// client.be -// } diff --git a/backend/storage/storage.go b/backend/storage/storage.go deleted file mode 100644 index e1fd431a6e..0000000000 --- a/backend/storage/storage.go +++ /dev/null @@ -1,43 +0,0 @@ -package storage - -// import "context" - -// type Storage interface { -// Write(ctx context.Context, commands ...Command) error -// } - -// type Command interface { -// // Subjects returns a list of subjects which describe the command -// // the type should always be in past tense -// // "." is used as separator -// // e.g. "user.created" -// Type() string -// // Payload returns the payload of the command -// // The payload can either be -// // - a struct -// // - a map -// // - nil -// Payload() any -// // Revision returns the revision of the command -// Revision() uint8 -// // Creator returns the user id who created the command -// Creator() string - -// // Object returns the object the command belongs to -// Object() Model -// // Parents returns the parents of the object -// // If the list is empty there are no parents -// Parents() []Model - -// // Objects returns the models to update during inserting the commands -// Objects() []Object -// } - -// type Model struct { -// Name string -// ID string -// } - -// type Object struct { -// Model Model -// } diff --git a/backend/telemetry/logging/logger.go b/backend/telemetry/logging/logger.go deleted file mode 100644 index 5a29d8aa01..0000000000 --- a/backend/telemetry/logging/logger.go +++ /dev/null @@ -1,55 +0,0 @@ -package logging - -import ( - "context" - "log" - "log/slog" - - "github.com/zitadel/zitadel/backend/handler" -) - -type Logger struct { - *slog.Logger -} - -func New(l *slog.Logger) *Logger { - return &Logger{Logger: l} -} - -func (l *Logger) With(args ...any) *Logger { - return &Logger{l.Logger.With(args...)} -} - -// Wrap decorates the given handle function with -// The function is safe to call with nil logger. -func Wrap[Req, Res any](logger *Logger, name string, handle handler.Handle[Req, Res]) handler.Handle[Req, Res] { - if logger == nil { - return handle - } - return func(ctx context.Context, r Req) (_ Res, err error) { - logger.Debug("execute", slog.String("handler", name)) - defer logger.Debug("done", slog.String("handler", name)) - log.Println("log.wrap", name) - return handle(ctx, r) - } -} - -// Decorate decorates the given handle function with logging. -// The function is safe to call with nil logger. -func Decorate[Req, Res any](logger *Logger, name string) handler.Middleware[Req, Res] { - return func(ctx context.Context, request Req, handle handler.Handle[Req, Res]) (res Res, err error) { - if logger == nil { - return handle(ctx, request) - } - logger = logger.With("handler", name) - logger.DebugContext(ctx, "execute") - log.Println("logged.decorate", name) - defer func() { - if err != nil { - logger.ErrorContext(ctx, "failed", slog.String("cause", err.Error())) - } - logger.DebugContext(ctx, "done") - }() - return handle(ctx, request) - } -} diff --git a/backend/telemetry/tracing/tracer.go b/backend/telemetry/tracing/tracer.go deleted file mode 100644 index d3634075e1..0000000000 --- a/backend/telemetry/tracing/tracer.go +++ /dev/null @@ -1,111 +0,0 @@ -package tracing - -import ( - "context" - "log" - "runtime" - - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/trace" - - "github.com/zitadel/zitadel/backend/handler" -) - -type Tracer struct{ trace.Tracer } - -func NewTracer(name string) *Tracer { - return &Tracer{otel.Tracer(name)} -} - -type DecorateOption func(*DecorateOptions) - -type DecorateOptions struct { - startOpts []trace.SpanStartOption - endOpts []trace.SpanEndOption - - spanName string - - span trace.Span -} - -func WithSpanName(name string) DecorateOption { - return func(o *DecorateOptions) { - o.spanName = name - } -} - -func WithSpanStartOptions(opts ...trace.SpanStartOption) DecorateOption { - return func(o *DecorateOptions) { - o.startOpts = append(o.startOpts, opts...) - } -} - -func WithSpanEndOptions(opts ...trace.SpanEndOption) DecorateOption { - return func(o *DecorateOptions) { - o.endOpts = append(o.endOpts, opts...) - } -} - -// Wrap decorates the given handle function with tracing. -// The function is safe to call with nil tracer. -func Wrap[Req, Res any](tracer *Tracer, name string, handle handler.Handle[Req, Res]) handler.Handle[Req, Res] { - if tracer == nil { - return handle - } - return func(ctx context.Context, r Req) (_ Res, err error) { - ctx, span := tracer.Start( - ctx, - name, - ) - log.Println("trace.wrap", name) - defer func() { - if err != nil { - span.RecordError(err) - } - span.End() - }() - return handle(ctx, r) - } -} - -// Decorate decorates the given handle function with -// The function is safe to call with nil tracer. -func Decorate[Req, Res any](tracer *Tracer, opts ...DecorateOption) handler.Middleware[Req, Res] { - return func(ctx context.Context, r Req, handle handler.Handle[Req, Res]) (_ Res, err error) { - if tracer == nil { - return handle(ctx, r) - } - o := new(DecorateOptions) - for _, opt := range opts { - opt(o) - } - log.Println("traced.decorate") - - ctx, end := o.Start(ctx, tracer) - defer end(err) - return handle(ctx, r) - } -} - -func (o *DecorateOptions) Start(ctx context.Context, tracer *Tracer) (context.Context, func(error)) { - if o.spanName == "" { - o.spanName = functionName() - } - ctx, o.span = tracer.Tracer.Start(ctx, o.spanName, o.startOpts...) - return ctx, o.end -} - -func (o *DecorateOptions) end(err error) { - o.span.RecordError(err) - o.span.End(o.endOpts...) -} - -func functionName() string { - counter, _, _, success := runtime.Caller(2) - - if !success { - return "zitadel" - } - - return runtime.FuncForPC(counter).Name() -} diff --git a/backend/v3/api/org/v2/org.go b/backend/v3/api/org/v2/org.go new file mode 100644 index 0000000000..b7b2283e4f --- /dev/null +++ b/backend/v3/api/org/v2/org.go @@ -0,0 +1,33 @@ +package orgv2 + +import ( + "context" + + "github.com/zitadel/zitadel/backend/v3/domain" + "github.com/zitadel/zitadel/pkg/grpc/org/v2" +) + +func CreateOrg(ctx context.Context, req *org.AddOrganizationRequest) (resp *org.AddOrganizationResponse, err error) { + cmd := domain.NewAddOrgCommand( + req.GetName(), + addOrgAdminToCommand(req.GetAdmins()...)..., + ) + err = domain.Invoke(ctx, cmd) + if err != nil { + return nil, err + } + return &org.AddOrganizationResponse{ + OrganizationId: cmd.ID, + }, nil +} + +func addOrgAdminToCommand(admins ...*org.AddOrganizationRequest_Admin) []*domain.AddMemberCommand { + cmds := make([]*domain.AddMemberCommand, len(admins)) + for i, admin := range admins { + cmds[i] = &domain.AddMemberCommand{ + UserID: admin.GetUserId(), + Roles: admin.GetRoles(), + } + } + return cmds +} diff --git a/backend/v3/api/org/v2/server.go b/backend/v3/api/org/v2/server.go new file mode 100644 index 0000000000..27e37344af --- /dev/null +++ b/backend/v3/api/org/v2/server.go @@ -0,0 +1,19 @@ +package orgv2 + +import ( + "github.com/zitadel/zitadel/backend/v3/telemetry/logging" + "github.com/zitadel/zitadel/backend/v3/telemetry/tracing" +) + +var ( + logger logging.Logger + tracer tracing.Tracer +) + +func SetLogger(l logging.Logger) { + logger = l +} + +func SetTracer(t tracing.Tracer) { + tracer = t +} diff --git a/backend/v3/domain/instance.go b/backend/v3/domain/instance.go index 54833a1173..95f4843951 100644 --- a/backend/v3/domain/instance.go +++ b/backend/v3/domain/instance.go @@ -3,6 +3,8 @@ package domain import ( "context" "time" + + "github.com/zitadel/zitadel/backend/v3/storage/database" ) type Instance struct { @@ -19,16 +21,43 @@ func (i *Instance) Keys(index string) (key []string) { return []string{} } -type InstanceRepository interface { - ByID(ctx context.Context, id string) (*Instance, error) - Create(ctx context.Context, instance *Instance) error - On(id string) InstanceOperation +type instanceColumns interface { + // IDColumn returns the column for the id field. + IDColumn() database.Column + // NameColumn returns the column for the name field. + NameColumn() database.Column + // CreatedAtColumn returns the column for the created at field. + CreatedAtColumn() database.Column + // UpdatedAtColumn returns the column for the updated at field. + UpdatedAtColumn() database.Column + // DeletedAtColumn returns the column for the deleted at field. + DeletedAtColumn() database.Column } -type InstanceOperation interface { - AdminRepository - Update(ctx context.Context, instance *Instance) error - Delete(ctx context.Context) error +type instanceConditions interface { + // IDCondition returns an equal filter on the id field. + IDCondition(instanceID string) database.Condition + // NameCondition returns a filter on the name field. + NameCondition(op database.TextOperation, name string) database.Condition +} + +type instanceChanges interface { + // SetName sets the name column. + SetName(name string) database.Change +} + +type InstanceRepository interface { + instanceColumns + instanceConditions + instanceChanges + + Member() MemberRepository + + Get(ctx context.Context, opts ...database.QueryOption) (*Instance, error) + + Create(ctx context.Context, instance *Instance) error + Update(ctx context.Context, condition database.Condition, changes ...database.Change) error + Delete(ctx context.Context, condition database.Condition) error } type CreateInstance struct { diff --git a/backend/v3/domain/org.go b/backend/v3/domain/org.go index 13e90dbef6..d7d2aeabaf 100644 --- a/backend/v3/domain/org.go +++ b/backend/v3/domain/org.go @@ -3,33 +3,95 @@ package domain import ( "context" "time" + + "github.com/zitadel/zitadel/backend/v3/storage/database" +) + +type OrgState uint8 + +const ( + OrgStateActive OrgState = iota + 1 + OrgStateInactive ) type Org struct { ID string `json:"id"` Name string `json:"name"` + State OrgState `json:"state"` + CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } +type orgColumns interface { + // InstanceIDColumn returns the column for the instance id field. + InstanceIDColumn() database.Column + // IDColumn returns the column for the id field. + IDColumn() database.Column + // NameColumn returns the column for the name field. + NameColumn() database.Column + // StateColumn returns the column for the state field. + StateColumn() database.Column + // CreatedAtColumn returns the column for the created at field. + CreatedAtColumn() database.Column + // UpdatedAtColumn returns the column for the updated at field. + UpdatedAtColumn() database.Column + // DeletedAtColumn returns the column for the deleted at field. + DeletedAtColumn() database.Column +} + +type orgConditions interface { + // InstanceIDCondition returns an equal filter on the instance id field. + InstanceIDCondition(instanceID string) database.Condition + // IDCondition returns an equal filter on the id field. + IDCondition(orgID string) database.Condition + // NameCondition returns a filter on the name field. + NameCondition(op database.TextOperation, name string) database.Condition + // StateCondition returns a filter on the state field. + StateCondition(op database.NumberOperation, state OrgState) database.Condition +} + +type orgChanges interface { + // SetName sets the name column. + SetName(name string) database.Change + // SetState sets the state column. + SetState(state OrgState) database.Change +} + type OrgRepository interface { - ByID(ctx context.Context, orgID string) (*Org, error) + orgColumns + orgConditions + orgChanges + + // Member returns the admin repository. + Member() MemberRepository + // Domain returns the domain repository. + Domain() DomainRepository + + // Get returns an org based on the given condition. + Get(ctx context.Context, opts ...database.QueryOption) (*Org, error) + // List returns a list of orgs based on the given condition. + List(ctx context.Context, opts ...database.QueryOption) ([]*Org, error) + // Create creates a new org. Create(ctx context.Context, org *Org) error - On(id string) OrgOperation + // Delete removes orgs based on the given condition. + Delete(ctx context.Context, condition database.Condition) error + // Update executes the given changes based on the given condition. + Update(ctx context.Context, condition database.Condition, changes ...database.Change) error } type OrgOperation interface { - AdminRepository + MemberRepository DomainRepository Update(ctx context.Context, org *Org) error Delete(ctx context.Context) error } -type AdminRepository interface { - AddAdmin(ctx context.Context, userID string, roles []string) error - SetAdminRoles(ctx context.Context, userID string, roles []string) error - RemoveAdmin(ctx context.Context, userID string) error +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 } type DomainRepository interface { diff --git a/backend/v3/domain/org_add.go b/backend/v3/domain/org_add.go index c7bbe56650..4d2fca35da 100644 --- a/backend/v3/domain/org_add.go +++ b/backend/v3/domain/org_add.go @@ -2,15 +2,17 @@ package domain import ( "context" + + "github.com/zitadel/zitadel/backend/v3/storage/eventstore" ) type AddOrgCommand struct { - ID string `json:"id"` - Name string `json:"name"` - Admins []AddAdminCommand `json:"admins"` + ID string `json:"id"` + Name string `json:"name"` + Admins []*AddMemberCommand `json:"admins"` } -func NewAddOrgCommand(name string, admins ...AddAdminCommand) *AddOrgCommand { +func NewAddOrgCommand(name string, admins ...*AddMemberCommand) *AddOrgCommand { return &AddOrgCommand{ Name: name, Admins: admins, @@ -39,11 +41,31 @@ func (cmd *AddOrgCommand) Execute(ctx context.Context, opts *CommandOpts) (err e return err } + for _, admin := range cmd.Admins { + admin.orgID = cmd.ID + if err = opts.Invoke(ctx, admin); err != nil { + return err + } + } + return nil } +// Events implements [eventer]. +func (cmd *AddOrgCommand) Events() []*eventstore.Event { + return []*eventstore.Event{ + { + AggregateType: "org", + AggregateID: cmd.ID, + Type: "org.added", + Payload: cmd, + }, + } +} + var ( _ Commander = (*AddOrgCommand)(nil) + _ eventer = (*AddOrgCommand)(nil) ) func (cmd *AddOrgCommand) ensureID() (err error) { @@ -54,21 +76,36 @@ func (cmd *AddOrgCommand) ensureID() (err error) { return err } -type AddAdminCommand struct { +type AddMemberCommand struct { + orgID string UserID string `json:"userId"` Roles []string `json:"roles"` } // Execute implements Commander. -func (a *AddAdminCommand) Execute(ctx context.Context, opts *CommandOpts) (err error) { +func (a *AddMemberCommand) Execute(ctx context.Context, opts *CommandOpts) (err error) { close, err := opts.EnsureTx(ctx) if err != nil { return err } defer func() { err = close(ctx, err) }() - return nil + + return orgRepo(opts.DB).Member().AddMember(ctx, a.orgID, a.UserID, a.Roles) +} + +// Events implements [eventer]. +func (a *AddMemberCommand) Events() []*eventstore.Event { + return []*eventstore.Event{ + { + AggregateType: "org", + AggregateID: a.UserID, + Type: "member.added", + Payload: a, + }, + } } var ( - _ Commander = (*AddAdminCommand)(nil) + _ Commander = (*AddMemberCommand)(nil) + _ eventer = (*AddMemberCommand)(nil) ) diff --git a/backend/v3/storage/database/mock/database.mock.go b/backend/v3/storage/database/dbmock/database.mock.go similarity index 99% rename from backend/v3/storage/database/mock/database.mock.go rename to backend/v3/storage/database/dbmock/database.mock.go index 2460d5b75c..215804ded8 100644 --- a/backend/v3/storage/database/mock/database.mock.go +++ b/backend/v3/storage/database/dbmock/database.mock.go @@ -3,11 +3,11 @@ // // Generated by this command: // -// mockgen -typed -package mock -destination ./mock/database.mock.go github.com/zitadel/zitadel/backend/v3/storage/database Pool,Client,Row,Rows,Transaction +// mockgen -typed -package dbmock -destination ./dbmock/database.mock.go github.com/zitadel/zitadel/backend/v3/storage/database Pool,Client,Row,Rows,Transaction // -// Package mock is a generated GoMock package. -package mock +// Package dbmock is a generated GoMock package. +package dbmock import ( context "context" diff --git a/backend/v3/storage/database/dialect/config.go b/backend/v3/storage/database/dialect/config.go index a044f7bd4e..77f571f67a 100644 --- a/backend/v3/storage/database/dialect/config.go +++ b/backend/v3/storage/database/dialect/config.go @@ -8,8 +8,8 @@ import ( "github.com/mitchellh/mapstructure" "github.com/spf13/viper" - "github.com/zitadel/zitadel/backend/storage/database" - "github.com/zitadel/zitadel/backend/storage/database/dialect/postgres" + "github.com/zitadel/zitadel/backend/v3/storage/database" + "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/postgres" ) type Hook struct { diff --git a/backend/v3/storage/database/gen_mock.go b/backend/v3/storage/database/gen_mock.go index e8a319c7f0..04d204cfa1 100644 --- a/backend/v3/storage/database/gen_mock.go +++ b/backend/v3/storage/database/gen_mock.go @@ -1,3 +1,3 @@ package database -//go:generate mockgen -typed -package mock -destination ./mock/database.mock.go github.com/zitadel/zitadel/backend/v3/storage/database Pool,Client,Row,Rows,Transaction +//go:generate mockgen -typed -package dbmock -destination ./dbmock/database.mock.go github.com/zitadel/zitadel/backend/v3/storage/database Pool,Client,Row,Rows,Transaction diff --git a/backend/v3/storage/database/operators.go b/backend/v3/storage/database/operators.go index d8e34d8b8b..8c9021e1f5 100644 --- a/backend/v3/storage/database/operators.go +++ b/backend/v3/storage/database/operators.go @@ -49,7 +49,7 @@ func writeTextOperation[T Text](builder *StatementBuilder, col Column, op TextOp case TextOperationEqual, TextOperationNotEqual: col.Write(builder) builder.WriteString(textOperations[op]) - builder.WriteString(builder.AppendArg(value)) + builder.WriteArg(value) case TextOperationEqualIgnoreCase, TextOperationNotEqualIgnoreCase: if ignoreCaseCol, ok := col.(ignoreCaseColumn); ok { ignoreCaseCol.WriteIgnoreCase(builder) @@ -60,12 +60,12 @@ func writeTextOperation[T Text](builder *StatementBuilder, col Column, op TextOp } builder.WriteString(textOperations[op]) builder.WriteString("LOWER(") - builder.WriteString(builder.AppendArg(value)) + builder.WriteArg(value) builder.WriteString(")") case TextOperationStartsWith: col.Write(builder) builder.WriteString(textOperations[op]) - builder.WriteString(builder.AppendArg(value)) + builder.WriteArg(value) builder.WriteString(" || '%'") case TextOperationStartsWithIgnoreCase: if ignoreCaseCol, ok := col.(ignoreCaseColumn); ok { @@ -77,7 +77,7 @@ func writeTextOperation[T Text](builder *StatementBuilder, col Column, op TextOp } builder.WriteString(textOperations[op]) builder.WriteString("LOWER(") - builder.WriteString(builder.AppendArg(value)) + builder.WriteArg(value) builder.WriteString(")") builder.WriteString(" || '%'") default: @@ -118,7 +118,7 @@ var numberOperations = map[NumberOperation]string{ func writeNumberOperation[T Number](builder *StatementBuilder, col Column, op NumberOperation, value T) { col.Write(builder) builder.WriteString(numberOperations[op]) - builder.WriteString(builder.AppendArg(value)) + builder.WriteArg(value) } type Boolean interface { @@ -135,5 +135,5 @@ const ( func writeBooleanOperation[T Boolean](builder *StatementBuilder, col Column, value T) { col.Write(builder) builder.WriteString(" IS ") - builder.WriteString(builder.AppendArg(value)) + builder.WriteArg(value) } diff --git a/backend/v3/storage/database/repository/clause.go b/backend/v3/storage/database/repository/clause.go deleted file mode 100644 index df0603d1bc..0000000000 --- a/backend/v3/storage/database/repository/clause.go +++ /dev/null @@ -1,160 +0,0 @@ -package repository - -import ( - "fmt" - - "github.com/zitadel/zitadel/backend/v3/domain" -) - -type field interface { - fmt.Stringer -} - -type fieldDescriptor struct { - schema string - table string - name string -} - -func (f fieldDescriptor) String() string { - return f.schema + "." + f.table + "." + f.name -} - -type ignoreCaseFieldDescriptor struct { - fieldDescriptor - fieldNameSuffix string -} - -func (f ignoreCaseFieldDescriptor) String() string { - return f.fieldDescriptor.String() + f.fieldNameSuffix -} - -type textFieldDescriptor struct { - field - isIgnoreCase bool -} - -type clause[Op domain.Operation] struct { - field field - op Op -} - -const ( - schema = "zitadel" - userTable = "users" -) - -var userFields = map[domain.UserField]field{ - domain.UserFieldInstanceID: fieldDescriptor{ - schema: schema, - table: userTable, - name: "instance_id", - }, - domain.UserFieldOrgID: fieldDescriptor{ - schema: schema, - table: userTable, - name: "org_id", - }, - domain.UserFieldID: fieldDescriptor{ - schema: schema, - table: userTable, - name: "id", - }, - domain.UserFieldUsername: textFieldDescriptor{ - field: ignoreCaseFieldDescriptor{ - fieldDescriptor: fieldDescriptor{ - schema: schema, - table: userTable, - name: "username", - }, - fieldNameSuffix: "_lower", - }, - }, - domain.UserHumanFieldEmail: textFieldDescriptor{ - field: ignoreCaseFieldDescriptor{ - fieldDescriptor: fieldDescriptor{ - schema: schema, - table: userTable, - name: "email", - }, - fieldNameSuffix: "_lower", - }, - }, - domain.UserHumanFieldEmailVerified: fieldDescriptor{ - schema: schema, - table: userTable, - name: "email_is_verified", - }, -} - -type textClause[V domain.Text] struct { - clause[domain.TextOperation] - value V -} - -var textOp map[domain.TextOperation]string = map[domain.TextOperation]string{ - domain.TextOperationEqual: " = ", - domain.TextOperationNotEqual: " <> ", - domain.TextOperationStartsWith: " LIKE ", - domain.TextOperationStartsWithIgnoreCase: " LIKE ", -} - -func (tc textClause[V]) Write(stmt *statement) { - placeholder := stmt.appendArg(tc.value) - var ( - left, right string - ) - switch tc.clause.op { - case domain.TextOperationEqual: - left = tc.clause.field.String() - right = placeholder - case domain.TextOperationNotEqual: - left = tc.clause.field.String() - right = placeholder - case domain.TextOperationStartsWith: - left = tc.clause.field.String() - right = placeholder + "%" - case domain.TextOperationStartsWithIgnoreCase: - left = tc.clause.field.String() - if _, ok := tc.clause.field.(ignoreCaseFieldDescriptor); !ok { - left = "LOWER(" + left + ")" - } - right = "LOWER(" + placeholder + "%)" - } - - stmt.builder.WriteString(left) - stmt.builder.WriteString(textOp[tc.clause.op]) - stmt.builder.WriteString(right) -} - -type boolClause[V domain.Bool] struct { - clause[domain.BoolOperation] - value V -} - -func (bc boolClause[V]) Write(stmt *statement) { - if !bc.value { - stmt.builder.WriteString("NOT ") - } - stmt.builder.WriteString(bc.clause.field.String()) -} - -type numberClause[V domain.Number] struct { - clause[domain.NumberOperation] - value V -} - -var numberOp map[domain.NumberOperation]string = map[domain.NumberOperation]string{ - domain.NumberOperationEqual: " = ", - domain.NumberOperationNotEqual: " <> ", - domain.NumberOperationLessThan: " < ", - domain.NumberOperationLessThanOrEqual: " <= ", - domain.NumberOperationGreaterThan: " > ", - domain.NumberOperationGreaterThanOrEqual: " >= ", -} - -func (nc numberClause[V]) Write(stmt *statement) { - stmt.builder.WriteString(nc.clause.field.String()) - stmt.builder.WriteString(numberOp[nc.clause.op]) - stmt.builder.WriteString(stmt.appendArg(nc.value)) -} diff --git a/backend/v3/storage/database/repository/crypto.go b/backend/v3/storage/database/repository/crypto.go deleted file mode 100644 index cd547af147..0000000000 --- a/backend/v3/storage/database/repository/crypto.go +++ /dev/null @@ -1,45 +0,0 @@ -package repository - -import ( - "context" - - "github.com/zitadel/zitadel/backend/v3/domain" - "github.com/zitadel/zitadel/backend/v3/storage/database" - "github.com/zitadel/zitadel/internal/crypto" -) - -type cryptoRepo struct { - database.QueryExecutor -} - -func Crypto(db database.QueryExecutor) domain.CryptoRepository { - return &cryptoRepo{ - QueryExecutor: db, - } -} - -const getEncryptionConfigQuery = "SELECT" + - " length" + - ", expiry" + - ", should_include_lower_letters" + - ", should_include_upper_letters" + - ", should_include_digits" + - ", should_include_symbols" + - " FROM encryption_config" - -func (repo *cryptoRepo) GetEncryptionConfig(ctx context.Context) (*crypto.GeneratorConfig, error) { - var config crypto.GeneratorConfig - row := repo.QueryRow(ctx, getEncryptionConfigQuery) - err := row.Scan( - &config.Length, - &config.Expiry, - &config.IncludeLowerLetters, - &config.IncludeUpperLetters, - &config.IncludeDigits, - &config.IncludeSymbols, - ) - if err != nil { - return nil, err - } - return &config, nil -} diff --git a/backend/v3/storage/database/repository/doc.go b/backend/v3/storage/database/repository/doc.go deleted file mode 100644 index ba567e747c..0000000000 --- a/backend/v3/storage/database/repository/doc.go +++ /dev/null @@ -1,7 +0,0 @@ -// Repository package provides the database repository for the application. -// It contains the implementation of the [repository pattern](https://martinfowler.com/eaaCatalog/repository.html) for the database. - -// funcs which need to interact with the database should create interfaces which are implemented by the -// [query] and [exec] structs respectively their factory methods [Query] and [Execute]. The [query] struct is used for read operations, while the [exec] struct is used for write operations. - -package repository diff --git a/backend/v3/storage/database/repository/stmt/v4/inheritance.sql b/backend/v3/storage/database/repository/inheritance.sql similarity index 100% rename from backend/v3/storage/database/repository/stmt/v4/inheritance.sql rename to backend/v3/storage/database/repository/inheritance.sql diff --git a/backend/v3/storage/database/repository/instance.go b/backend/v3/storage/database/repository/instance.go deleted file mode 100644 index d8432c546c..0000000000 --- a/backend/v3/storage/database/repository/instance.go +++ /dev/null @@ -1,54 +0,0 @@ -package repository - -import ( - "context" - - "github.com/zitadel/zitadel/backend/v3/domain" - "github.com/zitadel/zitadel/backend/v3/storage/database" -) - -type instance struct { - database.QueryExecutor -} - -func Instance(client database.QueryExecutor) domain.InstanceRepository { - return &instance{QueryExecutor: client} -} - -func (i *instance) ByID(ctx context.Context, id string) (*domain.Instance, error) { - var instance domain.Instance - err := i.QueryExecutor.QueryRow(ctx, `SELECT id, name, created_at, updated_at, deleted_at FROM instances WHERE id = $1`, id).Scan( - &instance.ID, - &instance.Name, - &instance.CreatedAt, - &instance.UpdatedAt, - &instance.DeletedAt, - ) - if err != nil { - return nil, err - } - return &instance, nil -} - -const createInstanceStmt = `INSERT INTO instances (id, name) VALUES ($1, $2) RETURNING created_at, updated_at` - -// Create implements [domain.InstanceRepository]. -func (i *instance) Create(ctx context.Context, instance *domain.Instance) error { - return i.QueryExecutor.QueryRow(ctx, createInstanceStmt, - instance.ID, - instance.Name, - ).Scan( - &instance.CreatedAt, - &instance.UpdatedAt, - ) -} - -// On implements [domain.InstanceRepository]. -func (i *instance) On(id string) domain.InstanceOperation { - return &instanceOperation{ - QueryExecutor: i.QueryExecutor, - id: id, - } -} - -var _ domain.InstanceRepository = (*instance)(nil) diff --git a/backend/v3/storage/database/repository/instance_operation.go b/backend/v3/storage/database/repository/instance_operation.go deleted file mode 100644 index 928c164a3c..0000000000 --- a/backend/v3/storage/database/repository/instance_operation.go +++ /dev/null @@ -1,52 +0,0 @@ -package repository - -import ( - "context" - - "github.com/zitadel/zitadel/backend/v3/domain" - "github.com/zitadel/zitadel/backend/v3/storage/database" -) - -type instanceOperation struct { - database.QueryExecutor - id string -} - -const addInstanceAdminStmt = `INSERT INTO instance_admins (instance_id, user_id, roles) VALUES ($1, $2, $3)` - -// AddAdmin implements [domain.InstanceOperation]. -func (i *instanceOperation) AddAdmin(ctx context.Context, userID string, roles []string) error { - return i.QueryExecutor.Exec(ctx, addInstanceAdminStmt, i.id, userID, roles) -} - -// Delete implements [domain.InstanceOperation]. -func (i *instanceOperation) Delete(ctx context.Context) error { - return i.QueryExecutor.Exec(ctx, `DELETE FROM instances WHERE id = $1`, i.id) -} - -const removeInstanceAdminStmt = `DELETE FROM instance_admins WHERE instance_id = $1 AND user_id = $2` - -// RemoveAdmin implements [domain.InstanceOperation]. -func (i *instanceOperation) RemoveAdmin(ctx context.Context, userID string) error { - return i.QueryExecutor.Exec(ctx, removeInstanceAdminStmt, i.id, userID) -} - -const setInstanceAdminRolesStmt = `UPDATE instance_admins SET roles = $1 WHERE instance_id = $2 AND user_id = $3` - -// SetAdminRoles implements [domain.InstanceOperation]. -func (i *instanceOperation) SetAdminRoles(ctx context.Context, userID string, roles []string) error { - return i.QueryExecutor.Exec(ctx, setInstanceAdminRolesStmt, roles, i.id, userID) -} - -const updateInstanceStmt = `UPDATE instances SET name = $1, updated_at = $2 WHERE id = $3 RETURNING updated_at` - -// Update implements [domain.InstanceOperation]. -func (i *instanceOperation) Update(ctx context.Context, instance *domain.Instance) error { - return i.QueryExecutor.QueryRow(ctx, updateInstanceStmt, - instance.Name, - instance.UpdatedAt, - i.id, - ).Scan(&instance.UpdatedAt) -} - -var _ domain.InstanceOperation = (*instanceOperation)(nil) diff --git a/backend/v3/storage/database/repository/org.go b/backend/v3/storage/database/repository/org.go new file mode 100644 index 0000000000..48df70f867 --- /dev/null +++ b/backend/v3/storage/database/repository/org.go @@ -0,0 +1,136 @@ +package repository + +import ( + "context" + + "github.com/zitadel/zitadel/backend/v3/domain" + "github.com/zitadel/zitadel/backend/v3/storage/database" +) + +// ------------------------------------------------------------- +// repository +// ------------------------------------------------------------- + +type org struct { + repository +} + +func OrgRepository(client database.QueryExecutor) domain.OrgRepository { + return &org{ + repository: repository{ + client: client, + }, + } +} + +// Create implements [domain.OrgRepository]. +func (o *org) Create(ctx context.Context, org *domain.Org) error { + panic("unimplemented") +} + +// Delete implements [domain.OrgRepository]. +func (o *org) Delete(ctx context.Context, condition database.Condition) error { + panic("unimplemented") +} + +// Get implements [domain.OrgRepository]. +func (o *org) Get(ctx context.Context, opts ...database.QueryOption) (*domain.Org, error) { + panic("unimplemented") +} + +// List implements [domain.OrgRepository]. +func (o *org) List(ctx context.Context, opts ...database.QueryOption) ([]*domain.Org, error) { + panic("unimplemented") +} + +// Update implements [domain.OrgRepository]. +func (o *org) Update(ctx context.Context, condition database.Condition, changes ...database.Change) error { + panic("unimplemented") +} + +func (o *org) Member() domain.MemberRepository { + return &orgMember{o} +} + +func (o *org) Domain() domain.DomainRepository { + return &orgDomain{o} +} + +// ------------------------------------------------------------- +// changes +// ------------------------------------------------------------- + +// SetName implements [domain.orgChanges]. +func (o *org) SetName(name string) database.Change { + return database.NewChange(o.NameColumn(), name) +} + +// SetState implements [domain.orgChanges]. +func (o *org) SetState(state domain.OrgState) database.Change { + return database.NewChange(o.StateColumn(), state) +} + +// ------------------------------------------------------------- +// conditions +// ------------------------------------------------------------- + +// IDCondition implements [domain.orgConditions]. +func (o *org) IDCondition(orgID string) database.Condition { + return database.NewTextCondition(o.IDColumn(), database.TextOperationEqual, orgID) +} + +// InstanceIDCondition implements [domain.orgConditions]. +func (o *org) InstanceIDCondition(instanceID string) database.Condition { + return database.NewTextCondition(o.InstanceIDColumn(), database.TextOperationEqual, instanceID) +} + +// NameCondition implements [domain.orgConditions]. +func (o *org) NameCondition(op database.TextOperation, name string) database.Condition { + return database.NewTextCondition(o.NameColumn(), op, name) +} + +// StateCondition implements [domain.orgConditions]. +func (o *org) StateCondition(op database.NumberOperation, state domain.OrgState) database.Condition { + return database.NewNumberCondition(o.StateColumn(), op, state) +} + +// ------------------------------------------------------------- +// columns +// ------------------------------------------------------------- + +// CreatedAtColumn implements [domain.orgColumns]. +func (o *org) CreatedAtColumn() database.Column { + return database.NewColumn("created_at") +} + +// DeletedAtColumn implements [domain.orgColumns]. +func (o *org) DeletedAtColumn() database.Column { + return database.NewColumn("deleted_at") +} + +// IDColumn implements [domain.orgColumns]. +func (o *org) IDColumn() database.Column { + return database.NewColumn("id") +} + +// InstanceIDColumn implements [domain.orgColumns]. +func (o *org) InstanceIDColumn() database.Column { + return database.NewColumn("instance_id") +} + +// NameColumn implements [domain.orgColumns]. +func (o *org) NameColumn() database.Column { + return database.NewColumn("name") +} + +// StateColumn implements [domain.orgColumns]. +func (o *org) StateColumn() database.Column { + return database.NewColumn("state") +} + +// UpdatedAtColumn implements [domain.orgColumns]. +func (o *org) UpdatedAtColumn() database.Column { + return database.NewColumn("updated_at") +} + +var _ domain.OrgRepository = (*org)(nil) diff --git a/backend/v3/storage/database/repository/org_domain.go b/backend/v3/storage/database/repository/org_domain.go new file mode 100644 index 0000000000..e494c6a369 --- /dev/null +++ b/backend/v3/storage/database/repository/org_domain.go @@ -0,0 +1,28 @@ +package repository + +import ( + "context" + + "github.com/zitadel/zitadel/backend/v3/domain" +) + +type orgDomain struct { + *org +} + +// AddDomain implements [domain.DomainRepository]. +func (o *orgDomain) AddDomain(ctx context.Context, domain string) error { + panic("unimplemented") +} + +// RemoveDomain implements [domain.DomainRepository]. +func (o *orgDomain) RemoveDomain(ctx context.Context, domain string) error { + panic("unimplemented") +} + +// SetDomainVerified implements [domain.DomainRepository]. +func (o *orgDomain) SetDomainVerified(ctx context.Context, domain string) error { + panic("unimplemented") +} + +var _ domain.DomainRepository = (*orgDomain)(nil) diff --git a/backend/v3/storage/database/repository/org_member.go b/backend/v3/storage/database/repository/org_member.go new file mode 100644 index 0000000000..08cea52224 --- /dev/null +++ b/backend/v3/storage/database/repository/org_member.go @@ -0,0 +1,28 @@ +package repository + +import ( + "context" + + "github.com/zitadel/zitadel/backend/v3/domain" +) + +type orgMember struct { + *org +} + +// AddMember implements [domain.MemberRepository]. +func (o *orgMember) AddMember(ctx context.Context, orgID string, userID string, roles []string) error { + panic("unimplemented") +} + +// RemoveMember implements [domain.MemberRepository]. +func (o *orgMember) RemoveMember(ctx context.Context, orgID string, userID string) error { + panic("unimplemented") +} + +// SetMemberRoles implements [domain.MemberRepository]. +func (o *orgMember) SetMemberRoles(ctx context.Context, orgID string, userID string, roles []string) error { + panic("unimplemented") +} + +var _ domain.MemberRepository = (*orgMember)(nil) diff --git a/backend/v3/storage/database/repository/query.go b/backend/v3/storage/database/repository/query.go deleted file mode 100644 index fc026bae43..0000000000 --- a/backend/v3/storage/database/repository/query.go +++ /dev/null @@ -1,17 +0,0 @@ -package repository - -import ( - "github.com/zitadel/zitadel/backend/v3/storage/database" -) - -type query struct{ database.Querier } - -func Query(querier database.Querier) *query { - return &query{Querier: querier} -} - -type executor struct{ database.Executor } - -func Execute(exec database.Executor) *executor { - return &executor{Executor: exec} -} diff --git a/backend/v3/storage/database/repository/repository.go b/backend/v3/storage/database/repository/repository.go new file mode 100644 index 0000000000..ebd99a66d6 --- /dev/null +++ b/backend/v3/storage/database/repository/repository.go @@ -0,0 +1,8 @@ +package repository + +import "github.com/zitadel/zitadel/backend/v3/storage/database" + +type repository struct { + builder database.StatementBuilder + client database.QueryExecutor +} diff --git a/backend/v3/storage/database/repository/statement.go b/backend/v3/storage/database/repository/statement.go deleted file mode 100644 index 50138c02b2..0000000000 --- a/backend/v3/storage/database/repository/statement.go +++ /dev/null @@ -1,21 +0,0 @@ -package repository - -import "strings" - -type statement struct { - builder strings.Builder - args []any -} - -func (s *statement) appendArg(arg any) (placeholder string) { - s.args = append(s.args, arg) - return "$" + string(len(s.args)) -} - -func (s *statement) appendArgs(args ...any) (placeholders []string) { - placeholders = make([]string, len(args)) - for i, arg := range args { - placeholders[i] = s.appendArg(arg) - } - return placeholders -} diff --git a/backend/v3/storage/database/repository/stmt/column.go b/backend/v3/storage/database/repository/stmt/column.go deleted file mode 100644 index c11b2b256e..0000000000 --- a/backend/v3/storage/database/repository/stmt/column.go +++ /dev/null @@ -1,43 +0,0 @@ -package stmt - -import "fmt" - -type Column[T any] interface { - fmt.Stringer - statementApplier[T] - scanner(t *T) any -} - -type columnDescriptor[T any] struct { - name string - scan func(*T) any -} - -func (cd columnDescriptor[T]) scanner(t *T) any { - return cd.scan(t) -} - -// Apply implements [Column]. -func (f columnDescriptor[T]) Apply(stmt *statement[T]) { - stmt.builder.WriteString(stmt.columnPrefix()) - stmt.builder.WriteString(f.String()) -} - -// String implements [Column]. -func (f columnDescriptor[T]) String() string { - return f.name -} - -var _ Column[any] = (*columnDescriptor[any])(nil) - -type ignoreCaseColumnDescriptor[T any] struct { - columnDescriptor[T] - fieldNameSuffix string -} - -func (f ignoreCaseColumnDescriptor[T]) ApplyIgnoreCase(stmt *statement[T]) { - stmt.builder.WriteString(f.String()) - stmt.builder.WriteString(f.fieldNameSuffix) -} - -var _ Column[any] = (*ignoreCaseColumnDescriptor[any])(nil) diff --git a/backend/v3/storage/database/repository/stmt/condition.go b/backend/v3/storage/database/repository/stmt/condition.go deleted file mode 100644 index bce0ca1b44..0000000000 --- a/backend/v3/storage/database/repository/stmt/condition.go +++ /dev/null @@ -1,97 +0,0 @@ -package stmt - -import "fmt" - -type statementApplier[T any] interface { - // Apply writes the statement to the builder. - Apply(stmt *statement[T]) -} - -type Condition[T any] interface { - statementApplier[T] -} - -type op interface { - TextOperation | NumberOperation | ListOperation - fmt.Stringer -} - -type operation[T any, O op] struct { - o O -} - -func (o operation[T, O]) String() string { - return o.o.String() -} - -func (o operation[T, O]) Apply(stmt *statement[T]) { - stmt.builder.WriteString(o.o.String()) -} - -type condition[V, T any, OP op] struct { - field Column[T] - op OP - value V -} - -func (c *condition[V, T, OP]) Apply(stmt *statement[T]) { - // placeholder := stmt.appendArg(c.value) - stmt.builder.WriteString(stmt.columnPrefix()) - stmt.builder.WriteString(c.field.String()) - // stmt.builder.WriteString(c.op) - // stmt.builder.WriteString(placeholder) -} - -type and[T any] struct { - conditions []Condition[T] -} - -func And[T any](conditions ...Condition[T]) *and[T] { - return &and[T]{ - conditions: conditions, - } -} - -// Apply implements [Condition]. -func (a *and[T]) Apply(stmt *statement[T]) { - if len(a.conditions) > 1 { - stmt.builder.WriteString("(") - defer stmt.builder.WriteString(")") - } - - for i, condition := range a.conditions { - if i > 0 { - stmt.builder.WriteString(" AND ") - } - condition.Apply(stmt) - } -} - -var _ Condition[any] = (*and[any])(nil) - -type or[T any] struct { - conditions []Condition[T] -} - -func Or[T any](conditions ...Condition[T]) *or[T] { - return &or[T]{ - conditions: conditions, - } -} - -// Apply implements [Condition]. -func (o *or[T]) Apply(stmt *statement[T]) { - if len(o.conditions) > 1 { - stmt.builder.WriteString("(") - defer stmt.builder.WriteString(")") - } - - for i, condition := range o.conditions { - if i > 0 { - stmt.builder.WriteString(" OR ") - } - condition.Apply(stmt) - } -} - -var _ Condition[any] = (*or[any])(nil) diff --git a/backend/v3/storage/database/repository/stmt/list.go b/backend/v3/storage/database/repository/stmt/list.go deleted file mode 100644 index 90114ace73..0000000000 --- a/backend/v3/storage/database/repository/stmt/list.go +++ /dev/null @@ -1,71 +0,0 @@ -package stmt - -type ListEntry interface { - Number | Text | any -} - -type ListCondition[E ListEntry, T any] struct { - condition[[]E, T, ListOperation] -} - -func (lc *ListCondition[E, T]) Apply(stmt *statement[T]) { - placeholder := stmt.appendArg(lc.value) - - switch lc.op { - case ListOperationEqual, ListOperationNotEqual: - lc.field.Apply(stmt) - operation[T, ListOperation]{lc.op}.Apply(stmt) - stmt.builder.WriteString(placeholder) - case ListOperationContainsAny, ListOperationContainsAll: - lc.field.Apply(stmt) - operation[T, ListOperation]{lc.op}.Apply(stmt) - stmt.builder.WriteString(placeholder) - case ListOperationNotContainsAny, ListOperationNotContainsAll: - stmt.builder.WriteString("NOT (") - lc.field.Apply(stmt) - operation[T, ListOperation]{lc.op}.Apply(stmt) - stmt.builder.WriteString(placeholder) - stmt.builder.WriteString(")") - default: - panic("unknown list operation") - } -} - -type ListOperation uint8 - -const ( - // ListOperationEqual checks if the arrays are equal including the order of the elements - ListOperationEqual ListOperation = iota + 1 - // ListOperationNotEqual checks if the arrays are not equal including the order of the elements - ListOperationNotEqual - - // ListOperationContains checks if the array column contains all the values of the specified array - ListOperationContainsAll - // ListOperationContainsAny checks if the arrays have at least one value in common - ListOperationContainsAny - // ListOperationContainsAll checks if the array column contains all the values of the specified array - - // ListOperationNotContainsAll checks if the specified array is not contained by the column - ListOperationNotContainsAll - // ListOperationNotContainsAny checks if the arrays column contains none of the values of the specified array - ListOperationNotContainsAny -) - -var listOperations = map[ListOperation]string{ - // ListOperationEqual checks if the lists are equal - ListOperationEqual: " = ", - // ListOperationNotEqual checks if the lists are not equal - ListOperationNotEqual: " <> ", - // ListOperationContainsAny checks if the arrays have at least one value in common - ListOperationContainsAny: " && ", - // ListOperationContainsAll checks if the array column contains all the values of the specified array - ListOperationContainsAll: " @> ", - // ListOperationNotContainsAny checks if the arrays column contains none of the values of the specified array - ListOperationNotContainsAny: " && ", // Base operator for NOT (A && B) - // ListOperationNotContainsAll checks if the array column is not contained by the specified array - ListOperationNotContainsAll: " <@ ", // Base operator for NOT (A <@ B) -} - -func (lo ListOperation) String() string { - return listOperations[lo] -} diff --git a/backend/v3/storage/database/repository/stmt/number.go b/backend/v3/storage/database/repository/stmt/number.go deleted file mode 100644 index 9dfb6e44bf..0000000000 --- a/backend/v3/storage/database/repository/stmt/number.go +++ /dev/null @@ -1,61 +0,0 @@ -package stmt - -import ( - "time" - - "golang.org/x/exp/constraints" -) - -type Number interface { - constraints.Integer | constraints.Float | constraints.Complex | time.Time | time.Duration -} - -type between[N Number] struct { - min, max N -} - -type NumberBetween[V Number, T any] struct { - condition[between[V], T, NumberOperation] -} - -func (nb *NumberBetween[V, T]) Apply(stmt *statement[T]) { - nb.field.Apply(stmt) - stmt.builder.WriteString(" BETWEEN ") - stmt.builder.WriteString(stmt.appendArg(nb.value.min)) - stmt.builder.WriteString(" AND ") - stmt.builder.WriteString(stmt.appendArg(nb.value.max)) -} - -type NumberCondition[V Number, T any] struct { - condition[V, T, NumberOperation] -} - -func (nc *NumberCondition[V, T]) Apply(stmt *statement[T]) { - nc.field.Apply(stmt) - operation[T, NumberOperation]{nc.op}.Apply(stmt) - stmt.builder.WriteString(stmt.appendArg(nc.value)) -} - -type NumberOperation uint8 - -const ( - NumberOperationEqual NumberOperation = iota + 1 - NumberOperationNotEqual - NumberOperationLessThan - NumberOperationLessThanOrEqual - NumberOperationGreaterThan - NumberOperationGreaterThanOrEqual -) - -var numberOperations = map[NumberOperation]string{ - NumberOperationEqual: " = ", - NumberOperationNotEqual: " <> ", - NumberOperationLessThan: " < ", - NumberOperationLessThanOrEqual: " <= ", - NumberOperationGreaterThan: " > ", - NumberOperationGreaterThanOrEqual: " >= ", -} - -func (no NumberOperation) String() string { - return numberOperations[no] -} diff --git a/backend/v3/storage/database/repository/stmt/statement.go b/backend/v3/storage/database/repository/stmt/statement.go deleted file mode 100644 index edc0b79967..0000000000 --- a/backend/v3/storage/database/repository/stmt/statement.go +++ /dev/null @@ -1,104 +0,0 @@ -package stmt - -import ( - "fmt" - "strings" - - "github.com/zitadel/zitadel/backend/v3/storage/database" -) - -type statement[T any] struct { - builder strings.Builder - client database.QueryExecutor - - columns []Column[T] - - schema string - table string - alias string - - condition Condition[T] - - limit uint32 - offset uint32 - // order by fieldname and sort direction false for asc true for desc - // orderBy SortingColumns[C] - args []any - existingArgs map[any]string -} - -func (s *statement[T]) scanners(t *T) []any { - scanners := make([]any, len(s.columns)) - for i, column := range s.columns { - scanners[i] = column.scanner(t) - } - return scanners -} - -func (s *statement[T]) query() string { - s.builder.WriteString(`SELECT `) - for i, column := range s.columns { - if i > 0 { - s.builder.WriteString(", ") - } - column.Apply(s) - } - s.builder.WriteString(` FROM `) - s.builder.WriteString(s.schema) - s.builder.WriteRune('.') - s.builder.WriteString(s.table) - if s.alias != "" { - s.builder.WriteString(" AS ") - s.builder.WriteString(s.alias) - } - - s.builder.WriteString(` WHERE `) - - s.condition.Apply(s) - - if s.limit > 0 { - s.builder.WriteString(` LIMIT `) - s.builder.WriteString(s.appendArg(s.limit)) - } - if s.offset > 0 { - s.builder.WriteString(` OFFSET `) - s.builder.WriteString(s.appendArg(s.offset)) - } - - return s.builder.String() -} - -// func (s *statement[T]) Where(condition Condition[T]) *statement[T] { -// s.condition = condition -// return s -// } - -// func (s *statement[T]) Limit(limit uint32) *statement[T] { -// s.limit = limit -// return s -// } - -// func (s *statement[T]) Offset(offset uint32) *statement[T] { -// s.offset = offset -// return s -// } - -func (s *statement[T]) columnPrefix() string { - if s.alias != "" { - return s.alias + "." - } - return s.schema + "." + s.table + "." -} - -func (s *statement[T]) appendArg(arg any) string { - if s.existingArgs == nil { - s.existingArgs = make(map[any]string) - } - if existing, ok := s.existingArgs[arg]; ok { - return existing - } - s.args = append(s.args, arg) - placeholder := fmt.Sprintf("$%d", len(s.args)) - s.existingArgs[arg] = placeholder - return placeholder -} diff --git a/backend/v3/storage/database/repository/stmt/stmt_test.go b/backend/v3/storage/database/repository/stmt/stmt_test.go deleted file mode 100644 index 2956a3dd10..0000000000 --- a/backend/v3/storage/database/repository/stmt/stmt_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package stmt_test - -import ( - "context" - "testing" - - "github.com/zitadel/zitadel/backend/v3/storage/database/repository/stmt" -) - -func Test_Bla(t *testing.T) { - stmt.User(nil).Where( - stmt.Or( - stmt.UserIDCondition("123"), - stmt.UserIDCondition("123"), - stmt.UserUsernameCondition(stmt.TextOperationEqualIgnoreCase, "test"), - ), - ).Limit(1).Offset(1).Get(context.Background()) -} diff --git a/backend/v3/storage/database/repository/stmt/text.go b/backend/v3/storage/database/repository/stmt/text.go deleted file mode 100644 index da1b00abc0..0000000000 --- a/backend/v3/storage/database/repository/stmt/text.go +++ /dev/null @@ -1,72 +0,0 @@ -package stmt - -type Text interface { - ~string | ~[]byte -} - -type TextCondition[V Text, T any] struct { - condition[V, T, TextOperation] -} - -func (tc *TextCondition[V, T]) Apply(stmt *statement[T]) { - placeholder := stmt.appendArg(tc.value) - - switch tc.op { - case TextOperationEqual, TextOperationNotEqual: - tc.field.Apply(stmt) - operation[T, TextOperation]{tc.op}.Apply(stmt) - stmt.builder.WriteString(placeholder) - case TextOperationEqualIgnoreCase: - if desc, ok := tc.field.(ignoreCaseColumnDescriptor[T]); ok { - desc.ApplyIgnoreCase(stmt) - } else { - stmt.builder.WriteString("LOWER(") - tc.field.Apply(stmt) - stmt.builder.WriteString(")") - } - operation[T, TextOperation]{tc.op}.Apply(stmt) - stmt.builder.WriteString("LOWER(") - stmt.builder.WriteString(placeholder) - stmt.builder.WriteString(")") - case TextOperationStartsWith: - tc.field.Apply(stmt) - operation[T, TextOperation]{tc.op}.Apply(stmt) - stmt.builder.WriteString(placeholder) - stmt.builder.WriteString("|| '%'") - case TextOperationStartsWithIgnoreCase: - if desc, ok := tc.field.(ignoreCaseColumnDescriptor[T]); ok { - desc.ApplyIgnoreCase(stmt) - } else { - stmt.builder.WriteString("LOWER(") - tc.field.Apply(stmt) - stmt.builder.WriteString(")") - } - operation[T, TextOperation]{tc.op}.Apply(stmt) - stmt.builder.WriteString("LOWER(") - stmt.builder.WriteString(placeholder) - stmt.builder.WriteString(")") - stmt.builder.WriteString("|| '%'") - } -} - -type TextOperation uint8 - -const ( - TextOperationEqual TextOperation = iota + 1 - TextOperationEqualIgnoreCase - TextOperationNotEqual - TextOperationStartsWith - TextOperationStartsWithIgnoreCase -) - -var textOperations = map[TextOperation]string{ - TextOperationEqual: " = ", - TextOperationEqualIgnoreCase: " = ", - TextOperationNotEqual: " <> ", - TextOperationStartsWith: " LIKE ", - TextOperationStartsWithIgnoreCase: " LIKE ", -} - -func (to TextOperation) String() string { - return textOperations[to] -} diff --git a/backend/v3/storage/database/repository/stmt/user.go b/backend/v3/storage/database/repository/stmt/user.go deleted file mode 100644 index e0f8d388e0..0000000000 --- a/backend/v3/storage/database/repository/stmt/user.go +++ /dev/null @@ -1,193 +0,0 @@ -package stmt - -import ( - "context" - - "github.com/zitadel/zitadel/backend/v3/domain" - "github.com/zitadel/zitadel/backend/v3/storage/database" -) - -type userStatement struct { - statement[domain.User] -} - -func User(client database.QueryExecutor) *userStatement { - return &userStatement{ - statement: statement[domain.User]{ - schema: "zitadel", - table: "users", - alias: "u", - client: client, - columns: []Column[domain.User]{ - userColumns[UserInstanceID], - userColumns[UserOrgID], - userColumns[UserColumnID], - userColumns[UserColumnUsername], - userColumns[UserCreatedAt], - userColumns[UserUpdatedAt], - userColumns[UserDeletedAt], - }, - }, - } -} - -func (s *userStatement) Where(condition Condition[domain.User]) *userStatement { - s.condition = condition - return s -} - -func (s *userStatement) Limit(limit uint32) *userStatement { - s.limit = limit - return s -} - -func (s *userStatement) Offset(offset uint32) *userStatement { - s.offset = offset - return s -} - -func (s *userStatement) Get(ctx context.Context) (*domain.User, error) { - var user domain.User - err := s.client.QueryRow(ctx, s.query(), s.statement.args...).Scan(s.scanners(&user)...) - - if err != nil { - return nil, err - } - - return &user, nil -} - -func (s *userStatement) List(ctx context.Context) ([]*domain.User, error) { - var users []*domain.User - rows, err := s.client.Query(ctx, s.query(), s.statement.args...) - if err != nil { - return nil, err - } - defer rows.Close() - - for rows.Next() { - var user domain.User - err = rows.Scan(s.scanners(&user)...) - if err != nil { - return nil, err - } - users = append(users, &user) - } - - return users, nil -} - -func (s *userStatement) SetUsername(ctx context.Context, username string) error { - return nil -} - -type UserColumn uint8 - -var ( - userColumns map[UserColumn]Column[domain.User] = map[UserColumn]Column[domain.User]{ - UserInstanceID: columnDescriptor[domain.User]{ - name: "instance_id", - scan: func(u *domain.User) any { - return &u.InstanceID - }, - }, - UserOrgID: columnDescriptor[domain.User]{ - name: "org_id", - scan: func(u *domain.User) any { - return &u.OrgID - }, - }, - UserColumnID: columnDescriptor[domain.User]{ - name: "id", - scan: func(u *domain.User) any { - return &u.ID - }, - }, - UserColumnUsername: ignoreCaseColumnDescriptor[domain.User]{ - columnDescriptor: columnDescriptor[domain.User]{ - name: "username", - scan: func(u *domain.User) any { - return &u.Username - }, - }, - fieldNameSuffix: "_lower", - }, - UserCreatedAt: columnDescriptor[domain.User]{ - name: "created_at", - scan: func(u *domain.User) any { - return &u.CreatedAt - }, - }, - UserUpdatedAt: columnDescriptor[domain.User]{ - name: "updated_at", - scan: func(u *domain.User) any { - return &u.UpdatedAt - }, - }, - UserDeletedAt: columnDescriptor[domain.User]{ - name: "deleted_at", - scan: func(u *domain.User) any { - return &u.DeletedAt - }, - }, - } - humanColumns = map[UserColumn]Column[domain.User]{ - UserHumanColumnEmail: ignoreCaseColumnDescriptor[domain.User]{ - columnDescriptor: columnDescriptor[domain.User]{ - name: "email", - scan: func(u *domain.User) any { - human, ok := u.Traits.(*domain.Human) - if !ok { - return nil - } - if human.Email == nil { - human.Email = new(domain.Email) - } - return &human.Email.Address - }, - }, - fieldNameSuffix: "_lower", - }, - UserHumanColumnEmailVerified: columnDescriptor[domain.User]{ - name: "email_is_verified", - scan: func(u *domain.User) any { - human, ok := u.Traits.(*domain.Human) - if !ok { - return nil - } - if human.Email == nil { - human.Email = new(domain.Email) - } - return &human.Email.IsVerified - }, - }, - } - machineColumns = map[UserColumn]Column[domain.User]{ - UserMachineDescription: columnDescriptor[domain.User]{ - name: "description", - scan: func(u *domain.User) any { - machine, ok := u.Traits.(*domain.Machine) - if !ok { - return nil - } - if machine == nil { - machine = new(domain.Machine) - } - return &machine.Description - }, - }, - } -) - -const ( - UserInstanceID UserColumn = iota + 1 - UserOrgID - UserColumnID - UserColumnUsername - UserHumanColumnEmail - UserHumanColumnEmailVerified - UserMachineDescription - UserCreatedAt - UserUpdatedAt - UserDeletedAt -) diff --git a/backend/v3/storage/database/repository/stmt/user_condition.go b/backend/v3/storage/database/repository/stmt/user_condition.go deleted file mode 100644 index ba138efe6b..0000000000 --- a/backend/v3/storage/database/repository/stmt/user_condition.go +++ /dev/null @@ -1,23 +0,0 @@ -package stmt - -import "github.com/zitadel/zitadel/backend/v3/domain" - -func UserIDCondition(id string) *TextCondition[string, domain.User] { - return &TextCondition[string, domain.User]{ - condition: condition[string, domain.User, TextOperation]{ - field: userColumns[UserColumnID], - op: TextOperationEqual, - value: id, - }, - } -} - -func UserUsernameCondition(op TextOperation, username string) *TextCondition[string, domain.User] { - return &TextCondition[string, domain.User]{ - condition: condition[string, domain.User, TextOperation]{ - field: userColumns[UserColumnUsername], - op: op, - value: username, - }, - } -} diff --git a/backend/v3/storage/database/repository/stmt/v2/table.go b/backend/v3/storage/database/repository/stmt/v2/table.go deleted file mode 100644 index 0efc396fe6..0000000000 --- a/backend/v3/storage/database/repository/stmt/v2/table.go +++ /dev/null @@ -1,135 +0,0 @@ -package stmt - -// type table struct { -// schema string -// name string - -// possibleJoins []*join - -// columns []*col -// } - -// type col struct { -// *table - -// name string -// } - -// type join struct { -// *table - -// on []*joinColumns -// } - -// type joinColumns struct { -// left, right *col -// } - -// var ( -// userTable = &table{ -// schema: "zitadel", -// name: "users", -// } -// userColumns = []*col{ -// userInstanceIDColumn, -// userOrgIDColumn, -// userIDColumn, -// userUsernameColumn, -// } -// userInstanceIDColumn = &col{ -// table: userTable, -// name: "instance_id", -// } -// userOrgIDColumn = &col{ -// table: userTable, -// name: "org_id", -// } -// userIDColumn = &col{ -// table: userTable, -// name: "id", -// } -// userUsernameColumn = &col{ -// table: userTable, -// name: "username", -// } -// userJoins = []*join{ -// { -// table: instanceTable, -// on: []*joinColumns{ -// { -// left: instanceIDColumn, -// right: userInstanceIDColumn, -// }, -// }, -// }, -// { -// table: orgTable, -// on: []*joinColumns{ -// { -// left: orgIDColumn, -// right: userOrgIDColumn, -// }, -// }, -// }, -// } -// ) - -// var ( -// instanceTable = &table{ -// schema: "zitadel", -// name: "instances", -// } -// instanceColumns = []*col{ -// instanceIDColumn, -// instanceNameColumn, -// } -// instanceIDColumn = &col{ -// table: instanceTable, -// name: "id", -// } -// instanceNameColumn = &col{ -// table: instanceTable, -// name: "name", -// } -// ) - -// var ( -// orgTable = &table{ -// schema: "zitadel", -// name: "orgs", -// } -// orgColumns = []*col{ -// orgInstanceIDColumn, -// orgIDColumn, -// orgNameColumn, -// } -// orgInstanceIDColumn = &col{ -// table: orgTable, -// name: "instance_id", -// } -// orgIDColumn = &col{ -// table: orgTable, -// name: "id", -// } -// orgNameColumn = &col{ -// table: orgTable, -// name: "name", -// } -// ) - -// func init() { -// instanceTable.columns = instanceColumns -// userTable.columns = userColumns - -// userTable.possibleJoins = []join{ -// { -// table: userTable, -// on: []joinColumns{ -// { -// left: userIDColumn, -// right: userIDColumn, -// }, -// }, -// }, -// } -// } diff --git a/backend/v3/storage/database/repository/stmt/v3/column.go b/backend/v3/storage/database/repository/stmt/v3/column.go deleted file mode 100644 index 60ba0e6750..0000000000 --- a/backend/v3/storage/database/repository/stmt/v3/column.go +++ /dev/null @@ -1,55 +0,0 @@ -package v3 - -type Column interface { - Name() string - Write(builder statementBuilder) -} - -type ignoreCaseColumn interface { - Column - WriteIgnoreCase(builder statementBuilder) -} - -var ( - columnNameID = "id" - columnNameName = "name" - columnNameCreatedAt = "created_at" - columnNameUpdatedAt = "updated_at" - columnNameDeletedAt = "deleted_at" - - columnNameInstanceID = "instance_id" - - columnNameOrgID = "org_id" -) - -type column struct { - table Table - name string -} - -// Write implements Column. -func (c *column) Write(builder statementBuilder) { - c.table.writeOn(builder) - builder.writeRune('.') - builder.writeString(c.name) -} - -// Name implements [Column]. -func (c *column) Name() string { - return c.name -} - -var _ Column = (*column)(nil) - -type columnIgnoreCase struct { - column - suffix string -} - -// WriteIgnoreCase implements ignoreCaseColumn. -func (c *columnIgnoreCase) WriteIgnoreCase(builder statementBuilder) { - c.Write(builder) - builder.writeString(c.suffix) -} - -var _ ignoreCaseColumn = (*columnIgnoreCase)(nil) diff --git a/backend/v3/storage/database/repository/stmt/v3/condition.go b/backend/v3/storage/database/repository/stmt/v3/condition.go deleted file mode 100644 index 1766242b89..0000000000 --- a/backend/v3/storage/database/repository/stmt/v3/condition.go +++ /dev/null @@ -1,182 +0,0 @@ -package v3 - -type statementBuilder interface { - write([]byte) - writeString(string) - writeRune(rune) - - appendArg(any) (placeholder string) - table() Table -} - -type Condition interface { - writeOn(builder statementBuilder) -} - -type and struct { - conditions []Condition -} - -func And(conditions ...Condition) *and { - return &and{conditions: conditions} -} - -// writeOn implements [Condition]. -func (a *and) writeOn(builder statementBuilder) { - if len(a.conditions) > 1 { - builder.writeString("(") - defer builder.writeString(")") - } - - for i, condition := range a.conditions { - if i > 0 { - builder.writeString(" AND ") - } - condition.writeOn(builder) - } -} - -var _ Condition = (*and)(nil) - -type or struct { - conditions []Condition -} - -func Or(conditions ...Condition) *or { - return &or{conditions: conditions} -} - -// writeOn implements [Condition]. -func (o *or) writeOn(builder statementBuilder) { - if len(o.conditions) > 1 { - builder.writeString("(") - defer builder.writeString(")") - } - - for i, condition := range o.conditions { - if i > 0 { - builder.writeString(" OR ") - } - condition.writeOn(builder) - } -} - -var _ Condition = (*or)(nil) - -type isNull struct { - column Column -} - -func IsNull(column Column) *isNull { - return &isNull{column: column} -} - -// writeOn implements [Condition]. -func (cond *isNull) writeOn(builder statementBuilder) { - cond.column.Write(builder) - builder.writeString(" IS NULL") -} - -var _ Condition = (*isNull)(nil) - -type isNotNull struct { - column Column -} - -func IsNotNull(column Column) *isNotNull { - return &isNotNull{column: column} -} - -// writeOn implements [Condition]. -func (cond *isNotNull) writeOn(builder statementBuilder) { - cond.column.Write(builder) - builder.writeString(" IS NOT NULL") -} - -var _ Condition = (*isNotNull)(nil) - -type condition[Op Operator, V Value] struct { - column Column - operator Op - value V -} - -// writeOn implements [Condition]. -func (cond condition[Op, V]) writeOn(builder statementBuilder) { - cond.column.Write(builder) - builder.writeString(cond.operator.String()) - builder.writeString(builder.appendArg(cond.value)) -} - -var _ Condition = (*condition[TextOperator, string])(nil) - -type textCondition[V Text] struct { - condition[TextOperator, V] -} - -func NewTextCondition[V Text](column Column, operator TextOperator, value V) *textCondition[V] { - return &textCondition[V]{ - condition: condition[TextOperator, V]{ - column: column, - operator: operator, - value: value, - }, - } -} - -// writeOn implements [Condition]. -func (cond *textCondition[V]) writeOn(builder statementBuilder) { - switch cond.operator { - case TextOperatorEqual, TextOperatorNotEqual: - cond.column.Write(builder) - builder.writeString(cond.operator.String()) - builder.writeString(builder.appendArg(cond.value)) - case TextOperatorEqualIgnoreCase, TextOperatorNotEqualIgnoreCase: - if col, ok := cond.column.(ignoreCaseColumn); ok { - col.WriteIgnoreCase(builder) - } else { - builder.writeString("LOWER(") - cond.column.Write(builder) - builder.writeString(")") - } - builder.writeString(cond.operator.String()) - builder.writeString("LOWER(") - builder.writeString(builder.appendArg(cond.value)) - builder.writeString(")") - case TextOperatorStartsWith: - cond.column.Write(builder) - builder.writeString(cond.operator.String()) - builder.writeString(builder.appendArg(cond.value)) - builder.writeString(" || '%'") - case TextOperatorStartsWithIgnoreCase: - if col, ok := cond.column.(ignoreCaseColumn); ok { - col.WriteIgnoreCase(builder) - } else { - builder.writeString("LOWER(") - cond.column.Write(builder) - builder.writeString(")") - } - builder.writeString(cond.operator.String()) - builder.writeString("LOWER(") - builder.writeString(builder.appendArg(cond.value)) - builder.writeString(") || '%'") - } -} - -var _ Condition = (*textCondition[string])(nil) - -type numberCondition[V Number] struct { - condition[NumberOperator, V] -} - -func NewNumberCondition[V Number](column Column, operator NumberOperator, value V) *numberCondition[V] { - return &numberCondition[V]{ - condition: condition[NumberOperator, V]{ - column: column, - operator: operator, - value: value, - }, - } -} - -var _ Condition = (*numberCondition[int])(nil) diff --git a/backend/v3/storage/database/repository/stmt/v3/instance.go b/backend/v3/storage/database/repository/stmt/v3/instance.go deleted file mode 100644 index 7967d4f788..0000000000 --- a/backend/v3/storage/database/repository/stmt/v3/instance.go +++ /dev/null @@ -1,104 +0,0 @@ -package v3 - -import ( - "time" - - "github.com/zitadel/zitadel/backend/v3/storage/database" -) - -type Instance struct { - id string - name string - - createdAt time.Time - updatedAt time.Time - deletedAt time.Time -} - -// Columns implements [object]. -func (Instance) Columns(table Table) []Column { - return []Column{ - &column{ - table: table, - name: columnNameID, - }, - &column{ - table: table, - name: columnNameName, - }, - &column{ - table: table, - name: columnNameCreatedAt, - }, - &column{ - table: table, - name: columnNameUpdatedAt, - }, - &column{ - table: table, - name: columnNameDeletedAt, - }, - } -} - -// Scan implements [object]. -func (i Instance) Scan(row database.Scanner) error { - return row.Scan( - &i.id, - &i.name, - &i.createdAt, - &i.updatedAt, - &i.deletedAt, - ) -} - -type instanceTable struct { - *table -} - -func InstanceTable() *instanceTable { - table := &instanceTable{ - table: newTable[Instance]("zitadel", "instances"), - } - - table.possibleJoins = func(t Table) map[string]Column { - switch on := t.(type) { - case *instanceTable: - return map[string]Column{ - columnNameID: on.IDColumn(), - } - case *orgTable: - return map[string]Column{ - columnNameID: on.InstanceIDColumn(), - } - case *userTable: - return map[string]Column{ - columnNameID: on.InstanceIDColumn(), - } - default: - return nil - } - } - - return table -} - -func (i *instanceTable) IDColumn() Column { - return i.columns[columnNameID] -} - -func (i *instanceTable) NameColumn() Column { - return i.columns[columnNameName] -} - -func (i *instanceTable) CreatedAtColumn() Column { - return i.columns[columnNameCreatedAt] -} - -func (i *instanceTable) UpdatedAtColumn() Column { - return i.columns[columnNameUpdatedAt] -} - -func (i *instanceTable) DeletedAtColumn() Column { - return i.columns[columnNameDeletedAt] -} diff --git a/backend/v3/storage/database/repository/stmt/v3/join.go b/backend/v3/storage/database/repository/stmt/v3/join.go deleted file mode 100644 index e35948cab4..0000000000 --- a/backend/v3/storage/database/repository/stmt/v3/join.go +++ /dev/null @@ -1,11 +0,0 @@ -package v3 - -type join struct { - table Table - conditions []joinCondition -} - -type joinCondition struct { - left Column - right Column -} diff --git a/backend/v3/storage/database/repository/stmt/v3/operator.go b/backend/v3/storage/database/repository/stmt/v3/operator.go deleted file mode 100644 index e9c1ff9c9f..0000000000 --- a/backend/v3/storage/database/repository/stmt/v3/operator.go +++ /dev/null @@ -1,82 +0,0 @@ -package v3 - -import ( - "fmt" - "time" - - "golang.org/x/exp/constraints" -) - -type Value interface { - Bool | Number | Text -} - -type Text interface { - ~string | ~[]byte -} - -type Number interface { - constraints.Integer | constraints.Float | constraints.Complex | time.Time | time.Duration -} - -type Bool interface { - ~bool -} - -type Operator interface { - fmt.Stringer -} - -type TextOperator uint8 - -// String implements [Operator]. -func (t TextOperator) String() string { - return textOperators[t] -} - -const ( - TextOperatorEqual TextOperator = iota + 1 - TextOperatorEqualIgnoreCase - TextOperatorNotEqual - TextOperatorNotEqualIgnoreCase - TextOperatorStartsWith - TextOperatorStartsWithIgnoreCase -) - -var textOperators = map[TextOperator]string{ - TextOperatorEqual: " = ", - TextOperatorEqualIgnoreCase: " LIKE ", - TextOperatorNotEqual: " <> ", - TextOperatorNotEqualIgnoreCase: " NOT LIKE ", - TextOperatorStartsWith: " LIKE ", - TextOperatorStartsWithIgnoreCase: " LIKE ", -} - -var _ Operator = TextOperator(0) - -type NumberOperator uint8 - -// String implements Operator. -func (n NumberOperator) String() string { - return numberOperators[n] -} - -const ( - NumberOperatorEqual NumberOperator = iota + 1 - NumberOperatorNotEqual - NumberOperatorLessThan - NumberOperatorLessThanOrEqual - NumberOperatorGreaterThan - NumberOperatorGreaterThanOrEqual -) - -var numberOperators = map[NumberOperator]string{ - NumberOperatorEqual: " = ", - NumberOperatorNotEqual: " <> ", - NumberOperatorLessThan: " < ", - NumberOperatorLessThanOrEqual: " <= ", - NumberOperatorGreaterThan: " > ", - NumberOperatorGreaterThanOrEqual: " >= ", -} - -var _ Operator = NumberOperator(0) diff --git a/backend/v3/storage/database/repository/stmt/v3/org.go b/backend/v3/storage/database/repository/stmt/v3/org.go deleted file mode 100644 index 27926ed7c7..0000000000 --- a/backend/v3/storage/database/repository/stmt/v3/org.go +++ /dev/null @@ -1,117 +0,0 @@ -package v3 - -import ( - "time" - - "github.com/zitadel/zitadel/backend/v3/storage/database" -) - -type Org struct { - instanceID string - id string - - name string - - createdAt time.Time - updatedAt time.Time - deletedAt time.Time -} - -// Columns implements [object]. -func (Org) Columns(table Table) []Column { - return []Column{ - &column{ - table: table, - name: columnNameInstanceID, - }, - &column{ - table: table, - name: columnNameID, - }, - &column{ - table: table, - name: columnNameName, - }, - &column{ - table: table, - name: columnNameCreatedAt, - }, - &column{ - table: table, - name: columnNameUpdatedAt, - }, - &column{ - table: table, - name: columnNameDeletedAt, - }, - } -} - -// Scan implements [object]. -func (o Org) Scan(row database.Scanner) error { - return row.Scan( - &o.instanceID, - &o.id, - &o.name, - &o.createdAt, - &o.updatedAt, - &o.deletedAt, - ) -} - -type orgTable struct { - *table -} - -func OrgTable() *orgTable { - table := &orgTable{ - table: newTable[Org]("zitadel", "orgs"), - } - - table.possibleJoins = func(table Table) map[string]Column { - switch on := table.(type) { - case *instanceTable: - return map[string]Column{ - columnNameInstanceID: on.IDColumn(), - } - case *orgTable: - return map[string]Column{ - columnNameInstanceID: on.InstanceIDColumn(), - columnNameID: on.IDColumn(), - } - case *userTable: - return map[string]Column{ - columnNameInstanceID: on.InstanceIDColumn(), - columnNameID: on.IDColumn(), - } - default: - return nil - } - } - - return table -} - -func (o *orgTable) InstanceIDColumn() Column { - return o.columns[columnNameInstanceID] -} - -func (o *orgTable) IDColumn() Column { - return o.columns[columnNameID] -} - -func (o *orgTable) NameColumn() Column { - return o.columns[columnNameName] -} - -func (o *orgTable) CreatedAtColumn() Column { - return o.columns[columnNameCreatedAt] -} - -func (o *orgTable) UpdatedAtColumn() Column { - return o.columns[columnNameUpdatedAt] -} - -func (o *orgTable) DeletedAtColumn() Column { - return o.columns[columnNameDeletedAt] -} diff --git a/backend/v3/storage/database/repository/stmt/v3/query.go b/backend/v3/storage/database/repository/stmt/v3/query.go deleted file mode 100644 index 4d1ada6a68..0000000000 --- a/backend/v3/storage/database/repository/stmt/v3/query.go +++ /dev/null @@ -1,188 +0,0 @@ -package v3 - -import ( - "context" - "fmt" - - "github.com/zitadel/zitadel/backend/v3/storage/database" -) - -type Query[O object] interface { - Where(condition Condition) - Join(tables ...Table) - Limit(limit uint32) - Offset(offset uint32) - OrderBy(columns ...Column) - - Result(ctx context.Context, client database.Querier) (*O, error) - Results(ctx context.Context, client database.Querier) ([]O, error) - - fmt.Stringer - statementBuilder -} - -type query[O object] struct { - *statement[O] - joins []join - limit uint32 - offset uint32 - orderBy []Column -} - -func NewQuery[O object](table Table) Query[O] { - return &query[O]{ - statement: newStatement[O](table), - } -} - -// Result implements [Query]. -func (q *query[O]) Result(ctx context.Context, client database.Querier) (*O, error) { - var object O - row := client.QueryRow(ctx, q.String(), q.args...) - if err := object.Scan(row); err != nil { - return nil, err - } - return &object, nil -} - -// Results implements [Query]. -func (q *query[O]) Results(ctx context.Context, client database.Querier) ([]O, error) { - var objects []O - rows, err := client.Query(ctx, q.String(), q.args...) - if err != nil { - return nil, err - } - defer rows.Close() - - for rows.Next() { - var object O - if err := object.Scan(rows); err != nil { - return nil, err - } - objects = append(objects, object) - } - - return objects, rows.Err() -} - -// Join implements [Query]. -func (q *query[O]) Join(tables ...Table) { - for _, tbl := range tables { - cols := q.tbl.(*table).possibleJoins(tbl) - if len(cols) == 0 { - panic(fmt.Sprintf("table %q does not have any possible joins with table %q", q.tbl.Name(), tbl.Name())) - } - - q.joins = append(q.joins, join{ - table: tbl, - conditions: make([]joinCondition, 0, len(cols)), - }) - - for colName, col := range cols { - q.joins[len(q.joins)-1].conditions = append(q.joins[len(q.joins)-1].conditions, joinCondition{ - left: q.tbl.(*table).columns[colName], - right: col, - }) - } - } -} - -func (q *query[O]) Limit(limit uint32) { - q.limit = limit -} - -func (q *query[O]) Offset(offset uint32) { - q.offset = offset -} - -func (q *query[O]) OrderBy(columns ...Column) { - for _, allowedColumn := range q.columns { - for _, column := range columns { - if allowedColumn.Name() == column.Name() { - q.orderBy = append(q.orderBy, column) - } - } - } -} - -// String implements [fmt.Stringer] and [Query]. -func (q *query[O]) String() string { - q.writeSelectColumns() - q.writeFrom() - q.writeJoins() - q.writeCondition() - q.writeOrderBy() - q.writeLimit() - q.writeOffset() - q.writeGroupBy() - return q.builder.String() -} - -func (q *query[O]) writeSelectColumns() { - q.builder.WriteString("SELECT ") - for i, column := range q.columns { - if i > 0 { - q.builder.WriteString(", ") - } - q.builder.WriteString(q.tbl.Alias()) - q.builder.WriteRune('.') - q.builder.WriteString(column.Name()) - } -} - -func (q *query[O]) writeJoins() { - for _, join := range q.joins { - q.builder.WriteString(" JOIN ") - q.builder.WriteString(join.table.Schema()) - q.builder.WriteRune('.') - q.builder.WriteString(join.table.Name()) - if join.table.Alias() != "" { - q.builder.WriteString(" AS ") - q.builder.WriteString(join.table.Alias()) - } - - q.builder.WriteString(" ON ") - for i, condition := range join.conditions { - if i > 0 { - q.builder.WriteString(" AND ") - } - q.builder.WriteString(condition.left.Name()) - q.builder.WriteString(" = ") - q.builder.WriteString(condition.right.Name()) - } - } -} - -func (q *query[O]) writeOrderBy() { - if len(q.orderBy) == 0 { - return - } - - q.builder.WriteString(" ORDER BY ") - for i, order := range q.orderBy { - if i > 0 { - q.builder.WriteString(", ") - } - order.Write(q) - } -} - -func (q *query[O]) writeLimit() { - if q.limit == 0 { - return - } - q.builder.WriteString(" LIMIT ") - q.builder.WriteString(q.appendArg(q.limit)) -} - -func (q *query[O]) writeOffset() { - if q.offset == 0 { - return - } - q.builder.WriteString(" OFFSET ") - q.builder.WriteString(q.appendArg(q.offset)) -} - -func (q *query[O]) writeGroupBy() { - q.builder.WriteString(" GROUP BY ") -} diff --git a/backend/v3/storage/database/repository/stmt/v3/statement.go b/backend/v3/storage/database/repository/stmt/v3/statement.go deleted file mode 100644 index 57884f357b..0000000000 --- a/backend/v3/storage/database/repository/stmt/v3/statement.go +++ /dev/null @@ -1,85 +0,0 @@ -package v3 - -import ( - "fmt" - "strings" -) - -type statement[T object] struct { - tbl Table - columns []Column - condition Condition - - builder strings.Builder - args []any - existingArgs map[any]string -} - -func newStatement[O object](t Table) *statement[O] { - var o O - return &statement[O]{ - tbl: t, - columns: o.Columns(t), - } -} - -// Where implements [Query]. -func (stmt *statement[T]) Where(condition Condition) { - stmt.condition = condition -} - -func (stmt *statement[T]) writeFrom() { - stmt.builder.WriteString(" FROM ") - stmt.builder.WriteString(stmt.tbl.Schema()) - stmt.builder.WriteRune('.') - stmt.builder.WriteString(stmt.tbl.Name()) - if stmt.tbl.Alias() != "" { - stmt.builder.WriteString(" AS ") - stmt.builder.WriteString(stmt.tbl.Alias()) - } -} - -func (stmt *statement[T]) writeCondition() { - if stmt.condition == nil { - return - } - stmt.builder.WriteString(" WHERE ") - stmt.condition.writeOn(stmt) -} - -// appendArg implements [statementBuilder]. -func (stmt *statement[T]) appendArg(arg any) (placeholder string) { - if stmt.existingArgs == nil { - stmt.existingArgs = make(map[any]string) - } - if placeholder, ok := stmt.existingArgs[arg]; ok { - return placeholder - } - - stmt.args = append(stmt.args, arg) - placeholder = fmt.Sprintf("$%d", len(stmt.args)) - stmt.existingArgs[arg] = placeholder - return placeholder -} - -// table implements [statementBuilder]. -func (stmt *statement[T]) table() Table { - return stmt.tbl -} - -// write implements [statementBuilder]. -func (stmt *statement[T]) write(data []byte) { - stmt.builder.Write(data) -} - -// writeRune implements [statementBuilder]. -func (stmt *statement[T]) writeRune(r rune) { - stmt.builder.WriteRune(r) -} - -// writeString implements [statementBuilder]. -func (stmt *statement[T]) writeString(s string) { - stmt.builder.WriteString(s) -} - -var _ statementBuilder = (*statement[Instance])(nil) diff --git a/backend/v3/storage/database/repository/stmt/v3/table.go b/backend/v3/storage/database/repository/stmt/v3/table.go deleted file mode 100644 index 95a0f6f58b..0000000000 --- a/backend/v3/storage/database/repository/stmt/v3/table.go +++ /dev/null @@ -1,84 +0,0 @@ -package v3 - -import "github.com/zitadel/zitadel/backend/v3/storage/database" - -type object interface { - User | Org | Instance - Columns(t Table) []Column - Scan(s database.Scanner) error -} - -type Table interface { - Schema() string - Name() string - Alias() string - Columns() []Column - - writeOn(builder statementBuilder) -} - -type table struct { - schema string - name string - alias string - - possibleJoins func(table Table) map[string]Column - - columns map[string]Column - colList []Column -} - -func newTable[O object](schema, name string) *table { - t := &table{ - schema: schema, - name: name, - } - - var o O - t.colList = o.Columns(t) - t.columns = make(map[string]Column, len(t.colList)) - for _, col := range t.colList { - t.columns[col.Name()] = col - } - - return t -} - -// Columns implements [Table]. -func (t *table) Columns() []Column { - if len(t.colList) > 0 { - return t.colList - } - - t.colList = make([]Column, 0, len(t.columns)) - for _, column := range t.columns { - t.colList = append(t.colList, column) - } - - return t.colList -} - -// Name implements [Table]. -func (t *table) Name() string { - return t.name -} - -// Schema implements [Table]. -func (t *table) Schema() string { - return t.schema -} - -// Alias implements [Table]. -func (t *table) Alias() string { - if t.alias != "" { - return t.alias - } - return t.schema + "." + t.name -} - -// writeOn implements [Table]. -func (t *table) writeOn(builder statementBuilder) { - builder.writeString(t.Alias()) -} - -var _ Table = (*table)(nil) diff --git a/backend/v3/storage/database/repository/stmt/v3/user.go b/backend/v3/storage/database/repository/stmt/v3/user.go deleted file mode 100644 index f872382902..0000000000 --- a/backend/v3/storage/database/repository/stmt/v3/user.go +++ /dev/null @@ -1,170 +0,0 @@ -package v3 - -import ( - "time" - - "github.com/zitadel/zitadel/backend/v3/storage/database" -) - -type User struct { - instanceID string - orgID string - id string - username string - - createdAt time.Time - updatedAt time.Time - deletedAt time.Time -} - -// Columns implements [object]. -func (u User) Columns(table Table) []Column { - return []Column{ - &column{ - table: table, - name: columnNameInstanceID, - }, - &column{ - table: table, - name: columnNameOrgID, - }, - &column{ - table: table, - name: columnNameID, - }, - &columnIgnoreCase{ - column: column{ - table: table, - name: userTableUsernameColumn, - }, - suffix: "_lower", - }, - &column{ - table: table, - name: columnNameCreatedAt, - }, - &column{ - table: table, - name: columnNameUpdatedAt, - }, - &column{ - table: table, - name: columnNameDeletedAt, - }, - } -} - -// Scan implements [object]. -func (u User) Scan(row database.Scanner) error { - return row.Scan( - &u.instanceID, - &u.orgID, - &u.id, - &u.username, - &u.createdAt, - &u.updatedAt, - &u.deletedAt, - ) -} - -type userTable struct { - *table -} - -const ( - userTableUsernameColumn = "username" -) - -func UserTable() *userTable { - table := &userTable{ - table: newTable[User]("zitadel", "users"), - } - - table.possibleJoins = func(table Table) map[string]Column { - switch on := table.(type) { - case *userTable: - return map[string]Column{ - columnNameInstanceID: on.InstanceIDColumn(), - columnNameOrgID: on.OrgIDColumn(), - columnNameID: on.IDColumn(), - } - case *orgTable: - return map[string]Column{ - columnNameInstanceID: on.InstanceIDColumn(), - columnNameOrgID: on.IDColumn(), - } - case *instanceTable: - return map[string]Column{ - columnNameInstanceID: on.IDColumn(), - } - default: - return nil - } - } - - return table -} - -func (t *userTable) InstanceIDColumn() Column { - return t.columns[columnNameInstanceID] -} - -func (t *userTable) OrgIDColumn() Column { - return t.columns[columnNameOrgID] -} - -func (t *userTable) IDColumn() Column { - return t.columns[columnNameID] -} - -func (t *userTable) UsernameColumn() Column { - return t.columns[userTableUsernameColumn] -} - -func (t *userTable) CreatedAtColumn() Column { - return t.columns[columnNameCreatedAt] -} - -func (t *userTable) UpdatedAtColumn() Column { - return t.columns[columnNameUpdatedAt] -} - -func (t *userTable) DeletedAtColumn() Column { - return t.columns[columnNameDeletedAt] -} - -func NewUserQuery() Query[User] { - q := NewQuery[User](UserTable()) - return q -} - -type userByIDCondition[T Text] struct { - id T -} - -func UserByID[T Text](id T) Condition { - return &userByIDCondition[T]{id: id} -} - -// writeOn implements Condition. -func (u *userByIDCondition[T]) writeOn(builder statementBuilder) { - NewTextCondition(builder.table().(*userTable).IDColumn(), TextOperatorEqual, u.id).writeOn(builder) -} - -var _ Condition = (*userByIDCondition[string])(nil) - -type userByUsernameCondition[T Text] struct { - username T - operator TextOperator -} - -func UserByUsername[T Text](username T, operator TextOperator) Condition { - return &userByUsernameCondition[T]{username: username, operator: operator} -} - -// writeOn implements Condition. -func (u *userByUsernameCondition[T]) writeOn(builder statementBuilder) { - NewTextCondition(builder.table().(*userTable).UsernameColumn(), u.operator, u.username).writeOn(builder) -} - -var _ Condition = (*userByUsernameCondition[string])(nil) diff --git a/backend/v3/storage/database/repository/stmt/v3/user_test.go b/backend/v3/storage/database/repository/stmt/v3/user_test.go deleted file mode 100644 index 4bcbca7ee9..0000000000 --- a/backend/v3/storage/database/repository/stmt/v3/user_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package v3_test - -import ( - "context" - "testing" - - v3 "github.com/zitadel/zitadel/backend/v3/storage/database/repository/stmt/v3" -) - -type user struct{} - -func TestUser(t *testing.T) { - query := v3.NewUserQuery() - query.Where( - v3.Or( - v3.UserByID("123"), - v3.UserByUsername("test", v3.TextOperatorStartsWithIgnoreCase), - ), - ) - query.Limit(10) - query.Offset(5) - // query.OrderBy( - - query.Result(context.TODO(), nil) -} diff --git a/backend/v3/storage/database/repository/stmt/v4/doc.go b/backend/v3/storage/database/repository/stmt/v4/doc.go deleted file mode 100644 index 41871c5e1d..0000000000 --- a/backend/v3/storage/database/repository/stmt/v4/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// this test focuses on queries rather than on tables -package v4 diff --git a/backend/v3/storage/database/repository/stmt/v4/org.go b/backend/v3/storage/database/repository/stmt/v4/org.go deleted file mode 100644 index fd39bb31f8..0000000000 --- a/backend/v3/storage/database/repository/stmt/v4/org.go +++ /dev/null @@ -1,17 +0,0 @@ -package v4 - -type Org struct { - InstanceID string - ID string - Name string -} - -type GetOrg struct{} - -type ListOrgs struct{} - -type CreateOrg struct{} - -type UpdateOrg struct{} - -type DeleteOrg struct{} diff --git a/backend/v3/storage/database/repository/stmt/v4/user.go b/backend/v3/storage/database/repository/stmt/v4/user.go deleted file mode 100644 index 8613611e29..0000000000 --- a/backend/v3/storage/database/repository/stmt/v4/user.go +++ /dev/null @@ -1,284 +0,0 @@ -package v4 - -import ( - "context" - "time" - - "github.com/zitadel/zitadel/backend/v3/domain" - "github.com/zitadel/zitadel/backend/v3/storage/database" -) - -const queryUserStmt = `SELECT instance_id, org_id, id, username, type, created_at, updated_at, deleted_at,` + - ` first_name, last_name, email_address, email_verified_at, phone_number, phone_verified_at, description` + - ` FROM users_view` - -type user struct { - builder database.StatementBuilder - client database.QueryExecutor -} - -func UserRepository(client database.QueryExecutor) domain.UserRepository { - return &user{ - client: client, - } -} - -var _ domain.UserRepository = (*user)(nil) - -// ------------------------------------------------------------- -// repository -// ------------------------------------------------------------- - -// Human implements [domain.UserRepository]. -func (u *user) Human() domain.HumanRepository { - return &userHuman{user: u} -} - -// Machine implements [domain.UserRepository]. -func (u *user) Machine() domain.MachineRepository { - return &userMachine{user: u} -} - -// List implements [domain.UserRepository]. -func (u *user) List(ctx context.Context, opts ...database.QueryOption) (users []*domain.User, err error) { - options := new(database.QueryOpts) - for _, opt := range opts { - opt(options) - } - - u.builder.WriteString(queryUserStmt) - options.WriteCondition(&u.builder) - options.WriteOrderBy(&u.builder) - options.WriteLimit(&u.builder) - options.WriteOffset(&u.builder) - - rows, err := u.client.Query(ctx, u.builder.String(), u.builder.Args()...) - if err != nil { - return nil, err - } - defer func() { - closeErr := rows.Close() - if err != nil { - return - } - err = closeErr - }() - for rows.Next() { - user, err := scanUser(rows) - if err != nil { - return nil, err - } - users = append(users, user) - } - if err := rows.Err(); err != nil { - return nil, err - } - return users, nil -} - -// Get implements [domain.UserRepository]. -func (u *user) Get(ctx context.Context, opts ...database.QueryOption) (*domain.User, error) { - options := new(database.QueryOpts) - for _, opt := range opts { - opt(options) - } - - u.builder.WriteString(queryUserStmt) - options.WriteCondition(&u.builder) - options.WriteOrderBy(&u.builder) - options.WriteLimit(&u.builder) - options.WriteOffset(&u.builder) - - return scanUser(u.client.QueryRow(ctx, u.builder.String(), u.builder.Args()...)) -} - -const ( - createHumanStmt = `INSERT INTO human_users (instance_id, org_id, user_id, username, first_name, last_name, email_address, email_verified_at, phone_number, phone_verified_at)` + - ` VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)` + - ` RETURNING created_at, updated_at` - createMachineStmt = `INSERT INTO user_machines (instance_id, org_id, user_id, username, description)` + - ` VALUES ($1, $2, $3, $4, $5)` + - ` RETURNING created_at, updated_at` -) - -// Create implements [domain.UserRepository]. -func (u *user) Create(ctx context.Context, user *domain.User) error { - u.builder.AppendArgs(user.InstanceID, user.OrgID, user.ID, user.Username, user.Traits.Type()) - switch trait := user.Traits.(type) { - case *domain.Human: - u.builder.WriteString(createHumanStmt) - u.builder.AppendArgs(trait.FirstName, trait.LastName, trait.Email.Address, trait.Email.VerifiedAt, trait.Phone.Number, trait.Phone.VerifiedAt) - case *domain.Machine: - u.builder.WriteString(createMachineStmt) - u.builder.AppendArgs(trait.Description) - } - return u.client.QueryRow(ctx, u.builder.String(), u.builder.Args()...).Scan(&user.CreatedAt, &user.UpdatedAt) -} - -// Delete implements [domain.UserRepository]. -func (u *user) Delete(ctx context.Context, condition database.Condition) error { - u.builder.WriteString("DELETE FROM users") - u.writeCondition(condition) - return u.client.Exec(ctx, u.builder.String(), u.builder.Args()...) -} - -// ------------------------------------------------------------- -// changes -// ------------------------------------------------------------- - -// SetUsername implements [domain.userChanges]. -func (u user) SetUsername(username string) database.Change { - return database.NewChange(u.UsernameColumn(), username) -} - -// ------------------------------------------------------------- -// conditions -// ------------------------------------------------------------- - -// InstanceIDCondition implements [domain.userConditions]. -func (u user) InstanceIDCondition(instanceID string) database.Condition { - return database.NewTextCondition(u.InstanceIDColumn(), database.TextOperationEqual, instanceID) -} - -// OrgIDCondition implements [domain.userConditions]. -func (u user) OrgIDCondition(orgID string) database.Condition { - return database.NewTextCondition(u.OrgIDColumn(), database.TextOperationEqual, orgID) -} - -// IDCondition implements [domain.userConditions]. -func (u user) IDCondition(userID string) database.Condition { - return database.NewTextCondition(u.IDColumn(), database.TextOperationEqual, userID) -} - -// UsernameCondition implements [domain.userConditions]. -func (u user) UsernameCondition(op database.TextOperation, username string) database.Condition { - return database.NewTextCondition(u.UsernameColumn(), op, username) -} - -// CreatedAtCondition implements [domain.userConditions]. -func (u user) CreatedAtCondition(op database.NumberOperation, createdAt time.Time) database.Condition { - return database.NewNumberCondition(u.CreatedAtColumn(), op, createdAt) -} - -// UpdatedAtCondition implements [domain.userConditions]. -func (u user) UpdatedAtCondition(op database.NumberOperation, updatedAt time.Time) database.Condition { - return database.NewNumberCondition(u.UpdatedAtColumn(), op, updatedAt) -} - -// DeletedCondition implements [domain.userConditions]. -func (u user) DeletedCondition(isDeleted bool) database.Condition { - if isDeleted { - return database.IsNotNull(u.DeletedAtColumn()) - } - return database.IsNull(u.DeletedAtColumn()) -} - -// DeletedAtCondition implements [domain.userConditions]. -func (u user) DeletedAtCondition(op database.NumberOperation, deletedAt time.Time) database.Condition { - return database.NewNumberCondition(u.DeletedAtColumn(), op, deletedAt) -} - -// ------------------------------------------------------------- -// columns -// ------------------------------------------------------------- - -// InstanceIDColumn implements [domain.userColumns]. -func (user) InstanceIDColumn() database.Column { - return database.NewColumn("instance_id") -} - -// OrgIDColumn implements [domain.userColumns]. -func (user) OrgIDColumn() database.Column { - return database.NewColumn("org_id") -} - -// IDColumn implements [domain.userColumns]. -func (user) IDColumn() database.Column { - return database.NewColumn("id") -} - -// UsernameColumn implements [domain.userColumns]. -func (user) UsernameColumn() database.Column { - return database.NewIgnoreCaseColumn("username", "_lower") -} - -// FirstNameColumn implements [domain.userColumns]. -func (user) CreatedAtColumn() database.Column { - return database.NewColumn("created_at") -} - -// UpdatedAtColumn implements [domain.userColumns]. -func (user) UpdatedAtColumn() database.Column { - return database.NewColumn("updated_at") -} - -// DeletedAtColumn implements [domain.userColumns]. -func (user) DeletedAtColumn() database.Column { - return database.NewColumn("deleted_at") -} - -func (u *user) writeCondition(condition database.Condition) { - if condition == nil { - return - } - u.builder.WriteString(" WHERE ") - condition.Write(&u.builder) -} - -func (u user) columns() database.Columns { - return database.Columns{ - u.InstanceIDColumn(), - u.OrgIDColumn(), - u.IDColumn(), - u.UsernameColumn(), - u.CreatedAtColumn(), - u.UpdatedAtColumn(), - u.DeletedAtColumn(), - } -} - -func scanUser(scanner database.Scanner) (*domain.User, error) { - var ( - user domain.User - human domain.Human - email domain.Email - phone domain.Phone - machine domain.Machine - typ domain.UserType - ) - err := scanner.Scan( - &user.InstanceID, - &user.OrgID, - &user.ID, - &user.Username, - &typ, - &user.CreatedAt, - &user.UpdatedAt, - &user.DeletedAt, - &human.FirstName, - &human.LastName, - &email.Address, - &email.VerifiedAt, - &phone.Number, - &phone.VerifiedAt, - &machine.Description, - ) - if err != nil { - return nil, err - } - - switch typ { - case domain.UserTypeHuman: - if email.Address != "" { - human.Email = &email - } - if phone.Number != "" { - human.Phone = &phone - } - user.Traits = &human - case domain.UserTypeMachine: - user.Traits = &machine - } - - return &user, nil -} diff --git a/backend/v3/storage/database/repository/user.go b/backend/v3/storage/database/repository/user.go index dcc0b64f0c..872650b9b5 100644 --- a/backend/v3/storage/database/repository/user.go +++ b/backend/v3/storage/database/repository/user.go @@ -1,39 +1,285 @@ package repository import ( + "context" + "time" + "github.com/zitadel/zitadel/backend/v3/domain" "github.com/zitadel/zitadel/backend/v3/storage/database" ) +const queryUserStmt = `SELECT instance_id, org_id, id, username, type, created_at, updated_at, deleted_at,` + + ` first_name, last_name, email_address, email_verified_at, phone_number, phone_verified_at, description` + + ` FROM users_view` + type user struct { - database.QueryExecutor + repository } -func User(client database.QueryExecutor) domain.UserRepository { - // return &user{QueryExecutor: client} - return nil -} - -// On implements [domain.UserRepository]. -func (exec *user) On(clauses ...domain.UserClause) domain.UserOperation { - return &userOperation{ - QueryExecutor: exec.QueryExecutor, - clauses: clauses, +func UserRepository(client database.QueryExecutor) domain.UserRepository { + return &user{ + repository: repository{ + client: client, + }, } } -// OnHuman implements [domain.UserRepository]. -func (exec *user) OnHuman(clauses ...domain.UserClause) domain.HumanOperation { - return &humanOperation{ - userOperation: *exec.On(clauses...).(*userOperation), +var _ domain.UserRepository = (*user)(nil) + +// ------------------------------------------------------------- +// repository +// ------------------------------------------------------------- + +// Human implements [domain.UserRepository]. +func (u *user) Human() domain.HumanRepository { + return &userHuman{user: u} +} + +// Machine implements [domain.UserRepository]. +func (u *user) Machine() domain.MachineRepository { + return &userMachine{user: u} +} + +// List implements [domain.UserRepository]. +func (u *user) List(ctx context.Context, opts ...database.QueryOption) (users []*domain.User, err error) { + options := new(database.QueryOpts) + for _, opt := range opts { + opt(options) + } + + u.builder.WriteString(queryUserStmt) + options.WriteCondition(&u.builder) + options.WriteOrderBy(&u.builder) + options.WriteLimit(&u.builder) + options.WriteOffset(&u.builder) + + rows, err := u.client.Query(ctx, u.builder.String(), u.builder.Args()...) + if err != nil { + return nil, err + } + defer func() { + closeErr := rows.Close() + if err != nil { + return + } + err = closeErr + }() + for rows.Next() { + user, err := scanUser(rows) + if err != nil { + return nil, err + } + users = append(users, user) + } + if err := rows.Err(); err != nil { + return nil, err + } + return users, nil +} + +// Get implements [domain.UserRepository]. +func (u *user) Get(ctx context.Context, opts ...database.QueryOption) (*domain.User, error) { + options := new(database.QueryOpts) + for _, opt := range opts { + opt(options) + } + + u.builder.WriteString(queryUserStmt) + options.WriteCondition(&u.builder) + options.WriteOrderBy(&u.builder) + options.WriteLimit(&u.builder) + options.WriteOffset(&u.builder) + + return scanUser(u.client.QueryRow(ctx, u.builder.String(), u.builder.Args()...)) +} + +const ( + createHumanStmt = `INSERT INTO human_users (instance_id, org_id, user_id, username, first_name, last_name, email_address, email_verified_at, phone_number, phone_verified_at)` + + ` VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)` + + ` RETURNING created_at, updated_at` + createMachineStmt = `INSERT INTO user_machines (instance_id, org_id, user_id, username, description)` + + ` VALUES ($1, $2, $3, $4, $5)` + + ` RETURNING created_at, updated_at` +) + +// Create implements [domain.UserRepository]. +func (u *user) Create(ctx context.Context, user *domain.User) error { + u.builder.AppendArgs(user.InstanceID, user.OrgID, user.ID, user.Username, user.Traits.Type()) + switch trait := user.Traits.(type) { + case *domain.Human: + u.builder.WriteString(createHumanStmt) + u.builder.AppendArgs(trait.FirstName, trait.LastName, trait.Email.Address, trait.Email.VerifiedAt, trait.Phone.Number, trait.Phone.VerifiedAt) + case *domain.Machine: + u.builder.WriteString(createMachineStmt) + u.builder.AppendArgs(trait.Description) + } + return u.client.QueryRow(ctx, u.builder.String(), u.builder.Args()...).Scan(&user.CreatedAt, &user.UpdatedAt) +} + +// Delete implements [domain.UserRepository]. +func (u *user) Delete(ctx context.Context, condition database.Condition) error { + u.builder.WriteString("DELETE FROM users") + u.writeCondition(condition) + return u.client.Exec(ctx, u.builder.String(), u.builder.Args()...) +} + +// ------------------------------------------------------------- +// changes +// ------------------------------------------------------------- + +// SetUsername implements [domain.userChanges]. +func (u user) SetUsername(username string) database.Change { + return database.NewChange(u.UsernameColumn(), username) +} + +// ------------------------------------------------------------- +// conditions +// ------------------------------------------------------------- + +// InstanceIDCondition implements [domain.userConditions]. +func (u user) InstanceIDCondition(instanceID string) database.Condition { + return database.NewTextCondition(u.InstanceIDColumn(), database.TextOperationEqual, instanceID) +} + +// OrgIDCondition implements [domain.userConditions]. +func (u user) OrgIDCondition(orgID string) database.Condition { + return database.NewTextCondition(u.OrgIDColumn(), database.TextOperationEqual, orgID) +} + +// IDCondition implements [domain.userConditions]. +func (u user) IDCondition(userID string) database.Condition { + return database.NewTextCondition(u.IDColumn(), database.TextOperationEqual, userID) +} + +// UsernameCondition implements [domain.userConditions]. +func (u user) UsernameCondition(op database.TextOperation, username string) database.Condition { + return database.NewTextCondition(u.UsernameColumn(), op, username) +} + +// CreatedAtCondition implements [domain.userConditions]. +func (u user) CreatedAtCondition(op database.NumberOperation, createdAt time.Time) database.Condition { + return database.NewNumberCondition(u.CreatedAtColumn(), op, createdAt) +} + +// UpdatedAtCondition implements [domain.userConditions]. +func (u user) UpdatedAtCondition(op database.NumberOperation, updatedAt time.Time) database.Condition { + return database.NewNumberCondition(u.UpdatedAtColumn(), op, updatedAt) +} + +// DeletedCondition implements [domain.userConditions]. +func (u user) DeletedCondition(isDeleted bool) database.Condition { + if isDeleted { + return database.IsNotNull(u.DeletedAtColumn()) + } + return database.IsNull(u.DeletedAtColumn()) +} + +// DeletedAtCondition implements [domain.userConditions]. +func (u user) DeletedAtCondition(op database.NumberOperation, deletedAt time.Time) database.Condition { + return database.NewNumberCondition(u.DeletedAtColumn(), op, deletedAt) +} + +// ------------------------------------------------------------- +// columns +// ------------------------------------------------------------- + +// InstanceIDColumn implements [domain.userColumns]. +func (user) InstanceIDColumn() database.Column { + return database.NewColumn("instance_id") +} + +// OrgIDColumn implements [domain.userColumns]. +func (user) OrgIDColumn() database.Column { + return database.NewColumn("org_id") +} + +// IDColumn implements [domain.userColumns]. +func (user) IDColumn() database.Column { + return database.NewColumn("id") +} + +// UsernameColumn implements [domain.userColumns]. +func (user) UsernameColumn() database.Column { + return database.NewIgnoreCaseColumn("username", "_lower") +} + +// FirstNameColumn implements [domain.userColumns]. +func (user) CreatedAtColumn() database.Column { + return database.NewColumn("created_at") +} + +// UpdatedAtColumn implements [domain.userColumns]. +func (user) UpdatedAtColumn() database.Column { + return database.NewColumn("updated_at") +} + +// DeletedAtColumn implements [domain.userColumns]. +func (user) DeletedAtColumn() database.Column { + return database.NewColumn("deleted_at") +} + +func (u *user) writeCondition(condition database.Condition) { + if condition == nil { + return + } + u.builder.WriteString(" WHERE ") + condition.Write(&u.builder) +} + +func (u user) columns() database.Columns { + return database.Columns{ + u.InstanceIDColumn(), + u.OrgIDColumn(), + u.IDColumn(), + u.UsernameColumn(), + u.CreatedAtColumn(), + u.UpdatedAtColumn(), + u.DeletedAtColumn(), } } -// OnMachine implements [domain.UserRepository]. -func (exec *user) OnMachine(clauses ...domain.UserClause) domain.MachineOperation { - return &machineOperation{ - userOperation: *exec.On(clauses...).(*userOperation), +func scanUser(scanner database.Scanner) (*domain.User, error) { + var ( + user domain.User + human domain.Human + email domain.Email + phone domain.Phone + machine domain.Machine + typ domain.UserType + ) + err := scanner.Scan( + &user.InstanceID, + &user.OrgID, + &user.ID, + &user.Username, + &typ, + &user.CreatedAt, + &user.UpdatedAt, + &user.DeletedAt, + &human.FirstName, + &human.LastName, + &email.Address, + &email.VerifiedAt, + &phone.Number, + &phone.VerifiedAt, + &machine.Description, + ) + if err != nil { + return nil, err } -} -// var _ domain.UserRepository = (*user)(nil) + switch typ { + case domain.UserTypeHuman: + if email.Address != "" { + human.Email = &email + } + if phone.Number != "" { + human.Phone = &phone + } + user.Traits = &human + case domain.UserTypeMachine: + user.Traits = &machine + } + + return &user, nil +} diff --git a/backend/v3/storage/database/repository/stmt/v4/user_human.go b/backend/v3/storage/database/repository/user_human.go similarity index 99% rename from backend/v3/storage/database/repository/stmt/v4/user_human.go rename to backend/v3/storage/database/repository/user_human.go index 05fa794f8f..123a834cb5 100644 --- a/backend/v3/storage/database/repository/stmt/v4/user_human.go +++ b/backend/v3/storage/database/repository/user_human.go @@ -1,4 +1,4 @@ -package v4 +package repository import ( "context" diff --git a/backend/v3/storage/database/repository/user_human_operation.go b/backend/v3/storage/database/repository/user_human_operation.go deleted file mode 100644 index cc4de1d5db..0000000000 --- a/backend/v3/storage/database/repository/user_human_operation.go +++ /dev/null @@ -1,36 +0,0 @@ -package repository - -import ( - "context" - - "github.com/zitadel/zitadel/backend/v3/domain" -) - -type humanOperation struct { - userOperation -} - -// GetEmail implements domain.HumanOperation. -func (h *humanOperation) GetEmail(ctx context.Context) (*domain.Email, error) { - var email domain.Email - err := h.QueryExecutor.QueryRow(ctx, `SELECT email, is_email_verified FROM human_users WHERE id = $1`, h.clauses).Scan( - &email.Address, - &email.IsVerified, - ) - if err != nil { - return nil, err - } - return &email, nil -} - -// SetEmail implements domain.HumanOperation. -func (h *humanOperation) SetEmail(ctx context.Context, email string) error { - return h.QueryExecutor.Exec(ctx, `UPDATE human_users SET email = $1 WHERE id = $2`, email, h.clauses) -} - -// SetEmailVerified implements domain.HumanOperation. -func (h *humanOperation) SetEmailVerified(ctx context.Context, email string) error { - return h.QueryExecutor.Exec(ctx, `UPDATE human_users SET is_email_verified = $1 WHERE id = $2 AND email = $3`, true, h.clauses, email) -} - -var _ domain.HumanOperation = (*humanOperation)(nil) diff --git a/backend/v3/storage/database/repository/stmt/v4/user_machine.go b/backend/v3/storage/database/repository/user_machine.go similarity index 99% rename from backend/v3/storage/database/repository/stmt/v4/user_machine.go rename to backend/v3/storage/database/repository/user_machine.go index 501722bd8a..f60001927b 100644 --- a/backend/v3/storage/database/repository/stmt/v4/user_machine.go +++ b/backend/v3/storage/database/repository/user_machine.go @@ -1,4 +1,4 @@ -package v4 +package repository import ( "context" diff --git a/backend/v3/storage/database/repository/user_machine_operation.go b/backend/v3/storage/database/repository/user_machine_operation.go deleted file mode 100644 index b01451f566..0000000000 --- a/backend/v3/storage/database/repository/user_machine_operation.go +++ /dev/null @@ -1,18 +0,0 @@ -package repository - -import ( - "context" - - "github.com/zitadel/zitadel/backend/v3/domain" -) - -type machineOperation struct { - userOperation -} - -// SetDescription implements domain.MachineOperation. -func (m *machineOperation) SetDescription(ctx context.Context, description string) error { - return m.QueryExecutor.Exec(ctx, `UPDATE machines SET description = $1 WHERE id = $2`, description, m.clauses) -} - -var _ domain.MachineOperation = (*machineOperation)(nil) diff --git a/backend/v3/storage/database/repository/user_operation.go b/backend/v3/storage/database/repository/user_operation.go deleted file mode 100644 index eea24463b8..0000000000 --- a/backend/v3/storage/database/repository/user_operation.go +++ /dev/null @@ -1,68 +0,0 @@ -package repository - -import ( - "context" - - "github.com/zitadel/zitadel/backend/v3/domain" - "github.com/zitadel/zitadel/backend/v3/storage/database" -) - -type userOperation struct { - database.QueryExecutor - clauses []domain.UserClause -} - -// Delete implements [domain.UserOperation]. -func (u *userOperation) Delete(ctx context.Context) error { - return u.QueryExecutor.Exec(ctx, `DELETE FROM users WHERE id = $1`, u.clauses) -} - -// SetUsername implements [domain.UserOperation]. -func (u *userOperation) SetUsername(ctx context.Context, username string) error { - var stmt statement - - stmt.builder.WriteString(`UPDATE users SET username = $1 WHERE `) - stmt.appendArg(username) - clausesToSQL(&stmt, u.clauses) - return u.QueryExecutor.Exec(ctx, stmt.builder.String(), stmt.args...) -} - -var _ domain.UserOperation = (*userOperation)(nil) - -func UserIDQuery(id string) domain.UserClause { - return textClause[string]{ - clause: clause[database.TextOperation]{ - field: userFields[domain.UserFieldID], - op: database.TextOperationEqual, - }, - value: id, - } -} - -func HumanEmailQuery(op database.TextOperation, email string) domain.UserClause { - return textClause[string]{ - clause: clause[database.TextOperation]{ - field: userFields[domain.UserHumanFieldEmail], - op: op, - }, - value: email, - } -} - -func HumanEmailVerifiedQuery(op database.BoolOperation) domain.UserClause { - return boolClause[database.BoolOperation]{ - clause: clause[database.BoolOperation]{ - field: userFields[domain.UserHumanFieldEmailVerified], - op: op, - }, - } -} - -func clausesToSQL(stmt *statement, clauses []domain.UserClause) { - for _, clause := range clauses { - - stmt.builder.WriteString(userFields[clause.Field()].String()) - stmt.builder.WriteString(clause.Operation().String()) - stmt.appendArg(clause.Args()...) - } -} diff --git a/backend/v3/storage/database/repository/stmt/v4/user_test.go b/backend/v3/storage/database/repository/user_test.go similarity index 78% rename from backend/v3/storage/database/repository/stmt/v4/user_test.go rename to backend/v3/storage/database/repository/user_test.go index 5b81fcc259..a1f7a8c7da 100644 --- a/backend/v3/storage/database/repository/stmt/v4/user_test.go +++ b/backend/v3/storage/database/repository/user_test.go @@ -1,4 +1,4 @@ -package v4_test +package repository_test import ( "context" @@ -6,12 +6,16 @@ import ( "github.com/stretchr/testify/assert" "github.com/zitadel/zitadel/backend/v3/storage/database" - v4 "github.com/zitadel/zitadel/backend/v3/storage/database/repository/stmt/v4" + "github.com/zitadel/zitadel/backend/v3/storage/database/dbmock" + "github.com/zitadel/zitadel/backend/v3/storage/database/repository" + "go.uber.org/mock/gomock" ) func TestQueryUser(t *testing.T) { t.Run("User filters", func(t *testing.T) { - user := v4.UserRepository(nil) + client := dbmock.NewMockClient(gomock.NewController(t)) + + user := repository.UserRepository(client) u, err := user.Get(context.Background(), database.WithCondition( database.And( @@ -30,7 +34,9 @@ func TestQueryUser(t *testing.T) { }) t.Run("machine and human filters", func(t *testing.T) { - user := v4.UserRepository(nil) + client := dbmock.NewMockClient(gomock.NewController(t)) + + user := repository.UserRepository(client) machine := user.Machine() human := user.Human() email, err := human.GetEmail(context.Background(), database.And( @@ -62,7 +68,7 @@ func TestArg(t *testing.T) { func TestWriteUser(t *testing.T) { t.Run("update user", func(t *testing.T) { - user := v4.UserRepository(nil) + user := repository.UserRepository(nil) user.Human().Update(context.Background(), user.IDCondition("test"), user.SetUsername("test")) }) } diff --git a/backend/v3/storage/database/statement.go b/backend/v3/storage/database/statement.go index 55e874a5a7..7d779fe360 100644 --- a/backend/v3/storage/database/statement.go +++ b/backend/v3/storage/database/statement.go @@ -12,16 +12,19 @@ const ( NullInstruction Instruction = "NULL" ) +// StatementBuilder is a helper to build SQL statement. type StatementBuilder struct { strings.Builder args []any existingArgs map[any]string } +// WriteArgs adds the argument to the statement and writes the placeholder to the query. func (b *StatementBuilder) WriteArg(arg any) { b.WriteString(b.AppendArg(arg)) } +// AppebdArg adds the argument to the statement and returns the placeholder. func (b *StatementBuilder) AppendArg(arg any) (placeholder string) { if b.existingArgs == nil { b.existingArgs = make(map[any]string) @@ -39,12 +42,14 @@ func (b *StatementBuilder) AppendArg(arg any) (placeholder string) { return placeholder } +// AppendArgs adds the arguments to the statement and doesn't return the placeholders. func (b *StatementBuilder) AppendArgs(args ...any) { for _, arg := range args { b.AppendArg(arg) } } +// Args returns the arguments added to the statement. func (b *StatementBuilder) Args() []any { return b.args }