Search code examples
node.jstypescriptgraphqlapolloapollo-server

GraphQL Apollo resolver context parameter type


I'm currently building out an auth microservice, as an Apollo subgraph. Part of this involves my Apollo-express-gateway decoding a JWT, and passing the subsequent payload to the subgraphs to identify the user/validate permissions/etc.

I've got my subgraph capturing the headers passed from my gateway, and passing them into context. However, I'm having some TS errors inside my resolvers where the context object is concerned. I've tried defining the types, but to no avail.

My function in my index file on the subgraph, where I'm populating context:

async function startServer() {
    const { url } = await startStandaloneServer(server(), {
        context: async ({ req }) => {
            // @ts-ignore - Type 'string[]' is not assignable to type 'string'. user will never be an array (comes from gateway)
            const user = req.headers.user ? await JSON.parse(req.headers.user) : null;
            return { user };
        },
        listen: { port: 4001 }
    });
    console.info(`🚀 Auth Microservice ready at: ${url}`);
}

Schema

import { gql } from 'apollo-server-core';

export const typeDefs = gql`
    extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable"])

    type User {
        id: Int!
        firstName: String
        lastName: String
        email: String!
        lastLogin: String
        lockedUntil: String
        createdAt: String
        updatedAt: String
    }

    type Query {
        user(id: Int): User
    }
`;

My resolver:


import type { User } from '@prisma/client';
import { PrismaClient } from '@prisma/client';
import { authContext } from '~/types/interfaces';
import { getUserArgs } from '~/types/arguments';

const prisma = new PrismaClient();

export const resolvers = {
    Query: {
        user: async (_: undefined, args: getUserArgs, context) => {
            return await prisma.user.findUnique({
                where: {
                    id: args.id ?? 0
                }
            });
        }
    }
};

authContext interface, which I tried to use to resolve the error to no avail (I also tried extending Context from apollo-server-core:

export interface authContext {
    user: {
        id: number;
        permissions: {
            read: [string?];
            write: [string?];
        };
    };
}

The error:

return new TSError(diagnosticText, diagnosticCodes, diagnostics);
           ^
TSError: ⨯ Unable to compile TypeScript:
src/server/index.ts(14,40): error TS2345: Argument of type '{ typeDefs: DocumentNode[]; resolvers: { Query: { users: (_: undefined, { start, limit, cursor, pagination, orderBy, orderDir }: getUsersArgs) => Promise<User[]>; user: (_: undefined, args: getUserArgs, { user }: authContext) => Promise<...>; }; User: { ...; }; Mutation: { ...; }; }; }' is not assignable to parameter of type 'DocumentNode | (DocumentNode | GraphQLSchemaModule)[] | LegacySchemaModule'.
  Types of property 'resolvers' are incompatible.
    Type '{ Query: { users: (_: undefined, { start, limit, cursor, pagination, orderBy, orderDir }: getUsersArgs) => Promise<User[]>; user: (_: undefined, args: getUserArgs, { user }: authContext) => Promise<User | null>; }; User: { profile: (parent: User) => Promise<{ avatar: UserAvatar | null; permissions: permissionsProps;...' is not assignable to type 'GraphQLResolverMap<unknown>'.
      Property 'Query' is incompatible with index signature.
        Type '{ users: (_: undefined, { start, limit, cursor, pagination, orderBy, orderDir }: getUsersArgs) => Promise<User[]>; user: (_: undefined, args: getUserArgs, { user }: authContext) => Promise<User | null>; }' is not assignable to type 'GraphQLScalarType<unknown, unknown> | { [enumValue: string]: string | number; } | { [fieldName: string]: GraphQLFieldResolver<any, unknown, any, unknown> | { requires?: string | undefined; resolve: GraphQLFieldResolver<...>; }; }'.
          Type '{ users: (_: undefined, { start, limit, cursor, pagination, orderBy, orderDir }: getUsersArgs) => Promise<User[]>; user: (_: undefined, args: getUserArgs, { user }: authContext) => Promise<User | null>; }' is not assignable to type '{ [fieldName: string]: GraphQLFieldResolver<any, unknown, any, unknown> | { requires?: string | undefined; resolve: GraphQLFieldResolver<any, unknown, any, unknown>; }; }'.
            Property 'user' is incompatible with index signature.
              Type '(_: undefined, args: getUserArgs, { user }: authContext) => Promise<User | null>' is not assignable to type 'GraphQLFieldResolver<any, unknown, any, unknown> | { requires?: string | undefined; resolve: GraphQLFieldResolver<any, unknown, any, unknown>; }'.
                Type '(_: undefined, args: getUserArgs, { user }: authContext) => Promise<User | null>' is not assignable to type 'GraphQLFieldResolver<any, unknown, any, unknown>'.
                  Types of parameters '__2' and 'context' are incompatible.
                    Type 'unknown' is not assignable to type 'authContext'.

Any help would be greatly appreciated. I'm still learning TS/graphql so maybe I'm missing some conceptual information here or misunderstanding something so any detailed explanations welcome as well.

Thanks :)

Reproduction repo: https://github.com/Tom-Pearce/apollo-context-issue


Solution

  • In this case the answer turned out to be an issue with my buildSubgraphSchema as soon as I started handling the context param in my handlers.

    I had to change this, where I was combining my schema with the graphql-constraint-directive packages typeDefs:

    const schema = buildSubgraphSchema({
        typeDefs: [gql(constraintDirectiveTypeDefs), typeDefs],
        resolvers
    });
    

    To this, where I'm essentially creating my schema (with my resolvers), and then combining it with the constraint directive afterwards. At least that's how I've interpreted it:

    const schema = buildSubgraphSchema([
        {
            typeDefs,
            resolvers
        },
        { typeDefs: gql(constraintDirectiveTypeDefs) }
    ]);
    

    Big thanks to Trevor in the GraphQL discord channel for finding the solution: https://discord.com/channels/625400653321076807/1039870349329829898/1040318591964282901