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
|
@@ -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,22 @@ 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 conventional class
|
|
59
|
+
# of a root field, or the method of the type's class. nil: the default reader.
|
|
60
|
+
def own_step(type, field_def, root_kind, bases, type_class)
|
|
61
|
+
return convention_step(type, field_def, root_kind, bases) if root_kind
|
|
62
|
+
|
|
63
|
+
type_method_step(type, field_def, type_class) if type_class
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Nothing to install when the class does not define the field's method: the
|
|
67
|
+
# field is read off the object.
|
|
68
|
+
def type_method_step(type, field_def, type_class)
|
|
69
|
+
resolver = Lighthouse::GraphQL::DirectiveResolvers::TypeMethodResolver
|
|
70
|
+
method_name = resolver.method_for(type_class, field_def)
|
|
71
|
+
[0, -> { resolver.new(field_def, type, method_name).define_resolver(type) }] if method_name
|
|
72
|
+
end
|
|
73
|
+
|
|
53
74
|
# An explicit @validator on the field already validates its arguments.
|
|
54
75
|
def input_validation?(field_def, directives)
|
|
55
76
|
directives.none? { |d| d.name == 'validator' } &&
|
|
@@ -69,6 +90,32 @@ module Lighthouse::GraphQL::FieldResolverStack
|
|
|
69
90
|
Lighthouse::GraphQL::DirectiveResolvers::ConvertEmptyStringsToNullDirectiveResolver.implicit?(field_def)
|
|
70
91
|
end
|
|
71
92
|
|
|
93
|
+
# The steps that change the arguments: empty strings to null and the rewriting
|
|
94
|
+
# directives (@globalId, @trim, @drop) outermost, so every wrapper sees them;
|
|
95
|
+
# the effectful ones (@hash, @bind, @upload) once @guard, @can* and @validator
|
|
96
|
+
# have let the call through (ArgumentTransformsResolver).
|
|
97
|
+
ARGUMENT_PHASES = { rewrite: 'convertEmptyStringsToNull', effects: 'cache' }.freeze
|
|
98
|
+
|
|
99
|
+
def argument_steps(type, field_def, directives, opts)
|
|
100
|
+
steps = []
|
|
101
|
+
steps << empty_strings_step(type, field_def, opts) if empty_string_conversion?(field_def, directives)
|
|
102
|
+
ARGUMENT_PHASES.each do |phase, alongside|
|
|
103
|
+
next unless Lighthouse::GraphQL::DirectiveResolvers::ArgumentTransformsResolver.transforms?(field_def, phase)
|
|
104
|
+
|
|
105
|
+
steps << transforms_step(type, field_def, opts.merge(phase: phase), Lighthouse::DirectiveRegistry.default.resolution_order(alongside))
|
|
106
|
+
end
|
|
107
|
+
steps
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Around whatever resolves the field, as @lazyLoad: the result is cut to the client's @limit.
|
|
111
|
+
def limit_step(type, field_def, opts)
|
|
112
|
+
[1, -> { Lighthouse::GraphQL::DirectiveResolvers::LimitResolver.new(field_def, type, nil, opts).define_resolver(type) }]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def transforms_step(type, field_def, opts, order)
|
|
116
|
+
[order, -> { Lighthouse::GraphQL::DirectiveResolvers::ArgumentTransformsResolver.new(field_def, type, nil, opts).define_resolver(type) }]
|
|
117
|
+
end
|
|
118
|
+
|
|
72
119
|
def empty_strings_step(type, field_def, opts)
|
|
73
120
|
resolver = Lighthouse::GraphQL::DirectiveResolvers::ConvertEmptyStringsToNullDirectiveResolver
|
|
74
121
|
[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,31 @@
|
|
|
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
|
+
module Lighthouse::GraphQL::GlobalId
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def encode(type, id)
|
|
14
|
+
Base64.strict_encode64("#{type}:#{id}")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def decode(global_id)
|
|
18
|
+
Base64.strict_decode64(global_id.to_s).split(':', 2)
|
|
19
|
+
rescue ArgumentError
|
|
20
|
+
[]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# [type, id] for a global id a client sent, through Config.global_id; a
|
|
24
|
+
# GraphQL::ExecutionError when it is not one.
|
|
25
|
+
def decode!(global_id)
|
|
26
|
+
type, id = Lighthouse::Config.global_id.decode(global_id)
|
|
27
|
+
return [type, id] if type.present? && !id.nil?
|
|
28
|
+
|
|
29
|
+
raise ::GraphQL::ExecutionError, "#{global_id.inspect} is not a valid global id."
|
|
30
|
+
end
|
|
31
|
+
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
|
|
@@ -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
|
|
@@ -60,12 +63,26 @@ require_relative 'namespaces'
|
|
|
60
63
|
complexity_analyzer
|
|
61
64
|
broadcasting
|
|
62
65
|
subscription
|
|
66
|
+
type
|
|
67
|
+
type_resolution
|
|
68
|
+
resolve_info
|
|
69
|
+
global_id
|
|
70
|
+
enum_directive
|
|
71
|
+
upload
|
|
72
|
+
upload_store
|
|
73
|
+
multipart_request
|
|
74
|
+
node_registry
|
|
75
|
+
scalar_coercion
|
|
63
76
|
pagination
|
|
64
77
|
relation_batch
|
|
78
|
+
relation_counts
|
|
65
79
|
nested_mutations
|
|
66
80
|
directives/pagination_manipulator
|
|
67
81
|
directives/where_conditions_directive
|
|
68
82
|
directives/soft_deletes_directive
|
|
83
|
+
directives/node_directive
|
|
84
|
+
directives/model_directive
|
|
85
|
+
directives/visibility_directive
|
|
69
86
|
directive_resolvers/aggregate_resolver
|
|
70
87
|
directive_resolvers/field_resolver
|
|
71
88
|
directive_resolvers/where_conditions_applier
|
|
@@ -83,6 +100,7 @@ require_relative 'namespaces'
|
|
|
83
100
|
directive_resolvers/association_resolvers
|
|
84
101
|
directive_resolvers/can_resolvers
|
|
85
102
|
directive_resolvers/convention_resolver
|
|
103
|
+
directive_resolvers/type_method_resolver
|
|
86
104
|
directive_resolvers/model_mutation_resolver
|
|
87
105
|
directive_resolvers/soft_delete_resolvers
|
|
88
106
|
directive_resolvers/cache_resolvers
|
|
@@ -91,15 +109,29 @@ require_relative 'namespaces'
|
|
|
91
109
|
directive_resolvers/broadcast_resolver
|
|
92
110
|
directive_resolvers/event_resolver
|
|
93
111
|
directive_resolvers/convert_empty_strings_resolver
|
|
112
|
+
directive_resolvers/with_resolver
|
|
113
|
+
directive_resolvers/with_count_resolver
|
|
114
|
+
directive_resolvers/lazy_load_resolver
|
|
115
|
+
directive_resolvers/limit_resolver
|
|
116
|
+
directive_resolvers/namespaced_resolver
|
|
117
|
+
directive_resolvers/throttle_resolver
|
|
118
|
+
directive_resolvers/async_resolver
|
|
119
|
+
directive_resolvers/global_id_resolver
|
|
120
|
+
directive_resolvers/argument_binding
|
|
121
|
+
directive_resolvers/argument_transforms_resolver
|
|
94
122
|
directives/arguments/base
|
|
95
123
|
directives/arguments/filters
|
|
124
|
+
directives/arguments/more_filters
|
|
96
125
|
directives/arguments/order_by
|
|
97
126
|
directives/arguments/soft_deletes
|
|
127
|
+
directives/definitions
|
|
98
128
|
directives/registry
|
|
99
129
|
dataloader_sources/association_loader
|
|
100
130
|
dataloader_sources/model_loader
|
|
101
131
|
dataloader_sources/relation_page_loader
|
|
102
132
|
dataloader_sources/relation_count_loader
|
|
133
|
+
dataloader_sources/relation_counts_loader
|
|
134
|
+
dataloader_sources/preload_loader
|
|
103
135
|
ide_helper
|
|
104
136
|
].each do |rel|
|
|
105
137
|
require_relative rel
|
|
@@ -109,6 +141,7 @@ end
|
|
|
109
141
|
|
|
110
142
|
# The activejob gem is optional: the job that queues @broadcast loads on first use.
|
|
111
143
|
Lighthouse::GraphQL.autoload(:BroadcastJob, File.expand_path('broadcast_job.rb', __dir__))
|
|
144
|
+
Lighthouse::GraphQL.autoload(:AsyncMutationJob, File.expand_path('async_mutation_job.rb', __dir__))
|
|
112
145
|
|
|
113
146
|
require_relative 'railtie' if defined?(Rails::Railtie)
|
|
114
147
|
|
|
@@ -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
|
|
@@ -31,8 +31,13 @@ module Lighthouse::GraphQL::ResolverBases
|
|
|
31
31
|
|
|
32
32
|
module_function
|
|
33
33
|
|
|
34
|
+
# graphql-ruby's base types that the type classes of the same kind
|
|
35
|
+
# (Types::PostType, Types::DateTimeType, ...) inherit from too.
|
|
36
|
+
TYPE_KINDS = %i[object union interface scalar].freeze
|
|
37
|
+
|
|
34
38
|
# Take the three keys out of opts[:base_types] (the caller's Hash is not
|
|
35
|
-
# changed) and return { root kind => base class } for those given
|
|
39
|
+
# changed) and return { root kind => base class } for those given, plus
|
|
40
|
+
# :types => { kind => base } for graphql-ruby's own, which stay in opts.
|
|
36
41
|
def extract!(opts)
|
|
37
42
|
base_types = opts[:base_types]
|
|
38
43
|
return {} unless base_types.is_a?(Hash)
|
|
@@ -40,7 +45,9 @@ module Lighthouse::GraphQL::ResolverBases
|
|
|
40
45
|
given = base_types.slice(*GRAPHQL_CLASSES.keys).compact
|
|
41
46
|
opts[:base_types] = base_types.except(*GRAPHQL_CLASSES.keys)
|
|
42
47
|
given.each { |key, klass| validate!(key, klass) }
|
|
43
|
-
KEYS.filter_map { |kind, key| [kind, given[key]] if given[key] }.to_h
|
|
48
|
+
bases = KEYS.filter_map { |kind, key| [kind, given[key]] if given[key] }.to_h
|
|
49
|
+
type_bases = base_types.slice(*TYPE_KINDS).compact
|
|
50
|
+
type_bases.empty? ? bases : bases.merge(types: type_bases)
|
|
44
51
|
end
|
|
45
52
|
|
|
46
53
|
def validate!(key, klass)
|