Slide 1

Slide 1 text

graphql-ruby under the hood and how to write more elegant APIs Dmitry Tsepelev, Evil Martians

Slide 2

Slide 2 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 2

Slide 3

Slide 3 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 3

Slide 4

Slide 4 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 4 evilmartians.com

Slide 5

Slide 5 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 5 evilmartians.com

Slide 6

Slide 6 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 6 uses graphql-ruby and wants to know how it works internally wants to understand, how GraphQL execution engines are implemented wants to extend the API of graphql-ruby wants to learn, how to dig into a popular open-source project and start contributing THIS TALK IS FOR PEOPLE, WHO

Slide 7

Slide 7 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 7 GraphQL query language how to use GraphQL in your projects pros and cons of GraphQL as a technology WHAT WE ARE NOT GOING TO DISCUSS

Slide 8

Slide 8 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 8 DUMP 2019 Saint P Rubyconf 2019 https:!//youtu.be/xUrLslKdnr8 https:!//youtu.be/CjOwKbf8L3I?t=9615

Slide 9

Slide 9 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 9 https:!//evl.ms/blog/graphql-on-rails-1-from-zero-to-the-first-query

Slide 10

Slide 10 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 10 GRAPHQL FOR DUMMIES GRAPHQL-RUBY UNDER THE HOOD EXTENDING LIBRARY API

Slide 11

Slide 11 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 11 every entity is a type each type has a list of fields some types are connected DATA AS A GRAPH

Slide 12

Slide 12 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 12 Request: query { user(id: 42) { orders { items { product { title } quantity } } } } Response: { "data": { "user": { "orders": [ { "items": [ { "product": { "title": "iPhone 7" }, "quantity": 2 } ] } ] } } } QUERY LANGUAGE

Slide 13

Slide 13 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 13 query language execution engine WHAT IS GRAPHQL?

Slide 14

Slide 14 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 14 type UserType { id: ID! name: String! orders: [OrderType]! } SCHEMA

Slide 15

Slide 15 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 15 gem serializes the data you provide implements execution engine rules WHAT IS GRAPHQL-RUBY?

Slide 16

Slide 16 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 16 $ rails generate graphql:install route post "/graphql", to: "graphql#execute" GraphqlController app/graphql/ contains some base classes

Slide 17

Slide 17 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 17 CONTROLLER AND SCHEMA class GraphqlController < ApplicationController def execute result = GraphqlSchema.execute( params[:query], variables: params[:variables], context: {}, operation_name: params[:operationName] ) render json: result end end class GraphqlSchema < GraphQL!::Schema query Types!::QueryType end

Slide 18

Slide 18 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 18 ROOT FIELDS class Types!::QueryType < GraphQL!::Schema!::Object field :user, Types!::UserType, null: true do argument :id, ID, required: true end def user(id:) User.find(id) end end

Slide 19

Slide 19 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 19 TYPE DEFINITION class Types!::UserType < GraphQL!::Schema!::Object field :id, ID, null: false field :name, String, null: false field :orders, resolver: Resolvers!::OrderResolver end

Slide 20

Slide 20 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 20 GRAPHQL FOR DUMMIES GRAPHQL-RUBY UNDER THE HOOD EXTENDING LIBRARY API

Slide 21

Slide 21 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev SCHEMA DEFINITION 21

Slide 22

Slide 22 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 22 GraphQL::Schema class GraphQL!::Schema class !<< self def query(query_type) @query_type = query_type end end end class GraphqlSchema < GraphQL!::Schema query Types!::QueryType end application code library code

Slide 23

Slide 23 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 23 class GraphqlSchema < GraphQL!::Schema lazy_resolve OrdersResolver, :orders end class Types!::UserType < GraphQL!::Schema!::Object field :orders, [OrderType], null: false def orders OrdersResolver.new(context, object.id) end end GraphQL::Schema#lazy_resolve

Slide 24

Slide 24 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 24 class OrdersResolver def initialize(query_ctx, user_id) @user_id = user_id @lazy_state = query_ctx[:lazy_find_orders] !!||= { pending_ids: Set.new, loaded_ids: {} } @lazy_state[:pending_ids] !<< user_id end def orders loaded_records = @lazy_state[:loaded_ids][@user_id] return loaded_records if loaded_records pending_ids = @lazy_state[:pending_ids].to_a @lazy_state[:loaded_ids] = Order.where(user_id: pending_ids).group_by(&:user_id) @lazy_state[:pending_ids].clear @lazy_state[:loaded_ids][@user_id] end end GraphQL::Schema#lazy_resolve

Slide 25

Slide 25 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev class GraphQL!::Schema class !<< self def lazy_resolve(lazy_class, value_method) lazy_classes[lazy_class] = value_method end def lazy_classes @lazy_classes !!||= {} end end end GraphQL::Schema#lazy_resolve 25

Slide 26

Slide 26 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 26 GraphQL::Schema::Object class GraphQL!::Schema!::Object class !<< self def field(*args, !**kwargs, &block) own_fields[field_defn.name] = field_class.from_options( *args, !**kwargs, &block ) end def own_fields @own_fields !!||= {} end end end class Types!::UserType field :id, ID, null: false end application code library code

Slide 27

Slide 27 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 27 GraphQL::Schema::Object class GraphQL!::Schema!::Object class !<< self def field(*args, !**kwargs, &block) own_fields[field_defn.name] = GraphQL!::Schema!::Field.from_options( *args, !**kwargs, &block ) end def own_fields @own_fields !!||= {} end end end class Types!::UserType field :id, ID, null: false end application code library code

Slide 28

Slide 28 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Schema::Field 28 class GraphQL!::Schema!::Field def self.from_options(name = nil, …, !**kwargs, &block) kwargs[:name] = name if name new(!**kwargs, &block) end attr_accessor :name def initialize(…, &definition_block) @name = name @type = type @null = null … end end

Slide 29

Slide 29 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev &definition_block 29 class Types!::QueryType < GraphQL!::Schema!::Object field :user, Types!::UserType, null: true do argument :id, ID, required: true end end

Slide 30

Slide 30 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 30 GraphQL::Schema::Field class GraphQL!::Schema!::Field def initialize(!!..., &definition_block) instance_eval(&definition_block) if definition_block end def argument(*args, !**kwargs, &block) arg_defn = GraphQL!::Schema!::Argument.new(*args, !**kwargs, &block) own_arguments[arg_defn.name] = arg_defn end def own_arguments @own_arguments !!||= {} end end

Slide 31

Slide 31 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 31 # FIELD DATA STRUCTURE class Types!::UserType < GraphQL!::Schema!::Object field :id, ID, null: false field :name, String, null: false field :orders, resolver: Resolvers!::OrderResolver end Types!::UserType.fields

Slide 32

Slide 32 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 32 # #, "perPage"!=> # } > class Types!::UserType < GraphQL!::Schema!::Object field :id, ID, null: false field :name, String, null: false field :orders, resolver: Resolvers!::OrderResolver end Types!::UserType.fields FIELD DATA STRUCTURE

Slide 33

Slide 33 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev RESOLVING QUERIES 33

Slide 34

Slide 34 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Schema#execute class GraphQL!::Schema def self.execute(query_str = nil, !**kwargs) kwargs[:query] = query_str if query_str all_results = GraphQL!::Execution!::Multiplex.run_all(self, [kwargs]) all_results[0] end end 34

Slide 35

Slide 35 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev class GraphQL!::Execution!::Multiplex class !<< self def run_all(schema, query_options, *args) queries = query_options.map do |opts| GraphQL!::Query.new(schema, nil, opts) end run_queries(schema, queries, *args) end end end 35 Schema.execute ⇨ Multiplex.run_all GraphQL::Execution::Multiplex.run_all

Slide 36

Slide 36 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Query#prepare_ast GraphQL!::Language!::Parser.parse !!<<-QUERY query { user { name } } QUERY 36 query user name

Slide 37

Slide 37 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev @operation_type= @name= @name= GraphQL::Query#prepare_ast 37 query user name Document OperationDefinition Field Field @selections @selections @definitions

Slide 38

Slide 38 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev class GraphQL!::Execution!::Multiplex class !<< self def run_all(schema, query_options, *args) queries = query_options.map do |opts| GraphQL!::Query.new(schema, nil, opts) end run_queries(schema, queries, *args) end end end GraphQL::Execution::Multiplex.run_all 38 Schema.execute ⇨ Multiplex.run_all

Slide 39

Slide 39 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Execution::Multiplex#run_as_multiplex class GraphQL!::Execution!::Multiplex def run_as_multiplex(multiplex) strategy = multiplex.schema.query_execution_strategy results = multiplex.queries.map do |query| strategy.begin_query(query, multiplex) end results.each_with_index.map do |data_result, idx| multiplex.queries[idx].result end end end 39 Schema.execute Multiplex.run_all … ⇨ Multiplex#run_as_multiplex

