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

GraphQL @ Shopify

GraphQL @ Shopify

Marc-Andre Giroux

October 26, 2016
Tweet

More Decks by Marc-Andre Giroux

Other Decks in Programming

Transcript

  1. 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
  2. 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
  3. query { shop { products { image { url }

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

    } } } SELECT `products`.* FROM `products` AND `products`.`shop_id` = 1
  5. 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
  6. 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
  7. query { shop { products { image { url }

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

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

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

    } } } SELECT `product_images`.* FROM `product_images` WHERE `product_images`.`product_id` IN (1, 2, 3)
  11. class Query def result Timeout.timeout(@timeout) { @query.result } rescue Timeout::Error

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

    TimeThrottle { "errors" => [{ "message" => "Throttled" }] } end end Throttling
  13. Conventions • Mutation actions are suffixed productCreate, not createProduct •

    Mutations return a `userErrors` field on errors • Support Relay spec (deletedId, newEdge, clientMutationId)
  14. Overview • Background • GraphQL Code Generator • Query Builders

    • Response Classes • Caching and Data Consistency
  15. Mobile Shopify App • Rebuilt to provide full Shopify Admin

    • Fast on slow networks by reducing response sizes (REST -> GraphQL) Since Sept. 27
  16. 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
  17. 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
  18. 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)
  19. 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)
  20. 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() …
  21. 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)
  22. 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)
  23. 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
  24. 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)
  25. 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) }
  26. 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
  27. 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)
  28. 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)
  29. 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) {
  30. 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());
  31. Caching & Data Consistency - Summary • Simplicity • Cache

    whole responses • Productive mobile developer • Type checked optimistic updates • Build for long term • Data consistency without coupling