silent-e-activerecord-activesalesforce-adapter 2.3.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/.gitignore +2 -0
  2. data/.project +17 -0
  3. data/README +80 -0
  4. data/Rakefile +21 -0
  5. data/VERSION +1 -0
  6. data/lib/active_record/connection_adapters/activesalesforce.rb +36 -0
  7. data/lib/active_record/connection_adapters/activesalesforce_adapter.rb +863 -0
  8. data/lib/active_record/connection_adapters/asf_active_record.rb +40 -0
  9. data/lib/active_record/connection_adapters/boxcar_command.rb +66 -0
  10. data/lib/active_record/connection_adapters/column_definition.rb +95 -0
  11. data/lib/active_record/connection_adapters/entity_definition.rb +59 -0
  12. data/lib/active_record/connection_adapters/id_resolver.rb +84 -0
  13. data/lib/active_record/connection_adapters/recording_binding.rb +90 -0
  14. data/lib/active_record/connection_adapters/relationship_definition.rb +81 -0
  15. data/lib/active_record/connection_adapters/result_array.rb +31 -0
  16. data/lib/active_record/connection_adapters/sid_authentication_filter.rb +57 -0
  17. data/lib/activerecord-activesalesforce-adapter.rb +1 -0
  18. data/lib/arel/engines/sql/compilers/activesalesforce_compiler.rb +6 -0
  19. data/lib/rforce.rb +87 -0
  20. data/lib/rforce/binding.rb +212 -0
  21. data/lib/rforce/method_keys.rb +14 -0
  22. data/lib/rforce/soap_pullable.rb +93 -0
  23. data/lib/rforce/soap_response.rb +9 -0
  24. data/lib/rforce/soap_response_expat.rb +36 -0
  25. data/lib/rforce/soap_response_hpricot.rb +75 -0
  26. data/lib/rforce/soap_response_rexml.rb +35 -0
  27. data/lib/rforce/version.rb +3 -0
  28. data/silent-e-activerecord-activesalesforce-adapter.gemspec +115 -0
  29. data/test/unit/basic_test.rb +204 -0
  30. data/test/unit/config.yml +5 -0
  31. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_add_notes_to_contact.recording +1966 -0
  32. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_assignment_rule_id.recording +1621 -0
  33. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_batch_insert.recording +1611 -0
  34. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_client_id.recording +1618 -0
  35. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_count_contacts.recording +1620 -0
  36. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_create_a_contact.recording +1611 -0
  37. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_a_contact.recording +1611 -0
  38. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_a_contact_by_first_name.recording +3468 -0
  39. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_a_contact_by_id.recording +1664 -0
  40. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_addresses.recording +1635 -0
  41. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_get_created_by_from_contact.recording +4307 -0
  42. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_master_detail.recording +1951 -0
  43. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_read_all_content_columns.recording +1611 -0
  44. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_save_a_contact.recording +1611 -0
  45. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_use_default_rule.recording +1618 -0
  46. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_use_update_mru.recording +1618 -0
  47. data/test/unit/recorded_test_case.rb +83 -0
  48. metadata +186 -0
