Graphql-ruby: Custom directives

Created on 7 Feb 2017  路  16Comments  路  Source: rmosolgo/graphql-ruby

It seems that custom directives are _almost_ possible with graphql-ruby, but they are missing something akin to resolve. Ideally the resolve for a directive would be able to either modify the output of the original resolve, or prevent it from being executed.

Something like this would be nice.

GraphQL::Directive.define do
  name 'bananaMe'
  description 'Does what it needs to.'
  locations [GraphQL::Directive::FIELD, GraphQL::Directive::FRAGMENT_SPREAD, GraphQL::Directive::INLINE_FRAGMENT]

  argument :force, !types.Boolean, 'Forces banana.'

  resolve ->(original_resolve, obj, args, ctx) {
    if args[:force]
      'Banana'
    else
      original_resolve(obj, args, ctx)
    end
  }
end

Right now that logic seems to exist here. It would take a real hack to change it without support.

Most helpful comment

It hit me last night while I was falling asleep, and now I confirmed, the current directive approach is flawed:

image

馃槚 (id should not be present in that response since its parent was skipped)

They're processed _after_ rewriting the query. I guess we need to skip nodes at the AST level so they never enter the rewritten tree.

I'll keep you posted on this issue but I'll need some time before I can get a powerful & maintainable public API for this!

All 16 comments

almost possible

I agree! I was looking at options while rewriting the query transformation last week but alas, left it as a # TODO.

I'm also interested in this because I want to implement @defer and @stream (and maybe @live!) in a non-intrusive, non-rewrite-everything way.

Here's kind of what I was imagining:

  1. Add a hook to schema definitions where people can opt into directives:

    MySchema = GraphQL::Schema.define do 
    # map `@banana` to user-defined `BananaDirective`
    directive banana: BananaDirective 
    end 
    
  2. Add an API for directives to modify flagged nodes (like resolve in your example above). I'm not _sure_ if wrapping resolve is enough for defer and stream because, when nodes are deferred, they should _entirely_ absent from the response, not nil. So I may need to add a way to modify the query itself.

  3. Port the current @skip and @include to this pattern, and add some kind of global GraphQL::Schema.default_directives map which includes them and applies them by default.

What do you think of something like that?

That sounds great!

I don't think having the modification API be a little complex is a huge deal either, as long as the logic can be easily contained. Writing a directive is a pretty rare activity.

It hit me last night while I was falling asleep, and now I confirmed, the current directive approach is flawed:

image

馃槚 (id should not be present in that response since its parent was skipped)

They're processed _after_ rewriting the query. I guess we need to skip nodes at the AST level so they never enter the rewritten tree.

I'll keep you posted on this issue but I'll need some time before I can get a powerful & maintainable public API for this!

It's going to be harder than that: for @defer, for example, we need:

  • Splitting the query tree based on presence / absence of @defer, maintaining everything downstream from a @defer in a separate tree
  • Runtime awareness of which parent nodes have deferred children (in separate trees), so that we can "tuck away" the underlying objects to be resumed later
  • Ability to resume execution from any (non-root) execution node

I don't want to commit to an API until I can be sure that bases are covered for known use cases. Did you have any specific ideas in mind? If you're interested in sharing, I'd be happy to consider them when I get more time to work on that.

I don't have any specific solutions to those problems, unfortuately. I've browsed the source but don't have a deep understanding of how everything works at this point.

The use-case I had in mind was to use a custom directive for a feature-flag system. Something like:

query {
  subtree @ifFeature(flag: "feature-flag") {
    ...
  }
}

Oh right, I meant _directive_ ideas, not rewriting-the-internals-ideas, so I think I'm with you there 馃槅

That's interesting -- it's like skip, but instead of getting the if: value from the query, it makes a test based on the current user?

Pretty much. We have a single codebase that runs a bunch of sites with mostly-but-not-completely overlapping feature sets. We manage that complexity with a feature flag system that also handles multivariate testing and staged rollouts.

Did you find a good way to handle this in the meantime?

Not really. Right now I'm waiting for the first graph query to return with the feature flag data, then re-fetching the branches I don't have. The directive could simplify this into one query.

Is nullability an option? I mean, could those fields return nil in cases when the current user isn't flagged for them?

(Or, maybe the relation between fields and flags isn't one-to-one?)

nil would be fine, but ya, which fields need to behind the feature flags is context-dependent.

Heya, i really want custom directives to work in a sense of meta data. Is there any way i can achieve that now? Basically just to annotate my models for my generators like how they do here:

https://www.graph.cool/graphql-up/

type Tweet {
  id: ID!
  title: String!
  author: User! @relation(name: "Tweets")
}

type User {
  id: ID!
  name: String!
  tweets: [Tweet!]! @relation(name: "Tweets")
}

There's no way you can achieve it now. It's something I'm interested in too though! If you want to hack on it, here's were schema definitions are turned into Ruby objects:

https://github.com/rmosolgo/graphql-ruby/blob/master/lib/graphql/schema/build_from_definition.rb#L54-L74

It looks like field definitions already support directives, so you can get the directive AST nodes with .directives.

@rmosolgo Just curious, how can I re use a result of a query to make another query within graphql e.g

mutation M {
   createUser(firstName: 'bob') { id }
   createPreferences(user_id: #createUser.id, preferences: "doesn't appreciate purple") {
       success
    }
}

how can I reuse the id from createUser in createPreferences

Runtime directives are available in 1.9.x: https://graphql-ruby.org/type_definitions/directives.html#custom-directives

Was this page helpful?
0 / 5 - 0 ratings