lib/hubspot/resource.rb in ruby_hubspot_api-0.2.1.1 vs lib/hubspot/resource.rb in ruby_hubspot_api-0.2.2
- old
+ new
@@ -4,94 +4,229 @@
require_relative './paged_collection'
require_relative './paged_batch'
module Hubspot
# rubocop:disable Metrics/ClassLength
- # Hubspot::Resource class
+
+ # HubSpot Resource Base Class
+ # This class provides common functionality for interacting with
+ # HubSpot API resources such as Contacts, Companies, etc
+ #
+ # It supports common operations like finding, creating, updating,
+ # and deleting resources, as well as batch operations.
+ #
+ # This class is meant to be inherited by specific resources
+ # like `Hubspot::Contact`.
+ #
+ # Example Usage:
+ # Hubspot::Contact.find(1)
+ # contact.name # 'Luke'
+ #
+ # company = Hubspot::Company.create(name: "Acme Corp")
+ # company.id.nil? # false
+ #
class Resource < ApiClient
METADATA_FIELDS = %w[createdate hs_object_id lastmodifieddate].freeze
- # Allow read/write access to properties and metadata
- attr_accessor :id, :properties, :changes, :metadata
+ # Allow read/write access to id, properties, changes and metadata
+ # the id of the object in hubspot
+ attr_accessor :id
+
+ # the properties as if read from the api
+ attr_accessor :properties
+
+ # track any changes made to properties before saving etc
+ attr_accessor :changes
+
+ # any other data sent from the api about the resource
+ attr_accessor :metadata
+
class << self
# Find a resource by ID and return an instance of the class
- def find(id)
- response = get("/crm/v3/objects/#{resource_name}/#{id}")
+ #
+ # id - [Integer] The ID (or hs_object_id) of the resource to fetch.
+ #
+ # Example:
+ # contact = Hubspot::Contact.find(1)
+ #
+ # Returns An instance of the resource.
+ def find(id, properties = nil)
+ all_properties = build_property_list(properties)
+ if all_properties.is_a?(Array) && !all_properties.empty?
+ params = { query: { properties: all_properties } }
+ end
+ response = get("#{api_root}/#{resource_name}/#{id}", params || {})
instantiate_from_response(response)
end
+ # Finds a resource by a given property and value.
+ #
+ # property - The property to search by (e.g., "email").
+ # value - The value of the property to match.
+ # properties - Optional list of properties to return.
+ #
+ # Example:
+ # properties = %w[firstname lastname email last_contacted]
+ # contact = Hubspot::Contact.find_by("email", "john@example.com", properties)
+ #
+ # Returns An instance of the resource.
def find_by(property, value, properties = nil)
params = { idProperty: property }
- params[:properties] = properties if properties.is_a?(Array)
- response = get("/crm/v3/objects/#{resource_name}/#{value}", query: params)
+
+ all_properties = build_property_list(properties)
+ params[:properties] = all_properties unless all_properties.empty?
+
+ response = get("#{api_root}/#{resource_name}/#{value}", query: params)
instantiate_from_response(response)
end
- # Create a new resource
+ # Creates a new resource with the given parameters.
+ #
+ # params - The properties to create the resource with.
+ #
+ # Example:
+ # contact = Hubspot::Contact.create(name: "John Doe", email: "john@example.com")
+ #
+ # Returns [Resource] The newly created resource.
def create(params)
- response = post("/crm/v3/objects/#{resource_name}", body: { properties: params }.to_json)
+ response = post("#{api_root}/#{resource_name}", body: { properties: params }.to_json)
instantiate_from_response(response)
end
+ # Updates an existing resource by ID.
+ #
+ # id - The ID of the resource to update.
+ # params - The properties to update.
+ #
+ # Example:
+ # contact.update(1, name: "Jane Doe")
+ #
+ # Returns True if the update was successful
def update(id, params)
- response = patch("/crm/v3/objects/#{resource_name}/#{id}", body: { properties: params }.to_json)
- raise Hubspot.error_from_response(response) unless response.success?
+ response = patch("#{api_root}/#{resource_name}/#{id}",
+ body: { properties: params }.to_json)
+ handle_response(response)
true
end
+ # Deletes a resource by ID.
+ #
+ # id - The ID of the resource to delete.
+ #
+ # Example:
+ # Hubspot::Contact.archive(1)
+ #
+ # Returns True if the deletion was successful
def archive(id)
- response = delete("/crm/v3/objects/#{resource_name}/#{id}")
- raise Hubspot.error_from_response(response) unless response.success?
+ response = delete("#{api_root}/#{resource_name}/#{id}")
+ handle_response(response)
true
end
+ # Lists all resources with optional filters and pagination.
+ #
+ # params - Optional parameters to filter or paginate the results.
+ #
+ # Example:
+ # contacts = Hubspot::Contact.list(limit: 100)
+ #
+ # Returns [PagedCollection] A collection of resources.
def list(params = {})
+ all_properties = build_property_list(params[:properties])
+
+ if all_properties.is_a?(Array) && !all_properties.empty?
+ params[:properties] = all_properties.join(',')
+ end
+
PagedCollection.new(
- url: "/crm/v3/objects/#{resource_name}",
+ url: list_page_uri,
params: params,
resource_class: self
)
end
- def batch_read(object_ids = [], id_property: 'id')
- params = id_property == 'id' ? {} : { idProperty: id_property }
+ # Performs a batch read operation to retrieve multiple resources by their IDs.
+ #
+ # object_ids - A list of resource IDs to fetch.
+ #
+ # id_property - The property to use for identifying resources (default: 'id').
+ #
+ #
+ # Example:
+ # Hubspot::Contact.batch_read([1, 2, 3])
+ #
+ # Returns [PagedBatch] A paged batch of resources
+ def batch_read(object_ids = [], properties: [], id_property: 'id')
+ params = {}
+ params[:idProperty] = id_property unless id_property == 'id'
+ params[:properties] = properties unless properties.blank?
PagedBatch.new(
- url: "/crm/v3/objects/#{resource_name}/batch/read",
- params: params,
+ url: "#{api_root}/#{resource_name}/batch/read",
+ params: params.empty? ? nil : params,
object_ids: object_ids,
resource_class: self
)
end
+ # Performs a batch read operation to retrieve multiple resources by their IDs
+ # until there are none left
+ #
+ # object_ids - A list of resource IDs to fetch. [Array<Integer>]
+ # id_property - The property to use for identifying resources (default: 'id').
+ #
+ # Example:
+ # Hubspot::Contact.batch_read_all(hubspot_contact_ids)
+ #
+ # Returns [Hubspot::Batch] A batch of resources that can be operated on further
def batch_read_all(object_ids = [], id_property: 'id')
Hubspot::Batch.read(self, object_ids, id_property: id_property)
end
- # Get the complete list of fields (properties) for the object
+ # Retrieve the complete list of properties for this resource class
+ #
+ # Returns [Array<Hubspot::Property>] An array of hubspot properties
def properties
@properties ||= begin
response = get("/crm/v3/properties/#{resource_name}")
handle_response(response)['results'].map { |hash| Property.new(hash) }
end
end
+ # Retrieve the complete list of user defined properties for this resource class
+ #
+ # Returns [Array<Hubspot::Property>] An array of hubspot properties
def custom_properties
properties.reject { |property| property['hubspotDefined'] }
end
+ # Retrieve the complete list of updatable properties for this resource class
+ #
+ # Returns [Array<Hubspot::Property>] An array of updateable hubspot properties
def updatable_properties
properties.reject(&:read_only?)
end
+ # Retrieve the complete list of read-only properties for this resource class
+ #
+ # Returns [Array<Hubspot::Property>] An array of read-only hubspot properties
def read_only_properties
- properties.select(&:read_only?)
+ properties.select(&:read_only)
end
+ # Retrieve information about a specific property
+ #
+ # Example:
+ # property = Hubspot::Contact.property('industry_sector')
+ # values_for_select = property.options.each_with_object({}) do |prop, hash|
+ # hash[prop['value']] = prop['label']
+ # end
+ #
+ # Returns [Hubspot::Property] A hubspot property
def property(property_name)
properties.detect { |prop| prop.name == property_name }
end
# Simplified search interface
@@ -104,10 +239,52 @@
'_neq' => 'NEQ',
'_in' => 'IN'
}.freeze
# rubocop:disable Metrics/MethodLength
+
+ # Search for resources using a flexible query format and optional properties.
+ #
+ # This method allows searching for resources by passing a query in the form of a string
+ # (for full-text search) or a hash with special suffixes on the keys to
+ # define different comparison operators.
+ #
+ # You can also specify which properties to return and the number of results per page.
+ #
+ # Available suffixes for query keys (when using a hash):
+ # - `_contains`: Matches values that contain the given string.
+ # - `_gt`: Greater than comparison.
+ # - `_lt`: Less than comparison.
+ # - `_gte`: Greater than or equal to comparison.
+ # - `_lte`: Less than or equal to comparison.
+ # - `_neq`: Not equal to comparison.
+ # - `_in`: Matches any of the values in the given array.
+ #
+ # If no suffix is provided, the default comparison is equality (`EQ`).
+ #
+ # query - [String, Hash] The query for searching. This can be either:
+ # - A String: for full-text search.
+ # - A Hash: where each key represents a property and may have suffixes for the comparison
+ # (e.g., `{ email_contains: 'example.org', age_gt: 30 }`).
+ # properties - An optional array of property names to return in the search results.
+ # If not specified or empty, HubSpot will return the default set of properties.
+ # page_size - The number of results to return per page
+ # (default is 10 for contacts and 100 for everything else).
+ #
+ # Example Usage:
+ # # Full-text search for 'example.org':
+ # props = %w[email firstname lastname]
+ # contacts = Hubspot::Contact.search(query: "example.org", properties: props, page_size: 50)
+ #
+ # # Search for contacts whose email contains 'example.org' and are older than 30:
+ # contacts = Hubspot::Contact.search(
+ # query: { email_contains: 'example.org', age_gt: 30 },
+ # properties: ["email", "firstname", "lastname"],
+ # page_size: 50
+ # )
+ #
+ # Returns [PagedCollection] A paged collection of results that can be iterated over.
def search(query:, properties: [], page_size: 100)
search_body = {}
# Add properties if specified
search_body[:properties] = properties unless properties.empty?
@@ -125,31 +302,49 @@
# Add the page size (passed as limit to the API)
search_body[:limit] = page_size
# Perform the search and return a PagedCollection
PagedCollection.new(
- url: "/crm/v3/objects/#{resource_name}/search",
+ url: "#{api_root}/#{resource_name}/search",
params: search_body,
resource_class: self,
method: :post
)
end
# rubocop:enable Metrics/MethodLength
+ # The root of the api call. Mostly this will be "crm"
+ # but you can override this to account for a different
+ # object hierarchy
+
# Define the resource name based on the class
def resource_name
name = self.name.split('::').last.downcase
if name.end_with?('y')
name.gsub(/y$/, 'ies') # Company -> companies
else
"#{name}s" # Contact -> contacts, Deal -> deals
end
end
+ # List of properties that will always be retrieved
+ # should be overridden in specific resource class
+ def required_properties
+ []
+ end
+
private
+ def api_root
+ '/crm/v3/objects'
+ end
+
+ def list_page_uri
+ "#{api_root}/#{resource_name}"
+ end
+
# Instantiate a single resource object from the response
def instantiate_from_response(response)
data = handle_response(response)
new(data) # Passing full response data to initialize
end
@@ -180,13 +375,40 @@
end
# Default to 'EQ' operator if no suffix is found
{ propertyName: key.to_s, operator: 'EQ' }
end
+
+ # Internal make a list of properties to request from the API
+ # will be merged with any required_properties defined on the class
+ def build_property_list(properties)
+ properties = [] unless properties.is_a?(Array)
+ raise 'Must be an array' unless required_properties.is_a?(Array)
+
+ properties.concat(required_properties).uniq
+ end
end
- # rubocop:disable Ling/MissingSuper
+ # rubocop:disable Lint/MissingSuper
+
+ # Public: Initialize a resouce
+ #
+ # data - [2D Hash, nested Hash] data to initialise the resourse This can be either:
+ # - A Simple 2D Hash, key value pairs of property => value (for the create option)
+ # - A structured hash consisting of { id: <hs_object_id>, properties: {}, ... }
+ # This is the same structure as per the API, and can be rebuilt if you store the id
+ # of the object against your own data
+ #
+ # Example:
+ # attrs = { firstname: 'Luke', lastname: 'Skywalker', email: 'luke@jedi.org' }
+ # contact = Hubspot::Contact.new(attrs)
+ # contact.persisted? # false
+ # contact.save # creates the record in Hubspot
+ # contact.persisted? # true
+ # puts "Contact saved with hubspot id #{contact.id}"
+ #
+ # existing_contact = Hubspot::Contact.new(id: hubspot_id, properties: contact.to_hubspot)
def initialize(data = {})
data.transform_keys!(&:to_s)
@id = extract_id(data)
@properties = {}
@metadata = {}
@@ -194,17 +416,26 @@
initialize_from_api(data)
else
initialize_new_object(data)
end
end
- # rubocop:enable Ling/MissingSuper
+ # rubocop:enable Lint/MissingSuper
+ # Determine the state of the object
+ #
+ # Returns Boolean
def changes?
!@changes.empty?
end
- # Instance methods for update (or save)
+ # Create or Update the resource.
+ # If the resource was already persisted (e.g. it was retrieved from the API)
+ # it will be updated using values from @changes
+ #
+ # If the resource is new (no id) it will be created
+ #
+ # Returns Boolean
def save
if persisted?
self.class.update(@id, @changes).tap do |result|
return false unless result
@@ -214,36 +445,76 @@
else
create_new
end
end
+ # If the resource exists in Hubspot
+ #
+ # Returns Boolean
def persisted?
@id ? true : false
end
- # Update the resource
- def update(params)
+ # Public - Update the resource and persist to the api
+ #
+ # attributes - hash of properties to update in key value pairs
+ #
+ # Example:
+ # contact = Hubspot::Contact.find(hubspot_contact_id)
+ # contact.update(status: 'gold customer', last_contacted_at: Time.now.utc.iso8601)
+ #
+ # Returns Boolean
+ def update(attributes)
raise 'Not able to update as not persisted' unless persisted?
- params.each do |key, value|
- send("#{key}=", value) # This will trigger the @changes tracking via method_missing
- end
+ update_attributes(attributes)
save
end
+ # Public - Update resource attributes
+ #
+ # Does not persist to the api but processes each attribute correctly
+ #
+ # Example:
+ # contact = Hubspot::Contact.find(hubspot_contact_id)
+ # contact.changes? # false
+ # contact.update_attributes(education: 'Graduate', university: 'Life')
+ # contact.education # Graduate
+ # contact.changes? # true
+ # contact.changes # { "education" => "Graduate", "university" => "Life" }
+ #
+ # Returns Hash of changes
+ def update_attributes(attributes)
+ raise ArgumentError, 'must be a hash' unless attributes.is_a?(Hash)
+
+ attributes.each do |key, value|
+ send("#{key}=", value) # This will trigger the @changes tracking via method_missing
+ end
+ end
+
+ # Archive the object in Hubspot
+ #
+ # Example:
+ # company = Hubspot::Company.find(hubspot_company_id)
+ # company.delete
+ #
def delete
self.class.archive(id)
end
alias archive delete
def resource_name
self.class.resource_name
end
# rubocop:disable Metrics/MethodLength
- # Handle dynamic getter and setter methods with method_missing
+
+ # getter: Check the properties and changes hashes to see if the method
+ # being called is a key, and return the corresponding value
+ # setter: If the method ends in "=" persist the value in the changes hash
+ # (when it is different from the corresponding value in properties if set)
def method_missing(method, *args)
method_name = method.to_s
# Handle setters
if method_name.end_with?('=')
@@ -265,13 +536,14 @@
end
# Fallback if the method or attribute is not found
super
end
+
# rubocop:enable Metrics/MethodLength
- # Ensure respond_to_missing? is properly overridden
+ # Ensure respond_to_missing? handles existing keys in the properties anc changes hashes
def respond_to_missing?(method_name, include_private = false)
property_name = method_name.to_s.chomp('=')
@properties.key?(property_name) || @changes.key?(property_name) || super
end
@@ -282,21 +554,27 @@
data['id'] ? data['id'].to_i : nil
end
# Initialize from API response, separating metadata from properties
def initialize_from_api(data)
- @metadata = extract_metadata(data)
- properties_data = data['properties'] || {}
+ if data['properties']
+ @metadata = data.reject { |key, _v| key == 'properties' }
+ handle_properties(data['properties'])
+ else
+ handle_properties(data)
+ end
+ @changes = {}
+ end
+
+ def handle_properties(properties_data)
properties_data.each do |key, value|
if METADATA_FIELDS.include?(key)
@metadata[key] = value
else
@properties[key] = value
end
end
-
- @changes = {}
end
# Initialize a new object (no API response)
def initialize_new_object(data)
@properties = {}