quo 1.0.0.beta2 → 2.0.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 +4 -4
- data/.devcontainer/Dockerfile +17 -0
- data/.devcontainer/compose.yml +10 -0
- data/.devcontainer/devcontainer.json +12 -0
- data/Appraisals +4 -12
- data/CHANGELOG.md +112 -1
- data/CLAUDE.md +19 -0
- data/Gemfile +7 -1
- data/LICENSE.txt +1 -1
- data/README.md +496 -203
- data/Rakefile +66 -6
- data/UPGRADING.md +216 -0
- data/badges/coverage_badge_total.svg +35 -0
- data/badges/rubycritic_badge_score.svg +35 -0
- data/claude-skill/README.md +100 -0
- data/claude-skill/SKILL.md +442 -0
- data/claude-skill/references/API_REFERENCE.md +462 -0
- data/claude-skill/references/COMPOSITION.md +396 -0
- data/claude-skill/references/PAGINATION.md +396 -0
- data/claude-skill/references/QUERY_TYPES.md +297 -0
- data/claude-skill/references/TRANSFORMERS.md +282 -0
- data/context/01-core-architecture.md +247 -0
- data/context/02-query-types-implementation.md +355 -0
- data/context/03-composition-transformation.md +441 -0
- data/context/04-pagination-results.md +485 -0
- data/context/05-testing-configuration.md +491 -0
- data/context/06-advanced-patterns-examples.md +153 -0
- data/gemfiles/rails_8.0.gemfile +10 -5
- data/gemfiles/rails_8.1.gemfile +20 -0
- data/lib/generators/quo/install/USAGE +21 -0
- data/lib/generators/quo/install/install_generator.rb +63 -0
- data/lib/quo/collection_backed_query.rb +21 -15
- data/lib/quo/collection_results.rb +1 -0
- data/lib/quo/composed_collection_backed_query.rb +42 -0
- data/lib/quo/composed_instance.rb +144 -0
- data/lib/quo/composed_query.rb +43 -178
- data/lib/quo/composed_relation_backed_query.rb +42 -0
- data/lib/quo/composing/base_strategy.rb +22 -0
- data/lib/quo/composing/class_strategy.rb +86 -0
- data/lib/quo/composing/class_strategy_registry.rb +31 -0
- data/lib/quo/composing/query_classes_strategy.rb +38 -0
- data/lib/quo/composing.rb +81 -0
- data/lib/quo/engine.rb +1 -0
- data/lib/quo/minitest/helpers.rb +14 -24
- data/lib/quo/preloadable.rb +1 -0
- data/lib/quo/query.rb +22 -5
- data/lib/quo/relation_backed_query.rb +24 -18
- data/lib/quo/relation_backed_query_specification.rb +44 -25
- data/lib/quo/relation_results.rb +1 -0
- data/lib/quo/results.rb +31 -2
- data/lib/quo/rspec/helpers.rb +15 -26
- data/lib/quo/testing/collection_backed_fake.rb +1 -0
- data/lib/quo/testing/fake_helpers.rb +30 -0
- data/lib/quo/testing/relation_backed_fake.rb +1 -0
- data/lib/quo/version.rb +1 -1
- data/lib/quo/wrapped_collection_backed_query.rb +21 -0
- data/lib/quo/wrapped_relation_backed_query.rb +21 -0
- data/lib/quo.rb +8 -0
- data/quo.png +0 -0
- data/sig/generated/quo/collection_backed_query.rbs +10 -4
- data/sig/generated/quo/collection_results.rbs +1 -0
- data/sig/generated/quo/composed_collection_backed_query.rbs +25 -0
- data/sig/generated/quo/composed_instance.rbs +61 -0
- data/sig/generated/quo/composed_query.rbs +23 -56
- data/sig/generated/quo/composed_relation_backed_query.rbs +25 -0
- data/sig/generated/quo/composing/base_strategy.rbs +16 -0
- data/sig/generated/quo/composing/class_strategy.rbs +38 -0
- data/sig/generated/quo/composing/class_strategy_registry.rbs +16 -0
- data/sig/generated/quo/composing/query_classes_strategy.rbs +24 -0
- data/sig/generated/quo/composing.rbs +40 -0
- data/sig/generated/quo/engine.rbs +1 -0
- data/sig/generated/quo/minitest/helpers.rbs +12 -0
- data/sig/generated/quo/preloadable.rbs +1 -0
- data/sig/generated/quo/query.rbs +15 -4
- data/sig/generated/quo/relation_backed_query.rbs +15 -5
- data/sig/generated/quo/relation_backed_query_specification.rbs +47 -39
- data/sig/generated/quo/relation_results.rbs +1 -0
- data/sig/generated/quo/results.rbs +11 -0
- data/sig/generated/quo/rspec/helpers.rbs +12 -0
- data/sig/generated/quo/testing/collection_backed_fake.rbs +1 -0
- data/sig/generated/quo/testing/fake_helpers.rbs +14 -0
- data/sig/generated/quo/testing/relation_backed_fake.rbs +1 -0
- data/sig/generated/quo/wrapped_collection_backed_query.rbs +13 -0
- data/sig/generated/quo/wrapped_relation_backed_query.rbs +13 -0
- data/sig/generated/quo.rbs +1 -0
- data/website/.gitignore +6 -0
- data/website/.nojekyll +0 -0
- data/website/404.html +26 -0
- data/website/Gemfile +24 -0
- data/website/_config.yml +50 -0
- data/website/_data/navigation.yml +8 -0
- data/website/_data/sidebar.yml +2 -0
- data/website/_data/social_links.yml +3 -0
- data/website/_docs/api.md +261 -0
- data/website/_docs/get-started.md +289 -0
- data/website/assets/quo.png +0 -0
- data/website/index.md +141 -0
- metadata +70 -13
- data/gemfiles/rails_7.0.gemfile +0 -15
- data/gemfiles/rails_7.1.gemfile +0 -15
- data/gemfiles/rails_7.2.gemfile +0 -15
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
> **Targets Quo `~> 2.0`.**
|
|
4
|
+
|
|
5
|
+
## Query class methods
|
|
6
|
+
|
|
7
|
+
### `.wrap(query = nil, props: {}, &block)`
|
|
8
|
+
|
|
9
|
+
Create a query class without defining a full subclass.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# Wrap a relation
|
|
13
|
+
RecentComments = Quo::RelationBackedQuery.wrap(Comment.where("created_at > ?", 1.day.ago))
|
|
14
|
+
RecentComments.new.results
|
|
15
|
+
|
|
16
|
+
# Wrap with typed props inside a block
|
|
17
|
+
CommentsByAuthor = Quo::RelationBackedQuery.wrap(props: {author_id: Integer}) do
|
|
18
|
+
Comment.joins(:post).where(posts: {author_id: author_id})
|
|
19
|
+
end
|
|
20
|
+
CommentsByAuthor.new(author_id: 1).results
|
|
21
|
+
|
|
22
|
+
# Wrap a collection
|
|
23
|
+
CachedAuthors = Quo::CollectionBackedQuery.wrap do
|
|
24
|
+
Rails.cache.fetch("authors") { Author.all.to_a }
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Returns:** Query class.
|
|
29
|
+
|
|
30
|
+
**Performance note:** `wrap` allocates a new class on each call. Treat it
|
|
31
|
+
as type-definition: assign the result to a constant. Don't call
|
|
32
|
+
`Quo::RelationBackedQuery.wrap(rel).new` inside a method that runs many
|
|
33
|
+
times per request.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
### `.compose(right, joins: nil)` (alias `+`)
|
|
38
|
+
|
|
39
|
+
Compose two query classes. Returns a new class that, when instantiated,
|
|
40
|
+
runs both underlying queries merged together.
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
ComposedClass = ActiveCommentsQuery.compose(NonSpamCommentsQuery)
|
|
44
|
+
# Equivalent to: ComposedClass = ActiveCommentsQuery + NonSpamCommentsQuery
|
|
45
|
+
ComposedClass.new(score: 0.5).results
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Parameters:**
|
|
49
|
+
- `right` — Query class to compose with
|
|
50
|
+
- `joins:` — optional join argument (Symbol/Hash/Array) for the AR merge
|
|
51
|
+
|
|
52
|
+
**Returns:** Composed query class.
|
|
53
|
+
|
|
54
|
+
See `references/COMPOSITION.md` for class-vs-instance composition guidance.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Query instance methods
|
|
59
|
+
|
|
60
|
+
### `#initialize(**props)`
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
query = CommentsByAuthorQuery.new(
|
|
64
|
+
author_id: 1,
|
|
65
|
+
since: 1.day.ago,
|
|
66
|
+
page: 1,
|
|
67
|
+
page_size: 25
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Parameters:** keyword arguments matching the query's `prop` declarations,
|
|
72
|
+
plus `page` and `page_size`.
|
|
73
|
+
|
|
74
|
+
**Raises:** `Literal::TypeError` on type mismatch or missing required props.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
### `#query` (RelationBackedQuery — must implement)
|
|
79
|
+
|
|
80
|
+
Return an `ActiveRecord::Relation` (or another `Quo::Query`).
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
def query
|
|
84
|
+
Comment.where(read: false).order(:created_at)
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
### `#collection` (CollectionBackedQuery — must implement)
|
|
91
|
+
|
|
92
|
+
Return an `Enumerable`.
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
def collection
|
|
96
|
+
items.select { |i| i.score > 0.5 }
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
### `#results`
|
|
103
|
+
|
|
104
|
+
Run the query and return a `Quo::Results`.
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
results = query.results
|
|
108
|
+
results.each { |row| ... }
|
|
109
|
+
results.count
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Returns:** `Quo::Results`.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
### `#copy(**overrides)`
|
|
117
|
+
|
|
118
|
+
Return a new query instance with overridden props. Doesn't mutate `self`.
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
page_2 = query.copy(page: 2)
|
|
122
|
+
larger = query.copy(page_size: 100)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Returns:** new query instance of the same class.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### `#merge(right, joins: nil)` (alias `+` for instances)
|
|
130
|
+
|
|
131
|
+
Compose two query instances. Returns a value-shaped query, no class
|
|
132
|
+
allocation.
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
left = CommentsByAuthorQuery.new(author_id: 1)
|
|
136
|
+
right = UnreadCommentsQuery.new
|
|
137
|
+
|
|
138
|
+
merged = left.merge(right)
|
|
139
|
+
# Or: merged = left + right
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Parameters:**
|
|
143
|
+
- `right` — query instance, AR relation, or enumerable
|
|
144
|
+
- `joins:` — optional join argument (Symbol/Hash/Array) for the AR merge
|
|
145
|
+
|
|
146
|
+
**Returns:** new composed query instance.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
### `#transform(&block)`
|
|
151
|
+
|
|
152
|
+
Attach a transformer that runs on each row of `results`.
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
query = CommentsByAuthorQuery.new(author_id: 1)
|
|
156
|
+
.transform { |c| CommentPresenter.new(c) }
|
|
157
|
+
|
|
158
|
+
query.results.first # => CommentPresenter
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Returns:** new query instance with the transformer attached.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
### `#next_page_query` / `#previous_page_query`
|
|
166
|
+
|
|
167
|
+
Return new query instances at adjacent pages. Both require a non-nil
|
|
168
|
+
`page` and raise `NoMethodError` otherwise (they compute `page + 1` /
|
|
169
|
+
`page - 1`).
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
query = CommentsByAuthorQuery.new(author_id: 1, page: 1, page_size: 25)
|
|
173
|
+
query.next_page_query.page # => 2
|
|
174
|
+
query.copy(page: 5).previous_page_query.page # => 4
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
### `#offset`
|
|
180
|
+
|
|
181
|
+
Computed: `(page - 1) * page_size`.
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
query = CommentsByAuthorQuery.new(author_id: 1, page: 3, page_size: 25)
|
|
185
|
+
query.offset # => 50
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
### `#unwrap` / `#unwrap_unpaginated`
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
query = CommentsByAuthorQuery.new(author_id: 1, page: 2, page_size: 25)
|
|
194
|
+
|
|
195
|
+
query.unwrap # AR::Relation w/ LIMIT 25 OFFSET 25
|
|
196
|
+
query.unwrap_unpaginated # AR::Relation, no LIMIT/OFFSET
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
For `CollectionBackedQuery`, `#unwrap` returns a paginated array slice
|
|
200
|
+
and `#unwrap_unpaginated` returns the full enumerable.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
### `#to_sql` (RelationBackedQuery only)
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
CommentsByAuthorQuery.new(author_id: 1).to_sql
|
|
208
|
+
# => "SELECT ... FROM comments INNER JOIN posts ..."
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
### `#to_collection`
|
|
214
|
+
|
|
215
|
+
Materialise a `RelationBackedQuery` into a `CollectionBackedQuery`.
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
relation_q = CommentsByAuthorQuery.new(author_id: 1)
|
|
219
|
+
collection_q = relation_q.to_collection
|
|
220
|
+
collection_q.collection? # => true
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
### Predicates
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
query.relation? # backed by AR relation?
|
|
229
|
+
query.collection? # backed by enumerable?
|
|
230
|
+
query.paged? # pagination enabled?
|
|
231
|
+
query.transform? # transformer attached?
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
### Property accessors
|
|
237
|
+
|
|
238
|
+
`#page`, `#page_size`, plus accessors for any `prop` you declared.
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
query = CommentsByAuthorQuery.new(author_id: 1, page: 3, page_size: 50)
|
|
242
|
+
query.page # => 3
|
|
243
|
+
query.page_size # => 50
|
|
244
|
+
query.author_id # => 1
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Fluent spec API (RelationBackedQuery only)
|
|
250
|
+
|
|
251
|
+
`Quo::RelationBackedQuery` routes spec-style method calls through
|
|
252
|
+
`method_missing` to a `Quo::RelationBackedQuerySpecification`. Each call
|
|
253
|
+
returns a new query instance with the spec updated; chains compose.
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
q = UnreadCommentsQuery.new
|
|
257
|
+
.where(read: false)
|
|
258
|
+
.order(created_at: :desc)
|
|
259
|
+
.joins(:post)
|
|
260
|
+
.includes(:author)
|
|
261
|
+
.limit(10)
|
|
262
|
+
.distinct
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Available methods (mirror their `ActiveRecord::Relation` counterparts):
|
|
266
|
+
|
|
267
|
+
- `#where(conditions)`
|
|
268
|
+
- `#order(order_clause)`
|
|
269
|
+
- `#reorder(order_clause)`
|
|
270
|
+
- `#group(*columns)`
|
|
271
|
+
- `#limit(value)`
|
|
272
|
+
- `#offset(value)`
|
|
273
|
+
- `#select(*fields)`
|
|
274
|
+
- `#joins(*tables)` — accepts multiple table args (Symbol, Hash, Array)
|
|
275
|
+
- `#left_outer_joins(*tables)`
|
|
276
|
+
- `#includes(*associations)`
|
|
277
|
+
- `#preload(*associations)`
|
|
278
|
+
- `#eager_load(*associations)`
|
|
279
|
+
- `#distinct(enabled = true)`
|
|
280
|
+
- `#extending(*modules)`
|
|
281
|
+
- `#unscope(*args)`
|
|
282
|
+
|
|
283
|
+
Each returns a new query; the underlying `_specification` is built up
|
|
284
|
+
immutably.
|
|
285
|
+
|
|
286
|
+
### `#with(options = {})`
|
|
287
|
+
|
|
288
|
+
Merge multiple spec options at once via a hash.
|
|
289
|
+
|
|
290
|
+
```ruby
|
|
291
|
+
q.with(
|
|
292
|
+
where: {read: false},
|
|
293
|
+
order: {created_at: :desc},
|
|
294
|
+
limit: 10
|
|
295
|
+
)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### `#with_specification(specification)`
|
|
299
|
+
|
|
300
|
+
Replace the entire spec on a copy of the query.
|
|
301
|
+
|
|
302
|
+
```ruby
|
|
303
|
+
spec = Quo::RelationBackedQuerySpecification.new(limit: 5)
|
|
304
|
+
q.with_specification(spec)
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Specs added on a composed instance apply to the merged relation at
|
|
308
|
+
unwrap time, on top of any specs on the individual operands.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Quo::Results methods
|
|
313
|
+
|
|
314
|
+
### Counts
|
|
315
|
+
|
|
316
|
+
| Method | Returns |
|
|
317
|
+
|---|---|
|
|
318
|
+
| `#count` | total rows (across all pages) |
|
|
319
|
+
| `#page_count` | rows in the current page |
|
|
320
|
+
| `#empty?` | true when there are no rows |
|
|
321
|
+
| `#exists?` | true when there's at least one row |
|
|
322
|
+
|
|
323
|
+
### Enumerable
|
|
324
|
+
|
|
325
|
+
`Quo::Results` includes `Enumerable` and delegates `#each`, `#map`,
|
|
326
|
+
`#select`, `#reject`, `#first`, `#last`, `#find`, `#group_by`, `#to_a`.
|
|
327
|
+
If a transformer is set, each yielded row passes through it.
|
|
328
|
+
|
|
329
|
+
```ruby
|
|
330
|
+
results.each { |c| ... }
|
|
331
|
+
results.map(&:body)
|
|
332
|
+
results.select { |c| c.read? }
|
|
333
|
+
results.group_by(&:read)
|
|
334
|
+
results.to_a
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### `#transform?`
|
|
338
|
+
|
|
339
|
+
Boolean — was a transformer attached to the query?
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## Property type reference
|
|
344
|
+
|
|
345
|
+
Quo uses [Literal](https://github.com/joeldrapper/literal). Its helper
|
|
346
|
+
methods on Quo classes are prefixed with underscore.
|
|
347
|
+
|
|
348
|
+
### Primitives
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
prop :name, String
|
|
352
|
+
prop :count, Integer
|
|
353
|
+
prop :price, Float
|
|
354
|
+
prop :enabled, _Boolean # NB: _Boolean, not Boolean
|
|
355
|
+
prop :data, Hash
|
|
356
|
+
prop :items, Array
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Custom classes
|
|
360
|
+
|
|
361
|
+
```ruby
|
|
362
|
+
prop :author, Author
|
|
363
|
+
prop :post, Post
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Arrays / Nilable / Unions
|
|
367
|
+
|
|
368
|
+
```ruby
|
|
369
|
+
prop :tags, _Array(String)
|
|
370
|
+
prop :ids, _Array(Integer)
|
|
371
|
+
prop :since, _Nilable(Time)
|
|
372
|
+
prop :id_or_slug, _Union(String, Integer)
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Defaults
|
|
376
|
+
|
|
377
|
+
Use a lambda for any non-frozen default to avoid shared mutable state.
|
|
378
|
+
|
|
379
|
+
```ruby
|
|
380
|
+
prop :tags, _Array(String), default: -> { [] }
|
|
381
|
+
prop :since, Time, default: -> { 1.day.ago }
|
|
382
|
+
prop :page_size, Integer, default: -> { 20 }
|
|
383
|
+
prop :status, String, default: "pending".freeze
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## Configuration
|
|
389
|
+
|
|
390
|
+
```ruby
|
|
391
|
+
# config/initializers/quo.rb
|
|
392
|
+
Quo.default_page_size = 25
|
|
393
|
+
Quo.max_page_size = 200
|
|
394
|
+
Quo.relation_backed_query_base_class = "ApplicationRelationQuery"
|
|
395
|
+
Quo.collection_backed_query_base_class = "ApplicationCollectionQuery"
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
The base class options let you set application-level defaults (e.g. a
|
|
399
|
+
`hello` method shared by every relation-backed query) by subclassing the
|
|
400
|
+
base classes once and pointing Quo at the subclass.
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## Errors
|
|
405
|
+
|
|
406
|
+
### `Literal::TypeError`
|
|
407
|
+
|
|
408
|
+
Raised at `#initialize` when a prop value violates its declared type.
|
|
409
|
+
|
|
410
|
+
```ruby
|
|
411
|
+
class StrictQuery < Quo::RelationBackedQuery
|
|
412
|
+
prop :author_id, Integer
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
StrictQuery.new(author_id: "1")
|
|
416
|
+
# => Literal::TypeError: author_id is "1", expected Integer
|
|
417
|
+
|
|
418
|
+
StrictQuery.new
|
|
419
|
+
# => Literal::TypeError: author_id is nil, expected Integer
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### `ArgumentError`
|
|
423
|
+
|
|
424
|
+
Raised by `Quo::Composing.composer` / `Quo::Composing.merge_instances`
|
|
425
|
+
if the operands aren't a valid combination.
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## End-to-end example
|
|
430
|
+
|
|
431
|
+
```ruby
|
|
432
|
+
class CommentsByAuthorQuery < Quo::RelationBackedQuery
|
|
433
|
+
prop :author_id, Integer
|
|
434
|
+
prop :since, _Nilable(Time)
|
|
435
|
+
prop :include_read, _Boolean, default: -> { true }
|
|
436
|
+
|
|
437
|
+
def query
|
|
438
|
+
scope = Comment
|
|
439
|
+
.joins(:post)
|
|
440
|
+
.where(posts: {author_id: author_id})
|
|
441
|
+
.order(created_at: :desc)
|
|
442
|
+
scope = scope.where("comments.created_at > ?", since) if since
|
|
443
|
+
scope = scope.where(read: false) unless include_read
|
|
444
|
+
scope
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# In a controller
|
|
449
|
+
def index
|
|
450
|
+
query = CommentsByAuthorQuery
|
|
451
|
+
.new(
|
|
452
|
+
author_id: params[:author_id].to_i,
|
|
453
|
+
since: params[:since] && Time.zone.parse(params[:since]),
|
|
454
|
+
include_read: params[:include_read] != "false",
|
|
455
|
+
page: params[:page] || 1,
|
|
456
|
+
page_size: 25,
|
|
457
|
+
)
|
|
458
|
+
.transform { |c| CommentPresenter.new(c, viewer: current_user) }
|
|
459
|
+
|
|
460
|
+
render :index, locals: {comments: query.results, paginator: query}
|
|
461
|
+
end
|
|
462
|
+
```
|