ruby_hubspot_api 0.1.2.1 → 0.2.1

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