# frozen_string_literal: true
require "graphql/schema/base_64_encoder"
require "graphql/schema/catchall_middleware"
require "graphql/schema/default_parse_error"
require "graphql/schema/default_type_error"
require "graphql/schema/find_inherited_value"
require "graphql/schema/finder"
require "graphql/schema/invalid_type_error"
require "graphql/schema/introspection_system"
require "graphql/schema/late_bound_type"
require "graphql/schema/middleware_chain"
require "graphql/schema/null_mask"
require "graphql/schema/possible_types"
require "graphql/schema/rescue_middleware"
require "graphql/schema/timeout"
require "graphql/schema/timeout_middleware"
require "graphql/schema/traversal"
require "graphql/schema/type_expression"
require "graphql/schema/unique_within_type"
require "graphql/schema/validation"
require "graphql/schema/warden"
require "graphql/schema/build_from_definition"
require "graphql/schema/member"
require "graphql/schema/wrapper"
require "graphql/schema/list"
require "graphql/schema/non_null"
require "graphql/schema/argument"
require "graphql/schema/enum_value"
require "graphql/schema/enum"
require "graphql/schema/field_extension"
require "graphql/schema/field"
require "graphql/schema/input_object"
require "graphql/schema/interface"
require "graphql/schema/scalar"
require "graphql/schema/object"
require "graphql/schema/union"
require "graphql/schema/directive"
require "graphql/schema/directive/deprecated"
require "graphql/schema/directive/include"
require "graphql/schema/directive/skip"
require "graphql/schema/directive/feature"
require "graphql/schema/directive/transform"
require "graphql/schema/type_membership"
require "graphql/schema/resolver"
require "graphql/schema/mutation"
require "graphql/schema/relay_classic_mutation"
require "graphql/schema/subscription"
module GraphQL
# A GraphQL schema which may be queried with {GraphQL::Query}.
#
# The {Schema} contains:
#
# - types for exposing your application
# - query analyzers for assessing incoming queries (including max depth & max complexity restrictions)
# - execution strategies for running incoming queries
#
# Schemas start with root types, {Schema#query}, {Schema#mutation} and {Schema#subscription}.
# The schema will traverse the tree of fields & types, using those as starting points.
# Any undiscoverable types may be provided with the `types` configuration.
#
# Schemas can restrict large incoming queries with `max_depth` and `max_complexity` configurations.
# (These configurations can be overridden by specific calls to {Schema#execute})
#
# Schemas can specify how queries should be executed against them.
# `query_execution_strategy`, `mutation_execution_strategy` and `subscription_execution_strategy`
# each apply to corresponding root types.
# #
# @example defining a schema
# class MySchema < GraphQL::Schema
# query QueryType
# # If types are only connected by way of interfaces, they must be added here
# orphan_types ImageType, AudioType
# end
#
class Schema
extend Forwardable
extend GraphQL::Schema::Member::AcceptsDefinition
extend GraphQL::Schema::Member::HasAstNode
include GraphQL::Define::InstanceDefinable
extend GraphQL::Schema::FindInheritedValue
class UnresolvedLateBoundTypeError < GraphQL::Error
attr_reader :type
def initialize(type:)
@type = type
super("Late bound type was never found: #{type.inspect}")
end
end
accepts_definitions \
:query_execution_strategy, :mutation_execution_strategy, :subscription_execution_strategy,
:max_depth, :max_complexity, :default_max_page_size,
:orphan_types, :resolve_type, :type_error, :parse_error,
:error_bubbling,
:raise_definition_error,
:object_from_id, :id_from_object,
:default_mask,
:cursor_encoder,
# If these are given as classes, normalize them. Accept `nil` when building from string.
query: ->(schema, t) { schema.query = t.respond_to?(:graphql_definition) ? t.graphql_definition : t },
mutation: ->(schema, t) { schema.mutation = t.respond_to?(:graphql_definition) ? t.graphql_definition : t },
subscription: ->(schema, t) { schema.subscription = t.respond_to?(:graphql_definition) ? t.graphql_definition : t },
disable_introspection_entry_points: ->(schema) { schema.disable_introspection_entry_points = true },
disable_schema_introspection_entry_point: ->(schema) { schema.disable_schema_introspection_entry_point = true },
disable_type_introspection_entry_point: ->(schema) { schema.disable_type_introspection_entry_point = true },
directives: ->(schema, directives) { schema.directives = directives.reduce({}) { |m, d| m[d.graphql_name] = d; m } },
directive: ->(schema, directive) { schema.directives[directive.graphql_name] = directive },
instrument: ->(schema, type, instrumenter, after_built_ins: false) {
if type == :field && after_built_ins
type = :field_after_built_ins
end
schema.instrumenters[type] << instrumenter
},
query_analyzer: ->(schema, analyzer) {
if analyzer == GraphQL::Authorization::Analyzer
warn("The Authorization query analyzer is deprecated. Authorizing at query runtime is generally a better idea.")
end
schema.query_analyzers << analyzer
},
multiplex_analyzer: ->(schema, analyzer) { schema.multiplex_analyzers << analyzer },
middleware: ->(schema, middleware) { schema.middleware << middleware },
lazy_resolve: ->(schema, lazy_class, lazy_value_method) { schema.lazy_methods.set(lazy_class, lazy_value_method) },
rescue_from: ->(schema, err_class, &block) { schema.rescue_from(err_class, &block) },
tracer: ->(schema, tracer) { schema.tracers.push(tracer) }
ensure_defined :introspection_system
attr_accessor \
:query, :mutation, :subscription,
:query_execution_strategy, :mutation_execution_strategy, :subscription_execution_strategy,
:max_depth, :max_complexity, :default_max_page_size,
:orphan_types, :directives,
:query_analyzers, :multiplex_analyzers, :instrumenters, :lazy_methods,
:cursor_encoder,
:ast_node,
:raise_definition_error,
:introspection_namespace,
:analysis_engine
# [Boolean] True if this object bubbles validation errors up from a field into its parent InputObject, if there is one.
attr_accessor :error_bubbling
# Single, long-lived instance of the provided subscriptions class, if there is one.
# @return [GraphQL::Subscriptions]
attr_accessor :subscriptions
# @return [MiddlewareChain] MiddlewareChain which is applied to fields during execution
attr_accessor :middleware
# @return [<#call(member, ctx)>] A callable for filtering members of the schema
# @see {Query.new} for query-specific filters with `except:`
attr_accessor :default_mask
# @see {GraphQL::Query::Context} The parent class of these classes
# @return [Class] Instantiated for each query
attr_accessor :context_class
# [Boolean] True if this object disables the introspection entry point fields
attr_accessor :disable_introspection_entry_points
def disable_introspection_entry_points?
!!@disable_introspection_entry_points
end
# [Boolean] True if this object disables the __schema introspection entry point field
attr_accessor :disable_schema_introspection_entry_point
def disable_schema_introspection_entry_point?
!!@disable_schema_introspection_entry_point
end
# [Boolean] True if this object disables the __type introspection entry point field
attr_accessor :disable_type_introspection_entry_point
def disable_type_introspection_entry_point?
!!@disable_type_introspection_entry_point
end
class << self
attr_writer :default_execution_strategy
end
def default_filter
GraphQL::Filter.new(except: default_mask)
end
# @return [Array<#trace(key, data)>] Tracers applied to every query
# @see {Query#tracers} for query-specific tracers
attr_reader :tracers
DYNAMIC_FIELDS = ["__type", "__typename", "__schema"].freeze
attr_reader :static_validator, :object_from_id_proc, :id_from_object_proc, :resolve_type_proc
def initialize
@tracers = []
@definition_error = nil
@orphan_types = []
@directives = {}
self.class.default_directives.each do |name, dir|
@directives[name] = dir.graphql_definition
end
@static_validator = GraphQL::StaticValidation::Validator.new(schema: self)
@middleware = MiddlewareChain.new(final_step: GraphQL::Execution::Execute::FieldResolveStep)
@query_analyzers = []
@multiplex_analyzers = []
@resolve_type_proc = nil
@object_from_id_proc = nil
@id_from_object_proc = nil
@type_error_proc = DefaultTypeError
@parse_error_proc = DefaultParseError
@instrumenters = Hash.new { |h, k| h[k] = [] }
@lazy_methods = GraphQL::Execution::Lazy::LazyMethodMap.new
@lazy_methods.set(GraphQL::Execution::Lazy, :value)
@cursor_encoder = Base64Encoder
# Default to the built-in execution strategy:
@analysis_engine = GraphQL::Analysis
@query_execution_strategy = self.class.default_execution_strategy
@mutation_execution_strategy = self.class.default_execution_strategy
@subscription_execution_strategy = self.class.default_execution_strategy
@default_mask = GraphQL::Schema::NullMask
@rebuilding_artifacts = false
@context_class = GraphQL::Query::Context
@introspection_namespace = nil
@introspection_system = nil
@interpreter = false
@error_bubbling = false
@disable_introspection_entry_points = false
@disable_schema_introspection_entry_point = false
@disable_type_introspection_entry_point = false
end
# @return [Boolean] True if using the new {GraphQL::Execution::Interpreter}
def interpreter?
@interpreter
end
# @api private
attr_writer :interpreter
def inspect
"#<#{self.class.name} ...>"
end
def initialize_copy(other)
super
@orphan_types = other.orphan_types.dup
@directives = other.directives.dup
@static_validator = GraphQL::StaticValidation::Validator.new(schema: self)
@middleware = other.middleware.dup
@query_analyzers = other.query_analyzers.dup
@multiplex_analyzers = other.multiplex_analyzers.dup
@tracers = other.tracers.dup
@possible_types = GraphQL::Schema::PossibleTypes.new(self)
@lazy_methods = other.lazy_methods.dup
@instrumenters = Hash.new { |h, k| h[k] = [] }
other.instrumenters.each do |key, insts|
@instrumenters[key].concat(insts)
end
if other.rescues?
@rescue_middleware = other.rescue_middleware
end
# This will be rebuilt when it's requested
# or during a later `define` call
@types = nil
@introspection_system = nil
end
def rescue_from(*args, &block)
rescue_middleware.rescue_from(*args, &block)
end
def remove_handler(*args, &block)
rescue_middleware.remove_handler(*args, &block)
end
def using_ast_analysis?
@analysis_engine == GraphQL::Analysis::AST
end
# For forwards-compatibility with Schema classes
alias :graphql_definition :itself
# Validate a query string according to this schema.
# @param string_or_document [String, GraphQL::Language::Nodes::Document]
# @return [Array<GraphQL::StaticValidation::Error >]
def validate(string_or_document, rules: nil, context: nil)
doc = if string_or_document.is_a?(String)
GraphQL.parse(string_or_document)
else
string_or_document
end
query = GraphQL::Query.new(self, document: doc, context: context)
validator_opts = { schema: self }
rules && (validator_opts[:rules] = rules)
validator = GraphQL::StaticValidation::Validator.new(**validator_opts)
res = validator.validate(query)
res[:errors]
end
def define(**kwargs, &block)
super
ensure_defined
# Assert that all necessary configs are present:
validation_error = Validation.validate(self)
validation_error && raise(GraphQL::RequiredImplementationMissingError, validation_error)
rebuild_artifacts
@definition_error = nil
nil
rescue StandardError => err
if @raise_definition_error || err.is_a?(CyclicalDefinitionError) || err.is_a?(GraphQL::RequiredImplementationMissingError)
raise
else
# Raise this error _later_ to avoid messing with Rails constant loading
@definition_error = err
end
nil
end
# Attach `instrumenter` to this schema for instrumenting events of `instrumentation_type`.
# @param instrumentation_type [Symbol]
# @param instrumenter
# @return [void]
def instrument(instrumentation_type, instrumenter)
@instrumenters[instrumentation_type] << instrumenter
if instrumentation_type == :field
rebuild_artifacts
end
end
# @return [Array<GraphQL::BaseType>] The root types of this schema
def root_types
@root_types ||= begin
rebuild_artifacts
@root_types
end
end
# @see [GraphQL::Schema::Warden] Restricted access to members of a schema
# @return [GraphQL::Schema::TypeMap] `{ name => type }` pairs of types in this schema
def types
@types ||= begin
rebuild_artifacts
@types
end
end
def get_type(type_name)
@types[type_name]
end
# @api private
def introspection_system
@introspection_system ||= begin
rebuild_artifacts
@introspection_system
end
end
# Returns a list of Arguments and Fields referencing a certain type
# @param type_name [String]
# @return [Hash]
def references_to(type_name = nil)
rebuild_artifacts unless defined?(@type_reference_map)
if type_name
@type_reference_map.fetch(type_name, [])
else
@type_reference_map
end
end
# Returns a list of Union types in which a type is a member
# @param type [GraphQL::ObjectType]
# @return [Array<GraphQL::UnionType>] list of union types of which the type is a member
def union_memberships(type)
rebuild_artifacts unless defined?(@union_memberships)
@union_memberships.fetch(type.name, [])
end
# Execute a query on itself. Raises an error if the schema definition is invalid.
# @see {Query#initialize} for arguments.
# @return [Hash] query result, ready to be serialized as JSON
def execute(query_str = nil, **kwargs)
if query_str
kwargs[:query] = query_str
end
# Some of the query context _should_ be passed to the multiplex, too
multiplex_context = if (ctx = kwargs[:context])
{
backtrace: ctx[:backtrace],
tracers: ctx[:tracers],
}
else
{}
end
# Since we're running one query, don't run a multiplex-level complexity analyzer
all_results = multiplex([kwargs], max_complexity: nil, context: multiplex_context)
all_results[0]
end
# Execute several queries on itself. Raises an error if the schema definition is invalid.
# @example Run several queries at once
# context = { ... }
# queries = [
# { query: params[:query_1], variables: params[:variables_1], context: context },
# { query: params[:query_2], variables: params[:variables_2], context: context },
# ]
# results = MySchema.multiplex(queries)
# render json: {
# result_1: results[0],
# result_2: results[1],
# }
#
# @see {Query#initialize} for query keyword arguments
# @see {Execution::Multiplex#run_queries} for multiplex keyword arguments
# @param queries [Array<Hash>] Keyword arguments for each query
# @param context [Hash] Multiplex-level context
# @return [Array<Hash>] One result for each query in the input
def multiplex(queries, **kwargs)
with_definition_error_check {
GraphQL::Execution::Multiplex.run_all(self, queries, **kwargs)
}
end
# Search for a schema member using a string path
# @example Finding a Field
# Schema.find("Ensemble.musicians")
#
# @see {GraphQL::Schema::Finder} for more examples
# @param path [String] A dot-separated path to the member
# @raise [Schema::Finder::MemberNotFoundError] if path could not be found
# @return [GraphQL::BaseType, GraphQL::Field, GraphQL::Argument, GraphQL::Directive] A GraphQL Schema Member
def find(path)
rebuild_artifacts unless defined?(@finder)
@find_cache[path] ||= @finder.find(path)
end
# Resolve field named `field_name` for type `parent_type`.
# Handles dynamic fields `__typename`, `__type` and `__schema`, too
# @param parent_type [String, GraphQL::BaseType]
# @param field_name [String]
# @return [GraphQL::Field, nil] The field named `field_name` on `parent_type`
# @see [GraphQL::Schema::Warden] Restricted access to members of a schema
def get_field(parent_type, field_name)
with_definition_error_check do
parent_type_name = case parent_type
when GraphQL::BaseType, Class, Module
parent_type.graphql_name
when String
parent_type
else
raise "Unexpected parent_type: #{parent_type}"
end
defined_field = @instrumented_field_map[parent_type_name][field_name]
if defined_field
defined_field
elsif parent_type == query && (entry_point_field = introspection_system.entry_point(name: field_name))
entry_point_field
elsif (dynamic_field = introspection_system.dynamic_field(name: field_name))
dynamic_field
else
nil
end
end
end
# Fields for this type, after instrumentation is applied
# @return [Hash<String, GraphQL::Field>]
def get_fields(type)
@instrumented_field_map[type.graphql_name]
end
def type_from_ast(ast_node, context:)
GraphQL::Schema::TypeExpression.build_type(self, ast_node)
end
# @see [GraphQL::Schema::Warden] Restricted access to members of a schema
# @param type_defn [GraphQL::InterfaceType, GraphQL::UnionType] the type whose members you want to retrieve
# @param context [GraphQL::Query::Context] The context for the current query
# @return [Array<GraphQL::ObjectType>] types which belong to `type_defn` in this schema
def possible_types(type_defn, context = GraphQL::Query::NullContext)
@possible_types ||= GraphQL::Schema::PossibleTypes.new(self)
@possible_types.possible_types(type_defn, context)
end
# @see [GraphQL::Schema::Warden] Resticted access to root types
# @return [GraphQL::ObjectType, nil]
def root_type_for_operation(operation)
case operation
when "query"
query
when "mutation"
mutation
when "subscription"
subscription
else
raise ArgumentError, "unknown operation type: #{operation}"
end
end
def execution_strategy_for_operation(operation)
case operation
when "query"
query_execution_strategy
when "mutation"
mutation_execution_strategy
when "subscription"
subscription_execution_strategy
else
raise ArgumentError, "unknown operation type: #{operation}"
end
end
# Determine the GraphQL type for a given object.
# This is required for unions and interfaces (including Relay's `Node` interface)
# @see [GraphQL::Schema::Warden] Restricted access to members of a schema
# @param type [GraphQL::UnionType, GraphQL:InterfaceType] the abstract type which is being resolved
# @param object [Any] An application object which GraphQL is currently resolving on
# @param ctx [GraphQL::Query::Context] The context for the current query
# @return [GraphQL::ObjectType] The type for exposing `object` in GraphQL
def resolve_type(type, object, ctx = :__undefined__)
check_resolved_type(type, object, ctx) do |ok_type, ok_object, ok_ctx|
if @resolve_type_proc.nil?
raise(GraphQL::RequiredImplementationMissingError, "Can't determine GraphQL type for: #{ok_object.inspect}, define `resolve_type (type, obj, ctx) -> { ... }` inside `Schema.define`.")
end
@resolve_type_proc.call(ok_type, ok_object, ok_ctx)
end
end
# This is a compatibility hack so that instance-level and class-level
# methods can get correctness checks without calling one another
# @api private
def check_resolved_type(type, object, ctx = :__undefined__)
if ctx == :__undefined__
# Old method signature
ctx = object
object = type
type = nil
end
if object.is_a?(GraphQL::Schema::Object)
object = object.object
end
if type.respond_to?(:graphql_definition)
type = type.graphql_definition
end
# Prefer a type-local function; fall back to the schema-level function
type_proc = type && type.resolve_type_proc
type_result = if type_proc
type_proc.call(object, ctx)
else
yield(type, object, ctx)
end
if type_result.nil?
nil
else
after_lazy(type_result) do |resolved_type_result|
if resolved_type_result.respond_to?(:graphql_definition)
resolved_type_result = resolved_type_result.graphql_definition
end
if !resolved_type_result.is_a?(GraphQL::BaseType)
type_str = "#{resolved_type_result} (#{resolved_type_result.class.name})"
raise "resolve_type(#{object}) returned #{type_str}, but it should return a GraphQL type"
else
resolved_type_result
end
end
end
end
def resolve_type=(new_resolve_type_proc)
callable = GraphQL::BackwardsCompatibility.wrap_arity(new_resolve_type_proc, from: 2, to: 3, last: true, name: "Schema#resolve_type(type, obj, ctx)")
@resolve_type_proc = callable
end
# Fetch an application object by its unique id
# @param id [String] A unique identifier, provided previously by this GraphQL schema
# @param ctx [GraphQL::Query::Context] The context for the current query
# @return [Any] The application object identified by `id`
def object_from_id(id, ctx)
if @object_from_id_proc.nil?
raise(GraphQL::RequiredImplementationMissingError, "Can't fetch an object for id \"#{id}\" because the schema's `object_from_id (id, ctx) -> { ... }` function is not defined")
else
@object_from_id_proc.call(id, ctx)
end
end
# @param new_proc [#call] A new callable for fetching objects by ID
def object_from_id=(new_proc)
@object_from_id_proc = new_proc
end
# When we encounter a type error during query execution, we call this hook.
#
# You can use this hook to write a log entry,
# add a {GraphQL::ExecutionError} to the response (with `ctx.add_error`)
# or raise an exception and halt query execution.
#
# @example A `nil` is encountered by a non-null field
# type_error ->(err, query_ctx) {
# err.is_a?(GraphQL::InvalidNullError) # => true
# }
#
# @example An object doesn't resolve to one of a {UnionType}'s members
# type_error ->(err, query_ctx) {
# err.is_a?(GraphQL::UnresolvedTypeError) # => true
# }
#
# @see {DefaultTypeError} is the default behavior.
# @param err [GraphQL::TypeError] The error encountered during execution
# @param ctx [GraphQL::Query::Context] The context for the field where the error occurred
# @return void
def type_error(err, ctx)
@type_error_proc.call(err, ctx)
end
# @param new_proc [#call] A new callable for handling type errors during execution
def type_error=(new_proc)
@type_error_proc = new_proc
end
# Can't delegate to `class`
alias :_schema_class :class
def_delegators :_schema_class, :unauthorized_object, :unauthorized_field, :inaccessible_fields
def_delegators :_schema_class, :directive
def_delegators :_schema_class, :error_handler
# Given this schema member, find the class-based definition object
# whose `method_name` should be treated as an application hook
# @see {.visible?}
# @see {.accessible?}
def call_on_type_class(member, method_name, context, default:)
member = if member.respond_to?(:type_class)
member.type_class
else
member
end
if member.respond_to?(:relay_node_type) && (t = member.relay_node_type)
member = t
end
if member.respond_to?(method_name)
member.public_send(method_name, context)
else
default
end
end
def visible?(member, context)
call_on_type_class(member, :visible?, context, default: true)
end
def accessible?(member, context)
call_on_type_class(member, :accessible?, context, default: true)
end
# A function to call when {#execute} receives an invalid query string
#
# @see {DefaultParseError} is the default behavior.
# @param err [GraphQL::ParseError] The error encountered during parsing
# @param ctx [GraphQL::Query::Context] The context for the query where the error occurred
# @return void
def parse_error(err, ctx)
@parse_error_proc.call(err, ctx)
end
# @param new_proc [#call] A new callable for handling parse errors during execution
def parse_error=(new_proc)
@parse_error_proc = new_proc
end
# Get a unique identifier from this object
# @param object [Any] An application object
# @param type [GraphQL::BaseType] The current type definition
# @param ctx [GraphQL::Query::Context] the context for the current query
# @return [String] a unique identifier for `object` which clients can use to refetch it
def id_from_object(object, type, ctx)
if @id_from_object_proc.nil?
raise(GraphQL::RequiredImplementationMissingError, "Can't generate an ID for #{object.inspect} of type #{type}, schema's `id_from_object` must be defined")
else
@id_from_object_proc.call(object, type, ctx)
end
end
# @param new_proc [#call] A new callable for generating unique IDs
def id_from_object=(new_proc)
@id_from_object_proc = new_proc
end
# Create schema with the result of an introspection query.
# @param introspection_result [Hash] A response from {GraphQL::Introspection::INTROSPECTION_QUERY}
# @return [GraphQL::Schema] the schema described by `input`
def self.from_introspection(introspection_result)
GraphQL::Schema::Loader.load(introspection_result)
end
# Create schema from an IDL schema or file containing an IDL definition.
# @param definition_or_path [String] A schema definition string, or a path to a file containing the definition
# @param default_resolve [<#call(type, field, obj, args, ctx)>] A callable for handling field resolution
# @param parser [Object] An object for handling definition string parsing (must respond to `parse`)
# @param using [Hash] Plugins to attach to the created schema with `use(key, value)`
# @param interpreter [Boolean] If false, the legacy {Execution::Execute} runtime will be used
# @return [Class] the schema described by `document`
def self.from_definition(definition_or_path, default_resolve: BuildFromDefinition::DefaultResolve, interpreter: true, parser: BuildFromDefinition::DefaultParser, using: {})
# If the file ends in `.graphql`, treat it like a filepath
definition = if definition_or_path.end_with?(".graphql")
File.read(definition_or_path)
else
definition_or_path
end
GraphQL::Schema::BuildFromDefinition.from_definition(definition, default_resolve: default_resolve, parser: parser, using: using, interpreter: interpreter)
end
# Error that is raised when [#Schema#from_definition] is passed an invalid schema definition string.
class InvalidDocumentError < Error; end;
# @return [Symbol, nil] The method name to lazily resolve `obj`, or nil if `obj`'s class wasn't registered wtih {#lazy_resolve}.
def lazy_method_name(obj)
@lazy_methods.get(obj)
end
# @return [Boolean] True if this object should be lazily resolved
def lazy?(obj)
!!lazy_method_name(obj)
end
# Return the GraphQL IDL for the schema
# @param context [Hash]
# @param only [<#call(member, ctx)>]
# @param except [<#call(member, ctx)>]
# @return [String]
def to_definition(only: nil, except: nil, context: {})
GraphQL::Schema::Printer.print_schema(self, only: only, except: except, context: context)
end
# Return the GraphQL::Language::Document IDL AST for the schema
# @param context [Hash]
# @param only [<#call(member, ctx)>]
# @param except [<#call(member, ctx)>]
# @return [GraphQL::Language::Document]
def to_document(only: nil, except: nil, context: {})
GraphQL::Language::DocumentFromSchemaDefinition.new(self, only: only, except: except, context: context).document
end
# Return the Hash response of {Introspection::INTROSPECTION_QUERY}.
# @param context [Hash]
# @param only [<#call(member, ctx)>]
# @param except [<#call(member, ctx)>]
# @return [Hash] GraphQL result
def as_json(only: nil, except: nil, context: {})
execute(Introspection::INTROSPECTION_QUERY, only: only, except: except, context: context).to_h
end
# Returns the JSON response of {Introspection::INTROSPECTION_QUERY}.
# @see {#as_json}
# @return [String]
def to_json(*args)
JSON.pretty_generate(as_json(*args))
end
def new_connections?
!!connections
end
attr_accessor :connections
class << self
extend Forwardable
# For compatibility, these methods all:
# - Cause the Schema instance to be created, if it hasn't been created yet
# - Delegate to that instance
# Eventually, the methods will be moved into this class, removing the need for the singleton.
def_delegators :graphql_definition,
# Execution
:execution_strategy_for_operation,
:validate, :multiplex_analyzers,
# Configuration
:metadata, :redefine,
:id_from_object_proc, :object_from_id_proc,
:id_from_object=, :object_from_id=,
:remove_handler
# @return [GraphQL::Subscriptions]
attr_accessor :subscriptions
# Returns the JSON response of {Introspection::INTROSPECTION_QUERY}.
# @see {#as_json}
# @return [String]
def to_json(*args)
JSON.pretty_generate(as_json(*args))
end
# Return the Hash response of {Introspection::INTROSPECTION_QUERY}.
# @param context [Hash]
# @param only [<#call(member, ctx)>]
# @param except [<#call(member, ctx)>]
# @return [Hash] GraphQL result
def as_json(only: nil, except: nil, context: {})
execute(Introspection::INTROSPECTION_QUERY, only: only, except: except, context: context).to_h
end
# Return the GraphQL IDL for the schema
# @param context [Hash]
# @param only [<#call(member, ctx)>]
# @param except [<#call(member, ctx)>]
# @return [String]
def to_definition(only: nil, except: nil, context: {})
GraphQL::Schema::Printer.print_schema(self, only: only, except: except, context: context)
end
# Return the GraphQL::Language::Document IDL AST for the schema
# @return [GraphQL::Language::Document]
def to_document
GraphQL::Language::DocumentFromSchemaDefinition.new(self).document
end
def find(path)
if !@finder
@find_cache = {}
@finder ||= GraphQL::Schema::Finder.new(self)
end
@find_cache[path] ||= @finder.find(path)
end
def graphql_definition
@graphql_definition ||= to_graphql
end
def default_filter
GraphQL::Filter.new(except: default_mask)
end
def default_mask(new_mask = nil)
if new_mask
@own_default_mask = new_mask
else
@own_default_mask || find_inherited_value(:default_mask, Schema::NullMask)
end
end
def static_validator
GraphQL::StaticValidation::Validator.new(schema: self)
end
def use(plugin, **kwargs)
if kwargs.any?
plugin.use(self, **kwargs)
else
plugin.use(self)
end
own_plugins << [plugin, kwargs]
end
def plugins
find_inherited_value(:plugins, EMPTY_ARRAY) + own_plugins
end
def to_graphql
schema_defn = self.new
schema_defn.raise_definition_error = true
schema_defn.query = query && query.graphql_definition
schema_defn.mutation = mutation && mutation.graphql_definition
schema_defn.subscription = subscription && subscription.graphql_definition
schema_defn.max_complexity = max_complexity
schema_defn.error_bubbling = error_bubbling
schema_defn.max_depth = max_depth
schema_defn.default_max_page_size = default_max_page_size
schema_defn.orphan_types = orphan_types.map(&:graphql_definition)
schema_defn.disable_introspection_entry_points = disable_introspection_entry_points?
schema_defn.disable_schema_introspection_entry_point = disable_schema_introspection_entry_point?
schema_defn.disable_type_introspection_entry_point = disable_type_introspection_entry_point?
prepped_dirs = {}
directives.each { |k, v| prepped_dirs[k] = v.graphql_definition}
schema_defn.directives = prepped_dirs
schema_defn.introspection_namespace = introspection
schema_defn.resolve_type = method(:resolve_type)
schema_defn.object_from_id = method(:object_from_id)
schema_defn.id_from_object = method(:id_from_object)
schema_defn.type_error = method(:type_error)
schema_defn.context_class = context_class
schema_defn.cursor_encoder = cursor_encoder
schema_defn.tracers.concat(tracers)
schema_defn.query_analyzers.concat(query_analyzers)
schema_defn.middleware.concat(all_middleware)
schema_defn.multiplex_analyzers.concat(multiplex_analyzers)
schema_defn.query_execution_strategy = query_execution_strategy
schema_defn.mutation_execution_strategy = mutation_execution_strategy
schema_defn.subscription_execution_strategy = subscription_execution_strategy
schema_defn.default_mask = default_mask
instrumenters.each do |step, insts|
insts.each do |inst|
schema_defn.instrumenters[step] << inst
end
end
lazy_methods.each do |lazy_class, value_method|
schema_defn.lazy_methods.set(lazy_class, value_method)
end
rescues.each do |err_class, handler|
schema_defn.rescue_from(err_class, &handler)
end
schema_defn.subscriptions ||= self.subscriptions
if !schema_defn.interpreter?
schema_defn.instrumenters[:query] << GraphQL::Schema::Member::Instrumentation
end
schema_defn.send(:rebuild_artifacts)
schema_defn
end
# Build a map of `{ name => type }` and return it
# @return [Hash<String => Class>] A dictionary of type classes by their GraphQL name
# @see get_type Which is more efficient for finding _one type_ by name, because it doesn't merge hashes.
def types
non_introspection_types.merge(introspection_system.types)
end
# @param type_name [String]
# @return [Module, nil] A type, or nil if there's no type called `type_name`
def get_type(type_name)
own_types[type_name] ||
introspection_system.types[type_name] ||
find_inherited_value(:types, EMPTY_HASH)[type_name]
end
# @return [GraphQL::Pagination::Connections] if installed
attr_accessor :connections
def new_connections?
!!connections
end
def query(new_query_object = nil)
if new_query_object
if @query_object
raise GraphQL::Error, "Second definition of `query(...)` (#{new_query_object.inspect}) is invalid, already configured with #{@query_object.inspect}"
else
@query_object = new_query_object
add_type_and_traverse(new_query_object, root: true)
nil
end
else
@query_object || find_inherited_value(:query)
end
end
def mutation(new_mutation_object = nil)
if new_mutation_object
if @mutation_object
raise GraphQL::Error, "Second definition of `mutation(...)` (#{new_mutation_object.inspect}) is invalid, already configured with #{@mutation_object.inspect}"
else
@mutation_object = new_mutation_object
add_type_and_traverse(new_mutation_object, root: true)
nil
end
else
@mutation_object || find_inherited_value(:mutation)
end
end
def subscription(new_subscription_object = nil)
if new_subscription_object
if @subscription_object
raise GraphQL::Error, "Second definition of `subscription(...)` (#{new_subscription_object.inspect}) is invalid, already configured with #{@subscription_object.inspect}"
else
@subscription_object = new_subscription_object
add_type_and_traverse(new_subscription_object, root: true)
nil
end
else
@subscription_object || find_inherited_value(:subscription)
end
end
# @see [GraphQL::Schema::Warden] Resticted access to root types
# @return [GraphQL::ObjectType, nil]
def root_type_for_operation(operation)
case operation
when "query"
query
when "mutation"
mutation
when "subscription"
subscription
else
raise ArgumentError, "unknown operation type: #{operation}"
end
end
def root_types
@root_types
end
# @param type [Module] The type definition whose possible types you want to see
# @return [Hash<String, Module>] All possible types, if no `type` is given.
# @return [Array<Module>] Possible types for `type`, if it's given.
def possible_types(type = nil, context = GraphQL::Query::NullContext)
if type
# TODO duck-typing `.possible_types` would probably be nicer here
if type.kind.union?
type.possible_types(context: context)
else
own_possible_types[type.graphql_name] ||
introspection_system.possible_types[type.graphql_name] ||
(
superclass.respond_to?(:possible_types) ?
superclass.possible_types(type, context) :
EMPTY_ARRAY
)
end
else
find_inherited_value(:possible_types, EMPTY_HASH)
.merge(own_possible_types)
.merge(introspection_system.possible_types)
end
end
def union_memberships(type = nil)
if type
own_um = own_union_memberships.fetch(type.graphql_name, EMPTY_ARRAY)
inherited_um = find_inherited_value(:union_memberships, EMPTY_HASH).fetch(type.graphql_name, EMPTY_ARRAY)
own_um + inherited_um
else
joined_um = own_union_memberships.dup
find_inherited_value(:union_memberhips, EMPTY_HASH).each do |k, v|
um = joined_um[k] ||= []
um.concat(v)
end
joined_um
end
end
def references_to(to_type = nil, from: nil)
@own_references_to ||= Hash.new { |h, k| h[k] = [] }
if to_type
if !to_type.is_a?(String)
to_type = to_type.graphql_name
end
if from
@own_references_to[to_type] << from
else
own_refs = @own_references_to[to_type]
inherited_refs = find_inherited_value(:references_to, EMPTY_HASH)[to_type] || EMPTY_ARRAY
own_refs + inherited_refs
end
else
# `@own_references_to` can be quite large for big schemas,
# and generally speaking, we won't inherit any values.
# So optimize the most common case -- don't create a duplicate Hash.
inherited_value = find_inherited_value(:references_to, EMPTY_HASH)
if inherited_value.any?
inherited_value.merge(@own_references_to)
else
@own_references_to
end
end
end
def type_from_ast(ast_node, context: nil)
type_owner = context ? context.warden : self
GraphQL::Schema::TypeExpression.build_type(type_owner, ast_node)
end
def get_field(type_or_name, field_name)
parent_type = case type_or_name
when LateBoundType
get_type(type_or_name.name)
when String
get_type(type_or_name)
when Module
type_or_name
else
raise ArgumentError, "unexpected field owner for #{field_name.inspect}: #{type_or_name.inspect} (#{type_or_name.class})"
end
if parent_type.kind.fields? && (field = parent_type.get_field(field_name))
field
elsif parent_type == query && (entry_point_field = introspection_system.entry_point(name: field_name))
entry_point_field
elsif (dynamic_field = introspection_system.dynamic_field(name: field_name))
dynamic_field
else
nil
end
end
def get_fields(type)
type.fields
end
def introspection(new_introspection_namespace = nil)
if new_introspection_namespace
@introspection = new_introspection_namespace
# reset this cached value:
@introspection_system = nil
else
@introspection || find_inherited_value(:introspection)
end
end
def introspection_system
if !@introspection_system
@introspection_system = Schema::IntrospectionSystem.new(self)
@introspection_system.resolve_late_bindings
end
@introspection_system
end
def cursor_encoder(new_encoder = nil)
if new_encoder
@cursor_encoder = new_encoder
end
@cursor_encoder || find_inherited_value(:cursor_encoder, Base64Encoder)
end
def default_max_page_size(new_default_max_page_size = nil)
if new_default_max_page_size
@default_max_page_size = new_default_max_page_size
else
@default_max_page_size || find_inherited_value(:default_max_page_size)
end
end
def query_execution_strategy(new_query_execution_strategy = nil)
if new_query_execution_strategy
@query_execution_strategy = new_query_execution_strategy
else
@query_execution_strategy || find_inherited_value(:query_execution_strategy, self.default_execution_strategy)
end
end
def mutation_execution_strategy(new_mutation_execution_strategy = nil)
if new_mutation_execution_strategy
@mutation_execution_strategy = new_mutation_execution_strategy
else
@mutation_execution_strategy || find_inherited_value(:mutation_execution_strategy, self.default_execution_strategy)
end
end
def subscription_execution_strategy(new_subscription_execution_strategy = nil)
if new_subscription_execution_strategy
@subscription_execution_strategy = new_subscription_execution_strategy
else
@subscription_execution_strategy || find_inherited_value(:subscription_execution_strategy, self.default_execution_strategy)
end
end
attr_writer :max_complexity
def max_complexity(max_complexity = nil)
if max_complexity
@max_complexity = max_complexity
elsif defined?(@max_complexity)
@max_complexity
else
find_inherited_value(:max_complexity)
end
end
attr_writer :analysis_engine
def analysis_engine
@analysis_engine || find_inherited_value(:analysis_engine, GraphQL::Analysis)
end
def using_ast_analysis?
analysis_engine == GraphQL::Analysis::AST
end
def interpreter?
if defined?(@interpreter)
@interpreter
else
find_inherited_value(:interpreter?, false)
end
end
attr_writer :interpreter
def error_bubbling(new_error_bubbling = nil)
if !new_error_bubbling.nil?
@error_bubbling = new_error_bubbling
else
@error_bubbling.nil? ? find_inherited_value(:error_bubbling) : @error_bubbling
end
end
attr_writer :error_bubbling
attr_writer :max_depth
def max_depth(new_max_depth = nil)
if new_max_depth
@max_depth = new_max_depth
elsif defined?(@max_depth)
@max_depth
else
find_inherited_value(:max_depth)
end
end
def disable_introspection_entry_points
@disable_introspection_entry_points = true
# TODO: this clears the cache made in `def types`. But this is not a great solution.
@introspection_system = nil
end
def disable_schema_introspection_entry_point
@disable_schema_introspection_entry_point = true
# TODO: this clears the cache made in `def types`. But this is not a great solution.
@introspection_system = nil
end
def disable_type_introspection_entry_point
@disable_type_introspection_entry_point = true
# TODO: this clears the cache made in `def types`. But this is not a great solution.
@introspection_system = nil
end
def disable_introspection_entry_points?
if instance_variable_defined?(:@disable_introspection_entry_points)
@disable_introspection_entry_points
else
find_inherited_value(:disable_introspection_entry_points?, false)
end
end
def disable_schema_introspection_entry_point?
if instance_variable_defined?(:@disable_schema_introspection_entry_point)
@disable_schema_introspection_entry_point
else
find_inherited_value(:disable_schema_introspection_entry_point?, false)
end
end
def disable_type_introspection_entry_point?
if instance_variable_defined?(:@disable_type_introspection_entry_point)
@disable_type_introspection_entry_point
else
find_inherited_value(:disable_type_introspection_entry_point?, false)
end
end
def orphan_types(*new_orphan_types)
if new_orphan_types.any?
new_orphan_types = new_orphan_types.flatten
add_type_and_traverse(new_orphan_types, root: false)
@orphan_types = new_orphan_types
own_orphan_types.concat(new_orphan_types.flatten)
end
find_inherited_value(:orphan_types, EMPTY_ARRAY) + own_orphan_types
end
def default_execution_strategy
if superclass <= GraphQL::Schema
superclass.default_execution_strategy
else
@default_execution_strategy ||= GraphQL::Execution::Execute
end
end
def context_class(new_context_class = nil)
if new_context_class
@context_class = new_context_class
else
@context_class || find_inherited_value(:context_class, GraphQL::Query::Context)
end
end
def rescue_from(*err_classes, &handler_block)
err_classes.each do |err_class|
own_rescues[err_class] = handler_block
end
end
# rubocop:disable Lint/DuplicateMethods
module ResolveTypeWithType
def resolve_type(type, obj, ctx)
first_resolved_type = if type.is_a?(Module) && type.respond_to?(:resolve_type)
type.resolve_type(obj, ctx)
else
super
end
after_lazy(first_resolved_type) do |resolved_type|
if resolved_type.nil? || (resolved_type.is_a?(Module) && resolved_type.respond_to?(:kind)) || resolved_type.is_a?(GraphQL::BaseType)
resolved_type
else
raise ".resolve_type should return a type definition, but got #{resolved_type.inspect} (#{resolved_type.class}) from `resolve_type(#{type}, #{obj}, #{ctx})`"
end
end
end
end
def resolve_type(type, obj, ctx)
if type.kind.object?
type
else
raise GraphQL::RequiredImplementationMissingError, "#{self.name}.resolve_type(type, obj, ctx) must be implemented to use Union types or Interface types (tried to resolve: #{type.name})"
end
end
# rubocop:enable Lint/DuplicateMethods
def inherited(child_class)
if self == GraphQL::Schema
child_class.directives(default_directives.values)
end
child_class.singleton_class.prepend(ResolveTypeWithType)
super
end
def rescues
find_inherited_value(:rescues, EMPTY_HASH).merge(own_rescues)
end
def object_from_id(node_id, ctx)
raise GraphQL::RequiredImplementationMissingError, "#{self.name}.object_from_id(node_id, ctx) must be implemented to load by ID (tried to load from id `#{node_id}`)"
end
def id_from_object(object, type, ctx)
raise GraphQL::RequiredImplementationMissingError, "#{self.name}.id_from_object(object, type, ctx) must be implemented to create global ids (tried to create an id for `#{object.inspect}`)"
end
def visible?(member, ctx)
member.type_class.visible?(ctx)
end
def accessible?(member, ctx)
member.type_class.accessible?(ctx)
end
# This hook is called when a client tries to access one or more
# fields that fail the `accessible?` check.
#
# By default, an error is added to the response. Override this hook to
# track metrics or return a different error to the client.
#
# @param error [InaccessibleFieldsError] The analysis error for this check
# @return [AnalysisError, nil] Return an error to skip the query
def inaccessible_fields(error)
error
end
# This hook is called when an object fails an `authorized?` check.
# You might report to your bug tracker here, so you can correct
# the field resolvers not to return unauthorized objects.
#
# By default, this hook just replaces the unauthorized object with `nil`.
#
# Whatever value is returned from this method will be used instead of the
# unauthorized object (accessible as `unauthorized_error.object`). If an
# error is raised, then `nil` will be used.
#
# If you want to add an error to the `"errors"` key, raise a {GraphQL::ExecutionError}
# in this hook.
#
# @param unauthorized_error [GraphQL::UnauthorizedError]
# @return [Object] The returned object will be put in the GraphQL response
def unauthorized_object(unauthorized_error)
nil
end
# This hook is called when a field fails an `authorized?` check.
#
# By default, this hook implements the same behavior as unauthorized_object.
#
# Whatever value is returned from this method will be used instead of the
# unauthorized field . If an error is raised, then `nil` will be used.
#
# If you want to add an error to the `"errors"` key, raise a {GraphQL::ExecutionError}
# in this hook.
#
# @param unauthorized_error [GraphQL::UnauthorizedFieldError]
# @return [Field] The returned field will be put in the GraphQL response
def unauthorized_field(unauthorized_error)
unauthorized_object(unauthorized_error)
end
def type_error(type_err, ctx)
DefaultTypeError.call(type_err, ctx)
end
# A function to call when {#execute} receives an invalid query string
#
# The default is to add the error to `context.errors`
# @param err [GraphQL::ParseError] The error encountered during parsing
# @param ctx [GraphQL::Query::Context] The context for the query where the error occurred
# @return void
def parse_error(parse_err, ctx)
ctx.errors.push(parse_err)
end
attr_writer :error_handler
# @return [GraphQL::Execution::Errors, Class<GraphQL::Execution::Errors::NullErrorHandler>]
def error_handler
@error_handler ||= GraphQL::Execution::Errors::NullErrorHandler
end
def lazy_resolve(lazy_class, value_method)
lazy_methods.set(lazy_class, value_method)
end
def instrument(instrument_step, instrumenter, options = {})
step = if instrument_step == :field && options[:after_built_ins]
:field_after_built_ins
else
instrument_step
end
own_instrumenters[step] << instrumenter
end
# Add several directives at once
# @param new_directives [Class]
def directives(new_directives = nil)
if new_directives
new_directives.each { |d| directive(d) }
end
find_inherited_value(:directives, default_directives).merge(own_directives)
end
# Attach a single directive to this schema
# @param new_directive [Class]
def directive(new_directive)
add_type_and_traverse(new_directive, root: false)
own_directives[new_directive.graphql_name] = new_directive
end
def default_directives
{
"include" => GraphQL::Schema::Directive::Include,
"skip" => GraphQL::Schema::Directive::Skip,
"deprecated" => GraphQL::Schema::Directive::Deprecated,
}
end
def tracer(new_tracer)
own_tracers << new_tracer
end
def tracers
find_inherited_value(:tracers, EMPTY_ARRAY) + own_tracers
end
def query_analyzer(new_analyzer)
if new_analyzer == GraphQL::Authorization::Analyzer
warn("The Authorization query analyzer is deprecated. Authorizing at query runtime is generally a better idea.")
end
own_query_analyzers << new_analyzer
end
def query_analyzers
find_inherited_value(:query_analyzers, EMPTY_ARRAY) + own_query_analyzers
end
def middleware(new_middleware = nil)
if new_middleware
own_middleware << new_middleware
else
# TODO make sure this is cached when running a query
MiddlewareChain.new(steps: all_middleware, final_step: GraphQL::Execution::Execute::FieldResolveStep)
end
end
def multiplex_analyzer(new_analyzer)
own_multiplex_analyzers << new_analyzer
end
def multiplex_analyzers
find_inherited_value(:multiplex_analyzers, EMPTY_ARRAY) + own_multiplex_analyzers
end
# Execute a query on itself.
# @see {Query#initialize} for arguments.
# @return [Hash] query result, ready to be serialized as JSON
def execute(query_str = nil, **kwargs)
if query_str
kwargs[:query] = query_str
end
# Some of the query context _should_ be passed to the multiplex, too
multiplex_context = if (ctx = kwargs[:context])
{
backtrace: ctx[:backtrace],
tracers: ctx[:tracers],
}
else
{}
end
# Since we're running one query, don't run a multiplex-level complexity analyzer
all_results = multiplex([kwargs], max_complexity: nil, context: multiplex_context)
all_results[0]
end
# Execute several queries on itself, concurrently.
#
# @example Run several queries at once
# context = { ... }
# queries = [
# { query: params[:query_1], variables: params[:variables_1], context: context },
# { query: params[:query_2], variables: params[:variables_2], context: context },
# ]
# results = MySchema.multiplex(queries)
# render json: {
# result_1: results[0],
# result_2: results[1],
# }
#
# @see {Query#initialize} for query keyword arguments
# @see {Execution::Multiplex#run_queries} for multiplex keyword arguments
# @param queries [Array<Hash>] Keyword arguments for each query
# @param context [Hash] Multiplex-level context
# @return [Array<Hash>] One result for each query in the input
def multiplex(queries, **kwargs)
schema = if interpreter?
self
else
graphql_definition
end
GraphQL::Execution::Multiplex.run_all(schema, queries, **kwargs)
end
def instrumenters
inherited_instrumenters = find_inherited_value(:instrumenters) || Hash.new { |h,k| h[k] = [] }
inherited_instrumenters.merge(own_instrumenters) do |_step, inherited, own|
inherited + own
end
end
# Call the given block at the right time, either:
# - Right away, if `value` is not registered with `lazy_resolve`
# - After resolving `value`, if it's registered with `lazy_resolve` (eg, `Promise`)
# @api private
def after_lazy(value)
if lazy?(value)
GraphQL::Execution::Lazy.new do
result = sync_lazy(value)
# The returned result might also be lazy, so check it, too
after_lazy(result) do |final_result|
yield(final_result) if block_given?
end
end
else
yield(value) if block_given?
end
end
# Override this method to handle lazy objects in a custom way.
# @param value [Object] an instance of a class registered with {.lazy_resolve}
# @param ctx [GraphQL::Query::Context] the context for this query
# @return [Object] A GraphQL-ready (non-lazy) object
def sync_lazy(value)
lazy_method = lazy_method_name(value)
if lazy_method
synced_value = value.public_send(lazy_method)
sync_lazy(synced_value)
else
value
end
end
# @return [Symbol, nil] The method name to lazily resolve `obj`, or nil if `obj`'s class wasn't registered wtih {#lazy_resolve}.
def lazy_method_name(obj)
lazy_methods.get(obj)
end
# @return [Boolean] True if this object should be lazily resolved
def lazy?(obj)
!!lazy_method_name(obj)
end
private
def lazy_methods
if !defined?(@lazy_methods)
if inherited_map = find_inherited_value(:lazy_methods)
# this isn't _completely_ inherited :S (Things added after `dup` won't work)
@lazy_methods = inherited_map.dup
else
@lazy_methods = GraphQL::Execution::Lazy::LazyMethodMap.new
@lazy_methods.set(GraphQL::Execution::Lazy, :value)
end
end
@lazy_methods
end
def own_types
@own_types ||= {}
end
def non_introspection_types
find_inherited_value(:non_introspection_types, EMPTY_HASH).merge(own_types)
end
def own_plugins
@own_plugins ||= []
end
def own_rescues
@own_rescues ||= {}
end
def own_orphan_types
@own_orphan_types ||= []
end
def own_possible_types
@own_possible_types ||= {}
end
def own_union_memberships
@own_union_memberships ||= {}
end
def own_directives
@own_directives ||= {}
end
def own_instrumenters
@own_instrumenters ||= Hash.new { |h,k| h[k] = [] }
end
def own_tracers
@own_tracers ||= []
end
def own_query_analyzers
@defined_query_analyzers ||= []
end
def all_middleware
find_inherited_value(:all_middleware, EMPTY_ARRAY) + own_middleware
end
def own_middleware
@own_middleware ||= []
end
def own_multiplex_analyzers
@own_multiplex_analyzers ||= []
end
# @param t [Module, Array<Module>]
# @return [void]
def add_type_and_traverse(t, root:)
if root
@root_types ||= []
@root_types << t
end
late_types = []
new_types = Array(t)
new_types.each { |t| add_type(t, owner: nil, late_types: late_types) }
missed_late_types = 0
while (late_type_vals = late_types.shift)
type_owner, lt = late_type_vals
if lt.is_a?(String)
type = Member::BuildType.constantize(lt)
# Reset the counter, since we might succeed next go-round
missed_late_types = 0
update_type_owner(type_owner, type)
add_type(type, owner: type_owner, late_types: late_types)
elsif lt.is_a?(LateBoundType)
if (type = get_type(lt.graphql_name))
# Reset the counter, since we might succeed next go-round
missed_late_types = 0
update_type_owner(type_owner, type)
add_type(type, owner: type_owner, late_types: late_types)
else
missed_late_types += 1
# Add it back to the list, maybe we'll be able to resolve it later.
late_types << [type_owner, lt]
if missed_late_types == late_types.size
# We've looked at all of them and haven't resolved one.
raise UnresolvedLateBoundTypeError.new(type: lt)
else
# Try the next one
end
end
else
raise ArgumentError, "Unexpected late type: #{lt.inspect}"
end
end
nil
end
def update_type_owner(owner, type)
case owner
when Class
if owner.kind.union?
# It's a union with possible_types
# Replace the item by class name
owner.type_memberships.each { |tm|
possible_type = tm.object_type
if possible_type.is_a?(String) && (possible_type == type.name)
# This is a match of Ruby class names, not graphql names,
# since strings are used to refer to constants.
tm.object_type = type
elsif possible_type.is_a?(LateBoundType) && possible_type.graphql_name == type.graphql_name
tm.object_type = type
end
}
own_possible_types[owner.graphql_name] = owner.possible_types
elsif type.kind.interface? && owner.kind.object?
new_interfaces = owner.interfaces.map do |t|
if t.is_a?(String) && t == type.graphql_name
type
elsif t.is_a?(LateBoundType) && t.graphql_name == type.graphql_name
type
else
t
end
end
owner.implements(*new_interfaces)
end
when nil
# It's a root type
own_types[type.graphql_name] = type
when GraphQL::Schema::Field, GraphQL::Schema::Argument
orig_type = owner.type
# Apply list/non-null wrapper as needed
if orig_type.respond_to?(:of_type)
transforms = []
while (orig_type.respond_to?(:of_type))
if orig_type.kind.non_null?
transforms << :to_non_null_type
elsif orig_type.kind.list?
transforms << :to_list_type
else
raise "Invariant: :of_type isn't non-null or list"
end
orig_type = orig_type.of_type
end
transforms.reverse_each { |t| type = type.public_send(t) }
end
owner.type = type
else
raise "Unexpected update: #{owner.inspect} #{type.inspect}"
end
end
def add_type(type, owner:, late_types:)
if type.respond_to?(:metadata) && type.metadata.is_a?(Hash)
type_class = type.metadata[:type_class]
if type_class.nil?
raise ArgumentError, "Can't add legacy type: #{type} (#{type.class})"
else
type = type_class
end
elsif type.is_a?(String) || type.is_a?(GraphQL::Schema::LateBoundType)
late_types << [owner, type]
return
end
if owner.is_a?(Class) && owner < GraphQL::Schema::Union
um = own_union_memberships[type.graphql_name] ||= []
um << owner
end
if (prev_type = own_types[type.graphql_name])
if prev_type != type
raise ArgumentError, "Conflicting type definitions for `#{type.graphql_name}`: #{prev_type} (#{prev_type.class}), #{type} #{type.class}"
else
# This type was already added
end
elsif type.is_a?(Class) && type < GraphQL::Schema::Directive
type.arguments.each do |_name, arg|
arg_type = arg.type.unwrap
references_to(arg_type, from: arg)
add_type(arg_type, owner: arg, late_types: late_types)
end
else
own_types[type.graphql_name] = type
if type.kind.fields?
type.fields.each do |_name, field|
field_type = field.type.unwrap
references_to(field_type, from: field)
add_type(field_type, owner: field, late_types: late_types)
field.arguments.each do |_name, arg|
arg_type = arg.type.unwrap
references_to(arg_type, from: arg)
add_type(arg_type, owner: arg, late_types: late_types)
end
end
end
if type.kind.input_object?
type.arguments.each do |_name, arg|
arg_type = arg.type.unwrap
references_to(arg_type, from: arg)
add_type(arg_type, owner: arg, late_types: late_types)
end
end
if type.kind.union?
own_possible_types[type.graphql_name] = type.possible_types
type.possible_types.each do |t|
add_type(t, owner: type, late_types: late_types)
end
end
if type.kind.object?
own_possible_types[type.graphql_name] = [type]
type.interfaces.each do |i|
implementers = own_possible_types[i.graphql_name] ||= []
implementers << type
add_type(i, owner: type, late_types: late_types)
end
end
end
end
end
# Call the given block at the right time, either:
# - Right away, if `value` is not registered with `lazy_resolve`
# - After resolving `value`, if it's registered with `lazy_resolve` (eg, `Promise`)
# @api private
def after_lazy(value)
if lazy?(value)
GraphQL::Execution::Lazy.new do
result = sync_lazy(value)
# The returned result might also be lazy, so check it, too
after_lazy(result) do |final_result|
yield(final_result) if block_given?
end
end
else
yield(value) if block_given?
end
end
# @see Schema.sync_lazy for a hook to override
# @api private
def sync_lazy(value)
lazy_method = lazy_method_name(value)
if lazy_method
synced_value = value.public_send(lazy_method)
sync_lazy(synced_value)
else
value
end
end
protected
def rescues?
!!@rescue_middleware
end
# Lazily create a middleware and add it to the schema
# (Don't add it if it's not used)
def rescue_middleware
@rescue_middleware ||= GraphQL::Schema::RescueMiddleware.new.tap { |m| middleware.insert(0, m) }
end
private
def rebuild_artifacts
if @rebuilding_artifacts
raise CyclicalDefinitionError, "Part of the schema build process re-triggered the schema build process, causing an infinite loop. Avoid using Schema#types, Schema#possible_types, and Schema#get_field during schema build."
else
@rebuilding_artifacts = true
@introspection_system = Schema::IntrospectionSystem.new(self)
traversal = Traversal.new(self)
@types = traversal.type_map
@root_types = [query, mutation, subscription]
@instrumented_field_map = traversal.instrumented_field_map
@type_reference_map = traversal.type_reference_map
@union_memberships = traversal.union_memberships
@find_cache = {}
@finder = Finder.new(self)
end
ensure
@rebuilding_artifacts = false
end
class CyclicalDefinitionError < GraphQL::Error
end
def with_definition_error_check
if @definition_error
raise @definition_error
else
yield
end
end
end
end