Skip to content

Conversation

@Fodoj
Copy link
Contributor

@Fodoj Fodoj commented Nov 3, 2025

Currently it works only with first 20 users. This PR fixes it, including this issue #36

Summary by CodeRabbit

  • New Features

    • Added full pagination for project user queries and paginated lookups by ID or email to support large projects.
  • Bug Fixes

    • Improved reliability of user lookups across pages, with better error handling, clearer per-page logging, and more robust import/read/create flows that preserve configured roles and handle permission edge cases.

@Fodoj Fodoj self-assigned this Nov 3, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 3, 2025

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description is minimal and lacks the required template structure. It does not include issue reference, type of change selection, testing methodology, or checklist items. Use the repository's description template. Add issue reference (Fixes #36), select change type, document testing approach, and complete the provided checklist.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: implementing auto-pagination for project users, which is the core objective of this PR.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-users

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a9ea6c3 and 46cb1bf.

📒 Files selected for processing (1)
  • internal/provider/data_source_openai_project_user.go (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
internal/provider/data_source_openai_project_user.go (2)
internal/client/client.go (1)
  • ProjectUser (160-166)
internal/provider/provider.go (1)
  • GetOpenAIClientWithAdminKey (83-110)
🔇 Additional comments (3)
internal/provider/data_source_openai_project_user.go (3)

6-6: LGTM!

The strings import is correctly added to support case-insensitive email matching.


120-120: LGTM! Case-insensitive email matching is appropriate.

Using strings.EqualFold correctly handles email comparisons per RFC standards.


139-218: LGTM! Read function correctly integrates the pagination helpers.

The refactored logic properly handles the tuple return values from the new helpers, maintains appropriate error handling, and correctly updates the resource state.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
internal/client/client.go (1)

1859-1897: Guard against empty cursors to prevent infinite pagination loops.

If the API ever returns has_more = true but omits last_id, after will stay unchanged and we’ll keep re-fetching the same page forever. Please add a fallback (e.g. break when last_id == "" or derive the cursor from the last element) so the loop always progresses or aborts safely.

internal/provider/resource_openai_project_user.go (1)

155-179: Reuse fixed cursor handling in retry path.

The duplicate pagination in the retry branch needs the same empty-LastID guard; otherwise a missing cursor will loop forever right after an already exists error. Please apply the same fix there.

🧹 Nitpick comments (1)
internal/provider/data_source_openai_project_user.go (1)

118-124: Use strings.EqualFold instead of fmt.Sprintf for email match.

fmt.Sprintf("%s", ...) is flagged by staticcheck S1025. Replace the redundant formatting with a case-insensitive comparison, e.g. strings.EqualFold(user.Email, email), to satisfy CI and make the intent clear.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 739f75e and a9ea6c3.

📒 Files selected for processing (4)
  • internal/client/client.go (1 hunks)
  • internal/provider/data_source_openai_project_user.go (3 hunks)
  • internal/provider/data_source_openai_project_users.go (1 hunks)
  • internal/provider/resource_openai_project_user.go (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
internal/client/client.go (1)
internal/provider/provider.go (1)
  • OpenAIClient (27-31)
internal/provider/resource_openai_project_user.go (2)
internal/client/client.go (1)
  • ProjectUser (160-166)
internal/provider/provider.go (1)
  • GetOpenAIClientWithAdminKey (83-110)
internal/provider/data_source_openai_project_user.go (2)
internal/client/client.go (1)
  • ProjectUser (160-166)
internal/provider/provider.go (1)
  • GetOpenAIClientWithAdminKey (83-110)
🪛 GitHub Actions: CI
internal/provider/data_source_openai_project_user.go

[error] 120-120: S1025: the argument is already a string, there's no need to use fmt.Sprintf (staticcheck)

🪛 GitHub Check: golangci
internal/provider/data_source_openai_project_user.go

[failure] 120-120:
S1025: the argument is already a string, there's no need to use fmt.Sprintf (staticcheck)

Comment on lines 115 to 134
}

// Look for the user with matching email in this page (case insensitive)
for _, user := range userList.Data {
if user.Email == email || (len(user.Email) == len(email) &&
fmt.Sprintf("%s", user.Email) == fmt.Sprintf("%s", email)) {
tflog.Debug(ctx, fmt.Sprintf("Found user with email %s in project %s on page %d", email, projectID, pageCount))
return &user, true, nil
}
}

// Check if there are more pages
hasMore = userList.HasMore
if hasMore && userList.LastID != "" {
after = userList.LastID
}
}

