graphql 1.5.15 → 1.6.0

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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql.rb +4 -19
  3. data/lib/graphql/analysis/analyze_query.rb +27 -2
  4. data/lib/graphql/analysis/query_complexity.rb +10 -11
  5. data/lib/graphql/argument.rb +7 -6
  6. data/lib/graphql/backwards_compatibility.rb +47 -0
  7. data/lib/graphql/compatibility/execution_specification.rb +14 -0
  8. data/lib/graphql/compatibility/execution_specification/specification_schema.rb +6 -1
  9. data/lib/graphql/compatibility/lazy_execution_specification.rb +19 -0
  10. data/lib/graphql/compatibility/lazy_execution_specification/lazy_schema.rb +15 -6
  11. data/lib/graphql/directive.rb +1 -6
  12. data/lib/graphql/execution.rb +1 -0
  13. data/lib/graphql/execution/execute.rb +174 -160
  14. data/lib/graphql/execution/field_result.rb +5 -1
  15. data/lib/graphql/execution/lazy.rb +2 -2
  16. data/lib/graphql/execution/lazy/resolve.rb +8 -11
  17. data/lib/graphql/execution/multiplex.rb +134 -0
  18. data/lib/graphql/execution/selection_result.rb +5 -0
  19. data/lib/graphql/field.rb +1 -8
  20. data/lib/graphql/filter.rb +53 -0
  21. data/lib/graphql/internal_representation/node.rb +11 -6
  22. data/lib/graphql/internal_representation/rewrite.rb +3 -3
  23. data/lib/graphql/query.rb +160 -78
  24. data/lib/graphql/query/arguments.rb +14 -25
  25. data/lib/graphql/query/arguments_cache.rb +6 -13
  26. data/lib/graphql/query/context.rb +28 -10
  27. data/lib/graphql/query/executor.rb +1 -0
  28. data/lib/graphql/query/literal_input.rb +10 -4
  29. data/lib/graphql/query/null_context.rb +1 -1
  30. data/lib/graphql/query/serial_execution/field_resolution.rb +5 -1
  31. data/lib/graphql/query/validation_pipeline.rb +12 -7
  32. data/lib/graphql/query/variables.rb +1 -1
  33. data/lib/graphql/rake_task.rb +140 -0
  34. data/lib/graphql/relay/array_connection.rb +29 -48
  35. data/lib/graphql/relay/base_connection.rb +9 -7
  36. data/lib/graphql/relay/mutation.rb +0 -11
  37. data/lib/graphql/relay/mutation/instrumentation.rb +2 -2
  38. data/lib/graphql/relay/mutation/resolve.rb +7 -10
  39. data/lib/graphql/relay/relation_connection.rb +98 -61
  40. data/lib/graphql/scalar_type.rb +1 -15
  41. data/lib/graphql/schema.rb +90 -25
  42. data/lib/graphql/schema/build_from_definition.rb +22 -23
  43. data/lib/graphql/schema/build_from_definition/resolve_map.rb +70 -0
  44. data/lib/graphql/schema/build_from_definition/resolve_map/default_resolve.rb +47 -0
  45. data/lib/graphql/schema/middleware_chain.rb +1 -1
  46. data/lib/graphql/schema/printer.rb +2 -1
  47. data/lib/graphql/schema/timeout_middleware.rb +6 -6
  48. data/lib/graphql/schema/type_map.rb +1 -1
  49. data/lib/graphql/schema/warden.rb +5 -9
  50. data/lib/graphql/static_validation/definition_dependencies.rb +1 -1
  51. data/lib/graphql/version.rb +1 -1
  52. data/spec/graphql/analysis/analyze_query_spec.rb +2 -2
  53. data/spec/graphql/analysis/max_query_complexity_spec.rb +28 -0
  54. data/spec/graphql/argument_spec.rb +3 -3
  55. data/spec/graphql/execution/lazy_spec.rb +8 -114
  56. data/spec/graphql/execution/multiplex_spec.rb +131 -0
  57. data/spec/graphql/internal_representation/rewrite_spec.rb +10 -0
  58. data/spec/graphql/query/arguments_spec.rb +14 -16
  59. data/spec/graphql/query/context_spec.rb +14 -1
  60. data/spec/graphql/query/literal_input_spec.rb +19 -13
  61. data/spec/graphql/query/variables_spec.rb +1 -1
  62. data/spec/graphql/query_spec.rb +12 -1
  63. data/spec/graphql/rake_task_spec.rb +57 -0
  64. data/spec/graphql/relay/array_connection_spec.rb +24 -3
  65. data/spec/graphql/relay/connection_instrumentation_spec.rb +23 -0
  66. data/spec/graphql/relay/mutation_spec.rb +2 -10
  67. data/spec/graphql/relay/page_info_spec.rb +2 -2
  68. data/spec/graphql/relay/relation_connection_spec.rb +167 -3
  69. data/spec/graphql/schema/build_from_definition_spec.rb +93 -19
  70. data/spec/graphql/schema/warden_spec.rb +80 -0
  71. data/spec/graphql/schema_spec.rb +26 -2
  72. data/spec/spec_helper.rb +4 -2
  73. data/spec/support/lazy_helpers.rb +152 -0
  74. data/spec/support/star_wars/schema.rb +23 -0
  75. metadata +28 -3
  76. data/lib/graphql/schema/mask.rb +0 -55
@@ -5,10 +5,9 @@ module GraphQL
5
5
  #
6
6
  # {Arguments} recursively wraps the input in {Arguments} instances.
7
7
  class Arguments
8
- extend GraphQL::Delegate
8
+ extend Forwardable
9
9
 
10
10
  def initialize(values, argument_definitions:)
11
- @original_values = values
12
11
  @argument_values = values.inject({}) do |memo, (inner_key, inner_value)|
13
12
  arg_defn = argument_definitions[inner_key.to_s]
14
13
 
@@ -22,24 +21,29 @@ module GraphQL
22
21
  # @param key [String, Symbol] name or index of value to access
23
22
  # @return [Object] the argument at that key
24
23
  def [](key)
25
- key_s = key.is_a?(String) ? key : key.to_s
26
- @argument_values.fetch(key_s, NULL_ARGUMENT_VALUE).value
24
+ @argument_values.fetch(key.to_s, NULL_ARGUMENT_VALUE).value
27
25
  end
28
26
 
29
27
  # @param key [String, Symbol] name of value to access
30
28
  # @return [Boolean] true if the argument was present in this field
31
29
  def key?(key)
32
- key_s = key.is_a?(String) ? key : key.to_s
33
- @argument_values.key?(key_s)
30
+ @argument_values.key?(key.to_s)
34
31
  end
35
32
 
36
- # Get the original Ruby hash
37
- # @return [Hash] the original values hash
33
+ # Get the hash of all values, with stringified keys
34
+ # @return [Hash] the stringified hash
38
35
  def to_h
39
- @unwrapped_values ||= unwrap_value(@original_values)
36
+ @to_h ||= begin
37
+ h = {}
38
+ each_value do |arg_value|
39
+ arg_key = arg_value.definition.expose_as
40
+ h[arg_key] = unwrap_value(arg_value.value)
41
+ end
42
+ h
43
+ end
40
44
  end
41
45
 
42
- def_delegators :string_key_values, :keys, :values, :each
46
+ def_delegators :to_h, :keys, :values, :each
43
47
 
44
48
  # Access each key, value and type for the arguments in this set.
45
49
  # @yield [argument_value] The {ArgumentValue} for each argument
@@ -101,21 +105,6 @@ module GraphQL
101
105
  value
102
106
  end
103
107
  end
104
-
105
- def string_key_values
106
- @string_key_values ||= stringify_keys(to_h)
107
- end
108
-
109
- def stringify_keys(value)
110
- case value
111
- when Hash
112
- value.inject({}) { |memo, (k, v)| memo[k.to_s] = stringify_keys(v); memo }
113
- when Array
114
- value.map { |v| stringify_keys(v) }
115
- else
116
- value
117
- end
118
- end
119
108
  end
120
109
  end
121
110
  end
@@ -5,21 +5,14 @@ module GraphQL
5
5
  # @return [Hash<InternalRepresentation::Node, GraphQL::Language::NodesDirectiveNode => Hash<GraphQL::Field, GraphQL::Directive => GraphQL::Query::Arguments>>]
6
6
  def self.build(query)
7
7
  Hash.new do |h1, irep_or_ast_node|
