lighthouse-graphql 0.3.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 +182 -1
- data/README.md +18 -7
- data/docs/best-practices.md +16 -6
- data/docs/configuration.md +33 -8
- data/docs/directives.md +318 -10
- data/docs/events.md +3 -1
- data/docs/federation.md +3 -2
- data/docs/generators.md +82 -0
- data/docs/getting-started.md +66 -32
- data/docs/motivation.md +3 -2
- data/docs/relationships.md +42 -0
- data/docs/relay.md +137 -0
- data/docs/resolvers.md +338 -25
- data/docs/security.md +227 -0
- data/docs/subscriptions.md +60 -37
- data/docs/types.md +219 -0
- data/docs/uploads.md +80 -0
- data/lib/generators/lighthouse/base_classes.rb +70 -0
- data/lib/generators/lighthouse/class_generator.rb +19 -8
- data/lib/generators/lighthouse/install/install_generator.rb +96 -20
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +19 -10
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +21 -2
- data/lib/generators/lighthouse/mutation_generator.rb +5 -4
- data/lib/generators/lighthouse/query_generator.rb +5 -4
- data/lib/generators/lighthouse/subscription_generator.rb +21 -16
- data/lib/lighthouse/async_mutation_job.rb +17 -0
- data/lib/lighthouse/complexity.rb +175 -0
- data/lib/lighthouse/complexity_analyzer.rb +56 -0
- data/lib/lighthouse/config.rb +16 -4
- 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 +16 -5
- data/lib/lighthouse/directive_resolvers/convert_empty_strings_resolver.rb +1 -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 +14 -5
- 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 -20
- data/lib/lighthouse/directives/visibility_directive.rb +71 -0
- data/lib/lighthouse/enum_directive.rb +41 -0
- data/lib/lighthouse/events/execution_trace.rb +2 -1
- data/lib/lighthouse/events/lifecycle.rb +3 -1
- data/lib/lighthouse/field_resolver_stack.rb +59 -11
- 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 +36 -0
- data/lib/lighthouse/relation_counts.rb +29 -0
- data/lib/lighthouse/resolve_info.rb +80 -0
- data/lib/lighthouse/resolver_bases.rb +68 -0
- 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_factory.rb +0 -1
- data/lib/lighthouse/schema_generator.rb +21 -12
- data/lib/lighthouse/schema_implementation.rb +4 -8
- data/lib/lighthouse/sdl_loader.rb +0 -2
- data/lib/lighthouse/support/environment.rb +13 -0
- data/lib/lighthouse/support/model_resolver.rb +8 -6
- 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/graphql.rbs +29 -0
- data/sig/lighthouse.rbs +18 -0
- metadata +43 -1
|
@@ -12,7 +12,8 @@ class Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
|
12
12
|
raise NotImplementedError
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
# `ctx`: the query's context, for @builder on an argument.
|
|
16
|
+
def apply_where_filters(relation, args, ctx = nil)
|
|
16
17
|
return relation unless relation.respond_to?(:where)
|
|
17
18
|
|
|
18
19
|
args_h = args.respond_to?(:to_h) ? args.to_h : (args || {})
|
|
@@ -22,7 +23,7 @@ class Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
|
22
23
|
relation = apply_where_has_conditions(relation, arg_node, args_h)
|
|
23
24
|
end
|
|
24
25
|
|
|
25
|
-
apply_arg_builders(relation, args_h)
|
|
26
|
+
apply_arg_builders(relation, args_h, ctx)
|
|
26
27
|
rescue ::GraphQL::ExecutionError
|
|
27
28
|
raise
|
|
28
29
|
rescue StandardError => e
|
|
@@ -33,16 +34,13 @@ class Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
|
33
34
|
# Compose argument-builder directives (@eq, @neq, @in, @notIn, @like,
|
|
34
35
|
# @where, @orderBy) onto the relation, in SDL argument order, for each
|
|
35
36
|
# field argument that carries one and that the client supplied a value
|
|
36
|
-
# for.
|
|
37
|
-
def apply_arg_builders(relation, args_h)
|
|
37
|
+
# for — and for the input fields of an argument carrying @nest.
|
|
38
|
+
def apply_arg_builders(relation, args_h, ctx = nil)
|
|
38
39
|
return relation unless relation.respond_to?(:where)
|
|
39
40
|
return relation unless @field_def.ast_node
|
|
40
41
|
|
|
41
|
-
arg_builders.each do |arg_node, dnode, builder_class|
|
|
42
|
-
|
|
43
|
-
next if value.nil?
|
|
44
|
-
|
|
45
|
-
relation = builder_class.new(arg_node, dnode).handle_builder(relation, value)
|
|
42
|
+
arg_builders.each do |path, arg_node, dnode, builder_class|
|
|
43
|
+
relation = apply_arg_builder(relation, args_h, ctx, [path, arg_node, dnode, builder_class])
|
|
46
44
|
end
|
|
47
45
|
|
|
48
46
|
relation
|
|
@@ -136,13 +134,64 @@ class Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
|
136
134
|
end
|
|
137
135
|
|
|
138
136
|
# Every (argument, directive, builder class) on the field, in SDL order.
|
|
137
|
+
# One argument directive: @builder whenever the client sent the argument,
|
|
138
|
+
# null included (as PHP Lighthouse's); the others when its value is not null.
|
|
139
|
+
def apply_arg_builder(relation, args_h, ctx, (path, arg_node, dnode, builder_class))
|
|
140
|
+
if builder_class == :builder
|
|
141
|
+
return relation unless arg_present?(args_h, path)
|
|
142
|
+
|
|
143
|
+
return Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier.apply_argument(relation, dnode, dig_arg(args_h, path), ctx)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
value = dig_arg(args_h, path)
|
|
147
|
+
value.nil? ? relation : builder_class.new(arg_node, dnode).handle_builder(relation, value)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Did the client send the argument at `path`, even as null?
|
|
151
|
+
def arg_present?(args_h, path)
|
|
152
|
+
parent = path.size == 1 ? args_h : dig_arg(args_h, path[0...-1])
|
|
153
|
+
hash = parent.respond_to?(:to_h) && !parent.is_a?(Array) ? parent.to_h : parent
|
|
154
|
+
hash.is_a?(Hash) && Lighthouse::Support::Naming.candidate_keys(path.last).any? { |key| hash.key?(key) }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# [path, node, directive, builder class] for every argument-builder
|
|
158
|
+
# directive, in SDL order. An argument carrying @nest adds its input type's
|
|
159
|
+
# fields', at any depth, with their path into the value.
|
|
139
160
|
def arg_builders
|
|
140
|
-
registry = Lighthouse::DirectiveRegistry.default
|
|
141
161
|
(@field_def.ast_node.arguments || []).flat_map do |arg_node|
|
|
142
|
-
(arg_node.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
162
|
+
builders_of(arg_node, [arg_node.name], @field_def.arguments[arg_node.name])
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def builders_of(node, path, argument)
|
|
167
|
+
registry = Lighthouse::DirectiveRegistry.default
|
|
168
|
+
own = (node.directives || []).filter_map do |dnode|
|
|
169
|
+
builder_class = dnode.name == 'builder' ? :builder : registry.arg_builder_class(dnode.name)
|
|
170
|
+
[path, node, dnode, builder_class] if builder_class
|
|
171
|
+
end
|
|
172
|
+
own + (argument && nest?(node) ? nested_builders(argument, path) : [])
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def nest?(node)
|
|
176
|
+
(node.directives || []).any? { |d| d.name == 'nest' }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def nested_builders(argument, path)
|
|
180
|
+
type = argument.type.unwrap
|
|
181
|
+
return [] unless type.kind.input_object?
|
|
182
|
+
|
|
183
|
+
type.arguments.each_value.flat_map do |field|
|
|
184
|
+
field.ast_node ? builders_of(field.ast_node, path + [field.ast_node.name], field) : []
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# The client's value at `path` (an argument, then input fields), or nil.
|
|
189
|
+
def dig_arg(args_h, path)
|
|
190
|
+
path.reduce(args_h) do |value, name|
|
|
191
|
+
hash = value.respond_to?(:to_h) && !value.is_a?(Array) ? value.to_h : value
|
|
192
|
+
break nil unless hash.is_a?(Hash)
|
|
193
|
+
|
|
194
|
+
Lighthouse::Support::Naming.fetch_arg(hash, name)
|
|
146
195
|
end
|
|
147
196
|
end
|
|
148
197
|
end
|
|
@@ -2,39 +2,99 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'base'
|
|
4
4
|
|
|
5
|
+
# The field-level query directives, in SDL order: @builder classes, and
|
|
6
|
+
# @whereAuth, which keeps the records the signed-in user owns. @builder on an
|
|
7
|
+
# argument is applied with the other argument directives (apply_argument).
|
|
8
|
+
#
|
|
9
|
+
# A builder is `class:` and `method:` (default `call`), or PHP Lighthouse's
|
|
10
|
+
# `method: "Class@method"`, looked up in Config.namespaces[:builders]. It gets
|
|
11
|
+
# the relation, a second value and the context, and returns the relation:
|
|
12
|
+
#
|
|
13
|
+
# on a field (relation, args, context), or the directive's `value:` instead of args
|
|
14
|
+
# on an argument (relation, the argument's value, context), whenever the client sends
|
|
15
|
+
# the argument, null included
|
|
5
16
|
class Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier
|
|
6
|
-
DEFINITION =
|
|
17
|
+
DEFINITION = <<~GRAPHQL.chomp
|
|
18
|
+
"""
|
|
19
|
+
Manipulate the query builder with a method.
|
|
20
|
+
"""
|
|
21
|
+
directive @builder(
|
|
22
|
+
"The builder class, in the builders namespace."
|
|
23
|
+
class: String
|
|
24
|
+
"The method to call: a name (default `call`), or a class and a method separated by `@`."
|
|
25
|
+
method: String
|
|
26
|
+
"On a field, the value the method gets after the relation, instead of the arguments."
|
|
27
|
+
value: BuilderValue
|
|
28
|
+
) repeatable on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | FIELD_DEFINITION
|
|
29
|
+
GRAPHQL
|
|
30
|
+
|
|
31
|
+
TYPE_DEFINITIONS = {
|
|
32
|
+
'BuilderValue' => <<~GRAPHQL.chomp
|
|
33
|
+
"Any constant literal value: https://graphql.github.io/graphql-spec/draft/#sec-Input-Values"
|
|
34
|
+
scalar BuilderValue
|
|
35
|
+
GRAPHQL
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
WHERE_AUTH_DEFINITION = <<~GRAPHQL.chomp
|
|
39
|
+
"""
|
|
40
|
+
Filter a type to only return instances owned by the current user.
|
|
41
|
+
"""
|
|
42
|
+
directive @whereAuth(
|
|
43
|
+
"The relation of the model that links it to the user: a belongs_to, a has_many, a :through."
|
|
44
|
+
relation: String!
|
|
45
|
+
"Accepted for PHP Lighthouse parity: the user is Config.authenticated_user's."
|
|
46
|
+
guards: [String!]
|
|
47
|
+
) on FIELD_DEFINITION
|
|
48
|
+
GRAPHQL
|
|
49
|
+
|
|
50
|
+
DEFINITIONS = { 'builder' => DEFINITION, 'whereAuth' => WHERE_AUTH_DEFINITION }.freeze
|
|
7
51
|
|
|
8
52
|
def self.apply(relation, field_def_ast_node, args, ctx, resolver_instance)
|
|
9
53
|
return relation unless field_def_ast_node&.directives
|
|
10
54
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
55
|
+
field_def_ast_node.directives.each do |directive_node|
|
|
56
|
+
case directive_node.name
|
|
57
|
+
when 'builder' then relation = apply_directive(relation, directive_node, args, ctx, resolver_instance)
|
|
58
|
+
when 'whereAuth' then relation = where_auth(relation, directive_node, ctx)
|
|
59
|
+
end
|
|
16
60
|
end
|
|
17
61
|
|
|
18
62
|
relation
|
|
19
63
|
end
|
|
20
64
|
|
|
65
|
+
# The records whose `relation:` links them to the signed-in user; none for a guest.
|
|
66
|
+
def self.where_auth(relation, directive_node, ctx)
|
|
67
|
+
user = Lighthouse::Support::Authorization.authenticated_user(ctx)
|
|
68
|
+
return relation.none unless user
|
|
69
|
+
|
|
70
|
+
model = relation.klass
|
|
71
|
+
name = extract_directive_arg(directive_node, 'relation').to_sym
|
|
72
|
+
reflection = model.reflect_on_association(name)
|
|
73
|
+
raise Lighthouse::DefinitionError, "@whereAuth: #{model} has no relation #{name}." unless reflection
|
|
74
|
+
return relation.where(name => user) if reflection.belongs_to?
|
|
75
|
+
|
|
76
|
+
owned = model.unscoped.joins(name).where(reflection.klass.table_name => { reflection.klass.primary_key => user.id })
|
|
77
|
+
relation.where(model.primary_key => owned.select(model.arel_table[model.primary_key]))
|
|
78
|
+
end
|
|
79
|
+
|
|
21
80
|
def self.apply_directive(relation, directive_node, args, ctx, _resolver_instance)
|
|
22
|
-
|
|
23
|
-
|
|
81
|
+
value = directive_node.arguments&.any? { |a| a.name == 'value' } ? extract_directive_arg(directive_node, 'value') : args
|
|
82
|
+
call_builder(relation, directive_node, value, ctx)
|
|
83
|
+
end
|
|
24
84
|
|
|
25
|
-
|
|
85
|
+
# @builder on an argument the client sent: the method gets its value.
|
|
86
|
+
def self.apply_argument(relation, directive_node, value, ctx)
|
|
87
|
+
call_builder(relation, directive_node, value, ctx)
|
|
88
|
+
end
|
|
26
89
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
builder_class = Lighthouse::Support::ModelResolver.constantize(
|
|
30
|
-
class_name, namespaces: Lighthouse::Config.namespaces[:builders]
|
|
31
|
-
)
|
|
90
|
+
def self.call_builder(relation, directive_node, value, ctx)
|
|
91
|
+
builder_class, method_name = reference(directive_node)
|
|
32
92
|
return relation unless builder_class
|
|
33
93
|
|
|
34
94
|
receiver = builder_receiver(builder_class, method_name)
|
|
35
95
|
return relation unless receiver
|
|
36
96
|
|
|
37
|
-
receiver.public_send(method_name, relation,
|
|
97
|
+
receiver.public_send(method_name, relation, value, ctx) || relation
|
|
38
98
|
rescue ::GraphQL::ExecutionError
|
|
39
99
|
# Client-facing errors (e.g. authorization / scoping guards raised by a
|
|
40
100
|
# builder) MUST propagate — never silently fall back to the unscoped
|
|
@@ -45,11 +105,21 @@ class Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier
|
|
|
45
105
|
relation
|
|
46
106
|
end
|
|
47
107
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
108
|
+
# [builder class, method name] from `class:` and `method:`, or from
|
|
109
|
+
# `method: "Class@method"`. Builders are not resolvers: they live in their
|
|
110
|
+
# own namespace (Config.namespaces[:builders], app/graphql/builders).
|
|
111
|
+
def self.reference(directive_node)
|
|
112
|
+
class_name = extract_directive_arg(directive_node, 'class')
|
|
113
|
+
method_name = extract_directive_arg(directive_node, 'method')
|
|
114
|
+
class_name, method_name = method_name.to_s.split('@', 2) if class_name.nil? && method_name
|
|
115
|
+
return [nil, nil] unless class_name
|
|
116
|
+
|
|
117
|
+
builder_class = Lighthouse::Support::ModelResolver.constantize(class_name, namespaces: Lighthouse::Config.namespaces[:builders])
|
|
118
|
+
[builder_class, method_name.presence || 'call']
|
|
119
|
+
end
|
|
51
120
|
|
|
52
|
-
|
|
121
|
+
def self.extract_directive_arg(directive_node, name)
|
|
122
|
+
Lighthouse::Support::DirectiveArgs.literal(directive_node, name)
|
|
53
123
|
end
|
|
54
124
|
|
|
55
125
|
# What answers the builder method: the class itself, else an instance
|
|
@@ -136,7 +136,7 @@ module Lighthouse::GraphQL::DirectiveResolvers
|
|
|
136
136
|
|
|
137
137
|
# The records the field's argument filters (@eq, @where, ...) select.
|
|
138
138
|
def authorize_before(_obj, args, ctx)
|
|
139
|
-
authorize_each!(ctx, apply_where_filters(scoped(model_class.all), args).to_a, args)
|
|
139
|
+
authorize_each!(ctx, apply_where_filters(scoped(model_class.all), args, ctx).to_a, args)
|
|
140
140
|
end
|
|
141
141
|
end
|
|
142
142
|
|
|
@@ -4,17 +4,21 @@ require_relative 'base'
|
|
|
4
4
|
|
|
5
5
|
# Resolves a Query or Mutation field that has no resolving directive by
|
|
6
6
|
# convention, as PHP Lighthouse does: `Query.latestPosts` resolves to
|
|
7
|
-
# `
|
|
8
|
-
#
|
|
7
|
+
# `Resolvers::LatestPosts` (a GraphQL::Schema::Resolver, by graphql-ruby's
|
|
8
|
+
# convention a Resolvers::BaseResolver), `Mutation.publishPost` to
|
|
9
|
+
# `Mutations::PublishPost` (a GraphQL::Schema::Mutation), looked up in
|
|
9
10
|
# Config.namespaces[:queries] / [:mutations] at call time so code reloading
|
|
10
11
|
# keeps working. Not a directive: SchemaGenerator installs it.
|
|
11
12
|
class Lighthouse::GraphQL::DirectiveResolvers::ConventionResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
12
|
-
|
|
13
|
+
# `base`: the class the schema's base_types require the class to inherit from.
|
|
14
|
+
def initialize(field_def, type_def, kind, base = nil)
|
|
13
15
|
super(field_def, type_def, nil)
|
|
14
16
|
@kind = kind
|
|
17
|
+
@base = base
|
|
15
18
|
end
|
|
16
19
|
|
|
17
20
|
def define_resolver(target_type_class)
|
|
21
|
+
Lighthouse::GraphQL::ResolveInfo.request_lookahead(@field_def)
|
|
18
22
|
me = self
|
|
19
23
|
inner = capture_inner(target_type_class)
|
|
20
24
|
|
|
@@ -31,6 +35,12 @@ class Lighthouse::GraphQL::DirectiveResolvers::ConventionResolver < Lighthouse::
|
|
|
31
35
|
ROOT_KINDS[type.respond_to?(:graphql_name) ? type.graphql_name : type.to_s]
|
|
32
36
|
end
|
|
33
37
|
|
|
38
|
+
# The class a root field resolves to, without its namespace: `LatestPosts`
|
|
39
|
+
# for `latestPosts`.
|
|
40
|
+
def self.class_name_for(field_def)
|
|
41
|
+
field_def.name.to_s.camelize
|
|
42
|
+
end
|
|
43
|
+
|
|
34
44
|
private
|
|
35
45
|
|
|
36
46
|
def resolve_by_convention(inner, obj, args, ctx)
|
|
@@ -43,8 +53,9 @@ class Lighthouse::GraphQL::DirectiveResolvers::ConventionResolver < Lighthouse::
|
|
|
43
53
|
"#{klass} resolves #{location}, so it must inherit from GraphQL::Schema::Resolver " \
|
|
44
54
|
'(or GraphQL::Schema::Mutation).'
|
|
45
55
|
end
|
|
56
|
+
Lighthouse::GraphQL::ResolverBases.check!(klass, @base, location)
|
|
46
57
|
|
|
47
|
-
Lighthouse::GraphQL::ResolverDispatch.call(klass, @field_def, obj, args, ctx)
|
|
58
|
+
Lighthouse::GraphQL::ResolverDispatch.call(klass, Lighthouse::GraphQL::ResolveInfo.for(@field_def, @type_def, obj, args, ctx))
|
|
48
59
|
end
|
|
49
60
|
|
|
50
61
|
# No class: the root object may still answer (a custom root_value);
|
|
@@ -61,7 +72,7 @@ class Lighthouse::GraphQL::DirectiveResolvers::ConventionResolver < Lighthouse::
|
|
|
61
72
|
end
|
|
62
73
|
|
|
63
74
|
def class_name
|
|
64
|
-
@class_name ||= @field_def
|
|
75
|
+
@class_name ||= self.class.class_name_for(@field_def)
|
|
65
76
|
end
|
|
66
77
|
|
|
67
78
|
def location
|
|
@@ -19,7 +19,7 @@ require_relative 'base'
|
|
|
19
19
|
# carried the directive.
|
|
20
20
|
#
|
|
21
21
|
# The conversion is a wrapper around the field's resolver (outermost, order
|
|
22
|
-
# 6), so every kind of resolver — directives, @field,
|
|
22
|
+
# 6), so every kind of resolver — directives, @field, Resolvers::/Mutations::
|
|
23
23
|
# classes, the default reader — and @validator see the converted arguments,
|
|
24
24
|
# and other fields sharing an input type are unaffected. SchemaGenerator
|
|
25
25
|
# installs it too, without the directive on the field, when an argument or
|
|
@@ -44,7 +44,7 @@ class Lighthouse::GraphQL::DirectiveResolvers::CountDirectiveResolver < Lighthou
|
|
|
44
44
|
args_h = arguments.respond_to?(:to_h) ? arguments.to_h : {}
|
|
45
45
|
relation = apply_scopes(relation)
|
|
46
46
|
relation = Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier.apply(relation, @field_def.ast_node, args_h, ctx, self)
|
|
47
|
-
relation = apply_where_filters(relation, args_h)
|
|
47
|
+
relation = apply_where_filters(relation, args_h, ctx)
|
|
48
48
|
relation.count
|
|
49
49
|
end
|
|
50
50
|
end
|
|
@@ -4,21 +4,49 @@ require_relative 'base'
|
|
|
4
4
|
|
|
5
5
|
class Lighthouse::GraphQL::DirectiveResolvers::FieldDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
6
6
|
def self.directive_name = 'field'
|
|
7
|
-
def self.definition = 'directive @field(resolver: String, class: String) on FIELD_DEFINITION'
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
def self.definition
|
|
9
|
+
<<~GRAPHQL.chomp
|
|
10
|
+
"""
|
|
11
|
+
Assign a resolver to a field: a class, or a class and a method separated by `@`
|
|
12
|
+
("Statistics@users_total"). Without a method, the class resolves, or is called.
|
|
13
|
+
"""
|
|
14
|
+
directive @field(resolver: String, class: String) on FIELD_DEFINITION
|
|
15
|
+
GRAPHQL
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# ["Statistics", "users_total"] for "Statistics@users_total"; [name, nil] without `@`.
|
|
19
|
+
def self.reference(resolver)
|
|
20
|
+
class_name, method = resolver.to_s.split('@', 2)
|
|
21
|
+
[class_name, method.presence]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The class @field names. On Query and Mutation (`kind` :queries / :mutations)
|
|
25
|
+
# it is looked up in that kind's namespaces, nested names included
|
|
26
|
+
# (`Feeds::Home` is Resolvers::Feeds::Home), then as written. On any other
|
|
27
|
+
# type, in the resolvers namespace first (`Posts::TagsCount` is
|
|
28
|
+
# Resolvers::Posts::TagsCount), then in Config.resolver_namespaces. Looked up
|
|
29
|
+
# at call time, so code reloading keeps working.
|
|
30
|
+
def self.lookup(class_name, kind)
|
|
31
|
+
resolver = Lighthouse::Support::ModelResolver
|
|
32
|
+
return resolver.constantize(class_name, namespaces: Lighthouse::Config.namespaces[kind], nested: true) if kind
|
|
33
|
+
|
|
34
|
+
resolver.constantize(class_name, namespaces: Lighthouse::Config.namespaces[:queries], nested: true, fallback: false) ||
|
|
35
|
+
resolver.constantize(class_name, namespaces: Lighthouse::Config.resolver_namespaces)
|
|
36
|
+
end
|
|
37
|
+
|
|
11
38
|
def define_resolver(target_type_class)
|
|
39
|
+
Lighthouse::GraphQL::ResolveInfo.request_lookahead(@field_def)
|
|
12
40
|
field_def = @field_def
|
|
13
|
-
class_name = extract_directive_arg('resolver') || extract_directive_arg('class')
|
|
41
|
+
class_name, method = self.class.reference(extract_directive_arg('resolver') || extract_directive_arg('class'))
|
|
14
42
|
kind = root_kind(target_type_class)
|
|
15
43
|
|
|
16
44
|
install(target_type_class, lambda do |obj, args, ctx|
|
|
17
|
-
|
|
18
|
-
klass = Lighthouse::Support::ModelResolver.constantize(class_name, namespaces: namespaces)
|
|
45
|
+
klass = Lighthouse::GraphQL::DirectiveResolvers::FieldDirectiveResolver.lookup(class_name, kind)
|
|
19
46
|
next nil unless klass
|
|
20
47
|
|
|
21
|
-
Lighthouse::GraphQL::
|
|
48
|
+
info = Lighthouse::GraphQL::ResolveInfo.for(field_def, target_type_class, obj, args, ctx)
|
|
49
|
+
Lighthouse::GraphQL::ResolverDispatch.call(klass, info, method: method)
|
|
22
50
|
end)
|
|
23
51
|
end
|
|
24
52
|
|
|
@@ -23,7 +23,7 @@ class Lighthouse::GraphQL::DirectiveResolvers::FirstDirectiveResolver < Lighthou
|
|
|
23
23
|
args_h = arguments.respond_to?(:to_h) ? arguments.to_h : {}
|
|
24
24
|
relation = resolver_instance.apply_scopes(model_class.all)
|
|
25
25
|
relation = Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier.apply(relation, field_ast_node, args_h, ctx, resolver_instance)
|
|
26
|
-
relation = resolver_instance.apply_where_filters(relation, args_h)
|
|
26
|
+
relation = resolver_instance.apply_where_filters(relation, args_h, ctx)
|
|
27
27
|
relation.first
|
|
28
28
|
end
|
|
29
29
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
# @globalId — Relay global ids, as PHP Lighthouse's directive: on a field it
|
|
6
|
+
# encodes the value with the type's name (`id: ID! @globalId` answers
|
|
7
|
+
# "VXNlcjox" for User 1); on an argument or input field it decodes what the
|
|
8
|
+
# client sent (ArgumentTransformsResolver). Config.global_id does the encoding.
|
|
9
|
+
class Lighthouse::GraphQL::DirectiveResolvers::GlobalIdDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
10
|
+
NAME = 'globalId'
|
|
11
|
+
|
|
12
|
+
def self.directive_name = NAME
|
|
13
|
+
|
|
14
|
+
def self.definition
|
|
15
|
+
<<~GRAPHQL.chomp
|
|
16
|
+
"""
|
|
17
|
+
Converts between IDs/types and global IDs.
|
|
18
|
+
|
|
19
|
+
When used upon a field, it encodes;
|
|
20
|
+
when used upon an argument, it decodes.
|
|
21
|
+
"""
|
|
22
|
+
directive @globalId(
|
|
23
|
+
"""
|
|
24
|
+
Decoding a global id produces a tuple of `$type` and `$id`.
|
|
25
|
+
This setting controls which of those is passed along.
|
|
26
|
+
"""
|
|
27
|
+
decode: GlobalIdDecode = ARRAY
|
|
28
|
+
) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION
|
|
29
|
+
GRAPHQL
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.type_definitions
|
|
33
|
+
{
|
|
34
|
+
'GlobalIdDecode' => <<~GRAPHQL.chomp
|
|
35
|
+
"Options for the `decode` argument of `@globalId`."
|
|
36
|
+
enum GlobalIdDecode {
|
|
37
|
+
"Return an array of `[$type, $id]`."
|
|
38
|
+
ARRAY
|
|
39
|
+
"Return just `$type`."
|
|
40
|
+
TYPE
|
|
41
|
+
"Return just `$id`."
|
|
42
|
+
ID
|
|
43
|
+
}
|
|
44
|
+
GRAPHQL
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def define_resolver(target_type_class)
|
|
49
|
+
inner = capture_inner(target_type_class)
|
|
50
|
+
type_name = target_type_class.graphql_name
|
|
51
|
+
|
|
52
|
+
install(target_type_class, lambda do |obj, args, ctx|
|
|
53
|
+
id = inner.call(obj, args, ctx)
|
|
54
|
+
id.nil? ? nil : Lighthouse::Config.global_id.encode(type_name, id)
|
|
55
|
+
end)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
# @lazyLoad(relations: ["replies"]) — eager-load relations of the records a field
|
|
6
|
+
# returns, as PHP Lighthouse's @lazyLoad does, so the fields the client selects on
|
|
7
|
+
# them do not query once per record. Works on a list, a relation, and a @paginate
|
|
8
|
+
# page; across every parent in the response at once, so a @hasMany's records for
|
|
9
|
+
# all its parents load their relations together.
|
|
10
|
+
#
|
|
11
|
+
# type Post {
|
|
12
|
+
# comments: [Comment!]! @hasMany @lazyLoad(relations: ["replies", "author"])
|
|
13
|
+
# }
|
|
14
|
+
class Lighthouse::GraphQL::DirectiveResolvers::LazyLoadDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
15
|
+
def self.directive_name = 'lazyLoad'
|
|
16
|
+
|
|
17
|
+
def self.definition
|
|
18
|
+
<<~GRAPHQL.chomp
|
|
19
|
+
"""
|
|
20
|
+
Eager-load relations of the records the field returns.
|
|
21
|
+
"""
|
|
22
|
+
directive @lazyLoad(
|
|
23
|
+
"The relations to load, nested with dots."
|
|
24
|
+
relations: [String!]!
|
|
25
|
+
) repeatable on FIELD_DEFINITION
|
|
26
|
+
GRAPHQL
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def define_resolver(target_type_class)
|
|
30
|
+
inner = capture_inner(target_type_class)
|
|
31
|
+
associations = Lighthouse::Support::Preload.associations(directive_arg('relations'))
|
|
32
|
+
|
|
33
|
+
install(target_type_class, lambda do |obj, args, ctx|
|
|
34
|
+
Lighthouse::GraphQL::DirectiveResolvers::LazyLoadDirectiveResolver.load(inner.call(obj, args, ctx), associations, ctx)
|
|
35
|
+
end)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# `result` with `associations` loaded into its records.
|
|
39
|
+
def self.load(result, associations, ctx)
|
|
40
|
+
return result.preload(*associations) if unloaded_relation?(result)
|
|
41
|
+
|
|
42
|
+
records = records_of(result).grep(::ActiveRecord::Base)
|
|
43
|
+
ctx.dataloader.with(Lighthouse::GraphQL::DataloaderSources::PreloadLoader, associations, []).load_all(records) if records.any?
|
|
44
|
+
result
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# A query not run yet takes the preload itself. An association's records (what
|
|
48
|
+
# @hasMany returns, loaded for every parent at once) or a loaded relation
|
|
49
|
+
# are records already: they load their relations together, in the Dataloader.
|
|
50
|
+
def self.unloaded_relation?(result)
|
|
51
|
+
result.is_a?(::ActiveRecord::Relation) && !result.is_a?(::ActiveRecord::Associations::CollectionProxy) && !result.loaded?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.records_of(result)
|
|
55
|
+
case result
|
|
56
|
+
when Lighthouse::GraphQL::Pagination::LazyPage then result.items
|
|
57
|
+
when Hash then Array(result['data'] || result[:data])
|
|
58
|
+
else Array(result)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
# @limit on an argument — the client caps how many results the field returns,
|
|
6
|
+
# as PHP Lighthouse's directive: `posts(limit: Int @limit): [Post!]! @all`. The
|
|
7
|
+
# argument must be an Int; null leaves the results alone. A query not run yet
|
|
8
|
+
# gets a LIMIT; a list already loaded (a @hasMany's records, loaded for every
|
|
9
|
+
# parent at once) is cut in memory, so one parent's limit never cuts another's.
|
|
10
|
+
# With `builder: true` it is a LIMIT on the field's query instead, through the
|
|
11
|
+
# argument directives (Arguments::Limit): for root fields, as PHP Lighthouse
|
|
12
|
+
# warns, since a relation batched for many parents would be cut as a whole.
|
|
13
|
+
# PHP Lighthouse also allows @limit on the field, where it marks the field the
|
|
14
|
+
# argument limits; it is accepted there and changes nothing. Not a directive
|
|
15
|
+
# resolver: FieldResolverStack installs it on a field whose arguments carry
|
|
16
|
+
# @limit, around whatever resolves the field.
|
|
17
|
+
class Lighthouse::GraphQL::DirectiveResolvers::LimitResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
18
|
+
# The keyword of the argument whose @limit cuts the results, or nil; every
|
|
19
|
+
# argument carrying @limit must be an Int.
|
|
20
|
+
def self.keyword(field_def)
|
|
21
|
+
limiting = field_def.arguments.each_value.select { |argument| directive(argument) }
|
|
22
|
+
limiting.each { |argument| validate!(field_def, argument) }
|
|
23
|
+
limiting.find { |argument| !Lighthouse::Support::DirectiveArgs.literal(directive(argument), 'builder') }&.keyword
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.directive(argument)
|
|
27
|
+
(argument.ast_node&.directives || []).find { |d| d.name == 'limit' }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.validate!(field_def, argument)
|
|
31
|
+
return if argument.type.unwrap.graphql_name == 'Int'
|
|
32
|
+
|
|
33
|
+
raise Lighthouse::DefinitionError,
|
|
34
|
+
"The @limit directive must only be used on arguments of type Int, got #{field_def.name}.#{argument.graphql_name} of type " \
|
|
35
|
+
"#{argument.type.unwrap.graphql_name}."
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def define_resolver(target_type_class)
|
|
39
|
+
keyword = self.class.keyword(@field_def)
|
|
40
|
+
inner = capture_inner(target_type_class)
|
|
41
|
+
|
|
42
|
+
install(target_type_class, lambda do |obj, args, ctx|
|
|
43
|
+
limit = args.is_a?(Hash) ? args[keyword] : nil
|
|
44
|
+
result = inner.call(obj, args, ctx)
|
|
45
|
+
limit.nil? ? result : Lighthouse::GraphQL::DirectiveResolvers::LimitResolver.cut(result, limit)
|
|
46
|
+
end)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.cut(result, limit)
|
|
50
|
+
raise ::GraphQL::ExecutionError, "The limit must be positive, got #{limit}." if limit.negative?
|
|
51
|
+
|
|
52
|
+
case result
|
|
53
|
+
when ::ActiveRecord::Associations::CollectionProxy then result.to_a.first(limit)
|
|
54
|
+
when ::ActiveRecord::Relation then result.loaded? ? result.to_a.first(limit) : result.limit(limit)
|
|
55
|
+
when Array then result.first(limit)
|
|
56
|
+
else result
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -8,6 +8,12 @@ require_relative 'base'
|
|
|
8
8
|
#
|
|
9
9
|
# labels: [String!]! @method(name: "label_list")
|
|
10
10
|
# canReply: Boolean @method(name: "can_reply?")
|
|
11
|
+
# excerpt(length: Int = 80): String @method
|
|
12
|
+
#
|
|
13
|
+
# The field's arguments reach the method: as snake_case keywords when it takes
|
|
14
|
+
# keywords (`def excerpt(length:)`), else positionally in the order the SDL
|
|
15
|
+
# declares them, as PHP Lighthouse passes them (`def excerpt(length)`). A method
|
|
16
|
+
# taking no parameters is called without them.
|
|
11
17
|
class Lighthouse::GraphQL::DirectiveResolvers::MethodDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
12
18
|
def self.directive_name = 'method'
|
|
13
19
|
def self.definition = 'directive @method(name: String) on FIELD_DEFINITION'
|
|
@@ -17,13 +23,42 @@ class Lighthouse::GraphQL::DirectiveResolvers::MethodDirectiveResolver < Lightho
|
|
|
17
23
|
target_method = (Lighthouse::Support::DirectiveArgs.literal(@directive_node, 'name') ||
|
|
18
24
|
Lighthouse::Support::Naming.attribute(@field_def.name)).to_s
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
field_def = @field_def
|
|
27
|
+
resolver = lambda do |obj, arguments, ctx|
|
|
28
|
+
next nil if obj.nil? || !obj.respond_to?(target_method)
|
|
22
29
|
|
|
23
|
-
|
|
30
|
+
Lighthouse::GraphQL::DirectiveResolvers::MethodDirectiveResolver.call_method(obj.method(target_method), field_def, arguments, ctx)
|
|
24
31
|
end
|
|
25
32
|
|
|
26
33
|
target_type_class.define_singleton_method(resolver_method, &resolver)
|
|
27
34
|
@field_def.resolve_proc = resolver if @field_def.respond_to?(:resolve_proc=)
|
|
28
35
|
end
|
|
36
|
+
|
|
37
|
+
KEYWORDS = %i[key keyreq keyrest].freeze
|
|
38
|
+
NAMED_KEYWORDS = %i[key keyreq].freeze
|
|
39
|
+
|
|
40
|
+
# Call `method` with the field's arguments, as the method takes them.
|
|
41
|
+
def self.call_method(method, field_def, arguments, ctx)
|
|
42
|
+
params = method.parameters
|
|
43
|
+
return method.call if field_def.arguments.empty? || params.empty?
|
|
44
|
+
|
|
45
|
+
values = Lighthouse::Support::Arguments.prepare(field_def, arguments, ctx)
|
|
46
|
+
return method.call(**keywords(params, values)) if params.any? { |type, _| KEYWORDS.include?(type) }
|
|
47
|
+
|
|
48
|
+
method.call(*positional(params, field_def, values))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The values in the order the SDL declares the arguments, as many as the
|
|
52
|
+
# method takes; all of them when it takes *rest.
|
|
53
|
+
def self.positional(params, field_def, values)
|
|
54
|
+
ordered = field_def.arguments.each_value.map { |arg| values[arg.keyword] }
|
|
55
|
+
params.any? { |type, _| type == :rest } ? ordered : ordered.first(params.size)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The values the method's keywords name; all of them when it takes **keywords.
|
|
59
|
+
def self.keywords(params, values)
|
|
60
|
+
return values if params.any? { |type, _| type == :keyrest }
|
|
61
|
+
|
|
62
|
+
values.slice(*params.filter_map { |type, name| name if NAMED_KEYWORDS.include?(type) })
|
|
63
|
+
end
|
|
29
64
|
end
|
|
@@ -196,7 +196,7 @@ module Lighthouse::GraphQL::DirectiveResolvers
|
|
|
196
196
|
args = Lighthouse::Support::Arguments.shallow(args)
|
|
197
197
|
raise ::GraphQL::ExecutionError, WOULD_MODIFY_ALL unless filtered?(args)
|
|
198
198
|
|
|
199
|
-
records = apply_where_filters(records_of(model), args).to_a
|
|
199
|
+
records = apply_where_filters(records_of(model), args, ctx).to_a
|
|
200
200
|
records.each do |record|
|
|
201
201
|
next if modify(record)
|
|
202
202
|
|