graphiti-api / graphiti-rails Goto Github PK
View Code? Open in Web Editor NEWLicense: MIT License
License: MIT License
Could someone provide a more detailed explanation of the way exception handling works? I added
register_exception ActiveRecord::RecordNotFound, status: 404
to my controller, but it does not seem to be caught. I stepped into it and verified that it is added to a list of handled exceptions, but couldn't find what I need to do for these to actually be rescued.
I previously used rescue_from
, and don't mind using it again, but I would need to produce a standard jsonapi response, and couldn't find a method (in either graphiti
or graphiti-rails
) that would do that. Also couldn't find something that converts object.errors
to such a format. I assume I'm missing something fundamental...
Some things to test:
@wadetandy and I agreed that it could go either way. Waiting for @richmolj to weigh in.
"Fatal" exceptions (5xx) should have different error messages and logging behavior than "expected" (4xx) ones. We've done some handling ourselves for this, but it should probably be discussed further and made more of a first-class concept in Graphiti.
Issue
After updating the namespace key in the .graphiticfg.yml
file and generating a resource, the old API namespacing is still applied to the generated files.
Expected Behavior
If I were to change the namespacing from /api/v1
to /api/v2
in .graphiticfg.yml
, I would expect all of my newly generated files to be prefixed by /api/v2
, not /api/v1
Solution
The API namespace is set upon initial use of the generator. If the install generator is not used, the ApplicationResource
class will be created by the first use of the resource generator. After that, files will be created with the namespacing based on the endpoint_namespace
attribute in the ApplicationResource
class.
If .graphiticfg.yml
is the source of truth, it seems like the ApplicationResource
class should be updated when the namespace is changed.
Let me know if you want help, I'm willing to throw up a quick fix for this!
Right now when debugging exceptions we only use the default handler. That means there's no equivalent of GraphitiErrors.register_exception
where a different handler can be specified. We also don't support the new InvalidRequest
error.
It seems that the stuff here:
graphiti-rails/lib/graphiti/rails.rb
Line 39 in 2b78740
The path
argument to the method containing the line below is often a symbol, which causes the call to fail, eventually resulting in Graphiti raising Graphiti::Errors::InvalidLink
. I suggest adding .to_s
to path
here.
The relevant JSON:API spec is here https://jsonapi.org/format/.
Some key items:
If an endpoint does not support the include parameter, it MUST respond with 400 Bad Request to any requests that include it.
If a server is unable to identify a relationship path or does not support inclusion of resources from a path, it MUST respond with 400 Bad Request.
If the server does not support sorting as specified in the query parameter sort, it MUST return 400 Bad Request.
If a server encounters a query parameter that does not follow the naming conventions above, and the server does not know how to process it as a query parameter from this specification, it MUST return 400 Bad Request.
A server MUST return 403 Forbidden in response to an unsupported request to create a resource with a client-generated ID.
A server MAY return 403 Forbidden in response to an unsupported request to create a resource.
A server MAY reject an attempt to do a full replacement of a to-many relationship. In such a case, the server MUST reject the entire update, and return a 403 Forbidden response.
A server MUST return 404 Not Found when processing a request to modify a resource that does not exist.
A server MUST return 404 Not Found when processing a request that references a related resource that does not exist.
A server MUST respond with 404 Not Found when processing a request to fetch a single resource that does not exist, except when the request warrants a 200 OK response with null as the primary data (as described above).
A server MUST return 404 Not Found when processing a request to fetch a relationship link URL that does not exist.
If a relationship link URL exists but the relationship is empty, then 200 OK MUST be returned, as described above.
A server SHOULD return a 404 Not Found status code if a deletion request fails due to the resource not existing.
A server MUST return 409 Conflict when processing a POST request to create a resource with a client-generated ID that already exists.
A server MUST return 409 Conflict when processing a POST request in which the resource object’s type is not among the type(s) that constitute the collection represented by the endpoint.
A server SHOULD include error details and provide enough information to recognize the source of the conflict.
A server MAY return 409 Conflict when processing a PATCH request to update a resource if that update would violate other server-enforced constraints (such as a uniqueness constraint on a property other than id).
A server MUST return 409 Conflict when processing a PATCH request in which the resource object’s type and id do not match the server’s endpoint.
We are facing a peculiar issue where we override the jsonapi
parser in order to make the incoming payload snake_case
instead of camelCase
.
We use a solution similar to the one proposed here:
graphiti-api/graphiti#286
where at some point we call:
ActionDispatch::Request.parameter_parsers[:jsonapi] = transformer
Now the problem is that graphiti-rails
does the same thing here in the Railtie:
We have this set up in quite a few repos but for one of our repos the initialisation process (for some reason I'm not sure of) is reversed and the order of the calls are as follows:
graphiti-rails
Railtie is called overriding our custom parserI'm not quite sure what determines the order of the Railtie call and our initialisation but I'm thinking that perhaps the gem should be resilient to this and only assign the transformer if one does not already exist, i.e. instead of the current code:
def register_parameter_parser
if ::Rails::VERSION::MAJOR >= 5
ActionDispatch::Request.parameter_parsers[:jsonapi] = PARSER
else
ActionDispatch::ParamsParser::DEFAULT_PARSERS[Mime[:jsonapi]] = PARSER
end
end
run:
def register_parameter_parser
if ::Rails::VERSION::MAJOR >= 5
ActionDispatch::Request.parameter_parsers[:jsonapi] ||= PARSER
else
ActionDispatch::ParamsParser::DEFAULT_PARSERS[Mime[:jsonapi]] ||= PARSER
end
end
Per @wadetandy, this would ideally be rolled up into InvalidRequest
though it may be harder to do. If it isn't rolled up soon we probably want to add the handler for it.
Have a bizarre issue where sideloads randomly stop getting included in responses periodically. It took me hours to debug this. Finally when I all else fails, I restart my server, and bam, they're back.
This is in development
and I haven't actually tried in prod yet as I'm still vetting the library.
Any ideas?
I am loading all my classes in development fyi, because it helps me spot some bugs. Maybe involved?
I believe this may be a deprecated module.
This is something that might be worth doing. We should also consider any potential downsides.
In the current implementation, we register two GraphitiErrors handlers, a default one and an InvalidError one, this mimics the default GraphitiErrors behavior. These handlers will get hit in the event that the request's content type is in Graphiti::Rails.handled_exception_formats
. By default, we only handle JSON:API requests since Rails' handling of these requests isn't compliant with the JSON:API spec. Users can also opt-in to handling for other content types, but this is disabled by default since it is changing the default exception handling behavior in the application.
The outcome of this is that exceptions are not always handled by the GraphitiErrors handlers. A user could choose to register a new handler with Graphiti::Rails.register_exception
but this could lead to a confusing outcome as the handler will not be used when hitting a content type that isn't in Graphiti::Rails.handled_exception_formats
.
One solution is to just tell users to use rescue_from
. However, this has the downside of requiring users to correctly format the response, especially in the case of JSON:API. If we do recommend this solution, we should try to provide some first-class handlers to streamline the process. At the moment, I suggest waiting for some real world feedback to determine what sort of APIs we should provide.
Overwriting existing controllers when generating a resource limits the utility of the gem. It makes it much more difficult to add Graphiti to an existing application.
Pain point
Adding Graphiti to an existing application with generate graphiti:resource [RESOURCE_NAME] --model=[MODEL_NAME]
overwrites the existing controller. You can skip the overwrite, but then the controller code must be written by hand. Also, if you use destroy graphiti:resource...
it deletes the controller file whether the generator initially overwrote the file or not. This inflexibility also increases the difficulty of calling the generator inside other custom generators.
Suggested solutions:
ResourceGenerator
into separate generators and create a single scaffold generator which incorporates all of the generators
--no-controller
).graphiticfg.yml
.Since ActionDispatch::DebugExceptions
logs some error information, we may not need to do any logging ourselves.
Right now, most tests live in the main graphiti repo. We should move the graphiti-rails ones here.
I'm on Rails 7 with latest Graphiti-rails 0.4.0
I can successfully override the api_test templates by placing them in:
lib/templates/graphiti/api_test/{create|show|...}_request_spec.rb.erb
However, dropping in resource.rb.erb or controller.rb.erb in:
lib/templates/graphiti/resource
is not picked up. I've also tried placing them in a templates/
subdir of the above.
I also tried lib/templates/graphiti/controller
for some reason. (I didn't expect that to work since the generator that uses these templates is graphiti:resource
which maps to ResourceGenerator
. But I tried it anyway.)
Am I doing something wrong? Has anyone else successfully used custom resource/controller templates?
There are a few places in the code specifically handling the :jsonapi content type:
cc6492a#diff-057c6c9dd4f8a50d518efafb3a39597bR52 cc6492a#diff-fdb5492c892a0baaa7b246e37a0593cfR19
This is smart, but we'll have to consider :json (and maybe :xml) as well. This is for users who want everything about graphiti, but they just dislike the crufty JSON:API payload. I think even these users want the JSON:API error payload - at worst they would want all the same data rendered slightly differently, but I think matching JSON:API works for now. It might even be a JSON:API gateway drug.
The point in explicitly matching JSON API is that unlike JSON and XML, it has a specific payload format for errors. I think we could definitely choose to have a flag to handle other types specially though.
– @wagenet
The __details__
payload is good because it better matches rails, but has the downside of duplicating the information already present in __raw_error__
. My thought is we should make this a configuration option, disabling __details__
by default.
recognize_path
doesn't allow for much configuration of the request which can be problematic if you have constraints in your router.
https://github.com/graphiti-api/graphiti-rails/blob/master/lib/graphiti/rails/railtie.rb#L134
The level could be a symbol here, which does not respond to zero?
. I suggest replacing it with blank?
Setting the format
in parameters has a special behavior in Rails. One side effect is respond_to
will now ignore any provided Accept header in favor of the provided format. The default Rails json parser doesn't set format and it doesn't seem to me that we need to set it either.
Graphiti's error classes have a lot of message
methods with helpful errors details. But as far as I can tell, graphiti-rails has the rendering of these disabled because it doesn't pass detail: :exception
to rescue-registry
here:
graphiti-rails/lib/graphiti/rails.rb
Lines 46 to 49 in 4a8a065
This results in detail: nil
in the jsonapi errors.
Wanted a place to capture random thoughts while looking over the code and also a place to address questions you ask in your comments.
I have a PostResource with title, upvotes and active as attributes. I'm making POST api call with the data as such
{"post":{
"title": "example1234",
"upvotes": 67,
"active": true
}
}
But the values for title, upvotes and active are always saves as NULL in the database. Can you help me solve this.
Here is my controller create action
def create
post_params = params.require(:post).permit(:title, :upvotes, :active)
@post = PostResource.build(post_params)
byebug
if @post.save
render jsonapi: @post, status: 201
else
render jsonapi_errors: @post
end
end
And here is output from debugger
(byebug) post_params <ActionController::Parameters {"title"=>"example1234", "upvotes"=>67, "active"=>true} permitted: true> (byebug) @post Post Load (1.3ms) SELECT "posts".* FROM "posts" LIMIT ? OFFSET ? [["LIMIT", 11], ["OFFSET", 0]] ↳ app/controllers/api/v1/posts_controller.rb:16:in
create' Post Load (1.0ms) SELECT "posts".* FROM "posts" LIMIT ? [["LIMIT", 11]] ↳ app/controllers/api/v1/posts_controller.rb:16:in create' #<Graphiti::ResourceProxy:0x00007f3c38c11378 @resource=#<PostResource:0x00007f3c38c05af0 @adapter=#<Graphiti::Adapters::ActiveRecord:0x00007f3c38c04e70 @resource=#<PostResource:0x00007f3c38c05af0 ...>>>, @scope=#<Graphiti::Scope:0x00007f3c38c13b50 @object=#<ActiveRecord::Relation [#<Post id: 1, title: "My title", upvotes: 10, active: true, created_at: "2020-03-02 12:44:55", updated_at: "2020-03-02 12:44:55">, #<Post id: 2, title: "Another title", upvotes: 20, active: false, created_at: "2020-03-02 12:44:55", updated_at: "2020-03-02 12:44:55">, #<Post id: 3, title: "OMG! A title", upvotes: 30, active: true, created_at: "2020-03-02 12:44:55", updated_at: "2020-03-02 12:44:55">, #<Post id: 4, title: "Dynamo", upvotes: 10, active: true, created_at: "2020-03-02 12:44:55", updated_at: "2020-03-02 12:44:55">, #<Post id: 5, title: "Panda", upvotes: 20, active: false, created_at: "2020-03-02 12:44:55", updated_at: "2020-03-02 12:44:55">, #<Post id: 6, title: "Mortal", upvotes: 30, active: true, created_at: "2020-03-02 12:44:55", updated_at: "2020-03-02 12:44:55">, #<Post id: 7, title: nil, upvotes: nil, active: nil, created_at: "2020-03-03 09:11:05", updated_at: "2020-03-03 09:11:05">, #<Post id: 8, title: nil, upvotes: nil, active: nil, created_at: "2020-03-03 09:13:07", updated_at: "2020-03-03 09:13:07">, #<Post id: 9, title: nil, upvotes: nil, active: nil, created_at: "2020-03-03 09:16:35", updated_at: "2020-03-03 09:16:35">, #<Post id: 10, title: nil, upvotes: nil, active: nil, created_at: "2020-03-03 09:21:10", updated_at: "2020-03-03 09:21:10">, ...]>, @resource=#<PostResource:0x00007f3c38c05af0 @adapter=#<Graphiti::Adapters::ActiveRecord:0x00007f3c38c04e70 @resource=#<PostResource:0x00007f3c38c05af0 ...>>>, @query=#<Graphiti::Query:0x00007f3c38c041c8 @resource=#<PostResource:0x00007f3c38c05af0 @adapter=#<Graphiti::Adapters::ActiveRecord:0x00007f3c38c04e70 @resource=#<PostResource:0x00007f3c38c05af0 ...>>>, @association_name=nil, @params={:title=>"example1234", :upvotes=>67, :active=>true}, @include_param=nil, @parents=[], @filters={}, @pagination={}, @fields={}, @extra_fields={}, @stats={}, @include_directive=#<JSONAPI::IncludeDirective:0x00007f3c38c138f8 @hash={}, @options={}>, @include_hash={}, @sideloads={}, @sideload_hash={}, @hash={}>, @opts={}, @unpaginated_object=#<ActiveRecord::Relation [#<Post id: 1, title: "My title", upvotes: 10, active: true, created_at: "2020-03-02 12:44:55", updated_at: "2020-03-02 12:44:55">, #<Post id: 2, title: "Another title", upvotes: 20, active: false, created_at: "2020-03-02 12:44:55", updated_at: "2020-03-02 12:44:55">, #<Post id: 3, title: "OMG! A title", upvotes: 30, active: true, created_at: "2020-03-02 12:44:55", updated_at: "2020-03-02 12:44:55">, #<Post id: 4, title: "Dynamo", upvotes: 10, active: true, created_at: "2020-03-02 12:44:55", updated_at: "2020-03-02 12:44:55">, #<Post id: 5, title: "Panda", upvotes: 20, active: false, created_at: "2020-03-02 12:44:55", updated_at: "2020-03-02 12:44:55">, #<Post id: 6, title: "Mortal", upvotes: 30, active: true, created_at: "2020-03-02 12:44:55", updated_at: "2020-03-02 12:44:55">, #<Post id: 7, title: nil, upvotes: nil, active: nil, created_at: "2020-03-03 09:11:05", updated_at: "2020-03-03 09:11:05">, #<Post id: 8, title: nil, upvotes: nil, active: nil, created_at: "2020-03-03 09:13:07", updated_at: "2020-03-03 09:13:07">, #<Post id: 9, title: nil, upvotes: nil, active: nil, created_at: "2020-03-03 09:16:35", updated_at: "2020-03-03 09:16:35">, #<Post id: 10, title: nil, upvotes: nil, active: nil, created_at: "2020-03-03 09:21:10", updated_at: "2020-03-03 09:21:10">, ...]>>, @query=#<Graphiti::Query:0x00007f3c38c041c8 @resource=#<PostResource:0x00007f3c38c05af0 @adapter=#<Graphiti::Adapters::ActiveRecord:0x00007f3c38c04e70 @resource=#<PostResource:0x00007f3c38c05af0 ...>>>, @association_name=nil, @params={:title=>"example1234", :upvotes=>67, :active=>true}, @include_param=nil, @parents=[], @filters={}, @pagination={}, @fields={}, @extra_fields={}, @stats={}, @include_directive=#<JSONAPI::IncludeDirective:0x00007f3c38c138f8 @hash={}, @options={}>, @include_hash={}, @sideloads={}, @sideload_hash={}, @hash={}>, @payload=#<Graphiti::Deserializer:0x00007f3c38c05410 @payload={}, @attributes={}, @relationships={}>, @single=true, @raise_on_missing=true> (byebug) c (0.2ms) begin transaction ↳ app/controllers/api/v1/posts_controller.rb:16:in
create' Post Create (2.1ms) INSERT INTO "posts" ("created_at", "updated_at") VALUES (?, ?) [["created_at", "2020-03-04 12:26:49.819298"], ["updated_at", "2020-03-04 12:26:49.819298"]] ↳ app/controllers/api/v1/posts_controller.rb:16:in create' (21.1ms) commit transaction ↳ app/controllers/api/v1/posts_controller.rb:16:in
create' Completed 201 Created in 43800ms (Views: 1.4ms | ActiveRecord: 27.5ms | Allocations: 54753) `
rescue_from
bypasses all default error handling. Ideally, we'd provide a nice API to call from this.
Work has begun here: https://github.com/wagenet/graphiti-rails/blob/master/lib/graphiti/rails/railtie.rb#L6.
For reference, this is the list of registered exceptions in my current app (without graphiti-rails):
{"ActionController::RoutingError"=>:not_found,
"AbstractController::ActionNotFound"=>:not_found,
"ActionController::MethodNotAllowed"=>:method_not_allowed,
"ActionController::UnknownHttpMethod"=>:method_not_allowed,
"ActionController::NotImplemented"=>:not_implemented,
"ActionController::UnknownFormat"=>:not_acceptable,
"ActionController::InvalidAuthenticityToken"=>:unprocessable_entity,
"ActionController::InvalidCrossOriginRequest"=>:unprocessable_entity,
"ActionDispatch::Http::Parameters::ParseError"=>:bad_request,
"ActionController::BadRequest"=>:bad_request,
"ActionController::ParameterMissing"=>:bad_request,
"Rack::QueryParser::ParameterTypeError"=>:bad_request,
"Rack::QueryParser::InvalidParameterError"=>:bad_request,
"ActiveRecord::RecordNotFound"=>:not_found,
"ActiveRecord::StaleObjectError"=>:conflict,
"ActiveRecord::RecordInvalid"=>:unprocessable_entity,
"ActiveRecord::RecordNotSaved"=>:unprocessable_entity,
"Survey::NotFound"=>:not_found,
"Survey::PreconditionNotMet"=>:unprocessable_entity}
# == Schema Information
#
# Table name: posts
#
# id :uuid not null, primary key
# content :text not null
# created_at :datetime not null
# updated_at :datetime not null
# user_id :uuid not null
#
# Indexes
#
# index_posts_on_user_id (user_id)
#
# Foreign Keys
#
# fk_rails_... (user_id => users.id)
#
bin/rails generate graphiti:resource Post --model=Post --rawid
Could not find type :text! This was specified on attribute :content within resource PostResource (Graphiti::Errors::TypeNotFound)
Valid types are: [:integer_id, :uuid, :string_enum, :integer_enum, :string, :integer, :big_decimal, :float, :boolean, :date, :datetime, :hash, :array, :array_of_integer_ids, :array_of_uuids, :array_of_string_enums, :array_of_integer_enums, :array_of_strings, :array_of_integers, :array_of_big_decimals, :array_of_floats, :array_of_dates, :array_of_datetimes]
I'm trying out a deep query filter and it's not working as I expect.
Here are my resources:
class RideResource < ApplicationResource
attribute :requested_start_time, :datetime
attribute :ride_completion_status, :string
attribute :ride_type, :string
belongs_to :account
belongs_to :transport_type
belongs_to :rider
end
class TransportTypeResource < ApplicationResource
attribute :name, :string
attribute :short_name, :string
attribute :full_name, :string
has_many :rides
end
Assuming there is no transport type resource with name "test123," I would expect the following request to return no results, but it's returning all the ride resources:
/api/v1/rides?include=transport_type&filter[transport_type.name]=test123
Is this a bug or do I need to configure my resources to behave the way I expect? I looked at the documentation and didn't see anything helpful.
Right now we're auto-including Graphiti code in ActionController::Base
source. This may be too aggressive.
First question is whether this has any potential downsides. If so, we should definitely let it be at least configurable. My inclination is to leave it for now and have us be less aggressive if people report issues.
It looks like it is set to nil
by default except for "API-only" apps where it is set to :api
. When set to :api
it renders errors in the requested content type. The default behavior is to always render as HTML. For now, I think the correct solution is to call it out in our docs.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.