althor880-activerecord-activesalesforce-adapter 2.3.5

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