ruby_hubspot_api 0.2.1.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
- 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