How to define a dynamic key in grape-entity gem? - ruby-on-rails

Lets say I have a User model and I am exposing User object via Grape::Entity. So in here I want to define a dynamic key which name and its value will be based on other keys(id, name, email) value.
module API
module Entities
class User < Grape::Entity
expose :id
expose :name
expose :email
expose :dynamic_key # dynamic key name will be generated based on other keys(`id`, `name`, `email`) value
private
def dynamic_key
#dynamic key value here based on other keys(`id`, `name`) value
end
end
end
end
How could I do it?
Thanks in advance!

You have access to the instance with object:
def dynamic_key
"#{object.id}_#{object.name}"
end
Edit: misunderstood the question. Don't think you can get the dynamic key this way. Can you just do something like this:
expose "#{object.id}_#{object.name}".to_sym

If someone is looking for a solution, this is what I do :
class Trucs < Grape::Entity
expose :id
# expose custom / dynamic fields
expose :details, merge: true # merge true to avoid "details": {...} in response
private
def details
details_list = {}
list_of_fields = %w[first_key second_key third_key] # keys can be dynamically created, this is just a sample.
list_of_fields.each do |field_name|
details_list[field_name.to_sym] = "#{field_name} value" # OR object.send(field_name) OR anythig you need
end
return details_list
end
end
Result is :
{
"id": 1,
"first_key": "first_key value",
"second_key": "second_key value",
"third_key": "third_key value"
}
Or for a simple case with just one dynamic key based on id and name
def details
{"#{object.id}_#{object.title}": 'my dynamic value'}
end

Why would you want to do this? If you want something dynamic, it should be the value the key maps to -- not the key itself. The consumer of your API should be able to depend on the key names being consistent with your documented definitions, and also keep in mind that the point of keys is to have a name you can consistently reference that maps to a dynamic value. So if you want to expose some dynamic_key attribute with a dynamically generated value, you can do this:
expose :id
expose :name
expose :email
expose :dynamic_key do |object, _options|
"#{object.id}_#{object.name}"
end
The hash { id: 1, name: 'Foo', email: 'foo#bar.done' } would be represented as:
{ id: 1, name: 'Foo', email: 'foo#bar.done', dynamic_key: '1_Foo' }

Related

How do I override the root key in a Grape API response payload?

module Entities
class StuffEntity < Grape::Entity
root 'stuffs', 'stuff'
...
How can I DRY up my code by reusing this entity while still having the flexibility to rename the root keys ('stuffs' and 'stuff') defined in the entity?
I might need to do this in a scenario where I'm exposing a subset of a collection represented by an existing entity or exposing an associated collection that can be represented by an existing entity.
Hiding the root key when you're exposing an associated object or collection
Let's say I have an object with a name attribute and some collection of scoped_stuff that I want to expose as some_stuffs. I could do that with an entity like this:
module Entities
class CoolStuffEntity < Grape::Entity
root 'cool_stuffs', 'cool_stuff'
expose :some_stuffs, documentation: {
type: Entities::StuffEntity
} do |object, _options|
Entities::StuffEntity.represent(
object.class.scoped_stuff,
root: false
)
end
expose :name, documentation: { type: 'string' }
end
end
Passing root: false to the represent method ensures that the nested association is represented without a root key. Here are what the representations look like with and without that argument:
# Without root: false
cool_stuff: {
some_stuffs: {
stuffs: [ /* collection represented by StuffEntity */ ]
},
name: 'Something'
}
# With root: false
cool_stuff: {
some_stuffs: [ /* collection represented by StuffEntity */ ],
name: 'Something'
}
In this instance, passing root: false ensures that the nested entity's root key isn't included in our representation.
Setting a root key name when presenting an entity with no defined root
Let's say we have this entity where we did not specify root:
module Entities
class StuffEntity < Grape::Entity
expose :name, documentation: { type: 'string' }
end
end
The serializable hash for an object represented with this entity will look like: { name: 'Object name' }
In our API, we can specify the response key like so:
get do
stuff_object = Stuff.find_by(user_id: current_user)
present stuff_object,
with: Entities::StuffEntity,
root: :stuff
end
So that our response will look like this: { stuff: { name: 'Object name' } }
Note that 'root' accepts string and symbol arguments here.
If you want to rename the root key in your API response
So what if I have an entity where I specified a root key and I want the key in my response to be different (e.g., exposing a subset of the collection)? Instead of using present, I can use represent again. Except this time, instead of disabling the root key by passing 'false', I can give it a key name:
get do
my_stuff = Stuff.find_by(user_id: current_user)
Entities::StuffEntity.represent(
my_stuff,
root: :my_stuff
)
end

How to get the field arguments in connection type in graphql ruby

Currently I have the user's query type which returns a list of all the users. I have defined a custom connection and used it in the user type.
module Types
class QueryType < Types::BaseObject
description "Get all the users"
field :users, Types::UserConnection, null: false do
argument :search, String, required: false
argument :limit, Integer, required: true
argument :offset, Integer, required: true
end
def users(search:, limit:, offset:)
User.search(search).limit(limit).offset(offset)
end
end
end
My user connection looks something like this...
class Types::UserEdgeType < GraphQL::Types::Relay::BaseEdge
node_type(Types::UserType)
end
class Types::UserConnection < GraphQL::Types::Relay::BaseConnection
edge_type(Types::UserEdgeType)
field :items_currently_on_page, Integer, null: false
field :total_pages, Integer, null: false
field :items_per_page, Integer, null: false
def items_currently_on_page
object.items.size
end
def total_pages
(User.all.count.to_f / items_per_page).ceil
end
def items_per_page
3
end
end
Currently I have hard-coded the items_per_page which corresponds to the limit defined in the user's arguments. My question is how can I get the user's limit argument and replace with the hard coded value.
If you are sending the query using variables, then you can use context.query.provided_variables. That's a hash containing the variables you sent.
In the resolve function of a field you have access to the context variable.
If you have a graphql query like this:
{
users(offset: 10, limit:10) {
id
name
}
}
you should be able to access the arguments and fields like that:
# .query: Access GraphQL::Query instance
# .lookahead: Access Class: GraphQL::Execution::Lookahead instance
# Lookahead creates a uniform interface to inspect the forthcoming selections.
# .ast_nodes: Access to Array<GraphQL::Language::Nodes::Field> (.length always 1 for one query)
# .selections: Access to Array<Nodes::Field> (.length always 1 for one query)
# .name returns to name of the query defined in query_type.rb for example "users"
# .children: Access to Class: GraphQL::Language::Nodes::AbstractNode instance
# AbstractNode is the base class for all nodes in a GraphQL AST.
# Seems to be the root of the field selection of a query.
# Contains all queried connection fields like nodes, edges, pageInfo, totalCount
# Also contains the provided arguments like first,last,after,before,limit,offset.
# nodes.selections: Access to Array<Nodes::Field>
# Contains all requested nodes like id, slug, name, [...]
arguments = context.query.lookahead.ast_nodes[0].selections[0].arguments
fields = context.query.lookahead.ast_nodes[0].selections[0].children.find {|c| c.name == "nodes"}.selections
To get the value of the limit argument this should work:
def items_per_page
context.query.lookahead.ast_nodes[0].selections[0].arguments.find {|a| a.name == "limit"}.value
end
use this function for get the count of the items_per_page
def items_per_page
object.nodes.size
end

Grape Entity for a Hash with string keys not working

I am using 'grape-entity', '~> 0.7.1'
I have a hash in the format:
temp_data = [{sheet_index: 0, other_names: []},{'sheet_index' => 1, 'other_names': ['a']}]
And I have the following entities
class Sheet < Grape::Entity
expose :sheet_index, documentation: {type: Integer, desc: "Sheet index"}
expose :other_names, documentation: {type: Array, desc: "Other names"}
end
class Sheets < Grape::Entity
present_collection true
expose :items, as: 'sheet_history', using Entities::Sheet
end
# response from the entities
present temp_data, with: Entities::Sheets
Now I need to make sure that no matter the type of keys in my Hash it should still give me the correct output for the above case
expected_response = {"sheet_history" => [{"sheet_index"=>0, "other_names"=>[]}, {"sheet_index"=>1, "other_names"=>["a"]}]}
but the response I am getting is in the format below
actual_response = {"sheet_history" => [{"sheet_index"=>0, "other_names"=>[]}, {"sheet_index"=>nil, "other_names"=>nil}]}
so in the actual response sheet_index and other_names of the second element are nil because their keys were Strings, not Symbols. (Refer to temp_data.)
I have referred to https://github.com/ruby-grape/grape-entity/pull/85 to get the above implementation but still am not able to make it work without using HashWithIndifferentAccess or OpenStructs
You are missing a colon after using, but I wouldn't set up multiple entities like that as it's likely to result in wonky behavior. Try this:
# Dummy definition of your class
class Item
include ActiveModel::Serialization
attr_accessor :sheet_index
attr_accessor :other_names
def initialize(index, names)
#sheet_index = index
#other_names = names
end
end
items = []
items << Item.new(0, [])
items << Item.new(1, ['a'])
=> [
#<Item:0x00007f860f740e40 #other_names=[], #sheet_index=0>,
#<Item:0x00007f860f513618 #other_names=["a"], #sheet_index=1>
]
# Entity Definition
class Sheet < Grape::Entity
# The first arg here is the key to use for a collection,
# the second is the key to use for a single object
root 'sheet_history', 'sheet_history'
expose :sheet_index, documentation: {
type: Integer,
desc: "Sheet index" # Plz use locales
}
expose :other_names, documentation: {
type: Array,
desc: "Other names" # Plz use locales
}
end
# Test it
representation = Sheet.represent(items)
=> {
"sheet_history"=>[
#<Sheet:70106854276160 sheet_index=0 other_names=[]>,
#<Sheet:70106854275680 sheet_index=1 other_names=["a"]>
]
}
# This is just more a more readable, but as you can see it's
# both mapping all the attributes correctly and
# setting the root key that you wanted:
representation['sheet_history'].map do |r| r.serializable_hash end
=> [
{
:sheet_index=>0,
:other_names=>[]
},
{
:sheet_index=>1,
:other_names=>["a"]
}
]
# Endpoint
get do
items = current_user.items # or whatever
present items, with: Entities::Sheet
end
You can send your array of hashes to the represent method, but it doesn't like the stringified key. Ideally you should be passing DB objects to your entity instead of hashes but, if you for some reason cannot, I would pass temp_data.map(&:symbolize_keys) as your argument to the entity to ensure the top-level keys in the hash it's parsing are symbols.

