Graphql: Feature: Introduce Middleware (or another name?) (`graphql-middleware` compatibility)

Created on 2 Jun 2020  Â·  15Comments  Â·  Source: nestjs/graphql

I'm not sure how to word this, so I'll just come up with an example.

Guards make the most sense when dealing with controllers, etc. They work decent for Queries and Mutations as well, however, the power in GraphQL can come from field level customization.

I know there are extensions, which is great (though doesn't work for Queries/Mutations/FieldResolvers(?) yet), but I'd like to create essentially a Query/Mutation/Field/FieldResolver middleware that all share pretty much identical looks (i.e. adopted from https://github.com/prisma-labs/graphql-middleware).

Why?

Well, Pipes/Guards/Interceptors are really slow when it comes to using them on GraphQL fields.

Enough of my typing... I'll just show a simple use-case that I feel would be awesome for nest's GraphQL implementation instead:

export class AuthMiddleware implements GraphQLMiddleware {
    async run(resolve, root, args, context, info) {
        context.user = /* perhaps verify JWT token from headers loosely */

        await resolve(root, args, context, info);
    }

    // alternatively you can have overridable methods.. i.e.)
    async before(@Context() context: any) {
        context.user = parseJWT(context.req);
    }
}
export class IsAdmin implements GraphQLMiddleware {
    async run(resolve, root, args, context, info) {
        if (!context.user || !context.user.hasRole(Roles.Admin)) {
           throw new UnauthorizedException();
       }

        await resolve(root, args, context, info);
    }

    // alternatively you can have overridable methods.. i.e.)
    async before(@Context() context: any) {
        if (!context.user || !context.user.hasRole(Roles.Admin)) {
           throw new UnauthorizedException();
       }
    }
}
@ObjectType()
@Entity()
export class User extends BaseEntity {
    @PrimaryGeneratedColumn()
    @Field(() => Number)
    id: number;

    @Column()
    @Field(() => String)
    @Use(IsAdmin) // converts to field resolver, wraps it with middleware, returns class value if middleware succeeds.
    address: string;
}
@ObjectType()
class UserResolver extends BaseEntityResolver {
  @Query(() => [User])
  async users(): Promise<User[]> {
    return this.repository.find();
  }

  @Query(() => [User])
  @Use(IsAdmin) // Yes, an auth guard can be here... but I'd like to share logic.
  async deletedUsers(): Promise<User[]> {
    return this.repository.find();
  }
}
@Module({
  imports: [
    GraphQLModule.forRoot({
      debug: false,
      playground: false,
      context: ({ req }) => ({ req }),
      // global GraphQL specific middleware
      middlewares: [AuthMiddleware]
    })
  ]
})
export class AppModule {}

You can create middleware for request logging, etc, etc. I know that in the global scope, you can use apollo plugins, however those are less flexible.

In the end of the day, this all may be possible by extracting out the schema, applying @Extensions to fields (though extensions support would have to work for resolver functions, such as Query, Mutation, and ResolveField), but this is quite a lot of work when so much of the GraphQL world already loves plugins (https://github.com/maticzav/graphql-shield would work automatically, as an example.. Someone could even make graphql-shield-nest ;))

feature

Most helpful comment

Field middleware feature was added in one of the recent releases https://docs.nestjs.com/graphql/field-middleware :)

All 15 comments

I know there are extensions, which is great (though doesn't work for Queries/Mutations/FieldResolvers(?) yet)

Extensions are available now https://docs.nestjs.com/graphql/extensions (I've just merged a PR yesterday). They are fairly complex though.

Well, Pipes/Guards/Interceptors are really slow when it comes to using them on GraphQL fields.

In fact, they are not slow. The issue pops up when you register an interceptor and try to return a large amount of data. Since interceptors are based on RxJS, a separate Observable would be created for each requested field (for each @ResolveField called) and this creates a lot of additional, unneeded overhead (and thus, we disabled them by default).

but I'd like to create essentially a Query/Mutation/Field/FieldResolver middleware that all share pretty much identical looks

We shouldn't introduce more abstraction for Queries/Mutations/Subscriptions. For those, everyone can safely use guards & interceptors & pipes without explicitly turning on the fieldEnhancers. Adding another building block would make it even more complicated. For sharing the logic among them, providers should be used.

However, I do agree that having a dedicated element (middleware? FieldMiddleware?) for just fields (@Field() and maybe @ResolveField() for the consistency) could be useful for implementing, for example, field-level permissions system (which is now possible only by traversing the schema in guard/interceptor + extensions). Though, I don't think adding another decorator makes sense here(?). I'm guessing that having the ability to register global field middleware should be enough.

If it’s global, how would you go about specifying different rules on
various fields. And regarding the extensions method. I saw the example in
the documentation. I’m not sure if I’d go that route where it’s done on
runtime every request. I’d probably implement it similarly to how
apollo-federation does it. I’d be more in favor of using extensions then
iterate over all schema types for it, then wrap the resolve function with
the extension logic. That way you’re not doing expensive logic to figure
out requested fields/find all extensions, for every request/field resolver.
I didn’t get too deep into what feels right yet.

A simple solution for field level/field resolver plugins/middleware seems
like the best user experience. I’d like to see it everywhere just so it’s
consistent (the ability to create a custom decorator ‘@Auth(Roles.Admin)’
and use it anywhere as an example). I understand you want this to fit in
the nest ecosystem etc though.

If it’s global, how would you go about specifying different rules on various fields.

Extensions could be used for this (+ extraction logic should be much simpler here).

I’m not sure if I’d go that route where it’s done on runtime every request.

Totally agree.

A simple solution for field level/field resolver plugins/middleware seems
like the best user experience. I’d like to see it everywhere just so it’s
consistent (the ability to create a custom decorator ‘@Auth(Roles.Admin)’

This should be feasible with extensions + global "field middleware" (or whatever name we choose for it).

Anyways, I've just figured that we could add a new middleware property to the @Field() decorator allowing for a more granular registration approach (if one doesn't want to use a global middleware) without adding another decorator for this. In theory, we could allow specifying middleware on the ObjectType level as well. Thoughts?

I wouldn't be too concerned over adding middleware decorators IMO. Nest.js has NestMiddleware. We can always know that when using NestMiddleware, it'll be executed on the context of the request. NestMiddleware can also be configured to run on specific paths and methods, etc. I'm assuming that it'd be entirely hard to just make the MiddlewareConsumer compatible with GraphQL.

So I feel that it being separate isn't a bad thing. They're completely different anyway.

If I could re-write my authentication logic. I'd like to simply create a global JWT based NestMiddleware that sets loosely sets a user (etc) into the request object. I may go back in forth with maybe just doing this as a global Guard. JWT is so easy that I'd probably keep it simple and have it as a middleware in front of GraphQL so I don't have to deal with context switching between GraphQL / Controllers, then in a perfect world, have

@UseMiddleware behave almost identically to graphql-middleware and type-graphql:

export class LogAccess implements MiddlewareInterface<TSource, TContext, TArgs> {
  constructor(private readonly logger: Logger) {}

  async use({ context, info }: ResolverData<TSource, TContext, TArgs>, next: NextFn) {
    const username: string = context.username || "guest";

    this.logger.log(`Logging access: ${username} -> ${info.parentType.name}.${info.fieldName}`);

    return next();
  }
}

I don't know how this would apply to ObjectType (or InputType), but it shouldn't be too hard to just add the middleware where you're defining the resolve function for the type.

Maybe if you define it on ObjectType then it's added to every field of that type? And globalMiddleware can behave like type-graphql and execute for every Query, Mutation, Subscription, and field resolver.

The biggest reason I'm more +1 of this method is simplicity. User-land would love it as well. Extensions aren't quite for normal users but more for library creators and advanced users. I LOVE extensions. I was one of the ones who was tried fixing the federation implementation b/c extensions are the future of graphql "extensibility". But middleware would fix a lot of issues as well as make GraphQL specific itches go away (field level authentication, etc).

@kamilmysliwiec I couldn't spend too much time on it, but got started a little bit.

https://github.com/nestjs/graphql/pull/925

I'm assuming that it'd be entirely hard to just make the MiddlewareConsumer compatible with GraphQL.

I'm not saying that we should make the MiddlewareConsumer compatible with GraphQL.

My idea was to:

1) add middleware (or any other name we choose, e.g., fieldMiddleware) property to the GraphQLModule.forRoot() options = global middleware that run for each field
2) add middleware property to the @Field decorator (e.g., @Field({ middleware: [A, B, C]})) = so you can selectively attach certain middleware to specific properties
3) maybe add middleware property to the @ObjectType decorator = so all these middleware would apply to fields registered under a specific object type class

Such middleware should support DI (as any other enhancer/provider in Nest) and would wrap the resolve function. It could also look identical to what's already proposed by graphql-middleware/type-graphql.

In the above example, we don't have to introduce any other decorators + we don't confuse users with what is a "middleware" vs "graphql middleware" and why they can't use, for example, @UseMiddleware with "normal" middleware.

Generally, I think we're thinking the same way, but we have different ideas for hooking up these middleware classes :)

@kamilmysliwiec So I was experimenting with using extensions and graphql-shield to create authentication. I got everything working... until I realized that when I use transformSchema to apply the middleware, it wraps the schema causing the guards to not be called before graphql shield resolvers.

I got pretty far and was super excited b/c this seemed to be working until I realized that the guards aren't called. I was going to just omit the use of guards completely and just use extensions everywhere so I can share the same @Auth() decorator.

I could either just give up on using the @Auth() decorator on queries and mutations, and create two separate ones, @Auth() (uses Guard), and @AuthField() (for fields within ObjectTypes). This route works, but it would have been cool to just have one decorator.

I guess middleware can fix this, but also feel that this limits the ability to do cool things with extensions like this.

I've been searching for a "decent" Authorization layer for cody-first GraphQL Servers for the past few days and after realizing that:

  • Graphql-Shield seems to have issues with subcriptions and you can't have object- and field-level rules at the same time
  • Type-Graphql's "Authorized" Decorator can only be applied "on a field, query or mutation.", sadly leaving out "ObjectTypes"
  • NestJS "Guards" only work on Queries and Mutations by default, with the option to enable them for field-resolvers, but not working on object-types or generic @Fields

I cobbled together something that "Seems to work"(TM) for me. Since this discussion revolved around some of the same points of contention I had, I though I'd share my approach using:

  • NestJS's custom schemaDirectives
  • Apollo's SchemaDirectiveVisitor
  • rule-syntax that I tried to model after GraphQL-Shield (why? because I found it neat and have no imagination)
  • A lot of temporary types and WIP-code ;)

authentication-directive.ts

import {
  AuthenticationError,
  SchemaDirectiveVisitor,
} from 'apollo-server-express';
import { defaultFieldResolver, GraphQLField, GraphQLObjectType } from 'graphql';
import { RuleFunctionParam } from './rule';
import { RuleStore } from './rule-store';

export interface ExtendedGraphqlField<
  TSource,
  TContext,
  TArgs = { [key: string]: any }
> extends GraphQLField<TSource, TContext, TArgs> {
  authorizationRules?: RuleFunctionParam[];
}

export interface ExtendedGraphQLObjectType<
  TSource,
  TContext,
  TArgs = { [key: string]: any }
> extends GraphQLObjectType<TSource, TContext, TArgs> {
  authorizationRulesApplied?: boolean;
}

export class AuthDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field: ExtendedGraphqlField<any, any>) {
    const ruleName = this.args.authorizationRule;
    const rule = RuleStore.getPreResolveRule(ruleName);
    this.authorizeField(field, rule);
  }

  visitObject(object: ExtendedGraphQLObjectType<any, any>) {
    if (object.authorizationRulesApplied) return;
    object.authorizationRulesApplied = true;
    const ruleName = this.args.authorizationRule;
    const rule = RuleStore.getPreResolveRule(ruleName);
    const fields = object.getFields();
    Object.keys(fields).forEach((fieldName) => {
      this.authorizeField(fields[fieldName], rule);
    });
  }

  private authorizeField(
    field: ExtendedGraphqlField<any, any>,
    rule: RuleFunctionParam
  ) {
    if (field.authorizationRules) {
      field.authorizationRules.push(rule);
    } else {
      field.authorizationRules = [rule];

      const originalResolve = field.resolve || defaultFieldResolver;
      field.resolve = async (source, args, context, info) => {
        const rules = field.authorizationRules;
        if (!rules)
          return originalResolve.apply(this, [source, args, context, info]);

        for (let i = 0; i < rules.length; i += 1) {
          const assignedRule = rules[i];
          const isAuthorized = await assignedRule(source, args, context, info);
          if (!isAuthorized) {
            throw new AuthenticationError(
              `Authorization failed for field ${field.name}`
            );
          }
        }
        return originalResolve.apply(this, [source, args, context, info]);
      };
    }
  }
}

rule.ts

import { GraphQLResolveInfo } from 'graphql';
import { v4 as uuidV4 } from 'uuid';
import { RuleStore } from './rule-store';

type preResolveRule = () => () => boolean;

export type RuleFunctionParam<TContext = any, TSource = any, TArgs = any> = (
  source: TSource,
  args: TArgs,
  context: TContext,
  info: GraphQLResolveInfo
) => Promise<boolean>;

export type RuleFunction<TContext, TSource, TArgs> = (
  ruleFunction: RuleFunctionParam<TContext, TSource, TArgs>
) => boolean;

export interface RuleArgs {
  name?: string;
}

export function preResolveRule<
  TContext extends {},
  TSource extends {},
  TArgs extends {}
>(args?: RuleArgs) {
  const ruleFunction = function (
    ruleFunctionParam: RuleFunctionParam<any, any, any>
  ) {
    const id = args?.name || uuidV4();
    RuleStore.preResolveRules[id] = ruleFunctionParam;
    return id;
  };
  return ruleFunction;
}

rule-store

import { RuleNotFoundError } from './errors';
import { RuleFunctionParam } from './rule';

export class RuleStore {
  static preResolveRules: Record<string, RuleFunctionParam> = {};

  static getPreResolveRule(id: string) {
    const rule = this.preResolveRules[id];
    if (!rule) throw new RuleNotFoundError(id);

    return rule;
  }
}

