227 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Ruby
		
	
	
	
			
		
		
	
	
			227 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Ruby
		
	
	
	
| # frozen_string_literal: true
 | |
| 
 | |
| module GraphqlHelpers
 | |
|   MutationDefinition = Struct.new(:query, :variables)
 | |
| 
 | |
|   # makes an underscored string look like a fieldname
 | |
|   # "merge_request" => "mergeRequest"
 | |
|   def self.fieldnamerize(underscored_field_name)
 | |
|     underscored_field_name.to_s.camelize(:lower)
 | |
|   end
 | |
| 
 | |
|   # Run a loader's named resolver
 | |
|   def resolve(resolver_class, obj: nil, args: {}, ctx: {})
 | |
|     resolver_class.new(object: obj, context: ctx).resolve(args)
 | |
|   end
 | |
| 
 | |
|   # Runs a block inside a BatchLoader::Executor wrapper
 | |
|   def batch(max_queries: nil, &blk)
 | |
|     wrapper = proc do
 | |
|       BatchLoader::Executor.ensure_current
 | |
|       yield
 | |
|     ensure
 | |
|       BatchLoader::Executor.clear_current
 | |
|     end
 | |
| 
 | |
|     if max_queries
 | |
|       result = nil
 | |
|       expect { result = wrapper.call }.not_to exceed_query_limit(max_queries)
 | |
|       result
 | |
|     else
 | |
|       wrapper.call
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   def graphql_query_for(name, attributes = {}, fields = nil)
 | |
|     <<~QUERY
 | |
|     {
 | |
|       #{query_graphql_field(name, attributes, fields)}
 | |
|     }
 | |
|     QUERY
 | |
|   end
 | |
| 
 | |
|   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 = prepare_input_for_mutation(input)
 | |
| 
 | |
|     result = { input_variable_name_for_mutation(name) => graphql_input }
 | |
| 
 | |
|     # Avoid trying to serialize multipart data into JSON
 | |
|     if graphql_input.values.none? { |value| io_value?(value) }
 | |
|       result.to_json
 | |
|     else
 | |
|       result
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   # Recursively convert a Hash with Ruby-style keys to GraphQL fieldname-style keys
 | |
|   #
 | |
|   # prepare_input_for_mutation({ 'my_key' => 1 })
 | |
|   #   => { 'myKey' => 1}
 | |
|   def prepare_input_for_mutation(input)
 | |
|     input.map do |name, value|
 | |
|       value = prepare_input_for_mutation(value) if value.is_a?(Hash)
 | |
| 
 | |
|       [GraphqlHelpers.fieldnamerize(name), value]
 | |
|     end.to_h
 | |
|   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
 | |
| 
 | |
|   def query_graphql_field(name, attributes = {}, fields = nil)
 | |
|     fields ||= all_graphql_fields_for(name.classify)
 | |
|     attributes = attributes_to_graphql(attributes)
 | |
|     attributes = "(#{attributes})" if attributes.present?
 | |
|     <<~QUERY
 | |
|       #{name}#{attributes}
 | |
|       #{wrap_fields(fields)}
 | |
|     QUERY
 | |
|   end
 | |
| 
 | |
|   def wrap_fields(fields)
 | |
|     fields = Array.wrap(fields).join("\n")
 | |
|     return unless fields.present?
 | |
| 
 | |
|     <<~FIELDS
 | |
|     {
 | |
|       #{fields}
 | |
|     }
 | |
|     FIELDS
 | |
|   end
 | |
| 
 | |
|   def all_graphql_fields_for(class_name, parent_types = Set.new)
 | |
|     allow_unlimited_graphql_complexity
 | |
|     allow_unlimited_graphql_depth
 | |
| 
 | |
|     type = GitlabSchema.types[class_name.to_s]
 | |
|     return "" unless type
 | |
| 
 | |
|     type.fields.map do |name, field|
 | |
|       # We can't guess arguments, so skip fields that require them
 | |
|       next if required_arguments?(field)
 | |
| 
 | |
|       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)
 | |
| 
 | |
|       if nested_fields?(field)
 | |
|         fields =
 | |
|           all_graphql_fields_for(singular_field_type, parent_types | [type])
 | |
| 
 | |
|         "#{name} { #{fields} }"
 | |
|       else
 | |
|         name
 | |
|       end
 | |
|     end.compact.join("\n")
 | |
|   end
 | |
| 
 | |
|   def attributes_to_graphql(attributes)
 | |
|     attributes.map do |name, value|
 | |
|       "#{GraphqlHelpers.fieldnamerize(name.to_s)}: \"#{value}\""
 | |
|     end.join(", ")
 | |
|   end
 | |
| 
 | |
|   def post_multiplex(queries, current_user: nil, headers: {})
 | |
|     post api('/', current_user, version: 'graphql'), params: { _json: queries }, headers: headers
 | |
|   end
 | |
| 
 | |
|   def post_graphql(query, current_user: nil, variables: nil, headers: {})
 | |
|     post api('/', current_user, version: 'graphql'), params: { query: query, variables: variables }, headers: headers
 | |
|   end
 | |
| 
 | |
|   def post_graphql_mutation(mutation, current_user: nil)
 | |
|     post_graphql(mutation.query, current_user: current_user, variables: mutation.variables)
 | |
|   end
 | |
| 
 | |
|   def graphql_data
 | |
|     json_response['data']
 | |
|   end
 | |
| 
 | |
|   def graphql_errors
 | |
|     case json_response
 | |
|     when Hash # regular query
 | |
|       json_response['errors']
 | |
|     when Array # multiplexed queries
 | |
|       json_response.map { |response| response['errors'] }
 | |
|     else
 | |
|       raise "Unknown GraphQL response type #{json_response.class}"
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   def graphql_mutation_response(mutation_name)
 | |
|     graphql_data[GraphqlHelpers.fieldnamerize(mutation_name)]
 | |
|   end
 | |
| 
 | |
|   def nested_fields?(field)
 | |
|     !scalar?(field) && !enum?(field)
 | |
|   end
 | |
| 
 | |
|   def scalar?(field)
 | |
|     field_type(field).kind.scalar?
 | |
|   end
 | |
| 
 | |
|   def enum?(field)
 | |
|     field_type(field).kind.enum?
 | |
|   end
 | |
| 
 | |
|   def required_arguments?(field)
 | |
|     field.arguments.values.any? { |argument| argument.type.non_null? }
 | |
|   end
 | |
| 
 | |
|   def io_value?(value)
 | |
|     Array.wrap(value).any? { |v| v.respond_to?(:to_io) }
 | |
|   end
 | |
| 
 | |
|   def field_type(field)
 | |
|     field_type = field.type
 | |
| 
 | |
|     # The type could be nested. For example `[GraphQL::STRING_TYPE]`:
 | |
|     # - List
 | |
|     # - String!
 | |
|     # - String
 | |
|     field_type = field_type.of_type while field_type.respond_to?(:of_type)
 | |
| 
 | |
|     field_type
 | |
|   end
 | |
| 
 | |
|   # for most tests, we want to allow unlimited complexity
 | |
|   def allow_unlimited_graphql_complexity
 | |
|     allow_any_instance_of(GitlabSchema).to receive(:max_complexity).and_return nil
 | |
|     allow(GitlabSchema).to receive(:max_query_complexity).with(any_args).and_return nil
 | |
|   end
 | |
| 
 | |
|   def allow_unlimited_graphql_depth
 | |
|     allow_any_instance_of(GitlabSchema).to receive(:max_depth).and_return nil
 | |
|     allow(GitlabSchema).to receive(:max_query_depth).with(any_args).and_return nil
 | |
|   end
 | |
| end
 | |
| 
 | |
| # This warms our schema, doing this as part of loading the helpers to avoid
 | |
| # duplicate loading error when Rails tries autoload the types.
 | |
| GitlabSchema.graphql_definition
 |