lighthouse-graphql 0.1.0 → 0.2.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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +168 -1
  3. data/README.md +46 -2
  4. data/docs/authorization.md +116 -57
  5. data/docs/best-practices.md +16 -3
  6. data/docs/caching.md +161 -0
  7. data/docs/configuration.md +83 -4
  8. data/docs/custom-directives.md +5 -1
  9. data/docs/directives.md +117 -38
  10. data/docs/getting-started.md +84 -0
  11. data/docs/mutations.md +260 -0
  12. data/docs/performance.md +138 -0
  13. data/docs/relationships.md +80 -33
  14. data/docs/resolvers.md +179 -0
  15. data/docs/validation.md +132 -0
  16. data/lib/generators/lighthouse/builder_generator.rb +30 -0
  17. data/lib/generators/lighthouse/class_generator.rb +61 -0
  18. data/lib/generators/lighthouse/install/install_generator.rb +127 -0
  19. data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +8 -0
  20. data/lib/generators/lighthouse/install/templates/graphql_controller.rb.tt +57 -0
  21. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +67 -0
  22. data/lib/generators/lighthouse/install/templates/schema.graphql.tt +19 -0
  23. data/lib/generators/lighthouse/install/templates/schema.rb.tt +10 -0
  24. data/lib/generators/lighthouse/mutation_generator.rb +30 -0
  25. data/lib/generators/lighthouse/query_generator.rb +29 -0
  26. data/lib/generators/lighthouse/validator_generator.rb +36 -0
  27. data/lib/lighthouse/cache_control.rb +126 -0
  28. data/lib/lighthouse/caching.rb +68 -0
  29. data/lib/lighthouse/config.rb +85 -19
  30. data/lib/lighthouse/contracts/arg_builder.rb +8 -12
  31. data/lib/lighthouse/contracts/arg_manipulator.rb +8 -12
  32. data/lib/lighthouse/contracts/directive.rb +10 -14
  33. data/lib/lighthouse/contracts/field_manipulator.rb +8 -12
  34. data/lib/lighthouse/contracts/field_middleware.rb +7 -11
  35. data/lib/lighthouse/contracts/field_resolver.rb +9 -13
  36. data/lib/lighthouse/dataloader_sources/association_loader.rb +28 -19
  37. data/lib/lighthouse/dataloader_sources/model_loader.rb +10 -14
  38. data/lib/lighthouse/dataloader_sources/relation_count_loader.rb +16 -0
  39. data/lib/lighthouse/dataloader_sources/relation_page_loader.rb +18 -0
  40. data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +57 -69
  41. data/lib/lighthouse/directive_resolvers/all_resolver.rb +26 -57
  42. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +161 -0
  43. data/lib/lighthouse/directive_resolvers/auth_resolver.rb +41 -38
  44. data/lib/lighthouse/directive_resolvers/base.rb +142 -86
  45. data/lib/lighthouse/directive_resolvers/builder_applier.rb +58 -56
  46. data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +133 -0
  47. data/lib/lighthouse/directive_resolvers/can_resolvers.rb +177 -0
  48. data/lib/lighthouse/directive_resolvers/convention_resolver.rb +70 -0
  49. data/lib/lighthouse/directive_resolvers/count_resolver.rb +42 -48
  50. data/lib/lighthouse/directive_resolvers/field_resolver.rb +27 -68
  51. data/lib/lighthouse/directive_resolvers/find_resolver.rb +42 -52
  52. data/lib/lighthouse/directive_resolvers/first_resolver.rb +39 -45
  53. data/lib/lighthouse/directive_resolvers/guard_resolver.rb +16 -22
  54. data/lib/lighthouse/directive_resolvers/method_resolver.rb +19 -25
  55. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +219 -0
  56. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +70 -128
  57. data/lib/lighthouse/directive_resolvers/rename_resolver.rb +18 -24
  58. data/lib/lighthouse/directive_resolvers/resolution.rb +31 -27
  59. data/lib/lighthouse/directive_resolvers/validator_resolver.rb +193 -0
  60. data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +164 -163
  61. data/lib/lighthouse/directives/arguments/base.rb +23 -31
  62. data/lib/lighthouse/directives/arguments/filters.rb +113 -93
  63. data/lib/lighthouse/directives/arguments/order_by.rb +41 -41
  64. data/lib/lighthouse/directives/pagination_info_types.rb +69 -0
  65. data/lib/lighthouse/directives/pagination_manipulator.rb +176 -0
  66. data/lib/lighthouse/directives/registry.rb +210 -141
  67. data/lib/lighthouse/directives/where_conditions_directive.rb +236 -200
  68. data/lib/lighthouse/errors.rb +27 -0
  69. data/lib/lighthouse/federation_directives.rb +53 -0
  70. data/lib/lighthouse/field_resolver_stack.rb +56 -0
  71. data/lib/lighthouse/generated_resolvers.rb +82 -0
  72. data/lib/lighthouse/graphql/version.rb +1 -1
  73. data/lib/lighthouse/graphql_ruby_patches.rb +18 -0
  74. data/lib/lighthouse/ide_helper.rb +98 -0
  75. data/lib/lighthouse/namespaces.rb +21 -0
  76. data/lib/lighthouse/nested_mutations/belongs_to_operations.rb +50 -0
  77. data/lib/lighthouse/nested_mutations/executor.rb +149 -0
  78. data/lib/lighthouse/nested_mutations/has_operations.rb +66 -0
  79. data/lib/lighthouse/nested_mutations/many_to_many_operations.rb +44 -0
  80. data/lib/lighthouse/nested_mutations/partitioner.rb +125 -0
  81. data/lib/lighthouse/nested_mutations.rb +210 -0
  82. data/lib/lighthouse/pagination.rb +221 -0
  83. data/lib/lighthouse/persisted_queries.rb +167 -0
  84. data/lib/lighthouse/railtie.rb +26 -0
  85. data/lib/lighthouse/rb_lighthouse.rb +50 -29
  86. data/lib/lighthouse/reference_resolver.rb +13 -15
  87. data/lib/lighthouse/relation_batch.rb +156 -0
  88. data/lib/lighthouse/resolver_dispatch.rb +26 -0
  89. data/lib/lighthouse/schema_cache.rb +120 -0
  90. data/lib/lighthouse/schema_factory.rb +27 -37
  91. data/lib/lighthouse/schema_generator.rb +196 -239
  92. data/lib/lighthouse/schema_implementation.rb +51 -48
  93. data/lib/lighthouse/sdl_loader.rb +17 -20
  94. data/lib/lighthouse/support/arguments.rb +119 -0
  95. data/lib/lighthouse/support/authorization.rb +18 -19
  96. data/lib/lighthouse/support/batching.rb +16 -0
  97. data/lib/lighthouse/support/directive_args.rb +26 -24
  98. data/lib/lighthouse/support/model_resolver.rb +56 -56
  99. data/lib/lighthouse/support/naming.rb +55 -59
  100. data/lib/lighthouse/tasks/lighthouse.rake +33 -0
  101. data/lib/lighthouse/where_conditions/operator_map.rb +25 -29
  102. metadata +52 -64
  103. data/lib/lighthouse/directive_resolvers/belongs_to_many_resolver.rb +0 -56
  104. data/lib/lighthouse/directive_resolvers/belongs_to_resolver.rb +0 -47
  105. data/lib/lighthouse/directive_resolvers/can_resolver.rb +0 -86
  106. data/lib/lighthouse/directive_resolvers/has_many_resolver.rb +0 -109
  107. data/lib/lighthouse/directive_resolvers/has_one_resolver.rb +0 -43
  108. data/lib/lighthouse/directives/relation_directive.rb +0 -332
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ # Resolves a Query or Mutation field that has no resolving directive by
6
+ # convention, as PHP Lighthouse does: `Query.latestPosts` resolves to
7
+ # `Queries::LatestPosts` (a GraphQL::Schema::Resolver), `Mutation.publishPost`
8
+ # to `Mutations::PublishPost` (a GraphQL::Schema::Mutation), looked up in
9
+ # Config.namespaces[:queries] / [:mutations] at call time so code reloading
10
+ # keeps working. Not a directive: SchemaGenerator installs it.
11
+ class Lighthouse::GraphQL::DirectiveResolvers::ConventionResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
12
+ def initialize(field_def, type_def, kind)
13
+ super(field_def, type_def, nil)
14
+ @kind = kind
15
+ end
16
+
17
+ def define_resolver(target_type_class)
18
+ me = self
19
+ inner = capture_inner(target_type_class)
20
+
21
+ install(target_type_class, lambda do |obj, args, ctx|
22
+ me.send(:resolve_by_convention, inner, obj, args, ctx)
23
+ end)
24
+ end
25
+
26
+ ROOT_KINDS = { 'Query' => :queries, 'Mutation' => :mutations }.freeze
27
+
28
+ # :queries / :mutations for the conventional root type names. The
29
+ # SchemaGenerator passes the schema's actual roots as `root_kind:`.
30
+ def self.root_kind_of(type)
31
+ ROOT_KINDS[type.respond_to?(:graphql_name) ? type.graphql_name : type.to_s]
32
+ end
33
+
34
+ private
35
+
36
+ def resolve_by_convention(inner, obj, args, ctx)
37
+ namespaces = Lighthouse::Config.namespaces[@kind]
38
+ klass = Lighthouse::Support::ModelResolver.constantize(class_name, namespaces: namespaces, fallback: false)
39
+ return missing(namespaces, inner, obj, args, ctx) unless klass
40
+
41
+ unless Lighthouse::GraphQL::ResolverDispatch.graphql_resolver?(klass)
42
+ raise Lighthouse::DefinitionError,
43
+ "#{klass} resolves #{location}, so it must inherit from GraphQL::Schema::Resolver " \
44
+ '(or GraphQL::Schema::Mutation).'
45
+ end
46
+
47
+ Lighthouse::GraphQL::ResolverDispatch.call(klass, @field_def, obj, args, ctx)
48
+ end
49
+
50
+ # No class: the root object may still answer (a custom root_value);
51
+ # otherwise report the misconfiguration through Config.on_error.
52
+ def missing(namespaces, inner, obj, args, ctx)
53
+ return inner.call(obj, args, ctx) if obj && !obj.is_a?(Hash) && obj.respond_to?(@field_def.name)
54
+
55
+ expected = Array(namespaces).map { |ns| "#{ns}::#{class_name}" }.join(' or ')
56
+ Lighthouse::Config.handle_error(
57
+ Lighthouse::DefinitionError.new("#{location} has no resolver: define #{expected}, or add a resolving directive."),
58
+ ctx
59
+ )
60
+ nil
61
+ end
62
+
63
+ def class_name
64
+ @class_name ||= @field_def.name.to_s.camelize
65
+ end
66
+
67
+ def location
68
+ @location ||= "#{@type_def.graphql_name}.#{@field_def.name}"
69
+ end
70
+ end
@@ -3,54 +3,48 @@
3
3
  require_relative 'base'
4
4
  require_relative 'builder_applier'
5
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
6
+ # @count(model: "Model", relation: "assoc") — return the number of records.
7
+ # Counts `Model.all` when `model:` is given, otherwise the named relation on
8
+ # the parent object (or a relation matching the field name). Honors `scopes:`, @builder
9
+ # and where/argument filters before counting.
10
+ class Lighthouse::GraphQL::DirectiveResolvers::CountDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
11
+ def self.directive_name = 'count'
12
+ def self.definition = 'directive @count(model: String, relation: String, scopes: [String!]) on FIELD_DEFINITION'
13
+
14
+ def define_resolver(target_type_class)
15
+ model_name = directive_arg('model')
16
+ relation_name = directive_arg('relation')
17
+ me = self
18
+
19
+ install(target_type_class, lambda do |obj, arguments, ctx|
20
+ me.send(:count_records, me.send(:countable, obj, model_name, relation_name), arguments, ctx)
21
+ end)
22
+ end
23
+
24
+ private
25
+
26
+ def directive_arg(name)
27
+ Lighthouse::Support::DirectiveArgs.literal(@directive_node, name)
28
+ end
29
+
30
+ def countable(obj, model_name, relation_name)
31
+ field_name = @field_def.name
32
+ if model_name
33
+ constantize_model(model_name)&.all
34
+ elsif relation_name && obj.respond_to?(relation_name)
35
+ obj.public_send(relation_name)
36
+ elsif obj.respond_to?(field_name)
37
+ obj.public_send(field_name)
54
38
  end
