How to write a graphQL mutation that allows for partial updates? - ruby-on-rails

So I'm working on trying to learn GraphQL for ruby for a project.
I've almost got some parts of it up and running, but I'm having issues with other parts. There are plenty of tutorials out there that cover ultra-basics, but none of them seem to expand in the right directions.
I have a mutation to update my user. So far so good. I can look up the user by their ID, and update a single specific field. I can extend that to updating two fields.
What I cannot do, and this is looking insane, is generalize those fields -- at all. My user model will wind up with over 20 fields attached to it -- phone numbers, addresses, job title, etc etc.
When I create the mutation, I have to define the arguments that go into the resolve method. So far so good. I then define the fields the mutation can return. Again, so far so good.
Then I get to the actual resolve method.
The initial syntax isn't bad. def resolve(user_id:, name:, email:). Then you discover that despite setting required to false, you have to include all the values. You need to specify default values for the optional variables. So it becomes def resolve(user_id:, name: null, email: null) -- but that actually nulls out those values, you can't do partial updates. Worse yet, imagine having 20 fields you have to set this way. You can play games by trying to convert the arguments into a dictionary and rejecting null values -- but then you can't set properties to nil if they need to be nil again.

The solution: a double splat operator. Your syntax becomes def resolve(user_id:, **args). From what I can tell, it turns all remaining named arguments into a dictionary -- and I think unnamed arguments would become an array. Not sure how it would react with a mix of the two.
Full model becomes:
argument :user_id, ID, required: true#, loads: Types::UserType
argument :name, String, required: false
argument :email, String, required: false
field :user, Types::UserType, null: true
field :errors, Types::UserType, null: true
def resolve(user_id:, **args)
user = User.find(user_id)
if user.update(args)
{
user: user,
errors: []
}
else
{
user: nil,
errors: user.errors.full_messages
}
end
end
end

Related

How to properly update model using graphql-ruby?

I'm working on a side project to learn implementation of GraphQL into a Rails 6 app. To do this, I'm using the graphql-ruby gem.
I've got a resolve method to update a Medium model that looks like this:
module Mutations
module Media
class UpdateMedia < GraphQL::Schema::Mutation
include ::GraphqlAuthenticationConcerns
include ::GraphqlActiveModelConcerns
description 'Update Media'
argument :id, Integer, required: true
argument :title, String, required: false
argument :preview_url, String, required: false
argument :preview_image, String, required: false
argument :watched, Boolean, required: false
field :success, Boolean, null: false
field :errors, [Types::ActiveModelError], null: false
field :media, Types::MediumType, null: false
def resolve(id:, title:, release_date:, preview_url:, preview_image:, watched:)
authenticate_user!
media = Medium.find(id)
media_params = {
title: title,
preview_url: preview_url,
preview_image: preview_image,
watched: watched,
}
if media.update(media_params)
success_response(media)
else
failed_response(media)
end
end
private
def success_response(media)
{
success: true,
errors: [],
media: media
}
end
def failed_response(media)
{
success: false,
errors: errors(media)
}
end
end
end
end
If I set up the arguments in this way and I want to only update the watched field, I receive a 500 error stating missing keywords: :title, :release_date, :preview_url, :preview_image.
I saw this issue in the graphql-ruby repo from someone with the same problem, however they were told to set default values to nil, and when I tried this it of course sets every column for that model to nil.
I want to be able to change just the fields that are actually being passed as arguments, without affecting others. How do I allow for both a required parameter (id), as well as optional arguments?
Finally figured it out. By defining the method like this:
def resolve(id:, **args)
authenticate_user!
media = Medium.find(id)
if media.update(args)
success_response(media)
else
failed_response(media)
end
end
this keeps the id argument as required, and allows other params to pass through without setting the entire record to nil.
Ended up being more of a general Ruby question rather than specific to graphql-ruby.

Rails + Graphql - Failed to implement Game.id

I'm trying to implement a query type that can search by name of the record instead of id. Here's its definition in query_type.rb.
# Get game by name
field :game_by_name, Types::GameType, null: false do
argument :name, String, required: true
end
def game_by_name(name:)
Game.where(name: name) //find a game using the name attribute
end
But when I run:
query {
gameByName(name: "League of Legends") {
id
name
}
}
I get the following error.
Failed to implement Game.id, tried:\n\n
- `Types::GameType#id`, which did not exist\n
- `Game::ActiveRecord_Relation#id`, which did not exist\n
- Looking up hash key `:id` or `\"id\"` on `#<Game::ActiveRecord_Relation:0x00007f5644442888>`, but it wasn't a Hash\n\n
To implement this field, define one of the methods above (and check for typos)\n
This is odd because the following query type works perfectly.
# Get game by ID
field :game_by_id, Types::GameType, null: false do
argument :id, ID, required: true
end
def game_by_id(id:)
Game.find(id)
end
Here's game_type.rb:
module Types
class GameType < Types::BaseObject
field :id, ID, null: false
field :name, String, null: false
end
end
How do I go about fixing this? Thank you!
I stumbled upon this, chasing a similar error. Not sure if you solved it or not, but I believe the issue is in the query type definition.
You're telling it to return a type of Types::GameType, but the Active Record query returns an Active Record Relation, which is a collection. So, graphql is expecting a single instance of Game, but is instead receiving a collection. Graphql is then trying to map the returned value from the query to the type definition, but is unable to. The best hint is from this line:
Looking up hash key `:id` or `\"id\"` on `#<Game::ActiveRecord_Relation:0x00007f5644442888>`, but it wasn't a Hash..
Graphql is trying to assign :id to the ActiveRecord_Relation and it can't do it.
Two paths forward, depending on how you want the API to behave. Do you want it to return 1 record or many?
Wrapping the Types::GameType within brackets will tell graphql it's a collection and to iterate over the records
# Get game by name
field :game_by_name, [Types::GameType], null: false do
argument :name, String, required: true
end
def game_by_name(name:)
Game.where(name: name) //find a game using the name attribute
end
or have Active Record return just 1 record, something like...
# Get game by name
field :game_by_name, Types::GameType, null: false do
argument :name, String, required: true
end
def game_by_name(name:)
Game.where(name: name).limit(1).first //find a game using the name attribute
end
I know this is months old, but just putting it out there for anyone else who stumbles upon this question, like I did!

Ruby on Rails 4.2 enum attributes

I'm trying to use new Enum type, everything works well except one issue. When writing functional tests I usually use structure:
order = Order.new(o_status: :one)
post :create, order: order.attributes
# Error message:
# ArgumentError: '0' is not a valid o_status
It's ok as long as I don't have Enum attribute. The problem with enums is that instead of String value .attributes returns it's Integer value which can't be posted as enum attribute value.
In above example model can look like this:
class Order < ActiveRecord::Base
enum o_status: [:one, :two]
end
I figured out that when I do:
order = Order.new(o_status: :one)
atts = order.attributes
atts[:o_status] = "one" # it must be string "one" not symbol or integer 0
post :create, order: order.attributes
It will work OK.
Is it normal or there is some better solution?
EDIT:
The only workaround which I found looks like this:
order = { o_status: :one.to_s }
post :create, order: order
pros: It is short and neat
cons: I cannot validate order with order.valid? before sending with post
This doesn't solve issue with order.attributes when there is Enum inside.
From the Enum documentation:
You can set the default value from the database declaration, like:
create_table :conversations do |t|
t.column :status, :integer, default: 0
end
Good practice is to let the first declared status be the default.
Best to follow that advice and avoid setting a value for an enum as part of create. Having a default value for a column does work in tests as well.

Mongoid queries not returning anything on new model fields

I have a Rails application where I am trying to iterate over each object in a Model class depending on whether the object has been archived or not.
class Model
include Mongoid::Document
include Mongoid::Timestamps
field :example_id, type: Integer
field :archived, type: Boolean, default: false
def archive_all
Model.all.where(archived: false).each do |m|
m.archive!
end
end
end
However, the where clause isn't returning anything. When I go into the console and enter these lines, here is what I get:
Model.where(example_id: 3).count #=> 23
Model.where(archived: false).count #=> 0
Model.all.map(&:archived) #=> [false, false, false, ...]
I have other where clauses throughout the application and they seem to work fine. If it makes any difference, the 'archived' field is one that I just recently added.
What is happening here? What am I doing wrong?
When you say:
Model.where(archived: false)
you're looking for documents in MongoDB the archived field is exactly false. If you just added your archived field then none of the documents in your database will have that field (and no, the :default doesn't matter) so there won't be any with archived: false. You're probably better off looking for documents where archived is not true:
Model.where(:archived.ne => true).each(&:archive!)
You might want to add a validation on archived to ensure that it is always true or false and that every document has that field.

Mongoid doesn't change the value of Array if sent params with missing field

I'm using Rails 4.2 and using the .update_attributes method.
After some debugging it's come to my attention that if the field is MISSING from the params, Mongoid will just keep the old value of the param.
Edit: It seems that no matter what I do, the following code doesn't work:
campaign = Campaign.find_by username: params[:id]
campaign.update_attributes params[:campaign].permit!
Here's what does work:
campaign.attributes = params[:campaign].permit!
# .update_attributes(params[:camapign]) would likely work, too
campaign.allowed_brokers = params[:campaign][:allowed_brokers]
campaign.custom_payouts = params[:campaign][:custom_payouts]
campaign.save
Both allowed_brokers and custom_payouts are Array type fields. It seems that line 1 takes care of anything that's not an array field.
I'm 100% sure that incoming params don't contain the old values (in fact, they're missing from params[:campaign] - there's no params[:campaign][:allowed_bidders] for example.
Why is Mongoid not updating the array fields?
Thanks in advance.
PS: Here's the model:
class Campaign
include Mongoid::Document
include Mongoid::Timestamps
include Mongoid::Attributes::Dynamic
field :name, type: String
# ==== Other irrelevant fields ====
# List of brokers and corresponding share %
field :custom_payouts, type: Array, default: []
# Taggable strings
field :allowed_brokers, type: Array, default: []
field :allowed_bidders, type: Array, default: []
field :can_start, type: Array, default: []
field :can_pause, type: Array, default: []
# Associations
has_and_belongs_to_many :bidders
end
PPS: I'm still trying to figure out why I have to manually amend the array fields. Meanwhile I've also found that despite the fact the fields are of type Array with a defined default of [], they can be set to null directly in the database which is bad. How do I avoid that? Maybe Attributes::Dynamic has something to do with that? If so, how'd I manually amend Arrays without it?

Resources