sikanhe/gqtx

Interface implementation object types are not exposed on schema when not used by any field

n1ru4l opened this issue · 3 comments

const GraphQLChatMessageInterfaceType = t.interfaceType<ChatMessageType>({
  name: "ChatMessage",
  fields: () => [
    t.abstractField("id", t.NonNull(t.ID)),
    t.abstractField(
      "content",
      t.NonNull(t.List(t.NonNull(GraphQLChatMessageNode)))
    ),
    t.abstractField("createdAt", t.NonNull(t.String)),
    t.abstractField("containsDiceRoll", t.NonNull(t.Boolean)),
  ],
});

const GraphQLOperationalChatMessageType = t.objectType<
  OperationalChatMessageType
>({
  interfaces: [GraphQLChatMessageInterfaceType],
  name: "OperationalChatMessage",
  fields: () => [
    t.field("id", {
      type: t.NonNull(t.ID),
      resolve: (message) => message.id,
    }),
    t.field("content", {
      type: t.NonNull(t.List(t.NonNull(GraphQLChatMessageNode))),
      resolve: (message) => message.content,
    }),
    t.field("createdAt", {
      type: t.NonNull(t.String),
      resolve: (message) => new Date(message.createdAt).toISOString(),
    }),
    t.field("containsDiceRoll", {
      type: t.NonNull(t.Boolean),
      resolve: (message) =>
        message.content.some((node) => node.type === "DICE_ROLL"),
    }),
  ],
});

const GraphQLUserChatMessageType = t.objectType<UserChatMessageType>({
  interfaces: [GraphQLChatMessageInterfaceType],
  name: "UserChatMessage",
  description: "A chat message",
  fields: () => [
    t.field("id", {
      type: t.NonNull(t.ID),
      resolve: (message) => message.id,
    }),
    t.field("authorName", {
      type: t.NonNull(t.String),
      resolve: (message) => message.authorName,
    }),
    t.field("content", {
      type: t.NonNull(t.List(t.NonNull(GraphQLChatMessageNode))),
      resolve: (message) => message.content,
    }),
    t.field("createdAt", {
      type: t.NonNull(t.String),
      resolve: (message) => new Date(message.createdAt).toISOString(),
    }),
    t.field("containsDiceRoll", {
      type: t.NonNull(t.Boolean),
      resolve: (message) =>
        message.content.some((node) => node.type === "DICE_ROLL"),
    }),
  ],
});

Unless I have no field that "consumes" the type GraphQLUserChatMessageType or GraphQLOperationalChatMessageType the both types are not available on the GraphQL Schema.

Also, I cannot find an option (in the TS Typings) for specifying to which actual object type the interface type resolves to.

I assumed there would be a similar API like with the union type e.g.:

const GraphQLDiceRollDetailNode = t.unionType<DiceRollDetail>({
  name: "DiceRollDetail",
  types: [
    GraphQLDiceRollOperatorNode,
    GraphQLDiceRollConstantNode,
    GraphQLDiceRollDiceRollNode,
    GraphQLDiceRollOpenParenNode,
    GraphQLDiceRollCloseParenNode,
  ],
  resolveType: (obj) => {
    if (obj.type === "Operator") return GraphQLDiceRollOperatorNode;
    else if (obj.type === "Constant") return GraphQLDiceRollConstantNode;
    else if (obj.type === "DiceRoll") return GraphQLDiceRollDiceRollNode;
    else if (obj.type === "OpenParen") return GraphQLDiceRollOpenParenNode;
    else if (obj.type === "CloseParen") return GraphQLDiceRollCloseParenNode;
    throw new Error("Invalid type");
  },
});

