graphql 1.8.3 → 1.8.4

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql.rb +4 -1
  3. data/lib/graphql/argument.rb +1 -0
  4. data/lib/graphql/authorization.rb +81 -0
  5. data/lib/graphql/boolean_type.rb +0 -1
  6. data/lib/graphql/compatibility/lazy_execution_specification/lazy_schema.rb +2 -1
  7. data/lib/graphql/execution/execute.rb +34 -10
  8. data/lib/graphql/execution/lazy.rb +5 -1
  9. data/lib/graphql/field.rb +7 -1
  10. data/lib/graphql/float_type.rb +0 -1
  11. data/lib/graphql/id_type.rb +0 -1
  12. data/lib/graphql/int_type.rb +0 -1
  13. data/lib/graphql/introspection/entry_points.rb +2 -2
  14. data/lib/graphql/object_type.rb +3 -3
  15. data/lib/graphql/query.rb +6 -0
  16. data/lib/graphql/query/arguments.rb +2 -0
  17. data/lib/graphql/query/context.rb +6 -0
  18. data/lib/graphql/query/variables.rb +7 -1
  19. data/lib/graphql/relay/connection_instrumentation.rb +2 -2
  20. data/lib/graphql/relay/connection_resolve.rb +7 -27
  21. data/lib/graphql/relay/connection_type.rb +1 -0
  22. data/lib/graphql/relay/edge_type.rb +1 -0
  23. data/lib/graphql/relay/edges_instrumentation.rb +9 -25
  24. data/lib/graphql/relay/mutation/instrumentation.rb +1 -2
  25. data/lib/graphql/relay/mutation/resolve.rb +2 -4
  26. data/lib/graphql/relay/node.rb +1 -6
  27. data/lib/graphql/relay/page_info.rb +1 -9
  28. data/lib/graphql/schema.rb +84 -11
  29. data/lib/graphql/schema/argument.rb +13 -0
  30. data/lib/graphql/schema/enum.rb +1 -1
  31. data/lib/graphql/schema/enum_value.rb +4 -0
  32. data/lib/graphql/schema/field.rb +44 -11
  33. data/lib/graphql/schema/interface.rb +20 -0
  34. data/lib/graphql/schema/introspection_system.rb +1 -1
  35. data/lib/graphql/schema/member/base_dsl_methods.rb +25 -3
  36. data/lib/graphql/schema/member/instrumentation.rb +15 -17
  37. data/lib/graphql/schema/mutation.rb +4 -0
  38. data/lib/graphql/schema/object.rb +33 -0
  39. data/lib/graphql/schema/possible_types.rb +2 -0
  40. data/lib/graphql/schema/resolver.rb +10 -0
  41. data/lib/graphql/schema/traversal.rb +9 -2
  42. data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +11 -2
  43. data/lib/graphql/string_type.rb +0 -1
  44. data/lib/graphql/types.rb +7 -0
  45. data/lib/graphql/types/relay.rb +31 -0
  46. data/lib/graphql/types/relay/base_connection.rb +87 -0
  47. data/lib/graphql/types/relay/base_edge.rb +51 -0
  48. data/lib/graphql/types/relay/base_field.rb +22 -0
  49. data/lib/graphql/types/relay/base_interface.rb +29 -0
  50. data/lib/graphql/types/relay/base_object.rb +26 -0
  51. data/lib/graphql/types/relay/node.rb +18 -0
  52. data/lib/graphql/types/relay/page_info.rb +23 -0
  53. data/lib/graphql/unauthorized_error.rb +20 -0
  54. data/lib/graphql/version.rb +1 -1
  55. data/spec/graphql/authorization_spec.rb +684 -0
  56. data/spec/graphql/query/variables_spec.rb +20 -0
  57. data/spec/graphql/relay/connection_instrumentation_spec.rb +1 -1
  58. data/spec/graphql/schema/resolver_spec.rb +31 -0
  59. data/spec/graphql/static_validation/rules/variable_default_values_are_correctly_typed_spec.rb +52 -0
  60. data/spec/support/dummy/schema.rb +16 -0
  61. data/spec/support/star_wars/schema.rb +28 -17
  62. metadata +15 -2
