-
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
Unmask types with explicit enabled: false
and default to leave the type alone
#12252
base: main
Are you sure you want to change the base?
Conversation
✅ Docs Preview ReadyConfiguration{
"repoOverrides": {
"apollographql/apollo-client@main": {
"remote": {
"owner": "apollographql",
"repo": "apollo-client",
"branch": "jerel/maybe-masked-defaults"
}
}
}
}
1 pages published. Build will be available for 30 days. |
🦋 Changeset detectedLatest commit: 20664cb 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 |
commit: |
✅ Deploy Preview for apollo-client-docs ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
size-limit report 📦
|
@phryneas this PR is good to review code-wise, but I'm working on the docs updates along with this, hence the draft status. Feel free to add suggestions though for the code itself 🙂 |
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.
This looks good to me code-wise. Once you are happy with the word-smithing, please feel free to get this out
@@ -44,8 +44,11 @@ export type MaybeMasked<TData> = | |||
// prevent "Type instantiation is excessively deep and possibly infinite." | |||
true extends IsAny<TData> ? TData | |||
: TData extends { __masked?: true } ? Prettify<RemoveMaskedMarker<TData>> | |||
: DataMasking extends { enabled: true } ? TData | |||
: true extends ContainsFragmentsRefs<TData> ? Unmasked<TData> | |||
: DataMasking extends { mode: "preserveTypes" } ? TData |
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 opted to make this explicit even though it is the default behavior without this line, that way we have a reference to it in code in case someone searches this codebase (see the docs changes for how this is conveyed in docs).
@@ -837,12 +837,12 @@ It's important that the parent query or fragment selects any [`keyFields`](../ca | |||
|
|||
### Nesting fragments in other fragments | |||
|
|||
As your UI grows in complexity, it is common to split up components into smaller, more reusable chunks. As a result you may end up with more deeply nested components that have their own data requirements. Much like queries, we can nest fragments within other fragments. | |||
|
|||
<Note> |
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 opted to swap the order of this note and the sentence above as I think it reads a bit better since it now includes an introduction before the side comment.
You can opt in to use the masked types in one of two ways. | ||
|
||
##### Opting in globally | ||
Additionally, if you use the [generated `useFragment` function](https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#the-usefragment-helper), you must use Apollo Client's [`useFragment`](#usefragment) hook instead. |
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 don't know if it makes sense to include a note on migrating from codegen's fragment masking feature. I had a difficult time figuring out how to mention it.
The steps to migrate are roughly:
- Migrate to Apollo Client's
useFragment
hook first - Disable fragment masking feature in codegen config
- Enable
dataMasking
on the client
The key thing here is that dataMasking
should remain off until fully migrated, otherwise it messes with the fragment masking feature since it needs the full result type.
That said, these 3 things are already sorta listed in this section, just not as a "migrate away from" type way. Do you think there is anything more to say or do you think there is enough here that makes it obvious what needs to be done?
|
||
To turn on masked types for your whole application at once, modify the `DataMasking` exported type from `@apollo/client` using TypeScript's [declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) ability. | ||
<MinVersion version="3.12.5"> | ||
#### Setting a types mode for masked types |
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.
This is the biggest change in the docs for this PR. With the move to mode
instead of enabled
, it didn't make much sense to talk about this as a "opt into masked types", since really all that was doing was preserving the type and preventing it from being unwrapped. I've oriented this now around the modes and what they do.
I've provided a migration path for 3.12.5 for those that want/need it in the PR description. Is that something we should include in the docs as well, or should we point to the PR for these instructions instead?
##### Modes | ||
|
||
<Note> | ||
Prior to Apollo Client version 3.12.5, the default was to [unmask](#unmask) the types. If you upgrade to 3.12.5 or later and wish to maintain the same default as 3.12.4 or below, set the mode to [`unmask`](#unmask). See pull request [#12252](https://github.com/apollographql/apollo-client/pull/12252) for more details on this change. |
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.
Prior to Apollo Client version 3.12.5, the default was to [unmask](#unmask) the types. If you upgrade to 3.12.5 or later and wish to maintain the same default as 3.12.4 or below, set the mode to [`unmask`](#unmask). See pull request [#12252](https://github.com/apollographql/apollo-client/pull/12252) for more details on this change. | |
Prior to Apollo Client version 3.12.5, the default was to [unmask](#unmask) the types. If you upgrade to 3.12.5 or later and wish to maintain the same default as 3.12.4 or below, set the mode to [`unmask`](#unmask). See pull request [#12252](https://github.com/apollographql/apollo-client/pull/12252) for more details on this change, including a section on migrating from 3.12.4 or below. |
Based on my previous comment, perhaps this is enough to satisfy communication on migrating to this new change?
|
||
##### Opting in per operation | ||
Setting the mode to `preserveTypes` makes no modification to the operation types regardless of whether the type definitions are masked or unmasked. This provides a simpler upgrade path when you're ready to [incrementally adopt](#incremental-adoption-in-an-existing-application) data masking. |
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.
Should I mention this is the default behavior in a followup sentence, or is the header clear enough that this is the default behavior?
@@ -1419,9 +1423,9 @@ new ApolloClient({ | |||
|
|||
> Enabling data masking early in the adoption process makes it much easier to adopt for newly added queries and fragments since masking becomes the default behavior. Ideally data masking is enabled in the same pull request as the `@unmask` changes to ensure that no new queries and fragments are introduced to the codebase without the `@unmask` modifications applied. | |||
|
|||
#### 3. Generate and opt in to use masked types |
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 really is no "opt in" anymore after this change so I tried to remove that language everywhere I could.
@Meschreiber the docs changes should be good to review now, barring any significant feedback that the switch to |
I switched the approach and would prefer a new review
!docs preview |
@@ -44,7 +44,8 @@ export type MaybeMasked<TData> = | |||
// prevent "Type instantiation is excessively deep and possibly infinite." |
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 would suggest moving the DataMasking
tests up:
export type MaybeMasked<TData> =
DataMasking extends { mode: "unmask" } ?
// distribute TData - in case of a union, do the next steps for each member
TData extends any ?
// prevent "Type instantiation is excessively deep and possibly infinite."
true extends IsAny<TData> ? TData
: TData extends { __masked?: true } ? Prettify<RemoveMaskedMarker<TData>>
: true extends ContainsFragmentsRefs<TData> ? Unmasked<TData>
: TData
: never
: DataMasking extends { mode: "preserveTypes" } ? TData
: TData;
This way, in cases where mode
is not "unmask"
, the types won't be touched at all.
While that might seem counterintuitive, the benefit is that TData
itself will not need to be inspected at all, which enables cases where TData
is unknown at the time of invoking MaybeMasked
.
Here is a new test that shows the problem:
test("MaybeMasked can be called with a generic if `mode` is not set to `unmask`", (prefix) => {
function withGenericResult<T extends { [key: string]: string }>(
arg: TypedDocumentNode<T, {}>
) {
bench(prefix + "Result generic - instantiations", () => {
const maybeMasked: MaybeMasked<typeof arg> = arg;
return maybeMasked;
}).types();
bench(prefix + "Result generic - functionality", () => {
const maybeMasked: MaybeMasked<typeof arg> = arg;
expectTypeOf(maybeMasked).toEqualTypeOf(arg);
});
}
function withGenericDocument<T extends TypedDocumentNode>(arg: T) {
bench(prefix + "Result generic - instantiations", () => {
const maybeMasked: MaybeMasked<T> = arg;
return maybeMasked;
}).types();
bench(prefix + "Result generic - functionality", () => {
const maybeMasked: MaybeMasked<T> = arg;
// cannot use unresolved generic with `expectTypeOf` here so we just try an assignment the other way round
const test: T = maybeMasked;
return test;
});
}
withGenericResult({} as any);
withGenericDocument({} as any);
});
The drawback is that the Masked
marker would not be removed, but if you don't set { mode: "unmask" }
I don't think Masked
has any use case, right?
Fixes #12248
Fixes #12251
After enough feedback from users upgrading to 3.12.x, we feel the default behavior of applying
Unmasked
withMaybeMasked
is the wrong default since this does type modification. That change has made it difficult for those currently using GraphQL Codegen's fragment masking feature and has introduced regressions in TypeScript compilation performance (#12248) simply from upgrading from an older version of the client.This PR changes the default behavior of
MaybeMasked
to do nothing with the types unless otherwise specified. Theenabled
flag has been deprecated in favor of a newmode
property which better describes the two different behaviors used for TypeScript:preserveTypes
This is the new default and will simply leave
TData
alone with no type modification. Since this is the new default, users don't need to set this to get this behavior.unmask
This will unwrap all operation types by default into their unmasked form. This is the same as the 3.12.4 or below default. Users will now need to explicitly opt into using the unmasked types
Setting the mode
To set the mode, you use the same declaration merging syntax as before:
Migrating from 3.12.4 or below
If you've opted into masked types by setting the
enabled
property totrue
, you can remove this configuration entirely:If you prefer to specify the behavior explicitly, switch from
enabled: true
, tomode: "preserveTypes"
:If you rely on the default behavior in 3.12.4 or below and would like to continue to use unmasked types by default, set the
mode
tounmask
: