attio 0.3.0 → 0.5.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.
@@ -104,6 +104,59 @@ module Attio
104
104
  private def handle_delete_request(connection, path, params)
105
105
  params.empty? ? connection.delete(path) : connection.delete(path, params)
106
106
  end
107
+
108
+ # Paginate through all results for a given endpoint
109
+ # @param path [String] The API endpoint path
110
+ # @param params [Hash] Query parameters including filters
111
+ # @param page_size [Integer] Number of items per page (default: 50)
112
+ # @return [Enumerator] Yields each item from all pages
113
+ private def paginate(path, params = {}, page_size: 50)
114
+ Enumerator.new do |yielder|
115
+ offset = 0
116
+ loop do
117
+ page_params = params.merge(limit: page_size, offset: offset)
118
+ # Use POST for query endpoints, GET for others
119
+ method = path.end_with?("/query") ? :post : :get
120
+ response = request(method, path, page_params)
121
+
122
+ data = response["data"] || []
123
+ data.each { |item| yielder << item }
124
+
125
+ # Stop if we got fewer items than requested (last page)
126
+ break if data.size < page_size
127
+
128
+ offset += page_size
129
+ end
130
+ end
131
+ end
132
+
133
+ # Build query parameters with filtering and sorting support
134
+ # @param options [Hash] Options including filter, sort, limit, offset
135
+ # @return [Hash] Formatted query parameters
136
+ private def build_query_params(options = {})
137
+ params = {}
138
+
139
+ # Add filtering
140
+ add_filter_param(params, options[:filter]) if options[:filter]
141
+
142
+ # Add standard parameters
143
+ %i[sort limit offset].each do |key|
144
+ params[key] = options[key] if options[key]
145
+ end
146
+
147
+ # Add any other parameters
148
+ options.each do |key, value|
149
+ next if %i[filter sort limit offset].include?(key)
150
+
151
+ params[key] = value
152
+ end
153
+
154
+ params
155
+ end
156
+
157
+ private def add_filter_param(params, filter)
158
+ params[:filter] = filter.is_a?(String) ? filter : filter.to_json
159
+ end
107
160
  end
108
161
  end
109
162
  end
@@ -196,7 +196,7 @@ module Attio
196
196
  raise ArgumentError, "Records array is required for bulk #{operation}" if records.nil?
197
197
  raise ArgumentError, "Records must be an array for bulk #{operation}" unless records.is_a?(Array)
198
198
  raise ArgumentError, "Records array cannot be empty for bulk #{operation}" if records.empty?
199
- raise ArgumentError, "Too many records (max #{MAX_BATCH_SIZE * 10})" if records.size > MAX_BATCH_SIZE * 10
199
+ raise ArgumentError, "Too many records (max 1000)" if records.size > MAX_BATCH_SIZE * 10
200
200
 
201
201
  records.each_with_index do |record, index|
202
202
  raise ArgumentError, "Record at index #{index} must be a hash" unless record.is_a?(Hash)
@@ -44,10 +44,205 @@ module Attio
44
44
  request(:delete, "lists/#{list_id}/entries/#{entry_id}")
45
45
  end
46
46
 
47
+ # Create a new list.
48
+ #
49
+ # Creates a new list with the specified configuration. The list can be
50
+ # associated with a parent object type and configured with various options.
51
+ #
52
+ # @param data [Hash] The list configuration data
53
+ # @option data [String] :name The name of the list (required)
54
+ # @option data [String] :parent_object The parent object type for the list
55
+ # @option data [Boolean] :is_public Whether the list is publicly accessible
56
+ # @option data [String] :description Optional description for the list
57
+ #
58
+ # @return [Hash] The created list data
59
+ # @raise [ArgumentError] if data is invalid
60
+ #
61
+ # @example Create a simple list
62
+ # list = client.lists.create(
63
+ # data: {
64
+ # name: "VIP Customers",
65
+ # parent_object: "people",
66
+ # is_public: true
67
+ # }
68
+ # )
69
+ def create(data:)
70
+ validate_list_data!(data)
71
+ request(:post, "lists", { data: data })
72
+ end
73
+
74
+ # Update an existing list.
75
+ #
76
+ # Updates the configuration of an existing list. You can modify the name,
77
+ # description, visibility, and other list properties.
78
+ #
79
+ # @param id_or_slug [String] The list ID or slug
80
+ # @param data [Hash] The list configuration updates
81
+ # @option data [String] :name New name for the list
82
+ # @option data [Boolean] :is_public New visibility setting
83
+ # @option data [String] :description New description for the list
84
+ #
85
+ # @return [Hash] The updated list data
86
+ # @raise [ArgumentError] if id_or_slug or data is invalid
87
+ #
88
+ # @example Update list name and visibility
89
+ # list = client.lists.update(
90
+ # id_or_slug: "list_123",
91
+ # data: {
92
+ # name: "Premium Customers",
93
+ # is_public: false
94
+ # }
95
+ # )
96
+ def update(id_or_slug:, data:)
97
+ validate_id!(id_or_slug, "List")
98
+ validate_list_data!(data)
99
+ request(:patch, "lists/#{id_or_slug}", { data: data })
100
+ end
101
+
102
+ # Query list entries with advanced filtering and sorting.
103
+ #
104
+ # Provides advanced querying capabilities for list entries, supporting
105
+ # complex filters, sorting, and pagination. This is more powerful than
106
+ # the basic entries method as it supports structured queries.
107
+ #
108
+ # @param id_or_slug [String] The list ID or slug
109
+ # @param filter [Hash, String, nil] Optional filter criteria for querying entries
110
+ # @param sort [Hash, Array, String, nil] Optional sorting configuration
111
+ # @param limit [Integer, nil] Maximum number of entries to return
112
+ # @param offset [Integer, nil] Number of entries to skip for pagination
113
+ #
114
+ # @return [Hash] Query results with entries and pagination info
115
+ # @raise [ArgumentError] if id_or_slug is invalid
116
+ #
117
+ # @example Query with filters
118
+ # entries = client.lists.query_entries(
119
+ # id_or_slug: "vip_customers",
120
+ # filter: { created_at: { gte: "2023-01-01" } },
121
+ # sort: { created_at: "desc" },
122
+ # limit: 50
123
+ # )
124
+ #
125
+ # @example Query all entries without filters
126
+ # entries = client.lists.query_entries(id_or_slug: "list_123")
127
+ def query_entries(id_or_slug:, filter: nil, sort: nil, limit: nil, offset: nil)
128
+ validate_id!(id_or_slug, "List")
129
+
130
+ query_params = build_query_params({
131
+ filter: filter,
132
+ sort: sort,
133
+ limit: limit,
134
+ offset: offset,
135
+ })
136
+
137
+ request(:post, "lists/#{id_or_slug}/entries/query", query_params)
138
+ end
139
+
140
+ # Assert (upsert) a list entry.
141
+ #
142
+ # Creates or updates a list entry based on a matching attribute. This is an
143
+ # upsert operation that will create a new entry if no match is found, or update
144
+ # an existing entry if a match is found based on the specified matching attribute.
145
+ #
146
+ # Required scopes: list_entry:read-write, list_configuration:read
147
+ #
148
+ # @param id_or_slug [String] The list ID or slug
149
+ # @param matching_attribute [String] The attribute to match against for upsert
150
+ # @param data [Hash] The entry data to create or update
151
+ # @option data [String] :record_id The ID of the record to add to the list
152
+ # @option data [Hash] :values Optional field values for the entry
153
+ # @option data [String] :notes Optional notes for the entry
154
+ #
155
+ # @return [Hash] The created or updated entry data
156
+ # @raise [ArgumentError] if parameters are invalid
157
+ #
158
+ # @example Assert entry with record ID
159
+ # entry = client.lists.assert_entry(
160
+ # id_or_slug: "vip_customers",
161
+ # matching_attribute: "record_id",
162
+ # data: {
163
+ # record_id: "rec_123",
164
+ # values: { priority: "high" },
165
+ # notes: "Premium customer"
166
+ # }
167
+ # )
168
+ #
169
+ # @example Assert entry with custom matching
170
+ # entry = client.lists.assert_entry(
171
+ # id_or_slug: "lead_list",
172
+ # matching_attribute: "email",
173
+ # data: {
174
+ # email: "john@example.com",
175
+ # values: { status: "qualified" }
176
+ # }
177
+ # )
178
+ def assert_entry(id_or_slug:, matching_attribute:, data:)
179
+ validate_id!(id_or_slug, "List")
180
+ validate_required_string!(matching_attribute, "Matching attribute")
181
+ validate_list_entry_data!(data)
182
+
183
+ request_body = {
184
+ data: data,
185
+ matching_attribute: matching_attribute,
186
+ }
187
+
188
+ request(:put, "lists/#{id_or_slug}/entries", request_body)
189
+ end
190
+
191
+ # Update an existing list entry.
192
+ #
193
+ # Updates the data for an existing list entry. This method requires the
194
+ # entry ID and will update only the provided fields, leaving other fields
195
+ # unchanged.
196
+ #
197
+ # @param id_or_slug [String] The list ID or slug
198
+ # @param entry_id [String] The entry ID to update
199
+ # @param data [Hash] The entry data to update
200
+ # @option data [Hash] :values Field values to update
201
+ # @option data [String] :notes Updated notes for the entry
202
+ #
203
+ # @return [Hash] The updated entry data
204
+ # @raise [ArgumentError] if parameters are invalid
205
+ #
206
+ # @example Update entry values
207
+ # entry = client.lists.update_entry(
208
+ # id_or_slug: "vip_customers",
209
+ # entry_id: "ent_456",
210
+ # data: {
211
+ # values: { priority: "medium", last_contacted: "2024-01-15" },
212
+ # notes: "Updated contact information"
213
+ # }
214
+ # )
215
+ #
216
+ # @example Update only notes
217
+ # entry = client.lists.update_entry(
218
+ # id_or_slug: "lead_list",
219
+ # entry_id: "ent_789",
220
+ # data: { notes: "Follow up scheduled" }
221
+ # )
222
+ def update_entry(id_or_slug:, entry_id:, data:)
223
+ validate_id!(id_or_slug, "List")
224
+ validate_id!(entry_id, "Entry")
225
+ validate_list_entry_data!(data)
226
+
227
+ request_body = { data: data }
228
+
229
+ request(:patch, "lists/#{id_or_slug}/entries/#{entry_id}", request_body)
230
+ end
231
+
47
232
  private def validate_list_entry_data!(data)
