graphql 0.17.2 → 0.18.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.
- checksums.yaml +4 -4
- data/lib/graphql.rb +1 -0
- data/lib/graphql/analysis/query_depth.rb +1 -1
- data/lib/graphql/base_type.rb +25 -1
- data/lib/graphql/define.rb +2 -0
- data/lib/graphql/define/assign_connection.rb +11 -0
- data/lib/graphql/define/assign_global_id_field.rb +11 -0
- data/lib/graphql/define/assign_object_field.rb +21 -20
- data/lib/graphql/define/defined_object_proxy.rb +2 -2
- data/lib/graphql/define/instance_definable.rb +13 -3
- data/lib/graphql/field.rb +1 -1
- data/lib/graphql/language/generation.rb +57 -6
- data/lib/graphql/language/lexer.rb +434 -212
- data/lib/graphql/language/lexer.rl +18 -0
- data/lib/graphql/language/nodes.rb +75 -0
- data/lib/graphql/language/parser.rb +853 -341
- data/lib/graphql/language/parser.y +114 -17
- data/lib/graphql/query.rb +15 -1
- data/lib/graphql/relay.rb +13 -0
- data/lib/graphql/relay/array_connection.rb +80 -0
- data/lib/graphql/relay/base_connection.rb +138 -0
- data/lib/graphql/relay/connection_field.rb +54 -0
- data/lib/graphql/relay/connection_type.rb +25 -0
- data/lib/graphql/relay/edge.rb +22 -0
- data/lib/graphql/relay/edge_type.rb +14 -0
- data/lib/graphql/relay/global_id_resolve.rb +15 -0
- data/lib/graphql/relay/global_node_identification.rb +124 -0
- data/lib/graphql/relay/mutation.rb +146 -0
- data/lib/graphql/relay/page_info.rb +13 -0
- data/lib/graphql/relay/relation_connection.rb +98 -0
- data/lib/graphql/schema.rb +3 -0
- data/lib/graphql/schema/printer.rb +12 -2
- data/lib/graphql/static_validation/message.rb +9 -5
- data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
- data/lib/graphql/static_validation/rules/arguments_are_defined.rb +1 -1
- data/lib/graphql/static_validation/rules/directives_are_defined.rb +3 -3
- data/lib/graphql/static_validation/rules/directives_are_in_valid_locations.rb +7 -7
- data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +4 -4
- data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +5 -5
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +6 -6
- data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +17 -9
- data/lib/graphql/static_validation/rules/fragment_types_exist.rb +1 -1
- data/lib/graphql/static_validation/rules/fragments_are_finite.rb +1 -1
- data/lib/graphql/static_validation/rules/fragments_are_on_composite_types.rb +1 -1
- data/lib/graphql/static_validation/rules/fragments_are_used.rb +17 -6
- data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +1 -1
- data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +2 -2
- data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +5 -5
- data/lib/graphql/static_validation/rules/variables_are_input_types.rb +1 -1
- data/lib/graphql/static_validation/rules/variables_are_used_and_defined.rb +12 -11
- data/lib/graphql/static_validation/type_stack.rb +33 -2
- data/lib/graphql/static_validation/validation_context.rb +5 -0
- data/lib/graphql/version.rb +1 -1
- data/readme.md +16 -4
- data/spec/graphql/analysis/analyze_query_spec.rb +31 -2
- data/spec/graphql/analysis/query_complexity_spec.rb +24 -0
- data/spec/graphql/argument_spec.rb +1 -1
- data/spec/graphql/define/instance_definable_spec.rb +9 -0
- data/spec/graphql/field_spec.rb +1 -1
- data/spec/graphql/internal_representation/rewrite_spec.rb +3 -3
- data/spec/graphql/language/generation_spec.rb +25 -4
- data/spec/graphql/language/parser_spec.rb +116 -1
- data/spec/graphql/query_spec.rb +10 -0
- data/spec/graphql/relay/array_connection_spec.rb +164 -0
- data/spec/graphql/relay/connection_type_spec.rb +37 -0
- data/spec/graphql/relay/global_node_identification_spec.rb +149 -0
- data/spec/graphql/relay/mutation_spec.rb +55 -0
- data/spec/graphql/relay/page_info_spec.rb +106 -0
- data/spec/graphql/relay/relation_connection_spec.rb +348 -0
- data/spec/graphql/schema/printer_spec.rb +8 -0
- data/spec/graphql/schema/reduce_types_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/argument_literals_are_compatible_spec.rb +12 -6
- data/spec/graphql/static_validation/rules/arguments_are_defined_spec.rb +8 -4
- data/spec/graphql/static_validation/rules/directives_are_defined_spec.rb +4 -2
- data/spec/graphql/static_validation/rules/directives_are_in_valid_locations_spec.rb +4 -2
- data/spec/graphql/static_validation/rules/fields_are_defined_on_type_spec.rb +7 -2
- data/spec/graphql/static_validation/rules/fields_have_appropriate_selections_spec.rb +4 -2
- data/spec/graphql/static_validation/rules/fragment_spreads_are_possible_spec.rb +6 -3
- data/spec/graphql/static_validation/rules/fragment_types_exist_spec.rb +5 -3
- data/spec/graphql/static_validation/rules/fragments_are_finite_spec.rb +4 -2
- data/spec/graphql/static_validation/rules/fragments_are_on_composite_types_spec.rb +5 -2
- data/spec/graphql/static_validation/rules/fragments_are_used_spec.rb +10 -2
- data/spec/graphql/static_validation/rules/required_arguments_are_present_spec.rb +6 -3
- data/spec/graphql/static_validation/rules/variable_default_values_are_correctly_typed_spec.rb +8 -4
- data/spec/graphql/static_validation/rules/variable_usages_are_allowed_spec.rb +8 -4
- data/spec/graphql/static_validation/rules/variables_are_input_types_spec.rb +6 -3
- data/spec/graphql/static_validation/rules/variables_are_used_and_defined_spec.rb +6 -3
- data/spec/spec_helper.rb +7 -0
- data/spec/support/dairy_app.rb +11 -10
- data/spec/support/star_wars_data.rb +65 -58
- data/spec/support/star_wars_schema.rb +192 -54
- metadata +84 -2
@@ -0,0 +1,54 @@
|
|
1
|
+
module GraphQL
|
2
|
+
module Relay
|
3
|
+
# Provided a GraphQL field which returns a collection of items,
|
4
|
+
# `ConnectionField.create` modifies that field to expose those items
|
5
|
+
# as a collection.
|
6
|
+
#
|
7
|
+
# The original resolve proc is used to fetch items,
|
8
|
+
# then a connection implementation is fetched with {BaseConnection.connection_for_items}.
|
9
|
+
class ConnectionField
|
10
|
+
ARGUMENT_DEFINITIONS = [
|
11
|
+
["first", GraphQL::INT_TYPE, "Returns the first _n_ elements from the list."],
|
12
|
+
["after", GraphQL::STRING_TYPE, "Returns the elements in the list that come after the specified global ID."],
|
13
|
+
["last", GraphQL::INT_TYPE, "Returns the last _n_ elements from the list."],
|
14
|
+
["before", GraphQL::STRING_TYPE, "Returns the elements in the list that come before the specified global ID."],
|
15
|
+
]
|
16
|
+
|
17
|
+
DEFAULT_ARGUMENTS = ARGUMENT_DEFINITIONS.reduce({}) do |memo, arg_defn|
|
18
|
+
argument = GraphQL::Argument.new
|
19
|
+
name, type, description = arg_defn
|
20
|
+
argument.name = name
|
21
|
+
argument.type = type
|
22
|
+
argument.description = description
|
23
|
+
memo[argument.name.to_s] = argument
|
24
|
+
memo
|
25
|
+
end
|
26
|
+
|
27
|
+
# Turn A GraphQL::Field into a connection by:
|
28
|
+
# - Merging in the default arguments
|
29
|
+
# - Transforming its resolve function to return a connection object
|
30
|
+
# @param [GraphQL::Field] A field which returns items to be wrapped as a connection
|
31
|
+
# @param max_page_size [Integer] The maximum number of items which may be requested (if a larger page is requested, it is limited to this number)
|
32
|
+
# @return [GraphQL::Field] A field which serves a connections
|
33
|
+
def self.create(underlying_field, max_page_size: nil)
|
34
|
+
underlying_field.arguments = DEFAULT_ARGUMENTS.merge(underlying_field.arguments)
|
35
|
+
original_resolve = underlying_field.resolve_proc
|
36
|
+
underlying_field.resolve = get_connection_resolve(underlying_field.name, original_resolve, max_page_size: max_page_size)
|
37
|
+
underlying_field
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Wrap the original resolve proc
|
43
|
+
# so you capture its value, then wrap it in a
|
44
|
+
# connection implementation
|
45
|
+
def self.get_connection_resolve(field_name, underlying_resolve, max_page_size: nil)
|
46
|
+
-> (obj, args, ctx) {
|
47
|
+
items = underlying_resolve.call(obj, args, ctx)
|
48
|
+
connection_class = GraphQL::Relay::BaseConnection.connection_for_items(items)
|
49
|
+
connection_class.new(items, args, max_page_size: max_page_size, parent: obj)
|
50
|
+
}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module GraphQL
|
2
|
+
module Relay
|
3
|
+
module ConnectionType
|
4
|
+
# Create a connection which exposes edges of this type
|
5
|
+
def self.create_type(wrapped_type, edge_type: nil, edge_class: nil, &block)
|
6
|
+
edge_type ||= wrapped_type.edge_type
|
7
|
+
edge_class ||= GraphQL::Relay::Edge
|
8
|
+
connection_type_name = "#{wrapped_type.name}Connection"
|
9
|
+
|
10
|
+
connection_type = ObjectType.define do
|
11
|
+
name(connection_type_name)
|
12
|
+
field :edges, types[edge_type] do
|
13
|
+
resolve -> (obj, args, ctx) {
|
14
|
+
obj.edge_nodes.map { |item| edge_class.new(item, obj) }
|
15
|
+
}
|
16
|
+
end
|
17
|
+
field :pageInfo, !PageInfo, property: :page_info
|
18
|
+
block && instance_eval(&block)
|
19
|
+
end
|
20
|
+
|
21
|
+
connection_type
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module GraphQL
|
2
|
+
module Relay
|
3
|
+
# Mostly an internal concern.
|
4
|
+
#
|
5
|
+
# Wraps an object as a `node`, and exposes a connection-specific `cursor`.
|
6
|
+
class Edge < GraphQL::ObjectType
|
7
|
+
attr_reader :node, :connection
|
8
|
+
def initialize(node, connection)
|
9
|
+
@node = node
|
10
|
+
@connection = connection
|
11
|
+
end
|
12
|
+
|
13
|
+
def cursor
|
14
|
+
@cursor ||= connection.cursor_from_node(node)
|
15
|
+
end
|
16
|
+
|
17
|
+
def parent
|
18
|
+
@parent ||= connection.parent
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module GraphQL
|
2
|
+
module Relay
|
3
|
+
module EdgeType
|
4
|
+
def self.create_type(wrapped_type, name: nil, &block)
|
5
|
+
GraphQL::ObjectType.define do
|
6
|
+
name("#{wrapped_type.name}Edge")
|
7
|
+
field :node, wrapped_type
|
8
|
+
field :cursor, !types.String
|
9
|
+
block && instance_eval(&block)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module GraphQL
|
2
|
+
module Relay
|
3
|
+
class GlobalIdResolve
|
4
|
+
def initialize(type_name:, property:)
|
5
|
+
@property = property
|
6
|
+
@type_name = type_name
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(obj, args, ctx)
|
10
|
+
id_value = obj.public_send(@property)
|
11
|
+
ctx.query.schema.node_identification.to_global_id(@type_name, id_value)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
module GraphQL
|
3
|
+
module Relay
|
4
|
+
# This object provides helpers for working with global IDs.
|
5
|
+
# It's assumed you'll only have 1!
|
6
|
+
#
|
7
|
+
# GlobalIdField depends on that, since it calls class methods
|
8
|
+
# which delegate to the singleton instance.
|
9
|
+
#
|
10
|
+
class GlobalNodeIdentification
|
11
|
+
include GraphQL::Define::InstanceDefinable
|
12
|
+
accepts_definitions(:object_from_id, :type_from_object, :to_global_id, :from_global_id, :description)
|
13
|
+
lazy_defined_attr_accessor :description
|
14
|
+
|
15
|
+
class << self
|
16
|
+
attr_accessor :id_separator
|
17
|
+
end
|
18
|
+
|
19
|
+
self.id_separator = "-"
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@to_global_id_proc = DEFAULT_TO_GLOBAL_ID
|
23
|
+
@from_global_id_proc = DEFAULT_FROM_GLOBAL_ID
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns `NodeInterface`, which all Relay types must implement
|
27
|
+
def interface
|
28
|
+
@interface ||= begin
|
29
|
+
ensure_defined
|
30
|
+
ident = self
|
31
|
+
GraphQL::InterfaceType.define do
|
32
|
+
name "Node"
|
33
|
+
field :id, !types.ID
|
34
|
+
resolve_type -> (obj, schema) {
|
35
|
+
ident.type_from_object(obj)
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns a field for finding objects from a global ID, which Relay needs
|
42
|
+
def field
|
43
|
+
ensure_defined
|
44
|
+
ident = self
|
45
|
+
GraphQL::Field.define do
|
46
|
+
type(ident.interface)
|
47
|
+
argument :id, !types.ID
|
48
|
+
resolve -> (obj, args, ctx) {
|
49
|
+
ctx.query.schema.node_identification.object_from_id(args[:id], ctx)
|
50
|
+
}
|
51
|
+
description ident.description
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
DEFAULT_TO_GLOBAL_ID = -> (type_name, id) {
|
56
|
+
id_str = id.to_s
|
57
|
+
if type_name.include?(self.id_separator) || id_str.include?(self.id_separator)
|
58
|
+
raise "to_global_id(#{type_name}, #{id}) contains reserved characters `#{self.id_separator}`"
|
59
|
+
end
|
60
|
+
Base64.strict_encode64([type_name, id_str].join(self.id_separator))
|
61
|
+
}
|
62
|
+
|
63
|
+
DEFAULT_FROM_GLOBAL_ID = -> (global_id) {
|
64
|
+
Base64.decode64(global_id).split(self.id_separator)
|
65
|
+
}
|
66
|
+
|
67
|
+
# Create a global ID for type-name & ID
|
68
|
+
# (This is an opaque transform)
|
69
|
+
def to_global_id(type_name, id)
|
70
|
+
ensure_defined
|
71
|
+
@to_global_id_proc.call(type_name, id)
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_global_id=(proc)
|
75
|
+
ensure_defined
|
76
|
+
@to_global_id_proc = proc
|
77
|
+
end
|
78
|
+
|
79
|
+
# Get type-name & ID from global ID
|
80
|
+
# (This reverts the opaque transform)
|
81
|
+
def from_global_id(global_id)
|
82
|
+
ensure_defined
|
83
|
+
@from_global_id_proc.call(global_id)
|
84
|
+
end
|
85
|
+
|
86
|
+
def from_global_id=(proc)
|
87
|
+
ensure_defined
|
88
|
+
@from_global_id_proc = proc
|
89
|
+
end
|
90
|
+
|
91
|
+
# Use the provided config to
|
92
|
+
# get a type for a given object
|
93
|
+
def type_from_object(object)
|
94
|
+
ensure_defined
|
95
|
+
type_result = @type_from_object_proc.call(object)
|
96
|
+
if type_result.nil?
|
97
|
+
nil
|
98
|
+
elsif !type_result.is_a?(GraphQL::BaseType)
|
99
|
+
type_str = "#{type_result} (#{type_result.class.name})"
|
100
|
+
raise "type_from_object(#{object}) returned #{type_str}, but it should return a GraphQL type"
|
101
|
+
else
|
102
|
+
type_result
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def type_from_object=(proc)
|
107
|
+
ensure_defined
|
108
|
+
@type_from_object_proc = proc
|
109
|
+
end
|
110
|
+
|
111
|
+
# Use the provided config to
|
112
|
+
# get an object from a UUID
|
113
|
+
def object_from_id(id, ctx)
|
114
|
+
ensure_defined
|
115
|
+
@object_from_id_proc.call(id, ctx)
|
116
|
+
end
|
117
|
+
|
118
|
+
def object_from_id=(proc)
|
119
|
+
ensure_defined
|
120
|
+
@object_from_id_proc = proc
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
module GraphQL
|
2
|
+
module Relay
|
3
|
+
# Define a Relay mutation:
|
4
|
+
# - give it a name (used for derived inputs & outputs)
|
5
|
+
# - declare its inputs
|
6
|
+
# - declare its outputs
|
7
|
+
# - declare the mutation procedure
|
8
|
+
#
|
9
|
+
# `resolve` should return a hash with a key for each of the `return_field`s
|
10
|
+
#
|
11
|
+
# Inputs may also contain a `clientMutationId`
|
12
|
+
#
|
13
|
+
# @example Updating the name of an item
|
14
|
+
# UpdateNameMutation = GraphQL::Relay::Mutation.define do
|
15
|
+
# name "UpdateName"
|
16
|
+
#
|
17
|
+
# input_field :name, !types.String
|
18
|
+
# input_field :itemId, !types.ID
|
19
|
+
#
|
20
|
+
# return_field :item, ItemType
|
21
|
+
#
|
22
|
+
# resolve -> (inputs, ctx) {
|
23
|
+
# item = Item.find_by_id(inputs[:id])
|
24
|
+
# item.update(name: inputs[:name])
|
25
|
+
# {item: item}
|
26
|
+
# }
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# MutationType = GraphQL::ObjectType.define do
|
30
|
+
# # The mutation object exposes a field:
|
31
|
+
# field :updateName, field: UpdateNameMutation.field
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# # Then query it:
|
35
|
+
# query_string = %|
|
36
|
+
# mutation updateName {
|
37
|
+
# updateName(input: {itemId: 1, name: "new name", clientMutationId: "1234"}) {
|
38
|
+
# item { name }
|
39
|
+
# clientMutationId
|
40
|
+
# }|
|
41
|
+
#
|
42
|
+
# GraphQL::Query.new(MySchema, query_string).result
|
43
|
+
# # {"data" => {
|
44
|
+
# # "updateName" => {
|
45
|
+
# # "item" => { "name" => "new name"},
|
46
|
+
# # "clientMutationId" => "1234"
|
47
|
+
# # }
|
48
|
+
# # }}
|
49
|
+
#
|
50
|
+
class Mutation
|
51
|
+
include GraphQL::Define::InstanceDefinable
|
52
|
+
accepts_definitions(
|
53
|
+
:name, :description, :resolve,
|
54
|
+
input_field: GraphQL::Define::AssignArgument,
|
55
|
+
return_field: GraphQL::Define::AssignObjectField,
|
56
|
+
)
|
57
|
+
lazy_defined_attr_accessor :name, :description
|
58
|
+
lazy_defined_attr_accessor :fields, :arguments
|
59
|
+
|
60
|
+
# For backwards compat, but do we need this separate API?
|
61
|
+
alias :return_fields :fields
|
62
|
+
alias :input_fields :arguments
|
63
|
+
|
64
|
+
def initialize
|
65
|
+
@fields = {}
|
66
|
+
@arguments = {}
|
67
|
+
end
|
68
|
+
|
69
|
+
def resolve=(proc)
|
70
|
+
ensure_defined
|
71
|
+
@resolve_proc = proc
|
72
|
+
end
|
73
|
+
|
74
|
+
def field
|
75
|
+
@field ||= begin
|
76
|
+
ensure_defined
|
77
|
+
field_return_type = self.return_type
|
78
|
+
field_input_type = self.input_type
|
79
|
+
field_resolve_proc = -> (obj, args, ctx){
|
80
|
+
results_hash = @resolve_proc.call(args[:input], ctx)
|
81
|
+
Result.new(arguments: args, result: results_hash)
|
82
|
+
}
|
83
|
+
GraphQL::Field.define do
|
84
|
+
type(field_return_type)
|
85
|
+
argument :input, !field_input_type
|
86
|
+
resolve(field_resolve_proc)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def return_type
|
92
|
+
@return_type ||= begin
|
93
|
+
ensure_defined
|
94
|
+
mutation_name = name
|
95
|
+
type_name = "#{mutation_name}Payload"
|
96
|
+
type_fields = return_fields
|
97
|
+
GraphQL::ObjectType.define do
|
98
|
+
name(type_name)
|
99
|
+
description("Autogenerated return type of #{mutation_name}")
|
100
|
+
field :clientMutationId, types.String, "A unique identifier for the client performing the mutation."
|
101
|
+
type_fields.each do |name, field_obj|
|
102
|
+
field name, field: field_obj
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def input_type
|
109
|
+
@input_type ||= begin
|
110
|
+
ensure_defined
|
111
|
+
mutation_name = name
|
112
|
+
type_name = "#{mutation_name}Input"
|
113
|
+
type_fields = input_fields
|
114
|
+
GraphQL::InputObjectType.define do
|
115
|
+
name(type_name)
|
116
|
+
description("Autogenerated input type of #{mutation_name}")
|
117
|
+
input_field :clientMutationId, types.String, "A unique identifier for the client performing the mutation."
|
118
|
+
type_fields.each do |name, field_obj|
|
119
|
+
input_field name, field_obj.type, field_obj.description, default_value: field_obj.default_value
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class Result
|
126
|
+
attr_reader :arguments, :result
|
127
|
+
def initialize(arguments:, result:)
|
128
|
+
@arguments = arguments
|
129
|
+
@result = result
|
130
|
+
end
|
131
|
+
|
132
|
+
def clientMutationId
|
133
|
+
arguments[:input][:clientMutationId]
|
134
|
+
end
|
135
|
+
|
136
|
+
def method_missing(name, *args, &block)
|
137
|
+
if result.key?(name)
|
138
|
+
result[name]
|
139
|
+
else
|
140
|
+
super
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module GraphQL
|
2
|
+
module Relay
|
3
|
+
# Wrap a Connection and expose its page info
|
4
|
+
PageInfo = GraphQL::ObjectType.define do
|
5
|
+
name("PageInfo")
|
6
|
+
description("Metadata about a connection")
|
7
|
+
field :hasNextPage, !types.Boolean, "Indicates if there are more pages to fetch", property: :has_next_page
|
8
|
+
field :hasPreviousPage, !types.Boolean, "Indicates if there are any pages prior to the current page", property: :has_previous_page
|
9
|
+
field :startCursor, types.String, "When paginating backwards, the cursor to continue", property: :start_cursor
|
10
|
+
field :endCursor, types.String, "When paginating forwards, the cursor to continue", property: :end_cursor
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module GraphQL
|
2
|
+
module Relay
|
3
|
+
# A connection implementation to expose SQL collection objects.
|
4
|
+
# It works for:
|
5
|
+
# - `ActiveRecord::Relation`
|
6
|
+
# - `Sequel::Dataset`
|
7
|
+
class RelationConnection < BaseConnection
|
8
|
+
def cursor_from_node(item)
|
9
|
+
offset = starting_offset + paged_nodes_array.index(item) + 1
|
10
|
+
Base64.strict_encode64(offset.to_s)
|
11
|
+
end
|
12
|
+
|
13
|
+
def has_next_page
|
14
|
+
!!(first && sliced_nodes.limit(limit + 1).count > limit)
|
15
|
+
end
|
16
|
+
|
17
|
+
def has_previous_page
|
18
|
+
!!(last && starting_offset > 0)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# apply first / last limit results
|
24
|
+
def paged_nodes
|
25
|
+
@paged_nodes ||= begin
|
26
|
+
items = sliced_nodes
|
27
|
+
items.limit(limit)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Apply cursors to edges
|
32
|
+
def sliced_nodes
|
33
|
+
@sliced_nodes ||= begin
|
34
|
+
items = object
|
35
|
+
items.offset(starting_offset)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def offset_from_cursor(cursor)
|
40
|
+
Base64.decode64(cursor).to_i
|
41
|
+
end
|
42
|
+
|
43
|
+
def starting_offset
|
44
|
+
@starting_offset ||= begin
|
45
|
+
if before
|
46
|
+
[previous_offset, 0].max
|
47
|
+
else
|
48
|
+
previous_offset
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Offset from the previous selection, if there was one
|
54
|
+
# Otherwise, zero
|
55
|
+
def previous_offset
|
56
|
+
@previous_offset ||= if after
|
57
|
+
offset_from_cursor(after)
|
58
|
+
elsif before
|
59
|
+
prev_page_size = [max_page_size, last].compact.min || 0
|
60
|
+
offset_from_cursor(before) - prev_page_size - 1
|
61
|
+
else
|
62
|
+
0
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Limit to apply to this query:
|
67
|
+
# - find a value from the query
|
68
|
+
# - don't exceed max_page_size
|
69
|
+
# - otherwise, don't limit
|
70
|
+
def limit
|
71
|
+
@limit ||= begin
|
72
|
+
limit_from_arguments = if first
|
73
|
+
first
|
74
|
+
else
|
75
|
+
if previous_offset < 0
|
76
|
+
previous_offset + (last ? last : 0)
|
77
|
+
else
|
78
|
+
last
|
79
|
+
end
|
80
|
+
end
|
81
|
+
[limit_from_arguments, max_page_size].compact.min
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def paged_nodes_array
|
86
|
+
@paged_nodes_array ||= paged_nodes.to_a
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
if defined?(ActiveRecord::Relation)
|
92
|
+
BaseConnection.register_connection_implementation(ActiveRecord::Relation, RelationConnection)
|
93
|
+
end
|
94
|
+
if defined?(Sequel::Dataset)
|
95
|
+
BaseConnection.register_connection_implementation(Sequel::Dataset, RelationConnection)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|