restforce-db 0.5.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +77 -21
- data/lib/restforce/db.rb +6 -0
- data/lib/restforce/db/associations/active_record.rb +1 -1
- data/lib/restforce/db/attribute_map.rb +5 -1
- data/lib/restforce/db/dsl.rb +80 -0
- data/lib/restforce/db/initializer.rb +3 -2
- data/lib/restforce/db/mapping.rb +20 -77
- data/lib/restforce/db/model.rb +7 -4
- data/lib/restforce/db/registry.rb +62 -0
- data/lib/restforce/db/strategies/always.rb +38 -0
- data/lib/restforce/db/strategies/passive.rb +37 -0
- data/lib/restforce/db/strategy.rb +25 -0
- data/lib/restforce/db/version.rb +1 -1
- data/test/cassettes/Restforce_DB_Initializer/_run/given_an_existing_Salesforce_record/{for_a_non-root_mapping → 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 +159 -0
- data/test/cassettes/Restforce_DB_Initializer/_run/given_an_existing_database_record/{for_a_root_mapping → for_an_Always_strategy}/populates_Salesforce_with_the_new_record.yml +44 -43
- data/test/cassettes/{Restforce_DB_Initializer/_run/given_an_existing_Salesforce_record/for_a_root_mapping/creates_a_matching_database_record.yml → Restforce_DB_Strategies_Always/_build_/given_a_Salesforce_record/wants_to_build_a_new_matching_record.yml} +30 -30
- 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 +158 -0
- data/test/lib/restforce/db/associations/active_record_test.rb +6 -6
- data/test/lib/restforce/db/attribute_map_test.rb +5 -1
- data/test/lib/restforce/db/dsl_test.rb +80 -0
- data/test/lib/restforce/db/initializer_test.rb +8 -8
- data/test/lib/restforce/db/mapping_test.rb +7 -17
- data/test/lib/restforce/db/model_test.rb +8 -9
- data/test/lib/restforce/db/record_types/active_record_test.rb +10 -6
- data/test/lib/restforce/db/registry_test.rb +43 -0
- data/test/lib/restforce/db/strategies/always_test.rb +38 -0
- data/test/lib/restforce/db/strategies/passive_test.rb +17 -0
- data/test/lib/restforce/db/strategy_test.rb +19 -0
- data/test/support/utilities.rb +7 -9
- metadata +17 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b090d0384c8a485ace8705c39d9247d02a7b6c98
|
4
|
+
data.tar.gz: 835e23b61f087b6ef9d66baf0a927f19d285e6d6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 05a85db585c7e7fe76c637dec68332123b1bf195c3ddde90747b33a3d202ce83a6e4ec0e9317bc81a6a2f80655cd62e85c7fb6882d442d0d89373187ec6c7a1a
|
7
|
+
data.tar.gz: d6a81ca6fece5297c1ce7b8e115f0fd04f24fef6110537bbed27a9b873f97694cd817475d6ec076f523696063694f891f9f6742412909a67c40e8abb79cd73e5
|
data/README.md
CHANGED
@@ -41,16 +41,14 @@ class Restaurant < ActiveRecord::Base
|
|
41
41
|
include Restforce::DB::Model
|
42
42
|
has_one :specialty, class_name: "Dish"
|
43
43
|
|
44
|
-
sync_with(
|
45
|
-
"
|
46
|
-
|
44
|
+
sync_with("Restaurant__c", :always) do
|
45
|
+
where "StarRating__c > 4"
|
46
|
+
belongs_to :specialty, through: %w(Specialty__c KeyIngredient__c)
|
47
|
+
maps(
|
47
48
|
name: "Name",
|
48
49
|
style: "Style__c",
|
49
|
-
|
50
|
-
|
51
|
-
specialty: "Specialty__c",
|
52
|
-
},
|
53
|
-
)
|
50
|
+
)
|
51
|
+
end
|
54
52
|
|
55
53
|
end
|
56
54
|
|
@@ -59,24 +57,82 @@ class Dish < ActiveRecord::Base
|
|
59
57
|
include Restforce::DB::Model
|
60
58
|
belongs_to :restaurant
|
61
59
|
|
62
|
-
sync_with(
|
63
|
-
"
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
60
|
+
sync_with("Dish__c", :passive) do
|
61
|
+
has_one :restaurant, through: "Specialty__c"
|
62
|
+
maps(
|
63
|
+
name: "Name",
|
64
|
+
origin: "Origin__c",
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
sync_with("Ingredient__c", :passive) do
|
69
|
+
has_one :restaurant, through: "KeyIngredient__c"
|
70
|
+
maps(
|
71
|
+
key_ingredient: "Name",
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
70
75
|
end
|
71
76
|
```
|
72
77
|
|
73
78
|
This will automatically register the models with entries in the `Restforce::DB::Mapping` collection. This collection defines the manner in which the database and Salesforce systems will be synchronized.
|
74
79
|
|
75
|
-
|
80
|
+
Demonstrated above, `Restforce::DB` has its own DSL for defining mappings, heavily inspired by the ActiveRecord model DSL. The various options are outlined here.
|
81
|
+
|
82
|
+
#### Synchronization Strategies
|
83
|
+
|
84
|
+
The second argument to `sync_with` is a Symbol, reflecting the desired synchronization strategy for the mapping. Valid options are as follows:
|
85
|
+
|
86
|
+
##### `:always`
|
87
|
+
|
88
|
+
An `always` synchronization strategy will create any new records it encounters while polling for changes, and once the object has been persisted in both systems, will update that object any time changes are made to the matching object in the other system.
|
89
|
+
|
90
|
+
Associations defined on an `always` mapping will trigger the creation of those associated records on initial record creation.
|
91
|
+
|
92
|
+
##### `:passive`
|
93
|
+
|
94
|
+
A `passive` synchronization strategy will update all modified records that already exist in both systems, but will not directly create any new records. Objects defined with a `passive` mapping can only be created as a by-product of another mapping's association definitions (via an `always` strategy).
|
95
|
+
|
96
|
+
##### `:associated`
|
97
|
+
|
98
|
+
_Coming Soon_
|
99
|
+
|
100
|
+
#### Lookup Conditions
|
101
|
+
|
102
|
+
`where` accepts one or more query strings which will be used to filter _all_ queries performed for the specific mapping. In the example above, Restaurant objects will only be detected in Salesforce if they exceed a certain value for the `StarRating__c` field.
|
103
|
+
|
104
|
+
Individual conditions supplied to `where` will be appended together with `AND` clauses, and must be composed of valid [`SOQL`](http://www.salesforce.com/us/developer/docs/soql_sosl/).
|
105
|
+
|
106
|
+
#### Field Mappings
|
107
|
+
|
108
|
+
`maps` defines a set of direct field-to-field mappings. It takes a Hash as an argument; the keys should line up with your ActiveRecord attribute names, while the values should line up with the matching field names in Salesforce.
|
109
|
+
|
110
|
+
#### Associations
|
111
|
+
|
112
|
+
Associations in `Restforce::DB` can be a little tricky, as they depend on your ActiveRecord association mappings, but are independent of those mappings, and can even (as seen above) seem to conflict with them.
|
113
|
+
|
114
|
+
If your Salesforce objects have parity with your ActiveRecord models, your association mappings will likely have parity, as well. But, as demonstrated above, you should define your association mappings based on your Salesforce schema.
|
115
|
+
|
116
|
+
##### `belongs_to`
|
117
|
+
|
118
|
+
This defines an association type in which the Lookup (i.e., foreign key) _is on the mapped Salesforce model_. In the example above, the `Restaurant__c` object type in Salesforce has two Lookup fields:
|
119
|
+
|
120
|
+
- `Specialty__c`, which corresponds to the `Dish__c` object type, and
|
121
|
+
- `KeyIngredient__c`, which corresponds to the `Ingredient__c` object type
|
122
|
+
|
123
|
+
Thus, the `Restaurant__c` mapping declares a `belongs_to` relationship to `:specialty`, with a `:through` argument referencing both of the Lookups used by the mappings on the associated `Dish` class.
|
124
|
+
|
125
|
+
As shown above, the `:through` option may contain _an array of Lookup field names_, which may be useful if more than one mapping on the associated ActiveRecord model refers to a Lookup on the same Salesforce record.
|
126
|
+
|
127
|
+
##### `has_one`
|
128
|
+
|
129
|
+
This defines an inverse relationship for a `belongs_to` relationship. In the example above, `Dish` defines _two_ `has_one` relationships with `:restaurant`, one for each mapping. The `:through` arguments for each call to `has_one` correspond to the relevant Lookup field on the parent object.
|
130
|
+
|
131
|
+
In the above example, given the relationships defined between our records, we can ascertain that `Restaurant__c.Specialty__c` is a `Lookup(Dish__c)` field in Salesforce, while ` Restaurant__c.KeyIngredient__c` is a `Lookup(Ingredient__c)`.
|
132
|
+
|
133
|
+
##### `has_many`
|
76
134
|
|
77
|
-
|
78
|
-
- `associations`: These are mappings of ActiveRecord associations to Salesforce lookups. Associations defined here will be built as part of the creation process for a newly-synced record.
|
79
|
-
- `through`: This should be set for models which are created through an association. It references the lookup field on its parent's Salesforce object type.
|
135
|
+
_Coming Soon_
|
80
136
|
|
81
137
|
### Run the daemon
|
82
138
|
|
@@ -100,7 +156,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
100
156
|
|
101
157
|
## Caveats
|
102
158
|
|
103
|
-
- **Update Prioritization.** When synchronization occurs,
|
159
|
+
- **Update Prioritization.** When synchronization occurs, the most recently updated record, Salesforce or database, gets to make the final call about the values of _all_ of the fields it observes. This means that race conditions can and probably will happen if both systems are updated within the same polling interval.
|
104
160
|
- **API Usage.** This gem performs most of its functionality via the Salesforce API (by way of the [`restforce`](https://github.com/ejholmes/restforce) gem). If you're at risk of hitting your Salesforce API limits, this may not be the right approach for you.
|
105
161
|
|
106
162
|
## Contributing
|
data/lib/restforce/db.rb
CHANGED
@@ -5,6 +5,9 @@ require "restforce/extensions"
|
|
5
5
|
|
6
6
|
require "restforce/db/version"
|
7
7
|
require "restforce/db/configuration"
|
8
|
+
require "restforce/db/registry"
|
9
|
+
require "restforce/db/strategy"
|
10
|
+
require "restforce/db/dsl"
|
8
11
|
|
9
12
|
require "restforce/db/associations/active_record"
|
10
13
|
|
@@ -16,6 +19,9 @@ require "restforce/db/record_types/base"
|
|
16
19
|
require "restforce/db/record_types/active_record"
|
17
20
|
require "restforce/db/record_types/salesforce"
|
18
21
|
|
22
|
+
require "restforce/db/strategies/always"
|
23
|
+
require "restforce/db/strategies/passive"
|
24
|
+
|
19
25
|
require "restforce/db/runner"
|
20
26
|
|
21
27
|
require "restforce/db/accumulator"
|
@@ -28,7 +28,7 @@ module Restforce
|
|
28
28
|
#
|
29
29
|
# Returns the constructed associated record.
|
30
30
|
def build(from_record)
|
31
|
-
|
31
|
+
Registry[associated.class].each do |mapping|
|
32
32
|
lookup_id = from_record[mapping.through]
|
33
33
|
apply(mapping, lookup_id)
|
34
34
|
end
|
@@ -59,7 +59,11 @@ module Restforce
|
|
59
59
|
#
|
60
60
|
# Examples
|
61
61
|
#
|
62
|
-
# mapping =
|
62
|
+
# mapping = AttributeMap.new(
|
63
|
+
# MyClass,
|
64
|
+
# "Object__c",
|
65
|
+
# some_key: "SomeField__c",
|
66
|
+
# )
|
63
67
|
#
|
64
68
|
# mapping.convert("Object__c", some_key: "some value")
|
65
69
|
# # => { "Some_Field__c" => "some value" }
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
module DB
|
4
|
+
|
5
|
+
# Restforce::DB::DSL defines a syntax through which a Mapping may be
|
6
|
+
# configured between a database model and an object type in Salesforce.
|
7
|
+
class DSL
|
8
|
+
|
9
|
+
attr_reader :mapping
|
10
|
+
|
11
|
+
# Public: Initialize a Restforce::DB::DSL.
|
12
|
+
#
|
13
|
+
# database_model - An ActiveRecord::Base subclass.
|
14
|
+
# salesforce_model - A String Salesforce object name.
|
15
|
+
# strategy_name - A Symbol initialization strategy name.
|
16
|
+
# options - A Hash of options to pass to the Strategy object.
|
17
|
+
#
|
18
|
+
# Returns nothing.
|
19
|
+
def initialize(database_model, salesforce_model, strategy_name, options = {})
|
20
|
+
strategy = Strategy.for(strategy_name, options)
|
21
|
+
@mapping = Mapping.new(database_model, salesforce_model, strategy)
|
22
|
+
Registry << @mapping
|
23
|
+
end
|
24
|
+
|
25
|
+
# Public: Define a set of conditions which should be used to filter the
|
26
|
+
# Salesforce record lookups for this mapping.
|
27
|
+
#
|
28
|
+
# conditions - An Array of String query conditions.
|
29
|
+
#
|
30
|
+
# Returns nothing.
|
31
|
+
def where(*conditions)
|
32
|
+
@mapping.conditions = conditions
|
33
|
+
end
|
34
|
+
|
35
|
+
# Public: Define a relationship in which the current mapping contains the
|
36
|
+
# lookup ID for another mapping.
|
37
|
+
#
|
38
|
+
# TODO: This should eventually be implemented as a full-fledged
|
39
|
+
# association on the mapping. Something like:
|
40
|
+
# @mapping.associate(association, Associations::BelongsTo, options)
|
41
|
+
#
|
42
|
+
# association - The name of the ActiveRecord association.
|
43
|
+
# options - A Hash of options to pass to the association.
|
44
|
+
#
|
45
|
+
# Returns nothing.
|
46
|
+
def belongs_to(association, options)
|
47
|
+
@mapping.associations[association] = options[:through]
|
48
|
+
end
|
49
|
+
|
50
|
+
# Public: Define a relationship in which the current mapping is referenced
|
51
|
+
# by one object through a lookup ID on another mapping.
|
52
|
+
#
|
53
|
+
# TODO: This should eventually be implemented as a full-fledged
|
54
|
+
# association on the mapping. Something like:
|
55
|
+
# @mapping.associate(association, Associations::HasOne, options)
|
56
|
+
#
|
57
|
+
# association - The name of the ActiveRecord association.
|
58
|
+
# options - A Hash of options to pass to the association.
|
59
|
+
#
|
60
|
+
# Returns nothing.
|
61
|
+
def has_one(_association, options) # rubocop:disable PredicateName
|
62
|
+
@mapping.through = options[:through]
|
63
|
+
end
|
64
|
+
|
65
|
+
# Public: Define a set of fields which should be synchronized between the
|
66
|
+
# database record and Salesforce.
|
67
|
+
#
|
68
|
+
# fields - A Hash, with keys corresponding to attributes of the database
|
69
|
+
# record, and values corresponding to field names in Salesforce.
|
70
|
+
#
|
71
|
+
# Returns nothing.
|
72
|
+
def maps(fields)
|
73
|
+
@mapping.fields = fields
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
@@ -14,6 +14,7 @@ module Restforce
|
|
14
14
|
# runner - A Restforce::DB::Runner.
|
15
15
|
def initialize(mapping, runner = Runner.new)
|
16
16
|
@mapping = mapping
|
17
|
+
@strategy = mapping.strategy
|
17
18
|
@runner = runner
|
18
19
|
end
|
19
20
|
|
@@ -21,7 +22,7 @@ module Restforce
|
|
21
22
|
#
|
22
23
|
# Returns nothing.
|
23
24
|
def run
|
24
|
-
return
|
25
|
+
return if @strategy.passive?
|
25
26
|
|
26
27
|
@runner.run(@mapping) do |run|
|
27
28
|
run.salesforce_records { |record| create_in_database(record) }
|
@@ -39,7 +40,7 @@ module Restforce
|
|
39
40
|
#
|
40
41
|
# Returns nothing.
|
41
42
|
def create_in_database(record)
|
42
|
-
return
|
43
|
+
return unless @strategy.build?(record)
|
43
44
|
@mapping.database_record_type.create!(record)
|
44
45
|
end
|
45
46
|
|
data/lib/restforce/db/mapping.rb
CHANGED
@@ -9,57 +9,9 @@ module Restforce
|
|
9
9
|
|
10
10
|
class InvalidMappingError < StandardError; end
|
11
11
|
|
12
|
-
class << self
|
13
|
-
|
14
|
-
include Enumerable
|
15
|
-
attr_accessor :collection
|
16
|
-
|
17
|
-
# Public: Get the Restforce::DB::Mapping entry for the specified model.
|
18
|
-
#
|
19
|
-
# model - A String or Class.
|
20
|
-
#
|
21
|
-
# Returns a Restforce::DB::Mapping.
|
22
|
-
def [](model)
|
23
|
-
collection[model]
|
24
|
-
end
|
25
|
-
|
26
|
-
# Public: Iterate through all registered Restforce::DB::Mappings.
|
27
|
-
#
|
28
|
-
# Yields one Mapping for each database-to-Salesforce mapping.
|
29
|
-
# Returns nothing.
|
30
|
-
def each
|
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
|
-
|
36
|
-
mappings.each do |mapping|
|
37
|
-
yield mapping
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
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
|
-
|
56
|
-
end
|
57
|
-
|
58
|
-
self.collection ||= {}
|
59
|
-
|
60
12
|
extend Forwardable
|
61
13
|
def_delegators(
|
62
|
-
|
14
|
+
:attribute_map,
|
63
15
|
:attributes,
|
64
16
|
:convert,
|
65
17
|
:convert_from_salesforce,
|
@@ -70,42 +22,32 @@ module Restforce
|
|
70
22
|
:salesforce_model,
|
71
23
|
:database_record_type,
|
72
24
|
:salesforce_record_type,
|
25
|
+
)
|
26
|
+
|
27
|
+
attr_accessor(
|
28
|
+
:fields,
|
73
29
|
:associations,
|
74
30
|
:conditions,
|
75
31
|
:through,
|
32
|
+
:strategy,
|
76
33
|
)
|
77
34
|
|
78
35
|
# Public: Initialize a new Restforce::DB::Mapping.
|
79
36
|
#
|
80
37
|
# database_model - A Class compatible with ActiveRecord::Base.
|
81
38
|
# salesforce_model - A String name of an object type in Salesforce.
|
82
|
-
#
|
83
|
-
|
84
|
-
# :fields - A Hash of mappings between database
|
85
|
-
# columns and fields in Salesforce.
|
86
|
-
# :associations - A Hash of mappings between Active
|
87
|
-
# Record association names and the
|
88
|
-
# corresponding Salesforce Lookup name.
|
89
|
-
# :conditions - An Array of lookup conditions which
|
90
|
-
# should be applied to the Salesforce
|
91
|
-
# queries.
|
92
|
-
# :root - A Boolean reflecting whether or not
|
93
|
-
# this is a root-level mapping.
|
94
|
-
def initialize(database_model, salesforce_model, options = {})
|
39
|
+
# strategy - A synchronization Strategy object.
|
40
|
+
def initialize(database_model, salesforce_model, strategy = Strategies::Always.new)
|
95
41
|
@database_model = database_model
|
96
42
|
@salesforce_model = salesforce_model
|
97
43
|
|
98
44
|
@database_record_type = RecordTypes::ActiveRecord.new(database_model, self)
|
99
45
|
@salesforce_record_type = RecordTypes::Salesforce.new(salesforce_model, self)
|
100
46
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
@attribute_map = AttributeMap.new(database_model, salesforce_model, @fields)
|
107
|
-
|
108
|
-
self.class << self
|
47
|
+
self.associations = {}
|
48
|
+
self.fields = {}
|
49
|
+
self.conditions = []
|
50
|
+
self.strategy = strategy
|
109
51
|
end
|
110
52
|
|
111
53
|
# Public: Get a list of the relevant Salesforce field names for this
|
@@ -113,7 +55,7 @@ module Restforce
|
|
113
55
|
#
|
114
56
|
# Returns an Array.
|
115
57
|
def salesforce_fields
|
116
|
-
|
58
|
+
fields.values + associations.values.flatten
|
117
59
|
end
|
118
60
|
|
119
61
|
# Public: Get a list of the relevant database column names for this
|
@@ -121,7 +63,7 @@ module Restforce
|
|
121
63
|
#
|
122
64
|
# Returns an Array.
|
123
65
|
def database_fields
|
124
|
-
|
66
|
+
fields.keys
|
125
67
|
end
|
126
68
|
|
127
69
|
# Public: Get the name of the database column which should be used to
|
@@ -144,12 +86,13 @@ module Restforce
|
|
144
86
|
end
|
145
87
|
end
|
146
88
|
|
147
|
-
|
148
|
-
|
89
|
+
private
|
90
|
+
|
91
|
+
# Internal: Get an AttributeMap for the fields defined for this mapping.
|
149
92
|
#
|
150
|
-
# Returns a
|
151
|
-
def
|
152
|
-
@
|
93
|
+
# Returns a Restforce::DB::AttributeMap.
|
94
|
+
def attribute_map
|
95
|
+
@attribute_map ||= AttributeMap.new(database_model, salesforce_model, fields)
|
153
96
|
end
|
154
97
|
|
155
98
|
end
|
data/lib/restforce/db/model.rb
CHANGED
@@ -16,14 +16,17 @@ module Restforce
|
|
16
16
|
module ClassMethods
|
17
17
|
|
18
18
|
# Public: Initializes a Restforce::DB::Mapping defining this model's
|
19
|
-
# relationship to a Salesforce object type.
|
19
|
+
# relationship to a Salesforce object type. Passes a provided block to
|
20
|
+
# the Restforce::DB::DSL for evaluation.
|
20
21
|
#
|
21
22
|
# salesforce_model - A String name of an object type in Salesforce.
|
23
|
+
# strategy - A Symbol naming a desired initialization strategy.
|
22
24
|
# options - A Hash of options to pass through to the Mapping.
|
25
|
+
# block - A block of code to evaluate through the DSL.
|
23
26
|
#
|
24
|
-
# Returns
|
25
|
-
def sync_with(salesforce_model,
|
26
|
-
|
27
|
+
# Returns nothing.
|
28
|
+
def sync_with(salesforce_model, strategy = :always, options = {}, &block)
|
29
|
+
Restforce::DB::DSL.new(self, salesforce_model, strategy, options).instance_eval(&block)
|
27
30
|
end
|
28
31
|
|
29
32
|
end
|