graphql 1.10.5 → 1.10.6

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.
@@ -12,7 +12,7 @@ module GraphQL
12
12
 
13
13
  def has_previous_page
14
14
  if @has_previous_page.nil?
15
- @has_previous_page = if @after_offset && @after_offset > 0
15
+ @has_previous_page = if after_offset && after_offset > 0
16
16
  true
17
17
  elsif last
18
18
  # See whether there are any nodes _before_ the current offset.
@@ -29,7 +29,7 @@ module GraphQL
29
29
 
30
30
  def has_next_page
31
31
  if @has_next_page.nil?
32
- @has_next_page = if @before_offset && @before_offset > 0
32
+ @has_next_page = if before_offset && before_offset > 0
33
33
  true
34
34
  elsif first
35
35
  relation_count(set_limit(sliced_nodes, first + 1)) == first + 1
@@ -105,33 +105,41 @@ module GraphQL
105
105
  def sliced_nodes
106
106
  @sliced_nodes ||= begin
107
107
  paginated_nodes = items
108
- @after_offset = after && offset_from_cursor(after)
109
- @before_offset = before && offset_from_cursor(before)
110
108
 
111
- if @after_offset
109
+ if after_offset
112
110
  previous_offset = relation_offset(items) || 0
113
- paginated_nodes = set_offset(paginated_nodes, previous_offset + @after_offset)
111
+ paginated_nodes = set_offset(paginated_nodes, previous_offset + after_offset)
114
112
  end
115
113
 
116
- if @before_offset && @after_offset
117
- if @after_offset < @before_offset
114
+ if before_offset && after_offset
115
+ if after_offset < before_offset
118
116
  # Get the number of items between the two cursors
119
- space_between = @before_offset - @after_offset - 1
117
+ space_between = before_offset - after_offset - 1
120
118
  paginated_nodes = set_limit(paginated_nodes, space_between)
121
119
  else
122
120
  # TODO I think this is untested
123
121
  # The cursors overextend one another to an empty set
124
122
  paginated_nodes = null_relation(paginated_nodes)
125
123
  end
126
- elsif @before_offset
124
+ elsif before_offset
127
125
  # Use limit to cut off the tail of the relation
128
- paginated_nodes = set_limit(paginated_nodes, @before_offset - 1)
126
+ paginated_nodes = set_limit(paginated_nodes, before_offset - 1)
129
127
  end
130
128
 
131
129
  paginated_nodes
132
130
  end
133
131
  end
134
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
+
135
143
  # Apply `first` and `last` to `sliced_nodes`,
136
144
  # returning a new relation
137
145
  def limited_nodes
@@ -3,6 +3,7 @@ require "graphql/query/arguments"
3
3
  require "graphql/query/arguments_cache"
4
4
  require "graphql/query/context"
5
5
  require "graphql/query/executor"
6
+ require "graphql/query/fingerprint"
6
7
  require "graphql/query/literal_input"
7
8
  require "graphql/query/null_context"
8
9
  require "graphql/query/result"
@@ -106,7 +107,7 @@ module GraphQL
106
107
  if variables.is_a?(String)
107
108
  raise ArgumentError, "Query variables should be a Hash, not a String. Try JSON.parse to prepare variables."
108
109
  else
109
- @provided_variables = variables
110
+ @provided_variables = variables || {}
110
111
  end
111
112
 
112
113
  @query_string = query_string || query
@@ -265,6 +266,32 @@ module GraphQL
265
266
  }
266
267
  end
267
268
 
