ruby_hubspot_api 0.2.1 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f455ed36bd0ae0284959198213bea7924099b97fc0b43b4ac50b4ad21a73def2
4
- data.tar.gz: 9ba207fcf4fdf8c14889d130a0cdd3284ff75816873d994084d80b410e8306e1
3
+ metadata.gz: 52c9ee03d417a29792dbd370399982a03662cb621c0ab3e5ec0a2abd7de89ac5
4
+ data.tar.gz: aae2926993d4b6dcf78b8e728c031631225f667c882f0b6d0710601ff62c0772
5
5
  SHA512:
6
- metadata.gz: 0d4274f0519d4fe99a09f06405689a3b45145a6890b4c5a1052e7e6e38bf91ae4ce9d96669d8bb97a5acd7e92110a0eeac4c73e7e0e6ea3b93fd1393f2f4c648
7
- data.tar.gz: 42829d5342d0dbd126b9200b609c662518dce48e55a82a6e3f6536ee4ed7bdf1c9af14007114e1f883ca036df53cc49522d239d803e5bdfceb525170a18adbac
6
+ metadata.gz: 6d2c9d82565c619a189bfc0780c914bb1b10cf769eb94b66e1e702fa5268fedadf7e3b9a188f6a1b0caa67c1b2d432e8f6401ffb890ca01efc768abc16ae3d3a
7
+ data.tar.gz: e66a9d5daa8373529a3a2ee07c474d288d94327fc40d83ed08e050e1dbb83c8f89217c95083035396f212ed78b97348f1a9437889d2719cd9dbf49d4624cbce3
data/.rubocop.yml CHANGED
@@ -1,6 +1,19 @@
1
- # .rubocop.yml
1
+ Documentation:
2
+ Enabled: false
3
+
4
+ Metrics/LineLength:
5
+ Max: 100
6
+ Exclude:
7
+ - spec/**/*.rb
8
+
9
+ # Metrics/MethodLength:
10
+ # Max: 20
11
+
12
+ # Metrics/ClassLength:
13
+ # Max: 200
2
14
 
3
- # Ignore Metrics/BlockLength for the spec directory
4
15
  Metrics/BlockLength:
5
- Exclude:
6
- - spec/**/*.rb
16
+ # Max: 50
17
+ # Ignore Metrics/BlockLength for the spec directory
18
+ Exclude:
19
+ - spec/**/*.rb
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ruby_hubspot_api (0.2)
4
+ ruby_hubspot_api (0.2.2)
5
5
  httparty (>= 0.1, < 1.0)
6
6
 
7
7
  GEM
@@ -9,35 +9,33 @@ GEM
9
9
  specs:
10
10
  addressable (2.8.7)
11
11
  public_suffix (>= 2.0.2, < 7.0)
12
- bigdecimal (3.1.8)
12
+ bigdecimal (3.0.2)
13
13
  byebug (11.1.3)
14
- codecov (0.2.12)
15
- json
16
- simplecov
14
+ codecov (0.6.0)
15
+ simplecov (>= 0.15, < 0.22)
17
16
  coderay (1.1.3)
18
17
  crack (1.0.0)
19
18
  bigdecimal
20
19
  rexml
21
20
  diff-lcs (1.5.1)
22
- docile (1.4.1)
21
+ docile (1.3.5)
23
22
  dotenv (2.8.1)
24
23
  hashdiff (1.1.1)
25
24
  httparty (0.21.0)
26
25
  mini_mime (>= 1.0.0)
27
26
  multi_xml (>= 0.5.2)
28
- json (2.7.2)
29
27
  method_source (1.1.0)
30
28
  mini_mime (1.1.2)
31
29
  multi_xml (0.6.0)
32
- pry (0.13.1)
30
+ pry (0.14.2)
33
31
  coderay (~> 1.1)
34
32
  method_source (~> 1.0)
35
- pry-byebug (3.9.0)
33
+ pry-byebug (3.8.0)
36
34
  byebug (~> 11.0)
37
- pry (~> 0.13.0)
35
+ pry (~> 0.10)
38
36
  public_suffix (4.0.7)
39
37
  rake (13.2.1)
40
- rexml (3.3.7)
38
+ rexml (3.2.5)
41
39
  rspec (3.13.0)
42
40
  rspec-core (~> 3.13.0)
43
41
  rspec-expectations (~> 3.13.0)
@@ -51,15 +49,13 @@ GEM
51
49
  diff-lcs (>= 1.2.0, < 2.0)
52
50
  rspec-support (~> 3.13.0)
53
51
  rspec-support (3.13.1)
54
- simplecov (0.22.0)
52
+ simplecov (0.18.5)
55
53
  docile (~> 1.1)
56
54
  simplecov-html (~> 0.11)
57
- simplecov_json_formatter (~> 0.1)
58
55
  simplecov-html (0.13.1)
