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
|
@@ -29,6 +29,8 @@ require_relative 'namespaces'
|
|
|
29
29
|
support/arguments
|
|
30
30
|
support/batching
|
|
31
31
|
support/soft_deletes
|
|
32
|
+
support/preload
|
|
33
|
+
support/environment
|
|
32
34
|
where_conditions/operator_map
|
|
33
35
|
contracts/directive
|
|
34
36
|
contracts/field_resolver
|
|
@@ -38,6 +40,7 @@ require_relative 'namespaces'
|
|
|
38
40
|
contracts/arg_builder
|
|
39
41
|
sdl_loader
|
|
40
42
|
generated_resolvers
|
|
43
|
+
default_resolve_dispatcher
|
|
41
44
|
federation_directives
|
|
42
45
|
field_resolver_stack
|
|
43
46
|
events
|
|
@@ -45,6 +48,7 @@ require_relative 'namespaces'
|
|
|
45
48
|
events/execution_trace
|
|
46
49
|
events/schema_build
|
|
47
50
|
resolver_bases
|
|
51
|
+
root_types
|
|
48
52
|
schema_generator
|
|
49
53
|
schema_cache
|
|
50
54
|
persisted_queries
|
|
@@ -60,12 +64,26 @@ require_relative 'namespaces'
|
|
|
60
64
|
complexity_analyzer
|
|
61
65
|
broadcasting
|
|
62
66
|
subscription
|
|
67
|
+
type
|
|
68
|
+
type_resolution
|
|
69
|
+
resolve_info
|
|
70
|
+
global_id
|
|
71
|
+
enum_directive
|
|
72
|
+
upload
|
|
73
|
+
upload_store
|
|
74
|
+
multipart_request
|
|
75
|
+
node_registry
|
|
76
|
+
scalar_coercion
|
|
63
77
|
pagination
|
|
64
78
|
relation_batch
|
|
79
|
+
relation_counts
|
|
65
80
|
nested_mutations
|
|
66
81
|
directives/pagination_manipulator
|
|
67
82
|
directives/where_conditions_directive
|
|
68
83
|
directives/soft_deletes_directive
|
|
84
|
+
directives/node_directive
|
|
85
|
+
directives/model_directive
|
|
86
|
+
directives/visibility_directive
|
|
69
87
|
directive_resolvers/aggregate_resolver
|
|
70
88
|
directive_resolvers/field_resolver
|
|
71
89
|
directive_resolvers/where_conditions_applier
|
|
@@ -83,6 +101,7 @@ require_relative 'namespaces'
|
|
|
83
101
|
directive_resolvers/association_resolvers
|
|
84
102
|
directive_resolvers/can_resolvers
|
|
85
103
|
directive_resolvers/convention_resolver
|
|
104
|
+
directive_resolvers/type_method_resolver
|
|
86
105
|
directive_resolvers/model_mutation_resolver
|
|
87
106
|
directive_resolvers/soft_delete_resolvers
|
|
88
107
|
directive_resolvers/cache_resolvers
|
|
@@ -91,15 +110,29 @@ require_relative 'namespaces'
|
|
|
91
110
|
directive_resolvers/broadcast_resolver
|
|
92
111
|
directive_resolvers/event_resolver
|
|
93
112
|
directive_resolvers/convert_empty_strings_resolver
|
|
113
|
+
directive_resolvers/with_resolver
|
|
114
|
+
directive_resolvers/with_count_resolver
|
|
115
|
+
directive_resolvers/lazy_load_resolver
|
|
116
|
+
directive_resolvers/limit_resolver
|
|
117
|
+
directive_resolvers/namespaced_resolver
|
|
118
|
+
directive_resolvers/throttle_resolver
|
|
119
|
+
directive_resolvers/async_resolver
|
|
120
|
+
directive_resolvers/global_id_resolver
|
|
121
|
+
directive_resolvers/argument_binding
|
|
122
|
+
directive_resolvers/argument_transforms_resolver
|
|
94
123
|
directives/arguments/base
|
|
95
124
|
directives/arguments/filters
|
|
125
|
+
directives/arguments/more_filters
|
|
96
126
|
directives/arguments/order_by
|
|
97
127
|
directives/arguments/soft_deletes
|
|
128
|
+
directives/definitions
|
|
98
129
|
directives/registry
|
|
99
130
|
dataloader_sources/association_loader
|
|
100
131
|
dataloader_sources/model_loader
|
|
101
132
|
dataloader_sources/relation_page_loader
|
|
102
133
|
dataloader_sources/relation_count_loader
|
|
134
|
+
dataloader_sources/relation_counts_loader
|
|
135
|
+
dataloader_sources/preload_loader
|
|
103
136
|
ide_helper
|
|
104
137
|
].each do |rel|
|
|
105
138
|
require_relative rel
|
|
@@ -109,6 +142,7 @@ end
|
|
|
109
142
|
|
|
110
143
|
# The activejob gem is optional: the job that queues @broadcast loads on first use.
|
|
111
144
|
Lighthouse::GraphQL.autoload(:BroadcastJob, File.expand_path('broadcast_job.rb', __dir__))
|
|
145
|
+
Lighthouse::GraphQL.autoload(:AsyncMutationJob, File.expand_path('async_mutation_job.rb', __dir__))
|
|
112
146
|
|
|
113
147
|
require_relative 'railtie' if defined?(Rails::Railtie)
|
|
114
148
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The counts @withCount loads, read off the record as Laravel's withCount
|
|
4
|
+
# attributes are: `user.posts_count`. The record is extended with this module
|
|
5
|
+
# (named, so it still marshals for @cache) and keeps them in an instance
|
|
6
|
+
# variable; a real column of the same name wins.
|
|
7
|
+
module Lighthouse::GraphQL::RelationCounts
|
|
8
|
+
def self.write(record, relation, count)
|
|
9
|
+
record.extend(self) unless record.is_a?(self)
|
|
10
|
+
counts = record.instance_variable_get(:@lighthouse_relation_counts) || {}
|
|
11
|
+
record.instance_variable_set(:@lighthouse_relation_counts, counts.merge("#{relation}_count" => count))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def respond_to_missing?(name, include_private = false)
|
|
15
|
+
lighthouse_relation_count?(name) || super
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def method_missing(name, *args, &block)
|
|
19
|
+
return super unless args.empty? && lighthouse_relation_count?(name)
|
|
20
|
+
|
|
21
|
+
@lighthouse_relation_counts[name.to_s]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def lighthouse_relation_count?(name)
|
|
27
|
+
@lighthouse_relation_counts&.key?(name.to_s)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# What PHP Lighthouse hands a resolver as its fourth argument, `$resolveInfo`:
|
|
4
|
+
# the field being resolved, where, with what, what the client selects below it
|
|
5
|
+
# (`lookahead`), and `enhance_builder`, which applies the field's own query
|
|
6
|
+
# directives to a query the resolver builds.
|
|
7
|
+
#
|
|
8
|
+
# type Query {
|
|
9
|
+
# posts(title: String @eq, orderBy: [PostOrderByClause!] @orderBy): [Post!]! @builder(class: "Published")
|
|
10
|
+
# }
|
|
11
|
+
#
|
|
12
|
+
# class Resolvers::Posts < Resolvers::BaseResolver
|
|
13
|
+
# def resolve(**)
|
|
14
|
+
# resolve_info.enhance_builder(Post.where(author: context[:current_user]), scopes: [:recent])
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# A Resolvers:: / Mutations:: class, a class @field names and a type class's
|
|
19
|
+
# methods read it as `resolve_info`; a plain callable gets it as a fourth
|
|
20
|
+
# argument when it takes one: `call(object, args, context, resolve_info)`.
|
|
21
|
+
class Lighthouse::GraphQL::ResolveInfo
|
|
22
|
+
LOOKAHEAD = :lighthouse_lookahead
|
|
23
|
+
|
|
24
|
+
attr_reader :field, :parent_type, :object, :arguments, :context
|
|
25
|
+
|
|
26
|
+
# graphql-ruby's GraphQL::Execution::Lookahead for the field: what the client
|
|
27
|
+
# selects below it, to load only that (`lookahead.selects?(:author)`,
|
|
28
|
+
# `lookahead.selection(:comments).arguments`). nil outside a query.
|
|
29
|
+
attr_reader :lookahead
|
|
30
|
+
|
|
31
|
+
# A field resolved by a class asks graphql-ruby for its lookahead.
|
|
32
|
+
def self.request_lookahead(field_def)
|
|
33
|
+
field_def.extras([:lookahead]) unless field_def.extras.include?(:lookahead)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def initialize(field:, parent_type:, object:, arguments:, context:)
|
|
37
|
+
@field = field
|
|
38
|
+
@parent_type = parent_type
|
|
39
|
+
@object = object
|
|
40
|
+
@arguments = arguments
|
|
41
|
+
@context = context
|
|
42
|
+
@lookahead = context.respond_to?(:[]) ? context[LOOKAHEAD] : nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def field_name = field.graphql_name
|
|
46
|
+
|
|
47
|
+
def return_type = field.type
|
|
48
|
+
|
|
49
|
+
# The path from the root to this field, as graphql-ruby tracks it: ["posts", 0, "author"].
|
|
50
|
+
def path
|
|
51
|
+
context.respond_to?(:[]) ? context[:current_path] : nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# `relation` with the field's model `scopes`, its @builder classes, and the
|
|
55
|
+
# argument directives the client used (@eq, @where, @in, @like, @orderBy,
|
|
56
|
+
# @whereConditions, @trashed, ...), as @all / @paginate apply them.
|
|
57
|
+
def enhance_builder(relation, scopes: [])
|
|
58
|
+
resolver = Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver.new(field, parent_type, nil)
|
|
59
|
+
args = arguments.respond_to?(:to_h) ? arguments.to_h : {}
|
|
60
|
+
relation = Array(scopes).reduce(relation) { |acc, scope| acc.public_send(scope) }
|
|
61
|
+
relation = Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier.apply(relation, field.ast_node, args, context, resolver)
|
|
62
|
+
resolver.apply_where_filters(relation, args, context)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# `resolve_info`, for the classes that resolve a field.
|
|
66
|
+
module Accessor
|
|
67
|
+
def resolve_info = @lighthouse_resolve_info
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Hand `info` to `resolver` (a GraphQL::Schema::Resolver or type class instance).
|
|
71
|
+
def self.for(field_def, parent_type, object, arguments, context)
|
|
72
|
+
new(field: field_def, parent_type: parent_type, object: object, arguments: arguments, context: context)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.attach(resolver, info)
|
|
76
|
+
resolver.extend(Accessor) unless resolver.respond_to?(:resolve_info)
|
|
77
|
+
resolver.instance_variable_set(:@lighthouse_resolve_info, info)
|
|
78
|
+
resolver
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
# it is first used, so none misses what the base shares;
|
|
19
19
|
# - for subscriptions, also resolves a Subscription field that has no class of
|
|
20
20
|
# its own, instead of Lighthouse::GraphQL::Subscription::Default.
|
|
21
|
+
#
|
|
22
|
+
# The code-first types graphql-ruby's install writes are bases too (RootTypes):
|
|
23
|
+
# `query_type:` / `mutation_type:` / `subscription_type:` (Types::QueryType, ...)
|
|
24
|
+
# are what the SDL's root types are built on, so their `field`s are the
|
|
25
|
+
# schema's too, and `node:` (Types::NodeType) is the SDL's Node interface.
|
|
21
26
|
module Lighthouse::GraphQL::ResolverBases
|
|
22
27
|
# base_types key => the graphql-ruby class the base must inherit from.
|
|
23
28
|
GRAPHQL_CLASSES = {
|
|
@@ -31,16 +36,32 @@ module Lighthouse::GraphQL::ResolverBases
|
|
|
31
36
|
|
|
32
37
|
module_function
|
|
33
38
|
|
|
34
|
-
#
|
|
35
|
-
#
|
|
39
|
+
# graphql-ruby's base types that the type classes of the same kind
|
|
40
|
+
# (Types::PostType, Types::DateTimeType, ...) inherit from too.
|
|
41
|
+
TYPE_KINDS = %i[object union interface scalar].freeze
|
|
42
|
+
|
|
43
|
+
# Take Lighthouse's keys out of opts[:base_types] (the caller's Hash is not
|
|
44
|
+
# changed) and return { root kind => base class } for those given, plus
|
|
45
|
+
# :types => { kind => base } for graphql-ruby's own, which stay in opts, and
|
|
46
|
+
# :roots => { query_type: ..., node: ... } for the code-first types (RootTypes).
|
|
36
47
|
def extract!(opts)
|
|
37
48
|
base_types = opts[:base_types]
|
|
38
49
|
return {} unless base_types.is_a?(Hash)
|
|
39
50
|
|
|
40
51
|
given = base_types.slice(*GRAPHQL_CLASSES.keys).compact
|
|
41
|
-
opts[:base_types] = base_types.except(*GRAPHQL_CLASSES.keys)
|
|
52
|
+
opts[:base_types] = base_types.except(*GRAPHQL_CLASSES.keys, *Lighthouse::GraphQL::RootTypes::KEYS)
|
|
42
53
|
given.each { |key, klass| validate!(key, klass) }
|
|
43
|
-
KEYS.filter_map { |kind, key| [kind, given[key]] if given[key] }.to_h
|
|
54
|
+
bases = KEYS.filter_map { |kind, key| [kind, given[key]] if given[key] }.to_h
|
|
55
|
+
type_bases = base_types.slice(*TYPE_KINDS).compact
|
|
56
|
+
bases = bases.merge(types: type_bases) unless type_bases.empty?
|
|
57
|
+
with_roots(bases, base_types)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# `bases` with the code-first root types given, validated, under :roots.
|
|
61
|
+
def with_roots(bases, base_types)
|
|
62
|
+
roots = base_types.slice(*Lighthouse::GraphQL::RootTypes::KEYS).compact
|
|
63
|
+
roots.each { |key, type| Lighthouse::GraphQL::RootTypes.validate!(key, type) }
|
|
64
|
+
roots.empty? ? bases : bases.merge(roots: roots)
|
|
44
65
|
end
|
|
45
66
|
|
|
46
67
|
def validate!(key, klass)
|
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
# namespaces follow graphql-ruby's contract — GraphQL::Schema::Resolver and
|
|
5
5
|
# GraphQL::Schema::Mutation: `resolve(**args)` with `object` and `context`,
|
|
6
6
|
# plus `ready?` / `authorized?` — exactly as in a graphql-ruby schema.
|
|
7
|
-
# @field also accepts a plain callable, `call(object, args, context)
|
|
7
|
+
# @field also accepts a plain callable, `call(object, args, context)`, which
|
|
8
|
+
# gets the ResolveInfo as a fourth argument when it takes one.
|
|
9
|
+
#
|
|
10
|
+
# `method:` is the method after `@` in `@field(resolver: "Class@method")`: it
|
|
11
|
+
# replaces `resolve` (keeping `ready?` / `authorized?`) on a resolver class, and
|
|
12
|
+
# `call` on a plain class, as a class method or else an instance method.
|
|
8
13
|
module Lighthouse::GraphQL::ResolverDispatch
|
|
9
14
|
module_function
|
|
10
15
|
|
|
@@ -12,15 +17,50 @@ module Lighthouse::GraphQL::ResolverDispatch
|
|
|
12
17
|
klass.is_a?(Class) && klass < ::GraphQL::Schema::Resolver
|
|
13
18
|
end
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
# `info`: the ResolveInfo of the field being resolved, which carries its
|
|
21
|
+
# object, arguments and context.
|
|
22
|
+
def call(klass, info, method: nil)
|
|
23
|
+
return call_resolver(klass, info, method) if graphql_resolver?(klass)
|
|
24
|
+
|
|
25
|
+
receiver = callable_receiver(klass, method || :call)
|
|
26
|
+
callable = receiver.method(method || (receiver.respond_to?(:call) ? :call : :resolve))
|
|
27
|
+
args = [info.object, info.arguments, info.context]
|
|
28
|
+
takes_info?(receiver, callable) ? callable.call(*args, info) : callable.call(*args)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def call_resolver(klass, info, method)
|
|
32
|
+
kwargs = Lighthouse::Support::Arguments.prepare(info.field, info.arguments, info.context)
|
|
33
|
+
instance = Lighthouse::GraphQL::ResolveInfo.attach(klass.new(object: info.object, context: info.context, field: info.field), info)
|
|
34
|
+
instance.define_singleton_method(:resolve) { |**values| public_send(method, **values) } if method
|
|
35
|
+
single_input?(instance, kwargs) ? instance.resolve_with_support(**kwargs) : resolve_as_mutation(instance, kwargs)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# graphql-ruby's RelayClassicMutation (the base mutation its install writes)
|
|
39
|
+
# takes one `input:` argument. A field that declares its arguments one by one
|
|
40
|
+
# calls it as a plain GraphQL::Schema::Mutation, with them as keywords: past
|
|
41
|
+
# HasSingleInputArgument's unwrapping of `input:`, and its authorization of
|
|
42
|
+
# the arguments of `input:`.
|
|
43
|
+
def single_input?(instance, kwargs)
|
|
44
|
+
!instance.is_a?(::GraphQL::Schema::HasSingleInputArgument) || kwargs.key?(:input)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def resolve_as_mutation(instance, kwargs)
|
|
48
|
+
instance.define_singleton_method(:authorize_arguments, ::GraphQL::Schema::Resolver.instance_method(:authorize_arguments))
|
|
49
|
+
instance.singleton_class.send(:private, :authorize_arguments)
|
|
50
|
+
::GraphQL::Schema::Mutation.instance_method(:resolve_with_support).bind_call(instance, **kwargs)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The class itself when it answers `name` as a class method, else an instance.
|
|
54
|
+
def callable_receiver(klass, name)
|
|
55
|
+
klass.respond_to?(name) ? klass : klass.new
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
POSITIONAL = %i[req opt].freeze
|
|
59
|
+
|
|
60
|
+
# Does the callable take a fourth argument? A lambda's own parameters count,
|
|
61
|
+
# not Proc#call's (*args).
|
|
62
|
+
def takes_info?(receiver, callable)
|
|
63
|
+
params = receiver.is_a?(Proc) && callable.name == :call ? receiver.parameters : callable.parameters
|
|
64
|
+
params.any? { |type, _| type == :rest } || params.count { |type, _| POSITIONAL.include?(type) } >= 4
|
|
25
65
|
end
|
|
26
66
|
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The code-first types graphql-ruby's install writes, which a schema builds on:
|
|
4
|
+
#
|
|
5
|
+
# base_types: { query_type: Types::QueryType, mutation_type: Types::MutationType,
|
|
6
|
+
# subscription_type: Types::SubscriptionType, node: Types::NodeType }
|
|
7
|
+
#
|
|
8
|
+
# - A root type given is what the SDL's root type of that kind is built on,
|
|
9
|
+
# instead of base_types[:object]: the SDL's `type Query` is a subclass of
|
|
10
|
+
# Types::QueryType, so the `field`s the class declares (graphql-ruby's
|
|
11
|
+
# `node` / `nodes`, what `graphql:mutation` adds to Types::MutationType)
|
|
12
|
+
# are the schema's too, beside the SDL's, and resolve as in graphql-ruby. An
|
|
13
|
+
# SDL field of the same name wins. With no such root type in the SDL, the
|
|
14
|
+
# class itself is the root, once it declares a field.
|
|
15
|
+
# - `node:` is the SDL's Node interface: `interface Node`, which @node adds and
|
|
16
|
+
# `implements Node` names, is that module, so graphql-ruby's Relay
|
|
17
|
+
# (Query.node / nodes, the schema's id_from_object / object_from_id) and the
|
|
18
|
+
# SDL's @node types share one Node. The `node` field @node adds to Query then
|
|
19
|
+
# gives way to the root type class's own.
|
|
20
|
+
#
|
|
21
|
+
# graphql-ruby's builder knows one base per kind, so the hook into it is a
|
|
22
|
+
# patch (graphql_ruby_patches.rb), which reads the bases of the build in
|
|
23
|
+
# progress from here.
|
|
24
|
+
module Lighthouse::GraphQL::RootTypes
|
|
25
|
+
KEYS = %i[query_type mutation_type subscription_type node].freeze
|
|
26
|
+
|
|
27
|
+
ROOTS = { query_type: :query, mutation_type: :mutation, subscription_type: :subscription }.freeze
|
|
28
|
+
|
|
29
|
+
# The resolver of the `node` field @node adds to Query.
|
|
30
|
+
NODE_RESOLVER = 'Lighthouse::GraphQL::NodeRegistry@resolve'
|
|
31
|
+
|
|
32
|
+
module_function
|
|
33
|
+
|
|
34
|
+
def validate!(key, type)
|
|
35
|
+
valid = key == :node ? Lighthouse::GraphQL::Type.kind_of(type) == :interface : type.is_a?(Class) && type < ::GraphQL::Schema::Object
|
|
36
|
+
return if valid
|
|
37
|
+
|
|
38
|
+
expected = key == :node ? 'a module including GraphQL::Schema::Interface' : 'a subclass of GraphQL::Schema::Object'
|
|
39
|
+
raise Lighthouse::DefinitionError, "base_types[:#{key}] must be #{expected}, not #{type.inspect}."
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The root type class the SDL's root type of `root_kind` (:queries,
|
|
43
|
+
# :mutations) is built on, when it is Lighthouse's (Lighthouse::GraphQL::Type):
|
|
44
|
+
# its methods resolve the root fields of their name.
|
|
45
|
+
def type_class(root_kind, roots)
|
|
46
|
+
klass = roots&.[]({ queries: :query_type, mutations: :mutation_type }[root_kind])
|
|
47
|
+
klass if klass&.include?(Lighthouse::GraphQL::Type)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Run the block (the build of `document`) with `roots` as the bases of the
|
|
51
|
+
# types they name.
|
|
52
|
+
def with(document, roots)
|
|
53
|
+
previous = Thread.current[:lighthouse_root_types]
|
|
54
|
+
Thread.current[:lighthouse_root_types] = by_type_name(document, roots || {})
|
|
55
|
+
yield
|
|
56
|
+
ensure
|
|
57
|
+
Thread.current[:lighthouse_root_types] = previous
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The class an SDL type of `name` is built on, or nil.
|
|
61
|
+
def base_for(name)
|
|
62
|
+
Thread.current[:lighthouse_root_types]&.[](name)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# `definition` without the fields `base` declares itself that @node added.
|
|
66
|
+
def definition_on(definition, base)
|
|
67
|
+
names = field_names(base)
|
|
68
|
+
fields = definition.fields.reject { |field| names.include?(field.name) && added_by_node?(field) }
|
|
69
|
+
fields.size == definition.fields.size ? definition : definition.merge(fields: fields)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The names of the fields a class and its ancestors declare. Not `fields`,
|
|
73
|
+
# which warns about a type with none (graphql-ruby's install leaves
|
|
74
|
+
# Types::MutationType empty once its example field goes).
|
|
75
|
+
def field_names(type)
|
|
76
|
+
type.ancestors.select { |ancestor| ancestor.respond_to?(:own_fields) }.flat_map { |ancestor| ancestor.own_fields.keys }.uniq
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The schema the block builds from `document`, with `roots` as the bases of
|
|
80
|
+
# the types they name; then the root type classes the SDL has no type for,
|
|
81
|
+
# and the subscriptions backend an app schema class `use`s (graphql_ruby_patches.rb).
|
|
82
|
+
def build(document, roots, &block)
|
|
83
|
+
schema = with(document, roots, &block)
|
|
84
|
+
apply_missing(schema, roots)
|
|
85
|
+
schema.extend(Lighthouse::GraphQL::InheritedSubscriptionRoot) if schema.respond_to?(:add_subscription_extension_if_necessary, true)
|
|
86
|
+
schema
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# A root type class the SDL has no type for is the root itself, once it
|
|
90
|
+
# declares a field (graphql-ruby's install writes them with an example one).
|
|
91
|
+
def apply_missing(schema, roots)
|
|
92
|
+
ROOTS.each do |key, root|
|
|
93
|
+
type = roots&.[](key)
|
|
94
|
+
next if type.nil? || schema.public_send(root) || field_names(type).empty?
|
|
95
|
+
|
|
96
|
+
schema.public_send(root, type)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def by_type_name(document, roots)
|
|
101
|
+
names = root_names(document)
|
|
102
|
+
mapped = ROOTS.each_with_object({}) { |(key, root), out| out[names[root]] = roots[key] if roots[key] }
|
|
103
|
+
roots[:node] ? mapped.merge('Node' => roots[:node]) : mapped
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# { query: 'Query', ... }, as the document's `schema { }` names them.
|
|
107
|
+
def root_names(document)
|
|
108
|
+
schema = document.definitions.find { |definition| definition.is_a?(::GraphQL::Language::Nodes::SchemaDefinition) }
|
|
109
|
+
{ query: schema&.query || 'Query', mutation: schema&.mutation || 'Mutation', subscription: schema&.subscription || 'Subscription' }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def added_by_node?(field)
|
|
113
|
+
field.directives.any? do |directive|
|
|
114
|
+
directive.name == 'field' && Lighthouse::Support::DirectiveArgs.literal(directive, 'resolver') == NODE_RESOLVER
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# How the values of an SDL scalar (`scalar DateTime`) are read from a query and
|
|
4
|
+
# written to a response, as a graphql-ruby scalar class does it with
|
|
5
|
+
# `coerce_input(value, context)` and `coerce_result(value, context)`. The class
|
|
6
|
+
# is, in order:
|
|
7
|
+
#
|
|
8
|
+
# 1. the one `@scalar(class: "Name")` on the scalar names, looked up in
|
|
9
|
+
# Config.namespaces[:types] (nested names too) and then as written. PHP
|
|
10
|
+
# Lighthouse's directive.
|
|
11
|
+
# 2. Its type class, Types::<Name>Type including Lighthouse::GraphQL::Type: a
|
|
12
|
+
# GraphQL::Schema::Scalar, as graphql-ruby's `rails g graphql:scalar` writes it.
|
|
13
|
+
# 3. Lighthouse's own, for `scalar Upload` (Lighthouse::GraphQL::Upload).
|
|
14
|
+
#
|
|
15
|
+
# A scalar with none passes its values through unchanged. A class raising
|
|
16
|
+
# GraphQL::CoercionError from coerce_input (or returning nil for a value) makes
|
|
17
|
+
# the argument invalid, with that message, as in graphql-ruby.
|
|
18
|
+
module Lighthouse::GraphQL::ScalarCoercion
|
|
19
|
+
DEFINITION = <<~GRAPHQL.chomp
|
|
20
|
+
"""
|
|
21
|
+
The class that reads and writes this scalar's values: coerce_input(value, context) and
|
|
22
|
+
coerce_result(value, context), as a GraphQL::Schema::Scalar.
|
|
23
|
+
"""
|
|
24
|
+
directive @scalar(class: String!) on SCALAR
|
|
25
|
+
GRAPHQL
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# Give every SDL scalar that has a class its coercion. The class is looked up
|
|
30
|
+
# again on each value, so code reloading keeps working; `bases` is the
|
|
31
|
+
# schema's base_types[:scalar], which a type class must inherit from.
|
|
32
|
+
def apply(schema, base = nil)
|
|
33
|
+
schema.types.each_value do |type|
|
|
34
|
+
next unless type.kind.scalar? && type.ast_node
|
|
35
|
+
|
|
36
|
+
klass = class_for(type, base)
|
|
37
|
+
install(type, klass) if klass
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Scalars Lighthouse brings a class for, when nothing else names one.
|
|
42
|
+
BUILT_IN = { 'Upload' => 'Lighthouse::GraphQL::Upload' }.freeze
|
|
43
|
+
|
|
44
|
+
def class_for(scalar_type, base = nil)
|
|
45
|
+
directive_class(scalar_type) || type_class(scalar_type, base) || BUILT_IN[scalar_type.graphql_name]&.constantize
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def directive_class(scalar_type)
|
|
49
|
+
directive = (scalar_type.ast_node.directives || []).find { |d| d.name == 'scalar' }
|
|
50
|
+
name = directive && Lighthouse::Support::DirectiveArgs.literal(directive, 'class')
|
|
51
|
+
return unless name
|
|
52
|
+
|
|
53
|
+
klass = Lighthouse::Support::ModelResolver.constantize(name, namespaces: Lighthouse::Config.namespaces[:types], nested: true)
|
|
54
|
+
return klass if klass.respond_to?(:coerce_input) && klass.respond_to?(:coerce_result)
|
|
55
|
+
|
|
56
|
+
raise Lighthouse::DefinitionError,
|
|
57
|
+
"@scalar(class: #{name.inspect}) on #{scalar_type.graphql_name} names no class with coerce_input(value, context) " \
|
|
58
|
+
'and coerce_result(value, context).'
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def type_class(scalar_type, base)
|
|
62
|
+
klass = Lighthouse::GraphQL::Type.class_for(scalar_type.graphql_name, :scalar)
|
|
63
|
+
Lighthouse::GraphQL::ResolverBases.check!(klass, base, "the #{scalar_type.graphql_name} scalar") if klass
|
|
64
|
+
klass
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def install(scalar_type, klass)
|
|
68
|
+
current = -> { klass.name ? klass.name.constantize : klass }
|
|
69
|
+
scalar_type.define_singleton_method(:coerce_input) { |value, ctx| current.call.coerce_input(value, ctx) }
|
|
70
|
+
scalar_type.define_singleton_method(:coerce_result) { |value, ctx| current.call.coerce_result(value, ctx) }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -72,7 +72,8 @@ module Lighthouse::GraphQL::SchemaCache
|
|
|
72
72
|
registry.directive_definitions.sort,
|
|
73
73
|
registry.type_definitions.sort,
|
|
74
74
|
manipulators,
|
|
75
|
-
Lighthouse::Config.pagination_default_count
|
|
75
|
+
Lighthouse::Config.pagination_default_count,
|
|
76
|
+
Lighthouse::Support::Environment.current # @hide / @show
|
|
76
77
|
].inspect
|
|
77
78
|
end
|
|
78
79
|
|
|
@@ -15,8 +15,8 @@ module Lighthouse::GraphQL::SchemaGenerator
|
|
|
15
15
|
obj.respond_to?(field_name) ? obj.public_send(field_name) : nil
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def resolve_type(
|
|
19
|
-
|
|
18
|
+
def resolve_type(abstract_type, obj, ctx)
|
|
19
|
+
Lighthouse::GraphQL::TypeResolution.resolve(ctx.respond_to?(:schema) && ctx.schema ? ctx.schema : schema, abstract_type, obj, ctx)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def coerce_input(_type, value, _ctx)
|
|
@@ -47,7 +47,7 @@ module Lighthouse::GraphQL::SchemaGenerator
|
|
|
47
47
|
impl = default_implementation
|
|
48
48
|
# base_types' resolver / mutation / subscription are Lighthouse's, not graphql-ruby's.
|
|
49
49
|
bases = Lighthouse::GraphQL::ResolverBases.extract!(opts)
|
|
50
|
-
schema = from_document(document, impl, opts)
|
|
50
|
+
schema = Lighthouse::GraphQL::RootTypes.build(document, bases[:roots]) { from_document(document, impl, opts) }
|
|
51
51
|
|
|
52
52
|
delegate_to_federation_sdl(schema)
|
|
53
53
|
Lighthouse::GraphQL::GeneratedResolvers.install_dispatcher(schema)
|
|
@@ -139,7 +139,10 @@ module Lighthouse::GraphQL::SchemaGenerator
|
|
|
139
139
|
# Apply object-level federation directives to generated types so the
|
|
140
140
|
# Apollo Federation helper can detect entities and add _entities.
|
|
141
141
|
Lighthouse::GraphQL::FederationDirectives.apply(type)
|
|
142
|
-
|
|
142
|
+
root_kind = root_kinds[type.graphql_name]
|
|
143
|
+
type_class = Lighthouse::GraphQL::DirectiveResolvers::TypeMethodResolver.type_class(type.graphql_name, bases, root_kind)
|
|
144
|
+
# A field a code-first base declares (RootTypes) resolves as graphql-ruby resolves it.
|
|
145
|
+
type.fields.each_value { |field_def| process_field(type, field_def, root_kind, bases, type_class) if field_def.owner == type }
|
|
143
146
|
end
|
|
144
147
|
|
|
145
148
|
# @complexity, and paginated fields, give graphql-ruby's complexity analysis their cost.
|
|
@@ -151,6 +154,9 @@ module Lighthouse::GraphQL::SchemaGenerator
|
|
|
151
154
|
# Every @broadcast must name a field of the Subscription type.
|
|
152
155
|
Lighthouse::GraphQL::Broadcasting.validate!(schema)
|
|
153
156
|
|
|
157
|
+
# Unions, interfaces and scalars get the classes that resolve and coerce them.
|
|
158
|
+
Lighthouse::GraphQL::Type.apply(schema, bases.fetch(:types, {}))
|
|
159
|
+
|
|
154
160
|
# return schema for convenience
|
|
155
161
|
schema
|
|
156
162
|
end
|
|
@@ -163,10 +169,11 @@ module Lighthouse::GraphQL::SchemaGenerator
|
|
|
163
169
|
|
|
164
170
|
# A field's generated resolvers, then the runtime hooks that make
|
|
165
171
|
# graphql-ruby reach them.
|
|
166
|
-
def process_field(type, field_def, root_kind, bases = {})
|
|
172
|
+
def process_field(type, field_def, root_kind, bases = {}, type_class = nil)
|
|
167
173
|
return unless field_def.ast_node
|
|
168
174
|
|
|
169
|
-
|
|
175
|
+
# Installed in resolution order (see FieldResolverStack).
|
|
176
|
+
Lighthouse::GraphQL::FieldResolverStack.apply(type, field_def, root_kind, bases: bases, type_class: type_class)
|
|
170
177
|
Lighthouse::GraphQL::GeneratedResolvers.route_runtime_resolver(type, field_def)
|
|
171
178
|
ensure_connection_extension(field_def)
|
|
172
179
|
end
|
|
@@ -187,11 +194,6 @@ module Lighthouse::GraphQL::SchemaGenerator
|
|
|
187
194
|
# ignore failures during post-processing
|
|
188
195
|
end
|
|
189
196
|
|
|
190
|
-
# A field's resolvers, installed in resolution order (see FieldResolverStack).
|
|
191
|
-
def apply_field_resolvers(type, field_def, directives, root_kind, bases = {})
|
|
192
|
-
Lighthouse::GraphQL::FieldResolverStack.apply(type, field_def, directives, root_kind, bases: bases)
|
|
193
|
-
end
|
|
194
|
-
|
|
195
197
|
# Scan SDL string for list-typed fields that carry relationship directives
|
|
196
198
|
# and produce Connection/Edge types where missing. This is a lightweight
|
|
197
199
|
# transformation that mirrors Lighthouse behavior: clients can declare
|
|
@@ -27,14 +27,10 @@ class SchemaImplementation
|
|
|
27
27
|
read_attribute(obj, field_name)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
Object.const_get("Types::#{type_name}Type")
|
|
35
|
-
rescue StandardError
|
|
36
|
-
nil
|
|
37
|
-
end
|
|
30
|
+
# The object type of a union's or interface's value (see TypeResolution).
|
|
31
|
+
def resolve_type(abstract_type, obj, ctx)
|
|
32
|
+
schema = ctx.respond_to?(:schema) && ctx.schema ? ctx.schema : @schema
|
|
33
|
+
Lighthouse::GraphQL::TypeResolution.resolve(schema, abstract_type, obj, ctx)
|
|
38
34
|
end
|
|
39
35
|
|
|
40
36
|
def coerce_input(_type, value, _ctx)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The application's environment, for @hide and @show: Rails.env, else
|
|
4
|
+
# RAILS_ENV or RACK_ENV, else "development".
|
|
5
|
+
module Lighthouse::Support::Environment
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def current
|
|
9
|
+
return Rails.env.to_s if defined?(Rails) && Rails.respond_to?(:env)
|
|
10
|
+
|
|
11
|
+
ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -26,14 +26,16 @@ module Lighthouse::Support::ModelResolver
|
|
|
26
26
|
# `namespaces:` overrides the configured resolver_namespaces, e.g. with
|
|
27
27
|
# Config.namespaces[:builders] to look a builder class up.
|
|
28
28
|
# `fallback: false` skips the bare name, so `Resolvers::User` is never
|
|
29
|
-
# mistaken for the `User` model.
|
|
30
|
-
|
|
29
|
+
# mistaken for the `User` model. `nested: true` tries the namespaces before a
|
|
30
|
+
# name that already contains `::` too, so `Feeds::Home` is found as
|
|
31
|
+
# `Resolvers::Feeds::Home`; otherwise such a name is used as-is.
|
|
32
|
+
def constantize(name, namespaces: self.namespaces, fallback: true, nested: false)
|
|
31
33
|
return nil unless name
|
|
32
34
|
|
|
33
35
|
raw = name.to_s.strip
|
|
34
36
|
return nil if raw.empty?
|
|
35
37
|
|
|
36
|
-
candidates_for(raw, namespaces, fallback: fallback).each do |candidate|
|
|
38
|
+
candidates_for(raw, namespaces, fallback: fallback, nested: nested).each do |candidate|
|
|
37
39
|
klass = try_constantize(candidate)
|
|
38
40
|
return klass if klass
|
|
39
41
|
end
|
|
@@ -41,8 +43,8 @@ module Lighthouse::Support::ModelResolver
|
|
|
41
43
|
nil
|
|
42
44
|
end
|
|
43
45
|
|
|
44
|
-
def candidates_for(raw, namespaces = self.namespaces, fallback: true)
|
|
45
|
-
if raw.include?('::')
|
|
46
|
+
def candidates_for(raw, namespaces = self.namespaces, fallback: true, nested: false)
|
|
47
|
+
if raw.include?('::') && !nested
|
|
46
48
|
[raw]
|
|
47
49
|
else
|
|
48
50
|
Array(namespaces).map { |ns| "#{ns}::#{raw}" } + (fallback ? [raw] : [])
|