graphql 1.9.17 → 1.11.7
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.
Potentially problematic release.
This version of graphql might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/lib/generators/graphql/core.rb +18 -2
- data/lib/generators/graphql/install_generator.rb +27 -0
- data/lib/generators/graphql/object_generator.rb +52 -8
- data/lib/generators/graphql/templates/base_argument.erb +2 -0
- data/lib/generators/graphql/templates/base_enum.erb +2 -0
- data/lib/generators/graphql/templates/base_field.erb +2 -0
- data/lib/generators/graphql/templates/base_input_object.erb +2 -0
- data/lib/generators/graphql/templates/base_interface.erb +2 -0
- data/lib/generators/graphql/templates/base_mutation.erb +2 -0
- data/lib/generators/graphql/templates/base_object.erb +2 -0
- data/lib/generators/graphql/templates/base_scalar.erb +2 -0
- data/lib/generators/graphql/templates/base_union.erb +2 -0
- data/lib/generators/graphql/templates/enum.erb +2 -0
- data/lib/generators/graphql/templates/graphql_controller.erb +14 -10
- data/lib/generators/graphql/templates/interface.erb +2 -0
- data/lib/generators/graphql/templates/loader.erb +2 -0
- data/lib/generators/graphql/templates/mutation.erb +2 -0
- data/lib/generators/graphql/templates/mutation_type.erb +2 -0
- data/lib/generators/graphql/templates/object.erb +2 -0
- data/lib/generators/graphql/templates/query_type.erb +2 -0
- data/lib/generators/graphql/templates/scalar.erb +2 -0
- data/lib/generators/graphql/templates/schema.erb +10 -0
- data/lib/generators/graphql/templates/union.erb +3 -1
- data/lib/graphql/analysis/ast/field_usage.rb +1 -1
- data/lib/graphql/analysis/ast/query_complexity.rb +178 -67
- data/lib/graphql/analysis/ast/visitor.rb +3 -3
- data/lib/graphql/analysis/ast.rb +12 -11
- data/lib/graphql/argument.rb +10 -38
- data/lib/graphql/backtrace/table.rb +10 -2
- data/lib/graphql/backtrace/tracer.rb +2 -1
- data/lib/graphql/base_type.rb +4 -0
- data/lib/graphql/compatibility/execution_specification/specification_schema.rb +2 -2
- data/lib/graphql/compatibility/query_parser_specification/parse_error_specification.rb +5 -9
- data/lib/graphql/define/assign_enum_value.rb +1 -1
- data/lib/graphql/define/assign_global_id_field.rb +2 -2
- data/lib/graphql/define/assign_object_field.rb +3 -3
- data/lib/graphql/define/defined_object_proxy.rb +3 -0
- data/lib/graphql/define/instance_definable.rb +18 -108
- data/lib/graphql/directive/deprecated_directive.rb +1 -12
- data/lib/graphql/directive.rb +8 -1
- data/lib/graphql/enum_type.rb +5 -71
- data/lib/graphql/execution/directive_checks.rb +2 -2
- data/lib/graphql/execution/errors.rb +2 -3
- data/lib/graphql/execution/execute.rb +1 -1
- data/lib/graphql/execution/instrumentation.rb +1 -1
- data/lib/graphql/execution/interpreter/argument_value.rb +28 -0
- data/lib/graphql/execution/interpreter/arguments.rb +51 -0
- data/lib/graphql/execution/interpreter/arguments_cache.rb +79 -0
- data/lib/graphql/execution/interpreter/handles_raw_value.rb +25 -0
- data/lib/graphql/execution/interpreter/runtime.rb +227 -254
- data/lib/graphql/execution/interpreter.rb +34 -11
- data/lib/graphql/execution/lazy/lazy_method_map.rb +4 -0
- data/lib/graphql/execution/lookahead.rb +39 -114
- data/lib/graphql/execution/multiplex.rb +14 -5
- data/lib/graphql/field.rb +14 -118
- data/lib/graphql/filter.rb +1 -1
- data/lib/graphql/function.rb +1 -30
- data/lib/graphql/input_object_type.rb +6 -24
- data/lib/graphql/integer_decoding_error.rb +17 -0
- data/lib/graphql/interface_type.rb +7 -23
- data/lib/graphql/internal_representation/scope.rb +2 -2
- data/lib/graphql/internal_representation/visit.rb +2 -2
- data/lib/graphql/introspection/base_object.rb +2 -5
- data/lib/graphql/introspection/directive_type.rb +1 -1
- data/lib/graphql/introspection/entry_points.rb +7 -7
- data/lib/graphql/introspection/field_type.rb +7 -3
- data/lib/graphql/introspection/input_value_type.rb +33 -9
- data/lib/graphql/introspection/introspection_query.rb +6 -92
- data/lib/graphql/introspection/schema_type.rb +4 -9
- data/lib/graphql/introspection/type_type.rb +11 -7
- data/lib/graphql/introspection.rb +96 -0
- data/lib/graphql/invalid_null_error.rb +18 -0
- data/lib/graphql/language/block_string.rb +24 -5
- data/lib/graphql/language/definition_slice.rb +21 -10
- data/lib/graphql/language/document_from_schema_definition.rb +89 -64
- data/lib/graphql/language/lexer.rb +7 -3
- data/lib/graphql/language/lexer.rl +7 -3
- data/lib/graphql/language/nodes.rb +52 -91
- data/lib/graphql/language/parser.rb +719 -717
- data/lib/graphql/language/parser.y +104 -98
- data/lib/graphql/language/printer.rb +1 -1
- data/lib/graphql/language/sanitized_printer.rb +222 -0
- data/lib/graphql/language/visitor.rb +2 -2
- data/lib/graphql/language.rb +2 -1
- data/lib/graphql/name_validator.rb +6 -7
- data/lib/graphql/non_null_type.rb +0 -10
- data/lib/graphql/object_type.rb +45 -56
- data/lib/graphql/pagination/active_record_relation_connection.rb +41 -0
- data/lib/graphql/pagination/array_connection.rb +77 -0
- data/lib/graphql/pagination/connection.rb +208 -0
- data/lib/graphql/pagination/connections.rb +145 -0
- data/lib/graphql/pagination/mongoid_relation_connection.rb +25 -0
- data/lib/graphql/pagination/relation_connection.rb +185 -0
- data/lib/graphql/pagination/sequel_dataset_connection.rb +28 -0
- data/lib/graphql/pagination.rb +6 -0
- data/lib/graphql/query/arguments.rb +4 -2
- data/lib/graphql/query/context.rb +36 -9
- data/lib/graphql/query/fingerprint.rb +26 -0
- data/lib/graphql/query/input_validation_result.rb +23 -6
- data/lib/graphql/query/literal_input.rb +30 -10
- data/lib/graphql/query/null_context.rb +5 -1
- data/lib/graphql/query/validation_pipeline.rb +4 -1
- data/lib/graphql/query/variable_validation_error.rb +1 -1
- data/lib/graphql/query/variables.rb +16 -7
- data/lib/graphql/query.rb +64 -15
- data/lib/graphql/rake_task/validate.rb +3 -0
- data/lib/graphql/rake_task.rb +9 -9
- data/lib/graphql/relay/array_connection.rb +10 -12
- data/lib/graphql/relay/base_connection.rb +23 -13
- data/lib/graphql/relay/connection_type.rb +2 -1
- data/lib/graphql/relay/edge_type.rb +1 -0
- data/lib/graphql/relay/edges_instrumentation.rb +1 -1
- data/lib/graphql/relay/mutation.rb +1 -86
- data/lib/graphql/relay/node.rb +2 -2
- data/lib/graphql/relay/range_add.rb +14 -5
- data/lib/graphql/relay/relation_connection.rb +8 -10
- data/lib/graphql/scalar_type.rb +15 -59
- data/lib/graphql/schema/argument.rb +113 -11
- data/lib/graphql/schema/base_64_encoder.rb +2 -0
- data/lib/graphql/schema/build_from_definition/resolve_map/default_resolve.rb +1 -1
- data/lib/graphql/schema/build_from_definition/resolve_map.rb +13 -5
- data/lib/graphql/schema/build_from_definition.rb +212 -190
- data/lib/graphql/schema/built_in_types.rb +5 -5
- data/lib/graphql/schema/default_type_error.rb +2 -0
- data/lib/graphql/schema/directive/deprecated.rb +18 -0
- data/lib/graphql/schema/directive/include.rb +1 -1
- data/lib/graphql/schema/directive/skip.rb +1 -1
- data/lib/graphql/schema/directive.rb +34 -3
- data/lib/graphql/schema/enum.rb +52 -4
- data/lib/graphql/schema/enum_value.rb +6 -1
- data/lib/graphql/schema/field/connection_extension.rb +44 -20
- data/lib/graphql/schema/field/scope_extension.rb +1 -1
- data/lib/graphql/schema/field.rb +200 -129
- data/lib/graphql/schema/find_inherited_value.rb +13 -0
- data/lib/graphql/schema/finder.rb +13 -11
- data/lib/graphql/schema/input_object.rb +131 -22
- data/lib/graphql/schema/interface.rb +26 -8
- data/lib/graphql/schema/introspection_system.rb +108 -37
- data/lib/graphql/schema/late_bound_type.rb +3 -2
- data/lib/graphql/schema/list.rb +47 -0
- data/lib/graphql/schema/loader.rb +134 -96
- data/lib/graphql/schema/member/base_dsl_methods.rb +29 -12
- data/lib/graphql/schema/member/build_type.rb +19 -5
- data/lib/graphql/schema/member/cached_graphql_definition.rb +5 -0
- data/lib/graphql/schema/member/has_arguments.rb +105 -5
- data/lib/graphql/schema/member/has_ast_node.rb +20 -0
- data/lib/graphql/schema/member/has_fields.rb +20 -10
- data/lib/graphql/schema/member/has_unresolved_type_error.rb +15 -0
- data/lib/graphql/schema/member/type_system_helpers.rb +2 -2
- data/lib/graphql/schema/member/validates_input.rb +33 -0
- data/lib/graphql/schema/member.rb +6 -0
- data/lib/graphql/schema/mutation.rb +5 -1
- data/lib/graphql/schema/non_null.rb +30 -0
- data/lib/graphql/schema/object.rb +65 -12
- data/lib/graphql/schema/possible_types.rb +9 -4
- data/lib/graphql/schema/printer.rb +0 -15
- data/lib/graphql/schema/relay_classic_mutation.rb +5 -3
- data/lib/graphql/schema/resolver/has_payload_type.rb +5 -2
- data/lib/graphql/schema/resolver.rb +26 -18
- data/lib/graphql/schema/scalar.rb +27 -3
- data/lib/graphql/schema/subscription.rb +8 -18
- data/lib/graphql/schema/timeout.rb +29 -15
- data/lib/graphql/schema/traversal.rb +1 -1
- data/lib/graphql/schema/type_expression.rb +21 -13
- data/lib/graphql/schema/type_membership.rb +2 -2
- data/lib/graphql/schema/union.rb +37 -3
- data/lib/graphql/schema/unique_within_type.rb +1 -2
- data/lib/graphql/schema/validation.rb +10 -2
- data/lib/graphql/schema/warden.rb +115 -29
- data/lib/graphql/schema.rb +903 -195
- data/lib/graphql/static_validation/all_rules.rb +1 -0
- data/lib/graphql/static_validation/base_visitor.rb +10 -6
- data/lib/graphql/static_validation/literal_validator.rb +52 -27
- data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +43 -83
- data/lib/graphql/static_validation/rules/argument_literals_are_compatible_error.rb +17 -5
- data/lib/graphql/static_validation/rules/arguments_are_defined.rb +33 -25
- data/lib/graphql/static_validation/rules/directives_are_in_valid_locations.rb +1 -1
- 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 +29 -21
- data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
- data/lib/graphql/static_validation/rules/input_object_names_are_unique.rb +30 -0
- data/lib/graphql/static_validation/rules/input_object_names_are_unique_error.rb +30 -0
- data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +2 -2
- data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +4 -5
- data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +12 -13
- data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +5 -6
- 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 +5 -3
- data/lib/graphql/static_validation/type_stack.rb +2 -2
- data/lib/graphql/static_validation/validation_context.rb +1 -1
- data/lib/graphql/static_validation/validation_timeout_error.rb +25 -0
- data/lib/graphql/static_validation/validator.rb +30 -8
- data/lib/graphql/static_validation.rb +1 -0
- data/lib/graphql/subscriptions/action_cable_subscriptions.rb +89 -19
- data/lib/graphql/subscriptions/broadcast_analyzer.rb +84 -0
- data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +21 -0
- data/lib/graphql/subscriptions/event.rb +23 -5
- data/lib/graphql/subscriptions/instrumentation.rb +10 -5
- data/lib/graphql/subscriptions/serialize.rb +22 -4
- data/lib/graphql/subscriptions/subscription_root.rb +15 -5
- data/lib/graphql/subscriptions.rb +108 -35
- data/lib/graphql/tracing/active_support_notifications_tracing.rb +14 -10
- data/lib/graphql/tracing/appoptics_tracing.rb +171 -0
- data/lib/graphql/tracing/appsignal_tracing.rb +8 -0
- data/lib/graphql/tracing/data_dog_tracing.rb +8 -0
- data/lib/graphql/tracing/new_relic_tracing.rb +9 -12
- data/lib/graphql/tracing/platform_tracing.rb +53 -9
- data/lib/graphql/tracing/prometheus_tracing/graphql_collector.rb +4 -1
- data/lib/graphql/tracing/prometheus_tracing.rb +8 -0
- data/lib/graphql/tracing/scout_tracing.rb +19 -0
- data/lib/graphql/tracing/skylight_tracing.rb +8 -0
- data/lib/graphql/tracing/statsd_tracing.rb +42 -0
- data/lib/graphql/tracing.rb +14 -34
- data/lib/graphql/types/big_int.rb +1 -1
- data/lib/graphql/types/int.rb +9 -2
- data/lib/graphql/types/iso_8601_date.rb +3 -3
- data/lib/graphql/types/iso_8601_date_time.rb +25 -10
- data/lib/graphql/types/relay/base_connection.rb +11 -7
- data/lib/graphql/types/relay/base_edge.rb +2 -1
- data/lib/graphql/types/string.rb +7 -1
- data/lib/graphql/unauthorized_error.rb +1 -1
- data/lib/graphql/union_type.rb +13 -28
- data/lib/graphql/unresolved_type_error.rb +2 -2
- data/lib/graphql/version.rb +1 -1
- data/lib/graphql.rb +31 -6
- data/readme.md +1 -1
- metadata +34 -9
- data/lib/graphql/literal_validation_error.rb +0 -6
@@ -0,0 +1,208 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Pagination
|
5
|
+
# A Connection wraps a list of items and provides cursor-based pagination over it.
|
6
|
+
#
|
7
|
+
# Connections were introduced by Facebook's `Relay` front-end framework, but
|
8
|
+
# proved to be generally useful for GraphQL APIs. When in doubt, use connections
|
9
|
+
# to serve lists (like Arrays, ActiveRecord::Relations) via GraphQL.
|
10
|
+
#
|
11
|
+
# Unlike the previous connection implementation, these default to bidirectional pagination.
|
12
|
+
#
|
13
|
+
# Pagination arguments and context may be provided at initialization or assigned later (see {Schema::Field::ConnectionExtension}).
|
14
|
+
class Connection
|
15
|
+
class PaginationImplementationMissingError < GraphQL::Error
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [Object] A list object, from the application. This is the unpaginated value passed into the connection.
|
19
|
+
attr_reader :items
|
20
|
+
|
21
|
+
# @return [GraphQL::Query::Context]
|
22
|
+
attr_accessor :context
|
23
|
+
|
24
|
+
# @return [Object] the object this collection belongs to
|
25
|
+
attr_accessor :parent
|
26
|
+
|
27
|
+
# Raw access to client-provided values. (`max_page_size` not applied to first or last.)
|
28
|
+
attr_accessor :before_value, :after_value, :first_value, :last_value
|
29
|
+
|
30
|
+
# @return [String, nil] the client-provided cursor. `""` is treated as `nil`.
|
31
|
+
def before
|
32
|
+
if defined?(@before)
|
33
|
+
@before
|
34
|
+
else
|
35
|
+
@before = @before_value == "" ? nil : @before_value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [String, nil] the client-provided cursor. `""` is treated as `nil`.
|
40
|
+
def after
|
41
|
+
if defined?(@after)
|
42
|
+
@after
|
43
|
+
else
|
44
|
+
@after = @after_value == "" ? nil : @after_value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# @param items [Object] some unpaginated collection item, like an `Array` or `ActiveRecord::Relation`
|
49
|
+
# @param context [Query::Context]
|
50
|
+
# @param parent [Object] The object this collection belongs to
|
51
|
+
# @param first [Integer, nil] The limit parameter from the client, if it provided one
|
52
|
+
# @param after [String, nil] A cursor for pagination, if the client provided one
|
53
|
+
# @param last [Integer, nil] Limit parameter from the client, if provided
|
54
|
+
# @param before [String, nil] A cursor for pagination, if the client provided one.
|
55
|
+
# @param max_page_size [Integer, nil] A configured value to cap the result size. Applied as `first` if neither first or last are given.
|
56
|
+
def initialize(items, parent: nil, context: nil, first: nil, after: nil, max_page_size: :not_given, last: nil, before: nil, edge_class: nil)
|
57
|
+
@items = items
|
58
|
+
@parent = parent
|
59
|
+
@context = context
|
60
|
+
@first_value = first
|
61
|
+
@after_value = after
|
62
|
+
@last_value = last
|
63
|
+
@before_value = before
|
64
|
+
@edge_class = edge_class || self.class::Edge
|
65
|
+
# This is only true if the object was _initialized_ with an override
|
66
|
+
# or if one is assigned later.
|
67
|
+
@has_max_page_size_override = max_page_size != :not_given
|
68
|
+
@max_page_size = if max_page_size == :not_given
|
69
|
+
nil
|
70
|
+
else
|
71
|
+
max_page_size
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def max_page_size=(new_value)
|
76
|
+
@has_max_page_size_override = true
|
77
|
+
@max_page_size = new_value
|
78
|
+
end
|
79
|
+
|
80
|
+
def max_page_size
|
81
|
+
if @has_max_page_size_override
|
82
|
+
@max_page_size
|
83
|
+
else
|
84
|
+
context.schema.default_max_page_size
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def has_max_page_size_override?
|
89
|
+
@has_max_page_size_override
|
90
|
+
end
|
91
|
+
|
92
|
+
attr_writer :first
|
93
|
+
# @return [Integer, nil]
|
94
|
+
# A clamped `first` value.
|
95
|
+
# (The underlying instance variable doesn't have limits on it.)
|
96
|
+
# If neither `first` nor `last` is given, but `max_page_size` is present, max_page_size is used for first.
|
97
|
+
def first
|
98
|
+
@first ||= begin
|
99
|
+
capped = limit_pagination_argument(@first_value, max_page_size)
|
100
|
+
if capped.nil? && last.nil?
|
101
|
+
capped = max_page_size
|
102
|
+
end
|
103
|
+
capped
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
attr_writer :last
|
108
|
+
# @return [Integer, nil] A clamped `last` value. (The underlying instance variable doesn't have limits on it)
|
109
|
+
def last
|
110
|
+
@last ||= limit_pagination_argument(@last_value, max_page_size)
|
111
|
+
end
|
112
|
+
|
113
|
+
# @return [Array<Edge>] {nodes}, but wrapped with Edge instances
|
114
|
+
def edges
|
115
|
+
@edges ||= nodes.map { |n| @edge_class.new(n, self) }
|
116
|
+
end
|
117
|
+
|
118
|
+
# @return [Class] A wrapper class for edges of this connection
|
119
|
+
attr_accessor :edge_class
|
120
|
+
|
121
|
+
# @return [Array<Object>] A slice of {items}, constrained by {@first_value}/{@after_value}/{@last_value}/{@before_value}
|
122
|
+
def nodes
|
123
|
+
raise PaginationImplementationMissingError, "Implement #{self.class}#nodes to paginate `@items`"
|
124
|
+
end
|
125
|
+
|
126
|
+
# A dynamic alias for compatibility with {Relay::BaseConnection}.
|
127
|
+
# @deprecated use {#nodes} instead
|
128
|
+
def edge_nodes
|
129
|
+
nodes
|
130
|
+
end
|
131
|
+
|
132
|
+
# The connection object itself implements `PageInfo` fields
|
133
|
+
def page_info
|
134
|
+
self
|
135
|
+
end
|
136
|
+
|
137
|
+
# @return [Boolean] True if there are more items after this page
|
138
|
+
def has_next_page
|
139
|
+
raise PaginationImplementationMissingError, "Implement #{self.class}#has_next_page to return the next-page check"
|
140
|
+
end
|
141
|
+
|
142
|
+
# @return [Boolean] True if there were items before these items
|
143
|
+
def has_previous_page
|
144
|
+
raise PaginationImplementationMissingError, "Implement #{self.class}#has_previous_page to return the previous-page check"
|
145
|
+
end
|
146
|
+
|
147
|
+
# @return [String] The cursor of the first item in {nodes}
|
148
|
+
def start_cursor
|
149
|
+
nodes.first && cursor_for(nodes.first)
|
150
|
+
end
|
151
|
+
|
152
|
+
# @return [String] The cursor of the last item in {nodes}
|
153
|
+
def end_cursor
|
154
|
+
nodes.last && cursor_for(nodes.last)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Return a cursor for this item.
|
158
|
+
# @param item [Object] one of the passed in {items}, taken from {nodes}
|
159
|
+
# @return [String]
|
160
|
+
def cursor_for(item)
|
161
|
+
raise PaginationImplementationMissingError, "Implement #{self.class}#cursor_for(item) to return the cursor for #{item.inspect}"
|
162
|
+
end
|
163
|
+
|
164
|
+
private
|
165
|
+
|
166
|
+
# @param argument [nil, Integer] `first` or `last`, as provided by the client
|
167
|
+
# @param max_page_size [nil, Integer]
|
168
|
+
# @return [nil, Integer] `nil` if the input was `nil`, otherwise a value between `0` and `max_page_size`
|
169
|
+
def limit_pagination_argument(argument, max_page_size)
|
170
|
+
if argument
|
171
|
+
if argument < 0
|
172
|
+
argument = 0
|
173
|
+
elsif max_page_size && argument > max_page_size
|
174
|
+
argument = max_page_size
|
175
|
+
end
|
176
|
+
end
|
177
|
+
argument
|
178
|
+
end
|
179
|
+
|
180
|
+
def decode(cursor)
|
181
|
+
context.schema.cursor_encoder.decode(cursor, nonce: true)
|
182
|
+
end
|
183
|
+
|
184
|
+
def encode(cursor)
|
185
|
+
context.schema.cursor_encoder.encode(cursor, nonce: true)
|
186
|
+
end
|
187
|
+
|
188
|
+
# A wrapper around paginated items. It includes a {cursor} for pagination
|
189
|
+
# and could be extended with custom relationship-level data.
|
190
|
+
class Edge
|
191
|
+
attr_reader :node
|
192
|
+
|
193
|
+
def initialize(node, connection)
|
194
|
+
@connection = connection
|
195
|
+
@node = node
|
196
|
+
end
|
197
|
+
|
198
|
+
def parent
|
199
|
+
@connection.parent
|
200
|
+
end
|
201
|
+
|
202
|
+
def cursor
|
203
|
+
@cursor ||= @connection.cursor_for(@node)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Pagination
|
5
|
+
# A schema-level connection wrapper manager.
|
6
|
+
#
|
7
|
+
# Attach as a plugin.
|
8
|
+
#
|
9
|
+
# @example Using new default connections
|
10
|
+
# class MySchema < GraphQL::Schema
|
11
|
+
# use GraphQL::Pagination::Connections
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# @example Adding a custom wrapper
|
15
|
+
# class MySchema < GraphQL::Schema
|
16
|
+
# use GraphQL::Pagination::Connections
|
17
|
+
# connections.add(MyApp::SearchResults, MyApp::SearchResultsConnection)
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# @example Removing default connection support for arrays (they can still be manually wrapped)
|
21
|
+
# class MySchema < GraphQL::Schema
|
22
|
+
# use GraphQL::Pagination::Connections
|
23
|
+
# connections.delete(Array)
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# @see {Schema.connections}
|
27
|
+
class Connections
|
28
|
+
class ImplementationMissingError < GraphQL::Error
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.use(schema_defn)
|
32
|
+
if schema_defn.is_a?(Class)
|
33
|
+
schema_defn.connections = self.new(schema: schema_defn)
|
34
|
+
else
|
35
|
+
# Unwrap a `.define` object
|
36
|
+
schema_defn = schema_defn.target
|
37
|
+
schema_defn.connections = self.new(schema: schema_defn)
|
38
|
+
schema_defn.class.connections = schema_defn.connections
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def initialize(schema:)
|
43
|
+
@schema = schema
|
44
|
+
@wrappers = {}
|
45
|
+
add_default
|
46
|
+
end
|
47
|
+
|
48
|
+
def add(nodes_class, implementation)
|
49
|
+
@wrappers[nodes_class] = implementation
|
50
|
+
end
|
51
|
+
|
52
|
+
def delete(nodes_class)
|
53
|
+
@wrappers.delete(nodes_class)
|
54
|
+
end
|
55
|
+
|
56
|
+
def all_wrappers
|
57
|
+
all_wrappers = {}
|
58
|
+
@schema.ancestors.reverse_each do |schema_class|
|
59
|
+
if schema_class.respond_to?(:connections) && (c = schema_class.connections)
|
60
|
+
all_wrappers.merge!(c.wrappers)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
all_wrappers
|
64
|
+
end
|
65
|
+
|
66
|
+
def wrapper_for(items, wrappers: all_wrappers)
|
67
|
+
impl = nil
|
68
|
+
|
69
|
+
items.class.ancestors.each { |cls|
|
70
|
+
impl = wrappers[cls]
|
71
|
+
break if impl
|
72
|
+
}
|
73
|
+
|
74
|
+
impl
|
75
|
+
end
|
76
|
+
|
77
|
+
# Used by the runtime to wrap values in connection wrappers.
|
78
|
+
# @api Private
|
79
|
+
def wrap(field, parent, items, arguments, context, wrappers: all_wrappers)
|
80
|
+
return items if GraphQL::Execution::Interpreter::RawValue === items
|
81
|
+
|
82
|
+
impl = wrapper_for(items, wrappers: wrappers)
|
83
|
+
|
84
|
+
if impl.nil?
|
85
|
+
raise ImplementationMissingError, "Couldn't find a connection wrapper for #{items.class} during #{field.path} (#{items.inspect})"
|
86
|
+
end
|
87
|
+
|
88
|
+
impl.new(
|
89
|
+
items,
|
90
|
+
context: context,
|
91
|
+
parent: parent,
|
92
|
+
max_page_size: field.max_page_size || context.schema.default_max_page_size,
|
93
|
+
first: arguments[:first],
|
94
|
+
after: arguments[:after],
|
95
|
+
last: arguments[:last],
|
96
|
+
before: arguments[:before],
|
97
|
+
edge_class: edge_class_for_field(field),
|
98
|
+
)
|
99
|
+
end
|
100
|
+
|
101
|
+
# use an override if there is one
|
102
|
+
# @api private
|
103
|
+
def edge_class_for_field(field)
|
104
|
+
conn_type = field.type.unwrap
|
105
|
+
conn_type_edge_type = conn_type.respond_to?(:edge_class) && conn_type.edge_class
|
106
|
+
if conn_type_edge_type && conn_type_edge_type != Relay::Edge
|
107
|
+
conn_type_edge_type
|
108
|
+
else
|
109
|
+
nil
|
110
|
+
end
|
111
|
+
end
|
112
|
+
protected
|
113
|
+
|
114
|
+
attr_reader :wrappers
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def add_default
|
119
|
+
add(Array, Pagination::ArrayConnection)
|
120
|
+
|
121
|
+
if defined?(ActiveRecord::Relation)
|
122
|
+
add(ActiveRecord::Relation, Pagination::ActiveRecordRelationConnection)
|
123
|
+
end
|
124
|
+
|
125
|
+
if defined?(Sequel::Dataset)
|
126
|
+
add(Sequel::Dataset, Pagination::SequelDatasetConnection)
|
127
|
+
end
|
128
|
+
|
129
|
+
if defined?(Mongoid::Criteria)
|
130
|
+
add(Mongoid::Criteria, Pagination::MongoidRelationConnection)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Mongoid 5 and 6
|
134
|
+
if defined?(Mongoid::Relations::Targets::Enumerable)
|
135
|
+
add(Mongoid::Relations::Targets::Enumerable, Pagination::MongoidRelationConnection)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Mongoid 7
|
139
|
+
if defined?(Mongoid::Association::Referenced::HasMany::Targets::Enumerable)
|
140
|
+
add(Mongoid::Association::Referenced::HasMany::Targets::Enumerable, Pagination::MongoidRelationConnection)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "graphql/pagination/relation_connection"
|
3
|
+
|
4
|
+
module GraphQL
|
5
|
+
module Pagination
|
6
|
+
class MongoidRelationConnection < Pagination::RelationConnection
|
7
|
+
def relation_offset(relation)
|
8
|
+
relation.options.skip
|
9
|
+
end
|
10
|
+
|
11
|
+
def relation_limit(relation)
|
12
|
+
relation.options.limit
|
13
|
+
end
|
14
|
+
|
15
|
+
def relation_count(relation)
|
16
|
+
# Mongo's `.count` doesn't apply limit or skip, which we need. So we have to load _everything_!
|
17
|
+
relation.to_a.count
|
18
|
+
end
|
19
|
+
|
20
|
+
def null_relation(relation)
|
21
|
+
relation.without_options.none
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "graphql/pagination/connection"
|
3
|
+
|
4
|
+
module GraphQL
|
5
|
+
module Pagination
|
6
|
+
# A generic class for working with database query objects.
|
7
|
+
class RelationConnection < Pagination::Connection
|
8
|
+
def nodes
|
9
|
+
load_nodes
|
10
|
+
@nodes
|
11
|
+
end
|
12
|
+
|
13
|
+
def has_previous_page
|
14
|
+
if @has_previous_page.nil?
|
15
|
+
@has_previous_page = if after_offset && after_offset > 0
|
16
|
+
true
|
17
|
+
elsif last
|
18
|
+
# See whether there are any nodes _before_ the current offset.
|
19
|
+
# If there _is no_ current offset, then there can't be any nodes before it.
|
20
|
+
# Assume that if the offset is positive, there are nodes before the offset.
|
21
|
+
limited_nodes
|
22
|
+
!(@paged_nodes_offset.nil? || @paged_nodes_offset == 0)
|
23
|
+
else
|
24
|
+
false
|
25
|
+
end
|
26
|
+
end
|
27
|
+
@has_previous_page
|
28
|
+
end
|
29
|
+
|
30
|
+
def has_next_page
|
31
|
+
if @has_next_page.nil?
|
32
|
+
@has_next_page = if before_offset && before_offset > 0
|
33
|
+
true
|
34
|
+
elsif first
|
35
|
+
relation_count(set_limit(sliced_nodes, first + 1)) == first + 1
|
36
|
+
else
|
37
|
+
false
|
38
|
+
end
|
39
|
+
end
|
40
|
+
@has_next_page
|
41
|
+
end
|
42
|
+
|
43
|
+
def cursor_for(item)
|
44
|
+
load_nodes
|
45
|
+
# index in nodes + existing offset + 1 (because it's offset, not index)
|
46
|
+
offset = nodes.index(item) + 1 + (@paged_nodes_offset || 0) + (relation_offset(items) || 0)
|
47
|
+
encode(offset.to_s)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# @param relation [Object] A database query object
|
53
|
+
# @return [Integer, nil] The offset value, or nil if there isn't one
|
54
|
+
def relation_offset(relation)
|
55
|
+
raise "#{self.class}#relation_offset(relation) must return the offset value for a #{relation.class} (#{relation.inspect})"
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param relation [Object] A database query object
|
59
|
+
# @return [Integer, nil] The limit value, or nil if there isn't one
|
60
|
+
def relation_limit(relation)
|
61
|
+
raise "#{self.class}#relation_limit(relation) must return the limit value for a #{relation.class} (#{relation.inspect})"
|
62
|
+
end
|
63
|
+
|
64
|
+
# @param relation [Object] A database query object
|
65
|
+
# @return [Integer, nil] The number of items in this relation (hopefully determined without loading all records into memory!)
|
66
|
+
def relation_count(relation)
|
67
|
+
raise "#{self.class}#relation_count(relation) must return the count of records for a #{relation.class} (#{relation.inspect})"
|
68
|
+
end
|
69
|
+
|
70
|
+
# @param relation [Object] A database query object
|
71
|
+
# @return [Object] A modified query object which will return no records
|
72
|
+
def null_relation(relation)
|
73
|
+
raise "#{self.class}#null_relation(relation) must return an empty relation for a #{relation.class} (#{relation.inspect})"
|
74
|
+
end
|
75
|
+
|
76
|
+
# @return [Integer]
|
77
|
+
def offset_from_cursor(cursor)
|
78
|
+
decode(cursor).to_i
|
79
|
+
end
|
80
|
+
|
81
|
+
# Abstract this operation so we can always ignore inputs less than zero.
|
82
|
+
# (Sequel doesn't like it, understandably.)
|
83
|
+
def set_offset(relation, offset_value)
|
84
|
+
if offset_value >= 0
|
85
|
+
relation.offset(offset_value)
|
86
|
+
else
|
87
|
+
relation.offset(0)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Abstract this operation so we can always ignore inputs less than zero.
|
92
|
+
# (Sequel doesn't like it, understandably.)
|
93
|
+
def set_limit(relation, limit_value)
|
94
|
+
if limit_value > 0
|
95
|
+
relation.limit(limit_value)
|
96
|
+
elsif limit_value == 0
|
97
|
+
null_relation(relation)
|
98
|
+
else
|
99
|
+
relation
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Apply `before` and `after` to the underlying `items`,
|
104
|
+
# returning a new relation.
|
105
|
+
def sliced_nodes
|
106
|
+
@sliced_nodes ||= begin
|
107
|
+
paginated_nodes = items
|
108
|
+
|
109
|
+
if after_offset
|
110
|
+
previous_offset = relation_offset(items) || 0
|
111
|
+
paginated_nodes = set_offset(paginated_nodes, previous_offset + after_offset)
|
112
|
+
end
|
113
|
+
|
114
|
+
if before_offset && after_offset
|
115
|
+
if after_offset < before_offset
|
116
|
+
# Get the number of items between the two cursors
|
117
|
+
space_between = before_offset - after_offset - 1
|
118
|
+
paginated_nodes = set_limit(paginated_nodes, space_between)
|
119
|
+
else
|
120
|
+
# TODO I think this is untested
|
121
|
+
# The cursors overextend one another to an empty set
|
122
|
+
paginated_nodes = null_relation(paginated_nodes)
|
123
|
+
end
|
124
|
+
elsif before_offset
|
125
|
+
# Use limit to cut off the tail of the relation
|
126
|
+
paginated_nodes = set_limit(paginated_nodes, before_offset - 1)
|
127
|
+
end
|
128
|
+
|
129
|
+
paginated_nodes
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# @return [Integer, nil]
|
134
|
+
def before_offset
|
135
|
+
@before_offset ||= before && offset_from_cursor(before)
|
136
|
+
end
|
137
|
+
|
138
|
+
# @return [Integer, nil]
|
139
|
+
def after_offset
|
140
|
+
@after_offset ||= after && offset_from_cursor(after)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Apply `first` and `last` to `sliced_nodes`,
|
144
|
+
# returning a new relation
|
145
|
+
def limited_nodes
|
146
|
+
@limited_nodes ||= begin
|
147
|
+
paginated_nodes = sliced_nodes
|
148
|
+
previous_limit = relation_limit(paginated_nodes)
|
149
|
+
|
150
|
+
if first && (previous_limit.nil? || previous_limit > first)
|
151
|
+
# `first` would create a stricter limit that the one already applied, so add it
|
152
|
+
paginated_nodes = set_limit(paginated_nodes, first)
|
153
|
+
end
|
154
|
+
|
155
|
+
if last
|
156
|
+
if (lv = relation_limit(paginated_nodes))
|
157
|
+
if last <= lv
|
158
|
+
# `last` is a smaller slice than the current limit, so apply it
|
159
|
+
offset = (relation_offset(paginated_nodes) || 0) + (lv - last)
|
160
|
+
paginated_nodes = set_offset(paginated_nodes, offset)
|
161
|
+
paginated_nodes = set_limit(paginated_nodes, last)
|
162
|
+
end
|
163
|
+
else
|
164
|
+
# No limit, so get the last items
|
165
|
+
sliced_nodes_count = relation_count(@sliced_nodes)
|
166
|
+
offset = (relation_offset(paginated_nodes) || 0) + sliced_nodes_count - [last, sliced_nodes_count].min
|
167
|
+
paginated_nodes = set_offset(paginated_nodes, offset)
|
168
|
+
paginated_nodes = set_limit(paginated_nodes, last)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
@paged_nodes_offset = relation_offset(paginated_nodes)
|
173
|
+
paginated_nodes
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Load nodes after applying first/last/before/after,
|
178
|
+
# returns an array of nodes
|
179
|
+
def load_nodes
|
180
|
+
# Return an array so we can consistently use `.index(node)` on it
|
181
|
+
@nodes ||= limited_nodes.to_a
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "graphql/pagination/relation_connection"
|
3
|
+
|
4
|
+
module GraphQL
|
5
|
+
module Pagination
|
6
|
+
# Customizes `RelationConnection` to work with `Sequel::Dataset`s.
|
7
|
+
class SequelDatasetConnection < Pagination::RelationConnection
|
8
|
+
private
|
9
|
+
|
10
|
+
def relation_offset(relation)
|
11
|
+
relation.opts[:offset]
|
12
|
+
end
|
13
|
+
|
14
|
+
def relation_limit(relation)
|
15
|
+
relation.opts[:limit]
|
16
|
+
end
|
17
|
+
|
18
|
+
def relation_count(relation)
|
19
|
+
# Remove order to make it faster
|
20
|
+
relation.order(nil).count
|
21
|
+
end
|
22
|
+
|
23
|
+
def null_relation(relation)
|
24
|
+
relation.where(false)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "graphql/pagination/array_connection"
|
3
|
+
require "graphql/pagination/active_record_relation_connection"
|
4
|
+
require "graphql/pagination/connections"
|
5
|
+
require "graphql/pagination/mongoid_relation_connection"
|
6
|
+
require "graphql/pagination/sequel_dataset_connection"
|
@@ -11,6 +11,7 @@ module GraphQL
|
|
11
11
|
def self.construct_arguments_class(argument_owner)
|
12
12
|
argument_definitions = argument_owner.arguments
|
13
13
|
argument_owner.arguments_class = Class.new(self) do
|
14
|
+
self.argument_owner = argument_owner
|
14
15
|
self.argument_definitions = argument_definitions
|
15
16
|
|
16
17
|
argument_definitions.each do |_arg_name, arg_definition|
|
@@ -85,7 +86,8 @@ module GraphQL
|
|
85
86
|
end
|
86
87
|
end
|
87
88
|
|
88
|
-
def_delegators :to_h, :keys, :values, :each
|
89
|
+
def_delegators :to_h, :keys, :values, :each
|
90
|
+
def_delegators :@argument_values, :any?
|
89
91
|
|
90
92
|
def prepare
|
91
93
|
self
|
@@ -101,7 +103,7 @@ module GraphQL
|
|
101
103
|
end
|
102
104
|
|
103
105
|
class << self
|
104
|
-
attr_accessor :argument_definitions
|
106
|
+
attr_accessor :argument_definitions, :argument_owner
|
105
107
|
end
|
106
108
|
|
107
109
|
NoArguments = Class.new(self) do
|