GitHub - BurnThis/graphql-ruby: Ruby implementation of Facebook's GraphQL

Build Status Gem Version Dependency Status Code Climate Test Coverage

Create a GraphQL interface by implementing nodes and connections, then running queries.

To do:

  • Allow a default connection class, or some way to infer connection from name
    • right now, Introspection::Connection isn't getting used, only ApplicationConnection is.
  • How do you express failure? HTTP response? errors key?
  • Handle blank objects in nested calls
  • Implement calls as arguments
  • double-check how to handle pals.first(3) { count }
  • Implement call argument introspection (wait for spec)
  • For fields that return objects, can they be queried without other fields? Or must they always have fields?
  • document (wait for spec)

Example Implementation

gql

Usage

  • Implement nodes that wrap objects in your application
  • Implement calls that return those objects (and may mutate the application state)
  • Execute queries and return the result.

Nodes

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)
  field.object(: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

Calls

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

Queries

When your system is setup, 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.