Skip to content
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

Cannot read/write nested fragments #140

Closed
johannesbraeunig opened this issue Aug 17, 2021 · 26 comments
Closed

Cannot read/write nested fragments #140

johannesbraeunig opened this issue Aug 17, 2021 · 26 comments
Assignees

Comments

@johannesbraeunig
Copy link

johannesbraeunig commented Aug 17, 2021

Versions

Versions

  • @graphql-codegen/add@3.0.0
  • @graphql-codegen/cli@2.0.1
  • @graphql-codegen/near-operation-file-preset@2.0.0
  • @graphql-codegen/plugin-helpers@2.0.0
  • @graphql-codegen/typed-document-node@2.0.0
  • @graphql-codegen/typescript@2.0.0
  • @graphql-codegen/typescript-graphql-files-modules@2.0.0
  • @graphql-codegen/typescript-operations@2.0.1

Describe the bug

In our project, we create a new GraphQL file for each query, mutation, and fragment. Out of that, a types file is generated, including the GraphQL document.

We're making use of the Apollo write/read fragment feature, in order to store some state in the Apollo cache or simply overwrite some data. For this, we use the document of the fragment from the generated types file.

With the latest update, we get an error, that no fragment was found for a specific name. We looked a bit deeper into that and found out, for fragments that import another fragment, the fragment is not included in the respective document.

fragment Zip on Zip {
   zip
}
#import "./zip.fragment.gql"
fragment Address on Address {
   street
   zip {
     ...Zip
   } 
}
import { AddressFragmentDoc } from './address.fragment.gql-types.tsx';

client.writeFragment({
   fragment: AddressFragmentDoc,
   data: {
      // the data
   }
})

This causes the following error: Invariant Violation: No fragment named Zip.

Looking into the changes of our current upgrade PR, we can see that for all fragments that all fragment.definitions assignments to the document got removed, while they been added to the respective query and mutations.

Example:

export const AddressFragmentDoc = {
  kind: 'Document',
  defintions: [
    {
       // the current fragment document defintion
    }
-    ...ZipFragmentDoc.definitions
  ]
}

It seems, by removing the definitions of the imported fragment we lose the information about the fragment.

Questions

  • is this a bug?
  • is there any configuration around this issue?
@johannesbraeunig johannesbraeunig changed the title Apollo can't read/write fragments because of missing fragments Cannot read/write nested fragments Aug 17, 2021
@n1ru4l
Copy link
Collaborator

n1ru4l commented Aug 19, 2021

@johannesbraeunig Can you please provide a reproduction or even better pr with a failing test for this?

@johannesbraeunig
Copy link
Author

johannesbraeunig commented Aug 19, 2021

@n1ru4l I was able to narrow it down to the option dedupeFragments. As soon as this option is enabled the imported fragment document doesn't get included in the fragment, while the import of the document of the imported fragment is still there.

Here is an example:

With dedupeFragment: false:

With dedupeFragment: true:

Bildschirmfoto 2021-08-19 um 12 38 11

This is exactly what happens in our project using dedupeFragment the option: All document definition assignments got deleted, while the import is still there. The respective query using the fragment which imports a fragment does assign all fragments.

This way we cannot use fragments, to read/write data to the cache, that import fragments because they do not contain the respective document definition of the imported fragment.

@kainosnoema
Copy link

I believe this changed in v2, as we got bit by this during the upgrade. Previously reading nested fragments worked fine, even with dedupeFragment enabled.

@whather
Copy link

whather commented Feb 18, 2022

I'm hitting this as well. Is there a fix in sight?

@process0
Copy link

Experiencing this as well

@yusufkandemir
Copy link

With dedupeFragments: false, the server complains due to the same fragment being included multiple times in the same operation. So, we have to use dedupeFragments: true. But, when we use it, the problem explained above happens. I have tried using dedupeFragments: true only in certain plugins such as typescript-operations, but that doesn't work either. I would expect the fragment definitions to be included in fragment docs, but get deduped in a smart way in operation docs.

Here is a really annoying workaround:

cache.writeFragment({
  fragment: {
    ...FooBarFragmentDoc,
    definitions: [
      ...FooBarFragmentDoc.definitions,
      ...NestedFragmentDoc.definitions,
      ...AnotherNestedFragmentDoc.definitions,
      // ... any other fragment that's needed, add more of them as you get errors
    ],
  },
  fragmentName: 'FooBar',
  data: {
    // ...
  }
});
fragment FooBar on FooBar {
  # some fields
  nested {
    ...Nested
  }
}

fragment Nested on SomeType {
  # some fields
  something {
    ...AnotherNested
  }
}

fragment AnotherNested on Something {
  # some fields
}

@adrienharnay
Copy link

adrienharnay commented Jan 4, 2023

Adding search terms for people bumping into this:

With dedupeFragments: false, when performing a query that invokes the same fragment multiple times, you get the message There can only be one fragment named \"fragmentName\"..

With dedupeFragments: true, when performing a readFragment or writeFragment with Apollo, with a fragment that invokes sub-fragments, you get the message No fragment named fragmentName.

There is no fix for either at the moment on the latest versions, only the workaround mentioned by @yusufkandemir (which I can't apply in my codebase as it is too cumbersome).

@adrienharnay
Copy link

Someone from my team (@Titozzz) found a workaround:

In the config:

plugins: [
  {
    add: {
      content:
        '// @ts-ignore\nconst usedFragments: string[] = [];',
    },
  },
]

With a node_modules patcher (yarn 3 patch or patch-package for @graphql-codegen/visitor-plugin-common, /cjs/client-side-base-visitor.js:

-                return `{"kind":"${graphql_1.Kind.DOCUMENT}","definitions":[${definitions}]}`;
+                return `{"kind":"${graphql_1.Kind.DOCUMENT}","definitions":[${definitions}].filter(x => x.kind !== "FragmentDefinition" || !usedFragments.includes(x.name.value) && usedFragments.push(x.name.value))}`;

It might not be safe so use with caution :) Until a real fix can be worked on!

@vemundeldegard
Copy link

vemundeldegard commented Jan 20, 2023

I spent a while wondering why these issues occur. I have the same issues as you @yusufkandemir, but I also stand with @adrienharnay that this is too cumbersome. For now, I will try to avoid reading fragments, which is a shame.

@thexpand
Copy link

thexpand commented Feb 4, 2023

I have the same problem. In my case, it's a fragment nested in another type. When the fragment is used somewhere, it generates a 2-level deeper structure to the type with $fragmentRefs.

So, instead of: data.foo.bar,
the generated type suggests using: data.foo[' $fragmentRefs']?.FooFieldsFragment.bar

which is wrong, because the actual data (in JavaScript) is at data.foo.bar.

So far, I haven't found a workaround. I tried changing the settings for dedupeFragments and inlineFragmentTypes to no avail. Here are my codegen.ts settings just for reference:

import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
    overwrite: true,
    schema: './schema.graphql',
    documents: 'lib/graphql/*.ts',
    generates: {
        'src/__generated__/': {
            preset: 'client',
            plugins: [],
            presetConfig: {
                gqlTagName: 'gql',
            },
        },
    },
    ignoreNoDocuments: true,
};

export default config;

@dotansimha dotansimha transferred this issue from dotansimha/graphql-code-generator Feb 5, 2023
@dkarten
Copy link

dkarten commented Feb 10, 2023

Have just run into this as well. Are there any updates on a forthcoming fix or better workarounds?

@erikwrede
Copy link

I believe the issue with dedupeFragments typed-document-node codegen in general is, that the MyFragmentDocs are re-used in the in the executable OperationDefinitions. That way, it isn't possible to have the fragments include the nested fragments when using dedupeFragments. Instead, the fragments would need to be generated as Standalone Fragments, including the dependencies.

However, it is mentioned in the main repo, that dedupeFragments is deprecated:

https://github.com/dotansimha/graphql-code-generator/blob/6b6fe3cbcc7de748754703adce0f62f3e070a098/packages/plugins/other/visitor-plugin-common/src/base-visitor.ts#L342-L350

