ruby_hubspot_api 0.1.2.1 → 0.2.1

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.
@@ -1,23 +1,70 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative './api_client'
4
+ require_relative './paged_collection'
5
+ require_relative './paged_batch'
4
6
 
5
7
  module Hubspot
6
8
  # rubocop:disable Metrics/ClassLength
7
- # Hubspot::Resource class
9
+
10
+ # HubSpot Resource Base Class
11
+ # This class provides common functionality for interacting with HubSpot API resources such as Contacts, Companies, etc
12
+ #
13
+ # It supports common operations like finding, creating, updating, and deleting resources, as well as batch operations.
14
+ #
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
18
+ #
19
+ # Example Usage:
20
+ # Hubspot::Contact.find(1)
21
+ # contact.name # 'Luke'
22
+ #
23
+ # company = Hubspot::Company.create(name: "Acme Corp")
24
+ # company.id.nil? # false
25
+ #
8
26
  class Resource < ApiClient
9
27
  METADATA_FIELDS = %w[createdate hs_object_id lastmodifieddate].freeze
10
28
 
11
- # Allow read/write access to properties and metadata
12
- attr_accessor :id, :properties, :changes, :metadata
29
+ # Allow read/write access to id, properties, changes and metadata
30
+
31
+ # the id of the object in hubspot
32
+ attr_accessor :id
33
+
34
+ # the properties as if read from the api
35
+ attr_accessor :properties
36
+
37
+ # track any changes made to properties before saving etc
38
+ attr_accessor :changes
39
+
40
+ # any other data sent from the api about the resource
41
+ attr_accessor :metadata
13
42
 
14
43
  class << self
15
44
  # Find a resource by ID and return an instance of the class
45
+ #
46
+ # id - [Integer] The ID (or hs_object_id) of the resource to fetch.
47
+ #
48
+ # Example:
49
+ # contact = Hubspot::Contact.find(1)
50
+ #
51
+ # Returns An instance of the resource.
16
52
  def find(id)
17
53
  response = get("/crm/v3/objects/#{resource_name}/#{id}")
18
54
  instantiate_from_response(response)
19
55
  end
20
56
 
57
+ # Finds a resource by a given property and value.
58
+ #
59
+ # property - The property to search by (e.g., "email").
60
+ # value - The value of the property to match.
61
+ # properties - Optional list of properties to return.
62
+ #
63
+ # Example:
64
+ # properties = %w[firstname lastname email last_contacted]
65
+ # contact = Hubspot::Contact.find_by("email", "john@example.com", properties)
66
+ #
67
+ # Returns An instance of the resource.
21
68
  def find_by(property, value, properties = nil)
22
69
  params = { idProperty: property }
23
70
  params[:properties] = properties if properties.is_a?(Array)
@@ -25,12 +72,28 @@ module Hubspot
25
72
  instantiate_from_response(response)
26
73
  end
27
74
 
28
- # Create a new resource
75
+ # Creates a new resource with the given parameters.
76
+ #
77
+ # params - The properties to create the resource with.
78
+ #
79
+ # Example:
80
+ # contact = Hubspot::Contact.create(name: "John Doe", email: "john@example.com")
81
+ #
82
+ # Returns [Resource] The newly created resource.
29
83
  def create(params)
30
84
  response = post("/crm/v3/objects/#{resource_name}", body: { properties: params }.to_json)
31
85
  instantiate_from_response(response)
32
86
  end
33
87
 
88
+ # Updates an existing resource by ID.
89
+ #
90
+ # id - The ID of the resource to update.
91
+ # params - The properties to update.
92
+ #
93
+ # Example:
94
+ # contact.update(1, name: "Jane Doe")
95
+ #
96
+ # Returns True if the update was successful, false if not
34
97
  def update(id, params)
35
98
  response = patch("/crm/v3/objects/#{resource_name}/#{id}", body: { properties: params }.to_json)
36
99
  raise Hubspot.error_from_response(response) unless response.success?
@@ -38,6 +101,14 @@ module Hubspot
38
101
  true
39
102
  end
40
103
 
104
+ # Deletes a resource by ID.
105
+ #
106
+ # id - The ID of the resource to delete.
107
+ #
108
+ # Example:
109
+ # Hubspot::Contact.archive(1)
110
+ #
111
+ # Returns True if the deletion was successful, false if not
41
112
  def archive(id)
