graphql 1.10.5 → 1.10.6

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