restforce-db 1.2.9 → 1.2.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/restforce/db/association_cache.rb +4 -1
  3. data/lib/restforce/db/associations/belongs_to.rb +27 -3
  4. data/lib/restforce/db/associations/has_many.rb +3 -3
  5. data/lib/restforce/db/associations/has_one.rb +2 -2
  6. data/lib/restforce/db/associator.rb +104 -0
  7. data/lib/restforce/db/cleaner.rb +3 -15
  8. data/lib/restforce/db/collector.rb +2 -4
  9. data/lib/restforce/db/initializer.rb +10 -10
  10. data/lib/restforce/db/record_types/active_record.rb +6 -7
  11. data/lib/restforce/db/record_types/base.rb +0 -2
  12. data/lib/restforce/db/record_types/salesforce.rb +6 -6
  13. data/lib/restforce/db/runner.rb +8 -8
  14. data/lib/restforce/db/runner_cache.rb +93 -0
  15. data/lib/restforce/db/version.rb +1 -1
  16. data/lib/restforce/db/worker.rb +23 -9
  17. data/lib/restforce/db.rb +2 -0
  18. data/restforce-db.gemspec +3 -3
  19. data/test/cassettes/Restforce_DB_Associations_BelongsTo/with_an_inverse_mapping/_lookups/returns_a_hash_of_the_associated_records_lookup_IDs.yml +195 -0
  20. data/test/cassettes/Restforce_DB_Associations_BelongsTo/with_an_inverse_mapping/_lookups/when_there_is_currently_no_associated_record/returns_a_nil_lookup_value_in_the_hash.yml +119 -0
  21. data/test/cassettes/Restforce_DB_Associator/_run/given_a_BelongsTo_association/given_another_record_for_association/when_the_Salesforce_association_is_out_of_date/updates_the_association_ID_in_Salesforce.yml +421 -0
  22. data/test/cassettes/Restforce_DB_Associator/_run/given_a_BelongsTo_association/given_another_record_for_association/when_the_database_association_is_out_of_date/updates_the_associated_record_in_the_database.yml +459 -0
  23. data/test/cassettes/Restforce_DB_RecordTypes_Salesforce/{_each/loops_through_the_existing_records_in_Salesforce.yml → _all/returns_a_list_of_the_existing_records_in_Salesforce.yml} +0 -0
  24. data/test/lib/restforce/db/accumulator_test.rb +3 -0
  25. data/test/lib/restforce/db/association_cache_test.rb +8 -0
  26. data/test/lib/restforce/db/associations/belongs_to_test.rb +18 -0
  27. data/test/lib/restforce/db/associator_test.rb +82 -0
  28. data/test/lib/restforce/db/configuration_test.rb +3 -0
  29. data/test/lib/restforce/db/dsl_test.rb +2 -0
  30. data/test/lib/restforce/db/record_types/salesforce_test.rb +3 -5
  31. data/test/lib/restforce/db/runner_cache_test.rb +60 -0
  32. data/test/lib/restforce/db/runner_test.rb +3 -0
  33. data/test/lib/restforce/db/strategies/passive_test.rb +3 -0
  34. data/test/lib/restforce/db/strategy_test.rb +2 -0
  35. data/test/lib/restforce/db/tracker_test.rb +3 -0
  36. data/test/support/stub.rb +37 -0
  37. metadata +18 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: aa1372a2f8baf6d0af77a254f0360d6ad37aef11
4
- data.tar.gz: 323beb3762fbf77ef7aa79f0465924260d22432d
3
+ metadata.gz: 34a3066aca21ed017edf09525c13198135e093e1
4
+ data.tar.gz: 1a16d555df7e28351425a1b3556361a203b3f0c6
5
5
  SHA512:
6
- metadata.gz: ad8b9ed0abf1e713b416a1e352f2c65de949dde70d7ee08db7c3fb40240995a90f7ac4b912b8e34cefa625db2630fd942b40d0a9b61f15661138a69548ed1706
7
- data.tar.gz: 5d065ba0b3078f5b5f67c6f7cb8f1de520eb5049bde8cd31b2f73c5ef6cf029ff16ae9a7337e128dc1fae18828b2d54cbe028613b74511f33a62522376f772ce
6
+ metadata.gz: ae15efd7ee63784a25e2a4946135d92ccb32a11950d377058e4f869f16130f4bc0e66b034507caf8dfba280e3c84dfa9d91c55ea0d37997449372a0169d7770a
7
+ data.tar.gz: 0a8268c35e4b99ae4586d51b5b69a34ae654c0a2939c07020133726fb6a0ea8ddbdc82cb623d75b3703ce27027fe009916ef109bce1d7f806fa0d49f79896062
@@ -10,8 +10,11 @@ module Restforce
10
10
  attr_reader :cache
11
11
 
12
12
  # Public: Initialize a new Restforce::DB::AssociationCache.
13
- def initialize
13
+ #
14
+ # record - An instance of ActiveRecord::Base (optional).
15
+ def initialize(record = nil)
14
16
  @cache = Hash.new { |h, k| h[k] = [] }
17
+ self << record if record
15
18
  end
16
19
 
17
20
  # Public: Add a record to the cache.
@@ -14,10 +14,10 @@ module Restforce
14
14
  #
15
15
  # database_record - An instance of an ActiveRecord::Base subclass.
16
16
  # salesforce_record - A Hashie::Mash representing a Salesforce object.
17
- # cache - A Restforce::DB::AssociationCache.
17
+ # cache - A Restforce::DB::AssociationCache (optional).
18
18
  #
19
19
  # Returns an Array of constructed association records.
20
- def build(database_record, salesforce_record, cache = AssociationCache.new)
20
+ def build(database_record, salesforce_record, cache = AssociationCache.new(database_record))
21
21
  @cache = cache
22
22
 
23
23
  lookups = {}
@@ -45,6 +45,30 @@ module Restforce
45
45
  @cache = nil
46
46
  end
47
47
 
48
+ # Public: Get a Hash of Lookup IDs for a specified database record,
49
+ # based on the current record for this association.
50
+ #
51
+ # database_record - An instance of an ActiveRecord::Base subclass.
52
+ #
53
+ # Returns a Hash.
54
+ def lookups(database_record)
55
+ ids = {}
56
+
57
+ for_mappings(database_record) do |mapping, lookup|
58
+ associated = database_record.association(name).reader
59
+
60
+ ids[lookup] =
61
+ if associated
62
+ # It's possible to define a belongs_to association in a Mapping
63
+ # for what is actually a one-to-many association on the
64
+ # ActiveRecord object.
65
+ Array(associated).first.send(mapping.lookup_column)
66
+ end
67
+ end
68
+
69
+ ids
70
+ end
71
+
48
72
  private
49
73
 
50
74
  # Internal: Iterate through all relevant mappings for the target
@@ -65,7 +89,7 @@ module Restforce
65
89
  # Internal: Get the Salesforce ID belonging to the associated record
66
90
  # for a supplied instance. Must be implemented per-association.
67
91
  #
68
- # instance - A Restforce::DB::Instances::Base
92
+ # instance - A Restforce::DB::Instances::Base.
69
93
  #
70
94
  # Returns a String.
71
95
  def associated_salesforce_id(instance)
@@ -14,17 +14,17 @@ module Restforce
14
14
  #
15
15
  # database_record - An instance of an ActiveRecord::Base subclass.
16
16
  # salesforce_record - A Hashie::Mash representing a Salesforce object.
17
- # cache - A Restforce::DB::AssociationCache.
17
+ # cache - A Restforce::DB::AssociationCache (optional).
18
18
  #
19
19
  # Returns an Array of constructed association records.
20
- def build(database_record, salesforce_record, cache = AssociationCache.new)
20
+ def build(database_record, salesforce_record, cache = AssociationCache.new(database_record))
21
21
  @cache = cache
22
22
 
23
23
  target = target_mapping(database_record)
24
24
  lookup_id = "#{lookup_field(target, database_record)} = '#{salesforce_record.Id}'"
25
25
 
26
26
  records = []
27
- target.salesforce_record_type.each(conditions: lookup_id) do |instance|
27
+ target.salesforce_record_type.all(conditions: lookup_id).each do |instance|
28
28
  records << construct_for(database_record, instance)
29
29
  end
30
30
 
@@ -14,10 +14,10 @@ module Restforce
14
14
  #
15
15
  # database_record - An instance of an ActiveRecord::Base subclass.
16
16
  # salesforce_record - A Hashie::Mash representing a Salesforce object.
17
- # cache - A Restforce::DB::AssociationCache.
17
+ # cache - A Restforce::DB::AssociationCache (optional).
18
18
  #
19
19
  # Returns an Array of constructed association records.
20
- def build(database_record, salesforce_record, cache = AssociationCache.new)
20
+ def build(database_record, salesforce_record, cache = AssociationCache.new(database_record))
21
21
  @cache = cache
22
22
 
23
23
  target = target_mapping(database_record)
@@ -0,0 +1,104 @@
1
+ module Restforce
2
+
3
+ module DB
4
+
5
+ # Restforce::DB::Associator is responsible for determining when one or more
6
+ # associations have been updated to point to a new Salesforce/database
7
+ # record, and propagate the modification to the opposite system when this
8
+ # occurs.
9
+ class Associator
10
+
11
+ # Public: Initialize a new Restforce::DB::Associator.
12
+ #
13
+ # mapping - A Restforce::DB::Mapping instance.
14
+ # runner - A Restforce::DB::Runner instance.
15
+ def initialize(mapping, runner = Runner.new)
16
+ @mapping = mapping
17
+ @runner = runner
18
+ end
19
+
20
+ # Public: Run the re-association process, pulling in records from
21
+ # Salesforce and the database to determine the most recently attached
22
+ # association, then propagating the change between systems.
23
+ #
24
+ # Returns nothing.
25
+ def run
26
+ return if belongs_to_associations.empty?
27
+
28
+ @runner.run(@mapping) do |run|
29
+ run.salesforce_instances.each { |instance| verify_associations(instance) }
30
+ run.database_instances.each { |instance| verify_associations(instance) }
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ # Internal: Ensure integrity between the lookup columns in Salesforce and
37
+ # the synchronized records in the database.
38
+ #
39
+ # instance - A Restforce::DB::Instances::Base.
40
+ #
41
+ # Returns nothing.
42
+ def verify_associations(instance)
43
+ database_instance, salesforce_instance =
44
+ case instance
45
+ when Restforce::DB::Instances::Salesforce
46
+ [@mapping.database_record_type.find(instance.id), instance]
47
+ when Restforce::DB::Instances::ActiveRecord
48
+ [instance, @mapping.salesforce_record_type.find(instance.id)]
49
+ end
50
+
51
+ return unless database_instance && salesforce_instance
52
+ sync_associations(database_instance, salesforce_instance)
53
+ end
54
+
55
+ # Internal: Given a database record and corresponding Salesforce data,
56
+ # synchronize the record associations in whichever system has the least
57
+ # recent data.
58
+ #
59
+ # database_instance - A Restforce::DB::Instances::ActiveRecord.
60
+ # salesforce_instance - A Restforce::DB::Instances::Salesforce.
61
+ #
62
+ # Returns nothing.
63
+ def sync_associations(database_instance, salesforce_instance)
64
+ ids = belongs_to_association_ids(database_instance)
65
+ return if ids.all? { |field, id| salesforce_instance.record[field] == id }
66
+
67
+ if database_instance.last_update > salesforce_instance.last_update
68
+ salesforce_instance.update!(ids)
69
+ else
70
+ database_record = database_instance.record
71
+ belongs_to_associations.each do |association|
72
+ association.build(database_record, salesforce_instance.record)
73
+ end
74
+ database_record.save!
75
+ end
76
+ end
77
+
78
+ # Internal: Get a Hash of associated lookup IDs for the passed database
79
+ # record.
80
+ #
81
+ # database_instance - A Restforce::DB::Instances::ActiveRecord.
82
+ #
83
+ # Returns a Hash.
84
+ def belongs_to_association_ids(database_instance)
85
+ belongs_to_associations.inject({}) do |ids, association|
86
+ ids.merge(association.lookups(database_instance.record))
87
+ end
88
+ end
89
+
90
+ # Internal: Get a list of the BelongsTo associations defined for the
91
+ # target mapping.
92
+ #
93
+ # Returns an Array of Restforce::DB::Association::BelongsTo objects.
94
+ def belongs_to_associations
95
+ @belongs_to_associations ||= @mapping.associations.select do |association|
96
+ association.is_a?(Restforce::DB::Associations::BelongsTo)
97
+ end
98
+ end
99
+
100
+ end
101
+
102
+ end
103
+
104
+ end
@@ -20,7 +20,7 @@ module Restforce
20
20
  #
21
21
  # Returns nothing.
22
22
  def run
23
- return if @strategy.passive?
23
+ return if @mapping.conditions.empty? || @strategy.passive?
24
24
  @mapping.database_record_type.destroy_all(invalid_salesforce_ids)
25
25
  end
26
26
 
@@ -40,15 +40,7 @@ module Restforce
40
40
  #
41
41
  # Returns an Array of IDs.
42
42
  def all_salesforce_ids
43
- all_ids = []
44
-
45
- @mapping.unscoped do |map|
46
- @runner.run(map) do |run|
47
- run.salesforce_records { |record| all_ids << record.id }
48
- end
49
- end
50
-
51
- all_ids
43
+ @mapping.unscoped { valid_salesforce_ids }
52
44
  end
53
45
 
54
46
  # Internal: Get the IDs of the recently-modified Salesforce records which
@@ -56,13 +48,9 @@ module Restforce
56
48
  #
57
49
  # Returns an Array of IDs.
58
50
  def valid_salesforce_ids
59
- valid_ids = []
60
-
61
51
  @runner.run(@mapping) do |run|
62
- run.salesforce_records { |record| valid_ids << record.id }
52
+ run.salesforce_instances.map(&:id)
63
53
  end
64
-
65
- valid_ids
66
54
  end
67
55
 
68
56
  end
@@ -8,8 +8,6 @@ module Restforce
8
8
  # locate recently-updated records and fetch their attributes.
9
9
  class Collector
10
10
 
11
- attr_reader :last_run
12
-
13
11
  # Public: Initialize a new Restforce::DB::Collector.
14
12
  #
15
13
  # mapping - A Restforce::DB::Mapping instance.
@@ -30,8 +28,8 @@ module Restforce
30
28
  @accumulated_changes = accumulator || accumulated_changes
31
29
 
32
30
  @runner.run(@mapping) do |run|
33
- run.salesforce_records { |record| accumulate(record) }
34
- run.database_records { |record| accumulate(record) }
31
+ run.salesforce_instances.each { |instance| accumulate(instance) }
32
+ run.database_instances.each { |instance| accumulate(instance) }
35
33
  end
36
34
 
37
35
  accumulated_changes
@@ -25,8 +25,8 @@ module Restforce
25
25
  return if @strategy.passive?
26
26
 
27
27
  @runner.run(@mapping) do |run|
28
- run.salesforce_records { |record| create_in_database(record) }
29
- run.database_records { |record| create_in_salesforce(record) }
28
+ run.salesforce_instances.each { |instance| create_in_database(instance) }
29
+ run.database_instances.each { |instance| create_in_salesforce(instance) }
30
30
  end
31
31
  end
32
32
 
@@ -36,24 +36,24 @@ module Restforce
36
36
  # passed Salesforce record. Does nothing if the Salesforce record has
37
37
  # already been synchronized into the system at least once.
38
38
  #
39
- # record - A Restforce::DB::Instances::Salesforce.
39
+ # instance - A Restforce::DB::Instances::Salesforce.
40
40
  #
41
41
  # Returns nothing.
42
- def create_in_database(record)
43
- return unless @strategy.build?(record)
44
- @mapping.database_record_type.create!(record)
42
+ def create_in_database(instance)
43
+ return unless @strategy.build?(instance)
44
+ @mapping.database_record_type.create!(instance)
45
45
  end
46
46
 
47
47
  # Internal: Attempt to create a partner record in Salesforce for the
48
48
  # passed database record. Does nothing if the database record already has
49
49
  # an associated Salesforce record.
50
50
  #
51
- # record - A Restforce::DB::Instances::ActiveRecord.
51
+ # instance - A Restforce::DB::Instances::ActiveRecord.
52
52
  #
53
53
  # Returns nothing.
54
- def create_in_salesforce(record)
55
- return if record.synced?
56
- @mapping.salesforce_record_type.create!(record)
54
+ def create_in_salesforce(instance)
55
+ return if instance.synced?
56
+ @mapping.salesforce_record_type.create!(instance)
57
57
  end
58
58
 
59
59
  end
@@ -44,8 +44,8 @@ module Restforce
44
44
  Instances::ActiveRecord.new(@record_type, record, @mapping)
45
45
  end
46
46
 
47
- # Public: Iterate through all recently-updated ActiveRecord records of
48
- # this type.
47
+ # Public: Get a collection of all modified ActiveRecord records of this
48
+ # type which match the passed criteria.
49
49
  #
50
50
  # options - A Hash of options which should be applied to the set of
51
51
  # fetched records. Allowed options are:
@@ -54,15 +54,14 @@ module Restforce
54
54
  # :after - A Time object defining the least recent update
55
55
  # timestamp for which records should be returned.
56
56
  #
57
- # Yields a series of Restforce::DB::Instances::ActiveRecord instances.
58
- # Returns nothing.
59
- def each(options = {})
57
+ # Returns an Array of Restforce::DB::Instances::ActiveRecord instances.
58
+ def all(options = {})
60
59
  scope = @record_type.where("updated_at > synchronized_at OR synchronized_at IS NULL")
61
60
  scope = scope.where("updated_at < ?", options[:before]) if options[:before]
62
61
  scope = scope.where("updated_at >= ?", options[:after]) if options[:after]
63
62
 
64
- scope.find_each do |record|
65
- yield Instances::ActiveRecord.new(@record_type, record, @mapping)
63
+ scope.map do |record|
64
+ Instances::ActiveRecord.new(@record_type, record, @mapping)
66
65
  end
67
66
  end
68
67
 
@@ -8,8 +8,6 @@ module Restforce
8
8
  # models defined in the Restforce::DB::RecordTypes namespace.
9
9
  class Base
10
10
 
11
- include Enumerable
12
-
13
11
  # Public: Initialize a new Restforce::DB::RecordTypes::Base.
14
12
  #
15
13
  # record_type - The name or class of the system record type.
@@ -47,7 +47,8 @@ module Restforce
47
47
  first("Id = '#{id}'")
48
48
  end
49
49
 
50
- # Public: Iterate through all Salesforce records of this type.
50
+ # Public: Get a collection of all Salesforce records of this type which
51
+ # match the passed criteria.
51
52
  #
52
53
  # options - A Hash of options which should be applied to the set of
53
54
  # fetched records. Allowed options are:
@@ -60,17 +61,16 @@ module Restforce
60
61
  # :conditions - An Array of conditions to append to the lookup
61
62
  # query.
62
63
  #
63
- # Yields a series of Restforce::DB::Instances::Salesforce instances.
64
- # Returns nothing.
65
- def each(options = {})
64
+ # Returns an Array of Restforce::DB::Instances::Salesforce instances.
65
+ def all(options = {})
66
66
  constraints = [
67
67
  ("SystemModstamp < #{options[:before].utc.iso8601}" if options[:before]),
68
68
  ("SystemModstamp >= #{options[:after].utc.iso8601}" if options[:after]),
69
69
  *options[:conditions],
70
70
  ]
71
71
 
72
- DB.client.query(query(*constraints)).each do |record|
73
- yield Instances::Salesforce.new(@record_type, record, @mapping)
72
+ DB.client.query(query(*constraints)).map do |record|
73
+ Instances::Salesforce.new(@record_type, record, @mapping)
74
74
  end
75
75
  end
76
76
 
@@ -19,6 +19,7 @@ module Restforce
19
19
  def initialize(delay = 0, last_run_time = DB.last_run)
20
20
  @delay = delay
21
21
  @last_run = last_run_time
22
+ @cache = RunnerCache.new
22
23
  end
23
24
 
24
25
  # Public: Indicate that a new phase of the run is beginning. Updates the
@@ -26,6 +27,7 @@ module Restforce
26
27
  #
27
28
  # Returns the new run Time.
28
29
  def tick!
30
+ @cache.reset
29
31
  run_time = Time.now
30
32
 
31
33
  @before = run_time - @delay
@@ -50,19 +52,17 @@ module Restforce
50
52
  # Public: Iterate through recently-updated records for the Salesforce
51
53
  # record type defined by the current mapping.
52
54
  #
53
- # Yields a series of Restforce::DB::Instances::Salesforce objects.
54
- # Returns nothing.
55
- def salesforce_records
56
- @mapping.salesforce_record_type.each(options) { |record| yield record }
55
+ # Returns an Enumerator yielding Restforce::DB::Instances::Salesforces.
56
+ def salesforce_instances
57
+ @cache.collection(@mapping, :salesforce_record_type, options)
57
58
  end
58
59
 
59
60
  # Public: Iterate through recently-updated records for the database model
60
61
  # record type defined by the current mapping.
61
62
  #
62
- # Yields a series of Restforce::DB::Instances::ActiveRecord objects.
63
- # Returns nothing.
64
- def database_records
65
- @mapping.database_record_type.each(options) { |record| yield record }
63
+ # Returns an Enumerator yielding Restforce::DB::Instances::ActiveRecords.
64
+ def database_instances
65
+ @cache.collection(@mapping, :database_record_type, options)
66
66
  end
67
67
 
68
68
  private
@@ -0,0 +1,93 @@
1
+ module Restforce
2
+
3
+ module DB
4
+
5
+ # Restforce::DB::RunnerCache serves as a means of caching the collections of
6
+ # recently-updated database and Salesforce instances for passed mappings.
7
+ # The general goal is to avoid making repetitive Salesforce API calls or
8
+ # database queries, and ensure a consistent list of objects during a
9
+ # synchronization run.
10
+ class RunnerCache
11
+
12
+ # Public: Initialize a new Restforce::DB::RunnerCache.
13
+ def initialize
14
+ reset
15
+ end
16
+
17
+ # Public: Iterate through the recently-updated instances of the specified
18
+ # type for the passed mapping. Memoizes the records if the collection has
19
+ # not previously been cached.
20
+ #
21
+ # mapping - A Restforce::DB::Mapping.
22
+ # record_type - A Symbol naming a mapping record type. Valid values are
23
+ # :salesforce_record_type or :database_record_type.
24
+ # options - A Hash of options to pass to `all` (optional).
25
+ #
26
+ # Returns an Array of Restforce::DB::Instances::Base.
27
+ def collection(mapping, record_type, options = {})
28
+ return cached_value(mapping, record_type) if cached?(mapping, record_type)
29
+ cache(mapping, record_type, mapping.send(record_type).all(options))
30
+ end
31
+
32
+ # Public: Reset the cache. Should be invoked between runs to ensure that
33
+ # new options are respected.
34
+ #
35
+ # Returns nothing.
36
+ def reset
37
+ @cache = Hash.new { |h, k| h[k] = {} }
38
+ end
39
+
40
+ private
41
+
42
+ # Internal: Store the supplied value in the cache for the passed mapping
43
+ # and record type.
44
+ #
45
+ # mapping - A Restforce::DB::Mapping.
46
+ # record_type - A Symbol naming a mapping record type. Valid values are
47
+ # :salesforce_record_type or :database_record_type.
48
+ #
49
+ # Returns the cached value.
50
+ def cache(mapping, record_type, value)
51
+ @cache[record_type][key_for(mapping)] = value
52
+ end
53
+
54
+ # Internal: Get the cached collection for the passed mapping and record
55
+ # type.
56
+ #
57
+ # mapping - A Restforce::DB::Mapping.
58
+ # record_type - A Symbol naming a mapping record type. Valid values are
59
+ # :salesforce_record_type or :database_record_type.
60
+ #
61
+ # Returns nil or an Array.
62
+ def cached_value(mapping, record_type)
63
+ @cache[record_type][key_for(mapping)]
64
+ end
65
+
66
+ # Internal: Have we cached a collection for the passed mapping and record
67
+ # type?
68
+ #
69
+ # mapping - A Restforce::DB::Mapping.
70
+ # record_type - A Symbol naming a mapping record type. Valid values are
71
+ # :salesforce_record_type or :database_record_type.
72
+ #
73
+ # Returns a Boolean.
74
+ def cached?(mapping, record_type)
75
+ !cached_value(mapping, record_type).nil?
76
+ end
77
+
78
+ # Internal: Get a unique key with enough information to look up the passed
79
+ # mapping in the cache. Scopes the mapping by its current list of
80
+ # conditions.
81
+ #
82
+ # mapping - A Restforce::DB::Mapping.
83
+ #
84
+ # Returns an Object.
85
+ def key_for(mapping)
86
+ [mapping, mapping.conditions]
87
+ end
88
+
89
+ end
90
+
91
+ end
92
+
93
+ end
@@ -3,7 +3,7 @@ module Restforce
3
3
  # :nodoc:
4
4
  module DB
5
5
 
6
- VERSION = "1.2.9"
6
+ VERSION = "1.2.10"
7
7
 
8
8
  end
9
9
 
@@ -37,15 +37,7 @@ module Restforce
37
37
  #
38
38
  # Returns nothing.
39
39
  def start
40
- trap("TERM") do
41
- Thread.new { log "Exiting..." }
42
- stop
43
- end
44
-
45
- trap("INT") do
46
- Thread.new { log "Exiting..." }
47
- stop
48
- end
40
+ trap_signals
49
41
 
50
42
  loop do
51
43
  runtime = Benchmark.realtime { perform }
@@ -60,11 +52,22 @@ module Restforce
60
52
  #
61
53
  # Returns nothing.
62
54
  def stop
55
+ Thread.new { log "Exiting..." }
63
56
  @exit = true
64
57
  end
65
58
 
66
59
  private
67
60
 
61
+ # Internal: Configure the main loop to trap specific signals, triggering
62
+ # an exit once the loop completes.
63
+ #
64
+ # Return nothing.
65
+ def trap_signals
66
+ %w(TERM INT).each do |signal|
67
+ trap(signal) { stop }
68
+ end
69
+ end
70
+
68
71
  # Internal: Perform the synchronization loop, recording the time that the
69
72
  # run is performed so that future runs can pick up where the last run
70
73
  # left off.
@@ -79,6 +82,7 @@ module Restforce
79
82
  task("PROPAGATING RECORDS", mapping) { propagate mapping }
80
83
  task("CLEANING RECORDS", mapping) { clean mapping }
81
84
  task("COLLECTING CHANGES", mapping) { collect mapping }
85
+ task("UPDATING ASSOCIATIONS", mapping) { associate mapping }
82
86
  end
83
87
 
84
88
  # NOTE: We can only perform the synchronization after all record
@@ -151,6 +155,16 @@ module Restforce
151
155
  Collector.new(mapping, runner).run(@changes)
152
156
  end
153
157
 
158
+ # Internal: Update the associated records and Salesforce lookups for
159
+ # records belonging to the passed mapping.
160
+ #
161
+ # mapping - A Restforce::DB::Mapping.
162
+ #
163
+ # Returns nothing.
164
+ def associate(mapping)
165
+ Associator.new(mapping, runner).run
166
+ end
167
+
154
168
  # Internal: Apply the aggregated changes to the objects in both systems,
155
169
  # according to the defined mappings.
156
170
  #
data/lib/restforce/db.rb CHANGED
@@ -28,9 +28,11 @@ require "restforce/db/strategies/always"
28
28
  require "restforce/db/strategies/associated"
29
29
  require "restforce/db/strategies/passive"
30
30
 
31
+ require "restforce/db/runner_cache"
31
32
  require "restforce/db/runner"
32
33
 
33
34
  require "restforce/db/accumulator"
35
+ require "restforce/db/associator"
34
36
  require "restforce/db/attribute_map"
35
37
  require "restforce/db/cleaner"
36
38
  require "restforce/db/collector"
data/restforce-db.gemspec CHANGED
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
21
21
 
22
22
  spec.files = `git ls-files`.split($RS)
23
23
  spec.bindir = "exe"
24
- spec.executables = spec.files.grep(/^exe\//) { |f| File.basename(f) }
24
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
25
  spec.require_paths = ["lib"]
26
26
 
27
27
  spec.add_dependency "activerecord"
@@ -30,11 +30,11 @@ Gem::Specification.new do |spec|
30
30
 
31
31
  spec.add_development_dependency "bundler", "~> 1.8"
32
32
  spec.add_development_dependency "database_cleaner"
33
- spec.add_development_dependency "minitest"
33
+ spec.add_development_dependency "minitest", "5.5.1"
34
34
  spec.add_development_dependency "minitest-spec-expect"
35
35
  spec.add_development_dependency "minitest-vcr"
36
36
  spec.add_development_dependency "rake", "~> 10.0"
37
- spec.add_development_dependency "rubocop"
37
+ spec.add_development_dependency "rubocop", ">= 0.30.0"
38
38
  spec.add_development_dependency "sqlite3"
39
39
  spec.add_development_dependency "webmock"
40
40
  end