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.
- 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
|