salesforce_record 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6ad16d2a059b16aff6a15262ab0552d83d3d12f9
4
+ data.tar.gz: 48123ec4187661eae7f4cd93c20bebbd9e5c4ead
5
+ SHA512:
6
+ metadata.gz: feea79ba0c8a43da14fa76a52c2cfc253357be6ab6d480f5b3373cea59fa7f99a4f24979e244687a693e1435cbdad23a31cbadf087929f53a182e2015f29677b
7
+ data.tar.gz: 577b8ab8cec74c6834298afdb5173443d3b8c3d4f9f03dda8f1d44ca58623eb18163bba24ca5e52a9f5711777aa82ff415b2d8360f2caf990e67bc3d6884d998
@@ -0,0 +1,69 @@
1
+ # SalesforceRecord
2
+
3
+ [![Build Status](https://travis-ci.org/mru2/salesforce_record.png?branch=master)](https://travis-ci.org/mru2/salesforce_record) [![Coverage Status](https://coveralls.io/repos/mru2/salesforce_record/badge.png)](https://coveralls.io/r/mru2/salesforce_record) [![Code Climate](https://codeclimate.com/github/mru2/salesforce_record.png)](https://codeclimate.com/github/mru2/salesforce_record)
4
+
5
+ ActiveRecord-like mixin for querying, fetching and updating Salesforce models
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'salesforce_record'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install salesforce_record
20
+
21
+ ## Usage
22
+
23
+ ```
24
+ require 'salesforce_adapter'
25
+ require 'salesforce_record'
26
+
27
+ $salesforce = SalesforceAdapter::Base.new(
28
+ :url => 'https://test.salesforce.com/services/Soap/u/16.0',
29
+ :login => 'your_salesforce_login',
30
+ :password => 'your_salesforce_password'
31
+ )
32
+
33
+ class MyLeadModel
34
+ include SalesforceRecord
35
+
36
+ is_salesforce_model :Lead
37
+ sf_adapter $salesforce
38
+ sf_attributes :Company, :FirstName, :LastName
39
+ end
40
+
41
+ # Finders
42
+ > lead = MyLeadModel.find('00Qg000000ORDER66')
43
+ => #<MyLeadModel:0x007fe47983b700 @Id="00Qg000000ORDER66", @Company="JEDI inc", @FirstName="Mace", @LastName="Windu">
44
+
45
+ # Attribute getters
46
+ > lead.Company
47
+ => "JEDI inc"
48
+
49
+ # Remote setters
50
+ > lead.update_fields(:Company => 'One-arm support group')
51
+ => true
52
+
53
+ # Record creation
54
+ > new_lead = MyLeadModel.create(:LastName => 'Binks', :FirstName => 'Jar-Jar', :Company => 'Lobby for a new Autocracy')
55
+ => #<MyLeadModel:0x007fe4798e1970 @Id="00Qg0000002DUMBASS", @Company="Unwilling Dictatorship Lobby", @FirstName="Jar-Jar", @LastName="Binks">
56
+
57
+ # Queries
58
+ > results = MyLeadModel.where(:LastName => 'Windu')
59
+ => [#<MyLeadModel:0x007fe47991bee0 @Id="00Qg000000ORDER66", @Company="One-arm support group", @FirstName="Mace", @LastName="Windu">]
60
+ ```
61
+
62
+
63
+ ## Contributing
64
+
65
+ 1. Fork it ( http://github.com/<my-github-username>/salesforce_record/fork )
66
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
67
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
68
+ 4. Push to the branch (`git push origin my-new-feature`)
69
+ 5. Create new Pull Request
@@ -0,0 +1,34 @@
1
+ # Module allowing a class to wrap a salesforce object
2
+ # Define accessors and finder methods
3
+
4
+ # Usage :
5
+ # class Lead
6
+ # is_salesforce_model :Lead
7
+ # include SalesforceRecord
8
+ # sf_adapter salesforce_adapter
9
+ # sf_attributes :ConvertedAccountId, :Street
10
+ # sf_attribute :owner_email, :from => 'Owner.Email'
11
+ # sf_attribute :Date_saisie_CB__c, :type => :date
12
+ # end
13
+ #
14
+ # lead = Lead.find('my_lead_id', rforce_salesforce_adapter)
15
+ # lead.owner_email # => "my@email.tld"
16
+
17
+ require 'salesforce_record/version'
18
+
19
+ require 'salesforce_record/base'
20
+ require 'salesforce_record/attributes'
21
+ require 'salesforce_record/finder'
22
+ require 'salesforce_record/persistence'
23
+
24
+ module SalesforceRecord
25
+
26
+ def self.included(base)
27
+ # Include dependencies
28
+ base.send :include, SalesforceRecord::Base
29
+ base.send :include, SalesforceRecord::Attributes
30
+ base.send :include, SalesforceRecord::Finder
31
+ base.send :include, SalesforceRecord::Persistence
32
+ end
33
+
34
+ end
@@ -0,0 +1,152 @@
1
+ # Attributes handling
2
+ # defines #new and #attributes
3
+ # also class methods for parsing / encoding attributes hash for salesforce
4
+
5
+ require 'salesforce_record/fields'
6
+
7
+ module SalesforceRecord
8
+ module Attributes
9
+
10
+ def self.included(base)
11
+ base.send(:extend, ClassMethods)
12
+
13
+ base.instance_eval do
14
+ # Store the attributes and aliases at the class level
15
+ @salesforce_fields = {} # name => field instance
16
+
17
+ # The base attributes
18
+ sf_attribute :Id, :type => :id
19
+ end
20
+ end
21
+
22
+
23
+ module ClassMethods
24
+
25
+ # The salesforce fields and options
26
+ def salesforce_fields ; @salesforce_fields ; end
27
+
28
+ # The attributes for this model
29
+ def salesforce_attributes ; @salesforce_fields.keys ; end
30
+
31
+ # Add salesforce attributes to the model
32
+ def sf_attributes(*attrs)
33
+ attrs.each do |attr|
34
+ sf_attribute attr
35
+ end
36
+ end
37
+
38
+ # Add a salesforce attribute to the model
39
+ # Possible options :
40
+ # :from => the remote key to fetch to populate the field
41
+ # :type => one of :date, :float, :integer, :boolean, :id
42
+ def sf_attribute(attribute, opts={})
43
+
44
+ case opts[:type]
45
+ when :date
46
+ @salesforce_fields[attribute] = Fields::DateField.new(attribute, opts)
47
+ when :float
48
+ @salesforce_fields[attribute] = Fields::FloatField.new(attribute, opts)
49
+ when :integer
50
+ @salesforce_fields[attribute] = Fields::IntegerField.new(attribute, opts)
51
+ else
52
+ @salesforce_fields[attribute] = Fields::BaseField.new(attribute, opts)
53
+ end
54
+
55
+ # Set its accessor
56
+ attr_reader attribute
57
+ end
58
+
59
+
60
+ # Create an imported record from salesforce
61
+ def from_salesforce(fields)
62
+ new parse_salesforce_fields(fields)
63
+ end
64
+
65
+ # Parse fields coming from salesforce
66
+ def parse_salesforce_fields(sf_fields)
67
+ {}.tap do |parsed_fields|
68
+ sf_fields.each do |name, value|
69
+ parsed_field = parse_salesforce_field(name, value) unless value.nil?
70
+ parsed_fields.merge! parsed_field if !parsed_field.nil?
71
+ end
72
+ end
73
+ end
74
+
75
+ # Encode fields for salesforce
76
+ def encode_salesforce_fields(fields)
77
+ {}.tap do |encoded_fields|
78
+ fields.each do |name, value|
79
+ encoded_field = encode_salesforce_field(name, value)
80
+ encoded_fields.merge! encoded_field if !encoded_field.nil?
81
+ end
82
+ end
83
+ end
84
+
85
+ # Parse a field coming from salesforce (type)
86
+ def parse_salesforce_field(name, value)
87
+ # If existing, parse it depending on type
88
+ if (field = get_field name)
89
+ { field.local_name => field.parse(value) }
90
+
91
+ # Else, try to find if it is an alias
92
+ else
93
+ find_matching_alias(name => value)
94
+ end
95
+ end
96
+
97
+ # Encode a field from salesforce
98
+ def encode_salesforce_field(name, value)
99
+ # If existing, encode it depending on type and alias
100
+ if (field = get_field name) && (!field.alias?)
101
+ { field.remote_name => field.encode(value) }
102
+ end
103
+ end
104
+
105
+ def get_field(name)
106
+ self.salesforce_fields[name]
107
+ end
108
+
109
+ def has_field?(name)
110
+ !!get_field(name)
111
+ end
112
+
113
+ def field_type(name)
114
+ has_field?(name) && self.salesforce_fields[name].type
115
+ end
116
+
117
+ def field_remote_name(name)
118
+ has_field?(name) && self.salesforce_fields[name].remote_name
119
+ end
120
+
121
+
122
+ # Tries to find a salesforce alias matching a nested hash. Returns the alias name and corresponding value if found
123
+ def find_matching_alias(nested_hash)
124
+ self.salesforce_fields.each do |name, field|
125
+ if (value = field.is_alias_of(nested_hash))
126
+ return parse_salesforce_field(name, value)
127
+ end
128
+ end
129
+ return nil
130
+ end
131
+
132
+ end
133
+
134
+
135
+ # Constructor
136
+ def initialize(attributes = {})
137
+ # Iterate over the salesforce attributes and sets them
138
+ attributes.each do |name, value|
139
+ instance_variable_set :"@#{name}", value
140
+ end
141
+ end
142
+
143
+
144
+ # The attributes hash : name => parsed value
145
+ def attributes
146
+ Hash[*self.class.salesforce_attributes.map{|attr|[attr, self.send(attr)]}.flatten]
147
+ end
148
+
149
+
150
+
151
+ end
152
+ end
@@ -0,0 +1,36 @@
1
+ # Base dependencies for the salesforce models
2
+ # Handles the attribute list, the salesforce adapter, ...
3
+
4
+ module SalesforceRecord
5
+ module Base
6
+
7
+ # Bulding the class
8
+ def self.included(base)
9
+ base.send(:extend, ClassMethods)
10
+
11
+ base.instance_eval do
12
+ @salesforce_table_name = nil
13
+ @salesforce_adapter = nil
14
+ end
15
+ end
16
+
17
+
18
+ module ClassMethods
19
+
20
+ # Defines the salesforce table name
21
+ def is_salesforce_model(table_name)
22
+ @salesforce_table_name = table_name
23
+ end
24
+ def salesforce_table_name ; @salesforce_table_name ; end
25
+
26
+
27
+ # Defines the salesforce adapter
28
+ def sf_adapter(adapter)
29
+ @salesforce_adapter = adapter
30
+ end
31
+ def salesforce_adapter ; @salesforce_adapter ; end
32
+
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,80 @@
1
+ # All the possible salesforce fields
2
+ # Responsible for parsing / encoding the values
3
+ # Also handle local and remote name, if different
4
+
5
+ module SalesforceRecord
6
+ module Fields
7
+
8
+ # The base class for the fields. No values parsing or encoding
9
+ class BaseField
10
+
11
+ attr_reader :local_name
12
+
13
+ def initialize(name, opts)
14
+ @local_name = name
15
+ @remote_name = opts[:from]
16
+ end
17
+
18
+ # Parse : SF => local
19
+ def parse(value)
20
+ value
21
+ end
22
+
23
+ # Encode : local => SF
24
+ def encode(value)
25
+ value
26
+ end
27
+
28
+ def remote_name
29
+ @remote_name || @local_name
30
+ end
31
+
32
+ def alias?
33
+ !!@remote_name
34
+ end
35
+
36
+ # Check if matching nested hash
37
+ def is_alias_of(nested_hash)
38
+ return false unless @remote_name
39
+ deep_fetch(nested_hash, remote_name.split('.').map(&:to_sym))
40
+ end
41
+
42
+ private
43
+
44
+ # Helper : fetch deep value from a hash
45
+ def deep_fetch(hash, keys)
46
+ keys.inject(hash){|subhash, key| subhash.is_a?(Hash) && subhash[key] }
47
+ end
48
+ end
49
+
50
+
51
+ # A date. YYYY-MM-DD on salesforce
52
+ class DateField < BaseField
53
+ def encode(value)
54
+ value.strftime("%Y-%m-%d")
55
+ end
56
+
57
+ def parse(value)
58
+ Date.parse(value)
59
+ end
60
+ end
61
+
62
+
63
+ # A float
64
+ class FloatField < BaseField
65
+ def parse(value)
66
+ value.to_f
67
+ end
68
+ end
69
+
70
+
71
+ # An integer
72
+ class IntegerField < BaseField
73
+ def parse(value)
74
+ value.to_i
75
+ end
76
+ end
77
+
78
+
79
+ end
80
+ end
@@ -0,0 +1,101 @@
1
+ # Finder methods
2
+ # defines #find and #where
3
+
4
+ module SalesforceRecord
5
+ module Finder
6
+
7
+ def self.included(base)
8
+ base.send(:extend, ClassMethods)
9
+ end
10
+
11
+ module ClassMethods
12
+
13
+ # Returns the object matching a salesforce id
14
+ def find(id)
15
+ where(:Id => id).first
16
+ end
17
+
18
+
19
+ # Returns a list of object matching a specific query
20
+ def where(query)
21
+
22
+ # Build the matching query string
23
+ query_string = query_string(query)
24
+
25
+ # Run the query and get the results (always an array)
26
+ sf_results = salesforce_adapter.query(query_string)
27
+
28
+ # Create new objets with the results and return them
29
+ sf_results.map{|sf_attributes| from_salesforce(sf_attributes)}
30
+
31
+ end
32
+
33
+
34
+ # Creates an soql query for the resource. The query matchers are the argument
35
+ def query_string(query)
36
+
37
+ # The first letter of the model used
38
+ # i.e : Lead => l
39
+ # Used in the query : Select l.Type_de_paiement__c from Lead l where Id='id-goes-here'
40
+ key =
41
+
42
+ # Build the base query : select all the attributes in the model table (except "type" : returned but should not be queried)
43
+ query_string = "Select "
44
+ query_string << (salesforce_attributes - [:type]).map{|attribute| remote_attribute_name(attribute)}.join(", ")
45
+ query_string << " from #{salesforce_table_name} #{salesforce_query_key}"
46
+
47
+ # Add the selectors
48
+ if query.is_a? String
49
+ soql_selector = query
50
+ else
51
+ soql_selector = query.map{|attribute, value| format_where_clause(attribute, value)}.join(' AND ')
52
+ end
53
+
54
+ query_string << " WHERE #{soql_selector}"
55
+
56
+ query_string
57
+
58
+ end
59
+
60
+
61
+ # The key used to namespace the salesforce queries : first character of the table name
62
+ # i.e : Lead => l
63
+ # Used in : Select l.Type_de_paiement__c from Lead l where Id='id-goes-here'
64
+ def salesforce_query_key
65
+ self.salesforce_table_name.to_s[0, 1].downcase
66
+ end
67
+
68
+ # Aliases : the remote name for an attribute
69
+ def remote_attribute_name(attribute)
70
+ if (field = self.get_field attribute)
71
+ name_without_alias = field.remote_name
72
+ else
73
+ name_without_alias = attribute
74
+ end
75
+ "#{salesforce_query_key}.#{name_without_alias}"
76
+ end
77
+
78
+
79
+
80
+ # Create the query string for an attribute/value pair
81
+ # Returns something along the lines of "key.attribute='value'", to be injected in a WHERE clause
82
+ def format_where_clause(attribute, value)
83
+ q = "#{remote_attribute_name(attribute)}=" #exact match
84
+
85
+ # No escaping boolean values
86
+ if value.is_a?(TrueClass) || value.is_a?(FalseClass)
87
+ q << value.to_s
88
+ elsif value.is_a? Date
89
+ q << value.strftime("%Y-%m-%d")
90
+ elsif value.nil?
91
+ q << "NULL"
92
+ else
93
+ q << "'#{value}'"
94
+ end
95
+
96
+ q
97
+ end
98
+ end
99
+
100
+ end
101
+ end
@@ -0,0 +1,79 @@
1
+ # Persisting the model to salesforce
2
+ # defines #update_fields and #create methods
3
+
4
+ require 'salesforce_adapter'
5
+
6
+ module SalesforceRecord
7
+ module Persistence
8
+
9
+ def self.included(base)
10
+ base.send(:extend, ClassMethods)
11
+ end
12
+
13
+
14
+ # Pushes new field values to salesforce and updates them locally
15
+ def update_fields(fields)
16
+
17
+ update_fields = self.class.encode_attributes(fields).merge({
18
+ :Id => self.Id,
19
+ :type => self.class.salesforce_table_name.to_s
20
+ })
21
+
22
+ begin
23
+ self.class.salesforce_adapter.update( self.class.salesforce_table_name, update_fields )
24
+ saved = true
25
+ rescue SalesforceAdapter::SalesforceFailedUpdate => e
26
+ # No check of the error code necessary here
27
+ saved = false
28
+ end
29
+
30
+ # Update the fields locally if the request passed
31
+ if saved
32
+ fields.each do |attribute, value|
33
+ self.instance_variable_set(:"@#{attribute}", value)
34
+ end
35
+ end
36
+
37
+ return saved
38
+ end
39
+
40
+
41
+
42
+ module ClassMethods
43
+
44
+ # Tries to create an instance, given a set of attributes
45
+ def create(attributes)
46
+
47
+ fields = encode_attributes(attributes).merge(:type => self.salesforce_table_name.to_s)
48
+
49
+ # Tries to create it remotely
50
+ begin
51
+ id = self.salesforce_adapter.create( self.salesforce_table_name, fields )
52
+
53
+ # If successful, return an instance with the given Id
54
+ return from_salesforce(fields.merge(:Id => id))
55
+
56
+ # TODO : have a real handling of failures
57
+ rescue => e
58
+ puts e.message
59
+ return nil
60
+ end
61
+
62
+ end
63
+
64
+
65
+ def encode_attributes(attributes)
66
+ {}.tap do |encoded_attributes|
67
+ attributes.each do |name, value|
68
+ if (field = self.get_field(name))
69
+ encoded_attributes[name] = field.encode value
70
+ else
71
+ encoded_attributes[name] = value
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ end
79
+ end
@@ -0,0 +1,3 @@
1
+ module SalesforceRecord
2
+ VERSION = "1.0.0"
3
+ end
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: salesforce_record
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - ClicRDV
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-04-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: salesforce_adapter
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 1.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 1.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.5'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '1.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description:
56
+ email:
57
+ - david.ruyer@clicrdv.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - README.md
63
+ - lib/salesforce_record.rb
64
+ - lib/salesforce_record/attributes.rb
65
+ - lib/salesforce_record/base.rb
66
+ - lib/salesforce_record/fields.rb
67
+ - lib/salesforce_record/finder.rb
68
+ - lib/salesforce_record/persistence.rb
69
+ - lib/salesforce_record/version.rb
70
+ homepage: ''
71
+ licenses:
72
+ - MIT
73
+ metadata: {}
74
+ post_install_message:
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubyforge_project:
90
+ rubygems_version: 2.2.1
91
+ signing_key:
92
+ specification_version: 4
93
+ summary: ActiveRecord-like mixin for querying, fetching and updating Salesforce models
94
+ test_files: []
95
+ has_rdoc: