ryandotsmith-asf-soap-adapter 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/LICENSE +20 -0
- data/README.rdoc +125 -0
- data/Rakefile +63 -0
- data/VERSION +1 -0
- data/asf-soap-adapter.gemspec +300 -0
- data/asf-soap-adapter.pptx +0 -0
- data/deploy +6 -0
- data/lib/active_record/connection_adapters/activesalesforce.rb +36 -0
- data/lib/active_record/connection_adapters/activesalesforce_adapter.rb +919 -0
- data/lib/active_record/connection_adapters/asf_active_record.rb +40 -0
- data/lib/active_record/connection_adapters/boxcar_command.rb +66 -0
- data/lib/active_record/connection_adapters/column_definition.rb +95 -0
- data/lib/active_record/connection_adapters/entity_definition.rb +59 -0
- data/lib/active_record/connection_adapters/id_resolver.rb +84 -0
- data/lib/active_record/connection_adapters/recording_binding.rb +93 -0
- data/lib/active_record/connection_adapters/relationship_definition.rb +81 -0
- data/lib/active_record/connection_adapters/result_array.rb +31 -0
- data/lib/active_record/connection_adapters/sid_authentication_filter.rb +57 -0
- data/lib/activerecord-activesalesforce-adapter.rb +1 -0
- data/lib/asf-soap-adapter.rb +34 -0
- data/lib/salesforce/account.rb +28 -0
- data/lib/salesforce/account_feed.rb +27 -0
- data/lib/salesforce/apex_log.rb +28 -0
- data/lib/salesforce/asset.rb +27 -0
- data/lib/salesforce/asset_feed.rb +27 -0
- data/lib/salesforce/campaign.rb +27 -0
- data/lib/salesforce/campaign_feed.rb +27 -0
- data/lib/salesforce/case.rb +27 -0
- data/lib/salesforce/case_feed.rb +27 -0
- data/lib/salesforce/case_team_member.rb +28 -0
- data/lib/salesforce/case_team_role.rb +28 -0
- data/lib/salesforce/chatter_feed.rb +291 -0
- data/lib/salesforce/contact.rb +27 -0
- data/lib/salesforce/contact_feed.rb +29 -0
- data/lib/salesforce/contract.rb +27 -0
- data/lib/salesforce/contract_feed.rb +27 -0
- data/lib/salesforce/entity_subscription.rb +27 -0
- data/lib/salesforce/feed_comment.rb +27 -0
- data/lib/salesforce/feed_post.rb +27 -0
- data/lib/salesforce/feed_tracked_change.rb +27 -0
- data/lib/salesforce/file_writer.rb +61 -0
- data/lib/salesforce/group.rb +28 -0
- data/lib/salesforce/group_member.rb +28 -0
- data/lib/salesforce/lead.rb +27 -0
- data/lib/salesforce/lead_feed.rb +27 -0
- data/lib/salesforce/news_feed.rb +27 -0
- data/lib/salesforce/opportunity.rb +27 -0
- data/lib/salesforce/opportunity_feed.rb +27 -0
- data/lib/salesforce/organization.rb +27 -0
- data/lib/salesforce/product2.rb +27 -0
- data/lib/salesforce/product2_feed.rb +27 -0
- data/lib/salesforce/sf_base.rb +94 -0
- data/lib/salesforce/sf_utility.rb +214 -0
- data/lib/salesforce/solution.rb +27 -0
- data/lib/salesforce/solution_feed.rb +27 -0
- data/lib/salesforce/solution_history.rb +28 -0
- data/lib/salesforce/task.rb +28 -0
- data/lib/salesforce/task_feed.rb +28 -0
- data/lib/salesforce/user.rb +27 -0
- data/lib/salesforce/user_feed.rb +27 -0
- data/lib/salesforce/user_profile_feed.rb +27 -0
- data/lib/salesforce/user_role.rb +28 -0
- data/ryandotsmith-asf-soap-adapter.gemspec +301 -0
- data/test/asf-soap-adapter-rails-app/README +243 -0
- data/test/asf-soap-adapter-rails-app/Rakefile +10 -0
- data/test/asf-soap-adapter-rails-app/app/controllers/adapter_homes_controller.rb +83 -0
- data/test/asf-soap-adapter-rails-app/app/controllers/application_controller.rb +10 -0
- data/test/asf-soap-adapter-rails-app/app/helpers/adapter_homes_helper.rb +2 -0
- data/test/asf-soap-adapter-rails-app/app/helpers/application_helper.rb +3 -0
- data/test/asf-soap-adapter-rails-app/app/models/adapter_home.rb +2 -0
- data/test/asf-soap-adapter-rails-app/app/views/adapter_homes/edit.html.erb +24 -0
- data/test/asf-soap-adapter-rails-app/app/views/adapter_homes/index.html.erb +24 -0
- data/test/asf-soap-adapter-rails-app/app/views/adapter_homes/new.html.erb +23 -0
- data/test/asf-soap-adapter-rails-app/app/views/adapter_homes/show.html.erb +18 -0
- data/test/asf-soap-adapter-rails-app/app/views/layouts/adapter_homes.html.erb +17 -0
- data/test/asf-soap-adapter-rails-app/config/boot.rb +110 -0
- data/test/asf-soap-adapter-rails-app/config/database.yml +30 -0
- data/test/asf-soap-adapter-rails-app/config/environment.rb +44 -0
- data/test/asf-soap-adapter-rails-app/config/environments/development.rb +17 -0
- data/test/asf-soap-adapter-rails-app/config/environments/production.rb +28 -0
- data/test/asf-soap-adapter-rails-app/config/environments/salesforce-default-realm.rb +30 -0
- data/test/asf-soap-adapter-rails-app/config/environments/test.rb +28 -0
- data/test/asf-soap-adapter-rails-app/config/initializers/backtrace_silencers.rb +7 -0
- data/test/asf-soap-adapter-rails-app/config/initializers/cookie_verification_secret.rb +7 -0
- data/test/asf-soap-adapter-rails-app/config/initializers/inflections.rb +10 -0
- data/test/asf-soap-adapter-rails-app/config/initializers/mime_types.rb +5 -0
- data/test/asf-soap-adapter-rails-app/config/initializers/new_rails_defaults.rb +21 -0
- data/test/asf-soap-adapter-rails-app/config/initializers/session_store.rb +15 -0
- data/test/asf-soap-adapter-rails-app/config/locales/en.yml +5 -0
- data/test/asf-soap-adapter-rails-app/config/routes.rb +46 -0
- data/test/asf-soap-adapter-rails-app/db/development.sqlite3 +0 -0
- data/test/asf-soap-adapter-rails-app/db/migrate/20101107202112_create_adapter_homes.rb +15 -0
- data/test/asf-soap-adapter-rails-app/db/schema.rb +22 -0
- data/test/asf-soap-adapter-rails-app/db/seeds.rb +7 -0
- data/test/asf-soap-adapter-rails-app/db/test.sqlite3 +0 -0
- data/test/asf-soap-adapter-rails-app/log/development.log +51 -0
- data/test/asf-soap-adapter-rails-app/log/salesforce-default-realm.log +928 -0
- data/test/asf-soap-adapter-rails-app/nbproject/private/config.properties +0 -0
- data/test/asf-soap-adapter-rails-app/nbproject/private/private.properties +3 -0
- data/test/asf-soap-adapter-rails-app/nbproject/private/private.xml +4 -0
- data/test/asf-soap-adapter-rails-app/nbproject/private/rake-d.txt +94 -0
- data/test/asf-soap-adapter-rails-app/nbproject/project.properties +5 -0
- data/test/asf-soap-adapter-rails-app/nbproject/project.xml +9 -0
- data/test/asf-soap-adapter-rails-app/public/404.html +30 -0
- data/test/asf-soap-adapter-rails-app/public/422.html +30 -0
- data/test/asf-soap-adapter-rails-app/public/500.html +30 -0
- data/test/asf-soap-adapter-rails-app/public/favicon.ico +0 -0
- data/test/asf-soap-adapter-rails-app/public/images/rails.png +0 -0
- data/test/asf-soap-adapter-rails-app/public/javascripts/application.js +2 -0
- data/test/asf-soap-adapter-rails-app/public/javascripts/controls.js +963 -0
- data/test/asf-soap-adapter-rails-app/public/javascripts/dragdrop.js +973 -0
- data/test/asf-soap-adapter-rails-app/public/javascripts/effects.js +1128 -0
- data/test/asf-soap-adapter-rails-app/public/javascripts/prototype.js +4320 -0
- data/test/asf-soap-adapter-rails-app/public/robots.txt +5 -0
- data/test/asf-soap-adapter-rails-app/public/stylesheets/scaffold.css +54 -0
- data/test/asf-soap-adapter-rails-app/script/about +4 -0
- data/test/asf-soap-adapter-rails-app/script/console +3 -0
- data/test/asf-soap-adapter-rails-app/script/dbconsole +3 -0
- data/test/asf-soap-adapter-rails-app/script/destroy +3 -0
- data/test/asf-soap-adapter-rails-app/script/generate +3 -0
- data/test/asf-soap-adapter-rails-app/script/performance/benchmarker +3 -0
- data/test/asf-soap-adapter-rails-app/script/performance/profiler +3 -0
- data/test/asf-soap-adapter-rails-app/script/plugin +3 -0
- data/test/asf-soap-adapter-rails-app/script/runner +3 -0
- data/test/asf-soap-adapter-rails-app/script/server +3 -0
- data/test/asf-soap-adapter-rails-app/test/performance/browsing_test.rb +9 -0
- data/test/asf-soap-adapter-rails-app/test/test_helper.rb +44 -0
- data/test/asf-soap-adapter-rails-app/test/test_keys.rb +4 -0
- data/test/asf-soap-adapter-rails-app/test/unit/adapter_home_test.rb +8 -0
- data/test/asf-soap-adapter-rails-app/test/unit/helpers/adapter_homes_helper_test.rb +4 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/account_feed_test.rb +16 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/account_test.rb +66 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/apex_log_test.rb +10 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/asset_feed_test.rb +14 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/asset_test.rb +13 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/campaign_feed_test.rb +13 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/campaign_test.rb +13 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/case_feed_test.rb +13 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/case_team_member_test.rb +10 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/case_team_role.rb +10 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/case_test.rb +13 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/chatter_feed_test.rb +53 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/contact_feed_test.rb +13 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/contact_test.rb +14 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/contract_feed_test.rb +13 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/contract_test.rb +13 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/entity_subscription_test.rb +13 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/feed_comment_test.rb +9 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/feed_post_test.rb +10 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/feed_tracked_change_test.rb +9 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/group_member_test.rb +10 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/group_test.rb +24 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/lead_feed_test.rb +13 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/lead_test.rb +40 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/news_feed_test.rb +13 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/opportunity_feed_test.rb +13 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/opportunity_test.rb +13 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/organization_test.rb +8 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/product2_feed_test.rb +13 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/product2_test.rb +13 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/salesforce_base_test.rb +10 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/solution_feed_test.rb +13 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/solution_history_test.rb +10 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/solution_test.rb +14 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/task_feed_test.rb +10 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/task_test.rb +10 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/user_feed_test.rb +13 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/user_profile_feed_test.rb +11 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/user_role_test.rb +10 -0
- data/test/asf-soap-adapter-rails-app/test/unit/salesforce/user_test.rb +48 -0
- data/test/basic_test.rb +204 -0
- data/test/config.yml +5 -0
- data/test/helper.rb +10 -0
- data/test/recorded_test_case.rb +87 -0
- data/test/simple_test.rb +96 -0
- data/test/tests_without_rail.rb +65 -0
- metadata +401 -0
Binary file
|
data/deploy
ADDED
@@ -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,919 @@
|
|
1
|
+
=begin
|
2
|
+
ActiveSalesforce
|
3
|
+
Copyright 2006 Doug Chasman
|
4
|
+
2010 updated by Raymond Gao
|
5
|
+
|
6
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
you may not use this file except in compliance with the License.
|
8
|
+
You may obtain a copy of the License at
|
9
|
+
|
10
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
|
12
|
+
Unless required by applicable law or agreed to in writing, software
|
13
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
See the License for the specific language governing permissions and
|
16
|
+
limitations under the License.
|
17
|
+
=end
|
18
|
+
|
19
|
+
require 'thread'
|
20
|
+
require 'benchmark'
|
21
|
+
|
22
|
+
require 'active_record'
|
23
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
24
|
+
|
25
|
+
# replaced hardlink to 'rforce' gem as a pre-requisite, allowing parallel evolution of both gem.
|
26
|
+
require 'rforce'
|
27
|
+
#require File.dirname(__FILE__) + '/../../rforce'
|
28
|
+
require File.dirname(__FILE__) + '/column_definition'
|
29
|
+
require File.dirname(__FILE__) + '/relationship_definition'
|
30
|
+
require File.dirname(__FILE__) + '/boxcar_command'
|
31
|
+
require File.dirname(__FILE__) + '/entity_definition'
|
32
|
+
require File.dirname(__FILE__) + '/asf_active_record'
|
33
|
+
require File.dirname(__FILE__) + '/id_resolver'
|
34
|
+
require File.dirname(__FILE__) + '/sid_authentication_filter'
|
35
|
+
require File.dirname(__FILE__) + '/recording_binding'
|
36
|
+
require File.dirname(__FILE__) + '/result_array'
|
37
|
+
|
38
|
+
# See http://www.rubular.com/ for Ruby Regular Expression
|
39
|
+
# See http://api.rubyonrails.org/classes/ActiveRecord/Base.html for documentation
|
40
|
+
# ActiveRecord
|
41
|
+
# To Understand how to write an adapter, see:
|
42
|
+
# http://ar.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/AbstractAdapter.html
|
43
|
+
# and
|
44
|
+
# http://rails.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/AbstractAdapter.html
|
45
|
+
|
46
|
+
module ActiveRecord
|
47
|
+
# Overrides the ActiveRecord::Base class to provide Salesforce Web Services services
|
48
|
+
# Particularly important are 1) getting a 'binding' and 2) 'api_version'
|
49
|
+
class Base
|
50
|
+
@@cache = {}
|
51
|
+
|
52
|
+
def self.debug(msg)
|
53
|
+
logger.debug(msg) if logger
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.flush_connections()
|
57
|
+
@@cache = {}
|
58
|
+
end
|
59
|
+
|
60
|
+
# Establishes a connection to the database that's used by all Active Record objects.
|
61
|
+
def self.activesalesforce_connection(config) # :nodoc:
|
62
|
+
debug("\nUsing ActiveSalesforce connection\n")
|
63
|
+
|
64
|
+
database_com_url = ENV["DATABASE_COM_URL"]
|
65
|
+
raise "Set DATABASE_COM_URL" if database_com_url.nil?
|
66
|
+
database_com_uri = URI.parse(database_com_url)
|
67
|
+
|
68
|
+
# Default to production system using 20.0 API
|
69
|
+
#url = config[:url]
|
70
|
+
#url = database_com_uri.path
|
71
|
+
url = "https://www.salesforce.com" #unless url
|
72
|
+
|
73
|
+
#Take the API version from the Config file e.g. 'database.yml' -> 'salesforce-default-realm'
|
74
|
+
api_version = config[:api_version] ? config[:api_version] : "20.0"
|
75
|
+
uri = URI.parse(url)
|
76
|
+
#uri.path = "/services/Soap/u/20.0"
|
77
|
+
uri.path = "/services/Soap/u/" + (api_version).to_s
|
78
|
+
url = uri.to_s
|
79
|
+
|
80
|
+
sid = config[:sid]
|
81
|
+
client_id = config[:client_id]
|
82
|
+
#username = config[:username].to_s
|
83
|
+
#password = config[:password].to_s
|
84
|
+
username = URI.unescape(database_com_uri.user)
|
85
|
+
password = URI.unescape(database_com_uri.password)
|
86
|
+
|
87
|
+
# Recording/playback support
|
88
|
+
recording_source = config[:recording_source]
|
89
|
+
recording = config[:recording]
|
90
|
+
|
91
|
+
if recording_source
|
92
|
+
recording_source = File.open(recording_source, recording ? "w" : "r")
|
93
|
+
binding = ActiveSalesforce::RecordingBinding.new(url, nil, recording != nil, recording_source, logger)
|
94
|
+
binding.client_id = client_id if client_id
|
95
|
+
binding.login(username, password) unless sid
|
96
|
+
end
|
97
|
+
|
98
|
+
# Check to insure that the second to last path component is a 'u' for Partner API
|
99
|
+
raise ActiveSalesforce::ASFError.new(logger, "Invalid salesforce server url '#{url}', must be a valid Parter API URL") unless url.match(/\/u\//mi)
|
100
|
+
|
101
|
+
if sid
|
102
|
+
binding = @@cache["sid=#{sid}"] unless binding
|
103
|
+
|
104
|
+
unless binding
|
105
|
+
debug("Establishing new connection for [sid='#{sid}']")
|
106
|
+
|
107
|
+
binding = RForce::Binding.new(url, sid)
|
108
|
+
@@cache["sid=#{sid}"] = binding
|
109
|
+
|
110
|
+
debug("Created new connection for [sid='#{sid}']")
|
111
|
+
else
|
112
|
+
debug("Reused existing connection for [sid='#{sid}']")
|
113
|
+
end
|
114
|
+
else
|
115
|
+
binding = @@cache["#{url}.#{username}.#{password}.#{client_id}"] unless binding
|
116
|
+
|
117
|
+
unless binding
|
118
|
+
debug("Establishing new connection for ['#{url}', '#{username}, '#{client_id}'")
|
119
|
+
|
120
|
+
seconds = Benchmark.realtime {
|
121
|
+
binding = RForce::Binding.new(url, sid)
|
122
|
+
binding.login(username, password)
|
123
|
+
|
124
|
+
@@cache["#{url}.#{username}.#{password}.#{client_id}"] = binding
|
125
|
+
}
|
126
|
+
|
127
|
+
debug("Created new connection for ['#{url}', '#{username}', '#{client_id}'] in #{seconds} seconds")
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
ConnectionAdapters::SalesforceAdapter.new(binding, logger, config)
|
132
|
+
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
|
137
|
+
module ConnectionAdapters
|
138
|
+
# Inherit from <em>ConnectionAdapters::AbstractAdapter</em> see:
|
139
|
+
# http://rails.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/AbstractAdapter.html
|
140
|
+
class SalesforceAdapter < AbstractAdapter
|
141
|
+
include StringHelper
|
142
|
+
|
143
|
+
MAX_BOXCAR_SIZE = 200
|
144
|
+
|
145
|
+
attr_accessor :batch_size
|
146
|
+
attr_reader :entity_def_map, :keyprefix_to_entity_def_map, :config, :class_to_entity_map
|
147
|
+
|
148
|
+
# Create a new instance of the connection adapter
|
149
|
+
def initialize(connection, logger, config)
|
150
|
+
super(connection, logger)
|
151
|
+
|
152
|
+
@connection_options = nil
|
153
|
+
@config = config
|
154
|
+
|
155
|
+
@entity_def_map = {}
|
156
|
+
@keyprefix_to_entity_def_map = {}
|
157
|
+
|
158
|
+
@command_boxcar = nil
|
159
|
+
@class_to_entity_map = {}
|
160
|
+
end
|
161
|
+
|
162
|
+
def set_class_for_entity(klass, entity_name)
|
163
|
+
debug("Setting @class_to_entity_map['#{entity_name.upcase}'] = #{klass} for connection #{self}")
|
164
|
+
@class_to_entity_map[entity_name.upcase] = klass
|
165
|
+
end
|
166
|
+
|
167
|
+
# returns the binding associated with the current adapter
|
168
|
+
# e.g Salesforce::User.first.connection.binding -> returns the binding.
|
169
|
+
def binding
|
170
|
+
@connection
|
171
|
+
end
|
172
|
+
|
173
|
+
#Sets the adapter name to "ActiveSalesforce"
|
174
|
+
def adapter_name #:nodoc:
|
175
|
+
'ActiveSalesforce'
|
176
|
+
end
|
177
|
+
|
178
|
+
def supports_migrations? #:nodoc:
|
179
|
+
false
|
180
|
+
end
|
181
|
+
|
182
|
+
# For Silent-e, added 'tables' method to solve ARel problem
|
183
|
+
def tables(name = nil) #:nodoc:
|
184
|
+
@connection.describeGlobal({}).describeGlobalResponse.result.types
|
185
|
+
end
|
186
|
+
|
187
|
+
def table_exists?(table_name)
|
188
|
+
true
|
189
|
+
end
|
190
|
+
|
191
|
+
#-- QUOTING ==================================================
|
192
|
+
def quote(value, column = nil)
|
193
|
+
case value
|
194
|
+
when NilClass then quoted_value = "NULL"
|
195
|
+
when TrueClass then quoted_value = "TRUE"
|
196
|
+
when FalseClass then quoted_value = "FALSE"
|
197
|
+
when Float, Fixnum, Bignum then quoted_value = "'#{value.to_s}'"
|
198
|
+
else quoted_value = super(value, column)
|
199
|
+
end
|
200
|
+
|
201
|
+
quoted_value
|
202
|
+
end
|
203
|
+
|
204
|
+
#-- CONNECTION MANAGEMENT ====================================
|
205
|
+
# Set the connection state to active
|
206
|
+
def active?
|
207
|
+
true
|
208
|
+
end
|
209
|
+
|
210
|
+
# Overrides the basic method for ActiveRecord::ConnectionAdapters::AbstractAdapter
|
211
|
+
def reconnect!
|
212
|
+
connect
|
213
|
+
end
|
214
|
+
|
215
|
+
# TRANSACTIOn SUPPORT (Boxcarring really because the salesforce.com api does not support transactions)
|
216
|
+
|
217
|
+
# Override AbstractAdapter's transaction method to implement
|
218
|
+
# per-connection support for nested transactions that do not commit until
|
219
|
+
# the outermost transaction is finished. ActiveRecord provides support
|
220
|
+
# for this, but does not distinguish between database connections, which
|
221
|
+
# prevents opening transactions to two different databases at the same
|
222
|
+
# time.
|
223
|
+
def transaction_with_nesting_support(*args, &block)
|
224
|
+
open = Thread.current["open_transactions_for_#{self.class.name.underscore}"] ||= 0
|
225
|
+
Thread.current["open_transactions_for_#{self.class.name.underscore}"] = open + 1
|
226
|
+
|
227
|
+
begin
|
228
|
+
transaction_without_nesting_support(&block)
|
229
|
+
ensure
|
230
|
+
Thread.current["open_transactions_for_#{self.class.name.underscore}"] -= 1
|
231
|
+
end
|
232
|
+
end
|
233
|
+
alias_method_chain :transaction, :nesting_support
|
234
|
+
|
235
|
+
# Begins the transaction (and turns off auto-committing).
|
236
|
+
def begin_db_transaction
|
237
|
+
log('Opening boxcar', 'begin_db_transaction()')
|
238
|
+
@command_boxcar = []
|
239
|
+
end
|
240
|
+
|
241
|
+
# Calls <em>RForce::Binding</em> -> <em>method_missing</em> -> <em>call_remote method, args[0] methods</em>
|
242
|
+
def send_commands(commands)
|
243
|
+
# Send the boxcar'ed command set
|
244
|
+
verb = commands[0].verb
|
245
|
+
|
246
|
+
args = []
|
247
|
+
commands.each do |command|
|
248
|
+
command.args.each { |arg| args << arg }
|
249
|
+
end
|
250
|
+
|
251
|
+
response = @connection.send(verb, args)
|
252
|
+
|
253
|
+
result = get_result(response, verb)
|
254
|
+
|
255
|
+
result = [ result ] unless result.is_a?(Array)
|
256
|
+
|
257
|
+
errors = []
|
258
|
+
result.each_with_index do |r, n|
|
259
|
+
success = r[:success] == "true"
|
260
|
+
|
261
|
+
# Give each command a chance to process its own result
|
262
|
+
command = commands[n]
|
263
|
+
command.after_execute(r)
|
264
|
+
|
265
|
+
# Handle the set of failures
|
266
|
+
errors << r[:errors] unless r[:success] == "true"
|
267
|
+
end
|
268
|
+
|
269
|
+
unless errors.empty?
|
270
|
+
message = errors.join("\n")
|
271
|
+
fault = (errors.map { |error| error[:message] }).join("\n")
|
272
|
+
raise ActiveSalesforce::ASFError.new(@logger, message, fault)
|
273
|
+
end
|
274
|
+
|
275
|
+
result
|
276
|
+
end
|
277
|
+
|
278
|
+
# Commits the transaction (and turns on auto-committing).
|
279
|
+
def commit_db_transaction()
|
280
|
+
log("Committing boxcar with #{@command_boxcar.length} commands", 'commit_db_transaction()')
|
281
|
+
|
282
|
+
previous_command = nil
|
283
|
+
commands = []
|
284
|
+
|
285
|
+
@command_boxcar.each do |command|
|
286
|
+
if commands.length >= MAX_BOXCAR_SIZE or (previous_command and (command.verb != previous_command.verb))
|
287
|
+
send_commands(commands)
|
288
|
+
|
289
|
+
commands = []
|
290
|
+
previous_command = nil
|
291
|
+
else
|
292
|
+
commands << command
|
293
|
+
previous_command = command
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
# Discard the command boxcar
|
298
|
+
@command_boxcar = nil
|
299
|
+
|
300
|
+
# Finish off the partial boxcar
|
301
|
+
send_commands(commands) unless commands.empty?
|
302
|
+
|
303
|
+
end
|
304
|
+
|
305
|
+
# Rolls back the transaction (and turns on auto-committing). Must be
|
306
|
+
# done if the transaction block raises an exception or returns false.
|
307
|
+
def rollback_db_transaction()
|
308
|
+
log('Rolling back boxcar', 'rollback_db_transaction()')
|
309
|
+
@command_boxcar = nil
|
310
|
+
end
|
311
|
+
|
312
|
+
|
313
|
+
# DATABASE STATEMENTS ======================================
|
314
|
+
# This method is very important. Pretty much all the 'find' methods call
|
315
|
+
# it. This is the place to toggle linebreak for debugging purpose.
|
316
|
+
# In essence, your finder method calls ActiveRecord, which generates a 'sql'
|
317
|
+
# And, this methods turn it into a 'soql' statement, and use Rforce to call
|
318
|
+
# Salesforce and gets a result.
|
319
|
+
# So, if you are having problems, make sure you check 'soql' and toggle the
|
320
|
+
# 'result' object.
|
321
|
+
def select_all(sql, name = nil) #:nodoc:
|
322
|
+
|
323
|
+
# silent-e's solution for single quote escape
|
324
|
+
# fix the single quote escape method in WHERE condition expression
|
325
|
+
sql = fix_single_quote_in_where(sql)
|
326
|
+
# Arel adds the class to the selection - we do not want this i.e...
|
327
|
+
# SELECT contacts.* FROM => SELECT * FROM
|
328
|
+
sql = sql.gsub(/SELECT\s+[^\(][A-Z]+\./mi," ")
|
329
|
+
|
330
|
+
|
331
|
+
raw_table_name = sql.match(/FROM (\w+)/mi)[1]
|
332
|
+
|
333
|
+
table_name, columns, entity_def = lookup(raw_table_name)
|
334
|
+
|
335
|
+
column_names = columns.map { |column| column.api_name }
|
336
|
+
|
337
|
+
# Check for SELECT COUNT(*) FROM query
|
338
|
+
|
339
|
+
# Rails 1.1
|
340
|
+
selectCountMatch = sql.match(/SELECT\s+COUNT\(\*\)\s+AS\s+count_all\s+FROM/mi)
|
341
|
+
|
342
|
+
# Rails 1.0
|
343
|
+
selectCountMatch = sql.match(/SELECT\s+COUNT\(\*\)\s+FROM/mi) unless selectCountMatch
|
344
|
+
|
345
|
+
if selectCountMatch
|
346
|
+
soql = "SELECT COUNT() FROM#{selectCountMatch.post_match}"
|
347
|
+
else
|
348
|
+
if sql.match(/SELECT\s+\*\s+FROM/mi)
|
349
|
+
# Always convert SELECT * to select all columns (required for the AR attributes mechanism to work correctly)
|
350
|
+
soql = sql.sub(/SELECT .+ FROM/mi, "SELECT #{column_names.join(', ')} FROM")
|
351
|
+
else
|
352
|
+
soql = sql
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
soql.sub!(/\s+FROM\s+\w+/mi, " FROM #{entity_def.api_name}")
|
357
|
+
|
358
|
+
if selectCountMatch
|
359
|
+
query_result = get_result(@connection.query(:queryString => soql), :query)
|
360
|
+
return [{ :count => query_result[:size] }]
|
361
|
+
end
|
362
|
+
|
363
|
+
# Look for a LIMIT clause
|
364
|
+
limit = extract_sql_modifier(soql, "LIMIT")
|
365
|
+
limit = MAX_BOXCAR_SIZE unless limit
|
366
|
+
|
367
|
+
# Look for an OFFSET clause
|
368
|
+
offset = extract_sql_modifier(soql, "OFFSET")
|
369
|
+
|
370
|
+
# Fixup column references to use api names
|
371
|
+
columns = entity_def.column_name_to_column
|
372
|
+
soql.gsub!(/((?:\w+\.)?\w+)(?=\s*(?:=|!=|<|>|<=|>=|like)\s*(?:'[^']*'|NULL|TRUE|FALSE))/mi) do |column_name|
|
373
|
+
# strip away any table alias
|
374
|
+
column_name.sub!(/\w+\./, '')
|
375
|
+
|
376
|
+
column = columns[column_name]
|
377
|
+
raise ActiveSalesforce::ASFError.new(@logger, "Column not found for #{column_name} - in <select_all> method!") unless column
|
378
|
+
|
379
|
+
column.api_name
|
380
|
+
end
|
381
|
+
|
382
|
+
# Update table name references
|
383
|
+
soql.sub!(/#{raw_table_name}\./mi, "#{entity_def.api_name}.")
|
384
|
+
|
385
|
+
@connection.batch_size = @batch_size if @batch_size
|
386
|
+
@batch_size = nil
|
387
|
+
|
388
|
+
query_result = get_result(@connection.query(:queryString => soql), :query)
|
389
|
+
result = ActiveSalesforce::ResultArray.new(query_result[:size].to_i)
|
390
|
+
return result unless query_result[:records]
|
391
|
+
|
392
|
+
add_rows(entity_def, query_result, result, limit)
|
393
|
+
|
394
|
+
while ((query_result[:done].casecmp("true") != 0) and (result.size < limit or limit == 0))
|
395
|
+
# Now queryMore
|
396
|
+
locator = query_result[:queryLocator];
|
397
|
+
query_result = get_result(@connection.queryMore(:queryLocator => locator), :queryMore)
|
398
|
+
|
399
|
+
add_rows(entity_def, query_result, result, limit)
|
400
|
+
end
|
401
|
+
|
402
|
+
result
|
403
|
+
end
|
404
|
+
|
405
|
+
# This methods constructs an array of result objects to be returned to your
|
406
|
+
# ActiveRecord's finder method.
|
407
|
+
def add_rows(entity_def, query_result, result, limit)
|
408
|
+
records = query_result[:records]
|
409
|
+
records = [ records ] unless records.is_a?(Array)
|
410
|
+
|
411
|
+
records.each do |record|
|
412
|
+
row = {}
|
413
|
+
|
414
|
+
record.each do |name, value|
|
415
|
+
if name != :type
|
416
|
+
# Ids may be returned in an array with 2 duplicate entries...
|
417
|
+
value = value[0] if name == :Id && value.is_a?(Array)
|
418
|
+
|
419
|
+
column = entity_def.api_name_to_column[name.to_s]
|
420
|
+
attribute_name = column.name
|
421
|
+
|
422
|
+
if column.type == :boolean
|
423
|
+
row[attribute_name] = (value.casecmp("true") == 0)
|
424
|
+
else
|
425
|
+
row[attribute_name] = value
|
426
|
+
end
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
result << row
|
431
|
+
|
432
|
+
break if result.size >= limit and limit != 0
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
# Calls the <b><em>'select_all'</em></b> method, but limits the result to return only a
|
437
|
+
# single row.
|
438
|
+
def select_one(sql, name = nil) #:nodoc:
|
439
|
+
self.batch_size = 1
|
440
|
+
|
441
|
+
result = select_all(sql, name)
|
442
|
+
|
443
|
+
result.nil? ? nil : result.first
|
444
|
+
end
|
445
|
+
|
446
|
+
# Insert object into Salesforce DB
|
447
|
+
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
|
448
|
+
log(sql, name) {
|
449
|
+
# Convert sql to sobject
|
450
|
+
table_name, columns, entity_def = lookup(sql.match(/INSERT\s+INTO\s+(\w+)\s+/mi)[1])
|
451
|
+
columns = entity_def.column_name_to_column
|
452
|
+
|
453
|
+
# Extract array of column names
|
454
|
+
names = sql.match(/\((.+)\)\s+VALUES/mi)[1].scan(/\w+/mi)
|
455
|
+
|
456
|
+
# Extract arrays of values
|
457
|
+
values = sql.match(/VALUES\s*\((.+)\)/mi)[1]
|
458
|
+
values = values.scan(/(NULL|TRUE|FALSE|'(?:(?:[^']|'')*)'),*/mi).flatten
|
459
|
+
values.map! { |v| v.first == "'" ? v.slice(1, v.length - 2) : v == "NULL" ? nil : v }
|
460
|
+
|
461
|
+
fields = get_fields(columns, names, values, :createable)
|
462
|
+
|
463
|
+
sobject = create_sobject(entity_def.api_name, nil, fields)
|
464
|
+
|
465
|
+
# Track the id to be able to update it when the create() is actually executed
|
466
|
+
id = String.new
|
467
|
+
queue_command ActiveSalesforce::BoxcarCommand::Insert.new(self, sobject, id)
|
468
|
+
|
469
|
+
id
|
470
|
+
}
|
471
|
+
end
|
472
|
+
|
473
|
+
# Updates object(s) via SQL. It is an adapter method to be used by ActiveRecord
|
474
|
+
def update(sql, name = nil) #:nodoc:
|
475
|
+
# From silent-e, solution for ARel
|
476
|
+
sql = sql.gsub(/WHERE\s+\([A-Z]+\./mi,"WHERE ")
|
477
|
+
|
478
|
+
|
479
|
+
#log(sql, name) {
|
480
|
+
# Convert sql to sobject
|
481
|
+
table_name, columns, entity_def = lookup(sql.match(/UPDATE\s+(\w+)\s+/mi)[1])
|
482
|
+
columns = entity_def.column_name_to_column
|
483
|
+
|
484
|
+
match = sql.match(/SET\s+(.+)\s+WHERE/mi)[1]
|
485
|
+
names = match.scan(/(\w+)\s*=\s*(?:'|NULL|TRUE|FALSE)/mi).flatten
|
486
|
+
|
487
|
+
values = match.scan(/=\s*(NULL|TRUE|FALSE|'(?:(?:[^']|'')*)'),*/mi).flatten
|
488
|
+
values.map! { |v| v.first == "'" ? v.slice(1, v.length - 2) : v == "NULL" ? nil : v }
|
489
|
+
|
490
|
+
fields = get_fields(columns, names, values, :updateable)
|
491
|
+
null_fields = get_null_fields(columns, names, values, :updateable)
|
492
|
+
|
493
|
+
ids = sql.match(/WHERE\s+id\s*=\s*'(\w+)'/mi)
|
494
|
+
return if ids.nil?
|
495
|
+
id = ids[1]
|
496
|
+
|
497
|
+
sobject = create_sobject(entity_def.api_name, id, fields, null_fields)
|
498
|
+
|
499
|
+
queue_command ActiveSalesforce::BoxcarCommand::Update.new(self, sobject)
|
500
|
+
#}
|
501
|
+
end
|
502
|
+
|
503
|
+
# Delete object(s) from Salesforce
|
504
|
+
def delete(sql, name = nil)
|
505
|
+
log(sql, name) {
|
506
|
+
# Extract the id
|
507
|
+
match = sql.match(/WHERE\s+id\s*=\s*'(\w+)'/mi)
|
508
|
+
|
509
|
+
if match
|
510
|
+
ids = [ match[1] ]
|
511
|
+
else
|
512
|
+
# Check for the form (id IN ('x', 'y'))
|
513
|
+
match = sql.match(/WHERE\s+\(\s*id\s+IN\s*\((.+)\)\)/mi)[1]
|
514
|
+
ids = match.scan(/\w+/)
|
515
|
+
end
|
516
|
+
|
517
|
+
ids_element = []
|
518
|
+
ids.each { |id| ids_element << :ids << id }
|
519
|
+
|
520
|
+
queue_command ActiveSalesforce::BoxcarCommand::Delete.new(self, ids_element)
|
521
|
+
}
|
522
|
+
end
|
523
|
+
|
524
|
+
# If a SF object is dirty, it is called to get fresh attributes.
|
525
|
+
def get_updated(object_type, start_date, end_date, name = nil)
|
526
|
+
msg = "get_updated(#{object_type}, #{start_date}, #{end_date})"
|
527
|
+
log(msg, name) {
|
528
|
+
get_updated_element = []
|
529
|
+
get_updated_element << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << object_type
|
530
|
+
get_updated_element << :startDate << start_date
|
531
|
+
get_updated_element << :endDate << end_date
|
532
|
+
|
533
|
+
result = get_result(@connection.getUpdated(get_updated_element), :getUpdated)
|
534
|
+
|
535
|
+
result[:ids]
|
536
|
+
}
|
537
|
+
end
|
538
|
+
|
539
|
+
# Get SF object(s) that have been marked as deleted but not yet permanently removed, like things in a recycle bin.
|
540
|
+
def get_deleted(object_type, start_date, end_date, name = nil)
|
541
|
+
msg = "get_deleted(#{object_type}, #{start_date}, #{end_date})"
|
542
|
+
log(msg, name) {
|
543
|
+
get_deleted_element = []
|
544
|
+
get_deleted_element << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << object_type
|
545
|
+
get_deleted_element << :startDate << start_date
|
546
|
+
get_deleted_element << :endDate << end_date
|
547
|
+
|
548
|
+
result = get_result(@connection.getDeleted(get_deleted_element), :getDeleted)
|
549
|
+
|
550
|
+
ids = []
|
551
|
+
result[:deletedRecords].each do |v|
|
552
|
+
ids << v[:id]
|
553
|
+
end
|
554
|
+
|
555
|
+
ids
|
556
|
+
}
|
557
|
+
end
|
558
|
+
|
559
|
+
# Returns information about the user account which is used to connect to salesforce.
|
560
|
+
# <b>Salesforce::User.first.connection.get_user_info</b>
|
561
|
+
def get_user_info(name = nil)
|
562
|
+
msg = "get_user_info()"
|
563
|
+
log(msg, name) {
|
564
|
+
get_result(@connection.getUserInfo([]), :getUserInfo)
|
565
|
+
}
|
566
|
+
end
|
567
|
+
|
568
|
+
# Get value given object type, fields, and Salesforce Object.
|
569
|
+
def retrieve_field_values(object_type, fields, ids, name = nil)
|
570
|
+
msg = "retrieve(#{object_type}, [#{ids.to_a.join(', ')}])"
|
571
|
+
log(msg, name) {
|
572
|
+
retrieve_element = []
|
573
|
+
retrieve_element << :fieldList << fields.to_a.join(", ")
|
574
|
+
retrieve_element << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << object_type
|
575
|
+
ids.to_a.each { |id| retrieve_element << :ids << id }
|
576
|
+
|
577
|
+
result = get_result(@connection.retrieve(retrieve_element), :retrieve)
|
578
|
+
|
579
|
+
result = [ result ] unless result.is_a?(Array)
|
580
|
+
|
581
|
+
# Remove unwanted :type and normalize :Id if required
|
582
|
+
field_values = []
|
583
|
+
result.each do |v|
|
584
|
+
v = v.dup
|
585
|
+
v.delete(:type)
|
586
|
+
v[:Id] = v[:Id][0] if v[:Id].is_a? Array
|
587
|
+
|
588
|
+
field_values << v
|
589
|
+
end
|
590
|
+
|
591
|
+
field_values
|
592
|
+
}
|
593
|
+
end
|
594
|
+
|
595
|
+
|
596
|
+
def get_fields(columns, names, values, access_check)
|
597
|
+
fields = {}
|
598
|
+
names.each_with_index do | name, n |
|
599
|
+
value = values[n]
|
600
|
+
|
601
|
+
if value
|
602
|
+
column = columns[name]
|
603
|
+
|
604
|
+
raise ActiveSalesforce::ASFError.new(@logger, "Column not found for #{name} - get_fields!") unless column
|
605
|
+
|
606
|
+
value.gsub!(/''/, "'") if value.is_a? String
|
607
|
+
|
608
|
+
include_field = ((not value.empty?) and column.send(access_check))
|
609
|
+
|
610
|
+
if (include_field)
|
611
|
+
case column.type
|
612
|
+
when :date
|
613
|
+
value = Time.parse(value + "Z").utc.strftime("%Y-%m-%d")
|
614
|
+
when :datetime
|
615
|
+
value = Time.parse(value + "Z").utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
616
|
+
end
|
617
|
+
|
618
|
+
fields[column.api_name] = value
|
619
|
+
end
|
620
|
+
end
|
621
|
+
end
|
622
|
+
|
623
|
+
fields
|
624
|
+
end
|
625
|
+
|
626
|
+
def get_null_fields(columns, names, values, access_check)
|
627
|
+
fields = {}
|
628
|
+
names.each_with_index do | name, n |
|
629
|
+
value = values[n]
|
630
|
+
|
631
|
+
if !value
|
632
|
+
column = columns[name]
|
633
|
+
fields[column.api_name] = nil if column.send(access_check) && column.api_name.casecmp("ownerid") != 0
|
634
|
+
end
|
635
|
+
end
|
636
|
+
|
637
|
+
fields
|
638
|
+
end
|
639
|
+
|
640
|
+
# Removed no valid SQL modifier, right now, only LIMIT is allowed.
|
641
|
+
# To use a custom SQL, it is better to interface with RForce, see
|
642
|
+
# Salesforce::SfBase query_by_sql(sql) method.
|
643
|
+
def extract_sql_modifier(soql, modifier)
|
644
|
+
value = soql.match(/\s+#{modifier}\s+(\d+)/mi)
|
645
|
+
if value
|
646
|
+
value = value[1].to_i
|
647
|
+
if !(modifier.upcase == "LIMIT")
|
648
|
+
# If it is not the keyword - LIMIT, remove it from the SOQL
|
649
|
+
soql.sub!(/\s+#{modifier}\s+\d+/mi, "")
|
650
|
+
else
|
651
|
+
# If it is the keyword - LIMIT, do not remove it from the SOQL
|
652
|
+
end
|
653
|
+
# SOQL now supports LIMIT clause. If the user is not an app admin &
|
654
|
+
# queries Newsfeed or EntitySubscription & without q limit (e.g. > 1000),
|
655
|
+
# it would cause MQL_FORMED_QUERY exception:
|
656
|
+
# However, OFFSET is still not supported by SOQL.
|
657
|
+
# ***NOTE: needs to change it in the gem to make it effective.
|
658
|
+
end
|
659
|
+
|
660
|
+
value
|
661
|
+
end
|
662
|
+
|
663
|
+
# A clever contraption to construct the key to the object array which is returned
|
664
|
+
# by Rforce from SOAP result.
|
665
|
+
def get_result(response, method)
|
666
|
+
responseName = (method.to_s + "Response").to_sym
|
667
|
+
finalResponse = response[responseName]
|
668
|
+
|
669
|
+
raise ActiveSalesforce::ASFError.new(@logger, response[:Fault][:faultstring], response.fault) unless finalResponse
|
670
|
+
|
671
|
+
result = finalResponse[:result]
|
672
|
+
end
|
673
|
+
|
674
|
+
|
675
|
+
def check_result(result)
|
676
|
+
result = [ result ] unless result.is_a?(Array)
|
677
|
+
|
678
|
+
result.each do |r|
|
679
|
+
raise ActiveSalesforce::ASFError.new(@logger, r[:errors], r[:errors][:message]) unless r[:success] == "true"
|
680
|
+
end
|
681
|
+
|
682
|
+
result
|
683
|
+
end
|
684
|
+
|
685
|
+
|
686
|
+
# A clever contract to get meta-attribute associated with a Salesforce Object.
|
687
|
+
# by Rforce from SOAP result. e.g.
|
688
|
+
# <b>pp user.connection.get_entity_def("AccountFeed") </b>
|
689
|
+
# <ActiveSalesforce::EntityDefinition:0x1030eee40
|
690
|
+
# @api_name_to_column=
|
691
|
+
# {"CreatedDate"=> <ActiveRecord::ConnectionAdapters::SalesforceColumn:0x1030f2c20
|
692
|
+
# @api_name="CreatedDate",
|
693
|
+
# @createable=false,
|
694
|
+
# .....
|
695
|
+
def get_entity_def(entity_name)
|
696
|
+
cached_entity_def = @entity_def_map[entity_name]
|
697
|
+
|
698
|
+
if cached_entity_def
|
699
|
+
# Check for the loss of asf AR setup
|
700
|
+
entity_klass = class_from_entity_name(entity_name)
|
701
|
+
|
702
|
+
configure_active_record(cached_entity_def) unless entity_klass.respond_to?(:asf_augmented?)
|
703
|
+
|
704
|
+
return cached_entity_def
|
705
|
+
end
|
706
|
+
|
707
|
+
cached_columns = []
|
708
|
+
cached_relationships = []
|
709
|
+
|
710
|
+
begin
|
711
|
+
metadata = get_result(@connection.describeSObject(:sObjectType => entity_name), :describeSObject)
|
712
|
+
custom = false
|
713
|
+
rescue ActiveSalesforce::ASFError
|
714
|
+
# Fallback and see if we can find a custom object with this name
|
715
|
+
debug(" Unable to find medata for '#{entity_name}', falling back to custom object name #{entity_name + "__c"}")
|
716
|
+
|
717
|
+
metadata = get_result(@connection.describeSObject(:sObjectType => entity_name + "__c"), :describeSObject)
|
718
|
+
custom = true
|
719
|
+
end
|
720
|
+
|
721
|
+
metadata[:fields].each do |field|
|
722
|
+
column = SalesforceColumn.new(field)
|
723
|
+
cached_columns << column
|
724
|
+
|
725
|
+
cached_relationships << SalesforceRelationship.new(field, column) if field[:type] =~ /reference/mi
|
726
|
+
end
|
727
|
+
|
728
|
+
relationships = metadata[:childRelationships]
|
729
|
+
if relationships
|
730
|
+
relationships = [ relationships ] unless relationships.is_a? Array
|
731
|
+
|
732
|
+
relationships.each do |relationship|
|
733
|
+
if relationship[:cascadeDelete] == "true"
|
734
|
+
r = SalesforceRelationship.new(relationship)
|
735
|
+
cached_relationships << r
|
736
|
+
end
|
737
|
+
end
|
738
|
+
end
|
739
|
+
|
740
|
+
key_prefix = metadata[:keyPrefix]
|
741
|
+
|
742
|
+
entity_def = ActiveSalesforce::EntityDefinition.new(self, entity_name, entity_klass,
|
743
|
+
cached_columns, cached_relationships, custom, key_prefix)
|
744
|
+
|
745
|
+
@entity_def_map[entity_name] = entity_def
|
746
|
+
@keyprefix_to_entity_def_map[key_prefix] = entity_def
|
747
|
+
|
748
|
+
configure_active_record(entity_def)
|
749
|
+
|
750
|
+
entity_def
|
751
|
+
end
|
752
|
+
|
753
|
+
|
754
|
+
def configure_active_record(entity_def)
|
755
|
+
entity_name = entity_def.name
|
756
|
+
klass = class_from_entity_name(entity_name)
|
757
|
+
|
758
|
+
class << klass
|
759
|
+
def asf_augmented?
|
760
|
+
true
|
761
|
+
end
|
762
|
+
end
|
763
|
+
|
764
|
+
# Add support for SID-based authentication
|
765
|
+
ActiveSalesforce::SessionIDAuthenticationFilter.register(klass)
|
766
|
+
|
767
|
+
klass.set_inheritance_column nil unless entity_def.custom?
|
768
|
+
klass.set_primary_key "id"
|
769
|
+
|
770
|
+
# Create relationships for any reference field
|
771
|
+
entity_def.relationships.each do |relationship|
|
772
|
+
referenceName = relationship.name
|
773
|
+
unless self.respond_to? referenceName.to_sym or relationship.reference_to == "Profile"
|
774
|
+
reference_to = relationship.reference_to
|
775
|
+
one_to_many = relationship.one_to_many
|
776
|
+
foreign_key = relationship.foreign_key
|
777
|
+
|
778
|
+
# DCHASMAN TODO Figure out how to handle polymorphic refs (e.g. Note.parent can refer to
|
779
|
+
# Account, Contact, Opportunity, Contract, Asset, Product2, <CustomObject1> ... <CustomObject(n)>
|
780
|
+
if reference_to.is_a? Array
|
781
|
+
debug(" Skipping unsupported polymophic one-to-#{one_to_many ? 'many' : 'one' } relationship '#{referenceName}' from #{klass} to [#{relationship.reference_to.join(', ')}] using #{foreign_key}")
|
782
|
+
next
|
783
|
+
end
|
784
|
+
|
785
|
+
# Handle references to custom objects
|
786
|
+
reference_to = reference_to.chomp("__c").camelize if reference_to.match(/__c$/)
|
787
|
+
|
788
|
+
begin
|
789
|
+
referenced_klass = class_from_entity_name(reference_to)
|
790
|
+
rescue NameError => e
|
791
|
+
# Automatically create a least a stub for the referenced entity
|
792
|
+
debug(" Creating ActiveRecord stub for the referenced entity '#{reference_to}'")
|
793
|
+
|
794
|
+
referenced_klass = klass.class_eval("Salesforce::#{reference_to} = Class.new(ActiveRecord::Base)")
|
795
|
+
referenced_klass.instance_variable_set("@asf_connection", klass.connection)
|
796
|
+
|
797
|
+
# Automatically inherit the connection from the referencee
|
798
|
+
def referenced_klass.connection
|
799
|
+
@asf_connection
|
800
|
+
end
|
801
|
+
end
|
802
|
+
|
803
|
+
if referenced_klass
|
804
|
+
if one_to_many
|
805
|
+
assoc_name = reference_to.underscore.pluralize.to_sym
|
806
|
+
klass.has_many assoc_name, :class_name => referenced_klass.name, :foreign_key => foreign_key
|
807
|
+
else
|
808
|
+
assoc_name = reference_to.underscore.singularize.to_sym
|
809
|
+
klass.belongs_to assoc_name, :class_name => referenced_klass.name, :foreign_key => foreign_key
|
810
|
+
end
|
811
|
+
|
812
|
+
debug(" Created one-to-#{one_to_many ? 'many' : 'one' } relationship '#{referenceName}' from #{klass} to #{referenced_klass} using #{foreign_key}")
|
813
|
+
end
|
814
|
+
end
|
815
|
+
end
|
816
|
+
|
817
|
+
end
|
818
|
+
|
819
|
+
|
820
|
+
# Return column names given a table_name, usage:
|
821
|
+
# >> pp user.connection.columns('account_feed') or ('AccountFeed')
|
822
|
+
def columns(table_name, name = nil)
|
823
|
+
table_name, columns, entity_def = lookup(table_name)
|
824
|
+
entity_def.columns
|
825
|
+
end
|
826
|
+
|
827
|
+
# Given a entity and show column names,
|
828
|
+
# >>pp user.connection.class_from_entity_name("AccountFeed")
|
829
|
+
def class_from_entity_name(entity_name)
|
830
|
+
entity_klass = @class_to_entity_map[entity_name.upcase]
|
831
|
+
debug("Found matching class '#{entity_klass}' for entity '#{entity_name}'") if entity_klass
|
832
|
+
|
833
|
+
# Constantize entities under the Salesforce namespace.
|
834
|
+
entity_klass = ("Salesforce::" + entity_name).constantize unless entity_klass
|
835
|
+
|
836
|
+
entity_klass
|
837
|
+
end
|
838
|
+
|
839
|
+
# Create a blank Salesforce object
|
840
|
+
def create_sobject(entity_name, id, fields, null_fields = [])
|
841
|
+
sobj = []
|
842
|
+
|
843
|
+
sobj << 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << entity_name
|
844
|
+
sobj << 'Id { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << id if id
|
845
|
+
|
846
|
+
# add any changed fields
|
847
|
+
fields.each do | name, value |
|
848
|
+
sobj << name.to_sym << value if value
|
849
|
+
end
|
850
|
+
|
851
|
+
# add null fields
|
852
|
+
null_fields.each do | name, value |
|
853
|
+
sobj << 'fieldsToNull { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << name
|
854
|
+
end
|
855
|
+
|
856
|
+
[ :sObjects, sobj ]
|
857
|
+
end
|
858
|
+
|
859
|
+
# Returns column names associated with a Salesforce Object
|
860
|
+
def column_names(table_name)
|
861
|
+
columns(table_name).map { |column| column.name }
|
862
|
+
end
|
863
|
+
|
864
|
+
|
865
|
+
def lookup(raw_table_name)
|
866
|
+
table_name = raw_table_name.singularize
|
867
|
+
|
868
|
+
# See if a table name to AR class mapping was registered
|
869
|
+
klass = @class_to_entity_map[table_name.upcase]
|
870
|
+
|
871
|
+
entity_name = klass ? raw_table_name : table_name.camelize
|
872
|
+
entity_def = get_entity_def(entity_name)
|
873
|
+
|
874
|
+
[table_name, entity_def.columns, entity_def]
|
875
|
+
end
|
876
|
+
|
877
|
+
|
878
|
+
def debug(msg)
|
879
|
+
@logger.debug(msg) if @logger
|
880
|
+
end
|
881
|
+
|
882
|
+
protected
|
883
|
+
|
884
|
+
# For Silent-e, added 'tables' method to solve ARel problem
|
885
|
+
# fix single-quote escape sequence for WHERE condition expressions. Salesforce enforces a backspace on SELECTs
|
886
|
+
# NOTE: this method is only used for SELECT queries. INSERT/UPDATE queries are smart enough to use the primary
|
887
|
+
# key for their WHERE statements, or so I've found.
|
888
|
+
def fix_single_quote_in_where(sql)
|
889
|
+
where_match = sql.match(/WHERE\s*\((.*)\)/mi)
|
890
|
+
|
891
|
+
return sql unless where_match
|
892
|
+
where_conditions = where_match[1]
|
893
|
+
|
894
|
+
# debug("where_conditions: #{where_conditions}")
|
895
|
+
where_conditions.gsub!(/''/, "\\\\'")
|
896
|
+
|
897
|
+
# debug("updated where_conditions: #{where_conditions.gsub(/''/, "\\\\'")}")
|
898
|
+
|
899
|
+
sql = "#{where_match.pre_match}WHERE (#{where_conditions})#{where_match.post_match}"
|
900
|
+
end
|
901
|
+
|
902
|
+
|
903
|
+
def queue_command(command)
|
904
|
+
# If @command_boxcar is not nil, then this is a transaction
|
905
|
+
# and commands should be queued in the boxcar
|
906
|
+
if @command_boxcar
|
907
|
+
@command_boxcar << command
|
908
|
+
|
909
|
+
# If a command is not executed within a transaction, it should
|
910
|
+
# be executed immediately
|
911
|
+
else
|
912
|
+
send_commands([command])
|
913
|
+
end
|
914
|
+
end
|
915
|
+
|
916
|
+
end
|
917
|
+
|
918
|
+
end
|
919
|
+
end
|