force 0.0.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.
Files changed (151) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +96 -0
  3. data/Gemfile +11 -0
  4. data/Gemfile.lock +107 -0
  5. data/Guardfile +8 -0
  6. data/LICENSE +22 -0
  7. data/README.md +421 -0
  8. data/Rakefile +10 -0
  9. data/coverage/assets/0.7.1/application.css +1110 -0
  10. data/coverage/assets/0.7.1/application.js +626 -0
  11. data/coverage/assets/0.7.1/fancybox/blank.gif +0 -0
  12. data/coverage/assets/0.7.1/fancybox/fancy_close.png +0 -0
  13. data/coverage/assets/0.7.1/fancybox/fancy_loading.png +0 -0
  14. data/coverage/assets/0.7.1/fancybox/fancy_nav_left.png +0 -0
  15. data/coverage/assets/0.7.1/fancybox/fancy_nav_right.png +0 -0
  16. data/coverage/assets/0.7.1/fancybox/fancy_shadow_e.png +0 -0
  17. data/coverage/assets/0.7.1/fancybox/fancy_shadow_n.png +0 -0
  18. data/coverage/assets/0.7.1/fancybox/fancy_shadow_ne.png +0 -0
  19. data/coverage/assets/0.7.1/fancybox/fancy_shadow_nw.png +0 -0
  20. data/coverage/assets/0.7.1/fancybox/fancy_shadow_s.png +0 -0
  21. data/coverage/assets/0.7.1/fancybox/fancy_shadow_se.png +0 -0
  22. data/coverage/assets/0.7.1/fancybox/fancy_shadow_sw.png +0 -0
  23. data/coverage/assets/0.7.1/fancybox/fancy_shadow_w.png +0 -0
  24. data/coverage/assets/0.7.1/fancybox/fancy_title_left.png +0 -0
  25. data/coverage/assets/0.7.1/fancybox/fancy_title_main.png +0 -0
  26. data/coverage/assets/0.7.1/fancybox/fancy_title_over.png +0 -0
  27. data/coverage/assets/0.7.1/fancybox/fancy_title_right.png +0 -0
  28. data/coverage/assets/0.7.1/fancybox/fancybox-x.png +0 -0
  29. data/coverage/assets/0.7.1/fancybox/fancybox-y.png +0 -0
  30. data/coverage/assets/0.7.1/fancybox/fancybox.png +0 -0
  31. data/coverage/assets/0.7.1/favicon_green.png +0 -0
  32. data/coverage/assets/0.7.1/favicon_red.png +0 -0
  33. data/coverage/assets/0.7.1/favicon_yellow.png +0 -0
  34. data/coverage/assets/0.7.1/loading.gif +0 -0
  35. data/coverage/assets/0.7.1/magnify.png +0 -0
  36. data/coverage/assets/0.7.1/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  37. data/coverage/assets/0.7.1/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
  38. data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  39. data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  40. data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  41. data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  42. data/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  43. data/coverage/assets/0.7.1/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  44. data/coverage/assets/0.7.1/smoothness/images/ui-icons_222222_256x240.png +0 -0
  45. data/coverage/assets/0.7.1/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
  46. data/coverage/assets/0.7.1/smoothness/images/ui-icons_454545_256x240.png +0 -0
  47. data/coverage/assets/0.7.1/smoothness/images/ui-icons_888888_256x240.png +0 -0
  48. data/coverage/assets/0.7.1/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
  49. data/coverage/index.html +19808 -0
  50. data/force.gemspec +27 -0
  51. data/lib/force.rb +74 -0
  52. data/lib/force/abstract_client.rb +9 -0
  53. data/lib/force/attachment.rb +21 -0
  54. data/lib/force/client.rb +3 -0
  55. data/lib/force/collection.rb +45 -0
  56. data/lib/force/concerns/api.rb +321 -0
  57. data/lib/force/concerns/authentication.rb +39 -0
  58. data/lib/force/concerns/base.rb +59 -0
  59. data/lib/force/concerns/caching.rb +24 -0
  60. data/lib/force/concerns/canvas.rb +10 -0
  61. data/lib/force/concerns/connection.rb +74 -0
  62. data/lib/force/concerns/picklists.rb +87 -0
  63. data/lib/force/concerns/streaming.rb +31 -0
  64. data/lib/force/concerns/verbs.rb +67 -0
  65. data/lib/force/config.rb +140 -0
  66. data/lib/force/data/client.rb +18 -0
  67. data/lib/force/mash.rb +66 -0
  68. data/lib/force/middleware.rb +27 -0
  69. data/lib/force/middleware/authentication.rb +73 -0
  70. data/lib/force/middleware/authentication/password.rb +17 -0
  71. data/lib/force/middleware/authentication/token.rb +15 -0
  72. data/lib/force/middleware/authorization.rb +15 -0
  73. data/lib/force/middleware/caching.rb +22 -0
  74. data/lib/force/middleware/gzip.rb +31 -0
  75. data/lib/force/middleware/instance_url.rb +14 -0
  76. data/lib/force/middleware/logger.rb +40 -0
  77. data/lib/force/middleware/mashify.rb +16 -0
  78. data/lib/force/middleware/multipart.rb +55 -0
  79. data/lib/force/middleware/raise_error.rb +25 -0
  80. data/lib/force/signed_request.rb +48 -0
  81. data/lib/force/sobject.rb +68 -0
  82. data/lib/force/tooling/client.rb +11 -0
  83. data/lib/force/upload_io.rb +20 -0
  84. data/lib/force/version.rb +3 -0
  85. data/spec/fixtures/auth_error_response.json +4 -0
  86. data/spec/fixtures/auth_success_response.json +7 -0
  87. data/spec/fixtures/blob.jpg +0 -0
  88. data/spec/fixtures/expired_session_response.json +6 -0
  89. data/spec/fixtures/reauth_success_response.json +7 -0
  90. data/spec/fixtures/refresh_error_response.json +4 -0
  91. data/spec/fixtures/refresh_success_response.json +7 -0
  92. data/spec/fixtures/services_data_success_response.json +12 -0
  93. data/spec/fixtures/sobject/create_success_response.json +5 -0
  94. data/spec/fixtures/sobject/delete_error_response.json +1 -0
  95. data/spec/fixtures/sobject/describe_sobjects_success_response.json +31 -0
  96. data/spec/fixtures/sobject/list_sobjects_success_response.json +31 -0
  97. data/spec/fixtures/sobject/org_query_response.json +11 -0
  98. data/spec/fixtures/sobject/query_aggregate_success_response.json +23 -0
  99. data/spec/fixtures/sobject/query_empty_response.json +5 -0
  100. data/spec/fixtures/sobject/query_error_response.json +6 -0
  101. data/spec/fixtures/sobject/query_paginated_first_page_response.json +14 -0
  102. data/spec/fixtures/sobject/query_paginated_last_page_response.json +13 -0
  103. data/spec/fixtures/sobject/query_success_response.json +38 -0
  104. data/spec/fixtures/sobject/recent_success_response.json +18 -0
  105. data/spec/fixtures/sobject/search_error_response.json +6 -0
  106. data/spec/fixtures/sobject/search_success_response.json +16 -0
  107. data/spec/fixtures/sobject/sobject_describe_error_response.json +6 -0
  108. data/spec/fixtures/sobject/sobject_describe_success_response.json +1429 -0
  109. data/spec/fixtures/sobject/sobject_find_error_response.json +6 -0
  110. data/spec/fixtures/sobject/sobject_find_success_response.json +29 -0
  111. data/spec/fixtures/sobject/upsert_created_success_response.json +5 -0
  112. data/spec/fixtures/sobject/upsert_error_response.json +6 -0
  113. data/spec/fixtures/sobject/upsert_multiple_error_response.json +4 -0
  114. data/spec/fixtures/sobject/upsert_updated_success_response.json +0 -0
  115. data/spec/fixtures/sobject/write_error_response.json +6 -0
  116. data/spec/integration/abstract_client_spec.rb +306 -0
  117. data/spec/integration/data/client_spec.rb +90 -0
  118. data/spec/spec_helper.rb +20 -0
  119. data/spec/support/client_integration.rb +45 -0
  120. data/spec/support/concerns.rb +18 -0
  121. data/spec/support/event_machine.rb +14 -0
  122. data/spec/support/fixture_helpers.rb +45 -0
  123. data/spec/support/matchers.rb +11 -0
  124. data/spec/support/middleware.rb +76 -0
  125. data/spec/support/mock_cache.rb +13 -0
  126. data/spec/unit/abstract_client_spec.rb +11 -0
  127. data/spec/unit/attachment_spec.rb +15 -0
  128. data/spec/unit/collection_spec.rb +52 -0
  129. data/spec/unit/concerns/api_spec.rb +244 -0
  130. data/spec/unit/concerns/authentication_spec.rb +98 -0
  131. data/spec/unit/concerns/base_spec.rb +42 -0
  132. data/spec/unit/concerns/caching_spec.rb +29 -0
  133. data/spec/unit/concerns/canvas_spec.rb +30 -0
  134. data/spec/unit/concerns/connection_spec.rb +22 -0
  135. data/spec/unit/config_spec.rb +99 -0
  136. data/spec/unit/data/client_spec.rb +10 -0
  137. data/spec/unit/mash_spec.rb +36 -0
  138. data/spec/unit/middleware/authentication/password_spec.rb +31 -0
  139. data/spec/unit/middleware/authentication/token_spec.rb +24 -0
  140. data/spec/unit/middleware/authentication_spec.rb +67 -0
  141. data/spec/unit/middleware/authorization_spec.rb +11 -0
  142. data/spec/unit/middleware/gzip_spec.rb +66 -0
  143. data/spec/unit/middleware/instance_url_spec.rb +24 -0
  144. data/spec/unit/middleware/logger_spec.rb +19 -0
  145. data/spec/unit/middleware/mashify_spec.rb +11 -0
  146. data/spec/unit/middleware/raise_error_spec.rb +32 -0
  147. data/spec/unit/signed_request_spec.rb +24 -0
  148. data/spec/unit/sobject_spec.rb +86 -0
  149. data/spec/unit/tooling/client_spec.rb +7 -0
  150. data/tmp/rspec_guard_result +1 -0
  151. metadata +383 -0
