active_fields 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -4
  3. data/CHANGELOG.md +33 -2
  4. data/README.md +478 -90
  5. data/app/models/active_fields/field/boolean.rb +3 -0
  6. data/app/models/active_fields/field/date.rb +3 -0
  7. data/app/models/active_fields/field/date_array.rb +3 -0
  8. data/app/models/active_fields/field/date_time.rb +4 -1
  9. data/app/models/active_fields/field/date_time_array.rb +4 -1
  10. data/app/models/active_fields/field/decimal.rb +6 -1
  11. data/app/models/active_fields/field/decimal_array.rb +6 -1
  12. data/app/models/active_fields/field/enum.rb +3 -0
  13. data/app/models/active_fields/field/enum_array.rb +3 -0
  14. data/app/models/active_fields/field/integer.rb +3 -0
  15. data/app/models/active_fields/field/integer_array.rb +3 -0
  16. data/app/models/active_fields/field/text.rb +3 -0
  17. data/app/models/active_fields/field/text_array.rb +3 -0
  18. data/app/models/active_fields/field.rb +5 -0
  19. data/app/models/concerns/active_fields/customizable_concern.rb +93 -4
  20. data/app/models/concerns/active_fields/field_concern.rb +26 -3
  21. data/db/migrate/20240229230000_create_active_fields_tables.rb +1 -1
  22. data/lib/active_fields/casters/date_time_caster.rb +1 -3
  23. data/lib/active_fields/casters/decimal_caster.rb +2 -5
  24. data/lib/active_fields/constants.rb +55 -0
  25. data/lib/active_fields/engine.rb +7 -0
  26. data/lib/active_fields/finders/array_finder.rb +112 -0
  27. data/lib/active_fields/finders/base_finder.rb +73 -0
  28. data/lib/active_fields/finders/boolean_finder.rb +20 -0
  29. data/lib/active_fields/finders/date_array_finder.rb +65 -0
  30. data/lib/active_fields/finders/date_finder.rb +32 -0
  31. data/lib/active_fields/finders/date_time_array_finder.rb +65 -0
  32. data/lib/active_fields/finders/date_time_finder.rb +32 -0
  33. data/lib/active_fields/finders/decimal_array_finder.rb +65 -0
  34. data/lib/active_fields/finders/decimal_finder.rb +32 -0
  35. data/lib/active_fields/finders/enum_array_finder.rb +41 -0
  36. data/lib/active_fields/finders/enum_finder.rb +20 -0
  37. data/lib/active_fields/finders/integer_array_finder.rb +65 -0
  38. data/lib/active_fields/finders/integer_finder.rb +32 -0
  39. data/lib/active_fields/finders/singular_finder.rb +66 -0
  40. data/lib/active_fields/finders/text_array_finder.rb +47 -0
  41. data/lib/active_fields/finders/text_finder.rb +81 -0
  42. data/lib/active_fields/has_active_fields.rb +3 -4
  43. data/lib/active_fields/registry.rb +38 -0
  44. data/lib/active_fields/version.rb +1 -1
  45. data/lib/active_fields.rb +79 -31
  46. data/lib/generators/active_fields/install/install_generator.rb +1 -1
  47. data/lib/generators/active_fields/scaffold/USAGE +9 -0
  48. data/lib/generators/active_fields/scaffold/scaffold_generator.rb +34 -0
  49. data/lib/generators/active_fields/scaffold/templates/controllers/active_fields_controller.rb +133 -0
  50. data/lib/generators/active_fields/scaffold/templates/controllers/concerns/active_fields_controller_concern.rb +33 -0
  51. data/lib/generators/active_fields/scaffold/templates/helpers/active_fields_helper.rb +100 -0
  52. data/lib/generators/active_fields/scaffold/templates/javascript/controllers/active_fields_finders_form_controller.js +59 -0
  53. data/lib/generators/active_fields/scaffold/templates/javascript/controllers/array_field_controller.js +25 -0
  54. data/lib/generators/active_fields/scaffold/templates/views/active_fields/edit.html.erb +5 -0
  55. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/_form.html.erb +42 -0
  56. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_array_size.html.erb +16 -0
  57. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_boolean.html.erb +21 -0
  58. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_date.html.erb +16 -0
  59. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_date_array.html.erb +16 -0
  60. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_datetime.html.erb +16 -0
  61. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_datetime_array.html.erb +16 -0
  62. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_decimal.html.erb +16 -0
  63. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_decimal_array.html.erb +16 -0
  64. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_enum.html.erb +16 -0
  65. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_enum_array.html.erb +16 -0
  66. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_integer.html.erb +16 -0
  67. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_integer_array.html.erb +16 -0
  68. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_text.html.erb +16 -0
  69. data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_text_array.html.erb +16 -0
  70. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_boolean.html.erb +53 -0
  71. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_date.html.erb +58 -0
  72. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_date_array.html.erb +70 -0
  73. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime.html.erb +63 -0
  74. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime_array.html.erb +75 -0
  75. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal.html.erb +63 -0
  76. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal_array.html.erb +76 -0
  77. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_enum.html.erb +61 -0
  78. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_enum_array.html.erb +73 -0
  79. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_integer.html.erb +58 -0
  80. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_integer_array.html.erb +70 -0
  81. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_text.html.erb +53 -0
  82. data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_text_array.html.erb +70 -0
  83. data/lib/generators/active_fields/scaffold/templates/views/active_fields/index.html.erb +41 -0
  84. data/lib/generators/active_fields/scaffold/templates/views/active_fields/new.html.erb +5 -0
  85. data/lib/generators/active_fields/scaffold/templates/views/active_fields/show.html.erb +29 -0
  86. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_boolean.html.erb +8 -0
  87. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_date.html.erb +4 -0
  88. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_date_array.html.erb +12 -0
  89. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_datetime.html.erb +4 -0
  90. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_datetime_array.html.erb +12 -0
  91. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_decimal.html.erb +4 -0
  92. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_decimal_array.html.erb +12 -0
  93. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_enum.html.erb +4 -0
  94. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_enum_array.html.erb +4 -0
  95. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_integer.html.erb +4 -0
  96. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_integer_array.html.erb +12 -0
  97. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_text.html.erb +4 -0
  98. data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_text_array.html.erb +12 -0
  99. data/lib/generators/active_fields/scaffold/templates/views/shared/_array_field.html.erb +19 -0
  100. metadata +78 -10
  101. data/lib/active_fields/customizable_config.rb +0 -24
