Skip to content

Commit

Permalink
general updates and refactors. (#151)
Browse files Browse the repository at this point in the history
  • Loading branch information
gmac authored Jul 20, 2024
1 parent 3d64888 commit a58ba20
Show file tree
Hide file tree
Showing 17 changed files with 80 additions and 52 deletions.
2 changes: 1 addition & 1 deletion docs/composer.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## GraphQL::Stitching::Composer

A `Composer` receives many individual `GraphQL:Schema` instances for various graph locations and _composes_ them into a combined [`Supergraph`](./supergraph.md) that is validated for integrity.
A `Composer` receives many individual `GraphQL::Schema` instances representing various graph locations and _composes_ them into one combined [`Supergraph`](./supergraph.md) that is validated for integrity.

### Configuring composition

Expand Down
6 changes: 5 additions & 1 deletion lib/graphql/stitching.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

module GraphQL
module Stitching
# @api private
EMPTY_OBJECT = {}.freeze

# @api private
EMPTY_ARRAY = [].freeze

class StitchingError < StandardError; end
Expand All @@ -18,6 +21,8 @@ def stitch_directive

attr_writer :stitch_directive

# Names of stitching directives to omit from the composed supergraph.
# @returns [Array<String>] list of stitching directive names.
def stitching_directive_names
[stitch_directive]
end
Expand All @@ -34,6 +39,5 @@ def stitching_directive_names
require_relative "stitching/planner"
require_relative "stitching/request"
require_relative "stitching/resolver"
require_relative "stitching/skip_include"
require_relative "stitching/util"
require_relative "stitching/version"
15 changes: 11 additions & 4 deletions lib/graphql/stitching/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,27 @@

module GraphQL
module Stitching
# Client is an out-of-the-box helper that assembles all
# stitching components into a workflow that executes requests.
class Client
class ClientError < StitchingError; end

# @return [Supergraph] composed supergraph that services incoming requests.
attr_reader :supergraph

# Builds a new client instance. Either `supergraph` or `locations` configuration is required.
# @param supergraph [Supergraph] optional, a pre-composed supergraph that bypasses composer setup.
# @param locations [Hash<Symbol, Hash<Symbol, untyped>>] optional, composer configurations for each graph location.
# @param composer [Composer] optional, a pre-configured composer instance for use with `locations` configuration.
def initialize(locations: nil, supergraph: nil, composer: nil)
@supergraph = if locations && supergraph
raise ClientError, "Cannot provide both locations and a supergraph."
elsif supergraph && !supergraph.is_a?(GraphQL::Stitching::Supergraph)
elsif supergraph && !supergraph.is_a?(Supergraph)
raise ClientError, "Provided supergraph must be a GraphQL::Stitching::Supergraph instance."
elsif supergraph
supergraph
else
composer ||= GraphQL::Stitching::Composer.new
composer ||= Composer.new
composer.perform(locations)
end

Expand All @@ -27,7 +34,7 @@ def initialize(locations: nil, supergraph: nil, composer: nil)
end

def execute(query:, variables: nil, operation_name: nil, context: nil, validate: true)
request = GraphQL::Stitching::Request.new(
request = Request.new(
@supergraph,
query,
operation_name: operation_name,
Expand Down Expand Up @@ -69,7 +76,7 @@ def on_error(&block)

def load_plan(request)
if @on_cache_read && plan_json = @on_cache_read.call(request)
plan = GraphQL::Stitching::Plan.from_json(JSON.parse(plan_json))
plan = Plan.from_json(JSON.parse(plan_json))

# only use plans referencing current resolver versions
if plan.ops.all? { |op| !op.resolver || @supergraph.resolvers_by_version[op.resolver] }
Expand Down
45 changes: 20 additions & 25 deletions lib/graphql/stitching/composer.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# frozen_string_literal: true

require_relative "./composer/base_validator"
require_relative "./composer/validate_interfaces"
require_relative "./composer/validate_resolvers"
require_relative "./composer/resolver_config"
require_relative "composer/base_validator"
require_relative "composer/validate_interfaces"
require_relative "composer/validate_resolvers"
require_relative "composer/resolver_config"

module GraphQL
module Stitching
# Composer receives many individual `GraphQL::Schema` instances
# representing various graph locations and merges them into one
# combined Supergraph that is validated for integrity.
class Composer
# @api private
NO_DEFAULT_VALUE = begin
Expand All @@ -26,9 +29,9 @@ class T < GraphQL::Schema::Object
BASIC_ROOT_FIELD_LOCATION_SELECTOR = ->(locations, _info) { locations.last }

# @api private
VALIDATORS = [
"ValidateInterfaces",
"ValidateResolvers",
COMPOSITION_VALIDATORS = [
ValidateInterfaces,
ValidateResolvers,
].freeze

# @return [String] name of the Query type in the composed schema.
Expand Down Expand Up @@ -59,18 +62,21 @@ def initialize(
@default_value_merger = default_value_merger || BASIC_VALUE_MERGER
@directive_kwarg_merger = directive_kwarg_merger || BASIC_VALUE_MERGER
@root_field_location_selector = root_field_location_selector || BASIC_ROOT_FIELD_LOCATION_SELECTOR

@field_map = {}
@resolver_map = {}
@resolver_configs = {}

@field_map = nil
@resolver_map = nil
@mapped_type_names = nil
@mapped_type_names = {}
@subgraph_directives_by_name_and_location = nil
@subgraph_types_by_name_and_location = nil
@schema_directives = nil
end

def perform(locations_input)
reset!
if @subgraph_types_by_name_and_location
raise CompositionError, "Composer may only perform once per instance."
end

schemas, executables = prepare_locations_input(locations_input)

# "directive_name" => "location" => subgraph_directive
Expand Down Expand Up @@ -163,9 +169,8 @@ def perform(locations_input)
executables: executables,
)

VALIDATORS.each do |validator|
klass = Object.const_get("GraphQL::Stitching::Composer::#{validator}")
klass.new.perform(supergraph, self)
COMPOSITION_VALIDATORS.each do |validator_class|
validator_class.new.perform(supergraph, self)
end

supergraph
Expand Down Expand Up @@ -660,16 +665,6 @@ def build_enum_usage_map(schemas)
memo[enum_name] << :write
end
end

private

def reset!
@field_map = {}
@resolver_map = {}
@mapped_type_names = {}
@subgraph_directives_by_name_and_location = nil
@schema_directives = nil
end
end
end
end
17 changes: 12 additions & 5 deletions lib/graphql/stitching/executor.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
# frozen_string_literal: true

require "json"
require_relative "./executor/resolver_source"
require_relative "./executor/root_source"
require_relative "./executor/shaper"
require_relative "executor/resolver_source"
require_relative "executor/root_source"
require_relative "executor/shaper"

module GraphQL
module Stitching
# Executor handles executing upon a planned request.
# All planned steps are initiated, their results merged,
# and loaded keys are collected for batching subsequent steps.
# Final execution results are then shaped to match the request selection.
class Executor
# @return [Request] the stitching request to execute.
attr_reader :request
Expand All @@ -20,6 +24,9 @@ class Executor
# @return [Integer] tally of queries performed while executing.
attr_accessor :query_count

# Builds a new executor.
# @param request [Request] the stitching request to execute.
# @param nonblocking [Boolean] specifies if the dataloader should use async concurrency.
def initialize(request, nonblocking: false)
@request = request
@data = {}
Expand Down Expand Up @@ -58,8 +65,8 @@ def exec!(next_steps = [0])
.select { next_steps.include?(_1.after) }
.group_by { [_1.location, _1.resolver.nil?] }
.map do |(location, root_source), ops|
source_type = root_source ? RootSource : ResolverSource
@dataloader.with(source_type, self, location).request_all(ops)
source_class = root_source ? RootSource : ResolverSource
@dataloader.with(source_class, self, location).request_all(ops)
end

tasks.each(&method(:exec_task))
Expand Down
3 changes: 3 additions & 0 deletions lib/graphql/stitching/http_executable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

module GraphQL
module Stitching
# HttpExecutable provides an out-of-the-box convenience for sending
# HTTP post requests to a remote location, or a base class
# for other implementations with GraphQL multipart uploads.
class HttpExecutable
# Builds a new executable for proxying subgraph requests via HTTP.
# @param url [String] the url of the remote location to proxy.
Expand Down
2 changes: 1 addition & 1 deletion lib/graphql/stitching/plan.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module GraphQL
module Stitching
# Immutable (in theory) structures representing a query plan.
# Immutable-ish structures representing a query plan.
# May serialize to/from JSON.
class Plan
Op = Struct.new(
Expand Down
6 changes: 4 additions & 2 deletions lib/graphql/stitching/planner.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# frozen_string_literal: true

require_relative "./planner/step"
require_relative "planner/step"

module GraphQL
module Stitching
# Planner partitions request selections by best-fit graph locations,
# and provides a query plan with sequential execution steps.
class Planner
SUPERGRAPH_LOCATIONS = [Supergraph::SUPERGRAPH_LOCATION].freeze
TYPENAME = "__typename"
Expand Down Expand Up @@ -281,7 +283,7 @@ def extract_locale_selections(
parent_selections.push(*resolver.key.export_nodes) if resolver.key
parent_selections.uniq! do |node|
export_node = node.is_a?(GraphQL::Language::Nodes::Field) && Resolver.export_key?(node.alias)
export_node ? node.alias : node
export_node ? node.alias : node.object_id
end

# E.2) Add a planner step for each new entrypoint location.
Expand Down
10 changes: 8 additions & 2 deletions lib/graphql/stitching/request.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
# frozen_string_literal: true

require_relative "request/skip_include"

module GraphQL
module Stitching
# Request combines a supergraph, GraphQL document, variables,
# variable/fragment definitions, and the selected operation.
# It provides the lifecycle of validating, preparing,
# planning, and executing upon these inputs.
class Request
SUPPORTED_OPERATIONS = ["query", "mutation"].freeze
SKIP_INCLUDE_DIRECTIVE = /@(?:skip|include)/
Expand Down Expand Up @@ -162,15 +168,15 @@ def plan(new_plan = nil)
raise StitchingError, "Plan must be a `GraphQL::Stitching::Plan`." unless new_plan.is_a?(Plan)
@plan = new_plan
else
@plan ||= GraphQL::Stitching::Planner.new(self).perform
@plan ||= Planner.new(self).perform
end
end

# Executes the request and returns the rendered response.
# @param raw [Boolean] specifies the result should be unshaped without pruning or null bubbling. Useful for debugging.
# @return [Hash] the rendered GraphQL response with "data" and "errors" sections.
def execute(raw: false)
GraphQL::Stitching::Executor.new(self).perform(raw: raw)
Executor.new(self).perform(raw: raw)
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

module GraphQL
module Stitching
module GraphQL::Stitching
class Request
# Faster implementation of an AST visitor for prerendering
# @skip and @include conditional directives into a document.
# This avoids unnecessary planning steps, and prepares result shaping.
Expand Down
6 changes: 3 additions & 3 deletions lib/graphql/stitching/resolver.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# frozen_string_literal: true

require_relative "./resolver/arguments"
require_relative "./resolver/keys"
require_relative "resolver/arguments"
require_relative "resolver/keys"

module GraphQL
module Stitching
# Defines a root resolver query that provides direct access to an entity type.
# Defines a type resolver query that provides direct access to an entity type.
class Resolver
extend ArgumentsParser
extend KeysParser
Expand Down
5 changes: 4 additions & 1 deletion lib/graphql/stitching/supergraph.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# frozen_string_literal: true

require_relative "./supergraph/to_definition"
require_relative "supergraph/to_definition"

module GraphQL
module Stitching
# Supergraph is the singuar representation of a stitched graph.
# It provides the combined GraphQL schema and delegation maps
# used to route selections across subgraph locations.
class Supergraph
SUPERGRAPH_LOCATION = "__super"

Expand Down
1 change: 1 addition & 0 deletions lib/graphql/stitching/util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

module GraphQL
module Stitching
# General utilities to aid with stitching.
class Util
TypeStructure = Struct.new(:list, :null, :name, keyword_init: true) do
alias_method :list?, :list
Expand Down
2 changes: 1 addition & 1 deletion test/graphql/stitching/executor/shaper_grooming_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
require "test_helper"
require_relative "../../../schemas/introspection"

describe "GraphQL::Stitching::Executor::GraphQL::Stitching::Executor::Shaper, grooming" do
describe "GraphQL::Stitching::Executor::Shaper, grooming" do
def test_prunes_stitching_fields
schema_sdl = "type Test { req: String! opt: String } type Query { test: Test }"
request = GraphQL::Stitching::Request.new(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

require "test_helper"

describe "GraphQL::Stitching::Executor::GraphQL::Stitching::Executor::Shaper, null bubbling" do
describe "GraphQL::Stitching::Executor::Shaper, null bubbling" do
def test_basic_object_structure
schema_sdl = "type Test { req: String! opt: String } type Query { test: Test }"
request = GraphQL::Stitching::Request.new(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

require "test_helper"
require_relative "../../schemas/example"
require_relative "../../../schemas/example"

describe "GraphQL::Stitching::Request" do
def setup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

require "test_helper"

describe "GraphQL::Stitching::SkipInclude" do
describe "GraphQL::Stitching::Request::SkipInclude" do
def test_omits_statically_skipped_nodes
render_skip_include "query {
a {
Expand Down Expand Up @@ -106,7 +106,7 @@ def test_lacking_conditionals_produces_no_changes
def render_skip_include(source, variables = {})
@source = source
@changed = false
@result = GraphQL::Stitching::SkipInclude.render(GraphQL.parse(@source), variables) do
@result = GraphQL::Stitching::Request::SkipInclude.render(GraphQL.parse(@source), variables) do
@changed = true
end
end
Expand Down

0 comments on commit a58ba20

Please sign in to comment.