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

Fragment definitions within a graphql document ought to be unique. #27

Closed
jnwng opened this issue Dec 7, 2016 · 7 comments · Fixed by #52
Closed

Fragment definitions within a graphql document ought to be unique. #27

jnwng opened this issue Dec 7, 2016 · 7 comments · Fixed by #52

Comments

@jnwng
Copy link
Contributor

jnwng commented Dec 7, 2016

This is a rehash of apollo-client#906, introduced again because the new fragment interpolation pattern doesn't go through the addFragmentsToDocument method that we fixed in apollo-client#913 to remove duplicate fragments.

While a recent change in graphql-tag no longer "warns" when duplicate fragments are in a document, we should probably strip the duplicate fragment from the document altogether in either graphql-tag or apollo-client since this will through errors when getting parsed on the server. Optionally we could consider stripping duplicate fragment definitions from the document altogether; although in the aforementioned commit, @tmeasday says

Note that we are simply checking fragment strings for equality, not ensuring that fragment definitions within the larger graphql document have exact equality. We could do the above, but it'd be quite a bit more complicated, and I'm not sure if there's much benefit. AFAICT the main reason for wanting that before was exactly this functionality.

Steps to Reproduce

Interpolating multiple fragments into the same document generates an invalid GraphQL document:

query {
   ...SomeFragment
}
${SomeFragment}
${SomeFragment}

generates a document like

query {
  ...SomeFragment
}
fragment SomeFragment on SomeResource {
  field
}
fragment SomeFragment on SomeResource {
  field
}

which is invalid (and a server may choke on an invalid GraphQL doc).

I filed this here instead of in apollo-client as the related commit I mentioned above asks why there'd be a benefit to de-duplicating fragments within the document, but this happens more often when the same fragment gets interpolated into the same document twice from different places in the query, not necessarily that the fragment itself is already defined in the document.

jnwng added a commit to jnwng/graphql-tag that referenced this issue Dec 7, 2016
While duplicate fragments will result in “warnings”, generating a document with duplicate fragments results in an invalid GraphQL document. This was recently introduced when fragment interpolation became the defacto way to insert fragments.

See upstream issue [here](apollographql#27). This is not intended to go upstream (yet).
@tmeasday
Copy link
Contributor

tmeasday commented Dec 7, 2016

Hi @jnwng, thanks for the report and you are right that this functionality probably does belong in graphql-tag.

The referenced commit wasn't actually considering the issue you've outlined here; it was simply referring to the original "ensure the user doesn't try and define two fragments with the same name" issue.

Probably the simplest approach to deal with this would be to just run over the generated AST and remove any duplicate fragments, so the reproduction/test case would be even simpler:

gql`
query {
  ...SomeFragment
}
fragment SomeFragment on SomeResource {
  field
}
fragment SomeFragment on SomeResource {
  field
}
` === gql`
query {
  ...SomeFragment
}
fragment SomeFragment on SomeResource {
  field
}
`

@tmeasday
Copy link
Contributor

tmeasday commented Dec 7, 2016

Rather than doing an equality check on the FragmentDefinitions, we would just check the names for equality.

@tmeasday
Copy link
Contributor

tmeasday commented Dec 7, 2016

Hmm, the tricky part about making the above give exact equality is that cache keys by input doc string and we don't really want to be messing around with that if we can help it. @stubailo do you think it is required that the two input strings above would output the literal same AST document?

@stubailo
Copy link
Contributor

stubailo commented Dec 7, 2016

No I don't think that's required. But they should be semanticallly equal probably.

@czert
Copy link
Contributor

czert commented Jan 25, 2017

This is still an issue when using graphql-tag/loader and the new #import feature. If a query uses two fragments, both of which #import a third fragment, this third fragment will end up duplicated in the resulting document.

I saw that this was mentioned in PR #28. If you can point me in the right direction, I'd be happy to work on deduplication in the webpack loader.

@smitt04
Copy link

smitt04 commented Jan 30, 2017

So is the loader issue being worked on?

@jameswyse
Copy link

jameswyse commented Feb 3, 2017

I ran in to this issue as well and threw together a quick workaround:

// schema/utils.js
const requireFragment = require.context('schema/fragments', true, /\.(graphql|gql)$/);
const requireQuery = require.context('schema/queries', true, /\.(graphql|gql)$/);
const requireMutation = require.context('schema/mutations', true, /\.(graphql|gql)$/);

export function removeDuplicateFragments (doc) {
  const usedFragments = {};

  doc.definitions = doc.definitions.filter(def => {
    const included = def.kind !== 'FragmentDefinition' || !usedFragments[def.name.value];
    if (included) {
      usedFragments[def.name.value] = true;
    }
    return included;
  });

  return doc;
}

export function getQuery (name) {
  const doc = requireQuery(`./${name}.gql`);
  return removeDuplicateFragments(doc);
}

export function getFragment (name) {
  const doc = requireFragment(`./${name}.gql`);
  return removeDuplicateFragments(doc);
}

export function getMutation (name) {
  const doc = requireMutation(`./${name}.gql`);
  return removeDuplicateFragments(doc);
}

With a directory structure like:

schema
├── utils.js
├── fragments
│   ├── foo.gql
│   ├── bar.gql
│   ├── baz.gql
├── mutations
│   ├── createFoo.gql
│   ├── createBar.gql
│   ├── createBaz.gql
├── queries
│   ├── someQuery.gql
│   ├── anotherQuery.gql

I can then do @graphql(getQuery('someQuery')) instead of importing directly. Hope it helps :)

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

Successfully merging a pull request may close this issue.

6 participants