pluckers 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +51 -0
  3. data/Appraisals +22 -0
  4. data/CHANGELOG +7 -0
  5. data/CODE_OF_CONDUCT.md +49 -0
  6. data/Gemfile +4 -0
  7. data/Gemfile.lock +56 -0
  8. data/LICENSE +674 -0
  9. data/README.md +40 -0
  10. data/Rakefile +10 -0
  11. data/circle.yml +15 -0
  12. data/doc/idea.md +49 -0
  13. data/doc/usage/basics.md +46 -0
  14. data/doc/usage/extending.md +109 -0
  15. data/doc/usage/globalize.md +54 -0
  16. data/doc/usage/relationships.md +216 -0
  17. data/doc/usage/renaming.md +26 -0
  18. data/lib/pluckers/base.rb +166 -0
  19. data/lib/pluckers/features/active_record_3_2/belongs_to_reflections.rb +16 -0
  20. data/lib/pluckers/features/active_record_3_2/globalize.rb +11 -0
  21. data/lib/pluckers/features/active_record_3_2/has_and_belongs_to_many_reflections.rb +40 -0
  22. data/lib/pluckers/features/active_record_3_2/has_many_reflections.rb +16 -0
  23. data/lib/pluckers/features/active_record_3_2/has_many_through_reflections.rb +17 -0
  24. data/lib/pluckers/features/active_record_3_2/has_one_reflections.rb +17 -0
  25. data/lib/pluckers/features/active_record_3_2/has_one_through_reflections.rb +17 -0
  26. data/lib/pluckers/features/active_record_3_2/pluck.rb +26 -0
  27. data/lib/pluckers/features/active_record_3_2/renaming.rb +11 -0
  28. data/lib/pluckers/features/active_record_3_2/simple_attributes.rb +11 -0
  29. data/lib/pluckers/features/active_record_3_2.rb +10 -0
  30. data/lib/pluckers/features/active_record_4_0/belongs_to_reflections.rb +16 -0
  31. data/lib/pluckers/features/active_record_4_0/globalize.rb +11 -0
  32. data/lib/pluckers/features/active_record_4_0/has_and_belongs_to_many_reflections.rb +40 -0
  33. data/lib/pluckers/features/active_record_4_0/has_many_reflections.rb +16 -0
  34. data/lib/pluckers/features/active_record_4_0/has_many_through_reflections.rb +17 -0
  35. data/lib/pluckers/features/active_record_4_0/has_one_reflections.rb +17 -0
  36. data/lib/pluckers/features/active_record_4_0/has_one_through_reflections.rb +17 -0
  37. data/lib/pluckers/features/active_record_4_0/pluck.rb +11 -0
  38. data/lib/pluckers/features/active_record_4_0/renaming.rb +11 -0
  39. data/lib/pluckers/features/active_record_4_0/simple_attributes.rb +11 -0
  40. data/lib/pluckers/features/active_record_4_0.rb +10 -0
  41. data/lib/pluckers/features/active_record_4_1/belongs_to_reflections.rb +16 -0
  42. data/lib/pluckers/features/active_record_4_1/globalize.rb +11 -0
  43. data/lib/pluckers/features/active_record_4_1/has_and_belongs_to_many_reflections.rb +40 -0
  44. data/lib/pluckers/features/active_record_4_1/has_many_reflections.rb +16 -0
  45. data/lib/pluckers/features/active_record_4_1/has_many_through_reflections.rb +17 -0
  46. data/lib/pluckers/features/active_record_4_1/has_one_reflections.rb +17 -0
  47. data/lib/pluckers/features/active_record_4_1/has_one_through_reflections.rb +17 -0
  48. data/lib/pluckers/features/active_record_4_1/pluck.rb +11 -0
  49. data/lib/pluckers/features/active_record_4_1/renaming.rb +11 -0
  50. data/lib/pluckers/features/active_record_4_1/simple_attributes.rb +11 -0
  51. data/lib/pluckers/features/active_record_4_1.rb +10 -0
  52. data/lib/pluckers/features/active_record_4_2/belongs_to_reflections.rb +15 -0
  53. data/lib/pluckers/features/active_record_4_2/globalize.rb +11 -0
  54. data/lib/pluckers/features/active_record_4_2/has_and_belongs_to_many_reflections.rb +39 -0
  55. data/lib/pluckers/features/active_record_4_2/has_many_reflections.rb +15 -0
  56. data/lib/pluckers/features/active_record_4_2/has_many_through_reflections.rb +17 -0
  57. data/lib/pluckers/features/active_record_4_2/has_one_reflections.rb +16 -0
  58. data/lib/pluckers/features/active_record_4_2/has_one_through_reflections.rb +17 -0
  59. data/lib/pluckers/features/active_record_4_2/pluck.rb +11 -0
  60. data/lib/pluckers/features/active_record_4_2/renaming.rb +11 -0
  61. data/lib/pluckers/features/active_record_4_2/simple_attributes.rb +11 -0
  62. data/lib/pluckers/features/active_record_4_2.rb +10 -0
  63. data/lib/pluckers/features/active_record_5_0/belongs_to_reflections.rb +15 -0
  64. data/lib/pluckers/features/active_record_5_0/globalize.rb +11 -0
  65. data/lib/pluckers/features/active_record_5_0/has_and_belongs_to_many_reflections.rb +39 -0
  66. data/lib/pluckers/features/active_record_5_0/has_many_reflections.rb +15 -0
  67. data/lib/pluckers/features/active_record_5_0/has_many_through_reflections.rb +17 -0
  68. data/lib/pluckers/features/active_record_5_0/has_one_reflections.rb +16 -0
  69. data/lib/pluckers/features/active_record_5_0/has_one_through_reflections.rb +17 -0
  70. data/lib/pluckers/features/active_record_5_0/pluck.rb +11 -0
  71. data/lib/pluckers/features/active_record_5_0/renaming.rb +11 -0
  72. data/lib/pluckers/features/active_record_5_0/simple_attributes.rb +11 -0
  73. data/lib/pluckers/features/active_record_5_0.rb +10 -0
  74. data/lib/pluckers/features/base/belongs_to_reflections.rb +131 -0
  75. data/lib/pluckers/features/base/globalize.rb +116 -0
  76. data/lib/pluckers/features/base/has_and_belongs_to_many_reflections.rb +190 -0
  77. data/lib/pluckers/features/base/has_many_reflections.rb +193 -0
  78. data/lib/pluckers/features/base/has_many_through_reflections.rb +131 -0
  79. data/lib/pluckers/features/base/has_one_reflections.rb +122 -0
  80. data/lib/pluckers/features/base/has_one_through_reflections.rb +129 -0
  81. data/lib/pluckers/features/base/pluck.rb +30 -0
  82. data/lib/pluckers/features/base/renaming.rb +55 -0
  83. data/lib/pluckers/features/base/simple_attributes.rb +64 -0
  84. data/lib/pluckers/version.rb +3 -0
  85. data/lib/pluckers.rb +7 -0
  86. data/pluckers.gemspec +38 -0
  87. metadata +236 -0
