Slide 1

Slide 1 text

THINKING IN GRAPHS why GraphQL is not about mapping database to schema Dmitry Tsepelev, Evil Martians

Slide 2

Slide 2 text

SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 2

Slide 3

Slide 3 text

SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 3 evilmartians.com

Slide 4

Slide 4 text

SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 4 evilmartians.com

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 10 The "REST" way POST /community-memberships

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev What is GraphQL? • API query language • execution engine 15

Slide 16

Slide 16 text

SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 16 ⇨ Making requests ⇦ Gotchas and downsides Schema definition Implementing backend Designing healthy schema

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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 } ] } ] } } }

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev Transport layer • transport-agnostic according to the spec • in implementations: - single endpoint - HTTP POST - 200 - OK 22

Slide 23

Slide 23 text

SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 23 Request: query { user(id: 404) { id name } } Response: { "data": null, "errors": [ { "message": "User not found" } ] } Error representation

Slide 24

Slide 24 text

SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 24 GraphQL allows to fetch all data in a single query

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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": "[email protected]", "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 } }

Slide 27

Slide 27 text

SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 27 Mutations: updating the data

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 30 Making requests ⇨ Gotchas and downsides ⇦ Schema definition Implementing backend Designing healthy schema

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 32 type CategoryType { name: String! subcategories: [CategoryType]! } query { categories { name subcategories { name subcategories { name } } } } No recursive queries

Slide 33

Slide 33 text

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 #

Slide 34

Slide 34 text

SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 34 Making requests Gotchas and downsides ⇨ Schema definition ⇦ Implementing backend Designing healthy schema

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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"] }] }

Slide 42

Slide 42 text

SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 42 enum UserModerationStatus { MODERATION APPROVED REJECTED } type UserType { name: String! moderationStatus: UserModerationStatus! } Enums

Slide 43

Slide 43 text

SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 43 scalar DateTime type OrderType { placedAt: DateTime! } { "placedAt": "2019-03-01T19:18:37+03:00" } Custom scalars

Slide 44

Slide 44 text

SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 44 Automatic documentation: GraphiQL github.com/graphql/graphiql

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 46 Making requests Gotchas and downsides Schema definition ⇨ Implementing backend ⇦ Designing healthy schema

Slide 47

Slide 47 text

SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 47 $ gem install graphql $ rails generate graphql:install

Slide 48

Slide 48 text

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)

Slide 49

Slide 49 text

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 }

Slide 50

Slide 50 text

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 }

Slide 51

Slide 51 text

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 }

Slide 52

Slide 52 text

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 }

Slide 53

Slide 53 text

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 }

Slide 54

Slide 54 text

SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 54 GraphqlSchema class GraphqlSchema < GraphQL"::Schema query QueryType max_depth 6 end

Slide 55

Slide 55 text

SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 55 query { user(id: 42) { orders { user { orders { user { … } } } } } } Depth and complexity vulnerabilities

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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 } }

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 73 Batching example dev.to/evilmartians/active-storage-meets-graphql-pt-2-exposing-attachment-urls-2mdn

Slide 74

Slide 74 text

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;

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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)

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 86 Making requests Gotchas and downsides Schema definition Implementing backend ⇨ Designing healthy schema ⇦

Slide 87

Slide 87 text

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! } ✅

Slide 88

Slide 88 text

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! } ✅

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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! }

Slide 99

Slide 99 text

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! }

Slide 100

Slide 100 text

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! }

Slide 101

Slide 101 text

SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 101 type ItemType { quantity: Integer! count: Integer! @deprecated( reason: "Use `quantity`." ) } API evolution: @deprecated

Slide 102

Slide 102 text

SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 102 class GraphqlSchema < GraphQL"::Schema use GraphQL"::Tracing"::NewRelicTracing # … end API evolution: usage monitoring

Slide 103

Slide 103 text

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" } ] }

Slide 104

Slide 104 text

SAINT P RUBYCONF 2019 DmitryTsepelev @dmitrytsepelev 104 evl.ms/blog

Slide 105

Slide 105 text

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