Skip to content

Commit

Permalink
Add Cursor based pagination support for Keys and Translations APIs (#52)
Browse files Browse the repository at this point in the history
* feat: Add Cursor based pagination support for Keys and Translations APIs

* Added additional test cases to pagination tests
  • Loading branch information
RonnyLV authored May 20, 2024
1 parent 502c2e3 commit 234f28c
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 13 deletions.
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,44 @@ t.SetPageOptions(lokalise.PageOptions{
resp, err := t.List()
```

### Cursor pagination

The [List Keys](https://developers.lokalise.com/reference/list-all-keys) and [List Translations](https://developers.lokalise.com/reference/list-all-translations) endpoints support cursor pagination, which is recommended for its faster performance compared to traditional "offset" pagination. By default, "offset" pagination is used, so you must explicitly set `pagination` to `"cursor"` to use cursor pagination.

```go
// This approach is also applicable for `client.Translations()`
keys := Api.Keys()
keys.SetPageOptions(lokalise.PageOptions{
Pagination: "cursor",
Cursor: "eyIxIjo1MjcyNjU2MTd9"
})

resp, err := keys.List()
```

After retrieving data from the Lokalise API, you can check for the availability of the next cursor and proceed accordingly:

```go
cursor := ""

for {
keys := client.Keys()
keys.SetListOptions(KeyListOptions{
Pagination: "cursor",
Cursor: cursor,
})
resp, _ := keys.List(projectId)

// Do something with the response

if !resp.Paged.HasNextCursor() {
// no more keys
break
}
cursor = resp.Paged.Cursor
}
```

## Queued Processes
Some resource actions, such as Files.upload, are subject to intensive processing before request fulfills.
These processes got optimised by becoming asynchronous.
Expand Down
4 changes: 2 additions & 2 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ func New(apiToken string, options ...ClientOption) (*Api, error) {

// predefined list options if any
prjOpts := ProjectListOptions{Page: c.pageOptions.Page, Limit: c.pageOptions.Limit}
keyOpts := KeyListOptions{Page: c.pageOptions.Page, Limit: c.pageOptions.Limit}
keyOpts := KeyListOptions{Pagination: c.pageOptions.Pagination, Page: c.pageOptions.Page, Limit: c.pageOptions.Limit, Cursor: c.pageOptions.Cursor}
taskOpts := TaskListOptions{Page: c.pageOptions.Page, Limit: c.pageOptions.Limit}
scOpts := ScreenshotListOptions{Page: c.pageOptions.Page, Limit: c.pageOptions.Limit}
trOpts := TranslationListOptions{Page: c.pageOptions.Page, Limit: c.pageOptions.Limit}
trOpts := TranslationListOptions{Pagination: c.pageOptions.Pagination, Page: c.pageOptions.Page, Limit: c.pageOptions.Limit, Cursor: c.pageOptions.Cursor}
fOpts := FileListOptions{Page: c.pageOptions.Page, Limit: c.pageOptions.Limit}

c.Projects = func() *ProjectService { return &ProjectService{BaseService: bs, opts: prjOpts} }
Expand Down
28 changes: 21 additions & 7 deletions pagination.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,22 @@ import (
"github.com/google/go-querystring/query"
)

const (
PaginationOffset = "offset"
PaginationCursor = "cursor"
)

type PageCounter interface {
NumberOfPages() int64
CurrentPage() int64
}

type Paged struct {
TotalCount int64 `json:"-"`
PageCount int64 `json:"-"`
Limit int64 `json:"-"`
Page int64 `json:"-"`
TotalCount int64 `json:"-"`
PageCount int64 `json:"-"`
Limit int64 `json:"-"`
Page int64 `json:"-"`
Cursor string `json:"-"`
}

func (p Paged) NumberOfPages() int64 {
Expand All @@ -28,13 +34,19 @@ func (p Paged) CurrentPage() int64 {
return p.Page
}

func (p Paged) NextCursor() string { return p.Cursor }

func (p Paged) HasNextCursor() bool { return p.Cursor != "" }

type OptionsApplier interface {
Apply(req *resty.Request)
}

type PageOptions struct {
Limit uint `url:"limit,omitempty"`
Page uint `url:"page,omitempty"`
Pagination string `url:"pagination,omitempty"`
Limit uint `url:"limit,omitempty"`
Page uint `url:"page,omitempty"`
Cursor string `url:"cursor,omitempty"`
}

func (options PageOptions) Apply(req *resty.Request) {
Expand All @@ -47,14 +59,16 @@ const (
headerPageCount = "X-Pagination-Page-Count"
headerLimit = "X-Pagination-Limit"
headerPage = "X-Pagination-Page"
headerNextCursor = "X-Pagination-Next-Cursor"
)

func applyPaged(res *resty.Response, paged *Paged) {
headers := res.Header()
paged.Limit = headerInt64(headers, headerLimit)
paged.TotalCount = headerInt64(headers, headerTotalCount)
paged.PageCount = headerInt64(headers, headerPageCount)
paged.Limit = headerInt64(headers, headerLimit)
paged.Page = headerInt64(headers, headerPage)
paged.Cursor = headers.Get(headerNextCursor)
}

func headerInt64(headers http.Header, headerKey string) int64 {
Expand Down
6 changes: 6 additions & 0 deletions service.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ func (s *BaseService) SetPageOptions(opts PageOptions) {
if opts.Page != 0 {
s.Page = opts.Page
}
if opts.Pagination != "" {
s.Pagination = opts.Pagination
}
if opts.Cursor != "" {
s.Cursor = opts.Cursor
}
}

// ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
Expand Down
6 changes: 4 additions & 2 deletions svc_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,10 @@ func (k NewKey) MarshalJSON() ([]byte, error) {
// List options
type KeyListOptions struct {
// page options
Page uint `url:"page,omitempty"`
Limit uint `url:"limit,omitempty"`
Pagination string `url:"pagination,omitempty"`
Page uint `url:"page,omitempty"`
Limit uint `url:"limit,omitempty"`
Cursor string `url:"cursor,omitempty"`

// Possible values are 1 and 0.
DisableReferences uint8 `url:"disable_references,omitempty"`
Expand Down
114 changes: 114 additions & 0 deletions svc_key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -839,3 +839,117 @@ func TestKeyService_Update_Empty_Tags(t *testing.T) {
t.Errorf("Keys.Update returned \n %+v\n want\n %+v", r.Key, want)
}
}

func TestKeyService_Retrieve_Paged_offset(t *testing.T) {
client, mux, _, teardown := setup()
defer teardown()

mux.HandleFunc(
fmt.Sprintf("/projects/%s/keys", testProjectID),
func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Pagination-Total-Count", "1000")
w.Header().Set("X-Pagination-Page-Count", "10")
w.Header().Set("X-Pagination-Limit", "100")
w.Header().Set("X-Pagination-Page", "1")
testMethod(t, r, "GET")
testHeader(t, r, apiTokenHeader, testApiToken)

_, _ = fmt.Fprint(w, `{}`)
})

r, err := client.Keys().List(testProjectID)
if err != nil {
t.Errorf("Keys.List.Paged returned error: %v", err)
}

want := Paged{
TotalCount: 1000,
PageCount: 10,
Limit: 100,
Page: 1,
}

if !reflect.DeepEqual(r.Paged, want) {
t.Errorf("Keys.List.Paged returned %+v, want %+v", r.Paged, want)
}

if r.Paged.CurrentPage() != 1 {
t.Errorf("Keys.List.Paged.CurrentPage() returned %+v, want %+v", r.Paged.CurrentPage(), 1)
}
}

func TestKeyService_Retrieve_Paged_cursor(t *testing.T) {
client, mux, _, teardown := setup()
defer teardown()

mux.HandleFunc(
fmt.Sprintf("/projects/%s/keys", testProjectID),
func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Pagination-Next-Cursor", "eyIxIjo5NzM1NjI0NX0=")
w.Header().Set("X-Pagination-Limit", "100")
testMethod(t, r, "GET")
testHeader(t, r, apiTokenHeader, testApiToken)

_, _ = fmt.Fprint(w, `{}`)
})

r, err := client.Keys().List(testProjectID)
if err != nil {
t.Errorf("Keys.List.Paged returned error: %v", err)
}

want := Paged{
TotalCount: -1,
PageCount: -1,
Limit: 100,
Page: -1,
Cursor: "eyIxIjo5NzM1NjI0NX0=",
}

if !reflect.DeepEqual(r.Paged, want) {
t.Errorf("Keys.List.Paged returned %+v, want %+v", r.Paged, want)
}

if !r.Paged.HasNextCursor() {
t.Errorf("Keys.List.Paged.HasNextCursor() returned false, want true")
}
}

func TestKeyService_Retrieve_Paged_cursor_empty(t *testing.T) {
client, mux, _, teardown := setup()
defer teardown()

mux.HandleFunc(
fmt.Sprintf("/projects/%s/keys", testProjectID),
func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Pagination-Limit", "100")
testMethod(t, r, "GET")
testHeader(t, r, apiTokenHeader, testApiToken)

_, _ = fmt.Fprint(w, `{}`)
})

r, err := client.Keys().List(testProjectID)
if err != nil {
t.Errorf("Keys.List.Paged returned error: %v", err)
}

want := Paged{
TotalCount: -1,
PageCount: -1,
Limit: 100,
Page: -1,
Cursor: "",
}

if !reflect.DeepEqual(r.Paged, want) {
t.Errorf("Keys.List.Paged returned %+v, want %+v", r.Paged, want)
}

if r.Paged.HasNextCursor() {
t.Errorf("Keys.List.Paged.HasNextCursor() returned true, want false")
}
}
1 change: 1 addition & 0 deletions svc_paymentcard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ func TestPaymentCardService_List(t *testing.T) {
PageCount: -1,
Page: -1,
Limit: -1,
Cursor: "",
},
WithUserID: WithUserID{
UserID: 420,
Expand Down
6 changes: 4 additions & 2 deletions svc_translation.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,10 @@ func (c *TranslationService) Update(projectID string, translationID int64, opts

type TranslationListOptions struct {
// page options
Page uint `url:"page,omitempty"`
Limit uint `url:"limit,omitempty"`
Pagination string `url:"pagination,omitempty"`
Page uint `url:"page,omitempty"`
Limit uint `url:"limit,omitempty"`
Cursor string `url:"cursor,omitempty"`

// Possible values are 1 and 0.
DisableReferences uint8 `url:"disable_references,omitempty"`
Expand Down
Loading

0 comments on commit 234f28c

Please sign in to comment.