scoped_search 4.1.5 → 4.1.6

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 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