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.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/CHANGELOG.md +83 -2
- data/CLAUDE.md +35 -4
- data/Gemfile.lock +1 -1
- data/META_IMPLEMENTATION_PLAN.md +205 -0
- data/README.md +361 -37
- data/lib/attio/circuit_breaker.rb +299 -0
- data/lib/attio/client.rb +11 -10
- data/lib/attio/connection_pool.rb +190 -35
- data/lib/attio/enhanced_client.rb +257 -0
- data/lib/attio/http_client.rb +54 -3
- data/lib/attio/observability.rb +424 -0
- data/lib/attio/resources/attributes.rb +244 -0
- data/lib/attio/resources/base.rb +53 -0
- data/lib/attio/resources/bulk.rb +1 -1
- data/lib/attio/resources/lists.rb +195 -0
- data/lib/attio/resources/meta.rb +103 -42
- data/lib/attio/resources/objects.rb +104 -0
- data/lib/attio/resources/records.rb +97 -2
- data/lib/attio/resources/workspaces.rb +11 -2
- data/lib/attio/version.rb +1 -1
- data/lib/attio/webhooks.rb +220 -0
- data/lib/attio.rb +9 -1
- metadata +6 -1
data/lib/attio/resources/base.rb
CHANGED
@@ -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
|
data/lib/attio/resources/bulk.rb
CHANGED
@@ -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
|
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
|
data/lib/attio/resources/meta.rb
CHANGED
@@ -2,70 +2,131 @@
|
|
2
2
|
|
3
3
|
module Attio
|
4
4
|
module Resources
|
5
|
-
# Meta resource for API
|
5
|
+
# Meta resource for getting information about the API token and workspace
|
6
6
|
#
|
7
|
-
#
|
8
|
-
#
|
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
|
12
|
-
# client.meta.
|
13
|
-
#
|
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
|
-
#
|
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]
|
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, "
|
39
|
+
request(:get, "self")
|
20
40
|
end
|
21
41
|
|
22
|
-
#
|
23
|
-
|
24
|
-
|
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
|
-
#
|
46
|
+
# Check if the current token is active
|
30
47
|
#
|
31
|
-
# @return [
|
32
|
-
|
33
|
-
|
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
|
60
|
+
# Get the workspace information
|
37
61
|
#
|
38
|
-
#
|
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]
|
46
|
-
|
47
|
-
|
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
|
80
|
+
# Get the token's permissions/scopes
|
81
|
+
#
|
82
|
+
# Parses the space-separated scope string into an array.
|
51
83
|
#
|
52
|
-
# @return [
|
53
|
-
|
54
|
-
|
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
|
-
#
|
94
|
+
# Check if token has a specific permission
|
58
95
|
#
|
59
|
-
# @
|
60
|
-
|
61
|
-
|
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
|
-
#
|
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]
|
67
|
-
|
68
|
-
|
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
|
-
|
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
|