prato 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/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +938 -0
- data/lib/prato/configuration.rb +99 -0
- data/lib/prato/internal/active_record_version.rb +24 -0
- data/lib/prato/internal/join_helper.rb +48 -0
- data/lib/prato/internal/join_helper_legacy.rb +171 -0
- data/lib/prato/internal/lazy_loader_cache.rb +25 -0
- data/lib/prato/internal/pipeline/filtering.rb +277 -0
- data/lib/prato/internal/pipeline/pagination.rb +30 -0
- data/lib/prato/internal/pipeline/serializer.rb +87 -0
- data/lib/prato/internal/pipeline/sorting.rb +78 -0
- data/lib/prato/internal/query_executor.rb +105 -0
- data/lib/prato/internal/query_state.rb +90 -0
- data/lib/prato/internal/specification.rb +101 -0
- data/lib/prato/internal/specification_builder.rb +361 -0
- data/lib/prato/internal/sql_support.rb +118 -0
- data/lib/prato/query/and_filter.rb +13 -0
- data/lib/prato/query/default_parser.rb +148 -0
- data/lib/prato/query/field_resolver.rb +23 -0
- data/lib/prato/query/filter.rb +15 -0
- data/lib/prato/query/or_filter.rb +13 -0
- data/lib/prato/query/parameters.rb +17 -0
- data/lib/prato/query/sort.rb +14 -0
- data/lib/prato/table.rb +39 -0
- data/lib/prato/table_builder.rb +40 -0
- data/lib/prato/types/aggregate_column.rb +93 -0
- data/lib/prato/types/association_column.rb +37 -0
- data/lib/prato/types/direct_column.rb +27 -0
- data/lib/prato/types/expression_column.rb +38 -0
- data/lib/prato/types/ruby_column.rb +31 -0
- data/lib/prato/version.rb +5 -0
- data/lib/prato.rb +66 -0
- metadata +96 -0
data/README.md
ADDED
|
@@ -0,0 +1,938 @@
|
|
|
1
|
+
# 
|
|
2
|
+
|
|
3
|
+
[Click here to see the interactive demo!](https://prato.trecitano.com/)
|
|
4
|
+
|
|
5
|
+
Prato is a library that simplifies the backend code required to support queryable data,
|
|
6
|
+
by mapping parameters onto a table structure,
|
|
7
|
+
allowing Prato to invoke Active Record methods like `.where`, `.order`, `.joins`, `.pluck` and others.
|
|
8
|
+
|
|
9
|
+
The immediate use case for this is fetching data for tables in the frontend,
|
|
10
|
+
and with a simple *Prato* table, it becomes trivial to provide any kind of filtering / sorting / pagination operations
|
|
11
|
+
over an Active Record relation.
|
|
12
|
+
|
|
13
|
+
A quick example of this in action:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
class BooksController < ApplicationController
|
|
17
|
+
def index
|
|
18
|
+
table = Prato.table(Book) do
|
|
19
|
+
column(:title)
|
|
20
|
+
|
|
21
|
+
section(:people) do
|
|
22
|
+
column(author_name: [:author, :name])
|
|
23
|
+
column(editor_name: [:editor, :name])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
column(:review_count, count: :reviews)
|
|
27
|
+
column(:avg_review_score, avg: [:reviews, :score])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
render json: table.page(Book.all, params)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Assuming Book has an association to `author`, `editor`, and `reviews`, this will generate the following result:
|
|
36
|
+
```json lines
|
|
37
|
+
{
|
|
38
|
+
"entries": [
|
|
39
|
+
{
|
|
40
|
+
"title": "Practical Object Conversations",
|
|
41
|
+
"people": {
|
|
42
|
+
"authorName": "Sandi Metz",
|
|
43
|
+
"editorName": "Martin Fowler"
|
|
44
|
+
},
|
|
45
|
+
"reviewCount": 2,
|
|
46
|
+
"avgReviewScore": 2.5
|
|
47
|
+
},
|
|
48
|
+
// ... 9 entries omitted
|
|
49
|
+
],
|
|
50
|
+
"totalCount": 24
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
That's it! Even if the request contains parameters (filters, ordering, field selection),
|
|
55
|
+
we don't have to change any of the backend code.
|
|
56
|
+
|
|
57
|
+
## Table of Contents
|
|
58
|
+
|
|
59
|
+
- [Why Prato](#why-prato)
|
|
60
|
+
- [Requirements](#requirements)
|
|
61
|
+
- [Installation](#installation)
|
|
62
|
+
- [Technical Overview](#technical-overview)
|
|
63
|
+
- [Usage](#usage)
|
|
64
|
+
- [Defining a Prato table](#defining-a-prato-table)
|
|
65
|
+
- [column](#column)
|
|
66
|
+
- [query_column](#query_column)
|
|
67
|
+
- [section](#section)
|
|
68
|
+
- [configuration](#configuration)
|
|
69
|
+
- [ruby_column (Advanced)](#ruby_column-advanced)
|
|
70
|
+
- [Materializing a scope](#materializing-a-scope)
|
|
71
|
+
- [Parameters / Request Details](#parameters--request-details)
|
|
72
|
+
- [Pagination](#pagination)
|
|
73
|
+
- [Filters](#filters)
|
|
74
|
+
- [Sorting](#sorting)
|
|
75
|
+
- [Fields](#fields)
|
|
76
|
+
- [Development](#development-todo)
|
|
77
|
+
- [Contributing](#contributing)
|
|
78
|
+
- [License](#license)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
## Why Prato
|
|
82
|
+
|
|
83
|
+
Prato was born as a way to tackle complexity at scale.
|
|
84
|
+
|
|
85
|
+
It's common for applications to have some web pages that display data in a tabular style. The default approach to solve this
|
|
86
|
+
is to write an Active Record scope, add any necessary `.where` or `.or` statements, add `.includes` for any relations
|
|
87
|
+
and then serialize the result into model objects, as this is more ergonomic than just using `.pluck`.
|
|
88
|
+
|
|
89
|
+
This has some downsides:
|
|
90
|
+
- The request can overfetch data from the database.
|
|
91
|
+
- (which is problematic when new columns are added, and we don't know how much data they might have!)
|
|
92
|
+
- The relation is materialized with model objects, which may invoke any number of callbacks that we are not aware of (`after_find` or `after_initialize`).
|
|
93
|
+
- The business requirements may change, requiring data from different models which causes association and serializaiton code to be revisited.
|
|
94
|
+
- It's necessary to write *a lot* of code.
|
|
95
|
+
|
|
96
|
+
For applications being worked on with multiple developers and with hundreds of database tables, it becomes tricky to ensure
|
|
97
|
+
that all code is performant and correct.
|
|
98
|
+
|
|
99
|
+
Prato's table structure offers a way of ensuring that all the problems above stop being a concern.
|
|
100
|
+
|
|
101
|
+
## Requirements
|
|
102
|
+
|
|
103
|
+
Prato requires Ruby 2.4 or later, Active Record 5.0 or later, and MySql, Sqlite or Postgres.
|
|
104
|
+
|
|
105
|
+
The library is actively tested against the following matrix:
|
|
106
|
+
|
|
107
|
+
| Ruby | Active Record |
|
|
108
|
+
|-------|---------------|
|
|
109
|
+
| 2.4.x | 5.0 |
|
|
110
|
+
| 2.5.x | 5.1 |
|
|
111
|
+
| 2.6.x | 5.2 |
|
|
112
|
+
| 2.7.x | 6.0, 6.1 |
|
|
113
|
+
| 3.0.x | 7.0 |
|
|
114
|
+
| 3.1.x | 7.1 |
|
|
115
|
+
| 3.2.x | 7.2, 8.0, 8.1 |
|
|
116
|
+
| 3.3.x | 7.2, 8.0, 8.1 |
|
|
117
|
+
| 3.4.x | 7.2, 8.0, 8.1 |
|
|
118
|
+
| 4.0.x | 7.2, 8.0, 8.1 |
|
|
119
|
+
|
|
120
|
+
## Installation
|
|
121
|
+
|
|
122
|
+
Install the gem and add it to your application's Gemfile by running:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
bundle add prato
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Technical Overview
|
|
129
|
+
|
|
130
|
+
Prato's guiding philosophy is that Active Record (AR) is already great at building SQL so Prato relies on it and Arel for generating SQL.
|
|
131
|
+
|
|
132
|
+
A Prato table specification uses `:symbols` to describe the fields that can be displayed, filtered, and sorted.
|
|
133
|
+
These symbols correspond to the method calls that otherwise would have to be written.
|
|
134
|
+
For example, `column(author_name: [:author, :name])` provides the same result as `<object>.author.name`.
|
|
135
|
+
|
|
136
|
+
By letting the request define what is required, Prato can decide at runtime which Active Record methods should be invoked.
|
|
137
|
+
Filters map to `.where` clauses, sorts map to `.order` clauses, association paths add the required joins, pagination adds `.limit` and `.offset`
|
|
138
|
+
and finally `.pluck` materializes any data that the request requires.
|
|
139
|
+
|
|
140
|
+
This lets application offer more functionality while having less code.
|
|
141
|
+
|
|
142
|
+
## Usage
|
|
143
|
+
|
|
144
|
+
Prato relies on two steps:
|
|
145
|
+
- Defining a Prato table.
|
|
146
|
+
- Use an Active Record relation on that table with `.page(scope, params)`, `.full(scope, params)` or `.batches(scope, params, ...)`.
|
|
147
|
+
|
|
148
|
+
### Defining a Prato table
|
|
149
|
+
|
|
150
|
+
A Prato table consists of columns and may also include sections and configuration.
|
|
151
|
+
The example below demonstrates many of the available features:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
table = Prato.table(Book) do
|
|
155
|
+
column(:title)
|
|
156
|
+
column("Display Title" => :title)
|
|
157
|
+
column(:author_name, [:author, :name])
|
|
158
|
+
column(:city, [:publisher, :address, :city])
|
|
159
|
+
|
|
160
|
+
column(:review_count, count: :reviews)
|
|
161
|
+
column(:review_sum, sum: [:reviews, :score])
|
|
162
|
+
column(:review_avg, avg: [:reviews, :score])
|
|
163
|
+
column(:review_min, min: [:reviews, :score])
|
|
164
|
+
column(:review_max, max: [:reviews, :score])
|
|
165
|
+
|
|
166
|
+
column(:title_upper, expression: "UPPER(books.title)")
|
|
167
|
+
|
|
168
|
+
column(:formatted_title, :title, format: ->(v) { v.downcase })
|
|
169
|
+
column(:status, filter: [:eq, :in])
|
|
170
|
+
column(:internal_id, :id, queryable: :filter)
|
|
171
|
+
|
|
172
|
+
section(:author) do
|
|
173
|
+
column(:name, [:author, :name])
|
|
174
|
+
column(:email, [:author, :email])
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
query_column(:author_id, [:author, :id])
|
|
178
|
+
|
|
179
|
+
configure(
|
|
180
|
+
key_transformation: :camelCase,
|
|
181
|
+
on_invalid_input: :raise,
|
|
182
|
+
parameter_parser: Prato::Query::DefaultParser,
|
|
183
|
+
default_page_size: 25,
|
|
184
|
+
maximum_page_size: 100,
|
|
185
|
+
default_queryable: :all,
|
|
186
|
+
default_ruby_column_queryable: :none
|
|
187
|
+
)
|
|
188
|
+
end
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Invoking `table.page(Book.all)` will output the following structure:
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
{
|
|
195
|
+
entries: [
|
|
196
|
+
{
|
|
197
|
+
title: "Practical Object Conversations",
|
|
198
|
+
"Display Title" => "Practical Object Conversations",
|
|
199
|
+
authorName: "Sandi Metz",
|
|
200
|
+
city: "Raleigh",
|
|
201
|
+
reviewCount: 4,
|
|
202
|
+
reviewSum: 18,
|
|
203
|
+
reviewAvg: 4.5,
|
|
204
|
+
reviewMin: 3,
|
|
205
|
+
reviewMax: 5,
|
|
206
|
+
titleUpper: "PRACTICAL OBJECT CONVERSATIONS",
|
|
207
|
+
formattedTitle: "practical object conversations",
|
|
208
|
+
status: "published",
|
|
209
|
+
author: {
|
|
210
|
+
name: "Sandi Metz",
|
|
211
|
+
email: "sandi@example.com"
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
# ... up to 24 more entries omitted (default_page_size: 25)
|
|
215
|
+
],
|
|
216
|
+
totalCount: 34
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
#### column
|
|
221
|
+
|
|
222
|
+
A `column` is backed by SQL and its values are obtained via `.pluck`, unless `ruby_columns` are used (see more below).
|
|
223
|
+
Filters and sorts applied to a `column` will generate SQL via Arel or Active Record methods.
|
|
224
|
+
|
|
225
|
+
The source of a column's value can be defined in different ways:
|
|
226
|
+
- A column on the base model, referenced by name.
|
|
227
|
+
- A column on an associated model, reached through an association path.
|
|
228
|
+
- An aggregate expression (`:count`, `:avg`, `:sum`, `:min`, `:max`).
|
|
229
|
+
- A custom SQL expression.
|
|
230
|
+
|
|
231
|
+
In the following subsections, every example will use the configuration `key_transformation: :camelCase`.
|
|
232
|
+
|
|
233
|
+
##### Basic Columns
|
|
234
|
+
|
|
235
|
+
Use a symbol to expose a model column directly:
|
|
236
|
+
|
|
237
|
+
| Example | Output field | SQL source |
|
|
238
|
+
|----------------------------|------------------|-------------------|
|
|
239
|
+
| `column(:release_year)` | `:releaseYear` | `release_year` |
|
|
240
|
+
| `column(:runtime_minutes)` | `:runtimeMinutes` | `runtime_minutes` |
|
|
241
|
+
| `column(:published_at)` | `:publishedAt` | `published_at` |
|
|
242
|
+
|
|
243
|
+
Use a hash when the output field should differ from the source column:
|
|
244
|
+
|
|
245
|
+
| Example | Output field | SQL source |
|
|
246
|
+
|---------------------------------------------|----------------|--------------------|
|
|
247
|
+
| `column(display_name: :name)` | `:displayName` | `name` |
|
|
248
|
+
| `column(released_on: :release_date)` | `:releasedOn` | `release_date` |
|
|
249
|
+
| `column("Box Office" => :box_office_total)` | `"Box Office"` | `box_office_total` |
|
|
250
|
+
|
|
251
|
+
Use an association path to read from a joined model:
|
|
252
|
+
|
|
253
|
+
| Example | Output field | SQL source |
|
|
254
|
+
|---------------------------------------------------|--------------------|---------------------|
|
|
255
|
+
| `column(studio_name: [:studio, :name])` | `:studioName` | `studios.name` |
|
|
256
|
+
| `column(director_country: [:director, :country])` | `:directorCountry` | `directors.country` |
|
|
257
|
+
| `column("Genre Label" => [:genre, :label])` | `"Genre Label"` | `genres.label` |
|
|
258
|
+
| `column(:publisher_city, [:publisher, :city])` | `:publisherCity` | `publishers.city` |
|
|
259
|
+
|
|
260
|
+
##### Aggregate Columns
|
|
261
|
+
|
|
262
|
+
Use an aggregate keyword to compute a value in SQL:
|
|
263
|
+
|
|
264
|
+
| Example | Output field | SQL source |
|
|
265
|
+
|------------------------------------------------------------|-------------------|-----------------------------|
|
|
266
|
+
| `column(:review_count, count: :reviews)` | `:reviewCount` | `COUNT(reviews.*)` |
|
|
267
|
+
| `column(:average_rating, avg: [:reviews, :rating])` | `:averageRating` | `AVG(reviews.rating)` |
|
|
268
|
+
| `column(:total_sales, sum: [:orders, :total_cents])` | `:totalSales` | `SUM(orders.total_cents)` |
|
|
269
|
+
| `column(:first_showtime, min: [:screenings, :starts_at])` | `:firstShowtime` | `MIN(screenings.starts_at)` |
|
|
270
|
+
| `column(:latest_purchase, max: [:purchases, :created_at])` | `:latestPurchase` | `MAX(purchases.created_at)` |
|
|
271
|
+
|
|
272
|
+
##### Expression Columns
|
|
273
|
+
|
|
274
|
+
Use `expression:` when the value should come from custom SQL:
|
|
275
|
+
|
|
276
|
+
| Example | Output field | SQL source |
|
|
277
|
+
|------------------------------------------------------------------------|-------------------|---------------------------------|
|
|
278
|
+
| `column(:lowercase_name, expression: "LOWER(name)")` | `:normalizedName` | `LOWER(name)` |
|
|
279
|
+
| `column(:release_decade, expression: "FLOOR(release_year / 10) * 10")` | `:releaseDecade` | `FLOOR(release_year / 10) * 10` |
|
|
280
|
+
| `column("Short Code", expression: "SUBSTRING(code, 1, 3)")` | `"Short Code"` | `SUBSTRING(code, 1, 3)` |
|
|
281
|
+
|
|
282
|
+
##### Options
|
|
283
|
+
|
|
284
|
+
**format**
|
|
285
|
+
|
|
286
|
+
Use `format:` to transform the raw SQL value before it is serialized.
|
|
287
|
+
```ruby
|
|
288
|
+
column(:title_length, :title, format: ->(value) { value.length })
|
|
289
|
+
```
|
|
290
|
+
If the database value for title is "Book title", the serialized value for titleLength will be 10.
|
|
291
|
+
|
|
292
|
+
**filter**
|
|
293
|
+
```ruby
|
|
294
|
+
column(:title, filter: [:eq])
|
|
295
|
+
```
|
|
296
|
+
This column can only be filtered with the `:eq` operator.
|
|
297
|
+
A query that attempts to filter this column with another operator will be treated as invalid input:
|
|
298
|
+
by default it returns an empty result, or raises ArgumentError when on_invalid_input: :raise is configured.
|
|
299
|
+
|
|
300
|
+
It is also possible to override the filtering behavior.
|
|
301
|
+
The proc receives the current relation, the requested operator, and the filter value.
|
|
302
|
+
It must return a relation, or nil to use the default filtering mechanism.
|
|
303
|
+
```ruby
|
|
304
|
+
column(:age, filter: lambda { |scope, operator, value|
|
|
305
|
+
case operator
|
|
306
|
+
when :eq
|
|
307
|
+
scope.where(age: 10 * value)
|
|
308
|
+
end
|
|
309
|
+
})
|
|
310
|
+
```
|
|
311
|
+
In the example above, only the "equals" operator is overridden. Any other operator will still use the default implementation.
|
|
312
|
+
To override filtering and reject all remaining operators, return an empty relation:
|
|
313
|
+
```ruby
|
|
314
|
+
column(:age, filter: lambda { |scope, operator, value|
|
|
315
|
+
case operator
|
|
316
|
+
when :eq then scope.where(age: 10 * value)
|
|
317
|
+
else scope.none
|
|
318
|
+
end
|
|
319
|
+
})
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
##### queryable
|
|
323
|
+
Use `queryable` to control whether a column can be filtered or sorted.
|
|
324
|
+
```ruby
|
|
325
|
+
column(:currency, queryable: :all) # Can be displayed, filtered and sorted
|
|
326
|
+
column(:title, queryable: :none) # Can only be displayed
|
|
327
|
+
column(:status, queryable: :filter) # Can be displayed and filtered, but not sorted
|
|
328
|
+
column(:created_at, queryable: :sort) # Can be displayed and sorted, but not filtered
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
#### query_column
|
|
332
|
+
|
|
333
|
+
A `query_column` behaves like a `column`, but it's not included in the serialized output.
|
|
334
|
+
|
|
335
|
+
Use it when a field should be available for filtering or sorting, but should not be rendered in the response.
|
|
336
|
+
|
|
337
|
+
```ruby
|
|
338
|
+
query_column(:author_id, [:author, :id])
|
|
339
|
+
query_column(:status, filter: [:eq])
|
|
340
|
+
query_column(:created_at, queryable: :sort)
|
|
341
|
+
```
|
|
342
|
+
For `query_column`, valid `queryable:` values are `:all`, `:filter`, and `:sort`.
|
|
343
|
+
|
|
344
|
+
#### section
|
|
345
|
+
|
|
346
|
+
Use `section` to group fields under a nested object in the serialized output.
|
|
347
|
+
|
|
348
|
+
```ruby
|
|
349
|
+
table = Prato.table(Book) do
|
|
350
|
+
column(:title)
|
|
351
|
+
section(:author) do
|
|
352
|
+
column(:name, [:author, :name])
|
|
353
|
+
column(:email, [:author, :email])
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
```
|
|
357
|
+
Invoking `table.page(Book.all)` produces:
|
|
358
|
+
```ruby
|
|
359
|
+
{
|
|
360
|
+
entries: [
|
|
361
|
+
{
|
|
362
|
+
title: "Practical Object Conversations",
|
|
363
|
+
author: {
|
|
364
|
+
name: "Sandi Metz",
|
|
365
|
+
email: "sandi@example.com"
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
],
|
|
369
|
+
totalCount: 1
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
Sections only affect the output shape, nesting together some columns. They can also be nested themselves:
|
|
373
|
+
```ruby
|
|
374
|
+
section(:publisher) do
|
|
375
|
+
section(:address) do
|
|
376
|
+
column(:city, %i[publisher address city])
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
```
|
|
380
|
+
When using the [default parser](lib/prato/query/default_parser.rb),
|
|
381
|
+
nested fields are referenced with dotted paths when filtering, sorting or selecting fields:
|
|
382
|
+
```ruby
|
|
383
|
+
table.page(
|
|
384
|
+
Book.all,
|
|
385
|
+
{
|
|
386
|
+
filters: [{ field: "author.name", operator: "eq", value: "Sandi Metz" }],
|
|
387
|
+
sorts: [{ field: "author.email", order: "asc" }],
|
|
388
|
+
fields: ["title", "author.name"]
|
|
389
|
+
}
|
|
390
|
+
)
|
|
391
|
+
```
|
|
392
|
+
Section names with symbols are transformed using `key_transformation`.
|
|
393
|
+
```ruby
|
|
394
|
+
configure(key_transformation: :snake_case)
|
|
395
|
+
section(:authorProfile) do
|
|
396
|
+
column(:displayName, %i[author name])
|
|
397
|
+
end
|
|
398
|
+
```
|
|
399
|
+
This serializes as:
|
|
400
|
+
```ruby
|
|
401
|
+
{
|
|
402
|
+
author_profile: {
|
|
403
|
+
display_name: "Sandi Metz"
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
#### configuration
|
|
409
|
+
|
|
410
|
+
Use `configure` inside a table definition to override the application-level settings.
|
|
411
|
+
|
|
412
|
+
```ruby
|
|
413
|
+
table = Prato.table(Book) do
|
|
414
|
+
column(:title)
|
|
415
|
+
column(:published_at)
|
|
416
|
+
|
|
417
|
+
configure(
|
|
418
|
+
key_transformation: :camelCase,
|
|
419
|
+
on_invalid_input: :empty,
|
|
420
|
+
parameter_parser: Prato::Query::DefaultParser.new,
|
|
421
|
+
default_page_size: 20,
|
|
422
|
+
maximum_page_size: 100,
|
|
423
|
+
default_queryable: :all,
|
|
424
|
+
default_ruby_column_queryable: :none
|
|
425
|
+
)
|
|
426
|
+
end
|
|
427
|
+
```
|
|
428
|
+
| Option | Default | Values |
|
|
429
|
+
|-----------------------------------|-----------------------------------|------------------------------------------------------------------|
|
|
430
|
+
| `key_transformation` | `:camelCase` | `:camelCase`, `:snake_case`, `:none` |
|
|
431
|
+
| `on_invalid_input` | `:empty` | `:empty`, `:raise` |
|
|
432
|
+
| `parameter_parser` | `Prato::Query::DefaultParser.new` | Any object responding to `parse_parameters(input, field_lookup)` |
|
|
433
|
+
| `default_page_size` | `20` | Integer |
|
|
434
|
+
| `maximum_page_size` | `100` | Integer |
|
|
435
|
+
| `default_queryable` | `:all` | `:all`, `:none`, `:filter`, `:sort` |
|
|
436
|
+
| `default_ruby_column_queryable` | `:none` | `:all`, `:none`, `:filter`, `:sort` |
|
|
437
|
+
|
|
438
|
+
##### `key_transformation`
|
|
439
|
+
Controls how output keys are transformed.
|
|
440
|
+
|
|
441
|
+
```ruby
|
|
442
|
+
configure(key_transformation: :camelCase)
|
|
443
|
+
column(:published_at)
|
|
444
|
+
# => :publishedAt
|
|
445
|
+
configure(key_transformation: :snake_case)
|
|
446
|
+
column(:publishedAt, :published_at)
|
|
447
|
+
# => :published_at
|
|
448
|
+
configure(key_transformation: :none)
|
|
449
|
+
column(:published_at)
|
|
450
|
+
# => :published_at
|
|
451
|
+
```
|
|
452
|
+
This applies to both column names and section names that use `:symbols`. Strings are not affected by the key transformation.
|
|
453
|
+
|
|
454
|
+
##### `on_invalid_input`
|
|
455
|
+
|
|
456
|
+
Controls what happens when parsed request parameters reference fields or operators that are not allowed.
|
|
457
|
+
```ruby
|
|
458
|
+
configure(on_invalid_input: :empty) # returns an empty result
|
|
459
|
+
configure(on_invalid_input: :raise) # raises an `ArgumentError`
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
##### `parameter_parser`
|
|
463
|
+
Controls how incoming request parameters are converted into Prato query parameters.
|
|
464
|
+
```ruby
|
|
465
|
+
configure(parameter_parser: MyCustomParser.new)
|
|
466
|
+
```
|
|
467
|
+
A custom parser must respond to:
|
|
468
|
+
```ruby
|
|
469
|
+
parse_parameters(input, field_lookup)
|
|
470
|
+
```
|
|
471
|
+
It should return a `Prato::Query::Parameters` object.
|
|
472
|
+
|
|
473
|
+
To define your own Parser, look at [how to implement a request parser](docs/implementing_a_parser.md)
|
|
474
|
+
|
|
475
|
+
##### `default_page_size` and `maximum_page_size`
|
|
476
|
+
Controls pagination defaults and limits.
|
|
477
|
+
```ruby
|
|
478
|
+
configure(
|
|
479
|
+
default_page_size: 25,
|
|
480
|
+
maximum_page_size: 100
|
|
481
|
+
)
|
|
482
|
+
```
|
|
483
|
+
If the request does not provide `per_page`, Prato uses `default_page_size`.
|
|
484
|
+
If the request asks for more than `maximum_page_size`, Prato caps the page size.
|
|
485
|
+
|
|
486
|
+
##### `default_queryable`
|
|
487
|
+
Sets the default `queryable:` behavior for columns that do not specify it explicitly.
|
|
488
|
+
```ruby
|
|
489
|
+
configure(default_queryable: :none)
|
|
490
|
+
column(:title)
|
|
491
|
+
column(:status, queryable: :filter)
|
|
492
|
+
column(:currency, queryable: :all)
|
|
493
|
+
```
|
|
494
|
+
In this example, `title` can not be filtered or sorted, while `status` is allowed to filter. `currency` can be filtered and sorted.
|
|
495
|
+
|
|
496
|
+
##### `default_ruby_column_queryable`
|
|
497
|
+
Same as `default_queryable`, but applied to `ruby_column`.
|
|
498
|
+
|
|
499
|
+
##### Global configuration
|
|
500
|
+
|
|
501
|
+
Use `Prato.setup` with a block to configure application-level defaults.
|
|
502
|
+
These defaults are used by tables that do not override them.
|
|
503
|
+
|
|
504
|
+
```ruby
|
|
505
|
+
Prato.setup do |config|
|
|
506
|
+
config.key_transformation = :snake_case
|
|
507
|
+
config.default_page_size = 50
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
table = Prato.table(Book) do
|
|
511
|
+
column(:published_at)
|
|
512
|
+
end
|
|
513
|
+
# Output key:
|
|
514
|
+
# => :published_at
|
|
515
|
+
````
|
|
516
|
+
|
|
517
|
+
##### Shared configuration
|
|
518
|
+
|
|
519
|
+
Use `Prato.setup` without a block to create a reusable configuration object.
|
|
520
|
+
That object can then be passed to one or more tables.
|
|
521
|
+
|
|
522
|
+
```ruby
|
|
523
|
+
config = Prato.setup
|
|
524
|
+
config.key_transformation = :snake_case
|
|
525
|
+
config.default_page_size = 50
|
|
526
|
+
|
|
527
|
+
books_table = Prato.table(Book) do
|
|
528
|
+
configure(config, maximum_page_size: 200)
|
|
529
|
+
column(:published_at)
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
authors_table = Prato.table(Author) do
|
|
533
|
+
configure(config)
|
|
534
|
+
column(:created_at)
|
|
535
|
+
end
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
Options passed directly to configure override the shared configuration object for that table.
|
|
539
|
+
|
|
540
|
+
#### ruby_column (Advanced)
|
|
541
|
+
|
|
542
|
+
**Warning!**
|
|
543
|
+
Requests that use `ruby_columns` requires Active Record objects to be materialized.
|
|
544
|
+
This disables some SQL-only optimizations, such as serializing directly with `.pluck`.
|
|
545
|
+
|
|
546
|
+
Use `ruby_column` when a value cannot be expressed as a SQL-backed `column`, or when the value should be loaded through Ruby code.
|
|
547
|
+
|
|
548
|
+
```ruby
|
|
549
|
+
table = Prato.table(Book) do
|
|
550
|
+
column(:title)
|
|
551
|
+
ruby_column(:title_length, key: :id) do |books, _loaders|
|
|
552
|
+
books.to_h do |book|
|
|
553
|
+
[book.id, book.title.length]
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
The idea behind a `ruby_column` is that sometimes, we need to have some values that cannot be calculated in the database.
|
|
560
|
+
(The example above can actually be computed in the database, but let's pretend it cannot!)
|
|
561
|
+
|
|
562
|
+
The way the `ruby_column` works is that it receives two arguments: an array of model objects and an hash of loaders.
|
|
563
|
+
- The array of model objects represent the data that is going to be displayed in the frontend
|
|
564
|
+
- The hash of loaders is useful when a `ruby_column` uses data from another `ruby_column`.
|
|
565
|
+
|
|
566
|
+
**Separate Loaders**
|
|
567
|
+
|
|
568
|
+
A loader can also be defined separately with ruby_loader.
|
|
569
|
+
This is useful when multiple Ruby columns need to share the same loading logic,
|
|
570
|
+
or when you want to keep the column declaration compact.
|
|
571
|
+
|
|
572
|
+
```ruby
|
|
573
|
+
table = Prato.table(Book) do
|
|
574
|
+
column(:title)
|
|
575
|
+
ruby_column(:review_summary, key: :id)
|
|
576
|
+
|
|
577
|
+
ruby_loader(:review_summary) do |books, _cache|
|
|
578
|
+
# This prevents a n+1 issue
|
|
579
|
+
review_counts = Review.where(book_id: books.map(&:id)).group(:book_id).count
|
|
580
|
+
books.to_h do |book|
|
|
581
|
+
count = review_counts.fetch(book.id, 0)
|
|
582
|
+
[book.id, "#{count} reviews"]
|
|
583
|
+
end
|
|
584
|
+
end
|
|
585
|
+
end
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
The name passed to ruby_column is used as both the output field and the loader name.
|
|
589
|
+
To use a different output field and loader name, pass both:
|
|
590
|
+
```ruby
|
|
591
|
+
ruby_column(:summary, :review_summary, key: :id)
|
|
592
|
+
ruby_loader(:review_summary) do |books, _cache|
|
|
593
|
+
# ...
|
|
594
|
+
end
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
**key**
|
|
598
|
+
|
|
599
|
+
By default, `ruby_column` uses the record's id.
|
|
600
|
+
```ruby
|
|
601
|
+
ruby_column(:availability) do |books, _cache|
|
|
602
|
+
books.to_h { |book| [book.id, "available"] }
|
|
603
|
+
end
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
Use a symbol to read a different attribute:
|
|
607
|
+
```ruby
|
|
608
|
+
ruby_column(:availability, key: :isbn) do |books, _cache|
|
|
609
|
+
Inventory.lookup(books.map(&:isbn))
|
|
610
|
+
end
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
Use a proc when the lookup key needs custom logic:
|
|
614
|
+
```ruby
|
|
615
|
+
ruby_column(:company_status, key: ->(book) { book.publisher&.company_id }) do |books, _cache|
|
|
616
|
+
# ...
|
|
617
|
+
end
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
**includes**
|
|
621
|
+
|
|
622
|
+
Use includes: when the loader needs associations from the materialized records.
|
|
623
|
+
```ruby
|
|
624
|
+
ruby_column(:publisher_name, key: :id, includes: :publisher) do |books, _cache|
|
|
625
|
+
books.to_h do |book|
|
|
626
|
+
[book.id, book.publisher&.name]
|
|
627
|
+
end
|
|
628
|
+
end
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
The association loading can also be declared on a separate loader:
|
|
632
|
+
```ruby
|
|
633
|
+
ruby_column(:publisher_name, key: :id)
|
|
634
|
+
ruby_loader(:publisher_name, includes: :publisher) do |books, _cache|
|
|
635
|
+
books.to_h { |book| [book.id, book.publisher&.name] }
|
|
636
|
+
end
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
**cache**
|
|
641
|
+
|
|
642
|
+
The second block argument is a loader cache. It can be used when one Ruby loader depends on another Ruby loader.
|
|
643
|
+
```ruby
|
|
644
|
+
table = Prato.table(Book) do
|
|
645
|
+
ruby_column(:review_count, key: :id)
|
|
646
|
+
|
|
647
|
+
ruby_loader(:review_count) do |books, _cache|
|
|
648
|
+
Review.where(book_id: books.map(&:id)).group(:book_id).count
|
|
649
|
+
end
|
|
650
|
+
ruby_column(:review_summary, key: :id) do |_books, cache|
|
|
651
|
+
counts = cache[:review_count]
|
|
652
|
+
counts.transform_values do |count|
|
|
653
|
+
"#{count} reviews"
|
|
654
|
+
end
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
```
|
|
658
|
+
Loader results are memoized, so referencing cache[:review_count] multiple times does not run that loader multiple times.
|
|
659
|
+
Additionally, the loaders are lazy loaded, so they can be declared in any order.
|
|
660
|
+
|
|
661
|
+
** Filtering and Sorting **
|
|
662
|
+
|
|
663
|
+
Filtering and sorting on `ruby_column` values should be enabled carefully, because they can be expensive.
|
|
664
|
+
|
|
665
|
+
When a `ruby_column` is only displayed, Prato can still apply SQL-backed filtering, sorting, and pagination before materializing records.
|
|
666
|
+
This keeps the amount of Ruby work limited to the records that are actually being returned.
|
|
667
|
+
Filtering or sorting by a `ruby_column` is different.
|
|
668
|
+
Since the value only exists in Ruby, Prato must load the matching records, compute the Ruby value for each one, and then apply the filter or sort in memory.
|
|
669
|
+
For large tables, this can mean materializing many records before pagination can be applied.
|
|
670
|
+
|
|
671
|
+
For this reason, Ruby columns should be treated as display-only by default, and filtering or sorting should only be enabled when the candidate result set is known to be small enough.
|
|
672
|
+
|
|
673
|
+
### Materializing a scope
|
|
674
|
+
|
|
675
|
+
There are three ways of materializing a scope - `page`, `full` and `batches`.
|
|
676
|
+
All 3 method calls receive the same two main arguments:
|
|
677
|
+
- scope: An Active Record relation.
|
|
678
|
+
- params: A user-provided object parsed by the configured parameter parser.
|
|
679
|
+
- By default, it's expected that `params` is an `ActionController::Parameters`, but it is not mandatory.
|
|
680
|
+
- This field can be omitted.
|
|
681
|
+
|
|
682
|
+
```ruby
|
|
683
|
+
table = Prato.table(Book) do
|
|
684
|
+
column(:title)
|
|
685
|
+
column(:published_at)
|
|
686
|
+
end
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
#### page
|
|
690
|
+
|
|
691
|
+
Use `page` when returning data for a paginated UI.
|
|
692
|
+
`page` applies filters, sorting, field selection, and pagination.
|
|
693
|
+
It returns a hash containing the serialized entries and the total number of matching records before pagination:
|
|
694
|
+
```ruby
|
|
695
|
+
table.page(Book.order(:id), params)
|
|
696
|
+
# returns:
|
|
697
|
+
|
|
698
|
+
{
|
|
699
|
+
entries: [
|
|
700
|
+
{
|
|
701
|
+
title: "Practical Object Conversations",
|
|
702
|
+
publishedAt: "2026-01-01"
|
|
703
|
+
}
|
|
704
|
+
],
|
|
705
|
+
totalCount: 42
|
|
706
|
+
}
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
If no pagination parameters are provided, Prato uses `default_page_size`.
|
|
710
|
+
|
|
711
|
+
#### full
|
|
712
|
+
|
|
713
|
+
Use `full` when the entire matching result should be returned.
|
|
714
|
+
`full` applies filters, sorting, and field selection, but does not apply pagination and does not return totalCount.
|
|
715
|
+
```ruby
|
|
716
|
+
table.full(Book.order(:id), params)
|
|
717
|
+
# returns:
|
|
718
|
+
|
|
719
|
+
[
|
|
720
|
+
{
|
|
721
|
+
title: "Practical Object Conversations",
|
|
722
|
+
publishedAt: "2026-01-01"
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
title: "Eloquent Ruby",
|
|
726
|
+
publishedAt: "2025-06-15"
|
|
727
|
+
}
|
|
728
|
+
]
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
#### batches
|
|
732
|
+
|
|
733
|
+
Use batches when processing large result sets without loading the whole result into memory at once.
|
|
734
|
+
```ruby
|
|
735
|
+
table.batches(Book.order(:id), params, batch_size: 1_000) do |batch|
|
|
736
|
+
batch.each do |entry|
|
|
737
|
+
# Process each serialized entry
|
|
738
|
+
end
|
|
739
|
+
end
|
|
740
|
+
# Each yielded batch is an array of serialized entries:
|
|
741
|
+
[
|
|
742
|
+
{
|
|
743
|
+
title: "Practical Object Conversations",
|
|
744
|
+
publishedAt: "2026-01-01"
|
|
745
|
+
}
|
|
746
|
+
]
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
If no block is given, batches returns an enumerator:
|
|
750
|
+
```ruby
|
|
751
|
+
enum = table.batches(Book.order(:id), params, batch_size: 1_000)
|
|
752
|
+
enum.each do |batch|
|
|
753
|
+
# Process batch
|
|
754
|
+
end
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
batches applies the same filters, sorting, and field selection as full,
|
|
758
|
+
but yields the serialized records in chunks instead of returning a single array.
|
|
759
|
+
|
|
760
|
+
### Parameters / Request details
|
|
761
|
+
|
|
762
|
+
Prato receives request data through the `params` argument passed to `.page`, `.full`, or `.batches`.
|
|
763
|
+
|
|
764
|
+
By default, params are parsed by `Prato::Query::DefaultParser`, which supports pagination, filters, sorting, and field selection.
|
|
765
|
+
|
|
766
|
+
A custom parser can be configured with `parameter_parser:`, which allows the application to use requests with different parameters and formats.
|
|
767
|
+
|
|
768
|
+
#### Pagination
|
|
769
|
+
|
|
770
|
+
- [parameters.rb](lib/prato/query/parameters.rb)
|
|
771
|
+
- [parameters.rbs](sig/prato/query/parameters.rbs)
|
|
772
|
+
|
|
773
|
+
Pagination in prato works by using Active Record's `.offset` and `.limit`.
|
|
774
|
+
|
|
775
|
+
The [default parser](lib/prato/query/default_parser.rb) reads two optional parameters:
|
|
776
|
+
|
|
777
|
+
| Parameter | Meaning |
|
|
778
|
+
|------------|----------------------------------|
|
|
779
|
+
| `page` | The page number to return |
|
|
780
|
+
| `per_page` | The number of records per page |
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
If `page` is not present, then the default page is 1.
|
|
784
|
+
If `per_page` is not present, then the used page size is the one in `Configuration.default_page_size`.
|
|
785
|
+
If `per_page` is greater than `Configuration.maximum_page_size`, Prato caps it to the configured maximum.
|
|
786
|
+
|
|
787
|
+
Example Request:
|
|
788
|
+
```http request
|
|
789
|
+
https://prato.trecitano.com/reviews.json?query={"page":2,"per_page":20}
|
|
790
|
+
```
|
|
791
|
+
Example params:
|
|
792
|
+
```ruby
|
|
793
|
+
{
|
|
794
|
+
page: 2,
|
|
795
|
+
per_page: 20
|
|
796
|
+
}
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
#### Filters
|
|
800
|
+
|
|
801
|
+
- [filter.rb](lib/prato/query/filter.rb)
|
|
802
|
+
- [filter.rbs](sig/prato/query/filter.rbs)
|
|
803
|
+
|
|
804
|
+
The following filters are supported:
|
|
805
|
+
|
|
806
|
+
| Filter | Meaning |
|
|
807
|
+
|---------------------------|--------------------------------|
|
|
808
|
+
| `:eq` | Equals |
|
|
809
|
+
| `:not_eq` | Not equals |
|
|
810
|
+
| `:lt` | Less than |
|
|
811
|
+
| `:lte` | Less than or equals |
|
|
812
|
+
| `:gt` | Greater than |
|
|
813
|
+
| `:gte` | Greater than or equals |
|
|
814
|
+
| `:present` | Is not nil |
|
|
815
|
+
| `:not_present` | Is nil |
|
|
816
|
+
| `:in` | Included in a list |
|
|
817
|
+
| `:not_in` | Not included in a list |
|
|
818
|
+
| `:contains` | Contains, case sensitive |
|
|
819
|
+
| `:not_contains` | Not contains, case sensitive |
|
|
820
|
+
| `:icontains` | Contains, case insensitive |
|
|
821
|
+
| `:not_icontains` | Not contains, case insensitive |
|
|
822
|
+
| `:between` | Between, inclusive |
|
|
823
|
+
| `:not_between` | Not between, inclusive |
|
|
824
|
+
| `:between_exclusive` | Between, exclusive |
|
|
825
|
+
| `:not_between_exclusive` | Not between, exclusive |
|
|
826
|
+
|
|
827
|
+
These work by invoking the underlying Arel methods; see the
|
|
828
|
+
[filter operator implementation](lib/prato/internal/pipeline/filtering.rb#L138-L160).
|
|
829
|
+
|
|
830
|
+
The [default parser](lib/prato/query/default_parser.rb) assumes that the request contains a parameter called `filters`
|
|
831
|
+
that contains an array of:
|
|
832
|
+
```json
|
|
833
|
+
{
|
|
834
|
+
"field": "<name of the field>",
|
|
835
|
+
"operator": "<one of the operators above>",
|
|
836
|
+
"value": "<any value>"
|
|
837
|
+
}
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
If `filters` is not present, then no filtering is applied.
|
|
841
|
+
|
|
842
|
+
```
|
|
843
|
+
Example request:
|
|
844
|
+
```http request
|
|
845
|
+
http://prato.trecitano.com/nested-relations.json?query={"filters":[{"field":"title","operator":"contains","value":"test2"}]}
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
Filters can also be nested with and and or:
|
|
849
|
+
```ruby
|
|
850
|
+
{
|
|
851
|
+
filters: [
|
|
852
|
+
{
|
|
853
|
+
or: [
|
|
854
|
+
{ field: "title", operator: "contains", value: "ruby" },
|
|
855
|
+
{ field: "author.name", operator: "eq", value: "Sandi Metz" }
|
|
856
|
+
]
|
|
857
|
+
}
|
|
858
|
+
]
|
|
859
|
+
}
|
|
860
|
+
```
|
|
861
|
+
Nested fields are referenced with dotted paths, matching the serialized output path.
|
|
862
|
+
|
|
863
|
+
#### Sorting
|
|
864
|
+
|
|
865
|
+
- [sort.rb](lib/prato/query/sort.rb)
|
|
866
|
+
- [sort.rbs](sig/prato/query/sort.rbs)
|
|
867
|
+
|
|
868
|
+
Prato's Sort objects are composed by 2 parameters:
|
|
869
|
+
- :field, the internal name of a field
|
|
870
|
+
- :is_desc
|
|
871
|
+
|
|
872
|
+
The default parser assumes that the request contains a parameter called "sorts" that contains an array of:
|
|
873
|
+
```
|
|
874
|
+
{
|
|
875
|
+
"field": <name of the field>,
|
|
876
|
+
"order": asc | desc
|
|
877
|
+
}
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
If "sorts" is not present, then no sorting is applied.
|
|
881
|
+
|
|
882
|
+
Example request:
|
|
883
|
+
```http request
|
|
884
|
+
http://localhost:3000/nested-relations.json?query={"sorts":[{"field":"title","order":"asc"}]}
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
Nested fields can be sorted with dotted paths:
|
|
888
|
+
```ruby
|
|
889
|
+
{
|
|
890
|
+
sorts: [
|
|
891
|
+
{
|
|
892
|
+
field: "author.name",
|
|
893
|
+
order: "asc"
|
|
894
|
+
}
|
|
895
|
+
]
|
|
896
|
+
}
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
#### Fields
|
|
900
|
+
|
|
901
|
+
- [parameters.rb](lib/prato/query/parameters.rb)
|
|
902
|
+
- [parameters.rbs](sig/prato/query/parameters.rbs)
|
|
903
|
+
|
|
904
|
+
Field selection controls which fields are included in the serialized response.
|
|
905
|
+
|
|
906
|
+
The default parser expects fields to contain an array of field names:
|
|
907
|
+
```ruby
|
|
908
|
+
{
|
|
909
|
+
fields: ["title", "author.name", "avgReviewScore"]
|
|
910
|
+
}
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
If fields is not present, every displayable field is included in the response.
|
|
914
|
+
|
|
915
|
+
Fields inside sections are referenced with dotted paths:
|
|
916
|
+
```ruby
|
|
917
|
+
{
|
|
918
|
+
fields: ["title", "classification.categoryName", "avgReviewScore"]
|
|
919
|
+
}
|
|
920
|
+
```
|
|
921
|
+
Field selection only affects the serialized output. Fields declared with query_column can still be used for filtering or sorting, but are never rendered.
|
|
922
|
+
|
|
923
|
+
Example request:
|
|
924
|
+
```http request
|
|
925
|
+
http://localhost:3000/nested-relations.json?query={"fields":["title","classification.categoryName","avgReviewScore"]}
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
## Development (TODO)
|
|
929
|
+
|
|
930
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/run-test-matrix` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
931
|
+
|
|
932
|
+
## Contributing
|
|
933
|
+
|
|
934
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/trecitano/prato.
|
|
935
|
+
|
|
936
|
+
## License
|
|
937
|
+
|
|
938
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|