Slide 1

Slide 1 text

G R APHQL S H O P IF Y

Slide 2

Slide 2 text

MA RC- ANDRE GIRO UX @__xuorig__ MONTREA L, C ANADA

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

G RAPH QL CORE TEAM

Slide 5

Slide 5 text

Goal: Make it easy for any team to build their own schemas

Slide 6

Slide 6 text

Goal: Make it easy for mobile developers to extend the schemas

Slide 7

Slide 7 text

Defining the Schema(s)

Slide 8

Slide 8 text

gem 'graphql' ❤

Slide 9

Slide 9 text

ShopType = ObjectType.define do name "Shop" description "An awesome shop!" field :name, types.String, "The shops's name” # `!` marks this field as non-null: field :currency, !types.Int # Returns a list of `ProductType`s field :products, types[PersonType] end

Slide 10

Slide 10 text

GraphModel / GraphApi

Slide 11

Slide 11 text

GraphQL gem GraphModel GraphApi

Slide 12

Slide 12 text

module GraphApi class Shop < GraphApi::ObjectType # Types are declared using symbols or type class field :name, :string # Explicit null kwarg to define non-null fields field :currency, :string, null: false field :products, [Product] end end

Slide 13

Slide 13 text

I can’t believe it’s not butter ActiveRecord!

Slide 14

Slide 14 text

module GraphApi class ProductVariant < GraphApi::ObjectType belongs_to :product, Product end end

Slide 15

Slide 15 text

module GraphApi class Product < GraphApi::ObjectType cache_has_many :variants, [ProductVariant] end end

Slide 16

Slide 16 text

module GraphApi class Product < GraphApi::ObjectType paginated :variants, ProductVariant end end

Slide 17

Slide 17 text

query { shop { products { image { url } } } }

Slide 18

Slide 18 text

query { shop { products { image { url } } } }

Slide 19

Slide 19 text

query { shop { products { image { url } } } } SELECT `shops`.* FROM `shops` WHERE `shops`.`id` = 1

Slide 20

Slide 20 text

query { shop { products { image { url } } } } SELECT `products`.* FROM `products` AND `products`.`shop_id` = 1

Slide 21

Slide 21 text

query { shop { products { image { url } } } } SELECT `product_images`.* FROM `product_images` WHERE `product_images`.`product_id` = 1 SELECT `product_images`.* FROM `product_images` WHERE `product_images`.`product_id` = 2 SELECT `product_images`.* FROM `product_images` WHERE `product_images`.`product_id` = 3

Slide 22

Slide 22 text

Products Image Image Image product1.image product3.image product2.image

Slide 23

Slide 23 text

Batching

Slide 24

Slide 24 text

https://github.com/Shopify/graphql-batch

Slide 25

Slide 25 text

loader = GraphModel::AssociationLoader.for(record.class, :image) loader.load(record) => # Return a Promise

Slide 26

Slide 26 text

With Batching Products Image Image Image loader.load(1) loader.load(2) loader.load(3) => # => # => #

Slide 27

Slide 27 text

class AssociationLoader < GraphQL::Batch::Loader def load(record) # => Cache and return a Promise end def perform(records) # Prefetch the associations and fulfill all promises! @model.prefetch_associations(@association_name, records) records.each { |record| fulfill(record, read_association(record)) } end end Defining a Loader

Slide 28

Slide 28 text

Promise.rb + Custom Executor

Slide 29

Slide 29 text

module GraphApi class Product < GraphApi::ObjectType field :image, ProductImage, preload: :image end end The API

Slide 30

Slide 30 text

module GraphApi class Product < GraphApi::ObjectType field :image, ProductImage, preload: :image end end The API

Slide 31

Slide 31 text

prefetch(obj, arguments, context).then do resolve(object, arguments, context) end Wrap resolve with promise

Slide 32

Slide 32 text

query { shop { products { image { url } } } } With Batching

Slide 33

Slide 33 text

query { shop { products { image { url } } } } SELECT `shops`.* FROM `shops` WHERE `shops`.`id` = 1

Slide 34

Slide 34 text

query { shop { products { image { url } } } } SELECT `products`.* FROM `products` AND `products`.`shop_id` = 1

Slide 35

Slide 35 text

query { shop { products { image { url } } } } SELECT `product_images`.* FROM `product_images` WHERE `product_images`.`product_id` IN (1, 2, 3)

Slide 36

Slide 36 text

query { shop { products { image { url } } } } SELECT `product_images`.* FROM `product_images` WHERE `product_images`.`product_id` IN (1, 2, 3)

Slide 37

Slide 37 text

Authorization / Authentication

Slide 38

Slide 38 text

module GraphApi class Order < GraphApi::ObjectType required_access(:orders) field :email, :string end end Type Access

Slide 39

Slide 39 text

module GraphApi class Order < GraphApi::ObjectType required_access(:orders) field :email, :string end end Type Access

Slide 40

Slide 40 text

module GraphApi class Order < GraphApi::ObjectType required_access(:orders, write: true) field :email, :string end end Type Access

Slide 41

Slide 41 text

module GraphApi class Order < GraphApi::ObjectType required_access(:orders) do |order| order.visible? end field :email, :string end end Type Access

Slide 42

Slide 42 text

context = { access: GraphApi::AccessControl.new(@access_token, @user) } AccessControl

Slide 43

Slide 43 text

Evil Clients

Slide 44

Slide 44 text

class Query def result Timeout.timeout(@timeout) { @query.result } rescue Timeout::Error { "errors" => [{ "message" => "Timeout" }] } end end Timeouts

Slide 45

Slide 45 text

class Query def result TimeThrottle.for(key, options) { @query.result } rescue TimeThrottle { "errors" => [{ "message" => "Throttled" }] } end end Throttling

Slide 46

Slide 46 text

Developer Experience

Slide 47

Slide 47 text

Checked-In IDL

Slide 48

Slide 48 text

Checked-In IDL

Slide 49

Slide 49 text

Protecting the Schema

Slide 50

Slide 50 text

Protecting the Schema

Slide 51

Slide 51 text

Protecting the Schema

Slide 52

Slide 52 text

Protecting the Schema

Slide 53

Slide 53 text

Conventions • Mutation actions are suffixed productCreate, not createProduct • Mutations return a `userErrors` field on errors • Support Relay spec (deletedId, newEdge, clientMutationId)

Slide 54

Slide 54 text

Conventions

Slide 55

Slide 55 text

Instrumentation

Slide 56

Slide 56 text

Deprecated Field Usage

Slide 57

Slide 57 text

Mobile Clients

Slide 58

Slide 58 text

Overview • Background • GraphQL Code Generator • Query Builders • Response Classes • Caching and Data Consistency

Slide 59

Slide 59 text

Mobile Shopify App • Rebuilt to provide full Shopify Admin • Fast on slow networks by reducing response sizes (REST -> GraphQL) Since Sept. 27

Slide 60

Slide 60 text

React-Native Experiment • ❤ GraphQL • ❤ Components • Quick to build, hard to polish • Switched to using native Android and iOS APIs • Need Java and Swift GraphQL clients

Slide 61

Slide 61 text

GraphQL Client Goals • Simplicity • Avoid unnecessary complexity • Quick to implement • Easy to understand • Productive mobile developer • Embrace static type system • IDE Integration (Android Studio & Xcode) • Build for long term • Components to decouple large codebase • Reduce schema evolution friction

Slide 62

Slide 62 text

GraphQL Schema The Killer Feature

Slide 63

Slide 63 text

Generate code from the schema script/update_schema Query Builders Classes Response Classes Schema

Slide 64

Slide 64 text

Query Builders

Slide 65

Slide 65 text

Swift Query Builder ApiSchema.buildQuery() { $0 .product(id: productId) { $0 .id() .title() .images(first: 1) { $0 .id() .src() } } } { product(id: "gid://shopify/Product/42") { id title images(first: 1) { id src } } } (Pretty Printed)

Slide 66

Slide 66 text

Java Query Builder APISchema.query(q -> q .product(productId, p -> p .id() .title() .images(1, img -> img .id() .src() ) ) ) • Requires java 8 lambda expressions: • Retrolambda backports to java 5-7 • New Jack Android toolchain { product(id: "gid://shopify/Product/42") { id title images(first: 1) { id src } } } (Pretty Printed)

Slide 67

Slide 67 text

