Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Cursor based pagination support for Keys and Translations APIs #52

Merged
merged 2 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 You can add a link to client.Translations() here

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:"-"`
Dosexe marked this conversation as resolved.
Show resolved Hide resolved
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 != "" }
Dosexe marked this conversation as resolved.
Show resolved Hide resolved

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)
}
}
Loading