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
Related
I'm working with GraphQL in rails and sending a mutation to create a User in Graphiql. I'm getting nothing but errors though.
I'm setting up the mutation types and logic in the following files. I'll just show my createUser mutation since that's the one I can't get past.
create_user.rb
class Mutations::User::CreateUser < Mutations::BaseMutation
argument :attributes, Types::UserInputType, required: true
field :user, Types::UserType, null: false
field :errors, [String], null: false
def resolve(attributes:)
user = User.new(attributes.to_kwargs)
if user.save
{
user: user,
errors: []
}
else
{
user: nil,
errors: user.errors.full_messages
}
end
end
end
module Types
class UserType < Types::BaseObject
field :id, ID, null: false
field :fname, String, null: true
field :lname, String, null: true
field :email, String, null: true
field :password, String, null: true
field :dpsst, String, null: true
field :reports, [Types::ReportType], null: true
field :reports_count, Integer, null: true
field :token, String, null: false
def reports_count
object.reports.size
end
end
end
user_input_type.rb
description "Attributes to create a user."
argument :fname, String,'Firstname of user', required: true
argument :lname, String,'Lastname of user', required: true
argument :email, String,'Email of user', required: true
argument :password, String,'Password of user', required: true
argument :passwordConfirmation, String, 'Password confirmation', required: true
argument :dpsst, String,'dpsst number of user', required: false
end
mutation_type.rb
module Types
class MutationType < Types::BaseObject
# TODO: remove me
# field :test_field, String, null: false,
# description: "An example field added by the generator"
# def test_field
# "Hello World"
# end
field :create_user, mutation: Mutations::User::CreateUser
field :login, mutation: Mutations::User::Login
field :token_login, mutation: Mutations::User::TokenLogin
field :logout, mutation: Mutations::User::Logout
field :update_user, mutation: Mutations::User::UpdateUser
field :reset_password, mutation: Mutations::User::ResetPassword
field :send_reset_password_instructions, mutation: Mutations::User::SendResetPasswordInstructions
field :unlock, mutation: Mutations::User::Unlock
field :resend_unlock_instructions, mutation: Mutations::User::ResendUnlockInstructions
end
end
Now Here's a couple of the mutation layouts I've tried below. I just don't know how to write them 100%! or potentially my graphiql is messed up, because I'm inserting mutations that should work according to the internet. I suspect it might have something to do with the to_kwargs method...
mutations:
mutation createUserMutation($userInput: CreateUserInput!){
createUser(input: $userInput) {
user {
id
fname
}
}
}
"userInput": {
"email":"test#gmail.com",
"password": "password",
"fname":"Jake",
"lname":"Tester",
"passwordConfirmation":"password",
"dpsst":"75323",
}
}
mutation createUserMutation($attributes: UserInput!){
createUser(input: {attributes: $attributes}) {
user {
token
id
fname
lname
}
}
}
{
"attributes": {
"email":"test#gmail.com",
"password": "password",
"fname":"Jake",
"lname":"Tester",
"passwordConfirmation":"password",
"dpsst":"75323"
}
}
With the most recent try, I'm receiving these errors:
{
"error": {
"message": "unknown attribute 'passwordConfirmation' for User.",
"backtrace":...(I can post this if you need it)
Please teach me how to make the proper mutation calls, or at least point me in the right direction. Thank you!
found the issue.
Due to devise needing the password confirmation, but my actual create user logic not needing it, I could delete that key before using the information to create a user. I fixed it with:
create_user.rb
class Mutations::User::CreateUser < Mutations::BaseMutation
argument :attributes, Types::UserInputType, required: true
field :user, Types::UserType, null: false
field :errors, [String], null: false
def resolve(attributes:)
userAttributes = attributes.to_kwargs
if userAttributes[:password] == userAttributes[:passwordConfirmation]
userAttributes.delete(:passwordConfirmation)
end
user = User.new(userAttributes)
# userAttributes.attributes.except
if user.save
{
user: user,
errors: []
}
else
{
user: nil,
errors: user.errors.full_messages
}
end
end
end
[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 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'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').