active_fields 1.0.0 → 2.0.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.
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