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 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.
@@ -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"
@@ -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,11 @@
1
+ module DataMapper
2
+ module Adapters
3
+ class SalesforceAdapter < DataObjectsAdapter
4
+ module Property
5
+ end
6
+ end
7
+ end
8
+ end
9
+
10
+ require 'dm-salesforce-adapter/property/serial'
11
+ require 'dm-salesforce-adapter/property/boolean'
@@ -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,5 @@
1
+ module DataObjects
2
+ module Salesforce
3
+ end
4
+ end
5
+
@@ -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
+