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
data/docs/resolvers.md
CHANGED
|
@@ -10,30 +10,39 @@ interchangeable. Each kind has its own contract and its own folder:
|
|
|
10
10
|
| Resolver | `app/graphql/resolvers` · `Resolvers::` | `Resolvers::BaseResolver` (a `GraphQL::Schema::Resolver`) | `Query` fields |
|
|
11
11
|
| Mutation | `app/graphql/mutations` · `Mutations::` | `Mutations::BaseMutation` (a `GraphQL::Schema::Mutation`) | `Mutation` fields |
|
|
12
12
|
| Subscription | `app/graphql/subscriptions` · `Subscriptions::` | `Subscriptions::BaseSubscription` (a `GraphQL::Schema::Subscription`) | `Subscription` fields ([subscriptions.md](subscriptions.md)) |
|
|
13
|
+
| Type | `app/graphql/types` · `Types::` | `Types::BaseObject` (a `GraphQL::Schema::Object`) including `Lighthouse::GraphQL::Type`, named `<Type>Type` | Fields of other types, through its methods ([type classes](#type-classes)); unions, interfaces and scalars too ([types.md](types.md)) |
|
|
13
14
|
| Builder | `app/graphql/builders` · `Builders::` | `call(relation, args, context)` → relation | `@builder` |
|
|
14
15
|
|
|
15
|
-
Validators (`app/graphql/validators`) are
|
|
16
|
+
Validators (`app/graphql/validators`) are one more kind. They are covered in
|
|
16
17
|
[validation.md](validation.md).
|
|
17
18
|
|
|
18
19
|
## Base classes
|
|
19
20
|
|
|
20
|
-
As in any graphql-ruby app, each of
|
|
21
|
-
|
|
21
|
+
As in any graphql-ruby app, each kind of class has a base class in its namespace,
|
|
22
|
+
and every class of the kind inherits from it. graphql-ruby's `graphql:install`,
|
|
23
|
+
which `rails g lighthouse:install` runs, writes `Resolvers::BaseResolver`,
|
|
24
|
+
`Mutations::BaseMutation` and the `Types::Base*` classes; Lighthouse adds
|
|
25
|
+
`Subscriptions::BaseSubscription`, which graphql-ruby has none of, and makes the
|
|
26
|
+
mutation base a plain `GraphQL::Schema::Mutation`, as the SDL declares a mutation's
|
|
27
|
+
arguments ([generators.md](generators.md)):
|
|
22
28
|
|
|
23
29
|
```ruby
|
|
24
|
-
# app/graphql/resolvers/base_resolver.rb
|
|
30
|
+
# app/graphql/resolvers/base_resolver.rb (graphql-ruby's)
|
|
25
31
|
module Resolvers
|
|
26
32
|
class BaseResolver < GraphQL::Schema::Resolver
|
|
27
33
|
end
|
|
28
34
|
end
|
|
29
35
|
|
|
30
|
-
# app/graphql/mutations/base_mutation.rb
|
|
36
|
+
# app/graphql/mutations/base_mutation.rb (graphql-ruby's, made a Mutation)
|
|
31
37
|
module Mutations
|
|
32
38
|
class BaseMutation < GraphQL::Schema::Mutation
|
|
39
|
+
argument_class Types::BaseArgument
|
|
40
|
+
field_class Types::BaseField
|
|
41
|
+
object_class Types::BaseObject
|
|
33
42
|
end
|
|
34
43
|
end
|
|
35
44
|
|
|
36
|
-
# app/graphql/subscriptions/base_subscription.rb
|
|
45
|
+
# app/graphql/subscriptions/base_subscription.rb (Lighthouse's)
|
|
37
46
|
module Subscriptions
|
|
38
47
|
class BaseSubscription < GraphQL::Schema::Subscription
|
|
39
48
|
include Lighthouse::GraphQL::Subscription
|
|
@@ -43,11 +52,8 @@ end
|
|
|
43
52
|
|
|
44
53
|
What every class of a kind shares goes in its base, once: a concern (as
|
|
45
54
|
`BaseSubscription` includes Lighthouse's, see [subscriptions.md](subscriptions.md)),
|
|
46
|
-
a helper, graphql-ruby settings such as `argument_class Types::BaseArgument`.
|
|
47
|
-
|
|
48
|
-
one they need when it is missing. graphql-ruby's own `graphql:install` writes
|
|
49
|
-
`Resolvers::BaseResolver` and `Mutations::BaseMutation` too, and Lighthouse keeps
|
|
50
|
-
whatever is already there: an existing base is never replaced.
|
|
55
|
+
a helper, graphql-ruby settings such as `argument_class Types::BaseArgument`. An
|
|
56
|
+
existing base is never replaced.
|
|
51
57
|
|
|
52
58
|
### Handing the bases to the schema
|
|
53
59
|
|
|
@@ -61,8 +67,13 @@ AppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(
|
|
|
61
67
|
base_types: {
|
|
62
68
|
resolver: Resolvers::BaseResolver,
|
|
63
69
|
mutation: Mutations::BaseMutation,
|
|
64
|
-
subscription: Subscriptions::BaseSubscription
|
|
65
|
-
|
|
70
|
+
subscription: Subscriptions::BaseSubscription,
|
|
71
|
+
object: Types::BaseObject,
|
|
72
|
+
interface: Types::BaseInterface,
|
|
73
|
+
union: Types::BaseUnion,
|
|
74
|
+
scalar: Types::BaseScalar,
|
|
75
|
+
enum: Types::BaseEnum,
|
|
76
|
+
input_object: Types::BaseInputObject
|
|
66
77
|
}
|
|
67
78
|
}
|
|
68
79
|
)
|
|
@@ -78,6 +89,10 @@ AppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(
|
|
|
78
89
|
(`GraphQL::Schema::Resolver`, `GraphQL::Schema::Mutation`, which
|
|
79
90
|
`RelayClassicMutation` is, `GraphQL::Schema::Subscription`), or the schema does not
|
|
80
91
|
build. graphql-ruby never sees these three keys.
|
|
92
|
+
- The others are graphql-ruby's: it builds the SDL's types of each kind from them.
|
|
93
|
+
A [type class](#type-classes) (one including `Lighthouse::GraphQL::Type`) that
|
|
94
|
+
does not inherit from its kind's raises `Lighthouse::DefinitionError` when the
|
|
95
|
+
schema is built.
|
|
81
96
|
|
|
82
97
|
`rails g lighthouse:install` writes the schema class with them. Without them,
|
|
83
98
|
Lighthouse requires only the graphql-ruby class (`GraphQL::Schema::Resolver`, …),
|
|
@@ -174,7 +189,233 @@ type Query {
|
|
|
174
189
|
```
|
|
175
190
|
|
|
176
191
|
Besides graphql-ruby resolver classes, `@field` also accepts a plain callable:
|
|
177
|
-
anything with `call(object, args, context)
|
|
192
|
+
anything with `call(object, args, context)`, and `call(object, args, context,
|
|
193
|
+
resolve_info)` when it takes a fourth argument.
|
|
194
|
+
|
|
195
|
+
A method after `@` (PHP Lighthouse's `Class@method`) replaces `call`, or `resolve` on
|
|
196
|
+
a resolver class (whose `authorized?` still runs). One class can then hold several
|
|
197
|
+
fields' resolvers:
|
|
198
|
+
|
|
199
|
+
```graphql
|
|
200
|
+
type Query {
|
|
201
|
+
counts: Counts! @field(resolver: "Statistics@counts")
|
|
202
|
+
latest: [Post!]! @field(resolver: "Statistics@latest")
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
class Resolvers::Statistics
|
|
208
|
+
def self.counts(_root, _args, _context) = { posts: Post.count, users: User.count }
|
|
209
|
+
def self.latest(_root, _args, _context) = Post.order(created_at: :desc).limit(5)
|
|
210
|
+
end
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### ResolveInfo
|
|
214
|
+
|
|
215
|
+
What PHP Lighthouse hands a resolver as `$resolveInfo`. A `Resolvers::` or
|
|
216
|
+
`Mutations::` class, a class `@field` names and a type class's methods read it as
|
|
217
|
+
`resolve_info`:
|
|
218
|
+
|
|
219
|
+
| | |
|
|
220
|
+
| --- | --- |
|
|
221
|
+
| `field_name`, `field`, `return_type` | the field being resolved |
|
|
222
|
+
| `parent_type` | the type it belongs to |
|
|
223
|
+
| `object`, `arguments`, `context` | what it resolves with |
|
|
224
|
+
| `path` | `["posts", 0, "author"]` |
|
|
225
|
+
| `lookahead` | graphql-ruby's [`Lookahead`](https://graphql-ruby.org/queries/lookahead.html): what the client selects below the field (`lookahead.selects?(:author)`) |
|
|
226
|
+
| `enhance_builder(relation, scopes: [])` | `relation` with the field's query directives applied |
|
|
227
|
+
|
|
228
|
+
`enhance_builder` lets a class build the query no directive could, and still honour
|
|
229
|
+
what the field declares: its `@builder` classes, and the argument directives the
|
|
230
|
+
client used (`@eq`, `@where`, `@in`, `@like`, `@orderBy`, `@whereConditions`, ...),
|
|
231
|
+
as `@all` and `@paginate` apply them. `scopes:` adds model scopes.
|
|
232
|
+
|
|
233
|
+
```graphql
|
|
234
|
+
type Query {
|
|
235
|
+
myPosts(title: String @like, orderBy: [PostOrderByClause!] @orderBy): [Post!]! @guard
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
class Resolvers::MyPosts < Resolvers::BaseResolver
|
|
241
|
+
def resolve(**)
|
|
242
|
+
resolve_info.enhance_builder(context[:current_user].posts, scopes: [:recent])
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Fields of other types
|
|
248
|
+
|
|
249
|
+
A field of any other type resolves with the default reader: `Post.commentsCount`
|
|
250
|
+
reads `post.comments_count`. When its value takes more than reading an attribute
|
|
251
|
+
or calling a method ([`@method`](directives.md#method)), name a class with
|
|
252
|
+
`@field`. The class is a resolver like the ones above, and its `object` is the
|
|
253
|
+
**parent**: the record the field belongs to (PHP Lighthouse's `$root`). It
|
|
254
|
+
resolves wherever the type appears in a response: in a list, under a
|
|
255
|
+
relationship, or under another class's result.
|
|
256
|
+
|
|
257
|
+
```graphql
|
|
258
|
+
type Post {
|
|
259
|
+
tagsCount: Int! @field(resolver: "Posts::TagsCount")
|
|
260
|
+
excerpt(length: Int = 80): String @field(resolver: "Posts::Excerpt")
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
```ruby
|
|
265
|
+
# app/graphql/resolvers/posts/tags_count.rb
|
|
266
|
+
module Resolvers
|
|
267
|
+
module Posts
|
|
268
|
+
class TagsCount < BaseResolver
|
|
269
|
+
def resolve
|
|
270
|
+
dataload_association(object, :tags).size
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# app/graphql/resolvers/posts/excerpt.rb
|
|
277
|
+
module Resolvers
|
|
278
|
+
module Posts
|
|
279
|
+
class Excerpt < BaseResolver
|
|
280
|
+
def resolve(length:)
|
|
281
|
+
object.body&.truncate(length, separator: ' ')
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
```graphql
|
|
289
|
+
{
|
|
290
|
+
posts(first: 10) { data { title tagsCount excerpt(length: 40) } }
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
The field's arguments arrive as keyword arguments, with the SDL's default when the
|
|
295
|
+
client leaves one out, as on a root field. The class runs once per parent, so load
|
|
296
|
+
anything beyond the parent's own columns through graphql-ruby's dataloader
|
|
297
|
+
(`dataload_association`, `dataload_record`, or a `GraphQL::Dataloader::Source` of
|
|
298
|
+
your own). Then listing ten posts runs one query for their tags, not ten.
|
|
299
|
+
|
|
300
|
+
A field can also resolve with a method of its type's class, as in a graphql-ruby
|
|
301
|
+
app. See [type classes](#type-classes), next: they suit a field that only the type
|
|
302
|
+
needs, and a class named by `@field` suits one with its own logic, its own
|
|
303
|
+
arguments, or that you want to find in the schema.
|
|
304
|
+
|
|
305
|
+
On these types the name is looked up in the resolvers namespace first, a nested
|
|
306
|
+
name too, so `Posts::TagsCount` is `Resolvers::Posts::TagsCount`, kept in
|
|
307
|
+
`app/graphql/resolvers/posts/`. A folder per type keeps these classes apart from
|
|
308
|
+
the `Query` ones. Name it in the plural (`Posts`): a `Resolvers::Post` module would
|
|
309
|
+
hide the `Post` model from code written inside `module Resolvers`. When the
|
|
310
|
+
resolvers namespace has no such class, the name is looked up in
|
|
311
|
+
[`resolver_namespaces`](configuration.md#resolver_namespaces), and a full name
|
|
312
|
+
(`Resolvers::Posts::TagsCount`) works as well. The schema's `base_types` are not
|
|
313
|
+
enforced here, but inheriting from `Resolvers::BaseResolver` still gives the class
|
|
314
|
+
what every resolver shares.
|
|
315
|
+
|
|
316
|
+
### Type classes
|
|
317
|
+
|
|
318
|
+
graphql-ruby keeps the Ruby side of each object type in `app/graphql/types`:
|
|
319
|
+
`Types::PostType`, inheriting from `Types::BaseObject`, as `rails g graphql:object
|
|
320
|
+
Post` writes it. Lighthouse keeps that structure. A type class that includes `Lighthouse::GraphQL::Type` is handed to
|
|
321
|
+
Lighthouse: the SDL still declares the type and its fields, and the class says how
|
|
322
|
+
the fields with no resolving directive resolve. A field calls the method of its
|
|
323
|
+
snake_case name, when the class defines it. Fields it does not define are read off
|
|
324
|
+
the object, as before, and fields with a resolving directive keep it.
|
|
325
|
+
|
|
326
|
+
That makes a type that is not a model as easy to resolve as a model's: the parent
|
|
327
|
+
returns any object (a Hash, a Struct, a client's response), and the type's class
|
|
328
|
+
reads or computes each field from it.
|
|
329
|
+
|
|
330
|
+
```graphql
|
|
331
|
+
type Post {
|
|
332
|
+
title: String!
|
|
333
|
+
readingTime: ReadingTime!
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
type ReadingTime {
|
|
337
|
+
words: Int!
|
|
338
|
+
minutes(wordsPerMinute: Int = 200): Int!
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
```ruby
|
|
343
|
+
# app/graphql/types/post_type.rb
|
|
344
|
+
module Types
|
|
345
|
+
class PostType < BaseObject
|
|
346
|
+
include Lighthouse::GraphQL::Type
|
|
347
|
+
|
|
348
|
+
def reading_time
|
|
349
|
+
{ words: object.body.to_s.split.size }
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# app/graphql/types/reading_time_type.rb
|
|
355
|
+
module Types
|
|
356
|
+
class ReadingTimeType < BaseObject
|
|
357
|
+
include Lighthouse::GraphQL::Type
|
|
358
|
+
|
|
359
|
+
def minutes(words_per_minute:)
|
|
360
|
+
[(object[:words] / words_per_minute.to_f).ceil, 1].max
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
`Post.title` is the model's column. `Post.readingTime` calls
|
|
367
|
+
`Types::PostType#reading_time`. `ReadingTime.words` is read off the Hash it
|
|
368
|
+
returns, and `ReadingTime.minutes` calls `Types::ReadingTimeType#minutes`.
|
|
369
|
+
|
|
370
|
+
As in graphql-ruby, the class is instantiated per parent, so a method has:
|
|
371
|
+
|
|
372
|
+
- `object`: the parent, here the post;
|
|
373
|
+
- `context`: the query's context;
|
|
374
|
+
- the field's arguments, as snake_case keywords, with the SDL's default when the
|
|
375
|
+
client leaves one out;
|
|
376
|
+
- graphql-ruby's `dataload`, `dataload_record` and `dataload_association`, to load
|
|
377
|
+
anything beyond the parent's own columns without N+1.
|
|
378
|
+
|
|
379
|
+
Wrapping directives (`@guard`, `@can*`, `@cache`) on the field run around the
|
|
380
|
+
method. A method every `GraphQL::Schema::Object` has (`object`, `context`) never
|
|
381
|
+
resolves a field.
|
|
382
|
+
|
|
383
|
+
#### The model
|
|
384
|
+
|
|
385
|
+
The type's model is the SDL's to say, as in PHP Lighthouse: a type named after its
|
|
386
|
+
model needs nothing, and [`@model`](directives.md#model) maps one that is not:
|
|
387
|
+
|
|
388
|
+
```graphql
|
|
389
|
+
type Article @model(class: "Post") {
|
|
390
|
+
title: String!
|
|
391
|
+
headline: String!
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
`Types::ArticleType`'s methods then only ever run on a `Post`: a parent of another
|
|
396
|
+
class (a Hash a resolver returned by mistake, another model) is reported through
|
|
397
|
+
[`on_error`](configuration.md#on_error), which raises in test, and the field
|
|
398
|
+
resolves to `null`. `@model` also gives `@all`, `@find`, `@paginate`, `@create`... on
|
|
399
|
+
fields returning `Article` their model, and resolves a `Post` in a union or
|
|
400
|
+
interface to `Article` ([types.md](types.md#the-default)).
|
|
401
|
+
|
|
402
|
+
#### Opting in
|
|
403
|
+
|
|
404
|
+
The class is the type's name plus `Type`, looked up in `namespaces[:types]`
|
|
405
|
+
(default `['Types']`) when the schema is built. Only a class that includes
|
|
406
|
+
`Lighthouse::GraphQL::Type` counts: any other `Types::PostType`, such as the ones
|
|
407
|
+
a graphql-ruby app already has, is left alone, and its methods resolve nothing.
|
|
408
|
+
Include the concern in `Types::BaseObject` to hand every type class over at once.
|
|
409
|
+
|
|
410
|
+
A class that includes the concern must inherit from the schema's
|
|
411
|
+
`base_types[:object]` when one is given, or the schema does not build. The concern
|
|
412
|
+
also hands over the classes of unions, interfaces and scalars, which pick the type
|
|
413
|
+
of a union's values or coerce a scalar's: see [types.md](types.md). It is not the schema's type: it only lends its
|
|
414
|
+
methods. Fields are declared in the SDL, so `field` declarations in it are ignored,
|
|
415
|
+
and graphql-ruby's type-level `authorized?` does not run; authorize with
|
|
416
|
+
[`@can*`](authorization.md). `Query`, `Mutation` and `Subscription` fields resolve
|
|
417
|
+
with their own classes, [by convention](#resolution-by-convention), not with a type
|
|
418
|
+
class.
|
|
178
419
|
|
|
179
420
|
### `@spread` and `@inject`
|
|
180
421
|
|
|
@@ -240,16 +481,14 @@ bin/rails generate lighthouse:mutation PublishPost # app/graphql/mutations/p
|
|
|
240
481
|
bin/rails generate lighthouse:subscription PostUpdated # app/graphql/subscriptions/post_updated.rb
|
|
241
482
|
bin/rails generate lighthouse:builder Published # app/graphql/builders/published.rb
|
|
242
483
|
bin/rails generate lighthouse:validator CreatePostInput # app/graphql/validators/create_post_input_validator.rb
|
|
484
|
+
bin/rails generate graphql:object Post # a type class: then include Lighthouse::GraphQL::Type
|
|
243
485
|
```
|
|
244
486
|
|
|
245
|
-
Each
|
|
246
|
-
|
|
247
|
-
from
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
generators do not collide with graphql-ruby's `graphql:*` generators. A
|
|
251
|
-
`lighthouse:mutation` and a `graphql:mutation` of the same name target the same
|
|
252
|
-
file, and Rails asks before overwriting it.
|
|
487
|
+
Each writes the class with its contract, into the first namespace configured for its
|
|
488
|
+
kind (or `--namespace=…`), inheriting from the namespace's base class. Type classes
|
|
489
|
+
come from graphql-ruby's own `graphql:object`, `graphql:scalar`, `graphql:union` and
|
|
490
|
+
`graphql:interface`. [generators.md](generators.md) has what each generator writes,
|
|
491
|
+
and which of graphql-ruby's fit a Lighthouse app.
|
|
253
492
|
|
|
254
493
|
## Changing the namespaces
|
|
255
494
|
|
data/docs/security.md
CHANGED
|
@@ -208,3 +208,20 @@ GraphQL::Analysis.analyze_query(query, [GraphQL::Analysis::QueryComplexity]).fir
|
|
|
208
208
|
`QueryComplexity` logs a warning unless the schema chooses an algorithm with
|
|
209
209
|
`AppSchema.complexity_cost_calculation_mode(:future)`. `max_complexity` and
|
|
210
210
|
`query_complexity` always use the `:future` algorithm.
|
|
211
|
+
|
|
212
|
+
## Rate limiting
|
|
213
|
+
|
|
214
|
+
[`@throttle`](directives.md#throttle) caps how often one client may call a field —
|
|
215
|
+
a login, a search, an expensive mutation:
|
|
216
|
+
|
|
217
|
+
```graphql
|
|
218
|
+
type Mutation {
|
|
219
|
+
login(email: String!, password: String!): User @throttle(maxAttempts: 5, decayMinutes: 1)
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Attempts are counted in [`cache_store`](configuration.md#cache_store), so use a store
|
|
224
|
+
the app servers share (Redis, Solid Cache) in production; `:null_store` counts
|
|
225
|
+
nothing. A client is the signed-in user, else the `:remote_ip` the controller concern
|
|
226
|
+
puts in the context (override `graphql_context` to set another). A refused call never
|
|
227
|
+
reaches `@guard` or the resolver.
|
data/docs/types.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Unions, Interfaces and Scalars
|
|
2
|
+
|
|
3
|
+
The SDL declares every type. Object types resolve their fields through
|
|
4
|
+
directives, the default reader, or a [type class](resolvers.md#type-classes).
|
|
5
|
+
Three more kinds need Ruby to say how their values work:
|
|
6
|
+
|
|
7
|
+
- a **union** or an **interface** needs to know which object type each value is;
|
|
8
|
+
- a custom **scalar** needs to know how to read its values from a query and write
|
|
9
|
+
them to a response.
|
|
10
|
+
|
|
11
|
+
Lighthouse answers both as [PHP Lighthouse](https://lighthouse-php.com/master/the-basics/types.html)
|
|
12
|
+
does by default, and lets you take over with a class, in graphql-ruby's
|
|
13
|
+
`app/graphql/types` structure, or with a directive.
|
|
14
|
+
|
|
15
|
+
## Unions and interfaces
|
|
16
|
+
|
|
17
|
+
```graphql
|
|
18
|
+
type Query {
|
|
19
|
+
search(term: String!): [SearchResult!]!
|
|
20
|
+
latestActivity: [Authored!]!
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
union SearchResult = Post | User
|
|
24
|
+
|
|
25
|
+
interface Authored {
|
|
26
|
+
id: ID!
|
|
27
|
+
author: User!
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type Post implements Authored { id: ID! title: String! author: User! }
|
|
31
|
+
type Comment implements Authored { id: ID! body: String! author: User! }
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
A resolver returns records, of any of the types:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
class Resolvers::Search < Resolvers::BaseResolver
|
|
38
|
+
def resolve(term:)
|
|
39
|
+
Post.where('title LIKE ?', "%#{term}%").to_a + User.where('name LIKE ?', "%#{term}%").to_a
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```graphql
|
|
45
|
+
{
|
|
46
|
+
search(term: "Grace") {
|
|
47
|
+
__typename
|
|
48
|
+
... on Post { title }
|
|
49
|
+
... on User { name }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### The default
|
|
55
|
+
|
|
56
|
+
Each value resolves to the first of these that is one of the union's (or the
|
|
57
|
+
interface's) types:
|
|
58
|
+
|
|
59
|
+
1. a Hash's `__typename` (`{ __typename: 'Movie', title: 'Heat' }`);
|
|
60
|
+
2. the type named after the value's class (a `Post` record is a `Post`), or after
|
|
61
|
+
one of its superclasses, so an `Admin < User` STI record is a `User` when no
|
|
62
|
+
`Admin` type exists;
|
|
63
|
+
3. the type whose [`@model`](directives.md#model) names the value's model. With
|
|
64
|
+
`type Article @model(class: "Post")`, a `Post` record is an `Article`.
|
|
65
|
+
|
|
66
|
+
Most schemas need nothing else.
|
|
67
|
+
|
|
68
|
+
### A type class
|
|
69
|
+
|
|
70
|
+
When the default is not enough, say when the values are Hashes from a search
|
|
71
|
+
service rather than records, write the union's class as graphql-ruby does
|
|
72
|
+
(`rails g graphql:union SearchResult`),
|
|
73
|
+
`Types::<Name>Type` including `Lighthouse::GraphQL::Type`, with `resolve_type`.
|
|
74
|
+
It returns a type name, or a type class, which stands for the SDL type of its
|
|
75
|
+
`graphql_name`:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
# app/graphql/types/search_result_type.rb
|
|
79
|
+
module Types
|
|
80
|
+
class SearchResultType < BaseUnion
|
|
81
|
+
include Lighthouse::GraphQL::Type
|
|
82
|
+
|
|
83
|
+
def self.resolve_type(object, context)
|
|
84
|
+
return unless object.is_a?(Hash)
|
|
85
|
+
|
|
86
|
+
object['index'] == 'users' ? Types::UserType : 'Post'
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
An interface's goes in graphql-ruby's `definition_methods`:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
# app/graphql/types/authored_type.rb
|
|
96
|
+
module Types
|
|
97
|
+
module AuthoredType
|
|
98
|
+
include BaseInterface
|
|
99
|
+
include Lighthouse::GraphQL::Type
|
|
100
|
+
|
|
101
|
+
definition_methods do
|
|
102
|
+
def resolve_type(object, context)
|
|
103
|
+
object['index'] == 'comments' ? 'Comment' : 'Post' if object.is_a?(Hash)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`nil` leaves the value to the default, so the class only handles the cases that
|
|
111
|
+
need it. The SDL still declares the members: `possible_types` in the class are
|
|
112
|
+
ignored.
|
|
113
|
+
|
|
114
|
+
### `@union` and `@interface`
|
|
115
|
+
|
|
116
|
+
PHP Lighthouse's directives name the class in the schema instead. It has
|
|
117
|
+
`resolve_type(object, context)` or `call(object, context)`, and is looked up in the
|
|
118
|
+
types namespace (nested names too), then as written:
|
|
119
|
+
|
|
120
|
+
```graphql
|
|
121
|
+
union SearchResult @union(resolveType: "SearchResultResolver") = Post | User
|
|
122
|
+
interface Authored @interface(resolveType: "AuthoredResolver") { id: ID! }
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The directive comes first, then the type class, then the default.
|
|
126
|
+
|
|
127
|
+
### When nothing resolves a value
|
|
128
|
+
|
|
129
|
+
A value no step resolves, or one resolved to a type the union or interface does
|
|
130
|
+
not include, raises `Lighthouse::DefinitionError`. It is a server bug, not a client
|
|
131
|
+
error, and the message names the ways out:
|
|
132
|
+
|
|
133
|
+
```text
|
|
134
|
+
Lighthouse could not tell the type of a Tag in the union SearchResult (Post | User):
|
|
135
|
+
name a type after its class, map one to it with @model(class: "Tag"), or resolve
|
|
136
|
+
it in Types::SearchResultType.resolve_type or with @union(resolveType:).
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
A `@union` or `@interface` naming no class, or a type class of another kind than
|
|
140
|
+
its SDL type (a union class for an interface), fails when the schema is built.
|
|
141
|
+
|
|
142
|
+
## Scalars
|
|
143
|
+
|
|
144
|
+
```graphql
|
|
145
|
+
"An ISO 8601 date and time."
|
|
146
|
+
scalar DateTime
|
|
147
|
+
|
|
148
|
+
type Post {
|
|
149
|
+
createdAt: DateTime!
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
type Query {
|
|
153
|
+
latestActivity(since: DateTime): [Authored!]!
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Write the scalar's class with graphql-ruby's `rails g graphql:scalar DateTime`,
|
|
158
|
+
`Types::<Name>Type` including `Lighthouse::GraphQL::Type`:
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
# app/graphql/types/date_time_type.rb
|
|
162
|
+
module Types
|
|
163
|
+
class DateTimeType < BaseScalar
|
|
164
|
+
include Lighthouse::GraphQL::Type
|
|
165
|
+
|
|
166
|
+
def self.coerce_input(value, context)
|
|
167
|
+
Time.iso8601(value.to_s)
|
|
168
|
+
rescue ArgumentError
|
|
169
|
+
raise GraphQL::CoercionError, "#{value.inspect} is not an ISO 8601 date and time"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def self.coerce_result(value, context)
|
|
173
|
+
value.utc.iso8601
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
- `coerce_input` reads an argument or variable. The resolver gets its result, here a
|
|
180
|
+
`Time`. Raising `GraphQL::CoercionError` (or returning `nil`) makes the argument
|
|
181
|
+
invalid, and the client gets the message.
|
|
182
|
+
- `coerce_result` writes a value a field returns, here `Post#created_at`.
|
|
183
|
+
|
|
184
|
+
`@scalar(class:)`, PHP Lighthouse's directive, names the class in the schema
|
|
185
|
+
instead, looked up in the types namespace, then as written:
|
|
186
|
+
|
|
187
|
+
```graphql
|
|
188
|
+
scalar Money @scalar(class: "Money::GraphQLScalar")
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
A scalar with neither passes its values through unchanged. A `@scalar` naming no
|
|
192
|
+
class with `coerce_input` and `coerce_result` fails when the schema is built.
|
|
193
|
+
|
|
194
|
+
## Opting in, and base classes
|
|
195
|
+
|
|
196
|
+
Only a class including `Lighthouse::GraphQL::Type` counts: any other
|
|
197
|
+
`Types::SearchResultType` or `Types::DateTimeType`, such as the ones a graphql-ruby
|
|
198
|
+
app already has, is left alone. The concern can be included in a subclass of
|
|
199
|
+
`GraphQL::Schema::Object`, `Union` or `Scalar`, or in a module including
|
|
200
|
+
`GraphQL::Schema::Interface`.
|
|
201
|
+
|
|
202
|
+
The classes are looked up in [`namespaces[:types]`](configuration.md#namespaces)
|
|
203
|
+
(default `['Types']`). When the schema's `base_types` name a base for a kind,
|
|
204
|
+
graphql-ruby builds the SDL's types of that kind from it, and a type class of that
|
|
205
|
+
kind must inherit from it, or the schema does not build:
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
AppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(
|
|
209
|
+
options: {
|
|
210
|
+
base_types: {
|
|
211
|
+
object: Types::BaseObject,
|
|
212
|
+
union: Types::BaseUnion,
|
|
213
|
+
interface: Types::BaseInterface,
|
|
214
|
+
scalar: Types::BaseScalar
|
|
215
|
+
# resolver:, mutation:, subscription: (resolvers.md#handing-the-bases-to-the-schema)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
```
|
data/docs/uploads.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# File Uploads
|
|
2
|
+
|
|
3
|
+
Clients send files with the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec),
|
|
4
|
+
which Apollo Upload Client, urql and most GraphQL clients speak: a multipart form with
|
|
5
|
+
the operation as JSON, a map of where each file goes, and the files.
|
|
6
|
+
`Lighthouse::GraphQL::Controller` reads it, so the endpoint needs nothing more.
|
|
7
|
+
|
|
8
|
+
## The `Upload` scalar
|
|
9
|
+
|
|
10
|
+
Declare it, and use it for arguments:
|
|
11
|
+
|
|
12
|
+
```graphql
|
|
13
|
+
"A file sent with a multipart request."
|
|
14
|
+
scalar Upload
|
|
15
|
+
|
|
16
|
+
type Mutation {
|
|
17
|
+
updateAvatar(avatar: Upload!): User! @update @inject(context: "current_user.id", name: "id")
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Lighthouse reads its values itself (`Lighthouse::GraphQL::Upload`): the resolver gets
|
|
22
|
+
the uploaded file, an `ActionDispatch::Http::UploadedFile`. A value that is not a file
|
|
23
|
+
makes the argument invalid. `Upload` is for arguments only, never a field's result.
|
|
24
|
+
|
|
25
|
+
An ActiveStorage attachment takes the file as it is: with `has_one_attached :avatar`
|
|
26
|
+
on `User`, the mutation above attaches it.
|
|
27
|
+
|
|
28
|
+
## `@upload`
|
|
29
|
+
|
|
30
|
+
`@upload` stores the file first, and passes along what was stored instead:
|
|
31
|
+
|
|
32
|
+
```graphql
|
|
33
|
+
type Mutation {
|
|
34
|
+
updateAvatar(avatar: Upload! @upload(path: "avatars")): User!
|
|
35
|
+
@guard
|
|
36
|
+
@update
|
|
37
|
+
@inject(context: "current_user.id", name: "id")
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
By default the file becomes an ActiveStorage blob: on the service `disk:` names (the
|
|
42
|
+
default service without it), with its key under `path:`. The argument then holds the
|
|
43
|
+
blob's **signed id**, which an attachment attribute attaches (`user.avatar = signed_id`)
|
|
44
|
+
and `ActiveStorage::Blob.find_signed` finds. Lists of files are stored one by one.
|
|
45
|
+
|
|
46
|
+
`@upload` runs once `@guard`, the `@can*` family and `@validator` have let the call
|
|
47
|
+
through, so a refused call stores nothing.
|
|
48
|
+
|
|
49
|
+
To store files elsewhere, give [`upload_store`](configuration.md#upload_store) an
|
|
50
|
+
object with `call(file, disk:, path:, public:)`; the argument holds what it returns:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
Lighthouse.configure do |config|
|
|
54
|
+
config.upload_store = ->(file, path: '/', **) { S3Uploader.put(file, prefix: path) } # returns the key
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## The request
|
|
59
|
+
|
|
60
|
+
```shell
|
|
61
|
+
curl localhost:3000/graphql \
|
|
62
|
+
-H 'Authorization: Bearer <token>' \
|
|
63
|
+
-F operations='{ "query": "mutation($avatar: Upload!) { updateAvatar(avatar: $avatar) { avatarUrl } }", "variables": { "avatar": null } }' \
|
|
64
|
+
-F map='{ "0": ["variables.avatar"] }' \
|
|
65
|
+
-F 0=@me.png
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
A batch sends an array as `operations`, and maps files to `"0.variables.avatar"`,
|
|
69
|
+
`"1.variables.files.0"`, and so on.
|
|
70
|
+
|
|
71
|
+
In a request spec:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
avatar = Rack::Test::UploadedFile.new('spec/fixtures/me.png', 'image/png')
|
|
75
|
+
post '/graphql', params: {
|
|
76
|
+
operations: { query: mutation, variables: { avatar: nil } }.to_json,
|
|
77
|
+
map: { '0' => ['variables.avatar'] }.to_json,
|
|
78
|
+
'0' => avatar
|
|
79
|
+
}
|
|
80
|
+
```
|