lighthouse-graphql 0.4.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 +139 -1
- data/README.md +8 -5
- data/docs/configuration.md +26 -2
- data/docs/directives.md +287 -4
- data/docs/generators.md +82 -0
- data/docs/getting-started.md +26 -30
- data/docs/relationships.md +42 -0
- data/docs/relay.md +137 -0
- data/docs/resolvers.md +261 -22
- data/docs/security.md +17 -0
- data/docs/types.md +219 -0
- data/docs/uploads.md +80 -0
- data/lib/generators/lighthouse/base_classes.rb +32 -49
- data/lib/generators/lighthouse/install/install_generator.rb +83 -24
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +14 -6
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +10 -3
- data/lib/lighthouse/async_mutation_job.rb +17 -0
- data/lib/lighthouse/complexity.rb +5 -4
- data/lib/lighthouse/config.rb +13 -2
- 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 +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 +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 +2 -1
- 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 -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 +53 -6
- 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 +33 -0
- data/lib/lighthouse/relation_counts.rb +29 -0
- data/lib/lighthouse/resolve_info.rb +80 -0
- data/lib/lighthouse/resolver_bases.rb +9 -2
- 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_generator.rb +11 -10
- 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 +26 -0
- data/lib/lighthouse/upload_store.rb +32 -0
- data/sig/lighthouse/directives.rbs +3 -2
- data/sig/lighthouse.rbs +18 -0
- metadata +38 -1
|
@@ -3,72 +3,55 @@
|
|
|
3
3
|
require 'rails/generators'
|
|
4
4
|
require 'lighthouse-graphql'
|
|
5
5
|
|
|
6
|
-
# The base class graphql-ruby's convention puts in each namespace
|
|
7
|
-
# Resolvers::BaseResolver,
|
|
8
|
-
# Subscriptions::BaseSubscription.
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
6
|
+
# The base class graphql-ruby's convention puts in each namespace, which the
|
|
7
|
+
# classes Lighthouse generates inherit from: Resolvers::BaseResolver,
|
|
8
|
+
# Mutations::BaseMutation and Subscriptions::BaseSubscription.
|
|
9
|
+
#
|
|
10
|
+
# graphql-ruby's `graphql:install` writes the first two (and every Types::Base*),
|
|
11
|
+
# and `lighthouse:install` runs it: Lighthouse does not write copies of them. It
|
|
12
|
+
# writes only the base graphql-ruby has none of, BaseSubscription, which
|
|
13
|
+
# includes Lighthouse's subscription concern. A generator that finds a base
|
|
14
|
+
# missing says how to get it.
|
|
12
15
|
#
|
|
13
16
|
# A base that already exists is never replaced, not even with --force: it holds
|
|
14
|
-
# the app's own code
|
|
17
|
+
# the app's own code.
|
|
15
18
|
module Lighthouse::Generators::BaseClasses
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
source: <<~RUBY
|
|
31
|
-
# What every class resolving a Mutation field shares: Lighthouse resolves
|
|
32
|
-
# Mutation.publishPost to Mutations::PublishPost by convention, and every such
|
|
33
|
-
# class inherits from this one. The SDL declares the arguments, so it is a
|
|
34
|
-
# plain GraphQL::Schema::Mutation; the GraphQL::Schema::RelayClassicMutation
|
|
35
|
-
# graphql-ruby's `graphql:install` writes (one `input:` argument) works too.
|
|
36
|
-
class BaseMutation < GraphQL::Schema::Mutation
|
|
37
|
-
end
|
|
38
|
-
RUBY
|
|
39
|
-
},
|
|
40
|
-
subscriptions: {
|
|
41
|
-
name: 'BaseSubscription',
|
|
42
|
-
source: <<~RUBY
|
|
43
|
-
# What every class resolving a Subscription field shares. The concern is PHP
|
|
44
|
-
# Lighthouse's model: every subscriber of a field gets every broadcast, and the
|
|
45
|
-
# class's `filter?` decides who receives it. A subclass that defines
|
|
46
|
-
# graphql-ruby's own `authorized?` or `update` keeps them, and `topic_by
|
|
47
|
-
# :arguments` brings back graphql-ruby's exact-argument matching.
|
|
48
|
-
class BaseSubscription < GraphQL::Schema::Subscription
|
|
49
|
-
include Lighthouse::GraphQL::Subscription
|
|
50
|
-
end
|
|
51
|
-
RUBY
|
|
52
|
-
}
|
|
19
|
+
NAMES = { queries: 'BaseResolver', mutations: 'BaseMutation', subscriptions: 'BaseSubscription' }.freeze
|
|
20
|
+
|
|
21
|
+
# The bases Lighthouse writes itself, graphql-ruby having none.
|
|
22
|
+
SOURCES = {
|
|
23
|
+
subscriptions: <<~RUBY
|
|
24
|
+
# What every class resolving a Subscription field shares. The concern is PHP
|
|
25
|
+
# Lighthouse's model: every subscriber of a field gets every broadcast, and the
|
|
26
|
+
# class's `filter?` decides who receives it. A subclass that defines
|
|
27
|
+
# graphql-ruby's own `authorized?` or `update` keeps them, and `topic_by
|
|
28
|
+
# :arguments` brings back graphql-ruby's exact-argument matching.
|
|
29
|
+
class BaseSubscription < GraphQL::Schema::Subscription
|
|
30
|
+
include Lighthouse::GraphQL::Subscription
|
|
31
|
+
end
|
|
32
|
+
RUBY
|
|
53
33
|
}.freeze
|
|
54
34
|
|
|
55
35
|
private
|
|
56
36
|
|
|
57
37
|
# The base class name for `kind` (`BaseResolver`), or nil for a kind without one.
|
|
58
38
|
def base_class_name(kind)
|
|
59
|
-
|
|
39
|
+
NAMES[kind]
|
|
60
40
|
end
|
|
61
41
|
|
|
62
|
-
# Write the base class of `kind` into `namespace
|
|
42
|
+
# Write the base class of `kind` into `namespace` when Lighthouse owns it and
|
|
43
|
+
# it is missing; say how to get one graphql-ruby owns.
|
|
63
44
|
def write_base_class(kind, namespace)
|
|
64
|
-
|
|
65
|
-
return unless base
|
|
45
|
+
return unless base_class_name(kind)
|
|
66
46
|
|
|
67
47
|
path = base_class_path(kind, namespace)
|
|
68
48
|
if File.exist?(File.join(destination_root, path))
|
|
69
49
|
say_status :skip, "#{path} already exists", :yellow
|
|
50
|
+
elsif SOURCES[kind]
|
|
51
|
+
create_file path, ruby_source(namespace.split('::'), SOURCES[kind])
|
|
70
52
|
else
|
|
71
|
-
|
|
53
|
+
say_status :missing, "#{path}: graphql-ruby writes it — run `bin/rails g lighthouse:install` (which runs graphql:install), " \
|
|
54
|
+
"or write #{namespace}::#{base_class_name(kind)} yourself", :red
|
|
72
55
|
end
|
|
73
56
|
end
|
|
74
57
|
|
|
@@ -1,23 +1,34 @@
|
|
|
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'
|
|
6
7
|
|
|
7
|
-
# `rails generate lighthouse:install` — sets a Rails app up for Lighthouse
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# graphql-
|
|
11
|
-
#
|
|
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:
|
|
12
14
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
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:`);
|
|
20
|
+
#
|
|
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.
|
|
17
26
|
class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
|
|
18
27
|
include Lighthouse::Generators::BaseClasses
|
|
19
28
|
|
|
20
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'
|
|
21
32
|
|
|
22
33
|
source_root File.expand_path('templates', __dir__)
|
|
23
34
|
|
|
@@ -32,6 +43,13 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
|
|
|
32
43
|
class_option :skip_ide, type: :boolean, default: false,
|
|
33
44
|
desc: 'Skip graphql.config.yml and the .gitignore entries for the IDE helper files'
|
|
34
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
|
+
|
|
35
53
|
def create_initializer
|
|
36
54
|
template 'initializer.rb.tt', 'config/initializers/lighthouse.rb'
|
|
37
55
|
end
|
|
@@ -46,23 +64,39 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
|
|
|
46
64
|
end
|
|
47
65
|
|
|
48
66
|
def create_schema_class
|
|
49
|
-
|
|
50
|
-
|
|
67
|
+
return if keep_existing?(schema_path)
|
|
68
|
+
|
|
69
|
+
template 'schema.rb.tt', schema_path, force: true
|
|
70
|
+
end
|
|
51
71
|
|
|
52
|
-
|
|
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) }
|
|
53
79
|
end
|
|
54
80
|
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
def
|
|
58
|
-
|
|
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
|
|
89
|
+
|
|
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)
|
|
59
93
|
end
|
|
60
94
|
|
|
61
95
|
def create_controller
|
|
62
|
-
return if options[:skip_controller]
|
|
96
|
+
return remove_graphql_ruby_controller if options[:skip_controller]
|
|
63
97
|
return if (@kept_controller = keep_existing?(CONTROLLER))
|
|
64
98
|
|
|
65
|
-
template 'graphql_controller.rb.tt', CONTROLLER
|
|
99
|
+
template 'graphql_controller.rb.tt', CONTROLLER, force: true
|
|
66
100
|
end
|
|
67
101
|
|
|
68
102
|
def add_route
|
|
@@ -104,23 +138,48 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
|
|
|
104
138
|
Lighthouse is installed. Next steps:
|
|
105
139
|
1. Describe your API in #{sdl_folder}/*.graphql (the starter schema assumes a User model).
|
|
106
140
|
2. Resolve a field in Ruby when a directive is not enough: bin/rails g lighthouse:query LatestPosts
|
|
107
|
-
(
|
|
108
|
-
|
|
109
|
-
|
|
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}
|
|
110
148
|
MSG
|
|
111
149
|
end
|
|
112
150
|
|
|
113
151
|
private
|
|
114
152
|
|
|
115
|
-
#
|
|
116
|
-
#
|
|
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.
|
|
117
155
|
def keep_existing?(path)
|
|
118
|
-
return false if options[:force] || !
|
|
156
|
+
return false if options[:force] || !before?(path)
|
|
119
157
|
|
|
120
158
|
say_status :skip, "#{path} already exists (pass --force to replace it)", :yellow
|
|
121
159
|
true
|
|
122
160
|
end
|
|
123
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
|
+
|
|
124
183
|
def schema_name
|
|
125
184
|
options[:schema].camelize
|
|
126
185
|
end
|
|
@@ -17,21 +17,29 @@ 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 them with
|
|
23
|
+
# `rails g lighthouse:query|mutation|subscription|type|builder|validator Name`.
|
|
22
24
|
# config.namespaces = {
|
|
23
25
|
# queries: ['Resolvers'], mutations: ['Mutations'], subscriptions: ['Subscriptions'],
|
|
24
|
-
# builders: ['Builders'], validators: ['Validators'], events: ['Events'], analyzers: ['Analyzers']
|
|
26
|
+
# builders: ['Builders'], validators: ['Validators'], events: ['Events'], analyzers: ['Analyzers'],
|
|
27
|
+
# types: ['Types']
|
|
25
28
|
# }
|
|
26
29
|
|
|
27
30
|
# Namespaces tried, in order, for models (@all(model: "User")) and for
|
|
28
|
-
# @field(resolver:) on types other than Query and Mutation
|
|
29
|
-
#
|
|
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.
|
|
30
34
|
# config.resolver_namespaces = ['App::Graphql']
|
|
31
35
|
|
|
32
|
-
# Store for @cache and @
|
|
36
|
+
# Store for @cache, @clearCache and @throttle's counts.
|
|
33
37
|
# config.cache_store = Rails.cache
|
|
34
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
|
+
|
|
35
43
|
# Run @create/@update/@createMany/@updateMany in a database transaction.
|
|
36
44
|
# config.transactional_mutations = true
|
|
37
45
|
|
|
@@ -4,15 +4,22 @@
|
|
|
4
4
|
# Dataloader batches relationship loads (@hasMany, @belongsTo, ...) to avoid N+1.
|
|
5
5
|
# base_types names the base classes: every class a Query, Mutation or Subscription
|
|
6
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.
|
|
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.
|
|
8
10
|
<%= schema_name %> = Lighthouse::GraphQL::RbLightHouse.get_schema(
|
|
9
11
|
options: {
|
|
10
12
|
using: { GraphQL::Dataloader => {} },
|
|
11
13
|
base_types: {
|
|
12
14
|
resolver: <%= base_class_constant(:queries) %>,
|
|
13
15
|
mutation: <%= base_class_constant(:mutations) %>,
|
|
14
|
-
subscription: <%= base_class_constant(:subscriptions)
|
|
15
|
-
|
|
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
|
|
16
23
|
}
|
|
17
24
|
}
|
|
18
25
|
)
|
|
@@ -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
|
|
@@ -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
|
@@ -20,7 +20,8 @@ module Lighthouse
|
|
|
20
20
|
validators: ['Validators'],
|
|
21
21
|
events: ['Events'],
|
|
22
22
|
subscriptions: ['Subscriptions'],
|
|
23
|
-
analyzers: ['Analyzers']
|
|
23
|
+
analyzers: ['Analyzers'],
|
|
24
|
+
types: ['Types']
|
|
24
25
|
}.freeze
|
|
25
26
|
|
|
26
27
|
# Every setting reset! forgets, so the next read falls back to its default.
|
|
@@ -32,13 +33,14 @@ module Lighthouse
|
|
|
32
33
|
convert_empty_strings_to_null
|
|
33
34
|
soft_deletes
|
|
34
35
|
queue_broadcasts broadcast_queue
|
|
36
|
+
global_id upload_store
|
|
35
37
|
].freeze
|
|
36
38
|
|
|
37
39
|
class << self
|
|
38
40
|
attr_writer :sdl_folder, :resolver_namespaces, :on_error, :authenticated_user, :policy_user, :authorizer, :cache_store,
|
|
39
41
|
:transactional_mutations, :schema_cache, :watch_sdl, :persisted_queries, :persisted_queries_store, :persisted_queries_only,
|
|
40
42
|
:queue_broadcasts,
|
|
41
|
-
:convert_empty_strings_to_null, :feature_checker, :feature_actor
|
|
43
|
+
:convert_empty_strings_to_null, :feature_checker, :feature_actor, :global_id, :upload_store
|
|
42
44
|
|
|
43
45
|
def sdl_folder
|
|
44
46
|
@sdl_folder ||= (defined?(Rails) && Rails.respond_to?(:root) ? Rails.root.join('graphql').to_s : nil)
|
|
@@ -104,6 +106,15 @@ module Lighthouse
|
|
|
104
106
|
# when the schema is built, so set it in an initializer.
|
|
105
107
|
def convert_empty_strings_to_null = @convert_empty_strings_to_null || false
|
|
106
108
|
|
|
109
|
+
# Encodes and decodes Relay global ids (@globalId, @node): `encode(type, id)`
|
|
110
|
+
# and `decode(global_id)` -> [type, id]. Base64 of "Type:id" by default.
|
|
111
|
+
def global_id = @global_id || Lighthouse::GraphQL::GlobalId
|
|
112
|
+
|
|
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
|
+
|
|
107
118
|
def persisted_queries_store
|
|
108
119
|
@persisted_queries_store || cache_store
|
|
109
120
|
end
|
|
@@ -19,7 +19,8 @@ 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
|
#
|
|
@@ -46,11 +47,11 @@ module Lighthouse::GraphQL::Controller
|
|
|
46
47
|
|
|
47
48
|
def execute
|
|
48
49
|
lighthouse_request do
|
|
49
|
-
|
|
50
|
-
if
|
|
51
|
-
render json: lighthouse_batch(
|
|
50
|
+
operations = lighthouse_operations
|
|
51
|
+
if operations.is_a?(Array)
|
|
52
|
+
render json: lighthouse_batch(operations.map { |operation| lighthouse_hash(operation) })
|
|
52
53
|
else
|
|
53
|
-
result = lighthouse_operation
|
|
54
|
+
result = lighthouse_operation(operations)
|
|
54
55
|
Lighthouse::GraphQL::CacheControl.apply(response, result)
|
|
55
56
|
render json: result
|
|
56
57
|
end
|
|
@@ -80,6 +81,11 @@ module Lighthouse::GraphQL::Controller
|
|
|
80
81
|
respond_to?(:current_user, true) ? { current_user: current_user } : {}
|
|
81
82
|
end
|
|
82
83
|
|
|
84
|
+
# graphql_context, with the client's IP (for @throttle) unless it sets its own.
|
|
85
|
+
def lighthouse_context
|
|
86
|
+
{ remote_ip: request.remote_ip }.merge(graphql_context)
|
|
87
|
+
end
|
|
88
|
+
|
|
83
89
|
def graphql_schema
|
|
84
90
|
source = lighthouse_schema_source
|
|
85
91
|
case source
|
|
@@ -103,11 +109,23 @@ module Lighthouse::GraphQL::Controller
|
|
|
103
109
|
end
|
|
104
110
|
end
|
|
105
111
|
|
|
106
|
-
|
|
112
|
+
# A multipart request's operation(s), a batch (a JSON array body), or the parameters.
|
|
113
|
+
def lighthouse_operations
|
|
114
|
+
Lighthouse::GraphQL::MultipartRequest.operations(params) || params[:_json] || params
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Parameters, or a Hash from a multipart request's JSON, read by key either way.
|
|
118
|
+
def lighthouse_hash(operation)
|
|
119
|
+
(operation.respond_to?(:to_unsafe_h) ? operation.to_unsafe_h : operation.to_h).with_indifferent_access
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# `operation`: the request's parameters, or a multipart request's operation.
|
|
123
|
+
def lighthouse_operation(operation = params)
|
|
124
|
+
operation = operation.with_indifferent_access if operation.is_a?(Hash)
|
|
107
125
|
Lighthouse::GraphQL::PersistedQueries.execute(
|
|
108
126
|
graphql_schema,
|
|
109
|
-
query:
|
|
110
|
-
variables: graphql_variables(
|
|
127
|
+
query: operation[:query], extensions: operation[:extensions],
|
|
128
|
+
variables: graphql_variables(operation[:variables]), operation_name: operation[:operationName], context: lighthouse_context
|
|
111
129
|
)
|
|
112
130
|
end
|
|
113
131
|
|
|
@@ -116,7 +134,7 @@ module Lighthouse::GraphQL::Controller
|
|
|
116
134
|
def lighthouse_batch(operations)
|
|
117
135
|
operations = operations.map do |operation|
|
|
118
136
|
{ query: operation[:query], extensions: operation[:extensions], variables: graphql_variables(operation[:variables]),
|
|
119
|
-
operation_name: operation[:operationName], context:
|
|
137
|
+
operation_name: operation[:operationName], context: lighthouse_context }
|
|
120
138
|
end
|
|
121
139
|
Lighthouse::GraphQL::PersistedQueries.multiplex(graphql_schema, operations)
|
|
122
140
|
end
|
|
@@ -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
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# What @bind(class:) puts in place of an argument's value: the record of a model
|
|
4
|
+
# whose `column` matches it (a list's records, for a list), eager-loading `with`;
|
|
5
|
+
# or what a class's `call(value, context)` returns. A value that matches nothing
|
|
6
|
+
# is a validation error on the argument, as PHP Lighthouse's, unless `optional`.
|
|
7
|
+
class Lighthouse::GraphQL::DirectiveResolvers::ArgumentBinding
|
|
8
|
+
def initialize(directive, argument, field_def)
|
|
9
|
+
@directive = directive
|
|
10
|
+
@argument = argument
|
|
11
|
+
@field_def = field_def
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def bind(value, ctx)
|
|
15
|
+
klass = Lighthouse::Support::ModelResolver.constantize(arg('class'))
|
|
16
|
+
raise Lighthouse::DefinitionError, "@bind(class: #{arg('class').inspect}) on #{@argument.name} names no class." unless klass
|
|
17
|
+
return call(klass, value, ctx) unless model?(klass)
|
|
18
|
+
|
|
19
|
+
value.is_a?(Array) ? records(klass, value) : record(klass, value)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def arg(name)
|
|
25
|
+
Lighthouse::Support::DirectiveArgs.literal(@directive, name)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def model?(klass)
|
|
29
|
+
defined?(::ActiveRecord::Base) && klass.is_a?(Class) && klass < ::ActiveRecord::Base
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def call(klass, value, ctx)
|
|
33
|
+
bound = (klass.respond_to?(:call) ? klass : klass.new).call(value, ctx)
|
|
34
|
+
bound.nil? ? missing : bound
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def scope(klass)
|
|
38
|
+
with = Array(arg('with'))
|
|
39
|
+
with.empty? ? klass.all : klass.preload(*Lighthouse::Support::Preload.associations(with))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def record(klass, value)
|
|
43
|
+
scope(klass).find_by(column => value) || missing
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def records(klass, values)
|
|
47
|
+
found = scope(klass).where(column => values).index_by { |record| record.public_send(column).to_s }
|
|
48
|
+
bound = values.map { |value| found[value.to_s] }
|
|
49
|
+
bound.include?(nil) ? missing(bound) : bound
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def column
|
|
53
|
+
arg('column') || 'id'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def missing(partial = nil)
|
|
57
|
+
return partial if arg('optional')
|
|
58
|
+
|
|
59
|
+
raise Lighthouse::ValidationError.new(@field_def.name, @argument.name => ["The selected #{@argument.name} is invalid."])
|
|
60
|
+
end
|
|
61
|
+
end
|