activesalesforce 0.2.8 → 0.2.9
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/lib/asf_adapter.rb +189 -187
- data/test/unit/profiler_results.txt +1072 -0
- data/test/unit/profiler_results_live.txt +1347 -0
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_add_notes_to_contact.recording +558 -558
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_count_contacts.recording +555 -555
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_create_a_contact.recording +463 -463
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_a_contact.recording +460 -460
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_a_contact_by_first_name.recording +473 -473
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_a_contact_by_id.recording +433 -433
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_get_created_by_from_contact.recording +2291 -2291
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_master_detail.recording +735 -735
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_read_all_content_columns.recording +463 -463
- data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_save_a_contact.recording +444 -444
- data/test/unit/recorded_test_case.rb +2 -0
- metadata +4 -2
data/lib/asf_adapter.rb
CHANGED
@@ -25,6 +25,7 @@ require 'rubygems'
|
|
25
25
|
require_gem 'rails', ">= 1.0.0"
|
26
26
|
|
27
27
|
require 'thread'
|
28
|
+
require 'benchmark'
|
28
29
|
|
29
30
|
require File.dirname(__FILE__) + '/rforce'
|
30
31
|
require File.dirname(__FILE__) + '/column_definition'
|
@@ -44,30 +45,29 @@ module ActiveRecord
|
|
44
45
|
|
45
46
|
# Establishes a connection to the database that's used by all Active Record objects.
|
46
47
|
def self.activesalesforce_connection(config) # :nodoc:
|
47
|
-
|
48
|
+
logger.debug("\nUsing ActiveSalesforce connection\n")
|
48
49
|
|
49
50
|
url = config[:url]
|
50
51
|
sid = config[:sid]
|
51
52
|
binding = config[:binding] if config[:binding]
|
52
53
|
|
53
54
|
if binding
|
54
|
-
|
55
|
-
#pp binding
|
55
|
+
logger.debug(" via provided binding #{binding}\n")
|
56
56
|
end
|
57
57
|
|
58
58
|
if sid
|
59
59
|
binding = @@cache["sid=#{sid}"] unless binding
|
60
60
|
|
61
61
|
unless binding
|
62
|
-
|
62
|
+
logger.debug("Establishing new connection for [sid='#{sid}']")
|
63
63
|
|
64
64
|
binding = RForce::Binding.new(url, sid)
|
65
65
|
@@cache["sid=#{sid}"] = binding
|
66
66
|
|
67
|
-
|
67
|
+
logger.debug("Created new connection for [sid='#{sid}']")
|
68
68
|
end
|
69
69
|
|
70
|
-
ConnectionAdapters::SalesforceAdapter.new(
|
70
|
+
ConnectionAdapters::SalesforceAdapter.new(binding, logger, [url, sid], config)
|
71
71
|
else
|
72
72
|
# Default to production system using 7.0 API
|
73
73
|
url = "https://www.salesforce.com/services/Soap/u/7.0" unless url
|
@@ -81,14 +81,16 @@ module ActiveRecord
|
|
81
81
|
binding = @@cache["#{url}.#{username}.#{password}"] unless binding
|
82
82
|
|
83
83
|
unless binding
|
84
|
-
|
84
|
+
logger.debug("Establishing new connection for ['#{url}', '#{username}']")
|
85
85
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
86
|
+
seconds = Benchmark.realtime {
|
87
|
+
binding = RForce::Binding.new(url, sid)
|
88
|
+
binding.login(username, password).result
|
89
|
+
|
90
|
+
@@cache["#{url}.#{username}.#{password}"] = binding
|
91
|
+
}
|
90
92
|
|
91
|
-
|
93
|
+
logger.debug("Created new connection for ['#{url}', '#{username}'] in #{seconds} seconds")
|
92
94
|
end
|
93
95
|
|
94
96
|
ConnectionAdapters::SalesforceAdapter.new(binding, logger, [url, username, password, sid], config)
|
@@ -106,7 +108,7 @@ module ActiveRecord
|
|
106
108
|
|
107
109
|
@fault = fault
|
108
110
|
|
109
|
-
logger.debug("\nSalesforceError:\n message='#{message}'\n fault='#{fault}'\n\n")
|
111
|
+
logger.debug("\nSalesforceError:\n message='#{message}'\n fault='#{fault}'\n\n")
|
110
112
|
end
|
111
113
|
end
|
112
114
|
|
@@ -190,166 +192,162 @@ module ActiveRecord
|
|
190
192
|
# DATABASE STATEMENTS ======================================
|
191
193
|
|
192
194
|
def select_all(sql, name = nil) #:nodoc:
|
193
|
-
log(sql, name)
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
end
|
200
|
-
|
201
|
-
raw_table_name = sql.match(/FROM (\w+)/i)[1]
|
202
|
-
table_name = raw_table_name.singularize
|
203
|
-
entity_name = entity_name_from_table(table_name)
|
204
|
-
entity_def = get_entity_def(entity_name)
|
205
|
-
|
206
|
-
column_names = api_column_names(table_name)
|
207
|
-
|
208
|
-
# Always (unless COUNT*)'ing) select all columns (required for the AR attributes mechanism to work correctly
|
209
|
-
soql = sql.sub(/SELECT .+ FROM/i, "SELECT #{column_names.join(', ')} FROM") unless selectCountMatch
|
210
|
-
|
211
|
-
soql.sub!(/ FROM \w+/i, " FROM #{entity_def.api_name}")
|
212
|
-
|
213
|
-
# Look for a LIMIT clause
|
214
|
-
soql.sub!(/LIMIT 1/i, "")
|
215
|
-
|
216
|
-
# Look for an OFFSET clause
|
217
|
-
soql.sub!(/\d+ OFFSET \d+/i, "")
|
218
|
-
|
219
|
-
# Fixup column references to use api names
|
220
|
-
columns = columns_map(table_name)
|
221
|
-
while soql =~ /\w+\.(\w+)/i
|
222
|
-
column_name = $~[1]
|
195
|
+
log(sql, name) {
|
196
|
+
# Check for SELECT COUNT(*) FROM query
|
197
|
+
selectCountMatch = sql.match(/SELECT COUNT\(\*\) FROM/i)
|
198
|
+
if selectCountMatch
|
199
|
+
soql = "SELECT id FROM#{selectCountMatch.post_match}"
|
200
|
+
end
|
223
201
|
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
# Update table name references
|
229
|
-
soql.sub!(/#{raw_table_name}\./i, "#{entity_def.api_name}.")
|
230
|
-
|
231
|
-
log(soql, name)
|
232
|
-
|
233
|
-
@connection.batch_size = @batch_size if @batch_size
|
234
|
-
@batch_size = nil
|
235
|
-
|
236
|
-
queryResult = get_result(@connection.query(:queryString => soql), :query)
|
237
|
-
records = queryResult[:records]
|
238
|
-
|
239
|
-
result = ResultArray.new(queryResult[:size].to_i)
|
240
|
-
return result unless records
|
241
|
-
|
242
|
-
records = [ records ] unless records.is_a?(Array)
|
243
|
-
|
244
|
-
records.each do |record|
|
245
|
-
row = {}
|
202
|
+
raw_table_name = sql.match(/FROM (\w+)/i)[1]
|
203
|
+
table_name = raw_table_name.singularize
|
204
|
+
entity_name = entity_name_from_table(table_name)
|
205
|
+
entity_def = get_entity_def(entity_name)
|
246
206
|
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
207
|
+
column_names = api_column_names(table_name)
|
208
|
+
|
209
|
+
# Always (unless COUNT*)'ing) select all columns (required for the AR attributes mechanism to work correctly
|
210
|
+
soql = sql.sub(/SELECT .+ FROM/i, "SELECT #{column_names.join(', ')} FROM") unless selectCountMatch
|
211
|
+
|
212
|
+
soql.sub!(/ FROM \w+/i, " FROM #{entity_def.api_name}")
|
213
|
+
|
214
|
+
# Look for a LIMIT clause
|
215
|
+
soql.sub!(/LIMIT 1/i, "")
|
216
|
+
|
217
|
+
# Look for an OFFSET clause
|
218
|
+
soql.sub!(/\d+ OFFSET \d+/i, "")
|
219
|
+
|
220
|
+
# Fixup column references to use api names
|
221
|
+
columns = columns_map(table_name)
|
222
|
+
while soql =~ /\w+\.(\w+)/i
|
223
|
+
column_name = $~[1]
|
224
|
+
|
225
|
+
column = columns[column_name]
|
226
|
+
soql = $~.pre_match + column.api_name + $~.post_match
|
227
|
+
end
|
228
|
+
|
229
|
+
# Update table name references
|
230
|
+
soql.sub!(/#{raw_table_name}\./i, "#{entity_def.api_name}.")
|
231
|
+
|
232
|
+
@connection.batch_size = @batch_size if @batch_size
|
233
|
+
@batch_size = nil
|
234
|
+
|
235
|
+
queryResult = get_result(@connection.query(:queryString => soql), :query)
|
236
|
+
records = queryResult[:records]
|
237
|
+
|
238
|
+
result = ResultArray.new(queryResult[:size].to_i)
|
239
|
+
return result unless records
|
240
|
+
|
241
|
+
records = [ records ] unless records.is_a?(Array)
|
242
|
+
|
243
|
+
records.each do |record|
|
244
|
+
row = {}
|
245
|
+
|
246
|
+
record.each do |name, value|
|
247
|
+
if name != :type
|
248
|
+
# Ids may be returned in an array with 2 duplicate entries...
|
249
|
+
value = value[0] if name == :Id && value.is_a?(Array)
|
250
|
+
|
251
|
+
column = entity_def.api_name_to_column[name.to_s]
|
252
|
+
attribute_name = column.name
|
253
|
+
|
254
|
+
if column.type == :boolean
|
255
|
+
row[attribute_name] = (value.casecmp("true") == 0)
|
256
|
+
else
|
257
|
+
row[attribute_name] = value
|
258
|
+
end
|
259
259
|
end
|
260
|
-
end
|
261
|
-
|
260
|
+
end
|
261
|
+
|
262
|
+
result << row
|
263
|
+
end
|
262
264
|
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
result
|
270
|
-
end
|
265
|
+
if selectCountMatch
|
266
|
+
[{ :count => result.actual_size }]
|
267
|
+
else
|
268
|
+
result
|
269
|
+
end
|
270
|
+
}
|
271
271
|
end
|
272
272
|
|
273
273
|
|
274
274
|
def select_one(sql, name = nil) #:nodoc:
|
275
275
|
self.batch_size = 1
|
276
|
-
|
277
|
-
log(sql, name)
|
278
|
-
|
276
|
+
|
279
277
|
result = select_all(sql, name)
|
280
|
-
|
278
|
+
|
281
279
|
result.nil? ? nil : result.first
|
282
280
|
end
|
283
281
|
|
284
282
|
|
285
283
|
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
|
286
|
-
log(sql, name)
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
values.map! { |v| v[7] }
|
301
|
-
|
302
|
-
fields = get_fields(columns, names, values, :createable)
|
303
|
-
|
304
|
-
sobject = create_sobject(entity_name, nil, fields)
|
284
|
+
log(sql, name) {
|
285
|
+
# Convert sql to sobject
|
286
|
+
table_name = sql.match(/INSERT INTO (\w+) /i)[1].singularize
|
287
|
+
entity_name = entity_name_from_table(table_name)
|
288
|
+
columns = columns_map(table_name)
|
289
|
+
|
290
|
+
# Extract array of column names
|
291
|
+
names = sql.match(/\((.+)\) VALUES/i)[1].scan(/\w+/i)
|
292
|
+
|
293
|
+
# Extract arrays of values
|
294
|
+
values = sql.match(/VALUES\s*\((.+)\)/i)[1]
|
295
|
+
values = values.scan(/(((NULL))|((TRUE))|((FALSE))|'(([^']|'')*)'),*/mi)
|
296
|
+
|
297
|
+
values.map! { |v| v[7] }
|
305
298
|
|
306
|
-
|
299
|
+
fields = get_fields(columns, names, values, :createable)
|
300
|
+
|
301
|
+
sobject = create_sobject(entity_name, nil, fields)
|
302
|
+
|
303
|
+
check_result(get_result(@connection.create(:sObjects => sobject), :create))[0][:id]
|
304
|
+
}
|
307
305
|
end
|
308
306
|
|
309
307
|
|
310
308
|
def update(sql, name = nil) #:nodoc:
|
311
|
-
log(sql, name)
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
309
|
+
log(sql, name) {
|
310
|
+
# Convert sql to sobject
|
311
|
+
table_name = sql.match(/UPDATE (\w+) /i)[1].singularize
|
312
|
+
entity_name = entity_name_from_table(table_name)
|
313
|
+
columns = columns_map(table_name)
|
314
|
+
|
315
|
+
match = sql.match(/SET\s+(.+)\s+WHERE/mi)[1]
|
316
|
+
names = match.scan(/(\w+)\s*=\s*('|NULL|TRUE|FALSE)/i)
|
317
|
+
names.map! { |v| v[0] }
|
318
|
+
|
319
|
+
values = match.scan(/=\s*(((NULL))|((TRUE))|((FALSE))|'(([^']|'')*)'),*/mi)
|
320
|
+
values.map! { |v| v[7] }
|
321
|
+
|
322
|
+
fields = get_fields(columns, names, values, :updateable)
|
323
|
+
|
324
|
+
id = sql.match(/WHERE\s+id\s*=\s*'(\w+)'/i)[1]
|
325
|
+
|
326
|
+
sobject = create_sobject(entity_name, id, fields)
|
327
|
+
|
328
|
+
check_result(get_result(@connection.update(:sObjects => sobject), :update))
|
329
|
+
}
|
332
330
|
end
|
333
331
|
|
334
332
|
|
335
333
|
def delete(sql, name = nil)
|
336
|
-
log(sql, name)
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
334
|
+
log(sql, name) {
|
335
|
+
# Extract the id
|
336
|
+
match = sql.match(/WHERE\s+id\s*=\s*'(\w+)'/mi)
|
337
|
+
|
338
|
+
if match
|
339
|
+
ids = [ match[1] ]
|
340
|
+
else
|
341
|
+
# Check for the form id IN ('x', 'y')
|
342
|
+
match = sql.match(/WHERE\s+id\s+IN\s*\((.+)\)/mi)[1]
|
343
|
+
ids = match.scan(/\w+/)
|
344
|
+
end
|
345
|
+
|
346
|
+
ids_element = []
|
347
|
+
ids.each { |id| ids_element << :ids << id }
|
348
|
+
|
349
|
+
check_result(get_result(@connection.delete(ids_element), :delete))
|
350
|
+
}
|
353
351
|
end
|
354
352
|
|
355
353
|
|
@@ -398,44 +396,48 @@ module ActiveRecord
|
|
398
396
|
def get_entity_def(entity_name)
|
399
397
|
cached_entity_def = @entity_def_map[entity_name]
|
400
398
|
return cached_entity_def if cached_entity_def
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
begin
|
406
|
-
metadata = get_result(@connection.describeSObject(:sObjectType => entity_name), :describeSObject)
|
407
|
-
custom = false
|
408
|
-
rescue SalesforceError => e
|
409
|
-
# Fallback and see if we can find a custom object with this name
|
410
|
-
metadata = get_result(@connection.describeSObject(:sObjectType => entity_name + "__c"), :describeSObject)
|
411
|
-
custom = true
|
412
|
-
end
|
413
|
-
|
414
|
-
metadata[:fields].each do |field|
|
415
|
-
column = SalesforceColumn.new(field)
|
416
|
-
cached_columns << column
|
399
|
+
|
400
|
+
log("Retrieving metadata for '#{entity_name}'", "get_entity_def()") {
|
401
|
+
cached_columns = []
|
402
|
+
cached_relationships = []
|
417
403
|
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
404
|
+
begin
|
405
|
+
metadata = get_result(@connection.describeSObject(:sObjectType => entity_name), :describeSObject)
|
406
|
+
custom = false
|
407
|
+
rescue SalesforceError => e
|
408
|
+
# Fallback and see if we can find a custom object with this name
|
409
|
+
@logger.info(" Unable to find medata for '#{entity_name}', falling back to custom object name #{entity_name + "__c"}")
|
410
|
+
|
411
|
+
metadata = get_result(@connection.describeSObject(:sObjectType => entity_name + "__c"), :describeSObject)
|
412
|
+
custom = true
|
413
|
+
end
|
414
|
+
|
415
|
+
metadata[:fields].each do |field|
|
416
|
+
column = SalesforceColumn.new(field)
|
417
|
+
cached_columns << column
|
418
|
+
|
419
|
+
cached_relationships << SalesforceRelationship.new(field, column) if field[:type] =~ /reference/i
|
420
|
+
end
|
421
|
+
|
422
|
+
relationships = metadata[:childRelationships]
|
423
|
+
if relationships
|
424
|
+
relationships = [ relationships ] unless relationships.is_a? Array
|
425
|
+
|
426
|
+
relationships.each do |relationship|
|
427
|
+
if relationship[:cascadeDelete] == "true"
|
428
|
+
r = SalesforceRelationship.new(relationship)
|
429
|
+
cached_relationships << r
|
430
|
+
end
|
429
431
|
end
|
430
432
|
end
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
433
|
+
|
434
|
+
entity_def = EntityDefinition.new(entity_name, cached_columns, cached_relationships, custom)
|
435
|
+
@entity_def_map[entity_name] = entity_def
|
436
|
+
|
437
|
+
configure_active_record entity_def
|
438
|
+
|
439
|
+
entity_def
|
440
|
+
}
|
439
441
|
end
|
440
442
|
|
441
443
|
|
@@ -459,7 +461,7 @@ module ActiveRecord
|
|
459
461
|
# DCHASMAN TODO Figure out how to handle polymorphic refs (e.g. Note.parent can refer to
|
460
462
|
# Account, Contact, Opportunity, Contract, Asset, Product2, <CustomObject1> ... <CustomObject(n)>
|
461
463
|
if reference_to.is_a? Array
|
462
|
-
|
464
|
+
@logger.info(" Skipping unsupported polymophic one-to-#{one_to_many ? 'many' : 'one' } relationship '#{referenceName}' from #{entity_name} to [#{relationship.reference_to.join(', ')}] using #{foreign_key}")
|
463
465
|
next
|
464
466
|
end
|
465
467
|
|
@@ -470,7 +472,7 @@ module ActiveRecord
|
|
470
472
|
referenced_klass = reference_to.constantize
|
471
473
|
rescue NameError => e
|
472
474
|
# Automatically create a least a stub for the referenced entity
|
473
|
-
|
475
|
+
@logger.info(" Creating ActiveRecord stub for the referenced entity '#{reference_to}'")
|
474
476
|
|
475
477
|
referenced_klass = klass.class_eval("::#{reference_to} = Class.new(ActiveRecord::Base)")
|
476
478
|
|
@@ -483,7 +485,7 @@ module ActiveRecord
|
|
483
485
|
klass.belongs_to referenceName.to_sym, :class_name => reference_to, :foreign_key => foreign_key, :dependent => false
|
484
486
|
end
|
485
487
|
|
486
|
-
|
488
|
+
@logger.info(" Created one-to-#{one_to_many ? 'many' : 'one' } relationship '#{referenceName}' from #{entity_name} to #{relationship.reference_to} using #{foreign_key}")
|
487
489
|
|
488
490
|
end
|
489
491
|
end
|