dotansimha / graphql-code-generator

A tool for generating code based on a GraphQL schema and GraphQL operations (query/mutation/subscription), with flexible support for custom plugins.
https://the-guild.dev/graphql/codegen/
MIT License
10.84k stars 1.33k forks source link

Support an incremental adoption of Fragment Masking #9075

Open charpeni opened 1 year ago

charpeni commented 1 year ago

Is your feature request related to a problem? Please describe.

I recently migrated a project to client-preset (from gql-tag-operations-preset) and would like to turn on Fragment Masking for it. Unfortunately, because of existing fragments that are not using Fragment Masking, we already have a thousand errors to handle to be able to.

When you enable Fragment Masking, all fragments, and operations are automatically masked, even though not all fragments or operations use Fragment Masking.

I would like the ability to unmask fragments recursively to support an incremental adoption of Fragment Masking.

Also related to:

Nested Fragment: <UserAvatar>

export const UserAvatarUserFragment = gql(`
  fragment UserAvatarUser on User {
    avatarUrl
  }
`);

type UserAvatarProps = {
  user: ResultOf<typeof UserAvatarUserFragment>;
};

function UserAvatar(props: UserAvatarProps) {
  return <img src={props.user.avatarUrl} />;
}

Fragment: <UserCard>

export const UserCardUserFragment = gql(`
  fragment UserCardUser on User {
    id
    name
    ...UserAvatarUserFragment
  }
`);

type UserCardProps = {
  user: ResultOf<typeof UserCardUserFragment>;
};

function UserCard(props: UserCardProps) {
  return (
    <>
      <UserAvatar user={props.user} />
      {/*         ^ Property 'avatarUrl' is missing in type  */}
      {/*         ^ Type '"UserCardUserFragment"' is not assignable to type '"UserAvatarUserFragment"' */}
      <div>ID: {props.user.id}</div>
      <div>Name: {props.user.name}</div>
    </>
  );
}

Query: <ExampleComponent>

export const ExampleComponentQuery = gql(`
  query ExampleComponent($id: String!) {
    user(id: $id) {
      ...UserCardUser
    }
  }
`);

type ExampleComponentProps = {
  id: VariablesOf<typeof ExampleComponentQuery>['id'];
};

export function ExampleComponent(props: ExampleComponentProps) {
  const { loading, data } = useQuery(ExampleComponentQuery, { variables: { id: props.id } });

  if (loading) {
    return <>loading...</>;
  }

  if (!data?.user) {
    return <>no user</>;
  }

  return <UserCard user={data.user} />;
}

Describe the solution you'd like

We could expose the following utility type: UnmaskResultOf<TypedDocumentNode>.

This would check if whether we're inside an operation or directly within a fragment and then recursively flatten fragments by resolving $fragmentRefs and merging them to the root fragment.

type UserCardProps = {
- user: ResultOf<typeof UserCardUserFragment>;
- // ^? (property) user: { __typename: "User"; id: string; name: string; ' $fragmentRefs': { ... }}
+ user: UnmaskResultOf<typeof UserCardUserFragment>;
+ // ^? (property) user: { __typename: "User"; id: string; name: string; avatarUrl: string }
};
const testUnmaskResultOf: UnmaskResultOf<ExampleComponentQuery> = {
  __typename: 'Query',
  user: {
    __typename: 'User',
    id: 'some-id',
    name: 'some-name',
    avatarUrl: 'some-avatar-url',
  },
};

See the implementation on TypeScript Playground: Link.

Warning: Pull request with the full implementation will follow.

Describe alternatives you've considered

No response

Is your feature request related to a problem? Please describe.

No response

charpeni commented 1 year ago

Here's the implementation: https://github.com/dotansimha/graphql-code-generator/pull/9380.

brandonwestcott commented 1 year ago

Our team also has the same pain points in our incremental adoption as well.

For others who may need a solution, we ended up generating both masked (src/gql) and fragmentMasking: false (types src/gql/unmasked), and then wrote a couple of helpers to utilize them:

In src/gql/full

import { ResultOf } from '@graphql-typed-document-node/core'
import type { graphql as graphqlUnmasked } from 'gql/unmasked'
import type * as UnmaskedNamespace from 'gql/unmasked/graphql'
import { graphql as graphqlMasked } from './gql'

export const graphql = graphqlMasked as typeof graphqlUnmasked

type Unmasked = typeof UnmaskedNamespace

type GraphqlNodeName = {
  [Key in keyof Unmasked as Key]: (
    Key extends `${infer FragmentName}FragmentDoc` ? FragmentName :
    Key extends `${infer DocumentName}Document` ? DocumentName :
    never
  )
}[keyof Unmasked]

export type FullResultOf<
  Name extends GraphqlNodeName,
  FragmentName = `${Name}FragmentDoc`,
  DocumentName = `${Name}Document`
> = (
    FragmentName extends keyof Unmasked ? ResultOf<Unmasked[FragmentName]> :
    DocumentName extends keyof Unmasked ? ResultOf<Unmasked[DocumentName]> :
    never
  )

Then to fetch the types you can either: Use the normal types, just importing graphql from gql/full

import { graphql } from 'gql/full'
import { ResultOf } from '@graphql-typed-document-node/core'

const Fragment = graphql(`
  fragment TestFragment_UserFragment on User {
    id
    ...NameCard_UserFragment
  }
`)

type FullUser = ResultOf<typeof Fragment>

Or you can look up the type via its name

import { FullResultOf } from 'gql/full'

const Fragment = graphql(`
  fragment TestFragment_UserFragment on User {
    id
    ...NameCard_UserFragment
  }
`)

type FullUser = FullResultOf<'TestFragment_UserFragment'>
bryanmylee commented 1 year ago

I've built an implementation that takes @charpeni's implementation further, and I've tested for:

  1. nested fragments / sibling fragments
  2. fragments in nested objects
  3. fragments in arrays
  4. fragments in optional fields

The implementation can be found here.