restforce-db 1.0.4 → 1.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/README.md +38 -22
- data/lib/restforce/db/associations/base.rb +152 -0
- data/lib/restforce/db/associations/belongs_to.rb +70 -0
- data/lib/restforce/db/associations/foreign_key.rb +80 -0
- data/lib/restforce/db/associations/has_many.rb +37 -0
- data/lib/restforce/db/associations/has_one.rb +33 -0
- data/lib/restforce/db/dsl.rb +25 -13
- data/lib/restforce/db/mapping.rb +2 -3
- data/lib/restforce/db/record_types/active_record.rb +3 -8
- data/lib/restforce/db/record_types/salesforce.rb +23 -8
- data/lib/restforce/db/strategies/associated.rb +58 -0
- data/lib/restforce/db/version.rb +1 -1
- data/lib/restforce/db.rb +6 -1
- data/restforce-db.gemspec +0 -1
- data/test/cassettes/Restforce_DB_Associations_BelongsTo/with_an_inverse_mapping/_build/returns_an_associated_record_populated_with_the_Salesforce_attributes.yml +271 -0
- data/test/cassettes/Restforce_DB_Associations_BelongsTo/with_an_inverse_mapping/_synced_for_/when_a_matching_associated_record_has_been_synchronized/returns_true.yml +271 -0
- data/test/cassettes/Restforce_DB_Associations_BelongsTo/with_an_inverse_mapping/_synced_for_/when_no_matching_associated_record_has_been_synchronized/returns_false.yml +271 -0
- data/test/cassettes/Restforce_DB_Associations_HasMany/with_an_inverse_mapping/_build/builds_a_number_of_associated_records_from_the_data_in_Salesforce.yml +439 -0
- data/test/cassettes/Restforce_DB_Associations_HasMany/with_an_inverse_mapping/_synced_for_/when_a_matching_associated_record_has_been_synchronized/returns_true.yml +439 -0
- data/test/cassettes/Restforce_DB_Associations_HasMany/with_an_inverse_mapping/_synced_for_/when_no_matching_associated_record_has_been_synchronized/returns_false.yml +196 -0
- data/test/cassettes/Restforce_DB_Associations_HasOne/with_an_inverse_mapping/_build/returns_an_associated_record_populated_with_the_Salesforce_attributes.yml +271 -0
- data/test/cassettes/Restforce_DB_Associations_HasOne/with_an_inverse_mapping/_synced_for_/when_a_matching_associated_record_has_been_synchronized/returns_true.yml +271 -0
- data/test/cassettes/Restforce_DB_Associations_HasOne/with_an_inverse_mapping/_synced_for_/when_no_matching_associated_record_has_been_synchronized/returns_false.yml +271 -0
- data/test/cassettes/Restforce_DB_Strategies_Associated/_build_/given_an_inverse_mapping/with_a_synchronized_association_record/wants_to_build_a_new_record.yml +275 -0
- 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 +237 -0
- 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 +275 -0
- data/test/lib/restforce/db/associations/belongs_to_test.rb +82 -0
- data/test/lib/restforce/db/associations/has_many_test.rb +96 -0
- data/test/lib/restforce/db/associations/has_one_test.rb +80 -0
- data/test/lib/restforce/db/dsl_test.rb +22 -3
- data/test/lib/restforce/db/mapping_test.rb +10 -8
- data/test/lib/restforce/db/record_types/active_record_test.rb +12 -5
- data/test/lib/restforce/db/runner_test.rb +2 -3
- data/test/lib/restforce/db/strategies/always_test.rb +2 -2
- data/test/lib/restforce/db/strategies/associated_test.rb +78 -0
- data/test/lib/restforce/db/strategies/passive_test.rb +1 -1
- data/test/lib/restforce/db/tracker_test.rb +0 -2
- data/test/support/active_record.rb +25 -2
- data/test/support/salesforce.rb +1 -1
- data/test/support/utilities.rb +5 -7
- data/test/test_helper.rb +0 -1
- metadata +24 -18
- data/lib/restforce/db/associations/active_record.rb +0 -68
- data/test/lib/restforce/db/associations/active_record_test.rb +0 -43
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8e6b77ef750532361625bbec55ad4a00e1ae4d21
|
4
|
+
data.tar.gz: b88d477b1e2a2ea7f2e301b35c54b08d77612bb4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 89a255caf32a25c81b24415bd5d7b389a19219e0055b7af73f3c59388a1b0914f8eb827dfe48752f528a46b65f766fb8ab9060261a6a116460b43575032da136
|
7
|
+
data.tar.gz: 53d5e840aeaee28b666979b7a97e31f85bebd7bcd6bb6d6611f125f3bc6ae7c995de187749680d19ee6fdb51c3b0728ec03ae6069aad68429201fca0d444fc4d
|
data/README.md
CHANGED
@@ -39,11 +39,13 @@ To register a Salesforce mapping in an `ActiveRecord` model, you'll need to add
|
|
39
39
|
class Restaurant < ActiveRecord::Base
|
40
40
|
|
41
41
|
include Restforce::DB::Model
|
42
|
-
has_one :
|
42
|
+
has_one :chef, inverse_of: :restaurant
|
43
|
+
has_many :dishes, inverse_of: :restaurant
|
43
44
|
|
44
45
|
sync_with("Restaurant__c", :always) do
|
45
46
|
where "StarRating__c > 4"
|
46
|
-
|
47
|
+
has_many :dishes, through: "Restaurant__c"
|
48
|
+
belongs_to :chef, through: %w(Chef__c Cuisine__c)
|
47
49
|
maps(
|
48
50
|
name: "Name",
|
49
51
|
style: "Style__c",
|
@@ -52,26 +54,33 @@ class Restaurant < ActiveRecord::Base
|
|
52
54
|
|
53
55
|
end
|
54
56
|
|
55
|
-
class
|
57
|
+
class Chef < ActiveRecord::Base
|
56
58
|
|
57
59
|
include Restforce::DB::Model
|
58
|
-
belongs_to :restaurant
|
60
|
+
belongs_to :restaurant, inverse_of: :chef
|
59
61
|
|
60
|
-
sync_with("
|
61
|
-
has_one :restaurant, through: "
|
62
|
-
maps
|
63
|
-
name: "Name",
|
64
|
-
origin: "Origin__c",
|
65
|
-
)
|
62
|
+
sync_with("Contact", :passive) do
|
63
|
+
has_one :restaurant, through: "Chef__c"
|
64
|
+
maps name: "Name"
|
66
65
|
end
|
67
66
|
|
68
|
-
sync_with("
|
69
|
-
has_one :restaurant, through: "
|
70
|
-
maps
|
71
|
-
key_ingredient: "Name",
|
72
|
-
)
|
67
|
+
sync_with("Cuisine__c", :passive) do
|
68
|
+
has_one :restaurant, through: "Cuisine__c"
|
69
|
+
maps style: "Name"
|
73
70
|
end
|
74
71
|
|
72
|
+
end
|
73
|
+
|
74
|
+
class Dish < ActiveRecord::Base
|
75
|
+
|
76
|
+
include Restforce::DB::Model
|
77
|
+
belongs_to :restaurant, inverse_of: :dishes
|
78
|
+
|
79
|
+
sync_with("Dish__c", :associated, with: :restaurant) do
|
80
|
+
belongs_to :restaurant, through: "Restaurant__c"
|
81
|
+
maps name: "Name"
|
82
|
+
end
|
83
|
+
|
75
84
|
end
|
76
85
|
```
|
77
86
|
|
@@ -95,7 +104,9 @@ A `passive` synchronization strategy will update all modified records that alrea
|
|
95
104
|
|
96
105
|
##### `:associated`
|
97
106
|
|
98
|
-
|
107
|
+
An `associated` synchronization strategy will create any new records it encounters _if and only if the named association for that record has already been synchronized_. The association is specified via the `:with` option. In the above example, new `Dish`/`Dish__c` records will be synchronized when the record identified by `Restaurant__c` has already been synchronized.
|
108
|
+
|
109
|
+
This allows for the selective addition of "relevant" records to the system over time.
|
99
110
|
|
100
111
|
#### Lookup Conditions
|
101
112
|
|
@@ -117,22 +128,27 @@ If your Salesforce objects have parity with your ActiveRecord models, your assoc
|
|
117
128
|
|
118
129
|
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
130
|
|
120
|
-
- `
|
121
|
-
- `
|
131
|
+
- `Chef__c`, which corresponds to the `Contact` object type, and
|
132
|
+
- `Cuisine__c`, which corresponds to the `Cuisine__c` object type
|
122
133
|
|
123
|
-
Thus, the `Restaurant__c` mapping declares a `belongs_to` relationship to `:
|
134
|
+
Thus, the `Restaurant__c` mapping declares a `belongs_to` relationship to `:chef`, with a `:through` argument referencing both of the Lookups used by the mappings on the associated `Chef` class.
|
124
135
|
|
125
136
|
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
137
|
|
127
138
|
##### `has_one`
|
128
139
|
|
129
|
-
This defines an inverse relationship for a `belongs_to` relationship. In the example above, `
|
140
|
+
This defines an inverse relationship for a `belongs_to` relationship. In the example above, `Chef` 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
141
|
|
131
|
-
In the above example, given the relationships defined between our records, we can ascertain that `Restaurant__c.
|
142
|
+
In the above example, given the relationships defined between our records, we can ascertain that `Restaurant__c.Chef__c` is a `Lookup(Contact)` field in Salesforce, while `Restaurant__c.Cuisine__c` is a `Lookup(Cuisine__c)`.
|
132
143
|
|
133
144
|
##### `has_many`
|
134
145
|
|
135
|
-
|
146
|
+
This _also_ defines an inverse relationship for a `belongs_to` relationship. The chief difference between this and `has_one` is that `has_many` relationships are one-to-many, rather than one-to-one.
|
147
|
+
|
148
|
+
In the above example, `Dish__c` is a Salesforce object type which references the `Restaurant__c` object type through an aptly-named Lookup. There is no restriction on the number of `Dish__c` objects that may reference the same `Restaurant__c`, so we define this relationship as a `has_many` associaition in our `Restaurant` mapping.
|
149
|
+
|
150
|
+
__NOTE__: Unlike `has_one` associations, `has_many` associations do not currently support multiple lookups from the same model. The Lookup is assumed
|
151
|
+
to always refer to the `Id` of the parent object.
|
136
152
|
|
137
153
|
### Run the daemon
|
138
154
|
|
@@ -0,0 +1,152 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
module DB
|
4
|
+
|
5
|
+
module Associations
|
6
|
+
|
7
|
+
# Restforce::DB::Associations::Base defines an association between two
|
8
|
+
# mappings in the Registry.
|
9
|
+
class Base
|
10
|
+
|
11
|
+
attr_reader :name, :lookup
|
12
|
+
|
13
|
+
# Public: Initialize a new Restforce::DB::Associations::Base.
|
14
|
+
#
|
15
|
+
# name - The name of the ActiveRecord association to construct.
|
16
|
+
# through - The name of the lookup field on the Salesforce record.
|
17
|
+
def initialize(name, through: nil)
|
18
|
+
@name = name.to_sym
|
19
|
+
@lookup = through.is_a?(Array) ? through.map(&:to_s) : through.to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
# Public: Build a record or series of records for the association
|
23
|
+
# defined by this class. Must be overridden in subclasses.
|
24
|
+
#
|
25
|
+
# Raises a NotImplementedError.
|
26
|
+
def build(_database_record, _salesforce_record)
|
27
|
+
raise NotImplementedError
|
28
|
+
end
|
29
|
+
|
30
|
+
# Public: Get a list of fields which should be included in the
|
31
|
+
# Salesforce record's lookups for any mapping including this
|
32
|
+
# association.
|
33
|
+
#
|
34
|
+
# Returns a list of Salesforce fields this record should return.
|
35
|
+
def fields
|
36
|
+
[*lookup]
|
37
|
+
end
|
38
|
+
|
39
|
+
# Public: Has a record for this association already been synchronized
|
40
|
+
# for the supplied instance?
|
41
|
+
#
|
42
|
+
# instance - A Restforce::DB::Instances::Base.
|
43
|
+
#
|
44
|
+
# Returns a Boolean.
|
45
|
+
def synced_for?(instance)
|
46
|
+
base_class = instance.mapping.database_model
|
47
|
+
reflection = base_class.reflect_on_association(name)
|
48
|
+
association_id = associated_salesforce_id(instance)
|
49
|
+
|
50
|
+
return false unless association_id
|
51
|
+
reflection.klass.exists?(
|
52
|
+
mapping_for(reflection).lookup_column => association_id,
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# Internal: Get the appropriate Salesforce Lookup ID field for the
|
59
|
+
# passed mapping.
|
60
|
+
#
|
61
|
+
# mapping - A Restforce::DB::Mapping.
|
62
|
+
# database_record - An instance of an ActiveRecord::Base subclass.
|
63
|
+
#
|
64
|
+
# Returns a String.
|
65
|
+
def lookup_field(mapping, database_record)
|
66
|
+
inverse = inverse_association_name(target_reflection(database_record))
|
67
|
+
mapping.associations.detect { |a| a.name == inverse }.lookup
|
68
|
+
end
|
69
|
+
|
70
|
+
# Internal: Get the class of the inverse ActiveRecord association.
|
71
|
+
#
|
72
|
+
# database_record - An instance of an ActiveRecord::Base subclass.
|
73
|
+
#
|
74
|
+
# Returns a Class.
|
75
|
+
def target_class(database_record)
|
76
|
+
target_reflection(database_record).klass
|
77
|
+
end
|
78
|
+
|
79
|
+
# Internal: Get an AssociationReflection for this association on the
|
80
|
+
# passed database record.
|
81
|
+
#
|
82
|
+
# database_record - An instance of an ActiveRecord::Base subclass.
|
83
|
+
#
|
84
|
+
# Returns an ActiveRecord::AssociationReflection.
|
85
|
+
def target_reflection(database_record)
|
86
|
+
database_record.class.reflect_on_association(name)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Internal: Get the name of the inverse association which corresponds
|
90
|
+
# to this one.
|
91
|
+
#
|
92
|
+
# reflection - An ActiveRecord::AssociationReflection.
|
93
|
+
#
|
94
|
+
# Returns a Symbol.
|
95
|
+
def inverse_association_name(reflection)
|
96
|
+
reflection.send(:inverse_name)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Internal: Get the first mapping which corresponds to an ActiveRecord
|
100
|
+
# reflection.
|
101
|
+
#
|
102
|
+
# reflection - An ActiveRecord::AssociationReflection.
|
103
|
+
#
|
104
|
+
# Returns a Restforce::DB::Mapping.
|
105
|
+
def mapping_for(reflection)
|
106
|
+
inverse = inverse_association_name(reflection)
|
107
|
+
Registry[reflection.klass].detect do |mapping|
|
108
|
+
mapping.associations.any? { |a| a.name == inverse }
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Internal: Get the first association which corresponds to an
|
113
|
+
# ActiveRecord reflection.
|
114
|
+
#
|
115
|
+
# reflection - An ActiveRecord::AssociationReflection.
|
116
|
+
#
|
117
|
+
# Returns a Restforce::DB::Associations::Base.
|
118
|
+
def association_for(reflection)
|
119
|
+
inverse = reflection.send(:inverse_name)
|
120
|
+
Registry[reflection.klass].detect do |mapping|
|
121
|
+
association = mapping.associations.detect { |a| a.name == inverse }
|
122
|
+
break association if association
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Internal: Get an ActiveRecord::Relation scope for the passed record's
|
127
|
+
# association.
|
128
|
+
#
|
129
|
+
# database_record - An instance of an ActiveRecord::Base subclass.
|
130
|
+
#
|
131
|
+
# Returns an ActiveRecord scope.
|
132
|
+
def association_scope(database_record)
|
133
|
+
database_record.association(name).scope
|
134
|
+
end
|
135
|
+
|
136
|
+
# Internal: Get the Salesforce ID belonging to the associated record
|
137
|
+
# for a supplied instance. Must be implemented per-association.
|
138
|
+
#
|
139
|
+
# instance - A Restforce::DB::Instances::Base
|
140
|
+
#
|
141
|
+
# Returns a String.
|
142
|
+
def associated_salesforce_id(_instance)
|
143
|
+
raise NotImplementedError
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
module DB
|
4
|
+
|
5
|
+
module Associations
|
6
|
+
|
7
|
+
# Restforce::DB::Associations::BelongsTo defines a relationship in which
|
8
|
+
# a Salesforce ID on the named database association exists on this
|
9
|
+
# Mapping's Salesforce record.
|
10
|
+
class BelongsTo < Base
|
11
|
+
|
12
|
+
# Public: Construct a database record from the Salesforce records
|
13
|
+
# associated with the supplied parent Salesforce record.
|
14
|
+
#
|
15
|
+
# database_record - An instance of an ActiveRecord::Base subclass.
|
16
|
+
# salesforce_record - A Hashie::Mash representing a Salesforce object.
|
17
|
+
#
|
18
|
+
# Returns the constructed association record.
|
19
|
+
def build(database_record, salesforce_record)
|
20
|
+
lookups = {}
|
21
|
+
|
22
|
+
attributes = Registry[target_class(database_record)].inject({}) do |hash, mapping|
|
23
|
+
lookup_id = salesforce_record[lookup_field(mapping, database_record)]
|
24
|
+
|
25
|
+
lookups[mapping.lookup_column] = lookup_id
|
26
|
+
hash.merge(attributes_for(mapping, lookup_id))
|
27
|
+
end
|
28
|
+
|
29
|
+
associated = association_scope(database_record).find_by(lookups)
|
30
|
+
associated ||= database_record.association(name).build(lookups)
|
31
|
+
|
32
|
+
associated.assign_attributes(attributes)
|
33
|
+
associated
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# Internal: Get a database-ready Hash of attributes from the Salesforce
|
39
|
+
# record identified by the passed lookup ID.
|
40
|
+
#
|
41
|
+
# mapping - A Restforce::DB::Mapping.
|
42
|
+
# lookup_id - A Lookup ID for the Salesforce record type in the Mapping.
|
43
|
+
#
|
44
|
+
# Returns a Hash.
|
45
|
+
def attributes_for(mapping, lookup_id)
|
46
|
+
salesforce_instance = mapping.salesforce_record_type.find(lookup_id)
|
47
|
+
mapping.convert(mapping.database_model, salesforce_instance.attributes)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Internal: Get the Salesforce ID belonging to the associated record
|
51
|
+
# for a supplied instance. Must be implemented per-association.
|
52
|
+
#
|
53
|
+
# instance - A Restforce::DB::Instances::Base
|
54
|
+
#
|
55
|
+
# Returns a String.
|
56
|
+
def associated_salesforce_id(instance)
|
57
|
+
reflection = instance.mapping.database_model.reflect_on_association(name)
|
58
|
+
inverse_association = association_for(reflection)
|
59
|
+
|
60
|
+
salesforce_instance = instance.mapping.salesforce_record_type.find(instance.id)
|
61
|
+
salesforce_instance.record[inverse_association.lookup] if salesforce_instance
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
module DB
|
4
|
+
|
5
|
+
module Associations
|
6
|
+
|
7
|
+
# Restforce::DB::Associations::ForeignKey defines a relationship in which
|
8
|
+
# the Salesforce IDs for any associated record(s) are present on a foreign
|
9
|
+
# record type.
|
10
|
+
class ForeignKey < Base
|
11
|
+
|
12
|
+
# Public: Get a list of fields which should be included in the
|
13
|
+
# Salesforce record's lookups for any mapping including this
|
14
|
+
# association.
|
15
|
+
#
|
16
|
+
# Returns a list of Salesforce fields this record should return.
|
17
|
+
def fields
|
18
|
+
[]
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# Internal: Identify the inverse mapping for this relationship by
|
24
|
+
# looking it up through the target association.
|
25
|
+
#
|
26
|
+
# database_record - An instance of an ActiveRecord::Base subclass.
|
27
|
+
#
|
28
|
+
# Returns a Restforce::DB::Mapping.
|
29
|
+
def target_mapping(database_record)
|
30
|
+
inverse = inverse_association_name(target_reflection(database_record))
|
31
|
+
Registry[target_class(database_record)].detect do |mapping|
|
32
|
+
mapping.associations.any? { |a| a.name == inverse }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Internal: Construct a single associated record from the supplied
|
37
|
+
# Salesforce instance.
|
38
|
+
#
|
39
|
+
# database_record - An instance of an ActiveRecord::Base subclass.
|
40
|
+
# salesforce_instance - A Restforce::DB::Instances::Salesforce.
|
41
|
+
#
|
42
|
+
# Returns the constructed object.
|
43
|
+
def construct_for(database_record, salesforce_instance)
|
44
|
+
mapping = salesforce_instance.mapping
|
45
|
+
lookups = { mapping.lookup_column => salesforce_instance.id }
|
46
|
+
associated = association_scope(database_record).find_by(lookups)
|
47
|
+
associated ||= database_record.association(name).build(lookups)
|
48
|
+
|
49
|
+
attributes = mapping.convert(
|
50
|
+
associated.class,
|
51
|
+
salesforce_instance.attributes,
|
52
|
+
)
|
53
|
+
|
54
|
+
associated.assign_attributes(attributes)
|
55
|
+
associated
|
56
|
+
end
|
57
|
+
|
58
|
+
# Internal: Get the Salesforce ID belonging to the associated record
|
59
|
+
# for a supplied instance. Must be implemented per-association.
|
60
|
+
#
|
61
|
+
# instance - A Restforce::DB::Instances::Base
|
62
|
+
#
|
63
|
+
# Returns a String.
|
64
|
+
def associated_salesforce_id(instance)
|
65
|
+
query = "#{lookup} = '#{instance.id}'"
|
66
|
+
|
67
|
+
reflection = instance.mapping.database_model.reflect_on_association(name)
|
68
|
+
inverse_mapping = mapping_for(reflection)
|
69
|
+
|
70
|
+
salesforce_instance = inverse_mapping.salesforce_record_type.first(query)
|
71
|
+
salesforce_instance.id if salesforce_instance
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
module DB
|
4
|
+
|
5
|
+
module Associations
|
6
|
+
|
7
|
+
# Restforce::DB::Associations::HasMany defines a relationship in which
|
8
|
+
# potentially several Salesforce records maintain a reference to the
|
9
|
+
# Salesforce record on the current Mapping.
|
10
|
+
class HasMany < ForeignKey
|
11
|
+
|
12
|
+
# Public: Construct a database record for each Salesforce record
|
13
|
+
# associated with the supplied parent Salesforce record.
|
14
|
+
#
|
15
|
+
# database_record - An instance of an ActiveRecord::Base subclass.
|
16
|
+
# salesforce_record - A Hashie::Mash representing a Salesforce object.
|
17
|
+
#
|
18
|
+
# Returns the constructed association records.
|
19
|
+
def build(database_record, salesforce_record)
|
20
|
+
target = target_mapping(database_record)
|
21
|
+
lookup_id = "#{lookup_field(target, database_record)} = '#{salesforce_record.Id}'"
|
22
|
+
|
23
|
+
records = []
|
24
|
+
target.salesforce_record_type.each(conditions: lookup_id) do |instance|
|
25
|
+
records << construct_for(database_record, instance)
|
26
|
+
end
|
27
|
+
|
28
|
+
records
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Restforce
|
2
|
+
|
3
|
+
module DB
|
4
|
+
|
5
|
+
module Associations
|
6
|
+
|
7
|
+
# Restforce::DB::Associations::HasOne defines a relationship in which a
|
8
|
+
# Salesforce ID for this Mapping's database record exists on the named
|
9
|
+
# database association's Mapping.
|
10
|
+
class HasOne < ForeignKey
|
11
|
+
|
12
|
+
# Public: Construct a database record from a single Salesforce record
|
13
|
+
# associated with the supplied parent Salesforce record.
|
14
|
+
#
|
15
|
+
# database_record - An instance of an ActiveRecord::Base subclass.
|
16
|
+
# salesforce_record - A Hashie::Mash representing a Salesforce object.
|
17
|
+
#
|
18
|
+
# Returns the constructed association record.
|
19
|
+
def build(database_record, salesforce_record)
|
20
|
+
target = target_mapping(database_record)
|
21
|
+
query = "#{lookup_field(target, database_record)} = '#{salesforce_record.Id}'"
|
22
|
+
|
23
|
+
instance = target.salesforce_record_type.first(query)
|
24
|
+
construct_for(database_record, instance)
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
data/lib/restforce/db/dsl.rb
CHANGED
@@ -35,31 +35,43 @@ module Restforce
|
|
35
35
|
# Public: Define a relationship in which the current mapping contains the
|
36
36
|
# lookup ID for another mapping.
|
37
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
38
|
# association - The name of the ActiveRecord association.
|
43
|
-
#
|
39
|
+
# through - A String or Array of Strings representing the Lookup IDs.
|
44
40
|
#
|
45
41
|
# Returns nothing.
|
46
|
-
def belongs_to(association,
|
47
|
-
@mapping.associations
|
42
|
+
def belongs_to(association, through:)
|
43
|
+
@mapping.associations << Associations::BelongsTo.new(
|
44
|
+
association,
|
45
|
+
through: through,
|
46
|
+
)
|
48
47
|
end
|
49
48
|
|
50
49
|
# Public: Define a relationship in which the current mapping is referenced
|
51
50
|
# by one object through a lookup ID on another mapping.
|
52
51
|
#
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
52
|
+
# association - The name of the ActiveRecord association.
|
53
|
+
# through - A String representing the Lookup ID.
|
54
|
+
#
|
55
|
+
# Returns nothing.
|
56
|
+
def has_one(association, through:) # rubocop:disable PredicateName
|
57
|
+
@mapping.associations << Associations::HasOne.new(
|
58
|
+
association,
|
59
|
+
through: through,
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Public: Define a relationship in which the current mapping is referenced
|
64
|
+
# by many objects through a lookup ID on another mapping.
|
56
65
|
#
|
57
66
|
# association - The name of the ActiveRecord association.
|
58
|
-
#
|
67
|
+
# through - A String representing the Lookup ID.
|
59
68
|
#
|
60
69
|
# Returns nothing.
|
61
|
-
def
|
62
|
-
@mapping.
|
70
|
+
def has_many(association, through:) # rubocop:disable PredicateName
|
71
|
+
@mapping.associations << Associations::HasMany.new(
|
72
|
+
association,
|
73
|
+
through: through,
|
74
|
+
)
|
63
75
|
end
|
64
76
|
|
65
77
|
# Public: Define a set of fields which should be synchronized between the
|
data/lib/restforce/db/mapping.rb
CHANGED
@@ -28,7 +28,6 @@ module Restforce
|
|
28
28
|
:fields,
|
29
29
|
:associations,
|
30
30
|
:conditions,
|
31
|
-
:through,
|
32
31
|
:strategy,
|
33
32
|
)
|
34
33
|
|
@@ -44,8 +43,8 @@ module Restforce
|
|
44
43
|
@database_record_type = RecordTypes::ActiveRecord.new(database_model, self)
|
45
44
|
@salesforce_record_type = RecordTypes::Salesforce.new(salesforce_model, self)
|
46
45
|
|
47
|
-
self.associations = {}
|
48
46
|
self.fields = {}
|
47
|
+
self.associations = []
|
49
48
|
self.conditions = []
|
50
49
|
self.strategy = strategy
|
51
50
|
end
|
@@ -55,7 +54,7 @@ module Restforce
|
|
55
54
|
#
|
56
55
|
# Returns an Array.
|
57
56
|
def salesforce_fields
|
58
|
-
fields.values + associations.
|
57
|
+
fields.values + associations.map(&:fields).flatten
|
59
58
|
end
|
60
59
|
|
61
60
|
# Public: Get a list of the relevant database column names for this
|
@@ -17,17 +17,12 @@ module Restforce
|
|
17
17
|
# Returns a Restforce::DB::Instances::ActiveRecord instance.
|
18
18
|
# Raises on any validation or database error.
|
19
19
|
def create!(from_record)
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
associations = @mapping.associations.map do |association, _|
|
25
|
-
Associations::ActiveRecord.new(record, association).build(from_record.record)
|
26
|
-
end
|
20
|
+
record = @record_type.find_or_initialize_by(@mapping.lookup_column => from_record.id)
|
21
|
+
record.assign_attributes(@mapping.convert(@record_type, from_record.attributes))
|
22
|
+
associations = @mapping.associations.flat_map { |a| a.build(record, from_record.record) }
|
27
23
|
|
28
24
|
record.transaction do
|
29
25
|
record.save!
|
30
|
-
|
31
26
|
# We touch the synchronization timestamps here to ensure that they
|
32
27
|
# exceed the last updated timestamp.
|
33
28
|
associations.each { |association| association.touch(:synchronized_at) }
|
@@ -25,26 +25,40 @@ module Restforce
|
|
25
25
|
find(record_id)
|
26
26
|
end
|
27
27
|
|
28
|
-
# Public: Find the Salesforce record
|
28
|
+
# Public: Find the first Salesforce record which meets the passed
|
29
|
+
# conditions.
|
29
30
|
#
|
30
|
-
#
|
31
|
+
# conditions - One or more String query conditions
|
31
32
|
#
|
32
33
|
# Returns nil or a Restforce::DB::Instances::Salesforce instance.
|
33
|
-
def
|
34
|
-
record = DB.client.query(query(
|
34
|
+
def first(*conditions)
|
35
|
+
record = DB.client.query(query(conditions)).first
|
35
36
|
return unless record
|
36
37
|
|
37
38
|
Instances::Salesforce.new(@record_type, record, @mapping)
|
38
39
|
end
|
39
40
|
|
41
|
+
# Public: Find the Salesforce record corresponding to the passed id.
|
42
|
+
#
|
43
|
+
# id - The id of the record in Salesforce.
|
44
|
+
#
|
45
|
+
# Returns nil or a Restforce::DB::Instances::Salesforce instance.
|
46
|
+
def find(id)
|
47
|
+
first("Id = '#{id}'")
|
48
|
+
end
|
49
|
+
|
40
50
|
# Public: Iterate through all Salesforce records of this type.
|
41
51
|
#
|
42
52
|
# options - A Hash of options which should be applied to the set of
|
43
53
|
# fetched records. Allowed options are:
|
44
|
-
# :before
|
45
|
-
#
|
46
|
-
#
|
47
|
-
#
|
54
|
+
# :before - A Time object defining the most recent update
|
55
|
+
# timestamp for which records should be
|
56
|
+
# returned.
|
57
|
+
# :after - A Time object defining the least recent update
|
58
|
+
# timestamp for which records should be
|
59
|
+
# returned.
|
60
|
+
# :conditions - An Array of conditions to append to the lookup
|
61
|
+
# query.
|
48
62
|
#
|
49
63
|
# Yields a series of Restforce::DB::Instances::Salesforce instances.
|
50
64
|
# Returns nothing.
|
@@ -52,6 +66,7 @@ module Restforce
|
|
52
66
|
constraints = [
|
53
67
|
("SystemModstamp < #{options[:before].utc.iso8601}" if options[:before]),
|
54
68
|
("SystemModstamp >= #{options[:after].utc.iso8601}" if options[:after]),
|
69
|
+
*options[:conditions],
|
55
70
|
]
|
56
71
|
|
57
72
|
DB.client.query(query(*constraints)).each do |record|
|