graphql_helpers.rb 4.5 KB
Newer Older
Nick Thomas's avatar
Nick Thomas committed
1
module GraphqlHelpers
2 3
  MutationDefinition = Struct.new(:query, :variables)

4 5 6 7 8 9 10 11 12
  # makes an underscored string look like a fieldname
  # "merge_request" => "mergeRequest"
  def self.fieldnamerize(underscored_field_name)
    graphql_field_name = underscored_field_name.to_s.camelize
    graphql_field_name[0] = graphql_field_name[0].downcase

    graphql_field_name
  end

Nick Thomas's avatar
Nick Thomas committed
13
  # Run a loader's named resolver
14 15
  def resolve(resolver_class, obj: nil, args: {}, ctx: {})
    resolver_class.new(object: obj, context: ctx).resolve(args)
Nick Thomas's avatar
Nick Thomas committed
16 17
  end

18
  # Runs a block inside a BatchLoader::Executor wrapper
Nick Thomas's avatar
Nick Thomas committed
19 20
  def batch(max_queries: nil, &blk)
    wrapper = proc do
21 22
      begin
        BatchLoader::Executor.ensure_current
23
        yield
24 25
      ensure
        BatchLoader::Executor.clear_current
Nick Thomas's avatar
Nick Thomas committed
26 27
      end
    end
28

Nick Thomas's avatar
Nick Thomas committed
29 30 31 32 33 34 35 36
    if max_queries
      result = nil
      expect { result = wrapper.call }.not_to exceed_query_limit(max_queries)
      result
    else
      wrapper.call
    end
  end
37

38
  def graphql_query_for(name, attributes = {}, fields = nil)
39 40 41 42 43 44 45
    <<~QUERY
    {
      #{query_graphql_field(name, attributes, fields)}
    }
    QUERY
  end

46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
  def graphql_mutation(name, input, fields = nil)
    mutation_name = GraphqlHelpers.fieldnamerize(name)
    input_variable_name = "$#{input_variable_name_for_mutation(name)}"
    mutation_field = GitlabSchema.mutation.fields[mutation_name]
    fields ||= all_graphql_fields_for(mutation_field.type)

    query = <<~MUTATION
      mutation(#{input_variable_name}: #{mutation_field.arguments['input'].type}) {
        #{mutation_name}(input: #{input_variable_name}) {
          #{fields}
        }
      }
    MUTATION
    variables = variables_for_mutation(name, input)

    MutationDefinition.new(query, variables)
  end

  def variables_for_mutation(name, input)
    graphql_input = input.map { |name, value| [GraphqlHelpers.fieldnamerize(name), value] }.to_h
    { input_variable_name_for_mutation(name) => graphql_input }.to_json
  end

  def input_variable_name_for_mutation(mutation_name)
    mutation_name = GraphqlHelpers.fieldnamerize(mutation_name)
    mutation_field = GitlabSchema.mutation.fields[mutation_name]
    input_type = field_type(mutation_field.arguments['input'])

    GraphqlHelpers.fieldnamerize(input_type)
  end

77
  def query_graphql_field(name, attributes = {}, fields = nil)
78 79
    fields ||= all_graphql_fields_for(name.classify)
    attributes = attributes_to_graphql(attributes)
80
    attributes = "(#{attributes})" if attributes.present?
81
    <<~QUERY
82
      #{name}#{attributes} {
83 84 85 86 87
        #{fields}
      }
    QUERY
  end

88
  def all_graphql_fields_for(class_name, parent_types = Set.new)
89
    type = GitlabSchema.types[class_name.to_s]
90 91 92
    return "" unless type

    type.fields.map do |name, field|
93
      # We can't guess arguments, so skip fields that require them
94
      next if required_arguments?(field)
95

96 97 98 99 100 101
      singular_field_type = field_type(field)

      # If field type is the same as parent type, then we're hitting into
      # mutual dependency. Break it from infinite recursion
      next if parent_types.include?(singular_field_type)

102
      if nested_fields?(field)
103 104 105 106
        fields =
          all_graphql_fields_for(singular_field_type, parent_types | [type])

        "#{name} { #{fields} }"
107 108
      else
        name
109
      end
110
    end.compact.join("\n")
111 112
  end

113 114 115 116 117 118
  def attributes_to_graphql(attributes)
    attributes.map do |name, value|
      "#{GraphqlHelpers.fieldnamerize(name.to_s)}: \"#{value}\""
    end.join(", ")
  end

119
  def post_graphql(query, current_user: nil, variables: nil)
120
    post api('/', current_user, version: 'graphql'), params: { query: query, variables: variables }
121 122 123 124
  end

  def post_graphql_mutation(mutation, current_user: nil)
    post_graphql(mutation.query, current_user: current_user, variables: mutation.variables)
125 126 127 128 129 130 131
  end

  def graphql_data
    json_response['data']
  end

  def graphql_errors
132 133 134 135 136
    json_response['errors']
  end

  def graphql_mutation_response(mutation_name)
    graphql_data[GraphqlHelpers.fieldnamerize(mutation_name)]
137 138
  end

139 140 141 142
  def nested_fields?(field)
    !scalar?(field) && !enum?(field)
  end

143 144 145 146
  def scalar?(field)
    field_type(field).kind.scalar?
  end

147 148 149 150 151 152 153 154
  def enum?(field)
    field_type(field).kind.enum?
  end

  def required_arguments?(field)
    field.arguments.values.any? { |argument| argument.type.non_null? }
  end

155
  def field_type(field)
156 157 158 159 160 161
    field_type = field.type

    # The type could be nested. For example `[GraphQL::STRING_TYPE]`:
    # - List
    # - String!
    # - String
162
    field_type = field_type.of_type while field_type.respond_to?(:of_type)
163 164

    field_type
165
  end
Nick Thomas's avatar
Nick Thomas committed
166
end