2026-02-20 08:19:11 +00:00
package store
import (
2026-02-23 05:21:39 +00:00
"iter"
2026-02-20 08:19:11 +00:00
"regexp"
"time"
2026-03-16 21:29:42 +00:00
2026-03-26 13:58:50 +00:00
core "dappco.re/go/core"
2026-02-20 08:19:11 +00:00
)
2026-03-30 17:37:50 +00:00
// validNamespace.MatchString("tenant-a") is true; validNamespace.MatchString("tenant_a") is false.
2026-02-20 08:19:11 +00:00
var validNamespace = regexp . MustCompile ( ` ^[a-zA-Z0-9-]+$ ` )
2026-03-30 20:46:43 +00:00
const defaultScopedGroupName = "default"
2026-03-30 19:06:00 +00:00
// QuotaConfig sets per-namespace key and group limits.
2026-03-26 19:17:11 +00:00
// Usage example: `quota := store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}`
2026-02-20 08:19:11 +00:00
type QuotaConfig struct {
2026-03-30 16:54:02 +00:00
// 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
2026-02-20 08:19:11 +00:00
}
2026-04-03 04:44:45 +00:00
// Usage example: `scopedStore := store.NewScoped(storeInstance, "tenant-a"); if scopedStore == nil { return }; if err := scopedStore.Set("config", "colour", "blue"); err != nil { return }`
2026-02-20 08:19:11 +00:00
type ScopedStore struct {
2026-04-03 05:32:46 +00:00
store * Store
namespace string
MaxKeys int
MaxGroups int
2026-02-20 08:19:11 +00:00
}
2026-03-30 19:06:00 +00:00
// NewScoped validates a namespace and prefixes groups with namespace + ":".
2026-04-03 04:44:45 +00:00
// Usage example: `scopedStore := store.NewScoped(storeInstance, "tenant-a")`
func NewScoped ( storeInstance * Store , namespace string ) * ScopedStore {
2026-03-30 19:29:48 +00:00
if storeInstance == nil {
2026-04-03 04:44:45 +00:00
return nil
2026-03-30 19:29:48 +00:00
}
2026-02-20 08:19:11 +00:00
if ! validNamespace . MatchString ( namespace ) {
2026-04-03 04:44:45 +00:00
return nil
2026-02-20 08:19:11 +00:00
}
2026-04-03 05:32:46 +00:00
scopedStore := & ScopedStore { store : storeInstance , namespace : namespace }
2026-04-03 04:44:45 +00:00
return scopedStore
2026-02-20 08:19:11 +00:00
}
2026-03-30 19:06:00 +00:00
// NewScopedWithQuota adds per-namespace key and group limits.
2026-03-30 16:41:56 +00:00
// Usage example: `scopedStore, err := store.NewScopedWithQuota(storeInstance, "tenant-a", store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}); if err != nil { return }`
2026-03-30 15:06:20 +00:00
func NewScopedWithQuota ( storeInstance * Store , namespace string , quota QuotaConfig ) ( * ScopedStore , error ) {
2026-04-03 04:44:45 +00:00
scopedStore := NewScoped ( storeInstance , namespace )
if scopedStore == nil {
if storeInstance == nil {
return nil , core . E ( "store.NewScopedWithQuota" , "store instance is nil" , nil )
}
return nil , core . E ( "store.NewScopedWithQuota" , core . Sprintf ( "namespace %q is invalid; use names like %q or %q" , namespace , "tenant-a" , "tenant-42" ) , nil )
2026-02-20 08:19:11 +00:00
}
2026-03-30 20:03:19 +00:00
if quota . MaxKeys < 0 || quota . MaxGroups < 0 {
return nil , core . E (
"store.NewScopedWithQuota" ,
core . Sprintf ( "quota values must be zero or positive; got MaxKeys=%d MaxGroups=%d" , quota . MaxKeys , quota . MaxGroups ) ,
nil ,
)
}
2026-03-30 20:46:43 +00:00
scopedStore . MaxKeys = quota . MaxKeys
scopedStore . MaxGroups = quota . MaxGroups
2026-03-30 14:54:34 +00:00
return scopedStore , nil
2026-02-20 08:19:11 +00:00
}
2026-03-30 15:02:28 +00:00
func ( scopedStore * ScopedStore ) namespacedGroup ( group string ) string {
return scopedStore . namespace + ":" + group
2026-02-20 08:19:11 +00:00
}
2026-03-30 19:14:09 +00:00
func ( scopedStore * ScopedStore ) namespacePrefix ( ) string {
return scopedStore . namespace + ":"
}
func ( scopedStore * ScopedStore ) trimNamespacePrefix ( groupName string ) string {
return core . TrimPrefix ( groupName , scopedStore . namespacePrefix ( ) )
}
2026-03-30 19:06:00 +00:00
// Namespace returns the namespace string.
2026-04-03 04:44:45 +00:00
// Usage example: `scopedStore := store.NewScoped(storeInstance, "tenant-a"); if scopedStore == nil { return }; namespace := scopedStore.Namespace(); fmt.Println(namespace)`
2026-03-30 15:02:28 +00:00
func ( scopedStore * ScopedStore ) Namespace ( ) string {
return scopedStore . namespace
2026-02-20 08:19:11 +00:00
}
2026-03-30 20:46:43 +00:00
// Usage example: `colourValue, err := scopedStore.Get("colour")`
2026-04-03 05:45:24 +00:00
func ( scopedStore * ScopedStore ) Get ( key string ) ( string , error ) {
return scopedStore . GetFrom ( defaultScopedGroupName , key )
2026-04-03 05:13:46 +00:00
}
// Usage example: `colourValue, err := scopedStore.GetFrom("config", "colour")`
func ( scopedStore * ScopedStore ) GetFrom ( group , key string ) ( string , error ) {
2026-04-03 05:32:46 +00:00
return scopedStore . store . Get ( scopedStore . namespacedGroup ( group ) , key )
2026-02-20 08:19:11 +00:00
}
2026-03-30 20:46:43 +00:00
// Usage example: `if err := scopedStore.Set("colour", "blue"); err != nil { return }`
2026-04-03 05:45:24 +00:00
func ( scopedStore * ScopedStore ) Set ( key , value string ) error {
return scopedStore . SetIn ( defaultScopedGroupName , key , value )
2026-04-03 05:13:46 +00:00
}
// 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 {
2026-02-20 08:19:11 +00:00
return err
}
2026-04-03 05:32:46 +00:00
return scopedStore . store . Set ( scopedStore . namespacedGroup ( group ) , key , value )
2026-02-20 08:19:11 +00:00
}
2026-03-30 16:41:56 +00:00
// Usage example: `if err := scopedStore.SetWithTTL("sessions", "token", "abc123", time.Hour); err != nil { return }`
2026-03-30 18:37:07 +00:00
func ( scopedStore * ScopedStore ) SetWithTTL ( group , key , value string , timeToLive time . Duration ) error {
2026-03-30 16:27:54 +00:00
if err := scopedStore . checkQuota ( "store.ScopedStore.SetWithTTL" , group , key ) ; err != nil {
2026-02-20 08:19:11 +00:00
return err
}
2026-04-03 05:32:46 +00:00
return scopedStore . store . SetWithTTL ( scopedStore . namespacedGroup ( group ) , key , value , timeToLive )
2026-02-20 08:19:11 +00:00
}
2026-03-30 18:49:17 +00:00
// Usage example: `if err := scopedStore.Delete("config", "colour"); err != nil { return }`
2026-03-30 15:02:28 +00:00
func ( scopedStore * ScopedStore ) Delete ( group , key string ) error {
2026-04-03 05:32:46 +00:00
return scopedStore . store . Delete ( scopedStore . namespacedGroup ( group ) , key )
2026-02-20 08:19:11 +00:00
}
2026-03-30 16:41:56 +00:00
// Usage example: `if err := scopedStore.DeleteGroup("cache"); err != nil { return }`
2026-03-30 15:02:28 +00:00
func ( scopedStore * ScopedStore ) DeleteGroup ( group string ) error {
2026-04-03 05:32:46 +00:00
return scopedStore . store . DeleteGroup ( scopedStore . namespacedGroup ( group ) )
2026-02-20 08:19:11 +00:00
}
2026-03-30 18:49:17 +00:00
// Usage example: `colourEntries, err := scopedStore.GetAll("config")`
2026-03-30 15:02:28 +00:00
func ( scopedStore * ScopedStore ) GetAll ( group string ) ( map [ string ] string , error ) {
2026-04-03 05:32:46 +00:00
return scopedStore . store . GetAll ( scopedStore . namespacedGroup ( group ) )
2026-02-20 08:19:11 +00:00
}
2026-03-30 17:32:09 +00:00
// Usage example: `for entry, err := range scopedStore.All("config") { if err != nil { break }; fmt.Println(entry.Key, entry.Value) }`
2026-03-30 15:02:28 +00:00
func ( scopedStore * ScopedStore ) All ( group string ) iter . Seq2 [ KeyValue , error ] {
2026-04-03 05:32:46 +00:00
return scopedStore . store . All ( scopedStore . namespacedGroup ( group ) )
2026-02-23 05:21:39 +00:00
}
2026-04-03 05:13:46 +00:00
// 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 ] {
2026-04-03 05:32:46 +00:00
return scopedStore . store . AllSeq ( scopedStore . namespacedGroup ( group ) )
2026-04-03 05:13:46 +00:00
}
2026-03-30 16:13:55 +00:00
// Usage example: `keyCount, err := scopedStore.Count("config")`
2026-03-30 15:02:28 +00:00
func ( scopedStore * ScopedStore ) Count ( group string ) ( int , error ) {
2026-04-03 05:32:46 +00:00
return scopedStore . store . Count ( scopedStore . namespacedGroup ( group ) )
2026-02-20 08:19:11 +00:00
}
2026-03-30 19:14:09 +00:00
// Usage example: `keyCount, err := scopedStore.CountAll("config")`
2026-03-30 20:46:43 +00:00
// Usage example: `keyCount, err := scopedStore.CountAll()`
func ( scopedStore * ScopedStore ) CountAll ( groupPrefix ... string ) ( int , error ) {
2026-04-03 05:40:53 +00:00
return scopedStore . store . CountAll ( scopedStore . namespacedGroup ( firstOrEmptyString ( groupPrefix ) ) )
2026-03-30 19:14:09 +00:00
}
// Usage example: `groupNames, err := scopedStore.Groups("config")`
2026-03-30 20:46:43 +00:00
// Usage example: `groupNames, err := scopedStore.Groups()`
func ( scopedStore * ScopedStore ) Groups ( groupPrefix ... string ) ( [ ] string , error ) {
2026-04-03 05:40:53 +00:00
groupNames , err := scopedStore . store . Groups ( scopedStore . namespacedGroup ( firstOrEmptyString ( groupPrefix ) ) )
2026-03-30 19:14:09 +00:00
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) }`
2026-03-30 20:46:43 +00:00
// 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 ] {
2026-03-30 19:14:09 +00:00
return func ( yield func ( string , error ) bool ) {
namespacePrefix := scopedStore . namespacePrefix ( )
2026-04-03 05:40:53 +00:00
for groupName , err := range scopedStore . store . GroupsSeq ( scopedStore . namespacedGroup ( firstOrEmptyString ( groupPrefix ) ) ) {
2026-03-30 19:14:09 +00:00
if err != nil {
if ! yield ( "" , err ) {
return
}
continue
}
if ! yield ( core . TrimPrefix ( groupName , namespacePrefix ) , nil ) {
return
}
}
}
}
2026-03-30 16:13:55 +00:00
// Usage example: `renderedTemplate, err := scopedStore.Render("Hello {{ .name }}", "user")`
2026-03-30 15:02:28 +00:00
func ( scopedStore * ScopedStore ) Render ( templateSource , group string ) ( string , error ) {
2026-04-03 05:32:46 +00:00
return scopedStore . store . Render ( templateSource , scopedStore . namespacedGroup ( group ) )
2026-02-20 08:19:11 +00:00
}
2026-03-30 19:14:09 +00:00
// 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 ) {
2026-04-03 05:32:46 +00:00
return scopedStore . store . GetSplit ( scopedStore . namespacedGroup ( group ) , key , separator )
2026-03-30 19:14:09 +00:00
}
// 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 ) {
2026-04-03 05:32:46 +00:00
return scopedStore . store . GetFields ( scopedStore . namespacedGroup ( group ) , key )
2026-03-30 19:14:09 +00:00
}
// Usage example: `removedRows, err := scopedStore.PurgeExpired(); if err != nil { return }; fmt.Println(removedRows)`
func ( scopedStore * ScopedStore ) PurgeExpired ( ) ( int64 , error ) {
2026-04-03 05:32:46 +00:00
removedRows , err := scopedStore . store . purgeExpiredMatchingGroupPrefix ( scopedStore . namespacePrefix ( ) )
2026-03-30 20:03:19 +00:00
if err != nil {
return 0 , core . E ( "store.ScopedStore.PurgeExpired" , "delete expired rows" , err )
}
return removedRows , nil
2026-03-30 19:14:09 +00:00
}
2026-03-30 18:49:17 +00:00
// checkQuota("store.ScopedStore.Set", "config", "colour") returns nil when the
2026-03-30 17:37:50 +00:00
// 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.
2026-03-30 16:27:54 +00:00
func ( scopedStore * ScopedStore ) checkQuota ( operation , group , key string ) error {
2026-03-30 20:46:43 +00:00
if scopedStore . MaxKeys == 0 && scopedStore . MaxGroups == 0 {
2026-02-20 08:19:11 +00:00
return nil
}
2026-03-30 15:02:28 +00:00
namespacedGroup := scopedStore . namespacedGroup ( group )
2026-03-30 19:14:09 +00:00
namespacePrefix := scopedStore . namespacePrefix ( )
2026-02-20 08:19:11 +00:00
// Check if this is an upsert (key already exists) — upserts never exceed quota.
2026-04-03 05:32:46 +00:00
_ , err := scopedStore . store . Get ( namespacedGroup , key )
2026-02-20 08:19:11 +00:00
if err == nil {
// Key exists — this is an upsert, no quota check needed.
return nil
}
2026-03-30 14:22:49 +00:00
if ! core . Is ( err , NotFoundError ) {
2026-03-09 08:20:38 +00:00
// A database error occurred, not just a "not found" result.
2026-03-30 16:27:54 +00:00
return core . E ( operation , "quota check" , err )
2026-03-09 08:20:38 +00:00
}
2026-02-20 08:19:11 +00:00
// Check MaxKeys quota.
2026-03-30 20:46:43 +00:00
if scopedStore . MaxKeys > 0 {
2026-04-03 05:32:46 +00:00
keyCount , err := scopedStore . store . CountAll ( namespacePrefix )
2026-02-20 08:19:11 +00:00
if err != nil {
2026-03-30 16:27:54 +00:00
return core . E ( operation , "quota check" , err )
2026-02-20 08:19:11 +00:00
}
2026-03-30 20:46:43 +00:00
if keyCount >= scopedStore . MaxKeys {
return core . E ( operation , core . Sprintf ( "key limit (%d)" , scopedStore . MaxKeys ) , QuotaExceededError )
2026-02-20 08:19:11 +00:00
}
}
// Check MaxGroups quota — only if this would create a new group.
2026-03-30 20:46:43 +00:00
if scopedStore . MaxGroups > 0 {
2026-04-03 05:32:46 +00:00
existingGroupCount , err := scopedStore . store . Count ( namespacedGroup )
2026-02-20 08:19:11 +00:00
if err != nil {
2026-03-30 16:27:54 +00:00
return core . E ( operation , "quota check" , err )
2026-02-20 08:19:11 +00:00
}
2026-03-30 14:54:34 +00:00
if existingGroupCount == 0 {
2026-02-20 08:19:11 +00:00
// This group is new — check if adding it would exceed the group limit.
2026-03-30 14:54:34 +00:00
knownGroupCount := 0
2026-04-03 05:32:46 +00:00
for _ , iterationErr := range scopedStore . store . GroupsSeq ( namespacePrefix ) {
2026-03-30 14:54:34 +00:00
if iterationErr != nil {
2026-03-30 16:27:54 +00:00
return core . E ( operation , "quota check" , iterationErr )
2026-02-23 05:21:39 +00:00
}
2026-03-30 14:54:34 +00:00
knownGroupCount ++
2026-02-20 08:19:11 +00:00
}
2026-03-30 20:46:43 +00:00
if knownGroupCount >= scopedStore . MaxGroups {
return core . E ( operation , core . Sprintf ( "group limit (%d)" , scopedStore . MaxGroups ) , QuotaExceededError )
2026-02-20 08:19:11 +00:00
}
}
}
return nil
}