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

misbehaving spreading fragments #166

Closed
fjmoreno opened this issue Mar 14, 2022 · 3 comments · Fixed by #197
Closed

misbehaving spreading fragments #166

fjmoreno opened this issue Mar 14, 2022 · 3 comments · Fixed by #197
Assignees
Labels
bug Something isn't working

Comments

@fjmoreno
Copy link
Contributor

Given Schema:

type Query {
  detailContent: [Content!]
}


type Post implements Content {
  id: ID!
  title: String!
  type: String!
  related: [Content]
}

type Article implements Content {
  id: ID!
  title: String!
  type: String!
}

interface Content {
  id: ID!
  title: String!
  type: String!
}

and query:

query TEST(
    $includeOnly: Boolean!
){
  detailContent{
    ...articleFragment
    ...on Post {
      ...postFragment 
      related{
          id
  	  ...articleFragment @include(if:$includeOnly)
    	}
    }
  }
}

fragment postFragment on Post {
  id
  title
}

fragment articleFragment on Article {
title
  type
}

Expect:

{
  "data": {
    "detailContent": [
      {
        "id": "post:1",
        "title": "Introduction to GraphQL!",
        "related": [
          {
            "id": "article:1",
          }
        ]
      },
      {
        "id": "post:2",
        "title": "GraphQL-Jit a fast engine for GraphQL",
        "related": [
          {
            "id": "article:2",
          }
        ]
      },
      {
        "title": "article Introduction to GraphQL!",
        "type": "article"
      },
      {
        "title": "article GraphQL-Jit a fast engine for GraphQL",
        "type": "article"
      }
    ]
  }
}

But receive:

{
  "data": {
    "detailContent": [
      {
        "id": "post:1",
        "title": "Introduction to GraphQL!",
        "related": [
          {}
        ]
      },
      {
        "id": "post:2",
        "title": "GraphQL-Jit a fast engine for GraphQL",
        "related": [
          {}
        ]
      },
      {},
      {}
    ]
  }
}
@oporkka
Copy link
Member

oporkka commented Mar 14, 2022

@fjmoreno are you planning to look into a fix yourself? Or would it be feasible for you to provide a failing test case?

@ayruslore
Copy link

After debugging the issue, we found that the issue is actually due to the fragment name. When the same fragment is applied on different levels and one has directive, the directive some how gets applied to both.

To be precise the include directive on the articleFragment spread gets applied both times, but should only be applied once.

To prove this, change the name of the fragment and we get the expected result. .i.e., For the same schema, change the query to

query TEST($includeOnly: Boolean!) {
      someQuery {
        ...bFrag
        ...on A {
          id
          related {
            id
            ...bFrag1 @include(if: $includeOnly)
          }
        }
      }
    }

    fragment bFrag on B {
      title
    }

    fragment bFrag1 on B {
      title
    }

Then Result is :

{
  "data": {
    "detailContent": [
      {
        "id": "post:1",
        "title": "Introduction to GraphQL!",
        "related": [
          {
            "id": "article:1",
          }
        ]
      },
      {
        "id": "post:2",
        "title": "GraphQL-Jit a fast engine for GraphQL",
        "related": [
          {
            "id": "article:2",
          }
        ]
      },
      {
        "title": "article Introduction to GraphQL!",
        "type": "article"
      },
      {
        "title": "article GraphQL-Jit a fast engine for GraphQL",
        "type": "article"
      }
    ]
  }
}

So the issue is misbehaving fragment spread in presence of @skip / @include directive

@ayruslore
Copy link

Please find the tests based on @fjmoreno 's example and a minimal reproduction of the bug described.

import { GraphQLSchema, parse } from "graphql";
import { compileQuery, isCompiledQuery } from "../index";
import { makeExecutableSchema } from "@graphql-tools/schema";

const data = {};

function executeTestQuery(
  query: string,
  variables = {},
  schema: GraphQLSchema
) {
  const ast = parse(query);
  const compiled: any = compileQuery(schema, ast, "", { debug: true } as any);
  if (!isCompiledQuery(compiled)) {
    return compiled;
  }
  return compiled.query(data, undefined, variables);
}

