basecamp-sdk 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +106 -10
  3. data/lib/basecamp/client.rb +8 -8
  4. data/lib/basecamp/config.rb +4 -3
  5. data/lib/basecamp/generated/metadata.json +7 -17
  6. data/lib/basecamp/generated/services/account_service.rb +1 -1
  7. data/lib/basecamp/generated/services/automation_service.rb +1 -1
  8. data/lib/basecamp/generated/services/boosts_service.rb +3 -3
  9. data/lib/basecamp/generated/services/campfires_service.rb +7 -7
  10. data/lib/basecamp/generated/services/card_columns_service.rb +1 -1
  11. data/lib/basecamp/generated/services/card_steps_service.rb +1 -1
  12. data/lib/basecamp/generated/services/card_tables_service.rb +1 -1
  13. data/lib/basecamp/generated/services/cards_service.rb +2 -2
  14. data/lib/basecamp/generated/services/checkins_service.rb +8 -8
  15. data/lib/basecamp/generated/services/client_approvals_service.rb +2 -2
  16. data/lib/basecamp/generated/services/client_correspondences_service.rb +2 -2
  17. data/lib/basecamp/generated/services/client_replies_service.rb +2 -2
  18. data/lib/basecamp/generated/services/comments_service.rb +2 -2
  19. data/lib/basecamp/generated/services/documents_service.rb +2 -2
  20. data/lib/basecamp/generated/services/events_service.rb +1 -1
  21. data/lib/basecamp/generated/services/everything_service.rb +16 -26
  22. data/lib/basecamp/generated/services/forwards_service.rb +5 -5
  23. data/lib/basecamp/generated/services/gauges_service.rb +3 -3
  24. data/lib/basecamp/generated/services/hill_charts_service.rb +1 -1
  25. data/lib/basecamp/generated/services/message_boards_service.rb +1 -1
  26. data/lib/basecamp/generated/services/message_types_service.rb +2 -2
  27. data/lib/basecamp/generated/services/messages_service.rb +2 -2
  28. data/lib/basecamp/generated/services/my_assignments_service.rb +3 -3
  29. data/lib/basecamp/generated/services/my_notifications_service.rb +2 -2
  30. data/lib/basecamp/generated/services/people_service.rb +8 -8
  31. data/lib/basecamp/generated/services/projects_service.rb +2 -2
  32. data/lib/basecamp/generated/services/recordings_service.rb +2 -2
  33. data/lib/basecamp/generated/services/reports_service.rb +5 -5
  34. data/lib/basecamp/generated/services/schedules_service.rb +4 -4
  35. data/lib/basecamp/generated/services/search_service.rb +2 -2
  36. data/lib/basecamp/generated/services/subscriptions_service.rb +1 -1
  37. data/lib/basecamp/generated/services/templates_service.rb +3 -3
  38. data/lib/basecamp/generated/services/timeline_service.rb +1 -1
  39. data/lib/basecamp/generated/services/timesheets_service.rb +4 -4
  40. data/lib/basecamp/generated/services/todolist_groups_service.rb +1 -1
  41. data/lib/basecamp/generated/services/todolists_service.rb +2 -2
  42. data/lib/basecamp/generated/services/todos_service.rb +2 -2
  43. data/lib/basecamp/generated/services/todosets_service.rb +1 -1
  44. data/lib/basecamp/generated/services/tools_service.rb +1 -1
  45. data/lib/basecamp/generated/services/uploads_service.rb +3 -3
  46. data/lib/basecamp/generated/services/vaults_service.rb +2 -2
  47. data/lib/basecamp/generated/services/webhooks_service.rb +2 -2
  48. data/lib/basecamp/generated/types.rb +1 -34
  49. data/lib/basecamp/http.rb +47 -19
  50. data/lib/basecamp/oauth/device_authorization.rb +37 -0
  51. data/lib/basecamp/oauth/device_flow.rb +873 -0
  52. data/lib/basecamp/oauth/device_flow_error.rb +46 -0
  53. data/lib/basecamp/oauth/discovery.rb +13 -11
  54. data/lib/basecamp/oauth/exchange.rb +44 -5
  55. data/lib/basecamp/oauth/fetcher.rb +626 -56
  56. data/lib/basecamp/oauth/oauth_error.rb +3 -1
  57. data/lib/basecamp/oauth/refresh_request.rb +8 -2
  58. data/lib/basecamp/oauth/resource.rb +13 -10
  59. data/lib/basecamp/oauth/token.rb +10 -3
  60. data/lib/basecamp/oauth.rb +47 -2
  61. data/lib/basecamp/version.rb +2 -2
  62. data/scripts/generate-services.rb +9 -6
  63. metadata +5 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 06b1a1a19b501f386b63739724bb50b0f1d778660d2765f04d2c6a0d2214ef14
