silent-e-activerecord-activesalesforce-adapter 2.3.8.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/.project +17 -0
- data/README +80 -0
- data/Rakefile +21 -0
- data/VERSION +1 -0
- data/lib/active_record/connection_adapters/activesalesforce.rb +36 -0
- data/lib/active_record/connection_adapters/activesalesforce_adapter.rb +863 -0
- data/lib/active_record/connection_adapters/asf_active_record.rb +40 -0
- data/lib/active_record/connection_adapters/boxcar_command.rb +66 -0
- data/lib/active_record/connection_adapters/column_definition.rb +95 -0
- data/lib/active_record/connection_adapters/entity_definition.rb +59 -0
- data/lib/active_record/connection_adapters/id_resolver.rb +84 -0
- data/lib/active_record/connection_adapters/recording_binding.rb +90 -0
- data/lib/active_record/connection_adapters/relationship_definition.rb +81 -0
- data/lib/active_record/connection_adapters/result_array.rb +31 -0
- data/lib/active_record/connection_adapters/sid_authentication_filter.rb +57 -0
- data/lib/activerecord-activesalesforce-adapter.rb +1 -0
- data/lib/arel/engines/sql/compilers/activesalesforce_compiler.rb +6 -0
- data/lib/rforce.rb +87 -0
- data/lib/rforce/binding.rb +212 -0
- data/lib/rforce/method_keys.rb +14 -0
- data/lib/rforce/soap_pullable.rb +93 -0
- data/lib/rforce/soap_response.rb +9 -0
- data/lib/rforce/soap_response_expat.rb +36 -0
- data/lib/rforce/soap_response_hpricot.rb +75 -0
- data/lib/rforce/soap_response_rexml.rb +35 -0
- data/lib/rforce/version.rb +3 -0
- data/silent-e-activerecord-activesalesforce-adapter.gemspec +115 -0
- data/test/unit/basic_test.rb +204 -0
- data/test/unit/config.yml +5 -0
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_add_notes_to_contact.recording +1966 -0
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_assignment_rule_id.recording +1621 -0
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_batch_insert.recording +1611 -0
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_client_id.recording +1618 -0
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_count_contacts.recording +1620 -0
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_create_a_contact.recording +1611 -0
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_a_contact.recording +1611 -0
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_a_contact_by_first_name.recording +3468 -0
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_a_contact_by_id.recording +1664 -0
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_addresses.recording +1635 -0
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_get_created_by_from_contact.recording +4307 -0
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_master_detail.recording +1951 -0
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_read_all_content_columns.recording +1611 -0
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_save_a_contact.recording +1611 -0
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_use_default_rule.recording +1618 -0
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_use_update_mru.recording +1618 -0
- data/test/unit/recorded_test_case.rb +83 -0
- metadata +186 -0
data/.gitignore
ADDED
data/.project
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<projectDescription>
|
3
|
+
<name>althor880-activerecord-activesalesforce-adapter</name>
|
4
|
+
<comment></comment>
|
5
|
+
<projects>
|
6
|
+
</projects>
|
7
|
+
<buildSpec>
|
8
|
+
<buildCommand>
|
9
|
+
<name>org.rubypeople.rdt.core.rubybuilder</name>
|
10
|
+
<arguments>
|
11
|
+
</arguments>
|
12
|
+
</buildCommand>
|
13
|
+
</buildSpec>
|
14
|
+
<natures>
|
15
|
+
<nature>org.rubypeople.rdt.core.rubynature</nature>
|
16
|
+
</natures>
|
17
|
+
</projectDescription>
|
data/README
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
== Welcome to Active Salesforce
|
2
|
+
|
3
|
+
ActiveSalesforce is an extension to the Rails Framework that allows for the dynamic creation and management of ActiveRecord objects through the use of Salesforce meta-data and uses a Salesforce.com organization as the backing store.
|
4
|
+
|
5
|
+
See notes below about my fork.
|
6
|
+
|
7
|
+
== Installation
|
8
|
+
|
9
|
+
I have a compiled build in my Downloads area or clone my repo, build the gem, and install.
|
10
|
+
|
11
|
+
== Getting started
|
12
|
+
|
13
|
+
1. If you have not already done so generate your initial rails app:
|
14
|
+
|
15
|
+
rails myappname
|
16
|
+
|
17
|
+
2. Edit config/environment.rb and add a config.gem requirement:
|
18
|
+
|
19
|
+
Rails::Initializer.run do |config|
|
20
|
+
...
|
21
|
+
config.gem "silent-e-activerecord-activesalesforce-adapter", :lib => 'activerecord-activesalesforce-adapter'
|
22
|
+
...
|
23
|
+
end
|
24
|
+
|
25
|
+
3. Edit database.yml
|
26
|
+
|
27
|
+
development:
|
28
|
+
adapter: activesalesforce
|
29
|
+
username: <salesforce user name goes here>
|
30
|
+
password: <salesforce password goes here>
|
31
|
+
|
32
|
+
NOTE: "url" is an optional parameter. If you want to access your Salesforce Sandbox account add the following line.
|
33
|
+
|
34
|
+
url: https://test.salesforce.com
|
35
|
+
|
36
|
+
4. Create your salesforce models using a Salesforce::<ModelName> namespace.
|
37
|
+
|
38
|
+
script/generate model Salesforce::Contact
|
39
|
+
|
40
|
+
5. Run a quick test to make sure things are working
|
41
|
+
|
42
|
+
> script/console
|
43
|
+
Loading development environment (Rails 2.3.9)
|
44
|
+
|
45
|
+
>> Salesforce::Contact.first
|
46
|
+
=> <Salesforce::Contact id: "003T000000GqvJsIAJ", ... >
|
47
|
+
|
48
|
+
6. Proceed using standard Rails development techniques!
|
49
|
+
|
50
|
+
== Advanced Features
|
51
|
+
|
52
|
+
1. Session ID based Authentication: Add the following to /app/controllers/application.rb to enable SID auth for all controllers
|
53
|
+
|
54
|
+
class ApplicationController < ActionController::Base
|
55
|
+
before_filter ActiveSalesforce::SessionIDAuthenticationFilter
|
56
|
+
end
|
57
|
+
|
58
|
+
2. Boxcar'ing of updates, inserts, and deletes. Use <YourModel>.transaction() to demark boxcar boundaries.
|
59
|
+
|
60
|
+
== Description of contents
|
61
|
+
|
62
|
+
lib
|
63
|
+
Application specific libraries. Basically, any kind of custom code that doesn't
|
64
|
+
belong under controllers, models, or helpers. This directory is in the load path.
|
65
|
+
|
66
|
+
script
|
67
|
+
Helper scripts for automation and generation.
|
68
|
+
|
69
|
+
test
|
70
|
+
Unit and functional tests along with fixtures.
|
71
|
+
|
72
|
+
== Note about my (silent-e) fork
|
73
|
+
|
74
|
+
I created this fork to fix a problem I was having with SELECT statements. Salesforce enforces single-quote escaping in strings with a backspace in WHERE condition expressions. However, in INSERT and UPDATE statements the VALUES are escaped with two consecutive single-quotes. See the table below for a visual explanation of my clumsy text one. :)
|
75
|
+
|
76
|
+
This fork specifically tweaks SELECT WHERE conditions and is based on blaines work on bringing ASF up to speed with Rails 3. I will attempt to keep it in step with blaines work until we can get a consensus and possibly get a official update to the canonical ASF gem.
|
77
|
+
|
78
|
+
| Query Type | Escape sequence required |
|
79
|
+
| WHERE (conditionExpression) | ... WHERE (Name = 'Let\'s do the Time Warp again!') |
|
80
|
+
| INSERT/UPDATE | ... VALUES ('Let''s do the Time Warp again!') |
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
begin
|
2
|
+
require 'jeweler'
|
3
|
+
Jeweler::Tasks.new do |gemspec|
|
4
|
+
gemspec.name = "silent-e-activerecord-activesalesforce-adapter"
|
5
|
+
gemspec.summary = "ActiveSalesforce (ASF) is a Rails connection adapter that provides direct access to Salesforce.com hosted data and metadata via the ActiveRecord model layer. Objects, fields, and relationships are all auto surfaced as active record attributes and rels."
|
6
|
+
gemspec.email = "matte@silent-e.com"
|
7
|
+
gemspec.homepage = "http://github.com/silent-e/activerecord-activesalesforce-adapter"
|
8
|
+
gemspec.authors = ["Doug Chasman","Luigi Montanez","Senthil Nayagam","Justin Ball","Jesse Hallett", "Andrew Freeberg", "Blaine Schanfeldt", "Matte Edens"]
|
9
|
+
gemspec.description = ''
|
10
|
+
|
11
|
+
gemspec.test_files = 'test/**/*'
|
12
|
+
|
13
|
+
gemspec.add_dependency('rails', '>= 2.3.3')
|
14
|
+
gemspec.add_dependency('builder', '>= 1.2.4')
|
15
|
+
gemspec.add_dependency('hpricot', '>=0.8.2')
|
16
|
+
|
17
|
+
|
18
|
+
end
|
19
|
+
rescue LoadError
|
20
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
21
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.3.8.1
|
@@ -0,0 +1,36 @@
|
|
1
|
+
=begin
|
2
|
+
ActiveSalesforce
|
3
|
+
Copyright 2006 Doug Chasman
|
4
|
+
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
you may not use this file except in compliance with the License.
|
7
|
+
You may obtain a copy of the License at
|
8
|
+
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
See the License for the specific language governing permissions and
|
15
|
+
limitations under the License.
|
16
|
+
=end
|
17
|
+
|
18
|
+
require 'activesalesforce_adapter'
|
19
|
+
|
20
|
+
module ActionView
|
21
|
+
module Helpers
|
22
|
+
# Provides a set of methods for making easy links and getting urls that depend on the controller and action. This means that
|
23
|
+
# you can use the same format for links in the views that you do in the controller. The different methods are even named
|
24
|
+
# synchronously, so link_to uses that same url as is generated by url_for, which again is the same url used for
|
25
|
+
# redirection in redirect_to.
|
26
|
+
module UrlHelper
|
27
|
+
def link_to_asf(active_record, column)
|
28
|
+
if column.reference_to
|
29
|
+
link_to(column.reference_to, { :action => 'show', :controller => column.reference_to.pluralize, :id => active_record.send(column.name) } )
|
30
|
+
else
|
31
|
+
active_record.send(column.name)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,863 @@
|
|
1
|
+
=begin
|
2
|
+
ActiveSalesforce
|
3
|
+
Copyright 2006 Doug Chasman
|
4
|
+
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
you may not use this file except in compliance with the License.
|
7
|
+
You may obtain a copy of the License at
|
8
|
+
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
See the License for the specific language governing permissions and
|
15
|
+
limitations under the License.
|
16
|
+
=end
|
17
|
+
|
18
|
+
require 'thread'
|
19
|
+
require 'benchmark'
|
20
|
+
|
21
|
+
require 'active_record'
|
22
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
23
|
+
|
24
|
+
require File.dirname(__FILE__) + '/../../rforce'
|
25
|
+
require File.dirname(__FILE__) + '/column_definition'
|
26
|
+
require File.dirname(__FILE__) + '/relationship_definition'
|
27
|
+
require File.dirname(__FILE__) + '/boxcar_command'
|
28
|
+
require File.dirname(__FILE__) + '/entity_definition'
|
29
|
+
require File.dirname(__FILE__) + '/asf_active_record'
|
30
|
+
require File.dirname(__FILE__) + '/id_resolver'
|
31
|
+
require File.dirname(__FILE__) + '/sid_authentication_filter'
|
32
|
+
require File.dirname(__FILE__) + '/recording_binding'
|
33
|
+
require File.dirname(__FILE__) + '/result_array'
|
34
|
+
|
35
|
+
|
36
|
+
module ActiveRecord
|
37
|
+
class Base
|
38
|
+
@@cache = {}
|
39
|
+
|
40
|
+
def self.debug(msg)
|
41
|
+
logger.debug(msg) if logger
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.flush_connections()
|
45
|
+
@@cache = {}
|
46
|
+
end
|
47
|
+
|
48
|
+
# Establishes a connection to the database that's used by all Active Record objects.
|
49
|
+
def self.activesalesforce_connection(config) # :nodoc:
|
50
|
+
debug("\nUsing ActiveSalesforce connection\n")
|
51
|
+
|
52
|
+
# Default to production system using 11.0 API
|
53
|
+
url = config[:url]
|
54
|
+
url = "https://www.salesforce.com" unless url
|
55
|
+
|
56
|
+
uri = URI.parse(url)
|
57
|
+
uri.path = "/services/Soap/u/11.0"
|
58
|
+
url = uri.to_s
|
59
|
+
|
60
|
+
sid = config[:sid]
|
61
|
+
client_id = config[:client_id]
|
62
|
+
username = config[:username].to_s
|
63
|
+
password = config[:password].to_s
|
64
|
+
|
65
|
+
# Recording/playback support
|
66
|
+
recording_source = config[:recording_source]
|
67
|
+
recording = config[:recording]
|
68
|
+
|
69
|
+
if recording_source
|
70
|
+
recording_source = File.open(recording_source, recording ? "w" : "r")
|
71
|
+
binding = ActiveSalesforce::RecordingBinding.new(url, nil, recording != nil, recording_source, logger)
|
72
|
+
binding.client_id = client_id if client_id
|
73
|
+
binding.login(username, password) unless sid
|
74
|
+
end
|
75
|
+
|
76
|
+
# Check to insure that the second to last path component is a 'u' for Partner API
|
77
|
+
raise ActiveSalesforce::ASFError.new(logger, "Invalid salesforce server url '#{url}', must be a valid Parter API URL") unless url.match(/\/u\//mi)
|
78
|
+
|
79
|
+
if sid
|
80
|
+
binding = @@cache["sid=#{sid}"] unless binding
|
81
|
+
|
82
|
+
unless binding
|
83
|
+
debug("Establishing new connection for [sid='#{sid}']")
|
84
|
+
|
85
|
+
binding = RForce::Binding.new(url, sid)
|
86
|
+
@@cache["sid=#{sid}"] = binding
|
87
|
+
|
88
|
+
debug("Created new connection for [sid='#{sid}']")
|
89
|
+
else
|
90
|
+
debug("Reused existing connection for [sid='#{sid}']")
|
91
|
+
end
|
92
|
+
else
|
93
|
+
binding = @@cache["#{url}.#{username}.#{password}.#{client_id}"] unless binding
|
94
|
+
|
95
|
+
unless binding
|
96
|
+
debug("Establishing new connection for ['#{url}', '#{username}, '#{client_id}'")
|
97
|
+
|
98
|
+
seconds = Benchmark.realtime {
|
99
|
+
binding = RForce::Binding.new(url, sid)
|
100
|
+
binding.login(username, password)
|
101
|
+
|
102
|
+
@@cache["#{url}.#{username}.#{password}.#{client_id}"] = binding
|
103
|
+
}
|
104
|
+
|
105
|
+
debug("Created new connection for ['#{url}', '#{username}', '#{client_id}'] in #{seconds} seconds")
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
ConnectionAdapters::SalesforceAdapter.new(binding, logger, config)
|
110
|
+
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
module ConnectionAdapters
|
116
|
+
|
117
|
+
class SalesforceAdapter < AbstractAdapter
|
118
|
+
include StringHelper
|
119
|
+
|
120
|
+
MAX_BOXCAR_SIZE = 200
|
121
|
+
|
122
|
+
attr_accessor :batch_size
|
123
|
+
attr_reader :entity_def_map, :keyprefix_to_entity_def_map, :config, :class_to_entity_map
|
124
|
+
|
125
|
+
def initialize(connection, logger, config)
|
126
|
+
super(connection, logger)
|
127
|
+
|
128
|
+
@connection_options = nil
|
129
|
+
@config = config
|
130
|
+
|
131
|
+
@entity_def_map = {}
|
132
|
+
@keyprefix_to_entity_def_map = {}
|
133
|
+
|
134
|
+
@command_boxcar = nil
|
135
|
+
@class_to_entity_map = {}
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
def set_class_for_entity(klass, entity_name)
|
140
|
+
debug("Setting @class_to_entity_map['#{entity_name.upcase}'] = #{klass} for connection #{self}")
|
141
|
+
@class_to_entity_map[entity_name.upcase] = klass
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
def binding
|
146
|
+
@connection
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
def adapter_name #:nodoc:
|
151
|
+
'ActiveSalesforce'
|
152
|
+
end
|
153
|
+
|
154
|
+
|
155
|
+
def supports_migrations? #:nodoc:
|
156
|
+
false
|
157
|
+
end
|
158
|
+
|
159
|
+
def tables(name = nil) #:nodoc:
|
160
|
+
@connection.describeGlobal({}).describeGlobalResponse.result.types
|
161
|
+
end
|
162
|
+
|
163
|
+
def table_exists?(table_name)
|
164
|
+
true
|
165
|
+
end
|
166
|
+
|
167
|
+
# QUOTING ==================================================
|
168
|
+
|
169
|
+
def quote(value, column = nil)
|
170
|
+
case value
|
171
|
+
when NilClass then quoted_value = "NULL"
|
172
|
+
when TrueClass then quoted_value = "TRUE"
|
173
|
+
when FalseClass then quoted_value = "FALSE"
|
174
|
+
when Float, Fixnum, Bignum then quoted_value = "'#{value.to_s}'"
|
175
|
+
else quoted_value = super(value, column)
|
176
|
+
end
|
177
|
+
|
178
|
+
quoted_value
|
179
|
+
end
|
180
|
+
|
181
|
+
# CONNECTION MANAGEMENT ====================================
|
182
|
+
|
183
|
+
def active?
|
184
|
+
true
|
185
|
+
end
|
186
|
+
|
187
|
+
|
188
|
+
def reconnect!
|
189
|
+
connect
|
190
|
+
end
|
191
|
+
|
192
|
+
|
193
|
+
# TRANSACTIOn SUPPORT (Boxcarring really because the salesforce.com api does not support transactions)
|
194
|
+
|
195
|
+
# Override AbstractAdapter's transaction method to implement
|
196
|
+
# per-connection support for nested transactions that do not commit until
|
197
|
+
# the outermost transaction is finished. ActiveRecord provides support
|
198
|
+
# for this, but does not distinguish between database connections, which
|
199
|
+
# prevents opening transactions to two different databases at the same
|
200
|
+
# time.
|
201
|
+
def transaction_with_nesting_support(*args, &block)
|
202
|
+
open = Thread.current["open_transactions_for_#{self.class.name.underscore}"] ||= 0
|
203
|
+
Thread.current["open_transactions_for_#{self.class.name.underscore}"] = open + 1
|
204
|
+
|
205
|
+
begin
|
206
|
+
transaction_without_nesting_support(&block)
|
207
|
+
ensure
|
208
|
+
Thread.current["open_transactions_for_#{self.class.name.underscore}"] -= 1
|
209
|
+
end
|
210
|
+
end
|
211
|
+
alias_method_chain :transaction, :nesting_support
|
212
|
+
|
213
|
+
# Begins the transaction (and turns off auto-committing).
|
214
|
+
def begin_db_transaction
|
215
|
+
# log('Opening boxcar', 'begin_db_transaction()')
|
216
|
+
@command_boxcar = []
|
217
|
+
end
|
218
|
+
|
219
|
+
|
220
|
+
def send_commands(commands)
|
221
|
+
# Send the boxcar'ed command set
|
222
|
+
verb = commands[0].verb
|
223
|
+
|
224
|
+
args = []
|
225
|
+
commands.each do |command|
|
226
|
+
command.args.each { |arg| args << arg }
|
227
|
+
end
|
228
|
+
|
229
|
+
response = @connection.send(verb, args)
|
230
|
+
|
231
|
+
result = get_result(response, verb)
|
232
|
+
|
233
|
+
result = [ result ] unless result.is_a?(Array)
|
234
|
+
|
235
|
+
errors = []
|
236
|
+
result.each_with_index do |r, n|
|
237
|
+
success = r[:success] == "true"
|
238
|
+
|
239
|
+
# Give each command a chance to process its own result
|
240
|
+
command = commands[n]
|
241
|
+
command.after_execute(r)
|
242
|
+
|
243
|
+
# Handle the set of failures
|
244
|
+
errors << r[:errors] unless r[:success] == "true"
|
245
|
+
end
|
246
|
+
|
247
|
+
unless errors.empty?
|
248
|
+
message = errors.join("\n")
|
249
|
+
fault = (errors.map { |error| error[:message] }).join("\n")
|
250
|
+
raise ActiveSalesforce::ASFError.new(@logger, message, fault)
|
251
|
+
end
|
252
|
+
|
253
|
+
result
|
254
|
+
end
|
255
|
+
|
256
|
+
|
257
|
+
# Commits the transaction (and turns on auto-committing).
|
258
|
+
def commit_db_transaction()
|
259
|
+
# log("Committing boxcar with #{@command_boxcar.length} commands", 'commit_db_transaction()')
|
260
|
+
# puts("Committing boxcar with #{@command_boxcar.length} commands", 'commit_db_transaction()')
|
261
|
+
|
262
|
+
previous_command = nil
|
263
|
+
commands = []
|
264
|
+
|
265
|
+
@command_boxcar.each do |command|
|
266
|
+
if commands.length >= MAX_BOXCAR_SIZE or (previous_command and (command.verb != previous_command.verb))
|
267
|
+
send_commands(commands)
|
268
|
+
|
269
|
+
commands = []
|
270
|
+
previous_command = nil
|
271
|
+
else
|
272
|
+
commands << command
|
273
|
+
previous_command = command
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
# Discard the command boxcar
|
278
|
+
@command_boxcar = nil
|
279
|
+
|
280
|
+
# Finish off the partial boxcar
|
281
|
+
send_commands(commands) unless commands.empty?
|
282
|
+
|
283
|
+
end
|
284
|
+
|
285
|
+
# Rolls back the transaction (and turns on auto-committing). Must be
|
286
|
+
# done if the transaction block raises an exception or returns false.
|
287
|
+
def rollback_db_transaction()
|
288
|
+
# log('Rolling back boxcar', 'rollback_db_transaction()')
|
289
|
+
@command_boxcar = nil
|
290
|
+
end
|
291
|
+
|
292
|
+
|
293
|
+
# DATABASE STATEMENTS ======================================
|
294
|
+
|
295
|
+
def select_all(sql, name = nil) #:nodoc:
|
296
|
+
|
297
|
+
# fix the single quote escape method in WHERE condition expression
|
298
|
+
sql = quote_where(sql)
|
299
|
+
|
300
|
+
# Arel adds the class to the selection - we do not want this i.e...
|
301
|
+
# SELECT contacts.* FROM => SELECT * FROM
|
302
|
+
sql = sql.gsub(/SELECT\s+[^\(][A-Z]+\./mi," ")
|
303
|
+
raw_table_name = sql.match(/FROM\s+(\w+)/mi)[1]
|
304
|
+
|
305
|
+
table_name, columns, entity_def = lookup(raw_table_name)
|
306
|
+
|
307
|
+
column_names = columns.map { |column| column.api_name }
|
308
|
+
|
309
|
+
# Check for SELECT COUNT(*) FROM query
|
310
|
+
|
311
|
+
# Rails 1.1
|
312
|
+
selectCountMatch = sql.match(/SELECT\s+COUNT\(\*\)\s+AS\s+count_all\s+FROM/mi)
|
313
|
+
|
314
|
+
# Rails 1.0
|
315
|
+
selectCountMatch = sql.match(/SELECT\s+COUNT\(\*\)\s+FROM/mi) unless selectCountMatch
|
316
|
+
|
317
|
+
if selectCountMatch
|
318
|
+
soql = "SELECT COUNT() FROM#{selectCountMatch.post_match}"
|
319
|
+
else
|
320
|
+
if sql.match(/SELECT\s+\*\s+FROM/mi)
|
321
|
+
# Always convert SELECT * to select all columns (required for the AR attributes mechanism to work correctly)
|
322
|
+
soql = sql.sub(/SELECT .+ FROM/mi, "SELECT #{column_names.join(', ')} FROM")
|
323
|
+
else
|
324
|
+
soql = sql
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
soql.sub!(/\s+FROM\s+\w+/mi, " FROM #{entity_def.api_name}")
|
329
|
+
|
330
|
+
if selectCountMatch
|
331
|
+
query_result = get_result(@connection.query(:queryString => soql), :query)
|
332
|
+
return [{ :count => query_result[:size] }]
|
333
|
+
end
|
334
|
+
|
335
|
+
# Look for a LIMIT clause
|
336
|
+
limit = extract_sql_modifier(soql, "LIMIT")
|
337
|
+
limit = MAX_BOXCAR_SIZE unless limit
|
338
|
+
|
339
|
+
# Look for an OFFSET clause
|
340
|
+
offset = extract_sql_modifier(soql, "OFFSET")
|
341
|
+
|
342
|
+
# Fixup column references to use api names
|
343
|
+
columns = entity_def.column_name_to_column
|
344
|
+
soql.gsub!(/((?:\w+\.)?\w+)(?=\s*(?:=|!=|<|>|<=|>=|like)\s*(?:'[^']*'|NULL|TRUE|FALSE))/mi) do |column_name|
|
345
|
+
# strip away any table alias
|
346
|
+
column_name.sub!(/\w+\./, '')
|
347
|
+
|
348
|
+
column = columns[column_name]
|
349
|
+
raise ActiveSalesforce::ASFError.new(@logger, "Column not found for #{column_name}!") unless column
|
350
|
+
|
351
|
+
column.api_name
|
352
|
+
end
|
353
|
+
|
354
|
+
# Update table name references
|
355
|
+
soql.sub!(/#{raw_table_name}\./mi, "#{entity_def.api_name}.")
|
356
|
+
|
357
|
+
@connection.batch_size = @batch_size if @batch_size
|
358
|
+
@batch_size = nil
|
359
|
+
|
360
|
+
query_result = get_result(@connection.query(:queryString => soql), :query)
|
361
|
+
result = ActiveSalesforce::ResultArray.new(query_result[:size].to_i)
|
362
|
+
return result unless query_result[:records]
|
363
|
+
|
364
|
+
add_rows(entity_def, query_result, result, limit)
|
365
|
+
|
366
|
+
while ((query_result[:done].casecmp("true") != 0) and (result.size < limit or limit == 0))
|
367
|
+
# Now queryMore
|
368
|
+
locator = query_result[:queryLocator];
|
369
|
+
query_result = get_result(@connection.queryMore(:queryLocator => locator), :queryMore)
|
370
|
+
|
371
|
+
add_rows(entity_def, query_result, result, limit)
|
372
|
+
end
|
373
|
+
|
374
|
+
result
|
375
|
+
end
|
376
|
+
|
377
|
+
def add_rows(entity_def, query_result, result, limit)
|
378
|
+
records = query_result[:records]
|
379
|
+
records = [ records ] unless records.is_a?(Array)
|
380
|
+
|
381
|
+
records.each do |record|
|
382
|
+
row = {}
|
383
|
+
|
384
|
+
record.each do |name, value|
|
385
|
+
if name != :type
|
386
|
+
# Ids may be returned in an array with 2 duplicate entries...
|
387
|
+
value = value[0] if name == :Id && value.is_a?(Array)
|
388
|
+
|
389
|
+
column = entity_def.api_name_to_column[name.to_s]
|
390
|
+
attribute_name = column.name
|
391
|
+
|
392
|
+
if column.type == :boolean
|
393
|
+
row[attribute_name] = (value.casecmp("true") == 0)
|
394
|
+
else
|
395
|
+
row[attribute_name] = value
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
result << row
|
401
|
+
|
402
|
+
break if result.size >= limit and limit != 0
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
def select_one(sql, name = nil) #:nodoc:
|
407
|
+
# debug "#{sql}"
|
408
|
+
# puts "#{sql}"
|
409
|
+
self.batch_size = 1
|
410
|
+
|
411
|
+
result = select_all(sql, name)
|
412
|
+
|
413
|
+
result.nil? ? nil : result.first
|
414
|
+
end
|
415
|
+
|
416
|
+
|
417
|
+
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
|
418
|
+
# log(sql, name) {
|
419
|
+
# Convert sql to sobject
|
420
|
+
table_name, columns, entity_def = lookup(sql.match(/INSERT\s+INTO\s+(\w+)\s+/mi)[1])
|
421
|
+
columns = entity_def.column_name_to_column
|
422
|
+
|
423
|
+
# Extract array of column names
|
424
|
+
names = sql.match(/\((.+)\)\s+VALUES/mi)[1].scan(/\w+/mi)
|
425
|
+
|
426
|
+
# Extract arrays of values
|
427
|
+
values = sql.match(/VALUES\s*\((.+)\)/mi)[1]
|
428
|
+
values = values.scan(/(NULL|TRUE|FALSE|'(?:(?:[^']|'')*)'),*/mi).flatten
|
429
|
+
values.map! { |v| v.first == "'" ? v.slice(1, v.length - 2) : v == "NULL" ? nil : v }
|
430
|
+
|
431
|
+
fields = get_fields(columns, names, values, :createable)
|
432
|
+
|
433
|
+
sobject = create_sobject(entity_def.api_name, nil, fields)
|
434
|
+
|
435
|
+
# Track the id to be able to update it when the create() is actually executed
|
436
|
+
id = String.new
|
437
|
+
queue_command ActiveSalesforce::BoxcarCommand::Insert.new(self, sobject, id)
|
438
|
+
|
439
|
+
id
|
440
|
+
# }
|
441
|
+
end
|
442
|
+
|
443
|
+
|
444
|
+
def update(sql, name = nil) #:nodoc:
|
445
|
+
sql = sql.gsub(/WHERE\s+\([A-Z]+\./mi,"WHERE ")
|
446
|
+
# puts("Update: #{sql}, #{name}")
|
447
|
+
# log(sql, name) {
|
448
|
+
# Convert sql to sobject
|
449
|
+
table_name, columns, entity_def = lookup(sql.match(/UPDATE\s+(\w+)\s+/mi)[1])
|
450
|
+
columns = entity_def.column_name_to_column
|
451
|
+
|
452
|
+
match = sql.match(/SET\s+(.+)\s+WHERE/mi)[1]
|
453
|
+
names = match.scan(/(\w+)\s*=\s*(?:'|NULL|TRUE|FALSE)/mi).flatten
|
454
|
+
|
455
|
+
values = match.scan(/=\s*(NULL|TRUE|FALSE|'(?:(?:[^']|'')*)'),*/mi).flatten
|
456
|
+
values.map! { |v| v.first == "'" ? v.slice(1, v.length - 2) : v == "NULL" ? nil : v }
|
457
|
+
|
458
|
+
fields = get_fields(columns, names, values, :updateable)
|
459
|
+
null_fields = get_null_fields(columns, names, values, :updateable)
|
460
|
+
|
461
|
+
ids = sql.match(/WHERE\s+id\s*=\s*'(\w+)'/mi)
|
462
|
+
# puts "return if ids.nil? #{ids}"
|
463
|
+
return if ids.nil?
|
464
|
+
id = ids[1]
|
465
|
+
|
466
|
+
sobject = create_sobject(entity_def.api_name, id, fields, null_fields)
|
467
|
+
|
468
|
+
queue_command ActiveSalesforce::BoxcarCommand::Update.new(self, sobject)
|
469
|
+
#}
|
470
|
+
end
|
471
|
+
|
472
|
+
|
473
|
+
def delete(sql, name = nil)
|
474
|
+
# log(sql, name) {
|
475
|
+
# Extract the id
|
476
|
+
match = sql.match(/WHERE\s+id\s*=\s*'(\w+)'/mi)
|
477
|
+
|
478
|
+
if match
|
479
|
+
ids = [ match[1] ]
|
480
|
+
else
|
481
|
+
# Check for the form (id IN ('x', 'y'))
|
482
|
+
match = sql.match(/WHERE\s+\(\s*id\s+IN\s*\((.+)\)\)/mi)[1]
|
483
|
+
ids = match.scan(/\w+/)
|
484
|
+
end
|
485
|
+
|
486
|
+
ids_element = []
|
487
|
+
ids.each { |id| ids_element << :ids << id }
|
488
|
+
|
489
|
+
queue_command ActiveSalesforce::BoxcarCommand::Delete.new(self, ids_element)
|
490
|
+
# }
|
491
|
+
end
|
492
|
+
|
493
|
+
|
494
|
+
def get_updated(object_type, start_date, end_date, name = nil)
|
495
|
+
msg = "get_updated(#{object_type}, #{start_date}, #{end_date})"
|
496
|
+
# log(msg, name) {
|
497
|
+
get_updated_element = []
|
498
|
+
get_updated_element << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << object_type
|
499
|
+
get_updated_element << :startDate << start_date
|
500
|
+
get_updated_element << :endDate << end_date
|
501
|
+
|
502
|
+
result = get_result(@connection.getUpdated(get_updated_element), :getUpdated)
|
503
|
+
|
504
|
+
result[:ids]
|
505
|
+
# }
|
506
|
+
end
|
507
|
+
|
508
|
+
|
509
|
+
def get_deleted(object_type, start_date, end_date, name = nil)
|
510
|
+
msg = "get_deleted(#{object_type}, #{start_date}, #{end_date})"
|
511
|
+
# log(msg, name) {
|
512
|
+
get_deleted_element = []
|
513
|
+
get_deleted_element << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << object_type
|
514
|
+
get_deleted_element << :startDate << start_date
|
515
|
+
get_deleted_element << :endDate << end_date
|
516
|
+
|
517
|
+
result = get_result(@connection.getDeleted(get_deleted_element), :getDeleted)
|
518
|
+
|
519
|
+
ids = []
|
520
|
+
result[:deletedRecords].each do |v|
|
521
|
+
ids << v[:id]
|
522
|
+
end
|
523
|
+
|
524
|
+
ids
|
525
|
+
# }
|
526
|
+
end
|
527
|
+
|
528
|
+
|
529
|
+
def get_user_info(name = nil)
|
530
|
+
msg = "get_user_info()"
|
531
|
+
# log(msg, name) {
|
532
|
+
get_result(@connection.getUserInfo([]), :getUserInfo)
|
533
|
+
# }
|
534
|
+
end
|
535
|
+
|
536
|
+
|
537
|
+
def retrieve_field_values(object_type, fields, ids, name = nil)
|
538
|
+
msg = "retrieve(#{object_type}, [#{ids.to_a.join(', ')}])"
|
539
|
+
# log(msg, name) {
|
540
|
+
retrieve_element = []
|
541
|
+
retrieve_element << :fieldList << fields.to_a.join(", ")
|
542
|
+
retrieve_element << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << object_type
|
543
|
+
ids.to_a.each { |id| retrieve_element << :ids << id }
|
544
|
+
|
545
|
+
result = get_result(@connection.retrieve(retrieve_element), :retrieve)
|
546
|
+
|
547
|
+
result = [ result ] unless result.is_a?(Array)
|
548
|
+
|
549
|
+
# Remove unwanted :type and normalize :Id if required
|
550
|
+
field_values = []
|
551
|
+
result.each do |v|
|
552
|
+
v = v.dup
|
553
|
+
v.delete(:type)
|
554
|
+
v[:Id] = v[:Id][0] if v[:Id].is_a? Array
|
555
|
+
|
556
|
+
field_values << v
|
557
|
+
end
|
558
|
+
|
559
|
+
field_values
|
560
|
+
# }
|
561
|
+
end
|
562
|
+
|
563
|
+
|
564
|
+
def get_fields(columns, names, values, access_check)
|
565
|
+
fields = {}
|
566
|
+
names.each_with_index do | name, n |
|
567
|
+
value = values[n]
|
568
|
+
|
569
|
+
if value
|
570
|
+
column = columns[name]
|
571
|
+
|
572
|
+
raise ActiveSalesforce::ASFError.new(@logger, "Column not found for #{name}!") unless column
|
573
|
+
|
574
|
+
value.gsub!(/''/, "'") if value.is_a? String
|
575
|
+
|
576
|
+
include_field = ((not value.empty?) and column.send(access_check))
|
577
|
+
|
578
|
+
if (include_field)
|
579
|
+
case column.type
|
580
|
+
when :date
|
581
|
+
value = Time.parse(value + "Z").utc.strftime("%Y-%m-%d")
|
582
|
+
when :datetime
|
583
|
+
value = Time.parse(value + "Z").utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
584
|
+
end
|
585
|
+
|
586
|
+
fields[column.api_name] = value
|
587
|
+
end
|
588
|
+
end
|
589
|
+
end
|
590
|
+
|
591
|
+
fields
|
592
|
+
end
|
593
|
+
|
594
|
+
def get_null_fields(columns, names, values, access_check)
|
595
|
+
fields = {}
|
596
|
+
names.each_with_index do | name, n |
|
597
|
+
value = values[n]
|
598
|
+
|
599
|
+
if !value
|
600
|
+
column = columns[name]
|
601
|
+
fields[column.api_name] = nil if column.send(access_check) && column.api_name.casecmp("ownerid") != 0
|
602
|
+
end
|
603
|
+
end
|
604
|
+
|
605
|
+
fields
|
606
|
+
end
|
607
|
+
|
608
|
+
def extract_sql_modifier(soql, modifier)
|
609
|
+
value = soql.match(/\s+#{modifier}\s+(\d+)/mi)
|
610
|
+
if value
|
611
|
+
value = value[1].to_i
|
612
|
+
soql.sub!(/\s+#{modifier}\s+\d+/mi, "")
|
613
|
+
end
|
614
|
+
|
615
|
+
value
|
616
|
+
end
|
617
|
+
|
618
|
+
|
619
|
+
def get_result(response, method)
|
620
|
+
responseName = (method.to_s + "Response").to_sym
|
621
|
+
finalResponse = response[responseName]
|
622
|
+
|
623
|
+
raise ActiveSalesforce::ASFError.new(@logger, response[:Fault][:faultstring], response.fault) unless finalResponse
|
624
|
+
|
625
|
+
result = finalResponse[:result]
|
626
|
+
end
|
627
|
+
|
628
|
+
|
629
|
+
def check_result(result)
|
630
|
+
result = [ result ] unless result.is_a?(Array)
|
631
|
+
|
632
|
+
result.each do |r|
|
633
|
+
raise ActiveSalesforce::ASFError.new(@logger, r[:errors], r[:errors][:message]) unless r[:success] == "true"
|
634
|
+
end
|
635
|
+
|
636
|
+
result
|
637
|
+
end
|
638
|
+
|
639
|
+
|
640
|
+
def get_entity_def(entity_name)
|
641
|
+
cached_entity_def = @entity_def_map[entity_name]
|
642
|
+
|
643
|
+
if cached_entity_def
|
644
|
+
# Check for the loss of asf AR setup
|
645
|
+
entity_klass = class_from_entity_name(entity_name)
|
646
|
+
|
647
|
+
configure_active_record(cached_entity_def) unless entity_klass.respond_to?(:asf_augmented?)
|
648
|
+
|
649
|
+
return cached_entity_def
|
650
|
+
end
|
651
|
+
|
652
|
+
cached_columns = []
|
653
|
+
cached_relationships = []
|
654
|
+
|
655
|
+
begin
|
656
|
+
metadata = get_result(@connection.describeSObject(:sObjectType => entity_name), :describeSObject)
|
657
|
+
custom = false
|
658
|
+
rescue ActiveSalesforce::ASFError
|
659
|
+
# Fallback and see if we can find a custom object with this name
|
660
|
+
debug(" Unable to find medata for '#{entity_name}', falling back to custom object name #{entity_name + "__c"}")
|
661
|
+
|
662
|
+
metadata = get_result(@connection.describeSObject(:sObjectType => entity_name + "__c"), :describeSObject)
|
663
|
+
custom = true
|
664
|
+
end
|
665
|
+
|
666
|
+
metadata[:fields].each do |field|
|
667
|
+
column = SalesforceColumn.new(field)
|
668
|
+
cached_columns << column
|
669
|
+
|
670
|
+
cached_relationships << SalesforceRelationship.new(field, column) if field[:type] =~ /reference/mi
|
671
|
+
end
|
672
|
+
|
673
|
+
relationships = metadata[:childRelationships]
|
674
|
+
if relationships
|
675
|
+
relationships = [ relationships ] unless relationships.is_a? Array
|
676
|
+
|
677
|
+
relationships.each do |relationship|
|
678
|
+
if relationship[:cascadeDelete] == "true"
|
679
|
+
r = SalesforceRelationship.new(relationship)
|
680
|
+
cached_relationships << r
|
681
|
+
end
|
682
|
+
end
|
683
|
+
end
|
684
|
+
|
685
|
+
key_prefix = metadata[:keyPrefix]
|
686
|
+
|
687
|
+
entity_def = ActiveSalesforce::EntityDefinition.new(self, entity_name, entity_klass,
|
688
|
+
cached_columns, cached_relationships, custom, key_prefix)
|
689
|
+
|
690
|
+
@entity_def_map[entity_name] = entity_def
|
691
|
+
@keyprefix_to_entity_def_map[key_prefix] = entity_def
|
692
|
+
|
693
|
+
configure_active_record(entity_def)
|
694
|
+
|
695
|
+
entity_def
|
696
|
+
end
|
697
|
+
|
698
|
+
|
699
|
+
def configure_active_record(entity_def)
|
700
|
+
entity_name = entity_def.name
|
701
|
+
klass = class_from_entity_name(entity_name)
|
702
|
+
|
703
|
+
class << klass
|
704
|
+
def asf_augmented?
|
705
|
+
true
|
706
|
+
end
|
707
|
+
end
|
708
|
+
|
709
|
+
# Add support for SID-based authentication
|
710
|
+
ActiveSalesforce::SessionIDAuthenticationFilter.register(klass)
|
711
|
+
|
712
|
+
klass.set_inheritance_column nil unless entity_def.custom?
|
713
|
+
klass.set_primary_key "id"
|
714
|
+
|
715
|
+
# Create relationships for any reference field
|
716
|
+
entity_def.relationships.each do |relationship|
|
717
|
+
referenceName = relationship.name
|
718
|
+
unless self.respond_to? referenceName.to_sym or relationship.reference_to == "Profile"
|
719
|
+
reference_to = relationship.reference_to
|
720
|
+
one_to_many = relationship.one_to_many
|
721
|
+
foreign_key = relationship.foreign_key
|
722
|
+
|
723
|
+
# DCHASMAN TODO Figure out how to handle polymorphic refs (e.g. Note.parent can refer to
|
724
|
+
# Account, Contact, Opportunity, Contract, Asset, Product2, <CustomObject1> ... <CustomObject(n)>
|
725
|
+
if reference_to.is_a? Array
|
726
|
+
debug(" Skipping unsupported polymophic one-to-#{one_to_many ? 'many' : 'one' } relationship '#{referenceName}' from #{klass} to [#{relationship.reference_to.join(', ')}] using #{foreign_key}")
|
727
|
+
next
|
728
|
+
end
|
729
|
+
|
730
|
+
# Handle references to custom objects
|
731
|
+
reference_to = reference_to.chomp("__c").camelize if reference_to.match(/__c$/)
|
732
|
+
|
733
|
+
begin
|
734
|
+
referenced_klass = class_from_entity_name(reference_to)
|
735
|
+
rescue NameError => e
|
736
|
+
# Automatically create a least a stub for the referenced entity
|
737
|
+
debug(" Creating ActiveRecord stub for the referenced entity '#{reference_to}'")
|
738
|
+
|
739
|
+
referenced_klass = klass.class_eval("Salesforce::#{reference_to} = Class.new(ActiveRecord::Base)")
|
740
|
+
referenced_klass.instance_variable_set("@asf_connection", klass.connection)
|
741
|
+
|
742
|
+
# Automatically inherit the connection from the referencee
|
743
|
+
def referenced_klass.connection
|
744
|
+
@asf_connection
|
745
|
+
end
|
746
|
+
end
|
747
|
+
|
748
|
+
if referenced_klass
|
749
|
+
if one_to_many
|
750
|
+
assoc_name = reference_to.underscore.pluralize.to_sym
|
751
|
+
klass.has_many assoc_name, :class_name => referenced_klass.name, :foreign_key => foreign_key
|
752
|
+
else
|
753
|
+
assoc_name = reference_to.underscore.singularize.to_sym
|
754
|
+
klass.belongs_to assoc_name, :class_name => referenced_klass.name, :foreign_key => foreign_key
|
755
|
+
end
|
756
|
+
|
757
|
+
debug(" Created one-to-#{one_to_many ? 'many' : 'one' } relationship '#{referenceName}' from #{klass} to #{referenced_klass} using #{foreign_key}")
|
758
|
+
end
|
759
|
+
end
|
760
|
+
end
|
761
|
+
|
762
|
+
end
|
763
|
+
|
764
|
+
|
765
|
+
def columns(table_name, name = nil)
|
766
|
+
table_name, columns, entity_def = lookup(table_name)
|
767
|
+
entity_def.columns
|
768
|
+
end
|
769
|
+
|
770
|
+
def primary_key(table)
|
771
|
+
"id"
|
772
|
+
end
|
773
|
+
|
774
|
+
def class_from_entity_name(entity_name)
|
775
|
+
entity_klass = @class_to_entity_map[entity_name.upcase]
|
776
|
+
debug("Found matching class '#{entity_klass}' for entity '#{entity_name}'") if entity_klass
|
777
|
+
|
778
|
+
# Constantize entities under the Salesforce namespace.
|
779
|
+
entity_klass = ("Salesforce::" + entity_name).constantize unless entity_klass
|
780
|
+
|
781
|
+
entity_klass
|
782
|
+
end
|
783
|
+
|
784
|
+
|
785
|
+
def create_sobject(entity_name, id, fields, null_fields = [])
|
786
|
+
sobj = []
|
787
|
+
|
788
|
+
sobj << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << entity_name
|
789
|
+
sobj << 'Id { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << id if id
|
790
|
+
|
791
|
+
# add any changed fields
|
792
|
+
fields.each do | name, value |
|
793
|
+
sobj << name.to_sym << value if value
|
794
|
+
end
|
795
|
+
|
796
|
+
# add null fields
|
797
|
+
null_fields.each do | name, value |
|
798
|
+
sobj << 'fieldsToNull { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << name
|
799
|
+
end
|
800
|
+
|
801
|
+
[ :sObjects, sobj ]
|
802
|
+
end
|
803
|
+
|
804
|
+
|
805
|
+
def column_names(table_name)
|
806
|
+
columns(table_name).map { |column| column.name }
|
807
|
+
end
|
808
|
+
|
809
|
+
|
810
|
+
def lookup(raw_table_name)
|
811
|
+
table_name = raw_table_name.singularize
|
812
|
+
|
813
|
+
# See if a table name to AR class mapping was registered
|
814
|
+
klass = @class_to_entity_map[table_name.upcase]
|
815
|
+
|
816
|
+
entity_name = klass ? raw_table_name : table_name.camelize
|
817
|
+
entity_def = get_entity_def(entity_name)
|
818
|
+
|
819
|
+
[table_name, entity_def.columns, entity_def]
|
820
|
+
end
|
821
|
+
|
822
|
+
|
823
|
+
def debug(msg)
|
824
|
+
@logger.debug(msg) if @logger
|
825
|
+
end
|
826
|
+
|
827
|
+
protected
|
828
|
+
|
829
|
+
# fix single-quote escape sequence for WHERE condition expressions. Salesforce enforces a backspace on SELECTs
|
830
|
+
# NOTE: this method is only used for SELECT queries. INSERT/UPDATE queries are smart enough to use the primary
|
831
|
+
# key for their WHERE statements, or so I've found.
|
832
|
+
def quote_where(sql)
|
833
|
+
where_match = sql.match(/WHERE\s*\((.*)\)/mi)
|
834
|
+
|
835
|
+
return sql unless where_match
|
836
|
+
where_conditions = where_match[1]
|
837
|
+
|
838
|
+
# debug("where_conditions: #{where_conditions}")
|
839
|
+
where_conditions.gsub!(/''/, "\\\\'")
|
840
|
+
|
841
|
+
# debug("updated where_conditions: #{where_conditions.gsub(/''/, "\\\\'")}")
|
842
|
+
|
843
|
+
sql = "#{where_match.pre_match}WHERE (#{where_conditions})#{where_match.post_match}"
|
844
|
+
end
|
845
|
+
|
846
|
+
def queue_command(command)
|
847
|
+
# puts("Queue: #{command}")
|
848
|
+
# If @command_boxcar is not nil, then this is a transaction
|
849
|
+
# and commands should be queued in the boxcar
|
850
|
+
if @command_boxcar
|
851
|
+
@command_boxcar << command
|
852
|
+
|
853
|
+
# If a command is not executed within a transaction, it should
|
854
|
+
# be executed immediately
|
855
|
+
else
|
856
|
+
send_commands([command])
|
857
|
+
end
|
858
|
+
end
|
859
|
+
|
860
|
+
end
|
861
|
+
|
862
|
+
end
|
863
|
+
end
|