package pagination import ( "encoding/base64" "encoding/json" "errors" "sort" "strings" "time" "gorm.io/gorm" ) type Cursor struct { Timestamp time.Time `json:"timestamp"` ID string `json:"id"` } func Encode(timestamp time.Time, id string) string { if timestamp.IsZero() || strings.TrimSpace(id) == "" { return "" } payload, err := json.Marshal(Cursor{ Timestamp: timestamp.UTC(), ID: id, }) if err != nil { return "" } return base64.RawURLEncoding.EncodeToString(payload) } func Decode(raw string) (*Cursor, error) { raw = strings.TrimSpace(raw) if raw == "" { return nil, nil } decoded, err := base64.RawURLEncoding.DecodeString(raw) if err != nil { return nil, err } var cursor Cursor if err := json.Unmarshal(decoded, &cursor); err != nil { return nil, err } if cursor.Timestamp.IsZero() || strings.TrimSpace(cursor.ID) == "" { return nil, errors.New("invalid cursor") } return &cursor, nil } func ComesAfter(timestamp time.Time, id string, cursor *Cursor) bool { if cursor == nil { return true } if timestamp.Before(cursor.Timestamp) { return true } return timestamp.Equal(cursor.Timestamp) && id < cursor.ID } func SortByKeyDesc[T any](items []T, key func(T) (time.Time, string)) { sort.SliceStable(items, func(i, j int) bool { leftTime, leftID := key(items[i]) rightTime, rightID := key(items[j]) if !leftTime.Equal(rightTime) { return leftTime.After(rightTime) } return leftID > rightID }) } func PageByCursor[T any](items []T, limit int, cursorRaw string, key func(T) (time.Time, string)) ([]T, string, error) { cursor, err := Decode(cursorRaw) if err != nil { return nil, "", err } filtered := make([]T, 0, len(items)) for _, item := range items { timestamp, id := key(item) if ComesAfter(timestamp, id, cursor) { filtered = append(filtered, item) } } if len(filtered) <= limit { return filtered, "", nil } page := filtered[:limit] lastTimestamp, lastID := key(page[len(page)-1]) return page, Encode(lastTimestamp, lastID), nil } func ApplyCreatedAtIDCursor(db *gorm.DB, cursor *Cursor, createdAtColumn string, idColumn string) *gorm.DB { if cursor == nil { return db } return db.Where( createdAtColumn+" < ? OR ("+createdAtColumn+" = ? AND "+idColumn+" < ?)", cursor.Timestamp, cursor.Timestamp, cursor.ID, ) }