4
- data.tar.gz: 3c2b5c8409d5a334eabb01b668e430510ad79b93cefc5ead3c5475f4b8f5d661
3
+ metadata.gz: b2b13ea2c32e1d4bf22a04af06e8c94d039e7698643a7aa7d9e0813f1526db2d
4
+ data.tar.gz: 942a8f09b4950764aa132ebd52a195d9d0eebb27b93656fb542e7e52b20ae1af
5
5
  SHA512:
6
- metadata.gz: 0d9134c7b9996490c035fccc0c949acd1c05faff5c52b40aa5f30ab913456c10701a8742b74df33aea35539d197dd8c4932df3a1305d73db3efe2ac3de7bfa34
7
- data.tar.gz: 3a4c47cb7196bd642f1c0863d25ff02fcf8ec607e102a6ce16cafe54aa5a74ef6290561c1897e365ff848e77911bdf8d7b3d31d85395adc4ba0b0753ba21d641
6
+ metadata.gz: '081775bc028c20205c904d026ab9f79bf077e7cde1d1bf5dd074b0e641fedf2ac47ebe3ca3cd6860f5bed05ad92d672abcaaf0332ec0f9aa94327ebc4cfcb130'
7
+ data.tar.gz: ffee6cf6e25a9ad523628b5d5f9da23f15460c3b01cd03dd8ab5df472942826e05d1a40afc09fc6cec9acaf9a3b2329c468f94b69a3ee4f95e2a784d94eb2775
data/README.md CHANGED
@@ -42,7 +42,6 @@ project = account.projects.get(project_id: 12345)
42
42
 
43
43
  # Create a todo