269
+ # This contains a few components:
270
+ #
271
+ # - The selected operation name (or `anonymous`)
272
+ # - The fingerprint of the query string
273
+ # - The number of given variables (for readability)
274
+ # - The fingerprint of the given variables
275
+ #
276
+ # This fingerprint can be used to track runs of the same operation-variables combination over time.
277
+ #
278
+ # @see operation_fingerprint
279
+ # @see variables_fingerprint
280
+ # @return [String] An opaque hash identifying this operation-variables combination
281
+ def fingerprint
282
+ @fingerprint ||= "#{operation_fingerprint}/#{variables_fingerprint}"
283
+ end
284
+
285
+ # @return [String] An opaque hash for identifying this query's given query string and selected operation
286
+ def operation_fingerprint
287
+ @operation_fingerprint ||= "#{selected_operation_name || "anonymous"}/#{Fingerprint.generate(query_string)}"
288
+ end
289
+
290
+ # @return [String] An opaque hash for identifying this query's given a variable values (not including defaults)
291
+ def variables_fingerprint
292
+ @variables_fingerprint ||= "#{provided_variables.size}/#{Fingerprint.generate(provided_variables.to_json)}"
293
+ end
294
+
268
295
  def validation_pipeline
269
296
  with_prepared_ast { @validation_pipeline }
270
297
  end
@@ -86,7 +86,8 @@ module GraphQL
86
86
  end
87
87
  end
88
88
 
89
- def_delegators :to_h, :keys, :values, :each, :any?
89
+ def_delegators :to_h, :keys, :values, :each
90
+ def_delegators :@argument_values, :any?
90
91
 
91
92
  def prepare
92
93
  self
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ class Query
5
+ # @api private
6
+ # @see Query#query_fingerprint
7
+ # @see Query#variables_fingerprint
8
+ # @see Query#fingerprint
9
+ module Fingerprint
10
+ # Make an obfuscated hash of the given string (either a query string or variables JSON)
11
+ # @param string [String]
12
+ # @return [String] A normalized, opaque hash
13
+ def self.generate(input_str)
14
+ # Implemented to be:
15
+ # - Short (and uniform) length
16
+ # - Stable
17
+ # - Irreversibly Opaque (don't want to leak variable values)
18
+ # - URL-friendly
19
+ bytes = Digest::SHA256.digest(input_str)
20
+ Base64.urlsafe_encode64(bytes)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -76,15 +76,7 @@ module GraphQL
76
76
  # Set the parameters of this task by passing keyword arguments
77
77
  # or assigning attributes inside the block
78
78
  def initialize(options = {})
79
- default_dependencies = if Rake::Task.task_defined?("environment")
80
- [:environment]
81
- else
82
- []
83
- end
84
-
85
- all_options = DEFAULT_OPTIONS
86
- .merge(dependencies: default_dependencies)
87
- .merge(options)
79
+ all_options = DEFAULT_OPTIONS.merge(options)
88
80
  all_options.each do |k, v|
89
81
  self.public_send("#{k}=", v)
90
82
  end
@@ -117,18 +109,26 @@ module GraphQL
117
109
  File.join(@directory, @json_outfile)
118
110
  end
119
111
 
112
+ def load_rails_environment_if_defined
113
+ if Rake::Task.task_defined?('environment')
114
+ Rake::Task['environment'].invoke
115
+ end
116
+ end
117
+
120
118
  # Use the Rake DSL to add tasks
121
119
  def define_task
122
120
  namespace(@namespace) do
123
121
  namespace("schema") do
124
122
  desc("Dump the schema to IDL in #{idl_path}")
125
123
  task :idl => @dependencies do
124
+ load_rails_environment_if_defined
126
125
  write_outfile(:to_definition, idl_path)
127
126
  puts "Schema IDL dumped into #{idl_path}"
128
127
  end
129
128
 
130
129
  desc("Dump the schema to JSON in #{json_path}")
131
130
  task :json => @dependencies do
131
+ load_rails_environment_if_defined
132
132
  write_outfile(:to_json, json_path)
133
133
  puts "Schema JSON dumped into #{json_path}"
134
134
  end
@@ -96,6 +96,67 @@ module GraphQL
96
96
  end
97
97
  end
98
98
 