Slide 40

Slide 40 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Execution::Execute 40 class GraphQL!::Execution!::Execute def self.begin_query(query, _multiplex) ExecutionFunctions.resolve_root_selection(query) end end Schema.execute Multiplex.run_all … Multiplex#run_as_multiplex ⇨ Execute.begin_query

Slide 41

Slide 41 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev ExecutionFunctions.resolve_root_selection 41 module ExecutionFunctions def resolve_root_selection(query) operation = query.selected_operation root_type = query.root_type_for_operation( operation.operation_type ) resolve_selection( query.root_value, root_type, query.context ) end end Schema.execute Multiplex.run_all … Multiplex#run_as_multiplex Execute.begin_query ⇨ ExecutionFunctions.resolve_root_selection

Slide 42

Slide 42 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev ExecutionFunctions.resolve_selection 42 module ExecutionFunctions def resolve_selection(object, current_type, current_ctx) current_ctx.value = {} selections_on_type = current_ctx.irep_node .typed_children[current_type] selections_on_type.each do |name| field_ctx = current_ctx.spawn_child(!!...) resolve_field(object, field_ctx) current_ctx.value[name] = field_ctx end current_ctx.value end end Schema.execute Multiplex.run_all … Multiplex#run_as_multiplex Execute.begin_query ExecutionFunctions.resolve_root_selection ⇨ ExecutionFunctions.resolve_selection resolve_selection

Slide 43

Slide 43 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev module ExecutionFunctions def resolve_field(object, field_ctx) raw_value = field_ctx.schema .middleware .invoke(object, field_ctx) field_ctx.value = resolve_value( raw_value, field_ctx.type, field_ctx ) end end ExecutionFunctions.resolve_field 43 Schema.execute Multiplex.run_all … Multiplex#run_as_multiplex Execute.begin_query ExecutionFunctions.resolve_root_selection ExecutionFunctions.resolve_selection ⇨ ExecutionFunctions.resolve_field resolve_field

Slide 44

Slide 44 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 44

Slide 45

Slide 45 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev FieldResolveStep 45 module FieldResolveStep def self.call( _parent_type, parent_object, field_definition, field_args, context ) field_definition.resolve( parent_object, field_args, context ) end end Schema.execute Multiplex.run_all … Multiplex#run_as_multiplex Execute.begin_query ExecutionFunctions.resolve_root_selection ExecutionFunctions.resolve_selection ExecutionFunctions.resolve_field … ⇨ FieldResolverStep.call

Slide 46

Slide 46 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Schema::Field#resolve 46 class GraphQL!::Schema!::Field def resolve(object, args, ctx) with_extensions(object, args, ctx) do |e_obj, e_args| field_receiver = if @resolver_class @resolver_class.new(object: e_obj, context: ctx) else e_obj end if field_receiver.respond_to?(@resolver_method) field_receiver.public_send(@resolver_method, !**e_args) else resolve_field_method(field_receiver, e_args, ctx) end end end end Schema.execute Multiplex.run_all … Multiplex#run_as_multiplex Execute.begin_query ExecutionFunctions.resolve_root_selection ExecutionFunctions.resolve_selection ExecutionFunctions.resolve_field … FieldResolverStep.call ⇨ Field#resolve resolve

Slide 47

Slide 47 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Schema::Field#resolve 47 class GraphQL!::Schema!::Field def resolve(object, args, ctx) with_extensions(object, args, ctx) do |e_obj, e_args| field_receiver = if @resolver_class @resolver_class.new(object: e_obj, context: ctx) else e_obj end if field_receiver.respond_to?(@resolver_method) field_receiver.public_send(@resolver_method, !**e_args) else resolve_field_method(field_receiver, e_args, ctx) end end end end resolve #

Slide 48

Slide 48 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Schema::Field#resolve 48 class GraphQL!::Schema!::Field def resolve(object, args, ctx) with_extensions(object, args, ctx) do |e_obj, e_args| field_receiver = OrderResolver.new( object: e_obj, context: ctx ) if field_receiver.respond_to?(@resolver_method) field_receiver.public_send(@resolver_method, !**e_args) else resolve_field_method(field_receiver, e_args, ctx) end end end end resolve #

Slide 49

Slide 49 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Schema::Field#resolve 49 class GraphQL!::Schema!::Field def resolve(object, args, ctx) with_extensions(object, args, ctx) do |e_obj, e_args| field_receiver = OrderResolver.new( object: e_obj, context: ctx ) if field_receiver.respond_to?(@resolver_method) field_receiver.public_send(@resolver_method, !**e_args) else resolve_field_method(field_receiver, e_args, ctx) end end end end resolve #

Slide 50

Slide 50 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Schema::Field#resolve 50 class GraphQL!::Schema!::Field def resolve(object, args, ctx) with_extensions(object, args, ctx) do |e_obj, e_args| field_receiver = OrderResolver.new( object: e_obj, context: ctx ) field_receiver.public_send(@resolver_method, !**e_args) end end end resolve #

Slide 51

Slide 51 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Schema::Field#resolve 51 class GraphQL!::Schema!::Field def resolve(object, args, ctx) with_extensions(object, args, ctx) do |e_obj, e_args| field_receiver = OrderResolver.new( object: e_obj, context: ctx ) field_receiver.public_send(:resolve, !**e_args) end end end resolve #

Slide 52

Slide 52 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Schema::Field#resolve 52 class GraphQL!::Schema!::Field def resolve(object, args, ctx) with_extensions(object, args, ctx) do |e_obj, e_args| field_receiver = if @resolver_class @resolver_class.new(object: e_obj, context: ctx) else e_obj end if field_receiver.respond_to?(@resolver_method) field_receiver.public_send(@resolver_method, !**e_args) else resolve_field_method(field_receiver, e_args, ctx) end end end end resolve #

Slide 53

Slide 53 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Schema::Field#resolve 53 class GraphQL!::Schema!::Field def resolve(object, args, ctx) with_extensions(object, args, ctx) do |e_obj, e_args| field_receiver = e_obj if field_receiver.respond_to?(@resolver_method) field_receiver.public_send(@resolver_method, !**e_args) else resolve_field_method(field_receiver, e_args, ctx) end end end end resolve #

Slide 54

Slide 54 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Schema::Field#resolve 54 class GraphQL!::Schema!::Field def resolve(object, args, ctx) with_extensions(object, args, ctx) do |e_obj, e_args| field_receiver = e_obj if field_receiver.respond_to?(@resolver_method) field_receiver.public_send(@resolver_method, !**e_args) else resolve_field_method(field_receiver, e_args, ctx) end end end end resolve #

Slide 55

Slide 55 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GraphQL::Schema::Field#resolve 55 class GraphQL!::Schema!::Field def resolve(object, args, ctx) with_extensions(object, args, ctx) do |e_obj, e_args| field_receiver = e_obj field_receiver.public_send(:name, !**e_args) end end end resolve #

Slide 56

Slide 56 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev module ExecutionFunctions def resolve_field(object, field_ctx) raw_value = field_ctx.schema .middleware .invoke(object, field_ctx) field_ctx.value = resolve_value( raw_value, field_ctx.type, field_ctx ) end end ExecutionFunctions.resolve_field 56 Schema.execute Multiplex.run_all … Multiplex#run_as_multiplex Execute.begin_query ExecutionFunctions.resolve_root_selection ExecutionFunctions.resolve_selection ⇨ ExecutionFunctions.resolve_field

Slide 57

Slide 57 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev ExecutionFunctions.resolve_value 57 module ExecutionFunctions def resolve_value(value, field_type, field_ctx) field_defn = field_ctx.field case field_type.kind when GraphQL!::TypeKinds!::SCALAR field_type.coerce_result(value, field_ctx) when GraphQL!::TypeKinds!::OBJECT resolve_selection(value, field_type, field_ctx) end end end Schema.execute Multiplex.run_all … Multiplex#run_as_multiplex Execute.begin_query ExecutionFunctions.resolve_root_selection ExecutionFunctions.resolve_selection ExecutionFunctions.resolve_field … ⇨ ExecutionFunctions.resolve_value resolve_value

Slide 58

Slide 58 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev class GraphQL!::Execution!::Multiplex def run_as_multiplex(multiplex) strategy = multiplex.schema.query_execution_strategy results = multiplex.queries.map do |query| strategy.begin_query(query, multiplex) end results.each_with_index.map do |data_result, idx| multiplex.queries[idx].result end end end GraphQL::Execution::Multiplex#run_as_multiplex 58 Schema.execute Multiplex.run_all … ⇨ Multiplex#run_as_multiplex

Slide 59

Slide 59 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev IS IT SLOW? 59 https:!//gist.github.com/DmitryTsepelev/36e290cf64b4ec0b18294d0a57fb26ff

Slide 60

Slide 60 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 60 GRAPHQL PARSER BENCHMARK fields – count of fields in each selection set nested levels – count of nested levels query { field1 { field1 field2 } field2 { field1 field2 } } fields = 2, nested levels = 1

Slide 61

Slide 61 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 61 user system total real 1 fields, 0 nested levels: 0.000153 0.000002 0.000155 ( 0.000152) 1 fields, 2 nested levels: 0.000188 0.000001 0.000189 ( 0.000187) 1 fields, 4 nested levels: 0.000252 0.000001 0.000253 ( 0.000240) 2 fields, 0 nested levels: 0.000134 0.000013 0.000147 ( 0.000132) 2 fields, 2 nested levels: 0.000420 0.000006 0.000426 ( 0.000411) 2 fields, 4 nested levels: 0.001695 0.000001 0.001696 ( 0.001694) 4 fields, 0 nested levels: 0.000154 0.000001 0.000155 ( 0.000153) 4 fields, 2 nested levels: 0.001744 0.000001 0.001745 ( 0.001744) 4 fields, 4 nested levels: 0.032188 0.000269 0.032457 ( 0.032581) 8 fields, 0 nested levels: 0.000316 0.000034 0.000350 ( 0.000352) 8 fields, 2 nested levels: 0.013464 0.000218 0.013682 ( 0.013917) 8 fields, 4 nested levels: 0.910159 0.008081 0.918240 ( 0.919507) GRAPHQL PARSER BENCHMARK

Slide 62

Slide 62 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 62 30.50 kB (285 objects) /users/1?include=orders&fields[users]=id,name&fields[orders]=id,placed_at { "data" !=> { "id"!=>"1", "type"!=>"users", "links"!=>{"self"!=>"users/1"}, "attributes"!=>{"id"!=>"1", "name"!=>"John Doe"} }, "included"!=> [ { "id"!=>"1", "type"!=>"orders", "links"!=>{"self"!=>"orders/1"}, "attributes"!=>{"id"!=>1, "placed_at"!=>"2019-09-21T12:56:31+03:00"} }, { "id"!=>"2", "type"!=>"orders", "links"!=>{"self"!=>"orders/2"}, "attributes"!=>{"id"!=>2, "placed_at"!=>"2019-09-21T12:56:31+03:00"} } ] } MEMORY PROFILER (JSON API)

Slide 63

Slide 63 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 63 113.19 kB (1059 objects) query GetUser($id: ID!) { user(id: $id) { id name orders { id placedAt } } } { "data" !=> { "user" !=> { "id"!=>"1", "name"!=>"John Doe", "orders"!=>[ { "id"!=>"1", "placedAt"!=>"2019-09-21T13:00:02+03:00" }, { "id"!=>"2", "placedAt"!=>"2019-09-21T13:00:02+03:00" } ] } } } MEMORY PROFILER (GRAPHQL)

Slide 64

Slide 64 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 64 GRAPHQL FOR DUMMIES GRAPHQL-RUBY UNDER THE HOOD EXTENDING LIBRARY API

Slide 65

Slide 65 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 65 BLOCK-BASED API Types!::Post = GraphQL!::ObjectType.define do field :comments, types[Types!::Comments] do argument :orderBy, Types!::CommentOrder resolve !->(obj, args, ctx) { obj.comments.order(args[:orderBy]) } end end

Slide 66

Slide 66 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 66 looks more like JavaScript than Ruby problems with constant loading (especially in Rails) hard to patch https:!//rmosolgo.github.io/blog/2018/03/25/why-a-new-schema-definition-api/ BLOCK-BASED API

Slide 67

Slide 67 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 67 graphql-ruby supports both APIs some classes look more complex than they could some old plugins do not work BLOCK-BASED API

Slide 68

Slide 68 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev GRAPHQL-BATCH 68

Slide 69

Slide 69 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 69 class GraphqlSchema < GraphQL!::Schema query QueryType use GraphQL!::Batch end class QueryType < GraphQL!::Schema!::Object field :product_title, String, null: true do argument :id, ID, required: true end def product_title(id:) RecordLoader.for(Product).load(id).then(&:title) end end SETUP AND USAGE

Slide 70

