Save 37% off PRO during our Black Friday Sale! »

Thinking in graphs or Why GraphQL is not about mapping database to schema

Thinking in graphs or Why GraphQL is not about mapping database to schema

This talk will be helpful for people who do not have any production experience with GraphQL or do not see any benefits compared to REST.

We'll find out what is GraphQL, talk about it's philosophy and answer the following questions:
— how to use GraphQL query language?
— are there any things you cannot do with GraphQL? (spoiler - there are!)
— what is schema and why do we need it?
— how to write your GraphQL API in Ruby?
— what are best practices of GraphQL schema design?

F5c2731f9a4dbfb4af319295a1f0cd28?s=128

Dmitry Tsepelev

June 01, 2019
Tweet

Transcript

  1. THINKING IN GRAPHS why GraphQL is not about mapping database

    to schema Dmitry Tsepelev, Evil Martians
  2. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 2

  3. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 3 evilmartians.com

  4. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 4 evilmartians.com

  5. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev REST GET /users GET

    /users/:id POST /users PATCH /users/:id DELETE /users/:id • resources are identified via URL • actions are identified via HTTP verbs • associations are represented via IDs 5
  6. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 6 REST • resources

    are identified via URL • actions are identified via HTTP verbs • associations are represented via IDs GET /users GET /users/:id POST /users PATCH /users/:id DELETE /users/:id
  7. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev GET /users/42 { "id":

    42, "name": "John Doe", "accountId": 23 } 7 REST • resources are identified via URL • actions are identified via HTTP verbs • associations are represented via IDs
  8. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev POST /users/42/join-community POST /communities/67/join

    8 Which to choose?
  9. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev POST /users/42/join-community POST /communities/67/join

    9 Which to choose? action is identified both via URL and HTTP verb
  10. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 10 The "REST" way

    POST /community-memberships
  11. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 11 Representing data as

    a graph • everything is a type • each type has a list of fields • some types are connected
  12. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 12 Representing data as

    a graph • everything is a type • each type has a list of fields • some types are connected
  13. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 13 Representing data as

    a graph • everything is a type • each type has a list of fields • some types are connected
  14. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 14 Representing data as

    a graph • everything is a type • each type has a list of fields • some types are connected
  15. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev What is GraphQL? •

    API query language • execution engine 15
  16. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 16 ⇨ Making requests

    ⇦ Gotchas and downsides Schema definition Implementing backend Designing healthy schema
  17. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 17 Request: query {

    currentUserId } Response: { "data": { "currentUserId": 42 } } Fields and selection sets
  18. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 18 Request: query {

    user(id: 42) { orders { items { product { title } quantity } } } } Response: { "data": { "user": { "orders": [ { "items": [ { "product": { "title": "iPhone 7" }, "quantity": 2 } ] } ] } } } Arguments
  19. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 19 Nested selection sets

    Request: query { user(id: 42) { orders { items { product { title } quantity } } } } Response: { "data": { "user": { "orders": [ { "items": [ { "product": { "title": "iPhone 7" }, "quantity": 2 } ] } ] } } }
  20. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 20 query FetchUser($userId: Int)

    { user(id: $userId) { orders { items { product { title } quantity } } } } variables { "userId": 42 } Operation name
  21. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 21 Variables query FetchUser($userId:

    Int) { user(id: $userId) { orders { items { product { title } quantity } } } } variables { "userId": 42 }
  22. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev Transport layer • transport-agnostic

    according to the spec • in implementations: - single endpoint - HTTP POST - 200 - OK 22
  23. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 23 Request: query {

    user(id: 404) { id name } } Response: { "data": null, "errors": [ { "message": "User not found" } ] } Error representation
  24. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 24 GraphQL allows to

    fetch all data in a single query
  25. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 25 GET /user GET

    /repos/:owner/:repo GET /repos/:owner/:repo/issues Underfetching in REST
  26. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 26 GET /user {

    "login": "octocat", "id": 1, "node_id": "MDQ6VXNlcjE=", "avatar_url": "https:"//github.com/images/error/octocat_happy.gif", "gravatar_id": "", "url": "https:"//api.github.com/users/octocat", "html_url": "https:"//github.com/octocat", "followers_url": "https:"//api.github.com/users/octocat/followers", "following_url": "https:"//api.github.com/users/octocat/following{/ other_user}", "gists_url": "https:"//api.github.com/users/octocat/gists{/gist_id}", "starred_url": "https:"//api.github.com/users/octocat/starred{/owner}{/ repo}", "subscriptions_url": "https:"//api.github.com/users/octocat/subscriptions", "organizations_url": "https:"//api.github.com/users/octocat/orgs", "repos_url": "https:"//api.github.com/users/octocat/repos", "events_url": "https:"//api.github.com/users/octocat/events{/privacy}", "received_events_url": "https:"//api.github.com/users/octocat/ received_events", "type": "User", "site_admin": false, "name": "monalisa octocat", … Overfetching in REST … "company": "GitHub", "blog": "https:"//github.com/blog", "location": "San Francisco", "email": "octocat@github.com", "hireable": false, "bio": "There once was""...", "public_repos": 2, "public_gists": 1, "followers": 20, "following": 0, "created_at": "2008-01-14T04:33:35Z", "updated_at": "2008-01-14T04:33:35Z", "private_gists": 81, "total_private_repos": 100, "owned_private_repos": 100, "disk_usage": 10000, "collaborators": 8, "two_factor_authentication": true, "plan": { "name": "Medium", "space": 400, "private_repos": 20, "collaborators": 0 } }
  27. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 27 Mutations: updating the

    data
  28. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 28 Request: mutation {

    createPost( title: "draft", content: "no content" ) { id } } Response: { "data": { "createPost" { "id": 4 } } } Mutations: updating the data
  29. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 29 Request: subscription {

    postCreated { id title content } } Response: { "data": { "postCreated": { "id": 4, "title": "draft", "content": "no content" } } } [Draft] Subscriptions: live updates
  30. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 30 Making requests ⇨

    Gotchas and downsides ⇦ Schema definition Implementing backend Designing healthy schema
  31. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 31 Request: query {

    articles { id author { name } } } Response: { "data": { "articles" [ { "id": "1", "author": { "name": "John" } }, { "id": "2", "author": { "name": "John" } }, ] } } Data duplication
  32. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 32 type CategoryType {

    name: String! subcategories: [CategoryType]! } query { categories { name subcategories { name subcategories { name } } } } No recursive queries
  33. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 33 Caching is hard

    • REST: a response depends on the HTTP verb and the URL • GraphQL: a response depends on the selection set • no easy and effective way to cache a GraphQL response #
  34. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 34 Making requests Gotchas

    and downsides ⇨ Schema definition ⇦ Implementing backend Designing healthy schema
  35. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 35 type UserType {

    name: String! orders(page: Int): [OrderType]! } Type definition
  36. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 36 Fields type UserType

    { name: String! orders(page: Int): [OrderType]! }
  37. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 37 Scalar types type

    UserType { # Int|Float|Boolean|String|ID name: String! orders(page: Int): [OrderType]! }
  38. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 38 List types type

    UserType { name: String! orders(page: Int): [OrderType]! }
  39. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 39 Field arguments type

    UserType { name: String! orders(page: Int): [OrderType]! }
  40. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 40 type QueryType {

    users: [UserType]! } type MutationType { signUp(login: String!, password: String!): UserType! } type SubscriptionType { userSignedUp: UserType! } Root types
  41. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 41 Input validation type

    QueryType { orders(page: Int):[OrderType]! } query { orders(page: "1") { id } } { "errors": [{ "message": "Argument 'page' on Field 'orders' has an invalid value. Expected type 'Int'.", "fields": ["query", "orders", "page"] }] }
  42. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 42 enum UserModerationStatus {

    MODERATION APPROVED REJECTED } type UserType { name: String! moderationStatus: UserModerationStatus! } Enums
  43. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 43 scalar DateTime type

    OrderType { placedAt: DateTime! } { "placedAt": "2019-03-01T19:18:37+03:00" } Custom scalars
  44. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 44 Automatic documentation: GraphiQL

    github.com/graphql/graphiql
  45. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 45 github.com/2fd/graphdoc Automatic documentation:

    GraphDoc
  46. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 46 Making requests Gotchas

    and downsides Schema definition ⇨ Implementing backend ⇦ Designing healthy schema
  47. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 47 $ gem install

    graphql $ rails generate graphql:install
  48. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 48 ruby-graphql generator •

    route post "/graphql", to: "graphql#execute" • GraphqlController • app/graphql/ directory with base classes • Graphiql engine mount (optionally)
  49. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 49 GraphqlController class GraphqlController

    < ApplicationController def execute result = GraphqlSchema.execute( params[:query], variables: params[:variables], operation_name: params[:operationName], context: {} ) render json: result end end query FetchUser($userId: Int) { user(id: $userId) { orders { items { product { title } quantity } } } } variables { "userId": 42 }
  50. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 50 GraphqlController class GraphqlController

    < ApplicationController def execute result = GraphqlSchema.execute( params[:query], variables: params[:variables], operation_name: params[:operationName], context: {} ) render json: result end end query FetchUser($userId: Int) { user(id: $userId) { orders { items { product { title } quantity } } } } variables { "userId": 42 }
  51. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 51 GraphqlController class GraphqlController

    < ApplicationController def execute result = GraphqlSchema.execute( params[:query], variables: params[:variables], operation_name: params[:operationName], context: {} ) render json: result end end query FetchUser($userId: Int) { user(id: $userId) { orders { items { product { title } quantity } } } } variables { "userId": 42 }
  52. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 52 GraphqlController class GraphqlController

    < ApplicationController def execute result = GraphqlSchema.execute( params[:query], variables: params[:variables], operation_name: params[:operationName], context: {} ) render json: result end end query FetchUser($userId: Int) { user(id: $userId) { orders { items { product { title } quantity } } } } variables { "userId": 42 }
  53. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 53 GraphqlController class GraphqlController

    < ApplicationController def execute result = GraphqlSchema.execute( params[:query], variables: params[:variables], operation_name: params[:operationName], context: {} ) render json: result end end query FetchUser($userId: Int) { user(id: $userId) { orders { items { product { title } quantity } } } } variables { "userId": 42 }
  54. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 54 GraphqlSchema class GraphqlSchema

    < GraphQL"::Schema query QueryType max_depth 6 end
  55. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 55 query { user(id:

    42) { orders { user { orders { user { … } } } } } } Depth and complexity vulnerabilities
  56. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 56 class QueryType <

    GraphQL"::Schema"::Object field :users, [UserType], description: "List of all users", null: false def users User.all end end Defining QueryType: define a field
  57. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 57 Defining QueryType: specify

    a type class QueryType < GraphQL"::Schema"::Object field :users, [UserType], description: "List of all users", null: false def users User.all end end
  58. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 58 Defining QueryType: define

    a resolver method class QueryType < GraphQL"::Schema"::Object field :users, [UserType], description: "List of all users", null: false def users User.all end end
  59. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 59 Defining QueryType: specify

    a data source class QueryType < GraphQL"::Schema"::Object field :users, [UserType], description: "List of all users", null: false def users User.all end end
  60. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 60 Defining QueryType: document

    your field class QueryType < GraphQL"::Schema"::Object field :users, [UserType], description: "List of all users", null: false def users User.all end end
  61. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 61 class UserType <

    GraphQL"::Schema"::Object field :name, String, null: false field :orders, [OrderType], null: false do argument :page, Int, required: false end def orders(page: nil) object.orders.page(page) end end How to define a type: field declaration
  62. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 62 How to define

    a type: resolver method class UserType < GraphQL"::Schema"::Object field :name, String, null: false field :orders, [OrderType], null: false do argument :page, Int, required: false end def orders(page: nil) object.orders.page(page) end end
  63. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 63 How to define

    a type: passing arguments class UserType < GraphQL"::Schema"::Object field :name, String, null: false field :orders, [OrderType], null: false do argument :page, Int, required: false end def orders(page: nil) object.orders.page(page) end end
  64. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 64 Writing tests: query

    to test query Users($page: Int, $per: Int) { users(page: $page, per: $per) { id name } }
  65. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 65 Writing tests: rspec

    RSpec.describe UsersResolver do let(:query) { … } let(:result) do GraphqlSchema.execute( query, variables: { page: 1, per: 5 }, context: {} ).as_json end let!(:users) { create_list(:user, 10) } it "returns first page" do expect(result.dig("data", "users")).to match_array( users[0, 5].map { |user| { "id" "=> user.id, "name" "=> user.name } } ) end end
  66. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 66 Writing tests: fixturama

    github.com/nepalez/fixturama RSpec.describe UsersResolver do subject { GraphqlSchema.execute(query).to_h } before do seed_fixture(""#{"__dir"__}/database.yml", profile_id: 42) end let(:query) { load_fixture ""#{"__dir"__}/query.graphql" } let(:result) { load_fixture ""#{"__dir"__}/result.yaml" } it { is_expected.to eq result } end
  67. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 67 class QueryType <

    GraphQL"::Schema"::Object field :users, [UserType], null: false def users # SELECT * FROM users; # SELECT * FROM orders where user_id = ?; # SELECT * FROM orders where user_id = ?; # SELECT * FROM orders where user_id = ?; # SELECT * FROM orders where user_id = ?; User.all end end class UserType < GraphQL"::Schema"::Object field :orders, [OrderType], null: false end N+1 workarounds
  68. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 68 class QueryType <

    GraphQL"::Schema"::Object field :users, [UserType], null: false def users # SELECT * FROM users; # SELECT * FROM orders where user_id IN (…); User.preload(:orders) end end class UserType < GraphQL"::Schema"::Object field :orders, [OrderType], null: false end N+1 workaround: top-level preload
  69. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 69 query { user(id:

    24) { id name } } # SELECT * FROM users; # SELECT * FROM orders where user_id IN (…); N+1 workaround: top-level preload
  70. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 70 class QueryType <

    GraphQL"::Schema"::Object field :users, [UserType], null: false, extras: [:lookahead] def users(lookahead:) if lookahead.selects?(:orders) User.preload(:orders) else User.all end end end class UserType < GraphQL"::Schema"::Object field :orders, [OrderType], null: false end N+1 workaround: lookahead
  71. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 71 N+1 workaround: lazy

    loading github.com/DmitryTsepelev/ar_lazy_preload class QueryType < GraphQL"::Schema"::Object field :users, [UserType], null: false def users User.lazy_preload(:orders) end end class UserType < GraphQL"::Schema"::Object field :orders, [OrderType], null: false end
  72. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 72 N+1 workaround: batching

    github.com/Shopify/graphql-batch class QueryType < GraphQL"::Schema"::Object field :users, [UserType], null: false def users User.all end end class UserType < GraphQL"::Schema"::Object field :orders, [OrderType], null: false def orders AssociationLoader.for(User, :orders).load(object) end end
  73. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 73 Batching example dev.to/evilmartians/active-storage-meets-graphql-pt-2-exposing-attachment-urls-2mdn

  74. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 74 Bonus: select only

    requested columns github.com/Arkweid/graphql-smart_select class UserType < GraphQL"::Schema"::Object field_class.prepend(GraphQL"::SmartSelect) field :orders, [OrderType], null: false, smart_select: true end query { currentUser { orders { id } } } SELECT id FROM orders;
  75. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 75 Authentication: changing context

    class GraphqlController < ApplicationController def execute result = GraphqlSchema.execute( params[:query], variables: params[:variables], operation_name: params[:operationName], context: context ) render json: result end def context @context ""||= { warden: request.env["warden"], current_user: request.env["warden"].user, } end end
  76. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 76 Authentication: adding mutation

    class GraphqlSchema < GraphQL"::Schema max_depth 6 query QueryType mutation MutationType end class MutationType < GraphQL"::Schema"::Object field :sign_in, mutation: SignInMutation end
  77. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 77 Authentication: implementing mutation

    class SignInMutation < GraphQL"::Schema"::Mutation type UserType null false argument :email, String, required: true argument :password, String, required: true def resolve(email:, password:) user = User.find_by(email: email) if user&.authenticate(password) context[:warden].set_user(user) else raise GraphQL"::ExecutionError, "Wrong email or password" end end end
  78. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 78 Authentication: implementing mutation

    class SignInMutation < GraphQL"::Schema"::Mutation type UserType null false argument :email, String, required: true argument :password, String, required: true def resolve(email:, password:) user = User.find_by(email: email) if user&.authenticate(password) context[:warden].set_user(user) else raise GraphQL"::ExecutionError, "Wrong email or password" end end end
  79. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 79 Authentication: implementing mutation

    class SignInMutation < GraphQL"::Schema"::Mutation type UserType null false argument :email, String, required: true argument :password, String, required: true def resolve(email:, password:) user = User.find_by(email: email) if user&.authenticate(password) context[:warden].set_user(user) else raise GraphQL"::ExecutionError, "Wrong email or password" end end end
  80. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 80 Authentication: implementing mutation

    class SignInMutation < GraphQL"::Schema"::Mutation type UserType null false argument :email, String, required: true argument :password, String, required: true def resolve(email:, password:) user = User.find_by(email: email) if user&.authenticate(password) context[:warden].set_user(user) else raise GraphQL"::ExecutionError, "Wrong email or password" end end end
  81. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 81 class QueryType <

    GraphQL"::Schema"::Object field :users, [UserType], null: false def users if context[:current_user].admin? User.all else raise GraphQL"::ExecutionError, "Access denied" end end end Authorization: application layer
  82. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 82 class UserType <

    GraphQL"::Schema"::Object class "<< self def authorized?(object, context) super "&& ( context[:current_user].admin? "|| context[:current_user] "== object ) end def visible?(context) super "&& context[:current_user]&.feature_enabled?(:user_list) end end end Authorization: built-in tooling
  83. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 83 Authorization: integration with

    frameworks • graphql-ruby pro (cancancan/pundit) • github.com/exAspArk/graphql-guard (cancancan/pundit) • github.com/palkan/action_policy-graphql (action_policy)
  84. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 84 What about subscriptions?

    class SubscriptionType < GraphQL"::Schema"::Object field :user_signed_up, UserType, null: false def user_signed_up; end end class SignUp def perform GraphqlSchema.subscriptions.trigger("userSignedUp", {}, user) end end
  85. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 85 What about subscriptions?

    • ActionCable-based subscriptions out of the box • issues with performance ( github.com/anycable/anycable-rails/ issues/40#issuecomment-411793852) • consider using AnyCable with github.com/Envek/graphql- anycable
  86. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 86 Making requests Gotchas

    and downsides Schema definition Implementing backend ⇨ Designing healthy schema ⇦
  87. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev type OrderType { items:

    [ItemType]! userId: ID! userName: String! } 87 Never expose data of other entities type UserType { id: ID! name: String! } type OrderType { items: [ItemType]! user: UserType! } ✅
  88. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev type UserType { id:

    ID! name: String! moderationStatus: ModerationStatusType! moderationDate: DateTime! } 88 Move coupled fields to separate type type UserModerationType { status: ModerationStatusType! date: DateTime! } type UserType { id: ID! name: String! moderation: UserModerationType! } ✅
  89. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev type ProductType { id:

    ID! } type ItemType { product: ProductType! quantity: Int! } type OrderType { items: [ItemType]! } How to check if order contains some product by ID? 89 Expose business logic and raw data
  90. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 90 const query =

    gql` query GetOrder($id: ID!) { order(id: $id) { items { product { id } } } } ` const order = request(query, { id }) const found = order.items.find(item "=> item.product.id "== productId) if (found ""!== null) { "//… } Expose business logic and raw data
  91. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 91 type ProductType {

    id: ID! } type ItemType { product: ProductType! quantity: Int! } type OrderType { items: [ItemType]! hasProduct(id: ID!): Bool! } Expose business logic and raw data
  92. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 92 const query =

    gql` query GetOrder($id: ID!, $productId: ID!) { order(id: $id) { hasProduct(productId: $productId) } } ` const order = request(query, { id, productId }) if (order.hasProduct) { "//… } Expose business logic and raw data
  93. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev Mutation design example: blog

    app • two pages - reader's view and edit form • title and content can be changed on edit form • post can be published/unpublished using the button on the reader's view • tags can be changed on the reader's view using a dropdown 93
  94. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev type MutationType { createPost(

    title: String, content: String, tags: [String], published: Bool ): PostType! updatePost( id: ID!, title: String, content: String, tags: [String], published: Bool ): PostType! deletePost(id: ID!): Bool } CRUD mutations 94
  95. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 95 input PostInput {

    title: String, content: String } type MutationType { createPost(postInput: PostInput): PostType! updatePost(id: ID!, postInput: PostInput): PostType! deletePost(id: ID!): Bool publishPost(id: ID!): PostType! unpublishPost(id: ID!): PostType! addTag(id: ID!, tag: String!): PostType! removeTag(id: ID!, tag: String!): PostType! } Atomic mutations
  96. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 96 input PostInput {

    title: String, content: String } type MutationType { createPost(postInput: PostInput): PostType! updatePost(id: ID!, postInput: PostInput): PostType! deletePost(id: ID!): Bool publishPost(id: ID!): PostType! unpublishPost(id: ID!): PostType! addTag(id: ID!, tag: String!): PostType! removeTag(id: ID!, tag: String!): PostType! } Atomic mutations
  97. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 97 input PostInput {

    title: String, content: String } type MutationType { createPost(postInput: PostInput): PostType! updatePost(id: ID!, postInput: PostInput): PostType! } Inputs • CQRS - Command Query Responsibility Segregation • input fields can be scalar or input
  98. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 98 Atomic mutations input

    PostInput { title: String, content: String } type MutationType { createPost(postInput: PostInput): PostType! updatePost(id: ID!, postInput: PostInput): PostType! deletePost(id: ID!): Bool publishPost(id: ID!): PostType! unpublishPost(id: ID!): PostType! addTag(id: ID!, tag: String!): PostType! removeTag(id: ID!, tag: String!): PostType! }
  99. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 99 Atomic mutations input

    PostInput { title: String, content: String } type MutationType { createPost(postInput: PostInput): PostType! updatePost(id: ID!, postInput: PostInput): PostType! deletePost(id: ID!): Bool publishPost(id: ID!): PostType! unpublishPost(id: ID!): PostType! addTag(id: ID!, tag: String!): PostType! removeTag(id: ID!, tag: String!): PostType! }
  100. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 100 type ItemType {

    id: ID! quantity: Integer! } API evolution: additive changes are safe type ItemType { id: ID! quantity: Integer! addedAt: DateTime! }
  101. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 101 type ItemType {

    quantity: Integer! count: Integer! @deprecated( reason: "Use `quantity`." ) } API evolution: @deprecated
  102. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 102 class GraphqlSchema <

    GraphQL"::Schema use GraphQL"::Tracing"::NewRelicTracing # … end API evolution: usage monitoring
  103. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 103 API evolution: emergency

    mutation turn off mutation { transferMoney(to: 42, amount: 100) { id } } { "data": null, "errors": [ { "mutationDeprecated": "transferMoney", "message": "Please update your app" } ] }
  104. SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 104 evl.ms/blog

  105. SAINT P RUBYCONF 2019 evl.ms/blog @dmitrytsepelev DmitryTsepelev @evilmartians evl.ms/telegram Thank

    you! Questions? 105