restforce-db 3.0.0 → 3.1.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/restforce/db.rb +2 -0
- data/lib/restforce/db/associator.rb +2 -11
- data/lib/restforce/db/attacher.rb +91 -0
- data/lib/restforce/db/cleaner.rb +3 -13
- data/lib/restforce/db/collector.rb +1 -10
- data/lib/restforce/db/field_processor.rb +53 -17
- data/lib/restforce/db/initializer.rb +6 -14
- data/lib/restforce/db/instances/salesforce.rb +2 -1
- data/lib/restforce/db/record_types/salesforce.rb +47 -20
- data/lib/restforce/db/synchronizer.rb +1 -9
- data/lib/restforce/db/task.rb +32 -0
- data/lib/restforce/db/version.rb +1 -1
- data/lib/restforce/db/worker.rb +21 -59
- data/test/cassettes/Restforce_DB/accessing_Salesforce/uses_the_configured_credentials.yml +6 -6
- data/test/cassettes/Restforce_DB_Associations_BelongsTo/with_an_inverse_mapping/_build/returns_an_associated_record_populated_with_the_Salesforce_attributes.yml +203 -52
- data/test/cassettes/Restforce_DB_Associations_BelongsTo/with_an_inverse_mapping/_build/when_no_salesforce_record_is_found_for_the_association/proceeds_without_constructing_any_records.yml +185 -34
- data/test/cassettes/Restforce_DB_Associations_BelongsTo/with_an_inverse_mapping/_build/when_the_associated_record_has_already_been_persisted/assigns_the_existing_record.yml +203 -52
- data/test/cassettes/Restforce_DB_Associations_BelongsTo/with_an_inverse_mapping/_build/when_the_associated_record_has_been_cached/uses_the_cached_record.yml +203 -52
- data/test/cassettes/Restforce_DB_Associations_BelongsTo/with_an_inverse_mapping/_build/when_the_association_is_non-building/proceeds_without_constructing_any_records.yml +97 -44
- data/test/cassettes/Restforce_DB_Associations_BelongsTo/with_an_inverse_mapping/_build/with_an_unrelated_association_mapping/proceeds_without_raising_an_error.yml +203 -52
- data/test/cassettes/Restforce_DB_Associations_BelongsTo/with_an_inverse_mapping/_lookups/returns_a_hash_of_the_associated_records_lookup_IDs.yml +36 -36
- data/test/cassettes/Restforce_DB_Associations_BelongsTo/with_an_inverse_mapping/_lookups/when_there_is_currently_no_associated_record/and_the_underlying_association_is_one-to-many/still_returns_a_nil_lookup_value_in_the_hash.yml +20 -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 +20 -20
- data/test/cassettes/Restforce_DB_Associations_BelongsTo/with_an_inverse_mapping/_synced_for_/when_a_matching_associated_record_has_been_synchronized/returns_true.yml +105 -52
- data/test/cassettes/Restforce_DB_Associations_BelongsTo/with_an_inverse_mapping/_synced_for_/when_no_matching_associated_record_has_been_synchronized/returns_false.yml +105 -52
- data/test/cassettes/Restforce_DB_Associations_HasMany/with_an_inverse_mapping/_build/builds_a_number_of_associated_records_from_the_data_in_Salesforce.yml +191 -87
- data/test/cassettes/Restforce_DB_Associations_HasMany/with_an_inverse_mapping/_build/when_no_salesforce_record_is_found_for_the_association/proceeds_without_constructing_any_records.yml +139 -35
- data/test/cassettes/Restforce_DB_Associations_HasMany/with_an_inverse_mapping/_build/when_the_associated_records_have_alrady_been_persisted/constructs_the_association_from_the_existing_records.yml +191 -87
- data/test/cassettes/Restforce_DB_Associations_HasMany/with_an_inverse_mapping/_build/when_the_associated_records_have_been_cached/uses_the_cached_records.yml +191 -87
- data/test/cassettes/Restforce_DB_Associations_HasMany/with_an_inverse_mapping/_build/when_the_association_is_non-building/proceeds_without_constructing_any_records.yml +81 -28
- data/test/cassettes/Restforce_DB_Associations_HasMany/with_an_inverse_mapping/_synced_for_/when_a_matching_associated_record_has_been_synchronized/returns_true.yml +191 -87
- data/test/cassettes/Restforce_DB_Associations_HasMany/with_an_inverse_mapping/_synced_for_/when_no_matching_associated_record_has_been_synchronized/returns_false.yml +139 -35
- data/test/cassettes/Restforce_DB_Associations_HasOne/with_an_inverse_mapping/_build/and_a_nested_association_on_the_associated_mapping/recursively_builds_all_associations.yml +278 -76
- data/test/cassettes/Restforce_DB_Associations_HasOne/with_an_inverse_mapping/_build/returns_an_associated_record_populated_with_the_Salesforce_attributes.yml +203 -52
- data/test/cassettes/Restforce_DB_Associations_HasOne/with_an_inverse_mapping/_build/when_no_salesforce_record_is_found_for_the_association/proceeds_without_constructing_any_records.yml +186 -35
- data/test/cassettes/Restforce_DB_Associations_HasOne/with_an_inverse_mapping/_build/when_the_associated_record_has_already_been_persisted/assigns_the_existing_record.yml +203 -52
- data/test/cassettes/Restforce_DB_Associations_HasOne/with_an_inverse_mapping/_build/when_the_associated_record_has_been_cached/uses_the_cached_record.yml +203 -52
- data/test/cassettes/Restforce_DB_Associations_HasOne/with_an_inverse_mapping/_build/when_the_association_is_non-building/proceeds_without_constructing_any_records.yml +142 -44
- data/test/cassettes/Restforce_DB_Associations_HasOne/with_an_inverse_mapping/_synced_for_/when_a_matching_associated_record_has_been_synchronized/returns_true.yml +203 -52
- data/test/cassettes/Restforce_DB_Associations_HasOne/with_an_inverse_mapping/_synced_for_/when_no_matching_associated_record_has_been_synchronized/returns_false.yml +203 -52
- 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 +124 -124
- 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 +224 -126
- data/test/cassettes/Restforce_DB_Attacher/_run/given_a_Salesforce_record_with_an_upsert_ID/for_a_Passive_strategy/does_nothing.yml +119 -0
- data/test/cassettes/Restforce_DB_Attacher/_run/given_a_Salesforce_record_with_an_upsert_ID/for_an_Always_strategy/links_the_Salesforce_record_to_the_matching_database_record.yml +246 -0
- data/test/cassettes/Restforce_DB_Attacher/_run/given_a_Salesforce_record_with_an_upsert_ID/for_an_Always_strategy/when_no_matching_database_record_can_be_found/wipes_the_SynchronizationID__c_on_the_Salesforce_record.yml +284 -0
- data/test/cassettes/Restforce_DB_Attacher/_run/given_a_Salesforce_record_with_an_upsert_ID/for_an_Always_strategy/when_the_matching_database_record_has_a_salesforce_id/does_not_change_the_current_Salesforce_ID.yml +246 -0
- data/test/cassettes/Restforce_DB_Attacher/_run/given_a_Salesforce_record_with_an_upsert_ID/for_an_Always_strategy/when_the_matching_database_record_has_a_salesforce_id/wipes_the_SynchronizationId__c.yml +284 -0
- data/test/cassettes/{Restforce_DB_Synchronizer/_run/given_a_Salesforce_record_with_an_associated_database_record/updates_the_salesforce_record.yml → Restforce_DB_Attacher/_run/given_a_Salesforce_record_with_an_upsert_ID/for_an_Always_strategy/when_the_upsert_ID_is_for_another_database_model/does_not_wipe_the_SynchronizationId__c.yml} +57 -95
- data/test/cassettes/{Restforce_DB_Synchronizer/_run/given_a_Salesforce_record_with_an_associated_database_record/updates_the_database_record.yml → Restforce_DB_Attacher/_run/given_a_Salesforce_record_with_an_upsert_ID/for_an_Always_strategy/wipes_the_SynchronizationId__c.yml} +77 -116
- data/test/cassettes/Restforce_DB_Cleaner/_run/given_a_synchronized_Salesforce_record/when_the_mapping_has_no_conditions/does_not_drop_the_synchronized_database_record.yml +20 -20
- data/test/cassettes/Restforce_DB_Cleaner/_run/given_a_synchronized_Salesforce_record/when_the_record_does_not_meet_the_mapping_conditions/but_meets_conditions_for_a_parallel_mapping/does_not_drop_the_synchronized_database_record.yml +98 -45
- data/test/cassettes/Restforce_DB_Cleaner/_run/given_a_synchronized_Salesforce_record/when_the_record_does_not_meet_the_mapping_conditions/drops_the_synchronized_database_record.yml +90 -37
- data/test/cassettes/Restforce_DB_Cleaner/_run/given_a_synchronized_Salesforce_record/when_the_record_has_been_deleted_in_Salesforce/drops_the_synchronized_database_record.yml +20 -20
- data/test/cassettes/Restforce_DB_Cleaner/_run/given_a_synchronized_Salesforce_record/when_the_record_meets_the_mapping_conditions/does_not_drop_the_synchronized_database_record.yml +89 -36
- data/test/cassettes/Restforce_DB_Collector/_run/given_a_Salesforce_record_with_an_associated_database_record/returns_the_attributes_from_both_records.yml +103 -50
- data/test/cassettes/Restforce_DB_Collector/_run/given_an_existing_Salesforce_record/which_has_been_synchronized/returns_the_attributes_from_the_Salesforce_record.yml +102 -73
- data/test/cassettes/Restforce_DB_Collector/_run/given_an_existing_Salesforce_record/which_has_not_been_synchronized/does_not_store_any_attributes.yml +81 -40
- data/test/cassettes/Restforce_DB_Collector/_run/given_an_existing_database_record/returns_the_attributes_from_the_database_record.yml +66 -13
- data/test/cassettes/Restforce_DB_Collector/_run/when_the_record_has_not_been_updated_outside_of_the_system/does_not_collect_any_changes.yml +82 -29
- data/test/cassettes/Restforce_DB_Initializer/_run/given_an_existing_Salesforce_record/for_a_Passive_strategy/does_not_create_a_database_record.yml +20 -20
- data/test/cassettes/Restforce_DB_Initializer/_run/given_an_existing_Salesforce_record/for_an_Always_strategy/creates_a_matching_database_record.yml +81 -28
- data/test/cassettes/Restforce_DB_Initializer/_run/given_an_existing_database_record/for_an_Always_strategy/populates_Salesforce_with_the_new_record.yml +103 -67
- data/test/cassettes/Restforce_DB_Instances_Salesforce/_synced_/when_a_matching_database_record_exists/returns_true.yml +81 -28
- data/test/cassettes/Restforce_DB_Instances_Salesforce/_synced_/when_no_matching_database_record_exists/returns_false.yml +81 -28
- data/test/cassettes/Restforce_DB_Instances_Salesforce/_update_/updates_the_local_record_with_the_passed_attributes.yml +59 -59
- data/test/cassettes/Restforce_DB_Instances_Salesforce/_update_/updates_the_record_in_Salesforce_with_the_passed_attributes.yml +67 -67
- data/test/cassettes/Restforce_DB_Instances_Salesforce/_updated_internally_/when_another_user_made_the_last_change/returns_false.yml +18 -18
- data/test/cassettes/Restforce_DB_Instances_Salesforce/_updated_internally_/when_our_client_made_the_last_change/returns_true.yml +101 -48
- data/test/cassettes/Restforce_DB_Model/given_a_database_model_which_includes_the_module/_force_sync_/given_a_previously-synchronized_record_for_a_mapped_model/force-updates_both_synchronized_records.yml +67 -67
- data/test/cassettes/Restforce_DB_Model/given_a_database_model_which_includes_the_module/_force_sync_/given_an_unsynchronized_record_for_a_mapped_model/creates_a_matching_record_in_Salesforce.yml +78 -42
- data/test/cassettes/Restforce_DB_RecordTypes_Salesforce/_all/returns_a_list_of_the_existing_records_in_Salesforce.yml +82 -29
- data/test/cassettes/Restforce_DB_RecordTypes_Salesforce/_create_/creates_a_record_in_Salesforce_from_the_passed_database_record_s_attributes.yml +70 -34
- data/test/cassettes/Restforce_DB_RecordTypes_Salesforce/_create_/updates_the_database_record_with_the_Salesforce_record_s_ID.yml +70 -34
- data/test/cassettes/Restforce_DB_RecordTypes_Salesforce/_create_/when_a_Salesforce_record_already_exists_for_the_database_instance/uses_the_existing_record.yml +76 -40
- data/test/cassettes/Restforce_DB_RecordTypes_Salesforce/_create_/wipes_the_temporary_SynchronizationId__c_value_used_for_upsert.yml +247 -0
- data/test/cassettes/Restforce_DB_RecordTypes_Salesforce/_find/finds_existing_records_in_Salesforce.yml +81 -28
- data/test/cassettes/Restforce_DB_RecordTypes_Salesforce/_find/given_a_set_of_mapping_conditions/when_a_record_does_not_meet_the_conditions/does_not_find_the_record.yml +80 -27
- data/test/cassettes/Restforce_DB_RecordTypes_Salesforce/_find/given_a_set_of_mapping_conditions/when_a_record_meets_the_conditions/finds_the_record.yml +81 -28
- data/test/cassettes/Restforce_DB_RecordTypes_Salesforce/_find/returns_nil_when_no_matching_record_exists.yml +65 -12
- data/test/cassettes/Restforce_DB_Strategies_Always/_build_/given_a_Salesforce_record/wants_to_build_a_new_matching_record.yml +81 -28
- data/test/cassettes/Restforce_DB_Strategies_Always/_build_/given_a_Salesforce_record/with_a_corresponding_database_record/does_not_want_to_build_a_new_record.yml +81 -28
- data/test/cassettes/Restforce_DB_Strategies_Associated/_build_/given_an_inverse_mapping/with_a_synchronized_association_record/wants_to_build_a_new_record.yml +103 -52
- data/test/cassettes/Restforce_DB_Strategies_Associated/_build_/given_an_inverse_mapping/with_an_existing_database_record/does_not_want_to_build_a_new_record.yml +95 -44
- data/test/cassettes/Restforce_DB_Strategies_Associated/_build_/given_an_inverse_mapping/with_no_synchronized_association_record/does_not_want_to_build_a_new_record.yml +103 -52
- data/test/cassettes/Restforce_DB_Synchronizer/_run/given_a_Salesforce_record_with_an_associated_database_record/when_the_change_timestamp_is_stale/does_not_update_the_database_record.yml +60 -96
- data/test/cassettes/Restforce_DB_Synchronizer/_run/given_a_Salesforce_record_with_an_associated_database_record/when_the_change_timestamp_is_stale/does_not_update_the_salesforce_record.yml +97 -44
- data/test/cassettes/Restforce_DB_Synchronizer/_run/given_a_Salesforce_record_with_an_associated_database_record/when_the_changes_are_current/updates_the_database_record.yml +77 -77
- data/test/cassettes/Restforce_DB_Synchronizer/_run/given_a_Salesforce_record_with_an_associated_database_record/when_the_changes_are_current/updates_the_salesforce_record.yml +85 -85
- data/test/cassettes/Restforce_DB_Synchronizer/_run/given_a_Salesforce_record_with_no_associated_database_record/does_nothing_for_this_specific_mapping.yml +89 -36
- data/test/cassettes/Restforce_DB_Worker/a_race_condition_during_synchronization/does_not_change_the_user-entered_name_on_the_database_record.yml +153 -116
- data/test/cassettes/Restforce_DB_Worker/a_race_condition_during_synchronization/overrides_the_stale-but-more-recent_name_on_the_Salesforce.yml +161 -124
- data/test/lib/restforce/db/attacher_test.rb +93 -0
- data/test/lib/restforce/db/field_processor_test.rb +62 -23
- data/test/lib/restforce/db/record_types/salesforce_test.rb +5 -0
- data/test/lib/restforce/db/worker_test.rb +4 -3
- metadata +13 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 008721b2ee67237a7bbcf0d8c8cdc9353fcbceb8
|
4
|
+
data.tar.gz: 25ddc50b58f134522817748ddbb36ba32dc57fa2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 97eb6aea0ac0e2fd2d5a6e8b5f41b3f7b7600c5a0da646519dea32e957e556081edabb72e1eef7639ed7df7b31e747985d7c87a9425bdb383520fb081dae1f9d
|
7
|
+
data.tar.gz: ba0c3c6772e0d7b60b5403530008b492997b0cdc98b2fe53fb0dcd8fe0b248d6e8439c6dda2689f2b42c4c988e2296e37bd06107db331e643349a380875b0107
|
data/lib/restforce/db.rb
CHANGED
@@ -35,7 +35,9 @@ require "restforce/db/record_cache"
|
|
35
35
|
require "restforce/db/timestamp_cache"
|
36
36
|
require "restforce/db/runner"
|
37
37
|
|
38
|
+
require "restforce/db/task"
|
38
39
|
require "restforce/db/accumulator"
|
40
|
+
require "restforce/db/attacher"
|
39
41
|
require "restforce/db/adapter"
|
40
42
|
require "restforce/db/associator"
|
41
43
|
require "restforce/db/attribute_map"
|
@@ -6,23 +6,14 @@ module Restforce
|
|
6
6
|
# associations have been updated to point to a new Salesforce/database
|
7
7
|
# record, and propagate the modification to the opposite system when this
|
8
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
|
9
|
+
class Associator < Task
|
19
10
|
|
20
11
|
# Public: Run the re-association process, pulling in records from
|
21
12
|
# Salesforce and the database to determine the most recently attached
|
22
13
|
# association, then propagating the change between systems.
|
23
14
|
#
|
24
15
|
# Returns nothing.
|
25
|
-
def run
|
16
|
+
def run(*_)
|
26
17
|
return if belongs_to_associations.empty?
|
27
18
|
|
28
19
|
@runner.run(@mapping) do |run|
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
module DB
|
4
|
+
|
5
|
+
# Restforce::DB::Attacher is responsible for cleaning up any orphaned
|
6
|
+
# upserted Salesforce records with the database records that they were
|
7
|
+
# originally supposed to be attached to. This allows us to successfully
|
8
|
+
# handle cases where a request timeout or partial failure would otherwise
|
9
|
+
# leave an upserted Salesforce record stranded without a related database
|
10
|
+
# record.
|
11
|
+
class Attacher < Task
|
12
|
+
|
13
|
+
# Public: Run the re-attachment process for any unsynchronized Salesforce
|
14
|
+
# records which have an external upsert ID.
|
15
|
+
#
|
16
|
+
# Returns nothing.
|
17
|
+
def run(*_)
|
18
|
+
return if @mapping.strategy.passive?
|
19
|
+
|
20
|
+
@runner.run(@mapping) do |run|
|
21
|
+
run.salesforce_instances.each { |instance| attach(instance) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
# Internal: Attach the passed Salesforce instance to a related database
|
28
|
+
# record.
|
29
|
+
#
|
30
|
+
# instance - A Restforce::DB::Instances::Salesforce.
|
31
|
+
#
|
32
|
+
# Returns nothing.
|
33
|
+
def attach(instance)
|
34
|
+
synchronization_id = instance.record.SynchronizationId__c
|
35
|
+
return unless synchronization_id
|
36
|
+
|
37
|
+
database_model, record_id = parsed_uuid(synchronization_id)
|
38
|
+
return unless valid_model?(database_model)
|
39
|
+
|
40
|
+
# If the instance is already synchronized, then we just want to wipe the
|
41
|
+
# Synchronization ID and proceed to the next instance.
|
42
|
+
if instance.synced?
|
43
|
+
instance.update!("SynchronizationId__c" => nil)
|
44
|
+
return
|
45
|
+
end
|
46
|
+
|
47
|
+
record = @mapping.database_model.find_by(
|
48
|
+
id: record_id,
|
49
|
+
@mapping.lookup_column => nil,
|
50
|
+
)
|
51
|
+
|
52
|
+
if record
|
53
|
+
attach_to = Instances::ActiveRecord.new(@mapping.database_model, record, @mapping)
|
54
|
+
attach_to.update!(@mapping.lookup_column => instance.id)
|
55
|
+
end
|
56
|
+
|
57
|
+
instance.update!("SynchronizationId__c" => nil)
|
58
|
+
rescue Faraday::Error::ClientError => e
|
59
|
+
DB.logger.error(SynchronizationError.new(e, instance))
|
60
|
+
end
|
61
|
+
|
62
|
+
# Internal: Does the passed database model correspond to the model defined
|
63
|
+
# on the mapping?
|
64
|
+
#
|
65
|
+
# database_model - A String name of an ActiveRecord::Base subclass.
|
66
|
+
#
|
67
|
+
# Returns a Boolean.
|
68
|
+
def valid_model?(database_model)
|
69
|
+
database_model == @mapping.database_model.to_s
|
70
|
+
end
|
71
|
+
|
72
|
+
# Internal: Parse a UUID into a database model and corresponding record
|
73
|
+
# identifier.
|
74
|
+
#
|
75
|
+
# uuid - A String UUID, in the format "<Model>::<Id>"
|
76
|
+
#
|
77
|
+
# Returns an Array of two Strings.
|
78
|
+
def parsed_uuid(uuid)
|
79
|
+
components = uuid.split("::")
|
80
|
+
|
81
|
+
database_model = components[0..-2].join("::")
|
82
|
+
id = components.last
|
83
|
+
|
84
|
+
[database_model, id]
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
data/lib/restforce/db/cleaner.rb
CHANGED
@@ -5,27 +5,17 @@ module Restforce
|
|
5
5
|
# Restforce::DB::Cleaner is responsible for culling the matching database
|
6
6
|
# records when a Salesforce record is no longer available to synchronize
|
7
7
|
# for a specific mapping.
|
8
|
-
class Cleaner
|
8
|
+
class Cleaner < Task
|
9
9
|
|
10
10
|
# Salesforce can take a few minutes to register record deletion. This
|
11
11
|
# buffer gives us a window of time (in seconds) to look back and see
|
12
12
|
# records which may not have been visible in previous runs.
|
13
13
|
DELETION_READ_BUFFER = 3 * 60
|
14
14
|
|
15
|
-
# Public: Initialize a Restforce::DB::Cleaner.
|
16
|
-
#
|
17
|
-
# mapping - A Restforce::DB::Mapping.
|
18
|
-
# runner - A Restforce::DB::Runner.
|
19
|
-
def initialize(mapping, runner = Runner.new)
|
20
|
-
@mapping = mapping
|
21
|
-
@strategy = mapping.strategy
|
22
|
-
@runner = runner
|
23
|
-
end
|
24
|
-
|
25
15
|
# Public: Run the database culling loop for this mapping.
|
26
16
|
#
|
27
17
|
# Returns nothing.
|
28
|
-
def run
|
18
|
+
def run(*_)
|
29
19
|
@mapping.database_record_type.destroy_all(dropped_salesforce_ids)
|
30
20
|
end
|
31
21
|
|
@@ -61,7 +51,7 @@ module Restforce
|
|
61
51
|
#
|
62
52
|
# Returns an Array of IDs.
|
63
53
|
def invalid_salesforce_ids
|
64
|
-
return [] if @mapping.conditions.empty? || @strategy.passive?
|
54
|
+
return [] if @mapping.conditions.empty? || @mapping.strategy.passive?
|
65
55
|
|
66
56
|
all_salesforce_ids - valid_salesforce_ids
|
67
57
|
end
|
@@ -6,16 +6,7 @@ module Restforce
|
|
6
6
|
# recently-updated records for purposes of synchronization. It relies on the
|
7
7
|
# mappings configured in instances of Restforce::DB::RecordTypes::Base to
|
8
8
|
# locate recently-updated records and fetch their attributes.
|
9
|
-
class Collector
|
10
|
-
|
11
|
-
# Public: Initialize a new Restforce::DB::Collector.
|
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
|
9
|
+
class Collector < Task
|
19
10
|
|
20
11
|
# Public: Run the collection process, pulling in records from Salesforce
|
21
12
|
# and the database to determine the lists of attributes to apply to all
|
@@ -6,6 +6,10 @@ module Restforce
|
|
6
6
|
# information for unwriteable fields from being submitted to Salesforce.
|
7
7
|
class FieldProcessor
|
8
8
|
|
9
|
+
# This token indicates that a relationship is being accessed for a
|
10
|
+
# specific field.
|
11
|
+
RELATIONSHIP_MATCHER = /(.+)__r\./.freeze
|
12
|
+
|
9
13
|
# Internal: Get a global cache with which to store/fetch the writable
|
10
14
|
# fields for each Salesforce SObject Type.
|
11
15
|
#
|
@@ -21,52 +25,84 @@ module Restforce
|
|
21
25
|
@field_cache = {}
|
22
26
|
end
|
23
27
|
|
24
|
-
# Public: Get a
|
25
|
-
#
|
28
|
+
# Public: Get a list of valid fields for a specific action from the passed
|
29
|
+
# list of proposed fields. Allows access to related object fields on a
|
30
|
+
# read-only basis.
|
26
31
|
#
|
27
32
|
# sobject_type - A String name of an SObject Type in Salesforce.
|
28
33
|
# attributes - A Hash with keys corresponding to Salesforce field names.
|
29
|
-
#
|
30
|
-
# values are :create and :update.
|
34
|
+
# action - A Symbol reflecting the action to perform. Accepted
|
35
|
+
# values are :read, :create, and :update.
|
31
36
|
#
|
32
37
|
# Returns a Hash.
|
33
|
-
def
|
34
|
-
|
35
|
-
|
36
|
-
|
38
|
+
def available_fields(sobject_type, fields, action = :read)
|
39
|
+
fields.select do |field|
|
40
|
+
known_field = available?(sobject_type, field, action)
|
41
|
+
relationship = action == :read && relationship?(field)
|
42
|
+
|
43
|
+
known_field || relationship
|
37
44
|
end
|
38
45
|
end
|
39
46
|
|
47
|
+
# Public: Get a restricted version of the passed attributes Hash, with
|
48
|
+
# inaccessible fields for the specified action stripped out.
|
49
|
+
#
|
50
|
+
# sobject_type - A String name of an SObject Type in Salesforce.
|
51
|
+
# attributes - A Hash with keys corresponding to Salesforce field names.
|
52
|
+
# action - A Symbol reflecting the action to perform. Accepted
|
53
|
+
# values are :create and :update.
|
54
|
+
#
|
55
|
+
# Returns a Hash.
|
56
|
+
def process(sobject_type, attributes, action)
|
57
|
+
attributes.select { |field, _| available?(sobject_type, field, action) }
|
58
|
+
end
|
59
|
+
|
40
60
|
private
|
41
61
|
|
42
|
-
# Internal: Is the passed attribute
|
62
|
+
# Internal: Is the passed attribute available for the specified action on
|
63
|
+
# the passed SObject Type?
|
43
64
|
#
|
44
65
|
# sobject_type - A String name of an SObject Type in Salesforce.
|
45
66
|
# field - A String Salesforce field API name.
|
46
|
-
#
|
47
|
-
#
|
67
|
+
# action - A Symbol reflecting the requested action. Accepted values
|
68
|
+
# are :read, :create, and :update.
|
48
69
|
#
|
49
70
|
# Returns a Boolean.
|
50
|
-
def
|
51
|
-
permissions =
|
71
|
+
def available?(sobject_type, field, action)
|
72
|
+
permissions = field_metadata(sobject_type)[field]
|
52
73
|
return false unless permissions
|
53
74
|
|
54
|
-
permissions[
|
75
|
+
permissions[action]
|
76
|
+
end
|
77
|
+
|
78
|
+
# Internal: Does the passed field description reference an attribute
|
79
|
+
# through an associated object?
|
80
|
+
#
|
81
|
+
# NOTE: It's not worth the trouble to validate that this relationship
|
82
|
+
# actually exists, or that the requested field exists on the related
|
83
|
+
# model. If a bad lookup is specified, the API will throw an error.
|
84
|
+
#
|
85
|
+
# field - A String Salesforce field API name.
|
86
|
+
#
|
87
|
+
# Rturns a Boolean.
|
88
|
+
def relationship?(field)
|
89
|
+
field =~ RELATIONSHIP_MATCHER
|
55
90
|
end
|
56
91
|
|
57
92
|
# Internal: Get a collection of all fields for the passed Salesforce
|
58
|
-
# SObject Type, with an indication of whether or not they are
|
59
|
-
# both create and update actions.
|
93
|
+
# SObject Type, with an indication of whether or not they are readable and
|
94
|
+
# writable for both create and update actions.
|
60
95
|
#
|
61
96
|
# sobject_type - A String name of an SObject Type in Salesforce.
|
62
97
|
#
|
63
98
|
# Returns a Hash.
|
64
|
-
def
|
99
|
+
def field_metadata(sobject_type)
|
65
100
|
self.class.field_cache[sobject_type] ||= begin
|
66
101
|
fields = Restforce::DB.client.describe(sobject_type).fields
|
67
102
|
|
68
103
|
fields.each_with_object({}) do |field, permissions|
|
69
104
|
permissions[field["name"]] = {
|
105
|
+
read: true,
|
70
106
|
create: field["createable"],
|
71
107
|
update: field["updateable"],
|
72
108
|
}
|
@@ -6,23 +6,13 @@ module Restforce
|
|
6
6
|
# are populated with the same records at the root level. It iterates through
|
7
7
|
# recently added or updated records in each system for a mapping, and
|
8
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
|
-
@strategy = mapping.strategy
|
18
|
-
@runner = runner
|
19
|
-
end
|
9
|
+
class Initializer < Task
|
20
10
|
|
21
11
|
# Public: Run the initialization loop for this mapping.
|
22
12
|
#
|
23
13
|
# Returns nothing.
|
24
|
-
def run
|
25
|
-
return if @strategy.passive?
|
14
|
+
def run(*_)
|
15
|
+
return if @mapping.strategy.passive?
|
26
16
|
|
27
17
|
@runner.run(@mapping) do |run|
|
28
18
|
run.salesforce_instances.each { |instance| create_in_database(instance) }
|
@@ -40,7 +30,8 @@ module Restforce
|
|
40
30
|
#
|
41
31
|
# Returns nothing.
|
42
32
|
def create_in_database(instance)
|
43
|
-
return unless @strategy.build?(instance)
|
33
|
+
return unless @mapping.strategy.build?(instance)
|
34
|
+
|
44
35
|
created = @mapping.database_record_type.create!(instance)
|
45
36
|
@runner.cache_timestamp created
|
46
37
|
rescue ActiveRecord::ActiveRecordError => e
|
@@ -56,6 +47,7 @@ module Restforce
|
|
56
47
|
# Returns nothing.
|
57
48
|
def create_in_salesforce(instance)
|
58
49
|
return if instance.synced?
|
50
|
+
|
59
51
|
created = @mapping.salesforce_record_type.create!(instance)
|
60
52
|
@runner.cache_timestamp created
|
61
53
|
rescue Faraday::Error::ClientError => e
|
@@ -11,6 +11,7 @@ module Restforce
|
|
11
11
|
|
12
12
|
INTERNAL_ATTRIBUTES = %w(
|
13
13
|
Id
|
14
|
+
SynchronizationId__c
|
14
15
|
SystemModstamp
|
15
16
|
LastModifiedById
|
16
17
|
).freeze
|
@@ -29,7 +30,7 @@ module Restforce
|
|
29
30
|
# Returns self.
|
30
31
|
# Raises if the update fails for any reason.
|
31
32
|
def update!(attributes)
|
32
|
-
super FieldProcessor.new.process(@record_type, attributes)
|
33
|
+
super FieldProcessor.new.process(@record_type, attributes, :update)
|
33
34
|
end
|
34
35
|
|
35
36
|
# Public: Get the time of the last update to this record.
|
@@ -9,6 +9,8 @@ module Restforce
|
|
9
9
|
# attribute mappings.
|
10
10
|
class Salesforce < Base
|
11
11
|
|
12
|
+
ALREADY_EXISTS_MESSAGE = /INVALID_FIELD_FOR_INSERT_UPDATE|DUPLICATE_VALUE/.freeze
|
13
|
+
|
12
14
|
# Public: Create an instance of this Salesforce model for the passed
|
13
15
|
# database record.
|
14
16
|
#
|
@@ -17,24 +19,19 @@ module Restforce
|
|
17
19
|
# Returns a Restforce::DB::Instances::Salesforce instance.
|
18
20
|
# Raises on any error from Salesforce.
|
19
21
|
def create!(from_record)
|
20
|
-
|
21
|
-
record_id = DB.client.upsert!(
|
22
|
-
@record_type,
|
23
|
-
"SynchronizationId__c",
|
24
|
-
from_attributes.merge("SynchronizationId__c" => from_record.uuid),
|
25
|
-
)
|
22
|
+
record_id = upsert!(from_record)
|
26
23
|
|
27
24
|
# NOTE: #upsert! returns a String Salesforce ID when a record is
|
28
|
-
# created,
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
25
|
+
# created, and returns `true` when an existing record was found.
|
26
|
+
instance =
|
27
|
+
if record_id.is_a?(String)
|
28
|
+
find(record_id)
|
29
|
+
else
|
30
|
+
first("SynchronizationId__c = '#{from_record.uuid}'")
|
31
|
+
end
|
32
|
+
|
33
|
+
from_record.update!(@mapping.lookup_column => instance.id)
|
34
|
+
instance.update!("SynchronizationId__c" => nil)
|
38
35
|
end
|
39
36
|
|
40
37
|
# Public: Find the first Salesforce record which meets the passed
|
@@ -88,13 +85,43 @@ module Restforce
|
|
88
85
|
|
89
86
|
private
|
90
87
|
|
91
|
-
# Internal: Get a
|
88
|
+
# Internal: Get a list of fields to look up when the record is
|
92
89
|
# fetched from Salesforce. Includes all configured mappings and a
|
93
90
|
# handful of attributes for internal use.
|
94
91
|
#
|
95
|
-
# Returns
|
92
|
+
# Returns an Array of Strings.
|
96
93
|
def lookups
|
97
|
-
|
94
|
+
FieldProcessor.new.available_fields(
|
95
|
+
@record_type,
|
96
|
+
(Instances::Salesforce::INTERNAL_ATTRIBUTES + @mapping.salesforce_fields).uniq,
|
97
|
+
)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Internal: Attempt to create a record in Salesforce from the passed
|
101
|
+
# database instance.
|
102
|
+
#
|
103
|
+
# Returns a String or Boolean.
|
104
|
+
def upsert!(from_record)
|
105
|
+
from_attributes = FieldProcessor.new.process(
|
106
|
+
@record_type,
|
107
|
+
from_record.attributes,
|
108
|
+
:create,
|
109
|
+
)
|
110
|
+
|
111
|
+
DB.client.upsert!(
|
112
|
+
@record_type,
|
113
|
+
"SynchronizationId__c",
|
114
|
+
from_attributes.merge("SynchronizationId__c" => from_record.uuid),
|
115
|
+
)
|
116
|
+
rescue Faraday::Error::ClientError => e
|
117
|
+
# If the error is complaining about attempting to update create-only
|
118
|
+
# fields, we've confirmed that the record already exists, and can
|
119
|
+
# safely resolve our object creation.
|
120
|
+
if e.message =~ ALREADY_EXISTS_MESSAGE
|
121
|
+
true
|
122
|
+
else
|
123
|
+
raise e
|
124
|
+
end
|
98
125
|
end
|
99
126
|
|
100
127
|
# Internal: Has this database record already been linked to a Salesforce
|
@@ -116,7 +143,7 @@ module Restforce
|
|
116
143
|
filters = (conditions + @mapping.conditions).compact.join(" and ")
|
117
144
|
filters = " where #{filters}" unless filters.empty?
|
118
145
|
|
119
|
-
"select #{lookups} from #{@record_type}#{filters}"
|
146
|
+
"select #{lookups.join(', ')} from #{@record_type}#{filters}"
|
120
147
|
end
|
121
148
|
|
122
149
|
end
|