federation icon indicating copy to clipboard operation
federation copied to clipboard

Federation relation filtering and arguments problems (federation design concerns / feature request)

Open terion-name opened this issue 5 years ago • 16 comments

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?

terion-name avatar Oct 17 '19 15:10 terion-name

I'm interested in this too. Is there a way to do pagination in a federated graph?

aaronleesmith avatar Nov 13 '19 23:11 aaronleesmith

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.

jon-frankel avatar Dec 27 '19 21:12 jon-frankel

Any resolution to this? Workarounds?

grydstedt avatar Feb 01 '20 16:02 grydstedt

@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 avatar Feb 02 '20 17:02 alanhoff

@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

MattZera avatar Feb 12 '21 00:02 MattZera

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.

vaptex avatar Feb 27 '21 18:02 vaptex

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 avatar Mar 01 '21 11:03 martijnwalraven

@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

terion-name avatar Mar 03 '21 00:03 terion-name

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

martijnwalraven avatar Mar 03 '21 10:03 martijnwalraven

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

wickning1 avatar Aug 05 '21 11:08 wickning1

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:

  1. What happens when referencing an entity that has defined a set of input types that resolves with filtering, excluding, limiting, and skipping results?
  2. What happens when extending an entity that has defined a set of input types that resolves with filtering, excluding, limiting, and skipping results?
  3. 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.

Vichoko avatar Oct 26 '21 14:10 Vichoko

Same problem here ✌️

TMInnovations avatar Apr 05 '22 21:04 TMInnovations

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?

helloiambguedes avatar Jun 07 '22 10:06 helloiambguedes

We're running in the same thing here. It does defeat a big reason why we are doing federated GraphQL

patrickdronk avatar Mar 03 '23 08:03 patrickdronk

@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

Facing this EXACT same issue. Has anyone found a solution as of yet?

thoughtworks-tcaceres avatar Apr 21 '23 14:04 thoughtworks-tcaceres

I'm having the same issue, the gateway is completely ignoring my field arguments. do you have any solution/workaround?

netronicus avatar May 22 '23 23:05 netronicus