Skip to content

Commit

Permalink
2022 refresh (#6)
Browse files Browse the repository at this point in the history
* Make most specs pass

* Update Hubspot::Association to use v4 API as v1 is buggy

* Run actions on all branches

* Pass secrets

* Test again ruby 3.1 and rails 7.9

* Clean up dead constants

* Add association definition for company-to-company association

* Introduce NotFoundError to differentiate a 404 from any other issue

* Code review
  • Loading branch information
Intrepidd authored Jun 29, 2022
1 parent 97bd450 commit 59f6915
Show file tree
Hide file tree
Showing 288 changed files with 30,475 additions and 257,732 deletions.
1 change: 0 additions & 1 deletion .env.test

This file was deleted.

15 changes: 6 additions & 9 deletions .github/workflows/actions.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
name: tests

on:
pull_request:
branches:
- master
push:
branches:
- master

on: push
jobs:
test:
runs-on: ubuntu-latest
Expand All @@ -16,9 +8,11 @@ jobs:
ruby:
- '2.7.x'
- '3.0.x'
- '3.1.x'
active_support:
- 'active_support_6.0.x'
- 'active_support_6.1.x'
- 'active_support_7.0.x'
steps:
- name: Checkout
uses: actions/checkout@v1
Expand All @@ -34,4 +28,7 @@ jobs:
gem install bundler
bundle install --jobs 4 --retry 3
- name: Test
env:
HUBSPOT_PORTAL_ID: ${{ secrets.HUBSPOT_PORTAL_ID }}
HUBSPOT_HAPI_KEY: ${{ secrets.HUBSPOT_HAPI_KEY }}
run: RUBYOPT='-W:deprecated' bundle exec rspec
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ tmp
*.sublime-workspace

# Ignore local environment variables
/.env
/.env*

# Byebug history
.byebug_history
5 changes: 5 additions & 0 deletions gemfiles/active_support_7.0.x.gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true
source 'https://rubygems.org'

gem "activesupport", "~> 7.0.3"
gemspec path: '../'
132 changes: 79 additions & 53 deletions lib/hubspot/association.rb
Original file line number Diff line number Diff line change
@@ -1,80 +1,106 @@
class Hubspot::Association
COMPANY_TO_CONTACT = 2
DEAL_TO_CONTACT = 3
CONTACT_TO_DEAL = 4
DEAL_TO_COMPANY = 5
COMPANY_TO_DEAL = 6
DEFINITION_TARGET_TO_CLASS = {
2 => Hubspot::Contact,
3 => Hubspot::Contact,
4 => Hubspot::Deal,
5 => Hubspot::Company,
6 => Hubspot::Deal
OBJECT_TARGET_TO_CLASS = {
"Contact" => Hubspot::Contact,
"Deal" => Hubspot::Deal,
"Company" => Hubspot::Company
}.freeze

BATCH_CREATE_PATH = '/crm-associations/v1/associations/create-batch'
BATCH_DELETE_PATH = '/crm-associations/v1/associations/delete-batch'
ASSOCIATIONS_PATH = '/crm-associations/v1/associations/:resource_id/HUBSPOT_DEFINED/:definition_id'
ASSOCIATION_DEFINITIONS = {
"Company" => {
"Contact" => 2,
"Deal" => 6,
"Company" => 13
},
"Deal" => {
"Company" => 5,
"Contact" => 3
},
"Contact" => {
"Deal" => 4
}
}.freeze

class << self
def create(from_id, to_id, definition_id)
batch_create([{ from_id: from_id, to_id: to_id, definition_id: definition_id }])
def create(object_type, object_id, to_object_type, to_object_id)
batch_create(object_type, to_object_type, [{from_id: object_id, to_id: to_object_id}])
end

# Make multiple associations in a single API call
# {https://developers.hubspot.com/docs/methods/crm-associations/batch-associate-objects}
# {https://developers.hubspot.com/docs/api/crm/associations}
# usage:
# Hubspot::Association.batch_create([{ from_id: 1, to_id: 2, definition_id: Hubspot::Association::COMPANY_TO_CONTACT }])
def batch_create(associations)
request = associations.map { |assocation| build_association_body(assocation) }
Hubspot::Connection.put_json(BATCH_CREATE_PATH, params: { no_parse: true }, body: request).success?
# Hubspot::Association.batch_create("Company", "Contact", [{from_id: 1, to_id: 2}]])
def batch_create(from_object_type, to_object_type, associations)
definition_id = ASSOCIATION_DEFINITIONS.dig(from_object_type, to_object_type)
request = { inputs: associations.map { |assocation| build_create_association_body(assocation, definition_id) } }
response = Hubspot::Connection.post_json("/crm/v4/associations/#{from_object_type}/#{to_object_type}/batch/create", params: { no_parse: true }, body: request)
return false if response.parsed_response["errors"].present?

response.success?
end

def delete(from_id, to_id, definition_id)
batch_delete([{from_id: from_id, to_id: to_id, definition_id: definition_id}])
def delete(object_type, object_id, to_object_type, to_object_id)
batch_delete(object_type, to_object_type, [{from_id: object_id, to_id: to_object_id}])
end

# Remove multiple associations in a single API call
# {https://developers.hubspot.com/docs/methods/crm-associations/batch-delete-associations}
# {https://developers.hubspot.com/docs/api/crm/associations}
# usage:
# Hubspot::Association.batch_delete([{ from_id: 1, to_id: 2, definition_id: Hubspot::Association::COMPANY_TO_CONTACT }])
def batch_delete(associations)
request = associations.map { |assocation| build_association_body(assocation) }
Hubspot::Connection.put_json(BATCH_DELETE_PATH, params: { no_parse: true }, body: request).success?
# Hubspot::Association.batch_delete("Company", "Contact", [{ from_id: 1, to_id: 2}])
def batch_delete(from_object_type, to_object_type, associations)
request = { inputs: build_delete_associations_body(associations) }
Hubspot::Connection.post_json("/crm/v4/associations/#{from_object_type}/#{to_object_type}/batch/archive", params: { no_parse: true }, body: request).success?
end

# Retrieve all associated resources given a source (resource_id) and a kind (definition_id)
# Example: if resource_id is a deal, using DEAL_TO_CONTACT will find every contact associated with the deal
# {https://developers.hubspot.com/docs/methods/crm-associations/get-associations}
# Warning: it will make N+M queries, where
# N is the number of PagedCollection requests necessary to get all ids,
# and M is the number of results, each resulting in a find
# usage:
# Hubspot::Association.all(42, Hubspot::Association::DEAL_TO_CONTACT)
def all(resource_id, definition_id)
opts = { resource_id: resource_id, definition_id: definition_id }
klass = DEFINITION_TARGET_TO_CLASS[definition_id]
raise(Hubspot::InvalidParams, 'Definition not supported') unless klass.present?
# Retrieve all associated resources given a source (object_type and object_id) and a relation type (to_object_type)
# {https://developers.hubspot.com/docs/api/crm/associations}
# Warning: it will return at most 1000 objects and make up to 1001 queries
# Hubspot::Association.all("Company", 42, "Contact")
def all(object_type, object_id, to_object_type)
klass = OBJECT_TARGET_TO_CLASS[to_object_type]
raise(Hubspot::InvalidParams, 'Object type not supported') unless klass.present?

collection = Hubspot::PagedCollection.new(opts) do |options, offset, limit|
params = options.merge(offset: offset, limit: limit)
response = Hubspot::Connection.get_json(ASSOCIATIONS_PATH, params)

resources = response['results'].map { |result| klass.find(result) }
[resources, response['offset'], response['has-more']]
end
collection.resources
response = Hubspot::Connection.get_json("/crm/v4/objects/#{object_type}/#{object_id}/associations/#{to_object_type}", {})
response['results'].map { |result| klass.find(result["toObjectId"]) }
end

private

def build_association_body(assocation)
def build_create_association_body(association, definition_id)
{
fromObjectId: assocation[:from_id],
toObjectId: assocation[:to_id],
category: 'HUBSPOT_DEFINED',
definitionId: assocation[:definition_id]
from: {
id: association[:from_id]
},
to: {
id: association[:to_id]
},
types: [
{
associationCategory: "HUBSPOT_DEFINED",
associationTypeId: definition_id
}
]
}
end

def build_delete_associations_body(associations)
normalized_associations = associations.inject({}) do |memo, association|
memo[association[:from_id]] ||= []
memo[association[:from_id]] << association[:to_id]
memo
end

normalized_associations.map do |from_id, to_ids|
{
from: {
id: from_id
},
to: to_ids.map do |to_id|
{
id: to_id
}
end
}
end
end
end
end
4 changes: 2 additions & 2 deletions lib/hubspot/company.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,11 @@ def recently_modified(opts = {})
end

def add_contact(id, contact_id)
Hubspot::Association.create(id, contact_id, Hubspot::Association::COMPANY_TO_CONTACT)
Hubspot::Association.create("Company", id, "Contact", contact_id)
end

def remove_contact(id, contact_id)
Hubspot::Association.delete(id, contact_id, Hubspot::Association::COMPANY_TO_CONTACT)
Hubspot::Association.delete("Company", id, "Contact", contact_id)
end

def batch_update(companies, opts = {})
Expand Down
26 changes: 12 additions & 14 deletions lib/hubspot/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def get_json(path, opts)
url = generate_url(path, opts)
response = get(url, format: :json, read_timeout: read_timeout(opts), open_timeout: open_timeout(opts))
log_request_and_response url, response
handle_response(response)
handle_response(response).parsed_response
end

def post_json(path, opts)
Expand All @@ -24,9 +24,9 @@ def post_json(path, opts)
)

log_request_and_response url, response, opts[:body]
raise(Hubspot::RequestError.new(response)) unless response.success?

no_parse ? response : response.parsed_response
handle_response(response).yield_self do |r|
no_parse ? r : r.parsed_response
end
end

def put_json(path, options)
Expand All @@ -43,17 +43,16 @@ def put_json(path, options)
)