48
233
  raise ArgumentError, "Data is required" if data.nil?
49
234
  raise ArgumentError, "Data must be a hash" unless data.is_a?(Hash)
50
235
  end
236
+
237
+ # Validates that the list data parameter is present and valid.
238
+ #
239
+ # @param data [Hash, nil] The data to validate
240
+ # @raise [ArgumentError] if data is nil or not a hash
241
+ # @api private
242
+ private def validate_list_data!(data)
243
+ raise ArgumentError, "Data is required" if data.nil?
244
+ raise ArgumentError, "Data must be a hash" unless data.is_a?(Hash)
245
+ end
51
246
  end
52
247
  end
53
248
  end
@@ -2,70 +2,131 @@
2
2
 
3
3
  module Attio
4
4
  module Resources
5
- # Meta resource for API metadata and identification
5
+ # Meta resource for getting information about the API token and workspace
6
6
  #
7
- # @example Identify the current API key
8
- # client.meta.identify
9
- # # => { "workspace" => { "id" => "...", "name" => "..." }, "user" => { ... } }
7
+ # The Meta resource provides a single endpoint (/v2/self) that returns
8
+ # information about the current access token, workspace, and permissions.
10
9
  #
11
- # @example Get API status
12
- # client.meta.status
13
- # # => { "status" => "operational", "version" => "v2" }
10
+ # @example Get token and workspace information
11
+ # meta_info = client.meta.identify
12
+ # puts "Workspace: #{meta_info['data']['workspace_name']}"
13
+ # puts "Token active: #{meta_info['data']['active']}"
14
+ #
15
+ # @example Check if token is active
16
+ # if client.meta.active?
17
+ # puts "Token is valid and active"
18
+ # end
19
+ #
20
+ # @example Get workspace details
21
+ # workspace = client.meta.workspace
22
+ # puts "Working in: #{workspace['name']} (#{workspace['id']})"
23
+ #
24
+ # @example Check permissions
25
+ # if client.meta.permission?("record_permission:read-write")
26
+ # # Can read and write records
27
+ # end
14
28
  class Meta < Base
15
- # Identify the current API key and get workspace/user information
29
+ # Get information about the current access token and workspace
30
+ #
31
+ # This is the primary method that calls the /v2/self endpoint.
32
+ # Returns full token metadata including workspace info and permissions.
16
33
  #
17
- # @return [Hash] Information about the authenticated workspace and user
34
+ # @return [Hash] Token and workspace information
35
+ # @example
36
+ # info = client.meta.identify
37
+ # # => { "data" => { "active" => true, "workspace_name" => "My Workspace", ... } }
18
38
  def identify
19
- request(:get, "meta/identify")
39
+ request(:get, "self")
20
40
  end
21
41
 
22
- # Get API status and version information
23
- #
24
- # @return [Hash] API status and version details
25
- def status
26
- request(:get, "meta/status")
27
- end
42
+ # Alias methods for convenience and clarity
43
+ alias self identify
44
+ alias get identify
28
45
 
29
- # Get rate limit information for the current API key
46
+ # Check if the current token is active
30
47
  #
31
- # @return [Hash] Current rate limit status
32
- def rate_limits
33
- request(:get, "meta/rate_limits")
48
+ # @return [Boolean] true if token is active, false otherwise
49
+ # @example
50
+ # if client.meta.active?
51
+ # # Proceed with API calls
52
+ # else
53
+ # # Token is inactive or invalid
54
+ # end
55
+ def active?
56
+ response = identify
57
+ response.dig("data", "active") || false
34
58
  end
35
59
 
36
- # Get workspace configuration and settings
60
+ # Get the workspace information
37
61
  #
38
- # @return [Hash] Workspace configuration details
39
- def workspace_config
40
- request(:get, "meta/workspace_config")
41
- end
42
-
43
- # Validate an API key without making changes
62
+ # Returns workspace details if token is active, nil otherwise.
44
63
  #
45
- # @return [Hash] Validation result with key permissions
46
- def validate_key
47
- request(:post, "meta/validate", {})
64
+ # @return [Hash, nil] Workspace details or nil if token inactive
65
+ # @example
66
+ # workspace = client.meta.workspace
67
+ # # => { "id" => "uuid", "name" => "My Workspace", "slug" => "my-workspace", "logo_url" => nil }
68
+ def workspace
69
+ response = identify
70
+ return nil unless response.dig("data", "active")
71
+
72
+ {
73
+ "id" => response.dig("data", "workspace_id"),
74
+ "name" => response.dig("data", "workspace_name"),
75
+ "slug" => response.dig("data", "workspace_slug"),
76
+ "logo_url" => response.dig("data", "workspace_logo_url"),
77
+ }
48
78
  end
49
79
 
50
- # Get available API endpoints and their documentation
80
+ # Get the token's permissions/scopes
81
+ #
82
+ # Parses the space-separated scope string into an array.
51
83
  #
52
- # @return [Hash] List of available endpoints with descriptions
53
- def endpoints
54
- request(:get, "meta/endpoints")
84
+ # @return [Array<String>] List of permission scopes
85
+ # @example
86
+ # permissions = client.meta.permissions
87
+ # # => ["comment:read-write", "list_configuration:read", "note:read-write"]
88
+ def permissions
89
+ response = identify
90
+ scope = response.dig("data", "scope") || ""
91
+ scope.split
55
92
  end
56
93
 
57
- # Get workspace usage statistics
94
+ # Check if token has a specific permission
58
95
  #
59
- # @return [Hash] Usage statistics including record counts, API calls, etc.
60
- def usage_stats
61
- request(:get, "meta/usage")
96
+ # @param permission [String] The permission to check (e.g., "comment:read-write")
97
+ # @return [Boolean] true if permission is granted
98
+ # @example
99
+ # if client.meta.permission?("record_permission:read-write")
100
+ # # Can manage records
101
+ # end
102
+ def permission?(permission)
103
+ permissions.include?(permission)
62
104
  end
63
105
 
64
- # Get feature flags and capabilities for the workspace
106
+ # Alias for backward compatibility
107
+ alias has_permission? permission?
108
+
109
+ # Get token expiration and metadata
110
+ #
111
+ # Returns detailed token information including expiration,
112
+ # issue time, client ID, and who authorized it.
65
113
  #
66
- # @return [Hash] Enabled features and capabilities
67
- def features
68
- request(:get, "meta/features")
114
+ # @return [Hash] Token metadata
115
+ # @example
116
+ # info = client.meta.token_info
117
+ # # => { "active" => true, "type" => "Bearer", "expires_at" => nil, ... }
118
+ def token_info
119
+ response = identify
120
+ return { "active" => false } unless response.dig("data", "active")
121
+
122
+ {
123
+ "active" => true,
124
+ "type" => response.dig("data", "token_type"),
125
+ "expires_at" => response.dig("data", "exp"),
126
+ "issued_at" => response.dig("data", "iat"),
127
+ "client_id" => response.dig("data", "client_id"),
128
+ "authorized_by" => response.dig("data", "authorized_by_workspace_member_id"),
129
+ }
69
130
  end
70
131
  end
71
132
  end
@@ -9,16 +9,120 @@ module Attio
9
9
  #
10
10
  # @example Listing all objects
11
11
  # client.objects.list
12
+ #
13
+ # @example Creating a custom object
14
+ # client.objects.create(
15
+ # api_slug: "projects",
16
+ # singular_noun: "Project",
17
+ # plural_noun: "Projects"
18
+ # )
19
+ #
20
+ # @example Updating a custom object
21
+ # client.objects.update(
22
+ # id_or_slug: "projects",
23
+ # plural_noun: "Active Projects"
24
+ # )
12
25
  class Objects < Base
26
+ # List all objects in the workspace
27
+ #
28
+ # @param params [Hash] Optional query parameters
29
+ # @return [Hash] List of objects
13
30
  def list(**params)
14
31
  request(:get, "objects", params)
15
32
  end
16
33
 
34
+ # Get a single object by ID or slug
35
+ #
36
+ # @param id_or_slug [String] The object ID or slug
37
+ # @return [Hash] The object details
17
38
  def get(id_or_slug:)
18
39
  validate_id_or_slug!(id_or_slug)
19
40
  request(:get, "objects/#{id_or_slug}")
20
41
  end
21
42
 
43
+ # Create a new custom object
44
+ #
45
+ # @param api_slug [String] Unique slug for the object (snake_case)
46
+ # @param singular_noun [String] Singular name of the object
47
+ # @param plural_noun [String] Plural name of the object
48
+ # @return [Hash] The created object with ID and timestamps
49
+ # @raise [ArgumentError] if required parameters are missing
50
+ # @example
51
+ # object = client.objects.create(
52
+ # api_slug: "projects",
53
+ # singular_noun: "Project",
54
+ # plural_noun: "Projects"
55
+ # )
56
+ def create(api_slug:, singular_noun:, plural_noun:)
57
+ validate_required_string!(api_slug, "API slug")
58
+ validate_required_string!(singular_noun, "Singular noun")
59
+ validate_required_string!(plural_noun, "Plural noun")
60
+
61
+ data = {
62
+ api_slug: api_slug,
63
+ singular_noun: singular_noun,
64
+ plural_noun: plural_noun,
65
+ }
66
+
67
+ request(:post, "objects", { data: data })
68
+ end
69
+
70
+ # Update an existing custom object
71
+ #
72
+ # @param id_or_slug [String] The object ID or slug to update
73
+ # @param api_slug [String, nil] New API slug (optional)
74
+ # @param singular_noun [String, nil] New singular noun (optional)
75
+ # @param plural_noun [String, nil] New plural noun (optional)
76
+ # @return [Hash] The updated object
77
+ # @raise [ArgumentError] if no update fields are provided
78
+ # @example Update just the plural noun
79
+ # client.objects.update(
80
+ # id_or_slug: "projects",
81
+ # plural_noun: "Active Projects"
82
+ # )
83
+ # @example Update multiple fields
84
+ # client.objects.update(
85
+ # id_or_slug: "old_slug",
86
+ # api_slug: "new_slug",
87
+ # singular_noun: "New Name",
88
+ # plural_noun: "New Names"
89
+ # )
90
+ def update(id_or_slug:, api_slug: nil, singular_noun: nil, plural_noun: nil)
91
+ validate_id_or_slug!(id_or_slug)
92
+
93
+ data = {}
94
+ data[:api_slug] = api_slug if api_slug
95
+ data[:singular_noun] = singular_noun if singular_noun
96
+ data[:plural_noun] = plural_noun if plural_noun
97
+
98
+ raise ArgumentError, "At least one field to update is required" if data.empty?
99
+
100
+ request(:patch, "objects/#{id_or_slug}", { data: data })
101
+ end
102
+
103
+ # Delete a custom object
104
+ #
105
+ # NOTE: The Attio API v2.0.0 does not currently support deleting custom objects.
106
+ # To delete a custom object, please visit your Attio settings at:
107
+ # Settings > Data Model > Objects
108
+ #
109
+ # @param id_or_slug [String] The object ID or slug to delete
110
+ # @raise [NotImplementedError] Always raised as the API doesn't support this operation
111
+ # @example
112
+ # client.objects.delete(id_or_slug: "projects")
113
+ # # => NotImplementedError: The Attio API does not currently support deleting custom objects.
114
+ # # Please delete objects through the Attio UI at: Settings > Data Model > Objects
115
+ def delete(id_or_slug:)
116
+ validate_id_or_slug!(id_or_slug)
117
+ raise NotImplementedError,
118
+ "The Attio API does not currently support deleting custom objects. " \
119
+ "Please delete objects through the Attio UI at: Settings > Data Model > Objects"
120
+ end
121
+
122
+ # Alias for delete method for consistency with other resources
123
+ # NOTE: See delete method for API limitations
124
+ alias destroy delete
125
+
22
126
  private def validate_id_or_slug!(id_or_slug)
23
127
  raise ArgumentError, "Object ID or slug is required" if id_or_slug.nil? || id_or_slug.to_s.strip.empty?
24
128
  end
@@ -44,9 +44,36 @@ module Attio
44
44
  # filters: { name: { contains: 'John' } },
45
45
  # limit: 50
46
46
  # )
47
- def list(object:, **params)
47
+ def list(object:, filter: nil, sort: nil, limit: nil, offset: nil, **params)
48
48
  validate_required_string!(object, "Object type")
49
- request(:post, "objects/#{object}/records/query", params)
49
+
50
+ # Build query parameters with filtering and sorting support
51
+ query_params = build_query_params({
52
+ filter: filter,
53
+ sort: sort,
54
+ limit: limit,
55
+ offset: offset,
56
+ **params,
57
+ })
58
+
59
+ request(:post, "objects/#{object}/records/query", query_params)
60
+ end
61
+
62
+ # List all records with automatic pagination
63
+ # @param object [String] The object type to query
64
+ # @param filter [Hash] Filtering criteria
65
+ # @param sort [String] Sorting option
66
+ # @param page_size [Integer] Number of records per page
67
+ # @return [Enumerator] Enumerator that yields each record
68
+ def list_all(object:, filter: nil, sort: nil, page_size: 50)
69
+ validate_required_string!(object, "Object type")
70
+
71
+ query_params = build_query_params({
72
+ filter: filter,
73
+ sort: sort,
74
+ })
75
+
76
+ paginate("objects/#{object}/records/query", query_params, page_size: page_size)
50
77
  end
51
78
 
52
79
  # Retrieve a specific record by ID.
@@ -126,6 +153,74 @@ module Attio
126
153
  request(:delete, "objects/#{object}/records/#{id}")
127
154
  end
128
155
 
156
+ # Assert (upsert) a record based on a matching attribute.
157
+ #
158
+ # This method creates or updates a record based on a matching attribute,
159
+ # providing upsert functionality. If a record with the matching attribute
160
+ # value exists, it will be updated; otherwise, a new record will be created.
161
+ #
162
+ # @param object [String] The object type (e.g., 'people', 'companies')
163
+ # @param matching_attribute [String] The attribute to match against for upsert
164
+ # @param data [Hash] The record data to create or update
165
+ #
166
+ # @return [Hash] The created or updated record data
167
+ # @raise [ArgumentError] if object, matching_attribute, or data is invalid
168
+ #
169
+ # @example Assert a person by email
170
+ # record = client.records.assert(
171
+ # object: 'people',
172
+ # matching_attribute: 'email',
173
+ # data: {
174
+ # name: 'Jane Doe',
175
+ # email: 'jane@example.com',
176
+ # company: { target_object: 'companies', target_record_id: 'company123' }
177
+ # }
178
+ # )
179
+ def assert(object:, matching_attribute:, data:)
180
+ validate_required_string!(object, "Object type")
181
+ validate_required_string!(matching_attribute, "Matching attribute")
182
+ validate_record_data!(data)
183
+
184
+ request_body = {
185
+ data: data,
186
+ matching_attribute: matching_attribute,
187
+ }
188
+
189
+ request(:put, "objects/#{object}/records", request_body)
190
+ end
191
+
192
+ # Update a record using PUT (replace operation).
193
+ #
194
+ # This method performs a complete replacement of the record, unlike the
195
+ # regular update method which uses PATCH. For multiselect fields, this
196
+ # overwrites the values instead of appending to them.
197
+ #
198
+ # @param object [String] The object type (e.g., 'people', 'companies')
199
+ # @param id [String] The record ID to replace
200
+ # @param data [Hash] The complete record data to replace with
201
+ #
202
+ # @return [Hash] The updated record data
203
+ # @raise [ArgumentError] if object, id, or data is invalid
204
+ #
205
+ # @example Replace a person's data
206
+ # record = client.records.update_with_put(
207
+ # object: 'people',
208
+ # id: 'abc123',
209
+ # data: {
210
+ # name: 'Jane Smith',
211
+ # email: 'jane.smith@example.com',
212
+ # tags: ['customer', 'vip'] # This will replace all existing tags
213
+ # }
214
+ # )
215
+ def update_with_put(object:, id:, data:)
216
+ validate_required_string!(object, "Object type")
217
+ validate_id!(id, "Record")
218
+ validate_record_data!(data)
219
+
220
+ request_body = { data: data }
221
+ request(:put, "objects/#{object}/records/#{id}", request_body)
222
+ end
223
+
129
224
  # Validates that the data parameter is present and is a hash.
130
225
  #
131
226
  # @param data [Hash, nil] The data to validate