graphql 1.11.10 → 1.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/install_generator.rb +5 -5
  3. data/lib/generators/graphql/relay_generator.rb +63 -0
  4. data/lib/generators/graphql/templates/base_connection.erb +8 -0
  5. data/lib/generators/graphql/templates/base_edge.erb +8 -0
  6. data/lib/generators/graphql/templates/node_type.erb +9 -0
  7. data/lib/generators/graphql/templates/object.erb +1 -1
  8. data/lib/generators/graphql/templates/query_type.erb +1 -3
  9. data/lib/generators/graphql/templates/schema.erb +8 -35
  10. data/lib/graphql/analysis/analyze_query.rb +7 -0
  11. data/lib/graphql/analysis/ast/visitor.rb +9 -1
  12. data/lib/graphql/analysis/ast.rb +11 -2
  13. data/lib/graphql/backtrace/legacy_tracer.rb +56 -0
  14. data/lib/graphql/backtrace/table.rb +22 -2
  15. data/lib/graphql/backtrace/tracer.rb +40 -9
  16. data/lib/graphql/backtrace.rb +28 -19
  17. data/lib/graphql/backwards_compatibility.rb +1 -0
  18. data/lib/graphql/compatibility/execution_specification.rb +1 -0
  19. data/lib/graphql/compatibility/lazy_execution_specification.rb +2 -0
  20. data/lib/graphql/compatibility/query_parser_specification.rb +2 -0
  21. data/lib/graphql/compatibility/schema_parser_specification.rb +2 -0
  22. data/lib/graphql/dataloader/null_dataloader.rb +21 -0
  23. data/lib/graphql/dataloader/request.rb +24 -0
  24. data/lib/graphql/dataloader/request_all.rb +22 -0
  25. data/lib/graphql/dataloader/source.rb +93 -0
  26. data/lib/graphql/dataloader.rb +197 -0
  27. data/lib/graphql/define/assign_global_id_field.rb +1 -1
  28. data/lib/graphql/define/instance_definable.rb +32 -2
  29. data/lib/graphql/define/type_definer.rb +5 -5
  30. data/lib/graphql/deprecated_dsl.rb +5 -0
  31. data/lib/graphql/enum_type.rb +2 -0
  32. data/lib/graphql/execution/errors.rb +4 -0
  33. data/lib/graphql/execution/execute.rb +7 -0
  34. data/lib/graphql/execution/interpreter/arguments.rb +51 -14
  35. data/lib/graphql/execution/interpreter/handles_raw_value.rb +0 -7
  36. data/lib/graphql/execution/interpreter/runtime.rb +210 -124
  37. data/lib/graphql/execution/interpreter.rb +10 -6
  38. data/lib/graphql/execution/multiplex.rb +20 -6
  39. data/lib/graphql/function.rb +4 -0
  40. data/lib/graphql/input_object_type.rb +2 -0
  41. data/lib/graphql/interface_type.rb +3 -1
  42. data/lib/graphql/language/document_from_schema_definition.rb +50 -23
  43. data/lib/graphql/object_type.rb +2 -0
  44. data/lib/graphql/pagination/connection.rb +5 -1
  45. data/lib/graphql/pagination/connections.rb +6 -16
  46. data/lib/graphql/query/context.rb +4 -0
  47. data/lib/graphql/query/serial_execution.rb +1 -0
  48. data/lib/graphql/query/validation_pipeline.rb +1 -1
  49. data/lib/graphql/query.rb +2 -0
  50. data/lib/graphql/relay/base_connection.rb +7 -0
  51. data/lib/graphql/relay/connection_instrumentation.rb +4 -4
  52. data/lib/graphql/relay/connection_type.rb +1 -1
  53. data/lib/graphql/relay/mutation.rb +1 -0
  54. data/lib/graphql/relay/node.rb +3 -0
  55. data/lib/graphql/relay/type_extensions.rb +2 -0
  56. data/lib/graphql/scalar_type.rb +2 -0
  57. data/lib/graphql/schema/argument.rb +25 -7
  58. data/lib/graphql/schema/build_from_definition.rb +139 -51
  59. data/lib/graphql/schema/directive/flagged.rb +57 -0
  60. data/lib/graphql/schema/directive.rb +76 -0
  61. data/lib/graphql/schema/enum.rb +3 -0
  62. data/lib/graphql/schema/enum_value.rb +12 -6
  63. data/lib/graphql/schema/field/connection_extension.rb +3 -2
  64. data/lib/graphql/schema/field.rb +28 -9
  65. data/lib/graphql/schema/input_object.rb +33 -22
  66. data/lib/graphql/schema/interface.rb +1 -0
  67. data/lib/graphql/schema/member/base_dsl_methods.rb +1 -0
  68. data/lib/graphql/schema/member/build_type.rb +3 -3
  69. data/lib/graphql/schema/member/has_arguments.rb +24 -6
  70. data/lib/graphql/schema/member/has_deprecation_reason.rb +25 -0
  71. data/lib/graphql/schema/member/has_directives.rb +98 -0
  72. data/lib/graphql/schema/member/has_validators.rb +31 -0
  73. data/lib/graphql/schema/member/type_system_helpers.rb +1 -1
  74. data/lib/graphql/schema/member.rb +4 -0
  75. data/lib/graphql/schema/object.rb +11 -0
  76. data/lib/graphql/schema/printer.rb +5 -4
  77. data/lib/graphql/schema/resolver/has_payload_type.rb +2 -0
  78. data/lib/graphql/schema/resolver.rb +7 -0
  79. data/lib/graphql/schema/subscription.rb +19 -1
  80. data/lib/graphql/schema/timeout_middleware.rb +2 -0
  81. data/lib/graphql/schema/validation.rb +2 -0
  82. data/lib/graphql/schema/validator/exclusion_validator.rb +31 -0
  83. data/lib/graphql/schema/validator/format_validator.rb +49 -0
  84. data/lib/graphql/schema/validator/inclusion_validator.rb +33 -0
  85. data/lib/graphql/schema/validator/length_validator.rb +57 -0
  86. data/lib/graphql/schema/validator/numericality_validator.rb +71 -0
  87. data/lib/graphql/schema/validator/required_validator.rb +68 -0
  88. data/lib/graphql/schema/validator.rb +163 -0
  89. data/lib/graphql/schema.rb +72 -49
  90. data/lib/graphql/static_validation/base_visitor.rb +0 -3
  91. data/lib/graphql/static_validation/rules/fields_will_merge.rb +4 -4
  92. data/lib/graphql/static_validation/rules/fragments_are_finite.rb +2 -2
  93. data/lib/graphql/static_validation/validation_context.rb +1 -6
  94. data/lib/graphql/static_validation/validator.rb +12 -14
  95. data/lib/graphql/subscriptions.rb +17 -20
  96. data/lib/graphql/tracing/appoptics_tracing.rb +3 -1
  97. data/lib/graphql/tracing/platform_tracing.rb +3 -1
  98. data/lib/graphql/tracing/skylight_tracing.rb +1 -1
  99. data/lib/graphql/tracing.rb +2 -2
  100. data/lib/graphql/types/relay/base_connection.rb +2 -92
  101. data/lib/graphql/types/relay/base_edge.rb +2 -35
  102. data/lib/graphql/types/relay/connection_behaviors.rb +123 -0
  103. data/lib/graphql/types/relay/default_relay.rb +27 -0
  104. data/lib/graphql/types/relay/edge_behaviors.rb +42 -0
  105. data/lib/graphql/types/relay/has_node_field.rb +41 -0
  106. data/lib/graphql/types/relay/has_nodes_field.rb +41 -0
  107. data/lib/graphql/types/relay/node.rb +2 -4
  108. data/lib/graphql/types/relay/node_behaviors.rb +15 -0
  109. data/lib/graphql/types/relay/node_field.rb +1 -19
  110. data/lib/graphql/types/relay/nodes_field.rb +1 -19
  111. data/lib/graphql/types/relay/page_info.rb +2 -14
  112. data/lib/graphql/types/relay/page_info_behaviors.rb +25 -0
  113. data/lib/graphql/types/relay.rb +11 -3
  114. data/lib/graphql/union_type.rb +2 -0
  115. data/lib/graphql/upgrader/member.rb +1 -0
  116. data/lib/graphql/upgrader/schema.rb +1 -0
  117. data/lib/graphql/version.rb +1 -1
  118. data/lib/graphql.rb +38 -4
  119. metadata +31 -6
  120. data/lib/graphql/types/relay/base_field.rb +0 -22
  121. data/lib/graphql/types/relay/base_interface.rb +0 -29
  122. data/lib/graphql/types/relay/base_object.rb +0 -26
@@ -193,26 +193,26 @@ module GraphQL
193
193
  if node1.name != node2.name
194
194
  errored_nodes = [node1.name, node2.name].sort.join(" or ")
195
195
  msg = "Field '#{response_key}' has a field conflict: #{errored_nodes}?"
196
- add_error(GraphQL::StaticValidation::FieldsWillMergeError.new(
196
+ context.errors << GraphQL::StaticValidation::FieldsWillMergeError.new(
197
197
  msg,
198
198
  nodes: [node1, node2],
199
199
  path: [],
200
200
  field_name: response_key,
201
201
  conflicts: errored_nodes
202
- ))
202
+ )
203
203
  end
204
204
 
205
205
  if !same_arguments?(node1, node2)
206
206
  args = [serialize_field_args(node1), serialize_field_args(node2)]
207
207
  conflicts = args.map { |arg| GraphQL::Language.serialize(arg) }.join(" or ")
208
208
  msg = "Field '#{response_key}' has an argument conflict: #{conflicts}?"
209
- add_error(GraphQL::StaticValidation::FieldsWillMergeError.new(
209
+ context.errors << GraphQL::StaticValidation::FieldsWillMergeError.new(
210
210
  msg,
211
211
  nodes: [node1, node2],
212
212
  path: [],
213
213
  field_name: response_key,
214
214
  conflicts: conflicts
215
- ))
215
+ )
216
216
  end
217
217
  end
218
218
 
@@ -7,12 +7,12 @@ module GraphQL
7
7
  dependency_map = context.dependencies
8
8
  dependency_map.cyclical_definitions.each do |defn|
9
9
  if defn.node.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
10
- add_error(GraphQL::StaticValidation::FragmentsAreFiniteError.new(
10
+ context.errors << GraphQL::StaticValidation::FragmentsAreFiniteError.new(
11
11
  "Fragment #{defn.name} contains an infinite loop",
12
12
  nodes: defn.node,
13
13
  path: defn.path,
14
14
  name: defn.name
15
- ))
15
+ )
16
16
  end
17
17
  end
18
18
  end
@@ -19,11 +19,10 @@ module GraphQL
19
19
 
20
20
  def_delegators :@query, :schema, :document, :fragments, :operations, :warden
21
21
 
22
- def initialize(query, visitor_class, max_errors)
22
+ def initialize(query, visitor_class)
23
23
  @query = query
24
24
  @literal_validator = LiteralValidator.new(context: query.context)
25
25
  @errors = []
26
- @max_errors = max_errors || Float::INFINITY
27
26
  @on_dependency_resolve_handlers = []
28
27
  @visitor = visitor_class.new(document, self)
29
28
  end
@@ -39,10 +38,6 @@ module GraphQL
39
38
  def validate_literal(ast_value, type)
40
39
  @literal_validator.validate(ast_value, type)
41
40
  end
42
-
43
- def too_many_errors?
44
- @errors.length >= @max_errors
45
- end
46
41
  end
47
42
  end
48
43
  end
@@ -22,9 +22,8 @@ module GraphQL
22
22
  # @param query [GraphQL::Query]
23
23
  # @param validate [Boolean]
24
24
  # @param timeout [Float] Number of seconds to wait before aborting validation. Any positive number may be used, including Floats to specify fractional seconds.
25
- # @param max_errors [Integer] Maximum number of errors before aborting validation. Any positive number will limit the number of errors. Defaults to nil for no limit.
26
25
  # @return [Array<Hash>]
27
- def validate(query, validate: true, timeout: nil, max_errors: nil)
26
+ def validate(query, validate: true, timeout: nil)
28
27
  query.trace("validate", { validate: validate, query: query }) do
29
28
  can_skip_rewrite = query.context.interpreter? && query.schema.using_ast_analysis? && query.schema.is_a?(Class)
30
29
  errors = if validate == false && can_skip_rewrite
@@ -33,26 +32,25 @@ module GraphQL
33
32
  rules_to_use = validate ? @rules : []
34
33
  visitor_class = BaseVisitor.including_rules(rules_to_use, rewrite: !can_skip_rewrite)
35
34
 
36
- context = GraphQL::StaticValidation::ValidationContext.new(query, visitor_class, max_errors)
35
+ context = GraphQL::StaticValidation::ValidationContext.new(query, visitor_class)
37
36
 
38
37
  begin
39
38
  # CAUTION: Usage of the timeout module makes the assumption that validation rules are stateless Ruby code that requires no cleanup if process was interrupted. This means no blocking IO calls, native gems, locks, or `rescue` clauses that must be reached.
40
39
  # A timeout value of 0 or nil will execute the block without any timeout.
41
40
  Timeout::timeout(timeout) do
42
-
43
- catch(:too_many_validation_errors) do
44
- # Attach legacy-style rules.
45
- # Only loop through rules if it has legacy-style rules
46
- unless (legacy_rules = rules_to_use - GraphQL::StaticValidation::ALL_RULES).empty?
47
- legacy_rules.each do |rule_class_or_module|
48
- if rule_class_or_module.method_defined?(:validate)
49
- rule_class_or_module.new.validate(context)
50
- end
41
+ # Attach legacy-style rules.
42
+ # Only loop through rules if it has legacy-style rules
43
+ unless (legacy_rules = rules_to_use - GraphQL::StaticValidation::ALL_RULES).empty?
44
+ legacy_rules.each do |rule_class_or_module|
45
+ if rule_class_or_module.method_defined?(:validate)
46
+ warn "Legacy validator rules will be removed from GraphQL-Ruby 2.0, use a module instead (see the built-in rules: https://github.com/rmosolgo/graphql-ruby/tree/master/lib/graphql/static_validation/rules)"
47
+ warn " -> Legacy validator: #{rule_class_or_module}"
48
+ rule_class_or_module.new.validate(context)
51
49
  end
52
50
  end
53
-
54
- context.visitor.visit
55
51
  end
52
+
53
+ context.visitor.visit
56
54
  end
57
55
  rescue Timeout::Error
58
56
  handle_timeout(query, context)
@@ -26,7 +26,9 @@ module GraphQL
26
26
 
27
27
  instrumentation = Subscriptions::Instrumentation.new(schema: schema)
28
28
  defn.instrument(:query, instrumentation)
29
- defn.instrument(:field, instrumentation)
29
+ if !schema.is_a?(Class)
30
+ defn.instrument(:field, instrumentation)
31
+ end
30
32
  options[:schema] = schema
31
33
  schema.subscriptions = self.new(**options)
32
34
  schema.add_subscription_extension_if_necessary
@@ -107,31 +109,26 @@ module GraphQL
107
109
  variables = query_data.fetch(:variables)
108
110
  context = query_data.fetch(:context)
109
111
  operation_name = query_data.fetch(:operation_name)
110
- result = nil
111
- # this will be set to `false` unless `.execute` is terminated
112
- # with a `throw :graphql_subscription_unsubscribed`
113
- unsubscribed = true
114
- catch(:graphql_subscription_unsubscribed) do
115
- catch(:graphql_no_subscription_update) do
116
- # Re-evaluate the saved query,
117
- # but if it terminates early with a `throw`,
118
- # it will stay `nil`
119
- result = @schema.execute(
120
- query: query_string,
121
- context: context,
122
- subscription_topic: event.topic,
123
- operation_name: operation_name,
124
- variables: variables,
125
- root_value: object,
126
- )
127
- end
128
- unsubscribed = false
112
+ result = @schema.execute(
113
+ query: query_string,
114
+ context: context,
115
+ subscription_topic: event.topic,
116
+ operation_name: operation_name,
117
+ variables: variables,
118
+ root_value: object,
119
+ )
120
+ subscriptions_context = result.context.namespace(:subscriptions)
121
+ if subscriptions_context[:no_update]
122
+ result = nil
129
123
  end
