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.
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