55
39
  end
40
+
41
+ def count_records(relation, arguments, ctx)
42
+ return 0 unless relation.respond_to?(:count)
43
+
44
+ args_h = arguments.respond_to?(:to_h) ? arguments.to_h : {}
45
+ relation = apply_scopes(relation)
46
+ relation = Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier.apply(relation, @field_def.ast_node, args_h, ctx, self)
47
+ relation = apply_where_filters(relation, args_h)
48
+ relation.count
49
+ end
56
50
  end
@@ -2,77 +2,36 @@
2
2
 
3
3
  require_relative 'base'
4
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
5
+ class Lighthouse::GraphQL::DirectiveResolvers::FieldDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
6
+ def self.directive_name = 'field'
7
+ def self.definition = 'directive @field(resolver: String, class: String) on FIELD_DEFINITION'
8
+
9
+ # On Query and Mutation the class is looked up in the queries / mutations
10
+ # namespaces; on any other type, in Config.resolver_namespaces.
11
+ def define_resolver(target_type_class)
12
+ field_def = @field_def
13
+ class_name = extract_directive_arg('resolver') || extract_directive_arg('class')
14
+ kind = root_kind(target_type_class)
15
+
16
+ install(target_type_class, lambda do |obj, args, ctx|
17
+ namespaces = kind ? Lighthouse::Config.namespaces[kind] : Lighthouse::Config.resolver_namespaces
18
+ klass = Lighthouse::Support::ModelResolver.constantize(class_name, namespaces: namespaces)
19
+ next nil unless klass
20
+
21
+ Lighthouse::GraphQL::ResolverDispatch.call(klass, field_def, obj, args, ctx)
22
+ end)
23
+ end
46
24
 
