Skip to content

Commit 92026d6

Browse files
authored
Add pagination to all SDK list methods (breaking, 0.4.0) (#173)
* Add wrapped pagination trait and timesheet pagination to Smithy spec Add `key` field to `basecampPagination` trait for wrapped-object pagination (responses like `{person, events}` where paginated items are nested under a key rather than returned as a bare array). Mark GetProjectTimesheet and GetRecordingTimesheet as paginated. Fix GetPersonProgress URI to include `.json` suffix via enhance-openapi script. Regenerate openapi.json, client.gen.go, and behavior-model.json. * Add wrapped pagination support to all SDK generators and runtimes Each SDK gains the ability to paginate responses where items are nested inside a wrapper object (e.g. `{person: {...}, events: [...]}`) rather than returned as a bare array. Ruby: `paginate_wrapped` helper + `wrap_paginated_wrapped` hook wrapper that fires on_operation_start eagerly and on_operation_end when the lazy Enumerator completes. TypeScript: `requestPaginatedWrapped<K, TItem>` method preserving wrapper fields alongside a `ListResult<TItem>` for the paginated key. Swift: `requestPaginated` overload accepting `itemsKey` parameter to extract items from wrapped responses. Kotlin: Generator emits custom `parseItems` lambda extracting items from the wrapper object's key. All four generators updated to read `x-basecamp-pagination.key` and route to the wrapped pagination path. * Regenerate SDK services and add wrapped pagination tests Regenerate Ruby, TypeScript, Swift, and Kotlin services from updated OpenAPI spec. PersonProgress now uses wrapped pagination (key: "events"), and timesheet for_project/for_recording are now paginated. Add multi-page wrapped pagination tests exercising the full person→events accumulation path in all five SDKs (Go test in next commit). Each test verifies wrapper fields (person) are preserved while events paginate across Link-header pages. * Add pagination support to all Go SDK list methods 21 list methods previously returned only page 1. Each now accepts optional list options (Limit, Page) and follows Link-header pagination via followPagination, returning *ListResult with TotalCount and Truncated metadata. Group A (12 methods): webhooks, client approvals/correspondences/replies, templates, campfires, campfire lines/uploads, boosts, todolist groups, message types — gain ListOptions parameter and followPagination. Group B (7 methods): progress, project timeline, person progress, search, pingable people, vault versions, chatbots — gain result wrapper types. Group C (2 methods): project/recording timesheet reports — gain pagination on existing TimesheetReportOptions. PersonProgress uses a custom wrapped pagination loop since each page returns {person, events} rather than a bare array. Includes multi-page wrapped pagination tests. BREAKING: All 21 methods have new signatures (added opts parameter and/or changed return types). Callers pass nil for default behavior. * Update conformance runners and bump version to 0.4.0 Adapt Go, Ruby, and TypeScript conformance runners for new pagination signatures. Update paths.json for .json-suffixed person progress route. Bump version to 0.4.0 across Go, Ruby, and root package.json to reflect breaking pagination API changes. * Tighten timeline limit contract and add missing test coverage Fix doc/code mismatch: docs said "Use -1 for unlimited" but code treats any negative value as unlimited. Update all three method docs to say "negative = unlimited" matching the actual behavior. Add default-cap (Limit:0 and nil with >100 items) and unlimited tests for ProjectTimeline and PersonProgress, achieving symmetric test coverage across all three timeline entrypoints. * Address bot review findings - Add missing negative-remaining guard in wrappedPaginationHandler test helper, matching the other two pagination handlers - Normalize negative Limit in ListChatbots to match all other list methods (was passing raw negative value through) - Fix Kotlin generator trailing comma no-op: else branch now emits "" instead of "," for last property in data class - Update Page field docs from "non-zero" to "positive" across all files changed in this PR, matching the actual `> 0` check - Regenerate Kotlin services (fixes trailing comma in PersonProgressResult) * Sanitize log output in conformance runner to fix CodeQL alert Strip newlines from test result messages before printing to prevent log injection (go/log-injection). Pre-existing code newly flagged by CodeQL due to surrounding changes. * Revert stale Upload.kt width/height Double back to Int The Kotlin generator produces Int (matching openapi.json type: integer). The Double was from a stale regeneration and doesn't match the spec.
1 parent 5834bd8 commit 92026d6

78 files changed

Lines changed: 3632 additions & 489 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

behavior-model.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,10 @@
782782
},
783783
"GetPersonProgress": {
784784
"readonly": true,
785+
"pagination": {
786+
"style": "link",
787+
"maxPageSize": 50
788+
},
785789
"retry": {
786790
"max": 3,
787791
"base_delay_ms": 1000,
@@ -850,6 +854,10 @@
850854
},
851855
"GetProjectTimesheet": {
852856
"readonly": true,
857+
"pagination": {
858+
"style": "link",
859+
"maxPageSize": 50
860+
},
853861
"retry": {
854862
"max": 3,
855863
"base_delay_ms": 1000,
@@ -914,6 +922,10 @@
914922
},
915923
"GetRecordingTimesheet": {
916924
"readonly": true,
925+
"pagination": {
926+
"style": "link",
927+
"maxPageSize": 50
928+
},
917929
"retry": {
918930
"max": 3,
919931
"base_delay_ms": 1000,

conformance/runner/go/main.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ func main() {
113113
fmt.Printf(" PASS: %s\n", tc.Name)
114114
} else {
115115
failed++
116-
fmt.Printf(" FAIL: %s\n %s\n", tc.Name, result.Message)
116+
sanitized := strings.ReplaceAll(strings.ReplaceAll(result.Message, "\n", " "), "\r", "")
117+
fmt.Printf(" FAIL: %s\n %s\n", tc.Name, sanitized)
117118
}
118119
}
119120
}
@@ -445,7 +446,7 @@ func executeOperation(ctx context.Context, account *basecamp.AccountClient, tc T
445446

446447
case "ListWebhooks":
447448
bucketID := getInt64Param(tc.PathParams, "bucketId")
448-
_, err := account.Webhooks().List(ctx, bucketID)
449+
_, err := account.Webhooks().List(ctx, bucketID, nil)
449450
return operationResult{err: err}
450451

451452
case "CreateWebhook":

conformance/runner/ruby/runner.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def call(operation, path_params: {}, query_params: {}, body: nil)
107107
when "GetProjectTimesheet"
108108
@account.timesheets.for_project(
109109
project_id: path_params["projectId"]
110-
)
110+
).to_a
111111
when "UpdateTimesheetEntry"
112112
@account.timesheets.update(
113113
entry_id: path_params["entryId"],

conformance/runner/typescript/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

conformance/tests/paths.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,17 @@
3131
"tags": ["path", "reports"]
3232
},
3333
{
34-
"name": "Person progress uses /reports/users/ path",
35-
"description": "Verifies that GetPersonProgress constructs the URL path using /reports/users/progress/{personId}",
34+
"name": "Person progress uses /reports/users/ path with .json",
35+
"description": "Verifies that GetPersonProgress constructs the URL path using /reports/users/progress/{personId}.json",
3636
"operation": "GetPersonProgress",
3737
"method": "GET",
38-
"path": "/reports/users/progress/{personId}",
38+
"path": "/reports/users/progress/{personId}.json",
3939
"pathParams": {"personId": 45678},
4040
"mockResponses": [
4141
{"status": 200, "body": {"person": {"id": 45678, "name": "Test"}, "events": []}}
4242
],
4343
"assertions": [
44-
{"type": "requestPath", "expected": "/999/reports/users/progress/45678"},
44+
{"type": "requestPath", "expected": "/999/reports/users/progress/45678.json"},
4545
{"type": "noError"}
4646
],
4747
"tags": ["path", "reports"]

go/pkg/basecamp/boosts.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ type BoostListOptions struct {
2828
// If 0, uses DefaultBoostLimit (50). Use -1 for unlimited.
2929
Limit int
3030

31-
// Page, if non-zero, disables automatic pagination and returns only the first page.
31+
// Page, if positive, disables automatic pagination and returns only the first page.
3232
// NOTE: The page number itself is not honored; setting Page=2 does NOT fetch page 2.
3333
Page int
3434
}
@@ -57,7 +57,7 @@ func NewBoostsService(client *AccountClient) *BoostsService {
5757
//
5858
// Pagination options:
5959
// - Limit: maximum number of boosts to return (0 = 50, -1 = unlimited)
60-
// - Page: if non-zero, disables pagination and returns first page only
60+
// - Page: if positive, disables pagination and returns first page only
6161
//
6262
// The returned BoostListResult includes pagination metadata (TotalCount from
6363
// X-Total-Count header) when available.
@@ -138,7 +138,7 @@ func (s *BoostsService) ListRecording(ctx context.Context, recordingID int64, op
138138
//
139139
// Pagination options:
140140
// - Limit: maximum number of boosts to return (0 = 50, -1 = unlimited)
141-
// - Page: if non-zero, disables pagination and returns first page only
141+
// - Page: if positive, disables pagination and returns first page only
142142
//
143143
// The returned BoostListResult includes pagination metadata (TotalCount from
144144
// X-Total-Count header) when available.

0 commit comments

Comments
 (0)