lighthouse-graphql 0.4.0 → 0.6.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 +256 -1
- data/README.md +26 -15
- data/docs/caching.md +4 -3
- data/docs/configuration.md +16 -12
- data/docs/directives.md +294 -4
- data/docs/generators.md +84 -0
- data/docs/getting-started.md +93 -60
- data/docs/performance.md +2 -2
- data/docs/relationships.md +47 -4
- data/docs/relay.md +200 -0
- data/docs/resolvers.md +326 -34
- data/docs/security.md +73 -8
- data/docs/subscriptions.md +16 -9
- data/docs/types.md +218 -0
- data/docs/uploads.md +107 -0
- data/lib/generators/lighthouse/base_classes.rb +27 -49
- data/lib/generators/lighthouse/install/graphql_ruby_install.rb +113 -0
- data/lib/generators/lighthouse/install/install_generator.rb +125 -45
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +10 -8
- data/lib/lighthouse/async_mutation_job.rb +17 -0
- data/lib/lighthouse/caching.rb +9 -3
- data/lib/lighthouse/complexity.rb +5 -4
- data/lib/lighthouse/config.rb +12 -18
- data/lib/lighthouse/controller.rb +53 -12
- 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/cache_resolvers.rb +1 -1
- 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 +59 -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 +136 -0
- data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +87 -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 +56 -6
- data/lib/lighthouse/generated_resolvers.rb +13 -8
- data/lib/lighthouse/global_id.rb +57 -0
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/graphql_ruby_patches.rb +45 -0
- 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 +34 -0
- data/lib/lighthouse/relation_counts.rb +29 -0
- data/lib/lighthouse/resolve_info.rb +80 -0
- data/lib/lighthouse/resolver_bases.rb +25 -4
- data/lib/lighthouse/resolver_dispatch.rb +51 -11
- data/lib/lighthouse/root_types.rb +117 -0
- data/lib/lighthouse/scalar_coercion.rb +72 -0
- data/lib/lighthouse/schema_cache.rb +2 -1
- data/lib/lighthouse/schema_generator.rb +13 -11
- 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 +28 -0
- data/lib/lighthouse/upload_store.rb +57 -0
- data/sig/lighthouse/directives.rbs +3 -2
- data/sig/lighthouse.rbs +10 -2
- metadata +40 -4
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +0 -22
- data/lib/generators/lighthouse/mutation_generator.rb +0 -31
- data/lib/generators/lighthouse/query_generator.rb +0 -30
data/docs/resolvers.md
CHANGED
|
@@ -8,32 +8,43 @@ interchangeable. Each kind has its own contract and its own folder:
|
|
|
8
8
|
| Kind | Folder / namespace | Contract | Used by |
|
|
9
9
|
| --- | --- | --- | --- |
|
|
10
10
|
| Resolver | `app/graphql/resolvers` · `Resolvers::` | `Resolvers::BaseResolver` (a `GraphQL::Schema::Resolver`) | `Query` fields |
|
|
11
|
-
| Mutation | `app/graphql/mutations` · `Mutations::` | `Mutations::BaseMutation` (
|
|
11
|
+
| Mutation | `app/graphql/mutations` · `Mutations::` | `Mutations::BaseMutation` (graphql-ruby's `GraphQL::Schema::RelayClassicMutation`) | `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 any object type, through its methods ([type classes](#type-classes), [root type classes](#root-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
|
+
writes `Resolvers::BaseResolver`, `Mutations::BaseMutation` and the `Types::Base*`
|
|
24
|
+
classes, and `rails g lighthouse:install` builds on them: it keeps them as
|
|
25
|
+
graphql-ruby wrote them, includes `Lighthouse::GraphQL::Type` in the base object,
|
|
26
|
+
interface, union and scalar, and with `--subscriptions` adds
|
|
27
|
+
`Subscriptions::BaseSubscription`, which graphql-ruby has none of
|
|
28
|
+
([generators.md](generators.md)):
|
|
22
29
|
|
|
23
30
|
```ruby
|
|
24
|
-
# app/graphql/resolvers/base_resolver.rb
|
|
31
|
+
# app/graphql/resolvers/base_resolver.rb (graphql-ruby's)
|
|
25
32
|
module Resolvers
|
|
26
33
|
class BaseResolver < GraphQL::Schema::Resolver
|
|
27
34
|
end
|
|
28
35
|
end
|
|
29
36
|
|
|
30
|
-
# app/graphql/mutations/base_mutation.rb
|
|
37
|
+
# app/graphql/mutations/base_mutation.rb (graphql-ruby's)
|
|
31
38
|
module Mutations
|
|
32
|
-
class BaseMutation < GraphQL::Schema::
|
|
39
|
+
class BaseMutation < GraphQL::Schema::RelayClassicMutation
|
|
40
|
+
argument_class Types::BaseArgument
|
|
41
|
+
field_class Types::BaseField
|
|
42
|
+
input_object_class Types::BaseInputObject
|
|
43
|
+
object_class Types::BaseObject
|
|
33
44
|
end
|
|
34
45
|
end
|
|
35
46
|
|
|
36
|
-
# app/graphql/subscriptions/base_subscription.rb
|
|
47
|
+
# app/graphql/subscriptions/base_subscription.rb (Lighthouse's, --subscriptions)
|
|
37
48
|
module Subscriptions
|
|
38
49
|
class BaseSubscription < GraphQL::Schema::Subscription
|
|
39
50
|
include Lighthouse::GraphQL::Subscription
|
|
@@ -43,11 +54,8 @@ end
|
|
|
43
54
|
|
|
44
55
|
What every class of a kind shares goes in its base, once: a concern (as
|
|
45
56
|
`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.
|
|
57
|
+
a helper, graphql-ruby settings such as `argument_class Types::BaseArgument`. An
|
|
58
|
+
existing base is never replaced.
|
|
51
59
|
|
|
52
60
|
### Handing the bases to the schema
|
|
53
61
|
|
|
@@ -56,16 +64,26 @@ schema holds every class to them:
|
|
|
56
64
|
|
|
57
65
|
```ruby
|
|
58
66
|
# app/graphql/app_schema.rb
|
|
59
|
-
AppSchema
|
|
67
|
+
class AppSchema < Lighthouse::GraphQL::RbLightHouse.get_schema(
|
|
60
68
|
options: {
|
|
61
69
|
base_types: {
|
|
70
|
+
query_type: Types::QueryType,
|
|
71
|
+
mutation_type: Types::MutationType,
|
|
72
|
+
node: Types::NodeType,
|
|
62
73
|
resolver: Resolvers::BaseResolver,
|
|
63
74
|
mutation: Mutations::BaseMutation,
|
|
64
|
-
subscription: Subscriptions::BaseSubscription
|
|
65
|
-
|
|
75
|
+
subscription: Subscriptions::BaseSubscription,
|
|
76
|
+
object: Types::BaseObject,
|
|
77
|
+
interface: Types::BaseInterface,
|
|
78
|
+
union: Types::BaseUnion,
|
|
79
|
+
scalar: Types::BaseScalar,
|
|
80
|
+
enum: Types::BaseEnum,
|
|
81
|
+
input_object: Types::BaseInputObject
|
|
66
82
|
}
|
|
67
83
|
}
|
|
68
84
|
)
|
|
85
|
+
use GraphQL::Dataloader
|
|
86
|
+
end
|
|
69
87
|
```
|
|
70
88
|
|
|
71
89
|
- A class a `Query`, `Mutation` or `Subscription` field resolves to, by convention
|
|
@@ -78,8 +96,17 @@ AppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(
|
|
|
78
96
|
(`GraphQL::Schema::Resolver`, `GraphQL::Schema::Mutation`, which
|
|
79
97
|
`RelayClassicMutation` is, `GraphQL::Schema::Subscription`), or the schema does not
|
|
80
98
|
build. graphql-ruby never sees these three keys.
|
|
81
|
-
|
|
82
|
-
|
|
99
|
+
- `query_type:` and `mutation_type:` are the code-first classes the SDL's `Query`
|
|
100
|
+
and `Mutation` are built on (see [root type classes](#root-type-classes)), and
|
|
101
|
+
`node:` is the SDL's `Node` interface ([relay.md](relay.md#relay-through-graphql-ruby)).
|
|
102
|
+
`subscription_type:` does the same for `Subscription`.
|
|
103
|
+
- The others are graphql-ruby's: it builds the SDL's types of each kind from them.
|
|
104
|
+
A [type class](#type-classes) (one including `Lighthouse::GraphQL::Type`) that
|
|
105
|
+
does not inherit from its kind's raises `Lighthouse::DefinitionError` when the
|
|
106
|
+
schema is built.
|
|
107
|
+
|
|
108
|
+
`rails g lighthouse:install` names in the schema class every one graphql-ruby's
|
|
109
|
+
install wrote. Without them,
|
|
83
110
|
Lighthouse requires only the graphql-ruby class (`GraphQL::Schema::Resolver`, …),
|
|
84
111
|
and a `Subscription` field with no class resolves with a built-in subscription that
|
|
85
112
|
includes the concern.
|
|
@@ -134,8 +161,9 @@ end
|
|
|
134
161
|
|
|
135
162
|
### Resolution by convention
|
|
136
163
|
|
|
137
|
-
A field on `Query` or `Mutation` with **no resolving directive
|
|
138
|
-
|
|
164
|
+
A field on `Query` or `Mutation` with **no resolving directive**, and no method of
|
|
165
|
+
its name on the [root type class](#root-type-classes), resolves to the class named
|
|
166
|
+
after it, as in PHP Lighthouse:
|
|
139
167
|
|
|
140
168
|
- `Query.latestPosts` resolves to `Resolvers::LatestPosts`;
|
|
141
169
|
- `Mutation.publishPost` resolves to `Mutations::PublishPost`.
|
|
@@ -147,9 +175,11 @@ field with no class is reported through [`on_error`](configuration.md#on_error):
|
|
|
147
175
|
it raises in test and logs elsewhere. Lighthouse never falls back to a top-level
|
|
148
176
|
constant, so a `post` field is never mistaken for the `Post` model.
|
|
149
177
|
|
|
150
|
-
graphql-ruby's `RelayClassicMutation
|
|
151
|
-
|
|
152
|
-
|
|
178
|
+
graphql-ruby's `RelayClassicMutation`, which `Mutations::BaseMutation` inherits from,
|
|
179
|
+
takes a single `input:` argument. A `Mutation` field whose SDL declares its arguments
|
|
180
|
+
one by one calls the class as a plain mutation, with those arguments as keywords
|
|
181
|
+
(`resolve(id:)` above). A field with a single `input:` argument goes through
|
|
182
|
+
`RelayClassicMutation`'s own `input:` handling.
|
|
153
183
|
|
|
154
184
|
Wrapping directives (`@guard`, the `@can*` family, `@validator`, `@cache`,
|
|
155
185
|
`@clearCache`) are not resolvers, so a field carrying only those still resolves by
|
|
@@ -161,6 +191,45 @@ type Mutation {
|
|
|
161
191
|
}
|
|
162
192
|
```
|
|
163
193
|
|
|
194
|
+
### Root type classes
|
|
195
|
+
|
|
196
|
+
graphql-ruby's install writes `Types::QueryType` and `Types::MutationType`, and the
|
|
197
|
+
schema's `base_types` name them (`query_type:`, `mutation_type:`). The SDL's `Query`
|
|
198
|
+
and `Mutation` are built on them, so they work as in a graphql-ruby app:
|
|
199
|
+
|
|
200
|
+
- A root field with no resolving directive calls the method of its snake_case name
|
|
201
|
+
on the class, when the class defines it and includes `Lighthouse::GraphQL::Type`
|
|
202
|
+
(`Types::BaseObject` does, after `lighthouse:install`). It comes before the
|
|
203
|
+
conventional class: `Query.latestPosts` calls `Types::QueryType#latest_posts`
|
|
204
|
+
before `Resolvers::LatestPosts`.
|
|
205
|
+
- The class's own `field`s are the schema's too, beside the SDL's, and resolve as
|
|
206
|
+
in graphql-ruby: Relay's `node` / `nodes`, and the `field :publish_post, mutation:
|
|
207
|
+
Mutations::PublishPost` that `rails g graphql:mutation` adds. An SDL field of the
|
|
208
|
+
same name wins.
|
|
209
|
+
- When the SDL has no `Mutation` type, `Types::MutationType` itself is the root,
|
|
210
|
+
once it declares a field.
|
|
211
|
+
|
|
212
|
+
```graphql
|
|
213
|
+
type Query {
|
|
214
|
+
latestPosts(limit: Int = 10): [Post!]!
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
# app/graphql/types/query_type.rb
|
|
220
|
+
module Types
|
|
221
|
+
class QueryType < Types::BaseObject
|
|
222
|
+
def latest_posts(limit:)
|
|
223
|
+
Post.order(created_at: :desc).limit(limit)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
As in a [type class](#type-classes), the method has `object`, `context`, the
|
|
230
|
+
`dataload*` helpers and `resolve_info`, and gets the field's arguments as snake_case
|
|
231
|
+
keywords. `Subscription` fields resolve with their classes, as before.
|
|
232
|
+
|
|
164
233
|
### Naming a class explicitly
|
|
165
234
|
|
|
166
235
|
`@field(resolver:)` points a field at a class by name. On `Query` and `Mutation`
|
|
@@ -174,7 +243,232 @@ type Query {
|
|
|
174
243
|
```
|
|
175
244
|
|
|
176
245
|
Besides graphql-ruby resolver classes, `@field` also accepts a plain callable:
|
|
177
|
-
anything with `call(object, args, context)
|
|
246
|
+
anything with `call(object, args, context)`, and `call(object, args, context,
|
|
247
|
+
resolve_info)` when it takes a fourth argument.
|
|
248
|
+
|
|
249
|
+
A method after `@` (PHP Lighthouse's `Class@method`) replaces `call`, or `resolve` on
|
|
250
|
+
a resolver class (whose `authorized?` still runs). One class can then hold several
|
|
251
|
+
fields' resolvers:
|
|
252
|
+
|
|
253
|
+
```graphql
|
|
254
|
+
type Query {
|
|
255
|
+
counts: Counts! @field(resolver: "Statistics@counts")
|
|
256
|
+
latest: [Post!]! @field(resolver: "Statistics@latest")
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
class Resolvers::Statistics
|
|
262
|
+
def self.counts(_root, _args, _context) = { posts: Post.count, users: User.count }
|
|
263
|
+
def self.latest(_root, _args, _context) = Post.order(created_at: :desc).limit(5)
|
|
264
|
+
end
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### ResolveInfo
|
|
268
|
+
|
|
269
|
+
What PHP Lighthouse hands a resolver as `$resolveInfo`. A `Resolvers::` or
|
|
270
|
+
`Mutations::` class, a class `@field` names and a type class's methods read it as
|
|
271
|
+
`resolve_info`:
|
|
272
|
+
|
|
273
|
+
| | |
|
|
274
|
+
| --- | --- |
|
|
275
|
+
| `field_name`, `field`, `return_type` | the field being resolved |
|
|
276
|
+
| `parent_type` | the type it belongs to |
|
|
277
|
+
| `object`, `arguments`, `context` | what it resolves with |
|
|
278
|
+
| `path` | `["posts", 0, "author"]` |
|
|
279
|
+
| `lookahead` | graphql-ruby's [`Lookahead`](https://graphql-ruby.org/queries/lookahead.html): what the client selects below the field (`lookahead.selects?(:author)`) |
|
|
280
|
+
| `enhance_builder(relation, scopes: [])` | `relation` with the field's query directives applied |
|
|
281
|
+
|
|
282
|
+
`enhance_builder` lets a class build the query no directive could, and still honour
|
|
283
|
+
what the field declares: its `@builder` classes, and the argument directives the
|
|
284
|
+
client used (`@eq`, `@where`, `@in`, `@like`, `@orderBy`, `@whereConditions`, ...),
|
|
285
|
+
as `@all` and `@paginate` apply them. `scopes:` adds model scopes.
|
|
286
|
+
|
|
287
|
+
```graphql
|
|
288
|
+
type Query {
|
|
289
|
+
myPosts(title: String @like, orderBy: [PostOrderByClause!] @orderBy): [Post!]! @guard
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
class Resolvers::MyPosts < Resolvers::BaseResolver
|
|
295
|
+
def resolve(**)
|
|
296
|
+
resolve_info.enhance_builder(context[:current_user].posts, scopes: [:recent])
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Fields of other types
|
|
302
|
+
|
|
303
|
+
A field of any other type resolves with the default reader: `Post.commentsCount`
|
|
304
|
+
reads `post.comments_count`. When its value takes more than reading an attribute
|
|
305
|
+
or calling a method ([`@method`](directives.md#method)), name a class with
|
|
306
|
+
`@field`. The class is a resolver like the ones above, and its `object` is the
|
|
307
|
+
**parent**: the record the field belongs to (PHP Lighthouse's `$root`). It
|
|
308
|
+
resolves wherever the type appears in a response: in a list, under a
|
|
309
|
+
relationship, or under another class's result.
|
|
310
|
+
|
|
311
|
+
```graphql
|
|
312
|
+
type Post {
|
|
313
|
+
tagsCount: Int! @field(resolver: "Posts::TagsCount")
|
|
314
|
+
excerpt(length: Int = 80): String @field(resolver: "Posts::Excerpt")
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
```ruby
|
|
319
|
+
# app/graphql/resolvers/posts/tags_count.rb
|
|
320
|
+
module Resolvers
|
|
321
|
+
module Posts
|
|
322
|
+
class TagsCount < BaseResolver
|
|
323
|
+
def resolve
|
|
324
|
+
dataload_association(object, :tags).size
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# app/graphql/resolvers/posts/excerpt.rb
|
|
331
|
+
module Resolvers
|
|
332
|
+
module Posts
|
|
333
|
+
class Excerpt < BaseResolver
|
|
334
|
+
def resolve(length:)
|
|
335
|
+
object.body&.truncate(length, separator: ' ')
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
```graphql
|
|
343
|
+
{
|
|
344
|
+
posts(first: 10) { data { title tagsCount excerpt(length: 40) } }
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
The field's arguments arrive as keyword arguments, with the SDL's default when the
|
|
349
|
+
client leaves one out, as on a root field. The class runs once per parent, so load
|
|
350
|
+
anything beyond the parent's own columns through graphql-ruby's dataloader
|
|
351
|
+
(`dataload_association`, `dataload_record`, or a `GraphQL::Dataloader::Source` of
|
|
352
|
+
your own). Then listing ten posts runs one query for their tags, not ten.
|
|
353
|
+
|
|
354
|
+
A field can also resolve with a method of its type's class, as in a graphql-ruby
|
|
355
|
+
app. See [type classes](#type-classes), next: they suit a field that only the type
|
|
356
|
+
needs, and a class named by `@field` suits one with its own logic, its own
|
|
357
|
+
arguments, or that you want to find in the schema.
|
|
358
|
+
|
|
359
|
+
On these types the name is looked up in the resolvers namespace first, a nested
|
|
360
|
+
name too, so `Posts::TagsCount` is `Resolvers::Posts::TagsCount`, kept in
|
|
361
|
+
`app/graphql/resolvers/posts/`. A folder per type keeps these classes apart from
|
|
362
|
+
the `Query` ones. Name it in the plural (`Posts`): a `Resolvers::Post` module would
|
|
363
|
+
hide the `Post` model from code written inside `module Resolvers`. When the
|
|
364
|
+
resolvers namespace has no such class, the name is looked up in
|
|
365
|
+
[`resolver_namespaces`](configuration.md#resolver_namespaces), and a full name
|
|
366
|
+
(`Resolvers::Posts::TagsCount`) works as well. The schema's `base_types` are not
|
|
367
|
+
enforced here, but inheriting from `Resolvers::BaseResolver` still gives the class
|
|
368
|
+
what every resolver shares.
|
|
369
|
+
|
|
370
|
+
### Type classes
|
|
371
|
+
|
|
372
|
+
graphql-ruby keeps the Ruby side of each object type in `app/graphql/types`:
|
|
373
|
+
`Types::PostType`, inheriting from `Types::BaseObject`, as `rails g graphql:object
|
|
374
|
+
Post` writes it. Lighthouse keeps that structure. A type class that includes
|
|
375
|
+
`Lighthouse::GraphQL::Type` is handed to Lighthouse. After `lighthouse:install`,
|
|
376
|
+
`Types::BaseObject` includes it, so every type class does: the SDL still declares the type and its fields, and the class says how
|
|
377
|
+
the fields with no resolving directive resolve. A field calls the method of its
|
|
378
|
+
snake_case name, when the class defines it. Fields it does not define are read off
|
|
379
|
+
the object, as before, and fields with a resolving directive keep it.
|
|
380
|
+
|
|
381
|
+
That makes a type that is not a model as easy to resolve as a model's: the parent
|
|
382
|
+
returns any object (a Hash, a Struct, a client's response), and the type's class
|
|
383
|
+
reads or computes each field from it.
|
|
384
|
+
|
|
385
|
+
```graphql
|
|
386
|
+
type Post {
|
|
387
|
+
title: String!
|
|
388
|
+
readingTime: ReadingTime!
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
type ReadingTime {
|
|
392
|
+
words: Int!
|
|
393
|
+
minutes(wordsPerMinute: Int = 200): Int!
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
```ruby
|
|
398
|
+
# app/graphql/types/post_type.rb
|
|
399
|
+
module Types
|
|
400
|
+
class PostType < BaseObject
|
|
401
|
+
def reading_time
|
|
402
|
+
{ words: object.body.to_s.split.size }
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# app/graphql/types/reading_time_type.rb
|
|
408
|
+
module Types
|
|
409
|
+
class ReadingTimeType < BaseObject
|
|
410
|
+
def minutes(words_per_minute:)
|
|
411
|
+
[(object[:words] / words_per_minute.to_f).ceil, 1].max
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
`Post.title` is the model's column. `Post.readingTime` calls
|
|
418
|
+
`Types::PostType#reading_time`. `ReadingTime.words` is read off the Hash it
|
|
419
|
+
returns, and `ReadingTime.minutes` calls `Types::ReadingTimeType#minutes`.
|
|
420
|
+
|
|
421
|
+
As in graphql-ruby, the class is instantiated per parent, so a method has:
|
|
422
|
+
|
|
423
|
+
- `object`: the parent, here the post;
|
|
424
|
+
- `context`: the query's context;
|
|
425
|
+
- the field's arguments, as snake_case keywords, with the SDL's default when the
|
|
426
|
+
client leaves one out;
|
|
427
|
+
- graphql-ruby's `dataload`, `dataload_record` and `dataload_association`, to load
|
|
428
|
+
anything beyond the parent's own columns without N+1.
|
|
429
|
+
|
|
430
|
+
Wrapping directives (`@guard`, `@can*`, `@cache`) on the field run around the
|
|
431
|
+
method. A method every `GraphQL::Schema::Object` has (`object`, `context`) never
|
|
432
|
+
resolves a field.
|
|
433
|
+
|
|
434
|
+
#### The model
|
|
435
|
+
|
|
436
|
+
The type's model is the SDL's to say, as in PHP Lighthouse: a type named after its
|
|
437
|
+
model needs nothing, and [`@model`](directives.md#model) maps one that is not:
|
|
438
|
+
|
|
439
|
+
```graphql
|
|
440
|
+
type Article @model(class: "Post") {
|
|
441
|
+
title: String!
|
|
442
|
+
headline: String!
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
`Types::ArticleType`'s methods then only ever run on a `Post`: a parent of another
|
|
447
|
+
class (a Hash a resolver returned by mistake, another model) is reported through
|
|
448
|
+
[`on_error`](configuration.md#on_error), which raises in test, and the field
|
|
449
|
+
resolves to `null`. `@model` also gives `@all`, `@find`, `@paginate`, `@create`... on
|
|
450
|
+
fields returning `Article` their model, and resolves a `Post` in a union or
|
|
451
|
+
interface to `Article` ([types.md](types.md#the-default)).
|
|
452
|
+
|
|
453
|
+
#### Opting in
|
|
454
|
+
|
|
455
|
+
The class is the type's name plus `Type`, looked up in `namespaces[:types]`
|
|
456
|
+
(default `['Types']`) when the schema is built. Only a class that includes
|
|
457
|
+
`Lighthouse::GraphQL::Type` counts: any other `Types::PostType` is left alone, and
|
|
458
|
+
its methods resolve nothing. `lighthouse:install` includes the concern in
|
|
459
|
+
`Types::BaseObject`, `BaseInterface`, `BaseUnion` and `BaseScalar`, which hands every
|
|
460
|
+
type class over at once. In an app that did not run it, include the concern in
|
|
461
|
+
those bases, or in each type class (`include Lighthouse::GraphQL::Type`).
|
|
462
|
+
|
|
463
|
+
A class that includes the concern must inherit from the schema's
|
|
464
|
+
`base_types[:object]` when one is given, or the schema does not build. The concern
|
|
465
|
+
also hands over the classes of unions, interfaces and scalars, which pick the type
|
|
466
|
+
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
|
|
467
|
+
methods. Fields are declared in the SDL, so `field` declarations in it are ignored,
|
|
468
|
+
and graphql-ruby's type-level `authorized?` does not run; authorize with
|
|
469
|
+
[`@can*`](authorization.md). `Query` and `Mutation` are the exception: they are
|
|
470
|
+
built on their [root type classes](#root-type-classes), whose `field`s count.
|
|
471
|
+
`Subscription` fields resolve with their own classes.
|
|
178
472
|
|
|
179
473
|
### `@spread` and `@inject`
|
|
180
474
|
|
|
@@ -235,21 +529,19 @@ end
|
|
|
235
529
|
## Generators
|
|
236
530
|
|
|
237
531
|
```shell
|
|
238
|
-
bin/rails generate lighthouse:query LatestPosts # app/graphql/resolvers/latest_posts.rb
|
|
239
|
-
bin/rails generate lighthouse:mutation PublishPost # app/graphql/mutations/publish_post.rb
|
|
240
532
|
bin/rails generate lighthouse:subscription PostUpdated # app/graphql/subscriptions/post_updated.rb
|
|
241
533
|
bin/rails generate lighthouse:builder Published # app/graphql/builders/published.rb
|
|
242
534
|
bin/rails generate lighthouse:validator CreatePostInput # app/graphql/validators/create_post_input_validator.rb
|
|
535
|
+
bin/rails generate graphql:object Post # a type class, Lighthouse's through Types::BaseObject
|
|
536
|
+
bin/rails generate graphql:mutation PublishPost # a code-first mutation, a field of Types::MutationType
|
|
243
537
|
```
|
|
244
538
|
|
|
245
|
-
Each
|
|
246
|
-
for its kind (or `--namespace=…`).
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
`lighthouse:mutation` and a `graphql:mutation` of the same name target the same
|
|
252
|
-
file, and Rails asks before overwriting it.
|
|
539
|
+
Each Lighthouse generator writes the class with its contract, into the first
|
|
540
|
+
namespace configured for its kind (or `--namespace=…`). Type classes come from
|
|
541
|
+
graphql-ruby's own `graphql:object`, `graphql:scalar`, `graphql:union` and
|
|
542
|
+
`graphql:interface`. A resolver class has no generator, as in graphql-ruby: write
|
|
543
|
+
it by hand, as above. [generators.md](generators.md) has what each generator writes,
|
|
544
|
+
and which of graphql-ruby's fit a Lighthouse app.
|
|
253
545
|
|
|
254
546
|
## Changing the namespaces
|
|
255
547
|
|
data/docs/security.md
CHANGED
|
@@ -10,20 +10,24 @@ Lighthouse works the same way on top of webonyx's validation rules.
|
|
|
10
10
|
|
|
11
11
|
## Setting the limits
|
|
12
12
|
|
|
13
|
-
The limits are graphql-ruby's own schema settings. Set them
|
|
14
|
-
|
|
13
|
+
The limits are graphql-ruby's own schema settings. Set them in the body of the
|
|
14
|
+
schema class, as in any graphql-ruby schema (graphql-ruby's install writes
|
|
15
|
+
`max_depth(15)` there):
|
|
15
16
|
|
|
16
17
|
```ruby
|
|
17
18
|
# app/graphql/app_schema.rb
|
|
18
|
-
AppSchema
|
|
19
|
+
class AppSchema < Lighthouse::GraphQL::RbLightHouse.get_schema(options: { base_types: { object: Types::BaseObject } })
|
|
20
|
+
use GraphQL::Dataloader
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
max_complexity(300) # refuse queries that score over 300
|
|
23
|
+
max_depth(12) # refuse queries nested deeper than 12 fields
|
|
24
|
+
disable_introspection_entry_points if Rails.env.production?
|
|
25
|
+
end
|
|
23
26
|
```
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
`
|
|
28
|
+
A schema assigned to a constant (`AppSchema = get_schema(...)`) takes them as calls
|
|
29
|
+
on it (`AppSchema.max_complexity(300)`). You can also set them on a base schema class
|
|
30
|
+
you hand to `get_schema` with `options: { base_schema: MyBaseSchema }`.
|
|
27
31
|
|
|
28
32
|
A query over the limit isn't executed. Its response holds only the error:
|
|
29
33
|
|
|
@@ -208,3 +212,64 @@ GraphQL::Analysis.analyze_query(query, [GraphQL::Analysis::QueryComplexity]).fir
|
|
|
208
212
|
`QueryComplexity` logs a warning unless the schema chooses an algorithm with
|
|
209
213
|
`AppSchema.complexity_cost_calculation_mode(:future)`. `max_complexity` and
|
|
210
214
|
`query_complexity` always use the `:future` algorithm.
|
|
215
|
+
|
|
216
|
+
## Rate limiting
|
|
217
|
+
|
|
218
|
+
Rails' own [`rate_limit`](https://api.rubyonrails.org/classes/ActionController/RateLimiting/ClassMethods.html)
|
|
219
|
+
limits the endpoint: every operation counts, and past the limit Rails answers 429.
|
|
220
|
+
[`@throttle`](directives.md#throttle) is the same limit on one field — a login, a search,
|
|
221
|
+
an expensive mutation — beside it:
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
class GraphqlController < ApplicationController
|
|
225
|
+
include Lighthouse::GraphQL::Controller
|
|
226
|
+
|
|
227
|
+
lighthouse_schema AppSchema
|
|
228
|
+
rate_limit to: 600, within: 1.minute, name: 'endpoint'
|
|
229
|
+
|
|
230
|
+
private
|
|
231
|
+
|
|
232
|
+
def too_many_requests
|
|
233
|
+
render json: { errors: [{ message: 'Too many requests. Try again later.' }] }, status: :too_many_requests
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
```graphql
|
|
239
|
+
type Mutation {
|
|
240
|
+
login(email: String!, password: String!): User @throttle(maxAttempts: 5)
|
|
241
|
+
createTag(name: String!): Tag! @throttle(maxAttempts: 20, by: "current_user.id", with: "too_many_requests") @guard @create
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`@throttle` counts as `rate_limit` does, under its key (`rate-limit:<scope>:<name>:<by>`):
|
|
246
|
+
|
|
247
|
+
- **Where.** In the controller's `cache_store`: `config.action_controller.cache_store`,
|
|
248
|
+
or `Rails.cache` when the app sets none (a controller can set its own with
|
|
249
|
+
`self.cache_store =`). Use a store the app servers share (Redis, Solid Cache) in
|
|
250
|
+
production; `:null_store` counts nothing.
|
|
251
|
+
- **Who.** The client's IP, as `rate_limit`'s default. `by:` reads another value off the
|
|
252
|
+
context, as `@inject` does (`by: "current_user.id"`); when it is `nil`, as for a guest,
|
|
253
|
+
those calls count together, as in Rails.
|
|
254
|
+
- **What counts together.** Each field counts under its own name (`Query.login`), apart
|
|
255
|
+
from the endpoint's `rate_limit` (`endpoint`). Give fields, or a field and a
|
|
256
|
+
controller's `rate_limit`, the same `name:` to count them together; `prefix:` is
|
|
257
|
+
`rate_limit`'s `scope:` (the controller's path by default).
|
|
258
|
+
- **Past the limit.** As `rate_limit`'s default, `ActionController::TooManyRequests` is
|
|
259
|
+
raised, and Rails answers the request with a 429 (in development too); on Rails 7.2,
|
|
260
|
+
which has no such error, `head :too_many_requests`. With `with:`, the controller's
|
|
261
|
+
method runs instead, as `rate_limit`'s `with:`. If it renders or redirects, the request
|
|
262
|
+
ends with that response and the operation stops there, as when a `before_action`
|
|
263
|
+
renders. If it returns, the call goes through. If it raises a `GraphQL::ExecutionError`,
|
|
264
|
+
only that field fails, and the rest of the operation is answered:
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
def rate_limited = raise(GraphQL::ExecutionError, 'Too many requests. Try again later.')
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Without Action Pack, the field fails (`Rate limit for Query.x exceeded. Try again later.`).
|
|
271
|
+
- **Instrumentation.** Each refused call is a `rate_limit.action_controller` event, with
|
|
272
|
+
`rate_limit`'s payload (`request`, `count`, `to`, `within`, `by`, `name`, `scope`,
|
|
273
|
+
`cache_key`).
|
|
274
|
+
|
|
275
|
+
A refused call never reaches `@guard` or the resolver.
|
data/docs/subscriptions.md
CHANGED
|
@@ -38,18 +38,24 @@ end
|
|
|
38
38
|
|
|
39
39
|
graphql-ruby delivers subscriptions through a *backend* that stores who subscribed
|
|
40
40
|
and sends the updates: ActionCable (bundled), or a gem such as Pusher or Ably.
|
|
41
|
-
Lighthouse needs none of its own; give the schema one
|
|
42
|
-
|
|
41
|
+
Lighthouse needs none of its own; give the schema one with `use`, in the schema
|
|
42
|
+
class body, as in any graphql-ruby schema:
|
|
43
43
|
|
|
44
44
|
```ruby
|
|
45
45
|
# app/graphql/app_schema.rb
|
|
46
|
-
AppSchema
|
|
47
|
-
options: {
|
|
46
|
+
class AppSchema < Lighthouse::GraphQL::RbLightHouse.get_schema(
|
|
47
|
+
options: { base_types: { subscription: Subscriptions::BaseSubscription } } # and the other bases
|
|
48
48
|
)
|
|
49
|
+
use GraphQL::Dataloader
|
|
50
|
+
use GraphQL::Subscriptions::ActionCableSubscriptions
|
|
51
|
+
end
|
|
49
52
|
```
|
|
50
53
|
|
|
51
|
-
Options of the backend go
|
|
52
|
-
|
|
54
|
+
Options of the backend go after it (`use GraphQL::Subscriptions::ActionCableSubscriptions, namespace: 'my-app'`).
|
|
55
|
+
A schema assigned to a constant instead (`AppSchema = get_schema(...)`) takes the
|
|
56
|
+
backend through the `using:` option, as `use` does:
|
|
57
|
+
`options: { using: { GraphQL::Subscriptions::ActionCableSubscriptions => {} } }`.
|
|
58
|
+
Either way the schema has a name, which a [queued](#queueing) broadcast finds it by.
|
|
53
59
|
|
|
54
60
|
Then add the channel graphql-ruby documents, which runs the operation and keeps
|
|
55
61
|
the client's subscription ids ([the full example](https://graphql-ruby.org/subscriptions/action_cable_implementation.html)):
|
|
@@ -182,7 +188,7 @@ the base every subscription inherits from, as graphql-ruby's convention puts wha
|
|
|
182
188
|
all classes of a kind share in their base:
|
|
183
189
|
|
|
184
190
|
```ruby
|
|
185
|
-
# app/graphql/subscriptions/base_subscription.rb (rails generate lighthouse:install)
|
|
191
|
+
# app/graphql/subscriptions/base_subscription.rb (rails generate lighthouse:install --subscriptions)
|
|
186
192
|
module Subscriptions
|
|
187
193
|
class BaseSubscription < GraphQL::Schema::Subscription
|
|
188
194
|
include Lighthouse::GraphQL::Subscription
|
|
@@ -411,8 +417,9 @@ end
|
|
|
411
417
|
|
|
412
418
|
The job carries the schema's name, the subscription's name, the trigger arguments,
|
|
413
419
|
the scope and the root; an Active Record model is serialized with GlobalID, any
|
|
414
|
-
other root must be something ActiveJob can serialize. A schema
|
|
415
|
-
`get_schema`
|
|
420
|
+
other root must be something ActiveJob can serialize. A schema class
|
|
421
|
+
(`class AppSchema < ...get_schema(...)`), or a schema assigned to a constant
|
|
422
|
+
(`AppSchema = ...`), has the name; an
|
|
416
423
|
anonymous one raises `Lighthouse::DefinitionError` when a queued broadcast needs
|
|
417
424
|
it. The `activejob` gem is optional: nothing loads it unless a broadcast is queued.
|
|
418
425
|
|