Skip to content

Commit 7df6daf

Browse files
committed
Add raise_on_blank_find_param resource setting
1 parent e9ee232 commit 7df6daf

File tree

4 files changed

+83
-7
lines changed

4 files changed

+83
-7
lines changed

README.md

+36-5
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,14 @@ u.update_attributes(
5252
c: "d"
5353
)
5454

55-
u.persisted?
55+
u.persisted?
5656
# => true
5757

5858
u.destroy
5959

60-
u.destroyed?
60+
u.destroyed?
6161
# => true
62-
u.persisted?
62+
u.persisted?
6363
# => false
6464

6565
u = MyApi::Person.create(
@@ -164,7 +164,7 @@ module MyApi
164164
class Account < JsonApiClient::Resource
165165
belongs_to :user
166166
end
167-
167+
168168
class Customer < JsonApiClient::Resource
169169
belongs_to :user, shallow_path: true
170170
end
@@ -476,7 +476,7 @@ end
476476
477477
##### Custom status handler
478478
479-
You can change handling of response status using `connection_options`. For example you can override 400 status handling.
479+
You can change handling of response status using `connection_options`. For example you can override 400 status handling.
480480
By default it raises `JsonApiClient::Errors::ClientError` but you can skip exception if you want to process errors from the server.
481481
You need to provide a `proc` which should call `throw(:handled)` default handler for this status should be skipped.
482482
```ruby
@@ -636,6 +636,37 @@ end
636636

637637
```
638638
639+
### Safe singular resource fetching
640+
641+
That is a bit curios, but `json_api_client` returns an array from `.find` method, always.
642+
The history of this fact was discussed [here](https://github.com/JsonApiClient/json_api_client/issues/75)
643+
644+
So, when we searching for a single resource by primary key, we typically write the things like
645+
646+
```ruby
647+
admin = User.find(id).first
648+
```
649+
650+
The next thing which we need to notice - `json_api_client` will just interpolate the incoming `.find` param to the end of API URL, just like that:
651+
652+
> http://somehost/api/v1/users/{id}
653+
654+
What will happen if we pass the blank id (nil or empty string) to the `.find` method then?.. Yeah, `json_api_client` will try to call the INDEX API endpoint instead of SHOW one:
655+
656+
> http://somehost/api/v1/users/
657+
658+
Lets sum all together - in case if `id` comes blank (from CGI for instance), we can silently receive the `admin` variable equal to some existing resource, with all the consequences.
659+
660+
Even worse, `admin` variable can equal to *random* resource, depends on ordering applied by INDEX endpoint.
661+
662+
If you prefer to get `JsonApiClient::Errors::NotFound` raised, please define in your base Resource class:
663+
664+
```ruby
665+
class Resource < JsonApiClient::Resource
666+
self.raise_on_blank_find_param = true
667+
end
668+
```
669+
639670
## Contributing
640671
641672
Contributions are welcome! Please fork this repo and send a pull request. Your pull request should have:

lib/json_api_client/query/builder.rb

+12-2
Original file line numberDiff line numberDiff line change
@@ -86,25 +86,35 @@ def params
8686
end
8787

8888
def to_a
89-
@to_a ||= find
89+
@to_a ||= _fetch
9090
end
9191
alias all to_a
9292

9393
def find(args = {})
94+
if klass.raise_on_blank_find_param && args.blank?
95+
raise Errors::NotFound, 'blank .find param'
96+
end
97+
9498
case args
9599
when Hash
96100
scope = where(args)
97101
else
98102
scope = _new_scope( primary_key: args )
99103
end
100104

101-
klass.requestor.get(scope.params)
105+
scope._fetch
102106
end
103107

104108
def method_missing(method_name, *args, &block)
105109
to_a.send(method_name, *args, &block)
106110
end
107111

112+
protected
113+
114+
def _fetch
115+
klass.requestor.get(params)
116+
end
117+
108118
private
109119

110120
def _new_scope( opts = {} )

lib/json_api_client/resource.rb

+2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class Resource
3636
:keep_request_params,
3737
:search_included_in_result_set,
3838
:custom_type_to_class,
39+
:raise_on_blank_find_param,
3940
instance_accessor: false
4041
class_attribute :add_defaults_to_changes,
4142
instance_writer: false
@@ -54,6 +55,7 @@ class Resource
5455
self.add_defaults_to_changes = false
5556
self.search_included_in_result_set = false
5657
self.custom_type_to_class = {}
58+
self.raise_on_blank_find_param = false
5759

5860
#:underscored_key, :camelized_key, :dasherized_key, or custom
5961
self.json_key_format = :underscored_key

test/unit/query_builder_test.rb

+33
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,39 @@ def test_find_with_args
257257
assert_requested find_stub, times: 1
258258
end
259259

260+
def test_find_with_blank_args
261+
blank_params = [nil, '', {}]
262+
263+
with_altered_config(Article, :raise_on_blank_find_param => true) do
264+
blank_params.each do |blank_param|
265+
assert_raises JsonApiClient::Errors::NotFound do
266+
Article.find(blank_param)
267+
end
268+
end
269+
end
270+
271+
# `.find('')` hits the INDEX URL with trailing slash, the others without one..
272+
collection_stub = stub_request(:get, %r{\Ahttp://example.com/articles/?\z})
273+
.to_return(headers: {content_type: "application/vnd.api+json"}, body: { data: [] }.to_json)
274+
275+
blank_params.each do |blank_param|
276+
Article.find(blank_param)
277+
end
278+
279+
assert_requested collection_stub, times: 3
280+
end
281+
282+
def test_all_on_blank_raising_resource
283+
with_altered_config(Article, :raise_on_blank_find_param => true) do
284+
all_stub = stub_request(:get, "http://example.com/articles")
285+
.to_return(headers: {content_type: "application/vnd.api+json"}, body: { data: [] }.to_json)
286+
287+
Article.all
288+
289+
assert_requested all_stub, times: 1
290+
end
291+
end
292+
260293
def test_build_does_not_propagate_values
261294
query = Article.where(name: 'John').
262295
includes(:author).

0 commit comments

Comments
 (0)