activerecord-activesalesforce-adapter 2.0.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 +777 -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 +105 -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 'asf_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,777 @@
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
+ id = sql.match(/WHERE\s+id\s*=\s*'(\w+)'/mi)[1]
421
+
422
+ sobject = create_sobject(entity_def.api_name, id, fields, null_fields)
423
+
424
+ @command_boxcar << ActiveSalesforce::BoxcarCommand::Update.new(self, sobject)
425
+ #}
426
+ end
427
+
428
+
429
+ def delete(sql, name = nil)
430
+ log(sql, name) {
431
+ # Extract the id
432
+ match = sql.match(/WHERE\s+id\s*=\s*'(\w+)'/mi)
433
+
434
+ if match
435
+ ids = [ match[1] ]
436
+ else
437
+ # Check for the form (id IN ('x', 'y'))
438
+ match = sql.match(/WHERE\s+\(\s*id\s+IN\s*\((.+)\)\)/mi)[1]
439
+ ids = match.scan(/\w+/)
440
+ end
441
+
442
+ ids_element = []
443
+ ids.each { |id| ids_element << :ids << id }
444
+
445
+ @command_boxcar << ActiveSalesforce::BoxcarCommand::Delete.new(self, ids_element)
446
+ }
447
+ end
448
+
449
+
450
+ def get_updated(object_type, start_date, end_date, name = nil)
451
+ msg = "get_updated(#{object_type}, #{start_date}, #{end_date})"
452
+ log(msg, name) {
453
+ get_updated_element = []
454
+ get_updated_element << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << object_type
455
+ get_updated_element << :startDate << start_date
456
+ get_updated_element << :endDate << end_date
457
+
458
+ result = get_result(@connection.getUpdated(get_updated_element), :getUpdated)
459
+
460
+ result[:ids]
461
+ }
462
+ end
463
+
464
+
465
+ def get_deleted(object_type, start_date, end_date, name = nil)
466
+ msg = "get_deleted(#{object_type}, #{start_date}, #{end_date})"
467
+ log(msg, name) {
468
+ get_deleted_element = []
469
+ get_deleted_element << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << object_type
470
+ get_deleted_element << :startDate << start_date
471
+ get_deleted_element << :endDate << end_date
472
+
473
+ result = get_result(@connection.getDeleted(get_deleted_element), :getDeleted)
474
+
475
+ ids = []
476
+ result[:deletedRecords].each do |v|
477
+ ids << v[:id]
478
+ end
479
+
480
+ ids
481
+ }
482
+ end
483
+
484
+
485
+ def get_user_info(name = nil)
486
+ msg = "get_user_info()"
487
+ log(msg, name) {
488
+ get_result(@connection.getUserInfo([]), :getUserInfo)
489
+ }
490
+ end
491
+
492
+
493
+ def retrieve_field_values(object_type, fields, ids, name = nil)
494
+ msg = "retrieve(#{object_type}, [#{ids.to_a.join(', ')}])"
495
+ log(msg, name) {
496
+ retrieve_element = []
497
+ retrieve_element << :fieldList << fields.to_a.join(", ")
498
+ retrieve_element << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << object_type
499
+ ids.to_a.each { |id| retrieve_element << :ids << id }
500
+
501
+ result = get_result(@connection.retrieve(retrieve_element), :retrieve)
502
+
503
+ result = [ result ] unless result.is_a?(Array)
504
+
505
+ # Remove unwanted :type and normalize :Id if required
506
+ field_values = []
507
+ result.each do |v|
508
+ v = v.dup
509
+ v.delete(:type)
510
+ v[:Id] = v[:Id][0] if v[:Id].is_a? Array
511
+
512
+ field_values << v
513
+ end
514
+
515
+ field_values
516
+ }
517
+ end
518
+
519
+
520
+ def get_fields(columns, names, values, access_check)
521
+ fields = {}
522
+ names.each_with_index do | name, n |
523
+ value = values[n]
524
+
525
+ if value
526
+ column = columns[name]
527
+
528
+ raise ActiveSalesforce::ASFError.new(@logger, "Column not found for #{name}!") unless column
529
+
530
+ value.gsub!(/''/, "'") if value.is_a? String
531
+
532
+ include_field = ((not value.empty?) and column.send(access_check))
533
+
534
+ if (include_field)
535
+ case column.type
536
+ when :date
537
+ value = Time.parse(value + "Z").utc.strftime("%Y-%m-%d")
538
+ when :datetime
539
+ value = Time.parse(value + "Z").utc.strftime("%Y-%m-%dT%H:%M:%SZ")
540
+ end
541
+
542
+ fields[column.api_name] = value
543
+ end
544
+ end
545
+ end
546
+
547
+ fields
548
+ end
549
+
550
+ def get_null_fields(columns, names, values, access_check)
551
+ fields = {}
552
+ names.each_with_index do | name, n |
553
+ value = values[n]
554
+
555
+ if !value
556
+ column = columns[name]
557
+ fields[column.api_name] = nil if column.send(access_check) && column.api_name.casecmp("ownerid") != 0
558
+ end
559
+ end
560
+
561
+ fields
562
+ end
563
+
564
+ def extract_sql_modifier(soql, modifier)
565
+ value = soql.match(/\s+#{modifier}\s+(\d+)/mi)
566
+ if value
567
+ value = value[1].to_i
568
+ soql.sub!(/\s+#{modifier}\s+\d+/mi, "")
569
+ end
570
+
571
+ value
572
+ end
573
+
574
+
575
+ def get_result(response, method)
576
+ responseName = (method.to_s + "Response").to_sym
577
+ finalResponse = response[responseName]
578
+
579
+ raise ActiveSalesforce::ASFError.new(@logger, response[:Fault][:faultstring], response.fault) unless finalResponse
580
+
581
+ result = finalResponse[:result]
582
+ end
583
+
584
+
585
+ def check_result(result)
586
+ result = [ result ] unless result.is_a?(Array)
587
+
588
+ result.each do |r|
589
+ raise ActiveSalesforce::ASFError.new(@logger, r[:errors], r[:errors][:message]) unless r[:success] == "true"
590
+ end
591
+
592
+ result
593
+ end
594
+
595
+
596
+ def get_entity_def(entity_name)
597
+ cached_entity_def = @entity_def_map[entity_name]
598
+
599
+ if cached_entity_def
600
+ # Check for the loss of asf AR setup
601
+ entity_klass = class_from_entity_name(entity_name)
602
+
603
+ configure_active_record(cached_entity_def) unless entity_klass.respond_to?(:asf_augmented?)
604
+
605
+ return cached_entity_def
606
+ end
607
+
608
+ cached_columns = []
609
+ cached_relationships = []
610
+
611
+ begin
612
+ metadata = get_result(@connection.describeSObject(:sObjectType => entity_name), :describeSObject)
613
+ custom = false
614
+ rescue ActiveSalesforce::ASFError
615
+ # Fallback and see if we can find a custom object with this name
616
+ debug(" Unable to find medata for '#{entity_name}', falling back to custom object name #{entity_name + "__c"}")
617
+
618
+ metadata = get_result(@connection.describeSObject(:sObjectType => entity_name + "__c"), :describeSObject)
619
+ custom = true
620
+ end
621
+
622
+ metadata[:fields].each do |field|
623
+ column = SalesforceColumn.new(field)
624
+ cached_columns << column
625
+
626
+ cached_relationships << SalesforceRelationship.new(field, column) if field[:type] =~ /reference/mi
627
+ end
628
+
629
+ relationships = metadata[:childRelationships]
630
+ if relationships
631
+ relationships = [ relationships ] unless relationships.is_a? Array
632
+
633
+ relationships.each do |relationship|
634
+ if relationship[:cascadeDelete] == "true"
635
+ r = SalesforceRelationship.new(relationship)
636
+ cached_relationships << r
637
+ end
638
+ end
639
+ end
640
+
641
+ key_prefix = metadata[:keyPrefix]
642
+
643
+ entity_def = ActiveSalesforce::EntityDefinition.new(self, entity_name, entity_klass,
644
+ cached_columns, cached_relationships, custom, key_prefix)
645
+
646
+ @entity_def_map[entity_name] = entity_def
647
+ @keyprefix_to_entity_def_map[key_prefix] = entity_def
648
+
649
+ configure_active_record(entity_def)
650
+
651
+ entity_def
652
+ end
653
+
654
+
655
+ def configure_active_record(entity_def)
656
+ entity_name = entity_def.name
657
+ klass = class_from_entity_name(entity_name)
658
+
659
+ class << klass
660
+ def asf_augmented?
661
+ true
662
+ end
663
+ end
664
+
665
+ # Add support for SID-based authentication
666
+ ActiveSalesforce::SessionIDAuthenticationFilter.register(klass)
667
+
668
+ klass.set_inheritance_column nil unless entity_def.custom?
669
+ klass.set_primary_key "id"
670
+
671
+ # Create relationships for any reference field
672
+ entity_def.relationships.each do |relationship|
673
+ referenceName = relationship.name
674
+ unless self.respond_to? referenceName.to_sym or relationship.reference_to == "Profile"
675
+ reference_to = relationship.reference_to
676
+ one_to_many = relationship.one_to_many
677
+ foreign_key = relationship.foreign_key
678
+
679
+ # DCHASMAN TODO Figure out how to handle polymorphic refs (e.g. Note.parent can refer to
680
+ # Account, Contact, Opportunity, Contract, Asset, Product2, <CustomObject1> ... <CustomObject(n)>
681
+ if reference_to.is_a? Array
682
+ debug(" Skipping unsupported polymophic one-to-#{one_to_many ? 'many' : 'one' } relationship '#{referenceName}' from #{klass} to [#{relationship.reference_to.join(', ')}] using #{foreign_key}")
683
+ next
684
+ end
685
+
686
+ # Handle references to custom objects
687
+ reference_to = reference_to.chomp("__c").capitalize if reference_to.match(/__c$/)
688
+
689
+ begin
690
+ referenced_klass = class_from_entity_name(reference_to)
691
+ rescue NameError => e
692
+ # Automatically create a least a stub for the referenced entity
693
+ debug(" Creating ActiveRecord stub for the referenced entity '#{reference_to}'")
694
+
695
+ referenced_klass = klass.class_eval("::#{reference_to} = Class.new(ActiveRecord::Base)")
696
+
697
+ # Automatically inherit the connection from the referencee
698
+ referenced_klass.connection = klass.connection
699
+ end
700
+
701
+ if referenced_klass
702
+ if one_to_many
703
+ klass.has_many referenceName.to_sym, :class_name => referenced_klass.name, :foreign_key => foreign_key, :dependent => :nullify
704
+ else
705
+ klass.belongs_to referenceName.to_sym, :class_name => referenced_klass.name, :foreign_key => foreign_key, :dependent => :nullify
706
+ end
707
+
708
+ debug(" Created one-to-#{one_to_many ? 'many' : 'one' } relationship '#{referenceName}' from #{klass} to #{referenced_klass} using #{foreign_key}")
709
+ end
710
+ end
711
+ end
712
+
713
+ end
714
+
715
+
716
+ def columns(table_name, name = nil)
717
+ table_name, columns, entity_def = lookup(table_name)
718
+ entity_def.columns
719
+ end
720
+
721
+
722
+ def class_from_entity_name(entity_name)
723
+ entity_klass = @class_to_entity_map[entity_name.upcase]
724
+ debug("Found matching class '#{entity_klass}' for entity '#{entity_name}'") if entity_klass
725
+
726
+ entity_klass = entity_name.constantize unless entity_klass
727
+
728
+ entity_klass
729
+ end
730
+
731
+
732
+ def create_sobject(entity_name, id, fields, null_fields = [])
733
+ sobj = []
734
+
735
+ sobj << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << entity_name
736
+ sobj << 'Id { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << id if id
737
+
738
+ # add any changed fields
739
+ fields.each do | name, value |
740
+ sobj << name.to_sym << value if value
741
+ end
742
+
743
+ # add null fields
744
+ null_fields.each do | name, value |
745
+ sobj << 'fieldsToNull { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << name
746
+ end
747
+
748
+ [ :sObjects, sobj ]
749
+ end
750
+
751
+
752
+ def column_names(table_name)
753
+ columns(table_name).map { |column| column.name }
754
+ end
755
+
756
+
757
+ def lookup(raw_table_name)
758
+ table_name = raw_table_name.singularize
759
+
760
+ # See if a table name to AR class mapping was registered
761
+ klass = @class_to_entity_map[table_name.upcase]
762
+
763
+ entity_name = klass ? raw_table_name : table_name.camelize
764
+ entity_def = get_entity_def(entity_name)
765
+
766
+ [table_name, entity_def.columns, entity_def]
767
+ end
768
+
769
+
770
+ def debug(msg)
771
+ @logger.debug(msg) if @logger
772
+ end
773
+
774
+ end
775
+
776
+ end
777
+ end