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

[AIC] Implement explicit fragments as feature #6

Merged
merged 3 commits into from
Jul 1, 2024

Conversation

liontariai
Copy link
Owner

Example usage:

import unions, { ArticleSelection } from "./unions2";

function titleOnly(this: any) {
    return ArticleSelection.bind(this)((s) => ({
        titleFromFragment: s.title,
    }));
}

const { op1 } = await unions((op) => ({
    op1: op.query((s) => ({
        b: s.books((s) => ({
            ...titleOnly(),
        })),
        a: s.articles((s) => ({
            ...s.$fragment(titleOnly),
        })),
        all: s.search(({ $on }) => ({
            ...$on.Book((s) => ({
                ...s.$scalars(),
            })),
            ...$on.Article((s) => ({
                ...s.$scalars(),
            })),
        })),
    })),
}));

// all books, with inline fragment
console.log(op1.b.map((b) => b.titleFromFragment));
// all articles, with named fragment (titleOnly) in query
console.log(op1.a);
// all books and articles, with union type and inline fragments
console.log(op1.all);

output:

[
    {
        query: `
        fragment titleOnly on Article { titleFromFragment: title  }
        
        query op1  {
            b: books {
                titleFromFragment: title
            }
            a: articles {
                ...titleOnly 
            }
            all: search {
                ... on Book { title author  }
                ... on Article { title publisher  }
            } 
        }
        `,
        variables: {},
    },
    {
        data: {
            b: [
                { titleFromFragment: "The Awakening" },
                { titleFromFragment: "City of Glass" },
            ],
            a: [
                { titleFromFragment: "GraphQL is awesome" },
                { titleFromFragment: "REST is dead" },
            ],
            all: [
                { title: "The Awakening", author: "Kate Chopin" },
                { title: "City of Glass", author: "Paul Auster" },
                { title: "GraphQL is awesome", publisher: "Apollo" },
                { title: "REST is dead", publisher: "Medium" },
            ],
        },
    },
    ["The Awakening", "City of Glass"],
    [
        {
            titleFromFragment: "GraphQL is awesome",
        },
        {
            titleFromFragment: "REST is dead",
        },
    ],
    [
        {
            title: "The Awakening",
            author: "Kate Chopin",
        },
        {
            title: "City of Glass",
            author: "Paul Auster",
        },
        {
            title: "GraphQL is awesome",
            publisher: "Apollo",
        },
        {
            title: "REST is dead",
            publisher: "Medium",
        },
    ],
];

Implicit Inline Fragments

The exported "*Selection" functions can be used as implicit inline fragments from anywhere.
It can be used and constructed anywhere outside the operation and then object-spread into the selection.
However, there is not typesafety for this right now. As one can see, the ArticlesSelection fragment also works on the Books type, because it also defines a title field.

Explicitly defined query fragments

When used extensively in the Query, it may be useful to use real GraphQL fragments.
This can be done using the $fragment helper selection function and a separately defined fragment function.

function titleOnly is such a function. Please note, that it is defined as function and not an arrow function, because it needs to have it's own scope with this. Also, you need to pass the this via bind to the *Selection function, so that it get's registered as named fragment later on.

Possible Usages

Apart from these requirements, you can do whatever you want in your custom fragment functions, opening possibilities to conditional and parameterized fragments (similar to what is being discussed here: graphql/graphql-spec#204 )
In case of named parameterized fragments, I have not yet tested what the generated query will look like when there're arguments in the selection. It should collect and hoist them to variables in the query but still reference them in the fragment. This might not be valid gql, but you can still use Implicit Inline Fragments, so you have the convenience of fragments being defined once as code and have them generate the wanted gql code based on your input.

Liontari added 3 commits June 30, 2024 14:45
in case parameterized fragment is used multiple times with different argument values, it needs to reference the correct variable
@liontariai
Copy link
Owner Author

Apparently it is valid GQL to use variables defined in the query/mutation in the fragment snippet:
https://graphql.org/learn/queries/#using-variables-inside-fragments

Therefore the generated GraphQL code is valid and also with 6080e1a now supports automatically resolving conflicts, in case the fragment is used multiple times and the variable is set to different values for each fragment. In this case multiple fragments are needed, so that the right variable is referenced.

import unions, { ArticleSelection } from "./unions2";

function titleOnly(this: any, language?: string) {
    return ArticleSelection.bind(this)((s) => ({
        titleFromFragment: s.title,
        books: s.books({
            language,
        })((s) => ({
            ...s.$scalars(),
        })),
    }));
}

const { op1 } = await unions((op) => ({
    op1: op.query((s) => ({
        b: s.books((s) => ({
            title: s.title,
        })),
        a_DE: s.articles((s) => ({
            ...s.$fragment(titleOnly)("de"),
        })),
        a_EN: s.articles((s) => ({
            ...s.$fragment(titleOnly)("en"),
        })),
        all: s.search(({ $on }) => ({
            ...$on.Book((s) => ({
                ...s.$scalars(),
            })),
            ...$on.Article((s) => ({
                ...s.$scalars(),
            })),
        })),
    })),
}));

generates:

fragment titleOnly_language on Article {
  titleFromFragment: title
  books: books(language: $language) {
    title
    author
  }
}
fragment titleOnly_language_1 on Article {
  titleFromFragment: title
  books: books(language: $language_1) {
    title
    author
  }
}

query op1($language: String, $language_1: String) {
  b: books {
    title
  }
  a_DE: articles {
    ...titleOnly_language
  }
  a_EN: articles {
    ...titleOnly_language_1
  }
  all: search {
    ... on Book {
      title
      author
    }
  }
}
variables: {
    language: "de",
    language_1: "en"
}

and results in:

[
  {
    "titleFromFragment": "GraphQL is awesome",
    "books": [
      {
        "title": "The Awakening (translated to de)",
        "author": "Kate Chopin"
      },
      {
        "title": "City of Glass (translated to de)",
        "author": "Paul Auster"
      }
    ]
  },
  {
    "titleFromFragment": "REST is dead",
    "books": [
      {
        "title": "The Awakening (translated to de)",
        "author": "Kate Chopin"
      },
      {
        "title": "City of Glass (translated to de)",
        "author": "Paul Auster"
      }
    ]
  }
]
[
  {
    "titleFromFragment": "GraphQL is awesome",
    "books": [
      {
        "title": "The Awakening",
        "author": "Kate Chopin"
      },
      {
        "title": "City of Glass",
        "author": "Paul Auster"
      }
    ]
  },
  {
    "titleFromFragment": "REST is dead",
    "books": [
      {
        "title": "The Awakening",
        "author": "Kate Chopin"
      },
      {
        "title": "City of Glass",
        "author": "Paul Auster"
      }
    ]
  }
]
[
  {
    title: "The Awakening",
    author: "Kate Chopin",
  }, {
    title: "City of Glass",
    author: "Paul Auster",
  }, {
    title: "GraphQL is awesome",
    publisher: "Apollo",
  }, {
    title: "REST is dead",
    publisher: "Medium",
  }
]

Therefore "Parameterized Fragments" (graphql/graphql-spec#204) is correctly implemented with this PR

@liontariai liontariai added the enhancement New feature or request label Jul 1, 2024
@liontariai liontariai merged commit ce14b9e into main Jul 1, 2024
@liontariai liontariai deleted the AIC-26-GraphQL-Fragments-support branch July 6, 2024 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant