restforce-db 1.0.4 → 1.1.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 +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|
|