From 3c5c8a7fe7aa5dddcbc4fda3d5947144d2f643c8 Mon Sep 17 00:00:00 2001 From: Robin Dittmar Date: Fri, 21 Aug 2026 20:51:23 +0200 Subject: [PATCH 1/8] feat: adding/removing users from list now works by email --- internal/api/handler/list.go | 25 +++++++++++++++++++++---- internal/api/request/list.go | 4 ++-- internal/domain/list.go | 6 +----- internal/domain/user.go | 5 +++++ internal/repository/user.go | 14 ++++++++++++++ 5 files changed, 43 insertions(+), 11 deletions(-) diff --git a/internal/api/handler/list.go b/internal/api/handler/list.go index 0732f34..cf025e1 100644 --- a/internal/api/handler/list.go +++ b/internal/api/handler/list.go @@ -11,6 +11,7 @@ import ( type ListHandler struct { ListService *domain.ListService + UserService *domain.UserService } func NewListHandler(listService *domain.ListService) *ListHandler { @@ -99,6 +100,7 @@ func (h *ListHandler) GetLists(w http.ResponseWriter, r *http.Request) { // @Param payload body request.AddUserToListPayload true "Add user to list payload" // @Success 204 {object} nil // @Error 400 {object} response.ErrorResponse "failed to decode request body" +// @Error 500 {object} response.ErrorResponse "failed to find email in system" // @Error 500 {object} response.ErrorResponse "failed to add user to list" // @Router /lists/user [post] func (h *ListHandler) AddUserToList(w http.ResponseWriter, r *http.Request) { @@ -118,14 +120,21 @@ func (h *ListHandler) AddUserToList(w http.ResponseWriter, r *http.Request) { return } - err = h.ListService.AddUserToList(ctx, authContext.UserID, payload.ListID, payload.UserID) + user, err := h.UserService.GetUserByEmail(ctx, payload.Email) + if err != nil { + slog.ErrorContext(ctx, "failed to get user by email", slog.Any("error", err)) + response.Error(ctx, w, http.StatusInternalServerError, "failed to find email in system") + return + } + + err = h.ListService.AddUserToList(ctx, authContext.UserID, payload.ListID, user.ID) if err != nil { slog.ErrorContext(ctx, "failed to add user to list", slog.Any("error", err)) response.Error(ctx, w, http.StatusInternalServerError, "failed to add user to list") return } - slog.InfoContext(ctx, "added user to list successfully", slog.Any("list_id", payload.ListID), slog.Any("user_id", payload.UserID)) + slog.InfoContext(ctx, "added user to list successfully", slog.Any("list_id", payload.ListID), slog.Any("email", user.Email)) response.Status(ctx, w, http.StatusNoContent) } @@ -139,6 +148,7 @@ func (h *ListHandler) AddUserToList(w http.ResponseWriter, r *http.Request) { // @Param payload body request.RemoveUserFromListPayload true "Remove user from list payload" // @Success 204 {object} nil // @Error 400 {object} response.ErrorResponse "failed to decode request body" +// @Error 500 {object} response.ErrorResponse "failed to find email in system" // @Error 500 {object} response.ErrorResponse "failed to remove user from list" // @Router /lists/user [delete] func (h *ListHandler) RemoveUserFromList(w http.ResponseWriter, r *http.Request) { @@ -158,14 +168,21 @@ func (h *ListHandler) RemoveUserFromList(w http.ResponseWriter, r *http.Request) return } - err = h.ListService.RemoveUserFromList(ctx, authContext.UserID, payload.ListID, payload.UserID) + user, err := h.UserService.GetUserByEmail(ctx, payload.Email) + if err != nil { + slog.ErrorContext(ctx, "failed to get user by email", slog.Any("error", err)) + response.Error(ctx, w, http.StatusInternalServerError, "failed to find email in system") + return + } + + err = h.ListService.RemoveUserFromList(ctx, authContext.UserID, payload.ListID, user.ID) if err != nil { slog.ErrorContext(ctx, "failed to remove user from list", slog.Any("error", err)) response.Error(ctx, w, http.StatusInternalServerError, "failed to remove user from list") return } - slog.InfoContext(ctx, "removed user from list successfully", slog.Any("list_id", payload.ListID), slog.Any("user_id", payload.UserID)) + slog.InfoContext(ctx, "removed user from list successfully", slog.Any("list_id", payload.ListID), slog.Any("email", user.Email)) response.Status(ctx, w, http.StatusNoContent) } diff --git a/internal/api/request/list.go b/internal/api/request/list.go index d425b44..00ef7ce 100644 --- a/internal/api/request/list.go +++ b/internal/api/request/list.go @@ -7,12 +7,12 @@ type CreateListPayload struct { type AddUserToListPayload struct { ListID string `json:"list_id"` - UserID string `json:"user_id"` + Email string `json:"email"` } type RemoveUserFromListPayload struct { ListID string `json:"list_id"` - UserID string `json:"user_id"` + Email string `json:"email"` } type CreateListItemPayload struct { diff --git a/internal/domain/list.go b/internal/domain/list.go index ba4d21f..7d25f33 100644 --- a/internal/domain/list.go +++ b/internal/domain/list.go @@ -78,13 +78,9 @@ func (s *ListService) AddUserToList(ctx context.Context, authUserID string, list return ErrUserIDEmpty } - inList, err := s.repo.IsUserInList(ctx, listID, authUserID) - if err != nil { + if err := s.userAllowedToAccessList(ctx, authUserID, listID); err != nil { return err } - if !inList { - return ErrUserNotInList - } return s.repo.AddUserToList(ctx, listID, userID) } diff --git a/internal/domain/user.go b/internal/domain/user.go index f7ebad4..4f57065 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -17,6 +17,7 @@ type User struct { type UserRepository interface { CreateUser(ctx context.Context, email string, name string, passwordHash string) (*User, error) + GetUserByEmail(ctx context.Context, email string) (*User, error) } type UserService struct { @@ -45,3 +46,7 @@ func (s *UserService) CreateUser(ctx context.Context, email string, name string, return s.repo.CreateUser(ctx, email, name, string(hash)) } + +func (s *UserService) GetUserByEmail(ctx context.Context, email string) (*User, error) { + return s.repo.GetUserByEmail(ctx, email) +} diff --git a/internal/repository/user.go b/internal/repository/user.go index 4749def..177a465 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -39,3 +39,17 @@ func (r *UserRepo) CreateUser(ctx context.Context, email string, name string, pa return user, nil } + +func (r *UserRepo) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) { + user := &domain.User{} + + err := r.db.QueryRowContext(ctx, + "SELECT id, email, name FROM users WHERE email = $1", + email, + ).Scan(&user.ID, &user.Email, &user.Name) + if err != nil { + return nil, fmt.Errorf("failed to get user: %w", err) + } + + return user, nil +} From 3c13e13c2e272c9eb5cdb2b31f1ac24a2a26631e Mon Sep 17 00:00:00 2001 From: Robin Dittmar Date: Fri, 21 Aug 2026 20:56:53 +0200 Subject: [PATCH 2/8] fix: list items are now ordered by modified_at --- internal/repository/list.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/repository/list.go b/internal/repository/list.go index 514cc19..eb6f6a1 100644 --- a/internal/repository/list.go +++ b/internal/repository/list.go @@ -177,7 +177,7 @@ func (r *ListRepo) SetListItemCompleted(ctx context.Context, listItemID string, func (r *ListRepo) GetListItems(ctx context.Context, listID string) ([]domain.ListItem, error) { rows, err := r.db.QueryContext(ctx, - "SELECT id, title, is_completed, created_at, modified_at FROM list_items WHERE list_id = $1", + "SELECT id, title, is_completed, created_at, modified_at FROM list_items WHERE list_id = $1 ORDER BY modified_at DESC", listID, ) if err != nil { From d2f0f1a4cf99a7b42be51ae14b20ad2f37457c37 Mon Sep 17 00:00:00 2001 From: Robin Dittmar Date: Fri, 21 Aug 2026 21:00:05 +0200 Subject: [PATCH 3/8] fix: list items are now ordered by is_completed and modified_at --- internal/repository/list.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/repository/list.go b/internal/repository/list.go index eb6f6a1..5383b69 100644 --- a/internal/repository/list.go +++ b/internal/repository/list.go @@ -61,7 +61,7 @@ func (r *ListRepo) CreateList(ctx context.Context, name string, userIDs []string func (r *ListRepo) GetLists(ctx context.Context, userID string) ([]domain.List, error) { rows, err := r.db.QueryContext(ctx, - "SELECT id, name, lists.created_at, modified_at FROM lists INNER JOIN list_users ON lists.id=list_users.list_id WHERE list_users.user_id = $1", + "SELECT id, name, lists.created_at, modified_at, COUNT(*) FROM lists INNER JOIN list_users ON lists.id=list_users.list_id WHERE list_users.user_id = $1", userID, ) if err != nil { @@ -177,7 +177,7 @@ func (r *ListRepo) SetListItemCompleted(ctx context.Context, listItemID string, func (r *ListRepo) GetListItems(ctx context.Context, listID string) ([]domain.ListItem, error) { rows, err := r.db.QueryContext(ctx, - "SELECT id, title, is_completed, created_at, modified_at FROM list_items WHERE list_id = $1 ORDER BY modified_at DESC", + "SELECT id, title, is_completed, created_at, modified_at FROM list_items WHERE list_id = $1 ORDER BY is_completed, modified_at DESC", listID, ) if err != nil { From be588dd405c1ba381366dc2e75d3c82646c02ed4 Mon Sep 17 00:00:00 2001 From: Robin Dittmar Date: Fri, 21 Aug 2026 21:10:17 +0200 Subject: [PATCH 4/8] feat: GET /lists now returns total items and completed items of that list --- internal/domain/list.go | 10 ++++++---- internal/repository/list.go | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/internal/domain/list.go b/internal/domain/list.go index 7d25f33..071f7d2 100644 --- a/internal/domain/list.go +++ b/internal/domain/list.go @@ -18,10 +18,12 @@ var ( ) type List struct { - ID string `json:"id"` - Name string `json:"name"` - CreatedAt time.Time `json:"created_at"` - ModifiedAt time.Time `json:"modified_at"` + ID string `json:"id"` + Name string `json:"name"` + CreatedAt time.Time `json:"created_at"` + ModifiedAt time.Time `json:"modified_at"` + TotalItems int `json:"total_items"` + CompletedItems int `json:"completed_items"` } type ListItem struct { diff --git a/internal/repository/list.go b/internal/repository/list.go index 5383b69..83eefed 100644 --- a/internal/repository/list.go +++ b/internal/repository/list.go @@ -61,7 +61,7 @@ func (r *ListRepo) CreateList(ctx context.Context, name string, userIDs []string func (r *ListRepo) GetLists(ctx context.Context, userID string) ([]domain.List, error) { rows, err := r.db.QueryContext(ctx, - "SELECT id, name, lists.created_at, modified_at, COUNT(*) FROM lists INNER JOIN list_users ON lists.id=list_users.list_id WHERE list_users.user_id = $1", + "SELECT l.id, l.name, l.created_at, l.modified_at, (SELECT COUNT(*) FROM list_items WHERE list_id=l.id), (SELECT COUNT(*) FROM list_items WHERE list_id=l.id AND is_completed=true) FROM lists AS l INNER JOIN list_users ON lists.id=list_users.list_id WHERE list_users.user_id = $1", userID, ) if err != nil { @@ -75,7 +75,7 @@ func (r *ListRepo) GetLists(ctx context.Context, userID string) ([]domain.List, var lists []domain.List for rows.Next() { var l domain.List - err = rows.Scan(&l.ID, &l.Name, &l.CreatedAt, &l.ModifiedAt) + err = rows.Scan(&l.ID, &l.Name, &l.CreatedAt, &l.ModifiedAt, &l.TotalItems, &l.CompletedItems) if err != nil { return nil, err } From a1ec7cc24a76fc6c49c81b6f2d202f0bd7374591 Mon Sep 17 00:00:00 2001 From: Robin Dittmar Date: Fri, 21 Aug 2026 21:16:16 +0200 Subject: [PATCH 5/8] fix: list_users now cascades delete when user is deleted --- internal/database/migrations/000002_create_lists.up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/database/migrations/000002_create_lists.up.sql b/internal/database/migrations/000002_create_lists.up.sql index 192494f..199c5d2 100644 --- a/internal/database/migrations/000002_create_lists.up.sql +++ b/internal/database/migrations/000002_create_lists.up.sql @@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS lists ( CREATE TABLE IF NOT EXISTS list_users ( list_id UUID REFERENCES lists(id) ON DELETE CASCADE, - user_id UUID NOT NULL, + user_id UUID REFERENCES users(id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (list_id, user_id) ); From c2912d8d7985aad62f4101295d011235f0ae82f3 Mon Sep 17 00:00:00 2001 From: Robin Dittmar Date: Fri, 21 Aug 2026 21:28:58 +0200 Subject: [PATCH 6/8] feat: added delete endpoints for lists and list items --- internal/api/handler/list.go | 83 ++++++++++++++++++++++++++++++++++- internal/api/router/router.go | 2 + internal/domain/list.go | 28 +++++++++++- internal/domain/list_test.go | 8 ++-- internal/repository/list.go | 18 ++++++++ 5 files changed, 133 insertions(+), 6 deletions(-) diff --git a/internal/api/handler/list.go b/internal/api/handler/list.go index cf025e1..c53323b 100644 --- a/internal/api/handler/list.go +++ b/internal/api/handler/list.go @@ -47,7 +47,7 @@ func (h *ListHandler) CreateList(w http.ResponseWriter, r *http.Request) { return } - list, err := h.ListService.Create(ctx, authContext.UserID, payload.Name, payload.UserIDs) + list, err := h.ListService.CreateList(ctx, authContext.UserID, payload.Name, payload.UserIDs) if err != nil { slog.ErrorContext(ctx, "failed to create list", slog.Any("error", err)) response.Error(ctx, w, http.StatusInternalServerError, "failed to create list") @@ -58,6 +58,46 @@ func (h *ListHandler) CreateList(w http.ResponseWriter, r *http.Request) { response.JSON(ctx, w, http.StatusCreated, list) } +// DeleteList handles the deletion of a list +// +// @Summary Delete list route +// @Description Deletes a list, cascading to user associations and items +// @Tags List +// @Accept json +// @Produce json +// @Param id path int true "List ID" +// @Success 204 +// @Error 400 {object} response.ErrorResponse "failed to decode request url" +// @Error 500 {object} response.ErrorResponse "failed to delete list" +// @Router /lists/{id} [delete] +func (h *ListHandler) DeleteList(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + listID := r.PathValue("id") + if listID == "" { + slog.ErrorContext(ctx, "failed to read list id from path") + response.Error(ctx, w, http.StatusBadRequest, "failed to decode request url") + return + } + + authContext, err := domain.GetAuthContext(ctx) + if err != nil { + slog.ErrorContext(ctx, "failed to get auth context", slog.Any("error", err)) + response.Error(ctx, w, http.StatusInternalServerError, "failed to delete list") + return + } + + err = h.ListService.DeleteList(ctx, authContext.UserID, listID) + if err != nil { + slog.ErrorContext(ctx, "failed to delete list", slog.Any("error", err)) + response.Error(ctx, w, http.StatusInternalServerError, "failed to delete list") + return + } + + slog.InfoContext(ctx, "deleted list successfully", slog.Any("list_id", listID)) + response.Status(ctx, w, http.StatusNoContent) +} + // GetLists handles fetching lists for the current user // // @Summary Returns all lists of the user @@ -226,6 +266,46 @@ func (h *ListHandler) CreateListItem(w http.ResponseWriter, r *http.Request) { response.JSON(ctx, w, http.StatusCreated, item) } +// DeleteListItem handles the deletion of a list item +// +// @Summary Delete list item route +// @Description Deletes an item +// @Tags List +// @Accept json +// @Produce json +// @Param id path int true "List Item ID" +// @Success 204 +// @Error 400 {object} response.ErrorResponse "failed to decode request url" +// @Error 500 {object} response.ErrorResponse "failed to delete list item" +// @Router /lists/item/{id} [delete] +func (h *ListHandler) DeleteListItem(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + listItemID := r.PathValue("id") + if listItemID == "" { + slog.ErrorContext(ctx, "failed to read list item id from path") + response.Error(ctx, w, http.StatusBadRequest, "failed to decode request url") + return + } + + authContext, err := domain.GetAuthContext(ctx) + if err != nil { + slog.ErrorContext(ctx, "failed to get auth context", slog.Any("error", err)) + response.Error(ctx, w, http.StatusInternalServerError, "failed to delete list item") + return + } + + err = h.ListService.DeleteListItem(ctx, authContext.UserID, listItemID) + if err != nil { + slog.ErrorContext(ctx, "failed to delete list item", slog.Any("error", err)) + response.Error(ctx, w, http.StatusInternalServerError, "failed to delete list item") + return + } + + slog.InfoContext(ctx, "deleted list item successfully", slog.Any("list_item_id", listItemID)) + response.Status(ctx, w, http.StatusNoContent) +} + // UpdateListItem handles updating of a list item // // @Summary Update list item @@ -274,6 +354,7 @@ func (h *ListHandler) UpdateListItem(w http.ResponseWriter, r *http.Request) { // @Tags List // @Accept json // @Produce json +// @Param id path int true "List Item ID" // @Param payload body request.SetListItemCompletedPayload true "Update list item is completed payload" // @Success 204 {object} nil // @Error 400 {object} response.ErrorResponse "failed to decode request url" diff --git a/internal/api/router/router.go b/internal/api/router/router.go index 4e19d74..17d5277 100644 --- a/internal/api/router/router.go +++ b/internal/api/router/router.go @@ -42,10 +42,12 @@ func NewMux(cfg Config) http.Handler { apiMux.Handle("POST /users", protected(userHandler.CreateUser)) apiMux.Handle("POST /lists", protected(listHandler.CreateList)) + apiMux.Handle("DELETE /lists/{id}", protected(listHandler.DeleteList)) apiMux.Handle("GET /lists", protected(listHandler.GetLists)) apiMux.Handle("POST /lists/user", protected(listHandler.AddUserToList)) apiMux.Handle("DELETE /lists/user", protected(listHandler.RemoveUserFromList)) apiMux.Handle("POST /lists/item", protected(listHandler.CreateListItem)) + apiMux.Handle("DELETE /lists/item/{id}", protected(listHandler.DeleteListItem)) apiMux.Handle("PUT /lists/item", protected(listHandler.UpdateListItem)) apiMux.Handle("POST /lists/items/{id}", protected(listHandler.SetListItemCompleted)) apiMux.Handle("GET /lists/{id}", protected(listHandler.GetListItems)) diff --git a/internal/domain/list.go b/internal/domain/list.go index 071f7d2..92a4665 100644 --- a/internal/domain/list.go +++ b/internal/domain/list.go @@ -37,12 +37,14 @@ type ListItem struct { type ListRepository interface { CreateList(ctx context.Context, name string, userIDs []string) (*List, error) + DeleteList(ctx context.Context, listID string) error GetLists(ctx context.Context, userID string) ([]List, error) AddUserToList(ctx context.Context, listID string, userID string) error RemoveUserFromList(ctx context.Context, listID string, userID string) error IsUserInList(ctx context.Context, listID string, userID string) (bool, error) IsUserInListByItemID(ctx context.Context, listItemID string, userID string) (bool, error) CreateListItem(ctx context.Context, listID string, title string) (*ListItem, error) + DeleteListItem(ctx context.Context, listItemID string) error UpdateListItem(ctx context.Context, listItemID string, title string, isCompleted bool) error SetListItemCompleted(ctx context.Context, listItemID string, isCompleted bool) error GetListItems(ctx context.Context, listID string) ([]ListItem, error) @@ -56,7 +58,7 @@ func NewListService(r ListRepository) *ListService { return &ListService{repo: r} } -func (s *ListService) Create(ctx context.Context, authUserID string, name string, userIDs []string) (*List, error) { +func (s *ListService) CreateList(ctx context.Context, authUserID string, name string, userIDs []string) (*List, error) { if name == "" { return nil, ErrListNameEmpty } @@ -68,6 +70,18 @@ func (s *ListService) Create(ctx context.Context, authUserID string, name string return s.repo.CreateList(ctx, name, userIDs) } +func (s *ListService) DeleteList(ctx context.Context, authUserID string, listID string) error { + if listID == "" { + return ErrListIDEmpty + } + + if err := s.userAllowedToAccessList(ctx, authUserID, listID); err != nil { + return err + } + + return s.repo.DeleteList(ctx, listID) +} + func (s *ListService) GetLists(ctx context.Context, authUserID string) ([]List, error) { return s.repo.GetLists(ctx, authUserID) } @@ -117,6 +131,18 @@ func (s *ListService) CreateListItem(ctx context.Context, authUserID string, lis return s.repo.CreateListItem(ctx, listID, title) } +func (s *ListService) DeleteListItem(ctx context.Context, authUserID string, listItemID string) error { + if listItemID == "" { + return ErrListItemIDEmpty + } + + if err := s.userAllowedToAccessListItem(ctx, authUserID, listItemID); err != nil { + return err + } + + return s.repo.DeleteListItem(ctx, listItemID) +} + func (s *ListService) UpdateListItem(ctx context.Context, authUserID string, listItemID string, title string, isCompleted bool) error { if listItemID == "" { return ErrListItemIDEmpty diff --git a/internal/domain/list_test.go b/internal/domain/list_test.go index 8d69e18..dc78401 100644 --- a/internal/domain/list_test.go +++ b/internal/domain/list_test.go @@ -37,7 +37,7 @@ func TestListService_Create_Success(t *testing.T) { repo.On("CreateList", mock.Anything, "My List", []string{"user1", "user2"}).Return(expectedList, nil) service := domain.NewListService(repo) - list, err := service.Create(context.Background(), "My List", []string{"user1", "user2"}) + list, err := service.CreateList(context.Background(), "My List", []string{"user1", "user2"}) require.NoError(t, err) assert.Equal(t, expectedList, list) @@ -48,7 +48,7 @@ func TestListService_Create_EmptyName(t *testing.T) { repo := new(mockListRepo) service := domain.NewListService(repo) - list, err := service.Create(context.Background(), "", []string{"user1"}) + list, err := service.CreateList(context.Background(), "", []string{"user1"}) require.Error(t, err) assert.EqualError(t, err, "list name must not be empty") @@ -60,7 +60,7 @@ func TestListService_Create_EmptyUsers(t *testing.T) { repo := new(mockListRepo) service := domain.NewListService(repo) - list, err := service.Create(context.Background(), "My List", []string{}) + list, err := service.CreateList(context.Background(), "My List", []string{}) require.Error(t, err) assert.EqualError(t, err, "users must have at least one associated user") @@ -75,7 +75,7 @@ func TestListService_Create_RepoError(t *testing.T) { service := domain.NewListService(repo) - list, err := service.Create(context.Background(), "My List", []string{"user1"}) + list, err := service.CreateList(context.Background(), "My List", []string{"user1"}) require.Error(t, err) assert.ErrorIs(t, err, expectedErr) diff --git a/internal/repository/list.go b/internal/repository/list.go index 83eefed..5173f4f 100644 --- a/internal/repository/list.go +++ b/internal/repository/list.go @@ -59,6 +59,15 @@ func (r *ListRepo) CreateList(ctx context.Context, name string, userIDs []string return list, nil } +func (r *ListRepo) DeleteList(ctx context.Context, listID string) error { + _, err := r.db.ExecContext(ctx, "DELETE FROM lists WHERE id = $1", listID) + if err != nil { + return fmt.Errorf("failed to delete list: %w", err) + } + + return nil +} + func (r *ListRepo) GetLists(ctx context.Context, userID string) ([]domain.List, error) { rows, err := r.db.QueryContext(ctx, "SELECT l.id, l.name, l.created_at, l.modified_at, (SELECT COUNT(*) FROM list_items WHERE list_id=l.id), (SELECT COUNT(*) FROM list_items WHERE list_id=l.id AND is_completed=true) FROM lists AS l INNER JOIN list_users ON lists.id=list_users.list_id WHERE list_users.user_id = $1", @@ -153,6 +162,15 @@ func (r *ListRepo) CreateListItem(ctx context.Context, listID string, title stri return l, nil } +func (r *ListRepo) DeleteListItem(ctx context.Context, listItemID string) error { + _, err := r.db.ExecContext(ctx, "DELETE FROM list_items WHERE id = $1", listItemID) + if err != nil { + return fmt.Errorf("failed to delete list item: %w", err) + } + + return nil +} + func (r *ListRepo) UpdateListItem(ctx context.Context, listItemID string, title string, isCompleted bool) error { _, err := r.db.ExecContext(ctx, "UPDATE list_items SET title = $1, is_completed = $2, modified_at = NOW() WHERE id = $3", title, isCompleted, listItemID, From f1ea27092f8c4d090e4596e48f0b660d7c1a2faf Mon Sep 17 00:00:00 2001 From: Robin Dittmar Date: Fri, 21 Aug 2026 21:40:25 +0200 Subject: [PATCH 7/8] fix: fixed log message --- internal/api/handler/list.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/api/handler/list.go b/internal/api/handler/list.go index c53323b..60218c3 100644 --- a/internal/api/handler/list.go +++ b/internal/api/handler/list.go @@ -122,7 +122,7 @@ func (h *ListHandler) GetLists(w http.ResponseWriter, r *http.Request) { lists, err := h.ListService.GetLists(ctx, authContext.UserID) if err != nil { - slog.ErrorContext(ctx, "failed to set list item completed", slog.Any("error", err)) + slog.ErrorContext(ctx, "failed to get lists", slog.Any("error", err)) response.Error(ctx, w, http.StatusInternalServerError, "failed to read list items") return } From b777e00d945f7c3417e8d74c23dddcc7b0158869 Mon Sep 17 00:00:00 2001 From: Robin Dittmar Date: Fri, 21 Aug 2026 21:44:00 +0200 Subject: [PATCH 8/8] fix: sql identifier in get lists query --- internal/repository/list.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/repository/list.go b/internal/repository/list.go index 5173f4f..dd451ed 100644 --- a/internal/repository/list.go +++ b/internal/repository/list.go @@ -70,7 +70,7 @@ func (r *ListRepo) DeleteList(ctx context.Context, listID string) error { func (r *ListRepo) GetLists(ctx context.Context, userID string) ([]domain.List, error) { rows, err := r.db.QueryContext(ctx, - "SELECT l.id, l.name, l.created_at, l.modified_at, (SELECT COUNT(*) FROM list_items WHERE list_id=l.id), (SELECT COUNT(*) FROM list_items WHERE list_id=l.id AND is_completed=true) FROM lists AS l INNER JOIN list_users ON lists.id=list_users.list_id WHERE list_users.user_id = $1", + "SELECT l.id, l.name, l.created_at, l.modified_at, (SELECT COUNT(*) FROM list_items WHERE list_id=l.id), (SELECT COUNT(*) FROM list_items WHERE list_id=l.id AND is_completed=true) FROM lists AS l INNER JOIN list_users ON l.id=list_users.list_id WHERE list_users.user_id = $1", userID, ) if err != nil {