Skip to content

Commit 0eba27e

Browse files
authored
Merge pull request #109 from onelogin/copilot/fix-108
DEVEX-2264: Add pagination support for GetAppUsers method
2 parents 12b4e30 + b70595f commit 0eba27e

File tree

7 files changed

+601
-14
lines changed

7 files changed

+601
-14
lines changed

PAGINATION_EXAMPLES.md

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
# App Users Pagination Examples
2+
3+
This document demonstrates how to use the new pagination features for the `GetAppUsers` method.
4+
5+
## Overview
6+
7+
The OneLogin Go SDK now supports pagination for retrieving app users, allowing you to handle applications with more than 100 users. The implementation maintains backward compatibility:
8+
9+
1. `GetAppUsers(appID int)` - **Original method** (fully backward compatible)
10+
2. `GetAppUsersWithPagination(appID int, queryParams *AppUserQuery)` - **New method** with pagination parameters
11+
3. `GetAppUsersWithPaginationAndContext(ctx context.Context, appID int, queryParams *AppUserQuery)` - **New method** with context support
12+
13+
## Basic Usage
14+
15+
### 1. Backward Compatible (Original Method - No Changes Required)
16+
17+
```go
18+
// This works exactly as before - returns up to 100 users
19+
// NO CODE CHANGES NEEDED for existing applications
20+
users, err := sdk.GetAppUsers(123)
21+
if err != nil {
22+
log.Fatalf("Failed to get app users: %v", err)
23+
}
24+
25+
// users is still interface{} - same as before
26+
fmt.Printf("Retrieved users: %+v\n", users)
27+
```
28+
29+
### 2. With Pagination Parameters (New Method)
30+
31+
```go
32+
// Create query with pagination parameters
33+
query := &models.AppUserQuery{
34+
Limit: "50", // Get 50 users per page
35+
Page: "2", // Get page 2
36+
}
37+
38+
result, err := sdk.GetAppUsersWithPagination(123, query)
39+
if err != nil {
40+
log.Fatalf("Failed to get app users: %v", err)
41+
}
42+
43+
// Access the users data
44+
users := result.Data
45+
46+
// Access pagination metadata
47+
fmt.Printf("Current page: %d\n", result.Pagination.CurrentPage)
48+
fmt.Printf("Total pages: %d\n", result.Pagination.TotalPages)
49+
fmt.Printf("Total count: %d\n", result.Pagination.TotalCount)
50+
fmt.Printf("Next page cursor: %s\n", result.Pagination.AfterCursor)
51+
```
52+
53+
### 3. With Context Support (New Method)
54+
55+
```go
56+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
57+
defer cancel()
58+
59+
query := &models.AppUserQuery{
60+
Limit: "50",
61+
Page: "1",
62+
}
63+
64+
result, err := sdk.GetAppUsersWithPaginationAndContext(ctx, 123, query)
65+
if err != nil {
66+
log.Fatalf("Failed to get app users: %v", err)
67+
}
68+
69+
// Same result structure as above
70+
users := result.Data
71+
pagination := result.Pagination
72+
```
73+
74+
## Advanced Usage
75+
76+
### Cursor-Based Pagination
77+
78+
For better performance with large datasets, use cursor-based pagination:
79+
80+
```go
81+
query := &models.AppUserQuery{
82+
Limit: "50",
83+
Cursor: "your_cursor_here",
84+
}
85+
86+
result, err := sdk.GetAppUsersWithPagination(123, query)
87+
```
88+
89+
### Iterating Through All Pages
90+
91+
```go
92+
var allUsers []interface{}
93+
cursor := ""
94+
ctx := context.Background()
95+
96+
for {
97+
query := &models.AppUserQuery{
98+
Limit: "50",
99+
Cursor: cursor,
100+
}
101+
102+
result, err := sdk.GetAppUsersWithPaginationAndContext(ctx, 123, query)
103+
if err != nil {
104+
log.Fatalf("Failed to get page: %v", err)
105+
}
106+
107+
// Add users from this page
108+
if users, ok := result.Data.([]interface{}); ok {
109+
allUsers = append(allUsers, users...)
110+
}
111+
112+
// Check if there's a next page
113+
if result.Pagination.AfterCursor == "" {
114+
break // No more pages
115+
}
116+
117+
cursor = result.Pagination.AfterCursor
118+
}
119+
120+
fmt.Printf("Retrieved %d total users\n", len(allUsers))
121+
```
122+
123+
## AppUserQuery Parameters
124+
125+
The `AppUserQuery` struct supports the following parameters:
126+
127+
- `Limit` (string): Number of results per page (e.g., "50", "100")
128+
- `Page` (string): Page number to retrieve (e.g., "1", "2", "3")
129+
- `Cursor` (string): Cursor for cursor-based pagination
130+
131+
## PagedResponse Structure
132+
133+
The new pagination methods return a `*PagedResponse` with:
134+
135+
```go
136+
type PagedResponse struct {
137+
Data interface{} `json:"data"` // The actual user data
138+
Pagination PaginationInfo `json:"pagination"` // Pagination metadata
139+
}
140+
141+
type PaginationInfo struct {
142+
Cursor string `json:"cursor,omitempty"`
143+
AfterCursor string `json:"after_cursor,omitempty"`
144+
BeforeCursor string `json:"before_cursor,omitempty"`
145+
TotalPages int `json:"total_pages,omitempty"`
146+
CurrentPage int `json:"current_page,omitempty"`
147+
TotalCount int `json:"total_count,omitempty"`
148+
}
149+
```
150+
151+
## Error Handling
152+
153+
The methods include improved error handling with context:
154+
155+
```go
156+
query := &models.AppUserQuery{
157+
Limit: "invalid_value",
158+
}
159+
160+
result, err := sdk.GetAppUsersWithPagination(123, query)
161+
if err != nil {
162+
// Handle validation error with context
163+
fmt.Printf("Parameter validation failed: %v\n", err)
164+
}
165+
```
166+
167+
## Migration Guide
168+
169+
### **No Breaking Changes - Existing Code Works Unchanged**
170+
171+
**Existing code (no changes needed):**
172+
```go
173+
users, err := sdk.GetAppUsers(appID)
174+
// This continues to work exactly as before
175+
```
176+
177+
**To add pagination (new functionality):**
178+
```go
179+
// Option 1: Add pagination support
180+
query := &models.AppUserQuery{Limit: "50", Page: "1"}
181+
result, err := sdk.GetAppUsersWithPagination(appID, query)
182+
users := result.Data
183+
184+
// Option 2: Add context support
185+
ctx := context.Background()
186+
result, err := sdk.GetAppUsersWithPaginationAndContext(ctx, appID, query)
187+
users := result.Data
188+
```
189+
190+
## Backward Compatibility Guarantee
191+
192+
-**Original `GetAppUsers(appID int)` method unchanged**
193+
-**Same return type: `(interface{}, error)`**
194+
-**Same behavior: returns up to 100 users**
195+
-**Existing code requires no modifications**
196+
-**New functionality available through new methods**
197+
198+
This approach ensures existing applications continue to work while providing new pagination capabilities through additional methods.

