Giter Club home page Giter Club logo

jsonapi.rb's Introduction

JSONAPI.rb ๐Ÿ”Œ

Build Status

So you say you need JSON:API support in your API...

  • hey how did your hackathon go?
  • not too bad, we got Babel set up
  • yepโ€ฆ
  • yep.

โ€” I Am Devloper

Here are some codes to help you build your next JSON:API compliable application easier and faster.

But why?

It's quite a hassle to setup a Ruby (Rails) web application to use and follow the JSON:API specifications.

The idea is simple, JSONAPI.rb offers a bunch of modules/mixins/glue, add them to your controllers, call some methods, profit!

Main goals:

  • No magic please
  • No DSLs please
  • Less code, less maintenance
  • Good docs and test coverage
  • Keep it up-to-date (or at least tell people this is for grabs)

The available features include:

But how?

Mainly by leveraging JSON:API Serializer and Ransack.

Thanks to everyone who worked on these amazing projects!

Sponsors

I'm grateful for the following companies for supporting this project!

Installation

Add this line to your application's Gemfile:

gem 'jsonapi.rb'

And then execute:

$ bundle

Or install it yourself as:

$ gem install jsonapi.rb

Usage


To enable the support for Rails, add this to an initializer:

# config/initializers/jsonapi.rb
require 'jsonapi'

JSONAPI::Rails.install!

This will register the mime type and the jsonapi and jsonapi_errors renderers.

Object serialization

The jsonapi renderer will try to guess and resolve the serializer class based on the object class, and if it is a collection, based on the first item in the collection.

The naming scheme follows the ModuleName::ClassNameSerializer for an instance of the ModuleName::ClassName.

Please follow the JSON:API Serializer guide on how to define a serializer.

To provide a different naming scheme implement the jsonapi_serializer_class method in your resource or application controller.

Here's an example:

class CustomNamingController < ActionController::Base

  # ...

  private

  def jsonapi_serializer_class(resource, is_collection)
    JSONAPI::Rails.serializer_class(resource, is_collection)
  rescue NameError
    # your serializer class naming implementation
  end
end

To provide extra parameters to the serializer, implement the jsonapi_serializer_params method.

Here's an example:

class CustomSerializerParamsController < ActionController::Base

  # ...

  private

  def jsonapi_serializer_params
    {
      first_name_upcase: params[:upcase].present?
    }
  end
end

Collection meta

To provide meta information for a collection, provide the jsonapi_meta controller method.

Here's an example:

class MyController < ActionController::Base
  def index
    render jsonapi: Model.all
  end

  private

  def jsonapi_meta(resources)
    { total: resources.count } if resources.respond_to?(:count)
  end
end

Error handling

JSONAPI::Errors provides a basic error handling. It will generate a valid error response on exceptions from strong parameters, on generic errors or when a record is not found.

To render the validation errors, just pass it to the error renderer.

To use an exception notifier, overwrite the render_jsonapi_internal_server_error method in your controller.

Here's an example:

class MyController < ActionController::Base
  include JSONAPI::Errors

  def update
    record = Model.find(params[:id])

    if record.update(params.require(:data).require(:attributes).permit!)
      render jsonapi: record
    else
      render jsonapi_errors: record.errors, status: :unprocessable_entity
    end
  end

  private

  def render_jsonapi_internal_server_error(exception)
    # Call your exception notifier here. Example:
    # Raven.capture_exception(exception)
    super(exception)
  end
end

Includes and sparse fields

JSONAPI::Fetching provides support on inclusion of related resources and serialization of only specific fields.

Here's an example:

class MyController < ActionController::Base
  include JSONAPI::Fetching

  def index
    render jsonapi: Model.all
  end

  private

  # Overwrite/whitelist the includes
  def jsonapi_include
    super & ['wanted_attribute']
  end
end

This allows you to run queries like:

$ curl -X GET /api/resources?fields[model]=model_attr,relationship

Filtering and sorting

JSONAPI::Filtering uses the power of Ransack to filter and sort over a collection of records. The support is pretty extended and covers also relationships and composite matchers.

Please add ransack to your Gemfile in order to benefit from this functionality!

Here's an example:

class MyController < ActionController::Base
  include JSONAPI::Filtering

  def index
    allowed = [:model_attr, :relationship_attr]

    jsonapi_filter(Model.all, allowed) do |filtered|
      render jsonapi: filtered.result
    end
  end
end

This allows you to run queries like:

$ curl -X GET \
  /api/resources?filter[model_attr_or_relationship_attr_cont_any]=value,name\
  &sort=-model_attr,relationship_attr

Sorting using expressions

You can use basic aggregations like min, max, avg, sum and count when sorting. This is an optional feature since SQL aggregations require grouping. To enable expressions along with filters, use the option flags:

options = { sort_with_expressions: true }
jsonapi_filter(User.all, allowed_fields, options) do |filtered|
  render jsonapi: filtered.result.group('id').to_a
end

This allows you to run queries like:

$ curl -X GET /api/resources?sort=-model_attr_sum

Pagination

JSONAPI::Pagination provides support for paginating model record sets as long as enumerables.

Here's an example:

class MyController < ActionController::Base
  include JSONAPI::Pagination

  def index
    jsonapi_paginate(Model.all) do |paginated|
      render jsonapi: paginated
    end
  end

end

This will generate the relevant pagination links.

If you want to add the pagination information to your meta, use the jsonapi_pagination_meta method:

  def jsonapi_meta(resources)
    pagination = jsonapi_pagination_meta(resources)

    { pagination: pagination } if pagination.present?
  end

If you want to change the default number of items per page or define a custom logic to handle page size, use the jsonapi_page_size method:

  def jsonapi_page_size(pagination_params)
    per_page = pagination_params[:size].to_f.to_i
    per_page = 30 if per_page > 30 || per_page < 1
    per_page
  end

Deserialization

JSONAPI::Deserialization provides a helper to transform a JSONAPI document into a flat dictionary that can be used to update an ActiveRecord::Base model.

Here's an example using the jsonapi_deserialize helper:

class MyController < ActionController::Base
  include JSONAPI::Deserialization

  def update
    model = MyModel.find(params[:id])

    if model.update(jsonapi_deserialize(params, only: [:attr1, :rel_one]))
      render jsonapi: model
    else
      render jsonapi_errors: model.errors, status: :unprocessable_entity
    end
  end
end

The jsonapi_deserialize helper accepts the following options:

  • only: returns exclusively attributes/relationship data in the provided list
  • except: returns exclusively attributes/relationship which are not in the list
  • polymorphic: will add and detect the _type attribute and class to the defined list of polymorphic relationships

This functionality requires support for inflections. If your project uses active_support or rails you don't need to do anything. Alternatively, we will try to load a lightweight alternative to active_support/inflector provided by the dry/inflector gem, please make sure it's added if you want to benefit from this feature.

Development

After checking out the repo, run bundle to install dependencies.

Then, run rake spec to run the tests.

To install this gem onto your local machine, run bundle exec rake install.

To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/stas/jsonapi.rb

This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

License

The gem is available as open source under the terms of the MIT License.

jsonapi.rb's People

Contributors

coffeejunk avatar dependabot[bot] avatar dmolesuc avatar ekampp avatar finnlawrence avatar fluxsaas avatar gabrielsandoval avatar gagalago avatar jf avatar jkorz avatar mamhoff avatar marclerodrigues avatar mylescc avatar petergoldstein avatar stas avatar xhs345 avatar ydakuka avatar zwolf 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  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  avatar  avatar  avatar  avatar  avatar

jsonapi.rb's Issues

Rails 7.0 compatibility

Expected Behavior

It should work

Actual Behavior

Still waiting compatible with rails 7

Steps to Reproduce the Problem

  1. Using rails 7

Specifications

  • Version: 1.7
  • Ruby version: 3.03

Issues using custom scopes

Hey, thanks for the great work on this gem.

I'm trying to figure out how to get custom scopes to work, but it seems like this method in filtering.rb is stripping them out

wants_array = predicates.any? && predicates.map(&:wants_array).any?
if to_filter.is_a?(String) && wants_array
to_filter = to_filter.split(',')
end
if predicates.any? && (field_names - allowed_fields).empty?
filtered[requested_field] = to_filter
end

Is there a reason that filter options without predicates are removed? I have a scope called awaiting_review_by_user_id. I've added it to the ransackable scopes, and that works fine, but the method above removes it anyways. Is there an easy way to get this scope working? Locally removing the predicates.any? check fixed my issue, but maybe I'm missing something that's already built in.

allow any field for allow fields?

I'm used to using ransack but this is the first time I've been using it through jsonapi.rb. I'm a bit annoyed that I have to set what fields I can allow filtering on. I guess I could do this:

jsonapi_filter(User.all, User.column_names, options) do |filtered|
  render jsonapi: result.group('id').to_a
end

But then if I set up a ransack scope to filter with I'd have to combine it with this. I guess I just want allowed_fields to be an optional field (maybe default nil?) and then if it's nil, don't restrict what can be filtered on.

Are there any qualms about this? I could setup a PR to allow for that, doesn't look like it'd be too involved.

Let `code` be usable in error objects response

Expected Behavior

That I can set the code in the error response objects

Actual Behavior

I can only set these attributes:

{"status"=>nil, "source"=>nil, "title"=>nil, "detail"=>nil}

Steps to Reproduce the Problem

  1. use render jsonapi_errors: { code: "some_code" }

I think it all comes from here:

[:status, :source, :title, :detail].each do |attr_name|

and according to the spec I should be able to do this:
https://jsonapi.org/format/#error-objects

maybe it should also support meta, id and links as well when you supply something that isn't an active model to jsonapi_errors ?

JSONAPI::Rails.is_collection? confused by 'size' attribute

Expected Behavior

  • Single model instance with size attribute is recognized as not a collection

Actual Behavior

  • Single model instance with size attribute mistakenly believed to be a collection
    due to this heuristic in JSONAPI::Rails.is_collection?
  • Serialization fails with NoMethodError when attempting to call any? here in JSONAPI::Rails.add_renderer!

Steps to Reproduce the Problem

  1. Create a model with an attribute size of type string.
  2. Try to serialize it, e.g. with render jsonapi: @the_instance in a controller method.

Possible solutions

Consider using resource.is_a?(Enumerable) instead of respond_to?(:any?). This seems to be what jsonapi-serializer does now after first realizing size was a problem and trying each instead, then deciding that was more trouble than it was worth.

Specifications

  • Version: 1.7.0
  • Ruby version: 3.0.3
  • jsonapi-serializer version: 2.2.0

warning to install ransack polute logs

Expected Behavior

no log about ransack if we don't use filtering and don't have ransack installed

Actual Behavior

we have a warning when this file is loaded by jsonapi.rb gem

Steps to Reproduce the Problem

  1. start the app without ransack gem installed
  2. look at logs

Proposition of solution

One way to fix this is to not load this file on the gem and let the user loading it when they need it (as already explained in the readme). That will need to bump the major version of the gem but should not affect people that already followed the readme.

What do you think? do you see another way to fix it? it's not a bug but a feature and you prefer to keep it as is?

Potentially simple improvement to increase flexibility

This line

if many && !resource.any?
triggers a query to check for an empty collection: any? == exists? (i.e. select 1), which removes scopes containing select attached to the chain, and throwing db error when creating using virtual field names in the select later referenced in the same query (i.e. where filter).

Proposed non-breaking simple solution: instead of any?, use limit(1).present? so rails doesn't do its magic conversion to a select 1 query.

Case-aware deserialization support

json-api clients adhering to the 1.0 recommendations may be likely to use dasherized (e.g. password-confirmation) keys.
json-api clients adhering to the 1.1 recommendations may be likely to use camelCase (e.g. passwordConfirmation) keys.
The JSONAPI::Deserialization jsonapi_deserialize method does not seem to offer modification to key names. It would be handy if it could (optionally) underscore-ify (snake-case) these keys, (e.g. password_confirmation) to conform to Rails expectations.

Filter and Paginate

Expected Behavior

Based on the docs, I figured that one would filter first, then paginate:

     jsonapi_filter(resource_class.all, allowed_fields, options) do |filtered|
        jsonapi_paginate(filtered) do |paginated|
          render jsonapi: paginated, status: :ok
        end
      end

Actual Behavior

When I do this, I get this exception:

Minitest::UnexpectedError: NoMethodError: undefined method `size' for Ransack::Search<class: Article, base: Grouping <combinator: and>>:Ransack::Search

perhaps it's related to this commit 3a31499

Steps to Reproduce the Problem

Code above โ˜๏ธ

Specifications

  • Version: 1.6.0
  • Ruby version: 2.7.2

Filtering on relational table properties

First off. This is by far the best JSON:API tool I have found. I salute you!

Secondly, I have a user table, which has email and name, I also have an access table, which stores access logs.

The user serializer has the latest login time as a first-class property, but on a db level it's not a first-class property.

What's the recommended way that I go about Using JSONAPI:Filtering (and in turn Ransack) to filter on related properties?

Based on this section of the Ransack Readme, it looks like I could use filter[access_created_at_match].

But I would like to separate the presentation layer (where it's a first-class property) from the database layer (where it's an association). I would like this separation to extend to the parameters the client can send to my resource endpoints.

Thanks in advance.

Mongoid Issue

Hello,

Love this idea and this project. Thank you for working on it. While installing I ran into a problem and I can't seem to figure it out.

  • I installed the jsonapi.rb gem
  • Then installed the ransack-mongoid lib gem 'ransack-mongoid', github: 'activerecord-hackery/ransack-mongoid'

Then when I start the rails server i get the error:
lib/jsonapi/patches.rb:37:in 'alias_method': undefined method 'visit_Ransack_Nodes_Sort' for class 'Ransack::Visitor' (NameError)

Which look like its here: https://github.com/stas/jsonapi.rb/blob/master/lib/jsonapi/patches.rb#L37.

Because I'm using mongoid i have active_record turned off. Could that be part of the problem?
Newish to rails and unfamiliar with ransack so any direction or ideas would be appreciated.
Thanks!

Sparse fields don't work with non-single-word types

Expected Behavior

Requests for a sparse fieldset to a resource with a type that includes two words, e.g. personAlias, should only return the specified fields.

Example request:

GET /person-aliases?fields[personAlias]=myField

Expected response:

{
    "data": [
        {
            "id": "1",
            "type": "personAlias",
            "attributes": {
                "myField": "foo"
            }
       }
    ]
}

Note that:

  • I'm using jsonapi.rb alongside jsonapi-serializer
  • My serializers have the option set_key_transform :camel_lower
  • I've manually set the the type in my serializers e.g. set_type :person_alias
  • A request with the type given in snake_case, e.g. GET /person-aliases?fields[person_alias]=myField or GET /person-aliases?fields[person_alias]=my_field, does not work either

If I manually set a one-word type, e.g. set_type :foo, things work as expected.

Actual Behavior

The response includes all fields, not the sparse fieldset requested, for example:

{
    "data": [
        {
            "id": "1",
            "type": "personAlias",
            "attributes": {
                "myField": "foo",
                "myOtherField": "foo",
                "anotherOne": "foo",
            }
       }
    ]
}

Steps to Reproduce the Problem

  1. In the serializer, add options set_type :foo_bar and set_key_transform :camel_lower
  2. In the controller, include JSONAPI::Fetching etc. and render with render jsonapi: result, status: :ok

Specifications

  • Version: 1.7.0
  • Ruby version: 2.7.1

Plain ruby examples

Expected Behavior

Will like the readme to have plain ruby examples

Actual Behavior

The readme looks very rails oriented

Steps to Reproduce the Problem

None

Specifications

  • Version:
  • Ruby version:

Invalid filters return unfiltered results

Expected Behavior

When making requests with an invalid filter, I would expect to have an error raised.
This can be set on Ransack with the following configuration:

Ransack.configure do |config|
  # Raise errors if a query contains an unknown predicate or attribute.
  # Default is true (do not raise error on unknown conditions).
  config.ignore_unknown_conditions = false
end

Although this does not seem to work.

Actual Behavior

When making a request with an invalid filter, the filter is not applied at all,
payments?filter[id_eqblabla]=1,2,3,4,5 will return all payments. This is far from ideal.

This happens regardless of the configuration above. JSONAPI::Filtering configures the opposite value (not sure what value has precedence over the other):

Ransack.configure do |config|
# Raise errors if a query contains an unknown predicate or attribute.
# Default is true (do not raise error on unknown conditions).
config.ignore_unknown_conditions = true

I have tried to change that line and see what happens as well without success.
Not sure if this is an issue with the way JSONAPI integrates with Ransack, or something not working as expected on Ransack.

Steps

  • Already described above.

Specifications

  • Version: latest
  • Ruby version: 2.6.6

Possible bug? render jsonapi returning error: undefined method `any?' for object