@@ -0,0 +1,2 @@
1
+ pkg/
2
+ *.gem
@@ -0,0 +1,17 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <projectDescription>
3
+ <name>althor880-activerecord-activesalesforce-adapter</name>
4
+ <comment></comment>
5
+ <projects>
6
+ </projects>
7
+ <buildSpec>
8
+ <buildCommand>
9
+ <name>org.rubypeople.rdt.core.rubybuilder</name>
10
+ <arguments>
11
+ </arguments>
12
+ </buildCommand>
13
+ </buildSpec>
14
+ <natures>
15
+ <nature>org.rubypeople.rdt.core.rubynature</nature>
16
+ </natures>
17
+ </projectDescription>
data/README ADDED
@@ -0,0 +1,80 @@
1
+ == Welcome to Active Salesforce
2
+
3
+ ActiveSalesforce is an extension to the Rails Framework that allows for the dynamic creation and management of ActiveRecord objects through the use of Salesforce meta-data and uses a Salesforce.com organization as the backing store.
4
+
5
+ See notes below about my fork.
6
+
7
+ == Installation
8
+
9
+ I have a compiled build in my Downloads area or clone my repo, build the gem, and install.
10
+
11
+ == Getting started
12
+
13
+ 1. If you have not already done so generate your initial rails app:
14
+
15
+ rails myappname
16
+
17
+ 2. Edit config/environment.rb and add a config.gem requirement:
18
+
19
+ Rails::Initializer.run do |config|
20
+ ...
21
+ config.gem "silent-e-activerecord-activesalesforce-adapter", :lib => 'activerecord-activesalesforce-adapter'
22
+ ...
23
+ end
24
+
25
+ 3. Edit database.yml
26
+
27
+ development:
28
+ adapter: activesalesforce
29
+ username: <salesforce user name goes here>
30
+ password: <salesforce password goes here>
31
+
32
+ NOTE: "url" is an optional parameter. If you want to access your Salesforce Sandbox account add the following line.
33
+
34
+ url: https://test.salesforce.com
35
+
36
+ 4. Create your salesforce models using a Salesforce::<ModelName> namespace.
37
+
38
+ script/generate model Salesforce::Contact
39
+
40
+ 5. Run a quick test to make sure things are working
41
+
42
+ > script/console
43
+ Loading development environment (Rails 2.3.9)
44
+
45
+ >> Salesforce::Contact.first
46
+ => <Salesforce::Contact id: "003T000000GqvJsIAJ", ... >
47
+
48
+ 6. Proceed using standard Rails development techniques!
49
+
50
+ == Advanced Features
51
+
52
+ 1. Session ID based Authentication: Add the following to /app/controllers/application.rb to enable SID auth for all controllers
53
+
54
+ class ApplicationController < ActionController::Base
55
+ before_filter ActiveSalesforce::SessionIDAuthenticationFilter
56
+ end
57
+
58
+ 2. Boxcar'ing of updates, inserts, and deletes. Use <YourModel>.transaction() to demark boxcar boundaries.
59
+
60
+ == Description of contents
61
+
62
+ lib
63
+ Application specific libraries. Basically, any kind of custom code that doesn't
64
+ belong under controllers, models, or helpers. This directory is in the load path.
65
+
66
+ script
67
+ Helper scripts for automation and generation.
68
+
69
+ test
70
+ Unit and functional tests along with fixtures.
71
+
72
+ == Note about my (silent-e) fork
73
+
74
+ I created this fork to fix a problem I was having with SELECT statements. Salesforce enforces single-quote escaping in strings with a backspace in WHERE condition expressions. However, in INSERT and UPDATE statements the VALUES are escaped with two consecutive single-quotes. See the table below for a visual explanation of my clumsy text one. :)
75
+
76
+ This fork specifically tweaks SELECT WHERE conditions and is based on blaines work on bringing ASF up to speed with Rails 3. I will attempt to keep it in step with blaines work until we can get a consensus and possibly get a official update to the canonical ASF gem.
77
+
78
+ | Query Type | Escape sequence required |
79
+ | WHERE (conditionExpression) | ... WHERE (Name = 'Let\'s do the Time Warp again!') |
80
+ | INSERT/UPDATE | ... VALUES ('Let''s do the Time Warp again!') |
@@ -0,0 +1,21 @@
1
+ begin
2
+ require 'jeweler'
3
+ Jeweler::Tasks.new do |gemspec|
4
+ gemspec.name = "silent-e-activerecord-activesalesforce-adapter"
5
+ gemspec.summary = "ActiveSalesforce (ASF) is a Rails connection adapter that provides direct access to Salesforce.com hosted data and metadata via the ActiveRecord model layer. Objects, fields, and relationships are all auto surfaced as active record attributes and rels."
6
+ gemspec.email = "matte@silent-e.com"
7
+ gemspec.homepage = "http://github.com/silent-e/activerecord-activesalesforce-adapter"
8
+ gemspec.authors = ["Doug Chasman","Luigi Montanez","Senthil Nayagam","Justin Ball","Jesse Hallett", "Andrew Freeberg", "Blaine Schanfeldt", "Matte Edens"]
9
+ gemspec.description = ''
10
+
11
+ gemspec.test_files = 'test/**/*'
12
+
13
+ gemspec.add_dependency('rails', '>= 2.3.3')
14
+ gemspec.add_dependency('builder', '>= 1.2.4')
15
+ gemspec.add_dependency('hpricot', '>=0.8.2')
16
+
17
+
18
+ end
19
+ rescue LoadError
20
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
21
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 2.3.8.1
@@ -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,863 @@
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 = nil
135
+ @class_to_entity_map = {}
136
+ end
137
+
138
+
139
+ def set_class_for_entity(klass, entity_name)
140
+ debug("Setting @class_to_entity_map['#{entity_name.upcase}'] = #{klass} for connection #{self}")
141
+ @class_to_entity_map[entity_name.upcase] = klass
142
+ end
143
+
144
+
145
+ def binding
146
+ @connection
147
+ end
148
+
149
+
150
+ def adapter_name #:nodoc:
151
+ 'ActiveSalesforce'
152
+ end
153
+
154
+
155
+ def supports_migrations? #:nodoc:
156
+ false
157
+ end
158
+
159
+ def tables(name = nil) #:nodoc:
160
+ @connection.describeGlobal({}).describeGlobalResponse.result.types
161
+ end
162
+
163
+ def table_exists?(table_name)
164
+ true
165
+ end
166
+
167
+ # QUOTING ==================================================
168
+
169
+ def quote(value, column = nil)
170
+ case value
171
+ when NilClass then quoted_value = "NULL"
172
+ when TrueClass then quoted_value = "TRUE"
173
+ when FalseClass then quoted_value = "FALSE"
174
+ when Float, Fixnum, Bignum then quoted_value = "'#{value.to_s}'"
175
+ else quoted_value = super(value, column)
176
+ end
177
+
178
+ quoted_value
179
+ end
180
+
181
+ # CONNECTION MANAGEMENT ====================================
182
+
183
+ def active?
184
+ true
185
+ end
186
+
187
+
188
+ def reconnect!
189
+ connect
190
+ end
191
+
192
+
193
+ # TRANSACTIOn SUPPORT (Boxcarring really because the salesforce.com api does not support transactions)
194
+
195
+ # Override AbstractAdapter's transaction method to implement
196
+ # per-connection support for nested transactions that do not commit until
197
+ # the outermost transaction is finished. ActiveRecord provides support
198
+ # for this, but does not distinguish between database connections, which
199
+ # prevents opening transactions to two different databases at the same
200
+ # time.
201
+ def transaction_with_nesting_support(*args, &block)
202
+ open = Thread.current["open_transactions_for_#{self.class.name.underscore}"] ||= 0
203
+ Thread.current["open_transactions_for_#{self.class.name.underscore}"] = open + 1
204
+
205
+ begin
206
+ transaction_without_nesting_support(&block)
207
+ ensure
208
+ Thread.current["open_transactions_for_#{self.class.name.underscore}"] -= 1
209
+ end
210
+ end
211
+ alias_method_chain :transaction, :nesting_support
212
+
213
+ # Begins the transaction (and turns off auto-committing).
214
+ def begin_db_transaction
215
+ # log('Opening boxcar', 'begin_db_transaction()')
216
+ @command_boxcar = []
217
+ end
218
+
219
+
220
+ def send_commands(commands)
221
+ # Send the boxcar'ed command set
222
+ verb = commands[0].verb
223
+
224
+ args = []
225
+ commands.each do |command|
226
+ command.args.each { |arg| args << arg }
227
+ end
228
+
229
+ response = @connection.send(verb, args)
230
+
231
+ result = get_result(response, verb)
232
+
233
+ result = [ result ] unless result.is_a?(Array)
234
+
235
+ errors = []
236
+ result.each_with_index do |r, n|
237
+ success = r[:success] == "true"
238
+
239
+ # Give each command a chance to process its own result
240
+ command = commands[n]
241
+ command.after_execute(r)
242
+
243
+ # Handle the set of failures
244
+ errors << r[:errors] unless r[:success] == "true"
245
+ end
246
+
247
+ unless errors.empty?
248
+ message = errors.join("\n")
249
+ fault = (errors.map { |error| error[:message] }).join("\n")
250
+ raise ActiveSalesforce::ASFError.new(@logger, message, fault)
251
+ end
252
+
253
+ result
254
+ end
255
+
256
+
257
+ # Commits the transaction (and turns on auto-committing).
258
+ def commit_db_transaction()
259
+ # log("Committing boxcar with #{@command_boxcar.length} commands", 'commit_db_transaction()')
260
+ # puts("Committing boxcar with #{@command_boxcar.length} commands", 'commit_db_transaction()')
261
+
262
+ previous_command = nil
263
+ commands = []
264
+
265
+ @command_boxcar.each do |command|
266
+ if commands.length >= MAX_BOXCAR_SIZE or (previous_command and (command.verb != previous_command.verb))
267
+ send_commands(commands)
268
+
269
+ commands = []
270
+ previous_command = nil
271
+ else
272
+ commands << command
273
+ previous_command = command
274
+ end
275
+ end
276
+
277
+ # Discard the command boxcar
278
+ @command_boxcar = nil
279
+
280
+ # Finish off the partial boxcar
281
+ send_commands(commands) unless commands.empty?
282
+
283
+ end
284
+
285
+ # Rolls back the transaction (and turns on auto-committing). Must be
286
+ # done if the transaction block raises an exception or returns false.
287
+ def rollback_db_transaction()
288
+ # log('Rolling back boxcar', 'rollback_db_transaction()')
289
+ @command_boxcar = nil
290
+ end
291
+
292
+
293
+ # DATABASE STATEMENTS ======================================
294
+
295
+ def select_all(sql, name = nil) #:nodoc:
296
+
297
+ # fix the single quote escape method in WHERE condition expression
298
+ sql = quote_where(sql)
299
+
300
+ # Arel adds the class to the selection - we do not want this i.e...
301
+ # SELECT contacts.* FROM => SELECT * FROM
302
+ sql = sql.gsub(/SELECT\s+[^\(][A-Z]+\./mi," ")
303
+ raw_table_name = sql.match(/FROM\s+(\w+)/mi)[1]
304
+
305
+ table_name, columns, entity_def = lookup(raw_table_name)
306
+
307
+ column_names = columns.map { |column| column.api_name }
308
+
309
+ # Check for SELECT COUNT(*) FROM query
310
+
311
+ # Rails 1.1
312
+ selectCountMatch = sql.match(/SELECT\s+COUNT\(\*\)\s+AS\s+count_all\s+FROM/mi)
313
+
314
+ # Rails 1.0
315
+ selectCountMatch = sql.match(/SELECT\s+COUNT\(\*\)\s+FROM/mi) unless selectCountMatch
316
+
317
+ if selectCountMatch
318
+ soql = "SELECT COUNT() FROM#{selectCountMatch.post_match}"
319
+ else
320
+ if sql.match(/SELECT\s+\*\s+FROM/mi)
321
+ # Always convert SELECT * to select all columns (required for the AR attributes mechanism to work correctly)
322
+ soql = sql.sub(/SELECT .+ FROM/mi, "SELECT #{column_names.join(', ')} FROM")
323
+ else
324
+ soql = sql
325
+ end
326
+ end
327
+
328
+ soql.sub!(/\s+FROM\s+\w+/mi, " FROM #{entity_def.api_name}")
329
+
330
+ if selectCountMatch
331
+ query_result = get_result(@connection.query(:queryString => soql), :query)
332
+ return [{ :count => query_result[:size] }]
333
+ end
334
+
335
+ # Look for a LIMIT clause
336
+ limit = extract_sql_modifier(soql, "LIMIT")
337
+ limit = MAX_BOXCAR_SIZE unless limit
338
+
339
+ # Look for an OFFSET clause
340
+ offset = extract_sql_modifier(soql, "OFFSET")
341
+
342
+ # Fixup column references to use api names
343
+ columns = entity_def.column_name_to_column
344
+ soql.gsub!(/((?:\w+\.)?\w+)(?=\s*(?:=|!=|<|>|<=|>=|like)\s*(?:'[^']*'|NULL|TRUE|FALSE))/mi) do |column_name|
345
+ # strip away any table alias
346
+ column_name.sub!(/\w+\./, '')
347
+
348
+ column = columns[column_name]
349
+ raise ActiveSalesforce::ASFError.new(@logger, "Column not found for #{column_name}!") unless column
350
+
351
+ column.api_name
352
+ end
353
+
354
+ # Update table name references
355
+ soql.sub!(/#{raw_table_name}\./mi, "#{entity_def.api_name}.")
356
+
357
+ @connection.batch_size = @batch_size if @batch_size
358
+ @batch_size = nil
359
+
360
+ query_result = get_result(@connection.query(:queryString => soql), :query)
361
+ result = ActiveSalesforce::ResultArray.new(query_result[:size].to_i)
362
+ return result unless query_result[:records]
363
+
364
+ add_rows(entity_def, query_result, result, limit)
365
+
366
+ while ((query_result[:done].casecmp("true") != 0) and (result.size < limit or limit == 0))
367
+ # Now queryMore
368
+ locator = query_result[:queryLocator];
369
+ query_result = get_result(@connection.queryMore(:queryLocator => locator), :queryMore)
370
+
371
+ add_rows(entity_def, query_result, result, limit)
372
+ end
373
+
374
+ result
375
+ end
376
+
377
+ def add_rows(entity_def, query_result, result, limit)
378
+ records = query_result[:records]
379
+ records = [ records ] unless records.is_a?(Array)
380
+
381
+ records.each do |record|
382
+ row = {}
383
+
384
+ record.each do |name, value|
385
+ if name != :type
386
+ # Ids may be returned in an array with 2 duplicate entries...
387
+ value = value[0] if name == :Id && value.is_a?(Array)
388
+
389
+ column = entity_def.api_name_to_column[name.to_s]
390
+ attribute_name = column.name
391
+
392
+ if column.type == :boolean
393
+ row[attribute_name] = (value.casecmp("true") == 0)
394
+ else
395
+ row[attribute_name] = value
396
+ end
397
+ end
398
+ end
399
+
400
+ result << row
401
+
402
+ break if result.size >= limit and limit != 0
403
+ end
404
+ end
405
+
406
+ def select_one(sql, name = nil) #:nodoc:
407
+ # debug "#{sql}"
408
+ # puts "#{sql}"
409
+ self.batch_size = 1
410
+
411
+ result = select_all(sql, name)
412
+
413
+ result.nil? ? nil : result.first
414
+ end
415
+
416
+
417
+ def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
418
+ # log(sql, name) {
419
+ # Convert sql to sobject
420
+ table_name, columns, entity_def = lookup(sql.match(/INSERT\s+INTO\s+(\w+)\s+/mi)[1])
421
+ columns = entity_def.column_name_to_column
422
+
423
+ # Extract array of column names
424
+ names = sql.match(/\((.+)\)\s+VALUES/mi)[1].scan(/\w+/mi)
425
+
426
+ # Extract arrays of values
427
+ values = sql.match(/VALUES\s*\((.+)\)/mi)[1]
428
+ values = values.scan(/(NULL|TRUE|FALSE|'(?:(?:[^']|'')*)'),*/mi).flatten
429
+ values.map! { |v| v.first == "'" ? v.slice(1, v.length - 2) : v == "NULL" ? nil : v }
430
+
431
+ fields = get_fields(columns, names, values, :createable)
432
+
433
+ sobject = create_sobject(entity_def.api_name, nil, fields)
434
+
435
+ # Track the id to be able to update it when the create() is actually executed
436
+ id = String.new
437
+ queue_command ActiveSalesforce::BoxcarCommand::Insert.new(self, sobject, id)
438
+
439
+ id
440
+ # }
441
+ end
442
+
443
+
444
+ def update(sql, name = nil) #:nodoc:
445
+ sql = sql.gsub(/WHERE\s+\([A-Z]+\./mi,"WHERE ")
446
+ # puts("Update: #{sql}, #{name}")
447
+ # log(sql, name) {
448
+ # Convert sql to sobject
449
+ table_name, columns, entity_def = lookup(sql.match(/UPDATE\s+(\w+)\s+/mi)[1])
450
+ columns = entity_def.column_name_to_column
451
+
452
+ match = sql.match(/SET\s+(.+)\s+WHERE/mi)[1]
453
+ names = match.scan(/(\w+)\s*=\s*(?:'|NULL|TRUE|FALSE)/mi).flatten
454
+
455
+ values = match.scan(/=\s*(NULL|TRUE|FALSE|'(?:(?:[^']|'')*)'),*/mi).flatten
456
+ values.map! { |v| v.first == "'" ? v.slice(1, v.length - 2) : v == "NULL" ? nil : v }
457
+
458
+ fields = get_fields(columns, names, values, :updateable)
459
+ null_fields = get_null_fields(columns, names, values, :updateable)
460
+
461
+ ids = sql.match(/WHERE\s+id\s*=\s*'(\w+)'/mi)
462
+ # puts "return if ids.nil? #{ids}"
463
+ return if ids.nil?
464
+ id = ids[1]
465
+
466
+ sobject = create_sobject(entity_def.api_name, id, fields, null_fields)
467
+
468
+ queue_command ActiveSalesforce::BoxcarCommand::Update.new(self, sobject)
469
+ #}
470
+ end
471
+
472
+
473
+ def delete(sql, name = nil)
474
+ # log(sql, name) {
475
+ # Extract the id
476
+ match = sql.match(/WHERE\s+id\s*=\s*'(\w+)'/mi)
477
+
478
+ if match
479
+ ids = [ match[1] ]
480
+ else
481
+ # Check for the form (id IN ('x', 'y'))
482
+ match = sql.match(/WHERE\s+\(\s*id\s+IN\s*\((.+)\)\)/mi)[1]
483
+ ids = match.scan(/\w+/)
484
+ end
485
+
486
+ ids_element = []
487
+ ids.each { |id| ids_element << :ids << id }
488
+
489
+ queue_command ActiveSalesforce::BoxcarCommand::Delete.new(self, ids_element)
490
+ # }
491
+ end
492
+
493
+
494
+ def get_updated(object_type, start_date, end_date, name = nil)
495
+ msg = "get_updated(#{object_type}, #{start_date}, #{end_date})"
496
+ # log(msg, name) {
497
+ get_updated_element = []
498
+ get_updated_element << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << object_type
499
+ get_updated_element << :startDate << start_date
500
+ get_updated_element << :endDate << end_date
501
+
502
+ result = get_result(@connection.getUpdated(get_updated_element), :getUpdated)
503
+
504
+ result[:ids]
505
+ # }
506
+ end
507
+
508
+
509
+ def get_deleted(object_type, start_date, end_date, name = nil)
510
+ msg = "get_deleted(#{object_type}, #{start_date}, #{end_date})"
511
+ # log(msg, name) {
512
+ get_deleted_element = []
513
+ get_deleted_element << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << object_type
514
+ get_deleted_element << :startDate << start_date
515
+ get_deleted_element << :endDate << end_date
516
+
517
+ result = get_result(@connection.getDeleted(get_deleted_element), :getDeleted)
518
+
519
+ ids = []
520
+ result[:deletedRecords].each do |v|
521
+ ids << v[:id]
522
+ end
523
+
524
+ ids
525
+ # }
526
+ end
527
+
528
+
529
+ def get_user_info(name = nil)
530
+ msg = "get_user_info()"
531
+ # log(msg, name) {
532
+ get_result(@connection.getUserInfo([]), :getUserInfo)
533
+ # }
534
+ end
535
+
536
+
537
+ def retrieve_field_values(object_type, fields, ids, name = nil)
538
+ msg = "retrieve(#{object_type}, [#{ids.to_a.join(', ')}])"
539
+ # log(msg, name) {
540
+ retrieve_element = []
541
+ retrieve_element << :fieldList << fields.to_a.join(", ")
542
+ retrieve_element << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << object_type
543
+ ids.to_a.each { |id| retrieve_element << :ids << id }
544
+
545
+ result = get_result(@connection.retrieve(retrieve_element), :retrieve)
546
+
547
+ result = [ result ] unless result.is_a?(Array)
548
+
549
+ # Remove unwanted :type and normalize :Id if required
550
+ field_values = []
551
+ result.each do |v|
552
+ v = v.dup
553
+ v.delete(:type)
554
+ v[:Id] = v[:Id][0] if v[:Id].is_a? Array
555
+
556
+ field_values << v
557
+ end
558
+
559
+ field_values
560
+ # }
561
+ end
562
+
563
+
564
+ def get_fields(columns, names, values, access_check)
565
+ fields = {}
566
+ names.each_with_index do | name, n |
567
+ value = values[n]
568
+
569
+ if value
570
+ column = columns[name]
571
+
572
+ raise ActiveSalesforce::ASFError.new(@logger, "Column not found for #{name}!") unless column
573
+
574
+ value.gsub!(/''/, "'") if value.is_a? String
575
+
576
+ include_field = ((not value.empty?) and column.send(access_check))
577
+
578
+ if (include_field)
579
+ case column.type
580
+ when :date
581
+ value = Time.parse(value + "Z").utc.strftime("%Y-%m-%d")
582
+ when :datetime
583
+ value = Time.parse(value + "Z").utc.strftime("%Y-%m-%dT%H:%M:%SZ")
584
+ end
585
+
586
+ fields[column.api_name] = value
587
+ end
588
+ end
589
+ end
590
+
591
+ fields
592
+ end
593
+
594
+ def get_null_fields(columns, names, values, access_check)
595
+ fields = {}
596
+ names.each_with_index do | name, n |
597
+ value = values[n]
598
+
599
+ if !value
600
+ column = columns[name]
601
+ fields[column.api_name] = nil if column.send(access_check) && column.api_name.casecmp("ownerid") != 0
602
+ end
603
+ end
604
+
605
+ fields
606
+ end
607
+
608
+ def extract_sql_modifier(soql, modifier)
609
+ value = soql.match(/\s+#{modifier}\s+(\d+)/mi)
610
+ if value
611
+ value = value[1].to_i
612
+ soql.sub!(/\s+#{modifier}\s+\d+/mi, "")
613
+ end
614
+
615
+ value
616
+ end
617
+
618
+
619
+ def get_result(response, method)
620
+ responseName = (method.to_s + "Response").to_sym
621
+ finalResponse = response[responseName]
622
+
623
+ raise ActiveSalesforce::ASFError.new(@logger, response[:Fault][:faultstring], response.fault) unless finalResponse
624
+
625
+ result = finalResponse[:result]
626
+ end
627
+
628
+
629
+ def check_result(result)
630
+ result = [ result ] unless result.is_a?(Array)
631
+
632
+ result.each do |r|
633
+ raise ActiveSalesforce::ASFError.new(@logger, r[:errors], r[:errors][:message]) unless r[:success] == "true"
634
+ end
635
+
636
+ result
637
+ end
638
+
639
+
640
+ def get_entity_def(entity_name)
641
+ cached_entity_def = @entity_def_map[entity_name]
642
+
643
+ if cached_entity_def
644
+ # Check for the loss of asf AR setup
645
+ entity_klass = class_from_entity_name(entity_name)
646
+
647
+ configure_active_record(cached_entity_def) unless entity_klass.respond_to?(:asf_augmented?)
648
+
649
+ return cached_entity_def
650
+ end
651
+
652
+ cached_columns = []
653
+ cached_relationships = []
654
+
655
+ begin
656
+ metadata = get_result(@connection.describeSObject(:sObjectType => entity_name), :describeSObject)
657
+ custom = false
658
+ rescue ActiveSalesforce::ASFError
659
+ # Fallback and see if we can find a custom object with this name
660
+ debug(" Unable to find medata for '#{entity_name}', falling back to custom object name #{entity_name + "__c"}")
661
+
662
+ metadata = get_result(@connection.describeSObject(:sObjectType => entity_name + "__c"), :describeSObject)
663
+ custom = true
664
+ end
665
+
666
+ metadata[:fields].each do |field|
667
+ column = SalesforceColumn.new(field)
668
+ cached_columns << column
669
+
670
+ cached_relationships << SalesforceRelationship.new(field, column) if field[:type] =~ /reference/mi
671
+ end
672
+
673
+ relationships = metadata[:childRelationships]
674
+ if relationships
675
+ relationships = [ relationships ] unless relationships.is_a? Array
676
+
677
+ relationships.each do |relationship|
678
+ if relationship[:cascadeDelete] == "true"
679
+ r = SalesforceRelationship.new(relationship)
680
+ cached_relationships << r
681
+ end
682
+ end
683
+ end
684
+
685
+ key_prefix = metadata[:keyPrefix]
686
+
687
+ entity_def = ActiveSalesforce::EntityDefinition.new(self, entity_name, entity_klass,
688
+ cached_columns, cached_relationships, custom, key_prefix)
689
+
690
+ @entity_def_map[entity_name] = entity_def
691
+ @keyprefix_to_entity_def_map[key_prefix] = entity_def
692
+
693
+ configure_active_record(entity_def)
694
+
695
+ entity_def
696
+ end
697
+
698
+
699
+ def configure_active_record(entity_def)
700
+ entity_name = entity_def.name
701
+ klass = class_from_entity_name(entity_name)
702
+
703
+ class << klass
704
+ def asf_augmented?
705
+ true
706
+ end
707
+ end
708
+
709
+ # Add support for SID-based authentication
710
+ ActiveSalesforce::SessionIDAuthenticationFilter.register(klass)
711
+
712
+ klass.set_inheritance_column nil unless entity_def.custom?
713
+ klass.set_primary_key "id"
714
+
715
+ # Create relationships for any reference field
716
+ entity_def.relationships.each do |relationship|
717
+ referenceName = relationship.name
718
+ unless self.respond_to? referenceName.to_sym or relationship.reference_to == "Profile"
719
+ reference_to = relationship.reference_to
720
+ one_to_many = relationship.one_to_many
721
+ foreign_key = relationship.foreign_key
722
+
723
+ # DCHASMAN TODO Figure out how to handle polymorphic refs (e.g. Note.parent can refer to
724
+ # Account, Contact, Opportunity, Contract, Asset, Product2, <CustomObject1> ... <CustomObject(n)>
725
+ if reference_to.is_a? Array
726
+ debug(" Skipping unsupported polymophic one-to-#{one_to_many ? 'many' : 'one' } relationship '#{referenceName}' from #{klass} to [#{relationship.reference_to.join(', ')}] using #{foreign_key}")
727
+ next
728
+ end
729
+
730
+ # Handle references to custom objects
731
+ reference_to = reference_to.chomp("__c").camelize if reference_to.match(/__c$/)
732
+
733
+ begin
734
+ referenced_klass = class_from_entity_name(reference_to)
735
+ rescue NameError => e
736
+ # Automatically create a least a stub for the referenced entity
737
+ debug(" Creating ActiveRecord stub for the referenced entity '#{reference_to}'")
738
+
739
+ referenced_klass = klass.class_eval("Salesforce::#{reference_to} = Class.new(ActiveRecord::Base)")
740
+ referenced_klass.instance_variable_set("@asf_connection", klass.connection)
741
+
742
+ # Automatically inherit the connection from the referencee
743
+ def referenced_klass.connection
744
+ @asf_connection
745
+ end
746
+ end
747
+
748
+ if referenced_klass
749
+ if one_to_many
750
+ assoc_name = reference_to.underscore.pluralize.to_sym
751
+ klass.has_many assoc_name, :class_name => referenced_klass.name, :foreign_key => foreign_key
752
+ else
753
+ assoc_name = reference_to.underscore.singularize.to_sym
754
+ klass.belongs_to assoc_name, :class_name => referenced_klass.name, :foreign_key => foreign_key
755
+ end
756
+
757
+ debug(" Created one-to-#{one_to_many ? 'many' : 'one' } relationship '#{referenceName}' from #{klass} to #{referenced_klass} using #{foreign_key}")
758
+ end
759
+ end
760
+ end
761
+
762
+ end
763
+
764
+
765
+ def columns(table_name, name = nil)
766
+ table_name, columns, entity_def = lookup(table_name)
767
+ entity_def.columns
768
+ end
769
+
770
+ def primary_key(table)
771
+ "id"
772
+ end
773
+
774
+ def class_from_entity_name(entity_name)
775
+ entity_klass = @class_to_entity_map[entity_name.upcase]
776
+ debug("Found matching class '#{entity_klass}' for entity '#{entity_name}'") if entity_klass
777
+
778
+ # Constantize entities under the Salesforce namespace.
779
+ entity_klass = ("Salesforce::" + entity_name).constantize unless entity_klass
780
+
781
+ entity_klass
782
+ end
783
+
784
+
785
+ def create_sobject(entity_name, id, fields, null_fields = [])
786
+ sobj = []
787
+
788
+ sobj << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << entity_name
789
+ sobj << 'Id { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << id if id
790
+
791
+ # add any changed fields
792
+ fields.each do | name, value |
793
+ sobj << name.to_sym << value if value
794
+ end
795
+
796
+ # add null fields
797
+ null_fields.each do | name, value |
798
+ sobj << 'fieldsToNull { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << name
799
+ end
800
+
801
+ [ :sObjects, sobj ]
802
+ end
803
+
804
+
805
+ def column_names(table_name)
806
+ columns(table_name).map { |column| column.name }
807
+ end
808
+
809
+
810
+ def lookup(raw_table_name)
811
+ table_name = raw_table_name.singularize
812
+
813
+ # See if a table name to AR class mapping was registered
814
+ klass = @class_to_entity_map[table_name.upcase]
815
+
816
+ entity_name = klass ? raw_table_name : table_name.camelize
817
+ entity_def = get_entity_def(entity_name)
818
+
819
+ [table_name, entity_def.columns, entity_def]
820
+ end
821
+
822
+
823
+ def debug(msg)
824
+ @logger.debug(msg) if @logger
825
+ end
826
+
827
+ protected
828
+
829
+ # fix single-quote escape sequence for WHERE condition expressions. Salesforce enforces a backspace on SELECTs
830
+ # NOTE: this method is only used for SELECT queries. INSERT/UPDATE queries are smart enough to use the primary
831
+ # key for their WHERE statements, or so I've found.
832
+ def quote_where(sql)
833
+ where_match = sql.match(/WHERE\s*\((.*)\)/mi)
834
+
835
+ return sql unless where_match
836
+ where_conditions = where_match[1]
837
+
838
+ # debug("where_conditions: #{where_conditions}")
839
+ where_conditions.gsub!(/''/, "\\\\'")
840
+
841
+ # debug("updated where_conditions: #{where_conditions.gsub(/''/, "\\\\'")}")
842
+
843
+ sql = "#{where_match.pre_match}WHERE (#{where_conditions})#{where_match.post_match}"
844
+ end
845
+
846
+ def queue_command(command)
847
+ # puts("Queue: #{command}")
848
+ # If @command_boxcar is not nil, then this is a transaction
849
+ # and commands should be queued in the boxcar
850
+ if @command_boxcar
851
+ @command_boxcar << command
852
+
853
+ # If a command is not executed within a transaction, it should
854
+ # be executed immediately
855
+ else
856
+ send_commands([command])
857
+ end
858
+ end
859
+
860
+ end
861
+
862
+ end
863
+ end