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

resolve `makeFragmentData` type problem by creating unmask Fragment utility type

Open tnyo43 opened this issue 2 years ago • 6 comments

You can try the useFragment function and UnmaskFragment type with graphql-code-generator-unmask-fragment package until it will be merged.

Description

Related https://github.com/dotansimha/graphql-code-generator/issues/9702 also related to https://github.com/dotansimha/graphql-code-generator/pull/9380

makeFragmentData can't make a mock data with a fragment includes multiple fragments (see #9702 ). I created UnmaskFragment and apply it to the first argument of the function to solve the problem.

Type of change

  • [x] Bug fix (non-breaking change which fixes an issue)
  • [x] Breaking change (fix or feature that would cause existing functionality to not work as expected)

This might be a breaking change if you are using nested makeFragmentData to create a mock data for a nested fragment as like follows:

makeFragmentData({
  bar: makeFragmentData({ ... }, BarFragment)
}, SomeFragment);

Screenshots/Sandbox (if appropriate/relevant):

sandbox of testing the UnmaskingFragment utility type

How Has This Been Tested?

  1. check the failing code with the current makeFragmentData at a sandbox 1.1. clone https://github.com/tnyo43/graphql-code-generator-issue-fragment-conflict and move to the root of it. 1.2. run pnpm run install & pnpm run generate. 1.3. open "./src/User.tsx" and check the mockData is not able to be typed without any type assertion.
    • this is because User_UserFragment includes multiple fragment.
  2. prepare this repository 2.1 clone this repository and checkout to this branch (tnyo43:update-make-fragment-data-args-type) 2.2 run yarn install & yarn build
  3. apply the change to the sandbox (see https://github.com/tnyo43/graphql-code-generator-issue-fragment-conflict/pull/1) 3.1. move to sandbox and update the dependency of "@graphql-codegen/client-preset" to refer to the local change (ex. "@graphql-codegen/client-preset": "file:../graphql-code-generator/packages/presets/client"). 3.2. run pnpm run install & pnpm run generate. 3.3. open "./src/User.tsx" and update mockData as like follows. You will see that we don't need any extra type assertion for making mock data!
const mockData = makeFragmentData(
  {
    id: "user_1",
    username: "aaa",
    avatarUrl: "aaa",
    email: "[email protected]",
  },
  User_UserFragment
);

Test Environment:

  • OS: macOS 14.0 (23A344)
  • @graphql-codegen/cli: 4.0.1
  • @graphql-typed-document-node/core: 3.2.0,
  • typescript: 5.2.2
  • NodeJS: 18.16.0

Checklist:

  • [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
  • [N/A] 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

Further comments

If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc...

tnyo43 avatar Oct 15 '23 08:10 tnyo43

🦋 Changeset detected

Latest commit: 015cefe3c2c5497ac6c3c7cbc905006de7dcb737

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 Oct 15 '23 08:10 changeset-bot[bot]

Can anyone help me to fix ci problem? https://github.com/dotansimha/graphql-code-generator/actions/runs/6524719251/job/17716636556?pr=9708

tnyo43 avatar Oct 16 '23 13:10 tnyo43

@saihaj Could you please provide some advice on how to approach this? As I mentioned in the description, I know it is kind a breaking change so we may need to discuss a lot to merge.

tnyo43 avatar Feb 23 '24 02:02 tnyo43

Hello,

I wanted to both bump this pr so hopefully @beerose @n1ru4l can take a look.

While this PR or another related get's merged I created this unmasked.ts

type Primitive = string | number | boolean | bigint | symbol | null | undefined;
type ExcludePrimitive<T> = Exclude<T, Primitive>;
type ExtractPrimitive<T> = Exclude<T, Exclude<T, Primitive>>;

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I,
) => void
  ? I
  : never;

type UnionToIntersectGroupByTypeName<U, V = U> = [V] extends [
  { __typename?: infer TypeName },
]
  ? TypeName extends any
    ? UnionToIntersection<U extends { __typename?: TypeName } ? U : never>
    : never
  : never;

type UnionFieldToIntersection<T> = [T] extends [never]
  ? never
  : [T] extends [Array<unknown>]
    ? Array<
        | UnionFieldToIntersection<ExcludePrimitive<T[number]>>
        | ExtractPrimitive<T[number]>
      >
    : UnionToIntersectGroupByTypeName<T> extends infer V
      ? {
          [Key in keyof V]:
            | UnionFieldToIntersection<ExcludePrimitive<V[Key]>>
            | ExtractPrimitive<V[Key]>;
        }
      : never;

type Flatten<F> = [F] extends [never]
  ? never
  : F extends Array<unknown>
    ? Array<Flatten<ExcludePrimitive<F[number]>> | ExtractPrimitive<F[number]>>
    : {
        [Key in keyof Omit<F, " $fragmentRefs" | " $fragmentName">]:
          | Flatten<ExcludePrimitive<F[Key]>>
          | ExtractPrimitive<F[Key]>;
      } & (F extends { " $fragmentRefs"?: { [K in string]: infer FRefs } }
        ? FRefs extends any
          ? Flatten<FRefs>
          : never
        : {});

export type UnmaskFragment<F> = UnionFieldToIntersection<Flatten<F>>;

Example query:

const query = graphql(`
      query GetFolderStructure($sitename: String!) {
        getSite(sitename: $sitename) {
          id
          name
          folders {
            ...FolderFragment
            children {
              ...FolderFragment
              children {
                ...FolderFragment
                children {
                  ...FolderFragment
                  children {
                    ...FolderFragment
                  }
                }
              }
            }
          }
        }
      }
    `);

And used it like:

const result = await execute(query, { sitename });
type UnmaskedResult = UnmaskFragment<NonNullable<typeof result>>;

But it does not work, perhaps this unmasked.ts file is incorrect or I'm using it wrong, I tried searching inside the changed files but there are way too many and not sure which one to use.

adriangalilea avatar Aug 21 '24 09:08 adriangalilea