lighthouse-graphql 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +139 -1
- data/README.md +8 -5
- data/docs/configuration.md +26 -2
- data/docs/directives.md +287 -4
- data/docs/generators.md +82 -0
- data/docs/getting-started.md +26 -30
- data/docs/relationships.md +42 -0
- data/docs/relay.md +137 -0
- data/docs/resolvers.md +261 -22
- data/docs/security.md +17 -0
- data/docs/types.md +219 -0
- data/docs/uploads.md +80 -0
- data/lib/generators/lighthouse/base_classes.rb +32 -49
- data/lib/generators/lighthouse/install/install_generator.rb +83 -24
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +14 -6
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +10 -3
- data/lib/lighthouse/async_mutation_job.rb +17 -0
- data/lib/lighthouse/complexity.rb +5 -4
- data/lib/lighthouse/config.rb +13 -2
- 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 +2 -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 +2 -1
- 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 -21
- data/lib/lighthouse/directives/visibility_directive.rb +71 -0
- data/lib/lighthouse/enum_directive.rb +41 -0
- data/lib/lighthouse/field_resolver_stack.rb +53 -6
- 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 +33 -0
- data/lib/lighthouse/relation_counts.rb +29 -0
- data/lib/lighthouse/resolve_info.rb +80 -0
- data/lib/lighthouse/resolver_bases.rb +9 -2
- 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_generator.rb +11 -10
- data/lib/lighthouse/schema_implementation.rb +4 -8
- data/lib/lighthouse/support/environment.rb +13 -0
- data/lib/lighthouse/support/model_resolver.rb +7 -5
- 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.rbs +18 -0
- metadata +38 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Lighthouse::GraphQL::Directives::Arguments
|
|
6
|
+
# @whereNull — `true` keeps the rows whose column is null, `false` those whose
|
|
7
|
+
# column is not. `posts(unpublished: Boolean @whereNull(key: "published_at"))`
|
|
8
|
+
class WhereNull < Base
|
|
9
|
+
def self.directive_name = 'whereNull'
|
|
10
|
+
|
|
11
|
+
def self.definition
|
|
12
|
+
<<~GRAPHQL.chomp
|
|
13
|
+
"""
|
|
14
|
+
Filter the value is null: true keeps the rows where the column is null, false the others.
|
|
15
|
+
"""
|
|
16
|
+
directive @whereNull(
|
|
17
|
+
"The database column. Defaults to the argument's name."
|
|
18
|
+
key: String
|
|
19
|
+
) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
|
|
20
|
+
GRAPHQL
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def handle_builder(relation, value)
|
|
24
|
+
return relation if value.nil?
|
|
25
|
+
|
|
26
|
+
value ? relation.where(column => nil) : relation.where.not(column => nil)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @whereNotNull — the inverse: `true` keeps the rows whose column is not null.
|
|
31
|
+
class WhereNotNull < Base
|
|
32
|
+
def self.directive_name = 'whereNotNull'
|
|
33
|
+
|
|
34
|
+
def self.definition
|
|
35
|
+
<<~GRAPHQL.chomp
|
|
36
|
+
"""
|
|
37
|
+
Filter the value is not null: true keeps the rows where the column is not null, false the others.
|
|
38
|
+
"""
|
|
39
|
+
directive @whereNotNull(
|
|
40
|
+
"The database column. Defaults to the argument's name."
|
|
41
|
+
key: String
|
|
42
|
+
) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
|
|
43
|
+
GRAPHQL
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def handle_builder(relation, value)
|
|
47
|
+
return relation if value.nil?
|
|
48
|
+
|
|
49
|
+
value ? relation.where.not(column => nil) : relation.where(column => nil)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @whereBetween — the column lies between two values: an input object's two
|
|
54
|
+
# fields, in order, or a list of two. `createdAt: DateRange @whereBetween(key: "created_at")`
|
|
55
|
+
class WhereBetween < Base
|
|
56
|
+
def self.directive_name = 'whereBetween'
|
|
57
|
+
|
|
58
|
+
def self.definition
|
|
59
|
+
<<~GRAPHQL.chomp
|
|
60
|
+
"""
|
|
61
|
+
Verify that a column's value is between two values: the argument's two input fields, in order.
|
|
62
|
+
"""
|
|
63
|
+
directive @whereBetween(
|
|
64
|
+
"The database column. Defaults to the argument's name."
|
|
65
|
+
key: String
|
|
66
|
+
) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
|
|
67
|
+
GRAPHQL
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def handle_builder(relation, value)
|
|
71
|
+
from, to = bounds(value)
|
|
72
|
+
return relation if from.nil? && to.nil?
|
|
73
|
+
|
|
74
|
+
relation.where(column => from..to)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def bounds(value)
|
|
80
|
+
values = value.respond_to?(:to_h) && !value.is_a?(Array) ? value.to_h.values : Array(value)
|
|
81
|
+
values.first(2)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @whereNotBetween — the column lies outside two values.
|
|
86
|
+
class WhereNotBetween < WhereBetween
|
|
87
|
+
def self.directive_name = 'whereNotBetween'
|
|
88
|
+
|
|
89
|
+
def self.definition
|
|
90
|
+
<<~GRAPHQL.chomp
|
|
91
|
+
"""
|
|
92
|
+
Verify that a column's value lies outside two values: the argument's two input fields, in order.
|
|
93
|
+
"""
|
|
94
|
+
directive @whereNotBetween(
|
|
95
|
+
"The database column. Defaults to the argument's name."
|
|
96
|
+
key: String
|
|
97
|
+
) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
|
|
98
|
+
GRAPHQL
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def handle_builder(relation, value)
|
|
102
|
+
from, to = bounds(value)
|
|
103
|
+
return relation if from.nil? && to.nil?
|
|
104
|
+
|
|
105
|
+
relation.where.not(column => from..to)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @whereJsonContains — a JSON column contains the value (Laravel's
|
|
110
|
+
# whereJsonContains), on PostgreSQL (@>), MySQL (JSON_CONTAINS) and SQLite
|
|
111
|
+
# (json_each). `key:` may point into the document: "options->tags".
|
|
112
|
+
class WhereJsonContains < Base
|
|
113
|
+
def self.directive_name = 'whereJsonContains'
|
|
114
|
+
|
|
115
|
+
def self.definition
|
|
116
|
+
<<~GRAPHQL.chomp
|
|
117
|
+
"""
|
|
118
|
+
Use an input value as a whereJsonContains filter.
|
|
119
|
+
"""
|
|
120
|
+
directive @whereJsonContains(
|
|
121
|
+
"The JSON column, optionally with a path into it: \\"options->tags\\". Defaults to the argument's name."
|
|
122
|
+
key: String
|
|
123
|
+
) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
|
|
124
|
+
GRAPHQL
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def handle_builder(relation, value)
|
|
128
|
+
return relation if value.nil?
|
|
129
|
+
|
|
130
|
+
connection = relation.connection
|
|
131
|
+
name, *path = column.to_s.split('->')
|
|
132
|
+
target = "#{connection.quote_table_name(relation.klass.table_name)}.#{connection.quote_column_name(name)}"
|
|
133
|
+
relation.where(*condition(connection.adapter_name, target, path, value))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def condition(adapter, target, path, value)
|
|
139
|
+
json_path = "$#{path.map { |segment| ".#{segment}" }.join}"
|
|
140
|
+
case adapter
|
|
141
|
+
when /postg/i then ["(#{target}#{path.map { |segment| "->'#{segment}'" }.join})::jsonb @> ?::jsonb", value.to_json]
|
|
142
|
+
when /mysql|trilogy/i then ["JSON_CONTAINS(#{target}, ?, ?)", value.to_json, json_path]
|
|
143
|
+
else sqlite(target, json_path, value)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Every value of a list must be in the array, as JSON containment means.
|
|
148
|
+
def sqlite(target, json_path, value)
|
|
149
|
+
values = Array(value)
|
|
150
|
+
sql = values.map { "EXISTS (SELECT 1 FROM json_each(#{target}, ?) WHERE json_each.value = ?)" }.join(' AND ')
|
|
151
|
+
[sql, *values.flat_map { |item| [json_path, item] }]
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# @limit — the argument directive behind LimitResolver. Only `builder: true`
|
|
156
|
+
# makes it one: a LIMIT on the field's query. Without it, LimitResolver cuts
|
|
157
|
+
# the results instead.
|
|
158
|
+
class Limit < Base
|
|
159
|
+
def self.directive_name = 'limit'
|
|
160
|
+
|
|
161
|
+
def self.definition
|
|
162
|
+
<<~GRAPHQL.chomp
|
|
163
|
+
"""
|
|
164
|
+
Allow clients to specify the maximum number of results to return when used on an argument,
|
|
165
|
+
or statically limit them when used on a field.
|
|
166
|
+
|
|
167
|
+
By default, this directive does not influence the number of results the resolver queries internally,
|
|
168
|
+
but limits how much of it is returned to clients. Use the `builder` argument to change this.
|
|
169
|
+
"""
|
|
170
|
+
directive @limit(
|
|
171
|
+
"""
|
|
172
|
+
You may set this to `true` if the field uses a query builder,
|
|
173
|
+
then this directive will apply a LIMIT clause to it.
|
|
174
|
+
Typically, this option should only be used for root fields,
|
|
175
|
+
as it may cause wrong results with batched relation queries.
|
|
176
|
+
"""
|
|
177
|
+
builder: Boolean! = false
|
|
178
|
+
) on ARGUMENT_DEFINITION | FIELD_DEFINITION
|
|
179
|
+
GRAPHQL
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def handle_builder(relation, value)
|
|
183
|
+
return relation unless value.is_a?(Integer) && Lighthouse::Support::DirectiveArgs.literal(@directive_node, 'builder')
|
|
184
|
+
|
|
185
|
+
relation.limit(value)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# @scope — call a model scope with the client's value.
|
|
190
|
+
# `posts(trending: Boolean @scope)` calls Post.trending(value); `name:` names another scope.
|
|
191
|
+
class Scope < Base
|
|
192
|
+
def self.directive_name = 'scope'
|
|
193
|
+
|
|
194
|
+
def self.definition
|
|
195
|
+
<<~GRAPHQL.chomp
|
|
196
|
+
"""
|
|
197
|
+
Adds a scope to the query builder.
|
|
198
|
+
The scope method will receive the client-given value of the argument as its argument.
|
|
199
|
+
"""
|
|
200
|
+
directive @scope(
|
|
201
|
+
"The name of the scope. Defaults to the name of the argument."
|
|
202
|
+
name: String
|
|
203
|
+
) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
|
|
204
|
+
GRAPHQL
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def handle_builder(relation, value)
|
|
208
|
+
return relation if value.nil?
|
|
209
|
+
|
|
210
|
+
relation.public_send(
|
|
211
|
+
Lighthouse::Support::DirectiveArgs.literal(@directive_node, 'name') || Lighthouse::Support::Naming.attribute(@arg_node.name), value
|
|
212
|
+
)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The SDL of the directives no registered resolver class contributes, and of
|
|
4
|
+
# the types they refer to: the field-level query directives (@builder,
|
|
5
|
+
# @whereAuth), the argument directives, the type-system ones (@union, @node,
|
|
6
|
+
# @model, ...), and those the schema reads while it is built. The registry
|
|
7
|
+
# injects each into a schema that uses it without declaring it.
|
|
8
|
+
module Lighthouse::GraphQL::Directives::Definitions
|
|
9
|
+
# Directives that only mark the SDL for others to read.
|
|
10
|
+
PASSIVE = {
|
|
11
|
+
'nest' => 'directive @nest on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION',
|
|
12
|
+
'spread' => 'directive @spread on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION',
|
|
13
|
+
'inject' => 'directive @inject(context: String!, name: String!) repeatable on FIELD_DEFINITION',
|
|
14
|
+
'cacheKey' => 'directive @cacheKey on FIELD_DEFINITION'
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# { name => "directive @name ..." }
|
|
20
|
+
def directives
|
|
21
|
+
graphql = Lighthouse::GraphQL
|
|
22
|
+
[
|
|
23
|
+
graphql::DirectiveResolvers::BuilderApplier::DEFINITIONS,
|
|
24
|
+
graphql::DirectiveResolvers::ArgumentTransformsResolver::DEFINITIONS,
|
|
25
|
+
PASSIVE,
|
|
26
|
+
{ 'cacheControl' => graphql::CacheControl::DEFINITION, 'feature' => graphql::FeatureDirective::DEFINITION,
|
|
27
|
+
'complexity' => graphql::Complexity::DEFINITION, 'scalar' => graphql::ScalarCoercion::DEFINITION,
|
|
28
|
+
'enum' => graphql::EnumDirective::DEFINITION },
|
|
29
|
+
through_aliases,
|
|
30
|
+
graphql::TypeResolution::DEFINITIONS,
|
|
31
|
+
graphql::Directives::NodeDirective::DEFINITIONS,
|
|
32
|
+
graphql::Directives::ModelDirective::DEFINITIONS,
|
|
33
|
+
graphql::Directives::VisibilityDirective::DEFINITIONS,
|
|
34
|
+
graphql::Directives::WhereConditionsDirective::DEFINITIONS,
|
|
35
|
+
graphql::Directives::SoftDeletesDirective::DEFINITIONS
|
|
36
|
+
].reduce(:merge)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @hasManyThrough and @hasOneThrough: @hasMany's and @hasOne's definitions, renamed.
|
|
40
|
+
def through_aliases
|
|
41
|
+
resolvers = Lighthouse::GraphQL::DirectiveResolvers
|
|
42
|
+
{ 'hasManyThrough' => resolvers::HasManyDirectiveResolver, 'hasOneThrough' => resolvers::HasOneDirectiveResolver }
|
|
43
|
+
.to_h { |name, klass| [name, klass.definition.sub("@#{klass.directive_name}(", "@#{name}(")] }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# { type name => its SDL }, for the types the directives above refer to.
|
|
47
|
+
def types
|
|
48
|
+
graphql = Lighthouse::GraphQL
|
|
49
|
+
[graphql::CacheControl::TYPE_DEFINITIONS, graphql::FeatureDirective::TYPE_DEFINITIONS, graphql::EnumDirective::TYPE_DEFINITIONS,
|
|
50
|
+
graphql::DirectiveResolvers::BuilderApplier::TYPE_DEFINITIONS].reduce(:merge)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -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,17 +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
|
-
register_definition('complexity', Lighthouse::GraphQL::Complexity::DEFINITION)
|
|
222
|
-
Lighthouse::GraphQL::FeatureDirective::TYPE_DEFINITIONS.each { |name, sdl| register_type_definition(name, sdl) }
|
|
223
|
-
Lighthouse::GraphQL::Directives::WhereConditionsDirective::DEFINITIONS.each do |name, definition|
|
|
224
|
-
register_definition(name, definition)
|
|
225
|
-
end
|
|
226
|
-
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) }
|
|
227
227
|
end
|
|
228
228
|
|
|
229
229
|
def safe_definition(klass)
|