99
+ module LazyHandlingMethods
100
+ # Call the given block at the right time, either:
101
+ # - Right away, if `value` is not registered with `lazy_resolve`
102
+ # - After resolving `value`, if it's registered with `lazy_resolve` (eg, `Promise`)
103
+ # @api private
104
+ def after_lazy(value)
105
+ if lazy?(value)
106
+ GraphQL::Execution::Lazy.new do
107
+ result = sync_lazy(value)
108
+ # The returned result might also be lazy, so check it, too
109
+ after_lazy(result) do |final_result|
110
+ yield(final_result) if block_given?
111
+ end
112
+ end
113
+ else
114
+ yield(value) if block_given?
115
+ end
116
+ end
117
+
118
+ # Override this method to handle lazy objects in a custom way.
119
+ # @param value [Object] an instance of a class registered with {.lazy_resolve}
120
+ # @return [Object] A GraphQL-ready (non-lazy) object
121
+ # @api private
122
+ def sync_lazy(value)
123
+ lazy_method = lazy_method_name(value)
124
+ if lazy_method
125
+ synced_value = value.public_send(lazy_method)
126
+ sync_lazy(synced_value)
127
+ else
128
+ value
129
+ end
130
+ end
131
+
132
+ # @return [Symbol, nil] The method name to lazily resolve `obj`, or nil if `obj`'s class wasn't registered wtih {#lazy_resolve}.
133
+ def lazy_method_name(obj)
134
+ lazy_methods.get(obj)
135
+ end
136
+
137
+ # @return [Boolean] True if this object should be lazily resolved
138
+ def lazy?(obj)
139
+ !!lazy_method_name(obj)
140
+ end
141
+
142
+ # Return a lazy if any of `maybe_lazies` are lazy,
143
+ # otherwise, call the block eagerly and return the result.
144
+ # @param maybe_lazies [Array]
145
+ # @api private
146
+ def after_any_lazies(maybe_lazies)
147
+ if maybe_lazies.any? { |l| lazy?(l) }
148
+ GraphQL::Execution::Lazy.all(maybe_lazies).then do |result|
149
+ yield
150
+ end
151
+ else
152
+ yield
153
+ end
154
+ end
155
+ end
156
+
157
+ include LazyHandlingMethods
158
+ extend LazyHandlingMethods
159
+
99
160
  accepts_definitions \
100
161
  :query_execution_strategy, :mutation_execution_strategy, :subscription_execution_strategy,
101
162
  :max_depth, :max_complexity, :default_max_page_size,
@@ -740,16 +801,6 @@ module GraphQL
740
801
  # Error that is raised when [#Schema#from_definition] is passed an invalid schema definition string.
741
802
  class InvalidDocumentError < Error; end;
742
803
 
743
- # @return [Symbol, nil] The method name to lazily resolve `obj`, or nil if `obj`'s class wasn't registered wtih {#lazy_resolve}.
744
- def lazy_method_name(obj)
745
- @lazy_methods.get(obj)
746
- end
747
-
748
- # @return [Boolean] True if this object should be lazily resolved
749
- def lazy?(obj)
750
- !!lazy_method_name(obj)
751
- end
752
-
753
804
  # Return the GraphQL IDL for the schema
754
805
  # @param context [Hash]
755
806
  # @param only [<#call(member, ctx)>]
@@ -1582,48 +1633,6 @@ module GraphQL
1582
1633
  end
1583
1634
  end
1584
1635
 
