graphql-ruby icon indicating copy to clipboard operation
graphql-ruby copied to clipboard

@export directive

Open tim-vandecasteele opened this issue 7 months ago • 3 comments
trafficstars

I was reading through the documentation and I found a few mentions of the @export directive.

I can see how you can easily create a custom directive. My plan of action would be to add the variable when the directive is resolved, but I don't know where to hook it up so you don't hit the Variable $xxx is used by anonymous query but not declared error. (I also don't know if the query is done top to bottom, so if I would add the @export directive on top and then use it later, if that would work.

Having very little experience with the source code of this gem, I was wondering if you deem it feasible to implement an @export directive with the current hooks. Any pointers in a specific direction would also be super helpful 😄.

(running the resolve and changing the variable is also not very clean at the moment in my proof of concept) I'm doing

context.query.variables.instance_eval("@storage")[arguments[:as]]

Which obviously goes deep in the internals of the implementation. But I don't know if there's a cleaner way for this?

tim-vandecasteele avatar Apr 15 '25 19:04 tim-vandecasteele

After a bit of experimentation, this seems to do the basic version of what I want, but it's very dirty. So wondering if there are better hooks/ways to do this:

module Directives
  class Export < GraphQL::Schema::Directive
    description "Exports the variable from one query to another."

    locations(
      GraphQL::Schema::Directive::FIELD,
    )

    argument :as, String, description: "The name of the variable to export"

    # Implement the Directive API
    def self.resolve(object, arguments, context)
      return_value = yield
      context.query.variables.instance_eval("@storage")[arguments[:as]] = return_value
      return_value
    end
  end
end

module GraphQL::StaticValidation::VariablesAreUsedAndDefined
  def on_directive(node, parent)
    # copy from on_operation_definition
    if node.name == "export"
        # initialize the hash of vars for this context:
        # mark variables as defined:
        usage_context = @variable_context_stack.last
        var_hash = @variable_usages_for_context[usage_context]
        var_name = node.arguments.find { |arg| arg.name == "as" }.value
        var_usage = var_hash[var_name]
        var_usage.declared_by = node
        var_usage.path = context.path
        super
    end
  end
end

tim-vandecasteele avatar Apr 15 '25 23:04 tim-vandecasteele

Hey! In general, your implementation above seems good to me. It looks like Query::Variables could be improved to have a first-class API for setting new values. As a slight improvement, you could use query.variables.instance_variable_get(:@storate) instead of instance_eval. (It does the same thing, but it avoids instance_eval which gives me the willies.)

rmosolgo avatar Apr 16 '25 15:04 rmosolgo

Thanks! It didn't feel super clean, but if you say this is the way, then I'm taking your word for it 😊.

Would there be a way to hook into the StaticValidation in a way that is actually 'supported'? For example a hook of some sorts on the directive?

tim-vandecasteele avatar Apr 16 '25 22:04 tim-vandecasteele

Both Query and Schema support setting a static validator into which you can customize the rules. So, just write your own rule and add it to the set along with all the defaults. See https://github.com/rmosolgo/graphql-ruby/pull/4529/files

gmac avatar Aug 08 '25 10:08 gmac