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

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

Radoslav Stankov

June 10, 2019
Tweet

More Decks by Radoslav Stankov

Other Decks in Technology

Transcript

  1. 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 } }
  2. 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 { .. }
  3. 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
  4. 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 } }
  5. 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 } }
  6. module Graph::Types class BaseRecord < BaseObject field :id, ID, null:

    false end end query { posts(date: "2019-06-10") { id name tagline } }
  7. 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 } }
  8. 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 } }
  9. 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 } }
  10. 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
  11. % 83 fields in query type & 197 types '

    190 mutations # 172 resolvers ( 6 developers Stats
  12. 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
  13. 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
  14. 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
  15. % Node interface & Connections ' Mutations GraphQL Relay Specification

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

    https://facebook.github.io/relay/docs/graphql-relay-specification.html
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. query { posts(date: "2019-06-10") { id isVoted } } [{

    id: 1, isVoted: true }, { id: 2, isVoted: true }, { id: 3, isVoted: true }]
  31. [{ 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}
  32. [{ id: 1, isVoted: true }, { id: 2, isVoted:

    true }, { id: 3, isVoted: true }]
  33. [{ id: 1, isVoted: [is post 1 voted by current

    user?] }, { id: 2, isVoted: [is post 2 voted by current user?] }, ...]
  34. [{ 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]
  35. [{ 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}
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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}
  42. 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} +
  43. 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
  44. 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