martinemde-dm-salesforce-adapter 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|