diff --git a/mongox/pagination.go b/mongox/pagination.go index cfd7c56..af1a458 100644 --- a/mongox/pagination.go +++ b/mongox/pagination.go @@ -27,64 +27,7 @@ func (c *Collection) Paginate(ctx context.Context, rawFilter any, s *usecasex.So filter = And(rawFilter, "", pFilter) } - sortKey := idKey - sortOrder := 1 - if s != nil && s.Key != "" { - sortKey = s.Key - if s.Reverted { - sortOrder = -1 - } - } - - if p.Cursor != nil && p.Cursor.Last != nil { - sortOrder *= -1 - } - - sort := bson.D{{Key: sortKey, Value: sortOrder}} - if sortKey != idKey { - sort = append(sort, bson.E{Key: idKey, Value: sortOrder}) - } - - findOpts := options.Find(). - SetSort(sort). - SetLimit(limit(*p)) - - if p.Offset != nil { - findOpts.SetSkip(p.Offset.Offset) - } - - cursor, err := c.collection.Find(ctx, filter, append([]*options.FindOptions{findOpts}, opts...)...) - if err != nil { - return nil, rerror.ErrInternalByWithContext(ctx, fmt.Errorf("failed to find: %w", err)) - } - defer func() { - _ = cursor.Close(ctx) - }() - - count, err := c.collection.CountDocuments(ctx, rawFilter) - if err != nil { - return nil, rerror.ErrInternalByWithContext(ctx, fmt.Errorf("failed to count: %w", err)) - } - - items, startCursor, endCursor, hasMore, err := consume(ctx, cursor, limit(*p)) - if err != nil { - return nil, err - } - - if p.Cursor != nil && p.Cursor.Last != nil { - reverse(items) - startCursor, endCursor = endCursor, startCursor - } - - for _, item := range items { - if err := consumer.Consume(item); err != nil { - return nil, err - } - } - - hasNextPage, hasPreviousPage := pageInfo(p, hasMore) - - return usecasex.NewPageInfo(count, startCursor, endCursor, hasNextPage, hasPreviousPage), nil + return c.paginate(ctx, rawFilter, s, p, filter, consumer, opts) } func (c *Collection) PaginateAggregation(ctx context.Context, pipeline []any, s *usecasex.Sort, p *usecasex.Pagination, consumer Consumer, opts ...*options.AggregateOptions) (*usecasex.PageInfo, error) { @@ -327,3 +270,84 @@ func sortDirection(p usecasex.Pagination, s *usecasex.Sort) int { } return 1 } + +func (c *Collection) PaginateProject(ctx context.Context, rawFilter any, s *usecasex.Sort, p *usecasex.Pagination, consumer Consumer, opts ...*options.FindOptions) (*usecasex.PageInfo, error) { + if p == nil || (p.Cursor == nil && p.Offset == nil) { + return nil, nil + } + + pFilter, err := c.pageFilter(ctx, *p, s) + if err != nil { + return nil, rerror.ErrInternalByWithContext(ctx, err) + } + + filter := rawFilter + if pFilter != nil { + filter = AddCondition(rawFilter, "", pFilter) + } + + return c.paginate(ctx, rawFilter, s, p, filter, consumer, opts) + +} + +func (c *Collection) paginate(ctx context.Context, rawFilter any, s *usecasex.Sort, p *usecasex.Pagination, filter any, consumer Consumer, opts []*options.FindOptions) (*usecasex.PageInfo, error) { + + sortKey := idKey + sortOrder := 1 + if s != nil && s.Key != "" { + sortKey = s.Key + if s.Reverted { + sortOrder = -1 + } + } + + if p.Cursor != nil && p.Cursor.Last != nil { + sortOrder *= -1 + } + + sort := bson.D{{Key: sortKey, Value: sortOrder}} + if sortKey != idKey { + sort = append(sort, bson.E{Key: idKey, Value: sortOrder}) + } + + findOpts := options.Find(). + SetSort(sort). + SetLimit(limit(*p)) + + if p.Offset != nil { + findOpts.SetSkip(p.Offset.Offset) + } + + cursor, err := c.collection.Find(ctx, filter, append([]*options.FindOptions{findOpts}, opts...)...) + if err != nil { + return nil, rerror.ErrInternalByWithContext(ctx, fmt.Errorf("failed to find: %w", err)) + } + defer func() { + _ = cursor.Close(ctx) + }() + + count, err := c.collection.CountDocuments(ctx, rawFilter) + if err != nil { + return nil, rerror.ErrInternalByWithContext(ctx, fmt.Errorf("failed to count: %w", err)) + } + + items, startCursor, endCursor, hasMore, err := consume(ctx, cursor, limit(*p)) + if err != nil { + return nil, err + } + + if p.Cursor != nil && p.Cursor.Last != nil { + reverse(items) + startCursor, endCursor = endCursor, startCursor + } + + for _, item := range items { + if err := consumer.Consume(item); err != nil { + return nil, err + } + } + + hasNextPage, hasPreviousPage := pageInfo(p, hasMore) + + return usecasex.NewPageInfo(count, startCursor, endCursor, hasNextPage, hasPreviousPage), nil +} diff --git a/mongox/util.go b/mongox/util.go index d1fb14c..bdb994a 100644 --- a/mongox/util.go +++ b/mongox/util.go @@ -157,3 +157,58 @@ func And(filter interface{}, key string, f interface{}) interface{} { } return AppendE(filter, bson.E{Key: key, Value: f}) } + +func isEmptyCondition(condition interface{}) bool { + switch c := condition.(type) { + case bson.M: + return len(c) == 0 + case bson.D: + return len(c) == 0 + case bson.A: + return len(c) == 0 + case []bson.M: + return len(c) == 0 + case []bson.D: + return len(c) == 0 + case []bson.A: + return len(c) == 0 + case []interface{}: + return len(c) == 0 + default: + return false + } +} + +func AddCondition(filter interface{}, key string, condition interface{}) interface{} { + if condition == nil || isEmptyCondition(condition) { + return filter + } + + filterMap, ok := filter.(bson.M) + if !ok || len(filterMap) == 0 { + filterMap = bson.M{} + } + + var newCondition interface{} + if key != "" { + if _, exists := filterMap[key]; exists { + return filter + } + newCondition = bson.M{key: condition} + } else { + newCondition = condition + } + + if existingAnd, ok := filterMap["$and"].(bson.A); ok { + filterMap["$and"] = append(existingAnd, newCondition) + } else { + existingConditions := make(bson.A, 0) + for k, v := range filterMap { + if k != "$and" { + existingConditions = append(existingConditions, bson.M{k: v}) + } + } + filterMap["$and"] = append(existingConditions, newCondition) + } + return filterMap +} diff --git a/mongox/util_test.go b/mongox/util_test.go index caab590..c3d2706 100644 --- a/mongox/util_test.go +++ b/mongox/util_test.go @@ -97,3 +97,337 @@ func TestAnd(t *testing.T) { }, }, And(bson.D{{Key: "$and", Value: []bson.M{{"a": "b"}}}}, "", "y")) } + +func TestAndEmptyFilter(t *testing.T) { + filter := bson.M{} + expected := bson.M{ + "$and": bson.A{ + bson.M{"b": bson.M{"c": 2}}, + }, + } + actual := AddCondition(filter, "b", bson.M{"c": 2}) + assert.Equal(t, expected, actual) +} + +func TestAndEmptyKey(t *testing.T) { + filter := bson.M{"a": 1} + expected := bson.M{ + "$and": bson.A{ + bson.M{"a": 1}, + bson.M{"c": 3}, + }, + "a": 1, + } + actual := AddCondition(filter, "", bson.M{"c": 3}) + assert.Equal(t, expected, actual) +} + +func TestAndExistingAddCondition(t *testing.T) { + filter := bson.M{ + "$and": bson.A{ + bson.M{"a": 1}, + bson.M{"b": 2}, + }, + } + expected := bson.M{ + "$and": bson.A{ + bson.M{"a": 1}, + bson.M{"b": 2}, + bson.M{"c": bson.M{"d": 3}}, + }, + } + actual := AddCondition(filter, "c", bson.M{"d": 3}) + assert.Equal(t, expected, actual) +} + +func TestAndWithOrAndEmptyKey(t *testing.T) { + filter := bson.M{ + "$or": bson.A{ + bson.M{"a": 1}, + bson.M{"b": 2}, + }, + } + expected := bson.M{ + "$and": bson.A{ + bson.M{ + "$or": bson.A{ + bson.M{"a": 1}, + bson.M{"b": 2}, + }, + }, + bson.M{"c": 3}, + }, + "$or": bson.A{ + bson.M{"a": 1}, + bson.M{"b": 2}, + }, + } + actual := AddCondition(filter, "", bson.M{"c": 3}) + assert.Equal(t, expected, actual) +} + +func TestAndComplexFilter(t *testing.T) { + filter := bson.M{ + "$and": bson.A{ + bson.M{"x": 10}, + bson.M{"y": 20}, + }, + "$or": bson.A{ + bson.M{"a": 1}, + bson.M{"b": 2}, + }, + } + expected := bson.M{ + "$and": bson.A{ + bson.M{"x": 10}, + bson.M{"y": 20}, + bson.M{"c": bson.M{"d": 3}}, + }, + "$or": bson.A{bson.M{"a": 1}, + bson.M{"b": 2}, + }, + } + actual := AddCondition(filter, "c", bson.M{"d": 3}) + assert.Equal(t, expected, actual) +} + +func TestAndNilFilter(t *testing.T) { + var filter interface{} + expected := bson.M{ + "$and": bson.A{ + bson.M{"b": bson.M{"c": 2}}, + }, + } + actual := AddCondition(filter, "b", bson.M{"c": 2}) + assert.Equal(t, expected, actual) +} + +func TestAndEmptySliceCondition(t *testing.T) { + filter := bson.M{"a": 1} + expected := bson.M{"a": 1} + actual := AddCondition(filter, "b", bson.A{}) + assert.Equal(t, expected, actual) +} + +func TestAndProjectRefetchFilter(t *testing.T) { + team := "team_id_example" + last := "last_project_id" + updatedat := 1654849072592 + filter := bson.M{ + "$and": bson.A{ + bson.M{ + "$or": bson.A{ + bson.M{"deleted": false}, + bson.M{"deleted": bson.M{"$exists": false}}, + }, + }, + bson.M{ + "$or": bson.A{ + bson.M{"coresupport": true}, + bson.M{"coresupport": bson.M{"$exists": false}}, + }, + }, + }, + "team": team, + } + + condition := bson.M{ + "$or": bson.A{ + bson.M{"updatedat": bson.M{"$lt": updatedat}}, + bson.M{"id": bson.M{"$lt": last}, "updatedat": updatedat}, + }, + } + + expected := bson.M{ + "$and": bson.A{ + bson.M{"$or": bson.A{ + bson.M{"deleted": false}, + bson.M{"deleted": bson.M{"$exists": false}}, + }, + }, + bson.M{"$or": bson.A{ + bson.M{"coresupport": true}, + bson.M{"coresupport": bson.M{"$exists": false}}, + }, + }, + bson.M{"$or": bson.A{ + bson.M{"updatedat": bson.M{"$lt": updatedat}}, + bson.M{"id": bson.M{"$lt": last}, "updatedat": updatedat}, + }, + }, + }, + "team": team, + } + + actual := AddCondition(filter, "", condition) + assert.Equal(t, expected, actual) +} + +func TestAddConditionEmptyFilter(t *testing.T) { + filter := bson.M{} + expected := bson.M{ + "$and": bson.A{ + bson.M{"b": bson.M{"c": 2}}, + }, + } + actual := AddCondition(filter, "b", bson.M{"c": 2}) + assert.Equal(t, expected, actual) +} + +func TestAddConditionEmptyKey(t *testing.T) { + filter := bson.M{"a": 1} + expected := bson.M{ + "$and": bson.A{ + bson.M{"a": 1}, + bson.M{"c": 3}, + }, + "a": 1, + } + actual := AddCondition(filter, "", bson.M{"c": 3}) + assert.Equal(t, expected, actual) +} + +func TestAddConditionExistingAddCondition(t *testing.T) { + filter := bson.M{ + "$and": bson.A{ + bson.M{"a": 1}, + bson.M{"b": 2}, + }, + } + expected := bson.M{ + "$and": bson.A{ + bson.M{"a": 1}, + bson.M{"b": 2}, + bson.M{"c": bson.M{"d": 3}}, + }, + } + actual := AddCondition(filter, "c", bson.M{"d": 3}) + assert.Equal(t, expected, actual) +} + +func TestAddConditionWithOrAndEmptyKey(t *testing.T) { + filter := bson.M{ + "$or": bson.A{ + bson.M{"a": 1}, + bson.M{"b": 2}, + }, + } + expected := bson.M{ + "$and": bson.A{ + bson.M{ + "$or": bson.A{ + bson.M{"a": 1}, + bson.M{"b": 2}, + }, + }, + bson.M{"c": 3}, + }, + "$or": bson.A{ + bson.M{"a": 1}, + bson.M{"b": 2}, + }, + } + actual := AddCondition(filter, "", bson.M{"c": 3}) + assert.Equal(t, expected, actual) +} + +func TestAddConditionComplexFilter(t *testing.T) { + filter := bson.M{ + "$and": bson.A{ + bson.M{"x": 10}, + bson.M{"y": 20}, + }, + "$or": bson.A{ + bson.M{"a": 1}, + bson.M{"b": 2}, + }, + } + expected := bson.M{ + "$and": bson.A{ + bson.M{"x": 10}, + bson.M{"y": 20}, + bson.M{"c": bson.M{"d": 3}}, + }, + "$or": bson.A{ + bson.M{"a": 1}, + bson.M{"b": 2}, + }, + } + actual := AddCondition(filter, "c", bson.M{"d": 3}) + assert.Equal(t, expected, actual) +} + +func TestAddConditionNilFilter(t *testing.T) { + var filter interface{} + expected := bson.M{ + "$and": bson.A{ + bson.M{"b": bson.M{"c": 2}}, + }, + } + actual := AddCondition(filter, "b", bson.M{"c": 2}) + assert.Equal(t, expected, actual) +} + +func TestAddConditionEmptySliceCondition(t *testing.T) { + filter := bson.M{"a": 1} + expected := bson.M{"a": 1} + actual := AddCondition(filter, "b", bson.A{}) + assert.Equal(t, expected, actual) +} + +func TestAddConditionProjectRefetchFilter(t *testing.T) { + team := "team_id_example" + last := "last_project_id" + updatedat := 1654849072592 + filter := bson.M{ + "$and": bson.A{ + bson.M{ + "$or": bson.A{ + bson.M{"deleted": false}, + bson.M{"deleted": bson.M{"$exists": false}}, + }, + }, + bson.M{ + "$or": bson.A{ + bson.M{"coresupport": true}, + bson.M{"coresupport": bson.M{"$exists": false}}, + }, + }, + }, + "team": team, + } + + condition := bson.M{ + "$or": bson.A{ + bson.M{"updatedat": bson.M{"$lt": updatedat}}, + bson.M{"id": bson.M{"$lt": last}, "updatedat": updatedat}, + }, + } + + expected := bson.M{ + "$and": bson.A{ + bson.M{ + "$or": bson.A{ + bson.M{"deleted": false}, + bson.M{"deleted": bson.M{"$exists": false}}, + }, + }, + bson.M{ + "$or": bson.A{ + bson.M{"coresupport": true}, + bson.M{"coresupport": bson.M{"$exists": false}}, + }, + }, + bson.M{ + "$or": bson.A{ + bson.M{"updatedat": bson.M{"$lt": updatedat}}, + bson.M{"id": bson.M{"$lt": last}, "updatedat": updatedat}, + }, + }, + }, + "team": team, + } + + actual := AddCondition(filter, "", condition) + assert.Equal(t, expected, actual) +}