@@ -0,0 +1,116 @@
1
+ module Pluckers
2
+ ##
3
+ # This module groups diferent modules that will configure and build the
4
+ # results for a specific kind of information (attributes, translations,
5
+ # relations...)
6
+ #
7
+ # All this modules will have two methods, one for configuration (e.g, attributes
8
+ # to be included in the real pluck) and one for building the final results
9
+ module Features
10
+
11
+ module Base
12
+
13
+ ##
14
+ # This module implements plucking belongs_to relationships in a recursive
15
+ # way.
16
+ #
17
+ # The options used in this feature are:
18
+ #
19
+ # * attributes: Names of attributes of the objects to be plucked. This
20
+ # attributes should be the names of the translated attributes by Globalize.
21
+ #
22
+ # * attributes_with_locale: A hash when the key is a locale and the value
23
+ # is an array of attributes to pluck. As a result we will have a series of
24
+ # attributes with the name following the syntax attreibute_locale. E.g: The
25
+ # option could be { es: [:name], en: [:name, :location]} and we would obtain
26
+ # :name_es, :name_en and :location_en keys in the hash result
27
+
28
+ module Globalize
29
+
30
+
31
+ ##
32
+ # Here we include in the @attributes_to_pluck array the attribute names
33
+ # and SQL required for Globalize
34
+ def configure_query
35
+ super
36
+
37
+ return if @klass_reflections[:translations].nil?
38
+
39
+ if @options[:attributes]
40
+
41
+ # First we get those attributes received bia the attributes option
42
+ # that must be translated
43
+ plucker_attributes = @options[:attributes].map(&:to_sym)
44
+
45
+ klass_translated_attributes = @records.try(:translated_attribute_names) || []
46
+ klass_translated_attributes = klass_translated_attributes.map(&:to_sym)
47
+
48
+ @translated_attributes = plucker_attributes & klass_translated_attributes
49
+
50
+ # And we remove it from the attributes options, so the simple
51
+ # attributes feature don't receive them
52
+ @options[:attributes] = plucker_attributes - klass_translated_attributes
53
+
54
+ end
55
+
56
+ # And initialize with an empty array if we didn't receive any
57
+ @translated_attributes ||= []
58
+
59
+ # And then get the attributes that must be returned for a specific locale
60
+ @attributes_with_locale = @options[:attributes_with_locale] || {}
61
+
62
+ unless @translated_attributes.blank? && @attributes_with_locale.blank?
63
+
64
+ # We obtain the info about the translations relation
65
+ translation_table_name = @records.klass::Translation.table_name
66
+ translation_foreign_key = @klass_reflections[:translations].foreign_key
67
+
68
+ # And we perform a join for each locale
69
+ fallbacks = ::Globalize.fallbacks(::Globalize.locale)
70
+
71
+ fallbacks.each do |locale|
72
+ @records = @records.joins(
73
+ "LEFT OUTER JOIN #{translation_table_name} AS locale_#{locale}_translation ON (
74
+ #{@records.klass.table_name}.id = locale_#{locale}_translation.#{translation_foreign_key} AND
75
+ locale_#{locale}_translation.locale = '#{locale}'
76
+ )")
77
+
78
+ end
79
+
80
+ # The attribute to pluck must get the first non nil field
81
+ @attributes_to_pluck += @translated_attributes.map do |field|
82
+ {
83
+ name: field,
84
+ sql: "COALESCE(NULL, #{
85
+ fallbacks.map{|locale| "locale_#{locale}_translation.#{field}" }.join(',')
86
+ })"
87
+ }
88
+ end
89
+
90
+ # For the attributes with locale (name_es, name_en...)
91
+ @attributes_with_locale.each do |locale, attributes|
92
+
93
+ # We add the locales that are not fallback
94
+ unless fallbacks.include? locale
95
+ @records = @records.joins(
96
+ "LEFT OUTER JOIN #{translation_table_name} AS locale_#{locale}_translation ON (
97
+ #{@records.klass.table_name}.id = locale_#{locale}_translation.#{translation_foreign_key} AND
98
+ locale_#{locale}_translation.locale = '#{locale}'
99
+ )")
100
+ end
101
+
102
+ # And we add the attribute to be plucked
103
+ @attributes_to_pluck += attributes.map do |field|
104
+ {
105
+ name: "#{field}_#{locale}".to_sym,
106
+ sql: "locale_#{locale}_translation.#{field}"
107
+ }
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,190 @@
1
+ module Pluckers
2
+ ##
3
+ # This module groups diferent modules that will configure and build the
4
+ # results for a specific kind of information (attributes, translations,
5
+ # relations...)
6
+ #
7
+ # All this modules will have two methods, one for configuration (e.g, attributes
8
+ # to be included in the real pluck) and one for building the final results
9
+ module Features
10
+
11
+ module Base
12
+
13
+ ##
14
+ # This module implements plucking has_and_belongs_to_many relationships in a recursive
15
+ # way.
16
+ #
17
+ # The options used in this feature are:
18
+ #
19
+ # * reflections: A hash of the reflections we will pluck recursively. The
20
+ # key of this hash will be the name of the reflection and the value is
21
+ # another hash of options.
22
+ #
23
+ # - scope: You can limit the scope of the objects plucked. E.g, you
24
+ # could use Author.active instead of Author.all. Notice that .all is
25
+ # the default.
26
+ #
27
+ # - plucker: You can use a custom plucker instead of Pluckers::Base in
28
+ # case you want any specific logic. Pluckers::Base is the default one.
29
+ #
30
+ # - Any other option will be passed to the plucker, so you can send any
31
+ # other regular option such as attributes, custom ones or even more
32
+ # reflections. Recursivity FTW!!
33
+ #
34
+ module HasAndBelongsToManyReflections
35
+
36
+
37
+ ##
38
+ # Here we obtain the has_many reflections to include in the pluck
39
+ # operation and also include the relation foreign key in the attributes to
40
+ # pluck for this model.
41
+ def configure_query
42
+ super
43
+
44
+ pluck_reflections = @options[:reflections] || {}
45
+
46
+ return if pluck_reflections.blank?
47
+
48
+ @has_and_belongs_to_many_reflections = { }
49
+
50
+ # We iterate through the class reflections passed as options
51
+ @klass_reflections.slice(*pluck_reflections.keys).
52
+ # And select those that are HasMany
53
+ select{|_, r| active_record_has_and_belongs_to_many_reflection?(r) }.
54
+ # And store them in the has_many_reflection hash that will be used later
55
+ each do |name, reflection|
56
+ name = name.to_sym
57
+ @has_and_belongs_to_many_reflections[name] = pluck_reflections[name]
58
+ end
59
+
60
+ end
61
+
62
+ ##
63
+ # In this method we get the reflections and for each one creates and
64
+ # executes a new plucker.
65
+ #
66
+ # This pluck gives the whole process a recursive character and options
67
+ # for that plucker may be passed in the options hash.
68
+ def build_results
69
+ super
70
+
71
+ return if @has_and_belongs_to_many_reflections.blank?
72
+
73
+ build_only_ids_has_and_belongs_to_many_reflections
74
+ build_complete_has_and_belongs_to_many_reflections
75
+
76
+ end
77
+
78
+ ##
79
+ # This method build the reflections completely, creating hashes for each record, etc.
80
+ #
81
+ # It searches reflections that has not the :only_ids option enabled and
82
+ # then creates pluckers for them.
83
+ private def build_complete_has_and_belongs_to_many_reflections
84
+
85
+ @has_and_belongs_to_many_reflections.reject {|_, reflection| reflection[:only_ids] }.each do |name, reflection|
86
+ # As an example we will imagine that we are plucking Authors and
87
+ # this relation is the :posts
88
+
89
+ # We get the meta information about the reflection
90
+ klass_reflection = @klass_reflections[name]
91
+
92
+ # initialize some options such as the plucker or the scope of the pluck
93
+ scope = reflection[:scope] || klass_reflection.klass.send(all_method)
94
+ plucker = reflection[:plucker] || Pluckers::Base
95
+
96
+ # We will use the _ids already fetched to check which records we should pluck
97
+ ids_reflection_name = "#{name.to_s.singularize}_ids".to_sym
98
+
99
+ ids_to_query = @results.map do |_, result|
100
+ result[ids_reflection_name]
101
+ end
102
+
103
+ ids_to_query = ids_to_query.flatten
104
+
105
+
106
+
107
+ # And now we create the plucker. Notice that we add a where to the
108
+ # scope, so we filter the records to pluck as we only get those with
109
+ # an id in the set of the _ids arrays already plucked
110
+ #
111
+ # In our Example we would be doing something like
112
+ # Category.all.where(id: category_ids)
113
+ reflection_plucker = plucker.new scope.where(id: ids_to_query), reflection
114
+
115
+ # We initialize so we return an empty array if there are no record
116
+ # related
117
+ @results.each do |_, result|
118
+ result[name] ||= []
119
+ end
120
+
121
+
122
+ reflection_plucker.pluck.each do |r|
123
+ @results.each do |_,result|
124
+ # For each related result (category) we search those records
125
+ # (BlogPost) that include the category id in its _ids array
126
+ if result[ids_reflection_name].include? r[:id].to_i
127
+ result[name] << r
128
+ end
129
+ end
130
+ end
131
+
132
+ # And now we get rid of duplicates
133
+ @results.each do |_,result|
134
+ result[name].uniq!
135
+ end
136
+
137
+ end
138
+ end
139
+
140
+ ##
141
+ # This method build the ids for the records instead of creating the hashes.
142
+ #
143
+ # Unlike the has_many relationships, we don't search reflections that
144
+ # has the :only_ids option enabled as we will need these ids also for
145
+ # the other relationships.
146
+ private def build_only_ids_has_and_belongs_to_many_reflections
147
+
148
+ @has_and_belongs_to_many_reflections.each do |name, reflection|
149
+ # As an example we will imagine that we are plucking BlogPosts and
150
+ # this relation is the :categories one
151
+
152
+ # We get the meta information about the reflection
153
+ klass_reflection = @klass_reflections[name]
154
+
155
+ # We get the ids. This query is dependant on the ActiveRecord
156
+ # version, so every feature version has a different implementation
157
+ join_results = has_and_belongs_to_many_ids(klass_reflection)
158
+
159
+ ids_reflection_name = "#{name.to_s.singularize}_ids".to_sym
160
+
161
+ # Next, we initialize the _ids array for each result
162
+ @results.each do |_, result|
163
+ result[ids_reflection_name] ||= []
164
+ end
165
+
166
+ # And now, the foreign_keys.
167
+ # In our example with BlogPost and Category they would be:
168
+ # model_foreign_key = blog_post_id
169
+ # related_model_foreign_key = category_id
170
+ model_foreign_key = klass_reflection.foreign_key
171
+ related_model_foreign_key = klass_reflection.association_foreign_key
172
+
173
+ # And for each result we fill the results
174
+ join_results.each do |r|
175
+ @results[r[model_foreign_key].to_i][ids_reflection_name] << r[related_model_foreign_key].to_i
176
+ end
177
+
178
+ # And eliminate duplicates
179
+ @results.each do |_,result|
180
+ result[ids_reflection_name].uniq!
181
+ end
182
+
183
+ end
184
+
185
+ end
186
+
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,193 @@
1
+ module Pluckers
2
+ ##
3
+ # This module groups diferent modules that will configure and build the
4
+ # results for a specific kind of information (attributes, translations,
5
+ # relations...)
6
+ #
7
+ # All this modules will have two methods, one for configuration (e.g, attributes
8
+ # to be included in the real pluck) and one for building the final results
9
+ module Features
10
+
11
+ module Base
12
+
13
+ ##
14
+ # This module implements plucking has_many relationships in a recursive
15
+ # way.
16
+ #
17
+ # The options used in this feature are:
18
+ #
19
+ # * reflections: A hash of the reflections we will pluck recursively. The
20
+ # key of this hash will be the name of the reflection and the value is
21
+ # another hash of options.
22
+ #
23
+ # - scope: You can limit the scope of the objects plucked. E.g, you
24
+ # could use Author.active instead of Author.all. Notice that .all is
25
+ # the default.
26
+ #
27
+ # - plucker: You can use a custom plucker instead of Pluckers::Base in
28
+ # case you want any specific logic. Pluckers::Base is the default one.
29
+ #
30
+ # - only_ids: We can get the _ids array instead of an array with hashes
31
+ # if we pass this option as true. If we do any fields or plucker
32
+ # option will be ignored.
33
+ #
34
+ # - Any other option will be passed to the plucker, so you can send any
35
+ # other regular option such as attributes, custom ones or even more
36
+ # reflections. Recursivity FTW!!
37
+ #
38
+ module HasManyReflections
39
+
40
+
41
+ ##
42
+ # Here we obtain the has_many reflections to include in the pluck
43
+ # operation and also include the relation foreign key in the attributes to
44
+ # pluck for this model.
45
+ def configure_query
46
+ super
47
+
48
+ pluck_reflections = @options[:reflections] || {}
49
+
50
+ return if pluck_reflections.blank?
51
+
52
+ @has_many_reflections = { }
53
+
54
+ # We iterate through the class reflections passed as options
55
+ @klass_reflections.slice(*pluck_reflections.keys).
56
+ # And select those that are HasMany
57
+ select{|_, r| active_record_has_many_reflection?(r)}.
58
+ # And store them in the has_many_reflection hash that will be used later
59
+ each do |name, reflection|
60
+ name = name.to_sym
61
+ @has_many_reflections[name] = pluck_reflections[name]
62
+ end
63
+
64
+ end
65
+
66
+ ##
67
+ # In this method we get the reflections and for each one creates and
68
+ # executes a new plucker.
69
+ #
70
+ # This pluck gives the whole process a recursive character and options
71
+ # for that plucker may be passed in the options hash.
72
+ def build_results
73
+ super
74
+
75
+ return if @has_many_reflections.blank?
76
+
77
+ build_complete_reflections
78
+ build_only_ids_reflections
79
+
80
+ end
81
+
82
+ ##
83
+ # This method build the reflections completely, creating hashes for each record, etc.
84
+ #
85
+ # It searches reflections that has not the :only_ids option enabled and
86
+ # then creates pluckers for them.
87
+ private def build_complete_reflections
88
+
89
+ @has_many_reflections.reject {|_, reflection| reflection[:only_ids] }.each do |name, reflection|
90
+ # As an example we will imagine that we are plucking Authors and
91
+ # this relation is the :posts
92
+
93
+ # We get the meta information about the reflection
94
+ klass_reflection = @klass_reflections[name]
95
+
96
+
97
+ # initialize some options such as the plucker or the scope of the pluck
98
+ scope = reflection[:scope] || klass_reflection.klass.send(all_method)
99
+ plucker = reflection[:plucker] || Pluckers::Base
100
+
101
+ # If there are attributes configured to be plucked we add the foreign
102
+ # key as we will need it to relate the records
103
+ reflection[:attributes] |= [klass_reflection.foreign_key.to_sym] if reflection[:attributes]
104
+
105
+
106
+ # And now we create the plucker. Notice that we add a where to the
107
+ # scope, so we filter the records to pluck as we only get those with
108
+ # an id in the set of the foreign keys of the records already
109
+ # plucked by the base plucker
110
+ #
111
+ # In our Example we would be doing something like
112
+ # BlogPost.all.where(author_id: author_ids)
113
+ reflection_plucker = plucker.new scope.where(
114
+ klass_reflection.foreign_key => @results.map{|_, r| r[klass_reflection.active_record_primary_key.to_sym] }
115
+ ),
116
+ reflection
117
+
118
+ # We initialize so we return an empty array if there are no record
119
+ # related
120
+ @results.each do |_, result|
121
+ result[name] ||= []
122
+ end
123
+
124
+ reflection_plucker.pluck.each do |r|
125
+ @results.each do |_,result|
126
+ # For each related result (Author) we search those records
127
+ # (BlogPost) that are related (author.id == post.author_id) and
128
+ # insert them in the relationship attributes
129
+ if result[klass_reflection.active_record_primary_key.to_sym] == r[klass_reflection.foreign_key.to_sym]
130
+ result[name] << r
131
+ end
132
+ end
133
+ end
134
+
135
+ end
136
+ end
137
+
138
+ ##
139
+ # This method build the ids for the records instead of creating the hashes.
140
+ #
141
+ # It searches reflections that has the :only_ids option enabled and then
142
+ # creates pluckers for them.
143
+ private def build_only_ids_reflections
144
+
145
+ @has_many_reflections.select {|_, reflection| reflection[:only_ids] }.each do |name, reflection|
146
+ # As an example we will imagine that we are plucking Authors and
147
+ # this relation is the :posts
148
+
149
+ # We get the meta information about the reflection
150
+ klass_reflection = @klass_reflections[name]
151
+
152
+ # We can send an scope option for filtering the related records
153
+ scope = reflection[:scope] || klass_reflection.klass.send(all_method)
154
+
155
+ # We override the attributes as we only get the required ones for
156
+ # relating the records
157
+ reflection[:attributes] = [klass_reflection.foreign_key.to_sym, klass_reflection.active_record_primary_key.to_sym]
158
+
159
+ # And now, create the plucker, filtering the records so we only get
160
+ # the related ones
161
+ reflection_plucker = Pluckers::Base.new scope.where(
162
+ klass_reflection.foreign_key => @results.map{|_, r| r[klass_reflection.active_record_primary_key.to_sym] }
163
+ ),
164
+ reflection
165
+
166
+ # Now we create the _ids attribute for each result
167
+ ids_reflection_name = "#{name.to_s.singularize}_ids".to_sym
168
+
169
+ @results.each do |_, result|
170
+ result[ids_reflection_name] ||= []
171
+ end
172
+
173
+
174
+ reflection_plucker.pluck.each do |r|
175
+ @results.
176
+ each do |_,result|
177
+ # For each related result (Author) we search those records
178
+ # (BlogPost) that are related (author.id == post.author_id) and
179
+ # insert the id in the _ids array
180
+ if result[klass_reflection.active_record_primary_key.to_sym] == r[klass_reflection.foreign_key.to_sym]
181
+ result[ids_reflection_name] << r[klass_reflection.active_record_primary_key.to_sym]
182
+ end
183
+ end
184
+ end
185
+
186
+ end
187
+
188
+ end
189
+
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,131 @@
1
+ module Pluckers
2
+ ##
3
+ # This module groups diferent modules that will configure and build the
4
+ # results for a specific kind of information (attributes, translations,
5
+ # relations...)
6
+ #
7
+ # All this modules will have two methods, one for configuration (e.g, attributes
8
+ # to be included in the real pluck) and one for building the final results
9
+ module Features
10
+
11
+ module Base
12
+
13
+ ##
14
+ # This module implements plucking has_many :through relationships in a
15
+ # recursive way.
16
+ #
17
+ # The options used in this feature are:
18
+ #
19
+ # * reflections: A hash of the reflections we will pluck recursively. The
20
+ # key of this hash will be the name of the reflection and the value is
21
+ # another hash of options.
22
+ #
23
+ # - scope: You can limit the scope of the objects plucked. E.g, you
24
+ # could use Author.active instead of Author.all. Notice that .all is
25
+ # the default.
26
+ #
27
+ # - plucker: You can use a custom plucker instead of Pluckers::Base in
28
+ # case you want any specific logic. Pluckers::Base is the default one.
29
+ #
30
+ # - Any other option will be passed to the plucker, so you can send any
31
+ # other regular option such as attributes, custom ones or even more
32
+ # reflections. Recursivity FTW!!
33
+ #
34
+ module HasManyThroughReflections
35
+
36
+
37
+ ##
38
+ # Here we obtain the has_many :through reflections to include in the pluck
39
+ # operation and also include the relation foreign key in the attributes to
40
+ # pluck for this model.
41
+ def configure_query
42
+ super
43
+
44
+ pluck_reflections = @options[:reflections] || {}
45
+
46
+ return if pluck_reflections.blank?
47
+
48
+ @has_many_through_reflections = { }
49
+
50
+ # We iterate through the class reflections passed as options
51
+ @klass_reflections.slice(*pluck_reflections.keys).
52
+ # And select those that are Through and which delegate reflection is a HasMany
53
+ select{|_, r| active_record_has_many_through_reflection?(r)}.
54
+ # And store them in the has_many_reflection hash that will be used later
55
+ each do |name, reflection|
56
+ name = name.to_sym
57
+ @has_many_through_reflections[name] = pluck_reflections[name]
58
+ end
59
+ end
60
+
61
+ ##
62
+ # In this method we get the reflections and for each one creates and
63
+ # executes a new plucker.
64
+ #
65
+ # This pluck gives the whole process a recursive character and options
66
+ # for that plucker may be passed in the options hash.
67
+ def build_results
68
+ super
69
+
70
+ return if @has_many_through_reflections.blank?
71
+
72
+ @has_many_through_reflections.each do |name, reflection|
73
+ # As an example we will imagine that we are plucking Authors and
74
+ # this relation is the :references, that is a relationship through
75
+ # BlogPost
76
+
77
+ # We get the meta information about the :through reflection (:references)
78
+ klass_reflection = @klass_reflections[name]
79
+
80
+ # And also the has_many reflection (:posts) as we wiil need it to fetch information
81
+ reflection_to_pluck = klass_reflection.chain.reverse.first
82
+
83
+ # initialize some options such as the plucker or the scope of the pluck
84
+ scope = reflection_to_pluck.klass.send(all_method)
85
+
86
+ # Essentially we are going to pluck the has_many relationship and
87
+ # add the reflections option so it recursively plucks the has_many
88
+ # :references reflection from BlogPost.
89
+ plucker = reflection[:plucker] || Pluckers::Base
90
+ plucker_options = {
91
+ attributes: [reflection_to_pluck.foreign_key.to_sym],
92
+ reflections: { klass_reflection.source_reflection.name => reflection }
93
+ }
94
+
95
+ # In order to create this intermediary plucker we add a where to the
96
+ # scope, so we filter the records to pluck as we only get those with
97
+ # an id in the set of the foreign keys of the records already
98
+ # plucked by the base plucker
99
+ #
100
+ # In our Example we would be doing something like
101
+ # BlogPost.all.where(author_id: author_ids)
102
+ reflection_plucker = plucker.new scope.where(
103
+ reflection_to_pluck.foreign_key => @results.map{|_, r| r[reflection_to_pluck.active_record_primary_key.to_sym] }
104
+ ),
105
+ plucker_options
106
+
107
+ # We initialize so we return an empty array if there are no record
108
+ # related
109
+ @results.each do |_, result|
110
+ result[name] ||= []
111
+ end
112
+
113
+ reflection_plucker.pluck.each do |r|
114
+ @results.each do |_,result|
115
+ # For each related result (BlogPost) we search those records
116
+ # (Author) that are related (author.id == post.author_id) and
117
+ # insert not the record itself but the desired reflection in the
118
+ # result
119
+ if result[reflection_to_pluck.active_record_primary_key.to_sym] == r[reflection_to_pluck.foreign_key.to_sym] &&
120
+ r[klass_reflection.source_reflection.name]
121
+
122
+ result[name] += [r[klass_reflection.source_reflection.name]].flatten
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end