graphql 0.17.2 → 0.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql.rb +1 -0
  3. data/lib/graphql/analysis/query_depth.rb +1 -1
  4. data/lib/graphql/base_type.rb +25 -1
  5. data/lib/graphql/define.rb +2 -0
  6. data/lib/graphql/define/assign_connection.rb +11 -0
  7. data/lib/graphql/define/assign_global_id_field.rb +11 -0
  8. data/lib/graphql/define/assign_object_field.rb +21 -20
  9. data/lib/graphql/define/defined_object_proxy.rb +2 -2
  10. data/lib/graphql/define/instance_definable.rb +13 -3
  11. data/lib/graphql/field.rb +1 -1
  12. data/lib/graphql/language/generation.rb +57 -6
  13. data/lib/graphql/language/lexer.rb +434 -212
  14. data/lib/graphql/language/lexer.rl +18 -0
  15. data/lib/graphql/language/nodes.rb +75 -0
  16. data/lib/graphql/language/parser.rb +853 -341
  17. data/lib/graphql/language/parser.y +114 -17
  18. data/lib/graphql/query.rb +15 -1
  19. data/lib/graphql/relay.rb +13 -0
  20. data/lib/graphql/relay/array_connection.rb +80 -0
  21. data/lib/graphql/relay/base_connection.rb +138 -0
  22. data/lib/graphql/relay/connection_field.rb +54 -0
  23. data/lib/graphql/relay/connection_type.rb +25 -0
  24. data/lib/graphql/relay/edge.rb +22 -0
  25. data/lib/graphql/relay/edge_type.rb +14 -0
  26. data/lib/graphql/relay/global_id_resolve.rb +15 -0
  27. data/lib/graphql/relay/global_node_identification.rb +124 -0
  28. data/lib/graphql/relay/mutation.rb +146 -0
  29. data/lib/graphql/relay/page_info.rb +13 -0
  30. data/lib/graphql/relay/relation_connection.rb +98 -0
  31. data/lib/graphql/schema.rb +3 -0
  32. data/lib/graphql/schema/printer.rb +12 -2
  33. data/lib/graphql/static_validation/message.rb +9 -5
  34. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
  35. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +1 -1
  36. data/lib/graphql/static_validation/rules/directives_are_defined.rb +3 -3
  37. data/lib/graphql/static_validation/rules/directives_are_in_valid_locations.rb +7 -7
  38. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +4 -4
  39. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +5 -5
  40. data/lib/graphql/static_validation/rules/fields_will_merge.rb +6 -6
  41. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +17 -9
  42. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +1 -1
  43. data/lib/graphql/static_validation/rules/fragments_are_finite.rb +1 -1
  44. data/lib/graphql/static_validation/rules/fragments_are_on_composite_types.rb +1 -1
  45. data/lib/graphql/static_validation/rules/fragments_are_used.rb +17 -6
  46. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +1 -1
  47. data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +2 -2
  48. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +5 -5
  49. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +1 -1
  50. data/lib/graphql/static_validation/rules/variables_are_used_and_defined.rb +12 -11
  51. data/lib/graphql/static_validation/type_stack.rb +33 -2
  52. data/lib/graphql/static_validation/validation_context.rb +5 -0
  53. data/lib/graphql/version.rb +1 -1
  54. data/readme.md +16 -4
  55. data/spec/graphql/analysis/analyze_query_spec.rb +31 -2
  56. data/spec/graphql/analysis/query_complexity_spec.rb +24 -0
  57. data/spec/graphql/argument_spec.rb +1 -1
  58. data/spec/graphql/define/instance_definable_spec.rb +9 -0
  59. data/spec/graphql/field_spec.rb +1 -1
  60. data/spec/graphql/internal_representation/rewrite_spec.rb +3 -3
  61. data/spec/graphql/language/generation_spec.rb +25 -4
  62. data/spec/graphql/language/parser_spec.rb +116 -1
  63. data/spec/graphql/query_spec.rb +10 -0
  64. data/spec/graphql/relay/array_connection_spec.rb +164 -0
  65. data/spec/graphql/relay/connection_type_spec.rb +37 -0
  66. data/spec/graphql/relay/global_node_identification_spec.rb +149 -0
  67. data/spec/graphql/relay/mutation_spec.rb +55 -0
  68. data/spec/graphql/relay/page_info_spec.rb +106 -0
  69. data/spec/graphql/relay/relation_connection_spec.rb +348 -0
  70. data/spec/graphql/schema/printer_spec.rb +8 -0
  71. data/spec/graphql/schema/reduce_types_spec.rb +1 -1
  72. data/spec/graphql/static_validation/rules/argument_literals_are_compatible_spec.rb +12 -6
  73. data/spec/graphql/static_validation/rules/arguments_are_defined_spec.rb +8 -4
  74. data/spec/graphql/static_validation/rules/directives_are_defined_spec.rb +4 -2
  75. data/spec/graphql/static_validation/rules/directives_are_in_valid_locations_spec.rb +4 -2
  76. data/spec/graphql/static_validation/rules/fields_are_defined_on_type_spec.rb +7 -2
  77. data/spec/graphql/static_validation/rules/fields_have_appropriate_selections_spec.rb +4 -2
  78. data/spec/graphql/static_validation/rules/fragment_spreads_are_possible_spec.rb +6 -3
  79. data/spec/graphql/static_validation/rules/fragment_types_exist_spec.rb +5 -3
  80. data/spec/graphql/static_validation/rules/fragments_are_finite_spec.rb +4 -2
  81. data/spec/graphql/static_validation/rules/fragments_are_on_composite_types_spec.rb +5 -2
  82. data/spec/graphql/static_validation/rules/fragments_are_used_spec.rb +10 -2
  83. data/spec/graphql/static_validation/rules/required_arguments_are_present_spec.rb +6 -3
  84. data/spec/graphql/static_validation/rules/variable_default_values_are_correctly_typed_spec.rb +8 -4
  85. data/spec/graphql/static_validation/rules/variable_usages_are_allowed_spec.rb +8 -4
  86. data/spec/graphql/static_validation/rules/variables_are_input_types_spec.rb +6 -3
  87. data/spec/graphql/static_validation/rules/variables_are_used_and_defined_spec.rb +6 -3
  88. data/spec/spec_helper.rb +7 -0
  89. data/spec/support/dairy_app.rb +11 -10
  90. data/spec/support/star_wars_data.rb +65 -58
  91. data/spec/support/star_wars_schema.rb +192 -54
  92. metadata +84 -2
@@ -11,6 +11,7 @@ rule
11
11
  definition:
12
12
  operation_definition
13
13
  | fragment_definition
14
+ | type_system_definition
14
15
 
15
16
  operation_definition:
16
17
  operation_type operation_name_opt variable_definitions_opt directives_list_opt selection_set {
@@ -52,7 +53,7 @@ rule
52
53
  | variable_definitions_list variable_definition { val[0] << val[1] }
53
54
 
54
55
  variable_definition:
55
- VAR_SIGN name COLON variable_definition_type_name variable_definition_default_value_opt {
56
+ VAR_SIGN name COLON type default_value_opt {
56
57
  return make_node(:VariableDefinition, {
57
58
  name: val[1],
58
59
  type: val[3],
@@ -61,12 +62,12 @@ rule
61
62
  })
62
63
  }
63
64
 
64
- variable_definition_type_name:
65
- name { return make_node(:TypeName, name: val[0])}
66
- | variable_definition_type_name BANG { return make_node(:NonNullType, of_type: val[0]) }
67
- | RBRACKET variable_definition_type_name LBRACKET { return make_node(:ListType, of_type: val[1]) }
65
+ type:
66
+ name { return make_node(:TypeName, name: val[0])}
67
+ | type BANG { return make_node(:NonNullType, of_type: val[0]) }
68
+ | RBRACKET type LBRACKET { return make_node(:ListType, of_type: val[1]) }
68
69
 
69
- variable_definition_default_value_opt:
70
+ default_value_opt:
70
71
  /* none */ { return nil }
71
72
  | EQUALS input_value { return val[1] }
72
73
 
@@ -116,14 +117,38 @@ rule
116
117
  name_without_on
117
118
  | ON
118
119
 
120
+ schema_keyword:
121
+ SCHEMA
122
+ | SCALAR
123
+ | TYPE
124
+ | IMPLEMENTS
125
+ | INTERFACE
126
+ | UNION
127
+ | ENUM
128
+ | INPUT
129
+
119
130
  name_without_on:
120
131
  IDENTIFIER
121
132
  | FRAGMENT
122
133
  | TRUE
123
134
  | FALSE
124
- | QUERY
125
- | MUTATION
126
- | SUBSCRIPTION
135
+ | operation_type
136
+ | schema_keyword
137
+
138
+ enum_name: /* any identifier, but not "true", "false" or "null" */
139
+ IDENTIFIER
140
+ | FRAGMENT
141
+ | ON
142
+ | operation_type
143
+ | schema_keyword
144
+
145
+ name_list:
146
+ name { return [val[0].to_s]}
147
+ | name_list name { val[0] << val[1].to_s }
148
+
149
+ enum_name_list:
150
+ enum_name { return [val[0].to_s]}
151
+ | enum_name_list enum_name { return val[0] << val[1].to_s }
127
152
 
128
153
  arguments_opt:
129
154
  /* none */ { return [] }
@@ -171,14 +196,6 @@ rule
171
196
 
172
197
  enum_value: enum_name { return make_node(:Enum, name: val[0], position_source: val[0])}
173
198
 
174
- enum_name: /* any identifier, but not "true", "false" or "null" */
175
- IDENTIFIER
176
- | FRAGMENT
177
- | ON
178
- | QUERY
179
- | MUTATION
180
- | SUBSCRIPTION
181
-
182
199
  directives_list_opt:
183
200
  /* none */ { return [] }
184
201
  | directives_list
@@ -221,6 +238,86 @@ rule
221
238
  }
222
239
  )
223
240
  }
241
+
242
+
243
+ type_system_definition:
244
+ schema_definition
245
+ | type_definition
246
+
247
+ schema_definition:
248
+ SCHEMA RCURLY operation_type_definition_list LCURLY { return make_node(:SchemaDefinition, val[2]) }
249
+
250
+ operation_type_definition_list:
251
+ operation_type_definition
252
+ | operation_type_definition_list operation_type_definition { return val[0].merge(val[1]) }
253
+
254
+ operation_type_definition:
255
+ operation_type COLON name { return { val[0].to_s.to_sym => val[2] } }
256
+
257
+ type_definition:
258
+ scalar_type_definition
259
+ | object_type_definition
260
+ | interface_type_definition
261
+ | union_type_definition
262
+ | enum_type_definition
263
+ | input_object_type_definition
264
+
265
+ scalar_type_definition: SCALAR name { return make_node(:ScalarTypeDefinition, name: val[1]) }
266
+
267
+ object_type_definition:
268
+ TYPE name implements_opt RCURLY field_definition_list LCURLY {
269
+ return make_node(:ObjectTypeDefinition, name: val[1], interfaces: val[2], fields: val[4])
270
+ }
271
+
272
+ implements_opt:
273
+ /* none */ { return [] }
274
+ | IMPLEMENTS name_list { return val[1] }
275
+
276
+ input_value_definition:
277
+ name COLON type default_value_opt {
278
+ return make_node(:InputValueDefinition, name: val[0], type: val[2], default_value: val[3])
279
+ }
280
+
281
+ input_value_definition_list:
282
+ input_value_definition { return [val[0]] }
283
+ | input_value_definition_list input_value_definition { val[0] << val[1] }
284
+
285
+ arguments_definitions_opt:
286
+ /* none */ { return [] }
287
+ | RPAREN input_value_definition_list LPAREN { return val[1] }
288
+
289
+ field_definition:
290
+ name arguments_definitions_opt COLON type {
291
+ return make_node(:FieldDefinition, name: val[0], arguments: val[1], type: val[3])
292
+ }
293
+
294
+ field_definition_list:
295
+ field_definition { return [val[0]] }
296
+ | field_definition_list field_definition { val[0] << val[1] }
297
+
298
+ interface_type_definition:
299
+ INTERFACE name RCURLY field_definition_list LCURLY {
300
+ return make_node(:InterfaceTypeDefinition, name: val[1], fields: val[3])
301
+ }
302
+
303
+ union_members:
304
+ name { return [val[0].to_s]}
305
+ | union_members PIPE name { val[0] << val[2].to_s }
306
+
307
+ union_type_definition:
308
+ UNION name EQUALS union_members {
309
+ return make_node(:UnionTypeDefinition, name: val[1], types: val[3])
310
+ }
311
+
312
+ enum_type_definition:
313
+ ENUM name RCURLY enum_name_list LCURLY {
314
+ return make_node(:EnumTypeDefinition, name: val[1], values: val[3])
315
+ }
316
+
317
+ input_object_type_definition:
318
+ INPUT name RCURLY input_value_definition_list LCURLY {
319
+ return make_node(:InputObjectTypeDefinition, name: val[1], fields: val[3])
320
+ }
224
321
  end
225
322
 
226
323
  ---- header ----
@@ -48,6 +48,8 @@ module GraphQL
48
48
  @fragments[part.name] = part
49
49
  elsif part.is_a?(GraphQL::Language::Nodes::OperationDefinition)
50
50
  @operations[part.name] = part
51
+ else
52
+ raise GraphQL::ExecutionError, "GraphQL query cannot contain a schema definition"
51
53
  end
52
54
  end
53
55
 
@@ -71,7 +73,10 @@ module GraphQL
71
73
  # If more than one operation is present, it must be named at runtime.
72
74
  # @return [GraphQL::Language::Nodes::OperationDefinition, nil]
73
75
  def selected_operation
74
- @selected_operation ||= find_operation(@operations, @operation_name)
76
+ @selected_operation ||= begin
77
+ perform_validation
78
+ @selected_operation
79
+ end
75
80
  end
76
81
 
77
82
  # Determine the values for variables of this query, using default values
@@ -116,9 +121,18 @@ module GraphQL
116
121
  private
117
122
 
118
123
  def perform_validation
124
+ @selected_operation = find_operation(@operations, @operation_name)
119
125
  validation_result = schema.static_validator.validate(self)
120
126
  @validation_errors = validation_result[:errors]
121
127
  @internal_representation = validation_result[:irep]
128
+ if @validation_errors.none?
129
+ # Accessing variables will raise errors if there are any :S
130
+ variables
131
+ end
132
+ nil
133
+ rescue GraphQL::Query::OperationNameMissingError, GraphQL::Query::VariableValidationError => err
134
+ @validation_errors = [err.to_h]
135
+ @internal_representation = nil
122
136
  nil
123
137
  end
124
138
 
