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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +38 -22
  3. data/lib/restforce/db/associations/base.rb +152 -0
  4. data/lib/restforce/db/associations/belongs_to.rb +70 -0
  5. data/lib/restforce/db/associations/foreign_key.rb +80 -0
  6. data/lib/restforce/db/associations/has_many.rb +37 -0
  7. data/lib/restforce/db/associations/has_one.rb +33 -0
  8. data/lib/restforce/db/dsl.rb +25 -13
  9. data/lib/restforce/db/mapping.rb +2 -3
  10. data/lib/restforce/db/record_types/active_record.rb +3 -8
  11. data/lib/restforce/db/record_types/salesforce.rb +23 -8
  12. data/lib/restforce/db/strategies/associated.rb +58 -0
  13. data/lib/restforce/db/version.rb +1 -1
  14. data/lib/restforce/db.rb +6 -1
  15. data/restforce-db.gemspec +0 -1
  16. data/test/cassettes/Restforce_DB_Associations_BelongsTo/with_an_inverse_mapping/_build/returns_an_associated_record_populated_with_the_Salesforce_attributes.yml +271 -0
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. data/test/cassettes/Restforce_DB_Associations_HasOne/with_an_inverse_mapping/_build/returns_an_associated_record_populated_with_the_Salesforce_attributes.yml +271 -0
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. data/test/lib/restforce/db/associations/belongs_to_test.rb +82 -0
  29. data/test/lib/restforce/db/associations/has_many_test.rb +96 -0
  30. data/test/lib/restforce/db/associations/has_one_test.rb +80 -0
  31. data/test/lib/restforce/db/dsl_test.rb +22 -3
  32. data/test/lib/restforce/db/mapping_test.rb +10 -8
  33. data/test/lib/restforce/db/record_types/active_record_test.rb +12 -5
  34. data/test/lib/restforce/db/runner_test.rb +2 -3
  35. data/test/lib/restforce/db/strategies/always_test.rb +2 -2
  36. data/test/lib/restforce/db/strategies/associated_test.rb +78 -0
  37. data/test/lib/restforce/db/strategies/passive_test.rb +1 -1
  38. data/test/lib/restforce/db/tracker_test.rb +0 -2
  39. data/test/support/active_record.rb +25 -2
  40. data/test/support/salesforce.rb +1 -1
  41. data/test/support/utilities.rb +5 -7
  42. data/test/test_helper.rb +0 -1
  43. metadata +24 -18
  44. data/lib/restforce/db/associations/active_record.rb +0 -68
  45. 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: dfc392eb1d43c78cb82873f3462c77f92040cac1
4
- data.tar.gz: aaa229b2207cbf203dad3fb10bb65d407afae437
3
+ metadata.gz: 8e6b77ef750532361625bbec55ad4a00e1ae4d21
4
+ data.tar.gz: b88d477b1e2a2ea7f2e301b35c54b08d77612bb4
5
5
  SHA512:
6
- metadata.gz: f35c317c7c078428840092490997eadad217291452f0087c720cc9930788a6063cff4ca430db559c3ec01082a0fcea21a3612199fc77abb09f04ab56ff9dfa1e
7
- data.tar.gz: 27c60936fdd1edc90989c184b29aa75664f0dcff228bd8b306b22a5958070632530344e5b908595a24a2100a4c714c5f379148a492c01fae317edb116c5034d3
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 :specialty, class_name: "Dish"
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
- belongs_to :specialty, through: %w(Specialty__c KeyIngredient__c)
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 Dish < ActiveRecord::Base
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("Dish__c", :passive) do
61
- has_one :restaurant, through: "Specialty__c"
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("Ingredient__c", :passive) do
69
- has_one :restaurant, through: "KeyIngredient__c"
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
- _Coming Soon_
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
- - `Specialty__c`, which corresponds to the `Dish__c` object type, and
121
- - `KeyIngredient__c`, which corresponds to the `Ingredient__c` object type
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 `:specialty`, with a `:through` argument referencing both of the Lookups used by the mappings on the associated `Dish` class.
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, `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.
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.Specialty__c` is a `Lookup(Dish__c)` field in Salesforce, while ` Restaurant__c.KeyIngredient__c` is a `Lookup(Ingredient__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
- _Coming Soon_
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
@@ -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
- # options - A Hash of options to pass to the association.
39
+ # through - A String or Array of Strings representing the Lookup IDs.
44
40
  #
45
41
  # Returns nothing.
46
- def belongs_to(association, options)
47
- @mapping.associations[association] = options[:through]
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
- # TODO: This should eventually be implemented as a full-fledged
54
- # association on the mapping. Something like:
55
- # @mapping.associate(association, Associations::HasOne, options)
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
- # options - A Hash of options to pass to the association.
67
+ # through - A String representing the Lookup ID.
59
68
  #
60
69
  # Returns nothing.
61
- def has_one(_association, options) # rubocop:disable PredicateName
62
- @mapping.through = options[:through]
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
@@ -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.values.flatten
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
- attributes = @mapping.convert(@record_type, from_record.attributes)
21
-
22
- record = @record_type.new(attributes.merge(@mapping.lookup_column => from_record.id))
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 corresponding to the passed id.
28
+ # Public: Find the first Salesforce record which meets the passed
29
+ # conditions.
29
30
  #
30
- # id - The id of the record in Salesforce.
31
+ # conditions - One or more String query conditions
31
32
  #
32
33
  # Returns nil or a Restforce::DB::Instances::Salesforce instance.
33
- def find(id)
34
- record = DB.client.query(query("Id = '#{id}'")).first
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 - A Time object defining the most recent update
45
- # timestamp for which records should be returned.
46
- # :after - A Time object defining the least recent update
47
- # timestamp for which records should be returned.
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|