Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Beyond REST: GraphQL in Ruby on Rails

vipulnsward
September 06, 2019

Beyond REST: GraphQL in Ruby on Rails

GraphQL in Ruby on Rails

vipulnsward

September 06, 2019
Tweet

More Decks by vipulnsward

Other Decks in Technology

Transcript

  1. 10

  2. # Add graphql-ruby boilerplate and mount graphiql in development rails

    g graphql:install # Make your first object type rails g graphql:object Post title:String rating:Int comments:[Comment]
  3. # app/graphql/types/post_type.rb class Types::PostType < Types::BaseObject field :id, ID, null:

    false field :title, String, null: false field :truncated_preview, String, null: false field :comments, [Types::CommentType], null: true end
  4. class QueryType < GraphQL::Schema::Object description "The query root of this

    schema" # First describe the field signature: field :post, PostType, null: true do description "Find a post by ID" argument :id, ID, required: true end # Then provide an implementation: def post(id:) Post.find(id) end end
  5. query_string = " { post(id: 1) { id title truncatedPreview

    } }" result_hash = Schema.execute(query_string) # { # "data" => { # "post" => { # "id" => 1, # "title" => "GraphQL is nice" # "truncatedPreview" => "GraphQL is..." # } # } # }
  6. type User { email: String handle: String! friends: [User!]! }

    GraphQL Schema Definition Language(SDL)
  7. class Types::User < GraphQL::Schema::Object field :email, String, null: true field

    :handle, String, null: false field :friends, [User], null: false end
  8. class Types::TodoList < Types::BaseObject field :name, String, null: false field

    :is_completed, String, null: false # Related Object: field :owner, Types::User, null: false # List field: field :viewers, [Types::User], null: false # Connection/for pagination-relay: field :items, Types::TodoItem.connection_type, null: false do argument :status, TodoStatus, required: false end end
  9. SCALAR field :name, String field :top_score, Integer, null: false field

    :avg_points_per_game, Float, null: false field :is_top_ranked, Boolean, null: false field :id, ID, null: false field :created_at, GraphQL::Types::ISO8601DateTime, null: false field :parameters, GraphQL::Types::JSON, null: false CustomScalars
  10. ENUMS class Types::MediaCategory < Types::BaseEnum value "AUDIO", "An audio file,

    such as music or spoken word" value "IMAGE", "A still image, such as a photo or graphic" value "TEXT", "Written words" value "VIDEO", "Motion picture, may have audio" end
  11. MySchema.multiplex([ {query: query_string_1}, {query: query_string_2}, {query: query_string_3}, ]) # [

    # { "data" => { ... } }, # { "data" => { ... } }, # { "data" => { ... } }, # ]
  12. context = { current_user: current_user, routes: Rails.application.routes.url_helpers, } query_string =

    "{ ... }" MySchema.execute(query_string, context: context) # { # "data" => { ... } # }
  13. query_string = " query getPost($postId: ID!) { post(id: $postId) {

    title } }" variables = { "postId" => "1" } MySchema.execute(query_string, variables: variables)
  14. class Queries::Commit::Detail < Queries::BaseResolver type Types::CommitType, null: false argument :id,

    String, required: true def resolve(id:) Commit.find(id) rescue ActiveRecord::RecordNotFound => error GraphQL::ExecutionError.new(error.message) end end
  15. class Mutations::PublishCommit < Mutations::BaseMutation type Types::MutationResponseType argument :id, String, required:

    true argument :body, String, required: false def resolve(id:, body: nil) commit = Commit.find(id) create_or_update_post(body, commit) if body.present? commit.published! commit rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid => error GraphQL::ExecutionError.new(error.message) end private def create_or_update_post(body, commit) post = Post.find_or_initialize_by(commit_id: commit.id) post.update body: body end end
  16. class Mutations::PublishCommit < Mutations::BaseMutation ... def resolve(id:, body: nil) commit

    = Commit.find(id) create_or_update_post(body, commit) if body.present? commit.published! commit rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid => error raise GraphQL::ExecutionError.new(error.message) # OR raise GraphQL::ExecutionError.new(commit.full_messages.join(", ")) end end
  17. class Types::UserError < Types::BaseObject field :message, String, null: false field

    :path, [String], null: true end
 class Mutations::UpdatePost < Mutations::BaseMutation # ... field :errors, [Types::UserError], null: false end

  18. def resolve(id:, attributes:) post = Post.find(id) if post.update(attributes) { post:

    post, errors: [], } else # Convert Rails model errors into GraphQL-ready error hashes user_errors = post.errors.map do |attribute, message| path = ["attributes", attribute.camelize] { path: path, message: message, } end { post: post, errors: user_errors, } end end
  19. class ChangelogSchema < GraphQL::Schema ... rescue_from(ActiveRecord::NotFound) do |err, obj, args,

    ctx, field| # Raise a graphql-friendly error with a custom message raise GraphQL::ExecutionError, "#{field.type.unwrap.graphql_name} not found" end end
  20. class LazyFindPerson def initialize(query_ctx, person_id) @person_id = person_id @lazy_state =

    query_ctx[:lazy_find_person] ||= { pending_ids: Set.new, loaded_ids: {}, } @lazy_state[:pending_ids] << person_id end def person loaded_record = @lazy_state[:loaded_ids][@person_id] if loaded_record loaded_record else pending_ids = @lazy_state[:pending_ids].to_a people = Person.where(id: pending_ids) people.each { |person| @lazy_state[:loaded_ids][person.id] = person } @lazy_state[:pending_ids].clear @lazy_state[:loaded_ids][@person_id] end end end
  21. class MySchema < GraphQL::Schema # ... lazy_resolve(LazyFindPerson, :person) end field

    :author, PersonType, null: true def author LazyFindPerson.new(context, object.author_id) end 
 
 { p1: post(id: 1) { author { name } } p2: post(id: 2) { author { name } } p3: post(id: 3) { author { name } } }