lighthouse-graphql 0.5.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 +118 -1
- data/README.md +24 -16
- data/docs/caching.md +4 -3
- data/docs/configuration.md +1 -21
- data/docs/directives.md +14 -7
- data/docs/generators.md +41 -39
- data/docs/getting-started.md +88 -51
- data/docs/performance.md +2 -2
- data/docs/relationships.md +5 -4
- data/docs/relay.md +63 -0
- data/docs/resolvers.md +90 -37
- data/docs/security.md +64 -16
- data/docs/subscriptions.md +16 -9
- data/docs/types.md +16 -17
- data/docs/uploads.md +41 -14
- data/lib/generators/lighthouse/base_classes.rb +9 -14
- data/lib/generators/lighthouse/install/graphql_ruby_install.rb +113 -0
- data/lib/generators/lighthouse/install/install_generator.rb +112 -91
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +3 -9
- data/lib/lighthouse/caching.rb +9 -3
- data/lib/lighthouse/config.rb +6 -23
- data/lib/lighthouse/controller.rb +28 -5
- data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +18 -18
- data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +1 -1
- data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +3 -1
- data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +85 -26
- data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +12 -6
- data/lib/lighthouse/directives/arguments/more_filters.rb +1 -1
- data/lib/lighthouse/field_resolver_stack.rb +9 -6
- data/lib/lighthouse/global_id.rb +29 -3
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/graphql_ruby_patches.rb +45 -0
- data/lib/lighthouse/rb_lighthouse.rb +1 -0
- data/lib/lighthouse/resolver_bases.rb +18 -4
- data/lib/lighthouse/resolver_dispatch.rb +16 -1
- data/lib/lighthouse/root_types.rb +117 -0
- data/lib/lighthouse/schema_generator.rb +4 -3
- data/lib/lighthouse/upload.rb +4 -2
- data/lib/lighthouse/upload_store.rb +39 -14
- data/sig/lighthouse.rbs +0 -10
- metadata +3 -4
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +0 -29
- data/lib/generators/lighthouse/mutation_generator.rb +0 -31
- data/lib/generators/lighthouse/query_generator.rb +0 -30
|
@@ -4,50 +4,66 @@ require 'rails/generators'
|
|
|
4
4
|
require 'generators/graphql/install_generator'
|
|
5
5
|
require 'lighthouse-graphql'
|
|
6
6
|
require_relative '../base_classes'
|
|
7
|
+
require_relative 'graphql_ruby_install'
|
|
7
8
|
|
|
8
|
-
# `rails generate lighthouse:install` —
|
|
9
|
-
#
|
|
10
|
-
# --
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# Lighthouse then puts its own over what graphql-ruby wrote in this run:
|
|
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:
|
|
14
14
|
#
|
|
15
|
-
# - the schema class
|
|
16
|
-
#
|
|
17
|
-
# -
|
|
18
|
-
#
|
|
19
|
-
#
|
|
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.
|
|
20
31
|
#
|
|
21
|
-
#
|
|
22
|
-
# and
|
|
23
|
-
#
|
|
24
|
-
# it), so an app already serving a graphql-ruby schema keeps doing so until it
|
|
25
|
-
# is pointed at ours.
|
|
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.
|
|
26
35
|
class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
|
|
27
36
|
include Lighthouse::Generators::BaseClasses
|
|
37
|
+
include Lighthouse::Generators::GraphqlRubyInstall
|
|
28
38
|
|
|
29
39
|
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'
|
|
32
40
|
|
|
33
41
|
source_root File.expand_path('templates', __dir__)
|
|
34
42
|
|
|
35
|
-
desc
|
|
43
|
+
desc "Install Lighthouse on graphql-ruby's install: schema class, base classes, controller, initializer, starter SDL and IDE config."
|
|
36
44
|
|
|
37
|
-
class_option :schema, type: :string,
|
|
38
|
-
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)"
|
|
39
47
|
class_option :sdl_folder, type: :string, default: 'graphql',
|
|
40
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'
|
|
41
51
|
class_option :skip_controller, type: :boolean, default: false,
|
|
42
|
-
desc: '
|
|
52
|
+
desc: "Leave graphql-ruby's GraphqlController as it is"
|
|
43
53
|
class_option :skip_ide, type: :boolean, default: false,
|
|
44
54
|
desc: 'Skip graphql.config.yml and the .gitignore entries for the IDE helper files'
|
|
45
55
|
|
|
46
|
-
# graphql-ruby's install
|
|
47
|
-
# replaces is noted, so a file the app had before is never touched.
|
|
56
|
+
# graphql-ruby's install: the app's, or one now, with graphql-ruby's defaults.
|
|
48
57
|
def install_graphql_ruby
|
|
49
|
-
|
|
50
|
-
|
|
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.'
|
|
51
67
|
end
|
|
52
68
|
|
|
53
69
|
def create_initializer
|
|
@@ -63,38 +79,60 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
|
|
|
63
79
|
template 'schema.graphql.tt', File.join(sdl_folder, 'schema.graphql')
|
|
64
80
|
end
|
|
65
81
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
template 'schema.rb.tt', schema_path, force: true
|
|
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]
|
|
70
85
|
end
|
|
71
86
|
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
77
94
|
|
|
78
|
-
|
|
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')
|
|
79
102
|
end
|
|
80
103
|
|
|
81
|
-
# The
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
gsub_file BASE_MUTATION, /^\s*input_object_class .*\n/, ''
|
|
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
|
|
88
110
|
end
|
|
89
111
|
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
93
128
|
end
|
|
94
129
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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))
|
|
98
136
|
|
|
99
137
|
template 'graphql_controller.rb.tt', CONTROLLER, force: true
|
|
100
138
|
end
|
|
@@ -106,8 +144,7 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
|
|
|
106
144
|
return unless File.exist?(routes)
|
|
107
145
|
return if File.read(routes).include?('graphql#execute')
|
|
108
146
|
|
|
109
|
-
# Byte-identical to graphql-ruby's install route
|
|
110
|
-
# runs second finds it in place and does not add it again.
|
|
147
|
+
# Byte-identical to graphql-ruby's install route.
|
|
111
148
|
route 'post "/graphql", to: "graphql#execute"'
|
|
112
149
|
end
|
|
113
150
|
|
|
@@ -128,21 +165,22 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
|
|
|
128
165
|
def show_next_steps
|
|
129
166
|
serve =
|
|
130
167
|
if @kept_controller
|
|
131
|
-
"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(...)."
|
|
132
170
|
else
|
|
133
171
|
'Query it: POST /graphql {"query": "{ users { data { id name } paginatorInfo { total } } }"}'
|
|
134
172
|
end
|
|
135
173
|
|
|
136
174
|
say <<~MSG
|
|
137
175
|
|
|
138
|
-
Lighthouse is installed. Next steps:
|
|
176
|
+
Lighthouse is installed on graphql-ruby's install (#{schema_path}). Next steps:
|
|
139
177
|
1. Describe your API in #{sdl_folder}/*.graphql (the starter schema assumes a User model).
|
|
140
|
-
2. Resolve a field in Ruby when a directive is not enough
|
|
141
|
-
(
|
|
142
|
-
|
|
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.
|
|
143
181
|
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
|
-
|
|
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)
|
|
146
184
|
4. Generate the IDE helper files: bin/rails lighthouse:ide_helper
|
|
147
185
|
5. #{serve}
|
|
148
186
|
MSG
|
|
@@ -150,43 +188,26 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
|
|
|
150
188
|
|
|
151
189
|
private
|
|
152
190
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
say_status :skip, "#{path} already exists (pass --force to replace it)", :yellow
|
|
159
|
-
true
|
|
160
|
-
end
|
|
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')
|
|
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})"
|
|
181
195
|
end
|
|
182
196
|
|
|
183
|
-
def
|
|
184
|
-
|
|
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
|
|
185
204
|
end
|
|
186
205
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
190
211
|
end
|
|
191
212
|
|
|
192
213
|
def sdl_folder
|
|
@@ -19,8 +19,9 @@ Lighthouse.configure do |config|
|
|
|
19
19
|
# @event(dispatch:) classes come from :events, and the graphql-ruby analyzers
|
|
20
20
|
# @complexity(resolver:) names from :analyzers. Post.readingTime, with no
|
|
21
21
|
# resolving directive, resolves with Types::PostType#reading_time (a
|
|
22
|
-
# Types::BaseObject including Lighthouse::GraphQL::Type), from :types. Generate
|
|
23
|
-
# `rails g lighthouse:
|
|
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`.
|
|
24
25
|
# config.namespaces = {
|
|
25
26
|
# queries: ['Resolvers'], mutations: ['Mutations'], subscriptions: ['Subscriptions'],
|
|
26
27
|
# builders: ['Builders'], validators: ['Validators'], events: ['Events'], analyzers: ['Analyzers'],
|
|
@@ -33,13 +34,6 @@ Lighthouse.configure do |config|
|
|
|
33
34
|
# Fully-qualified names (containing `::`) are used as-is.
|
|
34
35
|
# config.resolver_namespaces = ['App::Graphql']
|
|
35
36
|
|
|
36
|
-
# Store for @cache, @clearCache and @throttle's counts.
|
|
37
|
-
# config.cache_store = Rails.cache
|
|
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
|
-
|
|
43
37
|
# Run @create/@update/@createMany/@updateMany in a database transaction.
|
|
44
38
|
# config.transactional_mutations = true
|
|
45
39
|
|
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)
|
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)
|
|
@@ -26,21 +24,21 @@ module Lighthouse
|
|
|
26
24
|
|
|
27
25
|
# Every setting reset! forgets, so the next read falls back to its default.
|
|
28
26
|
SETTINGS = %i[
|
|
29
|
-
sdl_folder resolver_namespaces namespaces
|
|
27
|
+
sdl_folder resolver_namespaces namespaces transactional_mutations pagination_default_count pagination_max_count
|
|
30
28
|
schema_cache schema_cache_path watch_sdl persisted_queries persisted_queries_store persisted_queries_ttl
|
|
31
29
|
persisted_queries_manifest persisted_queries_only on_error directive_map authenticated_user policy_user authorizer
|
|
32
30
|
feature_checker feature_actor
|
|
33
31
|
convert_empty_strings_to_null
|
|
34
32
|
soft_deletes
|
|
35
33
|
queue_broadcasts broadcast_queue
|
|
36
|
-
global_id
|
|
34
|
+
global_id
|
|
37
35
|
].freeze
|
|
38
36
|
|
|
39
37
|
class << self
|
|
40
|
-
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,
|
|
41
39
|
:transactional_mutations, :schema_cache, :watch_sdl, :persisted_queries, :persisted_queries_store, :persisted_queries_only,
|
|
42
40
|
:queue_broadcasts,
|
|
43
|
-
:convert_empty_strings_to_null, :feature_checker, :feature_actor, :global_id
|
|
41
|
+
:convert_empty_strings_to_null, :feature_checker, :feature_actor, :global_id
|
|
44
42
|
|
|
45
43
|
def sdl_folder
|
|
46
44
|
@sdl_folder ||= (defined?(Rails) && Rails.respond_to?(:root) ? Rails.root.join('graphql').to_s : nil)
|
|
@@ -66,16 +64,6 @@ module Lighthouse
|
|
|
66
64
|
@namespaces = namespaces.merge(overrides.to_h.transform_keys(&:to_sym).transform_values { |v| Array(v) })
|
|
67
65
|
end
|
|
68
66
|
|
|
69
|
-
# Store backing @cache and @clearCache. Defaults to Rails.cache.
|
|
70
|
-
def cache_store
|
|
71
|
-
@cache_store ||=
|
|
72
|
-
if defined?(Rails) && Rails.respond_to?(:cache) && Rails.cache
|
|
73
|
-
Rails.cache
|
|
74
|
-
else
|
|
75
|
-
ActiveSupport::Cache::MemoryStore.new
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
|
|
79
67
|
# --- Subscriptions (used by @broadcast) ----------------------------------
|
|
80
68
|
#
|
|
81
69
|
# queue_broadcasts perform @broadcast triggers in a Lighthouse::GraphQL::BroadcastJob
|
|
@@ -91,7 +79,7 @@ module Lighthouse
|
|
|
91
79
|
# schema_cache_path where; default Rails.root/tmp/cache/lighthouse
|
|
92
80
|
# watch_sdl reload the app (and so the schema) when a .graphql file changes
|
|
93
81
|
# persisted_queries accept Apollo's Automatic Persisted Queries (PersistedQueries)
|
|
94
|
-
# persisted_queries_store where APQ keeps queries; default
|
|
82
|
+
# persisted_queries_store where APQ keeps queries; default Rails.cache
|
|
95
83
|
# persisted_queries_ttl how long they live there (seconds; nil: until evicted)
|
|
96
84
|
# persisted_queries_manifest a manifest of trusted operations, served by hash
|
|
97
85
|
# persisted_queries_only reject any operation not in the manifest
|
|
@@ -110,13 +98,8 @@ module Lighthouse
|
|
|
110
98
|
# and `decode(global_id)` -> [type, id]. Base64 of "Type:id" by default.
|
|
111
99
|
def global_id = @global_id || Lighthouse::GraphQL::GlobalId
|
|
112
100
|
|
|
113
|
-
# Stores the file an @upload argument carries: `call(file, disk:, path:,
|
|
114
|
-
# public:)` returns what the argument holds instead. An ActiveStorage blob's
|
|
115
|
-
# signed id by default.
|
|
116
|
-
def upload_store = @upload_store || Lighthouse::GraphQL::UploadStore
|
|
117
|
-
|
|
118
101
|
def persisted_queries_store
|
|
119
|
-
@persisted_queries_store ||
|
|
102
|
+
@persisted_queries_store || Lighthouse::GraphQL::Caching.store
|
|
120
103
|
end
|
|
121
104
|
|
|
122
105
|
# Pagination defaults, overridden by a directive's defaultCount / maxCount.
|
|
@@ -24,6 +24,11 @@ require 'json'
|
|
|
24
24
|
# Cache-Control header the selected fields' @cacheControl hints allow, renders
|
|
25
25
|
# the JSON and dispatches EndRequest. In development an error renders as JSON.
|
|
26
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
|
+
#
|
|
27
32
|
# Hooks, private so they are not actions: `graphql_context` (default:
|
|
28
33
|
# `{ current_user: current_user }` when the controller has a current_user, else
|
|
29
34
|
# `{}`), `graphql_schema` and `graphql_variables(param)`. A controller with an
|
|
@@ -32,6 +37,11 @@ require 'json'
|
|
|
32
37
|
module Lighthouse::GraphQL::Controller
|
|
33
38
|
extend ActiveSupport::Concern
|
|
34
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
|
+
|
|
35
45
|
included do
|
|
36
46
|
class_attribute :lighthouse_schema_source, instance_writer: false
|
|
37
47
|
end
|
|
@@ -57,7 +67,7 @@ module Lighthouse::GraphQL::Controller
|
|
|
57
67
|
end
|
|
58
68
|
end
|
|
59
69
|
rescue StandardError => e
|
|
60
|
-
raise e unless defined?(::Rails) && ::Rails.env.development?
|
|
70
|
+
raise e unless defined?(::Rails) && ::Rails.env.development? && !rails_response?(e)
|
|
61
71
|
|
|
62
72
|
lighthouse_development_error(e)
|
|
63
73
|
end
|
|
@@ -65,13 +75,18 @@ module Lighthouse::GraphQL::Controller
|
|
|
65
75
|
private
|
|
66
76
|
|
|
67
77
|
# Dispatch StartRequest, run the block, dispatch EndRequest with the
|
|
68
|
-
# 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.
|
|
69
80
|
def lighthouse_request
|
|
70
81
|
return yield if @lighthouse_request_started
|
|
71
82
|
|
|
72
83
|
@lighthouse_request_started = true
|
|
73
84
|
Lighthouse::Events.dispatch(Lighthouse::Events::StartRequest.new(request))
|
|
74
|
-
result =
|
|
85
|
+
result = begin
|
|
86
|
+
yield
|
|
87
|
+
rescue Performed
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
75
90
|
Lighthouse::Events.dispatch(Lighthouse::Events::EndRequest.new(response))
|
|
76
91
|
result
|
|
77
92
|
end
|
|
@@ -81,9 +96,11 @@ module Lighthouse::GraphQL::Controller
|
|
|
81
96
|
respond_to?(:current_user, true) ? { current_user: current_user } : {}
|
|
82
97
|
end
|
|
83
98
|
|
|
84
|
-
# graphql_context, with
|
|
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.
|
|
85
102
|
def lighthouse_context
|
|
86
|
-
{ remote_ip: request.remote_ip }.merge(graphql_context)
|
|
103
|
+
{ remote_ip: request.remote_ip, lighthouse_controller: self }.merge(graphql_context)
|
|
87
104
|
end
|
|
88
105
|
|
|
89
106
|
def graphql_schema
|
|
@@ -139,6 +156,12 @@ module Lighthouse::GraphQL::Controller
|
|
|
139
156
|
Lighthouse::GraphQL::PersistedQueries.multiplex(graphql_schema, operations)
|
|
140
157
|
end
|
|
141
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
|
+
|
|
142
165
|
def lighthouse_development_error(error)
|
|
143
166
|
logger.error(error.message)
|
|
144
167
|
logger.error(error.backtrace.join("\n"))
|
|
@@ -11,7 +11,7 @@ require_relative 'base'
|
|
|
11
11
|
# @hash the value hashed with BCrypt, for a password column
|
|
12
12
|
# @drop the value removed: the resolver never sees it
|
|
13
13
|
# @bind(class:) the value replaced by the record(s) it identifies
|
|
14
|
-
# @upload the uploaded file stored (
|
|
14
|
+
# @upload the uploaded file stored with ActiveStorage (UploadStore), its blob's signed id passed
|
|
15
15
|
#
|
|
16
16
|
# type Mutation {
|
|
17
17
|
# signUp(email: String! @trim, password: String! @trim @hash, captcha: String @drop): User! @create
|
|
@@ -60,26 +60,26 @@ class Lighthouse::GraphQL::DirectiveResolvers::ArgumentTransformsResolver < Ligh
|
|
|
60
60
|
"A model class, or a class whose `call(value, context)` returns what the value stands for."
|
|
61
61
|
class: String!
|
|
62
62
|
"The column the value is matched against, for a model."
|
|
63
|
-
column: String
|
|
63
|
+
column: String = "id"
|
|
64
64
|
"Relations to eager-load on the bound records."
|
|
65
|
-
with: [String!]
|
|
65
|
+
with: [String!] = []
|
|
66
66
|
"Pass null instead of failing when no record matches."
|
|
67
|
-
optional: Boolean
|
|
67
|
+
optional: Boolean = false
|
|
68
68
|
) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
|
|
69
69
|
GRAPHQL
|
|
70
70
|
'upload' => <<~GRAPHQL.chomp
|
|
71
71
|
"""
|
|
72
|
-
Store the file an Upload argument carries, and pass along
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
Store the file an Upload argument carries with ActiveStorage, and pass along the
|
|
73
|
+
signed id of its blob instead, which an attachment attribute (has_one_attached)
|
|
74
|
+
attaches. This does not change the schema from a client perspective.
|
|
75
75
|
"""
|
|
76
76
|
directive @upload(
|
|
77
|
-
"The
|
|
77
|
+
"The ActiveStorage service to store the file on (config/storage.yml). Defaults to config.active_storage.service."
|
|
78
78
|
disk: String
|
|
79
|
-
"
|
|
80
|
-
path: String
|
|
81
|
-
"
|
|
82
|
-
public: Boolean
|
|
79
|
+
"The directory the stored file's key starts with."
|
|
80
|
+
path: String = "/"
|
|
81
|
+
"Whether the file is public. ActiveStorage sets that per service, so it must agree with the service's."
|
|
82
|
+
public: Boolean = false
|
|
83
83
|
) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
|
|
84
84
|
GRAPHQL
|
|
85
85
|
}.freeze
|
|
@@ -154,8 +154,8 @@ class Lighthouse::GraphQL::DirectiveResolvers::ArgumentTransformsResolver < Ligh
|
|
|
154
154
|
send(TRANSFORMS.fetch(directive.name), directive, argument, value, ctx)
|
|
155
155
|
end
|
|
156
156
|
|
|
157
|
-
def decode_global_id(directive, _argument, value,
|
|
158
|
-
decode(value, Lighthouse::Support::DirectiveArgs.literal(directive, 'decode') || 'ARRAY')
|
|
157
|
+
def decode_global_id(directive, _argument, value, ctx)
|
|
158
|
+
decode(value, Lighthouse::Support::DirectiveArgs.literal(directive, 'decode') || 'ARRAY', ctx)
|
|
159
159
|
end
|
|
160
160
|
|
|
161
161
|
def trim_value(_directive, _argument, value, _ctx) = trim(value)
|
|
@@ -166,13 +166,13 @@ class Lighthouse::GraphQL::DirectiveResolvers::ArgumentTransformsResolver < Ligh
|
|
|
166
166
|
return value.map { |item| upload(directive, argument, item, ctx) } if value.is_a?(Array)
|
|
167
167
|
|
|
168
168
|
options = %w[disk path public].to_h { |name| [name.to_sym, Lighthouse::Support::DirectiveArgs.literal(directive, name)] }
|
|
169
|
-
Lighthouse::
|
|
169
|
+
Lighthouse::GraphQL::UploadStore.call(value, argument: argument.graphql_name, **options)
|
|
170
170
|
end
|
|
171
171
|
|
|
172
|
-
def decode(value, mode)
|
|
173
|
-
return value.map { |item| decode(item, mode) } if value.is_a?(Array)
|
|
172
|
+
def decode(value, mode, ctx = nil)
|
|
173
|
+
return value.map { |item| decode(item, mode, ctx) } if value.is_a?(Array)
|
|
174
174
|
|
|
175
|
-
type, id = Lighthouse::GraphQL::GlobalId.decode!(value)
|
|
175
|
+
type, id = Lighthouse::GraphQL::GlobalId.decode!(value, ctx)
|
|
176
176
|
{ 'TYPE' => type, 'ID' => id }.fetch(mode.to_s, [type, id])
|
|
177
177
|
end
|
|
178
178
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require_relative 'base'
|
|
4
4
|
|
|
5
5
|
module Lighthouse::GraphQL::DirectiveResolvers
|
|
6
|
-
# @cache — keep a field's resolved value in
|
|
6
|
+
# @cache — keep a field's resolved value in Rails.cache (Caching.store).
|
|
7
7
|
#
|
|
8
8
|
# The key covers the parent type and its key (the field marked @cacheKey,
|
|
9
9
|
# else the first `ID` field; none on Query/Mutation), the field, and its
|
|
@@ -51,7 +51,9 @@ class Lighthouse::GraphQL::DirectiveResolvers::GlobalIdDirectiveResolver < Light
|
|
|
51
51
|
|
|
52
52
|
install(target_type_class, lambda do |obj, args, ctx|
|
|
53
53
|
id = inner.call(obj, args, ctx)
|
|
54
|
-
id.nil?
|
|
54
|
+
next if id.nil?
|
|
55
|
+
|
|
56
|
+
Lighthouse::GraphQL::GlobalId.encode_field(target_type_class, obj, id, ctx) || Lighthouse::Config.global_id.encode(type_name, id)
|
|
55
57
|
end)
|
|
56
58
|
end
|
|
57
59
|
end
|