lighthouse-graphql 0.1.1 → 0.2.1

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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +175 -1
  3. data/README.md +47 -3
  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/motivation.md +184 -0
  12. data/docs/mutations.md +260 -0
  13. data/docs/performance.md +138 -0
  14. data/docs/relationships.md +80 -33
  15. data/docs/resolvers.md +189 -0
  16. data/docs/validation.md +132 -0
  17. data/lib/generators/lighthouse/builder_generator.rb +30 -0
  18. data/lib/generators/lighthouse/class_generator.rb +61 -0
  19. data/lib/generators/lighthouse/install/install_generator.rb +127 -0
  20. data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +8 -0
  21. data/lib/generators/lighthouse/install/templates/graphql_controller.rb.tt +57 -0
  22. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +67 -0
  23. data/lib/generators/lighthouse/install/templates/schema.graphql.tt +19 -0
  24. data/lib/generators/lighthouse/install/templates/schema.rb.tt +10 -0
  25. data/lib/generators/lighthouse/mutation_generator.rb +30 -0
  26. data/lib/generators/lighthouse/query_generator.rb +29 -0
  27. data/lib/generators/lighthouse/validator_generator.rb +36 -0
  28. data/lib/lighthouse/cache_control.rb +126 -0
  29. data/lib/lighthouse/caching.rb +68 -0
  30. data/lib/lighthouse/config.rb +85 -19
  31. data/lib/lighthouse/contracts/arg_builder.rb +8 -12
  32. data/lib/lighthouse/contracts/arg_manipulator.rb +8 -12
  33. data/lib/lighthouse/contracts/directive.rb +10 -14
  34. data/lib/lighthouse/contracts/field_manipulator.rb +8 -12
  35. data/lib/lighthouse/contracts/field_middleware.rb +7 -11
  36. data/lib/lighthouse/contracts/field_resolver.rb +9 -13
  37. data/lib/lighthouse/dataloader_sources/association_loader.rb +28 -19
  38. data/lib/lighthouse/dataloader_sources/model_loader.rb +10 -14
  39. data/lib/lighthouse/dataloader_sources/relation_count_loader.rb +16 -0
  40. data/lib/lighthouse/dataloader_sources/relation_page_loader.rb +18 -0
  41. data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +57 -69
  42. data/lib/lighthouse/directive_resolvers/all_resolver.rb +26 -57
  43. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +161 -0
  44. data/lib/lighthouse/directive_resolvers/auth_resolver.rb +41 -38
  45. data/lib/lighthouse/directive_resolvers/base.rb +142 -86
  46. data/lib/lighthouse/directive_resolvers/builder_applier.rb +58 -56
  47. data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +133 -0
  48. data/lib/lighthouse/directive_resolvers/can_resolvers.rb +177 -0
  49. data/lib/lighthouse/directive_resolvers/convention_resolver.rb +70 -0
  50. data/lib/lighthouse/directive_resolvers/count_resolver.rb +42 -48
  51. data/lib/lighthouse/directive_resolvers/field_resolver.rb +27 -68
  52. data/lib/lighthouse/directive_resolvers/find_resolver.rb +42 -52
  53. data/lib/lighthouse/directive_resolvers/first_resolver.rb +39 -45
  54. data/lib/lighthouse/directive_resolvers/guard_resolver.rb +16 -22
  55. data/lib/lighthouse/directive_resolvers/method_resolver.rb +19 -25
  56. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +219 -0
  57. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +70 -128
  58. data/lib/lighthouse/directive_resolvers/rename_resolver.rb +18 -24
  59. data/lib/lighthouse/directive_resolvers/resolution.rb +31 -27
  60. data/lib/lighthouse/directive_resolvers/validator_resolver.rb +193 -0
  61. data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +164 -163
  62. data/lib/lighthouse/directives/arguments/base.rb +23 -31
  63. data/lib/lighthouse/directives/arguments/filters.rb +113 -93
  64. data/lib/lighthouse/directives/arguments/order_by.rb +41 -41
  65. data/lib/lighthouse/directives/pagination_info_types.rb +69 -0
  66. data/lib/lighthouse/directives/pagination_manipulator.rb +176 -0
  67. data/lib/lighthouse/directives/registry.rb +210 -141
  68. data/lib/lighthouse/directives/where_conditions_directive.rb +236 -200
  69. data/lib/lighthouse/errors.rb +27 -0
  70. data/lib/lighthouse/federation_directives.rb +53 -0
  71. data/lib/lighthouse/field_resolver_stack.rb +56 -0
  72. data/lib/lighthouse/generated_resolvers.rb +82 -0
  73. data/lib/lighthouse/graphql/version.rb +1 -1
  74. data/lib/lighthouse/graphql_ruby_patches.rb +18 -0
  75. data/lib/lighthouse/ide_helper.rb +98 -0
  76. data/lib/lighthouse/namespaces.rb +21 -0
  77. data/lib/lighthouse/nested_mutations/belongs_to_operations.rb +50 -0
  78. data/lib/lighthouse/nested_mutations/executor.rb +149 -0
  79. data/lib/lighthouse/nested_mutations/has_operations.rb +66 -0
  80. data/lib/lighthouse/nested_mutations/many_to_many_operations.rb +44 -0
  81. data/lib/lighthouse/nested_mutations/partitioner.rb +125 -0
  82. data/lib/lighthouse/nested_mutations.rb +210 -0
  83. data/lib/lighthouse/pagination.rb +221 -0
  84. data/lib/lighthouse/persisted_queries.rb +167 -0
  85. data/lib/lighthouse/railtie.rb +26 -0
  86. data/lib/lighthouse/rb_lighthouse.rb +50 -29
  87. data/lib/lighthouse/reference_resolver.rb +13 -15
  88. data/lib/lighthouse/relation_batch.rb +156 -0
  89. data/lib/lighthouse/resolver_dispatch.rb +26 -0
  90. data/lib/lighthouse/schema_cache.rb +120 -0
  91. data/lib/lighthouse/schema_factory.rb +27 -37
  92. data/lib/lighthouse/schema_generator.rb +196 -239
  93. data/lib/lighthouse/schema_implementation.rb +51 -48
  94. data/lib/lighthouse/sdl_loader.rb +17 -20
  95. data/lib/lighthouse/support/arguments.rb +119 -0
  96. data/lib/lighthouse/support/authorization.rb +18 -19
  97. data/lib/lighthouse/support/batching.rb +16 -0
  98. data/lib/lighthouse/support/directive_args.rb +26 -24
  99. data/lib/lighthouse/support/model_resolver.rb +56 -56
  100. data/lib/lighthouse/support/naming.rb +55 -59
  101. data/lib/lighthouse/tasks/lighthouse.rake +33 -0
  102. data/lib/lighthouse/where_conditions/operator_map.rb +25 -29
  103. data/sig/lighthouse/directives.rbs +158 -0
  104. data/sig/lighthouse/graphql.rbs +78 -0
  105. data/sig/lighthouse.rbs +142 -0
  106. metadata +56 -64
  107. data/lib/lighthouse/directive_resolvers/belongs_to_many_resolver.rb +0 -56
  108. data/lib/lighthouse/directive_resolvers/belongs_to_resolver.rb +0 -47
  109. data/lib/lighthouse/directive_resolvers/can_resolver.rb +0 -86
  110. data/lib/lighthouse/directive_resolvers/has_many_resolver.rb +0 -109
  111. data/lib/lighthouse/directive_resolvers/has_one_resolver.rb +0 -43
  112. data/lib/lighthouse/directives/relation_directive.rb +0 -332
@@ -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
@@ -1,91 +1,147 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Lighthouse
4
- module GraphQL
5
- module DirectiveResolvers
6
- class BaseDirectiveResolver
7
- def initialize(field_def, type_def, directive_node, opts = {})
8
- @field_def = field_def
9
- @type_def = type_def
10
- @directive_node = directive_node
11
- @opts = opts
12
- end
13
-
14
- def define_resolver(target_type_class)
15
- raise NotImplementedError
16
- end
17
-
18
- def apply_where_filters(relation, args)
19
- return relation unless relation.respond_to?(:where)
20
-
21
- args_h = args.respond_to?(:to_h) ? args.to_h : (args || {})
22
-
23
- where_condition = args_h[:where] || args_h['where']
24
- relation = DirectiveResolvers::WhereConditionsApplier.apply(relation, where_condition) if where_condition
25
-
26
- @field_def.ast_node.arguments.each do |arg_node|
27
- next unless arg_node.directives.any? { |dir| dir.name == 'whereHasConditions' }
28
-
29
- arg_value = Lighthouse::Support::Naming.fetch_arg(args_h, arg_node.name)
30
- next unless arg_value
31
-
32
- relation_name = extract_relation_name_from_directive(arg_node) || Lighthouse::Support::Naming.derive_relation_name(arg_node.name)
33
- relation = DirectiveResolvers::WhereConditionsApplier.apply_has_argument(relation, arg_node.name, arg_value, relation_name)
34
- end
35
-
36
- apply_arg_builders(relation, args_h)
37
- rescue ::GraphQL::ExecutionError
38
- raise
39
- rescue StandardError => e
40
- Lighthouse::Config.handle_error(e)
41
- relation
42
- end
43
-
44
- # Compose argument-builder directives (@eq, @neq, @in, @notIn, @like,
45
- # @where, @orderBy) onto the relation, in SDL argument order, for each
46
- # field argument that carries one and that the client supplied a value
47
- # for.
48
- def apply_arg_builders(relation, args_h)
49
- return relation unless relation.respond_to?(:where)
50
- return relation unless @field_def.ast_node
51
-
52
- registry = Lighthouse::DirectiveRegistry.default
53
-
54
- (@field_def.ast_node.arguments || []).each do |arg_node|
55
- (arg_node.directives || []).each do |dnode|
56
- builder_class = registry.arg_builder_class(dnode.name)
57
- next unless builder_class
58
-
59
- value = Lighthouse::Support::Naming.fetch_arg(args_h, arg_node.name)
60
- next if value.nil?
61
-
62
- relation = builder_class.new(arg_node, dnode).handle_builder(relation, value)
63
- end
64
- end
65
-
66
- relation
67
- rescue ::GraphQL::ExecutionError
68
- raise
69
- rescue StandardError => e
70
- Lighthouse::Config.handle_error(e)
71
- relation
72
- end
73
-
74
- def extract_relation_name_from_directive(arg_node)
75
- directive = arg_node.directives.find { |dir| dir.name == 'whereHasConditions' }
76
- return nil unless directive
77
-
78
- relation_arg = directive.arguments.find { |arg| arg.name == 'relation' }
79
- return nil unless relation_arg
80
-
81
- relation_arg.value.respond_to?(:value) ? relation_arg.value.value : relation_arg.value
82
- rescue StandardError
83
- nil
84
- end
85
-
86
- def constantize_model(name)
87
- Lighthouse::Support::ModelResolver.constantize(name)
88
- end
3
+ class Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
4
+ def initialize(field_def, type_def, directive_node, opts = {})
5
+ @field_def = field_def
6
+ @type_def = type_def
7
+ @directive_node = directive_node
8
+ @opts = opts
9
+ end
10
+
11
+ def define_resolver(target_type_class)
12
+ raise NotImplementedError
13
+ end
14
+
15
+ def apply_where_filters(relation, args)
16
+ return relation unless relation.respond_to?(:where)
17
+
18
+ args_h = args.respond_to?(:to_h) ? args.to_h : (args || {})
19
+
20
+ relation = apply_where_conditions(relation, args_h)
21
+ @field_def.ast_node.arguments.each do |arg_node|
22
+ relation = apply_where_has_conditions(relation, arg_node, args_h)
23
+ end
24
+
25
+ apply_arg_builders(relation, args_h)
26
+ rescue ::GraphQL::ExecutionError
27
+ raise
28
+ rescue StandardError => e
29
+ Lighthouse::Config.handle_error(e)
30
+ relation
31
+ end
32
+
33
+ # Compose argument-builder directives (@eq, @neq, @in, @notIn, @like,
34
+ # @where, @orderBy) onto the relation, in SDL argument order, for each
35
+ # field argument that carries one and that the client supplied a value
36
+ # for.
37
+ def apply_arg_builders(relation, args_h)
38
+ return relation unless relation.respond_to?(:where)
39
+ return relation unless @field_def.ast_node
40
+
41
+ arg_builders.each do |arg_node, dnode, builder_class|
42
+ value = Lighthouse::Support::Naming.fetch_arg(args_h, arg_node.name)
43
+ next if value.nil?
44
+
45
+ relation = builder_class.new(arg_node, dnode).handle_builder(relation, value)
46
+ end
47
+
48
+ relation
49
+ rescue ::GraphQL::ExecutionError
50
+ raise
51
+ rescue StandardError => e
52
+ Lighthouse::Config.handle_error(e)
53
+ relation
54
+ end
55
+
56
+ def extract_relation_name_from_directive(arg_node)
57
+ directive = arg_node.directives.find { |dir| dir.name == 'whereHasConditions' }
58
+ return nil unless directive
59
+
60
+ relation_arg = directive.arguments.find { |arg| arg.name == 'relation' }
61
+ return nil unless relation_arg
62
+
63
+ relation_arg.value.respond_to?(:value) ? relation_arg.value.value : relation_arg.value
64
+ rescue StandardError
65
+ nil
66
+ end
67
+
68
+ def constantize_model(name)
69
+ Lighthouse::Support::ModelResolver.constantize(name)
70
+ end
71
+
72
+ # The model scopes the directive's `scopes:` argument names, in order.
73
+ def apply_scopes(relation)
74
+ Array(directive_arg('scopes')).reduce(relation) { |acc, scope| acc.public_send(scope) }
75
+ end
76
+
77
+ protected
78
+
79
+ def directive_arg(name)
80
+ Lighthouse::Support::DirectiveArgs.literal(@directive_node, name)
81
+ end
82
+
83
+ def resolver_method_name
84
+ "resolve_field_#{@field_def.name}"
85
+ end
86
+
87
+ # The field's resolver so far, for a directive that wraps it.
88
+ def capture_inner(target_type_class)
89
+ Lighthouse::GraphQL::DirectiveResolvers::Resolution.capture(target_type_class, resolver_method_name, @field_def, @field_def.name)
90
+ end
91
+
92
+ def install(target_type_class, callable)
93
+ target_type_class.define_singleton_method(resolver_method_name, &callable)
94
+ @field_def.resolve_proc = callable if @field_def.respond_to?(:resolve_proc=)
95
+ end
96
+
97
+ # The model a directive acts on: its `model:` argument, the `model:` of
98
+ # another directive on the field (`@count(model: "Post") @canQuery`), or
99
+ # the field's return type without a generated Paginator/Connection suffix.
100
+ def model_class
101
+ name = directive_arg('model') || sibling_model_name || return_type_model_name
102
+ constantize_model(name) || raise(Lighthouse::DefinitionError,
103
+ "@#{@directive_node&.name} on #{@type_def.graphql_name}.#{@field_def.name}: no model #{name}")
104
+ end
105
+
106
+ private
107
+
108
+ def sibling_model_name
109
+ (@field_def.ast_node&.directives || []).filter_map do |d|
110
+ Lighthouse::Support::DirectiveArgs.literal(d, 'model')
111
+ end.first
112
+ end
113
+
114
+ def return_type_model_name
115
+ @field_def.type.unwrap.graphql_name.sub(/(SimplePaginator|Paginator|Connection)\z/, '')
116
+ end
117
+
118
+ # The `where:` argument (@whereConditions), when the client supplied one.
119
+ def apply_where_conditions(relation, args_h)
120
+ where_condition = args_h[:where] || args_h['where']
121
+ return relation unless where_condition
122
+
123
+ Lighthouse::GraphQL::DirectiveResolvers::WhereConditionsApplier.apply(relation, where_condition)
124
+ end
125
+
126
+ # A `whereHasConditions` argument the client supplied filters the
127
+ # relation by the related records matching its condition.
128
+ def apply_where_has_conditions(relation, arg_node, args_h)
129
+ return relation unless arg_node.directives.any? { |dir| dir.name == 'whereHasConditions' }
130
+
131
+ arg_value = Lighthouse::Support::Naming.fetch_arg(args_h, arg_node.name)
132
+ return relation unless arg_value
133
+
134
+ relation_name = extract_relation_name_from_directive(arg_node) || Lighthouse::Support::Naming.derive_relation_name(arg_node.name)
135
+ Lighthouse::GraphQL::DirectiveResolvers::WhereConditionsApplier.apply_has_argument(relation, arg_node.name, arg_value, relation_name)
136
+ end
137
+
138
+ # Every (argument, directive, builder class) on the field, in SDL order.
139
+ def arg_builders
140
+ registry = Lighthouse::DirectiveRegistry.default
141
+ (@field_def.ast_node.arguments || []).flat_map do |arg_node|
142
+ (arg_node.directives || []).filter_map do |dnode|
143
+ builder_class = registry.arg_builder_class(dnode.name)
144
+ [arg_node, dnode, builder_class] if builder_class
89
145
  end
90
146
  end
91
147
  end