// Copyright (c) Abstract Machines // SPDX-License-Identifier: Apache-2.0 package postgres import ( "context" "encoding/json" "fmt" "log/slog" "time" "github.com/absmach/magistrala/bootstrap" "github.com/absmach/magistrala/pkg/errors" repoerr "github.com/absmach/magistrala/pkg/errors/repository" "github.com/absmach/magistrala/pkg/postgres" ) var _ bootstrap.BindingStore = (*bindingRepository)(nil) type bindingRepository struct { db postgres.Database log *slog.Logger } // NewBindingRepository instantiates a PostgreSQL implementation of BindingStore. func NewBindingRepository(db postgres.Database, log *slog.Logger) bootstrap.BindingStore { return &bindingRepository{db: db, log: log} } func (br bindingRepository) Save(ctx context.Context, configID string, bindings []bootstrap.BindingSnapshot) error { if len(bindings) == 0 { return nil } q := `INSERT INTO bindings (config_id, slot, type, resource_id, snapshot, secret_snapshot, updated_at) VALUES (:config_id, :slot, :type, :resource_id, :snapshot, :secret_snapshot, :updated_at) ON CONFLICT (config_id, slot) DO UPDATE SET type = EXCLUDED.type, resource_id = EXCLUDED.resource_id, snapshot = EXCLUDED.snapshot, secret_snapshot = EXCLUDED.secret_snapshot, updated_at = EXCLUDED.updated_at` now := time.Now().UTC() dbBindings := make([]dbBindingSnapshot, 0, len(bindings)) for _, b := range bindings { b.ConfigID = configID b.UpdatedAt = now dbb, err := toDBBindingSnapshot(b) if err != nil { return errors.Wrap(repoerr.ErrCreateEntity, err) } dbBindings = append(dbBindings, dbb) } if _, err := br.db.NamedExecContext(ctx, q, dbBindings); err != nil { return errors.Wrap(repoerr.ErrCreateEntity, err) } return nil } func (br bindingRepository) Retrieve(ctx context.Context, configID string) ([]bootstrap.BindingSnapshot, error) { q := `SELECT config_id, slot, type, resource_id, snapshot, secret_snapshot, updated_at FROM bindings WHERE config_id = $1 ORDER BY slot` rows, err := br.db.QueryxContext(ctx, q, configID) if err != nil { return nil, errors.Wrap(repoerr.ErrViewEntity, err) } defer rows.Close() var snapshots []bootstrap.BindingSnapshot for rows.Next() { var dbb dbBindingSnapshot if err := rows.StructScan(&dbb); err != nil { br.log.Error(fmt.Sprintf("failed to scan binding snapshot: %s", err)) return nil, errors.Wrap(repoerr.ErrViewEntity, err) } b, err := toBindingSnapshot(dbb) if err != nil { return nil, errors.Wrap(repoerr.ErrViewEntity, err) } snapshots = append(snapshots, b) } return snapshots, nil } func (br bindingRepository) Delete(ctx context.Context, configID, slot string) error { q := `DELETE FROM bindings WHERE config_id = $1 AND slot = $2` if _, err := br.db.ExecContext(ctx, q, configID, slot); err != nil { return errors.Wrap(repoerr.ErrRemoveEntity, err) } return nil } // dbBindingSnapshot is the database representation of a BindingSnapshot. type dbBindingSnapshot struct { ConfigID string `db:"config_id"` Slot string `db:"slot"` Type string `db:"type"` ResourceID string `db:"resource_id"` Snapshot []byte `db:"snapshot"` SecretSnapshot []byte `db:"secret_snapshot"` UpdatedAt time.Time `db:"updated_at"` } func toDBBindingSnapshot(b bootstrap.BindingSnapshot) (dbBindingSnapshot, error) { snap, err := json.Marshal(b.Snapshot) if err != nil { return dbBindingSnapshot{}, err } secret, err := json.Marshal(b.SecretSnapshot) if err != nil { return dbBindingSnapshot{}, err } return dbBindingSnapshot{ ConfigID: b.ConfigID, Slot: b.Slot, Type: b.Type, ResourceID: b.ResourceID, Snapshot: snap, SecretSnapshot: secret, UpdatedAt: b.UpdatedAt, }, nil } func toBindingSnapshot(dbb dbBindingSnapshot) (bootstrap.BindingSnapshot, error) { b := bootstrap.BindingSnapshot{ ConfigID: dbb.ConfigID, Slot: dbb.Slot, Type: dbb.Type, ResourceID: dbb.ResourceID, UpdatedAt: dbb.UpdatedAt, } if len(dbb.Snapshot) > 0 && string(dbb.Snapshot) != jsonNull { if err := json.Unmarshal(dbb.Snapshot, &b.Snapshot); err != nil { return bootstrap.BindingSnapshot{}, err } } if len(dbb.SecretSnapshot) > 0 && string(dbb.SecretSnapshot) != jsonNull { if err := json.Unmarshal(dbb.SecretSnapshot, &b.SecretSnapshot); err != nil { return bootstrap.BindingSnapshot{}, err } } return b, nil }