/graphql-ruby

Ruby implementation of Facebook's GraphQL

Primary LanguageRubyMIT LicenseMIT

graphql

Build Status Gem Version Code Climate Test Coverage built with love

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

Example Implementation

Usage

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

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

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 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.

To Do:

  • 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)