1585
- # Call the given block at the right time, either:
1586
- # - Right away, if `value` is not registered with `lazy_resolve`
1587
- # - After resolving `value`, if it's registered with `lazy_resolve` (eg, `Promise`)
1588
- # @api private
1589
- def after_lazy(value)
1590
- if lazy?(value)
1591
- GraphQL::Execution::Lazy.new do
1592
- result = sync_lazy(value)
1593
- # The returned result might also be lazy, so check it, too
1594
- after_lazy(result) do |final_result|
1595
- yield(final_result) if block_given?
1596
- end
1597
- end
1598
- else
1599
- yield(value) if block_given?
1600
- end
1601
- end
1602
-
1603
- # Override this method to handle lazy objects in a custom way.
1604
- # @param value [Object] an instance of a class registered with {.lazy_resolve}
1605
- # @param ctx [GraphQL::Query::Context] the context for this query
1606
- # @return [Object] A GraphQL-ready (non-lazy) object
1607
- def sync_lazy(value)
1608
- lazy_method = lazy_method_name(value)
1609
- if lazy_method
1610
- synced_value = value.public_send(lazy_method)
1611
- sync_lazy(synced_value)
1612
- else
1613
- value
1614
- end
1615
- end
1616
-
1617
- # @return [Symbol, nil] The method name to lazily resolve `obj`, or nil if `obj`'s class wasn't registered wtih {#lazy_resolve}.
1618
- def lazy_method_name(obj)
1619
- lazy_methods.get(obj)
1620
- end
1621
-
1622
- # @return [Boolean] True if this object should be lazily resolved
1623
- def lazy?(obj)
1624
- !!lazy_method_name(obj)
1625
- end
1626
-
1627
1636
  private
1628
1637
 
1629
1638
  def lazy_methods
@@ -1756,16 +1765,24 @@ module GraphQL
1756
1765
  }
1757
1766
  own_possible_types[owner.graphql_name] = owner.possible_types
1758
1767
  elsif type.kind.interface? && owner.kind.object?
1759
- new_interfaces = owner.interfaces.map do |t|
1760
- if t.is_a?(String) && t == type.graphql_name
1761
- type
1762
- elsif t.is_a?(LateBoundType) && t.graphql_name == type.graphql_name
1763
- type
1768
+ new_interfaces = []
1769
+ owner.interfaces.each do |int_t|
1770
+ if int_t.is_a?(String) && int_t == type.graphql_name
1771
+ new_interfaces << type
1772
+ elsif int_t.is_a?(LateBoundType) && int_t.graphql_name == type.graphql_name
1773
+ new_interfaces << type
1764
1774
  else
1765
- t
1775
+ # Don't re-add proper interface definitions,
1776
+ # they were probably already added, maybe with options.
1766
1777
  end
1767
1778
  end
1768
1779
  owner.implements(*new_interfaces)
1780
+ new_interfaces.each do |int|
1781
+ pt = own_possible_types[int.graphql_name] ||= []
1782
+ if !pt.include?(owner)
1783
+ pt << owner
1784
+ end
1785
+ end
1769
1786
  end
1770
1787
 
1771
1788
  when nil
@@ -1864,46 +1881,27 @@ module GraphQL
1864
1881
  end
1865
1882
  if type.kind.object?
1866
1883
  own_possible_types[type.graphql_name] = [type]
1867
- type.interfaces.each do |i|
1868
- implementers = own_possible_types[i.graphql_name] ||= []
1869
- implementers << type
1870
- add_type(i, owner: type, late_types: late_types, path: path + ["implements"])
1884
+ type.interface_type_memberships.each do |interface_type_membership|
1885
+ case interface_type_membership
1886
+ when Schema::TypeMembership
1887
+ interface_type = interface_type_membership.abstract_type
1888
+ # We can get these now; we'll have to get late-bound types later
1889
+ if interface_type.is_a?(Module)
1890
+ implementers = own_possible_types[interface_type.graphql_name] ||= []
1891
+ implementers << type
1892
+ end
1893
+ when String, Schema::LateBoundType
1894
+ interface_type = interface_type_membership
1895
+ else
1896
+ raise ArgumentError, "Invariant: unexpected type membership for #{type.graphql_name}: #{interface_type_membership.class} (#{interface_type_membership.inspect})"
1897
+ end
1898
+ add_type(interface_type, owner: type, late_types: late_types, path: path + ["implements"])
1871
1899
  end
