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

Document GCG Codegen for GraphQL operation and fragment tags #1221

Closed
kitten opened this issue Dec 10, 2020 · 9 comments
Closed

Document GCG Codegen for GraphQL operation and fragment tags #1221

kitten opened this issue Dec 10, 2020 · 9 comments
Labels
documentation 📖 This needs to be documented but won't need code changes hold ⚠️ This issue is accepted but no contributor is available yet or prerequisites must be fulfilled first.

Comments

@kitten
Copy link
Member

kitten commented Dec 10, 2020

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

@kitten kitten added the future 🔮 An enhancement or feature proposal that will be addressed after the next release label Dec 10, 2020
@kitten kitten added the hold ⚠️ This issue is accepted but no contributor is available yet or prerequisites must be fulfilled first. label Mar 25, 2021
@n1ru4l
Copy link
Contributor

n1ru4l commented Jul 14, 2021

We have some progress on generating inline types: dotansimha/graphql-code-generator#6267

@n1ru4l
Copy link
Contributor

n1ru4l commented Aug 7, 2021

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: dotansimha/graphql-code-generator#6442

@maraisr
Copy link
Contributor

maraisr commented Aug 7, 2021

Such a good thingy this! Thanks so much for the hard yards @n1ru4l 👌🏻

@JoviDeCroock
Copy link
Collaborator

JoviDeCroock commented Aug 9, 2021

@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?

@n1ru4l
Copy link
Contributor

n1ru4l commented Aug 9, 2021

@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;
};

@n1ru4l
Copy link
Contributor

n1ru4l commented Aug 17, 2021

@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'

Link

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;
}

Link

See n1ru4l/character-overlay#339 for a usage example in a small app.

Full PR is over here: dotansimha/graphql-code-generator#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 🤔

@kitten kitten removed the hold ⚠️ This issue is accepted but no contributor is available yet or prerequisites must be fulfilled first. label Aug 17, 2021
@n1ru4l
Copy link
Contributor

n1ru4l commented Sep 10, 2021

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 of gql exported from urql via module augmentation
    • gql/graphql.ts: Contains generated TypedDocumentNodes
  • Use exported gql from urql and module augmentation for inferring the correct types from the gql/index.d.ts file
  • Babel plugin replaces gql usages with actual TypedDocumentNode imports located in the gql/graphql.ts artifact 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: n1ru4l/character-overlay@52a4c8e#diff-6a3b01ba97829c9566ef2d8dc466ffcffb4bdac08706d3d6319e42e0aa6890ddR29-R57

@dotansimha
Copy link
Contributor

dotansimha commented Dec 9, 2021

Up next: a data/fragment matching support for this codegen preset ;) dotansimha/graphql-code-generator#6442

@kitten kitten added documentation 📖 This needs to be documented but won't need code changes and removed future 🔮 An enhancement or feature proposal that will be addressed after the next release labels Feb 16, 2022
@kitten kitten changed the title RFC: GCG Codegen for GraphQL operation and fragment tags Document GCG Codegen for GraphQL operation and fragment tags Feb 16, 2022
@JoviDeCroock
Copy link
Collaborator

JoviDeCroock commented Jun 14, 2022

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?

@kitten kitten added the hold ⚠️ This issue is accepted but no contributor is available yet or prerequisites must be fulfilled first. label Mar 19, 2023
@kitten kitten closed this as completed Apr 18, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation 📖 This needs to be documented but won't need code changes hold ⚠️ This issue is accepted but no contributor is available yet or prerequisites must be fulfilled first.
Projects
None yet
Development

No branches or pull requests

5 participants