obtiva-activerecord-activesalesforce-adapter 2.1.0

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