ruby_hubspot_api 0.2.1.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
- SHA1:
3
- metadata.gz: ae46636189d9ad859e0e342eed9b752ccbd64c06
4
- data.tar.gz: 3cf0de6c387b00810af776e6b4ff89bcdcc00230
2
+ SHA256:
3
+ metadata.gz: 52c9ee03d417a29792dbd370399982a03662cb621c0ab3e5ec0a2abd7de89ac5
4
+ data.tar.gz: aae2926993d4b6dcf78b8e728c031631225f667c882f0b6d0710601ff62c0772
5
5
  SHA512:
6
- metadata.gz: ebdaa4d4f2bb6f1f7b6c9fede064598daf78edcf2204322819d2a13e7dbd504ff1792a7afd75037a4eae24171cd9b52d962dc6e2eb7439fe5bb9a84ad6733a22
7
- data.tar.gz: fa2dd031fa494b3bd8c68880d68aa19603035bbf32395a7e3650eefa8906507d6e0790020e032002612d59c516c5eb97b371b69f2f1ff563a4819006aa5d6180
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.1)
4
+ ruby_hubspot_api (0.2.2)
5
5
  httparty (>= 0.1, < 1.0)
6
6
 
7
7
  GEM
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # HubSpot API Client
4
- # Handles all HTTP interactions with the HubSpot API. It manages GET, POST, PATCH, and DELETE requests.
4
+ # Handles all HTTP interactions with the HubSpot API.
5
+ # It manages GET, POST, PATCH, and DELETE requests.
5
6
  module Hubspot
6
7
  # All interations with the Hubspot API happen here...
7
8
  class ApiClient
@@ -59,10 +60,14 @@ module Hubspot
59
60
 
60
61
  def log_request(http_method, url, response, start_time, extra = nil)
61
62
  d = Time.now - start_time
62
- 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
+
63
68
  return unless Hubspot.logger.debug?
64
69
 
65
- Hubspot.logger.debug("Request body: #{extra}") if extra
70
+ Hubspot.logger.debug("Request body: #{extra[:body]}") if extra
66
71
  Hubspot.logger.debug("Response body: #{response.body}")
67
72
  end
68
73
 
@@ -93,7 +98,7 @@ module Hubspot
93
98
 
94
99
  def retry_request(request, retries)
95
100
  # Re-issues the original request using the retry logic
96
- http_method = request.http_method::METHOD.downcase # Use the METHOD constant to get the method string
101
+ http_method = request.http_method::METHOD.downcase
97
102
  response = HTTParty.send(http_method, request.uri, request.options)
98
103
  handle_response(response, retries)
99
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
@@ -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
@@ -6,71 +6,188 @@ require_relative './paged_batch'
6
6
 
7
7
  module Hubspot
8
8
  # rubocop:disable Metrics/ClassLength
9
- # Hubspot::Resource class
9
+
10
+ # HubSpot Resource Base Class
11
+ # This class provides common functionality for interacting with
12
+ # HubSpot API resources such as Contacts, Companies, etc
13
+ #
14
+ # It supports common operations like finding, creating, updating,
15
+ # and deleting resources, as well as batch operations.
16
+ #
17
+ # This class is meant to be inherited by specific resources
18
+ # like `Hubspot::Contact`.
19
+ #
20
+ # Example Usage:
21
+ # Hubspot::Contact.find(1)
22
+ # contact.name # 'Luke'
23
+ #
24
+ # company = Hubspot::Company.create(name: "Acme Corp")
25
+ # company.id.nil? # false
26
+ #
10
27
  class Resource < ApiClient
11
28
  METADATA_FIELDS = %w[createdate hs_object_id lastmodifieddate].freeze
12
29
 
13
- # Allow read/write access to properties and metadata
14
- attr_accessor :id, :properties, :changes, :metadata
30
+ # Allow read/write access to id, properties, changes and metadata
31
+
32
+ # the id of the object in hubspot
33
+ attr_accessor :id
34
+
35
+ # the properties as if read from the api
36
+ attr_accessor :properties
37
+
38
+ # track any changes made to properties before saving etc
39
+ attr_accessor :changes
40
+
41
+ # any other data sent from the api about the resource
42
+ attr_accessor :metadata
15
43
 
