actionset 0.8.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +5 -5
- data/.ruby-version +1 -1
- data/.travis.yml +1 -1
- data/CHANGELOG +67 -0
- data/Gemfile.lock +134 -126
- data/README.md +26 -0
- data/Rakefile +8 -1
- data/actionset.gemspec +2 -2
- data/lib/action_set.rb +8 -23
- data/lib/action_set/attribute_value.rb +7 -1
- data/lib/action_set/filter_instructions.rb +72 -0
- data/lib/action_set/helpers/helper_methods.rb +2 -1
- data/lib/action_set/helpers/pagination/record_description_for_helper.rb +20 -0
- data/lib/action_set/helpers/pagination/record_first_for_helper.rb +20 -0
- data/lib/action_set/helpers/pagination/record_last_for_helper.rb +26 -0
- data/lib/action_set/helpers/pagination/record_range_for_helper.rb +25 -0
- data/lib/action_set/helpers/pagination/record_size_for_helper.rb +9 -0
- data/lib/action_set/helpers/pagination/total_pages_for_helper.rb +3 -1
- data/lib/action_set/sort_instructions.rb +44 -0
- data/lib/active_set.rb +25 -2
- data/lib/active_set/active_record_set_instruction.rb +33 -32
- data/lib/active_set/attribute_instruction.rb +3 -3
- data/lib/active_set/enumerable_set_instruction.rb +13 -24
- data/lib/active_set/filtering/active_record/operators.rb +280 -0
- data/lib/active_set/filtering/active_record/query_column.rb +35 -0
- data/lib/active_set/filtering/active_record/query_value.rb +47 -0
- data/lib/active_set/filtering/active_record/set_instruction.rb +29 -0
- data/lib/active_set/filtering/active_record/strategy.rb +87 -0
- data/lib/active_set/filtering/constants.rb +349 -0
- data/lib/active_set/filtering/enumerable/operators.rb +308 -0
- data/lib/active_set/filtering/enumerable/set_instruction.rb +98 -0
- data/lib/active_set/filtering/enumerable/strategy.rb +90 -0
- data/lib/active_set/filtering/operation.rb +5 -8
- data/lib/active_set/paginating/active_record_strategy.rb +0 -2
- data/lib/active_set/sorting/active_record_strategy.rb +27 -3
- data/lib/active_set/sorting/enumerable_strategy.rb +12 -2
- data/lib/active_set/sorting/operation.rb +1 -2
- data/lib/helpers/flatten_keys_of.rb +53 -0
- data/lib/helpers/transform_to_sortable_numeric.rb +3 -3
- metadata +26 -13
- data/lib/active_set/filtering/active_record_strategy.rb +0 -85
- data/lib/active_set/filtering/enumerable_strategy.rb +0 -79
- data/lib/helpers/throws.rb +0 -19
- data/lib/patches/core_ext/hash/flatten_keys.rb +0 -58
@@ -9,15 +9,37 @@ class ActiveSet
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def initial_relation
|
12
|
-
return @
|
12
|
+
return @initial_relation if defined? @initial_relation
|
13
13
|
|
14
|
-
@
|
14
|
+
@initial_relation = if @attribute_instruction.associations_array.empty?
|
15
|
+
@set
|
16
|
+
else
|
17
|
+
@set.eager_load(@attribute_instruction.associations_hash)
|
18
|
+
end
|
15
19
|
end
|
16
20
|
|
17
|
-
def
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
+
def arel_column
|
22
|
+
return @arel_column if defined? @arel_column
|
23
|
+
|
24
|
+
arel_column = arel_table[@attribute_instruction.attribute]
|
25
|
+
arel_column = arel_column.lower if case_insensitive_operation?
|
26
|
+
|
27
|
+
@arel_column = arel_column
|
28
|
+
end
|
29
|
+
|
30
|
+
def arel_column_name
|
31
|
+
arel_table[@attribute_instruction.attribute].name
|
32
|
+
end
|
33
|
+
|
34
|
+
def attribute_model
|
35
|
+
return @set.klass if @attribute_instruction.associations_array.empty?
|
36
|
+
return @attribute_model if defined? @attribute_model
|
37
|
+
|
38
|
+
@attribute_model = @attribute_instruction
|
39
|
+
.associations_array
|
40
|
+
.reduce(@set) do |obj, assoc|
|
41
|
+
obj.reflections[assoc.to_s]&.klass
|
42
|
+
end
|
21
43
|
end
|
22
44
|
|
23
45
|
def arel_table
|
@@ -31,37 +53,16 @@ class ActiveSet
|
|
31
53
|
end
|
32
54
|
end
|
33
55
|
|
34
|
-
|
35
|
-
_arel_column = arel_table[@attribute_instruction.attribute]
|
36
|
-
return _arel_column.lower if case_insensitive_operation?
|
56
|
+
private
|
37
57
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
@attribute_instruction.operator(default: :eq)
|
43
|
-
end
|
44
|
-
|
45
|
-
def arel_value
|
46
|
-
_arel_value = @attribute_instruction.value
|
47
|
-
return _arel_value.downcase if case_insensitive_operation?
|
48
|
-
|
49
|
-
_arel_value
|
58
|
+
def arel_type
|
59
|
+
attribute_model
|
60
|
+
&.columns_hash[@attribute_instruction.attribute]
|
61
|
+
&.type
|
50
62
|
end
|
51
63
|
|
52
64
|
def case_insensitive_operation?
|
53
65
|
@attribute_instruction.case_insensitive? && arel_type.presence_in(%i[string text])
|
54
66
|
end
|
55
|
-
|
56
|
-
def attribute_model
|
57
|
-
return @set.klass if @attribute_instruction.associations_array.empty?
|
58
|
-
return @attribute_model if defined? @attribute_model
|
59
|
-
|
60
|
-
@attribute_model = @attribute_instruction
|
61
|
-
.associations_array
|
62
|
-
.reduce(@set) do |obj, assoc|
|
63
|
-
obj.reflections[assoc.to_s]&.klass
|
64
|
-
end
|
65
|
-
end
|
66
67
|
end
|
67
68
|
end
|
@@ -32,11 +32,11 @@ class ActiveSet
|
|
32
32
|
@attribute = attribute
|
33
33
|
end
|
34
34
|
|
35
|
-
def operator
|
35
|
+
def operator
|
36
36
|
return @operator if defined? @operator
|
37
37
|
|
38
38
|
attribute_instruction = @keypath.last
|
39
|
-
@operator =
|
39
|
+
@operator = attribute_instruction[operator_regex, 1]&.to_sym
|
40
40
|
end
|
41
41
|
|
42
42
|
def options
|
@@ -92,7 +92,7 @@ class ActiveSet
|
|
92
92
|
private
|
93
93
|
|
94
94
|
def operator_regex
|
95
|
-
|
95
|
+
/\((.*?)\)/
|
96
96
|
end
|
97
97
|
|
98
98
|
def options_regex
|
@@ -9,16 +9,21 @@ class ActiveSet
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def attribute_value_for(item)
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
12
|
+
@item_values ||= Hash.new do |h, key|
|
13
|
+
item_value = @attribute_instruction.value_for(item: key)
|
14
|
+
item_value = item_value.downcase if case_insensitive_operation_for?(item_value)
|
15
|
+
h[key] = item_value
|
16
|
+
end
|
17
|
+
|
18
|
+
@item_values[item]
|
16
19
|
end
|
17
20
|
|
18
|
-
def
|
19
|
-
|
20
|
-
|
21
|
-
|
21
|
+
def instruction_value
|
22
|
+
return @instruction_value if defined? @instruction_value
|
23
|
+
|
24
|
+
instruction_value = @attribute_instruction.value
|
25
|
+
instruction_value = instruction_value.downcase if case_insensitive_operation_for?(instruction_value)
|
26
|
+
@instruction_value = instruction_value
|
22
27
|
end
|
23
28
|
|
24
29
|
def case_insensitive_operation_for?(value)
|
@@ -26,21 +31,5 @@ class ActiveSet
|
|
26
31
|
|
27
32
|
value.is_a?(String) || value.is_a?(Symbol)
|
28
33
|
end
|
29
|
-
|
30
|
-
def attribute_instance
|
31
|
-
set_item = @set.find(&:present?)
|
32
|
-
return set_item if @attribute_instruction.associations_array.empty?
|
33
|
-
return @attribute_model if defined? @attribute_model
|
34
|
-
|
35
|
-
@attribute_model = @attribute_instruction
|
36
|
-
.associations_array
|
37
|
-
.reduce(set_item) do |obj, assoc|
|
38
|
-
obj.public_send(assoc)
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
def attribute_class
|
43
|
-
attribute_instance&.class
|
44
|
-
end
|
45
34
|
end
|
46
35
|
end
|
@@ -0,0 +1,280 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../constants'
|
4
|
+
|
5
|
+
class ActiveSet
|
6
|
+
module Filtering
|
7
|
+
module ActiveRecord
|
8
|
+
# rubocop:disable Metrics/ModuleLength
|
9
|
+
module Operators
|
10
|
+
RANGE_TRANSFORMER = proc do |raw:, sql:, type:|
|
11
|
+
if type.presence_in %i[boolean]
|
12
|
+
Range.new(*sql.sort)
|
13
|
+
else
|
14
|
+
Range.new(*raw.sort)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
BLANK_TRANSFORMER = proc do |type:, **_ctx|
|
18
|
+
if type.presence_in %i[date float integer time datetime boolean]
|
19
|
+
[nil]
|
20
|
+
else
|
21
|
+
Constants::BLANK_VALUES
|
22
|
+
end
|
23
|
+
end
|
24
|
+
MATCHER_TRANSFORMER = proc do |sql:, type:, **ctx|
|
25
|
+
next sql.map { |str| MATCHER_TRANSFORMER.call(sql: str, type: type, **ctx) } if sql.respond_to?(:map)
|
26
|
+
|
27
|
+
next sql if type != :decimal
|
28
|
+
next sql[0..-3] if sql.ends_with?('.0')
|
29
|
+
next sql[0..-4] + '%' if sql.ends_with?('.0%')
|
30
|
+
|
31
|
+
sql
|
32
|
+
end
|
33
|
+
START_MATCHER_TRANSFORMER = proc do |sql:, type:, **ctx|
|
34
|
+
next sql.map { |str| START_MATCHER_TRANSFORMER.call(sql: str, type: type, **ctx) } if sql.respond_to?(:map)
|
35
|
+
|
36
|
+
str = MATCHER_TRANSFORMER.call(sql: sql, type: type, **ctx)
|
37
|
+
next str if ['boolean'].include? type.to_s
|
38
|
+
|
39
|
+
str + '%'
|
40
|
+
end
|
41
|
+
END_MATCHER_TRANSFORMER = proc do |sql:, type:, **ctx|
|
42
|
+
next sql.map { |str| END_MATCHER_TRANSFORMER.call(sql: str, type: type, **ctx) } if sql.respond_to?(:map)
|
43
|
+
|
44
|
+
str = MATCHER_TRANSFORMER.call(sql: sql, type: type, **ctx)
|
45
|
+
next str if ['boolean'].include? type.to_s
|
46
|
+
|
47
|
+
'%' + str
|
48
|
+
end
|
49
|
+
CONTAIN_MATCHER_TRANSFORMER = proc do |sql:, type:, **ctx|
|
50
|
+
next sql.map { |str| CONTAIN_MATCHER_TRANSFORMER.call(sql: str, type: type, **ctx) } if sql.respond_to?(:map)
|
51
|
+
|
52
|
+
str = MATCHER_TRANSFORMER.call(sql: sql, type: type, **ctx)
|
53
|
+
next str if ['boolean'].include? type.to_s
|
54
|
+
|
55
|
+
'%' + str + '%'
|
56
|
+
end
|
57
|
+
|
58
|
+
PREDICATES = {
|
59
|
+
EQ: {
|
60
|
+
operator: :eq
|
61
|
+
},
|
62
|
+
NOT_EQ: {
|
63
|
+
operator: :not_eq
|
64
|
+
},
|
65
|
+
EQ_ANY: {
|
66
|
+
operator: :eq_any
|
67
|
+
},
|
68
|
+
EQ_ALL: {
|
69
|
+
operator: :eq_all
|
70
|
+
},
|
71
|
+
NOT_EQ_ANY: {
|
72
|
+
operator: :not_eq_any
|
73
|
+
},
|
74
|
+
NOT_EQ_ALL: {
|
75
|
+
operator: :not_eq_all
|
76
|
+
},
|
77
|
+
|
78
|
+
IN: {
|
79
|
+
operator: :in
|
80
|
+
},
|
81
|
+
NOT_IN: {
|
82
|
+
operator: :not_in
|
83
|
+
},
|
84
|
+
IN_ANY: {
|
85
|
+
operator: :in_any
|
86
|
+
},
|
87
|
+
IN_ALL: {
|
88
|
+
operator: :in_all
|
89
|
+
},
|
90
|
+
NOT_IN_ANY: {
|
91
|
+
operator: :not_in_any
|
92
|
+
},
|
93
|
+
NOT_IN_ALL: {
|
94
|
+
operator: :not_in_all
|
95
|
+
},
|
96
|
+
|
97
|
+
MATCHES: {
|
98
|
+
operator: :matches,
|
99
|
+
query_attribute_transformer: MATCHER_TRANSFORMER
|
100
|
+
},
|
101
|
+
DOES_NOT_MATCH: {
|
102
|
+
operator: :does_not_match,
|
103
|
+
query_attribute_transformer: MATCHER_TRANSFORMER
|
104
|
+
},
|
105
|
+
MATCHES_ANY: {
|
106
|
+
operator: :matches_any,
|
107
|
+
query_attribute_transformer: MATCHER_TRANSFORMER
|
108
|
+
},
|
109
|
+
MATCHES_ALL: {
|
110
|
+
operator: :matches_all,
|
111
|
+
query_attribute_transformer: MATCHER_TRANSFORMER
|
112
|
+
},
|
113
|
+
DOES_NOT_MATCH_ANY: {
|
114
|
+
operator: :does_not_match_any,
|
115
|
+
query_attribute_transformer: MATCHER_TRANSFORMER
|
116
|
+
},
|
117
|
+
DOES_NOT_MATCH_ALL: {
|
118
|
+
operator: :does_not_match_all,
|
119
|
+
query_attribute_transformer: MATCHER_TRANSFORMER
|
120
|
+
},
|
121
|
+
|
122
|
+
LT: {
|
123
|
+
operator: :lt
|
124
|
+
},
|
125
|
+
LTEQ: {
|
126
|
+
operator: :lteq
|
127
|
+
},
|
128
|
+
LT_ANY: {
|
129
|
+
operator: :lt_any
|
130
|
+
},
|
131
|
+
LT_ALL: {
|
132
|
+
operator: :lt_all
|
133
|
+
},
|
134
|
+
LTEQ_ANY: {
|
135
|
+
operator: :lteq_any
|
136
|
+
},
|
137
|
+
LTEQ_ALL: {
|
138
|
+
operator: :lteq_all
|
139
|
+
},
|
140
|
+
|
141
|
+
GT: {
|
142
|
+
operator: :gt
|
143
|
+
},
|
144
|
+
GTEQ: {
|
145
|
+
operator: :gteq
|
146
|
+
},
|
147
|
+
GT_ANY: {
|
148
|
+
operator: :gt_any
|
149
|
+
},
|
150
|
+
GT_ALL: {
|
151
|
+
operator: :gt_all
|
152
|
+
},
|
153
|
+
GTEQ_ANY: {
|
154
|
+
operator: :gteq_any
|
155
|
+
},
|
156
|
+
GTEQ_ALL: {
|
157
|
+
operator: :gteq_all
|
158
|
+
},
|
159
|
+
|
160
|
+
BETWEEN: {
|
161
|
+
operator: :between,
|
162
|
+
query_attribute_transformer: RANGE_TRANSFORMER
|
163
|
+
},
|
164
|
+
NOT_BETWEEN: {
|
165
|
+
operator: :not_between,
|
166
|
+
query_attribute_transformer: RANGE_TRANSFORMER
|
167
|
+
},
|
168
|
+
|
169
|
+
IS_TRUE: {
|
170
|
+
operator: :eq,
|
171
|
+
query_attribute_transformer: proc { |_| true }
|
172
|
+
},
|
173
|
+
IS_FALSE: {
|
174
|
+
operator: :eq,
|
175
|
+
query_attribute_transformer: proc { |_| false }
|
176
|
+
},
|
177
|
+
|
178
|
+
IS_NULL: {
|
179
|
+
operator: :eq
|
180
|
+
},
|
181
|
+
NOT_NULL: {
|
182
|
+
operator: :not_eq
|
183
|
+
},
|
184
|
+
|
185
|
+
IS_PRESENT: {
|
186
|
+
operator: :not_eq_all,
|
187
|
+
query_attribute_transformer: BLANK_TRANSFORMER
|
188
|
+
},
|
189
|
+
IS_BLANK: {
|
190
|
+
operator: :eq_any,
|
191
|
+
query_attribute_transformer: BLANK_TRANSFORMER
|
192
|
+
},
|
193
|
+
|
194
|
+
MATCH_START: {
|
195
|
+
operator: :matches,
|
196
|
+
query_attribute_transformer: START_MATCHER_TRANSFORMER
|
197
|
+
},
|
198
|
+
MATCH_START_ANY: {
|
199
|
+
operator: :matches_any,
|
200
|
+
query_attribute_transformer: START_MATCHER_TRANSFORMER
|
201
|
+
},
|
202
|
+
MATCH_START_ALL: {
|
203
|
+
operator: :matches_all,
|
204
|
+
query_attribute_transformer: START_MATCHER_TRANSFORMER
|
205
|
+
},
|
206
|
+
MATCH_NOT_START: {
|
207
|
+
operator: :does_not_match,
|
208
|
+
query_attribute_transformer: START_MATCHER_TRANSFORMER
|
209
|
+
},
|
210
|
+
MATCH_NOT_START_ANY: {
|
211
|
+
operator: :does_not_match_any,
|
212
|
+
query_attribute_transformer: START_MATCHER_TRANSFORMER
|
213
|
+
},
|
214
|
+
MATCH_NOT_START_ALL: {
|
215
|
+
operator: :does_not_match_all,
|
216
|
+
query_attribute_transformer: START_MATCHER_TRANSFORMER
|
217
|
+
},
|
218
|
+
MATCH_END: {
|
219
|
+
operator: :matches,
|
220
|
+
query_attribute_transformer: END_MATCHER_TRANSFORMER
|
221
|
+
},
|
222
|
+
MATCH_END_ANY: {
|
223
|
+
operator: :matches_any,
|
224
|
+
query_attribute_transformer: END_MATCHER_TRANSFORMER
|
225
|
+
},
|
226
|
+
MATCH_END_ALL: {
|
227
|
+
operator: :matches_all,
|
228
|
+
query_attribute_transformer: END_MATCHER_TRANSFORMER
|
229
|
+
},
|
230
|
+
MATCH_NOT_END: {
|
231
|
+
operator: :does_not_match,
|
232
|
+
query_attribute_transformer: END_MATCHER_TRANSFORMER
|
233
|
+
},
|
234
|
+
MATCH_NOT_END_ANY: {
|
235
|
+
operator: :does_not_match_any,
|
236
|
+
query_attribute_transformer: END_MATCHER_TRANSFORMER
|
237
|
+
},
|
238
|
+
MATCH_NOT_END_ALL: {
|
239
|
+
operator: :does_not_match_all,
|
240
|
+
query_attribute_transformer: END_MATCHER_TRANSFORMER
|
241
|
+
},
|
242
|
+
MATCH_CONTAIN: {
|
243
|
+
operator: :matches,
|
244
|
+
query_attribute_transformer: CONTAIN_MATCHER_TRANSFORMER
|
245
|
+
},
|
246
|
+
MATCH_CONTAIN_ANY: {
|
247
|
+
operator: :matches_any,
|
248
|
+
query_attribute_transformer: CONTAIN_MATCHER_TRANSFORMER
|
249
|
+
},
|
250
|
+
MATCH_CONTAIN_ALL: {
|
251
|
+
operator: :matches_all,
|
252
|
+
query_attribute_transformer: CONTAIN_MATCHER_TRANSFORMER
|
253
|
+
},
|
254
|
+
MATCH_NOT_CONTAIN: {
|
255
|
+
operator: :does_not_match,
|
256
|
+
query_attribute_transformer: CONTAIN_MATCHER_TRANSFORMER
|
257
|
+
},
|
258
|
+
MATCH_NOT_CONTAIN_ANY: {
|
259
|
+
operator: :does_not_match_any,
|
260
|
+
query_attribute_transformer: CONTAIN_MATCHER_TRANSFORMER
|
261
|
+
},
|
262
|
+
MATCH_NOT_CONTAIN_ALL: {
|
263
|
+
operator: :does_not_match_all,
|
264
|
+
query_attribute_transformer: CONTAIN_MATCHER_TRANSFORMER
|
265
|
+
}
|
266
|
+
}.freeze
|
267
|
+
|
268
|
+
def self.get(operator_name)
|
269
|
+
operator_key = operator_name.to_s.upcase.to_sym
|
270
|
+
|
271
|
+
base_operator_hash = Constants::PREDICATES.fetch(operator_key, {})
|
272
|
+
this_operator_hash = Operators::PREDICATES.fetch(operator_key, {})
|
273
|
+
|
274
|
+
base_operator_hash.merge(this_operator_hash)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
# rubocop:enable Metrics/ModuleLength
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ActiveSet
|
4
|
+
module Filtering
|
5
|
+
module ActiveRecord
|
6
|
+
module QueryColumn
|
7
|
+
def query_column
|
8
|
+
return @query_column if defined? @query_column
|
9
|
+
|
10
|
+
@query_column = if must_cast_numerical_column?
|
11
|
+
column_cast_as_char
|
12
|
+
else
|
13
|
+
arel_column
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def column_cast_as_char
|
20
|
+
# In order to use LIKE, we must CAST the numeric column as a CHAR column.
|
21
|
+
# NOTE: this is can be quite inefficient, as it forces the DB engine to perform that cast on all rows.
|
22
|
+
# https://www.ryadel.com/en/like-operator-equivalent-integer-numeric-columns-sql-t-sql-database/
|
23
|
+
Arel::Nodes::NamedFunction.new('CAST', [arel_column.as('CHAR')])
|
24
|
+
end
|
25
|
+
|
26
|
+
def must_cast_numerical_column?
|
27
|
+
# The LIKE operator can't be used if the column hosts numeric types.
|
28
|
+
return false unless arel_type.presence_in(%i[integer float])
|
29
|
+
|
30
|
+
arel_operator.to_s.downcase.include?('match')
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|