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.

Files changed (230) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/core.rb +18 -2
  3. data/lib/generators/graphql/install_generator.rb +27 -0
  4. data/lib/generators/graphql/object_generator.rb +52 -8
  5. data/lib/generators/graphql/templates/base_argument.erb +2 -0
  6. data/lib/generators/graphql/templates/base_enum.erb +2 -0
  7. data/lib/generators/graphql/templates/base_field.erb +2 -0
  8. data/lib/generators/graphql/templates/base_input_object.erb +2 -0
  9. data/lib/generators/graphql/templates/base_interface.erb +2 -0
  10. data/lib/generators/graphql/templates/base_mutation.erb +2 -0
  11. data/lib/generators/graphql/templates/base_object.erb +2 -0
  12. data/lib/generators/graphql/templates/base_scalar.erb +2 -0
  13. data/lib/generators/graphql/templates/base_union.erb +2 -0
  14. data/lib/generators/graphql/templates/enum.erb +2 -0
  15. data/lib/generators/graphql/templates/graphql_controller.erb +14 -10
  16. data/lib/generators/graphql/templates/interface.erb +2 -0
  17. data/lib/generators/graphql/templates/loader.erb +2 -0
  18. data/lib/generators/graphql/templates/mutation.erb +2 -0
  19. data/lib/generators/graphql/templates/mutation_type.erb +2 -0
  20. data/lib/generators/graphql/templates/object.erb +2 -0
  21. data/lib/generators/graphql/templates/query_type.erb +2 -0
  22. data/lib/generators/graphql/templates/scalar.erb +2 -0
  23. data/lib/generators/graphql/templates/schema.erb +10 -0
  24. data/lib/generators/graphql/templates/union.erb +3 -1
  25. data/lib/graphql/analysis/ast/field_usage.rb +1 -1
  26. data/lib/graphql/analysis/ast/query_complexity.rb +178 -67
  27. data/lib/graphql/analysis/ast/visitor.rb +3 -3
  28. data/lib/graphql/analysis/ast.rb +12 -11
  29. data/lib/graphql/argument.rb +10 -38
  30. data/lib/graphql/backtrace/table.rb +10 -2
  31. data/lib/graphql/backtrace/tracer.rb +2 -1
  32. data/lib/graphql/base_type.rb +4 -0
  33. data/lib/graphql/compatibility/execution_specification/specification_schema.rb +2 -2
  34. data/lib/graphql/compatibility/query_parser_specification/parse_error_specification.rb +5 -9
  35. data/lib/graphql/define/assign_enum_value.rb +1 -1
  36. data/lib/graphql/define/assign_global_id_field.rb +2 -2
  37. data/lib/graphql/define/assign_object_field.rb +3 -3
  38. data/lib/graphql/define/defined_object_proxy.rb +3 -0
  39. data/lib/graphql/define/instance_definable.rb +18 -108
  40. data/lib/graphql/directive/deprecated_directive.rb +1 -12
  41. data/lib/graphql/directive.rb +8 -1
  42. data/lib/graphql/enum_type.rb +5 -71
  43. data/lib/graphql/execution/directive_checks.rb +2 -2
  44. data/lib/graphql/execution/errors.rb +2 -3
  45. data/lib/graphql/execution/execute.rb +1 -1
  46. data/lib/graphql/execution/instrumentation.rb +1 -1
  47. data/lib/graphql/execution/interpreter/argument_value.rb +28 -0
  48. data/lib/graphql/execution/interpreter/arguments.rb +51 -0
  49. data/lib/graphql/execution/interpreter/arguments_cache.rb +79 -0
  50. data/lib/graphql/execution/interpreter/handles_raw_value.rb +25 -0
  51. data/lib/graphql/execution/interpreter/runtime.rb +227 -254
  52. data/lib/graphql/execution/interpreter.rb +34 -11
  53. data/lib/graphql/execution/lazy/lazy_method_map.rb +4 -0
  54. data/lib/graphql/execution/lookahead.rb +39 -114
  55. data/lib/graphql/execution/multiplex.rb +14 -5
  56. data/lib/graphql/field.rb +14 -118
  57. data/lib/graphql/filter.rb +1 -1
  58. data/lib/graphql/function.rb +1 -30
  59. data/lib/graphql/input_object_type.rb +6 -24
  60. data/lib/graphql/integer_decoding_error.rb +17 -0
  61. data/lib/graphql/interface_type.rb +7 -23
  62. data/lib/graphql/internal_representation/scope.rb +2 -2
  63. data/lib/graphql/internal_representation/visit.rb +2 -2
  64. data/lib/graphql/introspection/base_object.rb +2 -5
  65. data/lib/graphql/introspection/directive_type.rb +1 -1
  66. data/lib/graphql/introspection/entry_points.rb +7 -7
  67. data/lib/graphql/introspection/field_type.rb +7 -3
  68. data/lib/graphql/introspection/input_value_type.rb +33 -9
  69. data/lib/graphql/introspection/introspection_query.rb +6 -92
  70. data/lib/graphql/introspection/schema_type.rb +4 -9
  71. data/lib/graphql/introspection/type_type.rb +11 -7
  72. data/lib/graphql/introspection.rb +96 -0
  73. data/lib/graphql/invalid_null_error.rb +18 -0
  74. data/lib/graphql/language/block_string.rb +24 -5
  75. data/lib/graphql/language/definition_slice.rb +21 -10
  76. data/lib/graphql/language/document_from_schema_definition.rb +89 -64
  77. data/lib/graphql/language/lexer.rb +7 -3
  78. data/lib/graphql/language/lexer.rl +7 -3
  79. data/lib/graphql/language/nodes.rb +52 -91
  80. data/lib/graphql/language/parser.rb +719 -717
  81. data/lib/graphql/language/parser.y +104 -98
  82. data/lib/graphql/language/printer.rb +1 -1
  83. data/lib/graphql/language/sanitized_printer.rb +222 -0
  84. data/lib/graphql/language/visitor.rb +2 -2
  85. data/lib/graphql/language.rb +2 -1
  86. data/lib/graphql/name_validator.rb +6 -7
  87. data/lib/graphql/non_null_type.rb +0 -10
  88. data/lib/graphql/object_type.rb +45 -56
  89. data/lib/graphql/pagination/active_record_relation_connection.rb +41 -0
  90. data/lib/graphql/pagination/array_connection.rb +77 -0
  91. data/lib/graphql/pagination/connection.rb +208 -0
  92. data/lib/graphql/pagination/connections.rb +145 -0
  93. data/lib/graphql/pagination/mongoid_relation_connection.rb +25 -0
  94. data/lib/graphql/pagination/relation_connection.rb +185 -0
  95. data/lib/graphql/pagination/sequel_dataset_connection.rb +28 -0
  96. data/lib/graphql/pagination.rb +6 -0
  97. data/lib/graphql/query/arguments.rb +4 -2
  98. data/lib/graphql/query/context.rb +36 -9
  99. data/lib/graphql/query/fingerprint.rb +26 -0
  100. data/lib/graphql/query/input_validation_result.rb +23 -6
  101. data/lib/graphql/query/literal_input.rb +30 -10
  102. data/lib/graphql/query/null_context.rb +5 -1
  103. data/lib/graphql/query/validation_pipeline.rb +4 -1
  104. data/lib/graphql/query/variable_validation_error.rb +1 -1
  105. data/lib/graphql/query/variables.rb +16 -7
  106. data/lib/graphql/query.rb +64 -15
  107. data/lib/graphql/rake_task/validate.rb +3 -0
  108. data/lib/graphql/rake_task.rb +9 -9
  109. data/lib/graphql/relay/array_connection.rb +10 -12
  110. data/lib/graphql/relay/base_connection.rb +23 -13
  111. data/lib/graphql/relay/connection_type.rb +2 -1
  112. data/lib/graphql/relay/edge_type.rb +1 -0
  113. data/lib/graphql/relay/edges_instrumentation.rb +1 -1
  114. data/lib/graphql/relay/mutation.rb +1 -86
  115. data/lib/graphql/relay/node.rb +2 -2
  116. data/lib/graphql/relay/range_add.rb +14 -5
  117. data/lib/graphql/relay/relation_connection.rb +8 -10
  118. data/lib/graphql/scalar_type.rb +15 -59
  119. data/lib/graphql/schema/argument.rb +113 -11
  120. data/lib/graphql/schema/base_64_encoder.rb +2 -0
  121. data/lib/graphql/schema/build_from_definition/resolve_map/default_resolve.rb +1 -1
  122. data/lib/graphql/schema/build_from_definition/resolve_map.rb +13 -5
  123. data/lib/graphql/schema/build_from_definition.rb +212 -190
  124. data/lib/graphql/schema/built_in_types.rb +5 -5
  125. data/lib/graphql/schema/default_type_error.rb +2 -0
  126. data/lib/graphql/schema/directive/deprecated.rb +18 -0
  127. data/lib/graphql/schema/directive/include.rb +1 -1
  128. data/lib/graphql/schema/directive/skip.rb +1 -1
  129. data/lib/graphql/schema/directive.rb +34 -3
  130. data/lib/graphql/schema/enum.rb +52 -4
  131. data/lib/graphql/schema/enum_value.rb +6 -1
  132. data/lib/graphql/schema/field/connection_extension.rb +44 -20
  133. data/lib/graphql/schema/field/scope_extension.rb +1 -1
  134. data/lib/graphql/schema/field.rb +200 -129
  135. data/lib/graphql/schema/find_inherited_value.rb +13 -0
  136. data/lib/graphql/schema/finder.rb +13 -11
  137. data/lib/graphql/schema/input_object.rb +131 -22
  138. data/lib/graphql/schema/interface.rb +26 -8
  139. data/lib/graphql/schema/introspection_system.rb +108 -37
  140. data/lib/graphql/schema/late_bound_type.rb +3 -2
  141. data/lib/graphql/schema/list.rb +47 -0
  142. data/lib/graphql/schema/loader.rb +134 -96
  143. data/lib/graphql/schema/member/base_dsl_methods.rb +29 -12
  144. data/lib/graphql/schema/member/build_type.rb +19 -5
  145. data/lib/graphql/schema/member/cached_graphql_definition.rb +5 -0
  146. data/lib/graphql/schema/member/has_arguments.rb +105 -5
  147. data/lib/graphql/schema/member/has_ast_node.rb +20 -0
  148. data/lib/graphql/schema/member/has_fields.rb +20 -10
  149. data/lib/graphql/schema/member/has_unresolved_type_error.rb +15 -0
  150. data/lib/graphql/schema/member/type_system_helpers.rb +2 -2
  151. data/lib/graphql/schema/member/validates_input.rb +33 -0
  152. data/lib/graphql/schema/member.rb +6 -0
  153. data/lib/graphql/schema/mutation.rb +5 -1
  154. data/lib/graphql/schema/non_null.rb +30 -0
  155. data/lib/graphql/schema/object.rb +65 -12
  156. data/lib/graphql/schema/possible_types.rb +9 -4
  157. data/lib/graphql/schema/printer.rb +0 -15
  158. data/lib/graphql/schema/relay_classic_mutation.rb +5 -3
  159. data/lib/graphql/schema/resolver/has_payload_type.rb +5 -2
  160. data/lib/graphql/schema/resolver.rb +26 -18
  161. data/lib/graphql/schema/scalar.rb +27 -3
  162. data/lib/graphql/schema/subscription.rb +8 -18
  163. data/lib/graphql/schema/timeout.rb +29 -15
  164. data/lib/graphql/schema/traversal.rb +1 -1
  165. data/lib/graphql/schema/type_expression.rb +21 -13
  166. data/lib/graphql/schema/type_membership.rb +2 -2
  167. data/lib/graphql/schema/union.rb +37 -3
  168. data/lib/graphql/schema/unique_within_type.rb +1 -2
  169. data/lib/graphql/schema/validation.rb +10 -2
  170. data/lib/graphql/schema/warden.rb +115 -29
  171. data/lib/graphql/schema.rb +903 -195
  172. data/lib/graphql/static_validation/all_rules.rb +1 -0
  173. data/lib/graphql/static_validation/base_visitor.rb +10 -6
  174. data/lib/graphql/static_validation/literal_validator.rb +52 -27
  175. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +43 -83
  176. data/lib/graphql/static_validation/rules/argument_literals_are_compatible_error.rb +17 -5
  177. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +33 -25
  178. data/lib/graphql/static_validation/rules/directives_are_in_valid_locations.rb +1 -1
  179. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +4 -4
  180. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +5 -5
  181. data/lib/graphql/static_validation/rules/fields_will_merge.rb +29 -21
  182. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
  183. data/lib/graphql/static_validation/rules/input_object_names_are_unique.rb +30 -0
  184. data/lib/graphql/static_validation/rules/input_object_names_are_unique_error.rb +30 -0
  185. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +2 -2
  186. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +4 -5
  187. data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +12 -13
  188. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +5 -6
  189. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +1 -1
  190. data/lib/graphql/static_validation/rules/variables_are_used_and_defined.rb +5 -3
  191. data/lib/graphql/static_validation/type_stack.rb +2 -2
  192. data/lib/graphql/static_validation/validation_context.rb +1 -1
  193. data/lib/graphql/static_validation/validation_timeout_error.rb +25 -0
  194. data/lib/graphql/static_validation/validator.rb +30 -8
  195. data/lib/graphql/static_validation.rb +1 -0
  196. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +89 -19
  197. data/lib/graphql/subscriptions/broadcast_analyzer.rb +84 -0
  198. data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +21 -0
  199. data/lib/graphql/subscriptions/event.rb +23 -5
  200. data/lib/graphql/subscriptions/instrumentation.rb +10 -5
  201. data/lib/graphql/subscriptions/serialize.rb +22 -4
  202. data/lib/graphql/subscriptions/subscription_root.rb +15 -5
  203. data/lib/graphql/subscriptions.rb +108 -35
  204. data/lib/graphql/tracing/active_support_notifications_tracing.rb +14 -10
  205. data/lib/graphql/tracing/appoptics_tracing.rb +171 -0
  206. data/lib/graphql/tracing/appsignal_tracing.rb +8 -0
  207. data/lib/graphql/tracing/data_dog_tracing.rb +8 -0
  208. data/lib/graphql/tracing/new_relic_tracing.rb +9 -12
  209. data/lib/graphql/tracing/platform_tracing.rb +53 -9
  210. data/lib/graphql/tracing/prometheus_tracing/graphql_collector.rb +4 -1
  211. data/lib/graphql/tracing/prometheus_tracing.rb +8 -0
  212. data/lib/graphql/tracing/scout_tracing.rb +19 -0
  213. data/lib/graphql/tracing/skylight_tracing.rb +8 -0
  214. data/lib/graphql/tracing/statsd_tracing.rb +42 -0
  215. data/lib/graphql/tracing.rb +14 -34
  216. data/lib/graphql/types/big_int.rb +1 -1
  217. data/lib/graphql/types/int.rb +9 -2
  218. data/lib/graphql/types/iso_8601_date.rb +3 -3
  219. data/lib/graphql/types/iso_8601_date_time.rb +25 -10
  220. data/lib/graphql/types/relay/base_connection.rb +11 -7
  221. data/lib/graphql/types/relay/base_edge.rb +2 -1
  222. data/lib/graphql/types/string.rb +7 -1
  223. data/lib/graphql/unauthorized_error.rb +1 -1
  224. data/lib/graphql/union_type.rb +13 -28
  225. data/lib/graphql/unresolved_type_error.rb +2 -2
  226. data/lib/graphql/version.rb +1 -1
  227. data/lib/graphql.rb +31 -6
  228. data/readme.md +1 -1
  229. metadata +34 -9
  230. 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, :any?
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