graphqlite 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +46 -0
- data/LICENSE +21 -0
- data/README.md +339 -0
- data/lib/graphqlite/errors.rb +7 -0
- data/lib/graphqlite/executor.rb +380 -0
- data/lib/graphqlite/introspection.rb +222 -0
- data/lib/graphqlite/lexer.rb +266 -0
- data/lib/graphqlite/parser.rb +354 -0
- data/lib/graphqlite/schema.rb +238 -0
- data/lib/graphqlite/types.rb +336 -0
- data/lib/graphqlite/validator.rb +183 -0
- data/lib/graphqlite/version.rb +3 -0
- data/lib/graphqlite.rb +18 -0
- metadata +85 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
module GraphQLite
|
|
2
|
+
# Executor executes GraphQL operations against a schema
|
|
3
|
+
class Executor
|
|
4
|
+
attr_reader :schema
|
|
5
|
+
|
|
6
|
+
def initialize(schema)
|
|
7
|
+
@schema = schema
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def execute(document, variables: {}, operation_name: nil, context: {})
|
|
11
|
+
# Find the operation to execute
|
|
12
|
+
operation = find_operation(document, operation_name)
|
|
13
|
+
raise ExecutionError, "No operation found" unless operation
|
|
14
|
+
|
|
15
|
+
# Get the root type based on operation type
|
|
16
|
+
root_type = case operation.operation_type
|
|
17
|
+
when 'query'
|
|
18
|
+
@schema.query_type
|
|
19
|
+
when 'mutation'
|
|
20
|
+
@schema.mutation_type
|
|
21
|
+
when 'subscription'
|
|
22
|
+
@schema.subscription_type
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
raise ExecutionError, "Schema does not support #{operation.operation_type}" unless root_type
|
|
26
|
+
|
|
27
|
+
# Coerce variable values
|
|
28
|
+
variable_values = coerce_variable_values(operation.variable_definitions, variables)
|
|
29
|
+
|
|
30
|
+
# Execute the operation
|
|
31
|
+
data = execute_selection_set(
|
|
32
|
+
operation.selection_set,
|
|
33
|
+
root_type,
|
|
34
|
+
nil, # root value
|
|
35
|
+
variable_values,
|
|
36
|
+
context
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
{ 'data' => data }
|
|
40
|
+
rescue ExecutionError => e
|
|
41
|
+
{ 'data' => nil, 'errors' => [{ 'message' => e.message }] }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def find_operation(document, operation_name)
|
|
47
|
+
operations = document.definitions.select { |d| d.is_a?(Parser::OperationDefinition) }
|
|
48
|
+
|
|
49
|
+
if operation_name
|
|
50
|
+
operations.find { |op| op.name == operation_name }
|
|
51
|
+
elsif operations.length == 1
|
|
52
|
+
operations.first
|
|
53
|
+
else
|
|
54
|
+
raise ExecutionError, "Must provide operation name if query contains multiple operations"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def coerce_variable_values(variable_definitions, variables)
|
|
59
|
+
return {} if variable_definitions.empty?
|
|
60
|
+
|
|
61
|
+
variable_values = {}
|
|
62
|
+
|
|
63
|
+
variable_definitions.each do |var_def|
|
|
64
|
+
var_name = var_def.variable.name
|
|
65
|
+
var_type = resolve_type_ref(var_def.type)
|
|
66
|
+
default_value = var_def.default_value
|
|
67
|
+
|
|
68
|
+
if variables.key?(var_name) || variables.key?(var_name.to_sym)
|
|
69
|
+
value = variables[var_name] || variables[var_name.to_sym]
|
|
70
|
+
variable_values[var_name] = coerce_input_value(value, var_type)
|
|
71
|
+
elsif default_value
|
|
72
|
+
variable_values[var_name] = coerce_literal_value(default_value, var_type)
|
|
73
|
+
elsif var_type.is_a?(Types::NonNullType)
|
|
74
|
+
raise ExecutionError, "Variable $#{var_name} is required but not provided"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
variable_values
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def execute_selection_set(selection_set, object_type, object_value, variable_values, context)
|
|
82
|
+
return nil unless selection_set
|
|
83
|
+
|
|
84
|
+
# Collect fields
|
|
85
|
+
fields = collect_fields(selection_set, object_type, variable_values)
|
|
86
|
+
|
|
87
|
+
# Execute fields
|
|
88
|
+
result = {}
|
|
89
|
+
|
|
90
|
+
fields.each do |response_key, field_list|
|
|
91
|
+
field_value = execute_field(object_type, object_value, field_list, variable_values, context)
|
|
92
|
+
result[response_key] = field_value unless field_value == :skip
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
result
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def collect_fields(selection_set, object_type, variable_values, visited_fragments = {})
|
|
99
|
+
fields = Hash.new { |h, k| h[k] = [] }
|
|
100
|
+
|
|
101
|
+
selection_set.selections.each do |selection|
|
|
102
|
+
# Skip if directives say so
|
|
103
|
+
next if skip_selection?(selection, variable_values)
|
|
104
|
+
|
|
105
|
+
case selection
|
|
106
|
+
when Parser::Field
|
|
107
|
+
response_key = selection.alias || selection.name
|
|
108
|
+
fields[response_key] << selection
|
|
109
|
+
when Parser::FragmentSpread
|
|
110
|
+
fragment_name = selection.name
|
|
111
|
+
next if visited_fragments[fragment_name]
|
|
112
|
+
visited_fragments[fragment_name] = true
|
|
113
|
+
|
|
114
|
+
# Note: Fragment definitions would need to be passed through context
|
|
115
|
+
# For now, we'll skip fragment spreads in this minimal implementation
|
|
116
|
+
when Parser::InlineFragment
|
|
117
|
+
# Check if type condition matches
|
|
118
|
+
if !selection.type_condition || type_applies?(selection.type_condition, object_type)
|
|
119
|
+
fragment_fields = collect_fields(selection.selection_set, object_type, variable_values, visited_fragments)
|
|
120
|
+
fragment_fields.each do |key, field_list|
|
|
121
|
+
fields[key].concat(field_list)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
fields
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def skip_selection?(selection, variable_values)
|
|
131
|
+
return false unless selection.respond_to?(:directives)
|
|
132
|
+
|
|
133
|
+
selection.directives.each do |directive|
|
|
134
|
+
case directive.name
|
|
135
|
+
when 'skip'
|
|
136
|
+
if_arg = directive.arguments.find { |arg| arg.name == 'if' }
|
|
137
|
+
return true if if_arg && evaluate_argument(if_arg, variable_values)
|
|
138
|
+
when 'include'
|
|
139
|
+
if_arg = directive.arguments.find { |arg| arg.name == 'if' }
|
|
140
|
+
return true if if_arg && !evaluate_argument(if_arg, variable_values)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
false
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def type_applies?(type_condition, object_type)
|
|
148
|
+
# For now, simple name matching
|
|
149
|
+
# TODO: Handle interfaces and unions properly
|
|
150
|
+
type_condition.name == object_type.name
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def execute_field(object_type, object_value, field_list, variable_values, context)
|
|
154
|
+
field_ast = field_list.first
|
|
155
|
+
field_name = field_ast.name
|
|
156
|
+
|
|
157
|
+
# Handle introspection fields
|
|
158
|
+
case field_name
|
|
159
|
+
when '__typename'
|
|
160
|
+
return object_type.name
|
|
161
|
+
when '__schema'
|
|
162
|
+
return @schema if object_type == @schema.query_type
|
|
163
|
+
when '__type'
|
|
164
|
+
type_name_arg = field_ast.arguments.find { |arg| arg.name == 'name' }
|
|
165
|
+
type_name = evaluate_argument(type_name_arg, variable_values) if type_name_arg
|
|
166
|
+
return @schema.get_type(type_name) if object_type == @schema.query_type
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Get field definition
|
|
170
|
+
field_def = object_type.fields[field_name]
|
|
171
|
+
return nil unless field_def
|
|
172
|
+
|
|
173
|
+
# Coerce arguments
|
|
174
|
+
args = coerce_arguments(field_ast.arguments, field_def, variable_values)
|
|
175
|
+
|
|
176
|
+
# Resolve field value
|
|
177
|
+
resolved_value = resolve_field_value(field_def, object_value, args, context)
|
|
178
|
+
|
|
179
|
+
# Complete value
|
|
180
|
+
complete_value(field_def.type, resolved_value, field_ast.selection_set, variable_values, context)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def coerce_arguments(argument_asts, field_def, variable_values)
|
|
184
|
+
return {} if argument_asts.empty?
|
|
185
|
+
|
|
186
|
+
args = {}
|
|
187
|
+
|
|
188
|
+
argument_asts.each do |arg_ast|
|
|
189
|
+
arg_def = field_def.arguments[arg_ast.name]
|
|
190
|
+
next unless arg_def
|
|
191
|
+
|
|
192
|
+
value = evaluate_argument(arg_ast, variable_values)
|
|
193
|
+
args[arg_ast.name] = value
|
|
194
|
+
args[arg_ast.name.to_sym] = value # Allow both string and symbol access
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
args
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def evaluate_argument(argument, variable_values)
|
|
201
|
+
evaluate_value(argument.value, variable_values)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def evaluate_value(value, variable_values)
|
|
205
|
+
case value
|
|
206
|
+
when Parser::Variable
|
|
207
|
+
variable_values[value.name]
|
|
208
|
+
when Parser::IntValue
|
|
209
|
+
value.value
|
|
210
|
+
when Parser::FloatValue
|
|
211
|
+
value.value
|
|
212
|
+
when Parser::StringValue
|
|
213
|
+
value.value
|
|
214
|
+
when Parser::BooleanValue
|
|
215
|
+
value.value
|
|
216
|
+
when Parser::NullValue
|
|
217
|
+
nil
|
|
218
|
+
when Parser::EnumValue
|
|
219
|
+
value.value
|
|
220
|
+
when Parser::ListValue
|
|
221
|
+
value.values.map { |v| evaluate_value(v, variable_values) }
|
|
222
|
+
when Parser::ObjectValue
|
|
223
|
+
obj = {}
|
|
224
|
+
value.fields.each do |field|
|
|
225
|
+
obj[field.name] = evaluate_value(field.value, variable_values)
|
|
226
|
+
obj[field.name.to_sym] = obj[field.name]
|
|
227
|
+
end
|
|
228
|
+
obj
|
|
229
|
+
else
|
|
230
|
+
value
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def resolve_field_value(field_def, object_value, args, context)
|
|
235
|
+
if field_def.resolve
|
|
236
|
+
# Custom resolver
|
|
237
|
+
if field_def.resolve.arity == 0
|
|
238
|
+
field_def.resolve.call
|
|
239
|
+
elsif field_def.resolve.arity == 1
|
|
240
|
+
field_def.resolve.call(args)
|
|
241
|
+
else
|
|
242
|
+
field_def.resolve.call(object_value, args, context)
|
|
243
|
+
end
|
|
244
|
+
elsif object_value.is_a?(Hash)
|
|
245
|
+
# Hash object
|
|
246
|
+
object_value[field_def.name] || object_value[field_def.name.to_sym]
|
|
247
|
+
elsif object_value.respond_to?(field_def.name)
|
|
248
|
+
# Method call
|
|
249
|
+
object_value.public_send(field_def.name)
|
|
250
|
+
else
|
|
251
|
+
nil
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def complete_value(field_type, result, selection_set, variable_values, context)
|
|
256
|
+
# Resolve lazy type references
|
|
257
|
+
field_type = field_type.resolve if field_type.is_a?(Schema::TypeReference)
|
|
258
|
+
|
|
259
|
+
# Handle non-null types
|
|
260
|
+
if field_type.is_a?(Types::NonNullType)
|
|
261
|
+
completed = complete_value(field_type.of_type, result, selection_set, variable_values, context)
|
|
262
|
+
raise ExecutionError, "Cannot return null for non-null field" if completed.nil?
|
|
263
|
+
return completed
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Handle null values
|
|
267
|
+
return nil if result.nil?
|
|
268
|
+
|
|
269
|
+
# Handle list types
|
|
270
|
+
if field_type.is_a?(Types::ListType)
|
|
271
|
+
raise ExecutionError, "Expected list but got #{result.class}" unless result.is_a?(Array)
|
|
272
|
+
return result.map { |item| complete_value(field_type.of_type, item, selection_set, variable_values, context) }
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Handle scalar types
|
|
276
|
+
if field_type.is_a?(Types::ScalarType)
|
|
277
|
+
return field_type.serialize.call(result)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Handle enum types
|
|
281
|
+
if field_type.is_a?(Types::EnumType)
|
|
282
|
+
return result.to_s
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Handle object types
|
|
286
|
+
if field_type.is_a?(Types::ObjectType)
|
|
287
|
+
return execute_selection_set(selection_set, field_type, result, variable_values, context)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Handle interface and union types
|
|
291
|
+
if field_type.is_a?(Types::InterfaceType) || field_type.is_a?(Types::UnionType)
|
|
292
|
+
runtime_type = resolve_runtime_type(field_type, result)
|
|
293
|
+
return execute_selection_set(selection_set, runtime_type, result, variable_values, context)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
result
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def resolve_runtime_type(abstract_type, object_value)
|
|
300
|
+
if abstract_type.resolve_type
|
|
301
|
+
type_name = abstract_type.resolve_type.call(object_value)
|
|
302
|
+
@schema.get_type(type_name)
|
|
303
|
+
else
|
|
304
|
+
# Try to infer from object class name
|
|
305
|
+
class_name = object_value.class.name.split('::').last
|
|
306
|
+
@schema.get_type(class_name)
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def resolve_type_ref(type_ref)
|
|
311
|
+
case type_ref
|
|
312
|
+
when Parser::NamedType
|
|
313
|
+
@schema.get_type(type_ref.name)
|
|
314
|
+
when Parser::ListType
|
|
315
|
+
Types::ListType.new(resolve_type_ref(type_ref.type))
|
|
316
|
+
when Parser::NonNullType
|
|
317
|
+
Types::NonNullType.new(resolve_type_ref(type_ref.type))
|
|
318
|
+
when Schema::TypeReference
|
|
319
|
+
type_ref.resolve
|
|
320
|
+
else
|
|
321
|
+
type_ref
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def coerce_input_value(value, type)
|
|
326
|
+
if type.is_a?(Types::NonNullType)
|
|
327
|
+
raise ExecutionError, "Expected non-null value" if value.nil?
|
|
328
|
+
return coerce_input_value(value, type.of_type)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
return nil if value.nil?
|
|
332
|
+
|
|
333
|
+
if type.is_a?(Types::ListType)
|
|
334
|
+
return [coerce_input_value(value, type.of_type)] unless value.is_a?(Array)
|
|
335
|
+
return value.map { |v| coerce_input_value(v, type.of_type) }
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
if type.is_a?(Types::ScalarType)
|
|
339
|
+
return type.parse_value.call(value)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
if type.is_a?(Types::EnumType)
|
|
343
|
+
return value.to_s
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
if type.is_a?(Types::InputObjectType)
|
|
347
|
+
return {} unless value.is_a?(Hash)
|
|
348
|
+
result = {}
|
|
349
|
+
type.fields.each do |field_name, field_def|
|
|
350
|
+
field_value = value[field_name] || value[field_name.to_sym]
|
|
351
|
+
result[field_name] = coerce_input_value(field_value, field_def.type)
|
|
352
|
+
end
|
|
353
|
+
return result
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
value
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def coerce_literal_value(value, type)
|
|
360
|
+
# Similar to coerce_input_value but for AST values
|
|
361
|
+
if type.is_a?(Types::NonNullType)
|
|
362
|
+
raise ExecutionError, "Expected non-null value" if value.is_a?(Parser::NullValue)
|
|
363
|
+
return coerce_literal_value(value, type.of_type)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
return nil if value.is_a?(Parser::NullValue)
|
|
367
|
+
|
|
368
|
+
if type.is_a?(Types::ListType)
|
|
369
|
+
return [coerce_literal_value(value, type.of_type)] unless value.is_a?(Parser::ListValue)
|
|
370
|
+
return value.values.map { |v| coerce_literal_value(v, type.of_type) }
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
if type.is_a?(Types::ScalarType)
|
|
374
|
+
return type.parse_literal.call(value)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
evaluate_value(value, {})
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
module GraphQLite
|
|
2
|
+
# Introspection adds schema introspection capabilities
|
|
3
|
+
module Introspection
|
|
4
|
+
class << self
|
|
5
|
+
def add_introspection_types(schema)
|
|
6
|
+
# Add __DirectiveLocation enum first
|
|
7
|
+
directive_location_enum = schema.enum('__DirectiveLocation', values: {
|
|
8
|
+
'QUERY' => { value: 'QUERY' },
|
|
9
|
+
'MUTATION' => { value: 'MUTATION' },
|
|
10
|
+
'SUBSCRIPTION' => { value: 'SUBSCRIPTION' },
|
|
11
|
+
'FIELD' => { value: 'FIELD' },
|
|
12
|
+
'FRAGMENT_DEFINITION' => { value: 'FRAGMENT_DEFINITION' },
|
|
13
|
+
'FRAGMENT_SPREAD' => { value: 'FRAGMENT_SPREAD' },
|
|
14
|
+
'INLINE_FRAGMENT' => { value: 'INLINE_FRAGMENT' },
|
|
15
|
+
'SCHEMA' => { value: 'SCHEMA' },
|
|
16
|
+
'SCALAR' => { value: 'SCALAR' },
|
|
17
|
+
'OBJECT' => { value: 'OBJECT' },
|
|
18
|
+
'FIELD_DEFINITION' => { value: 'FIELD_DEFINITION' },
|
|
19
|
+
'ARGUMENT_DEFINITION' => { value: 'ARGUMENT_DEFINITION' },
|
|
20
|
+
'INTERFACE' => { value: 'INTERFACE' },
|
|
21
|
+
'UNION' => { value: 'UNION' },
|
|
22
|
+
'ENUM' => { value: 'ENUM' },
|
|
23
|
+
'ENUM_VALUE' => { value: 'ENUM_VALUE' },
|
|
24
|
+
'INPUT_OBJECT' => { value: 'INPUT_OBJECT' },
|
|
25
|
+
'INPUT_FIELD_DEFINITION' => { value: 'INPUT_FIELD_DEFINITION' }
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
# Add __TypeKind enum
|
|
29
|
+
type_kind_enum = schema.enum('__TypeKind', values: {
|
|
30
|
+
'SCALAR' => { value: 'SCALAR' },
|
|
31
|
+
'OBJECT' => { value: 'OBJECT' },
|
|
32
|
+
'INTERFACE' => { value: 'INTERFACE' },
|
|
33
|
+
'UNION' => { value: 'UNION' },
|
|
34
|
+
'ENUM' => { value: 'ENUM' },
|
|
35
|
+
'INPUT_OBJECT' => { value: 'INPUT_OBJECT' },
|
|
36
|
+
'LIST' => { value: 'LIST' },
|
|
37
|
+
'NON_NULL' => { value: 'NON_NULL' }
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
# Add __InputValue type (needed by __Field and __Directive)
|
|
41
|
+
input_value_type = schema.object('__InputValue', description: 'An input value (argument or input field)') do
|
|
42
|
+
field :name, 'String', null: false do
|
|
43
|
+
resolve { |value| value.name }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
field :description, 'String' do
|
|
47
|
+
resolve { |value| value.description }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
field :type, '__Type', null: false do
|
|
51
|
+
resolve { |value| value.type }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
field :defaultValue, 'String' do
|
|
55
|
+
resolve { |value| value.default_value&.to_s }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Add __EnumValue type
|
|
60
|
+
enum_value_type = schema.object('__EnumValue', description: 'An enum value') do
|
|
61
|
+
field :name, 'String', null: false do
|
|
62
|
+
resolve { |value| value.value.to_s }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
field :description, 'String' do
|
|
66
|
+
resolve { |value| value.description }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
field :isDeprecated, 'Boolean', null: false do
|
|
70
|
+
resolve { |value| !value.deprecation_reason.nil? }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
field :deprecationReason, 'String' do
|
|
74
|
+
resolve { |value| value.deprecation_reason }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Add __Field type
|
|
79
|
+
field_type = schema.object('__Field', description: 'A field in an object type') do
|
|
80
|
+
field :name, 'String', null: false do
|
|
81
|
+
resolve { |field| field.name }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
field :description, 'String' do
|
|
85
|
+
resolve { |field| field.description }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
field :args, ['__InputValue'], null: false do
|
|
89
|
+
resolve { |field| field.arguments.values }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
field :type, '__Type', null: false do
|
|
93
|
+
resolve { |field| field.type }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
field :isDeprecated, 'Boolean', null: false do
|
|
97
|
+
resolve { |field| field.deprecated? }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
field :deprecationReason, 'String' do
|
|
101
|
+
resolve { |field| field.deprecation_reason }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Add __Directive type (before __Schema since __Schema references it)
|
|
106
|
+
directive_type = schema.object('__Directive', description: 'A directive') do
|
|
107
|
+
field :name, 'String', null: false do
|
|
108
|
+
resolve { |directive| directive.name }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
field :description, 'String' do
|
|
112
|
+
resolve { |directive| directive.description }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
field :locations, ['__DirectiveLocation'], null: false do
|
|
116
|
+
resolve { |directive| directive.locations }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
field :args, ['__InputValue'], null: false do
|
|
120
|
+
resolve { |directive| directive.arguments.values }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Add __Schema type
|
|
125
|
+
schema_type = schema.object('__Schema', description: 'A GraphQL schema') do
|
|
126
|
+
field :types, [schema.object('__Type')] do
|
|
127
|
+
resolve { |args, ctx| schema.types.values }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
field :queryType, '__Type' do
|
|
131
|
+
resolve { schema.query_type }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
field :mutationType, '__Type' do
|
|
135
|
+
resolve { schema.mutation_type }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
field :subscriptionType, '__Type' do
|
|
139
|
+
resolve { schema.subscription_type }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
field :directives, ['__Directive'] do
|
|
143
|
+
resolve { schema.directives.values }
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Add __Type type
|
|
148
|
+
type_type = schema.object('__Type', description: 'A type in the GraphQL schema') do
|
|
149
|
+
field :kind, '__TypeKind', null: false do
|
|
150
|
+
resolve { |type| type.kind }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
field :name, 'String' do
|
|
154
|
+
resolve { |type| type.respond_to?(:name) ? type.name : nil }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
field :description, 'String' do
|
|
158
|
+
resolve { |type| type.respond_to?(:description) ? type.description : nil }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
field :fields, ['__Field'] do
|
|
162
|
+
argument :includeDeprecated, 'Boolean'
|
|
163
|
+
resolve do |type, args|
|
|
164
|
+
next nil unless type.respond_to?(:fields)
|
|
165
|
+
fields = type.fields.values
|
|
166
|
+
fields = fields.reject(&:deprecated?) unless args[:includeDeprecated]
|
|
167
|
+
fields
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
field :interfaces, ['__Type'] do
|
|
172
|
+
resolve do |type|
|
|
173
|
+
type.respond_to?(:interfaces) ? type.interfaces : nil
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
field :possibleTypes, ['__Type'] do
|
|
178
|
+
resolve do |type|
|
|
179
|
+
type.respond_to?(:types) ? type.types : nil
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
field :enumValues, ['__EnumValue'] do
|
|
184
|
+
argument :includeDeprecated, 'Boolean'
|
|
185
|
+
resolve do |type, args|
|
|
186
|
+
next nil unless type.is_a?(Types::EnumType)
|
|
187
|
+
values = type.values.values
|
|
188
|
+
values = values.reject { |v| v.deprecation_reason } unless args[:includeDeprecated]
|
|
189
|
+
values
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
field :inputFields, ['__InputValue'] do
|
|
194
|
+
resolve do |type|
|
|
195
|
+
type.is_a?(Types::InputObjectType) ? type.fields.values : nil
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
field :ofType, '__Type' do
|
|
200
|
+
resolve do |type|
|
|
201
|
+
type.respond_to?(:of_type) ? type.of_type : nil
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Add introspection fields to query type
|
|
207
|
+
if schema.query_type
|
|
208
|
+
schema.query_type.field('__schema', schema_type, description: 'Access the schema introspection system') do
|
|
209
|
+
resolve { schema }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
schema.query_type.field('__type', type_type, description: 'Query a type by name') do |field|
|
|
213
|
+
field.argument('name', Types::STRING.!)
|
|
214
|
+
field.resolve do |_, args|
|
|
215
|
+
schema.get_type(args[:name] || args['name'])
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|