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
@@ -1,19 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Lighthouse
4
- module GraphQL
5
- module DataloaderSources
6
- class ModelLoader < ::GraphQL::Dataloader::Source
7
- def initialize(model_class)
8
- @model_class = model_class
9
- end
3
+ class Lighthouse::GraphQL::DataloaderSources::ModelLoader < GraphQL::Dataloader::Source
4
+ def initialize(model_class)
5
+ super()
6
+ @model_class = model_class
7
+ end
10
8
 
11
- # keys are ids
12
- def fetch(keys)
13
- records = @model_class.where(id: keys).index_by(&:id)
14
- keys.map { |k| records[k] }
15
- end
16
- end
17
- end
9
+ # Keys are ids. GraphQL `ID` arguments arrive as strings while records
10
+ # carry their column type (usually Integer), so match on the string form.
11
+ def fetch(keys)
12
+ records = @model_class.where(id: keys).index_by { |record| record.id.to_s }
13
+ keys.map { |k| records[k.to_s] }
18
14
  end
19
15
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The total of a paginated relation for every parent the Dataloader collected,
4
+ # in one GROUP BY query. Only asked for when the client selects a total.
5
+ class Lighthouse::GraphQL::DataloaderSources::RelationCountLoader < GraphQL::Dataloader::Source
6
+ def initialize(batch)
7
+ super()
8
+ @batch = batch
9
+ end
10
+
11
+ # keys: the parent records
12
+ def fetch(owners)
13
+ counts = @batch.counts(owners)
14
+ owners.map { |owner| counts.fetch(@batch.owner_key(owner), 0) }
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # One page of a paginated relation for every parent the Dataloader collected,
4
+ # in one query (see Lighthouse::GraphQL::RelationBatch).
5
+ class Lighthouse::GraphQL::DataloaderSources::RelationPageLoader < GraphQL::Dataloader::Source
6
+ def initialize(batch, offset, limit)
7
+ super()
8
+ @batch = batch
9
+ @offset = offset
10
+ @limit = limit
11
+ end
12
+
13
+ # keys: the parent records
14
+ def fetch(owners)
15
+ pages = @batch.pages(owners, @offset, @limit)
16
+ owners.map { |owner| pages.fetch(@batch.owner_key(owner), []) }
17
+ end
18
+ end
@@ -3,84 +3,72 @@
3
3
  require_relative 'base'
4
4
  require_relative 'builder_applier'
5
5
 
6
- module Lighthouse
7
- module GraphQL
8
- module DirectiveResolvers
9
- class AggregateDirectiveResolver < BaseDirectiveResolver
10
- def define_resolver(target_type_class)
11
- field_name = @field_def.name
12
- method_name = "resolve_field_#{field_name}"
6
+ class Lighthouse::GraphQL::DirectiveResolvers::AggregateDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
7
+ def self.directive_name = 'aggregate'
13
8
 
14
- model_name = extract_directive_arg('model')
15
- relation_name = extract_directive_arg('relation')
16
- column_name = extract_directive_arg('column')
17
- function_name = extract_directive_arg('function')
18
- resolver_instance = self
19
- field_ast_node = @field_def.ast_node
9
+ # An unknown function aggregates to null.
10
+ AGGREGATIONS = {
11
+ 'SUM' => ->(relation, column) { relation.sum(column) },
12
+ 'AVG' => ->(relation, column) { relation.average(column)&.to_f },
13
+ 'MIN' => ->(relation, column) { relation.minimum(column) },
14
+ 'MAX' => ->(relation, column) { relation.maximum(column) },
15
+ 'COUNT' => ->(relation, _column) { relation.count }
16
+ }.freeze
20
17
 
