praxis 2.0.pre.13 → 2.0.pre.14
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/lib/praxis/extensions/attribute_filtering.rb +14 -1
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +20 -9
- data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +1 -1
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +2 -2
- data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +52 -12
- data/lib/praxis/version.rb +1 -1
- data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +18 -1
- data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +8 -1
- data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +13 -5
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6af11de4fdf970e848d638ed6fd1f47db86dcd27562bca90e50805398a82b346
|
4
|
+
data.tar.gz: dd4b664b1b8afde291e0402eeb0bb57178214ac1d8d6a4dc691f532fff7ae29f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 525c64c37e1cfe27fe74c1ddbd02d3c6e45860acd0f8a79a5d9a0832274368c728dacc8096b3b37f63c302fe265161793320a64dcf44a84d99ee5074d9de9da2
|
7
|
+
data.tar.gz: 79a70e175f17a14d98beddd66add981c03a8c720dd45855221b5b3223cb021d056381a5fd87880e3cbe2886755445182cf6cad3427de9f38b44220a26b23955d
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,15 @@
|
|
2
2
|
|
3
3
|
## next
|
4
4
|
|
5
|
+
## 2.0.pre.14
|
6
|
+
|
7
|
+
* More encoding/decoding robustness for filters.
|
8
|
+
* Specs for how to encode filters are now properly defined by:
|
9
|
+
* The "value" of the filters query string needs to be URI encoded (like any other query string value). This encoding is subject to the normal rules, and therefore "could" leave some of the URI unreserved characters (i.e., 'markers') unencoded depending on the client (Section 2.2 of https://tools.ietf.org/html/rfc2396).
|
10
|
+
* The "values" for any of the conditions in the contents of the filters, however, will need to be properly "escaped" as well (prior to URL-encoding the whole syntax string itself like described above). This means that any match value needs to ensure that it has (at least) "(",")","|","&" and "," escaped as they are reserved characters for the filter expression syntax. For example, if I want to search for a name with value "Rocket&(Pants)", I need to first compose the syntax by: "name=<escaped Rocket&(Pants)>, which is "name=Rocket%26%28Pants%29" and then, just URI encode that query string value for the filters parameter in the URL like any other. For example: "filters=name%3DRocket%2526%2528Pants%2529"
|
11
|
+
* When using a multi-match (csv-separated) list of values, you need to escape each of the values as well, leaving the 'comma' unescape, as that's part of the syntax. Then uri-encode it all for the filters query string parameter value like above.
|
12
|
+
* Now, one can properly differentiate between fuzzy query prefix/postfix, and the literal data to search for (which can be or include '*'). Report that multi-matches (i.e., csv separated values for a single field, which translate into "IN" clauses) is not allowed if fuzzy matches are received (need to use multiple OR clauses for it).
|
13
|
+
|
5
14
|
## 2.0.pre.13
|
6
15
|
|
7
16
|
* Fix filters parser regression, which would incorrectly decode url-encoded values
|
@@ -1,2 +1,15 @@
|
|
1
1
|
require 'praxis/extensions/attribute_filtering/filtering_params'
|
2
|
-
require 'praxis/extensions/attribute_filtering/filter_tree_node'
|
2
|
+
require 'praxis/extensions/attribute_filtering/filter_tree_node'
|
3
|
+
module Praxis
|
4
|
+
module Extensions
|
5
|
+
module AttributeFiltering
|
6
|
+
class MultiMatchWithFuzzyNotAllowedByAdapter < StandardError
|
7
|
+
def initialize
|
8
|
+
msg = 'Matching multiple, comma-separated values with fuzzy matches for a single field is not allowed by this DB adapter'\
|
9
|
+
'Please use multiple OR clauses instead.'
|
10
|
+
super(msg)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -72,7 +72,8 @@ module Praxis
|
|
72
72
|
column_prefix: column_prefix,
|
73
73
|
column_object: colo,
|
74
74
|
op: condition[:op],
|
75
|
-
value: condition[:value]
|
75
|
+
value: condition[:value],
|
76
|
+
fuzzy: condition[:fuzzy]
|
76
77
|
)
|
77
78
|
end
|
78
79
|
|
@@ -192,8 +193,8 @@ module Praxis
|
|
192
193
|
{associations_hash: h, conditions: conditions}
|
193
194
|
end
|
194
195
|
|
195
|
-
def self.add_clause(query:, column_prefix:, column_object:, op:, value:)
|
196
|
-
likeval = get_like_value(value)
|
196
|
+
def self.add_clause(query:, column_prefix:, column_object:, op:, value:,fuzzy:)
|
197
|
+
likeval = get_like_value(value,fuzzy)
|
197
198
|
case op
|
198
199
|
when '!' # name! means => name IS NOT NULL (and the incoming value is nil)
|
199
200
|
op = '!='
|
@@ -265,12 +266,22 @@ module Praxis
|
|
265
266
|
end
|
266
267
|
|
267
268
|
# Returns nil if the value was not a fuzzzy pattern
|
268
|
-
def self.get_like_value(value)
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
269
|
+
def self.get_like_value(value,fuzzy)
|
270
|
+
is_fuzzy = fuzzy.is_a?(Array) ? !fuzzy.compact.empty? : fuzzy
|
271
|
+
if is_fuzzy
|
272
|
+
unless value.is_a?(String)
|
273
|
+
raise MultiMatchWithFuzzyNotAllowedByAdapter.new
|
274
|
+
end
|
275
|
+
case fuzzy
|
276
|
+
when :start_end
|
277
|
+
'%'+value+'%'
|
278
|
+
when :start
|
279
|
+
'%'+value
|
280
|
+
when :end
|
281
|
+
value+'%'
|
282
|
+
end
|
283
|
+
else
|
284
|
+
nil
|
274
285
|
end
|
275
286
|
end
|
276
287
|
|
@@ -15,7 +15,7 @@ module Praxis
|
|
15
15
|
if components.empty?
|
16
16
|
return
|
17
17
|
elsif components.size == 1
|
18
|
-
@conditions << hash.slice(:name, :op, :value, :node_object)
|
18
|
+
@conditions << hash.slice(:name, :op, :value, :fuzzy, :node_object)
|
19
19
|
else
|
20
20
|
children_data[components.first] ||= []
|
21
21
|
children_data[components.first] << hash
|
@@ -182,7 +182,7 @@ module Praxis
|
|
182
182
|
else
|
183
183
|
spec[:values]
|
184
184
|
end
|
185
|
-
accum.push(name: attr_name, op: spec[:op], value: coerced , node_object: spec[:node_object])
|
185
|
+
accum.push(name: attr_name, op: spec[:op], value: coerced , fuzzy: spec[:fuzzies], node_object: spec[:node_object])
|
186
186
|
end
|
187
187
|
new(accum)
|
188
188
|
end
|
@@ -225,7 +225,7 @@ module Praxis
|
|
225
225
|
value = item[:value]
|
226
226
|
unless value.empty?
|
227
227
|
fuzzy_match = attr_filters[:fuzzy_match]
|
228
|
-
if
|
228
|
+
if item[:fuzzy] && !item[:fuzzy].empty? && !fuzzy_match
|
229
229
|
errors << "Fuzzy matching for #{attr_name} is not allowed (yet '*' was found in the value)"
|
230
230
|
end
|
231
231
|
end
|
@@ -15,41 +15,81 @@ module Praxis
|
|
15
15
|
# Example: [{:name=>"multi"@0, :op=>"="@5}, {:value=>"1"@6}, {:value=>"2"@8}]
|
16
16
|
def initialize(triad:, parent_group:)
|
17
17
|
@parent_group = parent_group
|
18
|
-
|
19
18
|
if triad.is_a? Array # several values coming in
|
20
19
|
spec, *values = triad
|
21
20
|
@name = spec[:name].to_sym
|
22
21
|
@op = spec[:op].to_s
|
23
22
|
|
24
|
-
|
25
|
-
""
|
23
|
+
if values.empty?
|
24
|
+
@values = ""
|
25
|
+
@fuzzies = nil
|
26
26
|
elsif values.size == 1
|
27
|
-
|
27
|
+
raw_val = values.first[:value].to_s
|
28
|
+
@values, @fuzzies = _compute_fuzzy(values.first[:value].to_s)
|
28
29
|
else
|
29
|
-
values
|
30
|
+
@values = []
|
31
|
+
@fuzzies = []
|
32
|
+
results = values.each do|e|
|
33
|
+
val, fuz = _compute_fuzzy(e[:value].to_s)
|
34
|
+
@values.push val
|
35
|
+
@fuzzies.push fuz
|
36
|
+
end
|
30
37
|
end
|
31
38
|
else # No values for the operand
|
32
39
|
@name = triad[:name].to_sym
|
33
40
|
@op = triad[:op].to_s
|
34
41
|
if ['!','!!'].include?(@op)
|
35
|
-
@values = nil
|
42
|
+
@values, @fuzzies = [nil, nil]
|
36
43
|
else
|
37
44
|
# Value operand without value? => convert it to empty string
|
38
45
|
raise "Interesting, didn't know this could happen. Oops!" if triad[:value].is_a?(Array) && !triad[:value].empty?
|
39
|
-
|
46
|
+
if triad[:value] == []
|
47
|
+
@values, @fuzzies = ['', nil]
|
48
|
+
else
|
49
|
+
@values, @fuzzies = _compute_fuzzy(triad[:value].to_s)
|
50
|
+
end
|
40
51
|
end
|
41
52
|
end
|
42
53
|
end
|
43
|
-
|
54
|
+
# Takes a raw val, and spits out the output val (unescaped), and the fuzzy definition
|
55
|
+
def _compute_fuzzy(raw_val)
|
56
|
+
starting = raw_val[0] == '*'
|
57
|
+
ending = raw_val[-1] == '*'
|
58
|
+
newval, fuzzy = if starting && ending
|
59
|
+
[raw_val[1..-2], :start_end]
|
60
|
+
elsif starting
|
61
|
+
[raw_val[1..-1], :start]
|
62
|
+
elsif ending
|
63
|
+
[raw_val[0..-2], :end]
|
64
|
+
else
|
65
|
+
[raw_val,nil]
|
66
|
+
end
|
67
|
+
newval = CGI.unescape(newval) if newval
|
68
|
+
[newval,fuzzy]
|
69
|
+
end
|
44
70
|
def flattened_conditions
|
45
|
-
[{name: @name, op: @op, values: @values, node_object: self}]
|
71
|
+
[{name: @name, op: @op, values: @values, fuzzies: @fuzzies, node_object: self}]
|
46
72
|
end
|
47
73
|
|
74
|
+
# Dumps the value, marking where the fuzzy might be, and removing the * to differentiate from literals
|
75
|
+
def _dump_value(val,fuzzy)
|
76
|
+
case fuzzy
|
77
|
+
when nil
|
78
|
+
val
|
79
|
+
when :start_end
|
80
|
+
'{*}' + val + '{*}'
|
81
|
+
when :start
|
82
|
+
'{*}' + val
|
83
|
+
when :end
|
84
|
+
val +'{*}'
|
85
|
+
end
|
86
|
+
end
|
48
87
|
def dump
|
49
88
|
vals = if values.is_a? Array
|
50
|
-
|
89
|
+
dumped = values.map.with_index{|val,i| _dump_value(val, @fuzzies[i])}
|
90
|
+
"[#{dumped.join(',')}]" # Purposedly enclose in brackets to make sure we differentiate
|
51
91
|
else
|
52
|
-
(values == '') ? "
|
92
|
+
(values == '') ? '""' : _dump_value(values,@fuzzies) # Dump the empty string explicitly with quotes if we've converted no value to empty string
|
53
93
|
end
|
54
94
|
"#{name}#{op}#{vals}"
|
55
95
|
end
|
@@ -134,7 +174,7 @@ module Praxis
|
|
134
174
|
end
|
135
175
|
|
136
176
|
rule(:name) { match('[a-zA-Z0-9_\.]').repeat(1) } # TODO: are these the only characters that we allow for names?
|
137
|
-
rule(:chars) { match('[^&|),]').repeat(0).as(:value) }
|
177
|
+
rule(:chars) { match('[^&|(),]').repeat(0).as(:value) }
|
138
178
|
rule(:value) { chars >> (comma >> chars ).repeat }
|
139
179
|
|
140
180
|
rule(:triad) {
|
data/lib/praxis/version.rb
CHANGED
@@ -71,7 +71,7 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
|
|
71
71
|
end
|
72
72
|
end
|
73
73
|
end
|
74
|
-
|
74
|
+
context 'that maps to a different name' do
|
75
75
|
let(:filters_string) { 'name=Book1'}
|
76
76
|
it_behaves_like 'subject_equivalent_to', ActiveBook.where(simple_name: 'Book1')
|
77
77
|
end
|
@@ -79,6 +79,23 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
|
|
79
79
|
let(:filters_string) { 'fake_nested.name=Book1'}
|
80
80
|
it_behaves_like 'subject_equivalent_to', ActiveBook.where(simple_name: 'Book1')
|
81
81
|
end
|
82
|
+
context 'passing multiple values' do
|
83
|
+
context 'without fuzzy matching' do
|
84
|
+
let(:filters_string) { 'category_uuid=deadbeef1,deadbeef2' }
|
85
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: ['deadbeef1','deadbeef2'])
|
86
|
+
end
|
87
|
+
context 'with fuzzy matching' do
|
88
|
+
let(:filters_string) { 'category_uuid=*deadbeef1,deadbeef2*' }
|
89
|
+
it 'is not supported' do
|
90
|
+
expect{
|
91
|
+
subject
|
92
|
+
}.to raise_error(
|
93
|
+
Praxis::Extensions::AttributeFiltering::MultiMatchWithFuzzyNotAllowedByAdapter,
|
94
|
+
/Please use multiple OR clauses instead/
|
95
|
+
)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
82
99
|
end
|
83
100
|
|
84
101
|
context 'by a field or a related model' do
|
@@ -29,7 +29,14 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams do
|
|
29
29
|
{ name: :two, op: '>', value: 'normal'},
|
30
30
|
]
|
31
31
|
expect(described_class.load(str).parsed_array.map{|i| i.slice(:name,:op,:value)}).to eq(parsed)
|
32
|
-
|
32
|
+
end
|
33
|
+
it 'does not handle badly escaped values that contain reserved chars ()|&,' do
|
34
|
+
badly_escaped = 'val('
|
35
|
+
str = "one=#{badly_escaped}&(two>normal|three!)"
|
36
|
+
expect{
|
37
|
+
described_class.load(str)
|
38
|
+
}.to raise_error(Parslet::ParseFailed)
|
39
|
+
end
|
33
40
|
end
|
34
41
|
context 'parses for operator' do
|
35
42
|
described_class::VALUE_OPERATORS.each do |op|
|
@@ -1,6 +1,5 @@
|
|
1
1
|
require 'praxis/extensions/attribute_filtering/filters_parser'
|
2
2
|
|
3
|
-
|
4
3
|
describe Praxis::Extensions::AttributeFiltering::FilteringParams::Condition do
|
5
4
|
end
|
6
5
|
|
@@ -121,17 +120,26 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams::Parser do
|
|
121
120
|
context 'supports everything (except &|(),) for values (even without encoding..not allowed, but just to ensure the parser does not bomb)' do
|
122
121
|
it_behaves_like 'round-trip-properly', {
|
123
122
|
'v=1123' => 'v=1123',
|
124
|
-
'v=*foo*' => 'v
|
125
|
-
'v=*^%$#@!foo' => 'v
|
123
|
+
'v=*foo*' => 'v={*}foo{*}',
|
124
|
+
'v=*^%$#@!foo' => 'v={*}^%$#@!foo',
|
126
125
|
'v=_-=\{}"?:><' => 'v=_-=\{}"?:><',
|
127
126
|
'v=_-=\{}"?:><,another_value!' => 'v=[_-=\{}"?:><,another_value!]',
|
128
127
|
}
|
129
128
|
end
|
129
|
+
context 'properly detects and handles fuzzy matching encoded as {*} in the dump' do
|
130
|
+
it_behaves_like 'round-trip-properly', {
|
131
|
+
'v=*foo' => 'v={*}foo',
|
132
|
+
'v=*foo*' => 'v={*}foo{*}',
|
133
|
+
'v=foo*' => 'v=foo{*}',
|
134
|
+
'v=*start,end*,*both*' => 'v=[{*}start,end{*},{*}both{*}]',
|
135
|
+
"v=*#{CGI.escape('***')},#{CGI.escape('*')}" => 'v=[{*}***,*]', # Simple exact match on 2nd
|
136
|
+
}
|
137
|
+
end
|
130
138
|
context 'properly handles url-encoded values' do
|
131
139
|
it_behaves_like 'round-trip-properly', {
|
132
140
|
"v=#{CGI.escape('1123')}" => 'v=1123',
|
133
|
-
"v
|
134
|
-
"v
|
141
|
+
"v=*#{CGI.escape('foo')}*" => 'v={*}foo{*}',
|
142
|
+
"v=*#{CGI.escape('^%$#@!foo')}" => 'v={*}^%$#@!foo',
|
135
143
|
"v=#{CGI.escape('~!@#$%^&*()_+-={}|[]\:";\'<>?,./`')}" => 'v=~!@#$%^&*()_+-={}|[]\:";\'<>?,./`',
|
136
144
|
"v=#{CGI.escape('_-+=\{}"?:><')},#{CGI.escape('another_value!')}" => 'v=[_-+=\{}"?:><,another_value!]',
|
137
145
|
}
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: praxis
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.pre.
|
4
|
+
version: 2.0.pre.14
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josep M. Blanquer
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2021-02-
|
12
|
+
date: 2021-02-26 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rack
|