graphql-stitching 1.4.2 → 1.5.0

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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -0
  3. data/README.md +4 -2
  4. data/docs/README.md +1 -0
  5. data/docs/composer.md +1 -1
  6. data/docs/subscriptions.md +208 -0
  7. data/examples/subscriptions/.gitattributes +9 -0
  8. data/examples/subscriptions/.gitignore +35 -0
  9. data/examples/subscriptions/Gemfile +65 -0
  10. data/examples/subscriptions/README.md +38 -0
  11. data/examples/subscriptions/Rakefile +6 -0
  12. data/examples/subscriptions/app/channels/graphql_channel.rb +50 -0
  13. data/examples/subscriptions/app/controllers/graphql_controller.rb +44 -0
  14. data/examples/subscriptions/app/graphql/entities_schema.rb +42 -0
  15. data/examples/subscriptions/app/graphql/stitched_schema.rb +10 -0
  16. data/examples/subscriptions/app/graphql/subscriptions_schema.rb +54 -0
  17. data/examples/subscriptions/app/models/repository.rb +39 -0
  18. data/examples/subscriptions/app/views/graphql/client.html.erb +159 -0
  19. data/examples/subscriptions/bin/bundle +109 -0
  20. data/examples/subscriptions/bin/docker-entrypoint +8 -0
  21. data/examples/subscriptions/bin/importmap +4 -0
  22. data/examples/subscriptions/bin/rails +4 -0
  23. data/examples/subscriptions/bin/rake +4 -0
  24. data/examples/subscriptions/bin/setup +33 -0
  25. data/examples/subscriptions/config/application.rb +14 -0
  26. data/examples/subscriptions/config/boot.rb +4 -0
  27. data/examples/subscriptions/config/cable.yml +10 -0
  28. data/examples/subscriptions/config/credentials.yml.enc +1 -0
  29. data/examples/subscriptions/config/database.yml +25 -0
  30. data/examples/subscriptions/config/environment.rb +5 -0
  31. data/examples/subscriptions/config/environments/development.rb +74 -0
  32. data/examples/subscriptions/config/environments/production.rb +91 -0
  33. data/examples/subscriptions/config/environments/test.rb +64 -0
  34. data/examples/subscriptions/config/initializers/content_security_policy.rb +25 -0
  35. data/examples/subscriptions/config/initializers/filter_parameter_logging.rb +8 -0
  36. data/examples/subscriptions/config/initializers/inflections.rb +16 -0
  37. data/examples/subscriptions/config/initializers/permissions_policy.rb +13 -0
  38. data/examples/subscriptions/config/locales/en.yml +31 -0
  39. data/examples/subscriptions/config/master.key +1 -0
  40. data/examples/subscriptions/config/puma.rb +35 -0
  41. data/examples/subscriptions/config/routes.rb +8 -0
  42. data/examples/subscriptions/config/storage.yml +34 -0
  43. data/examples/subscriptions/config.ru +6 -0
  44. data/examples/subscriptions/db/seeds.rb +9 -0
  45. data/examples/subscriptions/public/404.html +17 -0
  46. data/examples/subscriptions/public/422.html +17 -0
  47. data/examples/subscriptions/public/500.html +16 -0
  48. data/examples/subscriptions/public/apple-touch-icon-precomposed.png +0 -0
  49. data/examples/subscriptions/public/apple-touch-icon.png +0 -0
  50. data/examples/subscriptions/public/favicon.ico +0 -0
  51. data/examples/subscriptions/public/robots.txt +1 -0
  52. data/lib/graphql/stitching/client.rb +18 -11
  53. data/lib/graphql/stitching/composer/resolver_config.rb +1 -1
  54. data/lib/graphql/stitching/composer/validate_resolvers.rb +7 -1
  55. data/lib/graphql/stitching/composer.rb +30 -27
  56. data/lib/graphql/stitching/{shaper.rb → executor/shaper.rb} +3 -3
  57. data/lib/graphql/stitching/executor.rb +20 -11
  58. data/lib/graphql/stitching/http_executable.rb +3 -0
  59. data/lib/graphql/stitching/plan.rb +1 -1
  60. data/lib/graphql/stitching/{planner_step.rb → planner/step.rb} +3 -3
  61. data/lib/graphql/stitching/planner.rb +27 -7
  62. data/lib/graphql/stitching/{skip_include.rb → request/skip_include.rb} +2 -2
  63. data/lib/graphql/stitching/request.rb +42 -4
  64. data/lib/graphql/stitching/resolver/arguments.rb +2 -2
  65. data/lib/graphql/stitching/resolver/keys.rb +2 -3
  66. data/lib/graphql/stitching/resolver.rb +4 -4
  67. data/lib/graphql/stitching/supergraph.rb +5 -2
  68. data/lib/graphql/stitching/util.rb +1 -0
  69. data/lib/graphql/stitching/version.rb +1 -1
  70. data/lib/graphql/stitching.rb +18 -4
  71. metadata +51 -5
@@ -0,0 +1,16 @@
1
+ # Be sure to restart your server when you modify this file.
2
+
3
+ # Add new inflection rules using the following format. Inflections
4
+ # are locale specific, and you may define rules for as many different
5
+ # locales as you wish. All of these examples are active by default:
6
+ # ActiveSupport::Inflector.inflections(:en) do |inflect|
7
+ # inflect.plural /^(ox)$/i, "\\1en"
8
+ # inflect.singular /^(ox)en/i, "\\1"
9
+ # inflect.irregular "person", "people"
10
+ # inflect.uncountable %w( fish sheep )
11
+ # end
12
+
13
+ # These inflection rules are supported but not enabled by default:
14
+ # ActiveSupport::Inflector.inflections(:en) do |inflect|
15
+ # inflect.acronym "RESTful"
16
+ # end
@@ -0,0 +1,13 @@
1
+ # Be sure to restart your server when you modify this file.
2
+
3
+ # Define an application-wide HTTP permissions policy. For further
4
+ # information see: https://developers.google.com/web/updates/2018/06/feature-policy
5
+
6
+ # Rails.application.config.permissions_policy do |policy|
7
+ # policy.camera :none
8
+ # policy.gyroscope :none
9
+ # policy.microphone :none
10
+ # policy.usb :none
11
+ # policy.fullscreen :self
12
+ # policy.payment :self, "https://secure.example.com"
13
+ # end
@@ -0,0 +1,31 @@
1
+ # Files in the config/locales directory are used for internationalization and
2
+ # are automatically loaded by Rails. If you want to use locales other than
3
+ # English, add the necessary files in this directory.
4
+ #
5
+ # To use the locales, use `I18n.t`:
6
+ #
7
+ # I18n.t "hello"
8
+ #
9
+ # In views, this is aliased to just `t`:
10
+ #
11
+ # <%= t("hello") %>
12
+ #
13
+ # To use a different locale, set it with `I18n.locale`:
14
+ #
15
+ # I18n.locale = :es
16
+ #
17
+ # This would use the information in config/locales/es.yml.
18
+ #
19
+ # To learn more about the API, please read the Rails Internationalization guide
20
+ # at https://guides.rubyonrails.org/i18n.html.
21
+ #
22
+ # Be aware that YAML interprets the following case-insensitive strings as
23
+ # booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings
24
+ # must be quoted to be interpreted as strings. For example:
25
+ #
26
+ # en:
27
+ # "yes": yup
28
+ # enabled: "ON"
29
+
30
+ en:
31
+ hello: "Hello world"
@@ -0,0 +1 @@
1
+ b7cc7c63ca7597ff3f5fc4a6cb27b65d
@@ -0,0 +1,35 @@
1
+ # This configuration file will be evaluated by Puma. The top-level methods that
2
+ # are invoked here are part of Puma's configuration DSL. For more information
3
+ # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html.
4
+
5
+ # Puma can serve each request in a thread from an internal thread pool.
6
+ # The `threads` method setting takes two numbers: a minimum and maximum.
7
+ # Any libraries that use thread pools should be configured to match
8
+ # the maximum value specified for Puma. Default is set to 5 threads for minimum
9
+ # and maximum; this matches the default thread size of Active Record.
10
+ max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
11
+ min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
12
+ threads min_threads_count, max_threads_count
13
+
14
+ # Specifies that the worker count should equal the number of processors in production.
15
+ if ENV["RAILS_ENV"] == "production"
16
+ require "concurrent-ruby"
17
+ worker_count = Integer(ENV.fetch("WEB_CONCURRENCY") { Concurrent.physical_processor_count })
18
+ workers worker_count if worker_count > 1
19
+ end
20
+
21
+ # Specifies the `worker_timeout` threshold that Puma will use to wait before
22
+ # terminating a worker in development environments.
23
+ worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
24
+
25
+ # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
26
+ port ENV.fetch("PORT") { 3000 }
27
+
28
+ # Specifies the `environment` that Puma will run in.
29
+ environment ENV.fetch("RAILS_ENV") { "development" }
30
+
31
+ # Specifies the `pidfile` that Puma will use.
32
+ pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
33
+
34
+ # Allow puma to be restarted by `bin/rails restart` command.
35
+ plugin :tmp_restart
@@ -0,0 +1,8 @@
1
+ Rails.application.routes.draw do
2
+ mount ActionCable.server, at: "/cable"
3
+
4
+ post "/graphql", to: "graphql#execute"
5
+ get "/graphql/event", to: "graphql#event"
6
+
7
+ root "graphql#client"
8
+ end
@@ -0,0 +1,34 @@
1
+ test:
2
+ service: Disk
3
+ root: <%= Rails.root.join("tmp/storage") %>
4
+
5
+ local:
6
+ service: Disk
7
+ root: <%= Rails.root.join("storage") %>
8
+
9
+ # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
10
+ # amazon:
11
+ # service: S3
12
+ # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
13
+ # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
14
+ # region: us-east-1
15
+ # bucket: your_own_bucket-<%= Rails.env %>
16
+
17
+ # Remember not to checkin your GCS keyfile to a repository
18
+ # google:
19
+ # service: GCS
20
+ # project: your_project
21
+ # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
22
+ # bucket: your_own_bucket-<%= Rails.env %>
23
+
24
+ # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
25
+ # microsoft:
26
+ # service: AzureStorage
27
+ # storage_account_name: your_account_name
28
+ # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
29
+ # container: your_container_name-<%= Rails.env %>
30
+
31
+ # mirror:
32
+ # service: Mirror
33
+ # primary: local
34
+ # mirrors: [ amazon, google, microsoft ]
@@ -0,0 +1,6 @@
1
+ # This file is used by Rack-based servers to start the application.
2
+
3
+ require_relative "config/environment"
4
+
5
+ run Rails.application
6
+ Rails.application.load_server
@@ -0,0 +1,9 @@
1
+ # This file should ensure the existence of records required to run the application in every environment (production,
2
+ # development, test). The code here should be idempotent so that it can be executed at any point in every environment.
3
+ # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).
4
+ #
5
+ # Example:
6
+ #
7
+ # ["Action", "Comedy", "Drama", "Horror"].each do |genre_name|
8
+ # MovieGenre.find_or_create_by!(name: genre_name)
9
+ # end
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The page you were looking for doesn't exist (404)</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ </head>
7
+
8
+ <body class="rails-default-error-page">
9
+ <div class="dialog">
10
+ <div>
11
+ <h1>The page you were looking for doesn't exist.</h1>
12
+ <p>You may have mistyped the address or the page may have moved.</p>
13
+ </div>
14
+ <p>If you are the application owner check the logs for more information.</p>
15
+ </div>
16
+ </body>
17
+ </html>
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The change you wanted was rejected (422)</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ </head>
7
+
8
+ <body class="rails-default-error-page">
9
+ <div class="dialog">
10
+ <div>
11
+ <h1>The change you wanted was rejected.</h1>
12
+ <p>Maybe you tried to change something you didn't have access to.</p>
13
+ </div>
14
+ <p>If you are the application owner check the logs for more information.</p>
15
+ </div>
16
+ </body>
17
+ </html>
@@ -0,0 +1,16 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>We're sorry, but something went wrong (500)</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ </head>
7
+
8
+ <body class="rails-default-error-page">
9
+ <div class="dialog">
10
+ <div>
11
+ <h1>We're sorry, but something went wrong.</h1>
12
+ </div>
13
+ <p>If you are the application owner check the logs for more information.</p>
14
+ </div>
15
+ </body>
16
+ </html>
File without changes
@@ -0,0 +1 @@
1
+ # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
@@ -4,20 +4,27 @@ require "json"
4
4
 
5
5
  module GraphQL
6
6
  module Stitching
7
+ # Client is an out-of-the-box helper that assembles all
8
+ # stitching components into a workflow that executes requests.
7
9
  class Client
8
10
  class ClientError < StitchingError; end
9
11
 
12
+ # @return [Supergraph] composed supergraph that services incoming requests.
10
13
  attr_reader :supergraph
11
14
 
15
+ # Builds a new client instance. Either `supergraph` or `locations` configuration is required.
16
+ # @param supergraph [Supergraph] optional, a pre-composed supergraph that bypasses composer setup.
17
+ # @param locations [Hash<Symbol, Hash<Symbol, untyped>>] optional, composer configurations for each graph location.
18
+ # @param composer [Composer] optional, a pre-configured composer instance for use with `locations` configuration.
12
19
  def initialize(locations: nil, supergraph: nil, composer: nil)
13
20
  @supergraph = if locations && supergraph
14
21
  raise ClientError, "Cannot provide both locations and a supergraph."
15
- elsif supergraph && !supergraph.is_a?(GraphQL::Stitching::Supergraph)
22
+ elsif supergraph && !supergraph.is_a?(Supergraph)
16
23
  raise ClientError, "Provided supergraph must be a GraphQL::Stitching::Supergraph instance."
17
24
  elsif supergraph