16
44
  class << self
17
45
  # Find a resource by ID and return an instance of the class
18
- def find(id)
19
- response = get("/crm/v3/objects/#{resource_name}/#{id}")
46
+ #
47
+ # id - [Integer] The ID (or hs_object_id) of the resource to fetch.
48
+ #
49
+ # Example:
50
+ # contact = Hubspot::Contact.find(1)
51
+ #
52
+ # Returns An instance of the resource.
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 || {})
20
59
  instantiate_from_response(response)
21
60
  end
22
61
 
62
+ # Finds a resource by a given property and value.
63
+ #
64
+ # property - The property to search by (e.g., "email").
65
+ # value - The value of the property to match.
66
+ # properties - Optional list of properties to return.
67
+ #
68
+ # Example:
69
+ # properties = %w[firstname lastname email last_contacted]
70
+ # contact = Hubspot::Contact.find_by("email", "john@example.com", properties)
71
+ #
72
+ # Returns An instance of the resource.
23
73
  def find_by(property, value, properties = nil)
24
74
  params = { idProperty: property }
25
- params[:properties] = properties if properties.is_a?(Array)
26
- 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)
27
80
  instantiate_from_response(response)
28
81
  end
29
82
 
30
- # Create a new resource
83
+ # Creates a new resource with the given parameters.
84
+ #
85
+ # params - The properties to create the resource with.
86
+ #
87
+ # Example:
88
+ # contact = Hubspot::Contact.create(name: "John Doe", email: "john@example.com")
89
+ #
90
+ # Returns [Resource] The newly created resource.
31
91
  def create(params)
32
- response = post("/crm/v3/objects/#{resource_name}", body: { properties: params }.to_json)
92
+ response = post("#{api_root}/#{resource_name}", body: { properties: params }.to_json)
33
93
  instantiate_from_response(response)
34
94
  end
35
95
 
96
+ # Updates an existing resource by ID.
97
+ #
98
+ # id - The ID of the resource to update.
99
+ # params - The properties to update.
100
+ #
101
+ # Example:
102
+ # contact.update(1, name: "Jane Doe")
103
+ #
104
+ # Returns True if the update was successful
36
105
  def update(id, params)
37
- response = patch("/crm/v3/objects/#{resource_name}/#{id}", body: { properties: params }.to_json)
38
- 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)
39
109
 
40
110
  true
41
111
  end
42
112
 
113
+ # Deletes a resource by ID.
114
+ #
115
+ # id - The ID of the resource to delete.
116
+ #
117
+ # Example:
118
+ # Hubspot::Contact.archive(1)
119
+ #
120
+ # Returns True if the deletion was successful
43
121
  def archive(id)
44
- response = delete("/crm/v3/objects/#{resource_name}/#{id}")
45
- raise Hubspot.error_from_response(response) unless response.success?
122
+ response = delete("#{api_root}/#{resource_name}/#{id}")
123
+ handle_response(response)
46
124
 
47
125
  true
48
126
  end
49
127
 
128
+ # Lists all resources with optional filters and pagination.
129
+ #
130
+ # params - Optional parameters to filter or paginate the results.
131
+ #
132
+ # Example:
133
+ # contacts = Hubspot::Contact.list(limit: 100)
134
+ #
135
+ # Returns [PagedCollection] A collection of resources.
50
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
+
51
143
  PagedCollection.new(
52
- url: "/crm/v3/objects/#{resource_name}",
144
+ url: list_page_uri,
53
145
  params: params,
54
146
  resource_class: self
55
147
  )
56
148
  end
57
149
 
58
- def batch_read(object_ids = [], id_property: 'id')
59
- params = id_property == 'id' ? {} : { idProperty: id_property }
150
+ # Performs a batch read operation to retrieve multiple resources by their IDs.
151
+ #
152
+ # object_ids - A list of resource IDs to fetch.
153
+ #
154
+ # id_property - The property to use for identifying resources (default: 'id').
155
+ #
156
+ #
157
+ # Example:
158
+ # Hubspot::Contact.batch_read([1, 2, 3])
159
+ #
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?
60
165
 
