graphql-stitching 1.4.3 → 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.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/README.md +4 -2
- data/docs/README.md +1 -0
- data/docs/composer.md +1 -1
- data/docs/subscriptions.md +208 -0
- data/examples/subscriptions/.gitattributes +9 -0
- data/examples/subscriptions/.gitignore +35 -0
- data/examples/subscriptions/Gemfile +65 -0
- data/examples/subscriptions/README.md +38 -0
- data/examples/subscriptions/Rakefile +6 -0
- data/examples/subscriptions/app/channels/graphql_channel.rb +50 -0
- data/examples/subscriptions/app/controllers/graphql_controller.rb +44 -0
- data/examples/subscriptions/app/graphql/entities_schema.rb +42 -0
- data/examples/subscriptions/app/graphql/stitched_schema.rb +10 -0
- data/examples/subscriptions/app/graphql/subscriptions_schema.rb +54 -0
- data/examples/subscriptions/app/models/repository.rb +39 -0
- data/examples/subscriptions/app/views/graphql/client.html.erb +159 -0
- data/examples/subscriptions/bin/bundle +109 -0
- data/examples/subscriptions/bin/docker-entrypoint +8 -0
- data/examples/subscriptions/bin/importmap +4 -0
- data/examples/subscriptions/bin/rails +4 -0
- data/examples/subscriptions/bin/rake +4 -0
- data/examples/subscriptions/bin/setup +33 -0
- data/examples/subscriptions/config/application.rb +14 -0
- data/examples/subscriptions/config/boot.rb +4 -0
- data/examples/subscriptions/config/cable.yml +10 -0
- data/examples/subscriptions/config/credentials.yml.enc +1 -0
- data/examples/subscriptions/config/database.yml +25 -0
- data/examples/subscriptions/config/environment.rb +5 -0
- data/examples/subscriptions/config/environments/development.rb +74 -0
- data/examples/subscriptions/config/environments/production.rb +91 -0
- data/examples/subscriptions/config/environments/test.rb +64 -0
- data/examples/subscriptions/config/initializers/content_security_policy.rb +25 -0
- data/examples/subscriptions/config/initializers/filter_parameter_logging.rb +8 -0
- data/examples/subscriptions/config/initializers/inflections.rb +16 -0
- data/examples/subscriptions/config/initializers/permissions_policy.rb +13 -0
- data/examples/subscriptions/config/locales/en.yml +31 -0
- data/examples/subscriptions/config/master.key +1 -0
- data/examples/subscriptions/config/puma.rb +35 -0
- data/examples/subscriptions/config/routes.rb +8 -0
- data/examples/subscriptions/config/storage.yml +34 -0
- data/examples/subscriptions/config.ru +6 -0
- data/examples/subscriptions/db/seeds.rb +9 -0
- data/examples/subscriptions/public/404.html +17 -0
- data/examples/subscriptions/public/422.html +17 -0
- data/examples/subscriptions/public/500.html +16 -0
- data/examples/subscriptions/public/apple-touch-icon-precomposed.png +0 -0
- data/examples/subscriptions/public/apple-touch-icon.png +0 -0
- data/examples/subscriptions/public/favicon.ico +0 -0
- data/examples/subscriptions/public/robots.txt +1 -0
- data/lib/graphql/stitching/client.rb +18 -11
- data/lib/graphql/stitching/composer/resolver_config.rb +1 -1
- data/lib/graphql/stitching/composer/validate_resolvers.rb +7 -1
- data/lib/graphql/stitching/composer.rb +30 -27
- data/lib/graphql/stitching/executor/shaper.rb +1 -1
- data/lib/graphql/stitching/executor.rb +19 -11
- data/lib/graphql/stitching/http_executable.rb +3 -0
- data/lib/graphql/stitching/plan.rb +1 -1
- data/lib/graphql/stitching/planner.rb +21 -5
- data/lib/graphql/stitching/{skip_include.rb → request/skip_include.rb} +2 -2
- data/lib/graphql/stitching/request.rb +42 -4
- data/lib/graphql/stitching/resolver/arguments.rb +2 -2
- data/lib/graphql/stitching/resolver/keys.rb +2 -3
- data/lib/graphql/stitching/resolver.rb +3 -3
- data/lib/graphql/stitching/supergraph.rb +5 -2
- data/lib/graphql/stitching/util.rb +1 -0
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +17 -1
- metadata +49 -3
@@ -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,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,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
|
File without changes
|
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?(
|
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 ||=
|
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
|
30
|
-
request =
|
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 =
|
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(", ")},
|
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
|
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 "
|
4
|
-
require_relative "
|
5
|
-
require_relative "
|
6
|
-
require_relative "
|
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
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
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
|
-
|
167
|
-
|
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,12 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "json"
|
4
|
-
require_relative "
|
5
|
-
require_relative "
|
6
|
-
require_relative "
|
4
|
+
require_relative "executor/resolver_source"
|
5
|
+
require_relative "executor/root_source"
|
6
|
+
require_relative "executor/shaper"
|
7
7
|
|
8
8
|
module GraphQL
|
9
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.
|
10
14
|
class Executor
|
11
15
|
# @return [Request] the stitching request to execute.
|
12
16
|
attr_reader :request
|
@@ -20,10 +24,14 @@ module GraphQL
|
|
20
24
|
# @return [Integer] tally of queries performed while executing.
|
21
25
|
attr_accessor :query_count
|
22
26
|
|
23
|
-
|
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)
|
24
31
|
@request = request
|
25
|
-
@data =
|
26
|
-
@errors =
|
32
|
+
@data = data
|
33
|
+
@errors = errors
|
34
|
+
@after = after
|
27
35
|
@query_count = 0
|
28
36
|
@exec_cycles = 0
|
29
37
|
@dataloader = GraphQL::Dataloader.new(nonblocking: nonblocking)
|
@@ -40,13 +48,13 @@ module GraphQL
|
|
40
48
|
if @errors.length > 0
|
41
49
|
result["errors"] = @errors
|
42
50
|
end
|
43
|
-
|
44
|
-
result
|
51
|
+
|
52
|
+
GraphQL::Query::Result.new(query: @request, values: result)
|
45
53
|
end
|
46
54
|
|
47
55
|
private
|
48
56
|
|
49
|
-
def exec!(next_steps = [
|
57
|
+
def exec!(next_steps = [@after])
|
50
58
|
if @exec_cycles > @request.plan.ops.length
|
51
59
|
# sanity check... if we've exceeded queue size, then something went wrong.
|
52
60
|
raise StitchingError, "Too many execution requests attempted."
|
@@ -58,8 +66,8 @@ module GraphQL
|
|
58
66
|
.select { next_steps.include?(_1.after) }
|
59
67
|
.group_by { [_1.location, _1.resolver.nil?] }
|
60
68
|
.map do |(location, root_source), ops|
|
61
|
-
|
62
|
-
@dataloader.with(
|
69
|
+
source_class = root_source ? RootSource : ResolverSource
|
70
|
+
@dataloader.with(source_class, self, location).request_all(ops)
|
63
71
|
end
|
64
72
|
|
65
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.
|
@@ -1,14 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "
|
3
|
+
require_relative "planner/step"
|
4
4
|
|
5
5
|
module GraphQL
|
6
6
|
module Stitching
|
7
|
+
# Planner partitions request selections by best-fit graph locations,
|
8
|
+
# and provides a query plan with sequential execution steps.
|
7
9
|
class Planner
|
8
10
|
SUPERGRAPH_LOCATIONS = [Supergraph::SUPERGRAPH_LOCATION].freeze
|
9
|
-
TYPENAME = "__typename"
|
10
|
-
QUERY_OP = "query"
|
11
|
-
MUTATION_OP = "mutation"
|
12
11
|
ROOT_INDEX = 0
|
13
12
|
|
14
13
|
def initialize(request)
|
@@ -126,6 +125,7 @@ module GraphQL
|
|
126
125
|
parent_index: ROOT_INDEX,
|
127
126
|
parent_type: parent_type,
|
128
127
|
selections: selections,
|
128
|
+
operation_type: QUERY_OP,
|
129
129
|
)
|
130
130
|
end
|
131
131
|
|
@@ -154,6 +154,22 @@ module GraphQL
|
|
154
154
|
).index
|
155
155
|
end
|
156
156
|
|
157
|
+
when SUBSCRIPTION_OP
|
158
|
+
parent_type = @supergraph.schema.subscription
|
159
|
+
|
160
|
+
each_field_in_scope(parent_type, @request.operation.selections) do |node|
|
161
|
+
raise StitchingError, "Too many root fields for subscription." unless @steps_by_entrypoint.empty?
|
162
|
+
|
163
|
+
locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name] || SUPERGRAPH_LOCATIONS
|
164
|
+
add_step(
|
165
|
+
location: locations.first,
|
166
|
+
parent_index: ROOT_INDEX,
|
167
|
+
parent_type: parent_type,
|
168
|
+
selections: [node],
|
169
|
+
operation_type: SUBSCRIPTION_OP,
|
170
|
+
)
|
171
|
+
end
|
172
|
+
|
157
173
|
else
|
158
174
|
raise StitchingError, "Invalid operation type."
|
159
175
|
end
|
@@ -281,7 +297,7 @@ module GraphQL
|
|
281
297
|
parent_selections.push(*resolver.key.export_nodes) if resolver.key
|
282
298
|
parent_selections.uniq! do |node|
|
283
299
|
export_node = node.is_a?(GraphQL::Language::Nodes::Field) && Resolver.export_key?(node.alias)
|
284
|
-
export_node ? node.alias : node
|
300
|
+
export_node ? node.alias : node.object_id
|
285
301
|
end
|
286
302
|
|
287
303
|
# E.2) Add a planner step for each new entrypoint location.
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module GraphQL
|
4
|
-
|
3
|
+
module GraphQL::Stitching
|
4
|
+
class Request
|
5
5
|
# Faster implementation of an AST visitor for prerendering
|
6
6
|
# @skip and @include conditional directives into a document.
|
7
7
|
# This avoids unnecessary planning steps, and prepares result shaping.
|