draisine 0.7.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +4 -0
  5. data/Gemfile +4 -0
  6. data/Guardfile +70 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +134 -0
  9. data/Rakefile +6 -0
  10. data/app/controllers/draisine/soap_controller.rb +49 -0
  11. data/bin/console +7 -0
  12. data/bin/setup +6 -0
  13. data/config/routes.rb +4 -0
  14. data/draisine.gemspec +32 -0
  15. data/lib/draisine/active_record.rb +191 -0
  16. data/lib/draisine/auditor/result.rb +48 -0
  17. data/lib/draisine/auditor.rb +130 -0
  18. data/lib/draisine/concerns/array_setter.rb +23 -0
  19. data/lib/draisine/concerns/attributes_mapping.rb +46 -0
  20. data/lib/draisine/concerns/import.rb +36 -0
  21. data/lib/draisine/conflict_detector.rb +38 -0
  22. data/lib/draisine/conflict_resolver.rb +97 -0
  23. data/lib/draisine/engine.rb +6 -0
  24. data/lib/draisine/importer.rb +111 -0
  25. data/lib/draisine/ip_checker.rb +15 -0
  26. data/lib/draisine/jobs/inbound_delete_job.rb +8 -0
  27. data/lib/draisine/jobs/inbound_update_job.rb +8 -0
  28. data/lib/draisine/jobs/job_base.rb +39 -0
  29. data/lib/draisine/jobs/outbound_create_job.rb +7 -0
  30. data/lib/draisine/jobs/outbound_delete_job.rb +7 -0
  31. data/lib/draisine/jobs/outbound_update_job.rb +7 -0
  32. data/lib/draisine/jobs/soap_delete_job.rb +7 -0
  33. data/lib/draisine/jobs/soap_update_job.rb +7 -0
  34. data/lib/draisine/partitioner.rb +73 -0
  35. data/lib/draisine/poller.rb +101 -0
  36. data/lib/draisine/query_mechanisms/base.rb +15 -0
  37. data/lib/draisine/query_mechanisms/default.rb +13 -0
  38. data/lib/draisine/query_mechanisms/last_modified_date.rb +18 -0
  39. data/lib/draisine/query_mechanisms/system_modstamp.rb +18 -0
  40. data/lib/draisine/query_mechanisms.rb +18 -0
  41. data/lib/draisine/registry.rb +22 -0
  42. data/lib/draisine/setup.rb +97 -0
  43. data/lib/draisine/soap_handler.rb +79 -0
  44. data/lib/draisine/syncer.rb +52 -0
  45. data/lib/draisine/type_mapper.rb +105 -0
  46. data/lib/draisine/util/caching_client.rb +73 -0
  47. data/lib/draisine/util/hash_diff.rb +39 -0
  48. data/lib/draisine/util/parse_time.rb +14 -0
  49. data/lib/draisine/util/salesforce_comparisons.rb +53 -0
  50. data/lib/draisine/version.rb +3 -0
  51. data/lib/draisine.rb +48 -0
  52. data/lib/ext/databasedotcom.rb +98 -0
  53. data/lib/generators/draisine/delta_migration_generator.rb +77 -0
  54. data/lib/generators/draisine/integration_generator.rb +53 -0
  55. data/lib/generators/draisine/templates/delta_migration.rb +24 -0
  56. data/lib/generators/draisine/templates/migration.rb +21 -0
  57. data/lib/generators/draisine/templates/model.rb +11 -0
  58. data/salesforce/sample_delete_trigger.apex +7 -0
  59. data/salesforce/sample_test_class_for_delete_trigger.apex +15 -0
  60. metadata +242 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d532674344caaa230a569619645f1741bc250576
4
+ data.tar.gz: dfa98a38eaace1c4358310a3580c87020d8ee016
5
+ SHA512:
6
+ metadata.gz: 7c94c9310d3dea3f8d4a64f2c07bd162dc8409ad18fa715f575c80e271656d43e37e8335e2d146c27447ba6d6cfea50375313cf1503d76daa533508b2f37a1e9
7
+ data.tar.gz: df7c45a1aace9a9775ac5ea2bf0e3b1f8cecf260e72d3593c02778b602c647a08e65ed828930d02353ad4477d9172c6028e1ce8a0be90859e96bfb5aa497a8ca
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.1
4
+ before_install: gem install bundler -v 1.11.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in draisine.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,70 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ ## Uncomment and set this to only include directories you want to watch
5
+ # directories %w(app lib config test spec features) \
6
+ # .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
7
+
8
+ ## Note: if you are using the `directories` clause above and you are not
9
+ ## watching the project directory ('.'), then you will want to move
10
+ ## the Guardfile to a watched dir and symlink it back, e.g.
11
+ #
12
+ # $ mkdir config
13
+ # $ mv Guardfile config/
14
+ # $ ln -s config/Guardfile .
15
+ #
16
+ # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
17
+
18
+ # Note: The cmd option is now required due to the increasing number of ways
19
+ # rspec may be run, below are examples of the most common uses.
20
+ # * bundler: 'bundle exec rspec'
21
+ # * bundler binstubs: 'bin/rspec'
22
+ # * spring: 'bin/rspec' (This will use spring if running and you have
23
+ # installed the spring binstubs per the docs)
24
+ # * zeus: 'zeus rspec' (requires the server to be started separately)
25
+ # * 'just' rspec: 'rspec'
26
+
27
+ guard :rspec, cmd: "bundle exec rspec" do
28
+ require "guard/rspec/dsl"
29
+ dsl = Guard::RSpec::Dsl.new(self)
30
+
31
+ # Feel free to open issues for suggestions and improvements
32
+
33
+ # RSpec files
34
+ rspec = dsl.rspec
35
+ watch(rspec.spec_helper) { rspec.spec_dir }
36
+ watch(rspec.spec_support) { rspec.spec_dir }
37
+ watch(rspec.spec_files)
38
+
39
+ # Ruby files
40
+ ruby = dsl.ruby
41
+ dsl.watch_spec_files_for(ruby.lib_files)
42
+
43
+ # Rails files
44
+ rails = dsl.rails(view_extensions: %w(erb haml slim))
45
+ dsl.watch_spec_files_for(rails.app_files)
46
+ dsl.watch_spec_files_for(rails.views)
47
+
48
+ watch(rails.controllers) do |m|
49
+ [
50
+ rspec.spec.("routing/#{m[1]}_routing"),
51
+ rspec.spec.("controllers/#{m[1]}_controller"),
52
+ rspec.spec.("acceptance/#{m[1]}")
53
+ ]
54
+ end
55
+
56
+ # Rails config changes
57
+ watch(rails.spec_helper) { rspec.spec_dir }
58
+ watch(rails.routes) { "#{rspec.spec_dir}/routing" }
59
+ watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
60
+
61
+ # Capybara features specs
62
+ watch(rails.view_dirs) { |m| rspec.spec.("features/#{m[1]}") }
63
+ watch(rails.layouts) { |m| rspec.spec.("features/#{m[1]}") }
64
+
65
+ # Turnip features and steps
66
+ watch(%r{^spec/acceptance/(.+)\.feature$})
67
+ watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
68
+ Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance"
69
+ end
70
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Mark Abramov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # Draisine
2
+
3
+ ![Cho-choo](https://upload.wikimedia.org/wikipedia/commons/thumb/3/3d/GAZ-13_Chaika_draisine.jpg/600px-GAZ-13_Chaika_draisine.jpg)
4
+
5
+ A bi-directional syncing solution for Salesforce and ActiveRecord.
6
+
7
+ Gem overall design is heavily inspired by InfoTech's [salesforce_ar_sync gem](https://github.com/InfoTech/salesforce_ar_sync), but with focus on clearer and more modular code.
8
+
9
+ ## Dependencies
10
+
11
+ * Rails 4.2+ (for ActiveJob)
12
+ * databasedotcom (for salesforce connections)
13
+
14
+
15
+ ## Installation and configuration
16
+
17
+ After you've got the gem installed, you will need to setup the salesforce client. For example:
18
+
19
+ ```
20
+ sf_client = Databasedotcom::Client.new("config/databasedotcom.yml")
21
+ sf_client.authenticate :username => <username>, :password => <password>
22
+ Draisine.salesforce_client = sf_client
23
+ ```
24
+
25
+ You will also need to have your organization id set up:
26
+
27
+ ```
28
+ Draisine.organization_id = '123456789012345678'
29
+ ```
30
+
31
+ Use this [tool](https://cloudjedi.wordpress.com/no-fuss-salesforce-id-converter/) to convert your 15-char org id into 18-char.
32
+
33
+ ## Usage
34
+
35
+ Draisine adds a `salesforce_syncable` macro to ActiveRecord models, used like this:
36
+
37
+ ```
38
+ class Lead < Salesforce::Model
39
+ salesforce_syncable synced_attributes: [:FirstName, :LastName, ...],
40
+ mapping: { 'FirstName' => 'first_name', 'LastName' => 'last_name' },
41
+ operations: [:outbound_create, :outbound_update, :outbound_delete, :inbound_update, :inbound_delete],
42
+ salesforce_object_name: 'Lead',
43
+ sync: true
44
+ end
45
+ ```
46
+
47
+ Your model class must have `salesforce_id` string column for everything to work.
48
+
49
+ ### Available options
50
+
51
+ #### salesforce_object_name (String)
52
+
53
+ Self-explanatory. Defaults to the class name.
54
+
55
+ #### synced_attributes (Array[Symbol|String], required)
56
+
57
+ List of all Salesforce attributes that are required to be synced. If your ActiveRecord attributes should have different names, you can remap them later.
58
+
59
+ #### mapping (Hash[String => String])
60
+
61
+ #### sync (Boolean, default: true)
62
+
63
+ When set to true, all jobs are launched inline (via `#perform_now`), otherwise, they are set to perform as soon as workers get to them (`#perform_later`).
64
+
65
+ #### operations (Array[Symbol])
66
+
67
+ List of operations that must be synced with Salesforce.
68
+
69
+ Available operations: `[:outbound_create, :outbound_update, :outbound_delete, :inbound_update, :inbound_delete]`
70
+
71
+ #### non_audited_attributes (Array[Symbol|String])
72
+
73
+ ## Setting up outbound messages
74
+
75
+ In the left sidebar of salesforce interface, choose Create -> Workflow & Approvals -> Workflow Rules. Then click "New Rule". The rest is more or less self-explanatory. You would want to have all the necessary fields attached to your outbound message.
76
+
77
+ Assuming you mount draisine engine to `/salesforce`, endpoint url would be `/salesforce/sf_soap/lead` (for Lead object). Make sure you use full proper URL since salesforce will not follow redirects.
78
+
79
+ You can check out status for the latest sent messages in the Monitoring -> Outbound Messages section.
80
+
81
+ ## Handling special object types, e.g. `LeadHistory`
82
+
83
+ Some object types in salesforce are not directly user-editable and can't be set up to send outbound messages. One example of such object is LeadHistory. That means you'll have to poll them yourselves. Easiest way to do so is sort by `Id` both on your model (also known as `salesforce_id`) and at salesforce and get only the records with Id > max(salesforce_id).
84
+
85
+ ## Handling inbound deletes
86
+
87
+ Salesforce only sends outbound messages for record creates and updates, to sync deletes you'll have to go extra mile. You'll need to create a custom object, called `Deleted_Object` that has two fields: `Object_Id (text (18))` and `Object_Type (text (128))` and a trigger for every observed model that creates an instance of such object after every delete. Then you'll need to setup outbound messaging, like for a normal model, but use `/sf_soap/delete` instead of `/sf_soap/<modelname>` for endpoint.
88
+
89
+ See a [trigger example](salesforce/sample_delete_trigger.apex) and a [corresponding test class](salesforce/sample_test_class_for_delete_trigger.apex).
90
+
91
+
92
+ ### How to create trigger on your production instance
93
+
94
+ You might notice that unlike your sandbox instance, your production instance doesn't have "new trigger" button. Congratulations and welcome to Salesforce! You can't create new triggers on production instances directly, you'll have to use something called inbound/outbound change sets. In a nutshell, it's a protocol for generic object exchange between salesforce instances. Long story short, you'll need to export your apex trigger with its test class from sandbox to production instance.
95
+
96
+ To do so, go Deploy -> Outbound Change Set -> New -> etc etc etc.
97
+
98
+ If you export Deleted Object this way, don't forget to add custom fields to your change set. Also you must add test coverage for your trigger to the changeset or it won't apply. Good luck.
99
+
100
+
101
+ ## Error handling
102
+
103
+ You can setup handling most transient errors using `Draisine.job_error_handler = proc {|exception, job, arguments| }` setter. It will be called every time a job, such as `InboundUpdateJob` fails with any error.
104
+
105
+ ## Roadmap
106
+
107
+
108
+ * ~~ActiveRecord plugin and hooks~~
109
+ * ~~ActiveRecord -> Salesforce synchronization (outbound creates, updates, deletes)~~
110
+ * ~~ActiveJob delayed jobs~~
111
+ * ~~Salesforce -> ActiveRecord inbound updates~~
112
+ * ~~Salesforce -> ActiveRecord inbound deletes~~
113
+ * ~~Error handling inside delayed jobs~~
114
+ * ~~Auditing~~
115
+ * ~~Migration generator~~
116
+ * ~~Conflict resolution~~
117
+ * Use restforce instead of / alongside databasedotcom
118
+
119
+
120
+ ## Development
121
+
122
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
123
+
124
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
125
+
126
+ ## Contributing
127
+
128
+ Bug reports and pull requests are welcome on GitHub at https://github.com/chloeandisabel/draisine.
129
+
130
+
131
+ ## License
132
+
133
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
134
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,49 @@
1
+ module Draisine
2
+ class SoapController < ApplicationController
3
+ protect_from_forgery with: :null_session
4
+
5
+ before_filter :validate_ip
6
+
7
+ def update
8
+ message = request.body.read
9
+ if Draisine.sync_soap_operations?
10
+ Draisine::SoapUpdateJob.perform_now(message)
11
+ else
12
+ Draisine::SoapUpdateJob.perform_later(message)
13
+ end
14
+
15
+ render xml: xml_response, status: :created
16
+ end
17
+
18
+ def delete
19
+ message = request.body.read
20
+ if Draisine.sync_soap_operations?
21
+ Draisine::SoapDeleteJob.perform_now(message)
22
+ else
23
+ Draisine::SoapDeleteJob.perform_later(message)
24
+ end
25
+
26
+ render xml: xml_response, status: :created
27
+ end
28
+
29
+ protected
30
+
31
+ def validate_ip
32
+ ip_checker = IpChecker.new(Draisine.allowed_ip_ranges)
33
+ unless ip_checker.check(request.remote_ip)
34
+ render nothing: true, status: :forbidden
35
+ end
36
+ end
37
+
38
+ def xml_response
39
+ <<-EOF
40
+ <?xml version="1.0" encoding="UTF-8"?>
41
+ <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">
42
+ <soapenv:Body>
43
+ <notificationsResponse><Ack>true</Ack></notificationsResponse>
44
+ </soapenv:Body>
45
+ </soapenv:Envelope>
46
+ EOF
47
+ end
48
+ end
49
+ end
data/bin/console ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "draisine"
5
+ require "pry"
6
+
7
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
data/config/routes.rb ADDED
@@ -0,0 +1,4 @@
1
+ Draisine::Engine.routes.draw do
2
+ post '/sf_soap/delete' => 'draisine/soap#delete'
3
+ post '/sf_soap/*klass' => 'draisine/soap#update'
4
+ end
data/draisine.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'draisine/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "draisine"
8
+ spec.version = Draisine::VERSION
9
+ spec.authors = ["Mark Abramov"]
10
+ spec.email = ["mark.abramov@chloeandisabel.com"]
11
+
12
+ spec.summary = %q{Synchronization machinery for salesforce}
13
+ spec.description = %q{Bidirectional synchronization for salesforce / activerecord}
14
+ spec.homepage = "https://github.com/chloeandisabel/draisine"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.11"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "rspec", "~> 3.0"
25
+ spec.add_development_dependency "rspec-collection_matchers"
26
+ spec.add_development_dependency "guard"
27
+ spec.add_development_dependency "guard-rspec"
28
+ spec.add_development_dependency "pry"
29
+ spec.add_development_dependency "sqlite3"
30
+ spec.add_runtime_dependency "rails", ">= 4.2"
31
+ spec.add_runtime_dependency "databasedotcom", "~> 1.3"
32
+ end
@@ -0,0 +1,191 @@
1
+ module Draisine
2
+ ALL_OPS = [:outbound_create, :outbound_update, :outbound_delete,
3
+ :inbound_update, :inbound_delete]
4
+
5
+ module ActiveRecordMacro
6
+ def salesforce_syncable(options)
7
+ include Draisine::ActiveRecordPlugin
8
+
9
+ self.salesforce_object_name = options.fetch(:salesforce_object_name, name)
10
+ salesforce_synced_attributes = options.fetch(:synced_attributes, []).map(&:to_s)
11
+ default_mapping = salesforce_synced_attributes.map {|k| [k, k] }.to_h
12
+ mapping = options.fetch(:mapping, {}).map {|k,v| [k.to_s, v.to_s] }.to_h
13
+ self.salesforce_mapping = default_mapping.merge(mapping)
14
+ self.salesforce_ops = Set.new(options.fetch(:operations, ALL_OPS))
15
+ self.salesforce_sync_mode = options.fetch(:sync, true)
16
+ non_audited_attrs = options.fetch(:non_audited_attributes, []).map(&:to_s)
17
+ self.salesforce_audited_attributes = salesforce_synced_attributes - non_audited_attrs
18
+
19
+ options.fetch(:array_attributes, []).each do |attr|
20
+ salesforce_array_setter(attr)
21
+ end
22
+
23
+ Draisine.registry.register(self, salesforce_object_name)
24
+ end
25
+ end
26
+
27
+ module ActiveRecordPlugin
28
+ extend ActiveSupport::Concern
29
+ include Draisine::Concerns::ArraySetter
30
+ include Draisine::Concerns::AttributesMapping
31
+ include Draisine::Concerns::Import
32
+
33
+ module ClassMethods
34
+ attr_accessor :salesforce_object_name
35
+ attr_accessor :salesforce_audited_attributes
36
+ attr_accessor :salesforce_ops
37
+ attr_accessor :salesforce_sync_mode
38
+
39
+ ALL_OPS.each do |op|
40
+ define_method("salesforce_#{op}?") do
41
+ salesforce_ops.include?(op)
42
+ end
43
+ end
44
+
45
+ def salesforce_enqueue_or_run(job_class, *args, &block)
46
+ if salesforce_sync_mode
47
+ job_class.perform_now(*args, &block)
48
+ else
49
+ job_class.perform_later(*args, &block)
50
+ end
51
+ end
52
+
53
+ def salesforce_on_inbound_update(attributes)
54
+ salesforce_enqueue_or_run(InboundUpdateJob, self.name, attributes)
55
+ end
56
+
57
+ def salesforce_inbound_update(attributes, add_blanks = true)
58
+ if salesforce_inbound_update?
59
+ attributes = attributes.with_indifferent_access
60
+ id = attributes.fetch('Id')
61
+ (find_by(salesforce_id: id) || new).tap do |m|
62
+ m.salesforce_id = id
63
+ m.salesforce_inbound_update(attributes, add_blanks)
64
+ end
65
+ end
66
+ end
67
+
68
+ def salesforce_on_inbound_delete(salesforce_id)
69
+ salesforce_enqueue_or_run(InboundDeleteJob, self.name, salesforce_id)
70
+ end
71
+
72
+ def salesforce_inbound_delete(salesforce_id)
73
+ if salesforce_inbound_delete?
74
+ record = find_by(salesforce_id: salesforce_id)
75
+ if record
76
+ record.salesforce_skip_sync = true
77
+ record.destroy
78
+ salesforce_callback(:inbound_delete, salesforce_id)
79
+ end
80
+ end
81
+ end
82
+
83
+ def salesforce_syncer
84
+ @salesforce_syncer || Syncer.new(salesforce_object_name)
85
+ end
86
+
87
+ def salesforce_callback(type, salesforce_id, options = {})
88
+ Draisine.sync_callback.call(type, salesforce_id, options)
89
+ end
90
+ end
91
+
92
+ attr_accessor :salesforce_skip_sync
93
+
94
+ included do
95
+ after_create :salesforce_on_create
96
+ after_update :salesforce_on_update
97
+ after_destroy :salesforce_on_delete
98
+ end
99
+
100
+ def salesforce_inbound_update(attributes, add_blanks = true)
101
+ return unless should_process_inbound_update?(attributes)
102
+ self.salesforce_skip_sync = true
103
+ if add_blanks
104
+ attributes = self.class.salesforce_synced_attributes
105
+ .map {|attr| [attr, attributes[attr]] }
106
+ .to_h
107
+ end
108
+ salesforce_assign_attributes(attributes)
109
+ salesforce_callback(persisted? ? :inbound_update : :inbound_create, attributes: attributes)
110
+ save!
111
+ end
112
+
113
+ def salesforce_on_create
114
+ if !salesforce_skip_sync && self.class.salesforce_outbound_create?
115
+ self.class.salesforce_enqueue_or_run(OutboundCreateJob, self)
116
+ end
117
+ end
118
+
119
+ def salesforce_outbound_create
120
+ attrs = salesforce_attributes.compact
121
+ response = salesforce_syncer.create(attrs)
122
+ self.salesforce_id = response.fetch('id')
123
+ salesforce_skipping_sync { save! } if persisted?
124
+ salesforce_callback(:outbound_create, attributes: attrs)
125
+ end
126
+
127
+ def salesforce_on_update
128
+ if !salesforce_skip_sync && self.class.salesforce_outbound_update?
129
+ self.class.salesforce_enqueue_or_run(
130
+ OutboundUpdateJob,
131
+ self,
132
+ salesforce_attributes.slice(*changed)
133
+ )
134
+ end
135
+ end
136
+
137
+ def salesforce_outbound_update(updated_attributes)
138
+ self.class.transaction do
139
+ salesforce_syncer.update(salesforce_id, updated_attributes)
140
+ timestamp = salesforce_syncer.get_system_modstamp(salesforce_id)
141
+ update_column(:salesforce_updated_at, timestamp)
142
+ salesforce_callback(:outbound_update, attributes: updated_attributes)
143
+ end
144
+ end
145
+
146
+ def salesforce_on_delete
147
+ if !salesforce_skip_sync && self.class.salesforce_outbound_delete?
148
+ self.class.salesforce_enqueue_or_run(OutboundDeleteJob, self)
149
+ end
150
+ end
151
+
152
+ def salesforce_outbound_delete
153
+ salesforce_syncer.delete(salesforce_id)
154
+ salesforce_callback(:outbound_delete)
155
+ end
156
+
157
+ def salesforce_skipping_sync(&block)
158
+ old_sync = self.salesforce_skip_sync
159
+ self.salesforce_skip_sync = true
160
+ instance_eval(&block)
161
+ ensure
162
+ self.salesforce_skip_sync = old_sync
163
+ end
164
+
165
+ def salesforce_callback(type, options = {})
166
+ self.class.salesforce_callback(type, salesforce_id, {
167
+ local_record_type: self.class.name,
168
+ local_record_id: id
169
+ }.merge(options))
170
+ end
171
+
172
+ protected
173
+
174
+ def should_process_inbound_update?(attributes)
175
+ !salesforce_updated_at || !attributes['SystemModstamp'] ||
176
+ Draisine.parse_time(attributes['SystemModstamp']) > salesforce_updated_at
177
+ end
178
+
179
+ def salesforce_syncer
180
+ self.class.salesforce_syncer
181
+ end
182
+ end
183
+
184
+ def self.register_ar_macro
185
+ ActiveRecord::Base.extend(Draisine::ActiveRecordMacro)
186
+ end
187
+ end
188
+
189
+ if defined?(ActiveRecord::Base)
190
+ Draisine.register_ar_macro
191
+ end
@@ -0,0 +1,48 @@
1
+ module Draisine
2
+ class Auditor
3
+ Discrepancy = Struct.new(:type, :salesforce_type, :salesforce_id, :local_type, :local_id, :local_attributes, :remote_attributes, :diff_keys)
4
+
5
+ class Result
6
+ attr_reader :discrepancies, :status, :error
7
+ def initialize
8
+ @discrepancies = []
9
+ @status = :running
10
+ @error = nil
11
+ end
12
+
13
+ def calculate_result!
14
+ if discrepancies.any?
15
+ @status = :failure
16
+ else
17
+ @status = :success
18
+ end
19
+ self
20
+ end
21
+
22
+ def error!(ex)
23
+ @error = ex
24
+ @status = :failure
25
+ self
26
+ end
27
+
28
+ def success?
29
+ @status == :success
30
+ end
31
+
32
+ def failure?
33
+ @status == :failure
34
+ end
35
+
36
+ def running?
37
+ @status == :running
38
+ end
39
+
40
+ def discrepancy(type:, salesforce_type:, salesforce_id:,
41
+ local_type: nil, local_id: nil, local_attributes: nil, remote_attributes: nil, diff_keys: nil)
42
+
43
+ discrepancies << Discrepancy.new(type, salesforce_type, salesforce_id,
44
+ local_type, local_id, local_attributes, remote_attributes, diff_keys)
45
+ end
46
+ end
47
+ end
48
+ end