cursor pagination

This commit is contained in:
Mazyar
2026-05-16 15:51:18 +03:30
parent bcb345103f
commit 70f581da09
7 changed files with 405 additions and 111 deletions
+57 -11
View File
@@ -26,7 +26,7 @@ type TenderRepository interface {
GetTenderCountByCountry(ctx context.Context) (map[string]int64, error)
GetTenderCountByType(ctx context.Context) (map[string]int64, error)
GetTenderCountByClassification(ctx context.Context) (map[string]int64, error)
Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) ([]Tender, int64, error)
Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchPageResult, error)
GetExpiredTenders(ctx context.Context, beforeDate int64) ([]Tender, error)
GetUnScrapedTenders(ctx context.Context, limit, skip int) ([]Tender, int64, error)
GetUnSummarizedTenders(ctx context.Context, limit, skip int) ([]Tender, int64, error)
@@ -40,11 +40,27 @@ func collectionName() string {
return "tenders"
}
// deepOffsetSkipCountThreshold skips CountDocuments when offset pagination goes this deep.
const deepOffsetSkipCountThreshold = 1000
// SearchPageResult is the paginated outcome of a tender search/list query.
type SearchPageResult struct {
Items []Tender
TotalCount int64 // -1 when count was skipped
NextCursor string
HasMore bool
}
// tenderSearchListProjection excludes large fields not needed for list/search API payloads.
func tenderSearchListProjection() bson.M {
return bson.M{
"content_xml": 0,
"document_summaries": 0,
"content_xml": 0,
"document_summaries": 0,
"scraped_documents": 0,
"selection_criteria": 0,
"modifications": 0,
"source_file_url": 0,
"source_file_name": 0,
}
}
@@ -85,6 +101,26 @@ func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Te
*orm.NewIndex("tender_deadline_idx", bson.D{{Key: "tender_deadline", Value: 1}}),
*orm.NewIndex("estimated_value_idx", bson.D{{Key: "estimated_value", Value: -1}}),
*orm.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}),
// Stable keyset pagination for default admin list (created_at desc).
*orm.NewIndex("created_at_id_idx", bson.D{
{Key: "created_at", Value: -1},
{Key: "_id", Value: -1},
}),
*orm.NewIndex("status_created_at_id_idx", bson.D{
{Key: "status", Value: 1},
{Key: "created_at", Value: -1},
{Key: "_id", Value: -1},
}),
*orm.NewIndex("country_code_created_at_id_idx", bson.D{
{Key: "country_code", Value: 1},
{Key: "created_at", Value: -1},
{Key: "_id", Value: -1},
}),
*orm.NewIndex("notice_type_code_created_at_id_idx", bson.D{
{Key: "notice_type_code", Value: 1},
{Key: "created_at", Value: -1},
{Key: "_id", Value: -1},
}),
*orm.NewIndex("updated_at_idx", bson.D{{Key: "updated_at", Value: -1}}),
// Compound indexes for common queries
*orm.NewIndex("status_country_idx", bson.D{
@@ -321,10 +357,9 @@ func (r *tenderRepository) Delete(ctx context.Context, id string) error {
}
// Search searches tenders based on criteria
func (r *tenderRepository) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) ([]Tender, int64, error) {
func (r *tenderRepository) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchPageResult, error) {
filter := r.buildSearchFilter(form)
// Build sort
sort := "created_at"
if pagination.SortBy != "" {
sort = pagination.SortBy
@@ -335,23 +370,34 @@ func (r *tenderRepository) Search(ctx context.Context, form *SearchForm, paginat
sortOrder = pagination.SortOrder
}
// Build pagination
skipCount := pagination.Cursor != "" || pagination.Offset >= deepOffsetSkipCountThreshold
paginationBuilder := orm.NewPaginationBuilder().
Limit(pagination.Limit).
Skip(pagination.Offset).
SortBy(sort, sortOrder).
Projection(tenderSearchListProjection()).
Build()
SkipCount(skipCount)
result, err := r.ormRepo.FindAll(ctx, filter, paginationBuilder)
if pagination.Cursor != "" {
paginationBuilder.Cursor(pagination.Cursor)
} else {
paginationBuilder.Skip(pagination.Offset)
}
result, err := r.ormRepo.FindAll(ctx, filter, paginationBuilder.Build())
if err != nil {
r.logger.Error("Failed to search tenders", map[string]interface{}{
"error": err.Error(),
})
return nil, 0, err
return nil, err
}
return result.Items, result.TotalCount, nil
return &SearchPageResult{
Items: result.Items,
TotalCount: result.TotalCount,
NextCursor: result.NextCursor,
HasMore: result.HasMore,
}, nil
}
// GetExpiredTenders retrieves tenders that are expired