lighthouse-graphql 0.4.0 → 0.5.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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +139 -1
  3. data/README.md +8 -5
  4. data/docs/configuration.md +26 -2
  5. data/docs/directives.md +287 -4
  6. data/docs/generators.md +82 -0
  7. data/docs/getting-started.md +26 -30
  8. data/docs/relationships.md +42 -0
  9. data/docs/relay.md +137 -0
  10. data/docs/resolvers.md +261 -22
  11. data/docs/security.md +17 -0
  12. data/docs/types.md +219 -0
  13. data/docs/uploads.md +80 -0
  14. data/lib/generators/lighthouse/base_classes.rb +32 -49
  15. data/lib/generators/lighthouse/install/install_generator.rb +83 -24
  16. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +14 -6
  17. data/lib/generators/lighthouse/install/templates/schema.rb.tt +10 -3
  18. data/lib/lighthouse/async_mutation_job.rb +17 -0
  19. data/lib/lighthouse/complexity.rb +5 -4
  20. data/lib/lighthouse/config.rb +13 -2
  21. data/lib/lighthouse/controller.rb +27 -9
  22. data/lib/lighthouse/dataloader_sources/preload_loader.rb +16 -0
  23. data/lib/lighthouse/dataloader_sources/relation_counts_loader.rb +46 -0
  24. data/lib/lighthouse/default_resolve_dispatcher.rb +26 -0
  25. data/lib/lighthouse/directive_resolvers/all_resolver.rb +1 -1
  26. data/lib/lighthouse/directive_resolvers/argument_binding.rb +61 -0
  27. data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +220 -0
  28. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +2 -2
  29. data/lib/lighthouse/directive_resolvers/async_resolver.rb +90 -0
  30. data/lib/lighthouse/directive_resolvers/base.rb +63 -14
  31. data/lib/lighthouse/directive_resolvers/builder_applier.rb +89 -19
  32. data/lib/lighthouse/directive_resolvers/can_resolvers.rb +1 -1
  33. data/lib/lighthouse/directive_resolvers/convention_resolver.rb +2 -1
  34. data/lib/lighthouse/directive_resolvers/count_resolver.rb +1 -1
  35. data/lib/lighthouse/directive_resolvers/field_resolver.rb +35 -7
  36. data/lib/lighthouse/directive_resolvers/first_resolver.rb +1 -1
  37. data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +57 -0
  38. data/lib/lighthouse/directive_resolvers/lazy_load_resolver.rb +61 -0
  39. data/lib/lighthouse/directive_resolvers/limit_resolver.rb +59 -0
  40. data/lib/lighthouse/directive_resolvers/method_resolver.rb +38 -3
  41. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +1 -1
  42. data/lib/lighthouse/directive_resolvers/namespaced_resolver.rb +31 -0
  43. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +1 -1
  44. data/lib/lighthouse/directive_resolvers/resolution.rb +19 -4
  45. data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +2 -1
  46. data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +77 -0
  47. data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +81 -0
  48. data/lib/lighthouse/directive_resolvers/with_count_resolver.rb +43 -0
  49. data/lib/lighthouse/directive_resolvers/with_resolver.rb +42 -0
  50. data/lib/lighthouse/directives/arguments/more_filters.rb +215 -0
  51. data/lib/lighthouse/directives/definitions.rb +52 -0
  52. data/lib/lighthouse/directives/model_directive.rb +131 -0
  53. data/lib/lighthouse/directives/node_directive.rb +134 -0
  54. data/lib/lighthouse/directives/pagination_manipulator.rb +1 -1
  55. data/lib/lighthouse/directives/registry.rb +21 -21
  56. data/lib/lighthouse/directives/visibility_directive.rb +71 -0
  57. data/lib/lighthouse/enum_directive.rb +41 -0
  58. data/lib/lighthouse/field_resolver_stack.rb +53 -6
  59. data/lib/lighthouse/generated_resolvers.rb +13 -8
  60. data/lib/lighthouse/global_id.rb +31 -0
  61. data/lib/lighthouse/graphql/version.rb +1 -1
  62. data/lib/lighthouse/multipart_request.rb +43 -0
  63. data/lib/lighthouse/node_registry.rb +68 -0
  64. data/lib/lighthouse/pagination.rb +3 -0
  65. data/lib/lighthouse/rb_lighthouse.rb +33 -0
  66. data/lib/lighthouse/relation_counts.rb +29 -0
  67. data/lib/lighthouse/resolve_info.rb +80 -0
  68. data/lib/lighthouse/resolver_bases.rb +9 -2
  69. data/lib/lighthouse/resolver_dispatch.rb +36 -11
  70. data/lib/lighthouse/scalar_coercion.rb +72 -0
  71. data/lib/lighthouse/schema_cache.rb +2 -1
  72. data/lib/lighthouse/schema_generator.rb +11 -10
  73. data/lib/lighthouse/schema_implementation.rb +4 -8
  74. data/lib/lighthouse/support/environment.rb +13 -0
  75. data/lib/lighthouse/support/model_resolver.rb +7 -5
  76. data/lib/lighthouse/support/preload.rb +49 -0
  77. data/lib/lighthouse/type.rb +103 -0
  78. data/lib/lighthouse/type_resolution.rb +150 -0
  79. data/lib/lighthouse/upload.rb +26 -0
  80. data/lib/lighthouse/upload_store.rb +32 -0
  81. data/sig/lighthouse/directives.rbs +3 -2
  82. data/sig/lighthouse.rbs +18 -0
  83. metadata +38 -1
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @hide(env:) and @show(env:) — leave a field out of the schema in some
4
+ # environments, as PHP Lighthouse's directives: @hide removes it in the ones
5
+ # listed, @show keeps it only in those. The environment is Rails.env (see
6
+ # Support::Environment); the SDL is rewritten before the schema is built.
7
+ #
8
+ # type Query {
9
+ # debugInfo: String @show(env: ["development", "test"])
10
+ # legacyFeed: [Post!]! @hide(env: ["production"]) @all
11
+ # }
12
+ class Lighthouse::GraphQL::Directives::VisibilityDirective
13
+ DEFINITIONS = {
14
+ 'hide' => <<~GRAPHQL.chomp,
15
+ """
16
+ Excludes the annotated element from the schema conditionally.
17
+ """
18
+ directive @hide(
19
+ "Specify which environments must not use this field, e.g. [\\"production\\"]."
20
+ env: [String!]!
21
+ ) on FIELD_DEFINITION
22
+ GRAPHQL
23
+ 'show' => <<~GRAPHQL.chomp
24
+ """
25
+ Includes the annotated element in the schema conditionally.
26
+ """
27
+ directive @show(
28
+ "Specify which environments may use this field, e.g. [\\"testing\\"]."
29
+ env: [String!]!
30
+ ) on FIELD_DEFINITION
31
+ GRAPHQL
32
+ }.freeze
33
+
34
+ FIELDS = [
35
+ ::GraphQL::Language::Nodes::ObjectTypeDefinition, ::GraphQL::Language::Nodes::ObjectTypeExtension,
36
+ ::GraphQL::Language::Nodes::InterfaceTypeDefinition, ::GraphQL::Language::Nodes::InterfaceTypeExtension
37
+ ].freeze
38
+
39
+ DECLARATION = /directive\s+@(hide|show)\b/
40
+ USAGE = /@(hide|show)\b/
41
+
42
+ def self.manipulate(sdl)
43
+ return sdl unless sdl.is_a?(String) && sdl.gsub(DECLARATION, '').match?(USAGE)
44
+
45
+ document = ::GraphQL::Language::Parser.parse(sdl)
46
+ env = Lighthouse::Support::Environment.current
47
+ ::GraphQL::Language::Printer.new.print(document.merge(definitions: document.definitions.map { |definition| visible_fields(definition, env) }))
48
+ rescue ::GraphQL::ParseError
49
+ sdl
50
+ end
51
+
52
+ def self.visible_fields(definition, env)
53
+ return definition unless FIELDS.any? { |kind| definition.is_a?(kind) }
54
+
55
+ definition.merge(fields: definition.fields.select { |field| visible?(field, env) })
56
+ end
57
+
58
+ def self.visible?(field, env)
59
+ field.directives.all? do |directive|
60
+ case directive.name
61
+ when 'hide' then environments(directive).exclude?(env)
62
+ when 'show' then environments(directive).include?(env)
63
+ else true
64
+ end
65
+ end
66
+ end
67
+
68
+ def self.environments(directive)
69
+ Array(directive.arguments.find { |argument| argument.name == 'env' }&.value).map(&:to_s)
70
+ end
71
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @enum(value:) — the internal value of an enum value, as PHP Lighthouse's
4
+ # directive: what a resolver gets for it in an argument, and what it answers
5
+ # with in a result.
6
+ #
7
+ # enum Role {
8
+ # ADMIN @enum(value: "admin")
9
+ # MEMBER @enum(value: "member")
10
+ # }
11
+ #
12
+ # `users(role: Role @eq)` then filters on "admin", and a User whose role is
13
+ # "admin" answers ADMIN. Without the directive, a value is its own name.
14
+ module Lighthouse::GraphQL::EnumDirective
15
+ DEFINITION = <<~GRAPHQL.chomp
16
+ """
17
+ Assign an internal value to an enum key.
18
+ When dealing with the Enum type in your code,
19
+ you will receive the defined value instead of the string key.
20
+ """
21
+ directive @enum(
22
+ "The internal value of the enum key."
23
+ value: Mixed
24
+ ) on ENUM_VALUE
25
+ GRAPHQL
26
+
27
+ TYPE_DEFINITIONS = { 'Mixed' => 'scalar Mixed' }.freeze
28
+
29
+ module_function
30
+
31
+ def apply(schema)
32
+ schema.types.each_value { |type| apply_to(type) if type.kind.enum? }
33
+ end
34
+
35
+ def apply_to(enum_type)
36
+ enum_type.values.each_value do |enum_value|
37
+ directive = (enum_value.ast_node&.directives || []).find { |d| d.name == 'enum' }
38
+ enum_value.value(Lighthouse::Support::DirectiveArgs.literal(directive, 'value')) if directive
39
+ end
40
+ end
41
+ end
@@ -4,21 +4,26 @@
4
4
  # DirectiveRegistry#resolution_order), so each wrapper decorates the ones
5
5
  # before it. Besides its directives, a field gets the conventional
6
6
  # Resolvers::/Mutations:: resolver when it is a root field with no resolving
7
- # directive, and argument validation when an input type it takes carries
8
- # @validator. The registry replaces a hardcoded `case dnode.name`: adding a
7
+ # directive, the method of the type's class (Types::PostType) when another
8
+ # type's field has none, and argument validation when an input type it takes
9
+ # carries @validator. The registry replaces a hardcoded `case dnode.name`: adding a
9
10
  # directive is "register a class", not "edit the generator".
10
11
  module Lighthouse::GraphQL::FieldResolverStack
11
12
  module_function
12
13
 
13
14
  # `bases`: { root kind => base class } from the schema's base_types (ResolverBases).
14
- def apply(type, field_def, directives, root_kind, bases: {})
15
+ # `type_class`: the class of a non-root type (Types::PostType), whose methods
16
+ # resolve its fields that have no resolving directive.
17
+ def apply(type, field_def, root_kind, bases: {}, type_class: nil)
18
+ directives = field_def.ast_node.directives || []
15
19
  opts = { root_kind: root_kind, bases: bases }
16
20
  steps = directive_steps(type, field_def, directives, opts)
17
- steps << convention_step(type, field_def, root_kind, bases) if root_kind && !resolving_directive?(directives)
21
+ steps << limit_step(type, field_def, opts) if Lighthouse::GraphQL::DirectiveResolvers::LimitResolver.keyword(field_def)
22
+ steps << own_step(type, field_def, root_kind, bases, type_class) unless resolving_directive?(directives)
18
23
  steps << validator_step(type, field_def, opts) if input_validation?(field_def, directives)
19
- steps << empty_strings_step(type, field_def, opts) if empty_string_conversion?(field_def, directives)
24
+ steps.concat(argument_steps(type, field_def, directives, opts))
20
25
 
21
- steps.each_with_index.sort_by { |(order, _), index| [order, index] }.each { |(_, step), _| step.call }
26
+ steps.compact.each_with_index.sort_by { |(order, _), index| [order, index] }.each { |(_, step), _| step.call }
22
27
  end
23
28
 
24
29
  # [order, step] for every directive the registry has a resolver for.
@@ -50,6 +55,22 @@ module Lighthouse::GraphQL::FieldResolverStack
50
55
  [0, -> { resolver.define_resolver(type) }]
51
56
  end
52
57
 
58
+ # What resolves a field with no resolving directive: the conventional class
59
+ # of a root field, or the method of the type's class. nil: the default reader.
60
+ def own_step(type, field_def, root_kind, bases, type_class)
61
+ return convention_step(type, field_def, root_kind, bases) if root_kind
62
+
63
+ type_method_step(type, field_def, type_class) if type_class
64
+ end
65
+
66
+ # Nothing to install when the class does not define the field's method: the
67
+ # field is read off the object.
68
+ def type_method_step(type, field_def, type_class)
69
+ resolver = Lighthouse::GraphQL::DirectiveResolvers::TypeMethodResolver
70
+ method_name = resolver.method_for(type_class, field_def)
71
+ [0, -> { resolver.new(field_def, type, method_name).define_resolver(type) }] if method_name
72
+ end
73
+
53
74
  # An explicit @validator on the field already validates its arguments.
54
75
  def input_validation?(field_def, directives)
55
76
  directives.none? { |d| d.name == 'validator' } &&
@@ -69,6 +90,32 @@ module Lighthouse::GraphQL::FieldResolverStack
69
90
  Lighthouse::GraphQL::DirectiveResolvers::ConvertEmptyStringsToNullDirectiveResolver.implicit?(field_def)
70
91
  end
71
92
 
93
+ # The steps that change the arguments: empty strings to null and the rewriting
94
+ # directives (@globalId, @trim, @drop) outermost, so every wrapper sees them;
95
+ # the effectful ones (@hash, @bind, @upload) once @guard, @can* and @validator
96
+ # have let the call through (ArgumentTransformsResolver).
97
+ ARGUMENT_PHASES = { rewrite: 'convertEmptyStringsToNull', effects: 'cache' }.freeze
98
+
99
+ def argument_steps(type, field_def, directives, opts)
100
+ steps = []
101
+ steps << empty_strings_step(type, field_def, opts) if empty_string_conversion?(field_def, directives)
102
+ ARGUMENT_PHASES.each do |phase, alongside|
103
+ next unless Lighthouse::GraphQL::DirectiveResolvers::ArgumentTransformsResolver.transforms?(field_def, phase)
104
+
105
+ steps << transforms_step(type, field_def, opts.merge(phase: phase), Lighthouse::DirectiveRegistry.default.resolution_order(alongside))
106
+ end
107
+ steps
108
+ end
109
+
110
+ # Around whatever resolves the field, as @lazyLoad: the result is cut to the client's @limit.
111
+ def limit_step(type, field_def, opts)
112
+ [1, -> { Lighthouse::GraphQL::DirectiveResolvers::LimitResolver.new(field_def, type, nil, opts).define_resolver(type) }]
113
+ end
114
+
115
+ def transforms_step(type, field_def, opts, order)
116
+ [order, -> { Lighthouse::GraphQL::DirectiveResolvers::ArgumentTransformsResolver.new(field_def, type, nil, opts).define_resolver(type) }]
117
+ end
118
+
72
119
  def empty_strings_step(type, field_def, opts)
73
120
  resolver = Lighthouse::GraphQL::DirectiveResolvers::ConvertEmptyStringsToNullDirectiveResolver
74
121
  [Lighthouse::DirectiveRegistry.default.resolution_order(resolver.directive_name),
@@ -25,6 +25,7 @@ module Lighthouse::GraphQL::GeneratedResolvers
25
25
 
26
26
  # Generated resolvers take either (obj, args) or (obj, args, ctx).
27
27
  def invoke(resolver, obj, args, ctx)
28
+ args = take_lookahead(args, ctx)
28
29
  return resolver.call(obj, args) if resolver.parameters.length == 2
29
30
 
30
31
  resolver.call(obj, args, ctx)
@@ -37,13 +38,7 @@ module Lighthouse::GraphQL::GeneratedResolvers
37
38
  def install_dispatcher(schema)
38
39
  return unless schema.respond_to?(:definition_default_resolve=) && schema.respond_to?(:definition_default_resolve)
39
40
 
40
- previous_default = schema.definition_default_resolve
41
- schema.definition_default_resolve = proc do |type, field, obj, arguments, ctx|
42
- resolver = generated_resolver(schema, type, field)
43
- next invoke(resolver, obj, arguments, ctx) if resolver
44
-
45
- previous_default.call(type, field, obj, arguments, ctx)
46
- end
41
+ schema.definition_default_resolve = Lighthouse::GraphQL::DefaultResolveDispatcher.new(schema, schema.definition_default_resolve)
47
42
  end
48
43
 
49
44
  # Our generated resolver for `field`, looked up on the schema's own type
@@ -65,6 +60,16 @@ module Lighthouse::GraphQL::GeneratedResolvers
65
60
  end
66
61
  end
67
62
 
63
+ # The arguments without graphql-ruby's lookahead extra, which a field resolved
64
+ # by a class asks for (ResolveInfo.request_lookahead): it is kept in the
65
+ # field's scoped context, for its ResolveInfo, and no wrapper sees it.
66
+ def take_lookahead(args, ctx)
67
+ return args unless args.is_a?(Hash) && args[:lookahead].is_a?(::GraphQL::Execution::Lookahead)
68
+
69
+ ctx.scoped_set!(Lighthouse::GraphQL::ResolveInfo::LOOKAHEAD, args[:lookahead]) if ctx.respond_to?(:scoped_set!)
70
+ args.except(:lookahead)
71
+ end
72
+
68
73
  # GraphQL-Ruby's SDL builder defines `resolve_<field>` methods that call
69
74
  # the schema's `definition_default_resolve`. Override `resolve_<field>`
70
75
  # to delegate to our generated `resolve_field_<field>` when present, so
@@ -74,7 +79,7 @@ module Lighthouse::GraphQL::GeneratedResolvers
74
79
  return unless type.respond_to?(generated)
75
80
 
76
81
  type.define_singleton_method("resolve_#{field_def.name}") do |obj, arguments, ctx|
77
- public_send(generated, obj, arguments, ctx)
82
+ public_send(generated, obj, Lighthouse::GraphQL::GeneratedResolvers.take_lookahead(arguments, ctx), ctx)
78
83
  end
79
84
  rescue StandardError
80
85
  # ignore override failures
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ # Relay global ids, as PHP Lighthouse's Base64GlobalId writes them: the type
6
+ # name and the id, joined by a colon, in base64 ("User:1" -> "VXNlcjox").
7
+ # Config.global_id takes another object with `encode(type, id)` and
8
+ # `decode(global_id)` -> [type, id], as PHP Lighthouse lets the GlobalId
9
+ # contract be rebound.
10
+ module Lighthouse::GraphQL::GlobalId
11
+ module_function
12
+
13
+ def encode(type, id)
14
+ Base64.strict_encode64("#{type}:#{id}")
15
+ end
16
+
17
+ def decode(global_id)
18
+ Base64.strict_decode64(global_id.to_s).split(':', 2)
19
+ rescue ArgumentError
20
+ []
21
+ end
22
+
23
+ # [type, id] for a global id a client sent, through Config.global_id; a
24
+ # GraphQL::ExecutionError when it is not one.
25
+ def decode!(global_id)
26
+ type, id = Lighthouse::Config.global_id.decode(global_id)
27
+ return [type, id] if type.present? && !id.nil?
28
+
29
+ raise ::GraphQL::ExecutionError, "#{global_id.inspect} is not a valid global id."
30
+ end
31
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lighthouse
4
4
  module GraphQL
5
- VERSION = '0.4.0'
5
+ VERSION = '0.5.0'
6
6
  end
7
7
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ # The GraphQL multipart request specification
6
+ # (https://github.com/jaydenseric/graphql-multipart-request-spec), which file
7
+ # uploads use: a multipart form with `operations` (the JSON operation, or a
8
+ # batch of them), `map` (each file's form key => the paths in `operations` it
9
+ # goes to, e.g. { "0": ["variables.avatar"] }) and the files. The controller
10
+ # concern reads it into the operation(s), with every file in place.
11
+ module Lighthouse::GraphQL::MultipartRequest
12
+ module_function
13
+
14
+ # The operation (a Hash) or batch (an Array), with the files in place; nil
15
+ # when the request is not a multipart one.
16
+ def operations(params)
17
+ raw = param(params, 'operations')
18
+ return unless raw
19
+
20
+ operations = parse(raw)
21
+ parse(param(params, 'map') || '{}').each do |key, paths|
22
+ Array(paths).each { |path| place(operations, path.to_s.split('.'), param(params, key)) }
23
+ end
24
+ operations
25
+ end
26
+
27
+ def param(params, key)
28
+ params[key.to_s] || params[key.to_sym]
29
+ end
30
+
31
+ def parse(value)
32
+ value = value.to_unsafe_h if value.respond_to?(:to_unsafe_h)
33
+ value.is_a?(String) ? JSON.parse(value) : value
34
+ end
35
+
36
+ def place(target, segments, file)
37
+ *parents, last = segments
38
+ container = parents.reduce(target) { |node, segment| node.is_a?(Array) ? node[Integer(segment)] : node[segment] }
39
+ raise ArgumentError, "The multipart map names a path that is not in the operations: #{segments.join('.')}" unless container
40
+
41
+ container.is_a?(Array) ? container[Integer(last)] = file : container[last] = file
42
+ end
43
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Resolves Relay's `Query.node(id:)`, which @node adds, as PHP Lighthouse's
4
+ # NodeRegistry: the id is decoded into a type and an id, and the type must be
5
+ # one with @node. Its object comes from, in order:
6
+ #
7
+ # - `@node(resolver: "Class@method")`: the class (method `call` without `@`),
8
+ # looked up as @field looks a class up on a type, called with the id and the
9
+ # context;
10
+ # - `@node(model: "Name")`, or else the type's @model, or else the model named
11
+ # after the type:
12
+ # the record with that primary key, batched across every `node` in the query.
13
+ #
14
+ # It also tells the Node interface which type each object is (resolve_type):
15
+ # `node` hands back the object with the type its id named, so a `resolver:` may
16
+ # return any object, a Hash included, and one record may be two node types.
17
+ class Lighthouse::GraphQL::NodeRegistry
18
+ # What `node` returns: the object, and the type its global id named.
19
+ Found = Struct.new(:type, :object)
20
+
21
+ def self.resolve(_root, args, context, _info = nil)
22
+ type_name, id = args[:id]
23
+ type = context.schema.get_type(type_name.to_s)
24
+ raise ::GraphQL::ExecutionError, "[#{type_name}] is not a type that can be resolved by node." unless node_type?(type)
25
+
26
+ object = find(type, id, context)
27
+ object.nil? ? nil : Found.new(type, object)
28
+ end
29
+
30
+ # [type, object] for what `node` found; nil leaves any other Node value to the default.
31
+ def self.resolve_type(object, _context)
32
+ [object.type, object.object] if object.is_a?(Found)
33
+ end
34
+
35
+ def self.node_type?(type)
36
+ type&.kind&.object? && !directive(type).nil?
37
+ end
38
+
39
+ def self.directive(type)
40
+ (type.ast_node&.directives || []).find { |d| d.name == 'node' }
41
+ end
42
+
43
+ def self.find(type, id, context)
44
+ node = directive(type)
45
+ resolver = Lighthouse::Support::DirectiveArgs.literal(node, 'resolver')
46
+ return call_resolver(resolver, id, context) if resolver
47
+
48
+ model = model_for(type, Lighthouse::Support::DirectiveArgs.literal(node, 'model'))
49
+ raise Lighthouse::DefinitionError, "@node on #{type.graphql_name} finds no model: give it model: or resolver:." unless model
50
+
51
+ context.dataloader.with(::GraphQL::Dataloader::ActiveRecordSource, model).load(model.type_for_attribute(model.primary_key).cast(id))
52
+ end
53
+
54
+ def self.call_resolver(reference, id, context)
55
+ class_name, method = Lighthouse::GraphQL::DirectiveResolvers::FieldDirectiveResolver.reference(reference)
56
+ klass = Lighthouse::GraphQL::DirectiveResolvers::FieldDirectiveResolver.lookup(class_name, nil)
57
+ raise Lighthouse::DefinitionError, "@node(resolver: #{reference.inspect}) names no class." unless klass
58
+
59
+ receiver = Lighthouse::GraphQL::ResolverDispatch.callable_receiver(klass, method || :call)
60
+ receiver.public_send(method || :call, id, context)
61
+ end
62
+
63
+ def self.model_for(type, name)
64
+ return Lighthouse::Support::ModelResolver.constantize(name) if name
65
+
66
+ Lighthouse::GraphQL::Directives::ModelDirective.model_for(type)
67
+ end
68
+ end
@@ -110,6 +110,9 @@ module Lighthouse::GraphQL::Pagination
110
110
  @items = items
111
111
  end
112
112
 
113
+ # The records on the page, for @lazyLoad.
114
+ attr_reader :items
115
+
113
116
  def marshal_dump
114
117
  [info, @items]
115
118
  end
@@ -29,6 +29,8 @@ require_relative 'namespaces'
29
29
  support/arguments
30
30
  support/batching
31
31
  support/soft_deletes
32
+ support/preload
33
+ support/environment
32
34
  where_conditions/operator_map
33
35
  contracts/directive
34
36
  contracts/field_resolver
@@ -38,6 +40,7 @@ require_relative 'namespaces'
38
40
  contracts/arg_builder
39
41
  sdl_loader
40
42
  generated_resolvers
43
+ default_resolve_dispatcher
41
44
  federation_directives
42
45
  field_resolver_stack
43
46
  events
@@ -60,12 +63,26 @@ require_relative 'namespaces'
60
63
  complexity_analyzer
61
64
  broadcasting
62
65
  subscription
66
+ type
67
+ type_resolution
68
+ resolve_info
69
+ global_id
70
+ enum_directive
71
+ upload
72
+ upload_store
73
+ multipart_request
74
+ node_registry
75
+ scalar_coercion
63
76
  pagination
64
77
  relation_batch
78
+ relation_counts
65
79
  nested_mutations
66
80
  directives/pagination_manipulator
67
81
  directives/where_conditions_directive
68
82
  directives/soft_deletes_directive
83
+ directives/node_directive
84
+ directives/model_directive
85
+ directives/visibility_directive
69
86
  directive_resolvers/aggregate_resolver
70
87
  directive_resolvers/field_resolver
71
88
  directive_resolvers/where_conditions_applier
@@ -83,6 +100,7 @@ require_relative 'namespaces'
83
100
  directive_resolvers/association_resolvers
84
101
  directive_resolvers/can_resolvers
85
102
  directive_resolvers/convention_resolver
103
+ directive_resolvers/type_method_resolver
86
104
  directive_resolvers/model_mutation_resolver
87
105
  directive_resolvers/soft_delete_resolvers
88
106
  directive_resolvers/cache_resolvers
@@ -91,15 +109,29 @@ require_relative 'namespaces'
91
109
  directive_resolvers/broadcast_resolver
92
110
  directive_resolvers/event_resolver
93
111
  directive_resolvers/convert_empty_strings_resolver
112
+ directive_resolvers/with_resolver
113
+ directive_resolvers/with_count_resolver
114
+ directive_resolvers/lazy_load_resolver
115
+ directive_resolvers/limit_resolver
116
+ directive_resolvers/namespaced_resolver
117
+ directive_resolvers/throttle_resolver
118
+ directive_resolvers/async_resolver
119
+ directive_resolvers/global_id_resolver
120
+ directive_resolvers/argument_binding
121
+ directive_resolvers/argument_transforms_resolver
94
122
  directives/arguments/base
95
123
  directives/arguments/filters
124
+ directives/arguments/more_filters
96
125
  directives/arguments/order_by
97
126
  directives/arguments/soft_deletes
127
+ directives/definitions
98
128
  directives/registry
99
129
  dataloader_sources/association_loader
100
130
  dataloader_sources/model_loader
101
131
  dataloader_sources/relation_page_loader
102
132
  dataloader_sources/relation_count_loader
133
+ dataloader_sources/relation_counts_loader
134
+ dataloader_sources/preload_loader
103
135
  ide_helper
104
136
  ].each do |rel|
105
137
  require_relative rel
@@ -109,6 +141,7 @@ end
109
141
 
110
142
  # The activejob gem is optional: the job that queues @broadcast loads on first use.
111
143
  Lighthouse::GraphQL.autoload(:BroadcastJob, File.expand_path('broadcast_job.rb', __dir__))
144
+ Lighthouse::GraphQL.autoload(:AsyncMutationJob, File.expand_path('async_mutation_job.rb', __dir__))
112
145
 
113
146
  require_relative 'railtie' if defined?(Rails::Railtie)
114
147
 
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The counts @withCount loads, read off the record as Laravel's withCount
4
+ # attributes are: `user.posts_count`. The record is extended with this module
5
+ # (named, so it still marshals for @cache) and keeps them in an instance
6
+ # variable; a real column of the same name wins.
7
+ module Lighthouse::GraphQL::RelationCounts
8
+ def self.write(record, relation, count)
9
+ record.extend(self) unless record.is_a?(self)
10
+ counts = record.instance_variable_get(:@lighthouse_relation_counts) || {}
11
+ record.instance_variable_set(:@lighthouse_relation_counts, counts.merge("#{relation}_count" => count))
12
+ end
13
+
14
+ def respond_to_missing?(name, include_private = false)
15
+ lighthouse_relation_count?(name) || super
16
+ end
17
+
18
+ def method_missing(name, *args, &block)
19
+ return super unless args.empty? && lighthouse_relation_count?(name)
20
+
21
+ @lighthouse_relation_counts[name.to_s]
22
+ end
23
+
24
+ private
25
+
26
+ def lighthouse_relation_count?(name)
27
+ @lighthouse_relation_counts&.key?(name.to_s)
28
+ end
29
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ # What PHP Lighthouse hands a resolver as its fourth argument, `$resolveInfo`:
4
+ # the field being resolved, where, with what, what the client selects below it
5
+ # (`lookahead`), and `enhance_builder`, which applies the field's own query
6
+ # directives to a query the resolver builds.
7
+ #
8
+ # type Query {
9
+ # posts(title: String @eq, orderBy: [PostOrderByClause!] @orderBy): [Post!]! @builder(class: "Published")
10
+ # }
11
+ #
12
+ # class Resolvers::Posts < Resolvers::BaseResolver
13
+ # def resolve(**)
14
+ # resolve_info.enhance_builder(Post.where(author: context[:current_user]), scopes: [:recent])
15
+ # end
16
+ # end
17
+ #
18
+ # A Resolvers:: / Mutations:: class, a class @field names and a type class's
19
+ # methods read it as `resolve_info`; a plain callable gets it as a fourth
20
+ # argument when it takes one: `call(object, args, context, resolve_info)`.
21
+ class Lighthouse::GraphQL::ResolveInfo
22
+ LOOKAHEAD = :lighthouse_lookahead
23
+
24
+ attr_reader :field, :parent_type, :object, :arguments, :context
25
+
26
+ # graphql-ruby's GraphQL::Execution::Lookahead for the field: what the client
27
+ # selects below it, to load only that (`lookahead.selects?(:author)`,
28
+ # `lookahead.selection(:comments).arguments`). nil outside a query.
29
+ attr_reader :lookahead
30
+
31
+ # A field resolved by a class asks graphql-ruby for its lookahead.
32
+ def self.request_lookahead(field_def)
33
+ field_def.extras([:lookahead]) unless field_def.extras.include?(:lookahead)
34
+ end
35
+
36
+ def initialize(field:, parent_type:, object:, arguments:, context:)
37
+ @field = field
38
+ @parent_type = parent_type
39
+ @object = object
40
+ @arguments = arguments
41
+ @context = context
42
+ @lookahead = context.respond_to?(:[]) ? context[LOOKAHEAD] : nil
43
+ end
44
+
45
+ def field_name = field.graphql_name
46
+
47
+ def return_type = field.type
48
+
49
+ # The path from the root to this field, as graphql-ruby tracks it: ["posts", 0, "author"].
50
+ def path
51
+ context.respond_to?(:[]) ? context[:current_path] : nil
52
+ end
53
+
54
+ # `relation` with the field's model `scopes`, its @builder classes, and the
55
+ # argument directives the client used (@eq, @where, @in, @like, @orderBy,
56
+ # @whereConditions, @trashed, ...), as @all / @paginate apply them.
57
+ def enhance_builder(relation, scopes: [])
58
+ resolver = Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver.new(field, parent_type, nil)
59
+ args = arguments.respond_to?(:to_h) ? arguments.to_h : {}
60
+ relation = Array(scopes).reduce(relation) { |acc, scope| acc.public_send(scope) }
61
+ relation = Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier.apply(relation, field.ast_node, args, context, resolver)
62
+ resolver.apply_where_filters(relation, args, context)
63
+ end
64
+
65
+ # `resolve_info`, for the classes that resolve a field.
66
+ module Accessor
67
+ def resolve_info = @lighthouse_resolve_info
68
+ end
69
+
70
+ # Hand `info` to `resolver` (a GraphQL::Schema::Resolver or type class instance).
71
+ def self.for(field_def, parent_type, object, arguments, context)
72
+ new(field: field_def, parent_type: parent_type, object: object, arguments: arguments, context: context)
73
+ end
74
+
75
+ def self.attach(resolver, info)
76
+ resolver.extend(Accessor) unless resolver.respond_to?(:resolve_info)
77
+ resolver.instance_variable_set(:@lighthouse_resolve_info, info)
78
+ resolver
79
+ end
80
+ end
@@ -31,8 +31,13 @@ module Lighthouse::GraphQL::ResolverBases
31
31
 
32
32
  module_function
33
33
 
34
+ # graphql-ruby's base types that the type classes of the same kind
35
+ # (Types::PostType, Types::DateTimeType, ...) inherit from too.
36
+ TYPE_KINDS = %i[object union interface scalar].freeze
37
+
34
38
  # Take the three keys out of opts[:base_types] (the caller's Hash is not
35
- # changed) and return { root kind => base class } for those given.
39
+ # changed) and return { root kind => base class } for those given, plus
40
+ # :types => { kind => base } for graphql-ruby's own, which stay in opts.
36
41
  def extract!(opts)
37
42
  base_types = opts[:base_types]
38
43
  return {} unless base_types.is_a?(Hash)
@@ -40,7 +45,9 @@ module Lighthouse::GraphQL::ResolverBases
40
45
  given = base_types.slice(*GRAPHQL_CLASSES.keys).compact
41
46
  opts[:base_types] = base_types.except(*GRAPHQL_CLASSES.keys)
42
47
  given.each { |key, klass| validate!(key, klass) }
43
- KEYS.filter_map { |kind, key| [kind, given[key]] if given[key] }.to_h
48
+ bases = KEYS.filter_map { |kind, key| [kind, given[key]] if given[key] }.to_h
49
+ type_bases = base_types.slice(*TYPE_KINDS).compact
50
+ type_bases.empty? ? bases : bases.merge(types: type_bases)
44
51
  end
45
52
 
46
53
  def validate!(key, klass)