What is a good way to `update_or_initialize_with` in Mongoid?

Each user has one address.
class User
include Mongoid::Document
has_one :address
end
class Address
include Mongoid::Document
belongs_to :user
field :street_name, type:String
end
u = User.find(...)
u.address.update(street_name: 'Main St')
If we have a User without an Address, this will fail.
So, is there a good (built-in) way to do u.address.update_or_initialize_with?
Mongoid 5
I am not familiar with ruby. But I think I understand the problem. Your schema might looks like this.
user = {
_id : user1234,
address: address789
}
address = {
_id: address789,
street_name: ""
user: user1234
}
//in mongodb(javascript), you can get/update address of user this way
u = User.find({_id: user1234})
u.address //address789
db.address.update({user: u.address}, {street_name: "new_street name"})
//but since the address has not been created, the variable u does not even have property address.
u.address = undefined
Perhaps you can try to just create and attached it manually like this:
#create an address document, to get _id of this address
address = address.insert({street_name: "something"});
#link or attached it to u.address
u.update({address: address._id})
I had this problem recently. There is a built in way but it differs from active records' #find_or_initialize_by or #find_or_create_by method.
In my case, I needed to bulk insert records and update or create if not found, but I believe the same technique can be used even if you are not bulk inserting.
# returns an array of query hashes:
def update_command(users)
updates = []
users.each do |user|
updates << { 'q' => {'user_id' => user._id},
'u' => {'address' => 'address'},
'multi' => false,
'upsert' => true }
end
{ update: Address.collection_name.to_s, updates: updates, ordered: false }
end
def bulk_update(users)
client = Mongoid.default_client
command = bulk_command(users)
client.command command
client.close
end
since your not bulk updating, assuming you have a foreign key field called user_id in your Address collection. You might be able to:
Address.collection.update({ 'q' => {'user_id' => user._id},
'u' => {'address' => 'address'},
'multi' => false,
'upsert' => true }
which will match against the user_id, update the given fields when found (address in this case) or create a new one when not found.
For this to work, there is 1 last crucial step though.
You must add an index to your Address collection with a special flag.
The field you are querying on (user_id in this case)
must be indexed with a flag of either { unique: true }
or { sparse: true }. the unique flag will raise an error
if you have 2 or more nil user_id fields. The sparse option wont.
Use that if you think you may have nil values.
access your mongo db through the terminal
show dbs
use your_db_name
check if the addresses collection already has the index you are looking for
db.addresses.getIndexes()
if it already has an index on user_id, you may want to remove it
db.addresses.dropIndex( { user_id: 1} )
and create it again with the following flag:
db.addresses.createIndex( { user_id: 1}, { sparse: true } )
https://docs.mongodb.com/manual/reference/method/db.collection.update/
EDIT #1
There seems to have changes in Mongoid 5.. instead of User.collection.update you can use User.collection.update_one
https://docs.mongodb.com/manual/reference/method/db.collection.updateOne/
The docs show you need a filter rather than a query as first argument but they seem to be the same..
Address.collection.update_one( { user_id: user_id },
'$set' => { "address": 'the_address', upsert: true} )
PS:
If you only write { "address": 'the_address' } as your update clause without including an update operator such as $set, the whole document will get overwritten rather than updating just the address field.
EDIT#2
About why you may want to index with unique or sparse
If you look at the upsert section in the link bellow, you will see:
To avoid multiple upserts, ensure that the filter fields are uniquely
indexed.
https://docs.mongodb.com/manual/reference/method/db.collection.updateOne/

How do I add extra data to ActiveRecord records being serialized to JSON in Rails?

I'm exposing some resources via a simple API that returns JSON. I would like to inject the path to each of the resources so that the consumer need not construct them. Example of desired output for something like User.all.to_json:
users: [
{user: {
id: 1,
name: 'Zaphod',
url: 'http://domain.com/users/1.json'
}},
{user: {
id: 2,
name: 'Baron Munchausen',
url: 'http://domain.com/users/2.json'
}}
];
In order to generate the URL I'd like to continue using the helpers and not pollute the models with this kind of information. Is there a way to do this? Or am I better off just putting this into the model?
If you have a model method you want included in the json serialization you can just use the builtin to_json call with the :methods parameter:
class Range
def url
# generate url
...
end
end
Range.find(:first).to_json(:methods => :url)
Have you checked this out : http://json.rubyforge.org/ ?
class Range
def to_json(*a)
{
'json_class' => self.class.name,
'data' => [ first, last, exclude_end? ]
}.to_json(*a)
end
def self.json_create(o)
new(*o['data'])
end
end

Resources