hayes / pothos

Pothos GraphQL is library for creating GraphQL schemas in typescript using a strongly typed code first approach
https://pothos-graphql.dev
ISC License
2.28k stars 153 forks source link

loadableGroup load/group seems to infer the wrong type #1200

Closed KaisSalha closed 1 month ago

KaisSalha commented 1 month ago
export const Membership = builder.loadableNodeRef("Membership", {
      id: {
          resolve: (membership) => membership.id,
      },
      load: async (ids: string[]) => {
          const memberships = await db.query.memberships.findMany({
              where: (memberships, { inArray }) =>
                  inArray(
                      memberships.id,
                      ids.map((id) => parseInt(id))
                  ),
          });

          return ids.map((id) => {
              const membership = memberships.find(
                  (membership) => membership.id == parseInt(id)
              );

              if (!membership) {
                  return new Error(`Membership not found: ${id}`);
              }
              return membership;
          });
      },
  });

  Membership.implement({
      isTypeOf: (membership) => (membership as MembershipType).id !== undefined,
      fields: (t) => ({
          user: t.loadable({
              type: User,
              load: async (ids, { loadMany }) => loadMany(User, ids),
              resolve: (parent) => parent.user_id,
          }),
          role: t.loadable({
              type: Role,
              load: async (ids, { loadMany }) => loadMany(Role, ids),
              resolve: (parent) => parent.role_id,
          }),
          organization: t.loadable({
              type: Organization,
              load: async (ids, { loadMany }) => loadMany(Organization, ids),
              resolve: (parent) => parent.organization_id,
          }),
          created_at: t.string({
              nullable: true,
              resolve: (parent) => parent.created_at.toISOString(),
          }),
      }),
  });

  User.implement({
      isTypeOf: (user) => (user as UserType).id !== undefined,
      fields: (t) => ({
          email: t.exposeString("email"),
          first_name: t.exposeString("first_name", { nullable: true }),
          last_name: t.exposeString("last_name", { nullable: true }),
          type: t.exposeString("type"),
          memberships: t.loadableGroup({
              type: Membership,
              load: async (ids: number[]) => {
                const result = await db.query.memberships.findMany({
                    where: (memberships, { inArray }) =>
                        inArray(memberships.user_id, ids),
                });
                return result;
              },
              group: (membership) => membership.user_id,
              resolve: (parent) => parent.id,
          }),
          created_at: t.string({
              nullable: true,
              resolve: (parent) => parent.created_at.toISOString(),
          }),
      }),
  });

Property 'user_id' does not exist on type 'string | { id: number; created_at: Date; user_id: number; organization_id: number; permissions: unknown; role_id: number; }'. Property 'user_id' does not exist on type 'string'.ts(2339)

I don't understand why load may return a string.

Typeof result is the following:

const result: {
    id: number;
    created_at: Date;
    user_id: number;
    organization_id: number;
    permissions: unknown;
    role_id: number;
}[]

but typeof load is the following:

(property) load: (keys: number[], context: ContextType, args: never) => Promise<readonly (string | {
    id: number;
    created_at: Date;
    user_id: number;
    organization_id: number;
    permissions: unknown;
    role_id: number;
} | null | undefined)[]>
hayes commented 1 month ago

The issue here is that pothos isn't inferring the return type of load at all. What it does is that it ensures that load returns an array of whatever is expected for a resolver returning a Membership, and that same type is used as the argument in group. Because Membership is also a loadable type, you can return just a string to use the membership dataloader

KaisSalha commented 1 month ago

Thanks!