restforce-db 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/file_daemon.rb +42 -0
- data/lib/restforce/db/accumulator.rb +41 -0
- data/lib/restforce/db/attribute_map.rb +132 -0
- data/lib/restforce/db/collector.rb +79 -0
- data/lib/restforce/db/initializer.rb +62 -0
- data/lib/restforce/db/instances/base.rb +1 -14
- data/lib/restforce/db/instances/salesforce.rb +7 -0
- data/lib/restforce/db/mapping.rb +33 -79
- data/lib/restforce/db/record_types/base.rb +0 -30
- data/lib/restforce/db/runner.rb +80 -0
- data/lib/restforce/db/synchronizer.rb +29 -37
- data/lib/restforce/db/version.rb +1 -1
- data/lib/restforce/db/worker.rb +53 -40
- data/lib/restforce/db.rb +6 -0
- 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
- data/test/cassettes/Restforce_DB_Collector/_run/given_an_existing_Salesforce_record/returns_the_attributes_from_the_Salesforce_record.yml +197 -0
- data/test/cassettes/Restforce_DB_Collector/_run/given_an_existing_database_record/returns_the_attributes_from_the_database_record.yml +81 -0
- 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
- 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
- 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
- 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
- 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
- data/test/cassettes/Restforce_DB_Synchronizer/_run/given_a_Salesforce_record_with_an_associated_database_record/updates_the_database_record.yml +194 -0
- data/test/cassettes/Restforce_DB_Synchronizer/_run/given_a_Salesforce_record_with_an_associated_database_record/updates_the_salesforce_record.yml +233 -0
- data/test/lib/restforce/db/accumulator_test.rb +71 -0
- data/test/lib/restforce/db/attribute_map_test.rb +70 -0
- data/test/lib/restforce/db/collector_test.rb +91 -0
- data/test/lib/restforce/db/initializer_test.rb +92 -0
- data/test/lib/restforce/db/instances/active_record_test.rb +0 -13
- data/test/lib/restforce/db/instances/salesforce_test.rb +20 -13
- data/test/lib/restforce/db/mapping_test.rb +1 -37
- data/test/lib/restforce/db/record_types/active_record_test.rb +0 -40
- data/test/lib/restforce/db/runner_test.rb +40 -0
- data/test/lib/restforce/db/synchronizer_test.rb +26 -86
- metadata +23 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 42506d7bec2c00a7c2161188ef2b238459942da8
|
4
|
+
data.tar.gz: ce83cb60cbfdbd48388189aef560b3257d86995c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0b394e0887d9b4986320b8c1776f0098069aed032a0810c17465ab14d75ce57ac9bbc505cdbabe861b80a89fc7b559ba3f559954ae0beb2adfd398c5c5d502e9
|
7
|
+
data.tar.gz: 7821768fbd4a8da7e87ca397a9015059e9f81d1f08e064e63b4fd626705a58a2ee27a62e0c7499423f59843dfaf5d8d3d4b5b1b34f7697fc8d820b4336f67e8a
|
data/lib/file_daemon.rb
ADDED
@@ -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
|
#
|
data/lib/restforce/db/mapping.rb
CHANGED
@@ -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
|
-
#
|
19
|
+
# model - A String or Class.
|
21
20
|
#
|
22
21
|
# Returns a Restforce::DB::Mapping.
|
23
|
-
def [](
|
24
|
-
collection[
|
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 |
|
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
|
-
@
|
82
|
-
database_model => :database,
|
83
|
-
salesforce_model => :salesforce,
|
84
|
-
}
|
106
|
+
@attribute_map = AttributeMap.new(database_model, salesforce_model, @fields)
|
85
107
|
|
86
|
-
self.class
|
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
|