Files
Steve Munene 78804278d4
Property Based Tests / api-test (push) Has been cancelled
Continuous Delivery / lint-and-build (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
MG-3512 - Add rendered context field to update endpoint (#3513)
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
2026-05-14 21:51:35 +02:00

421 lines
11 KiB
Go

// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package postgres
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"log/slog"
"strings"
"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"
"github.com/jackc/pgerrcode"
"github.com/jackc/pgx/v5/pgconn"
)
const jsonNull = "null"
var _ bootstrap.ConfigRepository = (*configRepository)(nil)
type configRepository struct {
db postgres.Database
log *slog.Logger
}
// NewConfigRepository instantiates a PostgreSQL implementation of config
// repository.
func NewConfigRepository(db postgres.Database, log *slog.Logger) bootstrap.ConfigRepository {
return &configRepository{db: db, log: log}
}
func (cr configRepository) Save(ctx context.Context, cfg bootstrap.Config) (string, error) {
q := `INSERT INTO configs (id, domain_id, name, client_cert, client_key, ca_cert, external_id, external_key, content, status, profile_id, render_context)
VALUES (:id, :domain_id, :name, :client_cert, :client_key, :ca_cert, :external_id, :external_key, :content, :status, :profile_id, :render_context)`
dbcfg, err := toDBConfig(cfg)
if err != nil {
return "", errors.Wrap(repoerr.ErrCreateEntity, err)
}
if _, err := cr.db.NamedExecContext(ctx, q, dbcfg); err != nil {
switch pgErr := err.(type) {
case *pgconn.PgError:
if pgErr.Code == pgerrcode.UniqueViolation {
return "", repoerr.ErrConflict
}
}
return "", errors.Wrap(repoerr.ErrCreateEntity, err)
}
return cfg.ID, nil
}
func (cr configRepository) RetrieveByID(ctx context.Context, domainID, id string) (bootstrap.Config, error) {
q := `SELECT id, external_id, name, content, status, client_cert, client_key, ca_cert, profile_id, render_context
FROM configs
WHERE id = :id AND domain_id = :domain_id`
dbcfg := dbConfig{
ID: id,
DomainID: domainID,
}
row, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
if err != nil {
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
if !row.Next() {
return bootstrap.Config{}, repoerr.ErrNotFound
}
if err := row.StructScan(&dbcfg); err != nil {
return bootstrap.Config{}, err
}
cfg, err := toConfig(dbcfg)
if err != nil {
return bootstrap.Config{}, err
}
return cfg, nil
}
func (cr configRepository) RetrieveAll(ctx context.Context, domainID string, filter bootstrap.Filter, offset, limit uint64) bootstrap.ConfigsPage {
search, params := buildRetrieveQueryParams(domainID, filter)
n := len(params)
q := `SELECT id, external_id, name, content, status, profile_id, render_context
FROM configs %s ORDER BY id LIMIT $%d OFFSET $%d`
q = fmt.Sprintf(q, search, n+1, n+2)
rows, err := cr.db.QueryContext(ctx, q, append(params, limit, offset)...)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to retrieve configs due to %s", err))
return bootstrap.ConfigsPage{}
}
defer rows.Close()
var name, content, profileID sql.NullString
var renderContext []byte
configs := []bootstrap.Config{}
for rows.Next() {
c := bootstrap.Config{DomainID: domainID}
if err := rows.Scan(&c.ID, &c.ExternalID, &name, &content, &c.Status, &profileID, &renderContext); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read retrieved config due to %s", err))
return bootstrap.ConfigsPage{}
}
c.Name = name.String
c.Content = content.String
if profileID.Valid {
c.ProfileID = profileID.String
}
if len(renderContext) > 0 && string(renderContext) != jsonNull {
if err := json.Unmarshal(renderContext, &c.RenderContext); err != nil {
cr.log.Error(fmt.Sprintf("Failed to decode render context due to %s", err))
return bootstrap.ConfigsPage{}
}
}
configs = append(configs, c)
}
q = fmt.Sprintf(`SELECT COUNT(*) FROM configs %s`, search)
var total uint64
if err := cr.db.QueryRowxContext(ctx, q, params...).Scan(&total); err != nil {
cr.log.Error(fmt.Sprintf("Failed to count configs due to %s", err))
return bootstrap.ConfigsPage{}
}
return bootstrap.ConfigsPage{
Total: total,
Limit: limit,
Offset: offset,
Configs: configs,
}
}
func (cr configRepository) RetrieveByExternalID(ctx context.Context, externalID string) (bootstrap.Config, error) {
q := `SELECT id, external_key, domain_id, name, client_cert, client_key, ca_cert, content, status, profile_id, render_context
FROM configs
WHERE external_id = :external_id`
dbcfg := dbConfig{
ExternalID: externalID,
}
row, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
if err != nil {
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
if !row.Next() {
return bootstrap.Config{}, repoerr.ErrNotFound
}
if err := row.StructScan(&dbcfg); err != nil {
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
cfg, err := toConfig(dbcfg)
if err != nil {
return bootstrap.Config{}, err
}
return cfg, nil
}
func (cr configRepository) Update(ctx context.Context, cfg bootstrap.Config) error {
q := `UPDATE configs SET name = :name, content = :content, render_context = :render_context WHERE id = :id AND domain_id = :domain_id `
renderContext, err := json.Marshal(cfg.RenderContext)
if err != nil {
return errors.Wrap(repoerr.ErrUpdateEntity, err)
}
dbcfg := dbConfig{
Name: nullString(cfg.Name),
Content: nullString(cfg.Content),
RenderContext: renderContext,
ID: cfg.ID,
DomainID: cfg.DomainID,
}
res, err := cr.db.NamedExecContext(ctx, q, dbcfg)
if err != nil {
return errors.Wrap(repoerr.ErrUpdateEntity, err)
}
cnt, err := res.RowsAffected()
if err != nil {
return errors.Wrap(repoerr.ErrUpdateEntity, err)
}
if cnt == 0 {
return repoerr.ErrNotFound
}
return nil
}
func (cr configRepository) AssignProfile(ctx context.Context, domainID, id, profileID string) error {
q := `UPDATE configs SET profile_id = :profile_id WHERE id = :id AND domain_id = :domain_id`
dbcfg := dbConfig{
ID: id,
DomainID: domainID,
ProfileID: nullString(profileID),
}
res, err := cr.db.NamedExecContext(ctx, q, dbcfg)
if err != nil {
return errors.Wrap(repoerr.ErrUpdateEntity, err)
}
cnt, err := res.RowsAffected()
if err != nil {
return errors.Wrap(repoerr.ErrUpdateEntity, err)
}
if cnt == 0 {
return repoerr.ErrNotFound
}
return nil
}
func (cr configRepository) UpdateCert(ctx context.Context, domainID, id, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
q := `UPDATE configs SET client_cert = :client_cert, client_key = :client_key, ca_cert = :ca_cert WHERE id = :id AND domain_id = :domain_id
RETURNING id, client_cert, client_key, ca_cert, domain_id`
dbcfg := dbConfig{
ID: id,
ClientCert: nullString(clientCert),
DomainID: domainID,
ClientKey: nullString(clientKey),
CaCert: nullString(caCert),
}
row, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
if err != nil {
return bootstrap.Config{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
}
defer row.Close()
if ok := row.Next(); !ok {
return bootstrap.Config{}, errors.Wrap(repoerr.ErrNotFound, row.Err())
}
if err := row.StructScan(&dbcfg); err != nil {
return bootstrap.Config{}, err
}
cfg, err := toConfig(dbcfg)
if err != nil {
return bootstrap.Config{}, err
}
return cfg, nil
}
func (cr configRepository) Remove(ctx context.Context, domainID, id string) error {
q := `DELETE FROM configs WHERE id = :id AND domain_id = :domain_id`
dbcfg := dbConfig{
ID: id,
DomainID: domainID,
}
if _, err := cr.db.NamedExecContext(ctx, q, dbcfg); err != nil {
return errors.Wrap(repoerr.ErrRemoveEntity, err)
}
return nil
}
func (cr configRepository) ChangeStatus(ctx context.Context, domainID, id string, status bootstrap.Status) error {
q := `UPDATE configs SET status = :status WHERE id = :id AND domain_id = :domain_id;`
dbcfg := dbConfig{
ID: id,
Status: status,
DomainID: domainID,
}
res, err := cr.db.NamedExecContext(ctx, q, dbcfg)
if err != nil {
return errors.Wrap(repoerr.ErrUpdateEntity, err)
}
cnt, err := res.RowsAffected()
if err != nil {
return errors.Wrap(repoerr.ErrUpdateEntity, err)
}
if cnt == 0 {
return repoerr.ErrNotFound
}
return nil
}
func buildRetrieveQueryParams(domainID string, filter bootstrap.Filter) (string, []any) {
params := []any{}
queries := []string{}
if domainID != "" {
params = append(params, domainID)
queries = append(queries, fmt.Sprintf("domain_id = $%d", len(params)))
}
counter := len(params) + 1
for k, v := range filter.FullMatch {
if k == "status" {
status, err := bootstrap.ToStatus(v)
if err != nil {
return "", nil
}
if status == bootstrap.AllStatus {
continue
}
params = append(params, status)
queries = append(queries, fmt.Sprintf("%s = $%d", k, counter))
counter++
continue
}
params = append(params, v)
queries = append(queries, fmt.Sprintf("%s = $%d", k, counter))
counter++
}
for k, v := range filter.PartialMatch {
params = append(params, v)
queries = append(queries, fmt.Sprintf("LOWER(%s) LIKE '%%' || $%d || '%%'", k, counter))
counter++
}
if len(queries) > 0 {
return "WHERE " + strings.Join(queries, " AND "), params
}
return "", params
}
func nullString(s string) sql.NullString {
if s == "" {
return sql.NullString{}
}
return sql.NullString{String: s, Valid: true}
}
type dbConfig struct {
DomainID string `db:"domain_id"`
ID string `db:"id"`
Name sql.NullString `db:"name"`
ClientCert sql.NullString `db:"client_cert"`
ClientKey sql.NullString `db:"client_key"`
CaCert sql.NullString `db:"ca_cert"`
ExternalID string `db:"external_id"`
ExternalKey string `db:"external_key"`
Content sql.NullString `db:"content"`
Status bootstrap.Status `db:"status"`
ProfileID sql.NullString `db:"profile_id"`
RenderContext []byte `db:"render_context"`
}
func toDBConfig(cfg bootstrap.Config) (dbConfig, error) {
renderContext, err := json.Marshal(cfg.RenderContext)
if err != nil {
return dbConfig{}, err
}
return dbConfig{
ID: cfg.ID,
DomainID: cfg.DomainID,
Name: nullString(cfg.Name),
ClientCert: nullString(cfg.ClientCert),
ClientKey: nullString(cfg.ClientKey),
CaCert: nullString(cfg.CACert),
ExternalID: cfg.ExternalID,
ExternalKey: cfg.ExternalKey,
Content: nullString(cfg.Content),
Status: cfg.Status,
ProfileID: nullString(cfg.ProfileID),
RenderContext: renderContext,
}, nil
}
func toConfig(dbcfg dbConfig) (bootstrap.Config, error) {
cfg := bootstrap.Config{
ID: dbcfg.ID,
DomainID: dbcfg.DomainID,
ExternalID: dbcfg.ExternalID,
ExternalKey: dbcfg.ExternalKey,
Status: dbcfg.Status,
}
if dbcfg.ProfileID.Valid {
cfg.ProfileID = dbcfg.ProfileID.String
}
if dbcfg.Name.Valid {
cfg.Name = dbcfg.Name.String
}
if dbcfg.Content.Valid {
cfg.Content = dbcfg.Content.String
}
if len(dbcfg.RenderContext) > 0 && string(dbcfg.RenderContext) != jsonNull {
if err := json.Unmarshal(dbcfg.RenderContext, &cfg.RenderContext); err != nil {
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
}
if dbcfg.ClientCert.Valid {
cfg.ClientCert = dbcfg.ClientCert.String
}
if dbcfg.ClientKey.Valid {
cfg.ClientKey = dbcfg.ClientKey.String
}
if dbcfg.CaCert.Valid {
cfg.CACert = dbcfg.CaCert.String
}
return cfg, nil
}