dm-salesforce 0.9.10
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 +80 -0
- data/Rakefile +101 -0
- data/lib/dm-salesforce.rb +15 -0
- data/lib/dm-salesforce/adapter.rb +199 -0
- data/lib/dm-salesforce/connection.rb +145 -0
- data/lib/dm-salesforce/connection/errors.rb +44 -0
- data/lib/dm-salesforce/extensions.rb +45 -0
- data/lib/dm-salesforce/soap_wrapper.rb +53 -0
- data/lib/dm-salesforce/sql.rb +60 -0
- data/lib/dm-salesforce/version.rb +3 -0
- metadata +137 -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,80 @@
|
|
1
|
+
dm-salesforce
|
2
|
+
=============
|
3
|
+
|
4
|
+
A gem that provides a Salesforce Adapter for DataMapper.
|
5
|
+
|
6
|
+
The wsdl is converted into Ruby classes and stored in ~/.salesforce. This automatically
|
7
|
+
happens the first time you use the salesforce adapter, so you don't need to worry about
|
8
|
+
generating Ruby code. It just works if you have the wsdl, directions for getting going
|
9
|
+
are outlined below.
|
10
|
+
|
11
|
+
An example of using the adapter:
|
12
|
+
|
13
|
+
class Account
|
14
|
+
include DataMapper::Resource
|
15
|
+
|
16
|
+
def self.default_repository_name
|
17
|
+
:salesforce
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.salesforce_id_properties
|
21
|
+
:id
|
22
|
+
end
|
23
|
+
|
24
|
+
property :id, String, :serial => true
|
25
|
+
property :name, String
|
26
|
+
end
|
27
|
+
|
28
|
+
class Contact
|
29
|
+
include DataMapper::Resource
|
30
|
+
|
31
|
+
def self.default_repository_name
|
32
|
+
:salesforce
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.salesforce_id_properties
|
36
|
+
[:id, :account_id]
|
37
|
+
end
|
38
|
+
|
39
|
+
property :id, String, :serial => true
|
40
|
+
property :first_name, String
|
41
|
+
property :last_name, String
|
42
|
+
property :email, String
|
43
|
+
property :account_id, String
|
44
|
+
|
45
|
+
belongs_to :account
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
To get a test environment going with the free development tools you'll need to follow these steps.
|
50
|
+
|
51
|
+
|
52
|
+
* Get a developer account from http://force.salesforce.com
|
53
|
+
* Hit up https://login.salesforce.com, and login with the password they provided in your signup email
|
54
|
+
* Remember the password they force you to reset
|
55
|
+
* Grab the following from Salesforce's web UI
|
56
|
+
* Your Enterprise API WSDL [Click Setup][setup] and [Expand and Save As][getwsdl]
|
57
|
+
* Your API Token [Reset if needed][gettoken]
|
58
|
+
* Copy the WSDL file you downloaded to config/wsdl.xml
|
59
|
+
* Copy and modify config/database.rb-example to use your info. In this case my password is 'skateboards' and my API key is 'f938915c9cdc36ff5498881b':
|
60
|
+
|
61
|
+
DataMapper.setup(:salesforce, {:adapter => 'salesforce',
|
62
|
+
:username => 'salesforceuser@mydomain.com',
|
63
|
+
:password => 'skateboardsf938915c9cdc36ff5498881b',
|
64
|
+
:path => File.expand_path(File.dirname(__FILE__)+'/wsdl.xml'),
|
65
|
+
:host => ''})
|
66
|
+
|
67
|
+
VALID_USER = DataMapperSalesforce::UserDetails.new('salesforceuser@mydomain.com', 'skateboardsf938915c9cdc36ff5498881b')
|
68
|
+
VALID_SELF_SERVICE_USER = DataMapperSalesforce::UserDetails.new("quentin@example.com", "foo")
|
69
|
+
* Run 'bin/irb' and you should have access to the Account and Contact models
|
70
|
+
|
71
|
+
Special Thanks to Engine Yard Employees who helped
|
72
|
+
==================================================
|
73
|
+
* Corey Donohoe
|
74
|
+
* Andy Delcambre
|
75
|
+
* Ben Burkert
|
76
|
+
* Larry Diehl
|
77
|
+
|
78
|
+
[setup]: http://img.skitch.com/20090204-gaxdfxbi1emfita5dax48ids4m.jpg "Click on Setup"
|
79
|
+
[getwsdl]: http://img.skitch.com/20090204-nhurnuxwf5g3ufnjk2xkfjc5n4.jpg "Expand and Save"
|
80
|
+
[gettoken]: http://img.skitch.com/20090204-mnt182ce7bc4seecqbrjjxjbef.jpg "You can reset your token here"
|
data/Rakefile
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/vendor/gems/environments/default'
|
2
|
+
require 'rake/gempackagetask'
|
3
|
+
require 'rubygems/specification'
|
4
|
+
require 'date'
|
5
|
+
|
6
|
+
require File.dirname(__FILE__) + '/lib/dm-salesforce'
|
7
|
+
require 'bundler'
|
8
|
+
|
9
|
+
GEM = "dm-salesforce"
|
10
|
+
GEM_VERSION = DataMapperSalesforce::VERSION
|
11
|
+
AUTHORS = ["Yehuda Katz", 'Tim Carey-Smith']
|
12
|
+
EMAIL = "wycats@gmail.com"
|
13
|
+
HOMEPAGE = "http://www.yehudakatz.com"
|
14
|
+
SUMMARY = "A DataMapper adapter to the Salesforce API"
|
15
|
+
|
16
|
+
@spec = Gem::Specification.new do |s|
|
17
|
+
s.name = GEM
|
18
|
+
s.version = GEM_VERSION
|
19
|
+
s.platform = Gem::Platform::RUBY
|
20
|
+
s.has_rdoc = true
|
21
|
+
s.extra_rdoc_files = ["README.markdown", "LICENSE"]
|
22
|
+
s.summary = SUMMARY
|
23
|
+
s.description = s.summary
|
24
|
+
s.authors = AUTHORS
|
25
|
+
s.email = EMAIL
|
26
|
+
s.homepage = HOMEPAGE
|
27
|
+
|
28
|
+
manifest = Bundler::ManifestFile.load(File.dirname(__FILE__) + '/Gemfile')
|
29
|
+
manifest.dependencies.each do |d|
|
30
|
+
next unless d.in?(:release)
|
31
|
+
s.add_dependency(d.name, d.version)
|
32
|
+
end
|
33
|
+
|
34
|
+
s.require_path = 'lib'
|
35
|
+
s.files = %w(LICENSE README.markdown Rakefile) + Dir.glob("lib/**/*")
|
36
|
+
end
|
37
|
+
|
38
|
+
Rake::GemPackageTask.new(@spec) do |pkg|
|
39
|
+
pkg.gem_spec = @spec
|
40
|
+
end
|
41
|
+
|
42
|
+
desc "install the gem locally"
|
43
|
+
task :install => [:package] do
|
44
|
+
sh %{sudo gem install pkg/#{GEM}-#{GEM_VERSION} --no-ri --no-rdoc}
|
45
|
+
end
|
46
|
+
|
47
|
+
task :default => 'spec'
|
48
|
+
require 'spec'
|
49
|
+
require 'spec/rake/spectask'
|
50
|
+
desc "Run specs"
|
51
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
52
|
+
t.spec_opts << %w(-fs --color) << %w(-O spec/spec.opts)
|
53
|
+
t.spec_opts << '--loadby' << 'random'
|
54
|
+
t.spec_files = %w(adapter connection models).collect { |dir| Dir["spec/#{dir}/**/*_spec.rb"] }.flatten
|
55
|
+
t.rcov = ENV.has_key?('NO_RCOV') ? ENV['NO_RCOV'] != 'true' : true
|
56
|
+
t.rcov_opts << '--exclude' << '~/.salesforce,gems,spec,config,tmp'
|
57
|
+
t.rcov_opts << '--text-summary'
|
58
|
+
t.rcov_opts << '--sort' << 'coverage' << '--sort-reverse'
|
59
|
+
end
|
60
|
+
|
61
|
+
desc "Release the version"
|
62
|
+
task :release => :repackage do
|
63
|
+
version = DataMapperSalesforce::VERSION
|
64
|
+
puts "Releasing #{version}"
|
65
|
+
|
66
|
+
`git show-ref tags/v#{version}`
|
67
|
+
unless $?.success?
|
68
|
+
abort "There is no tag for v#{version}"
|
69
|
+
end
|
70
|
+
|
71
|
+
`git show-ref heads/releasing`
|
72
|
+
if $?.success?
|
73
|
+
abort "Remove the releasing branch, we need it!"
|
74
|
+
end
|
75
|
+
|
76
|
+
puts "Checking out to the releasing branch as the tag"
|
77
|
+
system("git", "checkout", "-b", "releasing", "tags/v#{version}")
|
78
|
+
|
79
|
+
puts "Reseting back to master"
|
80
|
+
system("git", "checkout", "master")
|
81
|
+
system("git", "branch", "-d", "releasing")
|
82
|
+
|
83
|
+
current = @spec.version.to_s + ".0"
|
84
|
+
next_version = Gem::Version.new(current).bump
|
85
|
+
|
86
|
+
puts "Changing the version to #{next_version}."
|
87
|
+
|
88
|
+
version_file = File.dirname(__FILE__)+"/lib/#{GEM}/version.rb"
|
89
|
+
File.open(version_file, "w") do |f|
|
90
|
+
f.puts <<-EOT
|
91
|
+
module DataMapperSalesforce
|
92
|
+
VERSION = "#{next_version}"
|
93
|
+
end
|
94
|
+
EOT
|
95
|
+
end
|
96
|
+
|
97
|
+
puts "Committing the version change"
|
98
|
+
system("git", "commit", version_file, "-m", "Next version: #{next_version}")
|
99
|
+
|
100
|
+
puts "Push the commit up! if you don't, you'll be hunted down"
|
101
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
$:.push File.expand_path(File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require 'dm-core'
|
4
|
+
require 'dm-validations'
|
5
|
+
require 'dm-salesforce/sql'
|
6
|
+
require 'dm-salesforce/extensions'
|
7
|
+
require 'dm-salesforce/adapter'
|
8
|
+
require 'dm-salesforce/connection'
|
9
|
+
require 'dm-salesforce/version'
|
10
|
+
|
11
|
+
DataMapper::Adapters::SalesforceAdapter = DataMapperSalesforce::Adapter
|
12
|
+
|
13
|
+
module DataMapperSalesforce
|
14
|
+
UserDetails = Struct.new(:username, :password)
|
15
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
module DataMapperSalesforce
|
2
|
+
class Adapter < DataMapper::Adapters::AbstractAdapter
|
3
|
+
def initialize(name, uri_or_options)
|
4
|
+
super
|
5
|
+
@resource_naming_convention = proc do |value|
|
6
|
+
klass = Extlib::Inflection.constantize(value)
|
7
|
+
if klass.respond_to?(:salesforce_class)
|
8
|
+
klass.salesforce_class
|
9
|
+
else
|
10
|
+
value.split("::").last
|
11
|
+
end
|
12
|
+
end
|
13
|
+
@field_naming_convention = proc do |property|
|
14
|
+
connection.field_name_for(property.model.storage_name(name), property.name.to_s)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def normalize_uri(uri_or_options)
|
19
|
+
if uri_or_options.kind_of?(Addressable::URI)
|
20
|
+
return uri_or_options
|
21
|
+
end
|
22
|
+
|
23
|
+
if uri_or_options.kind_of?(String)
|
24
|
+
uri_or_options = Addressable::URI.parse(uri_or_options)
|
25
|
+
end
|
26
|
+
|
27
|
+
adapter = uri_or_options.delete(:adapter).to_s
|
28
|
+
user = uri_or_options.delete(:username)
|
29
|
+
password = uri_or_options.delete(:password)
|
30
|
+
host = uri_or_options.delete(:host) || "."
|
31
|
+
path = uri_or_options.delete(:path)
|
32
|
+
query = uri_or_options.to_a.map { |pair| pair * '=' } * '&'
|
33
|
+
query = nil if query == ''
|
34
|
+
|
35
|
+
return Addressable::URI.new({:adapter => adapter, :user => user, :password => password, :host => host, :path => path, :query => query})
|
36
|
+
end
|
37
|
+
|
38
|
+
def connection
|
39
|
+
@connection ||= Connection.new(@uri.user, @uri.password, @uri.host + @uri.path)
|
40
|
+
end
|
41
|
+
|
42
|
+
def read_many(query)
|
43
|
+
::DataMapper::Collection.new(query) do |set|
|
44
|
+
read(query) do |result|
|
45
|
+
set.load(result)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def read_one(query)
|
51
|
+
read(query) do |result|
|
52
|
+
return query.model.load(result, query)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def create(resources)
|
57
|
+
arr = resources.map do |resource|
|
58
|
+
obj = make_salesforce_obj(resource, resource.dirty_attributes, nil)
|
59
|
+
end
|
60
|
+
|
61
|
+
result = connection.create(arr)
|
62
|
+
result.each_with_index do |record, i|
|
63
|
+
resource = resources[i]
|
64
|
+
id_field = resource.class.key(resource.repository.name).find {|p| p.serial?}
|
65
|
+
if id_field
|
66
|
+
normalized_value = normalize_id_value(resource.class, id_field, record.id)
|
67
|
+
id_field.set!(resource, normalized_value)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
result.size
|
71
|
+
rescue Connection::CreateError => e
|
72
|
+
populate_errors_for(e.records, resources)
|
73
|
+
e.successful_records.size
|
74
|
+
end
|
75
|
+
|
76
|
+
def update(attributes, query)
|
77
|
+
arr = if key_condition = query.conditions.find {|op,prop,val| prop.key?}
|
78
|
+
[ make_salesforce_obj(query, attributes, key_condition.last) ]
|
79
|
+
else
|
80
|
+
read_many(query).map do |obj|
|
81
|
+
obj = make_salesforce_obj(query, attributes, x.key)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
connection.update(arr).size
|
85
|
+
rescue Connection::UpdateError => e
|
86
|
+
populate_errors_for(e.records, arr, query)
|
87
|
+
e.successful_records.size
|
88
|
+
end
|
89
|
+
|
90
|
+
def delete(query)
|
91
|
+
keys = if key_condition = query.conditions.find {|op,prop,val| prop.key?}
|
92
|
+
[key_condition.last]
|
93
|
+
else
|
94
|
+
query.read_many.map {|r| r.key}
|
95
|
+
end
|
96
|
+
|
97
|
+
connection.delete(keys).size
|
98
|
+
end
|
99
|
+
|
100
|
+
def populate_errors_for(records, resources, query = nil)
|
101
|
+
records.each_with_index do |record,i|
|
102
|
+
next if record.success
|
103
|
+
|
104
|
+
if resources[i].is_a?(DataMapper::Resource)
|
105
|
+
resource = resources[i]
|
106
|
+
elsif resources[i].is_a?(SalesforceAPI::SObject)
|
107
|
+
resource = query.repository.identity_map(query.model)[[resources[i].id]]
|
108
|
+
else
|
109
|
+
resource = query.repository.identity_map(query.model)[[resources[i]]]
|
110
|
+
end
|
111
|
+
|
112
|
+
resource.class.send(:include, SalesforceExtensions)
|
113
|
+
record.errors.each do |error|
|
114
|
+
case error.statusCode
|
115
|
+
when "DUPLICATE_VALUE"
|
116
|
+
if error.message =~ /duplicate value found: (.*) duplicates/
|
117
|
+
resource.add_salesforce_error_for($1, error.message)
|
118
|
+
end
|
119
|
+
when "REQUIRED_FIELD_MISSING", "INVALID_EMAIL_ADDRESS"
|
120
|
+
error.fields.each do |field|
|
121
|
+
resource.add_salesforce_error_for(field, error.message)
|
122
|
+
end
|
123
|
+
when "SERVER_UNAVAILABLE"
|
124
|
+
raise Connection::ServerUnavailable, "The salesforce server is currently unavailable"
|
125
|
+
else
|
126
|
+
raise Connection::UnknownStatusCode, "Got an unknown statusCode: #{error.statusCode.inspect}"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# A dummy method to allow migrations without upsetting any data
|
133
|
+
def destroy_model_storage(*args)
|
134
|
+
true
|
135
|
+
end
|
136
|
+
|
137
|
+
# A dummy method to allow auto_migrate! to run
|
138
|
+
def upgrade_model_storage(*args)
|
139
|
+
true
|
140
|
+
end
|
141
|
+
|
142
|
+
# A dummy method to allow migrations without upsetting any data
|
143
|
+
def create_model_storage(*args)
|
144
|
+
true
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
def read(query, &block)
|
149
|
+
repository = query.repository
|
150
|
+
properties = query.fields
|
151
|
+
properties_with_indexes = Hash[*properties.zip((0...properties.size).to_a).flatten]
|
152
|
+
conditions = query.conditions.map {|c| SQL.from_condition(c, repository)}.compact.join(") AND (")
|
153
|
+
|
154
|
+
sql = "SELECT #{query.fields.map {|f| f.field(repository.name)}.join(", ")} from #{query.model.storage_name(repository.name)}"
|
155
|
+
sql << " WHERE (#{conditions})" unless conditions.empty?
|
156
|
+
sql << " ORDER BY #{SQL.order(query.order[0])}" unless query.order.empty?
|
157
|
+
sql << " LIMIT #{query.limit}" if query.limit
|
158
|
+
|
159
|
+
DataMapper.logger.debug sql
|
160
|
+
|
161
|
+
result = connection.query(sql)
|
162
|
+
|
163
|
+
return unless result.records
|
164
|
+
|
165
|
+
result.records.each do |record|
|
166
|
+
accum = []
|
167
|
+
properties_with_indexes.each do |(property, idx)|
|
168
|
+
meth = connection.field_name_for(property.model.storage_name(repository.name), property.field(repository.name))
|
169
|
+
accum[idx] = normalize_id_value(query.model, property, record.send(meth))
|
170
|
+
end
|
171
|
+
yield accum
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def make_salesforce_obj(query, attrs, key)
|
176
|
+
klass_name = query.model.storage_name(query.repository.name)
|
177
|
+
values = {}
|
178
|
+
if key
|
179
|
+
key_value = query.conditions.find {|op,prop,val| prop.key?}.last
|
180
|
+
values["id"] = normalize_id_value(query.model, query.model.properties[:id], key_value)
|
181
|
+
end
|
182
|
+
|
183
|
+
attrs.each do |property,value|
|
184
|
+
normalized_value = normalize_id_value(query.model, property, value)
|
185
|
+
values[property.field(query.repository.name)] = normalized_value
|
186
|
+
end
|
187
|
+
connection.make_object(klass_name, values)
|
188
|
+
end
|
189
|
+
|
190
|
+
def normalize_id_value(klass, property, value)
|
191
|
+
return value unless value
|
192
|
+
if klass.respond_to?(:salesforce_id_properties)
|
193
|
+
properties = Array(klass.salesforce_id_properties).map {|p| p.to_sym}
|
194
|
+
return value[0..14] if properties.include?(property.name)
|
195
|
+
end
|
196
|
+
value
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'soap/wsdlDriver'
|
2
|
+
require 'soap/header/simplehandler'
|
3
|
+
require "rexml/element"
|
4
|
+
require 'dm-salesforce/soap_wrapper'
|
5
|
+
|
6
|
+
module DataMapperSalesforce
|
7
|
+
class Connection
|
8
|
+
class HeaderHandler < SOAP::Header::SimpleHandler
|
9
|
+
def initialize(tag, value)
|
10
|
+
super(XSD::QName.new('urn:enterprise.soap.sforce.com', tag))
|
11
|
+
@tag = tag
|
12
|
+
@value = value
|
13
|
+
end
|
14
|
+
def on_simple_outbound
|
15
|
+
@value
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(username, password, wsdl_path, organization_id = nil)
|
20
|
+
@wrapper = SoapWrapper.new("SalesforceAPI", "Soap", wsdl_path, api_dir)
|
21
|
+
@username, @password, @organization_id = URI.unescape(username), password, organization_id
|
22
|
+
login
|
23
|
+
end
|
24
|
+
attr_reader :user_id, :user_details
|
25
|
+
|
26
|
+
def wsdl_path
|
27
|
+
@wrapper.wsdl_path
|
28
|
+
end
|
29
|
+
|
30
|
+
def organization_id
|
31
|
+
@user_details && @user_details.organizationId
|
32
|
+
end
|
33
|
+
|
34
|
+
def make_object(klass_name, values)
|
35
|
+
klass = SalesforceAPI.const_get(klass_name)
|
36
|
+
obj = klass.new
|
37
|
+
values.each do |property,value|
|
38
|
+
field = field_name_for(klass_name, property)
|
39
|
+
if value.nil? or value == ""
|
40
|
+
obj.fieldsToNull.push(field)
|
41
|
+
else
|
42
|
+
obj.send("#{field}=", value)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
obj
|
46
|
+
end
|
47
|
+
|
48
|
+
def field_name_for(klass_name, column)
|
49
|
+
klass = SalesforceAPI.const_get(klass_name)
|
50
|
+
fields = [column, column.camel_case, "#{column}__c".downcase]
|
51
|
+
options = /^(#{fields.join("|")})$/i
|
52
|
+
matches = klass.instance_methods(false).grep(options)
|
53
|
+
if matches.any?
|
54
|
+
matches.first
|
55
|
+
else
|
56
|
+
raise FieldNotFound,
|
57
|
+
"You specified #{column} as a field, but neither #{fields.join(" or ")} exist. " \
|
58
|
+
"Either manually specify the field name with :field, or check to make sure you have " \
|
59
|
+
"provided a correct field name."
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def query(string)
|
64
|
+
with_reconnection do
|
65
|
+
driver.query(:queryString => string).result
|
66
|
+
end
|
67
|
+
rescue SOAP::FaultError => e
|
68
|
+
raise QueryError.new(e.message, [])
|
69
|
+
end
|
70
|
+
|
71
|
+
def create(objects)
|
72
|
+
call_api(:create, CreateError, "creating", objects)
|
73
|
+
end
|
74
|
+
|
75
|
+
def update(objects)
|
76
|
+
call_api(:update, UpdateError, "updating", objects)
|
77
|
+
end
|
78
|
+
|
79
|
+
def delete(keys)
|
80
|
+
call_api(:delete, DeleteError, "deleting", keys)
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
def api_dir
|
85
|
+
ENV["SALESFORCE_DIR"] || "#{ENV["HOME"]}/.salesforce"
|
86
|
+
end
|
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.faultcode.to_obj == "sf:INVALID_LOGIN"
|
102
|
+
raise LoginFailed, "Could not login to Salesforce; #{error.faultstring.text}"
|
103
|
+
else
|
104
|
+
raise
|
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
|
+
raise exception_class.new("Got some errors while #{message} Salesforce objects", result)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def with_reconnection(&block)
|
127
|
+
yield
|
128
|
+
rescue SOAP::FaultError => error
|
129
|
+
retry_count ||= 0
|
130
|
+
if error.faultcode.text == "sf:INVALID_SESSION_ID"
|
131
|
+
$stderr.puts "Got a invalid session id; reconnecting"
|
132
|
+
@driver = nil
|
133
|
+
login
|
134
|
+
retry_count += 1
|
135
|
+
retry unless retry_count > 5
|
136
|
+
else
|
137
|
+
raise
|
138
|
+
end
|
139
|
+
|
140
|
+
raise SessionTimeout, "The Salesforce session could not be established"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
require 'dm-salesforce/connection/errors'
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module DataMapperSalesforce
|
2
|
+
class Connection
|
3
|
+
class Error < StandardError; end
|
4
|
+
class FieldNotFound < Error; end
|
5
|
+
class LoginFailed < Error; end
|
6
|
+
class SessionTimeout < Error; end
|
7
|
+
class UnknownStatusCode < Error; end
|
8
|
+
class ServerUnavailable < Error; end
|
9
|
+
|
10
|
+
|
11
|
+
class SOAPError < Error
|
12
|
+
def initialize(message, result)
|
13
|
+
@result = result
|
14
|
+
super("#{message}: #{result_message}")
|
15
|
+
end
|
16
|
+
|
17
|
+
def records
|
18
|
+
@result.to_a
|
19
|
+
end
|
20
|
+
|
21
|
+
def failed_records
|
22
|
+
@result.reject {|r| r.success}
|
23
|
+
end
|
24
|
+
|
25
|
+
def successful_records
|
26
|
+
@result.select {|r| r.success}
|
27
|
+
end
|
28
|
+
|
29
|
+
def result_message
|
30
|
+
failed_records.map do |r|
|
31
|
+
message_for_record(r)
|
32
|
+
end.join("; ")
|
33
|
+
end
|
34
|
+
|
35
|
+
def message_for_record(record)
|
36
|
+
record.errors.map {|e| "#{e.statusCode}: #{e.message}"}.join(", ")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
class CreateError < SOAPError; end
|
40
|
+
class QueryError < SOAPError; end
|
41
|
+
class DeleteError < SOAPError; end
|
42
|
+
class UpdateError < SOAPError; end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module DataMapperSalesforce
|
2
|
+
module SalesforceExtensions
|
3
|
+
def self.included(mod)
|
4
|
+
mod.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def properties_with_salesforce_validation
|
9
|
+
@properties_with_salesforce_validation ||= []
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_salesforce_validation_for(property)
|
13
|
+
unless properties_with_salesforce_validation.include?(property)
|
14
|
+
validates_with_block property.name do
|
15
|
+
if message = salesforce_errors[property]
|
16
|
+
[false, message]
|
17
|
+
else
|
18
|
+
true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
properties_with_salesforce_validation << property
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_salesforce_error_for(field, message)
|
27
|
+
if property = property_for_salesforce_field(field)
|
28
|
+
self.class.add_salesforce_validation_for(property)
|
29
|
+
salesforce_errors[property] = message
|
30
|
+
else
|
31
|
+
raise "Field not found"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def property_for_salesforce_field(name)
|
36
|
+
self.class.properties.find do |p|
|
37
|
+
p.field.downcase == name.downcase
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def salesforce_errors
|
42
|
+
@salesforce_errors ||= {}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
|
3
|
+
module DataMapperSalesforce
|
4
|
+
class SoapWrapper
|
5
|
+
class ClassesFailedToGenerate < StandardError; end
|
6
|
+
|
7
|
+
def initialize(module_name, driver_name, wsdl_path, api_dir)
|
8
|
+
@module_name, @driver_name, @wsdl_path, @api_dir = module_name, driver_name, File.expand_path(wsdl_path), File.expand_path(api_dir)
|
9
|
+
generate_soap_classes
|
10
|
+
driver
|
11
|
+
end
|
12
|
+
attr_reader :module_name, :driver_name, :wsdl_path, :api_dir
|
13
|
+
|
14
|
+
def driver
|
15
|
+
@driver ||= Object.const_get(module_name).const_get(driver_name).new
|
16
|
+
end
|
17
|
+
|
18
|
+
def generate_soap_classes
|
19
|
+
unless File.file?(wsdl_path)
|
20
|
+
raise Errno::ENOENT, "Could not find the WSDL at #{wsdl_path}"
|
21
|
+
end
|
22
|
+
|
23
|
+
unless File.directory?(wsdl_api_dir)
|
24
|
+
FileUtils.mkdir_p wsdl_api_dir
|
25
|
+
end
|
26
|
+
|
27
|
+
unless files_exist?
|
28
|
+
soap4r = Gem.loaded_specs['soap4r']
|
29
|
+
wsdl2ruby = File.expand_path(File.join(soap4r.full_gem_path, soap4r.bindir, "wsdl2ruby.rb"))
|
30
|
+
Dir.chdir(wsdl_api_dir) do
|
31
|
+
old_args = ARGV.dup
|
32
|
+
ARGV.replace %W(--wsdl #{wsdl_path} --module_path #{module_name} --classdef #{module_name} --type client)
|
33
|
+
load wsdl2ruby
|
34
|
+
ARGV.replace old_args
|
35
|
+
FileUtils.rm Dir["*Client.rb"]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
$:.push wsdl_api_dir
|
40
|
+
require "#{module_name}Driver"
|
41
|
+
end
|
42
|
+
|
43
|
+
def files_exist?
|
44
|
+
%w( .rb Driver.rb MappingRegistry.rb ).all? do |suffix|
|
45
|
+
File.exist?("#{wsdl_api_dir}/#{module_name}#{suffix}")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def wsdl_api_dir
|
50
|
+
"#{api_dir}/#{File.basename(wsdl_path)}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module DataMapperSalesforce
|
2
|
+
module SQL
|
3
|
+
class << self
|
4
|
+
def from_condition(condition, repository)
|
5
|
+
op, prop, value = condition
|
6
|
+
operator = case op
|
7
|
+
when String then operator
|
8
|
+
when :eql, :in then equality_operator(value)
|
9
|
+
when :not then inequality_operator(value)
|
10
|
+
when :like then "LIKE #{quote_value(value)}"
|
11
|
+
when :gt then "> #{quote_value(value)}"
|
12
|
+
when :gte then ">= #{quote_value(value)}"
|
13
|
+
when :lt then "< #{quote_value(value)}"
|
14
|
+
when :lte then "<= #{quote_value(value)}"
|
15
|
+
else raise "CAN HAS CRASH?"
|
16
|
+
end
|
17
|
+
case prop
|
18
|
+
when DataMapper::Property
|
19
|
+
"#{prop.field} #{operator}"
|
20
|
+
when DataMapper::Query::Path
|
21
|
+
rels = prop.relationships
|
22
|
+
names = rels.map {|r| storage_name(r, repository) }.join(".")
|
23
|
+
"#{names}.#{prop.field} #{operator}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def storage_name(rel, repository)
|
28
|
+
rel.parent_model.storage_name(repository.name)
|
29
|
+
end
|
30
|
+
|
31
|
+
def order(direction)
|
32
|
+
"#{direction.property.field} #{direction.direction.to_s.upcase}"
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def equality_operator(value)
|
37
|
+
case value
|
38
|
+
when Array then "IN #{quote_value(value)}"
|
39
|
+
else "= #{quote_value(value)}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def inequality_operator(value)
|
44
|
+
case value
|
45
|
+
when Array then "NOT IN #{quote_value(value)}"
|
46
|
+
else "!= #{quote_value(value)}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def quote_value(value)
|
51
|
+
case value
|
52
|
+
when Array then "(#{value.map {|v| quote_value(v)}.join(", ")})"
|
53
|
+
when NilClass then "NULL"
|
54
|
+
when String then "'#{value.gsub(/'/, "\\'").gsub(/\\/, %{\\\\})}'"
|
55
|
+
else "#{value}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
metadata
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dm-salesforce
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.10
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Yehuda Katz
|
8
|
+
- Tim Carey-Smith
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2009-08-06 00:00:00 -06:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: httpclient
|
18
|
+
type: :runtime
|
19
|
+
version_requirement:
|
20
|
+
version_requirements: !ruby/object:Gem::Requirement
|
21
|
+
requirements:
|
22
|
+
- - "="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 2.1.2
|
25
|
+
version:
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: extlib
|
28
|
+
type: :runtime
|
29
|
+
version_requirement:
|
30
|
+
version_requirements: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ~>
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 0.9.9
|
35
|
+
version:
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: dm-core
|
38
|
+
type: :runtime
|
39
|
+
version_requirement:
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ~>
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: 0.9.8
|
45
|
+
version:
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: dm-validations
|
48
|
+
type: :runtime
|
49
|
+
version_requirement:
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.9.8
|
55
|
+
version:
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: soap4r
|
58
|
+
type: :runtime
|
59
|
+
version_requirement:
|
60
|
+
version_requirements: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ~>
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: 1.5.8
|
65
|
+
version:
|
66
|
+
- !ruby/object:Gem::Dependency
|
67
|
+
name: data_objects
|
68
|
+
type: :runtime
|
69
|
+
version_requirement:
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ~>
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 0.9.9
|
75
|
+
version:
|
76
|
+
- !ruby/object:Gem::Dependency
|
77
|
+
name: do_sqlite3
|
78
|
+
type: :runtime
|
79
|
+
version_requirement:
|
80
|
+
version_requirements: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ~>
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: 0.9.9
|
85
|
+
version:
|
86
|
+
description: A DataMapper adapter to the Salesforce API
|
87
|
+
email: wycats@gmail.com
|
88
|
+
executables: []
|
89
|
+
|
90
|
+
extensions: []
|
91
|
+
|
92
|
+
extra_rdoc_files:
|
93
|
+
- README.markdown
|
94
|
+
- LICENSE
|
95
|
+
files:
|
96
|
+
- LICENSE
|
97
|
+
- README.markdown
|
98
|
+
- Rakefile
|
99
|
+
- lib/dm-salesforce/adapter.rb
|
100
|
+
- lib/dm-salesforce/connection/errors.rb
|
101
|
+
- lib/dm-salesforce/connection.rb
|
102
|
+
- lib/dm-salesforce/extensions.rb
|
103
|
+
- lib/dm-salesforce/soap_wrapper.rb
|
104
|
+
- lib/dm-salesforce/sql.rb
|
105
|
+
- lib/dm-salesforce/version.rb
|
106
|
+
- lib/dm-salesforce.rb
|
107
|
+
has_rdoc: true
|
108
|
+
homepage: http://www.yehudakatz.com
|
109
|
+
licenses: []
|
110
|
+
|
111
|
+
post_install_message:
|
112
|
+
rdoc_options: []
|
113
|
+
|
114
|
+
require_paths:
|
115
|
+
- lib
|
116
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: "0"
|
121
|
+
version:
|
122
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: "0"
|
127
|
+
version:
|
128
|
+
requirements: []
|
129
|
+
|
130
|
+
rubyforge_project:
|
131
|
+
rubygems_version: 1.3.5
|
132
|
+
signing_key:
|
133
|
+
source:
|
134
|
+
specification_version: 3
|
135
|
+
summary: A DataMapper adapter to the Salesforce API
|
136
|
+
test_files: []
|
137
|
+
|