active_fields 1.1.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 +27 -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 -3
- 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 +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.
|
@@ -15,16 +15,95 @@ module ActiveFields
|
|
15
15
|
dependent: :destroy
|
16
16
|
# rubocop:enable Rails/ReflectionClassName
|
17
17
|
|
18
|
+
# Searches customizables by active_values.
|
19
|
+
#
|
20
|
+
# Accepts an Array of Hashes (symbol/string keys),
|
21
|
+
# a Hash of Hashes generated from HTTP/HTML parameters
|
22
|
+
# or permitted params.
|
23
|
+
# Each element should contain:
|
24
|
+
# - <tt>:n</tt> or <tt>:name</tt> key matching the active_field record name;
|
25
|
+
# - <tt>:op</tt> or <tt>:operator</tt> key specifying search operation or operator;
|
26
|
+
# - <tt>:v</tt> or <tt>:value</tt> key specifying search value.
|
27
|
+
#
|
28
|
+
# Example:
|
29
|
+
#
|
30
|
+
# # Array of hashes
|
31
|
+
# CustomizableModel.where_active_fields(
|
32
|
+
# [
|
33
|
+
# { name: "integer_array", operator: "any_gteq", value: 5 }, # symbol keys
|
34
|
+
# { "name" => "text", operator: "=", "value" => "Lasso" }, # string keys
|
35
|
+
# { n: "boolean", op: "!=", v: false }, # compact form (string or symbol keys)
|
36
|
+
# ],
|
37
|
+
# )
|
38
|
+
#
|
39
|
+
# # Hash of hashes generated from HTTP/HTML parameters
|
40
|
+
# CustomizableModel.where_active_fields(
|
41
|
+
# {
|
42
|
+
# "0" => { name: "integer_array", operator: "any_gteq", value: 5 },
|
43
|
+
# "1" => { "name" => "text", operator: "=", "value" => "Lasso" },
|
44
|
+
# "2" => { n: "boolean", op: "!=", v: false },
|
45
|
+
# },
|
46
|
+
# )
|
47
|
+
#
|
48
|
+
# # Params (must be permitted)
|
49
|
+
# CustomizableModel.where_active_fields(permitted_params)
|
50
|
+
scope :where_active_fields, ->(filters) do
|
51
|
+
filters = filters.to_h if filters.respond_to?(:permitted?)
|
52
|
+
|
53
|
+
unless filters.is_a?(Array) || filters.is_a?(Hash)
|
54
|
+
raise ArgumentError, "Hash or Array expected for `where_active_fields`, got #{filters.class.name}"
|
55
|
+
end
|
56
|
+
|
57
|
+
# Handle `fields_for` params
|
58
|
+
filters = filters.values if filters.is_a?(Hash)
|
59
|
+
|
60
|
+
active_fields_by_name = active_fields.index_by(&:name)
|
61
|
+
|
62
|
+
filters.inject(self) do |scope, filter|
|
63
|
+
filter = filter.to_h if filter.respond_to?(:permitted?)
|
64
|
+
filter = filter.with_indifferent_access
|
65
|
+
|
66
|
+
active_field = active_fields_by_name[filter[:n] || filter[:name]]
|
67
|
+
next scope if active_field.nil?
|
68
|
+
next scope if active_field.value_finder.nil?
|
69
|
+
|
70
|
+
active_values = active_field.value_finder.search(
|
71
|
+
op: filter[:op] || filter[:operator],
|
72
|
+
value: filter[:v] || filter[:value],
|
73
|
+
)
|
74
|
+
next scope if active_values.nil?
|
75
|
+
|
76
|
+
scope.where(id: active_values.select(:customizable_id))
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
18
80
|
accepts_nested_attributes_for :active_values, allow_destroy: true
|
19
81
|
end
|
20
82
|
|
21
|
-
|
22
|
-
|
83
|
+
class_methods do
|
84
|
+
# Collection of active fields registered for this customizable
|
85
|
+
def active_fields
|
86
|
+
ActiveFields.config.field_base_class.for(name)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns active fields type names allowed for this customizable model.
|
90
|
+
def allowed_active_fields_type_names
|
91
|
+
ActiveFields.registry.field_type_names_for(name).to_a
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns active fields class names allowed for this customizable model.
|
95
|
+
def allowed_active_fields_class_names
|
96
|
+
ActiveFields.config.fields.values_at(*allowed_active_fields_type_names)
|
97
|
+
end
|
23
98
|
end
|
24
99
|
|
100
|
+
delegate :active_fields, to: :class
|
101
|
+
|
25
102
|
# Assigns the given attributes to the active_values association.
|
26
103
|
#
|
27
|
-
# Accepts an Array of Hashes (symbol/string keys)
|
104
|
+
# Accepts an Array of Hashes (symbol/string keys),
|
105
|
+
# a Hash of Hashes generated from HTTP/HTML parameters
|
106
|
+
# or permitted params.
|
28
107
|
# Each element should contain a <tt>:name</tt> key matching an existing active_field record.
|
29
108
|
# Element with a <tt>:value</tt> key will create an active_value if it doesn't exist
|
30
109
|
# or update an existing active_value, with the provided value.
|
@@ -40,6 +119,13 @@ module ActiveFields
|
|
40
119
|
# { "name" => "boolean", "_destroy" => true }, # destroy (string keys)
|
41
120
|
# permitted_params, # params could be passed, but they must be permitted
|
42
121
|
# ]
|
122
|
+
#
|
123
|
+
# customizable.active_fields_attributes = {
|
124
|
+
# "0" => { name: "integer_array", value: [1, 4, 5, 5, 0] }, # create or update (symbol keys)
|
125
|
+
# "1" => { "name" => "text", "value" => "Lasso" }, # create or update (string keys)
|
126
|
+
# "2" => { name: "date", _destroy: true }, # destroy (symbol keys)
|
127
|
+
# "3" => { "name" => "boolean", "_destroy" => true }, # destroy (string keys)
|
128
|
+
# }
|
43
129
|
def active_fields_attributes=(attributes)
|
44
130
|
attributes = attributes.to_h if attributes.respond_to?(:permitted?)
|
45
131
|
|
@@ -26,7 +26,7 @@ module ActiveFields
|
|
26
26
|
end
|
27
27
|
|
28
28
|
class_methods do
|
29
|
-
def acts_as_active_field(array: false, validator:, caster:)
|
29
|
+
def acts_as_active_field(array: false, validator:, caster:, finder: {})
|
30
30
|
include FieldArrayConcern if array
|
31
31
|
|
32
32
|
define_method(:array?) { array }
|
@@ -62,6 +62,14 @@ module ActiveFields
|
|
62
62
|
end
|
63
63
|
value_caster_class.new(**options)
|
64
64
|
end
|
65
|
+
|
66
|
+
define_method(:value_finder_class) do
|
67
|
+
@value_finder_class ||= finder[:class_name]&.constantize
|
68
|
+
end
|
69
|
+
|
70
|
+
define_method(:value_finder) do
|
71
|
+
value_finder_class&.new(active_field: self)
|
72
|
+
end
|
65
73
|
end
|
66
74
|
end
|
67
75
|
|
@@ -81,6 +89,21 @@ module ActiveFields
|
|
81
89
|
ActiveFields.config.fields.key(type)
|
82
90
|
end
|
83
91
|
|
92
|
+
# Returns customizable types that allow this field type.
|
93
|
+
#
|
94
|
+
# Notes:
|
95
|
+
# - The customizable model must be loaded to appear in this list.
|
96
|
+
# Relationships between customizable models and field types are established in the `has_active_fields` method,
|
97
|
+
# which is typically called within the customizable model.
|
98
|
+
# If eager loading is enabled, there should be no issues.
|
99
|
+
# However, if eager loading is disabled (common in development),
|
100
|
+
# the list will remain incomplete until all customizable models are loaded.
|
101
|
+
# - Code reloading may behave incorrectly at times.
|
102
|
+
# Restart your application if you make changes to the allowed types list in `has_active_fields`.
|
103
|
+
def available_customizable_types
|
104
|
+
ActiveFields.registry.customizable_types_for(type_name).to_a
|
105
|
+
end
|
106
|
+
|
84
107
|
private
|
85
108
|
|
86
109
|
def validate_default_value
|
@@ -99,8 +122,8 @@ module ActiveFields
|
|
99
122
|
end
|
100
123
|
|
101
124
|
def validate_customizable_model_allows_type
|
102
|
-
|
103
|
-
return true if
|
125
|
+
allowed_type_names = ActiveFields.registry.field_type_names_for(customizable_type).to_a
|
126
|
+
return true if allowed_type_names.include?(type_name)
|
104
127
|
|
105
128
|
errors.add(:customizable_type, :inclusion)
|
106
129
|
false
|
@@ -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
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveFields
|
4
|
+
module Finders
|
5
|
+
class BaseFinder
|
6
|
+
class << self
|
7
|
+
# Define search operation
|
8
|
+
def operation(name, operator:, &block)
|
9
|
+
name = name.to_sym
|
10
|
+
operator = operator.to_sym
|
11
|
+
|
12
|
+
__operations__[name] = {
|
13
|
+
operator: operator.to_sym,
|
14
|
+
block: block,
|
15
|
+
}
|
16
|
+
__operators__[operator] = name
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns operator for provided search operation name
|
20
|
+
def operator_for(operation_name)
|
21
|
+
__operations__.dig(operation_name.to_sym, :operator)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns search operation name for provided operator
|
25
|
+
def operation_for(operator)
|
26
|
+
__operators__[operator.to_sym]
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns all defined search operations names
|
30
|
+
def operations
|
31
|
+
__operations__.keys
|
32
|
+
end
|
33
|
+
|
34
|
+
# Storage for defined operations. Private.
|
35
|
+
def __operations__
|
36
|
+
@__operations__ ||= {}
|
37
|
+
end
|
38
|
+
|
39
|
+
# Index for finding operation by operator. Private.
|
40
|
+
def __operators__
|
41
|
+
@__operators__ ||= {}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
attr_reader :active_field
|
46
|
+
|
47
|
+
def initialize(active_field:)
|
48
|
+
@active_field = active_field
|
49
|
+
end
|
50
|
+
|
51
|
+
# Perform query operation
|
52
|
+
# @param op [String, Symbol] Operation name or operator
|
53
|
+
# @param value [Any] The value to search for
|
54
|
+
def search(op:, value:)
|
55
|
+
op = op.to_sym
|
56
|
+
operation = self.class.__operations__.key?(op) ? op : self.class.__operators__[op]
|
57
|
+
return if operation.nil?
|
58
|
+
|
59
|
+
instance_exec(value, &self.class.__operations__[operation][:block])
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# Name of the CTE, that is used in queries. It is the original values table name.
|
65
|
+
def cte_name = ActiveFields.config.value_class.table_name
|
66
|
+
|
67
|
+
# Base scope for querying
|
68
|
+
def scope
|
69
|
+
ActiveFields.config.value_class.with(cte_name => active_field.active_values)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveFields
|
4
|
+
module Finders
|
5
|
+
class BooleanFinder < SingularFinder
|
6
|
+
operation :eq, operator: OPS[:eq] do |value|
|
7
|
+
scope.where(eq(casted_value_field("boolean"), cast(value)))
|
8
|
+
end
|
9
|
+
operation :not_eq, operator: OPS[:not_eq] do |value|
|
10
|
+
scope.where(not_eq(casted_value_field("boolean"), cast(value)))
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def cast(value)
|
16
|
+
Casters::BooleanCaster.new.deserialize(value)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|