attio-ruby 0.1.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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +164 -0
  4. data/.simplecov +17 -0
  5. data/.yardopts +9 -0
  6. data/CHANGELOG.md +27 -0
  7. data/CONTRIBUTING.md +333 -0
  8. data/INTEGRATION_TEST_STATUS.md +149 -0
  9. data/LICENSE +21 -0
  10. data/README.md +638 -0
  11. data/Rakefile +8 -0
  12. data/attio-ruby.gemspec +61 -0
  13. data/docs/CODECOV_SETUP.md +34 -0
  14. data/examples/basic_usage.rb +149 -0
  15. data/examples/oauth_flow.rb +843 -0
  16. data/examples/oauth_flow_README.md +84 -0
  17. data/examples/typed_records_example.rb +167 -0
  18. data/examples/webhook_server.rb +463 -0
  19. data/lib/attio/api_resource.rb +539 -0
  20. data/lib/attio/builders/name_builder.rb +181 -0
  21. data/lib/attio/client.rb +160 -0
  22. data/lib/attio/errors.rb +126 -0
  23. data/lib/attio/internal/record.rb +359 -0
  24. data/lib/attio/oauth/client.rb +219 -0
  25. data/lib/attio/oauth/scope_validator.rb +162 -0
  26. data/lib/attio/oauth/token.rb +158 -0
  27. data/lib/attio/resources/attribute.rb +332 -0
  28. data/lib/attio/resources/comment.rb +114 -0
  29. data/lib/attio/resources/company.rb +224 -0
  30. data/lib/attio/resources/entry.rb +208 -0
  31. data/lib/attio/resources/list.rb +196 -0
  32. data/lib/attio/resources/meta.rb +113 -0
  33. data/lib/attio/resources/note.rb +213 -0
  34. data/lib/attio/resources/object.rb +66 -0
  35. data/lib/attio/resources/person.rb +294 -0
  36. data/lib/attio/resources/task.rb +147 -0
  37. data/lib/attio/resources/thread.rb +99 -0
  38. data/lib/attio/resources/typed_record.rb +98 -0
  39. data/lib/attio/resources/webhook.rb +224 -0
  40. data/lib/attio/resources/workspace_member.rb +136 -0
  41. data/lib/attio/util/configuration.rb +166 -0
  42. data/lib/attio/util/id_extractor.rb +115 -0
  43. data/lib/attio/util/webhook_signature.rb +175 -0
  44. data/lib/attio/version.rb +6 -0
  45. data/lib/attio/webhook/event.rb +114 -0
  46. data/lib/attio/webhook/signature_verifier.rb +73 -0
  47. data/lib/attio.rb +123 -0
  48. metadata +402 -0
