procore-sift 1.0.0 → 1.2.0

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
  SHA256:
3
- metadata.gz: 1594b92ce7a641195d2c67b981f3f138648566196d8e56d80d0089689dd7a7b3
4
- data.tar.gz: 97efc53749c62bdaad7b5888c5108c52ccf0c091af730a7ab12a9c7bee3d5f3a
3
+ metadata.gz: 596ec26cb7543f8577252a68435106152cbff901d51a327c8dfa633644c1d69d
4
+ data.tar.gz: 4bd86a6fd6c758227c22716aa50fd1a46e0cb816213a7ecac20c1f64b6de6f81
5
5
  SHA512:
6
- metadata.gz: 302c1147b6c5402449a4999492f176aceedf6648180673b5710fb21b522501e8de58945868dd487e286e0de7a403e8c6669431cbcf9565fa42c0b54041da5ffd
7
- data.tar.gz: 0d050ee3c4fbeb0a270cb5994efcdd71f926901f3c2db1b79b38d241a17ebe14428b386de10f8b128885c331b334d20b5fedd4b3d153d047bf7a6f17bd9774bf
6
+ metadata.gz: 24b0c037aaf2f0d1633a70ecbc9e719f2546d368d797dbcb19baeac89d9ecd936e0b1c72cb3089b2707b0998dd23ce36bd70b9a53fe01ebce812208ff79b59a8
7
+ data.tar.gz: c91c06e910abc752bd41e2345a5e528418a80405692d9db6e7a81006886331e4b984f89dbd212809c8171ee8dbc927159da8b9a532559cd06b177a80b6e753fe
data/README.md CHANGED
@@ -1,297 +1,45 @@
1
1
  # Sift
2
2
 
3
- [![Build Status](https://travis-ci.org/procore/sift.svg?branch=master)](https://travis-ci.org/procore/sift)
3
+ [![Tests](https://github.com/procore/sift/actions/workflows/test.yml/badge.svg)](https://github.com/procore/sift/actions/workflows/test.yml)
4
4
 
5
- A tool to build your own filters and sorts with Rails and Active Record!
5
+ A declarative DSL for building filters and sorts with Rails and Active Record.
6
6
 
7
- ## Developer Usage
7
+ ## Usage
8
8
 
9
- Include Sift in your controllers, and define some filters.
9
+ Include Sift in your controllers and define filters and sorts:
10
10
 
11
11
  ```ruby
12
12
  class PostsController < ApplicationController
13
13
  include Sift
14
14
 
15
15
  filter_on :title, type: :string
16
+ filter_on :priority, type: :int
17
+ filter_on :published_at, type: :datetime
18
+ filter_on :with_body, type: :scope
16
19
 
17
- def index
18
- render json: filtrate(Post.all)
19
- end
20
- end
21
- ```
22
-
23
- This will allow users to pass `?filters[title]=foo` and get the `Post`s with the title `foo`.
24
-
25
- Sift will also handle rendering errors using the standard rails errors structure. You can add this to your controller by adding,
26
-
27
- ```ruby
28
- before_action :render_filter_errors, unless: :filters_valid?
29
-
30
- def render_filter_errors
31
- render json: { errors: filter_errors }, status: :bad_request && return
32
- end
33
- ```
34
-
35
- to your controller.
36
-
37
- These errors are based on the type that you told sift your param was.
38
-
39
- ### Filter Types
40
-
41
- Every filter must have a type, so that Sift knows what to do with it. The current valid filter types are:
42
-
43
- - int - Filter on an integer column
44
- - decimal - Filter on a decimal column
45
- - boolean - Filter on a boolean column
46
- - string - Filter on a string column
47
- - text - Filter on a text column
48
- - date - Filter on a date column
49
- - time - Filter on a time column
50
- - datetime - Filter on a datetime column
51
- - scope - Filter on an ActiveRecord scope
52
- - jsonb - Filter on a jsonb column (supported only in PostgreSQL)
53
-
54
- ### Filter on Scopes
55
-
56
- Just as your filter values are used to scope queries on a column, values you
57
- pass to a scope filter will be used as arguments to that scope. For example:
20
+ sort_on :title, type: :string
21
+ sort_on :priority, type: :int
58
22
 
59
- ```ruby
60
- class Post < ActiveRecord::Base
61
- scope :with_body, ->(text) { where(body: text) }
62
- end
63
-
64
- class PostsController < ApplicationController
65
- include Sift
66
-
67
- filter_on :with_body, type: :scope
23
+ before_action :render_filter_errors, unless: :filters_valid?
68
24
 
69
25
  def index
70
26
  render json: filtrate(Post.all)
71
27
  end
72
- end
73
- ```
74
-
75
- Passing `?filters[with_body]=my_text` will call the `with_body` scope with
76
- `my_text` as the argument.
77
-
78
- Scopes that accept no arguments are currently not supported.
79
-
80
- #### Accessing Params with Filter Scopes
81
-
82
- Filters with `type: :scope` have access to the params hash by passing in the desired keys to the `scope_params`. The keys passed in will be returned as a hash with their associated values.
83
-
84
- ```ruby
85
- class Post < ActiveRecord::Base
86
- scope :user_posts_on_date, ->(date, options) {
87
- where(user_id: options[:user_id], blog_id: options[:blog_id], date: date)
88
- }
89
- end
90
-
91
- class UsersController < ApplicationController
92
- include Sift
93
28
 
94
- filter_on :user_posts_on_date, type: :scope, scope_params: [:user_id, :blog_id]
29
+ private
95
30
 
96
- def show
97
- render json: filtrate(Post.all)
31
+ def render_filter_errors
32
+ render json: { errors: filter_errors }, status: :bad_request
98
33
  end
99
34
  end
100
35
  ```
101
36
 
102
- Passing `?filters[user_posts_on_date]=10/12/20` will call the `user_posts_on_date` scope with
103
- `10/12/20` as the the first argument, and will grab the `user_id` and `blog_id` out of the params and pass them as a hash, as the second argument.
104
-
105
- ### Renaming Filter Params
106
-
107
- A filter param can have a different field name than the column or scope. Use `internal_name` with the correct name of the column or scope.
108
-
109
- ```ruby
110
- class PostsController < ApplicationController
111
- include Sift
37
+ Consumers can then filter and sort via query parameters:
112
38
 
113
- filter_on :post_id, type: :int, internal_name: :id
114
-
115
- end
116
39
  ```
117
-
118
- ### Filter on Ranges
119
-
120
- Some parameter types support ranges. Ranges are expected to be a string with the bounding values separated by `...`
121
-
122
- For example `?filters[price]=3...50` would return records with a price between 3 and 50.
123
-
124
- The following types support ranges
125
-
126
- - int
127
- - decimal
128
- - boolean
129
- - date
130
- - time
131
- - datetime
132
-
133
- ### Mutating Filters
134
-
135
- Filters can be mutated before the filter is applied using the `tap` argument. This is useful, for example, if you need to adjust the time zone of a `datetime` range filter.
136
-
137
- ```ruby
138
-
139
- class PostsController < ApplicationController
140
- include Sift
141
-
142
- filter_on :expiration, type: :datetime, tap: ->(value, params) {
143
- value.split("...").
144
- map do |str|
145
- str.to_date.in_time_zone(LOCAL_TIME_ZONE)
146
- end.
147
- join("...")
148
- }
149
- end
40
+ GET /posts?filters[title]=hello&filters[priority]=1...5&sort=-published_at,title
150
41
  ```
151
42
 
152
- ### Filter on jsonb column
153
-
154
- Usually JSONB columns stores values as an Array or an Object (key-value), in both cases the parameter needs to be sent in a JSON format
155
-
156
- **Array**
157
-
158
- It should be sent an array in the URL Query parameters
159
-
160
- - `?filters[metadata]=[1,2]`
161
-
162
- **key-value**
163
-
164
- It can be passed one or more Key values:
165
-
166
- - `?filters[metadata]={"data_1":"test"}`
167
- - `?filters[metadata]={"data_1":"test","data_2":"[1,2]"}`
168
-
169
- When the value is an array, it will filter records with those values or more, for example:
170
-
171
- - `?filters[metadata]={"data_2":"[1,2]"}`
172
-
173
- Will return records with next values stored in the JSONB column `metadata`:
174
-
175
- ```ruby
176
- { data_2: 1 }
177
- { data_2: 2 }
178
- { data_2: [1] }
179
- { data_2: [2] }
180
- { data_2: [1,2] }
181
- { data_2: [1,2,3] }
182
- ```
183
-
184
- When the `null` value is included in the array, it will return also all the records without any value in that property, for example:
185
-
186
- - `?filters[metadata]={"data_2":"[false,null]"}`
187
-
188
- Will return records with next values stored in the JSONB column `metadata`:
189
-
190
- ```ruby
191
- { data_2: null }
192
- { data_2: false }
193
- { data_2: [false] }
194
- { data_1: {another: 'information'} } # When the JSONB key "data_2" is not set.
195
- ```
196
-
197
- ### Filter on JSON Array
198
-
199
- `int` type filters support sending the values as an array in the URL Query parameters. For example `?filters[id]=[1,2]`. This is a way to keep payloads smaller for GET requests. When URI encoded this will become `filters%5Bid%5D=%5B1,2%5D` which is much smaller the standard format of `filters%5Bid%5D%5B%5D=1&&filters%5Bid%5D%5B%5D=2`.
200
-
201
- On the server side, the params will be received as:
202
-
203
- ```ruby
204
- # JSON array encoded result
205
- "filters"=>{"id"=>"[1,2]"}
206
-
207
- # standard array format
208
- "filters"=>{"id"=>["1", "2"]}
209
- ```
210
-
211
- Note that this feature cannot currently be wrapped in an array and should not be used in combination with sending array parameters individually.
212
-
213
- - `?filters[id][]=[1,2]` => invalid
214
- - `?filters[id][]=[1,2]&filters[id][]=3` => invalid
215
- - `?filters[id]=[1,2]&filters[id]=3` => valid but only 3 is passed through to the server
216
- - `?filters[id]=[1,2]` => valid
217
-
218
- #### A note on encoding for JSON Array feature
219
-
220
- JSON arrays contain the reserved characters "`,`" and "`[`" and "`]`". When encoding a JSON array in the URL there are two different ways to handle the encoding. Both ways are supported by Rails.
221
- For example, lets look at the following filter with a JSON array `?filters[id]=[1,2]`:
222
-
223
- - `?filters%5Bid%5D=%5B1,2%5D`
224
- - `?filters%5Bid%5D%3D%5B1%2C2%5D`
225
-
226
- In both cases Rails will correctly decode to the expected result of
227
-
228
- ```ruby
229
- { "filters" => { "id" => "[1,2]" } }
230
- ```
231
-
232
- ### Sort Types
233
-
234
- Every sort must have a type, so that Sift knows what to do with it. The current valid sort types are:
235
-
236
- - int - Sort on an integer column
237
- - decimal - Sort on a decimal column
238
- - string - Sort on a string column
239
- - text - Sort on a text column
240
- - date - Sort on a date column
241
- - time - Sort on a time column
242
- - datetime - Sort on a datetime column
243
- - scope - Sort on an ActiveRecord scope
244
-
245
- ### Sort on Scopes
246
-
247
- Just as your sort values are used to scope queries on a column, values you
248
- pass to a scope sort will be used as arguments to that scope. For example:
249
-
250
- ```ruby
251
- class Post < ActiveRecord::Base
252
- scope :order_on_body_no_params, -> { order(body: :asc) }
253
- scope :order_on_body, ->(direction) { order(body: direction) }
254
- scope :order_on_body_then_id, ->(body_direction, id_direction) { order(body: body_direction).order(id: id_direction) }
255
- end
256
-
257
- class PostsController < ApplicationController
258
- include Sift
259
-
260
- sort_on :order_by_body_ascending, internal_name: :order_on_body_no_params, type: :scope
261
- sort_on :order_by_body, internal_name: :order_on_body, type: :scope, scope_params: [:direction]
262
- sort_on :order_by_body_then_id, internal_name: :order_on_body_then_id, type: :scope, scope_params: [:direction, :asc]
263
-
264
-
265
- def index
266
- render json: filtrate(Post.all)
267
- end
268
- end
269
- ```
270
-
271
- `scope_params` takes an order-specific array of the scope's arguments. Passing in the param :direction allows the consumer to choose which direction to sort in (ex. `-order_by_body` will sort `:desc` while `order_by_body` will sort `:asc`)
272
-
273
- Passing `?sort=-order_by_body` will call the `order_on_body` scope with
274
- `:desc` as the argument. The direction is the only argument that the consumer has control over.
275
- Passing `?sort=-order_by_body_then_id` will call the `order_on_body_then_id` scope where the `body_direction` is `:desc`, and the `id_direction` is `:asc`. Note: in this example the user has no control over id_direction. To demonstrate:
276
- Passing `?sort=order_by_body_then_id` will call the `order_on_body_then_id` scope where the `body_direction` this time is `:asc`, but the `id_direction` remains `:asc`.
277
-
278
- Scopes that accept no arguments are currently supported, but you should note that the user has no say in which direction it will sort on.
279
-
280
- `scope_params` can also accept symbols that are keys in the `params` hash. The value will be fetched and passed on as an argument to the scope.
281
-
282
- ## Consumer Usage
283
-
284
- Filter:
285
- `?filters[<field_name>]=<value>`
286
-
287
- Filters are translated to Active Record `where`s and are chained together. The order they are applied is not guarenteed.
288
-
289
- Sort:
290
- `?sort=-published_at,position`
291
-
292
- Sort is translated to Active Record `order` The sorts are applied in the order they are passed by the client.
293
- the `-` symbol means to sort in `desc` order. By default, keys are sorted in `asc` order.
294
-
295
43
  ## Installation
296
44
 
297
45
  Add this line to your application's Gemfile:
@@ -306,58 +54,20 @@ And then execute:
306
54
  $ bundle
307
55
  ```
308
56
 
309
- Or install it yourself as:
310
-
311
- ```bash
312
- $ gem install procore-sift
313
- ```
314
-
315
- ## Without Rails
57
+ ## Documentation
316
58
 
317
- We have some future plans to remove the rails dependency so that other frameworks such as Sinatra could leverage this gem.
59
+ - [Filters](docs/filters.md) - Filter types, scopes, ranges, JSONB, defaults, validation
60
+ - [Sorts](docs/sorts.md) - Sort types and scope-based sorting
61
+ - [Consumer API](docs/api.md) - Query parameter format for API consumers
62
+ - [Contributing](docs/contributing.md) - Development setup and publishing
318
63
 
319
- ## Contributing
64
+ ## Changelog
320
65
 
321
- Installing gems before running tests:
322
-
323
- ```bash
324
- $ bundle exec appraisal install
325
- ```
326
-
327
- Running tests:
328
-
329
- ```bash
330
- $ bundle exec appraisal rake test
331
- ```
332
-
333
- ## Publishing
334
-
335
- Publishing is done use the `gem` commandline tool. You must have permissions to publish a new version. Users with permissions can be seen here https://rubygems.org/gems/procore-sift.
336
-
337
- When a bump is desired, the gemspec should have the version number bumped and merged into master.
338
-
339
- Step 1: build the new version
340
- `gem build sift.gemspec`
341
-
342
- ```
343
- Successfully built RubyGem
344
- Name: procore-sift
345
- Version: 0.14.0
346
- File: procore-sift-0.14.0.gem
347
- ```
348
-
349
- Step2: Push the updated build
350
- `gem push procore-sift-0.14.0.gem`
351
-
352
- ```
353
- Pushing gem to https://rubygems.org...
354
- Successfully registered gem: procore-sift (0.14.0)
355
- ```
66
+ See [CHANGELOG.md](CHANGELOG.md) for release notes.
356
67
 
357
68
  ## License
358
69
 
359
- The gem is available as open source under the terms of the [MIT
360
- License](http://opensource.org/licenses/MIT).
70
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
361
71
 
362
72
  ## About Procore
363
73
 
@@ -371,5 +81,4 @@ The Procore Gem is maintained by Procore Technologies.
371
81
 
372
82
  Procore - building the software that builds the world.
373
83
 
374
- Learn more about the #1 most widely used construction management software at
375
- [procore.com](https://www.procore.com/)
84
+ Learn more about the #1 most widely used construction management software at [procore.com](https://www.procore.com/)
data/lib/procore-sift.rb CHANGED
@@ -66,8 +66,8 @@ module Sift
66
66
  end
67
67
 
68
68
  class_methods do
69
- def filter_on(parameter, type:, internal_name: parameter, default: nil, validate: nil, scope_params: [], tap: nil)
70
- filters << Filter.new(parameter, type, internal_name, default, validate, scope_params, tap)
69
+ def filter_on(parameter, type:, internal_name: parameter, default: nil, validate: nil, scope_params: [], tap: nil, allow_nil: false)
70
+ filters << Filter.new(parameter, type, internal_name, default, validate, scope_params, tap, allow_nil: allow_nil)
71
71
  end
72
72
 
73
73
  def filters
data/lib/sift/filter.rb CHANGED
@@ -4,8 +4,8 @@ module Sift
4
4
  class Filter
5
5
  attr_reader :parameter, :default, :custom_validate, :scope_params
6
6
 
7
- def initialize(param, type, internal_name, default, custom_validate = nil, scope_params = [], tap = ->(value, _params) { value })
8
- @parameter = Parameter.new(param, type, internal_name)
7
+ def initialize(param, type, internal_name, default, custom_validate = nil, scope_params = [], tap = ->(value, _params) { value }, allow_nil: false)
8
+ @parameter = Parameter.new(param, type, internal_name, allow_nil: allow_nil)
9
9
  @default = default
10
10
  @custom_validate = custom_validate
11
11
  @scope_params = scope_params
@@ -41,7 +41,7 @@ module Sift
41
41
  end
42
42
 
43
43
  def type_validator
44
- @type_validator ||= Sift::TypeValidator.new(param, type)
44
+ @type_validator ||= Sift::TypeValidator.new(param, type, allow_nil: parameter.allow_nil)
45
45
  end
46
46
 
47
47
  def type
@@ -46,7 +46,7 @@ module Sift
46
46
 
47
47
  def active_filters
48
48
  filters.select do |filter|
49
- filter_params[filter.param].present? || filter.default || filter.always_active?
49
+ filter_params.include?(filter.param) || filter.default || filter.always_active?
50
50
  end
51
51
  end
52
52
  end
@@ -1,12 +1,13 @@
1
1
  module Sift
2
2
  # Value Object that wraps some handling of filter params
3
3
  class Parameter
4
- attr_reader :param, :type, :internal_name
4
+ attr_reader :param, :type, :internal_name, :allow_nil
5
5
 
6
- def initialize(param, type, internal_name = param)
6
+ def initialize(param, type, internal_name = param, allow_nil: false)
7
7
  @param = param
8
8
  @type = type
9
9
  @internal_name = internal_name
10
+ @allow_nil = allow_nil
10
11
  end
11
12
 
12
13
  def parse_options
@@ -17,9 +17,10 @@ module Sift
17
17
  :scope,
18
18
  :jsonb].freeze
19
19
 
20
- def initialize(param, type)
20
+ def initialize(param, type, allow_nil: false)
21
21
  @param = param
22
22
  @type = type
23
+ @allow_nil = allow_nil
23
24
  end
24
25
 
25
26
  attr_reader :param, :type
@@ -46,7 +47,7 @@ module Sift
46
47
  private
47
48
 
48
49
  def valid_int?
49
- { valid_int: true }
50
+ @allow_nil ? { valid_int: { allow_null_token: true } } : { valid_int: true }
50
51
  end
51
52
  end
52
53
  end
@@ -1,4 +1,6 @@
1
1
  class ValidIntValidator < ActiveModel::EachValidator
2
+ NULL_TOKEN = "null"
3
+
2
4
  def validate_each(record, attribute, value)
3
5
  record.errors.add attribute, (options[:message] || "must be integer, array of integers, or range") unless
4
6
  valid_int?(value)
@@ -6,8 +8,12 @@ class ValidIntValidator < ActiveModel::EachValidator
6
8
 
7
9
  private
8
10
 
11
+ def allow_null_token?
12
+ options.fetch(:allow_null_token, false)
13
+ end
14
+
9
15
  def valid_int?(value)
10
- integer_array?(value) || integer_or_range?(value)
16
+ (allow_null_token? && null_token?(value)) || integer_array?(value) || integer_or_range?(value)
11
17
  end
12
18
 
13
19
  def integer_array?(value)
@@ -15,10 +21,14 @@ class ValidIntValidator < ActiveModel::EachValidator
15
21
  value = Sift::ValueParser.new(value: value).array_from_json
16
22
  end
17
23
 
18
- value.is_a?(Array) && value.any? && value.all? { |v| integer_or_range?(v) }
24
+ value.is_a?(Array) && value.any? && value.all? { |v| integer_or_range?(v) || (allow_null_token? && null_token?(v)) }
19
25
  end
20
26
 
21
27
  def integer_or_range?(value)
22
28
  !!(/\A\d+(...\d+)?\z/ =~ value.to_s)
23
29
  end
30
+
31
+ def null_token?(value)
32
+ value.to_s.downcase == NULL_TOKEN
33
+ end
24
34
  end
@@ -35,7 +35,7 @@ module Sift
35
35
  parsed_jsonb.each_with_object({}) do |key_value, hash|
36
36
  key = key_value.first
37
37
  value = key_value.last
38
- hash[key] = value.is_a?(String) ? parse_json(value) : value
38
+ hash[key] = parse_jsonb_value(value)
39
39
  end
40
40
  end
41
41
 
@@ -53,9 +53,25 @@ module Sift
53
53
  attr_reader :value, :type, :supports_boolean, :supports_json, :supports_json_object, :supports_ranges
54
54
 
55
55
  def parse_as_range?(raw_value=value)
56
+ # jsonb values must not be parsed as a top-level range: the outer value is
57
+ # a Hash (nested params) or a JSON object string, and any "..." range lives
58
+ # inside an individual key's value, which is handled by parse_jsonb_value.
59
+ return false if raw_value.is_a?(Hash) || supports_json_object
60
+
56
61
  supports_ranges && raw_value.to_s.include?("...")
57
62
  end
58
63
 
64
+ # Parses a single value from a jsonb object. A "..." string is converted into
65
+ # a Range (with its endpoints normalized to DateTimes where possible) so the
66
+ # range handling lives here rather than in WhereHandler; other strings may be
67
+ # nested JSON (e.g. an embedded array) and are parsed accordingly.
68
+ def parse_jsonb_value(raw_value)
69
+ return raw_value unless raw_value.is_a?(String)
70
+ return date_range(raw_value) if raw_value.include?("...")
71
+
72
+ parse_json(raw_value)
73
+ end
74
+
59
75
  def range_value
60
76
  Range.new(*value.split("..."))
61
77
  end
@@ -84,15 +100,18 @@ module Sift
84
100
  from_date_string, end_date_string = raw_value.split("...")
85
101
  return unless end_date_string
86
102
 
87
- parsed_dates = [from_date_string, end_date_string].map do |date_string|
88
- begin
89
- DateTime.parse(date_string.to_s)
90
- rescue StandardError
91
- date_string
92
- end
93
- end
103
+ [from_date_string, end_date_string].map { |date_string| parse_date(date_string) }.join("...")
104
+ end
105
+
106
+ def date_range(raw_value)
107
+ from_date_string, end_date_string = raw_value.split("...")
108
+ Range.new(parse_date(from_date_string), parse_date(end_date_string))
109
+ end
94
110
 
95
- parsed_dates.join("...")
111
+ def parse_date(date_string)
112
+ DateTime.parse(date_string.to_s)
113
+ rescue StandardError
114
+ date_string
96
115
  end
97
116
  end
98
117
  end
data/lib/sift/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Sift
2
- VERSION = "1.0.0".freeze
2
+ VERSION = "1.2.0".freeze
3
3
  end
@@ -1,5 +1,7 @@
1
1
  module Sift
2
2
  class WhereHandler
3
+ NULL_TOKEN = "null"
4
+
3
5
  def initialize(param)
4
6
  @param = param
5
7
  end
@@ -7,6 +9,8 @@ module Sift
7
9
  def call(collection, value, _params, _scope_params)
8
10
  if @param.type == :jsonb
9
11
  apply_jsonb_conditions(collection, value)
12
+ elsif @param.allow_nil
13
+ apply_null_aware_conditions(collection, value)
10
14
  else
11
15
  collection.where(@param.internal_name => value)
12
16
  end
@@ -14,8 +18,23 @@ module Sift
14
18
 
15
19
  private
16
20
 
21
+ def apply_null_aware_conditions(collection, value)
22
+ values = Array(value)
23
+ has_null = values.any? { |v| v.to_s.downcase == NULL_TOKEN }
24
+ non_null_values = values.reject { |v| v.to_s.downcase == NULL_TOKEN }
25
+
26
+ if has_null && non_null_values.any?
27
+ collection.where(@param.internal_name => nil)
28
+ .or(collection.where(@param.internal_name => non_null_values))
29
+ elsif has_null
30
+ collection.where(@param.internal_name => nil)
31
+ else
32
+ collection.where(@param.internal_name => value)
33
+ end
34
+ end
35
+
17
36
  def apply_jsonb_conditions(collection, value)
18
- return collection.where("#{@param.internal_name} @> ?", val.to_s) if value.is_a?(Array)
37
+ return collection.where("#{@param.internal_name} @> ?", value.to_s) if value.is_a?(Array)
19
38
 
20
39
  value.each do |key, val|
21
40
  collection = if val.is_a?(Array)
@@ -26,6 +45,8 @@ module Sift
26
45
  "#{@param.internal_name}->>'#{key}' #{element === nil ? 'IS NULL' : "= :value_#{i}"}"
27
46
  end.join(' OR ')
28
47
  collection.where("(#{main_condition}) OR (#{sub_conditions})", elements)
48
+ elsif val.is_a?(Range)
49
+ collection.where("#{@param.internal_name}->>'#{key}' BETWEEN ? AND ?", val.first, val.last)
29
50
  else
30
51
  collection.where("#{@param.internal_name}->>'#{key}' = ?", val.to_s)
31
52
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: procore-sift
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Procore Technologies
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-04-03 00:00:00.000000000 Z
11
+ date: 2026-07-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,16 +16,16 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '6.1'
19
+ version: '7.2'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '6.1'
26
+ version: '7.2'
27
27
  - !ruby/object:Gem::Dependency
28
- name: pry
28
+ name: appraisal
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
@@ -39,21 +39,21 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: rails
42
+ name: minitest
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ">="
45
+ - - "<"
46
46
  - !ruby/object:Gem::Version
47
- version: '6.1'
47
+ version: '6'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ">="
52
+ - - "<"
53
53
  - !ruby/object:Gem::Version
54
- version: '6.1'
54
+ version: '6'
55
55
  - !ruby/object:Gem::Dependency
56
- name: rake
56
+ name: pry
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
@@ -67,21 +67,21 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: rubocop
70
+ name: rails
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - '='
73
+ - - ">="
74
74
  - !ruby/object:Gem::Version
75
- version: 0.71.0
75
+ version: '7.0'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - '='
80
+ - - ">="
81
81
  - !ruby/object:Gem::Version
82
- version: 0.71.0
82
+ version: '7.0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: sqlite3
84
+ name: rake
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - ">="
@@ -95,7 +95,21 @@ dependencies:
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
97
  - !ruby/object:Gem::Dependency
98
- name: appraisal
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.68'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.68'
111
+ - !ruby/object:Gem::Dependency
112
+ name: sqlite3
99
113
  requirement: !ruby/object:Gem::Requirement
100
114
  requirements:
101
115
  - - ">="
@@ -108,14 +122,14 @@ dependencies:
108
122
  - - ">="
109
123
  - !ruby/object:Gem::Version
110
124
  version: '0'
111
- description: Easily write arbitrary filters
125
+ description: A declarative DSL for building filters and sorts with Rails and Active
126
+ Record
112
127
  email:
113
128
  - dev@procore.com
114
129
  executables: []
115
130
  extensions: []
116
131
  extra_rdoc_files: []
117
132
  files:
118
- - MIT-LICENSE
119
133
  - README.md
120
134
  - Rakefile
121
135
  - lib/procore-sift.rb
@@ -137,7 +151,8 @@ files:
137
151
  homepage: https://github.com/procore/sift
138
152
  licenses:
139
153
  - MIT
140
- metadata: {}
154
+ metadata:
155
+ allowed_push_host: https://rubygems.org
141
156
  post_install_message:
142
157
  rdoc_options: []
143
158
  require_paths:
@@ -146,15 +161,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
146
161
  requirements:
147
162
  - - ">="
148
163
  - !ruby/object:Gem::Version
149
- version: 2.7.0
164
+ version: 3.3.0
150
165
  required_rubygems_version: !ruby/object:Gem::Requirement
151
166
  requirements:
152
167
  - - ">="
153
168
  - !ruby/object:Gem::Version
154
169
  version: '0'
155
170
  requirements: []
156
- rubygems_version: 3.4.8
171
+ rubygems_version: 3.5.22
157
172
  signing_key:
158
173
  specification_version: 4
159
- summary: Summary of Sift.
174
+ summary: Build dynamic filters and sorts for Rails and Active Record
160
175
  test_files: []
data/MIT-LICENSE DELETED
@@ -1,20 +0,0 @@
1
- Copyright 2016 Adam Hess
2
-
3
- Permission is hereby granted, free of charge, to any person obtaining
4
- a copy of this software and associated documentation files (the
5
- "Software"), to deal in the Software without restriction, including
6
- without limitation the rights to use, copy, modify, merge, publish,
7
- distribute, sublicense, and/or sell copies of the Software, and to
8
- permit persons to whom the Software is furnished to do so, subject to
9
- the following conditions:
10
-
11
- The above copyright notice and this permission notice shall be
12
- included in all copies or substantial portions of the Software.
13
-
14
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.