|
1 | 1 | # Data Masking |
2 | 2 |
|
3 | | -::: warning Work in Progress |
4 | | -This page is under construction. |
| 3 | +Data masking prevents components from accessing GraphQL fields they didn't explicitly request. This creates loosely coupled components that are more resistant to breaking changes. |
| 4 | + |
| 5 | +::: tip Recommended: Use with GraphQL Codegen |
| 6 | +Data masking works best with [GraphQL Codegen](https://the-guild.dev/graphql/codegen) for type-safe masked types. See the [TypeScript page](/data/typescript) for setup instructions. |
| 7 | +::: |
| 8 | + |
| 9 | +## The Problem |
| 10 | + |
| 11 | +Consider a parent component that fetches posts and a child component that displays post details: |
| 12 | + |
| 13 | +::: code-group |
| 14 | + |
| 15 | +```ts twoslash [Posts.vue (setup)] |
| 16 | +import type { TypedDocumentNode } from '@apollo/client' |
| 17 | +import { useQuery } from '@vue/apollo-composable' |
| 18 | + |
| 19 | +declare const POST_DETAILS_FRAGMENT: TypedDocumentNode<{ |
| 20 | + title: string |
| 21 | + publishedAt: string |
| 22 | +}> |
| 23 | +declare const gql: (literals: TemplateStringsArray, ...placeholders: any[]) => TypedDocumentNode<{ |
| 24 | + posts: Array<{ id: string, title: string, publishedAt: string }> |
| 25 | +}> |
| 26 | + |
| 27 | +// ---cut--- |
| 28 | +const GET_POSTS = gql` |
| 29 | + query GetPosts { |
| 30 | + posts { |
| 31 | + id |
| 32 | + ...PostDetailsFragment |
| 33 | + } |
| 34 | + } |
| 35 | + ${POST_DETAILS_FRAGMENT} |
| 36 | +` |
| 37 | + |
| 38 | +const { current } = useQuery(GET_POSTS) |
| 39 | + |
| 40 | +// Filter by publishedAt (defined in PostDetailsFragment) |
| 41 | +const published = current.value.result?.posts.filter(post => post.publishedAt) |
| 42 | +``` |
| 43 | + |
| 44 | +```ts twoslash [PostDetails.vue (setup)] |
| 45 | +import type { TypedDocumentNode } from '@apollo/client' |
| 46 | + |
| 47 | +declare const gql: (literals: TemplateStringsArray, ...placeholders: any[]) => TypedDocumentNode<{ |
| 48 | + title: string |
| 49 | + publishedAt: string |
| 50 | +}> |
| 51 | + |
| 52 | +// ---cut--- |
| 53 | +export const POST_DETAILS_FRAGMENT = gql` |
| 54 | + fragment PostDetailsFragment on Post { |
| 55 | + title |
| 56 | + publishedAt |
| 57 | + } |
| 58 | +` |
| 59 | +``` |
| 60 | + |
| 61 | +::: |
| 62 | + |
| 63 | +If `PostDetails` removes `publishedAt` from its fragment (because it no longer displays it), the parent component breaks silently. This **implicit dependency** between components becomes harder to track as applications grow. |
| 64 | + |
| 65 | +## Enabling Data Masking |
| 66 | + |
| 67 | +Enable data masking in the Apollo Client constructor: |
| 68 | + |
| 69 | +```ts twoslash {6} |
| 70 | +import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client' |
| 71 | + |
| 72 | +const client = new ApolloClient({ |
| 73 | + link: new HttpLink({ uri: 'https://api.example.com/graphql' }), |
| 74 | + cache: new InMemoryCache(), |
| 75 | + dataMasking: true, // Enable data masking |
| 76 | +}) |
| 77 | +``` |
| 78 | + |
| 79 | +With data masking enabled, fields defined in fragments are hidden from components that don't own them. The parent component can only access fields it explicitly requests. |
| 80 | + |
| 81 | +## Reading Masked Data |
| 82 | + |
| 83 | +Use [`useFragment`](/api/composable/functions/useFragment) to read masked fragment data in components: |
| 84 | + |
| 85 | +```vue twoslash |
| 86 | +<script setup lang="ts"> |
| 87 | +import { useFragment } from '@vue/apollo-composable' |
| 88 | +// ---cut-start--- |
| 89 | +// eslint-disable-next-line perfectionist/sort-imports |
| 90 | +import type { TypedDocumentNode } from '@apollo/client' |
| 91 | +
|
| 92 | +declare const POST_DETAILS_FRAGMENT: TypedDocumentNode<{ |
| 93 | + title: string |
| 94 | + publishedAt: string |
| 95 | +}> |
| 96 | +// ---cut-end--- |
| 97 | +
|
| 98 | +const { |
| 99 | + post |
| 100 | +} = defineProps<{ |
| 101 | + post: { __typename: 'Post', id: string } |
| 102 | +}>() |
| 103 | +
|
| 104 | +const { current } = useFragment({ |
| 105 | + fragment: POST_DETAILS_FRAGMENT, |
| 106 | + from: () => post, |
| 107 | +}) |
| 108 | +</script> |
| 109 | +
|
| 110 | +<template> |
| 111 | + <div v-if="current.resultState === 'complete'"> |
| 112 | + <h2>{{ current.result.title }}</h2> |
| 113 | + <p>{{ current.result.publishedAt }}</p> |
| 114 | + </div> |
| 115 | +</template> |
| 116 | +``` |
| 117 | + |
| 118 | +The `data` from `useFragment` contains only the fields defined in the fragment—not fields from parent queries or sibling fragments. |
| 119 | + |
| 120 | +## Fixing the Parent Component |
| 121 | + |
| 122 | +With data masking, the parent component must explicitly request any fields it needs: |
| 123 | + |
| 124 | +```ts |
| 125 | +const GET_POSTS = gql` |
| 126 | + query GetPosts { |
| 127 | + posts { |
| 128 | + id |
| 129 | + publishedAt # Now explicit - won't break if fragment changes |
| 130 | + ...PostDetailsFragment |
| 131 | + } |
| 132 | + } |
| 133 | + ${POST_DETAILS_FRAGMENT} |
| 134 | +` |
| 135 | +``` |
| 136 | + |
| 137 | +Now if `PostDetails` removes `publishedAt` from its fragment, the parent query still works because it requests the field directly. |
| 138 | + |
| 139 | +## The `@unmask` Directive |
| 140 | + |
| 141 | +Use `@unmask` to selectively disable masking for specific fragments: |
| 142 | + |
| 143 | +```graphql |
| 144 | +query GetPosts { |
| 145 | + posts { |
| 146 | + id |
| 147 | + ...PostDetailsFragment @unmask |
| 148 | + } |
| 149 | +} |
| 150 | +``` |
| 151 | + |
| 152 | +::: warning Use sparingly |
| 153 | +The `@unmask` directive is an escape hatch. Prefer adding needed fields to the parent query explicitly. `@unmask` is primarily useful during [incremental adoption](#incremental-adoption). |
5 | 154 | ::: |
| 155 | + |
| 156 | +### Migrate Mode |
| 157 | + |
| 158 | +During migration, use `@unmask(mode: "migrate")` to get development warnings when accessing would-be masked fields: |
| 159 | + |
| 160 | +```graphql |
| 161 | +query GetPosts { |
| 162 | + posts { |
| 163 | + id |
| 164 | + ...PostDetailsFragment @unmask(mode: "migrate") |
| 165 | + } |
| 166 | +} |
| 167 | +``` |
| 168 | + |
| 169 | +This logs warnings in development when you access fields that would be masked, helping you identify implicit dependencies. |
| 170 | + |
| 171 | +## What Gets Masked |
| 172 | + |
| 173 | +Data masking applies to all operation types that read data: |
| 174 | + |
| 175 | +| API | Masked | |
| 176 | +|-----|--------| |
| 177 | +| `useQuery` result | Yes | |
| 178 | +| `useMutation` result | Yes | |
| 179 | +| `useSubscription` result | Yes | |
| 180 | +| `useMutation` `update` callback | No | |
| 181 | +| `useMutation` `refetchQueries` callback | No | |
| 182 | +| `subscribeToMore` `updateQuery` callback | No | |
| 183 | +| Cache APIs (`readQuery`, `readFragment`) | No | |
| 184 | + |
| 185 | +## Incremental Adoption |
| 186 | + |
| 187 | +For existing applications, adopt data masking incrementally: |
| 188 | + |
| 189 | +### 1. Add `@unmask` to All Fragments |
| 190 | + |
| 191 | +Before enabling data masking, add `@unmask(mode: "migrate")` to all fragment spreads to prevent breaking changes: |
| 192 | + |
| 193 | +```graphql |
| 194 | +query GetPosts { |
| 195 | + posts { |
| 196 | + id |
| 197 | + ...PostDetailsFragment @unmask(mode: "migrate") |
| 198 | + } |
| 199 | +} |
| 200 | +``` |
| 201 | + |
| 202 | +### 2. Enable Data Masking |
| 203 | + |
| 204 | +```ts |
| 205 | +const client = new ApolloClient({ |
| 206 | + dataMasking: true, |
| 207 | + // ... |
| 208 | +}) |
| 209 | +``` |
| 210 | + |
| 211 | +### 3. Refactor Components |
| 212 | + |
| 213 | +Gradually refactor components to use `useFragment` and remove `@unmask` directives: |
| 214 | + |
| 215 | +1. Look for console warnings about accessing masked fields |
| 216 | +2. Update components to use `useFragment` for their fragment data |
| 217 | +3. Add any needed fields explicitly to parent queries |
| 218 | +4. Remove `@unmask` when no warnings remain |
| 219 | + |
| 220 | +## Next Steps |
| 221 | + |
| 222 | +- [Fragments](/data/fragments) - Learn about GraphQL fragments and `useFragment` |
| 223 | +- [TypeScript](/data/typescript) - Set up GraphQL Codegen for type-safe data masking |
0 commit comments