ruby_hubspot_api 0.3.1 → 0.3.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 62b690c7e426369cd5979c4f6b5b0cec8b5a7620
4
- data.tar.gz: 26047bb4aa34d3a91bf6c1aff80b6e3775220f4f
3
+ metadata.gz: 1a2e9bcd6887d8ca6e6aadd20459732e94e76114
4
+ data.tar.gz: '02728d9515a0ef2b5d1eecd6b7b703c7ba9cddb1'
5
5
  SHA512:
6
- metadata.gz: a6512174b13fa2ef07740690864c79a6c3593c908c1fed37170ef35bf08ca7ae5ceb9a3e53fe12d53079eb4e4bb47a58801430103b96c9a7b78dfd67eaaabaa5
7
- data.tar.gz: 6ea719cb62b217fdbbdb2009d64215369608ba8d3423b59245472fb64fcd957174c5cc28a0cbcbd1f5eaf904260d19e1989da528df913292c139b5f4d893110e
6
+ metadata.gz: 054defa6af013acea911e6f671b4c4d0fb200c25bba22c2969f52cadefa7c5beb2d776989936fff62d573f173acdd85514d3ae1dc0a95d1d35958b5f7fab4f0b
7
+ data.tar.gz: 959a83f088e542877d37ed660b20a59c062c314c6ae9343bccbaf8eed6fd63e350566ce8591b982db6649b835c8c87257ca3dc726faeba7caf3da10a962246fc
data/.env.sample CHANGED
@@ -8,33 +8,25 @@ export HUBSPOT_ACCESS_TOKEN=your_private_app_access_token
8
8
  export HUBSPOT_CLIENT_SECRET=your_client_secret
9
9
  export HUBSPOT_PORTAL_ID=portal_id
10
10
 
11
+ export HUBSPOT_NO_AUTH_ACCESS_TOKEN=your_private_app_access_token_with_no_scopes
12
+
11
13
  # VCR configuration
12
14
  # record mode can be none, once, newepisodes or all (default none)
13
15
  # see https://andrewmcodes.gitbook.io/vcr/record_modes
14
- export VCR_RECORD_MODE=once
15
- # to allow new request set to true
16
- export VCR_ALLOW_REQUESTS=false
17
-
18
- # Used in specs....
19
- # The values of the following are the defaults if the env var is not set
20
16
 
21
- # Company id used to test find and update (should be real)
22
- export HUBSPOT_TEST_COMPANY_ID=1
17
+ # export VCR_RECORD_MODE=once # (default)
18
+ # export VCR_RECORD_MODE=all # always record http requests (overwrites)
23
19
 
24
- # Company name to test create company
25
- export HUBSPOT_TEST_COMPANY_NAME="Numenor Capital"
26
-
27
- # Company id to test deleting (hint: make a dummy one first!)
28
- export HUBSPOT_TEST_COMPANY_ID_DELETE=666
29
-
30
- # Contact if to test find and update
31
- export HUBSPOT_TEST_CONTACT_ID=1
20
+ # to allow new request set to true
21
+ export VCR_ALLOW_REQUESTS=false
32
22
 
33
- # domain name to test the search by email contains
34
- export HUBSPOT_SEARCH_TEST_DOMAIN=hubspot.com
23
+ # SPECS:
24
+ # the following can be used when running the specs
35
25
 
36
- # maximum number of records to fetch when testing lists
37
- export HUBSPOT_SEARCH_LIMIT=5
26
+ export HUBSPOT_LOG_LEVEL=FATAL # no output during testing (default)
27
+ # export HUBSPOT_LOG_LEVEL=WARN # will show exceptions and batch load wait message
28
+ # export HUBSPOT_LOG_LEVEL=INFO # adds api request descriptions
29
+ # export HUBSPOT_LOG_LEVEL=DEBUG # adds api request body and full response (lots of output!)
38
30
 
39
- # email address for search on email = <email>
40
- export HUBSPOT_TEST_CONTACT_EMAIL=test@hubspot.com
31
+ # How long to wait after a batch is created (for propagation)
32
+ export HUBSPOT_BATCH_WAIT=0
data/.rspec CHANGED
@@ -1,3 +1,4 @@
1
1
  --format documentation
2
2
  --color
3
3
  --require spec_helper
4
+ --order random
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ruby_hubspot_api (0.3.1)
4
+ ruby_hubspot_api (0.3.3)
5
5
  httparty (>= 0.1, < 1.0)
6
6
 
7
7
  GEM
@@ -40,12 +40,12 @@ GEM
40
40
  rspec-core (~> 3.13.0)
41
41
  rspec-expectations (~> 3.13.0)
42
42
  rspec-mocks (~> 3.13.0)
43
- rspec-core (3.13.1)
43
+ rspec-core (3.13.2)
44
44
  rspec-support (~> 3.13.0)
45
45
  rspec-expectations (3.13.3)
46
46
  diff-lcs (>= 1.2.0, < 2.0)
47
47
  rspec-support (~> 3.13.0)
48
- rspec-mocks (3.13.1)
48
+ rspec-mocks (3.13.2)
49
49
  diff-lcs (>= 1.2.0, < 2.0)
50
50
  rspec-support (~> 3.13.0)
51
51
  rspec-support (3.13.1)
@@ -62,8 +62,6 @@ GEM
62
62
 
63
63
  PLATFORMS
64
64
  ruby
65
- x86_64-darwin-18
66
- x86_64-linux
67
65
 
68
66
  DEPENDENCIES
69
67
  bundler (>= 2.0, < 2.4.0)
@@ -80,4 +78,4 @@ DEPENDENCIES
80
78
  webmock (>= 3.0)
81
79
 
82
80
  BUNDLED WITH
83
- 2.2.34
81
+ 2.0.2
data/README.md CHANGED
@@ -78,7 +78,7 @@ Hubspot::Company # crm > companies
78
78
  Hubspot::User # hubspot users (also referred to as 'owners')
79
79
  Hubspot::Owner # alias of Hubspot::User if you prefer to use it
80
80
  ```
81
-
81
+
82
82
  however you can [add custom objects of your own](#user-content-custom-resources) based on your own custom defined Objects in Hubspot
83
83
 
84
84
  ### Creating and Saving an Object
@@ -254,7 +254,7 @@ Example:
254
254
 
255
255
  ```ruby
256
256
  # Search for contacts with email containing "hubspot.com"
257
- contacts = Hubspot::Contact.search(query: { email_contains: 'hubspot.com' })
257
+ contacts = Hubspot::Contact.search({ email_contains: 'hubspot.com' }, properties: %w[firstname lastname email])
258
258
 
259
259
  puts "Searching for Hubspot staff in the contacts CRM"
260
260
  puts ""
@@ -264,17 +264,18 @@ contacts.each do |contact|
264
264
  end
265
265
 
266
266
  # Search for companies with number of employees greater than or equal to 100
267
- companies = Hubspot::Company.search(query: { number_of_employees_gte: 100 })
268
-
269
267
  puts "Searching for medium to large companies"
270
268
  puts ""
271
269
 
272
- companies.each do |company|
273
- puts " Found: #{company.name} (#{company.number_of_employees} employees)"
270
+ companies = Hubspot::Company.search(number_of_employees_gte: 100).all # returns a PagedCollection
271
+ companies.each_page do |page_of_companies|
272
+ page_of_companies.each do |company|
273
+ puts " Found: #{company.name} (#{company.number_of_employees} employees)"
274
+ end
274
275
  end
275
276
 
276
277
  # Search for contacts with email in a specific list (IN operator)
277
- contacts = Hubspot::Contact.search(query: { email_in: ['user1@example.com', 'user2@example.com'] })
278
+ contacts = Hubspot::Contact.search(email: ['user1@example.com', 'user2@example.com'])
278
279
 
279
280
  contacts.each do |contact|
280
281
  puts "Found: #{contact.email}"
@@ -288,7 +289,6 @@ end
288
289
  - **gte**: Greater than or equal to.
289
290
  - **lt**: Less than.
290
291
  - **lte**: Less than or equal to.
291
- - **IN**: Matches any of the values in an array.
292
292
 
293
293
  #### Searching for empty values (NOT_HAS_PROPERTY)
294
294
 
@@ -296,7 +296,7 @@ Any empty value in your search will be matched using the correect filter in Hubs
296
296
 
297
297
  ```ruby
298
298
  # Search for companies with no value for a given field
299
- companies = Hubspot::Company.search(query: { client_category: nil }, properties: %w[name number_of_employees])
299
+ companies = Hubspot::Company.search({ client_category: nil }, properties: %w[name number_of_employees])
300
300
  # Request body: {"filterGroups":[{"filters":[{"propertyName":"client_category","operator":"NOT_HAS_PROPERTY"}]}]
301
301
 
302
302
  puts "Searching for uncategorised customers"
@@ -310,6 +310,47 @@ companies.each do |company|
310
310
  end
311
311
  ```
312
312
 
313
+ #### Searching for any value in an array (IN)
314
+
315
+ If value is an array the operator will be set to 'IN'
316
+
317
+ ```ruby
318
+ # Search for companies with no value for a given field
319
+ hot_contacts = Hubspot::Contact.search(hs_lead_status: %w[IN_PROGRESS OPEN_DEAL])
320
+ # Request body: {"filterGroups":[{"filters":[{"propertyName":"hs_lead_status","operator":"IN","values":["IN_PROGRESS", "OPEN_DEAL"]}]}]
321
+
322
+ puts "Current best leads"
323
+ puts ""
324
+
325
+ hot_contacts.each do |contact|
326
+ puts "#{contact.name} .... "
327
+ end
328
+
329
+ ```
330
+
331
+ #### Chaining search conditions (a la Active Record)
332
+
333
+ You can use the alternative Rails-like interface to return a PagedCollection and can chain calls. Use `where(search_params)` to filter and `.select('firstname', 'lastname', 'email', 'custom_property')` to determine the properties returned by the API.
334
+
335
+ ```ruby
336
+ contact = Hubspot::Contact.where(hs_lead_status: [])
337
+ ```
338
+
339
+ #### Getting the total number of records
340
+
341
+ Having created a search collection you can retrieve the total number of matching records from Hubspot by calling .total on the collection. This will make a single request to the api to retrieve the number of matching records
342
+
343
+ ```ruby
344
+ contacts_collection = Hubspot::Contact.search(lead_status: 'cold')
345
+
346
+ if contacts_collection.total > 200
347
+ puts "Contacts will be processed overnight"
348
+ # schedule_processing_job
349
+ else
350
+ puts "Found #{contacts_collection.total} contacts. Processing...."
351
+ process_contacts(contacts_collection)
352
+ ```
353
+
313
354
  #### Specifying Properties in Search
314
355
 
315
356
  When performing a search, you can also specify which properties to return.
@@ -320,7 +361,7 @@ Example:
320
361
  ```ruby
321
362
  # Search for contacts with email containing "hubspot.com" and only return specific properties
322
363
  contacts = Hubspot::Contact.search(
323
- query: { email_contains: 'hubspot.com' },
364
+ { email_contains: 'hubspot.com' },
324
365
  properties: ['firstname', 'lastname', 'email', 'mobile', 'custom_property_1']
325
366
  )
326
367
 
@@ -383,7 +424,7 @@ companies = batch.resources
383
424
  ```
384
425
 
385
426
  Example of reading contacts by email and the helper method `batch_read`
386
- By using this method you can page through the results as needed or collect them
427
+ By using this method you can page through the results as needed or collect them
387
428
 
388
429
  ```ruby
389
430
  email_addresses = my_selected_contacts.collect(&:email).compact
@@ -400,7 +441,7 @@ batch.each_page do |contacts|
400
441
  end
401
442
  ```
402
443
 
403
- Finally there is another helper method `batch_read_all` on any Hubspot::Resource class (Hubspot::Contact, Hubspot::Company, Hubspot::User etc) which will read all of the resources and return a HubSpot::Batch (with all of the resources).
444
+ Finally there is another helper method `batch_read_all` on any Hubspot::Resource class (Hubspot::Contact, Hubspot::Company, Hubspot::User etc) which will read all of the resources and return a HubSpot::Batch (with all of the resources).
404
445
 
405
446
  You can then update the resources and call `update` on the batch.... see below
406
447
 
@@ -456,7 +497,7 @@ In this example, if a contact with the given email already exists in HubSpot, it
456
497
  To archive objects in bulk, you can use the `archive` method. This removes the objects from HubSpot.
457
498
 
458
499
  ```ruby
459
- contacts = Hubspot::Contact.search(query: { email_contains: 'hubspot.com' }).all
500
+ contacts = Hubspot::Contact.where(email_contains: 'hubspot.com').all
460
501
 
461
502
  batch = Hubspot::Batch.new(contacts)
462
503
  batch.archive
@@ -489,7 +530,7 @@ require 'ruby_hubspot_api' # if not required by bundler already...
489
530
 
490
531
  module Hubspot
491
532
  class Project < Resource
492
-
533
+
493
534
  # resource_name (part of the url in the api) will default
494
535
  # to a simple plural of the class name - in this case 'projects'
495
536
  # if the url for your custom object is different you can override it
@@ -500,7 +541,7 @@ module Hubspot
500
541
  end
501
542
  end
502
543
 
503
- projects = Hubspot::Projects.search(query: { status_in: ['upcoming', 'active', 'overrun'] }).all
544
+ projects = Hubspot::Projects.where(status: ['upcoming', 'active', 'overrun']).all
504
545
 
505
546
  ```
506
547
 
@@ -7,7 +7,9 @@ module Hubspot
7
7
  attr_accessor :response
8
8
 
9
9
  def initialize(response, message = nil)
10
- message = response.parsed_response['message'] if !message && response.respond_to?(:parsed_response)
10
+ if !message && response.respond_to?(:parsed_response)
11
+ message = response.parsed_response['message']
12
+ end
11
13
  message += "\n" if message
12
14
  me = super("#{message}Response body: #{response.body}",)
13
15
  me.response = response
@@ -20,6 +22,7 @@ module Hubspot
20
22
  class NotConfiguredError < StandardError; end
21
23
  class ArgumentError < StandardError; end
22
24
  class NothingToDoError < StandardError; end
25
+ class NotImplementedError < StandardError; end
23
26
 
24
27
  class << self
25
28
  def error_from_response(response)
data/lib/hubspot/form.rb CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  module Hubspot
4
4
  class Form < Resource
5
- METADATA_FIELDS = %w[createdAt updatedAt archived].freeze
6
-
7
5
  # :nocov:
8
6
  def inspect
9
7
  "#<#{self.class.name} " \
@@ -13,6 +11,8 @@ module Hubspot
13
11
  # :nocov:
14
12
 
15
13
  class << self
14
+ private
15
+
16
16
  def api_root
17
17
  '/marketing/v3'
18
18
  end
@@ -24,5 +24,9 @@ module Hubspot
24
24
  def extract_id(id)
25
25
  id
26
26
  end
27
+
28
+ def metadata_fields
29
+ %w[createdAt updatedAt archived].freeze
30
+ end
27
31
  end
28
32
  end
@@ -3,27 +3,38 @@
3
3
  require 'json'
4
4
  require_relative './api_client'
5
5
  require_relative './exceptions'
6
+ require_relative './resource_filter'
6
7
 
7
8
  module Hubspot
8
9
  # Enumerable class for handling paged data from the API
9
10
  class PagedCollection < ApiClient
10
11
  include Enumerable
12
+ include ResourceFilter::FilterGroupMethods
11
13
 
12
- MAX_LIMIT = 100 # HubSpot max items per page
14
+ MAX_LIMIT = 200 # HubSpot max items per page
13
15
 
14
- def initialize(url:, params: {}, resource_class: nil, method: :get)
16
+ # rubocop:disable Lint/MissingSuper
17
+ def initialize(url:, params: {}, resource_class: nil, method: :get, results_param: 'results')
15
18
  @url = url
16
19
  @params = params
17
20
  @resource_class = resource_class
18
21
  @method = method.to_sym
22
+ @results_param = results_param
23
+ end
24
+ # rubocop:enable Lint/MissingSuper
25
+
26
+ def total
27
+ @total ||= determine_total
19
28
  end
20
29
 
21
30
  def each_page
22
31
  offset = nil
23
32
  loop do
24
33
  response = fetch_page(offset)
34
+ @total = response['total'] if response['total']
25
35
  mapped_results = process_results(response)
26
36
  yield mapped_results unless mapped_results.empty?
37
+ sleep wait_between_pages
27
38
  offset = response.dig('paging', 'next', 'after')
28
39
  break unless offset
29
40
  end
@@ -38,10 +49,12 @@ module Hubspot
38
49
  end
39
50
 
40
51
  # Override Enumerable's first method so as not to have to call each (via all)
52
+ # rubocop:disable Metrics/MethodLength
41
53
  def first(limit = 1)
42
54
  resources = []
43
55
  remaining = limit
44
56
 
57
+ original_limit = @params.delete(:limit)
45
58
  # Modify @params directly to set the limit
46
59
  @params[:limit] = [remaining, MAX_LIMIT].min
47
60
 
@@ -52,8 +65,10 @@ module Hubspot
52
65
  break if remaining <= 0
53
66
  end
54
67
 
68
+ @params[:limit] = original_limit
55
69
  limit == 1 ? resources.first : resources.first(limit)
56
70
  end
71
+ # rubocop:enable Metrics/MethodLength
57
72
 
58
73
  def each(&block)
59
74
  each_page do |page|
@@ -61,8 +76,63 @@ module Hubspot
61
76
  end
62
77
  end
63
78
 
79
+ def where(filters = {})
80
+ dup.where!(filters)
81
+ end
82
+
83
+ def where!(filters = {})
84
+ # Build new filter groups from the provided filters
85
+ new_filter_groups = build_filter_groups(filters)
86
+
87
+ # Merge new filters into the existing params structure
88
+ if @params[:filterGroups]&.any?
89
+ @params[:filterGroups][0][:filters].concat(new_filter_groups[0][:filters])
90
+ else
91
+ @params[:filterGroups] = new_filter_groups
92
+ end
93
+
94
+ self # Returning self for method chaining if needed
95
+ end
96
+
97
+ def select(*properties)
98
+ unless properties.blank?
99
+ @params ||= {}
100
+ @params[:properties] ||= []
101
+ @params[:properties].concat(properties.flatten)
102
+ end
103
+
104
+ self
105
+ end
106
+
64
107
  private
65
108
 
109
+ def determine_total
110
+ # We only get a response['total'] for the search endpoint
111
+ raise NotImplementedError, 'Total only available for search requests' unless search_request?
112
+
113
+ # if we don't already know the total we will make a single request and minimise the response
114
+ # size by asking for just one property and one record.
115
+
116
+ # store the current properties
117
+ original_properties = @params.delete(:properties)
118
+
119
+ # just request hs_object_id
120
+ @params[:properties] = ['hs_object_id']
121
+
122
+ # dummy request. @total will be set during the each_page evaluation
123
+ _first_page = first
124
+
125
+ # restore the original properties
126
+ @params[:properties] = original_properties
127
+
128
+ # return the now set total
129
+ @total
130
+ end
131
+
132
+ def search_request?
133
+ @url.include?('/search')
134
+ end
135
+
66
136
  def fetch_page(offset)
67
137
  params_with_offset = @params.dup
68
138
  params_with_offset.merge!(after: offset) if offset
@@ -82,8 +152,14 @@ module Hubspot
82
152
  end
83
153
  end
84
154
 
155
+ def wait_between_pages
156
+ return 0.2 if search_request?
157
+
158
+ 0
159
+ end
160
+
85
161
  def process_results(response)
86
- results = response['results'] || []
162
+ results = response[@results_param] || []
87
163
  return results unless @resource_class
88
164
 
89
165
  results.map { |result| @resource_class.new(result) }
@@ -3,6 +3,7 @@
3
3
  require_relative './api_client'
4
4
  require_relative './paged_collection'
5
5
  require_relative './paged_batch'
6
+ require_relative './resource_filter'
6
7
 
7
8
  module Hubspot
8
9
  # rubocop:disable Metrics/ClassLength
@@ -26,6 +27,7 @@ module Hubspot
26
27
  #
27
28
  class Resource < ApiClient
28
29
  METADATA_FIELDS = %w[createdate hs_object_id lastmodifieddate].freeze
30
+ extend ResourceFilter::FilterGroupMethods
29
31
 
30
32
  # Allow read/write access to id, properties, changes and metadata
31
33
 
@@ -42,6 +44,62 @@ module Hubspot
42
44
  attr_accessor :metadata
43
45
 
44
46
  class << self
47
+ # Return a paged_collection - similar to an ActiveRecord Relation
48
+ #
49
+ # Example:
50
+ # contacts_collection = Hubspot::Contact.all
51
+ # <PagedCollection>
52
+ #
53
+ # contacts_collection.where(email_contains: 'hubspot.com')
54
+ # <PagedCollection, @params={:filterGroups=> ....
55
+ def all
56
+ PagedCollection.new(
57
+ url: "#{api_root}/#{resource_name}/search",
58
+ params: {},
59
+ resource_class: self,
60
+ method: :post,
61
+ results_param: results_param
62
+ )
63
+ end
64
+
65
+ # Filter resources - allows chaining
66
+ #
67
+ # This method allows searching for resources by passing a hash with special suffixes
68
+ # on the keys to define different comparison operators.
69
+ #
70
+ # Available suffixes for query keys (when using a hash):
71
+ # - `_contains`: Matches values that contain the given string.
72
+ # - `_gt`: Greater than comparison.
73
+ # - `_lt`: Less than comparison.
74
+ # - `_gte`: Greater than or equal to comparison.
75
+ # - `_lte`: Less than or equal to comparison.
76
+ # - `_neq`: Not equal to comparison.
77
+ #
78
+ # If value is an array the 'IN' operator will be used
79
+ #
80
+ # Otherwise if no suffix is provided, the default comparison is equality (`EQ`).
81
+ #
82
+ # If no value is provided, or is empty the NOT_HAS_PROPERTY operator will be used
83
+ #
84
+ # filters - [Hash] The query for searching.
85
+ # each key represents a property and may have suffixes for the comparison
86
+ # (e.g., `{ email_contains: 'example.org', age_gt: 30 }`).
87
+
88
+ # Example:
89
+ #
90
+ # big_companies = Hubspot:Company.where(number_of_employees_gte: 100 )
91
+ # live_contacts = Hubspot::Contact.where(hs_lead_status: %w[NEW OPEN IN_PROGRESS])
92
+ #
93
+ # Returns a PagedCollection
94
+ def where(filters = {})
95
+ all.where!(filters)
96
+ end
97
+
98
+ # Select which properties to return from the api - allows chaining
99
+ def select(*properties)
100
+ all.select(*properties)
101
+ end
102
+
45
103
  # Find a resource by ID and return an instance of the class
46
104
  #
47
105
  # id - [Integer] The ID (or hs_object_id) of the resource to fetch.
@@ -53,11 +111,14 @@ module Hubspot
53
111
  #
54
112
  # Returns An instance of the resource.
55
113
  def find(id, properties: nil)
56
- all_properties = build_property_list(properties)
57
- if all_properties.is_a?(Array) && !all_properties.empty?
58
- params = { query: { properties: all_properties } }
59
- end
60
- response = get("#{api_root}/#{resource_name}/#{id}", params || {})
114
+ response = response_for_find_by_id(id, properties: properties)
115
+ return if response.not_found?
116
+
117
+ instantiate_from_response(response)
118
+ end
119
+
120
+ def find!(id, properties: nil)
121
+ response = response_for_find_by_id(id, properties: properties)
61
122
  instantiate_from_response(response)
62
123
  end
63
124
 
@@ -69,16 +130,16 @@ module Hubspot
69
130
  #
70
131
  # Example:
71
132
  # properties = %w[firstname lastname email last_contacted]
72
- # contact = Hubspot::Contact.find_by("email", "john@example.com", properties)
133
+ # contact = Hubspot::Contact.find_by("email", "john@example.com", properties: properties)
73
134
  #
74
135
  # Returns An instance of the resource.
75
- def find_by(property, value, properties = nil)
76
- params = { idProperty: property }
77
-
78
- all_properties = build_property_list(properties)
79
- params[:properties] = all_properties unless all_properties.empty?
136
+ def find_by(property, value, properties: nil)
137
+ response = response_for_find_by_property(property, value, properties: properties)
138
+ instantiate_from_response(response) unless response.not_found?
139
+ end
80
140
 
81
- response = get("#{api_root}/#{resource_name}/#{value}", query: params)
141
+ def find_by!(property, value, properties: nil)
142
+ response = response_for_find_by_property(property, value, properties: properties)
82
143
  instantiate_from_response(response)
83
144
  end
84
145
 
@@ -231,17 +292,6 @@ module Hubspot
231
292
  properties.detect { |prop| prop.name == property_name }
232
293
  end
233
294
 
234
- # Simplified search interface
235
- OPERATOR_MAP = {
236
- '_contains' => 'CONTAINS_TOKEN',
237
- '_gt' => 'GT',
238
- '_lt' => 'LT',
239
- '_gte' => 'GTE',
240
- '_lte' => 'LTE',
241
- '_neq' => 'NEQ',
242
- '_in' => 'IN'
243
- }.freeze
244
-
245
295
  # rubocop:disable Metrics/MethodLength
246
296
 
247
297
  # Search for resources using a flexible query format and optional properties.
@@ -287,11 +337,11 @@ module Hubspot
287
337
  # )
288
338
  #
289
339
  # Returns [PagedCollection] A paged collection of results that can be iterated over.
290
- def search(query:, properties: [], page_size: 100)
340
+ def search(query, properties: [], page_size: 200)
291
341
  search_body = {}
292
342
 
293
343
  # Add properties if specified
294
- search_body[:properties] = properties unless properties.empty?
344
+ search_body[:properties] = build_property_list(properties) unless properties.empty?
295
345
 
296
346
  # Handle the query using case-when for RuboCop compliance
297
347
  case query
@@ -311,16 +361,12 @@ module Hubspot
311
361
  url: "#{api_root}/#{resource_name}/search",
312
362
  params: search_body,
313
363
  resource_class: self,
314
- method: :post
364
+ method: :post,
365
+ results_param: results_param
315
366
  )
316
367
  end
317
-
318
368
  # rubocop:enable Metrics/MethodLength
319
369
 
320
- # The root of the api call. Mostly this will be "crm"
321
- # but you can override this to account for a different
322
- # object hierarchy
323
-
324
370
  # Define the resource name based on the class
325
371
  def resource_name
326
372
  name = self.name.split('::').last.downcase
@@ -339,49 +385,44 @@ module Hubspot
339
385
 
340
386
  private
341
387
 
388
+ # The root of the api call. Mostly this will be "crm"
389
+ # but you can override this to account for a different
390
+ # object hierarchy
342
391
  def api_root
343
392
  '/crm/v3/objects'
344
393
  end
345
394
 
346
- def list_page_uri
347
- "#{api_root}/#{resource_name}"
395
+ # In the response from the api the resources returned in this key
396
+ def results_param
397
+ 'results'
348
398
  end
349
399
 
350
- # Instantiate a single resource object from the response
351
- def instantiate_from_response(response)
352
- data = handle_response(response)
353
- new(data) # Passing full response data to initialize
400
+ def list_page_uri
401
+ "#{api_root}/#{resource_name}"
354
402
  end
355
403
 
356
- # Convert simple filters to HubSpot's filterGroups format
357
- def build_filter_groups(filters)
358
- filter_groups = [{ filters: [] }]
359
-
360
- filters.each do |key, value|
361
- filter = extract_property_and_operator(key, value)
362
- value_key = value.is_a?(Array) ? :values : :value
363
- filter[value_key] = value unless value.blank?
364
- filter_groups.first[:filters] << filter
404
+ def response_for_find_by_id(id, properties: nil)
405
+ all_properties = build_property_list(properties)
406
+ if all_properties.is_a?(Array) && !all_properties.empty?
407
+ params = { query: { properties: all_properties } }
365
408
  end
366
409
 
367
- filter_groups
410
+ get("#{api_root}/#{resource_name}/#{id}", params || {})
368
411
  end
369
412
 
370
- # Extract property name and operator from the key
371
- def extract_property_and_operator(key, value)
372
- return { propertyName: key.to_s, operator: 'NOT_HAS_PROPERTY' } if value.blank?
413
+ def response_for_find_by_property(property, value, properties: nil)
414
+ params = { idProperty: property }
415
+
416
+ all_properties = build_property_list(properties)
417
+ params[:properties] = all_properties unless all_properties.empty?
373
418
 
374
- OPERATOR_MAP.each do |suffix, hubspot_operator|
375
- if key.to_s.end_with?(suffix)
376
- return {
377
- propertyName: key.to_s.sub(suffix, ''),
378
- operator: hubspot_operator
379
- }
380
- end
381
- end
419
+ get("#{api_root}/#{resource_name}/#{value}", query: params)
420
+ end
382
421
 
383
- # Default to 'EQ' operator if no suffix is found
384
- { propertyName: key.to_s, operator: 'EQ' }
422
+ # Instantiate a single resource object from the response
423
+ def instantiate_from_response(response)
424
+ data = handle_response(response)
425
+ new(data) # Passing full response data to initialize
385
426
  end
386
427
 
387
428
  # Internal make a list of properties to request from the API
@@ -420,7 +461,7 @@ module Hubspot
420
461
  # existing_contact = Hubspot::Contact.new(id: hubspot_id, properties: contact.to_hubspot)
