quo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +350 -0
- data/Rakefile +14 -0
- data/lib/quo/eager_query.rb +47 -0
- data/lib/quo/enumerator.rb +74 -0
- data/lib/quo/merged_query.rb +41 -0
- data/lib/quo/query.rb +276 -0
- data/lib/quo/query_composer.rb +85 -0
- data/lib/quo/railtie.rb +7 -0
- data/lib/quo/rspec/helpers.rb +19 -0
- data/lib/quo/utilities/callstack.rb +19 -0
- data/lib/quo/utilities/compose.rb +19 -0
- data/lib/quo/utilities/sanitize.rb +20 -0
- data/lib/quo/utilities/wrap.rb +19 -0
- data/lib/quo/version.rb +5 -0
- data/lib/quo.rb +31 -0
- data/sig/quo.rbs +4 -0
- metadata +79 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5abae18b00be2197c2d35ed322af099766deaaa13f77f14fe53f597f2ed544b2
|
4
|
+
data.tar.gz: 95e1b9e161ad9e4d6fb627194cc6b408878945aa08c43ef498bbbfa0ea5c3dd3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9c6ce18b7bb3de2243561ad14da26117e7053a0c04255dc94b8408d942a1382e2c4eb2c6071c22f52d1a7cef9bf9fceeeb7746788f921c0c1c6a9896fa2f4717
|
7
|
+
data.tar.gz: 063f2dd88dcd09bd1b3778b8c6bc77b09a000daae113425fc3c7152c058c2e836cb5630fa58ef060fb099179eddf6119592e25e51cb7ad0380791cd7468a1cac
|
data/.standard.yml
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
# Specify your gem's dependencies in quo.gemspec
|
6
|
+
gemspec
|
7
|
+
|
8
|
+
gem "sqlite3"
|
9
|
+
|
10
|
+
gem "rails", ">= 6"
|
11
|
+
|
12
|
+
gem "rake", "~> 13.0"
|
13
|
+
|
14
|
+
gem "minitest", "~> 5.0"
|
15
|
+
|
16
|
+
gem "standard", "~> 1.3"
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2022 Stephen Ierodiaconou
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,350 @@
|
|
1
|
+
# Quo
|
2
|
+
|
3
|
+
Quo query objects can help you abstract ActiveRecord DB queries into reusable and composable objects with a chainable
|
4
|
+
interface.
|
5
|
+
|
6
|
+
The query object can also abstract over any array-like collection meaning that it is possible for example to cache the
|
7
|
+
data from a query and reuse it.
|
8
|
+
|
9
|
+
The core implementation provides the following functionality:
|
10
|
+
|
11
|
+
* wrap around an underlying ActiveRecord or array-like collection
|
12
|
+
* optionally provides paging behaviour to ActiveRecord based queries
|
13
|
+
* provides a number of utility methods that operate on the underlying collection (eg `exists?`)
|
14
|
+
* provides a `+` (`compose`) method which merges two query object instances (see section below for details!)
|
15
|
+
* can specify a mapping or transform method to `transform` to perform on results
|
16
|
+
* in development outputs the callstack that led to the execution of the query
|
17
|
+
* acts as a callable which executes the underlying query with `.first`
|
18
|
+
* can return an `Enumerable` of results
|
19
|
+
|
20
|
+
## Creating a Quo query object
|
21
|
+
|
22
|
+
The query object must inherit from `Quo::Query` and provide an implementation for the `query` method.
|
23
|
+
|
24
|
+
The `query` method must return either:
|
25
|
+
|
26
|
+
- an `ActiveRecord::Relation`
|
27
|
+
- an Array (an 'eager loaded' query)
|
28
|
+
- or another `Quo::Query` instance.
|
29
|
+
|
30
|
+
Remember that the query object should be useful in composition with other query objects. Thus it should not directly
|
31
|
+
specify things that are not directly needed to fetch the right data for the given context.
|
32
|
+
|
33
|
+
For example the ordering of the results is mostly something that is specified when the query object is used, not as
|
34
|
+
part of the query itself (as then it would always enforce the ordering on other queries it was composed with).
|
35
|
+
|
36
|
+
## Passing options to queries
|
37
|
+
|
38
|
+
If any parameters are need in `query`, these are provided when instantiating the query object using the `options` hash.
|
39
|
+
|
40
|
+
It is also possible to pass special configuration options to the constructor options hash.
|
41
|
+
|
42
|
+
Specifically when the underlying collection is a ActiveRecord relation then:
|
43
|
+
|
44
|
+
* `order`: the `order` condition for the relation (eg `:desc`)
|
45
|
+
* `includes`: the `includes` condition for the relation (eg `account: {user: :profile}`)
|
46
|
+
* `group`: the `group` condition for the relation
|
47
|
+
* `page`: the current page number to fetch
|
48
|
+
* `page_size`: the number of elements to fetch in the page
|
49
|
+
|
50
|
+
Note that the above options have no bearing on the query if it is backed by an array-like collection and that some
|
51
|
+
options can be configured using the following methods.
|
52
|
+
|
53
|
+
## Configuring queries
|
54
|
+
|
55
|
+
Note that it is possible to configure a query using chainable methods similar to ActiveRecord:
|
56
|
+
|
57
|
+
* limit
|
58
|
+
* order
|
59
|
+
* group
|
60
|
+
* includes
|
61
|
+
* left_outer_joins
|
62
|
+
* preload
|
63
|
+
* joins
|
64
|
+
|
65
|
+
Note that these return a new Quo Query and do not mutate the original instance.
|
66
|
+
|
67
|
+
## Composition of queries (merging or combining them)
|
68
|
+
|
69
|
+
Quo query objects are composeability. In `ActiveRecord::Relation` this is acheived using `merge`
|
70
|
+
and so under the hood `Quo::Query` uses that when composing relations. However since Queries can also abstract over
|
71
|
+
array-like collections (ie enumerable and define a `+` method) compose also handles concating them together.
|
72
|
+
|
73
|
+
Composing can be done with either
|
74
|
+
|
75
|
+
- `Quo::Query.compose(left, right)`
|
76
|
+
- or `left.compose(right)`
|
77
|
+
- or more simply with `left + right`
|
78
|
+
|
79
|
+
The composition methods also accept an optional parameter to pass to ActiveRecord relation merges for the `joins`.
|
80
|
+
This allows you to compose together Query objects which return relations which are of different models but still merge
|
81
|
+
them correctly with the appropriate joins. Note with the alias you cant neatly specify optional parameters for joins
|
82
|
+
on relations.
|
83
|
+
|
84
|
+
Note that the compose process creates a new query object instance, which is a instance of a `Quo::MergedQuery`.
|
85
|
+
|
86
|
+
Consider the following cases:
|
87
|
+
|
88
|
+
1. compose two query objects which return `ActiveRecord::Relation`s
|
89
|
+
2. compose two query objects, one of which returns a `ActiveRecord::Relation`, and the other an array-like
|
90
|
+
3. compose two query objects which return array-likes
|
91
|
+
|
92
|
+
In case (1) the compose process uses `ActiveRecords::Relation`'s `merge` method to create another query object
|
93
|
+
wrapped around a new 'composed' `ActiveRecords::Relation`.
|
94
|
+
|
95
|
+
In case (2) the query object with a `ActiveRecords::Relation` inside is executed, and the result is then concatenated
|
96
|
+
to the array-like with `+`
|
97
|
+
|
98
|
+
In case (3) the values contained with each 'eager' query object are concatenated with `+`
|
99
|
+
|
100
|
+
*Note that*
|
101
|
+
|
102
|
+
with `left.compose(right)`, `left` must obviously be an instance of a `Quo::Query`, and `right` can be either a
|
103
|
+
query object or and `ActiveRecord::Relation`. However `Quo::Query.compose(left, right)` also accepts
|
104
|
+
`ActiveRecord::Relation`s for left.
|
105
|
+
|
106
|
+
### Examples
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
class CompanyToBeApproved < Quo::Query
|
110
|
+
def query
|
111
|
+
Registration
|
112
|
+
.left_joins(:approval)
|
113
|
+
.where(approvals: {completed_at: nil})
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
class CompanyInUsState < Quo::Query
|
118
|
+
def query
|
119
|
+
Registration
|
120
|
+
.joins(company: :address)
|
121
|
+
.where(addresses: {state: options[:state]})
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
query1 = CompanyToBeApproved.new
|
126
|
+
query2 = CompanyInUsState.new(state: "California")
|
127
|
+
|
128
|
+
# Compose
|
129
|
+
composed = query1 + query2 # or Quo::Query.compose(query1, query2) or query1.compose(query2)
|
130
|
+
composed.first
|
131
|
+
```
|
132
|
+
|
133
|
+
This effectively executes:
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
Registration
|
137
|
+
.left_joins(:approval)
|
138
|
+
.joins(company: :address)
|
139
|
+
.where(approvals: {completed_at: nil})
|
140
|
+
.where(addresses: {state: options[:state]})
|
141
|
+
```
|
142
|
+
|
143
|
+
It is also possible to compose with an `ActiveRecord::Relation`. This can be useful in a Query object itself to help
|
144
|
+
build up the `query` relation. For example:
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
class RegistrationToBeApproved < Quo::Query
|
148
|
+
def query
|
149
|
+
done = Registration.where(step: "complete")
|
150
|
+
approved = CompanyToBeApproved.new
|
151
|
+
done + approved
|
152
|
+
end
|
153
|
+
end
|
154
|
+
```
|
155
|
+
|
156
|
+
Also you can use joins:
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
class TagByName < Quo::Query
|
160
|
+
def query
|
161
|
+
Tag.where(name: options[:name])
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
class CategoryByName < Quo::Query
|
166
|
+
def query
|
167
|
+
Category.where(name: options[:name])
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
tags = TagByName.new(name: "Intel")
|
172
|
+
for_category = CategoryByName.new(name: "CPUs")
|
173
|
+
tags.compose(for_category, :category) # perform join on tag association `category`
|
174
|
+
|
175
|
+
# equivalent to Tag.joins(:category).where(name: "Intel").where(categories: {name: "CPUs"})
|
176
|
+
```
|
177
|
+
|
178
|
+
Eager loaded queries can also be composed (see below sections for more details).
|
179
|
+
|
180
|
+
### Quo::MergedQuery
|
181
|
+
|
182
|
+
The new instance of `Quo::MergedQuery` from a compose process, retains references to the original entities that were
|
183
|
+
composed. These are then used to create a more useful output from `to_s`, so that it is easier to understand what the
|
184
|
+
merged query is actually made up of:
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
q = FooQuery.new + BarQuery.new
|
188
|
+
puts q
|
189
|
+
# > "Quo::MergedQuery[FooQuery, BarQuery]"
|
190
|
+
```
|
191
|
+
|
192
|
+
## Query Objects & Pagination
|
193
|
+
|
194
|
+
Specify extra options to enable pagination:
|
195
|
+
|
196
|
+
* `page`: the current page number to fetch
|
197
|
+
* `page_size`: the number of elements to fetch in the page
|
198
|
+
|
199
|
+
## 'Eager loaded' Quo::Query objects
|
200
|
+
|
201
|
+
When a query object returns an `Array` from `query` it is assumed as 'eager loaded', ie that the query has actually
|
202
|
+
already been executed and the array contains the return values. This can also be used to encapsulate data that doesn't
|
203
|
+
actually come from an ActiveRecord query.
|
204
|
+
|
205
|
+
For example, the Query below executes the query inside on the first call and memoises the resulting data. Note
|
206
|
+
however that this then means that this Query is not reusuable to `merge` with other ActiveRecord queries. If it is
|
207
|
+
`compose`d with other Query objects then it will be seen as an array-like, and concatenated to whatever it is being
|
208
|
+
joined to.
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
class CachedTags < Quo::Query
|
212
|
+
def query
|
213
|
+
@tags ||= Tag.where(active: true).to_a
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
q = CachedTags.new(active: false)
|
218
|
+
q.eager? # is it 'eager'? Yes it is!
|
219
|
+
q.count # array size
|
220
|
+
```
|
221
|
+
|
222
|
+
### `Quo::EagerQuery` objects
|
223
|
+
|
224
|
+
`Quo::EagerQuery` is a subclass of `Quo::Query` which takes a data value on instantiation and returns it on calls to `query`
|
225
|
+
|
226
|
+
```ruby
|
227
|
+
q = Quo::EagerQuery.new(collection: [1, 2, 3])
|
228
|
+
q.eager? # is it 'eager'? Yes it is!
|
229
|
+
q.count # '3'
|
230
|
+
```
|
231
|
+
|
232
|
+
This is useful to create eager loaded Queries without needing to create a explicit subclass of your own.
|
233
|
+
|
234
|
+
`Quo::EagerQuery` also uses `total_count` option value as the specified 'total count', useful when the data is
|
235
|
+
actually just a page of the data and not the total count.
|
236
|
+
|
237
|
+
Example of an EagerQuery used to wrap a page of enumerable data:
|
238
|
+
|
239
|
+
```ruby
|
240
|
+
Quo::EagerQuery.new(collection: my_data, total_count: 100, page: current_page)
|
241
|
+
```
|
242
|
+
|
243
|
+
### Composition
|
244
|
+
|
245
|
+
Examples of composition of eager loaded queries
|
246
|
+
|
247
|
+
```ruby
|
248
|
+
class CachedTags < Quo::Query
|
249
|
+
def query
|
250
|
+
@tags ||= Tag.where(active: true).to_a
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
composed = CachedTags.new(active: false) + [1, 2]
|
255
|
+
composed.last
|
256
|
+
# => 2
|
257
|
+
composed.first
|
258
|
+
# => #<Tag id: ...>
|
259
|
+
|
260
|
+
Quo::EagerQuery.new([3, 4]).compose(Quo::EagerQuery.new(collection: [1, 2])).last
|
261
|
+
# => 2
|
262
|
+
Quo::Query.compose([1, 2], [3, 4]).last
|
263
|
+
# => 4
|
264
|
+
```
|
265
|
+
|
266
|
+
## Transforming results
|
267
|
+
|
268
|
+
Sometimes you want to specify a block to execute on each result for any method that returns results, such as `first`,
|
269
|
+
`last` and `each`.
|
270
|
+
|
271
|
+
This can be specified using the `transform(&block)` instance method. For example:
|
272
|
+
|
273
|
+
```ruby
|
274
|
+
TagsQuery.new(
|
275
|
+
active: [true, false],
|
276
|
+
page: 1,
|
277
|
+
page_size: 30,
|
278
|
+
).transform { |tag| TagPresenter.new(tag) }
|
279
|
+
.first
|
280
|
+
# => #<TagPresenter ...>
|
281
|
+
```
|
282
|
+
|
283
|
+
## Tests & stubbing
|
284
|
+
|
285
|
+
Tests for Query objects themselves should exercise the actual underlying query. But in other code stubbing the query
|
286
|
+
maybe desirable.
|
287
|
+
|
288
|
+
The spec helper method `stub_query(query_class, {results: ..., with: ...})` can do this for you.
|
289
|
+
|
290
|
+
It stubs `.new` on the Query object and returns instances of `EagerQuery` instead with the given `results`.
|
291
|
+
The `with` option is passed to the Query object on initialisation and used when setting up the method stub on the
|
292
|
+
query class.
|
293
|
+
|
294
|
+
For example:
|
295
|
+
|
296
|
+
```ruby
|
297
|
+
stub_query(TagQuery, with: {name: "Something"}, results: [t1, t2])
|
298
|
+
expect(TagQuery.new(name: "Something").first).to eql t1
|
299
|
+
```
|
300
|
+
|
301
|
+
*Note that*
|
302
|
+
|
303
|
+
This returns an instance of EagerQuery, so will not work for cases were the actual type of the query instance is
|
304
|
+
important or where you are doing a composition of queries backed by relations!
|
305
|
+
|
306
|
+
If `compose` will be used then `Quo::Query.compose` needs to be stubbed. Something might be possible to make this
|
307
|
+
nicer in future.
|
308
|
+
|
309
|
+
## Other reading
|
310
|
+
|
311
|
+
See:
|
312
|
+
* [Includes vs preload vs eager_load](http://blog.scoutapp.com/articles/2017/01/24/activerecord-includes-vs-joins-vs-preload-vs-eager_load-when-and-where)
|
313
|
+
* [Objects on Rails](http://objectsonrails.com/#sec-14)
|
314
|
+
|
315
|
+
|
316
|
+
## Installation
|
317
|
+
|
318
|
+
Install the gem and add to the application's Gemfile by executing:
|
319
|
+
|
320
|
+
$ bundle add quo
|
321
|
+
|
322
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
323
|
+
|
324
|
+
$ gem install quo
|
325
|
+
|
326
|
+
## Usage
|
327
|
+
|
328
|
+
TODO: Write usage instructions here
|
329
|
+
|
330
|
+
## Development
|
331
|
+
|
332
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
333
|
+
|
334
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
335
|
+
|
336
|
+
## Contributing
|
337
|
+
|
338
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/stevegeek/quo.
|
339
|
+
|
340
|
+
## Inspired by `rectify`
|
341
|
+
|
342
|
+
Note this implementation is loosely based on that in the `Rectify` gem; https://github.com/andypike/rectify.
|
343
|
+
|
344
|
+
See https://github.com/andypike/rectify#query-objects for more information.
|
345
|
+
|
346
|
+
Thanks to Andy Pike for the inspiration.
|
347
|
+
|
348
|
+
## License
|
349
|
+
|
350
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rake/testtask"
|
5
|
+
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs << "test"
|
8
|
+
t.libs << "lib"
|
9
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
10
|
+
end
|
11
|
+
|
12
|
+
require "standard/rake"
|
13
|
+
|
14
|
+
task default: %i[test standard]
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quo
|
4
|
+
class EagerQuery < Quo::Query
|
5
|
+
def initialize(**options)
|
6
|
+
@collection = Array.wrap(options[:collection])
|
7
|
+
super(**options.except(:collection))
|
8
|
+
end
|
9
|
+
|
10
|
+
# Optionally return the `total_count` option
|
11
|
+
def count
|
12
|
+
options[:total_count] || super
|
13
|
+
end
|
14
|
+
alias_method :total_count, :count
|
15
|
+
alias_method :size, :count
|
16
|
+
|
17
|
+
# Is this query object paged? (when no total count)
|
18
|
+
def paged?
|
19
|
+
options[:total_count].nil? && current_page.present?
|
20
|
+
end
|
21
|
+
|
22
|
+
# Return the underlying collection
|
23
|
+
def query
|
24
|
+
preload_includes(collection) if options[:includes]
|
25
|
+
collection
|
26
|
+
end
|
27
|
+
|
28
|
+
def relation?
|
29
|
+
false
|
30
|
+
end
|
31
|
+
|
32
|
+
def eager?
|
33
|
+
true
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
attr_reader :collection
|
39
|
+
|
40
|
+
def preload_includes(records, preload = nil)
|
41
|
+
::ActiveRecord::Associations::Preloader.new(
|
42
|
+
records: records,
|
43
|
+
associations: preload || options[:includes]
|
44
|
+
)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
require_relative "./utilities/callstack"
|
5
|
+
|
6
|
+
module Quo
|
7
|
+
class Enumerator
|
8
|
+
extend Forwardable
|
9
|
+
include Quo::Utilities::Callstack
|
10
|
+
|
11
|
+
def initialize(query, transformer: nil)
|
12
|
+
@query = query
|
13
|
+
@unwrapped = query.unwrap
|
14
|
+
@transformer = transformer
|
15
|
+
end
|
16
|
+
|
17
|
+
def_delegators :unwrapped,
|
18
|
+
:include?,
|
19
|
+
:member?,
|
20
|
+
:all?,
|
21
|
+
:any?,
|
22
|
+
:none?,
|
23
|
+
:one?,
|
24
|
+
:tally,
|
25
|
+
:count,
|
26
|
+
:group_by,
|
27
|
+
:partition,
|
28
|
+
:slice_before,
|
29
|
+
:slice_after,
|
30
|
+
:slice_when,
|
31
|
+
:chunk,
|
32
|
+
:chunk_while,
|
33
|
+
:sum,
|
34
|
+
:zip
|
35
|
+
|
36
|
+
# Delegate other enumerable methods to underlying collection but also transform
|
37
|
+
def method_missing(method, *args, &block)
|
38
|
+
if unwrapped.respond_to?(method)
|
39
|
+
debug_callstack
|
40
|
+
if block
|
41
|
+
unwrapped.send(method, *args) do |*block_args|
|
42
|
+
x = block_args.first
|
43
|
+
transformed = transformer.present? ? transformer.call(x) : x
|
44
|
+
block.call(transformed, *block_args[1..])
|
45
|
+
end
|
46
|
+
else
|
47
|
+
raw = unwrapped.send(method, *args)
|
48
|
+
return raw if raw.is_a?(Quo::Enumerator) || raw.is_a?(::Enumerator)
|
49
|
+
transform_results(raw)
|
50
|
+
end
|
51
|
+
else
|
52
|
+
super
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def respond_to_missing?(method, include_private = false)
|
57
|
+
Enumerable.instance_methods.include?(method) || super
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
attr_reader :transformer, :unwrapped
|
63
|
+
|
64
|
+
def transform_results(results)
|
65
|
+
return results unless transformer.present?
|
66
|
+
|
67
|
+
if results.is_a?(Enumerable)
|
68
|
+
results.map.with_index { |item, i| transformer.call(item, i) }
|
69
|
+
else
|
70
|
+
transformer.call(results)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quo
|
4
|
+
class MergedQuery < Quo::Query
|
5
|
+
def initialize(options, source_queries = [])
|
6
|
+
@source_queries = source_queries
|
7
|
+
super(**options)
|
8
|
+
end
|
9
|
+
|
10
|
+
def query
|
11
|
+
@scope
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
left = operand_desc(source_queries_left)
|
16
|
+
right = operand_desc(source_queries_right)
|
17
|
+
"Quo::MergedQuery[#{left}, #{right}]"
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def source_queries_left
|
23
|
+
source_queries&.first
|
24
|
+
end
|
25
|
+
|
26
|
+
def source_queries_right
|
27
|
+
source_queries&.last
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :source_queries
|
31
|
+
|
32
|
+
def operand_desc(operand)
|
33
|
+
return unless operand
|
34
|
+
if operand.is_a? Quo::MergedQuery
|
35
|
+
operand.to_s
|
36
|
+
else
|
37
|
+
operand.class.name
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/quo/query.rb
ADDED
@@ -0,0 +1,276 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./utilities/callstack"
|
4
|
+
require_relative "./utilities/compose"
|
5
|
+
require_relative "./utilities/sanitize"
|
6
|
+
require_relative "./utilities/wrap"
|
7
|
+
|
8
|
+
module Quo
|
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
|
+
# Execute the query and return the first item
|
18
|
+
def call(**options)
|
19
|
+
new(**options).first
|
20
|
+
end
|
21
|
+
|
22
|
+
# Execute the query and return the first item, or raise an error if no item is found
|
23
|
+
def call!(**options)
|
24
|
+
new(**options).first!
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_reader :current_page, :page_size, :options
|
29
|
+
|
30
|
+
def initialize(**options)
|
31
|
+
@options = options
|
32
|
+
@current_page = options[:page]&.to_i || options[:current_page]&.to_i
|
33
|
+
@page_size = options[:page_size]&.to_i || 20
|
34
|
+
@scope = unwrap_relation(options[:scope])
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns a active record query, or a Quo::Query instance
|
38
|
+
# You must provide an implementation of this of pass the 'scope' option on instantiation
|
39
|
+
def query
|
40
|
+
return @scope unless @scope.nil?
|
41
|
+
raise NotImplementedError, "Query objects must define a 'query' method"
|
42
|
+
end
|
43
|
+
|
44
|
+
# Combine (compose) this query object with another composeable entity, see notes for `.compose` above.
|
45
|
+
# Compose is aliased as `+`. Can optionally take `joins()` parameters to perform a joins before the merge
|
46
|
+
def compose(right, joins = nil)
|
47
|
+
Quo::QueryComposer.new(self, right, joins).compose
|
48
|
+
end
|
49
|
+
|
50
|
+
alias_method :+, :compose
|
51
|
+
|
52
|
+
def copy(**options)
|
53
|
+
self.class.new(**@options.merge(options))
|
54
|
+
end
|
55
|
+
|
56
|
+
# Methods to prepare the query
|
57
|
+
def limit(limit)
|
58
|
+
copy(limit: limit)
|
59
|
+
end
|
60
|
+
|
61
|
+
def order(options)
|
62
|
+
copy(order: options)
|
63
|
+
end
|
64
|
+
|
65
|
+
def group(*options)
|
66
|
+
copy(group: options)
|
67
|
+
end
|
68
|
+
|
69
|
+
def includes(*options)
|
70
|
+
copy(includes: options)
|
71
|
+
end
|
72
|
+
|
73
|
+
def preload(*options)
|
74
|
+
copy(preload: options)
|
75
|
+
end
|
76
|
+
|
77
|
+
def select(*options)
|
78
|
+
copy(select: options)
|
79
|
+
end
|
80
|
+
|
81
|
+
# The following methods actually execute the underlying query
|
82
|
+
|
83
|
+
# Delegate SQL calculation methods to the underlying query
|
84
|
+
delegate :sum, :average, :minimum, :maximum, to: :query_with_logging
|
85
|
+
|
86
|
+
# Gets the count of all results ignoring the current page and page size (if set)
|
87
|
+
delegate :count, to: :underlying_query
|
88
|
+
alias_method :total_count, :count
|
89
|
+
alias_method :size, :count
|
90
|
+
|
91
|
+
# Gets the actual count of elements in the page of results (assuming paging is being used, otherwise the count of
|
92
|
+
# all results)
|
93
|
+
def page_count
|
94
|
+
query_with_logging.count
|
95
|
+
end
|
96
|
+
|
97
|
+
# Delegate methods that let us get the model class (available on AR relations)
|
98
|
+
delegate :model, :klass, to: :underlying_query
|
99
|
+
|
100
|
+
# Get first elements
|
101
|
+
def first(*args)
|
102
|
+
if transform?
|
103
|
+
res = query_with_logging.first(*args)
|
104
|
+
if res.is_a? Array
|
105
|
+
res.map.with_index { |r, i| transformer.call(r, i) }
|
106
|
+
elsif !res.nil?
|
107
|
+
transformer.call(query_with_logging.first(*args))
|
108
|
+
end
|
109
|
+
else
|
110
|
+
query_with_logging.first(*args)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def first!(*args)
|
115
|
+
item = first(*args)
|
116
|
+
raise ActiveRecord::RecordNotFound, "No item could be found!" unless item
|
117
|
+
item
|
118
|
+
end
|
119
|
+
|
120
|
+
# Get last elements
|
121
|
+
def last(*args)
|
122
|
+
if transform?
|
123
|
+
res = query_with_logging.last(*args)
|
124
|
+
if res.is_a? Array
|
125
|
+
res.map.with_index { |r, i| transformer.call(r, i) }
|
126
|
+
elsif !res.nil?
|
127
|
+
transformer.call(query_with_logging.last(*args))
|
128
|
+
end
|
129
|
+
else
|
130
|
+
query_with_logging.last(*args)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Convert to array
|
135
|
+
def to_a
|
136
|
+
arr = query_with_logging.to_a
|
137
|
+
transform? ? arr.map.with_index { |r, i| transformer.call(r, i) } : arr
|
138
|
+
end
|
139
|
+
|
140
|
+
# Convert to EagerQuery, and load all data
|
141
|
+
def to_eager(more_opts = {})
|
142
|
+
Quo::EagerQuery.new(collection: to_a, **options.merge(more_opts))
|
143
|
+
end
|
144
|
+
alias_method :load, :to_eager
|
145
|
+
|
146
|
+
# Return an enumerable
|
147
|
+
def enumerator
|
148
|
+
Quo::Enumerator.new(self, transformer: transformer)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Some convenience methods for iterating over the results
|
152
|
+
delegate :each, :map, :flat_map, :reduce, :reject, :filter, to: :enumerator
|
153
|
+
|
154
|
+
# Set a block used to transform data after query fetching
|
155
|
+
def transform(&block)
|
156
|
+
@options[:__transformer] = block
|
157
|
+
self
|
158
|
+
end
|
159
|
+
|
160
|
+
# Are there any results for this query?
|
161
|
+
def exists?
|
162
|
+
return query_with_logging.exists? if relation?
|
163
|
+
query_with_logging.present?
|
164
|
+
end
|
165
|
+
|
166
|
+
# Are there no results for this query?
|
167
|
+
def none?
|
168
|
+
!exists?
|
169
|
+
end
|
170
|
+
alias_method :empty?, :none?
|
171
|
+
|
172
|
+
# Is this query object a relation under the hood? (ie not eager loaded)
|
173
|
+
def relation?
|
174
|
+
test_relation(configured_query)
|
175
|
+
end
|
176
|
+
|
177
|
+
# Is this query object eager loaded data under the hood? (ie not a relation)
|
178
|
+
def eager?
|
179
|
+
test_eager(configured_query)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Is this query object paged? (ie is paging enabled)
|
183
|
+
def paged?
|
184
|
+
current_page.present?
|
185
|
+
end
|
186
|
+
|
187
|
+
# Is this query object transforming results?
|
188
|
+
def transform?
|
189
|
+
transformer.present?
|
190
|
+
end
|
191
|
+
|
192
|
+
# Return the SQL string for this query if its a relation type query object
|
193
|
+
def to_sql
|
194
|
+
configured_query.to_sql if relation?
|
195
|
+
end
|
196
|
+
|
197
|
+
# Unwrap the underlying query
|
198
|
+
def unwrap
|
199
|
+
configured_query
|
200
|
+
end
|
201
|
+
|
202
|
+
delegate :distinct, to: :configured_query
|
203
|
+
|
204
|
+
protected
|
205
|
+
|
206
|
+
def formatted_queries?
|
207
|
+
Quo.configuration&.formatted_query_log
|
208
|
+
end
|
209
|
+
|
210
|
+
# 'trim' a query, ie remove comments and remove newlines
|
211
|
+
# This will remove dashes from inside strings too
|
212
|
+
def trim_query(sql)
|
213
|
+
sql.gsub(/--[^\n'"]*\n/m, " ").tr("\n", " ").strip
|
214
|
+
end
|
215
|
+
|
216
|
+
def format_query(sql_str)
|
217
|
+
formatted_queries? ? sql_str : trim_query(sql_str)
|
218
|
+
end
|
219
|
+
|
220
|
+
def transformer
|
221
|
+
options[:__transformer]
|
222
|
+
end
|
223
|
+
|
224
|
+
def offset
|
225
|
+
per_page = sanitised_page_size
|
226
|
+
page = current_page.positive? ? current_page : 1
|
227
|
+
per_page * (page - 1)
|
228
|
+
end
|
229
|
+
|
230
|
+
# The configured query is the underlying query with paging
|
231
|
+
def configured_query
|
232
|
+
q = underlying_query
|
233
|
+
return q unless paged? && q.is_a?(ActiveRecord::Relation)
|
234
|
+
q.offset(offset).limit(sanitised_page_size)
|
235
|
+
end
|
236
|
+
|
237
|
+
def sanitised_page_size
|
238
|
+
(page_size.present? && page_size.positive?) ? [page_size.to_i, 200].min : 20
|
239
|
+
end
|
240
|
+
|
241
|
+
def query_with_logging
|
242
|
+
debug_callstack
|
243
|
+
configured_query
|
244
|
+
end
|
245
|
+
|
246
|
+
# The underlying query is essentially the configured query with optional extras setup
|
247
|
+
def underlying_query
|
248
|
+
@underlying_query ||=
|
249
|
+
begin
|
250
|
+
rel = unwrap_relation(query)
|
251
|
+
unless test_eager(rel)
|
252
|
+
rel = rel.group(@options[:group]) if @options[:group].present?
|
253
|
+
rel = rel.order(@options[:order]) if @options[:order].present?
|
254
|
+
rel = rel.limit(@options[:limit]) if @options[:limit].present?
|
255
|
+
rel = rel.preload(@options[:preload]) if @options[:preload].present?
|
256
|
+
rel = rel.includes(@options[:includes]) if @options[:includes].present?
|
257
|
+
rel = rel.joins(@options[:joins]) if @options[:joins].present?
|
258
|
+
rel = rel.select(@options[:select]) if @options[:select].present?
|
259
|
+
end
|
260
|
+
rel
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def unwrap_relation(query)
|
265
|
+
query.is_a?(Quo::Query) ? query.unwrap : query
|
266
|
+
end
|
267
|
+
|
268
|
+
def test_eager(rel)
|
269
|
+
rel.is_a?(Enumerable) && !test_relation(rel)
|
270
|
+
end
|
271
|
+
|
272
|
+
def test_relation(rel)
|
273
|
+
rel.is_a?(ActiveRecord::Relation)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quo
|
4
|
+
class QueryComposer
|
5
|
+
def initialize(left, right, joins = nil)
|
6
|
+
@left = left
|
7
|
+
@right = right
|
8
|
+
@joins = joins
|
9
|
+
end
|
10
|
+
|
11
|
+
def compose
|
12
|
+
combined = merge
|
13
|
+
Quo::MergedQuery.new(
|
14
|
+
merged_options.merge({scope: combined, source_queries: [left, right]})
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :left, :right, :joins
|
21
|
+
|
22
|
+
def merge
|
23
|
+
left_rel, right_rel = unwrap_relations
|
24
|
+
left_type, right_type = relation_types?
|
25
|
+
if both_relations?(left_type, right_type)
|
26
|
+
apply_joins(left_rel, joins).merge(right_rel)
|
27
|
+
elsif left_relation_right_eager?(left_type, right_type)
|
28
|
+
left_rel.to_a + right_rel
|
29
|
+
elsif left_eager_right_relation?(left_rel, left_type, right_type)
|
30
|
+
left_rel + right_rel.to_a
|
31
|
+
elsif both_eager_loaded?(left_rel, left_type, right_type)
|
32
|
+
left_rel + right_rel
|
33
|
+
else
|
34
|
+
raise_error
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def merged_options
|
39
|
+
return left.options.merge(right.options) if left.is_a?(Quo::Query) && right.is_a?(Quo::Query)
|
40
|
+
return left.options if left.is_a?(Quo::Query)
|
41
|
+
return right.options if right.is_a?(Quo::Query)
|
42
|
+
{}
|
43
|
+
end
|
44
|
+
|
45
|
+
def relation_types?
|
46
|
+
[left, right].map do |query|
|
47
|
+
if query.is_a?(Quo::Query)
|
48
|
+
query.relation?
|
49
|
+
else
|
50
|
+
query.is_a?(ActiveRecord::Relation)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def apply_joins(left_rel, joins)
|
56
|
+
joins.present? ? left_rel.joins(joins.to_sym) : left_rel
|
57
|
+
end
|
58
|
+
|
59
|
+
def both_relations?(left_rel_type, right_rel_type)
|
60
|
+
left_rel_type && right_rel_type
|
61
|
+
end
|
62
|
+
|
63
|
+
def left_relation_right_eager?(left_rel_type, right_rel_type)
|
64
|
+
left_rel_type && !right_rel_type
|
65
|
+
end
|
66
|
+
|
67
|
+
def left_eager_right_relation?(left_rel, left_rel_type, right_rel_type)
|
68
|
+
!left_rel_type && right_rel_type && left_rel.respond_to?(:+)
|
69
|
+
end
|
70
|
+
|
71
|
+
def both_eager_loaded?(left_rel, left_rel_type, right_rel_type)
|
72
|
+
!left_rel_type && !right_rel_type && left_rel.respond_to?(:+)
|
73
|
+
end
|
74
|
+
|
75
|
+
def unwrap_relations
|
76
|
+
[left, right].map { |query| query.is_a?(Quo::Query) ? query.unwrap : query }
|
77
|
+
end
|
78
|
+
|
79
|
+
def raise_error
|
80
|
+
raise ArgumentError, "Unable to composite queries #{left.class.name} and " \
|
81
|
+
"#{right.class.name}. You cannot compose queries where #query " \
|
82
|
+
"returns an ActiveRecord::Relation in one and an Enumerable in the other."
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/quo/railtie.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quo
|
4
|
+
module Rspec
|
5
|
+
module Helpers
|
6
|
+
def stub_query(query_class, options = {})
|
7
|
+
results = options.fetch(:results, [])
|
8
|
+
with = options[:with]
|
9
|
+
unless with.nil?
|
10
|
+
return(
|
11
|
+
allow(query_class).to receive(:new)
|
12
|
+
.with(with) { ::Quo::EagerQuery.new(collection: results) }
|
13
|
+
)
|
14
|
+
end
|
15
|
+
allow(query_class).to receive(:new) { ::Quo::EagerQuery.new(collection: results) }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quo
|
4
|
+
module Utilities
|
5
|
+
module Callstack
|
6
|
+
def debug_callstack
|
7
|
+
return unless Quo.configuration&.query_show_callstack_size&.positive? && Rails.env.development?
|
8
|
+
max_stack = Quo.configuration.query_show_callstack_size
|
9
|
+
working_dir = Dir.pwd
|
10
|
+
exclude = %r{/(gems/|rubies/|query\.rb)}
|
11
|
+
stack = Kernel.caller.grep_v(exclude).map { |l| l.gsub(working_dir + "/", "") }
|
12
|
+
trace_message = stack[0..max_stack].join("\n &> ")
|
13
|
+
message = "\n[Query stack]: -> #{trace_message}\n"
|
14
|
+
message += " (truncated to #{max_stack} most recent)" if stack.size > max_stack
|
15
|
+
Quo.configuration.logger&.info(message)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quo
|
4
|
+
module Utilities
|
5
|
+
module Compose
|
6
|
+
# Combine two query-like or composeable entities:
|
7
|
+
# These can be Quo::Query, Quo::MergedQuery, Quo::EagerQuery and ActiveRecord::Relations.
|
8
|
+
# See the `README.md` docs for more details.
|
9
|
+
def compose(query1, query2, joins = nil)
|
10
|
+
Quo::QueryComposer.new(query1, query2, joins).compose
|
11
|
+
end
|
12
|
+
|
13
|
+
# Determines if the object `query` is something which can be composed with query objects
|
14
|
+
def composable_with?(query)
|
15
|
+
query.is_a?(Quo::Query) || query.is_a?(ActiveRecord::Relation)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quo
|
4
|
+
module Utilities
|
5
|
+
module Sanitize
|
6
|
+
# ActiveRecord::Sanitization wrappers
|
7
|
+
def sanitize_sql_for_conditions(conditions)
|
8
|
+
ActiveRecord::Base.sanitize_sql_for_conditions(conditions)
|
9
|
+
end
|
10
|
+
|
11
|
+
def sanitize_sql_string(string)
|
12
|
+
sanitize_sql_for_conditions(["'%s'", string])
|
13
|
+
end
|
14
|
+
|
15
|
+
def sanitize_sql_parameter(value)
|
16
|
+
sanitize_sql_for_conditions(["?", value])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Quo
|
4
|
+
module Utilities
|
5
|
+
module Wrap
|
6
|
+
# Wrap a relation in a Query. If the passed in object is already a query object then just return it
|
7
|
+
def wrap(query_rel_or_data, **options)
|
8
|
+
if query_rel_or_data.is_a?(Quo::Query) && options.present?
|
9
|
+
return query_rel_or_data.copy(**options)
|
10
|
+
end
|
11
|
+
return query_rel_or_data if query_rel_or_data.is_a? Quo::Query
|
12
|
+
if query_rel_or_data.is_a? ActiveRecord::Relation
|
13
|
+
return new(**options.merge(scope: query_rel_or_data))
|
14
|
+
end
|
15
|
+
Quo::EagerQuery.new(**options.merge(collection: query_rel_or_data))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/quo/version.rb
ADDED
data/lib/quo.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "quo/version"
|
4
|
+
require_relative "quo/query"
|
5
|
+
require_relative "quo/eager_query"
|
6
|
+
require_relative "quo/merged_query"
|
7
|
+
require_relative "quo/query_composer"
|
8
|
+
require_relative "quo/enumerator"
|
9
|
+
|
10
|
+
module Quo
|
11
|
+
class << self
|
12
|
+
def configuration
|
13
|
+
@configuration ||= Configuration.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def configure
|
17
|
+
yield(configuration) if block_given?
|
18
|
+
configuration
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Configuration
|
23
|
+
attr_accessor :formatted_query_log, :query_show_callstack_size, :logger
|
24
|
+
|
25
|
+
def initialize
|
26
|
+
@formatted_query_log = true
|
27
|
+
@query_show_callstack_size = 10
|
28
|
+
@logger = nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/sig/quo.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: quo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Stephen Ierodiaconou
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-11-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '6'
|
27
|
+
description: Quo query objects are composable.
|
28
|
+
email:
|
29
|
+
- stevegeek@gmail.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- ".standard.yml"
|
35
|
+
- CHANGELOG.md
|
36
|
+
- Gemfile
|
37
|
+
- LICENSE.txt
|
38
|
+
- README.md
|
39
|
+
- Rakefile
|
40
|
+
- lib/quo.rb
|
41
|
+
- lib/quo/eager_query.rb
|
42
|
+
- lib/quo/enumerator.rb
|
43
|
+
- lib/quo/merged_query.rb
|
44
|
+
- lib/quo/query.rb
|
45
|
+
- lib/quo/query_composer.rb
|
46
|
+
- lib/quo/railtie.rb
|
47
|
+
- lib/quo/rspec/helpers.rb
|
48
|
+
- lib/quo/utilities/callstack.rb
|
49
|
+
- lib/quo/utilities/compose.rb
|
50
|
+
- lib/quo/utilities/sanitize.rb
|
51
|
+
- lib/quo/utilities/wrap.rb
|
52
|
+
- lib/quo/version.rb
|
53
|
+
- sig/quo.rbs
|
54
|
+
homepage: https://github.com/stevegeek/quo
|
55
|
+
licenses:
|
56
|
+
- MIT
|
57
|
+
metadata:
|
58
|
+
homepage_uri: https://github.com/stevegeek/quo
|
59
|
+
source_code_uri: https://github.com/stevegeek/quo
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options: []
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 2.7.0
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
requirements: []
|
75
|
+
rubygems_version: 3.3.7
|
76
|
+
signing_key:
|
77
|
+
specification_version: 4
|
78
|
+
summary: Quo is a query object gem for Rails
|
79
|
+
test_files: []
|