1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
|
module GraphqlHelpers
MutationDefinition = Struct.new(:query, :variables)
# 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
# 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 = 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
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)
return unless fields.strip.present?
<<~FIELDS
{
#{fields}
}
FIELDS
end
def all_graphql_fields_for(class_name, parent_types = Set.new)
allow_unlimited_graphql_complexity
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_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
json_response['errors']
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 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
end
|