active_fields 1.1.0 → 2.0.1
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 +32 -2
- data/README.md +411 -38
- 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 +89 -5
- data/app/models/concerns/active_fields/field_concern.rb +26 -5
- data/app/models/concerns/active_fields/value_concern.rb +0 -2
- 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 +2 -1
- 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 +29 -1
- data/lib/generators/active_fields/scaffold/scaffold_generator.rb +9 -0
- data/lib/generators/active_fields/scaffold/templates/controllers/active_fields_controller.rb +0 -10
- 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 +67 -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/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 +1 -1
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_date.html.erb +1 -1
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_date_array.html.erb +1 -1
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime.html.erb +2 -2
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_datetime_array.html.erb +2 -2
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal.html.erb +2 -2
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_decimal_array.html.erb +2 -2
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_enum.html.erb +1 -1
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_enum_array.html.erb +1 -1
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_integer.html.erb +1 -1
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_integer_array.html.erb +1 -1
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_text.html.erb +1 -1
- data/lib/generators/active_fields/scaffold/templates/views/active_fields/forms/_text_array.html.erb +1 -1
- data/lib/generators/active_fields/scaffold/templates/views/shared/_array_field.html.erb +1 -1
- metadata +42 -10
- data/lib/active_fields/customizable_config.rb +0 -24
@@ -12,6 +12,9 @@ module ActiveFields
|
|
12
12
|
class_name: "ActiveFields::Casters::DateTimeCaster",
|
13
13
|
options: -> { { precision: precision } },
|
14
14
|
},
|
15
|
+
finder: {
|
16
|
+
class_name: "ActiveFields::Finders::DateTimeFinder",
|
17
|
+
},
|
15
18
|
)
|
16
19
|
|
17
20
|
store_accessor :options, :required, :min, :max, :precision
|
@@ -19,7 +22,7 @@ module ActiveFields
|
|
19
22
|
validates :required, exclusion: [nil]
|
20
23
|
validates :max, comparison: { greater_than_or_equal_to: :min }, allow_nil: true, if: :min
|
21
24
|
validates :precision,
|
22
|
-
comparison: { greater_than_or_equal_to: 0, less_than_or_equal_to:
|
25
|
+
comparison: { greater_than_or_equal_to: 0, less_than_or_equal_to: ActiveFields::MAX_DATETIME_PRECISION },
|
23
26
|
allow_nil: true
|
24
27
|
|
25
28
|
# If precision is set after attributes that depend on it, deserialization will work correctly,
|
@@ -13,13 +13,16 @@ module ActiveFields
|
|
13
13
|
class_name: "ActiveFields::Casters::DateTimeArrayCaster",
|
14
14
|
options: -> { { precision: precision } },
|
15
15
|
},
|
16
|
+
finder: {
|
17
|
+
class_name: "ActiveFields::Finders::DateTimeArrayFinder",
|
18
|
+
},
|
16
19
|
)
|
17
20
|
|
18
21
|
store_accessor :options, :min, :max, :precision
|
19
22
|
|
20
23
|
validates :max, comparison: { greater_than_or_equal_to: :min }, allow_nil: true, if: :min
|
21
24
|
validates :precision,
|
22
|
-
comparison: { greater_than_or_equal_to: 0, less_than_or_equal_to:
|
25
|
+
comparison: { greater_than_or_equal_to: 0, less_than_or_equal_to: ActiveFields::MAX_DATETIME_PRECISION },
|
23
26
|
allow_nil: true
|
24
27
|
|
25
28
|
# If precision is set after attributes that depend on it, deserialization will work correctly,
|
@@ -12,13 +12,18 @@ module ActiveFields
|
|
12
12
|
class_name: "ActiveFields::Casters::DecimalCaster",
|
13
13
|
options: -> { { precision: precision } },
|
14
14
|
},
|
15
|
+
finder: {
|
16
|
+
class_name: "ActiveFields::Finders::DecimalFinder",
|
17
|
+
},
|
15
18
|
)
|
16
19
|
|
17
20
|
store_accessor :options, :required, :min, :max, :precision
|
18
21
|
|
19
22
|
validates :required, exclusion: [nil]
|
20
23
|
validates :max, comparison: { greater_than_or_equal_to: :min }, allow_nil: true, if: :min
|
21
|
-
validates :precision,
|
24
|
+
validates :precision,
|
25
|
+
comparison: { greater_than_or_equal_to: 0, less_than_or_equal_to: ActiveFields::MAX_DECIMAL_PRECISION },
|
26
|
+
allow_nil: true
|
22
27
|
|
23
28
|
# If precision is set after attributes that depend on it, deserialization will work correctly,
|
24
29
|
# but an incorrect internal value may be saved in the DB.
|
@@ -13,12 +13,17 @@ module ActiveFields
|
|
13
13
|
class_name: "ActiveFields::Casters::DecimalArrayCaster",
|
14
14
|
options: -> { { precision: precision } },
|
15
15
|
},
|
16
|
+
finder: {
|
17
|
+
class_name: "ActiveFields::Finders::DecimalArrayFinder",
|
18
|
+
},
|
16
19
|
)
|
17
20
|
|
18
21
|
store_accessor :options, :min, :max, :precision
|
19
22
|
|
20
23
|
validates :max, comparison: { greater_than_or_equal_to: :min }, allow_nil: true, if: :min
|
21
|
-
validates :precision,
|
24
|
+
validates :precision,
|
25
|
+
comparison: { greater_than_or_equal_to: 0, less_than_or_equal_to: ActiveFields::MAX_DECIMAL_PRECISION },
|
26
|
+
allow_nil: true
|
22
27
|
|
23
28
|
# If precision is set after attributes that depend on it, deserialization will work correctly,
|
24
29
|
# but an incorrect internal value may be saved in the DB.
|
@@ -6,25 +6,102 @@ module ActiveFields
|
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
8
|
included do
|
9
|
-
# rubocop:disable Rails/ReflectionClassName
|
10
9
|
has_many :active_values,
|
11
10
|
class_name: ActiveFields.config.value_class_name,
|
12
11
|
as: :customizable,
|
13
12
|
inverse_of: :customizable,
|
14
13
|
autosave: true,
|
15
14
|
dependent: :destroy
|
16
|
-
|
15
|
+
|
16
|
+
# Searches customizables by active_values.
|
17
|
+
#
|
18
|
+
# Accepts an Array of Hashes (symbol/string keys),
|
19
|
+
# a Hash of Hashes generated from HTTP/HTML parameters
|
20
|
+
# or permitted params.
|
21
|
+
# Each element should contain:
|
22
|
+
# - <tt>:n</tt> or <tt>:name</tt> key matching the active_field record name;
|
23
|
+
# - <tt>:op</tt> or <tt>:operator</tt> key specifying search operation or operator;
|
24
|
+
# - <tt>:v</tt> or <tt>:value</tt> key specifying search value.
|
25
|
+
#
|
26
|
+
# Example:
|
27
|
+
#
|
28
|
+
# # Array of hashes
|
29
|
+
# CustomizableModel.where_active_fields(
|
30
|
+
# [
|
31
|
+
# { name: "integer_array", operator: "any_gteq", value: 5 }, # symbol keys
|
32
|
+
# { "name" => "text", operator: "=", "value" => "Lasso" }, # string keys
|
33
|
+
# { n: "boolean", op: "!=", v: false }, # compact form (string or symbol keys)
|
34
|
+
# ],
|
35
|
+
# )
|
36
|
+
#
|
37
|
+
# # Hash of hashes generated from HTTP/HTML parameters
|
38
|
+
# CustomizableModel.where_active_fields(
|
39
|
+
# {
|
40
|
+
# "0" => { name: "integer_array", operator: "any_gteq", value: 5 },
|
41
|
+
# "1" => { "name" => "text", operator: "=", "value" => "Lasso" },
|
42
|
+
# "2" => { n: "boolean", op: "!=", v: false },
|
43
|
+
# },
|
44
|
+
# )
|
45
|
+
#
|
46
|
+
# # Params (must be permitted)
|
47
|
+
# CustomizableModel.where_active_fields(permitted_params)
|
48
|
+
scope :where_active_fields, ->(filters) do
|
49
|
+
filters = filters.to_h if filters.respond_to?(:permitted?)
|
50
|
+
|
51
|
+
unless filters.is_a?(Array) || filters.is_a?(Hash)
|
52
|
+
raise ArgumentError, "Hash or Array expected for `where_active_fields`, got #{filters.class.name}"
|
53
|
+
end
|
54
|
+
|
55
|
+
# Handle `fields_for` params
|
56
|
+
filters = filters.values if filters.is_a?(Hash)
|
57
|
+
|
58
|
+
active_fields_by_name = active_fields.index_by(&:name)
|
59
|
+
|
60
|
+
filters.inject(self) do |scope, filter|
|
61
|
+
filter = filter.to_h if filter.respond_to?(:permitted?)
|
62
|
+
filter = filter.with_indifferent_access
|
63
|
+
|
64
|
+
active_field = active_fields_by_name[filter[:n] || filter[:name]]
|
65
|
+
next scope if active_field.nil?
|
66
|
+
next scope if active_field.value_finder.nil?
|
67
|
+
|
68
|
+
active_values = active_field.value_finder.search(
|
69
|
+
op: filter[:op] || filter[:operator],
|
70
|
+
value: filter[:v] || filter[:value],
|
71
|
+
)
|
72
|
+
next scope if active_values.nil?
|
73
|
+
|
74
|
+
scope.where(id: active_values.select(:customizable_id))
|
75
|
+
end
|
76
|
+
end
|
17
77
|
|
18
78
|
accepts_nested_attributes_for :active_values, allow_destroy: true
|
19
79
|
end
|
20
80
|
|
21
|
-
|
22
|
-
|
81
|
+
class_methods do
|
82
|
+
# Collection of active fields registered for this customizable
|
83
|
+
def active_fields
|
84
|
+
ActiveFields.config.field_base_class.for(name)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns active fields type names allowed for this customizable model.
|
88
|
+
def allowed_active_fields_type_names
|
89
|
+
ActiveFields.registry.field_type_names_for(name).to_a
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns active fields class names allowed for this customizable model.
|
93
|
+
def allowed_active_fields_class_names
|
94
|
+
ActiveFields.config.fields.values_at(*allowed_active_fields_type_names)
|
95
|
+
end
|
23
96
|
end
|
24
97
|
|
98
|
+
delegate :active_fields, to: :class
|
99
|
+
|
25
100
|
# Assigns the given attributes to the active_values association.
|
26
101
|
#
|
27
|
-
# Accepts an Array of Hashes (symbol/string keys)
|
102
|
+
# Accepts an Array of Hashes (symbol/string keys),
|
103
|
+
# a Hash of Hashes generated from HTTP/HTML parameters
|
104
|
+
# or permitted params.
|
28
105
|
# Each element should contain a <tt>:name</tt> key matching an existing active_field record.
|
29
106
|
# Element with a <tt>:value</tt> key will create an active_value if it doesn't exist
|
30
107
|
# or update an existing active_value, with the provided value.
|
@@ -40,6 +117,13 @@ module ActiveFields
|
|
40
117
|
# { "name" => "boolean", "_destroy" => true }, # destroy (string keys)
|
41
118
|
# permitted_params, # params could be passed, but they must be permitted
|
42
119
|
# ]
|
120
|
+
#
|
121
|
+
# customizable.active_fields_attributes = {
|
122
|
+
# "0" => { name: "integer_array", value: [1, 4, 5, 5, 0] }, # create or update (symbol keys)
|
123
|
+
# "1" => { "name" => "text", "value" => "Lasso" }, # create or update (string keys)
|
124
|
+
# "2" => { name: "date", _destroy: true }, # destroy (symbol keys)
|
125
|
+
# "3" => { "name" => "boolean", "_destroy" => true }, # destroy (string keys)
|
126
|
+
# }
|
43
127
|
def active_fields_attributes=(attributes)
|
44
128
|
attributes = attributes.to_h if attributes.respond_to?(:permitted?)
|
45
129
|
|
@@ -6,13 +6,11 @@ module ActiveFields
|
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
8
|
included do
|
9
|
-
# rubocop:disable Rails/ReflectionClassName
|
10
9
|
has_many :active_values,
|
11
10
|
class_name: ActiveFields.config.value_class_name,
|
12
11
|
foreign_key: :active_field_id,
|
13
12
|
inverse_of: :active_field,
|
14
13
|
dependent: :destroy
|
15
|
-
# rubocop:enable Rails/ReflectionClassName
|
16
14
|
|
17
15
|
scope :for, ->(customizable_type) { where(customizable_type: customizable_type) }
|
18
16
|
|
@@ -26,7 +24,7 @@ module ActiveFields
|
|
26
24
|
end
|
27
25
|
|
28
26
|
class_methods do
|
29
|
-
def acts_as_active_field(array: false, validator:, caster:)
|
27
|
+
def acts_as_active_field(array: false, validator:, caster:, finder: {})
|
30
28
|
include FieldArrayConcern if array
|
31
29
|
|
32
30
|
define_method(:array?) { array }
|
@@ -62,6 +60,14 @@ module ActiveFields
|
|
62
60
|
end
|
63
61
|
value_caster_class.new(**options)
|
64
62
|
end
|
63
|
+
|
64
|
+
define_method(:value_finder_class) do
|
65
|
+
@value_finder_class ||= finder[:class_name]&.constantize
|
66
|
+
end
|
67
|
+
|
68
|
+
define_method(:value_finder) do
|
69
|
+
value_finder_class&.new(active_field: self)
|
70
|
+
end
|
65
71
|
end
|
66
72
|
end
|
67
73
|
|
@@ -81,6 +87,21 @@ module ActiveFields
|
|
81
87
|
ActiveFields.config.fields.key(type)
|
82
88
|
end
|
83
89
|
|
90
|
+
# Returns customizable types that allow this field type.
|
91
|
+
#
|
92
|
+
# Notes:
|
93
|
+
# - The customizable model must be loaded to appear in this list.
|
94
|
+
# Relationships between customizable models and field types are established in the `has_active_fields` method,
|
95
|
+
# which is typically called within the customizable model.
|
96
|
+
# If eager loading is enabled, there should be no issues.
|
97
|
+
# However, if eager loading is disabled (common in development),
|
98
|
+
# the list will remain incomplete until all customizable models are loaded.
|
99
|
+
# - Code reloading may behave incorrectly at times.
|
100
|
+
# Restart your application if you make changes to the allowed types list in `has_active_fields`.
|
101
|
+
def available_customizable_types
|
102
|
+
ActiveFields.registry.customizable_types_for(type_name).to_a
|
103
|
+
end
|
104
|
+
|
84
105
|
private
|
85
106
|
|
86
107
|
def validate_default_value
|
@@ -99,8 +120,8 @@ module ActiveFields
|
|
99
120
|
end
|
100
121
|
|
101
122
|
def validate_customizable_model_allows_type
|
102
|
-
|
103
|
-
return true if
|
123
|
+
allowed_type_names = ActiveFields.registry.field_type_names_for(customizable_type).to_a
|
124
|
+
return true if allowed_type_names.include?(type_name)
|
104
125
|
|
105
126
|
errors.add(:customizable_type, :inclusion)
|
106
127
|
false
|
@@ -7,12 +7,10 @@ module ActiveFields
|
|
7
7
|
|
8
8
|
included do
|
9
9
|
belongs_to :customizable, polymorphic: true, optional: false, inverse_of: :active_values
|
10
|
-
# rubocop:disable Rails/ReflectionClassName
|
11
10
|
belongs_to :active_field,
|
12
11
|
class_name: ActiveFields.config.field_base_class_name,
|
13
12
|
optional: false,
|
14
13
|
inverse_of: :active_values
|
15
|
-
# rubocop:enable Rails/ReflectionClassName
|
16
14
|
|
17
15
|
validates :active_field, uniqueness: { scope: :customizable }
|
18
16
|
validate :validate_value
|
@@ -3,8 +3,6 @@
|
|
3
3
|
module ActiveFields
|
4
4
|
module Casters
|
5
5
|
class DateTimeCaster < BaseCaster
|
6
|
-
MAX_PRECISION = RUBY_VERSION >= "3.2" ? 9 : 6 # AR max precision is 6 for old Rubies
|
7
|
-
|
8
6
|
def serialize(value)
|
9
7
|
value = value.iso8601 if value.is_a?(Date)
|
10
8
|
casted_value = caster.serialize(value)
|
@@ -28,7 +26,7 @@ module ActiveFields
|
|
28
26
|
# Use maximum precision by default to prevent the caster from truncating useful time information
|
29
27
|
# before precision is applied later
|
30
28
|
def precision
|
31
|
-
[options[:precision],
|
29
|
+
[options[:precision], ActiveFields::MAX_DATETIME_PRECISION].compact.min
|
32
30
|
end
|
33
31
|
|
34
32
|
def apply_precision(value)
|
@@ -15,14 +15,11 @@ module ActiveFields
|
|
15
15
|
private
|
16
16
|
|
17
17
|
def cast(value)
|
18
|
-
|
19
|
-
casted = casted.truncate(precision) if casted && precision
|
20
|
-
|
21
|
-
casted
|
18
|
+
BigDecimal(value, 0, exception: false)&.truncate(precision)
|
22
19
|
end
|
23
20
|
|
24
21
|
def precision
|
25
|
-
options[:precision]
|
22
|
+
[options[:precision], ActiveFields::MAX_DECIMAL_PRECISION].compact.min
|
26
23
|
end
|
27
24
|
end
|
28
25
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveFields
|
4
|
+
# Ruby supports up to 9 fractional seconds, but PostgreSQL, like most databases, supports only 6.
|
5
|
+
# Since we use PostgreSQL, we standardize on 6.
|
6
|
+
MAX_DATETIME_PRECISION = 6
|
7
|
+
|
8
|
+
# Ruby's BigDecimal class allows extremely high precision,
|
9
|
+
# but PostgreSQL supports a maximum of 16383 digits after the decimal point.
|
10
|
+
# Since we use PostgreSQL, we limit decimal precision to 16383.
|
11
|
+
MAX_DECIMAL_PRECISION = 2**14 - 1
|
12
|
+
|
13
|
+
OPS = {
|
14
|
+
eq: :"=",
|
15
|
+
not_eq: :"!=",
|
16
|
+
gt: :">",
|
17
|
+
gteq: :">=",
|
18
|
+
lt: :"<",
|
19
|
+
lteq: :"<=",
|
20
|
+
|
21
|
+
start_with: :"^",
|
22
|
+
end_with: :"$",
|
23
|
+
contain: :"~",
|
24
|
+
not_start_with: :"!^",
|
25
|
+
not_end_with: :"!$",
|
26
|
+
not_contain: :"!~",
|
27
|
+
istart_with: :"^*",
|
28
|
+
iend_with: :"$*",
|
29
|
+
icontain: :"~*",
|
30
|
+
not_istart_with: :"!^*",
|
31
|
+
not_iend_with: :"!$*",
|
32
|
+
not_icontain: :"!~*",
|
33
|
+
|
34
|
+
include: :"|=",
|
35
|
+
not_include: :"!|=",
|
36
|
+
any_gt: :"|>",
|
37
|
+
any_gteq: :"|>=",
|
38
|
+
any_lt: :"|<",
|
39
|
+
any_lteq: :"|<=",
|
40
|
+
all_gt: :"&>",
|
41
|
+
all_gteq: :"&>=",
|
42
|
+
all_lt: :"&<",
|
43
|
+
all_lteq: :"&<=",
|
44
|
+
|
45
|
+
size_eq: :"#=",
|
46
|
+
size_not_eq: :"#!=",
|
47
|
+
size_gt: :"#>",
|
48
|
+
size_gteq: :"#>=",
|
49
|
+
size_lt: :"#<",
|
50
|
+
size_lteq: :"#<=",
|
51
|
+
|
52
|
+
any_start_with: :"|^",
|
53
|
+
all_start_with: :"&^",
|
54
|
+
}.freeze
|
55
|
+
end
|
data/lib/active_fields/engine.rb
CHANGED
@@ -8,7 +8,8 @@ module ActiveFields
|
|
8
8
|
|
9
9
|
# Disable models reloading to avoid STI issues.
|
10
10
|
# Reloading can prevent subclasses from recognizing the base class.
|
11
|
-
config.autoload_once_paths
|
11
|
+
config.autoload_once_paths << "#{root}/app/models"
|
12
|
+
config.autoload_once_paths << "#{root}/app/models/concerns"
|
12
13
|
|
13
14
|
initializer "active_fields.active_record" do
|
14
15
|
ActiveSupport.on_load(:active_record) do
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveFields
|
4
|
+
module Finders
|
5
|
+
class ArrayFinder < BaseFinder
|
6
|
+
private
|
7
|
+
|
8
|
+
# This method defines a JSONPath query for searching within a `jsonb` field.
|
9
|
+
# It must be implemented in a subclass and include a `$value` parameter,
|
10
|
+
# which will be replaced with the provided value during search operations.
|
11
|
+
# Refer to descendant implementations for examples.
|
12
|
+
# For more details, see the PostgreSQL documentation: https://www.postgresql.org/docs/current/functions-json.html
|
13
|
+
def jsonpath(operator) = raise NotImplementedError
|
14
|
+
|
15
|
+
# Arel query node that searches for records where any element matches the JSONPath
|
16
|
+
def value_match_any(operator, value)
|
17
|
+
jsonb_path_exists(
|
18
|
+
value_field_jsonb,
|
19
|
+
jsonpath(operator),
|
20
|
+
{ value: value },
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Arel query node that searches for records where all elements match the JSONPath
|
25
|
+
def value_match_all(operator, value)
|
26
|
+
jsonb_array_length(value_field_jsonb).gt(0)
|
27
|
+
.and(
|
28
|
+
jsonb_array_length(value_field_jsonb).eq(
|
29
|
+
jsonb_array_length(
|
30
|
+
jsonb_path_query_array(
|
31
|
+
value_field_jsonb,
|
32
|
+
jsonpath(operator),
|
33
|
+
{ value: value },
|
34
|
+
),
|
35
|
+
),
|
36
|
+
),
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Arel query node that searches for records where the array size is equal to the provided value
|
41
|
+
def value_size_eq(value)
|
42
|
+
value = cast_int(value)
|
43
|
+
jsonb_array_length(value_field_jsonb).eq(value)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Arel query node that searches for records where the array size is not equal to the provided value
|
47
|
+
def value_size_not_eq(value)
|
48
|
+
value = cast_int(value)
|
49
|
+
jsonb_array_length(value_field_jsonb).not_eq(value)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Arel query node that searches for records where the array size is greater than the provided value
|
53
|
+
def value_size_gt(value)
|
54
|
+
value = cast_int(value)
|
55
|
+
jsonb_array_length(value_field_jsonb).gt(value)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Arel query node that searches for records where the array size is greater than or equal to the provided value
|
59
|
+
def value_size_gteq(value)
|
60
|
+
value = cast_int(value)
|
61
|
+
jsonb_array_length(value_field_jsonb).gteq(value)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Arel query node that searches for records where the array size is less than the provided value
|
65
|
+
def value_size_lt(value)
|
66
|
+
value = cast_int(value)
|
67
|
+
jsonb_array_length(value_field_jsonb).lt(value)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Arel query node that searches for records where the array size is less than or equal to the provided value
|
71
|
+
def value_size_lteq(value)
|
72
|
+
value = cast_int(value)
|
73
|
+
jsonb_array_length(value_field_jsonb).lteq(value)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Casts given value to integer, useful for querying by array size
|
77
|
+
def cast_int(value)
|
78
|
+
Casters::IntegerCaster.new.deserialize(value)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Arel node for `active_fields.value_meta->const`
|
82
|
+
def value_field_jsonb
|
83
|
+
Arel::Nodes::InfixOperation.new(
|
84
|
+
"->",
|
85
|
+
Arel::Table.new(cte_name)[:value_meta],
|
86
|
+
Arel::Nodes.build_quoted("const"),
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Arel node for `jsonb_path_exists` PostgreSQL function
|
91
|
+
def jsonb_path_exists(target, jsonpath, vars = nil)
|
92
|
+
Arel::Nodes::NamedFunction.new(
|
93
|
+
"jsonb_path_exists",
|
94
|
+
[target, *[jsonpath, vars&.to_json].compact.map { Arel::Nodes.build_quoted(_1) }],
|
95
|
+
)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Arel node for `jsonb_path_query_array` PostgreSQL function
|
99
|
+
def jsonb_path_query_array(target, jsonpath, vars = nil)
|
100
|
+
Arel::Nodes::NamedFunction.new(
|
101
|
+
"jsonb_path_query_array",
|
102
|
+
[target, *[jsonpath, vars&.to_json].compact.map { Arel::Nodes.build_quoted(_1) }],
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Arel node for `jsonb_array_length` PostgreSQL function
|
107
|
+
def jsonb_array_length(target)
|
108
|
+
Arel::Nodes::NamedFunction.new("jsonb_array_length", [target])
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|