martinemde-dm-salesforce-adapter 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +20 -0
- data/README.markdown +121 -0
- data/Rakefile +16 -0
- data/lib/dm-salesforce-adapter.rb +17 -0
- data/lib/dm-salesforce-adapter/adapter.rb +193 -0
- data/lib/dm-salesforce-adapter/connection.rb +148 -0
- data/lib/dm-salesforce-adapter/connection/errors.rb +56 -0
- data/lib/dm-salesforce-adapter/property.rb +11 -0
- data/lib/dm-salesforce-adapter/property/boolean.rb +23 -0
- data/lib/dm-salesforce-adapter/property/serial.rb +20 -0
- data/lib/dm-salesforce-adapter/resource.rb +12 -0
- data/lib/dm-salesforce-adapter/soap_wrapper.rb +85 -0
- data/lib/dm-salesforce-adapter/sql.rb +103 -0
- data/lib/do_salesforce.rb +5 -0
- data/lib/do_salesforce/command.rb +17 -0
- data/lib/do_salesforce/connection.rb +145 -0
- data/lib/do_salesforce/error.rb +53 -0
- data/lib/do_salesforce/errors.rb +52 -0
- data/lib/do_salesforce/reader.rb +49 -0
- metadata +186 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Yehuda Katz
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
dm-salesforce-adapter
|
2
|
+
=====================
|
3
|
+
|
4
|
+
This gem provides a Salesforce Adapter under DataMapper 1.0.x. A version of
|
5
|
+
this adapter supporting DM 0.10.x exists under the name "dm-salesforce". Older
|
6
|
+
versions of DM (0.9.x) are no longer supported.
|
7
|
+
|
8
|
+
What it looks like
|
9
|
+
==================
|
10
|
+
|
11
|
+
class Account
|
12
|
+
include DataMapper::Adapters::SalesforceAdapter::Resource
|
13
|
+
|
14
|
+
def self.default_repository_name
|
15
|
+
:salesforce
|
16
|
+
end
|
17
|
+
|
18
|
+
property :id, Serial
|
19
|
+
property :name, String
|
20
|
+
property :description, String
|
21
|
+
property :fax, String
|
22
|
+
property :phone, String
|
23
|
+
property :type, String
|
24
|
+
property :website, String
|
25
|
+
property :is_awesome, Boolean
|
26
|
+
|
27
|
+
has 0..n, :contacts
|
28
|
+
end
|
29
|
+
|
30
|
+
class Contact
|
31
|
+
include DataMapper::Adapters::SalesforceAdapter::Resource
|
32
|
+
|
33
|
+
def self.default_repository_name
|
34
|
+
:salesforce
|
35
|
+
end
|
36
|
+
|
37
|
+
property :id, Serial
|
38
|
+
property :first_name, String
|
39
|
+
property :last_name, String
|
40
|
+
property :email, String
|
41
|
+
|
42
|
+
belongs_to :account
|
43
|
+
end
|
44
|
+
|
45
|
+
DataMapper.setup(:salesforce, {:adapter => 'salesforce',
|
46
|
+
:username => 'salesforceuser@mydomain.com',
|
47
|
+
:password => 'skateboardsf938915c9cdc36ff5498881b',
|
48
|
+
:path => '/path/to/wsdl.xml',
|
49
|
+
:host => ''})
|
50
|
+
|
51
|
+
account = Account.first
|
52
|
+
account.is_awesome = true
|
53
|
+
account.save
|
54
|
+
|
55
|
+
See [the fixtures](http://github.com/cloudcrowd/dm-salesforce-adapter/tree/master/spec/fixtures) for more examples.
|
56
|
+
|
57
|
+
How it works
|
58
|
+
============
|
59
|
+
|
60
|
+
Salesforce provides an XML-based WSDL definition of an existing schema/object
|
61
|
+
model for download. dm-salesforce-adapter uses this WSDL to auto-generate a
|
62
|
+
SOAP-based Ruby driver and classes, which is then used to implement a basic,
|
63
|
+
low-level DataMapper Adapter.
|
64
|
+
|
65
|
+
Upon first access, the driver and classes are cached locally on disk in one of
|
66
|
+
the following locations (in order of precedence):
|
67
|
+
|
68
|
+
* In `apidir`, defined in `database.yml` (see included database.yml-example)
|
69
|
+
* In `ENV['SALESFORCE_DIR']`
|
70
|
+
* In `ENV['HOME']/.salesforce/`
|
71
|
+
|
72
|
+
Getting set up
|
73
|
+
==============
|
74
|
+
|
75
|
+
1. Obtain a working salesforce.com account
|
76
|
+
|
77
|
+
2. Get a valid security token (if you don't already have one)
|
78
|
+
* Login to `https://login.salesforce.com`
|
79
|
+
* [Click "Setup"][setup]
|
80
|
+
* [Click "Personal Setup" / "My Personal Information" / "Reset My Security Token"][gettoken]
|
81
|
+
* This will send a message to your account's email address with an "API key"
|
82
|
+
(looks like a 24 character token)
|
83
|
+
|
84
|
+
3. Get the Enterprise WSDL for your object model
|
85
|
+
* Login to `https://login.salesforce.com`
|
86
|
+
* [Click "Setup"][setup]
|
87
|
+
* [Click "App Setup" / "Develop" / "API"][getwsdl]
|
88
|
+
* Click "Generate Enterprise WSDL", then click the "Generate" button
|
89
|
+
* Save that to an .xml file somewhere (path/extension doesn't matter - you specify it
|
90
|
+
in database.yml / DataMapper.setup)
|
91
|
+
|
92
|
+
4. Copy and modify config/example.rb to use your info
|
93
|
+
* The :password field is the concatenation of your login password and the API key
|
94
|
+
* If your password is 'skateboards' and API key is 'f938915c9cdc36ff5498881b', then
|
95
|
+
the :password field you specify to DataMapper.setup should be
|
96
|
+
'skateboardsf938915c9cdc36ff5498881b'
|
97
|
+
|
98
|
+
Run 'ruby example.rb' and you should have access to the Account and Contact models (schema
|
99
|
+
differences withstanding).
|
100
|
+
|
101
|
+
**Don't forget to:**
|
102
|
+
|
103
|
+
* Retrieve a new copy of your WSDL anytime you make changes to your Salesforce schema
|
104
|
+
* Wipe the auto-generated SOAP classes anytime you update your WSDL
|
105
|
+
|
106
|
+
|
107
|
+
Special Thanks to those who helped
|
108
|
+
==================================================
|
109
|
+
* Yehuda Katz
|
110
|
+
* Corey Donohoe
|
111
|
+
* Tim Carey-Smith
|
112
|
+
* Andy Delcambre
|
113
|
+
* Ben Burkert
|
114
|
+
* Larry Diehl
|
115
|
+
* Jordan Ritter
|
116
|
+
* Martin Emde
|
117
|
+
* Jason Snell
|
118
|
+
|
119
|
+
[setup]: http://img.skitch.com/20090204-gaxdfxbi1emfita5dax48ids4m.jpg "Click on Setup"
|
120
|
+
[getwsdl]: http://img.skitch.com/20090204-nhurnuxwf5g3ufnjk2xkfjc5n4.jpg "Expand and Save"
|
121
|
+
[gettoken]: http://img.skitch.com/20090204-mnt182ce7bc4seecqbrjjxjbef.jpg "You can reset your token here"
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
RSpec::Core::RakeTask.new do |t|
|
6
|
+
t.rspec_opts = %w[--color]
|
7
|
+
t.pattern = 'spec/**/*_spec.rb'
|
8
|
+
end
|
9
|
+
task :default => :spec
|
10
|
+
|
11
|
+
RSpec::Core::RakeTask.new(:rcov) do |t|
|
12
|
+
t.rspec_opts = %w[--color]
|
13
|
+
t.pattern = 'spec/**/*_spec.rb'
|
14
|
+
t.rcov = true
|
15
|
+
t.rcov_opts = %w[--exclude spec/,gems/,Library/,.bundle]
|
16
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'dm-do-adapter'
|
2
|
+
|
3
|
+
module DataObjects
|
4
|
+
module Salesforce
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
module DataMapper
|
9
|
+
module Adapters
|
10
|
+
class SalesforceAdapter < DataObjectsAdapter
|
11
|
+
end
|
12
|
+
|
13
|
+
const_added(:SalesforceAdapter)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'dm-salesforce-adapter/adapter'
|
@@ -0,0 +1,193 @@
|
|
1
|
+
require 'dm-do-adapter'
|
2
|
+
require 'dm-core'
|
3
|
+
require 'dm-types'
|
4
|
+
require 'dm-validations'
|
5
|
+
|
6
|
+
require 'soap/wsdlDriver'
|
7
|
+
require 'soap/header/simplehandler'
|
8
|
+
require 'rexml/element'
|
9
|
+
|
10
|
+
require 'dm-salesforce-adapter/connection'
|
11
|
+
require 'dm-salesforce-adapter/sql'
|
12
|
+
require 'dm-salesforce-adapter/property'
|
13
|
+
require 'dm-salesforce-adapter/resource'
|
14
|
+
require 'dm-salesforce-adapter/soap_wrapper'
|
15
|
+
|
16
|
+
module DataMapper
|
17
|
+
module Adapters
|
18
|
+
class SalesforceAdapter < DataObjectsAdapter
|
19
|
+
|
20
|
+
include DataMapper::Adapters::SalesforceAdapter::SQL
|
21
|
+
|
22
|
+
def initialize(name, uri_or_options)
|
23
|
+
super
|
24
|
+
@resource_naming_convention = proc do |value|
|
25
|
+
klass = DataMapper::Inflector.constantize(value)
|
26
|
+
if klass.respond_to?(:salesforce_class)
|
27
|
+
klass.salesforce_class
|
28
|
+
else
|
29
|
+
value.split("::").last
|
30
|
+
end
|
31
|
+
end
|
32
|
+
@field_naming_convention = proc do |property|
|
33
|
+
connection.field_name_for(property.model.storage_name(name), property.name.to_s)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def schema_name
|
38
|
+
'salesforce'
|
39
|
+
end
|
40
|
+
|
41
|
+
def connection
|
42
|
+
@connection ||= Connection.new(options["username"], options["password"], options["path"], options["apidir"])
|
43
|
+
end
|
44
|
+
|
45
|
+
# FIXME: DM Adapters customarily throw exceptions when they experience errors,
|
46
|
+
# otherwise failed operations (e.g. Resource#save) still return true and thus
|
47
|
+
# confuse the caller.
|
48
|
+
#
|
49
|
+
# Someone needs to make a decision about legacy support and the consequences
|
50
|
+
# of changing the behaviour from broken-but-typical to
|
51
|
+
# correct-but-maybe-unexpected. Maybe a config file option about whether to
|
52
|
+
# raise exceptions or for the user to always check Model#valid? +
|
53
|
+
# Model#salesforce_errors?
|
54
|
+
#
|
55
|
+
# Needs to be applied to all CRUD operations.
|
56
|
+
def create(resources)
|
57
|
+
arr = resources.map do |resource|
|
58
|
+
make_salesforce_obj(resource, resource.dirty_attributes)
|
59
|
+
end
|
60
|
+
|
61
|
+
result = connection.create(arr)
|
62
|
+
result.each_with_index do |record, i|
|
63
|
+
resource = resources[i]
|
64
|
+
if id_field = resource.class.key.find {|p| p.serial?}
|
65
|
+
normalized_value = normalize_id_value(resource.class, id_field, record.id)
|
66
|
+
id_field.set!(resource, normalized_value)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
result.size
|
71
|
+
|
72
|
+
rescue Connection::SOAPError => e
|
73
|
+
handle_server_outage(e)
|
74
|
+
end
|
75
|
+
|
76
|
+
def update(attributes, collection)
|
77
|
+
query = collection.query
|
78
|
+
arr = collection.map { |obj| make_salesforce_obj(query, attributes) }
|
79
|
+
|
80
|
+
connection.update(arr).size
|
81
|
+
|
82
|
+
rescue Connection::SOAPError => e
|
83
|
+
handle_server_outage(e)
|
84
|
+
end
|
85
|
+
|
86
|
+
def delete(collection)
|
87
|
+
query = collection.query
|
88
|
+
keys = collection.map { |r| r.key }.flatten.uniq
|
89
|
+
|
90
|
+
connection.delete(keys).size
|
91
|
+
|
92
|
+
rescue Connection::SOAPError => e
|
93
|
+
handle_server_outage(e)
|
94
|
+
end
|
95
|
+
|
96
|
+
def handle_server_outage(error)
|
97
|
+
if error.server_unavailable?
|
98
|
+
raise Connection::ServerUnavailable, "The salesforce server is currently unavailable"
|
99
|
+
else
|
100
|
+
raise error
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Reading responses back from SELECTS:
|
105
|
+
# In the typical case, response.size reflects the # of records returned.
|
106
|
+
# In the aggregation case, response.size reflects the count.
|
107
|
+
#
|
108
|
+
# Interpretation of this field requires knowledge of whether we are expecting
|
109
|
+
# an aggregate result, thus the response from execute_select() is processed
|
110
|
+
# differently depending on invocation (read vs. aggregate).
|
111
|
+
def read(query)
|
112
|
+
properties = query.fields
|
113
|
+
repository = query.repository
|
114
|
+
|
115
|
+
response = execute_select(query)
|
116
|
+
return [] unless response.records
|
117
|
+
|
118
|
+
rows = response.records.inject([]) do |results, record|
|
119
|
+
results << properties.inject({}) do |result, property|
|
120
|
+
meth = connection.field_name_for(property.model.storage_name(repository.name), property.field)
|
121
|
+
result[property] = normalize_id_value(query.model, property, record.send(meth))
|
122
|
+
result
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
query.model.load(rows, query)
|
127
|
+
end
|
128
|
+
|
129
|
+
# http://www.salesforce.com/us/developer/docs/api90/Content/sforce_api_calls_soql.htm
|
130
|
+
# SOQL doesn't support anything but count(), so we catch it here and interpret
|
131
|
+
# the result. Requires 'dm-aggregates' to be loaded.
|
132
|
+
def aggregate(query)
|
133
|
+
query.fields.each do |f|
|
134
|
+
unless f.target == :all && f.operator == :count
|
135
|
+
raise ArgumentError, %{Aggregate function #{f.operator} not supported in SOQL}
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
[ execute_select(query).size ]
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
def execute_select(query)
|
144
|
+
repository = query.repository
|
145
|
+
conditions = query.conditions.map {|c| conditions_statement(c, repository)}.compact.join(") AND (")
|
146
|
+
|
147
|
+
fields = query.fields.map do |f|
|
148
|
+
case f
|
149
|
+
when DataMapper::Property
|
150
|
+
f.field
|
151
|
+
when DataMapper::Query::Operator
|
152
|
+
%{#{f.operator}()}
|
153
|
+
else
|
154
|
+
raise ArgumentError, "Unknown query field #{f.class}: #{f.inspect}"
|
155
|
+
end
|
156
|
+
end.join(", ")
|
157
|
+
|
158
|
+
sql = "SELECT #{fields} from #{query.model.storage_name(repository.name)}"
|
159
|
+
sql << " WHERE (#{conditions})" unless conditions.empty?
|
160
|
+
sql << " ORDER BY #{order(query.order[0])}" unless query.order.nil? || query.order.empty?
|
161
|
+
sql << " LIMIT #{query.limit}" if query.limit
|
162
|
+
|
163
|
+
DataMapper.logger.debug sql if DataMapper.logger
|
164
|
+
|
165
|
+
connection.query(sql)
|
166
|
+
end
|
167
|
+
|
168
|
+
def make_salesforce_obj(from, with_attrs)
|
169
|
+
klass_name = from.model.storage_name(from.repository.name)
|
170
|
+
values = {}
|
171
|
+
|
172
|
+
# FIXME: query.conditions is potentially a tree now
|
173
|
+
if from.is_a?(::DataMapper::Query)
|
174
|
+
key_value = from.conditions.find { |c| c.subject.key? }.value
|
175
|
+
values["id"] = normalize_id_value(from.model, from.model.properties[:id], key_value)
|
176
|
+
end
|
177
|
+
|
178
|
+
with_attrs.each do |property, value|
|
179
|
+
next if property.serial? || property.key? and value.nil?
|
180
|
+
values[property.field] = normalize_id_value(from.model, property, value)
|
181
|
+
end
|
182
|
+
|
183
|
+
connection.make_object(klass_name, values)
|
184
|
+
end
|
185
|
+
|
186
|
+
def normalize_id_value(klass, property, value)
|
187
|
+
return nil unless value
|
188
|
+
properties = Array(klass.send(:salesforce_id_properties)).map { |p| p.to_sym } rescue []
|
189
|
+
return properties.include?(property.name) ? value[0..14] : value
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'dm-salesforce-adapter/connection/errors'
|
2
|
+
|
3
|
+
module DataMapper
|
4
|
+
module Adapters
|
5
|
+
class SalesforceAdapter < DataObjectsAdapter
|
6
|
+
class Connection
|
7
|
+
include Errors
|
8
|
+
|
9
|
+
class HeaderHandler < SOAP::Header::SimpleHandler
|
10
|
+
def initialize(tag, value)
|
11
|
+
super(XSD::QName.new('urn:enterprise.soap.sforce.com', tag))
|
12
|
+
@tag = tag
|
13
|
+
@value = value
|
14
|
+
end
|
15
|
+
def on_simple_outbound
|
16
|
+
@value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(username, password, wsdl_path, api_dir, organization_id = nil)
|
21
|
+
@wrapper = SoapWrapper.new("SalesforceAPI", "Soap", wsdl_path, api_dir)
|
22
|
+
@username, @password, @organization_id = URI.unescape(username), password, organization_id
|
23
|
+
login
|
24
|
+
end
|
25
|
+
attr_reader :user_id, :user_details
|
26
|
+
|
27
|
+
def wsdl_path
|
28
|
+
@wrapper.wsdl_path
|
29
|
+
end
|
30
|
+
|
31
|
+
def api_dir
|
32
|
+
@wrapper.api_dir
|
33
|
+
end
|
34
|
+
|
35
|
+
def organization_id
|
36
|
+
@user_details && @user_details.organizationId
|
37
|
+
end
|
38
|
+
|
39
|
+
def make_object(klass_name, values)
|
40
|
+
obj = SalesforceAPI.const_get(klass_name).new
|
41
|
+
values.each do |property, value|
|
42
|
+
field = field_name_for(klass_name, property)
|
43
|
+
if value.nil? or value == ""
|
44
|
+
obj.fieldsToNull.push(field)
|
45
|
+
else
|
46
|
+
obj.send("#{field}=", value)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
obj
|
50
|
+
end
|
51
|
+
|
52
|
+
def field_name_for(klass_name, column)
|
53
|
+
klass = SalesforceAPI.const_get(klass_name)
|
54
|
+
# extlib vs. activesupport
|
55
|
+
fields = [column, DataMapper::Inflector.camelize(column), "#{column}__c".downcase]
|
56
|
+
options = /^(#{fields.join("|")})$/i
|
57
|
+
matches = klass.instance_methods(false).grep(options)
|
58
|
+
if matches.any?
|
59
|
+
matches.first
|
60
|
+
else
|
61
|
+
raise FieldNotFound,
|
62
|
+
"You specified #{column} as a field, but neither #{fields.join(" or ")} exist. " \
|
63
|
+
"Either manually specify the field name with :field, or check to make sure you have " \
|
64
|
+
"provided a correct field name."
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def query(string)
|
69
|
+
with_reconnection do
|
70
|
+
driver.query(:queryString => string).result
|
71
|
+
end
|
72
|
+
rescue SOAP::FaultError => e
|
73
|
+
raise QueryError.new(e.message, [])
|
74
|
+
end
|
75
|
+
|
76
|
+
def create(objects)
|
77
|
+
call_api(:create, CreateError, "creating", objects)
|
78
|
+
end
|
79
|
+
|
80
|
+
def update(objects)
|
81
|
+
call_api(:update, UpdateError, "updating", objects)
|
82
|
+
end
|
83
|
+
|
84
|
+
def delete(keys)
|
85
|
+
call_api(:delete, DeleteError, "deleting", keys)
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def driver
|
91
|
+
@wrapper.driver
|
92
|
+
end
|
93
|
+
|
94
|
+
def login
|
95
|
+
driver
|
96
|
+
if @organization_id
|
97
|
+
driver.headerhandler << HeaderHandler.new("LoginScopeHeader", :organizationId => @organization_id)
|
98
|
+
end
|
99
|
+
|
100
|
+
begin
|
101
|
+
result = driver.login(:username => @username, :password => @password).result
|
102
|
+
rescue SOAP::FaultError => error
|
103
|
+
if error.to_s =~ /INVALID_LOGIN/
|
104
|
+
raise LoginFailed, error.inspect
|
105
|
+
else
|
106
|
+
raise error
|
107
|
+
end
|
108
|
+
end
|
109
|
+
driver.endpoint_url = result.serverUrl
|
110
|
+
driver.headerhandler << HeaderHandler.new("SessionHeader", "sessionId" => result.sessionId)
|
111
|
+
driver.headerhandler << HeaderHandler.new("CallOptions", "client" => "client")
|
112
|
+
@user_id = result.userId
|
113
|
+
@user_details = result.userInfo
|
114
|
+
driver
|
115
|
+
end
|
116
|
+
|
117
|
+
def call_api(method, exception_class, message, args)
|
118
|
+
with_reconnection do
|
119
|
+
result = driver.send(method, args)
|
120
|
+
if result.all? {|r| r.success}
|
121
|
+
result
|
122
|
+
else
|
123
|
+
# TODO: be smarter about exceptions here
|
124
|
+
raise exception_class.new("Got some errors while #{message} Salesforce objects", result)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def with_reconnection(&block)
|
130
|
+
yield
|
131
|
+
rescue SOAP::FaultError => error
|
132
|
+
retry_count ||= 0
|
133
|
+
if error.faultcode.to_s =~ "INVALID_SESSION_ID"
|
134
|
+
DataMapper.logger.debug "Got a invalid session id; reconnecting" if DataMapper.logger
|
135
|
+
@driver = nil
|
136
|
+
login
|
137
|
+
retry_count += 1
|
138
|
+
retry unless retry_count > 5
|
139
|
+
else
|
140
|
+
raise error
|
141
|
+
end
|
142
|
+
|
143
|
+
raise SessionTimeout, "The Salesforce session could not be established"
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Adapters
|
3
|
+
class SalesforceAdapter < DataObjectsAdapter
|
4
|
+
class Connection
|
5
|
+
module Errors
|
6
|
+
class Error < StandardError; end
|
7
|
+
class FieldNotFound < Error; end
|
8
|
+
class LoginFailed < Error; end
|
9
|
+
class SessionTimeout < Error; end
|
10
|
+
class UnknownStatusCode < Error; end
|
11
|
+
class ServerUnavailable < Error; end
|
12
|
+
|
13
|
+
|
14
|
+
class SOAPError < Error
|
15
|
+
def initialize(message, result)
|
16
|
+
@result = result
|
17
|
+
super("#{message}: #{result_message}")
|
18
|
+
end
|
19
|
+
|
20
|
+
def records
|
21
|
+
@result.to_a
|
22
|
+
end
|
23
|
+
|
24
|
+
def failed_records
|
25
|
+
@result.reject {|r| r.success}
|
26
|
+
end
|
27
|
+
|
28
|
+
def successful_records
|
29
|
+
@result.select {|r| r.success}
|
30
|
+
end
|
31
|
+
|
32
|
+
def result_message
|
33
|
+
failed_records.map do |r|
|
34
|
+
message_for_record(r)
|
35
|
+
end.join("; ")
|
36
|
+
end
|
37
|
+
|
38
|
+
def message_for_record(record)
|
39
|
+
record.errors.map {|e| "#{e.statusCode}: #{e.message}"}.join(", ")
|
40
|
+
end
|
41
|
+
|
42
|
+
def server_unavailable?
|
43
|
+
failed_records.any? do |record|
|
44
|
+
record.errors.any? {|e| e.statusCode == "SERVER_UNAVAILABLE"}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
class CreateError < SOAPError; end
|
49
|
+
class QueryError < SOAPError; end
|
50
|
+
class DeleteError < SOAPError; end
|
51
|
+
class UpdateError < SOAPError; end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'dm-salesforce-adapter/property'
|
2
|
+
|
3
|
+
|
4
|
+
module DataMapper
|
5
|
+
module Adapters
|
6
|
+
class SalesforceAdapter < DataObjectsAdapter
|
7
|
+
module Property
|
8
|
+
class Boolean < ::DataMapper::Property::Integer
|
9
|
+
FALSE = 0
|
10
|
+
TRUE = 1
|
11
|
+
|
12
|
+
def load(value)
|
13
|
+
[true, 1, '1', 'true', 'TRUE', TRUE].include?(value) ? true : false
|
14
|
+
end
|
15
|
+
|
16
|
+
def typecast(value)
|
17
|
+
[true, 1, '1', 'true', 'TRUE', TRUE].include?(value) ? TRUE : FALSE
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'dm-salesforce-adapter/property'
|
2
|
+
|
3
|
+
module DataMapper
|
4
|
+
module Adapters
|
5
|
+
class SalesforceAdapter < DataObjectsAdapter
|
6
|
+
module Property
|
7
|
+
class Serial < ::DataMapper::Property::String
|
8
|
+
accept_options :serial
|
9
|
+
serial true
|
10
|
+
|
11
|
+
length 15
|
12
|
+
|
13
|
+
def typecast(value)
|
14
|
+
value.to_str[0..14] unless value.nil? || value.empty?
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Adapters
|
3
|
+
class SalesforceAdapter < DataObjectsAdapter
|
4
|
+
module Resource
|
5
|
+
def self.included(model)
|
6
|
+
model.send :include, DataMapper::Resource
|
7
|
+
model.send :include, DataMapper::Adapters::SalesforceAdapter::Property
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Adapters
|
3
|
+
class SalesforceAdapter < DataObjectsAdapter
|
4
|
+
class SoapWrapper
|
5
|
+
class ClassesFailedToGenerate < StandardError; end
|
6
|
+
|
7
|
+
attr_reader :module_name, :driver_name, :wsdl_path, :api_dir
|
8
|
+
|
9
|
+
def initialize(module_name, driver_name, wsdl_path, api_dir)
|
10
|
+
@module_name, @driver_name, @wsdl_path, @api_dir = module_name, driver_name, File.expand_path(wsdl_path), File.expand_path(api_dir)
|
11
|
+
generate_soap_classes
|
12
|
+
driver
|
13
|
+
end
|
14
|
+
|
15
|
+
def driver
|
16
|
+
@driver ||= Object.const_get(module_name).const_get(driver_name).new
|
17
|
+
end
|
18
|
+
|
19
|
+
=begin
|
20
|
+
# Attempt at a run-time equivalent. Works except for the API responses are
|
21
|
+
# in SOAP objects, not native ruby objects. Haven't figured that one out..
|
22
|
+
def driver
|
23
|
+
return @driver if @driver
|
24
|
+
|
25
|
+
require 'wsdl/soap/wsdl2ruby'
|
26
|
+
|
27
|
+
factory = SOAP::WSDLDriverFactory.new(wsdl_path)
|
28
|
+
class_name_creator = WSDL::SOAP::ClassNameCreator.new
|
29
|
+
|
30
|
+
eval(WSDL::SOAP::ClassDefCreator.new(factory.wsdl, class_name_creator, @module_name).dump, TOPLEVEL_BINDING)
|
31
|
+
eval(WSDL::SOAP::MappingRegistryCreator.new(factory.wsdl, class_name_creator, @module_name).dump, TOPLEVEL_BINDING)
|
32
|
+
|
33
|
+
@driver ||= factory.create_rpc_driver
|
34
|
+
end
|
35
|
+
=end
|
36
|
+
|
37
|
+
def generate_soap_classes
|
38
|
+
unless File.file?(wsdl_path)
|
39
|
+
raise Errno::ENOENT, "Could not find the WSDL at #{wsdl_path}"
|
40
|
+
end
|
41
|
+
|
42
|
+
FileUtils.mkdir_p(wsdl_api_dir)
|
43
|
+
|
44
|
+
generate_files unless files_exist?
|
45
|
+
|
46
|
+
$:.push wsdl_api_dir
|
47
|
+
require "#{module_name}Driver"
|
48
|
+
$:.delete wsdl_api_dir
|
49
|
+
end
|
50
|
+
|
51
|
+
# Good candidate for shipping out into a Rakefile.
|
52
|
+
def generate_files
|
53
|
+
require 'wsdl/soap/wsdl2ruby'
|
54
|
+
|
55
|
+
wsdl2ruby = WSDL::SOAP::WSDL2Ruby.new
|
56
|
+
wsdl2ruby.logger = $LOG if $LOG
|
57
|
+
wsdl2ruby.location = wsdl_path
|
58
|
+
wsdl2ruby.basedir = wsdl_api_dir
|
59
|
+
|
60
|
+
wsdl2ruby.opt.merge!({
|
61
|
+
'classdef' => module_name,
|
62
|
+
'module_path' => module_name,
|
63
|
+
'mapping_registry' => nil,
|
64
|
+
'driver' => nil,
|
65
|
+
'client_skelton' => nil,
|
66
|
+
})
|
67
|
+
|
68
|
+
wsdl2ruby.run
|
69
|
+
|
70
|
+
raise ClassesFailedToGenerate unless files_exist?
|
71
|
+
end
|
72
|
+
|
73
|
+
def files_exist?
|
74
|
+
["#{module_name}.rb", "#{module_name}MappingRegistry.rb", "#{module_name}Driver.rb"].all? do |name|
|
75
|
+
File.exist?("#{wsdl_api_dir}/#{name}")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def wsdl_api_dir
|
80
|
+
"#{api_dir}/#{File.basename(wsdl_path)}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Adapters
|
3
|
+
class SalesforceAdapter < DataObjectsAdapter
|
4
|
+
module SQL
|
5
|
+
def conditions_statement(conditions, repository)
|
6
|
+
case conditions
|
7
|
+
when DataMapper::Query::Conditions::NotOperation then negate_operation(conditions.operand, repository)
|
8
|
+
when DataMapper::Query::Conditions::AbstractOperation then conditions.operands.first # ignores AND/OR grouping for now.
|
9
|
+
when DataMapper::Query::Conditions::AbstractComparison then comparison_statement(conditions, repository)
|
10
|
+
else raise("Unkown condition type #{conditions.class}: #{conditions.inspect}")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def comparison_statement(comparison, repository)
|
15
|
+
subject = comparison.subject
|
16
|
+
value = comparison.value
|
17
|
+
|
18
|
+
if comparison.relationship?
|
19
|
+
return conditions_statement(comparison.foreign_key_mapping, repository)
|
20
|
+
elsif comparison.slug == :in && value.empty?
|
21
|
+
return [] # match everything
|
22
|
+
end
|
23
|
+
|
24
|
+
operator = comparison_operator(comparison)
|
25
|
+
column_name = property_to_column_name(subject, repository)
|
26
|
+
|
27
|
+
"#{column_name} #{operator} #{quote_value(value,subject)}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def comparison_operator(comparison)
|
31
|
+
subject = comparison.subject
|
32
|
+
value = comparison.value
|
33
|
+
|
34
|
+
case comparison.slug
|
35
|
+
when :eql then equality_operator(subject, value)
|
36
|
+
when :in then include_operator(subject, value)
|
37
|
+
when :not then inequality_operator(subject, value)
|
38
|
+
when :regexp then regexp_operator(value)
|
39
|
+
when :like then like_operator(value)
|
40
|
+
when :gt then '>'
|
41
|
+
when :lt then '<'
|
42
|
+
when :gte then '>='
|
43
|
+
when :lte then '<='
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def negate_operation(operand, repository)
|
48
|
+
statement = conditions_statement(operand, repository)
|
49
|
+
statement = "NOT(#{statement})" unless statement.nil?
|
50
|
+
statement
|
51
|
+
end
|
52
|
+
|
53
|
+
def property_to_column_name(prop, repository)
|
54
|
+
case prop
|
55
|
+
when DataMapper::Property
|
56
|
+
prop.field
|
57
|
+
when DataMapper::Query::Path
|
58
|
+
rels = prop.relationships
|
59
|
+
names = rels.map {|r| storage_name(r, repository) }.join(".")
|
60
|
+
"#{names}.#{prop.field}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def storage_name(rel, repository)
|
65
|
+
rel.parent_model.storage_name(repository.name)
|
66
|
+
end
|
67
|
+
|
68
|
+
def order(direction)
|
69
|
+
"#{direction.target.field} #{direction.operator.to_s.upcase}"
|
70
|
+
end
|
71
|
+
|
72
|
+
def equality_operator(property, operand)
|
73
|
+
operand.nil? ? 'IS' : '='
|
74
|
+
end
|
75
|
+
|
76
|
+
def include_operator(property, operand)
|
77
|
+
case operand
|
78
|
+
when Array then 'IN'
|
79
|
+
when Range then 'BETWEEN'
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def like_operator(operand)
|
84
|
+
"LIKE"
|
85
|
+
end
|
86
|
+
|
87
|
+
def quote_value(value, property)
|
88
|
+
if property.is_a? Property::Boolean
|
89
|
+
# True on salesforce needs to be TRUE/FALSE for WHERE clauses but not for inserts.
|
90
|
+
return value == Property::Boolean::TRUE ? 'TRUE' : 'FALSE'
|
91
|
+
end
|
92
|
+
|
93
|
+
case value
|
94
|
+
when Array then "(#{value.map {|v| quote_value(v, property)}.join(", ")})"
|
95
|
+
when NilClass then "NULL"
|
96
|
+
when String then "'#{value.gsub(/'/, "\\'").gsub(/\\/, %{\\\\})}'"
|
97
|
+
else "#{value}"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module DataObjects
|
2
|
+
module Salesforce
|
3
|
+
class Command
|
4
|
+
# Execute this command and return no dataset
|
5
|
+
def execute_non_query(*args)
|
6
|
+
end
|
7
|
+
|
8
|
+
# Execute this command and return a DataObjects::Reader for a dataset
|
9
|
+
def execute_reader(*args)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Assign an array of types for the columns to be returned by this command
|
13
|
+
def set_types(column_types)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'do_salesforce/error'
|
2
|
+
|
3
|
+
module DataObject
|
4
|
+
module Salesforce
|
5
|
+
class Connection
|
6
|
+
|
7
|
+
class HeaderHandler < SOAP::Header::SimpleHandler
|
8
|
+
def initialize(tag, value)
|
9
|
+
super(XSD::QName.new('urn:enterprise.soap.sforce.com', tag))
|
10
|
+
@tag = tag
|
11
|
+
@value = value
|
12
|
+
end
|
13
|
+
def on_simple_outbound
|
14
|
+
@value
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(username, password, wsdl_path, api_dir, organization_id = nil)
|
19
|
+
@wrapper = SoapWrapper.new("SalesforceAPI", "Soap", wsdl_path, api_dir)
|
20
|
+
@username, @password, @organization_id = URI.unescape(username), password, organization_id
|
21
|
+
login
|
22
|
+
end
|
23
|
+
attr_reader :user_id, :user_details
|
24
|
+
|
25
|
+
def wsdl_path
|
26
|
+
@wrapper.wsdl_path
|
27
|
+
end
|
28
|
+
|
29
|
+
def api_dir
|
30
|
+
@wrapper.api_dir
|
31
|
+
end
|
32
|
+
|
33
|
+
def organization_id
|
34
|
+
@user_details && @user_details.organizationId
|
35
|
+
end
|
36
|
+
|
37
|
+
def make_object(klass_name, values)
|
38
|
+
obj = SalesforceAPI.const_get(klass_name).new
|
39
|
+
values.each do |property, value|
|
40
|
+
field = field_name_for(klass_name, property)
|
41
|
+
if value.nil? or value == ""
|
42
|
+
obj.fieldsToNull.push(field)
|
43
|
+
else
|
44
|
+
obj.send("#{field}=", value)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
obj
|
48
|
+
end
|
49
|
+
|
50
|
+
def field_name_for(klass_name, column)
|
51
|
+
klass = SalesforceAPI.const_get(klass_name)
|
52
|
+
fields = [column, DataMapper::Inflector.camelize(column), "#{column}__c".downcase]
|
53
|
+
options = /^(#{fields.join("|")})$/i
|
54
|
+
matches = klass.instance_methods(false).grep(options)
|
55
|
+
if matches.any?
|
56
|
+
matches.first
|
57
|
+
else
|
58
|
+
raise FieldNotFound,
|
59
|
+
"You specified #{column} as a field, but neither #{fields.join(" or ")} exist. " \
|
60
|
+
"Either manually specify the field name with :field, or check to make sure you have " \
|
61
|
+
"provided a correct field name."
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def query(string)
|
66
|
+
with_reconnection do
|
67
|
+
driver.query(:queryString => string).result
|
68
|
+
end
|
69
|
+
rescue SOAP::FaultError => e
|
70
|
+
raise QueryError.new(e.message, [])
|
71
|
+
end
|
72
|
+
|
73
|
+
def create(objects)
|
74
|
+
debugger
|
75
|
+
call_api(:create, CreateError, "creating", objects)
|
76
|
+
end
|
77
|
+
|
78
|
+
def update(objects)
|
79
|
+
call_api(:update, UpdateError, "updating", objects)
|
80
|
+
end
|
81
|
+
|
82
|
+
def delete(keys)
|
83
|
+
call_api(:delete, DeleteError, "deleting", keys)
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def driver
|
89
|
+
@wrapper.driver
|
90
|
+
end
|
91
|
+
|
92
|
+
def login
|
93
|
+
driver
|
94
|
+
if @organization_id
|
95
|
+
driver.headerhandler << HeaderHandler.new("LoginScopeHeader", :organizationId => @organization_id)
|
96
|
+
end
|
97
|
+
|
98
|
+
begin
|
99
|
+
result = driver.login(:username => @username, :password => @password).result
|
100
|
+
rescue SOAP::FaultError => error
|
101
|
+
if error.to_s =~ /INVALID_LOGIN/
|
102
|
+
raise LoginFailed, error.inspect
|
103
|
+
else
|
104
|
+
raise error
|
105
|
+
end
|
106
|
+
end
|
107
|
+
driver.endpoint_url = result.serverUrl
|
108
|
+
driver.headerhandler << HeaderHandler.new("SessionHeader", "sessionId" => result.sessionId)
|
109
|
+
driver.headerhandler << HeaderHandler.new("CallOptions", "client" => "client")
|
110
|
+
@user_id = result.userId
|
111
|
+
@user_details = result.userInfo
|
112
|
+
driver
|
113
|
+
end
|
114
|
+
|
115
|
+
def call_api(method, exception_class, message, args)
|
116
|
+
with_reconnection do
|
117
|
+
result = driver.send(method, args)
|
118
|
+
if result.all? {|r| r.success}
|
119
|
+
result
|
120
|
+
else
|
121
|
+
# TODO: be smarter about exceptions here
|
122
|
+
raise exception_class.new("Got some errors while #{message} Salesforce objects", result)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def with_reconnection(&block)
|
128
|
+
yield
|
129
|
+
rescue SOAP::FaultError => error
|
130
|
+
retry_count ||= 0
|
131
|
+
if error.faultcode.to_s =~ "INVALID_SESSION_ID"
|
132
|
+
DataMapper.logger.debug "Got a invalid session id; reconnecting" if DataMapper.logger
|
133
|
+
@driver = nil
|
134
|
+
login
|
135
|
+
retry_count += 1
|
136
|
+
retry unless retry_count > 5
|
137
|
+
else
|
138
|
+
raise error
|
139
|
+
end
|
140
|
+
|
141
|
+
raise SessionTimeout, "The Salesforce session could not be established"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module DataObjects
|
2
|
+
class Salesforce
|
3
|
+
class Error < StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
class FieldNotFound < Error; end
|
7
|
+
class LoginFailed < Error; end
|
8
|
+
class SessionTimeout < Error; end
|
9
|
+
class UnknownStatusCode < Error; end
|
10
|
+
class ServerUnavailable < Error; end
|
11
|
+
|
12
|
+
|
13
|
+
class SOAPError < Error
|
14
|
+
def initialize(message, result)
|
15
|
+
@result = result
|
16
|
+
super("#{message}: #{result_message}")
|
17
|
+
end
|
18
|
+
|
19
|
+
def records
|
20
|
+
@result.to_a
|
21
|
+
end
|
22
|
+
|
23
|
+
def failed_records
|
24
|
+
@result.reject {|r| r.success}
|
25
|
+
end
|
26
|
+
|
27
|
+
def successful_records
|
28
|
+
@result.select {|r| r.success}
|
29
|
+
end
|
30
|
+
|
31
|
+
def result_message
|
32
|
+
failed_records.map do |r|
|
33
|
+
message_for_record(r)
|
34
|
+
end.join("; ")
|
35
|
+
end
|
36
|
+
|
37
|
+
def message_for_record(record)
|
38
|
+
record.errors.map {|e| "#{e.statusCode}: #{e.message}"}.join(", ")
|
39
|
+
end
|
40
|
+
|
41
|
+
def server_unavailable?
|
42
|
+
failed_records.any? do |record|
|
43
|
+
record.errors.any? {|e| e.statusCode == "SERVER_UNAVAILABLE"}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class CreateError < SOAPError; end
|
49
|
+
class QueryError < SOAPError; end
|
50
|
+
class DeleteError < SOAPError; end
|
51
|
+
class UpdateError < SOAPError; end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module DataObjects
|
2
|
+
class Salesforce
|
3
|
+
class Error < StandardError; end
|
4
|
+
|
5
|
+
class FieldNotFound < Error; end
|
6
|
+
class LoginFailed < Error; end
|
7
|
+
class SessionTimeout < Error; end
|
8
|
+
class UnknownStatusCode < Error; end
|
9
|
+
class ServerUnavailable < Error; end
|
10
|
+
|
11
|
+
|
12
|
+
class SOAPError < Error
|
13
|
+
def initialize(message, result)
|
14
|
+
@result = result
|
15
|
+
super("#{message}: #{result_message}")
|
16
|
+
end
|
17
|
+
|
18
|
+
def records
|
19
|
+
@result.to_a
|
20
|
+
end
|
21
|
+
|
22
|
+
def failed_records
|
23
|
+
@result.reject {|r| r.success}
|
24
|
+
end
|
25
|
+
|
26
|
+
def successful_records
|
27
|
+
@result.select {|r| r.success}
|
28
|
+
end
|
29
|
+
|
30
|
+
def result_message
|
31
|
+
failed_records.map do |r|
|
32
|
+
message_for_record(r)
|
33
|
+
end.join("; ")
|
34
|
+
end
|
35
|
+
|
36
|
+
def message_for_record(record)
|
37
|
+
record.errors.map {|e| "#{e.statusCode}: #{e.message}"}.join(", ")
|
38
|
+
end
|
39
|
+
|
40
|
+
def server_unavailable?
|
41
|
+
failed_records.any? do |record|
|
42
|
+
record.errors.any? {|e| e.statusCode == "SERVER_UNAVAILABLE"}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class CreateError < SOAPError; end
|
48
|
+
class QueryError < SOAPError; end
|
49
|
+
class DeleteError < SOAPError; end
|
50
|
+
class UpdateError < SOAPError; end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module DataObjects
|
2
|
+
module Salesforce
|
3
|
+
class Reader
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
end
|
7
|
+
|
8
|
+
# Return the array of field names
|
9
|
+
def fields
|
10
|
+
raise NotImplementedError.new
|
11
|
+
end
|
12
|
+
|
13
|
+
# Return the array of field values for the current row. Not legal after next! has returned false or before it's been called
|
14
|
+
def values
|
15
|
+
raise NotImplementedError.new
|
16
|
+
end
|
17
|
+
|
18
|
+
# Close the reader discarding any unread results.
|
19
|
+
def close
|
20
|
+
raise NotImplementedError.new
|
21
|
+
end
|
22
|
+
|
23
|
+
# Discard the current row (if any) and read the next one (returning true), or return nil if there is no further row.
|
24
|
+
def next!
|
25
|
+
raise NotImplementedError.new
|
26
|
+
end
|
27
|
+
|
28
|
+
# Return the number of fields in the result set.
|
29
|
+
def field_count
|
30
|
+
raise NotImplementedError.new
|
31
|
+
end
|
32
|
+
|
33
|
+
# Yield each row to the given block as a Hash
|
34
|
+
def each
|
35
|
+
begin
|
36
|
+
while next!
|
37
|
+
row = {}
|
38
|
+
fields.each_with_index { |field, index| row[field] = values[index] }
|
39
|
+
yield row
|
40
|
+
end
|
41
|
+
ensure
|
42
|
+
close
|
43
|
+
end
|
44
|
+
self
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
metadata
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: martinemde-dm-salesforce-adapter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 19
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 1.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Yehuda Katz
|
14
|
+
- Tim Carey-Smith
|
15
|
+
- Andy Delcambre
|
16
|
+
- Jordan Ritter
|
17
|
+
- Martin Emde
|
18
|
+
autorequire:
|
19
|
+
bindir: bin
|
20
|
+
cert_chain: []
|
21
|
+
|
22
|
+
date: 2011-04-26 00:00:00 -07:00
|
23
|
+
default_executable:
|
24
|
+
dependencies:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
27
|
+
none: false
|
28
|
+
requirements:
|
29
|
+
- - "="
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
hash: 119
|
32
|
+
segments:
|
33
|
+
- 2
|
34
|
+
- 1
|
35
|
+
- 5
|
36
|
+
- 2
|
37
|
+
version: 2.1.5.2
|
38
|
+
prerelease: false
|
39
|
+
type: :runtime
|
40
|
+
requirement: *id001
|
41
|
+
name: httpclient
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
44
|
+
none: false
|
45
|
+
requirements:
|
46
|
+
- - ~>
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
hash: 19
|
49
|
+
segments:
|
50
|
+
- 1
|
51
|
+
- 1
|
52
|
+
- 0
|
53
|
+
version: 1.1.0
|
54
|
+
prerelease: false
|
55
|
+
type: :runtime
|
56
|
+
requirement: *id002
|
57
|
+
name: dm-do-adapter
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
version_requirements: &id003 !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
61
|
+
requirements:
|
62
|
+
- - ~>
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
hash: 19
|
65
|
+
segments:
|
66
|
+
- 1
|
67
|
+
- 1
|
68
|
+
- 0
|
69
|
+
version: 1.1.0
|
70
|
+
prerelease: false
|
71
|
+
type: :runtime
|
72
|
+
requirement: *id003
|
73
|
+
name: dm-core
|
74
|
+
- !ruby/object:Gem::Dependency
|
75
|
+
version_requirements: &id004 !ruby/object:Gem::Requirement
|
76
|
+
none: false
|
77
|
+
requirements:
|
78
|
+
- - ~>
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
hash: 19
|
81
|
+
segments:
|
82
|
+
- 1
|
83
|
+
- 1
|
84
|
+
- 0
|
85
|
+
version: 1.1.0
|
86
|
+
prerelease: false
|
87
|
+
type: :runtime
|
88
|
+
requirement: *id004
|
89
|
+
name: dm-validations
|
90
|
+
- !ruby/object:Gem::Dependency
|
91
|
+
version_requirements: &id005 !ruby/object:Gem::Requirement
|
92
|
+
none: false
|
93
|
+
requirements:
|
94
|
+
- - ~>
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
hash: 19
|
97
|
+
segments:
|
98
|
+
- 1
|
99
|
+
- 1
|
100
|
+
- 0
|
101
|
+
version: 1.1.0
|
102
|
+
prerelease: false
|
103
|
+
type: :runtime
|
104
|
+
requirement: *id005
|
105
|
+
name: dm-types
|
106
|
+
- !ruby/object:Gem::Dependency
|
107
|
+
version_requirements: &id006 !ruby/object:Gem::Requirement
|
108
|
+
none: false
|
109
|
+
requirements:
|
110
|
+
- - ~>
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
hash: 19
|
113
|
+
segments:
|
114
|
+
- 1
|
115
|
+
- 5
|
116
|
+
- 8
|
117
|
+
version: 1.5.8
|
118
|
+
prerelease: false
|
119
|
+
type: :runtime
|
120
|
+
requirement: *id006
|
121
|
+
name: soap4r
|
122
|
+
description: A DataMapper 1.1.x adapter to the Salesforce API
|
123
|
+
email: jpr5@cloudcrowd.com
|
124
|
+
executables: []
|
125
|
+
|
126
|
+
extensions: []
|
127
|
+
|
128
|
+
extra_rdoc_files:
|
129
|
+
- README.markdown
|
130
|
+
- LICENSE
|
131
|
+
files:
|
132
|
+
- LICENSE
|
133
|
+
- README.markdown
|
134
|
+
- Rakefile
|
135
|
+
- lib/dm-salesforce-adapter/adapter.rb
|
136
|
+
- lib/dm-salesforce-adapter/connection/errors.rb
|
137
|
+
- lib/dm-salesforce-adapter/connection.rb
|
138
|
+
- lib/dm-salesforce-adapter/property/boolean.rb
|
139
|
+
- lib/dm-salesforce-adapter/property/serial.rb
|
140
|
+
- lib/dm-salesforce-adapter/property.rb
|
141
|
+
- lib/dm-salesforce-adapter/resource.rb
|
142
|
+
- lib/dm-salesforce-adapter/soap_wrapper.rb
|
143
|
+
- lib/dm-salesforce-adapter/sql.rb
|
144
|
+
- lib/dm-salesforce-adapter.rb
|
145
|
+
- lib/do_salesforce/command.rb
|
146
|
+
- lib/do_salesforce/connection.rb
|
147
|
+
- lib/do_salesforce/error.rb
|
148
|
+
- lib/do_salesforce/errors.rb
|
149
|
+
- lib/do_salesforce/reader.rb
|
150
|
+
- lib/do_salesforce.rb
|
151
|
+
has_rdoc: true
|
152
|
+
homepage: http://github.com/cloudcrowd/dm-salesforce-adapter
|
153
|
+
licenses: []
|
154
|
+
|
155
|
+
post_install_message:
|
156
|
+
rdoc_options: []
|
157
|
+
|
158
|
+
require_paths:
|
159
|
+
- lib
|
160
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
161
|
+
none: false
|
162
|
+
requirements:
|
163
|
+
- - ">="
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
hash: 3
|
166
|
+
segments:
|
167
|
+
- 0
|
168
|
+
version: "0"
|
169
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
170
|
+
none: false
|
171
|
+
requirements:
|
172
|
+
- - ">="
|
173
|
+
- !ruby/object:Gem::Version
|
174
|
+
hash: 3
|
175
|
+
segments:
|
176
|
+
- 0
|
177
|
+
version: "0"
|
178
|
+
requirements: []
|
179
|
+
|
180
|
+
rubyforge_project:
|
181
|
+
rubygems_version: 1.5.0
|
182
|
+
signing_key:
|
183
|
+
specification_version: 3
|
184
|
+
summary: A DataMapper 1.1.x adapter to the Salesforce API
|
185
|
+
test_files: []
|
186
|
+
|