Added pagination to invites #34

Merged
robin merged 2 commits from dev into main 2026-09-01 10:19:18 +02:00
6 changed files with 105 additions and 9 deletions
+1
View File
@@ -0,0 +1 @@
package dttmr_api
+62 -3
View File
@@ -3,6 +3,7 @@ package handler
import (
"log/slog"
"net/http"
"strconv"
"github.com/robindittmar/dttmr-api/internal/api/response"
"github.com/robindittmar/dttmr-api/internal/domain"
@@ -94,12 +95,59 @@ func (h *InviteHandler) DeleteInvite(w http.ResponseWriter, r *http.Request) {
// @Tags Invite
// @Accept json
// @Produce json
// @Success 200 {object} []domain.Invite
// @Param page query int false "page"
// @Param count query int false "count"
// @Success 200 {object} response.Paginated[domain.Invite]
// @Error 400 {object} response.ErrorResponse "failed to decode request body"
// @Error 400 {object} response.ErrorResponse "invalid value for page"
// @Error 400 {object} response.ErrorResponse "invalid value for count"
// @Error 500 {object} response.ErrorResponse "failed to get invites"
// @Router /user/invites [get]
func (h *InviteHandler) GetInvites(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
pageStr := r.URL.Query().Get("page")
if pageStr == "" {
pageStr = "1"
}
countStr := r.URL.Query().Get("count")
if countStr == "" {
countStr = "10"
}
page, err := strconv.Atoi(pageStr)
if err != nil {
slog.ErrorContext(ctx,
"failed to read page from query",
slog.String("page", pageStr))
response.Error(ctx, w, http.StatusBadRequest, "failed to decode request url")
return
}
count, err := strconv.Atoi(countStr)
if err != nil {
slog.ErrorContext(ctx,
"failed to read count from query",
slog.String("count", countStr))
response.Error(ctx, w, http.StatusBadRequest, "failed to decode request url")
return
}
if page < 1 {
slog.ErrorContext(ctx,
"page parameter is invalid",
slog.Int("page", page))
response.Error(ctx, w, http.StatusBadRequest, "invalid value for page")
return
}
if count <= 0 {
slog.ErrorContext(ctx,
"count parameter is invalid",
slog.Int("count", count))
response.Error(ctx, w, http.StatusBadRequest, "invalid value for count")
return
}
authContext, err := domain.GetAuthContext(ctx)
if err != nil {
slog.ErrorContext(ctx, "failed to get auth context", slog.Any("error", err))
@@ -107,12 +155,23 @@ func (h *InviteHandler) GetInvites(w http.ResponseWriter, r *http.Request) {
return
}
invites, err := h.InviteService.GetInvites(ctx, authContext.UserID)
invites, err := h.InviteService.GetInvites(ctx, authContext.UserID, page, count)
if err != nil {
slog.ErrorContext(ctx, "failed to get invites", slog.Any("error", err))
response.Error(ctx, w, http.StatusInternalServerError, "failed to get invites")
return
}
response.JSON(ctx, w, http.StatusOK, invites)
total, err := h.InviteService.CountInvites(ctx, authContext.UserID)
if err != nil {
slog.ErrorContext(ctx, "failed to count invites", slog.Any("error", err))
response.Error(ctx, w, http.StatusInternalServerError, "failed to get invites")
return
}
response.JSON(ctx, w, http.StatusOK, response.Paginated[domain.Invite]{
Count: len(invites),
Total: total,
Data: invites,
})
}
+6
View File
@@ -0,0 +1,6 @@
package request
type GetInvitesPayload struct {
Page int
CountPerPage int
}
+7
View File
@@ -0,0 +1,7 @@
package response
type Paginated[T any] struct {
Count int `json:"count"`
Total int `json:"total"`
Data []T `json:"data"`
}
+13 -3
View File
@@ -25,7 +25,8 @@ type InviteRepository interface {
DeleteInvite(ctx context.Context, userID string, inviteID string) error
ConsumeInvite(ctx context.Context, inviteID string, inviteeUserID string) error
GetInvite(ctx context.Context, code string) (*Invite, error)
GetInvites(ctx context.Context, userID string) ([]Invite, error)
GetInvites(ctx context.Context, userID string, offset int, count int) ([]Invite, error)
CountInvites(ctx context.Context, userID string) (int, error)
}
type InviteService struct {
@@ -89,10 +90,19 @@ func (s *InviteService) GetInvite(ctx context.Context, code string) (*Invite, er
return invite, nil
}
func (s *InviteService) GetInvites(ctx context.Context, userID string) ([]Invite, error) {
func (s *InviteService) GetInvites(ctx context.Context, userID string, page int, countPerPage int) ([]Invite, error) {
if userID == "" {
return nil, ErrUserIDMissing
}
return s.repo.GetInvites(ctx, userID)
offset := (page - 1) * countPerPage
return s.repo.GetInvites(ctx, userID, offset, countPerPage)
}
func (s *InviteService) CountInvites(ctx context.Context, userID string) (int, error) {
if userID == "" {
return 0, ErrUserIDMissing
}
return s.repo.CountInvites(ctx, userID)
}
+16 -3
View File
@@ -81,10 +81,10 @@ func (r *InviteRepo) GetInvite(ctx context.Context, code string) (*domain.Invite
return &invite, nil
}
func (r *InviteRepo) GetInvites(ctx context.Context, userID string) ([]domain.Invite, error) {
func (r *InviteRepo) GetInvites(ctx context.Context, userID string, offset int, count int) ([]domain.Invite, error) {
rows, err := r.db.QueryContext(ctx,
"SELECT id, code, expires_at, consumed_at FROM invites WHERE inviter_user_id=$1",
userID,
"SELECT id, code, expires_at, consumed_at FROM invites WHERE inviter_user_id=$1 ORDER BY created_at DESC OFFSET $2 LIMIT $3",
userID, offset, count,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
@@ -107,3 +107,16 @@ func (r *InviteRepo) GetInvites(ctx context.Context, userID string) ([]domain.In
return invites, nil
}
func (r *InviteRepo) CountInvites(ctx context.Context, userID string) (int, error) {
var count int
err := r.db.QueryRowContext(ctx,
"SELECT COUNT(*) FROM invites WHERE inviter_user_id=$1",
userID,
).Scan(&count)
if err != nil {
return 0, fmt.Errorf("failed to count invites: %w", err)
}
return count, nil
}