lighthouse-graphql 0.4.0 → 0.6.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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +256 -1
  3. data/README.md +26 -15
  4. data/docs/caching.md +4 -3
  5. data/docs/configuration.md +16 -12
  6. data/docs/directives.md +294 -4
  7. data/docs/generators.md +84 -0
  8. data/docs/getting-started.md +93 -60
  9. data/docs/performance.md +2 -2
  10. data/docs/relationships.md +47 -4
  11. data/docs/relay.md +200 -0
  12. data/docs/resolvers.md +326 -34
  13. data/docs/security.md +73 -8
  14. data/docs/subscriptions.md +16 -9
  15. data/docs/types.md +218 -0
  16. data/docs/uploads.md +107 -0
  17. data/lib/generators/lighthouse/base_classes.rb +27 -49
  18. data/lib/generators/lighthouse/install/graphql_ruby_install.rb +113 -0
  19. data/lib/generators/lighthouse/install/install_generator.rb +125 -45
  20. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +10 -8
  21. data/lib/lighthouse/async_mutation_job.rb +17 -0
  22. data/lib/lighthouse/caching.rb +9 -3
  23. data/lib/lighthouse/complexity.rb +5 -4
  24. data/lib/lighthouse/config.rb +12 -18
  25. data/lib/lighthouse/controller.rb +53 -12
  26. data/lib/lighthouse/dataloader_sources/preload_loader.rb +16 -0
  27. data/lib/lighthouse/dataloader_sources/relation_counts_loader.rb +46 -0
  28. data/lib/lighthouse/default_resolve_dispatcher.rb +26 -0
  29. data/lib/lighthouse/directive_resolvers/all_resolver.rb +1 -1
  30. data/lib/lighthouse/directive_resolvers/argument_binding.rb +61 -0
  31. data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +220 -0
  32. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +2 -2
  33. data/lib/lighthouse/directive_resolvers/async_resolver.rb +90 -0
  34. data/lib/lighthouse/directive_resolvers/base.rb +63 -14
  35. data/lib/lighthouse/directive_resolvers/builder_applier.rb +89 -19
  36. data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +1 -1
  37. data/lib/lighthouse/directive_resolvers/can_resolvers.rb +1 -1
  38. data/lib/lighthouse/directive_resolvers/convention_resolver.rb +2 -1
  39. data/lib/lighthouse/directive_resolvers/count_resolver.rb +1 -1
  40. data/lib/lighthouse/directive_resolvers/field_resolver.rb +35 -7
  41. data/lib/lighthouse/directive_resolvers/first_resolver.rb +1 -1
  42. data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +59 -0
  43. data/lib/lighthouse/directive_resolvers/lazy_load_resolver.rb +61 -0
  44. data/lib/lighthouse/directive_resolvers/limit_resolver.rb +59 -0
  45. data/lib/lighthouse/directive_resolvers/method_resolver.rb +38 -3
  46. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +1 -1
  47. data/lib/lighthouse/directive_resolvers/namespaced_resolver.rb +31 -0
  48. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +1 -1
  49. data/lib/lighthouse/directive_resolvers/resolution.rb +19 -4
  50. data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +2 -1
  51. data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +136 -0
  52. data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +87 -0
  53. data/lib/lighthouse/directive_resolvers/with_count_resolver.rb +43 -0
  54. data/lib/lighthouse/directive_resolvers/with_resolver.rb +42 -0
  55. data/lib/lighthouse/directives/arguments/more_filters.rb +215 -0
  56. data/lib/lighthouse/directives/definitions.rb +52 -0
  57. data/lib/lighthouse/directives/model_directive.rb +131 -0
  58. data/lib/lighthouse/directives/node_directive.rb +134 -0
  59. data/lib/lighthouse/directives/pagination_manipulator.rb +1 -1
  60. data/lib/lighthouse/directives/registry.rb +21 -21
  61. data/lib/lighthouse/directives/visibility_directive.rb +71 -0
  62. data/lib/lighthouse/enum_directive.rb +41 -0
  63. data/lib/lighthouse/field_resolver_stack.rb +56 -6
  64. data/lib/lighthouse/generated_resolvers.rb +13 -8
  65. data/lib/lighthouse/global_id.rb +57 -0
  66. data/lib/lighthouse/graphql/version.rb +1 -1
  67. data/lib/lighthouse/graphql_ruby_patches.rb +45 -0
  68. data/lib/lighthouse/multipart_request.rb +43 -0
  69. data/lib/lighthouse/node_registry.rb +68 -0
  70. data/lib/lighthouse/pagination.rb +3 -0
  71. data/lib/lighthouse/rb_lighthouse.rb +34 -0
  72. data/lib/lighthouse/relation_counts.rb +29 -0
  73. data/lib/lighthouse/resolve_info.rb +80 -0
  74. data/lib/lighthouse/resolver_bases.rb +25 -4
  75. data/lib/lighthouse/resolver_dispatch.rb +51 -11
  76. data/lib/lighthouse/root_types.rb +117 -0
  77. data/lib/lighthouse/scalar_coercion.rb +72 -0
  78. data/lib/lighthouse/schema_cache.rb +2 -1
  79. data/lib/lighthouse/schema_generator.rb +13 -11
  80. data/lib/lighthouse/schema_implementation.rb +4 -8
  81. data/lib/lighthouse/support/environment.rb +13 -0
  82. data/lib/lighthouse/support/model_resolver.rb +7 -5
  83. data/lib/lighthouse/support/preload.rb +49 -0
  84. data/lib/lighthouse/type.rb +103 -0
  85. data/lib/lighthouse/type_resolution.rb +150 -0
  86. data/lib/lighthouse/upload.rb +28 -0
  87. data/lib/lighthouse/upload_store.rb +57 -0
  88. data/sig/lighthouse/directives.rbs +3 -2
  89. data/sig/lighthouse.rbs +10 -2
  90. metadata +40 -4
  91. data/lib/generators/lighthouse/install/templates/schema.rb.tt +0 -22
  92. data/lib/generators/lighthouse/mutation_generator.rb +0 -31
  93. data/lib/generators/lighthouse/query_generator.rb +0 -30
@@ -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
@@ -45,6 +48,7 @@ require_relative 'namespaces'
45
48
  events/execution_trace
46
49
  events/schema_build
47
50
  resolver_bases
51
+ root_types
48
52
  schema_generator
49
53
  schema_cache
50
54
  persisted_queries
@@ -60,12 +64,26 @@ require_relative 'namespaces'
60
64
  complexity_analyzer
61
65
  broadcasting
62
66
  subscription
67
+ type
68
+ type_resolution
69
+ resolve_info
70
+ global_id
71
+ enum_directive
72
+ upload
73
+ upload_store
74
+ multipart_request
75
+ node_registry
76
+ scalar_coercion
63
77
  pagination
64
78
  relation_batch
79
+ relation_counts
65
80
  nested_mutations
66
81
  directives/pagination_manipulator
67
82
  directives/where_conditions_directive
68
83
  directives/soft_deletes_directive
84
+ directives/node_directive
85
+ directives/model_directive
86
+ directives/visibility_directive
69
87
  directive_resolvers/aggregate_resolver
70
88
  directive_resolvers/field_resolver
71
89
  directive_resolvers/where_conditions_applier
@@ -83,6 +101,7 @@ require_relative 'namespaces'
83
101
  directive_resolvers/association_resolvers
84
102
  directive_resolvers/can_resolvers
85
103
  directive_resolvers/convention_resolver
104
+ directive_resolvers/type_method_resolver
86
105
  directive_resolvers/model_mutation_resolver
87
106
  directive_resolvers/soft_delete_resolvers
88
107
  directive_resolvers/cache_resolvers
@@ -91,15 +110,29 @@ require_relative 'namespaces'
91
110
  directive_resolvers/broadcast_resolver
92
111
  directive_resolvers/event_resolver
93
112
  directive_resolvers/convert_empty_strings_resolver
113
+ directive_resolvers/with_resolver
114
+ directive_resolvers/with_count_resolver
115
+ directive_resolvers/lazy_load_resolver
116
+ directive_resolvers/limit_resolver
117
+ directive_resolvers/namespaced_resolver
118
+ directive_resolvers/throttle_resolver
119
+ directive_resolvers/async_resolver
120
+ directive_resolvers/global_id_resolver
121
+ directive_resolvers/argument_binding
122
+ directive_resolvers/argument_transforms_resolver
94
123
  directives/arguments/base
95
124
  directives/arguments/filters
125
+ directives/arguments/more_filters
96
126
  directives/arguments/order_by
97
127
  directives/arguments/soft_deletes
128
+ directives/definitions
98
129
  directives/registry
99
130
  dataloader_sources/association_loader
100
131
  dataloader_sources/model_loader
101
132
  dataloader_sources/relation_page_loader
102
133
  dataloader_sources/relation_count_loader
134
+ dataloader_sources/relation_counts_loader
135
+ dataloader_sources/preload_loader
103
136
  ide_helper
104
137
  ].each do |rel|
105
138
  require_relative rel
@@ -109,6 +142,7 @@ end
109
142
 
110
143
  # The activejob gem is optional: the job that queues @broadcast loads on first use.
111
144
  Lighthouse::GraphQL.autoload(:BroadcastJob, File.expand_path('broadcast_job.rb', __dir__))
145
+ Lighthouse::GraphQL.autoload(:AsyncMutationJob, File.expand_path('async_mutation_job.rb', __dir__))
112
146
 
113
147
  require_relative 'railtie' if defined?(Rails::Railtie)
114
148
 
@@ -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
@@ -18,6 +18,11 @@
18
18
  # it is first used, so none misses what the base shares;
19
19
  # - for subscriptions, also resolves a Subscription field that has no class of
20
20
  # its own, instead of Lighthouse::GraphQL::Subscription::Default.
21
+ #
22
+ # The code-first types graphql-ruby's install writes are bases too (RootTypes):
23
+ # `query_type:` / `mutation_type:` / `subscription_type:` (Types::QueryType, ...)
24
+ # are what the SDL's root types are built on, so their `field`s are the
25
+ # schema's too, and `node:` (Types::NodeType) is the SDL's Node interface.
21
26
  module Lighthouse::GraphQL::ResolverBases
22
27
  # base_types key => the graphql-ruby class the base must inherit from.
23
28
  GRAPHQL_CLASSES = {
@@ -31,16 +36,32 @@ module Lighthouse::GraphQL::ResolverBases
31
36
 
32
37
  module_function
33
38
 
34
- # 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
+ # graphql-ruby's base types that the type classes of the same kind
40
+ # (Types::PostType, Types::DateTimeType, ...) inherit from too.
41
+ TYPE_KINDS = %i[object union interface scalar].freeze
42
+
43
+ # Take Lighthouse's keys out of opts[:base_types] (the caller's Hash is not
44
+ # changed) and return { root kind => base class } for those given, plus
45
+ # :types => { kind => base } for graphql-ruby's own, which stay in opts, and
46
+ # :roots => { query_type: ..., node: ... } for the code-first types (RootTypes).
36
47
  def extract!(opts)
37
48
  base_types = opts[:base_types]
38
49
  return {} unless base_types.is_a?(Hash)
39
50
 
40
51
  given = base_types.slice(*GRAPHQL_CLASSES.keys).compact
41
- opts[:base_types] = base_types.except(*GRAPHQL_CLASSES.keys)
52
+ opts[:base_types] = base_types.except(*GRAPHQL_CLASSES.keys, *Lighthouse::GraphQL::RootTypes::KEYS)
42
53
  given.each { |key, klass| validate!(key, klass) }
43
- KEYS.filter_map { |kind, key| [kind, given[key]] if given[key] }.to_h
54
+ bases = KEYS.filter_map { |kind, key| [kind, given[key]] if given[key] }.to_h
55
+ type_bases = base_types.slice(*TYPE_KINDS).compact
56
+ bases = bases.merge(types: type_bases) unless type_bases.empty?
57
+ with_roots(bases, base_types)
58
+ end
59
+
60
+ # `bases` with the code-first root types given, validated, under :roots.
61
+ def with_roots(bases, base_types)
62
+ roots = base_types.slice(*Lighthouse::GraphQL::RootTypes::KEYS).compact
63
+ roots.each { |key, type| Lighthouse::GraphQL::RootTypes.validate!(key, type) }
64
+ roots.empty? ? bases : bases.merge(roots: roots)
44
65
  end
45
66
 
46
67
  def validate!(key, klass)
@@ -4,7 +4,12 @@
4
4
  # namespaces follow graphql-ruby's contract — GraphQL::Schema::Resolver and
5
5
  # GraphQL::Schema::Mutation: `resolve(**args)` with `object` and `context`,
6
6
  # plus `ready?` / `authorized?` — exactly as in a graphql-ruby schema.
7
- # @field also accepts a plain callable, `call(object, args, context)`.
7
+ # @field also accepts a plain callable, `call(object, args, context)`, which
8
+ # gets the ResolveInfo as a fourth argument when it takes one.
9
+ #
10
+ # `method:` is the method after `@` in `@field(resolver: "Class@method")`: it
11
+ # replaces `resolve` (keeping `ready?` / `authorized?`) on a resolver class, and
12
+ # `call` on a plain class, as a class method or else an instance method.
8
13
  module Lighthouse::GraphQL::ResolverDispatch
9
14
  module_function
10
15
 
@@ -12,15 +17,50 @@ module Lighthouse::GraphQL::ResolverDispatch
12
17
  klass.is_a?(Class) && klass < ::GraphQL::Schema::Resolver
13
18
  end
14
19
 
15
- def call(klass, field_def, obj, args, ctx)
16
- if graphql_resolver?(klass)
17
- kwargs = Lighthouse::Support::Arguments.prepare(field_def, args, ctx)
18
- klass.new(object: obj, context: ctx, field: field_def).resolve_with_support(**kwargs)
19
- elsif klass.respond_to?(:call)
20
- klass.call(obj, args, ctx)
21
- else
22
- instance = klass.new
23
- instance.respond_to?(:call) ? instance.call(obj, args, ctx) : instance.resolve(obj, args, ctx)
24
- end
20
+ # `info`: the ResolveInfo of the field being resolved, which carries its
21
+ # object, arguments and context.
22
+ def call(klass, info, method: nil)
23
+ return call_resolver(klass, info, method) if graphql_resolver?(klass)
24
+
25
+ receiver = callable_receiver(klass, method || :call)
26
+ callable = receiver.method(method || (receiver.respond_to?(:call) ? :call : :resolve))
27
+ args = [info.object, info.arguments, info.context]
28
+ takes_info?(receiver, callable) ? callable.call(*args, info) : callable.call(*args)
29
+ end
30
+
31
+ def call_resolver(klass, info, method)
32
+ kwargs = Lighthouse::Support::Arguments.prepare(info.field, info.arguments, info.context)
33
+ instance = Lighthouse::GraphQL::ResolveInfo.attach(klass.new(object: info.object, context: info.context, field: info.field), info)
34
+ instance.define_singleton_method(:resolve) { |**values| public_send(method, **values) } if method
35
+ single_input?(instance, kwargs) ? instance.resolve_with_support(**kwargs) : resolve_as_mutation(instance, kwargs)
36
+ end
37
+
38
+ # graphql-ruby's RelayClassicMutation (the base mutation its install writes)
39
+ # takes one `input:` argument. A field that declares its arguments one by one
40
+ # calls it as a plain GraphQL::Schema::Mutation, with them as keywords: past
41
+ # HasSingleInputArgument's unwrapping of `input:`, and its authorization of
42
+ # the arguments of `input:`.
43
+ def single_input?(instance, kwargs)
44
+ !instance.is_a?(::GraphQL::Schema::HasSingleInputArgument) || kwargs.key?(:input)
45
+ end
46
+
47
+ def resolve_as_mutation(instance, kwargs)
48
+ instance.define_singleton_method(:authorize_arguments, ::GraphQL::Schema::Resolver.instance_method(:authorize_arguments))
49
+ instance.singleton_class.send(:private, :authorize_arguments)
50
+ ::GraphQL::Schema::Mutation.instance_method(:resolve_with_support).bind_call(instance, **kwargs)
51
+ end
52
+
53
+ # The class itself when it answers `name` as a class method, else an instance.
54
+ def callable_receiver(klass, name)
55
+ klass.respond_to?(name) ? klass : klass.new
56
+ end
57
+
58
+ POSITIONAL = %i[req opt].freeze
59
+
60
+ # Does the callable take a fourth argument? A lambda's own parameters count,
61
+ # not Proc#call's (*args).
62
+ def takes_info?(receiver, callable)
63
+ params = receiver.is_a?(Proc) && callable.name == :call ? receiver.parameters : callable.parameters
64
+ params.any? { |type, _| type == :rest } || params.count { |type, _| POSITIONAL.include?(type) } >= 4
25
65
  end
26
66
  end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The code-first types graphql-ruby's install writes, which a schema builds on:
4
+ #
5
+ # base_types: { query_type: Types::QueryType, mutation_type: Types::MutationType,
6
+ # subscription_type: Types::SubscriptionType, node: Types::NodeType }
7
+ #
8
+ # - A root type given is what the SDL's root type of that kind is built on,
9
+ # instead of base_types[:object]: the SDL's `type Query` is a subclass of
10
+ # Types::QueryType, so the `field`s the class declares (graphql-ruby's
11
+ # `node` / `nodes`, what `graphql:mutation` adds to Types::MutationType)
12
+ # are the schema's too, beside the SDL's, and resolve as in graphql-ruby. An
13
+ # SDL field of the same name wins. With no such root type in the SDL, the
14
+ # class itself is the root, once it declares a field.
15
+ # - `node:` is the SDL's Node interface: `interface Node`, which @node adds and
16
+ # `implements Node` names, is that module, so graphql-ruby's Relay
17
+ # (Query.node / nodes, the schema's id_from_object / object_from_id) and the
18
+ # SDL's @node types share one Node. The `node` field @node adds to Query then
19
+ # gives way to the root type class's own.
20
+ #
21
+ # graphql-ruby's builder knows one base per kind, so the hook into it is a
22
+ # patch (graphql_ruby_patches.rb), which reads the bases of the build in
23
+ # progress from here.
24
+ module Lighthouse::GraphQL::RootTypes
25
+ KEYS = %i[query_type mutation_type subscription_type node].freeze
26
+
27
+ ROOTS = { query_type: :query, mutation_type: :mutation, subscription_type: :subscription }.freeze
28
+
29
+ # The resolver of the `node` field @node adds to Query.
30
+ NODE_RESOLVER = 'Lighthouse::GraphQL::NodeRegistry@resolve'
31
+
32
+ module_function
33
+
34
+ def validate!(key, type)
35
+ valid = key == :node ? Lighthouse::GraphQL::Type.kind_of(type) == :interface : type.is_a?(Class) && type < ::GraphQL::Schema::Object
36
+ return if valid
37
+
38
+ expected = key == :node ? 'a module including GraphQL::Schema::Interface' : 'a subclass of GraphQL::Schema::Object'
39
+ raise Lighthouse::DefinitionError, "base_types[:#{key}] must be #{expected}, not #{type.inspect}."
40
+ end
41
+
42
+ # The root type class the SDL's root type of `root_kind` (:queries,
43
+ # :mutations) is built on, when it is Lighthouse's (Lighthouse::GraphQL::Type):
44
+ # its methods resolve the root fields of their name.
45
+ def type_class(root_kind, roots)
46
+ klass = roots&.[]({ queries: :query_type, mutations: :mutation_type }[root_kind])
47
+ klass if klass&.include?(Lighthouse::GraphQL::Type)
48
+ end
49
+
50
+ # Run the block (the build of `document`) with `roots` as the bases of the
51
+ # types they name.
52
+ def with(document, roots)
53
+ previous = Thread.current[:lighthouse_root_types]
54
+ Thread.current[:lighthouse_root_types] = by_type_name(document, roots || {})
55
+ yield
56
+ ensure
57
+ Thread.current[:lighthouse_root_types] = previous
58
+ end
59
+
60
+ # The class an SDL type of `name` is built on, or nil.
61
+ def base_for(name)
62
+ Thread.current[:lighthouse_root_types]&.[](name)
63
+ end
64
+
65
+ # `definition` without the fields `base` declares itself that @node added.
66
+ def definition_on(definition, base)
67
+ names = field_names(base)
68
+ fields = definition.fields.reject { |field| names.include?(field.name) && added_by_node?(field) }
69
+ fields.size == definition.fields.size ? definition : definition.merge(fields: fields)
70
+ end
71
+
72
+ # The names of the fields a class and its ancestors declare. Not `fields`,
73
+ # which warns about a type with none (graphql-ruby's install leaves
74
+ # Types::MutationType empty once its example field goes).
75
+ def field_names(type)
76
+ type.ancestors.select { |ancestor| ancestor.respond_to?(:own_fields) }.flat_map { |ancestor| ancestor.own_fields.keys }.uniq
77
+ end
78
+
79
+ # The schema the block builds from `document`, with `roots` as the bases of
80
+ # the types they name; then the root type classes the SDL has no type for,
81
+ # and the subscriptions backend an app schema class `use`s (graphql_ruby_patches.rb).
82
+ def build(document, roots, &block)
83
+ schema = with(document, roots, &block)
84
+ apply_missing(schema, roots)
85
+ schema.extend(Lighthouse::GraphQL::InheritedSubscriptionRoot) if schema.respond_to?(:add_subscription_extension_if_necessary, true)
86
+ schema
87
+ end
88
+
89
+ # A root type class the SDL has no type for is the root itself, once it
90
+ # declares a field (graphql-ruby's install writes them with an example one).
91
+ def apply_missing(schema, roots)
92
+ ROOTS.each do |key, root|
93
+ type = roots&.[](key)
94
+ next if type.nil? || schema.public_send(root) || field_names(type).empty?
95
+
96
+ schema.public_send(root, type)
97
+ end
98
+ end
99
+
100
+ def by_type_name(document, roots)
101
+ names = root_names(document)
102
+ mapped = ROOTS.each_with_object({}) { |(key, root), out| out[names[root]] = roots[key] if roots[key] }
103
+ roots[:node] ? mapped.merge('Node' => roots[:node]) : mapped
104
+ end
105
+
106
+ # { query: 'Query', ... }, as the document's `schema { }` names them.
107
+ def root_names(document)
108
+ schema = document.definitions.find { |definition| definition.is_a?(::GraphQL::Language::Nodes::SchemaDefinition) }
109
+ { query: schema&.query || 'Query', mutation: schema&.mutation || 'Mutation', subscription: schema&.subscription || 'Subscription' }
110
+ end
111
+
112
+ def added_by_node?(field)
113
+ field.directives.any? do |directive|
114
+ directive.name == 'field' && Lighthouse::Support::DirectiveArgs.literal(directive, 'resolver') == NODE_RESOLVER
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ # How the values of an SDL scalar (`scalar DateTime`) are read from a query and
4
+ # written to a response, as a graphql-ruby scalar class does it with
5
+ # `coerce_input(value, context)` and `coerce_result(value, context)`. The class
6
+ # is, in order:
7
+ #
8
+ # 1. the one `@scalar(class: "Name")` on the scalar names, looked up in
9
+ # Config.namespaces[:types] (nested names too) and then as written. PHP
10
+ # Lighthouse's directive.
11
+ # 2. Its type class, Types::<Name>Type including Lighthouse::GraphQL::Type: a
12
+ # GraphQL::Schema::Scalar, as graphql-ruby's `rails g graphql:scalar` writes it.
13
+ # 3. Lighthouse's own, for `scalar Upload` (Lighthouse::GraphQL::Upload).
14
+ #
15
+ # A scalar with none passes its values through unchanged. A class raising
16
+ # GraphQL::CoercionError from coerce_input (or returning nil for a value) makes
17
+ # the argument invalid, with that message, as in graphql-ruby.
18
+ module Lighthouse::GraphQL::ScalarCoercion
19
+ DEFINITION = <<~GRAPHQL.chomp
20
+ """
21
+ The class that reads and writes this scalar's values: coerce_input(value, context) and
22
+ coerce_result(value, context), as a GraphQL::Schema::Scalar.
23
+ """
24
+ directive @scalar(class: String!) on SCALAR
25
+ GRAPHQL
26
+
27
+ module_function
28
+
29
+ # Give every SDL scalar that has a class its coercion. The class is looked up
30
+ # again on each value, so code reloading keeps working; `bases` is the
31
+ # schema's base_types[:scalar], which a type class must inherit from.
32
+ def apply(schema, base = nil)
33
+ schema.types.each_value do |type|
34
+ next unless type.kind.scalar? && type.ast_node
35
+
36
+ klass = class_for(type, base)
37
+ install(type, klass) if klass
38
+ end
39
+ end
40
+
41
+ # Scalars Lighthouse brings a class for, when nothing else names one.
42
+ BUILT_IN = { 'Upload' => 'Lighthouse::GraphQL::Upload' }.freeze
43
+
44
+ def class_for(scalar_type, base = nil)
45
+ directive_class(scalar_type) || type_class(scalar_type, base) || BUILT_IN[scalar_type.graphql_name]&.constantize
46
+ end
47
+
48
+ def directive_class(scalar_type)
49
+ directive = (scalar_type.ast_node.directives || []).find { |d| d.name == 'scalar' }
50
+ name = directive && Lighthouse::Support::DirectiveArgs.literal(directive, 'class')
51
+ return unless name
52
+
53
+ klass = Lighthouse::Support::ModelResolver.constantize(name, namespaces: Lighthouse::Config.namespaces[:types], nested: true)
54
+ return klass if klass.respond_to?(:coerce_input) && klass.respond_to?(:coerce_result)
55
+
56
+ raise Lighthouse::DefinitionError,
57
+ "@scalar(class: #{name.inspect}) on #{scalar_type.graphql_name} names no class with coerce_input(value, context) " \
58
+ 'and coerce_result(value, context).'
59
+ end
60
+
61
+ def type_class(scalar_type, base)
62
+ klass = Lighthouse::GraphQL::Type.class_for(scalar_type.graphql_name, :scalar)
63
+ Lighthouse::GraphQL::ResolverBases.check!(klass, base, "the #{scalar_type.graphql_name} scalar") if klass
64
+ klass
65
+ end
66
+
67
+ def install(scalar_type, klass)
68
+ current = -> { klass.name ? klass.name.constantize : klass }
69
+ scalar_type.define_singleton_method(:coerce_input) { |value, ctx| current.call.coerce_input(value, ctx) }
70
+ scalar_type.define_singleton_method(:coerce_result) { |value, ctx| current.call.coerce_result(value, ctx) }
71
+ end
72
+ end
@@ -72,7 +72,8 @@ module Lighthouse::GraphQL::SchemaCache
72
72
  registry.directive_definitions.sort,
73
73
  registry.type_definitions.sort,
74
74
  manipulators,
75
- Lighthouse::Config.pagination_default_count
75
+ Lighthouse::Config.pagination_default_count,
76
+ Lighthouse::Support::Environment.current # @hide / @show
76
77
  ].inspect
