lighthouse-graphql 0.3.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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +182 -1
  3. data/README.md +18 -7
  4. data/docs/best-practices.md +16 -6
  5. data/docs/configuration.md +33 -8
  6. data/docs/directives.md +318 -10
  7. data/docs/events.md +3 -1
  8. data/docs/federation.md +3 -2
  9. data/docs/generators.md +82 -0
  10. data/docs/getting-started.md +66 -32
  11. data/docs/motivation.md +3 -2
  12. data/docs/relationships.md +42 -0
  13. data/docs/relay.md +137 -0
  14. data/docs/resolvers.md +338 -25
  15. data/docs/security.md +227 -0
  16. data/docs/subscriptions.md +60 -37
  17. data/docs/types.md +219 -0
  18. data/docs/uploads.md +80 -0
  19. data/lib/generators/lighthouse/base_classes.rb +70 -0
  20. data/lib/generators/lighthouse/class_generator.rb +19 -8
  21. data/lib/generators/lighthouse/install/install_generator.rb +96 -20
  22. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +19 -10
  23. data/lib/generators/lighthouse/install/templates/schema.rb.tt +21 -2
  24. data/lib/generators/lighthouse/mutation_generator.rb +5 -4
  25. data/lib/generators/lighthouse/query_generator.rb +5 -4
  26. data/lib/generators/lighthouse/subscription_generator.rb +21 -16
  27. data/lib/lighthouse/async_mutation_job.rb +17 -0
  28. data/lib/lighthouse/complexity.rb +175 -0
  29. data/lib/lighthouse/complexity_analyzer.rb +56 -0
  30. data/lib/lighthouse/config.rb +16 -4
  31. data/lib/lighthouse/controller.rb +27 -9
  32. data/lib/lighthouse/dataloader_sources/preload_loader.rb +16 -0
  33. data/lib/lighthouse/dataloader_sources/relation_counts_loader.rb +46 -0
  34. data/lib/lighthouse/default_resolve_dispatcher.rb +26 -0
  35. data/lib/lighthouse/directive_resolvers/all_resolver.rb +1 -1
  36. data/lib/lighthouse/directive_resolvers/argument_binding.rb +61 -0
  37. data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +220 -0
  38. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +2 -2
  39. data/lib/lighthouse/directive_resolvers/async_resolver.rb +90 -0
  40. data/lib/lighthouse/directive_resolvers/base.rb +63 -14
  41. data/lib/lighthouse/directive_resolvers/builder_applier.rb +89 -19
  42. data/lib/lighthouse/directive_resolvers/can_resolvers.rb +1 -1
  43. data/lib/lighthouse/directive_resolvers/convention_resolver.rb +16 -5
  44. data/lib/lighthouse/directive_resolvers/convert_empty_strings_resolver.rb +1 -1
  45. data/lib/lighthouse/directive_resolvers/count_resolver.rb +1 -1
  46. data/lib/lighthouse/directive_resolvers/field_resolver.rb +35 -7
  47. data/lib/lighthouse/directive_resolvers/first_resolver.rb +1 -1
  48. data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +57 -0
  49. data/lib/lighthouse/directive_resolvers/lazy_load_resolver.rb +61 -0
  50. data/lib/lighthouse/directive_resolvers/limit_resolver.rb +59 -0
  51. data/lib/lighthouse/directive_resolvers/method_resolver.rb +38 -3
  52. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +1 -1
  53. data/lib/lighthouse/directive_resolvers/namespaced_resolver.rb +31 -0
  54. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +1 -1
  55. data/lib/lighthouse/directive_resolvers/resolution.rb +19 -4
  56. data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +14 -5
  57. data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +77 -0
  58. data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +81 -0
  59. data/lib/lighthouse/directive_resolvers/with_count_resolver.rb +43 -0
  60. data/lib/lighthouse/directive_resolvers/with_resolver.rb +42 -0
  61. data/lib/lighthouse/directives/arguments/more_filters.rb +215 -0
  62. data/lib/lighthouse/directives/definitions.rb +52 -0
  63. data/lib/lighthouse/directives/model_directive.rb +131 -0
  64. data/lib/lighthouse/directives/node_directive.rb +134 -0
  65. data/lib/lighthouse/directives/pagination_manipulator.rb +1 -1
  66. data/lib/lighthouse/directives/registry.rb +21 -20
  67. data/lib/lighthouse/directives/visibility_directive.rb +71 -0
  68. data/lib/lighthouse/enum_directive.rb +41 -0
  69. data/lib/lighthouse/events/execution_trace.rb +2 -1
  70. data/lib/lighthouse/events/lifecycle.rb +3 -1
  71. data/lib/lighthouse/field_resolver_stack.rb +59 -11
  72. data/lib/lighthouse/generated_resolvers.rb +13 -8
  73. data/lib/lighthouse/global_id.rb +31 -0
  74. data/lib/lighthouse/graphql/version.rb +1 -1
  75. data/lib/lighthouse/multipart_request.rb +43 -0
  76. data/lib/lighthouse/node_registry.rb +68 -0
  77. data/lib/lighthouse/pagination.rb +3 -0
  78. data/lib/lighthouse/rb_lighthouse.rb +36 -0
  79. data/lib/lighthouse/relation_counts.rb +29 -0
  80. data/lib/lighthouse/resolve_info.rb +80 -0
  81. data/lib/lighthouse/resolver_bases.rb +68 -0
  82. data/lib/lighthouse/resolver_dispatch.rb +36 -11
  83. data/lib/lighthouse/scalar_coercion.rb +72 -0
  84. data/lib/lighthouse/schema_cache.rb +2 -1
  85. data/lib/lighthouse/schema_factory.rb +0 -1
  86. data/lib/lighthouse/schema_generator.rb +21 -12
  87. data/lib/lighthouse/schema_implementation.rb +4 -8
  88. data/lib/lighthouse/sdl_loader.rb +0 -2
  89. data/lib/lighthouse/support/environment.rb +13 -0
  90. data/lib/lighthouse/support/model_resolver.rb +8 -6
  91. data/lib/lighthouse/support/preload.rb +49 -0
  92. data/lib/lighthouse/type.rb +103 -0
  93. data/lib/lighthouse/type_resolution.rb +150 -0
  94. data/lib/lighthouse/upload.rb +26 -0
  95. data/lib/lighthouse/upload_store.rb +32 -0
  96. data/sig/lighthouse/directives.rbs +3 -2
  97. data/sig/lighthouse/graphql.rbs +29 -0
  98. data/sig/lighthouse.rbs +18 -0
  99. metadata +43 -1
@@ -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.3.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,12 +40,14 @@ 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
44
47
  events/lifecycle
45
48
  events/execution_trace
46
49
  events/schema_build
50
+ resolver_bases
47
51
  schema_generator
48
52
  schema_cache
49
53
  persisted_queries
@@ -55,14 +59,30 @@ require_relative 'namespaces'
55
59
  caching
56
60
  cache_control
57
61
  feature_directive
62
+ complexity
63
+ complexity_analyzer
58
64
  broadcasting
59
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
60
76
  pagination
61
77
  relation_batch
78
+ relation_counts
62
79
  nested_mutations
63
80
  directives/pagination_manipulator
64
81
  directives/where_conditions_directive
65
82
  directives/soft_deletes_directive
83
+ directives/node_directive
84
+ directives/model_directive
85
+ directives/visibility_directive
66
86
  directive_resolvers/aggregate_resolver
67
87
  directive_resolvers/field_resolver
68
88
  directive_resolvers/where_conditions_applier
@@ -80,6 +100,7 @@ require_relative 'namespaces'
80
100
  directive_resolvers/association_resolvers
81
101
  directive_resolvers/can_resolvers
82
102
  directive_resolvers/convention_resolver
103
+ directive_resolvers/type_method_resolver
83
104
  directive_resolvers/model_mutation_resolver
84
105
  directive_resolvers/soft_delete_resolvers
85
106
  directive_resolvers/cache_resolvers
@@ -88,15 +109,29 @@ require_relative 'namespaces'
88
109
  directive_resolvers/broadcast_resolver
89
110
  directive_resolvers/event_resolver
90
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
91
122
  directives/arguments/base
92
123
  directives/arguments/filters
124
+ directives/arguments/more_filters
93
125
  directives/arguments/order_by
94
126
  directives/arguments/soft_deletes
127
+ directives/definitions
95
128
  directives/registry
96
129
  dataloader_sources/association_loader
97
130
  dataloader_sources/model_loader
98
131
  dataloader_sources/relation_page_loader
99
132
  dataloader_sources/relation_count_loader
133
+ dataloader_sources/relation_counts_loader
134
+ dataloader_sources/preload_loader
100
135
  ide_helper
101
136
  ].each do |rel|
102
137
  require_relative rel
@@ -106,6 +141,7 @@ end
106
141
 
107
142
  # The activejob gem is optional: the job that queues @broadcast loads on first use.
108
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__))
109
145
 
110
146
  require_relative 'railtie' if defined?(Rails::Railtie)
111
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
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The base classes of the classes root fields resolve to, given to `get_schema`
4
+ # beside graphql-ruby's own base types:
5
+ #
6
+ # Lighthouse::GraphQL::RbLightHouse.get_schema(options: { base_types: {
7
+ # resolver: Resolvers::BaseResolver, mutation: Mutations::BaseMutation,
8
+ # subscription: Subscriptions::BaseSubscription, object: Types::BaseObject
9
+ # } })
10
+ #
11
+ # graphql-ruby's `from_definition` knows only the SDL's own kinds (object,
12
+ # interface, union, scalar, enum, input_object), so these three are taken out
13
+ # before it sees the rest. Each one given:
14
+ #
15
+ # - is the class every class of its kind must inherit from: a Query, Mutation or
16
+ # Subscription field's class, found by convention or through
17
+ # @subscription(class:), that does not raises Lighthouse::DefinitionError when
18
+ # it is first used, so none misses what the base shares;
19
+ # - for subscriptions, also resolves a Subscription field that has no class of
20
+ # its own, instead of Lighthouse::GraphQL::Subscription::Default.
21
+ module Lighthouse::GraphQL::ResolverBases
22
+ # base_types key => the graphql-ruby class the base must inherit from.
23
+ GRAPHQL_CLASSES = {
24
+ resolver: ::GraphQL::Schema::Resolver,
25
+ mutation: ::GraphQL::Schema::Mutation,
26
+ subscription: ::GraphQL::Schema::Subscription
27
+ }.freeze
28
+
29
+ # Root kind => base_types key.
30
+ KEYS = { queries: :resolver, mutations: :mutation, subscriptions: :subscription }.freeze
31
+
32
+ module_function
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
+
38
+ # Take the three keys out of opts[:base_types] (the caller's Hash is not
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.
41
+ def extract!(opts)
42
+ base_types = opts[:base_types]
43
+ return {} unless base_types.is_a?(Hash)
44
+
45
+ given = base_types.slice(*GRAPHQL_CLASSES.keys).compact
46
+ opts[:base_types] = base_types.except(*GRAPHQL_CLASSES.keys)
47
+ given.each { |key, klass| validate!(key, klass) }
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)
51
+ end
52
+
53
+ def validate!(key, klass)
54
+ expected = GRAPHQL_CLASSES.fetch(key)
55
+ return if klass.is_a?(Class) && klass < expected
56
+
57
+ raise Lighthouse::DefinitionError, "base_types[:#{key}] must be a subclass of #{expected}, not #{klass.inspect}."
58
+ end
59
+
60
+ # Raise unless `klass`, which resolves the field at `location`, inherits from
61
+ # `base` (nil: no base was given).
62
+ def check!(klass, base, location)
63
+ return if base.nil? || klass <= base
64
+
65
+ raise Lighthouse::DefinitionError,
66
+ "#{klass} resolves #{location}, so it must inherit from #{base}, the schema's base class for it (base_types)."
67
+ end
68
+ end
@@ -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,35 @@ 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
+ instance.resolve_with_support(**kwargs)
36
+ end
37
+
38
+ # The class itself when it answers `name` as a class method, else an instance.
39
+ def callable_receiver(klass, name)
40
+ klass.respond_to?(name) ? klass : klass.new
41
+ end
42
+
43
+ POSITIONAL = %i[req opt].freeze
44
+
45
+ # Does the callable take a fourth argument? A lambda's own parameters count,
46
+ # not Proc#call's (*args).
47
+ def takes_info?(receiver, callable)
48
+ params = receiver.is_a?(Proc) && callable.name == :call ? receiver.parameters : callable.parameters
49
+ params.any? { |type, _| type == :rest } || params.count { |type, _| POSITIONAL.include?(type) } >= 4
25
50
  end
26
51
  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
 
@@ -30,7 +30,6 @@ module Lighthouse::GraphQL::SchemaFactory
30
30
  folder = sdl_folder || (Lighthouse::GraphQL::Config.sdl_folder if defined?(Lighthouse::GraphQL::Config))
31
31
  return Lighthouse::GraphQL::SDLLoader.load_from_folder(folder) if folder && Dir.exist?(folder)
32
32
 
33
- # fallback to apps/chatwoot/schema.graphql
34
33
  File.read(File.expand_path('../../schema.graphql', __dir__))
35
34
  end
36
35
  end