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,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# @model(class: "Post") — map an object type to the ActiveRecord model whose
|
|
4
|
+
# records it resolves, as PHP Lighthouse's directive, for when the type's name
|
|
5
|
+
# is not the model's:
|
|
6
|
+
#
|
|
7
|
+
# type Article @model(class: "Post") { id: ID! title: String! }
|
|
8
|
+
#
|
|
9
|
+
# type Query {
|
|
10
|
+
# articles: [Article!]! @paginate # pages through Post
|
|
11
|
+
# article(id: ID! @eq): Article @find # Post.find_by(id:)
|
|
12
|
+
# }
|
|
13
|
+
#
|
|
14
|
+
# - Every directive that takes `model:` (@all, @find, @first, @paginate, @create,
|
|
15
|
+
# @update, @upsert, @delete, @can*, ...) on a field returning the type gets it,
|
|
16
|
+
# unless it names its own: the SDL is rewritten before the schema is built.
|
|
17
|
+
# - A union or interface resolves a record of the model to the type.
|
|
18
|
+
# - @node finds the type's records in the model.
|
|
19
|
+
# - The schema does not build unless the class is an ActiveRecord model, and a
|
|
20
|
+
# type class's methods (Types::ArticleType) only run on its records.
|
|
21
|
+
class Lighthouse::GraphQL::Directives::ModelDirective
|
|
22
|
+
DEFINITIONS = {
|
|
23
|
+
'model' => <<~GRAPHQL.chomp
|
|
24
|
+
"""
|
|
25
|
+
Map a model class to an object type.
|
|
26
|
+
This can be used when the name of the model differs from the name of the type.
|
|
27
|
+
"""
|
|
28
|
+
directive @model(
|
|
29
|
+
"The class name of the corresponding model."
|
|
30
|
+
class: String!
|
|
31
|
+
) on OBJECT
|
|
32
|
+
GRAPHQL
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
OBJECTS = [::GraphQL::Language::Nodes::ObjectTypeDefinition, ::GraphQL::Language::Nodes::ObjectTypeExtension].freeze
|
|
36
|
+
|
|
37
|
+
DECLARATION = /directive\s+@model\b/
|
|
38
|
+
USAGE = /@model\b/
|
|
39
|
+
|
|
40
|
+
def self.manipulate(sdl)
|
|
41
|
+
return sdl unless sdl.is_a?(String) && sdl.gsub(DECLARATION, '').match?(USAGE)
|
|
42
|
+
|
|
43
|
+
new(::GraphQL::Language::Parser.parse(sdl)).manipulate(sdl)
|
|
44
|
+
rescue ::GraphQL::ParseError
|
|
45
|
+
sdl
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The model class name a schema type maps to with @model, or nil.
|
|
49
|
+
def self.class_name(type)
|
|
50
|
+
directive = (type.ast_node&.directives || []).find { |d| d.name == 'model' }
|
|
51
|
+
directive && Lighthouse::Support::DirectiveArgs.literal(directive, 'class')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The model of a schema type: the one @model names, else the one named after it.
|
|
55
|
+
def self.model_for(type)
|
|
56
|
+
Lighthouse::Support::ModelResolver.constantize(class_name(type) || type.graphql_name)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The model @model declares on a schema type, or nil.
|
|
60
|
+
def self.declared_model(type)
|
|
61
|
+
name = class_name(type)
|
|
62
|
+
name && Lighthouse::Support::ModelResolver.constantize(name)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Fail at build time: every @model names an ActiveRecord model.
|
|
66
|
+
def self.validate!(schema)
|
|
67
|
+
schema.types.each_value do |type|
|
|
68
|
+
name = type.kind.object? && class_name(type)
|
|
69
|
+
next unless name
|
|
70
|
+
|
|
71
|
+
model = declared_model(type)
|
|
72
|
+
next if model.is_a?(Class) && defined?(::ActiveRecord::Base) && model < ::ActiveRecord::Base
|
|
73
|
+
|
|
74
|
+
raise Lighthouse::DefinitionError, "@model(class: #{name.inspect}) on #{type.graphql_name} names no ActiveRecord model."
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The directives that take `model:` and act on a field, from the registry.
|
|
79
|
+
def self.model_directives
|
|
80
|
+
Lighthouse::DirectiveRegistry.default.directive_definitions
|
|
81
|
+
.select { |_, definition| definition.match?(/\bmodel:/) && definition.include?('FIELD_DEFINITION') }.keys
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def initialize(document)
|
|
85
|
+
@document = document
|
|
86
|
+
@models = document.definitions.filter_map { |definition| [definition.name, model_name(definition)] if object?(definition) }
|
|
87
|
+
.select(&:last).to_h
|
|
88
|
+
@directives = self.class.model_directives
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def manipulate(sdl)
|
|
92
|
+
return sdl if @models.empty?
|
|
93
|
+
|
|
94
|
+
definitions = @document.definitions.map { |definition| object?(definition) ? with_models(definition) : definition }
|
|
95
|
+
::GraphQL::Language::Printer.new.print(@document.merge(definitions: definitions))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def object?(definition)
|
|
101
|
+
OBJECTS.any? { |kind| definition.is_a?(kind) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The class @model names on a type definition, or nil.
|
|
105
|
+
def model_name(definition)
|
|
106
|
+
directive = definition.directives.find { |d| d.name == 'model' }
|
|
107
|
+
directive&.arguments&.find { |argument| argument.name == 'class' }&.value
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def with_models(definition)
|
|
111
|
+
definition.merge(fields: definition.fields.map { |field| with_model(field) })
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# The field's model directives with `model:` set to the model its return type maps to.
|
|
115
|
+
def with_model(field)
|
|
116
|
+
model = @models[unwrap(field.type).name]
|
|
117
|
+
return field unless model
|
|
118
|
+
|
|
119
|
+
field.merge(directives: field.directives.map { |directive| with_model_argument(directive, model) })
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def with_model_argument(directive, model)
|
|
123
|
+
return directive unless @directives.include?(directive.name) && directive.arguments.none? { |argument| argument.name == 'model' }
|
|
124
|
+
|
|
125
|
+
directive.merge(arguments: directive.arguments + [::GraphQL::Language::Nodes::Argument.new(name: 'model', value: model)])
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def unwrap(type)
|
|
129
|
+
type.respond_to?(:of_type) ? unwrap(type.of_type) : type
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# @node — Relay's global object identification, as PHP Lighthouse's directive.
|
|
4
|
+
# On a type, it makes the schema:
|
|
5
|
+
#
|
|
6
|
+
# "Any object implementing this type can be found by ID through `Query.node`."
|
|
7
|
+
# interface Node @interface(resolveType: "Lighthouse::GraphQL::NodeRegistry") {
|
|
8
|
+
# "Global identifier that can be used to resolve any Node implementation."
|
|
9
|
+
# id: ID!
|
|
10
|
+
# }
|
|
11
|
+
#
|
|
12
|
+
# type User implements Node @node { id: ID! @globalId ... }
|
|
13
|
+
#
|
|
14
|
+
# type Query {
|
|
15
|
+
# node(id: ID! @globalId): Node @field(resolver: "Lighthouse::GraphQL::NodeRegistry@resolve")
|
|
16
|
+
# }
|
|
17
|
+
#
|
|
18
|
+
# leaving alone what the SDL declares itself (a Node interface, a Query.node
|
|
19
|
+
# field, with its own description or middleware). The type keeps its own
|
|
20
|
+
# `id: ID! @globalId`, which encodes the ids `node` decodes. NodeRegistry finds
|
|
21
|
+
# the record: through the type's `model:` or model, or its `resolver:`.
|
|
22
|
+
class Lighthouse::GraphQL::Directives::NodeDirective
|
|
23
|
+
DEFINITIONS = {
|
|
24
|
+
'node' => <<~GRAPHQL.chomp
|
|
25
|
+
"""
|
|
26
|
+
Register a type for Relay's global object identification.
|
|
27
|
+
|
|
28
|
+
When used without any arguments, Lighthouse will attempt
|
|
29
|
+
to resolve the type through a model with the same name.
|
|
30
|
+
"""
|
|
31
|
+
directive @node(
|
|
32
|
+
"""
|
|
33
|
+
Reference to a class (and method, after `@`) that receives the decoded `id`
|
|
34
|
+
and the context, and returns the object.
|
|
35
|
+
|
|
36
|
+
Mutually exclusive with `model`.
|
|
37
|
+
"""
|
|
38
|
+
resolver: String
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
Specify the class name of the model to use.
|
|
42
|
+
This is only needed when the default model detection does not work.
|
|
43
|
+
|
|
44
|
+
Mutually exclusive with `resolver`.
|
|
45
|
+
"""
|
|
46
|
+
model: String
|
|
47
|
+
) on OBJECT
|
|
48
|
+
GRAPHQL
|
|
49
|
+
}.freeze
|
|
50
|
+
|
|
51
|
+
INTERFACE = <<~GRAPHQL.chomp
|
|
52
|
+
"Any object implementing this type can be found by ID through `Query.node`."
|
|
53
|
+
interface Node @interface(resolveType: "Lighthouse::GraphQL::NodeRegistry") {
|
|
54
|
+
"Global identifier that can be used to resolve any Node implementation."
|
|
55
|
+
id: ID!
|
|
56
|
+
}
|
|
57
|
+
GRAPHQL
|
|
58
|
+
|
|
59
|
+
NODE_FIELD = <<~GRAPHQL.chomp
|
|
60
|
+
type Query {
|
|
61
|
+
"Fetches an object given its globally unique `ID`."
|
|
62
|
+
node(id: ID! @globalId): Node @field(resolver: "Lighthouse::GraphQL::NodeRegistry@resolve")
|
|
63
|
+
}
|
|
64
|
+
GRAPHQL
|
|
65
|
+
|
|
66
|
+
OBJECTS = [::GraphQL::Language::Nodes::ObjectTypeDefinition, ::GraphQL::Language::Nodes::ObjectTypeExtension].freeze
|
|
67
|
+
|
|
68
|
+
DECLARATION = /directive\s+@node\b/
|
|
69
|
+
USAGE = /@node\b/
|
|
70
|
+
|
|
71
|
+
def self.manipulate(sdl)
|
|
72
|
+
return sdl unless sdl.is_a?(String) && sdl.gsub(DECLARATION, '').match?(USAGE)
|
|
73
|
+
|
|
74
|
+
new(::GraphQL::Language::Parser.parse(sdl)).manipulate(sdl)
|
|
75
|
+
rescue ::GraphQL::ParseError
|
|
76
|
+
sdl
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def initialize(document)
|
|
80
|
+
@document = document
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def manipulate(sdl)
|
|
84
|
+
return sdl unless @document.definitions.any? { |definition| node?(definition) }
|
|
85
|
+
|
|
86
|
+
definitions = @document.definitions.map { |definition| node?(definition) ? implement_node(definition) : definition }
|
|
87
|
+
definitions = with_node_field(definitions)
|
|
88
|
+
printed = ::GraphQL::Language::Printer.new.print(@document.merge(definitions: definitions))
|
|
89
|
+
printed = "#{printed}\n\n#{INTERFACE}" unless declared?('Node')
|
|
90
|
+
# The added @globalId, @field and @interface need their definitions too.
|
|
91
|
+
Lighthouse::GraphQL::SchemaGenerator.inject_directive_definitions(printed)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def node?(definition)
|
|
97
|
+
OBJECTS.any? { |kind| definition.is_a?(kind) } && definition.directives.any? { |directive| directive.name == 'node' }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def implement_node(definition)
|
|
101
|
+
return definition if definition.interfaces.any? { |interface| interface.name == 'Node' }
|
|
102
|
+
|
|
103
|
+
definition.merge(interfaces: definition.interfaces + [::GraphQL::Language::Nodes::TypeName.new(name: 'Node')])
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Query gains `node`, unless a Query definition or extension has one. With
|
|
107
|
+
# no Query at all, the schema gets one with `node` alone.
|
|
108
|
+
def with_node_field(definitions)
|
|
109
|
+
queries = definitions.select { |definition| query?(definition) }
|
|
110
|
+
return definitions if queries.any? { |query| node_field?(query) }
|
|
111
|
+
|
|
112
|
+
node_query = ::GraphQL::Language::Parser.parse(NODE_FIELD).definitions.first
|
|
113
|
+
return definitions + [node_query] if queries.empty?
|
|
114
|
+
|
|
115
|
+
definitions.map { |definition| definition.equal?(queries.first) ? definition.merge(fields: definition.fields + node_query.fields) : definition }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def node_field?(query)
|
|
119
|
+
query.fields.any? { |field| field.name == 'node' }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def query?(definition)
|
|
123
|
+
OBJECTS.any? { |kind| definition.is_a?(kind) } && definition.name == query_name
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def query_name
|
|
127
|
+
schema = @document.definitions.find { |definition| definition.is_a?(::GraphQL::Language::Nodes::SchemaDefinition) }
|
|
128
|
+
schema&.query || 'Query'
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def declared?(name)
|
|
132
|
+
@document.definitions.any? { |definition| definition.respond_to?(:name) && definition.name == name && !definition.is_a?(::GraphQL::Language::Nodes::ObjectTypeExtension) }
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -16,7 +16,7 @@ require_relative 'pagination_info_types'
|
|
|
16
16
|
# relation directive (no `type:`) and @all keep returning the list.
|
|
17
17
|
# The runtime side is Lighthouse::GraphQL::Pagination.
|
|
18
18
|
class Lighthouse::GraphQL::Directives::PaginationManipulator
|
|
19
|
-
RELATION_DIRECTIVES = %w[hasMany belongsToMany morphMany morphToMany].freeze
|
|
19
|
+
RELATION_DIRECTIVES = %w[hasMany hasManyThrough belongsToMany morphMany morphToMany].freeze
|
|
20
20
|
OBJECT_DEFINITIONS = [
|
|
21
21
|
::GraphQL::Language::Nodes::ObjectTypeDefinition,
|
|
22
22
|
::GraphQL::Language::Nodes::ObjectTypeExtension
|
|
@@ -61,13 +61,17 @@ class Lighthouse::DirectiveRegistry
|
|
|
61
61
|
# authorization, argument validation, then @cache (so a cache hit is still
|
|
62
62
|
# authorized and validated), @clearCache (which acts once the resolver
|
|
63
63
|
# succeeded), @event and @broadcast (likewise), and finally the data resolver (0).
|
|
64
|
+
# @throttle counts every call, before anything else runs; @async queues what is
|
|
65
|
+
# below @cache, once the request has authorized and validated the call.
|
|
64
66
|
RESOLUTION_ORDER = {
|
|
67
|
+
'throttle' => 7,
|
|
65
68
|
'convertEmptyStringsToNull' => 6,
|
|
66
69
|
'guard' => 5, 'auth' => 5,
|
|
67
70
|
'canFind' => 4, 'canModel' => 4, 'canQuery' => 4, 'canResolved' => 4, 'canRoot' => 4,
|
|
68
71
|
'validator' => 3,
|
|
69
|
-
'cache' => 2,
|
|
70
|
-
'clearCache' => 1, 'event' => 1, 'broadcast' => 1
|
|
72
|
+
'cache' => 2, 'async' => 2,
|
|
73
|
+
'clearCache' => 1, 'event' => 1, 'broadcast' => 1,
|
|
74
|
+
'with' => 1, 'withCount' => 1, 'lazyLoad' => 1, 'globalId' => 1
|
|
71
75
|
}.freeze
|
|
72
76
|
|
|
73
77
|
def resolution_order(name)
|
|
@@ -164,6 +168,8 @@ class Lighthouse::DirectiveRegistry
|
|
|
164
168
|
'rename' => :RenameDirectiveResolver,
|
|
165
169
|
'method' => :MethodDirectiveResolver,
|
|
166
170
|
'auth' => :AuthDirectiveResolver,
|
|
171
|
+
'hasManyThrough' => :HasManyDirectiveResolver,
|
|
172
|
+
'hasOneThrough' => :HasOneDirectiveResolver,
|
|
167
173
|
'guard' => :GuardDirectiveResolver
|
|
168
174
|
}.freeze
|
|
169
175
|
|
|
@@ -181,17 +187,15 @@ class Lighthouse::DirectiveRegistry
|
|
|
181
187
|
CacheDirectiveResolver ClearCacheDirectiveResolver EventDirectiveResolver
|
|
182
188
|
ValidatorDirectiveResolver ConvertEmptyStringsToNullDirectiveResolver
|
|
183
189
|
SubscriptionDirectiveResolver BroadcastDirectiveResolver
|
|
190
|
+
WithDirectiveResolver WithCountDirectiveResolver LazyLoadDirectiveResolver
|
|
191
|
+
GlobalIdDirectiveResolver NamespacedDirectiveResolver ThrottleDirectiveResolver AsyncDirectiveResolver
|
|
184
192
|
].freeze
|
|
185
193
|
|
|
186
194
|
# Argument-builder filter/sort directives.
|
|
187
|
-
ARG_BUILDERS = %i[
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
'spread' => 'directive @spread on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION',
|
|
192
|
-
'inject' => 'directive @inject(context: String!, name: String!) repeatable on FIELD_DEFINITION',
|
|
193
|
-
'cacheKey' => 'directive @cacheKey on FIELD_DEFINITION'
|
|
194
|
-
}.freeze
|
|
195
|
+
ARG_BUILDERS = %i[
|
|
196
|
+
Eq Neq In NotIn Like Where WhereKey OrderBy Trashed WithoutGlobalScopes
|
|
197
|
+
WhereNull WhereNotNull WhereBetween WhereNotBetween WhereJsonContains Scope Limit
|
|
198
|
+
].freeze
|
|
195
199
|
|
|
196
200
|
def register_defaults
|
|
197
201
|
resolvers = Lighthouse::GraphQL::DirectiveResolvers
|
|
@@ -200,9 +204,14 @@ class Lighthouse::DirectiveRegistry
|
|
|
200
204
|
ARG_BUILDERS.each { |constant| register(Lighthouse::GraphQL::Directives::Arguments.const_get(constant, false)) }
|
|
201
205
|
|
|
202
206
|
# SDL manipulators run in registration order during the manipulation phase.
|
|
207
|
+
# @hide / @show first, so a removed field generates nothing; then @model,
|
|
208
|
+
# which names the model of fields @paginate then rewrites.
|
|
209
|
+
register_manipulator(Lighthouse::GraphQL::Directives::VisibilityDirective)
|
|
210
|
+
register_manipulator(Lighthouse::GraphQL::Directives::ModelDirective)
|
|
203
211
|
register_manipulator(Lighthouse::GraphQL::Directives::PaginationManipulator)
|
|
204
212
|
register_manipulator(Lighthouse::GraphQL::Directives::WhereConditionsDirective)
|
|
205
213
|
register_manipulator(Lighthouse::GraphQL::Directives::SoftDeletesDirective)
|
|
214
|
+
register_manipulator(Lighthouse::GraphQL::Directives::NodeDirective)
|
|
206
215
|
|
|
207
216
|
register_default_definitions
|
|
208
217
|
self
|
|
@@ -213,16 +222,8 @@ class Lighthouse::DirectiveRegistry
|
|
|
213
222
|
# SDL of the directives (and the types they refer to) that no registered
|
|
214
223
|
# class contributes.
|
|
215
224
|
def register_default_definitions
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
register_definition('cacheControl', Lighthouse::GraphQL::CacheControl::DEFINITION)
|
|
219
|
-
Lighthouse::GraphQL::CacheControl::TYPE_DEFINITIONS.each { |name, sdl| register_type_definition(name, sdl) }
|
|
220
|
-
register_definition('feature', Lighthouse::GraphQL::FeatureDirective::DEFINITION)
|
|
221
|
-
Lighthouse::GraphQL::FeatureDirective::TYPE_DEFINITIONS.each { |name, sdl| register_type_definition(name, sdl) }
|
|
222
|
-
Lighthouse::GraphQL::Directives::WhereConditionsDirective::DEFINITIONS.each do |name, definition|
|
|
223
|
-
register_definition(name, definition)
|
|
224
|
-
end
|
|
225
|
-
Lighthouse::GraphQL::Directives::SoftDeletesDirective::DEFINITIONS.each { |name, definition| register_definition(name, definition) }
|
|
225
|
+
Lighthouse::GraphQL::Directives::Definitions.directives.each { |name, definition| register_definition(name, definition) }
|
|
226
|
+
Lighthouse::GraphQL::Directives::Definitions.types.each { |name, sdl| register_type_definition(name, sdl) }
|
|
226
227
|
end
|
|
227
228
|
|
|
228
229
|
def safe_definition(klass)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# @hide(env:) and @show(env:) — leave a field out of the schema in some
|
|
4
|
+
# environments, as PHP Lighthouse's directives: @hide removes it in the ones
|
|
5
|
+
# listed, @show keeps it only in those. The environment is Rails.env (see
|
|
6
|
+
# Support::Environment); the SDL is rewritten before the schema is built.
|
|
7
|
+
#
|
|
8
|
+
# type Query {
|
|
9
|
+
# debugInfo: String @show(env: ["development", "test"])
|
|
10
|
+
# legacyFeed: [Post!]! @hide(env: ["production"]) @all
|
|
11
|
+
# }
|
|
12
|
+
class Lighthouse::GraphQL::Directives::VisibilityDirective
|
|
13
|
+
DEFINITIONS = {
|
|
14
|
+
'hide' => <<~GRAPHQL.chomp,
|
|
15
|
+
"""
|
|
16
|
+
Excludes the annotated element from the schema conditionally.
|
|
17
|
+
"""
|
|
18
|
+
directive @hide(
|
|
19
|
+
"Specify which environments must not use this field, e.g. [\\"production\\"]."
|
|
20
|
+
env: [String!]!
|
|
21
|
+
) on FIELD_DEFINITION
|
|
22
|
+
GRAPHQL
|
|
23
|
+
'show' => <<~GRAPHQL.chomp
|
|
24
|
+
"""
|
|
25
|
+
Includes the annotated element in the schema conditionally.
|
|
26
|
+
"""
|
|
27
|
+
directive @show(
|
|
28
|
+
"Specify which environments may use this field, e.g. [\\"testing\\"]."
|
|
29
|
+
env: [String!]!
|
|
30
|
+
) on FIELD_DEFINITION
|
|
31
|
+
GRAPHQL
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
FIELDS = [
|
|
35
|
+
::GraphQL::Language::Nodes::ObjectTypeDefinition, ::GraphQL::Language::Nodes::ObjectTypeExtension,
|
|
36
|
+
::GraphQL::Language::Nodes::InterfaceTypeDefinition, ::GraphQL::Language::Nodes::InterfaceTypeExtension
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
DECLARATION = /directive\s+@(hide|show)\b/
|
|
40
|
+
USAGE = /@(hide|show)\b/
|
|
41
|
+
|
|
42
|
+
def self.manipulate(sdl)
|
|
43
|
+
return sdl unless sdl.is_a?(String) && sdl.gsub(DECLARATION, '').match?(USAGE)
|
|
44
|
+
|
|
45
|
+
document = ::GraphQL::Language::Parser.parse(sdl)
|
|
46
|
+
env = Lighthouse::Support::Environment.current
|
|
47
|
+
::GraphQL::Language::Printer.new.print(document.merge(definitions: document.definitions.map { |definition| visible_fields(definition, env) }))
|
|
48
|
+
rescue ::GraphQL::ParseError
|
|
49
|
+
sdl
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.visible_fields(definition, env)
|
|
53
|
+
return definition unless FIELDS.any? { |kind| definition.is_a?(kind) }
|
|
54
|
+
|
|
55
|
+
definition.merge(fields: definition.fields.select { |field| visible?(field, env) })
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.visible?(field, env)
|
|
59
|
+
field.directives.all? do |directive|
|
|
60
|
+
case directive.name
|
|
61
|
+
when 'hide' then environments(directive).exclude?(env)
|
|
62
|
+
when 'show' then environments(directive).include?(env)
|
|
63
|
+
else true
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.environments(directive)
|
|
69
|
+
Array(directive.arguments.find { |argument| argument.name == 'env' }&.value).map(&:to_s)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# @enum(value:) — the internal value of an enum value, as PHP Lighthouse's
|
|
4
|
+
# directive: what a resolver gets for it in an argument, and what it answers
|
|
5
|
+
# with in a result.
|
|
6
|
+
#
|
|
7
|
+
# enum Role {
|
|
8
|
+
# ADMIN @enum(value: "admin")
|
|
9
|
+
# MEMBER @enum(value: "member")
|
|
10
|
+
# }
|
|
11
|
+
#
|
|
12
|
+
# `users(role: Role @eq)` then filters on "admin", and a User whose role is
|
|
13
|
+
# "admin" answers ADMIN. Without the directive, a value is its own name.
|
|
14
|
+
module Lighthouse::GraphQL::EnumDirective
|
|
15
|
+
DEFINITION = <<~GRAPHQL.chomp
|
|
16
|
+
"""
|
|
17
|
+
Assign an internal value to an enum key.
|
|
18
|
+
When dealing with the Enum type in your code,
|
|
19
|
+
you will receive the defined value instead of the string key.
|
|
20
|
+
"""
|
|
21
|
+
directive @enum(
|
|
22
|
+
"The internal value of the enum key."
|
|
23
|
+
value: Mixed
|
|
24
|
+
) on ENUM_VALUE
|
|
25
|
+
GRAPHQL
|
|
26
|
+
|
|
27
|
+
TYPE_DEFINITIONS = { 'Mixed' => 'scalar Mixed' }.freeze
|
|
28
|
+
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
def apply(schema)
|
|
32
|
+
schema.types.each_value { |type| apply_to(type) if type.kind.enum? }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def apply_to(enum_type)
|
|
36
|
+
enum_type.values.each_value do |enum_value|
|
|
37
|
+
directive = (enum_value.ast_node&.directives || []).find { |d| d.name == 'enum' }
|
|
38
|
+
enum_value.value(Lighthouse::Support::DirectiveArgs.literal(directive, 'value')) if directive
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -33,7 +33,8 @@ module Lighthouse::Events::ExecutionTrace
|
|
|
33
33
|
result = query.result_values
|
|
34
34
|
return unless result.is_a?(Hash)
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
complexity = Lighthouse::GraphQL::Complexity.query_complexity(query)
|
|
37
|
+
merge_extensions(result, Lighthouse::Events.dispatch(Lighthouse::Events::BuildExtensionsResponse.new(result, complexity)))
|
|
37
38
|
manipulate = Lighthouse::Events::ManipulateResult.new(result)
|
|
38
39
|
Lighthouse::Events.dispatch(manipulate)
|
|
39
40
|
result.replace(manipulate.result) unless manipulate.result.equal?(result)
|
|
@@ -71,7 +71,9 @@ module Lighthouse::Events
|
|
|
71
71
|
|
|
72
72
|
# A query ran. A listener may return an Events::ExtensionsResponse (or an
|
|
73
73
|
# Array of them), which is merged into the result's "extensions".
|
|
74
|
-
# `query_complexity` is
|
|
74
|
+
# `query_complexity` is the query's complexity score, as graphql-ruby checks it
|
|
75
|
+
# against max_complexity (see GraphQL::Complexity); nil for a query that
|
|
76
|
+
# failed validation, which is never scored.
|
|
75
77
|
class BuildExtensionsResponse < Event
|
|
76
78
|
attr_reader :result, :query_complexity
|
|
77
79
|
|
|
@@ -3,21 +3,27 @@
|
|
|
3
3
|
# Install a field's resolvers innermost first (see
|
|
4
4
|
# DirectiveRegistry#resolution_order), so each wrapper decorates the ones
|
|
5
5
|
# before it. Besides its directives, a field gets the conventional
|
|
6
|
-
#
|
|
7
|
-
# directive,
|
|
8
|
-
#
|
|
6
|
+
# Resolvers::/Mutations:: resolver when it is a root field with no resolving
|
|
7
|
+
# directive, the method of the type's class (Types::PostType) when another
|
|
8
|
+
# type's field has none, and argument validation when an input type it takes
|
|
9
|
+
# carries @validator. The registry replaces a hardcoded `case dnode.name`: adding a
|
|
9
10
|
# directive is "register a class", not "edit the generator".
|
|
10
11
|
module Lighthouse::GraphQL::FieldResolverStack
|
|
11
12
|
module_function
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
# `bases`: { root kind => base class } from the schema's base_types (ResolverBases).
|
|
15
|
+
# `type_class`: the class of a non-root type (Types::PostType), whose methods
|
|
16
|
+
# resolve its fields that have no resolving directive.
|
|
17
|
+
def apply(type, field_def, root_kind, bases: {}, type_class: nil)
|
|
18
|
+
directives = field_def.ast_node.directives || []
|
|
19
|
+
opts = { root_kind: root_kind, bases: bases }
|
|
15
20
|
steps = directive_steps(type, field_def, directives, opts)
|
|
16
|
-
steps <<
|
|
21
|
+
steps << limit_step(type, field_def, opts) if Lighthouse::GraphQL::DirectiveResolvers::LimitResolver.keyword(field_def)
|
|
22
|
+
steps << own_step(type, field_def, root_kind, bases, type_class) unless resolving_directive?(directives)
|
|
17
23
|
steps << validator_step(type, field_def, opts) if input_validation?(field_def, directives)
|
|
18
|
-
steps
|
|
24
|
+
steps.concat(argument_steps(type, field_def, directives, opts))
|
|
19
25
|
|
|
20
|
-
steps.each_with_index.sort_by { |(order, _), index| [order, index] }.each { |(_, step), _| step.call }
|
|
26
|
+
steps.compact.each_with_index.sort_by { |(order, _), index| [order, index] }.each { |(_, step), _| step.call }
|
|
21
27
|
end
|
|
22
28
|
|
|
23
29
|
# [order, step] for every directive the registry has a resolver for.
|
|
@@ -38,17 +44,33 @@ module Lighthouse::GraphQL::FieldResolverStack
|
|
|
38
44
|
directives.any? { |d| registry.resolution_class(d.name) && !registry.wrapper?(d.name) }
|
|
39
45
|
end
|
|
40
46
|
|
|
41
|
-
def convention_step(type, field_def, root_kind)
|
|
47
|
+
def convention_step(type, field_def, root_kind, bases = {})
|
|
42
48
|
resolvers = Lighthouse::GraphQL::DirectiveResolvers
|
|
43
49
|
resolver =
|
|
44
50
|
if root_kind == :subscriptions
|
|
45
|
-
resolvers::SubscriptionDirectiveResolver.new(field_def, type, nil, root_kind: root_kind)
|
|
51
|
+
resolvers::SubscriptionDirectiveResolver.new(field_def, type, nil, root_kind: root_kind, bases: bases)
|
|
46
52
|
else
|
|
47
|
-
resolvers::ConventionResolver.new(field_def, type, root_kind)
|
|
53
|
+
resolvers::ConventionResolver.new(field_def, type, root_kind, bases[root_kind])
|
|
48
54
|
end
|
|
49
55
|
[0, -> { resolver.define_resolver(type) }]
|
|
50
56
|
end
|
|
51
57
|
|
|
58
|
+
# What resolves a field with no resolving directive: the conventional class
|
|
59
|
+
# of a root field, or the method of the type's class. nil: the default reader.
|
|
60
|
+
def own_step(type, field_def, root_kind, bases, type_class)
|
|
61
|
+
return convention_step(type, field_def, root_kind, bases) if root_kind
|
|
62
|
+
|
|
63
|
+
type_method_step(type, field_def, type_class) if type_class
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Nothing to install when the class does not define the field's method: the
|
|
67
|
+
# field is read off the object.
|
|
68
|
+
def type_method_step(type, field_def, type_class)
|
|
69
|
+
resolver = Lighthouse::GraphQL::DirectiveResolvers::TypeMethodResolver
|
|
70
|
+
method_name = resolver.method_for(type_class, field_def)
|
|
71
|
+
[0, -> { resolver.new(field_def, type, method_name).define_resolver(type) }] if method_name
|
|
72
|
+
end
|
|
73
|
+
|
|
52
74
|
# An explicit @validator on the field already validates its arguments.
|
|
53
75
|
def input_validation?(field_def, directives)
|
|
54
76
|
directives.none? { |d| d.name == 'validator' } &&
|
|
@@ -68,6 +90,32 @@ module Lighthouse::GraphQL::FieldResolverStack
|
|
|
68
90
|
Lighthouse::GraphQL::DirectiveResolvers::ConvertEmptyStringsToNullDirectiveResolver.implicit?(field_def)
|
|
69
91
|
end
|
|
70
92
|
|
|
93
|
+
# The steps that change the arguments: empty strings to null and the rewriting
|
|
94
|
+
# directives (@globalId, @trim, @drop) outermost, so every wrapper sees them;
|
|
95
|
+
# the effectful ones (@hash, @bind, @upload) once @guard, @can* and @validator
|
|
96
|
+
# have let the call through (ArgumentTransformsResolver).
|
|
97
|
+
ARGUMENT_PHASES = { rewrite: 'convertEmptyStringsToNull', effects: 'cache' }.freeze
|
|
98
|
+
|
|
99
|
+
def argument_steps(type, field_def, directives, opts)
|
|
100
|
+
steps = []
|
|
101
|
+
steps << empty_strings_step(type, field_def, opts) if empty_string_conversion?(field_def, directives)
|
|
102
|
+
ARGUMENT_PHASES.each do |phase, alongside|
|
|
103
|
+
next unless Lighthouse::GraphQL::DirectiveResolvers::ArgumentTransformsResolver.transforms?(field_def, phase)
|
|
104
|
+
|
|
105
|
+
steps << transforms_step(type, field_def, opts.merge(phase: phase), Lighthouse::DirectiveRegistry.default.resolution_order(alongside))
|
|
106
|
+
end
|
|
107
|
+
steps
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Around whatever resolves the field, as @lazyLoad: the result is cut to the client's @limit.
|
|
111
|
+
def limit_step(type, field_def, opts)
|
|
112
|
+
[1, -> { Lighthouse::GraphQL::DirectiveResolvers::LimitResolver.new(field_def, type, nil, opts).define_resolver(type) }]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def transforms_step(type, field_def, opts, order)
|
|
116
|
+
[order, -> { Lighthouse::GraphQL::DirectiveResolvers::ArgumentTransformsResolver.new(field_def, type, nil, opts).define_resolver(type) }]
|
|
117
|
+
end
|
|
118
|
+
|
|
71
119
|
def empty_strings_step(type, field_def, opts)
|
|
72
120
|
resolver = Lighthouse::GraphQL::DirectiveResolvers::ConvertEmptyStringsToNullDirectiveResolver
|
|
73
121
|
[Lighthouse::DirectiveRegistry.default.resolution_order(resolver.directive_name),
|
|
@@ -25,6 +25,7 @@ module Lighthouse::GraphQL::GeneratedResolvers
|
|
|
25
25
|
|
|
26
26
|
# Generated resolvers take either (obj, args) or (obj, args, ctx).
|
|
27
27
|
def invoke(resolver, obj, args, ctx)
|
|
28
|
+
args = take_lookahead(args, ctx)
|
|
28
29
|
return resolver.call(obj, args) if resolver.parameters.length == 2
|
|
29
30
|
|
|
30
31
|
resolver.call(obj, args, ctx)
|
|
@@ -37,13 +38,7 @@ module Lighthouse::GraphQL::GeneratedResolvers
|
|
|
37
38
|
def install_dispatcher(schema)
|
|
38
39
|
return unless schema.respond_to?(:definition_default_resolve=) && schema.respond_to?(:definition_default_resolve)
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
schema.definition_default_resolve = proc do |type, field, obj, arguments, ctx|
|
|
42
|
-
resolver = generated_resolver(schema, type, field)
|
|
43
|
-
next invoke(resolver, obj, arguments, ctx) if resolver
|
|
44
|
-
|
|
45
|
-
previous_default.call(type, field, obj, arguments, ctx)
|
|
46
|
-
end
|
|
41
|
+
schema.definition_default_resolve = Lighthouse::GraphQL::DefaultResolveDispatcher.new(schema, schema.definition_default_resolve)
|
|
47
42
|
end
|
|
48
43
|
|
|
49
44
|
# Our generated resolver for `field`, looked up on the schema's own type
|
|
@@ -65,6 +60,16 @@ module Lighthouse::GraphQL::GeneratedResolvers
|
|
|
65
60
|
end
|
|
66
61
|
end
|
|
67
62
|
|
|
63
|
+
# The arguments without graphql-ruby's lookahead extra, which a field resolved
|
|
64
|
+
# by a class asks for (ResolveInfo.request_lookahead): it is kept in the
|
|
65
|
+
# field's scoped context, for its ResolveInfo, and no wrapper sees it.
|
|
66
|
+
def take_lookahead(args, ctx)
|
|
67
|
+
return args unless args.is_a?(Hash) && args[:lookahead].is_a?(::GraphQL::Execution::Lookahead)
|
|
68
|
+
|
|
69
|
+
ctx.scoped_set!(Lighthouse::GraphQL::ResolveInfo::LOOKAHEAD, args[:lookahead]) if ctx.respond_to?(:scoped_set!)
|
|
70
|
+
args.except(:lookahead)
|
|
71
|
+
end
|
|
72
|
+
|
|
68
73
|
# GraphQL-Ruby's SDL builder defines `resolve_<field>` methods that call
|
|
69
74
|
# the schema's `definition_default_resolve`. Override `resolve_<field>`
|
|
70
75
|
# to delegate to our generated `resolve_field_<field>` when present, so
|
|
@@ -74,7 +79,7 @@ module Lighthouse::GraphQL::GeneratedResolvers
|
|
|
74
79
|
return unless type.respond_to?(generated)
|
|
75
80
|
|
|
76
81
|
type.define_singleton_method("resolve_#{field_def.name}") do |obj, arguments, ctx|
|
|
77
|
-
public_send(generated, obj, arguments, ctx)
|
|
82
|
+
public_send(generated, obj, Lighthouse::GraphQL::GeneratedResolvers.take_lookahead(arguments, ctx), ctx)
|
|
78
83
|
end
|
|
79
84
|
rescue StandardError
|
|
80
85
|
# ignore override failures
|