//! 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, } }