Ruby GraphQL: How to order arguments in an option? - ruby-on-rails

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

Related

How to set up an enum to allow multiple enums in a query? GraphQL Ruby

I have an enum, item_status.rb defined in app/graphql/graph/enums that looks something like this:
Graph::Enums::ItemStatus = GraphQL::EnumType.define do
name "ItemStatus"
description "lorem ipsum"
value "GOOD", "item is good", value: "good"
value "NORMAL", "item is normal", value: "normal"
value "BAD", "item is bad", value: "bad"
end
It's defined in my schema.graphql like this:
enum ItemStatus {
GOOD
NORMAL
BAD
}
I want to send a query from my front-end that contains multiple values for ItemStatus, for example in my queryVariables I'd send status: ["GOOD", "NORMAL"] and I want to receive all items with either of those statuses back. How do I modify my schema to accept an array of values as well as a single value?
Wouldn't the argument in your query just be an array of strings?
field :your_field, YourType do
argument :status, [String], required: true
end
def your_field(status:)
Mymodel.where(status: status)
end
I might be missing something about what you are trying to do though.
I prefer you need to send the array of enum like [GOOD,NORMAL] for query variable
Define your graphql field like this:
field :Items, ReturnType do
argument :statuses, [Graph::Enums::ItemStatus], required: true
end
def Items(statuses:)
Item.where(status: statuses)
end

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

Form helper for custom ransacker args - Rails

According to Ransack documentation, there is a possibility to pass custom arguments to a ransacker method:
class Person < ApplicationRecord
ransacker :author_max_title_of_article_where_body_length_between, args: [:parent, :ransacker_args] do |parent, args|
min, max = args
query = <<-SQL
(SELECT MAX(articles.title)
FROM articles
WHERE articles.person_id = people.id
AND CHAR_LENGTH(articles.body) BETWEEN #{min.to_i} AND #{max.to_i}
GROUP BY articles.person_id
)
SQL
Arel.sql(query)
end
end
In the same documentation there is even an example of passing custom arguments:
Person.ransack(
conditions: [{
attributes: {
'0' => {
name: 'author_max_title_of_article_where_body_length_between',
ransacker_args: [10, 100]
}
},
predicate_name: 'cont',
values: ['Ransackers can take arguments']
}]
)
However, there is no documentation on how can I pass that custom arguments from a view, as there appears to be no search_form_for helpers to do that.
Is there a way of passing these arguments through the form, without needing to parse the parameters in the controller to perform the search as pointed in the example?

Reuse GraphQL arguments in Ruby

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!

How to conditionally require a parameter based on a previous parameter

I am working with apipie for my rails app API and have a parameter that needs to be conditionally required. If a user is hired, they need to have a hired_at date. For other reasons those must be 2 separate columns. I cannot just check for the presence of a hired_at date...
What I currently have is essentially the following:
param :person, Hash, desc: "person information", required: true do
param :name, String, desc: "person name", required: true
param :hired, [true, false], desc: "person has been hired", required: true
param :dates, Array, of: Hash, desc: "important dates", required: true do
param :hired_at, String, desc: "date of fire", required: <if hired == true >
end
end
the <if hired==true> is pseudocode. That is the logic I need there but I don't know how to implement it.

Resources