lighthouse-graphql 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -1
- data/README.md +11 -3
- data/docs/best-practices.md +16 -6
- data/docs/configuration.md +7 -6
- data/docs/directives.md +31 -6
- data/docs/events.md +3 -1
- data/docs/federation.md +3 -2
- data/docs/getting-started.md +52 -14
- data/docs/motivation.md +3 -2
- data/docs/resolvers.md +93 -19
- data/docs/security.md +210 -0
- data/docs/subscriptions.md +60 -37
- data/lib/generators/lighthouse/base_classes.rb +87 -0
- data/lib/generators/lighthouse/class_generator.rb +19 -8
- data/lib/generators/lighthouse/install/install_generator.rb +23 -6
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +7 -6
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +14 -2
- data/lib/generators/lighthouse/mutation_generator.rb +5 -4
- data/lib/generators/lighthouse/query_generator.rb +5 -4
- data/lib/generators/lighthouse/subscription_generator.rb +21 -16
- data/lib/lighthouse/complexity.rb +174 -0
- data/lib/lighthouse/complexity_analyzer.rb +56 -0
- data/lib/lighthouse/config.rb +4 -3
- data/lib/lighthouse/directive_resolvers/convention_resolver.rb +14 -4
- data/lib/lighthouse/directive_resolvers/convert_empty_strings_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +12 -4
- data/lib/lighthouse/directives/registry.rb +1 -0
- data/lib/lighthouse/events/execution_trace.rb +2 -1
- data/lib/lighthouse/events/lifecycle.rb +3 -1
- data/lib/lighthouse/field_resolver_stack.rb +8 -7
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/rb_lighthouse.rb +3 -0
- data/lib/lighthouse/resolver_bases.rb +61 -0
- data/lib/lighthouse/schema_factory.rb +0 -1
- data/lib/lighthouse/schema_generator.rb +15 -7
- data/lib/lighthouse/sdl_loader.rb +0 -2
- data/lib/lighthouse/support/model_resolver.rb +1 -1
- data/sig/lighthouse/graphql.rbs +29 -0
- metadata +6 -1
|
@@ -2,9 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
# Executable schema built by Lighthouse from the SDL in <%= sdl_folder %>/. The
|
|
4
4
|
# Dataloader batches relationship loads (@hasMany, @belongsTo, ...) to avoid N+1.
|
|
5
|
+
# base_types names the base classes: every class a Query, Mutation or Subscription
|
|
6
|
+
# field resolves to must inherit from its kind's, and a Subscription field with no
|
|
7
|
+
# class of its own resolves with the subscription one.
|
|
5
8
|
<%= schema_name %> = Lighthouse::GraphQL::RbLightHouse.get_schema(
|
|
6
9
|
options: {
|
|
7
|
-
using: { GraphQL::Dataloader => {} }
|
|
8
|
-
|
|
10
|
+
using: { GraphQL::Dataloader => {} },
|
|
11
|
+
base_types: {
|
|
12
|
+
resolver: <%= base_class_constant(:queries) %>,
|
|
13
|
+
mutation: <%= base_class_constant(:mutations) %>,
|
|
14
|
+
subscription: <%= base_class_constant(:subscriptions) %>
|
|
15
|
+
# object: Types::BaseObject, ... # graphql-ruby's base classes of the SDL's types
|
|
16
|
+
}
|
|
9
17
|
}
|
|
10
18
|
)
|
|
19
|
+
|
|
20
|
+
# Refuse expensive queries before they run (see the gem's docs/security.md):
|
|
21
|
+
# <%= schema_name %>.max_complexity(300)
|
|
22
|
+
# <%= schema_name %>.max_depth(12)
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'class_generator'
|
|
4
4
|
|
|
5
|
-
# `rails generate lighthouse:mutation PublishPost` — a
|
|
6
|
-
# that `Mutation.publishPost`
|
|
5
|
+
# `rails generate lighthouse:mutation PublishPost` — the class (a
|
|
6
|
+
# Mutations::BaseMutation, so a GraphQL::Schema::Mutation) that `Mutation.publishPost`
|
|
7
|
+
# resolves to by convention.
|
|
7
8
|
class Lighthouse::Generators::MutationGenerator < Lighthouse::Generators::ClassGenerator
|
|
8
|
-
desc 'Generate a
|
|
9
|
+
desc 'Generate the class of a Mutation field (a Mutations::BaseMutation) in app/graphql/mutations.'
|
|
9
10
|
|
|
10
11
|
private
|
|
11
12
|
|
|
@@ -16,7 +17,7 @@ class Lighthouse::Generators::MutationGenerator < Lighthouse::Generators::ClassG
|
|
|
16
17
|
# Resolves Mutation.#{class_basename.camelize(:lower)}. Keyword arguments are the field's
|
|
17
18
|
# arguments in snake_case; `object` and `context` are available. Raise
|
|
18
19
|
# GraphQL::ExecutionError for failures the client should see.
|
|
19
|
-
class #{class_basename} <
|
|
20
|
+
class #{class_basename} < #{superclass_name}
|
|
20
21
|
def resolve(**args)
|
|
21
22
|
raise NotImplementedError
|
|
22
23
|
end
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'class_generator'
|
|
4
4
|
|
|
5
|
-
# `rails generate lighthouse:query LatestPosts` — a
|
|
6
|
-
# that `Query.latestPosts`
|
|
5
|
+
# `rails generate lighthouse:query LatestPosts` — the resolver (a
|
|
6
|
+
# Resolvers::BaseResolver, so a GraphQL::Schema::Resolver) that `Query.latestPosts`
|
|
7
|
+
# resolves to by convention.
|
|
7
8
|
class Lighthouse::Generators::QueryGenerator < Lighthouse::Generators::ClassGenerator
|
|
8
|
-
desc 'Generate a
|
|
9
|
+
desc 'Generate the resolver of a Query field (a Resolvers::BaseResolver) in app/graphql/resolvers.'
|
|
9
10
|
|
|
10
11
|
private
|
|
11
12
|
|
|
@@ -15,7 +16,7 @@ class Lighthouse::Generators::QueryGenerator < Lighthouse::Generators::ClassGene
|
|
|
15
16
|
<<~RUBY
|
|
16
17
|
# Resolves Query.#{class_basename.camelize(:lower)}. Keyword arguments are the field's
|
|
17
18
|
# arguments in snake_case; `object` and `context` are available.
|
|
18
|
-
class #{class_basename} <
|
|
19
|
+
class #{class_basename} < #{superclass_name}
|
|
19
20
|
def resolve(**args)
|
|
20
21
|
raise NotImplementedError
|
|
21
22
|
end
|
|
@@ -2,22 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'class_generator'
|
|
4
4
|
|
|
5
|
-
# `rails generate lighthouse:subscription PostUpdated` — a
|
|
6
|
-
# GraphQL::Schema::Subscription that
|
|
7
|
-
#
|
|
8
|
-
# Lighthouse::GraphQL::Subscription
|
|
5
|
+
# `rails generate lighthouse:subscription PostUpdated` — the class (a
|
|
6
|
+
# Subscriptions::BaseSubscription, so a GraphQL::Schema::Subscription) that
|
|
7
|
+
# `Subscription.postUpdated` resolves to by convention. The base Lighthouse writes
|
|
8
|
+
# includes Lighthouse::GraphQL::Subscription, so the class gets PHP Lighthouse's
|
|
9
|
+
# authorize?/filter?/resolve_payload hooks; under a base without the concern, it
|
|
10
|
+
# gets graphql-ruby's authorized?/update instead.
|
|
9
11
|
class Lighthouse::Generators::SubscriptionGenerator < Lighthouse::Generators::ClassGenerator
|
|
10
|
-
desc 'Generate a
|
|
11
|
-
|
|
12
|
-
class_option :lighthouse, type: :boolean, default: false,
|
|
13
|
-
desc: "Include Lighthouse::GraphQL::Subscription: PHP Lighthouse's authorize?/filter?/resolve_payload hooks"
|
|
12
|
+
desc 'Generate the class of a Subscription field (a Subscriptions::BaseSubscription) in app/graphql/subscriptions.'
|
|
14
13
|
|
|
15
14
|
private
|
|
16
15
|
|
|
17
16
|
def kind = :subscriptions
|
|
18
17
|
|
|
19
18
|
def body
|
|
20
|
-
|
|
19
|
+
lighthouse_base? ? lighthouse_body : plain_body
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Does the namespace's base class include Lighthouse's concern?
|
|
23
|
+
def lighthouse_base?
|
|
24
|
+
path = File.join(destination_root, base_class_path(kind, namespace_name))
|
|
25
|
+
File.exist?(path) && File.read(path).include?('Lighthouse::GraphQL::Subscription')
|
|
21
26
|
end
|
|
22
27
|
|
|
23
28
|
def plain_body
|
|
@@ -25,7 +30,7 @@ class Lighthouse::Generators::SubscriptionGenerator < Lighthouse::Generators::Cl
|
|
|
25
30
|
# Resolves Subscription.#{field_name}. Keyword arguments are the field's arguments in
|
|
26
31
|
# snake_case; `object` and `context` are available. `context` is the subscriber's:
|
|
27
32
|
# graphql-ruby stores it with the subscription and replays it for every update.
|
|
28
|
-
class #{class_basename} <
|
|
33
|
+
class #{class_basename} < #{superclass_name}
|
|
29
34
|
# Runs for the subscription request and again for every update. Raise a
|
|
30
35
|
# GraphQL::ExecutionError to refuse with a message (false refuses silently).
|
|
31
36
|
def authorized?(**args)
|
|
@@ -62,12 +67,12 @@ class Lighthouse::Generators::SubscriptionGenerator < Lighthouse::Generators::Cl
|
|
|
62
67
|
|
|
63
68
|
def lighthouse_body
|
|
64
69
|
<<~RUBY
|
|
65
|
-
# Resolves Subscription.#{field_name}.
|
|
66
|
-
# hooks: every subscriber of the field gets every broadcast, and
|
|
67
|
-
# All hooks are optional; `object` and `context` (the
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
# Resolves Subscription.#{field_name}. #{superclass_name} includes Lighthouse::GraphQL::Subscription,
|
|
71
|
+
# PHP Lighthouse's hooks: every subscriber of the field gets every broadcast, and
|
|
72
|
+
# `filter?` decides. All hooks are optional; `object` and `context` (the
|
|
73
|
+
# subscriber's) are available. graphql-ruby's own `authorized?` or `update`,
|
|
74
|
+
# defined here, win over them.
|
|
75
|
+
class #{class_basename} < #{superclass_name}
|
|
71
76
|
# May this client subscribe? Keyword arguments are the field's arguments.
|
|
72
77
|
def authorize?(**args)
|
|
73
78
|
true
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# @complexity — what a field costs in a query's complexity score, the score
|
|
4
|
+
# graphql-ruby checks against the schema's `max_complexity` before it runs a
|
|
5
|
+
# query (GraphQL::Analysis::MaxQueryComplexity), as PHP Lighthouse does with
|
|
6
|
+
# webonyx's QueryComplexity rule.
|
|
7
|
+
#
|
|
8
|
+
# Lighthouse scores nothing itself: it gives fields their cost through
|
|
9
|
+
# graphql-ruby's `Field#complexity_for` hook, and graphql-ruby's analyzers add
|
|
10
|
+
# the costs up. A field costs:
|
|
11
|
+
#
|
|
12
|
+
# - With a bare @complexity, and on every paginated field (@paginate, and the
|
|
13
|
+
# relationship directives given `type:`), 1 plus its selection's cost once per
|
|
14
|
+
# item asked for (`first`, which defaults to the page size).
|
|
15
|
+
# - With @complexity(resolver: "Name"), the `complexity` the analyzer class
|
|
16
|
+
# declares when it includes Lighthouse::GraphQL::ComplexityAnalyzer; else as
|
|
17
|
+
# with a bare @complexity.
|
|
18
|
+
# - When a GraphQL::Schema::Resolver class resolves it (by convention, as
|
|
19
|
+
# `Resolvers::Search` resolves `Query.search`, or through @field), the
|
|
20
|
+
# `complexity` that class declares, as graphql-ruby gives `field ..., resolver:`.
|
|
21
|
+
# - Otherwise graphql-ruby's default: 1 plus its selection's cost.
|
|
22
|
+
#
|
|
23
|
+
# `resolver:` names a GraphQL::Analysis::Analyzer subclass, looked up in
|
|
24
|
+
# Config.namespaces[:analyzers] (`Analyzers::Name`). Lighthouse installs it on
|
|
25
|
+
# the schema, as graphql-ruby's `query_analyzer Analyzers::Name` would, so it
|
|
26
|
+
# runs on every query.
|
|
27
|
+
#
|
|
28
|
+
# Analyzer scores each query for BuildExtensionsResponse#query_complexity while
|
|
29
|
+
# something listens to that event.
|
|
30
|
+
module Lighthouse::GraphQL::Complexity
|
|
31
|
+
DEFINITION = 'directive @complexity(resolver: String) on FIELD_DEFINITION'
|
|
32
|
+
|
|
33
|
+
CONTEXT_KEY = :lighthouse_query_complexity
|
|
34
|
+
|
|
35
|
+
module_function
|
|
36
|
+
|
|
37
|
+
# Give the schema's fields their cost and install the analyzers @complexity
|
|
38
|
+
# names (see above). `root_kinds` maps the root types' names to :queries /
|
|
39
|
+
# :mutations / :subscriptions.
|
|
40
|
+
def apply(schema, root_kinds = {})
|
|
41
|
+
schema.types.each_value do |type|
|
|
42
|
+
next unless type.kind.object? || type.kind.interface?
|
|
43
|
+
|
|
44
|
+
type.fields.each_value do |field|
|
|
45
|
+
calculator = calculator_for(schema, type, field, root_kinds[type.graphql_name])
|
|
46
|
+
install(field, calculator) if calculator
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
schema
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# A query's score, once Analyzer has run on it; nil otherwise.
|
|
53
|
+
def query_complexity(query)
|
|
54
|
+
query.context[CONTEXT_KEY]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# graphql-ruby asks the field for its cost through `complexity_for`, which wins
|
|
58
|
+
# over a number or proc in `complexity` and over its own scaling of connections.
|
|
59
|
+
def install(field, calculator)
|
|
60
|
+
field.define_singleton_method(:complexity_for) do |child_complexity:, query:, lookahead:|
|
|
61
|
+
calculator.call(query.context, lookahead.arguments, child_complexity)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# A callable (context, args, child_complexity) -> cost, or nil to leave the
|
|
66
|
+
# field to graphql-ruby.
|
|
67
|
+
def calculator_for(schema, type, field, kind)
|
|
68
|
+
return unless field.ast_node
|
|
69
|
+
|
|
70
|
+
directives = field.ast_node.directives || []
|
|
71
|
+
directive = directives.find { |d| d.name == 'complexity' }
|
|
72
|
+
return directive_calculator(schema, directive, "#{type.graphql_name}.#{field.name}") if directive
|
|
73
|
+
return method(:per_item) if paginated?(directives)
|
|
74
|
+
|
|
75
|
+
resolver_class_calculator(field, directives, kind)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def directive_calculator(schema, directive, path)
|
|
79
|
+
name = Lighthouse::Support::DirectiveArgs.literal(directive, 'resolver')
|
|
80
|
+
return method(:per_item) if name.nil? || name.to_s.strip.empty?
|
|
81
|
+
|
|
82
|
+
analyzer = analyzer_class(name.to_s, path)
|
|
83
|
+
schema.query_analyzer(analyzer) unless schema.query_analyzers.include?(analyzer)
|
|
84
|
+
return method(:per_item) unless analyzer.include?(Lighthouse::GraphQL::ComplexityAnalyzer)
|
|
85
|
+
|
|
86
|
+
lambda do |context, args, child_complexity|
|
|
87
|
+
cost = analyzer.complexity
|
|
88
|
+
cost.nil? ? per_item(context, args, child_complexity) : own_cost(cost, context, args, child_complexity)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def analyzer_class(name, path)
|
|
93
|
+
namespaces = Lighthouse::Config.namespaces[:analyzers]
|
|
94
|
+
klass = Lighthouse::Support::ModelResolver.constantize(name, namespaces: namespaces)
|
|
95
|
+
unless klass
|
|
96
|
+
raise Lighthouse::DefinitionError,
|
|
97
|
+
"@complexity on #{path}: no analyzer #{name} (looked up in #{Array(namespaces).join(', ')})"
|
|
98
|
+
end
|
|
99
|
+
return klass if klass.is_a?(Class) && klass < ::GraphQL::Analysis::Analyzer
|
|
100
|
+
|
|
101
|
+
raise Lighthouse::DefinitionError, "@complexity on #{path}: #{klass} must inherit from GraphQL::Analysis::Analyzer"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The field and its selection once per item asked for: PHP Lighthouse's
|
|
105
|
+
# default, and what a page of `first` items costs.
|
|
106
|
+
def per_item(_context, args, child_complexity)
|
|
107
|
+
1 + (child_complexity * (args[:first] || 1))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# graphql-ruby's default cost: the field, plus its selection.
|
|
111
|
+
def default_cost(child_complexity)
|
|
112
|
+
1 + child_complexity
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# A cost as graphql-ruby's Field#complexity takes it: a number, added to the
|
|
116
|
+
# selection's, or a proc (context, args, child_complexity).
|
|
117
|
+
def own_cost(cost, context, args, child_complexity)
|
|
118
|
+
cost.respond_to?(:call) ? cost.call(context, args, child_complexity) : cost + child_complexity
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def paginated?(directives)
|
|
122
|
+
relations = Lighthouse::GraphQL::Directives::PaginationManipulator::RELATION_DIRECTIVES
|
|
123
|
+
directives.any? do |d|
|
|
124
|
+
d.name == 'paginate' || (relations.include?(d.name) && Lighthouse::Support::DirectiveArgs.literal(d, 'type'))
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The `complexity` of the GraphQL::Schema::Resolver class that resolves the
|
|
129
|
+
# field, by convention or through @field; nil when no such class can. The
|
|
130
|
+
# class is looked up when a query is scored, so code reloading works.
|
|
131
|
+
def resolver_class_calculator(field, directives, kind)
|
|
132
|
+
lookup = resolver_class_lookup(field, directives, kind)
|
|
133
|
+
return unless lookup
|
|
134
|
+
|
|
135
|
+
lambda do |context, args, child_complexity|
|
|
136
|
+
klass = lookup.call
|
|
137
|
+
next default_cost(child_complexity) unless Lighthouse::GraphQL::ResolverDispatch.graphql_resolver?(klass)
|
|
138
|
+
|
|
139
|
+
own_cost(klass.complexity, context, args, child_complexity)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def resolver_class_lookup(field, directives, kind)
|
|
144
|
+
directive = directives.find { |d| d.name == 'field' }
|
|
145
|
+
return field_class_lookup(directive, kind) if directive
|
|
146
|
+
return unless %i[queries mutations].include?(kind) && !Lighthouse::GraphQL::FieldResolverStack.resolving_directive?(directives)
|
|
147
|
+
|
|
148
|
+
class_name = Lighthouse::GraphQL::DirectiveResolvers::ConventionResolver.class_name_for(field)
|
|
149
|
+
-> { Lighthouse::Support::ModelResolver.constantize(class_name, namespaces: Lighthouse::Config.namespaces[kind], fallback: false) }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# The class @field names, looked up as @field looks it up.
|
|
153
|
+
def field_class_lookup(directive, kind)
|
|
154
|
+
class_name = %w[resolver class].filter_map { |name| Lighthouse::Support::DirectiveArgs.literal(directive, name) }.first
|
|
155
|
+
return unless class_name
|
|
156
|
+
|
|
157
|
+
namespaces = kind ? Lighthouse::Config.namespaces[kind] : Lighthouse::Config.resolver_namespaces
|
|
158
|
+
-> { Lighthouse::Support::ModelResolver.constantize(class_name, namespaces: namespaces) }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Scores each query as graphql-ruby's MaxQueryComplexity scores it against
|
|
162
|
+
# `max_complexity`, for BuildExtensionsResponse#query_complexity. Runs only
|
|
163
|
+
# while something listens to that event.
|
|
164
|
+
class Analyzer < ::GraphQL::Analysis::QueryComplexity
|
|
165
|
+
def analyze?
|
|
166
|
+
Lighthouse::Events.listening?(Lighthouse::Events::BuildExtensionsResponse)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def result
|
|
170
|
+
query.context[CONTEXT_KEY] = max_possible_complexity
|
|
171
|
+
nil
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/concern'
|
|
4
|
+
|
|
5
|
+
# A concern for a graphql-ruby analyzer that @complexity(resolver:) names. Its
|
|
6
|
+
# one method, `complexity`, gives the fields naming the class their cost, as
|
|
7
|
+
# graphql-ruby's `complexity` gives a field or a GraphQL::Schema::Resolver one:
|
|
8
|
+
#
|
|
9
|
+
# class Analyzers::TagPostsComplexity < GraphQL::Analysis::QueryComplexity
|
|
10
|
+
# include Lighthouse::GraphQL::ComplexityAnalyzer
|
|
11
|
+
#
|
|
12
|
+
# complexity ->(_context, _args, child_complexity) { 1 + (child_complexity * 25) }
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# A number is added to the cost of the field's selection; a proc is called with
|
|
16
|
+
# (context, args, child_complexity) and returns the whole cost. Without
|
|
17
|
+
# `complexity`, the field costs what a bare @complexity gives.
|
|
18
|
+
#
|
|
19
|
+
# The class stays an analyzer, installed on the schema like any class @complexity
|
|
20
|
+
# names: a `result` of its own runs on every query, in graphql-ruby's analysis.
|
|
21
|
+
# Without one it only gives costs, and graphql-ruby skips it.
|
|
22
|
+
module Lighthouse::GraphQL::ComplexityAnalyzer
|
|
23
|
+
extend ActiveSupport::Concern
|
|
24
|
+
|
|
25
|
+
class_methods do
|
|
26
|
+
# Set the cost (a number or a proc taking context, args and child_complexity),
|
|
27
|
+
# or read it: the class's own, else its superclass's, else nil.
|
|
28
|
+
def complexity(new_complexity = nil)
|
|
29
|
+
return inherited_complexity if new_complexity.nil?
|
|
30
|
+
|
|
31
|
+
unless new_complexity.is_a?(Numeric) || (new_complexity.is_a?(Proc) && new_complexity.parameters.size == 3)
|
|
32
|
+
raise ArgumentError, "#{name}.complexity takes a number or a proc of (context, args, child_complexity), " \
|
|
33
|
+
"got #{new_complexity.inspect}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
@complexity = new_complexity
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def inherited_complexity
|
|
42
|
+
return @complexity if defined?(@complexity)
|
|
43
|
+
|
|
44
|
+
superclass.respond_to?(:complexity) ? superclass.complexity : nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Run in graphql-ruby's analysis only when the class has a `result` of its own.
|
|
49
|
+
def analyze?
|
|
50
|
+
self.class.instance_method(:result).owner != Lighthouse::GraphQL::ComplexityAnalyzer && super
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def result
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
end
|
data/lib/lighthouse/config.rb
CHANGED
|
@@ -12,14 +12,15 @@ module Lighthouse
|
|
|
12
12
|
# Where each kind of class a schema refers to by name is looked up. Query and
|
|
13
13
|
# mutation fields resolve to GraphQL::Schema::Resolver / Mutation classes,
|
|
14
14
|
# builders compose onto a relation, validators check arguments: distinct
|
|
15
|
-
# contracts, so distinct namespaces (app/graphql/
|
|
15
|
+
# contracts, so distinct namespaces (app/graphql/resolvers, .../mutations, ...).
|
|
16
16
|
DEFAULT_NAMESPACES = {
|
|
17
|
-
queries: ['
|
|
17
|
+
queries: ['Resolvers'],
|
|
18
18
|
mutations: ['Mutations'],
|
|
19
19
|
builders: ['Builders'],
|
|
20
20
|
validators: ['Validators'],
|
|
21
21
|
events: ['Events'],
|
|
22
|
-
subscriptions: ['Subscriptions']
|
|
22
|
+
subscriptions: ['Subscriptions'],
|
|
23
|
+
analyzers: ['Analyzers']
|
|
23
24
|
}.freeze
|
|
24
25
|
|
|
25
26
|
# Every setting reset! forgets, so the next read falls back to its default.
|
|
@@ -4,14 +4,17 @@ require_relative 'base'
|
|
|
4
4
|
|
|
5
5
|
# Resolves a Query or Mutation field that has no resolving directive by
|
|
6
6
|
# convention, as PHP Lighthouse does: `Query.latestPosts` resolves to
|
|
7
|
-
# `
|
|
8
|
-
#
|
|
7
|
+
# `Resolvers::LatestPosts` (a GraphQL::Schema::Resolver, by graphql-ruby's
|
|
8
|
+
# convention a Resolvers::BaseResolver), `Mutation.publishPost` to
|
|
9
|
+
# `Mutations::PublishPost` (a GraphQL::Schema::Mutation), looked up in
|
|
9
10
|
# Config.namespaces[:queries] / [:mutations] at call time so code reloading
|
|
10
11
|
# keeps working. Not a directive: SchemaGenerator installs it.
|
|
11
12
|
class Lighthouse::GraphQL::DirectiveResolvers::ConventionResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
12
|
-
|
|
13
|
+
# `base`: the class the schema's base_types require the class to inherit from.
|
|
14
|
+
def initialize(field_def, type_def, kind, base = nil)
|
|
13
15
|
super(field_def, type_def, nil)
|
|
14
16
|
@kind = kind
|
|
17
|
+
@base = base
|
|
15
18
|
end
|
|
16
19
|
|
|
17
20
|
def define_resolver(target_type_class)
|
|
@@ -31,6 +34,12 @@ class Lighthouse::GraphQL::DirectiveResolvers::ConventionResolver < Lighthouse::
|
|
|
31
34
|
ROOT_KINDS[type.respond_to?(:graphql_name) ? type.graphql_name : type.to_s]
|
|
32
35
|
end
|
|
33
36
|
|
|
37
|
+
# The class a root field resolves to, without its namespace: `LatestPosts`
|
|
38
|
+
# for `latestPosts`.
|
|
39
|
+
def self.class_name_for(field_def)
|
|
40
|
+
field_def.name.to_s.camelize
|
|
41
|
+
end
|
|
42
|
+
|
|
34
43
|
private
|
|
35
44
|
|
|
36
45
|
def resolve_by_convention(inner, obj, args, ctx)
|
|
@@ -43,6 +52,7 @@ class Lighthouse::GraphQL::DirectiveResolvers::ConventionResolver < Lighthouse::
|
|
|
43
52
|
"#{klass} resolves #{location}, so it must inherit from GraphQL::Schema::Resolver " \
|
|
44
53
|
'(or GraphQL::Schema::Mutation).'
|
|
45
54
|
end
|
|
55
|
+
Lighthouse::GraphQL::ResolverBases.check!(klass, @base, location)
|
|
46
56
|
|
|
47
57
|
Lighthouse::GraphQL::ResolverDispatch.call(klass, @field_def, obj, args, ctx)
|
|
48
58
|
end
|
|
@@ -61,7 +71,7 @@ class Lighthouse::GraphQL::DirectiveResolvers::ConventionResolver < Lighthouse::
|
|
|
61
71
|
end
|
|
62
72
|
|
|
63
73
|
def class_name
|
|
64
|
-
@class_name ||= @field_def
|
|
74
|
+
@class_name ||= self.class.class_name_for(@field_def)
|
|
65
75
|
end
|
|
66
76
|
|
|
67
77
|
def location
|
|
@@ -19,7 +19,7 @@ require_relative 'base'
|
|
|
19
19
|
# carried the directive.
|
|
20
20
|
#
|
|
21
21
|
# The conversion is a wrapper around the field's resolver (outermost, order
|
|
22
|
-
# 6), so every kind of resolver — directives, @field,
|
|
22
|
+
# 6), so every kind of resolver — directives, @field, Resolvers::/Mutations::
|
|
23
23
|
# classes, the default reader — and @validator see the converted arguments,
|
|
24
24
|
# and other fields sharing an input type are unaffected. SchemaGenerator
|
|
25
25
|
# installs it too, without the directive on the field, when an argument or
|
|
@@ -8,8 +8,9 @@ require_relative 'base'
|
|
|
8
8
|
# Config.namespaces[:subscriptions] at call time so code reloading keeps
|
|
9
9
|
# working. The class is a GraphQL::Schema::Subscription, which may include
|
|
10
10
|
# Lighthouse::GraphQL::Subscription for PHP Lighthouse's hooks. A field
|
|
11
|
-
# with no class still works: it
|
|
12
|
-
#
|
|
11
|
+
# with no class still works: it resolves with the schema's base_types[:subscription]
|
|
12
|
+
# when given, else with Lighthouse::GraphQL::Subscription::Default, whose update
|
|
13
|
+
# is the broadcast root, which is all `@broadcast` needs.
|
|
13
14
|
#
|
|
14
15
|
# graphql-ruby registers a subscription through the class its field was
|
|
15
16
|
# built from, which a field built from SDL has not. The field is therefore
|
|
@@ -38,20 +39,27 @@ class Lighthouse::GraphQL::DirectiveResolvers::SubscriptionDirectiveResolver < L
|
|
|
38
39
|
attr_reader :field_def
|
|
39
40
|
|
|
40
41
|
# The class this field stands for: the one @subscription names, else the
|
|
41
|
-
# conventional one, else the default.
|
|
42
|
+
# conventional one, else the schema's base subscription, else the default.
|
|
42
43
|
def subscription_class
|
|
43
44
|
klass = find_class
|
|
44
|
-
return Lighthouse::GraphQL::Subscription::Default unless klass
|
|
45
|
+
return base || Lighthouse::GraphQL::Subscription::Default unless klass
|
|
45
46
|
|
|
46
47
|
unless klass.is_a?(Class) && klass < ::GraphQL::Schema::Subscription
|
|
47
48
|
raise Lighthouse::DefinitionError, "#{klass} resolves #{location}, so it must inherit from GraphQL::Schema::Subscription."
|
|
48
49
|
end
|
|
49
50
|
|
|
51
|
+
Lighthouse::GraphQL::ResolverBases.check!(klass, base, location)
|
|
52
|
+
|
|
50
53
|
klass
|
|
51
54
|
end
|
|
52
55
|
|
|
53
56
|
private
|
|
54
57
|
|
|
58
|
+
# The schema's base_types[:subscription], or nil.
|
|
59
|
+
def base
|
|
60
|
+
(@opts[:bases] || {})[:subscriptions]
|
|
61
|
+
end
|
|
62
|
+
|
|
55
63
|
def find_class
|
|
56
64
|
namespaces = Lighthouse::Config.namespaces[:subscriptions]
|
|
57
65
|
named = directive_arg('class') if @directive_node
|
|
@@ -218,6 +218,7 @@ class Lighthouse::DirectiveRegistry
|
|
|
218
218
|
register_definition('cacheControl', Lighthouse::GraphQL::CacheControl::DEFINITION)
|
|
219
219
|
Lighthouse::GraphQL::CacheControl::TYPE_DEFINITIONS.each { |name, sdl| register_type_definition(name, sdl) }
|
|
220
220
|
register_definition('feature', Lighthouse::GraphQL::FeatureDirective::DEFINITION)
|
|
221
|
+
register_definition('complexity', Lighthouse::GraphQL::Complexity::DEFINITION)
|
|
221
222
|
Lighthouse::GraphQL::FeatureDirective::TYPE_DEFINITIONS.each { |name, sdl| register_type_definition(name, sdl) }
|
|
222
223
|
Lighthouse::GraphQL::Directives::WhereConditionsDirective::DEFINITIONS.each do |name, definition|
|
|
223
224
|
register_definition(name, definition)
|
|
@@ -33,7 +33,8 @@ module Lighthouse::Events::ExecutionTrace
|
|
|
33
33
|
result = query.result_values
|
|
34
34
|
return unless result.is_a?(Hash)
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
complexity = Lighthouse::GraphQL::Complexity.query_complexity(query)
|
|
37
|
+
merge_extensions(result, Lighthouse::Events.dispatch(Lighthouse::Events::BuildExtensionsResponse.new(result, complexity)))
|
|
37
38
|
manipulate = Lighthouse::Events::ManipulateResult.new(result)
|
|
38
39
|
Lighthouse::Events.dispatch(manipulate)
|
|
39
40
|
result.replace(manipulate.result) unless manipulate.result.equal?(result)
|
|
@@ -71,7 +71,9 @@ module Lighthouse::Events
|
|
|
71
71
|
|
|
72
72
|
# A query ran. A listener may return an Events::ExtensionsResponse (or an
|
|
73
73
|
# Array of them), which is merged into the result's "extensions".
|
|
74
|
-
# `query_complexity` is
|
|
74
|
+
# `query_complexity` is the query's complexity score, as graphql-ruby checks it
|
|
75
|
+
# against max_complexity (see GraphQL::Complexity); nil for a query that
|
|
76
|
+
# failed validation, which is never scored.
|
|
75
77
|
class BuildExtensionsResponse < Event
|
|
76
78
|
attr_reader :result, :query_complexity
|
|
77
79
|
|
|
@@ -3,17 +3,18 @@
|
|
|
3
3
|
# Install a field's resolvers innermost first (see
|
|
4
4
|
# DirectiveRegistry#resolution_order), so each wrapper decorates the ones
|
|
5
5
|
# before it. Besides its directives, a field gets the conventional
|
|
6
|
-
#
|
|
6
|
+
# Resolvers::/Mutations:: resolver when it is a root field with no resolving
|
|
7
7
|
# directive, and argument validation when an input type it takes carries
|
|
8
8
|
# @validator. The registry replaces a hardcoded `case dnode.name`: adding a
|
|
9
9
|
# directive is "register a class", not "edit the generator".
|
|
10
10
|
module Lighthouse::GraphQL::FieldResolverStack
|
|
11
11
|
module_function
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
# `bases`: { root kind => base class } from the schema's base_types (ResolverBases).
|
|
14
|
+
def apply(type, field_def, directives, root_kind, bases: {})
|
|
15
|
+
opts = { root_kind: root_kind, bases: bases }
|
|
15
16
|
steps = directive_steps(type, field_def, directives, opts)
|
|
16
|
-
steps << convention_step(type, field_def, root_kind) if root_kind && !resolving_directive?(directives)
|
|
17
|
+
steps << convention_step(type, field_def, root_kind, bases) if root_kind && !resolving_directive?(directives)
|
|
17
18
|
steps << validator_step(type, field_def, opts) if input_validation?(field_def, directives)
|
|
18
19
|
steps << empty_strings_step(type, field_def, opts) if empty_string_conversion?(field_def, directives)
|
|
19
20
|
|
|
@@ -38,13 +39,13 @@ module Lighthouse::GraphQL::FieldResolverStack
|
|
|
38
39
|
directives.any? { |d| registry.resolution_class(d.name) && !registry.wrapper?(d.name) }
|
|
39
40
|
end
|
|
40
41
|
|
|
41
|
-
def convention_step(type, field_def, root_kind)
|
|
42
|
+
def convention_step(type, field_def, root_kind, bases = {})
|
|
42
43
|
resolvers = Lighthouse::GraphQL::DirectiveResolvers
|
|
43
44
|
resolver =
|
|
44
45
|
if root_kind == :subscriptions
|
|
45
|
-
resolvers::SubscriptionDirectiveResolver.new(field_def, type, nil, root_kind: root_kind)
|
|
46
|
+
resolvers::SubscriptionDirectiveResolver.new(field_def, type, nil, root_kind: root_kind, bases: bases)
|
|
46
47
|
else
|
|
47
|
-
resolvers::ConventionResolver.new(field_def, type, root_kind)
|
|
48
|
+
resolvers::ConventionResolver.new(field_def, type, root_kind, bases[root_kind])
|
|
48
49
|
end
|
|
49
50
|
[0, -> { resolver.define_resolver(type) }]
|
|
50
51
|
end
|
|
@@ -44,6 +44,7 @@ require_relative 'namespaces'
|
|
|
44
44
|
events/lifecycle
|
|
45
45
|
events/execution_trace
|
|
46
46
|
events/schema_build
|
|
47
|
+
resolver_bases
|
|
47
48
|
schema_generator
|
|
48
49
|
schema_cache
|
|
49
50
|
persisted_queries
|
|
@@ -55,6 +56,8 @@ require_relative 'namespaces'
|
|
|
55
56
|
caching
|
|
56
57
|
cache_control
|
|
57
58
|
feature_directive
|
|
59
|
+
complexity
|
|
60
|
+
complexity_analyzer
|
|
58
61
|
broadcasting
|
|
59
62
|
subscription
|
|
60
63
|
pagination
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The base classes of the classes root fields resolve to, given to `get_schema`
|
|
4
|
+
# beside graphql-ruby's own base types:
|
|
5
|
+
#
|
|
6
|
+
# Lighthouse::GraphQL::RbLightHouse.get_schema(options: { base_types: {
|
|
7
|
+
# resolver: Resolvers::BaseResolver, mutation: Mutations::BaseMutation,
|
|
8
|
+
# subscription: Subscriptions::BaseSubscription, object: Types::BaseObject
|
|
9
|
+
# } })
|
|
10
|
+
#
|
|
11
|
+
# graphql-ruby's `from_definition` knows only the SDL's own kinds (object,
|
|
12
|
+
# interface, union, scalar, enum, input_object), so these three are taken out
|
|
13
|
+
# before it sees the rest. Each one given:
|
|
14
|
+
#
|
|
15
|
+
# - is the class every class of its kind must inherit from: a Query, Mutation or
|
|
16
|
+
# Subscription field's class, found by convention or through
|
|
17
|
+
# @subscription(class:), that does not raises Lighthouse::DefinitionError when
|
|
18
|
+
# it is first used, so none misses what the base shares;
|
|
19
|
+
# - for subscriptions, also resolves a Subscription field that has no class of
|
|
20
|
+
# its own, instead of Lighthouse::GraphQL::Subscription::Default.
|
|
21
|
+
module Lighthouse::GraphQL::ResolverBases
|
|
22
|
+
# base_types key => the graphql-ruby class the base must inherit from.
|
|
23
|
+
GRAPHQL_CLASSES = {
|
|
24
|
+
resolver: ::GraphQL::Schema::Resolver,
|
|
25
|
+
mutation: ::GraphQL::Schema::Mutation,
|
|
26
|
+
subscription: ::GraphQL::Schema::Subscription
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
# Root kind => base_types key.
|
|
30
|
+
KEYS = { queries: :resolver, mutations: :mutation, subscriptions: :subscription }.freeze
|
|
31
|
+
|
|
32
|
+
module_function
|
|
33
|
+
|
|
34
|
+
# Take the three keys out of opts[:base_types] (the caller's Hash is not
|
|
35
|
+
# changed) and return { root kind => base class } for those given.
|
|
36
|
+
def extract!(opts)
|
|
37
|
+
base_types = opts[:base_types]
|
|
38
|
+
return {} unless base_types.is_a?(Hash)
|
|
39
|
+
|
|
40
|
+
given = base_types.slice(*GRAPHQL_CLASSES.keys).compact
|
|
41
|
+
opts[:base_types] = base_types.except(*GRAPHQL_CLASSES.keys)
|
|
42
|
+
given.each { |key, klass| validate!(key, klass) }
|
|
43
|
+
KEYS.filter_map { |kind, key| [kind, given[key]] if given[key] }.to_h
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def validate!(key, klass)
|
|
47
|
+
expected = GRAPHQL_CLASSES.fetch(key)
|
|
48
|
+
return if klass.is_a?(Class) && klass < expected
|
|
49
|
+
|
|
50
|
+
raise Lighthouse::DefinitionError, "base_types[:#{key}] must be a subclass of #{expected}, not #{klass.inspect}."
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Raise unless `klass`, which resolves the field at `location`, inherits from
|
|
54
|
+
# `base` (nil: no base was given).
|
|
55
|
+
def check!(klass, base, location)
|
|
56
|
+
return if base.nil? || klass <= base
|
|
57
|
+
|
|
58
|
+
raise Lighthouse::DefinitionError,
|
|
59
|
+
"#{klass} resolves #{location}, so it must inherit from #{base}, the schema's base class for it (base_types)."
|
|
60
|
+
end
|
|
61
|
+
end
|