nestjs / graphql

GraphQL (TypeScript) module for Nest framework (node.js) 🍷
https://docs.nestjs.com/graphql/quick-start
MIT License
1.45k stars 391 forks source link

Types used in abstract resolvers are missing after the first E2E-test (code first) #2657

Open Lennard-Dietz opened 1 year ago

Lennard-Dietz commented 1 year ago

Did you read the migration guide?

Is there an existing issue that is already proposing this?

Potential Commit/PR that introduced the regression

No response

Versions

9.1.1 -> 10.2.0

Describe the regression

After Upgrading the nestjs packages in our repo, we had a bunch of failing tests. Upon further inspection, we found that all of our E2E tests, which are spawning a test nest application to run some gql-queries against it, were failing, if the related resolver was created by inheritance of another abstract resolver.

We are using the following additional packages:

Minimum reproduction code

import { gql } from "@apollo/client/core";
import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo";
import { INestApplication, Module } from "@nestjs/common";
import { Args, Field, GraphQLModule, InputType, Int, Mutation, Query, Resolver } from "@nestjs/graphql";
import { Test, TestingModule } from "@nestjs/testing";
import { ApolloServerBase } from "apollo-server-core";

interface CurrentThisContext {
  module: TestingModule;
  app: INestApplication;
  apolloServer: ApolloServerBase;
}

@InputType()
class MyArgs {
  @Field()
  public myInput: number;
}

@Resolver({ isAbstract: true })
abstract class MyBaseResolver {
  @Query(returns => Int)
  public myQuery(): number {
    return 10;
  }

  @Mutation(returns => Int)
  public myMutation(@Args({ type: () => MyArgs, name: "input" }) args: MyArgs): number {
    return args.myInput;
  }
}

@Resolver()
class MyResolver extends MyBaseResolver {}

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: true,
    }),
  ],
  providers: [MyResolver],
})
class MyModule {}

describe("MyIssue", function () {
  beforeEach(async function (this: CurrentThisContext) {
    const moduleRef = await Test.createTestingModule({ imports: [MyModule] });
    this.module = await moduleRef.compile();
    this.app = this.module.createNestApplication();
    await this.app.init();

    // Currently our "way" to run some gql-queries in the specs. There may be a better one, but this should be sufficient for the issue
    const graphqlModule: GraphQLModule = this.module.get<GraphQLModule<ApolloDriver>>(GraphQLModule);
    this.apolloServer = graphqlModule.graphQlAdapter["_apolloServer"];
  });

  afterEach(async function (this: CurrentThisContext) {
    await this.app.close();
  });

  function createMyTest() {
    it("myTest", async function (this: CurrentThisContext) {
      const mutation = gql`
        mutation MyTest {
          myMutation(input: { myInput: 5 })
        }
      `;
      const result = await this.apolloServer.executeOperation({ query: mutation });
      expect(result?.data?.myMutation).toEqual(5);
    });
  }

  // The first test that gets executed is successful, the second one fails with message 'Unknown argument "input" on field "Mutation.myMutation".'
  createMyTest();
  createMyTest();
});

After some deep digging it seems that the type-metadata.storage removes the methodArgs for the second run when compiling the schema. Example: Mutation metadata in the first run

{
  methodName: 'myMutation',
  schemaName: 'myMutation',
  target: [class MyResolver extends MyBaseResolver],
  typeFn: [Function (anonymous)],
  returnTypeOptions: {},
  description: undefined,
  deprecationReason: undefined,
  complexity: undefined,
  classMetadata: {
    target: [class MyResolver extends MyBaseResolver],
    typeFn: [Function (anonymous)],
    isAbstract: false
  },
  methodArgs: [
    {
      kind: 'arg',
      name: 'input',
      description: undefined,
      target: [class MyBaseResolver],
      methodName: 'myMutation',
      typeFn: [Function (anonymous)],
      index: 0,
      options: [Object]
    }
  ],
  directives: [],
  extensions: {}
}

Mutation metadata in the second run:

{
  methodName: 'myMutation',
  schemaName: 'myMutation',
  target: [class MyResolver extends MyBaseResolver],
  typeFn: [Function (anonymous)],
  returnTypeOptions: {},
  description: undefined,
  deprecationReason: undefined,
  complexity: undefined,
  classMetadata: {
    target: [class MyResolver extends MyBaseResolver],
    typeFn: [Function (anonymous)],
    isAbstract: false
  },
  methodArgs: [],
  directives: [],
  extensions: {}
}

Expected behavior

We expected this to work just like before.

Other

No response

kamilmysliwiec commented 1 year ago

Please provide a minimum reproduction repository (Git repository/StackBlitz/CodeSandbox project).

Lennard-Dietz commented 1 year ago

Here it is: https://stackblitz.com/edit/nestjs-typescript-starter-8qvafb?file=package.json,src%2Fissue-2657.spec.ts