Closed lennyburdette closed 5 months ago
Exploring other use cases for context — this one is interesting but I'm less sure about it.
query FeaturedBookDetails {
featuredBook {
title
author {
name
# I want a list of books that _does not_ include the featured book
otherBooks {
title
}
}
}
}
# books subgraph
type Book @key(fields: "id") @setContext(name: "bookFilter", from: "bookFilter { notEqual }") {
id: ID!
title: String
bookFilter: BookFilter @inaccessible # { notEqual: [this.id] }
author: Author
}
type Author @key(fields: "id") {
id: ID!
}
# authors subgraph
type Author @key(fields: "id") {
id: ID!
# this argument is nullable. if called outside of a
# `Book -> author -> otherBooks` chain, it will just return the author's books.
# (if not "inaccessible" it could also just be public API)
otherBooks(filter: BookFilter @fromContext(name: "bookFilter")): BookConnection
}
The following alternatives are possible today, and they don't seem that bad. But they quickly become onerous if there are any intermediary layers necessitating more key stuffing or extraneous fields.
+1
Experiencing a similar issue with a customer and their experience graph proof of concept.
The use-case involves presentational context being provided by the client determination of the constraints on the device and platform (100s of different varieties), which lines similarly to the use-case mentioned in your second post (homepage(client: clientContext) {}
). However in federation world, key stuffing is leaking presentational boundaries to other domains schemas and requires this pattern to be replicated across further nested entities. Thus, we've settled on header context propagation, and will explore a Rhai script or body extension approach driven by the experience subgraph.
Let's also consider the idea of the @export
directive which could help with some of these issues. https://github.com/graphql/graphql-spec/issues/377
I wanted to comment that a simpler option, while not declarative and not following any spec (and terrible to manage), is to use response extensions on subgraph responses and have a Router script/coprocessor to forward the extensions.context
object to all subgraph requests
@smyrick Part of the goal is for the context to be limited to a specific branch of resolving data cross subgraph as not all branches are guaranteed to need the same context (otherwise headers could just be used). I think that could be achieved with what you’re describing, but it would be a nightmare to manage. That being said, custom directives could probably be developed to make this extensions idea more declarative and simpler to manage at the subgraph level.
I do think this is a fairly large gap that federation can and should solve.
@gwardwell Totally agree, this would be a perfect fit for the query planner. I wanted to just add some possible workarounds for those "blocked" today
This feature request is resolved by the @context / @fromContext directives released in Federation 2.8.
This is inspired from a customer use case. They're doing server-driven UI and have a subgraph for returning "blocks" of user interface elements. The goal is avoiding the "data" subgraphs from knowing anything about "presentational" concerns, but this is challenging when we need to thread presentational context through a hierarchy of blocks.
Example
Notice that we have to add
template { id }
to entity keys in both subgraphs. This is the "leak" between the data and presentational boundaries.Example operation
```graphql query HomePage($url: String!) { page(url: $url) { __typename ...ProductCategoryBlockFragment } } fragment ProductCategoryBlockFragment on ProductCategoryBlock { category { id products { edges { blocks { __typename ...ProductCardBlockFragment } } } } } fragment ProductCardBlockFragment on ProductCardBlock { __typename ...ProductCardTitleFragment ...ProductCardDescriptionFragment ...ProductCardImageFragment ...ProductCardPriceFragment } fragment ProductCardTitleFragment on ProductCardTitle { title } fragment ProductCardDescriptionFragment on ProductCardDescription { description } fragment ProductCardImageFragment on ProductCardImage { imageUrl } fragment ProductCardPriceFragment on ProductCardPrice { priceFormatted } ```Example query plan
```graphql QueryPlan { Sequence { Fetch(service: "experience") { { page(url: $url) { __typename ... on ProductCategoryBlock { category { __typename id template { id } } } } } }, Flatten(path: "page.@.category") { Fetch(service: "data") { { ... on ProductCategory { __typename id template { id } } } => { ... on ProductCategory { products { edges { __typename node { id } template { id } } } } } }, }, Flatten(path: "page.@.category.products.edges.@") { Fetch(service: "experience") { { ... on ProductEdge { __typename node { id } template { id } } } => { ... on ProductEdge { blocks { __typename ... on ProductCardTitle { __typename product { __typename id } } ... on ProductCardDescription { __typename product { __typename id } } ... on ProductCardImage { __typename product { __typename id } } ... on ProductCardPrice { __typename product { __typename id } } } } } }, }, Flatten(path: "page.@.category.products.edges.@.blocks.@.product") { Fetch(service: "data") { { ... on Product { __typename id } } => { ... on Product { name description imageUrl price { value currency } } } }, }, Flatten(path: "page.@.category.products.edges.@.blocks.@") { Fetch(service: "experience") { { ... on ProductCardTitle { __typename product { name id } } ... on ProductCardDescription { __typename product { description id } } ... on ProductCardImage { __typename product { imageUrl id } } ... on ProductCardPrice { __typename product { price { value currency } id } } } => { ... on ProductCardTitle { title } ... on ProductCardDescription { description } ... on ProductCardImage { imageUrl } ... on ProductCardPrice { priceFormatted } } }, }, }, } ```Prop drilling vs
setContext
This feels conceptually similar to "prop drilling" in React components. React has a concept of "context", state that is local to a tree of components. A parent component sets the context, and a descendent component can retrieve the context without the intermediary components knowing anything about it.
A Federation version of "context" might solve this elegantly. A directive for setting the context when an entity is resolved, and another for injecting the context into descendent fields might look like:
I tried hacking this together using coprocessors and it was much harder than it seemed. The particularly nasty part is that entities are deduplicated. If I have:
ProductCategoryBlock
withtemplate-1
, which contains products P1, P2, P3ProductCategoryBlock
withtemplate-2
, which contains product P2, P3, P4The query planner flattens and batches the
ProductEdge
entities into a list of P1, P2, P3, P4. We've lost track of the association with the ProductCategoryBlocks, so we can't callProductEdge.blocks
with differenttemplate
arguments. (This is avoided when overloading keys because theProductEdge
entities are unique by template.) The query planner must keep this mapping internally so that it can appropriately nest the edge objects within the block objects.I bet there's another way to do this that we haven't thought of so I wanted to write this down and start the discussion!