130
124
 
125
+ unsubscribed = subscriptions_context[:unsubscribed]
126
+
131
127
  if unsubscribed
132
128
  # `unsubscribe` was called, clean up on our side
133
129
  # TODO also send `{more: false}` to client?
134
130
  delete_subscription(subscription_id)
131
+ result = nil
135
132
  end
136
133
 
137
134
  result
@@ -57,7 +57,7 @@ module GraphQL
57
57
  def platform_field_key(type, field)
58
58
  "graphql.#{type.graphql_name}.#{field.graphql_name}"
59
59
  end
60
-
60
+
61
61
  def platform_authorized_key(type)
62
62
  "graphql.authorized.#{type.graphql_name}"
63
63
  end
@@ -112,6 +112,8 @@ module GraphQL
112
112
  graphql_query_string(data[key])
113
113
  when :multiplex
114
114
  graphql_multiplex(data[key])
115
+ when :path
116
+ [key, data[key].join(".")]
115
117
  else
116
118
  [key, data[key]]
117
119
  end
@@ -96,7 +96,9 @@ module GraphQL
96
96
 
97
97
  def self.use(schema_defn, options = {})
98
98
  tracer = self.new(**options)
99
- schema_defn.instrument(:field, tracer)
99
+ if !schema_defn.is_a?(Class)
100
+ schema_defn.instrument(:field, tracer)
101
+ end
100
102
  schema_defn.tracer(tracer)
101
103
  end
102
104
 
@@ -18,7 +18,7 @@ module GraphQL
18
18
  # This is not advised if you run more than one query per HTTP request, for example, with `graphql-client` or multiplexing.
19
19
  # It can also be specified per-query with `context[:set_skylight_endpoint_name]`.
20
20
  def initialize(options = {})
21
- warn("GraphQL::Tracing::SkylightTracing is deprecated, please enable Skylight's GraphQL probe instead: https://www.skylight.io/support/getting-more-from-skylight#graphql.")
21
+ warn("GraphQL::Tracing::SkylightTracing is deprecated and will be removed in GraphQL-Ruby 2.0, please enable Skylight's GraphQL probe instead: https://www.skylight.io/support/getting-more-from-skylight#graphql.")
22
22
  @set_endpoint_name = options.fetch(:set_endpoint_name, false)
23
23
  super
24
24
  end
@@ -42,8 +42,8 @@ module GraphQL
42
42
  # execute_multiplex | `{ multiplex: GraphQL::Execution::Multiplex }`
43
43
  # execute_query | `{ query: GraphQL::Query }`
44
44
  # execute_query_lazy | `{ query: GraphQL::Query?, multiplex: GraphQL::Execution::Multiplex? }`
45
- # execute_field | `{ context: GraphQL::Query::Context::FieldResolutionContext?, owner: Class?, field: GraphQL::Schema::Field?, query: GraphQL::Query?, path: Array<String, Integer>?}`
46
- # execute_field_lazy | `{ context: GraphQL::Query::Context::FieldResolutionContext?, owner: Class?, field: GraphQL::Schema::Field?, query: GraphQL::Query?, path: Array<String, Integer>?}`
45
+ # execute_field | `{ owner: Class, field: GraphQL::Schema::Field, query: GraphQL::Query, path: Array<String, Integer>, ast_node: GraphQL::Language::Nodes::Field}`
46
+ # execute_field_lazy | `{ owner: Class, field: GraphQL::Schema::Field, query: GraphQL::Query, path: Array<String, Integer>, ast_node: GraphQL::Language::Nodes::Field}`
47
47
  # authorized | `{ context: GraphQL::Query::Context, type: Class, object: Object, path: Array<String, Integer> }`
48
48
  # authorized_lazy | `{ context: GraphQL::Query::Context, type: Class, object: Object, path: Array<String, Integer> }`
49
49
  # resolve_type | `{ context: GraphQL::Query::Context, type: Class, object: Object, path: Array<String, Integer> }`
@@ -27,98 +27,8 @@ module GraphQL
27
27
  # end
28
28
  #
29
29
  # @see Relay::BaseEdge for edge types
30
- class BaseConnection < Types::Relay::BaseObject
31
- extend Forwardable
32
- def_delegators :@object, :cursor_from_node, :parent
33
-
34
- class << self
35
- # @return [Class]
36
- attr_reader :node_type
37
-
38
- # @return [Class]
39
- attr_reader :edge_class
40
-
41
- # Configure this connection to return `edges` and `nodes` based on `edge_type_class`.
42
- #
43
- # This method will use the inputs to create:
44
- # - `edges` field
45
- # - `nodes` field
46
- # - description
47
- #
48
- # It's called when you subclass this base connection, trying to use the
49
- # class name to set defaults. You can call it again in the class definition
50
- # to override the default (or provide a value, if the default lookup failed).
51
- def edge_type(edge_type_class, edge_class: GraphQL::Relay::Edge, node_type: edge_type_class.node_type, nodes_field: true, node_nullable: true)
52
- # Set this connection's graphql name
53
- node_type_name = node_type.graphql_name
54
-
55
- @node_type = node_type
56
- @edge_type = edge_type_class
57
- @edge_class = edge_class
58
-
59
- field :edges, [edge_type_class, null: true],
60
- null: true,
61
- description: "A list of edges.",
62
- edge_class: edge_class
63
-
64
- define_nodes_field(node_nullable) if nodes_field
65
-
66
- description("The connection type for #{node_type_name}.")
67
- end
68
-
69
- # Filter this list according to the way its node type would scope them
70
- def scope_items(items, context)
71
- node_type.scope_items(items, context)
72
- end
73
-
74
- # Add the shortcut `nodes` field to this connection and its subclasses
75
- def nodes_field
76
- define_nodes_field
77
- end
78
-
79
- def authorized?(obj, ctx)
80
- true # Let nodes be filtered out
81
- end
82
-
83
- def accessible?(ctx)
84
- node_type.accessible?(ctx)
85
- end
86
-
87
- def visible?(ctx)
88
- node_type.visible?(ctx)
89
- end
90
-
91
- private
92
-
93
- def define_nodes_field(nullable = true)
94
- type = nullable ? [@node_type, null: true] : [@node_type]
95
- field :nodes, type,
96
- null: nullable,
97
- description: "A list of nodes.",
98
- connection: false
99
- end
100
- end
101
-
102
- field :page_info, GraphQL::Types::Relay::PageInfo, null: false, description: "Information to aid in pagination."
103
-
104
- # By default this calls through to the ConnectionWrapper's edge nodes method,
105
- # but sometimes you need to override it to support the `nodes` field
106
- def nodes
107
- @object.edge_nodes
108
- end
109
-
110
- def edges
111
- if @object.is_a?(GraphQL::Pagination::Connection)
112
- @object.edges
113
- elsif context.interpreter?
114
- context.schema.after_lazy(object.edge_nodes) do |nodes|
115
- nodes.map { |n| self.class.edge_class.new(n, object) }
116
- end
117
- else
118
- # This is done by edges_instrumentation
119
- @object.edge_nodes
120
- end
121
- end
30
+ class BaseConnection < Schema::Object
31
+ include ConnectionBehaviors
122
32
  end
123
33
  end
124
34
  end
@@ -21,41 +21,8 @@ module GraphQL
21
21
  # end
22
22
  #
23
23
  # @see {Relay::BaseConnection} for connection types
24
- class BaseEdge < Types::Relay::BaseObject
25
- description "An edge in a connection."
26
-
27
- class << self
28
- # Get or set the Object type that this edge wraps.
29
- #
30
- # @param node_type [Class] A `Schema::Object` subclass
31
- # @param null [Boolean]
32
- def node_type(node_type = nil, null: true)
33
- if node_type
34
- @node_type = node_type
35
- # Add a default `node` field
36
- field :node, node_type, null: null, description: "The item at the end of the edge.",
37
- connection: false
38
- end
39
- @node_type
40
- end
41
-
42
- def authorized?(obj, ctx)
43
- true
44
- end
45
-
46
- def accessible?(ctx)
47
- node_type.accessible?(ctx)
48
- end
49
-
50
- def visible?(ctx)
51
- node_type.visible?(ctx)
52
- end
53
- end
54
-
55
-
56
- field :cursor, String,
57
- null: false,
58
- description: "A cursor for use in pagination."
24
+ class BaseEdge < GraphQL::Schema::Object
25
+ include Types::Relay::EdgeBehaviors
59
26
  end
60
27
  end
61
28
  end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Types
5
+ module Relay
6
+ module ConnectionBehaviors
7
+ extend Forwardable
8
+ def_delegators :@object, :cursor_from_node, :parent
9
+
10
+ def self.included(child_class)
11
+ child_class.extend(ClassMethods)
12
+ child_class.extend(Relay::DefaultRelay)
13
+ child_class.default_relay(true)
14
+ child_class.node_nullable(true)
15
+ add_page_info_field(child_class)
16
+ end
17
+
18
+ module ClassMethods
19
+ # @return [Class]
20
+ attr_reader :node_type
21
+
22
+ # @return [Class]
23
+ attr_reader :edge_class
24
+
25
+ # Configure this connection to return `edges` and `nodes` based on `edge_type_class`.
26
+ #
27
+ # This method will use the inputs to create:
28
+ # - `edges` field
29
+ # - `nodes` field
30
+ # - description
31
+ #
32
+ # It's called when you subclass this base connection, trying to use the
33
+ # class name to set defaults. You can call it again in the class definition
34
+ # to override the default (or provide a value, if the default lookup failed).
35
+ def edge_type(edge_type_class, edge_class: GraphQL::Relay::Edge, node_type: edge_type_class.node_type, nodes_field: true, node_nullable: self.node_nullable)
36
+ # Set this connection's graphql name
37
+ node_type_name = node_type.graphql_name
38
+
39
+ @node_type = node_type
40
+ @edge_type = edge_type_class
41
+ @edge_class = edge_class
42
+
43
+ field :edges, [edge_type_class, null: true],
44
+ null: true,
45
+ description: "A list of edges.",
46
+ legacy_edge_class: edge_class, # This is used by the old runtime only, for EdgesInstrumentation
47
+ connection: false
48
+
49
+ define_nodes_field(node_nullable) if nodes_field
50
+
51
+ description("The connection type for #{node_type_name}.")
52
+ end
53
+
54
+ # Filter this list according to the way its node type would scope them
55
+ def scope_items(items, context)
56
+ node_type.scope_items(items, context)
57
+ end
58
+
59
+ # Add the shortcut `nodes` field to this connection and its subclasses
60
+ def nodes_field(node_nullable: self.node_nullable)
61
+ define_nodes_field(node_nullable)
62
+ end
63
+
64
+ def authorized?(obj, ctx)
65
+ true # Let nodes be filtered out
66
+ end
67
+
68
+ def accessible?(ctx)
69
+ node_type.accessible?(ctx)
70
+ end
71
+
72
+ def visible?(ctx)
73
+ node_type.visible?(ctx)
74
+ end
75
+
76
+ # Set the default `node_nullable` for this class and its child classes. (Defaults to `true`.)
77
+ # Use `node_nullable(false)` in your base class to make non-null `node` and `nodes` fields.
78
+ def node_nullable(new_value = nil)
79
+ if new_value.nil?
80
+ @node_nullable || superclass.node_nullable
81
+ else
82
+ @node_nullable ||= new_value
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def define_nodes_field(nullable)
89
+ field :nodes, [@node_type, null: nullable],
90
+ null: nullable,
91
+ description: "A list of nodes.",
92
+ connection: false
93
+ end
94
+ end
95
+
96
+ class << self
97
+ def add_page_info_field(obj_type)
98
+ obj_type.field :page_info, GraphQL::Types::Relay::PageInfo, null: false, description: "Information to aid in pagination."
99
+ end
100
+ end
101
+
102
+ # By default this calls through to the ConnectionWrapper's edge nodes method,
103
+ # but sometimes you need to override it to support the `nodes` field
104
+ def nodes
105
+ @object.edge_nodes
106
+ end
107
+
108
+ def edges
109
+ if @object.is_a?(GraphQL::Pagination::Connection)
110
+ @object.edges
111
+ elsif context.interpreter?
112
+ context.schema.after_lazy(object.edge_nodes) do |nodes|
113
+ nodes.map { |n| self.class.edge_class.new(n, object) }
114
+ end
115
+ else
116
+ # This is done by edges_instrumentation
117
+ @object.edge_nodes
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Types
5
+ module Relay
6
+ module DefaultRelay
7
+ def self.extended(child_class)
8
+ child_class.default_relay(true)
9
+ end
10
+
11
+ def default_relay(new_value)
12
+ @default_relay = new_value
13
+ end
14
+
15
+ def default_relay?
16
+ !!@default_relay
17
+ end
18
+
19
+ def to_graphql
20
+ type_defn = super
21
+ type_defn.default_relay = default_relay?
22
+ type_defn
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Types
5
+ module Relay
6
+ module EdgeBehaviors
7
+ def self.included(child_class)
8
+ child_class.description("An edge in a connection.")
9
+ child_class.field(:cursor, String, null: false, description: "A cursor for use in pagination.")
10
+ child_class.extend(ClassMethods)
11
+ end
12
+
13
+ module ClassMethods
14
+ # Get or set the Object type that this edge wraps.
15
+ #
16
+ # @param node_type [Class] A `Schema::Object` subclass
17
+ # @param null [Boolean]
18
+ def node_type(node_type = nil, null: true)
19
+ if node_type
20
+ @node_type = node_type
21
+ # Add a default `node` field
22
+ field :node, node_type, null: null, description: "The item at the end of the edge.", connection: false
23
+ end
24
+ @node_type
25
+ end
26
+
27
+ def authorized?(obj, ctx)
28
+ true
29
+ end
30
+
31
+ def accessible?(ctx)
32
+ node_type.accessible?(ctx)
33
+ end
34
+
35
+ def visible?(ctx)
36
+ node_type.visible?(ctx)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Types
5
+ module Relay
6
+ module HasNodeField
7
+ def self.included(child_class)
8
+ child_class.field(**field_options, &field_block)
9
+ end
10
+
11
+ class << self
12
+ def field_options
13
+ {
14
+ name: "node",
15
+ owner: nil,
16
+ type: GraphQL::Types::Relay::Node,
17
+ null: true,
18
+ description: "Fetches an object given its ID.",
19
+ relay_node_field: true,
20
+ }
21
+ end
22
+
23
+ def field_block
24
+ Proc.new {
25
+ argument :id, "ID!", required: true,
26
+ description: "ID of the object."
27
+
28
+ def resolve(obj, args, ctx)
29
+ ctx.schema.object_from_id(args[:id], ctx)
30
+ end
31
+
32
+ def resolve_field(obj, args, ctx)
33
+ resolve(obj, args, ctx)
34
+ end
35
+ }
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Types
5
+ module Relay
6
+ module HasNodesField
7
+ def self.included(child_class)
8
+ child_class.field(**field_options, &field_block)
9
+ end
10
+
11
+ class << self
12
+ def field_options
13
+ {
14
+ name: "nodes",
15
+ owner: nil,
16
+ type: [GraphQL::Types::Relay::Node, null: true],
17
+ null: false,
18
+ description: "Fetches a list of objects given a list of IDs.",
19
+ relay_nodes_field: true,
20
+ }
21
+ end
22
+
23
+ def field_block
24
+ Proc.new {
25
+ argument :ids, "[ID!]!", required: true,
26
+ description: "IDs of the objects."
27
+
28
+ def resolve(obj, args, ctx)
29
+ args[:ids].map { |id| ctx.schema.object_from_id(id, ctx) }
30
+ end
31
+
32
+ def resolve_field(obj, args, ctx)
33
+ resolve(obj, args, ctx)
34
+ end
35
+ }
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -7,10 +7,8 @@ module GraphQL
7
7
  # or you can take it as inspiration for your own implementation
8
8
  # of the `Node` interface.
9
9
  module Node
10
- include Types::Relay::BaseInterface
11
- default_relay(true)
12
- description "An object with an ID."
13
- field(:id, ID, null: false, description: "ID of the object.")
10
+ include GraphQL::Schema::Interface
11
+ include Types::Relay::NodeBehaviors
14
12
  end
15
13
  end
16
14
  end