776 lines
30 KiB
Go
776 lines
30 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"iter"
|
|
"regexp"
|
|
"sync"
|
|
"time"
|
|
|
|
core "dappco.re/go/core"
|
|
)
|
|
|
|
// validNamespace.MatchString("tenant-a") is true; validNamespace.MatchString("tenant_a") is false.
|
|
var validNamespace = regexp.MustCompile(`^[a-zA-Z0-9-]+$`)
|
|
|
|
const defaultScopedGroupName = "default"
|
|
|
|
// QuotaConfig sets per-namespace key and group limits.
|
|
// Usage example: `quota := store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}`
|
|
type QuotaConfig struct {
|
|
// Usage example: `store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}` limits a namespace to 100 keys.
|
|
MaxKeys int
|
|
// Usage example: `store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}` limits a namespace to 10 groups.
|
|
MaxGroups int
|
|
}
|
|
|
|
// Usage example: `if err := (store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}).Validate(); err != nil { return }`
|
|
func (quotaConfig QuotaConfig) Validate() error {
|
|
if quotaConfig.MaxKeys < 0 || quotaConfig.MaxGroups < 0 {
|
|
return core.E(
|
|
"store.QuotaConfig.Validate",
|
|
core.Sprintf("quota values must be zero or positive; got MaxKeys=%d MaxGroups=%d", quotaConfig.MaxKeys, quotaConfig.MaxGroups),
|
|
nil,
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ScopedStoreConfig combines namespace selection with optional quota limits.
|
|
// Usage example: `config := store.ScopedStoreConfig{Namespace: "tenant-a", Quota: store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}}`
|
|
type ScopedStoreConfig struct {
|
|
// Usage example: `config := store.ScopedStoreConfig{Namespace: "tenant-a"}`
|
|
Namespace string
|
|
// Usage example: `config := store.ScopedStoreConfig{Quota: store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}}`
|
|
Quota QuotaConfig
|
|
}
|
|
|
|
// Usage example: `if err := (store.ScopedStoreConfig{Namespace: "tenant-a", Quota: store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}}).Validate(); err != nil { return }`
|
|
func (scopedConfig ScopedStoreConfig) Validate() error {
|
|
if !validNamespace.MatchString(scopedConfig.Namespace) {
|
|
return core.E(
|
|
"store.ScopedStoreConfig.Validate",
|
|
core.Sprintf("namespace %q is invalid; use names like %q or %q", scopedConfig.Namespace, "tenant-a", "tenant-42"),
|
|
nil,
|
|
)
|
|
}
|
|
if err := scopedConfig.Quota.Validate(); err != nil {
|
|
return core.E("store.ScopedStoreConfig.Validate", "quota", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ScopedStore prefixes group names with namespace + ":" before delegating to Store.
|
|
//
|
|
// Usage example: `scopedStore, err := store.NewScoped(storeInstance, "tenant-a"); if err != nil { return }; if err := scopedStore.SetIn("config", "colour", "blue"); err != nil { return }`
|
|
type ScopedStore struct {
|
|
store *Store
|
|
namespace string
|
|
// Usage example: `scopedStore.MaxKeys = 100`
|
|
MaxKeys int
|
|
// Usage example: `scopedStore.MaxGroups = 10`
|
|
MaxGroups int
|
|
|
|
watcherLock sync.Mutex
|
|
watcherBridges map[uintptr]scopedWatcherBridge
|
|
}
|
|
|
|
type scopedWatcherBridge struct {
|
|
sourceGroup string
|
|
sourceEvents <-chan Event
|
|
done chan struct{}
|
|
}
|
|
|
|
// NewScoped validates a namespace and prefixes groups with namespace + ":".
|
|
// Usage example: `scopedStore, err := store.NewScoped(storeInstance, "tenant-a"); if err != nil { return }`
|
|
func NewScoped(storeInstance *Store, namespace string) (*ScopedStore, error) {
|
|
if storeInstance == nil {
|
|
return nil, core.E("store.NewScoped", "store instance is nil", nil)
|
|
}
|
|
if !validNamespace.MatchString(namespace) {
|
|
return nil, core.E("store.NewScoped", core.Sprintf("namespace %q is invalid; use names like %q or %q", namespace, "tenant-a", "tenant-42"), nil)
|
|
}
|
|
scopedStore := &ScopedStore{
|
|
store: storeInstance,
|
|
namespace: namespace,
|
|
watcherBridges: make(map[uintptr]scopedWatcherBridge),
|
|
}
|
|
return scopedStore, nil
|
|
}
|
|
|
|
// NewScopedConfigured validates the namespace and optional quota settings before constructing a ScopedStore.
|
|
// Usage example: `scopedStore, err := store.NewScopedConfigured(storeInstance, store.ScopedStoreConfig{Namespace: "tenant-a", Quota: store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}}); if err != nil { return }`
|
|
func NewScopedConfigured(storeInstance *Store, scopedConfig ScopedStoreConfig) (*ScopedStore, error) {
|
|
if storeInstance == nil {
|
|
return nil, core.E("store.NewScopedConfigured", "store instance is nil", nil)
|
|
}
|
|
if err := scopedConfig.Validate(); err != nil {
|
|
return nil, core.E("store.NewScopedConfigured", "validate config", err)
|
|
}
|
|
scopedStore, err := NewScoped(storeInstance, scopedConfig.Namespace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
scopedStore.MaxKeys = scopedConfig.Quota.MaxKeys
|
|
scopedStore.MaxGroups = scopedConfig.Quota.MaxGroups
|
|
return scopedStore, nil
|
|
}
|
|
|
|
func (scopedStore *ScopedStore) namespacedGroup(group string) string {
|
|
return scopedStore.namespace + ":" + group
|
|
}
|
|
|
|
func (scopedStore *ScopedStore) namespacePrefix() string {
|
|
return scopedStore.namespace + ":"
|
|
}
|
|
|
|
func (scopedStore *ScopedStore) defaultGroup() string {
|
|
return defaultScopedGroupName
|
|
}
|
|
|
|
func (scopedStore *ScopedStore) trimNamespacePrefix(groupName string) string {
|
|
return core.TrimPrefix(groupName, scopedStore.namespacePrefix())
|
|
}
|
|
|
|
// Namespace returns the namespace string.
|
|
// Usage example: `scopedStore, err := store.NewScoped(storeInstance, "tenant-a"); if err != nil { return }; namespace := scopedStore.Namespace(); fmt.Println(namespace)`
|
|
func (scopedStore *ScopedStore) Namespace() string {
|
|
return scopedStore.namespace
|
|
}
|
|
|
|
// Usage example: `colourValue, err := scopedStore.Get("colour")`
|
|
func (scopedStore *ScopedStore) Get(key string) (string, error) {
|
|
return scopedStore.store.Get(scopedStore.namespacedGroup(scopedStore.defaultGroup()), key)
|
|
}
|
|
|
|
// GetFrom reads a key from an explicit namespaced group.
|
|
// Usage example: `colourValue, err := scopedStore.GetFrom("config", "colour")`
|
|
func (scopedStore *ScopedStore) GetFrom(group, key string) (string, error) {
|
|
return scopedStore.store.Get(scopedStore.namespacedGroup(group), key)
|
|
}
|
|
|
|
// Usage example: `if err := scopedStore.Set("colour", "blue"); err != nil { return }`
|
|
func (scopedStore *ScopedStore) Set(key, value string) error {
|
|
defaultGroup := scopedStore.defaultGroup()
|
|
if err := scopedStore.checkQuota("store.ScopedStore.Set", defaultGroup, key); err != nil {
|
|
return err
|
|
}
|
|
return scopedStore.store.Set(scopedStore.namespacedGroup(defaultGroup), key, value)
|
|
}
|
|
|
|
// SetIn writes a key to an explicit namespaced group.
|
|
// Usage example: `if err := scopedStore.SetIn("config", "colour", "blue"); err != nil { return }`
|
|
func (scopedStore *ScopedStore) SetIn(group, key, value string) error {
|
|
if err := scopedStore.checkQuota("store.ScopedStore.SetIn", group, key); err != nil {
|
|
return err
|
|
}
|
|
return scopedStore.store.Set(scopedStore.namespacedGroup(group), key, value)
|
|
}
|
|
|
|
// Usage example: `if err := scopedStore.SetWithTTL("sessions", "token", "abc123", time.Hour); err != nil { return }`
|
|
func (scopedStore *ScopedStore) SetWithTTL(group, key, value string, timeToLive time.Duration) error {
|
|
if err := scopedStore.checkQuota("store.ScopedStore.SetWithTTL", group, key); err != nil {
|
|
return err
|
|
}
|
|
return scopedStore.store.SetWithTTL(scopedStore.namespacedGroup(group), key, value, timeToLive)
|
|
}
|
|
|
|
// Usage example: `if err := scopedStore.Delete("config", "colour"); err != nil { return }`
|
|
func (scopedStore *ScopedStore) Delete(group, key string) error {
|
|
return scopedStore.store.Delete(scopedStore.namespacedGroup(group), key)
|
|
}
|
|
|
|
// Usage example: `if err := scopedStore.DeleteGroup("cache"); err != nil { return }`
|
|
func (scopedStore *ScopedStore) DeleteGroup(group string) error {
|
|
return scopedStore.store.DeleteGroup(scopedStore.namespacedGroup(group))
|
|
}
|
|
|
|
// Usage example: `if err := scopedStore.DeletePrefix("cache"); err != nil { return }`
|
|
// Usage example: `if err := scopedStore.DeletePrefix(""); err != nil { return }`
|
|
func (scopedStore *ScopedStore) DeletePrefix(groupPrefix string) error {
|
|
return scopedStore.store.DeletePrefix(scopedStore.namespacedGroup(groupPrefix))
|
|
}
|
|
|
|
// Usage example: `colourEntries, err := scopedStore.GetAll("config")`
|
|
func (scopedStore *ScopedStore) GetAll(group string) (map[string]string, error) {
|
|
return scopedStore.store.GetAll(scopedStore.namespacedGroup(group))
|
|
}
|
|
|
|
// Usage example: `page, err := scopedStore.GetPage("config", 0, 25); if err != nil { return }; for _, entry := range page { fmt.Println(entry.Key, entry.Value) }`
|
|
func (scopedStore *ScopedStore) GetPage(group string, offset, limit int) ([]KeyValue, error) {
|
|
return scopedStore.store.GetPage(scopedStore.namespacedGroup(group), offset, limit)
|
|
}
|
|
|
|
// Usage example: `for entry, err := range scopedStore.All("config") { if err != nil { break }; fmt.Println(entry.Key, entry.Value) }`
|
|
func (scopedStore *ScopedStore) All(group string) iter.Seq2[KeyValue, error] {
|
|
return scopedStore.store.All(scopedStore.namespacedGroup(group))
|
|
}
|
|
|
|
// Usage example: `for entry, err := range scopedStore.AllSeq("config") { if err != nil { break }; fmt.Println(entry.Key, entry.Value) }`
|
|
func (scopedStore *ScopedStore) AllSeq(group string) iter.Seq2[KeyValue, error] {
|
|
return scopedStore.All(group)
|
|
}
|
|
|
|
// Usage example: `keyCount, err := scopedStore.Count("config")`
|
|
func (scopedStore *ScopedStore) Count(group string) (int, error) {
|
|
return scopedStore.store.Count(scopedStore.namespacedGroup(group))
|
|
}
|
|
|
|
// Usage example: `keyCount, err := scopedStore.CountAll("config")`
|
|
// Usage example: `keyCount, err := scopedStore.CountAll()`
|
|
func (scopedStore *ScopedStore) CountAll(groupPrefix ...string) (int, error) {
|
|
return scopedStore.store.CountAll(scopedStore.namespacedGroup(firstOrEmptyString(groupPrefix)))
|
|
}
|
|
|
|
// Usage example: `groupNames, err := scopedStore.Groups("config")`
|
|
// Usage example: `groupNames, err := scopedStore.Groups()`
|
|
func (scopedStore *ScopedStore) Groups(groupPrefix ...string) ([]string, error) {
|
|
groupNames, err := scopedStore.store.Groups(scopedStore.namespacedGroup(firstOrEmptyString(groupPrefix)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i, groupName := range groupNames {
|
|
groupNames[i] = scopedStore.trimNamespacePrefix(groupName)
|
|
}
|
|
return groupNames, nil
|
|
}
|
|
|
|
// Usage example: `for groupName, err := range scopedStore.GroupsSeq("config") { if err != nil { break }; fmt.Println(groupName) }`
|
|
// Usage example: `for groupName, err := range scopedStore.GroupsSeq() { if err != nil { break }; fmt.Println(groupName) }`
|
|
func (scopedStore *ScopedStore) GroupsSeq(groupPrefix ...string) iter.Seq2[string, error] {
|
|
return func(yield func(string, error) bool) {
|
|
namespacePrefix := scopedStore.namespacePrefix()
|
|
for groupName, err := range scopedStore.store.GroupsSeq(scopedStore.namespacedGroup(firstOrEmptyString(groupPrefix))) {
|
|
if err != nil {
|
|
if !yield("", err) {
|
|
return
|
|
}
|
|
continue
|
|
}
|
|
if !yield(core.TrimPrefix(groupName, namespacePrefix), nil) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Usage example: `renderedTemplate, err := scopedStore.Render("Hello {{ .name }}", "user")`
|
|
func (scopedStore *ScopedStore) Render(templateSource, group string) (string, error) {
|
|
return scopedStore.store.Render(templateSource, scopedStore.namespacedGroup(group))
|
|
}
|
|
|
|
// Usage example: `parts, err := scopedStore.GetSplit("config", "hosts", ","); if err != nil { return }; for part := range parts { fmt.Println(part) }`
|
|
func (scopedStore *ScopedStore) GetSplit(group, key, separator string) (iter.Seq[string], error) {
|
|
return scopedStore.store.GetSplit(scopedStore.namespacedGroup(group), key, separator)
|
|
}
|
|
|
|
// Usage example: `fields, err := scopedStore.GetFields("config", "flags"); if err != nil { return }; for field := range fields { fmt.Println(field) }`
|
|
func (scopedStore *ScopedStore) GetFields(group, key string) (iter.Seq[string], error) {
|
|
return scopedStore.store.GetFields(scopedStore.namespacedGroup(group), key)
|
|
}
|
|
|
|
// Usage example: `removedRows, err := scopedStore.PurgeExpired(); if err != nil { return }; fmt.Println(removedRows)`
|
|
func (scopedStore *ScopedStore) PurgeExpired() (int64, error) {
|
|
if scopedStore == nil {
|
|
return 0, core.E("store.ScopedStore.PurgeExpired", "scoped store is nil", nil)
|
|
}
|
|
if err := scopedStore.store.ensureReady("store.ScopedStore.PurgeExpired"); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
removedRows, err := purgeExpiredMatchingGroupPrefix(scopedStore.store.sqliteDatabase, scopedStore.namespacePrefix())
|
|
if err != nil {
|
|
return 0, core.E("store.ScopedStore.PurgeExpired", "delete expired rows", err)
|
|
}
|
|
return removedRows, nil
|
|
}
|
|
|
|
// Usage example: `events := scopedStore.Watch("config")`
|
|
// Usage example: `events := scopedStore.Watch("*")`
|
|
// The returned events always use namespace-local group names, so a write to
|
|
// `tenant-a:config` is delivered as `config`.
|
|
func (scopedStore *ScopedStore) Watch(group string) <-chan Event {
|
|
if scopedStore == nil || scopedStore.store == nil {
|
|
return closedEventChannel()
|
|
}
|
|
|
|
sourceGroup := scopedStore.namespacedGroup(group)
|
|
if group == "*" {
|
|
sourceGroup = "*"
|
|
}
|
|
|
|
sourceEvents := scopedStore.store.Watch(sourceGroup)
|
|
localEvents := make(chan Event, watcherEventBufferCapacity)
|
|
done := make(chan struct{})
|
|
localEventsPointer := channelPointer(localEvents)
|
|
|
|
scopedStore.watcherLock.Lock()
|
|
if scopedStore.watcherBridges == nil {
|
|
scopedStore.watcherBridges = make(map[uintptr]scopedWatcherBridge)
|
|
}
|
|
scopedStore.watcherBridges[localEventsPointer] = scopedWatcherBridge{
|
|
sourceGroup: sourceGroup,
|
|
sourceEvents: sourceEvents,
|
|
done: done,
|
|
}
|
|
scopedStore.watcherLock.Unlock()
|
|
|
|
go func() {
|
|
defer close(localEvents)
|
|
defer scopedStore.removeWatcherBridge(localEventsPointer)
|
|
|
|
for {
|
|
select {
|
|
case <-done:
|
|
return
|
|
case event, ok := <-sourceEvents:
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
localEvent, allowed := scopedStore.localiseWatchedEvent(event)
|
|
if !allowed {
|
|
continue
|
|
}
|
|
|
|
select {
|
|
case localEvents <- localEvent:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
return localEvents
|
|
}
|
|
|
|
// Usage example: `events := scopedStore.Watch("config"); scopedStore.Unwatch("config", events)`
|
|
// Usage example: `events := scopedStore.Watch("*"); scopedStore.Unwatch("*", events)`
|
|
func (scopedStore *ScopedStore) Unwatch(group string, events <-chan Event) {
|
|
if scopedStore == nil || events == nil {
|
|
return
|
|
}
|
|
|
|
scopedStore.watcherLock.Lock()
|
|
watcherBridge, ok := scopedStore.watcherBridges[channelPointer(events)]
|
|
if ok {
|
|
delete(scopedStore.watcherBridges, channelPointer(events))
|
|
}
|
|
scopedStore.watcherLock.Unlock()
|
|
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
close(watcherBridge.done)
|
|
scopedStore.store.Unwatch(watcherBridge.sourceGroup, watcherBridge.sourceEvents)
|
|
}
|
|
|
|
func (scopedStore *ScopedStore) removeWatcherBridge(pointer uintptr) {
|
|
if scopedStore == nil {
|
|
return
|
|
}
|
|
|
|
scopedStore.watcherLock.Lock()
|
|
delete(scopedStore.watcherBridges, pointer)
|
|
scopedStore.watcherLock.Unlock()
|
|
}
|
|
|
|
func (scopedStore *ScopedStore) localiseWatchedEvent(event Event) (Event, bool) {
|
|
if scopedStore == nil {
|
|
return Event{}, false
|
|
}
|
|
|
|
namespacePrefix := scopedStore.namespacePrefix()
|
|
if event.Group == "*" {
|
|
return event, true
|
|
}
|
|
if !core.HasPrefix(event.Group, namespacePrefix) {
|
|
return Event{}, false
|
|
}
|
|
|
|
event.Group = core.TrimPrefix(event.Group, namespacePrefix)
|
|
return event, true
|
|
}
|
|
|
|
// Usage example: `unregister := scopedStore.OnChange(func(event store.Event) { fmt.Println(event.Group, event.Key, event.Value) })`
|
|
// The callback receives the namespace-local group name, so a write to
|
|
// `tenant-a:config` is reported as `config`.
|
|
func (scopedStore *ScopedStore) OnChange(callback func(Event)) func() {
|
|
if scopedStore == nil || callback == nil {
|
|
return func() {}
|
|
}
|
|
if scopedStore.store == nil {
|
|
return func() {}
|
|
}
|
|
|
|
namespacePrefix := scopedStore.namespacePrefix()
|
|
return scopedStore.store.OnChange(func(event Event) {
|
|
if !core.HasPrefix(event.Group, namespacePrefix) {
|
|
return
|
|
}
|
|
event.Group = core.TrimPrefix(event.Group, namespacePrefix)
|
|
callback(event)
|
|
})
|
|
}
|
|
|
|
// ScopedStoreTransaction exposes namespace-local transaction helpers so callers
|
|
// can work inside a scoped namespace without manually prefixing group names.
|
|
//
|
|
// Usage example: `err := scopedStore.Transaction(func(transaction *store.ScopedStoreTransaction) error { return transaction.Set("theme", "dark") })`
|
|
type ScopedStoreTransaction struct {
|
|
scopedStore *ScopedStore
|
|
storeTransaction *StoreTransaction
|
|
}
|
|
|
|
// Usage example: `err := scopedStore.Transaction(func(transaction *store.ScopedStoreTransaction) error { return transaction.Set("theme", "dark") })`
|
|
func (scopedStore *ScopedStore) Transaction(operation func(*ScopedStoreTransaction) error) error {
|
|
if scopedStore == nil {
|
|
return core.E("store.ScopedStore.Transaction", "scoped store is nil", nil)
|
|
}
|
|
if operation == nil {
|
|
return core.E("store.ScopedStore.Transaction", "operation is nil", nil)
|
|
}
|
|
|
|
return scopedStore.store.Transaction(func(storeTransaction *StoreTransaction) error {
|
|
return operation(&ScopedStoreTransaction{
|
|
scopedStore: scopedStore,
|
|
storeTransaction: storeTransaction,
|
|
})
|
|
})
|
|
}
|
|
|
|
func (scopedStoreTransaction *ScopedStoreTransaction) ensureReady(operation string) error {
|
|
if scopedStoreTransaction == nil {
|
|
return core.E(operation, "scoped transaction is nil", nil)
|
|
}
|
|
if scopedStoreTransaction.scopedStore == nil {
|
|
return core.E(operation, "scoped transaction store is nil", nil)
|
|
}
|
|
if scopedStoreTransaction.storeTransaction == nil {
|
|
return core.E(operation, "scoped transaction database is nil", nil)
|
|
}
|
|
if err := scopedStoreTransaction.scopedStore.store.ensureReady(operation); err != nil {
|
|
return err
|
|
}
|
|
return scopedStoreTransaction.storeTransaction.ensureReady(operation)
|
|
}
|
|
|
|
// Usage example: `colourValue, err := scopedStoreTransaction.Get("colour")`
|
|
func (scopedStoreTransaction *ScopedStoreTransaction) Get(key string) (string, error) {
|
|
if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.Get"); err != nil {
|
|
return "", err
|
|
}
|
|
return scopedStoreTransaction.storeTransaction.Get(
|
|
scopedStoreTransaction.scopedStore.namespacedGroup(scopedStoreTransaction.scopedStore.defaultGroup()),
|
|
key,
|
|
)
|
|
}
|
|
|
|
// Usage example: `colourValue, err := scopedStoreTransaction.GetFrom("config", "colour")`
|
|
func (scopedStoreTransaction *ScopedStoreTransaction) GetFrom(group, key string) (string, error) {
|
|
if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.GetFrom"); err != nil {
|
|
return "", err
|
|
}
|
|
return scopedStoreTransaction.storeTransaction.Get(scopedStoreTransaction.scopedStore.namespacedGroup(group), key)
|
|
}
|
|
|
|
// Usage example: `if err := scopedStoreTransaction.Set("theme", "dark"); err != nil { return err }`
|
|
func (scopedStoreTransaction *ScopedStoreTransaction) Set(key, value string) error {
|
|
if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.Set"); err != nil {
|
|
return err
|
|
}
|
|
defaultGroup := scopedStoreTransaction.scopedStore.defaultGroup()
|
|
if err := scopedStoreTransaction.checkQuota("store.ScopedStoreTransaction.Set", defaultGroup, key); err != nil {
|
|
return err
|
|
}
|
|
return scopedStoreTransaction.storeTransaction.Set(
|
|
scopedStoreTransaction.scopedStore.namespacedGroup(defaultGroup),
|
|
key,
|
|
value,
|
|
)
|
|
}
|
|
|
|
// Usage example: `if err := scopedStoreTransaction.SetIn("config", "colour", "blue"); err != nil { return err }`
|
|
func (scopedStoreTransaction *ScopedStoreTransaction) SetIn(group, key, value string) error {
|
|
if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.SetIn"); err != nil {
|
|
return err
|
|
}
|
|
if err := scopedStoreTransaction.checkQuota("store.ScopedStoreTransaction.SetIn", group, key); err != nil {
|
|
return err
|
|
}
|
|
return scopedStoreTransaction.storeTransaction.Set(scopedStoreTransaction.scopedStore.namespacedGroup(group), key, value)
|
|
}
|
|
|
|
// Usage example: `if err := scopedStoreTransaction.SetWithTTL("sessions", "token", "abc123", time.Hour); err != nil { return err }`
|
|
func (scopedStoreTransaction *ScopedStoreTransaction) SetWithTTL(group, key, value string, timeToLive time.Duration) error {
|
|
if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.SetWithTTL"); err != nil {
|
|
return err
|
|
}
|
|
if err := scopedStoreTransaction.checkQuota("store.ScopedStoreTransaction.SetWithTTL", group, key); err != nil {
|
|
return err
|
|
}
|
|
return scopedStoreTransaction.storeTransaction.SetWithTTL(scopedStoreTransaction.scopedStore.namespacedGroup(group), key, value, timeToLive)
|
|
}
|
|
|
|
// Usage example: `if err := scopedStoreTransaction.Delete("config", "colour"); err != nil { return err }`
|
|
func (scopedStoreTransaction *ScopedStoreTransaction) Delete(group, key string) error {
|
|
if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.Delete"); err != nil {
|
|
return err
|
|
}
|
|
return scopedStoreTransaction.storeTransaction.Delete(scopedStoreTransaction.scopedStore.namespacedGroup(group), key)
|
|
}
|
|
|
|
// Usage example: `if err := scopedStoreTransaction.DeleteGroup("cache"); err != nil { return err }`
|
|
func (scopedStoreTransaction *ScopedStoreTransaction) DeleteGroup(group string) error {
|
|
if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.DeleteGroup"); err != nil {
|
|
return err
|
|
}
|
|
return scopedStoreTransaction.storeTransaction.DeleteGroup(scopedStoreTransaction.scopedStore.namespacedGroup(group))
|
|
}
|
|
|
|
// Usage example: `if err := scopedStoreTransaction.DeletePrefix("cache"); err != nil { return err }`
|
|
// Usage example: `if err := scopedStoreTransaction.DeletePrefix(""); err != nil { return err }`
|
|
func (scopedStoreTransaction *ScopedStoreTransaction) DeletePrefix(groupPrefix string) error {
|
|
if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.DeletePrefix"); err != nil {
|
|
return err
|
|
}
|
|
return scopedStoreTransaction.storeTransaction.DeletePrefix(scopedStoreTransaction.scopedStore.namespacedGroup(groupPrefix))
|
|
}
|
|
|
|
// Usage example: `colourEntries, err := scopedStoreTransaction.GetAll("config")`
|
|
func (scopedStoreTransaction *ScopedStoreTransaction) GetAll(group string) (map[string]string, error) {
|
|
if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.GetAll"); err != nil {
|
|
return nil, err
|
|
}
|
|
return scopedStoreTransaction.storeTransaction.GetAll(scopedStoreTransaction.scopedStore.namespacedGroup(group))
|
|
}
|
|
|
|
// Usage example: `page, err := scopedStoreTransaction.GetPage("config", 0, 25); if err != nil { return }; for _, entry := range page { fmt.Println(entry.Key, entry.Value) }`
|
|
func (scopedStoreTransaction *ScopedStoreTransaction) GetPage(group string, offset, limit int) ([]KeyValue, error) {
|
|
if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.GetPage"); err != nil {
|
|
return nil, err
|
|
}
|
|
return scopedStoreTransaction.storeTransaction.GetPage(scopedStoreTransaction.scopedStore.namespacedGroup(group), offset, limit)
|
|
}
|
|
|
|
// Usage example: `for entry, err := range scopedStoreTransaction.All("config") { if err != nil { break }; fmt.Println(entry.Key, entry.Value) }`
|
|
func (scopedStoreTransaction *ScopedStoreTransaction) All(group string) iter.Seq2[KeyValue, error] {
|
|
if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.All"); err != nil {
|
|
return func(yield func(KeyValue, error) bool) {
|
|
yield(KeyValue{}, err)
|
|
}
|
|
}
|
|
return scopedStoreTransaction.storeTransaction.All(scopedStoreTransaction.scopedStore.namespacedGroup(group))
|
|
}
|
|
|
|
// Usage example: `for entry, err := range scopedStoreTransaction.AllSeq("config") { if err != nil { break }; fmt.Println(entry.Key, entry.Value) }`
|
|
func (scopedStoreTransaction *ScopedStoreTransaction) AllSeq(group string) iter.Seq2[KeyValue, error] {
|
|
return scopedStoreTransaction.All(group)
|
|
}
|
|
|
|
// Usage example: `keyCount, err := scopedStoreTransaction.Count("config")`
|
|
func (scopedStoreTransaction *ScopedStoreTransaction) Count(group string) (int, error) {
|
|
if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.Count"); err != nil {
|
|
return 0, err
|
|
}
|
|
return scopedStoreTransaction.storeTransaction.Count(scopedStoreTransaction.scopedStore.namespacedGroup(group))
|
|
}
|
|
|
|
// Usage example: `keyCount, err := scopedStoreTransaction.CountAll("config")`
|
|
// Usage example: `keyCount, err := scopedStoreTransaction.CountAll()`
|
|
func (scopedStoreTransaction *ScopedStoreTransaction) CountAll(groupPrefix ...string) (int, error) {
|
|
if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.CountAll"); err != nil {
|
|
return 0, err
|
|
}
|
|
return scopedStoreTransaction.storeTransaction.CountAll(scopedStoreTransaction.scopedStore.namespacedGroup(firstOrEmptyString(groupPrefix)))
|
|
}
|
|
|
|
// Usage example: `groupNames, err := scopedStoreTransaction.Groups("config")`
|
|
// Usage example: `groupNames, err := scopedStoreTransaction.Groups()`
|
|
func (scopedStoreTransaction *ScopedStoreTransaction) Groups(groupPrefix ...string) ([]string, error) {
|
|
if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.Groups"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
groupNames, err := scopedStoreTransaction.storeTransaction.Groups(scopedStoreTransaction.scopedStore.namespacedGroup(firstOrEmptyString(groupPrefix)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i, groupName := range groupNames {
|
|
groupNames[i] = scopedStoreTransaction.scopedStore.trimNamespacePrefix(groupName)
|
|
}
|
|
return groupNames, nil
|
|
}
|
|
|
|
// Usage example: `for groupName, err := range scopedStoreTransaction.GroupsSeq("config") { if err != nil { break }; fmt.Println(groupName) }`
|
|
// Usage example: `for groupName, err := range scopedStoreTransaction.GroupsSeq() { if err != nil { break }; fmt.Println(groupName) }`
|
|
func (scopedStoreTransaction *ScopedStoreTransaction) GroupsSeq(groupPrefix ...string) iter.Seq2[string, error] {
|
|
return func(yield func(string, error) bool) {
|
|
if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.GroupsSeq"); err != nil {
|
|
yield("", err)
|
|
return
|
|
}
|
|
|
|
namespacePrefix := scopedStoreTransaction.scopedStore.namespacePrefix()
|
|
for groupName, err := range scopedStoreTransaction.storeTransaction.GroupsSeq(scopedStoreTransaction.scopedStore.namespacedGroup(firstOrEmptyString(groupPrefix))) {
|
|
if err != nil {
|
|
if !yield("", err) {
|
|
return
|
|
}
|
|
continue
|
|
}
|
|
if !yield(core.TrimPrefix(groupName, namespacePrefix), nil) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Usage example: `renderedTemplate, err := scopedStoreTransaction.Render("Hello {{ .name }}", "user")`
|
|
func (scopedStoreTransaction *ScopedStoreTransaction) Render(templateSource, group string) (string, error) {
|
|
if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.Render"); err != nil {
|
|
return "", err
|
|
}
|
|
return scopedStoreTransaction.storeTransaction.Render(templateSource, scopedStoreTransaction.scopedStore.namespacedGroup(group))
|
|
}
|
|
|
|
// Usage example: `parts, err := scopedStoreTransaction.GetSplit("config", "hosts", ","); if err != nil { return }; for part := range parts { fmt.Println(part) }`
|
|
func (scopedStoreTransaction *ScopedStoreTransaction) GetSplit(group, key, separator string) (iter.Seq[string], error) {
|
|
if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.GetSplit"); err != nil {
|
|
return nil, err
|
|
}
|
|
return scopedStoreTransaction.storeTransaction.GetSplit(scopedStoreTransaction.scopedStore.namespacedGroup(group), key, separator)
|
|
}
|
|
|
|
// Usage example: `fields, err := scopedStoreTransaction.GetFields("config", "flags"); if err != nil { return }; for field := range fields { fmt.Println(field) }`
|
|
func (scopedStoreTransaction *ScopedStoreTransaction) GetFields(group, key string) (iter.Seq[string], error) {
|
|
if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.GetFields"); err != nil {
|
|
return nil, err
|
|
}
|
|
return scopedStoreTransaction.storeTransaction.GetFields(scopedStoreTransaction.scopedStore.namespacedGroup(group), key)
|
|
}
|
|
|
|
// Usage example: `removedRows, err := scopedStoreTransaction.PurgeExpired(); if err != nil { return err }; fmt.Println(removedRows)`
|
|
func (scopedStoreTransaction *ScopedStoreTransaction) PurgeExpired() (int64, error) {
|
|
if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.PurgeExpired"); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
removedRows, err := purgeExpiredMatchingGroupPrefix(scopedStoreTransaction.storeTransaction.sqliteTransaction, scopedStoreTransaction.scopedStore.namespacePrefix())
|
|
if err != nil {
|
|
return 0, core.E("store.ScopedStoreTransaction.PurgeExpired", "delete expired rows", err)
|
|
}
|
|
return removedRows, nil
|
|
}
|
|
|
|
func (scopedStoreTransaction *ScopedStoreTransaction) checkQuota(operation, group, key string) error {
|
|
return enforceQuota(
|
|
operation,
|
|
group,
|
|
key,
|
|
scopedStoreTransaction.scopedStore.namespacePrefix(),
|
|
scopedStoreTransaction.scopedStore.namespacedGroup(group),
|
|
scopedStoreTransaction.scopedStore.MaxKeys,
|
|
scopedStoreTransaction.scopedStore.MaxGroups,
|
|
scopedStoreTransaction.storeTransaction.sqliteTransaction,
|
|
scopedStoreTransaction.storeTransaction,
|
|
)
|
|
}
|
|
|
|
// checkQuota("store.ScopedStore.Set", "config", "colour") returns nil when the
|
|
// namespace still has quota available and QuotaExceededError when a new key or
|
|
// group would exceed the configured limit. Existing keys are treated as
|
|
// upserts and do not consume quota.
|
|
func (scopedStore *ScopedStore) checkQuota(operation, group, key string) error {
|
|
return enforceQuota(
|
|
operation,
|
|
group,
|
|
key,
|
|
scopedStore.namespacePrefix(),
|
|
scopedStore.namespacedGroup(group),
|
|
scopedStore.MaxKeys,
|
|
scopedStore.MaxGroups,
|
|
scopedStore.store.sqliteDatabase,
|
|
scopedStore.store,
|
|
)
|
|
}
|
|
|
|
type quotaCounter interface {
|
|
CountAll(groupPrefix string) (int, error)
|
|
Count(group string) (int, error)
|
|
GroupsSeq(groupPrefix ...string) iter.Seq2[string, error]
|
|
}
|
|
|
|
func enforceQuota(
|
|
operation, group, key, namespacePrefix, namespacedGroup string,
|
|
maxKeys, maxGroups int,
|
|
queryable keyExistenceQuery,
|
|
counter quotaCounter,
|
|
) error {
|
|
if maxKeys == 0 && maxGroups == 0 {
|
|
return nil
|
|
}
|
|
|
|
exists, err := liveEntryExists(queryable, namespacedGroup, key)
|
|
if err != nil {
|
|
// A database error occurred, not just a "not found" result.
|
|
return core.E(operation, "quota check", err)
|
|
}
|
|
if exists {
|
|
// Key exists - this is an upsert, no quota check needed.
|
|
return nil
|
|
}
|
|
|
|
if maxKeys > 0 {
|
|
keyCount, err := counter.CountAll(namespacePrefix)
|
|
if err != nil {
|
|
return core.E(operation, "quota check", err)
|
|
}
|
|
if keyCount >= maxKeys {
|
|
return core.E(operation, core.Sprintf("key limit (%d)", maxKeys), QuotaExceededError)
|
|
}
|
|
}
|
|
|
|
if maxGroups > 0 {
|
|
existingGroupCount, err := counter.Count(namespacedGroup)
|
|
if err != nil {
|
|
return core.E(operation, "quota check", err)
|
|
}
|
|
if existingGroupCount == 0 {
|
|
knownGroupCount := 0
|
|
for _, iterationErr := range counter.GroupsSeq(namespacePrefix) {
|
|
if iterationErr != nil {
|
|
return core.E(operation, "quota check", iterationErr)
|
|
}
|
|
knownGroupCount++
|
|
}
|
|
if knownGroupCount >= maxGroups {
|
|
return core.E(operation, core.Sprintf("group limit (%d)", maxGroups), QuotaExceededError)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func liveEntryExists(queryable keyExistenceQuery, group, key string) (bool, error) {
|
|
var exists int
|
|
err := queryable.QueryRow(
|
|
"SELECT 1 FROM "+entriesTableName+" WHERE "+entryGroupColumn+" = ? AND "+entryKeyColumn+" = ? AND (expires_at IS NULL OR expires_at > ?) LIMIT 1",
|
|
group,
|
|
key,
|
|
time.Now().UnixMilli(),
|
|
).Scan(&exists)
|
|
if err == nil {
|
|
return true, nil
|
|
}
|
|
if err == sql.ErrNoRows {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
type keyExistenceQuery interface {
|
|
QueryRow(query string, args ...any) *sql.Row
|
|
}
|