soapforce 0.1.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/.gitignore +18 -0
- data/.travis.yml +9 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +145 -0
- data/Rakefile +9 -0
- data/lib/soapforce.rb +12 -0
- data/lib/soapforce/client.rb +333 -0
- data/lib/soapforce/configuration.rb +20 -0
- data/lib/soapforce/query_result.rb +42 -0
- data/lib/soapforce/sobject.rb +52 -0
- data/lib/soapforce/version.rb +3 -0
- data/resources/partner.wsdl.xml +3657 -0
- data/soapforce.gemspec +33 -0
- data/spec/fixtures/create_response.xml +11 -0
- data/spec/fixtures/delete_response.xml +11 -0
- data/spec/fixtures/describe_global_response.xml +55 -0
- data/spec/fixtures/describe_s_object_response.xml +9 -0
- data/spec/fixtures/describe_s_objects_response.xml +9 -0
- data/spec/fixtures/get_user_info_response.xml +32 -0
- data/spec/fixtures/login_response.xml +39 -0
- data/spec/fixtures/org_id_response.xml +17 -0
- data/spec/fixtures/query_all_response.xml +26 -0
- data/spec/fixtures/query_more_response.xml +26 -0
- data/spec/fixtures/query_response.xml +26 -0
- data/spec/fixtures/retrieve_response.xml +14 -0
- data/spec/fixtures/search_response.xml +8 -0
- data/spec/fixtures/update_response.xml +11 -0
- data/spec/fixtures/upsert_response.xml +17 -0
- data/spec/lib/client_spec.rb +301 -0
- data/spec/lib/configuration_spec.rb +28 -0
- data/spec/lib/query_result_spec.rb +79 -0
- data/spec/lib/sobject_spec.rb +44 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/support/fixture_helpers.rb +58 -0
- data/test.rb +58 -0
- metadata +167 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Joe Heth
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
# Soapforce
|
2
|
+
|
3
|
+
|
4
|
+
[](https://travis-ci.org/TinderBox/soapforce)
|
5
|
+
|
6
|
+
|
7
|
+
Soapforce is a ruby gem for the [Salesforce SOAP API](http://www.salesforce.com/us/developer/docs/api/index.htm).
|
8
|
+
This gem was modeled after the [restforce](https://github.com/ejholmes/restforce) gem and depends on [Savon 2](http://savonrb.com/version2/).
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add this line to your application's Gemfile:
|
13
|
+
|
14
|
+
gem 'soapforce'
|
15
|
+
|
16
|
+
Or to get the latest changes from the source:
|
17
|
+
|
18
|
+
gem 'soapforce', :git => "git://github.com/TinderBox/soapforce.git"
|
19
|
+
|
20
|
+
And then execute:
|
21
|
+
|
22
|
+
$ bundle
|
23
|
+
|
24
|
+
Or install it yourself as:
|
25
|
+
|
26
|
+
$ gem install soapforce
|
27
|
+
|
28
|
+
|
29
|
+
## Usage
|
30
|
+
|
31
|
+
For ISV Partners you can specify your client_id in a configuration block which will get included in the CallOptions header of every request.
|
32
|
+
|
33
|
+
# config/initializers/soapforce.rb
|
34
|
+
# This is your ISV Partner Client ID.
|
35
|
+
# It needs to be whitelisted to enable SOAP requests in Professional and Group Editions.
|
36
|
+
Soapforce.configure do |config|
|
37
|
+
config.client_id = "ParterName/SomeValue/"
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
#### Username/Password authentication
|
42
|
+
|
43
|
+
If you prefer to use a username and password to authenticate:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
client = Soapforce::Client.new
|
47
|
+
client.authenticate(:username => 'foo', :password => 'password_and_security_token')
|
48
|
+
```
|
49
|
+
|
50
|
+
#### Session authentication
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
client = Soapforce::Client.new
|
54
|
+
client.authenticate(:session_id => 'session id', :server_url => 'server url')
|
55
|
+
```
|
56
|
+
|
57
|
+
### find
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
client.find('Account', '006A000000Lbiiz')
|
61
|
+
# => #<Soapforce::SObject Id="006A000000Lbiiz" Name="Test" LastModifiedBy="005G0000003f1ABPIN" ... >
|
62
|
+
|
63
|
+
client.find('Account', '1234', 'Some_External_Id_Field__c')
|
64
|
+
# => #<Soapforce::SObject Id="006A000000Lbiiz" Name="Test" LastModifiedBy="005G0000003f1ABPIN" ... >
|
65
|
+
```
|
66
|
+
|
67
|
+
### find_where
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
client.find_where('Account', Name: "Test")
|
71
|
+
# => [#<Soapforce::SObject Id="006A000000Lbiiz" Name="Test" LastModifiedBy="005G0000003f1ABPIN" ... >]
|
72
|
+
|
73
|
+
client.find_where('Account', Some_External_Id_Field__c: 1, ["Id", "Name, "CreatedBy"])
|
74
|
+
# => [#<Soapforce::SObject Id="006A000000Lbiiz" Name="Test" CreatedBy="005G0000003f1ABPIN" ... >]
|
75
|
+
```
|
76
|
+
|
77
|
+
### search
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
# Find all occurrences of 'bar'
|
81
|
+
client.search('FIND {bar}')
|
82
|
+
# => #[<Hash>]
|
83
|
+
```
|
84
|
+
|
85
|
+
### create
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
# Add a new account
|
89
|
+
client.create('Account', Name: 'Foobar Inc.')
|
90
|
+
# => {id: '006A000000Lbiiz', success: => true}
|
91
|
+
```
|
92
|
+
|
93
|
+
### update
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
# Update the Account with Id '006A000000Lbiiz'
|
97
|
+
client.update('Account', Id: '006A000000Lbiiz', Name: 'Whizbang Corp')
|
98
|
+
# => {id: '006A000000Lbiiz', success: => true}
|
99
|
+
```
|
100
|
+
|
101
|
+
### upsert
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
# Update the record with external ID of 12
|
105
|
+
client.upsert('Account', 'External__c', External__c: 12, Name: 'Foobar')
|
106
|
+
# => {id: '006A000000Lbiiz', success: => true, created: false}
|
107
|
+
```
|
108
|
+
|
109
|
+
### destroy
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
# Delete the Account with Id '006A000000Lbiiz'
|
113
|
+
client.destroy('Account', '006A000000Lbiiz')
|
114
|
+
# => {id: '0016000000MRatd', success: => true}
|
115
|
+
```
|
116
|
+
|
117
|
+
### describe
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
# get the global describe for all sobjects
|
121
|
+
client.describe_global
|
122
|
+
# => { ... }
|
123
|
+
|
124
|
+
# get the describe for the Account object
|
125
|
+
client.describe('Account')
|
126
|
+
# => { ... }
|
127
|
+
|
128
|
+
# get the describe for Account and Opportunity object
|
129
|
+
client.describe(['Account', 'Opportunity'])
|
130
|
+
# => [{ ... },{ ... }]
|
131
|
+
```
|
132
|
+
|
133
|
+
### logout
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
client.logout
|
137
|
+
```
|
138
|
+
|
139
|
+
## Contributing
|
140
|
+
|
141
|
+
1. Fork it
|
142
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
143
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
144
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
145
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/lib/soapforce.rb
ADDED
@@ -0,0 +1,333 @@
|
|
1
|
+
module Soapforce
|
2
|
+
class Client
|
3
|
+
|
4
|
+
attr_reader :client
|
5
|
+
attr_reader :headers
|
6
|
+
|
7
|
+
# The partner.wsdl is used by default but can be changed by passing in a new :wsdl option.
|
8
|
+
# A client_id can be
|
9
|
+
def initialize(options={})
|
10
|
+
@describe_cache = {}
|
11
|
+
@headers = {}
|
12
|
+
@wsdl = options[:wsdl] || File.dirname(__FILE__) + "/../../resources/partner.wsdl.xml"
|
13
|
+
|
14
|
+
# If a client_id is provided then it needs to be included
|
15
|
+
# in the header for every request. This allows ISV Partners
|
16
|
+
# to make SOAP calls in Professional/Group Edition organizations.
|
17
|
+
|
18
|
+
client_id = options[:client_id] || Soapforce.configuration.client_id
|
19
|
+
@headers = {"tns:CallOptions" => {"tns:client" => client_id}} if client_id
|
20
|
+
|
21
|
+
@client = Savon.client(
|
22
|
+
wsdl: @wsdl,
|
23
|
+
soap_header: @headers,
|
24
|
+
convert_request_keys_to: :none,
|
25
|
+
pretty_print_xml: true
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Public: Get the names of all wsdl operations.
|
30
|
+
# List all available operations from the partner.wsdl
|
31
|
+
def operations
|
32
|
+
@client.operations
|
33
|
+
end
|
34
|
+
|
35
|
+
# Public: Get the names of all wsdl operations.
|
36
|
+
#
|
37
|
+
# Supports a username/password (with token) combination or session_id/server_url pair.
|
38
|
+
#
|
39
|
+
# Examples
|
40
|
+
#
|
41
|
+
# client.login(username: 'test', password: 'password_and_token')
|
42
|
+
# # => {...}
|
43
|
+
#
|
44
|
+
# client.login(session_id: 'abcd1234', server_url: 'https://na1.salesforce.com/')
|
45
|
+
# # => {...}
|
46
|
+
#
|
47
|
+
# Returns Hash of login response and user info
|
48
|
+
def login(options={})
|
49
|
+
result = nil
|
50
|
+
if options[:username] && options[:password]
|
51
|
+
response = @client.call(:login) do |locals|
|
52
|
+
locals.message :username => options[:username], :password => options[:password]
|
53
|
+
end
|
54
|
+
|
55
|
+
result = response.to_hash[:login_response][:result]
|
56
|
+
returned_endpoint = result[:server_url]
|
57
|
+
|
58
|
+
@session_id = result[:session_id]
|
59
|
+
@server_url = result[:server_url]
|
60
|
+
elsif options[:session_id] && options[:server_url]
|
61
|
+
@session_id = options[:session_id]
|
62
|
+
@server_url = options[:server_url]
|
63
|
+
else
|
64
|
+
raise ArgumentError.new("Must provide username/password or session_id/server_url.")
|
65
|
+
end
|
66
|
+
|
67
|
+
@headers = @headers.merge({"tns:SessionHeader" => {"tns:sessionId" => @session_id}})
|
68
|
+
|
69
|
+
@client = Savon.client(
|
70
|
+
wsdl: @wsdl,
|
71
|
+
soap_header: @headers,
|
72
|
+
convert_request_keys_to: :none,
|
73
|
+
endpoint: @server_url
|
74
|
+
)
|
75
|
+
|
76
|
+
# If a session_id/server_url were passed in then invoke get_user_info for confirmation.
|
77
|
+
# Method missing to call_soap_api
|
78
|
+
result = self.get_user_info if options[:session_id]
|
79
|
+
|
80
|
+
result
|
81
|
+
end
|
82
|
+
alias_method :authenticate, :login
|
83
|
+
|
84
|
+
|
85
|
+
# Public: Get the names of all sobjects on the org.
|
86
|
+
#
|
87
|
+
# Examples
|
88
|
+
#
|
89
|
+
# # get the names of all sobjects on the org
|
90
|
+
# client.list_sobjects
|
91
|
+
# # => ['Account', 'Lead', ... ]
|
92
|
+
#
|
93
|
+
# Returns an Array of String names for each SObject.
|
94
|
+
def list_sobjects
|
95
|
+
response = describe_global # method_missing
|
96
|
+
response[:sobjects].collect { |sobject| sobject[:name] }
|
97
|
+
end
|
98
|
+
|
99
|
+
# Public: Get the current organization's Id.
|
100
|
+
#
|
101
|
+
# Examples
|
102
|
+
#
|
103
|
+
# client.org_id
|
104
|
+
# # => '00Dx0000000BV7z'
|
105
|
+
#
|
106
|
+
# Returns the String organization Id
|
107
|
+
def org_id
|
108
|
+
object = query('select id from Organization').first
|
109
|
+
if object && object[:id]
|
110
|
+
return object[:id].is_a?(Array) ? object[:id].first : object[:id]
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Public: Returns a detailed describe result for the specified sobject
|
115
|
+
#
|
116
|
+
# sobject - String name of the sobject.
|
117
|
+
#
|
118
|
+
# Examples
|
119
|
+
#
|
120
|
+
# # get the describe for the Account object
|
121
|
+
# client.describe('Account')
|
122
|
+
# # => { ... }
|
123
|
+
#
|
124
|
+
# # get the describe for the Account object
|
125
|
+
# client.describe(['Account', 'Contact'])
|
126
|
+
# # => { ... }
|
127
|
+
#
|
128
|
+
# Returns the Hash representation of the describe call.
|
129
|
+
def describe(sobject_type)
|
130
|
+
if sobject_type.is_a?(Array)
|
131
|
+
list = sobject_type.map do |type|
|
132
|
+
{:sObjectType => type}
|
133
|
+
end
|
134
|
+
response = call_soap_api(:describe_s_objects, :sObjectType => sobject_type)
|
135
|
+
else
|
136
|
+
# Cache objects to avoid repeat lookups.
|
137
|
+
if @describe_cache[sobject_type].nil?
|
138
|
+
response = call_soap_api(:describe_s_object, :sObjectType => sobject_type)
|
139
|
+
@describe_cache[sobject_type] = response
|
140
|
+
else
|
141
|
+
response = @describe_cache[sobject_type]
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
response
|
146
|
+
end
|
147
|
+
|
148
|
+
def query(soql)
|
149
|
+
result = call_soap_api(:query, {:queryString => soql})
|
150
|
+
QueryResult.new(result)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Includes deleted (isDeleted) or archived (isArchived) records
|
154
|
+
def query_all(soql)
|
155
|
+
result = call_soap_api(:query_all, {:queryString => soql})
|
156
|
+
QueryResult.new(result)
|
157
|
+
end
|
158
|
+
|
159
|
+
def query_more(locator)
|
160
|
+
result = call_soap_api(:query_more, {:queryLocator => locator})
|
161
|
+
QueryResult.new(result)
|
162
|
+
end
|
163
|
+
|
164
|
+
def search(sosl)
|
165
|
+
call_soap_api(:search, {:searchString => sosl})
|
166
|
+
end
|
167
|
+
|
168
|
+
def create(sobject_type, properties)
|
169
|
+
call_soap_api(:create, sobjects_hash(sobject_type, properties))
|
170
|
+
end
|
171
|
+
|
172
|
+
def update(sobject_type, properties)
|
173
|
+
call_soap_api(:update, sobjects_hash(sobject_type, properties))
|
174
|
+
end
|
175
|
+
|
176
|
+
def upsert(sobject_type, external_id_field_name, objects)
|
177
|
+
message = {externalIDFieldName: external_id_field_name}.merge(sobjects_hash(sobject_type, objects))
|
178
|
+
call_soap_api(:upsert, message)
|
179
|
+
end
|
180
|
+
|
181
|
+
def delete(id)
|
182
|
+
ids = id.is_a?(Array) ? id : [id]
|
183
|
+
call_soap_api(:delete, {:ids => ids})
|
184
|
+
end
|
185
|
+
|
186
|
+
# Public: Finds a single record and returns all fields.
|
187
|
+
#
|
188
|
+
# sobject - The String name of the sobject.
|
189
|
+
# id - The id of the record. If field is specified, id should be the id
|
190
|
+
# of the external field.
|
191
|
+
# field - External ID field to use (default: nil).
|
192
|
+
#
|
193
|
+
# Returns Hash of sobject record.
|
194
|
+
def find(sobject, id, field=nil)
|
195
|
+
if field.nil? || field.downcase == "id"
|
196
|
+
retrieve(sobject, id)
|
197
|
+
else
|
198
|
+
find_by_field(sobject, id, field)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# Public: Finds record based on where condition and returns all fields.
|
203
|
+
#
|
204
|
+
# sobject - The String name of the sobject.
|
205
|
+
# where - String where clause or Hash made up of field => value pairs.
|
206
|
+
# select - Optional array of field names to return.
|
207
|
+
#
|
208
|
+
# Returns Hash of sobject record.
|
209
|
+
def find_where(sobject, where={}, select_fields=[])
|
210
|
+
|
211
|
+
if where.is_a?(String)
|
212
|
+
where_clause = where
|
213
|
+
elsif where.is_a?(Hash)
|
214
|
+
conditions = []
|
215
|
+
where.each {|k,v|
|
216
|
+
# Wrap strings in single quotes.
|
217
|
+
v = v.is_a?(String) ? "'#{v}'" : v
|
218
|
+
v = 'NULL' if v.nil?
|
219
|
+
|
220
|
+
# Handle IN clauses when value is an array.
|
221
|
+
if v.is_a?(Array)
|
222
|
+
# Wrap single quotes around String values.
|
223
|
+
values = v.map {|s| s.is_a?(String) ? "'#{s}'" : s}.join(", ")
|
224
|
+
conditions << "#{k} IN (#{values})"
|
225
|
+
else
|
226
|
+
conditions << "#{k} = #{v}"
|
227
|
+
end
|
228
|
+
}
|
229
|
+
where_clause = conditions.join(" AND ")
|
230
|
+
|
231
|
+
end
|
232
|
+
|
233
|
+
# Get list of fields if none were specified.
|
234
|
+
if select_fields.empty?
|
235
|
+
field_names = field_list(sobject)
|
236
|
+
else
|
237
|
+
field_names = select_fields
|
238
|
+
end
|
239
|
+
|
240
|
+
soql = "Select #{field_names.join(", ")} From #{sobject} Where #{where_clause}"
|
241
|
+
result = query(soql)
|
242
|
+
end
|
243
|
+
|
244
|
+
# Public: Finds a single record and returns all fields.
|
245
|
+
#
|
246
|
+
# sobject - The String name of the sobject.
|
247
|
+
# id - The id of the record. If field is specified, id should be the id
|
248
|
+
# of the external field.
|
249
|
+
# field - External ID field to use.
|
250
|
+
#
|
251
|
+
# Returns Hash of sobject record.
|
252
|
+
def find_by_field(sobject, id, field_name)
|
253
|
+
field_details = field_details(sobject, field_name)
|
254
|
+
field_names = field_list(sobject).join(", ")
|
255
|
+
|
256
|
+
if ["int", "currency", "double", "boolean", "percent"].include?(field_details[:type])
|
257
|
+
search_value = id
|
258
|
+
else
|
259
|
+
# default to quoted value
|
260
|
+
search_value = "'#{id}'"
|
261
|
+
end
|
262
|
+
|
263
|
+
soql = "Select #{field_names} From #{sobject} Where #{field_name} = #{search_value}"
|
264
|
+
result = query(soql)
|
265
|
+
# Return first query result.
|
266
|
+
result ? result.first : nil
|
267
|
+
end
|
268
|
+
|
269
|
+
# Public: Finds a single record and returns all fields.
|
270
|
+
#
|
271
|
+
# sobject - The String name of the sobject.
|
272
|
+
# id - The id of the record. If field is specified, id should be the id
|
273
|
+
# of the external field.
|
274
|
+
#
|
275
|
+
# Returns Hash of sobject record.
|
276
|
+
def retrieve(sobject, id)
|
277
|
+
ids = id.is_a?(Array) ? id : [id]
|
278
|
+
sobject = call_soap_api(:retrieve, {fieldList: field_list(sobject).join(","), sObjectType: sobject, ids: ids})
|
279
|
+
sobject ? SObject.new(sobject) : nil
|
280
|
+
end
|
281
|
+
|
282
|
+
def field_list(sobject)
|
283
|
+
description = describe(sobject)
|
284
|
+
field_list = description[:fields].collect {|c| c[:name] }
|
285
|
+
end
|
286
|
+
|
287
|
+
def field_details(sobject, field_name)
|
288
|
+
description = describe(sobject)
|
289
|
+
fields = description[:fields]
|
290
|
+
fields.find {|f| field_name.downcase == f[:name].downcase }
|
291
|
+
end
|
292
|
+
|
293
|
+
# Supports the following No Argument methods:
|
294
|
+
# get_user_info
|
295
|
+
# describe_global
|
296
|
+
# describe_softphone_layout
|
297
|
+
# describe_tabs
|
298
|
+
# logout
|
299
|
+
# get_server_timestamp
|
300
|
+
def method_missing(method, *args)
|
301
|
+
call_soap_api(method, *args)
|
302
|
+
end
|
303
|
+
|
304
|
+
def call_soap_api(method, message_hash={})
|
305
|
+
|
306
|
+
response = @client.call(method.to_sym) do |locals|
|
307
|
+
locals.message message_hash
|
308
|
+
end
|
309
|
+
# Convert SOAP XML to Hash
|
310
|
+
response = response.to_hash
|
311
|
+
# Get Response Body
|
312
|
+
response_body = response["#{method}_response".to_sym]
|
313
|
+
# Grab result section if exists.
|
314
|
+
result = response_body ? response_body[:result] : nil
|
315
|
+
return result
|
316
|
+
end
|
317
|
+
|
318
|
+
def sobjects_hash(sobject_type, sobject_hash)
|
319
|
+
|
320
|
+
if sobject_hash.is_a?(Array)
|
321
|
+
sobjects = sobject_hash
|
322
|
+
else
|
323
|
+
sobjects = [sobject_hash]
|
324
|
+
end
|
325
|
+
|
326
|
+
sobjects.map! do |obj|
|
327
|
+
{"ins0:type" => sobject_type}.merge(obj)
|
328
|
+
end
|
329
|
+
|
330
|
+
{sObjects: sobjects}
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|