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.
- checksums.yaml +4 -4
- data/.rubocop.yml +5 -4
- data/CHANGELOG.md +33 -2
- data/README.md +478 -90
- data/app/models/active_fields/field/boolean.rb +3 -0
- data/app/models/active_fields/field/date.rb +3 -0
- data/app/models/active_fields/field/date_array.rb +3 -0
- data/app/models/active_fields/field/date_time.rb +4 -1
- data/app/models/active_fields/field/date_time_array.rb +4 -1
- data/app/models/active_fields/field/decimal.rb +6 -1
- data/app/models/active_fields/field/decimal_array.rb +6 -1
- data/app/models/active_fields/field/enum.rb +3 -0
- data/app/models/active_fields/field/enum_array.rb +3 -0
- data/app/models/active_fields/field/integer.rb +3 -0
- data/app/models/active_fields/field/integer_array.rb +3 -0
- data/app/models/active_fields/field/text.rb +3 -0
- data/app/models/active_fields/field/text_array.rb +3 -0
- data/app/models/active_fields/field.rb +5 -0
- data/app/models/concerns/active_fields/customizable_concern.rb +93 -4
- data/app/models/concerns/active_fields/field_concern.rb +26 -3
- data/db/migrate/20240229230000_create_active_fields_tables.rb +1 -1
- data/lib/active_fields/casters/date_time_caster.rb +1 -3
- data/lib/active_fields/casters/decimal_caster.rb +2 -5
- data/lib/active_fields/constants.rb +55 -0
- data/lib/active_fields/engine.rb +7 -0
- data/lib/active_fields/finders/array_finder.rb +112 -0
- data/lib/active_fields/finders/base_finder.rb +73 -0
- data/lib/active_fields/finders/boolean_finder.rb +20 -0
- data/lib/active_fields/finders/date_array_finder.rb +65 -0
- data/lib/active_fields/finders/date_finder.rb +32 -0
- data/lib/active_fields/finders/date_time_array_finder.rb +65 -0
- data/lib/active_fields/finders/date_time_finder.rb +32 -0
- data/lib/active_fields/finders/decimal_array_finder.rb +65 -0
- data/lib/active_fields/finders/decimal_finder.rb +32 -0
- data/lib/active_fields/finders/enum_array_finder.rb +41 -0
- data/lib/active_fields/finders/enum_finder.rb +20 -0
- data/lib/active_fields/finders/integer_array_finder.rb +65 -0
- data/lib/active_fields/finders/integer_finder.rb +32 -0
- data/lib/active_fields/finders/singular_finder.rb +66 -0
- data/lib/active_fields/finders/text_array_finder.rb +47 -0
- data/lib/active_fields/finders/text_finder.rb +81 -0
- data/lib/active_fields/has_active_fields.rb +3 -4
- data/lib/active_fields/registry.rb +38 -0
- data/lib/active_fields/version.rb +1 -1
- data/lib/active_fields.rb +79 -31
- data/lib/generators/active_fields/install/install_generator.rb +1 -1
- data/lib/generators/active_fields/scaffold/USAGE +9 -0
- data/lib/generators/active_fields/scaffold/scaffold_generator.rb +34 -0
- data/lib/generators/active_fields/scaffold/templates/controllers/active_fields_controller.rb +133 -0
- data/lib/generators/active_fields/scaffold/templates/controllers/concerns/active_fields_controller_concern.rb +33 -0
- data/lib/generators/active_fields/scaffold/templates/helpers/active_fields_helper.rb +100 -0
- data/lib/generators/active_fields/scaffold/templates/javascript/controllers/active_fields_finders_form_controller.js +59 -0
- data/lib/generators/active_fields/scaffold/templates/javascript/controllers/array_field_controller.js +25 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/edit.html.erb +5 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/_form.html.erb +42 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_array_size.html.erb +16 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_boolean.html.erb +21 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_date.html.erb +16 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_date_array.html.erb +16 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_datetime.html.erb +16 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_datetime_array.html.erb +16 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_decimal.html.erb +16 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_decimal_array.html.erb +16 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_enum.html.erb +16 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_enum_array.html.erb +16 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_integer.html.erb +16 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_integer_array.html.erb +16 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_text.html.erb +16 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/finders/inputs/_text_array.html.erb +16 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_boolean.html.erb +53 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_date.html.erb +58 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_date_array.html.erb +70 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime.html.erb +63 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime_array.html.erb +75 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal.html.erb +63 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal_array.html.erb +76 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_enum.html.erb +61 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_enum_array.html.erb +73 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_integer.html.erb +58 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_integer_array.html.erb +70 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_text.html.erb +53 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_text_array.html.erb +70 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/index.html.erb +41 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/new.html.erb +5 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/show.html.erb +29 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_boolean.html.erb +8 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_date.html.erb +4 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_date_array.html.erb +12 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_datetime.html.erb +4 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_datetime_array.html.erb +12 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_decimal.html.erb +4 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_decimal_array.html.erb +12 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_enum.html.erb +4 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_enum_array.html.erb +4 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_integer.html.erb +4 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_integer_array.html.erb +12 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_text.html.erb +4 -0
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/values/inputs/_text_array.html.erb +12 -0
- data/lib/generators/active_fields/scaffold/templates/views/shared/_array_field.html.erb +19 -0
- metadata +78 -10
- data/lib/active_fields/customizable_config.rb +0 -24
data/README.md
CHANGED
@@ -4,14 +4,14 @@
|
|
4
4
|
[](https://rubygems.org/gems/active_fields)
|
5
5
|
[](https://github.com/lassoid/active_fields/actions/workflows/main.yml)
|
6
6
|
|
7
|
-
**ActiveFields** is a
|
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
|
-
- **
|
13
|
-
- **Active
|
14
|
-
- **
|
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
|
27
|
+
+ json default_value_meta
|
28
28
|
+ json options
|
29
29
|
}
|
30
30
|
class ActiveValue {
|
31
|
-
+ json
|
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
|
59
|
+
class Post < ApplicationRecord
|
60
60
|
has_active_fields
|
61
61
|
end
|
62
62
|
```
|
63
63
|
|
64
|
-
4.
|
64
|
+
4. Run scaffold generator.
|
65
65
|
|
66
|
-
This plugin provides a convenient API
|
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
|
-
|
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
|
-
|
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
|
-
- `
|
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
|
-
|
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 #
|
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("
|
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 =
|
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
|
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 :
|
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
|
-
###
|
992
|
+
### Registry
|
603
993
|
|
604
994
|
```ruby
|
605
|
-
|
606
|
-
|
607
|
-
|
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
|