restforce-db 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/file_daemon.rb +42 -0
  3. data/lib/restforce/db/accumulator.rb +41 -0
  4. data/lib/restforce/db/attribute_map.rb +132 -0
  5. data/lib/restforce/db/collector.rb +79 -0
  6. data/lib/restforce/db/initializer.rb +62 -0
  7. data/lib/restforce/db/instances/base.rb +1 -14
  8. data/lib/restforce/db/instances/salesforce.rb +7 -0
  9. data/lib/restforce/db/mapping.rb +33 -79
  10. data/lib/restforce/db/record_types/base.rb +0 -30
  11. data/lib/restforce/db/runner.rb +80 -0
  12. data/lib/restforce/db/synchronizer.rb +29 -37
  13. data/lib/restforce/db/version.rb +1 -1
  14. data/lib/restforce/db/worker.rb +53 -40
  15. data/lib/restforce/db.rb +6 -0
  16. 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
  17. data/test/cassettes/Restforce_DB_Collector/_run/given_an_existing_Salesforce_record/returns_the_attributes_from_the_Salesforce_record.yml +197 -0
  18. data/test/cassettes/Restforce_DB_Collector/_run/given_an_existing_database_record/returns_the_attributes_from_the_database_record.yml +81 -0
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. data/test/cassettes/Restforce_DB_Synchronizer/_run/given_a_Salesforce_record_with_an_associated_database_record/updates_the_database_record.yml +194 -0
  25. data/test/cassettes/Restforce_DB_Synchronizer/_run/given_a_Salesforce_record_with_an_associated_database_record/updates_the_salesforce_record.yml +233 -0
  26. data/test/lib/restforce/db/accumulator_test.rb +71 -0
  27. data/test/lib/restforce/db/attribute_map_test.rb +70 -0
  28. data/test/lib/restforce/db/collector_test.rb +91 -0
  29. data/test/lib/restforce/db/initializer_test.rb +92 -0
  30. data/test/lib/restforce/db/instances/active_record_test.rb +0 -13
  31. data/test/lib/restforce/db/instances/salesforce_test.rb +20 -13
  32. data/test/lib/restforce/db/mapping_test.rb +1 -37
  33. data/test/lib/restforce/db/record_types/active_record_test.rb +0 -40
  34. data/test/lib/restforce/db/runner_test.rb +40 -0
  35. data/test/lib/restforce/db/synchronizer_test.rb +26 -86
  36. metadata +23 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 98a9d9a418a15a7b4ca5ab954bc95d3e831e3bca
4
- data.tar.gz: 26effab90549afaecad3d7883ef6f0881c4e96d4
3
+ metadata.gz: 42506d7bec2c00a7c2161188ef2b238459942da8
4
+ data.tar.gz: ce83cb60cbfdbd48388189aef560b3257d86995c
5
5
  SHA512:
6
- metadata.gz: 5b8140eb2811c5fad0e94d82bf255219eb15d95105581eb32d84ce3cf0fd504495c5b39251d436581eee8f1955355311484491b26571b0041cb38c50f0a2e269
7
- data.tar.gz: 170e5593fe2cf93d1e2500927baaec31747d5670fd7bb4113619d6ba65e35fdabcbc48201cdc7213a696ea565772218111f95575945c3ad62e7b5554dda852c9
6
+ metadata.gz: 0b394e0887d9b4986320b8c1776f0098069aed032a0810c17465ab14d75ce57ac9bbc505cdbabe861b80a89fc7b559ba3f559954ae0beb2adfd398c5c5d502e9
7
+ data.tar.gz: 7821768fbd4a8da7e87ca397a9015059e9f81d1f08e064e63b4fd626705a58a2ee27a62e0c7499423f59843dfaf5d8d3d4b5b1b34f7697fc8d820b4336f67e8a
@@ -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
  #
@@ -36,6 +36,13 @@ module Restforce
36
36
  last_update
37
37
  end
38
38
 
39
+ # Public: Has this record been synced with Salesforce?
40
+ #
41
+ # Returns a Boolean.
42
+ def synced?
43
+ @mapping.database_model.exists?(@mapping.lookup_column => id)
44
+ end
45
+
39
46
  end
40
47
 
41
48
  end
@@ -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
- # database_model - A Class compatible with ActiveRecord::Base.
19
+ # model - A String or Class.
21
20
  #
22
21
  # Returns a Restforce::DB::Mapping.
23
- def [](database_model)
24
- collection[database_model]
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 |_, mappings|
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
- @types = {
82
- database_model => :database,
83
- salesforce_model => :salesforce,
84
- }
106
+ @attribute_map = AttributeMap.new(database_model, salesforce_model, @fields)
85
107
 
86
- self.class.collection[database_model] ||= []
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