dm-salesforce 0.9.12 → 0.10.1
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/README.markdown +33 -17
- data/Rakefile +8 -7
- data/lib/dm-salesforce.rb +19 -10
- data/lib/dm-salesforce/adapter.rb +91 -130
- data/lib/dm-salesforce/connection.rb +9 -16
- data/lib/dm-salesforce/connection/errors.rb +7 -1
- data/lib/dm-salesforce/resource.rb +8 -0
- data/lib/dm-salesforce/soap_wrapper.rb +25 -20
- data/lib/dm-salesforce/sql.rb +74 -46
- data/lib/dm-salesforce/types.rb +14 -0
- data/lib/dm-salesforce/types/boolean.rb +23 -0
- data/lib/dm-salesforce/types/float.rb +8 -0
- data/lib/dm-salesforce/types/integer.rb +12 -0
- data/lib/dm-salesforce/types/serial.rb +14 -0
- data/lib/dm-salesforce/version.rb +2 -2
- metadata +19 -13
- data/lib/dm-salesforce/extensions.rb +0 -45
data/README.markdown
CHANGED
@@ -1,12 +1,20 @@
|
|
1
1
|
dm-salesforce
|
2
2
|
=============
|
3
3
|
|
4
|
-
A gem that provides a Salesforce Adapter for DataMapper.
|
4
|
+
A gem that provides a Salesforce Adapter for DataMapper 0.10.x.
|
5
|
+
There are older versions of dm-salesforce specifically for 0.9.x.
|
5
6
|
|
6
|
-
The wsdl is converted into Ruby classes
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
The wsdl is automatically converted into Ruby classes upon the first
|
8
|
+
invocation of the dm-salesforce adapter. The classes in turn get
|
9
|
+
cached locally in one of the following locations, in order of
|
10
|
+
precedence:
|
11
|
+
|
12
|
+
:repositories:salesforce:apidir (see included database.yml-example)
|
13
|
+
ENV["SALESFORCE_DIR"]
|
14
|
+
~/.salesforce/
|
15
|
+
|
16
|
+
It just works if you have the wsdl, directions for getting going are
|
17
|
+
outlined below.
|
10
18
|
|
11
19
|
An example of using the adapter:
|
12
20
|
|
@@ -17,12 +25,20 @@ An example of using the adapter:
|
|
17
25
|
:salesforce
|
18
26
|
end
|
19
27
|
|
28
|
+
# One way to define which properties are SalesForce-style.
|
20
29
|
def self.salesforce_id_properties
|
21
30
|
:id
|
22
31
|
end
|
23
32
|
|
24
|
-
property :id,
|
25
|
-
property :name,
|
33
|
+
property :id, String, :key => true
|
34
|
+
property :name, String
|
35
|
+
property :description, String
|
36
|
+
property :fax, String
|
37
|
+
property :phone, String
|
38
|
+
property :type, String
|
39
|
+
property :website, String
|
40
|
+
|
41
|
+
has 0..n, :contacts
|
26
42
|
end
|
27
43
|
|
28
44
|
class Contact
|
@@ -32,15 +48,12 @@ An example of using the adapter:
|
|
32
48
|
:salesforce
|
33
49
|
end
|
34
50
|
|
35
|
-
|
36
|
-
|
37
|
-
end
|
38
|
-
|
39
|
-
property :id, String, :serial => true
|
51
|
+
# Alternatively, specify the :salesforce_id option.
|
52
|
+
property :id, String, :serial => true, :salesforce_id => true
|
40
53
|
property :first_name, String
|
41
|
-
property :last_name,
|
42
|
-
property :email,
|
43
|
-
property :account_id, String
|
54
|
+
property :last_name, String
|
55
|
+
property :email, String
|
56
|
+
property :account_id, String, :salesforce_id => true
|
44
57
|
|
45
58
|
belongs_to :account
|
46
59
|
end
|
@@ -48,7 +61,6 @@ An example of using the adapter:
|
|
48
61
|
|
49
62
|
To get a test environment going with the free development tools you'll need to follow these steps.
|
50
63
|
|
51
|
-
|
52
64
|
* Get a developer account from http://force.salesforce.com
|
53
65
|
* Hit up https://login.salesforce.com, and login with the password they provided in your signup email
|
54
66
|
* Remember the password they force you to reset
|
@@ -68,12 +80,16 @@ To get a test environment going with the free development tools you'll need to f
|
|
68
80
|
VALID_SELF_SERVICE_USER = DataMapperSalesforce::UserDetails.new("quentin@example.com", "foo")
|
69
81
|
* Run 'bin/irb' and you should have access to the Account and Contact models
|
70
82
|
|
71
|
-
Special Thanks to
|
83
|
+
Special Thanks to those who helped
|
72
84
|
==================================================
|
85
|
+
* Yehuda Katz
|
73
86
|
* Corey Donohoe
|
87
|
+
* Tim Carey-Smith
|
74
88
|
* Andy Delcambre
|
75
89
|
* Ben Burkert
|
76
90
|
* Larry Diehl
|
91
|
+
* Jordan Ritter
|
92
|
+
* Martin Emde
|
77
93
|
|
78
94
|
[setup]: http://img.skitch.com/20090204-gaxdfxbi1emfita5dax48ids4m.jpg "Click on Setup"
|
79
95
|
[getwsdl]: http://img.skitch.com/20090204-nhurnuxwf5g3ufnjk2xkfjc5n4.jpg "Expand and Save"
|
data/Rakefile
CHANGED
@@ -4,15 +4,16 @@ require 'rubygems/specification'
|
|
4
4
|
require 'bundler'
|
5
5
|
require 'date'
|
6
6
|
require 'pp'
|
7
|
+
require 'tmpdir'
|
7
8
|
|
8
9
|
Bundler.require_env
|
9
10
|
require File.dirname(__FILE__) + '/lib/dm-salesforce'
|
10
11
|
|
11
12
|
GEM = "dm-salesforce"
|
12
|
-
GEM_VERSION =
|
13
|
-
AUTHORS = ["Yehuda Katz", 'Tim Carey-Smith']
|
14
|
-
EMAIL = "
|
15
|
-
HOMEPAGE = "http://
|
13
|
+
GEM_VERSION = DataMapper::Salesforce::VERSION
|
14
|
+
AUTHORS = ["Yehuda Katz", 'Tim Carey-Smith', 'Corey Donohoe']
|
15
|
+
EMAIL = "ninja@engineyard.com"
|
16
|
+
HOMEPAGE = "http://github.com/halorgium/dm-salesforce"
|
16
17
|
SUMMARY = "A DataMapper adapter to the Salesforce API"
|
17
18
|
|
18
19
|
@spec = Gem::Specification.new do |s|
|
@@ -43,7 +44,7 @@ end
|
|
43
44
|
|
44
45
|
desc "install the gem locally"
|
45
46
|
task :install => [:package] do
|
46
|
-
sh %{
|
47
|
+
sh %{gem install pkg/#{GEM}-#{GEM_VERSION} --no-ri --no-rdoc}
|
47
48
|
end
|
48
49
|
|
49
50
|
task :default => 'spec'
|
@@ -53,9 +54,9 @@ desc "Run specs"
|
|
53
54
|
Spec::Rake::SpecTask.new(:spec) do |t|
|
54
55
|
t.spec_opts << %w(-fs --color) << %w(-O spec/spec.opts)
|
55
56
|
t.spec_opts << '--loadby' << 'random'
|
56
|
-
t.spec_files =
|
57
|
+
t.spec_files = Dir["spec/**/*_spec.rb"]
|
57
58
|
t.rcov = ENV.has_key?('NO_RCOV') ? ENV['NO_RCOV'] != 'true' : true
|
58
|
-
t.rcov_opts << '--exclude' <<
|
59
|
+
t.rcov_opts << '--exclude' << "~/.salesforce,gems,vendor,/var/folders,spec,config,tmp"
|
59
60
|
t.rcov_opts << '--text-summary'
|
60
61
|
t.rcov_opts << '--sort' << 'coverage' << '--sort-reverse'
|
61
62
|
end
|
data/lib/dm-salesforce.rb
CHANGED
@@ -1,15 +1,24 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
require "fileutils"
|
3
2
|
require 'dm-core'
|
4
3
|
require 'dm-validations'
|
5
|
-
require '
|
6
|
-
require '
|
7
|
-
require
|
8
|
-
require 'dm-salesforce/connection'
|
9
|
-
require 'dm-salesforce/version'
|
10
|
-
|
11
|
-
DataMapper::Adapters::SalesforceAdapter = DataMapperSalesforce::Adapter
|
4
|
+
require 'soap/wsdlDriver'
|
5
|
+
require 'soap/header/simplehandler'
|
6
|
+
require "rexml/element"
|
12
7
|
|
13
|
-
module
|
8
|
+
module DataMapper::Salesforce
|
14
9
|
UserDetails = Struct.new(:username, :password)
|
15
10
|
end
|
11
|
+
|
12
|
+
dir = File.expand_path(File.join(File.dirname(__FILE__), 'dm-salesforce'))
|
13
|
+
|
14
|
+
require dir / :resource
|
15
|
+
require dir / :connection
|
16
|
+
require dir / :connection / :errors
|
17
|
+
require dir / :soap_wrapper
|
18
|
+
require dir / :sql
|
19
|
+
require dir / :types
|
20
|
+
require dir / :version
|
21
|
+
|
22
|
+
require dir / :adapter
|
23
|
+
|
24
|
+
DataMapper::Adapters::SalesforceAdapter = DataMapper::Salesforce::Adapter
|
@@ -1,5 +1,7 @@
|
|
1
|
-
module
|
1
|
+
module DataMapper::Salesforce
|
2
2
|
class Adapter < DataMapper::Adapters::AbstractAdapter
|
3
|
+
include SQL
|
4
|
+
|
3
5
|
def initialize(name, uri_or_options)
|
4
6
|
super
|
5
7
|
@resource_naming_convention = proc do |value|
|
@@ -15,185 +17,144 @@ module DataMapperSalesforce
|
|
15
17
|
end
|
16
18
|
end
|
17
19
|
|
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
20
|
def connection
|
39
|
-
@connection ||= Connection.new(
|
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
|
21
|
+
@connection ||= Connection.new(options["username"], options["password"], options["path"], options["apidir"])
|
54
22
|
end
|
55
23
|
|
56
24
|
def create(resources)
|
57
25
|
arr = resources.map do |resource|
|
58
|
-
|
26
|
+
make_salesforce_obj(resource, resource.dirty_attributes)
|
59
27
|
end
|
60
28
|
|
61
29
|
result = connection.create(arr)
|
62
30
|
result.each_with_index do |record, i|
|
63
31
|
resource = resources[i]
|
64
|
-
id_field = resource.class.key
|
65
|
-
if id_field
|
32
|
+
if id_field = resource.class.key.find {|p| p.serial?}
|
66
33
|
normalized_value = normalize_id_value(resource.class, id_field, record.id)
|
67
34
|
id_field.set!(resource, normalized_value)
|
68
35
|
end
|
69
36
|
end
|
37
|
+
|
70
38
|
result.size
|
71
|
-
|
72
|
-
|
73
|
-
e
|
39
|
+
|
40
|
+
rescue Connection::SOAPError => e
|
41
|
+
handle_server_outage(e)
|
74
42
|
end
|
75
43
|
|
76
|
-
def update(attributes,
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
read_many(query).map do |obj|
|
81
|
-
obj = make_salesforce_obj(query, attributes, x.key)
|
82
|
-
end
|
83
|
-
end
|
44
|
+
def update(attributes, collection)
|
45
|
+
query = collection.query
|
46
|
+
arr = collection.map { |obj| make_salesforce_obj(query, attributes) }
|
47
|
+
|
84
48
|
connection.update(arr).size
|
85
|
-
|
86
|
-
|
87
|
-
e
|
49
|
+
|
50
|
+
rescue Connection::SOAPError => e
|
51
|
+
handle_server_outage(e)
|
88
52
|
end
|
89
53
|
|
90
|
-
def delete(
|
91
|
-
|
92
|
-
|
93
|
-
else
|
94
|
-
query.read_many.map {|r| r.key}
|
95
|
-
end
|
54
|
+
def delete(collection)
|
55
|
+
query = collection.query
|
56
|
+
keys = collection.map { |r| r.key }.flatten.uniq
|
96
57
|
|
97
58
|
connection.delete(keys).size
|
59
|
+
|
60
|
+
rescue Connection::SOAPError => e
|
61
|
+
handle_server_outage(e)
|
98
62
|
end
|
99
63
|
|
100
|
-
def
|
101
|
-
|
102
|
-
|
64
|
+
def handle_server_outage(error)
|
65
|
+
if error.server_unavailable?
|
66
|
+
raise Connection::ServerUnavailable, "The salesforce server is currently unavailable"
|
67
|
+
else
|
68
|
+
raise error
|
69
|
+
end
|
70
|
+
end
|
103
71
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
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
|
72
|
+
# Reading responses back from SELECTS:
|
73
|
+
# In the typical case, response.size reflects the # of records returned.
|
74
|
+
# In the aggregation case, response.size reflects the count.
|
75
|
+
#
|
76
|
+
# Interpretation of this field requires knowledge of whether we
|
77
|
+
# are expecting an aggregate result, thus the response is
|
78
|
+
# processed differently depending on invocation.
|
79
|
+
def read(query)
|
80
|
+
properties = query.fields
|
81
|
+
repository = query.repository
|
82
|
+
|
83
|
+
response = execute_query(query)
|
84
|
+
return [] unless response.records
|
85
|
+
|
86
|
+
rows = response.records.inject([]) do |results, record|
|
87
|
+
results << properties.inject({}) do |result, property|
|
88
|
+
meth = connection.field_name_for(property.model.storage_name(repository.name), property.field)
|
89
|
+
result[property] = normalize_id_value(query.model, property, record.send(meth))
|
90
|
+
result
|
128
91
|
end
|
129
92
|
end
|
130
|
-
end
|
131
93
|
|
132
|
-
|
133
|
-
def destroy_model_storage(*args)
|
134
|
-
true
|
94
|
+
query.model.load(rows, query)
|
135
95
|
end
|
136
96
|
|
137
|
-
#
|
138
|
-
|
139
|
-
|
140
|
-
|
97
|
+
# http://www.salesforce.com/us/developer/docs/api90/Content/sforce_api_calls_soql.htm
|
98
|
+
# SOQL doesn't support anything but count(), so we catch it here
|
99
|
+
# and interpret the result.
|
100
|
+
def aggregate(query)
|
101
|
+
query.fields.each do |f|
|
102
|
+
unless f.target == :all && f.operator == :count
|
103
|
+
raise ArgumentError, %{Aggregate function #{f.operator} not supported in SOQL}
|
104
|
+
end
|
105
|
+
end
|
141
106
|
|
142
|
-
|
143
|
-
def create_model_storage(*args)
|
144
|
-
true
|
107
|
+
[ execute_query(query).size ]
|
145
108
|
end
|
146
109
|
|
147
110
|
private
|
148
|
-
def
|
111
|
+
def execute_query(query)
|
149
112
|
repository = query.repository
|
150
|
-
|
151
|
-
|
152
|
-
|
113
|
+
conditions = query.conditions.map {|c| from_condition(c, repository)}.compact.join(") AND (")
|
114
|
+
|
115
|
+
fields = query.fields.map do |f|
|
116
|
+
case f
|
117
|
+
when DataMapper::Property
|
118
|
+
f.field
|
119
|
+
when DataMapper::Query::Operator
|
120
|
+
%{#{f.operator}()}
|
121
|
+
else
|
122
|
+
raise ArgumentError, "Unknown query field #{f.class}: #{f.inspect}"
|
123
|
+
end
|
124
|
+
end.join(", ")
|
153
125
|
|
154
|
-
sql = "SELECT #{
|
126
|
+
sql = "SELECT #{fields} from #{query.model.storage_name(repository.name)}"
|
155
127
|
sql << " WHERE (#{conditions})" unless conditions.empty?
|
156
|
-
sql << " ORDER BY #{
|
128
|
+
sql << " ORDER BY #{order(query.order[0])}" unless query.order.empty?
|
157
129
|
sql << " LIMIT #{query.limit}" if query.limit
|
158
130
|
|
159
|
-
DataMapper.logger.debug sql
|
131
|
+
DataMapper.logger.debug sql if DataMapper.logger
|
160
132
|
|
161
|
-
|
133
|
+
connection.query(sql)
|
134
|
+
end
|
162
135
|
|
163
|
-
|
136
|
+
def make_salesforce_obj(from, with_attrs)
|
137
|
+
klass_name = from.model.storage_name(from.repository.name)
|
138
|
+
values = {}
|
164
139
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
accum[idx] = normalize_id_value(query.model, property, record.send(meth))
|
170
|
-
end
|
171
|
-
yield accum
|
140
|
+
# FIXME: query.conditions is potentially a tree now
|
141
|
+
if from.is_a?(::DataMapper::Query)
|
142
|
+
key_value = from.conditions.find { |c| c.subject.key? }.value
|
143
|
+
values["id"] = normalize_id_value(from.model, from.model.properties[:id], key_value)
|
172
144
|
end
|
173
|
-
end
|
174
145
|
|
175
|
-
|
176
|
-
|
177
|
-
|
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)
|
146
|
+
with_attrs.each do |property, value|
|
147
|
+
next if property.serial? || property.key? and value.nil?
|
148
|
+
values[property.field] = normalize_id_value(from.model, property, value)
|
181
149
|
end
|
182
150
|
|
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
151
|
connection.make_object(klass_name, values)
|
188
152
|
end
|
189
153
|
|
190
154
|
def normalize_id_value(klass, property, value)
|
191
|
-
return
|
192
|
-
|
193
|
-
|
194
|
-
return value[0..14] if properties.include?(property.name)
|
195
|
-
end
|
196
|
-
value
|
155
|
+
return nil unless value
|
156
|
+
properties = Array(klass.send(:salesforce_id_properties)).map { |p| p.to_sym } rescue []
|
157
|
+
return properties.include?(property.name) ? value[0..14] : value
|
197
158
|
end
|
198
159
|
end
|
199
160
|
end
|
@@ -1,9 +1,4 @@
|
|
1
|
-
|
2
|
-
require 'soap/header/simplehandler'
|
3
|
-
require "rexml/element"
|
4
|
-
require 'dm-salesforce/soap_wrapper'
|
5
|
-
|
6
|
-
module DataMapperSalesforce
|
1
|
+
module DataMapper::Salesforce
|
7
2
|
class Connection
|
8
3
|
class HeaderHandler < SOAP::Header::SimpleHandler
|
9
4
|
def initialize(tag, value)
|
@@ -16,7 +11,7 @@ module DataMapperSalesforce
|
|
16
11
|
end
|
17
12
|
end
|
18
13
|
|
19
|
-
def initialize(username, password, wsdl_path, organization_id = nil)
|
14
|
+
def initialize(username, password, wsdl_path, api_dir, organization_id = nil)
|
20
15
|
@wrapper = SoapWrapper.new("SalesforceAPI", "Soap", wsdl_path, api_dir)
|
21
16
|
@username, @password, @organization_id = URI.unescape(username), password, organization_id
|
22
17
|
login
|
@@ -27,14 +22,17 @@ module DataMapperSalesforce
|
|
27
22
|
@wrapper.wsdl_path
|
28
23
|
end
|
29
24
|
|
25
|
+
def api_dir
|
26
|
+
@wrapper.api_dir
|
27
|
+
end
|
28
|
+
|
30
29
|
def organization_id
|
31
30
|
@user_details && @user_details.organizationId
|
32
31
|
end
|
33
32
|
|
34
33
|
def make_object(klass_name, values)
|
35
|
-
|
36
|
-
|
37
|
-
values.each do |property,value|
|
34
|
+
obj = SalesforceAPI.const_get(klass_name).new
|
35
|
+
values.each do |property, value|
|
38
36
|
field = field_name_for(klass_name, property)
|
39
37
|
if value.nil? or value == ""
|
40
38
|
obj.fieldsToNull.push(field)
|
@@ -81,9 +79,6 @@ module DataMapperSalesforce
|
|
81
79
|
end
|
82
80
|
|
83
81
|
private
|
84
|
-
def api_dir
|
85
|
-
ENV["SALESFORCE_DIR"] || "#{ENV["HOME"]}/.salesforce"
|
86
|
-
end
|
87
82
|
|
88
83
|
def driver
|
89
84
|
@wrapper.driver
|
@@ -98,7 +93,7 @@ module DataMapperSalesforce
|
|
98
93
|
begin
|
99
94
|
result = driver.login(:username => @username, :password => @password).result
|
100
95
|
rescue SOAP::FaultError => error
|
101
|
-
if error.faultcode.to_obj == "sf:INVALID_LOGIN"
|
96
|
+
if error.faultcode.to_obj == "sf:INVALID_LOGIN" || error.faultcode.to_obj == "INVALID_LOGIN"
|
102
97
|
raise LoginFailed, "Could not login to Salesforce; #{error.faultstring.text}"
|
103
98
|
else
|
104
99
|
raise
|
@@ -141,5 +136,3 @@ module DataMapperSalesforce
|
|
141
136
|
end
|
142
137
|
end
|
143
138
|
end
|
144
|
-
|
145
|
-
require 'dm-salesforce/connection/errors'
|
@@ -1,4 +1,4 @@
|
|
1
|
-
module
|
1
|
+
module DataMapper::Salesforce
|
2
2
|
class Connection
|
3
3
|
class Error < StandardError; end
|
4
4
|
class FieldNotFound < Error; end
|
@@ -35,6 +35,12 @@ module DataMapperSalesforce
|
|
35
35
|
def message_for_record(record)
|
36
36
|
record.errors.map {|e| "#{e.statusCode}: #{e.message}"}.join(", ")
|
37
37
|
end
|
38
|
+
|
39
|
+
def server_unavailable?
|
40
|
+
failed_records.any? do |record|
|
41
|
+
record.errors.any? {|e| e.statusCode == "SERVER_UNAVAILABLE"}
|
42
|
+
end
|
43
|
+
end
|
38
44
|
end
|
39
45
|
class CreateError < SOAPError; end
|
40
46
|
class QueryError < SOAPError; end
|
@@ -1,15 +1,14 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
module DataMapperSalesforce
|
1
|
+
module DataMapper::Salesforce
|
4
2
|
class SoapWrapper
|
5
3
|
class ClassesFailedToGenerate < StandardError; end
|
6
4
|
|
5
|
+
attr_reader :module_name, :driver_name, :wsdl_path, :api_dir
|
6
|
+
|
7
7
|
def initialize(module_name, driver_name, wsdl_path, api_dir)
|
8
8
|
@module_name, @driver_name, @wsdl_path, @api_dir = module_name, driver_name, File.expand_path(wsdl_path), File.expand_path(api_dir)
|
9
9
|
generate_soap_classes
|
10
10
|
driver
|
11
11
|
end
|
12
|
-
attr_reader :module_name, :driver_name, :wsdl_path, :api_dir
|
13
12
|
|
14
13
|
def driver
|
15
14
|
@driver ||= Object.const_get(module_name).const_get(driver_name).new
|
@@ -24,31 +23,37 @@ module DataMapperSalesforce
|
|
24
23
|
FileUtils.mkdir_p wsdl_api_dir
|
25
24
|
end
|
26
25
|
|
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
|
-
(Dir["*.rb"] - files).each do |filename|
|
36
|
-
FileUtils.rm(filename)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
26
|
+
generate_files unless files_exist?
|
40
27
|
|
41
28
|
$:.push wsdl_api_dir
|
42
29
|
require "#{module_name}Driver"
|
43
30
|
$:.delete wsdl_api_dir
|
44
31
|
end
|
45
32
|
|
46
|
-
|
47
|
-
|
33
|
+
# Good candidate for shipping out into a Rakefile.
|
34
|
+
def generate_files
|
35
|
+
require 'wsdl/soap/wsdl2ruby'
|
36
|
+
|
37
|
+
wsdl2ruby = WSDL::SOAP::WSDL2Ruby.new
|
38
|
+
wsdl2ruby.logger = $LOG if $LOG
|
39
|
+
wsdl2ruby.location = wsdl_path
|
40
|
+
wsdl2ruby.basedir = wsdl_api_dir
|
41
|
+
|
42
|
+
wsdl2ruby.opt.merge!({
|
43
|
+
'classdef' => module_name,
|
44
|
+
'module_path' => module_name,
|
45
|
+
'mapping_registry' => nil,
|
46
|
+
'driver' => nil,
|
47
|
+
'client_skelton' => nil,
|
48
|
+
})
|
49
|
+
|
50
|
+
wsdl2ruby.run
|
51
|
+
|
52
|
+
raise ClassesFailedToGenerate unless files_exist?
|
48
53
|
end
|
49
54
|
|
50
55
|
def files_exist?
|
51
|
-
|
56
|
+
["#{module_name}.rb", "#{module_name}MappingRegistry.rb", "#{module_name}Driver.rb"].all? do |name|
|
52
57
|
File.exist?("#{wsdl_api_dir}/#{name}")
|
53
58
|
end
|
54
59
|
end
|
data/lib/dm-salesforce/sql.rb
CHANGED
@@ -1,59 +1,87 @@
|
|
1
|
-
module
|
1
|
+
module DataMapper::Salesforce
|
2
2
|
module SQL
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
3
|
+
def from_condition(condition, repository)
|
4
|
+
slug = condition.class.slug
|
5
|
+
condition = case condition
|
6
|
+
when DataMapper::Query::Conditions::AbstractOperation then condition.operands.first
|
7
|
+
when DataMapper::Query::Conditions::AbstractComparison
|
8
|
+
if condition.subject.kind_of?(DataMapper::Associations::Relationship)
|
9
|
+
foreign_key_conditions(condition)
|
10
|
+
else
|
11
|
+
condition
|
12
|
+
end
|
13
|
+
else raise("Unkown condition type #{condition.class}: #{condition.inspect}")
|
14
|
+
end
|
26
15
|
|
27
|
-
|
28
|
-
|
16
|
+
value = condition.value
|
17
|
+
prop = condition.subject
|
18
|
+
operator = case slug
|
19
|
+
when String then operator
|
20
|
+
when :eql, :in then equality_operator(value)
|
21
|
+
when :not then inequality_operator(value)
|
22
|
+
when :like then "LIKE #{quote_value(value)}"
|
23
|
+
when :gt then "> #{quote_value(value)}"
|
24
|
+
when :gte then ">= #{quote_value(value)}"
|
25
|
+
when :lt then "< #{quote_value(value)}"
|
26
|
+
when :lte then "<= #{quote_value(value)}"
|
27
|
+
else raise "CAN HAS CRASH?"
|
28
|
+
end
|
29
|
+
case prop
|
30
|
+
when DataMapper::Property
|
31
|
+
"#{prop.field} #{operator}"
|
32
|
+
when DataMapper::Query::Path
|
33
|
+
rels = prop.relationships
|
34
|
+
names = rels.map {|r| storage_name(r, repository) }.join(".")
|
35
|
+
"#{names}.#{prop.field} #{operator}"
|
29
36
|
end
|
37
|
+
end
|
30
38
|
|
31
|
-
|
32
|
-
|
39
|
+
def foreign_key_conditions(condition)
|
40
|
+
subject = case condition.subject
|
41
|
+
when DataMapper::Associations::ManyToOne::Relationship
|
42
|
+
condition.subject.child_key.first
|
43
|
+
else
|
44
|
+
condition.subject.parent_key.first
|
45
|
+
end
|
46
|
+
|
47
|
+
case condition.value
|
48
|
+
when Array, DataMapper::Collection
|
49
|
+
value = condition.send(:expected).flatten
|
50
|
+
DataMapper::Query::Conditions::InclusionComparison.new(subject, value)
|
51
|
+
else
|
52
|
+
value = condition.send(:expected)
|
53
|
+
DataMapper::Query::Conditions::EqualToComparison.new(subject, value)
|
33
54
|
end
|
55
|
+
end
|
34
56
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
57
|
+
def storage_name(rel, repository)
|
58
|
+
rel.parent_model.storage_name(repository.name)
|
59
|
+
end
|
60
|
+
|
61
|
+
def order(direction)
|
62
|
+
"#{direction.target.field} #{direction.operator.to_s.upcase}"
|
63
|
+
end
|
64
|
+
|
65
|
+
def equality_operator(value)
|
66
|
+
case value
|
67
|
+
when Array then "IN #{quote_value(value)}"
|
68
|
+
else "= #{quote_value(value)}"
|
41
69
|
end
|
70
|
+
end
|
42
71
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
end
|
72
|
+
def inequality_operator(value)
|
73
|
+
case value
|
74
|
+
when Array then "NOT IN #{quote_value(value)}"
|
75
|
+
else "!= #{quote_value(value)}"
|
48
76
|
end
|
77
|
+
end
|
49
78
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
end
|
79
|
+
def quote_value(value)
|
80
|
+
case value
|
81
|
+
when Array then "(#{value.map {|v| quote_value(v)}.join(", ")})"
|
82
|
+
when NilClass then "NULL"
|
83
|
+
when String then "'#{value.gsub(/'/, "\\'").gsub(/\\/, %{\\\\})}'"
|
84
|
+
else "#{value}"
|
57
85
|
end
|
58
86
|
end
|
59
87
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module DataMapper::Salesforce
|
2
|
+
class Type < ::DataMapper::Type
|
3
|
+
end
|
4
|
+
|
5
|
+
module Types
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
dir = File.expand_path(File.dirname(__FILE__) / :types)
|
10
|
+
|
11
|
+
require dir / :serial
|
12
|
+
require dir / :boolean
|
13
|
+
require dir / :integer
|
14
|
+
require dir / :float
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module DataMapper::Salesforce
|
2
|
+
module Types
|
3
|
+
class Boolean < Type
|
4
|
+
primitive String
|
5
|
+
default false
|
6
|
+
|
7
|
+
def self.dump(value, property)
|
8
|
+
case value
|
9
|
+
when nil, false then '0'
|
10
|
+
else value
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.load(value, property)
|
15
|
+
case value
|
16
|
+
when TrueClass then value
|
17
|
+
when '1', 'true' then true
|
18
|
+
else false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -1,3 +1,3 @@
|
|
1
|
-
module
|
2
|
-
VERSION = "0.
|
1
|
+
module DataMapper::Salesforce
|
2
|
+
VERSION = "0.10.1"
|
3
3
|
end
|
metadata
CHANGED
@@ -1,16 +1,17 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dm-salesforce
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yehuda Katz
|
8
8
|
- Tim Carey-Smith
|
9
|
+
- Corey Donohoe
|
9
10
|
autorequire:
|
10
11
|
bindir: bin
|
11
12
|
cert_chain: []
|
12
13
|
|
13
|
-
date: 2009-
|
14
|
+
date: 2009-12-17 00:00:00 -08:00
|
14
15
|
default_executable:
|
15
16
|
dependencies:
|
16
17
|
- !ruby/object:Gem::Dependency
|
@@ -41,7 +42,7 @@ dependencies:
|
|
41
42
|
requirements:
|
42
43
|
- - ~>
|
43
44
|
- !ruby/object:Gem::Version
|
44
|
-
version: 0.
|
45
|
+
version: 0.10.1
|
45
46
|
version:
|
46
47
|
- !ruby/object:Gem::Dependency
|
47
48
|
name: dm-validations
|
@@ -51,40 +52,40 @@ dependencies:
|
|
51
52
|
requirements:
|
52
53
|
- - ~>
|
53
54
|
- !ruby/object:Gem::Version
|
54
|
-
version: 0.
|
55
|
+
version: 0.10.1
|
55
56
|
version:
|
56
57
|
- !ruby/object:Gem::Dependency
|
57
|
-
name:
|
58
|
+
name: dm-types
|
58
59
|
type: :runtime
|
59
60
|
version_requirement:
|
60
61
|
version_requirements: !ruby/object:Gem::Requirement
|
61
62
|
requirements:
|
62
63
|
- - ~>
|
63
64
|
- !ruby/object:Gem::Version
|
64
|
-
version:
|
65
|
+
version: 0.10.1
|
65
66
|
version:
|
66
67
|
- !ruby/object:Gem::Dependency
|
67
|
-
name:
|
68
|
+
name: do_sqlite3
|
68
69
|
type: :runtime
|
69
70
|
version_requirement:
|
70
71
|
version_requirements: !ruby/object:Gem::Requirement
|
71
72
|
requirements:
|
72
73
|
- - ~>
|
73
74
|
- !ruby/object:Gem::Version
|
74
|
-
version: 0.
|
75
|
+
version: 0.10.0
|
75
76
|
version:
|
76
77
|
- !ruby/object:Gem::Dependency
|
77
|
-
name:
|
78
|
+
name: soap4r
|
78
79
|
type: :runtime
|
79
80
|
version_requirement:
|
80
81
|
version_requirements: !ruby/object:Gem::Requirement
|
81
82
|
requirements:
|
82
83
|
- - ~>
|
83
84
|
- !ruby/object:Gem::Version
|
84
|
-
version:
|
85
|
+
version: 1.5.8
|
85
86
|
version:
|
86
87
|
description: A DataMapper adapter to the Salesforce API
|
87
|
-
email:
|
88
|
+
email: ninja@engineyard.com
|
88
89
|
executables: []
|
89
90
|
|
90
91
|
extensions: []
|
@@ -99,13 +100,18 @@ files:
|
|
99
100
|
- lib/dm-salesforce/adapter.rb
|
100
101
|
- lib/dm-salesforce/connection/errors.rb
|
101
102
|
- lib/dm-salesforce/connection.rb
|
102
|
-
- lib/dm-salesforce/
|
103
|
+
- lib/dm-salesforce/resource.rb
|
103
104
|
- lib/dm-salesforce/soap_wrapper.rb
|
104
105
|
- lib/dm-salesforce/sql.rb
|
106
|
+
- lib/dm-salesforce/types/boolean.rb
|
107
|
+
- lib/dm-salesforce/types/float.rb
|
108
|
+
- lib/dm-salesforce/types/integer.rb
|
109
|
+
- lib/dm-salesforce/types/serial.rb
|
110
|
+
- lib/dm-salesforce/types.rb
|
105
111
|
- lib/dm-salesforce/version.rb
|
106
112
|
- lib/dm-salesforce.rb
|
107
113
|
has_rdoc: true
|
108
|
-
homepage: http://
|
114
|
+
homepage: http://github.com/halorgium/dm-salesforce
|
109
115
|
licenses: []
|
110
116
|
|
111
117
|
post_install_message:
|
@@ -1,45 +0,0 @@
|
|
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
|