8
- h1[irep_or_ast_node] = Hash.new do |h2, definition|
8
+ Hash.new do |h2, definition|
9
9
  ast_node = irep_or_ast_node.is_a?(GraphQL::InternalRepresentation::Node) ? irep_or_ast_node.ast_node : irep_or_ast_node
10
10
  ast_arguments = ast_node.arguments
11
-
12
- h2[definition] = if ast_arguments.none?
13
- definition.default_arguments
14
- elsif definition.arguments.none?
15
- GraphQL::Query::Arguments::NO_ARGS
16
- else
17
- GraphQL::Query::LiteralInput.from_arguments(
18
- ast_arguments,
19
- definition.arguments,
20
- query.variables,
21
- )
22
- end
11
+ GraphQL::Query::LiteralInput.from_arguments(
12
+ ast_arguments,
13
+ definition.arguments,
14
+ query.variables,
15
+ )
23
16
  end
24
17
  end
25
18
  end
@@ -4,6 +4,7 @@ module GraphQL
4
4
  # Expose some query-specific info to field resolve functions.
5
5
  # It delegates `[]` to the hash that's passed to `GraphQL::Query#initialize`.
6
6
  class Context
7
+ extend Forwardable
7
8
  attr_reader :execution_strategy
8
9
  # `strategy` is required by GraphQL::Batch
9
10
  alias_method :strategy, :execution_strategy
@@ -41,24 +42,32 @@ module GraphQL
41
42
  def initialize(query:, values:)
42
43
  @query = query
43
44
  @schema = query.schema
44
- @values = values || {}
45
+ @provided_values = values || {}
46
+ # Namespaced storage, where user-provided values are in `nil` namespace:
47
+ @storage = Hash.new { |h, k| h[k] = {} }
48
+ @storage[nil] = @provided_values
45
49
  @errors = []
46
50
  @path = []
47
51
  end
48
52
 
49
- # Lookup `key` from the hash passed to {Schema#execute} as `context:`
50
- def [](key)
51
- @values[key]
52
- end
53
+ def_delegators :@provided_values, :[], :[]=, :to_h, :key?, :fetch
54
+
55
+ # @!method [](key)
56
+ # Lookup `key` from the hash passed to {Schema#execute} as `context:`
57
+
58
+ # @!method []=(key, value)
59
+ # Reassign `key` to the hash passed to {Schema#execute} as `context:`
53
60
 
54
61
  # @return [GraphQL::Schema::Warden]
55
62
  def warden
56
63
  @warden ||= @query.warden
57
64
  end
58
65
 
59
- # Reassign `key` to the hash passed to {Schema#execute} as `context:`
60
- def []=(key, value)
61
- @values[key] = value
66
+ # Get an isolated hash for `ns`. Doesn't affect user-provided storage.
67
+ # @param ns [Object] a usage-specific namespace identifier
68
+ # @return [Hash] namespaced storage
69
+ def namespace(ns)
70
+ @storage[ns]
62
71
  end
63
72
 
64
73
  def spawn(key:, selection:, parent_type:, field:)
@@ -71,8 +80,14 @@ module GraphQL
71
80
  )
72
81
  end
73
82
 
83
+ # Return this value to tell the runtime
84
+ # to exclude this field from the response altogether
85
+ def skip
86
+ GraphQL::Execution::Execute::SKIP
87
+ end
88
+
74
89
  class FieldResolutionContext
75
- extend GraphQL::Delegate
90
+ extend Forwardable
76
91
 
77
92
  attr_reader :path, :selection, :field, :parent_type
78
93
 
@@ -84,7 +99,10 @@ module GraphQL
84
99
  @parent_type = parent_type
85
100
  end
86
101
 
87
- def_delegators :@context, :[], :[]=, :spawn, :query, :schema, :warden, :errors, :execution_strategy, :strategy
102
+ def_delegators :@context,
103
+ :[], :[]=, :key?, :fetch, :to_h, :namespace,
104
+ :spawn, :query, :schema, :warden, :errors,
105
+ :execution_strategy, :strategy, :skip
88
106
 
89
107
  # @return [GraphQL::Language::Nodes::Field] The AST node for the currently-executing field
90
108
  def ast_node
@@ -8,6 +8,7 @@ module GraphQL
8
8
  attr_reader :query
