User Grape Entity with Arrays - ruby-on-rails

I was wondering whether Grape Entity would work for rendering arrays of hashes, I thought I remebered it worked but somehow I cannot get it to work right now, am I doing some obvious mistake? Here's my Entity:
class V1::Entities::Searchresult < Grape::Entity
expose :_type, as: :type
expose :_id, as: :id
expose :_score, as: :score
expose :highlight
end
In my API I call the rendering like this:
present result['hits']['hits'], with: V1::Entities::Searchresult, :params => params
The 'result['hits']['hits']' is filled with 10 hashes that contain the data. The data is present. However when I look at the result I get:
[
{
"type": null,
"id": null,
"score": null,
"highlight": null
},
{
"type": null,
"id": null,
"score": null,
"highlight": null
},
......
Am I doing something wrong, or is this simply not possible. I can't seem to dig up any documentation on the array toppic.
Cheers
Tom

I found the error, Grape::Entity::Delegator::HashObject fails to work with hashes that have string keys and not symbols. It cannot extract the values.
data = []
result['hits']['hits'].each do |item|
data << item.symbolize_keys
end
present data, with: V1::Entities::Searchresult, :params => params
This workaround ommits the problem. I will also open a github Issue for a fix since a simple
object[attribute] || object[attribute.to_s]
would solve the whole problem instead of only using
object[attribute]
to read the attribute.

Related

Using a List in a GraphQL Union type (in Ruby)

The GraphQL Ruby documentation shows how to define a union type:
class Types::CommentSubject < Types::BaseUnion
description "Objects which may be commented on"
possible_types Types::Post, Types::Image
# Optional: if this method is defined, it will override `Schema.resolve_type`
def self.resolve_type(object, context)
if object.is_a?(BlogPost)
Types::Post
else
Types::Image
end
end
end
and it shows how to declare that a field is a list outside of a union:
# A field returning a list type:
# Equivalent to `aliases: [String!]` above
field :aliases, [String]
# An argument which accepts a list type:
argument :categories, [Types::PostCategory], required: false
but I can't for the life of me figure out how to use a list as a possible type that a union member could be.
My code looks something like this:
class Types::ArgumentValueType < Types::BaseUnion
possible_types GraphQL::Types::String, GraphQL::Types::Boolean, GraphQL::Types::Int
def self.resolve_type(object, _context)
if object.is_a?(String)
GraphQL::Types::String
elsif object.is_a?(Array)
[GraphQL::Types::String]
elsif object.is_a?(FalseClass)
GraphQL::Types::Boolean
elsif object.is_a?(TrueClass)
GraphQL::Types::Boolean
elsif object.is_a?(Integer)
GraphQL::Types::Int
end
end
end
… which sort of works, except that when it's an array, this value comes back as a string. In GraphiQL it looks like this (we're looking at the value field):
{
"name": "top_box",
"type": "Array",
"description": "The chosen values of the scale which should be combined",
"position": 2,
"optional": false,
"value": "[\"8\", \"9\", \"10\"]"
}
We could potentially parse that in the client but ideally I'd like it to be an array of strings, like this:
{
"name": "top_box",
"type": "Array",
"description": "The chosen values of the scale which should be combined",
"position": 2,
"optional": false,
"value": [
"8",
"9",
"10"
]
},
But I can't see how to define that and the only information I could find anywhere is a brief comment in this answer to ‘GraphQL Union within Union’ which seems to suggest that it may not be possible.
Errors
If I try adding [GraphQL::Types::String] to possible_types, I get
undefined method `graphql_name' for [GraphQL::Types::String]:Array
If I try adding GraphQL::Schema::List.new(GraphQL::Types::String) to possible_types, I get
undefined method `directives' for #<GraphQL::Schema::List:0x000000010f690950 #of_type=GraphQL::Types::String>
and if I try replacing [GraphQL::Types::String] (under elsif object.is_a?(Array)) with GraphQL::Schema::List.new(GraphQL::Types::String), then I get
.resolve_type should return a type definition, but got #<GraphQL::Schema::List:0x000000010f9fd6b0
#of_type=GraphQL::Types::String> (GraphQL::Schema::List) from `resolve_type(Types::ArgumentValueType,
[\"8\", \"9\", \"10\"], #<GraphQL::Query::Context:0x000000010f8ed018>)`
Update
I managed to make an improvement by adding a wrapper class:
# frozen_string_literal: true
module Types
class ListOfStringsType < Types::BaseObject
field :values, [String]
end
end
Then, with a GraphQL query that looks a bit like this
arguments {
name
type
description
position
optional
value {
... on ListOfStrings {
values
}
}
}
It produces output like this
"arguments": [
{
"name": "top_box",
"type": "Array",
"description": "The chosen values of the scale which should be combined",
"position": 2,
"optional": false,
"value": {
"values": [
"8",
"9",
"10"
]
}
},
{
"name": "measure",
"type": "Measure",
"description": "The name of the measure to be \"top-boxed\"",
"position": 1,
"optional": false,
"value": "unique_and_different"
}
]
This is kind of okay, except that it has one extra level of indirection which I would prefer to avoid.
A colleague has pointed me at the following code, which works, but I'm afraid I still don't fully understand everything that's going on. But I will do my best to explain.
Use a Scalar
As I understand it, the idea with scalar types is that if you have your own fundamental type (normally not a complex object but just a single datum) which is none of the basic built-ins, you can define that as a scalar. (You can get a sense of examples of scalars from the graphql-scalars project). Ultimately everything is a string over the wire, of course, but in your backend you will define how to serialize your type, and in the frontend you will define how to unserialize it, and vice versa for writes of course.
So, you replace the contents of your argument_value_type.rb file up there with the following.
class Types::ArgumentValueType < Types::BaseScalar
description "A value"
def self.coerce_input(input_value, _context)
input_value
end
def self.coerce_result(ruby_value, _context)
ruby_value
end
end
As we can see from the Scalars reference:
self.coerce_input takes a GraphQL input and converts it into a Ruby value
self.coerce_result takes the return value of a field and prepares it for the GraphQL response JSON
… in other words, no conversion either way.
Then you can shove anything in there and it just comes out as-is. Assuming it's representable in JSON.
GraphQL Query
Your query is super-simple:
arguments {
name
type
description
position
optional
value // <-- this is the relevant bit
}
and you just get all the values back in whatever type they may be. Your front-end will have to handle it!
Future Improvements
Probably this could be improved to narrow it down a bit as to where it will break when an unexpected type is encountered.

Rails Active Storage REST API

I've been able to set up Active Storage file uploads and now I'm trying to return associated images when I do, for instance, Speaker.all or Speaker.find(2).
Calling the associated endpoint I get something like:
{
"id": 2,
"name": "Rafael",
"email": "rafael.almeida#mail-provider.com",
"company": "XING",
"social_media": "{\"twitter\": \"#rafaelcpalmeida\"}",
"created_at": "2018-10-01T17:21:50.993Z",
"updated_at": "2018-10-01T17:21:51.144Z"
}
How can I also return its associated avatar?
I figured out what to do in order to achieve the result I wanted. First, we need to add the active_model_serializers to the Gemfile, followed by bundle install.
After we installed the gem we should add include ActionController::Serialization to every controller that's going to use the Serializer.
We generate a new serializer using rails g serializer speaker. My SpeakerSerializer looks like:
class SpeakerSerializer < ActiveModel::Serializer
attributes :id, :name, :email, :company, :avatar
def avatar
rails_blob_path(object.avatar, only_path: true) if object.avatar.attached?
end
end
And my output looks like
{
"speaker": {
"id": 2,
"name": "Rafael",
"email": "rafael.almeida#xing.com",
"company": "XING",
"avatar": "/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCdz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--515a0de8817b3529b5d3d168871cebf6ccee0463/xing-photo.jpg"
}
}
Check if this approach is good for your needs. This is the case of has_one_attached.
First, fetch the record:
speaker = Speaker.find(2)
Then convert it to a Ruby hash (please note .as_json):
speaker_hash = speaker.as_json
Now, just append the pair key-value you need, using url_for helper:
speaker_hash['url'] = url_for(speaker.avatar)
Finally, convert the hash to json:
speaker_hash.to_json

Rails model to hash, exclude attributes

I am trying to form a json response that looks like this:
{
"user": {
"birthday": "2013-03-13",
"email": "example#example",
"id": 1,
"name": null,
"username": "example"
},
"other_data": "foo"
}
Before, when I was just returning the user, I used
render :json => #user, :except => [:hashed_password, :created_at, :updated_at]
to keep the hashed_password, created_at, and updated_at attributes from being sent. Is there a way to do this, but also allow additional data to be sent along with the user? Right now I'm just adding the attributes I want to send to the hash one by one, but this is obviously not ideal.
Rendering json data first automagically calls 'as_json' on your model, which returns a ruby hash. After that, 'to_json' is called on that to get a string representation of your hash.
To achieve what you wanted, you can call something like this:
render :json => {
:user => #user.as_json(:except => [:hashed_password]),
:some_other_data => {}
}
In this case, there is no object which responds to 'as_json', so the controller just calls 'to_json' to turn your hash to a string.
I would recommend to use this gem: https://github.com/fabrik42/acts_as_api

Rails 3 How can I allow nested attributes to be passed without the _attributes designation

When using accepts_nested_attributes_for, instead of having to pass "child_attributes", I'd like to pass "child". I'm pretty sure if I put a lot of the logic in my controller to create the the records and children, I could accomplish this. However, in an effort to keep my controllers clean and logic where it should be, the model in this case, I'd like to know how to switch rails 3 around to use this syntax when doing a POST or PUT.
{
"name": "test",
"child_attributes": [
{
"id": 1,
"name": "test_child_update"
},
{
"name": "test_child_create"
}
}
Rather
{
"name": "test",
"child": [
{
"id": 1,
"name": "test_child_update"
},
{
"name": "test_child_create"
}
}
Evidently, this can't be done.
The _attributes suffix adds no value to JSON requests and responses, but to get rid of it in the model layer, you would have to monkey patch ActiveRecord. Everyone hates monkey-patching ActiveRecord relations.
How about doing it in the controller layer?
#comment = Comment.new(attributify(:comment))
# snip
# ApplicationController
def attributify()
# Now I'll go and write this!
end
Edit: Done. The controller mixin is here: https://gist.github.com/johncant/6056036

Rails 2.3.8: how to parse JSON with field names other than DB columns

I'm sure there's an easy solution for this but I'm new to Rails and need help with syntax.
In my controller I have:
#products = Product.all
format.json { render :json => #products }
And it works fine, returning data with the default column names used in the DB:
"product": {
"created_at": "2010-10-08T17:24:27Z",
"id": 24,
"product_date": "2010-08-08",
"product_name": "Product One",
"updated_at": "2010-10-08T17:36:00Z"
}
What I need is something like:
"product": {
"created_at": "2010-10-08T17:24:27Z",
"id": 24,
"start": "2010-08-08",
"title": "Product One",
"updated_at": "2010-10-08T17:36:00Z"
}
That is, changing product_date to start and product_name to title, but only in the JSON output.
It seems like an easy problem to solve but I'm not sure how to express it in Ruby/Rails syntax, so I would really appreciate any help. I cannot rename the database columns.
If you want to alter the JSON output for all Products, everywhere and all the time, simply override the to_json method on your Product model.
Here's the simple way to do that (in your Product class definition):
def to_json
ActiveSupport::JSON.encode({
:created_at => created_at
:id => id
:start => product_date
:title => product_name
:updated_at => updated_at
})
end
You could get fancier and plug in a custom Serializer, but this should suffice for your purposes. One drawback of doing this so explicitly is that if your schema changes, this will have to be updated. This will also break the options usually available to the to_json method (:include, :only, etc) so, maybe it's not too hot.

Resources