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
data/docs/subscriptions.md
CHANGED
|
@@ -6,9 +6,12 @@ happens. Lighthouse builds on [graphql-ruby's subscriptions](https://graphql-rub
|
|
|
6
6
|
and keeps the shape of [PHP Lighthouse's](https://lighthouse-php.com/master/subscriptions/getting-started.html):
|
|
7
7
|
|
|
8
8
|
- a `Subscription` field is declared in the SDL, and **resolves by convention** to a class;
|
|
9
|
-
- that class
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
- that class inherits from `Subscriptions::BaseSubscription`, a graphql-ruby
|
|
10
|
+
[`GraphQL::Schema::Subscription`](https://graphql-ruby.org/subscriptions/subscription_classes.html),
|
|
11
|
+
as a `Query` field's class is a `Resolvers::BaseResolver` and a mutation's a
|
|
12
|
+
`Mutations::BaseMutation` (see [resolvers.md](resolvers.md#base-classes)). The base
|
|
13
|
+
includes [Lighthouse's concern](#the-subscription-concern): PHP
|
|
14
|
+
Lighthouse's model, one topic per field and a `filter?`;
|
|
12
15
|
- a mutation announces what it did with [`@broadcast`](#broadcast-from-a-mutation).
|
|
13
16
|
|
|
14
17
|
```graphql
|
|
@@ -23,10 +26,10 @@ type Mutation {
|
|
|
23
26
|
|
|
24
27
|
```ruby
|
|
25
28
|
# app/graphql/subscriptions/post_updated.rb (rails generate lighthouse:subscription PostUpdated)
|
|
26
|
-
class Subscriptions::PostUpdated <
|
|
27
|
-
#
|
|
28
|
-
def
|
|
29
|
-
|
|
29
|
+
class Subscriptions::PostUpdated < Subscriptions::BaseSubscription
|
|
30
|
+
# For each broadcast: skip the subscribers who may not see this post.
|
|
31
|
+
def filter?(post, **)
|
|
32
|
+
post.published? || post.author == context[:current_user]
|
|
30
33
|
end
|
|
31
34
|
end
|
|
32
35
|
```
|
|
@@ -94,12 +97,14 @@ type Subscription {
|
|
|
94
97
|
}
|
|
95
98
|
```
|
|
96
99
|
|
|
97
|
-
For anything more, write the class (
|
|
98
|
-
the
|
|
100
|
+
For anything more, write the class. (A field with no class of its own resolves with
|
|
101
|
+
the schema's `base_types[:subscription]`, else with a built-in class that includes
|
|
102
|
+
the concern: one topic for the field, no `filter?`.) `Subscription.postUpdated` resolves to
|
|
99
103
|
`Subscriptions::PostUpdated`, looked up in `Config.namespaces[:subscriptions]`
|
|
100
104
|
(default `['Subscriptions']`) each time it is needed, so code reloading works.
|
|
101
105
|
`rails generate lighthouse:subscription PostUpdated` writes it. It inherits from
|
|
102
|
-
`GraphQL::Schema::Subscription`: anything you
|
|
106
|
+
`Subscriptions::BaseSubscription`, a `GraphQL::Schema::Subscription`: anything you
|
|
107
|
+
know from graphql-ruby applies.
|
|
103
108
|
|
|
104
109
|
Like a query or a mutation class, it declares neither the arguments nor the return
|
|
105
110
|
type: the SDL does. The field's arguments arrive as `snake_case` keywords.
|
|
@@ -109,7 +114,10 @@ type: the SDL does. The field's arguments arrive as `snake_case` keywords.
|
|
|
109
114
|
graphql-ruby calls the class twice in a subscription's life: once for the
|
|
110
115
|
**subscription request** (the client's `subscription { ... }`), and then once for
|
|
111
116
|
**each update** (`@broadcast`, or `schema.subscriptions.trigger`). All hooks are
|
|
112
|
-
optional.
|
|
117
|
+
optional. These are graphql-ruby's own; the concern `BaseSubscription` includes
|
|
118
|
+
implements `authorized?` and `update` with PHP Lighthouse's hooks
|
|
119
|
+
([below](#the-subscription-concern)), and a class that defines them itself
|
|
120
|
+
keeps graphql-ruby's behavior.
|
|
113
121
|
|
|
114
122
|
| Hook | Called | Default |
|
|
115
123
|
| --- | --- | --- |
|
|
@@ -135,7 +143,7 @@ optional.
|
|
|
135
143
|
field comes back `null`, with no error.
|
|
136
144
|
|
|
137
145
|
```ruby
|
|
138
|
-
class Subscriptions::OrderShipped <
|
|
146
|
+
class Subscriptions::OrderShipped < Subscriptions::BaseSubscription
|
|
139
147
|
def authorized?(**)
|
|
140
148
|
raise GraphQL::ExecutionError, 'Sign in to follow your orders.' unless context[:current_user]
|
|
141
149
|
|
|
@@ -145,8 +153,6 @@ class Subscriptions::OrderShipped < GraphQL::Schema::Subscription
|
|
|
145
153
|
def update(**)
|
|
146
154
|
object.user_id == context[:current_user].id ? object : NO_UPDATE
|
|
147
155
|
end
|
|
148
|
-
|
|
149
|
-
def self.trigger_arguments(_order, _context) = {}
|
|
150
156
|
end
|
|
151
157
|
```
|
|
152
158
|
|
|
@@ -164,22 +170,32 @@ If you know PHP Lighthouse's `GraphQLSubscription`, this is where its methods we
|
|
|
164
170
|
One real difference is the **topic**. PHP Lighthouse puts every subscriber of a
|
|
165
171
|
field on one topic and `filter` decides who is updated. graphql-ruby's default
|
|
166
172
|
topic includes the arguments, so a trigger reaches only the subscribers whose
|
|
167
|
-
arguments *equal* its own.
|
|
168
|
-
`
|
|
173
|
+
arguments *equal* its own. The concern in `BaseSubscription` takes PHP Lighthouse's
|
|
174
|
+
topic; a class that keeps graphql-ruby's gives `@broadcast` its `trigger_arguments`.
|
|
175
|
+
See [below](#topics-trigger-arguments-and-filtering).
|
|
169
176
|
|
|
170
|
-
##
|
|
177
|
+
## The subscription concern
|
|
171
178
|
|
|
172
|
-
|
|
173
|
-
it and `resolve_payload` for the result,
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
its arguments as one would in graphql-ruby (or leaves them to the SDL).
|
|
177
|
-
`rails generate lighthouse:subscription PostUpdated --lighthouse` writes it:
|
|
179
|
+
`Lighthouse::GraphQL::Subscription` is PHP Lighthouse's model: one topic per field,
|
|
180
|
+
`filter?` to narrow it and `resolve_payload` for the result. It is included once, in
|
|
181
|
+
the base every subscription inherits from, as graphql-ruby's convention puts what
|
|
182
|
+
all classes of a kind share in their base:
|
|
178
183
|
|
|
179
184
|
```ruby
|
|
180
|
-
|
|
181
|
-
|
|
185
|
+
# app/graphql/subscriptions/base_subscription.rb (rails generate lighthouse:install)
|
|
186
|
+
module Subscriptions
|
|
187
|
+
class BaseSubscription < GraphQL::Schema::Subscription
|
|
188
|
+
include Lighthouse::GraphQL::Subscription
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
A subscription is still a plain `GraphQL::Schema::Subscription`, and declares its
|
|
194
|
+
arguments as one would in graphql-ruby (or leaves them to the SDL).
|
|
195
|
+
`rails generate lighthouse:subscription PostUpdated` writes one with the hooks:
|
|
182
196
|
|
|
197
|
+
```ruby
|
|
198
|
+
class Subscriptions::PostUpdated < Subscriptions::BaseSubscription
|
|
183
199
|
def filter?(post, author: nil)
|
|
184
200
|
author.nil? || post.author_id.to_s == author.to_s
|
|
185
201
|
end
|
|
@@ -205,17 +221,19 @@ optional.
|
|
|
205
221
|
[broadcast section](#topics-trigger-arguments-and-filtering) explain.
|
|
206
222
|
- Including it in a class that is not a `GraphQL::Schema::Subscription` raises a
|
|
207
223
|
`Lighthouse::DefinitionError`.
|
|
224
|
+
- To keep graphql-ruby's model for every subscription, leave the `include` out of
|
|
225
|
+
`BaseSubscription`; the generator then writes classes with `authorized?` and
|
|
226
|
+
`update` instead.
|
|
208
227
|
- **Precedence.** The concern sits between your class and
|
|
209
|
-
`GraphQL::Schema::Subscription
|
|
228
|
+
`GraphQL::Schema::Subscription` (it is in the base), so what the class defines
|
|
229
|
+
itself wins. A class
|
|
210
230
|
with its own `update` (or `authorized?`) is called instead of the concern's, and
|
|
211
231
|
its `filter?`, `resolve_payload` (or `authorize?`) are not, unless it calls
|
|
212
232
|
`super`. `subscribe`, `unsubscribe`, `subscription_scope` and `ready?` are
|
|
213
233
|
untouched, and combine freely with the hooks.
|
|
214
234
|
|
|
215
235
|
```ruby
|
|
216
|
-
class Subscriptions::OrderShipped <
|
|
217
|
-
include Lighthouse::GraphQL::Subscription
|
|
218
|
-
|
|
236
|
+
class Subscriptions::OrderShipped < Subscriptions::BaseSubscription
|
|
219
237
|
def authorize?(**)
|
|
220
238
|
context[:current_user].present?
|
|
221
239
|
end
|
|
@@ -251,8 +269,9 @@ type Subscription {
|
|
|
251
269
|
|
|
252
270
|
The name is looked up in `Config.namespaces[:subscriptions]` like the conventional
|
|
253
271
|
one, so `class: "PostUpdated"` finds `Subscriptions::PostUpdated`, and a name with
|
|
254
|
-
`::` is used as it is. A class that does not exist,
|
|
255
|
-
`GraphQL::Schema::Subscription`,
|
|
272
|
+
`::` is used as it is. A class that does not exist, is not a
|
|
273
|
+
`GraphQL::Schema::Subscription`, or does not inherit from the schema's
|
|
274
|
+
`base_types[:subscription]` raises `Lighthouse::DefinitionError`.
|
|
256
275
|
`@guard`, `@can*` and the other wrapping directives work on a subscription field
|
|
257
276
|
as on any other.
|
|
258
277
|
|
|
@@ -303,7 +322,8 @@ arguments (a Hash, `{}` by default) for the `root` a mutation returned, and
|
|
|
303
322
|
`subscription_scope` (graphql-ruby's way to give each tenant or user their own
|
|
304
323
|
stream).
|
|
305
324
|
|
|
306
|
-
|
|
325
|
+
When the topic includes the arguments (a class with `topic_by :arguments`, or under
|
|
326
|
+
a base that does not include the concern):
|
|
307
327
|
|
|
308
328
|
- **A field without arguments works out of the box.** Every subscriber's arguments
|
|
309
329
|
are `{}`, and so is the trigger's.
|
|
@@ -317,7 +337,9 @@ With a plain `GraphQL::Schema::Subscription`:
|
|
|
317
337
|
```
|
|
318
338
|
|
|
319
339
|
```ruby
|
|
320
|
-
class Subscriptions::MessagePosted <
|
|
340
|
+
class Subscriptions::MessagePosted < Subscriptions::BaseSubscription
|
|
341
|
+
topic_by :arguments
|
|
342
|
+
|
|
321
343
|
# The subscribers of `messagePosted(room: "<id>")`.
|
|
322
344
|
def self.trigger_arguments(message, _context) = { room: message.room_id.to_s }
|
|
323
345
|
end
|
|
@@ -336,11 +358,11 @@ With a plain `GraphQL::Schema::Subscription`:
|
|
|
336
358
|
those subscribers are updated, so give such a class a `trigger_arguments` too if
|
|
337
359
|
subscribers pass the argument.
|
|
338
360
|
- **To have one trigger reach subscribers with different arguments** (the argument
|
|
339
|
-
is a filter, not an identity), give the class one topic per
|
|
340
|
-
graphql-ruby documents, and let `update` return `NO_UPDATE`:
|
|
361
|
+
is a filter, not an identity) without the concern, give the class one topic per
|
|
362
|
+
field, as graphql-ruby documents, and let `update` return `NO_UPDATE`:
|
|
341
363
|
|
|
342
364
|
```ruby
|
|
343
|
-
class Subscriptions::PostPublished <
|
|
365
|
+
class Subscriptions::PostPublished < Subscriptions::BaseSubscription # a base without the concern
|
|
344
366
|
def self.topic_for(field:, scope:, **)
|
|
345
367
|
GraphQL::Subscriptions::Serialize.dump_recursive([scope, field.graphql_name])
|
|
346
368
|
end
|
|
@@ -417,7 +439,8 @@ the repository's own specs do, in `spec/support/in_memory_subscriptions.rb`.
|
|
|
417
439
|
graphql-ruby also ships `GraphQL::Testing::MockActionCable` for the real
|
|
418
440
|
ActionCable backend.
|
|
419
441
|
|
|
420
|
-
The repository's `example/` app is a complete setup:
|
|
442
|
+
The repository's `example/` app is a complete setup: subscription classes under
|
|
443
|
+
`Subscriptions::BaseSubscription`,
|
|
421
444
|
`GraphqlChannel`, `ApplicationCable::Connection` with token authentication, and
|
|
422
445
|
specs that drive a mutation through `POST /graphql` and watch the update arrive on
|
|
423
446
|
an ActionCable subscriber (`example/spec/subscriptions/delivery_spec.rb`).
|
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
|
+
```
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'lighthouse-graphql'
|
|
5
|
+
|
|
6
|
+
# The base class graphql-ruby's convention puts in each namespace, which the
|
|
7
|
+
# classes Lighthouse generates inherit from: Resolvers::BaseResolver,
|
|
8
|
+
# Mutations::BaseMutation and Subscriptions::BaseSubscription.
|
|
9
|
+
#
|
|
10
|
+
# graphql-ruby's `graphql:install` writes the first two (and every Types::Base*),
|
|
11
|
+
# and `lighthouse:install` runs it: Lighthouse does not write copies of them. It
|
|
12
|
+
# writes only the base graphql-ruby has none of, BaseSubscription, which
|
|
13
|
+
# includes Lighthouse's subscription concern. A generator that finds a base
|
|
14
|
+
# missing says how to get it.
|
|
15
|
+
#
|
|
16
|
+
# A base that already exists is never replaced, not even with --force: it holds
|
|
17
|
+
# the app's own code.
|
|
18
|
+
module Lighthouse::Generators::BaseClasses
|
|
19
|
+
NAMES = { queries: 'BaseResolver', mutations: 'BaseMutation', subscriptions: 'BaseSubscription' }.freeze
|
|
20
|
+
|
|
21
|
+
# The bases Lighthouse writes itself, graphql-ruby having none.
|
|
22
|
+
SOURCES = {
|
|
23
|
+
subscriptions: <<~RUBY
|
|
24
|
+
# What every class resolving a Subscription field shares. The concern is PHP
|
|
25
|
+
# Lighthouse's model: every subscriber of a field gets every broadcast, and the
|
|
26
|
+
# class's `filter?` decides who receives it. A subclass that defines
|
|
27
|
+
# graphql-ruby's own `authorized?` or `update` keeps them, and `topic_by
|
|
28
|
+
# :arguments` brings back graphql-ruby's exact-argument matching.
|
|
29
|
+
class BaseSubscription < GraphQL::Schema::Subscription
|
|
30
|
+
include Lighthouse::GraphQL::Subscription
|
|
31
|
+
end
|
|
32
|
+
RUBY
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# The base class name for `kind` (`BaseResolver`), or nil for a kind without one.
|
|
38
|
+
def base_class_name(kind)
|
|
39
|
+
NAMES[kind]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Write the base class of `kind` into `namespace` when Lighthouse owns it and
|
|
43
|
+
# it is missing; say how to get one graphql-ruby owns.
|
|
44
|
+
def write_base_class(kind, namespace)
|
|
45
|
+
return unless base_class_name(kind)
|
|
46
|
+
|
|
47
|
+
path = base_class_path(kind, namespace)
|
|
48
|
+
if File.exist?(File.join(destination_root, path))
|
|
49
|
+
say_status :skip, "#{path} already exists", :yellow
|
|
50
|
+
elsif SOURCES[kind]
|
|
51
|
+
create_file path, ruby_source(namespace.split('::'), SOURCES[kind])
|
|
52
|
+
else
|
|
53
|
+
say_status :missing, "#{path}: graphql-ruby writes it — run `bin/rails g lighthouse:install` (which runs graphql:install), " \
|
|
54
|
+
"or write #{namespace}::#{base_class_name(kind)} yourself", :red
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def base_class_path(kind, namespace)
|
|
59
|
+
File.join('app/graphql', *namespace.split('::').map(&:underscore), "#{base_class_name(kind).underscore}.rb")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Ruby source defining `body` inside the nested `modules`.
|
|
63
|
+
def ruby_source(modules, body)
|
|
64
|
+
lines = ['# frozen_string_literal: true', '']
|
|
65
|
+
modules.each_with_index { |mod, depth| lines << "#{' ' * depth}module #{mod}" }
|
|
66
|
+
body.each_line { |line| lines << (line.strip.empty? ? '' : "#{' ' * modules.size}#{line.chomp}") }
|
|
67
|
+
modules.size.times.reverse_each { |depth| lines << "#{' ' * depth}end" }
|
|
68
|
+
"#{lines.join("\n")}\n"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -2,16 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
require 'rails/generators'
|
|
4
4
|
require 'lighthouse-graphql'
|
|
5
|
+
require_relative 'base_classes'
|
|
5
6
|
|
|
6
7
|
# Base for the generators that scaffold the classes a schema refers to by
|
|
7
|
-
# name —
|
|
8
|
-
# namespace (Config.namespaces) and with its own contract.
|
|
8
|
+
# name — resolvers, mutations, subscriptions, builders and validators — each in
|
|
9
|
+
# its own namespace (Config.namespaces) and with its own contract. A resolver,
|
|
10
|
+
# mutation or subscription inherits from its namespace's base class
|
|
11
|
+
# (Resolvers::BaseResolver, ...), written first when it is missing.
|
|
9
12
|
class Lighthouse::Generators::ClassGenerator < Rails::Generators::NamedBase
|
|
13
|
+
include Lighthouse::Generators::BaseClasses
|
|
14
|
+
|
|
10
15
|
hide!
|
|
11
16
|
|
|
12
17
|
class_option :namespace, type: :string,
|
|
13
18
|
desc: 'Namespace to generate into (default: the first of Config.namespaces for this kind)'
|
|
14
19
|
|
|
20
|
+
def create_base_class
|
|
21
|
+
write_base_class(kind, namespace_name)
|
|
22
|
+
end
|
|
23
|
+
|
|
15
24
|
def create_class
|
|
16
25
|
create_file File.join('app/graphql', *modules.map(&:underscore), "#{class_basename.underscore}.rb"), source
|
|
17
26
|
end
|
|
@@ -22,7 +31,7 @@ class Lighthouse::Generators::ClassGenerator < Rails::Generators::NamedBase
|
|
|
22
31
|
|
|
23
32
|
private
|
|
24
33
|
|
|
25
|
-
# :queries, :mutations, :builders or :validators.
|
|
34
|
+
# :queries, :mutations, :subscriptions, :builders or :validators.
|
|
26
35
|
def kind
|
|
27
36
|
raise NotImplementedError
|
|
28
37
|
end
|
|
@@ -52,10 +61,12 @@ class Lighthouse::Generators::ClassGenerator < Rails::Generators::NamedBase
|
|
|
52
61
|
end
|
|
53
62
|
|
|
54
63
|
def source
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
ruby_source(modules, body)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The class a generated class inherits from: the namespace's base class,
|
|
68
|
+
# which Ruby finds from inside the namespace's modules.
|
|
69
|
+
def superclass_name
|
|
70
|
+
base_class_name(kind)
|
|
60
71
|
end
|
|
61
72
|
end
|