restforce-db 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rubocop/custom/method_documentation.rb +65 -0
- data/.rubocop.yml +39 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +68 -0
- data/Rakefile +13 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/generators/restforce_generator.rb +19 -0
- data/lib/generators/templates/config.yml +8 -0
- data/lib/generators/templates/script +6 -0
- data/lib/restforce/db/command.rb +98 -0
- data/lib/restforce/db/configuration.rb +50 -0
- data/lib/restforce/db/instances/active_record.rb +48 -0
- data/lib/restforce/db/instances/base.rb +66 -0
- data/lib/restforce/db/instances/salesforce.rb +46 -0
- data/lib/restforce/db/mapping.rb +106 -0
- data/lib/restforce/db/model.rb +50 -0
- data/lib/restforce/db/record_type.rb +77 -0
- data/lib/restforce/db/record_types/active_record.rb +80 -0
- data/lib/restforce/db/record_types/base.rb +44 -0
- data/lib/restforce/db/record_types/salesforce.rb +94 -0
- data/lib/restforce/db/synchronizer.rb +57 -0
- data/lib/restforce/db/version.rb +10 -0
- data/lib/restforce/db/worker.rb +156 -0
- data/lib/restforce/db.rb +77 -0
- data/lib/restforce/extensions.rb +19 -0
- data/restforce-db.gemspec +41 -0
- data/test/cassettes/Restforce_DB/accessing_Salesforce/uses_the_configured_credentials.yml +43 -0
- data/test/cassettes/Restforce_DB_Instances_Salesforce/_copy_/updates_the_record_with_the_attributes_from_the_copied_object.yml +193 -0
- data/test/cassettes/Restforce_DB_Instances_Salesforce/_update_/updates_the_local_record_with_the_passed_attributes.yml +193 -0
- data/test/cassettes/Restforce_DB_Instances_Salesforce/_update_/updates_the_record_in_Salesforce_with_the_passed_attributes.yml +232 -0
- data/test/cassettes/Restforce_DB_RecordTypes_Salesforce/_create_/creates_a_record_in_Salesforce_from_the_passed_database_record_s_attributes.yml +158 -0
- data/test/cassettes/Restforce_DB_RecordTypes_Salesforce/_create_/updates_the_database_record_with_the_Salesforce_record_s_ID.yml +158 -0
- data/test/cassettes/Restforce_DB_RecordTypes_Salesforce/_find/finds_existing_records_in_Salesforce.yml +157 -0
- data/test/cassettes/Restforce_DB_RecordTypes_Salesforce/_find/returns_nil_when_no_matching_record_exists.yml +81 -0
- data/test/cassettes/Restforce_DB_Synchronizer/_run/given_a_Salesforce_record_with_an_existing_record_in_the_database/updates_the_database_record.yml +158 -0
- data/test/cassettes/Restforce_DB_Synchronizer/_run/given_an_existing_Salesforce_record/populates_the_database_with_the_new_record.yml +158 -0
- data/test/cassettes/Restforce_DB_Synchronizer/_run/given_an_existing_database_record/populates_Salesforce_with_the_new_record.yml +235 -0
- data/test/lib/restforce/db/configuration_test.rb +38 -0
- data/test/lib/restforce/db/instances/active_record_test.rb +39 -0
- data/test/lib/restforce/db/instances/salesforce_test.rb +51 -0
- data/test/lib/restforce/db/mapping_test.rb +70 -0
- data/test/lib/restforce/db/model_test.rb +48 -0
- data/test/lib/restforce/db/record_type_test.rb +26 -0
- data/test/lib/restforce/db/record_types/active_record_test.rb +85 -0
- data/test/lib/restforce/db/record_types/salesforce_test.rb +46 -0
- data/test/lib/restforce/db/synchronizer_test.rb +84 -0
- data/test/lib/restforce/db_test.rb +24 -0
- data/test/support/active_record.rb +20 -0
- data/test/support/database_cleaner.rb +3 -0
- data/test/support/salesforce.rb +48 -0
- data/test/support/vcr.rb +23 -0
- data/test/test_helper.rb +25 -0
- metadata +287 -0
@@ -0,0 +1,106 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
module DB
|
4
|
+
|
5
|
+
# Restforce::DB::Mapping captures a set of mappings between database columns
|
6
|
+
# and Salesforce fields, providing utilities to transform hashes of
|
7
|
+
# attributes from one to the other.
|
8
|
+
class Mapping
|
9
|
+
|
10
|
+
attr_reader :mappings
|
11
|
+
|
12
|
+
# Public: Initialize a new Restforce::DB::Mapping.
|
13
|
+
#
|
14
|
+
# mappings - A Hash, with keys corresponding to the names of Ruby object
|
15
|
+
# attributes, and the values of those keys corresponding to the
|
16
|
+
# names of the related Salesforce fields.
|
17
|
+
def initialize(mappings = {})
|
18
|
+
@mappings = mappings
|
19
|
+
end
|
20
|
+
|
21
|
+
# Public: Append a new set of attribute mappings to the current set.
|
22
|
+
#
|
23
|
+
# mappings - A Hash, with keys corresponding to the names of Ruby object
|
24
|
+
# attributes, and the values of those keys corresponding to the
|
25
|
+
# names of the related Salesforce fields.
|
26
|
+
#
|
27
|
+
# Returns nothing.
|
28
|
+
def add_mappings(mappings = {})
|
29
|
+
@mappings.merge!(mappings)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Public: Get a list of the relevant Salesforce field names for this
|
33
|
+
# mapping.
|
34
|
+
#
|
35
|
+
# Returns an Array.
|
36
|
+
def salesforce_fields
|
37
|
+
@mappings.values
|
38
|
+
end
|
39
|
+
|
40
|
+
# Public: Get a list of the relevant database column names for this
|
41
|
+
# mapping.
|
42
|
+
#
|
43
|
+
# Returns an Array.
|
44
|
+
def database_fields
|
45
|
+
@mappings.keys
|
46
|
+
end
|
47
|
+
|
48
|
+
# Public: Build a normalized Hash of attributes from the appropriate set
|
49
|
+
# of mappings. The keys of the resulting mapping Hash will correspond to
|
50
|
+
# the database column names.
|
51
|
+
#
|
52
|
+
# in_format - A Symbol reflecting the expected attribute list. Accepted
|
53
|
+
# values are :database and :salesforce.
|
54
|
+
#
|
55
|
+
# Yields the attribute name.
|
56
|
+
# Returns a Hash.
|
57
|
+
def attributes(from_format)
|
58
|
+
use_mappings =
|
59
|
+
case from_format
|
60
|
+
when :salesforce
|
61
|
+
@mappings
|
62
|
+
when :database
|
63
|
+
# Generate a mapping of database column names to record attributes.
|
64
|
+
database_fields.zip(database_fields)
|
65
|
+
end
|
66
|
+
|
67
|
+
use_mappings.each_with_object({}) do |(attribute, mapping), values|
|
68
|
+
values[attribute] = yield(mapping)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Public: Convert a Hash of attributes to a format compatible with a
|
73
|
+
# specific platform.
|
74
|
+
#
|
75
|
+
# to_format - A Symbol reflecting the expected format. Accepted values
|
76
|
+
# are :database and :salesforce.
|
77
|
+
# attributes - A Hash, with keys corresponding to the attribute names in
|
78
|
+
# the format to convert away from.
|
79
|
+
#
|
80
|
+
# Examples
|
81
|
+
#
|
82
|
+
# mapping = Mapping.new(some_key: "Some_Field__c")
|
83
|
+
# mapping.convert(:salesforce, some_key: "some value")
|
84
|
+
# # => { "Some_Field__c" => "some value" }
|
85
|
+
#
|
86
|
+
# mapping.convert(:database, some_key: "some other value")
|
87
|
+
# # => { some_key: "some other value" }
|
88
|
+
#
|
89
|
+
# Returns a Hash.
|
90
|
+
def convert(to_format, attributes)
|
91
|
+
case to_format
|
92
|
+
when :database
|
93
|
+
attributes.dup
|
94
|
+
when :salesforce
|
95
|
+
@mappings.each_with_object({}) do |(attribute, mapping), converted|
|
96
|
+
next unless attributes.key?(attribute)
|
97
|
+
converted[mapping] = attributes[attribute]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
module DB
|
4
|
+
|
5
|
+
# Restforce::DB::Model is a helper module which attaches some special
|
6
|
+
# DSL-style methods to an ActiveRecord class, allowing for easier mapping
|
7
|
+
# of the ActiveRecord class to an object type in Salesforce.
|
8
|
+
module Model
|
9
|
+
|
10
|
+
# :nodoc:
|
11
|
+
def self.included(base)
|
12
|
+
base.extend(ClassMethods)
|
13
|
+
end
|
14
|
+
|
15
|
+
# :nodoc:
|
16
|
+
module ClassMethods
|
17
|
+
|
18
|
+
# Public: Initializes a Restforce::DB::RecordType defining this model's
|
19
|
+
# relationship to a Salesforce object type.
|
20
|
+
#
|
21
|
+
# salesforce_model - A String name of an object type in Salesforce.
|
22
|
+
# mappings - A Hash of mappings between database columns and
|
23
|
+
# fields in Salesforce.
|
24
|
+
#
|
25
|
+
# Returns a Restforce::DB::RecordType.
|
26
|
+
def map_to(salesforce_model, **mappings)
|
27
|
+
RecordType.new(
|
28
|
+
self,
|
29
|
+
salesforce_model,
|
30
|
+
mappings,
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Public: Append the passed mappings to this model.
|
35
|
+
#
|
36
|
+
# mappings - A Hash of database column names mapped to Salesforce
|
37
|
+
# fields.
|
38
|
+
#
|
39
|
+
# Returns nothing.
|
40
|
+
def add_mappings(mappings)
|
41
|
+
RecordType[self].add_mappings(mappings)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
module DB
|
4
|
+
|
5
|
+
# Restforce::DB::RecordType is an abstraction for a two-way binding between
|
6
|
+
# an ActiveRecord class and a Salesforce object type. It provides an
|
7
|
+
# interface for mapping database columns to Salesforce fields.
|
8
|
+
class RecordType
|
9
|
+
|
10
|
+
class << self
|
11
|
+
|
12
|
+
include Enumerable
|
13
|
+
attr_accessor :collection
|
14
|
+
|
15
|
+
# Public: Get the Restforce::DB::RecordType entry for the specified
|
16
|
+
# database model.
|
17
|
+
#
|
18
|
+
# database_model - A Class compatible with ActiveRecord::Base.
|
19
|
+
#
|
20
|
+
# Returns a Restforce::DB::RecordType.
|
21
|
+
def [](database_model)
|
22
|
+
collection[database_model]
|
23
|
+
end
|
24
|
+
|
25
|
+
# Public: Iterate through all registered Restforce::DB::RecordTypes.
|
26
|
+
#
|
27
|
+
# Yields one RecordType for each database-to-Salesforce mapping.
|
28
|
+
# Returns nothing.
|
29
|
+
def each
|
30
|
+
collection.each do |database_model, record_type|
|
31
|
+
yield database_model.name, record_type
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
self.collection ||= {}
|
38
|
+
attr_reader :mapping, :synchronizer
|
39
|
+
|
40
|
+
# Public: Initialize and register a Restforce::DB::RecordType.
|
41
|
+
#
|
42
|
+
# database_model - A Class compatible with ActiveRecord::Base.
|
43
|
+
# salesforce_model - A String name of an object type in Salesforce.
|
44
|
+
# mappings - A Hash of mappings between database columns and
|
45
|
+
# fields in Salesforce.
|
46
|
+
def initialize(database_model, salesforce_model, **mappings)
|
47
|
+
@mapping = Mapping.new(mappings)
|
48
|
+
@database_record_type = RecordTypes::ActiveRecord.new(database_model, @mapping)
|
49
|
+
@salesforce_record_type = RecordTypes::Salesforce.new(salesforce_model, @mapping)
|
50
|
+
@synchronizer = Synchronizer.new(@database_record_type, @salesforce_record_type)
|
51
|
+
|
52
|
+
self.class.collection[database_model] = self
|
53
|
+
end
|
54
|
+
|
55
|
+
# Public: Append the passed mappings to this model.
|
56
|
+
#
|
57
|
+
# mappings - A Hash of database column names mapped to Salesforce fields.
|
58
|
+
#
|
59
|
+
# Returns nothing.
|
60
|
+
def add_mappings(mappings)
|
61
|
+
@mapping.add_mappings mappings
|
62
|
+
end
|
63
|
+
|
64
|
+
# Public: Synchronize the records between the database and Salesforce.
|
65
|
+
#
|
66
|
+
# options - A Hash of options to pass to the synchronizer.
|
67
|
+
#
|
68
|
+
# Returns nothing.
|
69
|
+
def synchronize(options = {})
|
70
|
+
@synchronizer.run(options)
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
module DB
|
4
|
+
|
5
|
+
module RecordTypes
|
6
|
+
|
7
|
+
# Restforce::DB::RecordTypes::ActiveRecord serves as a wrapper for a
|
8
|
+
# single ActiveRecord::Base-compatible class, allowing for standard record
|
9
|
+
# lookups and attribute mappings.
|
10
|
+
class ActiveRecord < Base
|
11
|
+
|
12
|
+
# Public: Create an instance of this ActiveRecord model for the passed
|
13
|
+
# Salesforce instance.
|
14
|
+
#
|
15
|
+
# from_record - A Restforce::DB::Instances::Salesforce instance.
|
16
|
+
#
|
17
|
+
# Returns a Restforce::DB::Instances::ActiveRecord instance.
|
18
|
+
# Raises on any validation or database error.
|
19
|
+
def create!(from_record)
|
20
|
+
attributes = @mapping.convert(:database, from_record.attributes)
|
21
|
+
record = @record_type.create!(
|
22
|
+
attributes.merge(salesforce_id: from_record.id),
|
23
|
+
)
|
24
|
+
|
25
|
+
Instances::ActiveRecord.new(record, @mapping)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Public: Find the instance of this ActiveRecord model corresponding to
|
29
|
+
# the passed salesforce_id.
|
30
|
+
#
|
31
|
+
# salesforce_id - The id of the record in Salesforce.
|
32
|
+
#
|
33
|
+
# Returns nil or a Restforce::DB::Instances::ActiveRecord instance.
|
34
|
+
def find(id)
|
35
|
+
record = @record_type.find_by(salesforce_id: id)
|
36
|
+
return nil unless record
|
37
|
+
|
38
|
+
Instances::ActiveRecord.new(record, @mapping)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Public: Iterate through all ActiveRecord records of this type.
|
42
|
+
#
|
43
|
+
# options - A Hash of options which should be applied to the set of
|
44
|
+
# fetched records. Allowed options are:
|
45
|
+
# :before - A Time object defining the most recent update
|
46
|
+
# timestamp for which records should be returned.
|
47
|
+
# :after - A Time object defining the least recent update
|
48
|
+
# timestamp for which records should be returned.
|
49
|
+
#
|
50
|
+
# Yields a series of Restforce::DB::Instances::ActiveRecord instances.
|
51
|
+
# Returns nothing.
|
52
|
+
def each(options = {})
|
53
|
+
scope = @record_type
|
54
|
+
scope = scope.where("updated_at > ?", options[:after]) if options[:after]
|
55
|
+
scope = scope.where("updated_at < ?", options[:before]) if options[:before]
|
56
|
+
|
57
|
+
scope.find_each do |record|
|
58
|
+
yield Instances::ActiveRecord.new(record, @mapping)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# Internal: Has this Salesforce record already been linked to a database
|
65
|
+
# record?
|
66
|
+
#
|
67
|
+
# record - A Restforce::DB::Instances::Salesforce instance.
|
68
|
+
#
|
69
|
+
# Returns a Boolean.
|
70
|
+
def synced?(record)
|
71
|
+
@record_type.exists?(salesforce_id: record.id)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
module DB
|
4
|
+
|
5
|
+
module RecordTypes
|
6
|
+
|
7
|
+
# Restforce::DB::RecordTypes::Base defines common behavior for the other
|
8
|
+
# models defined in the Restforce::DB::RecordTypes namespace.
|
9
|
+
class Base
|
10
|
+
|
11
|
+
# Public: Initialize a new Restforce::DB::RecordTypes::Base.
|
12
|
+
#
|
13
|
+
# record_type - The name or class of the system record type.
|
14
|
+
# mapping - An instance of Restforce::DB::Mapping.
|
15
|
+
def initialize(record_type, mapping = Mapping.new)
|
16
|
+
@record_type = record_type
|
17
|
+
@mapping = mapping
|
18
|
+
end
|
19
|
+
|
20
|
+
# Public: Synchronize the passed record to the record type defined by
|
21
|
+
# this class.
|
22
|
+
#
|
23
|
+
# from_record - A Restforce::DB::Instances::Base instance.
|
24
|
+
#
|
25
|
+
# Returns a Restforce::DB::Instances::Base instance.
|
26
|
+
# Raises on any validation or external error.
|
27
|
+
def sync!(from_record)
|
28
|
+
if synced?(from_record)
|
29
|
+
record = find(from_record.id)
|
30
|
+
record.copy!(from_record)
|
31
|
+
|
32
|
+
record
|
33
|
+
else
|
34
|
+
create!(from_record)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
module DB
|
4
|
+
|
5
|
+
module RecordTypes
|
6
|
+
|
7
|
+
# Restforce::DB::RecordTypes::Salesforce serves as a wrapper for a single
|
8
|
+
# Salesforce object class, allowing for standard record lookups and
|
9
|
+
# attribute mappings.
|
10
|
+
class Salesforce < Base
|
11
|
+
|
12
|
+
# Public: Create an instance of this Salesforce model for the passed
|
13
|
+
# database record.
|
14
|
+
#
|
15
|
+
# from_record - A Restforce::DB::Instances::ActiveRecord instance.
|
16
|
+
#
|
17
|
+
# Returns a Restforce::DB::Instances::Salesforce instance.
|
18
|
+
# Raises on any error from Salesforce.
|
19
|
+
def create!(from_record)
|
20
|
+
attributes = @mapping.convert(:salesforce, from_record.attributes)
|
21
|
+
record_id = DB.client.create!(@record_type, attributes)
|
22
|
+
from_record.update!(salesforce_id: record_id)
|
23
|
+
|
24
|
+
find(record_id)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Public: Find the Salesforce record corresponding to the passed id.
|
28
|
+
#
|
29
|
+
# id - The id of the record in Salesforce.
|
30
|
+
#
|
31
|
+
# Returns nil or a Restforce::DB::Instances::Salesforce instance.
|
32
|
+
def find(id)
|
33
|
+
record = DB.client.query(
|
34
|
+
"select #{lookups} from #{@record_type} where Id = '#{id}'",
|
35
|
+
).first
|
36
|
+
|
37
|
+
return unless record
|
38
|
+
|
39
|
+
Instances::Salesforce.new(record, @mapping)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Public: Iterate through all Salesforce records of this type.
|
43
|
+
#
|
44
|
+
# options - A Hash of options which should be applied to the set of
|
45
|
+
# fetched records. Allowed options are:
|
46
|
+
# :before - A Time object defining the most recent update
|
47
|
+
# timestamp for which records should be returned.
|
48
|
+
# :after - A Time object defining the least recent update
|
49
|
+
# timestamp for which records should be returned.
|
50
|
+
#
|
51
|
+
# Yields a series of Restforce::DB::Instances::Salesforce instances.
|
52
|
+
# Returns nothing.
|
53
|
+
def each(options = {})
|
54
|
+
constraints = [
|
55
|
+
("SystemModstamp <= #{options[:before].utc.iso8601}" if options[:before]),
|
56
|
+
("SystemModstamp > #{options[:after].utc.iso8601}" if options[:after]),
|
57
|
+
].compact.join(" and ")
|
58
|
+
constraints = " where #{constraints}" unless constraints.empty?
|
59
|
+
|
60
|
+
query = "select #{lookups} from #{@record_type}#{constraints}"
|
61
|
+
|
62
|
+
DB.client.query(query).each do |record|
|
63
|
+
yield Instances::Salesforce.new(record, @mapping)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
# Internal: Get a String of values to look up when the record is
|
70
|
+
# fetched from Salesforce. Includes all configured mappings and a
|
71
|
+
# handful of attributes for internal use.
|
72
|
+
#
|
73
|
+
# Returns a String.
|
74
|
+
def lookups
|
75
|
+
(Instances::Salesforce::INTERNAL_ATTRIBUTES + @mapping.salesforce_fields).join(", ")
|
76
|
+
end
|
77
|
+
|
78
|
+
# Internal: Has this database record already been linked to a Salesforce
|
79
|
+
# record?
|
80
|
+
#
|
81
|
+
# record - A Restforce::DB::Instances::Salesforce instance.
|
82
|
+
#
|
83
|
+
# Returns a Boolean.
|
84
|
+
def synced?(record)
|
85
|
+
record.synced?
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
module DB
|
4
|
+
|
5
|
+
# Restforce::DB::Synchronizer is responsible for synchronizing the records
|
6
|
+
# in Salesforce with the records in the database. It relies on the mappings
|
7
|
+
# configured in instances of Restforce::DB::RecordTypes::Base to create and
|
8
|
+
# update records with the appropriate values.
|
9
|
+
class Synchronizer
|
10
|
+
|
11
|
+
attr_reader :last_run
|
12
|
+
|
13
|
+
# Public: Initialize a new Restforce::DB::Synchronizer.
|
14
|
+
#
|
15
|
+
# database_record_type - A Restforce::DB::RecordTypes::ActiveRecord
|
16
|
+
# instance.
|
17
|
+
# salesforce_record_type - A Restforce::DB::RecordTypes::Salesforce
|
18
|
+
# instance.
|
19
|
+
# last_run_time - A Time object reflecting the time of the most
|
20
|
+
# recent synchronization run. Runs will only
|
21
|
+
# synchronize data more recent than this stamp.
|
22
|
+
def initialize(database_record_type, salesforce_record_type, last_run_time = nil)
|
23
|
+
@database_record_type = database_record_type
|
24
|
+
@salesforce_record_type = salesforce_record_type
|
25
|
+
@last_run = last_run_time
|
26
|
+
end
|
27
|
+
|
28
|
+
# Public: Run the synchronize process, pulling in records from Salesforce
|
29
|
+
# and the database to determine which records need to be created and/or
|
30
|
+
# updated.
|
31
|
+
#
|
32
|
+
# NOTE: We bootstrap our record lookups to the exact same timespan, and
|
33
|
+
# run the Salesforce sync into the database first. This has the effect of
|
34
|
+
# overwriting recent changes to the database, in the event that Salesforce
|
35
|
+
# has also been updated since the last sync.
|
36
|
+
#
|
37
|
+
# options - A Hash of options for configuring the run. Currently unused.
|
38
|
+
#
|
39
|
+
# Returns the Time the run was performed.
|
40
|
+
def run(_options = {})
|
41
|
+
run_time = Time.now
|
42
|
+
|
43
|
+
@salesforce_record_type.each(after: last_run, before: run_time) do |record|
|
44
|
+
@database_record_type.sync!(record)
|
45
|
+
end
|
46
|
+
@database_record_type.each(after: last_run, before: run_time) do |record|
|
47
|
+
@salesforce_record_type.sync!(record)
|
48
|
+
end
|
49
|
+
|
50
|
+
@last_run = run_time
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
module DB
|
4
|
+
|
5
|
+
# Restforce::DB::Worker represents the primary polling loop through which
|
6
|
+
# all record synchronization occurs.
|
7
|
+
class Worker
|
8
|
+
|
9
|
+
DEFAULT_INTERVAL = 5
|
10
|
+
|
11
|
+
class << self
|
12
|
+
|
13
|
+
attr_accessor :logger, :interval
|
14
|
+
|
15
|
+
# Public: Store the list of currently open file descriptors so that they
|
16
|
+
# may be reopened when a new process is spawned.
|
17
|
+
#
|
18
|
+
# Returns nothing.
|
19
|
+
def before_fork
|
20
|
+
return if @files_to_reopen
|
21
|
+
|
22
|
+
@files_to_reopen = []
|
23
|
+
ObjectSpace.each_object(File) do |file|
|
24
|
+
@files_to_reopen << file unless file.closed?
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Public: Reopen all file descriptors that have been stored through the
|
29
|
+
# before_fork hook.
|
30
|
+
#
|
31
|
+
# Returns nothing.
|
32
|
+
def after_fork
|
33
|
+
@files_to_reopen.each do |file|
|
34
|
+
begin
|
35
|
+
file.reopen file.path, "a+"
|
36
|
+
file.sync = true
|
37
|
+
rescue ::Exception # rubocop:disable HandleExceptions, RescueException
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
# Public: Initialize a new Restforce::DB::Worker.
|
45
|
+
#
|
46
|
+
# options - A Hash of options to configure the worker's run. Currently
|
47
|
+
# supported options are:
|
48
|
+
# interval - The maximum polling loop rest time.
|
49
|
+
# config - The path to a client configuration file.
|
50
|
+
# verbose - Display command line output? Defaults to false.
|
51
|
+
def initialize(options = {})
|
52
|
+
@verbose = options.fetch(:verbose) { false }
|
53
|
+
self.class.interval = options.fetch(:interval) { DEFAULT_INTERVAL }
|
54
|
+
|
55
|
+
Restforce::DB.configure { |config| config.parse(options[:config]) }
|
56
|
+
end
|
57
|
+
|
58
|
+
# Public: Start the polling loop for this Worker. Synchronizes all
|
59
|
+
# registered record types between the database and Salesforce, looping
|
60
|
+
# indefinitely until processing is interrupted by a signal.
|
61
|
+
#
|
62
|
+
# Returns nothing.
|
63
|
+
def start
|
64
|
+
trap("TERM") do
|
65
|
+
Thread.new { log "Exiting..." }
|
66
|
+
stop
|
67
|
+
end
|
68
|
+
|
69
|
+
trap("INT") do
|
70
|
+
Thread.new { log "Exiting..." }
|
71
|
+
stop
|
72
|
+
end
|
73
|
+
|
74
|
+
log "Starting synchronization..."
|
75
|
+
|
76
|
+
loop do
|
77
|
+
runtime = Benchmark.realtime do
|
78
|
+
Restforce::DB::RecordType.each do |name, record_type|
|
79
|
+
synchronize name, record_type
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
if runtime < self.class.interval && !stop?
|
84
|
+
sleep(self.class.interval - runtime)
|
85
|
+
end
|
86
|
+
|
87
|
+
break if stop?
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Public: Instruct the worker to stop running at the end of the current
|
92
|
+
# processing loop.
|
93
|
+
#
|
94
|
+
# Returns nothing.
|
95
|
+
def stop
|
96
|
+
@exit = true
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
# Internal: Synchronize the objects in the database and Salesforce
|
102
|
+
# corresponding to the passed record type.
|
103
|
+
#
|
104
|
+
# name - The String name of the record type to synchronize.
|
105
|
+
# record_type - A Restforce::DB::RecordType.
|
106
|
+
#
|
107
|
+
# Returns a Boolean.
|
108
|
+
def synchronize(name, record_type)
|
109
|
+
log "(#{name}) SYNCHRONIZING"
|
110
|
+
runtime = Benchmark.realtime { record_type.synchronize }
|
111
|
+
log format("(#{name}) COMPLETED after %.4f", runtime)
|
112
|
+
|
113
|
+
return true
|
114
|
+
rescue => e
|
115
|
+
error(e)
|
116
|
+
|
117
|
+
return false
|
118
|
+
end
|
119
|
+
|
120
|
+
# Internal: Has this worker been instructed to stop?
|
121
|
+
#
|
122
|
+
# Returns a boolean.
|
123
|
+
def stop?
|
124
|
+
@exit == true
|
125
|
+
end
|
126
|
+
|
127
|
+
# Internal: Log the passed text at the specified level.
|
128
|
+
#
|
129
|
+
# text - The piece of text which should be logged for this worker.
|
130
|
+
# level - The level at which the text should be logged. Defaults to :info.
|
131
|
+
#
|
132
|
+
# Returns nothing.
|
133
|
+
def log(text, level = :info)
|
134
|
+
text = "[Restforce::DB] #{text}"
|
135
|
+
puts text if @verbose
|
136
|
+
|
137
|
+
return unless self.class.logger
|
138
|
+
|
139
|
+
self.class.logger.send(level, "#{Time.now.strftime('%FT%T%z')}: #{text}")
|
140
|
+
end
|
141
|
+
|
142
|
+
# Internal: Log an error for the worker, outputting the entire error
|
143
|
+
# stacktrace and applying the appropriate log level.
|
144
|
+
#
|
145
|
+
# exception - An Exception object.
|
146
|
+
#
|
147
|
+
# Returns nothing.
|
148
|
+
def error(exception)
|
149
|
+
log "#{exception.message}\n#{exception.backtrace.join("\n")}", :error
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|