Skip to content

Commit

Permalink
api: refactor pagination
Browse files Browse the repository at this point in the history
* indexer: ProcessList now returns the TotalProcessCount

* test: add TestAPIAccountsList and TestAPIElectionsList

* api/accounts: unify hardcoded structs into a new AccountsList

* api/elections: unify hardcoded structs into a new ElectionsList
* api/elections: add `total` field to /elections endpoint

TODO:
* api/accounts: add `total` field to endpoint /accounts
* api/accounts: add `total` field to endpoint /accounts/{organizationID}/elections/
* deduplicate `page` param parsing code
  • Loading branch information
altergui committed Jun 19, 2024
1 parent 7926087 commit 4881fa1
Show file tree
Hide file tree
Showing 12 changed files with 344 additions and 100 deletions.
67 changes: 51 additions & 16 deletions api/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,23 @@ func (a *API) enableAccountHandlers() error {
return err
}
if err := a.Endpoint.RegisterMethod(
"/accounts/page/{page}",
"/accounts",
"GET",
apirest.MethodAccessTypePublic,
a.accountListHandler,
); err != nil {
return err
}

// Legacy endpoints
if err := a.Endpoint.RegisterMethod(
"/accounts/page/{page}",
"GET",
apirest.MethodAccessTypePublic,
a.accountListLegacyHandler,
); err != nil {
return err
}
return nil
}

Expand Down Expand Up @@ -361,32 +370,32 @@ func (a *API) electionListHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContex
var pids [][]byte
switch ctx.URLParam("status") {
case "ready":
pids, err = a.indexer.ProcessList(organizationID, page, MaxPageSize, "", 0, 0, "READY", false)
pids, _, err = a.indexer.ProcessList(organizationID, page, MaxPageSize, "", 0, 0, "READY", false)
if err != nil {
return ErrCantFetchElectionList.WithErr(err)
}
case "paused":
pids, err = a.indexer.ProcessList(organizationID, page, MaxPageSize, "", 0, 0, "PAUSED", false)
pids, _, err = a.indexer.ProcessList(organizationID, page, MaxPageSize, "", 0, 0, "PAUSED", false)
if err != nil {
return ErrCantFetchElectionList.WithErr(err)
}
case "canceled":
pids, err = a.indexer.ProcessList(organizationID, page, MaxPageSize, "", 0, 0, "CANCELED", false)
pids, _, err = a.indexer.ProcessList(organizationID, page, MaxPageSize, "", 0, 0, "CANCELED", false)
if err != nil {
return ErrCantFetchElectionList.WithErr(err)
}
case "ended", "results":
pids, err = a.indexer.ProcessList(organizationID, page, MaxPageSize, "", 0, 0, "RESULTS", false)
pids, _, err = a.indexer.ProcessList(organizationID, page, MaxPageSize, "", 0, 0, "RESULTS", false)
if err != nil {
return ErrCantFetchElectionList.WithErr(err)
}
pids2, err := a.indexer.ProcessList(organizationID, page, MaxPageSize, "", 0, 0, "ENDED", false)
pids2, _, err := a.indexer.ProcessList(organizationID, page, MaxPageSize, "", 0, 0, "ENDED", false)
if err != nil {
return ErrCantFetchElectionList.WithErr(err)
}
pids = append(pids, pids2...)
case "":
pids, err = a.indexer.ProcessList(organizationID, page, MaxPageSize, "", 0, 0, "", false)
pids, _, err = a.indexer.ProcessList(organizationID, page, MaxPageSize, "", 0, 0, "", false)
if err != nil {
return ErrCantFetchElectionList.WithErr(err)
}
Expand Down Expand Up @@ -577,27 +586,53 @@ func (a *API) tokenTransfersCountHandler(_ *apirest.APIdata, ctx *httprouter.HTT
return ctx.Send(data, apirest.HTTPstatusOK)
}

// accountListLegacyHandler
//
// @Summary List of the existing accounts (using url params)
// @Description Returns information (address, balance and nonce) of the existing accounts. (Deprecated, in favor of /accounts?page=)
// @Tags Accounts
// @Accept json
// @Produce json
// @Param page path number true "Page "
// @Success 200 {object} object{accounts=[]indexertypes.Account}
// @Router /accounts/page/{page} [get]
func (a *API) accountListLegacyHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error {
page := 0
if p := ctx.URLParam(ParamPage); p != "" {
var err error
page, err = strconv.Atoi(p)
if err != nil {
return ErrCantParsePageNumber.With(p)
}
}
return a.accountList(ctx, page)
}

// accountListHandler
//
// @Summary List of the existing accounts
// @Description Returns information (address, balance and nonce) of the existing accounts
// @Tags Accounts
// @Accept json
// @Produce json
// @Param page path string true "Paginator page"
// @Success 200 {object} object{accounts=[]indexertypes.Account}
// @Router /accounts/page/{page} [get]
// @Param page query number false "Page"
// @Success 200 {object} AccountsList
// @Router /accounts [get]
func (a *API) accountListHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error {
var err error
page := 0
if ctx.URLParam("page") != "" {
page, err = strconv.Atoi(ctx.URLParam("page"))
if p := ctx.QueryParam(ParamPage); p != "" {
var err error
page, err = strconv.Atoi(p)
if err != nil {
return ErrCantParsePageNumber
return ErrCantParsePageNumber.With(p)
}
}
page = page * MaxPageSize
accounts, err := a.indexer.GetListAccounts(int32(page), MaxPageSize)
return a.accountList(ctx, page)
}

// accountList sends a marshalled AccountsList over ctx.Send
func (a *API) accountList(ctx *httprouter.HTTPContext, page int) error {
accounts, err := a.indexer.AccountsList(page*MaxPageSize, MaxPageSize)
if err != nil {
return ErrCantFetchTokenTransfers.WithErr(err)
}
Expand Down
9 changes: 7 additions & 2 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,13 @@ import (

// @securityDefinitions.basic BasicAuth

// MaxPageSize defines the maximum number of results returned by the paginated endpoints
const MaxPageSize = 10
const (
// MaxPageSize defines the maximum number of results returned by the paginated endpoints
MaxPageSize = 10

// These consts define the keywords for both query (?param=) and url (/url/param/) params
ParamPage = "page"
)

var (
ErrMissingModulesForHandler = fmt.Errorf("missing modules attached for enabling handler")
Expand Down
18 changes: 18 additions & 0 deletions api/api_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ type ElectionSummary struct {
ChainID string `json:"chainId"`
}

// ElectionsList wraps the elections list to consistently return the list inside an object,
// return empty object if the list does not contains any result
type ElectionsList struct {
Elections []ElectionSummary `json:"elections"`
Total uint64 `json:"total"`
}

// ElectionResults is the struct used to wrap the results of an election
type ElectionResults struct {
// ABIEncoded is the abi encoded election results
Expand Down Expand Up @@ -228,6 +235,17 @@ type Account struct {
SIK types.HexBytes `json:"sik"`
}

type AccountSummary struct {
Address types.AccountID `json:"address"`
Nonce uint32 `json:"nonce"`
Balance uint64 `json:"balance"`
}

type AccountsList struct {
Accounts []AccountSummary `json:"accounts"`
Total uint64 `json:"total"`
}

type AccountSet struct {
TxPayload []byte `json:"txPayload,omitempty" swaggerignore:"true"`
Metadata []byte `json:"metadata,omitempty" swaggerignore:"true"`
Expand Down
77 changes: 54 additions & 23 deletions api/elections.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const (

func (a *API) enableElectionHandlers() error {
if err := a.Endpoint.RegisterMethod(
"/elections/page/{page}",
"/elections",
"GET",
apirest.MethodAccessTypePublic,
a.electionFullListHandler,
Expand Down Expand Up @@ -121,46 +121,80 @@ func (a *API) enableElectionHandlers() error {
return err
}

// Legacy endpoints
if err := a.Endpoint.RegisterMethod(
"/elections/page/{page}",
"GET",
apirest.MethodAccessTypePublic,
a.electionFullListLegacyHandler,
); err != nil {
return err
}
return nil
}

// electionFullListLegacyHandler
//
// @Summary List elections (using url params)
// @Description Get a list of elections summaries. (Deprecated, in favor of /elections?page=)
// @Tags Elections
// @Accept json
// @Produce json
// @Param page path number true "Page"
// @Success 200 {object} ElectionsList
// @Router /elections/page/{page} [get]
func (a *API) electionFullListLegacyHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error {
page := 0
if p := ctx.URLParam(ParamPage); p != "" {
var err error
page, err = strconv.Atoi(p)
if err != nil {
return ErrCantParsePageNumber.With(p)
}
}
return a.electionFullList(ctx, page)
}

// electionFullListHandler
//
// @Summary List elections
// @Description Get a list of elections summaries.
// @Tags Elections
// @Accept json
// @Produce json
// @Param page path number true "Page "
// @Success 200 {object} ElectionSummary
// @Router /elections/page/{page} [get]
// @Param page query number false "Page"
// @Success 200 {object} ElectionsList
// @Router /elections [get]
func (a *API) electionFullListHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error {
page := 0
if ctx.URLParam("page") != "" {
if p := ctx.QueryParam(ParamPage); p != "" {
var err error
page, err = strconv.Atoi(ctx.URLParam("page"))
page, err = strconv.Atoi(p)
if err != nil {
return ErrCantParsePageNumber.With(ctx.URLParam("page"))
return ErrCantParsePageNumber.With(p)
}
}
elections, err := a.indexer.ProcessList(nil, page*MaxPageSize, MaxPageSize, "", 0, 0, "", false)
return a.electionFullList(ctx, page)
}

// electionFullList sends a marshalled ElectionsList over ctx.Send
func (a *API) electionFullList(ctx *httprouter.HTTPContext, page int) error {
elections, total, err := a.indexer.ProcessList(nil, page*MaxPageSize, MaxPageSize, "", 0, 0, "", false)
if err != nil {
return ErrCantFetchElectionList.WithErr(err)
}

list := []ElectionSummary{}
list := ElectionsList{
Total: total,
}
for _, eid := range elections {
e, err := a.indexer.ProcessInfo(eid)
if err != nil {
return ErrCantFetchElection.Withf("(%x): %v", eid, err)
}
list = append(list, a.electionSummary(e))
list.Elections = append(list.Elections, a.electionSummary(e))
}
// wrap list in a struct to consistently return list in an object, return empty
// object if the list does not contains any result
data, err := json.Marshal(struct {
Elections []ElectionSummary `json:"elections"`
}{list})
data, err := json.Marshal(list)
if err != nil {
return ErrMarshalingServerJSONFailed.WithErr(err)
}
Expand Down Expand Up @@ -647,7 +681,8 @@ func (a *API) electionFilterPaginatedHandler(msg *apirest.APIdata, ctx *httprout
withResults := false
body.WithResults = &withResults
}
elections, err := a.indexer.ProcessList(
// TODO: use returned total
elections, _, err := a.indexer.ProcessList(
body.OrganizationID,
page,
MaxPageSize,
Expand All @@ -664,20 +699,16 @@ func (a *API) electionFilterPaginatedHandler(msg *apirest.APIdata, ctx *httprout
return ErrElectionNotFound
}

var list []ElectionSummary
var list ElectionsList
// get election summary
for _, eid := range elections {
e, err := a.indexer.ProcessInfo(eid)
if err != nil {
return ErrCantFetchElection.WithErr(err)
}
list = append(list, a.electionSummary(e))
list.Elections = append(list.Elections, a.electionSummary(e))
}
data, err := json.Marshal(struct {
Elections []ElectionSummary `json:"elections"`
}{
Elections: list,
})
data, err := json.Marshal(list)
if err != nil {
return ErrMarshalingServerJSONFailed.WithErr(err)
}
Expand Down
6 changes: 6 additions & 0 deletions httprouter/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ func (h *HTTPContext) URLParam(key string) string {
return chi.URLParam(h.Request, key)
}

// QueryParam is a wrapper around go-chi to get the value of a query string parameter (like "?key=value").
// If key is not present, returns the empty string.
func (h *HTTPContext) QueryParam(key string) string {
return h.Request.URL.Query().Get(key)
}

// Send replies the request with the provided message.
func (h *HTTPContext) Send(msg []byte, httpStatusCode int) error {
defer func() {
Expand Down
Loading

0 comments on commit 4881fa1

Please sign in to comment.