@@ -0,0 +1,13 @@
1
+ require 'base64'
2
+
3
+ require 'graphql/relay/global_node_identification'
4
+ require 'graphql/relay/page_info'
5
+ require 'graphql/relay/edge'
6
+ require 'graphql/relay/edge_type'
7
+ require 'graphql/relay/base_connection'
8
+ require 'graphql/relay/array_connection'
9
+ require 'graphql/relay/relation_connection'
10
+ require 'graphql/relay/global_id_resolve'
11
+ require 'graphql/relay/mutation'
12
+ require 'graphql/relay/connection_field'
13
+ require 'graphql/relay/connection_type'
@@ -0,0 +1,80 @@
1
+ module GraphQL
2
+ module Relay
3
+ class ArrayConnection < BaseConnection
4
+ def cursor_from_node(item)
5
+ idx = starting_offset + sliced_nodes.find_index(item) + 1
6
+ Base64.strict_encode64(idx.to_s)
7
+ end
8
+
9
+ def has_next_page
10
+ !!(first && sliced_nodes.count > limit)
11
+ end
12
+
13
+ def has_previous_page
14
+ !!(last && starting_offset > 0)
15
+ end
16
+
17
+ private
18
+
19
+ # apply first / last limit results
20
+ def paged_nodes
21
+ @paged_nodes = begin
22
+ items = sliced_nodes
23
+
24
+ if limit
25
+ items.first(limit)
26
+ else
27
+ items
28
+ end
29
+ end
30
+ end
31
+
32
+ # Apply cursors to edges
33
+ def sliced_nodes
34
+ @sliced_nodes ||= begin
35
+ items = object
36
+ items[starting_offset..-1]
37
+ end
38
+ end
39
+
40
+ def index_from_cursor(cursor)
41
+ Base64.decode64(cursor).to_i
42
+ end
43
+
44
+ def starting_offset
45
+ @starting_offset = if before
46
+ [previous_offset, 0].max
47
+ else
48
+ previous_offset
49
+ end
50
+ end
51
+
52
+ def previous_offset
53
+ @previous_offset ||= if after
54
+ index_from_cursor(after)
55
+ elsif before
56
+ prev_page_size = [max_page_size, last].compact.min || 0
57
+ index_from_cursor(before) - prev_page_size - 1
58
+ else
59
+ 0
60
+ end
61
+ end
62
+
63
+ def limit
64
+ @limit ||= begin
65
+ limit_from_arguments = if first
66
+ first
67
+ else
68
+ if previous_offset < 0
69
+ previous_offset + (last ? last : 0)
70
+ else
71
+ last
72
+ end
73
+ end
74
+ [limit_from_arguments, max_page_size].compact.min
75
+ end
76
+ end
77
+ end
78
+ BaseConnection.register_connection_implementation(Array, ArrayConnection)
79
+ end
80
+ end
@@ -0,0 +1,138 @@
1
+ module GraphQL
2
+ module Relay
3
+ # Subclasses must implement:
4
+ # - {#cursor_from_node}, which returns an opaque cursor for the given item
5
+ # - {#sliced_nodes}, which slices by `before` & `after`
6
+ # - {#paged_nodes}, which applies `first` & `last` limits
7
+ #
8
+ # In a subclass, you have access to
9
+ # - {#object}, the object which the connection will wrap
10
+ # - {#first}, {#after}, {#last}, {#before} (arguments passed to the field)
11
+ # - {#max_page_size} (the specified maximum page size that can be returned from a connection)
12
+ #
13
+ class BaseConnection
14
+ # Just to encode data in the cursor, use something that won't conflict
15
+ CURSOR_SEPARATOR = "---"
16
+
17
+ # Map of collection class names -> connection_classes
18
+ # eg {"Array" => ArrayConnection}
19
+ CONNECTION_IMPLEMENTATIONS = {}
20
+
21
+ # Find a connection implementation suitable for exposing `items`
22
+ #
23
+ # @param [Object] A collection of items (eg, Array, AR::Relation)
24
+ # @return [subclass of BaseConnection] a connection Class for wrapping `items`
25
+ def self.connection_for_items(items)
26
+ # We check class membership by comparing class names rather than
27
+ # identity to prevent this from being broken by Rails autoloading.
28
+ # Changes to the source file for ItemsClass in Rails apps cause it to be
29
+ # reloaded as a new object, so if we were to use `is_a?` here, it would
30
+ # no longer match any registered custom connection types.
31
+ ancestor_names = items.class.ancestors.map(&:name)
32
+ implementation = CONNECTION_IMPLEMENTATIONS.find do |items_class_name, connection_class|
33
+ ancestor_names.include? items_class_name
34
+ end
35
+ if implementation.nil?
36
+ raise("No connection implementation to wrap #{items.class} (#{items})")
37
+ else
38
+ implementation[1]
39
+ end
40
+ end
41
+
42
+ # Add `connection_class` as the connection wrapper for `items_class`
43
+ # eg, `RelationConnection` is the implementation for `AR::Relation`
44
+ # @param [Class] A class representing a collection (eg, Array, AR::Relation)
45
+ # @param [Class] A class implementing Connection methods
46
+ def self.register_connection_implementation(items_class, connection_class)
47
+ CONNECTION_IMPLEMENTATIONS[items_class.name] = connection_class
48
+ end
49
+
50
+ attr_reader :object, :arguments, :max_page_size, :parent
51
+
52
+ # Make a connection, wrapping `object`
53
+ # @param The collection of results
54
+ # @param Query arguments
55
+ # @param max_page_size [Int] The maximum number of results to return
56
+ # @param parent [Object] The object which this collection belongs to
57
+ def initialize(object, arguments, max_page_size: nil, parent: nil)
58
+ @object = object
59
+ @arguments = arguments
60
+ @max_page_size = max_page_size
61
+ @parent = parent
62
+ end
63
+
64
+ # Provide easy access to provided arguments:
65
+ METHODS_FROM_ARGUMENTS = [:first, :after, :last, :before, :order]
66
+
67
+ # @!method first
68
+ # The value passed as `first:`, if there was one
69
+ # @!method after
70
+ # The value passed as `after:`, if there was one
71
+ # @!method last
72
+ # The value passed as `last:`, if there was one
73
+ # @!method before
74
+ # The value passed as `before:`, if there was one
75
+ # @!method order
76
+ # The value passed as `order:`, if there was one
77
+ METHODS_FROM_ARGUMENTS.each do |arg_name|
78
+ define_method(arg_name) do
79
+ arguments[arg_name]
80
+ end
81
+ end
82
+
83
+ # These are the items to render for this connection,
84
+ # probably wrapped by {GraphQL::Relay::Edge}
85
+ def edge_nodes
86
+ @edge_nodes ||= paged_nodes
87
+ end
88
+
89
+ # Support the `pageInfo` field
90
+ def page_info
91
+ self
92
+ end
93
+
94
+ # Used by `pageInfo`
95
+ def has_next_page
96
+ !!(first && sliced_nodes.count > first)
97
+ end
98
+
99
+ # Used by `pageInfo`
100
+ def has_previous_page
101
+ !!(last && sliced_nodes.count > last)
102
+ end
103
+
104
+ # Used by `pageInfo`
105
+ def start_cursor
106
+ if start_node = (respond_to?(:paged_nodes_array, true) ? paged_nodes_array : paged_nodes).first
107
+ return cursor_from_node(start_node)
108
+ else
109
+ return nil
110
+ end
111
+ end
112
+
113
+ # Used by `pageInfo`
114
+ def end_cursor
115
+ if end_node = (respond_to?(:paged_nodes_array, true) ? paged_nodes_array : paged_nodes).last
116
+ return cursor_from_node(end_node)
117
+ else
118
+ return nil
119
+ end
120
+ end
121
+
122
+ # An opaque operation which returns a connection-specific cursor.
123
+ def cursor_from_node(object)
124
+ raise NotImplementedError, "must return a cursor for this object/connection pair"
125
+ end
126
+
127
+ private
128
+
129
+ def paged_nodes
130
+ raise NotImplementedError, "must return items for this connection after paging"
131
+ end
132
+
133
+ def sliced_nodes
134
+ raise NotImplementedError, "must return all items for this connection after chopping off first and last"
135
+ end
136
+ end
137
+ end
138
+ end