Files
panels/services/panel-service/internal/postgres/panel.go

155 lines
5.8 KiB
Go

// Copyright 2023 Declan Teevan
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package postgres
import (
"context"
"encoding/json"
"errors"
"strings"
"github.com/doug-martin/goqu/v9"
_ "github.com/doug-martin/goqu/v9/dialect/postgres"
"github.com/jackc/pgerrcode"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/rs/zerolog/log"
"github.com/hexolan/panels/panel-service/internal"
)
type panelDatabaseRepo struct {
db *pgxpool.Pool
}
func NewPanelRepository(db *pgxpool.Pool) internal.PanelRepository {
return panelDatabaseRepo{
db: db,
}
}
func (r panelDatabaseRepo) transformToPatchData(data internal.PanelUpdate) goqu.Record {
// Ensure updated_at field is changed
patchData := goqu.Record{"updated_at": goqu.L("timezone('utc', now())")}
// Marshal the data to remove omitted keys
marshalled, _ := json.Marshal(data)
_ = json.Unmarshal(marshalled, &patchData)
return patchData
}
func (r panelDatabaseRepo) GetPanelIdFromName(ctx context.Context, name string) (*int64, error) {
var id int64
err := r.db.QueryRow(ctx, "SELECT id FROM panels WHERE LOWER(name)=LOWER($1)", name).Scan(&id)
if err != nil {
if err == pgx.ErrNoRows {
return nil, internal.WrapServiceError(err, internal.NotFoundErrorCode, "panel not found")
} else if strings.Contains(err.Error(), "failed to connect to") {
return nil, internal.WrapServiceError(err, internal.ConnectionErrorCode, "failed to connect to database")
}
log.Error().Err(err).Msg("unaccounted error whilst getting panel ID from name")
return nil, internal.WrapServiceError(err, internal.UnknownErrorCode, "failed to get panel")
}
return &id, nil
}
func (r panelDatabaseRepo) CreatePanel(ctx context.Context, data internal.PanelCreate) (*internal.Panel, error) {
var id int64
err := r.db.QueryRow(ctx, "INSERT INTO panels (name, description) VALUES ($1, $2) RETURNING id", data.Name, data.Description).Scan(&id)
if err != nil {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
if pgerrcode.IsIntegrityConstraintViolation(pgErr.Code) {
return nil, internal.WrapServiceError(err, internal.ConflictErrorCode, "panel name not unique")
}
} else if strings.Contains(err.Error(), "failed to connect to") {
return nil, internal.WrapServiceError(err, internal.ConnectionErrorCode, "failed to connect to database")
}
log.Error().Err(err).Msg("unaccounted error whilst creating panel")
return nil, internal.WrapServiceError(err, internal.UnknownErrorCode, "failed to create panel")
}
return r.GetPanel(ctx, id)
}
func (r panelDatabaseRepo) GetPanel(ctx context.Context, id int64) (*internal.Panel, error) {
var panel internal.Panel
row := r.db.QueryRow(ctx, "SELECT id, name, description, created_at, updated_at FROM panels WHERE id=$1", id)
err := row.Scan(&panel.Id, &panel.Name, &panel.Description, &panel.CreatedAt, &panel.UpdatedAt)
if err != nil {
if err == pgx.ErrNoRows {
return nil, internal.WrapServiceError(err, internal.NotFoundErrorCode, "panel not found")
} else if strings.Contains(err.Error(), "failed to connect to") {
return nil, internal.WrapServiceError(err, internal.ConnectionErrorCode, "failed to connect to database")
}
log.Error().Err(err).Msg("unaccounted error whilst getting panel")
return nil, internal.WrapServiceError(err, internal.UnknownErrorCode, "failed to get panel")
}
return &panel, nil
}
func (r panelDatabaseRepo) UpdatePanel(ctx context.Context, id int64, data internal.PanelUpdate) (*internal.Panel, error) {
patchData := r.transformToPatchData(data)
// Build a statement to update the panel
statement, args, _ := goqu.Dialect("postgres").Update("panels").Prepared(true).Set(patchData).Where(goqu.C("id").Eq(id)).ToSQL()
// Execute the query
result, err := r.db.Exec(ctx, statement, args...)
if err != nil {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
if pgerrcode.IsIntegrityConstraintViolation(pgErr.Code) {
return nil, internal.WrapServiceError(err, internal.ConflictErrorCode, "panel name not unique")
}
} else if strings.Contains(err.Error(), "failed to connect to") {
return nil, internal.WrapServiceError(err, internal.ConnectionErrorCode, "failed to connect to database")
}
log.Error().Err(err).Msg("unaccounted error whilst updating panel")
return nil, internal.WrapServiceError(err, internal.UnknownErrorCode, "failed to update panel")
}
// Check if any rows were affected from the query
rows_affected := result.RowsAffected()
if rows_affected != 1 {
return nil, internal.NewServiceError(internal.NotFoundErrorCode, "panel not found")
}
return r.GetPanel(ctx, id)
}
func (r panelDatabaseRepo) DeletePanel(ctx context.Context, id int64) error {
// Attempt to delete the panel
result, err := r.db.Exec(ctx, "DELETE FROM panels WHERE id=$1", id)
if err != nil {
if strings.Contains(err.Error(), "failed to connect to") {
return internal.WrapServiceError(err, internal.ConnectionErrorCode, "failed to connect to database")
}
log.Error().Err(err).Msg("unaccounted error whilst deleting panel")
return internal.WrapServiceError(err, internal.UnknownErrorCode, "failed to delete panel")
}
// Check if any rows were affected
rows_affected := result.RowsAffected()
if rows_affected != 1 {
return internal.NewServiceError(internal.NotFoundErrorCode, "panel not found")
}
return nil
}