restforce-db 0.4.0 → 0.5.0
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 +4 -4
- data/lib/file_daemon.rb +42 -0
- data/lib/restforce/db/accumulator.rb +41 -0
- data/lib/restforce/db/attribute_map.rb +132 -0
- data/lib/restforce/db/collector.rb +79 -0
- data/lib/restforce/db/initializer.rb +62 -0
- data/lib/restforce/db/instances/base.rb +1 -14
- data/lib/restforce/db/instances/salesforce.rb +7 -0
- data/lib/restforce/db/mapping.rb +33 -79
- data/lib/restforce/db/record_types/base.rb +0 -30
- data/lib/restforce/db/runner.rb +80 -0
- data/lib/restforce/db/synchronizer.rb +29 -37
- data/lib/restforce/db/version.rb +1 -1
- data/lib/restforce/db/worker.rb +53 -40
- data/lib/restforce/db.rb +6 -0
- data/test/cassettes/{Restforce_DB_Synchronizer/_run/given_a_Salesforce_record_with_an_associated_database_record/when_synchronization_is_stale/updates_the_database_record.yml → Restforce_DB_Collector/_run/given_a_Salesforce_record_with_an_associated_database_record/returns_the_attributes_from_both_records.yml} +36 -36
- data/test/cassettes/Restforce_DB_Collector/_run/given_an_existing_Salesforce_record/returns_the_attributes_from_the_Salesforce_record.yml +197 -0
- data/test/cassettes/Restforce_DB_Collector/_run/given_an_existing_database_record/returns_the_attributes_from_the_database_record.yml +81 -0
- data/test/cassettes/Restforce_DB_Initializer/_run/given_an_existing_Salesforce_record/for_a_non-root_mapping/does_not_create_a_database_record.yml +119 -0
- data/test/cassettes/{Restforce_DB_Synchronizer/_run/given_a_Salesforce_record_with_an_associated_database_record/when_synchronization_is_up-to-date/does_not_update_the_database_record.yml → Restforce_DB_Initializer/_run/given_an_existing_Salesforce_record/for_a_root_mapping/creates_a_matching_database_record.yml} +28 -28
- data/test/cassettes/{Restforce_DB_Synchronizer/_run/given_an_existing_database_record → Restforce_DB_Initializer/_run/given_an_existing_database_record/for_a_root_mapping}/populates_Salesforce_with_the_new_record.yml +44 -44
- data/test/cassettes/{Restforce_DB_Synchronizer/_run/given_an_existing_Salesforce_record/for_a_non-root_mapping/does_not_create_a_database_record.yml → Restforce_DB_Instances_Salesforce/_synced_/when_a_matching_database_record_exists/returns_true.yml} +30 -30
- data/test/cassettes/{Restforce_DB_Synchronizer/_run/given_an_existing_Salesforce_record/for_a_root_mapping/creates_a_matching_database_record.yml → Restforce_DB_Instances_Salesforce/_synced_/when_no_matching_database_record_exists/returns_false.yml} +30 -30
- data/test/cassettes/Restforce_DB_Synchronizer/_run/given_a_Salesforce_record_with_an_associated_database_record/updates_the_database_record.yml +194 -0
- data/test/cassettes/Restforce_DB_Synchronizer/_run/given_a_Salesforce_record_with_an_associated_database_record/updates_the_salesforce_record.yml +233 -0
- data/test/lib/restforce/db/accumulator_test.rb +71 -0
- data/test/lib/restforce/db/attribute_map_test.rb +70 -0
- data/test/lib/restforce/db/collector_test.rb +91 -0
- data/test/lib/restforce/db/initializer_test.rb +92 -0
- data/test/lib/restforce/db/instances/active_record_test.rb +0 -13
- data/test/lib/restforce/db/instances/salesforce_test.rb +20 -13
- data/test/lib/restforce/db/mapping_test.rb +1 -37
- data/test/lib/restforce/db/record_types/active_record_test.rb +0 -40
- data/test/lib/restforce/db/runner_test.rb +40 -0
- data/test/lib/restforce/db/synchronizer_test.rb +26 -86
- metadata +23 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 42506d7bec2c00a7c2161188ef2b238459942da8
|
4
|
+
data.tar.gz: ce83cb60cbfdbd48388189aef560b3257d86995c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0b394e0887d9b4986320b8c1776f0098069aed032a0810c17465ab14d75ce57ac9bbc505cdbabe861b80a89fc7b559ba3f559954ae0beb2adfd398c5c5d502e9
|
7
|
+
data.tar.gz: 7821768fbd4a8da7e87ca397a9015059e9f81d1f08e064e63b4fd626705a58a2ee27a62e0c7499423f59843dfaf5d8d3d4b5b1b34f7697fc8d820b4336f67e8a
|
data/lib/file_daemon.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# FileDaemon defines some standard hooks for forking processes which retain
|
2
|
+
# file descriptors. Implementation derived from the Delayed::Job library:
|
3
|
+
# https://github.com/collectiveidea/delayed_job/blob/master/lib/delayed/worker.rb#L77-L98.
|
4
|
+
module FileDaemon
|
5
|
+
|
6
|
+
# Public: Extend the including class with before/after_fork hooks.
|
7
|
+
#
|
8
|
+
# base - The including class.
|
9
|
+
#
|
10
|
+
# Returns nothing.
|
11
|
+
def self.included(base)
|
12
|
+
base.extend(ClassMethods)
|
13
|
+
end
|
14
|
+
|
15
|
+
# :nodoc:
|
16
|
+
module ClassMethods
|
17
|
+
|
18
|
+
# Public: Store the list of currently open file descriptors so that they
|
19
|
+
# may be reopened when a new process is spawned.
|
20
|
+
#
|
21
|
+
# Returns nothing.
|
22
|
+
def before_fork
|
23
|
+
@files_to_reopen ||= ObjectSpace.each_object(File).reject(&:closed?)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Public: Reopen all file descriptors that have been stored through the
|
27
|
+
# before_fork hook.
|
28
|
+
#
|
29
|
+
# Returns nothing.
|
30
|
+
def after_fork
|
31
|
+
@files_to_reopen.each do |file|
|
32
|
+
begin
|
33
|
+
file.reopen file.path, "a+"
|
34
|
+
file.sync = true
|
35
|
+
rescue ::IOError # rubocop:disable HandleExceptions
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
module DB
|
4
|
+
|
5
|
+
# Restforce::DB::Accumulator is responsible for the accumulation of changes
|
6
|
+
# over the course of a single synchronization run. As we iterate over the
|
7
|
+
# various mappings, we build a set of changes for each Salesforce ID, which
|
8
|
+
# is then applied to all objects synchronized with that Salesforce object.
|
9
|
+
class Accumulator < Hash
|
10
|
+
|
11
|
+
# Public: Get the accumulated list of attributes after all changes have
|
12
|
+
# been applied.
|
13
|
+
#
|
14
|
+
# Returns a Hash.
|
15
|
+
def attributes
|
16
|
+
sort.reverse.each_with_object({}) do |(_, changeset), final|
|
17
|
+
changeset.each { |attribute, value| final[attribute] ||= value }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Public: Get a Hash representing the changes that would need to be
|
22
|
+
# applied to make a passed Hash a subset of this Accumulator's derived
|
23
|
+
# attributes Hash.
|
24
|
+
#
|
25
|
+
# comparison - A Hash mapping of attributes to values.
|
26
|
+
#
|
27
|
+
# Returns a Hash.
|
28
|
+
def diff(comparison)
|
29
|
+
attributes.each_with_object({}) do |(attribute, value), diff|
|
30
|
+
next unless comparison.key?(attribute)
|
31
|
+
next if comparison[attribute] == value
|
32
|
+
|
33
|
+
diff[attribute] = value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
module DB
|
4
|
+
|
5
|
+
# Restforce::DB::AttributeMap encapsulates the logic for converting between
|
6
|
+
# various representations of attribute hashes.
|
7
|
+
class AttributeMap
|
8
|
+
|
9
|
+
# Public: Initialize a Restforce::DB::AttributeMap.
|
10
|
+
#
|
11
|
+
# database_model - A Class compatible with ActiveRecord::Base.
|
12
|
+
# salesforce_model - A String name of an object type in Salesforce.
|
13
|
+
# fields - A Hash of mappings between database columns and
|
14
|
+
# fields in Salesforce.
|
15
|
+
def initialize(database_model, salesforce_model, fields = {})
|
16
|
+
@database_model = database_model
|
17
|
+
@salesforce_model = salesforce_model
|
18
|
+
@fields = fields
|
19
|
+
|
20
|
+
@types = {
|
21
|
+
database_model => :database,
|
22
|
+
salesforce_model => :salesforce,
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
# Public: Build a normalized Hash of attributes from the appropriate set
|
27
|
+
# of mappings. The keys of the resulting mapping Hash will correspond to
|
28
|
+
# the database column names.
|
29
|
+
#
|
30
|
+
# from_format - A String or Class reflecting the record type from which
|
31
|
+
# the attribute Hash is being compiled.
|
32
|
+
#
|
33
|
+
# Yields a series of attribute names.
|
34
|
+
# Returns a Hash.
|
35
|
+
def attributes(from_format)
|
36
|
+
use_mappings =
|
37
|
+
case @types[from_format]
|
38
|
+
when :salesforce
|
39
|
+
@fields
|
40
|
+
when :database
|
41
|
+
# Generate a mapping of database column names to record attributes.
|
42
|
+
@fields.keys.zip(@fields.keys)
|
43
|
+
else
|
44
|
+
raise ArgumentError
|
45
|
+
end
|
46
|
+
|
47
|
+
use_mappings.each_with_object({}) do |(attribute, mapping), values|
|
48
|
+
values[attribute] = yield(mapping)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Public: Convert a Hash of normalized attributes to a format compatible
|
53
|
+
# with a specific platform.
|
54
|
+
#
|
55
|
+
# to_format - A String or Class reflecting the record type for which the
|
56
|
+
# attribute Hash is being compiled.
|
57
|
+
# attributes - A Hash of attributes, with keys corresponding to the
|
58
|
+
# normalized attribute names.
|
59
|
+
#
|
60
|
+
# Examples
|
61
|
+
#
|
62
|
+
# mapping = Mapping.new(MyClass, "Object__c", some_key: "SomeField__c")
|
63
|
+
#
|
64
|
+
# mapping.convert("Object__c", some_key: "some value")
|
65
|
+
# # => { "Some_Field__c" => "some value" }
|
66
|
+
#
|
67
|
+
# mapping.convert(MyClass, some_key: "some other value")
|
68
|
+
# # => { some_key: "some other value" }
|
69
|
+
#
|
70
|
+
# Returns a Hash.
|
71
|
+
def convert(to_format, attributes)
|
72
|
+
case @types[to_format]
|
73
|
+
when :database
|
74
|
+
attributes.dup
|
75
|
+
when :salesforce
|
76
|
+
@fields.each_with_object({}) do |(attribute, mapping), converted|
|
77
|
+
next unless attributes.key?(attribute)
|
78
|
+
converted[mapping] = attributes[attribute]
|
79
|
+
end
|
80
|
+
else
|
81
|
+
raise ArgumentError
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Public: Convert a Hash of Salesforce attributes to a format compatible
|
86
|
+
# with a specific platform.
|
87
|
+
#
|
88
|
+
# to_format - A String or Class reflecting the record type for which the
|
89
|
+
# attribute Hash is being compiled.
|
90
|
+
# attributes - A Hash of attributes, with keys corresponding to the
|
91
|
+
# Salesforce attribute names.
|
92
|
+
#
|
93
|
+
# Examples
|
94
|
+
#
|
95
|
+
# map = AttributeMap.new(
|
96
|
+
# MyClass,
|
97
|
+
# "Object__c",
|
98
|
+
# some_key: "SomeField__c",
|
99
|
+
# )
|
100
|
+
#
|
101
|
+
# map.convert_from_salesforce(
|
102
|
+
# "Object__c",
|
103
|
+
# "Some_Field__c" => "some value",
|
104
|
+
# )
|
105
|
+
# # => { "Some_Field__c" => "some value" }
|
106
|
+
#
|
107
|
+
# map.convert_from_salesforce(
|
108
|
+
# MyClass,
|
109
|
+
# "Some_Field__c" => "some other value",
|
110
|
+
# )
|
111
|
+
# # => { some_key: "some other value" }
|
112
|
+
#
|
113
|
+
# Returns a Hash.
|
114
|
+
def convert_from_salesforce(to_format, attributes)
|
115
|
+
case @types[to_format]
|
116
|
+
when :database
|
117
|
+
@fields.each_with_object({}) do |(attribute, mapping), converted|
|
118
|
+
next unless attributes.key?(mapping)
|
119
|
+
converted[attribute] = attributes[mapping]
|
120
|
+
end
|
121
|
+
when :salesforce
|
122
|
+
attributes.dup
|
123
|
+
else
|
124
|
+
raise ArgumentError
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
module DB
|
4
|
+
|
5
|
+
# Restforce::DB::Collector is responsible for grabbing the attributes from
|
6
|
+
# recently-updated records for purposes of synchronization. It relies on the
|
7
|
+
# mappings configured in instances of Restforce::DB::RecordTypes::Base to
|
8
|
+
# locate recently-updated records and fetch their attributes.
|
9
|
+
class Collector
|
10
|
+
|
11
|
+
attr_reader :last_run
|
12
|
+
|
13
|
+
# Public: Initialize a new Restforce::DB::Collector.
|
14
|
+
#
|
15
|
+
# mapping - A Restforce::DB::Mapping instance.
|
16
|
+
# runner - A Restforce::DB::Runner instance.
|
17
|
+
def initialize(mapping, runner = Runner.new)
|
18
|
+
@mapping = mapping
|
19
|
+
@runner = runner
|
20
|
+
end
|
21
|
+
|
22
|
+
# Public: Run the collection process, pulling in records from Salesforce
|
23
|
+
# and the database to determine the lists of attributes to apply to all
|
24
|
+
# mapped records.
|
25
|
+
#
|
26
|
+
# accumulator - A Hash-like accumulator object.
|
27
|
+
#
|
28
|
+
# Returns a Hash mapping Salesforce ID/type combinations to accumulators.
|
29
|
+
def run(accumulator = nil)
|
30
|
+
@accumulated_changes = accumulator || accumulated_changes
|
31
|
+
|
32
|
+
@runner.run(@mapping) do |run|
|
33
|
+
run.salesforce_records { |record| accumulate(record) }
|
34
|
+
run.database_records { |record| accumulate(record) }
|
35
|
+
end
|
36
|
+
|
37
|
+
accumulated_changes
|
38
|
+
ensure
|
39
|
+
# Clear out the results of this run so we start fresh next time.
|
40
|
+
@accumulated_changes = nil
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# Internal: Get a Hash to collect accumulated changes.
|
46
|
+
#
|
47
|
+
# Returns a Hash of Hashes.
|
48
|
+
def accumulated_changes
|
49
|
+
@accumulated_changes ||= Hash.new { |h, k| h[k] = {} }
|
50
|
+
end
|
51
|
+
|
52
|
+
# Internal: Append the passed record's attributes to its accumulated list
|
53
|
+
# of changesets.
|
54
|
+
#
|
55
|
+
# record - A Restforce::DB::Instances::Base.
|
56
|
+
#
|
57
|
+
# Returns nothing.
|
58
|
+
def accumulate(record)
|
59
|
+
accumulated_changes[key_for(record)].store(
|
60
|
+
record.last_update,
|
61
|
+
@mapping.convert(@mapping.salesforce_model, record.attributes),
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Internal: Get a unique key with enough information to look up the passed
|
66
|
+
# record in Salesforce.
|
67
|
+
#
|
68
|
+
# record - A Restforce::DB::Instances::Base.
|
69
|
+
#
|
70
|
+
# Returns an Object.
|
71
|
+
def key_for(record)
|
72
|
+
[record.id, record.mapping.salesforce_model]
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
module DB
|
4
|
+
|
5
|
+
# Restforce::DB::Initializer is responsible for ensuring that both systems
|
6
|
+
# are populated with the same records at the root level. It iterates through
|
7
|
+
# recently added or updated records in each system for a mapping, and
|
8
|
+
# creates a matching record in the other system, when necessary.
|
9
|
+
class Initializer
|
10
|
+
|
11
|
+
# Public: Initialize a Restforce::DB::Initializer.
|
12
|
+
#
|
13
|
+
# mapping - A Restforce::DB::Mapping.
|
14
|
+
# runner - A Restforce::DB::Runner.
|
15
|
+
def initialize(mapping, runner = Runner.new)
|
16
|
+
@mapping = mapping
|
17
|
+
@runner = runner
|
18
|
+
end
|
19
|
+
|
20
|
+
# Public: Run the initialization loop for this mapping.
|
21
|
+
#
|
22
|
+
# Returns nothing.
|
23
|
+
def run
|
24
|
+
return unless @mapping.root?
|
25
|
+
|
26
|
+
@runner.run(@mapping) do |run|
|
27
|
+
run.salesforce_records { |record| create_in_database(record) }
|
28
|
+
run.database_records { |record| create_in_salesforce(record) }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
# Internal: Attempt to create a partner record in the database for the
|
35
|
+
# passed Salesforce record. Does nothing if the Salesforce record has
|
36
|
+
# already been synchronized into the system at least once.
|
37
|
+
#
|
38
|
+
# record - A Restforce::DB::Instances::Salesforce.
|
39
|
+
#
|
40
|
+
# Returns nothing.
|
41
|
+
def create_in_database(record)
|
42
|
+
return if record.synced?
|
43
|
+
@mapping.database_record_type.create!(record)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Internal: Attempt to create a partner record in Salesforce for the
|
47
|
+
# passed database record. Does nothing if the database record already has
|
48
|
+
# an associated Salesforce record.
|
49
|
+
#
|
50
|
+
# record - A Restforce::DB::Instances::ActiveRecord.
|
51
|
+
#
|
52
|
+
# Returns nothing.
|
53
|
+
def create_in_salesforce(record)
|
54
|
+
return if record.synced?
|
55
|
+
@mapping.salesforce_record_type.create!(record)
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -8,7 +8,7 @@ module Restforce
|
|
8
8
|
# models defined in the Restforce::DB::Instances namespace.
|
9
9
|
class Base
|
10
10
|
|
11
|
-
attr_reader :record
|
11
|
+
attr_reader :record, :record_type, :mapping
|
12
12
|
|
13
13
|
# Public: Initialize a new Restforce::DB::Instances::Base instance.
|
14
14
|
#
|
@@ -32,19 +32,6 @@ module Restforce
|
|
32
32
|
after_sync
|
33
33
|
end
|
34
34
|
|
35
|
-
# Public: Update the instance with attributes copied from the passed
|
36
|
-
# record.
|
37
|
-
#
|
38
|
-
# record - An object responding to `#attributes`. Must return a Hash of
|
39
|
-
# attributes corresponding to the configured mappings for this
|
40
|
-
# instance.
|
41
|
-
#
|
42
|
-
# Returns self.
|
43
|
-
# Raises if the update fails for any reason.
|
44
|
-
def copy!(from_record)
|
45
|
-
update! @mapping.convert(@record_type, from_record.attributes)
|
46
|
-
end
|
47
|
-
|
48
35
|
# Public: Get a Hash mapping the configured attributes names to their
|
49
36
|
# values for this instance.
|
50
37
|
#
|
data/lib/restforce/db/mapping.rb
CHANGED
@@ -14,14 +14,13 @@ module Restforce
|
|
14
14
|
include Enumerable
|
15
15
|
attr_accessor :collection
|
16
16
|
|
17
|
-
# Public: Get the Restforce::DB::Mapping entry for the specified
|
18
|
-
# database model.
|
17
|
+
# Public: Get the Restforce::DB::Mapping entry for the specified model.
|
19
18
|
#
|
20
|
-
#
|
19
|
+
# model - A String or Class.
|
21
20
|
#
|
22
21
|
# Returns a Restforce::DB::Mapping.
|
23
|
-
def [](
|
24
|
-
collection[
|
22
|
+
def [](model)
|
23
|
+
collection[model]
|
25
24
|
end
|
26
25
|
|
27
26
|
# Public: Iterate through all registered Restforce::DB::Mappings.
|
@@ -29,17 +28,43 @@ module Restforce
|
|
29
28
|
# Yields one Mapping for each database-to-Salesforce mapping.
|
30
29
|
# Returns nothing.
|
31
30
|
def each
|
32
|
-
collection.each do |
|
31
|
+
collection.each do |model, mappings|
|
32
|
+
# Since each mapping is inserted twice, we ignore the half which
|
33
|
+
# were inserted via Salesforce model names.
|
34
|
+
next unless model.is_a?(Class)
|
35
|
+
|
33
36
|
mappings.each do |mapping|
|
34
37
|
yield mapping
|
35
38
|
end
|
36
39
|
end
|
37
40
|
end
|
38
41
|
|
42
|
+
# Public: Add a mapping to the overarching Mapping collection. Appends
|
43
|
+
# the mapping to the collection for both its database and salesforce
|
44
|
+
# object types.
|
45
|
+
#
|
46
|
+
# mapping - A Restforce::DB::Mapping.
|
47
|
+
#
|
48
|
+
# Returns nothing.
|
49
|
+
def <<(mapping)
|
50
|
+
[mapping.database_model, mapping.salesforce_model].each do |model|
|
51
|
+
collection[model] ||= []
|
52
|
+
collection[model] << mapping
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
39
56
|
end
|
40
57
|
|
41
58
|
self.collection ||= {}
|
42
59
|
|
60
|
+
extend Forwardable
|
61
|
+
def_delegators(
|
62
|
+
:@attribute_map,
|
63
|
+
:attributes,
|
64
|
+
:convert,
|
65
|
+
:convert_from_salesforce,
|
66
|
+
)
|
67
|
+
|
43
68
|
attr_reader(
|
44
69
|
:database_model,
|
45
70
|
:salesforce_model,
|
@@ -78,13 +103,9 @@ module Restforce
|
|
78
103
|
@conditions = options.fetch(:conditions) { [] }
|
79
104
|
@through = options.fetch(:through) { nil }
|
80
105
|
|
81
|
-
@
|
82
|
-
database_model => :database,
|
83
|
-
salesforce_model => :salesforce,
|
84
|
-
}
|
106
|
+
@attribute_map = AttributeMap.new(database_model, salesforce_model, @fields)
|
85
107
|
|
86
|
-
self.class
|
87
|
-
self.class.collection[database_model] << self
|
108
|
+
self.class << self
|
88
109
|
end
|
89
110
|
|
90
111
|
# Public: Get a list of the relevant Salesforce field names for this
|
@@ -131,73 +152,6 @@ module Restforce
|
|
131
152
|
@through.nil?
|
132
153
|
end
|
133
154
|
|
134
|
-
# Public: Build a normalized Hash of attributes from the appropriate set
|
135
|
-
# of mappings. The keys of the resulting mapping Hash will correspond to
|
136
|
-
# the database column names.
|
137
|
-
#
|
138
|
-
# in_format - A String or Class reflecting the record type from which the
|
139
|
-
# attribute Hash is being compiled.
|
140
|
-
#
|
141
|
-
# Yields the attribute name.
|
142
|
-
# Returns a Hash.
|
143
|
-
def attributes(from_format)
|
144
|
-
use_mappings =
|
145
|
-
case @types[from_format]
|
146
|
-
when :salesforce
|
147
|
-
@fields
|
148
|
-
when :database
|
149
|
-
# Generate a mapping of database column names to record attributes.
|
150
|
-
database_fields.zip(database_fields)
|
151
|
-
else
|
152
|
-
raise ArgumentError
|
153
|
-
end
|
154
|
-
|
155
|
-
use_mappings.each_with_object({}) do |(attribute, mapping), values|
|
156
|
-
values[attribute] = yield(mapping)
|
157
|
-
end
|
158
|
-
end
|
159
|
-
|
160
|
-
# Public: Convert a Hash of attributes to a format compatible with a
|
161
|
-
# specific platform.
|
162
|
-
#
|
163
|
-
# to_format - A String or Class reflecting the record type for which the
|
164
|
-
# attribute Hash is being compiled.
|
165
|
-
# attributes - A Hash of attributes, with keys corresponding to the
|
166
|
-
# normalized attribute names.
|
167
|
-
#
|
168
|
-
# Examples
|
169
|
-
#
|
170
|
-
# mapping = Mapping.new(MyClass, "Object__c", some_key: "SomeField__c")
|
171
|
-
#
|
172
|
-
# mapping.convert("Object__c", some_key: "some value")
|
173
|
-
# # => { "Some_Field__c" => "some value" }
|
174
|
-
#
|
175
|
-
# mapping.convert(MyClass, some_key: "some other value")
|
176
|
-
# # => { some_key: "some other value" }
|
177
|
-
#
|
178
|
-
# Returns a Hash.
|
179
|
-
def convert(to_format, attributes)
|
180
|
-
case @types[to_format]
|
181
|
-
when :database
|
182
|
-
attributes.dup
|
183
|
-
when :salesforce
|
184
|
-
@fields.each_with_object({}) do |(attribute, mapping), converted|
|
185
|
-
next unless attributes.key?(attribute)
|
186
|
-
converted[mapping] = attributes[attribute]
|
187
|
-
end
|
188
|
-
else
|
189
|
-
raise ArgumentError
|
190
|
-
end
|
191
|
-
end
|
192
|
-
|
193
|
-
# Public: Get a Synchronizer for the record types captured by this
|
194
|
-
# Mapping.
|
195
|
-
#
|
196
|
-
# Returns a Restforce::DB::Synchronizer.
|
197
|
-
def synchronizer
|
198
|
-
@synchronizer ||= Synchronizer.new(@database_record_type, @salesforce_record_type)
|
199
|
-
end
|
200
|
-
|
201
155
|
end
|
202
156
|
|
203
157
|
end
|
@@ -19,36 +19,6 @@ module Restforce
|
|
19
19
|
@mapping = mapping
|
20
20
|
end
|
21
21
|
|
22
|
-
# Public: Synchronize the passed record to the record type defined by
|
23
|
-
# this class.
|
24
|
-
#
|
25
|
-
# from_record - A Restforce::DB::Instances::Base instance.
|
26
|
-
#
|
27
|
-
# Returns a Restforce::DB::Instances::Base instance.
|
28
|
-
# Raises on any validation or external error.
|
29
|
-
def sync!(from_record)
|
30
|
-
if synced?(from_record)
|
31
|
-
update!(from_record)
|
32
|
-
elsif @mapping.root?
|
33
|
-
create!(from_record)
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
# Public: Update an existing record of this record type with the
|
38
|
-
# attributes from the passed record. Only applies changes if from_record
|
39
|
-
# has been more recently updated than the last record synchronization.
|
40
|
-
#
|
41
|
-
# from_record - A Restforce::DB::Instances::Base instance.
|
42
|
-
#
|
43
|
-
# Returns a Restforce::DB::Instances::Base instance.
|
44
|
-
# Raises on any validation or external error.
|
45
|
-
def update!(from_record)
|
46
|
-
record = find(from_record.id)
|
47
|
-
return record if from_record.last_update < record.last_synchronize
|
48
|
-
|
49
|
-
record.copy!(from_record)
|
50
|
-
end
|
51
|
-
|
52
22
|
end
|
53
23
|
|
54
24
|
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
module DB
|
4
|
+
|
5
|
+
# Restforce::DB::Runner provides an abstraction for lookup timing during the
|
6
|
+
# synchronization process. It provides methods for accessing only recently-
|
7
|
+
# modified records within the context of a specific Mapping.
|
8
|
+
class Runner
|
9
|
+
|
10
|
+
attr_reader :last_run, :before, :after
|
11
|
+
|
12
|
+
# Public: Initialize a new Restforce::DB::Runner.
|
13
|
+
#
|
14
|
+
# delay - A Numeric offet to apply to all record lookups. Can be
|
15
|
+
# used to mitigate server timing issues.
|
16
|
+
# last_run_time - A Time indicating the point at which new runs should
|
17
|
+
# begin.
|
18
|
+
def initialize(delay = 0, last_run_time = DB.last_run)
|
19
|
+
@delay = delay
|
20
|
+
@last_run = last_run_time
|
21
|
+
end
|
22
|
+
|
23
|
+
# Public: Indicate that a new phase of the run is beginning. Updates the
|
24
|
+
# before/after timestamp to ensure that new lookups are properly filtered.
|
25
|
+
#
|
26
|
+
# Returns the new run Time.
|
27
|
+
def tick!
|
28
|
+
run_time = Time.now
|
29
|
+
|
30
|
+
@before = run_time - @delay
|
31
|
+
@after = last_run - @delay if @last_run
|
32
|
+
|
33
|
+
@last_run = run_time
|
34
|
+
end
|
35
|
+
|
36
|
+
# Public: Grant access to recently-updated records for a specific mapping.
|
37
|
+
#
|
38
|
+
# mapping - A Restforce::DB::Mapping instance.
|
39
|
+
#
|
40
|
+
# Yields self, in the context of the passed mapping.
|
41
|
+
# Returns nothing.
|
42
|
+
def run(mapping)
|
43
|
+
@mapping = mapping
|
44
|
+
yield self
|
45
|
+
ensure
|
46
|
+
@mapping = nil
|
47
|
+
end
|
48
|
+
|
49
|
+
# Public: Iterate through recently-updated records for the Salesforce
|
50
|
+
# record type defined by the current mapping.
|
51
|
+
#
|
52
|
+
# Yields a series of Restforce::DB::Instances::Salesforce objects.
|
53
|
+
# Returns nothing.
|
54
|
+
def salesforce_records
|
55
|
+
@mapping.salesforce_record_type.each(options) { |record| yield record }
|
56
|
+
end
|
57
|
+
|
58
|
+
# Public: Iterate through recently-updated records for the database model
|
59
|
+
# record type defined by the current mapping.
|
60
|
+
#
|
61
|
+
# Yields a series of Restforce::DB::Instances::ActiveRecord objects.
|
62
|
+
# Returns nothing.
|
63
|
+
def database_records
|
64
|
+
@mapping.database_record_type.each(options) { |record| yield record }
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
# Internal: Get a Hash of options to apply to record lookups.
|
70
|
+
#
|
71
|
+
# Returns a Hash.
|
72
|
+
def options
|
73
|
+
{ after: after, before: before }
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|