lighthouse-graphql 0.1.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 +7 -0
- data/CHANGELOG.md +47 -0
- data/LICENSE.txt +21 -0
- data/README.md +134 -0
- data/docs/authorization.md +109 -0
- data/docs/best-practices.md +75 -0
- data/docs/configuration.md +70 -0
- data/docs/custom-directives.md +129 -0
- data/docs/directives.md +298 -0
- data/docs/federation.md +108 -0
- data/docs/filtering-and-ordering.md +128 -0
- data/docs/getting-started.md +152 -0
- data/docs/relationships.md +95 -0
- data/lib/lighthouse/config.rb +112 -0
- data/lib/lighthouse/contracts/arg_builder.rb +17 -0
- data/lib/lighthouse/contracts/arg_manipulator.rb +17 -0
- data/lib/lighthouse/contracts/directive.rb +17 -0
- data/lib/lighthouse/contracts/field_manipulator.rb +17 -0
- data/lib/lighthouse/contracts/field_middleware.rb +16 -0
- data/lib/lighthouse/contracts/field_resolver.rb +18 -0
- data/lib/lighthouse/dataloader_sources/association_loader.rb +25 -0
- data/lib/lighthouse/dataloader_sources/model_loader.rb +19 -0
- data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +86 -0
- data/lib/lighthouse/directive_resolvers/all_resolver.rb +73 -0
- data/lib/lighthouse/directive_resolvers/auth_resolver.rb +44 -0
- data/lib/lighthouse/directive_resolvers/base.rb +92 -0
- data/lib/lighthouse/directive_resolvers/belongs_to_many_resolver.rb +56 -0
- data/lib/lighthouse/directive_resolvers/belongs_to_resolver.rb +47 -0
- data/lib/lighthouse/directive_resolvers/builder_applier.rb +63 -0
- data/lib/lighthouse/directive_resolvers/can_resolver.rb +86 -0
- data/lib/lighthouse/directive_resolvers/count_resolver.rb +56 -0
- data/lib/lighthouse/directive_resolvers/field_resolver.rb +78 -0
- data/lib/lighthouse/directive_resolvers/find_resolver.rb +65 -0
- data/lib/lighthouse/directive_resolvers/first_resolver.rb +53 -0
- data/lib/lighthouse/directive_resolvers/guard_resolver.rb +32 -0
- data/lib/lighthouse/directive_resolvers/has_many_resolver.rb +109 -0
- data/lib/lighthouse/directive_resolvers/has_one_resolver.rb +43 -0
- data/lib/lighthouse/directive_resolvers/method_resolver.rb +35 -0
- data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +147 -0
- data/lib/lighthouse/directive_resolvers/rename_resolver.rb +35 -0
- data/lib/lighthouse/directive_resolvers/resolution.rb +35 -0
- data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +169 -0
- data/lib/lighthouse/directives/arguments/base.rb +40 -0
- data/lib/lighthouse/directives/arguments/filters.rb +101 -0
- data/lib/lighthouse/directives/arguments/order_by.rb +48 -0
- data/lib/lighthouse/directives/registry.rb +168 -0
- data/lib/lighthouse/directives/relation_directive.rb +332 -0
- data/lib/lighthouse/directives/where_conditions_directive.rb +208 -0
- data/lib/lighthouse/graphql/version.rb +7 -0
- data/lib/lighthouse/graphql.rb +8 -0
- data/lib/lighthouse/rb_lighthouse.rb +88 -0
- data/lib/lighthouse/reference_resolver.rb +24 -0
- data/lib/lighthouse/schema_factory.rb +46 -0
- data/lib/lighthouse/schema_generator.rb +244 -0
- data/lib/lighthouse/schema_implementation.rb +119 -0
- data/lib/lighthouse/sdl_loader.rb +27 -0
- data/lib/lighthouse/support/authorization.rb +26 -0
- data/lib/lighthouse/support/directive_args.rb +33 -0
- data/lib/lighthouse/support/model_resolver.rb +61 -0
- data/lib/lighthouse/support/naming.rb +71 -0
- data/lib/lighthouse/where_conditions/operator_map.rb +35 -0
- data/lib/lighthouse-graphql.rb +10 -0
- metadata +195 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Lighthouse
|
|
6
|
+
module GraphQL
|
|
7
|
+
module DirectiveResolvers
|
|
8
|
+
class BelongsToManyDirectiveResolver < BaseDirectiveResolver
|
|
9
|
+
def define_resolver(target_type_class)
|
|
10
|
+
field_name = @field_def.name
|
|
11
|
+
assoc_name = extract_relation_name || field_name
|
|
12
|
+
|
|
13
|
+
method_name = "resolve_field_#{field_name}"
|
|
14
|
+
|
|
15
|
+
target_type_class.define_singleton_method(method_name) do |obj, arguments, ctx|
|
|
16
|
+
# Use dataloader when available
|
|
17
|
+
if ctx && ctx.respond_to?(:dataloader) && ctx.dataloader
|
|
18
|
+
loader = ctx.dataloader.with(Lighthouse::GraphQL::DataloaderSources::AssociationLoader, obj.class, assoc_name)
|
|
19
|
+
return loader.load(obj)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Fallback: call the association directly
|
|
23
|
+
if obj.respond_to?(assoc_name)
|
|
24
|
+
args = arguments.respond_to?(:to_h) ? arguments.to_h : {}
|
|
25
|
+
return obj.public_send(assoc_name)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
if @field_def.respond_to?(:resolve_proc=)
|
|
32
|
+
@field_def.resolve_proc = ->(obj, arguments, ctx) do
|
|
33
|
+
if ctx && ctx.respond_to?(:dataloader) && ctx.dataloader
|
|
34
|
+
loader = ctx.dataloader.with(Lighthouse::GraphQL::DataloaderSources::AssociationLoader, obj.class, assoc_name)
|
|
35
|
+
loader.load(obj)
|
|
36
|
+
elsif obj.respond_to?(assoc_name)
|
|
37
|
+
args = arguments.respond_to?(:to_h) ? arguments.to_h : {}
|
|
38
|
+
obj.public_send(assoc_name)
|
|
39
|
+
else
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def extract_relation_name
|
|
49
|
+
arg = @directive_node.arguments&.find { |a| a.name == 'relation' }
|
|
50
|
+
return nil unless arg
|
|
51
|
+
arg.value.respond_to?(:value) ? arg.value.value : arg.value
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Lighthouse
|
|
6
|
+
module GraphQL
|
|
7
|
+
module DirectiveResolvers
|
|
8
|
+
class BelongsToDirectiveResolver < BaseDirectiveResolver
|
|
9
|
+
def define_resolver(target_type_class)
|
|
10
|
+
field_name = @field_def.name
|
|
11
|
+
assoc_name = extract_relation_name || field_name
|
|
12
|
+
method_name = "resolve_field_#{field_name}"
|
|
13
|
+
|
|
14
|
+
target_type_class.define_singleton_method(method_name) do |obj, _arguments, ctx|
|
|
15
|
+
if ctx && ctx.respond_to?(:dataloader) && ctx.dataloader
|
|
16
|
+
loader = ctx.dataloader.with(Lighthouse::GraphQL::DataloaderSources::AssociationLoader, obj.class, assoc_name)
|
|
17
|
+
return loader.load(obj)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
obj.public_send(assoc_name) if obj.respond_to?(assoc_name)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
if @field_def.respond_to?(:resolve_proc=)
|
|
24
|
+
@field_def.resolve_proc = ->(obj, _arguments, ctx) do
|
|
25
|
+
if ctx && ctx.respond_to?(:dataloader) && ctx.dataloader
|
|
26
|
+
loader = ctx.dataloader.with(Lighthouse::GraphQL::DataloaderSources::AssociationLoader, obj.class, assoc_name)
|
|
27
|
+
loader.load(obj)
|
|
28
|
+
elsif obj.respond_to?(assoc_name)
|
|
29
|
+
obj.public_send(assoc_name)
|
|
30
|
+
else
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def extract_relation_name
|
|
40
|
+
arg = @directive_node.arguments&.find { |a| a.name == 'relation' }
|
|
41
|
+
return nil unless arg
|
|
42
|
+
arg.value.respond_to?(:value) ? arg.value.value : arg.value
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Lighthouse
|
|
6
|
+
module GraphQL
|
|
7
|
+
module DirectiveResolvers
|
|
8
|
+
class BuilderApplier
|
|
9
|
+
def self.apply(relation, field_def_ast_node, args, ctx, resolver_instance)
|
|
10
|
+
return relation unless field_def_ast_node && field_def_ast_node.directives
|
|
11
|
+
|
|
12
|
+
builder_directives = field_def_ast_node.directives.select { |d| d.name == 'builder' }
|
|
13
|
+
return relation if builder_directives.empty?
|
|
14
|
+
|
|
15
|
+
builder_directives.each do |directive_node|
|
|
16
|
+
relation = apply_directive(relation, directive_node, args, ctx, resolver_instance)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
relation
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.apply_directive(relation, directive_node, args, ctx, resolver_instance)
|
|
23
|
+
class_name = extract_directive_arg(directive_node, 'class')
|
|
24
|
+
method_name = extract_directive_arg(directive_node, 'method') || 'call'
|
|
25
|
+
|
|
26
|
+
return relation unless class_name
|
|
27
|
+
|
|
28
|
+
builder_class = resolver_instance.send(:constantize_model, class_name)
|
|
29
|
+
return relation unless builder_class
|
|
30
|
+
|
|
31
|
+
if builder_class.respond_to?(method_name)
|
|
32
|
+
result = builder_class.public_send(method_name, relation, args, ctx)
|
|
33
|
+
result || relation
|
|
34
|
+
elsif builder_class.is_a?(Class)
|
|
35
|
+
instance = builder_class.new
|
|
36
|
+
if instance.respond_to?(method_name)
|
|
37
|
+
result = instance.public_send(method_name, relation, args, ctx)
|
|
38
|
+
result || relation
|
|
39
|
+
else
|
|
40
|
+
relation
|
|
41
|
+
end
|
|
42
|
+
else
|
|
43
|
+
relation
|
|
44
|
+
end
|
|
45
|
+
rescue ::GraphQL::ExecutionError
|
|
46
|
+
# Client-facing errors (e.g. authorization / scoping guards raised by a
|
|
47
|
+
# builder) MUST propagate — never silently fall back to the unscoped
|
|
48
|
+
# relation, or a builder that enforces tenancy could be bypassed.
|
|
49
|
+
raise
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
Lighthouse::Config.handle_error(e)
|
|
52
|
+
relation
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.extract_directive_arg(directive_node, name)
|
|
56
|
+
arg = directive_node.arguments&.find { |a| a.name == name }
|
|
57
|
+
return nil unless arg
|
|
58
|
+
arg.value.respond_to?(:value) ? arg.value.value : arg.value
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Lighthouse
|
|
6
|
+
module GraphQL
|
|
7
|
+
module DirectiveResolvers
|
|
8
|
+
# @can — authorize access to a field using a policy ability (Pundit by
|
|
9
|
+
# default; configurable via Config.authorizer). Mirrors PHP Lighthouse's
|
|
10
|
+
# @can* family. Repeatable.
|
|
11
|
+
#
|
|
12
|
+
# contact(id: ID!): Contact @field(resolver: "...") @can(ability: "show")
|
|
13
|
+
#
|
|
14
|
+
# Modes:
|
|
15
|
+
# * default — resolve the field, then authorize the resolved
|
|
16
|
+
# record (skips nil; passes collections through —
|
|
17
|
+
# use scoping for lists).
|
|
18
|
+
# * root: true — authorize the parent object before resolving.
|
|
19
|
+
# * find / model — look the model up by an argument and authorize it
|
|
20
|
+
# before resolving.
|
|
21
|
+
class CanDirectiveResolver < BaseDirectiveResolver
|
|
22
|
+
def self.directive_name = 'can'
|
|
23
|
+
def self.definition
|
|
24
|
+
'directive @can(ability: String!, find: String, model: String, root: Boolean) repeatable on FIELD_DEFINITION'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def define_resolver(target_type_class)
|
|
28
|
+
field_name = @field_def.name
|
|
29
|
+
method_name = "resolve_field_#{field_name}"
|
|
30
|
+
ability = directive_arg('ability')
|
|
31
|
+
return if ability.nil?
|
|
32
|
+
|
|
33
|
+
find_arg = directive_arg('find')
|
|
34
|
+
model_name = directive_arg('model')
|
|
35
|
+
root = directive_arg('root')
|
|
36
|
+
inner = Resolution.capture(target_type_class, method_name, @field_def, field_name)
|
|
37
|
+
resolver_instance = self
|
|
38
|
+
|
|
39
|
+
wrapper = lambda do |obj, arguments, ctx|
|
|
40
|
+
if root
|
|
41
|
+
resolver_instance.send(:authorize_or_raise, ctx, obj, ability)
|
|
42
|
+
next inner.call(obj, arguments, ctx)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
if find_arg && model_name
|
|
46
|
+
args_h = arguments.respond_to?(:to_h) ? arguments.to_h : {}
|
|
47
|
+
id = Lighthouse::Support::Naming.fetch_arg(args_h, find_arg)
|
|
48
|
+
model_class = resolver_instance.send(:constantize_model, model_name)
|
|
49
|
+
record = model_class&.find_by(id: id)
|
|
50
|
+
resolver_instance.send(:authorize_or_raise, ctx, record, ability)
|
|
51
|
+
next inner.call(obj, arguments, ctx)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
result = inner.call(obj, arguments, ctx)
|
|
55
|
+
resolver_instance.send(:authorize_resolved, ctx, result, ability)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
target_type_class.define_singleton_method(method_name, &wrapper)
|
|
59
|
+
@field_def.resolve_proc = wrapper if @field_def.respond_to?(:resolve_proc=)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def directive_arg(name)
|
|
65
|
+
Lighthouse::Support::DirectiveArgs.literal(@directive_node, name)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def authorize_resolved(ctx, result, ability)
|
|
69
|
+
return result if result.nil?
|
|
70
|
+
# Collections are filtered with scoping, not item-by-item authorization.
|
|
71
|
+
return result if result.respond_to?(:each) && !result.respond_to?(:id)
|
|
72
|
+
|
|
73
|
+
authorize_or_raise(ctx, result, ability)
|
|
74
|
+
result
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def authorize_or_raise(ctx, record, ability)
|
|
78
|
+
return if record.nil?
|
|
79
|
+
|
|
80
|
+
authorized = Lighthouse::Support::Authorization.authorized?(ctx, record, ability)
|
|
81
|
+
raise(::GraphQL::ExecutionError, 'You are not authorized to perform this action.') unless authorized
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'builder_applier'
|
|
5
|
+
|
|
6
|
+
module Lighthouse
|
|
7
|
+
module GraphQL
|
|
8
|
+
module DirectiveResolvers
|
|
9
|
+
# @count(model: "Model", relation: "assoc") — return the number of records.
|
|
10
|
+
# Counts `Model.all` when `model:` is given, otherwise the named relation on
|
|
11
|
+
# the parent object (or a relation matching the field name). Honors @builder
|
|
12
|
+
# and where/argument filters before counting.
|
|
13
|
+
class CountDirectiveResolver < BaseDirectiveResolver
|
|
14
|
+
def self.directive_name = 'count'
|
|
15
|
+
def self.definition = 'directive @count(model: String, relation: String, scopes: [String!]) on FIELD_DEFINITION'
|
|
16
|
+
|
|
17
|
+
def define_resolver(target_type_class)
|
|
18
|
+
field_name = @field_def.name
|
|
19
|
+
method_name = "resolve_field_#{field_name}"
|
|
20
|
+
model_name = directive_arg('model')
|
|
21
|
+
relation_name = directive_arg('relation')
|
|
22
|
+
resolver_instance = self
|
|
23
|
+
field_ast_node = @field_def.ast_node
|
|
24
|
+
|
|
25
|
+
resolver = lambda do |obj, arguments, ctx|
|
|
26
|
+
args_h = arguments.respond_to?(:to_h) ? arguments.to_h : {}
|
|
27
|
+
relation =
|
|
28
|
+
if model_name
|
|
29
|
+
model_class = resolver_instance.constantize_model(model_name)
|
|
30
|
+
model_class&.all
|
|
31
|
+
elsif relation_name && obj.respond_to?(relation_name)
|
|
32
|
+
obj.public_send(relation_name)
|
|
33
|
+
elsif obj.respond_to?(field_name)
|
|
34
|
+
obj.public_send(field_name)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
next 0 unless relation.respond_to?(:count)
|
|
38
|
+
|
|
39
|
+
relation = DirectiveResolvers::BuilderApplier.apply(relation, field_ast_node, args_h, ctx, resolver_instance)
|
|
40
|
+
relation = resolver_instance.apply_where_filters(relation, args_h)
|
|
41
|
+
relation.count
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
target_type_class.define_singleton_method(method_name, &resolver)
|
|
45
|
+
@field_def.resolve_proc = resolver if @field_def.respond_to?(:resolve_proc=)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def directive_arg(name)
|
|
51
|
+
Lighthouse::Support::DirectiveArgs.literal(@directive_node, name)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Lighthouse
|
|
6
|
+
module GraphQL
|
|
7
|
+
module DirectiveResolvers
|
|
8
|
+
class FieldDirectiveResolver < BaseDirectiveResolver
|
|
9
|
+
def define_resolver(target_type_class)
|
|
10
|
+
field_name = @field_def.name
|
|
11
|
+
method_name = "resolve_field_#{field_name}"
|
|
12
|
+
|
|
13
|
+
resolver_class_name = extract_directive_arg('resolver') || extract_directive_arg('class')
|
|
14
|
+
resolver_instance = self
|
|
15
|
+
|
|
16
|
+
resolver = proc do |obj, args, ctx|
|
|
17
|
+
begin
|
|
18
|
+
resolver_class = resolver_instance.constantize_model(resolver_class_name)
|
|
19
|
+
return nil unless resolver_class
|
|
20
|
+
|
|
21
|
+
if resolver_class.respond_to?(:call)
|
|
22
|
+
result = resolver_class.call(obj, args, ctx)
|
|
23
|
+
else
|
|
24
|
+
instance = resolver_class.new
|
|
25
|
+
if instance.respond_to?(:call)
|
|
26
|
+
result = instance.call(obj, args, ctx)
|
|
27
|
+
elsif instance.respond_to?(:resolve)
|
|
28
|
+
result = instance.resolve(obj, args, ctx)
|
|
29
|
+
else
|
|
30
|
+
result = nil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
result
|
|
35
|
+
rescue StandardError => e
|
|
36
|
+
raise e
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
target_type_class.define_singleton_method(method_name, &resolver)
|
|
41
|
+
|
|
42
|
+
if @field_def.respond_to?(:resolve_proc=)
|
|
43
|
+
@field_def.resolve_proc = resolver
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def extract_directive_arg(name)
|
|
50
|
+
arg = @directive_node.arguments&.find { |a| a.name == name }
|
|
51
|
+
return nil unless arg
|
|
52
|
+
arg.value.respond_to?(:value) ? arg.value.value : arg.value
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def execute_resolver(resolver_class, obj, args, ctx)
|
|
56
|
+
return nil unless resolver_class
|
|
57
|
+
|
|
58
|
+
if resolver_class.respond_to?(:call)
|
|
59
|
+
resolver_class.call(obj, args, ctx)
|
|
60
|
+
elsif resolver_class.respond_to?(:new)
|
|
61
|
+
instance = resolver_class.new
|
|
62
|
+
if instance.respond_to?(:call)
|
|
63
|
+
instance.call(obj, args, ctx)
|
|
64
|
+
elsif instance.respond_to?(:resolve)
|
|
65
|
+
instance.resolve(obj, args, ctx)
|
|
66
|
+
else
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
else
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
rescue StandardError => e
|
|
73
|
+
raise e
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Lighthouse
|
|
6
|
+
module GraphQL
|
|
7
|
+
module DirectiveResolvers
|
|
8
|
+
class FindDirectiveResolver < BaseDirectiveResolver
|
|
9
|
+
def define_resolver(target_type_class)
|
|
10
|
+
field_name = @field_def.name
|
|
11
|
+
method_name = "resolve_field_#{field_name}"
|
|
12
|
+
|
|
13
|
+
model_name_static = extract_model_name || @field_def.type.unwrap.name
|
|
14
|
+
model_class_static = constantize_model(model_name_static)
|
|
15
|
+
|
|
16
|
+
target_type_class.define_singleton_method(method_name) do |_obj, arguments, ctx|
|
|
17
|
+
args = arguments.respond_to?(:to_h) ? arguments.to_h : {}
|
|
18
|
+
id = args[:id] || args['id']
|
|
19
|
+
|
|
20
|
+
model_class = model_class_static
|
|
21
|
+
return nil unless model_class
|
|
22
|
+
|
|
23
|
+
if id && ctx&.dataloader
|
|
24
|
+
loader = ctx.dataloader.with(Lighthouse::GraphQL::DataloaderSources::ModelLoader, model_class)
|
|
25
|
+
return loader.load(id)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
model_class.find_by(id: id)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
if @field_def.respond_to?(:resolve_proc=)
|
|
32
|
+
@field_def.resolve_proc = ->(_obj, arguments, ctx) do
|
|
33
|
+
args = arguments.respond_to?(:to_h) ? arguments.to_h : {}
|
|
34
|
+
id = args[:id] || args['id']
|
|
35
|
+
|
|
36
|
+
model_class = model_class_static
|
|
37
|
+
if model_class.nil?
|
|
38
|
+
nil
|
|
39
|
+
elsif id && ctx&.dataloader
|
|
40
|
+
loader = ctx.dataloader.with(Lighthouse::GraphQL::DataloaderSources::ModelLoader, model_class)
|
|
41
|
+
loader.load(id)
|
|
42
|
+
else
|
|
43
|
+
model_class.find_by(id: id)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def extract_model_name
|
|
52
|
+
arg = @directive_node.arguments&.find { |a| a.name == 'model' }
|
|
53
|
+
return nil unless arg
|
|
54
|
+
arg.value.respond_to?(:value) ? arg.value.value : arg.value
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def constantize_model(name)
|
|
58
|
+
return nil unless name
|
|
59
|
+
name = name.to_s.split('::').last
|
|
60
|
+
Object.const_get(name) rescue nil
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'builder_applier'
|
|
5
|
+
|
|
6
|
+
module Lighthouse
|
|
7
|
+
module GraphQL
|
|
8
|
+
module DirectiveResolvers
|
|
9
|
+
# @first(model: "Model") — return a single record (the first match). Like
|
|
10
|
+
# @all but `.first`, honoring @builder and where/argument filters so it can
|
|
11
|
+
# be combined with @eq/@where to fetch one record by criteria.
|
|
12
|
+
class FirstDirectiveResolver < BaseDirectiveResolver
|
|
13
|
+
def self.directive_name = 'first'
|
|
14
|
+
def self.definition = 'directive @first(model: String, scopes: [String!]) on FIELD_DEFINITION'
|
|
15
|
+
|
|
16
|
+
def define_resolver(target_type_class)
|
|
17
|
+
method_name = "resolve_field_#{@field_def.name}"
|
|
18
|
+
model_name_static = directive_arg('model') || field_type_name
|
|
19
|
+
resolver_instance = self
|
|
20
|
+
field_ast_node = @field_def.ast_node
|
|
21
|
+
|
|
22
|
+
resolver = lambda do |_obj, arguments, ctx|
|
|
23
|
+
model_class = resolver_instance.constantize_model(model_name_static)
|
|
24
|
+
next nil unless model_class
|
|
25
|
+
|
|
26
|
+
args_h = arguments.respond_to?(:to_h) ? arguments.to_h : {}
|
|
27
|
+
relation = model_class.all
|
|
28
|
+
relation = DirectiveResolvers::BuilderApplier.apply(relation, field_ast_node, args_h, ctx, resolver_instance)
|
|
29
|
+
relation = resolver_instance.apply_where_filters(relation, args_h)
|
|
30
|
+
relation.first
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
target_type_class.define_singleton_method(method_name, &resolver)
|
|
34
|
+
@field_def.resolve_proc = resolver if @field_def.respond_to?(:resolve_proc=)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def directive_arg(name)
|
|
40
|
+
Lighthouse::Support::DirectiveArgs.literal(@directive_node, name)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def field_type_name
|
|
44
|
+
unwrapped = @field_def.type.respond_to?(:unwrap) ? @field_def.type.unwrap : @field_def.type
|
|
45
|
+
return unwrapped.graphql_name if unwrapped.respond_to?(:graphql_name) && unwrapped.graphql_name
|
|
46
|
+
return unwrapped.name if unwrapped.respond_to?(:name) && unwrapped.name
|
|
47
|
+
|
|
48
|
+
@field_def.type.to_type_signature.to_s.gsub(/[!\[\]]/, '')
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Lighthouse
|
|
6
|
+
module GraphQL
|
|
7
|
+
module DirectiveResolvers
|
|
8
|
+
# @guard — require an authenticated user before the field resolves.
|
|
9
|
+
# Wraps the field's resolver; raises "Unauthenticated." when no user is
|
|
10
|
+
# present (per Config.authenticated_user). Mirrors PHP Lighthouse's @guard.
|
|
11
|
+
class GuardDirectiveResolver < BaseDirectiveResolver
|
|
12
|
+
def self.directive_name = 'guard'
|
|
13
|
+
def self.definition = 'directive @guard on FIELD_DEFINITION'
|
|
14
|
+
|
|
15
|
+
def define_resolver(target_type_class)
|
|
16
|
+
field_name = @field_def.name
|
|
17
|
+
method_name = "resolve_field_#{field_name}"
|
|
18
|
+
inner = Resolution.capture(target_type_class, method_name, @field_def, field_name)
|
|
19
|
+
|
|
20
|
+
wrapper = lambda do |obj, arguments, ctx|
|
|
21
|
+
raise(::GraphQL::ExecutionError, 'Unauthenticated.') unless Lighthouse::Support::Authorization.authenticated?(ctx)
|
|
22
|
+
|
|
23
|
+
inner.call(obj, arguments, ctx)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
target_type_class.define_singleton_method(method_name, &wrapper)
|
|
27
|
+
@field_def.resolve_proc = wrapper if @field_def.respond_to?(:resolve_proc=)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Lighthouse
|
|
6
|
+
module GraphQL
|
|
7
|
+
module DirectiveResolvers
|
|
8
|
+
class HasManyDirectiveResolver < BaseDirectiveResolver
|
|
9
|
+
def define_resolver(target_type_class)
|
|
10
|
+
field_name = @field_def.name
|
|
11
|
+
assoc_name = extract_relation_name || field_name
|
|
12
|
+
is_connection = @field_def.respond_to?(:connection?) ? @field_def.connection? : false
|
|
13
|
+
|
|
14
|
+
resolver_instance = self
|
|
15
|
+
|
|
16
|
+
# Keep compatibility with SchemaImplementation-based resolution by
|
|
17
|
+
# defining a singleton method on the owner type (e.g. Inbox.resolve_field_conversations).
|
|
18
|
+
method_name = "resolve_field_#{field_name}"
|
|
19
|
+
|
|
20
|
+
target_type_class.define_singleton_method(method_name) do |*args|
|
|
21
|
+
# Support both batch and single resolution modes.
|
|
22
|
+
# Batch mode signature: (objects_array, ctx)
|
|
23
|
+
# Single mode signature: (obj, arguments, ctx)
|
|
24
|
+
if args.length == 2 && args[0].is_a?(Array)
|
|
25
|
+
parents, ctx = args
|
|
26
|
+
|
|
27
|
+
if ctx && ctx.respond_to?(:dataloader) && ctx.dataloader
|
|
28
|
+
loader = ctx.dataloader.with(Lighthouse::GraphQL::DataloaderSources::AssociationLoader, parents.first.class, assoc_name)
|
|
29
|
+
parents.map do |p|
|
|
30
|
+
promise = loader.load(p)
|
|
31
|
+
if is_connection
|
|
32
|
+
promise.then { |value| ::GraphQL::Pagination::ArrayConnection.new(value, context: ctx, parent: nil, field: nil, arguments: {}) }
|
|
33
|
+
else
|
|
34
|
+
promise
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
else
|
|
38
|
+
parents.map do |p|
|
|
39
|
+
value = p.respond_to?(assoc_name) ? p.public_send(assoc_name) : nil
|
|
40
|
+
is_connection && value ? ::GraphQL::Pagination::ArrayConnection.new(value, context: ctx, parent: nil, field: nil, arguments: {}) : value
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
else
|
|
44
|
+
obj, arguments, ctx = args
|
|
45
|
+
|
|
46
|
+
# Use dataloader when available
|
|
47
|
+
if ctx && ctx.respond_to?(:dataloader) && ctx.dataloader
|
|
48
|
+
args_h = arguments.respond_to?(:to_h) ? arguments.to_h : {}
|
|
49
|
+
loader = ctx.dataloader.with(Lighthouse::GraphQL::DataloaderSources::AssociationLoader, obj.class, assoc_name)
|
|
50
|
+
value = loader.load(obj)
|
|
51
|
+
if is_connection
|
|
52
|
+
value.then do |v|
|
|
53
|
+
filtered = resolver_instance.apply_where_filters(v, args_h)
|
|
54
|
+
::GraphQL::Pagination::ArrayConnection.new(filtered, context: ctx, parent: nil, field: nil, arguments: {})
|
|
55
|
+
end
|
|
56
|
+
else
|
|
57
|
+
value.then { |v| resolver_instance.apply_where_filters(v, args_h) }
|
|
58
|
+
end
|
|
59
|
+
elsif obj.respond_to?(assoc_name)
|
|
60
|
+
args_h = arguments.respond_to?(:to_h) ? arguments.to_h : {}
|
|
61
|
+
value = obj.public_send(assoc_name)
|
|
62
|
+
value = resolver_instance.apply_where_filters(value, args_h)
|
|
63
|
+
is_connection && value ? ::GraphQL::Pagination::ArrayConnection.new(value, context: ctx, parent: nil, field: nil, arguments: {}) : value
|
|
64
|
+
else
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Also attach a resolve proc where supported by the GraphQL runtime.
|
|
71
|
+
if @field_def.respond_to?(:resolve_proc=)
|
|
72
|
+
@field_def.resolve_proc = ->(obj, arguments, ctx) do
|
|
73
|
+
if ctx && ctx.respond_to?(:dataloader) && ctx.dataloader
|
|
74
|
+
args_h = arguments.respond_to?(:to_h) ? arguments.to_h : {}
|
|
75
|
+
loader = ctx.dataloader.with(Lighthouse::GraphQL::DataloaderSources::AssociationLoader, obj.class, assoc_name)
|
|
76
|
+
result = loader.load(obj)
|
|
77
|
+
if is_connection
|
|
78
|
+
result.then do |value|
|
|
79
|
+
filtered = resolver_instance.apply_where_filters(value, args_h)
|
|
80
|
+
::GraphQL::Pagination::ArrayConnection.new(filtered, context: ctx, parent: nil, field: nil, arguments: {})
|
|
81
|
+
end
|
|
82
|
+
else
|
|
83
|
+
result.then { |value| resolver_instance.apply_where_filters(value, args_h) }
|
|
84
|
+
end
|
|
85
|
+
elsif obj.respond_to?(assoc_name)
|
|
86
|
+
args = arguments.respond_to?(:to_h) ? arguments.to_h : {}
|
|
87
|
+
value = obj.public_send(assoc_name)
|
|
88
|
+
value = resolver_instance.apply_where_filters(value, args)
|
|
89
|
+
is_connection && value ? ::GraphQL::Pagination::ArrayConnection.new(value, context: ctx, parent: nil, field: nil, arguments: {}) : value
|
|
90
|
+
else
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def extract_relation_name
|
|
100
|
+
# directive syntax: @hasMany(relation: "name")
|
|
101
|
+
arg = @directive_node.arguments&.find { |a| a.name == 'relation' }
|
|
102
|
+
return nil unless arg
|
|
103
|
+
# value is AST literal node; use its value method if present
|
|
104
|
+
arg.value.respond_to?(:value) ? arg.value.value : arg.value
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|