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.
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|