graphql icon indicating copy to clipboard operation
graphql copied to clipboard

ResolveField ignores field middleware

Open equal-matt opened this issue 2 years ago • 5 comments

Is there an existing issue for this?

  • [X] I have searched the existing issues

Current behavior

When resolving a field with @ResolveField, any middleware defined on the corresponding ObjectType's @Field definition is ignored.

This is particularly annoying when a subset of fields for an object are resolved. Without also finding the resolver for the field that you're looking at, there is no guarantee that your middleware will execute as specified. To handle middleware-like transformations consistently, you are forced to handle all middleware-like transformations in field resolvers by hand.

Minimum reproduction code

https://stackblitz.com/edit/nestjs-typescript-starter-rkgnvv?file=src/example/example.resolver.ts&view=editor

Steps to reproduce

Use the GraphQL playground in the preview section to query for

query {
  allExamples {
    fieldMiddlewareField
    resolveMiddlewareField
    bothMiddlewareField
  }
}

Expected behavior

Middleware defined in an ObjectType's @Field decorator should apply to the returned values from the @ResolveField handler.

Package version

10.1.7

Graphql version

"@nestjs/apollo": "^10.1.7",
"@nestjs/graphql": "^10.1.7",
"@nestjs/platform-express": "^9.0.0",
"apollo-server-express": "^3.11.1",
"graphql": "^16.6.0",

NestJS version

9.2.1

Node.js version

16.14.2

In which operating systems have you tested?

  • [ ] macOS
  • [ ] Windows
  • [X] Linux

Other

No response

equal-matt avatar Jan 10 '23 14:01 equal-matt

@equal-matt I tried to fix example.resolver.ts, field middleware works correctly. I don't know details, it seems that returnType in @ResolveField needs to define type.

import { Query, ResolveField, Resolver } from '@nestjs/graphql';
import { Example } from './example.model';

@Resolver((_) => Example)
export class ExampleResolver {
  @Query(() => [Example])
  allExamples() {
    return [{ fieldMiddlewareField: '  will be trimmed  ' }];
  }

  @ResolveField(() => String, {
    middleware: [async (_, next) => (await next()).trim()],
  })
  resolveMiddlewareField() {
    return "  won't be trimmed  ";
  }

  @ResolveField(() => String, {
    middleware: [async (_, next) => (await next()).trim()],
  })
  bothMiddlewareField() {
    return "  still won't be trimmed  ";
  }
}

choco14t avatar Jan 26 '23 10:01 choco14t

Nice - can confirm that makes the field resolver middleware take precedence over the model middleware.

equal-matt avatar Jan 26 '23 10:01 equal-matt

Would you like to create a PR for this issue?

kamilmysliwiec avatar Feb 06 '23 14:02 kamilmysliwiec

If I get a moment I will, but I'd encourage anyone else to pick it up if they have the time. I'm pretty swamped right now!

equal-matt avatar Feb 13 '23 17:02 equal-matt

Not sure if it's related, but field middleware also doesn't appear to work when defined on an @InputType. e.g.

@InputType('SomeInput')
class SomeGqlInput {
    @Field(() => String, {
        description: 'The account email address.',
        nullable: false,
        // Field middleware in the below array doesn't seem to run
        middleware: [],
    })
    public email!: Email;
}

troywweber7 avatar Jan 23 '24 20:01 troywweber7