graphql_helpers.rb 4.7 KB
Newer Older
N
Nick Thomas 已提交
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

N
Nick Thomas 已提交
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)
N
Nick Thomas 已提交
16 17
  end

18
  # Runs a block inside a BatchLoader::Executor wrapper
N
Nick Thomas 已提交
19 20
  def batch(max_queries: nil, &blk)
    wrapper = proc do
21 22
      begin
        BatchLoader::Executor.ensure_current
B
Bob Van Landuyt 已提交
23
        yield
24 25
      ensure
        BatchLoader::Executor.clear_current
N
Nick Thomas 已提交
26 27
      end
    end
B
Bob Van Landuyt 已提交
28

N
Nick Thomas 已提交
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
B
Bob Van Landuyt 已提交
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 83
      #{name}#{attributes}
      #{wrap_fields(fields)}
84 85 86
    QUERY
  end

87 88 89 90 91 92 93 94 95 96
  def wrap_fields(fields)
    return unless fields.strip.present?

    <<~FIELDS
    {
      #{fields}
    }
    FIELDS
  end

97
  def all_graphql_fields_for(class_name, parent_types = Set.new)
98
    type = GitlabSchema.types[class_name.to_s]
B
Bob Van Landuyt 已提交
99 100 101
    return "" unless type

    type.fields.map do |name, field|
102
      # We can't guess arguments, so skip fields that require them
B
Bob Van Landuyt 已提交
103
      next if required_arguments?(field)
104

105 106 107 108 109 110
      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)

B
Bob Van Landuyt 已提交
111
      if nested_fields?(field)
112 113 114 115
        fields =
          all_graphql_fields_for(singular_field_type, parent_types | [type])

        "#{name} { #{fields} }"
B
Bob Van Landuyt 已提交
116 117
      else
        name
B
Bob Van Landuyt 已提交
118
      end
119
    end.compact.join("\n")
B
Bob Van Landuyt 已提交
120 121
  end

122 123 124 125 126 127
  def attributes_to_graphql(attributes)
    attributes.map do |name, value|
      "#{GraphqlHelpers.fieldnamerize(name.to_s)}: \"#{value}\""
    end.join(", ")
  end

128 129
  def post_graphql(query, current_user: nil, variables: nil, headers: {})
    post api('/', current_user, version: 'graphql'), params: { query: query, variables: variables }, headers: headers
130 131 132 133
  end

  def post_graphql_mutation(mutation, current_user: nil)
    post_graphql(mutation.query, current_user: current_user, variables: mutation.variables)
134 135 136 137 138 139 140
  end

  def graphql_data
    json_response['data']
  end

  def graphql_errors
141 142 143 144 145
    json_response['errors']
  end

  def graphql_mutation_response(mutation_name)
    graphql_data[GraphqlHelpers.fieldnamerize(mutation_name)]
B
Bob Van Landuyt 已提交
146 147
  end

B
Bob Van Landuyt 已提交
148 149 150 151
  def nested_fields?(field)
    !scalar?(field) && !enum?(field)
  end

B
Bob Van Landuyt 已提交
152 153 154 155
  def scalar?(field)
    field_type(field).kind.scalar?
  end

B
Bob Van Landuyt 已提交
156 157 158 159 160 161 162 163
  def enum?(field)
    field_type(field).kind.enum?
  end

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

B
Bob Van Landuyt 已提交
164
  def field_type(field)
165 166 167 168 169 170
    field_type = field.type

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

    field_type
B
Bob Van Landuyt 已提交
174
  end
N
Nick Thomas 已提交
175
end