hybrid pagination
This commit is contained in:
+22
-6
@@ -37,6 +37,7 @@ type PaginatedResult[T any] struct {
|
||||
TotalCount int64 `json:"totalCount"` // Total number of documents matching the filter
|
||||
NextCursor string `json:"nextCursor"` // Base64 encoded cursor for next page
|
||||
HasMore bool `json:"hasMore"` // Whether there are more documents
|
||||
PageOffset int `json:"pageOffset"` // Zero-based offset of the first item in this page
|
||||
}
|
||||
|
||||
// Repository defines the interface for MongoDB operations
|
||||
@@ -144,6 +145,16 @@ func (r *repository[T]) FindAll(ctx context.Context, filter bson.M, pagination P
|
||||
return nil, fmt.Errorf("failed to clone filter for count: %w", err)
|
||||
}
|
||||
|
||||
filtersHash := HashFilter(filter)
|
||||
pageOffset := pagination.Skip
|
||||
if pagination.Cursor != "" {
|
||||
pageCursor, err := decodePageCursor(pagination.Cursor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pageOffset = pageCursor.PageOffset
|
||||
}
|
||||
|
||||
// Build find options
|
||||
findOptions := options.Find().
|
||||
SetSort(sort).
|
||||
@@ -155,7 +166,7 @@ func (r *repository[T]) FindAll(ctx context.Context, filter bson.M, pagination P
|
||||
|
||||
// Handle cursor-based pagination
|
||||
if pagination.Cursor != "" {
|
||||
cursorFilter, err := r.buildCursorFilter(pagination.Cursor, pagination.SortField, pagination.SortOrder)
|
||||
cursorFilter, err := r.buildCursorFilter(pagination.Cursor, pagination.SortField, pagination.SortOrder, filtersHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -209,12 +220,13 @@ func (r *repository[T]) FindAll(ctx context.Context, filter bson.M, pagination P
|
||||
// Generate next cursor
|
||||
var nextCursor string
|
||||
if hasMore && len(items) > 0 {
|
||||
nextCursor, err = r.generateCursor(items[len(items)-1], pagination.SortField, pagination.SortOrder)
|
||||
nextPageOffset := pageOffset + pagination.Limit
|
||||
nextCursor, err = r.generateCursor(items[len(items)-1], pagination.SortField, pagination.SortOrder, filtersHash, nextPageOffset)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to generate next cursor", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
// Don't fail the entire query, just leave cursor empty
|
||||
return nil, fmt.Errorf("failed to generate pagination cursor: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,6 +235,7 @@ func (r *repository[T]) FindAll(ctx context.Context, filter bson.M, pagination P
|
||||
TotalCount: totalCount,
|
||||
NextCursor: nextCursor,
|
||||
HasMore: hasMore,
|
||||
PageOffset: pageOffset,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -454,7 +467,7 @@ func (r *repository[T]) validatePagination(pagination Pagination) error {
|
||||
}
|
||||
|
||||
// buildCursorFilter builds a filter for cursor-based pagination.
|
||||
func (r *repository[T]) buildCursorFilter(cursor, sortField string, sortOrder int) (bson.M, error) {
|
||||
func (r *repository[T]) buildCursorFilter(cursor, sortField string, sortOrder int, filtersHash string) (bson.M, error) {
|
||||
pageCursor, err := decodePageCursor(cursor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -462,11 +475,14 @@ func (r *repository[T]) buildCursorFilter(cursor, sortField string, sortOrder in
|
||||
if !pageCursor.matches(sortField, sortOrder) {
|
||||
return nil, fmt.Errorf("%w: cursor does not match requested sort", ErrInvalidCursor)
|
||||
}
|
||||
if pageCursor.FiltersHash != filtersHash {
|
||||
return nil, fmt.Errorf("%w: cursor does not match current filters", ErrInvalidCursor)
|
||||
}
|
||||
return pageCursor.mongoFilter()
|
||||
}
|
||||
|
||||
// generateCursor generates an opaque cursor from the last document in a page.
|
||||
func (r *repository[T]) generateCursor(doc T, sortField string, sortOrder int) (string, error) {
|
||||
func (r *repository[T]) generateCursor(doc T, sortField string, sortOrder int, filtersHash string, pageOffset int) (string, error) {
|
||||
docBytes, err := bson.Marshal(doc)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal document: %w", err)
|
||||
@@ -487,7 +503,7 @@ func (r *repository[T]) generateCursor(doc T, sortField string, sortOrder int) (
|
||||
return "", err
|
||||
}
|
||||
|
||||
return encodePageCursor(sortField, sortOrder, sortValue, idHex)
|
||||
return encodePageCursor(sortField, sortOrder, sortValue, idHex, filtersHash, pageOffset)
|
||||
}
|
||||
|
||||
// getModelID extracts the ID from a model
|
||||
|
||||
Reference in New Issue
Block a user