Tools like graphql-tools do the same (https://www.graphql-tools.com/docs/resolvers#unions-and-interfaces).

Also in addition I want to mention that the union types + their implementations could be a bit more type-safe by requiring to return a tuple of the type + the required object for that object type. in the resolveType function (See #12).

export type DiceRollDetail =
  | {
      type: "DiceRoll";
      content: string;
      detail: {
        max: number;
        min: number;
      };
      rolls: Array<number>;
    }
  | {
      type: "Constant";
      content: string;
    }
  | {
      type: "Operator";
      content: string;
    }
  | {
      type: "OpenParen";
      content: string;
    }
  | {
      type: "CloseParen";
      content: string;
    };

const GraphQLDiceRollOperatorNode = t.objectType<
  Extract<DiceRollDetail, { type: "Operator" }>
>({
  name: "DiceRollOperatorNode",
  fields: () => [
    t.field("content", {
      type: t.NonNull(t.String),
      resolve: (object) => object.content,
    }),
  ],
});

const GraphQLDiceRollDetailNode = t.unionType<DiceRollDetail>({
  name: "DiceRollDetail",
  types: [
    GraphQLDiceRollOperatorNode,
    GraphQLDiceRollConstantNode,
    GraphQLDiceRollDiceRollNode,
    GraphQLDiceRollOpenParenNode,
    GraphQLDiceRollCloseParenNode,
  ],
  resolveType: (obj) => {
    if (obj.type === "Operator") {
       obj // is Extract<DiceRollDetail, { type: "Operator" }>
       return [GraphQLDiceRollOperatorNode, obj]
    }
    else if (obj.type === "Constant") {
      obj // is Extract<DiceRollDetail, { type: "Constant" }>
      return [GraphQLDiceRollConstantNode, obj];
    }
    else if (obj.type === "DiceRoll") {
      obj // is Extract<DiceRollDetail, { type: "DiceRoll" }>
      return GraphQLDiceRollDiceRollNode;
    }
    else if (obj.type === "OpenParen") {
      obj // is Extract<DiceRollDetail, { type: "OpenParen" }>
      return [GraphQLDiceRollOpenParenNode, obj];
    }
    else if (obj.type === "CloseParen") {
      obj // is Extract<DiceRollDetail, { type: "CloseParen" }>
      return [GraphQLDiceRollCloseParenNode, obj];
    }
    throw new Error("Invalid type");
  },
});

I thought interface already have resolveType
https://github.com/sikanhe/gqtx/blob/master/src/types.ts#L157
https://github.com/sikanhe/gqtx/blob/master/src/define.ts#L215

Alternatively, I think the better way for interface implementation should be using isTypeOf on the subtypes

Quote from graphql creator Lee Byron on this issue:
graphql/graphql-js#876 (comment)

I thought interface already have resolveType
https://github.com/sikanhe/gqtx/blob/master/src/types.ts#L157
https://github.com/sikanhe/gqtx/blob/master/src/define.ts#L215

There is no option for defining the resolveType function with the define API with the interfaceType function:

gqtx/src/define.ts

Lines 224 to 242 in 460bcee

interfaceType<Src>({
name,
description,
fields,
}: {
name: string;
description?: string;
fields: (self: Interface<Ctx, Src | null>) => Array<AbstractField<Ctx, any>>;
}): Interface<Ctx, Src | null> {
const obj: Interface<Ctx, Src | null> = {
kind: 'Interface',
name,
description,
fieldsFn: undefined as any,
};
obj.fieldsFn = () => fields(obj) as any;
return obj;
},

Alternatively, I think the better way for interface implementation should be using isTypeOf on the subtypes

I never heard of that API actually. Looks interesting. and I will definitely check it out. Still, for completeness we should probably still provide the resolveType function option for the interfaceType function.

Unless I have no field that "consumes" the type GraphQLUserChatMessageType or GraphQLOperationalChatMessageType the both types are not available on the GraphQL Schema.

Any ideas on how we can tackle this? Sometimes I only have a field that returns an interface type but the interface implementation types are not directly returned by any field.

A workaround would be to add dummy fields to the Query/Mutation type that return null.

A more clean and clever solution would allow passing in those object types via the buildGraphQLSchema function.

The API could look something like this:

import { buildGraphQLSchema } from "gqtx";
// ... other imports

export const schema = buildGraphQLSchema({
  query: Query,
  subscription: Subscription,
  mutation: Mutation,
  additionalObjectTypes: {
    GraphQLDiceRollOperatorNode,
    GraphQLDiceRollConstantNode,
    GraphQLDiceRollDiceRollNode,
    GraphQLDiceRollOpenParenNode,
    GraphQLDiceRollCloseParenNode,
  }
});

An even more convenient way would be to auto-detect those types by reading the types from unionTypes(Edit: This is already happening right now). This does not work for interface types though.

Therefore the explicit way of passing them to the buildGraphQLSchema function could be the better solution.

I will create a new issue for union types and isTypeOf typing improvements