graphqlgraphql-jsapollo-server

How to create generics with the schema language?


Using facebook's reference library, I found a way to hack generic types like this:

type PagedResource<Query, Item> = (pagedQuery: PagedQuery<Query>) => PagedResponse<Item>
    ​
interface PagedQuery<Query> {
  query: Query;
  take: number;
  skip: number;
}
    ​
interface PagedResponse<Item> {
  items: Array<Item>; 
  total: number;
}

function pagedResource({type, resolve, args}) {
  return {
    type: pagedType(type),
    args: Object.assign(args, {
      page: { type: new GraphQLNonNull(pageQueryType()) }
    }),
    resolve
  };
  function pageQueryType() {
    return new GraphQLInputObjectType({
      name: 'PageQuery',
      fields: {
        skip: { type: new GraphQLNonNull(GraphQLInt) },
        take: { type: new GraphQLNonNull(GraphQLInt) }
      }
    });
  }
  function pagedType(type) {
    return new GraphQLObjectType({
      name: 'Paged' + type.toString(),
      fields: {
        items: { type: new GraphQLNonNull(new GraphQLList(type))},
        total: { type: new GraphQLNonNull(GraphQLInt) }
      }
    });
  }
}

But I like how with Apollo Server I can declaratively create the schema. So question is, how do you guys go about creating generic-like types with the schema language?


Solution

  • You can create an interface or union to achieve a similar result. I think this article does a good job explaining how to implement interfaces and unions correctly. Your schema would look something like this:

    type Query {
      pagedQuery(page: PageInput!): PagedResult
    }
    
    input PageInput {
      skip: Int!
      take: Int!
    }
    
    type PagedResult {
      items: [Pageable!]!
      total: Int
    }
    
    # Regular type definitions for Bar, Foo, Baz types...
    
    union Pageable = Bar | Foo | Baz
    

    You also need to define a resolveType method for the union. With graphql-tools, this is done through the resolvers:

    const resolvers = {
      Query: { ... },
      Pageable {
        __resolveType: (obj) => {
          // resolve logic here, needs to return a string specifying type
          // i.e. if (obj.__typename == 'Foo') return 'Foo'
        }
      }
    }
    

    __resolveType takes the business object being resolved as its first argument (typically your raw DB result that you give GraphQL to resolve). You need to apply some logic here to figure out of all the different Pageable types, which one we're handling. With most ORMs, you can just add some kind of typename field to the model instance you're working with and just have resolveType return that.

    Edit: As you pointed out, the downside to this approach is that the returned type in items is no longer transparent to the client -- the client would have to know what type is being returned and specify the fields for items within an inline fragment like ... on Foo. Of course, your clients will still have to have some idea about what type is being returned, otherwise they won't know what fields to request.

    I imagine creating generics the way you want is impossible when generating a schema declaratively. To get your schema to work the same way it currently does, you would have to bite the bullet and define PagedFoo when you define Foo, define PagedBar when you define Bar and so on.

    The only other alternative I can think of is to combine the two approaches. Create your "base" schema programatically. You would only need to define the paginated queries under the Root Query using your pagedResource function. You can then use printSchema from graphql/utilities to convert it to a String that can be concatenated with the rest of your type definitions. Within your type definitions, you can use the extend keyword to build on any of the types already declared in the base schema, like this:

    extend Query {
      nonPaginatedQuery: Result
    }
    

    If you go this route, you can skip passing a resolve function to pagedResource, or defining any resolvers on your programatically-defined types, and just utilize the resolvers object you normally pass to buildExecutableSchema.