Slide 70 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 70 class RecordLoader < GraphQL!::Batch!::Loader def initialize(model) @model = model end def perform(ids) @model.where(id: ids).each do |record| fulfill(record.id, record) end ids.each do |id| fulfill(id, nil) unless fulfilled?(id) end end end LOADING DATA

Slide 71

Slide 71 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev class GraphQL!::Schema class !<< self def use(plugin, options = {}) plugins !<< [plugin, options] end def plugins @plugins !!||= [] end def to_graphql plugins.each { |plugin| plugin.use(self) } end end end 71 HOW SCHEMA LOADS PLUGINS

Slide 72

Slide 72 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 72 PATCHING SCHEMA WITH #use module GraphQL"::Batch def self.use(schema_defn, executor_class: GraphQL!::Batch!::Executor) instrumentation = GraphQL!::Batch!::SetupMultiplex.new( schema, executor_class: executor_class ) schema_defn.instrument(:multiplex, instrumentation) schema_defn.lazy_resolve(!::Promise, :sync) end end

Slide 73

Slide 73 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 73 REDEFINING FIELD RESOLVER class GraphQL!::Batch!::SetupMultiplex def instrument(type, field) old_resolve_proc = field.resolve_proc field.redefine do resolve !->(obj, args, ctx) { !::Promise.sync(old_resolve_proc.call(obj, args, ctx)) } end end end

Slide 74

Slide 74 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev FIELD EXTENSIONS 74

Slide 75

Slide 75 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev HOW TO EXTEND A FIELD 75 class Types!::QueryType < GraphQL!::Schema!::Object field :users, [Types!::UserType], null: false, extensions: [SearchableExtension] def users(query:) User.search(query) end end

Slide 76

Slide 76 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev ADDING ARGUMENT TO A FIELD USING EXTENSION 76 class SearchableExtension < GraphQL!::Schema!::FieldExtension def apply field.argument(:query, String, required: false) end end

Slide 77

Slide 77 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev HOW EXTENSIONS WORK 77 class GraphQL!::Schema!::Field def resolve(object, args, ctx) with_extensions(object, args, ctx) do |e_obj, e_args| # resolve logic end end def with_extensions(obj, args, ctx) extensions.each do |extension| obj, args = extension.resolve( object: obj, arguments: args, context: ctx ) end yield(obj, args) end end

Slide 78

Slide 78 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev PLUGIN IN 5 MINUTES ⏰ 78 https:!//gist.github.com/DmitryTsepelev/065bb6bc796898f5745c4209d1b4bb21

Slide 79

Slide 79 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 79 WHAT WE ARE BUILDING class QueryType < BaseObject field :cached_val, String, null: false def cached_val "I'm cached at "#{Time.now}" end end

Slide 80

Slide 80 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 80 ADDING EXTENSION TO THE FIELD class QueryType < BaseObject field :cached_val, String, null: false, extensions: [CacheExtension] def cached_val "I'm cached at "#{Time.now}" end end

Slide 81

Slide 81 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 81 HOW TO IMPLEMENT THE EXTENSION class CacheExtension < GraphQL!::Schema!::FieldExtension def resolve(object:, arguments:, !**rest) key = cache_key(object, arguments) store[key] !!||= yield(object, arguments) end private def store Thread.current[:field_cache] !!||= {} end def cache_key(object, arguments) ""#{object.class.name}-"#{@field}-"#{arguments.hash}" end end

Slide 82

Slide 82 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 82 I WANT TO USE KWARG! class QueryType < BaseObject field :cached_val, String, null: false, cached: true def cached_val "I'm cached at "#{Time.now}" end end

Slide 83

Slide 83 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 83 PATCHING BASE CLASS class BaseObject < GraphQL!::Schema!::Object field_class.prepend(Module.new do def initialize(*args, !**kwargs, &block) if kwargs.delete(:cached) kwargs[:extensions] !!||= [] !<< CacheExtension end super end end) end

Slide 84

Slide 84 text

RUBYRUSSIA 2019 DmitryTsepelev @dmitrytsepelev 84 graphql-ruby is a complex library with a complex architecture this architecture allows to extend the functionality of the library benefits coming with GraphQL do not require to sacrifice the performance TO SUM UP

Slide 85

Slide 85 text

evl.ms/blog @dmitrytsepelev DmitryTsepelev @evilmartians evl.ms/telegram THANK YOU! RUBYRUSSIA 2019 85