77
78
  end
78
79
 
@@ -15,8 +15,8 @@ module Lighthouse::GraphQL::SchemaGenerator
15
15
  obj.respond_to?(field_name) ? obj.public_send(field_name) : nil
16
16
  end
17
17
 
18
- def resolve_type(_abstract_type, _obj, _ctx)
19
- nil
18
+ def resolve_type(abstract_type, obj, ctx)
19
+ Lighthouse::GraphQL::TypeResolution.resolve(ctx.respond_to?(:schema) && ctx.schema ? ctx.schema : schema, abstract_type, obj, ctx)
20
20
  end
21
21
 
22
22
  def coerce_input(_type, value, _ctx)
@@ -47,7 +47,7 @@ module Lighthouse::GraphQL::SchemaGenerator
47
47
  impl = default_implementation
48
48
  # base_types' resolver / mutation / subscription are Lighthouse's, not graphql-ruby's.
49
49
  bases = Lighthouse::GraphQL::ResolverBases.extract!(opts)
50
- schema = from_document(document, impl, opts)
50
+ schema = Lighthouse::GraphQL::RootTypes.build(document, bases[:roots]) { from_document(document, impl, opts) }
51
51
 
52
52
  delegate_to_federation_sdl(schema)
53
53
  Lighthouse::GraphQL::GeneratedResolvers.install_dispatcher(schema)
@@ -139,7 +139,10 @@ module Lighthouse::GraphQL::SchemaGenerator
139
139
  # Apply object-level federation directives to generated types so the
140
140
  # Apollo Federation helper can detect entities and add _entities.
141
141
  Lighthouse::GraphQL::FederationDirectives.apply(type)
142
- type.fields.each_value { |field_def| process_field(type, field_def, root_kinds[type.graphql_name], bases) }
142
+ root_kind = root_kinds[type.graphql_name]
143
+ type_class = Lighthouse::GraphQL::DirectiveResolvers::TypeMethodResolver.type_class(type.graphql_name, bases, root_kind)
144
+ # A field a code-first base declares (RootTypes) resolves as graphql-ruby resolves it.
145
+ type.fields.each_value { |field_def| process_field(type, field_def, root_kind, bases, type_class) if field_def.owner == type }
143
146
  end
144
147
 
145
148
  # @complexity, and paginated fields, give graphql-ruby's complexity analysis their cost.
@@ -151,6 +154,9 @@ module Lighthouse::GraphQL::SchemaGenerator
151
154
  # Every @broadcast must name a field of the Subscription type.
152
155
  Lighthouse::GraphQL::Broadcasting.validate!(schema)
153
156
 
157
+ # Unions, interfaces and scalars get the classes that resolve and coerce them.
158
+ Lighthouse::GraphQL::Type.apply(schema, bases.fetch(:types, {}))
159
+
154
160
  # return schema for convenience
155
161
  schema
156
162
  end
@@ -163,10 +169,11 @@ module Lighthouse::GraphQL::SchemaGenerator
163
169
 
164
170
  # A field's generated resolvers, then the runtime hooks that make
165
171
  # graphql-ruby reach them.
166
- def process_field(type, field_def, root_kind, bases = {})
172
+ def process_field(type, field_def, root_kind, bases = {}, type_class = nil)
167
173
  return unless field_def.ast_node
168
174
 
169
- apply_field_resolvers(type, field_def, field_def.ast_node.directives || [], root_kind, bases)
175
+ # Installed in resolution order (see FieldResolverStack).
176
+ Lighthouse::GraphQL::FieldResolverStack.apply(type, field_def, root_kind, bases: bases, type_class: type_class)
170
177
  Lighthouse::GraphQL::GeneratedResolvers.route_runtime_resolver(type, field_def)
171
178
  ensure_connection_extension(field_def)
172
179
  end
@@ -187,11 +194,6 @@ module Lighthouse::GraphQL::SchemaGenerator
187
194
  # ignore failures during post-processing
188
195
  end
189
196
 
190
- # A field's resolvers, installed in resolution order (see FieldResolverStack).
191
- def apply_field_resolvers(type, field_def, directives, root_kind, bases = {})
192
- Lighthouse::GraphQL::FieldResolverStack.apply(type, field_def, directives, root_kind, bases: bases)
193
- end
194
-
195
197
  # Scan SDL string for list-typed fields that carry relationship directives
