Share
Embed
Transcript
82 slides
-
Goal: Make it easy for any team to build their
own schemas -
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 -
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 -
query { shop { products { image { url }
} } } SELECT `shops`.* FROM `shops` WHERE `shops`.`id` = 1 -
query { shop { products { image { url }
} } } SELECT `products`.* FROM `products` AND `products`.`shop_id` = 1 -
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 -
With Batching Products Image Image Image loader.load(1) loader.load(2) loader.load(3) =>
#<Promise> => #<Promise> => #<Promise> -
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 -
module GraphApi class Product < GraphApi::ObjectType field :image, ProductImage, preload:
:image end end The API -
module GraphApi class Product < GraphApi::ObjectType field :image, ProductImage, preload:
:image end end The API -
query { shop { products { image { url }
} } } With Batching -
query { shop { products { image { url }
} } } SELECT `shops`.* FROM `shops` WHERE `shops`.`id` = 1 -
query { shop { products { image { url }
} } } SELECT `products`.* FROM `products` AND `products`.`shop_id` = 1 -
query { shop { products { image { url }
} } } SELECT `product_images`.* FROM `product_images` WHERE `product_images`.`product_id` IN (1, 2, 3) -
query { shop { products { image { url }
} } } SELECT `product_images`.* FROM `product_images` WHERE `product_images`.`product_id` IN (1, 2, 3) -
module GraphApi class Order < GraphApi::ObjectType required_access(:orders, write: true) field
:email, :string end end Type Access -
module GraphApi class Order < GraphApi::ObjectType required_access(:orders) do |order| order.visible?
end field :email, :string end end Type Access -
class Query def result Timeout.timeout(@timeout) { @query.result } rescue Timeout::Error
{ "errors" => [{ "message" => "Timeout" }] } end end Timeouts -
class Query def result TimeThrottle.for(key, options) { @query.result } rescue
TimeThrottle { "errors" => [{ "message" => "Throttled" }] } end end Throttling -
Conventions • Mutation actions are suffixed productCreate, not createProduct •
Mutations return a `userErrors` field on errors • Support Relay spec (deletedId, newEdge, clientMutationId) -
Overview • Background • GraphQL Code Generator • Query Builders
• Response Classes • Caching and Data Consistency -
Mobile Shopify App • Rebuilt to provide full Shopify Admin
• Fast on slow networks by reducing response sizes (REST -> GraphQL) Since Sept. 27 -
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 -
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 -
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) -
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) -
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() … -
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) -
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) -
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 -
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) -
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) } -
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 -
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) -
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) -
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) { -
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()); -
Caching & Data Consistency - Summary • Simplicity • Cache
whole responses • Productive mobile developer • Type checked optimistic updates • Build for long term • Data consistency without coupling