Create a GraphQL interface by implementing nodes and calls, then running queries.
- See test implementation in
/spec/support/dummy_app/nodes.rb
- See
graphql-ruby-demo
with Rails on github or heroku
Create a GraphQL interface:
- Implement nodes that wrap objects in your application
- Implement calls that expose those objects (and may mutate the application state)
- Execute queries on the system.
API docs: Ruby gem, master branch
Nodes are delegators that wrap objects in your app. You must whitelist fields by declaring them in the class definition.
class FishNode < GraphQL::Node
exposes "Fish"
cursor(:id)
field.number(:id)
field.string(:name)
field.string(:species)
# specify an `AquariumNode`:
field.aquarium(:aquarium)
end
You can also declare connections between objects:
class AquariumNode < GraphQL::Node
exposes "Aquarium"
cursor(:id)
field.number(:id)
field.number(:occupancy)
field.connection(:fishes)
end
You can make custom connections:
class FishSchoolConnection < GraphQL::Connection
type :fish_school # now it is a field type
call :largest, -> (prev_value, number) { fishes.sort_by(&:weight).first(number.to_i) }
field.number(:count) # delegated to `target`
field.boolean(:has_more)
def has_more
# the `largest()` call may have removed some items:
target.count < original_target.count
end
end
Then use them:
class AquariumNode < GraphQL::Node
field.fish_school(:fishes)
end
And in queries:
aquarium(1) {
name,
occupancy,
fishes.largest(3) {
edges {
node { name, species }
},
count,
has_more
}
}
}
Calls selectively expose your application to the world. They always return values and they may perform mutations.
Calls declare returns, declare arguments, and implement #execute!
.
This call just finds values:
class FindFishCall < GraphQL::RootCall
returns :fish
argument.number(:id)
def execute!(id)
Fish.find(id)
end
end
This call performs a mutation:
class RelocateFishCall < GraphQL::RootCall
returns :fish, :previous_aquarium, :new_aquarium
argument.number(:fish_id)
argument.number(:new_aquarium_id)
def execute!(fish_id, new_aquarium_id)
fish = Fish.find(fish_id)
# context is defined by the query, see below
if !context[:user].can_move?(fish)
raise RelocateNotAllowedError
end
previous_aquarium = fish.aquarium
new_aquarium = Aquarium.find(new_aquarium_id)
fish.update_attributes(aquarium: new_aquarium)
{
fish: fish,
previous_aquarium: previous_aquarium,
new_aquarium: new_aquarium,
}
end
end
When your system is set up, you can perform queries from a string.
query_str = "find_fish(1) { name, species } "
query = GraphQL::Query.new(query_str)
result = query.as_result
result
# {
# "1" => {
# "name" => "Sharky",
# "species" => "Goldfish",
# }
# }
Each query may also define a context
object which will be accessible at every point in execution.
query_str = "move_fish(1, 3) { fish { name }, new_aquarium { occupancy } }"
query_ctx = {user: current_user, request: request}
query = GraphQL::Query.new(query_str, context: query_ctx)
result = query.as_result
result
# {
# "fish" => {
# "name" => "Sharky"
# },
# "new_aquarium" => {
# "occupancy" => 12
# }
# }
You could do something like this inside a Rails controller.
- testing with JSON args
- Make root calls plain ol' calls, on the root?
- Make fields like calls with no args?
- improve debugging experience
- build nodes for Date, DateTime, Time, Hash
- How do you express failure? HTTP response?
errors
key? - Handle blank objects in nested calls (how? wait for spec)
- Implement calls as arguments
- double-check how to handle
pals.first(3) { count }
- Implement call argument introspection (wait for spec)