graphql 1.8.3 → 1.8.4

Sign up to get free protection for your applications and to get access to all the features.
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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9b0ed1ae533e9983c59332bf6ad38dd5db6e2c92
4
- data.tar.gz: 928cd4a961b87fdc43ffe2b71bfd34cea0ae0349
3
+ metadata.gz: 879e9096d14bb82f2c28f57c60d34e66c6706181
4
+ data.tar.gz: c39e6b2c81303fba7c2dfa1527932af7d8eb9d31
5
5
  SHA512:
6
- metadata.gz: 1f35a05c996558a3742b79e71e89bb83b5dbc41d432e42d6dd06a9a00c3a616c5d0cb1bd23d80b48b66dff52285866ce614d781fd6abe6c61f64df57f3fa41ce
7
- data.tar.gz: 6d3fba06e769adf32bd38f450c5d0d7b21f462e8b93160663c1f1c5f9731e1bf27985b25b20b850e9335be114dc28298f9a6dafd323ffb4d2b5640ae33906f80
6
+ metadata.gz: 79d69e540d7cb3063ce6fea65286f0e66634edeced066930cbc63de169eb9f2a53412f606aacfbf02e28fccfd11781122e95cc31ee234073c6b0f3246c7ac26e
7
+ data.tar.gz: 4fe1822b9f021f064a2ba383c65a8c889a4acd08927f0c99ed9d9e473890474c3529afa97ed5cb95fd4f67f5d7754b595cdd654734a2fa595ecfebebeb4ff2a8
@@ -67,8 +67,9 @@ require "graphql/language"
67
67
  require "graphql/analysis"
68
68
  require "graphql/tracing"
69
69
  require "graphql/execution"
70
- require "graphql/relay"
71
70
  require "graphql/schema"
71
+ require "graphql/types"
72
+ require "graphql/relay"
72
73
  require "graphql/boolean_type"
73
74
  require "graphql/float_type"
74
75
  require "graphql/id_type"
@@ -98,3 +99,5 @@ require "graphql/parse_error"
98
99
  require "graphql/backtrace"
99
100
 
100
101
  require "graphql/deprecated_dsl"
102
+ require "graphql/authorization"
103
+ require "graphql/unauthorized_error"
@@ -38,6 +38,7 @@ module GraphQL
38
38
  accepts_definitions :name, :type, :description, :default_value, :as, :prepare
39
39
  attr_accessor :type, :description, :default_value, :name, :as
40
40
  attr_accessor :ast_node
41
+ alias :graphql_name :name
41
42
 
42
43
  ensure_defined(:name, :description, :default_value, :type=, :type, :as, :expose_as, :prepare)
43
44
 
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ module Authorization
4
+ class InaccessibleFieldsError < GraphQL::AnalysisError
5
+ # @return [Array<Schema::Field, GraphQL::Field>] Fields that failed `.accessible?` checks
6
+ attr_reader :fields
7
+
8
+ # @return [GraphQL::Query::Context] The current query's context
9
+ attr_reader :context
10
+
11
+ # @return [Array<GraphQL::InternalRepresentation::Node>] The visited nodes that failed `.accessible?` checks
12
+ # @see {#fields} for the Field definitions
13
+ attr_reader :irep_nodes
14
+
15
+ def initialize(fields:, irep_nodes:, context:)
16
+ @fields = fields
17
+ @irep_nodes = irep_nodes
18
+ @context = context
19
+ super("Some fields in this query are not accessible: #{fields.map(&:graphql_name).join(", ")}")
20
+ end
21
+ end
22
+
23
+ module Analyzer
24
+ module_function
25
+ def initial_value(query)
26
+ {
27
+ schema: query.schema,
28
+ context: query.context,
29
+ inaccessible_nodes: [],
30
+ }
31
+ end
32
+
33
+ def call(memo, visit_type, irep_node)
34
+ if visit_type == :enter
35
+ field = irep_node.definition
36
+ if field
37
+ schema = memo[:schema]
38
+ ctx = memo[:context]
39
+ next_field_accessible = schema.accessible?(field, ctx)
40
+ if !next_field_accessible
41
+ memo[:inaccessible_nodes] << irep_node
42
+ else
43
+ arg_accessible = true
44
+ irep_node.arguments.argument_values.each do |name, arg_value|
45
+ arg_accessible = schema.accessible?(arg_value.definition, ctx)
46
+ if !arg_accessible
47
+ memo[:inaccessible_nodes] << irep_node
48
+ break
49
+ end
50
+ end
51
+ if arg_accessible
52
+ return_type = field.type.unwrap
53
+ next_type_accessible = schema.accessible?(return_type, ctx)
54
+ if !next_type_accessible
55
+ memo[:inaccessible_nodes] << irep_node
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ memo
62
+ end
63
+
64
+ def final_value(memo)
65
+ nodes = memo[:inaccessible_nodes]
66
+ if nodes.any?
67
+ fields = nodes.map do |node|
68
+ field_inst = node.definition
69
+ # Get the "source of truth" for this field
70
+ field_inst.metadata[:type_class] || field_inst
71
+ end
72
+ context = memo[:context]
73
+ err = InaccessibleFieldsError.new(fields: fields, irep_nodes: nodes, context: context)
74
+ context.schema.inaccessible_fields(err)
75
+ else
76
+ nil
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -1,3 +1,2 @@
1
1
  # frozen_string_literal: true
2
- require "graphql/types/boolean"
3
2
  GraphQL::BOOLEAN_TYPE = GraphQL::Types::Boolean.graphql_definition
@@ -32,7 +32,8 @@ module GraphQL
32
32
  @context[:pushes] << @context[:lazy_pushes]
33
33
  @context[:lazy_pushes] = []
34
34
  end
35
- self
35
+ # Something that _behaves_ like this object, but isn't registered lazy
36
+ OpenStruct.new(value: @value)
36
37
  end
37
38
  end
38
39
  end
@@ -110,23 +110,47 @@ module GraphQL
110
110
  err
111
111
  end
112
112
 
113
- # If the returned object is lazy (unfinished),
114
- # assign the lazy object to `.value=` so we can resolve it later.
115
- # When we resolve it later, reassign it to `.value=` so that
116
- # the finished value replaces the unfinished one.
117
- #
118
- # If the returned object is finished, continue to coerce
119
- # and resolve child fields
120
- if query.schema.lazy?(raw_value)
113
+ if field_ctx.schema.lazy?(raw_value)
121
114
  field_ctx.value = Execution::Lazy.new {
122
115
  inner_value = field_ctx.trace("execute_field_lazy", {context: field_ctx}) {
123
116
  begin
124
- field.lazy_resolve(raw_value, arguments, field_ctx)
117
+ begin
118
+ field_ctx.field.lazy_resolve(raw_value, arguments, field_ctx)
119
+ rescue GraphQL::UnauthorizedError => err
120
+ field_ctx.schema.unauthorized_object(err)
121
+ end
125
122
  rescue GraphQL::ExecutionError => err
126
123
  err
127
124
  end
128
125
  }
129
- field_ctx.value = continue_resolve_field(inner_value, field_ctx)
126
+ continue_or_wait(inner_value, arguments, field_ctx)
127
+ }
128
+ else
129
+ continue_or_wait(raw_value, arguments, field_ctx)
130
+ end
131
+ end
132
+
133
+ # If the returned object is lazy (unfinished),
134
+ # assign the lazy object to `.value=` so we can resolve it later.
135
+ # When we resolve it later, reassign it to `.value=` so that
136
+ # the finished value replaces the unfinished one.
137
+ #
138
+ # If the returned object is finished, continue to coerce
139
+ # and resolve child fields
140
+ def continue_or_wait(raw_value, arguments, field_ctx)
141
+ if (lazy_method = field_ctx.schema.lazy_method_name(raw_value))
142
+ field_ctx.value = Execution::Lazy.new {
143
+ inner_value = begin
144
+ begin
145
+ raw_value.public_send(lazy_method)
146
+ rescue GraphQL::UnauthorizedError => err
147
+ field_ctx.schema.unauthorized_object(err)
148
+ end
149
+ rescue GraphQL::ExecutionError => err
150
+ err
151
+ end
152
+
153
+ field_ctx.value = continue_or_wait(inner_value, arguments, field_ctx)
130
154
  }
131
155
  else
132
156
  field_ctx.value = continue_resolve_field(raw_value, field_ctx)
@@ -31,7 +31,11 @@ module GraphQL
31
31
  if !@resolved
32
32
  @resolved = true
33
33
  @value = begin
34
- @get_value_func.call
34
+ v = @get_value_func.call
35
+ if v.is_a?(Lazy)
36
+ v = v.value
37
+ end
38
+ v
35
39
  rescue GraphQL::ExecutionError => err
36
40
  err
37
41
  end
@@ -157,6 +157,7 @@ module GraphQL
157
157
 
158
158
  # @return [String] The name of this field on its {GraphQL::ObjectType} (or {GraphQL::InterfaceType})
159
159
  attr_accessor :name
160
+ alias :graphql_name :name
160
161
 
161
162
  # @return [String, nil] The client-facing description of this field
162
163
  attr_accessor :description
@@ -323,7 +324,12 @@ module GraphQL
323
324
  module DefaultLazyResolve
324
325
  def self.call(obj, args, ctx)
325
326
  method_name = ctx.schema.lazy_method_name(obj)
326
- obj.public_send(method_name)
327
+ next_obj = obj.public_send(method_name)
328
+ if ctx.schema.lazy?(next_obj)
329
+ call(next_obj, args, ctx)
330
+ else
331
+ next_obj
332
+ end
327
333
  end
328
334
  end
329
335
  end
@@ -1,3 +1,2 @@
1
1
  # frozen_string_literal: true
2
- require "graphql/types/float"
3
2
  GraphQL::FLOAT_TYPE = GraphQL::Types::Float.graphql_definition
@@ -1,3 +1,2 @@
1
1
  # frozen_string_literal: true
2
- require "graphql/types/id"
3
2
  GraphQL::ID_TYPE = GraphQL::Types::ID.graphql_definition
@@ -1,3 +1,2 @@
1
1
  # frozen_string_literal: true
2
- require "graphql/types/int"
3
2
  GraphQL::INT_TYPE = GraphQL::Types::Int.graphql_definition
@@ -11,7 +11,7 @@ module GraphQL
11
11
  # Apply wrapping manually since this field isn't wrapped by instrumentation
12
12
  schema = @context.query.schema
13
13
  schema_type = schema.introspection_system.schema_type
14
- schema_type.metadata[:type_class].new(schema, @context)
14
+ schema_type.metadata[:type_class].authorized_new(schema, @context)
15
15
  end
16
16
 
17
17
  def __type(name:)
@@ -19,7 +19,7 @@ module GraphQL
19
19
  if type
20
20
  # Apply wrapping manually since this field isn't wrapped by instrumentation
21
21
  type_type = @context.schema.introspection_system.type_type
22
- type_type.metadata[:type_class].new(type, @context)
22
+ type_type.metadata[:type_class].authorized_new(type, @context)
23
23
  else
24
24
  nil
25
25
  end
@@ -22,11 +22,11 @@ module GraphQL
22
22
  # end
23
23
  #
24
24
  class ObjectType < GraphQL::BaseType
25
- accepts_definitions :interfaces, :fields, :mutation, field: GraphQL::Define::AssignObjectField
25
+ accepts_definitions :interfaces, :fields, :mutation, :relay_node_type, field: GraphQL::Define::AssignObjectField
26
26
  accepts_definitions implements: ->(type, *interfaces, inherit: false) { type.implements(interfaces, inherit: inherit) }
27
27
 
28
- attr_accessor :fields, :mutation
29
- ensure_defined(:fields, :mutation, :interfaces)
28
+ attr_accessor :fields, :mutation, :relay_node_type
29
+ ensure_defined(:fields, :mutation, :interfaces, :relay_node_type)
30
30
 
31
31
  # @!attribute fields
32
32
  # @return [Hash<String => GraphQL::Field>] Map String fieldnames to their {GraphQL::Field} implementations
@@ -128,6 +128,12 @@ module GraphQL
128
128
 
129
129
  @result_values = nil
130
130
  @executed = false