tflog.Debug(ctx, fmt.Sprintf("User with email %s not found in project %s after checking %d pages", email, projectID, pageCount))
return nil, false, nil
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Break out when the API omits LastID.

To keep pagination resilient, bail when HasMore is true but LastID comes back empty (or compute the cursor from the last element). Without that, the loop may keep fetching the same page.

🧰 Tools
🪛 GitHub Actions: CI

[error] 120-120: S1025: the argument is already a string, there's no need to use fmt.Sprintf (staticcheck)

🪛 GitHub Check: golangci

[failure] 120-120:
S1025: the argument is already a string, there's no need to use fmt.Sprintf (staticcheck)

Comment on lines +101 to +140
// Automatic pagination - fetch all users with default batch size
const batchSize = 100
tflog.Debug(ctx, fmt.Sprintf("Fetching all users in project %s with batch size: %d", projectID, batchSize))

var allUsers []map[string]interface{}
var after string
hasMore := true
pageCount := 0

// Paginate through all results
for hasMore {
pageCount++
tflog.Debug(ctx, fmt.Sprintf("Fetching page %d for project %s (after: %s)", pageCount, projectID, after))

usersList, err := c.ListProjectUsers(projectID, after, batchSize)
if err != nil {
tflog.Error(ctx, fmt.Sprintf("Error listing users (page %d): %v", pageCount, err))
return diag.Errorf("Error listing users in project (page %d): %s", pageCount, err)
}

// Transform the users into a format appropriate for the schema
users := make([]map[string]interface{}, 0, len(usersList.Data))
for _, user := range usersList.Data {
userData := map[string]interface{}{
"id": user.ID,
"email": user.Email,
"role": user.Role,
"added_at": user.AddedAt,
// Transform the users from this page into a format appropriate for the schema
for _, user := range usersList.Data {
userData := map[string]interface{}{
"id": user.ID,
"email": user.Email,
"role": user.Role,
"added_at": user.AddedAt,
}
allUsers = append(allUsers, userData)
}

// Check if there are more pages
hasMore = usersList.HasMore
if hasMore && usersList.LastID != "" {
after = usersList.LastID
}
users = append(users, userData)
}

tflog.Debug(ctx, fmt.Sprintf("Fetched %d total users for project %s across %d pages", len(allUsers), projectID, pageCount))

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Handle missing last_id to avoid re-fetching the same page.

If HasMore comes back true but LastID is empty, after remains unchanged and the loop repeats the same request indefinitely. Please bail out (or derive the cursor from the last record) when usersList.LastID == "" so pagination can’t spin forever.

Comment on lines +68 to +101
const batchSize = 100
tflog.Debug(ctx, fmt.Sprintf("Finding user %s in project %s with pagination", userID, projectID))

var after string
hasMore := true
pageCount := 0

for hasMore {
pageCount++
tflog.Debug(ctx, fmt.Sprintf("Fetching page %d for project %s (after: %s)", pageCount, projectID, after))

userList, err := clientInstance.ListProjectUsers(projectID, after, batchSize)
if err != nil {
return nil, false, fmt.Errorf("error fetching project users (page %d): %w", pageCount, err)
}

// Look for the user in this page
for _, user := range userList.Data {
if user.ID == userID {
tflog.Debug(ctx, fmt.Sprintf("Found user %s in project %s on page %d", userID, projectID, pageCount))
return &user, true, nil
}
}

// Check if there are more pages
hasMore = userList.HasMore
if hasMore && userList.LastID != "" {
after = userList.LastID
}
}

tflog.Debug(ctx, fmt.Sprintf("User %s not found in project %s after checking %d pages", userID, projectID, pageCount))
return nil, false, nil
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Protect pagination loop from empty cursors.

Same cursor issue here: when HasMore is true yet LastID is blank, after never advances and we re-request the same page. Please guard against that case by breaking or extracting the ID from the last element before looping again.

🤖 Prompt for AI Agents
In internal/provider/resource_openai_project_user.go around lines 68 to 101 the
pagination loop can re-request the same page when userList.HasMore is true but
userList.LastID is empty; guard against that by: if HasMore is true and LastID
is non-empty set after = LastID, otherwise if HasMore is true but LastID is
empty then if userList.Data is non-empty set after to the ID of the last element
in userList.Data, else break the loop (no more progress); this ensures the
cursor advances or the loop exits instead of looping forever.

@Fodoj Fodoj merged commit 4580306 into main Nov 3, 2025
5 of 6 checks passed
@Fodoj Fodoj deleted the fix-users branch November 3, 2025 23:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants