attio-ruby 0.1.0 → 0.1.1
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/CHANGELOG.md +5 -0
- data/README.md +57 -12
- data/examples/app_specific_typed_record.md +1613 -0
- data/examples/deals.rb +112 -0
- data/examples/oauth_flow.rb +26 -49
- data/examples/typed_records_example.rb +10 -7
- data/lib/attio/internal/record.rb +17 -8
- data/lib/attio/resources/company.rb +26 -24
- data/lib/attio/resources/deal.rb +288 -0
- data/lib/attio/resources/meta.rb +43 -12
- data/lib/attio/resources/object.rb +24 -4
- data/lib/attio/resources/person.rb +22 -18
- data/lib/attio/resources/typed_record.rb +49 -6
- data/lib/attio/resources/workspace_member.rb +17 -4
- data/lib/attio/version.rb +1 -1
- data/lib/attio.rb +1 -0
- metadata +4 -2
- data/attio-ruby.gemspec +0 -61
data/examples/deals.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "attio"
|
6
|
+
|
7
|
+
# This example demonstrates working with Deals in Attio
|
8
|
+
|
9
|
+
# Setup
|
10
|
+
Attio.api_key = ENV["ATTIO_API_KEY"] || raise("Please set ATTIO_API_KEY")
|
11
|
+
owner_email = ENV["ATTIO_TEST_USER_EMAIL"] || "sales@example.com"
|
12
|
+
|
13
|
+
puts "=== Creating Deals ==="
|
14
|
+
|
15
|
+
# Create a basic deal
|
16
|
+
deal = Attio::Deal.create(
|
17
|
+
name: "Enterprise Software Deal",
|
18
|
+
value: 50000,
|
19
|
+
stage: "In Progress",
|
20
|
+
owner: owner_email
|
21
|
+
)
|
22
|
+
|
23
|
+
puts "Created deal: #{deal.name}"
|
24
|
+
puts "Value: $#{deal.value["currency_value"]}"
|
25
|
+
puts "Stage: #{deal.stage["status"]["title"]}"
|
26
|
+
|
27
|
+
# Create a deal with associations
|
28
|
+
partnership_deal = Attio::Deal.create(
|
29
|
+
name: "Partnership Deal",
|
30
|
+
value: 100000,
|
31
|
+
stage: "Lead",
|
32
|
+
owner: owner_email,
|
33
|
+
associated_people: ["partner@example.com", "contact@partner.com"],
|
34
|
+
associated_company: ["partner.com"]
|
35
|
+
)
|
36
|
+
|
37
|
+
puts "\nCreated partnership deal with associations"
|
38
|
+
|
39
|
+
puts "\n=== Searching Deals ==="
|
40
|
+
|
41
|
+
# Find high-value deals
|
42
|
+
big_deals = Attio::Deal.find_by_value_range(min: 75000)
|
43
|
+
puts "Found #{big_deals.count} deals worth $75k+"
|
44
|
+
|
45
|
+
# Find deals in a value range
|
46
|
+
mid_deals = Attio::Deal.find_by_value_range(min: 25000, max: 75000)
|
47
|
+
puts "Found #{mid_deals.count} deals worth $25k-$75k"
|
48
|
+
|
49
|
+
# Find deals by stage
|
50
|
+
in_progress = Attio::Deal.find_by(stage: "In Progress")
|
51
|
+
puts "Found #{in_progress ? 1 : 0} deals in progress"
|
52
|
+
|
53
|
+
puts "\n=== Updating Deals ==="
|
54
|
+
|
55
|
+
# Update deal stage
|
56
|
+
deal.update_stage("Won 🎉")
|
57
|
+
puts "Updated deal stage to: Won 🎉"
|
58
|
+
|
59
|
+
# Update deal value
|
60
|
+
deal.update_value(75000)
|
61
|
+
puts "Updated deal value to: $75,000"
|
62
|
+
|
63
|
+
# Check deal status
|
64
|
+
puts "\nDeal status checks:"
|
65
|
+
puts "Is open? #{deal.open?}"
|
66
|
+
puts "Is won? #{deal.won?}"
|
67
|
+
puts "Is lost? #{deal.lost?}"
|
68
|
+
|
69
|
+
puts "\n=== Deal Pipeline Analysis ==="
|
70
|
+
|
71
|
+
# List all deals and analyze by stage
|
72
|
+
all_deals = Attio::Deal.list(params: { limit: 50 })
|
73
|
+
stage_counts = Hash.new(0)
|
74
|
+
|
75
|
+
all_deals.each do |d|
|
76
|
+
stage_title = d.stage.dig("status", "title") if d.stage.is_a?(Hash)
|
77
|
+
stage_counts[stage_title || "Unknown"] += 1
|
78
|
+
end
|
79
|
+
|
80
|
+
puts "Pipeline breakdown:"
|
81
|
+
stage_counts.each do |stage, count|
|
82
|
+
puts " #{stage}: #{count} deals"
|
83
|
+
end
|
84
|
+
|
85
|
+
# Calculate total pipeline value
|
86
|
+
total_value = all_deals.sum do |d|
|
87
|
+
d.value.is_a?(Hash) ? (d.value["currency_value"] || 0) : 0
|
88
|
+
end
|
89
|
+
|
90
|
+
puts "\nTotal pipeline value: $#{total_value}"
|
91
|
+
|
92
|
+
puts "\n=== Working with Deal Relationships ==="
|
93
|
+
|
94
|
+
# Get associated company (if available)
|
95
|
+
if deal.company
|
96
|
+
begin
|
97
|
+
company = deal.company_record
|
98
|
+
puts "Deal is associated with: #{company.name}" if company
|
99
|
+
rescue => e
|
100
|
+
puts "Could not fetch company: #{e.message}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
puts "\n=== Cleanup ==="
|
105
|
+
|
106
|
+
# Clean up test deals
|
107
|
+
[deal, partnership_deal].each do |d|
|
108
|
+
d.destroy if d.persisted?
|
109
|
+
puts "Deleted deal: #{d.name}"
|
110
|
+
end
|
111
|
+
|
112
|
+
puts "\nDone!"
|
data/examples/oauth_flow.rb
CHANGED
@@ -223,22 +223,22 @@ class OAuthApp < Sinatra::Base
|
|
223
223
|
|
224
224
|
# Test 3: List People Records
|
225
225
|
begin
|
226
|
-
people = Attio::
|
226
|
+
people = Attio::Person.list(limit: 3)
|
227
227
|
results[:people] = people.count
|
228
|
-
puts "✓
|
228
|
+
puts "✓ Person.list successful (#{people.count} records)"
|
229
229
|
rescue => e
|
230
230
|
errors[:people] = e.message
|
231
|
-
puts "✗
|
231
|
+
puts "✗ Person.list failed: #{e.message}"
|
232
232
|
end
|
233
233
|
|
234
234
|
# Test 4: List Companies Records
|
235
235
|
begin
|
236
|
-
companies = Attio::
|
236
|
+
companies = Attio::Company.list(limit: 3)
|
237
237
|
results[:companies] = companies.count
|
238
|
-
puts "✓
|
238
|
+
puts "✓ Company.list successful (#{companies.count} records)"
|
239
239
|
rescue => e
|
240
240
|
errors[:companies] = e.message
|
241
|
-
puts "✗
|
241
|
+
puts "✗ Company.list failed: #{e.message}"
|
242
242
|
end
|
243
243
|
|
244
244
|
# Test 5: List Lists
|
@@ -399,23 +399,17 @@ class OAuthApp < Sinatra::Base
|
|
399
399
|
# Test 3: Create a Person
|
400
400
|
person = nil
|
401
401
|
begin
|
402
|
-
person = Attio::
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
first_name: "OAuth",
|
407
|
-
last_name: "Test #{Time.now.to_i}",
|
408
|
-
full_name: "OAuth Test #{Time.now.to_i}"
|
409
|
-
}],
|
410
|
-
email_addresses: ["oauth-#{Time.now.to_i}@example.com"]
|
411
|
-
}
|
402
|
+
person = Attio::Person.create(
|
403
|
+
first_name: "OAuth",
|
404
|
+
last_name: "Test #{Time.now.to_i}",
|
405
|
+
email: "oauth-#{Time.now.to_i}@example.com"
|
412
406
|
)
|
413
407
|
created_resources << {type: "person", id: person.id["record_id"]}
|
414
408
|
results[:create_person] = "Created person ID: #{person.id["record_id"][0..10]}..."
|
415
|
-
puts "✓
|
409
|
+
puts "✓ Person.create successful"
|
416
410
|
rescue => e
|
417
411
|
errors[:create_person] = e.message
|
418
|
-
puts "✗
|
412
|
+
puts "✗ Person.create failed: #{e.message}"
|
419
413
|
end
|
420
414
|
|
421
415
|
# Test 4: Create a Note
|
@@ -425,15 +419,12 @@ class OAuthApp < Sinatra::Base
|
|
425
419
|
puts " person.id: #{person.id.inspect}"
|
426
420
|
puts " person.id['record_id']: #{person.id["record_id"].inspect}"
|
427
421
|
|
428
|
-
|
422
|
+
Attio::Note.create(
|
429
423
|
parent_object: "people",
|
430
424
|
parent_record_id: person.id["record_id"],
|
431
425
|
content: "Test note created via OAuth at #{Time.now}",
|
432
426
|
format: "plaintext"
|
433
|
-
|
434
|
-
puts " note_params: #{note_params.inspect}"
|
435
|
-
|
436
|
-
Attio::Note.create(note_params)
|
427
|
+
)
|
437
428
|
results[:create_note] = "Created note on person"
|
438
429
|
puts "✓ Note.create successful"
|
439
430
|
rescue => e
|
@@ -571,31 +562,23 @@ class OAuthApp < Sinatra::Base
|
|
571
562
|
# Test 12: Update Operations
|
572
563
|
if person
|
573
564
|
begin
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
data: {
|
578
|
-
values: {
|
579
|
-
name: [{
|
580
|
-
first_name: "OAuth",
|
581
|
-
last_name: "Test Updated #{Time.now.to_i}",
|
582
|
-
full_name: "OAuth Test Updated #{Time.now.to_i}"
|
583
|
-
}]
|
584
|
-
}
|
585
|
-
}
|
565
|
+
person.set_name(
|
566
|
+
first: "OAuth",
|
567
|
+
last: "Test Updated #{Time.now.to_i}"
|
586
568
|
)
|
569
|
+
person.save
|
587
570
|
results[:update_record] = "Updated person name"
|
588
|
-
puts "✓
|
571
|
+
puts "✓ Person.update successful"
|
589
572
|
rescue => e
|
590
573
|
errors[:update_record] = e.message
|
591
|
-
puts "✗
|
574
|
+
puts "✗ Person.update failed: #{e.message}"
|
592
575
|
end
|
593
576
|
end
|
594
577
|
|
595
578
|
# Test 13: Error Handling
|
596
579
|
begin
|
597
580
|
# Use a properly formatted UUID that doesn't exist
|
598
|
-
Attio::
|
581
|
+
Attio::Person.retrieve("00000000-0000-0000-0000-000000000000")
|
599
582
|
rescue Attio::NotFoundError => e
|
600
583
|
results[:error_handling] = "404 errors handled correctly"
|
601
584
|
puts "✓ Error handling working correctly"
|
@@ -620,7 +603,7 @@ class OAuthApp < Sinatra::Base
|
|
620
603
|
case resource[:type]
|
621
604
|
when "person"
|
622
605
|
# Need to retrieve the record first to call destroy on the instance
|
623
|
-
record = Attio::
|
606
|
+
record = Attio::Person.retrieve(resource[:id])
|
624
607
|
record.destroy
|
625
608
|
cleanup_count += 1
|
626
609
|
when "task"
|
@@ -729,16 +712,10 @@ class OAuthApp < Sinatra::Base
|
|
729
712
|
begin
|
730
713
|
# Create a test person first
|
731
714
|
timestamp = Time.now.to_i
|
732
|
-
person = Attio::
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
first_name: "Note",
|
737
|
-
last_name: "Test#{timestamp}",
|
738
|
-
full_name: "Note Test#{timestamp}"
|
739
|
-
}],
|
740
|
-
email_addresses: ["note-test-#{timestamp}@example.com"]
|
741
|
-
}
|
715
|
+
person = Attio::Person.create(
|
716
|
+
first_name: "Note",
|
717
|
+
last_name: "Test#{timestamp}",
|
718
|
+
email: "note-test-#{timestamp}@example.com"
|
742
719
|
)
|
743
720
|
|
744
721
|
result_html = "<h1>Note Creation Test</h1>"
|
@@ -73,10 +73,12 @@ puts <<~NEW
|
|
73
73
|
puts person.email # => "john@example.com"
|
74
74
|
puts person.phone # => "+12125551234"
|
75
75
|
|
76
|
-
# Searching is simpler
|
76
|
+
# Searching is simpler with Rails-style find_by
|
77
77
|
people = Attio::Person.search("john")
|
78
|
-
|
79
|
-
|
78
|
+
person = Attio::Person.find_by(email: "john@example.com")
|
79
|
+
person = Attio::Person.find_by(name: "John Doe")
|
80
|
+
# Can combine multiple conditions
|
81
|
+
person = Attio::Person.find_by(email: "john@example.com", job_title: "CEO")
|
80
82
|
|
81
83
|
# Creating a company (no more array wrapping for simple names!)
|
82
84
|
company = Attio::Company.create(
|
@@ -103,10 +105,11 @@ puts <<~NEW
|
|
103
105
|
# Find company's team members
|
104
106
|
team = company.team_members
|
105
107
|
|
106
|
-
# Find companies by various criteria
|
107
|
-
company = Attio::Company.
|
108
|
-
company = Attio::Company.
|
109
|
-
|
108
|
+
# Find companies by various criteria using Rails-style find_by
|
109
|
+
company = Attio::Company.find_by(domain: "acme.com")
|
110
|
+
company = Attio::Company.find_by(name: "Acme")
|
111
|
+
# Can combine multiple conditions
|
112
|
+
tech_giant = Attio::Company.find_by(domain: "tech.com", employee_count: "1000+")
|
110
113
|
NEW
|
111
114
|
|
112
115
|
# Working example (if API key is set)
|
@@ -42,7 +42,9 @@ module Attio
|
|
42
42
|
validate_object_identifier!(object)
|
43
43
|
|
44
44
|
# Extract query parameters from opts
|
45
|
-
|
45
|
+
# Handle both opts[:params] (from find_by) and direct opts (from other callers)
|
46
|
+
params = opts[:params] || opts
|
47
|
+
query_params = build_query_params(params)
|
46
48
|
|
47
49
|
response = execute_request(:POST, "#{resource_path}/#{object}/records/query", query_params, opts)
|
48
50
|
|
@@ -80,7 +82,7 @@ module Attio
|
|
80
82
|
|
81
83
|
# Ensure object info is included
|
82
84
|
record_data = response["data"] || {}
|
83
|
-
record_data[
|
85
|
+
record_data["object_api_slug"] ||= object if record_data.is_a?(Hash)
|
84
86
|
|
85
87
|
new(record_data, opts)
|
86
88
|
end
|
@@ -96,7 +98,7 @@ module Attio
|
|
96
98
|
response = execute_request(:GET, "#{resource_path}/#{object}/records/#{simple_record_id}", {}, opts)
|
97
99
|
|
98
100
|
record_data = response["data"] || {}
|
99
|
-
record_data[
|
101
|
+
record_data["object_api_slug"] ||= object if record_data.is_a?(Hash)
|
100
102
|
|
101
103
|
new(record_data, opts)
|
102
104
|
end
|
@@ -120,7 +122,7 @@ module Attio
|
|
120
122
|
response = execute_request(:PUT, "#{resource_path}/#{object}/records/#{simple_record_id}", request_params, opts)
|
121
123
|
|
122
124
|
record_data = response["data"] || {}
|
123
|
-
record_data[
|
125
|
+
record_data["object_api_slug"] ||= object if record_data.is_a?(Hash)
|
124
126
|
|
125
127
|
new(record_data, opts)
|
126
128
|
end
|
@@ -188,7 +190,7 @@ module Attio
|
|
188
190
|
|
189
191
|
# Attributes that should be sent as simple arrays of strings or simple values
|
190
192
|
SIMPLE_ARRAY_ATTRIBUTES = %w[email_addresses domains].freeze
|
191
|
-
SIMPLE_VALUE_ATTRIBUTES = %w[description linkedin job_title employee_count].freeze
|
193
|
+
SIMPLE_VALUE_ATTRIBUTES = %w[description linkedin job_title employee_count value stage status close_date probability owner].freeze
|
192
194
|
# Attributes that are arrays of objects and should be sent as-is
|
193
195
|
OBJECT_ARRAY_ATTRIBUTES = %w[phone_numbers primary_location company].freeze
|
194
196
|
|
@@ -338,9 +340,16 @@ module Attio
|
|
338
340
|
when Hash
|
339
341
|
if value_data.key?(:value) || value_data.key?("value")
|
340
342
|
value_data[:value] || value_data["value"]
|
341
|
-
elsif value_data.key?(:target_object) || value_data.key?("target_object")
|
342
|
-
|
343
|
-
|
343
|
+
elsif value_data.key?(:target_object) || value_data.key?("target_object") ||
|
344
|
+
value_data.key?(:referenced_actor_type) || value_data.key?("referenced_actor_type")
|
345
|
+
# Reference value - return the full reference object
|
346
|
+
value_data
|
347
|
+
elsif value_data.key?(:currency_value) || value_data.key?("currency_value")
|
348
|
+
# Currency value - return the full object to preserve currency info
|
349
|
+
value_data
|
350
|
+
elsif value_data.key?(:status) || value_data.key?("status")
|
351
|
+
# Status value - return the full object to preserve status info
|
352
|
+
value_data
|
344
353
|
else
|
345
354
|
value_data
|
346
355
|
end
|
@@ -141,7 +141,10 @@ module Attio
|
|
141
141
|
company_id = id.is_a?(Hash) ? id["record_id"] : id
|
142
142
|
Person.list(**opts.merge(params: {
|
143
143
|
filter: {
|
144
|
-
company: {
|
144
|
+
company: {
|
145
|
+
target_object: "companies",
|
146
|
+
target_record_id: company_id
|
147
|
+
}
|
145
148
|
}
|
146
149
|
}))
|
147
150
|
end
|
@@ -173,29 +176,6 @@ module Attio
|
|
173
176
|
super(values: values, **opts)
|
174
177
|
end
|
175
178
|
|
176
|
-
# Find a company by domain
|
177
|
-
# @param domain [String] Domain to search for
|
178
|
-
def find_by_domain(domain, **opts)
|
179
|
-
# Normalize domain
|
180
|
-
domain = domain.sub(/^https?:\/\//, "")
|
181
|
-
|
182
|
-
list(**opts.merge(
|
183
|
-
filter: {
|
184
|
-
domains: {
|
185
|
-
domain: {
|
186
|
-
"$eq": domain
|
187
|
-
}
|
188
|
-
}
|
189
|
-
}
|
190
|
-
)).first
|
191
|
-
end
|
192
|
-
|
193
|
-
# Find companies by name
|
194
|
-
# @param name [String] Name to search for
|
195
|
-
def find_by_name(name, **opts)
|
196
|
-
results = search(name, **opts)
|
197
|
-
results.first
|
198
|
-
end
|
199
179
|
|
200
180
|
# Find companies by employee count range
|
201
181
|
# @param min [Integer] Minimum employee count
|
@@ -216,6 +196,28 @@ module Attio
|
|
216
196
|
|
217
197
|
list(**opts.merge(params: {filter: filter}))
|
218
198
|
end
|
199
|
+
|
200
|
+
private
|
201
|
+
|
202
|
+
# Build filter for domain field
|
203
|
+
def filter_by_domain(value)
|
204
|
+
# Strip protocol if present
|
205
|
+
normalized_domain = value.sub(/^https?:\/\//, "")
|
206
|
+
{
|
207
|
+
domains: {
|
208
|
+
domain: {
|
209
|
+
"$eq": normalized_domain
|
210
|
+
}
|
211
|
+
}
|
212
|
+
}
|
213
|
+
end
|
214
|
+
|
215
|
+
# Build filter for name field
|
216
|
+
def filter_by_name(value)
|
217
|
+
{
|
218
|
+
name: {"$contains": value}
|
219
|
+
}
|
220
|
+
end
|
219
221
|
end
|
220
222
|
end
|
221
223
|
|