scoped_search 4.1.5 → 4.1.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 25198636d3a83a3be37b174722b43786bfb66c40
4
- data.tar.gz: 2d51410c045dd98edffd782900a960f5111c47f4
3
+ metadata.gz: c94c65fc191d2afdddd82f3177121d2ade7e6238
4
+ data.tar.gz: 36dd864d06aafd40bd3015184a17222a92d5627f
5
5
  SHA512:
6
- metadata.gz: 467be1c841cb5a0ce8eff10af66df07db0527a8b7423f386ad8a5ea29bea2ab4e3d02edb0b480ff427bfbdc0078ca1ac138690866db4fb67f13c31322334320b
7
- data.tar.gz: fbc5a32d17479d8e919ca7b9e198416a0acb60bcbf0e1b2b3b7b850ad97af135a7cb5d57b0f93d3fee7a1b27c8ee7b47871367506ebe100e8beb58179041c01d
6
+ metadata.gz: 2a41edacae0b5140f56dc60bc921c8afb3349132b9ad3453313fc407abbda58b251c08d1da8d56706f9307520882e9e8006f18147833c91b7e3d0995962d843e
7
+ data.tar.gz: 31932dcd317193df257a4d0fd0c46fea28866e7670a0cfbd1d840d4477ada8381906d5f83b9289b322de3bbc44048ec249d9bf6af67f605bfc76589be9ccc7e3
@@ -8,6 +8,11 @@ Please add an entry to the "Unreleased changes" section in your pull requests.
8
8
 
9
9
  - Nothing yet
10
10
 
