Graphql-code-generator: Mapper types aren't being applied for all typescript-resolvers

Created on 22 May 2019  ·  7Comments  ·  Source: dotansimha/graphql-code-generator

Describe the bug
Hi everyone,

This is my first time creating a public issue (and I'm new to TS and Graphql), so please forgive me in advance for any mistake.

I believe that this issue is very similar to https://github.com/dotansimha/graphql-code-generator/issues/1041. It looks like the mapper isn't always mapping types correctly in the current version. I'm creating a new issue as it was requested in https://github.com/dotansimha/graphql-code-generator/issues/1041.

Here is the case that we have seen recently.

  1. My GraphQL schema:
type MovieInsights {
  movieStarsPerMonth: [MonthlyCounts!]!
  movieStarsCumulativePerDay: [MovieCounts!]!
}

type MonthlyCounts {
  month: Date!
  movieCounts: [MovieCounts!]!
}

type MovieCounts {
  # The dynamically resolved Movie type
  movie: Movie!
  stars: Int!
  period: Date!
}

The generated mappers:

Movie: MappedMovie
MonthlyCounts: MonthlyCounts
MovieCounts: Omit<MovieCounts, 'movie'> & {
  movie: ResolversTypes['Movie']
}

:point_up: See that the mapper correctly omitted the movie from the mapping for MovieCounts as it has its own resolver. It doesn't do the same for MonthlyCounts that indirectly uses Movie - it does so through the MonthlyCounts. The generated typescript types:

export type MonthlyCounts = {
  month: Scalars['Date']
  movieCounts: Array<MovieCounts>
}

export type MovieCounts = {
  movie: Movie
  stars: Scalars['Int']
}
  1. My GraphQL operations:

The problem comes when I'm implementing the resolver for movieStarsPerMonth (that uses MonthlyCounts). I keep getting the error: Type 'MappedMovie' is missing the following properties from type 'Movie': boxOffice, worldwideReleases, awards, and 15 more.ts(2322).

The implementation of each query:

// Works fine!
async movieStarsCumulativePerDay(
  { studioId },
   _args,
  { services }
){
  const starCounts = ... // get from service 
  return await Promise.all(
    starCounts.map(async (dailyCounts) => {
      // movieService returns a MappedMovie
      const movie = await services.movieService.getMovie(s.movieId)
      return {
        movie,
        period: dailyCounts.period,
        stars: dailyCounts.users
      }
    })
  )
}

// This gives me the error shown above: Type 'MappedMovie' is missing the following properties from type 'Movie': boxOffice, worldwideReleases, awards, and 15 more.ts(2322)
async movieStarsPerMonth(
  { studioId },
  _args,
  { services }
) {
  const yearlyStars = ... //get from a service
  const counts = yearlyStars.map(
    async (monthCounts) => {
      return {
        month: monthCounts.month,
        movieCounts: await Promise.all(
          monthCounts.movies.map(async (s: StarCounts) => {
            // movieService returns a MappedMovie
            const movie = await services.movieService.getMovie(s.movieId)
            return {
              movie,
              period: s.period,
              stars: s.stars
            }
          })
        )
      }
    }
  )
  return await Promise.all(counts)
}

Expected behavior
The mappers correctly map the dynamic MappedMovie to Movie and the compiler is happy 💯 .

Environment:

bug plugins waiting-for-release

Most helpful comment

@mshwery thank you for the reproduction, and sorry for the delay. I managed to understand the issue, reproduce it and fix it :)

Now for a given schema:

      type Movie {
        id: ID!
        title: String!
      }

      type Book {
        id: ID!
        author: String!
      }

      union MovieLike = Movie | Book

      type NonInterfaceHasNarrative {
        narrative: MovieLike!
        movie: Movie!
      }

      type LayerOfIndirection {
        id: ID!
        movies: [NonInterfaceHasNarrative!]!
      }

      type AnotherLayerOfIndirection {
        inner: LayerOfIndirection!
      }

you will get:

    export type ResolversTypes = {
      String: Scalars['String'],
      Boolean: Scalars['Boolean'],
      Movie: MovieEntity,
      ID: Scalars['ID'],
      Book: Book,
      MovieLike: ResolversTypes['Movie'] | ResolversTypes['Book'],
      NonInterfaceHasNarrative: Omit<NonInterfaceHasNarrative, 'narrative' | 'movie'> & { narrative: ResolversTypes['MovieLike'], movie: ResolversTypes['Movie'] },
      LayerOfIndirection: Omit<LayerOfIndirection, 'movies'> & { movies: Array<ResolversTypes['NonInterfaceHasNarrative']> },
      AnotherLayerOfIndirection: Omit<AnotherLayerOfIndirection, 'inner'> & { inner: ResolversTypes['LayerOfIndirection'] },
    };

Fix is in: https://github.com/dotansimha/graphql-code-generator/pull/2022 , can you please test it? it's available as alpha version (1.2.2-alpha-55c83745.27).

All 7 comments

Notably the same error exists after on @1.2.0 ☝️

@mshwery @solon-aguiar I think that I fixed that, maybe you can assist me with it?

I added this test before applying the last fix: https://github.com/dotansimha/graphql-code-generator/blob/master/packages/plugins/typescript/resolvers/tests/mapping.spec.ts#L58-L98 , but maybe it's missing something?

Thank you!

I think the missing piece is one layer of indirection:

type Movie {
  id: ID!
  title: String!
}

type Book {
  id: ID!
  author: String!
}

union MovieLike = Movie | Book

type NonInterfaceHasNarrative {
  narrative: MovieLike!
  movie: Movie!
}

# This one will have the issue – it resolves `movies: Array<NonInterfaceHasNarrative>` which is the fully resolved type from the graphql SDL, instead of applying `movie: ResolverTypes['Movie']`
type LayerOfIndirection {
  id: ID!
  movies: [NonInterfaceHasNarrative!]!
}

It seems strange that the SDL types themselves don't refer to ResolverTypes['MappedType']. Is there a reason for that?

@mshwery the basic types are generated by typescript and they are the base types, ResolversTypes just refers to it and replaces fields when needed.
I'll take a look soon and try to solve it. Thanks!

@mshwery thank you for the reproduction, and sorry for the delay. I managed to understand the issue, reproduce it and fix it :)

Now for a given schema:

      type Movie {
        id: ID!
        title: String!
      }

      type Book {
        id: ID!
        author: String!
      }

      union MovieLike = Movie | Book

      type NonInterfaceHasNarrative {
        narrative: MovieLike!
        movie: Movie!
      }

      type LayerOfIndirection {
        id: ID!
        movies: [NonInterfaceHasNarrative!]!
      }

      type AnotherLayerOfIndirection {
        inner: LayerOfIndirection!
      }

you will get:

    export type ResolversTypes = {
      String: Scalars['String'],
      Boolean: Scalars['Boolean'],
      Movie: MovieEntity,
      ID: Scalars['ID'],
      Book: Book,
      MovieLike: ResolversTypes['Movie'] | ResolversTypes['Book'],
      NonInterfaceHasNarrative: Omit<NonInterfaceHasNarrative, 'narrative' | 'movie'> & { narrative: ResolversTypes['MovieLike'], movie: ResolversTypes['Movie'] },
      LayerOfIndirection: Omit<LayerOfIndirection, 'movies'> & { movies: Array<ResolversTypes['NonInterfaceHasNarrative']> },
      AnotherLayerOfIndirection: Omit<AnotherLayerOfIndirection, 'inner'> & { inner: ResolversTypes['LayerOfIndirection'] },
    };

Fix is in: https://github.com/dotansimha/graphql-code-generator/pull/2022 , can you please test it? it's available as alpha version (1.2.2-alpha-55c83745.27).

Fixed in 1.3.0 🎉

Was this page helpful?
0 / 5 - 0 ratings