I'm writing a devise-jwt-based authentication system for my graphql-ruby using app. In the process, I've made a mutation for creating a new user account, which takes 7 parameters, which creates quite a lot of repetition in my code:
module Mutations
class SignUpMutation < Mutations::BaseMutation
argument :email, String, required: true
argument :password, String, required: true
argument :family_name, String, required: true
argument :family_name_phonetic, String, required: true
argument :given_name, String, required: true
argument :given_name_phonetic, String, required: true
argument :newsletter_optin, Boolean, required: false
field :token, String, null: true
field :user, Types::UserType, null: true
def resolve(email:, password:,
family_name:, family_name_phonetic:,
given_name:, given_name_phonetic:,
newsletter_optin:
)
result = {
token: nil,
user: nil
}
new_user = User.new(
email: email,
password: password,
family_name: family_name,
family_name_phonetic: family_name_phonetic,
given_name: given_name,
given_name_phonetic: given_name_phonetic,
newsletter_optin: newsletter_optin
)
if new_user.save!
result[:token] = new_user.token
result[:user] = new_user
end
result
end
end
end
How could I DRY this up to avoid repeating the names of the mutation arguments all over the place?
Thank you in advance!
Answering my own question. The correct way to not have to deal with so many parameters is to use Input Objects instead of separate parameters. From the graphql-ruby documentation:
Input object types are complex inputs for GraphQL operations. They’re great for fields that need a lot of structured input, like mutations or search fields.
So I've defined my Input Object as such:
module Types
class UserAttributes < Types::BaseInputObject
description 'Attributes for creating or updating a user'
argument :email, String, required: true
argument :password, String, required: true
argument :family_name, String, required: true
argument :family_name_phonetic, String, required: true
argument :given_name, String, required: true
argument :given_name_phonetic, String, required: true
argument :newsletter_optin, Boolean, required: false
end
end
and then refactored my mutation like this:
module Mutations
class SignUpMutation < Mutations::BaseMutation
argument :attributes, Types::UserAttributes, required: true
field :token, String, null: true
field :user, Types::UserType, null: true
def resolve(attributes:)
result = {
token: nil,
user: nil
}
new_user = User.new(attributes.to_hash)
if new_user.save!
result[:token] = new_user.token
result[:user] = new_user
end
result
end
end
end
Finally, this code feels more ruby-like :)
If you'd like, you could do something like this:
[
:email,
:password,
:family_name,
:family_name_phonetic,
:given_name,
:given_name_phonetic
].each do |arg|
argument arg, String, required: true
end
You might think any more than this is overkill, but Ruby is very flexible. If you really wanted to, you could even do something like
def resolve(email:, password:,
family_name:, family_name_phonetic:,
given_name:, given_name_phonetic:,
newsletter_optin:)
result = {
token: nil,
user: nil
}
params = method(__method__).parameters.map(&:last)
opts = params.map{|p| [p, eval(p.to_s)]}.to_h
new_user = User.new(opts)
if new_user.save!
result[:token] = new_user.token
result[:user] = new_user
end
result
end
You can see this answer for an explanation
If you wanted even more than this, you could use a more detailed field list, and define_method - you could get it all the way to the point where you only type e.g. :email once.
Would that be better? Maybe, if you've got hundreds of these to do.
Or if you want to start defining things at runtime.
You may try double splat (**) operator.
module Mutations
class SignUpMutation < Mutations::BaseMutation
argument :email, String, required: true
argument :password, String, required: true
argument :family_name, String, required: true
argument :family_name_phonetic, String, required: true
argument :given_name, String, required: true
argument :given_name_phonetic, String, required: true
argument :newsletter_optin, Boolean, required: false
field :token, String, null: true
field :user, Types::UserType, null: true
def resolve(**arguments)
result = {
token: nil,
user: nil
}
new_user = User.new(
email: arguments[:email],
password: arguments[:password],
family_name: arguments[:family_name],
family_name_phonetic: arguments[:family_name_phonetic],
given_name: arguments[:given_name],
given_name_phonetic: arguments[:given_name_phonetic],
newsletter_optin: arguments[:newsletter_optin]
)
if new_user.save!
result[:token] = new_user.token
result[:user] = new_user
end
result
end
end
end
Of course, creating a new type like you have done would be neater. But there're cases you can combine them together, like
module Mutations
class SignUpMutation < Mutations::BaseMutation
argument :another_attribute, String, required: true
argument :attributes, Types::UserAttributes, required: true
field :token, String, null: true
field :user, Types::UserType, null: true
def resolve(**arguments)
result = {
token: nil,
user: nil
}
# use arguments[:another_attribute] for something else.
new_user = User.new(arguments[:attributes].to_hash)
if new_user.save!
result[:token] = new_user.token
result[:user] = new_user
end
result
end
end
end
In your case I would use input objects as well, but what would you do if you had an existing API with clients relying on the schema and you want to "DRY up" those duplicated arguments that are all the same across different mutations/fields?
If you just go ahead and implement a new input object you'll change the schema and the clients will very likely break. I suppose there is no way of keeping the schema identical when moving existing arguments into an input object, right?
A better approach without disturbing the existing GraphQL schema would be to define a InputType with all the common arguments like:
module Types
module Inputs
class CommonInputType < Types::Root::BaseInputObject
graphql_name("my_common_input_type")
argument :email, String, required: true
argument :newsletter_optin, Boolean, required: true
...
argument :posts, [Types::Inputs::Post], required: true
end
end
end
& use it in some mutation with additional arguments like:
module Mutations
class CreateUser < Mutations::BaseMutation
argument :additional_arg_one, ID, required: true
argument :additional_arg_two, String, required: false
...
Types::Inputs::CommonInputType.arguments.each do |arg,properties|
argument arg.to_sym, properties.graphql_definition.type
end
end
end
I am trying to play around with rails hooked up to graphql and I have the following error trying to display a series of posts by a user
"Field 'posts' is missing required arguments: id"
Here is my query:
query {
posts(user_id: 10, type: "Video") {
title
file
}
}
And in my query_type.rb file I have the following defined:
field :posts, [Types::PostType], null: false do
argument :id, ID, required: true, as: :user_id
argument :type, String, required: true
end
def posts(user_id:, type:)
posts = Post.where("user_id = ? AND type = ?", user_id, type)
end
It is a simple query. I'm new to this technology (GraphQL) and I don't see what the problem is. Can someone pinpoint what is wrong? Thank you.
You need to send the exact name in the parameters when running the query.
In your schema definition you have 2 required arguments called id of type ID and type of type string. So you have 2 options:
Update your query to send in the correct name id:
query {
posts(id: "10", type: "Video") {
title
file
}
}
Or, update your schema definition to receive a user_id:
field :posts, [Types::PostType], null: false do
argument :user_id, ID, required: true, as: :user_id
argument :type, String, required: true
end
def posts(user_id:, type:)
posts = Post.where("user_id = ? AND type = ?", user_id, type)
end
So I'm trying to query on a single user within the database but end up getting:
"Field 'userSingle' doesn't accept argument 'first_name'"
I'm getting that in GraphiQL when I run the following query:
query GetSingleUser {
userSingle(first_name: "Test"){
first_name
last_name
}
}
In my query_type.rb I have the following:
field :userSingle, !types[Types::UserType] do
resolve -> (obj, args, ctx) {
argument :first_name, !types.String
argument :id, types.ID
User.find(id: args[:id])}
end
Originally I had:
field :userSingle, !types[Types::UserType] do
resolve -> (obj, args, ctx) {User.find(id: args[:id])}
end
Same issue. If I take out the id: same issue. Also the same issue with:
field :userSingle, !types[Types::UserType] do
resolve -> (obj, args, ctx) {
argument :first_name, !types.String
argument :id, types.ID
user = User.find_by(
id: args[:id],
first_name: args[:first_name])
}
end
You could create a user_type.rb file with the following:
class Types::UserType < Types::BaseObject
description "A user object"
field :id, Integer, null: false
field :first_name, String, null: false
end
Then have the following in query_type.rb file:
module Types
class QueryType < Types::BaseObject
...
# User query
field :user, UserType, null: true do
description "Find a user by first_name"
argument :first_name, String, required: true
end
def user(first_name:)
User.find_by(first_name: first_name)
end
end
end
With all this in place, the query should then look like this:
{
user(first_name: "name") {
id
firstName: first_name
}
}
Instead of, !types[Types::UserType] in query_type file to
field :userSingle do
type Types::UserType
argument
resolve
..
end
I have this in my model:
monetize :advance_amount_cents, allow_nil: true
monetize :rental_amount_cents, allow_nil: true
I use AutoNumeric to display the currency. It sends it back to the controller like this in params:
'rental_amount' = "2050.12"
Which returns this error from the model:
activerecord.errors.models.rental_period.attributes.rental_amount.invalid_currency
It accepts the currency when I can get it to be sent with a comma instead of a dot as decimal. What is the best practise here? Ideally I would like for all attributes that are monetized to accept anything as decimal separator, comma or dot. That's also how Monetize seems to do it:
pry(main)> Monetize.parse "2050.12"
=> #<Money fractional:205012 currency:USD>
pry(main)> Monetize.parse "2050,12"
=> #<Money fractional:205012 currency:USD>
Which is perfect. How can I configure my model (or the Monetize gem in general) to accept both as params (dot or comma).
Hopefully this is of use to someone.
Model:
monetize :rental_amount_cents, allow_nil: true
View:
= f.input :rental_amount, label: 'Rental amount' do
.input-group
= text_field_tag :rental_amount, #rental_period.rental_amount, class: 'form-control', id: "#{#rental_period.new_record? ? '' : (#rental_period.id.to_s + '_')}rental_amount_rate_rendered"
= f.hidden_field :rental_amount, class: 'rate-input'
%span.input-group-addon €
Javascript setup:
$('[id$=rate_rendered]').add('.flex-rate').autoNumeric('init', settings.money_nosign).on('keyup', function() {
var $hid;
$hid = $(this).parent().find('input.rate-input');
if ($(this).autoNumeric('get') !== '') {
return $hid.val($(this).autoNumeric('get').replace('.', ','));
} else {
return $hid.val(0);
}
});
In settings I have (only relevant part:
window.settings = {
money_nosign: {
aDec: ',',
aSep: '.',
vMin: '-999999999.99'
}
};
I've got the following MongoDB/Mongoid document:
#<Event
_id: 51e406a91d41c89fa2000002,
namespace: :namespace,
bucket: "order_created",
data: {
"order"=>{
"id"=>100,
"deleted_at"=>nil,
"created_at"=>2011-10-06 15:45:04 UTC,
"updated_at"=>2013-07-10 16:37:07 UTC,
"completed_at"=>2013-07-10 16:37:07 UTC
}
}>
Here is the event class definition:
class Event
include Mongoid::Document
field :namespace, type: Symbol
field :bucket, type: String
field :data, type: Hash
end
I want to find and update it using the find_and_modify method in Mongoid but I can't figure out how to properly structure the search criteria to search the data field properly.
Specifically, I want to find data.order.id = 100. I've tried the following with no luck:
Event.where(:data.order.id => 100)
Event.where(:'data["order"]["id"]' => 100)
Event.where( { data: { order: { id: 100 } } } )
Event.where( { data: { :"order" => { :"id" => 100 } } }
The latter returns nil, but the former (and, from the documentation I've read, the correct way to do it) gives me a SyntaxError: unexpected ':'.
This is with Mongoid 3.1.4 and MongoDB 2.4.5.
Answering my own question. The Event class is not referencing a collection, which is what's critical for the Criteria search to work. I've instantiated a new db object to use against the collection and the find/where methods work. Here's an example:
#db = Mongoid::Sessions.default
#db[:events].find().first['order']
#db[:events].where("data.order.id" => 100).first