11
+ === Version 4.1.6
12
+
13
+ - Support for UUID columns on PostgreSQL (#184)
14
+ - Allow value mapping on runtime to allow queries like "user = current" (#183)
15
+
11
16
  === Version 4.1.5
12
17
 
13
18
  - Bugfix related to auto-completion of virtual fields (#182)
@@ -201,10 +201,16 @@ module ScopedSearch
201
201
  return complete_date_value if field.temporal?
202
202
  return complete_key_value(field, token, val) if field.key_field
203
203
 
204
+ special_values = field.special_values.select { |v| v =~ /\A#{val}/ }
205
+ special_values + complete_value_from_db(field, special_values, val)
206
+ end
207
+
208
+ def complete_value_from_db(field, special_values, val)
209
+ count = 20 - special_values.count
204
210
  completer_scope(field)
205
211
  .where(value_conditions(field.quoted_field, val))
206
212
  .select(field.quoted_field)
207
- .limit(20)
213
+ .limit(count)
208
214
  .distinct
209
215
  .map(&field.field)
210
216
  .compact
@@ -17,7 +17,7 @@ module ScopedSearch
17
17
 
18
18
  attr_reader :definition, :field, :only_explicit, :relation, :key_relation, :full_text_search,
19
19
  :key_field, :complete_value, :complete_enabled, :offset, :word_size, :ext_method, :operators,
20
- :validator
20
+ :validator, :value_translation, :special_values
21
21
 
22
22
  # Initializes a Field instance given the definition passed to the
23
23
  # scoped_search call on the ActiveRecord-based model class.
@@ -42,7 +42,9 @@ module ScopedSearch
42
42
  profile: nil,
43
43
  relation: nil,
44
44
  rename: nil,
45
+ special_values: [],
45
46
  validator: nil,
47
+ value_translation: nil,
46
48
  word_size: 1,
47
49
  **kwargs)
48
50
 
@@ -50,6 +52,8 @@ module ScopedSearch
50
52
  raise ArgumentError, "Missing field or 'on' keyword argument" if on.nil?
51
53
  @field = on.to_sym
52
54
 
55
+ raise ArgumentError, "'special_values' must be an Array" unless special_values.kind_of?(Array)
56
+
53
57
  # Reserved Ruby keywords so access via kwargs instead, but deprecate them for future versions
54
58
  if kwargs.key?(:in)
55
59
  relation = kwargs.delete(:in)
@@ -77,8 +81,10 @@ module ScopedSearch
77
81
  @only_explicit = !!only_explicit
78
82
  @operators = operators
79
83
  @relation = relation
84
+ @special_values = special_values
80
85
  @validator = validator
81
86
  @word_size = word_size
87
+ @value_translation = value_translation
82
88
 
83
89
  # Store this field in the field array
84
90
  definition.define_field(rename || @field, self)
@@ -154,6 +160,10 @@ module ScopedSearch
154
160
  [:string, :text].include?(type)
155
161
  end
156
162
 
163
+ def uuid?
164
+ type == :uuid
165
+ end
166
+
157
167
  # Returns true if this is a set.
158
168
  def set?
159
169
  complete_value.is_a?(Hash)
@@ -238,7 +248,7 @@ module ScopedSearch
238
248
  return [] if field.nil?
239
249
  return field.operators if field.operators
240
250
  return ['=', '!=', '>', '<', '<=', '>=', '~', '!~', '^', '!^'] if field.virtual?
241
- return ['=', '!='] if field.set?
251
+ return ['=', '!='] if field.set? || field.uuid?
242
252
  return ['=', '>', '<', '<=', '>=', '!=', '^', '!^'] if field.numerical?
243
253
  return ['=', '!=', '~', '!~', '^', '!^'] if field.textual?
244
254
  return ['=', '>', '<'] if field.temporal?
@@ -247,6 +257,7 @@ module ScopedSearch
247
257
 
248
258
  NUMERICAL_REGXP = /^\-?\d+(\.\d+)?$/
249
259
  INTEGER_REGXP = /^\-?\d+$/
260
+ UUID_REGXP = /^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$/
250
261
 
251
262
  # Returns a list of appropriate fields to search in given a search keyword and operator.
252
263
  def default_fields_for(value, operator = nil)
@@ -255,6 +266,7 @@ module ScopedSearch
255
266
  column_types += [:string, :text] if [nil, :like, :unlike, :ne, :eq].include?(operator)
256
267
  column_types += [:double, :float, :decimal] if value =~ NUMERICAL_REGXP
257
268
  column_types += [:integer] if value =~ INTEGER_REGXP
269
+ column_types += [:uuid] if value =~ UUID_REGXP
258
270
  column_types += [:datetime, :date, :timestamp] if (parse_temporal(value))
259
271
 
260
272
  default_fields.select { |field| !field.set? && column_types.include?(field.type) }
@@ -181,6 +181,14 @@ module ScopedSearch
181
181
  translated_value
182
182
  end
183
183
 
184
+ def map_value(field, value)
185
+ old_value = value
186
+ translator = field.value_translation
187
+ value = translator.call(value) if translator
188
+ raise ScopedSearch::QueryNotSupported, "Translation from any value to nil is not allowed, translated '#{old_value}'" if value.nil?
189
+ value
190
+ end
191
+
184
192
  # A 'set' is group of possible values, for example a status might be "on", "off" or "unknown" and the database representation
185
193
  # could be for example a numeric value. This method will validate the input and translate it into the database representation.
186
194
  def set_test(field, operator,value, &block)
@@ -219,7 +227,7 @@ module ScopedSearch
219
227
  return "#{field.to_sql(operator, &block)} #{self.sql_operator(operator, field)} ?"
220
228
 
221
229
  elsif [:in, :notin].include?(operator)
222
- value.split(',').collect { |v| yield(:parameter, field.set? ? translate_value(field, v) : v.strip) }
230
+ value.split(',').collect { |v| yield(:parameter, map_value(field, field.set? ? translate_value(field, v) : v.strip)) }
223
231
  value = value.split(',').collect { "?" }.join(",")
224
232
  return "#{field.to_sql(operator, &block)} #{self.sql_operator(operator, field)} (#{value})"
225
233
 
@@ -231,6 +239,7 @@ module ScopedSearch
231
239
 
232
240
  elsif field.relation && definition.reflection_by_name(field.definition.klass, field.relation).macro == :has_many
233
241
  value = value.to_i if field.offset
242
+ value = map_value(field, value)
234
243
  yield(:parameter, value)
235
244
  connection = field.definition.klass.connection
236
245
  primary_key = "#{connection.quote_table_name(field.definition.klass.table_name)}.#{connection.quote_column_name(field.definition.klass.primary_key)}"
@@ -244,6 +253,7 @@ module ScopedSearch
244
253
 
245
254
  else
246
255
  value = value.to_i if field.offset
256
+ value = map_value(field, value)
247
257
  yield(:parameter, value)
248
258
  return "#{field.to_sql(operator, &block)} #{self.sql_operator(operator, field)} ?"
249
259
  end
@@ -517,7 +527,7 @@ module ScopedSearch
517
527
  def validate_value(field, value)
518
528
  validator = field.validator
519
529
  if validator
520
- valid = validator.call(value)
530
+ valid = field.special_values.include?(value) || validator.call(value)
521
531
  raise ScopedSearch::QueryNotSupported, "Value '#{value}' is not valid for field '#{field.field}'" unless valid
522
532
  end
523
533
  end
@@ -1,3 +1,3 @@
1
1
  module ScopedSearch
2
- VERSION = "4.1.5"
2
+ VERSION = "4.1.6"
3
3
  end
@@ -25,6 +25,12 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
25
25
  t.integer :int
26
26
  t.date :date
27
27
  t.integer :unindexed
28
+
29
+ if on_postgresql?
30
+ t.uuid :uuid
31
+ else
32
+ t.string :uuid
33
+ end
28
34
  end
29
35
 
30
36
  class ::Bar < ActiveRecord::Base
@@ -35,7 +41,7 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
35
41
  has_many :bars
36
42
  default_scope { order(:string) }
37
43
 
38
- scoped_search :on => [:string, :date]
44
+ scoped_search :on => [:string, :date, :uuid]
39
45
  scoped_search :on => [:int], :complete_value => true
40
46
  scoped_search :on => :another, :default_operator => :eq, :aliases => [:alias], :complete_value => true
41
47
  scoped_search :on => :explicit, :only_explicit => true, :complete_value => true
@@ -90,6 +96,12 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
90
96
  Foo.complete_for('date ').should =~ (["date = ", "date < ", "date > "])
91
97
  end
92
98
 
99
+ it "should complete the uuid comparators" do
100
+ if on_postgresql?
101
+ Foo.complete_for('uuid ').should =~ (["uuid = ", "uuid != "])
102
+ end
103
+ end
104
+
93
105
  it "should complete when query is already distinct" do
94
106
  Foo.distinct.complete_for('int =').length.should > 0
95
107
  end
@@ -190,7 +202,7 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
190
202
  context 'exceptional search strings' do
191
203
 
192
204
  it "query that starts with 'or'" do
193
- Foo.complete_for('or ').length.should == 9
205
+ Foo.complete_for('or ').length.should == 10
194
206
  end
195
207
 
196
208
  it "value completion with quotes" do
@@ -212,7 +224,7 @@ ScopedSearch::RSpec::Database.test_databases.each do |db|
212
224
  # the string.
213
225
  context 'dotted options in the completion list' do
214
226
  it "query that starts with space should not include the dotted options" do
215
- Foo.complete_for(' ').length.should == 9
227
+ Foo.complete_for(' ').length.should == 10
216
228
  end
217
229
 
218
230
  it "query that starts with the dotted string should include the dotted options" do
@@ -0,0 +1,57 @@
1
+ require "spec_helper"
2
+ require 'securerandom'
3
+
4
+ # These specs will run on all databases that are defined in the spec/database.yml file.
5
+ # Comment out any databases that you do not have available for testing purposes if needed.
6
+ ScopedSearch::RSpec::Database.test_databases.each do |db|
7
+ describe ScopedSearch, "using a #{db} database" do
8
+ before(:all) do
9
+ ScopedSearch::RSpec::Database.establish_named_connection(db)
10
+
11
+ columns = on_postgresql? ? { :uuid => :uuid } : { :uuid => :string }
12
+
13
+ @class = ScopedSearch::RSpec::Database.create_model(columns.merge(:string => :string)) do |klass|
14
+ klass.scoped_search :on => :uuid
15
+ klass.scoped_search :on => :string
16
+ end
17
+ end
18
+
19
+ after(:all) do
20
+ ScopedSearch::RSpec::Database.drop_model(@class)
21
+ ScopedSearch::RSpec::Database.close_connection
22
+ end
23
+
24
+ context 'querying boolean fields' do
25
+
26
+ before(:all) do
27
+ @record1 = @class.create!(:uuid => SecureRandom.uuid)
28
+ @record2 = @class.create!(:uuid => SecureRandom.uuid)
29
+ @record3 = @class.create!(:uuid => SecureRandom.uuid)
30
+ end
31
+
32
+ after(:all) do
33
+ @record1.destroy
34
+ @record2.destroy
35
+ @record3.destroy
36
+ end
37
+
38
+ it "should find the first record" do
39
+ @class.search_for("uuid = #{@record1.uuid}").length.should == 1
40
+ end
41
+
42
+ it "should find two records with negative match" do
43
+ @class.search_for("uuid != #{@record3.uuid}").length.should == 2
44
+ end
45
+
46
+ it "should find a record by just specifying the uuid" do
47
+ @class.search_for(@record1.uuid).first.uuid.should == @record1.uuid
48
+ end
49
+
50
+ it "should not find a record if the uuid is not a valid uuid" do
51
+ if on_postgresql?
52
+ @class.search_for(@record1.uuid[0..-2]).length.should == 0
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -10,6 +10,9 @@ require "#{File.dirname(__FILE__)}/lib/matchers"
10
10
  require "#{File.dirname(__FILE__)}/lib/database"
11
11
  require "#{File.dirname(__FILE__)}/lib/mocks"
12
12
 
13
+ def on_postgresql?
14
+ ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
15
+ end
13
16
 
14
17
  RSpec.configure do |config|
15
18
  config.include ScopedSearch::RSpec::Mocks
@@ -9,6 +9,8 @@ describe ScopedSearch::AutoCompleteBuilder do
9
9
  @definition.stub(:klass).and_return(klass)
10
10
  @definition.stub(:profile).and_return(:default)
11
11
  @definition.stub(:profile=).and_return(true)
12
+ @definition.klass.stub(:connection).and_return(double())
13
+ @definition.stub(:default_order).and_return(nil)
12
14
  end
13
15
 
14
16
  it "should return empty suggestions if the search query is nil" do
@@ -19,6 +21,19 @@ describe ScopedSearch::AutoCompleteBuilder do
19
21
  ScopedSearch::AutoCompleteBuilder.auto_complete(@definition, "").should == []
20
22
  end
21
23
 
24
+ it 'should suggest special values' do
25
+ field = double('field')
26
+ [:temporal?, :set?, :key_field, :validator, :virtual?, :relation, :offset, :value_translation, :to_sql].each { |key| field.stub(key) }
27
+ field.stub(:special_values).and_return %w(foo bar baz)
28
+ field.stub(:complete_value).and_return(true)
29
+ @definition.stub(:default_fields_for).and_return([])
30
+ @definition.stub(:field_by_name).and_return(field)
31
+ @definition.stub(:fields).and_return [field]
32
+ ScopedSearch::AutoCompleteBuilder.any_instance.stub(:complete_value_from_db).and_return([])
33
+ ScopedSearch::AutoCompleteBuilder.auto_complete(@definition, "custom_field =").should eq(['custom_field = foo', 'custom_field = bar', 'custom_field = baz'])
34
+ ScopedSearch::AutoCompleteBuilder.auto_complete(@definition, "custom_field = f").should eq(['custom_field = foo'])
35
+ end
36
+
22
37
  context "with ext_method" do
23
38
  before do
24
39
  @definition = ScopedSearch::Definition.new(klass)
@@ -42,6 +42,7 @@ describe ScopedSearch::QueryBuilder do
42
42
  field.stub(:only_explicit).and_return(true)
43
43
  field.stub(:field).and_return(:test_field)
44
44
  field.stub(:validator).and_return(->(_value) { false })
45
+ field.stub(:special_values).and_return([])
45
46
 
46
47
  @definition.stub(:field_by_name).and_return(field)
47
48
 
@@ -58,6 +59,8 @@ describe ScopedSearch::QueryBuilder do
58
59
  field.stub(:set?).and_return(false)
59
60
  field.stub(:to_sql).and_return('')
60
61
  field.stub(:validator).and_return(->(value) { value =~ /^\d+$/ })
62
+ field.stub(:value_translation).and_return(nil)
63
+ field.stub(:special_values).and_return([])
61
64
 
62
65
  @definition.stub(:field_by_name).and_return(field)
63
66
 
@@ -71,12 +74,48 @@ describe ScopedSearch::QueryBuilder do
71
74
  field.stub(:only_explicit).and_return(true)
72
75
  field.stub(:field).and_return(:test_field)
73
76
  field.stub(:validator).and_return(->(_value) { raise ScopedSearch::QueryNotSupported, 'my custom message' })
77
+ field.stub(:special_values).and_return([])
74
78
 
75
79
  @definition.stub(:field_by_name).and_return(field)
76
80
 
77
81
  lambda { ScopedSearch::QueryBuilder.build_query(@definition, 'test_field = test_val') }.should raise_error('my custom message')
78
82
  end
79
83
 
84
+ context 'with value_translation' do
85
+ let(:translator) do
86
+ ->(value) do
87
+ if %w(a b c).include?(value)
88
+ 'good'
89
+ end
90
+ end
91
+ end
92
+ let(:special_values) { %w(a b c) }
93
+ before do
94
+ field = double('field')
95
+ field.stub(:field).and_return(:test_field)
96
+ field.stub(:key_field).and_return(nil)
97
+ field.stub(:to_sql).and_return('test_field')
98
+ [:virtual?, :set?, :temporal?, :relation, :offset].each { |key| field.stub(key).and_return(false) }
99
+ field.stub(:validator).and_return(->(value) { value == 'x' }) # Nothing except for special_values and x is valid
100
+ field.stub(:special_values).and_return(special_values)
101
+ field.stub(:value_translation).and_return(translator)
102
+ @definition.stub(:field_by_name).and_return(field)
103
+ end
104
+
105
+ it 'should translate the value' do
106
+ ScopedSearch::QueryBuilder.build_query(@definition, 'test_field = a').should eq(conditions: ['(test_field = ?)', 'good'])
107
+ ScopedSearch::QueryBuilder.build_query(@definition, 'test_field ^ (a, b, c)').should eq(conditions: ['(test_field IN (?,?,?))', 'good', 'good', 'good'])
108
+ end
109
+
110
+ it 'should validate before translation' do
111
+ proc { ScopedSearch::QueryBuilder.build_query(@definition, 'test_field = d') }.should raise_error(ScopedSearch::QueryNotSupported, /Value 'd' is not valid for field/)
112
+ end
113
+
114
+ it 'should raise an error if translated value is nil' do
115
+ proc { ScopedSearch::QueryBuilder.build_query(@definition, 'test_field = x') }.should raise_error(ScopedSearch::QueryNotSupported, /Translation from any value to nil is not allowed/)
116
+ end
117
+ end
118
+
80
119
  context "with ext_method" do
81
120
  before do
82
121
  @definition = ScopedSearch::Definition.new(klass)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scoped_search
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.5
4
+ version: 4.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Amos Benari
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2018-09-19 00:00:00.000000000 Z
13
+ date: 2018-11-21 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -120,6 +120,7 @@ files:
120
120
  - spec/integration/set_query_spec.rb
121
121
  - spec/integration/sti_querying_spec.rb
122
122
  - spec/integration/string_querying_spec.rb
123
+ - spec/integration/uuid_query_spec.rb
123
124
  - spec/lib/database.rb
124
125
  - spec/lib/matchers.rb
125
126
  - spec/lib/mocks.rb
@@ -157,7 +158,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
157
158
  version: '0'
158
159
  requirements: []
159
160
  rubyforge_project:
160
- rubygems_version: 2.6.11
161
+ rubygems_version: 2.6.8
161
162
  signing_key:
162
163
  specification_version: 4
163
164
  summary: Easily search you ActiveRecord models with a simple query language using
@@ -177,6 +178,7 @@ test_files:
177
178
  - spec/integration/set_query_spec.rb
178
179
  - spec/integration/sti_querying_spec.rb
179
180
  - spec/integration/string_querying_spec.rb
181
+ - spec/integration/uuid_query_spec.rb
180
182
  - spec/lib/database.rb
181
183
  - spec/lib/matchers.rb
182
184
  - spec/lib/mocks.rb