# 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/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_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/list" require "graphql/schema/non_null" require "graphql/schema/argument" require "graphql/schema/enum_value" require "graphql/schema/enum" require "graphql/schema/field" require "graphql/schema/input_object" require "graphql/schema/interface" require "graphql/schema/resolver" require "graphql/schema/mutation" require "graphql/schema/relay_classic_mutation" require "graphql/schema/object" require "graphql/schema/scalar" require "graphql/schema/union" 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 # - middleware for interacting with execution # # 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. # # A schema accepts a `Relay::GlobalNodeIdentification` instance for use with Relay IDs. # # @example defining a schema # MySchema = GraphQL::Schema.define do # query QueryType # middleware PermissionMiddleware # rescue_from(ActiveRecord::RecordNotFound) { "Not found" } # # If types are only connected by way of interfaces, they must be added here # orphan_types ImageType, AudioType # end # class Schema extend GraphQL::Schema::Member::AcceptsDefinition include GraphQL::Define::InstanceDefinable accepts_definitions \ :query, :mutation, :subscription, :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, :raise_definition_error, :object_from_id, :id_from_object, :default_mask, :cursor_encoder, directives: ->(schema, directives) { schema.directives = directives.reduce({}) { |m, d| m[d.name] = d; m }}, 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) { 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) } 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 # 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 class << self attr_accessor :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 self.default_execution_strategy = GraphQL::Execution::Execute BUILT_IN_TYPES = Hash[[INT_TYPE, STRING_TYPE, FLOAT_TYPE, BOOLEAN_TYPE, ID_TYPE].map{ |type| [type.name, type] }] DIRECTIVES = [GraphQL::Directive::IncludeDirective, GraphQL::Directive::SkipDirective, GraphQL::Directive::DeprecatedDirective] DYNAMIC_FIELDS = ["__type", "__typename", "__schema"] attr_reader :static_validator, :object_from_id_proc, :id_from_object_proc, :resolve_type_proc def initialize @tracers = [] @definition_error = nil @orphan_types = [] @directives = DIRECTIVES.reduce({}) { |m, d| m[d.name] = d; m } @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::Relay::ConnectionResolve::LazyNodesWrapper, :never_called) @cursor_encoder = Base64Encoder # Default to the built-in execution strategy: @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 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 # 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] def validate(string_or_document, rules: 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) 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(NotImplementedError, validation_error) rebuild_artifacts @definition_error = nil nil rescue StandardError => err if @raise_definition_error || err.is_a?(CyclicalDefinitionError) 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] 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 # @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) rebuild_artifacts unless defined?(@type_reference_map) @type_reference_map.fetch(type_name, []) end # Returns a list of Union types in which a type is a member # @param type [GraphQL::ObjectType] # @return [Array] 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] Keyword arguments for each query # @param context [Hash] Multiplex-level context # @return [Array] 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 parent_type.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] def get_fields(type) @instrumented_field_map[type.name] end def type_from_ast(ast_node) GraphQL::Schema::TypeExpression.build_type(self.types, 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 # @return [Array] types which belong to `type_defn` in this schema def possible_types(type_defn) @possible_types ||= GraphQL::Schema::PossibleTypes.new(self) @possible_types.possible_types(type_defn) 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(NotImplementedError, "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 # 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.respond_to?(:graphql_definition) type_result = type_result.graphql_definition end if type_result.nil? nil elsif !type_result.is_a?(GraphQL::BaseType) type_str = "#{type_result} (#{type_result.class.name})" raise "resolve_type(#{object}) returned #{type_str}, but it should return a GraphQL type" else type_result 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(NotImplementedError, "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 # 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(NotImplementedError, "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`) # @return [GraphQL::Schema] the schema described by `document` def self.from_definition(definition_or_path, default_resolve: BuildFromDefinition::DefaultResolve, parser: BuildFromDefinition::DefaultParser) # 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) 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 # @return [GraphQL::Language::Document] def to_document GraphQL::Language::DocumentFromSchemaDefinition.new(self).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 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, # Schema structure :as_json, :to_json, :to_document, :to_definition, # Execution :execute, :multiplex, :static_validator, :introspection_system, :query_analyzers, :middleware, :tracers, :instrumenters, :query_execution_strategy, :mutation_execution_strategy, :subscription_execution_strategy, :validate, :multiplex_analyzers, :lazy?, :lazy_method_name, # Configuration :max_complexity=, :max_depth=, :metadata, :default_filter, :redefine, :id_from_object_proc, :object_from_id_proc, :id_from_object=, :object_from_id=, :type_error, :remove_handler, # Members :types, :get_fields, :find, :root_type_for_operation, :subscriptions, :union_memberships, :get_field, :root_types, :references_to, :type_from_ast, :possible_types, :get_field def graphql_definition @graphql_definition ||= to_graphql end def use(plugin, options = {}) plugins << [plugin, options] end def plugins @plugins ||= [] end def to_graphql schema_defn = self.new schema_defn.raise_definition_error = true schema_defn.query = query schema_defn.mutation = mutation schema_defn.subscription = subscription schema_defn.max_complexity = max_complexity schema_defn.max_depth = max_depth schema_defn.default_max_page_size = default_max_page_size schema_defn.orphan_types = orphan_types schema_defn.directives = directives 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(defined_tracers) schema_defn.query_analyzers.concat(defined_query_analyzers) schema_defn.middleware.concat(defined_middleware) schema_defn.multiplex_analyzers.concat(defined_multiplex_analyzers) defined_instrumenters.each do |step, insts| insts.each do |inst| schema_defn.instrumenters[step] << inst end end schema_defn.instrumenters[:query] << GraphQL::Schema::Member::Instrumentation lazy_classes.each do |lazy_class, value_method| schema_defn.lazy_methods.set(lazy_class, value_method) end if @rescues @rescues.each do |err_class, handler| schema_defn.rescue_from(err_class, &handler) end end if plugins.any? schema_plugins = plugins # TODO don't depend on .define schema_defn = schema_defn.redefine do schema_plugins.each do |plugin, options| if options.any? use(plugin, **options) else use(plugin) end end end end schema_defn.send(:rebuild_artifacts) schema_defn end def query(new_query_object = nil) if new_query_object @query_object = new_query_object else @query_object.respond_to?(:graphql_definition) ? @query_object.graphql_definition : @query_object end end def mutation(new_mutation_object = nil) if new_mutation_object @mutation_object = new_mutation_object else @mutation_object.respond_to?(:graphql_definition) ? @mutation_object.graphql_definition : @mutation_object end end def subscription(new_subscription_object = nil) if new_subscription_object @subscription_object = new_subscription_object else @subscription_object.respond_to?(:graphql_definition) ? @subscription_object.graphql_definition : @subscription_object end end def introspection(new_introspection_namespace = nil) if new_introspection_namespace @introspection = new_introspection_namespace else @introspection end end def cursor_encoder(new_encoder = nil) if new_encoder @cursor_encoder = new_encoder end @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 end end def max_complexity(max_complexity = nil) if max_complexity @max_complexity = max_complexity else @max_complexity end end def max_depth(new_max_depth = nil) if new_max_depth @max_depth = new_max_depth else @max_depth end end def orphan_types(*new_orphan_types) if new_orphan_types.any? @orphan_types = new_orphan_types.flatten else @orphan_types || [] end end def default_execution_strategy if superclass <= GraphQL::Schema superclass.default_execution_strategy else @default_execution_strategy end end def context_class(new_context_class = nil) if new_context_class @context_class = new_context_class else @context_class || GraphQL::Query::Context end end def rescue_from(err_class, &handler_block) @rescues ||= {} @rescues[err_class] = handler_block end def resolve_type(type, obj, ctx) raise NotImplementedError, "#{self.name}.resolve_type(type, obj, ctx) must be implemented to use Union types or Interface types (tried to resolve: #{type.name})" end def object_from_id(node_id, ctx) raise NotImplementedError, "#{self.name}.object_from_id(node_id, ctx) must be implemented to use the `node` field (tried to load from id `#{node_id}`)" end def id_from_object(object, type, ctx) raise NotImplementedError, "#{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 type_error(type_err, ctx) DefaultTypeError.call(type_err, ctx) end def lazy_resolve(lazy_class, value_method) lazy_classes[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 defined_instrumenters[step] << instrumenter end def directives(new_directives = nil) if new_directives @directives = new_directives.reduce({}) { |m, d| m[d.name] = d; m } end @directives ||= directives(DIRECTIVES) end def tracer(new_tracer) defined_tracers << new_tracer end def query_analyzer(new_analyzer) defined_query_analyzers << new_analyzer end def middleware(new_middleware = nil) if new_middleware defined_middleware << new_middleware else graphql_definition.middleware end end def multiplex_analyzer(new_analyzer) defined_multiplex_analyzers << new_analyzer end private def lazy_classes @lazy_classes ||= {} end def defined_instrumenters @defined_instrumenters ||= Hash.new { |h,k| h[k] = [] } end def defined_tracers @defined_tracers ||= [] end def defined_query_analyzers @defined_query_analyzers ||= [] end def defined_middleware @defined_middleware ||= [] end def defined_multiplex_analyzers @defined_multiplex_analyzers ||= [] end end def self.inherited(child_class) child_class.singleton_class.class_eval do prepend(MethodWrappers) end end module MethodWrappers # Wrap the user-provided resolve-type in a correctness check def resolve_type(type, obj, ctx = :__undefined__) graphql_definition.check_resolved_type(type, obj, ctx) do |ok_type, ok_obj, ok_ctx| super(ok_type, ok_obj, ok_ctx) end 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 # Wrap Relay-related objects in wrappers # @api private BUILT_IN_INSTRUMENTERS = [ GraphQL::Relay::ConnectionInstrumentation, GraphQL::Relay::EdgesInstrumentation, GraphQL::Relay::Mutation::Instrumentation, GraphQL::Schema::Member::Instrumentation, ] 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