federation
federation copied to clipboard
Federation relation filtering and arguments problems (federation design concerns / feature request)
Hello everybody.
I'm building my own gateway on top of federation protocol (specific needs that can't be handled by apollo's gateway) so digging up things deeply and seeing some problems there.
For resolving relations __resolveReference
is used. It is designed to return one and only one entity (throws error otherwise). From my perspective this is highly uneficient by itself, making gateway to produce enormous batch queries for fetching collections and/or multiple related entities and produces overcomplication on service side with dataloaders and complex stuff to optimize N+1 queries.
But ok, uneficient and complex, but has some pros (like batching from multiple sources) and can be handled (#2887).
Where real problem is - it makes literally impossible to filter and paginate queries. While some filtering can be done (btw will relation field arguments be passed down by gateway to _entities
query?) in form of constrains, null-returns and filtering (again, increased complexity), pagination — no deal.
Usecases are obvious. We have a user in users
service and user has posts in posts
service. User has hundreds of posts and I want to render his public feed. I need filters (status: public
) and pagination (cursor
or take/skip
based) for this. And now there is no proper way to do this as __resolveReference
has no idea of collection, it works with only one entity.
Ok, one could say that users
service should denormalise posts and keep fields needed for filtering and pagination at it's side, but this can work only for simple cases without complex filtering. If I want to fetch all users's posts that are public, published at specific location, with friends tagged and containing photos? "denormalise" entire post? No, filtering logic belongs to service hosting the entity.
Solution is fairly obvious at first glance: __resolveReference
should be able to return arrays and act somehow like this.
extend type User @key(fields: "id") {
id: ID! @external
posts(where: PostsWhereInput, take: Int, skip: Int): [Post!]!
}
const resolvers = {
Post: {
__resolveReference(posts, arguments) {
return products.where({id_in: posts.map(({id})=>id), ...arguments.where}, first: arguments.take, skip: arguments.skip); // and this returns array
}
},
}
Or maybe a separate resolver alike __resolveReferenceMany
(but looks kinda ugly).
Maybe this can't work with current internal logic of gateway and needs huge rework. But honestly this is a big concern for building really complex graphs.
Your thoughts?
I'm interested in this too. Is there a way to do pagination in a federated graph?
This is a core issue, and a blocker for implementing this feature, imho. I watched the videos claiming that federated GraphQL is the next generation from Apollo, but no indication of how to support this. Without it, I don’t see how you can support anything more advanced than toy Star Wars examples using arrays.
Any resolution to this? Workarounds?
@terion-name I'm not sure if I understood the problem. The example you provided should work just fine if you implement a resolver for that query that returns a collection. So inside your posts
service you would have something like this:
const typeDefs = gql`
type Pagination {
total: Int
posts: [Post]
}
extend type User @key(fields: "id") {
id: ID! @external
posts(where: PostsWhereInput, take: Int, skip: Int): Pagination
}
`;
const resolvers = {
User: {
async posts({id}, {where, skip, take}) {
const total = await db('posts').count({where: {...where, user_id: id}});
const posts = await db('posts').find({where: {...where, user_id: id}}, {skip, take});
return {
total,
posts
}
}
}
};
I've also modified the federation demo so I could test nested queries with filtering and pagination, everything works as expected:
- https://github.com/alanhoff/federation-pagination-demo
@alanhoff this doesn't solve the issue where the post itself is an external type
I am facing a similar issue in trying to implement a wishlist service
The wishlist service
will extend the user object but it only stores the id's of the products
The products are handled by the products service
which knows nothing about wishlists
type Pagination {
total: Int
products: [Product]
}
extend type User @key(fields: "id") {
id: ID! @external
wishlistItems: Pagination
}
extend type Product @key(fields: "id") {
id: ID! @external
}
My problem is that I would like to be able to sort on in-stock
in the product service
This does not seem possible at the moment because __resolveReference
does not pass more information than the ID of the product
Is there any update on this? I have several use cases like @MattZera / @terion-name mentioned that require to handle filtering on entities in other services.
I think we need to disentangle different issues, because the solution described by https://github.com/apollographql/federation/issues/359#issuecomment-581156044 is indeed the recommended pattern for the example that was originally posted. __resolveReference
is only meant to return a particular entity by key, and it is the responsibility of whatever resolver returns a list of entities to filter and/or paginate when needed. In many cases, that resolver will live in the service that also has the information needed to perform the filtering (here, that would be the posts service).
A separate issue that seems to underly the problem described by @MattZera is that a resolver sometimes needs additional information from another service to compute its result. @requires
can be used to request fields from the entity the resolver is defined on, but that isn't enough when you need fields from a list of (candidate) entities to filter and/or sort. This isn't easy to solve within the current model unfortunately. I think it would require either a notion of subqueries or a standardized understanding of collection semantics (for filtering/sorting/pagination).
@martijnwalraven
because the solution described by #359 (comment) is indeed the recommended pattern for the example that was originally posted
it dos not cover the problem originally posted at all. Users and Posts are different services tied by gateway
@martijnwalraven
because the solution described by #359 (comment) is indeed the recommended pattern for the example that was originally posted
it dos not cover the problem originally posted at all. Users and Posts are different services tied by gateway
Maybe I'm not understanding the example correctly. It seems to me the User.posts
resolver would live in the posts service and can perform the filtering and pagination by relying on its arguments and local information (about the status of the post). If that is the case, there is no need for __resolveReference
to be involved in this at all.
@MattZera's wishlist example is excellent, so I'm going to work with that, but for simplicity let's leave out pagination and say we're going to filter by in-stock instead of sort.
The situation is that both services know something about the products that should be returned - the wishlist service knows which products are on the user's wishlist, and the product service knows which products are in stock. So one of the services needs the other service to pre-filter before it applies its own filter. The only thing in the spec that allows one service to get information from another service behind the scenes is @requires
.
For separation of concerns we'd really like the wishlist service to be responsible for all wishlist related functionality, so let's try starting there first. We would try to add User.wishlistItems
from the wishlist service, but what could we possibly @requires
from a User that would help us see what's in stock?
So let's abandon that and consider adding this functionality to the product service. It breaks separation of concerns, and the product service team might get very cross with us for making them implement our wishlist functionality, but we're out of options, so we press on.
This method is ugly but it can work. We use the wishlist service to extend User with User.wishlistItemIds
; this is pretty simple, ids are what we have on the wishlist service. Then we use the product service to extend User with User.wishlistItems
and that @requires(fields: "wishlistItemIds")
. Now when we write our User.wishlistItems
resolver on the product service, we can start with the wishlistItemIds
from our User
stub, and then filter that down by products that are in stock.
I don't consider this an ideal solution and I do hope the Apollo team and anybody else working on federation can think about this example and how we might expand the federation spec to avoid:
- forced breaking of separation of concerns
- exposing API clients to ugly internal necessities like
User.wishlistItemIds
. I generally consider exposing relation ids like this an anti-pattern.
I think input type use cases as the ones exposed in the issue should be clearly exemplified in Apollo Federation Docs.
In my experience, a core characteristic of GraphQL schemas that actually need to be migrated to a Federated solution will in most cases introduce Input Types that filter, exclude, limit, sorts, and skip the set of results that the query will return. If not, as @jon-frankel said, the whole migration proposal docs it's just a Star Wars toy example.
After reading the whole documentation, it's still a mystery to me how input types are managed while federating, how can input types be merged when extending, if input types can be extended, how results are joined, and mostly the complexity of these mechanisms.
I have some use cases without answers yet:
- What happens when referencing an entity that has defined a set of input types that resolves with filtering, excluding, limiting, and skipping results?
- What happens when extending an entity that has defined a set of input types that resolves with filtering, excluding, limiting, and skipping results?
- It's possible to extend an input type? How it can be resolved?
I'm feeling forced to start prototyping the federation migration until we can see if the federation will work for our use cases, or if it has caveats that might force us to drop some of our features or implement our own ad-hoc federation-like solution. But this isn't the ideal workflow, as this is a big effort just to try something that might not work in the end.
Same problem here ✌️
I'm facing this problem right now. There are some use cases we can handle on the client but for most of them, I'm not seeing a good solution.
Any update on your scenarios?
We're running in the same thing here. It does defeat a big reason why we are doing federated GraphQL
@alanhoff this doesn't solve the issue where the post itself is an external type
I am facing a similar issue in trying to implement a wishlist service
The
wishlist service
will extend the user object but it only stores the id's of the productsThe products are handled by the
products service
which knows nothing about wishliststype Pagination { total: Int products: [Product] } extend type User @key(fields: "id") { id: ID! @external wishlistItems: Pagination } extend type Product @key(fields: "id") { id: ID! @external }
My problem is that I would like to be able to sort on
in-stock
in theproduct service
This does not seem possible at the moment because
__resolveReference
does not pass more information than the ID of the product
Facing this EXACT same issue. Has anyone found a solution as of yet?
I'm having the same issue, the gateway is completely ignoring my field arguments. do you have any solution/workaround?