restforce-db 3.0.0 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|