mirror of
https://github.com/Sosokker/go-chi-oapi-codegen-todolist.git
synced 2025-12-19 14:04:07 +01:00
330 lines
8.2 KiB
Go
330 lines
8.2 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/Sosokker/todolist-backend/internal/domain"
|
|
db "github.com/Sosokker/todolist-backend/internal/repository/sqlc/generated"
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
type pgxTodoRepository struct {
|
|
q *db.Queries
|
|
pool *pgxpool.Pool
|
|
// Consider adding a TagRepository dependency here for batch loading if needed
|
|
}
|
|
|
|
func NewPgxTodoRepository(queries *db.Queries, pool *pgxpool.Pool) TodoRepository {
|
|
return &pgxTodoRepository{q: queries, pool: pool}
|
|
}
|
|
|
|
// --- Mapping functions ---
|
|
|
|
func mapDbTodoToDomain(dbTodo db.Todo) *domain.Todo {
|
|
return &domain.Todo{
|
|
ID: dbTodo.ID,
|
|
UserID: dbTodo.UserID,
|
|
Title: dbTodo.Title,
|
|
Description: domain.NullStringToStringPtr(dbTodo.Description),
|
|
Status: domain.TodoStatus(dbTodo.Status),
|
|
Deadline: dbTodo.Deadline,
|
|
Attachments: dbTodo.Attachments,
|
|
CreatedAt: dbTodo.CreatedAt,
|
|
UpdatedAt: dbTodo.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func mapDbTagToDomain(dbTag db.Tag) domain.Tag {
|
|
return domain.Tag{
|
|
ID: dbTag.ID,
|
|
UserID: dbTag.UserID,
|
|
Name: dbTag.Name,
|
|
Color: domain.NullStringToStringPtr(nullStringFromText(dbTag.Color)),
|
|
Icon: domain.NullStringToStringPtr(nullStringFromText(dbTag.Icon)),
|
|
CreatedAt: dbTag.CreatedAt,
|
|
UpdatedAt: dbTag.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
// ――― TodoRepository methods ―――
|
|
|
|
func (r *pgxTodoRepository) Create(
|
|
ctx context.Context,
|
|
todo *domain.Todo,
|
|
) (*domain.Todo, error) {
|
|
params := db.CreateTodoParams{
|
|
UserID: todo.UserID,
|
|
Title: todo.Title,
|
|
Description: sql.NullString{String: derefString(todo.Description), Valid: todo.Description != nil},
|
|
Status: db.TodoStatus(todo.Status),
|
|
Deadline: todo.Deadline,
|
|
}
|
|
dbTodo, err := r.q.CreateTodo(ctx, params)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create todo: %w", err)
|
|
}
|
|
return mapDbTodoToDomain(dbTodo), nil
|
|
}
|
|
|
|
func (r *pgxTodoRepository) GetByID(
|
|
ctx context.Context,
|
|
id, userID uuid.UUID,
|
|
) (*domain.Todo, error) {
|
|
dbTodo, err := r.q.GetTodoByID(ctx, db.GetTodoByIDParams{ID: id, UserID: userID})
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, domain.ErrNotFound
|
|
}
|
|
return nil, fmt.Errorf("failed to get todo: %w", err)
|
|
}
|
|
return mapDbTodoToDomain(dbTodo), nil
|
|
}
|
|
|
|
func (r *pgxTodoRepository) ListByUser(
|
|
ctx context.Context,
|
|
params ListTodosParams,
|
|
) ([]domain.Todo, error) {
|
|
|
|
sqlcParams := db.ListUserTodosParams{
|
|
UserID: params.UserID,
|
|
Limit: int32(params.Limit),
|
|
Offset: int32(params.Offset),
|
|
StatusFilter: db.NullTodoStatus{Valid: false},
|
|
TagIDFilter: pgtype.UUID{Valid: false},
|
|
DeadlineBeforeFilter: nil,
|
|
DeadlineAfterFilter: nil,
|
|
}
|
|
|
|
if params.Status != nil {
|
|
sqlcParams.StatusFilter = db.NullTodoStatus{
|
|
TodoStatus: db.TodoStatus(*params.Status),
|
|
Valid: true,
|
|
}
|
|
}
|
|
|
|
if params.TagID != nil {
|
|
sqlcParams.TagIDFilter = pgtype.UUID{
|
|
Bytes: *params.TagID,
|
|
Valid: true,
|
|
}
|
|
}
|
|
|
|
if params.DeadlineBefore != nil {
|
|
sqlcParams.DeadlineBeforeFilter = params.DeadlineBefore
|
|
}
|
|
|
|
if params.DeadlineAfter != nil {
|
|
sqlcParams.DeadlineAfterFilter = params.DeadlineAfter
|
|
}
|
|
|
|
// Call the regenerated sqlc function with the correctly populated struct
|
|
dbTodos, err := r.q.ListUserTodos(ctx, sqlcParams)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return []domain.Todo{}, nil
|
|
}
|
|
return nil, fmt.Errorf("failed to list todos: %w", err)
|
|
}
|
|
|
|
todos := make([]domain.Todo, len(dbTodos))
|
|
for i, t := range dbTodos {
|
|
mappedTodo := mapDbTodoToDomain(t)
|
|
if mappedTodo != nil {
|
|
todos[i] = *mappedTodo
|
|
} else {
|
|
return nil, fmt.Errorf("failed to map database todo at index %d", i)
|
|
}
|
|
}
|
|
return todos, nil
|
|
}
|
|
|
|
func (r *pgxTodoRepository) Update(
|
|
ctx context.Context,
|
|
id, userID uuid.UUID,
|
|
updateData *domain.Todo,
|
|
) (*domain.Todo, error) {
|
|
if _, err := r.GetByID(ctx, id, userID); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
params := db.UpdateTodoParams{
|
|
ID: id,
|
|
UserID: userID,
|
|
Title: pgtype.Text{String: updateData.Title, Valid: true},
|
|
Description: sql.NullString{String: derefString(updateData.Description), Valid: updateData.Description != nil},
|
|
Status: db.NullTodoStatus{TodoStatus: db.TodoStatus(updateData.Status), Valid: true},
|
|
Deadline: updateData.Deadline,
|
|
Attachments: updateData.Attachments,
|
|
}
|
|
|
|
dbTodo, err := r.q.UpdateTodo(ctx, params)
|
|
if err != nil {
|
|
var pgErr *pgconn.PgError
|
|
if errors.As(err, &pgErr) && pgErr.Code == "23503" {
|
|
return nil, fmt.Errorf("foreign key violation: %w", domain.ErrBadRequest)
|
|
}
|
|
return nil, fmt.Errorf("failed to update todo: %w", err)
|
|
}
|
|
return mapDbTodoToDomain(dbTodo), nil
|
|
}
|
|
|
|
func (r *pgxTodoRepository) Delete(
|
|
ctx context.Context,
|
|
id, userID uuid.UUID,
|
|
) error {
|
|
if err := r.q.DeleteTodo(ctx, db.DeleteTodoParams{ID: id, UserID: userID}); err != nil {
|
|
return fmt.Errorf("failed to delete todo: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// --- Tag Associations ---
|
|
|
|
func (r *pgxTodoRepository) AddTag(
|
|
ctx context.Context,
|
|
todoID, tagID uuid.UUID,
|
|
) error {
|
|
if err := r.q.AddTagToTodo(ctx, db.AddTagToTodoParams{TodoID: todoID, TagID: tagID}); err != nil {
|
|
return fmt.Errorf("failed to add tag: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *pgxTodoRepository) RemoveTag(
|
|
ctx context.Context,
|
|
todoID, tagID uuid.UUID,
|
|
) error {
|
|
if err := r.q.RemoveTagFromTodo(ctx, db.RemoveTagFromTodoParams{TodoID: todoID, TagID: tagID}); err != nil {
|
|
return fmt.Errorf("failed to remove tag: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *pgxTodoRepository) SetTags(
|
|
ctx context.Context,
|
|
todoID uuid.UUID,
|
|
tagIDs []uuid.UUID,
|
|
) error {
|
|
tx, err := r.pool.Begin(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("begin tx: %w", err)
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
|
|
qtx := r.q.WithTx(tx)
|
|
if err := qtx.RemoveAllTagsFromTodo(ctx, todoID); err != nil {
|
|
return fmt.Errorf("remove existing tags: %w", err)
|
|
}
|
|
for _, tID := range tagIDs {
|
|
if err := qtx.AddTagToTodo(ctx, db.AddTagToTodoParams{TodoID: todoID, TagID: tID}); err != nil {
|
|
return fmt.Errorf("add tag %s: %w", tID, err)
|
|
}
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return fmt.Errorf("commit tags tx: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *pgxTodoRepository) GetTags(
|
|
ctx context.Context,
|
|
todoID uuid.UUID,
|
|
) ([]domain.Tag, error) {
|
|
dbTags, err := r.q.GetTagsForTodo(ctx, todoID)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return []domain.Tag{}, nil
|
|
}
|
|
return nil, fmt.Errorf("failed to get tags: %w", err)
|
|
}
|
|
tags := make([]domain.Tag, len(dbTags))
|
|
for i, t := range dbTags {
|
|
tags[i] = mapDbTagToDomain(t)
|
|
}
|
|
return tags, nil
|
|
}
|
|
|
|
// --- Attachments (String Identifiers in Array) ---
|
|
|
|
func (r *pgxTodoRepository) AddAttachment(
|
|
ctx context.Context,
|
|
todoID, userID uuid.UUID,
|
|
attachmentID string,
|
|
) error {
|
|
if _, err := r.GetByID(ctx, todoID, userID); err != nil {
|
|
return err
|
|
}
|
|
if err := r.q.AddAttachmentToTodo(ctx, db.AddAttachmentToTodoParams{
|
|
ArrayAppend: attachmentID,
|
|
ID: todoID,
|
|
UserID: userID,
|
|
}); err != nil {
|
|
return fmt.Errorf("failed to add attachment: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *pgxTodoRepository) RemoveAttachment(
|
|
ctx context.Context,
|
|
todoID, userID uuid.UUID,
|
|
attachmentID string,
|
|
) error {
|
|
if _, err := r.GetByID(ctx, todoID, userID); err != nil {
|
|
return err
|
|
}
|
|
if err := r.q.RemoveAttachmentFromTodo(ctx, db.RemoveAttachmentFromTodoParams{
|
|
ArrayRemove: attachmentID,
|
|
ID: todoID,
|
|
UserID: userID,
|
|
}); err != nil {
|
|
return fmt.Errorf("failed to remove attachment: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *pgxTodoRepository) SetAttachments(
|
|
ctx context.Context,
|
|
todoID, userID uuid.UUID,
|
|
attachmentIDs []string,
|
|
) error {
|
|
_, err := r.GetByID(ctx, todoID, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
updateParams := db.UpdateTodoParams{
|
|
ID: todoID,
|
|
UserID: userID,
|
|
Attachments: attachmentIDs,
|
|
}
|
|
_, err = r.q.UpdateTodo(ctx, updateParams)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set attachments using UpdateTodo: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ― Helpers ―
|
|
|
|
func derefString(s *string) string {
|
|
if s != nil {
|
|
return *s
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func contains(slice []string, val string) bool {
|
|
for _, s := range slice {
|
|
if s == val {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|