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.
Files changed (33) hide show
  1. data/README +51 -0
  2. data/lib/active_record/connection_adapters/activesalesforce.rb +36 -0
  3. data/lib/active_record/connection_adapters/activesalesforce_adapter.rb +784 -0
  4. data/lib/active_record/connection_adapters/asf_active_record.rb +40 -0
  5. data/lib/active_record/connection_adapters/boxcar_command.rb +66 -0
  6. data/lib/active_record/connection_adapters/column_definition.rb +95 -0
  7. data/lib/active_record/connection_adapters/entity_definition.rb +59 -0
  8. data/lib/active_record/connection_adapters/id_resolver.rb +84 -0
  9. data/lib/active_record/connection_adapters/recording_binding.rb +90 -0
  10. data/lib/active_record/connection_adapters/relationship_definition.rb +81 -0
  11. data/lib/active_record/connection_adapters/result_array.rb +31 -0
  12. data/lib/active_record/connection_adapters/rforce.rb +361 -0
  13. data/lib/active_record/connection_adapters/sid_authentication_filter.rb +57 -0
  14. data/test/unit/basic_test.rb +203 -0
  15. data/test/unit/config.yml +5 -0
  16. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_add_notes_to_contact.recording +1966 -0
  17. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_assignment_rule_id.recording +1621 -0
  18. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_batch_insert.recording +1611 -0
  19. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_client_id.recording +1618 -0
  20. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_count_contacts.recording +1620 -0
  21. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_create_a_contact.recording +1611 -0
  22. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_a_contact.recording +1611 -0
  23. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_a_contact_by_first_name.recording +3468 -0
  24. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_a_contact_by_id.recording +1664 -0
  25. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_addresses.recording +1635 -0
  26. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_get_created_by_from_contact.recording +4307 -0
  27. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_master_detail.recording +1951 -0
  28. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_read_all_content_columns.recording +1611 -0
  29. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_save_a_contact.recording +1611 -0
  30. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_use_default_rule.recording +1618 -0
  31. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_use_update_mru.recording +1618 -0
  32. data/test/unit/recorded_test_case.rb +83 -0
  33. 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