attio-ruby 0.1.0 → 0.1.2
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 +18 -0
- data/README.md +90 -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 +358 -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 +21 -8
- data/lib/attio/util/configuration.rb +12 -2
- data/lib/attio/version.rb +1 -1
- data/lib/attio.rb +1 -0
- metadata +4 -1
@@ -0,0 +1,358 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "typed_record"
|
4
|
+
|
5
|
+
module Attio
|
6
|
+
# Represents a Deal record in Attio
|
7
|
+
#
|
8
|
+
# @example Create a deal
|
9
|
+
# deal = Attio::Deal.create(
|
10
|
+
# name: "Enterprise Deal",
|
11
|
+
# value: 50000,
|
12
|
+
# stage: "In Progress"
|
13
|
+
# )
|
14
|
+
#
|
15
|
+
# @example Find deals by status
|
16
|
+
# open_deals = Attio::Deal.find_by(status: "open")
|
17
|
+
# won_deals = Attio::Deal.find_by(status: "won")
|
18
|
+
#
|
19
|
+
# @example Find high-value deals
|
20
|
+
# big_deals = Attio::Deal.find_by_value_range(min: 100000)
|
21
|
+
#
|
22
|
+
# @example Update deal status
|
23
|
+
# deal.update_status("won")
|
24
|
+
#
|
25
|
+
class Deal < TypedRecord
|
26
|
+
object_type "deals"
|
27
|
+
|
28
|
+
class << self
|
29
|
+
# Create a deal with a simplified interface
|
30
|
+
# @param attributes [Hash] Deal attributes
|
31
|
+
# @option attributes [String] :name Deal name (recommended)
|
32
|
+
# @option attributes [Numeric] :value Deal value (recommended)
|
33
|
+
# @option attributes [String] :stage Deal stage (recommended) - defaults: "Lead", "In Progress", "Won 🎉", "Lost"
|
34
|
+
# @option attributes [String] :status Deal status (alias for stage)
|
35
|
+
# @option attributes [String] :owner Owner email or workspace member (recommended)
|
36
|
+
# @option attributes [Array<String>] :associated_people Email addresses of associated people
|
37
|
+
# @option attributes [Array<String>] :associated_company Domains of associated companies
|
38
|
+
# @option attributes [Hash] :values Raw values hash (for advanced use)
|
39
|
+
def create(name:, value: nil, stage: nil, status: nil, owner: nil,
|
40
|
+
associated_people: nil, associated_company: nil, values: {}, **opts)
|
41
|
+
# Name is required and simple
|
42
|
+
values[:name] = name if name && !values[:name]
|
43
|
+
|
44
|
+
# Add optional fields
|
45
|
+
values[:value] = value if value && !values[:value]
|
46
|
+
|
47
|
+
# Handle stage vs status - API uses "stage" but we support both
|
48
|
+
if (stage || status) && !values[:stage]
|
49
|
+
values[:stage] = stage || status
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
# Handle owner - can be email address or workspace member reference
|
54
|
+
if owner && !values[:owner]
|
55
|
+
values[:owner] = owner
|
56
|
+
end
|
57
|
+
|
58
|
+
# Handle associated people - convert email array to proper format
|
59
|
+
if associated_people && !values[:associated_people]
|
60
|
+
values[:associated_people] = associated_people.map do |email|
|
61
|
+
{
|
62
|
+
target_object: "people",
|
63
|
+
email_addresses: [
|
64
|
+
{ email_address: email }
|
65
|
+
]
|
66
|
+
}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Handle associated company - convert domain array to proper format
|
71
|
+
if associated_company && !values[:associated_company]
|
72
|
+
# associated_company can be array of domains or single domain
|
73
|
+
domains = associated_company.is_a?(Array) ? associated_company : [associated_company]
|
74
|
+
values[:associated_company] = {
|
75
|
+
target_object: "companies",
|
76
|
+
domains: domains.map { |domain| { domain: domain } }
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
super(values: values, **opts)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Find deals by stage names
|
84
|
+
# @param stage_names [Array<String>] Array of stage names to filter by
|
85
|
+
# @return [Attio::ListObject] List of matching deals
|
86
|
+
def in_stage(stage_names:, **opts)
|
87
|
+
# If only one stage, use simple equality
|
88
|
+
if stage_names.length == 1
|
89
|
+
filter = { stage: stage_names.first }
|
90
|
+
else
|
91
|
+
# Multiple stages need $or operator
|
92
|
+
filter = {
|
93
|
+
"$or": stage_names.map { |stage| { stage: stage } }
|
94
|
+
}
|
95
|
+
end
|
96
|
+
|
97
|
+
list(**opts.merge(params: {filter: filter}))
|
98
|
+
end
|
99
|
+
|
100
|
+
# Find won deals using configured statuses
|
101
|
+
# @return [Attio::ListObject] List of won deals
|
102
|
+
def won(**opts)
|
103
|
+
in_stage(stage_names: Attio.configuration.won_statuses, **opts)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Find lost deals using configured statuses
|
107
|
+
# @return [Attio::ListObject] List of lost deals
|
108
|
+
def lost(**opts)
|
109
|
+
in_stage(stage_names: Attio.configuration.lost_statuses, **opts)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Find open deals (Lead + In Progress) using configured statuses
|
113
|
+
# @return [Attio::ListObject] List of open deals
|
114
|
+
def open_deals(**opts)
|
115
|
+
all_open_statuses = Attio.configuration.open_statuses + Attio.configuration.in_progress_statuses
|
116
|
+
in_stage(stage_names: all_open_statuses, **opts)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Find deals within a value range
|
120
|
+
# @param min [Numeric] Minimum value (optional)
|
121
|
+
# @param max [Numeric] Maximum value (optional)
|
122
|
+
# @return [Attio::ListObject] List of matching deals
|
123
|
+
def find_by_value_range(min: nil, max: nil, **opts)
|
124
|
+
filters = []
|
125
|
+
filters << {value: {"$gte": min}} if min
|
126
|
+
filters << {value: {"$lte": max}} if max
|
127
|
+
|
128
|
+
filter = if filters.length == 1
|
129
|
+
filters.first
|
130
|
+
elsif filters.length > 1
|
131
|
+
{"$and": filters}
|
132
|
+
else
|
133
|
+
{}
|
134
|
+
end
|
135
|
+
|
136
|
+
list(**opts.merge(params: {filter: filter}))
|
137
|
+
end
|
138
|
+
|
139
|
+
# # Find deals closing soon (requires close_date attribute)
|
140
|
+
# # @param days [Integer] Number of days from today
|
141
|
+
# # @return [Attio::ListObject] List of deals closing soon
|
142
|
+
# def closing_soon(days: 30, **opts)
|
143
|
+
# today = Date.today
|
144
|
+
# end_date = today + days
|
145
|
+
#
|
146
|
+
# list(**opts.merge(params: {
|
147
|
+
# filter: {
|
148
|
+
# "$and": [
|
149
|
+
# {close_date: {"$gte": today.iso8601}},
|
150
|
+
# {close_date: {"$lte": end_date.iso8601}},
|
151
|
+
# {stage: {"$ne": "Won 🎉"}},
|
152
|
+
# {stage: {"$ne": "Lost"}}
|
153
|
+
# ]
|
154
|
+
# }
|
155
|
+
# }))
|
156
|
+
# end
|
157
|
+
|
158
|
+
# Find deals by owner
|
159
|
+
# @param owner_id [String] The workspace member ID
|
160
|
+
# @return [Attio::ListObject] List of deals owned by the member
|
161
|
+
def find_by_owner(owner_id, **opts)
|
162
|
+
list(**opts.merge(params: {
|
163
|
+
filter: {
|
164
|
+
owner: {
|
165
|
+
target_object: "workspace_members",
|
166
|
+
target_record_id: owner_id
|
167
|
+
}
|
168
|
+
}
|
169
|
+
}))
|
170
|
+
end
|
171
|
+
|
172
|
+
private
|
173
|
+
|
174
|
+
# Build filter for status field (maps to stage)
|
175
|
+
def filter_by_status(value)
|
176
|
+
{ stage: value }
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Get the deal name
|
181
|
+
# @return [String, nil] The deal name
|
182
|
+
def name
|
183
|
+
self[:name]
|
184
|
+
end
|
185
|
+
|
186
|
+
# Get the deal value
|
187
|
+
# @return [Numeric, nil] The deal value
|
188
|
+
def value
|
189
|
+
self[:value]
|
190
|
+
end
|
191
|
+
|
192
|
+
# Get the deal stage (API uses "stage" but we provide status for compatibility)
|
193
|
+
# @return [String, nil] The deal stage
|
194
|
+
def stage
|
195
|
+
self[:stage]
|
196
|
+
end
|
197
|
+
|
198
|
+
# Alias for stage (for compatibility)
|
199
|
+
# @return [String, nil] The deal stage
|
200
|
+
def status
|
201
|
+
self[:stage]
|
202
|
+
end
|
203
|
+
|
204
|
+
# # Get the close date (if attribute exists)
|
205
|
+
# # @return [String, nil] The close date
|
206
|
+
# def close_date
|
207
|
+
# self[:close_date]
|
208
|
+
# end
|
209
|
+
|
210
|
+
# # Get the probability (if attribute exists)
|
211
|
+
# # @return [Numeric, nil] The win probability
|
212
|
+
# def probability
|
213
|
+
# self[:probability]
|
214
|
+
# end
|
215
|
+
|
216
|
+
# Get the owner reference
|
217
|
+
# @return [Hash, nil] The owner reference
|
218
|
+
def owner
|
219
|
+
self[:owner]
|
220
|
+
end
|
221
|
+
|
222
|
+
# Get the company reference
|
223
|
+
# @return [Hash, nil] The company reference
|
224
|
+
def company
|
225
|
+
self[:company]
|
226
|
+
end
|
227
|
+
|
228
|
+
# Update the deal stage
|
229
|
+
# @param new_stage [String] The new stage
|
230
|
+
# @return [Attio::Deal] The updated deal
|
231
|
+
def update_stage(new_stage, **opts)
|
232
|
+
self.class.update(id, values: {stage: new_stage}, **opts)
|
233
|
+
end
|
234
|
+
|
235
|
+
# Alias for update_stage (for compatibility)
|
236
|
+
# @param new_status [String] The new status/stage
|
237
|
+
# @return [Attio::Deal] The updated deal
|
238
|
+
def update_status(new_status, **opts)
|
239
|
+
update_stage(new_status, **opts)
|
240
|
+
end
|
241
|
+
|
242
|
+
# # Update the deal probability (if attribute exists)
|
243
|
+
# # @param new_probability [Numeric] The new probability (0-100)
|
244
|
+
# # @return [Attio::Deal] The updated deal
|
245
|
+
# def update_probability(new_probability, **opts)
|
246
|
+
# self.class.update(id, values: {probability: new_probability}, **opts)
|
247
|
+
# end
|
248
|
+
|
249
|
+
# Update the deal value
|
250
|
+
# @param new_value [Numeric] The new value
|
251
|
+
# @return [Attio::Deal] The updated deal
|
252
|
+
def update_value(new_value, **opts)
|
253
|
+
self.class.update(id, values: {value: new_value}, **opts)
|
254
|
+
end
|
255
|
+
|
256
|
+
# Get the associated company record
|
257
|
+
# @return [Attio::Company, nil] The company record if associated
|
258
|
+
def company_record(**opts)
|
259
|
+
return nil unless company
|
260
|
+
|
261
|
+
company_id = company.is_a?(Hash) ? company["target_record_id"] : company
|
262
|
+
Company.retrieve(company_id, **opts) if company_id
|
263
|
+
end
|
264
|
+
|
265
|
+
# Get the owner workspace member record
|
266
|
+
# @return [Attio::WorkspaceMember, nil] The owner record if assigned
|
267
|
+
def owner_record(**opts)
|
268
|
+
return nil unless owner
|
269
|
+
|
270
|
+
owner_id = if owner.is_a?(Hash)
|
271
|
+
owner["referenced_actor_id"] || owner["target_record_id"]
|
272
|
+
else
|
273
|
+
owner
|
274
|
+
end
|
275
|
+
WorkspaceMember.retrieve(owner_id, **opts) if owner_id
|
276
|
+
end
|
277
|
+
|
278
|
+
# # Calculate expected revenue (value * probability / 100)
|
279
|
+
# # @return [Numeric, nil] The expected revenue
|
280
|
+
# def expected_revenue
|
281
|
+
# return nil unless value && probability
|
282
|
+
# (value * probability / 100.0).round(2)
|
283
|
+
# end
|
284
|
+
|
285
|
+
# Get the current status title
|
286
|
+
# @return [String, nil] The current status title
|
287
|
+
def current_status
|
288
|
+
return nil unless stage
|
289
|
+
|
290
|
+
if stage.is_a?(Hash)
|
291
|
+
stage.dig("status", "title")
|
292
|
+
else
|
293
|
+
stage
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
# Get the timestamp when the status changed
|
298
|
+
# @return [Time, nil] The timestamp when status changed
|
299
|
+
def status_changed_at
|
300
|
+
return nil unless stage
|
301
|
+
|
302
|
+
if stage.is_a?(Hash) && stage["active_from"]
|
303
|
+
Time.parse(stage["active_from"])
|
304
|
+
else
|
305
|
+
nil
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
# Check if the deal is open
|
310
|
+
# @return [Boolean] True if the deal is open
|
311
|
+
def open?
|
312
|
+
return false unless current_status
|
313
|
+
|
314
|
+
all_open_statuses = Attio.configuration.open_statuses + Attio.configuration.in_progress_statuses
|
315
|
+
all_open_statuses.include?(current_status)
|
316
|
+
end
|
317
|
+
|
318
|
+
# Check if the deal is won
|
319
|
+
# @return [Boolean] True if the deal is won
|
320
|
+
def won?
|
321
|
+
return false unless current_status
|
322
|
+
|
323
|
+
Attio.configuration.won_statuses.include?(current_status)
|
324
|
+
end
|
325
|
+
|
326
|
+
# Check if the deal is lost
|
327
|
+
# @return [Boolean] True if the deal is lost
|
328
|
+
def lost?
|
329
|
+
return false unless current_status
|
330
|
+
|
331
|
+
Attio.configuration.lost_statuses.include?(current_status)
|
332
|
+
end
|
333
|
+
|
334
|
+
# Get the timestamp when the deal was won
|
335
|
+
# @return [Time, nil] The timestamp when deal was won, or nil if not won
|
336
|
+
def won_at
|
337
|
+
won? ? status_changed_at : nil
|
338
|
+
end
|
339
|
+
|
340
|
+
# Get the timestamp when the deal was closed (won or lost)
|
341
|
+
# @return [Time, nil] The timestamp when deal was closed, or nil if still open
|
342
|
+
def closed_at
|
343
|
+
(won? || lost?) ? status_changed_at : nil
|
344
|
+
end
|
345
|
+
|
346
|
+
# # Check if the deal is overdue
|
347
|
+
# # @return [Boolean] True if close date has passed and deal is still open
|
348
|
+
# def overdue?
|
349
|
+
# return false unless close_date && open?
|
350
|
+
# Date.parse(close_date) < Date.today
|
351
|
+
# end
|
352
|
+
end
|
353
|
+
|
354
|
+
# Alias for Deal (plural form)
|
355
|
+
# @example
|
356
|
+
# Attio::Deals.create(name: "New Deal", value: 10000)
|
357
|
+
Deals = Deal
|
358
|
+
end
|
data/lib/attio/resources/meta.rb
CHANGED
@@ -23,47 +23,78 @@ module Attio
|
|
23
23
|
alias_method :current, :identify
|
24
24
|
end
|
25
25
|
|
26
|
-
#
|
27
|
-
|
26
|
+
# Build workspace object from flat attributes
|
27
|
+
def workspace
|
28
|
+
return nil unless self[:workspace_id]
|
29
|
+
|
30
|
+
{
|
31
|
+
id: self[:workspace_id],
|
32
|
+
name: self[:workspace_name],
|
33
|
+
slug: self[:workspace_slug],
|
34
|
+
logo_url: self[:workspace_logo_url]
|
35
|
+
}.compact
|
36
|
+
end
|
37
|
+
|
38
|
+
# Build token object from flat attributes
|
39
|
+
def token
|
40
|
+
return nil unless self[:client_id]
|
41
|
+
|
42
|
+
{
|
43
|
+
id: self[:client_id],
|
44
|
+
type: self[:token_type] || "Bearer",
|
45
|
+
scope: self[:scope]
|
46
|
+
}.compact
|
47
|
+
end
|
48
|
+
|
49
|
+
# Build actor object from flat attributes
|
50
|
+
def actor
|
51
|
+
return nil unless self[:authorized_by_workspace_member_id]
|
52
|
+
|
53
|
+
{
|
54
|
+
type: "workspace-member",
|
55
|
+
id: self[:authorized_by_workspace_member_id]
|
56
|
+
}
|
57
|
+
end
|
28
58
|
|
29
59
|
# Convenience methods for workspace info
|
30
60
|
def workspace_id
|
31
|
-
|
61
|
+
self[:workspace_id]
|
32
62
|
end
|
33
63
|
|
34
64
|
# Get the workspace name
|
35
65
|
# @return [String, nil] The workspace name
|
36
66
|
def workspace_name
|
37
|
-
|
67
|
+
self[:workspace_name]
|
38
68
|
end
|
39
69
|
|
40
70
|
# Get the workspace slug
|
41
71
|
# @return [String, nil] The workspace slug
|
42
72
|
def workspace_slug
|
43
|
-
|
73
|
+
self[:workspace_slug]
|
44
74
|
end
|
45
75
|
|
46
76
|
# Convenience methods for token info
|
47
77
|
def token_id
|
48
|
-
|
78
|
+
self[:client_id]
|
49
79
|
end
|
50
80
|
|
51
81
|
# Get the token name
|
52
|
-
# @return [String, nil] The token name
|
82
|
+
# @return [String, nil] The token name (not available in flat format)
|
53
83
|
def token_name
|
54
|
-
|
84
|
+
nil
|
55
85
|
end
|
56
86
|
|
57
87
|
# Get the token type
|
58
88
|
# @return [String, nil] The token type
|
59
89
|
def token_type
|
60
|
-
|
90
|
+
self[:token_type]
|
61
91
|
end
|
62
92
|
|
63
93
|
# Get the token's OAuth scopes
|
64
94
|
# @return [Array<String>] Array of scope strings
|
65
95
|
def scopes
|
66
|
-
|
96
|
+
return [] unless self[:scope]
|
97
|
+
self[:scope].split(" ")
|
67
98
|
end
|
68
99
|
|
69
100
|
# Check if token has a specific scope
|
@@ -96,8 +127,6 @@ module Attio
|
|
96
127
|
raise InvalidRequestError, "Meta information is read-only"
|
97
128
|
end
|
98
129
|
|
99
|
-
private
|
100
|
-
|
101
130
|
def to_h
|
102
131
|
{
|
103
132
|
workspace: workspace,
|
@@ -109,5 +138,7 @@ module Attio
|
|
109
138
|
def inspect
|
110
139
|
"#<#{self.class.name}:#{object_id} workspace=#{workspace_slug.inspect} token=#{token_name.inspect}>"
|
111
140
|
end
|
141
|
+
|
142
|
+
private
|
112
143
|
end
|
113
144
|
end
|
@@ -45,11 +45,31 @@ module Attio
|
|
45
45
|
Internal::Record.create(object: api_slug || id, values: values, **)
|
46
46
|
end
|
47
47
|
|
48
|
-
# Find by
|
48
|
+
# Find by attribute using Rails-style syntax
|
49
|
+
def self.find_by(**conditions)
|
50
|
+
# Extract any opts that aren't conditions
|
51
|
+
opts = {}
|
52
|
+
known_opts = [:api_key, :timeout, :idempotency_key]
|
53
|
+
known_opts.each do |opt|
|
54
|
+
opts[opt] = conditions.delete(opt) if conditions.key?(opt)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Currently only supports slug
|
58
|
+
if conditions.key?(:slug)
|
59
|
+
slug = conditions[:slug]
|
60
|
+
begin
|
61
|
+
retrieve(slug, **opts)
|
62
|
+
rescue NotFoundError
|
63
|
+
list(**opts).find { |obj| obj.api_slug == slug }
|
64
|
+
end
|
65
|
+
else
|
66
|
+
raise ArgumentError, "find_by only supports slug attribute for objects"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Find by API slug (deprecated - use find_by(slug: ...) instead)
|
49
71
|
def self.find_by_slug(slug, **opts)
|
50
|
-
|
51
|
-
rescue NotFoundError
|
52
|
-
list(**opts).find { |obj| obj.api_slug == slug }
|
72
|
+
find_by(slug: slug, **opts)
|
53
73
|
end
|
54
74
|
|
55
75
|
# Get standard objects
|
@@ -251,19 +251,6 @@ module Attio
|
|
251
251
|
super(values: values, **opts)
|
252
252
|
end
|
253
253
|
|
254
|
-
# Find people by email
|
255
|
-
# @param email [String] Email address to search for
|
256
|
-
def find_by_email(email, **opts)
|
257
|
-
list(**opts.merge(
|
258
|
-
filter: {
|
259
|
-
email_addresses: {
|
260
|
-
email_address: {
|
261
|
-
"$eq": email
|
262
|
-
}
|
263
|
-
}
|
264
|
-
}
|
265
|
-
)).first
|
266
|
-
end
|
267
254
|
|
268
255
|
# Search people by query
|
269
256
|
# @param query [String] Query to search for
|
@@ -280,11 +267,28 @@ module Attio
|
|
280
267
|
))
|
281
268
|
end
|
282
269
|
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
270
|
+
private
|
271
|
+
|
272
|
+
# Build filter for email field
|
273
|
+
def filter_by_email(value)
|
274
|
+
{
|
275
|
+
email_addresses: {
|
276
|
+
email_address: {
|
277
|
+
"$eq": value
|
278
|
+
}
|
279
|
+
}
|
280
|
+
}
|
281
|
+
end
|
282
|
+
|
283
|
+
# Build filter for name field (searches across first, last, and full name)
|
284
|
+
def filter_by_name(value)
|
285
|
+
{
|
286
|
+
"$or": [
|
287
|
+
{name: {first_name: {"$contains": value}}},
|
288
|
+
{name: {last_name: {"$contains": value}}},
|
289
|
+
{name: {full_name: {"$contains": value}}}
|
290
|
+
]
|
291
|
+
}
|
288
292
|
end
|
289
293
|
end
|
290
294
|
end
|
@@ -61,12 +61,55 @@ module Attio
|
|
61
61
|
end
|
62
62
|
|
63
63
|
# Find by a specific attribute value
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
}
|
64
|
+
# Supports Rails-style hash syntax: find_by(name: "Test")
|
65
|
+
def find_by(**conditions)
|
66
|
+
raise ArgumentError, "find_by requires at least one condition" if conditions.empty?
|
67
|
+
|
68
|
+
# Extract any opts that aren't conditions (like api_key)
|
69
|
+
opts = {}
|
70
|
+
known_opts = [:api_key, :timeout, :idempotency_key]
|
71
|
+
known_opts.each do |opt|
|
72
|
+
opts[opt] = conditions.delete(opt) if conditions.key?(opt)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Build filter from conditions
|
76
|
+
filters = []
|
77
|
+
search_query = nil
|
78
|
+
|
79
|
+
conditions.each do |field, value|
|
80
|
+
# Check if there's a special filter method for this field
|
81
|
+
filter_method = "filter_by_#{field}"
|
82
|
+
if respond_to?(filter_method, true) # true = include private methods
|
83
|
+
result = send(filter_method, value)
|
84
|
+
# Check if this should be a search instead of a filter
|
85
|
+
if result == :use_search
|
86
|
+
search_query = value
|
87
|
+
else
|
88
|
+
filters << result
|
89
|
+
end
|
90
|
+
else
|
91
|
+
# Use the field as-is
|
92
|
+
filters << {field => value}
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# If we have a search query, use search instead of filter
|
97
|
+
if search_query
|
98
|
+
search(search_query, **opts).first
|
99
|
+
else
|
100
|
+
# Combine multiple filters with $and if needed
|
101
|
+
final_filter = if filters.length == 1
|
102
|
+
filters.first
|
103
|
+
elsif filters.length > 1
|
104
|
+
{"$and": filters}
|
105
|
+
else
|
106
|
+
{}
|
107
|
+
end
|
108
|
+
|
109
|
+
list(**opts.merge(params: {
|
110
|
+
filter: final_filter
|
111
|
+
})).first
|
112
|
+
end
|
70
113
|
end
|
71
114
|
end
|
72
115
|
|
@@ -103,20 +103,33 @@ module Attio
|
|
103
103
|
end
|
104
104
|
alias_method :current, :me
|
105
105
|
|
106
|
-
# Find member by
|
107
|
-
def
|
108
|
-
|
109
|
-
|
106
|
+
# Find member by attribute using Rails-style syntax
|
107
|
+
def find_by(**conditions)
|
108
|
+
# Extract any opts that aren't conditions
|
109
|
+
opts = {}
|
110
|
+
known_opts = [:api_key, :timeout, :idempotency_key]
|
111
|
+
known_opts.each do |opt|
|
112
|
+
opts[opt] = conditions.delete(opt) if conditions.key?(opt)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Currently only supports email
|
116
|
+
if conditions.key?(:email)
|
117
|
+
email = conditions[:email]
|
118
|
+
list(**opts).find { |member| member.email_address == email } ||
|
119
|
+
raise(NotFoundError, "Workspace member with email '#{email}' not found")
|
120
|
+
else
|
121
|
+
raise ArgumentError, "find_by only supports email attribute for workspace members"
|
122
|
+
end
|
110
123
|
end
|
111
124
|
|
112
125
|
# List active members only
|
113
|
-
def active(**)
|
114
|
-
list(**).select(&:active?)
|
126
|
+
def active(**opts)
|
127
|
+
list(**opts).select(&:active?)
|
115
128
|
end
|
116
129
|
|
117
130
|
# List admin members only
|
118
|
-
def admins(**)
|
119
|
-
list(**).select(&:admin?)
|
131
|
+
def admins(**opts)
|
132
|
+
list(**opts).select(&:admin?)
|
120
133
|
end
|
121
134
|
|
122
135
|
# This resource doesn't support creation, updates, or deletion
|
@@ -22,6 +22,10 @@ module Attio
|
|
22
22
|
ca_bundle_path
|
23
23
|
verify_ssl_certs
|
24
24
|
use_faraday
|
25
|
+
won_statuses
|
26
|
+
lost_statuses
|
27
|
+
open_statuses
|
28
|
+
in_progress_statuses
|
25
29
|
].freeze
|
26
30
|
|
27
31
|
# All available configuration settings
|
@@ -38,7 +42,11 @@ module Attio
|
|
38
42
|
debug: false,
|
39
43
|
ca_bundle_path: nil,
|
40
44
|
verify_ssl_certs: true,
|
41
|
-
use_faraday: true
|
45
|
+
use_faraday: true,
|
46
|
+
won_statuses: ["Won 🎉"].freeze,
|
47
|
+
lost_statuses: ["Lost"].freeze,
|
48
|
+
open_statuses: ["Lead"].freeze,
|
49
|
+
in_progress_statuses: ["In Progress"].freeze
|
42
50
|
}.freeze
|
43
51
|
|
44
52
|
attr_reader(*ALL_SETTINGS)
|
@@ -157,7 +165,9 @@ module Attio
|
|
157
165
|
|
158
166
|
def reset_without_lock!
|
159
167
|
DEFAULT_SETTINGS.each do |key, value|
|
160
|
-
|
168
|
+
# For arrays, create a new copy to avoid frozen arrays
|
169
|
+
actual_value = value.is_a?(Array) ? value.dup : value
|
170
|
+
instance_variable_set("@#{key}", actual_value)
|
161
171
|
end
|
162
172
|
@api_key = nil
|
163
173
|
end
|
data/lib/attio/version.rb
CHANGED