196
198
  # and produce Connection/Edge types where missing. This is a lightweight
197
199
  # transformation that mirrors Lighthouse behavior: clients can declare
@@ -27,14 +27,10 @@ class SchemaImplementation
27
27
  read_attribute(obj, field_name)
28
28
  end
29
29
 
30
- def resolve_type(abstract_type, _obj, _ctx)
31
- # Try to resolve to a Ruby type class under Types::<Name>Type
32
- type_name = abstract_type.respond_to?(:name) ? abstract_type.name : abstract_type.to_s
33
- begin
34
- Object.const_get("Types::#{type_name}Type")
35
- rescue StandardError
36
- nil
37
- end
30
+ # The object type of a union's or interface's value (see TypeResolution).
31
+ def resolve_type(abstract_type, obj, ctx)
32
+ schema = ctx.respond_to?(:schema) && ctx.schema ? ctx.schema : @schema
33
+ Lighthouse::GraphQL::TypeResolution.resolve(schema, abstract_type, obj, ctx)
38
34
  end
39
35
 
40
36
  def coerce_input(_type, value, _ctx)
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The application's environment, for @hide and @show: Rails.env, else
4
+ # RAILS_ENV or RACK_ENV, else "development".
5
+ module Lighthouse::Support::Environment
6
+ module_function
7
+
8
+ def current
9
+ return Rails.env.to_s if defined?(Rails) && Rails.respond_to?(:env)
10
+
11
+ ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
12
+ end
13
+ end
@@ -26,14 +26,16 @@ module Lighthouse::Support::ModelResolver
26
26
  # `namespaces:` overrides the configured resolver_namespaces, e.g. with
27
27
  # Config.namespaces[:builders] to look a builder class up.
28
28
  # `fallback: false` skips the bare name, so `Resolvers::User` is never
29
- # mistaken for the `User` model.
30
- def constantize(name, namespaces: self.namespaces, fallback: true)
29
+ # mistaken for the `User` model. `nested: true` tries the namespaces before a
30
+ # name that already contains `::` too, so `Feeds::Home` is found as
31
+ # `Resolvers::Feeds::Home`; otherwise such a name is used as-is.
32
+ def constantize(name, namespaces: self.namespaces, fallback: true, nested: false)
31
33
  return nil unless name
32
34
 
33
35
  raw = name.to_s.strip
34
36
  return nil if raw.empty?
35
37
 
36
- candidates_for(raw, namespaces, fallback: fallback).each do |candidate|
38
+ candidates_for(raw, namespaces, fallback: fallback, nested: nested).each do |candidate|
37
39
  klass = try_constantize(candidate)
38
40
  return klass if klass
39
41
  end
@@ -41,8 +43,8 @@ module Lighthouse::Support::ModelResolver
41
43
  nil
42
44
  end
43
45
 
44
- def candidates_for(raw, namespaces = self.namespaces, fallback: true)
45
- if raw.include?('::')
46
+ def candidates_for(raw, namespaces = self.namespaces, fallback: true, nested: false)
47
+ if raw.include?('::') && !nested
46
48
  [raw]
47
49
  else
48
50
  Array(namespaces).map { |ns| "#{ns}::#{raw}" } + (fallback ? [raw] : [])