copious-activerecord-activesalesforce-adapter 2.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README +51 -0
- data/lib/active_record/connection_adapters/activesalesforce.rb +36 -0
- data/lib/active_record/connection_adapters/activesalesforce_adapter.rb +784 -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/rforce.rb +361 -0
- data/lib/active_record/connection_adapters/sid_authentication_filter.rb +57 -0
- data/test/unit/basic_test.rb +203 -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 +108 -0
@@ -0,0 +1,784 @@
|
|
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 = []
|
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 table_exists?(table_name)
|
160
|
+
true
|
161
|
+
end
|
162
|
+
|
163
|
+
# QUOTING ==================================================
|
164
|
+
|
165
|
+
def quote(value, column = nil)
|
166
|
+
case value
|
167
|
+
when NilClass then quoted_value = "NULL"
|
168
|
+
when TrueClass then quoted_value = "TRUE"
|
169
|
+
when FalseClass then quoted_value = "FALSE"
|
170
|
+
when Float, Fixnum, Bignum then quoted_value = "'#{value.to_s}'"
|
171
|
+
else quoted_value = super(value, column)
|
172
|
+
end
|
173
|
+
|
174
|
+
quoted_value
|
175
|
+
end
|
176
|
+
|
177
|
+
# CONNECTION MANAGEMENT ====================================
|
178
|
+
|
179
|
+
def active?
|
180
|
+
true
|
181
|
+
end
|
182
|
+
|
183
|
+
|
184
|
+
def reconnect!
|
185
|
+
connect
|
186
|
+
end
|
187
|
+
|
188
|
+
|
189
|
+
# TRANSACTIOn SUPPORT (Boxcarring really because the salesforce.com api does not support transactions)
|
190
|
+
|
191
|
+
# Begins the transaction (and turns off auto-committing).
|
192
|
+
def begin_db_transaction()
|
193
|
+
log('Opening boxcar', 'begin_db_transaction()')
|
194
|
+
@command_boxcar = []
|
195
|
+
end
|
196
|
+
|
197
|
+
|
198
|
+
def send_commands(commands)
|
199
|
+
# Send the boxcar'ed command set
|
200
|
+
verb = commands[0].verb
|
201
|
+
|
202
|
+
args = []
|
203
|
+
commands.each do |command|
|
204
|
+
command.args.each { |arg| args << arg }
|
205
|
+
end
|
206
|
+
|
207
|
+
response = @connection.send(verb, args)
|
208
|
+
|
209
|
+
result = get_result(response, verb)
|
210
|
+
|
211
|
+
result = [ result ] unless result.is_a?(Array)
|
212
|
+
|
213
|
+
errors = []
|
214
|
+
result.each_with_index do |r, n|
|
215
|
+
success = r[:success] == "true"
|
216
|
+
|
217
|
+
# Give each command a chance to process its own result
|
218
|
+
command = commands[n]
|
219
|
+
command.after_execute(r)
|
220
|
+
|
221
|
+
# Handle the set of failures
|
222
|
+
errors << r[:errors] unless r[:success] == "true"
|
223
|
+
end
|
224
|
+
|
225
|
+
unless errors.empty?
|
226
|
+
message = errors.join("\n")
|
227
|
+
fault = (errors.map { |error| error[:message] }).join("\n")
|
228
|
+
raise ActiveSalesforce::ASFError.new(@logger, message, fault)
|
229
|
+
end
|
230
|
+
|
231
|
+
result
|
232
|
+
end
|
233
|
+
|
234
|
+
|
235
|
+
# Commits the transaction (and turns on auto-committing).
|
236
|
+
def commit_db_transaction()
|
237
|
+
log("Committing boxcar with #{@command_boxcar.length} commands", 'commit_db_transaction()')
|
238
|
+
|
239
|
+
previous_command = nil
|
240
|
+
commands = []
|
241
|
+
|
242
|
+
@command_boxcar.each do |command|
|
243
|
+
if commands.length >= MAX_BOXCAR_SIZE or (previous_command and (command.verb != previous_command.verb))
|
244
|
+
send_commands(commands)
|
245
|
+
|
246
|
+
commands = []
|
247
|
+
previous_command = nil
|
248
|
+
else
|
249
|
+
commands << command
|
250
|
+
previous_command = command
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
# Finish off the partial boxcar
|
255
|
+
send_commands(commands) unless commands.empty?
|
256
|
+
|
257
|
+
end
|
258
|
+
|
259
|
+
# Rolls back the transaction (and turns on auto-committing). Must be
|
260
|
+
# done if the transaction block raises an exception or returns false.
|
261
|
+
def rollback_db_transaction()
|
262
|
+
log('Rolling back boxcar', 'rollback_db_transaction()')
|
263
|
+
@command_boxcar = []
|
264
|
+
end
|
265
|
+
|
266
|
+
|
267
|
+
# DATABASE STATEMENTS ======================================
|
268
|
+
|
269
|
+
def select_all(sql, name = nil) #:nodoc:
|
270
|
+
raw_table_name = sql.match(/FROM (\w+)/mi)[1]
|
271
|
+
table_name, columns, entity_def = lookup(raw_table_name)
|
272
|
+
|
273
|
+
column_names = columns.map { |column| column.api_name }
|
274
|
+
|
275
|
+
# Check for SELECT COUNT(*) FROM query
|
276
|
+
|
277
|
+
# Rails 1.1
|
278
|
+
selectCountMatch = sql.match(/SELECT\s+COUNT\(\*\)\s+AS\s+count_all\s+FROM/mi)
|
279
|
+
|
280
|
+
# Rails 1.0
|
281
|
+
selectCountMatch = sql.match(/SELECT\s+COUNT\(\*\)\s+FROM/mi) unless selectCountMatch
|
282
|
+
|
283
|
+
if selectCountMatch
|
284
|
+
soql = "SELECT COUNT() FROM#{selectCountMatch.post_match}"
|
285
|
+
else
|
286
|
+
if sql.match(/SELECT\s+\*\s+FROM/mi)
|
287
|
+
# Always convert SELECT * to select all columns (required for the AR attributes mechanism to work correctly)
|
288
|
+
soql = sql.sub(/SELECT .+ FROM/mi, "SELECT #{column_names.join(', ')} FROM")
|
289
|
+
else
|
290
|
+
soql = sql
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
soql.sub!(/\s+FROM\s+\w+/mi, " FROM #{entity_def.api_name}")
|
295
|
+
|
296
|
+
if selectCountMatch
|
297
|
+
query_result = get_result(@connection.query(:queryString => soql), :query)
|
298
|
+
return [{ :count => query_result[:size] }]
|
299
|
+
end
|
300
|
+
|
301
|
+
# Look for a LIMIT clause
|
302
|
+
limit = extract_sql_modifier(soql, "LIMIT")
|
303
|
+
limit = MAX_BOXCAR_SIZE unless limit
|
304
|
+
|
305
|
+
# Look for an OFFSET clause
|
306
|
+
offset = extract_sql_modifier(soql, "OFFSET")
|
307
|
+
|
308
|
+
# Fixup column references to use api names
|
309
|
+
columns = entity_def.column_name_to_column
|
310
|
+
soql.gsub!(/((?:\w+\.)?\w+)(?=\s*(?:=|!=|<|>|<=|>=|like)\s*(?:'[^']*'|NULL|TRUE|FALSE))/mi) do |column_name|
|
311
|
+
# strip away any table alias
|
312
|
+
column_name.sub!(/\w+\./, '')
|
313
|
+
|
314
|
+
column = columns[column_name]
|
315
|
+
raise ActiveSalesforce::ASFError.new(@logger, "Column not found for #{column_name}!") unless column
|
316
|
+
|
317
|
+
column.api_name
|
318
|
+
end
|
319
|
+
|
320
|
+
# Update table name references
|
321
|
+
soql.sub!(/#{raw_table_name}\./mi, "#{entity_def.api_name}.")
|
322
|
+
|
323
|
+
@connection.batch_size = @batch_size if @batch_size
|
324
|
+
@batch_size = nil
|
325
|
+
|
326
|
+
query_result = get_result(@connection.query(:queryString => soql), :query)
|
327
|
+
result = ActiveSalesforce::ResultArray.new(query_result[:size].to_i)
|
328
|
+
return result unless query_result[:records]
|
329
|
+
|
330
|
+
add_rows(entity_def, query_result, result, limit)
|
331
|
+
|
332
|
+
while ((query_result[:done].casecmp("true") != 0) and (result.size < limit or limit == 0))
|
333
|
+
# Now queryMore
|
334
|
+
locator = query_result[:queryLocator];
|
335
|
+
query_result = get_result(@connection.queryMore(:queryLocator => locator), :queryMore)
|
336
|
+
|
337
|
+
add_rows(entity_def, query_result, result, limit)
|
338
|
+
end
|
339
|
+
|
340
|
+
result
|
341
|
+
end
|
342
|
+
|
343
|
+
def add_rows(entity_def, query_result, result, limit)
|
344
|
+
records = query_result[:records]
|
345
|
+
records = [ records ] unless records.is_a?(Array)
|
346
|
+
|
347
|
+
records.each do |record|
|
348
|
+
row = {}
|
349
|
+
|
350
|
+
record.each do |name, value|
|
351
|
+
if name != :type
|
352
|
+
# Ids may be returned in an array with 2 duplicate entries...
|
353
|
+
value = value[0] if name == :Id && value.is_a?(Array)
|
354
|
+
|
355
|
+
column = entity_def.api_name_to_column[name.to_s]
|
356
|
+
attribute_name = column.name
|
357
|
+
|
358
|
+
if column.type == :boolean
|
359
|
+
row[attribute_name] = (value.casecmp("true") == 0)
|
360
|
+
else
|
361
|
+
row[attribute_name] = value
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
result << row
|
367
|
+
|
368
|
+
break if result.size >= limit and limit != 0
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
def select_one(sql, name = nil) #:nodoc:
|
373
|
+
self.batch_size = 1
|
374
|
+
|
375
|
+
result = select_all(sql, name)
|
376
|
+
|
377
|
+
result.nil? ? nil : result.first
|
378
|
+
end
|
379
|
+
|
380
|
+
|
381
|
+
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
|
382
|
+
log(sql, name) {
|
383
|
+
# Convert sql to sobject
|
384
|
+
table_name, columns, entity_def = lookup(sql.match(/INSERT\s+INTO\s+(\w+)\s+/mi)[1])
|
385
|
+
columns = entity_def.column_name_to_column
|
386
|
+
|
387
|
+
# Extract array of column names
|
388
|
+
names = sql.match(/\((.+)\)\s+VALUES/mi)[1].scan(/\w+/mi)
|
389
|
+
|
390
|
+
# Extract arrays of values
|
391
|
+
values = sql.match(/VALUES\s*\((.+)\)/mi)[1]
|
392
|
+
values = values.scan(/(NULL|TRUE|FALSE|'(?:(?:[^']|'')*)'),*/mi).flatten
|
393
|
+
values.map! { |v| v.first == "'" ? v.slice(1, v.length - 2) : v == "NULL" ? nil : v }
|
394
|
+
|
395
|
+
fields = get_fields(columns, names, values, :createable)
|
396
|
+
|
397
|
+
sobject = create_sobject(entity_def.api_name, nil, fields)
|
398
|
+
|
399
|
+
# Track the id to be able to update it when the create() is actually executed
|
400
|
+
id = String.new
|
401
|
+
@command_boxcar << ActiveSalesforce::BoxcarCommand::Insert.new(self, sobject, id)
|
402
|
+
|
403
|
+
id
|
404
|
+
}
|
405
|
+
end
|
406
|
+
|
407
|
+
|
408
|
+
def update(sql, name = nil) #:nodoc:
|
409
|
+
#log(sql, name) {
|
410
|
+
# Convert sql to sobject
|
411
|
+
table_name, columns, entity_def = lookup(sql.match(/UPDATE\s+(\w+)\s+/mi)[1])
|
412
|
+
columns = entity_def.column_name_to_column
|
413
|
+
|
414
|
+
match = sql.match(/SET\s+(.+)\s+WHERE/mi)[1]
|
415
|
+
names = match.scan(/(\w+)\s*=\s*(?:'|NULL|TRUE|FALSE)/mi).flatten
|
416
|
+
|
417
|
+
values = match.scan(/=\s*(NULL|TRUE|FALSE|'(?:(?:[^']|'')*)'),*/mi).flatten
|
418
|
+
values.map! { |v| v.first == "'" ? v.slice(1, v.length - 2) : v == "NULL" ? nil : v }
|
419
|
+
|
420
|
+
fields = get_fields(columns, names, values, :updateable)
|
421
|
+
null_fields = get_null_fields(columns, names, values, :updateable)
|
422
|
+
|
423
|
+
ids = sql.match(/WHERE\s+id\s*=\s*'(\w+)'/mi)
|
424
|
+
return if ids.nil?
|
425
|
+
id = ids[1]
|
426
|
+
|
427
|
+
sobject = create_sobject(entity_def.api_name, id, fields, null_fields)
|
428
|
+
|
429
|
+
@command_boxcar << ActiveSalesforce::BoxcarCommand::Update.new(self, sobject)
|
430
|
+
#}
|
431
|
+
end
|
432
|
+
|
433
|
+
|
434
|
+
def delete(sql, name = nil)
|
435
|
+
log(sql, name) {
|
436
|
+
# Extract the id
|
437
|
+
match = sql.match(/WHERE\s+id\s*=\s*'(\w+)'/mi)
|
438
|
+
|
439
|
+
if match
|
440
|
+
ids = [ match[1] ]
|
441
|
+
else
|
442
|
+
# Check for the form (id IN ('x', 'y'))
|
443
|
+
match = sql.match(/WHERE\s+\(\s*id\s+IN\s*\((.+)\)\)/mi)[1]
|
444
|
+
ids = match.scan(/\w+/)
|
445
|
+
end
|
446
|
+
|
447
|
+
ids_element = []
|
448
|
+
ids.each { |id| ids_element << :ids << id }
|
449
|
+
|
450
|
+
@command_boxcar << ActiveSalesforce::BoxcarCommand::Delete.new(self, ids_element)
|
451
|
+
}
|
452
|
+
end
|
453
|
+
|
454
|
+
|
455
|
+
def get_updated(object_type, start_date, end_date, name = nil)
|
456
|
+
msg = "get_updated(#{object_type}, #{start_date}, #{end_date})"
|
457
|
+
log(msg, name) {
|
458
|
+
get_updated_element = []
|
459
|
+
get_updated_element << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << object_type
|
460
|
+
get_updated_element << :startDate << start_date
|
461
|
+
get_updated_element << :endDate << end_date
|
462
|
+
|
463
|
+
result = get_result(@connection.getUpdated(get_updated_element), :getUpdated)
|
464
|
+
|
465
|
+
result[:ids]
|
466
|
+
}
|
467
|
+
end
|
468
|
+
|
469
|
+
|
470
|
+
def get_deleted(object_type, start_date, end_date, name = nil)
|
471
|
+
msg = "get_deleted(#{object_type}, #{start_date}, #{end_date})"
|
472
|
+
log(msg, name) {
|
473
|
+
get_deleted_element = []
|
474
|
+
get_deleted_element << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << object_type
|
475
|
+
get_deleted_element << :startDate << start_date
|
476
|
+
get_deleted_element << :endDate << end_date
|
477
|
+
|
478
|
+
result = get_result(@connection.getDeleted(get_deleted_element), :getDeleted)
|
479
|
+
|
480
|
+
ids = []
|
481
|
+
result[:deletedRecords].each do |v|
|
482
|
+
ids << v[:id]
|
483
|
+
end
|
484
|
+
|
485
|
+
ids
|
486
|
+
}
|
487
|
+
end
|
488
|
+
|
489
|
+
|
490
|
+
def get_user_info(name = nil)
|
491
|
+
msg = "get_user_info()"
|
492
|
+
log(msg, name) {
|
493
|
+
get_result(@connection.getUserInfo([]), :getUserInfo)
|
494
|
+
}
|
495
|
+
end
|
496
|
+
|
497
|
+
|
498
|
+
def retrieve_field_values(object_type, fields, ids, name = nil)
|
499
|
+
msg = "retrieve(#{object_type}, [#{ids.to_a.join(', ')}])"
|
500
|
+
log(msg, name) {
|
501
|
+
retrieve_element = []
|
502
|
+
retrieve_element << :fieldList << fields.to_a.join(", ")
|
503
|
+
retrieve_element << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << object_type
|
504
|
+
ids.to_a.each { |id| retrieve_element << :ids << id }
|
505
|
+
|
506
|
+
result = get_result(@connection.retrieve(retrieve_element), :retrieve)
|
507
|
+
|
508
|
+
result = [ result ] unless result.is_a?(Array)
|
509
|
+
|
510
|
+
# Remove unwanted :type and normalize :Id if required
|
511
|
+
field_values = []
|
512
|
+
result.each do |v|
|
513
|
+
v = v.dup
|
514
|
+
v.delete(:type)
|
515
|
+
v[:Id] = v[:Id][0] if v[:Id].is_a? Array
|
516
|
+
|
517
|
+
field_values << v
|
518
|
+
end
|
519
|
+
|
520
|
+
field_values
|
521
|
+
}
|
522
|
+
end
|
523
|
+
|
524
|
+
|
525
|
+
def get_fields(columns, names, values, access_check)
|
526
|
+
fields = {}
|
527
|
+
names.each_with_index do | name, n |
|
528
|
+
value = values[n]
|
529
|
+
|
530
|
+
if value
|
531
|
+
column = columns[name]
|
532
|
+
|
533
|
+
raise ActiveSalesforce::ASFError.new(@logger, "Column not found for #{name}!") unless column
|
534
|
+
|
535
|
+
value.gsub!(/''/, "'") if value.is_a? String
|
536
|
+
|
537
|
+
include_field = ((not value.empty?) and column.send(access_check))
|
538
|
+
|
539
|
+
if (include_field)
|
540
|
+
case column.type
|
541
|
+
when :date
|
542
|
+
value = Time.parse(value + "Z").utc.strftime("%Y-%m-%d")
|
543
|
+
when :datetime
|
544
|
+
value = Time.parse(value + "Z").utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
545
|
+
end
|
546
|
+
|
547
|
+
fields[column.api_name] = value
|
548
|
+
end
|
549
|
+
end
|
550
|
+
end
|
551
|
+
|
552
|
+
fields
|
553
|
+
end
|
554
|
+
|
555
|
+
def get_null_fields(columns, names, values, access_check)
|
556
|
+
fields = {}
|
557
|
+
names.each_with_index do | name, n |
|
558
|
+
value = values[n]
|
559
|
+
|
560
|
+
if !value
|
561
|
+
column = columns[name]
|
562
|
+
fields[column.api_name] = nil if column.send(access_check) && column.api_name.casecmp("ownerid") != 0
|
563
|
+
end
|
564
|
+
end
|
565
|
+
|
566
|
+
fields
|
567
|
+
end
|
568
|
+
|
569
|
+
def extract_sql_modifier(soql, modifier)
|
570
|
+
value = soql.match(/\s+#{modifier}\s+(\d+)/mi)
|
571
|
+
if value
|
572
|
+
value = value[1].to_i
|
573
|
+
soql.sub!(/\s+#{modifier}\s+\d+/mi, "")
|
574
|
+
end
|
575
|
+
|
576
|
+
value
|
577
|
+
end
|
578
|
+
|
579
|
+
|
580
|
+
def get_result(response, method)
|
581
|
+
responseName = (method.to_s + "Response").to_sym
|
582
|
+
finalResponse = response[responseName]
|
583
|
+
|
584
|
+
raise ActiveSalesforce::ASFError.new(@logger, response[:Fault][:faultstring], response.fault) unless finalResponse
|
585
|
+
|
586
|
+
result = finalResponse[:result]
|
587
|
+
end
|
588
|
+
|
589
|
+
|
590
|
+
def check_result(result)
|
591
|
+
result = [ result ] unless result.is_a?(Array)
|
592
|
+
|
593
|
+
result.each do |r|
|
594
|
+
raise ActiveSalesforce::ASFError.new(@logger, r[:errors], r[:errors][:message]) unless r[:success] == "true"
|
595
|
+
end
|
596
|
+
|
597
|
+
result
|
598
|
+
end
|
599
|
+
|
600
|
+
|
601
|
+
def get_entity_def(entity_name)
|
602
|
+
cached_entity_def = @entity_def_map[entity_name]
|
603
|
+
|
604
|
+
if cached_entity_def
|
605
|
+
# Check for the loss of asf AR setup
|
606
|
+
entity_klass = class_from_entity_name(entity_name)
|
607
|
+
|
608
|
+
configure_active_record(cached_entity_def) unless entity_klass.respond_to?(:asf_augmented?)
|
609
|
+
|
610
|
+
return cached_entity_def
|
611
|
+
end
|
612
|
+
|
613
|
+
cached_columns = []
|
614
|
+
cached_relationships = []
|
615
|
+
|
616
|
+
begin
|
617
|
+
metadata = get_result(@connection.describeSObject(:sObjectType => entity_name), :describeSObject)
|
618
|
+
custom = false
|
619
|
+
rescue ActiveSalesforce::ASFError
|
620
|
+
# Fallback and see if we can find a custom object with this name
|
621
|
+
debug(" Unable to find medata for '#{entity_name}', falling back to custom object name #{entity_name + "__c"}")
|
622
|
+
|
623
|
+
metadata = get_result(@connection.describeSObject(:sObjectType => entity_name + "__c"), :describeSObject)
|
624
|
+
custom = true
|
625
|
+
end
|
626
|
+
|
627
|
+
metadata[:fields].each do |field|
|
628
|
+
column = SalesforceColumn.new(field)
|
629
|
+
cached_columns << column
|
630
|
+
|
631
|
+
cached_relationships << SalesforceRelationship.new(field, column) if field[:type] =~ /reference/mi
|
632
|
+
end
|
633
|
+
|
634
|
+
relationships = metadata[:childRelationships]
|
635
|
+
if relationships
|
636
|
+
relationships = [ relationships ] unless relationships.is_a? Array
|
637
|
+
|
638
|
+
relationships.each do |relationship|
|
639
|
+
if relationship[:cascadeDelete] == "true"
|
640
|
+
r = SalesforceRelationship.new(relationship)
|
641
|
+
cached_relationships << r
|
642
|
+
end
|
643
|
+
end
|
644
|
+
end
|
645
|
+
|
646
|
+
key_prefix = metadata[:keyPrefix]
|
647
|
+
|
648
|
+
entity_def = ActiveSalesforce::EntityDefinition.new(self, entity_name, entity_klass,
|
649
|
+
cached_columns, cached_relationships, custom, key_prefix)
|
650
|
+
|
651
|
+
@entity_def_map[entity_name] = entity_def
|
652
|
+
@keyprefix_to_entity_def_map[key_prefix] = entity_def
|
653
|
+
|
654
|
+
configure_active_record(entity_def)
|
655
|
+
|
656
|
+
entity_def
|
657
|
+
end
|
658
|
+
|
659
|
+
|
660
|
+
def configure_active_record(entity_def)
|
661
|
+
entity_name = entity_def.name
|
662
|
+
klass = class_from_entity_name(entity_name)
|
663
|
+
|
664
|
+
class << klass
|
665
|
+
def asf_augmented?
|
666
|
+
true
|
667
|
+
end
|
668
|
+
end
|
669
|
+
|
670
|
+
# Add support for SID-based authentication
|
671
|
+
ActiveSalesforce::SessionIDAuthenticationFilter.register(klass)
|
672
|
+
|
673
|
+
klass.set_inheritance_column nil unless entity_def.custom?
|
674
|
+
klass.set_primary_key "id"
|
675
|
+
|
676
|
+
# Create relationships for any reference field
|
677
|
+
entity_def.relationships.each do |relationship|
|
678
|
+
referenceName = relationship.name
|
679
|
+
unless self.respond_to? referenceName.to_sym or relationship.reference_to == "Profile"
|
680
|
+
reference_to = relationship.reference_to
|
681
|
+
one_to_many = relationship.one_to_many
|
682
|
+
foreign_key = relationship.foreign_key
|
683
|
+
|
684
|
+
# DCHASMAN TODO Figure out how to handle polymorphic refs (e.g. Note.parent can refer to
|
685
|
+
# Account, Contact, Opportunity, Contract, Asset, Product2, <CustomObject1> ... <CustomObject(n)>
|
686
|
+
if reference_to.is_a? Array
|
687
|
+
debug(" Skipping unsupported polymophic one-to-#{one_to_many ? 'many' : 'one' } relationship '#{referenceName}' from #{klass} to [#{relationship.reference_to.join(', ')}] using #{foreign_key}")
|
688
|
+
next
|
689
|
+
end
|
690
|
+
|
691
|
+
# Handle references to custom objects
|
692
|
+
reference_to = reference_to.chomp("__c").capitalize if reference_to.match(/__c$/)
|
693
|
+
|
694
|
+
begin
|
695
|
+
referenced_klass = class_from_entity_name(reference_to)
|
696
|
+
rescue NameError => e
|
697
|
+
# Automatically create a least a stub for the referenced entity
|
698
|
+
debug(" Creating ActiveRecord stub for the referenced entity '#{reference_to}'")
|
699
|
+
|
700
|
+
referenced_klass = klass.class_eval("::#{reference_to} = Class.new(ActiveRecord::Base)")
|
701
|
+
|
702
|
+
# Automatically inherit the connection from the referencee
|
703
|
+
referenced_klass.connection = klass.connection
|
704
|
+
end
|
705
|
+
|
706
|
+
if referenced_klass
|
707
|
+
if one_to_many
|
708
|
+
klass.has_many referenceName.to_sym, :class_name => referenced_klass.name, :foreign_key => foreign_key
|
709
|
+
else
|
710
|
+
klass.belongs_to referenceName.to_sym, :class_name => referenced_klass.name, :foreign_key => foreign_key
|
711
|
+
end
|
712
|
+
|
713
|
+
debug(" Created one-to-#{one_to_many ? 'many' : 'one' } relationship '#{referenceName}' from #{klass} to #{referenced_klass} using #{foreign_key}")
|
714
|
+
end
|
715
|
+
end
|
716
|
+
end
|
717
|
+
|
718
|
+
end
|
719
|
+
|
720
|
+
|
721
|
+
def columns(table_name, name = nil)
|
722
|
+
table_name, columns, entity_def = lookup(table_name)
|
723
|
+
entity_def.columns
|
724
|
+
end
|
725
|
+
|
726
|
+
|
727
|
+
def class_from_entity_name(entity_name)
|
728
|
+
entity_klass = @class_to_entity_map[entity_name.upcase]
|
729
|
+
debug("Found matching class '#{entity_klass}' for entity '#{entity_name}'") if entity_klass
|
730
|
+
|
731
|
+
# Try to constantize entities under the Salesforce namespace; but
|
732
|
+
# fall back on un-namespaced classes.
|
733
|
+
entity_klass = ("Salesforce::" + entity_name).constantize rescue entity_name.constantize unless entity_klass
|
734
|
+
|
735
|
+
entity_klass
|
736
|
+
end
|
737
|
+
|
738
|
+
|
739
|
+
def create_sobject(entity_name, id, fields, null_fields = [])
|
740
|
+
sobj = []
|
741
|
+
|
742
|
+
sobj << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << entity_name
|
743
|
+
sobj << 'Id { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << id if id
|
744
|
+
|
745
|
+
# add any changed fields
|
746
|
+
fields.each do | name, value |
|
747
|
+
sobj << name.to_sym << value if value
|
748
|
+
end
|
749
|
+
|
750
|
+
# add null fields
|
751
|
+
null_fields.each do | name, value |
|
752
|
+
sobj << 'fieldsToNull { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << name
|
753
|
+
end
|
754
|
+
|
755
|
+
[ :sObjects, sobj ]
|
756
|
+
end
|
757
|
+
|
758
|
+
|
759
|
+
def column_names(table_name)
|
760
|
+
columns(table_name).map { |column| column.name }
|
761
|
+
end
|
762
|
+
|
763
|
+
|
764
|
+
def lookup(raw_table_name)
|
765
|
+
table_name = raw_table_name.singularize
|
766
|
+
|
767
|
+
# See if a table name to AR class mapping was registered
|
768
|
+
klass = @class_to_entity_map[table_name.upcase]
|
769
|
+
|
770
|
+
entity_name = klass ? raw_table_name : table_name.camelize
|
771
|
+
entity_def = get_entity_def(entity_name)
|
772
|
+
|
773
|
+
[table_name, entity_def.columns, entity_def]
|
774
|
+
end
|
775
|
+
|
776
|
+
|
777
|
+
def debug(msg)
|
778
|
+
@logger.debug(msg) if @logger
|
779
|
+
end
|
780
|
+
|
781
|
+
end
|
782
|
+
|
783
|
+
end
|
784
|
+
end
|