@@ -12,8 +12,7 @@ module GraphQL
12
12
  def self.instrument(type, field)
13
13
  if field.mutation.is_a?(GraphQL::Relay::Mutation) || (field.mutation.is_a?(Class) && field.mutation < GraphQL::Schema::RelayClassicMutation)
14
14
  new_resolve = Mutation::Resolve.new(field.mutation, field.resolve_proc)
15
- new_lazy_resolve = Mutation::Resolve.new(field.mutation, field.lazy_resolve_proc)
16
- field.redefine(resolve: new_resolve, lazy_resolve: new_lazy_resolve)
15
+ field.redefine(resolve: new_resolve)
17
16
  else
18
17
  field
19
18
  end
@@ -21,10 +21,8 @@ module GraphQL
21
21
  err
22
22
  end
23
23
 
24
- if ctx.schema.lazy?(mutation_result)
25
- mutation_result
26
- else
27
- build_result(mutation_result, args, ctx)
24
+ ctx.schema.after_lazy(mutation_result) do |res|
25
+ build_result(res, args, ctx)
28
26
  end
29
27
  end
30
28
 
@@ -41,12 +41,7 @@ module GraphQL
41
41
 
42
42
  # @return [GraphQL::InterfaceType] The interface which all Relay types must implement
43
43
  def self.interface
44
- @interface ||= GraphQL::InterfaceType.define do
45
- name("Node")
46
- description("An object with an ID.")
47
- field(:id, types.ID.to_non_null_type, "ID of the object.")
48
- default_relay(true)
49
- end
44
+ @interface ||= GraphQL::Types::Relay::Node.graphql_definition
50
45
  end
51
46
 
52
47
  # A field resolve for finding objects by IDs
@@ -2,14 +2,6 @@
2
2
  module GraphQL
3
3
  module Relay
4
4
  # Wrap a Connection and expose its page info
5
- PageInfo = GraphQL::ObjectType.define do
6
- name("PageInfo")
7
- description("Information about pagination in a connection.")
8
- field :hasNextPage, !types.Boolean, "When paginating forwards, are there more items?", property: :has_next_page
9
- field :hasPreviousPage, !types.Boolean, "When paginating backwards, are there more items?", property: :has_previous_page
10
- field :startCursor, types.String, "When paginating backwards, the cursor to continue.", property: :start_cursor
11
- field :endCursor, types.String, "When paginating forwards, the cursor to continue.", property: :end_cursor
12
- default_relay true
13
- end
5
+ PageInfo = GraphQL::Types::Relay::PageInfo.graphql_definition
14
6
  end
15
7
  end
@@ -69,6 +69,7 @@ module GraphQL
69
69
  # end
70
70
  #
71
71
  class Schema
72
+ extend Forwardable
72
73
  extend GraphQL::Schema::Member::AcceptsDefinition
73
74
  include GraphQL::Define::InstanceDefinable
74
75
  accepts_definitions \
@@ -155,7 +156,7 @@ module GraphQL
155
156
  @parse_error_proc = DefaultParseError
156
157
  @instrumenters = Hash.new { |h, k| h[k] = [] }
157
158
  @lazy_methods = GraphQL::Execution::Lazy::LazyMethodMap.new
158
- @lazy_methods.set(GraphQL::Relay::ConnectionResolve::LazyNodesWrapper, :never_called)
159
+ @lazy_methods.set(GraphQL::Execution::Lazy, :value)
159
160
  @cursor_encoder = Base64Encoder
160
161
  # Default to the built-in execution strategy:
161
162
  @query_execution_strategy = self.class.default_execution_strategy
@@ -534,6 +535,10 @@ module GraphQL
534
535
  @type_error_proc = new_proc
535
536
  end
536
537
 