61
166
  PagedBatch.new(
62
- url: "/crm/v3/objects/#{resource_name}/batch/read",
63
- params: params,
167
+ url: "#{api_root}/#{resource_name}/batch/read",
168
+ params: params.empty? ? nil : params,
64
169
  object_ids: object_ids,
65
170
  resource_class: self
66
171
  )
67
172
  end
68
173
 
174
+ # Performs a batch read operation to retrieve multiple resources by their IDs
175
+ # until there are none left
176
+ #
177
+ # object_ids - A list of resource IDs to fetch. [Array<Integer>]
178
+ # id_property - The property to use for identifying resources (default: 'id').
179
+ #
180
+ # Example:
181
+ # Hubspot::Contact.batch_read_all(hubspot_contact_ids)
182
+ #
183
+ # Returns [Hubspot::Batch] A batch of resources that can be operated on further
69
184
  def batch_read_all(object_ids = [], id_property: 'id')
70
185
  Hubspot::Batch.read(self, object_ids, id_property: id_property)
71
186
  end
72
187
 
73
- # Get the complete list of fields (properties) for the object
188
+ # Retrieve the complete list of properties for this resource class
189
+ #
190
+ # Returns [Array<Hubspot::Property>] An array of hubspot properties
74
191
  def properties
75
192
  @properties ||= begin
76
193
  response = get("/crm/v3/properties/#{resource_name}")
@@ -78,18 +195,36 @@ module Hubspot
78
195
  end
79
196
  end
80
197
 
198
+ # Retrieve the complete list of user defined properties for this resource class
199
+ #
200
+ # Returns [Array<Hubspot::Property>] An array of hubspot properties
81
201
  def custom_properties
82
202
  properties.reject { |property| property['hubspotDefined'] }
83
203
  end
84
204
 
205
+ # Retrieve the complete list of updatable properties for this resource class
206
+ #
207
+ # Returns [Array<Hubspot::Property>] An array of updateable hubspot properties
85
208
  def updatable_properties
86
209
  properties.reject(&:read_only?)
87
210
  end
88
211
 
212
+ # Retrieve the complete list of read-only properties for this resource class
213
+ #
214
+ # Returns [Array<Hubspot::Property>] An array of read-only hubspot properties
89
215
  def read_only_properties
90
- properties.select(&:read_only?)
216
+ properties.select(&:read_only)
91
217
  end
92
218
 
219
+ # Retrieve information about a specific property
220
+ #
221
+ # Example:
222
+ # property = Hubspot::Contact.property('industry_sector')
223
+ # values_for_select = property.options.each_with_object({}) do |prop, hash|
224
+ # hash[prop['value']] = prop['label']
225
+ # end
226
+ #
227
+ # Returns [Hubspot::Property] A hubspot property
93
228
  def property(property_name)
94
229
  properties.detect { |prop| prop.name == property_name }
95
230
  end
@@ -106,6 +241,48 @@ module Hubspot
106
241
  }.freeze
107
242
 
108
243
  # rubocop:disable Metrics/MethodLength