21
- resolver_lambda = lambda do |obj, args, ctx|
22
- relation = if model_name
23
- model_class = resolver_instance.send(:constantize_model, model_name)
24
- model_class ? model_class.all : nil
25
- elsif relation_name && obj.respond_to?(relation_name)
26
- obj.public_send(relation_name)
27
- else
28
- nil
29
- end
18
+ def self.definition
19
+ 'directive @aggregate(function: String!, column: String, model: String, relation: String) on FIELD_DEFINITION'
20
+ end
30
21
 
31
- return nil unless relation
22
+ def define_resolver(target_type_class)
23
+ model_name = extract_directive_arg('model')
24
+ relation_name = extract_directive_arg('relation')
25
+ column_name = extract_directive_arg('column')
26
+ function_name = extract_directive_arg('function')
27
+ resolver_instance = self
28
+ field_ast_node = @field_def.ast_node
32
29
 
33
- relation = DirectiveResolvers::BuilderApplier.apply(relation, field_ast_node, args, ctx, resolver_instance)
34
- resolver_instance.send(:compute_aggregation, relation, column_name, function_name)
35
- end
30
+ install(target_type_class, lambda do |obj, args, ctx|
31
+ relation = resolver_instance.send(:aggregate_source, obj, model_name, relation_name)
32
+ return nil unless relation
36
33
 
37
- target_type_class.define_singleton_method(method_name, &resolver_lambda)
38
- @field_def.resolve_proc = resolver_lambda if @field_def.respond_to?(:resolve_proc=)
39
- end
34
+ relation = Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier.apply(relation, field_ast_node, args, ctx, resolver_instance)
35
+ resolver_instance.send(:compute_aggregation, relation, column_name, function_name)
36
+ end)
37
+ end
40
38
 
41
- private
39
+ private
42
40
 
43
- def extract_directive_arg(name)
44
- arg = @directive_node.arguments&.find { |a| a.name == name }
45
- return nil unless arg
46
- v = arg.value.respond_to?(:value) ? arg.value.value : arg.value
47
- v.is_a?(::GraphQL::Language::Nodes::Enum) ? v.name : v
48
- end
41
+ def extract_directive_arg(name)
42
+ arg = @directive_node.arguments&.find { |a| a.name == name }
43
+ return nil unless arg
49
44
 
50
- def compute_aggregation(relation, column, function_name)
51
- return nil unless relation.respond_to?(:any?)
52
- column = column.to_s
53
- case function_name.to_s.upcase
54
- when 'SUM'
55
- relation.sum(column)
56
- when 'AVG'
57
- relation.average(column)&.to_f
58
- when 'MIN'
59
- relation.minimum(column)
60
- when 'MAX'
61
- relation.maximum(column)
62
- when 'COUNT'
63
- relation.count
64
- else
65
- nil
66
- end
67
- rescue StandardError
68
- nil
69
- end
45
+ v = arg.value.respond_to?(:value) ? arg.value.value : arg.value
46
+ v.is_a?(::GraphQL::Language::Nodes::Enum) ? v.name : v
47
+ end
70
48
 
71
- def set_resolve_proc(method_name)
72
- if @field_def.respond_to?(:resolve_proc=)
73
- @field_def.resolve_proc = ->(obj, arguments, ctx) do
74
- target_type_class = self.class
75
- if target_type_class.respond_to?(method_name)
76
- target_type_class.public_send(method_name, obj, arguments, ctx)
77
- else
78
- nil
79
- end
80
- end
81
- end
82
- end
83
- end
49
+ # The model's table when `model:` is given, else the parent's relation.
50
+ def aggregate_source(obj, model_name, relation_name)
51
+ if model_name
52
+ constantize_model(model_name)&.all
53
+ elsif relation_name && obj.respond_to?(relation_name)
54
+ obj.public_send(relation_name)
84
55
  end
85
56
  end
57
+
58
+ def compute_aggregation(relation, column, function_name)
59
+ return nil unless relation.respond_to?(:any?)
60
+
61
+ AGGREGATIONS[function_name.to_s.upcase]&.call(relation, column.to_s)
62
+ rescue StandardError
63
+ nil
64
+ end
65
+
66
+ def install_resolve_proc(method_name)
67
+ return unless @field_def.respond_to?(:resolve_proc=)
68
+
69
+ @field_def.resolve_proc = lambda { |obj, arguments, ctx|
70
+ target_type_class = self.class
71
+ target_type_class.public_send(method_name, obj, arguments, ctx) if target_type_class.respond_to?(method_name)
72
+ }
73
+ end
86
74
  end
@@ -3,71 +3,40 @@
3
3
  require_relative 'base'
4
4
  require_relative 'builder_applier'
5
5
 
6
- module Lighthouse
7
- module GraphQL
8
- module DirectiveResolvers
9
- class AllDirectiveResolver < BaseDirectiveResolver
10
- def define_resolver(target_type_class)
11
- field_name = @field_def.name
6
+ class Lighthouse::GraphQL::DirectiveResolvers::AllDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
7
+ def self.directive_name = 'all'
8
+ def self.definition = 'directive @all(model: String, scopes: [String!]) on FIELD_DEFINITION'
12
9
 
13
- resolver_instance = self
14
- field_ast_node = @field_def.ast_node
10
+ def define_resolver(target_type_class)
11
+ model_class = constantize_model(extract_model_name || @field_def.type.unwrap.graphql_name)
12
+ me = self
15
13
 
16
- method_name = "resolve_field_#{field_name}"
17
-
18
- model_name_static = extract_model_name || @field_def.type.unwrap.name
19
- model_class_static = constantize_model(model_name_static)
20
-
21
- target_type_class.define_singleton_method(method_name) do |_obj, arguments, ctx|
22
- model_class = model_class_static
23
- return [] unless model_class
24
-
25
- # apply simple pagination args if present
26
- args = arguments.respond_to?(:to_h) ? arguments.to_h : {}
27
- relation = model_class.all
28
-
29
- relation = DirectiveResolvers::BuilderApplier.apply(relation, field_ast_node, args, ctx, resolver_instance)
14
+ install(target_type_class, lambda do |_obj, arguments, ctx|
15
+ me.send(:resolve_all, model_class, arguments, ctx)
16
+ end)
17
+ end
30
18
 
31
- relation = resolver_instance.apply_where_filters(relation, args)
32
- relation = relation.limit(args[:first]) if args[:first]
33
- relation = relation.offset(args[:offset]) if args[:offset]
34
- relation
35
- end
19
+ private
36
20
 
37
- if @field_def.respond_to?(:resolve_proc=)
38
- @field_def.resolve_proc = ->(_obj, arguments, ctx) do
39
- model_class = model_class_static
40
- if model_class.nil?
41
- []
42
- else
43
- args = arguments.respond_to?(:to_h) ? arguments.to_h : {}
44
- relation = model_class.all
21
+ def resolve_all(model_class, arguments, ctx)
22
+ return [] unless model_class
45
23
 
46
- relation = DirectiveResolvers::BuilderApplier.apply(relation, field_ast_node, args, ctx, resolver_instance)
24
+ # apply simple pagination args if present
25
+ args = arguments.respond_to?(:to_h) ? arguments.to_h : {}
26
+ relation = apply_scopes(model_class.all)
47
27
 
48
- relation = resolver_instance.apply_where_filters(relation, args)
49
- relation = relation.limit(args[:first]) if args[:first]
50
- relation = relation.offset(args[:offset]) if args[:offset]
51
- relation
52
- end
53
- end
54
- end
55
- end
28
+ relation = Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier.apply(relation, @field_def.ast_node, args, ctx, self)
56
29
 
57
- private
30
+ relation = apply_where_filters(relation, args)
31
+ relation = relation.limit(args[:first]) if args[:first]
32
+ relation = relation.offset(args[:offset]) if args[:offset]
33
+ relation
34
+ end
58
35
 