log_request_and_response(url, response, options[:body])
raise(Hubspot::RequestError.new(response)) unless response.success?

no_parse ? response : response.parsed_response
handle_response(response).yield_self do |r|
no_parse ? r : r.parsed_response
end
end

def delete_json(path, opts)
url = generate_url(path, opts)
response = delete(url, format: :json, read_timeout: read_timeout(opts), open_timeout: open_timeout(opts))
log_request_and_response url, response, opts[:body]
raise(Hubspot::RequestError.new(response)) unless response.success?
response
handle_response(response)
end

protected
Expand All @@ -67,11 +66,10 @@ def open_timeout(opts = {})
end

def handle_response(response)
if response.success?
response.parsed_response
else
raise(Hubspot::RequestError.new(response))
end
return response if response.success?

raise(Hubspot::NotFoundError.new(response)) if response.not_found?
raise(Hubspot::RequestError.new(response))
end

def log_request_and_response(uri, response, body=nil)
Expand Down
7 changes: 0 additions & 7 deletions lib/hubspot/contact_list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ class ContactList
RECENT_CONTACTS_PATH = LIST_PATH + '/contacts/recent'
ADD_CONTACT_PATH = LIST_PATH + '/add'
REMOVE_CONTACT_PATH = LIST_PATH + '/remove'
REFRESH_PATH = LIST_PATH + '/refresh'

class << self
# {http://developers.hubspot.com/docs/methods/lists/create_list}
Expand Down Expand Up @@ -92,12 +91,6 @@ def contacts(opts={})
end
end

# {http://developers.hubspot.com/docs/methods/lists/refresh_list}
def refresh
response = Hubspot::Connection.post_json(REFRESH_PATH, params: { list_id: @id, no_parse: true }, body: {})
response.code == 204
end

# {http://developers.hubspot.com/docs/methods/lists/add_contact_to_list}
def add(contacts)
contact_ids = [contacts].flatten.uniq.compact.map(&:id)
Expand Down
48 changes: 34 additions & 14 deletions lib/hubspot/deal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,27 +64,47 @@ def update!(id, properties = {})
# Usage
# Hubspot::Deal.associate!(45146940, [32], [52])
def associate!(deal_id, company_ids=[], vids=[])
associations = company_ids.map do |id|
{ from_id: deal_id, to_id: id, definition_id: Hubspot::Association::DEAL_TO_COMPANY }
company_associations = associations = company_ids.map do |id|
{ from_id: deal_id, to_id: id }
end
associations += vids.map do |id|
{ from_id: deal_id, to_id: id, definition_id: Hubspot::Association::DEAL_TO_CONTACT }

contact_associations = vids.map do |id|
{ from_id: deal_id, to_id: id}
end

results = []
if company_associations.any?
results << HubSpot::Association.batch_create("Deal", "Company", company_associations)
end
Hubspot::Association.batch_create(associations)
if contact_associations.any?
results << HubSpot::Association.batch_create("Deal", "Contact", contact_associations)
end

results.all?
end

# Didssociate a deal with a contact or company
# {https://developers.hubspot.com/docs/methods/deals/delete_association}
# Usage
# Hubspot::Deal.dissociate!(45146940, [32], [52])
def dissociate!(deal_id, company_ids=[], vids=[])
associations = company_ids.map do |id|
{ from_id: deal_id, to_id: id, definition_id: Hubspot::Association::DEAL_TO_COMPANY }
company_associations = company_ids.map do |id|
{ from_id: deal_id, to_id: id }
end
associations += vids.map do |id|
{ from_id: deal_id, to_id: id, definition_id: Hubspot::Association::DEAL_TO_CONTACT }

contact_associations = vids.map do |id|
{ from_id: deal_id, to_id: id }
end

results = []
if company_associations.any?
results << HubSpot::Association.batch_delete("Deal", "Company", company_associations)
end
Hubspot::Association.batch_delete(associations)
if contact_associations.any?
results << HubSpot::Association.batch_delete("Deal", "Contact", contact_associations)
end

results.all?
end

def find(deal_id)
Expand Down Expand Up @@ -134,12 +154,12 @@ def find_by_contact(contact)
# @param object [Hubspot::Contact || Hubspot::Company] a contact or company
# @return [Array] Array of Hubspot::Deal records
def find_by_association(object)
definition = case object
when Hubspot::Company then Hubspot::Association::COMPANY_TO_DEAL
when Hubspot::Contact then Hubspot::Association::CONTACT_TO_DEAL
to_object_type = case object
when Hubspot::Company then "Company"
when Hubspot::Contact then "Contact"
else raise(Hubspot::InvalidParams, 'Instance type not supported')
end
Hubspot::Association.all(object.id, definition)
Hubspot::Association.all(to_object_type, object.id, "Deal")
end
end

Expand Down
2 changes: 2 additions & 0 deletions lib/hubspot/exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ def initialize(response, message=nil)
end
end

class NotFoundError < RequestError; end

class ConfigurationError < StandardError; end
class MissingInterpolation < StandardError; end
class ContactExistsError < RequestError; end
Expand Down
2 changes: 1 addition & 1 deletion lib/hubspot/properties.rb
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def valid_property_params(params)

def valid_group_params(params)
return {} if params.blank?
result = params.slice(*PROPERTY_SPECS[:group_field_names])
result = params.with_indifferent_access.slice(*PROPERTY_SPECS[:group_field_names])
result['properties'] = valid_property_params(result['properties']) unless result['properties'].blank?
result
end
Expand Down
Loading

0 comments on commit 59f6915

Please sign in to comment.