Apollo-server: field.resolve in Directive is just broken

Created on 12 Apr 2019  路  14Comments  路  Source: apollographql/apollo-server

Have a schema like this:

extend type Query {
    paymentList(filter: String skip: Int pageSize: Int): PaymentList @auth
}

directive @auth on FIELD_DEFINITION

And have directive resolver:

import { defaultFieldResolver } from 'graphql';
import { SchemaDirectiveVisitor } from 'apollo-server';  

class AuthDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field) {
    const { resolve = defaultFieldResolver } = field;
    const { role } = this.args;
    console.log('visit field def');
    field.resolve = async function(...args) {
      console.log('field.resolve');
    };
  }
}

export default AuthDirective;

Used in makeExecutableSchema:

import AuthDirective from './directives/Auth';

export const schema = makeExecutableSchema({
  typeDefs,
  resolvers,
  schemaDirectives: {
    auth: AuthDirective
  }
});

When I run the server, I can see text from console.log('visit field def');, however when I query from client, inside of field.resolve never invoking.

Using Apollo-Server @2.0.7 with [email protected] and [email protected].

Most helpful comment

Hello?

All 14 comments

I tested with latest [email protected], but still not working.

Hello?

One tip for debugging directives until you understand the particulars, is to overload the definition.

On the directive, add some or all of the locations enums such as

... on FIELD_DEFINITION | FIELD | OBJECT | ...

{
  QUERY:                    'Location adjacent to a query operation.',
  MUTATION:                 'Location adjacent to a mutation operation.',
  SUBSCRIPTION:             'Location adjacent to a subscription operation.',
  FIELD:                    'Location adjacent to a field.',
  FRAGMENT_DEFINITION:      'Location adjacent to a fragment definition.',
  FRAGMENT_SPREAD:          'Location adjacent to a fragment spread.',
  INLINE_FRAGMENT:          'Location adjacent to an inline fragment.',
  SCHEMA:                   'Location adjacent to a schema definition.',
  SCALAR:                   'Location adjacent to a scalar definition.',
  OBJECT:                   'Location adjacent to an object type definition.',
  FIELD_DEFINITION:         'Location adjacent to a field definition.',
  ARGUMENT_DEFINITION:      'Location adjacent to an argument definition.',
  INTERFACE:                'Location adjacent to an interface definition.',
  UNION:                    'Location adjacent to a union definition.',
  ENUM:                     'Location adjacent to an enum definition.',
  ENUM_VALUE:               'Location adjacent to an enum value definition.',
  INPUT_OBJECT:             'Location adjacent to an input object type definition.',
  INPUT_FIELD_DEFINITION:   'Location adjacent to an input object field definition.',
}

And extend your class and add console.logs/debug to the available methods.

Then you can see which are firing and when etc.

class SomeDirective extends SchemaDirectiveVisitor {
  visitSchema(schema: GraphQLSchema) {}
  visitObject(object: GraphQLObjectType) {}
  visitFieldDefinition(field: GraphQLField<any, any>) {}
  visitArgumentDefinition(argument: GraphQLArgument) {}
  visitInterface(iface: GraphQLInterfaceType) {}
  visitInputObject(object: GraphQLInputObjectType) {}
  visitInputFieldDefinition(field: GraphQLInputField) {}
  visitScalar(scalar: GraphQLScalarType) {}
  visitUnion(union: GraphQLUnionType) {}
  visitEnum(type: GraphQLEnumType) {}
  visitEnumValue(value: GraphQLEnumValue) {}
}

I'm having the same issue. Field.resolve does not seem to be called. Here is my code:

schema.js:

  directive @auth on FIELD_DEFINITION
  type Team {
    team_id: Int!
    name: String! @auth
    hostname: String!
    created_at: GraphQLDateTime
  }

app.js:

var { AuthDirective } = require('./data/directives');
...
const schema = makeExecutableSchema({
  typeDefs,
  resolvers,
  schemaDirectives: {
    auth: AuthDirective
  }
});

directives.js:

var { SchemaDirectiveVisitor } = require('graphql-tools'),
  { defaultFieldResolver } = require('graphql-tools');

class AuthDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field, details) {
    console.log('visitFieldDef', field.name, details);
    const { resolve = defaultFieldResolver } = field;
    field.resolve = async function(...args) {
      console.log('Resolving field.');
      throw new Error('no auth!');
      return resolve.apply(this, args);
    };
  }
}

module.exports = {
  AuthDirective
};

I see the initial console log "visitFieldDef name { objectType: Team }" on server startup, but the log "Resolving field" for field.resolve never gets called when querying my Team data.

apollo-server-express version 2.5.0
graphql-tools version 4.0.4

Even I'm having the same issue, field.resolve never gets called

AuthDirective.js

// @flow

import {SchemaDirectiveVisitor} from "apollo-server";

class AuthDirective extends SchemaDirectiveVisitor {
    visitObject(object) {
        console.log("in visitObject");
        this.ensureFieldsWrapped(object);
    }

    visitFieldDefinition(field) {
        const {resolve} = field;

        console.log("in visitFieldDefinition");

        field.resolve = async function (...args) {
            console.log("in auth resolver");
            const context = args[2];
            const requiredRole = "ADMIN";

            const user = await getUser(context.headers.authToken);
            if (!user.hasRole(requiredRole)) {
                throw new Error("not authorized");
            }

            return resolve.apply(this, args);
        };
    }

    ensureFieldsWrapped(objectType) {

        const fields = objectType.getFields();

        Object.keys(fields).forEach(fieldName => {
            const field = fields[fieldName];
            const {resolve} = field;
            field.resolve = async function (...args) {

                console.log("in auth resolver");

                const context = args[2];
                const requiredRole = "ADMIN";

                const user = await getUser(context.headers.authToken);
                if (!user.hasRole(requiredRole)) {
                    throw new Error("not authorized");
                }

                return resolve.apply(this, args);
            };
        });
    }
}

export default AuthDirective;
const RootQuery= `
    directive @auth(
        requires: String
    ) on FIELD | FIELD_DEFINITION | OBJECT | QUERY

    type Campaign {
        name: String @auth(requires: "ADMIN")
    }

    type Query {
        campaigns: [Campaign]
    }
`; 

async function getMergedSchema() {
    const remoteSchema = await makeMergedRemoteSchema(); // introspects microservice schemas using makeRemoteExecutableSchema

    const schema = makeExecutableSchema({
        typeDefs: RootQuery,
        schemaDirectives: {
            auth: AuthDirective
        }
    });

    return [
        ...remoteSchema,
        schema
    ];
}

//app.js
 const schemas = await getMergedSchema();
 const mergedSchema = mergeSchemas({
        schemas,
        resolvers: resolvers,
        mergeDirectives: true
 }});

 const server = new ApolloServer({
            schema: mergedSchema,
            debug: true,
            introspection: true,
            tracing: true,
            engine: {
                apiKey: config.get("engine.apiKey"),
                schemaTag: process.env.NODE_CONFIG_ENV,
                generateClientInfo: ({request}) => {
                    const headers = request.http && request.http.headers;
                    if (headers) {
                        return {
                            clientName: headers.get("X-clientId"),
                            clientVersion: headers.get("X-AppVersion"),
                        };
                    } else {
                        return {
                            clientName: "Unknown",
                            clientVersion: "Unknown"
                        };
                    }
                }
            },
            cacheControl: true,
            playground: {
                settings: {
                    "editor.theme": "dark",
                },
            },
            persistedQueries: {
                cache: new MemcachedCache(
                    ['memcached-server'],
                    {retries: 10, retry: 10000, ttl: 10000},
                ),
            },
            subscriptions: {
                path: "/sub"
            },
            context: ({req}) => ({
                request: req
            })
        });

        server.applyMiddleware({
            app,
            gui: true,
            bodyParserConfig: {limit: "50mb"},
            cors: {
                origin: "*"
            },
            onHealthCheck: () => new Promise((resolve) => {
                //database check or other asynchronous action
                resolve();
            })
        });

        const httpServer = http.createServer(app);
        server.installSubscriptionHandlers(httpServer);

        httpServer.listen({
                port: ((port: any): number)
            }, () => {
                Logger.info(`馃殌 Server ready at http://localhost:${port}${server.graphqlPath}`);
                Logger.info(`Try your health check at: http://localhost:${port}/.well-known/apollo/server-health`);
            }
        )

I'm using the following packages

  {
     ...,
     "dependencies": {
       "@types/graphql": "14.2.0",
       "apollo-engine": "1.1.2",
       "apollo-fetch": "0.7.0",
       "apollo-link-context": "1.0.17",
       "apollo-link-http": "1.5.14",
       "apollo-server": "2.6.0-alpha.0",
       "apollo-server-cache-memcached": "0.4.0",
       "apollo-server-express": "2.6.0-alpha.0",
       "apollo-server-memcached": "0.1.0-rc.10",
       "express": "4.16.4",
       "graphql": "14.3.1",
       "graphql-resolve-batch": "1.0.2",
       "graphql-subscriptions": "1.1.0",
       "graphql-tag": "2.10.1",
       "graphql-tools": "5.0.0-rc.1",
       "node-fetch": "2.5.0",
       "request": "2.88.0",
       "subscriptions-transport-ws": "0.9.16",
        "ws": "5.2.1",
        ...
     }
  }

Anyone solve this, getting this issue to.

I had the same issues and this seems to work for me.

Package.json

{
  "dependencies": {
    "apollo-server-express": "^2.4.8",
    "graphql": "^14.2.1",
  }
}

Directives in schema

directive @authenticated on OBJECT | FIELD_DEFINITION
directive @hasScope(scope: String) on OBJECT | FIELD_DEFINITION
directive @hasRole(role: String) on OBJECT | FIELD_DEFINITION

type Query {
  authenticated: AuthenticatedUserResponse @authenticated
  user(id: ID): User @hasScope(scope: "view:user")
  users: [User!]! @hasRole(role: "admin")
}

type Mutation {
  createUser(input: CreateUserInput!): User @hasScope(scope: "create:user")
}

Server.ts

  const schema = makeExecutableSchema({
    resolvers,
    typeDefs,
    schemaDirectives: {
      authenticated: AuthenticatedDirective,
      hasRole: HasRoleDirective,
      hasScope: HasScopeDirective
    }
  });

  const apolloServer = new ApolloServer({
    schema,
    context: ({ req, connection }: any) => {
      // check if its from websocket or http
      const token = connection
        ? connection.context["authorization"]
        : req.headers["authorization"];

      if (token) {
        const verified = AuthService.jwt.verify(token);

        if (verified && verified.user) {
          return {
            user: verified.user,
          };
        }
      }

      return {};
    }
  });

AuthenticatedDirective.ts

import {
  SchemaDirectiveVisitor,
  AuthenticationError
} from "apollo-server-express";

export class AuthenticatedDirective extends SchemaDirectiveVisitor {
  visitObject(obj: any) {
    const fields = obj.getFields();

    Object.keys(fields).forEach(fieldName => {
      const field = fields[fieldName];
      const next = field.resolve;

      field.resolve = function(
        result: any,
        args: any,
        context: any,
        info: any
      ) {
        const { user } = context;
        if (!user) {
          throw new AuthenticationError(
            "You must be signed in to view this resource."
          );
        }
        return next(result, args, context, info);
      };
    });
  }

  visitFieldDefinition(field: any) {
    const next = field.resolve;

    field.resolve = function(result: any, args: any, context: any, info: any) {
      const { user } = context;
      if (!user) {
        throw new AuthenticationError(
          "You must be signed in to view this resource."
        );
      }
      return next(result, args, context, info);
    };
  }
}

HasRoleDirective.ts

import {
  AuthenticationError,
  SchemaDirectiveVisitor
} from "apollo-server-express";

export class HasRoleDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field: any) {
    const expectedRole = this.args.role;
    const next = field.resolve;

    field.resolve = function(result: any, args: any, context: any, info: any) {
      const { user } = context;
      if (!user) {
        throw new AuthenticationError(
          "You must be signed in to view this resource."
        );
      }
      const userRole = context.user.role;
      const hasRole = userRole.includes(expectedRole);

      if (!hasRole) {
        throw new AuthenticationError(
          "Authorization error: Incorrect permissions"
        );
      }

      return next(result, args, context, info);
    };
  }

  visitObject(obj: any) {
    const fields = obj.getFields();
    const expectedRole = this.args.role;

    Object.keys(fields).forEach(fieldName => {
      const field = fields[fieldName];
      const next = field.resolve;
      field.resolve = function(
        result: any,
        args: any,
        context: any,
        info: any
      ) {
        const { user } = context;
        if (!user) {
          throw new AuthenticationError(
            "You must be signed in to view this resource."
          );
        }

        const userRole = context.user.role;
        const hasRole = userRole.includes(expectedRole);

        if (!hasRole) {
          throw new AuthenticationError(
            "Authorization error: Incorrect permissions"
          );
        }

        return next(result, args, context, info);
      };
    });
  }
}

HasScopeDirective.ts

import {
  AuthenticationError,
  SchemaDirectiveVisitor
} from "apollo-server-express";

export class HasScopeDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field: any) {
    const expectedScope = this.args.scope;
    const next = field.resolve;

    field.resolve = function(result: any, args: any, context: any, info: any) {
      const { user } = context;
      if (!user) {
        throw new AuthenticationError(
          "You must be signed in to view this resource."
        );
      }
      const userScopes = context.user.scopes;
      const hasScope = userScopes.includes(expectedScope);

      if (!hasScope) {
        throw new AuthenticationError(
          "Authorization error: Incorrect permissions"
        );
      }

      return next(result, args, context, info);
    };
  }

  visitObject(obj: any) {
    const fields = obj.getFields();
    const expectedScope = this.args.scope;

    Object.keys(fields).forEach(fieldName => {
      const field = fields[fieldName];
      const next = field.resolve;
      field.resolve = function(
        result: any,
        args: any,
        context: any,
        info: any
      ) {
        const { user } = context;
        if (!user) {
          throw new AuthenticationError(
            "You must be signed in to view this resource."
          );
        }

        const userScopes = context.user.scopes;
        const hasScope = userScopes.includes(expectedScope);

        if (!hasScope) {
          throw new AuthenticationError(
            "Authorization error: Incorrect permissions"
          );
        }

        return next(result, args, context, info);
      };
    });
  }
}

SchemaDirectiveVisitor is part of graphql-tools, so you may want to open an issue there. Note that we're investigating a new approach for schema directives however, and will likely deprecate this API in the future.

I had this issue and was resolved by implementing placing my typeDefs and resolvers in an executableSchema with makeExecutableSchema.

Can't believe this was left hanging for 2 months, even though the Apollo docs still describe what everyone is doing as the officially supported way to implement directives. And here were are 8 or 9 months later and it's still documented that way and still not working.

But yeah, go on pretending that graphql-tools is a separate entity and not part of the same company as a way to close out more issues.

For this to work as expected, you have to add all of this code
resolvers, typeDefs, schemaDirectives: { authenticated: AuthenticatedDirective, hasRole: HasRoleDirective, hasScope: HasScopeDirective }

inside the makeExecutableSchema({}); instead of ApolloServer [options].

@martijnwalraven @arizonatribe

I think the following issue on the graphql-tools git might be of great value: https://github.com/ardatan/graphql-tools/issues/1462

If I understand it correctly they deprecated the suggested use in the Apollo docs as of v5.

Also check my comment for more details: https://github.com/ardatan/graphql-tools/issues/1462#issuecomment-629128203

@tafelnl The above issues predated the release of v5 for some time and are likely unrelated.

@yaacovCR Ah, yes I see that now too. So you are probably right that it is unrelated. Might still be useful for someone finding this on a Google search though :)

Was this page helpful?
0 / 5 - 0 ratings