Document GCG Codegen for GraphQL operation and fragment tags
Summary
This is connected to #901 in that we'd like to expand what is done with GraphQL Code Generator.
The basic idea is to have code generation that can keep up with Relay so that we enable and encourage better fragment best practices and enable a better control of the operation/fragment pipeline in the future. For now the goal is to be able to write queries with gql tags, interpolate fragments into them from other files (also written with gql tags), and to expose all gql tags A) with types somehow, and B) have separate __generated files with a large combined operation.
This encourages to define data requirements in the form of fragments in several places, and it actually creates a very compelling end to end experience with urql, where it becomes more Relay/Framework-like and encourages that paradigm. (Together with #964 this will be really strong)
Proposed Solution
TBD (This is still a placeholder and no proposed implementation path exists yet)
Requirements
TBD
cc @JoviDeCroock @dotansimha
We have some progress on generating inline types: https://github.com/dotansimha/graphql-code-generator/pull/6267
The gql-tag-operations preset is now available: https://www.graphql-code-generator.com/docs/presets/gql-tag-operations
Also started a fragment type masking experiment: https://github.com/dotansimha/graphql-code-generator/pull/6442
Such a good thingy this! Thanks so much for the hard yards @n1ru4l 👌🏻
@n1ru4l is there a way we can use the gql operator exported from urql? Started a bit of an experiment a while back here.
Maybe an options to prefix the documents array with your own gql function?
@JoviDeCroock Should be possible with module augmentation/declaration merging:
I quickly generated the contents of the index.ts file to the following:
/* eslint-disable */
import * as graphql from './graphql';
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
const documents = {
'\n query Foo {\n Tweets {\n id\n }\n }\n': graphql.FooDocument,
'\n fragment Lel on Tweet {\n id\n body\n }\n': graphql.LelFragmentDoc,
'\n query Bar {\n Tweets {\n ...Lel\n }\n }\n': graphql.BarDocument,
};
// export function gql(source: string): unknown;
// export function gql(source: string) {
// return (documents as any)[source] ?? {};
// }
export type DocumentType<TDocumentNode extends DocumentNode<any, any>> = TDocumentNode extends DocumentNode<
infer TType,
any
>
? TType
: never;
declare module '@urql/core' {
export function gql(
source: '\n query Foo {\n Tweets {\n id\n }\n }\n'
): typeof documents['\n query Foo {\n Tweets {\n id\n }\n }\n'];
export function gql(
source: '\n fragment Lel on Tweet {\n id\n body\n }\n'
): typeof documents['\n fragment Lel on Tweet {\n id\n body\n }\n'];
export function gql(
source: '\n query Bar {\n Tweets {\n ...Lel\n }\n }\n'
): typeof documents['\n query Bar {\n Tweets {\n ...Lel\n }\n }\n'];
}
Which worked instantly
/* eslint-disable @typescript-eslint/no-unused-vars */
import { gql } from "urql"
import { DocumentType } from '../gql';
const FooQuery = gql(/* GraphQL */ `
query Foo {
Tweets {
id
}
}
`);
const LelFragment = gql(/* GraphQL */ `
fragment Lel on Tweet {
id
body
}
`);
const BarQuery = gql(/* GraphQL */ `
query Bar {
Tweets {
...Lel
}
}
`);
const doSth = (params: { lel: DocumentType<typeof LelFragment> }) => {
params.lel.id;
};
@JoviDeCroock I adjusted the preset! Shoutouts to @PabloSzx for helping me with this. 🚀
It is now possible to provide an augmentedModuleName config option for the preset.
./dev-test/gql-tag-operations-urql/gql:
schema: ./dev-test/gql-tag-operations-urql/schema.graphql
documents: './dev-test/gql-tag-operations-urql/src/**/*.ts'
preset: gql-tag-operations-preset
presetConfig:
augmentedModuleName: '@urql/core'
That allows writing this code:
/* eslint-disable @typescript-eslint/no-unused-vars */
import { gql } from 'urql';
const FooQuery = gql(/* GraphQL */ `
query Foo {
Tweets {
id
}
}
`);
const LelFragment = gql(/* GraphQL */ `
fragment Lel on Tweet {
id
body
}
`);
const BarQuery = gql(/* GraphQL */ `
query Bar {
Tweets {
...Lel
}
}
`);
Which will generate an index.d.ts file:
/* eslint-disable */
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
declare module '@urql/core' {
export function gql(
source: '\n query Foo {\n Tweets {\n id\n }\n }\n'
): typeof import('./graphql').FooDocument;
export function gql(
source: '\n fragment Lel on Tweet {\n id\n body\n }\n'
): typeof import('./graphql').LelFragmentDoc;
export function gql(
source: '\n query Bar {\n Tweets {\n ...Lel\n }\n }\n'
): typeof import('./graphql').BarDocument;
export function gql(source: string): unknown;
export type DocumentType<TDocumentNode extends DocumentNode<any, any>> = TDocumentNode extends DocumentNode<
infer TType,
any
>
? TType
: never;
}
See https://github.com/n1ru4l/character-overlay/pull/339 for a usage example in a small app.
Full PR is over here: https://github.com/dotansimha/graphql-code-generator/pull/6492
This approach does not increase the application bundle size as all the codegen artifacts are only used for module augmentation during development 🥳
EDIT:
This still would require minor adjustment within urql to work. The gql tag function must be altered so it is able to automatically add global fragment definitions to query/mutation/subscriptions documents before sending them to the server, as we cannot spread them in the operation (would destroy the string literal typings).
We can infer type:
const CharacterQuery = gql(/* GraphQL */ `
query CharacterQuery($characterId: ID!) @live {
character(id: $characterId) {
id
...CharacterViewFragment
}
}
`);
We cannot infer the type :(
const CharacterQuery = gql(/* GraphQL */ `
query CharacterQuery($characterId: ID!) @live {
character(id: $characterId) {
id
...CharacterViewFragment
}
}
${CharacterViewFragment}
`);
Maybe there might be some hacks that are possible.
I tried altering the existing gql function, to have a global cache from where it tries to get the fragments and add them to the parsed document. However, this is flawed in scenarios where a fragment has not been registered yet (e.g. lazy-loaded code 🤔 ).
const globalCache = new Map();
function gql() {
var e = arguments;
var n = new Map;
var a = [];
var o = [];
var i = Array.isArray(arguments[0]) ? arguments[0][0] : arguments[0] || "";
for (var u = 1; u < arguments.length; u++) {
var c = e[u];
if (c && c.definitions) {
o.push.apply(o, c.definitions);
} else {
i += c;
}
i += e[0][u];
}
applyDefinitions(n, a, t(i).definitions);
applyDefinitions(n, a, o);
const document = t({
kind: r.DOCUMENT,
definitions: a
});
// I added this block
if (document.definitions[0].kind === "FragmentDefinition") {
console.log("register fragment", document.definitions[0].name.value)
globalCache.set(document.definitions[0].name.value, document.definitions[0])
} else {
const visit = (selectionSet) => {
for (const selection of selectionSet.selections) {
if ((selection.kind === "Field" || selection.kind === "InlineFragment") && selection.selectionSet !== undefined) {
visit(selection.selectionSet)
} else if (selection.kind === "FragmentSpread") {
console.log("add fragment", selection.name.value, globalCache.get(selection.name.value))
const fragmentDefinition = globalCache.get(selection.name.value)
document.definitions.push(fragmentDefinition)
visit(fragmentDefinition.selectionSet)
}
}
}
visit(document.definitions[0].selectionSet)
}
return document
}
Another possible solution would be the usage of a babel plugin, that fixes this during transpilation 🤔
I madesome progress on a babel plugin that rewrites gql tag statements to TypedDocumentNode imports from the generated artifacts.
The usage flow is the following:
- Run codegen in watch mode for generating these files:
gql/index.d.ts: Overrides function signature ofgqlexported from urql via module augmentationgql/graphql.ts: Contains generated TypedDocumentNodes
- Use exported
gqlfrom urql and module augmentation for inferring the correct types from thegql/index.d.tsfile - Babel plugin replaces
gqlusages with actual TypedDocumentNode imports located in thegql/graphql.tsartifact from graphql-codegen, resolving the unreferenced fragment issue (due to referencing global fragments; fragment string interpolation would break type inference)
References
Babel Plugin Code: https://github.com/dotansimha/graphql-code-generator/pull/6492/files?file-filters%5B%5D=.lock&file-filters%5B%5D=.md&file-filters%5B%5D=.ts&file-filters%5B%5D=.yml#diff-253be75542b9e0e3e80021c380f34c220a00466081f7f236d14336e169996a98
Example babel plugin usage setup with vitejs: https://github.com/n1ru4l/character-overlay/commit/52a4c8ec94cc42a2a8b6f726986c9238fa4ae152#diff-6a3b01ba97829c9566ef2d8dc466ffcffb4bdac08706d3d6319e42e0aa6890ddR29-R57
Up next: a data/fragment matching support for this codegen preset ;) https://github.com/dotansimha/graphql-code-generator/pull/6442
Coming back to this we can leverage the GCG near operation file config to advise people on creating the static-typings, ... however at the end of the day we still have this two-way street where we would require people to include the fragments in their queries, we could tweak urql to look for FragmentSpread in queries before sending them and automatically include the definitions from a cache we keep in the gql helper, WDYT about that?