254 lines
5.8 KiB
Go
254 lines
5.8 KiB
Go
//! 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"`
|
|
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 {
|
|
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,
|
|
}
|
|
}
|