59
56
  simplecov-lcov (0.8.0)
60
- simplecov_json_formatter (0.1.4)
61
57
  vcr (6.0.0)
62
- webmock (3.23.1)
58
+ webmock (3.18.1)
63
59
  addressable (>= 2.8.0)
64
60
  crack (>= 0.3.2)
65
61
  hashdiff (>= 0.4.0, < 2.0.0)
@@ -84,4 +80,4 @@ DEPENDENCIES
84
80
  webmock (>= 3.0)
85
81
 
86
82
  BUNDLED WITH
87
- 2.3.27
83
+ 2.2.34
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # HubSpot API Client
4
+ # Handles all HTTP interactions with the HubSpot API.
5
+ # It manages GET, POST, PATCH, and DELETE requests.
3
6
  module Hubspot
4
7
  # All interations with the Hubspot API happen here...
5
8
  class ApiClient
@@ -57,10 +60,14 @@ module Hubspot
57
60
 
58
61
  def log_request(http_method, url, response, start_time, extra = nil)
59
62
  d = Time.now - start_time
60
- Hubspot.logger.info("#{http_method.to_s.upcase} #{url} took #{d.round(2)}s with status #{response.code}")
63
+
64
+ Hubspot.logger.info(
65
+ "#{http_method.to_s.upcase} #{url} took #{d.round(2)}s with status #{response.code}"
66
+ )
67
+
61
68
  return unless Hubspot.logger.debug?
62
69
 
63
- Hubspot.logger.debug("Request body: #{extra}") if extra
70
+ Hubspot.logger.debug("Request body: #{extra[:body]}") if extra
64
71
  Hubspot.logger.debug("Response body: #{response.body}")
65
72
  end
66
73
 
@@ -91,7 +98,7 @@ module Hubspot
91
98
 
92
99
  def retry_request(request, retries)
93
100
  # Re-issues the original request using the retry logic
94
- http_method = request.http_method::METHOD.downcase # Use the METHOD constant to get the method string
101
+ http_method = request.http_method::METHOD.downcase
95
102
  response = HTTParty.send(http_method, request.uri, request.options)
96
103
  handle_response(response, retries)
97
104
  end
data/lib/hubspot/batch.rb CHANGED
@@ -31,8 +31,24 @@ module Hubspot
31
31
  CONTACT_LIMIT = 10
32
32
  DEFAULT_LIMIT = 100
33
33
 
34
+ def inspect
35
+ "#<#{self.class.name} " \
36
+ "@resource_count=#{@resources.size}, " \
37
+ "@id_property=#{@id_property.inspect}, " \
38
+ "@resource_type=#{@resources.first&.resource_name}, " \
39
+ "@responses_count=#{@responses.size}>"
40
+ end
41
+
34
42
  # rubocop:disable Lint/MissingSuper
35
- def initialize(resources = [], id_property: 'id')
43
+ def initialize(resources = [], id_property: 'id', resource_matcher: nil)
44
+ if resource_matcher
45
+ unless resource_matcher.is_a?(Proc) && resource_matcher.arity == 2
46
+ raise ArgumentError, 'resource_matcher must be a proc that accepts exactly 2 arguments'
47
+ end
48
+
49
+ @resource_matcher = resource_matcher
50
+ end
51
+
36
52
  @resources = []
37
53
  @id_property = id_property # Set id_property for the batch (default: 'id')
38
54
  @responses = [] # Store multiple BatchResponse objects here
@@ -122,9 +138,12 @@ module Hubspot
122
138
  next if resource.changes.empty?
123
139
 
124
140
  {
125
- id: resource.public_send(@id_property), # Dynamically get the ID based on the batch's id_property
126
- idProperty: determine_id_property, # Use the helper method to decide whether to include idProperty
127
- properties: resource.changes # Gather the changes for the resource
141
+ # Dynamically get the ID based on the batch's id_property
142
+ id: resource.public_send(@id_property),
143
+ # Use the helper method to decide whether to include idProperty
144
+ idProperty: determine_id_property,
145
+ # Gather the changes for the resource
146
+ properties: resource.changes
128
147
  }.compact # Removes nil keys
129
148
  end.compact # Removes nil entries
130
149
  end
@@ -146,7 +165,8 @@ module Hubspot
146
165
 
147
166
  # Perform batch request based on the provided action (upsert, update, create, or archive)
148
167
  def batch_request(type, inputs, action)
149
- response = self.class.post("/crm/v3/objects/#{type}/batch/#{action}", body: { inputs: inputs }.to_json)
168
+ response = self.class.post("/crm/v3/objects/#{type}/batch/#{action}",
169
+ body: { inputs: inputs }.to_json)
150
170
  BatchResponse.new(response.code, handle_response(response))
151
171
  end
152
172
 
@@ -157,7 +177,8 @@ module Hubspot
157
177
  # check if there are any resources without a value from the id_property
158
178
  return unless @resources.any? { |resource| resource.public_send(id_property).blank? }
159
179
 
160
- raise ArgumentError, "All resources must have a non-blank value for #{@id_property} to perform upsert"
180
+ raise ArgumentError,
181
+ "All resources must have a non-blank value for #{@id_property} to perform upsert"
161
182
  end
162
183
 
163
184
  # Return the appropriate batch size limit for the resource type
@@ -167,6 +188,8 @@ module Hubspot
167
188
 
168
189
  # Process responses from the batch API call
169
190
  def process_responses
191
+ # TODO: issue a warning if the id_property is email and the action is upsert*
192
+ # people may have more than one email address abd Hubspot views that as one record
170
193
  @responses.each do |response|
171
194
  next unless response['results']
172
195
 
@@ -192,36 +215,56 @@ module Hubspot
192
215
  end
193
216
 
194
217
  def find_resource_from_result(result)
195
- case @action
196
- when 'update', 'upsert'
197
- find_resource_from_id(result['id'].to_i)
198
-
199
- #
200
- # when specifying idProperty in the upsert request
201
- # the Hubspot API returns the id value (aka the hs_object_id)
202
- # instead of the value of the <idProperty> field, so the value of the field
203
- # is only stored in results['properties'][@id_property] if it changed!
204
- # so this condition is redundant but left here in case Hubspot updates the response
205
- #
206
- # when 'upsert'
207
- # resource_id = result.dig('properties', @id_property) || result['id']
208
- # find_resource_from_id_property(resource_id)
209
- #
210
- when 'create'
211
- # For create, check if the resource's changes are entirely contained in the result's properties
212
- @resources.reject(&:persisted?).find do |resource|
213
- resource.changes.any? && resource.changes.all? { |key, value| result['properties'][key.to_s] == value }
214
- end
218
+ action_method = method_for_action
219
+ send(action_method, result) if action_method
220
+ end
221
+
222
+ def method_for_action
223
+ {
224
+ 'create' => :find_resource_for_created_result,
225
+ 'update' => :find_resource_from_updated_result,
226
+ 'upsert' => :find_resource_from_upserted_result
227
+ }[@action]
228
+ end
229
+
230
+ def find_resource_for_created_result(result)
231
+ properties = result['properties']
232
+
233
+ @resources.reject(&:persisted?).find do |resource|
234
+ next unless resource.changes.any?
235
+
236
+ resource.changes.all? { |key, value| properties[key.to_s] == value }
215
237
  end
216
238
  end
217
239
 
240
+ def find_resource_from_updated_result(result)
241
+ resource_id = id_property == 'id' ? result['id'].to_i : result.dig('properties', id_property)
242
+ find_resource_from_id(resource_id)
243
+ end
244
+
218
245
  def find_resource_from_id(resource_id)
219
- @resources.find { |r| r.id == resource_id }
246
+ return find_resource_from_id_property(resource_id) unless @id_property == 'id'
247
+
248
+ @resources.find { |resource| resource.id == resource_id }
220
249
  end
221
250
 
222
- # def find_resource_from_id_property(resource_id)
223
- # @resources.find { |r| r.public_send(@id_property) == resource_id }
224
- # end
251
+ def find_resource_from_id_property(resource_id)
252
+ @resources.find do |resource|
253
+ resource.respond_to?(@id_property) && resource.public_send(@id_property) == resource_id
254
+ end
255
+ end
256
+
257
+ def find_resource_from_upserted_result(result)
258
+ # if this was inserted then match on all the fields
259
+ return find_resource_for_created_result(result['properties']) if result['new']
260
+
261
+ # call the custom resource matcher if specified
262
+ if @resource_matcher
263
+ @resources.find { |resource| @resource_matcher.call(resource, result) }
264
+ else
265
+ find_resource_from_updated_result(result)
266
+ end
267
+ end
225
268
 
226
269
  def update_resource_properties(resource, properties)
227
270
  properties.each do |key, value|
@@ -238,7 +281,9 @@ module Hubspot
238
281
 
239
282
  class << self
240
283
  def read(object_class, object_ids = [], id_property: 'id')
241
- raise ArgumentError, 'Must be a valid Hubspot resource class' unless object_class < Hubspot::Resource
284
+ unless object_class < Hubspot::Resource
285
+ raise ArgumentError, 'Must be a valid Hubspot resource class'
286
+ end
242
287
 
243
288
  # fetch all the matching resources with paging handled
244
289
  resources = object_class.batch_read(object_ids, id_property: id_property).all
@@ -15,6 +15,11 @@ module Hubspot
15
15
  apply_log_level
16
16
  end
17
17
 
18
+ # Apply the log level to the logger
19
+ def apply_log_level
20
+ @logger.level = @log_level
21
+ end
22
+
18
23
  private
19
24
 
20
25
  # Initialize the default logger
@@ -43,11 +48,6 @@ module Hubspot
43
48
  end
44
49
  # rubocop:enable Metrics/MethodLength
45
50
 
46
- # Apply the log level to the logger
47
- def apply_log_level
48
- @logger.level = @log_level
49
- end
50
-
51
51
  # Set the default log level based on environment
52
52
  def default_log_level
53
53
  if defined?(Rails) && Rails.env.test?
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hubspot
4
+ class Form < Resource
5
+ METADATA_FIELDS = %w[createdAt updatedAt archived].freeze
6
+
7
+ def inspect
8
+ "#<#{self.class.name} " \
9
+ "@name=#{name}, " \
10
+ "@fieldGroups=#{respond_to?('fieldGroups') ? fieldGroups.size : '-'}>"
11
+ end
12
+
13
+ class << self
14
+ def api_root
15
+ '/marketing/v3'
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ # Extract ID from data and leave as a string
22
+ def extract_id(data)
23
+ data.delete('id')
24
+ end
25
+ end
26
+ end
@@ -10,6 +10,15 @@ module Hubspot
10
10
 
11
11
  MAX_LIMIT = 100 # HubSpot max items per page
12
12
 
13
+ # customised inspect
14
+ def inspect
15
+ "#<#{self.class.name} " \
16
+ "@url=#{@url.inspect}, " \
17
+ "@params=#{@params.inspect}, " \
18
+ "@resource_class=#{@resource_class.inspect}, " \
19
+ "@object_ids_count=#{@object_ids.size}>"
20
+ end
21
+
13
22
  # rubocop:disable Lint/MissingSuper
14
23
  def initialize(url:, params: {}, resource_class: nil, object_ids: [])
15
24
  @url = url
@@ -22,8 +31,7 @@ module Hubspot
22
31
  def each_page
23
32
  @object_ids.each_slice(MAX_LIMIT) do |ids|
24
33
  response = fetch_page(ids)
25
- results = response['results'] || []
26
- mapped_results = @resource_class ? results.map { |result| @resource_class.new(result) } : results
34
+ mapped_results = process_results(response)
27
35
  yield mapped_results unless mapped_results.empty?
28
36
  end
29
37
  end
@@ -45,12 +53,19 @@ module Hubspot
45
53
  private
46
54
 
47
55
  def fetch_page(object_ids)
48
- params_with_ids = @params.dup
56
+ params_with_ids = @params.dup || {}
49
57
  params_with_ids[:inputs] = object_ids.map { |id| { id: id } }
50
58
 
51
59
  response = self.class.post(@url, body: params_with_ids.to_json)
52
60
 
53
61
  handle_response(response)
54
62
  end
63
+
64
+ def process_results(response)
65
+ results = response['results'] || []
66
+ return results unless @resource_class
67
+
68
+ results.map { |result| @resource_class.new(result) }
69
+ end
55
70
  end
56
71
  end
@@ -10,21 +10,18 @@ module Hubspot
10
10
 
11
11
  MAX_LIMIT = 100 # HubSpot max items per page
12
12
 
13
- # rubocop:disable Lint/MissingSuper
14
13
  def initialize(url:, params: {}, resource_class: nil, method: :get)
15
14
  @url = url
16
15
  @params = params
17
16
  @resource_class = resource_class
18
17
  @method = method.to_sym
19
18
  end
20
- # rubocop:enable Lint/MissingSuper
21
19
 
22
20
  def each_page
23
21
  offset = nil
24
22
  loop do
25
23
  response = fetch_page(offset)
26
- results = response['results'] || []
27
- mapped_results = @resource_class ? results.map { |result| @resource_class.new(result) } : results
24
+ mapped_results = process_results(response)
28
25
  yield mapped_results unless mapped_results.empty?
29
26
  offset = response.dig('paging', 'next', 'after')
30
27
  break unless offset
@@ -83,5 +80,12 @@ module Hubspot
83
80
  self.class.send(@method, @url, body: params.to_json)
84
81
  end
85
82
  end
83
+
84
+ def process_results(response)
85
+ results = response['results'] || []
86
+ return results unless @resource_class
87
+
88
+ results.map { |result| @resource_class.new(result) }
89
+ end
86
90
  end
87
91
  end
@@ -8,13 +8,14 @@ module Hubspot
8
8
  # rubocop:disable Metrics/ClassLength
9
9
 
10
10
  # HubSpot Resource Base Class
11
- # This class provides common functionality for interacting with HubSpot API resources such as Contacts, Companies, etc
11
+ # This class provides common functionality for interacting with
12
+ # HubSpot API resources such as Contacts, Companies, etc
12
13
  #
