ruby_hubspot_api 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
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
  - - ">="