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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +182 -1
- data/README.md +18 -7
- data/docs/best-practices.md +16 -6
- data/docs/configuration.md +33 -8
- data/docs/directives.md +318 -10
- data/docs/events.md +3 -1
- data/docs/federation.md +3 -2
- data/docs/generators.md +82 -0
- data/docs/getting-started.md +66 -32
- data/docs/motivation.md +3 -2
- data/docs/relationships.md +42 -0
- data/docs/relay.md +137 -0
- data/docs/resolvers.md +338 -25
- data/docs/security.md +227 -0
- data/docs/subscriptions.md +60 -37
- data/docs/types.md +219 -0
- data/docs/uploads.md +80 -0
- data/lib/generators/lighthouse/base_classes.rb +70 -0
- data/lib/generators/lighthouse/class_generator.rb +19 -8
- data/lib/generators/lighthouse/install/install_generator.rb +96 -20
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +19 -10
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +21 -2
- data/lib/generators/lighthouse/mutation_generator.rb +5 -4
- data/lib/generators/lighthouse/query_generator.rb +5 -4
- data/lib/generators/lighthouse/subscription_generator.rb +21 -16
- data/lib/lighthouse/async_mutation_job.rb +17 -0
- data/lib/lighthouse/complexity.rb +175 -0
- data/lib/lighthouse/complexity_analyzer.rb +56 -0
- data/lib/lighthouse/config.rb +16 -4
- data/lib/lighthouse/controller.rb +27 -9
- data/lib/lighthouse/dataloader_sources/preload_loader.rb +16 -0
- data/lib/lighthouse/dataloader_sources/relation_counts_loader.rb +46 -0
- data/lib/lighthouse/default_resolve_dispatcher.rb +26 -0
- data/lib/lighthouse/directive_resolvers/all_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/argument_binding.rb +61 -0
- data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +220 -0
- data/lib/lighthouse/directive_resolvers/association_resolvers.rb +2 -2
- data/lib/lighthouse/directive_resolvers/async_resolver.rb +90 -0
- data/lib/lighthouse/directive_resolvers/base.rb +63 -14
- data/lib/lighthouse/directive_resolvers/builder_applier.rb +89 -19
- data/lib/lighthouse/directive_resolvers/can_resolvers.rb +1 -1
- data/lib/lighthouse/directive_resolvers/convention_resolver.rb +16 -5
- data/lib/lighthouse/directive_resolvers/convert_empty_strings_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/count_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/field_resolver.rb +35 -7
- data/lib/lighthouse/directive_resolvers/first_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +57 -0
- data/lib/lighthouse/directive_resolvers/lazy_load_resolver.rb +61 -0
- data/lib/lighthouse/directive_resolvers/limit_resolver.rb +59 -0
- data/lib/lighthouse/directive_resolvers/method_resolver.rb +38 -3
- data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/namespaced_resolver.rb +31 -0
- data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/resolution.rb +19 -4
- data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +14 -5
- data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +77 -0
- data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +81 -0
- data/lib/lighthouse/directive_resolvers/with_count_resolver.rb +43 -0
- data/lib/lighthouse/directive_resolvers/with_resolver.rb +42 -0
- data/lib/lighthouse/directives/arguments/more_filters.rb +215 -0
- data/lib/lighthouse/directives/definitions.rb +52 -0
- data/lib/lighthouse/directives/model_directive.rb +131 -0
- data/lib/lighthouse/directives/node_directive.rb +134 -0
- data/lib/lighthouse/directives/pagination_manipulator.rb +1 -1
- data/lib/lighthouse/directives/registry.rb +21 -20
- data/lib/lighthouse/directives/visibility_directive.rb +71 -0
- data/lib/lighthouse/enum_directive.rb +41 -0
- data/lib/lighthouse/events/execution_trace.rb +2 -1
- data/lib/lighthouse/events/lifecycle.rb +3 -1
- data/lib/lighthouse/field_resolver_stack.rb +59 -11
- data/lib/lighthouse/generated_resolvers.rb +13 -8
- data/lib/lighthouse/global_id.rb +31 -0
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/multipart_request.rb +43 -0
- data/lib/lighthouse/node_registry.rb +68 -0
- data/lib/lighthouse/pagination.rb +3 -0
- data/lib/lighthouse/rb_lighthouse.rb +36 -0
- data/lib/lighthouse/relation_counts.rb +29 -0
- data/lib/lighthouse/resolve_info.rb +80 -0
- data/lib/lighthouse/resolver_bases.rb +68 -0
- data/lib/lighthouse/resolver_dispatch.rb +36 -11
- data/lib/lighthouse/scalar_coercion.rb +72 -0
- data/lib/lighthouse/schema_cache.rb +2 -1
- data/lib/lighthouse/schema_factory.rb +0 -1
- data/lib/lighthouse/schema_generator.rb +21 -12
- data/lib/lighthouse/schema_implementation.rb +4 -8
- data/lib/lighthouse/sdl_loader.rb +0 -2
- data/lib/lighthouse/support/environment.rb +13 -0
- data/lib/lighthouse/support/model_resolver.rb +8 -6
- data/lib/lighthouse/support/preload.rb +49 -0
- data/lib/lighthouse/type.rb +103 -0
- data/lib/lighthouse/type_resolution.rb +150 -0
- data/lib/lighthouse/upload.rb +26 -0
- data/lib/lighthouse/upload_store.rb +32 -0
- data/sig/lighthouse/directives.rbs +3 -2
- data/sig/lighthouse/graphql.rbs +29 -0
- data/sig/lighthouse.rbs +18 -0
- 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
|
|
@@ -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
|
|
@@ -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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|