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
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