-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Introduce the removeTypenameFromVariables
link to remove __typename
from variables
#10853
Conversation
🦋 Changeset detectedLatest commit: b94fbda The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
excludeScalars?: (string | ScalarConfig)[]; | ||
} | ||
|
||
export function removeTypenameFromVariables( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've been using the term "strip typename", but I felt "remove typename" felt a bit more "professional". Let me know what you think! I'm happy to rename this back to stripTypenameFromVariables
if we prefer that verbiage.
src/link/remove-typename/__tests__/removeTypenameFromVariables.ts
Outdated
Show resolved
Hide resolved
@@ -1,14 +1,28 @@ | |||
import { DeepOmit } from '../types/DeepOmit'; | |||
import { isPlainObject } from './objects'; | |||
|
|||
export function omitDeep<T, K extends string>(value: T, key: K) { | |||
return __omitDeep(value, key); | |||
const BREAK: unique symbol = Symbol('BREAK'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if naming this something like KEEP_ALL
would be more semantically meaningful. Would love to hear thoughts on whether BREAK
makes sense as a name here or not. This is meant to mean "stop traversal and keep the omitted field for all values inside this object"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"break" as in the sense of "early return for this subtree" makes sense for me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thats where I got the name from initially 😆
removeTypenameFromVariables
link to allow __typename
to be stripped from variablesremoveTypenameFromVariables
link to remove __typename
from variables
const link = removeTypenameFromVariables({ | ||
excludeScalars: [ | ||
{ | ||
scalar: 'JSON', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait... in this test, nothing mentions JSON
anywhere. Can you explain what exactly the scalar
property here does?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The paths
describe all the fields where JSON
scalar live. For example, if your schema with an input object looks like this:
scalar JSON
type Query {
someField(foo: FooInput)
}
input FooInput {
bar: JSON
baz: JSON
}
All we can detect from the query is that foo
is of type FooInput
, but without full schema introspection, its difficult to know which fields on input objects are of type JSON
, hence the paths
option.
This scalar declaration declares JSON
as a type that should be excluded when removing the typename. While this test doesn't include it, this means variables of type JSON
should be excluded when removing the typename:
query Test($foo: JSON) {
someField(foo: $foo)
}
Does this help?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll add one more clarifying example that might hammer this home. Let's say my query looks like this:
query TestQuery($foo: FooInput, $json: JSON) {
someField(foo: $foo, json: $json)
}
With configuration:
removeTypenameFromVariables({
excludeScalars: [
{ scalar: 'JSON', paths: { FooInput: ['bar'] } }
]
});
This would keep __typename
on variable json
since its of type JSON
and it would also keep __typename
on the field bar
of variable foo
since its of type FooInput
with a JSON
scalar field bar
.
Since excludeScalars
is an array, this allows you to define several scalar types that should keep the __typename
value:
removeTypenameFromVariables({
excludeScalars: [
{ scalar: 'JSON', paths: { FooInput: ['bar'] },
{ scalar: 'Raw' },
{ scalar: 'Config' }
]
});
// or you can use the string shorthand for scalar configs that don't have a `paths` option
removeTypenameFromVariables({
excludeScalars: [
'Raw',
'Config',
{ scalar: 'JSON', paths: { FooInput: ['bar'] },
]
});
Hopefully that helps give some more context
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@phryneas I changed the API signature after some feedback. I've updated the PR description to reflect the change. Let me know what you think about the change!
After the initial review with @phryneas and talking this over a bit more with @benjamn, the API proposed in this PR seems a bit too confusing. It's not clear enough the effect the Instead, I am going to experiment with a fully object-based configuration. Scalars are just types that happens to live at the leaves of the tree, so the configuration doesn't have to differentiate itself from other non-scalar types with path configurations. The configuration might look something like this: removeTypenameFromVariables({
exclude: {
JSON: true,
FooInput: {
bar: true,
deeply: {
nested: true
}
}
}
}) This also has the advantage that it looks a little more like GraphQL queries themselves. The trick here is figuring out the leaf delimiter. While // import name TBD
import { removeTypenameFromVariables, KEEP } from '@apollo/client/link/remove-typename';
removeTypenameFromVariables({
exclude: {
JSON: KEEP,
FooInput: {
bar: KEEP,
deeply: {
nested: KEEP
}
}
}
}) where |
const link = removeTypenameFromVariables({ | ||
except: { | ||
FooInput: { | ||
bar: KEEP, | ||
baz: { | ||
qux: KEEP, | ||
}, | ||
}, | ||
}, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So readable!
variables: stripTypename(variables, { | ||
keep: (variablePath) => { | ||
const typename = variableDefinitions[variablePath[0]]; | ||
|
||
// The path configurations do not include array indexes, so we | ||
// omit them when checking the `trie` for a configured path | ||
const withoutArrayIndexes = variablePath.filter( | ||
(segment) => typeof segment === 'string' | ||
); | ||
|
||
// Our path configurations use the typename as the root so we need to | ||
// replace the first segment in the variable path with the typename | ||
// instead of the top-level variable name. | ||
return trie.peekArray([typename, ...withoutArrayIndexes.slice(1)]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How essential is this options.keep
callback API for stripTypename
and omitDeep
? If there was another way to implement this code, would we still want to ship options.keep
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Its essential in the fact that we need to know when to leave the key at a certain path alone vs remove it, but the callback is more an implementation detail than a necessity. I had considered an object-based approach, but couldn't figure out a good way to represent the syntax for array items.
Since omitDeep
is designed to be a general purpose utility, I could see cases where we might want to use it in some way to iterate over data from an interface type, where we conditionally omit a field in that union type based on the concrete type (though I have no use case for something like this at the moment). The callback style API felt the most flexible way to allow for everything.
I'm curious, what do you have in mind? I'm all ears and not attached to this if you have a better idea in mind.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My hypothetical concern was that we might end up calling keep
for every possible path in a very large variables
object, which could be expensive since the keep
function is creating three new arrays in addition to the Trie lookup.
Instead, we might be able to call stripTypename
on just the subtrees that are not protected by KEEP
, without needing to configure the stripTypename
call with a keep
callback. In other words, we'd be calling stripTypename
multiple times on different subtrees of the object, and since each of those calls strips all __typename
fields in its subtree, we already know what keep
would return for that entire subtree, so calling keep
becomes unnecessary.
On the other hand, if/when we come up with something more efficient, we should be able to switch over to a different implementation without changing the behavior of this code. In that scenario, we might not end up using keep
internally, but I don't have a problem with the keep
API existing as part of stripTypename
and omitDeep
.
In other words, this question doesn't need to block the PR, but I will keep thinking about it, in case a good alternative occurs to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ya this is a great observation. Totally agree, let's keep noodling on this. If we can find a good way to implement this, I'd be all for this optimization.
I'll go ahead and merge this PR as-is and once we have a good optimization solution, we can open up a follow-up refactor PR. I would consider these helpers internal so we can make breaking changes to the function signature if/when we need to.
Thanks for thinking this through!
In this example, how can I exclude the typename on |
@phryneas the configuration is meant to represent the fields on types that have JSON scalar as their types. scalar JSON
type Query {
someField(foo: FooInput)
}
input FooInput {
bar: JSON
baz: BazInput
}
input BazInput {
deeply: DeeplyInput
}
input DeeplyInput {
nested: JSON
} Leaving The configuration was designed with input types in mind where you could tell us which fields are the JSON scalar types since we can't determine that from the query itself. In that way you should only be able to configure a Does that make sense? |
Yeah, with that explanation it does. I get a feeling that this one will need a very good example in the docs, though. |
Oh absolutely. I'm willing to bet this is a very very VERY small percentage of users that will even care about keeping |
size-limit report 📦
|
Closes #10767
The removal of
__typename
from variables introduced in #10724 was determined to be too much of a breaking change as JSON-scalars could be utilizing__typename
without violating the GraphQL spec (more information on this discussion can be found in #10767).This PR moves this behavior into a new link called
removeTypenameFromVariables
that can be added to any link chain. This has the advantage that it can be used by any terminating link, unlike #10724 where this functionality was limited toHttpLink
,BatchHttpLink
, andGraphQLWsLink
.By default, this link will remove all
__typename
keys fromvariables
. If a user has a JSON-scalar that utilizes__typename
, the scalar can be configured with the link to keep__typename
in those fields. JSON scalar fields nested inside input objects can also be configured to ensure__typename
is kept at those paths. NOTE: JSON scalars don't literally have to be calledJSON
. These scalars can show up with other names (e.g.Raw
,Config
, etc).The API introduced in this PR was created with code generation in mind. Much like
possibleTypes
, we should be able to recommend a code generation approach that will extract the configuration for you.The API is as follows:
These types are detected from the variable declarations in the query.
Checklist: