veza/veza-backend-api/internal/utils/pagination.go

255 lines
5.8 KiB
Go
Raw Normal View History

2025-12-03 19:29:37 +00:00
//! Utilitaires de pagination optimisée
//!
//! Ce module implémente la pagination cursor-based qui est plus performante
//! que la pagination offset-based pour les grandes datasets.
package utils
import (
"encoding/base64"
"encoding/json"
"fmt"
"strconv"
"time"
)
// PaginationRequest représente une requête de pagination
type PaginationRequest struct {
Limit int `json:"limit" form:"limit"`
Cursor string `json:"cursor" form:"cursor"`
}
// PaginationResponse représente une réponse paginée
type PaginationResponse struct {
Data interface{} `json:"data"`
NextCursor string `json:"next_cursor,omitempty"`
PrevCursor string `json:"prev_cursor,omitempty"`
HasNext bool `json:"has_next"`
HasPrev bool `json:"has_prev"`
Total int64 `json:"total,omitempty"`
}
// Cursor représente un curseur de pagination
type Cursor struct {
ID string `json:"id"`
2025-12-03 19:29:37 +00:00
CreatedAt time.Time `json:"created_at"`
}
// EncodeCursor encode un curseur en string base64
func EncodeCursor(cursor *Cursor) (string, error) {
if cursor == nil {
return "", nil
}
data, err := json.Marshal(cursor)
if err != nil {
return "", fmt.Errorf("failed to marshal cursor: %w", err)
}
return base64.URLEncoding.EncodeToString(data), nil
}
// DecodeCursor décode un curseur depuis une string base64
func DecodeCursor(cursorStr string) (*Cursor, error) {
if cursorStr == "" {
return nil, nil
}
data, err := base64.URLEncoding.DecodeString(cursorStr)
if err != nil {
return nil, fmt.Errorf("failed to decode cursor: %w", err)
}
var cursor Cursor
if err := json.Unmarshal(data, &cursor); err != nil {
return nil, fmt.Errorf("failed to unmarshal cursor: %w", err)
}
return &cursor, nil
}
// CreateCursor crée un nouveau curseur à partir d'un ID et d'une date
func CreateCursor(id string, createdAt time.Time) *Cursor {
2025-12-03 19:29:37 +00:00
return &Cursor{
ID: id,
CreatedAt: createdAt,
}
}
// ValidatePaginationRequest valide une requête de pagination
func ValidatePaginationRequest(req *PaginationRequest) error {
if req.Limit <= 0 {
req.Limit = 20 // Valeur par défaut
}
if req.Limit > 100 {
req.Limit = 100 // Limite maximale
}
return nil
}
// BuildPaginationResponse construit une réponse paginée
func BuildPaginationResponse(
data interface{},
nextCursor *Cursor,
prevCursor *Cursor,
hasNext bool,
hasPrev bool,
total int64,
) (*PaginationResponse, error) {
response := &PaginationResponse{
Data: data,
HasNext: hasNext,
HasPrev: hasPrev,
Total: total,
}
// Encoder le curseur suivant
if nextCursor != nil {
nextCursorStr, err := EncodeCursor(nextCursor)
if err != nil {
return nil, fmt.Errorf("failed to encode next cursor: %w", err)
}
response.NextCursor = nextCursorStr
}
// Encoder le curseur précédent
if prevCursor != nil {
prevCursorStr, err := EncodeCursor(prevCursor)
if err != nil {
return nil, fmt.Errorf("failed to encode prev cursor: %w", err)
}
response.PrevCursor = prevCursorStr
}
return response, nil
}
// ParseLimit parse et valide la limite de pagination
func ParseLimit(limitStr string, defaultLimit int) int {
if limitStr == "" {
return defaultLimit
}
limit, err := strconv.Atoi(limitStr)
if err != nil || limit <= 0 {
return defaultLimit
}
if limit > 100 {
return 100
}
return limit
}
// ParseCursor parse et valide un curseur
func ParseCursor(cursorStr string) (*Cursor, error) {
if cursorStr == "" {
return nil, nil
}
return DecodeCursor(cursorStr)
}
// OffsetPaginationRequest représente une requête de pagination offset-based (legacy)
type OffsetPaginationRequest struct {
Page int `json:"page" form:"page"`
Limit int `json:"limit" form:"limit"`
}
// OffsetPaginationResponse représente une réponse paginée offset-based
type OffsetPaginationResponse struct {
Data interface{} `json:"data"`
Page int `json:"page"`
Limit int `json:"limit"`
Total int64 `json:"total"`
TotalPages int `json:"total_pages"`
HasNext bool `json:"has_next"`
HasPrev bool `json:"has_prev"`
}
// BuildOffsetPaginationResponse construit une réponse paginée offset-based
func BuildOffsetPaginationResponse(
data interface{},
page int,
limit int,
total int64,
) *OffsetPaginationResponse {
totalPages := int((total + int64(limit) - 1) / int64(limit))
return &OffsetPaginationResponse{
Data: data,
Page: page,
Limit: limit,
Total: total,
TotalPages: totalPages,
HasNext: page < totalPages,
HasPrev: page > 1,
}
}
// ValidateOffsetPaginationRequest valide une requête de pagination offset-based
func ValidateOffsetPaginationRequest(req *OffsetPaginationRequest) error {
if req.Page <= 0 {
req.Page = 1
}
if req.Limit <= 0 {
req.Limit = 20
}
if req.Limit > 100 {
req.Limit = 100
}
return nil
}
// CalculateOffset calcule l'offset pour la pagination offset-based
func CalculateOffset(page, limit int) int {
return (page - 1) * limit
}
// PaginationHelper contient des méthodes utilitaires pour la pagination
type PaginationHelper struct{}
// NewPaginationHelper crée un nouveau helper de pagination
func NewPaginationHelper() *PaginationHelper {
return &PaginationHelper{}
}
// GetDefaultLimit retourne la limite par défaut
func (h *PaginationHelper) GetDefaultLimit() int {
return 20
}
// GetMaxLimit retourne la limite maximale
func (h *PaginationHelper) GetMaxLimit() int {
return 100
}
// ValidateLimit valide et ajuste une limite
func (h *PaginationHelper) ValidateLimit(limit int) int {
if limit <= 0 {
return h.GetDefaultLimit()
}
if limit > h.GetMaxLimit() {
return h.GetMaxLimit()
}
return limit
}
// CreateEmptyResponse crée une réponse paginée vide
func (h *PaginationHelper) CreateEmptyResponse() *PaginationResponse {
return &PaginationResponse{
Data: []interface{}{},
HasNext: false,
HasPrev: false,
Total: 0,
}
}