Reuse GraphQL arguments in Ruby - ruby-on-rails

I'm building my first GraphQL api using rails and the graphql-ruby gem. So far it's been very simple and just awesome.
I'm kinda stuck now in regards to duplicate code. I've got a project management rails app which has spaces, todos (they belong to a space), users.
What I want to achieve is to be able to query spaces and its todos but also all the todos for the current user for example. For todos I want to be able to filter them using different arguments:
Done - Boolean,
Scope - String (today or thisWeek),
Assignee - Integer (Id of a user)
query {
space(id: 5) {
todos(done: false) {
name
done
dueAt
}
}
}
query {
me {
todos(done: false, scope: today) {
name
done
dueAt
}
}
}
That means everytime I got the field todos I want to be able to filter them whether they are done or due today etc.
I know how to use arguments and everything works fine but currently I got the same coder over and over again. How (and where) could I extract that code to make it reusable everytime I have the same todos field?
field :todos, [TodoType], null: true do
argument :done, Boolean, required: false
argument :scope, String, required: false
argument :limit, Integer, required: false
end

Okay, as #jklina also said I ended up using a custom resolver.
I changed:
field :todos, [TodoType], null: true do
argument :done, Boolean, required: false
argument :scope, String, required: false
argument :limit, Integer, required: false
end
to:
field :todos, resolver: Resolvers::Todos, description: "All todos that belong to a given space"
and added a Resolver:
module Resolvers
class Todos < Resolvers::Base
type [Types::TodoType], null: false
TodoScopes = GraphQL::EnumType.define do
name "TodoScopes"
description "Group of available scopes for todos"
value "TODAY", "List all todos that are due to today", value: :today
value "THISWEEK", "List all todos that are due to the end of the week", value: :this_week
end
argument :done, Boolean, required: false
argument :scope, TodoScopes, required: false
argument :limit, Integer, required: false
argument :assignee_id, Integer, required: false
def resolve(done: nil, scope:nil, limit:5000, assignee_id: nil)
todos = object.todos
# filtering the records...
return todos.limit(limit)
end
end
end
Quite easy!

Related

Graphql-Ruby: Creating interfaces that objects can inherit fields from

module Types::ProgramType
include Types::BaseInterface
description "Objects which inherit from Program"
graphql_name "Program"
orphan_types Types::SomeProgramType, Types::AnotherProgramType, Types::ThirdProgramType
field :id, ID, null: false
field :type, Integer, null: false
field :name, String, null: false
definition_methods do
def self.resolve_type(object, _context)
case object.class
when SomeProgram then SomeProgramType
when AnotherProgram then AnotherProgramType
when ThirdProgram then ThirdProgramType
else
raise "Unknown program type"
end
end
end
end
module Types
class SomeProgramType < Types::BaseObject
implements Types:ProgramType
field :description, String, null: false
end
end
I also added queries for SomeProgram types in the query_type file. I was under the impression that adding "implement " to the object types, would allow them to inherit the fields from the interface (per this link: https://graphql-ruby.org/type_definitions/interfaces) and I would be able to query like this:
query {
someProgram(id: 1) {
name
type
description
}
}
But I am getting errors in graphiql, like "Field 'name' doesn't exist on type 'SomeProgram'". What am I missing?
Update:
My QueryType class:
class QueryType < Types::BaseObject
# Add `node(id: ID!) and `nodes(ids: [ID!]!)`
include GraphQL::Types::Relay::HasNodeField
include GraphQL::Types::Relay::HasNodesField
field :some_programs, [SomeProgramType], null: false,
description: "all some programs"
def some_programs
SomeProgram.all
end
field :some_program, SomeProgramType, null: false do
argument :id, ID, required: true
end
def some_program(id:)
SomeProgram.find(id)
end
end
Can you also share how you expose someProgram in your query type?
You can also debug this by looking into the schema if you are using GraphiQL app. You can see the return type of someProgram which should be type of ProgramType.
You can also update your query to include __typename so maybe start with
query {
someProgram(id: 1) {
__typename
}
}
First to see what is the return type and if it's being properly handled in the resolve_type
I figured out the issue. I had put implements Types:SomeProgram instead of implements Types::SomeProgram. I was missing a colon.

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.

Converting Relay Node ID to Rails ID when updating related records?

Background:
I have a Location model that has_one Address and has_many Rooms. When I want to update a location, either by updating its name, its address or its rooms, I'm using the following InputObjects to do this:
module Types
# Input interface for creating locations
class LocationUpdateType < Types::BaseInputObject
argument :id, ID, required: false
argument :name, String, required: true
argument :address, AddressUpdateType, required: true, as: :address_attributes
argument :rooms, [RoomUpdateType], required: true, as: :rooms_attributes
end
class AddressUpdateType < Types::BaseInputObject
argument :id, ID, required: true
# not sure if I need this or not but it's commented out for now
# argument :location_id, ID, required: true
argument :street, String, required: true
argument :city, String, required: true
argument :state, String, required: true
argument :zip_code, String, required: true
# I'm not using this yet but I'm anticipating it because
# accepts_nested_attributes can use it as an indicator to destroy this address
argument :_destroy, Boolean, required: false
end
class RoomUpdateType < Types::BaseInputObject
argument :id, ID, required: false
# same thing with this ID.
# argument :location_id, ID, required: true
argument :name, String, required: true
# accepts_nested_attributes flag for destroying related room records.
# like above, I'm not using it yet but I plan to.
argument :_destroy, Boolean, required: false
end
end
When I make a GraphQL request, I'm getting the following in my logs:
Processing by GraphqlController#execute as JSON
Variables: {"input"=>
{"id"=>"TG9jYXRpb24tMzM=",
"location"=>
{"name"=>"A New Building",
"address"=>
{"city"=>"Anytown",
"id"=>"QWRkcmVzcy0zMw==",
"state"=>"CA",
"street"=>"444 New Rd Suite 4",
"zipCode"=>"93400"},
"rooms"=>[{"id"=>"Um9vbS00Mw==", "name"=>"New Room"}]}}}
mutation locationUpdate($input:LocationUpdateInput!) {
locationUpdate(input: $input) {
errors
location {
id
name
address {
city
id
state
street
zipCode
}
rooms {
id
name
}
}
}
}
Which makes sense, I don't want to use real IDs on the client but the obfuscated Relay Node IDs.
Problem:
When my request goes to be resolved I'm using this Mutation:
module Mutations
# Update a Location, its address and rooms.
class LocationUpdate < AdminMutation
null true
description 'Updates a locations for an account'
field :location, Types::LocationType, null: true
field :errors, [String], null: true
argument :id, ID, required: true
argument :location, Types::LocationUpdateType, required: true
def resolve(id:, location:)
begin
l = ApptSchema.object_from_id(id)
rescue StandardError
l = nil
end
return { location: nil, errors: ['Location not found'] } if l.blank?
print location.to_h
# return { location: nil }
# This is throwing an error because it doesn't like the Relay Node IDs.
l.update(location.to_h)
return { location: nil, errors: l.errors.full_messages } unless l.valid?
{ location: l }
end
end
end
When I print the hash that gets passed into this resolver I get the following:
{
:name=>"A New Building",
:address_attributes=>{
:id=>"QWRkcmVzcy0zMw==",
:street=>"444 New Rd Suite 4",
:city=>"Anytown",
:state=>"CA",
:zip_code=>"93400"
},
:rooms_attributes=>[{:id=>"Um9vbS00Mw==", :name=>"New Room"}]
}
When l.update runs, I get the following error:
Couldn't find Room with ID=Um9vbS00Mw== for Location with ID=33
This makes perfect sense to me because the Relay Node IDs aren't stored in the database so I guess I'm trying to figure out how to convert the room.id from a Relay Node ID, to the ID in the database.
Now, I could dig through the hash and use the ApptSchema.object_from_id and convert all the Relay Node IDs to Rails IDs but that requires a database hit for each one. I see the documentation for Connections listed here but this looks more like how to deal with queries and pagination.
Do I need to send the database IDs to the client if I plan on updating records with related records? Doesn't that defeat the purpose of Relay Node IDs? Is there a way to configure my Input object types to convert the Relay Node IDs to Rails IDs so I get the proper IDs in the hash sent to my resolver?
After lots of research, it seems that Relay UUIDs aren't really feasible when trying to update records through relationships. I think Relay assumes that you're using something like MongoDB which does this with their record's primary keys by default.
Using friendly_id as #Nuclearman has suggested seems to be the best way to obfuscate urls. What I've decided to do is to add friendly_id to records that I want to view in a "detail mode" like /posts/3aacmw9mudnoitrswkh9vdrt by creating a concern like this:
module Sluggable
extend ActiveSupport::Concern
included do
validates :slug, presence: true
extend FriendlyId
friendly_id :create_slug_id, use: :slugged
private def create_slug_id
# Try to see if the slug has already been created before generating a new one.
#create_slug_id ||= self.slug
#create_slug_id ||= SecureRandom.alphanumeric(24)
end
end
end
And then including the concern in any model you want to have friendly_ids in like so:
class Location < ApplicationRecord
include Sluggable
end
Then in your GraphQL type do something like this:
module Types
class LocationType < Types::BaseObject
field :id, ID, null: false
# Only include this for model types that have friendly_id
field :friendly_id String, null: false
field :name, String, null: false
# AddressType and RoomType classes won't have friendly_id because
# I'm not going to have a url like /location/3aacmw9mudn/address/oitrswkh9vdrt
# or /location/3aacmw9mudn/rooms/oitrswkh9vdrt
field :address, AddressType, null: false
field :rooms, [RoomType], null: false
end
end
module Types
class LocationUpdateType < Types::BaseInputObject
argument :name, String, required: true
argument :address, AddressUpdateType, required: true, as: :address_attributes
argument :rooms, [RoomUpdateType], required: true, as: :rooms_attributes
end
end
You might want to add friendly_id to all your models but my guess is you may only want it for "important" ones. Variables passed to the query no longer use UUIDs for dependent relationships but only for objects you're updating when finding them by ID like so:
{"input"=>
{"id"=>"3aacmw9mudn",
"location"=>
{"name"=>"A New Building",
"address"=>
{"city"=>"Anytown",
"id"=>"4",
"state"=>"CA",
"street"=>"444 New Rd Suite 4",
"zipCode"=>"93400"},
"rooms"=>[{"id"=>"13", "name"=>"New Room"}]}}}
And now your resolver might look something like:
module Mutations
# Update a Location, its address and rooms.
class LocationUpdate < AdminMutation
null true
description 'Updates a locations for an account'
field :location, Types::LocationType, null: true
field :errors, [String], null: true
argument :id, ID, required: true
argument :location, Types::LocationUpdateType, required: true
def resolve(id:, location:)
# Here, we're passing the friendly_id to this resolver (see above)
# not that actual ID of the record.
l = Location.friendly.find(id)
return { l: nil, errors: ['Location not found'] } if l.blank?
l.update(location.to_h)
return { location: nil, errors: l.errors.full_messages } unless l.valid?
{ location: l }
end
end
end

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 GraphQL: How to order arguments in an option?

I am using SearchObject with GraphQL in Ruby on Rails and created an option called :order_by
option :order_by, type: LinksOrderBy, with: :apply_order_by
The type LinksOrderBy is defined by this:
class LinksOrderBy < ::Types::BaseInputObject
argument :created_at, AscDescEnum, required: false
argument :description, AscDescEnum, required: false
argument :id, AscDescEnum, required: false
argument :updated_at, AscDescEnum, required: false
argument :url, AscDescEnum, required: false
end
But when I try something like this in GraphQL query:
{allLinks(order_by:{id: asc, description: desc}) {
id
description
}}
I don't get it in the right order:
def apply_order_by(scope, value)
scope.order(value.arguments.to_h.map{|k, v| k + ' ' + v}.join(', ')) # "description desc, id asc"
end
As you can see the right order should be "id, description", not "description, id".
The ruby Hash instances are not ordered (see Is order of a Ruby hash literal guaranteed?)
To leverage the optional multi sorting options in GraphQL input types, I usually use the following structure:
1 enum to contain all filterable/sortable/searchable field of a resource (ex: UserField)
1 enum to contain the 2 sort directions (asc and desc)
1 field accepting an optional list of { field: UserField!, sortDir: SortDir! } inputs.
This then enables the API consumers to simply do queries like:
allUsers(sort_by: [{field: username, sortDir: desc}, {field: id, sortDir: asc}]) {
# ...
}
And this pattern can be easily re-used for searching and filtering:
allUsers(search: [{field: username, comparator: like, value: 'bob'}]) {}
allUsers(search: [{field: age, comparator: greater_than, value: '22'}]) {} # type casting is done server-side
allUsers(search: [{field: username, comparator: equal, value: 'bob'}]) {} # equivalent of filtering
Eventually, with further deeper work you can allow complex and/or for the input:
allUsers(
search: [
{
left: {field: username, comparator: like, value: 'bob'}
operator: and
right: {field: dateOfBirth, comparator: geater_than, value: '2001-01-01'}
}
]
)
Disclaimer: the last example above is one of the many things I want to implement in my GQL API but I haven't had the time to think it through yet, it's just a draft

Resources