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. 3.
  2. 4.
  3. 5.
  4. 6.
  5. 8.
  6. 10.
  7. 11.
  8. 12.
  9. 13.
  10. 14.
  11. 17.
  12. 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 } }
  13. 19.
  14. 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 { .. }
  15. 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
  16. 22.
  17. 23.
  18. 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 } }
  19. 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 } }
  20. 28.

    module Graph::Types class BaseRecord < BaseObject field :id, ID, null:

    false end end query { posts(date: "2019-06-10") { id name tagline } }
  21. 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 } }
  22. 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 } }
  23. 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 } }
  24. 32.
  25. 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
  26. 35.
  27. 36.
  28. 37.
  29. 38.
  30. 39.
  31. 40.

    % 83 fields in query type & 197 types '

    190 mutations # 172 resolvers ( 6 developers Stats
  32. 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
  33. 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
  34. 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
  35. 50.

    % Node interface & Connections ' Mutations GraphQL Relay Specification

    https://facebook.github.io/relay/docs/graphql-relay-specification.html
  36. 51.

    ) Node interface & Connections ' Mutations GraphQL Relay Specification

    https://facebook.github.io/relay/docs/graphql-relay-specification.html
  37. 54.
  38. 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
  39. 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
  40. 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
  41. 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
  42. 71.
  43. 72.
  44. 73.
  45. 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
  46. 77.
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. 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
  55. 92.
  56. 93.
  57. 95.

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

    id: 1, isVoted: true }, { id: 2, isVoted: true }, { id: 3, isVoted: true }]
  58. 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}
  59. 97.
  60. 98.

    [{ id: 1, isVoted: true }, { id: 2, isVoted:

    true }, { id: 3, isVoted: true }]
  61. 99.
  62. 101.

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

    user?] }, { id: 2, isVoted: [is post 2 voted by current user?] }, ...]
  63. 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]
  64. 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}
  65. 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
  66. 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
  67. 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
  68. 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
  69. 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
  70. 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}
  71. 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} +
  72. 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
  73. 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
  74. 116.
  75. 117.
  76. 118.