graphql-code-generator-community icon indicating copy to clipboard operation
graphql-code-generator-community copied to clipboard

feat: add graceful graph ql interfaces

Open DevRichter opened this issue 1 year ago • 1 comments

Description

This PR introduces a way to gracefully handle graphql interfaces. Currently working with interfaces in client side is always a pain where you only get access to the queried fields after a if (queriedType.__typeName === 'TypeName') check. You also only get access to the queried fields within the scope of the if.

You could then map the types to a new type, but that always leaves work for the developer open. Using the generated type TypeName from the typescript codegen plugin to typecast is also not valid, as that type includes all fields defined by the typedefs in your schema. You might have queried less fields, and then get access to more fields that can possibly be there. This can also be potentially harmful.

To aid in that specific problem the package also introduces the possibilty to remove export modifiers for the baseTypes E.g. TypeName, so you can only use TypeNameOfQueryName.

This PR attempts to go around the issue of having to do manual labor, by generating type-guards, query-types and helper functions, to easily work with graphql interfaces in the frontend.

Quick example:

query HeroDetails($episode: Episode) {
      hero(episode: $episode) {
        name
        ... on Human {
          height
        }
        ... on Droid {
          primaryFunction
        }
      }
 }

the hero query is of the interface type Character so we generate the types for this query:

export type HumanOfHeroDetailsQuery = HeroStateTemplate<HeroDetailsQuery, 'Human'>;

export type DroidOfHeroDetailsQuery = HeroStateTemplate<HeroDetailsQuery, 'Droid'>;

export type HeroOfHeroDetailsQuery = HeroDetailsQuery['hero'];

export const isCharacterOfHeroDetailsQueryHuman = (
  entity: HeroOfHeroDetailsQuery,
): entity is HumanOfHeroDetailsQuery => isEntityOfType<HumanOfHeroDetailsQuery>(entity, 'Human');

export const isCharacterOfHeroDetailsQueryDroid = (
  entity: HeroOfHeroDetailsQuery,
): entity is DroidOfHeroDetailsQuery => isEntityOfType<DroidOfHeroDetailsQuery>(entity, 'Droid');

export const getHumanOfHeroDetailsQueryOfCharacter = (character?: HeroOfHeroDetailsQuery): HumanOfHeroDetailsQuery | null => {
  if (!character) return null;
  return getEntityByType<HumanOfHeroDetailsQuery>(character, 'Human');
};

export const getDroidOfHeroDetailsQueryOfCharacter = (character?: HeroOfHeroDetailsQuery): DroidOfHeroDetailsQuery | null => {
  if (!character) return null;
  return getEntityByType<DroidOfHeroDetailsQuery>(character, 'Droid');
};

They can then be easily used by inputting the query result of the specifc query, to get access to ONLY the Droid and its queried fields by using getDroidOfHeroDetailsQueryOfCharacter for example.

The biggest use case in my opinion, is when used for when you query arrays of interfaces or union types. The helper functions then look like this:

export const getHumanOfHeroesQueryOfCharacters = (characters?: HeroeOfHeroesQuery[]): HumanOfHeroesQuery[] => {
  if (!characters) return [];
  return getEntitiesByType<HumanOfHeroesQuery>(characters, 'Human');
};


export const getDroidOfHeroesQueryOfCharacters = (characters?: HeroeOfHeroesQuery[]): DroidOfHeroesQuery[] => {
  if (!characters) return [];
  return getEntitiesByType<DroidOfHeroesQuery>(characters, 'Droid');
};

They essentialy filter out the desired type and give type security on top of that. I have also added the discriminator "__queryName" to the generated queryTypes, so you can only input the result of one query into a helper function or type guard from that query, otherwise an TS error is thrown (this is configurable though.)

*** NO related Issue as this was created on a whim i had and then it grew by itself (oops)***

Type of change

Please delete options that are not relevant.

  • [x] New feature (non-breaking change which adds functionality)
  • [x] This change requires a documentation update

How Has This Been Tested?

Ive created a test suite - basic right now - but i will expand it. We also use it in different repos in production already released within our company scope.

Checklist:

  • [x] I have followed the CONTRIBUTING doc and the style guidelines of this project
  • [ ] I have performed a self-review of my own code
  • [x] I have commented my code, particularly in hard-to-understand areas
  • [ ] I have made corresponding changes to the documentation
  • [x] My changes generate no new warnings
  • [x] I have added tests that prove my fix is effective or that my feature works
  • [x] New and existing unit tests pass locally with my changes
  • [x] Any dependent changes have been merged and published in downstream modules

DevRichter avatar Dec 04 '23 16:12 DevRichter