lighthouse-graphql 0.4.0 → 0.6.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 +256 -1
- data/README.md +26 -15
- data/docs/caching.md +4 -3
- data/docs/configuration.md +16 -12
- data/docs/directives.md +294 -4
- data/docs/generators.md +84 -0
- data/docs/getting-started.md +93 -60
- data/docs/performance.md +2 -2
- data/docs/relationships.md +47 -4
- data/docs/relay.md +200 -0
- data/docs/resolvers.md +326 -34
- data/docs/security.md +73 -8
- data/docs/subscriptions.md +16 -9
- data/docs/types.md +218 -0
- data/docs/uploads.md +107 -0
- data/lib/generators/lighthouse/base_classes.rb +27 -49
- data/lib/generators/lighthouse/install/graphql_ruby_install.rb +113 -0
- data/lib/generators/lighthouse/install/install_generator.rb +125 -45
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +10 -8
- data/lib/lighthouse/async_mutation_job.rb +17 -0
- data/lib/lighthouse/caching.rb +9 -3
- data/lib/lighthouse/complexity.rb +5 -4
- data/lib/lighthouse/config.rb +12 -18
- data/lib/lighthouse/controller.rb +53 -12
- 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/cache_resolvers.rb +1 -1
- data/lib/lighthouse/directive_resolvers/can_resolvers.rb +1 -1
- data/lib/lighthouse/directive_resolvers/convention_resolver.rb +2 -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 +59 -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 +2 -1
- data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +136 -0
- data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +87 -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 -21
- data/lib/lighthouse/directives/visibility_directive.rb +71 -0
- data/lib/lighthouse/enum_directive.rb +41 -0
- data/lib/lighthouse/field_resolver_stack.rb +56 -6
- data/lib/lighthouse/generated_resolvers.rb +13 -8
- data/lib/lighthouse/global_id.rb +57 -0
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/graphql_ruby_patches.rb +45 -0
- 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 +34 -0
- data/lib/lighthouse/relation_counts.rb +29 -0
- data/lib/lighthouse/resolve_info.rb +80 -0
- data/lib/lighthouse/resolver_bases.rb +25 -4
- data/lib/lighthouse/resolver_dispatch.rb +51 -11
- data/lib/lighthouse/root_types.rb +117 -0
- data/lib/lighthouse/scalar_coercion.rb +72 -0
- data/lib/lighthouse/schema_cache.rb +2 -1
- data/lib/lighthouse/schema_generator.rb +13 -11
- data/lib/lighthouse/schema_implementation.rb +4 -8
- data/lib/lighthouse/support/environment.rb +13 -0
- data/lib/lighthouse/support/model_resolver.rb +7 -5
- 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 +28 -0
- data/lib/lighthouse/upload_store.rb +57 -0
- data/sig/lighthouse/directives.rbs +3 -2
- data/sig/lighthouse.rbs +10 -2
- metadata +40 -4
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +0 -22
- data/lib/generators/lighthouse/mutation_generator.rb +0 -31
- data/lib/generators/lighthouse/query_generator.rb +0 -30
|
@@ -1,37 +1,71 @@
|
|
|
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
|
require_relative '../base_classes'
|
|
7
|
+
require_relative 'graphql_ruby_install'
|
|
6
8
|
|
|
7
|
-
# `rails generate lighthouse:install` —
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
9
|
+
# `rails generate lighthouse:install` — puts Lighthouse on top of graphql-ruby's
|
|
10
|
+
# own install (`rails g graphql:install`, with whichever of its options the app
|
|
11
|
+
# chose: --relay, --batch, --skip-mutation-root-type, --schema, --directory,
|
|
12
|
+
# ...). When the app has none, it runs graphql:install first, with its
|
|
13
|
+
# defaults. It then adapts what graphql-ruby wrote, instead of writing its own:
|
|
12
14
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
15
|
+
# - the schema class (`class AppSchema < GraphQL::Schema`) becomes Lighthouse's,
|
|
16
|
+
# built from the SDL on everything graphql-ruby wrote: its base types, base
|
|
17
|
+
# resolver and base mutation, and its code-first Types::QueryType /
|
|
18
|
+
# MutationType, which the SDL's root types extend, so their `field`s (Relay's
|
|
19
|
+
# `node` / `nodes`, what `graphql:mutation` adds) stay. Its Relay
|
|
20
|
+
# Types::NodeType is the SDL's Node. Everything else in the class is
|
|
21
|
+
# graphql-ruby's and stays, but the `resolve_type` stub (Lighthouse resolves
|
|
22
|
+
# types) and the root types it is built on;
|
|
23
|
+
# - the example `test_field` goes from the root types;
|
|
24
|
+
# - graphql-ruby's base object, interface, union and scalar include
|
|
25
|
+
# Lighthouse::GraphQL::Type, so every type class (Types::QueryType,
|
|
26
|
+
# Types::NodeType, what `graphql:object` writes) is Lighthouse's;
|
|
27
|
+
# - with --batch, the schema uses GraphQL::Dataloader too, which Lighthouse's
|
|
28
|
+
# directives batch through;
|
|
29
|
+
# - graphql-ruby's controller, as its install wrote it, becomes one with
|
|
30
|
+
# Lighthouse::GraphQL::Controller.
|
|
31
|
+
#
|
|
32
|
+
# It adds the initializer, a starter SDL file and the graphql-config file IDEs
|
|
33
|
+
# read, and with --subscriptions, Subscriptions::BaseSubscription. Rerunning it
|
|
34
|
+
# skips what is in place.
|
|
17
35
|
class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
|
|
18
36
|
include Lighthouse::Generators::BaseClasses
|
|
37
|
+
include Lighthouse::Generators::GraphqlRubyInstall
|
|
19
38
|
|
|
20
39
|
CONTROLLER = 'app/controllers/graphql_controller.rb'
|
|
21
40
|
|
|
22
41
|
source_root File.expand_path('templates', __dir__)
|
|
23
42
|
|
|
24
|
-
desc
|
|
43
|
+
desc "Install Lighthouse on graphql-ruby's install: schema class, base classes, controller, initializer, starter SDL and IDE config."
|
|
25
44
|
|
|
26
|
-
class_option :schema, type: :string,
|
|
27
|
-
desc:
|
|
45
|
+
class_option :schema, type: :string,
|
|
46
|
+
desc: "The schema class to adopt; for graphql:install, when it runs, its name (default: graphql-ruby's)"
|
|
28
47
|
class_option :sdl_folder, type: :string, default: 'graphql',
|
|
29
48
|
desc: 'Folder, relative to the app root, holding the *.graphql files'
|
|
49
|
+
class_option :subscriptions, type: :boolean, default: false,
|
|
50
|
+
desc: 'Add Subscriptions::BaseSubscription, the base of the classes Subscription fields resolve to'
|
|
30
51
|
class_option :skip_controller, type: :boolean, default: false,
|
|
31
|
-
desc: '
|
|
52
|
+
desc: "Leave graphql-ruby's GraphqlController as it is"
|
|
32
53
|
class_option :skip_ide, type: :boolean, default: false,
|
|
33
54
|
desc: 'Skip graphql.config.yml and the .gitignore entries for the IDE helper files'
|
|
34
55
|
|
|
56
|
+
# graphql-ruby's install: the app's, or one now, with graphql-ruby's defaults.
|
|
57
|
+
def install_graphql_ruby
|
|
58
|
+
return if schema_path
|
|
59
|
+
|
|
60
|
+
say_status :invoke, 'graphql:install (no graphql-ruby schema found)', :blue
|
|
61
|
+
invoke 'graphql:install', [], graphql_install_options
|
|
62
|
+
@schema_path = nil
|
|
63
|
+
return if schema_path
|
|
64
|
+
|
|
65
|
+
raise Thor::Error, "No graphql-ruby schema class (class #{options[:schema] || 'AppSchema'} < GraphQL::Schema) under app/: " \
|
|
66
|
+
'run `bin/rails g graphql:install` first.'
|
|
67
|
+
end
|
|
68
|
+
|
|
35
69
|
def create_initializer
|
|
36
70
|
template 'initializer.rb.tt', 'config/initializers/lighthouse.rb'
|
|
37
71
|
end
|
|
@@ -45,24 +79,62 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
|
|
|
45
79
|
template 'schema.graphql.tt', File.join(sdl_folder, 'schema.graphql')
|
|
46
80
|
end
|
|
47
81
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
82
|
+
# Subscriptions::BaseSubscription, which graphql-ruby has none of (see BaseClasses).
|
|
83
|
+
def create_base_subscription
|
|
84
|
+
write_base_class(:subscriptions, Lighthouse::Config.namespaces[:subscriptions].first) if options[:subscriptions]
|
|
85
|
+
end
|
|
51
86
|
|
|
52
|
-
|
|
87
|
+
# graphql-ruby's schema class, built by Lighthouse from the SDL on its classes.
|
|
88
|
+
def adopt_schema_class
|
|
89
|
+
source = read(schema_path)
|
|
90
|
+
if lighthouse?(source)
|
|
91
|
+
say_status :skip, "#{schema_path} is Lighthouse's already", :yellow
|
|
92
|
+
return
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
declaration = /^([ \t]*)class (\S+) < GraphQL::Schema[ \t]*\n/
|
|
96
|
+
indent, name = source.match(declaration).captures
|
|
97
|
+
gsub_file schema_path, declaration, "#{indent}#{schema_comment(indent)}#{indent}class #{name} < #{schema_superclass(indent)}\n"
|
|
98
|
+
gsub_file schema_path, /^[ \t]*(query|mutation|subscription)\(Types::(Query|Mutation|Subscription)Type\)\n/, ''
|
|
99
|
+
gsub_file schema_path, /^(#{indent}\)\n)\n+/, '\\1'
|
|
100
|
+
gsub_file schema_path, RESOLVE_TYPE_STUB, "\n"
|
|
101
|
+
add_dataloader if source.include?('use GraphQL::Batch') && source.exclude?('use GraphQL::Dataloader')
|
|
53
102
|
end
|
|
54
103
|
|
|
55
|
-
#
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
104
|
+
# The example field goes; the root types' own fields stay.
|
|
105
|
+
def adapt_root_types
|
|
106
|
+
%w[query_type mutation_type subscription_type].each do |name|
|
|
107
|
+
path = types_path("#{name}.rb")
|
|
108
|
+
gsub_file path, TEST_FIELD, "\n" if exist?(path) && read(path).match?(TEST_FIELD)
|
|
109
|
+
end
|
|
59
110
|
end
|
|
60
111
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
112
|
+
# Every type class is Lighthouse's: graphql-ruby's base object, interface,
|
|
113
|
+
# union and scalar include Lighthouse::GraphQL::Type, so the classes that
|
|
114
|
+
# inherit from them (Types::QueryType, Types::NodeType, what `graphql:object`
|
|
115
|
+
# writes) lend their methods to the SDL's types of their name.
|
|
116
|
+
def adapt_type_bases
|
|
117
|
+
TYPE_CONCERN_ANCHORS.each do |name, anchor|
|
|
118
|
+
path = types_path("#{name}.rb")
|
|
119
|
+
next if !exist?(path) || read(path).include?('Lighthouse::GraphQL::Type')
|
|
120
|
+
|
|
121
|
+
indent = read(path)[anchor, 1]
|
|
122
|
+
next unless indent
|
|
123
|
+
|
|
124
|
+
indent += ' ' unless name == 'base_interface'
|
|
125
|
+
inject_into_file path, "#{indent}# Lighthouse's: the class of the SDL type of its name lends that type its methods.\n" \
|
|
126
|
+
"#{indent}include Lighthouse::GraphQL::Type\n", after: anchor
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# graphql-ruby's controller, as its install wrote it, serves the schema with
|
|
131
|
+
# Lighthouse's concern. One the app has changed is left alone.
|
|
132
|
+
def adapt_controller
|
|
133
|
+
return if options[:skip_controller] || !exist?(CONTROLLER)
|
|
134
|
+
return if read(CONTROLLER).include?('Lighthouse::GraphQL::Controller')
|
|
135
|
+
return @kept_controller = true unless graphql_ruby_controller?(read(CONTROLLER))
|
|
64
136
|
|
|
65
|
-
template 'graphql_controller.rb.tt', CONTROLLER
|
|
137
|
+
template 'graphql_controller.rb.tt', CONTROLLER, force: true
|
|
66
138
|
end
|
|
67
139
|
|
|
68
140
|
def add_route
|
|
@@ -72,8 +144,7 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
|
|
|
72
144
|
return unless File.exist?(routes)
|
|
73
145
|
return if File.read(routes).include?('graphql#execute')
|
|
74
146
|
|
|
75
|
-
# Byte-identical to graphql-ruby's install route
|
|
76
|
-
# runs second finds it in place and does not add it again.
|
|
147
|
+
# Byte-identical to graphql-ruby's install route.
|
|
77
148
|
route 'post "/graphql", to: "graphql#execute"'
|
|
78
149
|
end
|
|
79
150
|
|
|
@@ -94,40 +165,49 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
|
|
|
94
165
|
def show_next_steps
|
|
95
166
|
serve =
|
|
96
167
|
if @kept_controller
|
|
97
|
-
"Serve it: #{CONTROLLER} was
|
|
168
|
+
"Serve it: #{CONTROLLER} was changed from graphql-ruby's, so it was kept — " \
|
|
169
|
+
"include Lighthouse::GraphQL::Controller in it, or keep calling #{schema_name}.execute(...)."
|
|
98
170
|
else
|
|
99
171
|
'Query it: POST /graphql {"query": "{ users { data { id name } paginatorInfo { total } } }"}'
|
|
100
172
|
end
|
|
101
173
|
|
|
102
174
|
say <<~MSG
|
|
103
175
|
|
|
104
|
-
Lighthouse is installed. Next steps:
|
|
176
|
+
Lighthouse is installed on graphql-ruby's install (#{schema_path}). Next steps:
|
|
105
177
|
1. Describe your API in #{sdl_folder}/*.graphql (the starter schema assumes a User model).
|
|
106
|
-
2. Resolve a field in Ruby when a directive is not enough
|
|
107
|
-
(
|
|
108
|
-
|
|
109
|
-
|
|
178
|
+
2. Resolve a field in Ruby when a directive is not enough, as in graphql-ruby: a method of
|
|
179
|
+
Types::QueryType (Query.latestPosts -> #latest_posts), a `field` of its own there,
|
|
180
|
+
`bin/rails g graphql:mutation`, or a Resolvers::BaseResolver subclass named after the field.
|
|
181
|
+
3. Give a type Ruby methods, a union or interface its resolve_type, a scalar its coercion:
|
|
182
|
+
bin/rails g graphql:object Post (or graphql:union, graphql:interface, graphql:scalar);
|
|
183
|
+
the class is Lighthouse's through its base (Lighthouse::GraphQL::Type)
|
|
184
|
+
4. Generate the IDE helper files: bin/rails lighthouse:ide_helper
|
|
185
|
+
5. #{serve}
|
|
110
186
|
MSG
|
|
111
187
|
end
|
|
112
188
|
|
|
113
189
|
private
|
|
114
190
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
say_status :skip, "#{path} already exists (pass --force to replace it)", :yellow
|
|
121
|
-
true
|
|
191
|
+
def schema_superclass(indent)
|
|
192
|
+
lines = base_types.map { |key, name| "#{indent} #{key}: #{name}" }
|
|
193
|
+
"Lighthouse::GraphQL::RbLightHouse.get_schema(\n#{indent} options: {\n#{indent} base_types: {\n" \
|
|
194
|
+
"#{lines.join(",\n")}\n#{indent} }\n#{indent} }\n#{indent})"
|
|
122
195
|
end
|
|
123
196
|
|
|
124
|
-
def
|
|
125
|
-
|
|
197
|
+
def schema_comment(indent)
|
|
198
|
+
<<~RUBY.gsub(/^/, indent).delete_prefix(indent)
|
|
199
|
+
# Lighthouse builds the schema from the SDL in #{sdl_folder}/, on the classes graphql-ruby's
|
|
200
|
+
# install wrote (base_types): the SDL's root types extend the code-first ones, whose
|
|
201
|
+
# `field`s stay the schema's, and every type, resolver and mutation class inherits from
|
|
202
|
+
# its kind's base. The rest of the class is graphql-ruby's.
|
|
203
|
+
RUBY
|
|
126
204
|
end
|
|
127
205
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
206
|
+
def add_dataloader
|
|
207
|
+
anchor = /^([ \t]*)use GraphQL::Batch\n/
|
|
208
|
+
indent = read(schema_path)[anchor, 1]
|
|
209
|
+
inject_into_file schema_path, "#{indent}# Lighthouse's directives batch through GraphQL::Dataloader, beside GraphQL::Batch.\n" \
|
|
210
|
+
"#{indent}use GraphQL::Dataloader\n", after: anchor
|
|
131
211
|
end
|
|
132
212
|
|
|
133
213
|
def sdl_folder
|
|
@@ -17,21 +17,23 @@ Lighthouse.configure do |config|
|
|
|
17
17
|
# come from :builders and @validator contracts from :validators. A Subscription
|
|
18
18
|
# field resolves to Subscriptions::PostUpdated (a Subscriptions::BaseSubscription),
|
|
19
19
|
# @event(dispatch:) classes come from :events, and the graphql-ruby analyzers
|
|
20
|
-
# @complexity(resolver:) names from :analyzers.
|
|
21
|
-
#
|
|
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 a
|
|
23
|
+
# subscription, builder or validator with `rails g lighthouse:subscription|builder|validator Name`,
|
|
24
|
+
# and a type class with graphql-ruby's `rails g graphql:object|union|interface|scalar Name`.
|
|
22
25
|
# config.namespaces = {
|
|
23
26
|
# queries: ['Resolvers'], mutations: ['Mutations'], subscriptions: ['Subscriptions'],
|
|
24
|
-
# builders: ['Builders'], validators: ['Validators'], events: ['Events'], analyzers: ['Analyzers']
|
|
27
|
+
# builders: ['Builders'], validators: ['Validators'], events: ['Events'], analyzers: ['Analyzers'],
|
|
28
|
+
# types: ['Types']
|
|
25
29
|
# }
|
|
26
30
|
|
|
27
31
|
# Namespaces tried, in order, for models (@all(model: "User")) and for
|
|
28
|
-
# @field(resolver:) on types other than Query and Mutation
|
|
29
|
-
#
|
|
32
|
+
# @field(resolver:) on types other than Query and Mutation, after :queries
|
|
33
|
+
# (@field(resolver: "Posts::TagsCount") is Resolvers::Posts::TagsCount).
|
|
34
|
+
# Fully-qualified names (containing `::`) are used as-is.
|
|
30
35
|
# config.resolver_namespaces = ['App::Graphql']
|
|
31
36
|
|
|
32
|
-
# Store for @cache and @clearCache.
|
|
33
|
-
# config.cache_store = Rails.cache
|
|
34
|
-
|
|
35
37
|
# Run @create/@update/@createMany/@updateMany in a database transaction.
|
|
36
38
|
# config.transactional_mutations = true
|
|
37
39
|
|
|
@@ -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
|
data/lib/lighthouse/caching.rb
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'active_support/cache'
|
|
3
4
|
require 'digest'
|
|
4
5
|
require 'json'
|
|
5
6
|
require 'securerandom'
|
|
6
7
|
|
|
7
|
-
#
|
|
8
|
-
#
|
|
8
|
+
# The app's cache stores, and keys and invalidation for @cache / @clearCache.
|
|
9
|
+
#
|
|
10
|
+
# Lighthouse keeps no store of its own, as PHP Lighthouse uses Laravel's: @cache,
|
|
11
|
+
# @clearCache and APQ use Rails.cache (config.cache_store), and @throttle counts
|
|
12
|
+
# where Rails' own `rate_limit` does, in the controller's cache_store.
|
|
9
13
|
#
|
|
10
14
|
# PHP Lighthouse invalidates with cache tags, which Rails cache stores do not
|
|
11
15
|
# have (and `delete_matched` is not universal). Instead every entry key embeds
|
|
@@ -19,8 +23,10 @@ module Lighthouse::GraphQL::Caching
|
|
|
19
23
|
|
|
20
24
|
module_function
|
|
21
25
|
|
|
26
|
+
# Rails.cache; outside Rails, one in-memory store for the process.
|
|
22
27
|
def store
|
|
23
|
-
|
|
28
|
+
rails_cache = ::Rails.cache if defined?(::Rails) && ::Rails.respond_to?(:cache)
|
|
29
|
+
rails_cache || (@memory_store ||= ActiveSupport::Cache::MemoryStore.new)
|
|
24
30
|
end
|
|
25
31
|
|
|
26
32
|
def entry_key(type, id, field, args, user_key)
|
|
@@ -151,11 +151,12 @@ module Lighthouse::GraphQL::Complexity
|
|
|
151
151
|
|
|
152
152
|
# The class @field names, looked up as @field looks it up.
|
|
153
153
|
def field_class_lookup(directive, kind)
|
|
154
|
-
|
|
155
|
-
return unless
|
|
154
|
+
reference = %w[resolver class].filter_map { |name| Lighthouse::Support::DirectiveArgs.literal(directive, name) }.first
|
|
155
|
+
return unless reference
|
|
156
156
|
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
class_name, = Lighthouse::GraphQL::DirectiveResolvers::FieldDirectiveResolver.reference(reference)
|
|
158
|
+
|
|
159
|
+
-> { Lighthouse::GraphQL::DirectiveResolvers::FieldDirectiveResolver.lookup(class_name, kind) }
|
|
159
160
|
end
|
|
160
161
|
|
|
161
162
|
# Scores each query as graphql-ruby's MaxQueryComplexity scores it against
|
data/lib/lighthouse/config.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'active_support/cache'
|
|
4
|
-
|
|
5
3
|
module Lighthouse
|
|
6
4
|
# Convenience entrypoint: `Lighthouse.configure { |c| c.resolver_namespaces = [...] }`.
|
|
7
5
|
def self.configure(&block)
|
|
@@ -20,25 +18,27 @@ module Lighthouse
|
|
|
20
18
|
validators: ['Validators'],
|
|
21
19
|
events: ['Events'],
|
|
22
20
|
subscriptions: ['Subscriptions'],
|
|
23
|
-
analyzers: ['Analyzers']
|
|
21
|
+
analyzers: ['Analyzers'],
|
|
22
|
+
types: ['Types']
|
|
24
23
|
}.freeze
|
|
25
24
|
|
|
26
25
|
# Every setting reset! forgets, so the next read falls back to its default.
|
|
27
26
|
SETTINGS = %i[
|
|
28
|
-
sdl_folder resolver_namespaces namespaces
|
|
27
|
+
sdl_folder resolver_namespaces namespaces transactional_mutations pagination_default_count pagination_max_count
|
|
29
28
|
schema_cache schema_cache_path watch_sdl persisted_queries persisted_queries_store persisted_queries_ttl
|
|
30
29
|
persisted_queries_manifest persisted_queries_only on_error directive_map authenticated_user policy_user authorizer
|
|
31
30
|
feature_checker feature_actor
|
|
32
31
|
convert_empty_strings_to_null
|
|
33
32
|
soft_deletes
|
|
34
33
|
queue_broadcasts broadcast_queue
|
|
34
|
+
global_id
|
|
35
35
|
].freeze
|
|
36
36
|
|
|
37
37
|
class << self
|
|
38
|
-
attr_writer :sdl_folder, :resolver_namespaces, :on_error, :authenticated_user, :policy_user, :authorizer,
|
|
38
|
+
attr_writer :sdl_folder, :resolver_namespaces, :on_error, :authenticated_user, :policy_user, :authorizer,
|
|
39
39
|
:transactional_mutations, :schema_cache, :watch_sdl, :persisted_queries, :persisted_queries_store, :persisted_queries_only,
|
|
40
40
|
:queue_broadcasts,
|
|
41
|
-
:convert_empty_strings_to_null, :feature_checker, :feature_actor
|
|
41
|
+
:convert_empty_strings_to_null, :feature_checker, :feature_actor, :global_id
|
|
42
42
|
|
|
43
43
|
def sdl_folder
|
|
44
44
|
@sdl_folder ||= (defined?(Rails) && Rails.respond_to?(:root) ? Rails.root.join('graphql').to_s : nil)
|
|
@@ -64,16 +64,6 @@ module Lighthouse
|
|
|
64
64
|
@namespaces = namespaces.merge(overrides.to_h.transform_keys(&:to_sym).transform_values { |v| Array(v) })
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
# Store backing @cache and @clearCache. Defaults to Rails.cache.
|
|
68
|
-
def cache_store
|
|
69
|
-
@cache_store ||=
|
|
70
|
-
if defined?(Rails) && Rails.respond_to?(:cache) && Rails.cache
|
|
71
|
-
Rails.cache
|
|
72
|
-
else
|
|
73
|
-
ActiveSupport::Cache::MemoryStore.new
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
67
|
# --- Subscriptions (used by @broadcast) ----------------------------------
|
|
78
68
|
#
|
|
79
69
|
# queue_broadcasts perform @broadcast triggers in a Lighthouse::GraphQL::BroadcastJob
|
|
@@ -89,7 +79,7 @@ module Lighthouse
|
|
|
89
79
|
# schema_cache_path where; default Rails.root/tmp/cache/lighthouse
|
|
90
80
|
# watch_sdl reload the app (and so the schema) when a .graphql file changes
|
|
91
81
|
# persisted_queries accept Apollo's Automatic Persisted Queries (PersistedQueries)
|
|
92
|
-
# persisted_queries_store where APQ keeps queries; default
|
|
82
|
+
# persisted_queries_store where APQ keeps queries; default Rails.cache
|
|
93
83
|
# persisted_queries_ttl how long they live there (seconds; nil: until evicted)
|
|
94
84
|
# persisted_queries_manifest a manifest of trusted operations, served by hash
|
|
95
85
|
# persisted_queries_only reject any operation not in the manifest
|
|
@@ -104,8 +94,12 @@ module Lighthouse
|
|
|
104
94
|
# when the schema is built, so set it in an initializer.
|
|
105
95
|
def convert_empty_strings_to_null = @convert_empty_strings_to_null || false
|
|
106
96
|
|
|
97
|
+
# Encodes and decodes Relay global ids (@globalId, @node): `encode(type, id)`
|
|
98
|
+
# and `decode(global_id)` -> [type, id]. Base64 of "Type:id" by default.
|
|
99
|
+
def global_id = @global_id || Lighthouse::GraphQL::GlobalId
|
|
100
|
+
|
|
107
101
|
def persisted_queries_store
|
|
108
|
-
@persisted_queries_store ||
|
|
102
|
+
@persisted_queries_store || Lighthouse::GraphQL::Caching.store
|
|
109
103
|
end
|
|
110
104
|
|
|
111
105
|
# Pagination defaults, overridden by a directive's defaultCount / maxCount.
|
|
@@ -19,10 +19,16 @@ require 'json'
|
|
|
19
19
|
#
|
|
20
20
|
# `execute` dispatches StartRequest, reads the variables (a JSON String, a Hash
|
|
21
21
|
# or parameters), runs the operation through PersistedQueries.execute (or
|
|
22
|
-
# PersistedQueries.multiplex for a batch, a JSON array body
|
|
22
|
+
# PersistedQueries.multiplex for a batch, a JSON array body; a multipart
|
|
23
|
+
# request's operations, with its files, as MultipartRequest reads them), sets the
|
|
23
24
|
# Cache-Control header the selected fields' @cacheControl hints allow, renders
|
|
24
25
|
# the JSON and dispatches EndRequest. In development an error renders as JSON.
|
|
25
26
|
#
|
|
27
|
+
# A field's @throttle that renders past its limit (its `with:` method, or
|
|
28
|
+
# `head :too_many_requests`, rate_limit's default on Rails 7.2) ends the request
|
|
29
|
+
# with that response, as a before_action that renders does: the operation stops
|
|
30
|
+
# there (Performed), and `execute` renders nothing more.
|
|
31
|
+
#
|
|
26
32
|
# Hooks, private so they are not actions: `graphql_context` (default:
|
|
27
33
|
# `{ current_user: current_user }` when the controller has a current_user, else
|
|
28
34
|
# `{}`), `graphql_schema` and `graphql_variables(param)`. A controller with an
|
|
@@ -31,6 +37,11 @@ require 'json'
|
|
|
31
37
|
module Lighthouse::GraphQL::Controller
|
|
32
38
|
extend ActiveSupport::Concern
|
|
33
39
|
|
|
40
|
+
# Raised once a @throttle `with:` method has performed the response: control
|
|
41
|
+
# flow, as `throw :abort` in a before_action, so no `rescue StandardError` (a
|
|
42
|
+
# schema's rescue_from) catches it on its way out of the operation.
|
|
43
|
+
class Performed < Exception; end # rubocop:disable Lint/InheritException
|
|
44
|
+
|
|
34
45
|
included do
|
|
35
46
|
class_attribute :lighthouse_schema_source, instance_writer: false
|
|
36
47
|
end
|
|
@@ -46,17 +57,17 @@ module Lighthouse::GraphQL::Controller
|
|
|
46
57
|
|
|
47
58
|
def execute
|
|
48
59
|
lighthouse_request do
|
|
49
|
-
|
|
50
|
-
if
|
|
51
|
-
render json: lighthouse_batch(
|
|
60
|
+
operations = lighthouse_operations
|
|
61
|
+
if operations.is_a?(Array)
|
|
62
|
+
render json: lighthouse_batch(operations.map { |operation| lighthouse_hash(operation) })
|
|
52
63
|
else
|
|
53
|
-
result = lighthouse_operation
|
|
64
|
+
result = lighthouse_operation(operations)
|
|
54
65
|
Lighthouse::GraphQL::CacheControl.apply(response, result)
|
|
55
66
|
render json: result
|
|
56
67
|
end
|
|
57
68
|
end
|
|
58
69
|
rescue StandardError => e
|
|
59
|
-
raise e unless defined?(::Rails) && ::Rails.env.development?
|
|
70
|
+
raise e unless defined?(::Rails) && ::Rails.env.development? && !rails_response?(e)
|
|
60
71
|
|
|
61
72
|
lighthouse_development_error(e)
|
|
62
73
|
end
|
|
@@ -64,13 +75,18 @@ module Lighthouse::GraphQL::Controller
|
|
|
64
75
|
private
|
|
65
76
|
|
|
66
77
|
# Dispatch StartRequest, run the block, dispatch EndRequest with the
|
|
67
|
-
# response. Only the outermost call dispatches
|
|
78
|
+
# response. Only the outermost call dispatches, and stops the block where a
|
|
79
|
+
# @throttle `with:` method performed the response.
|
|
68
80
|
def lighthouse_request
|
|
69
81
|
return yield if @lighthouse_request_started
|
|
70
82
|
|
|
71
83
|
@lighthouse_request_started = true
|
|
72
84
|
Lighthouse::Events.dispatch(Lighthouse::Events::StartRequest.new(request))
|
|
73
|
-
result =
|
|
85
|
+
result = begin
|
|
86
|
+
yield
|
|
87
|
+
rescue Performed
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
74
90
|
Lighthouse::Events.dispatch(Lighthouse::Events::EndRequest.new(response))
|
|
75
91
|
result
|
|
76
92
|
end
|
|
@@ -80,6 +96,13 @@ module Lighthouse::GraphQL::Controller
|
|
|
80
96
|
respond_to?(:current_user, true) ? { current_user: current_user } : {}
|
|
81
97
|
end
|
|
82
98
|
|
|
99
|
+
# graphql_context, with what @throttle reads unless it sets its own: the
|
|
100
|
+
# client's IP (rate_limit's default by:) and the controller, whose cache_store,
|
|
101
|
+
# controller_path and methods rate_limit uses.
|
|
102
|
+
def lighthouse_context
|
|
103
|
+
{ remote_ip: request.remote_ip, lighthouse_controller: self }.merge(graphql_context)
|
|
104
|
+
end
|
|
105
|
+
|
|
83
106
|
def graphql_schema
|
|
84
107
|
source = lighthouse_schema_source
|
|
85
108
|
case source
|
|
@@ -103,11 +126,23 @@ module Lighthouse::GraphQL::Controller
|
|
|
103
126
|
end
|
|
104
127
|
end
|
|
105
128
|
|
|
106
|
-
|
|
129
|
+
# A multipart request's operation(s), a batch (a JSON array body), or the parameters.
|
|
130
|
+
def lighthouse_operations
|
|
131
|
+
Lighthouse::GraphQL::MultipartRequest.operations(params) || params[:_json] || params
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Parameters, or a Hash from a multipart request's JSON, read by key either way.
|
|
135
|
+
def lighthouse_hash(operation)
|
|
136
|
+
(operation.respond_to?(:to_unsafe_h) ? operation.to_unsafe_h : operation.to_h).with_indifferent_access
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# `operation`: the request's parameters, or a multipart request's operation.
|
|
140
|
+
def lighthouse_operation(operation = params)
|
|
141
|
+
operation = operation.with_indifferent_access if operation.is_a?(Hash)
|
|
107
142
|
Lighthouse::GraphQL::PersistedQueries.execute(
|
|
108
143
|
graphql_schema,
|
|
109
|
-
query:
|
|
110
|
-
variables: graphql_variables(
|
|
144
|
+
query: operation[:query], extensions: operation[:extensions],
|
|
145
|
+
variables: graphql_variables(operation[:variables]), operation_name: operation[:operationName], context: lighthouse_context
|
|
111
146
|
)
|
|
112
147
|
end
|
|
113
148
|
|
|
@@ -116,11 +151,17 @@ module Lighthouse::GraphQL::Controller
|
|
|
116
151
|
def lighthouse_batch(operations)
|
|
117
152
|
operations = operations.map do |operation|
|
|
118
153
|
{ query: operation[:query], extensions: operation[:extensions], variables: graphql_variables(operation[:variables]),
|
|
119
|
-
operation_name: operation[:operationName], context:
|
|
154
|
+
operation_name: operation[:operationName], context: lighthouse_context }
|
|
120
155
|
end
|
|
121
156
|
Lighthouse::GraphQL::PersistedQueries.multiplex(graphql_schema, operations)
|
|
122
157
|
end
|
|
123
158
|
|
|
159
|
+
# An error Rails answers with a status of its own (config.action_dispatch.rescue_responses):
|
|
160
|
+
# ActionController::TooManyRequests from @throttle is a 429 in development too.
|
|
161
|
+
def rails_response?(error)
|
|
162
|
+
::ActionDispatch::ExceptionWrapper.status_code_for_exception(error.class.name) != 500
|
|
163
|
+
end
|
|
164
|
+
|
|
124
165
|
def lighthouse_development_error(error)
|
|
125
166
|
logger.error(error.message)
|
|
126
167
|
logger.error(error.backtrace.join("\n"))
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Eager-loads associations into every record the Dataloader collected, of
|
|
4
|
+
# every parent, in one query per association: @with and @lazyLoad.
|
|
5
|
+
class Lighthouse::GraphQL::DataloaderSources::PreloadLoader < GraphQL::Dataloader::Source
|
|
6
|
+
def initialize(associations, scopes)
|
|
7
|
+
super()
|
|
8
|
+
@associations = associations
|
|
9
|
+
@scopes = scopes
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# keys: the records
|
|
13
|
+
def fetch(records)
|
|
14
|
+
Lighthouse::Support::Preload.records(records, @associations, @scopes)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# How many related records each parent has, for @withCount: every parent the
|
|
4
|
+
# Dataloader collected in one GROUP BY query when the relation can be batched
|
|
5
|
+
# (RelationBatch), else one COUNT per parent. Each count is kept on its parent
|
|
6
|
+
# as `<relation>_count` (RelationCounts).
|
|
7
|
+
class Lighthouse::GraphQL::DataloaderSources::RelationCountsLoader < GraphQL::Dataloader::Source
|
|
8
|
+
# Applies the directive's scopes to the related records, as RelationBatch
|
|
9
|
+
# applies a field's filters. A value, so equal scopes batch together.
|
|
10
|
+
Scopes = Struct.new(:names) do
|
|
11
|
+
def decorate_relation(relation, _args, _context)
|
|
12
|
+
Array(names).reduce(relation) { |scoped, scope| scoped.public_send(scope) }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(relation, scopes)
|
|
17
|
+
super()
|
|
18
|
+
@relation = relation.to_s
|
|
19
|
+
@scopes = Scopes.new(Array(scopes))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# keys: the parent records, of one class or several
|
|
23
|
+
def fetch(parents)
|
|
24
|
+
counts = {}.compare_by_identity
|
|
25
|
+
parents.group_by(&:class).each_value { |group| counts.merge!(counts_for(group)) }
|
|
26
|
+
parents.map do |parent|
|
|
27
|
+
Lighthouse::GraphQL::RelationCounts.write(parent, @relation, counts[parent])
|
|
28
|
+
counts[parent]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# { parent => count } for parents of one class.
|
|
35
|
+
def counts_for(parents)
|
|
36
|
+
batch = Lighthouse::GraphQL::RelationBatch.for(parents.first, @relation, {}, @scopes, nil)
|
|
37
|
+
return parents.index_with { |parent| own_count(parent) } unless batch
|
|
38
|
+
|
|
39
|
+
counts = batch.counts(parents)
|
|
40
|
+
parents.index_with { |parent| counts.fetch(batch.owner_key(parent), 0) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def own_count(parent)
|
|
44
|
+
@scopes.decorate_relation(parent.public_send(@relation), nil, nil).count
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The schema's default resolve while Lighthouse runs it. graphql-ruby asks that
|
|
4
|
+
# one object for every field, for the type of a union's or interface's value
|
|
5
|
+
# (`resolve_type`) and for scalar coercion: the dispatcher answers fields with
|
|
6
|
+
# our generated resolvers, and hands everything else to the implementation it
|
|
7
|
+
# replaced (SchemaImplementation).
|
|
8
|
+
class Lighthouse::GraphQL::DefaultResolveDispatcher
|
|
9
|
+
def initialize(schema, previous)
|
|
10
|
+
@schema = schema
|
|
11
|
+
@previous = previous
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(type, field, obj, arguments, ctx)
|
|
15
|
+
resolver = Lighthouse::GraphQL::GeneratedResolvers.generated_resolver(@schema, type, field)
|
|
16
|
+
return Lighthouse::GraphQL::GeneratedResolvers.invoke(resolver, obj, arguments, ctx) if resolver
|
|
17
|
+
|
|
18
|
+
@previous.call(type, field, obj, arguments, ctx)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def resolve_type(abstract_type, obj, ctx) = @previous.resolve_type(abstract_type, obj, ctx)
|
|
22
|
+
|
|
23
|
+
def coerce_input(type, value, ctx) = @previous.coerce_input(type, value, ctx)
|
|
24
|
+
|
|
25
|
+
def coerce_result(type, value, ctx) = @previous.coerce_result(type, value, ctx)
|
|
26
|
+
end
|
|
@@ -27,7 +27,7 @@ class Lighthouse::GraphQL::DirectiveResolvers::AllDirectiveResolver < Lighthouse
|
|
|
27
27
|
|
|
28
28
|
relation = Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier.apply(relation, @field_def.ast_node, args, ctx, self)
|
|
29
29
|
|
|
30
|
-
relation = apply_where_filters(relation, args)
|
|
30
|
+
relation = apply_where_filters(relation, args, ctx)
|
|
31
31
|
relation = relation.limit(args[:first]) if args[:first]
|
|
32
32
|
relation = relation.offset(args[:offset]) if args[:offset]
|
|
33
33
|
relation
|