421
462
  def initialize(data = {})
422
463
  data.transform_keys!(&:to_s)
423
- @id = extract_id(data.delete('id'))
464
+ @id = extract_id(data.delete(api_id_field))
424
465
  @properties = {}
425
466
  @metadata = {}
426
467
  if @id
@@ -571,6 +612,10 @@ module Hubspot
571
612
 
572
613
  private
573
614
 
615
+ def api_id_field
616
+ 'id'
617
+ end
618
+
574
619
  # Extract ID from data and convert to integer
575
620
  def extract_id(id)
576
621
  id&.to_i
@@ -616,9 +661,13 @@ module Hubspot
616
661
  end
617
662
  end
618
663
 
619
- # allows overwriting in other resource classes
620
664
  def metadata_field?(key)
621
- METADATA_FIELDS.include?(key)
665
+ metadata_fields.include?(key)
666
+ end
667
+
668
+ # allows overwriting in other resource classes
669
+ def metadata_fields
670
+ METADATA_FIELDS
622
671
  end
623
672
 
624
673
  # Initialize a new object (no API response)
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hubspot
4
+ module ResourceFilter
5
+ module FilterGroupMethods
6
+ # Simplified search interface
7
+ OPERATOR_MAP = {
8
+ '_contains' => 'CONTAINS_TOKEN',
9
+ '_gt' => 'GT',
10
+ '_lt' => 'LT',
11
+ '_gte' => 'GTE',
12
+ '_lte' => 'LTE',
13
+ '_neq' => 'NEQ',
14
+ '_in' => 'IN'
15
+ }.freeze
16
+
17
+ # Convert simple filters to HubSpot's filterGroups format
18
+ def build_filter_groups(filters)
19
+ filter_groups = [{ filters: [] }]
20
+
21
+ filters.each do |key, value|
22
+ filter = extract_property_and_operator(key, value)
23
+ value_key = value.is_a?(Array) ? :values : :value
24
+ filter[value_key] = value unless value.blank?
25
+ filter_groups.first[:filters] << filter
26
+ end
27
+
28
+ filter_groups
29
+ end
30
+
31
+ # Extract property name and operator from the key
32
+ def extract_property_and_operator(key, value)
33
+ return { propertyName: key.to_s, operator: 'NOT_HAS_PROPERTY' } if value.blank?
34
+
35
+ OPERATOR_MAP.each do |suffix, hubspot_operator|
36
+ if key.to_s.end_with?(suffix)
37
+ return {
38
+ propertyName: key.to_s.sub(suffix, ''),
39
+ operator: hubspot_operator
40
+ }
41
+ end
42
+ end
43
+
44
+ # Default to 'EQ' operator if no suffix is found
45
+ { propertyName: key.to_s, operator: 'EQ' }
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hubspot
4
- VERSION = '0.3.1'
4
+ VERSION = '0.3.3'
5
5
  end
data/lib/hubspot.rb CHANGED
@@ -24,14 +24,20 @@ module Hubspot
24
24
  end
25
25
 
26
26
  def configured?
27
- return true unless @config.nil?
27
+ true unless @config.nil?
28
28
  end
29
29
 
30
30
  private
31
31
 
32
- # Set Authorization header on Hubspot::ApiClient when access_token is configured
32
+ # Set Authorization header on all the resource classes when access_token is configured
33
33
  def set_client_headers
34
- Hubspot::ApiClient.headers 'Authorization' => "Bearer #{config.access_token}"
34
+ api_classes = ObjectSpace.each_object(Class).select do |klass|
35
+ klass < Hubspot::ApiClient && !klass.singleton_class?
36
+ end
37
+
38
+ api_classes.each do |subclass|
39
+ subclass.headers 'Authorization' => "Bearer #{config.access_token}"
40
+ end
35
41
  end
36
42
 
37
43
  def set_request_timeouts
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_hubspot_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Brook
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-10-16 00:00:00.000000000 Z
11
+ date: 2024-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -228,6 +228,7 @@ files:
228
228
  - lib/hubspot/paged_collection.rb
229
229
  - lib/hubspot/property.rb
230
230
  - lib/hubspot/resource.rb
231
+ - lib/hubspot/resource_filter.rb
231
232
  - lib/hubspot/user.rb
232
233
  - lib/hubspot/version.rb
233
234
  - lib/ruby_hubspot_api.rb