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
data/lib/quo/query.rb
CHANGED
@@ -1,302 +1,184 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
require_relative "./utilities/compose"
|
5
|
-
require_relative "./utilities/sanitize"
|
6
|
-
require_relative "./utilities/wrap"
|
3
|
+
# rbs_inline: enabled
|
7
4
|
|
8
|
-
|
9
|
-
class Query
|
10
|
-
include Quo::Utilities::Callstack
|
11
|
-
|
12
|
-
extend Quo::Utilities::Compose
|
13
|
-
extend Quo::Utilities::Sanitize
|
14
|
-
extend Quo::Utilities::Wrap
|
15
|
-
|
16
|
-
class << self
|
17
|
-
def call(**options)
|
18
|
-
new(**options).first
|
19
|
-
end
|
20
|
-
|
21
|
-
def call!(**options)
|
22
|
-
new(**options).first!
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
attr_reader :current_page, :page_size, :options
|
5
|
+
require "literal"
|
27
6
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
@page_size = options[:page_size]&.to_i || Quo.configuration.default_page_size || 20
|
32
|
-
end
|
33
|
-
|
34
|
-
# Returns a active record query, or a Quo::Query instance
|
35
|
-
def query
|
36
|
-
raise NotImplementedError, "Query objects must define a 'query' method"
|
37
|
-
end
|
7
|
+
module Quo
|
8
|
+
class Query < Literal::Struct
|
9
|
+
include Literal::Types
|
38
10
|
|
39
|
-
|
40
|
-
|
41
|
-
def compose(right, joins: nil)
|
42
|
-
Quo::QueryComposer.new(self, right, joins).compose
|
11
|
+
def self.inspect
|
12
|
+
"#{name || "(anonymous)"}<#{superclass}>"
|
43
13
|
end
|
44
14
|
|
45
|
-
|
46
|
-
|
47
|
-
def copy(**options)
|
48
|
-
self.class.new(**@options.merge(options))
|
15
|
+
def self.to_s
|
16
|
+
inspect
|
49
17
|
end
|
50
18
|
|
51
|
-
|
52
|
-
|
53
|
-
copy(limit: limit)
|
19
|
+
def inspect
|
20
|
+
"#{self.class.name || "(anonymous)"}<#{self.class.superclass} #{paged? ? "" : "not "}paginated>#{super}"
|
54
21
|
end
|
55
22
|
|
56
|
-
def
|
57
|
-
|
23
|
+
def to_s
|
24
|
+
inspect
|
58
25
|
end
|
59
26
|
|
60
|
-
|
61
|
-
|
27
|
+
# TODO: put this in a module with the composer and merge_instances methods
|
28
|
+
# Compose is aliased as `+`. Can optionally take `joins` parameters to add joins on merged relation.
|
29
|
+
# @rbs right: Quo::Query | ActiveRecord::Relation | Object & Enumerable[untyped]
|
30
|
+
# @rbs joins: Symbol | Hash[Symbol, untyped] | Array[Symbol | Hash[Symbol, untyped]]
|
31
|
+
# @rbs return: Quo::Query & Quo::ComposedQuery
|
32
|
+
def self.compose(right, joins: nil)
|
33
|
+
super_class = if self < Quo::CollectionBackedQuery || right < Quo::CollectionBackedQuery
|
34
|
+
Quo.collection_backed_query_base_class
|
35
|
+
else
|
36
|
+
Quo.relation_backed_query_base_class
|
37
|
+
end
|
38
|
+
ComposedQuery.composer(super_class, self, right, joins: joins)
|
62
39
|
end
|
40
|
+
singleton_class.alias_method :+, :compose
|
63
41
|
|
64
|
-
|
65
|
-
|
42
|
+
COERCE_TO_INT = ->(value) do #: (untyped value) -> Integer?
|
43
|
+
return if value == Literal::Null
|
44
|
+
value&.to_i
|
66
45
|
end
|
67
46
|
|
68
|
-
|
69
|
-
|
70
|
-
|
47
|
+
# @rbs!
|
48
|
+
# attr_accessor page (): Integer?
|
49
|
+
# attr_accessor page_size (): Integer?
|
50
|
+
# @current_page: Integer?
|
51
|
+
prop :page, _Nilable(Integer), &COERCE_TO_INT
|
52
|
+
prop(:page_size, _Nilable(Integer), default: -> { Quo.default_page_size || 20 }, &COERCE_TO_INT)
|
71
53
|
|
72
|
-
def
|
73
|
-
copy(
|
54
|
+
def next_page_query #: Quo::Query
|
55
|
+
copy(page: page + 1)
|
74
56
|
end
|
75
57
|
|
76
|
-
|
77
|
-
|
78
|
-
# Delegate SQL calculation methods to the underlying query
|
79
|
-
delegate :sum, :average, :minimum, :maximum, to: :query_with_logging
|
80
|
-
|
81
|
-
# Gets the count of all results ignoring the current page and page size (if set)
|
82
|
-
delegate :count, to: :underlying_query
|
83
|
-
alias_method :total_count, :count
|
84
|
-
alias_method :size, :count
|
85
|
-
|
86
|
-
# Gets the actual count of elements in the page of results (assuming paging is being used, otherwise the count of
|
87
|
-
# all results)
|
88
|
-
def page_count
|
89
|
-
query_with_logging.count
|
58
|
+
def previous_page_query #: Quo::Query
|
59
|
+
copy(page: [page - 1, 1].max)
|
90
60
|
end
|
91
61
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
def first(limit = nil)
|
97
|
-
if transform?
|
98
|
-
res = query_with_logging.first(limit)
|
99
|
-
if res.is_a? Array
|
100
|
-
res.map.with_index { |r, i| transformer&.call(r, i) }
|
101
|
-
elsif !res.nil?
|
102
|
-
transformer&.call(query_with_logging.first(limit))
|
103
|
-
end
|
104
|
-
elsif limit
|
105
|
-
query_with_logging.first(limit)
|
62
|
+
def offset #: Integer
|
63
|
+
per_page = sanitised_page_size
|
64
|
+
page_with_default = if page&.positive?
|
65
|
+
page
|
106
66
|
else
|
107
|
-
|
108
|
-
query_with_logging.first
|
67
|
+
1
|
109
68
|
end
|
69
|
+
per_page * (page_with_default - 1)
|
110
70
|
end
|
111
71
|
|
112
|
-
|
113
|
-
|
114
|
-
raise
|
115
|
-
item
|
72
|
+
# Returns a active record query, or a Quo::Query instance
|
73
|
+
def query #: Quo::Query | ::ActiveRecord::Relation
|
74
|
+
raise NotImplementedError, "Query objects must define a 'query' method"
|
116
75
|
end
|
117
76
|
|
118
|
-
#
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
res.map.with_index { |r, i| transformer&.call(r, i) }
|
124
|
-
elsif !res.nil?
|
125
|
-
transformer&.call(res)
|
126
|
-
end
|
127
|
-
elsif limit
|
128
|
-
query_with_logging.last(limit)
|
129
|
-
else
|
130
|
-
query_with_logging.last
|
77
|
+
# @rbs **overrides: untyped
|
78
|
+
# @rbs return: Quo::Query
|
79
|
+
def copy(**overrides)
|
80
|
+
self.class.new(**to_h.merge(overrides)).tap do |q|
|
81
|
+
q.instance_variable_set(:@__transformer, transformer)
|
131
82
|
end
|
132
83
|
end
|
133
84
|
|
134
|
-
#
|
135
|
-
|
136
|
-
|
137
|
-
|
85
|
+
# Compose is aliased as `+`. Can optionally take `joins` parameters to add joins on merged relation.
|
86
|
+
# @rbs right: Quo::Query | ::ActiveRecord::Relation
|
87
|
+
# @rbs joins: untyped
|
88
|
+
# @rbs return: Quo::ComposedQuery
|
89
|
+
def merge(right, joins: nil)
|
90
|
+
ComposedQuery.merge_instances(self, right, joins: joins)
|
138
91
|
end
|
92
|
+
alias_method :+, :merge
|
139
93
|
|
140
|
-
|
141
|
-
Quo::LoadedQuery.new(to_a, **options.merge(more_opts))
|
142
|
-
end
|
143
|
-
alias_method :load, :to_eager
|
144
|
-
|
145
|
-
def results
|
146
|
-
Quo::Results.new(self, transformer: transformer)
|
147
|
-
end
|
148
|
-
|
149
|
-
# Some convenience methods for working with results
|
150
|
-
delegate :each,
|
151
|
-
:find_each,
|
152
|
-
:map,
|
153
|
-
:flat_map,
|
154
|
-
:reduce,
|
155
|
-
:reject,
|
156
|
-
:filter,
|
157
|
-
:find,
|
158
|
-
:include?,
|
159
|
-
:each_with_object,
|
160
|
-
to: :results
|
94
|
+
# @rbs @__transformer: nil | ^(untyped, ?Integer) -> untyped
|
161
95
|
|
162
96
|
# Set a block used to transform data after query fetching
|
97
|
+
# @rbs block: ^(untyped, ?Integer) -> untyped
|
98
|
+
# @rbs return: self
|
163
99
|
def transform(&block)
|
164
|
-
@
|
100
|
+
@__transformer = block
|
165
101
|
self
|
166
102
|
end
|
167
103
|
|
168
|
-
#
|
169
|
-
def
|
170
|
-
return query_with_logging.exists? if relation?
|
171
|
-
query_with_logging.present?
|
172
|
-
end
|
173
|
-
|
174
|
-
# Are there no results for this query?
|
175
|
-
def none?
|
176
|
-
!exists?
|
177
|
-
end
|
178
|
-
alias_method :empty?, :none?
|
179
|
-
|
180
|
-
# Is this query object a relation under the hood? (ie not eager loaded)
|
181
|
-
def relation?
|
104
|
+
# Is this query object a ActiveRecord relation under the hood?
|
105
|
+
def relation? #: bool
|
182
106
|
test_relation(configured_query)
|
183
107
|
end
|
184
108
|
|
185
|
-
# Is this query object
|
186
|
-
def
|
187
|
-
|
109
|
+
# Is this query object loaded data/collection under the hood? (ie not a AR relation)
|
110
|
+
def collection? #: bool
|
111
|
+
is_collection?(configured_query)
|
188
112
|
end
|
189
113
|
|
190
114
|
# Is this query object paged? (ie is paging enabled)
|
191
|
-
def paged?
|
192
|
-
|
115
|
+
def paged? #: bool
|
116
|
+
page.present?
|
193
117
|
end
|
194
118
|
|
195
119
|
# Is this query object transforming results?
|
196
|
-
def transform?
|
120
|
+
def transform? #: bool
|
197
121
|
transformer.present?
|
198
122
|
end
|
199
123
|
|
200
|
-
# Return the SQL string for this query if its a relation type query object
|
201
|
-
def to_sql
|
202
|
-
configured_query.to_sql if relation?
|
203
|
-
end
|
204
|
-
|
205
124
|
# Unwrap the paginated query
|
206
|
-
def unwrap
|
125
|
+
def unwrap #: ActiveRecord::Relation
|
207
126
|
configured_query
|
208
127
|
end
|
209
128
|
|
210
129
|
# Unwrap the un-paginated query
|
211
|
-
def unwrap_unpaginated
|
130
|
+
def unwrap_unpaginated #: ActiveRecord::Relation
|
212
131
|
underlying_query
|
213
132
|
end
|
214
133
|
|
215
|
-
delegate :distinct, to: :configured_query
|
216
|
-
|
217
134
|
private
|
218
135
|
|
219
|
-
def
|
220
|
-
|
221
|
-
end
|
222
|
-
|
223
|
-
# 'trim' a query, ie remove comments and remove newlines
|
224
|
-
# This will remove dashes from inside strings too
|
225
|
-
def trim_query(sql)
|
226
|
-
sql.gsub(/--[^\n'"]*\n/m, " ").tr("\n", " ").strip
|
227
|
-
end
|
228
|
-
|
229
|
-
def format_query(sql_str)
|
230
|
-
formatted_queries? ? sql_str : trim_query(sql_str)
|
136
|
+
def transformer
|
137
|
+
@__transformer
|
231
138
|
end
|
232
139
|
|
233
|
-
def
|
234
|
-
|
140
|
+
def validated_query
|
141
|
+
raise NoMethodError, "Query objects must define a 'validated_query' method"
|
235
142
|
end
|
236
143
|
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
current_page
|
241
|
-
else
|
242
|
-
1
|
243
|
-
end
|
244
|
-
per_page * (page - 1)
|
144
|
+
# The underlying query is essentially the configured query with optional extras setup
|
145
|
+
def underlying_query #: void
|
146
|
+
raise NoMethodError, "Query objects must define a 'underlying_query' method"
|
245
147
|
end
|
246
148
|
|
247
149
|
# The configured query is the underlying query with paging
|
248
|
-
def configured_query
|
249
|
-
|
250
|
-
return q unless paged? && q.is_a?(ActiveRecord::Relation)
|
251
|
-
q.offset(offset).limit(sanitised_page_size)
|
150
|
+
def configured_query #: void
|
151
|
+
raise NoMethodError, "Query objects must define a 'configured_query' method"
|
252
152
|
end
|
253
153
|
|
254
|
-
def sanitised_page_size
|
255
|
-
if page_size
|
154
|
+
def sanitised_page_size #: Integer
|
155
|
+
if page_size&.positive?
|
256
156
|
given_size = page_size.to_i
|
257
|
-
max_page_size = Quo.
|
157
|
+
max_page_size = Quo.max_page_size || 200
|
258
158
|
if given_size > max_page_size
|
259
159
|
max_page_size
|
260
160
|
else
|
261
161
|
given_size
|
262
162
|
end
|
263
163
|
else
|
264
|
-
Quo.
|
164
|
+
Quo.default_page_size || 20
|
265
165
|
end
|
266
166
|
end
|
267
167
|
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
# The underlying query is essentially the configured query with optional extras setup
|
274
|
-
def underlying_query
|
275
|
-
@underlying_query ||=
|
276
|
-
begin
|
277
|
-
rel = unwrap_relation(query)
|
278
|
-
unless test_eager(rel)
|
279
|
-
rel = rel.group(@options[:group]) if @options[:group].present?
|
280
|
-
rel = rel.order(@options[:order]) if @options[:order].present?
|
281
|
-
rel = rel.limit(@options[:limit]) if @options[:limit].present?
|
282
|
-
rel = rel.preload(@options[:preload]) if @options[:preload].present?
|
283
|
-
rel = rel.includes(@options[:includes]) if @options[:includes].present?
|
284
|
-
rel = rel.select(@options[:select]) if @options[:select].present?
|
285
|
-
end
|
286
|
-
rel
|
287
|
-
end
|
288
|
-
end
|
289
|
-
|
290
|
-
def unwrap_relation(query)
|
291
|
-
query.is_a?(Quo::Query) ? query.unwrap : query
|
292
|
-
end
|
293
|
-
|
294
|
-
def test_eager(rel)
|
295
|
-
rel.is_a?(Quo::LoadedQuery) || (rel.is_a?(Enumerable) && !test_relation(rel))
|
168
|
+
# @rbs rel: untyped
|
169
|
+
# @rbs return: bool
|
170
|
+
def is_collection?(rel)
|
171
|
+
rel.is_a?(Quo::CollectionBackedQuery) || (rel.is_a?(Enumerable) && !test_relation(rel))
|
296
172
|
end
|
297
173
|
|
174
|
+
# @rbs rel: untyped
|
175
|
+
# @rbs return: bool
|
298
176
|
def test_relation(rel)
|
299
177
|
rel.is_a?(ActiveRecord::Relation)
|
300
178
|
end
|
179
|
+
|
180
|
+
def quo_unwrap_unpaginated_query(q)
|
181
|
+
q.is_a?(Quo::Query) ? q.unwrap_unpaginated : q
|
182
|
+
end
|
301
183
|
end
|
302
184
|
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rbs_inline: enabled
|
4
|
+
|
5
|
+
require "literal"
|
6
|
+
|
7
|
+
module Quo
|
8
|
+
class RelationBackedQuery < Query
|
9
|
+
# @rbs query: ActiveRecord::Relation | Quo::Query
|
10
|
+
# @rbs props: Hash[Symbol, untyped]
|
11
|
+
# @rbs &block: () -> ActiveRecord::Relation | Quo::Query | Object & Enumerable[untyped]
|
12
|
+
# @rbs return: Quo::RelationBackedQuery
|
13
|
+
def self.wrap(query = nil, props: {}, &block)
|
14
|
+
raise ArgumentError, "either a query or a block must be provided" unless query || block
|
15
|
+
|
16
|
+
klass = Class.new(self) do
|
17
|
+
props.each do |name, property|
|
18
|
+
if property.is_a?(Literal::Property)
|
19
|
+
prop name, property.type, property.kind, reader: property.reader, writer: property.writer, default: property.default
|
20
|
+
else
|
21
|
+
prop name, property
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
if block
|
26
|
+
klass.define_method(:query, &block)
|
27
|
+
else
|
28
|
+
klass.define_method(:query) { query }
|
29
|
+
end
|
30
|
+
klass
|
31
|
+
end
|
32
|
+
|
33
|
+
# @rbs conditions: untyped?
|
34
|
+
# @rbs return: String
|
35
|
+
def self.sanitize_sql_for_conditions(conditions)
|
36
|
+
ActiveRecord::Base.sanitize_sql_for_conditions(conditions)
|
37
|
+
end
|
38
|
+
|
39
|
+
# @rbs string: String
|
40
|
+
# @rbs return: String
|
41
|
+
def self.sanitize_sql_string(string)
|
42
|
+
sanitize_sql_for_conditions(["'%s'", string])
|
43
|
+
end
|
44
|
+
|
45
|
+
# @rbs value: untyped
|
46
|
+
# @rbs return: String
|
47
|
+
def self.sanitize_sql_parameter(value)
|
48
|
+
sanitize_sql_for_conditions(["?", value])
|
49
|
+
end
|
50
|
+
|
51
|
+
# The query specification stores all options related to building the query
|
52
|
+
# @rbs!
|
53
|
+
# @_specification: Quo::RelationBackedQuerySpecification?
|
54
|
+
prop :_specification, _Nilable(Quo::RelationBackedQuerySpecification),
|
55
|
+
default: -> { RelationBackedQuerySpecification.blank },
|
56
|
+
reader: false,
|
57
|
+
writer: false
|
58
|
+
|
59
|
+
# Apply a query specification to this query
|
60
|
+
# @rbs specification: Quo::RelationBackedQuerySpecification
|
61
|
+
# @rbs return: Quo::Query
|
62
|
+
def with_specification(specification)
|
63
|
+
copy(_specification: specification)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Apply query options using the specification
|
67
|
+
# @rbs options: Hash[Symbol, untyped]
|
68
|
+
# @rbs return: Quo::Query
|
69
|
+
def with(options = {})
|
70
|
+
spec = @_specification || RelationBackedQuerySpecification.blank
|
71
|
+
with_specification(spec.merge(options))
|
72
|
+
end
|
73
|
+
|
74
|
+
# Delegate methods that let us get the model class (available on AR relations)
|
75
|
+
# @rbs def model: () -> (untyped | nil)
|
76
|
+
# @rbs def klass: () -> (untyped | nil)
|
77
|
+
delegate :model, :klass, to: :underlying_query
|
78
|
+
|
79
|
+
# @rbs return: Quo::CollectionBackedQuery
|
80
|
+
def to_collection(total_count: nil)
|
81
|
+
Quo.collection_backed_query_base_class.wrap(results.to_a).new(total_count:)
|
82
|
+
end
|
83
|
+
|
84
|
+
def results #: Quo::Results
|
85
|
+
Quo::RelationResults.new(self, transformer: transformer)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Return the SQL string for this query if its a relation type query object
|
89
|
+
def to_sql #: String
|
90
|
+
configured_query.to_sql if relation?
|
91
|
+
end
|
92
|
+
|
93
|
+
# Implements a fluent API for query methods
|
94
|
+
# This allows methods to be chained like query.where(...).order(...).limit(...)
|
95
|
+
# @rbs method_name: Symbol
|
96
|
+
# @rbs *args: untyped
|
97
|
+
# @rbs **kwargs: untyped
|
98
|
+
# @rbs &block: untyped
|
99
|
+
# @rbs return: Quo::Query
|
100
|
+
def method_missing(method_name, *args, **kwargs, &block)
|
101
|
+
spec = @_specification || RelationBackedQuerySpecification.blank
|
102
|
+
|
103
|
+
# Check if the method exists in RelationBackedQuerySpecification
|
104
|
+
if spec.respond_to?(method_name)
|
105
|
+
# Call the method on the specification and return a new query with the updated specification
|
106
|
+
updated_spec = spec.method(method_name).call(*args, **kwargs, &block)
|
107
|
+
return with_specification(updated_spec)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Forward to underlying query if method not found in RelationBackedQuerySpecification
|
111
|
+
super
|
112
|
+
end
|
113
|
+
|
114
|
+
# @rbs method_name: Symbol
|
115
|
+
# @rbs include_private: bool
|
116
|
+
# @rbs return: bool
|
117
|
+
def respond_to_missing?(method_name, include_private = false)
|
118
|
+
spec_instance = RelationBackedQuerySpecification.new
|
119
|
+
spec_instance.respond_to?(method_name, include_private) || super
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def validated_query
|
125
|
+
query.tap do |q|
|
126
|
+
raise ArgumentError, "#query must return an ActiveRecord Relation or a Quo::Query instance" unless query.nil? || q.is_a?(::ActiveRecord::Relation) || q.is_a?(Quo::Query)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# The underlying query is essentially the configured query with optional extras setup
|
131
|
+
def underlying_query #: ActiveRecord::Relation
|
132
|
+
rel = quo_unwrap_unpaginated_query(validated_query)
|
133
|
+
|
134
|
+
# Apply specification if it exists
|
135
|
+
if @_specification
|
136
|
+
@_specification.apply_to(rel)
|
137
|
+
else
|
138
|
+
rel
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# The configured query is the underlying query with paging
|
143
|
+
def configured_query #: ActiveRecord::Relation
|
144
|
+
q = underlying_query
|
145
|
+
return q unless paged?
|
146
|
+
|
147
|
+
q.offset(offset).limit(sanitised_page_size)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|