42
113
  response = delete("/crm/v3/objects/#{resource_name}/#{id}")
43
114
  raise Hubspot.error_from_response(response) unless response.success?
@@ -45,6 +116,14 @@ module Hubspot
45
116
  true
46
117
  end
47
118
 
119
+ # Lists all resources with optional filters and pagination.
120
+ #
121
+ # params - Optional parameters to filter or paginate the results.
122
+ #
123
+ # Example:
124
+ # contacts = Hubspot::Contact.list(limit: 100)
125
+ #
126
+ # Returns [PagedCollection] A collection of resources.
48
127
  def list(params = {})
49
128
  PagedCollection.new(
50
129
  url: "/crm/v3/objects/#{resource_name}",
@@ -53,7 +132,45 @@ module Hubspot
53
132
  )
54
133
  end
55
134
 
56
- # Get the complete list of fields (properties) for the object
135
+ # Performs a batch read operation to retrieve multiple resources by their IDs.
136
+ #
137
+ # object_ids - A list of resource IDs to fetch.
138
+ #
139
+ # id_property - The property to use for identifying resources (default: 'id').
140
+ #
141
+ #
142
+ # Example:
143
+ # Hubspot::Contact.batch_read([1, 2, 3])
144
+ #
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 }
148
+
149
+ PagedBatch.new(
150
+ url: "/crm/v3/objects/#{resource_name}/batch/read",
151
+ params: params,
152
+ object_ids: object_ids,
153
+ resource_class: self
154
+ )
155
+ end
156
+
157
+ # Performs a batch read operation to retrieve multiple resources by their IDs
158
+ # until there are none left
159
+ #
160
+ # object_ids - A list of resource IDs to fetch. [Array<Integer>]
161
+ # id_property - The property to use for identifying resources (default: 'id').
162
+ #
163
+ # Example:
164
+ # Hubspot::Contact.batch_read([1, 2, 3])
165
+ #
166
+ # Returns [Hubspot::Batch] A batch of resources that can be operated on further
167
+ def batch_read_all(object_ids = [], id_property: 'id')
168
+ Hubspot::Batch.read(self, object_ids, id_property: id_property)
169
+ end
170
+
171
+ # Retrieve the complete list of properties for this resource class
172
+ #
173
+ # Returns [Array<Hubspot::Property>] An array of hubspot properties
57
174
  def properties
58
175
  @properties ||= begin
59
176
  response = get("/crm/v3/properties/#{resource_name}")
@@ -61,10 +178,34 @@ module Hubspot
61
178
  end
62
179
  end
63
180
 
181
+ # Retrieve the complete list of user defined properties for this resource class
182
+ #
183
+ # Returns [Array<Hubspot::Property>] An array of hubspot properties
64
184
  def custom_properties
65
185
  properties.reject { |property| property['hubspotDefined'] }
66
186
  end
67
187
 
188
+ # Retrieve the complete list of updatable properties for this resource class
189
+ #
190
+ # Returns [Array<Hubspot::Property>] An array of updateable hubspot properties
191
+ def updatable_properties
192
+ properties.reject(&:read_only?)
193
+ end
194
+
195
+ # Retrieve the complete list of read-only properties for this resource class
196
+ #
197
+ # Returns [Array<Hubspot::Property>] An array of read-only hubspot properties
198
+ def read_only_properties
199
+ properties.select(&:read_only)
200
+ end
201
+
202
+ # Retrieve information about a specific property
203
+ #
204
+ # Example:
205
+ # property = Hubspot::Contact.property('industry_sector')
206
+ # values_for_select = property.options.each_with_object({}) { |prop, ps| ps[prop['value']] = prop['label'] }
207
+ #
208
+ # Returns [Array<Hubspot::Property>] An array of hubspot properties
68
209
  def property(property_name)
69
210
  properties.detect { |prop| prop.name == property_name }
70
211
  end
@@ -81,6 +222,45 @@ module Hubspot
81
222
  }.freeze
82
223
 
83
224
  # rubocop:disable Metrics/MethodLength
225
+
226
+ # Search for resources using a flexible query format and optional properties.
227
+ #
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.
230
+ # You can also specify which properties to return and the number of results per page.
231
+ #
232
+ # Available suffixes for query keys (when using a hash):
233
+ # - `_contains`: Matches values that contain the given string.
234
+ # - `_gt`: Greater than comparison.
235
+ # - `_lt`: Less than comparison.
236
+ # - `_gte`: Greater than or equal to comparison.
237
+ # - `_lte`: Less than or equal to comparison.
238
+ # - `_neq`: Not equal to comparison.
239
+ # - `_in`: Matches any of the values in the given array.
240
+ #
241
+ # If no suffix is provided, the default comparison is equality (`EQ`).
242
+ #
243
+ # query - [String, Hash] The query for searching. This can be either:
244
+ # - A String: for full-text search.
245
+ # - A Hash: where each key represents a property and may have suffixes for the comparison
246
+ # (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>]
248
+ # 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).
250
+ #
251
+ # Example Usage:
252
+ # # Full-text search for 'example.org':
253
+ # contacts = Hubspot::Contact.search(query: "example.org",
254
+ # properties: ["email", "firstname", "lastname"], page_size: 50)
255
+ #
256
+ # # Search for contacts whose email contains 'example.org' and are older than 30:
257
+ # contacts = Hubspot::Contact.search(
258
+ # query: { email_contains: 'example.org', age_gt: 30 },
259
+ # properties: ["email", "firstname", "lastname"],
260
+ # page_size: 50
261
+ # )
262
+ #
263
+ # Returns [PagedCollection] A paged collection of results that can be iterated over.
84
264
  def search(query:, properties: [], page_size: 100)
85
265
  search_body = {}
86
266
 
@@ -111,8 +291,6 @@ module Hubspot
111
291
 
112
292
  # rubocop:enable Metrics/MethodLength
113
293
 
114
- private
115
-
116
294
  # Define the resource name based on the class
117
295
  def resource_name
118
296
  name = self.name.split('::').last.downcase
@@ -123,6 +301,8 @@ module Hubspot
123
301
  end
124
302
  end
125
303
 
304
+ private
305
+
126
306
  # Instantiate a single resource object from the response
127
307
  def instantiate_from_response(response)
128
308
  data = handle_response(response)
@@ -160,20 +340,51 @@ module Hubspot
160
340
  end
161
341
 
162
342
  # rubocop:disable Ling/MissingSuper
343
+
344
+ # Public: Initialize a resouce
345
+ #
346
+ # data - [2D Hash, nested Hash] data to initialise the resourse This can be either:
347
+ # - A Simple 2D Hash, key value pairs of property => value (for the create option)
348
+ # - A structured hash consisting of { id: <hs_object_id>, properties: {}, ... }
349
+ # This is the same structure as per the API, and can be rebuilt if you store the id
350
+ # of the object against your own data
351
+ #
352
+ # Example:
353
+ # contact = Hubspot::Contact.new(firstname: 'Luke', lastname: 'Skywalker', email: 'luke@jedi.org')
354
+ # contact.persisted? # false
355
+ # contact.save # creates the record in Hubspot
356
+ # contact.persisted? # true
357
+ # puts "Contact saved with hubspot id #{contact.id}"
358
+ #
359
+ # existing_contact = Hubspot::Contact.new(id: hubspot_id, properties: contact.to_hubspot_properties)
163
360
  def initialize(data = {})
361
+ data.transform_keys!(&:to_s)
164
362
  @id = extract_id(data)
165
363
  @properties = {}
166
364
  @metadata = {}
167
-
168
365
  if @id
169
366
  initialize_from_api(data)
170
367
  else
171
368
  initialize_new_object(data)
172
369
  end
173
370
  end
371
+
174
372
  # rubocop:enable Ling/MissingSuper
175
373
 
176
- # Instance methods for update (or save)
374
+ # Determine the state of the object
375
+ #
376
+ # Returns Boolean
377
+ def changes?
378
+ !@changes.empty?
379
+ end
380
+
381
+ # Create or Update the resource.
382
+ # If the resource was already persisted (e.g. it was retrieved from the API)
383
+ # it will be updated using values from @changes
384
+ #
385
+ # If the resource is new (no id) it will be created
386
+ #
387
+ # Returns Boolean
177
388
  def save
178
389
  if persisted?
179
390
  self.class.update(@id, @changes).tap do |result|
@@ -187,11 +398,22 @@ module Hubspot
187
398
  end
188
399
  end
189
400
 
