graphql 0.17.2 → 0.18.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|