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
@@ -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
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Lighthouse::GraphQL::DirectiveResolvers
6
+ # Shared by @create, @createMany, @update, @updateMany, @upsert and
7
+ # @upsertMany: saves the model a field returns through ActiveRecord, so the
8
+ # model's own validations decide what is valid — the Rails way — and nested
9
+ # input on its associations runs as nested mutations (see NestedMutations).
10
+ # Failures raise a Lighthouse::ValidationError keyed by argument path. The
11
+ # whole field runs in one transaction (Config.transactional_mutations).
12
+ #
13
+ # Arguments map to attributes by their snake_case name, or @rename's
14
+ # `attribute:`; @spread merges an input object into its parent; @inject
15
+ # adds server-side values. On an argument, these directives take
16
+ # `relation:` and act on that association of the parent instead.
17
+ class ModelMutationResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
18
+ ARGUMENTS = 'model: String, relation: String'
19
+ LOCATIONS = 'FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
20
+
21
+ def define_resolver(target_type_class)
22
+ me = self
23
+ install(target_type_class, lambda do |obj, args, ctx|
24
+ model = me.send(:model_class)
25
+ me.send(:transaction, model) { me.send(:mutate, model, obj, args, ctx) }
26
+ end)
27
+ end
28
+
29
+ protected
30
+
31
+ def mutate(_model, _obj, _args, _ctx)
32
+ raise NotImplementedError
33
+ end
34
+
35
+ def executor
36
+ Lighthouse::GraphQL::NestedMutations::Executor.new(@field_def.name)
37
+ end
38
+
39
+ def transaction(model, &block)
40
+ Lighthouse::Config.transactional_mutations ? model.transaction(&block) : yield
41
+ end
42
+
43
+ def field_input(args, ctx)
44
+ values = Lighthouse::Support::Arguments.inject(@field_def.ast_node, Lighthouse::Support::Arguments.shallow(args), ctx)
45
+ Lighthouse::GraphQL::NestedMutations::Input.new(values, @field_def.arguments, [])
46
+ end
47
+
48
+ NOT_EXACTLY_ONE_ARGUMENT = '@*Many directives must ensure that clients pass exactly one field argument.'
49
+ ARGUMENT_NOT_LIST = '@*Many directives must ensure that the single argument value is a list.'
50
+ LIST_ITEM_NOT_INPUT_OBJECT = '@*Many directives must ensure that the list items of its single argument value are input objects.'
51
+
52
+ # The inputs of the single list-of-inputs argument of the *Many directives.
53
+ def item_inputs(args)
54
+ argument = list_argument
55
+ items = Array(Lighthouse::Support::Arguments.shallow(args)[argument.keyword])
56
+ items.each_with_index.map do |item, index|
57
+ Lighthouse::GraphQL::NestedMutations::Input.new(Lighthouse::Support::Arguments.shallow(item), argument.type.unwrap.arguments,
58
+ [argument.graphql_name, index.to_s])
59
+ end
60
+ end
61
+
62
+ # The field's single argument, which must be a list of input objects.
63
+ def list_argument
64
+ argument = @field_def.arguments.values.first
65
+ raise Lighthouse::DefinitionError, NOT_EXACTLY_ONE_ARGUMENT unless @field_def.arguments.size == 1
66
+ raise Lighthouse::DefinitionError, ARGUMENT_NOT_LIST unless argument.type.list?
67
+ raise Lighthouse::DefinitionError, LIST_ITEM_NOT_INPUT_OBJECT unless argument.type.unwrap.kind.input_object?
68
+
69
+ argument
70
+ end
71
+
72
+ # Save every item, then raise once with all their errors (the
73
+ # transaction rolls the saved ones back).
74
+ def each_item(inputs)
75
+ messages = {}
76
+ records = inputs.map do |input|
77
+ yield input
78
+ rescue Lighthouse::ValidationError => e
79
+ messages.merge!(e.messages) { |_, a, b| a + b }
80
+ nil
81
+ end
82
+ raise Lighthouse::ValidationError.new(@field_def.name, messages) unless messages.empty?
83
+
84
+ records
85
+ end
86
+
87
+ def identifying_columns
88
+ return nil unless @directive_node.arguments.any? { |a| a.name == 'identifyingColumns' }
89
+
90
+ Array(directive_arg('identifyingColumns'))
91
+ end
92
+
93
+ # An empty list would match any record; refuse it when the schema is built.
94
+ def validate_identifying_columns!
95
+ return unless identifying_columns && identifying_columns.empty?
96
+
97
+ raise Lighthouse::DefinitionError,
98
+ "@#{@directive_node.name} on #{@type_def.graphql_name}.#{@field_def.name}: identifyingColumns must not be empty."
99
+ end
100
+ end
101
+
102
+ # @create — create a model from the field's arguments.
103
+ class CreateDirectiveResolver < ModelMutationResolver
104
+ def self.directive_name = 'create'
105
+ def self.definition = "directive @create(#{ARGUMENTS}) on #{LOCATIONS}"
106
+
107
+ protected
108
+
109
+ def mutate(model, _obj, args, ctx) = executor.create(model, field_input(args, ctx))
110
+ end
111
+
112
+ # @createMany — create one model per input in the field's single list argument.
113
+ class CreateManyDirectiveResolver < ModelMutationResolver
114
+ def self.directive_name = 'createMany'
115
+ def self.definition = "directive @createMany(#{ARGUMENTS}) on #{LOCATIONS}"
116
+
117
+ protected
118
+
119
+ def mutate(model, _obj, args, _ctx)
120
+ each_item(item_inputs(args)) { |input| executor.create(model, input) }
121
+ end
122
+ end
123
+
124
+ # @update — find a model by its primary key argument and update it.
125
+ class UpdateDirectiveResolver < ModelMutationResolver
126
+ def self.directive_name = 'update'
127
+ def self.definition = "directive @update(#{ARGUMENTS}) on #{LOCATIONS}"
128
+
129
+ protected
130
+
131
+ def mutate(model, _obj, args, ctx) = executor.update(model, field_input(args, ctx))
132
+ end
133
+
134
+ # @updateMany — update one model per input (each carrying its primary key).
135
+ class UpdateManyDirectiveResolver < ModelMutationResolver
136
+ def self.directive_name = 'updateMany'
137
+ def self.definition = "directive @updateMany(#{ARGUMENTS}) on #{LOCATIONS}"
138
+
139
+ protected
140
+
141
+ def mutate(model, _obj, args, _ctx)
142
+ each_item(item_inputs(args)) { |input| executor.update(model, input) }
143
+ end
144
+ end
145
+
146
+ # @upsert — update the model the arguments identify (by `identifyingColumns`,
147
+ # else primary key), or create it.
148
+ class UpsertDirectiveResolver < ModelMutationResolver
149
+ def self.directive_name = 'upsert'
150
+ def self.definition = "directive @upsert(#{ARGUMENTS}, identifyingColumns: [String!]) on #{LOCATIONS}"
151
+
152
+ def define_resolver(target_type_class)
153
+ validate_identifying_columns!
154
+ super
155
+ end
156
+
157
+ protected
158
+
159
+ def mutate(model, _obj, args, ctx)
160
+ # rubocop:disable-next Rails/SkipsModelValidations -- Executor#upsert, not ActiveRecord's: it saves through validations.
161
+ executor.upsert(model, field_input(args, ctx), identifying: identifying_columns)
162
+ end
163
+ end
164
+
165
+ # @upsertMany — @upsert for each input in the field's single list argument.
166
+ class UpsertManyDirectiveResolver < ModelMutationResolver
167
+ def self.directive_name = 'upsertMany'
168
+ def self.definition = "directive @upsertMany(#{ARGUMENTS}, identifyingColumns: [String!]) on #{LOCATIONS}"
169
+
170
+ def define_resolver(target_type_class)
171
+ validate_identifying_columns!
172
+ super
173
+ end
174
+
175
+ protected
176
+
177
+ def mutate(model, _obj, args, _ctx)
178
+ columns = identifying_columns
179
+ # rubocop:disable-next Rails/SkipsModelValidations -- Executor#upsert, not ActiveRecord's: it saves through validations.
180
+ each_item(item_inputs(args)) { |input| executor.upsert(model, input, identifying: columns) }
181
+ end
182
+ end
183
+
184
+ # @delete — destroy the models the field's argument filters select
185
+ # (@whereKey, @eq, ...), running their callbacks, and return them: a list
186
+ # for a list field, else the first or null. Refuses to run without a
187
+ # filter argument, which would delete every row.
188
+ class DeleteDirectiveResolver < ModelMutationResolver
189
+ def self.directive_name = 'delete'
190
+ def self.definition = "directive @delete(#{ARGUMENTS}, scopes: [String!]) on #{LOCATIONS}"
191
+
192
+ WOULD_MODIFY_ALL = 'Would modify all models, use an argument to filter.'
193
+
194
+ protected
195
+
196
+ # A record whose callbacks refuse the destroy is reported as an error next
197
+ # to the data, as PHP Lighthouse does; the others are still deleted.
198
+ def mutate(model, _obj, args, ctx)
199
+ args = Lighthouse::Support::Arguments.shallow(args)
200
+ raise ::GraphQL::ExecutionError, WOULD_MODIFY_ALL unless filtered?(args)
201
+
202
+ relation = Array(directive_arg('scopes')).reduce(model.all) { |acc, scope| acc.public_send(scope) }
203
+ records = apply_where_filters(relation, args).to_a
204
+ records.each do |record|
205
+ next if record.destroy
206
+
207
+ ctx.add_error(::GraphQL::ExecutionError.new("Could not modify model #{model.name} with ID #{record.id}."))
208
+ end
209
+ @field_def.type.list? ? records : records.first
210
+ end
211
+
212
+ def filtered?(args)
213
+ registry = Lighthouse::DirectiveRegistry.default
214
+ (@field_def.ast_node.arguments || []).any? do |argument|
215
+ args.key?(argument.name.underscore.to_sym) && argument.directives.any? { |d| registry.arg_builder_class(d.name) }
216
+ end
217
+ end
218
+ end
219
+ end