Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apollo misses populating properties when fragments are spread #7648

Open
pastelsky opened this issue Feb 4, 2021 · 10 comments
Open

Apollo misses populating properties when fragments are spread #7648

pastelsky opened this issue Feb 4, 2021 · 10 comments

Comments

@pastelsky
Copy link

pastelsky commented Feb 4, 2021

Consider the following query —

  query GetMarketplaceAppListingById {
    marketplaceApp(appId: 1215460) {
      versions(first: 1) {
        edges {
          node {
            deployment {
              compatibleProducts {
                id
                ...Fragment1
              }
            }
          }
        }
      }
    }
  }

  fragment Fragment1 on CompatibleAtlassianProduct {
    __typename
    atlassianProduct {
      name
    }
  }

Here, If I spread Fragment1 instead of inlining properties, Apollo will not make the properties inside Fragment1 (e.g. atlassianProduct) available as data when using useQuery.

However, using the same query with a fetch POST request returns expected results.

image

Intended outcome:
All fields inside the spread fragment should be visible in data

Actual outcome:
All fields inside the spread fragment are ignored. However, If I inline properties of the fragment, expected results are obtained.

How to reproduce the issue:
See minimal reproduction —
https://codesandbox.io/s/apollo-graphql-fragment-bug-50yzl
Versions
3.3.x

@benjamn
Copy link
Member

benjamn commented Feb 4, 2021

@pastelsky The issue here is that Apollo Client does not automatically know that CompatibleAtlassianDataCenterProduct is a subtype of the fragment type condition CompatibleAtlassianProduct. If the type names matched exactly, everything would work, but when subtyping is involved, you need some additional cache configuration.

The way to configure this information is by passing a possibleTypes object to the InMemoryCache constructor:

const client = new ApolloClient({
  cache: new InMemoryCache({
    possibleTypes: {
      CompatibleAtlassianProduct: [
        // List of all known direct subtypes of CompatibleAtlassianProduct:
        "CompatibleAtlassianDataCenterProduct",
        // ...
      ],
    },
  }),
  uri: "https://api.atlassian.com/graphql"
});

While you can mostly get away with manually listing only the subtypes that you actually need, you might want to generate possibleTypes automatically from your schema. Here are some instructions for how you might set that up.

@pastelsky
Copy link
Author

Thanks @benjamn . I'll try this solution out. But regardless, I think it would be helpful to inform the user that a field type might not be resolvable using the currently available information and possibleTypes should be provided.

I found skipping of fields to be quite non-intuitive.

@benjamn
Copy link
Member

benjamn commented Feb 17, 2021

@pastelsky I hear you, but giving a useful warning is difficult here, because you don't need possibleTypes if all your fragment type conditions are concrete type names (like CompatibleAtlassianDataCenterProduct instead of CompatibleAtlassianProduct), and the client can't automatically detect when a fragment type condition is referring to an abstract interface or union supertype (which is the case where we would like to warn if you don't have possibleTypes, or you don't include that supertype in your possibleTypes). If there was a good way to detect this situation, I'd be happy to warn about it.

@Simply007
Copy link

Simply007 commented Nov 15, 2021

Hello, I might have a similar scenario.

I have an interface _Item that with this object implementations: LandingPage, ListingPage SimplePage.

If I run the introspection query:

{
  __schema {
    types {
      interfaces {
        name
        possibleTypes {name}
      }
    }
  }
}

I get

{
# ...
        {
          "name": "SimplePage", # same for ListingPage and LandingPage
          "kind": "OBJECT",
          "possibleTypes": null,
          "interfaces": [
            {
              "name": "_Item",
              "possibleTypes": [
                {
                  "name": "ListingPage"
                },
                {
                  "name": "LandingPage"
                },
                {
                  "name": "SimplePage"
                },
              ]
            }
          ]
        },
        # ...
        {
          "name": "_Homepage_Content",
          "kind": "UNION",
          "possibleTypes": [
            {
              "name": "LandingPage"
            },
            {
              "name": "SimplePage"
            }
          ],
          "interfaces": null
        },
# ...
}

I have a query the content property of type _Homepage_Content, which is a union type (see above in response).

{
  homepage(codename: "homepage") {
    content {
      ... on _Item {
        _system_ {
          name
          codename
        }
      }
    }
  }
}

Which is also in suggestions in GraphiQL (so the information should be in the scheme).
image

Direct request returns the data, but Apollo Client does not.

EDIT: I have added the following configuration in the client initialization and it worked, but the scheme is dynamic possible types are changing, so this is not a long time work around.

const client = new ApolloClient({
    cache: new InMemoryCache({
        // https://github.com/apollographql/apollo-client/issues/7648
        possibleTypes: {
            _Item: [
                "LandingPage",
                "ListingPage",
                "SimplePage"
            ]
        }
    }),
    uri: `${GQL_ENDPOINT}/${PROJECT_ID}`,
});

@rsimp
Copy link

rsimp commented Feb 28, 2022

I'm running into the same issue using inline fragments with an interface type. The query runs fine in graphiql or directly with the server, but not with apollo client. This is actually shown as an example in the union types section of graphql.org. Very similar to the example used for the documentation of possibleTypes.

I'm just not understanding why resolving an interface type is a concern of the client to begin with. The client is naturally going to be worse at resolving these types than the server, and it's unclear to me why it needs to.

I think it would help if the fragments section of the documentation gave an example of a feature where apollo client needs this information. In other words why can't apollo client just rely on the server and pass through the same data? What use cases are being affected by not having this information in the client?

Explaining the requirements of apollo also explains the need for manual configuration or a process to automate it. Especially when data is being discarded executing an example from graphql.org, but works querying the server directly.

@bignimbus bignimbus removed the 🏓 awaiting-contributor-response requires input from a contributor label Jan 17, 2023
@parithibang
Copy link

parithibang commented Apr 20, 2023

@benjamn

I too have the same issue. I am using "@apollo/client": "^3.7.10", The data is visible in the network tab but can't able to access it in the application.

Query:

const CREATED_BY = 
  fragment CreatedBy on Person {
        name
        jobTitle
        url
    }
;
const REVIEWED_BY = 
    fragment ReviewedBy on Person {
        name
        jobTitle
        url
    }
;
const getArticleQuery = (article: IArticleIdentifier) => ({
    document: 
${CREATED_BY}
${REVIEWED_BY}
query ArticleContentQuery($id: String!) {
    ${article.type}(where: { id: $id }) {
        text(urlKind: identifier)
        lastReviewed
        nextReviewed
        printableCopy
        headline
        version
        introduction
        creator {
            ...CreatedBy
        }
        reviewedBy {
            ...ReviewedBy
        }
        audience {
            audienceType
        }
        identifier(name: "MedicalAuthors") {
            value
        }
    }
}`,
    variables: { id: article.id },
});

This is appolo client object

const cache = new InMemoryCache({
    addTypename: false,
    typePolicies: {
        Query: {
            fields: {
                search: {
                    keyArgs: ["term", "audienceType", "type"],
                    // eslint-disable-next-line @typescript-eslint/default-param-last
                    merge(existing = { results: [] }, incoming) {
                        return { ...incoming, results: existing.results.concat(incoming.results) };
                    },
                },
            },
        },
    },
});

const useApolloClient = (accessToken?: string): ApolloClient<any> => {
    return new ApolloClient({
        link: authMiddleware(accessToken).concat(httpLink),
        cache,
    });
};

@phryneas
Copy link
Member

@parithibang I assume that your creator is not of type Person, but of a type implementing that interface? As stated above you need to provide possibleTypes in that scenario.

@parithibang
Copy link

@phryneas

The creator type name is person only

creator {
    __typename
}
"creator": {
    "__typename": "Person"
},

So my possible types should be

const cache = new InMemoryCache({
    addTypename: false,
    possibleTypes: {
        Person: ["Person"],
    },
    typePolicies: {
        Query: {
            fields: {
                search: {
                    keyArgs: ["term", "audienceType", "type"],
                    // eslint-disable-next-line @typescript-eslint/default-param-last
                    merge(existing = { results: [] }, incoming) {
                        return { ...incoming, results: existing.results.concat(incoming.results) };
                    },
                },
            },
        },
    },
});

Screenshot 2023-04-20 at 3 48 20 PM

@phryneas
Copy link
Member

@parithibang in that case, you do have a separate issue than the issue that was discussed here originally. Could you try to create a minimal reproduction for this and open a new issue?

@johnson-liang
Copy link

johnson-liang commented Jun 6, 2023

We are moving from a custom GraphQL client implementation (which does not mask fragment fields) to Apollo-client, and encountered this issue. I would like to echo @rsimp's arguments -- I am also not convinced that a GraphQL client should need to know possible types to resolve fragments in interfaces.

The previous documentation for Apollo client v2 has more statements on why clients wants supertype-subtype relationship information, although the solution back in v2 was using IntrospectionFragmentMatcher.

Coming from a much simpler GraphQL client, I am not convinced by the reasons inside the documentation. Specifically,

  • "Apollo Client cannot check the server response for you": GraphQL server implementations should already block the fields not defined in schema. Also, generated typescript types can correctly resolve the fields from the query with interface. I do not expect checking fields from server is apollo-client's duty.
  • "it cannot tell you when you're manually writing invalid data into the store using update, updateQuery, writeQuery": This may be the reason. It's just that our application don't need to mutate the store this way.

For workarounds, we have:

  • setting up possibleTypes as instructed in the documentation
  • avoid query fields under interface fragment, but repeating the reused fields in each individual types that implements the interface -- which makes the query super long.

I would like to know if it is possible to have an option that bypasses the fragment masking / matching behavior of useQuery.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants