obtiva-activerecord-activesalesforce-adapter 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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