44
44
  todo = account.todos.create(
45
- project_id: 12345,
46
45
  todolist_id: 67890,
47
46
  content: "Review PR",
48
47
  due_on: "2024-12-31"
@@ -57,7 +56,7 @@ todo = account.todos.create(
57
56
  config = Basecamp::Config.new(
58
57
  base_url: "https://3.basecampapi.com", # Default
59
58
  timeout: 30, # Request timeout in seconds
60
- max_retries: 3, # Max retry attempts for GET requests
59
+ max_retries: 3, # Total request attempts for GET requests (including the initial request)
61
60
  base_delay: 1.0, # Base delay for exponential backoff
62
61
  max_pages: 10_000 # Max pages for pagination
63
62
  )
@@ -72,7 +71,7 @@ client = Basecamp::Client.new(config: config, token_provider: token_provider)
72
71
  |--------|---------|-------------|
73
72
  | `base_url` | `https://3.basecampapi.com` | Basecamp API base URL |
74
73
  | `timeout` | `30` | HTTP request timeout (seconds) |
75
- | `max_retries` | `3` | Maximum retry attempts for GET requests |
74
+ | `max_retries` | `3` | Total request attempts for GET requests (including the initial request) |
76
75
  | `base_delay` | `1.0` | Base delay for exponential backoff (seconds) |
77
76
  | `max_jitter` | `0.1` | Maximum random jitter added to delays |
78
77
  | `max_pages` | `10_000` | Maximum pages to fetch during pagination |
@@ -133,6 +132,99 @@ if token.expired?
133
132
  end
134
133
  ```
135
134
 
135
+ ### Device Authorization Grant (RFC 8628)
136
+
137
+ For input-constrained clients (CLIs, TVs) that can't host a redirect URI, the
138
+ device flow trades a redirect for a user-entered code. Basecamp pre-registers the
139
+ public `basecamp-cli` client (`token_endpoint_auth_method: none`, no secret); an
140
+ omitted scope defaults to `read` — prefer pinning it explicitly with
141
+ `scope: "read"`. Pass bare origins everywhere (no trailing slash): binding is
142
+ code-point exact — a trailing-slash `expected_issuer` raises a **hard**
143
+ `expected_issuer_unavailable`, while a trailing-slash resource origin breaks
144
+ the hop-1 binding and silently soft-falls back to Launchpad.
145
+
146
+ ```ruby
147
+ # The device grant runs against an ALREADY-SELECTED config whose issuer advertises
148
+ # a device_authorization_endpoint. Resolve one with resource-first discovery —
149
+ # NOT discover_launchpad: Launchpad advertises no device endpoint, so the
150
+ # capability guard would reject it.
151
+ result = Basecamp::Oauth.discover_from_resource("https://3.basecampapi.com")
152
+ raise "device login is not available for this resource" unless result.selected?
153
+ config = result.config
154
+
155
+ # One call runs the whole grant against the SELECTED config: it guards
156
+ # capability, requests a device/user code, shows it via the display hook, then
157
+ # polls the token endpoint until the user approves.
158
+ token = Basecamp::Oauth.perform_device_login(
159
+ config: config,
160
+ client_id: "basecamp-cli",
161
+ display: ->(auth) do
162
+ puts "Visit #{auth.verification_uri} and enter code: #{auth.user_code}"
163
+ puts "Or open: #{auth.verification_uri_complete}" if auth.verification_uri_complete
164
+ end
165
+ )
166
+
167
+ client = Basecamp.client(access_token: token.access_token)
168
+
169
+ # BC5 device logins as basecamp-cli mint MULTI-ACCOUNT refresh tokens: the
170
+ # token carries an RFC 8707 resource indicator (token.resource,
171
+ # "urn:bc:account:<id>"), and refreshing without echoing it is rejected
172
+ # (400 invalid_request). Persist token.resource alongside the tokens and
173
+ # echo it on refresh:
174
+ # A device-token response MAY omit refresh_token — guard it: without one,
175
+ # refreshing is impossible and the user must re-run the device login.
176
+ if token.expired? && token.refresh_token
177
+ fresh = Basecamp::Oauth.refresh_token(
178
+ token_endpoint: config.token_endpoint,
179
+ refresh_token: token.refresh_token,
180
+ client_id: "basecamp-cli", # public client — no secret
181
+ resource: token.resource
182
+ )
183
+ # A refresh response MAY omit resource (the binding is unchanged) — persist
184
+ # `fresh.resource || token.resource` so the next refresh still echoes it.
185
+ end
186
+ ```
187
+
188
+ The capability guard requires BOTH `config.device_authorization_endpoint` AND the
189
+ exact grant-type URN `urn:ietf:params:oauth:grant-type:device_code` in
190
+ `config.grant_types_supported` — servers advertise the full URN, so checking for
191
+ a bare `device_code` entry would wrongly conclude the flow is unavailable;
192
+ otherwise it raises
193
+ `Basecamp::Oauth::DeviceFlowError` with `reason: :unavailable` before any request
194
+ is issued. The two lower-level steps are also exposed directly:
195
+
196
+ ```ruby
197
+ auth = Basecamp::Oauth.request_device_authorization(
198
+ device_authorization_endpoint: config.device_authorization_endpoint,
199
+ client_id: "basecamp-cli"
200
+ )
201
+
202
+ token = Basecamp::Oauth.poll_device_token(
203
+ token_endpoint: config.token_endpoint,
204
+ client_id: "basecamp-cli",
205
+ device_code: auth.device_code,
206
+ interval: auth.interval,
207
+ expires_in: auth.expires_in
208
+ )
209
+ ```
210
+
211
+ The polling loop waits at least `interval` seconds between polls against a
212
+ **monotonic** deadline, sustains a `slow_down` bump (+5s for every later poll),
213
+ and backs off exponentially on connection timeouts. A terminal outcome raises
214
+ `DeviceFlowError` carrying a `reason` whose parent `type` is derived from it:
215
+
216
+ | `reason` | `type` | Meaning |
217
+ |---|---|---|
218
+ | `:access_denied` | `auth` | The user declined the request |
219
+ | `:expired` | `auth` | The code expired before approval |
220
+ | `:transport` | `network` (retryable) | A network failure ended the flow |
221
+ | `:unavailable` | `validation` | The config can't do device flow |
222
+ | `:cancelled` | `usage` | The caller cancelled the flow |
223
+
224
+ `poll_device_token` and `perform_device_login` accept injectable `clock`,
225
+ `sleeper`, and `cancelled` callables — pass a `cancelled` probe (e.g. one that
226
+ checks a signal set by SIGINT) to abort a pending login cleanly.
227
+
136
228
  ### Resource-First Discovery (RFC 9728 + RFC 8414)
137
229
 
138
230
  BC5's Authorization Server (AS) metadata lives only at the canonical issuer (the
@@ -179,7 +271,7 @@ aborts before an oversized body is buffered.
179
271
 
180
272
  ## Services
181
273
 
182
- The SDK provides 45 account-scoped services. The table below covers the common ones; see `lib/basecamp/generated/services/` for the authoritative, complete set:
274
+ The SDK provides 46 account-scoped services. The table below covers the common ones; see `lib/basecamp/generated/services/` for the authoritative, complete set:
183
275
 
184
276
  | Service | Description |
185
277
  |---------|-------------|
@@ -260,13 +352,15 @@ result = account.download_url(url)
260
352
 
261
353
  ## Retry Behavior
262
354
 
263
- GET requests automatically retry on transient failures with exponential backoff:
264
-
265
- - **Retryable errors**: 429 (rate limit), 502, 503, 504 (gateway errors)
266
- - **Backoff**: Exponential with jitter (1s, 2s, 4s...)
267
- - **Rate limits**: Respects `Retry-After` header
355
+ Only plain GET requests retry — automatically, with exponential backoff. Mutation operations (POST, PUT, DELETE) do **not** retry to prevent data duplication, and the raw upload and download paths skip the retry loop entirely (the upload path is strictly one request; the download hop keeps only the one-shot 401 replay below).
268
356
 
269
- Mutation operations (POST, PUT, DELETE) do **not** retry to prevent data duplication.
357
+ - **Which errors**: Retry keys off the error's `retryable?` classification, not a declared status list — 429 (rate limit), 500, 502, 503, 504, and any other 5xx all retry, as does `NetworkError` (connection failures, including DNS and connect-phase timeouts). Read timeouts are the exception: Faraday surfaces them as a status-less `ApiError` with `retryable? == false`, so a GET that times out mid-response fails on the first attempt. 400, 401, 403, 404, and 422 never retry.
358
+ - **`max_retries`**: Total request attempts for GET requests, including the initial request — the default `3` means one initial attempt plus two retries. **`max_retries: 0` sends zero requests** and raises `Basecamp::ApiError` (`"Request failed after 0 attempts"`).
359
+ - **Backoff**: Exponential with jitter — `base_delay * 2^(attempt - 1) + rand * max_jitter` — uncapped, bounded in practice by the attempt budget.
360
+ - **Rate limits**: A 429's `Retry-After` header overrides the calculated backoff. Only 429 carries it: 5xx and network errors always use the exponential backoff.
361
+ - **401 responses**: With a refresh-capable token provider, the SDK refreshes the token and replays the request **once** — for all methods, including mutations — outside the `max_retries` budget. A second 401 is surfaced. The raw upload path has no 401 replay.
362
+ - **Per-operation metadata**: The retry policy operations declare (`retry_on` statuses, per-operation `max`) is inert in Ruby — every API GET issued through the client, including the Launchpad authorization fetch, rides the same classification-based loop bounded by `config.max_retries` alone. (The download flow's redirect hop and OAuth discovery use their own single-attempt transports.)
363
+ - **`retryable?`**: Unlike SDKs where the error classification is only a hint for your own code, in Ruby an error's `retryable?` (and `retry_after`) is exactly what the transport acts on for GET requests.
270
364
 
271
365
  ## Error Handling
272
366
 
@@ -335,6 +429,8 @@ client = Basecamp::Client.new(
335
429
  | `BASECAMP_TOKEN` | OAuth access token |
336
430
  | `BASECAMP_ACCOUNT_ID` | Account ID |
337
431
  | `BASECAMP_BASE_URL` | API base URL (default: `https://3.basecampapi.com`) |
432
+ | `BASECAMP_TIMEOUT` | Request timeout in seconds (default: `30`) |
433
+ | `BASECAMP_MAX_RETRIES` | Total request attempts for GET requests, including the initial request (default: `3`) |
338
434
 
339
435
  ## Development
340
436
 
@@ -152,8 +152,8 @@ module Basecamp
152
152
  # @param path [String] URL path (without account prefix)
153
153
  # @param params [Hash] query parameters
154
154
  # @return [Response]
155
- def get(path, params: {})
156
- @parent.http.get(account_path(path), params: params)
155
+ def get(path, params: {}, operation: nil)
156
+ @parent.http.get(account_path(path), params: params, operation: operation)
157
157
  end
158
158
 
159
159
  # Performs a POST request scoped to this account.
@@ -204,8 +204,8 @@ module Basecamp
204
204
  # @param params [Hash] query parameters
205
205
  # @yield [Hash] each item from the response
206
206
  # @return [Enumerator] if no block given
207
- def paginate(path, params: {}, &)
208
- @parent.http.paginate(account_path(path), params: params, &)
207
+ def paginate(path, params: {}, operation: nil, &)
208
+ @parent.http.paginate(account_path(path), params: params, operation: operation, &)
209
209
  end
210
210
 
211
211
  # Fetches all pages of a paginated resource, extracting items from a key.
@@ -215,8 +215,8 @@ module Basecamp
215
215
  # @param params [Hash] query parameters
216
216
  # @yield [Hash] each item from the response
217
217
  # @return [Enumerator] if no block given
218
- def paginate_key(path, key:, params: {}, &)
219
- @parent.http.paginate_key(account_path(path), key: key, params: params, &)
218
+ def paginate_key(path, key:, params: {}, operation: nil, &)
219
+ @parent.http.paginate_key(account_path(path), key: key, params: params, operation: operation, &)
220
220
  end
221
221
 
222
222
  # Fetches a wrapped paginated resource, returning wrapper fields + lazy paginated items.
@@ -224,8 +224,8 @@ module Basecamp
224
224
  # @param key [String] the key containing the array of paginated items
225
225
  # @param params [Hash] query parameters
226
226
  # @return [Hash] wrapper fields merged with key => Enumerator of all items
227
- def paginate_wrapped(path, key:, params: {})
228
- @parent.http.paginate_wrapped(account_path(path), key: key, params: params)
227
+ def paginate_wrapped(path, key:, params: {}, operation: nil)
228
+ @parent.http.paginate_wrapped(account_path(path), key: key, params: params, operation: operation)
229
229
  end
230
230
 
231
231
  # Downloads file content from any API-routable download URL.
@@ -24,7 +24,8 @@ module Basecamp
24
24
  # @return [Integer] request timeout in seconds
25
25
  attr_accessor :timeout
26
26
 
27
- # @return [Integer] maximum retry attempts for GET requests
27
+ # @return [Integer] total request attempts for GET requests, including the
28
+ # initial request (0 sends no requests at all and raises)
28
29
  attr_accessor :max_retries
29
30
 
30
31
  # @return [Float] initial backoff delay in seconds
@@ -48,7 +49,7 @@ module Basecamp
48
49
  #
49
50
  # @param base_url [String] API base URL
50
51
  # @param timeout [Integer] request timeout in seconds
51
- # @param max_retries [Integer] maximum retry attempts
52
+ # @param max_retries [Integer] total request attempts for GET requests, including the initial request
52
53
  # @param base_delay [Float] initial backoff delay
53
54
  # @param max_jitter [Float] maximum jitter
54
55
  # @param max_pages [Integer] maximum pages to fetch
@@ -78,7 +79,7 @@ module Basecamp
78
79
  # Environment variables:
79
80
  # - BASECAMP_BASE_URL: API base URL
80
81
  # - BASECAMP_TIMEOUT: Request timeout in seconds
81
- # - BASECAMP_MAX_RETRIES: Maximum retry attempts
82
+ # - BASECAMP_MAX_RETRIES: Total request attempts for GET requests, including the initial request
82
83
  #
83
84
  # @return [Config]
84
85
  def self.from_env
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://basecamp.com/schemas/sdk-metadata.json",
3
3
  "version": "1.0.0",
4
- "generated": "2026-07-28T21:34:14Z",
4
+ "generated": "2026-07-31T06:58:29Z",
5
5
  "operations": {
6
6
  "GetAccount": {
7
7
  "retry": {
@@ -67,22 +67,6 @@
67
67
  ]
68
68
  }
69
69
  },
70
- "GetEverythingBoosts": {
71
- "retry": {
72
- "maxAttempts": 3,
73
- "baseDelayMs": 1000,
74
- "backoff": "exponential",
75
- "retryOn": [
76
- 429,
77
- 503
78
- ]
79
- },
80
- "pagination": {
81
- "style": "link",
82
- "totalCountHeader": "X-Total-Count",
83
- "maxPageSize": 50
84
- }
85
- },
86
70
  "GetBoost": {
87
71
  "retry": {
88
72
  "maxAttempts": 3,
@@ -131,6 +115,9 @@
131
115
  429,
132
116
  503
133
117
  ]
118
+ },
119
+ "idempotent": {
120
+ "natural": true
134
121
  }
135
122
  },
136
123
  "DisableCardColumnOnHold": {
@@ -2054,6 +2041,9 @@
2054
2041
  429,
2055
2042
  503
2056
2043
  ]
2044
+ },
2045
+ "idempotent": {
2046
+ "natural": true
2057
2047
  }
2058
2048
  },
2059
2049
  "UpdateSubscription": {
@@ -11,7 +11,7 @@ module Basecamp
11
11
  # @return [Hash] response data
12
12
  def get_account()
13
13
  with_operation(service: "account", operation: "get_account", is_mutation: false) do
14
- http_get("/account.json").json
14
+ http_get("/account.json", operation: "GetAccount").json
15
15
  end
16
16
  end
17
17
 
@@ -11,7 +11,7 @@ module Basecamp
11
11
  # @return [Array<Hash>] response data
12
12
  def list_lineup_markers()
13
13
  with_operation(service: "automation", operation: "list_lineup_markers", is_mutation: false) do
14
- http_get("/lineup/markers.json").json
14
+ http_get("/lineup/markers.json", operation: "ListLineupMarkers").json
15
15
  end
16
16
  end
17
17
  end
@@ -12,7 +12,7 @@ module Basecamp
12
12
  # @return [Hash] response data
13
13
  def get_boost(boost_id:)
14
14
  with_operation(service: "boosts", operation: "get_boost", is_mutation: false, resource_id: boost_id) do
15
- http_get("/boosts/#{boost_id}").json
15
+ http_get("/boosts/#{boost_id}", operation: "GetBoost").json
16
16
  end
17
17
  end
18
18
 
@@ -31,7 +31,7 @@ module Basecamp
31
31
  # @return [Enumerator<Hash>] paginated results
32
32
  def list_recording_boosts(recording_id:)
33
33
  wrap_paginated(service: "boosts", operation: "list_recording_boosts", is_mutation: false, resource_id: recording_id) do
34
- paginate("/recordings/#{recording_id}/boosts.json")
34
+ paginate("/recordings/#{recording_id}/boosts.json", operation: "ListRecordingBoosts")
35
35
  end
36
36
  end
37
37
 
@@ -51,7 +51,7 @@ module Basecamp
51
51
  # @return [Enumerator<Hash>] paginated results
52
52
  def list_event_boosts(recording_id:, event_id:)
53
53
  wrap_paginated(service: "boosts", operation: "list_event_boosts", is_mutation: false, resource_id: event_id) do
54
- paginate("/recordings/#{recording_id}/events/#{event_id}/boosts.json")
54
+ paginate("/recordings/#{recording_id}/events/#{event_id}/boosts.json", operation: "ListEventBoosts")
55
55
  end
56
56
  end
57
57
 
@@ -13,7 +13,7 @@ module Basecamp
13
13
  # @return [Enumerator<Hash>] paginated results
14
14
  def list()
15
15
  wrap_paginated(service: "campfires", operation: "list", is_mutation: false) do
16
- paginate("/chats.json")
16
+ paginate("/chats.json", operation: "ListCampfires")
17
17
  end
18
18
  end
19
19
 
@@ -22,7 +22,7 @@ module Basecamp
22
22
  # @return [Hash] response data
23
23
  def get(campfire_id:)
24
24
  with_operation(service: "campfires", operation: "get", is_mutation: false, resource_id: campfire_id) do
25
- http_get("/chats/#{campfire_id}").json
25
+ http_get("/chats/#{campfire_id}", operation: "GetCampfire").json
26
26
  end
27
27
  end
28
28
 
@@ -31,7 +31,7 @@ module Basecamp
31
31
  # @return [Enumerator<Hash>] paginated results
32
32
  def list_chatbots(campfire_id:)
33
33
  wrap_paginated(service: "campfires", operation: "list_chatbots", is_mutation: false, resource_id: campfire_id) do
34
- paginate("/chats/#{campfire_id}/integrations.json")
34
+ paginate("/chats/#{campfire_id}/integrations.json", operation: "ListChatbots")
35
35
  end
36
36
  end
37
37
 
@@ -52,7 +52,7 @@ module Basecamp
52
52
  # @return [Hash] response data
53
53
  def get_chatbot(campfire_id:, chatbot_id:)
54
54
  with_operation(service: "campfires", operation: "get_chatbot", is_mutation: false, resource_id: chatbot_id) do
55
- http_get("/chats/#{campfire_id}/integrations/#{chatbot_id}").json
55
+ http_get("/chats/#{campfire_id}/integrations/#{chatbot_id}", operation: "GetChatbot").json
56
56
  end
57
57
  end
58
58
 
@@ -87,7 +87,7 @@ module Basecamp
87
87
  def list_lines(campfire_id:, sort: nil, direction: nil)
88
88
  wrap_paginated(service: "campfires", operation: "list_lines", is_mutation: false, resource_id: campfire_id) do
89
89
  params = compact_query_params(sort: sort, direction: direction)
90
- paginate("/chats/#{campfire_id}/lines.json", params: params)
90
+ paginate("/chats/#{campfire_id}/lines.json", params: params, operation: "ListCampfireLines")
91
91
  end
92
92
  end
93
93
 
@@ -108,7 +108,7 @@ module Basecamp
108
108
  # @return [Hash] response data
109
109
  def get_line(campfire_id:, line_id:)
110
110
  with_operation(service: "campfires", operation: "get_line", is_mutation: false, resource_id: line_id) do
111
- http_get("/chats/#{campfire_id}/lines/#{line_id}").json
111
+ http_get("/chats/#{campfire_id}/lines/#{line_id}", operation: "GetCampfireLine").json
112
112
  end
113
113
  end
114
114
 
@@ -143,7 +143,7 @@ module Basecamp
143
143
  def list_uploads(campfire_id:, sort: nil, direction: nil)
144
144
  wrap_paginated(service: "campfires", operation: "list_uploads", is_mutation: false, resource_id: campfire_id) do
145
145
  params = compact_query_params(sort: sort, direction: direction)
146
- paginate("/chats/#{campfire_id}/uploads.json", params: params)
146
+ paginate("/chats/#{campfire_id}/uploads.json", params: params, operation: "ListCampfireUploads")
147
147
  end
148
148
  end
149
149
 
@@ -43,7 +43,7 @@ module Basecamp
43
43
  # @return [Hash] response data
44
44
  def get(column_id:)
45
45
  with_operation(service: "cardcolumns", operation: "get", is_mutation: false, resource_id: column_id) do
46
- http_get("/card_tables/columns/#{column_id}").json
46
+ http_get("/card_tables/columns/#{column_id}", operation: "GetCardColumn").json
47
47
  end
48
48
  end
49
49
 
@@ -36,7 +36,7 @@ module Basecamp
36
36
  # @return [Hash] response data
37
37
  def get(step_id:)
38
38
  with_operation(service: "cardsteps", operation: "get", is_mutation: false, resource_id: step_id) do
39
- http_get("/card_tables/steps/#{step_id}").json
39
+ http_get("/card_tables/steps/#{step_id}", operation: "GetCardStep").json
40
40
  end
41
41
  end
42
42
 
@@ -12,7 +12,7 @@ module Basecamp
12
12
  # @return [Hash] response data
13
13
  def get(card_table_id:)
14
14
  with_operation(service: "cardtables", operation: "get", is_mutation: false, resource_id: card_table_id) do
15
- http_get("/card_tables/#{card_table_id}").json
15
+ http_get("/card_tables/#{card_table_id}", operation: "GetCardTable").json
16
16
  end
17
17
  end
18
18
  end
@@ -12,7 +12,7 @@ module Basecamp
12
12
  # @return [Hash] response data
13
13
  def get(card_id:)
14
14
  with_operation(service: "cards", operation: "get", is_mutation: false, resource_id: card_id) do
15
- http_get("/card_tables/cards/#{card_id}").json
15
+ http_get("/card_tables/cards/#{card_id}", operation: "GetCard").json
16
16
  end
17
17
  end
18
18
 
@@ -46,7 +46,7 @@ module Basecamp
46
46
  # @return [Enumerator<Hash>] paginated results
47
47
  def list(column_id:)
48
48
  wrap_paginated(service: "cards", operation: "list", is_mutation: false, resource_id: column_id) do
49
- paginate("/card_tables/lists/#{column_id}/cards.json")
49
+ paginate("/card_tables/lists/#{column_id}/cards.json", operation: "ListCards")
50
50
  end
51
51
  end
52
52
 
@@ -11,7 +11,7 @@ module Basecamp
11
11
  # @return [Enumerator<Hash>] paginated results
12
12
  def reminders()
13
13
  wrap_paginated(service: "checkins", operation: "reminders", is_mutation: false) do
14
- paginate("/my/question_reminders.json")
14
+ paginate("/my/question_reminders.json", operation: "GetQuestionReminders")
15
15
  end
16
16
  end
17
17
 
@@ -20,7 +20,7 @@ module Basecamp
20
20
  # @return [Hash] response data
21
21
  def get_answer(answer_id:)
22
22
  with_operation(service: "checkins", operation: "get_answer", is_mutation: false, resource_id: answer_id) do
23
- http_get("/question_answers/#{answer_id}").json
23
+ http_get("/question_answers/#{answer_id}", operation: "GetAnswer").json
24
24
  end
25
25
  end
26
26
 
@@ -41,7 +41,7 @@ module Basecamp
41
41
  # @return [Hash] response data
42
42
  def get_questionnaire(questionnaire_id:)
43
43
  with_operation(service: "checkins", operation: "get_questionnaire", is_mutation: false, resource_id: questionnaire_id) do
44
- http_get("/questionnaires/#{questionnaire_id}").json
44
+ http_get("/questionnaires/#{questionnaire_id}", operation: "GetQuestionnaire").json
45
45
  end
46
46
  end
47
47
 
@@ -50,7 +50,7 @@ module Basecamp
50
50
  # @return [Enumerator<Hash>] paginated results
51
51
  def list_questions(questionnaire_id:)
52
52
  wrap_paginated(service: "checkins", operation: "list_questions", is_mutation: false, resource_id: questionnaire_id) do
53
- paginate("/questionnaires/#{questionnaire_id}/questions.json")
53
+ paginate("/questionnaires/#{questionnaire_id}/questions.json", operation: "ListQuestions")
54
54
  end
55
55
  end
56
56
 
@@ -71,7 +71,7 @@ module Basecamp
71
71
  # @return [Hash] response data
72
72
  def get_question(question_id:)
73
73
  with_operation(service: "checkins", operation: "get_question", is_mutation: false, resource_id: question_id) do
74
- http_get("/questions/#{question_id}").json
74
+ http_get("/questions/#{question_id}", operation: "GetQuestion").json
75
75
  end
76
76
  end
77
77
 
@@ -92,7 +92,7 @@ module Basecamp
92
92
  # @return [Enumerator<Hash>] paginated results
93
93
  def list_answers(question_id:)
94
94
  wrap_paginated(service: "checkins", operation: "list_answers", is_mutation: false, resource_id: question_id) do
95
- paginate("/questions/#{question_id}/answers.json")
95
+ paginate("/questions/#{question_id}/answers.json", operation: "ListAnswers")
96
96
  end
97
97
  end
98
98
 
@@ -112,7 +112,7 @@ module Basecamp
112
112
  # @return [Enumerator<Hash>] paginated results
113
113
  def answerers(question_id:)
114
114
  wrap_paginated(service: "checkins", operation: "answerers", is_mutation: false, resource_id: question_id) do
115
- paginate("/questions/#{question_id}/answers/by.json")
115
+ paginate("/questions/#{question_id}/answers/by.json", operation: "ListQuestionAnswerers")
116
116
  end
117
117
  end
118
118
 
@@ -122,7 +122,7 @@ module Basecamp
122
122
  # @return [Enumerator<Hash>] paginated results
123
123
  def by_person(question_id:, person_id:)
124
124
  wrap_paginated(service: "checkins", operation: "by_person", is_mutation: false, resource_id: person_id) do
125
- paginate("/questions/#{question_id}/answers/by/#{person_id}")
125
+ paginate("/questions/#{question_id}/answers/by/#{person_id}", operation: "GetAnswersByPerson")
126
126
  end
127
127
  end
128
128
 
@@ -14,7 +14,7 @@ module Basecamp
14
14
  def list(sort: nil, direction: nil)
15
15
  wrap_paginated(service: "clientapprovals", operation: "list", is_mutation: false) do
16
16
  params = compact_query_params(sort: sort, direction: direction)
17
- paginate("/client/approvals.json", params: params)
17
+ paginate("/client/approvals.json", params: params, operation: "ListClientApprovals")
18
18
  end
19
19
  end
20
20
 
@@ -23,7 +23,7 @@ module Basecamp
23
23
  # @return [Hash] response data
24
24
  def get(approval_id:)
25
25
  with_operation(service: "clientapprovals", operation: "get", is_mutation: false, resource_id: approval_id) do
26
- http_get("/client/approvals/#{approval_id}").json
26
+ http_get("/client/approvals/#{approval_id}", operation: "GetClientApproval").json
27
27
  end
28
28
  end
29
29
  end
@@ -14,7 +14,7 @@ module Basecamp
14
14
  def list(sort: nil, direction: nil)
15
15
  wrap_paginated(service: "clientcorrespondences", operation: "list", is_mutation: false) do
16
16
  params = compact_query_params(sort: sort, direction: direction)
17
- paginate("/client/correspondences.json", params: params)
17
+ paginate("/client/correspondences.json", params: params, operation: "ListClientCorrespondences")
18
18
  end
19
19
  end
20
20
 
@@ -23,7 +23,7 @@ module Basecamp
23
23
  # @return [Hash] response data
24
24
  def get(correspondence_id:)
25
25
  with_operation(service: "clientcorrespondences", operation: "get", is_mutation: false, resource_id: correspondence_id) do
26
- http_get("/client/correspondences/#{correspondence_id}").json
26
+ http_get("/client/correspondences/#{correspondence_id}", operation: "GetClientCorrespondence").json
27
27
  end
28
28
  end
29
29
  end
@@ -12,7 +12,7 @@ module Basecamp
12
12
  # @return [Enumerator<Hash>] paginated results
13
13
  def list(recording_id:)
14
14
  wrap_paginated(service: "clientreplies", operation: "list", is_mutation: false, resource_id: recording_id) do
15
- paginate("/client/recordings/#{recording_id}/replies.json")
15
+ paginate("/client/recordings/#{recording_id}/replies.json", operation: "ListClientReplies")
16
16
  end
17
17
  end
18
18
 
@@ -22,7 +22,7 @@ module Basecamp
22
22
  # @return [Hash] response data
23
23
  def get(recording_id:, reply_id:)
24
24
  with_operation(service: "clientreplies", operation: "get", is_mutation: false, resource_id: reply_id) do
25
- http_get("/client/recordings/#{recording_id}/replies/#{reply_id}").json
25
+ http_get("/client/recordings/#{recording_id}/replies/#{reply_id}", operation: "GetClientReply").json
26
26
  end
27
27
  end
28
28
  end
@@ -12,7 +12,7 @@ module Basecamp
12
12
  # @return [Hash] response data
13
13
  def get(comment_id:)
14
14
  with_operation(service: "comments", operation: "get", is_mutation: false, resource_id: comment_id) do
15
- http_get("/comments/#{comment_id}").json
15
+ http_get("/comments/#{comment_id}", operation: "GetComment").json
16
16
  end
17
17
  end
18
18
 
@@ -31,7 +31,7 @@ module Basecamp
31
31
  # @return [Enumerator<Hash>] paginated results
32
32
  def list(recording_id:)
33
33
  wrap_paginated(service: "comments", operation: "list", is_mutation: false, resource_id: recording_id) do
34
- paginate("/recordings/#{recording_id}/comments.json")
34
+ paginate("/recordings/#{recording_id}/comments.json", operation: "ListComments")
35
35
  end
36
36
  end
37
37
 
@@ -12,7 +12,7 @@ module Basecamp
12
12
  # @return [Hash] response data
13
13
  def get(document_id:)
14
14
  with_operation(service: "documents", operation: "get", is_mutation: false, resource_id: document_id) do
15
- http_get("/documents/#{document_id}").json
15
+ http_get("/documents/#{document_id}", operation: "GetDocument").json
16
16
  end
17
17
  end
18
18
 
@@ -32,7 +32,7 @@ module Basecamp
32
32
  # @return [Enumerator<Hash>] paginated results
33
33
  def list(vault_id:)
34
34
  wrap_paginated(service: "documents", operation: "list", is_mutation: false, resource_id: vault_id) do
35
- paginate("/vaults/#{vault_id}/documents.json")
35
+ paginate("/vaults/#{vault_id}/documents.json", operation: "ListDocuments")
36
36
  end
37
37
  end
38
38
 
@@ -12,7 +12,7 @@ module Basecamp
12
12
  # @return [Enumerator<Hash>] paginated results
13
13
  def list(recording_id:)
14
14
  wrap_paginated(service: "events", operation: "list", is_mutation: false, resource_id: recording_id) do
15
- paginate("/recordings/#{recording_id}/events.json")
15
+ paginate("/recordings/#{recording_id}/events.json", operation: "ListEvents")
16
16
  end
17
17
  end
18
18
  end