data/force.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/force/version', __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "force"
6
+ s.version = Force::VERSION
7
+ s.authors = ["Eric J. Holmes", "Mattt Thompson"]
8
+ s.email = ["eric@ejholmes.net", "mattt@heroku.com"]
9
+ s.description = "A lightweight ruby client for the Salesforce REST api."
10
+ s.summary = "A lightweight ruby client for the Salesforce REST api."
11
+ s.homepage = "https://github.com/heroku/force"
12
+
13
+ s.files = Dir["./**/*"].reject { |file| file =~ /\.\/(bin|example|log|pkg|script|spec|test|vendor)/ }
14
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16
+ s.require_paths = ["lib"]
17
+
18
+ s.add_dependency 'faraday', '~> 0.8.4'
19
+ s.add_dependency 'faraday_middleware', '>= 0.8.8'
20
+ s.add_dependency 'json', ['>= 1.7.5', '< 1.9.0']
21
+ s.add_dependency 'hashie', ['>= 1.2.0', '< 2.1']
22
+
23
+ s.add_development_dependency 'rspec', '~> 2.14.0'
24
+ s.add_development_dependency 'webmock', '~> 1.13.0'
25
+ s.add_development_dependency 'simplecov', '~> 0.7.1'
26
+ s.add_development_dependency 'faye' unless RUBY_PLATFORM == 'java'
27
+ end
data/lib/force.rb ADDED
@@ -0,0 +1,74 @@
1
+ require 'faraday'
2
+ require 'faraday_middleware'
3
+ require 'json'
4
+
5
+ require 'force/version'
6
+ require 'force/config'
7
+
8
+ module Force
9
+ autoload :AbstractClient, 'force/abstract_client'
10
+ autoload :SignedRequest, 'force/signed_request'
11
+ autoload :Collection, 'force/collection'
12
+ autoload :Middleware, 'force/middleware'
13
+ autoload :Attachment, 'force/attachment'
14
+ autoload :UploadIO, 'force/upload_io'
15
+ autoload :SObject, 'force/sobject'
16
+ autoload :Client, 'force/client'
17
+ autoload :Mash, 'force/mash'
18
+
19
+ module Concerns
20
+ autoload :Authentication, 'force/concerns/authentication'
21
+ autoload :Connection, 'force/concerns/connection'
22
+ autoload :Picklists, 'force/concerns/picklists'
23
+ autoload :Streaming, 'force/concerns/streaming'
24
+ autoload :Caching, 'force/concerns/caching'
25
+ autoload :Canvas, 'force/concerns/canvas'
26
+ autoload :Verbs, 'force/concerns/verbs'
27
+ autoload :Base, 'force/concerns/base'
28
+ autoload :API, 'force/concerns/api'
29
+ end
30
+
31
+ module Data
32
+ autoload :Client, 'force/data/client'
33
+ end
34
+
35
+ module Tooling
36
+ autoload :Client, 'force/tooling/client'
37
+ end
38
+
39
+ Error = Class.new(StandardError)
40
+ AuthenticationError = Class.new(Error)
41
+ UnauthorizedError = Class.new(Error)
42
+
43
+ class << self
44
+ # Alias for Force::Data::Client.new
45
+ #
46
+ # Shamelessly pulled from https://github.com/pengwynn/octokit/blob/master/lib/octokit.rb
47
+ def new(*args)
48
+ data(*args)
49
+ end
50
+
51
+ def data(*args)
52
+ Force::Data::Client.new(*args)
53
+ end
54
+
55
+ def tooling(*args)
56
+ Force::Tooling::Client.new(*args)
57
+ end
58
+
59
+ # Helper for decoding signed requests.
60
+ def decode_signed_request(*args)
61
+ SignedRequest.decode(*args)
62
+ end
63
+ end
64
+
65
+ # Add .tap method in Ruby 1.8
66
+ module CoreExtensions
67
+ def tap
68
+ yield self
69
+ self
70
+ end
71
+ end
72
+
73
+ Object.send :include, Force::CoreExtensions unless Object.respond_to? :tap
74
+ end
@@ -0,0 +1,9 @@
1
+ module Force
2
+ class AbstractClient
3
+ include Force::Concerns::Base
4
+ include Force::Concerns::Connection
5
+ include Force::Concerns::Authentication
6
+ include Force::Concerns::Caching
7
+ include Force::Concerns::API
8
+ end
9
+ end
@@ -0,0 +1,21 @@
1
+ module Force
2
+ class Attachment < Force::SObject
3
+ # Public: Returns the body of the attachment.
4
+ #
5
+ # Examples
6
+ #
7
+ # attachment = client.query('select Id, Name, Body from Attachment').first
8
+ # File.open(attachment.Name, 'wb') { |f| f.write(attachment.Body) }
9
+ def Body
10
+ ensure_id && ensure_body
11
+ @client.get(super).body
12
+ end
13
+
14
+ private
15
+
16
+ def ensure_body
17
+ return true if self.Body?
18
+ raise 'You need to query the Body for the record first.'
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ module Force
2
+ Client = Data::Client
3
+ end
@@ -0,0 +1,45 @@
1
+ module Force
2
+ class Collection
3
+ include Enumerable
4
+
5
+ # Given a hash and client, will create an Enumerator that will lazily
6
+ # request Salesforce for the next page of results.
7
+ def initialize(hash, client)
8
+ @client = client
9
+ @raw_page = hash
10
+ end
11
+
12
+ # Yield each value on each page.
13
+ def each
14
+ @raw_page['records'].each { |record| yield Force::Mash.build(record, @client) }
15
+
16
+ next_page.each { |record| yield record } if has_next_page?
17
+ end
18
+
19
+ # Return the size of the Collection without making any additional requests.
20
+ def size
21
+ @raw_page['totalSize']
22
+ end
23
+ alias_method :length, :size
24
+
25
+ # Return array of the elements on the current page
26
+ def current_page
27
+ first(@raw_page['records'].size)
28
+ end
29
+
30
+ # Return the current and all of the following pages.
31
+ def pages
32
+ [self] + (has_next_page? ? next_page.pages : [])
33
+ end
34
+
35
+ # Returns true if there is a pointer to the next page.
36
+ def has_next_page?
37
+ !@raw_page['nextRecordsUrl'].nil?
38
+ end
39
+
40
+ # Returns the next page as a Force::Collection if it's available, nil otherwise.
41
+ def next_page
42
+ @next_page ||= @client.get(@raw_page['nextRecordsUrl']).body if has_next_page?
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,321 @@
1
+ require 'force/concerns/verbs'
2
+
3
+ module Force
4
+ module Concerns
5
+ module API
6
+ extend Force::Concerns::Verbs
7
+
8
+ # Public: Helper methods for performing arbitrary actions against the API using
9
+ # various HTTP verbs.
10
+ #
11
+ # Examples
12
+ #
13
+ # # Perform a get request
14
+ # client.get '/services/data/v24.0/sobjects'
15
+ # client.api_get 'sobjects'
16
+ #
17
+ # # Perform a post request
18
+ # client.post '/services/data/v24.0/sobjects/Account', { ... }
19
+ # client.api_post 'sobjects/Account', { ... }
20
+ #
21
+ # # Perform a put request
22
+ # client.put '/services/data/v24.0/sobjects/Account/001D000000INjVe', { ... }
23
+ # client.api_put 'sobjects/Account/001D000000INjVe', { ... }
24
+ #
25
+ # # Perform a delete request
26
+ # client.delete '/services/data/v24.0/sobjects/Account/001D000000INjVe'
27
+ # client.api_delete 'sobjects/Account/001D000000INjVe'
28
+ #
29
+ # Returns the Faraday::Response.
30
+ define_verbs :get, :post, :put, :delete, :patch, :head
31
+
32
+ # Public: Get the names of all sobjects on the org.
33
+ #
34
+ # Examples
35
+ #
36
+ # # get the names of all sobjects on the org
37
+ # client.list_sobjects
38
+ # # => ['Account', 'Lead', ... ]
39
+ #
40
+ # Returns an Array of String names for each SObject.
41
+ def list_sobjects
42
+ describe.collect { |sobject| sobject['name'] }
43
+ end
44
+
45
+ # Public: Returns a detailed describe result for the specified sobject
46
+ #
47
+ # sobject - Stringish name of the sobject (default: nil).
48
+ #
49
+ # Examples
50
+ #
51
+ # # get the global describe for all sobjects
52
+ # client.describe
53
+ # # => { ... }
54
+ #
55
+ # # get the describe for the Account object
56
+ # client.describe('Account')
57
+ # # => { ... }
58
+ #
59
+ # Returns the Hash representation of the describe call.
60
+ def describe(sobject = nil)
61
+ if sobject
62
+ api_get("sobjects/#{sobject.to_s}/describe").body
63
+ else
64
+ api_get('sobjects').body['sobjects']
65
+ end
66
+ end
67
+
68
+ # Public: Returns a detailed description of the Page Layout for the
69
+ # specified sobject type, or URIs for layouts if the sobject has
70
+ # multiple Record Types.
71
+ #
72
+ # This resource was introduced in version 28.0.
73
+ #
74
+ # Examples:
75
+ # # get the layouts for the sobject
76
+ # client.describe_layouts('Account')
77
+ # # => { ... }
78
+ #
79
+ # # get the layout for the specified Id for the sobject
80
+ # client.describe_layouts('Account', '012E0000000RHEp')
81
+ # # => { ... }
82
+ #
83
+ # Returns the Hash representation of the describe_layouts result
84
+ def describe_layouts(sobject, layout_id = nil)
85
+ if layout_id
86
+ api_get("sobjects/#{sobject.to_s}/describe/layouts/#{layout_id}").body
87
+ else
88
+ api_get("sobjects/#{sobject.to_s}/describe/layouts").body
89
+ end
90
+ end
91
+
92
+ # Public: Get the current organization's Id.
93
+ #
94
+ # Examples
95
+ #
96
+ # client.org_id
97
+ # # => '00Dx0000000BV7z'
98
+ #
99
+ # Returns the String organization Id
100
+ def org_id
101
+ query('select id from Organization').first['Id']
102
+ end
103
+
104
+ # Public: Executs a SOQL query and returns the result.
105
+ #
106
+ # soql - A SOQL expression.
107
+ #
108
+ # Examples
109
+ #
110
+ # # Find the names of all Accounts
111
+ # client.query('select Name from Account').map(&:Name)
112
+ # # => ['Foo Bar Inc.', 'Whizbang Corp']
113
+ #
114
+ # Returns a Force::Collection if Force.configuration.mashify is true.
115
+ # Returns an Array of Hash for each record in the result if Force.configuration.mashify is false.
116
+ def query(soql)
117
+ response = api_get 'query', :q => soql
118
+ mashify? ? response.body : response.body['records']
119
+ end
120
+
121
+ # Public: Perform a SOSL search
122
+ #
123
+ # sosl - A SOSL expression.
124
+ #
125
+ # Examples
126
+ #
127
+ # # Find all occurrences of 'bar'
128
+ # client.search('FIND {bar}')
129
+ # # => #<Force::Collection >
130
+ #
131
+ # # Find accounts match the term 'genepoint' and return the Name field
132
+ # client.search('FIND {genepoint} RETURNING Account (Name)').map(&:Name)
133
+ # # => ['GenePoint']
134
+ #
135
+ # Returns a Force::Collection if Force.configuration.mashify is true.
136
+ # Returns an Array of Hash for each record in the result if Force.configuration.mashify is false.
137
+ def search(sosl)
138
+ api_get('search', :q => sosl).body
139
+ end
140
+
141
+ # Public: Insert a new record.
142
+ #
143
+ # sobject - String name of the sobject.
144
+ # attrs - Hash of attributes to set on the new record.
145
+ #
146
+ # Examples
147
+ #
148
+ # # Add a new account
149
+ # client.create('Account', Name: 'Foobar Inc.')
150
+ # # => '0016000000MRatd'
151
+ #
152
+ # Returns the String Id of the newly created sobject.
153
+ # Returns false if something bad happens.
154
+ def create(*args)
155
+ create!(*args)
156
+ rescue *exceptions
157
+ false
158
+ end
159
+ alias_method :insert, :create
160
+
161
+ # Public: Insert a new record.
162
+ #
163
+ # sobject - String name of the sobject.
164
+ # attrs - Hash of attributes to set on the new record.
165
+ #
166
+ # Examples
167
+ #
168
+ # # Add a new account
169
+ # client.create!('Account', Name: 'Foobar Inc.')
170
+ # # => '0016000000MRatd'
171
+ #
172
+ # Returns the String Id of the newly created sobject.
173
+ # Raises exceptions if an error is returned from Salesforce.
174
+ def create!(sobject, attrs)
175
+ api_post("sobjects/#{sobject}", attrs).body['id']
176
+ end
177
+ alias_method :insert!, :create!
178
+
179
+ # Public: Update a record.
180
+ #
181
+ # sobject - String name of the sobject.
182
+ # attrs - Hash of attributes to set on the record.
183
+ #
184
+ # Examples
185
+ #
186
+ # # Update the Account with Id '0016000000MRatd'
187
+ # client.update('Account', Id: '0016000000MRatd', Name: 'Whizbang Corp')
188
+ #
189
+ # Returns true if the sobject was successfully updated.
190
+ # Returns false if there was an error.
191
+ def update(*args)
192
+ update!(*args)
193
+ rescue *exceptions
194
+ false
195
+ end
196
+
197
+ # Public: Update a record.
198
+ #
199
+ # sobject - String name of the sobject.
200
+ # attrs - Hash of attributes to set on the record.
201
+ #
202
+ # Examples
203
+ #
204
+ # # Update the Account with Id '0016000000MRatd'
205
+ # client.update!('Account', Id: '0016000000MRatd', Name: 'Whizbang Corp')
206
+ #
207
+ # Returns true if the sobject was successfully updated.
208
+ # Raises an exception if an error is returned from Salesforce.
209
+ def update!(sobject, attrs)
210
+ id = attrs.delete(attrs.keys.find { |k| k.to_s.downcase == 'id' })
211
+ raise ArgumentError, 'Id field missing from attrs.' unless id
212
+ api_patch "sobjects/#{sobject}/#{id}", attrs
213
+ true
214
+ end
215
+
216
+ # Public: Update or create a record based on an external ID
217
+ #
218
+ # sobject - The name of the sobject to created.
219
+ # field - The name of the external Id field to match against.
220
+ # attrs - Hash of attributes for the record.
221
+ #
222
+ # Examples
223
+ #
224
+ # # Update the record with external ID of 12
225
+ # client.upsert('Account', 'External__c', External__c: 12, Name: 'Foobar')
226
+ #
227
+ # Returns true if the record was found and updated.
228
+ # Returns the Id of the newly created record if the record was created.
229
+ # Returns false if something bad happens.
230
+ def upsert(*args)
231
+ upsert!(*args)
232
+ rescue *exceptions
233
+ false
234
+ end
235
+
236
+ # Public: Update or create a record based on an external ID
237
+ #
238
+ # sobject - The name of the sobject to created.
239
+ # field - The name of the external Id field to match against.
240
+ # attrs - Hash of attributes for the record.
241
+ #
242
+ # Examples
243
+ #
244
+ # # Update the record with external ID of 12
245
+ # client.upsert!('Account', 'External__c', External__c: 12, Name: 'Foobar')
246
+ #
247
+ # Returns true if the record was found and updated.
248
+ # Returns the Id of the newly created record if the record was created.
249
+ # Raises an exception if an error is returned from Salesforce.
250
+ def upsert!(sobject, field, attrs)
251
+ external_id = attrs.delete(attrs.keys.find { |k| k.to_s.downcase == field.to_s.downcase })
252
+ response = api_patch "sobjects/#{sobject}/#{field.to_s}/#{external_id}", attrs
253
+ (response.body && response.body['id']) ? response.body['id'] : true
254
+ end
255
+
256
+ # Public: Delete a record.
257
+ #
258
+ # sobject - String name of the sobject.
259
+ # id - The Salesforce ID of the record.
260
+ #
261
+ # Examples
262
+ #
263
+ # # Delete the Account with Id '0016000000MRatd'
264
+ # client.destroy('Account', '0016000000MRatd')
265
+ #
266
+ # Returns true if the sobject was successfully deleted.
267
+ # Returns false if an error is returned from Salesforce.
268
+ def destroy(*args)
269
+ destroy!(*args)
270
+ rescue *exceptions
271
+ false
272
+ end
273
+
274
+ # Public: Delete a record.
275
+ #
276
+ # sobject - String name of the sobject.
277
+ # id - The Salesforce ID of the record.
278
+ #
279
+ # Examples
280
+ #
281
+ # # Delete the Account with Id '0016000000MRatd'
282
+ # client.destroy('Account', '0016000000MRatd')
283
+ #
284
+ # Returns true of the sobject was successfully deleted.
285
+ # Raises an exception if an error is returned from Salesforce.
286
+ def destroy!(sobject, id)
287
+ api_delete "sobjects/#{sobject}/#{id}"
288
+ true
289
+ end
290
+
291
+ # Public: Finds a single record and returns all fields.
292
+ #
293
+ # sobject - The String name of the sobject.
294
+ # id - The id of the record. If field is specified, id should be the id
295
+ # of the external field.
296
+ # field - External ID field to use (default: nil).
297
+ #
298
+ # Returns the Force::SObject sobject record.
299
+ def find(sobject, id, field=nil)
300
+ api_get(field ? "sobjects/#{sobject}/#{field}/#{id}" : "sobjects/#{sobject}/#{id}").body
301
+ end
302
+
303
+ private
304
+
305
+ # Internal: Returns a path to an api endpoint
306
+ #
307
+ # Examples
308
+ #
309
+ # api_path('sobjects')
310
+ # # => '/services/data/v24.0/sobjects'
311
+ def api_path(path)
312
+ "/services/data/v#{options[:api_version]}/#{path}"
313
+ end
314
+
315
+ # Internal: Errors that should be rescued from in non-bang methods
316
+ def exceptions
317
+ [Faraday::Error::ClientError]
318
+ end
319
+ end
320
+ end
321
+ end