-
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
Data masking with fragment that should never be masked #12168
Comments
@Cellule if you're using GraphQL Codegen, it should keep those types masked. Support for Apollo's For that |
I did set For
I also strip all |
I think the thing I'm not quite understanding is why you'd want that data available runtime but not in types? I see you've made two statements here in terms of fragments:
To make sure I'm understanding this case, is it safe to assume you're passing data as a prop to a child component here? And what you're asking for is to use
I'm curious, where would you be consuming that data? Is there a reason you want to use |
Correct
Yup! Give me a bit of time to get good examples in a readable format and I'll post it here |
Ok! On that first point, unfortunately this isn't a situation we really planned on since we wanted the runtime behavior to mimic the types as much as possible. I think your custom solution is probably the best bet here for now. One other option I'll throw out there. Are you opposed to changing the fragment so that it operates on a normalized parent type? I don't know your app so I don't know how feasible this is, but this might also be a solution that avoids the need to do all the custom codegen stuff to make this work. This would also allow you to use If we get a lot of demand for what you're asking for (masked parent type, but data available as a prop), this is something we could see if we could find a solution for natively. Ultimately though I think the right answer is to support non-normalized data in fragments via |
Answering your question first
I'm not opposed to it, but it would likely require a huge amount of refactoring and I'm not 100% it would be feasible in all cases. Alright, I'll try to break down my use cases into different scenarios. Normal unmasking// List.gql.ts
gql`
fragment ListItem_Fragment on ListItem {
id
name
}
`
gql`
query ListQuery {
list {
...ListItem_Fragment @unmask
}
}
`
gql`
mutation EditListItemMutation($id: ID!, $name: String!) {
editListItem(id: $id, name: $name) {
item {
...ListItem_Fragment
}
}
}
`
// List.tsx
import { useListQuery, useEditListItemMutation } from "./List.gql.gen"
function List() {
const { data } = useListQuery()
const [editListItem] = useEditListItemMutation()
// Not a great example, but you get the point
const handleEdit = (item: IListItem_Fragment) => {
// ...
editListItem({ variables: { id: item.id, name: item.name } })
}
// Here the component, from some third party library, expects an array of items, so there's no opportunity to use `useFragment`
return <SomeListComponent items={data?.list} onEdit={handleEdit} />
} Unormalized fragments// TokenizedText.gql.ts
gql`
fragment TokenizedText_Fragment on TokenizedText @disableMasking(maskTypings: true) {
# No id available
text
tokens {
type
value
... on WorkOrderToken {
workOrderId
}
}
}
`
// TokenizedText.tsx
import { useTokenizedTextQuery } from "./TokenizedText.gql.gen"
// props.text expects a masked type of the fragment
function TokenizedText(props: { text: TokenizedText_Fragment }) {
// Here we can't use `useFragment` because the fragment is not normalized
const text = unmaskTokenizedText_Fragment(props.text)
return <div>{text.text}</div>
}
// EntityX.gql.ts
gql`
query EntityXQuery($id: ID!) {
entityX(id: $id) {
id
description {
...TokenizedText_Fragment @unmask
}
}
}
`
// EntityX.tsx
import { useEntityXQuery } from "./EntityX.gql.gen"
function EntityX(props: { id: string }) {
const { data } = useEntityXQuery({ variables: { id: props.id } })
// data.entityX.description should be masked in the typings so it cannot be used here, but the data is unmasked to be passed down to the TokenizedText component
return (
<div>
<div>{data?.entityX.id}</div>
<TokenizedText text={data?.entityX.description} />
</div>
)
} Unnormalized fragments with maskTypings disabledThese are fragment used purely to simplify data fetching from an interface. // SingleEntityResponse.gql.ts
gql`
fragment SingleEntityResponse_Fragment on ISingleEntityMutationResponse @disableMasking(maskTypings: false) {
success
errors {
fieldPath
fieldValue
error
}
}
`
// List.gql.ts
gql`
mutation EditListItemMutation($id: ID!, $name: String!) {
editListItem(id: $id, name: $name) {
...SingleEntityResponse_Fragment @unmask
item {
...ListItem_Fragment
}
}
}
`
// List.tsx
import { useListQuery, useEditListItemMutation } from "./List.gql.gen"
function List() {
const { data } = useListQuery()
const [editListItem] = useEditListItemMutation()
const handleEdit = async (item: IListItem_Fragment) => {
// ...
const response = await editListItem({ variables: { id: item.id, name: item.name } })
// No need to unmask the response, it's already unmasked
if (response.data?.editListItem.success) {
// ...
} else {
// ...
}
}
return <SomeListComponent items={data?.list} onEdit={handleEdit} />
} Validation of @disableMasking to avoid common mistakesThe piece that I struggled with the most is how to properly document to devs which fragments are "safe" to use gql`
fragment SomeFragment on SomeType @disableMasking(maskTypings: true) {
id
}
`
gql`
query SomeQuery {
some {
...SomeFragment # Error missing @unmask
...SomeFragment @nonreactive # Error @nonreactive is not allowed on fragments that disabled masking
}
}
` ConclusionRight now I'm trying to find the best in between to respect the following criteria:
I'm fine with keeping my custom solution, but I wanted to at least chime in about our use cases and if there's something we're doing terribly wrong or if there might be an option for official support either from Apollo or GraphQL Codegen I feel that now is a good time to open up the discussion |
Let me throw this out there. Our data masking docs just went live since we just published the release. There is some information in here about incremental adoption so maybe this answers a few questions about how we envision that path for existing apps: https://www.apollographql.com/docs/react/data/fragments#incremental-adoption-in-an-existing-application
Everything looks good here. Something I'll mention as well specifically with mutations is that the mutation callbacks that handle updating the cache (like the Just wanted to mention this so that you know that
Ya unfortunately this is just not something we have support for in the way you're looking. Your custom solution still makes sense after seeing this. We didn't provide any TypeScript escape hatches to give you the best of both worlds while using
The case with a fragment for use with a mutation makes sense, though again check out the reference in case one of the callbacks makes sense here which gives you the full unmasked type/value without the need for
I'd like to understand your definition of "safe" here, but if its related to that code sample, then I think you have that correct since I assume you'll want rerenders on cache updates. If you can, I'd try and limit As an aside, from a client perspective, it should be fine to use Other than that, I don't know I have a better idea than what you're doing on how to enforce that you've added
Again, I'll point you to the incremental adoption docs because you might be able to leverage some of this as part of your upgrade path sooner than later. This would give you access to Unless you enable data masking in the client, the client should function the same as it does in 3.11. While not ideal, I don't know that I have much better solutions than what you're doing right now without a native solution in place. Again I think we want to allow for non-normalized data with masking in the future, but until then I think this is your best bet. |
Yeah in 90% of the case we don't even bother updating the cache manually for simple cases, we just let the magic of the cache work
Yeah I'm considering it, but my biggest fear is dev education. Sure I can do a 1-time migration and it'll mostly work, but then I need to make sure devs either use proper fragment masking or use Otherwise thanks for your input! I'll keep that in mind and indeed I hope that Since we are prop drilling, right now we're using a failsafe where if useFragment fails to return data, we return the data passed down as props. One last thing that I haven't hit yet, but might become a problem is some devs "craft" data to look like the right data and pass that down in props. In such case the data wouldn't be in the cache and |
Urgg I just saw the |
You're on 3.12 correct? If so, thats odd because it should have the apollo-client/src/core/types.ts Line 196 in 36b93b9
|
Oops 🙊 I hadn't updated yet haha |
Phew 🎉. For a second I thought 3.12.1 might have to be on its way 😆 |
Alright then I'll close this issue for now, I don't think there's anything more to do on Apollo's side at this time. |
Do you have any feedback for the maintainers? Please tell us by taking a one-minute survey. Your responses will help us understand Apollo Client usage and allow us to serve you better. |
That would be amazing! Hope all goes well when you do start using it! Appreciate the conversation! |
This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
Moved from #11666 (comment)
What happens if a fragment is expected to NOT be masked, always?
I have some fragment on types that don't have clear ids, so I can't useFragment on it.
So far I've been using useFragment from GraphQL Codegen client preset (which I renamed unmaskFragmentData) in the component using the fragment
In that case it is expected that, while the typings are masked, the data is available at runtime to be unmasked.
I can go put
@unmask
everywhere that fragment is spread, but that's error-prone.I'm fiddling with a custom directive @disableMasking on the FragmentDefinition to ensure all places using that fragment are using
@unmask
and are not using@nonreactive
Now I'm hitting a typings issue because it unmasks the fragment, effectively making all the data available top-level, but I really only want it to be available on the component.
How can I make sure that the data will be available at runtime, but is masked in the typings?
I basically want
@unmask
at runtime, but it shouldn't be there for typings codegenRight now, my approach is to enforce the
@unmask
directive for those fragment spreads and during codegen I strip them before running typescript-operations and leave them for the runtime (at some point because I'm not ready to try apollo data masking)Note that I'm working with a heavily customized fork GraphQL Codegen client preset to make it work with Apollo client more natively
The text was updated successfully, but these errors were encountered: