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