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,95 @@
1
+ # Relationships
2
+
3
+ Just like in Rails, you can resolve associations directly from your schema. Add a
4
+ field named after the association and annotate it with the matching directive.
5
+
6
+ ```ruby
7
+ class Post < ApplicationRecord
8
+ belongs_to :author, class_name: 'User'
9
+ has_many :comments
10
+ end
11
+ ```
12
+
13
+ ```graphql
14
+ type Post {
15
+ author: User! @belongsTo(relation: "author")
16
+ comments: [Comment!]! @hasMany
17
+ }
18
+ ```
19
+
20
+ ## Avoiding the N+1 problem
21
+
22
+ Resolving relationships naively triggers one query per parent record. Lighthouse's
23
+ relationship directives load through graphql-ruby's **Dataloader**, batching all
24
+ sibling loads into a single query. Enable it on your schema:
25
+
26
+ ```ruby
27
+ class AppSchema < BaseAppSchema
28
+ use GraphQL::Dataloader
29
+ end
30
+ ```
31
+
32
+ With the Dataloader in place, a query for 100 posts and their authors issues two
33
+ queries, not 101.
34
+
35
+ ## One-to-many — `@hasMany`
36
+
37
+ ```graphql
38
+ type User {
39
+ posts: [Post!]! @hasMany
40
+ }
41
+ ```
42
+
43
+ `@hasMany` returns a **connection** type, so it supports pagination arguments and
44
+ `edges { node { ... } }` / `nodes { ... }` selections out of the box. The inverse
45
+ is `@belongsTo`.
46
+
47
+ ## One-to-one — `@hasOne`
48
+
49
+ ```graphql
50
+ type User {
51
+ profile: Profile @hasOne
52
+ }
53
+ ```
54
+
55
+ ## Inverse — `@belongsTo`
56
+
57
+ ```graphql
58
+ type Post {
59
+ author: User! @belongsTo(relation: "author")
60
+ }
61
+ ```
62
+
63
+ ## Many-to-many — `@belongsToMany`
64
+
65
+ ```graphql
66
+ type User {
67
+ roles: [Role!]! @belongsToMany
68
+ }
69
+ ```
70
+
71
+ ## Renaming relations
72
+
73
+ By default the relation name equals the field name (snake_cased). When they
74
+ differ, pass `relation:`:
75
+
76
+ ```graphql
77
+ type Post {
78
+ writtenBy: User! @belongsTo(relation: "author")
79
+ }
80
+ ```
81
+
82
+ ## Filtering a relationship
83
+
84
+ Relationship fields accept the same [argument filters](filtering-and-ordering.md)
85
+ and [`@whereConditions`](filtering-and-ordering.md#complex-where-conditions) as
86
+ root queries, so you can scope the related records:
87
+
88
+ ```graphql
89
+ type User {
90
+ publishedPosts(
91
+ status: String @eq
92
+ orderBy: [OrderByClause!] @orderBy
93
+ ): [Post!]! @hasMany(relation: "posts")
94
+ }
95
+ ```
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lighthouse
4
+ # Convenience entrypoint: `Lighthouse.configure { |c| c.resolver_namespaces = [...] }`.
5
+ def self.configure(&block)
6
+ Config.configure(&block)
7
+ end
8
+
9
+ module Config
10
+ class << self
11
+ attr_writer :sdl_folder, :resolver_namespaces, :on_error,
12
+ :authenticated_user, :policy_user, :authorizer
13
+
14
+ def sdl_folder
15
+ @sdl_folder ||= (defined?(Rails) && Rails.respond_to?(:root) ? File.join(Rails.root, 'graphql') : nil)
16
+ end
17
+
18
+ def directive_map
19
+ @directive_map ||= {}
20
+ end
21
+
22
+ # Namespaces tried (in order) when resolving a class name from a directive
23
+ # argument, e.g. `@field(resolver: "ContactPerson")` or `@all(model: "X")`.
24
+ # Keeping this in config means the library holds no app-specific namespace
25
+ # and can be extracted into a gem as-is.
26
+ def resolver_namespaces
27
+ @resolver_namespaces ||= ['App::Graphql']
28
+ end
29
+
30
+ # Hook invoked when a directive/resolver swallows an error. Defaults to
31
+ # re-raising in test (so failures surface) and logging otherwise. Override
32
+ # via `Lighthouse.configure { |c| c.on_error = ->(e, ctx) { ... } }`.
33
+ def on_error
34
+ @on_error ||= default_on_error
35
+ end
36
+
37
+ def handle_error(error, context = nil)
38
+ on_error.call(error, context)
39
+ end
40
+
41
+ # --- Authorization seams (used by @guard / @can) -------------------------
42
+ # The library stays framework-agnostic: how to read the authenticated user
43
+ # and how to check an ability are both configurable. The defaults work with
44
+ # a `current_user`/`pundit_user` context and Pundit.
45
+
46
+ # Returns the authenticated user from the GraphQL context (nil if none).
47
+ def authenticated_user
48
+ @authenticated_user ||= ->(ctx) { ctx && (ctx[:current_user] || ctx['current_user']) }
49
+ end
50
+
51
+ # The "user" object passed to the authorizer (e.g. Pundit's user context).
52
+ # Defaults to ctx[:pundit_user], falling back to the authenticated user.
53
+ def policy_user
54
+ @policy_user ||= lambda do |ctx|
55
+ (ctx && (ctx[:pundit_user] || ctx['pundit_user'])) || authenticated_user.call(ctx)
56
+ end
57
+ end
58
+
59
+ # Checks an ability: `->(user, record, ability) { true/false }`.
60
+ # Defaults to Pundit: `Pundit.policy!(user, record).<ability>?`.
61
+ def authorizer
62
+ @authorizer ||= default_authorizer
63
+ end
64
+
65
+ def configure
66
+ yield(self) if block_given?
67
+ end
68
+
69
+ def [](k)
70
+ public_send(k)
71
+ end
72
+
73
+ def reset!
74
+ @sdl_folder = nil
75
+ @resolver_namespaces = nil
76
+ @on_error = nil
77
+ @directive_map = nil
78
+ @authenticated_user = nil
79
+ @policy_user = nil
80
+ @authorizer = nil
81
+ end
82
+
83
+ private
84
+
85
+ def default_authorizer
86
+ lambda do |user, record, ability|
87
+ unless defined?(::Pundit)
88
+ raise 'Lighthouse: configure `Config.authorizer` to use @can (Pundit not detected).'
89
+ end
90
+
91
+ ::Pundit.policy!(user, record).public_send("#{ability}?")
92
+ end
93
+ end
94
+
95
+ def default_on_error
96
+ lambda do |error, _context|
97
+ if defined?(Rails) && Rails.respond_to?(:env) && Rails.env.test?
98
+ raise error
99
+ elsif defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
100
+ Rails.logger.warn("[Lighthouse] #{error.class}: #{error.message}")
101
+ else
102
+ warn("[Lighthouse] #{error.class}: #{error.message}")
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ module GraphQL
110
+ Config = Lighthouse::Config unless const_defined?(:Config, false)
111
+ end
112
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'directive'
4
+
5
+ module Lighthouse
6
+ module Contracts
7
+ # A directive on a field ARGUMENT that composes a constraint onto an
8
+ # ActiveRecord relation at resolve time, e.g. @eq, @neq, @in, @notIn, @like,
9
+ # @where, @orderBy. These compose in SDL argument order via the
10
+ # ArgBuilderPipeline.
11
+ #
12
+ # Implementors provide `#handle_builder(relation, value)` returning a relation.
13
+ module ArgBuilder
14
+ include Directive
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'directive'
4
+
5
+ module Lighthouse
6
+ module Contracts
7
+ # A directive on a field ARGUMENT that mutates the schema AST before build,
8
+ # e.g. @whereConditions / @whereHasConditions generating input/enum types and
9
+ # swapping the `_` placeholder argument type.
10
+ #
11
+ # Implementors provide (at the class level)
12
+ # `manipulate_argument_definition(manipulation_context, parent, field, arg, directive)`.
13
+ module ArgManipulator
14
+ include Directive
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lighthouse
4
+ # Contracts are duck-typed marker modules that mirror the PHP Lighthouse
5
+ # directive interfaces. A directive class includes whichever contracts it
6
+ # fulfils; the DirectiveRegistry and SchemaGenerator detect capabilities via
7
+ # `klass.include?(Contracts::X)` and invoke the corresponding hook.
8
+ module Contracts
9
+ # Base marker. Every Lighthouse directive class includes this.
10
+ #
11
+ # Implementors are expected to expose (at the class level):
12
+ # - `directive_name` -> String the SDL name without the leading '@'
13
+ # - `definition` -> String? the SDL `directive @name(...) on ...` line
14
+ module Directive
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'directive'
4
+
5
+ module Lighthouse
6
+ module Contracts
7
+ # A directive that mutates the schema AST before it is built, e.g. @paginate
8
+ # and the relationship directives injecting Connection/Paginator types, or
9
+ # @orderBy generating its column enum.
10
+ #
11
+ # Implementors provide (at the class level)
12
+ # `manipulate_field_definition(manipulation_context, parent, field, directive)`.
13
+ module FieldManipulator
14
+ include Directive
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'directive'
4
+
5
+ module Lighthouse
6
+ module Contracts
7
+ # A directive that wraps an existing field resolver, e.g. @auth wrapping the
8
+ # underlying resolver to enforce authentication.
9
+ #
10
+ # Implementors provide `#wrap_resolver(next_resolver)` returning a new
11
+ # `#call(obj, args, ctx)` lambda that may invoke `next_resolver`.
12
+ module FieldMiddleware
13
+ include Directive
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'directive'
4
+
5
+ module Lighthouse
6
+ module Contracts
7
+ # A directive that provides the runtime resolver for a field, e.g. @all,
8
+ # @find, @paginate, @hasMany, @belongsTo, @field, @auth, @aggregate.
9
+ #
10
+ # During the resolution phase the SchemaGenerator instantiates the directive
11
+ # and installs its resolver onto the field. Legacy resolvers fulfil this via
12
+ # `#define_resolver(target_type)`; clean directives implement `#resolve_field`
13
+ # returning a `#call(obj, args, ctx)` lambda.
14
+ module FieldResolver
15
+ include Directive
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lighthouse
4
+ module GraphQL
5
+ module DataloaderSources
6
+ class AssociationLoader < ::GraphQL::Dataloader::Source
7
+ def initialize(model_class, association_name)
8
+ @model_class = model_class
9
+ @association_name = association_name.to_s
10
+ end
11
+
12
+ # keys: array of parent objects
13
+ def fetch(keys)
14
+ keys.map do |parent|
15
+ if parent.respond_to?(@association_name)
16
+ parent.public_send(@association_name)
17
+ else
18
+ nil
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
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
10
+
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
18
+ end
19
+ end
@@ -0,0 +1,86 @@
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
+ class AggregateDirectiveResolver < BaseDirectiveResolver
10
+ def define_resolver(target_type_class)
11
+ field_name = @field_def.name
12
+ method_name = "resolve_field_#{field_name}"
13
+
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
20
+
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
30
+
31
+ return nil unless relation
32
+
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
36
+
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
40
+
41
+ private
42
+
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
49
+
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
70
+
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
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,73 @@
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
+ class AllDirectiveResolver < BaseDirectiveResolver
10
+ def define_resolver(target_type_class)
11
+ field_name = @field_def.name
12
+
13
+ resolver_instance = self
14
+ field_ast_node = @field_def.ast_node
15
+
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)
30
+
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
36
+
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
45
+
46
+ relation = DirectiveResolvers::BuilderApplier.apply(relation, field_ast_node, args, ctx, resolver_instance)
47
+
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
56
+
57
+ private
58
+
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
64
+
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
72
+ end
73
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
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
40
+ end
41
+ end
42
+ end
43
+ end
44
+
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
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
89
+ end
90
+ end
91
+ end
92
+ end