=begin
ActiveSalesforce
Copyright 2006 Doug Chasman
2010 updated by Raymond Gao
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
=end
require 'thread'
require 'benchmark'
require 'active_record'
require 'active_record/connection_adapters/abstract_adapter'
# replaced hardlink to 'rforce' gem as a pre-requisite, allowing parallel evolution of both gem.
require 'rforce'
#require File.dirname(__FILE__) + '/../../rforce'
require File.dirname(__FILE__) + '/column_definition'
require File.dirname(__FILE__) + '/relationship_definition'
require File.dirname(__FILE__) + '/boxcar_command'
require File.dirname(__FILE__) + '/entity_definition'
require File.dirname(__FILE__) + '/asf_active_record'
require File.dirname(__FILE__) + '/id_resolver'
require File.dirname(__FILE__) + '/sid_authentication_filter'
require File.dirname(__FILE__) + '/recording_binding'
require File.dirname(__FILE__) + '/result_array'
# See http://www.rubular.com/ for Ruby Regular Expression
# See http://api.rubyonrails.org/classes/ActiveRecord/Base.html for documentation
# ActiveRecord
# To Understand how to write an adapter, see:
# http://ar.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/AbstractAdapter.html
# and
# http://rails.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/AbstractAdapter.html
module ActiveRecord
# Overrides the ActiveRecord::Base class to provide Salesforce Web Services services
# Particularly important are 1) getting a 'binding' and 2) 'api_version'
class Base
@@cache = {}
def self.debug(msg)
logger.debug(msg) if logger
end
def self.flush_connections()
@@cache = {}
end
# Establishes a connection to the database that's used by all Active Record objects.
def self.activesalesforce_connection(config) # :nodoc:
debug("\nUsing ActiveSalesforce connection\n")
# Default to production system using 20.0 API
url = config[:url]
url = "https://www.salesforce.com" unless url
#Take the API version from the Config file e.g. 'database.yml' -> 'salesforce-default-realm'
api_version = config[:api_version] ? config[:api_version] : "20.0"
uri = URI.parse(url)
#uri.path = "/services/Soap/u/20.0"
uri.path = "/services/Soap/u/" + (api_version).to_s
url = uri.to_s
sid = config[:sid]
client_id = config[:client_id]
username = config[:username].to_s
password = config[:password].to_s
# Recording/playback support
recording_source = config[:recording_source]
recording = config[:recording]
if recording_source
recording_source = File.open(recording_source, recording ? "w" : "r")
binding = ActiveSalesforce::RecordingBinding.new(url, nil, recording != nil, recording_source, logger)
binding.client_id = client_id if client_id
binding.login(username, password) unless sid
end
# Check to insure that the second to last path component is a 'u' for Partner API
raise ActiveSalesforce::ASFError.new(logger, "Invalid salesforce server url '#{url}', must be a valid Parter API URL") unless url.match(/\/u\//mi)
if sid
binding = @@cache["sid=#{sid}"] unless binding
unless binding
debug("Establishing new connection for [sid='#{sid}']")
binding = RForce::Binding.new(url, sid)
@@cache["sid=#{sid}"] = binding
debug("Created new connection for [sid='#{sid}']")
else
debug("Reused existing connection for [sid='#{sid}']")
end
else
binding = @@cache["#{url}.#{username}.#{password}.#{client_id}"] unless binding
unless binding
debug("Establishing new connection for ['#{url}', '#{username}, '#{client_id}'")
seconds = Benchmark.realtime {
binding = RForce::Binding.new(url, sid)
binding.login(username, password)
@@cache["#{url}.#{username}.#{password}.#{client_id}"] = binding
}
debug("Created new connection for ['#{url}', '#{username}', '#{client_id}'] in #{seconds} seconds")
end
end
ConnectionAdapters::SalesforceAdapter.new(binding, logger, config)
end
end
module ConnectionAdapters
# Inherit from ConnectionAdapters::AbstractAdapter see:
# http://rails.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/AbstractAdapter.html
class SalesforceAdapter < AbstractAdapter
include StringHelper
MAX_BOXCAR_SIZE = 200
attr_accessor :batch_size
attr_reader :entity_def_map, :keyprefix_to_entity_def_map, :config, :class_to_entity_map
# Create a new instance of the connection adapter
def initialize(connection, logger, config)
super(connection, logger)
@connection_options = nil
@config = config
@entity_def_map = {}
@keyprefix_to_entity_def_map = {}
@command_boxcar = nil
@class_to_entity_map = {}
end
def set_class_for_entity(klass, entity_name)
debug("Setting @class_to_entity_map['#{entity_name.upcase}'] = #{klass} for connection #{self}")
@class_to_entity_map[entity_name.upcase] = klass
end
# returns the binding associated with the current adapter
# e.g Salesforce::User.first.connection.binding -> returns the binding.
def binding
@connection
end
#Sets the adapter name to "ActiveSalesforce"
def adapter_name #:nodoc:
'ActiveSalesforce'
end
def supports_migrations? #:nodoc:
false
end
# For Silent-e, added 'tables' method to solve ARel problem
def tables(name = nil) #:nodoc:
@connection.describeGlobal({}).describeGlobalResponse.result.types
end
def table_exists?(table_name)
true
end
#-- QUOTING ==================================================
def quote(value, column = nil)
case value
when NilClass then quoted_value = "NULL"
when TrueClass then quoted_value = "TRUE"
when FalseClass then quoted_value = "FALSE"
when Float, Fixnum, Bignum then quoted_value = "'#{value.to_s}'"
else quoted_value = super(value, column)
end
quoted_value
end
#-- CONNECTION MANAGEMENT ====================================
# Set the connection state to active
def active?
true
end
# Overrides the basic method for ActiveRecord::ConnectionAdapters::AbstractAdapter
def reconnect!
connect
end
# TRANSACTIOn SUPPORT (Boxcarring really because the salesforce.com api does not support transactions)
# Override AbstractAdapter's transaction method to implement
# per-connection support for nested transactions that do not commit until
# the outermost transaction is finished. ActiveRecord provides support
# for this, but does not distinguish between database connections, which
# prevents opening transactions to two different databases at the same
# time.
def transaction_with_nesting_support(*args, &block)
open = Thread.current["open_transactions_for_#{self.class.name.underscore}"] ||= 0
Thread.current["open_transactions_for_#{self.class.name.underscore}"] = open + 1
begin
transaction_without_nesting_support(&block)
ensure
Thread.current["open_transactions_for_#{self.class.name.underscore}"] -= 1
end
end
alias_method_chain :transaction, :nesting_support
# Begins the transaction (and turns off auto-committing).
def begin_db_transaction
log('Opening boxcar', 'begin_db_transaction()')
@command_boxcar = []
end
# Calls RForce::Binding -> method_missing -> call_remote method, args[0] methods
def send_commands(commands)
# Send the boxcar'ed command set
verb = commands[0].verb
args = []
commands.each do |command|
command.args.each { |arg| args << arg }
end
response = @connection.send(verb, args)
result = get_result(response, verb)
result = [ result ] unless result.is_a?(Array)
errors = []
result.each_with_index do |r, n|
success = r[:success] == "true"
# Give each command a chance to process its own result
command = commands[n]
command.after_execute(r)
# Handle the set of failures
errors << r[:errors] unless r[:success] == "true"
end
unless errors.empty?
message = errors.join("\n")
fault = (errors.map { |error| error[:message] }).join("\n")
raise ActiveSalesforce::ASFError.new(@logger, message, fault)
end
result
end
# Commits the transaction (and turns on auto-committing).
def commit_db_transaction()
log("Committing boxcar with #{@command_boxcar.length} commands", 'commit_db_transaction()')
previous_command = nil
commands = []
@command_boxcar.each do |command|
if commands.length >= MAX_BOXCAR_SIZE or (previous_command and (command.verb != previous_command.verb))
send_commands(commands)
commands = []
previous_command = nil
else
commands << command
previous_command = command
end
end
# Discard the command boxcar
@command_boxcar = nil
# Finish off the partial boxcar
send_commands(commands) unless commands.empty?
end
# Rolls back the transaction (and turns on auto-committing). Must be
# done if the transaction block raises an exception or returns false.
def rollback_db_transaction()
log('Rolling back boxcar', 'rollback_db_transaction()')
@command_boxcar = nil
end
# DATABASE STATEMENTS ======================================
# This method is very important. Pretty much all the 'find' methods call
# it. This is the place to toggle linebreak for debugging purpose.
# In essence, your finder method calls ActiveRecord, which generates a 'sql'
# And, this methods turn it into a 'soql' statement, and use Rforce to call
# Salesforce and gets a result.
# So, if you are having problems, make sure you check 'soql' and toggle the
# 'result' object.
def select_all(sql, name = nil) #:nodoc:
# silent-e's solution for single quote escape
# fix the single quote escape method in WHERE condition expression
sql = fix_single_quote_in_where(sql)
# Arel adds the class to the selection - we do not want this i.e...
# SELECT contacts.* FROM => SELECT * FROM
sql = sql.gsub(/SELECT\s+[^\(][A-Z]+\./mi," ")
raw_table_name = sql.match(/FROM (\w+)/mi)[1]
table_name, columns, entity_def = lookup(raw_table_name)
column_names = columns.map { |column| column.api_name }
# Check for SELECT COUNT(*) FROM query
# Rails 1.1
selectCountMatch = sql.match(/SELECT\s+COUNT\(\*\)\s+AS\s+count_all\s+FROM/mi)
# Rails 1.0
selectCountMatch = sql.match(/SELECT\s+COUNT\(\*\)\s+FROM/mi) unless selectCountMatch
if selectCountMatch
soql = "SELECT COUNT() FROM#{selectCountMatch.post_match}"
else
if sql.match(/SELECT\s+\*\s+FROM/mi)
# Always convert SELECT * to select all columns (required for the AR attributes mechanism to work correctly)
soql = sql.sub(/SELECT .+ FROM/mi, "SELECT #{column_names.join(', ')} FROM")
else
soql = sql
end
end
soql.sub!(/\s+FROM\s+\w+/mi, " FROM #{entity_def.api_name}")
if selectCountMatch
query_result = get_result(@connection.query(:queryString => soql), :query)
return [{ :count => query_result[:size] }]
end
# Look for a LIMIT clause
limit = extract_sql_modifier(soql, "LIMIT")
limit = MAX_BOXCAR_SIZE unless limit
# Look for an OFFSET clause
offset = extract_sql_modifier(soql, "OFFSET")
# Fixup column references to use api names
columns = entity_def.column_name_to_column
soql.gsub!(/((?:\w+\.)?\w+)(?=\s*(?:=|!=|<|>|<=|>=|like)\s*(?:'[^']*'|NULL|TRUE|FALSE))/mi) do |column_name|
# strip away any table alias
column_name.sub!(/\w+\./, '')
column = columns[column_name]
raise ActiveSalesforce::ASFError.new(@logger, "Column not found for #{column_name} - in method!") unless column
column.api_name
end
# Update table name references
soql.sub!(/#{raw_table_name}\./mi, "#{entity_def.api_name}.")
@connection.batch_size = @batch_size if @batch_size
@batch_size = nil
query_result = get_result(@connection.query(:queryString => soql), :query)
result = ActiveSalesforce::ResultArray.new(query_result[:size].to_i)
return result unless query_result[:records]
add_rows(entity_def, query_result, result, limit)
while ((query_result[:done].casecmp("true") != 0) and (result.size < limit or limit == 0))
# Now queryMore
locator = query_result[:queryLocator];
query_result = get_result(@connection.queryMore(:queryLocator => locator), :queryMore)
add_rows(entity_def, query_result, result, limit)
end
result
end
# This methods constructs an array of result objects to be returned to your
# ActiveRecord's finder method.
def add_rows(entity_def, query_result, result, limit)
records = query_result[:records]
records = [ records ] unless records.is_a?(Array)
records.each do |record|
row = {}
record.each do |name, value|
if name != :type
# Ids may be returned in an array with 2 duplicate entries...
value = value[0] if name == :Id && value.is_a?(Array)
column = entity_def.api_name_to_column[name.to_s]
attribute_name = column.name
if column.type == :boolean
row[attribute_name] = (value.casecmp("true") == 0)
else
row[attribute_name] = value
end
end
end
result << row
break if result.size >= limit and limit != 0
end
end
# Calls the 'select_all' method, but limits the result to return only a
# single row.
def select_one(sql, name = nil) #:nodoc:
self.batch_size = 1
result = select_all(sql, name)
result.nil? ? nil : result.first
end
# Insert object into Salesforce DB
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
log(sql, name) {
# Convert sql to sobject
table_name, columns, entity_def = lookup(sql.match(/INSERT\s+INTO\s+(\w+)\s+/mi)[1])
columns = entity_def.column_name_to_column
# Extract array of column names
names = sql.match(/\((.+)\)\s+VALUES/mi)[1].scan(/\w+/mi)
# Extract arrays of values
values = sql.match(/VALUES\s*\((.+)\)/mi)[1]
values = values.scan(/(NULL|TRUE|FALSE|'(?:(?:[^']|'')*)'),*/mi).flatten
values.map! { |v| v.first == "'" ? v.slice(1, v.length - 2) : v == "NULL" ? nil : v }
fields = get_fields(columns, names, values, :createable)
sobject = create_sobject(entity_def.api_name, nil, fields)
# Track the id to be able to update it when the create() is actually executed
id = String.new
queue_command ActiveSalesforce::BoxcarCommand::Insert.new(self, sobject, id)
id
}
end
# Updates object(s) via SQL. It is an adapter method to be used by ActiveRecord
def update(sql, name = nil) #:nodoc:
# From silent-e, solution for ARel
sql = sql.gsub(/WHERE\s+\([A-Z]+\./mi,"WHERE ")
#log(sql, name) {
# Convert sql to sobject
table_name, columns, entity_def = lookup(sql.match(/UPDATE\s+(\w+)\s+/mi)[1])
columns = entity_def.column_name_to_column
match = sql.match(/SET\s+(.+)\s+WHERE/mi)[1]
names = match.scan(/(\w+)\s*=\s*(?:'|NULL|TRUE|FALSE)/mi).flatten
values = match.scan(/=\s*(NULL|TRUE|FALSE|'(?:(?:[^']|'')*)'),*/mi).flatten
values.map! { |v| v.first == "'" ? v.slice(1, v.length - 2) : v == "NULL" ? nil : v }
fields = get_fields(columns, names, values, :updateable)
null_fields = get_null_fields(columns, names, values, :updateable)
ids = sql.match(/WHERE\s+id\s*=\s*'(\w+)'/mi)
return if ids.nil?
id = ids[1]
sobject = create_sobject(entity_def.api_name, id, fields, null_fields)
queue_command ActiveSalesforce::BoxcarCommand::Update.new(self, sobject)
#}
end
# Delete object(s) from Salesforce
def delete(sql, name = nil)
log(sql, name) {
# Extract the id
match = sql.match(/WHERE\s+id\s*=\s*'(\w+)'/mi)
if match
ids = [ match[1] ]
else
# Check for the form (id IN ('x', 'y'))
match = sql.match(/WHERE\s+\(\s*id\s+IN\s*\((.+)\)\)/mi)[1]
ids = match.scan(/\w+/)
end
ids_element = []
ids.each { |id| ids_element << :ids << id }
queue_command ActiveSalesforce::BoxcarCommand::Delete.new(self, ids_element)
}
end
# If a SF object is dirty, it is called to get fresh attributes.
def get_updated(object_type, start_date, end_date, name = nil)
msg = "get_updated(#{object_type}, #{start_date}, #{end_date})"
log(msg, name) {
get_updated_element = []
get_updated_element << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << object_type
get_updated_element << :startDate << start_date
get_updated_element << :endDate << end_date
result = get_result(@connection.getUpdated(get_updated_element), :getUpdated)
result[:ids]
}
end
# Get SF object(s) that have been marked as deleted but not yet permanently removed, like things in a recycle bin.
def get_deleted(object_type, start_date, end_date, name = nil)
msg = "get_deleted(#{object_type}, #{start_date}, #{end_date})"
log(msg, name) {
get_deleted_element = []
get_deleted_element << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << object_type
get_deleted_element << :startDate << start_date
get_deleted_element << :endDate << end_date
result = get_result(@connection.getDeleted(get_deleted_element), :getDeleted)
ids = []
result[:deletedRecords].each do |v|
ids << v[:id]
end
ids
}
end
# Returns information about the user account which is used to connect to salesforce.
# Salesforce::User.first.connection.get_user_info
def get_user_info(name = nil)
msg = "get_user_info()"
log(msg, name) {
get_result(@connection.getUserInfo([]), :getUserInfo)
}
end
# Get value given object type, fields, and Salesforce Object.
def retrieve_field_values(object_type, fields, ids, name = nil)
msg = "retrieve(#{object_type}, [#{ids.to_a.join(', ')}])"
log(msg, name) {
retrieve_element = []
retrieve_element << :fieldList << fields.to_a.join(", ")
retrieve_element << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << object_type
ids.to_a.each { |id| retrieve_element << :ids << id }
result = get_result(@connection.retrieve(retrieve_element), :retrieve)
result = [ result ] unless result.is_a?(Array)
# Remove unwanted :type and normalize :Id if required
field_values = []
result.each do |v|
v = v.dup
v.delete(:type)
v[:Id] = v[:Id][0] if v[:Id].is_a? Array
field_values << v
end
field_values
}
end
def get_fields(columns, names, values, access_check)
fields = {}
names.each_with_index do | name, n |
value = values[n]
if value
column = columns[name]
raise ActiveSalesforce::ASFError.new(@logger, "Column not found for #{name} - get_fields!") unless column
value.gsub!(/''/, "'") if value.is_a? String
include_field = ((not value.empty?) and column.send(access_check))
if (include_field)
case column.type
when :date
value = Time.parse(value + "Z").utc.strftime("%Y-%m-%d")
when :datetime
value = Time.parse(value + "Z").utc.strftime("%Y-%m-%dT%H:%M:%SZ")
end
fields[column.api_name] = value
end
end
end
fields
end
def get_null_fields(columns, names, values, access_check)
fields = {}
names.each_with_index do | name, n |
value = values[n]
if !value
column = columns[name]
fields[column.api_name] = nil if column.send(access_check) && column.api_name.casecmp("ownerid") != 0
end
end
fields
end
# Removed no valid SQL modifier, right now, only LIMIT is allowed.
# To use a custom SQL, it is better to interface with RForce, see
# Salesforce::SfBase query_by_sql(sql) method.
def extract_sql_modifier(soql, modifier)
value = soql.match(/\s+#{modifier}\s+(\d+)/mi)
if value
value = value[1].to_i
if !(modifier.upcase == "LIMIT")
# If it is not the keyword - LIMIT, remove it from the SOQL
soql.sub!(/\s+#{modifier}\s+\d+/mi, "")
else
# If it is the keyword - LIMIT, do not remove it from the SOQL
end
# SOQL now supports LIMIT clause. If the user is not an app admin &
# queries Newsfeed or EntitySubscription & without q limit (e.g. > 1000),
# it would cause MQL_FORMED_QUERY exception:
# However, OFFSET is still not supported by SOQL.
# ***NOTE: needs to change it in the gem to make it effective.
end
value
end
# A clever contraption to construct the key to the object array which is returned
# by Rforce from SOAP result.
def get_result(response, method)
responseName = (method.to_s + "Response").to_sym
finalResponse = response[responseName]
raise ActiveSalesforce::ASFError.new(@logger, response[:Fault][:faultstring], response.fault) unless finalResponse
result = finalResponse[:result]
end
def check_result(result)
result = [ result ] unless result.is_a?(Array)
result.each do |r|
raise ActiveSalesforce::ASFError.new(@logger, r[:errors], r[:errors][:message]) unless r[:success] == "true"
end
result
end
# A clever contract to get meta-attribute associated with a Salesforce Object.
# by Rforce from SOAP result. e.g.
# pp user.connection.get_entity_def("AccountFeed")
# entity_name), :describeSObject)
custom = false
rescue ActiveSalesforce::ASFError
# Fallback and see if we can find a custom object with this name
debug(" Unable to find medata for '#{entity_name}', falling back to custom object name #{entity_name + "__c"}")
metadata = get_result(@connection.describeSObject(:sObjectType => entity_name + "__c"), :describeSObject)
custom = true
end
metadata[:fields].each do |field|
column = SalesforceColumn.new(field)
cached_columns << column
cached_relationships << SalesforceRelationship.new(field, column) if field[:type] =~ /reference/mi
end
relationships = metadata[:childRelationships]
if relationships
relationships = [ relationships ] unless relationships.is_a? Array
relationships.each do |relationship|
if relationship[:cascadeDelete] == "true"
r = SalesforceRelationship.new(relationship)
cached_relationships << r
end
end
end
key_prefix = metadata[:keyPrefix]
entity_def = ActiveSalesforce::EntityDefinition.new(self, entity_name, entity_klass,
cached_columns, cached_relationships, custom, key_prefix)
@entity_def_map[entity_name] = entity_def
@keyprefix_to_entity_def_map[key_prefix] = entity_def
configure_active_record(entity_def)
entity_def
end
def configure_active_record(entity_def)
entity_name = entity_def.name
klass = class_from_entity_name(entity_name)
class << klass
def asf_augmented?
true
end
end
# Add support for SID-based authentication
ActiveSalesforce::SessionIDAuthenticationFilter.register(klass)
klass.set_inheritance_column nil unless entity_def.custom?
klass.set_primary_key "id"
# Create relationships for any reference field
entity_def.relationships.each do |relationship|
referenceName = relationship.name
unless self.respond_to? referenceName.to_sym or relationship.reference_to == "Profile"
reference_to = relationship.reference_to
one_to_many = relationship.one_to_many
foreign_key = relationship.foreign_key
# DCHASMAN TODO Figure out how to handle polymorphic refs (e.g. Note.parent can refer to
# Account, Contact, Opportunity, Contract, Asset, Product2, ...
if reference_to.is_a? Array
debug(" Skipping unsupported polymophic one-to-#{one_to_many ? 'many' : 'one' } relationship '#{referenceName}' from #{klass} to [#{relationship.reference_to.join(', ')}] using #{foreign_key}")
next
end
# Handle references to custom objects
reference_to = reference_to.chomp("__c").camelize if reference_to.match(/__c$/)
begin
referenced_klass = class_from_entity_name(reference_to)
rescue NameError => e
# Automatically create a least a stub for the referenced entity
debug(" Creating ActiveRecord stub for the referenced entity '#{reference_to}'")
referenced_klass = klass.class_eval("Salesforce::#{reference_to} = Class.new(ActiveRecord::Base)")
referenced_klass.instance_variable_set("@asf_connection", klass.connection)
# Automatically inherit the connection from the referencee
def referenced_klass.connection
@asf_connection
end
end
if referenced_klass
if one_to_many
assoc_name = reference_to.underscore.pluralize.to_sym
klass.has_many assoc_name, :class_name => referenced_klass.name, :foreign_key => foreign_key
else
assoc_name = reference_to.underscore.singularize.to_sym
klass.belongs_to assoc_name, :class_name => referenced_klass.name, :foreign_key => foreign_key
end
debug(" Created one-to-#{one_to_many ? 'many' : 'one' } relationship '#{referenceName}' from #{klass} to #{referenced_klass} using #{foreign_key}")
end
end
end
end
# Return column names given a table_name, usage:
# >> pp user.connection.columns('account_feed') or ('AccountFeed')
def columns(table_name, name = nil)
table_name, columns, entity_def = lookup(table_name)
entity_def.columns
end
# Given a entity and show column names,
# >>pp user.connection.class_from_entity_name("AccountFeed")
def class_from_entity_name(entity_name)
entity_klass = @class_to_entity_map[entity_name.upcase]
debug("Found matching class '#{entity_klass}' for entity '#{entity_name}'") if entity_klass
# Constantize entities under the Salesforce namespace.
entity_klass = ("Salesforce::" + entity_name).constantize unless entity_klass
entity_klass
end
# Create a blank Salesforce object
def create_sobject(entity_name, id, fields, null_fields = [])
sobj = []
sobj << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << entity_name
sobj << 'Id { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << id if id
# add any changed fields
fields.each do | name, value |
sobj << name.to_sym << value if value
end
# add null fields
null_fields.each do | name, value |
sobj << 'fieldsToNull { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << name
end
[ :sObjects, sobj ]
end
# Returns column names associated with a Salesforce Object
def column_names(table_name)
columns(table_name).map { |column| column.name }
end
def lookup(raw_table_name)
table_name = raw_table_name.singularize
# See if a table name to AR class mapping was registered
klass = @class_to_entity_map[table_name.upcase]
entity_name = klass ? raw_table_name : table_name.camelize
entity_def = get_entity_def(entity_name)
[table_name, entity_def.columns, entity_def]
end
def debug(msg)
@logger.debug(msg) if @logger
end
protected
# For Silent-e, added 'tables' method to solve ARel problem
# fix single-quote escape sequence for WHERE condition expressions. Salesforce enforces a backspace on SELECTs
# NOTE: this method is only used for SELECT queries. INSERT/UPDATE queries are smart enough to use the primary
# key for their WHERE statements, or so I've found.
def fix_single_quote_in_where(sql)
where_match = sql.match(/WHERE\s*\((.*)\)/mi)
return sql unless where_match
where_conditions = where_match[1]
# debug("where_conditions: #{where_conditions}")
where_conditions.gsub!(/''/, "\\\\'")
# debug("updated where_conditions: #{where_conditions.gsub(/''/, "\\\\'")}")
sql = "#{where_match.pre_match}WHERE (#{where_conditions})#{where_match.post_match}"
end
def queue_command(command)
# If @command_boxcar is not nil, then this is a transaction
# and commands should be queued in the boxcar
if @command_boxcar
@command_boxcar << command
# If a command is not executed within a transaction, it should
# be executed immediately
else
send_commands([command])
end
end
end
end
end