graphql 1.3.0 → 1.4.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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/base_type.rb +33 -5
  3. data/lib/graphql/boolean_type.rb +1 -0
  4. data/lib/graphql/compatibility/execution_specification.rb +1 -1
  5. data/lib/graphql/compatibility/execution_specification/specification_schema.rb +1 -0
  6. data/lib/graphql/directive.rb +10 -2
  7. data/lib/graphql/directive/deprecated_directive.rb +1 -0
  8. data/lib/graphql/directive/include_directive.rb +1 -0
  9. data/lib/graphql/directive/skip_directive.rb +1 -0
  10. data/lib/graphql/enum_type.rb +1 -0
  11. data/lib/graphql/execution/execute.rb +1 -13
  12. data/lib/graphql/float_type.rb +1 -0
  13. data/lib/graphql/id_type.rb +1 -0
  14. data/lib/graphql/input_object_type.rb +12 -2
  15. data/lib/graphql/int_type.rb +1 -0
  16. data/lib/graphql/interface_type.rb +1 -0
  17. data/lib/graphql/introspection/directive_location_enum.rb +1 -0
  18. data/lib/graphql/introspection/directive_type.rb +1 -0
  19. data/lib/graphql/introspection/enum_value_type.rb +1 -0
  20. data/lib/graphql/introspection/field_type.rb +1 -0
  21. data/lib/graphql/introspection/input_fields_field.rb +1 -1
  22. data/lib/graphql/introspection/input_value_type.rb +1 -0
  23. data/lib/graphql/introspection/schema_type.rb +2 -0
  24. data/lib/graphql/introspection/type_kind_enum.rb +1 -0
  25. data/lib/graphql/introspection/type_type.rb +1 -0
  26. data/lib/graphql/list_type.rb +1 -0
  27. data/lib/graphql/non_null_type.rb +1 -0
  28. data/lib/graphql/object_type.rb +1 -0
  29. data/lib/graphql/query.rb +50 -13
  30. data/lib/graphql/query/context.rb +5 -4
  31. data/lib/graphql/query/serial_execution/field_resolution.rb +1 -22
  32. data/lib/graphql/relay/array_connection.rb +3 -1
  33. data/lib/graphql/relay/connection_type.rb +15 -1
  34. data/lib/graphql/relay/node.rb +1 -0
  35. data/lib/graphql/relay/page_info.rb +1 -0
  36. data/lib/graphql/relay/relation_connection.rb +2 -0
  37. data/lib/graphql/schema.rb +21 -13
  38. data/lib/graphql/schema/catchall_middleware.rb +2 -2
  39. data/lib/graphql/schema/middleware_chain.rb +71 -13
  40. data/lib/graphql/schema/null_mask.rb +10 -0
  41. data/lib/graphql/schema/printer.rb +85 -59
  42. data/lib/graphql/schema/rescue_middleware.rb +2 -2
  43. data/lib/graphql/schema/timeout_middleware.rb +2 -2
  44. data/lib/graphql/schema/validation.rb +1 -12
  45. data/lib/graphql/schema/warden.rb +48 -24
  46. data/lib/graphql/static_validation/literal_validator.rb +2 -2
  47. data/lib/graphql/string_type.rb +1 -0
  48. data/lib/graphql/union_type.rb +7 -1
  49. data/lib/graphql/version.rb +1 -1
  50. data/readme.md +0 -19
  51. data/spec/graphql/directive/skip_directive_spec.rb +8 -0
  52. data/spec/graphql/directive_spec.rb +9 -3
  53. data/spec/graphql/input_object_type_spec.rb +67 -0
  54. data/spec/graphql/query/variables_spec.rb +1 -1
  55. data/spec/graphql/relay/array_connection_spec.rb +9 -0
  56. data/spec/graphql/relay/connection_type_spec.rb +20 -0
  57. data/spec/graphql/relay/node_spec.rb +6 -0
  58. data/spec/graphql/relay/page_info_spec.rb +4 -0
  59. data/spec/graphql/relay/relation_connection_spec.rb +8 -0
  60. data/spec/graphql/scalar_type_spec.rb +4 -0
  61. data/spec/graphql/schema/middleware_chain_spec.rb +27 -13
  62. data/spec/graphql/schema/printer_spec.rb +121 -6
  63. data/spec/graphql/schema/rescue_middleware_spec.rb +4 -4
  64. data/spec/graphql/schema/warden_spec.rb +16 -12
  65. data/spec/graphql/schema_spec.rb +9 -0
  66. data/spec/graphql/string_type_spec.rb +10 -4
  67. data/spec/spec_helper.rb +2 -1
  68. data/spec/support/star_wars_schema.rb +2 -2
  69. metadata +19 -2
@@ -30,9 +30,9 @@ module GraphQL
30
30
  end
31
31
 
32
32
  # Implement the requirement for {GraphQL::Schema::MiddlewareChain}
33
- def call(*args, next_middleware)
33
+ def call(*args)
34
34
  begin
35
- next_middleware.call
35
+ yield
36
36
  rescue StandardError => err
37
37
  attempt_rescue(err)
38
38
  end
@@ -34,13 +34,13 @@ module GraphQL
34
34
  @error_handler = block
35
35
  end
36
36
 
37
- def call(parent_type, parent_object, field_definition, field_args, query_context, next_middleware)
37
+ def call(parent_type, parent_object, field_definition, field_args, query_context)
38
38
  timeout_at = query_context[@context_key] ||= Time.now + @max_seconds
39
39
 
40
40
  if timeout_at < Time.now
41
41
  on_timeout(parent_type, parent_object, field_definition, field_args, query_context)
42
42
  else
43
- next_middleware.call
43
+ yield
44
44
  end
45
45
  end
46
46
 
@@ -178,7 +178,7 @@ module GraphQL
178
178
  }
179
179
 
180
180
  RESERVED_TYPE_NAME = ->(type) {
181
- if type.name.start_with?('__') && INTROSPECTION_TYPES[type.name] != type
181
+ if type.name.start_with?('__') && !type.introspection?
182
182
  # TODO: make this a hard failure in a later version
183
183
  warn("Name #{type.name.inspect} must not begin with \"__\", which is reserved by GraphQL introspection.")
184
184
  nil
@@ -246,17 +246,6 @@ module GraphQL
246
246
  Rules::SCHEMA_INSTRUMENTERS_ARE_VALID,
247
247
  ],
248
248
  }
249
-
250
- INTROSPECTION_TYPES = Hash[[
251
- GraphQL::Introspection::TypeType,
252
- GraphQL::Introspection::TypeKindEnum,
253
- GraphQL::Introspection::FieldType,
254
- GraphQL::Introspection::InputValueType,
255
- GraphQL::Introspection::EnumValueType,
256
- GraphQL::Introspection::DirectiveType,
257
- GraphQL::Introspection::DirectiveLocationEnum,
258
- GraphQL::Introspection::SchemaType,
259
- ].map{ |type| [type.name, type] }]
260
249
  end
261
250
  end
262
251
  end
@@ -12,7 +12,7 @@ module GraphQL
12
12
  # Masks can be provided in {Schema#execute} (or {Query#initialize}) with the `mask:` keyword.
13
13
  #
14
14
  # @example Hidding private fields
15
- # private_members = -> (member) { member.metadata[:private] }
15
+ # private_members = -> (member, ctx) { member.metadata[:private] }
16
16
  # result = Schema.execute(query_string, except: private_members)
17
17
  #
18
18
  # @example Custom mask implementation
@@ -23,7 +23,7 @@ module GraphQL
23
23
  # end
24
24
  #
25
25
  # # Return `false` if any required flags are missing
26
- # def call(member)
26
+ # def call(member, ctx)
27
27
  # member.metadata[:required_flags].any? do |flag|
28
28
  # !@user.has_flag?(flag)
29
29
  # end
@@ -36,69 +36,88 @@ module GraphQL
36
36
  # # This query can only access members which match the user's flags
37
37
  # result = Schema.execute(query_string, except: missing_required_flags)
38
38
  #
39
+ # @api private
39
40
  class Warden
41
+ # @param mask [<#call(member)>] Objects are hidden when `.call(member, ctx)` returns true
42
+ # @param context [GraphQL::Query::Context]
40
43
  # @param schema [GraphQL::Schema]
41
- # @param mask [<#call(member)>] Objects are hidden when `.call(member)` returns true
42
- def initialize(schema, mask)
44
+ # @param deep_check [Boolean]
45
+ def initialize(mask, context:, schema:)
43
46
  @mask = mask
47
+ @context = context
44
48
  @schema = schema
45
49
  end
46
50
 
47
51
  # @return [Array<GraphQL::BaseType>] Visible types in the schema
48
52
  def types
49
- @schema.types.each_value.select { |t| visible?(t) }
53
+ @types ||= @schema.types.each_value.select { |t| visible?(t) }
50
54
  end
51
55
 
52
56
  # @return [GraphQL::BaseType, nil] The type named `type_name`, if it exists (else `nil`)
53
57
  def get_type(type_name)
54
- type_defn = @schema.types.fetch(type_name, nil)
55
- if type_defn && visible?(type_defn)
56
- type_defn
57
- else
58
- nil
58
+ @visible_types ||= read_through do |name|
59
+ type_defn = @schema.types.fetch(name, nil)
60
+ if type_defn && visible?(type_defn)
61
+ type_defn
62
+ else
63
+ nil
64
+ end
59
65
  end
66
+
67
+ @visible_types[type_name]
60
68
  end
61
69
 
62
70
  # @return [GraphQL::Field, nil] The field named `field_name` on `parent_type`, if it exists
63
71
  def get_field(parent_type, field_name)
64
- field_defn = @schema.get_field(parent_type, field_name)
65
- if field_defn && visible_field?(field_defn)
66
- field_defn
67
- else
68
- nil
72
+
73
+ @visible_parent_fields ||= read_through do |type|
74
+ read_through do |f_name|
75
+ field_defn = @schema.get_field(type, f_name)
76
+ if field_defn && visible_field?(field_defn)
77
+ field_defn
78
+ else
79
+ nil
80
+ end
81
+ end
69
82
  end
83
+
84
+ @visible_parent_fields[parent_type][field_name]
70
85
  end
71
86
 
72
87
  # @return [Array<GraphQL::BaseType>] The types which may be member of `type_defn`
73
88
  def possible_types(type_defn)
74
- @schema.possible_types(type_defn).select { |t| visible?(t) }
89
+ @visible_possible_types ||= read_through { |type_defn| @schema.possible_types(type_defn).select { |t| visible?(t) } }
90
+ @visible_possible_types[type_defn]
75
91
  end
76
92
 
77
93
  # @param type_defn [GraphQL::ObjectType, GraphQL::InterfaceType]
78
94
  # @return [Array<GraphQL::Field>] Fields on `type_defn`
79
95
  def fields(type_defn)
80
- type_defn.all_fields.select { |f| visible_field?(f) }
96
+ @visible_fields ||= read_through { |t| t.all_fields.select { |f| visible_field?(f) } }
97
+ @visible_fields[type_defn]
81
98
  end
82
99
 
83
100
  # @param argument_owner [GraphQL::Field, GraphQL::InputObjectType]
84
101
  # @return [Array<GraphQL::Argument>] Visible arguments on `argument_owner`
85
102
  def arguments(argument_owner)
86
- argument_owner.arguments.each_value.select { |a| visible_field?(a) }
103
+ @visible_arguments ||= read_through { |o| o.arguments.each_value.select { |a| visible_field?(a) } }
104
+ @visible_arguments[argument_owner]
87
105
  end
88
106
 
89
107
  # @return [Array<GraphQL::EnumType::EnumValue>] Visible members of `enum_defn`
90
108
  def enum_values(enum_defn)
91
- enum_defn.values.each_value.select { |enum_value_defn| visible?(enum_value_defn) }
109
+ @visible_enum_values ||= read_through { |e| e.values.each_value.select { |enum_value_defn| visible?(enum_value_defn) } }
110
+ @visible_enum_values[enum_defn]
92
111
  end
93
112
 
94
113
  # @return [Array<GraphQL::InterfaceType>] Visible interfaces implemented by `obj_type`
95
114
  def interfaces(obj_type)
96
- obj_type.interfaces.select { |t| visible?(t) }
115
+ @visible_interfaces ||= read_through { |t| t.interfaces.select { |i| visible?(i) } }
116
+ @visible_interfaces[obj_type]
97
117
  end
98
118
 
99
- # @return [Array<GraphQL::Field>] Visible input fields on `input_obj_type`
100
- def input_fields(input_obj_type)
101
- input_obj_type.arguments.each_value.select { |f| visible_field?(f) }
119
+ def directives
120
+ @schema.directives.each_value.select { |d| visible?(d) }
102
121
  end
103
122
 
104
123
  private
@@ -108,7 +127,12 @@ module GraphQL
108
127
  end
109
128
 
110
129
  def visible?(member)
111
- !@mask.call(member)
130
+ @visibility_cache ||= read_through { |m| !@mask.call(m, @context) }
131
+ @visibility_cache[member]
132
+ end
133
+
134
+ def read_through
135
+ Hash.new { |h, k| h[k] = yield(k) }
112
136
  end
113
137
  end
114
138
  end
@@ -34,7 +34,7 @@ module GraphQL
34
34
 
35
35
 
36
36
  def required_input_fields_are_present(type, ast_node)
37
- required_field_names = @warden.input_fields(type)
37
+ required_field_names = @warden.arguments(type)
38
38
  .select { |f| f.type.kind.non_null? }
39
39
  .map(&:name)
40
40
  present_field_names = ast_node.arguments.map(&:name)
@@ -43,7 +43,7 @@ module GraphQL
43
43
  end
44
44
 
45
45
  def present_input_field_values_are_valid(type, ast_node)
46
- field_map = @warden.input_fields(type).reduce({}) { |m, f| m[f.name] = f; m}
46
+ field_map = @warden.arguments(type).reduce({}) { |m, f| m[f.name] = f; m}
47
47
  ast_node.arguments.all? do |value|
48
48
  field = field_map[value.name]
49
49
  field && validate(value.value, field.type)
@@ -5,4 +5,5 @@ GraphQL::STRING_TYPE = GraphQL::ScalarType.define do
5
5
 
6
6
  coerce_result ->(value) { value.to_s }
7
7
  coerce_input ->(value) { value.is_a?(String) ? value : nil }
8
+ default_scalar true
8
9
  end
@@ -24,9 +24,15 @@ module GraphQL
24
24
  # }
25
25
  #
26
26
  class UnionType < GraphQL::BaseType
27
- accepts_definitions :possible_types, :resolve_type
27
+ accepts_definitions :possible_types
28
28
  ensure_defined :possible_types
29
29
 
30
+ def initialize
31
+ super
32
+ @dirty_possible_types = []
33
+ @clean_possible_types = nil
34
+ end
35
+
30
36
  def initialize_copy(other)
31
37
  super
32
38
  @clean_possible_types = nil
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "1.3.0"
3
+ VERSION = "1.4.0"
4
4
  end