describe("actual example from user", () => {
  const testSchema = makeExecutableSchema({
    typeDefs: `type Query {
    detailContent: [Content!]
    }

    type Post implements Content {
        id: ID!
        title: String!
        type: String!
        related: [Content]
    }

    type Article implements Content {
        id: ID!
        title: String!
        type: String!
    }

    interface Content {
        id: ID!
        title: String!
        type: String!
    }`,
    resolvers: {
      Query: {
        detailContent: () => [
          {
            __typename: "Post",
            id: "post:1",
            title: "Introduction to GraphQL!",
            related: [
              {
                __typename: "Article",
                id: "article:1",
                title: "article Introduction to GraphQL!",
                type: "article"
              }
            ]
          },
          {
            __typename: "Post",
            id: "post:2",
            title: "GraphQL-Jit a fast engine for GraphQL",
            related: [
              {
                __typename: "Article",
                id: "article:2",
                title: "article GraphQL-Jit a fast engine for GraphQL",
                type: "article"
              }
            ]
          },
          {
            __typename: "Article",
            id: "article:1",
            title: "article Introduction to GraphQL!",
            type: "article"
          },
          {
            __typename: "Article",
            id: "article:2",
            title: "article GraphQL-Jit a fast engine for GraphQL",
            type: "article"
          }
        ]
      }
    }
  });

  test("spreads misbehaving", async () => {
    const query = `query TEST(
      $includeOnly: Boolean!
      ){
      detailContent{
        ...articleFragment
        ...on Post {
          ...postFragment
          related{
              id
          ...articleFragment1 @include(if:$includeOnly)
          }
        }
      }
      }

      fragment postFragment on Post {
      id
      title
      }

      fragment articleFragment on Article {
      title
      type
      }

      fragment articleFragment1 on Article {
        title
        type
        }`;
    const result = executeTestQuery(query, { includeOnly: false }, testSchema);
    console.log(JSON.stringify(result.data, null, 2));
  });

  test("inline behaving correctly", async () => {
    const query = `query TEST(
      $includeOnly: Boolean!
      ){
      detailContent{
        ...on Article {
          title
          type
        }
        ...on Post {
          ...postFragment
          related{
              id
          ...articleFragment @include(if:$includeOnly)
          }
        }
      }
      }

      fragment postFragment on Post {
      id
      title
      }

      fragment articleFragment on Article {
      title
      type
      }`;
    const result = executeTestQuery(query, { includeOnly: false }, testSchema);
    console.log(JSON.stringify(result.data, null, 2));
  });
});

describe("reproduce minimally", () => {
  const schema = makeExecutableSchema({
    typeDefs: `type Query {
      someQuery: [X!]
    }

    interface X {
      id: ID!
      title: String!
    }

    type A implements X {
      id: ID!
      title: String!
      related: [X]
    }

    type B implements X {
      id: ID!
      title: String!
    }
    `,
    resolvers: {
      Query: {
        someQuery: () => [
          {
            __typename: "A",
            id: "a:1",
            title: "a - one",
            related: [
              {
                __typename: "B",
                id: "b:1",
                title: "b - one"
              }
            ]
          },
          {
            __typename: "B",
            id: "b:1",
            title: "b - one"
          }
        ]
      }
    }
  });

  // test passes
  test("testing success with include directive", async () => {
    const query = `query TEST($includeOnly: Boolean!) {
      someQuery {
        ...bFrag
        ...on A {
          id
          related {
            id
            ...bFrag1 @include(if: $includeOnly)
          }
        }
      }
    }

    fragment bFrag on B {
      title
    }

    fragment bFrag1 on B {
      title
    }`;
    const result = executeTestQuery(query, { includeOnly: false }, schema);
    console.log(JSON.stringify(result.data, null, 2));
    expect(result.data).toEqual({
      someQuery: [
        {
          id: "a:1",
          related: [
            {
              id: "b:1"
            }
          ]
        },
        {
          title: "b - one"
        }
      ]
    });
  });

  // test fails
  test("testing the actual issue with include directive", async () => {
    const query = `query TEST($includeOnly: Boolean!) {
      someQuery {
        ...bFrag
        ...on A {
          id
          related {
            id
            ...bFrag @include(if: $includeOnly)
          }
        }
      }
    }

    fragment bFrag on B {
      title
    }`;
    const result = executeTestQuery(query, { includeOnly: false }, schema);
    console.log(JSON.stringify(result.data, null, 2));
    expect(result.data).toEqual({
      someQuery: [
        {
          id: "a:1",
          related: [
            {
              id: "b:1"
            }
          ]
        },
        {
          title: "b - one"
        }
      ]
    });
  });

  // test passes
  test("testing success with skip directive", async () => {
    const query = `query TEST($skipOnly: Boolean!) {
      someQuery {
        ...bFrag
        ...on A {
          id
          related {
            id
            ...bFrag1 @skip(if: $skipOnly)
          }
        }
      }
    }

    fragment bFrag on B {
      title
    }

    fragment bFrag1 on B {
      title
    }`;
    const result = executeTestQuery(query, { skipOnly: true }, schema);
    console.log(JSON.stringify(result.data, null, 2));
    expect(result.data).toEqual({
      someQuery: [
        {
          id: "a:1",
          related: [
            {
              id: "b:1"
            }
          ]
        },
        {
          title: "b - one"
        }
      ]
    });
  });

  // test fails
  test("testing the actual issue with skip directive", async () => {
    const query = `query TEST($includeOnly: Boolean!) {
      someQuery {
        ...bFrag
        ...on A {
          id
          related {
            id
            ...bFrag @skip(if: $skipOnly)
          }
        }
      }
    }

    fragment bFrag on B {
      title
    }`;
    const result = executeTestQuery(query, { skipOnly: true }, schema);
    console.log(JSON.stringify(result.data, null, 2));
    expect(result.data).toEqual({
      someQuery: [
        {
          id: "a:1",
          related: [
            {
              id: "b:1"
            }
          ]
        },
        {
          title: "b - one"
        }
      ]
    });
  });
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants