GithubHelp home page GithubHelp logo

graphiti-rails's People

Contributors

dependabot[bot] avatar leipeleon avatar mkamensky avatar mrhardikjoshi avatar richmolj avatar superslau avatar wadetandy avatar wagenet avatar wilsonsilva avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

graphiti-rails's Issues

Investigate `debug_exception_response_format`

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.

Ensure JSON:API spec compliance for exceptions

The relevant JSON:API spec is here https://jsonapi.org/format/.

Some key items:

400

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

403

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

404

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

406

  • Servers MUST respond with a 406 Not Acceptable status code if a request’s Accept header contains the JSON:API media type and all instances of that media type are modified with media type parameters.

409

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

415

  • Servers MUST respond with a 415 Unsupported Media Type status code if a request specifies the header Content-Type: application/vnd.api+json with any media type parameters.

422

  • In the responses ... with a 422 status code, 400 Bad Request would also be acceptable. (More details.) JSON:API doesn’t take a position on 400 vs. 422.

Also relevant

  • When a server encounters multiple problems for a single request, the most generally applicable HTTP error code SHOULD be used in the response. For instance, 400 Bad Request might be appropriate for multiple 4xx errors or 500 Internal Server Error might be appropriate for multiple 5xx errors.

Notes & Questions

Wanted a place to capture random thoughts while looking over the code and also a place to address questions you ask in your comments.

Add config option to omit __details__

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.

Unable to override resource or controller templates

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?

Move more tests here

Right now, most tests live in the main graphiti repo. We should move the graphiti-rails ones here.

Write Tests

Some things to test:

  • Basic requests still work
  • All JSON API errors are rendered in correct format
  • Graphiti errors are handled correctly

My attribute values are saved as NULL

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

Revisit "fatal" exception handling

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

ResourceGenerator overwrites existing controller

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:

  • Break out the responsibilities of the ResourceGenerator into separate generators and create a single scaffold generator which incorporates all of the generators
    • The scaffold generator could have options to exclude specific generators (e.g. --no-controller)
  • Instead of overwriting controllers, create separate namespaced controllers specifically for Graphiti. The file structure could be sourced from the namespace recorded in .graphiticfg.yml.

First-class support for custom handlers

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.

Sideloads randomly stop getting included in responses

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?

Generated file namespacing incorrect after updating namespace

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!

Possibly provide detailed error messages for non-JSON API responses

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.

@richmolj

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

Add exceptions to ExceptionWrapper

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}

Feature Tracking for Initial Release

  • Register exceptions with Rails #4
  • Use either default exception handler or InvalidRequest handler
  • Use Graphiti handler only for JSON API by default, with opt-in for other content types

Deep Query Filter

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.

Attribute text not support in generator

Schema Information for model Post:

# == 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)
#

Command

bin/rails generate graphiti:resource Post --model=Post --rawid

Output

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]

Decide if auto-include is correct

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.

Custom `:jsonapi` parameter_parsers being overriden by `graphiti-rails`

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:

ActionDispatch::Request.parameter_parsers[:jsonapi] = PARSER

which then overrides our custom implementation if called after our custom initialisation.

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:

  1. Our custom initialiser is called
  2. graphiti-rails Railtie is called overriding our custom parser

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

Exception handling

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

Error responses missing exception message details

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:

register_exception Graphiti::Errors::InvalidRequest, status: 400, handler: Graphiti::Rails::InvalidRequestHandler
register_exception Graphiti::Errors::RecordNotFound, status: 404, handler: Graphiti::Rails::ExceptionHandler
register_exception Graphiti::Errors::RemoteWrite, status: 400, handler: Graphiti::Rails::ExceptionHandler
register_exception Graphiti::Errors::SingularSideload, status: 400, handler: Graphiti::Rails::ExceptionHandler

This results in detail: nil in the jsonapi errors.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.