quo 0.6.0 → 1.0.0.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.standard.yml +4 -1
- data/Appraisals +15 -0
- data/CHANGELOG.md +78 -0
- data/Gemfile +6 -4
- data/LICENSE.txt +1 -1
- data/README.md +222 -232
- data/Steepfile +0 -2
- data/gemfiles/rails_7.0.gemfile +15 -0
- data/gemfiles/rails_7.1.gemfile +15 -0
- data/gemfiles/rails_7.2.gemfile +15 -0
- data/gemfiles/rails_8.0.gemfile +15 -0
- data/lib/quo/collection_backed_query.rb +87 -0
- data/lib/quo/collection_results.rb +44 -0
- data/lib/quo/composed_query.rb +278 -0
- data/lib/quo/engine.rb +11 -0
- data/lib/quo/minitest/helpers.rb +41 -0
- data/lib/quo/preloadable.rb +46 -0
- data/lib/quo/query.rb +97 -215
- data/lib/quo/relation_backed_query.rb +150 -0
- data/lib/quo/relation_backed_query_specification.rb +154 -0
- data/lib/quo/relation_results.rb +58 -0
- data/lib/quo/results.rb +48 -44
- data/lib/quo/rspec/helpers.rb +31 -9
- data/lib/quo/testing/collection_backed_fake.rb +29 -0
- data/lib/quo/testing/relation_backed_fake.rb +52 -0
- data/lib/quo/version.rb +3 -1
- data/lib/quo.rb +23 -30
- data/rbs_collection.yaml +0 -2
- data/sig/generated/quo/collection_backed_query.rbs +39 -0
- data/sig/generated/quo/collection_results.rbs +30 -0
- data/sig/generated/quo/composed_query.rbs +112 -0
- data/sig/generated/quo/engine.rbs +6 -0
- data/sig/generated/quo/preloadable.rbs +29 -0
- data/sig/generated/quo/query.rbs +98 -0
- data/sig/generated/quo/relation_backed_query.rbs +67 -0
- data/sig/generated/quo/relation_backed_query_specification.rbs +94 -0
- data/sig/generated/quo/relation_results.rbs +38 -0
- data/sig/generated/quo/results.rbs +39 -0
- data/sig/generated/quo/testing/collection_backed_fake.rbs +13 -0
- data/sig/generated/quo/testing/relation_backed_fake.rbs +23 -0
- data/sig/generated/quo/version.rbs +5 -0
- data/sig/generated/quo.rbs +9 -0
- data/sig/literal.rbs +7 -0
- metadata +77 -37
- data/lib/quo/eager_query.rb +0 -51
- data/lib/quo/loaded_query.rb +0 -18
- data/lib/quo/merged_query.rb +0 -36
- data/lib/quo/query_composer.rb +0 -78
- data/lib/quo/railtie.rb +0 -7
- data/lib/quo/utilities/callstack.rb +0 -21
- data/lib/quo/utilities/compose.rb +0 -18
- data/lib/quo/utilities/sanitize.rb +0 -19
- data/lib/quo/utilities/wrap.rb +0 -23
- data/lib/quo/wrapped_query.rb +0 -18
- data/sig/quo/eager_query.rbs +0 -15
- data/sig/quo/loaded_query.rbs +0 -7
- data/sig/quo/merged_query.rbs +0 -19
- data/sig/quo/query.rbs +0 -83
- data/sig/quo/query_composer.rbs +0 -32
- data/sig/quo/results.rbs +0 -22
- data/sig/quo/utilities/callstack.rbs +0 -7
- data/sig/quo/utilities/compose.rbs +0 -8
- data/sig/quo/utilities/sanitize.rbs +0 -9
- data/sig/quo/utilities/wrap.rbs +0 -11
- data/sig/quo/wrapped_query.rbs +0 -11
- data/sig/quo.rbs +0 -41
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rbs_inline: enabled
|
4
|
+
|
5
|
+
module Quo
|
6
|
+
class CollectionBackedQuery < Query
|
7
|
+
prop :total_count, _Nilable(Integer), reader: false
|
8
|
+
|
9
|
+
# Wrap an enumerable collection or a block that returns an enumerable collection
|
10
|
+
# @rbs data: untyped, props: Symbol => untyped, block: () -> untyped
|
11
|
+
# @rbs return: Quo::CollectionBackedQuery
|
12
|
+
def self.wrap(data = nil, props: {}, &block)
|
13
|
+
klass = Class.new(self) do
|
14
|
+
props.each do |name, property|
|
15
|
+
if property.is_a?(Literal::Property)
|
16
|
+
prop name, property.type, property.kind, reader: property.reader, writer: property.writer, default: property.default
|
17
|
+
else
|
18
|
+
prop name, property
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
if block
|
23
|
+
klass.define_method(:collection, &block)
|
24
|
+
elsif data
|
25
|
+
klass.define_method(:collection) { data }
|
26
|
+
else
|
27
|
+
raise ArgumentError, "either a query or a block must be provided"
|
28
|
+
end
|
29
|
+
# klass.set_temporary_name = "quo::Wrapper" # Ruby 3.3+
|
30
|
+
klass
|
31
|
+
end
|
32
|
+
|
33
|
+
# @rbs return: Object & Enumerable[untyped]
|
34
|
+
def collection
|
35
|
+
raise NotImplementedError, "Collection backed query objects must define a 'collection' method"
|
36
|
+
end
|
37
|
+
|
38
|
+
# The default implementation of `query` just calls `collection`, however you can also
|
39
|
+
# override this method to return an ActiveRecord::Relation or any other query-like object as usual in a Query object.
|
40
|
+
# @rbs return: Object & Enumerable[untyped]
|
41
|
+
def query
|
42
|
+
collection
|
43
|
+
end
|
44
|
+
|
45
|
+
def results
|
46
|
+
Quo::CollectionResults.new(self, transformer: transformer, total_count: @total_count)
|
47
|
+
end
|
48
|
+
|
49
|
+
# @rbs override
|
50
|
+
def relation?
|
51
|
+
false
|
52
|
+
end
|
53
|
+
|
54
|
+
# @rbs override
|
55
|
+
def collection?
|
56
|
+
true
|
57
|
+
end
|
58
|
+
|
59
|
+
# @rbs override
|
60
|
+
def to_collection
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def validated_query
|
67
|
+
query
|
68
|
+
end
|
69
|
+
|
70
|
+
# @rbs return: Object & Enumerable[untyped]
|
71
|
+
def underlying_query
|
72
|
+
validated_query
|
73
|
+
end
|
74
|
+
|
75
|
+
# The configured query is the underlying query with paging
|
76
|
+
def configured_query #: Object & Enumerable[untyped]
|
77
|
+
q = underlying_query
|
78
|
+
return q unless paged?
|
79
|
+
|
80
|
+
if q.respond_to?(:[])
|
81
|
+
q[offset, sanitised_page_size]
|
82
|
+
else
|
83
|
+
q
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rbs_inline: enabled
|
4
|
+
|
5
|
+
module Quo
|
6
|
+
class CollectionResults < Results
|
7
|
+
# @rbs override
|
8
|
+
def initialize(query, transformer: nil, total_count: nil)
|
9
|
+
raise ArgumentError, "Query must be a CollectionBackedQuery" unless query.is_a?(Quo::CollectionBackedQuery)
|
10
|
+
@total_count = total_count
|
11
|
+
@query = query
|
12
|
+
@configured_query = query.unwrap
|
13
|
+
@transformer = transformer
|
14
|
+
end
|
15
|
+
|
16
|
+
# Are there any results for this query?
|
17
|
+
def exists? #: bool
|
18
|
+
@configured_query.present?
|
19
|
+
end
|
20
|
+
|
21
|
+
def empty? #: bool
|
22
|
+
!exists?
|
23
|
+
end
|
24
|
+
|
25
|
+
# Gets the count of all results ignoring the current page and page size (if set).
|
26
|
+
# Optionally return the `total_count` option if it has been set.
|
27
|
+
# This is useful when the total count is known and not equal to size
|
28
|
+
# of wrapped collection.
|
29
|
+
# @rbs override
|
30
|
+
def total_count #: Integer
|
31
|
+
@total_count || @query.unwrap_unpaginated.size
|
32
|
+
end
|
33
|
+
|
34
|
+
# Gets the actual count of elements in the page of results (assuming paging is being used, otherwise the count of
|
35
|
+
# all results)
|
36
|
+
def page_count #: Integer
|
37
|
+
@configured_query.size
|
38
|
+
end
|
39
|
+
|
40
|
+
# @rbs @query: Quo::CollectionBackedQuery
|
41
|
+
# @rbs @transformer: (^(untyped, ?Integer) -> untyped)?
|
42
|
+
# @rbs @configured_query: Object & Enumerable[untyped]
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,278 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rbs_inline: enabled
|
4
|
+
|
5
|
+
module Quo
|
6
|
+
module ComposedQuery
|
7
|
+
# Combine two Query classes into a new composed query class
|
8
|
+
# Combine two query-like or composeable entities:
|
9
|
+
# These can be Quo::Query, Quo::ComposedQuery, Quo::CollectionBackedQuery and ActiveRecord::Relations.
|
10
|
+
# See the `README.md` docs for more details.
|
11
|
+
# @rbs chosen_superclass: singleton(Quo::RelationBackedQuery | Quo::CollectionBackedQuery)
|
12
|
+
# @rbs left_query_class: singleton(Quo::Query | ::ActiveRecord::Relation)
|
13
|
+
# @rbs right_query_class: singleton(Quo::Query | ::ActiveRecord::Relation)
|
14
|
+
# @rbs joins: untyped
|
15
|
+
# @rbs return: singleton(Quo::ComposedQuery)
|
16
|
+
def composer(chosen_superclass, left_query_class, right_query_class, joins: nil)
|
17
|
+
validate_query_classes(left_query_class, right_query_class)
|
18
|
+
|
19
|
+
props = collect_properties(left_query_class, right_query_class)
|
20
|
+
klass = create_composed_class(chosen_superclass, props)
|
21
|
+
|
22
|
+
assign_query_metadata(klass, left_query_class, right_query_class, joins)
|
23
|
+
klass
|
24
|
+
end
|
25
|
+
module_function :composer
|
26
|
+
|
27
|
+
# We can also merge instance of prepared queries
|
28
|
+
# @rbs left_instance: Quo::Query | ::ActiveRecord::Relation
|
29
|
+
# @rbs right_instance: Quo::Query | ::ActiveRecord::Relation
|
30
|
+
# @rbs joins: untyped
|
31
|
+
# @rbs return: Quo::ComposedQuery
|
32
|
+
def merge_instances(left_instance, right_instance, joins: nil)
|
33
|
+
validate_instances(left_instance, right_instance)
|
34
|
+
|
35
|
+
if left_instance.is_a?(Quo::Query) && right_instance.is_a?(::ActiveRecord::Relation)
|
36
|
+
return merge_query_and_relation(left_instance, right_instance, joins)
|
37
|
+
elsif right_instance.is_a?(Quo::Query) && left_instance.is_a?(::ActiveRecord::Relation)
|
38
|
+
return merge_relation_and_query(left_instance, right_instance, joins)
|
39
|
+
elsif left_instance.is_a?(Quo::Query) && right_instance.is_a?(Quo::Query)
|
40
|
+
return merge_query_instances(left_instance, right_instance, joins)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Both are AR relations
|
44
|
+
composer(Quo.relation_backed_query_base_class, left_instance, right_instance, joins: joins).new
|
45
|
+
end
|
46
|
+
module_function :merge_instances
|
47
|
+
|
48
|
+
# @rbs override
|
49
|
+
def query
|
50
|
+
merge_left_and_right
|
51
|
+
end
|
52
|
+
|
53
|
+
# @rbs override
|
54
|
+
def inspect
|
55
|
+
klass_name = is_a?(Quo::RelationBackedQuery) ? Quo::RelationBackedQuery.name : Quo::CollectionBackedQuery.name
|
56
|
+
"#{klass_name}<Quo::ComposedQuery>[#{self.class.quo_operand_desc(left.class)}, #{self.class.quo_operand_desc(right.class)}](#{super})"
|
57
|
+
end
|
58
|
+
|
59
|
+
class << self
|
60
|
+
private
|
61
|
+
|
62
|
+
# @rbs left_query_class: singleton(Quo::Query | ::ActiveRecord::Relation)
|
63
|
+
# @rbs right_query_class: singleton(Quo::Query | ::ActiveRecord::Relation)
|
64
|
+
def validate_query_classes(left_query_class, right_query_class)
|
65
|
+
unless left_query_class.respond_to?(:<) && right_query_class.respond_to?(:<)
|
66
|
+
raise ArgumentError, "Cannot compose #{left_query_class} and #{right_query_class}, are they both classes? If you want to use instances use `.merge_instances`"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# @rbs left_query_class: singleton(Quo::Query | ::ActiveRecord::Relation)
|
71
|
+
# @rbs right_query_class: singleton(Quo::Query | ::ActiveRecord::Relation)
|
72
|
+
def collect_properties(left_query_class, right_query_class)
|
73
|
+
props = {}
|
74
|
+
props.merge!(left_query_class.literal_properties.properties_index) if left_query_class < Quo::Query
|
75
|
+
props.merge!(right_query_class.literal_properties.properties_index) if right_query_class < Quo::Query
|
76
|
+
props
|
77
|
+
end
|
78
|
+
|
79
|
+
def create_composed_class(chosen_superclass, props)
|
80
|
+
Class.new(chosen_superclass) do
|
81
|
+
include Quo::ComposedQuery
|
82
|
+
|
83
|
+
class << self
|
84
|
+
attr_reader :_composing_joins, :_left_query, :_right_query
|
85
|
+
|
86
|
+
def inspect
|
87
|
+
left_desc = quo_operand_desc(_left_query)
|
88
|
+
right_desc = quo_operand_desc(_right_query)
|
89
|
+
klass_name = determine_class_name
|
90
|
+
"#{klass_name}<Quo::ComposedQuery>[#{left_desc}, #{right_desc}]"
|
91
|
+
end
|
92
|
+
|
93
|
+
# @rbs operand: Quo::ComposedQuery | Quo::Query | ::ActiveRecord::Relation
|
94
|
+
# @rbs return: String
|
95
|
+
def quo_operand_desc(operand)
|
96
|
+
if operand < Quo::ComposedQuery
|
97
|
+
operand.inspect
|
98
|
+
else
|
99
|
+
operand.name || operand.superclass&.name || "(anonymous)"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
# @rbs return: String
|
106
|
+
def determine_class_name
|
107
|
+
if self < Quo::RelationBackedQuery
|
108
|
+
Quo.relation_backed_query_base_class.name
|
109
|
+
else
|
110
|
+
Quo.collection_backed_query_base_class.name
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
props.each do |name, property|
|
116
|
+
prop(
|
117
|
+
name,
|
118
|
+
property.type,
|
119
|
+
property.kind,
|
120
|
+
reader: property.reader,
|
121
|
+
writer: property.writer,
|
122
|
+
default: property.default
|
123
|
+
)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# @rbs klass: Class
|
129
|
+
# @rbs left_query_class: singleton(Quo::Query | ::ActiveRecord::Relation)
|
130
|
+
# @rbs right_query_class: singleton(Quo::Query | ::ActiveRecord::Relation)
|
131
|
+
# @rbs joins: untyped
|
132
|
+
def assign_query_metadata(klass, left_query_class, right_query_class, joins)
|
133
|
+
klass.instance_variable_set(:@_composing_joins, joins)
|
134
|
+
klass.instance_variable_set(:@_left_query, left_query_class)
|
135
|
+
klass.instance_variable_set(:@_right_query, right_query_class)
|
136
|
+
end
|
137
|
+
|
138
|
+
# @rbs left_instance: Quo::Query | ::ActiveRecord::Relation
|
139
|
+
# @rbs right_instance: Quo::Query | ::ActiveRecord::Relation
|
140
|
+
def validate_instances(left_instance, right_instance)
|
141
|
+
unless left_instance.is_a?(Quo::Query) || left_instance.is_a?(::ActiveRecord::Relation)
|
142
|
+
raise ArgumentError, "Cannot merge, left has incompatible type #{left_instance.class}"
|
143
|
+
end
|
144
|
+
|
145
|
+
unless right_instance.is_a?(Quo::Query) || right_instance.is_a?(::ActiveRecord::Relation)
|
146
|
+
raise ArgumentError, "Cannot merge, right has incompatible type #{right_instance.class}"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# @rbs relation: ::ActiveRecord::Relation
|
151
|
+
# @rbs query: Quo::Query
|
152
|
+
# @rbs joins: untyped
|
153
|
+
def merge_query_and_relation(query, relation, joins)
|
154
|
+
base_class = query.is_a?(Quo::RelationBackedQuery) ?
|
155
|
+
Quo.relation_backed_query_base_class :
|
156
|
+
Quo.collection_backed_query_base_class
|
157
|
+
|
158
|
+
composer(base_class, query.class, relation, joins: joins).new(**query.to_h)
|
159
|
+
end
|
160
|
+
|
161
|
+
# @rbs relation: ::ActiveRecord::Relation
|
162
|
+
# @rbs query: Quo::Query
|
163
|
+
# @rbs joins: untyped
|
164
|
+
def merge_relation_and_query(relation, query, joins)
|
165
|
+
base_class = query.is_a?(Quo::RelationBackedQuery) ?
|
166
|
+
Quo.relation_backed_query_base_class :
|
167
|
+
Quo.collection_backed_query_base_class
|
168
|
+
|
169
|
+
composer(base_class, relation, query.class, joins: joins).new(**query.to_h)
|
170
|
+
end
|
171
|
+
|
172
|
+
# @rbs left_query: Quo::Query | ::ActiveRecord::Relation
|
173
|
+
# @rbs right_query: Quo::Query | ::ActiveRecord::Relation
|
174
|
+
def merge_query_instances(left_query, right_query, joins)
|
175
|
+
props = left_query.to_h.merge(right_query.to_h.compact)
|
176
|
+
|
177
|
+
base_class = determine_base_class_for_queries(left_query, right_query)
|
178
|
+
composer(base_class, left_query.class, right_query.class, joins: joins).new(**props)
|
179
|
+
end
|
180
|
+
|
181
|
+
# @rbs left_query: Quo::Query | ::ActiveRecord::Relation
|
182
|
+
# @rbs right_query: Quo::Query | ::ActiveRecord::Relation
|
183
|
+
def determine_base_class_for_queries(left_query, right_query)
|
184
|
+
both_relation_backed = left_query.is_a?(Quo::RelationBackedQuery) &&
|
185
|
+
right_query.is_a?(Quo::RelationBackedQuery)
|
186
|
+
|
187
|
+
both_relation_backed ? Quo.relation_backed_query_base_class :
|
188
|
+
Quo.collection_backed_query_base_class
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
private
|
193
|
+
|
194
|
+
# @rbs return: Hash[Symbol, untyped]
|
195
|
+
def child_options(query_class)
|
196
|
+
names = property_names(query_class)
|
197
|
+
to_h.slice(*names)
|
198
|
+
end
|
199
|
+
|
200
|
+
# @rbs return: Array[Symbol]
|
201
|
+
def property_names(query_class)
|
202
|
+
query_class.literal_properties.properties_index.keys
|
203
|
+
end
|
204
|
+
|
205
|
+
# @rbs return: Quo::Query | ::ActiveRecord::Relation
|
206
|
+
def left
|
207
|
+
lq = self.class._left_query
|
208
|
+
return lq if is_relation?(lq)
|
209
|
+
lq.new(**child_options(lq))
|
210
|
+
end
|
211
|
+
|
212
|
+
# @rbs return: Quo::Query | ::ActiveRecord::Relation
|
213
|
+
def right
|
214
|
+
rq = self.class._right_query
|
215
|
+
return rq if is_relation?(rq)
|
216
|
+
rq.new(**child_options(rq))
|
217
|
+
end
|
218
|
+
|
219
|
+
# @rbs return: ActiveRecord::Relation | CollectionBackedQuery
|
220
|
+
def merge_left_and_right
|
221
|
+
left_rel = quo_unwrap_unpaginated_query(left)
|
222
|
+
right_rel = quo_unwrap_unpaginated_query(right)
|
223
|
+
|
224
|
+
if both_relations?(left_rel, right_rel)
|
225
|
+
merge_active_record_relations(left_rel, right_rel)
|
226
|
+
elsif left_relation_right_enumerable?(left_rel, right_rel)
|
227
|
+
left_rel.to_a + right_rel
|
228
|
+
elsif left_enumerable_right_relation?(left_rel, right_rel) && left_rel.respond_to?(:+)
|
229
|
+
left_rel + right_rel.to_a
|
230
|
+
elsif left_rel.respond_to?(:+)
|
231
|
+
left_rel + right_rel
|
232
|
+
else
|
233
|
+
raise ArgumentError, "Cannot merge #{left.class} with #{right.class}"
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# @rbs left_rel: ActiveRecord::Relation
|
238
|
+
# @rbs right_rel: ActiveRecord::Relation
|
239
|
+
# @rbs return: ActiveRecord::Relation
|
240
|
+
def merge_active_record_relations(left_rel, right_rel)
|
241
|
+
apply_joins(left_rel).merge(right_rel)
|
242
|
+
end
|
243
|
+
|
244
|
+
# @rbs left_rel: ActiveRecord::Relation
|
245
|
+
# @rbs return: ActiveRecord::Relation
|
246
|
+
def apply_joins(left_rel)
|
247
|
+
joins = self.class._composing_joins
|
248
|
+
joins.present? ? left_rel.joins(joins) : left_rel
|
249
|
+
end
|
250
|
+
|
251
|
+
# @rbs rel: untyped
|
252
|
+
# @rbs return: bool
|
253
|
+
def is_relation?(rel)
|
254
|
+
rel.is_a?(::ActiveRecord::Relation)
|
255
|
+
end
|
256
|
+
|
257
|
+
# @rbs left: untyped
|
258
|
+
# @rbs right: untyped
|
259
|
+
# @rbs return: bool
|
260
|
+
def both_relations?(left, right)
|
261
|
+
is_relation?(left) && is_relation?(right)
|
262
|
+
end
|
263
|
+
|
264
|
+
# @rbs left: untyped
|
265
|
+
# @rbs right: untyped
|
266
|
+
# @rbs return: bool
|
267
|
+
def left_relation_right_enumerable?(left, right)
|
268
|
+
is_relation?(left) && !is_relation?(right)
|
269
|
+
end
|
270
|
+
|
271
|
+
# @rbs left: untyped
|
272
|
+
# @rbs right: untyped
|
273
|
+
# @rbs return: bool
|
274
|
+
def left_enumerable_right_relation?(left, right)
|
275
|
+
!is_relation?(left) && is_relation?(right)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
data/lib/quo/engine.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "minitest/mock"
|
4
|
+
|
5
|
+
require_relative "../testing/collection_backed_fake"
|
6
|
+
require_relative "../testing/relation_backed_fake"
|
7
|
+
|
8
|
+
module Quo
|
9
|
+
module Minitest
|
10
|
+
module Helpers
|
11
|
+
def fake_query(query_class, results: [], total_count: nil, page_count: nil, &block)
|
12
|
+
# make it so that results of instances of this class return a fake Result object
|
13
|
+
# of the right type which returns the results passed in
|
14
|
+
if query_class < Quo::CollectionBackedQuery
|
15
|
+
klass = Class.new(Quo::Testing::CollectionBackedFake) do
|
16
|
+
if query_class < Quo::Preloadable
|
17
|
+
include Quo::Preloadable
|
18
|
+
|
19
|
+
def query
|
20
|
+
collection
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
query_class.stub(:new, ->(**kwargs) {
|
25
|
+
klass.new(results: results, total_count: total_count, page_count: page_count)
|
26
|
+
}) do
|
27
|
+
yield
|
28
|
+
end
|
29
|
+
elsif query_class < Quo::RelationBackedQuery
|
30
|
+
query_class.stub(:new, ->(**kwargs) {
|
31
|
+
Quo::Testing::RelationBackedFake.new(results: results, total_count: total_count, page_count: page_count)
|
32
|
+
}) do
|
33
|
+
yield
|
34
|
+
end
|
35
|
+
else
|
36
|
+
raise ArgumentError, "Not a Query class: #{query_class}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rbs_inline: enabled
|
4
|
+
|
5
|
+
module Quo
|
6
|
+
module Preloadable
|
7
|
+
def self.included(base)
|
8
|
+
base.prop :_rel_preload, base._Nilable(base._Any), reader: false, writer: false
|
9
|
+
end
|
10
|
+
|
11
|
+
# This implementation of `query` calls `collection` and preloads the includes.
|
12
|
+
# @rbs return: Object & Enumerable[untyped]
|
13
|
+
def query
|
14
|
+
records = collection
|
15
|
+
preload_includes(records) if @_rel_preload
|
16
|
+
records
|
17
|
+
end
|
18
|
+
|
19
|
+
# For use with collections of ActiveRecord models.
|
20
|
+
# Configures ActiveRecord::Associations::Preloader to load associations of models in the collection
|
21
|
+
# @rbs *options: untyped
|
22
|
+
# @rbs return: Quo::Query
|
23
|
+
def preload(*options)
|
24
|
+
copy(_rel_preload: options)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Alias for `preload`
|
28
|
+
# @rbs *options: untyped
|
29
|
+
# @rbs return: Quo::Query
|
30
|
+
def includes(*options)
|
31
|
+
preload(*options)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# @rbs @_rel_preload: untyped?
|
37
|
+
|
38
|
+
# @rbs (untyped records, ?untyped? preload) -> untyped
|
39
|
+
def preload_includes(records, preload = nil)
|
40
|
+
::ActiveRecord::Associations::Preloader.new(
|
41
|
+
records: records,
|
42
|
+
associations: preload || @_rel_preload
|
43
|
+
).call
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|