diff --git a/internal/api/handler/invite.go b/internal/api/handler/invite.go index c8619eb..20d570b 100644 --- a/internal/api/handler/invite.go +++ b/internal/api/handler/invite.go @@ -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, + }) } diff --git a/internal/api/request/invite.go b/internal/api/request/invite.go new file mode 100644 index 0000000..5047925 --- /dev/null +++ b/internal/api/request/invite.go @@ -0,0 +1,6 @@ +package request + +type GetInvitesPayload struct { + Page int + CountPerPage int +} diff --git a/internal/api/response/paginated.go b/internal/api/response/paginated.go new file mode 100644 index 0000000..91867e1 --- /dev/null +++ b/internal/api/response/paginated.go @@ -0,0 +1,7 @@ +package response + +type Paginated[T any] struct { + Count int `json:"count"` + Total int `json:"total"` + Data []T `json:"data"` +} diff --git a/internal/domain/invite.go b/internal/domain/invite.go index 3ca7076..c70119c 100644 --- a/internal/domain/invite.go +++ b/internal/domain/invite.go @@ -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) } diff --git a/internal/repository/invite.go b/internal/repository/invite.go index 0df87f4..776b86c 100644 --- a/internal/repository/invite.go +++ b/internal/repository/invite.go @@ -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 +}