9
9
 
10
10
  def initialize(query)
11
+ warn("Executor is deprecated; use Schema#execute")
11
12
  @query = query
12
13
  end
13
14
 
@@ -32,7 +32,7 @@ module GraphQL
32
32
  end
33
33
 
34
34
  def self.defaults_for(argument_defns)
35
- if argument_defns.none? { |name, arg| arg.default_value? }
35
+ if argument_defns.values.none?(&:default_value?)
36
36
  GraphQL::Query::Arguments::NO_ARGS
37
37
  else
38
38
  from_arguments([], argument_defns, nil)
@@ -40,7 +40,8 @@ module GraphQL
40
40
  end
41
41
 
42
42
  def self.from_arguments(ast_arguments, argument_defns, variables)
43
-
43
+ # Variables is nil when making .defaults_for
44
+ context = variables ? variables.context : nil
44
45
  values_hash = {}
45
46
  indexed_arguments = ast_arguments.each_with_object({}) { |a, memo| memo[a.name] = a }
46
47
 
@@ -56,7 +57,7 @@ module GraphQL
56
57
  if (!value_is_a_variable || (value_is_a_variable && variables.key?(ast_arg.value.name)))
57
58
 
58
59
  value = coerce(arg_defn.type, ast_arg.value, variables)
59
- value = arg_defn.prepare(value)
60
+ value = arg_defn.prepare(value, context)
60
61
 
61
62
  if value.is_a?(GraphQL::ExecutionError)
62
63
  value.ast_node = ast_arg
@@ -72,7 +73,12 @@ module GraphQL
72
73
  # a value wasn't provided from the AST,
73
74
  # then add the default value.
74
75
  if arg_defn.default_value? && !values_hash.key?(arg_name)
75
- values_hash[arg_name] = arg_defn.default_value
76
+ value = arg_defn.default_value
77
+ # `context` isn't present when pre-calculating defaults
78
+ if context
79
+ value = arg_defn.prepare(value, context)
80
+ end
81
+ values_hash[arg_name] = value
76
82
  end
77
83
  end
78
84
 
@@ -9,7 +9,7 @@ module GraphQL
9
9
  @query = nil
10
10
  @schema = GraphQL::Schema.new
11
11
  @warden = GraphQL::Schema::Warden.new(
12
- GraphQL::Schema::NullMask,
12
+ GraphQL::Filter.new,
13
13
  context: self,
14
14
  schema: @schema,
15
15
  )
@@ -24,7 +24,11 @@ module GraphQL
24
24
  def result
25
25
  result_name = irep_node.name
26
26
  raw_value = get_raw_value
27
- { result_name => get_finished_value(raw_value) }
27
+ if raw_value == GraphQL::Execution::Execute::SKIP
28
+ {}
29
+ else
30
+ { result_name => get_finished_value(raw_value) }
31
+ end
28
32
  end
29
33
 
30
34
  # GraphQL::Batch depends on this
@@ -52,6 +52,11 @@ module GraphQL
52
52
  @internal_representation
53
53
  end
54
54
 
55
+ def analyzers
56
+ ensure_has_validated
57
+ @query_analyzers
58
+ end
59
+
55
60
  private
56
61
 
57
62
  # If the pipeline wasn't run yet, run it.
@@ -80,13 +85,13 @@ module GraphQL
80
85
  end
81
86
 
82
87
  if @validation_errors.none?
83
- query_analyzers = build_analyzers(@schema, @max_depth, @max_complexity)
84
- if query_analyzers.any?
85
- analysis_results = GraphQL::Analysis.analyze_query(@query, query_analyzers)
86
- @analysis_errors = analysis_results
87
- .flatten # accept n-dimensional array
88
- .select { |r| r.is_a?(GraphQL::AnalysisError) }
89
- end
88
+ @query_analyzers = build_analyzers(@schema, @max_depth, @max_complexity)
89
+ # if query_analyzers.any?
90
+ # analysis_results = GraphQL::Analysis.analyze_query(@query, query_analyzers)
91
+ # @analysis_errors = analysis_results
92
+ # .flatten # accept n-dimensional array
93
+ # .select { |r| r.is_a?(GraphQL::AnalysisError) }
94
+ # end
90
95
  end
91
96
  end