401
+ # If the resource exists in Hubspot
402
+ #
403
+ # Returns Boolean
190
404
  def persisted?
191
405
  @id ? true : false
192
406
  end
193
407
 
194
408
  # Update the resource
409
+ #
410
+ # params - hash of properties to update in key value pairs
411
+ #
412
+ # Example:
413
+ # contact = Hubspot::Contact.find(hubspot_contact_id)
414
+ # contact.update(status: 'gold customer', last_contacted_at: Time.now.utc.iso8601)
415
+ #
416
+ # Returns Boolean
195
417
  def update(params)
196
418
  raise 'Not able to update as not persisted' unless persisted?
197
419
 
@@ -202,13 +424,27 @@ module Hubspot
202
424
  save
203
425
  end
204
426
 
427
+ # Archive the object in Hubspot
428
+ #
429
+ # Example:
430
+ # company = Hubspot::Company.find(hubspot_company_id)
431
+ # company.delete
432
+ #
205
433
  def delete
206
434
  self.class.archive(id)
207
435
  end
208
436
  alias archive delete
209
437
 
438
+ def resource_name
439
+ self.class.resource_name
440
+ end
441
+
210
442
  # rubocop:disable Metrics/MethodLength
211
- # Handle dynamic getter and setter methods with method_missing
443
+
444
+ # getter: Check the properties and changes hashes to see if the method
445
+ # being called is a key, and return the corresponding value
446
+ # setter: If the method ends in "=" persist the value in the changes hash
447
+ # (when it is different from the corresponding value in properties if set)
212
448
  def method_missing(method, *args)
213
449
  method_name = method.to_s
214
450
 
@@ -232,19 +468,16 @@ module Hubspot
232
468
  end
233
469
 
234
470
  # Fallback if the method or attribute is not found
235
- # :nocov:
236
471
  super
237
- # :nocov:
238
472
  end
473
+
239
474
  # rubocop:enable Metrics/MethodLength
240
475
 
241
- # Ensure respond_to_missing? is properly overridden
242
- # :nocov:
476
+ # Ensure respond_to_missing? handles existing keys in the properties anc changes hashes
243
477
  def respond_to_missing?(method_name, include_private = false)
244
478
  property_name = method_name.to_s.chomp('=')
245
479
  @properties.key?(property_name) || @changes.key?(property_name) || super
246
480
  end
247
- # :nocov:
248
481
 
249
482
  private
250
483
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hubspot
4
- VERSION = '0.1.2.1'
4
+ VERSION = '0.2.1'
5
5
  end
data/lib/hubspot.rb CHANGED
@@ -19,6 +19,7 @@ module Hubspot
19
19
  def configure
20
20
  yield(config) if block_given?
21
21
  set_client_headers if config.access_token
22
+ set_request_timeouts
22
23
  end
23
24
 
24
25
  def configured?
@@ -31,5 +32,18 @@ module Hubspot
31
32
  def set_client_headers
32
33
  Hubspot::ApiClient.headers 'Authorization' => "Bearer #{config.access_token}"
33
34
  end
35
+
36
+ def set_request_timeouts
37
+ config.timeout && Hubspot::ApiClient.default_timeout(config.timeout)
38
+ timeouts = %i[open_timeout read_timeout]
39
+ timeouts << :write_timeout if RUBY_VERSION >= '2.6'
40
+
41
+ timeouts.each do |t|
42
+ timeout = config.send(t)
43
+ next unless timeout
44
+
45
+ Hubspot::ApiClient.send(t, timeout)
46
+ end
47
+ end
34
48
  end
35
49
  end
@@ -20,4 +20,7 @@ require_relative 'hubspot/company'
20
20
  require_relative 'hubspot/user'
21
21
 
22
22
  # Load other components
23
+ require_relative 'hubspot/batch'
23
24
  require_relative 'hubspot/paged_collection'
25
+
26
+ require_relative 'support/patches'
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # simplest monkey patch, honest ;)
4
+ class Object
5
+ # Only define blank? if it's not already defined
6
+ unless method_defined?(:blank?)
7
+ def blank?
8
+ respond_to?(:empty?) ? empty? : !self
9
+ end
10
+ end
11
+ end
@@ -40,12 +40,14 @@ Gem::Specification.new do |spec|
40
40
  # Define development dependencies
41
41
  spec.add_development_dependency 'rake', '>= 11.0', '< 14.0'
