lib/hubspot/resource.rb in ruby_hubspot_api-0.3.2 vs lib/hubspot/resource.rb in ruby_hubspot_api-0.3.3

- old
+ new

@@ -1,10 +1,11 @@ # frozen_string_literal: true require_relative './api_client' require_relative './paged_collection' require_relative './paged_batch' +require_relative './resource_filter' module Hubspot # rubocop:disable Metrics/ClassLength # HubSpot Resource Base Class @@ -24,10 +25,11 @@ # company = Hubspot::Company.create(name: "Acme Corp") # company.id.nil? # false # class Resource < ApiClient METADATA_FIELDS = %w[createdate hs_object_id lastmodifieddate].freeze + extend ResourceFilter::FilterGroupMethods # Allow read/write access to id, properties, changes and metadata # the id of the object in hubspot attr_accessor :id @@ -40,10 +42,66 @@ # any other data sent from the api about the resource attr_accessor :metadata class << self + # Return a paged_collection - similar to an ActiveRecord Relation + # + # Example: + # contacts_collection = Hubspot::Contact.all + # <PagedCollection> + # + # contacts_collection.where(email_contains: 'hubspot.com') + # <PagedCollection, @params={:filterGroups=> .... + def all + PagedCollection.new( + url: "#{api_root}/#{resource_name}/search", + params: {}, + resource_class: self, + method: :post, + results_param: results_param + ) + end + + # Filter resources - allows chaining + # + # This method allows searching for resources by passing a hash with special suffixes + # on the keys to define different comparison operators. + # + # 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. + # + # If value is an array the 'IN' operator will be used + # + # Otherwise if no suffix is provided, the default comparison is equality (`EQ`). + # + # If no value is provided, or is empty the NOT_HAS_PROPERTY operator will be used + # + # filters - [Hash] The query for searching. + # each key represents a property and may have suffixes for the comparison + # (e.g., `{ email_contains: 'example.org', age_gt: 30 }`). + + # Example: + # + # big_companies = Hubspot:Company.where(number_of_employees_gte: 100 ) + # live_contacts = Hubspot::Contact.where(hs_lead_status: %w[NEW OPEN IN_PROGRESS]) + # + # Returns a PagedCollection + def where(filters = {}) + all.where!(filters) + end + + # Select which properties to return from the api - allows chaining + def select(*properties) + all.select(*properties) + end + # Find a resource by ID and return an instance of the class # # id - [Integer] The ID (or hs_object_id) of the resource to fetch. # properties - an array of property names to fetch in the result # @@ -51,36 +109,39 @@ # contact = Hubspot::Contact.find(1) # contact = Hubspot::Contact.find(1, properties: %w[email firstname lastname custom_field]) # # 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 || {}) + response = response_for_find_by_id(id, properties: properties) + return if response.not_found? + instantiate_from_response(response) end + def find!(id, properties: nil) + response = response_for_find_by_id(id, properties: properties) + 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) + # contact = Hubspot::Contact.find_by("email", "john@example.com", properties: properties) # # Returns An instance of the resource. - def find_by(property, value, properties = nil) - params = { idProperty: property } + def find_by(property, value, properties: nil) + response = response_for_find_by_property(property, value, properties: properties) + instantiate_from_response(response) unless response.not_found? + end - all_properties = build_property_list(properties) - params[:properties] = all_properties unless all_properties.empty? - - response = get("#{api_root}/#{resource_name}/#{value}", query: params) + def find_by!(property, value, properties: nil) + response = response_for_find_by_property(property, value, properties: properties) instantiate_from_response(response) end # Creates a new resource with the given parameters. # @@ -229,21 +290,10 @@ # Returns [Hubspot::Property] A hubspot property def property(property_name) properties.detect { |prop| prop.name == property_name } end - # Simplified search interface - OPERATOR_MAP = { - '_contains' => 'CONTAINS_TOKEN', - '_gt' => 'GT', - '_lt' => 'LT', - '_gte' => 'GTE', - '_lte' => 'LTE', - '_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 @@ -285,15 +335,15 @@ # 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) + def search(query, properties: [], page_size: 200) search_body = {} # Add properties if specified - search_body[:properties] = properties unless properties.empty? + search_body[:properties] = build_property_list(properties) unless properties.empty? # Handle the query using case-when for RuboCop compliance case query when String search_body[:query] = query @@ -309,11 +359,12 @@ # Perform the search and return a PagedCollection PagedCollection.new( url: "#{api_root}/#{resource_name}/search", params: search_body, resource_class: self, - method: :post + method: :post, + results_param: results_param ) end # rubocop:enable Metrics/MethodLength # Define the resource name based on the class @@ -339,51 +390,43 @@ # object hierarchy def api_root '/crm/v3/objects' end + # In the response from the api the resources returned in this key + def results_param + 'results' + 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 - - # Convert simple filters to HubSpot's filterGroups format - def build_filter_groups(filters) - filter_groups = [{ filters: [] }] - - filters.each do |key, value| - filter = extract_property_and_operator(key, value) - value_key = value.is_a?(Array) ? :values : :value - filter[value_key] = value unless value.blank? - filter_groups.first[:filters] << filter + def response_for_find_by_id(id, properties: nil) + all_properties = build_property_list(properties) + if all_properties.is_a?(Array) && !all_properties.empty? + params = { query: { properties: all_properties } } end - filter_groups + get("#{api_root}/#{resource_name}/#{id}", params || {}) end - # Extract property name and operator from the key - def extract_property_and_operator(key, value) - return { propertyName: key.to_s, operator: 'NOT_HAS_PROPERTY' } if value.blank? + def response_for_find_by_property(property, value, properties: nil) + params = { idProperty: property } - OPERATOR_MAP.each do |suffix, hubspot_operator| - if key.to_s.end_with?(suffix) - return { - propertyName: key.to_s.sub(suffix, ''), - operator: hubspot_operator - } - end - end + all_properties = build_property_list(properties) + params[:properties] = all_properties unless all_properties.empty? - # Default to 'EQ' operator if no suffix is found - { propertyName: key.to_s, operator: 'EQ' } + get("#{api_root}/#{resource_name}/#{value}", query: params) 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 + # 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) @@ -416,11 +459,11 @@ # 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.delete('id')) + @id = extract_id(data.delete(api_id_field)) @properties = {} @metadata = {} if @id initialize_from_api(data) else @@ -567,10 +610,14 @@ end end private + def api_id_field + 'id' + end + # Extract ID from data and convert to integer def extract_id(id) id&.to_i end @@ -612,12 +659,16 @@ @properties[attribute] if @properties.key?(attribute) end end - # allows overwriting in other resource classes def metadata_field?(key) - METADATA_FIELDS.include?(key) + metadata_fields.include?(key) + end + + # allows overwriting in other resource classes + def metadata_fields + METADATA_FIELDS end # Initialize a new object (no API response) def initialize_new_object(data) @properties = {}