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. G R APHQL S H O P IF Y

    View full-size slide

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

    View full-size slide

  3. G RAPH QL CORE TEAM

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  6. Defining the Schema(s)

    View full-size slide

  7. gem 'graphql' ❤

    View full-size slide

  8. 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

    View full-size slide

  9. GraphModel / GraphApi

    View full-size slide

  10. GraphQL gem
    GraphModel
    GraphApi

    View full-size slide

  11. 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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  16. query {
    shop {
    products {
    image {
    url
    }
    }
    }
    }

    View full-size slide

  17. query {
    shop {
    products {
    image {
    url
    }
    }
    }
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  20. 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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  25. 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

    View full-size slide

  26. Promise.rb + Custom Executor

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  35. Authorization / Authentication

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  41. Evil Clients

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  44. Developer Experience

    View full-size slide

  45. Checked-In IDL

    View full-size slide

  46. Checked-In IDL

    View full-size slide

  47. Protecting the Schema

    View full-size slide

  48. Protecting the Schema

    View full-size slide

  49. Protecting the Schema

    View full-size slide

  50. Protecting the Schema

    View full-size slide

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

    View full-size slide

  52. Instrumentation

    View full-size slide

  53. Deprecated Field Usage

    View full-size slide

  54. Mobile Clients

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  57. 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

    View full-size slide

  58. 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

    View full-size slide

  59. GraphQL Schema
    The Killer Feature

    View full-size slide

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

    View full-size slide

  61. Query Builders

    View full-size slide

  62. 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)

    View full-size slide

  63. 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)

    View full-size slide

  64. 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()

    View full-size slide

  65. 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)

    View full-size slide

  66. Type-Safety and IDE Integration

    View full-size slide

  67. 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)

    View full-size slide

  68. Response Classes

    View full-size slide

  69. 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

    View full-size slide

  70. 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)

    View full-size slide

  71. 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)
    }

    View full-size slide

  72. 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

    View full-size slide

  73. Caching and Data Consistency

    View full-size slide

  74. 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)

    View full-size slide

  75. 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)

    View full-size slide

  76. 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)
    {

    View full-size slide

  77. 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());

    View full-size slide

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

    View full-size slide

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

    View full-size slide