1872
1900
  end
1873
1901
  end
1874
1902
  end
1875
1903
  end
1876
1904
 
1877
- # Call the given block at the right time, either:
1878
- # - Right away, if `value` is not registered with `lazy_resolve`
1879
- # - After resolving `value`, if it's registered with `lazy_resolve` (eg, `Promise`)
1880
- # @api private
1881
- def after_lazy(value)
1882
- if lazy?(value)
1883
- GraphQL::Execution::Lazy.new do
1884
- result = sync_lazy(value)
1885
- # The returned result might also be lazy, so check it, too
1886
- after_lazy(result) do |final_result|
1887
- yield(final_result) if block_given?
1888
- end
1889
- end
1890
- else
1891
- yield(value) if block_given?
1892
- end
1893
- end
1894
-
1895
- # @see Schema.sync_lazy for a hook to override
1896
- # @api private
1897
- def sync_lazy(value)
1898
- lazy_method = lazy_method_name(value)
1899
- if lazy_method
1900
- synced_value = value.public_send(lazy_method)
1901
- sync_lazy(synced_value)
1902
- else
1903
- value
1904
- end
1905
- end
1906
-
1907
1905
  protected
1908
1906
 
1909
1907
  def rescues?
@@ -173,7 +173,7 @@ module GraphQL
173
173
  # @param resolver_method [Symbol] The method on the type to call to resolve this field (defaults to `name`)
174
174
  # @param connection [Boolean] `true` if this field should get automagic connection behavior; default is to infer by `*Connection` in the return type name
175
175
  # @param connection_extension [Class] The extension to add, to implement connections. If `nil`, no extension is added.
176
- # @param max_page_size [Integer] For connections, the maximum number of items to return from this field
176
+ # @param max_page_size [Integer, nil] For connections, the maximum number of items to return from this field, or `nil` to allow unlimited results.
177
177
  # @param introspection [Boolean] If true, this field will be marked as `#introspection?` and the name may begin with `__`
178
178
  # @param resolve [<#call(obj, args, ctx)>] **deprecated** for compatibility with <1.8.0
179
179
  # @param field [GraphQL::Field, GraphQL::Schema::Field] **deprecated** for compatibility with <1.8.0
@@ -188,7 +188,7 @@ module GraphQL
188
188
  # @param trace [Boolean] If true, a {GraphQL::Tracing} tracer will measure this scalar field
189
189
  # @param ast_node [Language::Nodes::FieldDefinition, nil] If this schema was parsed from definition, this AST node defined the field
190
190
  # @param method_conflict_warning [Boolean] If false, skip the warning if this field's method conflicts with a built-in method
191
- def initialize(type: nil, name: nil, owner: nil, null: nil, field: nil, function: nil, description: nil, deprecation_reason: nil, method: nil, hash_key: nil, resolver_method: nil, resolve: nil, connection: nil, max_page_size: nil, scope: nil, introspection: false, camelize: true, trace: nil, complexity: 1, ast_node: nil, extras: [], extensions: EMPTY_ARRAY, connection_extension: self.class.connection_extension, resolver_class: nil, subscription_scope: nil, relay_node_field: false, relay_nodes_field: false, method_conflict_warning: true, arguments: EMPTY_HASH, &definition_block)
191
+ def initialize(type: nil, name: nil, owner: nil, null: nil, field: nil, function: nil, description: nil, deprecation_reason: nil, method: nil, hash_key: nil, resolver_method: nil, resolve: nil, connection: nil, max_page_size: :not_given, scope: nil, introspection: false, camelize: true, trace: nil, complexity: 1, ast_node: nil, extras: [], extensions: EMPTY_ARRAY, connection_extension: self.class.connection_extension, resolver_class: nil, subscription_scope: nil, relay_node_field: false, relay_nodes_field: false, method_conflict_warning: true, arguments: EMPTY_HASH, &definition_block)
192
192
  if name.nil?
193
193
  raise ArgumentError, "missing first `name` argument or keyword `name:`"
194
194
  end
@@ -242,7 +242,8 @@ module GraphQL
242
242
  @return_type_expr = type
243
243
  @return_type_null = null
244
244
  @connection = connection
245
- @max_page_size = max_page_size
245
+ @has_max_page_size = max_page_size != :not_given
246
+ @max_page_size = max_page_size == :not_given ? nil : max_page_size
246
247
  @introspection = introspection
247
248
  @extras = extras
248
249
  @resolver_class = resolver_class
@@ -381,7 +382,12 @@ module GraphQL
381
382
  end
382
383
  end
383
384
 
384
- # @return [Integer, nil] Applied to connections if present
385
+ # @return [Boolean] True if this field's {#max_page_size} should override the schema default.
386
+ def has_max_page_size?
387
+ @has_max_page_size
388
+ end
389
+
390
+ # @return [Integer, nil] Applied to connections if {#has_max_page_size?}
385
391
  attr_reader :max_page_size
386
392
 
387
393
  # @return [GraphQL::Field]
@@ -646,22 +652,34 @@ module GraphQL
646
652
  if graphql_args.any? || @extras.any?
647
653
  # Splat the GraphQL::Arguments to Ruby keyword arguments
648
654
  ruby_kwargs = graphql_args.to_kwargs
655
+ maybe_lazies = []
649
656
  # Apply any `prepare` methods. Not great code organization, can this go somewhere better?
650
657
  arguments.each do |name, arg_defn|
651
658
  ruby_kwargs_key = arg_defn.keyword
652
659
 
653
- loads = arg_defn.loads
654
- if ruby_kwargs.key?(ruby_kwargs_key) && loads && !arg_defn.from_resolver?
660
+ if ruby_kwargs.key?(ruby_kwargs_key)
661
+ loads = arg_defn.loads
655
662
  value = ruby_kwargs[ruby_kwargs_key]
656
- ruby_kwargs[ruby_kwargs_key] = if arg_defn.type.list?
657
- value.map { |val| load_application_object(arg_defn, loads, val, field_ctx.query.context) }
663
+ loaded_value = if loads && !arg_defn.from_resolver?
664
+ if arg_defn.type.list?
665
+ loaded_values = value.map { |val| load_application_object(arg_defn, loads, val, field_ctx.query.context) }
666
+ maybe_lazies.concat(loaded_values)
667
+ else
668
+ load_application_object(arg_defn, loads, value, field_ctx.query.context)
669
+ end
658
670
  else
659
- load_application_object(arg_defn, loads, value, field_ctx.query.context)
671
+ value
660
672
  end
661
- end
662
673
 
663
- if ruby_kwargs.key?(ruby_kwargs_key) && arg_defn.prepare
664
- ruby_kwargs[ruby_kwargs_key] = arg_defn.prepare_value(obj, ruby_kwargs[ruby_kwargs_key])
674
+ maybe_lazies << field_ctx.schema.after_lazy(loaded_value) do |loaded_value|
675
+ prepared_value = if arg_defn.prepare
676
+ arg_defn.prepare_value(obj, loaded_value)
677
+ else
678
+ loaded_value
679
+ end
680
+
681
+ ruby_kwargs[ruby_kwargs_key] = prepared_value
682
+ end
665
683
  end
666
684
  end
667
685
 
@@ -669,7 +687,9 @@ module GraphQL
669
687
  ruby_kwargs[extra_arg] = fetch_extra(extra_arg, field_ctx)
670
688
  end
671
689
 
672
- ruby_kwargs
690
+ field_ctx.schema.after_any_lazies(maybe_lazies) do
691
+ ruby_kwargs
692
+ end
673
693
  else
674
694
  NO_ARGS
675
695
  end