lighthouse-graphql 0.4.0 → 0.5.0

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