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

Automatically strip __typename for input data in variables #10724

Merged
merged 26 commits into from
Apr 13, 2023

Conversation

jerelmiller
Copy link
Member

@jerelmiller jerelmiller commented Apr 5, 2023

Closes apollographql/apollo-feature-requests#6

A highly requested feature of ours is the ask for the library to automatically strip __typename fields for arguments passed to a mutation. Because we automatically request __typename as part of the GraphQL document, this can cause issues when trying to use that query data as an input argument to a mutation since the GraphQL spec states that input fields must not have a name that begins with __ (two underscores).

Looking through that feature request, you'll notice many have attempted to solve this in creative ways. While a lot of the solutions are stripping them correctly (e.g. stripping __typename in variables), there is risk that a naive implementation might either try and stop the __typename from being requested at all, or by trying to post-process the query result in a custom link and removing the __typename field. These solutions are ill-advised as the cache and type policies rely on __typename to reliably work.

Instead of asking our users to get creative with ways to strip __typename, this PR instead will automatically strip variables of any __typename field (perhaps deeply nested) for any query, mutation, or subscription sent through HttpLink or BatchHttpLink, or GraphQLWsLink. This allows users to use existing query data as input to other GraphQL operations more naturally (most commonly mutations).


As an example, say I have a query that requests a todo:

const GET_TODO = gql`
  query GetTodo($id: ID!) {
    todo(id: $id) {
      id
      name
      completed
    }
  }
`;

const { data } = useQuery(GET_TODO);

Now lets say I have a mutation that allows me to update that todo in some way. An easy way to do this would be to reuse the data returned from my query with updated property values.

const UPDATE_TODO = gql`
  mutation UpdateTodo($todo: TodoInput!) {
    updateTodo(todo: $todo) {
      id
      name
      completed
    }
  }
`;
const [updateTodo] = useMutation(UPDATE_TODO);

const { data } = useQuery(GET_TODO);

<button 
  onClick={() => updateTodo({ ...data.todo, completed: true })}
>
  Complete Todo
</button>

This mutation will fail however since the __typename returned from the query is still present in data.todo which is sent to the server.


While we could ask users to strip __typename out, as demonstrated by the feature request, this is done in many different ways. I'd also argue that for more "beginner" users (users that are new to Apollo Client), its not apparent that __typename is even there. If you look at the code samples above, nowhere do you see this field requested, so its not visibly apparent that you are going to run into issues.

I find this small addition to be a huge quality of life improvement, especially since the default behavior of the client is to automatically add __typename to all outgoing GraphQL documents.

Checklist:

  • If this PR contains changes to the library itself (not necessary for e.g. docs updates), please include a changeset (see CONTRIBUTING.md)
  • If this PR is a new feature, please reference an issue where a consensus about the design was reached (not necessary for small changes)
  • Make sure all of the significant new logic is covered by tests

@changeset-bot
Copy link

changeset-bot bot commented Apr 5, 2023

🦋 Changeset detected

Latest commit: 12d4dae

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@apollo/client Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Member

@benjamn benjamn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This omitDeep utility seems super useful! A few suggestions for perfecting the implementation.

Comment on lines 28 to 38
if (isNonNullObject(value)) {
const obj = Object.create(Object.getPrototypeOf(value));
known.set(value, obj);

Object.keys(value).forEach((k) => {
if (k !== key) {
obj[k] = __omitDeep(value[k], key, known);
}
});

return obj;
Copy link
Member

@benjamn benjamn Apr 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we take advantage of situations where value does not contain key, and none of its children/descendents do, either?

I'm imagining __omitDeep could sometimes return its input value unmodified even if it's an object or an array, because there was nothing to modify, and then you could take advantage of those shortcuts by not always creating a new obj object here (if all the child values come back === and value does not contain key).

Ideally, if you pass a large object tree to omitDeep that doesn't have the key anywhere within it, you'd get back the same (===) object, not a deep copy.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a really great point! I'll add this behavior.

Copy link
Member Author

@jerelmiller jerelmiller Apr 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this behavior in 72c7d98. Let me know what you think!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed some additional behavior in c741fbf

src/utilities/types/DeepOmit.ts Show resolved Hide resolved
src/link/subscriptions/index.ts Outdated Show resolved Hide resolved
src/utilities/types/DeepOmit.ts Outdated Show resolved Hide resolved
@jerelmiller jerelmiller requested a review from benjamn April 7, 2023 16:20
Copy link
Member

@benjamn benjamn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with one extra safety consideration (see comment below).

src/utilities/index.ts Show resolved Hide resolved
src/utilities/types/DeepOmit.ts Show resolved Hide resolved
src/utilities/common/omitDeep.ts Outdated Show resolved Hide resolved
@jerelmiller
Copy link
Member Author

This PR is ready to go. I'll rebase and merge once the type issues are fixed on the base release-3.8 branch.

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

Successfully merging this pull request may close these issues.

3 participants