apollographql / federation

🌐  Build and scale a single data graph across multiple services with Apollo's federation gateway.
https://apollographql.com/docs/federation/
Other
666 stars 252 forks source link

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

Open terion-name opened 5 years ago

terion-name commented 5 years ago

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?

aaronleesmith commented 4 years ago

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

jon-frankel commented 4 years ago

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.

grydstedt commented 4 years ago

Any resolution to this? Workarounds?

alanhoff commented 4 years ago

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

MattZera commented 3 years ago

@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

vaptex commented 3 years ago

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.

martijnwalraven commented 3 years ago

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

terion-name commented 3 years ago

@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 commented 3 years ago

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

wickning1 commented 3 years ago

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

Vichoko commented 3 years ago

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.

TMInnovations commented 2 years ago

Same problem here ✌️

helloiambguedes commented 2 years ago

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?

patrickdronk commented 1 year ago

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

thoughtworks-tcaceres commented 1 year ago

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

netronicus commented 1 year ago

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