search_cop 1.0.8 → 1.0.9
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/.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
|