# frozen_string_literal: true # Methods for working with instances of global to soql objects, not global overall # See https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_sobject_describe.htm module SoqlGlobalObjectData # @return [Hash] List of ids to delete when deleting a particular object attr_accessor :ids_to_delete # Override to handle removing dependent records def remove_dependent_records(_id); end # Return key and value to look up for a provided hash # @return [Array] Array with [column_to_lookup, value_to_look_for] def extract_lookup(lookup) raise 'Need to pass to Soql object key value pair to look up by' unless lookup.first.is_a? Array lookup_key, lookup_value = lookup.first raise 'Need to set lookup_key' unless lookup_key [lookup_key, lookup_value] end # Url enconding needs to be used when searching for special characters (+ => '%2B') # (see https://www.w3schools.com/tags/ref_urlencode.asp) # @param [String] soql_query String representing SOQL query # @param [Boolean] wait Whether to wait for record if no result returned # @example Find an account with test organisation name # my_query = "SELECT Name from Account WHERE Name = 'TEST Org 001'" # query my_query # => "SELECT+Name+from+Account+WHERE+Name+=+'TEST+Org+001'" # @return [self] Exchange object from which JSON response can be obtained (i.e, with exchange.response) def query(soql_query, wait: false) rest_query = soql_query.gsub('%', '%25').gsub('+', '%2B').tr(' ', '+') if wait new("SOQL Query: #{soql_query}", method: :get, suburl: "query/?q=#{rest_query}").until(timeout: 20, interval: 1) do response.body.include? '"url"' # Could be waiting for element to be created end else new("SOQL Query: #{soql_query}", method: :get, suburl: "query/?q=#{rest_query}") end end # @param [Hash] lookup Hash representing look up performed # @param [String] url Url to get def data_from_url(url, lookup) new("Id at #{url}", method: :get, suburl: url.split("v#{SoqlHandler.api_version}/").last) rescue NoElementAtPath raise NoElementAtPath, "No result found for #{lookup} under user #{LeapSalesforce.api_user}" end # For dates (ending with .000Z), query is always greater than # @param [Hash] lookup Hash to look up values according to # @return [String] SOQL query to filter results def soql_lookup_filter(lookup) limit = lookup.delete(:limit) conditional = '' lookup.each do |key, value| conditional_term = conditional.empty? ? 'WHERE' : 'AND' key_used = map_key key conditional += "#{conditional_term} #{key_used} #{condition_for(value)} " end query = conditional + 'ORDER BY CreatedDate DESC NULLS FIRST' query += " LIMIT #{limit}" if limit query end # Find the data for a single SoqlObject using the calling class's table. # Will get the latest created date. # @example Get a contact where LastName is 'Bob' using backend name # Contact.find(LastName: 'Bob') # @example Get a contact that includes 'Test' in their first name (using ruby accessor name) # Contact.find(first_name: '~%Test%') # @example Get a contact created 10 days ago # Contact.find CreatedDate: "<#{10.days.ago}" # @param [Hash] lookup Key value pair unique to Salesforce to query for # @option lookup [Boolean] :teardown Whether to remove id after scenario finished # @return [self] Instance of itself storing reference to found object def find(lookup) teardown = lookup.delete(:teardown) wait = lookup.delete(:wait) || false SoqlHandler.new("Query on #{self}").use instance_to_get = if lookup.key? :Id new("Lookup id: #{lookup[:Id]}", method: :get, suburl: "sobjects/#{soql_object_name}/#{lookup[:Id]}") else initial_query = query "SELECT Id FROM #{soql_object_name} #{soql_lookup_filter(lookup)}", wait: wait data_from_url initial_query['$..url'], lookup end SoqlData.ids_to_delete[self] = instance_to_get[:id] if teardown instance_to_get end # @deprecated # Get details of itself by searching for it's id # Store response within itself # @return [Exchange] Exchange with details of data def get(lookup) LeapSalesforce.logger.warn "Method 'get' called when it is deprecated" \ " from #{caller_locations[0]}" find(lookup) end # @return [self] SoqlData object that is the result of looking up id based on lookup criteria def lookup_id(lookup) teardown = lookup.delete(:teardown) SoqlHandler.new("Query on #{self}").use result = query "SELECT Id FROM #{soql_object_name} #{soql_lookup_filter(lookup)}", wait: false SoqlData.ids_to_delete[self] = id if teardown result end # @return [String] Id that matches filter def id_where(lookup) lookup_id(lookup).id end # @return [Boolean] Whether any result for lookup def any_where?(lookup) lookup_id(lookup)[:totalSize] != 0 end # Perform the code in the block for all the ids matching a query. # If no block, return a list of objects # @param [Hash] lookup Key value pair unique to Salesforce to query for # @yield [id] Perform block for each id returned. The 'id' parameter in a block represents an id matching the query # @return [Array] List of ids matching criteria. Only used if no block given def each_id_with(lookup) lookup[:limit] ||= nil # Don't limit results returned SoqlHandler.new("Each Id where #{self}").use results = query "SELECT Id FROM #{soql_object_name} #{soql_lookup_filter(lookup)}", wait: false ids = results.ids if block_given? ids.each { |id| yield(id) } else ids end end # @param [Hash] lookup Key value pair unique to Salesforce to query for # @return [Array] List of Soql objects matching criteria def each_with(lookup) ids = each_id_with lookup ids.collect { |id| find(Id: id) } end # Remove all ids from table that match lookup criteria # @param [Hash] lookup Key value pair unique to Salesforce to query for def delete_ids_with(lookup) each_id_with(lookup, &method(:delete)) end # Remove object from Salesforce with provided id # @param [String] id Id of element to update # @param [Hash] data Key value pairs with data to update # @return [self] SoqlData object representing result of API update call def update(id, data) must_pass = data.delete(:must_pass) data = data.transform_values do |value| value.is_a?(Time) ? value.salesforce_format : value end data.transform_keys! { |key| map_key(key) } # Map keys to valid field names SoqlHandler.new("Update #{id}").use update = new("Update #{self}, #{id} with '#{data}'", method: :patch, suburl: "sobjects/#{soql_object_name}/#{id}", body: data) update.call return update unless must_pass successful? update end # Remove object from Salesforce with provided id # @example Delete a contact with a specified id, failing if the delete fails # Contact.delete '0032v00002rgv2pAAA', must_pass: true # # @param [String] id Id of element to remove # @param [Boolean] must_pass Whether to raise exception if call is not successful # @return [self] Exchange object making delete call def delete(id, must_pass: false) SoqlData.ids_to_delete.reject! { |table, id_to_remove| table == self && id_to_remove == id } # Remove id from list to delete remove_dependent_records(id) SoqlHandler.new("Delete #{id}").use delete = new("SOQL Delete #{id}", method: :delete, suburl: "sobjects/#{soql_object_name}/#{id}") delete.call return delete unless must_pass delete.successful? delete end # rubocop:disable Metrics/MethodLength # rubocop:disable Metrics/AbcSize def soql_element(name, backend_name) # Either set the element (if creating a new record) or update the object # @todo: ensure next time an element is retrieved it's not from a cached value # @param [String] new_value Value to update record to define_method("#{name}=") do |new_value| if @response @response = update(backend_name => new_value).response else self[backend_name] = new_value.class < SoqlData ? new_value.id : new_value end end # @return [String] Value of backend name define_method name.to_s do begin self[backend_name] rescue NoElementAtPath raise diagnose_error if error_message? # TODO: Response here should not be necessary. Only find is @response = find.response self[backend_name] end end # @return [String] Name of backend name for element define_method("#{name}_element") { backend_name } end # rubocop:enable Metrics/MethodLength # rubocop:enable Metrics/AbcSize # Map key to a field name if used directly or field defined through 'soql_element' # @param [Symbol, String] key Key to map to Table field name # @return [String] Field name of Salesforce entity to use def map_key(key) if field_names.include? key.to_s key.to_s else return new.send("#{key}_element") if new.respond_to?("#{key}_element") raise LeapSalesforce::RequestError, "#{key} not in #{self}. " \ " Must be one of #{field_names} or a field name described in" \ " #{self}::FieldNames" end end # Returns SOQL condition for value passed to be used in SOQL query # @example Greater than or equal to yesterday's date # condition_for("<=#{Time.mktime(2019,1,1)}") # => '<= 2019-01-01T00:00:00.000Z' # @param [String] value Value to search for. Certain characters modify this to be more of a complex # check # @return [String] Condition criteria to match value using SOQL def condition_for(value) operator, value = case value[0] when '>', '<' then extract_comparator(value) when '~' then ['LIKE', value[1..-1]] else return "= '#{value}'" unless value.type_of_time? return "= #{value.to_zulu_date_string}" end return "#{operator} #{value.to_zulu_date_string}" if value.type_of_time? "#{operator} '#{value}'" end # @param [String] value Value to extract comparator for # @return [Array] Extract of >, >, >= from string and rest of string def extract_comparator(value) if value[1] == '=' [value[0..1], value[2..-1]] else [value[0], value[1..-1]] end end end