package meta import ( "context" "encoding/json" "errors" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/sql" "github.com/FAU-CDI/wisski-distillery/internal/models" "github.com/tkw1536/goprogram/lib/collection" "gorm.io/gorm" ) // Key represents a key for metadata. type Key string // ErrMetadatumNotSet is returned by various [MetaStorage] functions when a metadatum is not set var ErrMetadatumNotSet = errors.New("metadatum not set") // Storage manages metadata for either the entire distillery, or a single slug type Storage struct { Slug string sql *sql.SQL } // Get retrieves metadata with the provided key and deserializes the first one into target. // If no metadatum exists, returns [ErrMetadatumNotSet]. func (s Storage) Get(ctx context.Context, key Key, target any) error { table, err := s.sql.QueryTable(ctx, true, models.GrantTable) if err != nil { return err } // read the datum from the database var datum models.Metadatum status := table.Where(&models.Metadatum{Slug: s.Slug, Key: string(key)}).Order("pk DESC").Find(&datum) // check if there was an error if err := status.Error; err != nil { return err } // check if e actually found it! if status.RowsAffected == 0 { return ErrMetadatumNotSet } // and do the unmarshaling! return json.Unmarshal(datum.Value, target) } // GetAll receives all metadata with the provided keys. // For each received value, the targets function is called with the current index, and total number of results. // The function is intended to return a target for deserialization. // // When no metadatum exists, targets is not called, and nil error is returned. func (s Storage) GetAll(ctx context.Context, key Key, target func(index, total int) any) error { table, err := s.sql.QueryTable(ctx, true, models.GrantTable) if err != nil { return err } // read the datum from the database var data []models.Metadatum status := table.Where(&models.Metadatum{Slug: s.Slug, Key: string(key)}).Find(&data) // check if there was an error if err := status.Error; err != nil { return err } // unpack all of them into the destination for index, datum := range data { err := json.Unmarshal(datum.Value, target(index, len(data))) if err != nil { return err } } return nil } // Delete deletes all metadata with the provided key. func (s Storage) Delete(ctx context.Context, key Key) error { table, err := s.sql.QueryTable(ctx, true, models.GrantTable) if err != nil { return err } // delete all the values status := table.Where(&models.Metadatum{Slug: s.Slug, Key: string(key)}).Delete(&models.Metadatum{}) if err := status.Error; err != nil { return err } return nil } // Set serializes value and stores it with the provided key. // Any other metadata with the same key is deleted. func (s Storage) Set(ctx context.Context, key Key, value any) error { table, err := s.sql.QueryTable(ctx, true, models.GrantTable) if err != nil { return err } // marshal the value bytes, err := json.Marshal(value) if err != nil { return err } return table.Transaction(func(tx *gorm.DB) error { // delete the old values status := tx.Where(&models.Metadatum{Slug: s.Slug, Key: string(key)}).Delete(&models.Metadatum{}) if err := status.Error; err != nil { return err } // create the new item to insert status = tx.Create(&models.Metadatum{ Key: string(key), Slug: s.Slug, Value: bytes, }) if err := status.Error; err != nil { return err } return nil }) } // Set serializes values and stores them with the provided key. // Any other metadata with the same key is deleted. func (s Storage) SetAll(ctx context.Context, key Key, values ...any) error { table, err := s.sql.QueryTable(ctx, true, models.GrantTable) if err != nil { return err } return table.Transaction(func(tx *gorm.DB) error { // delete the old values status := tx.Where(&models.Metadatum{Slug: s.Slug, Key: string(key)}).Delete(&models.Metadatum{}) if err := status.Error; err != nil { return err } for _, value := range values { bytes, err := json.Marshal(value) if err != nil { return err } // create the new item to insert status := tx.Create(&models.Metadatum{ Key: string(key), Slug: s.Slug, Value: bytes, }) if err := status.Error; err != nil { return err } } return nil }) } // Purge removes all metadata, regardless of key. func (s Storage) Purge(ctx context.Context) error { table, err := s.sql.QueryTable(ctx, true, models.GrantTable) if err != nil { return err } status := table.Where("slug = ?", s.Slug).Delete(&models.Metadatum{}) if status.Error != nil { return status.Error } return nil } // TypedKey represents a convenience wrapper for a given with a given value. type TypedKey[Value any] Key func (f TypedKey[Value]) Get(ctx context.Context, s *Storage) (value Value, err error) { err = s.Get(ctx, Key(f), &value) return } func (f TypedKey[Value]) GetOrSet(ctx context.Context, s *Storage, dflt Value) (value Value, err error) { value, err = f.Get(ctx, s) if err == ErrMetadatumNotSet { value = dflt err = f.Set(ctx, s, value) } return } func (f TypedKey[Value]) GetAll(ctx context.Context, m *Storage) (values []Value, err error) { err = m.GetAll(ctx, Key(f), func(index, total int) any { if values == nil { values = make([]Value, total) } return &values[index] }) return values, err } func (f TypedKey[Value]) Set(ctx context.Context, m *Storage, value Value) error { return m.Set(ctx, Key(f), value) } func (f TypedKey[Value]) SetAll(ctx context.Context, m *Storage, values ...Value) error { return m.SetAll(ctx, Key(f), collection.AsAny(values)...) } func (f TypedKey[Value]) Delete(ctx context.Context, m *Storage) error { return m.Delete(ctx, Key(f)) }