244
+
245
+ # Search for resources using a flexible query format and optional properties.
246
+ #
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
+ #
251
+ # You can also specify which properties to return and the number of results per page.
252
+ #
253
+ # Available suffixes for query keys (when using a hash):
254
+ # - `_contains`: Matches values that contain the given string.
255
+ # - `_gt`: Greater than comparison.
256
+ # - `_lt`: Less than comparison.
257
+ # - `_gte`: Greater than or equal to comparison.
258
+ # - `_lte`: Less than or equal to comparison.
259
+ # - `_neq`: Not equal to comparison.
260
+ # - `_in`: Matches any of the values in the given array.
261
+ #
262
+ # If no suffix is provided, the default comparison is equality (`EQ`).
263
+ #
264
+ # query - [String, Hash] The query for searching. This can be either:
265
+ # - A String: for full-text search.
266
+ # - A Hash: where each key represents a property and may have suffixes for the comparison
267
+ # (e.g., `{ email_contains: 'example.org', age_gt: 30 }`).
268
+ # properties - An optional array of property names to return in the search results.
269
+ # If not specified or empty, HubSpot will return the default set of properties.
270
+ # page_size - The number of results to return per page
271
+ # (default is 10 for contacts and 100 for everything else).
272
+ #
273
+ # Example Usage:
274
+ # # Full-text search for 'example.org':
275
+ # props = %w[email firstname lastname]
276
+ # contacts = Hubspot::Contact.search(query: "example.org", properties: props, page_size: 50)
277
+ #
278
+ # # Search for contacts whose email contains 'example.org' and are older than 30:
279
+ # contacts = Hubspot::Contact.search(
280
+ # query: { email_contains: 'example.org', age_gt: 30 },
281
+ # properties: ["email", "firstname", "lastname"],
282
+ # page_size: 50
283
+ # )
284
+ #
285
+ # Returns [PagedCollection] A paged collection of results that can be iterated over.
109
286
  def search(query:, properties: [], page_size: 100)
110
287
  search_body = {}
111
288
 
@@ -127,7 +304,7 @@ module Hubspot
127
304
 
128
305
  # Perform the search and return a PagedCollection