It can be initialized inside an app-module like this:

@Module({
  imports: [
    // ...
    GraphQLModule.forRoot({
      installSubscriptionHandlers: true,
      autoSchemaFile: `${__dirname}/schema.gql`,
      schemaDirectives: {
        Authorized: AuthDirective,
      },
    }),
   // ...
})
export class AppModule {}

Rule Definition:

import { preResolveRule } from '@monorepo/graphql/authentication-directive';

export const demoRule = preResolveRule()(
  async (source, args, context, info) => {
    return false; // I'd advise using some more complex logic here
  }
);

Aplying the rule (Obviously this would need it's own decorator to wrap this a little more neatly):

@Entity()
@ObjectType()
@Directive(`@Authorized(authorizationRule: "${demoRule}")`)
export default class User extends AppEntity {
  @Column()
  public firstName!: string;

  @Column()
  public lastName!: string;

  @Column()
  public email!: string;
}

Hopefully this can serve some of you as a rough proof-of-concept.

@Gonozal I did something similar using extensions but realized the global auth guards don't get called which is where my user + context setting logic is.

Using directives, do you know if that custom resolver gets called before or after a global auth guard? This is the problem I was seeing with getting fancy with custom resolver over-rides.

In your case, does originalResolve.apply(this, [source, args, context, info]); call the guards here or before your resolve function?

I think @kamilmysliwiec's way of middleware would be a decent start though.

Another random question to @kamilmysliwiec. In my usage (extensions) (and probably same issue as @Gonozal's example), is there a way to call a global nest.js guard on the request before executing the GraphQL query? I really didn't mind going the extension route, but it just limits me to field level stuff since wrapped resolve calls handle execution of guards, pipes, and interceptors.

And for my example usage of extensions... applyShieldMiddleware simply iterates through schema to find extensions with 'auth' and array of rules: @Extensions({ auth: ['admin'] }). I'd be able to use this on any object type. Queries, mutations, object types, field, etc. However, global auth guards don't get set, so I'd have to not use them and do authentication logic manually on the context function.

// apply-shield-middleware.ts

import { UnauthorizedException } from '@nestjs/common';
import { applyMiddleware } from 'graphql-middleware';
import {rule, shield, allow, IRule, or} from 'graphql-shield';
import { GraphQLSchema, GraphQLObjectType, isObjectType } from 'graphql';
import { AuthUser } from "../interfaces";

const ruleCache = new Map<string, IRule>();

/**
 * Creates a graphql-shield "rule" based off the given user "role".  Caches
 * role for re-use.
 */
function createShieldRuleForRole(role: string = 'default') {
    if (ruleCache.has(role)) {
        return ruleCache.get(role);
    }

    const validate: import('graphql-shield/dist/types').IRuleFunction = async (_parent: any, _args: any, ctx: any) => {
        const user: AuthUser = ctx.req?.user;

        if (!user) {
            return new UnauthorizedException();
        }

        // user can be anything
        if (role === 'default') {
            return true;
        }

        if (user && user.hasRole(role)) {
            return true;
        }

        return new UnauthorizedException();
    };

    ruleCache.set(role, rule({ cache: 'contextual' })(validate));

    return ruleCache.get(role);
}

/**
 * Iterates over all the GraphQL Schema's ObjectTypes and checks
 * if they have the "roles" extension, then add's specific role.
 */
export function applyShieldMiddleware(schema: GraphQLSchema) {
    const typeMap = schema.getTypeMap();

    const ruleTree = Object.keys(typeMap)
        .filter((type) => isObjectType(typeMap[type]) && !['Mutation'].includes(typeMap[type].name) && !typeMap[type].name.startsWith('__'))
        .reduce<any>((ruleTree, typeName) => {
            const type = typeMap[typeName] as GraphQLObjectType;

            const fieldMap = type.getFields();

            const fieldRules = Object.keys(fieldMap)
                .filter(fieldName => {
                    const roles = (fieldMap[fieldName]?.extensions || {}).auth;

                    return Array.isArray(roles) && roles.length;
                })
                .reduce<any>((fieldRules, fieldName) => {
                    const roles = fieldMap[fieldName].extensions.auth;
                    const rules = roles.map(createShieldRuleForRole);

                    return {
                        ...fieldRules,
                        // no need for "or" if there's only one rule.
                        [fieldName]: rules.length === 1 ? rules[0] : or(...rules)
                    }
                }, {});

            if (Object.keys(fieldRules).length) {
                return {
                    ...ruleTree,
                    [typeName]: fieldRules
                };
            } else {
                return ruleTree;
            }
        }, {});

    const permissions = shield(ruleTree, {
        fallbackRule: allow
    });

    schema = applyMiddleware(schema, permissions);

    return schema;
}

// then to setup using `transformSchema`

GraphQLModule.forRoot({
  installSubscriptionHandlers: true,
  autoSchemaFile: 'schema.gql',
  introspection: true,
  playground: true,
  transformSchema: (schema) => {
    schema = applyShieldMiddleware(schema);

    return schema;
  },
  context: ({ req }) => ({ req }),
  debug: false
}),

@Gonozal I modified my above example using extensions to allow for ObjectType as well. I ended up removing all of my shields / passport stuff in favor of a plain Nest.js middleware. My Middleware now does the JWT authentication using @nestjs/jwt, sets the user to the request, and done. Everything works using one decorator. Just can't use extensions on the GraphQL resolver class to automatically add them to each resolver function, but that's fine for by me!

@Gonozal I did something similar using extensions but realized the global auth guards don't get called which is where my user + context setting logic is.

Using directives, do you know if that custom resolver gets called before or after a global auth guard? This is the problem I was seeing with getting fancy with custom resolver over-rides.

In your case, does originalResolve.apply(this, [source, args, context, info]); call the guards here or before your resolve function?

Well, this seemed easy enough to test, so I gave it a shot. The answer is a definite "Well, it depends".

main.ts:

import {
  CanActivate,
  ExecutionContext,
  Injectable,
  ValidationPipe,
} from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { Observable } from 'rxjs';
import { AppModule } from './app.module';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext
  ): boolean | Promise<boolean> | Observable<boolean> {
    console.log('auth guard');
    return true;
  }
}

async function bootstrap() {
  const app = await NestFactory.create(AppModule, { logger: ['verbose'] });
  app.useGlobalPipes(new ValidationPipe());
  app.useGlobalGuards(new AuthGuard());

  await app.listen(3000);
  console.log(`Application is running on: ${await app.getUrl()}`);
}

bootstrap();

user.authorization.ts

import { preResolveRule } from '@monorepo/graphql/authentication-directive';

export const demoRule = preResolveRule()(
  async (source, args, context, info) => {
    console.log('rule evaluated');
    return true;
  }
);

If you apply the rule (see my comment above) to an object-type (or field of an object-type), the custom directive logic gets executed after:

  • Global Guards
  • The resolver did it's thing (so any db-query to retrieve the decorated object is executed, which makes sense now that I think about it)

console output:

auth guard
query: SELECT "User"."id" AS "User_id", [...]
rule evaluated
rule evaluated
[...]

If you apply the rule to a query or mutation, it get's executed before:

  • Global Guards (bummer)
  • The resolver
rule evaluated
auth guard
query: SELECT "User"."id" AS "User_id", [...]
[...]

BUT: You could switch the logic in the authentication-directive.ts a little bit:

 // ...
      field.resolve = async (source, args, context, info) => {
        const preResolveRules = field.preResolveRules;
        const resolvedValue = await originalResolve.apply(this, [
          source,
          args,
          context,
          info,
        ]);
        if (!preResolveRules) return resolvedValue;
        for (let i = 0; i < preResolveRules.length; i += 1) {
          const preResolveRule = preResolveRules[i];
          const isAuthorized = await preResolveRule(
            source,
            args,
            context,
            info
          );
          if (!isAuthorized) {
            throw new AuthenticationError(
              `Authorization failed for field ${field.name}`
            );
          }
        }

        return resolvedValue;
      };
// ...

In which case the directive is executed after the auth guard, but also after the resolver did it's thing (ok-ish for queries, terrible for mutations).

Overall I'm afraid this isn't a very satisfying answer, even for my use case this now seems more problematic than before.

Maybe global middleware could be used to handle authentication and "preparing" the user-context, though it'd probably go against quite a few NestJS best practices.

For me however, Authorization has always become a major source of distributed, hidden and
hard to understand complexity in larger apps, so I'm still set on making an approach like this work, even if it'll be a little "hacky".

We had similar issues getting access control setup on a field level with nestjs. I opted to go for shield with graphql middleware, and with some workarounds it looks like it works. We are doing shield configuration separate from our models for now, but you should be able to generate it using extensions (see @j 's post above).

I will be sharing what I ran into and how I fixed it here. I can also add a PR for the documentation to add a recipe for this, but some of this is still a bit hacky. Also, if this is off topic, feel free to let me know and I will post it in a more appropriate place. I think this post is more to illustrate what we can use the current graphql-middleware for outside of most of the nestjs constructs.

Part of the reason I went with shield is because we needed the returned objects in our rules, and I wanted to use the fragment replacement feature of shield. Below is how I set that up.

import { wrapSchema } from '@graphql-tools/wrap';
import { ReplaceFieldWithFragment } from '@graphql-tools/delegate';
import { applyMiddleware } from 'graphql-middleware';
import { appPermissions } from './rules';
...
      transformSchema: schema => {
        const newSchema = applyMiddleware(schema, appPermissions);
        if (newSchema.schema && newSchema.fragmentReplacements) {
          const transforms = [
            new ReplaceFieldWithFragment(
              newSchema.schema,
              newSchema.fragmentReplacements,
            ),
          ];
          const finalSchema = wrapSchema(newSchema.schema, transforms);
          return finalSchema;
        } else {
          return newSchema;
        }
      },

This allows me to add graphql shield rules on types, along with fragments. Unfortunately the issue where guards only runs after shield execution for queries and mutations is still a problem. After trying a couple of different approaches, I ended up creating my own graphql middleware that runs the passport logic (in a similar way the AuthGuard does) before the shield middleware runs.

import { Request, Response } from 'express';
import { authenticate } from 'passport';
import { User } from 'src/common/interfaces/user.interface';
import { IMiddlewareFunction } from 'graphql-middleware';
import { GraphQLResolveInfo } from 'graphql';
import { AuthenticationError } from 'apollo-server-express';

export const gqlAuthMiddleware: IMiddlewareFunction<
  unknown,
  { req: Request; res: Response },
  unknown
> = async (resolve, root, args, context, info: GraphQLResolveInfo) => {
  const passportFn = createPassportContext(context.req, context.res);
  try {
    const user = await passportFn(
      'jwt',
      { session: false },
      (err, user, info, ctx, status) =>
        handleRequest(err, user, info, ctx, status),
    );
    context.req.user = user;
    return resolve(root, args, context, info) as unknown;
  } catch (err) {
    throw err;
  }
};

const handleRequest = (
  err,
  user: User | false,
  info: Record<string, string> | null,
  _context,
  _status,
): User => {
  if (err || !user) {
    throw (
      err || new AuthenticationError(info?.message || 'authentication failed')
    );
  }
  return user;
};

/* eslint-disable */
const createPassportContext = (request: Request, response) => (
  type,
  options,
  callback: (...args: any[]) => User,
): Promise<User> => {
  return new Promise<User>((resolve, reject) =>
    authenticate(type, options, (err, user, info, status) => {
      try {
        request.authInfo = info;
        return resolve(callback(err, user, info, status));
      } catch (err) {
        reject(err);
      }
    })(request, response, err => (err ? reject(err) : resolve())),
  );
};
/* eslint-enable */

used in the transform schema like this:

        const newSchema = applyMiddleware(
          schema,
          { Query: gqlAuthMiddleware, Mutation: gqlAuthMiddleware },
          appPermissions,
        );

I am sure some of this can be simplified, since I am not dynamically creating the middleware like the nestjs passport module does.
The big downside of this approach is that the authentication logic will run on every query or mutation. This is not a problem for us but tweaking this to fit that use-case should not be too difficult.

Another issue I ran into were that shield sometimes returns an error, nested in an object, resulting in a null exception for fields where data were returned, but because of the nested error return this code did not find the error, and masked the actual error with a null returned for non-null error.

https://github.com/graphql/graphql-js/blob/156c76ed77dc0d9b2dc3c988264d44763e8334aa/src/execution/execute.js#L778-L781

I am not sure what the actual reason for this is, if it is related to how nest adds resolvers to the graphql schema, or if it is related to the cursor pagination pattern that we are using. As a workaround I added a plugin method that traverses the nested object and throws if it finds the nested error.

import { PluginDefinition } from 'apollo-server-core';
import { GraphQLNonNull } from 'graphql';

const getNestedError = (source: unknown, depth = 0): Error | null => {
  if (depth > 10) {
    return null;
  }
  if (source instanceof Error) {
    return source;
  } else if (source instanceof Array) {
    return (
      source
        .map(x => getNestedError(x, depth + 1))
        .find(x => x instanceof Error) || null
    );
  } else if (source instanceof Object) {
    return (
      Object.values(source)
        .map(x => getNestedError(x, depth + 1))
        .find(x => x instanceof Error) || null
    );
  } else {
    return null;
  }
};

export const throwNestedErrorPlugin: PluginDefinition = {
  requestDidStart() {
    return {
      executionDidStart() {
        return {
          willResolveField(ctx) {
            const returnType = ctx.info.returnType;
            const source: unknown = ctx.source;
            return (err, result) => {
              if (!result && returnType instanceof GraphQLNonNull) {
                const err = getNestedError(source);
                if (err instanceof Error) {
                  throw err;
                }
              }
            };
          },
        };
      },
    };
  },
};

and using it in the config like this

       plugins: [throwNestedErrorPlugin],

I hope this is of help to someone, and helps illustrate at least one use-case when adding this feature to nest.

Field middleware feature was added in one of the recent releases https://docs.nestjs.com/graphql/field-middleware :)

Was this page helpful?
0 / 5 - 0 ratings