search_cop 1.0.8 → 1.0.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +3 -3
- data/README.md +56 -23
- data/lib/search_cop/hash_parser.rb +6 -2
- data/lib/search_cop/search_scope.rb +6 -1
- data/lib/search_cop/version.rb +1 -1
- data/lib/search_cop/visitors/visitor.rb +5 -0
- data/lib/search_cop_grammar/attributes.rb +20 -0
- data/lib/search_cop_grammar/nodes.rb +1 -0
- data/test/hash_test.rb +10 -0
- data/test/test_helper.rb +4 -0
- data/test/visitor_test.rb +9 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ec8667b85e0e2df63ebb83e3d4af5319e52483b9
|
4
|
+
data.tar.gz: 0a1fd1d2c76c9395ac50adc9e1139dbc58ea2cd5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ca9b2397fb9bd9b7e3b339f001c58b36c42be50f5a9314094735f6633bb75cb26a91b9e08120a10750653ae5067a82eda96d2a562cff31b92ea3aaa390487c51
|
7
|
+
data.tar.gz: 6479e2365da9aa3ae8c9a9f6587c6f00a0f0c7d2546d05438dc4738eb810b5eed9a0d5ff432b91a1a9757486d99f795e86f556d55e379a4ce240ed21d89714de
|
data/.travis.yml
CHANGED
@@ -18,13 +18,13 @@ env:
|
|
18
18
|
- DATABASE=postgres
|
19
19
|
matrix:
|
20
20
|
include:
|
21
|
-
- rvm: 2.
|
21
|
+
- rvm: 2.4.1
|
22
22
|
gemfile: gemfiles/5.0-Gemfile
|
23
23
|
env: DATABASE=sqlite
|
24
|
-
- rvm: 2.
|
24
|
+
- rvm: 2.4.1
|
25
25
|
gemfile: gemfiles/5.0-Gemfile
|
26
26
|
env: DATABASE=mysql
|
27
|
-
- rvm: 2.
|
27
|
+
- rvm: 2.4.1
|
28
28
|
gemfile: gemfiles/5.0-Gemfile
|
29
29
|
env: DATABASE=postgres
|
30
30
|
allow_failures:
|
data/README.md
CHANGED
@@ -29,16 +29,17 @@ to make optimal use of them. Read more below.
|
|
29
29
|
Complex hash-based queries are supported as well:
|
30
30
|
|
31
31
|
```ruby
|
32
|
-
Book.search(:
|
33
|
-
Book.search(:
|
34
|
-
Book.search(:
|
35
|
-
Book.search(:
|
32
|
+
Book.search(author: "Rowling", title: "Harry Potter")
|
33
|
+
Book.search(or: [{author: "Rowling"}, {author: "Tolkien"}])
|
34
|
+
Book.search(and: [{price: {gt: 10}}, {not: {stock: 0}}, or: [{title: "Potter"}, {author: "Rowling"}]])
|
35
|
+
Book.search(or: [{query: "Rowling -Potter"}, {query: "Tolkien -Rings"}])
|
36
|
+
Book.search(title: {my_custom_sql_query: "Rowl"}})
|
36
37
|
# ...
|
37
38
|
```
|
38
39
|
|
39
40
|
## Installation
|
40
41
|
|
41
|
-
|
42
|
+
Add this line to your application's Gemfile:
|
42
43
|
|
43
44
|
gem 'search_cop'
|
44
45
|
|
@@ -61,8 +62,8 @@ class Book < ActiveRecord::Base
|
|
61
62
|
|
62
63
|
search_scope :search do
|
63
64
|
attributes :title, :description, :stock, :price, :created_at, :available
|
64
|
-
attributes :
|
65
|
-
attributes :
|
65
|
+
attributes comment: ["comments.title", "comments.message"]
|
66
|
+
attributes author: "author.name"
|
66
67
|
# ...
|
67
68
|
end
|
68
69
|
|
@@ -114,7 +115,7 @@ As `Book.search(...)` returns an `ActiveRecord::Relation`, you are free to pre-
|
|
114
115
|
or post-process the search results in every possible way:
|
115
116
|
|
116
117
|
```ruby
|
117
|
-
Book.where(:
|
118
|
+
Book.where(available: true).search("Harry Potter").order("books.id desc").paginate(page: params[:page])
|
118
119
|
```
|
119
120
|
|
120
121
|
## Fulltext index capabilities
|
@@ -164,12 +165,12 @@ search in, such that SearchCop must no longer search within all fields:
|
|
164
165
|
|
165
166
|
```ruby
|
166
167
|
search_scope :search do
|
167
|
-
attributes :
|
168
|
+
attributes all: [:author, :title]
|
168
169
|
|
169
|
-
options :all, :type => :fulltext, :
|
170
|
+
options :all, :type => :fulltext, default: true
|
170
171
|
|
171
|
-
# Use :
|
172
|
-
# Use :
|
172
|
+
# Use default: true to explicitly enable fields as default fields (whitelist approach)
|
173
|
+
# Use default: false to explicitly disable fields as default fields (blacklist approach)
|
173
174
|
end
|
174
175
|
```
|
175
176
|
|
@@ -298,7 +299,7 @@ class User < ActiveRecord::Base
|
|
298
299
|
search_scope :search do
|
299
300
|
attributes :username
|
300
301
|
|
301
|
-
options :username, :
|
302
|
+
options :username, left_wildcard: false
|
302
303
|
end
|
303
304
|
|
304
305
|
# ...
|
@@ -322,7 +323,7 @@ class Book < ActiveRecord::Base
|
|
322
323
|
belongs_to :author
|
323
324
|
|
324
325
|
search_scope :search do
|
325
|
-
attributes :
|
326
|
+
attributes author: "author.name"
|
326
327
|
end
|
327
328
|
|
328
329
|
# ...
|
@@ -356,13 +357,13 @@ class Book < ActiveRecord::Base
|
|
356
357
|
# ...
|
357
358
|
|
358
359
|
search_scope :search do
|
359
|
-
attributes :
|
360
|
+
attributes similar: ["similar_books.title", "similar_books.description"]
|
360
361
|
|
361
362
|
scope do
|
362
363
|
joins "left outer join books similar_books on ..."
|
363
364
|
end
|
364
365
|
|
365
|
-
aliases :
|
366
|
+
aliases similar_books: Book # Tell SearchCop how to map SQL aliases to models
|
366
367
|
end
|
367
368
|
|
368
369
|
# ...
|
@@ -379,7 +380,7 @@ class Book < ActiveRecord::Base
|
|
379
380
|
has_many :users, :through => :comments
|
380
381
|
|
381
382
|
search_scope :search do
|
382
|
-
attributes :
|
383
|
+
attributes user: "users.username"
|
383
384
|
end
|
384
385
|
|
385
386
|
# ...
|
@@ -402,7 +403,7 @@ class Book < ActiveRecord::Base
|
|
402
403
|
belongs_to :user
|
403
404
|
|
404
405
|
search_scope :search do
|
405
|
-
attributes :
|
406
|
+
attributes user: ["user.username", "users_books.username"]
|
406
407
|
end
|
407
408
|
|
408
409
|
# ...
|
@@ -440,12 +441,44 @@ Query string queries support `AND/and`, `OR/or`, `:`, `=`, `!=`, `<`, `<=`,
|
|
440
441
|
and `matches`, `OR` has precedence over `AND`. `NOT` can only be used as infix
|
441
442
|
operator regarding a single attribute.
|
442
443
|
|
443
|
-
Hash based queries support
|
444
|
-
of
|
445
|
-
|
446
|
-
arguments. Moreover,
|
444
|
+
Hash based queries support `and: [...]` and `or: [...]`, which take an array
|
445
|
+
of `not: {...}`, `matches: {...}`, `eq: {...}`, `not_eq: {...}`,
|
446
|
+
`lt: {...}`, `lteq: {...}`, `gt: {...}`, `gteq: {...}` and `query: "..."`
|
447
|
+
arguments. Moreover, `query: "..."` makes it possible to create sub-queries.
|
447
448
|
The other rules for query string queries apply to hash based queries as well.
|
448
449
|
|
450
|
+
### Custom operators (Hash based queries)
|
451
|
+
|
452
|
+
SearchCop also provides the ability to define custom operators by defining a
|
453
|
+
`generator` in `search_scope`. They can then be used with the hash based query
|
454
|
+
search. This is useful when you want to use database operators that are not
|
455
|
+
supported by SearchCop.
|
456
|
+
|
457
|
+
For example, if you wanted to perform a `LIKE` query where a book title starts
|
458
|
+
with a string, you can define the search scope like so:
|
459
|
+
|
460
|
+
```ruby
|
461
|
+
search_scope :search do
|
462
|
+
attributes :title
|
463
|
+
|
464
|
+
generator :starts_with do |column_name, raw_value|
|
465
|
+
pattern = "#{raw_value}%"
|
466
|
+
"#{column_name} LIKE #{quote pattern}"
|
467
|
+
end
|
468
|
+
end
|
469
|
+
```
|
470
|
+
|
471
|
+
When you want to perform the search you use it like this:
|
472
|
+
|
473
|
+
```ruby
|
474
|
+
Book.search(title: { starts_with: "The Great" })
|
475
|
+
```
|
476
|
+
|
477
|
+
Security Note: The query returned from the generator will be interpolated
|
478
|
+
directly into the query that goes to your database. This opens up a potential
|
479
|
+
SQL Injection point in your app. If you use this feature you'll want to make
|
480
|
+
sure the query you're returning is safe to execute.
|
481
|
+
|
449
482
|
## Mapping
|
450
483
|
|
451
484
|
When searching in boolean, datetime, timestamp, etc. fields, SearchCop
|
@@ -530,7 +563,7 @@ class Product < ActiveRecord::Base
|
|
530
563
|
search_scope :search do
|
531
564
|
attributes :title, :description
|
532
565
|
|
533
|
-
options :title, :
|
566
|
+
options :title, default: true
|
534
567
|
end
|
535
568
|
end
|
536
569
|
|
@@ -31,9 +31,13 @@ class SearchCop::HashParser
|
|
31
31
|
collection = SearchCopGrammar::Attributes::Collection.new(query_info, key.to_s)
|
32
32
|
|
33
33
|
if value.is_a?(Hash)
|
34
|
-
raise(SearchCop::ParseError, "Unknown operator #{value.keys.first}") unless
|
34
|
+
raise(SearchCop::ParseError, "Unknown operator #{value.keys.first}") unless collection.valid_operator?(value.keys.first)
|
35
35
|
|
36
|
-
collection.
|
36
|
+
if generator = collection.generator_for(value.keys.first)
|
37
|
+
collection.generator generator, value.values.first
|
38
|
+
else
|
39
|
+
collection.send value.keys.first, value.values.first.to_s
|
40
|
+
end
|
37
41
|
else
|
38
42
|
collection.send :matches, value.to_s
|
39
43
|
end
|
@@ -1,12 +1,13 @@
|
|
1
1
|
|
2
2
|
module SearchCop
|
3
3
|
class Reflection
|
4
|
-
attr_accessor :attributes, :options, :aliases, :scope
|
4
|
+
attr_accessor :attributes, :options, :aliases, :scope, :generators
|
5
5
|
|
6
6
|
def initialize
|
7
7
|
self.attributes = {}
|
8
8
|
self.options = {}
|
9
9
|
self.aliases = {}
|
10
|
+
self.generators = {}
|
10
11
|
end
|
11
12
|
|
12
13
|
def default_attributes
|
@@ -46,6 +47,10 @@ module SearchCop
|
|
46
47
|
reflection.scope = block
|
47
48
|
end
|
48
49
|
|
50
|
+
def generator(name, &block)
|
51
|
+
reflection.generators[name] = block
|
52
|
+
end
|
53
|
+
|
49
54
|
private
|
50
55
|
|
51
56
|
def attributes_hash(hash)
|
data/lib/search_cop/version.rb
CHANGED
@@ -55,6 +55,10 @@ module SearchCop
|
|
55
55
|
"NOT (#{visit node.object})"
|
56
56
|
end
|
57
57
|
|
58
|
+
def visit_SearchCopGrammar_Nodes_Generator(node)
|
59
|
+
instance_exec visit(node.left), node.right[:value], &node.right[:generator]
|
60
|
+
end
|
61
|
+
|
58
62
|
def quote_table_name(name)
|
59
63
|
connection.quote_table_name name
|
60
64
|
end
|
@@ -90,6 +94,7 @@ module SearchCop
|
|
90
94
|
alias :visit_Float :quote
|
91
95
|
alias :visit_Fixnum :quote
|
92
96
|
alias :visit_Symbol :quote
|
97
|
+
alias :visit_Integer :quote
|
93
98
|
end
|
94
99
|
end
|
95
100
|
end
|
@@ -6,6 +6,8 @@ module SearchCopGrammar
|
|
6
6
|
class Collection
|
7
7
|
attr_reader :query_info, :key
|
8
8
|
|
9
|
+
INCLUDED_OPERATORS = [:matches, :eq, :not_eq, :gt, :gteq, :lt, :lteq].freeze
|
10
|
+
|
9
11
|
def initialize(query_info, key)
|
10
12
|
raise(SearchCop::UnknownColumn, "Unknown column #{key}") unless query_info.scope.reflection.attributes[key]
|
11
13
|
|
@@ -31,6 +33,12 @@ module SearchCopGrammar
|
|
31
33
|
end
|
32
34
|
end
|
33
35
|
|
36
|
+
def generator(generator, value)
|
37
|
+
attributes.collect! do |attribute|
|
38
|
+
SearchCopGrammar::Nodes::Generator.new(attribute, generator: generator, value: value)
|
39
|
+
end.inject(:or)
|
40
|
+
end
|
41
|
+
|
34
42
|
def matches(value)
|
35
43
|
if fulltext?
|
36
44
|
SearchCopGrammar::Nodes::MatchesFulltext.new self, value.to_s
|
@@ -88,6 +96,18 @@ module SearchCopGrammar
|
|
88
96
|
|
89
97
|
Attributes.const_get(klass.columns_hash[column].type.to_s.classify).new(klass, alias_for(table), column, options)
|
90
98
|
end
|
99
|
+
|
100
|
+
def generator_for(name)
|
101
|
+
generators[name]
|
102
|
+
end
|
103
|
+
|
104
|
+
def valid_operator?(operator)
|
105
|
+
(INCLUDED_OPERATORS + generators.keys).include?(operator)
|
106
|
+
end
|
107
|
+
|
108
|
+
def generators
|
109
|
+
query_info.scope.reflection.generators
|
110
|
+
end
|
91
111
|
end
|
92
112
|
|
93
113
|
class Base
|
data/test/hash_test.rb
CHANGED
@@ -93,5 +93,15 @@ class HashTest < SearchCop::TestCase
|
|
93
93
|
assert_includes results, expected
|
94
94
|
refute_includes results, rejected
|
95
95
|
end
|
96
|
+
|
97
|
+
def test_custom_matcher
|
98
|
+
expected = create(:product, :title => "Expected")
|
99
|
+
rejected = create(:product, :title => "Rejected")
|
100
|
+
|
101
|
+
results = Product.search(:title => { :custom_eq => "Expected" })
|
102
|
+
|
103
|
+
assert_includes results, expected
|
104
|
+
refute_includes results, rejected
|
105
|
+
end
|
96
106
|
end
|
97
107
|
|
data/test/test_helper.rb
CHANGED
@@ -52,6 +52,10 @@ class Product < ActiveRecord::Base
|
|
52
52
|
if DATABASE == "postgres"
|
53
53
|
options :title, :dictionary => "english"
|
54
54
|
end
|
55
|
+
|
56
|
+
generator :custom_eq do |column_name, raw_value|
|
57
|
+
"#{column_name} = #{quote raw_value}"
|
58
|
+
end
|
55
59
|
end
|
56
60
|
|
57
61
|
search_scope :user_search do
|
data/test/visitor_test.rb
CHANGED
@@ -97,5 +97,14 @@ class VisitorTest < SearchCop::TestCase
|
|
97
97
|
assert_equal("(MATCH(`products`.`title`) AGAINST('(Query1) (Query2)' IN BOOLEAN MODE))", SearchCop::Visitors::Visitor.new(ActiveRecord::Base.connection).visit(node)) if ENV["DATABASE"] == "mysql"
|
98
98
|
assert_equal("(to_tsvector('english', COALESCE(\"products\".\"title\", '')) @@ to_tsquery('english', '(''Query1'') | (''Query2'')'))", SearchCop::Visitors::Visitor.new(ActiveRecord::Base.connection).visit(node)) if ENV["DATABASE"] == "postgres"
|
99
99
|
end
|
100
|
+
|
101
|
+
def test_generator
|
102
|
+
generator = ->(column_name, value) do
|
103
|
+
"#{column_name} = #{quote value}"
|
104
|
+
end
|
105
|
+
node = SearchCopGrammar::Attributes::Collection.new(SearchCop::QueryInfo.new(Product, Product.search_scopes[:search]), "title").generator(generator, "value").optimize!
|
106
|
+
|
107
|
+
assert_equal "#{quote_table_name "products"}.#{quote_column_name "title"} = 'value'", SearchCop::Visitors::Visitor.new(ActiveRecord::Base.connection).visit(node)
|
108
|
+
end
|
100
109
|
end
|
101
110
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: search_cop
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Benjamin Vetter
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-07-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: treetop
|