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.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +182 -1
  3. data/README.md +18 -7
  4. data/docs/best-practices.md +16 -6
  5. data/docs/configuration.md +33 -8
  6. data/docs/directives.md +318 -10
  7. data/docs/events.md +3 -1
  8. data/docs/federation.md +3 -2
  9. data/docs/generators.md +82 -0
  10. data/docs/getting-started.md +66 -32
  11. data/docs/motivation.md +3 -2
  12. data/docs/relationships.md +42 -0
  13. data/docs/relay.md +137 -0
  14. data/docs/resolvers.md +338 -25
  15. data/docs/security.md +227 -0
  16. data/docs/subscriptions.md +60 -37
  17. data/docs/types.md +219 -0
  18. data/docs/uploads.md +80 -0
  19. data/lib/generators/lighthouse/base_classes.rb +70 -0
  20. data/lib/generators/lighthouse/class_generator.rb +19 -8
  21. data/lib/generators/lighthouse/install/install_generator.rb +96 -20
  22. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +19 -10
  23. data/lib/generators/lighthouse/install/templates/schema.rb.tt +21 -2
  24. data/lib/generators/lighthouse/mutation_generator.rb +5 -4
  25. data/lib/generators/lighthouse/query_generator.rb +5 -4
  26. data/lib/generators/lighthouse/subscription_generator.rb +21 -16
  27. data/lib/lighthouse/async_mutation_job.rb +17 -0
  28. data/lib/lighthouse/complexity.rb +175 -0
  29. data/lib/lighthouse/complexity_analyzer.rb +56 -0
  30. data/lib/lighthouse/config.rb +16 -4
  31. data/lib/lighthouse/controller.rb +27 -9
  32. data/lib/lighthouse/dataloader_sources/preload_loader.rb +16 -0
  33. data/lib/lighthouse/dataloader_sources/relation_counts_loader.rb +46 -0
  34. data/lib/lighthouse/default_resolve_dispatcher.rb +26 -0
  35. data/lib/lighthouse/directive_resolvers/all_resolver.rb +1 -1
  36. data/lib/lighthouse/directive_resolvers/argument_binding.rb +61 -0
  37. data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +220 -0
  38. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +2 -2
  39. data/lib/lighthouse/directive_resolvers/async_resolver.rb +90 -0
  40. data/lib/lighthouse/directive_resolvers/base.rb +63 -14
  41. data/lib/lighthouse/directive_resolvers/builder_applier.rb +89 -19
  42. data/lib/lighthouse/directive_resolvers/can_resolvers.rb +1 -1
  43. data/lib/lighthouse/directive_resolvers/convention_resolver.rb +16 -5
  44. data/lib/lighthouse/directive_resolvers/convert_empty_strings_resolver.rb +1 -1
  45. data/lib/lighthouse/directive_resolvers/count_resolver.rb +1 -1
  46. data/lib/lighthouse/directive_resolvers/field_resolver.rb +35 -7
  47. data/lib/lighthouse/directive_resolvers/first_resolver.rb +1 -1
  48. data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +57 -0
  49. data/lib/lighthouse/directive_resolvers/lazy_load_resolver.rb +61 -0
  50. data/lib/lighthouse/directive_resolvers/limit_resolver.rb +59 -0
  51. data/lib/lighthouse/directive_resolvers/method_resolver.rb +38 -3
  52. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +1 -1
  53. data/lib/lighthouse/directive_resolvers/namespaced_resolver.rb +31 -0
  54. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +1 -1
  55. data/lib/lighthouse/directive_resolvers/resolution.rb +19 -4
  56. data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +14 -5
  57. data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +77 -0
  58. data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +81 -0
  59. data/lib/lighthouse/directive_resolvers/with_count_resolver.rb +43 -0
  60. data/lib/lighthouse/directive_resolvers/with_resolver.rb +42 -0
  61. data/lib/lighthouse/directives/arguments/more_filters.rb +215 -0
  62. data/lib/lighthouse/directives/definitions.rb +52 -0
  63. data/lib/lighthouse/directives/model_directive.rb +131 -0
  64. data/lib/lighthouse/directives/node_directive.rb +134 -0
  65. data/lib/lighthouse/directives/pagination_manipulator.rb +1 -1
  66. data/lib/lighthouse/directives/registry.rb +21 -20
  67. data/lib/lighthouse/directives/visibility_directive.rb +71 -0
  68. data/lib/lighthouse/enum_directive.rb +41 -0
  69. data/lib/lighthouse/events/execution_trace.rb +2 -1
  70. data/lib/lighthouse/events/lifecycle.rb +3 -1
  71. data/lib/lighthouse/field_resolver_stack.rb +59 -11
  72. data/lib/lighthouse/generated_resolvers.rb +13 -8
  73. data/lib/lighthouse/global_id.rb +31 -0
  74. data/lib/lighthouse/graphql/version.rb +1 -1
  75. data/lib/lighthouse/multipart_request.rb +43 -0
  76. data/lib/lighthouse/node_registry.rb +68 -0
  77. data/lib/lighthouse/pagination.rb +3 -0
  78. data/lib/lighthouse/rb_lighthouse.rb +36 -0
  79. data/lib/lighthouse/relation_counts.rb +29 -0
  80. data/lib/lighthouse/resolve_info.rb +80 -0
  81. data/lib/lighthouse/resolver_bases.rb +68 -0
  82. data/lib/lighthouse/resolver_dispatch.rb +36 -11
  83. data/lib/lighthouse/scalar_coercion.rb +72 -0
  84. data/lib/lighthouse/schema_cache.rb +2 -1
  85. data/lib/lighthouse/schema_factory.rb +0 -1
  86. data/lib/lighthouse/schema_generator.rb +21 -12
  87. data/lib/lighthouse/schema_implementation.rb +4 -8
  88. data/lib/lighthouse/sdl_loader.rb +0 -2
  89. data/lib/lighthouse/support/environment.rb +13 -0
  90. data/lib/lighthouse/support/model_resolver.rb +8 -6
  91. data/lib/lighthouse/support/preload.rb +49 -0
  92. data/lib/lighthouse/type.rb +103 -0
  93. data/lib/lighthouse/type_resolution.rb +150 -0
  94. data/lib/lighthouse/upload.rb +26 -0
  95. data/lib/lighthouse/upload_store.rb +32 -0
  96. data/sig/lighthouse/directives.rbs +3 -2
  97. data/sig/lighthouse/graphql.rbs +29 -0
  98. data/sig/lighthouse.rbs +18 -0
  99. metadata +43 -1
data/docs/resolvers.md CHANGED
@@ -1,4 +1,4 @@
1
- # Queries, Mutations and Builders
1
+ # Resolvers, Mutations and Builders
2
2
 
3
3
  Most fields need no Ruby: a directive (`@all`, `@find`, `@paginate`, `@create`,
4
4
  …) or the default attribute reader resolves them. For the rest, Lighthouse looks
@@ -7,17 +7,101 @@ interchangeable. Each kind has its own contract and its own folder:
7
7
 
8
8
  | Kind | Folder / namespace | Contract | Used by |
9
9
  | --- | --- | --- | --- |
10
- | Query | `app/graphql/queries` · `Queries::` | `GraphQL::Schema::Resolver` | `Query` fields |
11
- | Mutation | `app/graphql/mutations` · `Mutations::` | `GraphQL::Schema::Mutation` | `Mutation` fields |
12
- | Subscription | `app/graphql/subscriptions` · `Subscriptions::` | `GraphQL::Schema::Subscription` | `Subscription` fields ([subscriptions.md](subscriptions.md)) |
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 |
12
+ | Subscription | `app/graphql/subscriptions` · `Subscriptions::` | `Subscriptions::BaseSubscription` (a `GraphQL::Schema::Subscription`) | `Subscription` fields ([subscriptions.md](subscriptions.md)) |
13
+ | Type | `app/graphql/types` · `Types::` | `Types::BaseObject` (a `GraphQL::Schema::Object`) including `Lighthouse::GraphQL::Type`, named `<Type>Type` | Fields of other types, through its methods ([type classes](#type-classes)); unions, interfaces and scalars too ([types.md](types.md)) |
13
14
  | Builder | `app/graphql/builders` · `Builders::` | `call(relation, args, context)` → relation | `@builder` |
14
15
 
15
- Validators (`app/graphql/validators`) are 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
- ## Queries and mutations: graphql-ruby's contract
19
+ ## Base classes
19
20
 
20
- A query or mutation class is a plain graphql-ruby
21
+ As in any graphql-ruby app, each kind of class has a base class in its namespace,
22
+ and every class of the kind inherits from it. graphql-ruby's `graphql:install`,
23
+ which `rails g lighthouse:install` runs, writes `Resolvers::BaseResolver`,
24
+ `Mutations::BaseMutation` and the `Types::Base*` classes; Lighthouse adds
25
+ `Subscriptions::BaseSubscription`, which graphql-ruby has none of, and makes the
26
+ mutation base a plain `GraphQL::Schema::Mutation`, as the SDL declares a mutation's
27
+ arguments ([generators.md](generators.md)):
28
+
29
+ ```ruby
30
+ # app/graphql/resolvers/base_resolver.rb (graphql-ruby's)
31
+ module Resolvers
32
+ class BaseResolver < GraphQL::Schema::Resolver
33
+ end
34
+ end
35
+
36
+ # app/graphql/mutations/base_mutation.rb (graphql-ruby's, made a Mutation)
37
+ module Mutations
38
+ class BaseMutation < GraphQL::Schema::Mutation
39
+ argument_class Types::BaseArgument
40
+ field_class Types::BaseField
41
+ object_class Types::BaseObject
42
+ end
43
+ end
44
+
45
+ # app/graphql/subscriptions/base_subscription.rb (Lighthouse's)
46
+ module Subscriptions
47
+ class BaseSubscription < GraphQL::Schema::Subscription
48
+ include Lighthouse::GraphQL::Subscription
49
+ end
50
+ end
51
+ ```
52
+
53
+ What every class of a kind shares goes in its base, once: a concern (as
54
+ `BaseSubscription` includes Lighthouse's, see [subscriptions.md](subscriptions.md)),
55
+ a helper, graphql-ruby settings such as `argument_class Types::BaseArgument`. An
56
+ existing base is never replaced.
57
+
58
+ ### Handing the bases to the schema
59
+
60
+ Name the bases in `base_types`, beside graphql-ruby's own base types, and the
61
+ schema holds every class to them:
62
+
63
+ ```ruby
64
+ # app/graphql/app_schema.rb
65
+ AppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(
66
+ options: {
67
+ base_types: {
68
+ resolver: Resolvers::BaseResolver,
69
+ mutation: Mutations::BaseMutation,
70
+ subscription: Subscriptions::BaseSubscription,
71
+ object: Types::BaseObject,
72
+ interface: Types::BaseInterface,
73
+ union: Types::BaseUnion,
74
+ scalar: Types::BaseScalar,
75
+ enum: Types::BaseEnum,
76
+ input_object: Types::BaseInputObject
77
+ }
78
+ }
79
+ )
80
+ ```
81
+
82
+ - A class a `Query`, `Mutation` or `Subscription` field resolves to, by convention
83
+ or through `@subscription(class:)`, that does not inherit from its kind's base
84
+ raises `Lighthouse::DefinitionError` when the field is first used (`…so it must
85
+ inherit from Resolvers::BaseResolver…`). No class misses what the base shares.
86
+ - A `Subscription` field with no class of its own resolves with the subscription
87
+ base, so its concern and settings apply there too.
88
+ - A base must be a subclass of the graphql-ruby class of its kind
89
+ (`GraphQL::Schema::Resolver`, `GraphQL::Schema::Mutation`, which
90
+ `RelayClassicMutation` is, `GraphQL::Schema::Subscription`), or the schema does not
91
+ build. graphql-ruby never sees these three keys.
92
+ - The others are graphql-ruby's: it builds the SDL's types of each kind from them.
93
+ A [type class](#type-classes) (one including `Lighthouse::GraphQL::Type`) that
94
+ does not inherit from its kind's raises `Lighthouse::DefinitionError` when the
95
+ schema is built.
96
+
97
+ `rails g lighthouse:install` writes the schema class with them. Without them,
98
+ Lighthouse requires only the graphql-ruby class (`GraphQL::Schema::Resolver`, …),
99
+ and a `Subscription` field with no class resolves with a built-in subscription that
100
+ includes the concern.
101
+
102
+ ## Resolvers and mutations: graphql-ruby's contract
103
+
104
+ A resolver or mutation class is a plain graphql-ruby
21
105
  [resolver](https://graphql-ruby.org/fields/resolvers.html) or
22
106
  [mutation](https://graphql-ruby.org/mutations/mutation_classes.html), so everything
23
107
  you know from graphql-ruby applies:
@@ -40,9 +124,9 @@ type Mutation {
40
124
  ```
41
125
 
42
126
  ```ruby
43
- # app/graphql/queries/latest_posts.rb
44
- module Queries
45
- class LatestPosts < GraphQL::Schema::Resolver
127
+ # app/graphql/resolvers/latest_posts.rb
128
+ module Resolvers
129
+ class LatestPosts < BaseResolver
46
130
  def resolve(limit: 10)
47
131
  Post.order(created_at: :desc).limit(limit)
48
132
  end
@@ -51,7 +135,7 @@ end
51
135
 
52
136
  # app/graphql/mutations/publish_post.rb
53
137
  module Mutations
54
- class PublishPost < GraphQL::Schema::Mutation
138
+ class PublishPost < BaseMutation
55
139
  def authorized?(id:)
56
140
  context[:current_user]&.admin? || raise(GraphQL::ExecutionError, 'Admins only.')
57
141
  end
@@ -68,7 +152,7 @@ end
68
152
  A field on `Query` or `Mutation` with **no resolving directive** resolves to the
69
153
  class named after it, as in PHP Lighthouse:
70
154
 
71
- - `Query.latestPosts` resolves to `Queries::LatestPosts`;
155
+ - `Query.latestPosts` resolves to `Resolvers::LatestPosts`;
72
156
  - `Mutation.publishPost` resolves to `Mutations::PublishPost`.
73
157
 
74
158
  The class is looked up on each call, so code reloading works. Classes found by
@@ -78,8 +162,8 @@ field with no class is reported through [`on_error`](configuration.md#on_error):
78
162
  it raises in test and logs elsewhere. Lighthouse never falls back to a top-level
79
163
  constant, so a `post` field is never mistaken for the `Post` model.
80
164
 
81
- graphql-ruby's `RelayClassicMutation` (what `rails g graphql:install` generates
82
- `Mutations::BaseMutation` from) works too, as long as the SDL field takes the
165
+ graphql-ruby's `RelayClassicMutation` (what `rails g graphql:install` makes
166
+ `Mutations::BaseMutation` inherit from) works too, as long as the SDL field takes the
83
167
  `input:` argument it expects.
84
168
 
85
169
  Wrapping directives (`@guard`, the `@can*` family, `@validator`, `@cache`,
@@ -95,17 +179,243 @@ type Mutation {
95
179
  ### Naming a class explicitly
96
180
 
97
181
  `@field(resolver:)` points a field at a class by name. On `Query` and `Mutation`
98
- the name is looked up in the queries and mutations namespaces; on any other type,
182
+ the name is looked up in the resolvers and mutations namespaces; on any other type,
99
183
  in [`resolver_namespaces`](configuration.md#resolver_namespaces).
100
184
 
101
185
  ```graphql
102
186
  type Query {
103
- feed: [Post!]! @field(resolver: "Feeds::Home") # Queries::Feeds::Home
187
+ feed: [Post!]! @field(resolver: "Feeds::Home") # Resolvers::Feeds::Home
104
188
  }
105
189
  ```
106
190
 
107
191
  Besides graphql-ruby resolver classes, `@field` also accepts a plain callable:
108
- anything with `call(object, args, context)`.
192
+ anything with `call(object, args, context)`, and `call(object, args, context,
193
+ resolve_info)` when it takes a fourth argument.
194
+
195
+ A method after `@` (PHP Lighthouse's `Class@method`) replaces `call`, or `resolve` on
196
+ a resolver class (whose `authorized?` still runs). One class can then hold several
197
+ fields' resolvers:
198
+
199
+ ```graphql
200
+ type Query {
201
+ counts: Counts! @field(resolver: "Statistics@counts")
202
+ latest: [Post!]! @field(resolver: "Statistics@latest")
203
+ }
204
+ ```
205
+
206
+ ```ruby
207
+ class Resolvers::Statistics
208
+ def self.counts(_root, _args, _context) = { posts: Post.count, users: User.count }
209
+ def self.latest(_root, _args, _context) = Post.order(created_at: :desc).limit(5)
210
+ end
211
+ ```
212
+
213
+ ### ResolveInfo
214
+
215
+ What PHP Lighthouse hands a resolver as `$resolveInfo`. A `Resolvers::` or
216
+ `Mutations::` class, a class `@field` names and a type class's methods read it as
217
+ `resolve_info`:
218
+
219
+ | | |
220
+ | --- | --- |
221
+ | `field_name`, `field`, `return_type` | the field being resolved |
222
+ | `parent_type` | the type it belongs to |
223
+ | `object`, `arguments`, `context` | what it resolves with |
224
+ | `path` | `["posts", 0, "author"]` |
225
+ | `lookahead` | graphql-ruby's [`Lookahead`](https://graphql-ruby.org/queries/lookahead.html): what the client selects below the field (`lookahead.selects?(:author)`) |
226
+ | `enhance_builder(relation, scopes: [])` | `relation` with the field's query directives applied |
227
+
228
+ `enhance_builder` lets a class build the query no directive could, and still honour
229
+ what the field declares: its `@builder` classes, and the argument directives the
230
+ client used (`@eq`, `@where`, `@in`, `@like`, `@orderBy`, `@whereConditions`, ...),
231
+ as `@all` and `@paginate` apply them. `scopes:` adds model scopes.
232
+
233
+ ```graphql
234
+ type Query {
235
+ myPosts(title: String @like, orderBy: [PostOrderByClause!] @orderBy): [Post!]! @guard
236
+ }
237
+ ```
238
+
239
+ ```ruby
240
+ class Resolvers::MyPosts < Resolvers::BaseResolver
241
+ def resolve(**)
242
+ resolve_info.enhance_builder(context[:current_user].posts, scopes: [:recent])
243
+ end
244
+ end
245
+ ```
246
+
247
+ ### Fields of other types
248
+
249
+ A field of any other type resolves with the default reader: `Post.commentsCount`
250
+ reads `post.comments_count`. When its value takes more than reading an attribute
251
+ or calling a method ([`@method`](directives.md#method)), name a class with
252
+ `@field`. The class is a resolver like the ones above, and its `object` is the
253
+ **parent**: the record the field belongs to (PHP Lighthouse's `$root`). It
254
+ resolves wherever the type appears in a response: in a list, under a
255
+ relationship, or under another class's result.
256
+
257
+ ```graphql
258
+ type Post {
259
+ tagsCount: Int! @field(resolver: "Posts::TagsCount")
260
+ excerpt(length: Int = 80): String @field(resolver: "Posts::Excerpt")
261
+ }
262
+ ```
263
+
264
+ ```ruby
265
+ # app/graphql/resolvers/posts/tags_count.rb
266
+ module Resolvers
267
+ module Posts
268
+ class TagsCount < BaseResolver
269
+ def resolve
270
+ dataload_association(object, :tags).size
271
+ end
272
+ end
273
+ end
274
+ end
275
+
276
+ # app/graphql/resolvers/posts/excerpt.rb
277
+ module Resolvers
278
+ module Posts
279
+ class Excerpt < BaseResolver
280
+ def resolve(length:)
281
+ object.body&.truncate(length, separator: ' ')
282
+ end
283
+ end
284
+ end
285
+ end
286
+ ```
287
+
288
+ ```graphql
289
+ {
290
+ posts(first: 10) { data { title tagsCount excerpt(length: 40) } }
291
+ }
292
+ ```
293
+
294
+ The field's arguments arrive as keyword arguments, with the SDL's default when the
295
+ client leaves one out, as on a root field. The class runs once per parent, so load
296
+ anything beyond the parent's own columns through graphql-ruby's dataloader
297
+ (`dataload_association`, `dataload_record`, or a `GraphQL::Dataloader::Source` of
298
+ your own). Then listing ten posts runs one query for their tags, not ten.
299
+
300
+ A field can also resolve with a method of its type's class, as in a graphql-ruby
301
+ app. See [type classes](#type-classes), next: they suit a field that only the type
302
+ needs, and a class named by `@field` suits one with its own logic, its own
303
+ arguments, or that you want to find in the schema.
304
+
305
+ On these types the name is looked up in the resolvers namespace first, a nested
306
+ name too, so `Posts::TagsCount` is `Resolvers::Posts::TagsCount`, kept in
307
+ `app/graphql/resolvers/posts/`. A folder per type keeps these classes apart from
308
+ the `Query` ones. Name it in the plural (`Posts`): a `Resolvers::Post` module would
309
+ hide the `Post` model from code written inside `module Resolvers`. When the
310
+ resolvers namespace has no such class, the name is looked up in
311
+ [`resolver_namespaces`](configuration.md#resolver_namespaces), and a full name
312
+ (`Resolvers::Posts::TagsCount`) works as well. The schema's `base_types` are not
313
+ enforced here, but inheriting from `Resolvers::BaseResolver` still gives the class
314
+ what every resolver shares.
315
+
316
+ ### Type classes
317
+
318
+ graphql-ruby keeps the Ruby side of each object type in `app/graphql/types`:
319
+ `Types::PostType`, inheriting from `Types::BaseObject`, as `rails g graphql:object
320
+ Post` writes it. Lighthouse keeps that structure. A type class that includes `Lighthouse::GraphQL::Type` is handed to
321
+ Lighthouse: the SDL still declares the type and its fields, and the class says how
322
+ the fields with no resolving directive resolve. A field calls the method of its
323
+ snake_case name, when the class defines it. Fields it does not define are read off
324
+ the object, as before, and fields with a resolving directive keep it.
325
+
326
+ That makes a type that is not a model as easy to resolve as a model's: the parent
327
+ returns any object (a Hash, a Struct, a client's response), and the type's class
328
+ reads or computes each field from it.
329
+
330
+ ```graphql
331
+ type Post {
332
+ title: String!
333
+ readingTime: ReadingTime!
334
+ }
335
+
336
+ type ReadingTime {
337
+ words: Int!
338
+ minutes(wordsPerMinute: Int = 200): Int!
339
+ }
340
+ ```
341
+
342
+ ```ruby
343
+ # app/graphql/types/post_type.rb
344
+ module Types
345
+ class PostType < BaseObject
346
+ include Lighthouse::GraphQL::Type
347
+
348
+ def reading_time
349
+ { words: object.body.to_s.split.size }
350
+ end
351
+ end
352
+ end
353
+
354
+ # app/graphql/types/reading_time_type.rb
355
+ module Types
356
+ class ReadingTimeType < BaseObject
357
+ include Lighthouse::GraphQL::Type
358
+
359
+ def minutes(words_per_minute:)
360
+ [(object[:words] / words_per_minute.to_f).ceil, 1].max
361
+ end
362
+ end
363
+ end
364
+ ```
365
+
366
+ `Post.title` is the model's column. `Post.readingTime` calls
367
+ `Types::PostType#reading_time`. `ReadingTime.words` is read off the Hash it
368
+ returns, and `ReadingTime.minutes` calls `Types::ReadingTimeType#minutes`.
369
+
370
+ As in graphql-ruby, the class is instantiated per parent, so a method has:
371
+
372
+ - `object`: the parent, here the post;
373
+ - `context`: the query's context;
374
+ - the field's arguments, as snake_case keywords, with the SDL's default when the
375
+ client leaves one out;
376
+ - graphql-ruby's `dataload`, `dataload_record` and `dataload_association`, to load
377
+ anything beyond the parent's own columns without N+1.
378
+
379
+ Wrapping directives (`@guard`, `@can*`, `@cache`) on the field run around the
380
+ method. A method every `GraphQL::Schema::Object` has (`object`, `context`) never
381
+ resolves a field.
382
+
383
+ #### The model
384
+
385
+ The type's model is the SDL's to say, as in PHP Lighthouse: a type named after its
386
+ model needs nothing, and [`@model`](directives.md#model) maps one that is not:
387
+
388
+ ```graphql
389
+ type Article @model(class: "Post") {
390
+ title: String!
391
+ headline: String!
392
+ }
393
+ ```
394
+
395
+ `Types::ArticleType`'s methods then only ever run on a `Post`: a parent of another
396
+ class (a Hash a resolver returned by mistake, another model) is reported through
397
+ [`on_error`](configuration.md#on_error), which raises in test, and the field
398
+ resolves to `null`. `@model` also gives `@all`, `@find`, `@paginate`, `@create`... on
399
+ fields returning `Article` their model, and resolves a `Post` in a union or
400
+ interface to `Article` ([types.md](types.md#the-default)).
401
+
402
+ #### Opting in
403
+
404
+ The class is the type's name plus `Type`, looked up in `namespaces[:types]`
405
+ (default `['Types']`) when the schema is built. Only a class that includes
406
+ `Lighthouse::GraphQL::Type` counts: any other `Types::PostType`, such as the ones
407
+ a graphql-ruby app already has, is left alone, and its methods resolve nothing.
408
+ Include the concern in `Types::BaseObject` to hand every type class over at once.
409
+
410
+ A class that includes the concern must inherit from the schema's
411
+ `base_types[:object]` when one is given, or the schema does not build. The concern
412
+ also hands over the classes of unions, interfaces and scalars, which pick the type
413
+ of a union's values or coerce a scalar's: see [types.md](types.md). It is not the schema's type: it only lends its
414
+ methods. Fields are declared in the SDL, so `field` declarations in it are ignored,
415
+ and graphql-ruby's type-level `authorized?` does not run; authorize with
416
+ [`@can*`](authorization.md). `Query`, `Mutation` and `Subscription` fields resolve
417
+ with their own classes, [by convention](#resolution-by-convention), not with a type
418
+ class.
109
419
 
110
420
  ### `@spread` and `@inject`
111
421
 
@@ -166,26 +476,29 @@ end
166
476
  ## Generators
167
477
 
168
478
  ```shell
169
- bin/rails generate lighthouse:query LatestPosts # app/graphql/queries/latest_posts.rb
479
+ bin/rails generate lighthouse:query LatestPosts # app/graphql/resolvers/latest_posts.rb
170
480
  bin/rails generate lighthouse:mutation PublishPost # app/graphql/mutations/publish_post.rb
171
481
  bin/rails generate lighthouse:subscription PostUpdated # app/graphql/subscriptions/post_updated.rb
172
482
  bin/rails generate lighthouse:builder Published # app/graphql/builders/published.rb
173
483
  bin/rails generate lighthouse:validator CreatePostInput # app/graphql/validators/create_post_input_validator.rb
484
+ bin/rails generate graphql:object Post # a type class: then include Lighthouse::GraphQL::Type
174
485
  ```
175
486
 
176
- Each one writes the class with its contract, into the first namespace configured
177
- for its kind (or `--namespace=…`). Namespaced names nest modules:
178
- `lighthouse:mutation Blog::Publish` writes `Mutations::Blog::Publish`. These
179
- generators do not collide with graphql-ruby's `graphql:*` generators. A
180
- `lighthouse:mutation` and a `graphql:mutation` of the same name target the same
181
- file, and Rails asks before overwriting it.
487
+ Each writes the class with its contract, into the first namespace configured for its
488
+ kind (or `--namespace=…`), inheriting from the namespace's base class. Type classes
489
+ come from graphql-ruby's own `graphql:object`, `graphql:scalar`, `graphql:union` and
490
+ `graphql:interface`. [generators.md](generators.md) has what each generator writes,
491
+ and which of graphql-ruby's fit a Lighthouse app.
182
492
 
183
493
  ## Changing the namespaces
184
494
 
185
495
  ```ruby
186
496
  Lighthouse.configure do |config|
187
- config.namespaces = { queries: ['Api::Queries'], mutations: ['Api::Mutations'] }
497
+ config.namespaces = { queries: ['Api::Resolvers'], mutations: ['Api::Mutations'] }
188
498
  end
189
499
  ```
190
500
 
191
501
  Kinds you leave out keep their defaults. Each kind takes a list, tried in order.
502
+ The `queries` kind is the classes of `Query` fields. Before 0.4.0 it defaulted to
503
+ `Queries`; an app keeping its classes in `app/graphql/queries` sets
504
+ `config.namespaces = { queries: ['Queries'] }`.
data/docs/security.md ADDED
@@ -0,0 +1,227 @@
1
+ # Query Complexity & Depth
2
+
3
+ A single GraphQL request can ask for a lot: a page of 1,000 posts, each with
4
+ its 1,000 comments, each with its author. graphql-ruby can refuse such a query
5
+ **before it runs**. It scores the query from the fields it selects, then
6
+ compares the score with a limit you set. Lighthouse doesn't add a scorer of its
7
+ own. It gives your SDL's fields their cost, and graphql-ruby's analyzers
8
+ (`GraphQL::Analysis::MaxQueryComplexity`, `MaxQueryDepth`) do the rest. PHP
9
+ Lighthouse works the same way on top of webonyx's validation rules.
10
+
11
+ ## Setting the limits
12
+
13
+ The limits are graphql-ruby's own schema settings. Set them on the schema
14
+ Lighthouse builds:
15
+
16
+ ```ruby
17
+ # app/graphql/app_schema.rb
18
+ AppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(options: { using: { GraphQL::Dataloader => {} } })
19
+
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?
23
+ ```
24
+
25
+ You can also set them on a base schema class you hand to `get_schema` with
26
+ `options: { base_schema: MyBaseSchema }`.
27
+
28
+ A query over the limit isn't executed. Its response holds only the error:
29
+
30
+ ```json
31
+ { "errors": [{ "message": "Query has complexity of 3001, which exceeds max complexity of 300" }] }
32
+ ```
33
+
34
+ To give one request a different limit, pass it to `execute`:
35
+ `AppSchema.execute(query, max_complexity: 1000)`.
36
+
37
+ ## How a query is scored
38
+
39
+ Each selected field adds its cost, and a field's cost includes the cost of the
40
+ fields selected inside it (its *child complexity*). By default, graphql-ruby
41
+ counts **1 per field**:
42
+
43
+ ```graphql
44
+ { posts { id title author { name } } } # posts 1 + id 1 + title 1 + author 1 + name 1 = 5
45
+ ```
46
+
47
+ Lighthouse changes that default in three places:
48
+
49
+ | Field | Cost |
50
+ | --- | --- |
51
+ | Paginated: `@paginate`, or `@hasMany` / `@belongsToMany` / `@morphMany` / `@morphToMany` given `type:` | `1 + child complexity × first` |
52
+ | Carrying `@complexity` | `1 + child complexity × first`, or the `complexity` of the analyzer it names (below) |
53
+ | Resolved by a `GraphQL::Schema::Resolver` class, by convention (`Query.search` → `Resolvers::Search`) or through `@field` | the `complexity` that class declares (default: `1 + child complexity`) |
54
+
55
+ Paginated fields scale by `first`, which already holds the page size when the
56
+ client leaves it out (the directive's `defaultCount`, else
57
+ `pagination_default_count`). That way, asking for 1,000 items costs 1,000 times
58
+ as much as asking for one:
59
+
60
+ ```graphql
61
+ { posts(first: 100) { data { id title } } } # 1 + (data 1 + id 1 + title 1) × 100 = 301
62
+ ```
63
+
64
+ For a connection (`type: CONNECTION`) the same rule applies, multiplying
65
+ everything under `edges` and `pageInfo`, as PHP Lighthouse does. It replaces
66
+ graphql-ruby's own connection scaling, which knows nothing about `defaultCount`.
67
+
68
+ ## `@complexity`
69
+
70
+ Customize a field's cost, or attach a graphql-ruby analyzer to the schema.
71
+
72
+ ```graphql
73
+ type Query {
74
+ "1 + child complexity × first, as on a paginated field."
75
+ search(first: Int, term: String!): [Post!]! @complexity
76
+
77
+ "What Analyzers::PostsComplexity says."
78
+ posts(includeFullText: Boolean): [Post!]! @all @complexity(resolver: "PostsComplexity")
79
+ }
80
+ ```
81
+
82
+ | Argument | Meaning |
83
+ | --- | --- |
84
+ | `resolver: String` | A `GraphQL::Analysis::Analyzer` subclass, looked up in `namespaces[:analyzers]` (`Analyzers::PostsComplexity`). A name containing `::` is taken as written. |
85
+
86
+ Without `resolver:`, the field costs `1 + child complexity × first` (`first` is 1
87
+ when the field has no such argument).
88
+
89
+ ### The analyzer
90
+
91
+ `resolver:` names a graphql-ruby analyzer, written as graphql-ruby's
92
+ [analysis guide](https://graphql-ruby.org/queries/ast_analysis.html) writes it.
93
+ Lighthouse installs it on the schema, as `query_analyzer Analyzers::PostsComplexity`
94
+ would, once however many fields name it. Like every graphql-ruby analyzer, it
95
+ runs on **every query**, not only those that select the field:
96
+
97
+ ```ruby
98
+ # app/graphql/analyzers/agent_bot_analyzer.rb
99
+ class Analyzers::AgentBotAnalyzer < GraphQL::Analysis::Analyzer
100
+ def analyze? = subject.context[:current_user].is_a?(AgentBot)
101
+
102
+ def on_enter_field(node, _parent, visitor)
103
+ @refused = true if visitor.parent_type_definition == visitor.query.schema.mutation
104
+ end
105
+
106
+ def result
107
+ GraphQL::AnalysisError.new('Bots may not run mutations') if @refused
108
+ end
109
+ end
110
+ ```
111
+
112
+ A field naming a plain analyzer costs what a bare `@complexity` gives. An
113
+ analyzer that can't be found, or a class that isn't a
114
+ `GraphQL::Analysis::Analyzer`, stops the schema from building
115
+ (`Lighthouse::DefinitionError`).
116
+
117
+ ### Giving the field its cost: `Lighthouse::GraphQL::ComplexityAnalyzer`
118
+
119
+ Include the concern, and the analyzer's `complexity` becomes the cost of the
120
+ fields naming it. It's the `complexity` graphql-ruby gives a field or a
121
+ `GraphQL::Schema::Resolver`: a number, added to the cost of the field's
122
+ selection, or a proc called with the query's context, the field's arguments
123
+ (snake_case Symbol keys) and the child complexity, returning the whole cost.
124
+
125
+ As with resolvers and mutations, the concern goes once in a base class, and each
126
+ analyzer that prices a field inherits from it:
127
+
128
+ ```ruby
129
+ # app/graphql/analyzers/base_complexity.rb
130
+ class Analyzers::BaseComplexity < GraphQL::Analysis::QueryComplexity
131
+ include Lighthouse::GraphQL::ComplexityAnalyzer
132
+ end
133
+
134
+ # app/graphql/analyzers/posts_complexity.rb
135
+ class Analyzers::PostsComplexity < Analyzers::BaseComplexity
136
+ complexity ->(_context, args, child_complexity) { child_complexity * (args[:include_full_text] ? 3 : 2) }
137
+ end
138
+ ```
139
+
140
+ `complexity 5` gives a fixed cost: 5 plus the selection's. A subclass inherits
141
+ its superclass's `complexity`. Without one, the field costs what a bare
142
+ `@complexity` gives. The cost wins over the pagination default.
143
+
144
+ The class is still an analyzer. With a `result` of its own, it runs on every
145
+ query like any other, so one class can price a field and act on the whole query,
146
+ for example refusing a guest's query over a smaller budget:
147
+
148
+ ```ruby
149
+ class Analyzers::PostsComplexity < Analyzers::BaseComplexity
150
+ complexity 10
151
+
152
+ def result
153
+ cost = max_possible_complexity
154
+ return if query.context[:current_user] || cost <= 200
155
+
156
+ GraphQL::AnalysisError.new("Guests may run queries up to 200, this one costs #{cost}")
157
+ end
158
+ end
159
+ ```
160
+
161
+ Without a `result` of its own, it only gives costs, and graphql-ruby skips it
162
+ during analysis.
163
+
164
+ ## Resolver classes declare their own cost
165
+
166
+ A root field without a resolving directive resolves to a
167
+ `GraphQL::Schema::Resolver` by convention (see [resolvers.md](resolvers.md)).
168
+ That class's `complexity` applies, exactly as with graphql-ruby's
169
+ `field :search, resolver: Resolvers::Search`:
170
+
171
+ ```ruby
172
+ class Resolvers::Search < Resolvers::BaseResolver
173
+ complexity ->(_ctx, args, child_complexity) { args[:limit] * child_complexity }
174
+
175
+ def resolve(term:, limit:) = Post.search(term).limit(limit)
176
+ end
177
+ ```
178
+
179
+ The same goes for a class named by `@field(resolver:)`, and for a mutation
180
+ class. An explicit `@complexity` on the field takes precedence over the class.
181
+
182
+ ## Reading the score
183
+
184
+ `BuildExtensionsResponse` carries the score as `query_complexity`. It's the same
185
+ number `max_complexity` is checked against, so you can report it to clients or
186
+ to your metrics:
187
+
188
+ ```ruby
189
+ Lighthouse::Events.listen(Lighthouse::Events::BuildExtensionsResponse) do |event|
190
+ Lighthouse::Events::ExtensionsResponse.new('complexity', event.query_complexity)
191
+ end
192
+ # => { "data" => {...}, "extensions" => { "complexity" => 301 } }
193
+ ```
194
+
195
+ The query is scored only while something listens to that event, and a query
196
+ that failed validation is never scored (`query_complexity` is `nil`). A query
197
+ refused for going over `max_complexity` still carries its score, so you can log
198
+ what was refused. See
199
+ [events.md](events.md#execution-events).
200
+
201
+ To score a query yourself, run graphql-ruby's analyzer:
202
+
203
+ ```ruby
204
+ query = GraphQL::Query.new(AppSchema, '{ posts(first: 10) { data { id } } }')
205
+ GraphQL::Analysis.analyze_query(query, [GraphQL::Analysis::QueryComplexity]).first # => 21
206
+ ```
207
+
208
+ `QueryComplexity` logs a warning unless the schema chooses an algorithm with
209
+ `AppSchema.complexity_cost_calculation_mode(:future)`. `max_complexity` and
210
+ `query_complexity` always use the `:future` algorithm.
211
+
212
+ ## Rate limiting
213
+
214
+ [`@throttle`](directives.md#throttle) caps how often one client may call a field —
215
+ a login, a search, an expensive mutation:
216
+
217
+ ```graphql
218
+ type Mutation {
219
+ login(email: String!, password: String!): User @throttle(maxAttempts: 5, decayMinutes: 1)
220
+ }
221
+ ```
222
+
223
+ Attempts are counted in [`cache_store`](configuration.md#cache_store), so use a store
224
+ the app servers share (Redis, Solid Cache) in production; `:null_store` counts
225
+ nothing. A client is the signed-in user, else the `:remote_ip` the controller concern
226
+ puts in the context (override `graphql_context` to set another). A refused call never
227
+ reaches `@guard` or the resolver.