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 +4 -4
- data/README.md +24 -315
- data/lib/procore-sift.rb +2 -2
- data/lib/sift/filter.rb +3 -3
- data/lib/sift/filtrator.rb +1 -1
- data/lib/sift/parameter.rb +3 -2
- data/lib/sift/type_validator.rb +3 -2
- data/lib/sift/validators/valid_int_validator.rb +12 -2
- data/lib/sift/value_parser.rb +28 -9
- data/lib/sift/version.rb +1 -1
- data/lib/sift/where_handler.rb +22 -1
- metadata +39 -24
- data/MIT-LICENSE +0 -20
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 596ec26cb7543f8577252a68435106152cbff901d51a327c8dfa633644c1d69d
|
|
4
|
+
data.tar.gz: 4bd86a6fd6c758227c22716aa50fd1a46e0cb816213a7ecac20c1f64b6de6f81
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 24b0c037aaf2f0d1633a70ecbc9e719f2546d368d797dbcb19baeac89d9ecd936e0b1c72cb3089b2707b0998dd23ce36bd70b9a53fe01ebce812208ff79b59a8
|
|
7
|
+
data.tar.gz: c91c06e910abc752bd41e2345a5e528418a80405692d9db6e7a81006886331e4b984f89dbd212809c8171ee8dbc927159da8b9a532559cd06b177a80b6e753fe
|
data/README.md
CHANGED
|
@@ -1,297 +1,45 @@
|
|
|
1
1
|
# Sift
|
|
2
2
|
|
|
3
|
-
[](https://github.com/procore/sift/actions/workflows/test.yml)
|
|
4
4
|
|
|
5
|
-
A
|
|
5
|
+
A declarative DSL for building filters and sorts with Rails and Active Record.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Usage
|
|
8
8
|
|
|
9
|
-
Include Sift in your controllers
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
+
private
|
|
95
30
|
|
|
96
|
-
def
|
|
97
|
-
render json:
|
|
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
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
```bash
|
|
312
|
-
$ gem install procore-sift
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
## Without Rails
|
|
57
|
+
## Documentation
|
|
316
58
|
|
|
317
|
-
|
|
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
|
-
##
|
|
64
|
+
## Changelog
|
|
320
65
|
|
|
321
|
-
|
|
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
|
data/lib/sift/filtrator.rb
CHANGED
data/lib/sift/parameter.rb
CHANGED
|
@@ -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
|
data/lib/sift/type_validator.rb
CHANGED
|
@@ -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
|
data/lib/sift/value_parser.rb
CHANGED
|
@@ -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] =
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
data/lib/sift/where_handler.rb
CHANGED
|
@@ -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} @> ?",
|
|
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.
|
|
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:
|
|
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: '
|
|
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: '
|
|
26
|
+
version: '7.2'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
|
-
name:
|
|
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:
|
|
42
|
+
name: minitest
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
44
44
|
requirements:
|
|
45
|
-
- - "
|
|
45
|
+
- - "<"
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: '6
|
|
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
|
|
54
|
+
version: '6'
|
|
55
55
|
- !ruby/object:Gem::Dependency
|
|
56
|
-
name:
|
|
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:
|
|
70
|
+
name: rails
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
|
72
72
|
requirements:
|
|
73
|
-
- -
|
|
73
|
+
- - ">="
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
|
-
version:
|
|
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:
|
|
82
|
+
version: '7.0'
|
|
83
83
|
- !ruby/object:Gem::Dependency
|
|
84
|
-
name:
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
171
|
+
rubygems_version: 3.5.22
|
|
157
172
|
signing_key:
|
|
158
173
|
specification_version: 4
|
|
159
|
-
summary:
|
|
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.
|