Delete lists and add users via email #22

Merged
robin merged 8 commits from dev into main 2026-08-21 21:51:53 +02:00
5 changed files with 133 additions and 6 deletions
Showing only changes of commit c2912d8d79 - Show all commits
+82 -1
View File
@@ -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"
+2
View File
@@ -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))
+27 -1
View File
@@ -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
+4 -4
View File
@@ -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)
+18
View File
@@ -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,