pkg/onelogin/apps.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package onelogin
22

33
import (
4+
"context"
5+
"fmt"
6+
"strconv"
7+
48
mod "github.com/onelogin/onelogin-go-sdk/v4/pkg/onelogin/models"
59
utl "github.com/onelogin/onelogin-go-sdk/v4/pkg/onelogin/utilities"
610
)
@@ -96,6 +100,72 @@ func (sdk *OneloginSDK) GetAppUsers(appID int) (interface{}, error) {
96100
return utl.CheckHTTPResponse(resp)
97101
}
98102

103+
// GetAppUsersWithPagination retrieves users for an app with optional pagination parameters
104+
func (sdk *OneloginSDK) GetAppUsersWithPagination(appID int, queryParams *mod.AppUserQuery) (*mod.PagedResponse, error) {
105+
return sdk.GetAppUsersWithPaginationAndContext(context.Background(), appID, queryParams)
106+
}
107+
108+
// GetAppUsersWithPaginationAndContext retrieves users for an app with optional query parameters using the provided context
109+
// Returns pagination metadata along with the user data
110+
func (sdk *OneloginSDK) GetAppUsersWithPaginationAndContext(ctx context.Context, appID int, queryParams *mod.AppUserQuery) (*mod.PagedResponse, error) {
111+
p, err := utl.BuildAPIPath(AppPath, appID, "users")
112+
if err != nil {
113+
return nil, fmt.Errorf("failed to build API path: %w", err)
114+
}
115+
116+
// Validate query parameters if provided
117+
if queryParams != nil {
118+
validators := queryParams.GetKeyValidators()
119+
if !utl.ValidateQueryParams(queryParams, validators) {
120+
return nil, fmt.Errorf("invalid query parameters: validation failed")
121+
}
122+
}
123+
124+
// Make the API request with context
125+
resp, err := sdk.Client.GetWithContext(ctx, &p, queryParams)
126+
if err != nil {
127+
return nil, fmt.Errorf("failed to get app users: %w", err)
128+
}
129+
130+
// Extract data from response
131+
data, err := utl.CheckHTTPResponse(resp)
132+
if err != nil {
133+
return nil, fmt.Errorf("failed to process response: %w", err)
134+
}
135+
136+
// Extract pagination information from headers
137+
pagination := mod.PaginationInfo{
138+
Cursor: resp.Header.Get("Cursor"),
139+
AfterCursor: resp.Header.Get("After-Cursor"),
140+
BeforeCursor: resp.Header.Get("Before-Cursor"),
141+
}
142+
143+
// Try to parse total pages and current page
144+
if totalPages := resp.Header.Get("Total-Pages"); totalPages != "" {
145+
if i, err := strconv.Atoi(totalPages); err == nil {
146+
pagination.TotalPages = i
147+
}
148+
}
149+
150+
if currentPage := resp.Header.Get("Current-Page"); currentPage != "" {
151+
if i, err := strconv.Atoi(currentPage); err == nil {
152+
pagination.CurrentPage = i
153+
}
154+
}
155+
156+
if totalCount := resp.Header.Get("Total-Count"); totalCount != "" {
157+
if i, err := strconv.Atoi(totalCount); err == nil {
158+
pagination.TotalCount = i
159+
}
160+
}
161+
162+
// Combine data and pagination info
163+
return &mod.PagedResponse{
164+
Data: data,
165+
Pagination: pagination,
166+
}, nil
167+
}
168+
99169
// App Rules APIs
100170
// list all rules for an app
101171
func (sdk *OneloginSDK) GetAppRules(appID int, queryParams mod.Queryable) (interface{}, error) {

pkg/onelogin/models/app.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,18 @@ func (q *AppQuery) GetKeyValidators() map[string]func(interface{}) bool {
137137
"auth_method": validateInt,
138138
}
139139
}
140+
141+
// AppUserQuery represents query parameters for listing app users with pagination support
142+
type AppUserQuery struct {
143+
Limit string `json:"limit,omitempty"`
144+
Page string `json:"page,omitempty"`
145+
Cursor string `json:"cursor,omitempty"`
146+
}
147+
148+
func (q *AppUserQuery) GetKeyValidators() map[string]func(interface{}) bool {
149+
return map[string]func(interface{}) bool{
150+
"limit": validateString,
151+
"page": validateString,
152+
"cursor": validateString,
153+
}
154+
}

