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.
data/README.md CHANGED
@@ -3,462 +3,42 @@
3
3
  [![Gem Version](https://badge.fury.io/rb/rokaki.svg)](https://badge.fury.io/rb/rokaki)
4
4
  [![Run RSpec tests](https://github.com/tevio/rokaki/actions/workflows/spec.yml/badge.svg)](https://github.com/tevio/rokaki/actions/workflows/spec.yml)
5
5
 
6
- This gem was written to dry up filtering services in ActiveRecord based Rails apps or any plain Ruby app looking to implement "filters" or "faceted" search.
6
+ Supported backends:
7
7
 
8
- 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.
8
+ [![PostgreSQL](https://img.shields.io/badge/PostgreSQL-336791?logo=postgresql&logoColor=white)](https://github.com/tevio/rokaki/actions/workflows/spec.yml)
9
+ [![MySQL](https://img.shields.io/badge/MySQL-4479A1?logo=mysql&logoColor=white)](https://github.com/tevio/rokaki/actions/workflows/spec.yml)
10
+ [![SQL Server](https://img.shields.io/badge/SQL%20Server-CC2927?logo=microsoft-sql-server&logoColor=white)](https://github.com/tevio/rokaki/actions/workflows/spec.yml)
11
+ [![Oracle](https://img.shields.io/badge/Oracle-F80000?logo=oracle&logoColor=white)](https://github.com/tevio/rokaki/actions/workflows/spec.yml)
12
+ [![SQLite](https://img.shields.io/badge/SQLite-003B57?logo=sqlite&logoColor=white)](https://github.com/tevio/rokaki/actions/workflows/spec.yml)
9
13
 
10
- 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.
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
- 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`.
13
- ## Installation
14
-
15
- Add this line to your application's Gemfile:
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
- class AdvancedFilterable
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
- #### Additional `Filterable` options
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
- class ArticleFilter
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
- ### Specialised test runner
445
- 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.
446
-
447
- From the rokaki root directory run: `./spec/ordered_run.sh`. This is the same script that runs on the Github CI here: [![Run RSpec tests](https://github.com/tevio/rokaki/actions/workflows/spec.yml/badge.svg)](https://github.com/tevio/rokaki/actions/workflows/spec.yml)
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
- ## License
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
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
40
+ ---
461
41
 
462
- ## Code of Conduct
42
+ ## Further reading
463
43
 
464
- 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).
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 Oracle.
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`).
@@ -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), and SQL Server (2022) services and runs the ordered spec suite. See `.github/workflows/spec.yml`.
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, and SQL Server
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.13"
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.13"
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.