graphql 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
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