Simple Query Composition func loadProduct() { let query := ApiSchema.buildQuery { $0 .product(id: productId, self.productDetailQuery) .shop(self.shopDetailQuery) } … } func productDetailQuery(_ product: ApiSchema.ProductQuery) { product .id() .title() …

Slide 68

Slide 68 text

Inline Fragments on Type ApiSchema.buildQuery { $0 .event(id: eventId) { $0 .message() .onCommentEvent { $0 .author() } } } { event(id: “gid://shopify/Event/42") { __typename id message ... on CommentEvent { author } } } (Pretty Printed)

Slide 69

Slide 69 text

Type-Safety and IDE Integration

Slide 70

Slide 70 text

Query Builders - Summary • Simplicity • No GraphQL variables, named fragments or directives • No IDE plugin required • Productive mobile developer • Type-safe • IDE features just work • Build for long term • Can declare data dependancies close to their use • script/update_schema to adapt to schema changes (e.g. deprecations)

Slide 71

Slide 71 text

Response Classes

Slide 72

Slide 72 text

Response objects guard let response = response.product else { logger.error("tried to view product details for deleted product") return } variants = product.variants.edges.map { $0.node } • Validates and deserializes fields based on their type in constructor • Field methods have appropriate return types • Leverages language type system, including swift optional types • Runtime error for accessing unqueried field • Replace model layer

Slide 73

Slide 73 text

Custom Scalars GraphQLGenerator::Swift.new(schema, nest_under: 'ApiSchema', custom_scalars: [ GraphQLGenerator::Swift::Scalar.new( type_name: 'Money', swift_type: 'NSDecimalNumber', deserialize_expr: ->(expr) { "NSDecimalNumber(string: #{expr}, locale: GraphQL.posixLocale)" }, serialize_expr: ->(expr) { "#{expr}.description(withLocale: GraphQL.posixLocale)" } ) ] ).save(output_directory)

Slide 74

Slide 74 text

Unknown Value/Type switch variant.inventoryPolicy { case .deny: // … case .unknownValue // generated enum value // handle values added in the future } // no default to handle all known cases // type check on GraphQL interface if let comment = event as? ApiSchema.CommentEvent { appendMessage(comment.message, author: comment.author) } else { // use interface fields for other/unknown object types appendMessage(comment.message) }

Slide 75

Slide 75 text

Response Classes - Summary • Simplicity • Replace hand written model layer • Productive mobile developer • Use field methods without coercion • Type system imposes constraints (e.g. enums, optionals) • IDE features work • Build for long term • Unknown types/values leave room for schema evolution • Server-side computed fields avoid inaccuracies as business logic changes

Slide 76

Slide 76 text

Caching and Data Consistency

Slide 77

Slide 77 text

Response Caching • Response cache, keyed by query string, persisted to disk • Optimistically render using cached response, but request still sent extension ProductDetailsViewController: Relayable { func handleRelayQuery( query: ApiSchema.QueryRootQuery, response: ApiSchema.QueryRoot?, error: GraphQueryError?, cached: Bool) { session.relayContainer.queryGraph(query: query, relayable: self)

Slide 78

Slide 78 text

Active Queries • List of active query stored in memory to render data changes • Weak reference to cleanup active queries after their view is freed extension ProductDetailsViewController: Relayable { func handleRelayQuery( query: ApiSchema.QueryRootQuery, response: ApiSchema.QueryRoot?, error: GraphQueryError?, cached: Bool) { session.relayContainer.queryGraph(query: query, relayable: self)

Slide 79

Slide 79 text

Mutations • Mutations apply to all active queries • Find nodes in all active queries that match a node in the payload by id: • Deep merge fields from payload node on active query node • Call handleRelayQuery to update view for active query session.relayContainer.mutateGraph(mutationQuery: mutation) { … } extension ProductDetailsViewController: Relayable { func handleRelayQuery( query: ApiSchema.QueryRootQuery, response: ApiSchema.QueryRoot?, error: GraphQueryError?, cached: Bool) {

Slide 80

Slide 80 text

Optimistic Updates • Optimistic update actions apply to all matching nodes in active queries • Rollback optimistically updated active queries from response cache before applying updates from mutation response let optimisticUpdate = RelayAction.Destroy(product) self.session.relayContainer.mutateGraph( mutationQuery: mutation, updates: [optimisticUpdate]) { (mutation, error) in APISchema.Order optimisticUpdate = new APISchema.Order(orderId()) .setClosed(true) .setClosedAt(SimpleRelativeDateUtil.now().getTimeInMillis());

Slide 81

Slide 81 text

Caching & Data Consistency - Summary • Simplicity • Cache whole responses • Productive mobile developer • Type checked optimistic updates • Build for long term • Data consistency without coupling

Slide 82

Slide 82 text

Follow our work https://engineering.shopify.com