538
+ # Can't delegate to `class`
539
+ alias :_schema_class :class
540
+ def_delegators :_schema_class, :visible?, :accessible?, :authorized?, :unauthorized_object, :inaccessible_fields
541
+
537
542
  # A function to call when {#execute} receives an invalid query string
538
543
  #
539
544
  # @see {DefaultParseError} is the default behavior.
@@ -647,7 +652,7 @@ module GraphQL
647
652
  :static_validator, :introspection_system,
648
653
  :query_analyzers, :middleware, :tracers, :instrumenters,
649
654
  :query_execution_strategy, :mutation_execution_strategy, :subscription_execution_strategy,
650
- :validate, :multiplex_analyzers, :lazy?, :lazy_method_name,
655
+ :validate, :multiplex_analyzers, :lazy?, :lazy_method_name, :after_lazy,
651
656
  # Configuration
652
657
  :max_complexity=, :max_depth=,
653
658
  :metadata,
@@ -696,6 +701,7 @@ module GraphQL
696
701
  schema_defn.cursor_encoder = cursor_encoder
697
702
  schema_defn.tracers.concat(defined_tracers)
698
703
  schema_defn.query_analyzers.concat(defined_query_analyzers)
704
+ schema_defn.query_analyzers << GraphQL::Authorization::Analyzer
699
705
  schema_defn.middleware.concat(defined_middleware)
700
706
  schema_defn.multiplex_analyzers.concat(defined_multiplex_analyzers)
701
707
  defined_instrumenters.each do |step, insts|
@@ -835,6 +841,41 @@ module GraphQL
835
841
  raise NotImplementedError, "#{self.name}.id_from_object(object, type, ctx) must be implemented to create global ids (tried to create an id for `#{object.inspect}`)"
836
842
  end
837
843
 
844
+ def visible?(member, context)
845
+ call_on_type_class(member, :visible?, context, default: true)
846
+ end
847
+
848
+ def accessible?(member, context)
849
+ call_on_type_class(member, :accessible?, context, default: true)
850
+ end
851
+
852
+ # This hook is called when a client tries to access one or more
853
+ # fields that fail the `accessible?` check.
854
+ #
855
+ # By default, an error is added to the response. Override this hook to
856
+ # track metrics or return a different error to the client.
857
+ #
858
+ # @param error [InaccessibleFieldsError] The analysis error for this check
859
+ # @return [AnalysisError, nil] Return an error to skip the query
860
+ def inaccessible_fields(error)
861
+ error
862
+ end
863
+
864
+ # This hook is called when an object fails an `authorized?` check.
865
+ # You might report to your bug tracker here, so you can correct
866
+ # the field resolvers not to return unauthorized objects.
867
+ #
868
+ # By default, this hook just replaces the unauthorized object with `nil`.
869
+ #
870
+ # If you want to add an error to the `"errors"` key, raise a {GraphQL::ExecutionError}
871
+ # in this hook.
872
+ #
873
+ # @param unauthorized_error [GraphQL::UnauthorizedError]
874
+ # @return [Object] The returned object will be put in the GraphQL response
875
+ def unauthorized_object(unauthorized_error)
876
+ nil
877
+ end
878
+
838
879
  def type_error(type_err, ctx)
839
880
  DefaultTypeError.call(type_err, ctx)
840
881
  end
@@ -905,6 +946,29 @@ module GraphQL
905
946
  def defined_multiplex_analyzers
906
947
  @defined_multiplex_analyzers ||= []
907
948
  end
949
+
950
+ # Given this schema member, find the class-based definition object
951
+ # whose `method_name` should be treated as an application hook
952
+ # @see {.visible?}
953
+ # @see {.accessible?}
954
+ # @see {.authorized?}
955
+ def call_on_type_class(member, method_name, *args, default:)
956
+ member = if member.respond_to?(:metadata)
957
+ member.metadata[:type_class] || member
958
+ else
959
+ member
960
+ end
961
+
962
+ if member.respond_to?(:relay_node_type) && (t = member.relay_node_type)
963
+ member = t
964
+ end
965
+
966
+ if member.respond_to?(method_name)
967
+ member.public_send(method_name, *args)
968
+ else
969
+ default
970
+ end
971
+ end
908
972
  end
909
973
 
910
974
 
@@ -923,6 +987,24 @@ module GraphQL
923
987
  end
924
988
  end
925
989
 
990
+ # Call the given block at the right time, either:
991
+ # - Right away, if `value` is not registered with `lazy_resolve`
992
+ # - After resolving `value`, if it's registered with `lazy_resolve` (eg, `Promise`)
993
+ # @api private
994
+ def after_lazy(value)
995
+ if (lazy_method = lazy_method_name(value))
996
+ GraphQL::Execution::Lazy.new do
997
+ result = value.public_send(lazy_method)
998
+ # The returned result might also be lazy, so check it, too
999
+ after_lazy(result) do |final_result|
1000
+ yield(final_result)
1001
+ end
1002
+ end
1003
+ else
1004
+ yield(value)
1005
+ end
1006
+ end
1007
+
926
1008
  protected
927
1009
 
928
1010
  def rescues?
@@ -937,15 +1019,6 @@ module GraphQL
937
1019
 
938
1020
  private
939
1021
 
940
- # Wrap Relay-related objects in wrappers
941
- # @api private
942
- BUILT_IN_INSTRUMENTERS = [
943
- GraphQL::Relay::ConnectionInstrumentation,
944
- GraphQL::Relay::EdgesInstrumentation,
945
- GraphQL::Relay::Mutation::Instrumentation,
946
- GraphQL::Schema::Member::Instrumentation,
947
- ]
948
-
949
1022
  def rebuild_artifacts
950
1023
  if @rebuilding_artifacts
951
1024
  raise CyclicalDefinitionError, "Part of the schema build process re-triggered the schema build process, causing an infinite loop. Avoid using Schema#types, Schema#possible_types, and Schema#get_field during schema build."
@@ -9,6 +9,7 @@ module GraphQL
9
9
 
10
10
  # @return [String] the GraphQL name for this argument, camelized unless `camelize: false` is provided
11
11
  attr_reader :name
12
+ alias :graphql_name :name
12
13
 
13
14
  # @return [GraphQL::Schema::Field, Class] The field or input object this argument belongs to
14
15
  attr_reader :owner
@@ -53,6 +54,18 @@ module GraphQL
53
54
  end
54
55
  end
55
56
 
57
+ def visible?(context)
58
+ true
59
+ end
60
+
61
+ def accessible?(context)
62
+ true
63
+ end
64
+
65
+ def authorized?(obj, ctx)
66
+ true
67
+ end
68
+
56
69
  def to_graphql
57
70
  argument = GraphQL::Argument.new
58
71
  argument.name = @name
@@ -24,7 +24,7 @@ module GraphQL
24
24
 
25
25
  class << self
26
26
  extend Forwardable
27
- def_delegators :graphql_definition, :coerce_isolated_input, :coerce_isolated_result
27
+ def_delegators :graphql_definition, :coerce_isolated_input, :coerce_isolated_result, :coerce_input, :coerce_result
28
28
 
29
29
  # Define a value for this enum
30
30
  # @param graphql_name [String, Symbol] the GraphQL value for this, usually `SCREAMING_CASE`
@@ -69,6 +69,10 @@ module GraphQL
69
69
  enum_value.metadata[:type_class] = self
70
70
  enum_value
71
71
  end
72
+
73
+ def visible?(_ctx); true; end
74
+ def accessible?(_ctx); true; end
75
+ def authorized?(_ctx); true; end
72
76
  end
73
77
  end
74
78
  end
@@ -8,7 +8,8 @@ module GraphQL
8
8
  include GraphQL::Schema::Member::HasArguments
9
9
 
10
10
  # @return [String] the GraphQL name for this field, camelized unless `camelize: false` is provided
11
- attr_accessor :name
11
+ attr_reader :name
12
+ alias :graphql_name :name
12
13
 
13
14
  # @return [String]
14
15
  attr_accessor :description
@@ -274,20 +275,52 @@ module GraphQL
274
275
  raise ArgumentError, "Failed to build return type for #{@owner.graphql_name}.#{name} from #{@return_type_expr.inspect}: #{$!.message}", $!.backtrace
275
276
  end
276
277
 
278
+ def visible?(context)
279
+ if @resolver_class
280
+ @resolver_class.visible?(context)
281
+ else
282
+ true
283
+ end
284
+ end
285
+
286
+ def accessible?(context)
287
+ if @resolver_class
288
+ @resolver_class.accessible?(context)
289
+ else
290
+ true
291
+ end
292
+ end
293
+
294
+ def authorized?(object, context)
295
+ if @resolver_class
296
+ @resolver_class.authorized?(object, context)
297
+ else
298
+ true
299
+ end
300
+ end
301
+
277
302
  # Implement {GraphQL::Field}'s resolve API.
278
303
  #
279
304
  # Eventually, we might hook up field instances to execution in another way. TBD.
280
305
  def resolve_field(obj, args, ctx)
281
- if @resolve_proc
282
- # Might be nil, still want to call the func in that case
283
- inner_obj = obj && obj.object
284
- @resolve_proc.call(inner_obj, args, ctx)
285
- elsif @resolver_class
286
- inner_obj = obj && obj.object
287
- singleton_inst = @resolver_class.new(object: inner_obj, context: ctx.query.context)
288
- public_send_field(singleton_inst, args, ctx)
289
- else
290
- public_send_field(obj, args, ctx)
306
+ ctx.schema.after_lazy(obj) do |after_obj|
307
+ # First, apply auth ...
308
+ query_ctx = ctx.query.context
309
+ inner_obj = after_obj && after_obj.object
310
+ if authorized?(inner_obj, query_ctx) && arguments.each_value.all? { |a| a.authorized?(inner_obj, query_ctx) }
311
+ # Then if it passed, resolve the field
312
+ if @resolve_proc
313
+ # Might be nil, still want to call the func in that case
314
+ @resolve_proc.call(inner_obj, args, ctx)
315
+ elsif @resolver_class
316
+ singleton_inst = @resolver_class.new(object: inner_obj, context: query_ctx)
317
+ public_send_field(singleton_inst, args, ctx)
318
+ else
319
+ public_send_field(after_obj, args, ctx)
320
+ end
321
+ else
322
+ nil
323
+ end
291
324
  end
292
325
  end
293
326
 
@@ -17,6 +17,26 @@ module GraphQL
17
17
  self::DefinitionMethods.module_eval(&block)
18
18
  end
19
19
 
20
+ # The interface is visible if any of its possible types are visible
21
+ def visible?(context)
22
+ context.schema.possible_types(self).each do |type|
23
+ if context.schema.visible?(type, context)
24
+ return true
25
+ end
26
+ end
27
+ false
28
+ end
29
+
30
+ # The interface is accessible if any of its possible types are accessible
31
+ def accessible?(context)
32
+ context.schema.possible_types(self).each do |type|
33
+ if context.schema.accessible?(type, context)
34
+ return true
35
+ end
36
+ end
37
+ false
38
+ end
39
+
20
40
  # Here's the tricky part. Make sure behavior keeps making its way down the inheritance chain.
21
41
  def included(child_class)
22
42
  if !child_class.is_a?(Class)
@@ -84,7 +84,7 @@ module GraphQL
84
84
  if obj.is_a?(GraphQL::Schema::Object)
85
85
  obj = obj.object
86
86
  end
87
- wrapped_object = @object_class.new(obj, query_ctx)
87
+ wrapped_object = @object_class.authorized_new(obj, query_ctx)
88
88
  @inner_resolve.call(wrapped_object, args, ctx)
89
89
  end
90
90
  end
@@ -22,7 +22,7 @@ module GraphQL
22
22
  overridden
23
23
  else # Fallback to Ruby constant name
24
24
  raise NotImplementedError, 'Anonymous class should declare an `graphql_name`' if name.nil?
