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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +256 -1
  3. data/README.md +26 -15
  4. data/docs/caching.md +4 -3
  5. data/docs/configuration.md +16 -12
  6. data/docs/directives.md +294 -4
  7. data/docs/generators.md +84 -0
  8. data/docs/getting-started.md +93 -60
  9. data/docs/performance.md +2 -2
  10. data/docs/relationships.md +47 -4
  11. data/docs/relay.md +200 -0
  12. data/docs/resolvers.md +326 -34
  13. data/docs/security.md +73 -8
  14. data/docs/subscriptions.md +16 -9
  15. data/docs/types.md +218 -0
  16. data/docs/uploads.md +107 -0
  17. data/lib/generators/lighthouse/base_classes.rb +27 -49
  18. data/lib/generators/lighthouse/install/graphql_ruby_install.rb +113 -0
  19. data/lib/generators/lighthouse/install/install_generator.rb +125 -45
  20. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +10 -8
  21. data/lib/lighthouse/async_mutation_job.rb +17 -0
  22. data/lib/lighthouse/caching.rb +9 -3
  23. data/lib/lighthouse/complexity.rb +5 -4
  24. data/lib/lighthouse/config.rb +12 -18
  25. data/lib/lighthouse/controller.rb +53 -12
  26. data/lib/lighthouse/dataloader_sources/preload_loader.rb +16 -0
  27. data/lib/lighthouse/dataloader_sources/relation_counts_loader.rb +46 -0
  28. data/lib/lighthouse/default_resolve_dispatcher.rb +26 -0
  29. data/lib/lighthouse/directive_resolvers/all_resolver.rb +1 -1
  30. data/lib/lighthouse/directive_resolvers/argument_binding.rb +61 -0
  31. data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +220 -0
  32. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +2 -2
  33. data/lib/lighthouse/directive_resolvers/async_resolver.rb +90 -0
  34. data/lib/lighthouse/directive_resolvers/base.rb +63 -14
  35. data/lib/lighthouse/directive_resolvers/builder_applier.rb +89 -19
  36. data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +1 -1
  37. data/lib/lighthouse/directive_resolvers/can_resolvers.rb +1 -1
  38. data/lib/lighthouse/directive_resolvers/convention_resolver.rb +2 -1
  39. data/lib/lighthouse/directive_resolvers/count_resolver.rb +1 -1
  40. data/lib/lighthouse/directive_resolvers/field_resolver.rb +35 -7
  41. data/lib/lighthouse/directive_resolvers/first_resolver.rb +1 -1
  42. data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +59 -0
  43. data/lib/lighthouse/directive_resolvers/lazy_load_resolver.rb +61 -0
  44. data/lib/lighthouse/directive_resolvers/limit_resolver.rb +59 -0
  45. data/lib/lighthouse/directive_resolvers/method_resolver.rb +38 -3
  46. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +1 -1
  47. data/lib/lighthouse/directive_resolvers/namespaced_resolver.rb +31 -0
  48. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +1 -1
  49. data/lib/lighthouse/directive_resolvers/resolution.rb +19 -4
  50. data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +2 -1
  51. data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +136 -0
  52. data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +87 -0
  53. data/lib/lighthouse/directive_resolvers/with_count_resolver.rb +43 -0
  54. data/lib/lighthouse/directive_resolvers/with_resolver.rb +42 -0
  55. data/lib/lighthouse/directives/arguments/more_filters.rb +215 -0
  56. data/lib/lighthouse/directives/definitions.rb +52 -0
  57. data/lib/lighthouse/directives/model_directive.rb +131 -0
  58. data/lib/lighthouse/directives/node_directive.rb +134 -0
  59. data/lib/lighthouse/directives/pagination_manipulator.rb +1 -1
  60. data/lib/lighthouse/directives/registry.rb +21 -21
  61. data/lib/lighthouse/directives/visibility_directive.rb +71 -0
  62. data/lib/lighthouse/enum_directive.rb +41 -0
  63. data/lib/lighthouse/field_resolver_stack.rb +56 -6
  64. data/lib/lighthouse/generated_resolvers.rb +13 -8
  65. data/lib/lighthouse/global_id.rb +57 -0
  66. data/lib/lighthouse/graphql/version.rb +1 -1
  67. data/lib/lighthouse/graphql_ruby_patches.rb +45 -0
  68. data/lib/lighthouse/multipart_request.rb +43 -0
  69. data/lib/lighthouse/node_registry.rb +68 -0
  70. data/lib/lighthouse/pagination.rb +3 -0
  71. data/lib/lighthouse/rb_lighthouse.rb +34 -0
  72. data/lib/lighthouse/relation_counts.rb +29 -0
  73. data/lib/lighthouse/resolve_info.rb +80 -0
  74. data/lib/lighthouse/resolver_bases.rb +25 -4
  75. data/lib/lighthouse/resolver_dispatch.rb +51 -11
  76. data/lib/lighthouse/root_types.rb +117 -0
  77. data/lib/lighthouse/scalar_coercion.rb +72 -0
  78. data/lib/lighthouse/schema_cache.rb +2 -1
  79. data/lib/lighthouse/schema_generator.rb +13 -11
  80. data/lib/lighthouse/schema_implementation.rb +4 -8
  81. data/lib/lighthouse/support/environment.rb +13 -0
  82. data/lib/lighthouse/support/model_resolver.rb +7 -5
  83. data/lib/lighthouse/support/preload.rb +49 -0
  84. data/lib/lighthouse/type.rb +103 -0
  85. data/lib/lighthouse/type_resolution.rb +150 -0
  86. data/lib/lighthouse/upload.rb +28 -0
  87. data/lib/lighthouse/upload_store.rb +57 -0
  88. data/sig/lighthouse/directives.rbs +3 -2
  89. data/sig/lighthouse.rbs +10 -2
  90. metadata +40 -4
  91. data/lib/generators/lighthouse/install/templates/schema.rb.tt +0 -22
  92. data/lib/generators/lighthouse/mutation_generator.rb +0 -31
  93. 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` (a `GraphQL::Schema::Mutation`) | `Mutation` fields |
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 the fourth kind. They are covered in
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 the first three kinds has a base class in its
21
- namespace, and every class of the kind inherits from it:
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::Mutation
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
- `rails g lighthouse:install` writes the three, and the class generators write the
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 = Lighthouse::GraphQL::RbLightHouse.get_schema(
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
- # object: Types::BaseObject, ... (graphql-ruby's, for the SDL's types)
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
- `rails g lighthouse:install` writes the schema class with them. Without them,
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** resolves to the
138
- class named after it, as in PHP Lighthouse:
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` (what `rails g graphql:install` makes
151
- `Mutations::BaseMutation` inherit from) works too, as long as the SDL field takes the
152
- `input:` argument it expects.
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 one writes the class with its contract, into the first namespace configured
246
- for its kind (or `--namespace=…`). A resolver, mutation or subscription inherits
247
- from the namespace's base class, which is written first when it is missing.
248
- Namespaced names nest modules: `lighthouse:mutation Blog::Publish` writes
249
- `Mutations::Blog::Publish < BaseMutation`. These
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.
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 on the schema
14
- Lighthouse builds:
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 = Lighthouse::GraphQL::RbLightHouse.get_schema(options: { using: { GraphQL::Dataloader => {} } })
19
+ class AppSchema < Lighthouse::GraphQL::RbLightHouse.get_schema(options: { base_types: { object: Types::BaseObject } })
20
+ use GraphQL::Dataloader
19
21
 
20
- AppSchema.max_complexity(300) # refuse queries that score over 300
21
- AppSchema.max_depth(12) # refuse queries nested deeper than 12 fields
22
- AppSchema.disable_introspection_entry_points if Rails.env.production?
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
- You can also set them on a base schema class you hand to `get_schema` with
26
- `options: { base_schema: MyBaseSchema }`.
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.
@@ -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. `get_schema` passes the
42
- `using:` option to graphql-ruby, as `use` does in a class-based schema:
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 = Lighthouse::GraphQL::RbLightHouse.get_schema(
47
- options: { using: { GraphQL::Subscriptions::ActionCableSubscriptions => {} } }
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 in the hash (`{ ActionCableSubscriptions => { namespace: 'my-app' } }`).
52
- Assign the schema to a constant: a [queued](#queueing) broadcast finds it by name.
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 built by
415
- `get_schema` and assigned to a constant (`AppSchema = ...`) has the name; an
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