martinemde-dm-salesforce-adapter 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/LICENSE 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
+