File: databasedotcom/sobject/sobject.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  module: Databasedotcom#1
  module: Sobject#2
  class: Sobject#4
extends
  Naming ( Unknown-Module::ActiveModel )
inherits from
  Object ( Builtin-Module )
has properties
method: == / 1 #8
method: initialize / 1 #14
method: attributes (1/2) #23
method: attributes= / 1 #31
method: persisted? #38
method: new_record? #43
method: to_model #48
method: to_key #53
method: to_param #58
method: update_attribute / 2 #67
method: update_attributes / 1 #76
method: save / 1 #96
constant: Id #109
method: delete (1/2) #120
method: reload #131
method: [] / 1 #137
method: []= / 2 #142
class method: attributes (2/E) #151
class method: materialize / 1 #156
class method: field_type / 1 #183
class method: label_for / 1 #188
class method: picklist_values / 1 #193
class method: updateable? / 1 #198
class method: createable? / 1 #203
class method: find / 1 #211
class method: all #219
class method: query / 1 #227
class method: search / 1 #232
class method: first / 1 #237
class method: last / 1 #243
class method: upsert / 3 #249
class method: delete (2/E) / 1 #254
class method: count #259
class method: method_missing / 3 #271
class method: create / 1 #305
class method: coerce_params / 1 #311
class method: register_field / 2 #328
class method: field_list #341
class method: type_map_attr / 2 #345
class method: soql_conditions_for / 1 #350

Class Hierarchy

Code

   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            self.send("#{field["name"]}=", field["defaultValueFormula"])
  18          end
  19          self.attributes=(attrs)
  20        end
  21 
  22        # Returns a hash representing the state of this object
  23        def attributes
  24          self.class.attributes.inject({}) do |hash, attr|
  25            hash[attr] = self.send(attr.to_sym)
  26            hash
  27          end
  28        end
  29        
  30        # Set attributes of this object, from a hash, in bulk
  31        def attributes=(attrs)
  32          attrs.each do |key, value|
  33            self.send("#{key}=", value)
  34          end
  35        end
  36 
  37        # Returns true if the object has been persisted in the Force.com database.
  38        def persisted?
  39          !self.Id.nil?
  40        end
  41 
  42        # Returns true if this record has not been persisted in the Force.com database.
  43        def new_record?
  44          !self.persisted?
  45        end
  46 
  47        # Returns self.
  48        def to_model
  49          self
  50        end
  51 
  52        # Returns a unique object id for self.
  53        def to_key
  54          [object_id]
  55        end
  56 
  57        # Returns the Force.com Id for this instance.
  58        def to_param
  59          self.Id
  60        end
  61 
  62        # Updates the corresponding record on Force.com by setting the attribute +attr_name+ to +attr_value+.
  63        #
  64        #    client.materialize("Car")
  65        #    c = Car.new
  66        #    c.update_attribute("Color", "Blue")
  67        def update_attribute(attr_name, attr_value)
  68          update_attributes(attr_name => attr_value)
  69        end
  70 
  71        # Updates the corresponding record on Force.com with the attributes specified by the +new_attrs+ hash.
  72        #
  73        #    client.materialize("Car")
  74        #    c = Car.new
  75        #    c.update_attributes {"Color" => "Blue", "Year" => "2012"}
  76        def update_attributes(new_attrs)
  77          if self.client.update(self.class, self.Id, new_attrs)
  78            new_attrs = new_attrs.is_a?(Hash) ? new_attrs : JSON.parse(new_attrs)
  79            new_attrs.each do |attr, value|
  80              self.send("#{attr}=", value)
  81            end
  82          end
  83          self
  84        end
  85 
  86        # Updates the corresponding record on Force.com with the attributes of self.
  87        #
  88        #    client.materialize("Car")
  89        #    c = Car.find_by_Color("Yellow")
  90        #    c.Color = "Green"
  91        #    c.save
  92        #
  93        # _options_ can contain the following keys:
  94        #
  95        #    exclusions # an array of field names (case sensitive) to exclude from save
  96        def save(options={})
  97          attr_hash = {}
  98          selection_attr = self.Id.nil? ? "createable" : "updateable"
  99          self.class.description["fields"].select { |f| f[selection_attr] }.collect { |f| f["name"] }.each { |attr| attr_hash[attr] = self.send(attr) }
 100 
 101          # allow fields to be removed on a case by case basis as some data is not allowed to be saved 
 102          # (e.g. Name field on Account with record type of Person Account) despite the API listing 
 103          # some fields as editable
 104          if options[:exclusions] and options[:exclusions].respond_to?(:include?) then
 105            attr_hash.delete_if { |key, value| options[:exclusions].include?(key.to_s) }
 106          end
 107 
 108          if self.Id.nil?
 109            self.Id = self.client.create(self.class, attr_hash).Id
 110          else
 111            self.client.update(self.class, self.Id, attr_hash)
 112          end
 113        end
 114 
 115        # Deletes the corresponding record from the Force.com database. Returns self.
 116        #
 117        #    client.materialize("Car")
 118        #    c = Car.find_by_Color("Yellow")
 119        #    c.delete
 120        def delete
 121          if self.client.delete(self.class, self.Id)
 122            self
 123          end
 124        end
 125 
 126        # Reloads the record from the Force.com database. Returns self.
 127        #
 128        #    client.materialize("Car")
 129        #    c = Car.find_by_Color("Yellow")
 130        #    c.reload
 131        def reload
 132          self.attributes = self.class.find(self.Id).attributes
 133          self
 134        end
 135 
 136        # Get a named attribute on this object
 137        def [](attr_name)
 138          self.send(attr_name) rescue nil
 139        end
 140 
 141        # Set a named attribute on this object
 142        def []=(attr_name, value)
 143          raise ArgumentError.new("No attribute named #{attr_name}") unless self.class.attributes.include?(attr_name)
 144          self.send("#{attr_name}=", value)
 145        end
 146 
 147        # Returns an Array of attribute names that this Sobject has.
 148        #
 149        #    client.materialize("Car")
 150        #    Car.attributes               #=> ["Id", "Name", "Color", "Year"]
 151        def self.attributes
 152          self.description["fields"].collect { |f| [f["name"], f["relationshipName"]] }.flatten.compact
 153        end
 154 
 155        # 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
 156        def self.materialize(sobject_name)
 157          self.cattr_accessor :description
 158          self.cattr_accessor :type_map
 159          self.cattr_accessor :sobject_name
 160 
 161          self.sobject_name = sobject_name
 162          self.description = self.client.describe_sobject(self.sobject_name)
 163          self.type_map = {}
 164 
 165          self.description["fields"].each do |field|
 166 
 167            # Register normal fields
 168            name = field["name"]
 169            register_field( field["name"], field )
 170 
 171            # Register relationship fields.
 172            if( field["type"] == "reference" and field["relationshipName"] )
 173              register_field( field["relationshipName"], field )
 174            end
 175 
 176          end
 177        end
 178 
 179        # Returns the Force.com type of the attribute +attr_name+. Raises ArgumentError if attribute does not exist.
 180        #
 181        #    client.materialize("Car")
 182        #    Car.field_type("Color")    #=> "string"
 183        def self.field_type(attr_name)
 184          self.type_map_attr(attr_name, :type)
 185        end
 186 
 187        # Returns the label for the attribute +attr_name+. Raises ArgumentError if attribute does not exist.
 188        def self.label_for(attr_name)
 189          self.type_map_attr(attr_name, :label)
 190        end
 191 
 192        # 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.
 193        def self.picklist_values(attr_name)
 194          self.type_map_attr(attr_name, :picklist_values)
 195        end
 196 
 197        # Returns true if the attribute +attr_name+ can be updated. Raises ArgumentError if attribute does not exist.
 198        def self.updateable?(attr_name)
 199          self.type_map_attr(attr_name, :updateable?)
 200        end
 201 
 202        # Returns true if the attribute +attr_name+ can be created. Raises ArgumentError if attribute does not exist.
 203        def self.createable?(attr_name)
 204          self.type_map_attr(attr_name, :createable?)
 205        end
 206 
 207        # Delegates to Client.find with arguments +record_id+ and self
 208        #
 209        #    client.materialize("Car")
 210        #    Car.find("rid")    #=>   #<Car @Id="rid", ...>
 211        def self.find(record_id)
 212          self.client.find(self, record_id)
 213        end
 214 
 215        # Returns all records of type self as instances.
 216        #
 217        #    client.materialize("Car")
 218        #    Car.all    #=>   [#<Car @Id="1", ...>, #<Car @Id="2", ...>, #<Car @Id="3", ...>, ...]
 219        def self.all
 220          self.client.query("SELECT #{self.field_list} FROM #{self.sobject_name}")
 221        end
 222 
 223        # Returns a collection of instances of self that match the conditional +where_expr+, which is the WHERE part of a SOQL query.
 224        #
 225        #    client.materialize("Car")
 226        #    Car.query("Color = 'Blue'")    #=>   [#<Car @Id="1", @Color="Blue", ...>, #<Car @Id="5", @Color="Blue", ...>, ...]
 227        def self.query(where_expr)
 228          self.client.query("SELECT #{self.field_list} FROM #{self.sobject_name} WHERE #{where_expr}")
 229        end
 230 
 231        # Delegates to Client.search
 232        def self.search(sosl_expr)
 233          self.client.search(sosl_expr)
 234        end
 235 
 236        # Find the first record. If the +where_expr+ argument is present, it must be the WHERE part of a SOQL query
 237        def self.first(where_expr=nil)
 238          where = where_expr ? "WHERE #{where_expr} " : ""
 239          self.client.query("SELECT #{self.field_list} FROM #{self.sobject_name} #{where}ORDER BY Id ASC LIMIT 1").first
 240        end
 241 
 242        # Find the last record. If the +where_expr+ argument is present, it must be the WHERE part of a SOQL query
 243        def self.last(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 DESC LIMIT 1").first
 246        end
 247 
 248        #Delegates to Client.upsert with arguments self, +field+, +values+, and +attrs+
 249        def self.upsert(field, value, attrs)
 250          self.client.upsert(self.sobject_name, field, value, attrs)
 251        end
 252 
 253        # Delegates to Client.delete with arguments +record_id+ and self
 254        def self.delete(record_id)
 255          self.client.delete(self.sobject_name, record_id)
 256        end
 257 
 258        # Get the total number of records
 259        def self.count
 260          self.client.query("SELECT COUNT() FROM #{self.sobject_name}").total_size
 261        end
 262 
 263        # Sobject objects support dynamic finders similar to ActiveRecord.
 264        #
 265        #    client.materialize("Car")
 266        #    Car.find_by_Color("Blue")
 267        #    Car.find_all_by_Year("2011")
 268        #    Car.find_by_Color_and_Year("Blue", "2011")
 269        #    Car.find_or_create_by_Year("2011")
 270        #    Car.find_or_initialize_by_Name("Foo")
 271        def self.method_missing(method_name, *args, &block)
 272          if method_name.to_s =~ /^find_(or_create_|or_initialize_)?by_(.+)$/ || method_name.to_s =~ /^find_(all_)by_(.+)$/
 273            named_attrs = $2.split('_and_')
 274            attrs_and_values_for_find = {}
 275            hash_args = args.length == 1 && args[0].is_a?(Hash)
 276            attrs_and_values_for_write = hash_args ? args[0] : {}
 277 
 278            named_attrs.each_with_index do |attr, index|
 279              value = hash_args ? args[0][attr] : args[index]
 280              attrs_and_values_for_find[attr] = value
 281              attrs_and_values_for_write[attr] = value unless hash_args
 282            end
 283 
 284            limit_clause = method_name.to_s.include?('_all_by_') ? "" : " LIMIT 1"
 285 
 286            results = self.client.query("SELECT #{self.field_list} FROM #{self.sobject_name} WHERE #{soql_conditions_for(attrs_and_values_for_find)}#{limit_clause}")
 287            results = limit_clause == "" ? results : results.first rescue nil
 288 
 289            if results.nil?
 290              if method_name.to_s =~ /^find_or_create_by_(.+)$/
 291                results = self.client.create(self, attrs_and_values_for_write)
 292              elsif method_name.to_s =~ /^find_or_initialize_by_(.+)$/
 293                results = self.new
 294                attrs_and_values_for_write.each { |attr, val| results.send("#{attr}=", val) }
 295              end
 296            end
 297 
 298            results
 299          else
 300            super
 301          end
 302        end
 303 
 304        # Delegates to Client.create with arguments +object_attributes+ and self
 305        def self.create(object_attributes)
 306          self.client.create(self, object_attributes)
 307        end
 308 
 309        # Coerce values submitted from a Rails form to the values expected by the database
 310        # returns a new hash with updated values
 311        def self.coerce_params(params)
 312          params.each do |attr, value|
 313            case self.field_type(attr)
 314              when "boolean"
 315                params[attr] = value.is_a?(String) ? value.to_i != 0 : value
 316              when "currency", "percent", "double"
 317                params[attr] = value.to_f
 318              when "date"
 319                params[attr] = Date.parse(value) rescue Date.today
 320              when "datetime"
 321                params[attr] = DateTime.parse(value) rescue DateTime.now
 322            end
 323          end
 324        end
 325 
 326        private
 327 
 328        def self.register_field( name, field )
 329          public
 330          attr_accessor name.to_sym
 331          private
 332          self.type_map[name] = {
 333            :type => field["type"],
 334            :label => field["label"],
 335            :picklist_values => field["picklistValues"],
 336            :updateable? => field["updateable"],
 337            :createable? => field["createable"]
 338          }
 339        end
 340 
 341        def self.field_list
 342          self.description['fields'].collect { |f| f['name'] }.join(',')
 343        end
 344 
 345        def self.type_map_attr(attr_name, key)
 346          raise ArgumentError.new("No attribute named #{attr_name}") unless self.type_map.has_key?(attr_name)
 347          self.type_map[attr_name][key]
 348        end
 349 
 350        def self.soql_conditions_for(params)
 351          params.inject([]) do |arr, av|
 352            case av[1]
 353              when String
 354                value_str = "'#{av[1].gsub("'", "\\\\'")}'"
 355              when DateTime, Time
 356                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, ":")
 357              when Date
 358                value_str = av[1].strftime("%Y-%m-%d")
 359              else
 360                value_str = av[1].to_s
 361            end
 362 
 363            arr << "#{av[0]} = #{value_str}"
 364            arr
 365          end.join(" AND ")
 366        end
 367      end
 368    end
 369  end