salesforce_ar_sync 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Liam Nediger
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,413 @@
1
+ # SalesforceArSync
2
+
3
+ SalesforceARSync allows you to sync models and fields with Salesforce through a combination of
4
+ Outbound Messaging, SOAP and databasedotcom.
5
+
6
+ ## Installation
7
+
8
+ ### Requirements
9
+
10
+ * Rails >= 3.1
11
+ * Salesforce.com instance
12
+ * [Have your 18 character organization id ready](#finding-your-18-character-organization-id)
13
+ * databasedotcom gem >= 1.3 installed and configured [see below](#databasedotcom)
14
+ * delayed_job gem >= 3.0 installed and configured
15
+
16
+ ### Salesforce Setup
17
+
18
+ Before you can start syncing your data several things must be completed in Salesforce.
19
+
20
+ #### 1. Setup Remote Access
21
+ Create a new Remote Access Application entry by going to
22
+
23
+ Setup -> Develop -> Remote Access
24
+
25
+ You can use http://localhost/nothing for the _Callback URL_
26
+
27
+ #### 2. Setup Outbound Messaging
28
+ Each model you wish to sync requires a workflow to trigger outbound messaging. You can set the worflow
29
+ to trigger on the specific fields you wish to update.
30
+
31
+ Setup -> Create -> Workflow & Approvals -> Worflow Rules
32
+
33
+ Click _New Rule_, select the object (model) you wish to sync and click _Next_, give the rule a name, select
34
+ _Every time a record is created or edited_ and set a rule on the field(s) you want to sync ( a formula checking
35
+ if the fields have changed is recommended). Click _Save & Next_, in the _Add Worflow Action_ dropdown select
36
+ _New Outbound Message_. Enter a name and set the _Endpoint URL_ to be http://yoursite.com/integration/sf_soap/model_name.
37
+ Select the fields you wish to sync (Id and SystemModstamp are required).
38
+
39
+ *You need to do this for each object/model you want to sync.
40
+
41
+ ### databasedotcom
42
+
43
+ Before using the salesforce_ar_sync gem you must ensure you have the databasedotcom gem installed and configured
44
+ properly. Make sure each of the models you wish to sync are materialized.
45
+
46
+ ````ruby
47
+ $sf_client = Databasedotcom::Client.new("config/databasedotcom.yml")
48
+ $sf_client.authenticate :username => <username>, :password => <password>
49
+ $sf_client.sobject_module = Databasedotcom
50
+ $sf_client.materialize "User"
51
+ ````
52
+
53
+ ### Gem Installation
54
+
55
+ Add this line to your application's Gemfile:
56
+
57
+ gem 'salesforce_ar_sync'
58
+
59
+ And then execute:
60
+
61
+ $ bundle
62
+
63
+ Or install it yourself as:
64
+
65
+ $ gem install salesforce_ar_sync
66
+
67
+ ### Application Setup
68
+
69
+ Before using the gem you must complete the setup of your rails app.
70
+
71
+ The gem needs to know your 18 character organization id, it can be stored in a YAML file or in the ENV class.
72
+
73
+ To create the yaml file run
74
+
75
+ $ rails generate salesforce_ar_sync:configuration <organization id>
76
+
77
+ Next you will need to decide which models you want to sync. For each model you must create a migration and run them
78
+
79
+ $ rails generate salesforce_ar_sync:migrations <models> --migrate
80
+
81
+ To mount the engine add the following line to your routes.rb file
82
+
83
+ mount SalesforceArSync::Engine => '/integration'
84
+
85
+ You can change '/integration' to whatever you want, all of the engine routes will be based off of this path. Running
86
+
87
+ $ rake routes | grep salesforce_ar_sync
88
+
89
+ will show you all of the gems routes, make sure you point your outbound messages at these urls.
90
+
91
+ Next you will need to tell the gem which models are syncable by adding _salesforce_syncable_ to your model class
92
+ and specifying which attributes you would like to sync.
93
+
94
+ ````ruby
95
+ salesforce_syncable :sync_attributes => {:FirstName => :first_name, :LastName => :last_name}
96
+ ````
97
+
98
+ The first parameter in the _:sync_attributes_ hash is the Salesforce field name and the second is the model attribute
99
+ name.
100
+
101
+ ## Usage
102
+
103
+ ### Configuration Options
104
+
105
+ The gem can be configured using a YAML file or with the ENV variable.
106
+
107
+ The options available to configure are
108
+
109
+ * __organization_id__: the 18 character organization id of your Salesforce instance
110
+ * __sync_enabled__: a global sync enabled flag which is a boolean true/false
111
+
112
+ To generate a YAML file
113
+
114
+ $ rails generate salesforce_ar_sync:configuration
115
+
116
+ Or with an organization id
117
+
118
+ $ rails generate salesforce_ar_sync:configuration 123456789123456789
119
+
120
+ which will create a template salesforce_ar_sync.yml in /config that looks like the following
121
+
122
+ organization_id: <organization id> #18 character organization_id
123
+ sync_enabled: true
124
+
125
+
126
+ To use the ENV variable you must pass environemnt variables to rails via the _export_ command in bash or before the
127
+ initializer loads the ENV settings.
128
+
129
+ $ export SALESFORCE_AR_SYNC_ORGANIZATION_ID=123456789123456789
130
+ $ export SALESFORCE_AR_SYNC_SYNC_ENABLED=true
131
+
132
+ ### Model Options
133
+ The model can have several options set:
134
+
135
+ [__salesforce_sync_enabled__](#salesforce_sync_enabled)
136
+ [__sync_attributes__](#sync_attributes)
137
+ [__async_attributes__](#async_attributes)
138
+ [__default_attributes_for_create__](#default_attributes_for_create)
139
+ [__salesforce_id_attribute_name__](#salesforce_id_attribute_name)
140
+ [__web_id_attribute_name__](#web_id_attribute_name)
141
+ [__salesforce_sync_web_id__](#salesforce_sync_web_id)
142
+ [__web_class_name__](#web_class_name)
143
+ [__salesforce_object_name__](#salesforce_object_name)
144
+ [__salesforce_object__](#salesforce_object)
145
+ [__except__](#except)
146
+
147
+ #### <a id="salesforce_sync_enabled"></a>salesforce_sync_enabled
148
+ Model level option to enable disable the sync, defaults to true.
149
+
150
+ ````ruby
151
+ :salesforce_sync_enabled => false
152
+ ````
153
+
154
+ #### sync_attributes
155
+ Hash mapping of Salesforce attributes to web attributes, defaults to empty hash.
156
+ "Web" attributes can be actual method names to return a custom value.If you are providing a method name to return a
157
+ value, you should also implement a corresponding my_method_changed? to return if the value has changed. Otherwise
158
+ it will always be synced.
159
+
160
+ ````ruby
161
+ :sync_attributes => { :Email => :login, :FirstName => :first_name, :LastName => :last_name }
162
+ ````
163
+
164
+ #### async_attributes
165
+ 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.
166
+ 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.
167
+
168
+ ````ruby
169
+ :async_attributes => ["Last_Login__c", "Login_Count__c"]
170
+ ````
171
+
172
+ Note: The model will fall back to synchronous sync if non-synchronous attributes are changed along with async
173
+ attributes
174
+
175
+ #### default_attributes_for_create
176
+ A hash of default attributes that should be used when we are creating a new record, defaults to empty hash.
177
+
178
+ ````ruby
179
+ :default_attributes_for_create => {:password_change_required => true}
180
+ ````
181
+
182
+ #### salesforce_id_attribute_name
183
+ The "Id" attribute of the corresponding Salesforce object, defaults to _Id_.
184
+
185
+ ````ruby
186
+ :salesforce_id_attribute_name => :Id
187
+ ````
188
+
189
+ #### web_id_attribute_name
190
+ The field name of the web id attribute in the Salesforce Object, defaults to _WebId__c_
191
+
192
+ ````ruby
193
+ :web_id_attribute_name => :WebId__c
194
+ ````
195
+
196
+ #### salesforce_sync_web_id
197
+ 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.
198
+
199
+ ````ruby
200
+ :salesforce_sync_web_id => false
201
+ ````
202
+
203
+ #### web_class_name
204
+ 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
205
+ different web object. Defaults to the model name. This would generally be used if you wanted to flatten a web object
206
+ into a larger SF object like Contact.
207
+
208
+ ````ruby
209
+ :web_class_name => 'Contact',
210
+ ````
211
+
212
+ #### salesforce_object_name
213
+ Optionally holds the name of a method which will return the name of the Salesforce object to sync to, defaults to nil.
214
+
215
+ ````ruby
216
+ :salesforce_object_name => :salesforce_object_name_method_name
217
+ ````
218
+
219
+ #### salesforce_object
220
+ Optionally holds the name of a method which will retrieve a Salesforce object. Default implementation is called if no
221
+ method is specified, defaults to nil.
222
+
223
+ ````ruby
224
+ :salesforce_object => :salesforce_object_method_name
225
+ ````
226
+
227
+ #### except
228
+ Optionally holds the name of a method which can contain logic to determine if a record should be synced on save. If no
229
+ method is given then only the salesforce_skip_sync attribute is used. Defaults to nil.
230
+
231
+ ````ruby
232
+ :except => :except_method_name
233
+ ````
234
+
235
+ ### Stopping the Sync
236
+
237
+ Stopping the gem from syncing can be done on three levels.
238
+
239
+ * The global level before the app starts via the .yml file, ENV variables or after the app starts with the gem's
240
+ configuration variable _SALESFORCE_AR_SYNC_CONFIG["SYNC_ENABLED"]_
241
+ * The model level by setting the _:salesforce_sync_enabled => false_ or _:except => :method_name_
242
+ * The instance level by setting _:salesforce_skip_sync => true_ in the instance
243
+
244
+ ## Examples
245
+
246
+ ### Our Basic Example Model
247
+
248
+ ```ruby
249
+ class Contact < ActiveRecord::Base
250
+ attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
251
+ attr_accessor :first_name, :last_name, :phone, :email, :last_login_time
252
+ end
253
+ ```
254
+
255
+ ### Making the Model Syncable
256
+
257
+ ```ruby
258
+ class Contact < ActiveRecord::Base
259
+ attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
260
+ attr_accessor :first_name, :last_name, :phone, :email
261
+
262
+ salesforce_syncable :sync_attributes => {:FirstName => :first_name, :LastName => :last_name, :Phone => :phone, :Email => :email}
263
+ end
264
+ ```
265
+
266
+ ### Stopping the Model from Syncing with a Flag
267
+
268
+ ```ruby
269
+ class Contact < ActiveRecord::Base
270
+ attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
271
+ attr_accessor :first_name, :last_name, :phone, :email
272
+
273
+ salesforce_syncable :sync_attributes => {:FirstName => :first_name, :LastName => :last_name, :Phone => :phone, :Email => :email},
274
+ :salesforce_sync_enabled => false
275
+ end
276
+ ```
277
+
278
+ ### Stopping the Model from Syncing with a Method
279
+
280
+ ```ruby
281
+ class Contact < ActiveRecord::Base
282
+ attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
283
+ attr_accessor :first_name, :last_name, :phone, :email
284
+
285
+ salesforce_syncable :sync_attributes => {:FirstName => :first_name, :LastName => :last_name, :Phone => :phone, :Email => :email},
286
+ :except => :skip_sync?
287
+
288
+ def skip_sync?
289
+ if first_name.blank?
290
+ return true
291
+ end
292
+ end
293
+ end
294
+ ```
295
+
296
+ ### Stopping a Record from Syncing
297
+
298
+ ```ruby
299
+ customer = Contact.find_by_email('test@example.com')
300
+ customer.salesforce_skip_sync = true
301
+ ```
302
+
303
+ ### Specify Async Attributes
304
+
305
+ ```ruby
306
+ class Contact < ActiveRecord::Base
307
+ attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
308
+ attr_accessor :first_name, :last_name, :phone, :email, :last_login_time
309
+
310
+ salesforce_syncable :sync_attributes => {:FirstName => :first_name, :LastName => :last_name, :Phone => :phone, :Email => :email, :Last_Login_Time__c => :last_login_time},
311
+ :async_attributes => ["Last_Login_Time__c"]
312
+ end
313
+ ```
314
+
315
+ ### Specify Default Attributes when an Object is Created
316
+
317
+ ```ruby
318
+ class Contact < ActiveRecord::Base
319
+ attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
320
+ attr_accessor :first_name, :last_name, :phone, :email, :last_login_time
321
+
322
+ salesforce_syncable :sync_attributes => {:FirstName => :first_name, :LastName => :last_name, :Phone => :phone, :Email => :email},
323
+ :default_attributes_for_create => {:password_change_required => true}
324
+ end
325
+ ```
326
+
327
+ ### Relationships
328
+ If you want to keep the standard ActiveRecord associations in place, but need to populate these relationships from Salesforce records, you can define
329
+ methods in your models to add to the attribute mapping.
330
+
331
+ 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
332
+ 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
333
+ using the 18 digit Salesforce id, but maintain our ActiveRecord relationships.
334
+
335
+ ```ruby
336
+
337
+ class Contact < ActiveRecord::Base
338
+ attributes :first_name, :last_name, :account_id
339
+ attr_accessor :first_name, :last_name, :account_id
340
+
341
+ salesforce_syncable :sync_attributes => { :FirstName => :first_name,
342
+ :LastName => :last_name,
343
+ :AccountId => :salesforce_account_id }
344
+
345
+ def salesforce_account_id_changed?
346
+ account_id_changed?
347
+ end
348
+
349
+ def salesforce_account_id
350
+ return nil if account_id.nil?
351
+ account.salesforce_id
352
+ end
353
+
354
+ def salesforce_account_id=(account_id)
355
+ self.account = nil and return if account_id.nil?
356
+ self.account = Account.find_or_create_by_salesforce_id(account_id)
357
+ end
358
+ end
359
+
360
+ ```
361
+
362
+ ### Defining a Custom Salesforce Object
363
+
364
+ ```ruby
365
+ class Contact < ActiveRecord::Base
366
+ attributes :first_name, :last_name, :phone, :email, :last_login_time, :salesforce_id, :salesforce_updated_at
367
+ attr_accessor :first_name, :last_name, :phone, :email, :last_login_time
368
+
369
+ salesforce_syncable :sync_attributes => {:FirstName => :first_name, :LastName => :last_name, :Phone => :phone, :Email => :email},
370
+ :salesforce_object => :custom_salesforce_object
371
+
372
+ def custom_salesforce_object
373
+ "CustomContact__c"
374
+ end
375
+ end
376
+ ```
377
+
378
+ ## Deletes
379
+ 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
380
+ an object is deleted. To work around this you will need to create a new Custom Object in your Salesforce environment:
381
+
382
+ ```
383
+ Deleted_Object__C
384
+ Object_Id__c_ => Text(18)
385
+ Object_Type__c_ => Text(255)
386
+ ```
387
+
388
+ Object_Id__c will hold the 18 digit Id of the record being deleted.
389
+ Object_Type__c will hold the name of the Rails Model that the Salesforce object is synced with.
390
+
391
+ 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
392
+ whenever a Deleted_Object__c record is created, the corresponding record will be removed from your Rails app.
393
+
394
+ ## Errors
395
+
396
+ ### Outbound Message Errors
397
+
398
+ 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
399
+
400
+ Setup -> Monitoring -> Outbound Messages
401
+
402
+ ## <a id="orga_id"></a>Finding your 18 Character Organization ID
403
+ Your 15 character organization id can be found in _Setup -> Company Profile -> Company Information_. You must convert
404
+ it to an 18 character id by running it through the tool located here:
405
+ http://cloudjedi.wordpress.com/no-fuss-salesforce-id-converter/ or by installing the Force.com Utility Belt for Chrome.
406
+
407
+ ## Contributing
408
+
409
+ 1. Fork it
410
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
411
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
412
+ 4. Push to the branch (`git push origin my-new-feature`)
413
+ 5. Create new Pull Request
@@ -0,0 +1,17 @@
1
+ module SalesforceArSync
2
+ module Generators
3
+ class ConfigurationGenerator < Rails::Generators::Base
4
+ include Rails::Generators::Migration
5
+
6
+ source_root File.expand_path('../templates', __FILE__)
7
+
8
+ desc "Generates migrations to add required columns for Salesforce sync on specified models."
9
+
10
+ argument :organization_id, :type => :string, :banner => "organization_id", :default => "#18 character organization_id"
11
+
12
+ def create_yaml
13
+ template "salesforce_ar_sync.yml", "config/salesforce_ar_sync.yml"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ production:
2
+ organization_id: <%= @organization_id %>
3
+ sync_enabled: true
4
+ # Salesforce owned IPs from: https://help.salesforce.com/apex/HTViewSolution?language=en_US&id=000003652
5
+ ip_ranges: "204.14.232.0/23,204.14.237.0/24,96.43.144.0/22,96.43.148.0/22,204.14.234.0/23,204.14.238.0/23,182.50.76.0/22"
6
+
7
+ development:
8
+ organization_id: <%= @organization_id %>
9
+ sync_enabled: false
10
+ # Salesforce owned IPs from: https://help.salesforce.com/apex/HTViewSolution?language=en_US&id=000003652
11
+ ip_ranges: "204.14.232.0/23,204.14.237.0/24,96.43.144.0/22,96.43.148.0/22,204.14.234.0/23,204.14.238.0/23,182.50.76.0/22"
12
+
13
+ test:
14
+ organization_id: <%= @organization_id %>
15
+ sync_enabled: false
16
+ # Salesforce owned IPs from: https://help.salesforce.com/apex/HTViewSolution?language=en_US&id=000003652
17
+ ip_ranges: "204.14.232.0/23,204.14.237.0/24,96.43.144.0/22,96.43.148.0/22,204.14.234.0/23,204.14.238.0/23,182.50.76.0/22"
@@ -0,0 +1,53 @@
1
+ require 'rails/generators/active_record'
2
+
3
+ module SalesforceArSync
4
+ module Generators
5
+ class MigrationsGenerator < Rails::Generators::Base
6
+ include Rails::Generators::Migration
7
+
8
+ source_root File.expand_path('../templates', __FILE__)
9
+
10
+ desc "Generates migrations to add required columns for Salesforce sync on specified models."
11
+
12
+ #The migrate option is used to specify whether to run the migrations in the end or not
13
+ class_option :migrate, :type => :string, :banner => "[yes|no]", :lazy_default => "yes"
14
+ #The list of models to create migrations for
15
+ argument :models, :type => :array, :banner => "model1 model2 model3...", :required => true
16
+
17
+
18
+ def create_migrations
19
+ models.each do |model|
20
+ create_migration(model)
21
+ end
22
+
23
+ if options[:migrate] == "yes"
24
+ say "Performing Migrations"
25
+ run_migrations
26
+ end
27
+ end
28
+
29
+ protected
30
+
31
+ def run_migrations
32
+ rake("db:migrate")
33
+ end
34
+
35
+ def self.next_migration_number(path)
36
+ unless @prev_migration_nr
37
+ @prev_migration_nr = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
38
+ else
39
+ @prev_migration_nr += 1
40
+ end
41
+ @prev_migration_nr.to_s
42
+ end
43
+
44
+ def create_migration(model_name)
45
+ #we can't load all the models in so let's assume it follows the standard nameing convention
46
+ @table_name = model_name.tableize
47
+ @model_name = model_name
48
+
49
+ migration_template "migration.rb", "db/migrate/add_salesforce_fields_to_#{@table_name}.rb"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,13 @@
1
+ class AddSalesforceFieldsTo<%= @model_name.pluralize %> < ActiveRecord::Migration
2
+ def up
3
+ add_column :<%= @table_name %>, :salesforce_id, :string, :limit => 18
4
+ add_column :<%= @table_name %>, :salesforce_updated_at, :datetime
5
+
6
+ add_index :<%= @table_name %>, :salesforce_id, :unique => true
7
+ end
8
+
9
+ def down
10
+ remove_column :<%= @table_name %>, :salesforce_id
11
+ remove_column :<%= @table_name %>, :salesforce_updated_at
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ require 'salesforce_ar_sync/engine'
2
+ require 'salesforce_ar_sync/version'
3
+ require 'salesforce_ar_sync/extenders/salesforce_syncable'
4
+ require 'salesforce_ar_sync/salesforce_object_sync'
5
+ require 'salesforce_ar_sync/soap_handler/base'
6
+ require 'salesforce_ar_sync/soap_handler/delete'
7
+ require 'salesforce_ar_sync/ip_constraint'
8
+
9
+ module SalesforceArSync
10
+ mattr_accessor :app_root
11
+
12
+ def self.setup
13
+ yield self
14
+ end
15
+ end
16
+
17
+ if defined?(ActiveRecord::Base)
18
+ ActiveRecord::Base.extend SalesforceArSync::Extenders::SalesforceSyncable
19
+ end
@@ -0,0 +1,9 @@
1
+ module SalesforceArSync
2
+ class Engine < ::Rails::Engine
3
+ initializer "salesforce_ar_sync.load_app_instance_data" do |app|
4
+ SalesforceArSync.setup do |config|
5
+ config.app_root = app.root
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,57 @@
1
+ module SalesforceArSync
2
+ module Extenders
3
+ module SalesforceSyncable
4
+ def salesforce_syncable(options = {})
5
+ require 'salesforce_ar_sync/salesforce_sync'
6
+ include SalesforceArSync::SalesforceSync
7
+
8
+ self.salesforce_sync_enabled = options.has_key?(:salesforce_sync_enabled) ? options[:salesforce_sync_enabled] : true
9
+ self.salesforce_sync_attribute_mapping = options.has_key?(:sync_attributes) ? options[:sync_attributes].stringify_keys : {}
10
+ self.salesforce_async_attributes = options.has_key?(:async_attributes) ? options[:async_attributes] : {}
11
+ self.salesforce_default_attributes_for_create = options.has_key?(:default_attributes_for_create) ? options[:default_attributes_for_create] : {}
12
+ self.salesforce_id_attribute_name = options.has_key?(:salesforce_id_attribute_name) ? options[:salesforce_id_attribute_name] : :Id
13
+ self.salesforce_web_id_attribute_name = options.has_key?(:web_id_attribute_name) ? options[:web_id_attribute_name] : :WebId__c
14
+ self.salesforce_sync_web_id = options.has_key?(:salesforce_sync_web_id) ? options[:salesforce_sync_web_id] : false
15
+ self.salesforce_web_class_name = options.has_key?(:web_class_name) ? options[:web_class_name] : self.name
16
+
17
+ self.salesforce_object_name_method = options.has_key?(:salesforce_object_name) ? options[:salesforce_object_name] : nil
18
+ self.salesforce_object_method = options.has_key?(:salesforce_object) ? options[:salesforce_object] : nil
19
+ self.salesforce_skip_sync_method = options.has_key?(:except) ? options[:except] : nil
20
+
21
+ instance_eval do
22
+ before_save :salesforce_sync
23
+ after_create :sync_web_id
24
+
25
+ def salesforce_sync_web_id?
26
+ self.salesforce_sync_web_id
27
+ end
28
+ end
29
+
30
+ class_eval do
31
+ # Calls a method if provided to return the name of the Salesforce object the model is syncing to.
32
+ # If no method is provided, defaults to the class name
33
+ def salesforce_object_name
34
+ return send(self.class.salesforce_object_name_method) if self.class.salesforce_object_name_method.present?
35
+ return self.class.name
36
+ end
37
+
38
+ # Calls a method, if provided, to retrieve an object from Salesforce. Calls the default implementation if
39
+ # no custom method is specified
40
+ def salesforce_object
41
+ return send(self.class.salesforce_object_method) if self.class.salesforce_object_method.present?
42
+ return send(:salesforce_object_default)
43
+ end
44
+
45
+ # Calls a method, if provided, to determine if a record should be synced to Salesforce.
46
+ # The salesforce_skip_sync instance variable is also used.
47
+ # The SALESFORCE_AR_SYNC_ENABLED flag overrides all the others if set to false
48
+ def salesforce_skip_sync?
49
+ return true if SALESFORCE_AR_SYNC_CONFIG["SYNC_ENABLED"] == false
50
+ return (salesforce_skip_sync || !self.class.salesforce_sync_enabled || send(self.class.salesforce_skip_sync_method)) if self.class.salesforce_skip_sync_method.present?
51
+ return (salesforce_skip_sync || !self.class.salesforce_sync_enabled)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,17 @@
1
+ require 'ipaddr'
2
+
3
+ module SalesforceArSync
4
+ class IPConstraint
5
+ def initialize
6
+ @ip_ranges = SALESFORCE_AR_SYNC_CONFIG["IP_RANGES"]
7
+ end
8
+
9
+ def matches?(request)
10
+ if Rails.env == 'development' || Rails.env == 'test'
11
+ true
12
+ else
13
+ @ip_ranges.any?{|r| IPAddr.new(r).include?(IPAddr.new(request.remote_ip)) }
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,16 @@
1
+ module SalesforceArSync
2
+ # simple object to be serialized when asynchronously sending data to Salesforce
3
+ class SalesforceObjectSync < Struct.new(:web_object_name, :salesforce_object_name, :salesforce_id, :attributes)
4
+ def perform
5
+ sf_object = "Databasedotcom::#{salesforce_object_name}".constantize.find_by_Id salesforce_id
6
+
7
+ if sf_object
8
+ sf_object.update_attributes(attributes)
9
+ sf_object.reload
10
+
11
+ web_object = "#{web_object_name}".constantize.find_by_salesforce_id salesforce_id
12
+ web_object.update_attribute(:salesforce_updated_at, sf_object.SystemModstamp) unless web_object.nil?
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,197 @@
1
+ require 'active_support/concern'
2
+
3
+ module SalesforceArSync
4
+ module SalesforceSync
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ # Optionally holds the value to determine if salesforce syncing is enabled. Defaults to true. If set
9
+ # to false syncing will be disabled for the class
10
+ attr_accessor :salesforce_sync_enabled
11
+
12
+ # Hash mapping of Salesforce attributes to web attributes
13
+ # Example:
14
+ # { :Email => :login, :FirstName => :first_name, :LastName => :last_name }
15
+ #
16
+ # "Web" attributes can be actual method names to return a custom value
17
+ # If you are providing a method name to return a value, you should also implement a corresponding my_method_changed? to
18
+ # return if the value has changed. Otherwise it will always be synced.
19
+ attr_accessor :salesforce_sync_attribute_mapping
20
+
21
+ # Returns an array of Salesforce attributes which should be synced asynchronously
22
+ # Example: ["Last_Login_Date__c", "Login_Count__c" ]
23
+ # Note: The model will fall back to synchronous sync if non-synchronous attributes are changed along with async attributes
24
+ attr_accessor :salesforce_async_attributes
25
+
26
+ # Returns a hash of default attributes that should be used when we are creating a new record
27
+ attr_accessor :salesforce_default_attributes_for_create
28
+
29
+ # Returns the "Id" attribute of the corresponding Salesforce object
30
+ attr_accessor :salesforce_id_attribute_name
31
+
32
+ # Returns the name of the Web Objects class. A custom value can be provided if you wish
33
+ # to sync to a SF object and back to a different web object. This would generally be used
34
+ # if you wanted to flatten a web object into a larger SF object like Contact
35
+ attr_accessor :salesforce_web_class_name
36
+
37
+ attr_accessor :salesforce_web_id_attribute_name
38
+ attr_accessor :salesforce_sync_web_id
39
+
40
+ # Optionally holds the name of a method which will return the name of the Salesforce object to sync to
41
+ attr_accessor :salesforce_object_name_method
42
+
43
+ # Optionally holds the name of a method which will retrieve a Salesforce object. Default implementation is called
44
+ # if no method is specified
45
+ attr_accessor :salesforce_object_method
46
+
47
+ # Optionally holds the name of a method which can contain logic to determine if a record should be synced on save.
48
+ # If no method is given then only the salesforce_skip_sync attribute is used.
49
+ attr_accessor :salesforce_skip_sync_method
50
+
51
+ # Accepts values from an outbound message hash and will either update an existing record OR create a new record
52
+ # Firstly attempts to find an object by the salesforce_id attribute
53
+ # Secondly attempts to look an object up by it's ID (WebId__c in outbound message)
54
+ # Lastly it will create a new record setting it's salesforce_id
55
+ def salesforce_update(attributes={})
56
+ raise ArgumentError, "#{salesforce_id_attribute_name} parameter required" if attributes[salesforce_id_attribute_name].blank?
57
+
58
+ object = self.find_by_salesforce_id attributes[salesforce_id_attribute_name]
59
+ object ||= self.find_by_id attributes[salesforce_web_id_attribute_name] if salesforce_sync_web_id? && attributes[salesforce_web_id_attribute_name]
60
+
61
+ if object.nil?
62
+ object = self.new
63
+ salesforce_default_attributes_for_create.merge(:salesforce_id => attributes[salesforce_id_attribute_name]).each_pair do |k, v|
64
+ object.send("#{k}=", v)
65
+ end
66
+ end
67
+
68
+ object.salesforce_process_update(attributes) if object && (object.salesforce_updated_at.nil? || (object.salesforce_updated_at && object.salesforce_updated_at < Time.parse(attributes[:SystemModstamp])))
69
+ end
70
+ end
71
+
72
+ # if this instance variable is set to true, the salesforce_sync method will return without attempting
73
+ # to sync data to Salesforce
74
+ attr_accessor :salesforce_skip_sync
75
+
76
+ # Salesforce completely excludes any empty/null fields from Outbound Messages
77
+ # We initialize all declared attributes as nil before mapping the values from the message
78
+ def salesforce_empty_attributes
79
+ {}.tap do |hash|
80
+ self.class.salesforce_sync_attribute_mapping.each do |key, value|
81
+ hash[key] = nil
82
+ end
83
+ end
84
+ end
85
+
86
+ # An internal method used to get a hash of values that we are going to set from a Salesforce outbound message hash
87
+ def salesforce_attributes_to_set(attributes = {})
88
+ {}.tap do |hash|
89
+ # loop through the hash of attributes from the outbound message, and compare to our sf mappings and
90
+ # create a reversed hash of value's and key's to pass to update_attributes
91
+ attributes.each do |key, value|
92
+ # make sure our sync_mapping contains the salesforce attribute AND that our object has a setter for it
93
+ hash[self.class.salesforce_sync_attribute_mapping[key.to_s].to_sym] = value if self.class.salesforce_sync_attribute_mapping.include?(key.to_s) && self.respond_to?("#{self.class.salesforce_sync_attribute_mapping[key.to_s]}=")
94
+ end
95
+
96
+ # remove the web_id from hash if it exists, as we don't want to modify a web_id
97
+ hash.delete(:id) if hash[:id]
98
+
99
+ # update the sf_updated_at field with the system mod stamp from sf
100
+ hash[:salesforce_updated_at] = attributes[:SystemModstamp]
101
+
102
+ # incase we looked up via the WebId__c, we should set the salesforce_id
103
+ hash[:salesforce_id] = attributes[self.class.salesforce_id_attribute_name]
104
+ end
105
+ end
106
+
107
+ # Gets passed the Salesforce outbound message hash of changed values and updates the corresponding model
108
+ def salesforce_process_update(attributes = {})
109
+ attributes_to_update = salesforce_attributes_to_set(self.new_record? ? attributes : salesforce_empty_attributes.merge(attributes)) # only merge empty attributes for updates, so we don't overwrite the default create attributes
110
+ attributes_to_update.each_pair do |k, v|
111
+ self.send("#{k}=", v)
112
+ end
113
+
114
+ # we don't want to keep going in a endless loop. SF has just updated these values.
115
+ self.salesforce_skip_sync = true
116
+ self.save!
117
+ end
118
+
119
+ # This method will fetch the salesforce object and go looking for it if it doesn't exist
120
+ def salesforce_object_default
121
+ return @sf_object if @sf_object.present?
122
+
123
+ @sf_object = "Databasedotcom::#{self.salesforce_object_name}".constantize.send("find_by_#{self.class.salesforce_id_attribute_name.to_s}", salesforce_id) unless salesforce_id.nil?
124
+
125
+ # Look up and link object by web id
126
+ @sf_object ||= "Databasedotcom::#{self.salesforce_object_name}".constantize.send("find_by_#{self.class.salesforce_web_id_attribute_name.to_s}", id) if self.class.salesforce_sync_web_id? && !new_record?
127
+
128
+ return @sf_object
129
+ end
130
+
131
+ # Checks if the passed in attribute should be updated in Salesforce.com
132
+ def salesforce_should_update_attribute?(attribute)
133
+ !self.respond_to?("#{attribute}_changed?") || (self.respond_to?("#{attribute}_changed?") && self.send("#{attribute}_changed?"))
134
+ end
135
+
136
+ # create a hash of updates to send to salesforce
137
+ def salesforce_attributes_to_update(include_all = false)
138
+ {}.tap do |hash|
139
+ self.class.salesforce_sync_attribute_mapping.each do |key, value|
140
+ if self.respond_to?(value)
141
+
142
+ #Checkboxes in SFDC Cannot be nil. Here we check for boolean field type and set nil values to be false
143
+ attribute_value = self.send(value)
144
+ if is_boolean?(value) && attribute_value.nil?
145
+ attribute_value = false
146
+ end
147
+
148
+ hash[key] = attribute_value if include_all || salesforce_should_update_attribute?(value)
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ def is_boolean?(attribute)
155
+ self.column_for_attribute(attribute) && self.column_for_attribute(attribute).type == :boolean
156
+ end
157
+
158
+ def salesforce_create_object(attributes)
159
+ attributes.merge!(self.class.salesforce_web_id_attribute_name.to_s => id) if self.class.salesforce_sync_web_id? && !new_record?
160
+ @sf_object = "Databasedotcom::#{self.salesforce_object_name}".constantize.create(attributes)
161
+ end
162
+
163
+ def salesforce_update_object(attributes)
164
+ attributes.merge!(self.class.salesforce_web_id_attribute_name.to_s => id) if self.class.salesforce_sync_web_id? && !new_record?
165
+ salesforce_object.update_attributes(attributes) if salesforce_object
166
+ end
167
+
168
+ # if attributes specified in the async_attributes array are the only attributes being modified, then sync the data
169
+ # via delayed_job
170
+ def salesforce_perform_async_call?
171
+ return false if salesforce_attributes_to_update.empty? || self.class.salesforce_async_attributes.empty?
172
+ salesforce_attributes_to_update.keys.all? {|key| self.class.salesforce_async_attributes.include?(key) } && salesforce_id.present?
173
+ end
174
+
175
+ # sync model data to Salesforce, adding any Salesforce validation errors to the models errors
176
+ def salesforce_sync
177
+ return if self.salesforce_skip_sync?
178
+
179
+ if salesforce_perform_async_call?
180
+ Delayed::Job.enqueue(SalesforceArSync::SalesforceObjectSync.new(self.class.salesforce_web_class_name, self.salesforce_object_name, salesforce_id, salesforce_attributes_to_update), :priority => 50)
181
+ else
182
+ salesforce_update_object(salesforce_attributes_to_update) if salesforce_attributes_to_update.present? && salesforce_object
183
+ salesforce_create_object(salesforce_attributes_to_update(!new_record?)) if salesforce_id.nil? && salesforce_object.nil?
184
+
185
+ self.salesforce_id = salesforce_object.send(self.class.salesforce_id_attribute_name) unless salesforce_object.nil?
186
+ end
187
+ rescue Exception => ex
188
+ self.errors[:base] << ex.message
189
+ return false
190
+ end
191
+
192
+ def sync_web_id
193
+ return false if !self.class.salesforce_sync_web_id? || self.salesforce_skip_sync?
194
+ salesforce_object.update_attribute(self.class.salesforce_web_id_attribute_name.to_s, id) if salesforce_id && salesforce_object
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,57 @@
1
+ module SalesforceArSync
2
+ module SoapHandler
3
+ class Base
4
+ attr_reader :xml_hashed, :options, :sobjects
5
+
6
+ def initialize organization, options = {}
7
+ @options = options
8
+ @xml_hashed = options
9
+ @organization = SALESFORCE_AR_SYNC_CONFIG["ORGANIZATION_ID"]
10
+ @sobjects = collect_sobjects if valid?
11
+ end
12
+
13
+ # queues each individual record from the message for update
14
+ def process_notifications(priority = 90)
15
+ batch_process do |sobject|
16
+ options[:klass].camelize.constantize.delay(:priority => priority, :run_at => 5.seconds.from_now).salesforce_update(sobject)
17
+ end
18
+ end
19
+
20
+ # ensures that the received message is properly formed, and that it comes from the expected Salesforce Org
21
+ def valid?
22
+ notifications = @xml_hashed.try(:[], "Envelope").try(:[], "Body").try(:[], "notifications")
23
+
24
+ organization_id = notifications.try(:[], "OrganizationId")
25
+ return !notifications.try(:[], "Notification").nil? && organization_id == @organization # we sent this to ourselves
26
+ end
27
+
28
+ def batch_process(&block)
29
+ return if sobjects.nil? || !block_given?
30
+ sobjects.each do | sobject |
31
+ yield sobject
32
+ end
33
+ end
34
+
35
+ #xml for SFDC response
36
+ #called from soap_message_controller
37
+ def generate_response(error = nil)
38
+ response = "<Ack>#{sobjects.nil? ? false : true}</Ack>" unless error
39
+ if error
40
+ response = "<soapenv:Fault><faultcode>soap:Receiver</faultcode><faultstring>#{error.message}</faultstring></soapenv:Fault>"
41
+ end
42
+ return "<?xml version=\"1.0\" encoding=\"UTF-8\"?><soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"><soapenv:Body><notificationsResponse>#{response}</notificationsResponse></soapenv:Body></soapenv:Envelope>"
43
+ end
44
+
45
+ private
46
+
47
+ def collect_sobjects
48
+ notification = @xml_hashed["Envelope"]["Body"]["notifications"]["Notification"]
49
+ if notification.is_a? Array
50
+ return notification.collect{ |h| h["sObject"].symbolize_keys}
51
+ else
52
+ return [notification["sObject"].try(:symbolize_keys)]
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,19 @@
1
+ module SalesforceArSync
2
+ module SoapHandler
3
+ class Delete < SalesforceArSync::SoapHandler::Base
4
+ def process_notifications(priority = 90)
5
+ batch_process do |sobject|
6
+ SalesforceArSync::SoapHandler::Delete.delay(:priority => priority, :run_at => 5.seconds.from_now).delete_object(sobject)
7
+ end
8
+ end
9
+
10
+ def self.delete_object(hash = {})
11
+ raise ArgumentError, "Object_Id__c parameter required" if hash[:Object_Id__c].blank?
12
+ raise ArgumentError, "Object_Type__c parameter required" if hash[:Object_Type__c].blank?
13
+
14
+ object = hash[:Object_Type__c].constantize.find_by_salesforce_id(hash[:Object_Id__c])
15
+ object.destroy if object
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,3 @@
1
+ module SalesforceArSync
2
+ VERSION = "1.0.0"
3
+ end
metadata ADDED
@@ -0,0 +1,212 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: salesforce_ar_sync
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Michael Halliday
9
+ - Nick Neufeld
10
+ - Andrew Coates
11
+ - Devon Noonan
12
+ - Liam Nediger
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+ date: 2012-09-17 00:00:00.000000000 Z
17
+ dependencies:
18
+ - !ruby/object:Gem::Dependency
19
+ name: rails
20
+ requirement: !ruby/object:Gem::Requirement
21
+ none: false
22
+ requirements:
23
+ - - ! '>='
24
+ - !ruby/object:Gem::Version
25
+ version: 3.1.0
26
+ type: :runtime
27
+ prerelease: false
28
+ version_requirements: !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: 3.1.0
34
+ - !ruby/object:Gem::Dependency
35
+ name: rake
36
+ requirement: !ruby/object:Gem::Requirement
37
+ none: false
38
+ requirements:
39
+ - - ! '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ type: :development
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ none: false
46
+ requirements:
47
+ - - ! '>='
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ - !ruby/object:Gem::Dependency
51
+ name: rspec
52
+ requirement: !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ! '>='
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ type: :development
59
+ prerelease: false
60
+ version_requirements: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ - !ruby/object:Gem::Dependency
67
+ name: webmock
68
+ requirement: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ! '>='
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ type: :development
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ! '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: vcr
84
+ requirement: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ none: false
94
+ requirements:
95
+ - - ! '>='
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: ammeter
100
+ requirement: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ~>
104
+ - !ruby/object:Gem::Version
105
+ version: 0.2.8
106
+ type: :development
107
+ prerelease: false
108
+ version_requirements: !ruby/object:Gem::Requirement
109
+ none: false
110
+ requirements:
111
+ - - ~>
112
+ - !ruby/object:Gem::Version
113
+ version: 0.2.8
114
+ - !ruby/object:Gem::Dependency
115
+ name: webmock
116
+ requirement: !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ! '>='
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ type: :development
123
+ prerelease: false
124
+ version_requirements: !ruby/object:Gem::Requirement
125
+ none: false
126
+ requirements:
127
+ - - ! '>='
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ - !ruby/object:Gem::Dependency
131
+ name: supermodel
132
+ requirement: !ruby/object:Gem::Requirement
133
+ none: false
134
+ requirements:
135
+ - - ! '>='
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ none: false
142
+ requirements:
143
+ - - ! '>='
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ - !ruby/object:Gem::Dependency
147
+ name: databasedotcom
148
+ requirement: !ruby/object:Gem::Requirement
149
+ none: false
150
+ requirements:
151
+ - - ! '>='
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ type: :runtime
155
+ prerelease: false
156
+ version_requirements: !ruby/object:Gem::Requirement
157
+ none: false
158
+ requirements:
159
+ - - ! '>='
160
+ - !ruby/object:Gem::Version
161
+ version: '0'
162
+ description: ActiveRecord extension & rails engine for syncing data with Salesforce.com
163
+ email:
164
+ - mhalliday@infotech.com
165
+ - nneufeld@infotech.com
166
+ - acoates@infotech.com
167
+ - dnoonan@infotech.com
168
+ - lnediger@infotech.com.com
169
+ executables: []
170
+ extensions: []
171
+ extra_rdoc_files: []
172
+ files:
173
+ - README.md
174
+ - LICENSE
175
+ - lib/generators/salesforce_ar_sync/configuration/configuration_generator.rb
176
+ - lib/generators/salesforce_ar_sync/configuration/templates/salesforce_ar_sync.yml
177
+ - lib/generators/salesforce_ar_sync/migrations/migrations_generator.rb
178
+ - lib/generators/salesforce_ar_sync/migrations/templates/migration.rb
179
+ - lib/salesforce_ar_sync/engine.rb
180
+ - lib/salesforce_ar_sync/extenders/salesforce_syncable.rb
181
+ - lib/salesforce_ar_sync/ip_constraint.rb
182
+ - lib/salesforce_ar_sync/salesforce_object_sync.rb
183
+ - lib/salesforce_ar_sync/salesforce_sync.rb
184
+ - lib/salesforce_ar_sync/soap_handler/base.rb
185
+ - lib/salesforce_ar_sync/soap_handler/delete.rb
186
+ - lib/salesforce_ar_sync/version.rb
187
+ - lib/salesforce_ar_sync.rb
188
+ homepage: http://github.com/InfoTech/
189
+ licenses: []
190
+ post_install_message:
191
+ rdoc_options: []
192
+ require_paths:
193
+ - lib
194
+ required_ruby_version: !ruby/object:Gem::Requirement
195
+ none: false
196
+ requirements:
197
+ - - ! '>='
198
+ - !ruby/object:Gem::Version
199
+ version: '0'
200
+ required_rubygems_version: !ruby/object:Gem::Requirement
201
+ none: false
202
+ requirements:
203
+ - - ! '>='
204
+ - !ruby/object:Gem::Version
205
+ version: '0'
206
+ requirements: []
207
+ rubyforge_project:
208
+ rubygems_version: 1.8.24
209
+ signing_key:
210
+ specification_version: 3
211
+ summary: ActiveRecord extension & rails engine for syncing data with Salesforce.com
212
+ test_files: []