glg-databasedotcom 1.3.2.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.
@@ -0,0 +1,37 @@
1
+ module Databasedotcom
2
+ # A collection of Sobject or Record objects that holds a single page of results, and understands how to
3
+ # retrieve the next page, if any. Inherits from Array, thus, behaves as an Enumerable.
4
+
5
+ class Collection < Array
6
+ attr_reader :total_size, :next_page_url, :previous_page_url, :current_page_url, :client
7
+
8
+ # Creates a paginatable collection. You should never need to call this.
9
+ def initialize(client, total_size, next_page_url=nil, previous_page_url=nil, current_page_url=nil) #:nodoc:
10
+ @client = client
11
+ @total_size = total_size
12
+ @next_page_url = next_page_url
13
+ @previous_page_url = previous_page_url
14
+ @current_page_url = current_page_url
15
+ end
16
+
17
+ # Does this collection have a next page?
18
+ def next_page?
19
+ !!self.next_page_url
20
+ end
21
+
22
+ # Retrieve the next page of this collection. Returns the new collection, which is an empty collection if no next page exists
23
+ def next_page
24
+ self.next_page? ? @client.next_page(@next_page_url) : Databasedotcom::Collection.new(self.client, 0)
25
+ end
26
+
27
+ # Does this collection have a previous page?
28
+ def previous_page?
29
+ !!self.previous_page_url
30
+ end
31
+
32
+ # Retrieve the previous page of this collection. Returns the new collection, which is an empty collection if no previous page exists
33
+ def previous_page
34
+ self.previous_page? ? @client.previous_page(@previous_page_url) : Databasedotcom::Collection.new(self.client, 0)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1 @@
1
+ require 'databasedotcom/core_extensions/string_extensions'
@@ -0,0 +1,9 @@
1
+ # This extends String to add the +resourcerize+ method.
2
+ class String
3
+
4
+ # Dasherizes and downcases a camelcased string. Used for Feed types.
5
+ def resourcerize
6
+ self.gsub(/([a-z])([A-Z])/, '\1-\2').downcase
7
+ end
8
+
9
+ end
@@ -0,0 +1,25 @@
1
+ module Databasedotcom
2
+ # A Salesforce API endpoint that can trigger predefinted flows in salesforce to kick off salesforce side processes.
3
+ # A flow can be triggered by its unique name and can be passed a hash of input variables.
4
+
5
+ class Flow
6
+ def initialize(client, flow_name, params = {}, options = {})
7
+ @client = client
8
+ @flow_name = flow_name
9
+ @params = params
10
+ @version = options[:version] || '32.0'
11
+ end
12
+
13
+ def run
14
+ params_hash = {inputs: [@params]}
15
+ @client.http_post(flow_url, params_hash.to_json)
16
+ end
17
+
18
+ protected
19
+
20
+ def flow_url
21
+ "/services/data/v#{@version}/actions/custom/flow/#{@flow_name}"
22
+ end
23
+ end
24
+ end
25
+
@@ -0,0 +1,31 @@
1
+ module Databasedotcom
2
+ # An exception raised when any non successful request is made to Force.com.
3
+ class SalesForceError < StandardError
4
+ # the Net::HTTPResponse from the API call
5
+ attr_accessor :response
6
+ # the +errorCode+ from the server response body
7
+ attr_accessor :error_code
8
+
9
+ def initialize(response)
10
+ self.response = response
11
+ parsed_body = JSON.parse(response.body) rescue nil
12
+ if parsed_body
13
+ if parsed_body.is_a?(Array)
14
+ if parsed_body[0]["errors"]
15
+ message = parsed_body[0]["errors"][0]["message"]
16
+ self.error_code = parsed_body[0]["errors"][0]["statusCode"]
17
+ else
18
+ message = parsed_body[0]["message"]
19
+ self.error_code = parsed_body[0]["errorCode"]
20
+ end
21
+ else
22
+ message = parsed_body["error_description"]
23
+ self.error_code = parsed_body["error"]
24
+ end
25
+ else
26
+ message = response.body
27
+ end
28
+ super(message)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,2 @@
1
+ require 'databasedotcom/sobject/sobject'
2
+
@@ -0,0 +1,376 @@
1
+ module Databasedotcom
2
+ module Sobject
3
+ # Parent class of dynamically created sobject types. Interacts with Force.com through a Client object that is passed in during materialization.
4
+ class Sobject
5
+ cattr_accessor :client
6
+ extend ActiveModel::Naming if defined?(ActiveModel::Naming)
7
+
8
+ def ==(other)
9
+ return false unless other.is_a?(self.class)
10
+ self.Id == other.Id
11
+ end
12
+
13
+ # Returns a new Sobject. The default values for all attributes are set based on its description.
14
+ def initialize(attrs = {})
15
+ super()
16
+ self.class.description["fields"].each do |field|
17
+ if field['type'] =~ /(picklist|multipicklist)/ && picklist_option = field['picklistValues'].find { |p| p['defaultValue'] }
18
+ self.send("#{field["name"]}=", picklist_option["value"])
19
+ elsif field['type'] =~ /boolean/
20
+ self.send("#{field["name"]}=", field["defaultValue"])
21
+ else
22
+ self.send("#{field["name"]}=", field["defaultValueFormula"])
23
+ end
24
+ end
25
+ self.attributes=(attrs)
26
+ end
27
+
28
+ # Returns a hash representing the state of this object
29
+ def attributes
30
+ self.class.attributes.inject({}) do |hash, attr|
31
+ hash[attr] = self.send(attr.to_sym) if self.respond_to?(attr.to_sym)
32
+ hash
33
+ end
34
+ end
35
+
36
+ # Set attributes of this object, from a hash, in bulk
37
+ def attributes=(attrs)
38
+ attrs.each do |key, value|
39
+ self.send("#{key}=", value)
40
+ end
41
+ end
42
+
43
+ # Returns true if the object has been persisted in the Force.com database.
44
+ def persisted?
45
+ !self.Id.nil?
46
+ end
47
+
48
+ # Returns true if this record has not been persisted in the Force.com database.
49
+ def new_record?
50
+ !self.persisted?
51
+ end
52
+
53
+ # Returns self.
54
+ def to_model
55
+ self
56
+ end
57
+
58
+ # Returns a unique object id for self.
59
+ def to_key
60
+ [object_id]
61
+ end
62
+
63
+ # Returns the Force.com Id for this instance.
64
+ def to_param
65
+ self.Id
66
+ end
67
+
68
+ # Updates the corresponding record on Force.com by setting the attribute +attr_name+ to +attr_value+.
69
+ #
70
+ # client.materialize("Car")
71
+ # c = Car.new
72
+ # c.update_attribute("Color", "Blue")
73
+ def update_attribute(attr_name, attr_value)
74
+ update_attributes(attr_name => attr_value)
75
+ end
76
+
77
+ # Updates the corresponding record on Force.com with the attributes specified by the +new_attrs+ hash.
78
+ #
79
+ # client.materialize("Car")
80
+ # c = Car.new
81
+ # c.update_attributes {"Color" => "Blue", "Year" => "2012"}
82
+ def update_attributes(new_attrs)
83
+ if self.client.update(self.class, self.Id, new_attrs)
84
+ new_attrs = new_attrs.is_a?(Hash) ? new_attrs : JSON.parse(new_attrs)
85
+ new_attrs.each do |attr, value|
86
+ self.send("#{attr}=", value)
87
+ end
88
+ end
89
+ self
90
+ end
91
+
92
+ # Updates the corresponding record on Force.com with the attributes of self.
93
+ #
94
+ # client.materialize("Car")
95
+ # c = Car.find_by_Color("Yellow")
96
+ # c.Color = "Green"
97
+ # c.save
98
+ #
99
+ # _options_ can contain the following keys:
100
+ #
101
+ # exclusions # an array of field names (case sensitive) to exclude from save
102
+ def save(options={})
103
+ attr_hash = {}
104
+ selection_attr = self.Id.nil? ? "createable" : "updateable"
105
+ self.class.description["fields"].select { |f| f[selection_attr] }.collect { |f| f["name"] }.each { |attr| attr_hash[attr] = self.send(attr) }
106
+
107
+ # allow fields to be removed on a case by case basis as some data is not allowed to be saved
108
+ # (e.g. Name field on Account with record type of Person Account) despite the API listing
109
+ # some fields as editable
110
+ if options[:exclusions] and options[:exclusions].respond_to?(:include?) then
111
+ attr_hash.delete_if { |key, value| options[:exclusions].include?(key.to_s) }
112
+ end
113
+
114
+ if self.Id.nil?
115
+ self.Id = self.client.create(self.class, attr_hash).Id
116
+ else
117
+ self.client.update(self.class, self.Id, attr_hash)
118
+ end
119
+ end
120
+
121
+ # Deletes the corresponding record from the Force.com database. Returns self.
122
+ #
123
+ # client.materialize("Car")
124
+ # c = Car.find_by_Color("Yellow")
125
+ # c.delete
126
+ def delete
127
+ if self.client.delete(self.class, self.Id)
128
+ self
129
+ end
130
+ end
131
+
132
+ # Reloads the record from the Force.com database. Returns self.
133
+ #
134
+ # client.materialize("Car")
135
+ # c = Car.find_by_Color("Yellow")
136
+ # c.reload
137
+ def reload
138
+ self.attributes = self.class.find(self.Id).attributes
139
+ self
140
+ end
141
+
142
+ # Get a named attribute on this object
143
+ def [](attr_name)
144
+ self.send(attr_name) rescue nil
145
+ end
146
+
147
+ # Set a named attribute on this object
148
+ def []=(attr_name, value)
149
+ raise ArgumentError.new("No attribute named #{attr_name}") unless self.class.attributes.include?(attr_name)
150
+ self.send("#{attr_name}=", value)
151
+ end
152
+
153
+ # Returns an Array of attribute names that this Sobject has.
154
+ #
155
+ # client.materialize("Car")
156
+ # Car.attributes #=> ["Id", "Name", "Color", "Year"]
157
+ def self.attributes
158
+ self.description["fields"].collect { |f| [f["name"], f["relationshipName"]] }.flatten.compact
159
+ end
160
+
161
+ # Materializes the dynamically created Sobject class by adding all attribute accessors for each field as described in the description of the object on Force.com
162
+ def self.materialize(sobject_name)
163
+ self.cattr_accessor :description
164
+ self.cattr_accessor :type_map
165
+ self.cattr_accessor :sobject_name
166
+
167
+ self.sobject_name = sobject_name
168
+ self.description = self.client.describe_sobject(self.sobject_name)
169
+ self.type_map = {}
170
+
171
+ self.description["fields"].each do |field|
172
+
173
+ # Register normal fields
174
+ name = field["name"]
175
+ register_field( field["name"], field )
176
+
177
+ # Register relationship fields.
178
+ if( field["type"] == "reference" and field["relationshipName"] )
179
+ register_field( field["relationshipName"], field )
180
+ end
181
+
182
+ end
183
+ end
184
+
185
+ # Returns the Force.com type of the attribute +attr_name+. Raises ArgumentError if attribute does not exist.
186
+ #
187
+ # client.materialize("Car")
188
+ # Car.field_type("Color") #=> "string"
189
+ def self.field_type(attr_name)
190
+ self.type_map_attr(attr_name, :type)
191
+ end
192
+
193
+ # Returns the label for the attribute +attr_name+. Raises ArgumentError if attribute does not exist.
194
+ def self.label_for(attr_name)
195
+ self.type_map_attr(attr_name, :label)
196
+ end
197
+
198
+ # Returns the possible picklist options for the attribute +attr_name+. If +attr_name+ is not of type picklist or multipicklist, [] is returned. Raises ArgumentError if attribute does not exist.
199
+ def self.picklist_values(attr_name)
200
+ self.type_map_attr(attr_name, :picklist_values)
201
+ end
202
+
203
+ # Returns true if the attribute +attr_name+ can be updated. Raises ArgumentError if attribute does not exist.
204
+ def self.updateable?(attr_name)
205
+ self.type_map_attr(attr_name, :updateable?)
206
+ end
207
+
208
+ # Returns true if the attribute +attr_name+ can be created. Raises ArgumentError if attribute does not exist.
209
+ def self.createable?(attr_name)
210
+ self.type_map_attr(attr_name, :createable?)
211
+ end
212
+
213
+ # Delegates to Client.find with arguments +record_id+ and self
214
+ #
215
+ # client.materialize("Car")
216
+ # Car.find("rid") #=> #<Car @Id="rid", ...>
217
+ def self.find(record_id)
218
+ self.client.find(self, record_id)
219
+ end
220
+
221
+ # Returns all records of type self as instances.
222
+ #
223
+ # client.materialize("Car")
224
+ # Car.all #=> [#<Car @Id="1", ...>, #<Car @Id="2", ...>, #<Car @Id="3", ...>, ...]
225
+ def self.all
226
+ self.client.query("SELECT #{self.field_list} FROM #{self.sobject_name}")
227
+ end
228
+
229
+ # Returns a collection of instances of self that match the conditional +where_expr+, which is the WHERE part of a SOQL query.
230
+ #
231
+ # client.materialize("Car")
232
+ # Car.query("Color = 'Blue'") #=> [#<Car @Id="1", @Color="Blue", ...>, #<Car @Id="5", @Color="Blue", ...>, ...]
233
+ def self.query(where_expr)
234
+ self.client.query("SELECT #{self.field_list} FROM #{self.sobject_name} WHERE #{where_expr}")
235
+ end
236
+
237
+ # Delegates to Client.search
238
+ def self.search(sosl_expr)
239
+ self.client.search(sosl_expr)
240
+ end
241
+
242
+ # Find the first record. If the +where_expr+ argument is present, it must be the WHERE part of a SOQL query
243
+ def self.first(where_expr=nil)
244
+ where = where_expr ? "WHERE #{where_expr} " : ""
245
+ self.client.query("SELECT #{self.field_list} FROM #{self.sobject_name} #{where}ORDER BY Id ASC LIMIT 1").first
246
+ end
247
+
248
+ # Find the last record. If the +where_expr+ argument is present, it must be the WHERE part of a SOQL query
249
+ def self.last(where_expr=nil)
250
+ where = where_expr ? "WHERE #{where_expr} " : ""
251
+ self.client.query("SELECT #{self.field_list} FROM #{self.sobject_name} #{where}ORDER BY Id DESC LIMIT 1").first
252
+ end
253
+
254
+ #Delegates to Client.upsert with arguments self, +field+, +values+, and +attrs+
255
+ def self.upsert(field, value, attrs)
256
+ self.client.upsert(self.sobject_name, field, value, attrs)
257
+ end
258
+
259
+ # Delegates to Client.delete with arguments +record_id+ and self
260
+ def self.delete(record_id)
261
+ self.client.delete(self.sobject_name, record_id)
262
+ end
263
+
264
+ # Get the total number of records
265
+ def self.count
266
+ self.client.query("SELECT COUNT() FROM #{self.sobject_name}").total_size
267
+ end
268
+
269
+ # Sobject objects support dynamic finders similar to ActiveRecord.
270
+ #
271
+ # client.materialize("Car")
272
+ # Car.find_by_Color("Blue")
273
+ # Car.find_all_by_Year("2011")
274
+ # Car.find_by_Color_and_Year("Blue", "2011")
275
+ # Car.find_or_create_by_Year("2011")
276
+ # Car.find_or_initialize_by_Name("Foo")
277
+ def self.method_missing(method_name, *args, &block)
278
+ if method_name.to_s =~ /^find_(or_create_|or_initialize_)?by_(.+)$/ || method_name.to_s =~ /^find_(all_)by_(.+)$/
279
+ named_attrs = $2.split('_and_')
280
+ attrs_and_values_for_find = {}
281
+ hash_args = args.length == 1 && args[0].is_a?(Hash)
282
+ attrs_and_values_for_write = hash_args ? args[0] : {}
283
+
284
+ named_attrs.each_with_index do |attr, index|
285
+ value = hash_args ? args[0][attr] : args[index]
286
+ attrs_and_values_for_find[attr] = value
287
+ attrs_and_values_for_write[attr] = value unless hash_args
288
+ end
289
+
290
+ limit_clause = method_name.to_s.include?('_all_by_') ? "" : " LIMIT 1"
291
+
292
+ results = self.client.query("SELECT #{self.field_list} FROM #{self.sobject_name} WHERE #{soql_conditions_for(attrs_and_values_for_find)}#{limit_clause}")
293
+ results = limit_clause == "" ? results : results.first rescue nil
294
+
295
+ if results.nil?
296
+ if method_name.to_s =~ /^find_or_create_by_(.+)$/
297
+ results = self.client.create(self, attrs_and_values_for_write)
298
+ elsif method_name.to_s =~ /^find_or_initialize_by_(.+)$/
299
+ results = self.new
300
+ attrs_and_values_for_write.each { |attr, val| results.send("#{attr}=", val) }
301
+ end
302
+ end
303
+
304
+ results
305
+ else
306
+ super
307
+ end
308
+ end
309
+
310
+ # Delegates to Client.create with arguments +object_attributes+ and self
311
+ def self.create(object_attributes)
312
+ self.client.create(self, object_attributes)
313
+ end
314
+
315
+ # Coerce values submitted from a Rails form to the values expected by the database
316
+ # returns a new hash with updated values
317
+ def self.coerce_params(params)
318
+ params.each do |attr, value|
319
+ case self.field_type(attr)
320
+ when "boolean"
321
+ params[attr] = value.is_a?(String) ? value.to_i != 0 : value
322
+ when "currency", "percent", "double"
323
+ value = value.gsub(/[^-0-9.0-9]/, '').to_f if value.respond_to?(:gsub)
324
+ params[attr] = value.to_f
325
+ when "date"
326
+ params[attr] = Date.parse(value) rescue Date.today
327
+ when "datetime"
328
+ params[attr] = DateTime.parse(value) rescue DateTime.now
329
+ end
330
+ end
331
+ end
332
+
333
+ private
334
+
335
+ def self.register_field( name, field )
336
+ public
337
+ attr_accessor name.to_sym
338
+ private
339
+ self.type_map[name] = {
340
+ :type => field["type"],
341
+ :label => field["label"],
342
+ :picklist_values => field["picklistValues"],
343
+ :updateable? => field["updateable"],
344
+ :createable? => field["createable"]
345
+ }
346
+ end
347
+
348
+ def self.field_list
349
+ self.description['fields'].collect { |f| f['name'] }.join(',')
350
+ end
351
+
352
+ def self.type_map_attr(attr_name, key)
353
+ raise ArgumentError.new("No attribute named #{attr_name}") unless self.type_map.has_key?(attr_name)
354
+ self.type_map[attr_name][key]
355
+ end
356
+
357
+ def self.soql_conditions_for(params)
358
+ params.inject([]) do |arr, av|
359
+ case av[1]
360
+ when String
361
+ value_str = "'#{av[1].gsub("'", "\\\\'")}'"
362
+ when DateTime, Time
363
+ value_str = av[1].strftime(RUBY_VERSION.match(/^1.8/) ? "%Y-%m-%dT%H:%M:%S.000%z" : "%Y-%m-%dT%H:%M:%S.%L%z").insert(-3, ":")
364
+ when Date
365
+ value_str = av[1].strftime("%Y-%m-%d")
366
+ else
367
+ value_str = av[1].to_s
368
+ end
369
+
370
+ arr << "#{av[0]} = #{value_str}"
371
+ arr
372
+ end.join(" AND ")
373
+ end
374
+ end
375
+ end
376
+ end