heftapp / graphql_codegen

MIT License
142 stars 53 forks source link

Feature Request: Generate types from GraphQL in Dart files #130

Open baconcheese113 opened 2 years ago

baconcheese113 commented 2 years ago

As discussed in #123

Do you have plans to allow Queries/Fragments to be defined in dart files? Can you elaborate about where the challenge is here and how it might be approached?

Response

I've considered it, it amounts to analyzing the dart files and come up with a way to link the generated queries to the right methods/widgets. It is doable, but will be different than Relay because of the difference between the TS/Flow type system and Darts typesystem. Not unmanageable, but not started.

While using graphql_flutter without type generation from Artemis, I would define my queries/fragments in the same file as the widget that was planned to use them. This made for a great developer experience for several reasons: 1) Easy upkeep/refactoring: can use Ctrl+F to Ctrl+D to quickly check if variables are still used or rename them 2) Less hierarchy bloat: If a widget in home.dart had both a query and a mutation, it would go from 1 file to 6 when switched to use graphql_codegen... home.dart home.query.graphql home.query.graphql.dart home.query.graphql.g.dart home.mutation.graphql home.mutation.graphql.dart home.mutation.graphql.g.dart The generated files are fine, because they don't require much manual work and can be ignored in a __generated__ folder easily, but needing to create several .graphql files for each widget quickly adds up 3) Easier to drill into dependencies: Since importing the fragment came from the file housing the widget, I could command click into the child fragment quickly and follow the hierarchy down. With operations in separate files I either have to command click into the type and look at the class names that it implements and then pair them to the widget, or find the associated widget, switch to that file, search for the child fragment, then search for a widget that uses it's type. Or take guesses at the other files in the folder.

Before switching to generated types, I would declare fragments in the widget as a static variable like below in FeedCard


class FeedCard extends StatelessWidget {
  final Map<String, dynamic> hubFrag;
  const FeedCard({Key? key, required this.hubFrag}) : super(key: key);

  static final fragment = addFragments(gql(r'''
      fragment feedCard_hub on Hub {
        id
        name
        serial
        ...feedCardArm_hub
        ...feedCardMap_hub
        ...hubUpdater_hub
      }
    '''), [FeedCardArm.fragment, FeedCardMap.fragment, HubUpdater.fragment]);

  @override
  Widget build(BuildContext context) {
  ...

where addFragments is defined as

DocumentNode addFragments(DocumentNode doc, List<DocumentNode> fragments) {
  final newDefinitions = Set<DefinitionNode>.from(doc.definitions);
  for (final frag in fragments) {
    newDefinitions.addAll(frag.definitions);
  }
  return DocumentNode(definitions: newDefinitions.toList(), span: doc.span);
}

In React/ApolloClient I would define them as below in SceneEditor the sceneEditorQuery type is generated and SceneViewer exports an object named fragments where the keys are each fragment for it's component. Again, able to click straight into the file from wherever the fragment is spread

export default function SceneEditor() {
  const { data, loading, error } = useQuery<sceneEditorQuery>(
    gql`
      query sceneEditorQuery($id: Int!) {
        scene(where: { id: $id }) {
          id
          ...sceneViewer_scene
        }
      }
      ${SceneViewer.fragments.scene}
    `,
    {
      variables: { id: currentScene },
    },
  )
  ...
}

In React/Relay I used the FragmentContainer HOC:

function TodoList(props) => // return component using props.list fragment

export default createFragmentContainer(TodoList, {
  // This `list` fragment corresponds to the prop named `list` that is
  // expected to be populated with server data by the `<TodoList>` component.
  list: graphql`
    fragment TodoList_list on TodoList {
      # Specify any fields required by '<TodoList>' itself.
      title
      # Include a reference to the fragment from the child component.
      todoItems {
        ...TodoItem_item
      }
    }
  `,
});

Although they have since introduced a hook for defining the fragment

import type {UserComponent_user$key} from 'UserComponent_user.graphql';
const UsernameSection = require('./UsernameSection.react');

type Props = {
  user: UserComponent_user$key,
};

function UserComponent(props: Props) {
  const user = useFragment(
    graphql`
      fragment UserComponent_user on User {
        name
        age
        # Include child fragment:
        ...UsernameSection_user
      }
    `,
    props.user,
  );

  ....
}
budde377 commented 2 years ago

I love the idea, just a note on the file bloat: You can definitely define multiple operations/fragments per .graphql file.