Two+ Years GraphQL at ProductHunt

Two+ Years GraphQL at ProductHunt

Experience and learnings from running GraphQL for two-plus years in production at Product Hunt.

Main focus is on the Ruby on Rails backend. The talk explains how GraphQL is implemented in Product Hunt. There are a lot of code samples streight out from the real codebase.

Gems mentioned in the talk:

- graphiql-rails
- graphql
- graphql-cache
- graphql-cache
- search_object
- search_object_graphql

Links from the talk:

- https://www.apollographql.com/
- https://facebook.github.io/relay/
- https://facebook.github.io/relay/docs/graphql-relay-specification.html
- https://facebook.github.io/relay/docs/graphql-connections.html
- https://facebook.github.io/relay/docs/graphql-mutations.html
- https://github.com/Shopify/graphql-batch/blob/master/examples/association_loader.rb
- https://gist.github.com/RStankov/48070003a31d71a66f57a237e27d5865

7a0e72a6f55811246bb5d9a946fd2e49?s=128

Radoslav Stankov

June 10, 2019
Tweet

Transcript

  1. Two Years GraphQL in Production Radoslav Stankov 10/06/2018

  2. Radoslav Stankov @rstankov blog.rstankov.com github.com/rstankov
 twitter.com/rstankov

  3. None
  4. None
  5. None
  6. None
  7. https://speakerdeck.com/rstankov

  8. Agenda

  9. ! Basics " Structure # Authentication & Authorization $ Performance

  10. Basics

  11. None
  12. None
  13. None
  14. None
  15. posts { id name tagline votesCount commentsCount thumbnailUrl state isVoted

    }
  16. posts(date: "2019-06-10") { id name tagline votesCount commentsCount thumbnailUrl state

    isVoted }
  17. query { posts(date: "2019-06-10") { id name tagline votesCount commentsCount

    thumbnailUrl state isVoted } } POST /graphql
  18. POST /graphql { "data": { "posts": { "id": "1", "name":

    "Playdate", "tagline": "A new handheld gaming system "votesCount": 1352, "commentsCount": 25,
 "thumbnailUrl": "https://example.com/C22 "state": "pre_launch", "isVoted": true, } } }
 query { posts(date: "2019-06-10") { id name tagline votesCount commentsCount thumbnailUrl state isVoted } }
  19. None
  20. productRequests(first: 1, order: FEATURED) { ... } goalsWorkedToday(first: 12) {

    .,. } radioEpisodes(first: 1) { ... } newsletters(first: 1) { ... } stories(first: 3) { ... } posts(first: 1, order: FEATURED, date: $date) { ... } viewer { .. }
  21. query Homepage { viewer { .. } productRequests(first: 1, order:

    FEATURED) { ... } goalsWorkedToday(first: 12) { .. } radioEpisodes(first: 1) { ... } posts(first: 1, order: FEATURED, date: $date) { ... } newsletters(first: 1) { ... } stories(first: 3) { ... } ... } POST /graphql
  22. None
  23. None
  24. gem "graphql"

  25. query { posts(date: "2019-06-10") { id name tagline } }

  26. module Graph::Types class PostType < BaseObject field :id, ID, null:

    false field :name, String, null: false field :tagline, String, null: false end end query { posts(date: "2019-06-10") { id name tagline } }
  27. module Graph::Types class PostType < BaseObject field :id, ID, null:

    false field :name, String, null: false field :tagline, String, null: false end end query { posts(date: "2019-06-10") { id name tagline } }
  28. module Graph::Types class BaseRecord < BaseObject field :id, ID, null:

    false end end query { posts(date: "2019-06-10") { id name tagline } }
  29. module Graph::Types class PostType < BaseRecord field :name, String, null:

    false field :tagline, String, null: false end end query { posts(date: "2019-06-10") { id name tagline } }
  30. module Graph::Types class PostType < BaseRecord field :name, String, null:

    false field :tagline, String, null: false field :votes_count, Integer, null: false end end query { posts(date: "2019-06-10") { id name tagline votesCount } }
  31. module Graph::Types class QueryType < BaseObject field :posts, [PostType], null:

    true do argument :date, String, required: true end def posts(date:) Post.for_date(date) end end end query { posts(date: "2019-06-10") { id name tagline
 votesCount } }
  32. class Graph::Schema < GraphQL::Schema query Graph::Types::QueryType # ... end query

    { posts(date: "2019-06-10") { id name tagline } }
  33. class GraphqlController < Frontend::BaseController def index render json: Graph::Schema.execute(query, variables:

    variables, context: context) end private def query params[:query] end def context { current_user: current_user, } end def variables convert_to_hash params[:variables] end def convert_to_hash(variables) # ... end end
  34. Rails.application.routes.draw do post '/graphql' => 'graphql#index', defaults: { format: :json

    } # ... end
  35. None
  36. None
  37. None
  38. None
  39. Structure

  40. % 83 fields in query type & 197 types '

    190 mutations # 172 resolvers ( 6 developers Stats
  41. class Graph::Query < Graph::Types::BaseObject field :posts, [PostType], null: true do

    argument :date, String, required: true end def posts(date:) Post.for_date(date) end end
  42. class Graph::Query < Graph::Types::BaseObject field :posts, [PostType], null: true do

    argument :date, String, required: false argument :name, String, required: false end def posts(date: nil, name: nil) scope = Post.featured scope = scope.for_date(date) if date scope = scope.for_name(name) if name scope end end
  43. class Graph::Query < Graph::Types::BaseObject field :posts, function: Graph::Resolvers::Posts::Search end

  44. gem 'search_object' gem 'search_object_graphql'

  45. class Graph::Resolvers::Posts::Search < Graph::Resolvers::BaseSearch scope { Post.featured } type [Graph::Types::PostType]

    option :date, type: types.Int, with: :apply_date_filter option :name, type: types.String, with: :apply_name_filter def apply_date_filter(scope, value) scope.for_date(value) end def apply_query_filter(scope, value) scope.for_name(value) end end
  46. https://www.apollographql.com/

  47. https://facebook.github.io/relay/

  48. https://facebook.github.io/relay/ Library Specification for how to design you GraphQL schema

  49. https://facebook.github.io/relay/ Library Specification for how to design you GraphQL schema

  50. % Node interface & Connections ' Mutations GraphQL Relay Specification

    https://facebook.github.io/relay/docs/graphql-relay-specification.html
  51. ) Node interface & Connections ' Mutations GraphQL Relay Specification

    https://facebook.github.io/relay/docs/graphql-relay-specification.html
  52. & Connections ' Mutations GraphQL Relay Specification https://facebook.github.io/relay/docs/graphql-relay-specification.html

  53. https://facebook.github.io/relay/docs/graphql-connections.html Connections query Homepage { posts { id name tagline

    } }
  54. https://facebook.github.io/relay/docs/graphql-connections.html Connections query Homepage($cursor: String) { posts(first: 10, after: $cursor)

    {
 edges {
 node {
 id name tagline
 }
 }
 pageInfo {
 hasNextPage
 } } }
  55. class Graph::Resolvers::Posts::Search < Graph::Resolvers::BaseSearch scope { Post.featured } type Graph::Types::PostType.connection_type

    option :date, type: types.Int, with: :apply_date_filter option :query, type: types.String, with: :apply_query_filter def apply_date_filter(scope, value) scope.for_date(value) end def apply_query_filter(scope, value) scope.for_query(query) end end
  56. & Connections ' Mutations GraphQL Relay Specification https://facebook.github.io/relay/docs/graphql-relay-specification.html

  57. mutation CollectionPostAdd($input: CollectionPostAddInput!) { response: collectionPostAdd(input: $input) { node {

    ...SomeFragment } errors { name messages } } } Mutations
  58. https://facebook.github.io/relay/docs/graphql-connections.html Mutations https://facebook.github.io/relay/docs/graphql-mutations.html mutation CollectionPostAdd($input: CollectionPostAddInput!) { response: collectionPostAdd(input: $input)

    { node { ...SomeFragment } errors { name messages } } }
  59. Mutations mutation CollectionPostAdd($input: CollectionPostAddInput!) { response: collectionPostAdd(input: $input) { node

    { ...SomeFragment } errors { name messages } } }
  60. Mutations mutation CollectionPostAdd($input: CollectionPostAddInput!) { response: collectionPostAdd(input: $input) { node

    { ...SomeFragment } errors { name messages } } }
  61. Mutations mutation CollectionPostAdd($input: CollectionPostAddInput!) { response: collectionPostAdd(input: $input) { node

    { ...SomeFragment } errors { name messages } } }
  62. Mutations mutation CollectionPostAdd($input: CollectionPostAddInput!) { response: collectionPostAdd(input: $input) { node

    { ...SomeFragment } errors { name messages } } }
  63. [module][object][action] Mutations

  64. [module][object][action]
 
 CommentCreate
 CollectionPostAdd PostVoteCreate PostVoteDestroy PostSubmissionCreate ShipContactCreate
 ShipContactDestroy Mutations

  65. [module][object][action]
 
 CommentCreate CollectionPostAdd PostVoteCreate PostVoteDestroy PostSubmissionCreate ShipContactCreate
 ShipContactDestroy Mutations

  66. class Graph::Schema < GraphQL::Schema mutation Graph::Types::MutationType # ... end

  67. module Graph::Types class MutationType < Types::BaseObject def self.mutation_field(mutation) field mutation.name.demodulize.underscore,

    mutation: mutation end mutation_field Graph::Mutations::CommentCreate
 mutation_field Graph::Mutations::CollectionPostAdd mutation_field Graph::Mutations::PostVoteCreate mutation_field Graph::Mutations::PostVoteDestroy mutation_field Graph::Mutations::PostSubmissionCreate mutation_field Graph::Mutations::ShipContactCreate mutation_field Graph::Mutations::ShipContactDestroy # ... end end
  68. module Graph::Mutations class CollectionPostAdd < BaseMutation argument_record :collection, Collection argument_record

    :post, Post argument :description, String, required: false returns Graph::Types::CollectionPostType def perform(collection:, post:, description: nil) Collections.collect( current_user: current_user, collection: collection, post: post, description: description ) end end end
  69. module Graph::Mutations class CollectionPostAdd < BaseMutation argument_record :collection, Collection argument_record

    :post, Post argument :description, String, required: false returns Graph::Types::CollectionPostType def perform(collection:, post:, description: nil) Collections.collect( current_user: current_user, collection: collection, post: post, description: description ) end end end
  70. Authentication & Authorization

  71. None
  72. None
  73. None
  74. class Frontend::GraphqlController < Frontend::BaseController before_action :ensure_query def index render json:

    Graph::Schema.execute(query, variables: variables, context: context) rescue => e handle_error e end private # ... def context { current_user: current_user, request: request, } end # ... end
  75. module Graph::Query < Graph::Types::BaseObject field :viewer, ViewerType, null: true def

    viewer context.current_user end end
  76. query { viewer { email user { id username //

    ...
 } // ... } }
  77. None
  78. class Graph::Schema < GraphQL::Schema # ... authorization Graph::Utils::AuthorizationStrategy instrument :field,

    Graph::Utils::FallbackInstrumenter # ... end
  79. class Graph::Utils::AuthorizationStrategy def initialize(context) @current_user = context.current_user end def allowed?(gate,

    value) ApplicationPolicy.can? @current_user, gate.role, value end end
  80. class Graph::Utils::AuthorizationStrategy def initialize(context) @current_user = context.current_user end def allowed?(gate,

    value) ApplicationPolicy.can? @current_user, gate.role, value end end
  81. module Graph::Types class PostType < BaseRecord authorize :read field :submission,

    SubmissionType, authorize: :edit, fallback: nil field :can_edit, resolve: Graph::Resolvers::Can.for(:edit) # ... end end
  82. module Graph::Types class PostType < BaseRecord authorize :read field :submission,

    SubmissionType, authorize: :edit, fallback: nil field :can_edit, resolve: Graph::Resolvers::Can.for(:edit) # ... end end
  83. module Graph::Types class PostType < BaseRecord authorize :read field :submission,

    SubmissionType, authorize: :edit, fallback: nil field :can_edit, resolve: Graph::Resolvers::Can.for(:edit) # ... end end
  84. module Graph::Mutations class CollectionPostAdd < BaseMutation argument_record :collection, Collection argument_record

    :post, Post argument :description, String, required: false authorize :edit, :collection returns Graph::Types::CollectionPostType def perform(collection:, post:, description: nil) Collections.collect( current_user: current_user, collection: collection, post: post, description: description ) end end end
  85. module Graph::Mutations class CollectionPostAdd < BaseMutation argument_record :collection, Collection argument_record

    :post, Post argument :description, String, required: false authorize :edit, :collection returns Graph::Types::CollectionPostType def perform(collection:, post:, description: nil) Collections.collect( collection: collection, post: post, description: description ) end end end
  86. Performance

  87. module Graph::Types class UserType < BaseRecord field :activity_streak, Integer, null:

    false def activity_streak # NOTE: Very, very expensive calculation * Users::ActivityStreak.for(object) end end end
  88. gem "graphql-cache"

  89. class Graph::Schema < GraphQL::Schema # ... use GraphQL::Cache # ...

    end
  90. module Graph::Types class UserType < BaseRecord field :activity_streak, Integer, null:

    false, cache: true def activity_streak # NOTE: Very, very expensive calculation * Users::ActivityStreak.for(object) end end end
  91. module Graph::Types class UserType < BaseRecord field :activity_streak, Integer, null:

    false, cache: true def activity_streak # NOTE: Very, very expensive calculation Users::ActivityStreak.for(object) end end end
  92. None
  93. None
  94. query { posts(date: "2019-06-10") { id name tagline votesCount commentsCount

    thumbnailUUID state isVoted } }
  95. query { posts(date: "2019-06-10") { id isVoted } } [{

    id: 1, isVoted: true }, { id: 2, isVoted: true }, { id: 3, isVoted: true }]
  96. [{ id: 1, isVoted: true }, { id: 2, isVoted:

    true }, { id: 3, isVoted: true }] SELECT * FROM posts WHERE DATE(featured_at) = {date} SELECT * FROM votes WHERE post_id={post.id} and user_id={user.id} SELECT * FROM votes WHERE post_id={post.id} and user_id={user.id} SELECT * FROM votes WHERE post_id={post.id} and user_id={user.id}
  97. None
  98. [{ id: 1, isVoted: true }, { id: 2, isVoted:

    true }, { id: 3, isVoted: true }]
  99. [...]

  100. [{ id: 1, isVoted: [is post 1 voted by current

    user?] }, ...]
  101. [{ id: 1, isVoted: [is post 1 voted by current

    user?] }, { id: 2, isVoted: [is post 2 voted by current user?] }, ...]
  102. [{ id: 1, isVoted: [is post 1 voted by current

    user?] }, { id: 2, isVoted: [is post 2 voted by current user?] }, { id: 3, isVoted: [is post 3 voted by current user?] }] [is post 1, 2, 3 voted for current user]
  103. [{ id: 1, isVoted: [is post 1 voted?] }, {

    id: 2, isVoted: [is post 2 voted?] }, { id: 3, isVoted: [is post 3 voted?] }] SELECT * FROM votes WHERE post_id IN {post_ids} and user_id={user_id}
  104. gem "graphql-batch"

  105. class Graph::Schema < GraphQL::Schema # ... use GraphQL::Batch lazy_resolve Promise,

    :sync # ... end
  106. module Graph::Types class PostType < BaseRecord field :name, String, null:

    false field :tagline, String, null: false field :votes_count, Integer, null: false field :is_voted, resolve: Graph::Resolvers::Posts::IsVoted end end
  107. module Graph::Types class PostType < BaseRecord field :name, String, null:

    false field :tagline, String, null: false field :votes_count, Integer, null: false field :is_voted, resolve: Graph::Resolvers::Posts::IsVoted end end
  108. class Graph::Resolvers::Posts::IsVoted < Resolvers::Base type Boolean, null: false def resolve

    user = context.current_user return false if user.nil? Loader.for(user).load(object) end class Loader < GraphQL::Batch::Loader def initialize(user) @user = user end def perform(posts) voted_ids = @user.votes.where(post_id: posts.map(&:id)).pluck(:post_id) posts.each do |post| fulfill post, voted_ids.include?(post.id) end end end end
  109. class Graph::Resolvers::Posts::IsVoted < Resolvers::Base type Boolean, null: false def resolve

    user = context.current_user return false if user.nil? Loader.for(user).load(object) end class Loader < GraphQL::Batch::Loader def initialize(user) @user = user end def perform(posts) voted_ids = @user.votes.where(post_id: posts.map(&:id)).pluck(:post_id) posts.each do |post| fulfill post, voted_ids.include?(post.id) end end end end
  110. module Graph::Types class PostType < BaseRecord field :name, String, null:

    false field :tagline, String, null: false field :votes_count, Integer, null: false field :is_voted, resolve: Graph::Resolvers::Posts::IsVoted field :topics, [TopicType], null: false end end
  111. module Graph::Types class PostType < BaseRecord field :name, String, null:

    false field :tagline, String, null: false field :votes_count, Integer, null: false field :is_voted, resolve: Graph::Resolvers::Posts::IsVoted field :topics, [TopicType], null: false end end SELECT * FROM topics JOIN post_topics WHERE post_id IN {post_id}
  112. module Graph::Types class PostType < BaseRecord field :name, String, null:

    false field :tagline, String, null: false field :votes_count, Integer, null: false field :is_voted, resolve: Graph::Resolvers::Posts::IsVoted field :topics, [TopicType], null: false end end SELECT * FROM topics JOIN post_topics WHERE post_id IN {post_id} +
  113. https://github.com/Shopify/graphql-batch/blob/master/examples/association_loader.rb https://gist.github.com/RStankov/48070003a31d71a66f57a237e27d5865

  114. module Graph::Types class PostType < BaseRecord field :name, String, null:

    false field :tagline, String, null: false field :votes_count, Integer, null: false field :is_voted, resolve: Graph::Resolvers::Posts::IsVoted field :topics, function: Graph::Resolvers::Association.new(:topics, TopicsT end end
  115. module Graph::Types class PostType < BaseRecord field :name, String, null:

    false field :tagline, String, null: false field :votes_count, Integer, null: false field :is_voted, resolve: Graph::Resolvers::Posts::IsVoted association :topics, [TopicType] end end
  116. Conclusion

  117. None
  118. Thanks ,

  119. https://speakerdeck.com/rstankov