Apologies for a second issue in a few days. This one is probably something I'm doing wrong, but I don't know how to fix it and hoping someone can help.

I'm using render jsonapi: @record across every show, create, and update action in my API, and it works great, except for in one controller.

I have a model Media::Image and a serializer Media::ImageSerializer. Here's the serializer:

class Media::ImageSerializer < ApplicationSerializer
  set_type :image

  attribute :original do |record|
    record.file.url.presence if record.file.url
  end

  attribute :thumb do |record|
    record.file.versions[:th].url.presence if record.file.versions
  end

  attributes  :kind, :title, :description, :height, :width, :size, :year,
              :date, :usage_type, :attributed_name, :attributed_url,
              :source_url, :slug

  belongs_to  :uploaded_by, serializer: UserSerializer
  belongs_to  :media_license, serializer: Media::LicenseSerializer
  belongs_to  :place, serializer: PlaceSerializer
  has_many    :articles, serializer: ArticleSerializer

  meta do |record|
    base_meta(record)
  end
end

Expected Behavior

When setting @record = Media::Image.find(some_id) and calling render jsonapi: @record, status: :ok, the serializer should serialize the resource.

Actual Behavior

What actually happens is that an error is thrown, e.g.

NoMethodError: undefined method `any?' for #<Media::Image:0x00007fa11c436218>

Even if I remove every attribute and relationship from the serializer, I still get the error.

I can render collections just fine though.

I also have a Media::LicenseSerializer, and a corresponding model, which work as they should, so I don't think this is a naming issue.

Steps to Reproduce the Problem

I don't know how to reproduce this. This is just happening with one model & serializer, and I don't know why.

The method in jsonapi.rb that uses any? seem to be add_renderer!. But I'm not sure why this particular controller/serializer is acting this way. Could it be the renderer thinks it's dealing with a collection for some reason?

Right now I'm having to use render json: Media::ImageSerializer.new(@record).serializable_hash.to_json for single records, which means I can't use fields or include queries.

Specifications

  • Version: 1.7.0
  • Ruby version: 2.7.1

Filter not working

I am using this url http://localhost:3000/api/v2/posts?filter[id]=1.

But its retrun all posts in my database.

I have defind @allowed_filtered_fields = %i[id number]

And i have this code that excute at the end but filtered.result retrun all resources

     jsonapi_filter(@resources, @allowed_filtered_fields) do |filtered|
          @resource_count = filtered.result.count
          jsonapi_paginate(filtered.result) do |paginated|
            render jsonapi: paginated
          end
        end

I also noted that the output of jsonapi_filter_params function is {}

Any help please ?

  • Version: 1.7
  • Ruby version: 2.7.2
  • Rails 6.0.3

Off-by-one error when paginating an array

resources = resources[(offset)..(offset + limit)]

When paginating based on an array, the line above should read:

resources = resources[(offset)..(offset + limit - 1)]

otherwise there will always be an extra element in each page.

To give a trivial example, if you were using this to paginate with a page size of 1, starting on page 2:

> resources = [0,1,2,3,4,5,6,7,8,9,10]
=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
> offset = 1
=> 1
> limit = 1
=> 1
> resources[(offset)..(offset + limit)]
=> [1, 2]

however:

> resources[(offset)..(offset + limit - 1)]
=> [1]

When using OJ, colons prepended to keys in the response

Hello, first of all thank you very much for your dedication in maintaining this project.
I'm trying to hook up OJ, by following the example shown in jsonapi-serializer repository, but for some reason the response looks weird. Could you please point me in the right direction?

Expected Behavior

{
  "data": {
    "id": "999",
    "type": "user"
  }
}

Actual Behavior

{
  ":data": {
    ":id": "999",
    ":type": "user"
  }
}

Steps to Reproduce the Problem

  1. Extend from BaseSerializer (https://github.com/jsonapi-serializer/jsonapi-serializer/blob/master/docs/json_serialization.md)
  2. Invoke render jsonapi: model

Specifications

  • Version: 1.7.0
  • Ruby version: 2.5.8

filter using scope doesn't work

Expected Behavior

?filter[status]=active should only return active bookings only

and the Ransack::Search
should return Ransack::Search<class: Booking, scope: {"status"=>"active"}, base: Grouping <combinator: and>>
(need to return the same ransack search object when using Ransack directly using this code`