13
- # It supports common operations like finding, creating, updating, and deleting resources, as well as batch operations.
14
+ # It supports common operations like finding, creating, updating,
15
+ # and deleting resources, as well as batch operations.
14
16
  #
15
- # This class is meant to be inherited by specific resources like `Hubspot::Contact`.
16
- #
17
- # You can access the properties of a resource instance by calling the property name as method
17
+ # This class is meant to be inherited by specific resources
18
+ # like `Hubspot::Contact`.
18
19
  #
19
20
  # Example Usage:
20
21
  # Hubspot::Contact.find(1)
@@ -49,8 +50,12 @@ module Hubspot
49
50
  # contact = Hubspot::Contact.find(1)
50
51
  #
51
52
  # Returns An instance of the resource.
52
- def find(id)
53
- response = get("/crm/v3/objects/#{resource_name}/#{id}")
53
+ def find(id, properties = nil)
54
+ all_properties = build_property_list(properties)
55
+ if all_properties.is_a?(Array) && !all_properties.empty?
56
+ params = { query: { properties: all_properties } }
57
+ end
58
+ response = get("#{api_root}/#{resource_name}/#{id}", params || {})
54
59
  instantiate_from_response(response)
55
60
  end
56
61
 
@@ -67,8 +72,11 @@ module Hubspot
67
72
  # Returns An instance of the resource.
68
73
  def find_by(property, value, properties = nil)
69
74
  params = { idProperty: property }
70
- params[:properties] = properties if properties.is_a?(Array)
71
- response = get("/crm/v3/objects/#{resource_name}/#{value}", query: params)
75
+
76
+ all_properties = build_property_list(properties)
77
+ params[:properties] = all_properties unless all_properties.empty?
78
+
79
+ response = get("#{api_root}/#{resource_name}/#{value}", query: params)
72
80
  instantiate_from_response(response)
73
81
  end
74
82
 
@@ -81,7 +89,7 @@ module Hubspot
81
89
  #
82
90
  # Returns [Resource] The newly created resource.
83
91
  def create(params)
84
- response = post("/crm/v3/objects/#{resource_name}", body: { properties: params }.to_json)
92
+ response = post("#{api_root}/#{resource_name}", body: { properties: params }.to_json)
85
93
  instantiate_from_response(response)
86
94
  end
87
95
 
@@ -93,10 +101,11 @@ module Hubspot
93
101
  # Example:
94
102
  # contact.update(1, name: "Jane Doe")
95
103
  #
96
- # Returns True if the update was successful, false if not
104
+ # Returns True if the update was successful
97
105
  def update(id, params)
98
- response = patch("/crm/v3/objects/#{resource_name}/#{id}", body: { properties: params }.to_json)
99
- raise Hubspot.error_from_response(response) unless response.success?
106
+ response = patch("#{api_root}/#{resource_name}/#{id}",
107
+ body: { properties: params }.to_json)
108
+ handle_response(response)
100
109
 
101
110
  true
102
111
  end
@@ -108,10 +117,10 @@ module Hubspot
108
117
  # Example:
109
118
  # Hubspot::Contact.archive(1)
110
119
  #
111
- # Returns True if the deletion was successful, false if not
120
+ # Returns True if the deletion was successful
112
121
  def archive(id)
113
- response = delete("/crm/v3/objects/#{resource_name}/#{id}")
114
- raise Hubspot.error_from_response(response) unless response.success?
122
+ response = delete("#{api_root}/#{resource_name}/#{id}")
123
+ handle_response(response)
115
124
 
116
125
  true
117
126
  end
@@ -125,8 +134,14 @@ module Hubspot
125
134
  #
126
135
  # Returns [PagedCollection] A collection of resources.
127
136
  def list(params = {})
137
+ all_properties = build_property_list(params[:properties])
138
+
139
+ if all_properties.is_a?(Array) && !all_properties.empty?
140
+ params[:properties] = all_properties.join(',')
141
+ end
142
+
128
143
  PagedCollection.new(
129
- url: "/crm/v3/objects/#{resource_name}",
144
+ url: list_page_uri,
130
145
  params: params,
131
146
  resource_class: self
132
147
  )
@@ -142,13 +157,15 @@ module Hubspot
142
157
  # Example:
143
158
  # Hubspot::Contact.batch_read([1, 2, 3])
144
159
  #
145
- # Returns [PagedBatch] A paged batch of resources (call .each_page to cycle through pages from the API)
146
- def batch_read(object_ids = [], id_property: 'id')
147
- params = id_property == 'id' ? {} : { idProperty: id_property }
160
+ # Returns [PagedBatch] A paged batch of resources
161
+ def batch_read(object_ids = [], properties: [], id_property: 'id')
162
+ params = {}
163
+ params[:idProperty] = id_property unless id_property == 'id'
164
+ params[:properties] = properties unless properties.blank?
148
165
 
149
166
  PagedBatch.new(
150
- url: "/crm/v3/objects/#{resource_name}/batch/read",
151
- params: params,
167
+ url: "#{api_root}/#{resource_name}/batch/read",
168
+ params: params.empty? ? nil : params,
152
169
  object_ids: object_ids,
153
170
  resource_class: self
154
171
  )
@@ -161,7 +178,7 @@ module Hubspot
161
178
  # id_property - The property to use for identifying resources (default: 'id').
162
179
  #
163
180
  # Example:
164
- # Hubspot::Contact.batch_read([1, 2, 3])
181
+ # Hubspot::Contact.batch_read_all(hubspot_contact_ids)
165
182
  #
166
183
  # Returns [Hubspot::Batch] A batch of resources that can be operated on further
167
184
  def batch_read_all(object_ids = [], id_property: 'id')
@@ -203,9 +220,11 @@ module Hubspot
203
220
  #
204
221
  # Example:
205
222
  # property = Hubspot::Contact.property('industry_sector')
206
- # values_for_select = property.options.each_with_object({}) { |prop, ps| ps[prop['value']] = prop['label'] }
223
+ # values_for_select = property.options.each_with_object({}) do |prop, hash|
224
+ # hash[prop['value']] = prop['label']
225
+ # end
207
226
  #
208
- # Returns [Array<Hubspot::Property>] An array of hubspot properties
227
+ # Returns [Hubspot::Property] A hubspot property
209
228
  def property(property_name)
210
229
  properties.detect { |prop| prop.name == property_name }
211
230
  end
@@ -225,8 +244,10 @@ module Hubspot
225
244
 
226
245
  # Search for resources using a flexible query format and optional properties.
227
246
  #
228
- # This method allows searching for resources by passing a query in the form of a string (for full-text search)
229
- # or a hash with special suffixes on the keys to define different comparison operators.
247
+ # This method allows searching for resources by passing a query in the form of a string
248
+ # (for full-text search) or a hash with special suffixes on the keys to
249
+ # define different comparison operators.
250
+ #
230
251
  # You can also specify which properties to return and the number of results per page.
231
252
  #
232
253
  # Available suffixes for query keys (when using a hash):
@@ -244,14 +265,15 @@ module Hubspot
244
265
  # - A String: for full-text search.
245
266
  # - A Hash: where each key represents a property and may have suffixes for the comparison
246
267
  # (e.g., `{ email_contains: 'example.org', age_gt: 30 }`).
247
- # properties - An optional array of property names to return in the search results. [Array<String>]
268
+ # properties - An optional array of property names to return in the search results.
248
269
  # If not specified or empty, HubSpot will return the default set of properties.
249
- # page_size - The number of results to return per page (default is 10 for contacts and 100 for everything else).
270
+ # page_size - The number of results to return per page
271
+ # (default is 10 for contacts and 100 for everything else).
250
272
  #
251
273
  # Example Usage:
252
274
  # # Full-text search for 'example.org':
253
- # contacts = Hubspot::Contact.search(query: "example.org",
254
- # properties: ["email", "firstname", "lastname"], page_size: 50)
275
+ # props = %w[email firstname lastname]
276
+ # contacts = Hubspot::Contact.search(query: "example.org", properties: props, page_size: 50)
255
277
  #
256
278
  # # Search for contacts whose email contains 'example.org' and are older than 30:
257
279
  # contacts = Hubspot::Contact.search(
@@ -282,7 +304,7 @@ module Hubspot
282
304
 
283
305
  # Perform the search and return a PagedCollection
284
306
  PagedCollection.new(
285
- url: "/crm/v3/objects/#{resource_name}/search",
307
+ url: "#{api_root}/#{resource_name}/search",
286
308
  params: search_body,
287
309
  resource_class: self,
288
310
  method: :post
@@ -291,6 +313,10 @@ module Hubspot
291
313
 
292
314
  # rubocop:enable Metrics/MethodLength
293
315
 
316
+ # The root of the api call. Mostly this will be "crm"
317
+ # but you can override this to account for a different
318
+ # object hierarchy
319
+
294
320
  # Define the resource name based on the class
295
321
  def resource_name
296
322
  name = self.name.split('::').last.downcase
@@ -301,8 +327,22 @@ module Hubspot
301
327
  end
302
328
  end
303
329
 
330
+ # List of properties that will always be retrieved
331
+ # should be overridden in specific resource class
332
+ def required_properties
333
+ []
334
+ end
335
+
304
336
  private
305
337
 
338
+ def api_root
339
+ '/crm/v3/objects'
340
+ end
341
+
342
+ def list_page_uri
343
+ "#{api_root}/#{resource_name}"
344
+ end
345
+
306
346
  # Instantiate a single resource object from the response
307
347
  def instantiate_from_response(response)
308
348
  data = handle_response(response)
@@ -337,9 +377,18 @@ module Hubspot
337
377
  # Default to 'EQ' operator if no suffix is found
338
378
  { propertyName: key.to_s, operator: 'EQ' }
339
379
  end
380
+
381
+ # Internal make a list of properties to request from the API
382
+ # will be merged with any required_properties defined on the class
383
+ def build_property_list(properties)
384
+ properties = [] unless properties.is_a?(Array)
385
+ raise 'Must be an array' unless required_properties.is_a?(Array)
386
+
387
+ properties.concat(required_properties).uniq
388
+ end
340
389
  end
341
390
 
342
- # rubocop:disable Ling/MissingSuper
391
+ # rubocop:disable Lint/MissingSuper
343
392
 
344
393
  # Public: Initialize a resouce
345
394
  #
@@ -350,13 +399,14 @@ module Hubspot
350
399
  # of the object against your own data
351
400
  #
352
401
  # Example:
353
- # contact = Hubspot::Contact.new(firstname: 'Luke', lastname: 'Skywalker', email: 'luke@jedi.org')
402
+ # attrs = { firstname: 'Luke', lastname: 'Skywalker', email: 'luke@jedi.org' }
403
+ # contact = Hubspot::Contact.new(attrs)
354
404
  # contact.persisted? # false
355
405
  # contact.save # creates the record in Hubspot
356
406
  # contact.persisted? # true
357
407
  # puts "Contact saved with hubspot id #{contact.id}"
358
408
  #
359
- # existing_contact = Hubspot::Contact.new(id: hubspot_id, properties: contact.to_hubspot_properties)
409
+ # existing_contact = Hubspot::Contact.new(id: hubspot_id, properties: contact.to_hubspot)
360
410
  def initialize(data = {})
361
411
  data.transform_keys!(&:to_s)
362
412
  @id = extract_id(data)
@@ -368,8 +418,7 @@ module Hubspot
368
418
  initialize_new_object(data)
369
419
  end
370
420
  end
371
-
372
- # rubocop:enable Ling/MissingSuper
421
+ # rubocop:enable Lint/MissingSuper
373
422
 
374
423
  # Determine the state of the object
375
424
  #
@@ -405,25 +454,44 @@ module Hubspot
405
454
  @id ? true : false
406
455
  end
407
456
 
408
- # Update the resource
457
+ # Public - Update the resource and persist to the api
409
458
  #
410
- # params - hash of properties to update in key value pairs
459
+ # attributes - hash of properties to update in key value pairs
411
460
  #
412
461
  # Example:
413
462
  # contact = Hubspot::Contact.find(hubspot_contact_id)
414
463
  # contact.update(status: 'gold customer', last_contacted_at: Time.now.utc.iso8601)
415
464
  #
416
465
  # Returns Boolean
417
- def update(params)
466
+ def update(attributes)
418
467
  raise 'Not able to update as not persisted' unless persisted?
419
468
 
420
- params.each do |key, value|
421
- send("#{key}=", value) # This will trigger the @changes tracking via method_missing
422
- end
469
+ update_attributes(attributes)
423
470
 
424
471
  save
425
472
  end
426
473
 
474
+ # Public - Update resource attributes
475
+ #
476
+ # Does not persist to the api but processes each attribute correctly
477
+ #
478
+ # Example:
479
+ # contact = Hubspot::Contact.find(hubspot_contact_id)
480
+ # contact.changes? # false
481
+ # contact.update_attributes(education: 'Graduate', university: 'Life')
482
+ # contact.education # Graduate
483
+ # contact.changes? # true
484
+ # contact.changes # { "education" => "Graduate", "university" => "Life" }
485
+ #
486
+ # Returns Hash of changes
487
+ def update_attributes(attributes)
488
+ raise ArgumentError, 'must be a hash' unless attributes.is_a?(Hash)
489
+
490
+ attributes.each do |key, value|
491
+ send("#{key}=", value) # This will trigger the @changes tracking via method_missing
492
+ end
493
+ end
494
+
427
495
  # Archive the object in Hubspot
428
496
  #
429
497
  # Example:
@@ -488,9 +556,17 @@ module Hubspot
488
556
 
489
557
  # Initialize from API response, separating metadata from properties
490
558
  def initialize_from_api(data)
491
- @metadata = extract_metadata(data)
492
- properties_data = data['properties'] || {}
559
+ if data['properties']
560
+ @metadata = data.reject { |key, _v| key == 'properties' }
561
+ handle_properties(data['properties'])
562
+ else
563
+ handle_properties(data)
564
+ end
493
565
 
566
+ @changes = {}
567
+ end
568
+
569
+ def handle_properties(properties_data)
494
570
  properties_data.each do |key, value|
495
571
  if METADATA_FIELDS.include?(key)
496
572
  @metadata[key] = value
@@ -498,8 +574,6 @@ module Hubspot
498
574
  @properties[key] = value
499
575
  end
500
576
  end
501
-
502
- @changes = {}
503
577
  end
504
578
 
505
579
  # Initialize a new object (no API response)
data/lib/hubspot/user.rb CHANGED
@@ -1,8 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hubspot
4
+ # ORM for hubspot users
5
+ #
6
+ # Hubspot users consist mostly of read_only attributes (you can add custom properties).
7
+ # As such we extend this class to ensure that we retrieve useful data back from the API
8
+ # and provide helper methods to resolve hubspot fields e.g. user.email calls user.hs_email etc
4
9
  class User < Resource
10
+ class << self
11
+ def required_properties
12
+ %w[hs_email hs_given_name hs_family_name]
13
+ end
14
+ end
15
+
16
+ def first_name
17
+ hs_given_name
18
+ end
19
+ alias firstname first_name
20
+
21
+ def last_name
22
+ hs_family_name
23
+ end
24
+ alias lastname last_name
25
+
26
+ def email
27
+ hs_email
28
+ end
5
29
  end
6
30
 
7
- Owner = User
31
+ Owner = User
8
32
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hubspot
4
- VERSION = '0.2.1'
4
+ VERSION = '0.2.2'
5
5
  end
data/lib/hubspot.rb CHANGED
@@ -20,6 +20,7 @@ module Hubspot
20
20
  yield(config) if block_given?
21
21
  set_client_headers if config.access_token
22
22
  set_request_timeouts
23
+ config.apply_log_level
23
24
  end
24
25
 
25
26
  def configured?
@@ -12,13 +12,18 @@ require_relative 'hubspot/config'
12
12
  require_relative 'hubspot/exceptions'
13
13
  require_relative 'hubspot/api_client'
14
14
 
15
- # load base class then modules
15
+ # load base class then models
16
16
  require_relative 'hubspot/resource'
17
17
  require_relative 'hubspot/property'
18
+
19
+ # load CRM models
18
20
  require_relative 'hubspot/contact'
19
21
  require_relative 'hubspot/company'
20
22
  require_relative 'hubspot/user'
21
23
 
24
+ # load marketing models
25
+ require_relative 'hubspot/form'
26
+
22
27
  # Load other components
23
28
  require_relative 'hubspot/batch'
24
29
  require_relative 'hubspot/paged_collection'
@@ -9,3 +9,30 @@ class Object
9
9
  end
10
10
  end
11
11
  end
12
+
13
+ # At some point this will seem like a bad idea ;)
14
+
15
+ # :nocov:
16
+ if RUBY_VERSION < '2.5.0'
17
+ class Hash
18
+ # Non-mutating version (returns a new hash with transformed keys)
19
+ def transform_keys
20
+ return enum_for(:transform_keys) unless block_given?
21
+ result = {}
22
+ each_key do |key|
23
+ result[yield(key)] = self[key]
24
+ end
25
+ result
26
+ end
27
+
28
+ # Mutating version (modifies the hash in place)
29
+ def transform_keys!
30
+ return enum_for(:transform_keys!) unless block_given?
31
+ keys.each do |key|
32
+ self[yield(key)] = delete(key)
33
+ end
34
+ self
35
+ end
36
+ end
37
+ end
38
+ # :nocov:end
@@ -16,7 +16,7 @@ Gem::Specification.new do |spec|
16
16
  spec.homepage = 'https://github.com/sensadrome/ruby_hubspot_api'
17
17
  spec.license = 'MIT'
18
18
 
19
- spec.required_ruby_version = '>= 2.5'
19
+ spec.required_ruby_version = '>= 2.4'
20
20
 
21
21
  # Prevent pushing this gem to RubyGems.org.
22
22
  # To allow pushes either set the 'allowed_push_host'
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.2.1
4
+ version: 0.2.2
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-01 00:00:00.000000000 Z
11
+ date: 2024-10-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -223,6 +223,7 @@ files:
223
223
  - lib/hubspot/config.rb
224
224
  - lib/hubspot/contact.rb
225
225
  - lib/hubspot/exceptions.rb
226
+ - lib/hubspot/form.rb
226
227
  - lib/hubspot/paged_batch.rb
227
228
  - lib/hubspot/paged_collection.rb
228
229
  - lib/hubspot/property.rb
@@ -246,7 +247,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
246
247
  requirements:
247
248
  - - ">="
248
249
  - !ruby/object:Gem::Version
249
- version: '2.5'
250
+ version: '2.4'
250
251
  required_rubygems_version: !ruby/object:Gem::Requirement
251
252
  requirements:
252
253
  - - ">="