lighthouse-graphql 0.3.0 → 0.5.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 +182 -1
- data/README.md +18 -7
- data/docs/best-practices.md +16 -6
- data/docs/configuration.md +33 -8
- data/docs/directives.md +318 -10
- data/docs/events.md +3 -1
- data/docs/federation.md +3 -2
- data/docs/generators.md +82 -0
- data/docs/getting-started.md +66 -32
- data/docs/motivation.md +3 -2
- data/docs/relationships.md +42 -0
- data/docs/relay.md +137 -0
- data/docs/resolvers.md +338 -25
- data/docs/security.md +227 -0
- data/docs/subscriptions.md +60 -37
- data/docs/types.md +219 -0
- data/docs/uploads.md +80 -0
- data/lib/generators/lighthouse/base_classes.rb +70 -0
- data/lib/generators/lighthouse/class_generator.rb +19 -8
- data/lib/generators/lighthouse/install/install_generator.rb +96 -20
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +19 -10
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +21 -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/async_mutation_job.rb +17 -0
- data/lib/lighthouse/complexity.rb +175 -0
- data/lib/lighthouse/complexity_analyzer.rb +56 -0
- data/lib/lighthouse/config.rb +16 -4
- data/lib/lighthouse/controller.rb +27 -9
- data/lib/lighthouse/dataloader_sources/preload_loader.rb +16 -0
- data/lib/lighthouse/dataloader_sources/relation_counts_loader.rb +46 -0
- data/lib/lighthouse/default_resolve_dispatcher.rb +26 -0
- data/lib/lighthouse/directive_resolvers/all_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/argument_binding.rb +61 -0
- data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +220 -0
- data/lib/lighthouse/directive_resolvers/association_resolvers.rb +2 -2
- data/lib/lighthouse/directive_resolvers/async_resolver.rb +90 -0
- data/lib/lighthouse/directive_resolvers/base.rb +63 -14
- data/lib/lighthouse/directive_resolvers/builder_applier.rb +89 -19
- data/lib/lighthouse/directive_resolvers/can_resolvers.rb +1 -1
- data/lib/lighthouse/directive_resolvers/convention_resolver.rb +16 -5
- data/lib/lighthouse/directive_resolvers/convert_empty_strings_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/count_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/field_resolver.rb +35 -7
- data/lib/lighthouse/directive_resolvers/first_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +57 -0
- data/lib/lighthouse/directive_resolvers/lazy_load_resolver.rb +61 -0
- data/lib/lighthouse/directive_resolvers/limit_resolver.rb +59 -0
- data/lib/lighthouse/directive_resolvers/method_resolver.rb +38 -3
- data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/namespaced_resolver.rb +31 -0
- data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/resolution.rb +19 -4
- data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +14 -5
- data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +77 -0
- data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +81 -0
- data/lib/lighthouse/directive_resolvers/with_count_resolver.rb +43 -0
- data/lib/lighthouse/directive_resolvers/with_resolver.rb +42 -0
- data/lib/lighthouse/directives/arguments/more_filters.rb +215 -0
- data/lib/lighthouse/directives/definitions.rb +52 -0
- data/lib/lighthouse/directives/model_directive.rb +131 -0
- data/lib/lighthouse/directives/node_directive.rb +134 -0
- data/lib/lighthouse/directives/pagination_manipulator.rb +1 -1
- data/lib/lighthouse/directives/registry.rb +21 -20
- data/lib/lighthouse/directives/visibility_directive.rb +71 -0
- data/lib/lighthouse/enum_directive.rb +41 -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 +59 -11
- data/lib/lighthouse/generated_resolvers.rb +13 -8
- data/lib/lighthouse/global_id.rb +31 -0
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/multipart_request.rb +43 -0
- data/lib/lighthouse/node_registry.rb +68 -0
- data/lib/lighthouse/pagination.rb +3 -0
- data/lib/lighthouse/rb_lighthouse.rb +36 -0
- data/lib/lighthouse/relation_counts.rb +29 -0
- data/lib/lighthouse/resolve_info.rb +80 -0
- data/lib/lighthouse/resolver_bases.rb +68 -0
- data/lib/lighthouse/resolver_dispatch.rb +36 -11
- data/lib/lighthouse/scalar_coercion.rb +72 -0
- data/lib/lighthouse/schema_cache.rb +2 -1
- data/lib/lighthouse/schema_factory.rb +0 -1
- data/lib/lighthouse/schema_generator.rb +21 -12
- data/lib/lighthouse/schema_implementation.rb +4 -8
- data/lib/lighthouse/sdl_loader.rb +0 -2
- data/lib/lighthouse/support/environment.rb +13 -0
- data/lib/lighthouse/support/model_resolver.rb +8 -6
- data/lib/lighthouse/support/preload.rb +49 -0
- data/lib/lighthouse/type.rb +103 -0
- data/lib/lighthouse/type_resolution.rb +150 -0
- data/lib/lighthouse/upload.rb +26 -0
- data/lib/lighthouse/upload_store.rb +32 -0
- data/sig/lighthouse/directives.rbs +3 -2
- data/sig/lighthouse/graphql.rbs +29 -0
- data/sig/lighthouse.rbs +18 -0
- metadata +43 -1
|
@@ -1,23 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'rails/generators'
|
|
4
|
+
require 'generators/graphql/install_generator'
|
|
4
5
|
require 'lighthouse-graphql'
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
6
|
+
require_relative '../base_classes'
|
|
7
|
+
|
|
8
|
+
# `rails generate lighthouse:install` — sets a Rails app up for Lighthouse, on
|
|
9
|
+
# top of graphql-ruby's own install, which it runs first (`graphql:install
|
|
10
|
+
# --no-relay --skip-graphiql --skip-query-logs`, keeping the files the app has):
|
|
11
|
+
# graphql-ruby writes the base classes every graphql-ruby app has (Types::Base*,
|
|
12
|
+
# Resolvers::BaseResolver, Mutations::BaseMutation), its controller and route.
|
|
13
|
+
# Lighthouse then puts its own over what graphql-ruby wrote in this run:
|
|
14
|
+
#
|
|
15
|
+
# - the schema class, built from the SDL (graphql-ruby's is code-first);
|
|
16
|
+
# - the controller, with Lighthouse::GraphQL::Controller;
|
|
17
|
+
# - no Types::QueryType / MutationType: the SDL declares the root types;
|
|
18
|
+
# - Mutations::BaseMutation a GraphQL::Schema::Mutation, as the SDL declares a
|
|
19
|
+
# mutation's arguments (graphql-ruby's is a RelayClassicMutation, one `input:`);
|
|
10
20
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# serving a graphql-ruby schema keeps doing so until it
|
|
21
|
+
# and adds the initializer, a starter SDL file, Subscriptions::BaseSubscription
|
|
22
|
+
# and the graphql-config file IDEs read. Rerunning it skips what is in place. A
|
|
23
|
+
# schema class or controller the app had before is kept (pass --force to replace
|
|
24
|
+
# it), so an app already serving a graphql-ruby schema keeps doing so until it
|
|
25
|
+
# is pointed at ours.
|
|
15
26
|
class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
|
|
27
|
+
include Lighthouse::Generators::BaseClasses
|
|
28
|
+
|
|
16
29
|
CONTROLLER = 'app/controllers/graphql_controller.rb'
|
|
30
|
+
ROOT_TYPES = %w[app/graphql/types/query_type.rb app/graphql/types/mutation_type.rb].freeze
|
|
31
|
+
BASE_MUTATION = 'app/graphql/mutations/base_mutation.rb'
|
|
17
32
|
|
|
18
33
|
source_root File.expand_path('templates', __dir__)
|
|
19
34
|
|
|
20
|
-
desc 'Install Lighthouse: initializer, starter schema, schema class, controller, route and IDE config.'
|
|
35
|
+
desc 'Install Lighthouse: initializer, starter schema, schema class, base classes, controller, route and IDE config.'
|
|
21
36
|
|
|
22
37
|
class_option :schema, type: :string, default: 'AppSchema',
|
|
23
38
|
desc: 'Name of the schema constant to generate'
|
|
@@ -28,6 +43,13 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
|
|
|
28
43
|
class_option :skip_ide, type: :boolean, default: false,
|
|
29
44
|
desc: 'Skip graphql.config.yml and the .gitignore entries for the IDE helper files'
|
|
30
45
|
|
|
46
|
+
# graphql-ruby's install, for the base classes. What it writes that Lighthouse
|
|
47
|
+
# replaces is noted, so a file the app had before is never touched.
|
|
48
|
+
def install_graphql_ruby
|
|
49
|
+
@before = ([schema_path, CONTROLLER, BASE_MUTATION] + ROOT_TYPES).select { |path| exist?(path) }
|
|
50
|
+
invoke 'graphql:install', [], schema: schema_name, relay: false, skip_graphiql: true, skip_query_logs: true, skip: true
|
|
51
|
+
end
|
|
52
|
+
|
|
31
53
|
def create_initializer
|
|
32
54
|
template 'initializer.rb.tt', 'config/initializers/lighthouse.rb'
|
|
33
55
|
end
|
|
@@ -42,17 +64,39 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
|
|
|
42
64
|
end
|
|
43
65
|
|
|
44
66
|
def create_schema_class
|
|
45
|
-
|
|
46
|
-
|
|
67
|
+
return if keep_existing?(schema_path)
|
|
68
|
+
|
|
69
|
+
template 'schema.rb.tt', schema_path, force: true
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The SDL declares the root types: graphql-ruby's code-first ones go. Not when
|
|
73
|
+
# the app keeps a code-first schema of its own, which graphql-ruby's install
|
|
74
|
+
# pointed at them.
|
|
75
|
+
def remove_root_types
|
|
76
|
+
return unless exist?(schema_path) && File.read(File.join(destination_root, schema_path)).include?('RbLightHouse.get_schema')
|
|
77
|
+
|
|
78
|
+
ROOT_TYPES.each { |path| remove_file path unless before?(path) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The SDL declares a mutation's arguments, so its class is a plain
|
|
82
|
+
# GraphQL::Schema::Mutation, not graphql-ruby's RelayClassicMutation.
|
|
83
|
+
def adapt_base_mutation
|
|
84
|
+
return if before?(BASE_MUTATION) || !exist?(BASE_MUTATION)
|
|
85
|
+
|
|
86
|
+
gsub_file BASE_MUTATION, 'GraphQL::Schema::RelayClassicMutation', 'GraphQL::Schema::Mutation'
|
|
87
|
+
gsub_file BASE_MUTATION, /^\s*input_object_class .*\n/, ''
|
|
88
|
+
end
|
|
47
89
|
|
|
48
|
-
|
|
90
|
+
# Subscriptions::BaseSubscription, which graphql-ruby has none of (see BaseClasses).
|
|
91
|
+
def create_base_subscription
|
|
92
|
+
write_base_class(:subscriptions, Lighthouse::Config.namespaces[:subscriptions].first)
|
|
49
93
|
end
|
|
50
94
|
|
|
51
95
|
def create_controller
|
|
52
|
-
return if options[:skip_controller]
|
|
96
|
+
return remove_graphql_ruby_controller if options[:skip_controller]
|
|
53
97
|
return if (@kept_controller = keep_existing?(CONTROLLER))
|
|
54
98
|
|
|
55
|
-
template 'graphql_controller.rb.tt', CONTROLLER
|
|
99
|
+
template 'graphql_controller.rb.tt', CONTROLLER, force: true
|
|
56
100
|
end
|
|
57
101
|
|
|
58
102
|
def add_route
|
|
@@ -93,26 +137,58 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
|
|
|
93
137
|
|
|
94
138
|
Lighthouse is installed. Next steps:
|
|
95
139
|
1. Describe your API in #{sdl_folder}/*.graphql (the starter schema assumes a User model).
|
|
96
|
-
2.
|
|
97
|
-
|
|
140
|
+
2. Resolve a field in Ruby when a directive is not enough: bin/rails g lighthouse:query LatestPosts
|
|
141
|
+
(it inherits from Resolvers::BaseResolver, which graphql-ruby's install wrote; lighthouse:mutation and
|
|
142
|
+
lighthouse:subscription write the others)
|
|
143
|
+
3. Give a type Ruby methods, a union or interface its resolve_type, a scalar its coercion:
|
|
144
|
+
bin/rails g graphql:object Post (or graphql:union, graphql:interface, graphql:scalar),
|
|
145
|
+
then include Lighthouse::GraphQL::Type in the class
|
|
146
|
+
4. Generate the IDE helper files: bin/rails lighthouse:ide_helper
|
|
147
|
+
5. #{serve}
|
|
98
148
|
MSG
|
|
99
149
|
end
|
|
100
150
|
|
|
101
151
|
private
|
|
102
152
|
|
|
103
|
-
#
|
|
104
|
-
#
|
|
153
|
+
# A file the app had before this run is left alone unless --force is given;
|
|
154
|
+
# one graphql-ruby's install wrote in this run is Lighthouse's to replace.
|
|
105
155
|
def keep_existing?(path)
|
|
106
|
-
return false if options[:force] || !
|
|
156
|
+
return false if options[:force] || !before?(path)
|
|
107
157
|
|
|
108
158
|
say_status :skip, "#{path} already exists (pass --force to replace it)", :yellow
|
|
109
159
|
true
|
|
110
160
|
end
|
|
111
161
|
|
|
162
|
+
def before?(path)
|
|
163
|
+
@before.include?(path)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def exist?(path)
|
|
167
|
+
File.exist?(File.join(destination_root, path))
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def schema_path
|
|
171
|
+
File.join('app/graphql', "#{schema_name.underscore}.rb")
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# --skip-controller: the app serves the schema itself, so graphql-ruby's
|
|
175
|
+
# controller and route, if its install just wrote them, go.
|
|
176
|
+
def remove_graphql_ruby_controller
|
|
177
|
+
return if before?(CONTROLLER)
|
|
178
|
+
|
|
179
|
+
remove_file CONTROLLER
|
|
180
|
+
gsub_file 'config/routes.rb', %r{^\s*post "/graphql", to: "graphql#execute"\n}, '' if exist?('config/routes.rb')
|
|
181
|
+
end
|
|
182
|
+
|
|
112
183
|
def schema_name
|
|
113
184
|
options[:schema].camelize
|
|
114
185
|
end
|
|
115
186
|
|
|
187
|
+
# `Resolvers::BaseResolver`, for the schema class template.
|
|
188
|
+
def base_class_constant(kind)
|
|
189
|
+
"#{Lighthouse::Config.namespaces[kind].first}::#{base_class_name(kind)}"
|
|
190
|
+
end
|
|
191
|
+
|
|
116
192
|
def sdl_folder
|
|
117
193
|
options[:sdl_folder].delete_suffix('/')
|
|
118
194
|
end
|
|
@@ -12,25 +12,34 @@ Lighthouse.configure do |config|
|
|
|
12
12
|
|
|
13
13
|
# Where classes named by the schema are looked up, by kind. A Query or Mutation
|
|
14
14
|
# field without a resolving directive resolves by convention: Query.latestPosts
|
|
15
|
-
# to
|
|
16
|
-
# to Mutations::PublishPost (a
|
|
15
|
+
# to Resolvers::LatestPosts (a Resolvers::BaseResolver), Mutation.publishPost
|
|
16
|
+
# to Mutations::PublishPost (a Mutations::BaseMutation). @builder classes
|
|
17
17
|
# come from :builders and @validator contracts from :validators. A Subscription
|
|
18
|
-
# field resolves to Subscriptions::PostUpdated (a
|
|
19
|
-
#
|
|
20
|
-
#
|
|
18
|
+
# field resolves to Subscriptions::PostUpdated (a Subscriptions::BaseSubscription),
|
|
19
|
+
# @event(dispatch:) classes come from :events, and the graphql-ruby analyzers
|
|
20
|
+
# @complexity(resolver:) names from :analyzers. Post.readingTime, with no
|
|
21
|
+
# resolving directive, resolves with Types::PostType#reading_time (a
|
|
22
|
+
# Types::BaseObject including Lighthouse::GraphQL::Type), from :types. Generate them with
|
|
23
|
+
# `rails g lighthouse:query|mutation|subscription|type|builder|validator Name`.
|
|
21
24
|
# config.namespaces = {
|
|
22
|
-
# queries: ['
|
|
23
|
-
# builders: ['Builders'], validators: ['Validators'], events: ['Events']
|
|
25
|
+
# queries: ['Resolvers'], mutations: ['Mutations'], subscriptions: ['Subscriptions'],
|
|
26
|
+
# builders: ['Builders'], validators: ['Validators'], events: ['Events'], analyzers: ['Analyzers'],
|
|
27
|
+
# types: ['Types']
|
|
24
28
|
# }
|
|
25
29
|
|
|
26
30
|
# Namespaces tried, in order, for models (@all(model: "User")) and for
|
|
27
|
-
# @field(resolver:) on types other than Query and Mutation
|
|
28
|
-
#
|
|
31
|
+
# @field(resolver:) on types other than Query and Mutation, after :queries
|
|
32
|
+
# (@field(resolver: "Posts::TagsCount") is Resolvers::Posts::TagsCount).
|
|
33
|
+
# Fully-qualified names (containing `::`) are used as-is.
|
|
29
34
|
# config.resolver_namespaces = ['App::Graphql']
|
|
30
35
|
|
|
31
|
-
# Store for @cache and @
|
|
36
|
+
# Store for @cache, @clearCache and @throttle's counts.
|
|
32
37
|
# config.cache_store = Rails.cache
|
|
33
38
|
|
|
39
|
+
# Where @upload stores a file: call(file, disk:, path:, public:) returns what the
|
|
40
|
+
# argument holds. By default an ActiveStorage blob, and its signed id.
|
|
41
|
+
# config.upload_store = Lighthouse::GraphQL::UploadStore
|
|
42
|
+
|
|
34
43
|
# Run @create/@update/@createMany/@updateMany in a database transaction.
|
|
35
44
|
# config.transactional_mutations = true
|
|
36
45
|
|
|
@@ -2,9 +2,28 @@
|
|
|
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. graphql-ruby builds the
|
|
8
|
+
# SDL's types from the others (written by `rails g graphql:install`), and type
|
|
9
|
+
# classes (Types::PostType, Types::DateTimeType, ...) inherit from them.
|
|
5
10
|
<%= schema_name %> = Lighthouse::GraphQL::RbLightHouse.get_schema(
|
|
6
11
|
options: {
|
|
7
|
-
using: { GraphQL::Dataloader => {} }
|
|
8
|
-
|
|
12
|
+
using: { GraphQL::Dataloader => {} },
|
|
13
|
+
base_types: {
|
|
14
|
+
resolver: <%= base_class_constant(:queries) %>,
|
|
15
|
+
mutation: <%= base_class_constant(:mutations) %>,
|
|
16
|
+
subscription: <%= base_class_constant(:subscriptions) %>,
|
|
17
|
+
object: Types::BaseObject,
|
|
18
|
+
interface: Types::BaseInterface,
|
|
19
|
+
union: Types::BaseUnion,
|
|
20
|
+
scalar: Types::BaseScalar,
|
|
21
|
+
enum: Types::BaseEnum,
|
|
22
|
+
input_object: Types::BaseInputObject
|
|
23
|
+
}
|
|
9
24
|
}
|
|
10
25
|
)
|
|
26
|
+
|
|
27
|
+
# Refuse expensive queries before they run (see the gem's docs/security.md):
|
|
28
|
+
# <%= schema_name %>.max_complexity(300)
|
|
29
|
+
# <%= 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,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_job'
|
|
4
|
+
|
|
5
|
+
# Runs what resolves an @async mutation field, from a queue. Loaded on first
|
|
6
|
+
# use — the activejob gem is optional — so it is autoloaded, as BroadcastJob.
|
|
7
|
+
# The schema is passed by name; the field's resolver runs with the arguments
|
|
8
|
+
# and context values the request serialized, inside the schema's Dataloader.
|
|
9
|
+
class Lighthouse::GraphQL::AsyncMutationJob < ActiveJob::Base # rubocop:disable Rails/ApplicationJob
|
|
10
|
+
def perform(schema_name, type_name, field_name, arguments, context_values)
|
|
11
|
+
schema = schema_name.constantize
|
|
12
|
+
type = schema.get_type(type_name)
|
|
13
|
+
inner = Lighthouse::GraphQL::DirectiveResolvers::AsyncDirectiveResolver.inner(type, field_name)
|
|
14
|
+
context = ::GraphQL::Query.new(schema, '{ __typename }', context: context_values.merge(lighthouse_async: true)).context
|
|
15
|
+
context.dataloader.run_isolated { inner.call(nil, arguments.transform_keys(&:to_sym), context) }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
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
|
+
reference = %w[resolver class].filter_map { |name| Lighthouse::Support::DirectiveArgs.literal(directive, name) }.first
|
|
155
|
+
return unless reference
|
|
156
|
+
|
|
157
|
+
class_name, = Lighthouse::GraphQL::DirectiveResolvers::FieldDirectiveResolver.reference(reference)
|
|
158
|
+
|
|
159
|
+
-> { Lighthouse::GraphQL::DirectiveResolvers::FieldDirectiveResolver.lookup(class_name, kind) }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Scores each query as graphql-ruby's MaxQueryComplexity scores it against
|
|
163
|
+
# `max_complexity`, for BuildExtensionsResponse#query_complexity. Runs only
|
|
164
|
+
# while something listens to that event.
|
|
165
|
+
class Analyzer < ::GraphQL::Analysis::QueryComplexity
|
|
166
|
+
def analyze?
|
|
167
|
+
Lighthouse::Events.listening?(Lighthouse::Events::BuildExtensionsResponse)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def result
|
|
171
|
+
query.context[CONTEXT_KEY] = max_possible_complexity
|
|
172
|
+
nil
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
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
|