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
data/README.md
CHANGED
|
@@ -3,462 +3,42 @@
|
|
|
3
3
|
[](https://badge.fury.io/rb/rokaki)
|
|
4
4
|
[](https://github.com/tevio/rokaki/actions/workflows/spec.yml)
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Supported backends:
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
[](https://github.com/tevio/rokaki/actions/workflows/spec.yml)
|
|
9
|
+
[](https://github.com/tevio/rokaki/actions/workflows/spec.yml)
|
|
10
|
+
[](https://github.com/tevio/rokaki/actions/workflows/spec.yml)
|
|
11
|
+
[](https://github.com/tevio/rokaki/actions/workflows/spec.yml)
|
|
12
|
+
[](https://github.com/tevio/rokaki/actions/workflows/spec.yml)
|
|
9
13
|
|
|
10
|
-
|
|
14
|
+
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, Oracle, and SQLite.
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
You can install from Rubygems:
|
|
18
|
-
```
|
|
19
|
-
gem 'rokaki'
|
|
20
|
-
```
|
|
21
|
-
Or from github
|
|
22
|
-
|
|
23
|
-
```ruby
|
|
24
|
-
gem 'rokaki', git: 'https://github.com/tevio/rokaki.git'
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
And then execute:
|
|
28
|
-
|
|
29
|
-
$ bundle
|
|
30
|
-
|
|
31
|
-
## `Rokaki::Filterable` - Usage
|
|
32
|
-
|
|
33
|
-
To use the DSL first include the `Rokaki::Filterable` module in your [por](http://blog.jayfields.com/2007/10/ruby-poro.html) class.
|
|
34
|
-
|
|
35
|
-
### `#define_filter_keys`
|
|
36
|
-
#### A Simple Example
|
|
37
|
-
|
|
38
|
-
A simple example might be:-
|
|
39
|
-
|
|
40
|
-
```ruby
|
|
41
|
-
class FilterArticles
|
|
42
|
-
include Rokaki::Filterable
|
|
43
|
-
|
|
44
|
-
def initialize(filters:)
|
|
45
|
-
@filters = filters
|
|
46
|
-
@articles = Article
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
attr_accessor :filters
|
|
50
|
-
|
|
51
|
-
define_filter_keys :date, author: [:first_name, :last_name]
|
|
52
|
-
|
|
53
|
-
def filter_results
|
|
54
|
-
@articles = @articles.where(date: date) if date
|
|
55
|
-
@articles = @articles.joins(:author).where(authors: { first_name: author_first_name }) if author_first_name
|
|
56
|
-
@articles = @articles.joins(:author).where(authors: { last_name: author_last_name }) if author_last_name
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
article_filter = FilterArticles.new(filters: {
|
|
61
|
-
date: '10-10-10',
|
|
62
|
-
author: {
|
|
63
|
-
first_name: 'Steve',
|
|
64
|
-
last_name: 'Martin'
|
|
65
|
-
}})
|
|
66
|
-
article_filter.author_first_name == 'Steve'
|
|
67
|
-
article_filter.author_last_name == 'Martin'
|
|
68
|
-
article_filter.date == '10-10-10'
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
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.
|
|
72
|
-
|
|
73
|
-
#### A More Complex Example
|
|
16
|
+
- Works with ActiveRecord 7.1 and 8.x
|
|
17
|
+
- LIKE modes: `:prefix`, `:suffix`, `:circumfix` (+ synonyms) and array‑of‑terms
|
|
18
|
+
- Nested filters with auto‑joins and qualified columns
|
|
19
|
+
- Block‑form DSL (`filter_map do ... end`) and classic argument form
|
|
20
|
+
- Runtime usage: build an anonymous filter class from a payload (no predeclared class needed)
|
|
74
21
|
|
|
22
|
+
Install
|
|
75
23
|
```ruby
|
|
76
|
-
|
|
77
|
-
include Rokaki::Filterable
|
|
78
|
-
|
|
79
|
-
def initialize(filters:)
|
|
80
|
-
@fyltrz = filters
|
|
81
|
-
end
|
|
82
|
-
attr_accessor :fyltrz
|
|
83
|
-
|
|
84
|
-
filterable_object_name :fyltrz
|
|
85
|
-
filter_key_infix :__
|
|
86
|
-
define_filter_keys :basic, advanced: {
|
|
87
|
-
filter_key_1: [:filter_key_2, { filter_key_3: :deep_node }],
|
|
88
|
-
filter_key_4: :deep_leaf_array
|
|
89
|
-
}
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
advanced_filterable = AdvancedFilterable.new(filters: {
|
|
94
|
-
basic: 'ABC',
|
|
95
|
-
advanced: {
|
|
96
|
-
filter_key_1: {
|
|
97
|
-
filter_key_2: '123',
|
|
98
|
-
filter_key_3: { deep_node: 'NODE' }
|
|
99
|
-
},
|
|
100
|
-
filter_key_4: { deep_leaf_array: [1,2,3,4] }
|
|
101
|
-
}
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
advanced_filterable.advanced__filter_key_4__deep_leaf_array == [1,2,3,4]
|
|
105
|
-
advanced_filterable.advanced__filter_key_1__filter_key_3__deep_node == 'NODE'
|
|
106
|
-
```
|
|
107
|
-
### `#define_filter_map`
|
|
108
|
-
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.
|
|
109
|
-
|
|
110
|
-
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.
|
|
111
|
-
|
|
112
|
-
#### A Simple Example
|
|
113
|
-
```ruby
|
|
114
|
-
class FilterMap
|
|
115
|
-
include Rokaki::Filterable
|
|
116
|
-
|
|
117
|
-
def initialize(fylterz:)
|
|
118
|
-
@fylterz = fylterz
|
|
119
|
-
end
|
|
120
|
-
attr_accessor :fylterz
|
|
121
|
-
|
|
122
|
-
filterable_object_name :fylterz
|
|
123
|
-
define_filter_map :query, :mapped_a, association: :field
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
filter_map = FilterMap.new(fylterz: { query: 'H2O' })
|
|
127
|
-
|
|
128
|
-
filter_map.mapped_a == 'H2O'
|
|
129
|
-
filter_map.association_field = 'H2O'
|
|
24
|
+
gem "rokaki"
|
|
130
25
|
```
|
|
131
26
|
|
|
132
|
-
|
|
133
|
-
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.
|
|
134
|
-
|
|
135
|
-
`filter_key_prefix :__` would result in key accessors like `__author_first_name`
|
|
136
|
-
|
|
137
|
-
`filter_key_infix :__` would result in key accessors like `author__first_name`
|
|
138
|
-
|
|
139
|
-
`filterable_object_name :fylterz` would use an internal filter state object named `@fyltrz` instead of the default `@filters`
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
## `Rokaki::FilterModel` - Usage
|
|
143
|
-
|
|
144
|
-
### ActiveRecord
|
|
145
|
-
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:-
|
|
146
|
-
|
|
147
|
-
```ruby
|
|
148
|
-
# Given the models
|
|
149
|
-
class Author < ActiveRecord::Base
|
|
150
|
-
has_many :articles, inverse_of: :author
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
class Article < ActiveRecord::Base
|
|
154
|
-
belongs_to :author, inverse_of: :articles, required: true
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
class ArticleFilter
|
|
159
|
-
include Rokaki::FilterModel
|
|
160
|
-
|
|
161
|
-
filters :date, :title, author: [:first_name, :last_name]
|
|
162
|
-
|
|
163
|
-
attr_accessor :filters
|
|
164
|
-
|
|
165
|
-
def initialize(filters:, model: Article)
|
|
166
|
-
@filters = filters
|
|
167
|
-
@model = model
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
filter = ArticleFilter.new(filters: params[:filters])
|
|
172
|
-
|
|
173
|
-
filtered_results = filter.results
|
|
174
|
-
|
|
175
|
-
```
|
|
176
|
-
### Arrays of params
|
|
177
|
-
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'] } }`.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
### Partial matching
|
|
181
|
-
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:-
|
|
182
|
-
|
|
183
|
-
#### 1. The `filter` command syntax
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
```ruby
|
|
187
|
-
class ArticleFilter
|
|
188
|
-
include Rokaki::FilterModel
|
|
189
|
-
|
|
190
|
-
filter :article,
|
|
191
|
-
like: { # you can use ilike here instead if you want case insensitive results
|
|
192
|
-
author: {
|
|
193
|
-
first_name: :circumfix,
|
|
194
|
-
last_name: :circumfix
|
|
195
|
-
}
|
|
196
|
-
},
|
|
197
|
-
|
|
198
|
-
attr_accessor :filters
|
|
199
|
-
|
|
200
|
-
def initialize(filters:)
|
|
201
|
-
@filters = filters
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
```
|
|
205
|
-
Or
|
|
206
|
-
|
|
207
|
-
#### 2. The `filter_map` command syntax
|
|
208
|
-
`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`)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
```ruby
|
|
212
|
-
class AuthorFilter
|
|
213
|
-
include Rokaki::FilterModel
|
|
214
|
-
|
|
215
|
-
filter_map :author, :query,
|
|
216
|
-
like: {
|
|
217
|
-
articles: {
|
|
218
|
-
title: :circumfix,
|
|
219
|
-
reviews: {
|
|
220
|
-
title: :circumfix
|
|
221
|
-
}
|
|
222
|
-
},
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
attr_accessor :filters, :model
|
|
226
|
-
|
|
227
|
-
def initialize(filters:)
|
|
228
|
-
@filters = filters
|
|
229
|
-
end
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
filters = { query: "Jiddu" }
|
|
233
|
-
filtered_authors = AuthorFilter.new(filters: filters).results
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
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.
|
|
237
|
-
|
|
238
|
-
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:-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
```ruby
|
|
242
|
-
class AuthorFilter
|
|
243
|
-
include Rokaki::FilterModel
|
|
244
|
-
|
|
245
|
-
filter_map :author, :query,
|
|
246
|
-
like: {
|
|
247
|
-
articles: {
|
|
248
|
-
title: :circumfix,
|
|
249
|
-
or: { # the or is aware of the join and will generate a compound join aware or query
|
|
250
|
-
reviews: {
|
|
251
|
-
title: :circumfix
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
},
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
attr_accessor :filters, :model
|
|
258
|
-
|
|
259
|
-
def initialize(filters:)
|
|
260
|
-
@filters = filters
|
|
261
|
-
end
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
filters = { query: "Lao" }
|
|
265
|
-
filtered_authors = AuthorFilter.new(filters: filters).results
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
## CAVEATS
|
|
269
|
-
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:-
|
|
270
|
-
|
|
271
|
-
### #structurally_incompatible_values_for_or
|
|
272
|
-
|
|
273
|
-
``` ruby
|
|
274
|
-
module ActiveRecord
|
|
275
|
-
module QueryMethods
|
|
276
|
-
def structurally_incompatible_values_for_or(other)
|
|
277
|
-
Relation::SINGLE_VALUE_METHODS.reject { |m| send("#{m}_value") == other.send("#{m}_value") } +
|
|
278
|
-
(Relation::MULTI_VALUE_METHODS - [:joins, :eager_load, :references, :extending]).reject { |m| send("#{m}_values") == other.send("#{m}_values") } +
|
|
279
|
-
(Relation::CLAUSE_METHODS - [:having, :where]).reject { |m| send("#{m}_clause") == other.send("#{m}_clause") }
|
|
280
|
-
end
|
|
281
|
-
end
|
|
282
|
-
end
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
### A has one relation to a model called Or
|
|
286
|
-
If you happen to have a model/table named 'Or' then you can override the `or:` key syntax by specifying a special `or_key`:-
|
|
287
|
-
|
|
288
|
-
```ruby
|
|
289
|
-
class AuthorFilter
|
|
290
|
-
include Rokaki::FilterModel
|
|
291
|
-
|
|
292
|
-
or_key :my_or
|
|
293
|
-
filter_map :author, :query,
|
|
294
|
-
like: {
|
|
295
|
-
articles: {
|
|
296
|
-
title: :circumfix,
|
|
297
|
-
my_or: { # the or is aware of the join and will generate a compound join aware or query
|
|
298
|
-
or: { # The Or model has a title field
|
|
299
|
-
title: :circumfix
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
},
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
attr_accessor :filters, :model
|
|
306
|
-
|
|
307
|
-
def initialize(filters:)
|
|
308
|
-
@filters = filters
|
|
309
|
-
end
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
filters = { query: "Syntaxes" }
|
|
313
|
-
filtered_authors = AuthorFilter.new(filters: filters).results
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
See [this issue](https://github.com/rails/rails/issues/24055) for details.
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
#### 3. The porcelain command syntax
|
|
321
|
-
|
|
322
|
-
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`
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
```ruby
|
|
326
|
-
class ArticleFilter
|
|
327
|
-
include Rokaki::FilterModel
|
|
328
|
-
|
|
329
|
-
filters :date, :title, author: [:first_name, :last_name]
|
|
330
|
-
like title: :circumfix
|
|
331
|
-
# ilike title: :circumfix # case insensitive mode
|
|
332
|
-
|
|
333
|
-
attr_accessor :filters
|
|
334
|
-
|
|
335
|
-
def initialize(filters:, model: Article)
|
|
336
|
-
@filters = filters
|
|
337
|
-
@model = model
|
|
338
|
-
end
|
|
339
|
-
end
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
Or without the model in the initializer
|
|
343
|
-
|
|
344
|
-
```ruby
|
|
345
|
-
class ArticleFilter
|
|
346
|
-
include Rokaki::FilterModel
|
|
347
|
-
|
|
348
|
-
filters :date, :title, author: [:first_name, :last_name]
|
|
349
|
-
like title: :circumfix
|
|
350
|
-
filter_model :article
|
|
351
|
-
|
|
352
|
-
attr_accessor :filters
|
|
353
|
-
|
|
354
|
-
def initialize(filters:)
|
|
355
|
-
@filters = filters
|
|
356
|
-
end
|
|
357
|
-
end
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
Would produce a query with a LIKE which circumfixes '%' around the filter term, like:-
|
|
361
|
-
|
|
362
|
-
```ruby
|
|
363
|
-
@model = @model.where('title LIKE :query', query: "%#{title}%")
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
### Deep nesting
|
|
367
|
-
You can filter joins both with basic matching and partial matching
|
|
368
|
-
```ruby
|
|
369
|
-
class ArticleFilter
|
|
370
|
-
include Rokaki::FilterModel
|
|
371
|
-
|
|
372
|
-
filter :author,
|
|
373
|
-
like: {
|
|
374
|
-
articles: {
|
|
375
|
-
reviews: {
|
|
376
|
-
title: :circumfix
|
|
377
|
-
}
|
|
378
|
-
},
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
attr_accessor :filters
|
|
382
|
-
|
|
383
|
-
def initialize(filters:)
|
|
384
|
-
@filters = filters
|
|
385
|
-
end
|
|
386
|
-
end
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
### Array params
|
|
390
|
-
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
|
|
27
|
+
Or from github
|
|
391
28
|
|
|
392
29
|
```ruby
|
|
393
|
-
|
|
394
|
-
include Rokaki::FilterModel
|
|
395
|
-
|
|
396
|
-
filter :article,
|
|
397
|
-
like: {
|
|
398
|
-
author: {
|
|
399
|
-
first_name: :circumfix,
|
|
400
|
-
last_name: :circumfix
|
|
401
|
-
}
|
|
402
|
-
},
|
|
403
|
-
match: %i[title created_at],
|
|
404
|
-
db: :postgres
|
|
405
|
-
|
|
406
|
-
attr_accessor :filters
|
|
407
|
-
|
|
408
|
-
def initialize(filters:)
|
|
409
|
-
@filters = filters
|
|
410
|
-
end
|
|
411
|
-
end
|
|
412
|
-
|
|
413
|
-
filterable = ArticleFilter.new(filters:
|
|
414
|
-
{
|
|
415
|
-
author: {
|
|
416
|
-
first_name: ['Match One', 'Match Two']
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
)
|
|
420
|
-
|
|
421
|
-
filterable.results
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
## Development
|
|
426
|
-
|
|
427
|
-
### Ruby setup
|
|
428
|
-
After checking out the repo, run `bin/setup` to install dependencies.
|
|
429
|
-
|
|
430
|
-
### Setting up the test databases
|
|
431
|
-
|
|
432
|
-
#### Postgres
|
|
433
|
-
```
|
|
434
|
-
docker pull postgres
|
|
435
|
-
docker run --name rokaki-postgres -e POSTGRES_USER=rokaki -e POSTGRES_PASSWORD=rokaki -d -p 5432:5432 postgres
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
#### Mysql
|
|
439
|
-
```
|
|
440
|
-
docker pull mysql
|
|
441
|
-
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
|
|
30
|
+
gem 'rokaki', git: 'https://github.com/tevio/rokaki.git'
|
|
442
31
|
```
|
|
443
32
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
### Standard test runner (only recommended for development cycles)
|
|
450
|
-
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.
|
|
451
|
-
|
|
452
|
-
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).
|
|
453
|
-
|
|
454
|
-
## Contributing
|
|
455
|
-
|
|
456
|
-
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.
|
|
33
|
+
Docs
|
|
34
|
+
- Usage and examples: https://tevio.github.io/rokaki/usage
|
|
35
|
+
- Adapters: https://tevio.github.io/rokaki/adapters
|
|
36
|
+
- Configuration: https://tevio.github.io/rokaki/configuration
|
|
457
37
|
|
|
458
|
-
|
|
38
|
+
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.
|
|
459
39
|
|
|
460
|
-
|
|
40
|
+
---
|
|
461
41
|
|
|
462
|
-
##
|
|
42
|
+
## Further reading
|
|
463
43
|
|
|
464
|
-
|
|
44
|
+
[Legacy README](README.legacy.md)
|
data/docs/adapters.md
CHANGED
|
@@ -4,7 +4,7 @@ title: Database adapters
|
|
|
4
4
|
permalink: /adapters
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
Rokaki generates adapter‑aware SQL for PostgreSQL, MySQL, SQL Server, and
|
|
7
|
+
Rokaki generates adapter‑aware SQL for PostgreSQL, MySQL, SQL Server, Oracle, and SQLite.
|
|
8
8
|
|
|
9
9
|
## Overview
|
|
10
10
|
|
|
@@ -20,6 +20,12 @@ Rokaki generates adapter‑aware SQL for PostgreSQL, MySQL, SQL Server, and Orac
|
|
|
20
20
|
- Uses `LIKE` with safe escaping
|
|
21
21
|
- Multi‑term input expands to OR‑chained predicates (e.g., `(col LIKE :q0 OR col LIKE :q1 ...)`) with `ESCAPE '\\'`
|
|
22
22
|
- Case sensitivity follows DB collation by default; future versions may add inline `COLLATE` options
|
|
23
|
+
- Oracle
|
|
24
|
+
- Uses `LIKE`; arrays of terms are OR‑chained; case‑insensitive paths use `UPPER(column) LIKE UPPER(:q)`
|
|
25
|
+
- SQLite
|
|
26
|
+
- Embedded (no separate server needed)
|
|
27
|
+
- Uses `LIKE`; arrays of terms are OR‑chained across predicates
|
|
28
|
+
- Case sensitivity follows SQLite defaults (generally case‑sensitive for ASCII)
|
|
23
29
|
|
|
24
30
|
## LIKE modes
|
|
25
31
|
|
|
@@ -44,3 +50,21 @@ When you pass an array of terms, Rokaki composes adapter‑appropriate SQL that
|
|
|
44
50
|
- PostgreSQL: `ILIKE` is case‑insensitive; `LIKE` is case‑sensitive depending on collation/LC settings but generally treated as case‑sensitive for ASCII.
|
|
45
51
|
- MySQL: `LIKE` case sensitivity depends on column collation; `LIKE BINARY` forces byte comparison (case‑sensitive for ASCII).
|
|
46
52
|
- SQL Server: The server/database/column collation determines sensitivity. Rokaki currently defers to your DB’s default. If you need deterministic behavior regardless of DB defaults, consider using a case‑sensitive collation on the column or open an issue to discuss inline `COLLATE` options.
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
## SQLite
|
|
56
|
+
|
|
57
|
+
SQLite is embedded and requires no separate server process. Rokaki treats it as a first-class adapter.
|
|
58
|
+
|
|
59
|
+
- Default test configuration uses an in-memory database.
|
|
60
|
+
- Arrays of terms in LIKE filters are OR-chained across predicates.
|
|
61
|
+
- Case sensitivity follows SQLite defaults (generally case-sensitive for ASCII); collations can affect this.
|
|
62
|
+
|
|
63
|
+
Example config (tests):
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
adapter: sqlite3
|
|
67
|
+
database: ":memory:"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
To persist a database file locally, set `SQLITE_DATABASE` to a path (e.g., `tmp/test.sqlite3`).
|
data/docs/configuration.md
CHANGED
|
@@ -31,6 +31,17 @@ Rokaki's test helpers (used in the specs) support environment variable overrides
|
|
|
31
31
|
- `POSTGRES_PASSWORD` (default: `postgres`)
|
|
32
32
|
- `POSTGRES_DATABASE` (default: `rokaki`)
|
|
33
33
|
|
|
34
|
+
### Oracle
|
|
35
|
+
- `ORACLE_HOST` (default: `127.0.0.1`)
|
|
36
|
+
- `ORACLE_PORT` (default: `1521`)
|
|
37
|
+
- `ORACLE_USERNAME` (default: `ROKAKI`)
|
|
38
|
+
- `ORACLE_PASSWORD` (default: `rokaki`)
|
|
39
|
+
- `ORACLE_DATABASE` (service/alias; default: `/freepdb1` in CI)
|
|
40
|
+
- `ORACLE_SERVICE_NAME` (optional; if set, Rokaki builds a full descriptor string)
|
|
41
|
+
|
|
42
|
+
### SQLite
|
|
43
|
+
- `SQLITE_DATABASE` (path to a SQLite file; if unset, tests use an in-memory DB via `":memory:"`)
|
|
44
|
+
|
|
34
45
|
## SQL Server notes
|
|
35
46
|
|
|
36
47
|
- Rokaki uses `LIKE` with proper escaping and OR expansion for arrays of terms.
|
|
@@ -60,4 +71,4 @@ bundle exec rspec spec/lib/03_sqlserver_aware_spec.rb
|
|
|
60
71
|
|
|
61
72
|
## GitHub Actions
|
|
62
73
|
|
|
63
|
-
The repository includes CI that starts MySQL (9.4), PostgreSQL (13),
|
|
74
|
+
The repository includes CI that starts MySQL (9.4), PostgreSQL (13), SQL Server (2022), and Oracle (23 Free) services and runs the ordered spec suite. SQLite is embedded and requires no service container. See `.github/workflows/spec.yml`.
|
data/docs/index.md
CHANGED
|
@@ -6,7 +6,7 @@ permalink: /
|
|
|
6
6
|
|
|
7
7
|
Rokaki is a small Ruby library that helps you build safe, composable filters for ActiveRecord queries in web requests.
|
|
8
8
|
|
|
9
|
-
- Works with PostgreSQL, MySQL,
|
|
9
|
+
- Works with PostgreSQL, MySQL, SQL Server, Oracle, and SQLite
|
|
10
10
|
- Supports simple and nested filters
|
|
11
11
|
- LIKE-based matching with prefix/suffix/circumfix modes (circumfix also accepts synonyms: parafix, confix, ambifix)
|
|
12
12
|
- Array-of-terms matching (adapter-aware)
|
|
@@ -21,7 +21,7 @@ Get started below or jump to:
|
|
|
21
21
|
Add to your application's Gemfile:
|
|
22
22
|
|
|
23
23
|
```ruby
|
|
24
|
-
gem "rokaki", "~> 0.
|
|
24
|
+
gem "rokaki", "~> 0.15"
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
Then:
|
|
@@ -41,7 +41,7 @@ class ArticleQuery
|
|
|
41
41
|
include Rokaki::FilterModel
|
|
42
42
|
|
|
43
43
|
# Tell Rokaki which model to query and which DB adapter semantics to use
|
|
44
|
-
filter_model :article, db: :postgres # or :mysql, :sqlserver
|
|
44
|
+
filter_model :article, db: :postgres # or :mysql, :sqlserver, :oracle, :sqlite
|
|
45
45
|
|
|
46
46
|
# Map a single query key (:q) to multiple LIKE targets on Article
|
|
47
47
|
define_query_key :q
|
|
@@ -109,5 +109,5 @@ All modes accept either a single string or an array of terms.
|
|
|
109
109
|
## Next steps
|
|
110
110
|
|
|
111
111
|
- Learn the full DSL and examples in [Usage](./usage)
|
|
112
|
-
- See adapter specifics (PostgreSQL/MySQL/SQL Server) in [Database adapters](./adapters)
|
|
112
|
+
- See adapter specifics (PostgreSQL/MySQL/SQL Server/Oracle/SQLite) in [Database adapters](./adapters)
|
|
113
113
|
- Configure connections and environment variables in [Configuration](./configuration)
|
data/docs/usage.md
CHANGED
|
@@ -11,7 +11,7 @@ This page shows how to use Rokaki to define filters and apply them to ActiveReco
|
|
|
11
11
|
Add the gem to your Gemfile and bundle:
|
|
12
12
|
|
|
13
13
|
```ruby
|
|
14
|
-
gem "rokaki", "~> 0.
|
|
14
|
+
gem "rokaki", "~> 0.15"
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
```bash
|
|
@@ -32,7 +32,7 @@ class ArticleQuery
|
|
|
32
32
|
belongs_to :author
|
|
33
33
|
|
|
34
34
|
# Choose model and adapter
|
|
35
|
-
filter_model :article, db: :postgres # or :mysql, :sqlserver
|
|
35
|
+
filter_model :article, db: :postgres # or :mysql, :sqlserver, :oracle, :sqlite
|
|
36
36
|
|
|
37
37
|
# Map a single query key (:q) to multiple LIKE targets
|
|
38
38
|
define_query_key :q
|
|
@@ -186,3 +186,69 @@ f.__author__location__city # => 'London'
|
|
|
186
186
|
Tips:
|
|
187
187
|
- `filter_key_prefix` and `filter_key_infix` control the generated accessor names.
|
|
188
188
|
- Inside the block, `nested :association` affects all `filters` declared within it.
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
## Dynamic runtime listener (no code changes needed)
|
|
192
|
+
|
|
193
|
+
You can construct a Rokaki filter class at runtime from a payload (e.g., JSON → Hash) and use it immediately — no prior class is required. Rokaki will compile the tiny class on the fly and generate the methods once.
|
|
194
|
+
|
|
195
|
+
### FilterModel example
|
|
196
|
+
```ruby
|
|
197
|
+
# Example payload (e.g., parsed JSON)
|
|
198
|
+
payload = {
|
|
199
|
+
model: :article,
|
|
200
|
+
db: :postgres, # or :mysql, :sqlserver, :oracle
|
|
201
|
+
query_key: :q, # the key in params with search term(s)
|
|
202
|
+
like: { # like mappings (deeply nested allowed)
|
|
203
|
+
title: :circumfix,
|
|
204
|
+
author: { first_name: :prefix }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# Build an anonymous class at runtime and use it immediately
|
|
209
|
+
listener = Class.new do
|
|
210
|
+
include Rokaki::FilterModel
|
|
211
|
+
|
|
212
|
+
filter_model payload[:model], db: payload[:db]
|
|
213
|
+
define_query_key payload[:query_key]
|
|
214
|
+
|
|
215
|
+
filter_map do
|
|
216
|
+
like payload[:like]
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
attr_accessor :filters
|
|
220
|
+
def initialize(filters: {})
|
|
221
|
+
@filters = filters
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
results = listener.new(filters: { q: ["Ada", "Turing"] }).results
|
|
226
|
+
# => ActiveRecord::Relation
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Filterable example (no SQL)
|
|
230
|
+
```ruby
|
|
231
|
+
mapper = Class.new do
|
|
232
|
+
include Rokaki::Filterable
|
|
233
|
+
filter_key_prefix :__
|
|
234
|
+
|
|
235
|
+
filter_map do
|
|
236
|
+
filters :date, author: [:first_name, :last_name]
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
attr_reader :filters
|
|
240
|
+
def initialize(filters: {})
|
|
241
|
+
@filters = filters
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
m = mapper.new(filters: { date: '2025-01-01', author: { first_name: 'Ada', last_name: 'Lovelace' } })
|
|
246
|
+
m.__date # => '2025-01-01'
|
|
247
|
+
m.__author__first_name # => 'Ada'
|
|
248
|
+
m.__author__last_name # => 'Lovelace'
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Notes:
|
|
252
|
+
- This approach is production‑ready and requires no core changes to Rokaki.
|
|
253
|
+
- You can cache the generated class by a digest of the payload to avoid recompiling.
|
|
254
|
+
- For maximum safety, validate/allow‑list models/columns coming from untrusted payloads.
|