131
+
132
+ # TODO add a general way to define schema-level filters
133
+ # TODO also add this to schema dumps
134
+ if @schema.respond_to?(:visible?)
135
+ merge_filters(only: @schema.method(:visible?))
136
+ end
131
137
  end
132
138
 
133
139
  def subscription_update?
@@ -35,6 +35,8 @@ module GraphQL
35
35
  end
36
36
  end
37
37
 
38
+ attr_reader :argument_values
39
+
38
40
  def initialize(values, context:, defaults_used:)
39
41
  @argument_values = values.inject({}) do |memo, (inner_key, inner_value)|
40
42
  arg_name = inner_key.to_s
@@ -207,8 +207,14 @@ module GraphQL
207
207
  @query = context.query
208
208
  @schema = context.schema
209
209
  @tracers = @query.tracers
210
+ # This hack flag is required by ConnectionResolve
211
+ @wrapped_connection = false
212
+ @wrapped_object = false
210
213
  end
211
214
 
215
+ # @api private
216
+ attr_accessor :wrapped_connection, :wrapped_object
217
+
212
218
  def path
213
219
  @path ||= @parent.path.dup << @key
214
220
  end
@@ -30,7 +30,13 @@ module GraphQL
30
30
  provided_value = @provided_variables[variable_name]
31
31
  value_was_provided = @provided_variables.key?(variable_name)
32
32
 
33
- validation_result = variable_type.validate_input(provided_value, ctx)
33
+ begin
34
+ validation_result = variable_type.validate_input(provided_value, ctx)
35
+ rescue GraphQL::CoercionError => ex
36
+ validation_result = GraphQL::Query::InputValidationResult.new
37
+ validation_result.add_problem(ex.message)
38
+ end
39
+
34
40
  if !validation_result.valid?
35
41
  # This finds variables that were required but not provided
36
42
  @errors << GraphQL::Query::VariableValidationError.new(ast_variable, variable_type, provided_value, validation_result)
@@ -36,8 +36,8 @@ module GraphQL
36
36
  connection_arguments = default_arguments.merge(field.arguments)
37
37
  original_resolve = field.resolve_proc
38
38
  original_lazy_resolve = field.lazy_resolve_proc
39
- connection_resolve = GraphQL::Relay::ConnectionResolve.new(field, original_resolve, lazy: false)
40
- connection_lazy_resolve = GraphQL::Relay::ConnectionResolve.new(field, original_lazy_resolve, lazy: true)
39
+ connection_resolve = GraphQL::Relay::ConnectionResolve.new(field, original_resolve)
40
+ connection_lazy_resolve = GraphQL::Relay::ConnectionResolve.new(field, original_lazy_resolve)
41
41
  field.redefine(
42
42
  resolve: connection_resolve,
43
43
  lazy_resolve: connection_lazy_resolve,
@@ -2,34 +2,24 @@
2
2
  module GraphQL
3
3
  module Relay
4
4
  class ConnectionResolve
5
- def initialize(field, underlying_resolve, lazy:)
5
+ def initialize(field, underlying_resolve)
6
6
  @field = field
7
7
  @underlying_resolve = underlying_resolve
8
8
  @max_page_size = field.connection_max_page_size
9
- @lazy = lazy
10
9
  end
11
10
 
12
11
  def call(obj, args, ctx)
13
- if @lazy && obj.is_a?(LazyNodesWrapper)
14
- parent = obj.parent
15
- obj = obj.lazy_object
16
- else
17
- parent = obj
18
- end
12
+ # in a lazy ressolve hook, obj is the promise,
13
+ # get the object that the promise was
14
+ # originally derived from
15
+ parent = ctx.object
19
16
 
20
17
  nodes = @underlying_resolve.call(obj, args, ctx)
21
18
 
22
- if nodes.nil?
23
- nil
24
- elsif ctx.schema.lazy?(nodes)
25
- if !@lazy
26
- LazyNodesWrapper.new(obj, nodes)
27
- else
28
- nodes
29
- end
30
- elsif nodes.is_a?(GraphQL::Execution::Execute::Skip)
19
+ if nodes.nil? || ctx.schema.lazy?(nodes) || nodes.is_a?(GraphQL::Execution::Execute::Skip) || ctx.wrapped_connection
31
20
  nodes
32
21
  else
22
+ ctx.wrapped_connection = true
33
23
  build_connection(nodes, args, parent, ctx)
34
24
  end
35
25
  end
@@ -48,16 +38,6 @@ module GraphQL
48
38
  connection_class.new(nodes, args, field: @field, max_page_size: @max_page_size, parent: parent, context: ctx)
49
39
  end
50
40
  end
51
-
52
- # A container for the proper `parent` of connection nodes.
53
- # Without this wrapper, the lazy object _itself_ is passed into `build_connection`
54
- # and it becomes the parent, which is wrong.
55
- #
56
- # We can get away with it because we know that this instrumentation will be applied last.
57
- # That means its code after `underlying_resolve` will be _last_ on the way in.
58
- # And, its code before `underlying_resolve` will be _first_ during lazy resolution.
59
- # @api private
60
- LazyNodesWrapper = Struct.new(:parent, :lazy_object)
61
41
  end
62
42
  end
63
43
  end
@@ -31,6 +31,7 @@ module GraphQL
31
31
  end
32
32
 
33
33
  field :pageInfo, !PageInfo, "Information to aid in pagination.", property: :page_info
34
+ relay_node_type(wrapped_type)
34
35
  block && instance_eval(&block)
35
36
  end
36
37
  end
@@ -9,6 +9,7 @@ module GraphQL
9
9
  description "An edge in a connection."
10
10
  field :node, wrapped_type, "The item at the end of the edge."
11
11
  field :cursor, !types.String, "A cursor for use in pagination."
12
+ relay_node_type(wrapped_type)
12
13
  block && instance_eval(&block)
13
14
  end
14
15
  end
@@ -4,15 +4,12 @@ module GraphQL
4
4
  module EdgesInstrumentation
5
5
  def self.instrument(type, field)
6
6
  if field.edges?
7
- edges_resolve = EdgesResolve.new(
8
- edge_class: field.edge_class,
9
- resolve: field.resolve_proc,
10
- lazy_resolve: field.lazy_resolve_proc,
11
- )
7
+ edges_resolve = EdgesResolve.new(edge_class: field.edge_class, resolve: field.resolve_proc)
8
+ edges_lazy_resolve = EdgesResolve.new(edge_class: field.edge_class, resolve: field.lazy_resolve_proc)
12
9
 
13
10
  field.redefine(
14
- resolve: edges_resolve.method(:resolve),
15
- lazy_resolve: edges_resolve.method(:lazy_resolve),
11
+ resolve: edges_resolve,
12
+ lazy_resolve: edges_lazy_resolve,
16
13
  )
17
14
  else
18
15
  field
@@ -21,35 +18,22 @@ module GraphQL
21
18
 
22
19
 
23
20
  class EdgesResolve
24
- def initialize(edge_class:, resolve:, lazy_resolve:)
21
+ def initialize(edge_class:, resolve:)
25
22
  @edge_class = edge_class
26
23
  @resolve_proc = resolve
27
- @lazy_resolve_proc = lazy_resolve
28
24
  end
29
25
 
30
26
  # A user's custom Connection may return a lazy object,
31
27
  # if so, handle it later.
32
- def resolve(obj, args, ctx)
28
+ def call(obj, args, ctx)
29
+ parent = ctx.object
33
30
  nodes = @resolve_proc.call(obj, args, ctx)
34
31
  if ctx.schema.lazy?(nodes)
35
- ConnectionResolve::LazyNodesWrapper.new(obj, nodes)
32
+ nodes
36
33
  else
37
- build_edges(nodes, obj)
34
+ nodes.map { |item| @edge_class.new(item, parent) }
38
35
  end
39
36
  end
40
-
41
- # If we get this far, unwrap the wrapper,
42
- # resolve the lazy object and make the edges as usual
43
- def lazy_resolve(obj, args, ctx)
44
- items = @lazy_resolve_proc.call(obj.lazy_object, args, ctx)
45
- build_edges(items, obj.parent)
46
- end
47
-
48
- private
49
-
50
- def build_edges(items, connection)
51
- items.map { |item| @edge_class.new(item, connection) }
52
- end
53
37
  end
54
38
  end
55
39
  end