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

GraphQL backend with Ruby

GraphQL backend with Ruby

Implementing GraphQL API with Ruby

Daniel Hejl

April 20, 2017
Tweet

More Decks by Daniel Hejl

Other Decks in Programming

Transcript

  1. PostType = GraphQL::ObjectType.define do name "Post" description "A blog post"

    field :id, !types.ID field :title, !types.String field :body, !types.String field :author, !AuthorType field :comments, types[!CommentType] field :teaser, types.String, "The teaser of the Post" do resolve ->(post, args, ctx) { post.body[0, 40] } end end
  2. QueryType = GraphQL::ObjectType.define do name "Query" description "The query root

    of this schema" field :post do type PostType argument :id, !types.ID description "Find a Post by ID" resolve ->(obj, args, context) { Post.find(args["id"]) } end field :posts do type types[PostType] resolve ->(obj, args, context) { Post.all } end end Schema = GraphQL::Schema.define do query QueryType end
  3. def create query_string = params[:query] query_variables = params[:variables] || {}

    document = GraphQL.parse(query_string) result = Schema.execute(document: document, variables: query_variables) render json: result end
  4. PostInputType = GraphQL::InputObjectType.define do name "PostInputType" description "Properties for creating

    a Post" argument :title, !types.String argument :body, types.String end
  5. MutationRoot = GraphQL::ObjectType.define do name "Mutation" field :addPost, Post do

    description "Adds a Post." argument :post, PostInputType resolve ->(t, args, c) { Post.create( title: args['post']['title'], body: args['post']['body'] ) } end end Schema = GraphQL::Schema.define do mutation MutationRoot end
  6. PostType = GraphQL::ObjectType.define do # ... field :errors, types[types.String] do

    description "Reasons the object couldn't be created or updated" resolve ->(obj, args, ctx) { obj.errors.full_messages } end end
  7. { query { posts { id title teaser author {

    name } } } } QueryType = GraphQL::ObjectType.define do name "Query" description "The query root of this schema" field :post do type PostType argument :id, !types.ID description "Find a Post by ID" resolve ->(obj, args, context) { Post.find(args["id"]) } end field :posts do type types[PostType] resolve ->(obj, args, context) { Post.all } end end
  8. { query { posts { id title teaser author {

    name } } } } QueryType = GraphQL::ObjectType.define do name "Query" description "The query root of this schema" field :post do type PostType argument :id, !types.ID description "Find a Post by ID" resolve ->(obj, args, context) { Post.find(args["id"]) } end field :posts do type types[PostType] resolve ->(obj, args, context) { Post.all } end end
  9. { query { posts { id title teaser author {

    name } } } } Post Load (2.3ms) SELECT "posts".* FROM "posts"
  10. { query { posts { id title teaser author {

    name } } } } PostType = GraphQL::ObjectType.define do name "Post" description "A blog post" field :id, !types.ID field :title, !types.String field :body, !types.String field :author, !AuthorType field :comments, types[!CommentType] field :teaser, types.String, do resolve ->(post, args, ctx) { post.body[0, 40] } end end
  11. { query { posts { id title teaser author {

    name } } } } Author Load (1.4ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = $1 LIMIT 1 [["id", 12]] Author Load (1.2ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = $1 LIMIT 1 [["id", 13]] Author Load (1.3ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = $1 LIMIT 1 [["id", 14]]
  12. class RecordLoader < GraphQL::Batch::Loader def initialize(model) @model = model end

    def perform(ids) @model.where(id: ids).each { |record| fulfill(record.id, record) } ids.each { |id| fulfill(id, nil) unless fulfilled?(id) } end end # Then, in your type definition field :author, !AuthorType do resolve -> (post, args, context) { RecordLoader.for(Author).load(post.author_id) } end
  13. { query { posts { id title teaser author {

    name } } } } Author Load (1.9ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = IN (12, 13, 14)
  14. http://mgiroux.me/2016/graphql-query-caching-with-rails/ Query parsing & caching def create query_string = params[:query]

    query_variables = params[:variables] || {} document = get_document(query_string) result = Schema.execute(document: document, variables: query_variables) render json: result end def get_document(query_string) cache_key = Digest::MD5.hexdigest(query_string) document = Rails.cache.fetch(cache_key) unless document document = GraphQL.parse(query_string) Rails.cache.write(cache_key, document) end document end
  15. Persisted queries WebRepository = GraphQL::Pro::Repository.define do schema MySchema path Rails.root.join("app/graphql/documents/")

    end result = WebRepository.execute( operation_name: params[:operationName] variables: ensure_hash(params[:variables]) ) render json: result
  16. Limiting Visibility top_secret = ->(schema_member, ctx) { schema_member.metadata[:internal_only]} MySchema.execute(query_string, except:

    top_secret) # OR filter = PermissionWhitelist.new(@current_user) MySchema.execute(query_string, only: filter) class PermissionWhitelist def initialize(user) @user = user end def call(schema_member, ctx) Permissions.allowed?(user, schema_member) end end
  17. Prevent deeply nested queries # Schema-level: MySchema = GraphQL::Schema.define do

    # ... max_depth 10 end # Query-level, which overrides the schema-level setting: MySchema.execute(query_string, max_depth: 10)
  18. Calculate complexity # Constant complexity: field :top_score, types.Int, complexity: 10

    # Dynamic complexity: field :top_scorers, types[PlayerType] do argument :limit, types.Int, default_value: 5 complexity ->(ctx, args, child_complexity) { if ctx[:current_user].staff? 0 else args[:limit] * child_complexity end } end MySchema = GraphQL::Schema.define do # ... max_complexity 100 end
  19. Custom execution decorator (or GraphQL::Pro) https://eatcodeplay.com/graphql-performance-monitoring-with-new-relic-20070571a5d2 class SerialExecutionWithNewRelic < GraphQL::Query::SerialExecution

    def execute(ast_operation, root_type, query_object) irep_root = query_object.internal_representation[ast_operation.name] begin ::NewRelic::Agent.set_transaction_name(newrelic_name(irep_root)) ::NewRelic::Agent.add_custom_attributes(provided_variables(query_object)) rescue Exception => e Rails.logger.error('NewRelic set_transaction_name and add_custom_attributes failure: ' + e.message) end super end def newrelic_name(irep_root) name_to_report = irep_root.name || irep_root.children.keys.join(' - ') ['GraphQL', name_to_report].join(' - ') end def provided_variables(query_object) variables = query_object.variables.instance_variable_get(:@provided_variables) || {} ActionDispatch::Http::ParameterFilter.new(Rails.application.config.filter_parameters).filter(variables) end end