rokaki 0.14.0 → 0.15.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/.github/workflows/spec.yml +1 -1
- data/CHANGELOG.md +13 -0
- data/Gemfile.lock +1 -1
- data/README.legacy.md +533 -0
- data/README.md +24 -444
- data/docs/adapters.md +25 -1
- data/docs/configuration.md +12 -1
- data/docs/index.md +4 -4
- data/docs/usage.md +68 -2
- data/lib/rokaki/filter_model/nested_like_filters.rb +21 -16
- data/lib/rokaki/filter_model.rb +18 -0
- data/lib/rokaki/version.rb +1 -1
- data/rokaki.gemspec +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e754206bd6927db93fa0977aed409762b9bc9aa2a663c5b0d94977c2d18ba284
|
|
4
|
+
data.tar.gz: 9df4b1d7d067be43ac1df89f1b5962b2950b91e6bc5e915817cde2ef625f55f0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bf35a2999e21604de1d4daa615042036b0f16b651e251f569b4a13706d9e0e0c17022d0856303dfa71183b2c77af03631e3cae83e3b4cb790c855d47980ccc1c
|
|
7
|
+
data.tar.gz: ad6b7c1e51bfee821d54923ccf6447ed91b0633bc084d0cd0adbd6a901d36200025334b3415c53814f62c0cf644bbb8e5a6e1a8418c1c8bac9cdf5add470e8f8
|
data/.github/workflows/spec.yml
CHANGED
|
@@ -50,7 +50,7 @@ jobs:
|
|
|
50
50
|
- name: Install system dependencies
|
|
51
51
|
run: |
|
|
52
52
|
sudo apt-get update
|
|
53
|
-
sudo apt-get install -y build-essential libpq-dev default-libmysqlclient-dev freetds-dev netcat-openbsd unzip curl libaio1t64 libaio-dev
|
|
53
|
+
sudo apt-get install -y build-essential libpq-dev default-libmysqlclient-dev freetds-dev netcat-openbsd unzip curl libaio1t64 libaio-dev libsqlite3-dev
|
|
54
54
|
sudo ln -s /usr/lib/x86_64-linux-gnu/libaio.so.1t64 /usr/lib/x86_64-linux-gnu/libaio.so.1
|
|
55
55
|
|
|
56
56
|
- name: Set up Ruby
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
### 0.15.0 — 2025-10-27
|
|
2
|
+
- Add first-class SQLite support: adapter-aware LIKE behavior with OR expansion for arrays.
|
|
3
|
+
- Added SQLite badge in README.
|
|
4
|
+
- Updated documentation: adapters, configuration, index and usage; noted SQLite default in-memory config and env override `SQLITE_DATABASE`.
|
|
5
|
+
- Internal: introduced `generic_like` helper for generic adapters (used by SQLite); no breaking changes for other adapters.
|
|
6
|
+
|
|
1
7
|
### 0.13.0 — 2025-10-25
|
|
2
8
|
- Add block-form DSL parity across both FilterModel and Filterable (`filter_map do ... end` with `like`, `ilike`, `nested`, and `filters`).
|
|
3
9
|
- Support circumfix affix synonyms: `:parafix`, `:confix`, `:ambifix` (treated as `:circumfix`).
|
|
@@ -15,3 +21,10 @@
|
|
|
15
21
|
|
|
16
22
|
### 0.10.0 and earlier
|
|
17
23
|
- Core DSL: Filterable and FilterModel modes, LIKE matching with prefix/suffix/circumfix, nested filters, and adapter-aware SQL for Postgres/MySQL.
|
|
24
|
+
|
|
25
|
+
### 0.14.1 — 2025-10-25
|
|
26
|
+
- Oracle enabled by default in tests and CI; added Oracle service and Instant Client install in GitHub Actions (Ubuntu 24.04: use libaio1t64, Instant Client ZIP install).
|
|
27
|
+
- Removed `ORACLE_ENABLED` flag from runner; Oracle suite runs in `./spec/ordered_run.sh` by default.
|
|
28
|
+
- ActiveRecord 8 support: relaxed dev dependency to `>= 7.1, < 9.0`.
|
|
29
|
+
- Added dynamic runtime listener tests (anonymous classes) across all adapters and documentation section.
|
|
30
|
+
- Simplified README with concise overview and links to GitHub Pages; moved legacy content to `README.legacy.md`.
|
data/Gemfile.lock
CHANGED
data/README.legacy.md
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
Rokaki is a small DSL for building safe, composable filters for ActiveRecord queries — without writing SQL. It maps incoming params to predicates on models and associations and works across PostgreSQL, MySQL, SQL Server, and Oracle.
|
|
2
|
+
|
|
3
|
+
- Works with ActiveRecord 7.1 and 8.x
|
|
4
|
+
- LIKE modes: `:prefix`, `:suffix`, `:circumfix` (+ synonyms) and array‑of‑terms
|
|
5
|
+
- Nested filters with auto‑joins and qualified columns
|
|
6
|
+
- Block‑form DSL (`filter_map do ... end`) and classic argument form
|
|
7
|
+
- Runtime usage: build an anonymous filter class from a payload (no predeclared class needed)
|
|
8
|
+
|
|
9
|
+
Install
|
|
10
|
+
```ruby
|
|
11
|
+
gem "rokaki"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Docs
|
|
15
|
+
- Usage and examples: https://tevio.github.io/rokaki/usage
|
|
16
|
+
- Adapters: https://tevio.github.io/rokaki/adapters
|
|
17
|
+
- Configuration: https://tevio.github.io/rokaki/configuration
|
|
18
|
+
|
|
19
|
+
Tip: For a dynamic runtime listener (build a filter class from a JSON/hash payload at runtime), see “Dynamic runtime listener” in the Usage docs.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# Rokaki
|
|
24
|
+
|
|
25
|
+
[](https://badge.fury.io/rb/rokaki)
|
|
26
|
+
[](https://github.com/tevio/rokaki/actions/workflows/spec.yml)
|
|
27
|
+
|
|
28
|
+
Rokaki is a small DSL for building safe, composable filters for ActiveRecord queries — without writing SQL. It maps incoming params to predicates on models and associations and works across PostgreSQL, MySQL, SQL Server, and Oracle.
|
|
29
|
+
|
|
30
|
+
- Works with ActiveRecord 7.1 and 8.x
|
|
31
|
+
- LIKE modes: `:prefix`, `:suffix`, `:circumfix` (+ synonyms) and array‑of‑terms
|
|
32
|
+
- Nested filters with auto‑joins and qualified columns
|
|
33
|
+
- Block‑form DSL (`filter_map do ... end`) and classic argument form
|
|
34
|
+
- Runtime usage: build an anonymous filter class from a payload (no predeclared class needed)
|
|
35
|
+
|
|
36
|
+
Install
|
|
37
|
+
```ruby
|
|
38
|
+
gem "rokaki"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Minimal example
|
|
42
|
+
```ruby
|
|
43
|
+
class ArticleQuery
|
|
44
|
+
include Rokaki::FilterModel
|
|
45
|
+
filter_model :article, db: :postgres
|
|
46
|
+
define_query_key :q
|
|
47
|
+
filter_map do
|
|
48
|
+
like title: :circumfix
|
|
49
|
+
nested :author do
|
|
50
|
+
like first_name: :prefix
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
attr_accessor :filters
|
|
54
|
+
def initialize(filters: {}) ; @filters = filters ; end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
ArticleQuery.new(filters: { q: ["Kavya", "Mateo"] }).results
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Docs
|
|
61
|
+
- Usage and examples: https://tevio.github.io/rokaki/usage
|
|
62
|
+
- Adapters: https://tevio.github.io/rokaki/adapters
|
|
63
|
+
- Configuration: https://tevio.github.io/rokaki/configuration
|
|
64
|
+
|
|
65
|
+
Tip: For a dynamic runtime listener (build a filter class from a JSON/hash payload at runtime), see “Dynamic runtime listener” in the Usage docs.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Further reading
|
|
70
|
+
|
|
71
|
+
- Usage and examples: https://tevio.github.io/rokaki/usage
|
|
72
|
+
- Database adapters: https://tevio.github.io/rokaki/adapters
|
|
73
|
+
- Configuration and environment variables: https://tevio.github.io/rokaki/configuration
|
|
74
|
+
|
|
75
|
+
Changelog: see `CHANGELOG.md`.
|
|
76
|
+
|
|
77
|
+
The overall vision is to abstract away all of the lower level repetitive SQL and relational code to allow you to write model filters in a simple, relatively intuitive way, using ruby hashes and arrays mostly.
|
|
78
|
+
|
|
79
|
+
The DSL allows you to construct complex search models to filter results through without writing any SQL. I would recommend the reader to consult the specs in order to understand the features and syntax in detail, an intermediate understanding of Ruby and rspec TDD, and basic relational logic are recommended.
|
|
80
|
+
|
|
81
|
+
There are two modes of use, `Filterable` (designed for plain Ruby) and `FilterModel` (designed for Rails) that can be activated through the use of two mixins respectively, `include Rokaki::Filterable` or `include Rokaki::FilterModel`.
|
|
82
|
+
## Installation
|
|
83
|
+
|
|
84
|
+
Add this line to your application's Gemfile:
|
|
85
|
+
|
|
86
|
+
You can install from Rubygems:
|
|
87
|
+
```
|
|
88
|
+
gem 'rokaki'
|
|
89
|
+
```
|
|
90
|
+
Or from github
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
gem 'rokaki', git: 'https://github.com/tevio/rokaki.git'
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
And then execute:
|
|
97
|
+
|
|
98
|
+
$ bundle
|
|
99
|
+
|
|
100
|
+
## `Rokaki::Filterable` - Usage
|
|
101
|
+
|
|
102
|
+
To use the DSL first include the `Rokaki::Filterable` module in your [por](http://blog.jayfields.com/2007/10/ruby-poro.html) class.
|
|
103
|
+
|
|
104
|
+
### `#define_filter_keys`
|
|
105
|
+
#### A Simple Example
|
|
106
|
+
|
|
107
|
+
A simple example might be:-
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
class FilterArticles
|
|
111
|
+
include Rokaki::Filterable
|
|
112
|
+
|
|
113
|
+
def initialize(filters:)
|
|
114
|
+
@filters = filters
|
|
115
|
+
@articles = Article
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
attr_accessor :filters
|
|
119
|
+
|
|
120
|
+
define_filter_keys :date, author: [:first_name, :last_name]
|
|
121
|
+
|
|
122
|
+
def filter_results
|
|
123
|
+
@articles = @articles.where(date: date) if date
|
|
124
|
+
@articles = @articles.joins(:author).where(authors: { first_name: author_first_name }) if author_first_name
|
|
125
|
+
@articles = @articles.joins(:author).where(authors: { last_name: author_last_name }) if author_last_name
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
article_filter = FilterArticles.new(filters: {
|
|
130
|
+
date: '10-10-10',
|
|
131
|
+
author: {
|
|
132
|
+
first_name: 'Steve',
|
|
133
|
+
last_name: 'Martin'
|
|
134
|
+
}})
|
|
135
|
+
article_filter.author_first_name == 'Steve'
|
|
136
|
+
article_filter.author_last_name == 'Martin'
|
|
137
|
+
article_filter.date == '10-10-10'
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
In this example Rokaki maps the "flat" attribute "keys" `date`, `author_first_name` and `author_last_name` to a `@filters` object with the expected deep structure `{ date: '10-10-10', author: { first_name: 'Steve' } }`, to make it simple to use them in filter queries.
|
|
141
|
+
|
|
142
|
+
#### A More Complex Example
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
class AdvancedFilterable
|
|
146
|
+
include Rokaki::Filterable
|
|
147
|
+
|
|
148
|
+
def initialize(filters:)
|
|
149
|
+
@fyltrz = filters
|
|
150
|
+
end
|
|
151
|
+
attr_accessor :fyltrz
|
|
152
|
+
|
|
153
|
+
filterable_object_name :fyltrz
|
|
154
|
+
filter_key_infix :__
|
|
155
|
+
define_filter_keys :basic, advanced: {
|
|
156
|
+
filter_key_1: [:filter_key_2, { filter_key_3: :deep_node }],
|
|
157
|
+
filter_key_4: :deep_leaf_array
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
advanced_filterable = AdvancedFilterable.new(filters: {
|
|
163
|
+
basic: 'ABC',
|
|
164
|
+
advanced: {
|
|
165
|
+
filter_key_1: {
|
|
166
|
+
filter_key_2: '123',
|
|
167
|
+
filter_key_3: { deep_node: 'NODE' }
|
|
168
|
+
},
|
|
169
|
+
filter_key_4: { deep_leaf_array: [1,2,3,4] }
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
advanced_filterable.advanced__filter_key_4__deep_leaf_array == [1,2,3,4]
|
|
174
|
+
advanced_filterable.advanced__filter_key_1__filter_key_3__deep_node == 'NODE'
|
|
175
|
+
```
|
|
176
|
+
### `#define_filter_map`
|
|
177
|
+
The define_filter_map method is more suited to classic "search", where you might want to search multiple fields on a model or across a graph. See the section on [filter_map](https://github.com/tevio/rokaki#2-the-filter_map-command-syntax) with OR for more on this kind of application.
|
|
178
|
+
|
|
179
|
+
This method takes a single field in the passed in filters hash and maps it to fields named in the second param, this is useful if you want to search for a single value across many different fields or associated tables simultaneously.
|
|
180
|
+
|
|
181
|
+
#### A Simple Example
|
|
182
|
+
```ruby
|
|
183
|
+
class FilterMap
|
|
184
|
+
include Rokaki::Filterable
|
|
185
|
+
|
|
186
|
+
def initialize(fylterz:)
|
|
187
|
+
@fylterz = fylterz
|
|
188
|
+
end
|
|
189
|
+
attr_accessor :fylterz
|
|
190
|
+
|
|
191
|
+
filterable_object_name :fylterz
|
|
192
|
+
define_filter_map :query, :mapped_a, association: :field
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
filter_map = FilterMap.new(fylterz: { query: 'H2O' })
|
|
196
|
+
|
|
197
|
+
filter_map.mapped_a == 'H2O'
|
|
198
|
+
filter_map.association_field = 'H2O'
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
#### Additional `Filterable` options
|
|
202
|
+
You can specify several configuration options, for example a `filter_key_prefix` and a `filter_key_infix` to change the structure of the generated filter accessors.
|
|
203
|
+
|
|
204
|
+
`filter_key_prefix :__` would result in key accessors like `__author_first_name`
|
|
205
|
+
|
|
206
|
+
`filter_key_infix :__` would result in key accessors like `author__first_name`
|
|
207
|
+
|
|
208
|
+
`filterable_object_name :fyltrz` would use an internal filter state object named `@fyltrz` instead of the default `@filters`
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
## `Rokaki::FilterModel` - Usage
|
|
212
|
+
|
|
213
|
+
### ActiveRecord
|
|
214
|
+
Include `Rokaki::FilterModel` in any ActiveRecord model (only AR >= 8.0.3 tested so far) you can generate the filter keys and the actual filter lookup code using the `filters` keyword on a model like so:-
|
|
215
|
+
|
|
216
|
+
```ruby
|
|
217
|
+
# Given the models
|
|
218
|
+
class Author < ActiveRecord::Base
|
|
219
|
+
has_many :articles, inverse_of: :author
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
class Article < ActiveRecord::Base
|
|
223
|
+
belongs_to :author, inverse_of: :articles, required: true
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class ArticleFilter
|
|
228
|
+
include Rokaki::FilterModel
|
|
229
|
+
|
|
230
|
+
filters :date, :title, author: [:first_name, :last_name]
|
|
231
|
+
|
|
232
|
+
attr_accessor :filters
|
|
233
|
+
|
|
234
|
+
def initialize(filters:, model: Article)
|
|
235
|
+
@filters = filters
|
|
236
|
+
@model = model
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
filter = ArticleFilter.new(filters: params[:filters])
|
|
241
|
+
|
|
242
|
+
filtered_results = filter.results
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
### Arrays of params
|
|
246
|
+
You can also filter collections of fields, simply pass an array of filter values instead of a single value, eg:- `{ date: '10-10-10', author: { first_name: ['Author1', 'Author2'] } }`.
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
### Partial matching
|
|
250
|
+
You can use `like` or the case insensitive `ilike` to perform a partial match on a specific field, there are 3 options:- `:prefix`, `:circumfix` and `:suffix`. There are two syntaxes you can use for this:-
|
|
251
|
+
|
|
252
|
+
#### 1. The `filter` command syntax
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
class ArticleFilter
|
|
257
|
+
include Rokaki::FilterModel
|
|
258
|
+
|
|
259
|
+
filter :article,
|
|
260
|
+
like: { # you can use ilike here instead if you want case insensitive results
|
|
261
|
+
author: {
|
|
262
|
+
first_name: :circumfix,
|
|
263
|
+
last_name: :circumfix
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
attr_accessor :filters
|
|
268
|
+
|
|
269
|
+
def initialize(filters:)
|
|
270
|
+
@filters = filters
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
```
|
|
274
|
+
Or
|
|
275
|
+
|
|
276
|
+
#### 2. The `filter_map` command syntax
|
|
277
|
+
`filter_map` takes the model name, then a single 'query' field and maps it to fields named in the options, this is useful if you want to search for a single value across many different fields or associated tables simultaneously. (builds on `define_filter_map`)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
class AuthorFilter
|
|
282
|
+
include Rokaki::FilterModel
|
|
283
|
+
|
|
284
|
+
filter_map :author, :query,
|
|
285
|
+
like: {
|
|
286
|
+
articles: {
|
|
287
|
+
title: :circumfix,
|
|
288
|
+
reviews: {
|
|
289
|
+
title: :circumfix
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
attr_accessor :filters, :model
|
|
295
|
+
|
|
296
|
+
def initialize(filters:)
|
|
297
|
+
@filters = filters
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
filters = { query: "Jiddu" }
|
|
302
|
+
filtered_authors = AuthorFilter.new(filters: filters).results
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
In the above example we search for authors who have written articles containing the word "Jiddu" in the title that also have reviews containing the sames word in their titles.
|
|
306
|
+
|
|
307
|
+
The above example performs an "ALL" like query, where all fields must satisfy the query term. Conversly you can use `or` to perform an "ANY", where any of the fields within the `or` will satisfy the query term, like so:-
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
```ruby
|
|
311
|
+
class AuthorFilter
|
|
312
|
+
include Rokaki::FilterModel
|
|
313
|
+
|
|
314
|
+
filter_map :author, :query,
|
|
315
|
+
like: {
|
|
316
|
+
articles: {
|
|
317
|
+
title: :circumfix,
|
|
318
|
+
or: { # the or is aware of the join and will generate a compound join aware or query
|
|
319
|
+
reviews: {
|
|
320
|
+
title: :circumfix
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
attr_accessor :filters, :model
|
|
327
|
+
|
|
328
|
+
def initialize(filters:)
|
|
329
|
+
@filters = filters
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
filters = { query: "Lao" }
|
|
334
|
+
filtered_authors = AuthorFilter.new(filters: filters).results
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## CAVEATS
|
|
338
|
+
Active record OR over a join may require you to add something like the following in an initializer in order for it to function properly:-
|
|
339
|
+
|
|
340
|
+
### #structurally_incompatible_values_for_or
|
|
341
|
+
|
|
342
|
+
``` ruby
|
|
343
|
+
module ActiveRecord
|
|
344
|
+
module QueryMethods
|
|
345
|
+
def structurally_incompatible_values_for_or(other)
|
|
346
|
+
Relation::SINGLE_VALUE_METHODS.reject { |m| send("#{m}_value") == other send("#{m}_value") } +
|
|
347
|
+
(Relation::MULTI_VALUE_METHODS - [:joins, :eager_load, :references, :extending]).reject { |m| send("#{m}_values") == other send("#{m}_values") } +
|
|
348
|
+
(Relation::CLAUSE_METHODS - [:having, :where]).reject { |m| send("#{m}_clause") == other send("#{m}_clause") }
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### A has one relation to a model called Or
|
|
355
|
+
If you happen to have a model/table named 'Or' then you can override the `or:` key syntax by specifying a special `or_key`:-
|
|
356
|
+
|
|
357
|
+
```ruby
|
|
358
|
+
class AuthorFilter
|
|
359
|
+
include Rokaki::FilterModel
|
|
360
|
+
|
|
361
|
+
or_key :my_or
|
|
362
|
+
filter_map :author, :query,
|
|
363
|
+
like: {
|
|
364
|
+
articles: {
|
|
365
|
+
title: :circumfix,
|
|
366
|
+
my_or: { # the or is aware of the join and will generate a compound join aware or query
|
|
367
|
+
or: { # The Or model has a title field
|
|
368
|
+
title: :circumfix
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
attr_accessor :filters, :model
|
|
375
|
+
|
|
376
|
+
def initialize(filters:)
|
|
377
|
+
@filters = filters
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
filters = { query: "Syntaxes" }
|
|
382
|
+
filtered_authors = AuthorFilter.new(filters: filters).results
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
See [this issue](https://github.com/rails/rails/issues/24055) for details.
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
#### 3. The porcelain command syntax
|
|
390
|
+
|
|
391
|
+
In this syntax you will need to provide three keywords:- `filters`, `like` and `filter_model` if you are not passing in the model type and assigning it to `@model`
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
```ruby
|
|
395
|
+
class ArticleFilter
|
|
396
|
+
include Rokaki::FilterModel
|
|
397
|
+
|
|
398
|
+
filters :date, :title, author: [:first_name, :last_name]
|
|
399
|
+
like title: :circumfix
|
|
400
|
+
# ilike title: :circumfix # case insensitive mode
|
|
401
|
+
|
|
402
|
+
attr_accessor :filters
|
|
403
|
+
|
|
404
|
+
def initialize(filters:, model: Article)
|
|
405
|
+
@filters = filters
|
|
406
|
+
@model = model
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Or without the model in the initializer
|
|
412
|
+
|
|
413
|
+
```ruby
|
|
414
|
+
class ArticleFilter
|
|
415
|
+
include Rokaki::FilterModel
|
|
416
|
+
|
|
417
|
+
filters :date, :title, author: [:first_name, :last_name]
|
|
418
|
+
like title: :circumfix
|
|
419
|
+
filter_model :article
|
|
420
|
+
|
|
421
|
+
attr_accessor :filters
|
|
422
|
+
|
|
423
|
+
def initialize(filters:)
|
|
424
|
+
@filters = filters
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
Would produce a query with a LIKE which circumfixes '%' around the filter term, like:-
|
|
430
|
+
|
|
431
|
+
```ruby
|
|
432
|
+
@model = @model.where('title LIKE :query', query: "%#{title}%")
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Deep nesting
|
|
436
|
+
You can filter joins both with basic matching and partial matching
|
|
437
|
+
```ruby
|
|
438
|
+
class ArticleFilter
|
|
439
|
+
include Rokaki::FilterModel
|
|
440
|
+
|
|
441
|
+
filter :author,
|
|
442
|
+
like: {
|
|
443
|
+
articles: {
|
|
444
|
+
reviews: {
|
|
445
|
+
title: :circumfix
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
attr_accessor :filters
|
|
451
|
+
|
|
452
|
+
def initialize(filters:)
|
|
453
|
+
@filters = filters
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Array params
|
|
459
|
+
You can pass array params (and partially match them), to filters (search multiple matches) in databases that support it (postgres) by passing the `db` param to the filter keyword, and passing an array of search terms at runtine
|
|
460
|
+
|
|
461
|
+
```ruby
|
|
462
|
+
class ArticleFilter
|
|
463
|
+
include Rokaki::FilterModel
|
|
464
|
+
|
|
465
|
+
filter :article,
|
|
466
|
+
like: {
|
|
467
|
+
author: {
|
|
468
|
+
first_name: :circumfix,
|
|
469
|
+
last_name: :circumfix
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
match: %i[title created_at],
|
|
473
|
+
db: :postgres
|
|
474
|
+
|
|
475
|
+
attr_accessor :filters
|
|
476
|
+
|
|
477
|
+
def initialize(filters:)
|
|
478
|
+
@filters = filters
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
filterable = ArticleFilter.new(filters:
|
|
483
|
+
{
|
|
484
|
+
author: {
|
|
485
|
+
first_name: ['Match One', 'Match Two']
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
filterable.results
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
## Development
|
|
495
|
+
|
|
496
|
+
### Ruby setup
|
|
497
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
|
498
|
+
|
|
499
|
+
### Setting up the test databases
|
|
500
|
+
|
|
501
|
+
#### Postgres
|
|
502
|
+
```
|
|
503
|
+
docker pull postgres
|
|
504
|
+
docker run --name rokaki-postgres -e POSTGRES_USER=rokaki -e POSTGRES_PASSWORD=rokaki -d -p 5432:5432 postgres
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
#### Mysql
|
|
508
|
+
```
|
|
509
|
+
docker pull mysql
|
|
510
|
+
docker run --name rokaki-mysql -e MYSQL_ROOT_PASSWORD=rokaki -e MYSQL_PASSWORD=rokaki -e MYSQL_DATABASE=rokaki -e MYSQL_USER=rokaki -d -p 3306:3306 mysql:latest mysqld
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### Specialised test runner
|
|
514
|
+
The test suite is designed to run against all supported database backends, since this can cause problems with timing and connection pools, the recommended way to run the tests is via a shell script.
|
|
515
|
+
|
|
516
|
+
From the rokaki root directory run: `./spec/ordered_run.sh`. This is the same script that runs on the Github CI here: [](https://github.com/tevio/rokaki/actions/workflows/spec.yml)
|
|
517
|
+
|
|
518
|
+
### Standard test runner (only recommended for development cycles)
|
|
519
|
+
You can still run `rake spec` to run the tests, there's no guarantee they will all pass due to race conditions from using multiple db backends (see above), but this mode is recommended for focusing on specific backends or tests during development (comment out what you don't want). You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
520
|
+
|
|
521
|
+
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 tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
522
|
+
|
|
523
|
+
## Contributing
|
|
524
|
+
|
|
525
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/tevio/rokaki. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
|
526
|
+
|
|
527
|
+
## License
|
|
528
|
+
|
|
529
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
530
|
+
|
|
531
|
+
## Code of Conduct
|
|
532
|
+
|
|
533
|
+
Everyone interacting in the Rokaki project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/tevio/rokaki/blob/master/CODE_OF_CONDUCT.md).
|