25
-
25
+
26
26
  name.split("::").last.sub(/Type\Z/, "")
27
27
  end
28
28
  end
@@ -72,12 +72,34 @@ module GraphQL
72
72
  raise NotImplementedError
73
73
  end
74
74
 
75
+ alias :unwrap :itself
76
+
75
77
  def overridden_graphql_name
76
78
  @graphql_name || find_inherited_method(:overridden_graphql_name, nil)
77
79
  end
78
80
 
79
- def unwrap
80
- self
81
+ def visible?(context)
82
+ if @mutation
83
+ @mutation.visible?(context)
84
+ else
85
+ true
86
+ end
87
+ end
88
+
89
+ def accessible?(context)
90
+ if @mutation
91
+ @mutation.accessible?(context)
92
+ else
93
+ true
94
+ end
95
+ end
96
+
97
+ def authorized?(object, context)
98
+ if @mutation
99
+ @mutation.authorized?(object, context)
100
+ else
101
+ true
102
+ end
81
103
  end
82
104
 
83
105
  private
@@ -27,7 +27,7 @@ module GraphQL
27
27
  # If it has a wrapper, apply it
28
28
  wrapper_class = root_type.metadata[:type_class]
29
29
  if wrapper_class
30
- new_root_value = wrapper_class.new(query.root_value, query.context)
30
+ new_root_value = wrapper_class.authorized_new(query.root_value, query.context)
31
31
  query.root_value = new_root_value
32
32
  end
33
33
  end
@@ -72,39 +72,37 @@ module GraphQL
72
72
 
73
73
  def call(obj, args, ctx)
74
74
  result = @inner_resolve.call(obj, args, ctx)
75
- if ctx.schema.lazy?(result)
76
- # Wrap it later
77
- result
78
- elsif ctx.skip == result
75
+ if ctx.skip == result || ctx.schema.lazy?(result) || result.nil? || result.is_a?(GraphQL::ExecutionError) || ctx.wrapped_object
79
76
  result
80
77
  else
81
- proxy_to_depth(result, @list_depth, @inner_return_type, ctx)
78
+ ctx.wrapped_object = true
79
+ proxy_to_depth(result, @list_depth, ctx)
82
80
  end
81
+ rescue GraphQL::UnauthorizedError => err
82
+ ctx.schema.unauthorized_object(err)
83
83
  end
84
84
 
85
85
  private
86
86
 
87
- def proxy_to_depth(obj, depth, type, ctx)
88
- if obj.nil?
89
- obj
90
- elsif depth > 0
91
- obj.map { |inner_obj| proxy_to_depth(inner_obj, depth - 1, type, ctx) }
87
+ def proxy_to_depth(inner_obj, depth, ctx)
88
+ if depth > 0
89
+ inner_obj.map { |i| proxy_to_depth(i, depth - 1, ctx) }
92
90
  else
93
- concrete_type = case type
91
+ concrete_type = case @inner_return_type
94
92
  when GraphQL::UnionType, GraphQL::InterfaceType
95
- ctx.query.resolve_type(type, obj)
93
+ ctx.query.resolve_type(@inner_return_type, inner_obj)
96
94
  when GraphQL::ObjectType
97
- type
95
+ @inner_return_type
98
96
  else
99
- raise "unexpected proxying type #{type} for #{obj} at #{ctx.owner_type}.#{ctx.field.name}"
97
+ raise "unexpected proxying type #{@inner_return_type} for #{inner_obj} at #{ctx.owner_type}.#{ctx.field.name}"
100
98
  end
101
99
 
102
100
  if concrete_type && (object_class = concrete_type.metadata[:type_class])
103
101
  # use the query-level context here, since it won't be field-specific anyways
104
102
  query_ctx = ctx.query.context
105
- object_class.new(obj, query_ctx)
103
+ object_class.authorized_new(inner_obj, query_ctx)
106
104
  else
107
- obj
105
+ inner_obj
108
106
  end
109
107
  end
110
108
  end