pkg/onelogin/models/pagination.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package models
2+
3+
// PaginationInfo represents pagination metadata from API responses
4+
type PaginationInfo struct {
5+
Cursor string `json:"cursor,omitempty"`
6+
AfterCursor string `json:"after_cursor,omitempty"`
7+
BeforeCursor string `json:"before_cursor,omitempty"`
8+
TotalPages int `json:"total_pages,omitempty"`
9+
CurrentPage int `json:"current_page,omitempty"`
10+
TotalCount int `json:"total_count,omitempty"`
11+
}
12+
13+
// PagedResponse represents a paginated response with both data and pagination information
14+
type PagedResponse struct {
15+
Data interface{} `json:"data"`
16+
Pagination PaginationInfo `json:"pagination"`
17+
}

pkg/onelogin/models/user.go

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,7 @@ const (
2121
StatusSecurityQuestionsRequired // The user has not yet set their security questions.
2222
)
2323

24-
// PaginationInfo represents pagination metadata from API responses
25-
type PaginationInfo struct {
26-
Cursor string `json:"cursor,omitempty"`
27-
AfterCursor string `json:"after_cursor,omitempty"`
28-
BeforeCursor string `json:"before_cursor,omitempty"`
29-
TotalPages int `json:"total_pages,omitempty"`
30-
CurrentPage int `json:"current_page,omitempty"`
31-
TotalCount int `json:"total_count,omitempty"`
32-
}
3324

34-
// PagedResponse represents a paginated response with both data and pagination information
35-
type PagedResponse struct {
36-
Data interface{} `json:"data"`
37-
Pagination PaginationInfo `json:"pagination"`
38-
}
3925

4026
// UserQuery represents available query parameters
4127
type UserQuery struct {

pkg/services/apps/v2.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,35 @@ func (svc *V2Service) GetUsers(id int32) ([]mod.UserApp, error) {
9696
return users, err
9797
}
9898

99+
// GetUsersWithQuery retrieves the list of users for a given app by id with optional query parameters,
100+
// it returns an array of users for the app.
101+
func (svc *V2Service) GetUsersWithQuery(id int32, query mod.Queryable) ([]mod.UserApp, error) {
102+
resp, err := svc.Repository.Read(mod.OLHTTPRequest{
103+
URL: fmt.Sprintf("%s/%d/users", svc.Endpoint, id),
104+
Headers: map[string]string{"Content-Type": "application/json"},
105+
AuthMethod: "bearer",
106+
Payload: query,
107+
})
108+
if err != nil {
109+
return nil, err
110+
}
111+
112+
var users []mod.UserApp
113+
for _, bytes := range resp {
114+
var unmarshalled []mod.UserApp
115+
if err := json.Unmarshal(bytes, &unmarshalled); err != nil {
116+
return nil, err
117+
}
118+
if len(users) == 0 {
119+
users = unmarshalled
120+
} else {
121+
users = append(users, unmarshalled...)
122+
}
123+
}
124+
125+
return users, nil
126+
}
127+
99128
// Create creates a new app, and if successful, it returns a pointer to the app.
100129
func (svc *V2Service) Create(app *mod.App) error {
101130
resp, err := svc.Repository.Create(mod.OLHTTPRequest{

0 commit comments

Comments
 (0)