basecamp-sdk 0.16.0 → 0.17.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67cf91504a5aadff973463d83b4a4de98aa4ad35e2dd7d0d4e5f4f26021e8423
4
- data.tar.gz: 88eacbb6628831a01e6a9cb56503618377ffec4a85ad85d5a514b025ff53dd2a
3
+ metadata.gz: 6831e79b8c78f4ba54ee64287457227c5553fbd2837e71b09d022601084ac2af
4
+ data.tar.gz: 3dd5ac9aefd6f03e78fa9fa4c9833ac98d217a09a05ea129b8b2b6f3d7049939
5
5
  SHA512:
6
- metadata.gz: 8cede9f49e112c50b65d7c7c4901f442cba400211bfe4676a9af95a95ac4ca5da1a1477c529f72456745a0f8423b766162c151135b89a2c52ce1abbacb9eb17f
7
- data.tar.gz: 695198c7697799719f0d66657463ed8ad76739bcd3bbe11877ad0a16d1fdd6b20602e2d6efa623c9d6e05a2b78ef5528e346ebec5975dd1697e5379e96b1e640
6
+ metadata.gz: 007f77e92a111a5687b26740caedc75e71e640ddfc10a32057b39b1627ab7cedd2412faebc75647b26402551e7f235b4bf77743236e8ab9da5fc26e8b8286c0b
7
+ data.tar.gz: dff2430d3c5225c2ad563c0848d61d09a1da0620c58d93438947318f567b2f551548b5a899dfbadb55d14f84b495efb8982f786515c8fd934a885757b5f9db5e
@@ -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-09-03T02:27:35Z",
4
+ "generated": "2026-09-08T23:57:38Z",
5
5
  "operations": {
6
6
  "GetAccount": {
7
7
  "retry": {
@@ -1746,6 +1746,34 @@
1746
1746
  "natural": true
1747
1747
  }
1748
1748
  },
1749
+ "EnableProjectClients": {
1750
+ "retry": {
1751
+ "maxAttempts": 3,
1752
+ "baseDelayMs": 1000,
1753
+ "backoff": "exponential",
1754
+ "retryOn": [
1755
+ 429,
1756
+ 503
1757
+ ]
1758
+ },
1759
+ "idempotent": {
1760
+ "natural": true
1761
+ }
1762
+ },
1763
+ "DisableProjectClients": {
1764
+ "retry": {
1765
+ "maxAttempts": 3,
1766
+ "baseDelayMs": 1000,
1767
+ "backoff": "exponential",
1768
+ "retryOn": [
1769
+ 429,
1770
+ 503
1771
+ ]
1772
+ },
1773
+ "idempotent": {
1774
+ "natural": true
1775
+ }
1776
+ },
1749
1777
  "ToggleGauge": {
1750
1778
  "retry": {
1751
1779
  "maxAttempts": 2,
@@ -1803,6 +1831,19 @@
1803
1831
  "maxPageSize": 50
1804
1832
  }
1805
1833
  },
1834
+ "UpdateProjectClientAccess": {
1835
+ "retry": {
1836
+ "maxAttempts": 3,
1837
+ "baseDelayMs": 1000,
1838
+ "backoff": "exponential",
1839
+ "retryOn": [
1840
+ 503
1841
+ ]
1842
+ },
1843
+ "idempotent": {
1844
+ "natural": true
1845
+ }
1846
+ },
1806
1847
  "UpdateProjectAccess": {
1807
1848
  "retry": {
1808
1849
  "maxAttempts": 3,
@@ -107,6 +107,24 @@ module Basecamp
107
107
  end
108
108
  end
109
109
 
110
+ # Enable clients on a project so client users can be added to it
111
+ # @param project_id [Integer] project id ID
112
+ # @return [Hash] response data
113
+ def enable_project_clients(project_id:)
114
+ with_operation(service: "people", operation: "enable_project_clients", is_mutation: true, project_id: project_id) do
115
+ http_post("/projects/#{project_id}/client_enablement.json").json
116
+ end
117
+ end
118
+
119
+ # Disable clients on a project
120
+ # @param project_id [Integer] project id ID
121
+ # @return [Hash] response data
122
+ def disable_project_clients(project_id:)
123
+ with_operation(service: "people", operation: "disable_project_clients", is_mutation: true, project_id: project_id) do
124
+ http_delete("/projects/#{project_id}/client_enablement.json").json
125
+ end
126
+ end
127
+
110
128
  # List all active people on a project
111
129
  # @param project_id [Integer] project id ID
112
130
  # @param page [Integer, nil] Page number for paginating through results. Defaults to 1. A positive value selects exactly that page, not a starting offset; see SPEC section 8.
@@ -119,6 +137,18 @@ module Basecamp
119
137
  end
120
138
  end
121
139
 
140
+ # Update project client access (grant/revoke/create client users)
141
+ # @param project_id [Integer] project id ID
142
+ # @param grant [Array, nil] Existing client people IDs to add to the project.
143
+ # @param revoke [Array, nil] Client people IDs to remove from the project.
144
+ # @param create [Array, nil] New clients to invite by email.
145
+ # @return [Hash] response data
146
+ def update_project_client_access(project_id:, grant: nil, revoke: nil, create: nil)
147
+ with_operation(service: "people", operation: "update_project_client_access", is_mutation: true, project_id: project_id) do
148
+ http_put("/projects/#{project_id}/people/client_users.json", body: compact_params(grant: grant, revoke: revoke, create: create)).json
149
+ end
150
+ end
151
+
122
152
  # Update project access (grant/revoke/create people)
123
153
  # @param project_id [Integer] project id ID
124
154
  # @param grant [Array, nil] grant
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Auto-generated from OpenAPI spec. Do not edit manually.
4
- # Generated: 2026-09-03T02:27:36Z
4
+ # Generated: 2026-09-08T23:57:39Z
5
5
 
6
6
  require "json"
7
7
  require "time"
@@ -1135,6 +1135,58 @@ module Basecamp
1135
1135
  end
1136
1136
  end
1137
1137
 
1138
+ # ClientInvitationError
1139
+ class ClientInvitationError
1140
+ include TypeHelpers
1141
+ attr_accessor :email_address, :messages
1142
+
1143
+ # @return [Array<Symbol>]
1144
+ def self.required_fields
1145
+ %i[email_address messages].freeze
1146
+ end
1147
+
1148
+ def initialize(data = {})
1149
+ @email_address = data["email_address"]
1150
+ @messages = data["messages"]
1151
+ end
1152
+
1153
+ def to_h
1154
+ {
1155
+ "email_address" => @email_address,
1156
+ "messages" => @messages,
1157
+ }.reject { |k, v| v.nil? && !["email_address"].include?(k) }
1158
+ end
1159
+
1160
+ def to_json(*args)
1161
+ to_h.to_json(*args)
1162
+ end
1163
+ end
1164
+
1165
+ # ClientInvitationErrors
1166
+ class ClientInvitationErrors
1167
+ include TypeHelpers
1168
+ attr_accessor :errors
1169
+
1170
+ # @return [Array<Symbol>]
1171
+ def self.required_fields
1172
+ %i[errors].freeze
1173
+ end
1174
+
1175
+ def initialize(data = {})
1176
+ @errors = parse_array(data["errors"], "ClientInvitationError")
1177
+ end
1178
+
1179
+ def to_h
1180
+ {
1181
+ "errors" => @errors,
1182
+ }.compact
1183
+ end
1184
+
1185
+ def to_json(*args)
1186
+ to_h.to_json(*args)
1187
+ end
1188
+ end
1189
+
1138
1190
  # ClientReply
1139
1191
  class ClientReply
1140
1192
  include TypeHelpers
@@ -1374,6 +1426,37 @@ module Basecamp
1374
1426
  end
1375
1427
  end
1376
1428
 
1429
+ # CreateClientRequest
1430
+ class CreateClientRequest
1431
+ include TypeHelpers
1432
+ attr_accessor :email_address, :company_name, :name, :title
1433
+
1434
+ # @return [Array<Symbol>]
1435
+ def self.required_fields
1436
+ %i[email_address].freeze
1437
+ end
1438
+
1439
+ def initialize(data = {})
1440
+ @email_address = data["email_address"]
1441
+ @company_name = data["company_name"]
1442
+ @name = data["name"]
1443
+ @title = data["title"]
1444
+ end
1445
+
1446
+ def to_h
1447
+ {
1448
+ "email_address" => @email_address,
1449
+ "company_name" => @company_name,
1450
+ "name" => @name,
1451
+ "title" => @title,
1452
+ }.compact
1453
+ end
1454
+
1455
+ def to_json(*args)
1456
+ to_h.to_json(*args)
1457
+ end
1458
+ end
1459
+
1377
1460
  # CreatePersonRequest
1378
1461
  class CreatePersonRequest
1379
1462
  include TypeHelpers
@@ -3249,6 +3332,31 @@ module Basecamp
3249
3332
  end
3250
3333
  end
3251
3334
 
3335
+ # ProjectClientEnablement
3336
+ class ProjectClientEnablement
3337
+ include TypeHelpers
3338
+ attr_accessor :clients_enabled
3339
+
3340
+ # @return [Array<Symbol>]
3341
+ def self.required_fields
3342
+ %i[clients_enabled].freeze
3343
+ end
3344
+
3345
+ def initialize(data = {})
3346
+ @clients_enabled = parse_boolean(data["clients_enabled"])
3347
+ end
3348
+
3349
+ def to_h
3350
+ {
3351
+ "clients_enabled" => @clients_enabled,
3352
+ }.compact
3353
+ end
3354
+
3355
+ def to_json(*args)
3356
+ to_h.to_json(*args)
3357
+ end
3358
+ end
3359
+
3252
3360
  # ProjectConstruction
3253
3361
  class ProjectConstruction
3254
3362
  include TypeHelpers
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Basecamp
4
- VERSION = "0.16.0"
4
+ VERSION = "0.17.0"
5
5
  API_VERSION = "2026-09-02"
6
6
  end
data/lib/basecamp.rb CHANGED
@@ -245,7 +245,9 @@ module Basecamp
245
245
 
246
246
  data = JSON.parse(body)
247
247
  errors = data.is_a?(Hash) ? data["errors"] : nil
248
- if errors.is_a?(Hash)
248
+ if errors.is_a?(Array)
249
+ parse_row_errors(errors)
250
+ elsif errors.is_a?(Hash)
249
251
  field_errors = errors.each_with_object({}) do |(field, values), result|
250
252
  next unless values.is_a?(Array)
251
253
 
@@ -260,6 +262,35 @@ module Basecamp
260
262
  nil
261
263
  end
262
264
 
265
+ # Extracts a row-keyed errors list — the batch-invite rendering
266
+ # {"errors" => [{"email_address" => "...", "messages" => ["..."]}, ...]}
267
+ # (SPEC section 6 step 1b), one element per rejected row. Rows are keyed by
268
+ # their email_address when it is a non-empty string, else by an integer
269
+ # index, else by their position in the list; a repeated key appends.
270
+ # All-or-nothing: one element without a usable messages array means this is
271
+ # some other list, and the slot stays absent.
272
+ # @param rows [Array] the parsed "errors" array
273
+ # @return [Hash{String => Array<String>}, nil]
274
+ def self.parse_row_errors(rows)
275
+ return nil if rows.empty?
276
+
277
+ rows.each_with_index.each_with_object({}) do |(row, position), result|
278
+ return nil unless row.is_a?(Hash) && row["messages"].is_a?(Array)
279
+
280
+ messages = row["messages"].select { |message| message.is_a?(String) && !message.empty? }
281
+ return nil if messages.empty?
282
+
283
+ key = if row["email_address"].is_a?(String) && !row["email_address"].empty?
284
+ row["email_address"]
285
+ elsif row["index"].is_a?(Integer)
286
+ row["index"].to_s
287
+ else
288
+ position.to_s
289
+ end
290
+ (result[key] ||= []).concat(messages)
291
+ end
292
+ end
293
+
263
294
  # Extracts an unwrapped field map — the `render json: @webhook.errors`
264
295
  # rendering, where the whole body is {"field" => ["msg", ...]}. The gate is
265
296
  # all-or-nothing by design (SPEC section 6 step 2): with no "errors" key to
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: basecamp-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.0
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Basecamp
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-03 00:00:00.000000000 Z
11
+ date: 2026-09-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday