graphql-code-generator icon indicating copy to clipboard operation
graphql-code-generator copied to clipboard

Implement fragment unmasking utility types

Open charpeni opened this issue 2 years ago • 7 comments

Description

Implementation of fragment unmasking to support an incremental adoption of fragment masking or utils for tests.

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

We can expose the following utility types: UnmaskResultOf<TypedDocumentNode> & UnmaskFragmentType<TType>.

It checks whether we're inside an operation or directly within a fragment and then recursively flattens fragments by resolving $fragmentRefs and merging them to the root fragment.

const FilmItemFragment = graphql(`
  fragment FilmItem on Film {
    id
    title
  }
`);

const AllFilmsFragment = graphql(`
  fragment AllFilms on Root {
    allFilms {
      films {
        ...FilmItem
      }
    }
  }
`);

const myQuery = graphql(`
  query Films {
    ...AllFilms
  }
`); // DocumentTypeDecoration<R, V>

// Fragment references are all inlined rather than referring to ` $fragmentRefs`.
type UnmaskedData = UnmaskResultOf<typeof myQuery>;
//   ^? type UnmaskedData = {
//        __typename: 'Root' | undefined,
//        allFilms: {
//          films: {
//            __typename?: "Film" | undefined;
//            id: string;
//            title?: string | null | undefined;
//          }[] | null | undefined
//        } | null | undefined
//      }

Related #9075


I also introduced expect-type to test those newly generated TypeScript utilities to prevent regressions. Currently, there's no way to test it before generating fragment-masking.ts as definitions are living within strings, so we need to generate examples and then rely on a Jest test within apollo-client's example combined with expect-type to test them.

Type of change

  • [x] New feature (non-breaking change which adds functionality)
  • [x] This change requires a documentation update

Screenshots/Sandbox (if appropriate/relevant):

Screenshot 2023-05-09 at 9 32 23 AM Screenshot 2023-06-05 at 3 42 09 PM

How Has This Been Tested?

  • [x] Added tests to cover TypeScript utility types
  • [x] TypeScript Playground
  • [x] I have followed the CONTRIBUTING doc and the style guidelines of this project
  • [x] I have performed a self-review of my own code
  • [x] I have commented my code, particularly in hard-to-understand areas
  • [x] I have made corresponding changes to the documentation
  • [x] My changes generate no new warnings
  • [x] I have added tests that prove my fix is effective or that my feature works
  • [x] New and existing unit tests pass locally with my changes
  • [x] Any dependent changes have been merged and published in downstream modules

charpeni avatar May 09 '23 13:05 charpeni

🦋 Changeset detected

Latest commit: f18151a427a71b62dced5d9f726f023c5ca067cb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@graphql-codegen/client-preset Minor

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

changeset-bot[bot] avatar May 09 '23 13:05 changeset-bot[bot]

@charpeni just wanted to provide feedback as our team attempted to utilize these proposed types in our application. In our case, any fragment over 2 to 3 levels deep caused widespread TS2589: Type instantiation is excessively deep and possibly infinite..

brandonwestcott avatar Jun 14 '23 15:06 brandonwestcott

@brandonwestcott Do you believe it's something that could be reproduced on TypeScript Playground?

Not directly related to GraphQL Code Generator, but I noticed some weird behaviors starting with TypeScript 5.0 and worse with 5.1 related to Type instantiation is excessively deep and possibly infinite when it's not excessively deep at all.

I tried to reproduce it on TypeScript Playground with one query and five nested fragments, and I couldn't: 🔗 TypeScript Playground.

But! Weirdly enough, I was able to reproduce it when trying to unmask a type that couldn't be resolved: 🔗 TypeScript Playground.

Screenshot 2023-06-23 at 2 43 24 PM

charpeni avatar Jun 23 '23 18:06 charpeni

I like the idea a lot. But here's my problem: I'm in Vue, using bindings from @urql/vue (useQuery, useFragment and so on). Since the types proposed here accept as input the result from a graphql('...') function call, I'd have to

const rawQuery = graphql(`...`);
const queryResData = useQuery({
  query: rawQuery
}).data as UnmaskResultOf<typeof rawQuery>

which is quite entangled. I'd love to be able to write a function unmaskAllFragments, which would be a no-op only doing the necessary assertion. To be used like

const queryRes = unmaskAllFragments(useQuery({
  query: graphql(`...`)
});

This would require the generic type accepted by UnmaskResultOf to be something other than DocumentTypeDecoration, because that is no longer present in the result of useQuery (but, all the necessary information should still be there, since I can "manually" unmask the fragment with useFragment).

I'll dig around a bit more, maybe I'll find a way to achieve this, but I wouldn't bet on it ;)

MatthiasvB avatar Jul 12 '23 09:07 MatthiasvB

Just to note, the utility type doesn't work on fragments applied to nested objects of the query, which my team unfortunately depends on.

Playground link.

bryanmylee avatar Aug 01 '23 15:08 bryanmylee

Adapted your implementation to take it one step 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.

bryanmylee avatar Aug 01 '23 16:08 bryanmylee

No more updates? I would like to use this utility type too. I will try to implement it in another PR if there is no further update

tnyo43 avatar Oct 12 '23 11:10 tnyo43