Previous: Chapter 6 - The Round
To finish up our user story, we need to expose rounds through our API. We’ll do that with a new root-level field named rounds and new types, RoundType and SelectionType. Most fields won’t be noteworthy, except for two. We’ll add a selections field to RoundType and a user field to SelectionType.
Generate a RoundType.
Terminal
bin/rails g graphql:object Round selections:\[Selection\]
Make sure selections can’t be null.
app/graphql/types/round_type.rb
module Types
class RoundType < Types::BaseObject
field :id, ID, null: false
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
field :selections, [Types::SelectionType], null: false
end
end
Generate a SelectionType.
Terminal
bin/rails g graphql:object Selection user:User
We’ll make two changes to SelectionType. First we’ll remove round_id and user_id since we’ll want to leverage the graph instead of these particular ID fields. Second, we’ll make sure user can’t be null.
Make those changes.
app/graphql/types/selection_type.rb
module Types
class SelectionType < Types::BaseObject
field :id, ID, null: false
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
field :user, Types::UserType, null: false
end
end
You might notice that RoundType has a selections field but the Round model doesn’t have any method with that name. Before we fix that, let’s talk about order. You might think that either the database or Rails will use a default sort order. You might even fetch some rows in the console and see results ordered by ID. That would be a coincidence. PostgreSQL does not actually guarantee row order, unless specified. Since we want results to be exact and consistent, we should alway specify. We could do this by adding a custom selections method on RoundType, but I would rather avoid custom methods on model types. The approach we’ll take is to set the order when declaring the relation.
Add the relation.
app/models/round.rb
class Round < ApplicationRecord
has_many :selections, -> { order(:id) }
end
Add a test.
test/models/round_test.rb
require "test_helper"
class RoundTest < ActiveSupport::TestCase
context "associations" do
should have_many(:selections).order(:id)
end
end
Run the new test.
Terminal
bin/rails t test/models/round_test.rb
Now we’re ready to add our rounds field. You might be tempted to just write Round.all. I see it often, but we want results to be exact and consistent so we will use Round.all.order(:id).
Add the new field.
app/graphql/types/query_type.rb
module Types
class QueryType < Types::BaseObject
# Add `node(id: ID!) and `nodes(ids: [ID!]!)`
include GraphQL::Types::Relay::HasNodeField
include GraphQL::Types::Relay::HasNodesField
# Add root-level fields here.
# They will be entry points for queries on your schema.
field :rounds, [RoundType], null: false,
description: "List all rounds in creation order"
field :users, [Types::UserType], null: false,
description: "List all users in email order"
field :user, Types::UserType, "Find a user by ID", null: false do
argument :id, ID
end
def rounds
Round.all.order(:id)
end
def users
User.all.order(:email)
end
def user(id:)
User.find(id)
end
end
end
You might be wondering how we can write exact and consistent tests when our fixtures do not specify IDs. We could specify IDs for all of our fixtures, but Rails will actually generate consistent IDs based on the fixture’s label. Armed with this knowledge, we can spy on the ordering using something like Selection.all.order(:id).map { |s| s.user.name }.
Generate a test for the new field.
Terminal
bin/rails g integration_test types/query_type/rounds
Fill in the generated test.
test/integration/types/query_type/rounds_test.rb
require "test_helper"
class Types::QueryType::RoundsTest < ActionDispatch::IntegrationTest
test "rounds" do
query = <<~GRAPHQL
{
rounds {
id
createdAt
updatedAt
selections {
id
createdAt
updatedAt
user {
id
name
email
createdAt
updatedAt
}
}
}
}
GRAPHQL
post graphql_path, params: { query: query }
rounds_in_id_order = {
rounds(:full) => [
selections(:full_adrian),
selections(:full_daniel),
selections(:full_chantel),
selections(:full_sarah),
],
rounds(:empty) => [],
}
assert_equal(
{
"data" => {
"rounds" => rounds_in_id_order.map { |round, selections|
{
"id" => round.id.to_s,
"createdAt" => round.created_at.iso8601,
"updatedAt" => round.updated_at.iso8601,
"selections" => selections.map { |selection|
{
"id" => selection.id.to_s,
"createdAt" => selection.created_at.iso8601,
"updatedAt" => selection.updated_at.iso8601,
"user" => {
"id" => selection.user.id.to_s,
"name" => selection.user.name,
"email" => selection.user.email,
"createdAt" => selection.user.created_at.iso8601,
"updatedAt" => selection.user.updated_at.iso8601,
},
}
},
}
},
},
},
@response.parsed_body
)
end
end
Run the new test.
Terminal
bin/rails t test/integration/types/query_type/rounds_test.rb
Check for regressions by running all tests.
Terminal
bin/rails t
Success!
✅ Make a commit
✅ As an app user, I want to see all rounds and selections, so that I can see selection history.
Next: Chapter 8 - The N