data/readme.md CHANGED
@@ -41,22 +41,3 @@ See "Getting Started" on the [website](https://rmosolgo.github.io/graphql-ruby/)
41
41
  - __Features & patches__ are welcome! Consider discussing it in an [issue](https://github.com/rmosolgo/graphql-ruby/issues) or in the [#ruby channel on Slack](https://graphql-slack.herokuapp.com/) to make sure we're on the same page.
42
42
  - __Run the tests__ with `rake test` or start up guard with `bundle exec guard`.
43
43
  - __Build the site__ with `rake site:serve`, then visit `http://localhost:4000/graphql-ruby/`.
44
-
45
- ## To Do
46
-
47
- - StaticValidation improvements ([in progress ??](https://github.com/rmosolgo/graphql-ruby/pull/268))
48
- - Use catch-all type/field/argument definitions instead of terminating traversal
49
- - Reduce ad-hoc traversals?
50
- - Validators are order-dependent, is this a smell?
51
- - Tests for interference between validators are poor
52
- - Maybe this is a candidate for a rewrite?
53
- - Relay:
54
- - Reduce duplication in ArrayConnection / RelationConnection
55
- - Improve API for creating edges (better RANGE_ADD support)
56
- - If the new edge isn't a member of the connection's objects, raise a nice error
57
- - `args` should whitelist keys -- if you request a key that isn't defined for the field, it should 💥
58
- - Support non-instance-eval `.define`, eg `.define { |defn| ... }`
59
- - Add immutable transformation API to AST
60
- - Support working with AST as data
61
- - Adding fields to selections (`__typename` can go anywhere, others are type-specific)
62
- - Renaming fragments from local names to unique names
@@ -0,0 +1,8 @@
1
+ require "spec_helper"
2
+
3
+ describe GraphQL::Directive::SkipDirective do
4
+ let(:directive) { GraphQL::Directive::SkipDirective }
5
+ it "is a default directive" do
6
+ assert directive.default_directive?
7
+ end
8
+ end
@@ -134,7 +134,7 @@ describe GraphQL::Directive do
134
134
  let(:skip?) { true }
135
135
  it "is included" do assert field_included? end
136
136
  end
137
- end
137
+ end
138
138
  end
139
139
  describe "when evaluating conflicting @skip and @include on query selection and fragment" do
140
140
  let(:query_string) {"
@@ -215,13 +215,19 @@ describe GraphQL::Directive do
215
215
  end
216
216
 
217
217
  describe "defining a directive" do
218
- it "can accept an array of arguments" do
219
- directive = GraphQL::Directive.define do
218
+ let(:directive) {
219
+ GraphQL::Directive.define do
220
220
  arguments [GraphQL::Argument.define(name: 'skip')]
221
221
  end
222
+ }
222
223
 
224
+ it "can accept an array of arguments" do
223
225
  assert_equal 1, directive.arguments.length
224
226
  assert_equal 'skip', directive.arguments.first.name
225
227
  end
228
+
229
+ it "is not default" do
230
+ assert_equal false, directive.default_directive?
231
+ end
226
232
  end
227
233
  end
@@ -83,6 +83,22 @@ describe GraphQL::InputObjectType do
83
83
  end
84
84
  end
85
85
 
86
+ if ActionPack::VERSION::MAJOR > 3
87
+ describe "with a ActionController::Parameters" do
88
+ let(:input) do
89
+ ActionController::Parameters.new(
90
+ "source" => "COW",
91
+ "fatContent" => 0.4,
92
+ )
93
+ end
94
+ let(:result) { DairyProductInputType.validate_input(input, PermissiveWarden) }
95
+
96
+ it "returns a valid result" do
97
+ assert(result.valid?)
98
+ end
99
+ end
100
+ end
101
+
86
102
  describe "with bad enum and float" do
87
103
  let(:result) { DairyProductInputType.validate_input({"source" => "KOALA", "fatContent" => "bad_num"}, PermissiveWarden) }
88
104
 
@@ -106,6 +122,57 @@ describe GraphQL::InputObjectType do
106
122
  end
107
123
  end
108
124
 
125
+ describe 'with a string as input' do
126
+ let(:result) { DairyProductInputType.validate_input("just a string", PermissiveWarden) }
127
+
128
+ it "returns an invalid result" do
129
+ assert(!result.valid?)
130
+ end
131
+
132
+ it "has problem with correct path" do
133
+ paths = result.problems.map { |p| p["path"] }
134
+ assert(paths.include?([]))
135
+ end
136
+
137
+ it "has correct problem explanation" do
138
+ assert(result.problems[0]["explanation"].include?("to be a key, value object"))
139
+ end
140
+ end
141
+
142
+ describe 'with an array as input' do
143
+ let(:result) { DairyProductInputType.validate_input(["string array"], PermissiveWarden) }
144
+
145
+ it "returns an invalid result" do
146
+ assert(!result.valid?)
147
+ end
148
+
149
+ it "has problem with correct path" do
150
+ paths = result.problems.map { |p| p["path"] }
151
+ assert(paths.include?([]))
152
+ end
153
+
154
+ it "has correct problem explanation" do
155
+ assert(result.problems[0]["explanation"].include?("to be a key, value object"))
156
+ end
157
+ end
158
+
159
+ describe 'with a int as input' do
160
+ let(:result) { DairyProductInputType.validate_input(10, PermissiveWarden) }
161
+
162
+ it "returns an invalid result" do
163
+ assert(!result.valid?)
164
+ end
165
+
166
+ it "has problem with correct path" do
167
+ paths = result.problems.map { |p| p["path"] }
168
+ assert(paths.include?([]))
169
+ end
170
+
171
+ it "has correct problem explanation" do
172
+ assert(result.problems[0]["explanation"].include?("to be a key, value object"))
173
+ end
174
+ end
175
+
109
176
  describe "with extra argument" do
110
177
  let(:result) { DairyProductInputType.validate_input({"source" => "COW", "fatContent" => 0.4, "isDelicious" => false}, PermissiveWarden) }
111
178
 
@@ -13,7 +13,7 @@ describe GraphQL::Query::Variables do
13
13
  let(:schema) { DummySchema }
14
14
  let(:variables) { GraphQL::Query::Variables.new(
15
15
  schema,
16
- GraphQL::Schema::Warden.new(schema, GraphQL::Query::NullExcept),
16
+ GraphQL::Schema::Warden.new(schema.default_mask, schema: schema, context: nil),
17
17
  ast_variables,
18
18
  provided_variables)
19
19
  }
@@ -79,6 +79,15 @@ describe GraphQL::Relay::ArrayConnection do
79
79
 
80
80
  result = star_wars_query(query_string, "before" => last_cursor, "last" => 2)
81
81
  assert_equal(["X-Wing", "Y-Wing"], get_names(result))
82
+
83
+ result = star_wars_query(query_string, "last" => 2)
84
+ assert_equal(["Millenium Falcon", "Home One"], get_names(result))
85
+ end
86
+
87
+ it 'handles cursors beyond the bounds of the array' do
88
+ overreaching_cursor = Base64.strict_encode64("100")
89
+ result = star_wars_query(query_string, "after" => overreaching_cursor, "first" => 2)
90
+ assert_equal([], get_names(result))
82
91
  end
83
92
 
84
93
  it 'applies custom arguments' do
@@ -36,5 +36,25 @@ describe GraphQL::Relay::ConnectionType do
36
36
  assert_equal [upcased_rebels_name] , bases["edges"].map { |e| e["upcasedParentName"] }.uniq
37
37
  end
38
38
  end
39
+
40
+ describe "connections with nodes field" do
41
+ let(:query_string) {%|
42
+ {
43
+ rebels {
44
+ bases: basesWithCustomEdge {
45
+ nodes {
46
+ name
47
+ }
48
+ }
49
+ }
50
+ }
51
+ |}
52
+
53
+ it "uses the custom edge and custom connection" do
54
+ result = star_wars_query(query_string)
55
+ bases = result["data"]["rebels"]["bases"]
56
+ assert_equal ["Yavin", "Echo Base", "Secret Hideout"] , bases["nodes"].map { |e| e["name"] }
57
+ end
58
+ end
39
59
  end
40
60
  end
@@ -2,6 +2,12 @@
2
2
  require "spec_helper"
3
3
 
4
4
  describe GraphQL::Relay::Node do
5
+ describe ".interface" do
6
+ it "is a default relay type" do
7
+ assert_equal true, GraphQL::Relay::Node.interface.default_relay?
8
+ end
9
+ end
10
+
5
11
  describe ".field" do
6
12
  describe "Custom global IDs" do
7
13
  before do