59
- def extract_model_name
60
- arg = @directive_node.arguments&.find { |a| a.name == 'model' }
61
- return nil unless arg
62
- arg.value.respond_to?(:value) ? arg.value.value : arg.value
63
- end
36
+ def extract_model_name
37
+ arg = @directive_node.arguments&.find { |a| a.name == 'model' }
38
+ return nil unless arg
64
39
 
65
- def constantize_model(name)
66
- return nil unless name
67
- name = name.to_s.split('::').last
68
- Object.const_get(name) rescue nil
69
- end
70
- end
71
- end
40
+ arg.value.respond_to?(:value) ? arg.value.value : arg.value
72
41
  end
73
42
  end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Lighthouse::GraphQL::DirectiveResolvers
6
+ # Relationship directives: resolve an ActiveRecord association of the
7
+ # parent. `relation:` names it (default: the field name in snake_case);
8
+ # `scopes:` applies model scopes. The Morph* directives are PHP Lighthouse's
9
+ # names for polymorphic associations, which ActiveRecord resolves the same way.
10
+ #
11
+ # Without query constraints the association is batch-loaded through the
12
+ # Dataloader (one query per association per batch). Collection directives
13
+ # take PHP Lighthouse's `type:` — PAGINATOR, SIMPLE or CONNECTION — to
14
+ # paginate (see Directives::PaginationManipulator), and the field's argument
15
+ # filters (@eq, @where, @orderBy, ...) apply to the association's query.
16
+ # Pages are batched too: every parent's page in one query, and the totals
17
+ # in another (see RelationBatch).
18
+ class AssociationDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
19
+ CONDITION_DIRECTIVES = %w[whereConditions whereHasConditions].freeze
20
+
21
+ def self.collection? = false
22
+
23
+ def define_resolver(target_type_class)
24
+ me = self
25
+ install(target_type_class, lambda do |obj, args, ctx|
26
+ next nil if obj.nil?
27
+
28
+ me.send(:resolve_association, obj, Lighthouse::Support::Arguments.shallow(args), ctx)
29
+ end)
30
+ end
31
+
32
+ protected
33
+
34
+ def resolve_association(obj, args, ctx)
35
+ name = association_name
36
+ return nil unless obj.respond_to?(name)
37
+
38
+ return paginate(obj, name, args, ctx) if pagination_type
39
+ return constrained(association_scope(obj, name), args) if constrained?(args)
40
+
41
+ dataloader = Lighthouse::Support::Batching.dataloader(ctx)
42
+ return obj.public_send(name) unless dataloader && obj.class.respond_to?(:reflect_on_association)
43
+
44
+ dataloader.with(Lighthouse::GraphQL::DataloaderSources::AssociationLoader, obj.class, name).load(obj)
45
+ end
46
+
47
+ def association_name
48
+ (directive_arg('relation') || Lighthouse::Support::Naming.attribute(@field_def.name)).to_s
49
+ end
50
+
51
+ def pagination_type
52
+ self.class.collection? && directive_arg('type')&.to_s&.upcase
53
+ end
54
+
55
+ # Scopes or a supplied argument with a filter directive need a query of
56
+ # their own instead of the batched load.
57
+ def constrained?(args)
58
+ return true if directive_arg('scopes')
59
+
60
+ registry = Lighthouse::DirectiveRegistry.default
61
+ (@field_def.ast_node&.arguments || []).any? do |argument|
62
+ args.key?(argument.name.underscore.to_sym) && argument.directives.any? { |d| filter_directive?(registry, d) }
63
+ end
64
+ end
65
+
66
+ # An argument builder (@eq, @where, ...) or a where-conditions filter.
67
+ def filter_directive?(registry, directive)
68
+ registry.arg_builder_class(directive.name) || CONDITION_DIRECTIVES.include?(directive.name)
69
+ end
70
+
71
+ def constrained(relation, args)
72
+ relation = apply_where_filters(scoped(relation), args)
73
+ self.class.collection? ? relation : relation.first
74
+ end
75
+
76
+ def paginate(obj, name, args, ctx)
77
+ limits = { default_count: directive_arg('defaultCount') }
78
+ limits[:max_count] = directive_arg('maxCount') || :unlimited if directive_given?('maxCount')
79
+ Lighthouse::GraphQL::Pagination.paginate(paginated_list(obj, name, args, ctx), pagination_type, args, limits)
80
+ end
81
+
82
+ # This parent's page, batched with its siblings' when the Dataloader can.
83
+ def paginated_list(obj, name, args, ctx)
84
+ dataloader = Lighthouse::Support::Batching.dataloader(ctx)
85
+ batch = dataloader && Lighthouse::GraphQL::RelationBatch.for(obj, name, args, self, ctx)
86
+ return Lighthouse::GraphQL::Pagination::BatchedList.new(batch, obj, dataloader) if batch
87
+
88
+ decorate_relation(association_scope(obj, name), args, ctx)
89
+ end
90
+
91
+ public
92
+
93
+ # The field's scopes and filters on the association's query, for one
94
+ # parent or a RelationBatch of them.
95
+ def decorate_relation(relation, args, _ctx)
96
+ apply_where_filters(scoped(relation), args)
97
+ end
98
+
99
+ protected
100
+
101
+ # The association as a query (for a singular association too), so scopes
102
+ # and filters can be applied to it.
103
+ def association_scope(obj, name)
104
+ obj.respond_to?(:association) ? obj.association(name.to_sym).scope : obj.public_send(name)
105
+ end
106
+
107
+ def scoped(relation)
108
+ Array(directive_arg('scopes')).reduce(relation) { |acc, scope| acc.public_send(scope) }
109
+ end
110
+
111
+ def directive_given?(name)
112
+ @directive_node&.arguments&.any? { |a| a.name == name }
113
+ end
114
+ end
115
+
116
+ COLLECTION_ARGUMENTS = 'relation: String, scopes: [String!], type: PaginateType, defaultCount: Int, maxCount: Int'
117
+
118
+ class HasManyDirectiveResolver < AssociationDirectiveResolver
119
+ def self.collection? = true
120
+ def self.directive_name = 'hasMany'
121
+ def self.definition = "directive @hasMany(#{COLLECTION_ARGUMENTS}) on FIELD_DEFINITION"
122
+ end
123
+
124
+ class BelongsToManyDirectiveResolver < AssociationDirectiveResolver
125
+ def self.collection? = true
126
+ def self.directive_name = 'belongsToMany'
127
+ def self.definition = "directive @belongsToMany(#{COLLECTION_ARGUMENTS}) on FIELD_DEFINITION"
128
+ end
129
+
130
+ class MorphManyDirectiveResolver < AssociationDirectiveResolver
131
+ def self.collection? = true
132
+ def self.directive_name = 'morphMany'
133
+ def self.definition = "directive @morphMany(#{COLLECTION_ARGUMENTS}) on FIELD_DEFINITION"
134
+ end
135
+
136
+ class MorphToManyDirectiveResolver < AssociationDirectiveResolver
137
+ def self.collection? = true
138
+ def self.directive_name = 'morphToMany'
139
+ def self.definition = "directive @morphToMany(#{COLLECTION_ARGUMENTS}) on FIELD_DEFINITION"
140
+ end
141
+
142
+ class HasOneDirectiveResolver < AssociationDirectiveResolver
143
+ def self.directive_name = 'hasOne'
144
+ def self.definition = 'directive @hasOne(relation: String, scopes: [String!]) on FIELD_DEFINITION'
145
+ end
146
+
147
+ class MorphOneDirectiveResolver < AssociationDirectiveResolver
148
+ def self.directive_name = 'morphOne'
149
+ def self.definition = 'directive @morphOne(relation: String, scopes: [String!]) on FIELD_DEFINITION'
150
+ end
151
+
152
+ class BelongsToDirectiveResolver < AssociationDirectiveResolver
153
+ def self.directive_name = 'belongsTo'
154
+ def self.definition = 'directive @belongsTo(relation: String, scopes: [String!]) on FIELD_DEFINITION'
155
+ end
156
+
157
+ class MorphToDirectiveResolver < AssociationDirectiveResolver
158
+ def self.directive_name = 'morphTo'
159
+ def self.definition = 'directive @morphTo(relation: String, scopes: [String!]) on FIELD_DEFINITION'
160
+ end
161
+ end
@@ -1,44 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Lighthouse
4
- module GraphQL
5
- module DirectiveResolvers
6
- class AuthDirectiveResolver < BaseDirectiveResolver
7
- def define_resolver(target_type_class)
8
- field_name = @field_def.name
9
- method_name = "resolve_field_#{field_name}"
10
-
11
- original_singleton =
12
- if target_type_class.respond_to?(method_name)
13
- target_type_class.method(method_name)
14
- end
15
-
16
- original_proc =
17
- if @field_def.respond_to?(:resolve_proc)
18
- @field_def.resolve_proc
19
- end
20
-
21
- wrapper = lambda do |obj, arguments, ctx|
22
- user = ctx && (ctx[:current_user] || ctx['current_user'])
23
- raise(::GraphQL::ExecutionError, 'Unauthenticated.') unless user
24
-
25
- if original_singleton
26
- original_singleton.call(obj, arguments, ctx)
27
- elsif original_proc
28
- original_proc.call(obj, arguments, ctx)
29
- elsif obj.respond_to?(field_name)
30
- obj.public_send(field_name)
31
- else
32
- snake = field_name.to_s.underscore
33
- obj.respond_to?(snake) ? obj.public_send(snake) : nil
34
- end
35
- end
36
-
37
- target_type_class.define_singleton_method(method_name, &wrapper)
38
- @field_def.resolve_proc = wrapper if @field_def.respond_to?(:resolve_proc=)
39
- end
3
+ class Lighthouse::GraphQL::DirectiveResolvers::AuthDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
4
+ def self.directive_name = 'auth'
5
+ def self.definition = 'directive @auth on FIELD_DEFINITION'
6
+
7
+ def define_resolver(target_type_class)
8
+ inner = original_resolver(target_type_class)
9
+
10
+ install(target_type_class, lambda do |obj, arguments, ctx|
11
+ user = ctx && (ctx[:current_user] || ctx['current_user'])
12
+ raise(::GraphQL::ExecutionError, 'Unauthenticated.') unless user
13
+
14
+ inner.call(obj, arguments, ctx)
15
+ end)
16
+ end
17
+
18
+ private
19
+
20
+ # The field's resolver so far: an existing `resolve_field_*` method, the
21
+ # resolve_proc, or the parent's attribute reader.
22
+ def original_resolver(target_type_class)
23
+ field_name = @field_def.name
24
+ original_singleton =
25
+ (target_type_class.method(resolver_method_name) if target_type_class.respond_to?(resolver_method_name))
26
+
27
+ original_proc =
28
+ (@field_def.resolve_proc if @field_def.respond_to?(:resolve_proc))
29
+
30
+ lambda do |obj, arguments, ctx|
31
+ if original_singleton
32
+ original_singleton.call(obj, arguments, ctx)
33
+ elsif original_proc
34
+ original_proc.call(obj, arguments, ctx)
35
+ else
36
+ read_attribute(obj, field_name)
40
37
  end
41
38
  end
42
39
  end
43
- end
44
40
 
41
+ def read_attribute(obj, field_name)
42
+ return obj.public_send(field_name) if obj.respond_to?(field_name)
43
+
44
+ snake = field_name.to_s.underscore
45
+ obj.respond_to?(snake) ? obj.public_send(snake) : nil
46
+ end
47
+ end