Booking.ransack(status: "active")

Actual Behavior

Return all bookings and the ransack search object return this Ransack::Search<class: Booking, base: Grouping <combinator: and>>
without scope: {"status"=>"active"}, so the endpoints return all bookings without scope

TheController

  def index
    allowed_fields = %i[
      status
      created_at
      updated_at
    ]

    bookings = Booking.where(user: current_user)
    jsonapi_filter(bookings, allowed_fields) do |filtered|
      jsonapi_paginate(filtered.result) do |paginated|
        render jsonapi: paginated
      end
    end
  end

Error on `jsonapi_pagination_meta` when using a grouped collection

Expected Behavior

Grouped resources pagination should pass

Actual Behavior

Got error

undefined method `to_f' for {2=>5, 3=>6, 1=>2}:Hash
Did you mean?  to_s
               to_a
               to_h

Steps to Reproduce the Problem

  1. include JSONAPI::Pagination
  2. implement jsonapi_meta
  def jsonapi_meta(resources)
  pagination = jsonapi_pagination_meta(resources)

  { pagination: pagination } if pagination.present?
end
  1. Add collection which grouped Model.left_joins(:another_models).group(:some_model_attribute).select("models.*, COUNT(another_models.id) AS custom_attribute")

Specifications

  • JSONAPI Version: 1.7.0
  • Rails Version: 6.1.3
  • Ruby version: 3.0.0

Suggestion

I think it's better to use .length instead .size, so it will work with grouped resources

total = resources.unscope(:limit, :offset, :order).size

same with here

total ||= resources.size

Includes don't seem to get paginated?

If you set up pagination on your primary resource, it gets paginated fine. Now imagine that someone includes a relationship in the query, and that result set is in the 1000s.

What I see happening right now is that the top level resource is paginated correctly, but any included fields are fetched in total with no pagination whatsoever.

Is this expected? How can it be made so that anything included is also paginated correctly?

Example code producing division by 0 error

Expected Behavior

No error when using the example code

Actual Behavior

Getting error FloatDomainError: Infinity when using example code for jsonapi_page_size

Steps to Reproduce the Problem

Include code

  def jsonapi_page_size(pagination_params)
    per_page = pagination_params[:size].to_f.to_i
    per_page = 30 if per_page > 30
    per_page
  end

and don't provide size in the params. Method jsonapi_pagination_params will return limit 0 and jsonapi_pagination_meta will divide by it's value of 0, causing the error.

Fixed it for me by updating the code to per_page = 30 if per_page > 30 || per_page === 0

Specifications

  • Version: 1.7.0
  • Ruby version: 2.5.5

Field attributes containing patched predicates are mishandled

Hello,

Expected Behavior

Field attributes containing/ending with any of the custom predicates added by the gem should extract the correct field, and returns the filtered records.

Actual Behavior

Using field attributes that can contains or ends with _max (and all other custom predicates added by the gem: min, max, avg, sum and count) like bar_abs_max, bar_abs_min are mishandled, the gem is extracting as field bar_abs, so using filter like bar_abs_max_gteq does not work: it returns all records.

Steps to Reproduce the Problem

  1. create a model/record field containing/ending with one of the custom predicates (min, max, avg, sum and count)
  2. request with any filter and other ransack predicates
  3. check the SQL query & result

Specifications

  • Version: 1.7.0
  • Ruby version: 3.0.2
  • Rails version : 6.1.x

A quick solution would be to give the developer the choice to enable or not these custom predicates

Using dot notation for relationship attributes when sorting

Expected Behavior

Based on the JsonAPI spec it is recommended to use the dot notation for relationships:

Note: It is recommended that dot-separated (U+002E FULL-STOP, โ€œ.โ€) sort fields be used to request sorting based upon relationship attributes. For example, a sort field of author.name could be used to request that the primary data be sorted based upon the name attribute of the author relationship.

(https://jsonapi.org/format/#fetching-sorting)

Actual Behavior

Based on the Ransack implementation an underscore is used to combine relationship model name and the attribute (e.g. author_name).

I'm not aware of any workarounds for this, so was curious is anyone else ran into this or just ignored the recommendation.

Error message have a blank value in the pointer field for model attributes with a underscore

Expected Behavior

When there is a validation error to return a json error with pointer filed for attributes with an underscore

Actual Behavior

The JSON:API error message has a blank pointer value in the error payload when the attribute contain a underscore

Steps to Reproduce the Problem

Create a model with a field that contain a underscore. Set the serializer to set_key_transform :dash
Send an update or a create that violate a validation on the field of the underscore
The json error pointer value is empty

Specifications

  • Version: 1.6
  • Ruby version: 2.5

filter by e.g. `notes_count_eq` does not work

Hello!

filtering by counter cache (e.g. notes_count) columns on the model does not seem to work.

Expected Behavior

it should work :)

Actual Behavior

it does not work.

Steps to Reproduce the Problem

see:

Update#1:

problematic is the method

Ransack::Predicate.detect_and_strip_from_string!(field_name)

which also detects and extracts ["count", "_count"] from a string. So this is likely more a ransack issue.

more info:

detect_and_strip_from_string uses the method predicates.sorted_names_with_underscores to match and extracts predicates from a string. see: https://github.com/activerecord-hackery/ransack/blob/master/lib/ransack/predicate.rb#L20

Update#2:

we can mitigate this problem, because we have the allowed_fields value, which enables us to stop picking apart the requested_field string:

while Ransack::Predicate.detect_from_string(field_name).present? do
  # break if we have an exact match with an allowed_fields
  # allowing us to filter by e.g. by counter cache attributes like `notes_count`
  break if allowed_fields.include?(field_name)

  predicate = Ransack::Predicate
    .detect_and_strip_from_string!(field_name)
  predicates << Ransack::Predicate.named(predicate)
end

Update#3:

with the approach from Update#2, i fixed the failings specs.

How to show all the errors that merged to a single model serializer?

Expected Behavior

Show merged errors from another model. because there will be a lot of validations, so I want to merge the errors.

Actual Behavior

undefined method 'error_attribute_from_another_model_that_merged'

Steps to Reproduce the Problem

  1. Create 2 models, FirstModel and SecondModel
  2. On FirstModel there are 3 attributes, attr_a, attr_b, and attr_c
  3. On SecondModel there are 2 attributes, attr_x and attr_z
  4. FirstModel HAS MANY SecondModel
  5. SecondModel BELONGS TO FirstModel
  6. On create / save on SecondModel, it will validate the data on FirstModel based on if condition that checking the current date, and let's assume it will check if attr_a is a valid date, if it's not valid, then it will add errors to FirstModel
  7. On Secondmodel custom validate, it will get the errors from the associated model, and merge the errors to SecondModel object

Code that merge the errors

class Bookings
 ...
  private 

  def check_user
    return if user.able_to_book?

    errors.merge!(user.errors)
  end

  def check_lesson
    return if lesson.bookable_by_user?(user)
    
    errors.merge!(lesson.errors)
  end

Booking Serializer

class BookingSerializer
  include JSONAPI::Serializer
  
  attributes :waiting_list, :canceled_at

  belongs_to :user
  belongs_to :lesson
  belongs_to :lesson_schedule
end

User model

  def able_to_book?
    if active_languages.none?
      errors.add :base, :no_active_lang

      return false
    end

    errors.add :free_trial_chances, :used_up if is_trial? && !free_trial_chances?
    errors.add :base, :suspended if suspended?

    errors.none?
  end

Specifications

  • Version: 1.7
  • Ruby version: 3.0.0

`jsonapi_pagination_meta` produces wrong values if called after calling `jsonapi_paginate`

When using both jsonapi_paginate and jsonapi_pagination_meta to return pagination info, the order when each one is called matters.

Expected Behavior

Calling jsonapi_pagination_meta after calling jsonapi_paginate would produce the same results as calling them on the reverse order.

Actual Behavior

jsonapi_pagination_meta is returning the wrong data if it is called after jsonapi_paginate.

Steps to Reproduce the Problem

  1. Create 6 records of a model;
  2. On the controller, build your result by calling jsonapi_pagination_meta after jsonapi_paginate. For example:
    MySerializer.new(
      objects,
      links: jsonapi_pagination(objects),
      meta: jsonapi_pagination_meta(objects),
    )
    
  3. Call the index URL with params ?page[size]=2&page[number]=1;
  4. The result is:
    "meta": {
        "current": 3,
        "first": 1,
        "prev": 2
    },
    "links": {
        "self": "(...)?page[size]=2&page[number]=1",
        "current": "(...)?filter=&page[number]=1&page[size]=2&sort=",
        "next": "(...)?filter=&page[number]=2&page[size]=2&sort=",
        "last": "(...)?filter=&page[number]=3&page[size]=2&sort="
    }
    
    instead of:
    "meta": {
        "current": 1,
        "first": 2,
        "prev": 3
    },
    "links": {
        "self": "(...)?page[size]=2&page[number]=1",
        "current": "(...)?filter=&page[number]=1&page[size]=2&sort=",
        "next": "(...)?filter=&page[number]=2&page[size]=2&sort=",
        "last": "(...)?filter=&page[number]=3&page[size]=2&sort="
    }
    

Note that the links are correct, the error is on meta.

More Details

The reason seems to be on the fact that we are changing the original page number param on jsonapi_paginate, so when we call jsonapi_pagination_meta the original page number is changed, thus returning the wrong meta info:

def jsonapi_pagination(resources)
  (...)
  pagination.each do |page_name, number|
    original_params[:page][:number] = number # <== here is the issue.
                                             # original_params != params, *but* original_params[:page] == params[:page]
    links[page_name] = original_url + CGI.unescape(
      original_params.to_query
    )
  end

  links
end

The fix seems to be simple: just use a different object for the params. This means simply cloning original_params[:page]:

         *request.path_parameters.keys.map(&:to_s)
       ).to_unsafe_h.with_indifferent_access
 
-      original_params[:page] ||= {}
+      original_params[:page] = original_params[:page].clone || {}
       original_url = request.base_url + request.path + '?'
 
       pagination.each do |page_name, number|

or marshaling the entire params (to be safer):

       original_params = params.except(
         *request.path_parameters.keys.map(&:to_s)
       ).to_unsafe_h.with_indifferent_access
+      original_params = Marshal.load(Marshal.dump(original_params))
 
       original_params[:page] ||= {}
       original_url = request.base_url + request.path + '?'

What am I missing in using jsonapi.rb to render json api?

Expected Behavior

Expected to return all the list of items in the database with a GET request.

Actual Behavior/Response

{
  "errors": [
    {
      "status": "500",
      "source": null,
      "title": "Internal Server Error",
      "detail": null
    }
  ]
}

Steps to Reproduce the Problem

  1. app/controller/api/v1
module Api
  module V1
    class TransactionsController < ApplicationController
      def index
        render jsonapi: Transaction.all.load
      end

      def show; end

      private

      def create_action_params
        params.require(:transaction).permit(permitted_transaction_attributes)
      end
    end
  end
end
  1. transaction.rb
# frozen_string_literal: true

class Transaction < ApplicationRecord
  belongs_to :user
end
  1. app/serializers/transaction_serializer.rb
# frozen_string_literal: true

class TransactionSerializer
  include JSONAPI::Serializer

  attributes :tx_uuid, :user_id, :input_amount, :input_currency, :output_amount, :output_currency, :tx_date
end
  1. routes.rb
# frozen_string_literal: true

Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :transactions, only: %i[index show]
    end
  end
end

Specifications

  • Version: gem 'jsonapi.rb', '~> 2.0'
  • Ruby version: Ruby 3.0.0

How to deserialize a collection?

Am I correct that the jsonapi_deserialize method is meant to only deserialize a document containing a single resource object? Is there a recommended approach for deserializing a document containing an array of resource objects, as with a GET request that returns a collection?

There is no way to return page with more than 30 items

Recently we've found that with page[size] parameter greater than 30 is omitted in the response.

    "meta": {
        "pagination": {
            "current": 1
        },
        "page_size": 30,
        "total_count": 100
    }

It works well with lower numbers, so I was wondering - is it a protection for performance reasons or rather unexpected thing? :)

Expected Behavior

Page size equals 50 and returns 50 items

Actual Behavior

Page size equals 30 and returns 30 items

Steps to Reproduce the Problem

  1. Add pagination
  2. Add query param ?page[size]=50
  3. Check response and meta page_size, which is 30 in this case.

Specifications

  • Version: 1.5.7
  • Ruby version: 2.7.1p83

Option to specify the serializer explicitly

Expected Behavior

Is it possible to make it possible to specify the serializer explicitly. Gem has a method jsonapi_serializer_class, but there are actually cases where I need a different serializer, so it could be possible to specify one, as is done in rails:

def index
  render jsonapi: foo, each_serializer: FooSerializer
end

def show
  render jsonapi: foo
end

def jsonapi_serializer_class
  BarSerializer
end  

Actual Behavior

def index
  render json: foo, each_serializer: FooSerializer
end

def show
  render jsonapi: foo
end

def jsonapi_serializer_class
  BarSerializer
end

Steps to Reproduce the Problem

N/A

Specifications

  • Version: N/A
  • Ruby version: N/A

Use the gem without ActiveRecord

Hello! I have an application in which we're trying to create a public API and we wanted to use the JSON API specification to maintain the pattern inside the company. Unfortunately, the application we're trying to install and use this Gem does not have ActiveRecord because it doesn't need to. It accesses the models using MongoDB through the Mongoid gem.

Is there any way to overcome this issue? I've been thinking about putting in the repo a simple ActiveRecord configuration file with only a physical or in-memory database so I can establish a connection and move forward. I've also tried to disable ActiveRecord from trying to connect to a database and skip to the loading part but it doesn't seem to work. Can you think or another possible workaround to this issue? Thank you!

Here's a similar issue #55

Steps to Reproduce the Problem

  1. Create a Ruby application without ActiveRecord
  2. Install the jsonapi.rb gem
  3. Run the application

It should return the following error:

15:28:52 web.1  | => Booting Thin
15:28:52 web.1  | => Rails 6.0.6.1 application starting in development http://0.0.0.0:3001
15:28:52 web.1  | => Run `rails server --help` for more startup options
15:28:52 web.1  | AssetSync: using /app/config/initializers/asset_sync.rb
15:28:52 web.1  | Exiting
15:28:53 web.1  | /usr/local/bundle/gems/activerecord-6.0.6.1/lib/active_record/connection_handling.rb:217:in `connection_pool': ActiveRecord::ConnectionNotEstablished (ActiveRecord::ConnectionNotEstablished)
15:28:53 web.1  | 	from /usr/local/bundle/gems/activerecord-6.0.6.1/lib/active_record/connection_handling.rb:213:in `connection_config'

Specifications

  • Version: 2.01
  • Ruby version: 3.0.6
  • Rails version: 6.0.6.1

Documentaion error in Deserialization?

Hi there, just starting to implement this library.

I was trying out the deserialization and it was returning blank (in a rails 6 controller). Your readme shows the deserialization method called like this

jsonapi_deserialize(only: [:attr1, :rel_one])

But that didn't work, looking into your code, it looked like I need to pass the params directly.

jsonapi_deserialize(params, only: [:attr1, :rel_one])

Not sure if it's just a documentation error, or maybe something else. Just wanted to check in about it.

Errors handling for development mode

Expected Behavior

Raise exception if ::Rails.env.development?

Actual Behavior

Handle and rescue from an error.

Steps to Reproduce the Problem

  1. raise an error in development mode

Specifications

  • Version: 1.7.0
  • Ruby version: 3.0.1
  • Rails version: 6.1.4

Rails association Errors

Expected Behavior

Should working with #<ActiveModel::Errors [#ActiveModel::NestedError

Actual Behavior

Giving error related to relational data errors.

Like

class Book < ApplicationRecord
  belongs_to :author, autosave: true

  delegate :author_name, :author_name=, to: :lazy_load_author

  def lazy_load_author
    author || build_author
  end
end

# Let's go with author_name, instead name
class Author < ApplicationRecord
  has_many :books
  validates :author_name, presence: true
end

book = Book.new(book_name: 'ABC', author_name: '').save
render jsonapi_errors: book.errors, status: :unprocessable_entity

This one giving error
NoMethodError - undefined method `book.author_name' for #<Book

