liontariai / samarium

https://liontari.ai/
Apache License 2.0
12 stars 0 forks source link

[AIC] Implement explicit fragments as feature #6

Closed liontariai closed 2 months ago

liontariai commented 2 months ago

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: https://github.com/graphql/graphql-spec/issues/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.

liontariai commented 2 months ago

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" (https://github.com/graphql/graphql-spec/issues/204) is correctly implemented with this PR