SalesforceARSync allows you to sync models and fields with Salesforce through a combination of Outbound Messaging, SOAP and restforce.
- Rails ~> 4.0
- Salesforce.com instance
- Have your 18 character organization id ready
- restforce gem >= 5.0.4 installed and configured see below
Before you can start syncing your data several things must be completed in Salesforce.
Create a new Remote Access Application entry by going to
Setup -> Develop -> Remote Access
You can use http://localhost/nothing for the Callback URL
Each model you wish to sync requires a workflow to trigger outbound messaging. You can set the worflow to trigger on the specific fields you wish to update.
Setup -> Create -> Workflow & Approvals -> Worflow Rules
Click New Rule, select the object (model) you wish to sync and click Next, give the rule a name, select Every time a record is created or edited and set a rule on the field(s) you want to sync ( a formula checking if the fields have changed is recommended). Click Save & Next, in the Add Worflow Action dropdown select New Outbound Message. Enter a name and set the Endpoint URL to be http://yoursite.com/integration/sf_soap/model_name. Select the fields you wish to sync (Id and SystemModstamp are required).
*You need to do this for each object/model you want to sync.
Before using the salesforce_ar_sync gem you must ensure you have the restforce gem installed and configured properly. Make sure each of the models you wish to sync are materialized.
# Note: You can name the config to anything you want
$sf_client = Restforce::Client.new("config/salesforce.yml")
$sf_client.authenticate username: <username>, password: <password>
module SalesforceArSync::SalesforceSync
SF_CLIENT = $sf_client
end
Add this line to your application's Gemfile:
gem 'salesforce_ar_sync'
And then execute:
$ bundle
Or install it yourself as:
$ gem install salesforce_ar_sync
Before using the gem you must complete the setup of your rails app.
The gem needs to know your 18 character organization id, it can be stored in a YAML file or in the ENV class.
To create the yaml file run
$ rails generate salesforce_ar_sync:configuration <organization id>
Next you will need to decide which models you want to sync. For each model you must create a migration and run them
$ rails generate salesforce_ar_sync:migrations <models> --migrate
To mount the engine add the following line to your routes.rb file
mount SalesforceArSync::Engine => '/integration'
You can change '/integration' to whatever you want, all of the engine routes will be based off of this path. Running
$ rake routes | grep salesforce_ar_sync
will show you all of the gems routes, make sure you point your outbound messages at these urls.
Next you will need to tell the gem which models are syncable by adding salesforce_syncable to your model class and specifying which attributes you would like to sync.
salesforce_syncable :sync_attributes => {FirstName: :first_name, LastName: :last_name}
The first parameter in the :sync_attributes hash is the Salesforce field name and the second is the model attribute name.
The gem can be configured using a YAML file or with the ENV variable.
The options available to configure are
- organization_id: the 18 character organization id of your Salesforce instance
- sync_enabled: a global sync enabled flag which is a boolean true/false
- namespace_prefix: Namespace prefix of your Salesforce app in case you specified one
- deletion_map: Salesforce object names mapped to internal app name.
- job_queue: A symbol naming the ActiveJob queue you wish sync and delete jobs to use. (default is :default)
To generate a YAML file
$ rails generate salesforce_ar_sync:configuration
Or with an organization id
$ rails generate salesforce_ar_sync:configuration 123456789123456789
which will create a template salesforce_ar_sync.yml in /config that looks like the following
organization_id: <organization id> #18 character organization_id
sync_enabled: true
namespace_prefix:
deletion_map:
To use the ENV variable you must pass environment variables to rails via the export command in bash or before the initializer loads the ENV settings.
$ export SALESFORCE_AR_SYNC_ORGANIZATION_ID=123456789123456789
$ export SALESFORCE_AR_SYNC_SYNC_ENABLED=true
$ export SALESFORCE_AR_NAMESPACE_PREFIX=my_prefix
An example of adding an aliased object to the deletion map should look like the following:
deletion_map:
-
Account: 'YourModelName'
The model can have several options set:
activerecord_web_id_attribute_name
Model level option to enable disable the sync, defaults to true.
salesforce_sync_enabled: false
Hash mapping of Salesforce attributes to web attributes, defaults to empty hash. "Web" attributes can be actual method names to return a custom value.If you are providing a method name to return a value, you should also implement a corresponding my_method_changed? to return if the value has changed. Otherwise it will always be synced.
sync_attributes: { Email: :login, FirstName: :first_name, LastName: :last_name }
An array of Salesforce attributes which should be synced asynchronously, defaults to an empty array. When an object is saved and only attributes contained in this array, the save to Salesforce will be queued and processed asyncronously. Use this carefully, nothing is done to ensure data integrity, if multiple jobs are queued for a single object there is no way to guarentee that they are processed in order, or that the save to Salesforce will succeed.
async_attributes: ['Last_Login__c', 'Login_Count__c']
Note: The model will fall back to synchronous sync if non-synchronous attributes are changed along with async attributes
A hash of default attributes that should be used when we are creating a new record, defaults to empty hash.
default_attributes_for_create: { password_change_required: true }
The "Id" attribute of the corresponding Salesforce object, defaults to Id.
salesforce_id_attribute_name: :Id
The field name of the web id attribute in the Salesforce Object, defaults to WebId__c
web_id_attribute_name: :WebId__c
The field name of the web id attribute in the Active Record Object, defaults to id
activerecord_web_id_attribute_name: :id
Enable or disable sync of the web id, defaults to false. Use this if you have a need for the id field of the ActiveRecord model to by synced to Salesforce.
salesforce_sync_web_id: false
Optionally can specify what fields can be used for finding the object that should be updated if object was not found by salesforce id or web id
additional_lookup_fields: { login: :User_ID_Email__c }
The name of the Web Objects class. A custom value can be provided if you wish to sync to a SF object and back to a different web object. Defaults to the model name. This would generally be used if you wanted to flatten a web object into a larger SF object like Contact.
web_class_name: 'Contact',
Optionally holds the name of a method which will return the name of the Salesforce object to sync to, defaults to nil.
salesforce_object_name: :salesforce_object_name_method_name
Optionally holds the name of a method which can contain logic to determine if a record should be synced on save. If no method is given then only the salesforce_skip_sync attribute is used. Defaults to nil.
except: :except_method_name
Optionally holds the name of a method which contains custom logic for saving on sync. Defaults to ActiveRecord::Base#save!
save_method: :save_method_name
Enable bypassing the default scope when searching for records to update. This is useful when using a soft deletion strategy that can respect SF undeletion.
unscoped_updates: true
Optionally set fields on the salesforce object that have been defined as Read Only in Salesforce. This helps to ensure that those fields are not synced from the model to salesforce (but still syncable the other way). Accepts the salesforce field, not the model's fields.
readonly_fields: %i[NumberOfPosts__c ActiveSeats__c]
Stopping the gem from syncing can be done on three levels.
- The global level before the app starts via the .yml file, ENV variables or after the app starts with the gem's configuration variable SALESFORCE_AR_SYNC_CONFIG["SYNC_ENABLED"]
- The model level by setting the :salesforce_sync_enabled => false or :except => :method_name
- The instance level by setting :salesforce_skip_sync => true in the instance
class Contact < ActiveRecord::Base
attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
attr_accessor :first_name, :last_name, :phone, :email, :last_login_time
end
class Contact < ActiveRecord::Base
attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
attr_accessor :first_name, :last_name, :phone, :email
salesforce_syncable sync_attributes: { FirstName: :first_name, LastName: :last_name, Phone: :phone, Email: :email }
end
class Contact < ActiveRecord::Base
attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
attr_accessor :first_name, :last_name, :phone, :email
salesforce_syncable sync_attributes: {FirstName: :first_name, LastName: :last_name, Phone: :phone, Email: :email},
salesforce_sync_enabled: false
end
class Contact < ActiveRecord::Base
attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
attr_accessor :first_name, :last_name, :phone, :email
salesforce_syncable sync_attributes: {FirstName: :first_name, LastName: :last_name, Phone: :phone, Email: :email},
except: :skip_sync?
def skip_sync?
if first_name.blank?
return true
end
end
end
customer = Contact.find_by_email('test@example.com')
customer.salesforce_skip_sync = true
class Contact < ActiveRecord::Base
attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
attr_accessor :first_name, :last_name, :phone, :email, :last_login_time
salesforce_syncable sync_attributes: { FirstName: :first_name, LastName: :last_name, Phone: :phone, Email: :email, Last_Login_Time__c: :last_login_time },
async_attributes: ['Last_Login_Time__c']
end
class Contact < ActiveRecord::Base
attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
attr_accessor :first_name, :last_name, :phone, :email, :last_login_time
salesforce_syncable sync_attributes: { FirstName: :first_name, LastName: :last_name, Phone: :phone, Email: :email },
default_attributes_for_create: { password_change_required: true }
end
If you want to keep the standard ActiveRecord associations in place, but need to populate these relationships from Salesforce records, you can define methods in your models to add to the attribute mapping.
The following example shows a Contact model, which is related to an Account model through account_id, we implement a getter, setter and _changed? method to do our lookups and map these methods in our sync_attributes mapping instead of the standard attributes. This allows us to send/receive messages from Salesforce using the 18 digit Salesforce id, but maintain our ActiveRecord relationships.
class Contact < ActiveRecord::Base
attributes :first_name, :last_name, :account_id
attr_accessor :first_name, :last_name, :account_id
salesforce_syncable sync_attributes: { FirstName: :first_name,
LastName: :last_name,
AccountId: :salesforce_account_id }
def salesforce_account_id_changed?
account_id_changed?
end
def salesforce_account_id
return nil if account_id.nil?
account.salesforce_id
end
def salesforce_account_id=(account_id)
self.account = nil if account_id.nil? and return
self.account = Account.find_or_create_by_salesforce_id(account_id)
end
end
class Contact < ActiveRecord::Base
attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
attr_accessor :first_name, :last_name, :phone, :email, :last_login_time
salesforce_syncable sync_attributes: { FirstName: :first_name, LastName: :last_name, Phone: :phone, Email: :email },
salesforce_object_name: :custom_salesforce_object_name
def custom_salesforce_object_name
"CustomContact__c"
end
end
In order to handle the delete of objects coming from Salesforce, a bit of code is necessary because an Outbound Message cannot be triggered when an object is deleted. To work around this you will need to create a new Custom Object in your Salesforce environment:
Deleted_Object__C
Object_Id__c_ => Text(18)
Object_Type__c_ => Text(255)
Object_Id__c will hold the 18 digit Id of the record being deleted. Object_Type__c will hold the name of the Rails Model that the Salesforce object is synced with.
If you trigger a record to be written to this object whenever another object is deleted, and configure an Outbound Message to send to the /sf_soap/delete action whenever a Deleted_Object__c record is created, the corresponding record will be removed from your Rails app.
Syncing inbound deletes is enabled by default, but can be configured in the Rails Model. This is done using the :sync_inbound_delete option, which can take either a boolean value, or the name of a method that returns a boolean value.
salesforce_syncable sync_inbound_delete: :inbound_delete
#sync_inbound_delete: true
def inbound_delete
return self.comments.count == 0
end
Syncing outbound deletes to Salesforce is disabled by default, but can be configured in the Rails Model. This is done using the :sync_outbound_delete option, which can take either a boolean value, or the name of a method that returns a boolean value.
salesforce_syncable sync_outbound_delete: :outbound_delete
#sync_outbound_delete: false
def outbound_delete
return self.is_trial_user?
end
Setting unscoped_updates to true will permit you to find deleted objects to sync changes to. You can implement virtual attributes matching the undelete field in SF to implement the soft deletion strategy of your choosing. For the paranoia gem:
def undeleted=(value)
restore if value
end
If the SOAP handler encounters an error it will be recorded in the log of the outbound message in Salesforce. To view the message go to
Setup -> Monitoring -> Outbound Messages
Your 15 character organization id can be found in Setup -> Company Profile -> Company Information. You must convert it to an 18 character id by running it through the tool located here: http://cloudjedi.wordpress.com/no-fuss-salesforce-id-converter/ or by installing the Force.com Utility Belt for Chrome.
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Added some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request