Steps to Reproduce the Problem

Specifications

  • Version: Latest
  • Ruby version: Latest

How to serialize errors from not valid Ruby Objects?

Hey,

This may be a duplicate of #22

Here is my snippet:

# controller
class CountriesController < ApplicationController

  include JSONAPI::Errors

  def index
    # All good here:
    # non_valid_active_record_object = Country.new
    # render jsonapi_errors: non_valid_active_record_object.errors

    # Another example:
    service_object_with_errors = NonValidServiceObject.new
    service_object_with_errors.save
    puts service_object_with_errors.errors.to_h.inspect # => {:name=>"can't be blank"}

    render jsonapi_errors: service_object_with_errors.errors, status: 422
  end
end

# ruby object that uses ActiveModel validations
class NonValidServiceObject
  include ActiveModel::Model

  validates :name, presence: true
  attr_accessor :name

  def save
    if valid?

    else
      self
    end
  end
end

Response:

{"errors":[{"status":"500","source":null,"title":"Internal Server Error","detail":null}]}

Logs:

Started GET "/countries" for ::1 at 2020-07-04 22:04:01 +0200
Processing by CountriesController#index as HTML
#<NonValidServiceObject:0x00007fa83c3068d0>
___________________________________________
{:name=>"can't be blank"}
DEPRECATION WARNING: Rails 6.1 will return Content-Type header without modification. If you want just the MIME type, please use `#media_type` instead. (called from index at /Users/rafaltrojanowski/Projects/coffeehaunt/app/controllers/countries_controller.rb:36)
DEPRECATION WARNING: Rails 6.1 will return Content-Type header without modification. If you want just the MIME type, please use `#media_type` instead. (called from content_type at /Users/rafaltrojanowski/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/actionpack-6.0.0/lib/action_controller/metal.rb:150)
Completed 500 Internal Server Error in 5ms (Views: 0.4ms | ActiveRecord: 0.0ms | Allocations: 3079)