42
42
 
43
- spec.add_development_dependency 'bundler', '>= 2.0'
43
+ spec.add_development_dependency 'bundler', '>= 2.0', '< 2.4.0'
44
+ spec.add_development_dependency 'codecov'
44
45
  spec.add_development_dependency 'dotenv', '>= 2.0'
45
46
  spec.add_development_dependency 'pry', '>= 0.1'
46
47
  spec.add_development_dependency 'pry-byebug', '>= 3.0'
47
48
  spec.add_development_dependency 'rspec', '>= 3.0'
48
- spec.add_development_dependency 'simplecov', '>= 0.22', '< 1.0'
49
+ spec.add_development_dependency 'simplecov'
50
+ spec.add_development_dependency 'simplecov-lcov'
49
51
  spec.add_development_dependency 'vcr', '>= 6.0'
50
52
  spec.add_development_dependency 'webmock', '>= 3.0'
51
53
 
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.1.2.1
4
+ version: 0.2.1
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-09-25 00:00:00.000000000 Z
11
+ date: 2024-10-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -37,6 +37,9 @@ dependencies:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: '2.0'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: 2.4.0
40
43
  type: :development
41
44
  prerelease: false
42
45
  version_requirements: !ruby/object:Gem::Requirement
@@ -44,6 +47,23 @@ dependencies:
44
47
  - - ">="
45
48
  - !ruby/object:Gem::Version
46
49
  version: '2.0'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: 2.4.0
53
+ - !ruby/object:Gem::Dependency
54
+ name: codecov
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ type: :development
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
47
67
  - !ruby/object:Gem::Dependency
48
68
  name: dotenv
49
69
  requirement: !ruby/object:Gem::Requirement
@@ -106,20 +126,28 @@ dependencies:
106
126
  requirements:
107
127
  - - ">="
108
128
  - !ruby/object:Gem::Version
109
- version: '0.22'
110
- - - "<"
111
- - !ruby/object:Gem::Version
112
- version: '1.0'
129
+ version: '0'
113
130
  type: :development
114
131
  prerelease: false
115
132
  version_requirements: !ruby/object:Gem::Requirement
116
133
  requirements:
117
134
  - - ">="
118
135
  - !ruby/object:Gem::Version
119
- version: '0.22'
120
- - - "<"
136
+ version: '0'
137
+ - !ruby/object:Gem::Dependency
138
+ name: simplecov-lcov
139
+ requirement: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
121
142
  - !ruby/object:Gem::Version
122
- version: '1.0'
143
+ version: '0'
144
+ type: :development
145
+ prerelease: false
146
+ version_requirements: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: '0'
123
151
  - !ruby/object:Gem::Dependency
124
152
  name: vcr
125
153
  requirement: !ruby/object:Gem::Requirement
@@ -176,10 +204,10 @@ extensions: []
176
204
  extra_rdoc_files: []
177
205
  files:
178
206
  - ".env.sample"
207
+ - ".github/workflows/ruby.yml"
179
208
  - ".gitignore"
180
209
  - ".rspec"
181
210
  - ".rubocop.yml"
182
- - ".ruby-version"
183
211
  - CHANGELOG.md
184
212
  - Gemfile
185
213
  - Gemfile.lock
@@ -190,16 +218,19 @@ files:
190
218
  - bin/setup
191
219
  - lib/hubspot.rb
192
220
  - lib/hubspot/api_client.rb
221
+ - lib/hubspot/batch.rb
193
222
  - lib/hubspot/company.rb
194
223
  - lib/hubspot/config.rb
195
224
  - lib/hubspot/contact.rb
196
225
  - lib/hubspot/exceptions.rb
226
+ - lib/hubspot/paged_batch.rb
197
227
  - lib/hubspot/paged_collection.rb
198
228
  - lib/hubspot/property.rb
199
229
  - lib/hubspot/resource.rb
200
230
  - lib/hubspot/user.rb
201
231
  - lib/hubspot/version.rb
202
232
  - lib/ruby_hubspot_api.rb
233
+ - lib/support/patches.rb
203
234
  - ruby_hubspot_api.gemspec
204
235
  homepage: https://github.com/sensadrome/ruby_hubspot_api
205
236
  licenses:
data/.ruby-version DELETED
@@ -1 +0,0 @@
1
- 2.5.3