Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use GraphQL::Query as request basis #167

Merged
merged 1 commit into from
Mar 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions docs/request.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ request = GraphQL::Stitching::Request.new(

A `Request` provides the following information:

- `req.document`: parsed AST of the GraphQL source.
- `req.variables`: a hash of user-submitted variables.
- `req.string`: the original GraphQL source string, or printed document.
- `req.digest`: a digest of the request string, hashed by the `Stitching.digest` implementation.
Expand All @@ -31,6 +30,5 @@ A request manages the flow of stitching behaviors. These are sequenced by the `C
component, or you may invoke them manually:

1. `request.validate`: runs static validations on the request using the combined schema.
2. `request.prepare!`: inserts variable defaults and pre-renders skip/include conditional shaping.
3. `request.plan`: builds a plan for the request. May act as a setter for plans pulled from cache.
4. `request.execute`: executes the request, and returns the resulting data.
2. `request.plan`: builds a plan for the request. May act as a setter for plans pulled from cache.
3. `request.execute`: executes the request, and returns the resulting data.
1 change: 0 additions & 1 deletion lib/graphql/stitching/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ def execute(raw_query = nil, query: nil, variables: nil, operation_name: nil, co
return error_result(request, validation_errors) if validation_errors.any?
end

request.prepare!
load_plan(request)
request.execute
rescue GraphQL::ParseError, GraphQL::ExecutionError => e
Expand Down
4 changes: 2 additions & 2 deletions lib/graphql/stitching/executor/shaper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def initialize(request)
end

def perform!(raw)
@root_type = @supergraph.schema.root_type_for_operation(@request.operation.operation_type)
@root_type = @request.query.root_type_for_operation(@request.operation.operation_type)
resolve_object_scope(raw, @root_type, @request.operation.selections, @root_type.graphql_name)
end

Expand Down Expand Up @@ -118,7 +118,7 @@ def introspection_field?(parent_type, node)
def typename_in_type?(typename, type)
return true if type.graphql_name == typename

type.kind.abstract? && @supergraph.schema.possible_types(type).any? do |t|
type.kind.abstract? && @request.query.possible_types(type).any? do |t|
t.graphql_name == typename
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/graphql/stitching/http_executable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def extract_ast_node(ast_node, value, files_by_path, path, request)
elsif @upload_types.include?(ast_node.name)
files_by_path[path.dup] = value
else
type_def = request.supergraph.schema.get_type(ast_node.name)
type_def = request.query.get_type(ast_node.name)
extract_type_node(type_def, value, files_by_path, path) if type_def&.kind&.input_object?
end
end
Expand Down
4 changes: 2 additions & 2 deletions lib/graphql/stitching/planner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def add_step(

# A) Group all root selections by their preferred entrypoint locations.
def build_root_entrypoints
parent_type = @supergraph.schema.root_type_for_operation(@request.operation.operation_type)
parent_type = @request.query.root_type_for_operation(@request.operation.operation_type)

case @request.operation.operation_type
when QUERY_OP
Expand Down Expand Up @@ -331,7 +331,7 @@ def expand_interface_selections(current_location, parent_type, input_selections)
end

if expanded_selections
@supergraph.schema.possible_types(parent_type).each do |possible_type|
@request.query.possible_types(parent_type).each do |possible_type|
next unless @supergraph.locations_by_type[possible_type.graphql_name].include?(current_location)

type_name = GraphQL::Language::Nodes::TypeName.new(name: possible_type.graphql_name)
Expand Down
123 changes: 64 additions & 59 deletions lib/graphql/stitching/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,21 @@ class Request
# @return [Supergraph] supergraph instance that resolves the request.
attr_reader :supergraph

# @return [GraphQL::Language::Nodes::Document] the parsed GraphQL AST document.
attr_reader :document

# @return [Hash] input variables for the request.
attr_reader :variables

# @return [String] operation name selected for the request.
attr_reader :operation_name
# @return [GraphQL::Query] query object defining the request.
attr_reader :query

# @return [Hash] contextual object passed through resolver flows.
attr_reader :context

# Creates a new supergraph request.
# @param supergraph [Supergraph] supergraph instance that resolves the request.
# @param document [String, GraphQL::Language::Nodes::Document] the request string or parsed AST.
# @param source [String, GraphQL::Language::Nodes::Document] the request string or parsed AST.
# @param operation_name [String, nil] operation name selected for the request.
# @param variables [Hash, nil] input variables for the request.
# @param context [Hash, nil] a contextual object passed through resolver flows.
def initialize(supergraph, document, operation_name: nil, variables: nil, context: nil)
def initialize(supergraph, source, operation_name: nil, variables: nil, context: nil)
@supergraph = supergraph
@prepared_document = nil
@string = nil
@digest = nil
@normalized_string = nil
Expand All @@ -44,29 +39,32 @@ def initialize(supergraph, document, operation_name: nil, variables: nil, contex
@fragment_definitions = nil
@plan = nil

@document = if document.is_a?(String)
@string = document
GraphQL.parse(document)
params = {
operation_name: operation_name,
variables: variables,
context: context,
}

if source.is_a?(String)
@string = source
params[:query] = source
else
document
params[:document] = source
end

@operation_name = operation_name
@variables = variables || {}

@query = GraphQL::Query.new(@supergraph.schema, document: @document, context: context)
@query = GraphQL::Query.new(@supergraph.schema, **params)
@context = @query.context
@context[:request] = self
end

# @return [String] the original document string, or a print of the parsed AST document.
def string
@string || normalized_string
with_prepared_document { @string || normalized_string }
end

# @return [String] a print of the parsed AST document with consistent whitespace.
def normalized_string
@normalized_string ||= @document.to_query_string
@normalized_string ||= prepared_document.to_query_string
end

# @return [String] a digest of the original document string. Generally faster but less consistent.
Expand All @@ -81,43 +79,48 @@ def normalized_digest

# @return [GraphQL::Language::Nodes::OperationDefinition] The selected root operation for the request.
def operation
@operation ||= begin
operation_defs = @document.definitions.select do |d|
@operation ||= with_prepared_document do
selected_op = @query.selected_operation
raise GraphQL::ExecutionError, "No operation selected" unless selected_op

@prepared_document.definitions.find do |d|
next unless d.is_a?(GraphQL::Language::Nodes::OperationDefinition)
@operation_name ? d.name == @operation_name : true
end

if operation_defs.length < 1
raise GraphQL::ExecutionError, "Invalid root operation for given name and operation type."
elsif operation_defs.length > 1
raise GraphQL::ExecutionError, "An operation name is required when sending multiple operations."
selected_op.name.nil? || d.name == selected_op.name
end
end
end

operation_defs.first
def operation_name
operation.name
end

# @return [String] A string of directives applied to the root operation. These are passed through in all subgraph requests.
def operation_directives
@operation_directives ||= if operation.directives.any?
printer = GraphQL::Language::Printer.new
operation.directives.map { printer.print(_1) }.join(" ")
end
end

# @return [Boolean] true if operation type is a query
def query?
operation.operation_type == QUERY_OP
@query.query?
end

# @return [Boolean] true if operation type is a mutation
def mutation?
operation.operation_type == MUTATION_OP
@query.mutation?
end

# @return [Boolean] true if operation type is a subscription
def subscription?
operation.operation_type == SUBSCRIPTION_OP
@query.subscription?
end

# @return [String] A string of directives applied to the root operation. These are passed through in all subgraph requests.
def operation_directives
@operation_directives ||= if operation.directives.any?
printer = GraphQL::Language::Printer.new
operation.directives.map { printer.print(_1) }.join(" ")
end
# @return [Hash<String, Any>] provided variables hash filled in with default values from definitions
def variables
@variables || with_prepared_document { @variables }
end

# @return [Hash<String, GraphQL::Language::Nodes::AbstractNode>] map of variable names to AST type definitions.
Expand All @@ -129,34 +132,15 @@ def variable_definitions

# @return [Hash<String, GraphQL::Language::Nodes::FragmentDefinition>] map of fragment names to their AST definitions.
def fragment_definitions
@fragment_definitions ||= @document.definitions.each_with_object({}) do |d, memo|
@fragment_definitions ||= prepared_document.definitions.each_with_object({}) do |d, memo|
memo[d.name] = d if d.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
end
end

# Validates the request using the combined supergraph schema.
# @return [Array<GraphQL::ExecutionError>] an array of static validation errors
def validate
result = @supergraph.schema.static_validator.validate(@query)
result[:errors]
end

# Prepares the request for stitching by inserting variable defaults and applying @skip/@include conditionals.
def prepare!
operation.variables.each do |v|
@variables[v.name] = v.default_value if @variables[v.name].nil? && !v.default_value.nil?
end

if @string.nil? || @string.match?(SKIP_INCLUDE_DIRECTIVE)
SkipInclude.render(@document, @variables) do |modified_ast|
@document = modified_ast
@string = @normalized_string = nil
@digest = @normalized_digest = nil
@operation = @operation_directives = @variable_definitions = @plan = nil
end
end

self
@query.static_errors
end

# Gets and sets the query plan for the request. Assigned query plans may pull from a cache,
Expand Down Expand Up @@ -191,6 +175,27 @@ def execute(raw: false)

private

# Prepares the request for stitching by applying @skip/@include conditionals.
def prepared_document
@prepared_document || with_prepared_document { @prepared_document }
end

def with_prepared_document
unless @prepared_document
@variables = @query.variables.to_h

@prepared_document = if @string.nil? || @string.match?(SKIP_INCLUDE_DIRECTIVE)
changed = false
doc = SkipInclude.render(@query.document, @variables) { changed = true }
@string = @normalized_string = doc.to_query_string if changed
doc
else
@query.document
end
end
yield
end

# Adds a handler into context for enriching subscription updates with stitched data
def add_subscription_update_handler
request = self
Expand Down
4 changes: 2 additions & 2 deletions test/graphql/stitching/client_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ def test_returns_error_for_required_operation_name
result = @client.execute(queries)

expected_errors = [
{ "message" => "An operation name is required when sending multiple operations." },
{ "message" => "An operation name is required" },
]
assert_equal expected_errors, result["errors"]
end
Expand All @@ -197,7 +197,7 @@ def test_returns_error_for_operation_name_not_found
result = @client.execute(queries, operation_name: "Sfoo")

expected_errors = [
{ "message" => "Invalid root operation for given name and operation type." },
{ "message" => 'No operation named "Sfoo"' },
]
assert_equal expected_errors, result["errors"]
end
Expand Down
4 changes: 2 additions & 2 deletions test/graphql/stitching/composer/merge_interface_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def test_merges_interface_fields

supergraph = compose_definitions({ "a" => a, "b" => b })

assert_equal ["code", "id", "name"], supergraph.schema.types["I"].fields.keys.sort
assert_equal ["code", "id", "name"], supergraph.schema.types["T"].fields.keys.sort
assert_equal ["code", "id", "name"], supergraph.schema.get_type("I").fields.keys.sort
assert_equal ["code", "id", "name"], supergraph.schema.get_type("T").fields.keys.sort
end
end
4 changes: 2 additions & 2 deletions test/graphql/stitching/composer/merge_scalar_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def test_merges_scalar_descriptions
description_merger: ->(str_by_location, _info) { str_by_location.values.join("/") }
})

assert_equal "a/b", info.schema.types["URL"].description
assert_equal "a/b", info.schema.get_type("URL").description
end

def test_merges_scalar_directives
Expand All @@ -32,6 +32,6 @@ def test_merges_scalar_directives
directive_kwarg_merger: ->(str_by_location, _info) { str_by_location.values.join("/") }
})

assert_equal "a/b", supergraph.schema.types["Thing"].directives.first.arguments.keyword_arguments[:arg]
assert_equal "a/b", supergraph.schema.get_type("Thing").directives.first.arguments.keyword_arguments[:arg]
end
end
6 changes: 3 additions & 3 deletions test/graphql/stitching/composer/merge_union_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def test_merges_union_types

info = compose_definitions({ "a" => a, "b" => b })

assert_equal ["A", "B", "C"], info.schema.types["Thing"].possible_types.map(&:graphql_name).sort
assert_equal ["A", "B", "C"], info.schema.get_type("Thing").possible_types.map(&:graphql_name).sort
end

def test_merges_union_descriptions
Expand All @@ -21,7 +21,7 @@ def test_merges_union_descriptions
description_merger: ->(str_by_location, _info) { str_by_location.values.join("/") }
})

assert_equal "a/b", info.schema.types["Thing"].description
assert_equal "a/b", info.schema.get_type("Thing").description
end

def test_merges_union_directives
Expand All @@ -43,6 +43,6 @@ def test_merges_union_directives
directive_kwarg_merger: ->(str_by_location, _info) { str_by_location.values.join("/") }
})

assert_equal "a/b", supergraph.schema.types["Thing"].directives.first.arguments.keyword_arguments[:arg]
assert_equal "a/b", supergraph.schema.get_type("Thing").directives.first.arguments.keyword_arguments[:arg]
end
end
2 changes: 1 addition & 1 deletion test/graphql/stitching/executor/executor_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def mock_execs(source, returns, operation_name: nil, variables: nil)
source,
operation_name: operation_name,
variables: variables,
).prepare!.execute
).execute

results
end
Expand Down
Loading
Loading