data/README.md CHANGED
@@ -4,14 +4,14 @@
4
4
  [![Gem downloads count](https://img.shields.io/gem/dt/active_fields)](https://rubygems.org/gems/active_fields)
5
5
  [![Github Actions CI](https://github.com/lassoid/active_fields/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/lassoid/active_fields/actions/workflows/main.yml)
6
6
 
7
- **ActiveFields** is a Rails plugin that implements the Entity-Attribute-Value (EAV) pattern,
7
+ **ActiveFields** is a _Rails_ plugin that implements the _Entity-Attribute-Value_ (_EAV_) pattern,
8
8
  enabling the addition of custom fields to any model at runtime without requiring changes to the database schema.
9
9
 
10
10
  ## Key Concepts
11
11
 
12
- - **Active Field**: A record with the definition of a custom field.
13
- - **Active Value**: A record that stores the value of an _Active Field_ for a specific _Customizable_.
14
- - **Customizable**: A record that has custom fields.
12
+ - **Customizable**: A record that has custom fields (_Entity_).
13
+ - **Active Field**: A record with the definition of a custom field (_Attribute_).
14
+ - **Active Value**: A record that stores the value of an _Active Field_ for a specific _Customizable_ (_Value_).
15
15
 
16
16
  ## Models Structure
17
17
 
@@ -24,11 +24,11 @@ classDiagram
24
24
  + string name
25
25
  + string type
26
26
  + string customizable_type
27
- + json default_value
27
+ + json default_value_meta
28
28
  + json options
29
29
  }
30
30
  class ActiveValue {
31
- + json value
31
+ + json value_meta
32
32
  }
33
33
  class Customizable {
34
34
  // This is your model
@@ -56,85 +56,113 @@ such as booleans, strings, numbers, arrays, etc.
56
56
  3. Add the `has_active_fields` method to any models where you want to enable custom fields:
57
57
 
58
58
  ```ruby
59
- class Author < ApplicationRecord
59
+ class Post < ApplicationRecord
60
60
  has_active_fields
61
61
  end
62
62
  ```
63
63
 
64
- 4. Implement the necessary code to work with _Active Fields_.
64
+ 4. Run scaffold generator.
65
65
 
66
- This plugin provides a convenient API and helpers, allowing you to write code that meets your specific needs
66
+ This plugin provides a convenient API, allowing you to write code that meets your specific needs
67
67
  without being forced to use predefined implementations that is hard to extend.
68
68
 
69
- Generally, you should:
70
- - Implement a controller and UI for managing _Active Fields_.
71
- - Add inputs for _Active Values_ in _Customizable_ forms and permit their params in the controller.
72
-
73
- To set _Active Values_ for your _Customizable_, use the `active_fields_attributes=` method,
74
- that integrates with Rails `fields_for` to generate appropriate form fields.
75
- Alternatively, the alias `active_fields=` can be used in contexts without `fields_for`, such as APIs.
76
-
77
- To prepare a collection of _Active Values_ for use with the `fields_for` builder,
78
- call the `initialize_active_values` method.
79
-
80
- **Note:** By default, Rails form fields insert an empty string into array (multiple) parameters.
81
- You’ll need to handle the removal of these empty strings.
82
-
83
- ```ruby
84
- # app/controllers/posts_controller.rb
85
- # ...
86
-
87
- def new
88
- @post = Post.new
89
- @post.initialize_active_values
90
- end
91
-
92
- def edit
93
- @post.initialize_active_values
94
- end
95
-
96
- def post_params
97
- permitted_params = params.require(:post).permit(
98
- # ...
99
- active_fields_attributes: [:name, :value, :_destroy, value: []],
100
- )
101
- permitted_params[:active_fields_attributes]&.each do |_index, value_attrs|
102
- value_attrs[:value] = compact_array_param(value_attrs[:value]) if value_attrs[:value].is_a?(Array)
103
- end
104
-
105
- permitted_params
106
- end
107
-
108
- def compact_array_param(value)
109
- if value.first == ""
110
- value[1..-1]
111
- else
112
- value
113
- end
114
- end
115
- ```
116
-
117
- ```erb
118
- # app/views/posts/_form.html.erb
119
- # ...
120
-
121
- <%= form.fields_for :active_fields, post.active_values.sort_by(&:active_field_id), include_id: false do |active_fields_form| %>
122
- <%= active_fields_form.hidden_field :name %>
123
- # Render appropriate Active Value input and (optionally) destroy flag here
124
- <% end %>
125
-
126
- # ...
127
- ```
128
-
129
- You can find a detailed [example](https://github.com/lassoid/active_fields/blob/main/spec/dummy)
130
- of how to implement this in a full-stack Rails application.
131
- Feel free to explore the source code and run it locally:
69
+ However, for a quick start, you can generate a scaffold by running the following command:
132
70
 
133
71
  ```shell
134
- spec/dummy/bin/setup
135
- bin/rails s
72
+ bin/rails generate active_fields:scaffold
136
73
  ```
137
74
 
75
+ This command generates a controller, routes, views for managing _Active Fields_,
76
+ along with form inputs for _Active Values_, search form and some useful helper methods that will be used in next steps.
77
+
78
+ **Note:** The array field helper and search form use _Stimulus_ for interactivity.
79
+ If your app doesn't already include _Stimulus_, you can [easily add it](https://github.com/hotwired/stimulus-rails).
80
+ Alternatively, if you prefer not to use _Stimulus_, you should implement your own JavaScript code.
81
+
82
+ 5. Add _Active Fields_ inputs in _Customizables_ forms and permit their params in controllers.
83
+
84
+ There are two methods available on _Customizable_ models for retrieving _Active Values_:
85
+ - `active_values` returns collection of only existing _Active Values_.
86
+ - `initialize_active_values` builds any missing _Active Values_ and returns the full collection.
87
+
88
+ Choose the method that suits your requirements.
89
+ In most cases, however, `initialize_active_values` is the more suitable option.
90
+
91
+ ```erb
92
+ # app/views/posts/_form.html.erb
93
+ # ...
94
+
95
+ <%= form.fields_for :active_fields, post.initialize_active_values.sort_by(&:active_field_id), include_id: false do |active_fields_form| %>
96
+ <%= active_fields_form.hidden_field :name %>
97
+ <%= render_active_value_input(form: active_fields_form, active_value: active_fields_form.object) %>
98
+ <% end %>
99
+
100
+ # ...
101
+ ```
102
+
103
+ Permit the _Active Fields_ attributes in your _Customizables_ controllers:
104
+
105
+ ```ruby
106
+ # app/controllers/posts_controller.rb
107
+ # ...
108
+
109
+ def post_params
110
+ permitted_params = params.require(:post).permit(
111
+ # ...
112
+ active_fields_attributes: [:name, :value, :_destroy, value: []],
113
+ )
114
+ permitted_params[:active_fields_attributes]&.each do |_index, value_attrs|
115
+ value_attrs[:value] = compact_array_param(value_attrs[:value]) if value_attrs[:value].is_a?(Array)
116
+ end
117
+
118
+ permitted_params
119
+ end
120
+ ```
121
+
122
+ **Note:** Here we use the `active_fields_attributes=` method (as a permitted parameter),
123
+ that integrates well with _Rails_ `fields_for` to generate appropriate form fields.
124
+ Alternatively, the alias `active_fields=` can be used in contexts without `fields_for`, such as API controllers.
125
+
126
+ **Note:** `compact_array_param` is a helper method, that was added by scaffold generator.
127
+ It removes an empty string from the beginning of the array parameter.
128
+
129
+ 6. Use the `where_active_fields` query method to filter records and add a search form in _Customizables_ index actions.
130
+
131
+ ```ruby
132
+ # app/controllers/posts_controller.rb
133
+ # ...
134
+
135
+ def index
136
+ @posts = Post.where_active_fields(active_fields_finders_params)
137
+ end
138
+ ```
139
+
140
+ **Note:** `active_fields_finders_params` is a helper method, that was added by scaffold generator.
141
+ It permits params from search form.
142
+
143
+ ```erb
144
+ # app/views/posts/index.html.erb
145
+ # ...
146
+
147
+ <%= render_active_fields_finders_form(active_fields: Post.active_fields, url: posts_path) %>
148
+
149
+ # ...
150
+ ```
151
+
152
+ That's it!
153
+ You can now add _Active Fields_ to _Customizables_ at `http://localhost:3000/active_fields`,
154
+ fill in _Active Values_ within _Customizable_ forms
155
+ and search _Customizables_ using their index actions.
156
+
157
+ You can also explore the [Demo app](https://github.com/lassoid/active_fields/blob/main/spec/dummy)
158
+ where the plugin is fully integrated into a full-stack _Rails_ application.
159
+ Feel free to explore the source code and run it locally:
160
+
161
+ ```shell
162
+ spec/dummy/bin/setup
163
+ bin/rails s
164
+ ```
165
+
138
166
  ## Field Types
139
167
 
140
168
  The plugin comes with a structured set of _Active Fields_ types:
@@ -251,7 +279,7 @@ classDiagram
251
279
  - `name`(`string`)
252
280
  - `type`(`string`)
253
281
  - `customizable_type`(`string`)
254
- - `default_value` (`json`)
282
+ - `default_value_meta` (`json`)
255
283
 
256
284
  ### Field Types Summary
257
285
 
@@ -277,6 +305,238 @@ We replace it with `**` for conciseness.
277
305
 
278
306
  **Note:** Options marked with **\*** are mandatory.
279
307
 
308
+ ### Search Functionality
309
+
310
+ **Note:** This feature is compatible with _PostgreSQL_ 17 and above.
311
+
312
+ The gem provides a built-in search capability. Like _Rails_ nested attributes functionality, it accepts the following argument types:
313
+
314
+ - An array of hashes.
315
+
316
+ ```ruby
317
+ Post.where_active_fields(
318
+ [
319
+ { name: "integer_array", operator: "any_gteq", value: 5 }, # symbol keys
320
+ { "name" => "text", operator: "=", "value" => "Lasso" }, # string keys
321
+ { n: "boolean", op: "!=", v: false }, # compact form (string or symbol keys)
322
+ ],
323
+ )
324
+ ```
325
+
326
+ - A hash of hashes (typically generated by _Rails_ `fields_for` form helper).
327
+
328
+ ```ruby
329
+ Post.where_active_fields(
330
+ {
331
+ "0" => { name: "integer_array", operator: "any_gteq", value: 5 },
332
+ "1" => { "name" => "text", operator: "=", "value" => "Lasso" },
333
+ "2" => { n: "boolean", op: "!=", v: false },
334
+ },
335
+ )
336
+ ```
337
+
338
+ - Permitted parameters (can contain either an array of hashes or a hash of hashes).
339
+
340
+ ```ruby
341
+ Post.where_active_fields(permitted_params)
342
+ ```
343
+
344
+ Key details:
345
+ - `n`/`name` argument must specify the name of an _Active Field_.
346
+ - `v`/`value` argument will be automatically cast to the appropriate type.
347
+ - `op`/`operator` argument can contain either _operation_ or _operator_.
348
+
349
+ Supported _operations_ and _operators_ for each _Active Field_ type are listed below.
350
+
351
+ #### Boolean
352
+
353
+ | Operation | Operator | Description |
354
+ |------------|----------|-----------------------------|
355
+ | `eq` | `=` | Value is equal to given |
356
+ | `not_eq` | `!=` | Value is not equal to given |
357
+
358
+ #### Date
359
+
360
+ | Operation | Operator | Description |
361
+ |------------|----------|-----------------------------------------|
362
+ | `eq` | `=` | Value is equal to given |
363
+ | `not_eq` | `!=` | Value is not equal to given |
364
+ | `gt` | `>` | Value is greater than given |
365
+ | `gteq` | `>=` | Value is greater than or equal to given |
366
+ | `lt` | `<` | Value is less than given |
367
+ | `lteq` | `<=` | Value is less than or equal to given |
368
+
369
+ #### DateArray
370
+
371
+ | Operation | Operator | Description |
372
+ |---------------|----------|----------------------------------------------------------------|
373
+ | `include` | `\|=` | Array value includes given element |
374
+ | `not_include` | `!\|=` | Array value doesn't include given element |
375
+ | `any_gt` | `\|>` | Array value contains an element greater than given |
376
+ | `any_gteq` | `\|>=` | Array value contains an element greater than or equal to given |
377
+ | `any_lt` | `\|<` | Array value contains an element less than given |
378
+ | `any_lteq` | `\|<=` | Array value contains an element less than or equal to given |
379
+ | `all_gt` | `&>` | All elements of array value are greater than given |
380
+ | `all_gteq` | `&>=` | All elements of array value are greater than or equal to given |
381
+ | `all_lt` | `&<` | All elements of array value are less than given |
382
+ | `all_lteq` | `&<=` | All elements of array value are less than or equal to given |
383
+ | `size_eq` | `#=` | Array value size is equal to given |
384
+ | `size_not_eq` | `#!=` | Array value size is not equal to given |
385
+ | `size_gt` | `#>` | Array value size is greater than given |
386
+ | `size_gteq` | `#>=` | Array value size is greater than or equal to given |
387
+ | `size_lt` | `#<` | Array value size is less than given |
388
+ | `size_lteq` | `#<=` | Array value size is less than or equal to given |
389
+
390
+ #### DateTime
391
+
392
+ | Operation | Operator | Description |
393
+ |------------|----------|-----------------------------------------|
394
+ | `eq` | `=` | Value is equal to given |
395
+ | `not_eq` | `!=` | Value is not equal to given |
396
+ | `gt` | `>` | Value is greater than given |
397
+ | `gteq` | `>=` | Value is greater than or equal to given |
398
+ | `lt` | `<` | Value is less than given |
399
+ | `lteq` | `<=` | Value is greater than or equal to given |
400
+
401
+ #### DateTimeArray
402
+
403
+ | Operation | Operator | Description |
404
+ |---------------|----------|----------------------------------------------------------------|
405
+ | `include` | `\|=` | Array value includes given element |
406
+ | `not_include` | `!\|=` | Array value doesn't include given element |
407
+ | `any_gt` | `\|>` | Array value contains an element greater than given |
408
+ | `any_gteq` | `\|>=` | Array value contains an element greater than or equal to given |
409
+ | `any_lt` | `\|<` | Array value contains an element less than given |
410
+ | `any_lteq` | `\|<=` | Array value contains an element less than or equal to given |
411
+ | `all_gt` | `&>` | All elements of array value are greater than given |
412
+ | `all_gteq` | `&>=` | All elements of array value are greater than or equal to given |
413
+ | `all_lt` | `&<` | All elements of array value are less than given |
414
+ | `all_lteq` | `&<=` | All elements of array value are less than or equal to given |
415
+ | `size_eq` | `#=` | Array value size is equal to given |
416
+ | `size_not_eq` | `#!=` | Array value size is not equal to given |
417
+ | `size_gt` | `#>` | Array value size is greater than given |
418
+ | `size_gteq` | `#>=` | Array value size is greater than or equal to given |
419
+ | `size_lt` | `#<` | Array value size is less than given |
420
+ | `size_lteq` | `#<=` | Array value size is less than or equal to given |
421
+
422
+ #### Decimal
423
+
424
+ | Operation | Operator | Description |
425
+ |------------|----------|-----------------------------------------|
426
+ | `eq` | `=` | Value is equal to given |
427
+ | `not_eq` | `!=` | Value is not equal to given |
428
+ | `gt` | `>` | Value is greater than given |
429
+ | `gteq` | `>=` | Value is greater than or equal to given |
430
+ | `lt` | `<` | Value is less than given |
431
+ | `lteq` | `<=` | Value is greater than or equal to given |
432
+
433
+ #### DecimalArray
434
+
435
+ | Operation | Operator | Description |
436
+ |---------------|----------|----------------------------------------------------------------|
437
+ | `include` | `\|=` | Array value includes given element |
438
+ | `not_include` | `!\|=` | Array value doesn't include given element |
439
+ | `any_gt` | `\|>` | Array value contains an element greater than given |
440
+ | `any_gteq` | `\|>=` | Array value contains an element greater than or equal to given |
441
+ | `any_lt` | `\|<` | Array value contains an element less than given |
442
+ | `any_lteq` | `\|<=` | Array value contains an element less than or equal to given |
443
+ | `all_gt` | `&>` | All elements of array value are greater than given |
444
+ | `all_gteq` | `&>=` | All elements of array value are greater than or equal to given |
445
+ | `all_lt` | `&<` | All elements of array value are less than given |
446
+ | `all_lteq` | `&<=` | All elements of array value are less than or equal to given |
447
+ | `size_eq` | `#=` | Array value size is equal to given |
448
+ | `size_not_eq` | `#!=` | Array value size is not equal to given |
449
+ | `size_gt` | `#>` | Array value size is greater than given |
450
+ | `size_gteq` | `#>=` | Array value size is greater than or equal to given |
451
+ | `size_lt` | `#<` | Array value size is less than given |
452
+ | `size_lteq` | `#<=` | Array value size is less than or equal to given |
453
+
454
+ #### Enum
455
+
456
+ | Operation | Operator | Description |
457
+ |------------|----------|-----------------------------|
458
+ | `eq` | `=` | Value is equal to given |
459
+ | `not_eq` | `!=` | Value is not equal to given |
460
+
461
+ #### EnumArray
462
+
463
+ | Operation | Operator | Description |
464
+ |---------------|----------|----------------------------------------------------|
465
+ | `include` | `\|=` | Array value includes given element |
466
+ | `not_include` | `!\|=` | Array value doesn't include given element |
467
+ | `size_eq` | `#=` | Array value size is equal to given |
468
+ | `size_not_eq` | `#!=` | Array value size is not equal to given |
469
+ | `size_gt` | `#>` | Array value size is greater than given |
470
+ | `size_gteq` | `#>=` | Array value size is greater than or equal to given |
471
+ | `size_lt` | `#<` | Array value size is less than given |
472
+ | `size_lteq` | `#<=` | Array value size is less than or equal to given |
473
+
474
+ #### Integer
475
+
476
+ | Operation | Operator | Description |
477
+ |------------|----------|-----------------------------------------|
478
+ | `eq` | `=` | Value is equal to given |
479
+ | `not_eq` | `!=` | Value is not equal to given |
480
+ | `gt` | `>` | Value is greater than given |
481
+ | `gteq` | `>=` | Value is greater than or equal to given |
482
+ | `lt` | `<` | Value is less than given |
483
+ | `lteq` | `<=` | Value is greater than or equal to given |
484
+
485
+ #### IntegerArray
486
+
487
+ | Operation | Operator | Description |
488
+ |---------------|----------|----------------------------------------------------------------|
489
+ | `include` | `\|=` | Array value includes given element |
490
+ | `not_include` | `!\|=` | Array value doesn't include given element |
491
+ | `any_gt` | `\|>` | Array value contains an element greater than given |
492
+ | `any_gteq` | `\|>=` | Array value contains an element greater than or equal to given |
493
+ | `any_lt` | `\|<` | Array value contains an element less than given |
494
+ | `any_lteq` | `\|<=` | Array value contains an element less than or equal to given |
495
+ | `all_gt` | `&>` | All elements of array value are greater than given |
496
+ | `all_gteq` | `&>=` | All elements of array value are greater than or equal to given |
497
+ | `all_lt` | `&<` | All elements of array value are less than given |
498
+ | `all_lteq` | `&<=` | All elements of array value are less than or equal to given |
499
+ | `size_eq` | `#=` | Array value size is equal to given |
500
+ | `size_not_eq` | `#!=` | Array value size is not equal to given |
501
+ | `size_gt` | `#>` | Array value size is greater than given |
502
+ | `size_gteq` | `#>=` | Array value size is greater than or equal to given |
503
+ | `size_lt` | `#<` | Array value size is less than given |
504
+ | `size_lteq` | `#<=` | Array value size is less than or equal to given |
505
+
506
+ #### Text
507
+
508
+ | Operation | Operator | Description |
509
+ |-------------------|----------|-------------------------------------------------------------|
510
+ | `eq` | `=` | Value is equal to given |
511
+ | `not_eq` | `!=` | Value is not equal to given |
512
+ | `start_with` | `^` | Value starts with given substring |
513
+ | `end_with` | `$` | Value ends with given substring |
514
+ | `contain` | `~` | Value contains given substring |
515
+ | `not_start_with` | `!^` | Value doesn't start with given substring |
516
+ | `not_end_with` | `!$` | Value doesn't end with given substring |
517
+ | `not_contain` | `!~` | Value doesn't contain given substring |
518
+ | `istart_with` | `^*` | Value starts with given substring (case-insensitive) |
519
+ | `iend_with` | `$*` | Value ends with given substring (case-insensitive) |
520
+ | `icontain` | `~*` | Value contains given substring (case-insensitive) |
521
+ | `not_istart_with` | `!^*` | Value doesn't start with given substring (case-insensitive) |
522
+ | `not_iend_with` | `!$*` | Value doesn't end with given substring (case-insensitive) |
523
+ | `not_icontain` | `!~*` | Value doesn't contain given substring (case-insensitive) |
524
+
525
+ #### TextArray
526
+
527
+ | Operation | Operator | Description |
528
+ |------------------|----------|-------------------------------------------------------------|
529
+ | `include` | `\|=` | Array value includes given element |
530
+ | `not_include` | `!\|=` | Array value doesn't include given element |
531
+ | `any_start_with` | `\|^` | Array value contains an element starts with given substring |
532
+ | `all_start_with` | `&^` | All elements of array value starts with given substring |
533
+ | `size_eq` | `#=` | Array value size is equal to given |
534
+ | `size_not_eq` | `#!=` | Array value size is not equal to given |
535
+ | `size_gt` | `#>` | Array value size is greater than given |
536
+ | `size_gteq` | `#>=` | Array value size is greater than or equal to given |
537
+ | `size_lt` | `#<` | Array value size is less than given |
538
+ | `size_lteq` | `#<=` | Array value size is less than or equal to given |
539
+
280
540
  ## Configuration
281
541
 
282
542
  ### Limiting Field Types for a Customizable
@@ -336,6 +596,25 @@ class CustomValue < ApplicationRecord
336
596
  end
337
597
  ```
338
598
 
599
+ **Note:** To avoid _STI_ (_Single Table Inheritance_) issues in environments with code reloading (`config.enable_reloading = true`),
600
+ you should ensure that your custom model classes, along with all their superclasses and mix-ins, are non-reloadable.
601
+ Follow these steps:
602
+ - Move your custom model classes to a separate folder, such as `app/models/active_fields`.
603
+ - If your custom model classes subclass `ApplicationRecord` (or other reloadable class) or mix-in reloadable modules,
604
+ move those superclasses and modules to another folder, such as `app/models/core`.
605
+ - After organizing your files, add the following code to your `config/application.rb`:
606
+ ```ruby
607
+ # Disable custom models reloading to avoid STI issues.
608
+ custom_models_dir = "#{root}/app/models/active_fields"
609
+ models_core_dir = "#{root}/app/models/core"
610
+ Rails.autoloaders.main.ignore(custom_models_dir, models_core_dir)
611
+ Rails.autoloaders.once.collapse(custom_models_dir, models_core_dir)
612
+ config.autoload_once_paths += [custom_models_dir, models_core_dir]
613
+ config.eager_load_paths += [custom_models_dir, models_core_dir]
614
+ ```
615
+ This configuration disables namespaces for these folders
616
+ and adds them to `autoload_once_paths`, ensuring they are not reloaded.
617
+
339
618
  ### Adding Custom Field Types
340
619
 
341
620
  To add a custom _Active Field_ type, create a subclass of the `ActiveFields.config.field_base_class`,
@@ -360,6 +639,9 @@ class IpField < ActiveFields.config.field_base_class
360
639
  class_name: "IpCaster",
361
640
  options: -> { { strip: strip? } }, # options that will be passed to the caster
362
641
  },
642
+ finder: { # Optional
643
+ class_name: "IpFinder",
644
+ },
363
645
  )
364
646
 
365
647
  # Store specific attributes in `options`
@@ -411,12 +693,18 @@ class IpArrayField < ActiveFields.config.field_base_class
411
693
  caster: {
412
694
  class_name: "IpArrayCaster",
413
695
  },
696
+ finder: { # Optional
697
+ class_name: "IpArrayFinder",
698
+ },
414
699
  )
415
700
  # ...
416
701
  end
417
702
  ```
418
703
 
419
- For each custom _Active Field_ type, you must define a **validator** and a **caster**:
704
+ **Note:** Similar to custom model classes, you should disable code reloading for custom _Active Field_ type models.
705
+ Place them in the `app/models/active_fields` folder too.
706
+
707
+ For each custom _Active Field_ type, you must define a **validator**, a **caster** and optionally a **finder**:
420
708
 
421
709
  #### Validator
422
710
 
@@ -471,6 +759,96 @@ class IpCaster < ActiveFields::Casters::BaseCaster
471
759
  end
472
760
  ```
473
761
 
762
+ #### Finder
763
+
764
+ To create your custom finder, you should define a class that inherits from one of the following base classes:
765
+ - `ActiveFields::Finders::SingularFinder` - for singular values,
766
+ - `ActiveFields::Finders::ArrayFinder` - for array values,
767
+ - `ActiveFields::Finders::BaseCaster` - if you don’t need built-in helper methods.
768
+
769
+ Finder classes include a DSL for defining search operations and provide helper methods to simplify query building.
770
+ Explore the source code to discover all these methods.
771
+
772
+ ```ruby
773
+ # lib/ip_finder.rb (or anywhere you want)
774
+ class IpFinder < ActiveFields::Finders::SingularFinder
775
+ operation :eq, operator: "=" do |value|
776
+ scope.where(eq(casted_value_field("text"), cast(value)))
777
+ # Equivalent to:
778
+ # if value.is_a?(TrueClass) || value.is_a?(FalseClass) || value.is_a?(NilClass)
779
+ # scope.where("CAST(active_fields_values.value_meta ->> 'const' AS text) IS ?)", cast(value))
780
+ # else
781
+ # scope.where("CAST(active_fields_values.value_meta ->> 'const' AS text) = ?)", cast(value))
782
+ # end
783
+ end
784
+ operation :not_eq, operator: "!=" do |value|
785
+ scope.where(not_eq(casted_value_field("text"), cast(value)))
786
+ end
787
+
788
+ def cast(value)
789
+ IpCaster.new.deserialize(value)
790
+ end
791
+ end
792
+
793
+ # lib/ip_array_finder.rb (or anywhere you want)
794
+ class IpArrayFinder < ActiveFields::Finders::ArrayFinder
795
+ operation :include, operator: "|=" do |value|
796
+ scope.where(value_match_any("==", cast(value)))
797
+ # Equivalent to:
798
+ # scope.where("jsonb_path_exists(active_fields_values.value_meta -> 'const', ?, ?)", "$[*] ? (@ == $value)", { value: cast(value) }.to_json)
799
+ end
800
+ operation :not_include, operator: "!|=" do |value|
801
+ scope.where.not(value_match_any("==", cast(value)))
802
+ end
803
+ operation :size_eq, operator: "#=" do |value|
804
+ scope.where(value_size_eq(value))
805
+ # Equivalent to:
806
+ # scope.where("jsonb_array_length(active_fields_values.value_meta -> 'const') = ?", value&.to_i)
807
+ end
808
+ operation :size_not_eq, operator: "#!=" do |value|
809
+ scope.where(value_size_not_eq(value))
810
+ end
811
+ operation :size_gt, operator: "#>" do |value|
812
+ scope.where(value_size_gt(value))
813
+ end
814
+ operation :size_gteq, operator: "#>=" do |value|
815
+ scope.where(value_size_gteq(value))
816
+ end
817
+ operation :size_lt, operator: "#<" do |value|
818
+ scope.where(value_size_lt(value))
819
+ end
820
+ operation :size_lteq, operator: "#<=" do |value|
821
+ scope.where(value_size_lteq(value))
822
+ end
823
+
824
+ private
825
+
826
+ def cast(value)
827
+ caster = IpCaster.new
828
+ caster.serialize(caster.deserialize(value))
829
+ end
830
+
831
+ # This method must be defined to utilize the `value_match_any` and `value_match_all` helper methods in your class.
832
+ # It should return a valid JSONPath expression for use in PostgreSQL jsonb query functions.
833
+ def jsonpath(operator) = "$[*] ? (@ #{operator} $value)"
834
+ end
835
+ ```
836
+
837
+ Once defined, every _Active Value_ of this type will support the specified search operations!
838
+
839
+ ```ruby
840
+ # Find customizables
841
+ Author.where_active_fields([
842
+ { name: "main_ip", operator: "eq", value: "127.0.0.1" },
843
+ { n: "all_ips", op: "#>=", v: 5 },
844
+ { name: "all_ips", operator: "|=", value: "0.0.0.0" },
845
+ ])
846
+
847
+ # Find Active Values
848
+ IpFinder.new(active_field: ip_active_field).search(op: "eq", value: "127.0.0.1")
849
+ IpArrayFinder.new(active_field: ip_array_active_field).search(op: "#>=", value: 5)
850
+ ```
851
+
474
852
  ### Localization (I18n)
475
853
 
476
854
  The built-in _validators_ primarily use _Rails_ default error types.
@@ -483,12 +861,10 @@ For an example, refer to the [locale file](https://github.com/lassoid/active_fie
483
861
 
484
862
  ## Current Restrictions
485
863
 
486
- 1. Only _PostgreSQL_ is fully supported.
864
+ 1. Only _PostgreSQL_ 17+ is fully supported.
487
865
 
488
866
  The gem is tested exclusively with _PostgreSQL_. Support for other databases is not guaranteed.
489
867
 
490
- However, you can give it a try! :)
491
-
492
868
  2. Updating some _Active Fields_ options may be unsafe.
493
869
 
494
870
  This could cause existing _Active Values_ to become invalid,
@@ -510,7 +886,7 @@ active_field.type # Class name of this Active Field (utilizing STI)
510
886
  active_field.customizable_type # Name of the Customizable model this Active Field is registered to
511
887
  active_field.name # Identifier of this Active Field, it should be unique in scope of customizable_type
512
888
  active_field.default_value_meta # JSON column declaring the default value. Consider using `default_value` instead
513
- active_field.options # A hash (json) containing type-specific attributes for this Active Field
889
+ active_field.options # JSON column containing type-specific attributes for this Active Field
514
890
 
515
891
  # Methods:
516
892
  active_field.default_value # Default value for all Active Values associated with this Active Field
@@ -521,9 +897,10 @@ active_field.value_caster_class # Class used for values casting
521
897
  active_field.value_caster # Caster object that performs values casting
522
898
  active_field.customizable_model # Customizable model class
523
899
  active_field.type_name # Identifier of the type of this Active Field (instead of class name)
900
+ active_field.available_customizable_types # Available Customizable types for this Active Field
524
901
 
525
902
  # Scopes:
526
- ActiveFields::Field::Boolean.for("Author") # Collection of Active Fields registered for the specified Customizable type
903
+ ActiveFields::Field::Boolean.for("Post") # Collection of Active Fields registered for the specified Customizable type
527
904
  ```
528
905
 
529
906
  ### Values API
@@ -546,13 +923,16 @@ active_value.name # Name of the associated Active Field
546
923
  ### Customizable API
547
924
 
548
925
  ```ruby
549
- customizable = Author.take
926
+ customizable = Post.take
550
927
 
551
928
  # Associations:
552
929
  customizable.active_values # `has_many` association with Active Values linked to this Customizable
553
930
 
554
931
  # Methods:
555
932
  customizable.active_fields # Collection of Active Fields registered for this record
933
+ Post.active_fields # Collection of Active Fields registered for this model
934
+ Post.allowed_active_fields_type_names # Active Fields type names allowed for this Customizable model
935
+ Post.allowed_active_fields_class_names # Active Fields class names allowed for this Customizable model
556
936
 
557
937
  # Create, update or destroy Active Values.
558
938
  customizable.active_fields_attributes = [
@@ -577,10 +957,20 @@ customizable.active_fields = [
577
957
  # Please use `active_fields_attributes=`/`active_fields=` instead.
578
958
  customizable.active_values_attributes = attributes
579
959
 
580
- # Build an Active Value, if it doesn't exist, with the default value for each Active Field.
960
+ # Build not existing Active Values, with the default value for each Active Field.
961
+ # Returns full collection of Active Values.
581
962
  # This method is useful with `fields_for`, allowing you to pass the collection as an argument to render new Active Values:
582
- # `form.fields_for :active_values, customizable.active_values`.
963
+ # `form.fields_for :active_fields, customizable.initialize_active_values`.
583
964
  customizable.initialize_active_values
965
+
966
+ # Query Customizables by Active Values.
967
+ Post.where_active_fields(
968
+ [
969
+ { name: "integer_array", operator: "any_gteq", value: 5 }, # symbol keys
970
+ { "name" => "text", operator: "=", "value" => "Lasso" }, # string keys
971
+ { n: "boolean", op: "!=", v: false }, # compact form (string or symbol keys)
972
+ ],
973
+ )
584
974
  ```
585
975
 
586
976
  ### Global Config
@@ -599,14 +989,12 @@ ActiveFields.config.type_class_names # Registered Active Fields class names
599
989
  ActiveFields.config.register_field(:ip, "IpField") # Register a custom Active Field type
600
990
  ```
601
991
 
602
- ### Customizable Config
992
+ ### Registry
603
993
 
604
994
  ```ruby
605
- customizable_model = Author
606
- customizable_model.active_fields_config # Access the Customizable's configuration
607
- customizable_model.active_fields_config.customizable_model # The Customizable model itself
608
- customizable_model.active_fields_config.types # Allowed Active Field types (e.g., `[:boolean]`)
609
- customizable_model.active_fields_config.types_class_names # Allowed Active Field class names (e.g., `[ActiveFields::Field::Boolean]`)
995
+ ActiveFields.registry.add(:boolean, "Post") # Stores relation between Active Field type and customizable type. Please do not use directly.
996
+ ActiveFields.registry.customizable_types_for(:boolean) # Returns Customizable types that allow provided Active Field type name
997
+ ActiveFields.registry.field_type_names_for("Post") # Returns Active Field type names, allowed for given Customizable type
610
998
  ```
611
999
 
612
1000
  ## Development