restforce-db 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|