Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bring attachDirectiveResolvers to graphql-tools #518

Merged
merged 32 commits into from
Dec 12, 2017

Conversation

giautm
Copy link
Contributor

@giautm giautm commented Dec 3, 2017

Description: This PR allows implementing custom directives that can be used for translation, authentication and error tracking.

API:

const directiveResolvers = {
  // directive resolvers implement
},

const schema = makeExecutableSchema({
  // ...
  directiveResolvers,
});

Example:

  • Using @upper directive to upper string from resolver:
directive @upper on FIELD_DEFINITION
type RootQuery {
  hello: String @upper
}
schema {
  query: RootQuery
}

Implement of @upper directive.

const directiveResolvers = {
  upper(
    next: NextResolver,
    src: any,
    args: { [argName: string]: any },
    context: any,
  ) {
    return next().then((str) => {
      if (typeof(str) === 'string') {
        return str.toUpperCase();
      }
      return str;
    });
  },
}
  • Using @auth directive check roles before run resolver:
directive @auth(roles: [String]) on FIELD_DEFINITION
type RootQuery {
  secret: String @auth(roles: ["admin"])
}
schema {
  query: RootQuery
}

Implement of @auth directive.

const directiveResolvers = {
  auth(
    next: NextResolver,
    src: any,
    args: { [argName: string]: any },
    context: any,
  ) {
    const { roles } = context; // We asume has roles of current user in context;
    const expectedRoles = args.roles || [];
    if (expectedRoles.length === 0 || expectedRoles.some((r) => roles.includes(r))) {
      // Call next to continues process resolver.
      return next();
    }

    // We has two options here. throw an error or return null (if field is nullable).
    throw new Error(`You are not authorized. Expected roles: ${expectedRoles.join(', ')}`);
  },
}
  • Catch internal error.
directive @errorCatch on FIELD_DEFINITION

type RootQuery {
  throwError: String @errorCatch
}
schema {
  query: RootQuery
}

Implement of @errorCatch directive.

// example from: https://github.com/thebigredgeek/apollo-resolvers
import { createError, isInstance } from 'apollo-errors';

const ExposedError = createError('ExposedError', {
  message: 'An unknown error has occurred'
});

const directiveResolvers = {
  errorCatch(
    next: NextResolver,
    src: any,
    args: { [argName: string]: any },
    context: any,
  ) {
    return next().catch((err) => {
      if (!isInstance(err)) {
        throw new ExposedError({
          // overload the message
          message: error.message
        });
      }

      throw err;
    });
  },
}

Issue: #212
TODO:

  • If this PR is a new feature, reference an issue where a consensus about the design was reached (not necessary for small changes)
  • Make sure all of the significant new logic is covered by tests
  • Rebase your changes on master so that they can be merged easily
  • Make sure all tests and linter rules pass
  • Update CHANGELOG.md with your change

@stubailo
Copy link
Contributor

stubailo commented Dec 3, 2017

@giautm hi, thanks for opening the PR! This looks like a very useful feature. I think we should make sure we agree on the API and what the directives should do. There’s been a lot of effort already put in on the code and it looks good, but we also need to go over the developer experience of using the new feature.

Can you add in your PR description:

  1. A short summary of the goals you want to achieve with this feature
  2. A code snippet or two of what it’s like to use it

@giautm
Copy link
Contributor Author

giautm commented Dec 4, 2017

@stubailo: I just update PR description and example. Please take a look. I'm not good for writing document. So can you help me writing it? Thanks.

@stubailo
Copy link
Contributor

stubailo commented Dec 4, 2017

Wow awesome, this is exactly what I was looking for! The docs are actually awesome since the best documentation is examples :)

Can you tell me a bit more what the "QUERY | FIELD" thing means?

@giautm
Copy link
Contributor Author

giautm commented Dec 4, 2017

@stubailo 👍

Note that we have a locations property, which states where the directive can be used (remember we said directives can be placed on fields, fragments, and queries). We also specify that the directive has a tag argument, which is what we’ll use to track performance in our system.

From tutorial: https://medium.com/the-graphqlhub/graphql-tour-directives-558dee4fa903

Copy link
Contributor

@stubailo stubailo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is it true that all of these examples are only FIELD_DEFINITION? I don't think these directives are used on queries anywhere in the tests.

According to the new schema definition language spec @leebyron is working on, here are the acceptable locations:

DirectiveLocation : one of QUERY SCHEMA ENUM MUTATION SCALAR ENUM_VALUE SUBSCRIPTION OBJECT INPUT_OBJECT FIELD FIELD_DEFINITION INPUT_FIELD_DEFINITION FRAGMENT_DEFINITION ARGUMENT_DEFINITION FRAGMENT_SPREAD INTERFACE INLINE_FRAGMENT UNION

Would it be fair to say that this PR specifically adds support for directives on field definitions? Since the implementation is resolver-based, it feels like it wouldn't work for any other use case.


Here's an interesting alternative design:

Directives could be allowed to operate on the entire unit that they are attached to. For example, a field directive would get to modify everything about the field it's on, using the graphql-js GraphQLField: https://github.com/graphql/graphql-js/blob/9e6d32af108abc979d07f4aac3b52808b019dabd/src/type/definition.js#L675

That way, you could actually implement stuff like the @deprecated directive using this feature (but with the current version, you couldn't).


So based on the above generalization, maybe these should be called "resolver directives" since they are specifically directives that modify how the resolver function works.

I think I need another day or so to think about this. Directives are a core part of GraphQL, and are being added to the schema language in the spec right now, so it feels important to get it right.

* Attach directive resolvers to Schema
* https://github.com/apollographql/graphql-tools/issues/212#issuecomment-324447078
*
* @author Agon Bina <agon_bina@hotmail.com>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually don't track authors in this way, since we don't have them for any other functions. Would you prefer to have some attribution other than just Git/GitHub's tracking of committers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed


describe('attachDirectives', () => {
const testSchemaWithDirectives = `
directive @upper on QUERY | FIELD
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should just be on FIELD_DEFINITION.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edited

@giautm giautm changed the title Bring attachDirectives to graphql-tools Bring attachDirectiveResolvers to graphql-tools Dec 4, 2017
@giautm
Copy link
Contributor Author

giautm commented Dec 10, 2017

@stubailo hi, are you done with any changes?

@viiiprock
Copy link

I'm waiting for this merging too :)

@giautm giautm closed this Dec 12, 2017
@giautm giautm reopened this Dec 12, 2017
@giautm
Copy link
Contributor Author

giautm commented Dec 12, 2017

Close-and-reopen for CI deploy

@locnguyenvu
Copy link

Waiting for merging

@vladshcherbin
Copy link
Contributor

@giautm One of the most wanted features, thanks! Hope it will be merged soon. 🙏

@stubailo stubailo changed the base branch from master to directives-staging December 12, 2017 18:53
@stubailo stubailo merged commit fcb89e1 into ardatan:directives-staging Dec 12, 2017
@bermanboris
Copy link

Wow, this is amazing! Thanks!

giautm pushed a commit to giautm/graphql-tools that referenced this pull request Feb 12, 2018
freiksenet pushed a commit that referenced this pull request Feb 12, 2018
* Added some doc for #518

* Update correct link to example

* Remove tabs
@benjamn
Copy link
Contributor

benjamn commented Feb 16, 2018

Follow-up to this PR, for anyone interested in visiting other kinds of schema types/fields/values: #640

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants