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
|
@@ -61,13 +61,17 @@ class Lighthouse::DirectiveRegistry
|
|
|
61
61
|
# authorization, argument validation, then @cache (so a cache hit is still
|
|
62
62
|
# authorized and validated), @clearCache (which acts once the resolver
|
|
63
63
|
# succeeded), @event and @broadcast (likewise), and finally the data resolver (0).
|
|
64
|
+
# @throttle counts every call, before anything else runs; @async queues what is
|
|
65
|
+
# below @cache, once the request has authorized and validated the call.
|
|
64
66
|
RESOLUTION_ORDER = {
|
|
67
|
+
'throttle' => 7,
|
|
65
68
|
'convertEmptyStringsToNull' => 6,
|
|
66
69
|
'guard' => 5, 'auth' => 5,
|
|
67
70
|
'canFind' => 4, 'canModel' => 4, 'canQuery' => 4, 'canResolved' => 4, 'canRoot' => 4,
|
|
68
71
|
'validator' => 3,
|
|
69
|
-
'cache' => 2,
|
|
70
|
-
'clearCache' => 1, 'event' => 1, 'broadcast' => 1
|
|
72
|
+
'cache' => 2, 'async' => 2,
|
|
73
|
+
'clearCache' => 1, 'event' => 1, 'broadcast' => 1,
|
|
74
|
+
'with' => 1, 'withCount' => 1, 'lazyLoad' => 1, 'globalId' => 1
|
|
71
75
|
}.freeze
|
|
72
76
|
|
|
73
77
|
def resolution_order(name)
|
|
@@ -164,6 +168,8 @@ class Lighthouse::DirectiveRegistry
|
|
|
164
168
|
'rename' => :RenameDirectiveResolver,
|
|
165
169
|
'method' => :MethodDirectiveResolver,
|
|
166
170
|
'auth' => :AuthDirectiveResolver,
|
|
171
|
+
'hasManyThrough' => :HasManyDirectiveResolver,
|
|
172
|
+
'hasOneThrough' => :HasOneDirectiveResolver,
|
|
167
173
|
'guard' => :GuardDirectiveResolver
|
|
168
174
|
}.freeze
|
|
169
175
|
|
|
@@ -181,17 +187,15 @@ class Lighthouse::DirectiveRegistry
|
|
|
181
187
|
CacheDirectiveResolver ClearCacheDirectiveResolver EventDirectiveResolver
|
|
182
188
|
ValidatorDirectiveResolver ConvertEmptyStringsToNullDirectiveResolver
|
|
183
189
|
SubscriptionDirectiveResolver BroadcastDirectiveResolver
|
|
190
|
+
WithDirectiveResolver WithCountDirectiveResolver LazyLoadDirectiveResolver
|
|
191
|
+
GlobalIdDirectiveResolver NamespacedDirectiveResolver ThrottleDirectiveResolver AsyncDirectiveResolver
|
|
184
192
|
].freeze
|
|
185
193
|
|
|
186
194
|
# Argument-builder filter/sort directives.
|
|
187
|
-
ARG_BUILDERS = %i[
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
'spread' => 'directive @spread on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION',
|
|
192
|
-
'inject' => 'directive @inject(context: String!, name: String!) repeatable on FIELD_DEFINITION',
|
|
193
|
-
'cacheKey' => 'directive @cacheKey on FIELD_DEFINITION'
|
|
194
|
-
}.freeze
|
|
195
|
+
ARG_BUILDERS = %i[
|
|
196
|
+
Eq Neq In NotIn Like Where WhereKey OrderBy Trashed WithoutGlobalScopes
|
|
197
|
+
WhereNull WhereNotNull WhereBetween WhereNotBetween WhereJsonContains Scope Limit
|
|
198
|
+
].freeze
|
|
195
199
|
|
|
196
200
|
def register_defaults
|
|
197
201
|
resolvers = Lighthouse::GraphQL::DirectiveResolvers
|
|
@@ -200,9 +204,14 @@ class Lighthouse::DirectiveRegistry
|
|
|
200
204
|
ARG_BUILDERS.each { |constant| register(Lighthouse::GraphQL::Directives::Arguments.const_get(constant, false)) }
|
|
201
205
|
|
|
202
206
|
# SDL manipulators run in registration order during the manipulation phase.
|
|
207
|
+
# @hide / @show first, so a removed field generates nothing; then @model,
|
|
208
|
+
# which names the model of fields @paginate then rewrites.
|
|
209
|
+
register_manipulator(Lighthouse::GraphQL::Directives::VisibilityDirective)
|
|
210
|
+
register_manipulator(Lighthouse::GraphQL::Directives::ModelDirective)
|
|
203
211
|
register_manipulator(Lighthouse::GraphQL::Directives::PaginationManipulator)
|
|
204
212
|
register_manipulator(Lighthouse::GraphQL::Directives::WhereConditionsDirective)
|
|
205
213
|
register_manipulator(Lighthouse::GraphQL::Directives::SoftDeletesDirective)
|
|
214
|
+
register_manipulator(Lighthouse::GraphQL::Directives::NodeDirective)
|
|
206
215
|
|
|
207
216
|
register_default_definitions
|
|
208
217
|
self
|
|
@@ -213,17 +222,8 @@ class Lighthouse::DirectiveRegistry
|
|
|
213
222
|
# SDL of the directives (and the types they refer to) that no registered
|
|
214
223
|
# class contributes.
|
|
215
224
|
def register_default_definitions
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
register_definition('cacheControl', Lighthouse::GraphQL::CacheControl::DEFINITION)
|
|
219
|
-
Lighthouse::GraphQL::CacheControl::TYPE_DEFINITIONS.each { |name, sdl| register_type_definition(name, sdl) }
|
|
220
|
-
register_definition('feature', Lighthouse::GraphQL::FeatureDirective::DEFINITION)
|
|
221
|
-
register_definition('complexity', Lighthouse::GraphQL::Complexity::DEFINITION)
|
|
222
|
-
Lighthouse::GraphQL::FeatureDirective::TYPE_DEFINITIONS.each { |name, sdl| register_type_definition(name, sdl) }
|
|
223
|
-
Lighthouse::GraphQL::Directives::WhereConditionsDirective::DEFINITIONS.each do |name, definition|
|
|
224
|
-
register_definition(name, definition)
|
|
225
|
-
end
|
|
226
|
-
Lighthouse::GraphQL::Directives::SoftDeletesDirective::DEFINITIONS.each { |name, definition| register_definition(name, definition) }
|
|
225
|
+
Lighthouse::GraphQL::Directives::Definitions.directives.each { |name, definition| register_definition(name, definition) }
|
|
226
|
+
Lighthouse::GraphQL::Directives::Definitions.types.each { |name, sdl| register_type_definition(name, sdl) }
|
|
227
227
|
end
|
|
228
228
|
|
|
229
229
|
def safe_definition(klass)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# @hide(env:) and @show(env:) — leave a field out of the schema in some
|
|
4
|
+
# environments, as PHP Lighthouse's directives: @hide removes it in the ones
|
|
5
|
+
# listed, @show keeps it only in those. The environment is Rails.env (see
|
|
6
|
+
# Support::Environment); the SDL is rewritten before the schema is built.
|
|
7
|
+
#
|
|
8
|
+
# type Query {
|
|
9
|
+
# debugInfo: String @show(env: ["development", "test"])
|
|
10
|
+
# legacyFeed: [Post!]! @hide(env: ["production"]) @all
|
|
11
|
+
# }
|
|
12
|
+
class Lighthouse::GraphQL::Directives::VisibilityDirective
|
|
13
|
+
DEFINITIONS = {
|
|
14
|
+
'hide' => <<~GRAPHQL.chomp,
|
|
15
|
+
"""
|
|
16
|
+
Excludes the annotated element from the schema conditionally.
|
|
17
|
+
"""
|
|
18
|
+
directive @hide(
|
|
19
|
+
"Specify which environments must not use this field, e.g. [\\"production\\"]."
|
|
20
|
+
env: [String!]!
|
|
21
|
+
) on FIELD_DEFINITION
|
|
22
|
+
GRAPHQL
|
|
23
|
+
'show' => <<~GRAPHQL.chomp
|
|
24
|
+
"""
|
|
25
|
+
Includes the annotated element in the schema conditionally.
|
|
26
|
+
"""
|
|
27
|
+
directive @show(
|
|
28
|
+
"Specify which environments may use this field, e.g. [\\"testing\\"]."
|
|
29
|
+
env: [String!]!
|
|
30
|
+
) on FIELD_DEFINITION
|
|
31
|
+
GRAPHQL
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
FIELDS = [
|
|
35
|
+
::GraphQL::Language::Nodes::ObjectTypeDefinition, ::GraphQL::Language::Nodes::ObjectTypeExtension,
|
|
36
|
+
::GraphQL::Language::Nodes::InterfaceTypeDefinition, ::GraphQL::Language::Nodes::InterfaceTypeExtension
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
DECLARATION = /directive\s+@(hide|show)\b/
|
|
40
|
+
USAGE = /@(hide|show)\b/
|
|
41
|
+
|
|
42
|
+
def self.manipulate(sdl)
|
|
43
|
+
return sdl unless sdl.is_a?(String) && sdl.gsub(DECLARATION, '').match?(USAGE)
|
|
44
|
+
|
|
45
|
+
document = ::GraphQL::Language::Parser.parse(sdl)
|
|
46
|
+
env = Lighthouse::Support::Environment.current
|
|
47
|
+
::GraphQL::Language::Printer.new.print(document.merge(definitions: document.definitions.map { |definition| visible_fields(definition, env) }))
|
|
48
|
+
rescue ::GraphQL::ParseError
|
|
49
|
+
sdl
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.visible_fields(definition, env)
|
|
53
|
+
return definition unless FIELDS.any? { |kind| definition.is_a?(kind) }
|
|
54
|
+
|
|
55
|
+
definition.merge(fields: definition.fields.select { |field| visible?(field, env) })
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.visible?(field, env)
|
|
59
|
+
field.directives.all? do |directive|
|
|
60
|
+
case directive.name
|
|
61
|
+
when 'hide' then environments(directive).exclude?(env)
|
|
62
|
+
when 'show' then environments(directive).include?(env)
|
|
63
|
+
else true
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.environments(directive)
|
|
69
|
+
Array(directive.arguments.find { |argument| argument.name == 'env' }&.value).map(&:to_s)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# @enum(value:) — the internal value of an enum value, as PHP Lighthouse's
|
|
4
|
+
# directive: what a resolver gets for it in an argument, and what it answers
|
|
5
|
+
# with in a result.
|
|
6
|
+
#
|
|
7
|
+
# enum Role {
|
|
8
|
+
# ADMIN @enum(value: "admin")
|
|
9
|
+
# MEMBER @enum(value: "member")
|
|
10
|
+
# }
|
|
11
|
+
#
|
|
12
|
+
# `users(role: Role @eq)` then filters on "admin", and a User whose role is
|
|
13
|
+
# "admin" answers ADMIN. Without the directive, a value is its own name.
|
|
14
|
+
module Lighthouse::GraphQL::EnumDirective
|
|
15
|
+
DEFINITION = <<~GRAPHQL.chomp
|
|
16
|
+
"""
|
|
17
|
+
Assign an internal value to an enum key.
|
|
18
|
+
When dealing with the Enum type in your code,
|
|
19
|
+
you will receive the defined value instead of the string key.
|
|
20
|
+
"""
|
|
21
|
+
directive @enum(
|
|
22
|
+
"The internal value of the enum key."
|
|
23
|
+
value: Mixed
|
|
24
|
+
) on ENUM_VALUE
|
|
25
|
+
GRAPHQL
|
|
26
|
+
|
|
27
|
+
TYPE_DEFINITIONS = { 'Mixed' => 'scalar Mixed' }.freeze
|
|
28
|
+
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
def apply(schema)
|
|
32
|
+
schema.types.each_value { |type| apply_to(type) if type.kind.enum? }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def apply_to(enum_type)
|
|
36
|
+
enum_type.values.each_value do |enum_value|
|
|
37
|
+
directive = (enum_value.ast_node&.directives || []).find { |d| d.name == 'enum' }
|
|
38
|
+
enum_value.value(Lighthouse::Support::DirectiveArgs.literal(directive, 'value')) if directive
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -4,21 +4,26 @@
|
|
|
4
4
|
# DirectiveRegistry#resolution_order), so each wrapper decorates the ones
|
|
5
5
|
# before it. Besides its directives, a field gets the conventional
|
|
6
6
|
# Resolvers::/Mutations:: resolver when it is a root field with no resolving
|
|
7
|
-
# directive,
|
|
8
|
-
#
|
|
7
|
+
# directive, the method of the type's class (Types::PostType) when another
|
|
8
|
+
# type's field has none, and argument validation when an input type it takes
|
|
9
|
+
# carries @validator. The registry replaces a hardcoded `case dnode.name`: adding a
|
|
9
10
|
# directive is "register a class", not "edit the generator".
|
|
10
11
|
module Lighthouse::GraphQL::FieldResolverStack
|
|
11
12
|
module_function
|
|
12
13
|
|
|
13
14
|
# `bases`: { root kind => base class } from the schema's base_types (ResolverBases).
|
|
14
|
-
|
|
15
|
+
# `type_class`: the class of a non-root type (Types::PostType), whose methods
|
|
16
|
+
# resolve its fields that have no resolving directive.
|
|
17
|
+
def apply(type, field_def, root_kind, bases: {}, type_class: nil)
|
|
18
|
+
directives = field_def.ast_node.directives || []
|
|
15
19
|
opts = { root_kind: root_kind, bases: bases }
|
|
16
20
|
steps = directive_steps(type, field_def, directives, opts)
|
|
17
|
-
steps <<
|
|
21
|
+
steps << limit_step(type, field_def, opts) if Lighthouse::GraphQL::DirectiveResolvers::LimitResolver.keyword(field_def)
|
|
22
|
+
steps << own_step(type, field_def, root_kind, bases, type_class) unless resolving_directive?(directives)
|
|
18
23
|
steps << validator_step(type, field_def, opts) if input_validation?(field_def, directives)
|
|
19
|
-
steps
|
|
24
|
+
steps.concat(argument_steps(type, field_def, directives, opts))
|
|
20
25
|
|
|
21
|
-
steps.each_with_index.sort_by { |(order, _), index| [order, index] }.each { |(_, step), _| step.call }
|
|
26
|
+
steps.compact.each_with_index.sort_by { |(order, _), index| [order, index] }.each { |(_, step), _| step.call }
|
|
22
27
|
end
|
|
23
28
|
|
|
24
29
|
# [order, step] for every directive the registry has a resolver for.
|
|
@@ -50,6 +55,25 @@ module Lighthouse::GraphQL::FieldResolverStack
|
|
|
50
55
|
[0, -> { resolver.define_resolver(type) }]
|
|
51
56
|
end
|
|
52
57
|
|
|
58
|
+
# What resolves a field with no resolving directive: the method of the
|
|
59
|
+
# type's class (Types::QueryType#latest_posts too, as in graphql-ruby), else
|
|
60
|
+
# for a Query or Mutation field the conventional class. nil: the default reader.
|
|
61
|
+
def own_step(type, field_def, root_kind, bases, type_class)
|
|
62
|
+
return convention_step(type, field_def, root_kind, bases) if root_kind == :subscriptions
|
|
63
|
+
|
|
64
|
+
step = type_method_step(type, field_def, type_class, root: !root_kind.nil?) if type_class
|
|
65
|
+
step || (convention_step(type, field_def, root_kind, bases) if root_kind)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Nothing to install when the class does not define the field's method: the
|
|
69
|
+
# field is read off the object.
|
|
70
|
+
# `root`: the class is the root type's, which the type is built on, not one found by name.
|
|
71
|
+
def type_method_step(type, field_def, type_class, root: false)
|
|
72
|
+
resolver = Lighthouse::GraphQL::DirectiveResolvers::TypeMethodResolver
|
|
73
|
+
method_name = resolver.method_for(type_class, field_def)
|
|
74
|
+
[0, -> { resolver.new(field_def, type, method_name, root ? type_class : nil).define_resolver(type) }] if method_name
|
|
75
|
+
end
|
|
76
|
+
|
|
53
77
|
# An explicit @validator on the field already validates its arguments.
|
|
54
78
|
def input_validation?(field_def, directives)
|
|
55
79
|
directives.none? { |d| d.name == 'validator' } &&
|
|
@@ -69,6 +93,32 @@ module Lighthouse::GraphQL::FieldResolverStack
|
|
|
69
93
|
Lighthouse::GraphQL::DirectiveResolvers::ConvertEmptyStringsToNullDirectiveResolver.implicit?(field_def)
|
|
70
94
|
end
|
|
71
95
|
|
|
96
|
+
# The steps that change the arguments: empty strings to null and the rewriting
|
|
97
|
+
# directives (@globalId, @trim, @drop) outermost, so every wrapper sees them;
|
|
98
|
+
# the effectful ones (@hash, @bind, @upload) once @guard, @can* and @validator
|
|
99
|
+
# have let the call through (ArgumentTransformsResolver).
|
|
100
|
+
ARGUMENT_PHASES = { rewrite: 'convertEmptyStringsToNull', effects: 'cache' }.freeze
|
|
101
|
+
|
|
102
|
+
def argument_steps(type, field_def, directives, opts)
|
|
103
|
+
steps = []
|
|
104
|
+
steps << empty_strings_step(type, field_def, opts) if empty_string_conversion?(field_def, directives)
|
|
105
|
+
ARGUMENT_PHASES.each do |phase, alongside|
|
|
106
|
+
next unless Lighthouse::GraphQL::DirectiveResolvers::ArgumentTransformsResolver.transforms?(field_def, phase)
|
|
107
|
+
|
|
108
|
+
steps << transforms_step(type, field_def, opts.merge(phase: phase), Lighthouse::DirectiveRegistry.default.resolution_order(alongside))
|
|
109
|
+
end
|
|
110
|
+
steps
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Around whatever resolves the field, as @lazyLoad: the result is cut to the client's @limit.
|
|
114
|
+
def limit_step(type, field_def, opts)
|
|
115
|
+
[1, -> { Lighthouse::GraphQL::DirectiveResolvers::LimitResolver.new(field_def, type, nil, opts).define_resolver(type) }]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def transforms_step(type, field_def, opts, order)
|
|
119
|
+
[order, -> { Lighthouse::GraphQL::DirectiveResolvers::ArgumentTransformsResolver.new(field_def, type, nil, opts).define_resolver(type) }]
|
|
120
|
+
end
|
|
121
|
+
|
|
72
122
|
def empty_strings_step(type, field_def, opts)
|
|
73
123
|
resolver = Lighthouse::GraphQL::DirectiveResolvers::ConvertEmptyStringsToNullDirectiveResolver
|
|
74
124
|
[Lighthouse::DirectiveRegistry.default.resolution_order(resolver.directive_name),
|
|
@@ -25,6 +25,7 @@ module Lighthouse::GraphQL::GeneratedResolvers
|
|
|
25
25
|
|
|
26
26
|
# Generated resolvers take either (obj, args) or (obj, args, ctx).
|
|
27
27
|
def invoke(resolver, obj, args, ctx)
|
|
28
|
+
args = take_lookahead(args, ctx)
|
|
28
29
|
return resolver.call(obj, args) if resolver.parameters.length == 2
|
|
29
30
|
|
|
30
31
|
resolver.call(obj, args, ctx)
|
|
@@ -37,13 +38,7 @@ module Lighthouse::GraphQL::GeneratedResolvers
|
|
|
37
38
|
def install_dispatcher(schema)
|
|
38
39
|
return unless schema.respond_to?(:definition_default_resolve=) && schema.respond_to?(:definition_default_resolve)
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
schema.definition_default_resolve = proc do |type, field, obj, arguments, ctx|
|
|
42
|
-
resolver = generated_resolver(schema, type, field)
|
|
43
|
-
next invoke(resolver, obj, arguments, ctx) if resolver
|
|
44
|
-
|
|
45
|
-
previous_default.call(type, field, obj, arguments, ctx)
|
|
46
|
-
end
|
|
41
|
+
schema.definition_default_resolve = Lighthouse::GraphQL::DefaultResolveDispatcher.new(schema, schema.definition_default_resolve)
|
|
47
42
|
end
|
|
48
43
|
|
|
49
44
|
# Our generated resolver for `field`, looked up on the schema's own type
|
|
@@ -65,6 +60,16 @@ module Lighthouse::GraphQL::GeneratedResolvers
|
|
|
65
60
|
end
|
|
66
61
|
end
|
|
67
62
|
|
|
63
|
+
# The arguments without graphql-ruby's lookahead extra, which a field resolved
|
|
64
|
+
# by a class asks for (ResolveInfo.request_lookahead): it is kept in the
|
|
65
|
+
# field's scoped context, for its ResolveInfo, and no wrapper sees it.
|
|
66
|
+
def take_lookahead(args, ctx)
|
|
67
|
+
return args unless args.is_a?(Hash) && args[:lookahead].is_a?(::GraphQL::Execution::Lookahead)
|
|
68
|
+
|
|
69
|
+
ctx.scoped_set!(Lighthouse::GraphQL::ResolveInfo::LOOKAHEAD, args[:lookahead]) if ctx.respond_to?(:scoped_set!)
|
|
70
|
+
args.except(:lookahead)
|
|
71
|
+
end
|
|
72
|
+
|
|
68
73
|
# GraphQL-Ruby's SDL builder defines `resolve_<field>` methods that call
|
|
69
74
|
# the schema's `definition_default_resolve`. Override `resolve_<field>`
|
|
70
75
|
# to delegate to our generated `resolve_field_<field>` when present, so
|
|
@@ -74,7 +79,7 @@ module Lighthouse::GraphQL::GeneratedResolvers
|
|
|
74
79
|
return unless type.respond_to?(generated)
|
|
75
80
|
|
|
76
81
|
type.define_singleton_method("resolve_#{field_def.name}") do |obj, arguments, ctx|
|
|
77
|
-
public_send(generated, obj, arguments, ctx)
|
|
82
|
+
public_send(generated, obj, Lighthouse::GraphQL::GeneratedResolvers.take_lookahead(arguments, ctx), ctx)
|
|
78
83
|
end
|
|
79
84
|
rescue StandardError
|
|
80
85
|
# ignore override failures
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
|
|
5
|
+
# Relay global ids, as PHP Lighthouse's Base64GlobalId writes them: the type
|
|
6
|
+
# name and the id, joined by a colon, in base64 ("User:1" -> "VXNlcjox").
|
|
7
|
+
# Config.global_id takes another object with `encode(type, id)` and
|
|
8
|
+
# `decode(global_id)` -> [type, id], as PHP Lighthouse lets the GlobalId
|
|
9
|
+
# contract be rebound.
|
|
10
|
+
#
|
|
11
|
+
# A schema that implements graphql-ruby's Relay ids, `id_from_object` and
|
|
12
|
+
# `object_from_id` (graphql-ruby's install writes them, on Rails' GlobalID),
|
|
13
|
+
# has one kind of global id: @globalId encodes an object's own id with
|
|
14
|
+
# `id_from_object`, and decodes with `object_from_id`, as graphql-ruby's
|
|
15
|
+
# Query.node and Node's `id` do.
|
|
16
|
+
module Lighthouse::GraphQL::GlobalId
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def encode(type, id)
|
|
20
|
+
Base64.strict_encode64("#{type}:#{id}")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def decode(global_id)
|
|
24
|
+
Base64.strict_decode64(global_id.to_s).split(':', 2)
|
|
25
|
+
rescue ArgumentError
|
|
26
|
+
[]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# [type, id] for a global id a client sent, through the schema's
|
|
30
|
+
# object_from_id (schema_ids?) or Config.global_id; a
|
|
31
|
+
# GraphQL::ExecutionError when it is not one.
|
|
32
|
+
def decode!(global_id, context = nil)
|
|
33
|
+
type, id = schema_ids?(context&.schema) ? decode_object(global_id, context) : Lighthouse::Config.global_id.decode(global_id)
|
|
34
|
+
return [type, id] if type.present? && !id.nil?
|
|
35
|
+
|
|
36
|
+
raise ::GraphQL::ExecutionError, "#{global_id.inspect} is not a valid global id."
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The global id of `object`'s own id (`id`, what a field of `type` resolved)
|
|
40
|
+
# through the schema's id_from_object (schema_ids?); nil for anything else.
|
|
41
|
+
def encode_field(type, object, id, context)
|
|
42
|
+
return unless schema_ids?(context&.schema) && object.respond_to?(:id) && object.id.to_s == id.to_s
|
|
43
|
+
|
|
44
|
+
context.schema.id_from_object(object, type, context)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Does `schema` define graphql-ruby's object_from_id (GraphQL::Schema's own raises)?
|
|
48
|
+
def schema_ids?(schema)
|
|
49
|
+
schema.respond_to?(:object_from_id) && schema.method(:object_from_id).owner != ::GraphQL::Schema.singleton_class
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# [type, id] of the object the schema's object_from_id finds.
|
|
53
|
+
def decode_object(global_id, context)
|
|
54
|
+
object = context.schema.object_from_id(global_id, context)
|
|
55
|
+
object.nil? ? [] : [object.class.name.demodulize, object.id]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -15,4 +15,49 @@ module Lighthouse::GraphQL
|
|
|
15
15
|
# skip the fix rather than fail to load (explicit nulls then fail as before).
|
|
16
16
|
builder = ::GraphQL::Schema::BuildFromDefinition.const_get(:Builder) if ::GraphQL::Schema::BuildFromDefinition.const_defined?(:Builder)
|
|
17
17
|
builder.prepend(NullDirectiveArguments) if builder && (builder.method_defined?(:args_to_kwargs) || builder.private_method_defined?(:args_to_kwargs))
|
|
18
|
+
|
|
19
|
+
# graphql-ruby builds every SDL object type on base_types[:object], and every
|
|
20
|
+
# interface from base_types[:interface]. RootTypes builds a root type on the
|
|
21
|
+
# app's code-first one (Types::QueryType) instead, and takes the app's Node
|
|
22
|
+
# interface (Types::NodeType) as the SDL's Node.
|
|
23
|
+
module RootTypeBuilder
|
|
24
|
+
def build_object_type(definition, type_resolver, base_type)
|
|
25
|
+
root = Lighthouse::GraphQL::RootTypes.base_for(definition.name)
|
|
26
|
+
return super unless root
|
|
27
|
+
|
|
28
|
+
super(Lighthouse::GraphQL::RootTypes.definition_on(definition, root), type_resolver, root)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def build_interface_type(definition, type_resolver, base_type)
|
|
32
|
+
Lighthouse::GraphQL::RootTypes.base_for(definition.name) || super
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# As above, a graphql-ruby release that reshapes the builder skips the hook:
|
|
37
|
+
# root types are then built on base_types[:object], as before.
|
|
38
|
+
if builder && %i[build_object_type build_interface_type].all? { |name| builder.method_defined?(name) || builder.private_method_defined?(name) }
|
|
39
|
+
builder.prepend(RootTypeBuilder)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The app's schema class inherits Lighthouse's (`class AppSchema <
|
|
43
|
+
# get_schema(...)`), so its Subscription root type is the parent's. A
|
|
44
|
+
# subscriptions backend the subclass `use`s (ActionCableSubscriptions) then
|
|
45
|
+
# finds no root type of the subclass's own, and graphql-ruby adds its
|
|
46
|
+
# subscription extension to no field. Look the root type up through the
|
|
47
|
+
# parent too. Extended onto the schemas Lighthouse builds (SchemaGenerator).
|
|
48
|
+
module InheritedSubscriptionRoot
|
|
49
|
+
def add_subscription_extension_if_necessary
|
|
50
|
+
return super if @subscription_object.is_a?(Class) || !defined?(::GraphQL::Subscriptions::DefaultSubscriptionResolveExtension)
|
|
51
|
+
return unless inherited_subscription_root_pending?
|
|
52
|
+
|
|
53
|
+
@subscription_extension_added = true
|
|
54
|
+
extension = ::GraphQL::Subscriptions::DefaultSubscriptionResolveExtension
|
|
55
|
+
subscription.all_field_definitions.each { |field| field.extension(extension) unless field.extensions.any?(extension) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# A backend, a root type from the parent, and no extension added yet.
|
|
59
|
+
def inherited_subscription_root_pending?
|
|
60
|
+
!defined?(@subscription_extension_added) && subscription.is_a?(Class) && !subscriptions.nil?
|
|
61
|
+
end
|
|
62
|
+
end
|
|
18
63
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
# The GraphQL multipart request specification
|
|
6
|
+
# (https://github.com/jaydenseric/graphql-multipart-request-spec), which file
|
|
7
|
+
# uploads use: a multipart form with `operations` (the JSON operation, or a
|
|
8
|
+
# batch of them), `map` (each file's form key => the paths in `operations` it
|
|
9
|
+
# goes to, e.g. { "0": ["variables.avatar"] }) and the files. The controller
|
|
10
|
+
# concern reads it into the operation(s), with every file in place.
|
|
11
|
+
module Lighthouse::GraphQL::MultipartRequest
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# The operation (a Hash) or batch (an Array), with the files in place; nil
|
|
15
|
+
# when the request is not a multipart one.
|
|
16
|
+
def operations(params)
|
|
17
|
+
raw = param(params, 'operations')
|
|
18
|
+
return unless raw
|
|
19
|
+
|
|
20
|
+
operations = parse(raw)
|
|
21
|
+
parse(param(params, 'map') || '{}').each do |key, paths|
|
|
22
|
+
Array(paths).each { |path| place(operations, path.to_s.split('.'), param(params, key)) }
|
|
23
|
+
end
|
|
24
|
+
operations
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def param(params, key)
|
|
28
|
+
params[key.to_s] || params[key.to_sym]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def parse(value)
|
|
32
|
+
value = value.to_unsafe_h if value.respond_to?(:to_unsafe_h)
|
|
33
|
+
value.is_a?(String) ? JSON.parse(value) : value
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def place(target, segments, file)
|
|
37
|
+
*parents, last = segments
|
|
38
|
+
container = parents.reduce(target) { |node, segment| node.is_a?(Array) ? node[Integer(segment)] : node[segment] }
|
|
39
|
+
raise ArgumentError, "The multipart map names a path that is not in the operations: #{segments.join('.')}" unless container
|
|
40
|
+
|
|
41
|
+
container.is_a?(Array) ? container[Integer(last)] = file : container[last] = file
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Resolves Relay's `Query.node(id:)`, which @node adds, as PHP Lighthouse's
|
|
4
|
+
# NodeRegistry: the id is decoded into a type and an id, and the type must be
|
|
5
|
+
# one with @node. Its object comes from, in order:
|
|
6
|
+
#
|
|
7
|
+
# - `@node(resolver: "Class@method")`: the class (method `call` without `@`),
|
|
8
|
+
# looked up as @field looks a class up on a type, called with the id and the
|
|
9
|
+
# context;
|
|
10
|
+
# - `@node(model: "Name")`, or else the type's @model, or else the model named
|
|
11
|
+
# after the type:
|
|
12
|
+
# the record with that primary key, batched across every `node` in the query.
|
|
13
|
+
#
|
|
14
|
+
# It also tells the Node interface which type each object is (resolve_type):
|
|
15
|
+
# `node` hands back the object with the type its id named, so a `resolver:` may
|
|
16
|
+
# return any object, a Hash included, and one record may be two node types.
|
|
17
|
+
class Lighthouse::GraphQL::NodeRegistry
|
|
18
|
+
# What `node` returns: the object, and the type its global id named.
|
|
19
|
+
Found = Struct.new(:type, :object)
|
|
20
|
+
|
|
21
|
+
def self.resolve(_root, args, context, _info = nil)
|
|
22
|
+
type_name, id = args[:id]
|
|
23
|
+
type = context.schema.get_type(type_name.to_s)
|
|
24
|
+
raise ::GraphQL::ExecutionError, "[#{type_name}] is not a type that can be resolved by node." unless node_type?(type)
|
|
25
|
+
|
|
26
|
+
object = find(type, id, context)
|
|
27
|
+
object.nil? ? nil : Found.new(type, object)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# [type, object] for what `node` found; nil leaves any other Node value to the default.
|
|
31
|
+
def self.resolve_type(object, _context)
|
|
32
|
+
[object.type, object.object] if object.is_a?(Found)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.node_type?(type)
|
|
36
|
+
type&.kind&.object? && !directive(type).nil?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.directive(type)
|
|
40
|
+
(type.ast_node&.directives || []).find { |d| d.name == 'node' }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.find(type, id, context)
|
|
44
|
+
node = directive(type)
|
|
45
|
+
resolver = Lighthouse::Support::DirectiveArgs.literal(node, 'resolver')
|
|
46
|
+
return call_resolver(resolver, id, context) if resolver
|
|
47
|
+
|
|
48
|
+
model = model_for(type, Lighthouse::Support::DirectiveArgs.literal(node, 'model'))
|
|
49
|
+
raise Lighthouse::DefinitionError, "@node on #{type.graphql_name} finds no model: give it model: or resolver:." unless model
|
|
50
|
+
|
|
51
|
+
context.dataloader.with(::GraphQL::Dataloader::ActiveRecordSource, model).load(model.type_for_attribute(model.primary_key).cast(id))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.call_resolver(reference, id, context)
|
|
55
|
+
class_name, method = Lighthouse::GraphQL::DirectiveResolvers::FieldDirectiveResolver.reference(reference)
|
|
56
|
+
klass = Lighthouse::GraphQL::DirectiveResolvers::FieldDirectiveResolver.lookup(class_name, nil)
|
|
57
|
+
raise Lighthouse::DefinitionError, "@node(resolver: #{reference.inspect}) names no class." unless klass
|
|
58
|
+
|
|
59
|
+
receiver = Lighthouse::GraphQL::ResolverDispatch.callable_receiver(klass, method || :call)
|
|
60
|
+
receiver.public_send(method || :call, id, context)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.model_for(type, name)
|
|
64
|
+
return Lighthouse::Support::ModelResolver.constantize(name) if name
|
|
65
|
+
|
|
66
|
+
Lighthouse::GraphQL::Directives::ModelDirective.model_for(type)
|
|
67
|
+
end
|
|
68
|
+
end
|