92
97
 
@@ -3,7 +3,7 @@ module GraphQL
3
3
  class Query
4
4
  # Read-only access to query variables, applying default values if needed.
5
5
  class Variables
6
- extend GraphQL::Delegate
6
+ extend Forwardable
7
7
 
8
8
  # @return [Array<GraphQL::Query::VariableValidationError>] Any errors encountered when parsing the provided variables and literal values
9
9
  attr_reader :errors
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+ require "fileutils"
3
+ module GraphQL
4
+ # A rake task for dumping a schema as IDL or JSON.
5
+ #
6
+ # By default, schemas are looked up by name as constants using `schema_name:`.
7
+ # You can provide a `load_schema` function to return your schema another way.
8
+ #
9
+ # `load_context:`, `only:` and `except:` are supported so that
10
+ # you can keep an eye on how filters affect your schema.
11
+ #
12
+ # @example Dump a Schema to .graphql + .json files
13
+ # require "graphql/rake_task"
14
+ # GraphQL::RakeTask.new(schema_name: "MySchema")
15
+ #
16
+ # # $ rake graphql:schema:dump
17
+ # # Schema IDL dumped to ./schema.graphql
18
+ # # Schema JSON dumped to ./schema.json
19
+ #
20
+ # @example Invoking the task from Ruby
21
+ # require "rake"
22
+ # Rake::Task["graphql:schema:dump"].invoke
23
+ class RakeTask
24
+ include Rake::DSL
25
+
26
+ DEFAULT_OPTIONS = {
27
+ namespace: "graphql",
28
+ dependencies: nil,
29
+ schema_name: nil,
30
+ load_schema: ->(task) { Object.const_get(task.schema_name) },
31
+ load_context: ->(task) { {} },
32
+ only: nil,
33
+ except: nil,
34
+ directory: ".",
35
+ idl_outfile: "schema.graphql",
36
+ json_outfile: "schema.json",
37
+ }
38
+
39
+ # @return [String] Namespace for generated tasks
40
+ attr_writer :namespace
41
+
42
+ def rake_namespace
43
+ @namespace
44
+ end
45
+
46
+ # @return [Array<String>]
47
+ attr_accessor :dependencies
48
+
49
+ # @return [String] By default, used to find the schema as a constant.
50
+ # @see {#load_schema} for loading a schema another way
51
+ attr_accessor :schema_name
52
+
53
+ # @return [<#call(task)>] A proc for loading the target GraphQL schema
54
+ attr_accessor :load_schema
55
+
56
+ # @return [<#call(task)>] A callable for loading the query context
57
+ attr_accessor :load_context
58
+
59
+ # @return [<#call(member, ctx)>, nil] A filter for this task
60
+ attr_accessor :only
61
+
62
+ # @return [<#call(member, ctx)>, nil] A filter for this task
63
+ attr_accessor :except
64
+
65
+ # @return [String] target for IDL task
66
+ attr_accessor :idl_outfile
67
+
68
+ # @return [String] target for JSON task
69
+ attr_accessor :json_outfile
70
+
71
+ # @return [String] directory for IDL & JSON files
72
+ attr_accessor :directory
73
+
74
+ # Set the parameters of this task by passing keyword arguments
75
+ # or assigning attributes inside the block
76
+ def initialize(options = {})
77
+ default_dependencies = if Rake::Task.task_defined?("environment")
78
+ [:environment]
79
+ else
80
+ []
81
+ end
82
+
83
+ all_options = DEFAULT_OPTIONS
84
+ .merge(dependencies: default_dependencies)
85
+ .merge(options)
86
+ all_options.each do |k, v|
87
+ self.public_send("#{k}=", v)
88
+ end
89
+
90
+ if block_given?
91
+ yield(self)
92
+ end
93
+
94
+ define_task
95
+ end
96
+
97
+ private
98
+
99
+ # Use the provided `method_name` to generate a string from the specified schema
100
+ # then write it to `file`.
101
+ def write_outfile(method_name, file)
102
+ schema = @load_schema.call(self)
103
+ context = @load_context.call(self)
104
+ result = schema.public_send(method_name, only: @only, except: @except, context: context)
105
+ dir = File.dirname(file)
106
+ FileUtils.mkdir_p(dir)
107
+ File.write(file, result)
108
+ end
109
+
110
+ def idl_path
111
+ File.join(@directory, @idl_outfile)
112
+ end
113
+
114
+ def json_path
115
+ File.join(@directory, @json_outfile)
116
+ end
117
+
118
+ # Use the Rake DSL to add tasks
119
+ def define_task
120
+ namespace(@namespace) do
121
+ namespace("schema") do
122
+ desc("Dump the schema to IDL in #{idl_path}")
123
+ task :idl => @dependencies do
124
+ write_outfile(:to_definition, idl_path)
125
+ puts "Schema IDL dumped into #{idl_path}"
126
+ end
127
+
128
+ desc("Dump the schema to JSON in #{json_path}")
129
+ task :json => @dependencies do
130
+ write_outfile(:to_json, json_path)
131
+ puts "Schema JSON dumped into #{json_path}"
132
+ end
133
+
134
+ desc("Dump the schema to JSON and IDL")
135
+ task :dump => [:idl, :json]
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -3,78 +3,59 @@ module GraphQL
3
3
  module Relay
