databasedotcom_cloudfuji 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,3 @@
1
+ require 'databasedotcom/core_extensions/class_extensions'
2
+ require 'databasedotcom/core_extensions/string_extensions'
3
+ require 'databasedotcom/core_extensions/hash_extensions'
@@ -0,0 +1,41 @@
1
+ # This extends Class to be able to use +cattr_accessor+ if active_support is not being used.
2
+ class Class
3
+ unless respond_to?(:cattr_reader)
4
+ def cattr_reader(sym)
5
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
6
+ unless defined? @@#{sym}
7
+ @@#{sym} = nil
8
+ end
9
+
10
+ def self.#{sym}
11
+ @@#{sym}
12
+ end
13
+
14
+ def #{sym}
15
+ @@#{sym}
16
+ end
17
+ EOS
18
+ end
19
+
20
+ def cattr_writer(sym)
21
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
22
+ unless defined? @@#{sym}
23
+ @@#{sym} = nil
24
+ end
25
+
26
+ def self.#{sym}=(obj)
27
+ @@#{sym} = obj
28
+ end
29
+
30
+ def #{sym}=(obj)
31
+ @@#{sym} = obj
32
+ end
33
+ EOS
34
+ end
35
+
36
+ def cattr_accessor(*syms, &blk)
37
+ cattr_reader(*syms)
38
+ cattr_writer(*syms, &blk)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,8 @@
1
+ class Hash
2
+ def symbolize_keys!
3
+ keys.each do |key|
4
+ self[(key.to_sym rescue key) || key] = delete(key)
5
+ end
6
+ self
7
+ end
8
+ end
@@ -0,0 +1,16 @@
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
+ def constantize
10
+ unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ self
11
+ raise NameError, "#{self.inspect} is not a valid constant name!"
12
+ end
13
+ Object.module_eval("::#{$1}", __FILE__, __LINE__)
14
+ end
15
+
16
+ end
@@ -0,0 +1,26 @@
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
+ message = parsed_body[0]["message"]
15
+ self.error_code = parsed_body[0]["errorCode"]
16
+ else
17
+ message = parsed_body["error_description"]
18
+ self.error_code = parsed_body["error"]
19
+ end
20
+ else
21
+ message = response.body
22
+ end
23
+ super(message)
24
+ end
25
+ end
26
+ 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