Skip to content
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
22 changes: 0 additions & 22 deletions guides/code_loading.md

This file was deleted.

12 changes: 0 additions & 12 deletions guides/parser_cache.md

This file was deleted.

50 changes: 26 additions & 24 deletions guides/schema/definition.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,8 @@ For defining GraphQL types, see the guides for those types: {% internal_link "ob

## Types in the Schema

{{ "Schema.query" | api_doc }}, {{ "Schema.mutation" | api_doc }}, and {{ "Schema.subscription" | api_doc}} declare the [entry-point types](https://graphql.org/learn/schema/#the-query-and-mutation-types) of the schema.

{{ "Schema.orphan_types" | api_doc }} declares object types which implement {% internal_link "Interfaces", "/type_definitions/interfaces" %} but aren't used as field return types in the schema. For more about this specific scenario, see {% internal_link "Orphan Types", "/type_definitions/interfaces#orphan-types" %}
- {{ "Schema.query" | api_doc }}, {{ "Schema.mutation" | api_doc }}, and {{ "Schema.subscription" | api_doc}} declare the [entry-point types](https://graphql.org/learn/schema/#the-query-and-mutation-types) of the schema.
- {{ "Schema.orphan_types" | api_doc }} declares object types which implement {% internal_link "Interfaces", "/type_definitions/interfaces" %} but aren't used as field return types in the schema. For more about this specific scenario, see {% internal_link "Orphan Types", "/type_definitions/interfaces#orphan-types" %}

### Lazy-loading types

Expand Down Expand Up @@ -88,42 +87,45 @@ Additionally, {{ "Schema.resolve_type" | api_doc }} is called by GraphQL-Ruby to

## Error Handling

{{ "Schema.type_error" | api_doc }} handles type errors at runtime, read more in the {% internal_link "Type errors guide", "/errors/type_errors" %}.

{{ "Schema.rescue_from" | api_doc }} defines error handlers for application errors. See the {% internal_link "error handling guide", "/errors/error_handling" %} for more.

{{ "Schema.parse_error" | api_doc }} and {{ "Schema.query_stack_error" | api_doc }} provide hooks for reporting errors to your bug tracker.
- {{ "Schema.type_error" | api_doc }} handles type errors at runtime, read more in the {% internal_link "Type errors guide", "/errors/type_errors" %}.
- {{ "Schema.rescue_from" | api_doc }} defines error handlers for application errors. See the {% internal_link "error handling guide", "/errors/error_handling" %} for more.
- {{ "Schema.parse_error" | api_doc }} and {{ "Schema.query_stack_error" | api_doc }} provide hooks for reporting errors to your bug tracker.

## Default Limits

{{ "Schema.max_depth" | api_doc }} and {{ "Schema.max_complexity" | api_doc }} apply some limits to incoming queries. See {% internal_link "Complexity and Depth", "/queries/complexity_and_depth" %} for more.

{{ "Schema.default_max_page_size" | api_doc }} applies limits to {% internal_link "connection fields", "/pagination/overview" %}.

{{ "Schema.validate_timeout" | api_doc }}, {{ "Schema.validate_max_errors" | api_doc }} and {{ "Schema.max_query_string_tokens" | api_doc }} all apply limits to query execution. See {% internal_link "Timeout", "/queries/timeout" %} for more.
- {{ "Schema.max_depth" | api_doc }} and {{ "Schema.max_complexity" | api_doc }} apply some limits to incoming queries. See {% internal_link "Complexity and Depth", "/queries/complexity_and_depth" %} for more.
- {{ "Schema.default_max_page_size" | api_doc }} applies limits to {% internal_link "connection fields", "/pagination/overview" %}.
- {{ "Schema.validate_timeout" | api_doc }}, {{ "Schema.validate_max_errors" | api_doc }} and {{ "Schema.max_query_string_tokens" | api_doc }} all apply limits to query execution. See {% internal_link "Timeout", "/queries/timeout" %} for more.

## Introspection

{{ "Schema.extra_types" | api_doc }} declares types which should be printed in the SDL and returned in introspection queries, but aren't otherwise used in the schema.

{{ "Schema.introspection" | api_doc }} can attach a {% internal_link "custom introspection system", "/schema/introspection" %} to the schema.
- {{ "Schema.extra_types" | api_doc }} declares types which should be printed in the SDL and returned in introspection queries, but aren't otherwise used in the schema.
- {{ "Schema.introspection" | api_doc }} can attach a {% internal_link "custom introspection system", "/schema/introspection" %} to the schema.

## Authorization

{{ "Schema.unauthorized_object" | api_doc }} and {{ "Schema.unauthorized_field" | api_doc }} are called when {% internal_link "authorization hooks", "/authorization/authorization" %} return `false` during query execution.
- {{ "Schema.unauthorized_object" | api_doc }} and {{ "Schema.unauthorized_field" | api_doc }} are called when {% internal_link "authorization hooks", "/authorization/authorization" %} return `false` during query execution.

## Execution Configuration

{{ "Schema.trace_with" | api_doc }} attaches tracer modules. See {% internal_link "Tracing", "/queries/tracing" %} for more.
- {{ "Schema.trace_with" | api_doc }} attaches tracer modules. See {% internal_link "Tracing", "/queries/tracing" %} for more.
- {{ "Schema.query_analyzer" | api_doc }} and {{ "Schema.multiplex_analyzer" }} accept processors for ahead-of-time query analysis, see {% internal_link "Analysis", "/queries/ast_analysis" %} for more.
- {{ "Schema.default_logger" | api_doc }} configures a logger for runtime. See {% internal_link "Logging", "/queries/logging" %}.
- {{ "Schema.context_class" | api_doc }} and {{ "Schema.query_class" | api_doc }} attach custom subclasses to your schema to use during execution.
- {{ "Schema.lazy_resolve" | api_doc }} registers classes with {% internal_link "lazy execution", "/schema/lazy_execution" %}.

{{ "Schema.query_analyzer" | api_doc }} and {{ "Schema.multiplex_analyzer" }} accept processors for ahead-of-time query analysis, see {% internal_link "Analysis", "/queries/ast_analysis" %} for more.
## Plugins

{{ "Schema.default_logger" | api_doc }} configures a logger for runtime. See {% internal_link "Logging", "/queries/logging" %}.
- {{ "Schema.use" | api_doc }} adds plugins to your schema. For example, {{ "GraphQL::Dataloader" | api_doc }} and {{ "GraphQL::Schema::Visibility" | api_doc }} are installed this way.

{{ "Schema.context_class" | api_doc }} and {{ "Schema.query_class" | api_doc }} attach custom subclasses to your schema to use during execution.
## Production Considerations

{{ "Schema.lazy_resolve" | api_doc }} registers classes with {% internal_link "lazy execution", "/schema/lazy_execution" %}.
- __Parser caching__: if your application parses GraphQL _files_ (queries or schema definition), it may benefit from enabling {{ "GraphQL::Parser::Cache" | api_doc }}.
- __Eager loading the library__: by default, GraphQL-Ruby autoloads its constants as-needed. In production, they should be autoloaded instead, using `GraphQL.eager_load!`.

## Plugins
- Rails: enabled automatically. (ActiveSupport calls `.eager_load!`.)
- Sinatra: add `configure(:production) { GraphQL.eager_load! }` to your application file.
- Hanami: add `environment(:production) { GraphQL.eager_load! }` to your application file.
- Other frameworks: call `GraphQL.eager_load!` when your application is booting in production mode.

{{ "Schema.use" | api_doc }} adds plugins to your schema. For example, {{ "GraphQL::Dataloader" | api_doc }} and {{ "GraphQL::Schema::Visibility" | api_doc }} are installed this way.
See {{"GraphQL::Autoload#eager_load!" | api_doc }} for more details.
34 changes: 28 additions & 6 deletions lib/graphql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@
module GraphQL
extend Autoload

# Load all `autoload`-configured classes, and also eager-load dependents who have autoloads of their own.
def self.eager_load!
super
Query.eager_load!
Types.eager_load!
end

class Error < StandardError
end

Expand Down Expand Up @@ -74,24 +81,39 @@ module EmptyObjects
class << self
# If true, the parser should raise when an integer or float is followed immediately by an identifier (instead of a space or punctuation)
attr_accessor :reject_numbers_followed_by_names
end

self.reject_numbers_followed_by_names = false

class << self
# If `production?` is detected but `eager_load!` wasn't called, emit a warning.
# @return [void]
def ensure_eager_load!
if production? && !eager_loading?
warn "GraphQL should be eager loaded in production environments!"
warn <<~WARNING
GraphQL-Ruby thinks this is a production deployment but didn't eager-load its constants. Address this by:

- Calling `GraphQL.eager_load!` in a production-only initializer or setup hook
- Assign `GraphQL.env = "..."` to something _other_ than `"production"` (for example, `GraphQL.env = "development"`)

More details: https://graphql-ruby.org/schema/definition#production-considerations
WARNING
end
end

attr_accessor :env

private

# Detect whether this is a production deployment or not
def production?
(env = ENV["RACK_ENV"] || ENV["RAILS_ENV"] || ENV["HANAMI_ENV"] || ENV["APP_ENV"]) && env.to_s.downcase == "production"
if env
# Manually assigned to production?
env == "production"
else
(detected_env = ENV["RACK_ENV"] || ENV["RAILS_ENV"] || ENV["HANAMI_ENV"] || ENV["APP_ENV"]) && detected_env.to_s.downcase == "production"
end
end
end

self.reject_numbers_followed_by_names = false

autoload :ExecutionError, "graphql/execution_error"
autoload :RuntimeTypeError, "graphql/runtime_type_error"
autoload :UnresolvedTypeError, "graphql/unresolved_type_error"
Expand Down
9 changes: 9 additions & 0 deletions lib/graphql/autoload.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,34 @@

module GraphQL
module Autoload
# Register a constant named `const_name` to be loaded from `path`.
# This is like `Kernel#autoload` but it tracks the constants so they can be eager-loaded with {#eager_load!}
# @param const_name [Symbol]
# @param path [String]
# @return [void]
def autoload(const_name, path)
@_eagerloaded_constants ||= []
@_eagerloaded_constants << const_name

super const_name, path
end

# Call this to load this constant's `autoload` dependents and continue calling recursively
# @return [void]
def eager_load!
@_eager_loading = true
if @_eagerloaded_constants
@_eagerloaded_constants.each { |const_name| const_get(const_name) }
@_eagerloaded_constants = nil
end
nil
ensure
@_eager_loading = false
end

private

# @return [Boolean] `true` if GraphQL-Ruby is currently eager-loading its constants
def eager_loading?
@_eager_loading ||= false
end
Expand Down
13 changes: 13 additions & 0 deletions lib/graphql/language/cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,25 @@

module GraphQL
module Language
# This cache is used by {GraphQL::Language::Parser.parse_file} when it's enabled.
#
# With Rails, parser caching may enabled by setting `config.graphql.parser_cache = true` in your Rails application.
#
# The cache may be manually built by assigning `GraphQL::Language::Parser.cache = GraphQL::Language::Cache.new("some_dir")`.
# This will create a directory (`tmp/cache/graphql` by default) that stores a cache of parsed files.
#
# Much like [bootsnap](https://github.com/Shopify/bootsnap), the parser cache needs to be cleaned up manually.
# You will need to clear the cache directory for each new deployment of your application.
# Also note that the parser cache will grow as your schema is loaded, so the cache directory must be writable.
#
# @see GraphQL::Railtie for simple Rails integration
class Cache
def initialize(path)
@path = path
end

DIGEST = Digest::SHA256.new << GraphQL::VERSION

def fetch(filename)
hash = DIGEST.dup << filename
begin
Expand Down
6 changes: 6 additions & 0 deletions lib/graphql/railtie.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# frozen_string_literal: true

module GraphQL
# Support {GraphQL::Parser::Cache}
#
# @example Enable the parser cache with default directory
#
# config.graphql.parser_cache = true
#
class Railtie < Rails::Railtie
config.graphql = ActiveSupport::OrderedOptions.new
config.graphql.parser_cache = false
Expand Down
7 changes: 7 additions & 0 deletions spec/fixtures/eager_module/nested_eager_module.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true
module EagerModule
module NestedEagerModule
extend GraphQL::Autoload
autoload(:NestedEagerClass, "fixtures/eager_module/nested_eager_module/nested_eager_class")
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true
module EagerModule
module NestedEagerModule
class NestedEagerClass
end
end
end
55 changes: 55 additions & 0 deletions spec/graphql/autoload_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,18 @@ module EagerModule
extend GraphQL::Autoload
autoload(:EagerClass, "fixtures/eager_module/eager_class")
autoload(:OtherEagerClass, "fixtures/eager_module/other_eager_class")
autoload(:NestedEagerModule, "fixtures/eager_module/nested_eager_module")

def self.eager_load!
super

NestedEagerModule.eager_load!
end
end

describe "#autoload" do
it "sets autoload" do
assert LazyModule.const_defined?(:LazyClass)
assert_equal("fixtures/lazy_module/lazy_class", LazyModule.autoload?(:LazyClass))
LazyModule::LazyClass
assert_nil(LazyModule.autoload?(:LazyClass))
Expand All @@ -24,10 +32,57 @@ module EagerModule

describe "#eager_load!" do
it "eagerly loads autoload entries" do
assert EagerModule.autoload?(:EagerClass)
assert EagerModule.autoload?(:OtherEagerClass)
assert EagerModule.autoload?(:NestedEagerModule)

EagerModule.eager_load!

assert_nil(EagerModule.autoload?(:EagerClass))
assert_nil(EagerModule.autoload?(:OtherEagerClass))
assert_nil(EagerModule.autoload?(:NestedEagerModule))
assert_nil(EagerModule::NestedEagerModule.autoload?(:NestedEagerClass))
assert EagerModule::NestedEagerModule::NestedEagerClass
end
end


describe "warning in production" do
before do
@prev_env = ENV.to_hash
ENV.update("HANAMI_ENV" => "production")
end

after do
ENV.update(@prev_env)
end

it "emits a warning when not eager-loading" do
stdout, stderr = capture_io do
GraphQL.ensure_eager_load!
end

assert_equal "", stdout
expected_warning = "GraphQL-Ruby thinks this is a production deployment but didn't eager-load its constants. Address this by:

- Calling `GraphQL.eager_load!` in a production-only initializer or setup hook
- Assign `GraphQL.env = \"...\"` to something _other_ than `\"production\"` (for example, `GraphQL.env = \"development\"`)

More details: https://graphql-ruby.org/schema/definition#production-considerations
"
assert_equal expected_warning, stderr
end

it "silences the warning when GraphQL.env is assigned" do
prev_env = GraphQL.env
GraphQL.env = "staging"
stdout, stderr = capture_io do
GraphQL.ensure_eager_load!
end
assert_equal "", stdout
assert_equal "", stderr
ensure
GraphQL.env = prev_env
end
end
end
Loading