loading...

Ruby on Rails GraphQL API Tutorial: Filtering with Custom Fields and Class Methods

isalevine profile image Isa Levine Updated on ・7 min read

Ruby on Rails GraphQL API Tutorial (3 Part Series)

1) Ruby on Rails GraphQL API Tutorial: From 'rails new' to First Query 2) Ruby on Rails GraphQL API Tutorial: Creating Data with Mutations 3) Ruby on Rails GraphQL API Tutorial: Filtering with Custom Fields and Class Methods

Building on our Rails GraphQL API from previous articles, we will now look at filtering data using two tools:

  1. Custom Fields in our GraphQL Queries
  2. Class Methods directly on our Rails Models (as inspired by this great StackOverflow response)

We will add a status column to our Payments table, and add a filter to only return "Successful" payments in our queries.

Once again, this tutorial is largely adapted from this AMAZING tutorial by Matt Boldt. Thanks again, Matt!!

Overview

In this third article, we'll go through the steps to:

  • add a status column to our Payments table (and update our seed file)
  • add a successful class method to our Order model to filter by "Successful" payments
  • add a custom field to our GraphQL order_type to call the .successful class method
  • write and execute a GraphQL query to demonstrate the filter in Insomnia

Let's dive in!

Use Case: Filtering by Status

Let's say we want our API to know the difference between "Successful" and "Failed" payments. This would allow us to use only "Successful" payments when doing things like calculating a total balance, generating receipts, or other situations where we don't want to expose every single payment.

Rails Migration: Add status to Payments

Run rails g migration AddColumnStatusToPayments. This will create a new database migration file. Open it up, and create a new column for status on the Payments table:

# /db/migrate/20190929153644_add_column_status_to_payments.rb

class AddColumnStatusToPayments < ActiveRecord::Migration[5.2]
  def change
    add_column :payments, :status, :string
  end
end

We'll also update our seed file to add some "Successful" payments to our database, along with one "Failed" payment on our first Order:

# /db/seeds.rb

Order.destroy_all
Payment.destroy_all

order1 = Order.create(description: "King of the Hill DVD", total: 100.00)
order2 = Order.create(description: "Mega Man 3 OST", total: 29.99)
order3 = Order.create(description: "Punch Out!! NES", total: 0.75)

payment1 = Payment.create(order_id: order1.id, amount: 20.00, status: "Successful")
payment2 = Payment.create(order_id: order2.id, amount: 1.00, status: "Successful")
payment3 = Payment.create(order_id: order3.id, amount: 0.25, status: "Successful")
payment4 = Payment.create(order_id: order1.id, amount: 5.00, status: "Failed")

Now, run rails db:migrate and rails db:seed to run the migration and re-seed the database.

Check out our database with rails c

Go ahead and run rails c to open up a Rails console in your terminal.

Remember that we created our Order model to have a has_many relationship with Payments:

# /app/models/order.rb

class Order < ApplicationRecord
    has_many :payments
end

In our Rails console, we can use Order.all[0] to check out the first Order in the database, and Order.all[0].payments to see its Payments:

[10:28:27] (master) devto-graphql-ruby-api
// ♥ rails c

Running via Spring preloader in process 6225
Loading development environment (Rails 5.2.3)

2.6.1 :001 > Order.all[0]
  Order Load (0.5ms)  SELECT "orders".* FROM "orders"
 => #<Order id: 16, description: "King of the Hill DVD", total: 100.0, created_at: "2019-09-29 17:20:34", updated_at: "2019-09-29 17:20:34"> 

2.6.1 :002 > Order.all[0].payments
  Order Load (0.2ms)  SELECT "orders".* FROM "orders"
  Payment Load (0.2ms)  SELECT  "payments".* FROM "payments" WHERE "payments"."order_id" = ? LIMIT ?  [["order_id", 16], ["LIMIT", 11]]
 => #<ActiveRecord::Associations::CollectionProxy [#<Payment id: 1, order_id: 16, amount: 20.0, created_at: "2019-09-29 17:20:34", updated_at: "2019-09-29 17:20:34", status: "Successful">, #<Payment id: 4, order_id: 16, amount: 5.0, created_at: "2019-09-29 17:20:34", updated_at: "2019-09-29 17:20:34", status: "Failed">]> 

Cool! We can see that our first Order has both a "Successful" and a "Failed" payment in its associations.

Now, let's look at filtering our results to only return "Successful" payments with our GraphQL queries!

Class Methods on Rails Models

In my previous Rails projects, I didn't do much in my Models' files beyond setting up has_many / belongs_to relationships. However, a great StackOverflow discussion showed me we can expand a has_many declaration with additional functionality. The article itself demonstrates this with a has_many-through relationship, but the pattern works the same for simple has_many relationships too!

Open up our Order model, and build out the has_many declaration by adding a do...end block:

# /app/models/order.rb

class Order < ApplicationRecord
    has_many :payments do
        # we can add additional functionality here!
    end
end

This is the perfect place to filter our payments: any methods we define here can be chained directly onto order.payments!

Let's make a method to use SQL to filter payments to only "Successful" ones:

# /app/models/order.rb

class Order < ApplicationRecord
    has_many :payments do
        def successful
            where("status = ?", "Successful")
        end
    end
end

Now, if we run order.payments.successful, we will automatically invoke the ActiveRecord where method. This will only allow payments with the status equal to "Successful" to be returned!

Save the order.rb file, and open up a Rails console with rails c again. Now run Order.all[0].payments, then Order.all[0].payments.successful to see the filter in action:

[10:41:36] (master) devto-graphql-ruby-api
// ♥ rails c

Running via Spring preloader in process 6277
Loading development environment (Rails 5.2.3)

2.6.1 :001 > Order.all[0].payments
  Order Load (1.0ms)  SELECT "orders".* FROM "orders"
  Payment Load (0.2ms)  SELECT  "payments".* FROM "payments" WHERE "payments"."order_id" = ? LIMIT ?  [["order_id", 16], ["LIMIT", 11]]
 => #<ActiveRecord::Associations::CollectionProxy [#<Payment id: 1, order_id: 16, amount: 20.0, created_at: "2019-09-29 17:20:34", updated_at: "2019-09-29 17:20:34", status: "Successful">, #<Payment id: 4, order_id: 16, amount: 5.0, created_at: "2019-09-29 17:20:34", updated_at: "2019-09-29 17:20:34", status: "Failed">]> 

2.6.1 :002 > Order.all[0].payments.successful
  Order Load (0.2ms)  SELECT "orders".* FROM "orders"
  Payment Load (0.4ms)  SELECT  "payments".* FROM "payments" WHERE "payments"."order_id" = ? AND (status = 'Successful') LIMIT ?  [["order_id", 16], ["LIMIT", 11]]
 => #<ActiveRecord::AssociationRelation [#<Payment id: 1, order_id: 16, amount: 20.0, created_at: "2019-09-29 17:20:34", updated_at: "2019-09-29 17:20:34", status: "Successful">]> 

Great! Now we can chain order.payments.successful to use this filter. Now let's connect this functionality to our GraphQL query!

Add a Custom Field to a Query

Update PaymentType with the new :status field

Turning our attention back to our GraphQL Types, here's what our current PaymentType and its fields look like:

# /app/graphql/types/payment_type.rb

module Types
  class PaymentType < Types::BaseObject
    field :id, ID, null: false
    field :amount, Float, null: false
  end
end

Since we've added a status column to the Rails model, let's add a :status field to our GraphQL type:

# /app/graphql/types/payment_type.rb

module Types
  class PaymentType < Types::BaseObject
    field :id, ID, null: false
    field :amount, Float, null: false
    field :status, String, null: false
  end
end

We can now update our previous query for allOrders to include :status too:

query {
    allOrders {
        id
        description
        total      
        payments {
            id
            amount
            status
        }
        paymentsCount
    }
}

Run rails s to start the Rails server, then send the query in Insomnia to http://localhost:3000/graphql/ :

GraphQL query in Insomnia showing status field added

Now let's get filterin'!

Update OrderType with a new :successful_payments custom field

Our OrderType currently looks like this:

# /app/graphql/types/order_type.rb

module Types
  class OrderType < Types::BaseObject
    field :id, ID, null: false
        field :description, String, null: false
        field :total, Float, null: false
        field :payments, [Types::PaymentType], null: false
        field :payments_count, Integer, null: false

        def payments_count
            object.payments.size
        end
  end
end

Our :payments field uses the has_many relationship to pull all the belonging PaymentType instances into the response.

We also have one custom field, :payments_count, where we can call class methods from the Order object. (Don't forget that quirk about using object to refer to the Order instance!)

Let's add a new custom field, :successful_payments, and define a method (with the same name) that will simply use our new order.payments.successful method chain:

# /app/graphql/types/order_type.rb

module Types
  class OrderType < Types::BaseObject
    field :id, ID, null: false
        field :description, String, null: false
        field :total, Float, null: false
        field :payments, [Types::PaymentType], null: false
        field :payments_count, Integer, null: false
        field :successful_payments, [Types::PaymentType], null: false

        def payments_count
            object.payments.size
        end

        def successful_payments
            object.payments.successful
        end
  end
end

Our new custom field :successful_payments returns an array of PaymentTypes via [Types::PaymentType] (just like the :payments field does). We also set null: false by default to catch errors with the data.

Let's update the allOrders query to include the new :successful_payments field. (I've also taken out the payments and paymentCount fields.)

Don't forget to change the snake_case to camelCase! (:successful_payments => successfulPayments)

query {
    allOrders {
        id
        description
        total      
        successfulPayments {
            id
            amount
            status
        }
    }
}

Start the Rails server with rails s, and run the query in Insomnia:

GraphQL query in Insomnia showing successfulPayments field

Awesome! Now, our Rails model Order is providing a simple, one-word .successful filter for its Payments. Using it in our GraphQL query is as simple as making a new field that calls that method!

Conclusion

We've now implemented a GraphQL API with the ability to filter the Payments belonging to an Order by their "Successful" status! From here, we can build additional functionality to use the successful payments` data--for instance, calculating a current balance based on the order's total.

Here's the repo for the code in this article, too. Feel free to tinker around with it!

Here's another shameless plug for that awesome StackOverflow reply demonstrating how you can build out functionality on a Rails model's has_many and has_many-through relationship: https://stackoverflow.com/a/9547179

And once again -- thank you to Matt Boldt and his AWESOME Rails GraphQL tutorial for helping me get this far! <3

Any tips or advice for using GraphQL in Rails, or GraphQL in general? Feel free to contribute below!

Ruby on Rails GraphQL API Tutorial (3 Part Series)

1) Ruby on Rails GraphQL API Tutorial: From 'rails new' to First Query 2) Ruby on Rails GraphQL API Tutorial: Creating Data with Mutations 3) Ruby on Rails GraphQL API Tutorial: Filtering with Custom Fields and Class Methods

Posted on by:

isalevine profile

Isa Levine

@isalevine

Isa (ee-suh). She/her pronouns. Full stack developer working with Rails and Vue. Drinks too much bubbly water.

Discussion

markdown guide
 

Hi Isa, thanks for these tutorials, so great to get started with graphql.

I ran into a problem, looks like there shouldn't be a do in this def:

def successful do
  where("status = ?", "Successful")
end
 

Yes, you are absolutely right Maia--thank you for catching that! The only do should be after has_many :payments do. Snippet has been corrected! :)