Skip to content

Commit

Permalink
feat: Add Cursor based pagination support for Keys and Translations APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
RonnyLV committed May 15, 2024
1 parent 502c2e3 commit 46df30c
Show file tree
Hide file tree
Showing 9 changed files with 214 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
69 changes: 69 additions & 0 deletions svc_key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -839,3 +839,72 @@ 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)
}
}

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)
}
}
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
69 changes: 69 additions & 0 deletions svc_translation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,3 +363,72 @@ func JsonCompact(text string) string {
}
return compactedBuffer.String()
}

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

mux.HandleFunc(
fmt.Sprintf("/projects/%s/translations", 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.Translations().List(testProjectID)
if err != nil {
t.Errorf("Translations.List returned error: %v", err)
}

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

if !reflect.DeepEqual(r.Paged, want) {
t.Errorf(assertionTemplate, "Translations.List.Paged", r.Paged, want)
}
}

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

mux.HandleFunc(
fmt.Sprintf("/projects/%s/translations", 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.Translations().List(testProjectID)
if err != nil {
t.Errorf("Translations.List returned error: %v", err)
}

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

if !reflect.DeepEqual(r.Paged, want) {
t.Errorf(assertionTemplate, "Translations.List.Paged", r.Paged, want)
}
}

0 comments on commit 46df30c

Please sign in to comment.