Peter Sanchez: 1 Fixing "wonky" previous (back) pagination issues. 9 files changed, 354 insertions(+), 792 deletions(-)
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.code.netlandish.com/~netlandish/links-dev/patches/122/mbox | git am -3Learn more about email & git
Created generic functions to query correctly and also paginate correctly given the pagination needs and requirements for this project. Fixes: https://todo.code.netlandish.com/~netlandish/links/102 Changelog-added: Generic functions to fetch data and paginate correctly so all views that need pagination can easily plug it in. --- This should resolve the weirdness when paginating via "prev" links throughout the site. Also there was a lot of inconsistencies in the pagination. Sometimes the ordering was wrong on display even though the data was returned correctly (needed reversing, etc.). I've made it all uniform and use the same generic functions to keep everything in sync. Some more tweaks are probably needed in the future to customize sort ordering, etc. admin/routes.go | 15 + api/graph/pagination.go | 81 +++ api/graph/pagination_test.go | 77 +++ api/graph/schema.resolvers.go | 942 ++++++---------------------------- billing/routes.go | 5 + core/routes.go | 8 + list/routes.go | 8 + mattermost/routes.go | 6 - short/routes.go | 4 + 9 files changed, 354 insertions(+), 792 deletions(-) create mode 100644 api/graph/pagination.go create mode 100644 api/graph/pagination_test.go diff --git a/admin/routes.go b/admin/routes.go index 1e730b1..d6f30a8 100644 --- a/admin/routes.go +++ b/admin/routes.go @@ -7,6 +7,7 @@ import ( "links/internal/localizer" "links/models" "net/http" + "slices" "strconv" "strings" "time" @@ -446,6 +447,10 @@ func (s *Service) OrgDetail(c echo.Context) error { return err } + if c.QueryParam("prev") != "" { + slices.Reverse(historyResult.Payments.Result) + } + type GraphQLStats struct { Stats struct { Links int `json:"links"` @@ -1005,6 +1010,10 @@ func (s *Service) BillingList(c echo.Context) error { return err } + if c.QueryParam("prev") != "" { + slices.Reverse(historyResult.Payments.Result) + } + gmap := gobwebs.Map{ "pd": pd, "navFlag": "admin", @@ -1164,6 +1173,9 @@ func (s *Service) DomainList(c echo.Context) error { if err != nil { return err } + if c.QueryParam("prev") != "" { + slices.Reverse(result.Organizations.Result) + } if result.Organizations.PageInfo.HasPrevPage { gmap["prevURL"] = links.GetPaginationParams(c, "prev", result.Organizations.PageInfo.Cursor, "next") } @@ -1630,6 +1642,9 @@ func (s *Service) UserList(c echo.Context) error { if err != nil { return err } + if c.QueryParam("prev") != "" { + slices.Reverse(result.GetUsers.Result) + } if result.GetUsers.PageInfo.HasPrevPage { gmap["prevURL"] = links.GetPaginationParams(c, "prev", result.GetUsers.PageInfo.Cursor, "next") } diff --git a/api/graph/pagination.go b/api/graph/pagination.go new file mode 100644 index 0000000..82043a4 --- /dev/null +++ b/api/graph/pagination.go @@ -0,0 +1,81 @@ +package graph + +import ( + "context" + "links/api/graph/model" + + sq "github.com/Masterminds/squirrel" + "netlandish.com/x/gobwebs/database" +) + +func PaginateResults[T any](items []T, limit int, before, after *model.Cursor, + getID func(T) int) ([]T, *model.PageInfo) { + overFetched := len(items) > limit + if overFetched { + items = items[:limit] + } + + var pageInfo model.PageInfo + if len(items) > 0 { + pageInfo.Cursor.Limit = limit + pageInfo.Cursor.Before = getID(items[0]) + pageInfo.Cursor.After = getID(items[len(items)-1]) + } + + if before != nil { + pageInfo.HasPrevPage = overFetched + pageInfo.HasNextPage = true + pageInfo.Cursor.Before, pageInfo.Cursor.After = + pageInfo.Cursor.After, pageInfo.Cursor.Before + } else if after != nil { + pageInfo.HasPrevPage = true + pageInfo.HasNextPage = overFetched + } else { + pageInfo.HasNextPage = overFetched + } + + return items, &pageInfo +} + +func QueryModel[T any]( + ctx context.Context, + opts *database.FilterOptions, + idField string, + limit *int, + before, after *model.Cursor, + getModels func(context.Context, *database.FilterOptions) ([]T, error), + getID func(T) int, +) ([]T, *model.PageInfo, error) { + numElements := model.PaginationDefault + if after != nil { + opts.Filter = sq.And{ + opts.Filter, + sq.Lt{idField: after.After}, + } + numElements = after.Limit + } else if before != nil { + opts.Filter = sq.And{ + opts.Filter, + sq.Gt{idField: before.Before}, + } + opts.OrderBy = idField + " ASC" + numElements = before.Limit + } + + // If limit specifically set, it overrides any cursor limit + if limit != nil && *limit > 0 { + numElements = *limit + } + if numElements > model.PaginationMax { + numElements = model.PaginationMax + } + + opts.Limit = numElements + 1 + items, err := getModels(ctx, opts) + if err != nil { + return nil, nil, err + } + + items, pageInfo := PaginateResults(items, numElements, before, after, getID) + return items, pageInfo, nil +} diff --git a/api/graph/pagination_test.go b/api/graph/pagination_test.go new file mode 100644 index 0000000..82e7500 --- /dev/null +++ b/api/graph/pagination_test.go @@ -0,0 +1,77 @@ +package graph + +import ( + "links/api/graph/model" + "slices" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPaginationForwardAndBackward(t *testing.T) { + var allItems []int + for i := 126; i >= 101; i-- { + allItems = append(allItems, i) + } + + getPage := func(after, before *model.Cursor) ([]int, *model.PageInfo) { + var filtered []int + for _, id := range allItems { + // ORDER BY id DESC + if after != nil && id >= after.After { + continue + } + // ORDER BY id ASC + if before != nil && id <= before.Before { + continue + } + filtered = append(filtered, id) + } + if before != nil { + slices.Reverse(filtered) + } + return PaginateResults(filtered, 5, before, after, func(v int) int { + return v + }) + } + + t.Run("Forward Pagination", func(t *testing.T) { + var after *model.Cursor + seen := make(map[int]bool) + for { + page, info := getPage(after, nil) + // t.Logf("Backward Page: %v", page) + for _, id := range page { + if seen[id] { + t.Fatalf("duplicate forward id %d", id) + } + seen[id] = true + } + if !info.HasNextPage { + break + } + after = &model.Cursor{After: info.Cursor.After, Limit: 5} + } + assert.Equal(t, 26, len(seen)) + }) + + t.Run("Backward Pagination", func(t *testing.T) { + before := &model.Cursor{Before: 0} + seen := make(map[int]bool) + for { + page, info := getPage(nil, before) + // t.Logf("Backward Page: %v", page) + for _, id := range page { + if seen[id] { + t.Fatalf("duplicate backward id %d", id) + } + seen[id] = true + } + if !info.HasPrevPage { + break + } + before = &model.Cursor{Before: info.Cursor.Before, Limit: 5} + } + assert.Equal(t, 26, len(seen)) + }) +} diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go index a00cc26..78e264e 100644 --- a/api/graph/schema.resolvers.go +++ b/api/graph/schema.resolvers.go @@ -27,6 +27,7 @@ import ( "net/url" "os" "regexp" + "slices" "strconv" "strings" "time" @@ -53,7 +54,7 @@ import ( ) // Metadata is the resolver for the metadata field. -func (r *auditLogResolver) Metadata(ctx context.Context, obj *auditlog.AuditLog) (map[string]interface{}, error) { +func (r *auditLogResolver) Metadata(ctx context.Context, obj *auditlog.AuditLog) (map[string]any, error) { return obj.Metadata, nil } @@ -1832,11 +1833,8 @@ func (r *mutationResolver) UpdateProfile(ctx context.Context, input *model.Profi WithCode(valid.ErrValidationCode) var validTZ bool - for _, tz := range timezone.GetTZList() { - if input.Timezone == tz { - validTZ = true - break - } + if slices.Contains(timezone.GetTZList(), input.Timezone) { + validTZ = true } validator.Expect(validTZ, "%s", lt.Translate("Timezone is invalid")). @@ -1848,11 +1846,8 @@ func (r *mutationResolver) UpdateProfile(ctx context.Context, input *model.Profi WithCode(valid.ErrValidationCode) var validLang bool - for _, lang := range localizer.GetLangList() { - if input.DefaultLang == lang { - validLang = true - break - } + if slices.Contains(localizer.GetLangList(), input.DefaultLang) { + validLang = true } validator.Expect(validLang, "%s", lt.Translate("Lang is invalid")). @@ -4886,10 +4881,6 @@ func (r *queryResolver) GetPaymentHistory(ctx context.Context, input *model.GetP lang := links.GetLangFromRequest(server.EchoForContext(ctx).Request(), user) lt := localizer.GetLocalizer(lang) - opts := &database.FilterOptions{ - Filter: sq.And{}, Limit: 1, - } - validator := valid.New(ctx) // If you are not super user we have to validate // org permission to see the payment history @@ -4899,6 +4890,17 @@ func (r *queryResolver) GetPaymentHistory(ctx context.Context, input *model.GetP return nil, nil } + if input.After != nil && input.Before != nil { + validator.Error("%s", lt.Translate("You can not send both after and before cursors")). + WithCode(valid.ErrValidationGlobalCode) + return nil, nil + } + + opts := &database.FilterOptions{ + Filter: sq.And{}, + Limit: 1, + } + var org *models.Organization if input.OrgSlug != nil { opts.Filter = sq.And{ @@ -4926,7 +4928,7 @@ func (r *queryResolver) GetPaymentHistory(ctx context.Context, input *model.GetP opts = &database.FilterOptions{ Filter: sq.And{}, - OrderBy: "i.created_on DESC", + OrderBy: "i.id DESC", } if org != nil { @@ -4983,91 +4985,22 @@ func (r *queryResolver) GetPaymentHistory(ctx context.Context, input *model.GetP } } - // Pagination - if input.After != nil && input.Before != nil { - validator.Error("%s", lt.Translate("You can not send both after and before cursors")). - WithCode(valid.ErrValidationGlobalCode) - return nil, nil - } - numElements := model.PaginationDefault - var hasPrevPage bool - var hasNextPage bool - if input.After != nil { - opts.Filter = sq.And{ - opts.Filter, - sq.LtOrEq{"i.id": input.After.After}, - } - numElements = input.After.Limit - } else if input.Before != nil { - opts.Filter = sq.And{ - opts.Filter, - sq.GtOrEq{"i.id": input.Before.Before}, - } - opts.OrderBy = "i.id ASC" - numElements = input.Before.Limit - } - // If limit specifically set, it overrides any cursor limit - if input.Limit != nil && *input.Limit > 0 { - numElements = *input.Limit - } - if numElements > model.PaginationMax { - numElements = model.PaginationMax - } - - opts.Limit = numElements + 2 - invoices, err := models.GetInvoices(ctx, opts) + invoices, pageInfo, err := QueryModel( + ctx, + opts, + "i.id", + input.Limit, + input.Before, + input.After, + models.GetInvoices, + func(x *models.Invoice) int { + return x.ID + }, + ) if err != nil { return nil, err } - c := model.Cursor{Limit: numElements} - count := len(invoices) - if count > 0 { - // Checking for previous page - if input.Before != nil { - if int(invoices[0].ID) == input.Before.Before { - hasPrevPage = true - invoices = invoices[1:] - count-- - } - } else if input.After != nil { - if int(invoices[0].ID) == input.After.After { - hasPrevPage = true - invoices = invoices[1:] - count-- - } - } - if count == opts.Limit { - // No previous page - invoices = invoices[:count-1] - count-- - } - - if count > numElements { - hasNextPage = true - invoices = invoices[:count-1] - count-- - } - if count > 0 { - if input.Before != nil { - tmp := hasPrevPage - hasPrevPage = hasNextPage - hasNextPage = tmp - c.After = int(invoices[0].ID) - c.Before = int(invoices[count-1].ID) - } else { - c.After = int(invoices[count-1].ID) - c.Before = int(invoices[0].ID) - } - } else { - hasPrevPage = false - } - } - pageInfo := &model.PageInfo{ - Cursor: c, - HasNextPage: hasNextPage, - HasPrevPage: hasPrevPage, - } // Payment is a wrap for invoice aimed // to expose only the required fields for payment history var payments []*model.Payment @@ -5278,33 +5211,18 @@ func (r *queryResolver) GetOrgLinks(ctx context.Context, input *model.GetLinkInp } - numElements := model.PaginationDefault - var hasPrevPage bool - var hasNextPage bool - if input.After != nil { - linkOpts.Filter = sq.And{ - linkOpts.Filter, - sq.Expr("ol.id <= ?", input.After.After), - } - numElements = input.After.Limit - } else if input.Before != nil { - linkOpts.Filter = sq.And{ - linkOpts.Filter, - sq.Expr("ol.id >= ?", input.Before.Before), - } - numElements = input.Before.Limit - } - - // If limit specifically set, it overrides any cursor limit - if input.Limit != nil && *input.Limit > 0 { - numElements = *input.Limit - } - if numElements > model.PaginationMax { - numElements = model.PaginationMax - } - - linkOpts.Limit = numElements + 2 - orgLinks, err := models.GetOrgLinks(ctx, linkOpts) + orgLinks, pageInfo, err := QueryModel( + ctx, + linkOpts, + "ol.id", + input.Limit, + input.Before, + input.After, + models.GetOrgLinks, + func(link *models.OrgLink) int { + return link.ID + }, + ) if err != nil { return nil, err } @@ -5318,55 +5236,11 @@ func (r *queryResolver) GetOrgLinks(ctx context.Context, input *model.GetLinkInp } } - c := model.Cursor{Limit: numElements} - count := len(orgLinks) - if count > 0 { - // Checking for previous page - if input.Before != nil { - if orgLinks[0].ID == input.Before.Before { - hasPrevPage = true - orgLinks = orgLinks[1:] - count-- - } - } else if input.After != nil { - if orgLinks[0].ID == input.After.After { - hasPrevPage = true - orgLinks = orgLinks[1:] - count-- - } - } - if count == linkOpts.Limit { - // No previous page - orgLinks = orgLinks[:count-1] - count-- - } - - if count > numElements { - hasNextPage = true - orgLinks = orgLinks[:count-1] - count-- - } - if count > 0 { - if input.Before != nil { - tmp := hasPrevPage - hasPrevPage = hasNextPage - hasNextPage = tmp - c.After = orgLinks[0].ID - c.Before = orgLinks[count-1].ID - } else { - c.After = orgLinks[count-1].ID - c.Before = orgLinks[0].ID - } - } else { - hasPrevPage = false - } - } - pageInfo := &model.PageInfo{ - Cursor: c, - HasNextPage: hasNextPage, - HasPrevPage: hasPrevPage, - } - return &model.OrgLinkCursor{Result: orgLinks, PageInfo: pageInfo, RestrictedCount: &resCount}, nil + return &model.OrgLinkCursor{ + Result: orgLinks, + PageInfo: pageInfo, + RestrictedCount: &resCount, + }, nil } // GetOrgMembers is the resolver for the getOrgMembers field. @@ -5579,7 +5453,7 @@ func (r *queryResolver) GetLinkShorts(ctx context.Context, input *model.GetLinkS Filter: sq.And{ sq.Expr("l.org_id = ?", org.ID), }, - OrderBy: "id DESC", + OrderBy: "l.id DESC", } if input.After != nil && input.Before != nil { validator.Error("%s", lt.Translate("You can not send both after and before cursors")). @@ -5599,86 +5473,22 @@ func (r *queryResolver) GetLinkShorts(ctx context.Context, input *model.GetLinkS } } - numElements := model.PaginationDefault - var hasPrevPage bool - var hasNextPage bool - if input.After != nil { - linkOpts.Filter = sq.And{ - linkOpts.Filter, - sq.Expr("l.id <= ?", input.After.After), - } - numElements = input.After.Limit - } else if input.Before != nil { - linkOpts.Filter = sq.And{ - linkOpts.Filter, - sq.Expr("l.id >= ?", input.Before.Before), - } - linkOpts.OrderBy = "l.id ASC" - numElements = input.Before.Limit - } - - // If limit specifically set, it overrides any cursor limit - if input.Limit != nil && *input.Limit > 0 { - numElements = *input.Limit - } - if numElements > model.PaginationMax { - numElements = model.PaginationMax - } - - linkOpts.Limit = numElements + 2 - linkShorts, err := models.GetLinkShorts(ctx, linkOpts) + linkShorts, pageInfo, err := QueryModel( + ctx, + linkOpts, + "l.id", + input.Limit, + input.Before, + input.After, + models.GetLinkShorts, + func(x *models.LinkShort) int { + return x.ID + }, + ) if err != nil { return nil, err } - c := model.Cursor{Limit: numElements} - count := len(linkShorts) - if count > 0 { - // Checking for previous page - if input.Before != nil { - if linkShorts[0].ID == input.Before.Before { - hasPrevPage = true - linkShorts = linkShorts[1:] - count-- - } - } else if input.After != nil { - if linkShorts[0].ID == input.After.After { - hasPrevPage = true - linkShorts = linkShorts[1:] - count-- - } - } - if count == linkOpts.Limit { - // No previous page - linkShorts = linkShorts[:count-1] - count-- - } - - if count > numElements { - hasNextPage = true - linkShorts = linkShorts[:count-1] - count-- - } - if count > 0 { - if input.Before != nil { - tmp := hasPrevPage - hasPrevPage = hasNextPage - hasNextPage = tmp - c.After = linkShorts[0].ID - c.Before = linkShorts[count-1].ID - } else { - c.After = linkShorts[count-1].ID - c.Before = linkShorts[0].ID - } - } else { - hasPrevPage = false - } - } - pageInfo := &model.PageInfo{ - Cursor: c, - HasNextPage: hasNextPage, - HasPrevPage: hasPrevPage, - } return &model.LinkShortCursor{Result: linkShorts, PageInfo: pageInfo}, nil } @@ -5786,85 +5596,22 @@ func (r *queryResolver) GetListings(ctx context.Context, input *model.GetListing } } - numElements := 10 - var hasPrevPage bool - var hasNextPage bool - if input.After != nil { - listingOpts.Filter = sq.And{ - listingOpts.Filter, - sq.Expr("l.id <= ?", input.After.After), - } - numElements = input.After.Limit - } else if input.Before != nil { - listingOpts.Filter = sq.And{ - listingOpts.Filter, - sq.Expr("l.id >= ?", input.Before.Before), - } - listingOpts.OrderBy = "l.id ASC" - numElements = input.Before.Limit - } - - // If limit specifically set, it overrides any cursor limit - if input.Limit != nil && *input.Limit > 0 { - numElements = *input.Limit - } - if numElements > model.PaginationMax { - numElements = model.PaginationMax - } - - listingOpts.Limit = numElements + 2 - listings, err := models.GetListings(ctx, listingOpts) + listings, pageInfo, err := QueryModel( + ctx, + listingOpts, + "l.id", + input.Limit, + input.Before, + input.After, + models.GetListings, + func(x *models.Listing) int { + return x.ID + }, + ) if err != nil { return nil, err } - c := model.Cursor{Limit: numElements} - count := len(listings) - if count > 0 { - // Checking for previous page - if input.Before != nil { - if listings[0].ID == input.Before.Before { - hasPrevPage = true - listings = listings[1:] - count-- - } - } else if input.After != nil { - if listings[0].ID == input.After.After { - hasPrevPage = true - listings = listings[1:] - count-- - } - } - if count == listingOpts.Limit { - // No previous page - listings = listings[:count-1] - count-- - } - if count > numElements { - hasNextPage = true - listings = listings[:count-1] - count-- - } - if count > 0 { - if input.Before != nil { - tmp := hasPrevPage - hasPrevPage = hasNextPage - hasNextPage = tmp - c.After = listings[0].ID - c.Before = listings[count-1].ID - } else { - c.After = listings[count-1].ID - c.Before = listings[0].ID - } - } else { - hasPrevPage = false - } - } - pageInfo := &model.PageInfo{ - Cursor: c, - HasNextPage: hasNextPage, - HasPrevPage: hasPrevPage, - } return &model.ListingCursor{Result: listings, PageInfo: pageInfo}, nil } @@ -5933,85 +5680,22 @@ func (r *queryResolver) GetListing(ctx context.Context, input *model.GetListingD OrderBy: "ll.link_order ASC", } - numElements := 10 - var hasPrevPage bool - var hasNextPage bool - if input.After != nil { - linkOpts.Filter = sq.And{ - linkOpts.Filter, - sq.LtOrEq{"ll.id": input.After.After}, - } - numElements = input.After.Limit - } else if input.Before != nil { - linkOpts.Filter = sq.And{ - linkOpts.Filter, - sq.GtOrEq{"ll.id": input.Before.Before}, - } - linkOpts.OrderBy = "ll.id ASC" - numElements = input.Before.Limit - } - - // If limit specifically set, it overrides any cursor limit - if input.Limit != nil && *input.Limit > 0 { - numElements = *input.Limit - } - if numElements > model.PaginationMax { - numElements = model.PaginationMax - } - - linkOpts.Limit = numElements + 2 - listingLinks, err := models.GetListingLinks(ctx, linkOpts) + listingLinks, pageInfo, err := QueryModel( + ctx, + linkOpts, + "ll.link_order", + input.Limit, + input.Before, + input.After, + models.GetListingLinks, + func(x *models.ListingLink) int { + return x.ID + }, + ) if err != nil { return nil, err } - c := model.Cursor{Limit: numElements} - count := len(listingLinks) - if count > 0 { - // Checking for previous page - if input.Before != nil { - if listingLinks[0].ID == input.Before.Before { - hasPrevPage = true - listingLinks = listingLinks[1:] - count-- - } - } else if input.After != nil { - if listingLinks[0].ID == input.After.After { - hasPrevPage = true - listingLinks = listingLinks[1:] - count-- - } - } - if count == linkOpts.Limit { - // No previous page - listingLinks = listingLinks[:count-1] - count-- - } - if count > numElements { - hasNextPage = true - listingLinks = listingLinks[:count-1] - count-- - } - if count > 0 { - if input.Before != nil { - tmp := hasPrevPage - hasPrevPage = hasNextPage - hasNextPage = tmp - c.After = listingLinks[0].ID - c.Before = listingLinks[count-1].ID - } else { - c.After = listingLinks[count-1].ID - c.Before = listingLinks[0].ID - } - } else { - hasPrevPage = false - } - } - pageInfo := &model.PageInfo{ - Cursor: c, - HasNextPage: hasNextPage, - HasPrevPage: hasPrevPage, - } return &model.ListingLinkCursor{Result: listing, Links: listingLinks, PageInfo: pageInfo}, nil } @@ -6530,6 +6214,12 @@ func (r *queryResolver) GetFeed(ctx context.Context, input *model.GetFeedInput) ctx = timezone.Context(ctx, links.GetUserTZ(user)) validator := valid.New(ctx) + if input.After != nil && input.Before != nil { + validator.Error("%s", lt.Translate("You can not send both after and before cursors")). + WithCode(valid.ErrValidationGlobalCode) + return nil, nil + } + linkOpts := &database.FilterOptions{ Filter: sq.Or{ // An user follows another user @@ -6569,92 +6259,22 @@ func (r *queryResolver) GetFeed(ctx context.Context, input *model.GetFeedInput) } - if input.After != nil && input.Before != nil { - validator.Error("%s", lt.Translate("You can not send both after and before cursors")). - WithCode(valid.ErrValidationGlobalCode) - return nil, nil - } - - numElements := model.PaginationDefault - var hasPrevPage bool - var hasNextPage bool - if input.After != nil { - linkOpts.Filter = sq.And{ - linkOpts.Filter, - sq.Expr("ol.id <= ?", input.After.After), - } - numElements = input.After.Limit - } else if input.Before != nil { - linkOpts.Filter = sq.And{ - linkOpts.Filter, - sq.Expr("ol.id >= ?", input.Before.Before), - } - linkOpts.OrderBy = "ol.id ASC" - numElements = input.Before.Limit - } - - // If limit specifically set, it overrides any cursor limit - if input.Limit != nil && *input.Limit > 0 { - numElements = *input.Limit - } - if numElements > model.PaginationMax { - numElements = model.PaginationMax - } - - linkOpts.Limit = numElements + 2 - orgLinks, err := models.GetOrgLinks(ctx, linkOpts) + orgLinks, pageInfo, err := QueryModel( + ctx, + linkOpts, + "ol.id", + input.Limit, + input.Before, + input.After, + models.GetOrgLinks, + func(x *models.OrgLink) int { + return x.ID + }, + ) if err != nil { return nil, err } - c := model.Cursor{Limit: numElements} - count := len(orgLinks) - if count > 0 { - // Checking for previous page - if input.Before != nil { - if orgLinks[0].ID == input.Before.Before { - hasPrevPage = true - orgLinks = orgLinks[1:] - count-- - } - } else if input.After != nil { - if orgLinks[0].ID == input.After.After { - hasPrevPage = true - orgLinks = orgLinks[1:] - count-- - } - } - if count == linkOpts.Limit { - // No previous page - orgLinks = orgLinks[:count-1] - count-- - } - - if count > numElements { - hasNextPage = true - orgLinks = orgLinks[:count-1] - count-- - } - if count > 0 { - if input.Before != nil { - tmp := hasPrevPage - hasPrevPage = hasNextPage - hasNextPage = tmp - c.After = orgLinks[0].ID - c.Before = orgLinks[count-1].ID - } else { - c.After = orgLinks[count-1].ID - c.Before = orgLinks[0].ID - } - } else { - hasPrevPage = false - } - } - pageInfo := &model.PageInfo{ - Cursor: c, - HasNextPage: hasNextPage, - HasPrevPage: hasPrevPage, - } return &model.OrgLinkCursor{Result: orgLinks, PageInfo: pageInfo}, nil } @@ -6814,86 +6434,22 @@ func (r *queryResolver) GetAuditLogs(ctx context.Context, input *model.AuditLogI } } - numElements := model.PaginationDefault - var hasPrevPage, hasNextPage bool - if input.After != nil { - opts.Filter = sq.And{ - opts.Filter, - sq.Expr("al.id <= ?", input.After.After), - } - numElements = input.After.Limit - } else if input.Before != nil { - opts.Filter = sq.And{ - opts.Filter, - sq.Expr("al.id >= ?", input.Before.Before), - } - opts.OrderBy = "al.id ASC" - numElements = input.Before.Limit - } - - // If limit specifically set, it overrides any cursor limit - if input.Limit != nil && *input.Limit > 0 { - numElements = *input.Limit - } - if numElements > model.PaginationMax { - numElements = model.PaginationMax - } - opts.Limit = numElements + 2 - - alogs, err := auditlog.GetAuditLogs(ctx, opts) + alogs, pageInfo, err := QueryModel( + ctx, + opts, + "al.id", + input.Limit, + input.Before, + input.After, + auditlog.GetAuditLogs, + func(x *auditlog.AuditLog) int { + return x.ID + }, + ) if err != nil { return nil, err } - c := model.Cursor{Limit: numElements} - count := len(alogs) - if count > 0 { - // Checking for previous page - if input.Before != nil { - if alogs[0].ID == input.Before.Before { - hasPrevPage = true - alogs = alogs[1:] - count-- - } - } else if input.After != nil { - if alogs[0].ID == input.After.After { - hasPrevPage = true - alogs = alogs[1:] - count-- - } - } - if count == opts.Limit { - // No previous page - alogs = alogs[:count-1] - count-- - } - - if count > numElements { - hasNextPage = true - alogs = alogs[:count-1] - count-- - } - if count > 0 { - if input.Before != nil { - tmp := hasPrevPage - hasPrevPage = hasNextPage - hasNextPage = tmp - c.After = alogs[0].ID - c.Before = alogs[count-1].ID - } else { - c.After = alogs[count-1].ID - c.Before = alogs[0].ID - } - } else { - hasPrevPage = false - } - } - - pageInfo := &model.PageInfo{ - Cursor: c, - HasNextPage: hasNextPage, - HasPrevPage: hasPrevPage, - } return &model.AuditLogCursor{Result: alogs, PageInfo: pageInfo}, nil } @@ -6941,84 +6497,22 @@ func (r *queryResolver) GetUsers(ctx context.Context, input *model.GetUserInput) } - numElements := model.PaginationDefault - var hasPrevPage bool - var hasNextPage bool - if input.After != nil { - opts.Filter = sq.And{ - opts.Filter, - sq.LtOrEq{"u.id": input.After.After}, - } - numElements = input.After.Limit - } else if input.Before != nil { - opts.Filter = sq.And{ - opts.Filter, - sq.GtOrEq{"u.id": input.Before.Before}, - } - opts.OrderBy = "u.id ASC" - numElements = input.Before.Limit - } - // If limit specifically set, it overrides any cursor limit - if input.Limit != nil && *input.Limit > 0 { - numElements = *input.Limit - } - if numElements > model.PaginationMax { - numElements = model.PaginationMax - } - - opts.Limit = numElements + 2 - users, err := models.GetUsers(ctx, opts) + users, pageInfo, err := QueryModel( + ctx, + opts, + "u.id", + input.Limit, + input.Before, + input.After, + models.GetUsers, + func(x *models.User) int { + return int(x.ID) + }, + ) if err != nil { return nil, err } - c := model.Cursor{Limit: numElements} - count := len(users) - if count > 0 { - // Checking for previous page - if input.Before != nil { - if int(users[0].ID) == input.Before.Before { - hasPrevPage = true - users = users[1:] - count-- - } - } else if input.After != nil { - if int(users[0].ID) == input.After.After { - hasPrevPage = true - users = users[1:] - count-- - } - } - if count == opts.Limit { - // No previous page - users = users[:count-1] - count-- - } - if count > numElements { - hasNextPage = true - users = users[:count-1] - count-- - } - if count > 0 { - if input.Before != nil { - tmp := hasPrevPage - hasPrevPage = hasNextPage - hasNextPage = tmp - c.After = int(users[0].ID) - c.Before = int(users[count-1].ID) - } else { - c.After = int(users[count-1].ID) - c.Before = int(users[0].ID) - } - } else { - hasPrevPage = false - } - } - pageInfo := &model.PageInfo{ - Cursor: c, - HasNextPage: hasNextPage, - HasPrevPage: hasPrevPage, - } return &model.UserCursor{Result: users, PageInfo: pageInfo}, nil } @@ -7094,84 +6588,22 @@ func (r *queryResolver) GetAdminOrganizations(ctx context.Context, input *model. } } - numElements := model.PaginationDefault - var hasPrevPage bool - var hasNextPage bool - if input.After != nil { - opts.Filter = sq.And{ - opts.Filter, - sq.LtOrEq{"o.id": input.After.After}, - } - numElements = input.After.Limit - } else if input.Before != nil { - opts.Filter = sq.And{ - opts.Filter, - sq.GtOrEq{"o.id": input.Before.Before}, - } - opts.OrderBy = "o.id ASC" - numElements = input.Before.Limit - } - // If limit specifically set, it overrides any cursor limit - if input.Limit != nil && *input.Limit > 0 { - numElements = *input.Limit - } - if numElements > model.PaginationMax { - numElements = model.PaginationMax - } - - opts.Limit = numElements + 2 - orgs, err := models.GetOrganizations(ctx, opts) + orgs, pageInfo, err := QueryModel( + ctx, + opts, + "o.id", + input.Limit, + input.Before, + input.After, + models.GetOrganizations, + func(x *models.Organization) int { + return x.ID + }, + ) if err != nil { return nil, err } - c := model.Cursor{Limit: numElements} - count := len(orgs) - if count > 0 { - // Checking for previous page - if input.Before != nil { - if int(orgs[0].ID) == input.Before.Before { - hasPrevPage = true - orgs = orgs[1:] - count-- - } - } else if input.After != nil { - if int(orgs[0].ID) == input.After.After { - hasPrevPage = true - orgs = orgs[1:] - count-- - } - } - if count == opts.Limit { - // No previous page - orgs = orgs[:count-1] - count-- - } - if count > numElements { - hasNextPage = true - orgs = orgs[:count-1] - count-- - } - if count > 0 { - if input.Before != nil { - tmp := hasPrevPage - hasPrevPage = hasNextPage - hasNextPage = tmp - c.After = int(orgs[0].ID) - c.Before = int(orgs[count-1].ID) - } else { - c.After = int(orgs[count-1].ID) - c.Before = int(orgs[0].ID) - } - } else { - hasPrevPage = false - } - } - pageInfo := &model.PageInfo{ - Cursor: c, - HasNextPage: hasNextPage, - HasPrevPage: hasPrevPage, - } return &model.OrganizationCursor{Result: orgs, PageInfo: pageInfo}, nil } @@ -7353,6 +6785,12 @@ func (r *queryResolver) GetAdminDomains(ctx context.Context, input *model.GetAdm return nil, nil } + if input.After != nil && input.Before != nil { + validator.Error("%s", lt.Translate("You can not send both after and before cursors")). + WithCode(valid.ErrValidationGlobalCode) + return nil, nil + } + opts := &database.FilterOptions{ Filter: sq.And{}, OrderBy: "d.id DESC", @@ -7371,12 +6809,6 @@ func (r *queryResolver) GetAdminDomains(ctx context.Context, input *model.GetAdm } } - if input.After != nil && input.Before != nil { - validator.Error("%s", lt.Translate("You can not send both after and before cursors")). - WithCode(valid.ErrValidationGlobalCode) - return nil, nil - } - if input.FilterLevel != nil { opts.Filter = sq.And{ opts.Filter, @@ -7406,84 +6838,22 @@ func (r *queryResolver) GetAdminDomains(ctx context.Context, input *model.GetAdm } - numElements := model.PaginationDefault - var hasPrevPage bool - var hasNextPage bool - if input.After != nil { - opts.Filter = sq.And{ - opts.Filter, - sq.LtOrEq{"d.id": input.After.After}, - } - numElements = input.After.Limit - } else if input.Before != nil { - opts.Filter = sq.And{ - opts.Filter, - sq.GtOrEq{"d.id": input.Before.Before}, - } - opts.OrderBy = "d.id ASC" - numElements = input.Before.Limit - } - // If limit specifically set, it overrides any cursor limit - if input.Limit != nil && *input.Limit > 0 { - numElements = *input.Limit - } - if numElements > model.PaginationMax { - numElements = model.PaginationMax - } - - opts.Limit = numElements + 2 - domains, err := models.GetDomains(ctx, opts) + domains, pageInfo, err := QueryModel( + ctx, + opts, + "d.id", + input.Limit, + input.Before, + input.After, + models.GetDomains, + func(x *models.Domain) int { + return x.ID + }, + ) if err != nil { return nil, err } - c := model.Cursor{Limit: numElements} - count := len(domains) - if count > 0 { - // Checking for previous page - if input.Before != nil { - if int(domains[0].ID) == input.Before.Before { - hasPrevPage = true - domains = domains[1:] - count-- - } - } else if input.After != nil { - if int(domains[0].ID) == input.After.After { - hasPrevPage = true - domains = domains[1:] - count-- - } - } - if count == opts.Limit { - // No previous page - domains = domains[:count-1] - count-- - } - if count > numElements { - hasNextPage = true - domains = domains[:count-1] - count-- - } - if count > 0 { - if input.Before != nil { - tmp := hasPrevPage - hasPrevPage = hasNextPage - hasNextPage = tmp - c.After = int(domains[0].ID) - c.Before = int(domains[count-1].ID) - } else { - c.After = int(domains[count-1].ID) - c.Before = int(domains[0].ID) - } - } else { - hasPrevPage = false - } - } - pageInfo := &model.PageInfo{ - Cursor: c, - HasNextPage: hasNextPage, - HasPrevPage: hasPrevPage, - } return &model.DomainCursor{Result: domains, PageInfo: pageInfo}, nil } diff --git a/billing/routes.go b/billing/routes.go index 5fd4e83..7849f1e 100644 --- a/billing/routes.go @@ -7,6 +7,7 @@ import ( "links/internal/localizer" "links/models" "net/http" + "slices" "strconv" "git.sr.ht/~emersion/gqlclient" @@ -273,6 +274,10 @@ func (s *Service) SubscriptionHistory(c echo.Context) error { return err } + if c.QueryParam("prev") != "" { + slices.Reverse(result.Payments.Result) + } + gmap := gobwebs.Map{ "pd": pd, "invoices": result.Payments.Result, diff --git a/core/routes.go b/core/routes.go index d6bdf55..94a8a23 100644 --- a/core/routes.go +++ b/core/routes.go @@ -13,6 +13,7 @@ import ( "links/slack" "net/http" "net/url" + "slices" "strconv" "strings" "time" @@ -1835,6 +1836,9 @@ func (s *Service) UserFeed(c echo.Context) error { pd.Data["clear"] = lt.Translate("Clear") pd.Data["followings"] = lt.Translate("Followings") orgLinks := result.OrgLinks.Result + if c.QueryParam("prev") != "" { + slices.Reverse(orgLinks) + } if links.IsRSS(c.Path()) { domain := fmt.Sprintf("%s://%s", gctx.Server.Config.Scheme, gctx.Server.Config.Domain) @@ -2213,6 +2217,10 @@ func (s *Service) OrgLinksList(c echo.Context) error { return links.ServerRSSFeed(c.Response(), rss) } + if c.QueryParam("prev") != "" { + slices.Reverse(orgLinks) + } + seoData := links.GetSEOData(c) url := links.GetLinksDomainURL(c) if isOrgLink { diff --git a/list/routes.go b/list/routes.go index 1271fbf..2f82358 100644 --- a/list/routes.go +++ b/list/routes.go @@ -11,6 +11,7 @@ import ( "links/models" "net/http" "net/url" + "slices" "strconv" "strings" "time" @@ -569,6 +570,10 @@ func (s *Service) ListingLinksManage(c echo.Context) error { return err } + if c.QueryParam("prev") != "" { + slices.Reverse(result.Listing.Links) + } + lt := localizer.GetSessionLocalizer(c) pd := localizer.NewPageData(lt.Translate("%s Links", result.Listing.Result.Title)) pd.Data["edit"] = lt.Translate("Edit") @@ -1106,6 +1111,9 @@ func (s *Service) ListingList(c echo.Context) error { } return err } + if c.QueryParam("prev") != "" { + slices.Reverse(result.Listings.Result) + } gmap := gobwebs.Map{ "pd": pd, "org": org, diff --git a/mattermost/routes.go b/mattermost/routes.go index e9094d0..19ebef1 100644 --- a/mattermost/routes.go +++ b/mattermost/routes.go @@ -135,12 +135,6 @@ func (s *Service) Connect(c echo.Context) error { return s.Render(c, http.StatusOK, "connect_mattermost.html", gmap) } - opts = &database.FilterOptions{ - Filter: sq.And{ - sq.Eq{"o.owner_id": user.ID}, - sq.Eq{"o.is_active": true}, - }, - } gmap := gobwebs.Map{ "pd": pd, "org": org, diff --git a/short/routes.go b/short/routes.go index 96d1106..ebbac75 100644 --- a/short/routes.go +++ b/short/routes.go @@ -11,6 +11,7 @@ import ( "links/models" "net/http" "net/url" + "slices" "strconv" "strings" "time" @@ -167,6 +168,9 @@ func (s *Service) LinkShortList(c echo.Context) error { pd.Data["clear"] = lt.Translate("Clear") linkShorts := result.LinkShorts.Result + if c.QueryParam("prev") != "" { + slices.Reverse(linkShorts) + } gmap := gobwebs.Map{ "pd": pd, "links": linkShorts, -- 2.47.2
Applied. To git@git.code.netlandish.com:~netlandish/links cbe9bed..9d69e58 master -> master