activesalesforce 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README +23 -0
- data/lib/column_definition.rb +82 -0
- data/lib/rforce.rb +226 -0
- data/lib/salesforce_active_record.rb +131 -0
- data/lib/salesforce_connection_adapter.rb +212 -0
- data/lib/salesforce_login.rb +39 -0
- data/lib/sobject_attributes.rb +132 -0
- data/test/unit/account_test.rb +42 -0
- data/test/unit/sobject_attributes_test.rb +38 -0
- metadata +71 -0
data/README
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
== Welcome to Active Salesforce
|
2
|
+
|
3
|
+
ActiveSalesforce is an extension to the Rails Framework that allows for the dynamic creation and management of
|
4
|
+
ActiveRecord objects through the use of Salesforce meta-data and uses a Salesforce.com organization as the backing store.
|
5
|
+
|
6
|
+
|
7
|
+
== Getting started
|
8
|
+
|
9
|
+
1. TBD Add in info on editing scripts/generate and scripts/server + database.yml
|
10
|
+
|
11
|
+
|
12
|
+
== Description of contents
|
13
|
+
|
14
|
+
lib
|
15
|
+
Application specific libraries. Basically, any kind of custom code that doesn't
|
16
|
+
belong under controllers, models, or helpers. This directory is in the load path.
|
17
|
+
|
18
|
+
script
|
19
|
+
Helper scripts for automation and generation.
|
20
|
+
|
21
|
+
test
|
22
|
+
Unit and functional tests along with fixtures.
|
23
|
+
|
@@ -0,0 +1,82 @@
|
|
1
|
+
=begin
|
2
|
+
ActiveSalesforce
|
3
|
+
Copyright (c) 2006 Doug Chasman
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
22
|
+
=end
|
23
|
+
|
24
|
+
require 'rubygems'
|
25
|
+
require_gem 'rails', ">= 1.0.0"
|
26
|
+
|
27
|
+
#require 'active_record/connection_adapters/abstract_adapter'
|
28
|
+
#require 'active_record/connection_adapters/abstract/schema_definitions'
|
29
|
+
|
30
|
+
module ActiveRecord
|
31
|
+
module ConnectionAdapters
|
32
|
+
class SalesforceColumn < Column
|
33
|
+
attr_reader :label, :readonly, :reference_to
|
34
|
+
|
35
|
+
def initialize(field)
|
36
|
+
@name = field[:name]
|
37
|
+
@type = get_type(field[:type])
|
38
|
+
@limit = field[:length]
|
39
|
+
@label = field[:label]
|
40
|
+
|
41
|
+
@text = [:string, :text].include? @type
|
42
|
+
@number = [:float, :integer].include? @type
|
43
|
+
|
44
|
+
@readonly = (field[:updateable] != "true" or field[:createable] != "true")
|
45
|
+
|
46
|
+
if field[:type] =~ /reference/i
|
47
|
+
@reference_to = field[:referenceTo]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def get_type(field_type)
|
52
|
+
case field_type
|
53
|
+
when /int/i
|
54
|
+
:integer
|
55
|
+
when /currency|percent/i
|
56
|
+
:float
|
57
|
+
when /datetime/i
|
58
|
+
:datetime
|
59
|
+
when /date/i
|
60
|
+
:date
|
61
|
+
when /id|string|textarea/i
|
62
|
+
:text
|
63
|
+
when /phone|fax|email|url/i
|
64
|
+
:string
|
65
|
+
when /blob|binary/i
|
66
|
+
:binary
|
67
|
+
when /boolean/i
|
68
|
+
:boolean
|
69
|
+
when /picklist/i
|
70
|
+
:text
|
71
|
+
when /reference/i
|
72
|
+
:text
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def human_name
|
77
|
+
@label
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/rforce.rb
ADDED
@@ -0,0 +1,226 @@
|
|
1
|
+
require 'net/https'
|
2
|
+
require 'uri'
|
3
|
+
require 'rexml/document'
|
4
|
+
require 'rexml/xpath'
|
5
|
+
require 'rubygems'
|
6
|
+
require_gem 'builder'
|
7
|
+
|
8
|
+
=begin
|
9
|
+
RForce v0.1
|
10
|
+
Copyright (c) 2005 Ian Dees
|
11
|
+
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
14
|
+
in the Software without restriction, including without limitation the rights
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
17
|
+
furnished to do so, subject to the following conditions:
|
18
|
+
|
19
|
+
The above copyright notice and this permission notice shall be included in
|
20
|
+
all copies or substantial portions of the Software.
|
21
|
+
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
28
|
+
SOFTWARE.
|
29
|
+
=end
|
30
|
+
|
31
|
+
# RForce is a simple Ruby binding to the SalesForce CRM system.
|
32
|
+
# Rather than enforcing adherence to the sforce.com schema,
|
33
|
+
# RForce assumes you are familiar with the API. Ruby method names
|
34
|
+
# become SOAP method names. Nested Ruby hashes become nested
|
35
|
+
# XML elements.
|
36
|
+
#
|
37
|
+
# Example:
|
38
|
+
#
|
39
|
+
# binding = RForce::Binding.new 'na1-api.salesforce.com'
|
40
|
+
# binding.login 'username', 'password'
|
41
|
+
# answer = binding.search(
|
42
|
+
# :searchString =>
|
43
|
+
# 'find {Some Account Name} in name fields returning account(id)')
|
44
|
+
# account_id = answer.searchResponse.result.searchRecords.record.Id
|
45
|
+
#
|
46
|
+
# opportunity = {
|
47
|
+
# :accountId => account_id,
|
48
|
+
# :amount => "10.00",
|
49
|
+
# :name => "New sale",
|
50
|
+
# :closeDate => "2005-09-01",
|
51
|
+
# :stageName => "Closed Won"
|
52
|
+
# }
|
53
|
+
#
|
54
|
+
# binding.create 'sObject {"xsi:type" => "Opportunity"}' => opportunity
|
55
|
+
#
|
56
|
+
module RForce
|
57
|
+
|
58
|
+
#Allows indexing hashes like method calls: hash.key
|
59
|
+
#to supplement the traditional way of indexing: hash[key]
|
60
|
+
module FlashHash
|
61
|
+
def method_missing(method, *args)
|
62
|
+
self[method]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
#Turns an XML response from the server into a Ruby
|
67
|
+
#object whose methods correspond to nested XML elements.
|
68
|
+
class SoapResponse
|
69
|
+
include FlashHash
|
70
|
+
|
71
|
+
#Parses an XML string into structured data.
|
72
|
+
def initialize(content)
|
73
|
+
document = REXML::Document.new content
|
74
|
+
node = REXML::XPath.first document, '//soapenv:Body'
|
75
|
+
@parsed = SoapResponse.parse node
|
76
|
+
end
|
77
|
+
|
78
|
+
#Allows this object to act like a hash (and therefore
|
79
|
+
#as a FlashHash via the include above).
|
80
|
+
def [](symbol)
|
81
|
+
@parsed[symbol]
|
82
|
+
end
|
83
|
+
|
84
|
+
#Digests an XML DOM node into nested Ruby types.
|
85
|
+
def SoapResponse.parse(node)
|
86
|
+
#Convert text nodes into simple strings.
|
87
|
+
return node.text unless node.has_elements?
|
88
|
+
|
89
|
+
#Convert nodes with children into FlashHashes.
|
90
|
+
elements = {}
|
91
|
+
class << elements
|
92
|
+
include FlashHash
|
93
|
+
end
|
94
|
+
|
95
|
+
#Add all the element's children to the hash.
|
96
|
+
node.each_element do |e|
|
97
|
+
name = e.name.to_sym
|
98
|
+
|
99
|
+
case elements[name]
|
100
|
+
#The most common case: unique child element tags.
|
101
|
+
when NilClass: elements[name] = parse(e)
|
102
|
+
|
103
|
+
#Non-unique child elements become arrays:
|
104
|
+
|
105
|
+
#We've already created the array: just
|
106
|
+
#add the element.
|
107
|
+
when Array: elements[name] << parse(e)
|
108
|
+
|
109
|
+
#We haven't created the array yet: do so,
|
110
|
+
#then put the existing element in, followed
|
111
|
+
#by the new one.
|
112
|
+
else
|
113
|
+
elements[name] = [elements[name]]
|
114
|
+
elements[name] << parse(e)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
return elements
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
#Implements the connection to the SalesForce server.
|
123
|
+
class Binding
|
124
|
+
#Fill in the guts of this typical SOAP envelope
|
125
|
+
#with the session ID and the body of the SOAP request.
|
126
|
+
Envelope = <<-HERE
|
127
|
+
<?xml version="1.0" encoding="utf-8" ?>
|
128
|
+
<env:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
129
|
+
xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"
|
130
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
131
|
+
<env:Header>
|
132
|
+
<SessionHeader>
|
133
|
+
<sessionId>%s</sessionId>
|
134
|
+
</SessionHeader>
|
135
|
+
</env:Header>
|
136
|
+
<env:Body>
|
137
|
+
%s
|
138
|
+
</env:Body>
|
139
|
+
</env:Envelope>
|
140
|
+
HERE
|
141
|
+
|
142
|
+
#Connect to the server securely.
|
143
|
+
def initialize(url)
|
144
|
+
@url = URI.parse(url)
|
145
|
+
@server = Net::HTTP.new(@url.host, @url.port)
|
146
|
+
@server.use_ssl = @url.scheme == 'https'
|
147
|
+
|
148
|
+
# run ruby with -d to see SOAP wiredumps.
|
149
|
+
@server.set_debug_output $stderr if $DEBUG
|
150
|
+
|
151
|
+
@session_id = ''
|
152
|
+
end
|
153
|
+
|
154
|
+
#Log in to the server and remember the session ID
|
155
|
+
#returned to us by SalesForce.
|
156
|
+
def login(user, pass)
|
157
|
+
response = call_remote(:login, [:username, user, :password, pass])
|
158
|
+
|
159
|
+
raise "Incorrect user name / password [#{response.fault}]" unless response.loginResponse
|
160
|
+
@session_id = response.loginResponse.result.sessionId
|
161
|
+
response
|
162
|
+
end
|
163
|
+
|
164
|
+
#Call a method on the remote server. Arguments can be
|
165
|
+
#a hash or (if order is important) an array of alternating
|
166
|
+
#keys and values.
|
167
|
+
def call_remote(method, args)
|
168
|
+
#Create XML text from the arguments.
|
169
|
+
expanded = ''
|
170
|
+
@builder = Builder::XmlMarkup.new(:target => expanded)
|
171
|
+
expand({method => args}, 'urn:partner.soap.sforce.com')
|
172
|
+
|
173
|
+
#Fill in the blanks of the SOAP envelope with our
|
174
|
+
#session ID and the expanded XML of our request.
|
175
|
+
request = (Envelope % [@session_id, expanded])
|
176
|
+
|
177
|
+
#Send the request to the server and read the response.
|
178
|
+
response = @server.post2(@url.path, request, {'SOAPAction' => method.to_s, 'content-type' => 'text/xml'})
|
179
|
+
SoapResponse.new(response.body)
|
180
|
+
end
|
181
|
+
|
182
|
+
#Turns method calls on this object into remote SOAP calls.
|
183
|
+
def method_missing(method, *args)
|
184
|
+
unless args.size == 1 && [Hash, Array].include?(args[0].class)
|
185
|
+
raise 'Expected 1 Hash or Array argument'
|
186
|
+
end
|
187
|
+
|
188
|
+
call_remote method, args[0]
|
189
|
+
end
|
190
|
+
|
191
|
+
#Expand Ruby data structures into XML.
|
192
|
+
def expand(args, xmlns = nil)
|
193
|
+
#Nest arrays: [:a, 1, :b, 2] => [[:a, 1], [:b, 2]]
|
194
|
+
if (args.class == Array)
|
195
|
+
args.each_index{|i| args[i, 2] = [args[i, 2]]}
|
196
|
+
end
|
197
|
+
|
198
|
+
args.each do |key, value|
|
199
|
+
attributes = xmlns ? {:xmlns => xmlns} : {}
|
200
|
+
|
201
|
+
#If the XML tag requires attributes,
|
202
|
+
#the tag name will contain a space
|
203
|
+
#followed by a string representation
|
204
|
+
#of a hash of attributes.
|
205
|
+
#
|
206
|
+
#e.g. 'sObject {"xsi:type" => "Opportunity"}'
|
207
|
+
#becomes <sObject xsi:type="Opportunity>...</sObject>
|
208
|
+
if key.is_a? String
|
209
|
+
key, modifier = key.split(' ', 2)
|
210
|
+
|
211
|
+
attributes.merge!(eval(modifier)) if modifier
|
212
|
+
end
|
213
|
+
|
214
|
+
#Create an XML element and fill it with this
|
215
|
+
#value's sub-items.
|
216
|
+
case value
|
217
|
+
when Hash, Array
|
218
|
+
@builder.tag!(key, attributes) do expand value; end
|
219
|
+
|
220
|
+
when String
|
221
|
+
@builder.tag!(key, attributes) { @builder.text! value }
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
=begin
|
2
|
+
ActiveSalesforce
|
3
|
+
Copyright (c) 2006 Doug Chasman
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
22
|
+
=end
|
23
|
+
|
24
|
+
require 'xsd/datatypes'
|
25
|
+
require 'soap/soap'
|
26
|
+
|
27
|
+
require File.dirname(__FILE__) + '/sobject_attributes'
|
28
|
+
|
29
|
+
|
30
|
+
module ActiveRecord
|
31
|
+
# Active Records will automatically record creation and/or update timestamps of database objects
|
32
|
+
# if fields of the names created_at/created_on or updated_at/updated_on are present. This module is
|
33
|
+
# automatically included, so you don't need to do that manually.
|
34
|
+
#
|
35
|
+
# This behavior can be turned off by setting <tt>ActiveRecord::Base.record_timestamps = false</tt>.
|
36
|
+
# This behavior can use GMT by setting <tt>ActiveRecord::Base.timestamps_gmt = true</tt>
|
37
|
+
module SalesforceRecord
|
38
|
+
include SOAP, XSD
|
39
|
+
|
40
|
+
NS1 = 'urn:partner.soap.sforce.com'
|
41
|
+
NS2 = "urn:sobject.partner.soap.sforce.com"
|
42
|
+
|
43
|
+
def self.append_features(base) # :nodoc:
|
44
|
+
super
|
45
|
+
|
46
|
+
base.class_eval do
|
47
|
+
alias_method :create, :create_with_sforce_api
|
48
|
+
alias_method :update, :update_with_sforce_api
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def create_with_sforce_api
|
53
|
+
return if not @attributes.changed?
|
54
|
+
puts "create_with_sforce_api creating #{self.class}"
|
55
|
+
connection.create(:sObjects => create_sobject())
|
56
|
+
end
|
57
|
+
|
58
|
+
def update_with_sforce_api
|
59
|
+
return if not @attributes.changed?
|
60
|
+
puts "update_with_sforce_api updating #{self.class}('#{self.Id}')"
|
61
|
+
connection.update(:sObjects => create_sobject())
|
62
|
+
end
|
63
|
+
|
64
|
+
def create_sobject()
|
65
|
+
fields = @attributes.changed_fields
|
66
|
+
|
67
|
+
sobj = [ 'type { :xmlns => "urn:sobject.partner.soap.sforce.com" }', self.class.name ]
|
68
|
+
sobj << 'Id { :xmlns => "urn:sobject.partner.soap.sforce.com" }' << self.Id if self.Id
|
69
|
+
|
70
|
+
# now add any changed fields
|
71
|
+
fieldValues = {}
|
72
|
+
fields.each do |fieldName|
|
73
|
+
value = @attributes[fieldName]
|
74
|
+
sobj << fieldName.to_sym << value if value
|
75
|
+
end
|
76
|
+
|
77
|
+
sobj
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
class Base
|
83
|
+
set_inheritance_column nil
|
84
|
+
lock_optimistically = false
|
85
|
+
record_timestamps = false
|
86
|
+
default_timezone = :utc
|
87
|
+
|
88
|
+
def after_initialize()
|
89
|
+
if not @attributes.is_a?(Salesforce::SObjectAttributes)
|
90
|
+
# Insure that SObjectAttributes is always used for our atttributes
|
91
|
+
originalAttributes = @attributes
|
92
|
+
|
93
|
+
@attributes = Salesforce::SObjectAttributes.new(connection.columns_map(self.class.table_name))
|
94
|
+
|
95
|
+
originalAttributes.each { |name, value| self[name] = value }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.table_name
|
100
|
+
class_name_of_active_record_descendant(self)
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.primary_key
|
104
|
+
"Id"
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.construct_finder_sql(options)
|
108
|
+
soql = "SELECT #{column_names.join(', ')} FROM #{table_name} "
|
109
|
+
add_conditions!(soql, options[:conditions])
|
110
|
+
soql
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.construct_conditions_from_arguments(attribute_names, arguments)
|
114
|
+
conditions = []
|
115
|
+
attribute_names.each_with_index { |name, idx| conditions << "#{name} #{attribute_condition(arguments[idx])} " }
|
116
|
+
[ conditions.join(" AND "), *arguments[0...attribute_names.length] ]
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.count(conditions = nil, joins = nil)
|
120
|
+
soql = "SELECT Id FROM #{table_name} "
|
121
|
+
add_conditions!(soql, conditions)
|
122
|
+
|
123
|
+
count_by_sql(soql)
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.count_by_sql(soql)
|
127
|
+
connection.select_all(soql, "#{name} Count").length
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
=begin
|
2
|
+
ActiveSalesforce
|
3
|
+
Copyright (c) 2006 Doug Chasman
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
22
|
+
=end
|
23
|
+
|
24
|
+
require 'rubygems'
|
25
|
+
require_gem 'rails', ">= 1.0.0"
|
26
|
+
|
27
|
+
require 'thread'
|
28
|
+
|
29
|
+
require File.dirname(__FILE__) + '/salesforce_login'
|
30
|
+
require File.dirname(__FILE__) + '/sobject_attributes'
|
31
|
+
require File.dirname(__FILE__) + '/salesforce_active_record'
|
32
|
+
require File.dirname(__FILE__) + '/column_definition'
|
33
|
+
|
34
|
+
ActiveRecord::Base.class_eval do
|
35
|
+
include ActiveRecord::SalesforceRecord
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
module ActiveRecord
|
40
|
+
class Base
|
41
|
+
@@cache = {}
|
42
|
+
|
43
|
+
# Establishes a connection to the database that's used by all Active Record objects.
|
44
|
+
def self.salesforce_connection(config) # :nodoc:
|
45
|
+
puts "Using Salesforce connection!"
|
46
|
+
|
47
|
+
config = config.symbolize_keys
|
48
|
+
|
49
|
+
url = config[:url].to_s
|
50
|
+
username = config[:username].to_s
|
51
|
+
password = config[:password].to_s
|
52
|
+
|
53
|
+
connection = @@cache["#{url}.#{username}.#{password}"]
|
54
|
+
|
55
|
+
if not connection
|
56
|
+
connection = SalesforceLogin.new(url, username, password).proxy
|
57
|
+
@@cache["#{url}.#{username}.#{password}"] = connection
|
58
|
+
puts "Created new connection for [#{url}, #{username}]"
|
59
|
+
end
|
60
|
+
|
61
|
+
#puts "connected to Salesforce as #{connection.getUserInfo(nil).result['userFullName']}"
|
62
|
+
|
63
|
+
ConnectionAdapters::SalesforceAdapter.new(connection, logger, [url, username, password], config)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
module ConnectionAdapters
|
69
|
+
class SalesforceError < StandardError
|
70
|
+
end
|
71
|
+
|
72
|
+
class SalesforceAdapter < AbstractAdapter
|
73
|
+
|
74
|
+
def initialize(connection, logger, connection_options, config)
|
75
|
+
super(connection, logger)
|
76
|
+
|
77
|
+
@connection_options, @config = connection_options, config
|
78
|
+
@columns_name_map = {}
|
79
|
+
end
|
80
|
+
|
81
|
+
def adapter_name #:nodoc:
|
82
|
+
'Salesforce'
|
83
|
+
end
|
84
|
+
|
85
|
+
def supports_migrations? #:nodoc:
|
86
|
+
false
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
# QUOTING ==================================================
|
91
|
+
|
92
|
+
def quote(value, column = nil)
|
93
|
+
if value.kind_of?(String) && column && column.type == :binary
|
94
|
+
s = column.class.string_to_binary(value).unpack("H*")[0]
|
95
|
+
"x'#{s}'"
|
96
|
+
else
|
97
|
+
super
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def quote_column_name(name) #:nodoc:
|
102
|
+
"`#{name}`"
|
103
|
+
end
|
104
|
+
|
105
|
+
def quote_string(string) #:nodoc:
|
106
|
+
string
|
107
|
+
end
|
108
|
+
|
109
|
+
def quoted_true
|
110
|
+
"TRUE"
|
111
|
+
end
|
112
|
+
|
113
|
+
def quoted_false
|
114
|
+
"FALSE"
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
# CONNECTION MANAGEMENT ====================================
|
119
|
+
|
120
|
+
def active?
|
121
|
+
true
|
122
|
+
end
|
123
|
+
|
124
|
+
def reconnect!
|
125
|
+
connect
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
# DATABASE STATEMENTS ======================================
|
130
|
+
|
131
|
+
def select_all(soql, name = nil) #:nodoc:
|
132
|
+
log(soql, name)
|
133
|
+
|
134
|
+
records = @connection.query(:queryString => soql).queryResponse.result.records
|
135
|
+
|
136
|
+
records = [ records ] unless records.is_a?(Array)
|
137
|
+
|
138
|
+
result = []
|
139
|
+
records.each do |record|
|
140
|
+
attributes = Salesforce::SObjectAttributes.new(columns_map(record[:type]), record)
|
141
|
+
result << attributes
|
142
|
+
end
|
143
|
+
|
144
|
+
result
|
145
|
+
end
|
146
|
+
|
147
|
+
def select_one(sql, name = nil) #:nodoc:
|
148
|
+
result = select_all(sql, name)
|
149
|
+
result.nil? ? nil : result.first
|
150
|
+
end
|
151
|
+
|
152
|
+
def create(sobject, name = nil) #:nodoc:
|
153
|
+
result = @connection.create(sobject).createResponse.result
|
154
|
+
|
155
|
+
raise SalesforceError, result.errors.message unless result.success == "true"
|
156
|
+
|
157
|
+
# @connection.affected_rows
|
158
|
+
end
|
159
|
+
|
160
|
+
def update(sobject, name = nil) #:nodoc:
|
161
|
+
result = @connection.update(sobject).updateResponse.result
|
162
|
+
|
163
|
+
raise SalesforceError, result.errors.message unless result.success == "true"
|
164
|
+
|
165
|
+
# @connection.affected_rows
|
166
|
+
end
|
167
|
+
|
168
|
+
alias_method :delete, :update
|
169
|
+
|
170
|
+
def columns(table_name, name = nil)
|
171
|
+
columns = []
|
172
|
+
|
173
|
+
metadata = @connection.describeSObject(:sObjectType => table_name).describeSObjectResponse.result
|
174
|
+
|
175
|
+
metadata.fields.each do |field|
|
176
|
+
columns << SalesforceColumn.new(field)
|
177
|
+
end
|
178
|
+
|
179
|
+
columns
|
180
|
+
end
|
181
|
+
|
182
|
+
def columns_map(table_name, name = nil)
|
183
|
+
columns_map = @columns_name_map[table_name]
|
184
|
+
return columns_map if columns_map
|
185
|
+
|
186
|
+
columns_map = {}
|
187
|
+
@columns_name_map[table_name] = columns_map
|
188
|
+
|
189
|
+
columns(table_name).each { |column| columns_map[column.name] = column }
|
190
|
+
|
191
|
+
columns_map
|
192
|
+
end
|
193
|
+
|
194
|
+
private
|
195
|
+
|
196
|
+
def select(sql, name = nil)
|
197
|
+
puts "select(#{sql}, (#{name}))"
|
198
|
+
@connection.query_with_result = true
|
199
|
+
result = execute(sql, name)
|
200
|
+
rows = []
|
201
|
+
if @null_values_in_each_hash
|
202
|
+
result.each_hash { |row| rows << row }
|
203
|
+
else
|
204
|
+
all_fields = result.fetch_fields.inject({}) { |fields, f| fields[f.name] = nil; fields }
|
205
|
+
result.each_hash { |row| rows << all_fields.dup.update(row) }
|
206
|
+
end
|
207
|
+
result.free
|
208
|
+
rows
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
=begin
|
4
|
+
ActiveSalesforce
|
5
|
+
Copyright (c) 2006 Doug Chasman
|
6
|
+
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
9
|
+
in the Software without restriction, including without limitation the rights
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
12
|
+
furnished to do so, subject to the following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be included in
|
15
|
+
all copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
23
|
+
SOFTWARE.
|
24
|
+
=end
|
25
|
+
|
26
|
+
require File.dirname(__FILE__) + '/rforce.rb'
|
27
|
+
|
28
|
+
|
29
|
+
class SalesforceLogin
|
30
|
+
attr_reader :proxy
|
31
|
+
|
32
|
+
def initialize(url, username, password)
|
33
|
+
puts "SalesforceLogin.initialize()"
|
34
|
+
|
35
|
+
@proxy = RForce::Binding.new(url)
|
36
|
+
|
37
|
+
login_result = @proxy.login(username, password).result
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
=begin
|
2
|
+
ActiveSalesforce
|
3
|
+
Copyright (c) 2006 Doug Chasman
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
22
|
+
=end
|
23
|
+
|
24
|
+
require 'set'
|
25
|
+
|
26
|
+
|
27
|
+
module Salesforce
|
28
|
+
|
29
|
+
class SObjectAttributes
|
30
|
+
include Enumerable
|
31
|
+
|
32
|
+
def initialize(columns, record = nil)
|
33
|
+
@columns = columns
|
34
|
+
@values = {}
|
35
|
+
|
36
|
+
if record
|
37
|
+
record.each do |name, value|
|
38
|
+
# Replace nil element with nil
|
39
|
+
value = nil if value.respond_to?(:xmlattr_nil) and value.xmlattr_nil
|
40
|
+
|
41
|
+
# Ids are returned in an array with 2 duplicate entries...
|
42
|
+
value = value[0] if name == :Id
|
43
|
+
|
44
|
+
self[name.to_s] = value
|
45
|
+
end
|
46
|
+
else
|
47
|
+
columns.values.each { |column| self[column.name] = nil }
|
48
|
+
end
|
49
|
+
|
50
|
+
clear_changed!
|
51
|
+
end
|
52
|
+
|
53
|
+
def [](key)
|
54
|
+
@values[key].freeze
|
55
|
+
end
|
56
|
+
|
57
|
+
def []=(key, value)
|
58
|
+
column = @columns[key]
|
59
|
+
return unless column
|
60
|
+
|
61
|
+
value = nil if value == ""
|
62
|
+
|
63
|
+
return if @values[key] == value and @values.include?(key)
|
64
|
+
|
65
|
+
originalClass = value.class
|
66
|
+
originalValue = value
|
67
|
+
|
68
|
+
if value
|
69
|
+
# Convert strings representation of dates and datetimes to date and time objects
|
70
|
+
case column.type
|
71
|
+
when :date
|
72
|
+
value = value.is_a?(Date) ? value : Date.parse(value)
|
73
|
+
when :datetime
|
74
|
+
value = value.is_a?(Time) ? value : Time.parse(value)
|
75
|
+
else
|
76
|
+
value = column.type_cast(value)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
@values[key] = value
|
81
|
+
|
82
|
+
#puts "setting #{key} = #{value} [#{originalValue}] (#{originalClass}, #{value.class})"
|
83
|
+
|
84
|
+
if not column.readonly
|
85
|
+
@changed = Set.new unless @changed
|
86
|
+
@changed.add(key)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def include?(key)
|
91
|
+
@values.include?(key)
|
92
|
+
end
|
93
|
+
|
94
|
+
def has_key?(key)
|
95
|
+
@values.has_key?(key)
|
96
|
+
end
|
97
|
+
|
98
|
+
def length
|
99
|
+
@values.length
|
100
|
+
end
|
101
|
+
|
102
|
+
def keys
|
103
|
+
@values.keys
|
104
|
+
end
|
105
|
+
|
106
|
+
def clear
|
107
|
+
@values.clear
|
108
|
+
clear_changed
|
109
|
+
end
|
110
|
+
|
111
|
+
def clear_changed!
|
112
|
+
@changed = nil
|
113
|
+
end
|
114
|
+
|
115
|
+
def changed?
|
116
|
+
@changed != nil
|
117
|
+
end
|
118
|
+
|
119
|
+
def changed_fields
|
120
|
+
@changed
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
# Enumerable support
|
125
|
+
|
126
|
+
def each(&block)
|
127
|
+
@values.each(&block)
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '../../config/boot')
|
2
|
+
require '~/dev/activesfdc/trunk/ActiveSalesforce/src/salesforce_connection_adapter'
|
3
|
+
require File.dirname(__FILE__) + '/../test_helper'
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
class AccountTest < Test::Unit::TestCase
|
8
|
+
|
9
|
+
def test_get_account
|
10
|
+
products = Account.find(:all)
|
11
|
+
#pp products
|
12
|
+
|
13
|
+
products.each { |product| puts "#{product.Name}, #{product.Id}, #{product.LastModifiedById}, #{product.Description}" }
|
14
|
+
|
15
|
+
acme = Account.find(:first, :conditions => ["Name = 'Acme'"])
|
16
|
+
puts acme.Name
|
17
|
+
|
18
|
+
acme = Account.find_by_Id(acme.Id)
|
19
|
+
puts acme.Name
|
20
|
+
|
21
|
+
acme = Account.find_by_Name_and_LastModifiedById('salesforce.com', acme.LastModifiedById)
|
22
|
+
puts acme.Name
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_update_account
|
26
|
+
#return
|
27
|
+
|
28
|
+
acme = Account.find_by_Name('Acme')
|
29
|
+
puts acme.Name
|
30
|
+
|
31
|
+
acme.Website = "http://www.dutchforce.com/myImage2.jpg"
|
32
|
+
|
33
|
+
acme.save
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_xcreate_account
|
37
|
+
dutchCo = Account.new
|
38
|
+
dutchCo.Name = "DutchCo"
|
39
|
+
dutchCo.save
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
puts "Yahoo"
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
require File.dirname(__FILE__) + '/../../src/sobject_attributes'
|
5
|
+
|
6
|
+
class SobjectAttributesTest < Test::Unit::TestCase
|
7
|
+
|
8
|
+
def setup
|
9
|
+
@attributes = Salesforce::SObjectAttributes.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_add_values()
|
13
|
+
assert((not @attributes.changed?))
|
14
|
+
|
15
|
+
@attributes['name'] = 'value'
|
16
|
+
assert(@attributes.changed?)
|
17
|
+
|
18
|
+
assert_equal('value', @attributes['name'])
|
19
|
+
|
20
|
+
assert_equal(Set.new('name'), @attributes.changed_fields)
|
21
|
+
|
22
|
+
@attributes.clear_changed!
|
23
|
+
assert((not @attributes.changed?))
|
24
|
+
|
25
|
+
assert_equal('value', @attributes['name'])
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_enumeration
|
29
|
+
10.times { |n| @attributes["name_#{n}"] = "value_#{n}" }
|
30
|
+
|
31
|
+
assert_equal(10, @attributes.length)
|
32
|
+
|
33
|
+
5.times { |n| @attributes["name_#{n + 10}"] = "value_#{n + 10}" }
|
34
|
+
|
35
|
+
@attributes.each { |name, value| assert_equal(name[/_\d/], value[/_\d/]) }
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.8.11
|
3
|
+
specification_version: 1
|
4
|
+
name: activesalesforce
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.0.2
|
7
|
+
date: 2006-01-18 00:00:00 -05:00
|
8
|
+
summary: ActiveSalesforce is an extension to the Rails Framework that allows for the dynamic creation and management of ActiveRecord objects through the use of Salesforce meta-data and uses a Salesforce.com organization as the backing store.
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: dchasman@salesforce.com
|
12
|
+
homepage: http://rubyforge.org/projects/activesfdc/
|
13
|
+
rubyforge_project:
|
14
|
+
description:
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
authors:
|
29
|
+
- Doug Chasman
|
30
|
+
files:
|
31
|
+
- lib/salesforce_login.rb
|
32
|
+
- lib/salesforce_active_record.rb
|
33
|
+
- lib/column_definition.rb
|
34
|
+
- lib/rforce.rb
|
35
|
+
- lib/sobject_attributes.rb
|
36
|
+
- lib/salesforce_connection_adapter.rb
|
37
|
+
- test/unit
|
38
|
+
- test/unit/sobject_attributes_test.rb
|
39
|
+
- test/unit/account_test.rb
|
40
|
+
- README
|
41
|
+
test_files: []
|
42
|
+
|
43
|
+
rdoc_options: []
|
44
|
+
|
45
|
+
extra_rdoc_files:
|
46
|
+
- README
|
47
|
+
executables: []
|
48
|
+
|
49
|
+
extensions: []
|
50
|
+
|
51
|
+
requirements: []
|
52
|
+
|
53
|
+
dependencies:
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: rails
|
56
|
+
version_requirement:
|
57
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.0.0
|
62
|
+
version:
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: builder
|
65
|
+
version_requirement:
|
66
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: 1.2.4
|
71
|
+
version:
|