4
4
  class ArrayConnection < BaseConnection
5
5
  def cursor_from_node(item)
6
- idx = starting_offset + sliced_nodes.find_index(item) + 1
6
+ idx = (after ? index_from_cursor(after) : 0) + sliced_nodes.find_index(item) + 1
7
7
  encode(idx.to_s)
8
8
  end
9
9
 
10
- def has_next_page
11
- !!(first && sliced_nodes.count > limit)
12
- end
10
+ private
11
+
12
+ def first
13
+ return @first if defined? @first
13
14
 
14
- def has_previous_page
15
- !!(last && starting_offset > 0)
15
+ @first = get_limited_arg(:first)
16
+ @first = max_page_size if @first && max_page_size && @first > max_page_size
17
+ @first
16
18
  end
17
19
 
18
- private
20
+ def last
21
+ return @last if defined? @last
22
+
23
+ @last = get_limited_arg(:last)
24
+ @last = max_page_size if @last && max_page_size && @last > max_page_size
25
+ @last
26
+ end
19
27
 
20
28
  # apply first / last limit results
21
29
  def paged_nodes
22
30
  @paged_nodes ||= begin
23
31
  items = sliced_nodes
24
32
 
25
- if limit
26
- items.first(limit)
27
- else
28
- items
29
- end
33
+ items = items.first(first) if first
34
+ items = items.last(last) if last
35
+ items = items.first(max_page_size) if max_page_size && !first && !last
36
+
37
+ items
30
38
  end
31
39
  end
32
40
 
33
41
  # Apply cursors to edges
34
42
  def sliced_nodes
35
- @sliced_nodes ||= nodes[starting_offset..-1] || []
36
- end
37
-
38
- def index_from_cursor(cursor)
39
- decode(cursor).to_i
40
- end
41
-
42
- def starting_offset
43
- @starting_offset = if before
44
- [previous_offset, 0].max
45
- elsif last
46
- [nodes.count - last, 0].max
47
- else
48
- previous_offset
49
- end
50
- end
51
-
52
- def previous_offset
53
- @previous_offset ||= if after
54
- index_from_cursor(after)
43
+ @sliced_nodes ||= if before && after
44
+ nodes[index_from_cursor(after)..index_from_cursor(before)-1] || []
55
45
  elsif before
56
- prev_page_size = [max_page_size, last].compact.min || 0
57
- index_from_cursor(before) - prev_page_size - 1
46
+ nodes[0..index_from_cursor(before)-2] || []
47
+ elsif after
48
+ nodes[index_from_cursor(after)..-1] || []
58
49
  else
59
- 0
50
+ nodes
60
51
  end
61
52
  end
62
53
 
63
- def limit
64
- @limit ||= begin
65
- limit_from_arguments = if first
66
- first
67
- else
68
- if previous_offset < 0
69
- previous_offset + (last ? last : 0)
70
- else
71
- last
72
- end
73
- end
74
- [limit_from_arguments, max_page_size].compact.min
75
- end
54
+ def index_from_cursor(cursor)
55
+ decode(cursor).to_i
76
56
  end
77
57
  end
58
+
78
59
  BaseConnection.register_connection_implementation(Array, ArrayConnection)
79
60
  end
80
61
  end