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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +47 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +134 -0
  5. data/docs/authorization.md +109 -0
  6. data/docs/best-practices.md +75 -0
  7. data/docs/configuration.md +70 -0
  8. data/docs/custom-directives.md +129 -0
  9. data/docs/directives.md +298 -0
  10. data/docs/federation.md +108 -0
  11. data/docs/filtering-and-ordering.md +128 -0
  12. data/docs/getting-started.md +152 -0
  13. data/docs/relationships.md +95 -0
  14. data/lib/lighthouse/config.rb +112 -0
  15. data/lib/lighthouse/contracts/arg_builder.rb +17 -0
  16. data/lib/lighthouse/contracts/arg_manipulator.rb +17 -0
  17. data/lib/lighthouse/contracts/directive.rb +17 -0
  18. data/lib/lighthouse/contracts/field_manipulator.rb +17 -0
  19. data/lib/lighthouse/contracts/field_middleware.rb +16 -0
  20. data/lib/lighthouse/contracts/field_resolver.rb +18 -0
  21. data/lib/lighthouse/dataloader_sources/association_loader.rb +25 -0
  22. data/lib/lighthouse/dataloader_sources/model_loader.rb +19 -0
  23. data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +86 -0
  24. data/lib/lighthouse/directive_resolvers/all_resolver.rb +73 -0
  25. data/lib/lighthouse/directive_resolvers/auth_resolver.rb +44 -0
  26. data/lib/lighthouse/directive_resolvers/base.rb +92 -0
  27. data/lib/lighthouse/directive_resolvers/belongs_to_many_resolver.rb +56 -0
  28. data/lib/lighthouse/directive_resolvers/belongs_to_resolver.rb +47 -0
  29. data/lib/lighthouse/directive_resolvers/builder_applier.rb +63 -0
  30. data/lib/lighthouse/directive_resolvers/can_resolver.rb +86 -0
  31. data/lib/lighthouse/directive_resolvers/count_resolver.rb +56 -0
  32. data/lib/lighthouse/directive_resolvers/field_resolver.rb +78 -0
  33. data/lib/lighthouse/directive_resolvers/find_resolver.rb +65 -0
  34. data/lib/lighthouse/directive_resolvers/first_resolver.rb +53 -0
  35. data/lib/lighthouse/directive_resolvers/guard_resolver.rb +32 -0
  36. data/lib/lighthouse/directive_resolvers/has_many_resolver.rb +109 -0
  37. data/lib/lighthouse/directive_resolvers/has_one_resolver.rb +43 -0
  38. data/lib/lighthouse/directive_resolvers/method_resolver.rb +35 -0
  39. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +147 -0
  40. data/lib/lighthouse/directive_resolvers/rename_resolver.rb +35 -0
  41. data/lib/lighthouse/directive_resolvers/resolution.rb +35 -0
  42. data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +169 -0
  43. data/lib/lighthouse/directives/arguments/base.rb +40 -0
  44. data/lib/lighthouse/directives/arguments/filters.rb +101 -0
  45. data/lib/lighthouse/directives/arguments/order_by.rb +48 -0
  46. data/lib/lighthouse/directives/registry.rb +168 -0
  47. data/lib/lighthouse/directives/relation_directive.rb +332 -0
  48. data/lib/lighthouse/directives/where_conditions_directive.rb +208 -0
  49. data/lib/lighthouse/graphql/version.rb +7 -0
  50. data/lib/lighthouse/graphql.rb +8 -0
  51. data/lib/lighthouse/rb_lighthouse.rb +88 -0
  52. data/lib/lighthouse/reference_resolver.rb +24 -0
  53. data/lib/lighthouse/schema_factory.rb +46 -0
  54. data/lib/lighthouse/schema_generator.rb +244 -0
  55. data/lib/lighthouse/schema_implementation.rb +119 -0
  56. data/lib/lighthouse/sdl_loader.rb +27 -0
  57. data/lib/lighthouse/support/authorization.rb +26 -0
  58. data/lib/lighthouse/support/directive_args.rb +33 -0
  59. data/lib/lighthouse/support/model_resolver.rb +61 -0
  60. data/lib/lighthouse/support/naming.rb +71 -0
  61. data/lib/lighthouse/where_conditions/operator_map.rb +35 -0
  62. data/lib/lighthouse-graphql.rb +10 -0
  63. 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