129
306
  PagedCollection.new(
130
- url: "/crm/v3/objects/#{resource_name}/search",
307
+ url: "#{api_root}/#{resource_name}/search",
131
308
  params: search_body,
132
309
  resource_class: self,
133
310
  method: :post
@@ -136,6 +313,10 @@ module Hubspot
136
313
 
137
314
  # rubocop:enable Metrics/MethodLength
138
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
+
139
320
  # Define the resource name based on the class
140
321
  def resource_name
141
322
  name = self.name.split('::').last.downcase
@@ -146,8 +327,22 @@ module Hubspot
146
327
  end
147
328
  end
148
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
+
149
336
  private
150
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
+
151
346
  # Instantiate a single resource object from the response
152
347
  def instantiate_from_response(response)
153
348
  data = handle_response(response)
@@ -182,9 +377,36 @@ module Hubspot
182
377
  # Default to 'EQ' operator if no suffix is found
183
378
  { propertyName: key.to_s, operator: 'EQ' }
184
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
185
389
  end
186
390
 
187
- # rubocop:disable Ling/MissingSuper
391
+ # rubocop:disable Lint/MissingSuper
392
+
393
+ # Public: Initialize a resouce
394
+ #
395
+ # data - [2D Hash, nested Hash] data to initialise the resourse This can be either:
396
+ # - A Simple 2D Hash, key value pairs of property => value (for the create option)
397
+ # - A structured hash consisting of { id: <hs_object_id>, properties: {}, ... }
398
+ # This is the same structure as per the API, and can be rebuilt if you store the id
399
+ # of the object against your own data
400
+ #
401
+ # Example:
402
+ # attrs = { firstname: 'Luke', lastname: 'Skywalker', email: 'luke@jedi.org' }
403
+ # contact = Hubspot::Contact.new(attrs)
404
+ # contact.persisted? # false
405
+ # contact.save # creates the record in Hubspot
406
+ # contact.persisted? # true
407
+ # puts "Contact saved with hubspot id #{contact.id}"
408
+ #
409
+ # existing_contact = Hubspot::Contact.new(id: hubspot_id, properties: contact.to_hubspot)
188
410
  def initialize(data = {})
189
411
  data.transform_keys!(&:to_s)
190
412
  @id = extract_id(data)
@@ -196,13 +418,22 @@ module Hubspot
196
418
  initialize_new_object(data)
197
419
  end
198
420
  end
199
- # rubocop:enable Ling/MissingSuper
421
+ # rubocop:enable Lint/MissingSuper
200
422
 
423
+ # Determine the state of the object
424
+ #
425
+ # Returns Boolean
201
426
  def changes?
202
427
  !@changes.empty?
203
428
  end
204
429
 
205
- # Instance methods for update (or save)
430
+ # Create or Update the resource.
431
+ # If the resource was already persisted (e.g. it was retrieved from the API)
432
+ # it will be updated using values from @changes
433
+ #
434
+ # If the resource is new (no id) it will be created
435
+ #
436
+ # Returns Boolean
206
437
  def save
207
438
  if persisted?
208
439
  self.class.update(@id, @changes).tap do |result|
@@ -216,21 +447,57 @@ module Hubspot
216
447
  end
217
448
  end
218
449
 
450
+ # If the resource exists in Hubspot
451
+ #
452
+ # Returns Boolean
219
453
  def persisted?
220
454
  @id ? true : false
221
455
  end
222
456
 
223
- # Update the resource
224
- def update(params)
457
+ # Public - Update the resource and persist to the api
458
+ #
459
+ # attributes - hash of properties to update in key value pairs
460
+ #
461
+ # Example:
462
+ # contact = Hubspot::Contact.find(hubspot_contact_id)
463
+ # contact.update(status: 'gold customer', last_contacted_at: Time.now.utc.iso8601)
464
+ #
465
+ # Returns Boolean
466
+ def update(attributes)
225
467
  raise 'Not able to update as not persisted' unless persisted?
226
468
 
227
- params.each do |key, value|
228
- send("#{key}=", value) # This will trigger the @changes tracking via method_missing
229
- end
469
+ update_attributes(attributes)
230
470
 
231
471
  save
232
472
  end
233
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
+
495
+ # Archive the object in Hubspot
496
+ #
497
+ # Example:
498
+ # company = Hubspot::Company.find(hubspot_company_id)
499
+ # company.delete
500
+ #
234
501
  def delete
235
502
  self.class.archive(id)
236
503
  end
@@ -241,7 +508,11 @@ module Hubspot
241
508
  end
242
509
 
243
510
  # rubocop:disable Metrics/MethodLength
244
- # Handle dynamic getter and setter methods with method_missing
511
+
512
+ # getter: Check the properties and changes hashes to see if the method
513
+ # being called is a key, and return the corresponding value
514
+ # setter: If the method ends in "=" persist the value in the changes hash
515
+ # (when it is different from the corresponding value in properties if set)
245
516
  def method_missing(method, *args)
246
517
  method_name = method.to_s
247
518
 
@@ -267,9 +538,10 @@ module Hubspot
267
538
  # Fallback if the method or attribute is not found
268
539
  super
269
540
  end
541
+
270
542
  # rubocop:enable Metrics/MethodLength
271
543
 
272
- # Ensure respond_to_missing? is properly overridden
544
+ # Ensure respond_to_missing? handles existing keys in the properties anc changes hashes
273
545
  def respond_to_missing?(method_name, include_private = false)
274
546
  property_name = method_name.to_s.chomp('=')
275
547
  @properties.key?(property_name) || @changes.key?(property_name) || super
@@ -284,9 +556,17 @@ module Hubspot
284
556
 
285
557
  # Initialize from API response, separating metadata from properties
286
558
  def initialize_from_api(data)
287
- @metadata = extract_metadata(data)
288
- 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
289
565
 
566
+ @changes = {}
567
+ end
568
+
569
+ def handle_properties(properties_data)
290
570
  properties_data.each do |key, value|
291
571
  if METADATA_FIELDS.include?(key)
292
572
  @metadata[key] = value
@@ -294,8 +574,6 @@ module Hubspot
294
574
  @properties[key] = value
295
575
  end
296
576
  end
297
-
298
- @changes = {}
299
577
  end
300
578
 
301
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.1'
4
+ VERSION = '0.2.2'
5
5
  end
@@ -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'
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.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
@@ -253,8 +254,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
253
254
  - !ruby/object:Gem::Version
254
255
  version: '0'
255
256
  requirements: []
256
- rubyforge_project:
257
- rubygems_version: 2.6.14.4
257
+ rubygems_version: 3.2.3
258
258
  signing_key:
259
259
  specification_version: 4
260
260
  summary: ruby_hubspot_api is an ORM-like wrapper for the Hubspot API