18
25
  supergraph
19
26
  else
20
- composer ||= GraphQL::Stitching::Composer.new
27
+ composer ||= Composer.new
21
28
  composer.perform(locations)
22
29
  end
23
30
 
@@ -26,10 +33,10 @@ module GraphQL
26
33
  @on_error = nil
27
34
  end
28
35
 
29
- def execute(query:, variables: nil, operation_name: nil, context: nil, validate: true)
30
- request = GraphQL::Stitching::Request.new(
36
+ def execute(raw_query = nil, query: nil, variables: nil, operation_name: nil, context: nil, validate: true)
37
+ request = Request.new(
31
38
  @supergraph,
32
- query,
39
+ raw_query || query, # << for parity with GraphQL Ruby Schema.execute
33
40
  operation_name: operation_name,
34
41
  variables: variables,
35
42
  context: context,
@@ -37,17 +44,17 @@ module GraphQL
37
44
 
38
45
  if validate
39
46
  validation_errors = request.validate
40
- return error_result(validation_errors) if validation_errors.any?
47
+ return error_result(request, validation_errors) if validation_errors.any?
41
48
  end
42
49
 
43
50
  request.prepare!
44
51
  load_plan(request)
45
52
  request.execute
46
53
  rescue GraphQL::ParseError, GraphQL::ExecutionError => e
47
- error_result([e])
54
+ error_result(request, [e])
48
55
  rescue StandardError => e
49
56
  custom_message = @on_error.call(request, e) if @on_error
50
- error_result([{ "message" => custom_message || "An unexpected error occured." }])
57
+ error_result(request, [{ "message" => custom_message || "An unexpected error occured." }])
51
58
  end
52
59
 
53
60
  def on_cache_read(&block)
@@ -69,7 +76,7 @@ module GraphQL
69
76
 
70
77
  def load_plan(request)
71
78
  if @on_cache_read && plan_json = @on_cache_read.call(request)
72
- plan = GraphQL::Stitching::Plan.from_json(JSON.parse(plan_json))
79
+ plan = Plan.from_json(JSON.parse(plan_json))
73
80
 
74
81
  # only use plans referencing current resolver versions
75
82
  if plan.ops.all? { |op| !op.resolver || @supergraph.resolvers_by_version[op.resolver] }
@@ -86,12 +93,12 @@ module GraphQL
86
93
  plan
87
94
  end
88
95
 
89
- def error_result(errors)
96
+ def error_result(request, errors)
90
97
  public_errors = errors.map! do |e|
91
98
  e.is_a?(Hash) ? e : e.to_h
92
99
  end
93
100
 
94
- { "errors" => public_errors }
101
+ GraphQL::Query::Result.new(query: request, values: { "errors" => public_errors })
95
102
  end
96
103
  end
97
104
  end
@@ -38,7 +38,7 @@ module GraphQL::Stitching
38
38
  memo[field_path] << new(
39
39
  key: key.to_definition,
40
40
  type_name: entity_type.graphql_name,
41
- arguments: "representations: { #{key_fields.join(", ")}, __typename: $.__typename }",
41
+ arguments: "representations: { #{key_fields.join(", ")}, #{TYPENAME}: $.#{TYPENAME} }",
42
42
  )
43
43
  end
44
44
  end
@@ -5,10 +5,16 @@ module GraphQL::Stitching
5
5
  class ValidateResolvers < BaseValidator
6
6
 
7
7
  def perform(supergraph, composer)
8
+ root_types = [
9
+ supergraph.schema.query,
10
+ supergraph.schema.mutation,
11
+ supergraph.schema.subscription,
12
+ ].tap(&:compact!)
13
+
8
14
  supergraph.schema.types.each do |type_name, type|
9
15
  # objects and interfaces that are not the root operation types
10
16
  next unless type.kind.object? || type.kind.interface?
11
- next if supergraph.schema.query == type || supergraph.schema.mutation == type
17
+ next if root_types.include?(type)
12
18
  next if type.graphql_name.start_with?("__")
13
19
 
14
20
  # multiple subschemas implement the type
@@ -1,12 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "./composer/base_validator"
4
- require_relative "./composer/validate_interfaces"
5
- require_relative "./composer/validate_resolvers"
6
- require_relative "./composer/resolver_config"
3
+ require_relative "composer/base_validator"
4
+ require_relative "composer/validate_interfaces"
5
+ require_relative "composer/validate_resolvers"
6
+ require_relative "composer/resolver_config"
7
7
 
8
8
  module GraphQL
9
9
  module Stitching
10
+ # Composer receives many individual `GraphQL::Schema` instances
11
+ # representing various graph locations and merges them into one
12
+ # combined Supergraph that is validated for integrity.
10
13
  class Composer
11
14
  # @api private
12
15
  NO_DEFAULT_VALUE = begin
@@ -26,9 +29,9 @@ module GraphQL
26
29
  BASIC_ROOT_FIELD_LOCATION_SELECTOR = ->(locations, _info) { locations.last }
27
30
 
28
31
  # @api private
29
- VALIDATORS = [
30
- "ValidateInterfaces",
31
- "ValidateResolvers",
32
+ COMPOSITION_VALIDATORS = [
33
+ ValidateInterfaces,
34
+ ValidateResolvers,
32
35
  ].freeze
33
36
 
34
37
  # @return [String] name of the Query type in the composed schema.
@@ -37,6 +40,9 @@ module GraphQL
37
40
  # @return [String] name of the Mutation type in the composed schema.
38
41
  attr_reader :mutation_name
39
42
 
43
+ # @return [String] name of the Subscription type in the composed schema.
44
+ attr_reader :subscription_name
45
+
40
46
  # @api private
41
47
  attr_reader :subgraph_types_by_name_and_location
42
48
 
@@ -46,6 +52,7 @@ module GraphQL
46
52
  def initialize(
47
53
  query_name: "Query",
48
54
  mutation_name: "Mutation",
55
+ subscription_name: "Subscription",
49
56
  description_merger: nil,
50
57
  deprecation_merger: nil,
51
58
  default_value_merger: nil,
@@ -54,23 +61,27 @@ module GraphQL
54
61
  )
55
62
  @query_name = query_name
56
63
  @mutation_name = mutation_name
64
+ @subscription_name = subscription_name
57
65
  @description_merger = description_merger || BASIC_VALUE_MERGER
58
66
  @deprecation_merger = deprecation_merger || BASIC_VALUE_MERGER
59
67
  @default_value_merger = default_value_merger || BASIC_VALUE_MERGER
60
68
  @directive_kwarg_merger = directive_kwarg_merger || BASIC_VALUE_MERGER
61
69
  @root_field_location_selector = root_field_location_selector || BASIC_ROOT_FIELD_LOCATION_SELECTOR
70
+
71
+ @field_map = {}
72
+ @resolver_map = {}
62
73
  @resolver_configs = {}
63
-
64
- @field_map = nil
65
- @resolver_map = nil
66
- @mapped_type_names = nil
74
+ @mapped_type_names = {}
67
75
  @subgraph_directives_by_name_and_location = nil
68
76
  @subgraph_types_by_name_and_location = nil
69
77
  @schema_directives = nil
70
78
  end
71
79
 
72
80
  def perform(locations_input)
73
- reset!
81
+ if @subgraph_types_by_name_and_location
82
+ raise CompositionError, "Composer may only perform once per instance."
83
+ end
84
+
74
85
  schemas, executables = prepare_locations_input(locations_input)
75
86
 
76
87
  # "directive_name" => "location" => subgraph_directive
@@ -91,7 +102,6 @@ module GraphQL
91
102
  # "Typename" => "location" => subgraph_type
92
103
  @subgraph_types_by_name_and_location = schemas.each_with_object({}) do |(location, schema), memo|
93
104
  raise CompositionError, "Location keys must be strings" unless location.is_a?(String)
94
- raise CompositionError, "The subscription operation is not supported." if schema.subscription
95
105
 
96
106
  introspection_types = schema.introspection_system.types.keys
97
107
  schema.types.each do |type_name, subgraph_type|
@@ -101,10 +111,13 @@ module GraphQL
101
111
  raise CompositionError, "Query name \"#{@query_name}\" is used by non-query type in #{location} schema."
102
112
  elsif type_name == @mutation_name && subgraph_type != schema.mutation
103
113
  raise CompositionError, "Mutation name \"#{@mutation_name}\" is used by non-mutation type in #{location} schema."
114
+ elsif type_name == @subscription_name && subgraph_type != schema.subscription
115
+ raise CompositionError, "Subscription name \"#{@subscription_name}\" is used by non-subscription type in #{location} schema."
104
116
  end
105
117
 
106
118
  type_name = @query_name if subgraph_type == schema.query
107
119
  type_name = @mutation_name if subgraph_type == schema.mutation
120
+ type_name = @subscription_name if subgraph_type == schema.subscription
108
121
  @mapped_type_names[subgraph_type.graphql_name] = type_name if subgraph_type.graphql_name != type_name
109
122
 
110
123
  memo[type_name] ||= {}
@@ -148,6 +161,7 @@ module GraphQL
148
161
  orphan_types(schema_types.values.select { |t| t.respond_to?(:kind) && t.kind.object? })
149
162
  query schema_types[builder.query_name]
150
163
  mutation schema_types[builder.mutation_name]
164
+ subscription schema_types[builder.subscription_name]
151
165
  directives builder.schema_directives.values
152
166
 
153
167
  own_orphan_types.clear
@@ -163,9 +177,8 @@ module GraphQL
163
177
  executables: executables,
164
178
  )
165
179
 
166
- VALIDATORS.each do |validator|
167
- klass = Object.const_get("GraphQL::Stitching::Composer::#{validator}")
168
- klass.new.perform(supergraph, self)
180
+ COMPOSITION_VALIDATORS.each do |validator_class|
181
+ validator_class.new.perform(supergraph, self)
169
182
  end
170
183
 
171
184
  supergraph
@@ -588,7 +601,7 @@ module GraphQL
588
601
  # @!scope class
589
602
  # @!visibility private
590
603
  def select_root_field_locations(schema)
591
- [schema.query, schema.mutation].tap(&:compact!).each do |root_type|
604
+ [schema.query, schema.mutation, schema.subscription].tap(&:compact!).each do |root_type|
592
605
  root_type.fields.each do |root_field_name, root_field|
593
606
  root_field_locations = @field_map[root_type.graphql_name][root_field_name]
594
607
  next unless root_field_locations.length > 1
@@ -660,16 +673,6 @@ module GraphQL
660
673
  memo[enum_name] << :write
661
674
  end
662
675
  end
663
-
664
- private
665
-
666
- def reset!
667
- @field_map = {}
668
- @resolver_map = {}
669
- @mapped_type_names = {}
670
- @subgraph_directives_by_name_and_location = nil
671
- @schema_directives = nil
672
- end
673
676
  end
674
677
  end
675
678
  end
@@ -1,8 +1,8 @@
1
1
  # typed: false
2
2
  # frozen_string_literal: true
3
3
 
4
- module GraphQL
5
- module Stitching
4
+ module GraphQL::Stitching
5
+ class Executor
6
6
  # Shapes the final results payload to the request selection and schema definition.
7
7
  # This eliminates unrequested export selections and applies null bubbling.
8
8
  # @api private
@@ -105,7 +105,7 @@ module GraphQL
105
105
  is_root = parent_type == @root_type
106
106
 
107
107
  case node.name
108
- when "__typename"
108
+ when TYPENAME
109
109
  yield(is_root)
110
110
  true
111
111
  when "__schema", "__type"
@@ -1,11 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
- require_relative "./executor/resolver_source"
5
- require_relative "./executor/root_source"
4
+ require_relative "executor/resolver_source"
5
+ require_relative "executor/root_source"
6
+ require_relative "executor/shaper"
6
7
 
7
8
  module GraphQL
8
9
  module Stitching
10
+ # Executor handles executing upon a planned request.
11
+ # All planned steps are initiated, their results merged,
12
+ # and loaded keys are collected for batching subsequent steps.
13
+ # Final execution results are then shaped to match the request selection.
9
14
  class Executor
10
15
  # @return [Request] the stitching request to execute.
11
16
  attr_reader :request
@@ -19,10 +24,14 @@ module GraphQL
19
24
  # @return [Integer] tally of queries performed while executing.
20
25
  attr_accessor :query_count
21
26
 
22
- def initialize(request, nonblocking: false)
27
+ # Builds a new executor.
28
+ # @param request [Request] the stitching request to execute.
29
+ # @param nonblocking [Boolean] specifies if the dataloader should use async concurrency.
30
+ def initialize(request, data: {}, errors: [], after: 0, nonblocking: false)
23
31
  @request = request
24
- @data = {}
25
- @errors = []
32
+ @data = data
33
+ @errors = errors
34
+ @after = after
26
35
  @query_count = 0
27
36
  @exec_cycles = 0
28
37
  @dataloader = GraphQL::Dataloader.new(nonblocking: nonblocking)
@@ -33,19 +42,19 @@ module GraphQL
33
42
  result = {}
34
43
 
35
44
  if @data && @data.length > 0
36
- result["data"] = raw ? @data : GraphQL::Stitching::Shaper.new(@request).perform!(@data)
45
+ result["data"] = raw ? @data : Shaper.new(@request).perform!(@data)
37
46
  end
38
47
 
39
48
  if @errors.length > 0
40
49
  result["errors"] = @errors
41
50
  end
42
-
43
- result
51
+
52
+ GraphQL::Query::Result.new(query: @request, values: result)
44
53
  end
45
54
 
46
55
  private
47
56
 
48
- def exec!(next_steps = [0])
57
+ def exec!(next_steps = [@after])
49
58
  if @exec_cycles > @request.plan.ops.length
50
59
  # sanity check... if we've exceeded queue size, then something went wrong.
51
60
  raise StitchingError, "Too many execution requests attempted."
@@ -57,8 +66,8 @@ module GraphQL
57
66
  .select { next_steps.include?(_1.after) }
58
67
  .group_by { [_1.location, _1.resolver.nil?] }
59
68
  .map do |(location, root_source), ops|
60
- source_type = root_source ? RootSource : ResolverSource
61
- @dataloader.with(source_type, self, location).request_all(ops)
69
+ source_class = root_source ? RootSource : ResolverSource
70
+ @dataloader.with(source_class, self, location).request_all(ops)
62
71
  end
63
72
 
64
73
  tasks.each(&method(:exec_task))
@@ -6,6 +6,9 @@ require "json"
6
6
 
7
7
  module GraphQL
8
8
  module Stitching
9
+ # HttpExecutable provides an out-of-the-box convenience for sending
10
+ # HTTP post requests to a remote location, or a base class
11
+ # for other implementations with GraphQL multipart uploads.
9
12
  class HttpExecutable
10
13
  # Builds a new executable for proxying subgraph requests via HTTP.
11
14
  # @param url [String] the url of the remote location to proxy.
@@ -2,7 +2,7 @@
2
2
 
3
3
  module GraphQL
4
4
  module Stitching
5
- # Immutable structures representing a query plan.
5
+ # Immutable-ish structures representing a query plan.
6
6
  # May serialize to/from JSON.
7
7
  class Plan
8
8
  Op = Struct.new(
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module GraphQL
4
- module Stitching
3
+ module GraphQL::Stitching
4
+ class Planner
5
5
  # A planned step in the sequence of stitching entrypoints together.
6
6
  # This is a mutable object that may change throughout the planning process.
7
7
  # It ultimately builds an immutable Plan::Op at the end of planning.
8
- class PlannerStep
8
+ class Step
9
9
  GRAPHQL_PRINTER = GraphQL::Language::Printer.new
10
10
 
11
11
  attr_reader :index, :location, :parent_type, :operation_type, :path