|
|
@@ -2,12 +2,42 @@ package services
|
|
|
|
|
|
import (
|
|
|
"context"
|
|
|
+ "encoding/base64"
|
|
|
"errors"
|
|
|
"fmt"
|
|
|
+ "strconv"
|
|
|
+ "strings"
|
|
|
+ "time"
|
|
|
|
|
|
"gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/models"
|
|
|
)
|
|
|
|
|
|
+func encodeCursor(p *models.Place) string {
|
|
|
+ s := p.ID + "|" + strconv.FormatInt(p.CreatedAt.UnixNano(), 10) + "|" + fmt.Sprintf("%.6f", p.Rating)
|
|
|
+ return base64.RawURLEncoding.EncodeToString([]byte(s))
|
|
|
+}
|
|
|
+
|
|
|
+func DecodeCursor(cursor string) (id string, createdAt time.Time, rating float64, err error) {
|
|
|
+ b, err := base64.RawURLEncoding.DecodeString(cursor)
|
|
|
+ if err != nil {
|
|
|
+ return "", time.Time{}, 0, fmt.Errorf("decode cursor: %w", err)
|
|
|
+ }
|
|
|
+ parts := strings.SplitN(string(b), "|", 3)
|
|
|
+ if len(parts) < 2 {
|
|
|
+ return "", time.Time{}, 0, fmt.Errorf("invalid cursor format")
|
|
|
+ }
|
|
|
+ id = parts[0]
|
|
|
+ createdAtUnix, err := strconv.ParseInt(parts[1], 10, 64)
|
|
|
+ if err != nil {
|
|
|
+ return "", time.Time{}, 0, fmt.Errorf("parse cursor created_at: %w", err)
|
|
|
+ }
|
|
|
+ createdAt = time.Unix(0, createdAtUnix)
|
|
|
+ if len(parts) > 2 {
|
|
|
+ rating, _ = strconv.ParseFloat(parts[2], 64)
|
|
|
+ }
|
|
|
+ return
|
|
|
+}
|
|
|
+
|
|
|
var (
|
|
|
ErrNotYourPlace = errors.New("not your place")
|
|
|
ErrPlaceNotFound = errors.New("place not found")
|
|
|
@@ -105,7 +135,7 @@ func (s *PlaceService) GetByID(ctx context.Context, id string, fetchTags bool) (
|
|
|
return place, nil
|
|
|
}
|
|
|
|
|
|
-func (s *PlaceService) List(ctx context.Context, filter models.PlaceFilter) ([]*models.Place, error) {
|
|
|
+func (s *PlaceService) List(ctx context.Context, filter models.PlaceFilter) (*models.PaginatedPlaces, error) {
|
|
|
places, err := s.placeRepo.List(ctx, filter)
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
@@ -137,7 +167,22 @@ func (s *PlaceService) List(ctx context.Context, filter models.PlaceFilter) ([]*
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- return places, nil
|
|
|
+ limit := filter.Limit()
|
|
|
+ hasMore := len(places) > limit
|
|
|
+ if hasMore {
|
|
|
+ places = places[:limit]
|
|
|
+ }
|
|
|
+
|
|
|
+ var nextCursor string
|
|
|
+ if hasMore && len(places) > 0 {
|
|
|
+ nextCursor = encodeCursor(places[len(places)-1])
|
|
|
+ }
|
|
|
+
|
|
|
+ return &models.PaginatedPlaces{
|
|
|
+ Data: places,
|
|
|
+ NextCursor: nextCursor,
|
|
|
+ HasMore: hasMore,
|
|
|
+ }, nil
|
|
|
}
|
|
|
|
|
|
type UpdatePlaceInput struct {
|