Skip to content

Commit a124646

Browse files
committed
feat(mongox): fix pagination
1 parent 586dfb9 commit a124646

File tree

2 files changed

+171
-34
lines changed

2 files changed

+171
-34
lines changed

mongox/pagination.go

Lines changed: 77 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,38 @@ func (c *Collection) Paginate(ctx context.Context, rawFilter any, s *usecasex.So
1717
return nil, nil
1818
}
1919

20-
pFilter, pOpts, err := c.findFilter(ctx, *p, s)
20+
pFilter, err := c.pageFilter(ctx, *p, s)
2121
if err != nil {
2222
return nil, rerror.ErrInternalByWithContext(ctx, err)
2323
}
24+
2425
filter := rawFilter
2526
if pFilter != nil {
2627
filter = And(rawFilter, "", pFilter)
2728
}
2829

29-
cursor, err := c.collection.Find(ctx, filter, append([]*options.FindOptions{pOpts}, opts...)...)
30+
sortKey := idKey
31+
sortOrder := 1
32+
if s != nil && s.Key != "" {
33+
sortKey = s.Key
34+
if s.Reverted {
35+
sortOrder = -1
36+
}
37+
}
38+
39+
if p.Cursor != nil && p.Cursor.Last != nil {
40+
sortOrder *= -1
41+
}
42+
43+
findOpts := options.Find().
44+
SetSort(bson.D{{Key: sortKey, Value: sortOrder}, {Key: idKey, Value: sortOrder}}).
45+
SetLimit(limit(*p))
46+
47+
if p.Offset != nil {
48+
findOpts.SetSkip(p.Offset.Offset)
49+
}
50+
51+
cursor, err := c.collection.Find(ctx, filter, append([]*options.FindOptions{findOpts}, opts...)...)
3052
if err != nil {
3153
return nil, rerror.ErrInternalByWithContext(ctx, fmt.Errorf("failed to find: %w", err))
3254
}
@@ -46,6 +68,7 @@ func (c *Collection) Paginate(ctx context.Context, rawFilter any, s *usecasex.So
4668

4769
if p.Cursor != nil && p.Cursor.Last != nil {
4870
reverse(items)
71+
startCursor, endCursor = endCursor, startCursor
4972
}
5073

5174
for _, item := range items {
@@ -59,7 +82,6 @@ func (c *Collection) Paginate(ctx context.Context, rawFilter any, s *usecasex.So
5982
return usecasex.NewPageInfo(count, startCursor, endCursor, hasNextPage, hasPreviousPage), nil
6083
}
6184

62-
6385
func (c *Collection) PaginateAggregation(ctx context.Context, pipeline []any, s *usecasex.Sort, p *usecasex.Pagination, consumer Consumer, opts ...*options.AggregateOptions) (*usecasex.PageInfo, error) {
6486
if p == nil || p.Cursor == nil && p.Offset == nil {
6587
return nil, nil
@@ -212,56 +234,77 @@ func aggregateOptionsFromPagination(_ usecasex.Pagination, _ *usecasex.Sort) *op
212234
return options.Aggregate().SetAllowDiskUse(true).SetCollation(&collation)
213235
}
214236

215-
func (c *Collection) pageFilter(ctx context.Context, p usecasex.Pagination, s *usecasex.Sort) (any, error) {
237+
func (c *Collection) pageFilter(ctx context.Context, p usecasex.Pagination, s *usecasex.Sort) (bson.M, error) {
216238
if p.Cursor == nil {
217239
return nil, nil
218240
}
219241

242+
var filter bson.M
243+
sortKey := idKey
244+
sortOrder := 1
245+
246+
if s != nil && s.Key != "" {
247+
sortKey = s.Key
248+
if s.Reverted {
249+
sortOrder = -1
250+
}
251+
}
252+
253+
var cursor *usecasex.Cursor
220254
var op string
221-
var cur *usecasex.Cursor
222255

223-
if p.Cursor.First != nil {
256+
if p.Cursor.After != nil {
257+
cursor = p.Cursor.After
224258
op = "$gt"
225-
cur = p.Cursor.After
226-
} else if p.Cursor.Last != nil {
259+
} else if p.Cursor.Before != nil {
260+
cursor = p.Cursor.Before
227261
op = "$lt"
228-
cur = p.Cursor.Before
229-
} else {
230-
return nil, errors.New("neither first nor last are set")
231262
}
232-
if cur == nil {
233-
return nil, nil
234-
}
235-
236-
var sortKey *string
237-
if s != nil {
238-
sortKey = &s.Key
239-
}
240-
var paginationFilter bson.M
241-
if sortKey == nil || *sortKey == "" {
242-
paginationFilter = bson.M{idKey: bson.M{op: *cur}}
243-
} else {
244-
var cursorDoc bson.M
245-
if err := c.collection.FindOne(ctx, bson.M{idKey: *cur}).Decode(&cursorDoc); err != nil {
246-
return nil, fmt.Errorf("failed to find cursor element")
247-
}
248263

249-
if cursorDoc[*sortKey] == nil {
250-
return nil, fmt.Errorf("invalied sort key")
264+
if cursor != nil {
265+
cursorDoc, err := c.getCursorDocument(ctx, *cursor)
266+
if err != nil {
267+
return nil, err
251268
}
252269

253-
paginationFilter = bson.M{
270+
filter = bson.M{
254271
"$or": []bson.M{
255-
{*sortKey: bson.M{op: cursorDoc[*sortKey]}},
272+
{sortKey: bson.M{op: cursorDoc[sortKey]}},
256273
{
257-
*sortKey: cursorDoc[*sortKey],
258-
idKey: bson.M{op: *cur},
274+
sortKey: cursorDoc[sortKey],
275+
idKey: bson.M{op: cursorDoc[idKey]},
259276
},
260277
},
261278
}
279+
280+
if sortOrder == -1 {
281+
if op == "$gt" {
282+
op = "$lt"
283+
} else {
284+
op = "$gt"
285+
}
286+
filter = bson.M{
287+
"$or": []bson.M{
288+
{sortKey: bson.M{op: cursorDoc[sortKey]}},
289+
{
290+
sortKey: cursorDoc[sortKey],
291+
idKey: bson.M{op: cursorDoc[idKey]},
292+
},
293+
},
294+
}
295+
}
262296
}
263297

264-
return paginationFilter, nil
298+
return filter, nil
299+
}
300+
301+
func (c *Collection) getCursorDocument(ctx context.Context, cursor usecasex.Cursor) (bson.M, error) {
302+
var cursorDoc bson.M
303+
err := c.collection.FindOne(ctx, bson.M{idKey: cursor}).Decode(&cursorDoc)
304+
if err != nil {
305+
return nil, fmt.Errorf("failed to find cursor element: %w", err)
306+
}
307+
return cursorDoc, nil
265308
}
266309

267310
func sortFilter(p usecasex.Pagination, s *usecasex.Sort) bson.D {

mongox/pagination_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,100 @@ func TestClientCollection_PaginateWithUpdatedAtSort(t *testing.T) {
338338
assert.Equal(t, []usecasex.Cursor{"c", "d", "e"}, con.Cursors)
339339
}
340340

341+
func TestClientCollection_DetailedPagination(t *testing.T) {
342+
ctx := context.Background()
343+
initDB := mongotest.Connect(t)
344+
c := NewCollection(initDB(t).Collection("test"))
345+
346+
// Seed data
347+
seeds := []struct {
348+
id string
349+
updatedAt int64
350+
}{
351+
{"a", 1000},
352+
{"b", 2000},
353+
{"c", 3000},
354+
{"d", 4000},
355+
{"e", 5000},
356+
}
357+
358+
_, _ = c.Client().InsertMany(ctx, lo.Map(seeds, func(s struct {
359+
id string
360+
updatedAt int64
361+
}, i int) any {
362+
return bson.M{"id": s.id, "updatedAt": s.updatedAt}
363+
}))
364+
365+
testCases := []struct {
366+
name string
367+
sort *usecasex.Sort
368+
pagination *usecasex.CursorPagination
369+
expected []string
370+
}{
371+
{
372+
name: "First 2, Ascending",
373+
sort: &usecasex.Sort{Key: "updatedAt", Reverted: false},
374+
pagination: &usecasex.CursorPagination{First: lo.ToPtr(int64(2))},
375+
expected: []string{"a", "b"},
376+
},
377+
{
378+
name: "First 2, Descending",
379+
sort: &usecasex.Sort{Key: "updatedAt", Reverted: true},
380+
pagination: &usecasex.CursorPagination{First: lo.ToPtr(int64(2))},
381+
expected: []string{"e", "d"},
382+
},
383+
{
384+
name: "Last 2, Ascending",
385+
sort: &usecasex.Sort{Key: "updatedAt", Reverted: false},
386+
pagination: &usecasex.CursorPagination{Last: lo.ToPtr(int64(2))},
387+
expected: []string{"d", "e"},
388+
},
389+
{
390+
name: "Last 2, Descending",
391+
sort: &usecasex.Sort{Key: "updatedAt", Reverted: true},
392+
pagination: &usecasex.CursorPagination{Last: lo.ToPtr(int64(2))},
393+
expected: []string{"b", "a"},
394+
},
395+
{
396+
name: "First 2 After 'b', Ascending",
397+
sort: &usecasex.Sort{Key: "updatedAt", Reverted: false},
398+
pagination: &usecasex.CursorPagination{First: lo.ToPtr(int64(2)), After: usecasex.Cursor("b").Ref()},
399+
expected: []string{"c", "d"},
400+
},
401+
{
402+
name: "First 2 After 'd', Descending",
403+
sort: &usecasex.Sort{Key: "updatedAt", Reverted: true},
404+
pagination: &usecasex.CursorPagination{First: lo.ToPtr(int64(2)), After: usecasex.Cursor("d").Ref()},
405+
expected: []string{"c", "b"},
406+
},
407+
{
408+
name: "Last 2 Before 'd', Ascending",
409+
sort: &usecasex.Sort{Key: "updatedAt", Reverted: false},
410+
pagination: &usecasex.CursorPagination{Last: lo.ToPtr(int64(2)), Before: usecasex.Cursor("d").Ref()},
411+
expected: []string{"b", "c"},
412+
},
413+
{
414+
name: "Last 2 Before 'b', Descending",
415+
sort: &usecasex.Sort{Key: "updatedAt", Reverted: true},
416+
pagination: &usecasex.CursorPagination{Last: lo.ToPtr(int64(2)), Before: usecasex.Cursor("b").Ref()},
417+
expected: []string{"d", "c"},
418+
},
419+
}
420+
421+
for _, tc := range testCases {
422+
t.Run(tc.name, func(t *testing.T) {
423+
con := &consumer{}
424+
_, err := c.Paginate(ctx, bson.M{}, tc.sort, tc.pagination.Wrap(), con)
425+
assert.NoError(t, err)
426+
427+
gotIDs := lo.Map(con.Cursors, func(c usecasex.Cursor, _ int) string {
428+
return string(c)
429+
})
430+
assert.Equal(t, tc.expected, gotIDs)
431+
})
432+
}
433+
}
434+
341435
type consumer struct {
342436
Cursors []usecasex.Cursor
343437
}

0 commit comments

Comments
 (0)