47
- private
25
+ private
48
26
 
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
27
+ def extract_directive_arg(name)
28
+ arg = @directive_node.arguments&.find { |a| a.name == name }
29
+ return nil unless arg
54
30
 
55
- def execute_resolver(resolver_class, obj, args, ctx)
56
- return nil unless resolver_class
31
+ arg.value.respond_to?(:value) ? arg.value.value : arg.value
32
+ end
57
33
 
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
34
+ def root_kind(target_type_class)
35
+ @opts[:root_kind] || Lighthouse::GraphQL::DirectiveResolvers::ConventionResolver.root_kind_of(target_type_class)
77
36
  end
78
37
  end
@@ -2,64 +2,54 @@
2
2
 
3
3
  require_relative 'base'
4
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
5
+ # @find — the single record the field's argument filters select:
6
+ #
7
+ # user(id: ID! @eq): User @find
8
+ # userByEmail(email: String! @eq): User @find
9
+ #
10
+ # A lookup by `id` alone is batched through the Dataloader, and an `id`
11
+ # argument without a filter directive selects by primary key. More than one
12
+ # match is an error, as in PHP Lighthouse; none returns null.
13
+ class Lighthouse::GraphQL::DirectiveResolvers::FindDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
14
+ def self.directive_name = 'find'
15
+ def self.definition = 'directive @find(model: String, scopes: [String!]) on FIELD_DEFINITION'
16
+
17
+ def define_resolver(target_type_class)
18
+ me = self
19
+ install(target_type_class, lambda do |_obj, args, ctx|
20
+ me.send(:find, Lighthouse::Support::Arguments.shallow(args), ctx)
21
+ end)
22
+ end
22
23
 
23
- if id && ctx&.dataloader
24
- loader = ctx.dataloader.with(Lighthouse::GraphQL::DataloaderSources::ModelLoader, model_class)
25
- return loader.load(id)
26
- end
24
+ protected
27
25
 
28
- model_class.find_by(id: id)
29
- end
26
+ def find(args, ctx)
27
+ model = model_class
28
+ dataloader = Lighthouse::Support::Batching.dataloader(ctx)
29
+ return dataloader.with(Lighthouse::GraphQL::DataloaderSources::ModelLoader, model).load(args[:id]) if dataloader && batchable?(args)
30
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']
31
+ records = find_relation(model, args).limit(2).to_a
32
+ raise ::GraphQL::ExecutionError, 'The query returned more than one result.' if records.size > 1
35
33
 
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
34
+ records.first
35
+ end
48
36
 
49
- private
37
+ # Only a lookup by `id` alone, with no scopes to apply, can be batched.
38
+ def batchable?(args)
39
+ args.keys == [:id] && !directive_arg('scopes')
40
+ end
50
41
 
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
42
+ def find_relation(model, args)
43
+ relation = apply_scopes(model.all)
44
+ relation = relation.where(model.primary_key => args[:id]) if args.key?(:id) && !filtered?('id')
45
+ apply_where_filters(relation, args)
46
+ end
56
47
 
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
48
+ # Does the argument carry a filter directive (@eq, @where, ...)? An `id`
49
+ # without one still selects by primary key.
50
+ def filtered?(name)
51
+ argument = (@field_def.ast_node&.arguments || []).find { |a| a.name == name }
52
+ registry = Lighthouse::DirectiveRegistry.default
53
+ argument&.directives&.any? { |d| registry.arg_builder_class(d.name) }
64
54
  end
65
55
  end
@@ -3,51 +3,45 @@
3
3
  require_relative 'base'
4
4
  require_relative 'builder_applier'
5
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
6
+ # @first(model: "Model") — return a single record (the first match). Like
7
+ # @all but `.first`, honoring `scopes:`, @builder and where/argument filters so it can
8
+ # be combined with @eq/@where to fetch one record by criteria.
9
+ class Lighthouse::GraphQL::DirectiveResolvers::FirstDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
10
+ def self.directive_name = 'first'
11
+ def self.definition = 'directive @first(model: String, scopes: [String!]) on FIELD_DEFINITION'
12
+
13
+ def define_resolver(target_type_class)
14
+ method_name = "resolve_field_#{@field_def.name}"
15
+ model_name_static = directive_arg('model') || field_type_name
16
+ resolver_instance = self
17
+ field_ast_node = @field_def.ast_node
18
+
19
+ resolver = lambda do |_obj, arguments, ctx|
20
+ model_class = resolver_instance.constantize_model(model_name_static)
21
+ next nil unless model_class
22
+
23
+ args_h = arguments.respond_to?(:to_h) ? arguments.to_h : {}
24
+ relation = resolver_instance.apply_scopes(model_class.all)
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)
27
+ relation.first
51
28
  end
29
+
30
+ target_type_class.define_singleton_method(method_name, &resolver)
31
+ @field_def.resolve_proc = resolver if @field_def.respond_to?(:resolve_proc=)
32
+ end
33
+
34
+ private
35
+
36
+ def directive_arg(name)
37
+ Lighthouse::Support::DirectiveArgs.literal(@directive_node, name)
38
+ end
39
+
40
+ def field_type_name
41
+ unwrapped = @field_def.type.respond_to?(:unwrap) ? @field_def.type.unwrap : @field_def.type
42
+ return unwrapped.graphql_name if unwrapped.respond_to?(:graphql_name) && unwrapped.graphql_name
43
+ return unwrapped.name if unwrapped.respond_to?(:name) && unwrapped.name
44
+
45
+ @field_def.type.to_type_signature.to_s.gsub(/[!\[\]]/, '')
52
46
  end
53
47
  end
@@ -2,31 +2,25 @@
2
2
 
3
3
  require_relative 'base'
4
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'
5
+ # @guard — require an authenticated user before the field resolves.
6
+ # Wraps the field's resolver; raises "Unauthenticated." when no user is
7
+ # present (per Config.authenticated_user). Mirrors PHP Lighthouse's @guard.
8
+ class Lighthouse::GraphQL::DirectiveResolvers::GuardDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
9
+ def self.directive_name = 'guard'
10
+ def self.definition = 'directive @guard on FIELD_DEFINITION'
14
11
 
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)
12
+ def define_resolver(target_type_class)
13
+ field_name = @field_def.name
14
+ method_name = "resolve_field_#{field_name}"
15
+ inner = Lighthouse::GraphQL::DirectiveResolvers::Resolution.capture(target_type_class, method_name, @field_def, field_name)
19
16
 
20
- wrapper = lambda do |obj, arguments, ctx|
21
- raise(::GraphQL::ExecutionError, 'Unauthenticated.') unless Lighthouse::Support::Authorization.authenticated?(ctx)
17
+ wrapper = lambda do |obj, arguments, ctx|
18
+ raise(::GraphQL::ExecutionError, 'Unauthenticated.') unless Lighthouse::Support::Authorization.authenticated?(ctx)
22
19
 
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
20
+ inner.call(obj, arguments, ctx)
30
21
  end
22
+
23
+ target_type_class.define_singleton_method(method_name, &wrapper)
24
+ @field_def.resolve_proc = wrapper if @field_def.respond_to?(:resolve_proc=)
31
25
  end
32
26
  end
@@ -2,34 +2,28 @@
2
2
 
3
3
  require_relative 'base'
4
4
 
5
- module Lighthouse
6
- module GraphQL
7
- module DirectiveResolvers
8
- # @method(name: "someMethod") — resolve the field by calling a method on the
9
- # parent object. Mirrors PHP Lighthouse's @method. Defaults to the field's
10
- # own (snake_cased) name when `name` is omitted.
11
- #
12
- # labels: [String!]! @method(name: "label_list")
13
- # canReply: Boolean @method(name: "can_reply?")
14
- class MethodDirectiveResolver < BaseDirectiveResolver
15
- def self.directive_name = 'method'
16
- def self.definition = 'directive @method(name: String) on FIELD_DEFINITION'
5
+ # @method(name: "someMethod") — resolve the field by calling a method on the
6
+ # parent object. Mirrors PHP Lighthouse's @method. Defaults to the field's
7
+ # own (snake_cased) name when `name` is omitted.
8
+ #
9
+ # labels: [String!]! @method(name: "label_list")
10
+ # canReply: Boolean @method(name: "can_reply?")
11
+ class Lighthouse::GraphQL::DirectiveResolvers::MethodDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
12
+ def self.directive_name = 'method'
13
+ def self.definition = 'directive @method(name: String) on FIELD_DEFINITION'
17
14
 
18
- def define_resolver(target_type_class)
19
- resolver_method = "resolve_field_#{@field_def.name}"
20
- target_method = (Lighthouse::Support::DirectiveArgs.literal(@directive_node, 'name') ||
21
- Lighthouse::Support::Naming.attribute(@field_def.name)).to_s
15
+ def define_resolver(target_type_class)
16
+ resolver_method = "resolve_field_#{@field_def.name}"
17
+ target_method = (Lighthouse::Support::DirectiveArgs.literal(@directive_node, 'name') ||
18
+ Lighthouse::Support::Naming.attribute(@field_def.name)).to_s
22
19
 
23
- resolver = lambda do |obj, _arguments, _ctx|
24
- next nil if obj.nil?
20
+ resolver = lambda do |obj, _arguments, _ctx|
21
+ next nil if obj.nil?
25
22
 
26
- obj.respond_to?(target_method) ? obj.public_send(target_method) : nil
27
- end
28
-
29
- target_type_class.define_singleton_method(resolver_method, &resolver)
30
- @field_def.resolve_proc = resolver if @field_def.respond_to?(:resolve_proc=)
31
- end
32
- end
23
+ obj.respond_to?(target_method) ? obj.public_send(target_method) : nil
33
24
  end
25
+
26
+ target_type_class.define_singleton_method(resolver_method, &resolver)
27
+ @field_def.resolve_proc = resolver if @field_def.respond_to?(:resolve_proc=)
34
28
  end
35
29
  end