[Field 'signoutUser' doesn't accept argument 'input']
Here is the error I am getting
I tried to code for logout mutation using Graphql on Ruby on Rails. However, it shows an error shown above. Try to click the link above to see the error. How can I solve the error of logout mutation?
Here is my code for
app/graphql/mutations/sign_out_user.rb:
module Mutations
class SignOutUser < BaseMutation
null true
field :user, Types::UserType, null: false
field :token, String, null: false
argument :email, String, required: false
def resolve(email:)
user = User.find_by email: email[:email]
return user.logout
token = token.destroy!
{ user: user, token: token }
end
end
end
this one works
module Mutations
class SignOutUser < BaseMutation
null true
argument :token, String, required: true
field :token, String, null: false
field :message, String, null: false
def resolve(token:)
user = User.find_by(session_token: token)
if user
user.session_token = ""
user.save!
{
token: user.session_token,
message: "signout success!"
}
else
context.add_error(GraphQL::ExecutionError.new("Invalid token", extensions: { "code" => "INVALID_TOKEN" }))
end
end
end
end
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 using graphiql-ruby gem for using graphql. I am trying to get the list of users with its userprofile and its company. However, it consumes more than 5 minutes and hits the api several times. It even crashes my server. Can anyone help me at this, please?
this is what i have done
module Queries
module Users
extend ActiveSupport::Concern
include GraphQL::Schema::Member::GraphQLTypeNames
included do
field :users, [Types::UserType], null: false
field :user, Types::UserType, null: false do
argument :id, ID, required: true
end
field :currentUser, Types::UserType, null: false
end
# Get all users list except admins
def users
User.where.not(roles: ['admin'])
# context[:current_user].logs.info!('User Data Fetched') && user
end
def user(id:)
User.find(id)
end
def current_user
context[:current_user]
end
end
end
module Types
class UserType < Types::BaseObject
field :id, ID, null: false
field :email, String, null: false
field :user_profile, Types::UserProfileType, null: false
field :grants, [Types::GrantType], null: true
field :companies, [Types::CompanyType], null: true
field :my_companies_with_grants, [Types::CompanyGrantType], null: true
def grants
Grant.where(user_id: object.id)
end
def my_companies_with_grants
current_user = User.find_by(id: object.id)
company = current_user.companies
userid = current_user.id
company.map {|comp| { company: comp, grants: comp.get_user_grants(userid), company_id: comp.id } }
end
end
end
Here is how i am querying for the list of users
query {
employees: users {
id
email
userProfile {
...UserProfile
}
roles
createdAt
companies {
...Company
}
grants {
...Grant
}
}
}
${COMPANY}
${GRANT}
${USER_PROFILE}
why it is taking too long time to load all the users ? How can it be make efficient?
UPDATE
class ApiSchema < GraphQL::Schema
mutation(Types::MutationType)
query(Types::QueryType)
use GraphQL::Batch
end
Record loader
class RecordLoader < GraphQL::Batch::Loader
def initialize(model)
#model = model
end
def perform(ids)
#model.where(id: ids).each { |record| fulfill(record.id, record) }
ids.each { |id| fulfill(id, nil) unless fulfilled?(id) }
end
end
I changed my user_type.rb and did the following for user_profile
field :user_profile, [Types::UserProfileType, null: true], null: false do
argument :id, [ID], required: true
end
def user_profile(ids:)
RecordLoader.for(UserProfile).load_many(ids)
end
But i get "message":"Field 'userProfile' is missing required arguments: ids" issue
I setup the graphql-batch but could not figure out where should i use RecordLoader as per the way I am using(types and resolvers)
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've inherited a rails api and I'm trying to test controllers. I have an endpoint '/api/v1/vitcords' where I create new vitcords. The video model only has a validation name. So my doubt is how to test that when I create a new video without specify a name, I get the message error I want, that in this case is "Vitcord name has to be specified". Thanks.
This is the Vcord model
class Vcord
include Mongoid::Document
include Mongoid::Timestamps
include Mongoid::Spacial::Document
include Concerns::Vitcord::M3u8
include Concerns::Vitcord::User
# Validations
validates_presence_of :name
# Atributes
field :name, type: String
field :location, type: Array, spacial: true
field :address, type: String
field :closed, type: Boolean, default: false
field :public, type: Boolean, default: false
field :thumb, type: String
end
This is the controller video_controller.rb
module Api::V1
class VitcordsController < ApiController
def create
user = current_resource_owner
# Validation
param! :name, String, required: true
param! :location, Hash, required: false
param! :address, String, required: false
ap params[:location]
ap params[:location][:latitude]
ap params[:location][:longitude]
# Creating
vcord = Vcord.new()
vcord.name = params[:name] if params[:name]
if params[:location] && params[:location]['latitude'] && params[:location]['longitude']
vcord.location = {
lng: params[:location]['longitude'],
lat: params[:location]['latitude']
}
end
vcord.address = params[:address] if params[:address]
vcord.creator = user
if vcord.save
vcord.members.each do |member|
Notification.generate_notification_for_vitcord_invited(vcord, member)
end
#vitcord = vcord
else
error(:internal_server_error, ["Vitcord name has to be specified"], nil)
end
end
end
And this is the spec
require 'rails_helper'
describe "POST /api/v1/vitcords" do
before(:each) do
db_drop
post "/oauth/token", {
:grant_type => 'assertion',
:assertion => TOKEN
}
#token = response_json['access_token']
end
it "sends an error if a vitcord is created with name nil" do
header 'Authorization', "Bearer #{#token}"
post '/api/v1/vitcords', {
address: "Calle Rue del Percebe",
location: {
latitude: 40.7127837,
longitude: -74.00594130000002
}
}
//This does not work but it would be something like this
expect(error).to eq("Name has to be specified")
end
end
Well, you should refactor your code, but answering your question, you can add an error to you object by doing (look that I used #vcord and not vcord):
#vcord.errors.add(:name, 'Vitcord name has to be specified')
(as you can see here http://api.rubyonrails.org/classes/ActiveModel/Errors.html#method-i-add)
and on your test:
expect(assigns(:vcord).errors.name).to eq('Vitcord name has to be specified').