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