Started GET "/serviceworker.js" for ::1 at 2020-07-04 22:04:03 +0200

Expected Behavior

Actual Behavior

Steps to Reproduce the Problem

Specifications

  • Version:
  • Ruby version:

new jsonapi_page_size option

Hello!

regarding the new jsonapi_page_size implementation, i would like to suggest to always call the method jsonapi_page_size even if the params page[size] is passed to the request:

def jsonapi_pagination_params
pagination = params[:page].try(:slice, :number, :size) || {}
per_page = pagination[:size].to_f.to_i
per_page = jsonapi_page_size if per_page < 1
num = [1, pagination[:number].to_f.to_i].max
[(num - 1) * per_page, per_page, num]
end

a use case could be to implement a logic to set/overwrite the max size of a response:

def jsonapi_page_size
  pagination = params[:page].try(:size) || {}
  per_page = pagination[:size] || 50
  per_page = 50 if per_page.to_f.to_i > 50
  return per_page
end

best โœŒ๏ธ

Total in meta with pagination

Expected Behavior

I'm calling
http://{{host}}/api/tasks?page[size]=10

I would expect that meta["total"] equals the number of items passed to jsonapi_paginate method.

Actual Behavior

    "meta": {
        "pagination": {
            "current": 1,
            "next": 2,
            "last": 20
        },
        "total": 10
    },

However Model.all size that I'm passing to jsonapi_paginate is different that total from meta.
Is it expected behaviour?

Steps to Reproduce the Problem

Specifications

  • Version: 1.5.7
  • Ruby version: 2.7.1

Non-ORM/AcrtiveRecord support

Hi, there.

We're using JSON:API do display some information from a graph database. Would you be interested in supporting a PR to add support for other ORMs than ActiveRecord?

As we develop our product we would probably be able to organize and extract the Neo4J logic that we use to support JAON:API on top of Neo4J. Perhaps others would like that option as well.

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.