@@ -0,0 +1,332 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../api_resource"
4
+
5
+ module Attio
6
+ # Represents a custom attribute on an Attio object
7
+ # Attributes define the schema for data stored on records
8
+ class Attribute < APIResource
9
+ api_operations :list, :retrieve, :create, :update
10
+
11
+ # Attribute types
12
+ TYPES = %w[
13
+ text
14
+ number
15
+ checkbox
16
+ date
17
+ timestamp
18
+ rating
19
+ currency
20
+ status
21
+ select
22
+ multiselect
23
+ email
24
+ phone
25
+ url
26
+ user
27
+ record_reference
28
+ location
29
+ ].freeze
30
+
31
+ # Type configurations
32
+ TYPE_CONFIGS = {
33
+ "text" => {supports_default: true, supports_required: true},
34
+ "number" => {supports_default: true, supports_required: true, supports_unique: true},
35
+ "checkbox" => {supports_default: true},
36
+ "date" => {supports_default: true, supports_required: true},
37
+ "timestamp" => {supports_default: true, supports_required: true},
38
+ "rating" => {supports_default: true, max_value: 5},
39
+ "currency" => {supports_default: true, supports_required: true},
40
+ "status" => {requires_options: true},
41
+ "select" => {requires_options: true, supports_default: true},
42
+ "multiselect" => {requires_options: true},
43
+ "email" => {supports_unique: true, supports_required: true},
44
+ "phone" => {supports_required: true},
45
+ "url" => {supports_required: true},
46
+ "user" => {supports_required: true},
47
+ "record_reference" => {requires_target_object: true, supports_required: true},
48
+ "location" => {supports_required: true}
49
+ }.freeze
50
+
51
+ # API endpoint path for attributes
52
+ # @return [String] The API path
53
+ def self.resource_path
54
+ "attributes"
55
+ end
56
+
57
+ # Define known attributes with proper accessors
58
+ attr_attio :name, :description, :is_required, :is_unique,
59
+ :is_default_value_enabled, :default_value, :options
60
+
61
+ # Read-only attributes
62
+ attr_reader :api_slug, :type, :attio_object_id, :object_api_slug,
63
+ :parent_object_id, :created_by_actor, :is_archived, :archived_at,
64
+ :title
65
+
66
+ def initialize(attributes = {}, opts = {})
67
+ super
68
+ normalized_attrs = normalize_attributes(attributes)
69
+ @api_slug = normalized_attrs[:api_slug]
70
+ @type = normalized_attrs[:type]
71
+ @attio_object_id = normalized_attrs[:object_id]
72
+ @object_api_slug = normalized_attrs[:object_api_slug]
73
+ @parent_object_id = normalized_attrs[:parent_object_id]
74
+ @created_by_actor = normalized_attrs[:created_by_actor]
75
+ @is_archived = normalized_attrs[:is_archived] || false
76
+ @archived_at = parse_timestamp(normalized_attrs[:archived_at])
77
+ @title = normalized_attrs[:title]
78
+ end
79
+
80
+ # Archive this attribute
81
+ def archive(**opts)
82
+ raise InvalidRequestError, "Cannot archive an attribute without an ID" unless persisted?
83
+
84
+ response = self.class.send(:execute_request, :POST, "#{resource_path}/archive", {}, opts)
85
+ update_from(response[:data] || response)
86
+ self
87
+ end
88
+
89
+ # Unarchive this attribute
90
+ def unarchive(**opts)
91
+ raise InvalidRequestError, "Cannot unarchive an attribute without an ID" unless persisted?
92
+
93
+ response = self.class.send(:execute_request, :POST, "#{resource_path}/unarchive", {}, opts)
94
+ update_from(response[:data] || response)
95
+ self
96
+ end
97
+
98
+ def archived?
99
+ @is_archived == true
100
+ end
101
+
102
+ def required?
103
+ is_required == true
104
+ end
105
+
106
+ def unique?
107
+ is_unique == true
108
+ end
109
+
110
+ def has_default?
111
+ is_default_value_enabled == true
112
+ end
113
+
114
+ # Convert attribute to hash representation
115
+ # @return [Hash] Attribute data as a hash
116
+ def to_h
117
+ super.merge(
118
+ api_slug: api_slug,
119
+ name: name,
120
+ description: description,
121
+ type: type,
122
+ is_required: is_required,
123
+ is_unique: is_unique,
124
+ is_default_value_enabled: is_default_value_enabled,
125
+ default_value: default_value,
126
+ options: options,
127
+ object_id: attio_object_id,
128
+ object_api_slug: object_api_slug,
129
+ parent_object_id: parent_object_id,
130
+ created_by_actor: created_by_actor,
131
+ is_archived: is_archived,
132
+ archived_at: archived_at&.iso8601
133
+ ).compact
134
+ end
135
+
136
+ def resource_path
137
+ raise InvalidRequestError, "Cannot generate path without an ID" unless persisted?
138
+ attribute_id = Util::IdExtractor.extract_for_resource(id, :attribute)
139
+ "#{self.class.resource_path}/#{attribute_id}"
140
+ end
141
+
142
+ # Override save to handle nested ID
143
+ def save(**)
144
+ raise InvalidRequestError, "Cannot save an attribute without an ID" unless persisted?
145
+ return self unless changed?
146
+
147
+ # Pass the full ID (including object context) to update method
148
+ self.class.update(id, changed_attributes, **)
149
+ end
150
+
151
+ class << self
152
+ # Override retrieve to handle object-scoped attributes
153
+ def retrieve(id, **opts)
154
+ # Extract simple ID if it's a nested hash
155
+ attribute_id = Util::IdExtractor.extract_for_resource(id, :attribute)
156
+ validate_id!(attribute_id)
157
+
158
+ # For attributes, we need the object context - check if it's in the nested ID
159
+ if id.is_a?(Hash) && id["object_id"]
160
+ object_id = id["object_id"]
161
+ response = execute_request(:GET, "objects/#{object_id}/attributes/#{attribute_id}", {}, opts)
162
+ else
163
+ # Fall back to regular attributes endpoint
164
+ response = execute_request(:GET, "#{resource_path}/#{attribute_id}", {}, opts)
165
+ end
166
+
167
+ new(response["data"] || response, opts)
168
+ end
169
+
170
+ # Override update to handle object-scoped attributes
171
+ def update(id, params = {}, **opts)
172
+ # Extract simple ID if it's a nested hash
173
+ attribute_id = Util::IdExtractor.extract_for_resource(id, :attribute)
174
+ validate_id!(attribute_id)
175
+
176
+ # For attributes, we need the object context
177
+ if id.is_a?(Hash) && id["object_id"]
178
+ object_id = id["object_id"]
179
+ prepared_params = prepare_params_for_update(params)
180
+ response = execute_request(:PATCH, "objects/#{object_id}/attributes/#{attribute_id}", prepared_params, opts)
181
+ else
182
+ # Fall back to regular attributes endpoint
183
+ prepared_params = prepare_params_for_update(params)
184
+ response = execute_request(:PATCH, "#{resource_path}/#{attribute_id}", prepared_params, opts)
185
+ end
186
+
187
+ new(response["data"] || response, opts)
188
+ end
189
+
190
+ # Override create to handle validation and object parameter
191
+ def prepare_params_for_create(params)
192
+ validate_type!(params[:type])
193
+ validate_type_config!(params)
194
+
195
+ # Generate api_slug from name if not provided
196
+ api_slug = params[:api_slug] || params[:name].downcase.gsub(/[^a-z0-9]+/, "_")
197
+
198
+ {
199
+ data: {
200
+ title: params[:name] || params[:title],
201
+ api_slug: api_slug,
202
+ type: params[:type],
203
+ description: params[:description],
204
+ is_required: params[:is_required] || false,
205
+ is_unique: params[:is_unique] || false,
206
+ is_multiselect: params[:is_multiselect] || false,
207
+ default_value: params[:default_value],
208
+ config: params[:config] || {}
209
+ }.compact
210
+ }
211
+ end
212
+
213
+ # Override update params preparation
214
+ def prepare_params_for_update(params)
215
+ # Only certain fields can be updated
216
+ updateable_fields = %i[
217
+ name
218
+ title
219
+ description
220
+ is_required
221
+ is_unique
222
+ default_value
223
+ options
224
+ ]
225
+
226
+ update_params = params.slice(*updateable_fields)
227
+ update_params[:options] = prepare_options(update_params[:options]) if update_params[:options]
228
+
229
+ # Wrap in data for API
230
+ {
231
+ data: update_params
232
+ }
233
+ end
234
+
235
+ # Override list to handle object-specific attributes
236
+ def list(params = {}, **opts)
237
+ if params[:object]
238
+ object = params.delete(:object)
239
+ validate_object_identifier!(object)
240
+
241
+ response = execute_request(:GET, "objects/#{object}/attributes", params, opts)
242
+ APIResource::ListObject.new(response, self, params.merge(object: object), opts)
243
+ else
244
+ raise ArgumentError, "Attributes must be listed for a specific object. Use Attribute.for_object(object_slug) or pass object: parameter"
245
+ end
246
+ end
247
+
248
+ # Override create to handle object-specific attributes
249
+ def create(params = {}, **opts)
250
+ object = params[:object]
251
+ validate_object_identifier!(object)
252
+
253
+ prepared_params = prepare_params_for_create(params)
254
+ response = execute_request(:POST, "objects/#{object}/attributes", prepared_params, opts)
255
+ new(response["data"] || response, opts)
256
+ end
257
+
258
+ # List attributes for a specific object
259
+ def for_object(object, params = {}, **)
260
+ list(params.merge(object: object), **)
261
+ end
262
+
263
+ private
264
+
265
+ def validate_object_identifier!(object)
266
+ raise ArgumentError, "Object identifier is required" if object.nil? || object.to_s.empty?
267
+ end
268
+
269
+ def validate_type!(type)
270
+ raise ArgumentError, "Attribute type is required" if type.nil? || type.to_s.empty?
271
+ unless TYPES.include?(type.to_s)
272
+ raise ArgumentError, "Invalid attribute type: #{type}. Valid types: #{TYPES.join(", ")}"
273
+ end
274
+ end
275
+
276
+ def validate_type_config!(params)
277
+ type = params[:type]
278
+ config = TYPE_CONFIGS[type.to_s]
279
+ return unless config
280
+
281
+ # Check required options
282
+ if config[:requires_options]
283
+ options = params[:options]
284
+ if options.nil? || (options.is_a?(Array) && options.empty?)
285
+ raise ArgumentError, "Attribute type '#{type}' requires options"
286
+ end
287
+ end
288
+
289
+ # Check required target object
290
+ if config[:requires_target_object]
291
+ target = params[:target_object]
292
+ if target.nil? || target.to_s.empty?
293
+ raise ArgumentError, "Attribute type '#{type}' requires target_object"
294
+ end
295
+ end
296
+
297
+ # Validate unsupported features
298
+ if params[:is_unique] && !config[:supports_unique]
299
+ raise ArgumentError, "Attribute type '#{type}' does not support unique constraint"
300
+ end
301
+
302
+ if params[:is_required] && !config[:supports_required]
303
+ raise ArgumentError, "Attribute type '#{type}' does not support required constraint"
304
+ end
305
+
306
+ if params[:is_default_value_enabled] && !config[:supports_default]
307
+ raise ArgumentError, "Attribute type '#{type}' does not support default values"
308
+ end
309
+ end
310
+
311
+ def prepare_options(options)
312
+ return nil unless options
313
+
314
+ case options
315
+ when Array
316
+ options.map do |opt|
317
+ case opt
318
+ when String
319
+ {title: opt}
320
+ when Hash
321
+ opt
322
+ else
323
+ {title: opt.to_s}
324
+ end
325
+ end
326
+ else
327
+ options
328
+ end
329
+ end
330
+ end
331
+ end
332
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../api_resource"
4
+
5
+ module Attio
6
+ # Represents a comment in an Attio thread
7
+ # Comments are immutable once created
8
+ class Comment < APIResource
9
+ # Comments only support create, retrieve, and delete (no list or update)
10
+ api_operations :retrieve, :delete
11
+
12
+ # API endpoint path for comments
13
+ # @return [String] The API path
14
+ def self.resource_path
15
+ "comments"
16
+ end
17
+
18
+ # Custom create implementation
19
+ def self.create(content: nil, format: "plaintext", author: nil, thread_id: nil, created_at: nil, **opts)
20
+ raise ArgumentError, "Content is required" if content.nil? || content.to_s.empty?
21
+ raise ArgumentError, "Thread ID is required" if thread_id.nil? || thread_id.to_s.empty?
22
+ raise ArgumentError, "Author is required" if author.nil?
23
+
24
+ request_params = {
25
+ data: {
26
+ format: format,
27
+ content: content,
28
+ author: author,
29
+ thread_id: thread_id
30
+ }
31
+ }
32
+
33
+ # Only add created_at if provided
34
+ request_params[:data][:created_at] = created_at if created_at
35
+
36
+ response = execute_request(:POST, resource_path, request_params, opts)
37
+ new(response["data"] || response, opts)
38
+ end
39
+
40
+ # Define attribute accessors
41
+ attr_attio :content_plaintext, :thread_id, :author, :record, :entry, :resolved_by
42
+
43
+ # Parse resolved_at as Time
44
+ def resolved_at
45
+ value = @attributes[:resolved_at]
46
+ return nil if value.nil?
47
+
48
+ case value
49
+ when Time
50
+ value
51
+ when String
52
+ Time.parse(value)
53
+ else
54
+ value
55
+ end
56
+ end
57
+
58
+ # Comments are immutable
59
+ def immutable?
60
+ true
61
+ end
62
+
63
+ # Override save to raise error since comments are immutable
64
+ def save(**opts)
65
+ raise InvalidRequestError, "Comments are immutable and cannot be updated"
66
+ end
67
+
68
+ # Override destroy to use the correct comment ID
69
+ def destroy(**opts)
70
+ raise InvalidRequestError, "Cannot destroy a comment without an ID" unless persisted?
71
+
72
+ comment_id = extract_comment_id
73
+ self.class.send(:execute_request, :DELETE, "#{self.class.resource_path}/#{comment_id}", {}, opts)
74
+ @attributes.clear
75
+ @changed_attributes.clear
76
+ @id = nil
77
+ true
78
+ end
79
+
80
+ private
81
+
82
+ def extract_comment_id
83
+ case id
84
+ when Hash
85
+ id[:comment_id] || id["comment_id"]
86
+ else
87
+ id
88
+ end
89
+ end
90
+
91
+ def resource_path
92
+ comment_id = extract_comment_id
93
+ "#{self.class.resource_path}/#{comment_id}"
94
+ end
95
+
96
+ def to_h
97
+ {
98
+ id: id,
99
+ thread_id: thread_id,
100
+ content_plaintext: content_plaintext,
101
+ entry: entry,
102
+ record: record,
103
+ resolved_at: resolved_at&.iso8601,
104
+ resolved_by: resolved_by,
105
+ created_at: created_at&.iso8601,
106
+ author: author
107
+ }.compact
108
+ end
109
+
110
+ def inspect
111
+ "#<#{self.class.name}:#{object_id} id=#{id.inspect} thread=#{thread_id} content=#{content_plaintext&.truncate(30).inspect}>"
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "typed_record"
4
+
5
+ module Attio
6
+ # Represents a company record in Attio
7
+ # Provides convenient methods for working with companies and their attributes
8
+ class Company < TypedRecord
9
+ object_type "companies"
10
+
11
+ # Set the company name (much simpler than person names!)
12
+ # @param name [String] The company name
13
+ def name=(name)
14
+ self[:name] = name
15
+ end
16
+
17
+ # Get the company name
18
+ # @return [String, nil] The company name or nil if not set
19
+ def name
20
+ self[:name]
21
+ end
22
+
23
+ # Add a domain
24
+ # @param domain [String] The domain to add (e.g., "example.com")
25
+ def add_domain(domain)
26
+ domains = self[:domains] || []
27
+ # Ensure it's an array
28
+ domains = [domains] unless domains.is_a?(Array)
29
+
30
+ # Normalize domain (remove protocol if present)
31
+ domain = domain.sub(/^https?:\/\//, "")
32
+
33
+ # Check if domain already exists
34
+ exists = domains.any? { |d|
35
+ d.is_a?(Hash) ? (d["domain"] == domain || d[:domain] == domain) : d == domain
36
+ }
37
+
38
+ unless exists
39
+ # Extract just the domain strings if we have hashes
40
+ domain_strings = domains.filter_map { |d|
41
+ d.is_a?(Hash) ? (d["domain"] || d[:domain]) : d
42
+ }
43
+
44
+ # Add the new domain
45
+ domain_strings << domain
46
+
47
+ # Set as simple array of strings
48
+ self[:domains] = domain_strings
49
+ end
50
+ end
51
+
52
+ # Get the primary domain
53
+ # @return [String, nil] The primary domain or nil if not set
54
+ def domain
55
+ domains = self[:domains]
56
+ return nil unless domains
57
+
58
+ extract_primary_value(domains, "domain")
59
+ end
60
+
61
+ # Get all domains
62
+ # @return [Array<String>] Array of domain strings
63
+ def domains_list
64
+ domains = self[:domains]
65
+ return [] unless domains
66
+
67
+ case domains
68
+ when Array
69
+ domains.filter_map { |d| extract_field_value(d, "domain") }
70
+ else
71
+ [domain].compact
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ # Extract primary value from various data structures
78
+ # @param value [Array, Hash, Object] The value to extract from
79
+ # @param field [String] The field name for hash extraction
80
+ # @return [String, nil] The extracted value
81
+ def extract_primary_value(value, field)
82
+ case value
83
+ when Array
84
+ return nil if value.empty?
85
+ extract_field_value(value.first, field)
86
+ when Hash
87
+ value[field] || value[field.to_sym]
88
+ else
89
+ value.to_s
90
+ end
91
+ end
92
+
93
+ # Extract a value from a hash or convert to string
94
+ # @param item [Hash, Object] The item to extract from
95
+ # @param field [String] The field name for hash extraction
96
+ # @return [String] The extracted value
97
+ def extract_field_value(item, field)
98
+ case item
99
+ when Hash
100
+ item[field] || item[field.to_sym]
101
+ else
102
+ item.to_s
103
+ end
104
+ end
105
+
106
+ public
107
+
108
+ # Set the company description
109
+ # @param description [String] The company description
110
+ def description=(desc)
111
+ self[:description] = desc
112
+ end
113
+
114
+ # Set the employee count
115
+ # @param count [Integer, String] The employee count or range (e.g., "10-50")
116
+ def employee_count=(count)
117
+ self[:employee_count] = count.to_s
118
+ end
119
+
120
+ # Add a team member (person) to this company
121
+ # @param person [Person, String] A Person instance or person ID
122
+ def add_team_member(person)
123
+ # This would typically be done from the Person side
124
+ # but we can provide a convenience method
125
+ if person.is_a?(Person)
126
+ person.company = self
127
+ person.save
128
+ elsif person.is_a?(String)
129
+ # If it's an ID, we need to fetch and update the person
130
+ retrieved_person = Person.retrieve(person)
131
+ retrieved_person.company = self
132
+ retrieved_person.save
133
+ else
134
+ raise ArgumentError, "Team member must be a Person instance or ID string"
135
+ end
136
+ end
137
+
138
+ # Get all people associated with this company
139
+ # @return [Attio::ListObject] List of people
140
+ def team_members(**opts)
141
+ company_id = id.is_a?(Hash) ? id["record_id"] : id
142
+ Person.list(**opts.merge(params: {
143
+ filter: {
144
+ company: {"$references": company_id}
145
+ }
146
+ }))
147
+ end
148
+
149
+ class << self
150
+ # Create a company with a simplified interface
151
+ # @param attributes [Hash] Company attributes
152
+ # @option attributes [String] :name Company name (required)
153
+ # @option attributes [String, Array<String>] :domain Domain(s)
154
+ # @option attributes [String] :description Company description
155
+ # @option attributes [String, Integer] :employee_count Employee count
156
+ # @option attributes [Hash] :values Raw values hash (for advanced use)
157
+ def create(name:, domain: nil, domains: nil, description: nil,
158
+ employee_count: nil, values: {}, **opts)
159
+ # Name is required and simple for companies
160
+ values[:name] = name
161
+
162
+ # Handle domains
163
+ if domain || domains
164
+ domain_list = []
165
+ domain_list << domain if domain
166
+ domain_list += Array(domains) if domains
167
+ values[:domains] = domain_list.uniq unless domain_list.empty?
168
+ end
169
+
170
+ values[:description] = description if description
171
+ values[:employee_count] = employee_count.to_s if employee_count
172
+
173
+ super(values: values, **opts)
174
+ end
175
+
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
+
200
+ # Find companies by employee count range
201
+ # @param min [Integer] Minimum employee count
202
+ # @param max [Integer] Maximum employee count (optional)
203
+ def find_by_size(min, max = nil, **opts)
204
+ filter = if max
205
+ {
206
+ employee_count: {
207
+ "$gte": min.to_s,
208
+ "$lte": max.to_s
209
+ }
210
+ }
211
+ else
212
+ {
213
+ employee_count: {"$gte": min.to_s}
214
+ }
215
+ end
216
+
217
+ list(**opts.merge(params: {filter: filter}))
218
+ end
219
+ end
220
+ end
221
+
222
+ # Convenience alias
223
+ Companies = Company
224
+ end