Does that mean, that in the future everyone will run into this problem, or is there now another way around dedupeFragments?

@n1ru4l
Copy link
Collaborator

n1ru4l commented Feb 26, 2023

@erikwrede See dotansimha/graphql-code-generator#8971

The dedupeFragments option was broken by design and in the latest release we default to always inline definitions to avoid the limitations.

@erikwrede
Copy link

@n1ru4l thanks for the quick reply! Please excuse the confusion and using this thread to ask, but which version of codegen do I need to use to try this out? I'm on graphql-codegen/cli, but the PR says the release is in graphql-cli/codegen - however, I can't find it in any of the release notes.

@n1ru4l
Copy link
Collaborator

n1ru4l commented Feb 26, 2023

@erikwrede @graphql-codegen/visitor-plugin-common@3.0.1, see the changelog: https://github.com/dotansimha/graphql-code-generator/releases/tag/release-1676995873

@erikwrede
Copy link

Can confirm that this is fixed now, thanks for the help! ☺️

@n1ru4l
Copy link
Collaborator

n1ru4l commented Feb 26, 2023

Cool, I am going to close this issue then. 🥳

@n1ru4l n1ru4l closed this as completed Feb 26, 2023
@adrienharnay
Copy link

Hello @n1ru4l, thank you for the fix that seems to work on my side too (I need to test more). However, it seems fragments are still imported even if they are not used, which results in TS errors. Is this an oversight in the refactoring or something I'm doing wrong?

@erikwrede
Copy link

Interesting, I don't observe this behavior. Have you got an example @adrienharnay?

@adrienharnay
Copy link

Yes @erikwrede , here you go: https://codesandbox.io/s/gql-code-generator-without-dedup-fragments-forked-35lp34?file=/src/user.fragment.generated.tsx

Run yarn codegen and check the user.fragment.generated file and you will see an unused import of AddressFragmentDoc

@n1ru4l
Copy link
Collaborator

n1ru4l commented Feb 28, 2023

@adrienharnay Please open a new issue for this, can you help fixing it?

@adrienharnay
Copy link

Hi @n1ru4l, I just created the issue: #289. I can help fixing it but would need the help of the person who did the refactoring I guess?

@thexpand
Copy link

thexpand commented Mar 3, 2023

I don't see how this is fixed. I am still getting the $fragmentRefs after the update.

@adrienharnay
Copy link

Hi @n1ru4l , I opened a PR: dotansimha/graphql-code-generator#9136

@dwknippers
Copy link

I don't see how this is fixed. I am still getting the $fragmentRefs after the update.

Running into the same issue, seems to be purely typescript related e.g.

(property) IItem:item {
    ' $fragmentRefs'?: {
        ItemFragment: ItemFragment;
    } | undefined;
}
(property) IHeader.item: {
    ' $fragmentRefs'?: {
        ItemHeaderFragment: ItemHeaderFragment;
    } | undefined;
}

Note: the item fragmentRefs should contain the fragment type of the sub header fragment.

This is using client-preset with React useFragment as described in documentation.

A workaround is using an explicit cast e.g. item as FragmentType<typeof ItemHeaderFragment>

@akin-fagbohun
Copy link

akin-fagbohun commented Apr 20, 2024

I was able to work through this a different way to @dwknippers by using the codegen generated useFragment helper (not hook).

Where I was casting as they recommended above with...

const someInaccessibleData = data.someInaccesibleData as SomeInaccessibleFragment

I'm now doing

import { SomeInaccessibleFragmentDoc } from './generated-types-location' // not the Fragment, but the FragmentDoc
import { useFragment } from './generated-fragment-masking-location'

...

const someInaccessibleData = useFragment(SomeInaccessibleFragmentDoc, data.someInaccessibleData)

Your linter might then complain about what appears to be a use hook, however codegen have notes on this within their docs and provide a work-around here

After making the recommended change to the useFragment naming convention, I end up with const someInaccessibleData = getFragmentData(SomeInaccessibleFragmentDoc, data.someInaccessibleData) and have access to the types from the fragment where they were previously out of reach.

@thexpand tagging you because my problem was exactly the same as yours.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests